@hx-midway/edge
v1.0.6
Published
midway edge.js 模版渲染器
Downloads
2
Maintainers
Readme
基于 edge.js
的 midway
模版引擎
介绍
用 midway 做后端模板渲染, 现在官方提供了ejs
和 Nunjucks
两种模板引擎, 这两种模板引擎的对比使用中均有遇到过不同程度的问题,使用体验并不是很好;所以最后决定基于edge
引擎做支撑,开发midway 模板渲染组件;Edge 是一个用于 Node.js 的模板引擎。他可以呈现任何文本格式, 无论是 HTML、Markdown、还是其他文本格式。
模板引擎对比
- Edge 和 ejs 二者均支持 javascript 语法,但是相比于 ejs, edge 提供了更为灵活的 js 语法支撑,使用体验优于 ejs ; 而与 Edge 相比 Nunjucks 语法更趋近于 jinjia2, 这里 python 同学也许会很开心, 但是对于用 js 做全栈开发来说其语法有些地方书写起来同样很别扭;
<!-- ejs -->
<% if(user) { %>
<h1> <%= user.name %> </h1>
<% } %>
<ul>
<% users.forEach(function(user){ %>
<li> <%= user.name %> </li>
<% }) %>
</ul>
<!-- Nunjucks -->
{% if user %}
<h1> {{ suer.name }} </h1>
{% endif %}
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>
<!-- Edge -->
@if(user)
<h1>{{user.name}}</h1>
@end
<!-- Edge 同时也支持可选链操作 -->
<h1>{{ user?.name }}</h1>
<ul>
@each(user in users)
<li>{{user?.name||'默认值'}}</li>
@end
</ul>
<!-- Edge 同时也支持 同、异步方法 -->
<h1>{{ getUserName() }}</h1>
<h1>{{ await user.getAge() }}</h1>
模板语法
使用 Edge 不需要了解过多的新概念, 而是依赖于 Javascript 的语言功能。
Edge 的语法主要有两个基本单元:
* 大括号 `{{}} `用于 计算表达式并显示输出值;
* tags 标签 `@section('')` 用于 向模板引擎添加新功能,并且还可以用于添加自定义标签。
大括号
Edge 使用 双大括号 方法来执行 javascript 表达式,您可以在大括号内使用任何有效的 javascript 表达式。
{{ user?.name ||'默认用户'}}
{{user?.name?.toUpperCase() }}
{{ (11+6)*3/5 }}
{{ (await getUser())?.name }}
同时双括号会表达式的输出进行转义,以便于保护模板免受 XSS 攻击。
{{ '<script>alert('Hello')</script>'}}
<!-- 输出内容为: -->
<script> alert(`foo`) </script>
当内容受您信任,不希望对表达式进行转义时, 可以使用三个大括号:
{{{ '<script> alert(`Hello`) </script>' }}}
<!-- 输出内容为: -->
<script> alert(`Hello`) </script>
忽略大括号
您可以在括号前面添加 @
前缀来指示 Edge 忽略大括号。
Hello @{{username}}
<!-- 输出内容为: -->
Hello {{username}}
Tags 标签
标签是以 @
开头的表达式,用于向模板添加功能:
注意:标签需出现在行首, 且不可与其他内容混用。
{{-- ✅ 正确 --}}
@if(user)
@end
{{-- ❌ 错误 --}}
Hello @if(user)
@end
标签有不同的划分,用以满足不同的模板需求:
块级标签
块级标签你是可以插入内容的标签, 且必须与闭合标签
@end
成对出现, 例如:{{ -- if 是块级标签 -- }} @if(user) @end {{ -- section 是块级标签 -- }} @section('body') @end
内嵌标签
内嵌标签不可插入内容,且是自闭合的, 无需额外使用闭合标签
@end
, 例如:{{ -- include 是内嵌标签 -- }} @include('components/button') {{ -- layout 是内嵌标签 -- }} @layout('layouts/base')
子封闭块级标签
有时候块级标签无需插入内容, 因此为了简化闭合标签的写法, 所以可以进行自闭合,常用的有
@component
{{ -- 插入内容的 button 组件 -- }} @component('components/button',{ type: 'submit' }) <i class="iconfont"></i> @end {{ -- 无需插入内容 -- }} @!component('components/button',{ type: 'submit' })
注释
{{ -- 这是一个注释 -- }} <!-- 这是一个注释 -->
末尾换行
<p>Hello @if(user)~ {{ user?.name }} @endif~ </p> {{ -- 执行后输出如下,若移除 ~ 责会在末尾产出空白行 -- }} <p>Hello 张三</p>
数据渲染
await this.ctx.render('index', {
name: "张三",
age: 66
})
- 全局变量
可通过 global
import { EdgeView } from "@hx-midway/edge"
class DemoController {
@Inject()
view: EdgeView
async demo(){
// 全局共享 tdk 数据
this.view.global('tdk', {
title: "标题",
keywords: "关键词",
description: "简介"
})
// 全局方法 换行符替换为 br 标签
this.view.global('nl2br', (text)=>{
return text?.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br />$2')
})
}
}
<p> {{{ nl2br(test.txt) }}} </p>
<meta name="keywords" content="{{tdk?.keywords}}">
<meta name="description" content="{{tdk?.description}}">
<title>{{tdk?.title}}</title>
内联变量
可使用
@set
标签来定义内联变量@set('title', '这是测试标题') <title>{{title}}</title>
内联变量作用域类似于
let
, 若是在标签内定义, 则标签外无效@each(item in list) @set('price', item?.price*item?.discount||0) <span>{{price||item.price}}</span> @end
条件渲染
您可是使用 @if
标签来写条件语句, 语法类似于 javascript 的 if/else 语句:
@if(user)
<p>{{user?.name}}</p>
@end
@if(user?.fullName)
<p>Hello {{user.fullName}}</p>
@elseif(user?.firstName)
<p>Hello {{user.firstName}}</p>
@else
<p>Hello 张三</p>
@end
同时还有 @unless
标签:
@unless(userInfo?.status)
<p>您的账户状态异常, 请联系客服人员进行处理</p>
@end
循环
@each
标签可用于数据循环,原理类似于 Javascript 的 forEach
@each(user in users)
<li> {{ user?.name }} </li>
@end
{{ -- 使用索引 -- }}
@each( (user, index) in users)
<li> {{ user?.name }} {{index+1}}</li>
@end
循环同样适用于对象的遍历:
@each((name, age) in user)
<li> 姓名:{{ name }} 年龄: {{ age }} </li>
@end
标签呢也可以执行同异步方法:
this.ctx.render('index', {
users: [
{
name: "张三",
publish: async ()=> [{title: "测试1"}]
},
{
name: "李四",
publish: async ()=> [{title: "测试1"},{title: "测试2"}]
}
]
})
@each(user in users)
<h2>{{user.name}}</h2>
<ul>
@each(item in await user.publish())
<li>{{item.name}}</li>
@end
</ul>
@end
部分
基本示例
有如下文件结构:
├── view
│ ├── components
│ │ ├── footer.index
│ │ ├── header.index
│ │ └── sidebar.index
│ └── index.heml
index.html
中可这样使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
@include('components/header')
<section>
@include('components/sidebar')
<main></main>
</section>
@include('components/footer')
</body>
</html>
布局
有以下文件结构
├── views
│ ├── components
│ │ ├── header.index
│ │ └── footer.html
│ ├── layouts
│ │ └── base.html
│ └── index.html
layouts/base.html` 中如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
@include('components/header')
@!section('body')
@include('components/footer')
</body>
</html>
index.html
中如下:
@layout('layouts/base')
@section('body')
<p>Hello 张三</p>
@end
组件
创建组件:
{{ -- components/button.html -- }}
<button type="{{ type }}">
{{ text }}
</button>
{{ -- demo.html -- }}
@!component('components/button', {
type: 'primary',
text: '确定'
})
@component
标签共接收两个参数, 第一个是组件路径, 第二个则是要传递的 props
注意:组件无法访问父模板的状态, 即 父模板 使用@set 标签创建的变量, 子组件无法获取并修改
$props
同时你也可以使用 $props
来实现传递的属性转化:
并且提供了 serializeExcept serializeOnly serialize
等方法:
{{ -- components/button.html -- }}
<button {{ $props.serializeExcept(['text']) }}>
{{ text }}
</button>
{{-- demo.html --}}
@!component('components/button', {
type: 'primary',
text: '确定',
class: 'flex items-center justify-center bg-primary rounded-2'
})
插槽 $slots
{{ -- components/button.html -- }}
<button type="{{ type }}">
{{{ await $slots.main() }}}
</button>
{{-- demo.html --}}
@component('components/button')
<i class="fa-icon-lock" />
<span>登录</span>
@end
命名插槽:
{{ -- components/dialog.html -- }}
<div>
<header>
{{{ await $slots.header() }}}
</header>
<main>
{{{ await $slots.main() }}}
</main>
<footer>
@if($slots.footer)
{{{ await $slots.footer() }}}
@else
Default footer
@end
</footer>
</div>
{{ -- index.html -- }}
@component('components/dialog')
@slot('header')
<h1> 提示 </h1>
@end
@slot('main')
<div>
<p>此操作不可逆, 是否继续?</p>
</div>
@end
@slot('actions')
<div class="flex">
<button>取消</button>
<button>确定</button>
</div>
@end
@end
插槽也可提供数据输出:
{{ -- components/item.html -- }}
<div>
{{{ await $slots.main({ name, age }) }}}
</div>
{{ -- index.html -- }}
@each(user in users)
@component('components/item')
@slot('main', scope)
<h2>{{scope?.name}}</h2>
<p>{{scope?.age}}</p>
@end
@end
@end
@inject
变量注入, 组件允许使用@inject 将变量共享给组件数
{{ -- components/button -- }}
@set('conuter', {value:0})
@inject({counter})
<button>
{{{ await $slots.main() }}}
</button>
{{ -- index.html -- }}
@component('components/button')
<p> {{ $context.counter.value }}</p>
<!-- 修改变量 -->
@set($context, 'counter.value', $context.counter.value+1)
<p>{{ $context.counter.value }}</p>
@end
组件简化写法:
{{ -- components/button.html -- }}
<button type="{{ type }}">
{{ text }}
</button>
{{ -- demo.html -- }}
@!button( {
type: 'primary',
text: '确定'
})