近几年前端工程化越来越完善,打包工具也已经是前端标配了,像seajs这种老古董早已停止维护,而且使用的人估计也几个了。但这并不能阻止好奇的我,为了了解当年的前端前辈们是如何在浏览器进行代码模块化的,我鼓起勇气翻开了Seajs的源码。下面就和我一起细细品味Seajs源码吧。
如何使用seajs
在看Seajs源码之前,先看看Seajs是如何使用的,毕竟刚入行的时候,大家就都使用browserify、webpack之类的东西了,还从来没有用过Seajs。
<!-- 首先在页面中引入sea.js,也可以使用CDN资源 --> <script type="text/javascript" src="https://www.jb51.net/article/sea.js"></script> <script> // 设置一些参数 seajs.config({ debug: true, // debug为false时,在模块加载完毕后会移除head中的script标签 base: './js/', // 通过路径加载其他模块的默认根目录 alias: { // 别名 jquery: 'https://cdn.bootcss.com/jquery/3.2.1/jquery' } }) seajs.use('main', function(main) { alert(main) }) </script> //main.js define(function (require, exports, module) { // require('jquery') // var $ = window.$ module.exports = 'main-module' })
seajs的参数配置
首先通过script导入seajs,然后对seajs进行一些配置。seajs的配置参数很多具体不详细介绍,seajs将配置项会存入一个私有对象data中,并且如果之前有设置过某个属性,并且这个属性是数组或者对象,会将新值与旧值进行合并。
(function (global, undefined) { if (global.seajs) { return } var data = seajs.data = {} seajs.config = function (configData) { for (var key in configData) { var curr = configData[key] // 获取当前配置 var prev = data[key] // 获取之前的配置 if (prev && isObject(prev)) { // 如果之前已经设置过,且为一个对象 for (var k in curr) { prev[k] = curr[k] // 用新值覆盖旧值,旧值保留不变 } } else { // 如果之前的值为数组,进行concat if (isArray(prev)) { curr = prev.concat(curr) } // 确保 base 为一个路径 else if (key === "base") { // 必须已 "https://www.jb51.net/" 结尾 if (curr.slice(-1) !== "https://www.jb51.net/") { curr += "https://www.jb51.net/" } curr = addBase(curr) // 转换为绝对路径 } // Set config data[key] = curr } } } })(this);
设置的时候还有个比较特殊的地方,就是base这个属性。这表示所有模块加载的基础路径,所以格式必须为一个路径,并且该路径最后会转换为绝对路径。比如,我的配置为base: './js',我当前访问的域名为,最后base属性会被转化为。然后,所有依赖的模块id都会根据该路径转换为uri,除非有定义其他配置,关于配置点到为止,到用到的地方再来细说。
模块的加载与执行
下面我们调用了use方法,该方法就是用来加载模块的地方,类似与requirejs中的require方法。
// requirejs require(['main'], function (main) { console.log(main) });
只是这里的依赖项,seajs可以传入字符串,而requirejs必须为一个数组,seajs会将字符串转为数组,在内部seajs.use会直接调用Module.use。这个Module为一个构造函数,里面挂载了所有与模块加载相关的方法,还有很多静态方法,比如实例化Module、转换模块id为uri、定义模块等等,废话不多说直接看代码。
seajs.use = function(ids, callback) { Module.use(ids, callback, data.cwd + "_use_" + cid()) return seajs } // 该方法用来加载一个匿名模块 Module.use = function (ids, callback, uri) { //如果是通过seajs.use调用,uri是自动生成的 var mod = Module.get( uri, isArray(ids) ? ids : [ids] // 这里会将依赖模块转成数组 ) mod._entry.push(mod) // 表示当前模块的入口为本身,后面还会把这个值传入他的依赖模块 mod.history = {} mod.remain = 1 // 这个值后面会用来标识依赖模块是否已经全部加载完毕 mod.callback = function() { //设置模块加载完毕的回调,这一部分很重要,尤其是exec方法 var exports = [] var uris = mod.resolve() for (var i = 0, len = uris.length; i < len; i++) { exports[i] = cachedMods[uris[i]].exec() } if (callback) { callback.apply(global, exports) //执行回调 } } mod.load() }
这个use方法一共做了三件事:
1.调用Module.get,进行Module实例化
2.为模块绑定回调函数
3.调用load,进行依赖模块的加载
实例化模块,一切的开端
首先use方法调用了get静态方法,这个方法是对Module进行实例化,并且将实例化的对象存入到全局对象cachedMods中进行缓存,并且以uri作为模块的标识,如果之后有其他模块加载该模块就能直接在缓存中获取。