Edge 模板

本指南介绍在 AdonisJS 应用中使用 Edge 模板的方法。你将学习如何从控制器渲染模板、向模板传递数据、使用布局与组件、使用 Hypermedia 起步套件中的预构建组件,以及调试模板问题。

概述

Edge 是一个面向 Node.js 的服务器端模板引擎,允许你在服务端组合 HTML 标记,并将最终的静态 HTML 发送给浏览器。由于模板完全在服务器端执行,它们可以直接调用框架的核心特性,例如身份验证、授权检查以及国际化系统。

当你在 AdonisJS 中创建 Hypermedia 应用时,Edge 已预先配置好并可直接使用。模板以 .edge 为扩展名存放在 resources/views 目录中,你可以从路由处理器或控制器中,通过 HTTP 上下文的 view 属性来渲染它们。

Edge 在 edgejs.dev 上有完整的文档,详细涵盖了模板语法、组件系统以及所有可用特性。本指南专注于在 AdonisJS 应用中使用 Edge,并介绍 Hypermedia 起步套件内置的预构建组件。

你的第一个模板

让我们创建一个简单的页面,用于展示博客文章列表。这个示例演示了在 AdonisJS 中渲染模板的基本工作流。

  1. 创建模板文件

    使用 Ace 命令生成新模板。

    node ace make:view pages/posts/index
    # CREATE: resources/views/pages/posts/index.edge
  2. 添加模板内容

    打开 resources/views/pages/posts/index.edge 并添加以下内容。

    resources/views/pages/posts/index.edge
    @layout()
      @each(post in posts)
        <div>
          <h2>
            {{ post.title }}
          </h2>
          <div>
            <p>{{{ excerpt(post.content, 280) }}}</p>
          </div>
        </div>
      @end
    @end

    关于这个模板,有几个要点需要理解:

    • @layout() 组件用完整的 HTML 文档结构(包括 <html><head><body> 标签)包裹你的内容。我们会在本指南后面详细介绍布局。

    • @each 标签遍历 posts 数组,并为每篇文章渲染内容。Edge 提供了多个标签用于在模板中编写逻辑,例如 @if@else@elseif。你可以在 Edge 语法参考 中了解所有可用标签。

    • 双花括号 {{ }} 求值并输出一个 JavaScript 表达式。三花括号 {{{ }}} 功能相同,但不会转义 HTML,这在渲染富文本内容时非常有用。

  3. 定义路由

    创建一个路由来处理对文章页面的请求。

    start/routes.ts
    import router from '@adonisjs/core/services/router'
    import { controllers } from '#generated/controllers'
    
    router.get('posts', [controllers.PostsController, 'index'])
  4. 创建控制器

    创建一个控制器,用文章数据渲染模板。

    app/controllers/posts_controller.ts
    import Post from '#models/post'
    import type { HttpContext } from '@adonisjs/core/http'
    
    export default class PostsController {
      async index({ view }: HttpContext) {
        /**
         * 渲染位于 resources/views/pages/posts/index.edge 的模板
         * 第一个参数是模板路径(相对于 resources/views)
         * 第二个参数是模板状态(与模板共享的数据)
         */
        return view.render('pages/posts/index', {
          posts: await Post.all(),
        })
      }
    }
  5. 查看结果

    在浏览器中访问 http://localhost:3333/posts 即可看到渲染后的页面。

你也可以不使用控制器,直接从路由渲染模板。

start/routes.ts
import router from '@adonisjs/core/services/router'

/**
 * router.on().render() 简写会直接渲染一个模板。
 * 第一个参数是模板路径。
 */
router.on('/').render('pages/home')

理解模板状态

你传给 view.render() 的数据对象被称为 模板状态(template state)。该对象中的所有属性在模板中都可作为变量使用。

除了你显式传递的数据外,AdonisJS 还会自动与每个模板共享某些全局对象:

  • 用于访问请求数据的 request 对象
  • 用于检查身份验证状态的 auth 对象
  • Edge 辅助函数,如 excerpt()truncate(),以及路由辅助函数

你可以在 Edge 参考指南 中查看所有可用的辅助函数和全局属性。

模板语法回顾

Edge 结合使用花括号和标签来为模板添加动态行为。以下是最常见的语法模式快速回顾:

输出变量:

{{ post.title }}

输出未转义的 HTML:

{{{ post.content }}}

条件判断:

@if(user)
  <p>欢迎回来,{{ user.name }}</p>
@else
  <p>请登录</p>
@end

循环:

@each(post in posts)
  <h2>{{ post.title }}</h2>
@end

求值 JavaScript 表达式:

{{ post.createdAt.toFormat('dd LLL yyyy') }}
{{ posts.length > 0 ? '有文章' : '暂无文章' }}

有关 Edge 模板语法的完整讲解,包括局部模板、插槽、自定义标签等高级特性,请参阅 Edge 语法参考

使用布局与组件

你在第一个示例中看到的 @layout() 组件用完整的 HTML 文档结构包裹你的页面内容。该组件存放在 resources/views/components/layout.edge,包含标准的 HTML 样板代码:

resources/views/components/layout.edge
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My App</title>
</head>
<body>
  {{{ await $slots.main() }}}
</body>
</html>

$slots.main() 方法调用会渲染你放置在页面模板中 @layout() 开始和结束标签之间的所有内容。这就是 Edge 的插槽(slots)特性,它允许组件接收来自其使用者(consumer)的内容。

创建组件

Edge 中的组件是可复用的模板片段,存放在 resources/views/components 目录中。该目录中的任何模板文件都可作为 Edge 标签使用。

例如,如果你在 resources/views/components/card.edge 创建了一个文件:

resources/views/components/card.edge
<div class="card">
  {{{ await $slots.main() }}}
</div>

你就可以在模板中这样使用它:

@card()
  <h2>卡片标题</h2>
  <p>卡片内容</p>
@end

组件可以接收 props(参数),并支持多个命名插槽以进行更复杂的组合。有关构建和使用组件的完整指南,请参阅 Edge 组件指南

起步套件组件

Hypermedia 起步套件包含一组无样式的组件,用于构建表单和常见 UI 模式。每个组件最多渲染一个 HTML 元素,并将未知的 props 透传为 HTML 属性,从而允许你直接应用 class 和其他属性。

Layout

渲染包含 head 和 body 元素的 HTML 文档。

  • Props:无
  • Slotsmain(默认)
@layout()
  <main>页面内容放在这里</main>
@end

Form

渲染一个 HTML 表单元素,并自动注入 CSRF token。

PropTypeDescription
actionstring表单 action 的 URL
methodstringHTTP 方法。支持通过方法伪装使用 PUTPATCHDELETE
routestring根据命名路由计算 action 的 URL
routeParamsarray命名路由所需的参数
routeOptionsobject用于生成 URL 的额外选项(例如查询字符串)
@form({ route: 'posts.store', method: 'POST' })
  {{-- 表单字段 --}}
@end

@form({ route: 'posts.update', method: 'PUT', routeParams: [post.id] })
  {{-- 表单字段 --}}
@end

Field 组件

表单字段组件协同工作,用于创建带有标签和验证错误显示的、可访问的表单输入。

field.root

建立子字段组件上下文的容器。

PropTypeDescription
namestring字段名称,用于查找错误信息
idstring元素 ID,用于将标签与输入关联起来

field.label

渲染与字段关联的 label 元素。

PropTypeDescription
textstring标签文本(替代使用插槽的方式)

field.error

显示该字段的验证错误。会根据字段名称自动查找错误。

@field.root({ name: 'email' })
  @!field.label({ text: '邮箱地址' })
  @!input.control({ type: 'email', autocomplete: 'email' })
  @!field.error()
@end

Input control

渲染一个 input 元素。必须是 @field.root 的子级。它将所有 props 作为 HTML 属性透传给 input 元素。

@field.root({ name: 'username' })
  @!field.label({ text: '用户名' })
  @!input.control({ type: 'text', minlength: '3', maxlength: '20' })
  @!field.error()
@end

Select control

渲染一个带选项的 select 元素。必须是 @field.root 的子级。

PropTypeDescription
optionsarray由包含 namevalue 属性的对象组成的数组
@field.root({ name: 'country' })
  @!field.label({ text: '国家' })
  @!select.control({
    options: countries.map((country) => ({
      name: country.name,
      value: country.code
    }))
  })
  @!field.error()
@end

Textarea control

渲染一个 textarea 元素。必须是 @field.root 的子级。它将所有 props 作为 HTML 属性透传给 textarea 元素。

@field.root({ name: 'bio' })
  @!field.label({ text: '个人简介' })
  @!textarea.control({ rows: '4' })
  @!field.error()
@end

Checkbox 组件

Checkbox 组件创建共享命名的复选框输入,便于表单提交。

checkbox.group

建立子复选框共享名称的容器。

PropTypeDescription
namestring组中所有复选框的共享名称

checkbox.control

渲染一个复选框输入。必须同时嵌套在 @checkbox.group@field.root 中。它将所有 props 作为 HTML 属性透传。使用 value 设置复选框的值。

@checkbox.group({ name: 'services' })
  @field.root({ id: 'design' })
    @!checkbox.control({ value: 'design' })
    @!field.label({ text: '设计' })
  @end

  @field.root({ id: 'development' })
    @!checkbox.control({ value: 'development' })
    @!field.label({ text: '开发' })
  @end

  @field.root({ id: 'marketing' })
    @!checkbox.control({ value: 'marketing' })
    @!field.label({ text: '市场' })
  @end
@end

Radio 组件

Radio 组件在组内创建互斥的选项。

radio.group

建立子单选按钮共享名称的容器。

PropTypeDescription
namestring组中所有单选按钮的共享名称

radio.control

渲染一个单选输入。必须同时嵌套在 @radio.group@field.root 中。它将所有 props 作为 HTML 属性透传。使用 value 设置单选按钮的值。

@radio.group({ name: 'payment_plan' })
  @field.root({ id: 'free' })
    @!radio.control({ value: 'free' })
    @!field.label({ text: '免费' })
  @end

  @field.root({ id: 'pro' })
    @!radio.control({ value: 'pro' })
    @!field.label({ text: '专业版 $29/月' })
  @end

  @field.root({ id: 'enterprise' })
    @!radio.control({ value: 'enterprise' })
    @!field.label({ text: '定制价格' })
  @end
@end

Alert 组件

Alert 组件用于显示通知消息,可选择自动消失行为。

alert.root

建立 alert 标题和描述上下文的容器。

PropTypeDescription
variantstringAlert 变体(例如 destructivesuccess
autoDismissboolean是否自动消失

alert.title

渲染 alert 标题。

PropTypeDescription
textstring标题文本(替代使用插槽的方式)

alert.description

渲染 alert 正文文本。

PropTypeDescription
textstring描述文本(替代使用插槽的方式)
@alert.root({ variant: 'destructive', autoDismiss: true })
  @!alert.title({ text: '未授权' })
  @!alert.description({ text: '你无权访问此页面' })
@end

Button

渲染一个 button 元素。

PropTypeDescription
textstring按钮文本(替代使用插槽的方式)
@!button({ text: '注册', type: 'submit' })

@button({ type: 'button', class: 'btn-secondary' })
  <span>取消</span>
@end

渲染一个 anchor 元素,并基于路由生成 URL。

PropTypeDescription
textstring链接文本(替代使用插槽的方式)
routestring根据命名路由计算 href
routeParamsarray命名路由所需的参数
routeOptionsobject用于生成 URL 的额外选项
hrefstring直接 URL(替代 route 使用)
@!link({ route: 'posts.show', routeParams: [post.id], text: '查看文章' })

@link({ route: 'posts.edit', routeParams: [post.id] })
  <span>编辑</span>
  <svg>{{-- 图标 --}}</svg>
@end

Avatar

渲染用户头像的图片或首字母缩写。

PropTypeDescription
srcstring头像图片 URL(渲染为 img 元素)
initialsstring兜底首字母(渲染为 span 元素)
{{-- 使用图片 --}}
@!avatar({ src: user.avatarUrl, alt: user.name })

{{-- 使用首字母 --}}
@!avatar({ initials: user.initials })

调试模板

在处理模板时,你可能需要检查模板中可用的数据,或者排查某些值未按预期显示的原因。Edge 为此提供了 @dump 标签。

@dump 标签会美化打印一个变量的值,使检查数据结构变得轻松:

@dump(posts)

要查看整个模板状态(模板中所有可用的变量),使用:

@dump(state)

输出会显示在你渲染的 HTML 中,展示数据的结构和值。在开发过程中,模板在发生更改时会自动重新加载,因此你无需重启服务器即可在浏览器中立即看到更新。

配置

在 Hypermedia 应用中,Edge 已预先配置好,开箱即用。但是,如果你需要注册自定义辅助函数、标签或插件,可以通过创建一个预加载(preload)文件来自定义 Edge。

为 Edge 配置创建一个预加载文件:

node ace make:preload view

这会创建一个预加载文件,你可以在应用启动前在其中自定义 Edge。在该文件中,你可以注册 Edge 全局对象、插件和自定义标签。

如果你需要自定义模板存放的目录,可以修改 adonisrc.ts 文件中的 directories 选项。有关配置选项的更多详情,请参阅 AdonisRC 参考指南

另请参阅