当前位置:   article > 正文

vue源码-mustache模板引擎分析(四)-手写实现mustache库_mustache npm

mustache npm

一.构建环境

(1)新建名为TemplateEngine的文件夹,然后
      

npm init  

npm i

npm i webpack@4 -D

npm i webpack-cli@3 -D

npm i webpack-dev-server@3 -D

使用webpack和webpack-dev-server构建 

-webpack侧重开发体验,结合webpack-dev-server实时更新

-nodeis调试控制台不太好用

-rollup更擅长于最终把几个js文件打包一起

-生成库UMD,同时在nodejs.js环境和浏览器环境中使用,实现UMD,只需一个“通用头”

 新建一个webpack.config.js文件

  1. const path = require('path')
  2. module.exports = {
  3. //模式,开发
  4. mode:'development',
  5. //入口
  6. entry:'./src/index.js',
  7. //打包到什么文件
  8. output:{
  9. filename:'bundle.js'
  10. },
  11. //配置一下webpack-dev-server
  12. devServer:{
  13. //静态文件根目录
  14. contentBase:path.join(__dirname,'www'),
  15. //不压缩
  16. compress:false,
  17. //端口号
  18. port:8080,
  19. //虚拟打包的路径,bundle.js文件没有真正的生成
  20. publicPath:'/xuni/'
  21. }
  22. }

接着在package.json文件改动:

  1. "scripts": {
  2. "dev": "webpack-dev-server"
  3. },

刚刚看到关于学习源码的建议:

(1)源码思想要借鉴,而不要抄袭,要能够发现源码中书写的精彩的地方。

(2)将独立的功能拆写为独立的js文件中完成,通常是一个独立的类,每个单独的功能必须能独立的“单元测试”。

(3)应该围绕中心功能,先把主干完成,然后修剪枝叶

(4)功能并不需要一步到位,功能的扩展要一步步完成,有的非核心功能甚至不需要实现


二.将模板字符串变成tokens

 举个简单的例子~

我买了一个{{thing}},好{{mood}}

主要思想:

设置一个指针,从下标为0开始扫描,碰到 {{ 则停止扫描并返回之前经过的文字【“我买了一个”】,接着又继续扫描碰到 }} 则停止扫描并返回之前经过的文字【“thing”】,又继续干活碰到 {{ 停止扫描并返回之前经过的文字【“,好”】,接着又继续扫描碰到 }} 则停止扫描并返回之前经过的文字【“mood”】,接下来以此类推。

新建一个Scanner.js文件,实现一个Scanner类【扫描器类】,类里实现两个方法:

一个是scanUtils方法,主要功能:让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字;

另一个是scan方法,主要功能:走过指定内容,没有返回值

三个属性:

 templateStr 接收过来的模板字符串

 pos 起始位置(可以理解为指针)

tail 扫描过后的字符串,一开始设置为templateStr

  1. export default class Scanner{
  2. constructor(templateStr){
  3. this.templateStr = templateStr;
  4. this.pos = 0;
  5. this.tail = this.templateStr
  6. }
  7. //功能弱,就是走过指定内容,没有返回值
  8. scan(tag){
  9. if(this.tail.indexOf(tag)==0){
  10. this.pos+=tag.length
  11. this.tail = this.templateStr.substring(this.pos)
  12. }
  13. }
  14. //让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
  15. scanUtils(stopTag){
  16. let pos_backup = this.pos
  17. //当尾巴的开头不是stopTag的时候,就说明还没有扫描stopTag
  18. while(this.tail.indexOf(stopTag)!=0 && !this.eos()){
  19. this.pos++
  20. //改变尾巴为从当前指针这个字符开始,到后面全部字符
  21. this.tail = this.templateStr.substring(this.pos)
  22. }
  23. return this.templateStr.substring(pos_backup,this.pos)
  24. }
  25. //判断指针是否到头,返回布尔值
  26. eos(){
  27. return this.pos>=this.templateStr.length
  28. }
  29. }


三.生成tokens数组

新建一个parseTemplateToTokens.js文件,向外暴露一个函数,函数名为parseTemplateToTokens,接收的参数为templateStr

var tokens = []

var words;

扫描器类已经准备好,接下来就让扫描器干活!

var scanner = new Scanner(templateStr)

思想:

当this.pos<this.templateStr.length时,先执行scanner.scanUtils('{{'),返回一个word用于收集开始标记出现之前的文字;接着判断文字内容不为空的话就push到tokens数组里,push的内容为['text',words],然后过双大括号scanner.scan("{{"),执行到这一步,意味着咱们进入到了 {{ 里,接着 words = scanner.scanUtils('}}'),这里的words就是{{}}中间的东西;接着判断一下首字符words[0],如果为#,tokens.push(['#',words.substring(1)]);如果为/,tokens.push(['/',words.substring(1)]);否则就返回tokens.push(['name',words]),接着过掉双大括号scanner.scan('}}'),最后就返回token。

  1. import Scanner from './Scanner'
  2. import nestTokens from './nestTokens'
  3. export default function parseTemplateToTokens(templateStr){
  4. var tokens = [];
  5. var words ;
  6. //创建扫描器
  7. var scanner = new Scanner(templateStr)
  8. //让扫描器工作
  9. while(!scanner.eos()){
  10. //收集开始标记出现之前的文字
  11. words = scanner.scanUtils('{{')
  12. //存起来
  13. if(words!=''){
  14. tokens.push(['text',words])
  15. }
  16. //过双大括号
  17. scanner.scan("{{")
  18. //收集开始标记出现之前的文字
  19. words = scanner.scanUtils('}}')
  20. //这里的words就是{{}}中间的东西,接着判断一下首字符
  21. if(words!=""){
  22. if(words[0]=='#'){
  23. //存起来,从下标为1的项开始存,因为下标为0的项是#
  24. tokens.push(['#',words.substring(1)])
  25. }else if(words[0]=='/'){
  26. //存起来,从下标为1的项开始存,因为下标为0的项是/
  27. tokens.push(['/',words.substring(1)])
  28. }else{
  29. tokens.push(['name',words])
  30. }
  31. }
  32. //过双大括号
  33. scanner.scan('}}')
  34. }
  35. return tokens;
  36. //return nestTokens(tokens);
  37. }

四.将零散的tokens嵌套起来

新建一个nestTokens.js,向外暴露一个函数,函数名为nestTokens,接收的参数为tokens

 var nestTokens = [];

  var sections = [];

  var collector = nestTokens;//这一步是关键,利用引用类型传递

思想:

利用栈(sections)先进后出的特点,遍历所有的tokens,

遇到一个 #,就把当前 token 放入这个栈中,让 collector 指向这个 token 的第三个元素。遇到下一个 # 就把新的 token 放入栈中,collector 指向新的 token 的第三个元素。遇到 / 就把栈顶的 token 移出栈,collector 指向移出完后的栈顶 token。

利用了栈的先进后出的特点,保证了遍历的每个 token 都能放在正确的地方,也就是 collector 都能指向正确的地址。

  1. export default function nestTokens(tokens){
  2. var nestTokens = [];
  3. var sections = [];
  4. var collector = nestTokens;
  5. var token;
  6. for(let i=0;i<tokens.length;i++){
  7. token = tokens[i];
  8. switch(token[0]){
  9. case '#':
  10. collector.push(token);
  11. sections.push(token);
  12. collector = token[2] = [];
  13. break;
  14. case '/':
  15. sections.pop();
  16. collector = sections.length>0?sections[sections.length-1][2]:nestTokens
  17. break;
  18. default:
  19. collector.push(token)
  20. break;
  21. }
  22. }
  23. return nestTokens
  24. }

五.tokens结合数据,解析为dom字符串

上面已经将模板字符串编译为tokens,接下来让数据与tokens结合,新建一个文件名叫renderTemplate.js,向外暴露renderTemplate函数,接收的参数为tokens和data。

在index.js中

  1. //全局提供TemplateEngine对象
  2. window.TemplateEngine = {
  3. //渲染方法
  4. render(templateStr,data){
  5. //让模板字符串变为tokens数组
  6. var tokens = parseTemplateToTokens(templateStr)
  7. //让tokens数组变为dom字符串
  8. var result = renderTemplate(tokens,data)
  9. console.log(result)
  10. return result
  11. }
  12. }

在www/index.html中

  1. var str = TemplateEngine.render(template,data);
  2. var box = document.getElementById("box")
  3. box.innerHTML = str

思想:

 遍历tokens,例如:['text',"<li>学生"],若token开头的第一个字符是text,拼接token[1]

例如:['name',"name"],若token开头的第一个字符是name,拼接data[token[1]],但在这里要注意的是,如果在模板字符串中使用{{a.b.c}},data['a.b.c']读不出来,会显示undefined;

所以在这里写一个函数,可以在dataObj对象中寻找用连续点符号.的keyName属性,比如

  1. dataObj = {
  2.    a:{
  3.     b:{
  4.          c:100
  5.         }
  6.      }
  7.   }
  8.   lookup(dataObj,'a.b.c') ---> 100
  1. export default function lookup(dataObj,keyName){
  2. var temp ;
  3. var keys;
  4. if(keyName.indexOf(".")!=0&&keyName!='.'){
  5. keys = keyName.split(".");//['a','b','c']
  6. temp = dataObj
  7. for(let i=0;i<keys.length;i++){
  8. temp = temp[keys[i]]
  9. }
  10. return temp
  11. }
  12. return dataObj[keyName]
  13. }

例如:['#',"students",[...]],若token开头的第一个字符是#,我们就是去解析当前的token,要再次递归调用 renderTemplate 函数。

这里新定义了一个 parseArray 函数来处理,接收的参数是当前的token和data,返回字符串。通过lookup(data,token[1])得到整体数据data中这个数组要使用的部分,接着遍历这个数组,而不是遍历token,数组中的数据有几条就遍历几次。再次递归调用renderTemplate(),传入第一个参数是token[2],第二个参数是当前的v[i],但在这里还要考虑的一点是如果data只是一个简单循环数组,在模板上通过{{.}}显示到页面,那么第二个参数传的就是 ".":v[i]

  1. import lookup from './lookup'
  2. import renderTemplate from './renderTemplate'
  3. /**
  4. * 处理数组,结合renderTemplate实现递归
  5. * 注意:这个函数接收的参数是token而不是tokens
  6. * token为['#', 'arr',[...]]
  7. * data[arr]--->数据
  8. */
  9. export default function parseArray(token,data){
  10. //得到整体数据data中这个数组要使用的部分
  11. let v = lookup(data,token[1])
  12. //结果字符串
  13. let resultStr = ""
  14. //遍历v数组,v一定是数组
  15. //注意:下面这个循环可能是整个包中最难思考的一个循环
  16. //它是遍历数据,而不是遍历token。数组中的数据有几条,就要遍历几条
  17. for(let i=0;i<v.length;i++){
  18. //若循环简单数组时
  19. resultStr += renderTemplate(token[2],{
  20. ".":v[i],
  21. ...v[i]
  22. })
  23. }
  24. return resultStr;
  25. }

最后!renderTemplate函数:

  1. import lookup from './lookup'
  2. import parseArray from './parseArray'
  3. export default function renderTemplate(tokens,data){
  4. var str = ''
  5. var token;
  6. for(let i=0;i<tokens.length;i++){
  7. token = tokens[i];
  8. if(token[0]=='text'){
  9. str+=token[1]
  10. }else if(token[0]=='name'){
  11. str+=lookup(data,token[1]);
  12. }else if(token[0]=='#'){
  13. str+=parseArray(token,data)
  14. }
  15. }
  16. return str
  17. }

一个简单的模板引擎源码就写出来啦!!!

源码:https://github.com/crush12132/study_TemplateEngine

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/610791
推荐阅读
相关标签
  

闽ICP备14008679号