EJS 项目源码阅读
ejs 项目大名鼎鼎,应该就不需要介绍了,主要收获就是得知了实现一个模板引擎的流程,ejs 是将模板作为字符串逐个解析,遇到正常的 html 代码,就放进一个数组中去,遇到 js 代码则进行过滤器、包含等的处理,最后数组 join 成一个可以成为 Function 构造函数第二个参数的字符串,构造成构造函数之后就是调用返回最终的 html 字符串。以下是阅读源码的笔记,因为源码中遗憾有很多说明,所以笔记很少。
ejs = (function(){ // CommonJS require() function require(p){ if ('fs' == p) return {}; if ('path' == p) return {}; var path = require.resolve(p) , mod = require.modules[path]; if (!mod) throw new Error('failed to require "' + p + '"'); if (!mod.exports) { mod.exports = {}; mod.call(mod.exports, mod, mod.exports, require.relative(path)); } return mod.exports; } require.modules = {}; require.resolve = function (path){ var orig = path , reg = path + '.js' , index = path + '/index.js'; return require.modules[reg] && reg || require.modules[index] && index || orig; }; require.register = function (path, fn){ require.modules[path] = fn; }; require.relative = function (parent) { return function(p){ if ('.' != p.substr(0, 1)) return require(p); var path = parent.split('/') , segs = p.split('/'); path.pop(); for (var i = 0; i < segs.length; i++) { var seg = segs[i]; if ('..' == seg) path.pop(); else if ('.' != seg) path.push(seg); } return require(path.join('/')); }; }; require.register("ejs.js", function(module, exports, require){ /*! * EJS * Copyright(c) 2012 TJ Holowaychuk <tj@vision-media.ca> * MIT Licensed */ /** * Module dependencies. */ var utils = require('./utils') , path = require('path') , dirname = path.dirname , extname = path.extname , join = path.join , fs = require('fs') , read = fs.readFileSync; /** * Filters. * * @type Object */ var filters = exports.filters = require('./filters'); /** * Intermediate js cache. * * @type Object */ var cache = {}; /** * Clear intermediate js cache. * * @api public */ exports.clearCache = function(){ cache = {}; }; /** * Translate filtered code into function calls. * * @param {String} js * @return {String} * @api private */ function filtered(js) { return js.substr(1).split('|').reduce(function(js, filter){ var parts = filter.split(':') , name = parts.shift() , args = parts.join(':') || ''; if (args) args = ', ' + args; return 'filters.' + name + '(' + js + args + ')'; }); }; /** * Re-throw the given `err` in context to the * `str` of ejs, `filename`, and `lineno`. * * @param {Error} err * @param {String} str * @param {String} filename * @param {String} lineno * @api private */ function rethrow(err, str, filename, lineno){ var lines = str.split('n') , start = Math.max(lineno - 3, 0) , end = Math.min(lines.length, lineno + 3); // Error context var context = lines.slice(start, end).map(function(line, i){ var curr = i + start + 1; return (curr == lineno ? ' >> ' : ' ') + curr + '| ' + line; }).join('n'); // Alter exception message err.path = filename; err.message = (filename || 'ejs') + ':' + lineno + 'n' + context + 'nn' + err.message; throw err; } /** * Parse the given `str` of ejs, returning the function body. * * @param {String} str * @return {String} * @api public */ var parse = exports.parse = function(str, options){ var options = options || {} , open = options.open || exports.open || '<%' , close = options.close || exports.close || '%>' , filename = options.filename , compileDebug = options.compileDebug !== false , buf = ""; buf += 'var buf = [];'; if (false !== options._with) buf += 'nwith (locals || {}) { (function(){ '; buf += 'n buf.push(''; var lineno = 1; var consumeEOL = false; for (var i = 0, len = str.length; i < len; ++i) { var stri = str[i]; if (str.slice(i, open.length + i) == open) { i += open.length var prefix, postfix, line = (compileDebug ? '__stack.lineno=' : '') + lineno; switch (str[i]) { case '=': prefix = "', escape((" + line + ', '; postfix = ")), '"; ++i; break; case '-': prefix = "', (" + line + ', '; postfix = "), '"; ++i; break; default: prefix = "');" + line + ';'; postfix = "; buf.push('"; } // 查找对应的结尾 var end = str.indexOf(close, i); if (end < 0){ throw new Error('Could not find matching close tag "' + close + '".'); } // 提取模板中的js代码 var js = str.substring(i, end) , start = i , include = null , n = 0; // 结尾-表示不需要转义,这种情况<%- code -%> if ('-' == js[js.length-1]){ js = js.substring(0, js.length - 2); consumeEOL = true; } // 处理调用include情况 <%- include() %> if (0 == js.trim().indexOf('include')) { var name = js.trim().slice(7).trim(); if (!filename) throw new Error('filename option is required for includes'); var path = resolveInclude(name, filename); include = read(path, 'utf8'); include = exports.parse(include, { filename: path, _with: false, open: open, close: close, compileDebug: compileDebug }); buf += "' + (function(){" + include + "})() + '"; js = ''; } // 处理js代码换行的情况,又路遇装逼犯,~(n = js.indexOf("n", n))就是存在换行符情况之下执行 while (~(n = js.indexOf("n", n))) n++, lineno++; // 过滤器处理,<%=: users | map:'name' | join %> if (js.substr(0, 1) == ':') js = filtered(js); if (js) { if (js.lastIndexOf('//') > js.lastIndexOf('n')) js += 'n'; buf += prefix; buf += js; buf += postfix; } i += end - start + close.length - 1; } else if (stri == "\") { buf += "\\"; } else if (stri == "'") { buf += "\'"; } else if (stri == "r") { // ignore } else if (stri == "n") { if (consumeEOL) { consumeEOL = false; } else { buf += "\n"; lineno++; } } else { buf += stri; } } if (false !== options._with) buf += "'); })();n} nreturn buf.join('');"; else buf += "');nreturn buf.join('');"; return buf; }; /** * Compile the given `str` of ejs into a `Function`. * * @param {String} str * @param {Object} options * @return {Function} * @api public */ var compile = exports.compile = function(str, options){ options = options || {}; var escape = options.escape || utils.escape; var input = JSON.stringify(str) , compileDebug = options.compileDebug !== false , client = options.client , filename = options.filename ? JSON.stringify(options.filename) : 'undefined'; if (compileDebug) { // Adds the fancy stack trace meta info str = [ 'var __stack = { lineno: 1, input: ' + input + ', filename: ' + filename + ' };', rethrow.toString(), 'try {', exports.parse(str, options), '} catch (err) {', ' rethrow(err, __stack.input, __stack.filename, __stack.lineno);', '}' ].join("n"); } else { str = exports.parse(str, options); } if (options.debug) console.log(str); if (client) str = 'escape = escape || ' + escape.toString() + ';n' + str; try { // 别忘了,可以使用构造函数定义函数呀 var fn = new Function('locals, filters, escape, rethrow', str); } catch (err) { if ('SyntaxError' == err.name) { err.message += options.filename ? ' in ' + filename : ' while compiling ejs'; } throw err; } if (client) return fn; return function(locals){ return fn.call(this, locals, filters, escape, rethrow); } }; /** * Render the given `str` of ejs. * * Options: * * - `locals` Local variables object * - `cache` Compiled functions are cached, requires `filename` * - `filename` Used by `cache` to key caches * - `scope` Function execution context * - `debug` Output generated function body * - `open` Open tag, defaulting to "<%" * - `close` Closing tag, defaulting to "%>" * * @param {String} str * @param {Object} options * @return {String} * @api public */ // 渲染函数 exports.render = function(str, options){ var fn , options = options || {}; // 检查是否缓存 if (options.cache) { if (options.filename) { fn = cache[options.filename] || (cache[options.filename] = compile(str, options)); } else { throw new Error('"cache" option requires "filename".'); } } else { fn = compile(str, options); } options.__proto__ = options.locals; return fn.call(options.scope, options); }; /** * Render an EJS file at the given `path` and callback `fn(err, str)`. * * @param {String} path * @param {Object|Function} options or callback * @param {Function} fn * @api public */ exports.renderFile = function(path, options, fn){ var key = path + ':string'; if ('function' == typeof options) { fn = options, options = {}; } options.filename = path; var str; try { str = options.cache ? cache[key] || (cache[key] = read(path, 'utf8')) : read(path, 'utf8'); } catch (err) { fn(err); return; } fn(null, exports.render(str, options)); }; /** * Resolve include `name` relative to `filename`. * * @param {String} name * @param {String} filename * @return {String} * @api private */ function resolveInclude(name, filename) { var path = join(dirname(filename), name); var ext = extname(name); if (!ext) path += '.ejs'; return path; } // express support exports.__express = exports.renderFile; /** * Expose to require(). */ if (require.extensions) { require.extensions['.ejs'] = function (module, filename) { filename = filename || module.filename; var options = { filename: filename, client: true } , template = fs.readFileSync(filename).toString() , fn = compile(template, options); module._compile('module.exports = ' + fn.toString() + ';', filename); }; } else if (require.registerExtension) { require.registerExtension('.ejs', function(src) { return compile(src, {}); }); } }); // module: ejs.js require.register("filters.js", function(module, exports, require){ /*! * EJS - Filters * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> * MIT Licensed */ /** * First element of the target `obj`. */ // 过滤函数 exports.first = function(obj) { return obj[0]; }; /** * Last element of the target `obj`. */ // 过滤函数 exports.last = function(obj) { return obj[obj.length - 1]; }; /** * Capitalize the first letter of the target `str`. */ // 过滤函数 exports.capitalize = function(str){ str = String(str); return str[0].toUpperCase() + str.substr(1, str.length); }; /** * Downcase the target `str`. */ // 过滤函数 exports.downcase = function(str){ return String(str).toLowerCase(); }; /** * Uppercase the target `str`. */ // 过滤函数 exports.upcase = function(str){ return String(str).toUpperCase(); }; /** * Sort the target `obj`. */ // 过滤函数 exports.sort = function(obj){ return Object.create(obj).sort(); }; /** * Sort the target `obj` by the given `prop` ascending. */ exports.sort_by = function(obj, prop){ return Object.create(obj).sort(function(a, b){ a = a[prop], b = b[prop]; if (a > b) return 1; if (a < b) return -1; return 0; }); }; /** * Size or length of the target `obj`. */ exports.size = exports.length = function(obj) { return obj.length; }; /** * Add `a` and `b`. */ exports.plus = function(a, b){ return Number(a) + Number(b); }; /** * Subtract `b` from `a`. */ exports.minus = function(a, b){ return Number(a) - Number(b); }; /** * Multiply `a` by `b`. */ exports.times = function(a, b){ return Number(a) * Number(b); }; /** * Divide `a` by `b`. */ exports.divided_by = function(a, b){ return Number(a) / Number(b); }; /** * Join `obj` with the given `str`. */ exports.join = function(obj, str){ return obj.join(str || ', '); }; /** * Truncate `str` to `len`. */ exports.truncate = function(str, len, append){ str = String(str); if (str.length > len) { str = str.slice(0, len); if (append) str += append; } return str; }; /** * Truncate `str` to `n` words. */ exports.truncate_words = function(str, n){ var str = String(str) , words = str.split(/ +/); return words.slice(0, n).join(' '); }; /** * Replace `pattern` with `substitution` in `str`. */ exports.replace = function(str, pattern, substitution){ return String(str).replace(pattern, substitution || ''); }; /** * Prepend `val` to `obj`. */ exports.prepend = function(obj, val){ return Array.isArray(obj) ? [val].concat(obj) : val + obj; }; /** * Append `val` to `obj`. */ exports.append = function(obj, val){ return Array.isArray(obj) ? obj.concat(val) : obj + val; }; /** * Map the given `prop`. */ exports.map = function(arr, prop){ return arr.map(function(obj){ return obj[prop]; }); }; /** * Reverse the given `obj`. */ exports.reverse = function(obj){ return Array.isArray(obj) ? obj.reverse() : String(obj).split('').reverse().join(''); }; /** * Get `prop` of the given `obj`. */ exports.get = function(obj, prop){ return obj[prop]; }; /** * Packs the given `obj` into json string */ exports.json = function(obj){ return JSON.stringify(obj); }; }); // module: filters.js require.register("utils.js", function(module, exports, require){ /*! * EJS * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> * MIT Licensed */ /** * Escape the given string of `html`. * * @param {String} html * @return {String} * @api private */ exports.escape = function(html){ return String(html) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/'/g, ''') .replace(/"/g, '"'); }; }); // module: utils.js return require("ejs"); })();
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: For 循环异步操作问题小结
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论