大家好,我是Kason。今天我们就来说说如何用90行代码实现一个现代的JS模块打包器。我们的packager虽然小,但是实现了webpack的核心功能。另外,我知道你看到大块代码会头疼,所以这篇文章都是关于图表的。看完有兴趣的话,这里是完整代码的仓库地址[1],只有90行代码。让我们找点乐子。生成依赖图如果应用程序是一团羊毛,那么入口文件就是线程。打包器做的第一件事是:从行首过滤整行假设入口文件是entry.js://entry.jsimportafrom'./a.js';importbfrom'./b.js';console.log(a,b);他依赖a.js和b.js。em...有点太粗糙了,我们展开a.js和b.js://a.jsimportcfrom'./c.js';//...//b.jsimportdfrom'./d。js';importefrom'./e.js';//...所以整个依赖关系是这样的:打包器会从入口文件开始,尝试建立模块(即js文件)之间的依赖关系,这也就是我们刚才说的“跟随线程的尾部,开始过滤整个线程的方向”。通过分析模块代码中的导入声明语句,可以知道模块间的依赖关系。为了分析import声明语句,可以使用babel等编译工具将模块代码分解成AST(AbstractSyntaxTree)。遍历AST,类型为ImportDeclaration的节点为导入声明语句。最后,我们将AST重新转换为可执行的目标代码,可能还需要根据代码执行的宿主环境(通常是浏览器)对代码进行一些转换。比如浏览器不支持import'./a.js'这样的ESM语法,那么我们就需要将ESM语法全部转换为CJS语法。//源码import'./a.js';//转换后require('./a.js');因此,对于任何一个模块(js文件),都会经历:右边包含目标代码和模块间依赖关系的数据结构,称为资产。每个资产都可以通过模块间的依赖找到依赖的模块,重复这个过程,生成新的资产,最终形成整个应用所有资产之间的依赖关系:应用完整的依赖关系称为“依赖图”(dependencygraph))。打包代码接下来只需要遍历“依赖图”,将所有资产的目标代码打包在一起即可。所有代码将被打包在一个“立即执行函数”中:(function(modules){//packagedcode})(modules)modules保存所有资产及其依赖。如果对模块的细节感兴趣,可以去文末仓库翻代码。刚才说了asset的objectcode是CJS规范,类似://entry.jsrequire('./a.js');require('./b.js');这意味着我们需要实现:require方法(用于引入依赖的其他资产的目标代码)模块对象(用于保存当前资产的目标代码执行后导出的数据)同时,为了防止不同资产的目标代码中的变量相互污染,每个目标代码都需要一个独立的作用域。我们将目标代码包装在一个函数中://我们操作的是一个字符串模板`function(require,module,exports){${asset.code}}`所以,最终的包装结果是:(function(modules){functionrequire(){//...}require(entryassetID)})(modules)ThisstringwrappedinthebrowserThisstringwrappedinthebrowser
