前言
看到这个标题,估计有同学会想,又要重复造轮子么?其实重复造轮子在大多数情况下确实是不太可取的,既浪费了精力又浪费了时间。但这并不能说明重复造轮子完全不可取,比如你想要某个轮子的精简版,又比如你想学习某个轮子的制造方法,重复造轮子也可以是有意义的。
简介
接下来,我们就来学学某个轮子简易版制造方法,这个轮子就是模块加载工具。
说起模块加载工具,估计大家就会想起 webpack、commonjs 等,更“ 久远” 一点的会想起 requirejs 和 seajs。这些工具都源于前端的模块化思想。
为什么前端需要模块化?这主要得益于前端技术的发展,使得前端不再像以前那样只能展示一下静态内容,撑死加上几个飞来飞去的动画。现在的前端内容越来越丰富,我们可以播放视频,可以协同工作,还可以玩游戏。这就导致了前端代码量剧增。当代码行数噌噌噌往上涨时,模块化思想就自然而然地出来了。
对于前端来说,最简单的模块化就是拆分成多个文件,然后在 html 里就会出现如下的代码:
1 2 3 4 5 6 7 |
<script src="/js/module_a/a1.js"></script> <script src="/js/module_a/a2.js"></script> <script src="/js/module_b/b1.js"></script> <script src="/js/module_c/c1.js"></script> <script src="/js/module_c/c2.js"></script> <script src="/js/module_c/c3.js"></script> <script src="/js/module_c/c4.js"></script> |
各位有没有觉得这种代码有点儿难看?像这样的代码不止难看,依赖也不清晰,假如上面的 module_b 只是因为 module_a 的需要才引入的,那么当我们去掉 module_a 时还得搜一下相关文档或者源码,当我们检索出确确实实只有 module_a 才依赖了 module_b,我们才敢放心的把 module_b 给去掉。
因此,就衍生了像 requirejs 之类模块加载工具,同时还能处理依赖关系。其实像 requirejs 和 webpack 之类的构建工具处理模块化时很相似,只是处理模块依赖的时机不同,requirejs 是直接在浏览器里处理,而 webpack 则是在上线前就将模块进行打包。而在代码上两者最大的差异就是,requirejs 需要每个模块包裹一层依赖代码(其实这层代码也可以借由构建工具生成),而 webpack 则会在打包后的代码里注入一下模块化的脚本。事实上这两者也不是水火不容,这主要看项目的技术选型。
说了那么多,接下来就来进入正题,我们这次就是来造一个简易版的类似 requirejs 的模块加载工具,注意是简易版,所以这个轮子最好不要直接投入到生产环境中,造这个轮子更多的目的是为了一起学习 XD。
需求
使用方式我们就做得简单一点,只暴露一个方法出来:define 方法。
当我们需要定义一个模块时,可以像如下方式编写代码:
1 2 3 4 5 6 7 |
define(['/js/a.js', '/js/b.js'], function(a, b) { return { doSth: function() { a.a(b) } }; }); |
每个模块都用 define 来定义,声明依赖的模块和回调方法。回调中可以返回一个对象,也可以不返回值。如果返回对象则会被注入到依赖这个模块的模块回调方法中,如果不返回值则注入空对象。同时依赖的模块可以是纯文本文件或 json 文件,如果纯文本,注入进来的会是该文件的字符串内容,如果是 json 文件则注入 json 对象:
1 2 3 4 5 6 7 8 |
define(['/json/a.json', '/html/a.html'], function(data, html) { return { doSth: function() { console.log(JSON.stringify(data)); // 输出a.json的内容 console.log(html); // 输出a.html的内容 } } }); |
设计与思考
我们这里有如下几个问题需要思考一下:
- 如何注入依赖?
- 如何获取依赖模块的绝对路径?
- 如何加载依赖的模块?
- 如何处理循环依赖?
针对这几个问题我们来对这个模块加载工具进行设计。
如何注入依赖?
需要注入依赖到当前模块,就得保证依赖是先于当前模块加载并执行完,这样我们就需要维护一个模块队列,保证模块加载的顺序和保存模块的状态。
当遇到 define 方法进行模块定义时,先获取依赖,将依赖的加载顺序置于当前模块之前,这个我们通过维护一个模块的状态列表就可以达成。状态设计成以下三种:
- LOADING:模块正在加载中。
- WAITING:模块已经加载完毕,正在等待依赖模块加载。
- DEFINED:模块和其依赖均已经加载完毕,并且执行过回调,完成模块定义。
每次定义模块,我们就检查该模块所依赖的模块状态,如果依赖都已定义,则进入执行回调阶段;如果依赖未完全就位,则设置为等待中,将未加载的模块放入加载列表进行加载。具体流程如下:
1 2 3 |
模块定义 --> 检查依赖 --> 依赖都已就位 --> 注入依赖,执行回调 --> 完成定义 | |--> 依赖未完 |