artTemplate源码
主要实现:通过拼接字符串的方式构建编译函数,辅助函数通过在编译函数字符串体内以var methodName=function(){}方式传入,因此编译函数字符串体内就可以使用methodName方式加以调用;用户数据通过向编译函数传参注入,赋值给$data后,就可以使用$data.value的方式使用;if、each语句预先通过parser方法将其拼接为js形式的if、each语法。
1.构建编译函数
compile.js
- /**
- * 编译模板
- * 2012-6-6 @TooBug: define 方法名改为 compile,与 Node Express 保持一致
- * @name template.compile
- * @param {String} 模板字符串
- * @param {Object} 编译选项
- *
- * - openTag {String} // 逻辑语法开始标签 "{{" 或 "<%"
- * - closeTag {String} // 逻辑语法开始标签 "}}" 或 "%>"
- * - filename {String} // 用于报错时提醒用户模板字符串的模板名,并作为cacheStore的属性存储编译函数
- * - escape {Boolean} // html字符串转义,编码: <%=value%> 不编码:<%=#value%>
- * - compress {Boolean} // 是否压缩多余空白和注释
- * - debug {Boolean} // 是否开启调试模式编译模板字符串
- * - cache {Boolean} // 是否缓存模板字符串编译结果
- * - parser {Function} // 语法转换插件钩子,"<%"、"%>"间内部值预处理,默认defaults.parser
- *
- * @return {Function} 渲染方法
- */
- // 通过compiler以字符串形式拼接编译函数体,最终转化成函数输出
- var compile = template.compile = function (source, options) {
- // 合并默认配置
- options = options || {};
- for (var name in defaults) {
- if (options[name] === undefined) {
- options[name] = defaults[name];
- }
- }
- var filename = options.filename;
- try {
- var Render = compiler(source, options);
- } catch (e) {
- e.filename = filename || 'anonymous';
- e.name = 'Syntax Error';
- return showDebugInfo(e);
- }
- // 对编译结果进行一次包装
- function render (data) {
- try {
- return new Render(data, filename) + '';
- } catch (e) {
- // 运行时出错后自动开启调试模式重新编译
- if (!options.debug) {
- options.debug = true;
- return compile(source, options)(data);
- }
- return showDebugInfo(e)();
- }
- }
- render.prototype = Render.prototype;
- render.toString = function () {
- return Render.toString();
- };
- if (filename && options.cache) {
- // 缓存模板字符串解析函数
- cacheStore[filename] = render;
- }
- return render;
- };
- // 数组迭代
- var forEach = utils.$each;
- // 静态分析模板变量
- var KEYWORDS =
- // 关键字
- 'break,case,catch,continue,debugger,default,delete,do,else,false'
- + ',finally,for,function,if,in,instanceof,new,null,return,switch,this'
- + ',throw,true,try,typeof,var,void,while,with'
- // 保留字
- + ',abstract,boolean,byte,char,class,const,double,enum,export,extends'
- + ',final,float,goto,implements,import,int,interface,long,native'
- + ',package,private,protected,public,short,static,super,synchronized'
- + ',throws,transient,volatile'
- // ECMA 5 - use strict
- + ',arguments,let,yield'
- + ',undefined';
- // 滤除多行注释、单行注释、单双引号包裹字符串、点号+空格后的字符串
- var REMOVE_RE = /\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g;
- // 滤除变量,如{{if admin}}中的admin
- var SPLIT_RE = /[^\w$]+/g;
- // 滤除js关键字
- var KEYWORDS_RE = new RegExp(["\\b" + KEYWORDS.replace(/,/g, '\\b|\\b') + "\\b"].join('|'), 'g');
- // 滤除数字
- var NUMBER_RE = /^\d[^,]*|,\d[^,]*/g;
- // 滤除起始、结尾的多个逗号
- var BOUNDARY_RE = /^,+|,+$/g;
- // 以$或,分割
- var SPLIT2_RE = /^$|,+/;
- // 获取变量
- function getVariable (code) {
- return code
- .replace(REMOVE_RE, '')
- .replace(SPLIT_RE, ',')
- .replace(KEYWORDS_RE, '')
- .replace(NUMBER_RE, '')
- .replace(BOUNDARY_RE, '')
- .split(SPLIT2_RE);
- };
- // 字符串转义
- function stringify (code) {
- return "'" + code
- // 单引号与反斜杠转义
- .replace(/('|\\)/g, '\\$1')
- // 换行符转义(windows + linux)
- .replace(/\r/g, '\\r')
- .replace(/\n/g, '\\n') + "'";
- }
- // 通过模板字符串和options配置,拼接编译函数体,template.compile方法中转化成函数
- function compiler (source, options) {
- var debug = options.debug;// 是否开启调试模式编译模板字符串
- var openTag = options.openTag;// 逻辑语法开始标签 "{{"
- var closeTag = options.closeTag;// 逻辑语法闭合标签 "}}"
- var parser = options.parser;// 语法转换插件钩子,默认的钩子为拼接if、each、echo等语句
- var compress = options.compress;// 是否压缩多余空白和注释
- var escape = options.escape;// html字符串转义,编码: <%=value%> 不编码:<%=#value%>
- var line = 1;
- // uniq记录定义编译函数体内已定义的方法名或属性名,防止重复定义
- var uniq = {$data:1,$filename:1,$utils:1,$helpers:1,$out:1,$line:1};
- var isNewEngine = ''.trim;// '__proto__' in {}
- var replaces = isNewEngine
- ? ["$out='';", "$out+=", ";", "$out"]
- : ["$out=[];", "$out.push(", ");", "$out.join('')"];
- var concat = isNewEngine
- ? "$out+=text;return $out;"
- : "$out.push(text);";
- var print = "function(){"
- + "var text=''.concat.apply('',arguments);"
- + concat
- + "}";
- var include = "function(filename,data){"
- + "data=data||$data;"
- + "var text=$utils.$include(filename,data,$filename);"
- + concat
- + "}";
- var headerCode = "'use strict';"
- + "var $utils=this,$helpers=$utils.$helpers,"
- + (debug ? "$line=0," : "");
- var mainCode = replaces[0];
- var footerCode = "return new String(" + replaces[3] + ");"
- // html与逻辑语法分离
- forEach(source.split(openTag), function (code) {
- code = code.split(closeTag);
- var $0 = code[0];
- var $1 = code[1];
- // code: [html] 以openTag起始,无closeTag闭合,处理成html字符串形式
- if (code.length === 1) {
- mainCode += html($0);
- // code: [logic, html] 以openTag起始,有closeTag闭合,处理成logic+html字符串形式
- } else {
- mainCode += logic($0);
- if ($1) {
- mainCode += html($1);
- }
- }
- });
- var code = headerCode + mainCode + footerCode;
- // 调试语句,试用try-catch方法捕获错误,报错
- if (debug) {
- code = "try{" + code + "}catch(e){"
- + "throw {"
- + "filename:$filename,"
- + "name:'Render Error',"
- + "message:e.message,"
- + "line:$line,"
- + "source:" + stringify(source)
- + ".split(/\\n/)[$line-1].replace(/^\\s+/,'')"
- + "};"
- + "}";
- }
- try {
- // code用于拼接字符串构建函数
- var Render = new Function("$data", "$filename", code);
- Render.prototype = utils;
- return Render;
- } catch (e) {
- e.temp = "function anonymous($data,$filename) {" + code + "}";
- throw e;
- }
- // 处理 HTML 语句
- function html (code) {
- // 记录行号,调试模式下输出处理失败的行号
- line += code.split(/\n/).length - 1;
- // 压缩多余空白与注释
- if (compress) {
- code = code
- .replace(/\s+/g, ' ')
- .replace(/<!--[\w\W]*?-->/g, '');
- }
- if (code) {
- code = replaces[1] + stringify(code) + replaces[2] + "\n";
- }
- return code;
- }
- // 处理逻辑语句
- function logic (code) {
- var thisLine = line;
- if (parser) {
- // 语法转换插件钩子,默认的钩子为拼接if、each、echo等语句
- code = parser(code, options);
- } else if (debug) {
- // 记录行号
- code = code.replace(/\n/g, function () {
- line ++;
- return "$line=" + line + ";";
- });
- }
- // 输出语句. 编码: <%=value%> 不编码:<%=#value%>
- // <%=#value%> 等同 v2.0.3 之前的 <%==value%>
- if (code.indexOf('=') === 0) {
- var escapeSyntax = escape && !/^=[=#]/.test(code);
- code = code.replace(/^=[=#]?|[\s;]*$/g, '');
- // 对内容编码
- if (escapeSyntax) {
- var name = code.replace(/\s*\([^\)]+\)/, '');
- // 排除 utils.* | include | print,当name值为utils中内部方法或print、include
- // headerCode中,this关键字指向$utils,$escape可直接调用,对html中'、"、<、>、&进行转义
- if (!utils[name] && !/^(include|print)$/.test(name)) {
- code = "$escape(" + code + ")";
- }
- // 不编码
- } else {
- code = "$string(" + code + ")";
- }
- code = replaces[1] + code + replaces[2];
- }
- if (debug) {
- code = "$line=" + thisLine + ";" + code;
- }
- // 提取模板中的方法名,在headerCode中注入该方法的内容,拼接的函数体内就可以通过方法名调用
- forEach(getVariable(code), function (name) {
- // name 值可能为空,在安卓低版本浏览器下
- if (!name || uniq[name]) {
- return;
- }
- var value;
- // 声明模板变量
- // 赋值优先级:
- // [include, print] > utils > helpers > data
- if (name === 'print') {
- value = print;
- } else if (name === 'include') {
- value = include;
- } else if (utils[name]) {
- value = "$utils." + name;
- } else if (helpers[name]) {
- value = "$helpers." + name;
- } else {
- value = "$data." + name;
- }
- headerCode += name + "=" + value + ",";
- uniq[name] = true;
- });
- return code + "\n";
- }
- };
2.if、each、print、echo、include语句及过滤函数预处理
syntax.js
- // 语法转换插件钩子,"<%"、"%>"间内部值预处理,拼接if、each、print、include、echo语句等,参见compiler模块
- defaults.openTag = '{{';
- defaults.closeTag = '}}';
- // {{value | filterA:'abcd' | filterB}}形式,调用$helpers下方法对value进行过滤处理
- var filtered = function (js, filter) {
- var parts = filter.split(':');
- var name = parts.shift();
- var args = parts.join(':') || '';
- if (args) {
- args = ', ' + args;
- }
- return '$helpers.' + name + '(' + js + args + ')';
- }
- // 语法转换插件钩子,"<%"、"%>"间内部值预处理,拼接if、each、print、include、echo语句等,参见compiler模块
- defaults.parser = function (code, options) {
- // var match = code.match(/([\w\$]*)(\b.*)/); // \b单词边界符
- // var key = match[1];
- // var args = match[2];
- // var split = args.split(' ');
- // split.shift();
- code = code.replace(/^\s/, '');// 滤除起始的空格
- var split = code.split(' ');
- var key = split.shift();
- var args = split.join(' ');
- switch (key) {
- // 拼接if语句
- case 'if':
- code = 'if(' + args + '){';
- break;
- case 'else':
- if (split.shift() === 'if') {
- split = ' if(' + split.join(' ') + ')';
- } else {
- split = '';
- }
- code = '}else' + split + '{';
- break;
- case '/if':
- code = '}';
- break;
- // 拼接each语句
- case 'each':
- var object = split[0] || '$data';
- var as = split[1] || 'as';
- var value = split[2] || '$value';
- var index = split[3] || '$index';
- var param = value + ',' + index;
- if (as !== 'as') {
- object = '[]';
- }
- code = '$each(' + object + ',function(' + param + '){';
- break;
- case '/each':
- code = '});';
- break;
- // 拼接print语句
- case 'echo':
- code = 'print(' + args + ');';
- break;
- // 拼接print、include语句
- case 'print':
- case 'include':
- code = key + '(' + split.join(',') + ');';
- break;
- default:
- // 过滤器(辅助方法),value为待过滤的变量,filterA为helpers下方法名,'abcd'为filterA参数
- // {{value | filterA:'abcd' | filterB}}
- // >>> $helpers.filterB($helpers.filterA(value, 'abcd'))
- // TODO: {{ddd||aaa}} 不包含空格
- if (/^\s*\|\s*[\w\$]/.test(args)) {
- var escape = true;
- // {{#value | link}}
- if (code.indexOf('#') === 0) {
- code = code.substr(1);
- escape = false;
- }
- var i = 0;
- var array = code.split('|');
- var len = array.length;
- var val = array[i++];
- for (; i < len; i ++) {
- val = filtered(val, array[i]);
- }
- code = (escape ? '=' : '=#') + val;
- // 即将弃用 {{helperName value}}
- } else if (template.helpers[key]) {
- code = '=#' + key + '(' + split.join(',') + ');';
- // 内容直接输出 {{value}}
- } else {
- code = '=' + code;
- }
- break;
- }
- return code;
- };
3.辅助函数
utils.js
- var toString = function (value, type) {
- if (typeof value !== 'string') {
- type = typeof value;
- if (type === 'number') {
- value += '';
- } else if (type === 'function') {
- value = toString(value.call(value));
- } else {
- value = '';
- }
- }
- return value;
- };
- var escapeMap = {
- "<": "<",
- ">": ">",
- '"': """,
- "'": "'",
- "&": "&"
- };
- var escapeFn = function (s) {
- return escapeMap[s];
- };
- var escapeHTML = function (content) {
- return toString(content)
- .replace(/&(?![\w#]+;)|[<>"']/g, escapeFn);
- };
- var isArray = Array.isArray || function (obj) {
- return ({}).toString.call(obj) === '[object Array]';
- };
- var each = function (data, callback) {
- var i, len;
- if (isArray(data)) {
- for (i = 0, len = data.length; i < len; i++) {
- callback.call(data, data[i], i, data);
- }
- } else {
- for (i in data) {
- callback.call(data, data[i], i);
- }
- }
- };
- var utils = template.utils = {
- $helpers: {},
- $include: renderFile,
- $string: toString,
- $escape: escapeHTML,
- $each: each
- };
helper.js,可由用户添加过滤函数等
- /**
- * 添加模板辅助方法
- * @name template.helper
- * @param {String} 名称
- * @param {Function} 方法
- */
- template.helper = function (name, helper) {
- helpers[name] = helper;
- };
- var helpers = template.helpers = utils.$helpers;
4.编译接口
template.js
- /**
- * 模板引擎
- * @name template
- * @param {String} 模板名
- * @param {Object, String} 数据。如果为字符串,则作为模板字符串进行编译,缓存并返回编译函数
- * 如果为对象,则作为传给编译函数的数据,最终返回编译结果
- * @return {String, Function} 渲染好的HTML字符串或者渲染方法
- */
- var template = function (filename, content) {
- return typeof content === 'string'
- ? compile(content, {
- filename: filename
- })
- : renderFile(filename, content);
- };
- template.version = '3.0.0';
renderFile.js
- /**
- * 渲染模板(根据模板名)
- * @name template.render
- * @param {String} 模板名,页面元素id
- * @param {Object} 数据,data传入为空时,返回结果为编译函数
- * @return {String} 渲染好的字符串
- */
- var renderFile = template.renderFile = function (filename, data) {
- var fn = template.get(filename) || showDebugInfo({
- filename: filename,
- name: 'Render Error',
- message: 'Template not found'
- });
- return data ? fn(data) : fn;
- };
get.js
- /**
- * 获取编译缓存(可由外部重写此方法)
- * @param {String} 模板名
- * @param {Function} 编译好的函数
- */
- template.get = function (filename) {
- var cache;
- if (cacheStore[filename]) {
- // 获取使用内存缓存的编译函数
- cache = cacheStore[filename];
- } else if (typeof document === 'object') {
- // 通过模板名获取模板字符串,编译,并返回编译函数
- var elem = document.getElementById(filename);
- if (elem) {
- var source = (elem.value || elem.innerHTML)
- .replace(/^\s*|\s*$/g, '');
- cache = compile(source, {
- filename: filename
- });
- }
- }
- return cache;
- };
render.js
- /**
- * 渲染模板
- * @name template.render
- * @param {String} 模板字符串
- * @param {Object} 数据
- * @return {String} 编译函数
- */
- template.render = function (source, options) {
- return compile(source, options);
- };
5.错误提示
onerror.js
- /**
- * 模板错误事件(可由外部重写此方法),触发console.error提示错误信息
- * @name template.onerror
- * @event
- */
- template.onerror = function (e) {
- var message = 'Template Error\n\n';
- for (var name in e) {
- message += '<' + name + '>\n' + e[name] + '\n\n';
- }
- if (typeof console === 'object') {
- console.error(message);
- }
- };
- // 模板调试器
- var showDebugInfo = function (e) {
- template.onerror(e);
- return function () {
- return '{Template Error}';
- };
- };
6.配置
compile.js
- /**
- * 设置全局配置
- * @name template.config
- * @param {String} 名称
- * @param {Any} 值
- */
- template.config = function (name, value) {
- defaults[name] = value;
- };
- var defaults = template.defaults = {
- openTag: '<%', // 逻辑语法开始标签
- closeTag: '%>', // 逻辑语法结束标签
- escape: true, // 是否编码输出变量的 HTML 字符
- cache: true, // 是否开启缓存(依赖 options 的 filename 字段)
- compress: false, // 是否压缩输出
- parser: null // 自定义语法格式器 @see: template-syntax.js
- };
7.外层包裹,适用于amd/cmd/commonjs环境,同seajs
intro.js
- /*!
- * artTemplate - Template Engine
- * https://github.com/aui/artTemplate
- * Released under the MIT, BSD, and GPL Licenses
- */
- !(function () {
outro.js
- // RequireJS && SeaJS
- if (typeof define === 'function') {
- define(function() {
- return template;
- });
- // NodeJS
- } else if (typeof exports !== 'undefined') {
- module.exports = template;
- } else {
- this.template = template;
- }
- })();
附:拼接js文件实现使用grunt
Gruntfile.js配置
- module.exports = function (grunt) {
- var sources_native = [
- 'src/intro.js',
- 'src/template.js',
- 'src/config.js',
- 'src/cache.js',
- 'src/render.js',
- 'src/renderFile.js',
- 'src/get.js',
- 'src/utils.js',
- 'src/helper.js',
- 'src/onerror.js',
- 'src/compile.js',
- //<<<< 'src/syntax.js',
- 'src/outro.js'
- ];
- var sources_simple = Array.apply(null, sources_native);
- sources_simple.splice(sources_native.length - 1, 0, 'src/syntax.js');
- grunt.initConfig({
- pkg: grunt.file.readJSON('package.json'),
- meta: {
- banner: '/*!<%= pkg.name %> - Template Engine | <%= pkg.homepage %>*/\n'
- },
- concat: {
- options: {
- separator: ''
- },
- 'native': {
- src: sources_native,
- dest: 'dist/template-native-debug.js'
- },
- simple: {
- src: sources_simple,
- dest: 'dist/template-debug.js'
- }
- },
- uglify: {
- options: {
- banner: '<%= meta.banner %>'
- },
- 'native': {
- src: '<%= concat.native.dest %>',
- dest: 'dist/template-native.js'
- },
- simple: {
- src: '<%= concat.simple.dest %>',
- dest: 'dist/template.js'
- }
- },
- qunit: {
- files: ['test/**/*.html']
- },
- jshint: {
- files: [
- 'dist/template-native.js',
- 'dist/template.js'
- ],
- options: {
- curly: true,
- eqeqeq: true,
- immed: true,
- latedef: true,
- newcap: true,
- noarg: true,
- sub: true,
- undef: true,
- boss: true,
- eqnull: true,
- browser: true
- },
- globals: {
- console: true,
- define: true,
- global: true,
- module: true
- }
- },
- watch: {
- files: '<config:lint.files>',
- tasks: 'lint qunit'
- }
- });
- grunt.loadNpmTasks('grunt-contrib-uglify');
- grunt.loadNpmTasks('grunt-contrib-jshint');
- //grunt.loadNpmTasks('grunt-contrib-qunit');
- //grunt.loadNpmTasks('grunt-contrib-watch');
- grunt.loadNpmTasks('grunt-contrib-concat');
- grunt.registerTask('default', ['concat', /*'jshint',*/ 'uglify']);
- };
package.json
- {
- "name": "art-template",
- "description": "JavaScript Template Engine",
- "homepage": "http://aui.github.com/artTemplate/",
- "keywords": [
- "util",
- "functional",
- "template"
- ],
- "author": "tangbin <sugarpie.tang@gmail.com>",
- "repository": {
- "type": "git",
- "url": "git://github.com/aui/artTemplate.git"
- },
- "main": "./node/template.js",
- "version": "3.0.3",
- "devDependencies": {
- "express": "~4.4.3",
- "grunt-cli": "*",
- "grunt": "*",
- "grunt-contrib-jshint": "*",
- "grunt-contrib-concat": "*",
- "grunt-contrib-uglify": "*"
- },
- "license": "BSD"
- }
其他
[我的博客,欢迎交流!](http://rattenking.gitee.io/stone/index.html)
[我的CSDN博客,欢迎交流!](https://blog.csdn.net/m0_38082783)
[微信小程序专栏](https://blog.csdn.net/column/details/18335.html)
[前端笔记专栏](https://blog.csdn.net/column/details/18321.html)
[微信小程序实现部分高德地图功能的DEMO下载](http://download.csdn.net/download/m0_38082783/10244082)
[微信小程序实现MUI的部分效果的DEMO下载](http://download.csdn.net/download/m0_38082783/10196944)
[微信小程序实现MUI的GIT项目地址](https://github.com/Rattenking/WXTUI-DEMO)
[微信小程序实例列表](http://blog.csdn.net/m0_38082783/article/details/78853722)
[前端笔记列表](http://blog.csdn.net/m0_38082783/article/details/79208205)
[游戏列表](http://blog.csdn.net/m0_38082783/article/details/79035621)