diff --git a/docs/laytpl/detail/demo.md b/docs/laytpl/detail/demo.md index bb77a7a0..f0027251 100644 --- a/docs/laytpl/detail/demo.md +++ b/docs/laytpl/detail/demo.md @@ -1,59 +1,59 @@ -
+
   
+!}}
 
diff --git a/docs/laytpl/detail/options.md b/docs/laytpl/detail/options.md index b26499cc..4f689ea3 100644 --- a/docs/laytpl/detail/options.md +++ b/docs/laytpl/detail/options.md @@ -7,72 +7,85 @@ 标签 描述 - + 类型 + 默认值 + {{! -{{= }} +open - -转义输出。若字段存在 HTML,将进行转义。 -``` -

{{= d.title }}

-``` +用于设置起始界定符 + + +string + + +`{{` -{{- }} 2.8+ +close - -原始输出。若字段存在 HTML,将正常渲染。 -``` -
{{- d.content }}
-``` +用于设置结束界定符 -该语句一般在需要正常渲染 HTML 时用到,但若字段存在 script 等标签,为防止 xss 问题,可采用 `{{= }}` 进行转义输出。 + +string + -> ### 注意 -> 由于 `2.6.11` 版本对 laytpl 语句进行了重要调整,原 `{{ }}` 语法即等同 `{{- }}`,升级版本时,请进行相应调整。可参考:https://gitee.com/layui/layui/issues/I5AXSP +`}}` -{{# }} +cache 2.11+ - - JavaScript 语句。一般用于逻辑处理。 - ``` -
-{{# - var fn = function(){ - return '2017-08-18'; - }; -}} -{{# if(true){ }} - 开始日期:{{= fn() }} -{{# } else { }} - 已截止 -{{# } }} -
- ``` +是否开启模板缓存,以便下次渲染时不重新编译模板 - !}} - -{{!{{! !}}!}} +boolean - -对一段指定的模板区域进行过滤,即不解析该区域的模板。 -``` -{{! {{! 这里面的模板不会被解析 !}} !}} -``` +`true` - + +condense 2.11+ + + +是否压缩模板空白符,如:将多个连续的空白符压缩为单个空格 + + +boolean + + +`true` + + + + +tagStyle
2.11+ + + +设置标签风格。可选值: + +- `legacy`: 采用 `< 2.11` 旧版本的标签风格 +- `modern`: 采用 `2.11+` 新版本的标签风格 + +为了保持向下兼容,默认仍然采用旧版本的标签风格,但在后续版本可能会将默认值设置为 `modern`,因此,**实际使用时,建议显式设置该选项值,以免升级时产生不兼容的问题**。 + + +string + + +`legacy` + + + + !}} diff --git a/docs/laytpl/detail/tags.md b/docs/laytpl/detail/tags.md new file mode 100644 index 00000000..0def79d4 --- /dev/null +++ b/docs/laytpl/detail/tags.md @@ -0,0 +1,86 @@ + + + + + + + + + + + + {{! + + + + + + + + + + + + + + + + + + + + !}} + + + + + +
标签描述
{{= }} + +转义输出。若字段存在 HTML,将进行转义。 + +
{{- }} 2.8+ + +原文输出。即不对 HTML 字符进行转义,但需做好 XSS 防护。 + +
{{# }} + +**旧版本风格**(`< 2.11`)「Scriptlet 标签」。一般用于流程控制,如: + + ```js +{{#if (d.title) { }} + 标题:{{= d.title }} +{{#} else { }} + 默认标题 +{{#} }} + ``` + +
{{ }} 2.11+ + +**新版本风格**「Scriptlet 标签」。一般用于流程控制,如: + + ```js +{{ if (d.title) { }} + 标题:{{= d.title }} +{{ } else { }} + 默认标题 +{{ } }} + ``` + +需设置 `tagStyle: 'modern'` 后生效,否则模板会报错。 + +
{{# }} 2.11+ + +**新版本风格**「注释标签」。即仅在模板中显示,不在视图中输出。 + +需设置 `tagStyle: 'modern'` 后生效,否则会被视为旧版本的 Scriptlet 标签。 + +
{{!{{! !}}!}} + +忽略标签。即该区域中的标签不会被解析,一般用于输出原始标签。如: + +```js +{{! {{! 这里面的 {{= escape }} 等模板标签不会被解析 !}} !}} +``` + +
diff --git a/docs/laytpl/index.md b/docs/laytpl/index.md index 4ade28ad..ea5808c2 100644 --- a/docs/laytpl/index.md +++ b/docs/laytpl/index.md @@ -5,11 +5,11 @@ toc: true # 模板引擎 -> `laytpl` 是 Layui 的一款轻量 JavaScript 模板引擎,在字符解析上有着比较出色的表现。 +> `laytpl` 是 Layui 内置的 JavaScript 模板引擎,采用原生控制流,在模板解析上有着比较出色的表现。

在线测试

-在以下*模板*或*数据*中进行编辑,下方*视图*将呈现对应结果。 +对文本框中的*模板*或*数据*进行编辑,下方将呈现对应的*渲染结果*。注:自 2.11+ 版本开始,你可以设置 `tagStyle: 'modern'` 让模板采用新的标签风格。为了保持向下兼容,默认仍然采用旧版本的标签风格。
{{- d.include("/laytpl/detail/demo.md") }} @@ -20,135 +20,256 @@ toc: true | API | 描述 | | --- | --- | | var laytpl = layui.laytpl | 获得 `laytpl` 模块。 | -| [laytpl(str, options).render(data, callback)](#render) | laytpl 组件渲染,核心方法。 | -| [laytpl.config(options)](#config) | 配置 laytpl 全局属性 | +| [var templateInst = laytpl(template, options)](#laytpl) | 创建模板实例。 | +| [laytpl.config(options)](#config) | 设置基础选项默认值 | +| [laytpl.extendVars(variables)](#variables) 2.11+ | 扩展模板内部变量 | -

模板解析和渲染

+

创建模板实例

-`laytpl(str, options).render(data, callback);` +`var templateInst = laytpl(template, options)` -- 参数 `str` : 模板原始字符 -- 参数 `options` 2.8+ : 当前模板实例的属性选项。可选项详见:[#属性选项](#config) -- 参数 `data` : 模板数据 -- 参数 `callback` : 模板渲染完毕的回调函数,并返回渲染后的字符 +- 参数 `template` : 原始模板字符 +- 参数 `options` 2.8+ : 当前模板实例的选项。详见下述:[#基础选项](#options) + +该方法返回一个模板编译器实例,用于对模板进行数据渲染等操作,实例对象的成员有: + +| 实例成员 | 描述 | +| --- | --- | +| templateInst.render(data, callback) | 给模板实例进行数据渲染,返回渲染后的 HTML 字符 | +| templateInst.compile(template) 2.11+ | 编译新的模板,会强制清除旧模板缓存 | +| templateInst.config 2.11+ | 获取当前模板实例的配置选项 | + +通过将模板编译与渲染两个环节分开,我们可以在模板仅编译一次的情况下,对其渲染不同的数据,如: {{! +```js +var laytpl = layui.laytpl; -``` -layui.use('laytpl', function(){ - var laytpl = layui.laytpl; +// 创建模板实例 +var templateInst = laytpl('{{= d.name }}是一名{{= d.role }}'); - // 直接解析字符 - laytpl('{{= d.name }}是一名前端工程师').render({ - name: '张三' - }, function(str){ - console.log(str); // 张三是一名前端工程师 - }); +// 数据渲染 1 +templateInst.render({ + name: '张三', + role: '全栈开发者' +}, function(html) { + console.log(html); // 张三是一名全栈开发者 +}); - // 同步写法 - var str = laytpl('{{= d.name }}是一名前端工程师').render({ - name: '张三' - }); - console.log(str); // 张三是一名前端工程师 +// 数据渲染 2 +var html = templateInst.render({ + name: '王五', + role: '架构师' }); ``` +!}} -若模板字符较大,可存放在页面某个标签中,如: +若每次需要对不同的模板进行编译和数据渲染,你也可以使用链式写法,如: +{{! +```js +laytpl('{{= d.name }}是一名{{= d.role }}').render({ + name: '张三', + role: '全栈开发者' +}, function(html) { + console.log(html); // 张三是一名全栈开发者 +}); ``` - -
+
``` - !}} -在实际使用时,若模板通用,而数据不同,为减少模板解析的开销,可将语句分开书写,如。 +实际使用时,若模板通用,而数据不同,为了避免对模板进行不必要的重复编译,推荐将创建模板实例与数据渲染分开书写。 -``` -var compile = laytpl(str); // 模板解析 -compile.render(data, callback); // 模板渲染 -``` +

基础选项

-

标签语法

+创建模板实例时,你还可以对其设置一些选项,如: + +{{! +```js +// 创建模板实例 +var templateInst = laytpl(` + {{ let role = d.role || '全栈开发者'; }} + {{= d.name }}是一名{{= role }} +`, { + tagStyle: 'modern' // 采用新版本的标签风格 +}); +var html = templateInst.render({ name: '张三' }); +``` +!}} + +支持设置的完整选项如下:
{{- d.include("/laytpl/detail/options.md") }}
-> ### 注意 -> 开发者在使用模板语法时,需确保模板中的 JS 语句不来自于页面用户输入,即必须在页面开发者自身的可控范围内,否则请避免使用该模板引擎。 +

标签规则

-

选项配置

+
+{{- d.include("/laytpl/detail/tags.md") }} +
+ +#### ⚡ 请注意: +> *开发者在使用模板标签时,需确保模板中待输出的内容在开发者自身的可控范围内,尤其对于用户输入的字符要做好 XSS 防护,否则请避免使用该模板引擎,以免产生 XSS 安全隐患*。 + +

导入子模板 2.11+

+ +{{! +laytpl 支持在模板中通过添加 `{{- include(id, data) }}` 语句引入子模板。`include` 语句参数解释: + +- `id` : 子模板 ID +- `data` : 向子模版传入的数据 + +为了引入的子模板不被转义,因此这里应该使用 `{{- }}`,即对子模板进行原文输出。示例: + +
+  
+
+ +!}} + +若在 Node.js 环境,可通过 `laytpl.extendVars()` 方法重置 `include` 语句实现模板文件的导入。 + +

设置选项默认值

`laytpl.config(options);` -- 参数 `options` : 属性选项。可选项详见下表 +- 参数 `options`: 基础选项 -| 属性 | 描述 | -| --- | --- | -| open | 标签符前缀 | -| close | 标签符后缀 | +你可以设置任意选项的默认值,如: -### 全局配置 - -若模板默认的标签符与其他模板存在冲突,可通过该方法重新设置标签符,如: - -``` +{{! +```js laytpl.config({ - open: '<%', - close: '%>' + open: '<%', // 自定义起始界定符 + close: '%>', // 自定义起始界定符 + tagStyle: 'modern' // 采用新版本的标签风格 }); -// 模板语法将默认采用上述定义的标签符书写 -laytpl(` - <%# var job = ["前端工程师"]; %> - <%= d.name %>是一名<%= job[d.type] %>。 -`).render({ +// 创建模板实例 +var templateInst = laytpl(` + <% var roles = ["前端工程师","全栈工程师","架构师"]; %> + <%= d.name %>是一名<%= roles[d.role] %> +`); +// 渲染 +templateInst.render({ name: '张三', - type: 0 + role: 1 }, function(string){ - console.log(string); // 张三是一名前端工程师。 + console.log(string); // 张三是一名全栈工程师 }); ``` +!}} -### 局部配置 2.8+ +

扩展模板内变量

-若不想受到上述全局配置的影响,可在 `laytpl(str, options)` 方法的第二个参数中设置当前模板的局部属性,如: +`laytpl.extendVars(variables)` -``` -laytpl('<%= d.name %>是一名前端工程师', { - open: '<%', - close: '%>' -}).render({name: '张三'}, function(string){ - console.log(string); // 张三是一名前端工程师。 +- 参数 `variables` : 扩展的变量列表,变量值通常是一个函数 + +事实上 laytpl 内置了一些模板内部方法,如 `_escape, include`。你可以对它们进行重构,或扩展更多内部变量,如: + +{{! +```js +// 扩展模板内部变量 +laytpl.extendVars({ + // 重构 include 方法,实现引入模板文件 + include: function(filename, data) { + // … + }, + // 添加 toDataString 方法 + toDataString: function(date) { + date = date || new Date(); + return new Date(date).toLocaleDateString(); + } +}); + +// 在模板中使用扩展的变量 +var templateInst = laytpl('日期:{{= toDataString(d.time) }}'); +templateInst.render({ time: 1742745600000 }, function(html) { + console.log(html); }); ``` +!}} +## 💖 心语 -## 贴士 +我们在 `2.11` 版本对 laytpl 完成了重要重构,使其能够具备应对更多复杂模板结构的解析能力。同时,为了与业界常用的 JavaScript 模板引擎 ejs 对齐,我们新增了与 ejs 相同的标签规则,这意味着同一套模板可以在 laytpl 和 ejs 中任意切换。 -> Layui table 等组件的动态模板功能,均采用 laytpl 驱动。 laytpl 亦可承载单页面应用开发中的视图模板。 +作为 Layui 为数不多的一个纯功能型的模块,laytpl 承载了一些重要组件的功能支撑,如 table, dropdown 等,使得它们也能够自定义动态模板,增强了组件的可定制化。当然,laytpl 也可以作为前端单页面应用及 Express 等 Web 框架的视图引擎。 diff --git a/docs/modules.md b/docs/modules.md index a03b3b62..8864c832 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -3,7 +3,7 @@ title: 模块系统 toc: true --- -

模块系统

+

模块系统

> Layui 制定了一套适合自身应用场景的轻量级模块规范,以便在不同规模的项目中,也能对前端代码进行很好的管理或维护。 Layui 的轻量级模块系统,并非有意违背 CommonJS 和 ES Module ,而是试图以更简单的方式去诠释高效,这种对*返璞归真*的执念源于在主流标准尚未完全普及的前 ES5 时代,后来也成为 Layui 独特的表达方式,而沿用至今。 @@ -13,20 +13,20 @@ toc: true // 定义模块(通常单独作为一个 JS 文件) layui.define([mods], function(exports){ // … - + exports('mod1', api); // 输出模块 -}); - +}); + // 使用模块 layui.use(['mod1'], function(args){ var mod1 = layui.mod1; - + // … }); ``` 我们可以将其视为「像使用普通 API 一样来管理模块」,在此前提下,组件的承载也变得轻松自如,我们完全可以游刃在以浏览器为宿主的原生态的 HTML/CSS/JavaScript 的开发模式中,而不必卷入层出不穷的主流框架的浪潮之中,给心灵一个栖息之所。 - + 当然,Layui 自然也不是一个模块加载器,而是一套相对完整的 UI 解决方案,但与 Bootstrap 又并不相同,除了 HTML+CSS 本身的静态化处理,Layui 的组件更倾向于 JavaScript 的动态化渲染,并为之提供了相对丰富和统一的 API,使用时,只需稍加熟悉,便可在各种交互中应付自如。 @@ -41,42 +41,42 @@ layui.use(['mod1'], function(args){ /** demo.js **/ layui.define(function(exports){ // do something - + // 输出 demo 模块 exports('demo', { msg: 'Hello Demo' }); }); - + // 若该模块需要依赖别的模块,则在 `mods` 参数中声明即可: // layui.define(['layer', 'form'], callback); ``` 如上所示,`callback` 返回的 `exports` 参数是一个函数,它接受两个参数:参数一为*模块名*,参数二为*模块接口*。 - + 另外, `callback` 将会在初次加载该模块时被自动执行。而有时,在某些特殊场景中可能需要再次执行该 `callback`,那么可以通过 `layui.factory(mod)` 方法获得。如: ``` var demoCallback = layui.factory('demo'); // 得到定义 demo 模块时的 `callback` -``` +``` - **模块命名空间** Layui 定义的模块将会被绑定在 `layui` 对象下,如:`var demo = layui.demo;` 每个模块都有一个特定命名,且无法被占用,所以你无需担心模块的命名空间被污染,除非通过 `layui.disuse([mods])` 方法弃用已定义的模块。 - + 以下是定义一个「依赖 Layui 内置模块」的模块示例: ``` layui.define(['layer', 'laydate'], function(exports){ var layer = layui.layer // 获得 layer 模块 var laydate = layui.laydate; // 获得 laydate 模块 - + // 输出模块 exports('demo', {}); // 模块名 demo 未被占用,此时模块定义成功 // exports('layer', {}); // 模块名 layer 已经存在,此时模块定义失败 -}); +}); ``` - + 同样的,在「扩展模块」时,也同样不能命名已经存在的模块名。 @@ -94,17 +94,17 @@ layui.define(['layer', 'laydate'], function(exports){ layui.use(['layer', 'table'], function(){ var layer = layui.layer; var table = layui.table; - + // do something }); - + // 使用所有内置模块(layui v2.6 开始支持) layui.use(function(){ var layer = layui.layer; var table = layui.table; var laydate = layui.laydate; // … - + // do something }); ``` @@ -115,7 +115,7 @@ layui.use(function(){ layui.use(['layer', 'table'], function(layer, table){ // 使用 layer layer.msg('test'); - + // 使用 table table.render({}); }); @@ -127,64 +127,97 @@ layui.use(['layer', 'table'], function(layer, table){ ``` // 在单页面视图碎片渲染时,再次调用「定义模块」时的 `callback` -layui.use('demo', layui.factory('demo')); +layui.use('demo', layui.factory('demo')); ``` -

扩展模块

+

扩展模块

-`layui.extend(obj);` +`layui.extend(settings);` -- 参数 `obj` 是一个对象,必选,用于声明模块别名。 +- 参数 `settings` : 扩展模块的相关配置,如模块名、模块路径等。 -除了 Layui 的内置模块,在实际项目开发时,必不可少也需要扩展模块(可以简单理解为符合 layui 模块规范的 JS 文件)。 现在,让我们尝试着扩展一个 Layui 第三方模块: +除了 Layui 的内置模块,在实际项目开发时,必不可少也需要扩展模块。我们在前文的「模块命名空间」提到,模块名具有唯一性,即不可被占用,因此我们扩展的模块必须是一个未被定义过的模块名。 -1. **创建模块** +现在,让我们尝试扩展一个 Layui 第三方模块。 -我们在前文的「模块命名空间」提到,模块名具有唯一性,即不可被占用,因此我们扩展的模块必须是一个未被定义过的模块名。假设为:`firstMod`,然后新建一个 `firstMod.js` 文件并放入项目的任意目录中(最好不要放入到 Layui 原始目录) +### 扩展遵循 Layui 规范的模块 -2. **编写模块** +1. **创建模块和定义模块** -接下来我们开始定义 `firstMod` 模块,并编写该模块主体代码。 +假设创建一个模块名为 `testModule` 的模块,新建 `testModule.js` 文件并放入项目的任意目录中(但应避免放入到 Layui 原始目录)。接着我们开始定义 `testModule` 模块,并编写该模块主体代码。 -``` +```js /** - * 编写一个 firstMod 模块 + * 定义 testModule 模块 **/ layui.define(function(exports){ // 也可以依赖其他模块 var obj = { hello: function(str){ - alert('Hello '+ (str || 'firstMod')); + alert('Hello '+ (str || 'TestModule')); } }; - - // 输出 firstMod 接口 - exports('firstMod', obj); + + // 输出 testModule 接口 + exports('testModule', obj); }); ``` -3. **声明模块** +2. **声明模块和使用模块** 现在,我们只需声明模块名及模块文件路径,即完成模块扩展。 -``` -// 假设 firstMod 模块文件所在路径在: /js/layui_exts/firstMod.js +```js +// 假设 testModule 模块文件所在路径在:/js/layui_exts/testModule.js layui.config({ - base: '/js/layui_exts/' // 配置 Layui 第三方扩展模块存放的基础目录 + base: '/js/layui_exts/' // 设置用于扩展模块的基础路径 }).extend({ - firstMod: 'firstMod', // 定义模块名和模块文件路径,继承 layui.config 的 base 路径 - // mod2: 'mod2' // 可同时声明其他更多模块 + testModule: 'testModule', // 定义模块名和模块路径,会前置追加 base 基础路径 + // test1: 'test1' // 还可同时声明其他更多模块 }); - -// 也可以不继承 layui.config 的 base 路径,即单独指定路径 + +// 也可以不前置追加 base 基础路径,即设置单独路径 layui.extend({ - firstMod: '{/}/js/layui_exts/firstMod' // 开头特定符 {/} 即代表采用单独路径 + testModule: '{/}/js/layui_exts/testModule' // 开头特定符 {/} 即代表采用单独路径 }); - + // 然后我们就可以像使用内置模块一样使用扩展模块 -layui.use(['firstMod'], function(){ - var firstMod = layui.firstMod; - - firstMod.hello('World'); +layui.use(['testModule', 'test1'], function(){ + var testModule = layui.testModule; + // var test1 = layui.test1; + + testModule.hello('World'); +}); +``` + +

扩展任意外部模块 2.11+

+ +我们在 `2.11.0` 版本新增了无缝扩展任意外部模块的支持,即无需遵循 Layui 模块规范的第三方库也能通过 Layui 去加载,并且无需对外部模块做任何的代码改动,只需在 `layui.extend()` 方法中声明模块名、路径和接口即可。 + +当声明的模块接受的是一个 `object` 类型时,即意味着声明任意外部模块。声明外部模块的对象由以下选项组成: + +- `src` : 模块路径,可以是项目的相对路径,也可以是任意外部模块的公共 CDN 地址; +- `api` : 接口名称,通常是模块提供的全局对象 + +下面是一个扩展任意外部模块的示例: + +```js +// 扩展任意外部模块 +layui.extend({ + marked: { + src: 'https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js', // 模块路径 + api: 'marked' // 接口名称 + }, + Prism: { + src: 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js', + api: 'Prism' + } +}); + +// 加载扩展模块 +layui.use(['marked', 'Prism'], function() { + console.log('任意外部模块 loaded: ') + console.log(' > marked: ', layui.marked); + console.log(' > Prism: ', layui.Prism); }); ``` @@ -196,7 +229,7 @@ layui.use(['firstMod'], function(){ 在不同的页面中,可能需要用到不同的业务模块。以首页为例: ``` - + + - - + // 加载外部样式 + layui.link('https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css', function(link) { + console.log('prism.min.css loaded'); + }, 'prism'); + + diff --git a/examples/js/index.js b/examples/extends/index.js similarity index 62% rename from examples/js/index.js rename to examples/extends/index.js index e4eca313..e6fbbd77 100644 --- a/examples/js/index.js +++ b/examples/extends/index.js @@ -1,8 +1,8 @@ layui.define(function(exports){ - + exports('index', { - title: '模块入口' + title: 'index 扩展模块' }); -}); \ No newline at end of file +}); diff --git a/examples/extends/temp.js b/examples/extends/temp.js deleted file mode 100644 index d7487d59..00000000 --- a/examples/extends/temp.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - - @Name:layui.modDemo XX组件 - @Author:贤心 - @License:MIT - - */ - -layui.define(['laytpl'], function(exports){ - "use strict"; - - var $ = layui.$ - ,laytpl = layui.laytpl - - //模块名 - ,MOD_NAME = 'modDemo' - - //外部接口 - ,modeDemo = { - config: {} - ,index: layui[MOD_NAME] ? (layui[MOD_NAME].index + 10000) : 0 - - //设置全局项 - ,set: function(options){ - var that = this; - that.config = $.extend({}, that.config, options); - return that; - } - - //事件监听 - ,on: function(events, callback){ - return layui.onevent.call(this, MOD_NAME, events, callback); - } - } - - //操作当前实例 - ,thisModule = function(){ - var that = this - ,options = that.config - ,id = options.id || that.index; - - thisModule.that[id] = that; //记录当前实例对象 - thisModule.config[id] = options; //记录当前实例配置项 - - return { - config: options - //重置实例 - ,reload: function(options){ - that.reload.call(that, options); - } - } - } - - //获取当前实例配置项 - ,getThisModuleConfig = function(id){ - var config = thisModule.config[id]; - if(!config) hint.error('The ID option was not found in the '+ MOD_NAME +' instance'); - return config || null; - } - - //字符常量 - ,ELEM = 'layui-modeDemo' - - - //主模板 - ,TPL_MAIN = ['
' - - ,'
'].join('') - - //构造器 - ,Class = function(options){ - var that = this; - that.index = ++transfer.index; - that.config = $.extend({}, that.config, transfer.config, options); - that.render(); - }; - - //默认配置 - Class.prototype.config = { - - }; - - //重载实例 - Class.prototype.reload = function(options){ - var that = this; - - layui.each(options, function(key, item){ - if(item.constructor === Array) delete that.config[key]; - }); - - that.config = $.extend(true, {}, that.config, options); - that.render(); - }; - - //渲染 - Class.prototype.render = function(){ - var that = this - ,options = that.config; - - //解析模板 - that.elem = $(TPL_MAIN); - - var othis = options.elem = $(options.elem); - if(!othis[0]) return; - - //索引 - that.key = options.id || that.index; - - //插入组件结构 - othis.html(that.elem); - - that.events(); //事件 - }; - - //事件 - Class.prototype.events = function(){ - var that = this; - - - }; - - - - //记录所有实例 - thisModule.that = {}; //记录所有实例对象 - thisModule.config = {}; //记录所有实例配置项 - - //重载实例 - modeDemo.reload = function(id, options){ - var that = thisModule.that[id]; - that.reload(options); - - return thisModule.call(that); - }; - - //核心入口 - modeDemo.render = function(options){ - var inst = new Class(options); - return thisTransfer.call(inst); - }; - - exports(MOD_NAME, modeDemo); -}); diff --git a/examples/js/child/test.js b/examples/extends/test.js similarity index 53% rename from examples/js/child/test.js rename to examples/extends/test.js index 56083520..94fc194b 100644 --- a/examples/js/child/test.js +++ b/examples/extends/test.js @@ -1,8 +1,9 @@ - +/** + * test + */ layui.define(function(exports){ - exports('test', { - title: '子目录模块加载' + title: 'test 扩展模块' }) -}); \ No newline at end of file +}); diff --git a/examples/extends/test/test1.js b/examples/extends/test/test1.js new file mode 100644 index 00000000..f66f7d26 --- /dev/null +++ b/examples/extends/test/test1.js @@ -0,0 +1,9 @@ +/** + * test1 + */ + +layui.define(function(exports){ + exports('test1', { + title: 'test1 扩展模块' + }) +}); diff --git a/examples/extends/test/test2.js b/examples/extends/test/test2.js new file mode 100644 index 00000000..5a15c077 --- /dev/null +++ b/examples/extends/test/test2.js @@ -0,0 +1,9 @@ +/** + * test2 + */ + +layui.define(function(exports){ + exports('test2', { + title: 'test2 扩展模块' + }) +}); diff --git a/examples/laytpl.html b/examples/laytpl.html index efff147c..a2df7f6f 100644 --- a/examples/laytpl.html +++ b/examples/laytpl.html @@ -1,74 +1,45 @@ - + - - - -视图模板引擎 - layui + + + + 模板引擎 - Layui + + - - - -
- -
-
-
模板
- -
- -
+ #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; + } + + + +
-
数据
-
- + -
- -
-
-
-
视图
-
-
-
- +
+
+
+
    +
  • 渲染结果
  • +
  • 源码
  • +
+
+
+ +
+
+ +
+
+
+
+
+
-
-
-
-
- - - - - +
- - + - - // 渲染模板 - var thisTpl = laytpl(data.template); + - // 执行渲染 - 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: '张三' - ,number: 100+i + + + + + + +
+ + + - + }) + }); + + diff --git a/src/layui.js b/src/layui.js index d637cf4a..1867ec03 100644 --- a/src/layui.js +++ b/src/layui.js @@ -4,28 +4,40 @@ * MIT Licensed */ -;!function(win){ - "use strict"; +(function(window) { + 'use strict'; - var doc = win.document; + // 便于打包时的字符压缩 + var document = window.document; + var location = window.location; + + // 基础配置 var config = { - modules: {}, // 模块物理路径 - status: {}, // 模块加载状态 timeout: 10, // 符合规范的模块请求最长等待秒数 - event: {} // 模块自定义事件 + debug: false, // 是否开启调试模式 + version: false // 是否在模块请求时加入版本号参数(以更新模块缓存) }; - var Layui = function(){ - this.v = '2.10.3'; // Layui 版本号 + // 模块加载缓存信息 + var cache = { + modules: {}, // 模块物理路径 + status: {}, // 模块加载就绪状态 + event: {}, // 模块自定义事件 + callback: {} // 模块的回调 + }; + + // constructor + var Class = function() { + this.v = '2.11.0-beta.1'; // 版本号 }; // 识别预先可能定义的指定全局对象 - var GLOBAL = win.LAYUI_GLOBAL || {}; + var GLOBAL = window.LAYUI_GLOBAL || {}; // 获取 layui 所在目录 - var getPath = function(){ - var jsPath = (doc.currentScript && doc.currentScript.tagName.toUpperCase() === 'SCRIPT') ? doc.currentScript.src : function(){ - var js = doc.getElementsByTagName('script'); + var getPath = function() { + var jsPath = (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') ? document.currentScript.src : function(){ + var js = document.getElementsByTagName('script'); var last = js.length - 1; var src; for(var i = last; i > 0; i--){ @@ -41,15 +53,13 @@ }(); // 异常提示 - var error = function(msg, type){ + var error = function(msg, type) { type = type || 'log'; - win.console && console[type] && console[type]('layui error hint: ' + msg); + window.console && console[type] && console[type]('layui error hint: ' + msg); }; - var isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]'; - // 内置模块 - var modules = config.builtin = { + var builtinModules = config.builtin = { lay: 'lay', // 基础 DOM 操作 layer: 'layer', // 弹层 laydate: 'laydate', // 日期 @@ -78,93 +88,174 @@ 'layui.all': 'layui.all' // 聚合标识(功能性的,非真实模块) }; - // 记录基础数据 - Layui.prototype.cache = config; + /** + * 低版本浏览器适配 + * @see polyfill + */ - // 定义模块 - Layui.prototype.define = function(deps, factory){ + // Object.assign + if (typeof Object.assign !== 'function') { + Object.assign = function(target) { + var to = Object(target); + if (arguments.length < 2) return to; + + var sourcesIndex = 1; + for (; sourcesIndex < arguments.length; sourcesIndex++) { + var nextSource = arguments[sourcesIndex]; + if (!(nextSource === undefined || nextSource === null)) { + for (var nextKey in nextSource) { + // 确保属性是源对象自身的(而非来自继承) + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; + } + + /** + * 节点加载事件 + * @param {HTMLElement} node - script 或 link 节点 + * @param {Function} done + * @param {Function} error + */ + var onNodeLoad = function(node, done, error) { + // 资源加载完毕 + var onCompleted = function (e) { + var readyRegExp = /^(complete|loaded)$/; + if (e.type === 'load' || (readyRegExp.test((e.currentTarget || e.srcElement).readyState))) { + removeListener(); + typeof done === 'function' && done(e); + } + }; + // 资源加载失败 + var onError = function (e) { + removeListener(); + typeof error === 'function' && error(e); + }; + + // 移除事件 + var removeListener = function() { + if (node.detachEvent) { + node.detachEvent('onreadystatechange', onCompleted); + } else { + node.removeEventListener('load', onCompleted, false); + node.removeEventListener('error', onError, false); + } + }; + + // 添加事件 + if(node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0)){ + // 此处考虑到 IE9+ load 的稳定性,固仍然采用 onreadystatechange + node.attachEvent('onreadystatechange', onCompleted); + } else { + node.addEventListener('load', onCompleted, false); + node.addEventListener('error', onError, false); + } + }; + + // 或许配置及临时缓存信息 + Class.prototype.cache = Object.assign(config, cache); + + /** + * 全局配置 + * @param {Object} options + */ + Class.prototype.config = function(options) { + Object.assign(config, options); + return this; + }; + + /** + * 定义模块 + * @param {(string|string[])} deps - 依赖的模块列表 + * @param {Function} callback - 模块的回调 + */ + Class.prototype.define = function(deps, callback) { var that = this; - var type = typeof deps === 'function'; - var callback = function(){ - var setApp = function(app, exports){ - layui[app] = exports; - config.status[app] = true; + var useCallback = function() { + var setModule = function(mod, exports) { + layui[mod] = exports; // 将模块接口赋值在 layui 对象中 + cache.status[mod] = true; // 标记模块注册完成 }; - typeof factory === 'function' && factory(function(app, exports){ - setApp(app, exports); - config.callback[app] = function(){ - factory(setApp); + // 执行模块的回调 + typeof callback === 'function' && callback(function(mod, exports) { + setModule(mod, exports); + // 记录模块回调,以便需要时再执行 + cache.callback[mod] = function() { + callback(setModule); } }); return this; }; - type && ( - factory = deps, - deps = [] - ); + // 若未依赖模块 + if (typeof deps === 'function') { + callback = deps; + deps = []; + } - that.use(deps, callback, null, 'define'); + that.use(deps, useCallback, null, 'define'); return that; }; - // 使用特定模块 - Layui.prototype.use = function(apps, callback, exports, from){ + /** + * 使用模块 + * @param {(string|string[])} mods - 模块列表 + * @param {Function} callback - 回调 + */ + Class.prototype.use = function(mods, callback, exports, from) { var that = this; var dir = config.dir = config.dir ? config.dir : getPath; - var head = doc.getElementsByTagName('head')[0]; - apps = function(){ - if(typeof apps === 'string'){ - return [apps]; + // 整理模块队列 + mods = (function() { + if (typeof mods === 'string') { + return [mods]; } - // 当第一个参数为 function 时,则自动加载所有内置模块,且执行的回调即为该 function 参数; - else if(typeof apps === 'function'){ - callback = apps; + // 若第一个参数为 function ,则自动加载所有内置模块,且执行的回调即为该 function 参数; + else if(typeof mods === 'function') { + callback = mods; return ['all']; } - return apps; - }(); + return mods; + })(); - // 如果页面已经存在 jQuery 1.7+ 库且所定义的模块依赖 jQuery,则不加载内部 jquery 模块 - if(win.jQuery && jQuery.fn.on){ - that.each(apps, function(index, item){ - if(item === 'jquery'){ - apps.splice(index, 1); + // 获取 layui 静态资源所在 host + if (!config.host) { + config.host = (dir.match(/\/\/([\s\S]+?)\//)||['//'+ location.host +'/'])[0]; + } + + // 若参数异常 + if (!mods) return that; + + // 若页面已经存在 jQuery 且所定义的模块依赖 jQuery,则不加载内部 jquery 模块 + if (window.jQuery && jQuery.fn.on) { + that.each(mods, function(index, item) { + if (item === 'jquery') { + mods.splice(index, 1); } }); - layui.jquery = layui.$ = jQuery; + layui.jquery = layui.$ = window.jQuery; } - var item = apps[0]; - var timeout = 0; - + // 将模块的接口作为回调的参数传递 exports = exports || []; - // 静态资源host - config.host = config.host || (dir.match(/\/\/([\s\S]+?)\//)||['//'+ location.host +'/'])[0]; + // 加载当前队列的第一个模块 + var item = mods[0]; + var modInfo = that.modules[item]; // 当前模块信息 + // 是否为外部模块,即无需遵循 layui 轻量级模块规范的任意第三方组件。 + var isExternalModule = typeof modInfo === 'object'; - // 加载完毕 - function onScriptLoad(e, url){ - var readyRegExp = navigator.platform === 'PLaySTATION 3' ? /^complete$/ : /^(complete|loaded)$/ - if (e.type === 'load' || (readyRegExp.test((e.currentTarget || e.srcElement).readyState))) { - config.modules[item] = url; - head.removeChild(node); - (function poll() { - if(++timeout > config.timeout * 1000 / 4){ - return error(item + ' is not a valid module', 'error'); - } - config.status[item] ? onCallback() : setTimeout(poll, 4); - }()); - } - } - - // 回调 - function onCallback(){ + // 回调触发 + var onCallback = function () { exports.push(layui[item]); - apps.length > 1 ? - that.use(apps.slice(1), callback, exports, from) - : ( typeof callback === 'function' && function(){ + mods.length > 1 + ? that.use(mods.slice(1), callback, exports, from) + : (typeof callback === 'function' && function() { // 保证文档加载完毕再执行回调 if(layui.jquery && typeof layui.jquery === 'function' && from !== 'define'){ return layui.jquery(function(){ @@ -173,182 +264,241 @@ } callback.apply(layui, exports); }() ); - } + }; - // 如果引入了聚合板,内置的模块则不必重复加载 - if( apps.length === 0 || (layui['layui.all'] && modules[item]) ){ + // 回调轮询 + var pollCallback = function () { + var timeout = 0; // 超时计数器(秒) + var delay = 5; // 轮询等待毫秒数 + + // 轮询模块加载完毕状态 + (function poll() { + if (++timeout > config.timeout * 1000 / delay) { + return error(item + ' is not a valid module', 'error'); + }; + + // 根据模块加载完毕的标志来完成轮询 + // 若为任意外部模块,则标志为该模块接口是否存在; + // 若为遵循 layui 规范的模块,则标志为模块的 status 状态值 + (isExternalModule ? layui[item] = window[modInfo.api] : cache.status[item]) + ? onCallback() + : setTimeout(poll, delay); + })(); + }; + + // 若为发行版,则内置模块不必异步加载 + if (mods.length === 0 || (layui['layui.all'] && builtinModules[item])) { return onCallback(), that; } - /* - * 获取加载的模块 URL - * 如果是内置模块,则按照 dir 参数拼接模块路径 - * 如果是扩展模块,则判断模块路径值是否为 {/} 开头, - * 如果路径值是 {/} 开头,则模块路径即为后面紧跟的字符。 - * 否则,则按照 base 参数拼接模块路径 - */ + // 当前模块所在路径 + var modSrc = isExternalModule ? modInfo.src : modInfo; - var url = ( modules[item] ? (dir + 'modules/') - : (/^\{\/\}/.test(that.modules[item]) ? '' : (config.base || '')) - ) + (that.modules[item] || item) + '.js'; - url = url.replace(/^\{\/\}/, ''); + // 基础路径 + var basePath = builtinModules[item] + ? (dir + 'modules/') // 若为内置模块,则按照默认 dir 参数拼接模块 URL + : (modSrc ? '' : config.base); // 若为扩展模块,且模块路径已设置,则不必再重复拼接基础路径 - // 如果扩展模块(即:非内置模块)对象已经存在,则不必再加载 - if(!config.modules[item] && layui[item]){ - config.modules[item] = url; // 并记录起该扩展模块的 url + // 若从 layui.modules 为获取到模块路径, 则将传入的模块名视为路径名 + if (!modSrc) modSrc = item; + + // 过滤空格符和 .js 后缀 + modSrc = modSrc.replace(/\s/g, '').replace(/\.js[^\/\.]*$/, ''); + + // 拼接最终模块 URL + var url = basePath + modSrc + '.js'; + + // 若扩展模块对象已经存在,则不必再重复加载 + if(!cache.modules[item] && layui[item]){ + cache.modules[item] = url; // 并记录起该扩展模块的 url } // 首次加载模块 - if(!config.modules[item]){ - var node = doc.createElement('script'); + if (!cache.modules[item]) { + var head = document.getElementsByTagName('head')[0]; + var node = document.createElement('script'); node.async = true; - node.charset = 'utf-8'; - node.src = url + function(){ + node.charset = 'utf-8'; // 避免 IE9 的编码问题 + node.src = url + function() { var version = config.version === true - ? (config.v || (new Date()).getTime()) - : (config.version||''); + ? (config.v || (new Date()).getTime()) + : (config.version || ''); return version ? ('?v=' + version) : ''; }(); head.appendChild(node); - if(node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera){ - node.attachEvent('onreadystatechange', function(e){ - onScriptLoad(e, url); - }); - } else { - node.addEventListener('load', function(e){ - onScriptLoad(e, url); - }, false); - } + // 节点加载事件 + onNodeLoad(node, function() { + head.removeChild(node); + pollCallback(); + }, function() { + head.removeChild(node); + }); - config.modules[item] = url; - } else { // 缓存 - (function poll() { - if(++timeout > config.timeout * 1000 / 4){ - return error(item + ' is not a valid module', 'error'); - } - (typeof config.modules[item] === 'string' && config.status[item]) - ? onCallback() - : setTimeout(poll, 4); - }()); + // 模块已首次加载的标记 + cache.modules[item] = url; + } else { // 再次 use 模块 + pollCallback(); } return that; }; - // 弃用原有的指定模块,以便重新扩展新的同名模块 - Layui.prototype.disuse = function(apps){ + // 记录全部模块 + Class.prototype.modules = Object.assign({}, builtinModules); + + /** + * 拓展模块 + * @param {Object} settings - 拓展模块的配置 + */ + Class.prototype.extend = function(settings) { var that = this; - apps = that.isArray(apps) ? apps : [apps]; - that.each(apps, function (index, item) { - if (!config.status[item]) { - // return error('module ' + item + ' is not exist'); + var base = config.base || ''; + var firstSymbolEXP = /^\{\/\}/; // 模块单独路径首字符表达式 + + settings = settings || {}; + + // 遍历拓展模块 + for (var modName in settings) { + if (that[modName] || that.modules[modName]) { // 验证模块是否被占用 + error('the '+ modName + ' module already exists, extend failure'); + } else { + var modInfo = settings[modName]; + // 若直接传入模块路径字符 + if (typeof modInfo === 'string') { + // 判断传入的模块路径是否特定首字符 + // 若存在特定首字符,则模块 URL 即为该首字符后面紧跟的字符 + // 否则,则按照 config.base 路径进行拼接 + if (firstSymbolEXP.test(modInfo)) base = ''; + modInfo = (base + modInfo).replace(firstSymbolEXP, ''); + } + that.modules[modName] = modInfo; } + } + + return that; + }; + + /** + * 弃用指定的模块,以便重新扩展新的同名模块。 + * @param {(string|string[])} mods - 模块列表 + */ + Class.prototype.disuse = function(mods) { + var that = this; + mods = that.isArray(mods) ? mods : [mods]; + that.each(mods, function (index, item) { delete that[item]; - delete modules[item]; + delete builtinModules[item]; delete that.modules[item]; - delete config.status[item]; - delete config.modules[item]; + delete cache.status[item]; + delete cache.modules[item]; }); return that; }; - // 获取节点的 style 属性值 - // currentStyle.getAttribute 参数为 camelCase 形式的字符串 - Layui.prototype.getStyle = function(node, name){ - var style = node.currentStyle ? node.currentStyle : win.getComputedStyle(node, null); + /** + * 获取节点的 style 属性值 + * currentStyle.getAttribute 参数为 camelCase 形式的字符串 + * @param {HTMLElement} node - 节点 + * @param {string} name - 属性名 + * @returns 属性值 + */ + Class.prototype.getStyle = function(node, name) { + var style = node.currentStyle ? node.currentStyle : window.getComputedStyle(node, null); return style.getPropertyValue ? style.getPropertyValue(name) : style.getAttribute(name.replace(/-(\w)/g, function(_, c){ return c ? c.toUpperCase() : '';})); }; - // css 外部加载器 - Layui.prototype.link = function(href, fn, cssname){ + /** + * CSS 外部加载器 + * @param {string} href - 外部 CSS 文件路径 + * @param {Function} callback - 回调函数 + * @param {string} id - 定义 link 标签的 id + */ + Class.prototype.link = function(href, callback, id) { var that = this; - var head = doc.getElementsByTagName('head')[0]; - var link = doc.createElement('link'); + var head = document.getElementsByTagName('head')[0]; + var link = document.createElement('link'); - if(typeof fn === 'string') cssname = fn; + // 若第二个参数为 string 类型,则该参数为 id + if (typeof callback === 'string') { + id = callback; + } - var app = (cssname || href).replace(/\.|\//g, ''); - var id = 'layuicss-'+ app; - var STAUTS_NAME = 'creating'; - var timeout = 0; + // 若加载多个 + if (typeof href === 'object') { + var isArr = that.type(id) === 'array'; + return that.each(href, function(index, value){ + that.link( + value, + index === href.length - 1 && callback, + isArr && id[index] + ); + }); + } + + // 若为传入 id ,则取路径 `//` 后面的字符拼接为 id,不含.与参数 + id = id || href.replace(/^(#|(http(s?)):\/\/|\/\/)|\.|\/|\?.+/g, ''); + id = 'layuicss-'+ id; link.href = href + (config.debug ? '?v='+new Date().getTime() : ''); link.rel = 'stylesheet'; link.id = id; - link.media = 'all'; - if(!doc.getElementById(id)){ + // 插入节点 + if (!document.getElementById(id)) { head.appendChild(link); } - if(typeof fn !== 'function') return that; + // 是否执行回调 + if (typeof callback !== 'function') { + return that; + } - // 轮询 css 是否加载完毕 - (function poll(status) { - var delay = 100; - var getLinkElem = doc.getElementById(id); // 获取动态插入的 link 元素 - - // 如果轮询超过指定秒数,则视为请求文件失败或 css 文件不符合规范 - if(++timeout > config.timeout * 1000 / delay){ - return error(href + ' timeout'); - } - - // css 加载就绪 - if(parseInt(that.getStyle(getLinkElem, 'width')) === 1989){ - // 如果参数来自于初始轮询(即未加载就绪时的),则移除 link 标签状态 - if(status === STAUTS_NAME) getLinkElem.removeAttribute('lay-status'); - // 如果 link 标签的状态仍为「创建中」,则继续进入轮询,直到状态改变,则执行回调 - getLinkElem.getAttribute('lay-status') === STAUTS_NAME ? setTimeout(poll, delay) : fn(); - } else { - getLinkElem.setAttribute('lay-status', STAUTS_NAME); - setTimeout(function(){ - poll(STAUTS_NAME); - }, delay); - } - }()); - - // 轮询css是否加载完毕 - /* - (function poll() { - if(++timeout > config.timeout * 1000 / 100){ - return error(href + ' timeout'); - }; - parseInt(that.getStyle(doc.getElementById(id), 'width')) === 1989 ? function(){ - fn(); - }() : setTimeout(poll, 100); - }()); - */ + onNodeLoad(link, function() { + callback(link); + }, function() { + error(href + ' load error', 'error'); + head.removeChild(link); // 移除节点 + }); return that; }; - // css 内部加载器 - Layui.prototype.addcss = function(firename, fn, cssname){ - return layui.link(config.dir + 'css/' + firename, fn, cssname); + /** + * CSS 内部加载器 + * @param {string} modName - 模块名 + */ + Class.prototype.addcss = function(modName, callback, id) { + return layui.link(config.dir + 'css/' + modName, callback, id); }; - // 存储模块的回调 - config.callback = {}; - - // 重新执行模块的工厂函数 - Layui.prototype.factory = function(modName){ - if(layui[modName]){ + /** + * 获取执行定义模块时的回调函数,factory 为向下兼容 + * @param {string} modName - 模块名 + * @returns {Function} + */ + Class.prototype.factory = function(modName) { + if (layui[modName]) { return typeof config.callback[modName] === 'function' ? config.callback[modName] : null; } }; - // 图片预加载 - Layui.prototype.img = function(url, callback, error) { + /** + * 图片预加载 + * @param {string} url - 图片路径 + * @param {Function} callback - 成功回调 + * @param {Function} error - 错误回调 + */ + Class.prototype.img = function(url, callback, error) { var img = new Image(); img.src = url; - if(img.complete){ + if (img.complete) { return callback(img); } img.onload = function(){ @@ -361,43 +511,12 @@ }; }; - // 全局配置 - Layui.prototype.config = function(options){ - options = options || {}; - for(var key in options){ - config[key] = options[key]; - } - return this; - }; - - // 记录全部模块 - Layui.prototype.modules = function(){ - var clone = {}; - for(var o in modules){ - clone[o] = modules[o]; - } - return clone; - }(); - - // 拓展模块 - Layui.prototype.extend = function(options){ - var that = this; - - // 验证模块是否被占用 - options = options || {}; - for(var o in options){ - if(that[o] || that.modules[o]){ - error(o+ ' Module already exists', 'error'); - } else { - that.modules[o] = options[o]; - } - } - - return that; - }; - - // location.hash 路由解析 - Layui.prototype.router = Layui.prototype.hash = function(hash){ + /** + * location.hash 路由解析 + * @param {string} hash 值 + * @returns {Object} + */ + Class.prototype.router = Class.prototype.hash = function(hash) { var that = this; var hash = hash || location.hash; var data = { @@ -414,7 +533,7 @@ hash = hash.replace(/([^#])(#.*$)/, '$1').split('/') || []; // 提取 Hash 结构 - that.each(hash, function(index, item){ + that.each(hash, function(index, item) { /^\w+=/.test(item) ? function(){ item = item.split('='); data.search[item[0]] = item[1]; @@ -424,12 +543,16 @@ return data; }; - // URL 解析 - Layui.prototype.url = function(href){ + /** + * URL 解析 + * @param {string} href - url 路径 + * @returns {Object} + */ + Class.prototype.url = function(href) { var that = this; var data = { // 提取 url 路径 - pathname: function(){ + pathname: function() { var pathname = href ? function(){ var str = (href.match(/\.[^.]+?\/.+/) || [])[0] || ''; @@ -451,19 +574,19 @@ ).replace(/^\?+/, '').split('&'); // 去除 ?,按 & 分割参数 // 遍历分割后的参数 - that.each(search, function(index, item){ - var _index = item.indexOf('=') - ,key = function(){ // 提取 key - if(_index < 0){ + that.each(search, function(index, item) { + var _index = item.indexOf('='); + var key = function() { // 提取 key + if (_index < 0) { return item.substr(0, item.length); - } else if(_index === 0){ + } else if(_index === 0) { return false; } else { return item.substr(0, _index); } }(); // 提取 value - if(key){ + if (key) { obj[key] = _index > 0 ? item.substr(_index + 1) : null; } }); @@ -472,7 +595,7 @@ }(), // 提取 Hash - hash: that.router(function(){ + hash: that.router(function() { return href ? ((href.match(/#.+/) || [])[0] || '/') : location.hash; @@ -482,15 +605,19 @@ return data; }; - // 本地持久存储 - Layui.prototype.data = function(table, settings, storage){ + /** + * 本地持久存储 + * @param {string} table - 表名 + * @param {Object} settings - 设置项 + * @param {Storage} storage - 存储对象,localStorage 或 sessionStorage + * @returns {Object} + */ + Class.prototype.data = function(table, settings, storage) { table = table || 'layui'; storage = storage || localStorage; - if(!win.JSON || !win.JSON.parse) return; - // 如果 settings 为 null,则删除表 - if(settings === null){ + if (settings === null) { return delete storage[table]; } @@ -504,24 +631,33 @@ var data = {}; } - if('value' in settings) data[settings.key] = settings.value; - if(settings.remove) delete data[settings.key]; + if ('value' in settings) data[settings.key] = settings.value; + if (settings.remove) delete data[settings.key]; storage[table] = JSON.stringify(data); return settings.key ? data[settings.key] : data; }; - // 本地临时存储 - Layui.prototype.sessionData = function(table, settings){ + /** + * 本地临时存储 + * @param {string} table - 表名 + * @param {Object} settings - 设置项 + * @returns {Object} + */ + Class.prototype.sessionData = function(table, settings) { return this.data(table, settings, sessionStorage); } - // 设备信息 - Layui.prototype.device = function(key){ + /** + * 设备信息 + * @param {string} key - 任意 key + * @returns {Object} + */ + Class.prototype.device = function(key) { var agent = navigator.userAgent.toLowerCase(); // 获取版本号 - var getVersion = function(label){ + var getVersion = function(label) { var exp = new RegExp(label + '/([^\\s\\_\\-]+)'); label = (agent.match(exp)||[])[1]; return label || false; @@ -529,19 +665,19 @@ // 返回结果集 var result = { - os: function(){ // 底层操作系统 - if(/windows/.test(agent)){ + os: function() { // 底层操作系统 + if (/windows/.test(agent)) { return 'windows'; - } else if(/linux/.test(agent)){ + } else if(/linux/.test(agent)) { return 'linux'; - } else if(/iphone|ipod|ipad|ios/.test(agent)){ + } else if(/iphone|ipod|ipad|ios/.test(agent)) { return 'ios'; - } else if(/mac/.test(agent)){ + } else if(/mac/.test(agent)) { return 'mac'; } }(), - ie: function(){ // ie 版本 - return (!!win.ActiveXObject || "ActiveXObject" in win) ? ( + ie: function() { // ie 版本 + return (!!window.ActiveXObject || "ActiveXObject" in window) ? ( (agent.match(/msie\s(\d+)/) || [])[1] || '11' // 由于 ie11 并没有 msie 的标识 ) : false; }(), @@ -549,7 +685,7 @@ }; // 任意的 key - if(key && !result[key]){ + if (key && !result[key]) { result[key] = getVersion(key); } @@ -562,18 +698,22 @@ }; // 提示 - Layui.prototype.hint = function(){ + Class.prototype.hint = function() { return { error: error }; }; - // typeof 类型细分 -> string/number/boolean/undefined/null、object/array/function/… - Layui.prototype._typeof = Layui.prototype.type = function(operand){ + /** + * typeof 类型细分 -> string/number/boolean/undefined/null、object/array/function/… + * @param {*} operand - 任意值 + * @returns {string} + */ + Class.prototype._typeof = Class.prototype.type = function(operand) { if(operand === null) return String(operand); // 细分引用类型 - return (typeof operand === 'object' || typeof operand === 'function') ? function(){ + return (typeof operand === 'object' || typeof operand === 'function') ? function() { var type = Object.prototype.toString.call(operand).match(/\s(.+)\]$/) || []; // 匹配类型字符 var classType = 'Function|Array|Date|RegExp|Object|Error|Symbol'; // 常见类型字符 @@ -586,13 +726,17 @@ }() : typeof operand; }; - // 对象是否具备数组结构(此处为兼容 jQuery 对象) - Layui.prototype._isArray = Layui.prototype.isArray = function(obj){ + /** + * 对象是否具备数组结构(此处为兼容 jQuery 对象) + * @param {Object} obj - 任意对象 + * @returns {boolean} + */ + Class.prototype._isArray = Class.prototype.isArray = function(obj) { var that = this; var len; var type = that.type(obj); - if(!obj || (typeof obj !== 'object') || obj === win) return false; + if (!obj || (typeof obj !== 'object') || obj === window) return false; len = 'length' in obj && obj.length; // 兼容 ie return type === 'array' || len === 0 || ( @@ -600,47 +744,61 @@ ); }; - // 遍历 - Layui.prototype.each = function(obj, fn){ + /** + * 遍历 + * @param {Object} obj - 任意对象 + * @param {Function} fn - 遍历回调 + */ + Class.prototype.each = function(obj, fn) { var key; var that = this; - var callFn = function(key, obj){ // 回调 - return fn.call(obj[key], key, obj[key]) + var callback = function(key, obj) { + return fn.call(obj[key], key, obj[key]); }; - if(typeof fn !== 'function') return that; + if (typeof fn !== 'function') { + return that; + } + obj = obj || []; // 优先处理数组结构 - if(that.isArray(obj)){ - for(key = 0; key < obj.length; key++){ - if(callFn(key, obj)) break; + if (that.isArray(obj)) { + for (key = 0; key < obj.length; key++) { + if(callback(key, obj)) break; } } else { - for(key in obj){ - if(callFn(key, obj)) break; + for (key in obj) { + if(callback(key, obj)) break; } } return that; }; - // 将数组中的成员对象按照某个 key 的 value 值进行排序 - Layui.prototype.sort = function(arr, key, desc, notClone){ + /** + * 将数组中的成员对象按照某个 key 的 value 值进行排序 + * @param {Object[]} arr - 任意数组 + * @param {string} key - 任意 key + * @param {boolean} desc - 是否降序 + * @param {boolean} notClone - 是否不对 arr 进行克隆 + * @returns {Object[]} + */ + Class.prototype.sort = function(arr, key, desc, notClone) { var that = this; var clone = notClone ? (arr || []) : JSON.parse( JSON.stringify(arr || []) ); // 若未传入 key,则直接返回原对象 - if(that.type(arr) === 'object' && !key){ + if (that.type(arr) === 'object' && !key) { return clone; - } else if(typeof arr !== 'object'){ // 若 arr 非对象 + } else if(typeof arr !== 'object') { // 若 arr 非对象 return [clone]; } // 开始排序 - clone.sort(function(o1, o2){ + clone.sort(function(o1, o2) { var v1 = o1[key]; var v2 = o2[key]; @@ -650,16 +808,17 @@ */ // 若比较的成员均为数字 - if(!isNaN(o1) && !isNaN(o2)) return o1 - o2; + if (!isNaN(o1) && !isNaN(o2)) return o1 - o2; + // 若比较的成员只存在某一个非对象 - if(!isNaN(o1) && isNaN(o2)){ + if (!isNaN(o1) && isNaN(o2)) { if(key && typeof o2 === 'object'){ v1 = o1; } else { return -1; } - } else if (isNaN(o1) && !isNaN(o2)){ - if(key && typeof o1 === 'object'){ + } else if (isNaN(o1) && !isNaN(o2)) { + if (key && typeof o1 === 'object') { v2 = o2; } else { return 1; @@ -676,10 +835,10 @@ var isNum = [!isNaN(v1), !isNaN(v2)]; // 若为数字比较 - if(isNum[0] && isNum[1]){ - if(v1 && (!v2 && v2 !== 0)){ // 数字 vs 空 + if (isNum[0] && isNum[1]) { + if(v1 && (!v2 && v2 !== 0)) { // 数字 vs 空 return 1; - } else if((!v1 && v1 !== 0) && v2){ // 空 vs 数字 + } else if((!v1 && v1 !== 0) && v2) { // 空 vs 数字 return -1; } else { // 数字 vs 数字 return v1 - v2; @@ -691,9 +850,9 @@ */ // 若为非数字比较 - if(!isNum[0] && !isNum[1]){ + if (!isNum[0] && !isNum[1]) { // 字典序比较 - if(v1 > v2){ + if (v1 > v2) { return 1; } else if (v1 < v2) { return -1; @@ -703,7 +862,7 @@ } // 若为混合比较 - if(isNum[0] || !isNum[1]){ // 数字 vs 非数字 + if (isNum[0] || !isNum[1]) { // 数字 vs 非数字 return -1; } else if(!isNum[0] || isNum[1]) { // 非数字 vs 数字 return 1; @@ -715,10 +874,14 @@ return clone; }; - // 阻止事件冒泡 - Layui.prototype.stope = function(thisEvent){ - thisEvent = thisEvent || win.event; - try { thisEvent.stopPropagation() } catch(e){ + /** + * 阻止事件冒泡 + * @param {Event} thisEvent - 事件对象 + */ + Class.prototype.stope = function(thisEvent) { + try { + thisEvent.stopPropagation(); + } catch(e) { thisEvent.cancelBubble = true; } }; @@ -726,51 +889,63 @@ // 字符常理 var EV_REMOVE = 'LAYUI-EVENT-REMOVE'; - // 自定义模块事件 - Layui.prototype.onevent = function(modName, events, callback){ - if(typeof modName !== 'string' - || typeof callback !== 'function') return this; - - return Layui.event(modName, events, null, callback); + /** + * 自定义模块事件 + * @param {string} modName - 模块名 + * @param {string} events - 事件名 + * @param {Function} callback - 回调 + * @returns {Object} + */ + Class.prototype.onevent = function(modName, events, callback) { + if (typeof modName !== 'string' || typeof callback !== 'function') { + return this; + } + return Class.event(modName, events, null, callback); }; - // 执行自定义模块事件 - Layui.prototype.event = Layui.event = function(modName, events, params, fn){ + /** + * 执行自定义模块事件 + * @param {string} modName - 模块名 + * @param {string} events - 事件名 + * @param {Object} params - 参数 + * @param {Function} fn - 回调 + */ + Class.prototype.event = Class.event = function(modName, events, params, fn) { var that = this; var result = null; var filter = (events || '').match(/\((.*)\)$/)||[]; // 提取事件过滤器字符结构,如:select(xxx) var eventName = (modName + '.'+ events).replace(filter[0], ''); // 获取事件名称,如:form.select - var filterName = filter[1] || ''; // 获取过滤器名称,,如:xxx - var callback = function(_, item){ + var filterName = filter[1] || ''; // 获取过滤器名称, 如:xxx + var callback = function(_, item) { var res = item && item.call(that, params); res === false && result === null && (result = false); }; // 如果参数传入特定字符,则执行移除事件 - if(params === EV_REMOVE){ + if (params === EV_REMOVE) { delete (that.cache.event[eventName] || {})[filterName]; return that; } // 添加事件 - if(fn){ - config.event[eventName] = config.event[eventName] || {}; + if (fn) { + cache.event[eventName] = cache.event[eventName] || {}; if (filterName) { - // 带filter不支持重复事件 - config.event[eventName][filterName] = [fn]; + // 带 filter 不支持重复事件 + cache.event[eventName][filterName] = [fn]; } else { - // 不带filter处理的是所有的同类事件,应该支持重复事件 - config.event[eventName][filterName] = config.event[eventName][filterName] || []; - config.event[eventName][filterName].push(fn); + // 不带 filter 处理的是所有的同类事件,应该支持重复事件 + cache.event[eventName][filterName] = cache.event[eventName][filterName] || []; + cache.event[eventName][filterName].push(fn); } return this; } // 执行事件回调 - layui.each(config.event[eventName], function(key, item){ + layui.each(cache.event[eventName], function(key, item) { // 执行当前模块的全部事件 - if(filterName === '{*}'){ + if (filterName === '{*}') { layui.each(item, callback); return; } @@ -783,20 +958,36 @@ return result; }; - // 新增模块事件 - Layui.prototype.on = function(events, modName, callback){ + /** + * 新增模块事件 + * @param {string} events - 事件名 + * @param {string} modName - 模块名 + * @param {Function} callback - 回调 + * @returns {Object} + */ + Class.prototype.on = function(events, modName, callback) { var that = this; return that.onevent.call(that, modName, events, callback); } - // 移除模块事件 - Layui.prototype.off = function(events, modName){ + /** + * 移除模块事件 + * @param {string} events - 事件名 + * @param {string} modName - 模块名 + * @returns {Object} + */ + Class.prototype.off = function(events, modName) { var that = this; return that.event.call(that, modName, events, EV_REMOVE); }; - // 防抖 - Layui.prototype.debounce = function (func, wait) { + /** + * 防抖 + * @param {Function} func - 回调 + * @param {number} wait - 延时执行的毫秒数 + * @returns {Function} + */ + Class.prototype.debounce = function (func, wait) { var timeout; return function () { var context = this; @@ -808,8 +999,12 @@ } }; - // 节流 - Layui.prototype.throttle = function (func, wait) { + /** + * 节流 + * @param {Function} func - 回调 + * @param {number} wait - 不重复执行的毫秒数 + */ + Class.prototype.throttle = function (func, wait) { var cooldown = false; return function () { var context = this; @@ -824,8 +1019,6 @@ } }; - // exports layui - win.layui = new Layui(); - -}(window); // gulp build: layui-footer - + // export layui + window.layui = new Class(); +})(window); diff --git a/src/modules/laytpl.js b/src/modules/laytpl.js index 0e67d9a2..4dbc29c4 100644 --- a/src/modules/laytpl.js +++ b/src/modules/laytpl.js @@ -1,162 +1,475 @@ /** - * laytpl 轻量模板引擎 + * laytpl + * 轻量级通用模板引擎 */ -layui.define(function(exports){ - "use strict"; +(function(global) { + 'use strict'; - // 默认属性 - var config = { - open: '{{', // 标签符前缀 - close: '}}' // 标签符后缀 - }; + var MOD_NAME = 'laytpl'; - // 模板工具 - var tool = { - escape: function(html){ - var exp = /[<"'>]|&(?=#[a-zA-Z0-9]+)/g; - if(html === undefined || html === null) return ''; - - html += ''; - if(!exp.test(html)) return html; - - return html.replace(/&(?!#?[a-zA-Z0-9]+;)/g, '&') - .replace(//g, '>') - .replace(/'/g, ''').replace(/"/g, '"'); - } - }; - - // 内部方法 - var inner = { - exp: function(str){ - return new RegExp(str, 'g'); - }, - // 错误提示 - error: function(e, source){ - var error = 'Laytpl Error: '; - typeof console === 'object' && console.error(error + e + '\n'+ (source || '')); - return error + e; - } - }; - - // constructor - var Class = function(template, options){ + // 实例接口 + var thisModule = function() { var that = this; - that.config = that.config || {}; - that.template = template; + var options = that.config; + return { + config: options, - // 简单属性合并 - var extend = function(obj){ - for(var i in obj){ - that.config[i] = obj[i]; + /** + * 渲染模板 + * @param {Object} data - 模板数据 + * @param {Function} callback - 回调函数 + * @returns {string} 渲染后的模板 + */ + render: function(data, callback) { + options.data = data + var html = that.render(); + + // 如果传入目标元素选择器,则直接将模板渲染到目标元素中 + if (options.target) { + var elem = document.querySelector(options.target); + if (elem) { + elem.innerHTML = html; + } + } + + // 返回结果 + return typeof callback === 'function' + ? (callback(html), this) + : html; + }, + + /** + * 编译新的模板 + * @param {string} template - 模板 + * @returns {this} + */ + compile: function(template) { + options.template = template; + delete that.compilerCache; // 清除模板缓存 + // that.compile(template); + return this; + }, + + /** + * 模板编译错误事件 + * @param {Function} callback + * @returns {this} + */ + error: function(callback) { + callback && (options.error = callback); + return this; + }, + + /** + * 以下为兼容旧版本相关方法 + */ + + // 解析并渲染模板 + parse: function(template, data) { + return this.compile(template).render(data); } }; - - extend(config); - extend(options); }; - // 标签正则 - Class.prototype.tagExp = function(type, _, __){ - var options = this.config; - var types = [ - '#([\\s\\S])+?', // js 语句 - '([^{#}])*?' // 普通字段 - ][type || 0]; - - return inner.exp((_||'') + options.open + types + options.close + (__||'')); + // 模板内部变量 + var vars = { + // 字符转义 + escape: function(html) { + var exp = /[<"'>]|&(?=#[a-zA-Z0-9]+)/g; + if (html === undefined || html === null) return ''; + html = ''+ html; + if (!exp.test(html)) return html; + return html.replace(exp, function(str) { + return '&#'+ str.charCodeAt(0) + ';'; + }); + } }; - // 模版解析 - Class.prototype.parse = function(template, data){ + // 组件工具类方法 + var tools = { + regex: function(str, mod) { + return new RegExp(str, mod || 'g'); + }, + + /** + * 错误提示 + * @param {string} e - 原始错误信息 + * @param {Object} opts - 自定义选项 + * @param {Function} error - 错误回调 + * @returns {string} - 错误提示 + */ + error: function(e, opts, error) { + opts = opts || {}; + opts = Object.assign({ + debug: '', + message: 'Laytpl '+ (opts.type || '') +'Error: ' + e + }, opts); + + // 向控制台输出错误信息 + typeof console === 'object' && console.error(opts.message, '\n', opts.debug, '\n', opts); + typeof error === 'function' && error(opts); // 执行错误回调 + return opts.message; // 向视图返回错误提示 + } + }; + + // 默认配置 + var config = { + open: '{{', // 起始界定符 + close: '}}', // 结束界定符 + cache: true, // 是否开启模板缓存,以便下次渲染时不重新编译模板 + condense: true, // 是否压缩模板空白符,如:将多个连续的空白符压缩为单个空格 + tagStyle: '' // 标签风格。默认采用 < 2.11 的风格,设置 modern 则采用 2.11+ 风格 + }; + + // 构造器 + var Class = function(template, options) { + var that = this; + + // 选项合并 + options = that.config = Object.assign({ + template: template + }, config, options); + + // 当前实例的模板内工具 + that.vars = Object.assign({ + /** + * 引用外部模板。若在 Node.js 环境,可通过重置该方法实现模板文件导入 + * @param {string} id - 模板 ID + * @param {Object} data - 模板数据 + * @returns {string} 模板渲染后内容 + */ + include: function(id, data) { + var elem = document.getElementById(id); + var template = elem ? elem.innerHTML : ''; + return template ? that.render(template, data) : ''; + } + }, vars); + + // 编译模板 + that.compile(options.template); + }; + + /** + * 渲染 + * @param {Object} template - 模板 + * @param {Object} data - 数据 + * @returns {string} 渲染后的模板内容 + */ + Class.prototype.render = function(template, data) { + var that = this; + var options = that.config; + + // 获得模板渲染函数 + var compiler = template ? that.compile(template) : ( + that.compilerCache || that.compile(options.template) + ); + + // 获取渲染后的字符 + var html = function() { + data = data || options.data || {}; + try { + return compiler(data); + } catch(e) { + template = template || options.template; + return tools.error(e, { + debug: that.checkErrorArea(template, data), + template: template, + type: 'Render' + }, options.error); + } + }(); + + // 缓存编译器 + if (options.cache && !template) { + that.compilerCache = compiler; + } + + return html; // 返回渲染后的字符 + }; + + /** + * 编译模板 + * @param {string} template - 原始模板 + * @returns {Function} 模板编译器,用于后续数据渲染 + */ + Class.prototype.compile = function(template) { var that = this; var options = that.config; var source = template; - var jss = inner.exp('^'+ options.open +'#', ''); - var jsse = inner.exp(options.close +'$', ''); + var openDelimiter = options.open; + var closeDelimiter = options.close; + var condense = options.condense; + var regex = tools.regex; + const placeholder = '\u2028'; // Unicode 行分隔符 - // 模板必须为 string 类型 - if(typeof template !== 'string') return template; + // console.log('compile'); - // 正则解析 - template = template.replace(/\s+|\r|\t|\n/g, ' ') - .replace(inner.exp(options.open +'#'), options.open +'# ') - .replace(inner.exp(options.close +'}'), '} '+ options.close).replace(/\\/g, '\\\\') - - // 不匹配指定区域的内容 - .replace(inner.exp(options.open + '!(.+?)!' + options.close), function(str){ - str = str.replace(inner.exp('^'+ options.open + '!'), '') - .replace(inner.exp('!'+ options.close), '') - .replace(inner.exp(options.open + '|' + options.close), function(tag){ - return tag.replace(/(.)/g, '\\$1') - }); - return str - }) - - // 匹配 JS 语法 - .replace(/(?="|')/g, '\\').replace(that.tagExp(), function(str){ - str = str.replace(jss, '').replace(jsse, ''); - return '";' + str.replace(/\\(.)/g, '$1') + ';view+="'; - }) - - // 匹配普通输出语句 - .replace(that.tagExp(1), function(str){ - var start = '"+laytpl.escape('; - if(str.replace(/\s/g, '') === options.open + options.close){ + // 模板必须为 string 类型,且不能为空 + if (typeof template !== 'string' || !template) { + return function() { return ''; - } - str = str.replace(inner.exp(options.open + '|' + options.close), ''); - if(/^=/.test(str)){ - str = str.replace(/^=/, ''); - } else if(/^-/.test(str)){ - str = str.replace(/^-/, ''); - start = '"+('; - } - return start + str.replace(/\\(.)/g, '$1') + ')+"'; - }); + }; + } - template = '"use strict";var view = "' + template + '";return view;'; + /** + * 完整标签正则 + * @param {string[]} cores - 标签内部核心表达式,含:前置、主体、后置 + * @param {Object} sides - 标签两侧外部表达式 + * @returns {RegExp} + */ + var tagRegex = function(cores, sides) { + var arr = [ + '(?:'+ openDelimiter + (cores[0] || '') +'\\s*)', // 界定符前置 + '('+ (cores[1] || '[\\s\\S]') +'*?)', // 标签主体 + '(?:\\s*'+ (cores[2] || '') + closeDelimiter +')' // 界定符后置 + ]; + sides = sides || {}; + sides.before && arr.unshift(sides.before); // 标签前面的表达式 + sides.after && arr.push(sides.after); // 标签后面的表达式 + return regex(arr.join('')); + }; + + // 匹配非输出类型标签两侧的换行符和空白符,避免渲染后占用一行 + var sidesRegex = condense ? ['', ''] : ['(?:(?:\\n)*\\s*)', '(?:\\s*?)']; + var delimSides = { + before: sidesRegex[0], + after: sidesRegex[1] + }; + + /** + * 清理多余符号 + * @param {string} body - 标签主体字符 + * @param {boolean} nowrap - 是否强制不换行 + * @returns {string} 清理后的字符 + */ + var clear = function(body, nowrap) { + if (!condense) { + // 还原语句中的 Unicode 行分隔符 + body = body.replace(regex(placeholder), nowrap ? '' : '\n'); + } + body = body.replace(/\\(\\|")/g, '$1'); // 去除多余反斜杠 + return body; + }; + + // 纠正标签结构 + var correct = function(tpl) { + return tpl.replace(regex('([}\\]])'+ closeDelimiter), '$1 '+ closeDelimiter); + }; + + // 模板解析 + var parse = that.parse = function(tpl) { + tpl = tpl || ''; + if (!tpl) return tpl; + + // 压缩连续空白符 + if (condense) { + tpl = tpl.replace(/\t/g, ' ').replace(/\s+/g, ' '); + } + + // 初始整理 + tpl = correct(tpl) // 纠正标签 + .replace(/(?=\\|")/g, '\\') // 转义反斜杠和双引号 + .replace(/\r?\n/g, condense ? '' : placeholder); // 整理换行符 + + // 忽略标签 - 即区域中的内容不进行标签解析 + tpl = tpl.replace(tagRegex(['!', '', '!'], delimSides), function(str, body) { + body = body.replace(regex(openDelimiter + '|' + closeDelimiter), function(tag) { + return tag.replace(/(?=.)/g, '\\'); + }); + return body; + }); + + // 模板字符拼接 + var strConcatenation = function(body) { + // 通过对 20k+ 行的模板进行编译测试, 发现 Chrome `+=` 性能竟优于 `push` + // 1k 次循环 + 1k 行数据量进行模板编译+渲染,发现 `+=` 性能仍然优于 `push` + // 考虑可能是 V8 做了 Ropes 结构优化? 或跟模板采用「静态拼接」的实现有关(可能性更大) + return ['";', body, '__laytpl__+="'].join('\n'); + // return ['");', body, '__laytpl__.push("'].join('\n'); + }; + + // 解析输出标签 + var output = function(str, delimiter, body) { + var _escape; + + if (!body) return ''; + body = clear(body, true); + + // 输出方式 + if (delimiter === '-') { // 原文输出,即不对 HTML 原文进行转义 + _escape = ''; + } else { // 转义输出 + _escape = '_escape'; + } + + return body ? strConcatenation( + '__laytpl__+='+ _escape +'('+ body +');' + // '__laytpl__.push('+ _escape +'('+ body +'));' + ) : ''; + }; + + // 解析 Scriptlet + var statement = function(str, body) { + if (!body) return ''; + body = clear(body); + return strConcatenation(body); + }; + + // 标签风格 + if (options.tagStyle === 'modern') { // 2.11+ 版本风格 + // 注释标签 - 仅在模板中显示,不进行解析,也不在视图中输出 + tpl = tpl.replace(tagRegex(['#'], delimSides), ''); + // 输出标签 + tpl = tpl.replace(tagRegex(['(=|-)']), output); + // Scriptlet 标签 + tpl = tpl.replace(tagRegex([], delimSides), statement); + } else { // < 2.11 版本风格 + // Scriptlet 标签 + tpl = tpl.replace(tagRegex(['#'], delimSides), statement); + // 输出标签 + tpl = tpl.replace(tagRegex(['(=|-)*']), output); + } + + // 恢复换行符 + if (!condense) { + tpl = tpl.replace(regex(placeholder), '\\n'); + } + + return tpl; + }; + + // 创建模板编译器 + var createCompiler = that.createCompiler = function(template) { + var codeBuilder = [ + 'function(d){', + '"use strict";', + 'var __laytpl__="",'+ + function() { // 内部变量 + // 内部方法 + var arr = []; + for (var key in that.vars) { + arr.push(((key === 'escape' ? '_' : '') + key) +'=laytpl.'+ key); + } + return arr.join(','); + }() + ';', + '__laytpl__="'+ parse(template) +'";', + 'return __laytpl__;', + // '__laytpl__.push("'+ parse(template) +'");', + // 'return __laytpl__.join("");', + '};' + ].join('\n'); + // console.log(codeBuilder); - try { /** * 请注意: 开发者在使用模板语法时,需确保模板中的 JS 语句不来自于页面用户输入。 * 即模板中的 JS 语句必须在页面开发者自身的可控范围内,否则请避免使用该模板解析。 */ - that.cache = template = new Function('d, laytpl', template); - return template(data, tool); + return new Function('laytpl', 'return '+ codeBuilder)(that.vars); + }; + + try { + return createCompiler(template); // 返回编译器 } catch(e) { - delete that.cache; - return inner.error(e, source); + delete that.compilerCache; + return function() { + return tools.error(e, { + debug: that.checkErrorArea(source), + template: source, + type: 'Compile' + }, options.error); + }; } }; - // 数据渲染 - Class.prototype.render = function(data, callback){ - data = data || {}; - + /** + * 校验出错区域 + * @param {string} source - 原始模板 + * @param {Object} data - 数据 + * @returns {string} 出错区域的模板碎片 + */ + Class.prototype.checkErrorArea = function(source, data) { var that = this; - var result = that.cache ? that.cache(data, tool) : that.parse(that.template, data); + var srcs = source.split(/\n/g); + var validLine = -1; // 有效行 - // 返回渲染结果 - typeof callback === 'function' && callback(result); - return result; - }; - - // 创建实例 - var laytpl = function(template, options){ - return new Class(template, options); - }; - - // 配置全局属性 - laytpl.config = function(options){ - options = options || {}; - for(var i in options){ - config[i] = options[i]; + // 逐行查找 + var i = 0; + var str = ''; + var len = srcs.length; + for (; i < len; i++) { + str += srcs[i]; + try { + data + ? that.createCompiler(str)(data) + : new Function('"use strict";_laytpl__="'+ that.parse(str) +'";'); + validLine = i; + } catch(e) { + continue; + } } + + // 呈现模板出错大致区域 + var errorArea = function(errLine) { + var arr = []; + var addLine = 3; // 错误行上下延伸的行数 + var i = 0; + var len = srcs.length; + + if (errLine < 0) errLine = 0; + if (errLine > len - 1) errLine = len - 1; + + i = errLine - addLine; + if (i < 0) i = 0; + + for (; i < len; i++) { + arr.push((i == errLine ? '? ' : ' ') +(i + 1)+ '| '+ srcs[i]); + if (i >= errLine + addLine) break; + } + + return '\n'+ arr.join('\n'); + }; + + return errorArea(validLine + 1); // 有效行的下一行即为出错行 }; - laytpl.v = '2.0.0'; + /** + * 创建实例 + * @param {string} template - 模板 + * @param {Object} options - 选项 + * @returns + */ + var laytpl = function(template, options) { + var inst = new Class(template, options); + return thisModule.call(inst); + }; - // export - exports('laytpl', laytpl); -}); + /** + * 扩展模板内部变量 + * @param {Object} variables - 扩展内部变量,变量值通常为函数 + */ + laytpl.extendVars = function(variables) { + Object.assign(vars, variables); + }; + + /** + * 设置默认配置 + * @param {Object} options - 选项 + */ + laytpl.config = laytpl.set = function(options) { + Object.assign(config, options); + }; + + // 输出接口 + typeof module === 'object' && typeof exports === 'object' + ? module.exports = laytpl // CommonJS + : ( // 浏览器 + typeof layui === 'object' ? layui.define(function(exports) { // Layui + exports(MOD_NAME, laytpl); + }) : ( + typeof define === 'function' && define.amd ? define(function() { // RequireJS + return laytpl; + }) : global.laytpl = laytpl // 单独引入 + ) + ); +})(this); \ No newline at end of file