网站建设案例方案,wordpress主题购买网站,搭建网站是seo的入门,公司app开发收费价目表个人简介 #x1f440;个人主页#xff1a; 前端杂货铺 #x1f64b;♂️学习方向#xff1a; 主攻前端方向#xff0c;正逐渐往全干发展 #x1f4c3;个人状态#xff1a; 研发工程师#xff0c;现效力于中国工业软件事业 #x1f680;人生格言#xff1a; 积跬步…个人简介 个人主页 前端杂货铺 ♂️学习方向 主攻前端方向正逐渐往全干发展 个人状态 研发工程师现效力于中国工业软件事业 人生格言 积跬步至千里积小流成江海 推荐学习前端面试宝典 100个小功能 Vue2 Vue3 Vue2/3项目实战 Node.js实战 Three.js 个人推广每篇文章最下方都有加入方式旨在交流学习资源分享快加入进来吧 文章目录 前言将数据变为视图纯DOM法数组的 join 方法ES6 的反引号法 mustache 基本使用mustache 核心理念手写 mustache搭建环境手撕 mustache 总结 前言
知其然知其所以然本篇文档我们来学习 Vue 源码之 mustache 模板引擎。
将数据变为视图
纯DOM法
纯 DOM 方法的关键点在于使用 createElement 创建节点和使用 appendChild 向节点的子节点列表的末尾添加新的子节点。
!doctype html
html langen
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1titleDocument/title
/head
style.hd {color: orange;}.bd {color: skyblue;}
/style
body
ul idlist
/ul
script// 定义人的基本信息const arr [{name: 小明, age: 30, gender: male},{name: 小小, age: 20, gender: female},{name: 小山, age: 10, gender: male},]// 获取 ulconst list document.getElementById(list);// 循环人的基本信息数组把信息构建至页面中for (let i 0; i arr.length; i) {// 创建li标签let oLi document.createElement(li);// 创建hd这个div存放 “xx的基本信息”let hdDiv document.createElement(div);hdDiv.className hd;hdDiv.innerText arr[i].name 的基本信息;// 创建bd这个div存放人的详细基本信息let bdDiv document.createElement(div);bdDiv.className bd;// 人的基本信息姓名、年龄、性别放入 p 标签中let p1 document.createElement(p);p1.innerText 姓名 arr[i].name;bdDiv.appendChild(p1);let p2 document.createElement(p);p2.innerText 年龄 arr[i].age;bdDiv.appendChild(p2);let p3 document.createElement(p);p3.innerText 性别 arr[i].gender;bdDiv.appendChild(p3);oLi.appendChild(hdDiv);hdDiv.appendChild(bdDiv);list.appendChild(oLi);}
/script
/body
/html数组的 join 方法
数组 join 方法关键在于字符串的拼接。
!doctype html
html langen
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1titleDocument/title
/head
body
ul idlist
/ul
script// 定义人的基本信息const arr [{name: 小明, age: 30, gender: male},{name: 小小, age: 20, gender: female},{name: 小山, age: 10, gender: male},]const list document.getElementById(list);for (let i 0; i arr.length; i) {list.innerHTML [li, div classhd/div arr[i].name 的信息/div, div classbd, p姓名 arr[i].name /p, p年龄 arr[i].age /p, p性别 arr[i].gender /p, /div,/li].join()}
/script
/body
/htmlES6 的反引号法
ES6 反引号法可以更优雅的把 html 标签写入字符串中。
!doctype html
html langen
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1titleDocument/title
/head
body
ul idlist/ul
/body
script// 定义人的基本信息const arr [{name: 小明, age: 30, gender: male},{name: 小小, age: 20, gender: female},{name: 小山, age: 10, gender: male},]const list document.getElementById(list);for (let i 0; i arr.length; i) {list.innerText lidiv classhd${arr[i].name}的基本信息/divdiv classbdp姓名${arr[i].name}/pp性别${arr[i].gender}/pp年龄${arr[i].age}/p/div/li}
/script
/htmlmustache 基本使用
mustache.js 是一个简单强大的 JavaScript 模板引擎使用它可以简化在 js 代码中的 html 编写。
基本使用
// 初始化
npm init -y// 安装 mastache
npm install mastachemustache 的使用非常简单 使用花括号 {{#xxx}} 开始使用 花括号 {{/xxx}} 结束中间包裹着 key 值最后再使用 mustache.render(templateStr, data)完成数据的渲染。
!DOCTYPE html
html langenheadmeta charsetUTF-8 /meta nameviewport contentwidthdevice-width, initial-scale1 /titleDocument/title/headbodydiv idcontainer/div!-- 模版 --script typetext/template idmyTemplateul{{#arr}}lidiv classhd{{name}}的基本信息/divdiv classbdp姓名{{name}}/pp性别{{gender}}/pp年龄{{age}}/p/div/li{{/arr}}/ul/scriptscript typemoduleimport mustache from ./node_modules/mustache/mustache.mjs;// 获取模版const templateStr document.getElementById(myTemplate).innerHTML;// 定义数据const data {arr: [{ name: 小明, age: 30, gender: male },{ name: 小小, age: 20, gender: female },{ name: 小山, age: 10, gender: male },],};const domStr mustache.render(templateStr, data);const container document.getElementById(container);container.innerHTML domStr;/script/body
/htmlmustache 核心理念
tokens 是一个 JS 的嵌套数组是模板字符串的 JS 表示。
对于一维模板字符串及对应 tokens如下 对于二维模板字符串及 tokens 如下 mustache 库底层重点要做的两件事情
将模板字符串编译为 tokens 形式将 tokens 结合数据解析为 dom 字符串 手写 mustache
搭建环境
总体目录如下 新建一个文件夹打开终端输入以下命令后回车以初始化项目。
npm init -y安装依赖。
npm install安装 webpack、webpack-dev-server、webpck-cli。
npm i webpack4 webpack-dev-server3 webpack-cli3修改 package.json 文件的 dev 内容如下更换项目启动方式。
{name: templateengine,version: 1.0.0,main: index.js,scripts: {dev: webpack-dev-server},keywords: [],author: ,license: ISC,description: ,devDependencies: {webpack: ^4.47.0,webpack-cli: ^3.3.12,webpack-dev-server: ^3.11.3}
}手动创建 webpack.config.js 文件并进行如下配置。
const path require(path);module.exports {// 模式开发mode: development,// 入口entry: ./src/index.js,// 打包到什么文件output: {filename: bundle.js,},// 配置一下 webpack-dev-serverdevServer: {// 静态文件根目录contentBase: path.join(__dirname, www),// 压缩compress: false,// 端口号port: 8080,// 虚拟打包到路径bundle.js文件没有真正的生成publicPath: /xuni/,},
};手动创建 www 文件及 www/index.html 文件添加如下内容。
!DOCTYPE html
html langenheadmeta charsetUTF-8 /meta nameviewport contentwidthdevice-width, initial-scale1.0 /titleDocument/title/headbodyh1你好这里是前端杂货铺!/h1script src/xuni/bundle.js/script/body
/html手动创建 src 文件及 src/index.js 文件添加如下内容。
console.log(hello);OK此时基本配置已完成接下来我们打开终端键入 npm run dev 命令回车然后访问 8080 端口。 手撕 mustache
手撕 mustache 源码地址
接下来我们来手写简易版的 mustache体验一下模版引擎的设计巧妙之处。
代码文件结构如下 在 index.html 文件中我们编写 模版字符串 和 数据并使用 TemplateEngine.render(模板字符串, 数据) 来获取 dom 数据之后添加到 div 容器中。
!DOCTYPE html
html langenheadmeta charsetUTF-8 /meta nameviewport contentwidthdevice-width, initial-scale1.0 /titleDocument/title/headbodydiv idcontainer/divscript src/xuni/bundle.js/scriptscript// 模板字符串const templateStr divul{{#students}}li学生{{name}}的爱好是ol{{#hobbies}}li{{.}}/li{{/hobbies}}/ol/li{{/students}}/ul/div;// 数据const data {students: [{name: 张三,hobbies: [敲代码, 乒乓球, 健身],},{name: 李四,hobbies: [游泳, 跑步],},{name: 王五,hobbies: [唱歌, 跳舞],},],};// 获取 dom 数据const domStr TemplateEngine.render(templateStr, data);const container document.getElementById(container);container.innerHTML domStr;/script/body
/html在 src/index.js 文件中全局提供模板引擎对象渲染方法的入参为定义的 模板字符串 和 数据通过 parseTemplateToTokens 解析字符串为 tokens通过 renderTemplate 渲染 tokens 数组为 dom 字符串。
import parseTemplateToTokens from ./parseTemplateToTokens;
import renderTemplate from ./renderTemplate;
// 全局提供模版引擎对象
window.TemplateEngine {// 渲染方法render(templateStr, data) {// 模版字符串变为tokens数组const tokens parseTemplateToTokens(templateStr);// 调用renderTemplate方法渲染tokens数组为dom字符串const domStr renderTemplate(tokens, data);// 返回组织好的 dom 字符串return domStr;},
};通过上面两个核心方法我们将获得如下的数据。 扫描器类 Scanner用于扫描传入的模板字符串。当遇到 {{ 和 }} 内容时停止扫描并返回停止扫描之前路过的内容。
/*** 扫描器类*/
export default class Scanner {constructor(templateStr) {this.templateStr templateStr;// 指针this.pos 0;// 尾巴初始值为传入的字符串this.tail templateStr;}// 走过指定的内容没有返回值用于过滤掉 {{ 和 }} 这样的内容scan(tag) {// 如果捕获到了tag就让指针后移tag的长度不会做任何处理只是过滤掉if (this.tail.indexOf(tag) 0) {// {{ 和 }} 的长度为2就让指针后移2位this.pos tag.length;// 改变尾巴this.tail this.templateStr.substring(this.pos);}}// 让指针进行扫描直到遇见指定内容结束并且返回结束之前路过的文字scanUtil(stopTag) {// 记录执行本方法时pos的值const pos_backup this.pos;// 当尾巴的开头不是stopTag的时候就说明还没有扫描到stopTagwhile (!this.eos() this.tail.indexOf(stopTag) ! 0) {this.pos;// 改变尾巴为当前指针这个字符开始到最后的全部字符this.tail this.templateStr.substring(this.pos);}// 返回扫描过的内容不包括stopTagreturn this.templateStr.substring(pos_backup, this.pos);}// 指针是否已经到头eos() {return this.pos this.templateStr.length;}
}parseTemplateToTokens.js 文件用于将模板字符串变为初步的 tokens 数组。在 Scanner 的加持下按照一定的规则 “制作” tokens。
import Scanner from ./Scanner;
import nestTokens from ./nestTokens;
/*** 将模板字符串变为tokens数组*/
export default function parseTemplateToTokens(templateStr) {// 存储 tokensconst tokens [];// 创建扫描器扫描模板字符串const scanner new Scanner(templateStr);let words;// 让扫描器工作当扫描器的指针没有到头时while (!scanner.eos()) {// 扫描字符直到遇到 {{ 时结束因为 {{ 之前的内容是text类型words scanner.scanUtil({{);// 如果扫描的内容不为空就作为text类型存入tokensif (words ! ) {tokens.push([text, words]);}// 过滤 {{指针后移2位因为 {{ 并没有实质作用过滤掉就好scanner.scan({{);// 再次扫描字符直到遇到 }} 时结束因为 {{ 和 }} 之间的内容是需要处理的name类型words scanner.scanUtil(}});// 根据再次扫描的内容的第一个字符判断应该 push 到 tokens 中的类型if (words ! ) {// 按照第一个字符组织tokens# 为遍历的开始/ 为遍历的结束if (words[0] #) {tokens.push([#, words.substring(1)]);} else if (words[0] /) {tokens.push([/, words.substring(1)]);} else {tokens.push([name, words]);}}scanner.scan(}});}// 返回折叠收集的tokensreturn nestTokens(tokens);
}要进行进一步的 tokens 处理多层级就需要 nestTokens.js 来大显身手了。
/*** 折叠tokens将#和/之间的tokens能够整合起来作为它下标为2的项*/
export default function nestTokens(tokens) {// 结果数组需要最后返回的const nestedTokens [];// 栈结构用于保存 # 的tokenconst sections [];// 收集器引用默认指向nestedTokenslet collector nestedTokens;// 遍历在 parseTemplateToTokens 中组织好的 tokensfor (let i 0; i tokens.length; i) {const token tokens[i];switch (token[0]) {// 第一个字符如果为 #就往收集器中放入这个token并且入栈case #:// 收集器中放入这个tokencollector.push(token);// 入栈sections.push(token);// 收集器此时要换位这个token的下标为2的项因为之后需要push的是它的子项collector token[2] [];break;case /:// 出栈sections.pop();// 改变收集器为栈结构末尾的数组collector sections.length 0 ? sections[sections.length - 1][2] : nestedTokens;break;default:// 收集器中放入这个tokencollector.push(token);}}return nestedTokens;
}下面我们要做的就是把最终的 tokens 转为 dom 字符串了。
编写 lookUp.js 文件用于在对象中查找 key 值。如 dataObj {a: {b: {c: 100}}}keyName a.b.c那么调用该方法将返回100。
/*** 在 dataObj 对象中查找 keyName 的值* 如dataObj {a: {b: {c: 100}}}keyName a.b.c返回100* param {*} dataObj 对象* param {*} keyName 要查找的key* returns*/
export default function lookUp(dataObj, keyName) {// 查看 keyName 中是否有 ., 但不能是 .if (keyName ! . keyName.indexOf(.) ! -1) {// 以 . 分隔例如 a.b.c先拆分为 [a, b, c]const keys keyName.split(.);// 临时变量用于逐层查找let temp dataObj;// 每找一层就把它设置为新的临时变量for (let i 0; i keys.length; i) {temp temp[keys[i]];}return temp;}// 没有.直接返回return dataObj[keyName];
}下面我们编写 parseArray.js 和 renderTemplate.js 文件分别用于处理数组和渲染模板字符串。
import lookUp from ./lookUp;
import renderTemplate from ./renderTemplate;
/*** 处理数组结合 renderTemplate 方法实现递归* token[#, students, [xxx]]** 这个函数要递归调用 renderTemplate 方法* 调用次数取决于 data 中的数组长度*/
export default function parseArray(token, data) {// 得到数据整体data中这个数组要使用的数据const v lookUp(data, token[1]);// 结果字符串let resultStr ;// 遍历数据而不是tokens。数组中的数据有几条就遍历几次for (let i 0; i v.length; i) {// 拼接在此处要补一个 “.” 属性resultStr renderTemplate(token[2], {...v[i], // 简单数组直接展开.: v[i], // 为了处理 {{.}} 的情况});}return resultStr;
}import lookUp from ./lookUp;
import parseArray from ./parseArray;/*** 渲染模版* param {*} tokens* param {*} data* returns*/
export default function renderTemplate(tokens, data) {let resultStr ;for (let i 0; i tokens.length; i) {const token tokens[i];if (token[0] text) {resultStr token[1];} else if (token[0] name) {// name类型获取值resultStr lookUp(data, token[1]);} else if (token[0] #) {// 递归处理下标为2·的数组resultStr parseArray(token, data);}}return resultStr;
}此时编码完毕打开浏览器查看渲染内容如下至此手撕完毕 总结
本篇文章我们首先认识了几种将模型变为视图的方法。
之后学习了Mustache 的基本使用和核心理念。
通过手写 Mustache 模板引擎我们 深入理解 了其 工作原理和实现细节。虽然手写版本可能不如官方实现那么完善但 这个过程让我对模板引擎的设计有了更深刻的认识。Mustache 的简洁性和无逻辑特性使其成为一种非常灵活的模板引擎适用于各种场景。
如果你也对模板引擎的实现感兴趣我建议你尝试手写一个简化版的 Mustache 或类似的模板引擎。这不仅能够加深你对前端技术的理解还能提升你的编程能力。
好啦本篇文章到这里就要和大家说再见啦祝你这篇文章阅读愉快你下篇文章的阅读愉快留着我下篇文章再祝 参考资料 mustache 百度百科Vue源码解析之mustache模板引擎尚硅谷