feat: 重构 laytpl,增强对更多复杂模板结构的解析能力 (#2577)

* feat: 重构 laytpl,增强对更多复杂模板结构的解析能力

* Squashed commit of the following:

commit 6884f80378
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Wed Mar 19 14:45:58 2025 +0800

    release: v2.10.1

commit 8d643ad6dc
Merge: 5521e48c 213fe5a2
Author: corededitor <107152508+corededitor@users.noreply.github.com>
Date:   Wed Mar 19 14:24:50 2025 +0800

    feat: Merge pull request #2566 from layui/feat/component

    feat: 优化 component, tabs 若干功能

commit 213fe5a209
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Wed Mar 19 14:15:39 2025 +0800

    docs: 添加 component 文档中实验性选项标记

commit 5521e48c05
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Wed Mar 19 14:10:46 2025 +0800

    fix: 修复 `body` 初始 `line-height` 无效的问题 (#2569)

commit 8c7cf0f606
Author: 青崖 <33601030+bxjt123@users.noreply.github.com>
Date:   Wed Mar 19 14:10:08 2025 +0800

    优化 checkbox 标签风格选中且禁用时的显示 (#2563)

commit 23b21254d4
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Wed Mar 19 14:04:44 2025 +0800

    docs: Squashed commit of the following:

    commit 95a0503f41
    Merge: e6eb86ba 87ba4c43
    Author: 贤心 <3277200+sentsim@users.noreply.github.com>
    Date:   Wed Mar 19 14:03:13 2025 +0800

        Merge branch 'main' into 2.x

    commit e6eb86bacb
    Author: morning-star <26325820+Sight-wcg@users.noreply.github.com>
    Date:   Wed Mar 19 14:02:05 2025 +0800

        docs(slider): 修正错别字 (#2578)

    commit 46f7a9783e
    Merge: df1fc4f4 c204590a
    Author: 贤心 <3277200+sentsim@users.noreply.github.com>
    Date:   Fri Mar 14 19:07:50 2025 +0800

        Merge branch 'main' into 2.x

    commit df1fc4f419
    Author: itletu <itletu@163.com>
    Date:   Mon Mar 10 13:54:06 2025 +0800

        docs: 更正 class 公共类文档错误 (#2544)

        | layui-border-box | 设置元素及其所有子元素均为 `box-sizing: border-box` 模型的容器 |

commit 87ba4c4394
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Wed Mar 19 13:41:33 2025 +0800

    docs(version): 优化 2.9.x 锚点

commit a0f533f0fd
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Wed Mar 19 13:37:29 2025 +0800

    docs: 修复 tabs 文档示例异常问题

commit 0f0584e2ed
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Wed Mar 19 13:35:00 2025 +0800

    docs: 修复 tabs 文档中自定义事件示例重新点击 Preview 失效的问题

commit 172957d243
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Wed Mar 19 13:29:09 2025 +0800

    docs: 优化文档中的用词细节 (#2571)

commit 094be4ddcc
Author: letianpailove <113023596+letianpailove@users.noreply.github.com>
Date:   Wed Mar 19 13:28:46 2025 +0800

    fix: 更正 class 公共类文档错误 (#2562)

commit 53ded26cb9
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Sun Mar 16 00:15:25 2025 +0800

    fix: 优化 tabs 重载时未按照传入的 closable 正确渲染可关闭状态

commit bd892bf87e
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Sun Mar 16 00:15:17 2025 +0800

    feat(component): 新增 cache 原型方法,用于元素缓存操作

commit 6ccc5a453d
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Sun Mar 16 00:14:18 2025 +0800

    fix(component): 优化元素 lay-options 属性上的配置在重载时的优先级

commit 79b0a56f50
Author: 贤心 <3277200+sentsim@users.noreply.github.com>
Date:   Sun Mar 16 00:13:55 2025 +0800

    fix(component): 修复 reload 时传入的选项未正确合并的问题

* refactor(laytpl): 优化代码细节

* docs: 重写 laytpl 模块文档
This commit is contained in:
贤心
2025-03-27 23:55:04 +08:00
committed by GitHub
parent b4fbb28a04
commit 169f6ff9b8
47 changed files with 1489 additions and 656 deletions

View File

@@ -1,74 +1,45 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>视图模板引擎 - layui</title>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>模板引擎 - Layui</title>
<link rel="stylesheet" href="../src/css/layui.css">
<style>
.laytpl-demo{border: 1px solid #eee;}
.laytpl-demo:first-child{border-right: none;}
.laytpl-demo>textarea{position: relative; display: block; width:100%; height: 300px; padding: 11px; border: 0; box-sizing: border-box; resize: none; background-color: #fff; font-family: Courier New; font-size: 13px;}
.laytpl-demo>div:first-child{height: 32px; line-height: 32px; padding: 6px 11px; border-bottom: 1px solid #eee; background-color: #F8F9FA;}
.laytpl-demo .layui-tabs{top: -1px;}
<link rel="stylesheet" href="../src/css/layui.css">
<style>
.laytpl-demo{border: 1px solid #EBEBEB;}
.laytpl-demo>textarea{position: relative; display: block; width:100%; height: 300px; padding: 11px; border: 0; box-sizing: border-box; resize: none; background-color: #fff; font-family: Courier New; font-size: 13px;}
.laytpl-demo>div:first-child{height: 32px; line-height: 32px; padding: 6px 11px; border-bottom: 1px solid #EBEBEB; background-color: #F8F9FA;}
</style>
</head>
<body>
<div>
<div class="layui-row">
<div class="layui-col-xs6 laytpl-demo">
<div>模板</div>
<textarea id="demoTPL1"><h1>{{ d.title }}</h1>
<p>转义输出(HTML){{ d.desc }}</p>
<p>转义输出(HTML){{= d.desc }}</p>
<p>原始输出(HTML){{- d.desc }}</p>
{{#}}
<div class="layui-section">
<hr>
<ul>
{{# var str = "a b c";
layui.each(d.items, function(index, item){ }}
<li class="{{ index > 0 ? 'list' : '' }}">
<strong>{{ item.title }}</strong>
{{# if(item.content){ }}
<span>{{ item.content }}</span>
{{# } }}
<span>{{ item.time || '' }}</span>
{{ str }}
</li>
{{# }); if(d.items.length === 0){ }}
无数据
{{# } }}
</ul>
<hr>
</div>
<div>
{{ d.content || '' }}
{{ }} {{}}
{{ }}
\a
'12'"""""
"哈''哈"
</div>
<p>渲染时间:{{ layui.util.toDateString(new Date()) }}</p></textarea>
</div>
<div class="layui-col-xs6 laytpl-demo">
#ID-tpl-view-body {
height: calc(100vh - 430px); overflow: auto; clear: both;
}
#ID-tpl-view-body > div {
display: none;
}
.laytpl-demo pre {
padding: 16px; background-color: #20222A; color: #F8F9FA; font-family: 'Courier New',Consolas, monospace;
}
</style>
</head>
<body>
<div class="layui-padding-3">
<div class="layui-row">
<div class="layui-col-xs3">数据</div>
<div class="layui-col-xs9" style="text-align: right">
<button class="layui-btn layui-btn-sm layui-btn-primary" lay-on="createData">生成数据</button>
<div class="layui-col-xs6 laytpl-demo">
<div>
<a href="javascript:;" id="ID-tpl-src-title">
<cite><strong>模板</strong></cite>
<i class="layui-icon layui-icon-down layui-font-12"></i>
</a>
</div>
<textarea id="ID-tpl-src"></textarea>
</div>
</div>
<textarea id="demoData1">
<div class="layui-col-xs6 laytpl-demo">
<div class="layui-row">
<div><strong>数据</strong></div>
</div>
<textarea id="ID-tpl-data">
{
"title": "标题",
"desc": "<a href=\"\" style=\"color:blue;\">一段描述</a>",
@@ -91,156 +62,316 @@
{"title": "list 3"}
]
}</textarea>
</div>
<div class="layui-col-xs12 laytpl-demo">
<div class="layui-row">
<div class="layui-col-xs4">视图</div>
<div class="layui-col-xs4" style="text-align: center">
<button class="layui-btn layui-btn-sm layui-btn-primary" lay-on="test1">性能测试</button>
</div>
<div class="layui-col-xs4" style="text-align: right">
<span id="demoViewTime"></span>
<div class="layui-col-xs12 laytpl-demo" style="border-top: none;">
<div class="layui-row">
<div class="layui-col-xs4 layui-tabs" id="ID-tpl-view-header">
<ul class="layui-tabs-header">
<li><strong>渲染结果</strong></li>
<li><strong>源码</strong></li>
</ul>
</div>
<div class="layui-col-xs4" style="text-align: center">
<button class="layui-btn layui-btn-sm layui-btn-border" lay-on="test">性能测试</button>
</div>
<div class="layui-col-xs4" style="text-align: right">
<span class="layui-badge" id="ID-tpl-view-time"></span>
</div>
</div>
<div id="ID-tpl-view-body">
<div class="layui-show layui-padding-3 layui-text" id="ID-tpl-view"></div>
<div><pre id="ID-tpl-view-code"></pre></div>
</div>
</div>
</div>
<div class="layui-padding-sm" id="demoView1"></div>
</div>
</div>
<script type="type/html" template id="demoTplCommon">
公共模板 - {{ d.title }}
</script>
<script type="type/html" template id="demoTplList">
{{# if(d.items && d.items.length > 0){ }}
<ul>
{{# layui.each(d.items, function(index, item){ }}
<li><strong>{{ item.title }}</strong>{{ laytpl.include('demoTplList', {items: item.child}) }}</li>
{{# }); }}
</ul>
{{# } }}
</script>
</div>
<script type="type/html" template id="laytplTestTpl">
{{# for(var i = 0; i < d.items.length; i++){ }}
{{= d.items[i].index }}Name: {{- d.items[i].name }} Number: {{= d.items[i].number }}
{{# } }}
</script>
<script type="text/html" id="ID-tpl-template-modern">
<h2>
{{= d.title }} - {{= d.title ? '#' : '' }}
{{ if(true){ }}AAAA{{='A'}}{{ } }}
</h2>
{{- include('ID-tpl-template-common', {title: '头部'}) }}
<hr>
<p>转义输出{{= d.desc }}</p>
<p>原文输出{{- d.desc }}</p>
{{# 这是一段注释仅在模板中显示不在视图中输出 }}
{{!
这是一段不进行模板解析的区域可显示原始标签
{{ let a = 0; }}{{= escape }}{{- source }}{{# comments }}&#123;&#123;! ignore !&#125;&#125;
!}}
{{# 空主体测试 }}
{{}} {{ }} {{ }} {{= }} {{=}} {{= }}
<div>
{{- !0 ? '<br>循环输出:' : '' }}
<hr>
<ul>
{{
var str = "一级列表 a \\ b c";
d.items.forEach(function(value, index) {
}}
<li class="{{= index > 0 ? 'list' : '' }}">
<strong>{{= value.title }}</strong>
{{ if(value.content){ }}
<span>{{= value.content }}</span>
{{ } }}
<span>{{= value.time || '' }}</span>
{{= str }}
{{- include('ID-tpl-template-list', { items: value.child }) }}
</li>
{{ }); }}
{{ if(d.items.length === 0){ }}
无数据
{{ } }}
</ul>
<hr>
</div>
<script src="../src/layui.js"></script>
<script>
layui.use(['laytpl', 'util'], function(){
var laytpl = layui.laytpl;
var util = layui.util;
var $ = layui.$;
// 获取模板和数据
var get = function(type){
return {
template: $('#demoTPL1').val(), // 获取模板
data: function(){ // 获取数据
try {
return JSON.parse($('#demoData1').val());
} catch(e){
$('#demoView1').html(e);
}
}()
};
};
var data = get();
// 耗时计算
var startTime = new Date().getTime(), timer = function(startTime, title){
var endTime = new Date().getTime();
$('#demoViewTime').html((title || '模板解析耗时:')+ (endTime - startTime) + 'ms');
};
<div>
{{= d.content || '' }}
\反斜杠 | '单引号' "双引号" ""''"" | "左双右单' | '左单右双"
</div>
// 全局设置
/*laytpl.config({
open: '<%',
close: '%>'
});*/
<p>渲染时间{{= layui.util.toDateString(new Date()) }}</p>
</script>
// 渲染模板
var thisTpl = laytpl(data.template);
<script type="text/html" id="ID-tpl-template-common">
公共模板 - {{= d.title }}
</script>
// 执行渲染
thisTpl.render(data.data, function(view){
timer(startTime);
$('#demoView1').html(view);
});
// 编辑
$('.laytpl-demo textarea').on('input', function(){
var data = get();
if(!data.data) return;
// 计算模板渲染耗时
var startTime = new Date().getTime();
// 若模板有变化,则重新解析模板;若模板没变,数据有变化,则从模板缓存中直接渲染(效率大增)
if(this.id === 'demoTPL1'){
thisTpl.parse(data.template, data.data); // 解析模板
}
// 执行渲染
thisTpl.render(data.data, function(view){
timer(startTime);
$('#demoView1').html(view);
});
});
// 事件
util.on({
// 性能测试
test1: function(){
var dataLen = 1000 // 数据量
var renderTimes = 1000; // 渲染次数
// 初始化数据
var data = {
title: '性能测试',
items: function(items){
for(var i = 0; i < dataLen; i++){
items.push({
index: i
,name: '<strong style="color: red;">张三</strong>'
,number: 100+i
<script type="text/html" id="ID-tpl-template-list">
{{ if(d.items && d.items.length > 0){ }}
<ul>
{{ layui.each(d.items, function(index, item){ }}
<li>
<strong>{{= item.title }}</strong>
{{- include('ID-tpl-template-list', {items: item.child}) }}
</li>
{{ }); }}
</ul>
{{ } }}
</script>
<script type="text/html" id="ID-tpl-template-test">
{{ for (var i = 0; i < d.items.length; i++) { }}
{{= d.items[i].index }} Name: {{- d.items[i].name }} Number: {{= d.items[i].number }}
{{ } }}
</script>
<!-- 旧版本模板 -->
<script type="text/html" id="ID-tpl-template-legacy">
<h2>
{{= d.title }} - {{= d.title ? '#' : '' }}
{{# if(true){ }}AAAA{{='A'}}{{# } }}
</h2>
<hr>
<p>转义输出{{ d.desc }}</p>
<p>转义输出{{= d.desc }}</p>
<p>原文输出{{- d.desc }}</p>
{{}} {{ }} {{ }} {{= }} {{=}} {{= }}
<div>
<ul>
{{# var str = "a b c"; }}
{{# layui.each(d.items, function(index, item) { }}
<li class="{{= index > 0 ? 'list' : '' }}">
<strong>{{= item.title }}</strong>
{{# if(item.content){ }}
<span>{{= item.content }}</span>
{{# } }}
<span>{{= item.time || '' }}</span>
{{ str }}
</li>
{{# }); }}
{{# if (d.items.length === 0) { }}
无数据
{{# } }}
</ul>
<hr>
</div>
<div>
{{= d.content || '' }}
\反斜杠 | '单引号' "双引号" ""''"" | "左双右单' | '左单右双"
</div>
<p>渲染时间{{ layui.util.toDateString(new Date()) }}</p>
</script>
</div>
<script src="../src/layui.js"></script>
<script>
layui.use(['laytpl', 'util', 'tabs', 'dropdown'], function() {
var laytpl = layui.laytpl;
var util = layui.util;
var tabs = layui.tabs;
var dropdown = layui.dropdown;
var $ = layui.$;
// 默认设置
laytpl.config({
tagStyle: 'modern' // 初始采用新版标签风格
})
// 获取模板和数据
var getData = function(type) {
return {
template: $('#ID-tpl-src').val(), // 获取模板
data: function(){ // 获取数据
try {
return JSON.parse($('#ID-tpl-data').val());
} catch(e) {
$('#ID-tpl-view').html(e);
}
}()
};
};
// 视图渲染
var renderView = function(html, startTime) {
timer(startTime);
$('#ID-tpl-view').html(html);
$('#ID-tpl-view-code').html(util.escape(html));
};
// 生成模板
var createTemplate = function(opts) {
opts = $.extend({
tagStyle: 'modern'
}, opts);
// 初始化模板
var elem = $('#ID-tpl-template-'+ opts.tagStyle);
$('#ID-tpl-src').val(elem.html());
return opts;
};
var tplConfig = createTemplate();
var data = getData();
// 耗时计算
var timer = function(startTime, title) {
var endTime = new Date();
$('#ID-tpl-view-time').html((title || '模板解析耗时:')+ (endTime - startTime) + 'ms');
};
var startTime = new Date();
// 创建一个模板实例
var templateInst = laytpl(data.template, {
condense: false, // 不处理连续空白符,即保留模板原始结构
tagStyle: tplConfig.tagStyle
});
// 初始渲染
templateInst.render(data.data, function(html) {
renderView(html, startTime);
});
// 编辑
$('.laytpl-demo textarea').on('input', function() {
var data = getData();
var startTime = new Date();
// 若模板有变化,则重新编译模板
if (this.id === 'ID-tpl-src') {
templateInst.compile(data.template);
}
// 若模板没变,数据有变化,则从模板缓存中直接渲染数据(效率大增)
templateInst.render(data.data, function(html) {
renderView(html, startTime);
});
});
// 事件
util.on({
// 性能测试
test: function() {
var dataLen = 1000 // 数据量
var renderTimes = 1000; // 渲染次数
// 初始化数据
var data = {
title: '性能测试',
items: function(items) {
for (var i = 0; i < dataLen; i++) {
items.push({
index: i,
name: '<strong style="color: red;">张三</strong>',
number: 100+i
});
}
return items;
}([])
};
// 模板
var startTime = new Date();
for (var j = 0; j < renderTimes; j++) {
var template = document.getElementById('ID-tpl-template-test').innerHTML;
var html = laytpl(template).render(data);
}
renderView(html, startTime);
}
});
// 局部自定义标签符
laytpl(`
<% var job = ["前端工程师"]; %>
<%= d.name %>是一名<%= job[d.index] %>。
`, {
open: '<%',
close: '%>'
}).render({
name: '张三',
index: 0
}, function(str) {
console.log(str); // 张三是一名前端工程师。
});
// 视图结果 tabs
tabs.render({
elem: '#ID-tpl-view-header',
body: ['#ID-tpl-view-body', '>div']
});
// 切换模板
dropdown.render({
elem: '#ID-tpl-src-title',
data: [{
title: '新版本模板',
tagStyle: 'modern'
}, {
title: '旧版本模板',
tagStyle: 'legacy'
}],
click: function(obj){
createTemplate({
tagStyle: obj.tagStyle
});
this.elem.children('cite').html(obj.title);
// 同步设置标签风格
templateInst.config.tagStyle = obj.tagStyle;
var data = getData();
var startTime = new Date();
// 重新渲染
templateInst.compile(data.template).render(data.data, function(html) {
renderView(html, startTime);
});
}
return items;
}([])
};
// 模板
var startTime = new Date();
for(var j = 0; j < renderTimes; j++){
var template = document.getElementById('laytplTestTpl').innerHTML;
var html = laytpl(template).render(data);
}
timer(startTime, '本次测试耗时:');
$('#demoView1').html(html);
}
});
// 自定义标签符
laytpl(`
<%# var job = ["前端工程师"]; %>
<%= d.name %>是一名<%= job[d.type] %>。
`, {
open: '<%',
close: '%>'
}).render({
name: '张三',
type: 0
}, function(str){
console.log(str); // 张三是一名前端工程师。
});
});
</script>
</body>
})
});
</script>
</body>
</html>