# 正则表达式简单实现
| |
| const templateStr = `<h2>我喜欢吃{{peach}}, {{ lain }}也喜欢吃 {{peach}}吗?</h2>` |
| |
| |
| const data = { |
| lain: 'lain', |
| peach: '桃子' |
| } |
| |
| |
| function render(templateStr, data) { |
| return templateStr.replace(/\{\{\s*(\w+)\s*\}\}/g, function (findStr, $1) { |
| console.log(data[$1]); |
| return data[$1] |
| }) |
| } |
| |
| |
| const newStr = render(templateStr, data) |
| console.log(newStr) |
# 什么是 tokens
| [ |
| ["test", "<h2>我喜欢吃"], |
| ["name", "peach"], |
| ["test", ", "], |
| ["name", "lain"], |
| ["test", "也喜欢吃"], |
| ["name", "peach"], |
| ["test ", "吗?</h2>"], |
| ] |
| <ul> |
| {{#arr}} |
| <li> |
| <span>{{.}}</span> |
| </li> |
| {{/arr}} |
| </ul> |
| [ |
| ['text', '<ul>'], |
| [ '#', 'arr', [ |
| ['text', '<li><span>'], |
| ['name', '.'], |
| ['text', '</span></li>'] |
| ]], |
| ['text', '</ul>'] |
| ] |
mustache
库底层重点要做两个事情:
将模板字符串编译为token形式
将tokens结合数据,解析为dom字符串
# 使用 webpack 和 webpack-dev-server 构建
- 模块化打包工具有 webpack (webpack-dev-server)、rollup、Parcel 等
- mustache
官方库使用rollup进行模块化打包
,而我们使用今天使用 webpack (webpack-dev-server)进行模块化打包
,这是因为 webpack (webpack-dev-server) 能让我们更方便地在 浏览器中 (而不是node.js环境中)
实时调式程序,相比 node.js 控制后台, 览器控制台更好用
,比如能够点击展开数组地每项
- 生成库是
UMD
的,这意味着它可以同时在 node.js 环境中使用,也可以在浏览器环境中使用。实现 UMD 不难,只需要一个 通用头
即可
# webpack 所需配置文件
| { |
| "name": "mustache", |
| "version": "1.0.0", |
| "description": "", |
| "main": "index.js", |
| "scripts": { |
| "dev": "webpack-dev-server" |
| }, |
| "keywords": [], |
| "author": "", |
| "license": "ISC", |
| "devDependencies": { |
| "webpack": "^4.44.2", |
| "webpack-cli": "^3.3.12", |
| "webpack-dev-server": "^3.11.0" |
| } |
| } |
| const path = require('path') |
| |
| module.exports = { |
| |
| mode: 'development', |
| |
| entry: './src/index.js', |
| |
| output: { |
| filename: 'bundle.js' |
| }, |
| |
| devServer: { |
| hot: true, |
| |
| contentBase: path.join(__dirname), |
| |
| compress: false, |
| |
| port: 8080, |
| |
| publicPath: '/dist/' |
| } |
| } |
| <script src="/dist/bundle.js"></script> |
# Scanner
| export default class Scanner { |
| constructor(template) { |
| |
| this.pos = 0 |
| this.tail = template |
| this.template = template |
| } |
| |
| |
| scan(tag) { |
| |
| this.pos += tag.length |
| |
| |
| this.tail = this.template.substring(this.pos) |
| } |
| |
| |
| |
| scanUntil(stopTag) { |
| |
| let pos_backup = this.pos |
| |
| |
| while (!this.eos() && this.tail.indexOf(stopTag) != 0) { |
| |
| this.pos++ |
| |
| |
| this.tail = this.template.substring(this.pos) |
| } |
| |
| |
| return this.template.substring(pos_backup, this.pos) |
| } |
| |
| |
| eos() { |
| return this.pos >= this.template.length |
| } |
| } |
# parseTemplateToTokens
| import Scanner from "./Scanner" |
| import nestTokens from "./nestTokens" |
| |
| export default function parseTemplateToTokens(template) { |
| |
| const tokens = [] |
| |
| |
| const scanner = new Scanner(template) |
| |
| |
| let words = '' |
| |
| |
| function trimWords(words) { |
| return words.replace(/\s+(<)|(>)\s+/g, '$1$2') |
| } |
| |
| |
| while (!scanner.eos()) { |
| |
| words = scanner.scanUntil('{{') |
| |
| |
| if(words) tokens.push(['text', trimWords(words)]) |
| |
| |
| scanner.scan('{{') |
| |
| |
| words = scanner.scanUntil('}}') |
| |
| |
| if (words) { |
| |
| if (words[0] == '#') tokens.push(['#', trimWords(words.substring(1))]) |
| |
| |
| else if (words[0] == '/') tokens.push(['/', trimWords(words.substring(1))]) |
| |
| |
| else tokens.push(['name', words]) |
| } |
| |
| |
| scanner.scan('}}') |
| } |
| |
| |
| return nestTokens(tokens) |
| } |
# nestTokens
折叠tokens,将#与/之间的tokens整合起来,作为下标为2的项
| export default function nestTokens(tokens) { |
| |
| const nestTokens = [] |
| |
| |
| let sections = [] |
| |
| |
| let collector = nestTokens |
| |
| |
| for (let i = 0; i < tokens.length; i++){ |
| |
| let token = tokens[i] |
| |
| |
| switch (token[0]) { |
| |
| case '#': |
| |
| collector.push(token) |
| sections.push(token) |
| |
| |
| |
| collector = token[2] = [] |
| break; |
| |
| case '/': |
| |
| sections.pop() |
| |
| |
| |
| |
| |
| |
| collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens |
| break; |
| default: |
| |
| collector.push(token) |
| } |
| |
| } |
| |
| |
| return nestTokens |
| } |
# renderTemplate
| import lookup from "./lookup"; |
| import parseToken from "./parseToken"; |
| |
| export default function renderTemplate(tokens, data) { |
| |
| let resultStr = '' |
| |
| |
| for (let i = 0; i < tokens.length; i++){ |
| |
| let token = tokens[i] |
| |
| |
| switch (token[0]) { |
| |
| case 'text': |
| resultStr += token[1] |
| break; |
| |
| |
| case 'name': |
| resultStr += lookup(data, token[1]) |
| break; |
| |
| |
| case '#': |
| |
| resultStr += parseToken(token, data) |
| break; |
| } |
| } |
| |
| |
| return resultStr |
| } |
# lookup
-
在对象中寻找连续点符号的keyName属性
-
举个栗子:
| const dataObj = { |
| a: { |
| b: { |
| c: 100 |
| } |
| } |
| } |
| |
| lookup(dataObj, 'a.b.c') |
| export default function lookup(dataObj, keyName) { |
| |
| |
| if (keyName != '.' && keyName.indexOf('.') != -1) { |
| |
| let keys = keyName.split('.') |
| |
| |
| let tempObj = dataObj |
| |
| |
| |
| for (let i = 0; i < keys.length; i++) tempObj = tempObj[keys[i]] |
| |
| |
| return tempObj |
| } |
| |
| return dataObj[keyName] |
| } |
# parseToken
- 处理
tokens
数组, 结合 parseTemplateToTokens
实现递归
- 注意函数参数时 token!不是
tokens
token
是一个简单的 ['#', 'friends', []]
| import lookup from "./lookup" |
| import renderTemplate from "./renderTemplate"; |
| |
| |
| export default function parseToken(token, data) { |
| |
| const v = lookup(data, token[1]) |
| |
| |
| let resultStr = '' |
| |
| |
| for (let i = 0; i < v.length; i++){ |
| resultStr += renderTemplate(token[2], { |
| ...v[i], |
| '.': v |
| }) |
| } |
| |
| |
| return resultStr |
| } |
# index
- 将模板转字符串转化为 tokens
- 将 tokens 渲染为 DOMString 结构
- 将 DOMString 返回
| import Scanner from './Scanner.js' |
| import parseTemplateToTokens from './parseTemplateToTokens.js' |
| import renderTemplate from './renderTemplate.js' |
| |
| global.TemplateEngine = { |
| render(template, data) { |
| |
| const tokens = parseTemplateToTokens(template) |
| |
| |
| const domStr = renderTemplate(tokens, data) |
| |
| |
| return domStr |
| } |
| } |
- 总结一下:这里尽量使用了
switch
, 而没有使用 if else if
,是因为我认为 switch
效率会比 if else if
高
# 测试代码
| const templateStr = |
| `<ul> |
| {%#123;#arr}%#125; |
| <li> |
| {%#123;.}%#125; |
| </li> |
| {%#123;/arr}%#125; |
| <ul> |
| ` |
| const data = { |
| arr: ['苹果', '西瓜', '樱桃'] |
| } |
| const domStr = TemplateEngine.render(templateStr, data) |
| console.log(domStr); |
| container.innerHTML = domStr |
| <div id="container"></div> |
| <script src="./dist/bundle.js"></script> |
| <script> |
| |
| const templateStr = |
| `<ul> |
| {{#arr}} |
| <li class="a"> |
| <span>姓名:{{name}}</span> |
| <span>年龄:{{age}}</span> |
| <ol> |
| {{#freiends}} |
| <li>freiends: {{.}}</li> |
| {{/freiends}} |
| </ol> |
| </li> |
| {{/arr}} |
| <ul> |
| ` |
| |
| const data = { |
| name: 'lain', |
| age: 16, |
| arr: [ |
| { tag: 'li', name: 'lain', age: 16, freiends: ['saber', '樱岛麻衣'] }, |
| { tag: 'li', name: 'saber', age: 17, freiends: ['樱岛麻衣', '稚名真白'] }, |
| { tag: 'li', name: '樱岛麻衣', age: 16, freiends: ['稚名真白', 'lain'] }, |
| { tag: 'li', name: '稚名真白', age: 17, freiends: ['lain', 'saber'] } |
| ] |
| } |
| |
| |
| const domStr = TemplateEngine.render(templateStr, data) |
| console.log(domStr) |
| |
| |
| container.innerHTML = domStr |
| </script> |
# Mustache 源码
| <script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.1.0/mustache.js"></script> |