模块化 随着前端代码量的徒然增多,前端模块化必然趋势。 总的来说,起初前端模块化主要是为了解决一下问题:
全局变量污染命名冲突代码复用依赖管理前端模块化的发展历程又可以分为一下几个阶段:
利用自执行函数进行模块的封装,利用闭包导出公有方法nodejs的模块化规范commonjsrequirejs的模块化规范AMDseajs的模块化规范CMDES6模块化前端模块化工具 模块化工具总的来说是为做下面几件事:
加载优化依赖管理各种性能优化
好啦,前言知识就说到这里,下面正式开始小结一下我这次写的模块加载器:
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
CMD告诉我们这样定义一个模块:https://github.com/seajs/seajs/issues/242
seajs是一个模块化工具,也可以说是一个模块加载器,使用seajs的模块必须按照CMD规范定义,这个工具主要能:
实现模块按需加载实现异步加载模块进行模块间的依赖管理,加载顺序管理 实现提前加载,延迟执行(这点也是与requirejs的主要区别)具体接口是下面这样的:
seajs.use()程序启动入口define()定义模块require()获取其他模块提供的接口模块标识id:尽量遵循路径即 ID原则,减轻记忆模块 ID 的负担。exports:Object用来在模块内部对外提供接口。module:Object模块对象https://aotu.io/notes/2016/08/29/SeaJs-From-Entry-To-The-Principle/
本文的模块加载器就仿照seajs的原理基于CMD规范进行简单的基本实现。我们暂且叫他mmd,主要实现以下几个功能:
模块按需加载模块异步加载依赖管理懒执行按需加载 通过入口文件,进行递归的依赖分析,加载需要用到的模块
异步加载 通过动态脚本或ajax等方法实现模块的异步加载
依赖管理 保证模块执行时所依赖的模块已加载好,需要用到的依赖模块的接口均可用。
懒执行 提前加载但延迟执行,在require模块时才执行该模块。
定义全局对象mmd为模块加载器,定义全局方法define,require进行模块的定义。
mmd.use(ids,callback) 程序启动入口,接受两个参数,入口模块数组及回调函数。采用路径即id的原则,用路径唯一标识一个模块。
利用Promise.all()实现入口模块的并行加载,所有入口模块都加载成功后执行回调函数,并将入口模块对外接口作为参数传给回调函数。
mmd.use=function (ids,callback) { //并行加载入口模块 Promise.all( ids.map(function (id) { return mmd.loader(id); }) ).then(function (list) { //所有依赖加载完毕后执行回调函数 if(typeof callback==="function"){ callback.apply(window,list) } }).catch(function (err) { console.log(err); }) }mmd.loader(id) 加载模块方法,接受模块id作为参数,加载对应路径的模块。返回一个Promise对象。在该模块及其相关依赖模块均加载完毕后进行resolve(),并将接口进行传递。
通过上面两个方法,一个大致的功能流程已经很清晰了: 通过mmd.use并行加载入口模块,在入口模块加载完毕后,调用回调函数。距离加载由mmd.loader实现。
所以问题来了: Q1:我怎么知道入口模块什么时候加载完,也就是说我怎么知道各个入口模块什么时候resolve? Q1:我怎么知道一个模块的依赖有那些? Q2:怎样才知道一个模块的相关依赖均已加载完呢?
Module构造函数
我们将每个模块定义为一个Module对象,利用Module.create()方法进行模块的创建。并将所有模块对象以id为key缓存在mmd.Modules对象中。
Module对象有很多属性,比如:
Module.dependence:[] —— 模块依赖 Module.callback:[]——各种状态下的回调函数 Module.status——模块加载状态,总共有四种: pending—初始,loading—正在加载,complete—模块及其相关依赖均加载完毕,error—各种错误.
Module对象方法: on(event,callback)——模拟事件监听器,主要作用是缓存callback。事件类型有complete,error。 trigger(event)——模拟事件触发,主要是根据响应的事件类型,调用缓存中的对应回调 setStatus(status)——状态更新,在状态更新后触发响应事件。
define(factory)方法 模块定义,define方法接受一个factory函数。 当一个模块及其依赖均加载完毕后,将该模块的状态更新为complete.
当模块被插入文档后,执行的肯定是define方法,该函数主要干了这两件事: 1.调用factory.toString()方法,对factory字符串进行正则匹配,进行依赖分析。并更新模块的依赖数组。 2.如果依赖项存在,则加载依赖(加载方法同加载入口文件),加载完成后更新模块状态为complete,如果依赖项不存在,则直接更新模块状态为complete。
require(module)方法 通过调用mmd.getModuleExports(module)获取模块接口,getModuleExports具体实现是查看模块对象的exports属性,如果存在,说明模块对象的factory已经执行过了,直接调用缓存接口。如果exports属性不存在,则调用模块对应的factory方法,导出模块接口,并进行模块接口的缓存。
首先整理下上文的Q1—Q3: A1:我们为每个模块都创建了对应的模块对象,每个模块对象都有一个状态属性,初始时是pending,当该模块及其依赖都加载完毕后,会调用setstatus方法状态更新为complete,并且调用trigger方法激活对应的回调函数,即resolve Promise对象。
A2:创建模块对象时就立即开始将模块插入文档进行异步加载,在加载完成后,即开始调用define方法。 define方法时这样的,他不是直接执行factory函数,而是通过调用函数的toString()方法,正则匹配进行依赖分析,找出模块的依赖模块并更新模块的dependence属性和factory属性。
A3:再找到模块依赖后,如果依赖不存在,则直接更新模块状态为complete,如果依赖存在,则采用和加载入口模块相同的方法进行模块加载,在所有模块加载完后进行父模块的状态更新。
以上也是模块加载器的整个执行流程,下面说说我认为的难点:
1.懒执行的实现? 在加载模块时并不执行factory函数,而是在遇到require方法时,执行函数并缓存接口。方便下次直接使用。
2.事件模式的设计? 主要就是关于整个模块对象的设计,和怎样判定一个入口文件可以进行resolve。 本文的模块加载器主要就是在创建模块后就对各个状态相应的回调函数进行缓存,在通过在特定时候对模块状态进行更新,再调用对应的回调函数。
https://github.com/zjjxj/mmd-module-loader
1.CMD&AMD,seajs&requirejs区别
https://www.zhihu.com/question/20351507