一篇文章教你如何写成Strview.js之源码剖析

前言

前段时间我自己开发了一款Strview.js,它是一个可以将字符串转换为视图的JS库。什么意思呢?就像下面这段代码:

成都创新互联公司长期为上千家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为万全企业提供专业的成都网站建设、做网站,万全网站改版等技术服务。拥有10年丰富建站经验和众多成功案例,为您定制开发。

 
 
 
 
  1.  
  2.  
  3.  
  4.  
  5.      
  6.      
  7.      
  8.     Strview.js 
  9.  
  10.  
  11.  
  12.     
 
  •      
  •      
  •  
  •  
  •  
  • 显示如下页面:

    你会看到页面上显示了一个Hello World字样,而我们看到HTML代码中除了一个ID名是app标签之外,其他标签并没有,更没有Hello World文本。这时,继续往下看,在JS代码中,我们引入了Strview.js,并且我们调用了它一个createView方法,最后传入了一个对象。我们在对象中发现了Hello World字符串,并且我们在template属性中看到它多所对应的值是一个标签,就是这个标签

    {msg}

    ,另外,里面我们会看到使用{}包裹的msg字符。与data对象中的msg属性相对应,正好它的值为Hello World。我们现在改变下msg属性对应的值来看下页面是否发生改变。

    果然,发生了改变,所以我们知道Strview.js就是这么将字符串转换为视图的。

    这里,我们只是简单介绍了Strview.js的简单用法,如果想继续了解其他用法的话,可以去Strview.js中文官网:

    https://www.maomin.club/site/strviewjs/zh

    下面的内容呢,我们将看下Strview.js源码,看它是如何实现的。

    剖析源码

    本篇分析Strview.js版本为1.9.0

    首先,我们获取到源码,这里我们使用生产环境下的Strview.js,也就是上面实例中的这个地址:

    https://cdn.jsdelivr.net/npm/strview@1.9.0/dist/strview.global.js

    我们,先大体看下源码,加上空行,源码一共125行。不压缩的话,仅仅4kb。

     
     
     
     
    1. var Strview = (function (exports) { 
    2.     'use strict'; 
    3.  
    4.     // global object 
    5.     const globalObj = { 
    6.         _nHtml: [], 
    7.         _oHtml: [], 
    8.         _el: null, 
    9.         _data: null, 
    10.         _template: null, 
    11.         _sourceTemplate: null 
    12.     }; 
    13.  
    14.     // initialization 
    15.     function createView(v) { 
    16.         globalObj._data = v.data; 
    17.         globalObj._template = v.template; 
    18.         globalObj._sourceTemplate = v.template; 
    19.         globalObj._el = v.el; 
    20.         v.el ? document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) : console.error("Error: Please set el property!"); 
    21.     } 
    22.  
    23.     // event listeners 
    24.     function eventListener(el, event, cb) { 
    25.         document.querySelector(el).addEventListener(event, cb); 
    26.     } 
    27.  
    28.     // processing simple values 
    29.     function ref() { 
    30.         return new Proxy(globalObj._data, { 
    31.             get: (target, key) => { 
    32.                 return target[key] 
    33.             }, 
    34.             set: (target, key, newValue) => { 
    35.                 target[key] = newValue; 
    36.                 setTemplate(); 
    37.                 return true; 
    38.             } 
    39.         }) 
    40.     } 
    41.  
    42.     // reactiveHandlers 
    43.     const reactiveHandlers = { 
    44.         get: (target, key) => { 
    45.             if (typeof target[key] === 'object' && target[key] !== null) { 
    46.                 return new Proxy(target[key], reactiveHandlers); 
    47.             } 
    48.             return Reflect.get(target, key); 
    49.         }, 
    50.         set: (target, key, value) => { 
    51.             Reflect.set(target, key, value); 
    52.             setTemplate(); 
    53.             return true 
    54.         } 
    55.     }; 
    56.  
    57.     // respond to complex objects 
    58.     function reactive() { 
    59.         return new Proxy(globalObj._data, reactiveHandlers) 
    60.     } 
    61.  
    62.     // update the view 
    63.     function setTemplate() { 
    64.         const oNode = document.querySelector(globalObj._el); 
    65.         const nNode = toHtml(render(globalObj._sourceTemplate)); 
    66.         compile(oNode, 'o'); 
    67.         compile(nNode, 'n'); 
    68.         if (globalObj._oHtml.length === globalObj._nHtml.length) { 
    69.             for (let index = 0; index < globalObj._oHtml.length; index++) { 
    70.                 const element = globalObj._oHtml[index]; 
    71.                 element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent); 
    72.             } 
    73.         } 
    74.     } 
    75.  
    76.     // judge text node 
    77.     function isTextNode(node) { 
    78.         return node.nodeType === 3; 
    79.     } 
    80.  
    81.     // compile DOM 
    82.     function compile(node, type) { 
    83.         const childNodesArr = node.childNodes; 
    84.         for (let index = 0; index < Array.from(childNodesArr).length; index++) { 
    85.             const item = Array.from(childNodesArr)[index]; 
    86.             if (item.childNodes && item.childNodes.length) { 
    87.                 compile(item, type); 
    88.             } else if (isTextNode(item) && item.textContent.trim().length !== 0) { 
    89.                 type === 'o' ? globalObj._oHtml.push(item) : globalObj._nHtml.push(item); 
    90.             } 
    91.         } 
    92.     } 
    93.  
    94.     // string to DOM 
    95.     function toHtml(domStr) { 
    96.         const parser = new DOMParser(); 
    97.         return parser.parseFromString(domStr, "text/html"); 
    98.     } 
    99.  
    100.     // template engine 
    101.     function render(template) { 
    102.         const reg = /\{(.+?)\}/; 
    103.         if (reg.test(template)) { 
    104.             const key = reg.exec(template)[1]; 
    105.             if (globalObj._data.hasOwnProperty(key)) { 
    106.                 template = template.replace(reg, globalObj._data[key]); 
    107.             } else { 
    108.                 template = template.replace(reg, eval(`globalObj._data.${key}`)); 
    109.             } 
    110.             return render(template) 
    111.         } 
    112.  
    113.         return template; 
    114.     } 
    115.  
    116.     // exports 
    117.     exports.createView = createView; 
    118.     exports.eventListener = eventListener; 
    119.     exports.reactive = reactive; 
    120.     exports.ref = ref; 
    121.  
    122.     Object.defineProperty(exports, '__esModule', { value: true }); 
    123.  
    124.     return exports; 
    125. }({})); 

    首先,我们会看到最外层定义了一个Strview变量,暴露在外面,并将一个立即执行函数(IIFE)赋予这个变量。

    我们先来看下这个立即执行函数。

     
     
     
     
    1. var Strview = (function (exports) { 
    2. // ... 
    3.  
    4. }({})); 

    函数中需要传一个形参exports,并且又立即传入一个空对象。

    然后,我们来看下函数内的内容。

    我们会看到函数中有很多变量与函数方法,那么我们就按功能来分析。

    首先,我们看到了一个全局对象,全局对象中分别定义了几个属性。这样做是为了减少全局变量污染,JS可以随意定义保存所有应用资源的全局变量,但全局变量可以削弱程序灵活性,增大了模块之间的耦合性。最小化使用全局变量的一个方法是在你的应用中只创建唯一一个全局变量。

     
     
     
     
    1. // global object 
    2. const globalObj = { 
    3.     _nHtml: [], // 存放新DOM数组 
    4.     _oHtml: [], // 存放旧DOM数组 
    5.     _el: null, // 挂载DOM节点 
    6.     _data: null, // 存放数据 
    7.     _template: null, // 模板字符串 
    8.     _sourceTemplate: null // 源模板字符串 
    9. }; 

    然后,我们接着看初始化阶段,这个阶段是将模板字符串转换成视图。

     
     
     
     
    1. // initialization 
    2. function createView(v) { 
    3.     globalObj._data = v.data; 
    4.     globalObj._template = v.template; 
    5.     globalObj._sourceTemplate = v.template; 
    6.     globalObj._el = v.el; 
    7.     v.el ? document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template)) : console.error("Error: Please set el property!"); 

    我们看到这个createView方法传入了一个参数,也就是我们之前传入的那个对象:

     
     
     
     
    1. Strview.createView({ 
    2.         el: "#app", 
    3.         data: { 
    4.             msg: 'Hello World' 
    5.         }, 
    6.         template: `

      {msg}

      `, 
    7.     }); 

     我们看到传入的对象中的属性分别赋给全局对象globalObj。在最后一行中通过判断v.el是否是真值,如果是就执行这行代码:

     
     
     
     
    1. document.querySelector(v.el).insertAdjacentHTML("beforeEnd", render(globalObj._template))  

    这行代码执行了insertAdjacentHTML()方法,这个方法在MDN上是这样解释它的。

    insertAdjacentHTML()方法传入的第二个参数是是要被解析为HTML或XML元素,并插入到DOM树中的DOMString,render(globalObj._template)这个方法就是返回的DOMString。

    如果是假,就执行console.error("Error: Please set el property!"),在浏览器上输出错误。

    既然这个用到了render(globalObj._template)这个方法,那么我们下面来看下。

     
     
     
     
    1. // template engine 
    2. function render(template) { 
    3.     const reg = /\{(.+?)\}/; 
    4.     if (reg.test(template)) { 
    5.         const key = reg.exec(template)[1]; 
    6.         if (globalObj._data.hasOwnProperty(key)) { 
    7.             template = template.replace(reg, globalObj._data[key]); 
    8.         } else { 
    9.             template = template.replace(reg, eval(`globalObj._data.${key}`)); 
    10.         } 
    11.         return render(template) 
    12.     } 
    13.  
    14.     return template; 

    首先,这个render(template)方法传入了一个参数,第一个参数是模板字符串。

    然后,我们进入这个方法中看一下,首先,我们定义了正则/\{(.+?)\}/,用于匹配模板字符串中的{}中的内容。如果匹配为真,就进入这个逻辑:

     
     
     
     
    1. const key = reg.exec(template)[1]; 
    2. if (globalObj._data.hasOwnProperty(key)) { 
    3.     template = template.replace(reg, globalObj._data[key]); 
    4. } else { 
    5.     template = template.replace(reg, eval(`globalObj._data.${key}`)); 
    6. return render(template) 

    我们在第一行代码中看到了这行代码const key = reg.exec(template)[1],这里使用的是reg.exec()方法,MDN这样解释它:

    所以,通过这个方法我们取到了模板字符串中的{}中的内容,它一般是我们存取数据_data中的属性。首先,我们判断globalObj._data对象中是否有这个key,如果有我们就使用字符串替换方法replace来把对应的占位符key替换成所对应的值。下面接着进行递归,直到reg.test(template)返回为false。最终,render()方法返回处理后的template。

    看完render()方法,我们来看下事件处理阶段,也就是eventListener()方法。

     
     
     
     
    1. // event listeners 
    2. function eventListener(el, event, cb) { 
    3.     document.querySelector(el).addEventListener(event, cb); 

    这个方法很简单,第一个参数传入DOM选择器,第二个参数传入一个事件名,第三个参数传入一个回调函数。

    最后,我们来看下Strview.js的数据响应系统。

     
     
     
     
    1. // processing simple values 
    2. function ref() { 
    3.     return new Proxy(globalObj._data, { 
    4.         get: (target, key) => { 
    5.             return target[key] 
    6.         }, 
    7.         set: (target, key, newValue) => { 
    8.             target[key] = newValue; 
    9.             setTemplate(); 
    10.             return true; 
    11.         } 
    12.     }) 
    13.  
    14. // reactiveHandlers 
    15. const reactiveHandlers = { 
    16.     get: (target, key) => { 
    17.         if (typeof target[key] === 'object' && target[key] !== null) { 
    18.             return new Proxy(target[key], reactiveHandlers); 
    19.         } 
    20.         return Reflect.get(target, key); 
    21.     }, 
    22.     set: (target, key, value) => { 
    23.         Reflect.set(target, key, value); 
    24.         setTemplate(); 
    25.         return true 
    26.     } 
    27. }; 
    28.  
    29. // respond to complex objects 
    30. function reactive() { 
    31.     return new Proxy(globalObj._data, reactiveHandlers) 

    上面这些代码主要是reactive()、ref()这两个方法的实现。reactive()方法是针对复杂数据的处理,比如嵌套对象以及数组。ref()方法主要是针对简单数据的处理,像是原始值与单一非嵌套对象。

    它们两个都是基于Proxy代理来实现数据的拦截与响应,MDN中这样定义它。

    它们两个Proxy对象第一个参数都是我们在初始化定义的globalObj._data,第二个参数是一个通常以函数作为属性的对象。这里都定义了get()方法、set()方法,get()是属性读取操作的捕捉器,set()是属性设置操作的捕捉器。

    reactive()、ref()这两个方法实现不一样的地方是reactive()方法加上了对嵌套对象判断来实现递归。

    我们在set()方法中看到它们都调用了setTemplate()方法,下面,我们来看下这个方法。

     
     
     
     
    1. // update the view 
    2. function setTemplate() { 
    3.     const oNode = document.querySelector(globalObj._el); 
    4.     const nNode = toHtml(render(globalObj._sourceTemplate)); 
    5.     compile(oNode, 'o'); 
    6.     compile(nNode, 'n'); 
    7.     if (globalObj._oHtml.length === globalObj._nHtml.length) { 
    8.         for (let index = 0; index < globalObj._oHtml.length; index++) { 
    9.             const element = globalObj._oHtml[index]; 
    10.             element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent); 
    11.         } 
    12.     } 

    首先,我们取到初始化时挂载的DOM节点,接着我们使用toHtml()方法将render(globalObj._sourceTemplate)方法作为第一个参数传入。

    我们先来看下toHtml()方法,这里的第一个参数domStr,也就是render(globalObj._sourceTemplate)。

     
     
     
     
    1. // string to DOM 
    2. function toHtml(domStr) { 
    3.     const parser = new DOMParser(); 
    4.     return parser.parseFromString(domStr, "text/html"); 

    toHtml()方法第一行我们实例化了一个DOMParser对象。一旦建立了一个解析对象以后,你就可以使用它的parseFromString方法来解析一个html字符串。

    然后,我们回到setTemplate()方法中,变量nNode被赋值了toHtml(render(globalObj._sourceTemplate)),这里是被处理成一个DOM对象。

    接着,执行compile()方法。

     
     
     
     
    1. compile(oNode, 'o'); 
    2. compile(nNode, 'n'); 

    我们来看下这个compile()方法。

     
     
     
     
    1. // compile DOM 
    2. function compile(node, type) { 
    3.     const childNodesArr = node.childNodes; 
    4.     for (let index = 0; index < Array.from(childNodesArr).length; index++) { 
    5.         const item = Array.from(childNodesArr)[index]; 
    6.         if (item.childNodes && item.childNodes.length) { 
    7.             compile(item, type); 
    8.         } else if (isTextNode(item) && item.textContent.trim().length !== 0) { 
    9.             type === 'o' ? globalObj._oHtml.push(item) : globalObj._nHtml.push(item); 
    10.         } 
    11.     } 

    这个方法是将遍历DOM元素并把每一项存储到我们初始化定义的数组里面,分别是globalObj._oHtml和globalObj._nHtml,这个方法中用到了isTextNode()方法。

     
     
     
     
    1. // judge text node 
    2. function isTextNode(node) { 
    3.     return node.nodeType === 3; 

    这个方法第一个参数是一个Node节点,如果它的nodeType属性等于3就说明这个节点是文本节点。

    最后,我们又回到setTemplate()方法中,接着执行以下代码:

     
     
     
     
    1. if (globalObj._oHtml.length === globalObj._nHtml.length) { 
    2.     for (let index = 0; index < globalObj._oHtml.length; index++) { 
    3.         const element = globalObj._oHtml[index]; 
    4.         element.textContent !== globalObj._nHtml[index].textContent && (element.textContent = globalObj._nHtml[index].textContent); 
    5.     } 

    判断两个数组的长度是否一样,如果一样就遍历globalObj._oHtml,最后判断globalObj._nHtml[index].textContent是否等于globalObj._oHtml[index].textContent,如果不相等,直接将globalObj._nHtml[index].textContent赋于globalObj._OHtml[index].textContent,完成更新。

    最后,将这几个定义的方法赋于传入的exports对象并返回这个对象。

     
     
     
     
    1. // exports 
    2. exports.createView = createView; 
    3. exports.eventListener = eventListener; 
    4. exports.reactive = reactive; 
    5. exports.ref = ref; 
    6.  
    7. Object.defineProperty(exports, '__esModule', { value: true }); 
    8.  
    9. return exports; 

    这里,有一行代码Object.defineProperty(exports, '__esModule', { value: true }),这行代码其实也可以这么写exports.__esModule = true。表面上看就是把一个导出对象标识为一个 ES 模块。

    随着 JS 不断发展和 Node.js 的出现,JS 慢慢有了模块化方案。在 ES6 之前,最有名的就是 CommonJS / AMD,AMD 就不提了现在基本不用。CommonJS 被 Node.js 采用至今,与 ES 模块共存。由于 Node.js 早期模块化方案选择了 CommonJS,导致现在 NPM 上仍然存在大量的 CommonJS 模块,JS 圈子一时半会儿是丢不掉 CommonJS 了。

    Webpack 实现了一套 CommonJS 模块化方案,支持打包 CommonJS 模块,同时也支持打包 ES 模块。但是两种模块格式混用的时候问题就来了,ES 模块和 CommonJS 模块并不完全兼容,CommonJS 的 module.exports 在 ES 模块中没有对应的表达方式,和默认导出 export default 是不一样的。

    而__esModule则是用来兼容 ES 模块导入 CommonJS 模块默认导出方案。

    结语

    至此,Strview.js的源码分析完毕。谢谢阅读~

    开发版本

    推荐使用StrviewCLI搭建StrviewApp项目脚手架。

    生产版本

    直接引入CDN链接,目前版本为1.9.0。

    网页题目:一篇文章教你如何写成Strview.js之源码剖析
    本文链接:http://www.shufengxianlan.com/qtweb/news2/262252.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

    猜你还喜欢下面的内容

    移动网站建设知识

    行业网站建设