├── example ├── module2.js ├── module1.js └── index.js ├── .gitignore ├── src ├── _test.js └── roid.js ├── package.json ├── LICENSE ├── output.js └── README.md /example/module2.js: -------------------------------------------------------------------------------- 1 | export const roid = 'zheng Fang' 2 | -------------------------------------------------------------------------------- /example/module1.js: -------------------------------------------------------------------------------- 1 | import { roid } from './module2.js' 2 | 3 | export default () => `hello ${roid}!` 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | npm-debug.log* 4 | .DS_Store 5 | yarn-error.log* 6 | yarn.lock 7 | package-lock.json 8 | .vscode 9 | -------------------------------------------------------------------------------- /src/_test.js: -------------------------------------------------------------------------------- 1 | // 教程不在这里,看 roid.js 2 | const roid = require('./roid') 3 | const vm = require('vm') 4 | 5 | const jsBundle = roid(process.argv[2]) 6 | vm.runInThisContext(jsBundle) 7 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { roid } from './module2.js' 2 | import module1 from './module1.js' 3 | 4 | 5 | const text = module1() 6 | console.log() 7 | console.log(text) 8 | 9 | console.log("welcome to roid, I'm", roid) 10 | console.log("\nif you love roid and learnt any thing, please give me a star") 11 | console.log("https://github.com/Foveluy/roid") 12 | console.log() 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roid", 3 | "version": "0.1.0", 4 | "description": "", 5 | "author": "Zheng Fang", 6 | "license": "MIT", 7 | "dependencies": { 8 | "babel-core": "^6.26.0", 9 | "babel-preset-env": "^1.6.1", 10 | "babel-preset-es2015": "^6.24.1", 11 | "babel-traverse": "^6.26.0", 12 | "eslint": "^4.17.0", 13 | "eslint-config-airbnb-base": "^12.1.0", 14 | "eslint-plugin-import": "^2.8.0" 15 | }, 16 | "devDependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zheng Fang (snakegear@163.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /output.js: -------------------------------------------------------------------------------- 1 | 2 | (function(modules) { 3 | function require(id) { 4 | const [fn, idMapping] = modules[id]; 5 | function childRequire(filename) { 6 | return require(idMapping[filename]); 7 | } 8 | const newModule = {exports: {}}; 9 | fn(childRequire, newModule, newModule.exports); 10 | return newModule.exports 11 | } 12 | require(0); 13 | })({0:[ 14 | function(require,module,exports){"use strict"; 15 | 16 | var _message = require("./message.js"); 17 | 18 | var _message2 = _interopRequireDefault(_message); 19 | 20 | var _name = require("./name.js"); 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 23 | 24 | console.log((0, _message2.default)()); 25 | console.log(_name.name);}, 26 | {"./message.js":1,"./name.js":2}, 27 | ],1:[ 28 | function(require,module,exports){"use strict"; 29 | 30 | Object.defineProperty(exports, "__esModule", { 31 | value: true 32 | }); 33 | 34 | var _name = require("./name.js"); 35 | 36 | exports.default = function () { 37 | return "hello " + _name.name + "!"; 38 | };}, 39 | {"./name.js":3}, 40 | ],2:[ 41 | function(require,module,exports){"use strict"; 42 | 43 | Object.defineProperty(exports, "__esModule", { 44 | value: true 45 | }); 46 | var name = exports.name = 'world';}, 47 | {}, 48 | ],3:[ 49 | function(require,module,exports){"use strict"; 50 | 51 | Object.defineProperty(exports, "__esModule", { 52 | value: true 53 | }); 54 | var name = exports.name = 'world';}, 55 | {}, 56 | ],}); -------------------------------------------------------------------------------- /src/roid.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('fs') 2 | const path = require('path') 3 | const traverse = require('babel-traverse').default 4 | const { transformFromAst, transform } = require('babel-core') 5 | 6 | // 全局的自增 id 7 | // 记录每一个载入的模块的 id,我们将所有的模块都用唯一标识符进行标示 8 | // 因此自增 id 是最有效也是最直观的,有多少个模块,一统计就出来了 9 | let ID = 0 10 | 11 | // 当前用户的操作的目录 12 | const currentPath = process.cwd() 13 | 14 | // 首先,我们对每一个文件进行处理。因为这只是一个简单版本的 bundler 15 | // 因此,我们并不考虑如何去解析 css、md、txt 等等之类的格式,我们专心处理好 js 文件的打包 16 | // 因为对于其他文件而言,处理起来过程不太一样,用文件后缀很容易将他们区分进行不同的处理 17 | // 在这个版本,我们还是专注 js 18 | function parseDependecies(filename) { 19 | // 函数注入一个 filename 顾名思义,就是文件名,读取其的文件文本内容 20 | const rawCode = readFileSync(filename, 'utf-8') 21 | 22 | // 首先,我们使用 babel 的 transform 方法去转换我们的原始代码 23 | // 通过转换以后,我们的代码变成了抽象语法树( AST ),你可以通过 https://astexplorer.net/ 24 | // 这个可视化的网站,看看 AST 生成的是什么 25 | const ast = transform(rawCode).ast 26 | 27 | // 当我们解析完以后,我们就可以提取当前文件中的 dependencies 28 | // dependencies 翻译为依赖,也就是我们文件中所有的 `import xxxx from xxxx` 29 | // 我们将这些依赖都放在 dependencies 的数组里面,之后统一进行导出 30 | const dependencies = [] 31 | 32 | // traverse 函数是一个遍历 AST 的方法,由 babel-traverse 提供 33 | // 他的遍历模式是经典的 visitor 模式 34 | // visitor 模式就是定义一系列的 visitor ,当碰到 AST 的 type === visitor 名字时 35 | // 就会进入这个 visitor 的函数 36 | traverse(ast, { 37 | // 类型为 `ImportDeclaration` 的 AST 节点,其实就是我们的 `import xxx from xxxx` 38 | ImportDeclaration(path) { 39 | // 其中 path.node.source.value 的值,就是我们 import from xxxx 中的地址 40 | const sourcePath = path.node.source.value 41 | //将地址 push 到 dependencies 中 42 | dependencies.push(sourcePath) 43 | } 44 | }) 45 | 46 | // 当我们完成依赖的收集以后,我们就可以把我们的代码从 AST 转换成 CommenJS 的代码 47 | // 这样子兼容性更高,更好 48 | const es5Code = transformFromAst(ast, null, { 49 | presets: ['env'] 50 | }).code 51 | 52 | // 还记得我们的 webpack-loader 系统吗? 53 | // 具体实现就是在这里可以实现 54 | // 通过将文件名和代码都传入 loader 中,进行判断,甚至用户定义行为再进行转换 55 | // 就可以实现 loader 的机制,当然,我们在这里,就做一个弱智版的 loader 就可以了 56 | // parcel 在这里的优化技巧是很有意思的,在 webpack 中,我们每一个 loader 之间传递的是转换好的代码 57 | // 而不是 AST,那么我们必须要在每一个 loader 进行 code -> AST 的转换,这样时非常耗时的 58 | // parcel 的做法其实就是将 AST 直接传递,而不是转换好的代码,这样,速度就快起来了 59 | const customCode = loader(filename, es5Code) 60 | 61 | // 最后模块导出 62 | // 不要忘记了,每导出一个文件模块,我们都往全局自增 id 中 + 1,以保证每一个文件模块的唯一性 63 | return { 64 | id: ID++, 65 | code: customCode, 66 | dependencies, 67 | filename 68 | } 69 | } 70 | 71 | // 接下来,我们对模块进行更高级的处理。 72 | // 我们之前已经写了一个 parseDependecies 函数,那么现在我们要来写一个 parseGraph 函数 73 | // 我们将所有文件模块组成的集合叫做 graph ,用于描述我们这个项目的所有的依赖关系 74 | // parseGraph 从 entry (入口) 出发,一直手机完所有的以来文件为止 75 | function parseGraph(entry) { 76 | // 从 entry 出发,首先收集 entry 文件的依赖 77 | const entryAsset = parseDependecies(path.resolve(currentPath, entry)) 78 | 79 | // graph 其实是一个数组,我们将最开始的入口模块放在最开头 80 | const graph = [entryAsset] 81 | 82 | // 在这里我们使用 for of 循环而不是 foreach ,原因是因为我们在循环之中会不断的向 graph 中 83 | // push 进东西,graph 会不断增加,用 for of 会一直持续这个循环直到 graph 不会再被推进去东西 84 | // 这就意味着,所有的依赖已经解析完毕,graph 数组数量不会继续增加 85 | // 但是用 foreach 是不行的,只会遍历一次 86 | for (const asset of graph) { 87 | // asset 代表解析好的模块,里面有 filename,code,dependencies 等东西 88 | // asset.idMapping 是一个不太好理解的概念 89 | // 我们每一个文件都会进行 import 操作,import 操作在之后会被转换成 require 90 | // 每一个文件中的 require 的 path 其实会对应一个数字自增 id 91 | // 这个自增 id 其实就是我们一开始的时候设置的 id 92 | // 我们通过将 path-id 利用键值对,对应起来,之后我们在文件中 require 就能够轻松的找到文件的代码 93 | // 解释这么啰嗦的原因是往往模块之间的引用是错中复杂的,这恰巧是这个概念难以解释的原因 94 | if (!asset.idMapping) asset.idMapping = {} 95 | 96 | // 获取 asset 中文件对应的文件夹 97 | const dir = path.dirname(asset.filename) 98 | 99 | // 每个文件都会被 parse 出一个 dependencise,他是一个数组,在之前的函数中已经讲到 100 | // 因此,我们要遍历这个数组,将有用的信息全部取出来 101 | // 值得关注的是 asset.idMapping[dependencyPath] = denpendencyAsset.id 操作 102 | // 我们往下看 103 | asset.dependencies.forEach(dependencyPath => { 104 | // 获取文件中模块的绝对路径,比如 import ABC from './world' 105 | // 会转换成 /User/xxxx/desktop/xproject/world 这样的形式 106 | const absolutePath = path.resolve(dir, dependencyPath) 107 | 108 | // 解析这些依赖 109 | const denpendencyAsset = parseDependecies(absolutePath) 110 | 111 | // 获取唯一 id 112 | const id = denpendencyAsset.id 113 | 114 | // 这里是重要的点了,我们解析每解析一个模块,我们就将他记录在这个文件模块 asset 下的 idMapping 中 115 | // 之后我们 require 的时候,能够通过这个 id 值,找到这个模块对应的代码,并进行运行 116 | asset.idMapping[dependencyPath] = denpendencyAsset.id 117 | 118 | // 将解析的模块推入 graph 中去 119 | graph.push(denpendencyAsset) 120 | }) 121 | } 122 | 123 | // 返回这个 graph 124 | return graph 125 | } 126 | 127 | // 我们完成了 graph 的收集,那么就到我们真正的代码打包了 128 | // 这个函数使用了大量的字符串处理,你们不要觉得奇怪,为什么代码和字符串可以混起来写 129 | // 如果你跳出写代码的范畴,看我们的代码,实际上,代码就是字符串,只不过他通过特殊的语言形式组织起来而已 130 | // 对于脚本语言 JS 来说,字符串拼接成代码,然后跑起来,这种操作在前端非常的常见 131 | // 我认为,这种思维的转换,是拥有自动化、工程化的第一步 132 | function build(graph) { 133 | // 我们的 modules 就是一个字符串 134 | let modules = '' 135 | 136 | // 我们将 graph 中所有的 asset 取出来,然后使用 node.js 制造模块的方法来将一份代码包起来 137 | // 我之前做过一个《庖丁解牛:教你如何实现》node.js 模块的文章,不懂的可以去看看 138 | // https://zhuanlan.zhihu.com/p/34974579 139 | // 140 | // 在这里简单讲述,我们将转换好的源码,放进一个 function(require,module,exports){} 函数中 141 | // 这个函数的参数就是我们随处可用的 require,module,以及 exports 142 | // 这就是为什么我们可以随处使用这三个玩意的原因,因为我们每一个文件的代码终将被这样一个函数包裹起来 143 | // 144 | // 不过这段代码中比较奇怪的是,我们将代码封装成了 `1:[...],2:[...]`的形式 145 | // 我们在最后导入模块的时候,会为这个字符串加上一个 {} 146 | // 变成 {1:[...],2:[...]},你没看错,这是一个对象,这个对象里用数字作为 key 147 | // 一个二维元组作为值, 148 | // [0] 第一个就是我们被包裹的代码 149 | // [1] 第二个就是我们的 mapping 150 | graph.forEach(asset => { 151 | modules += `${asset.id}:[ 152 | function(require,module,exports){${asset.code}}, 153 | ${JSON.stringify(asset.idMapping)}, 154 | ],` 155 | }) 156 | 157 | // 马上要见到曙光了 158 | // 这一段代码实际上才是模块引入的核心逻辑 159 | // 我们制造一个顶层的 require 函数,这个函数接收一个 id 作为值,并且返回一个全新的 module 对象 160 | // 我们倒入我们刚刚制作好的模块,给他加上 {},使其成为 {1:[...],2:[...]} 这样一个完整的形式 161 | // 然后塞入我们的立即执行函数中(function(modules) {...})() 162 | // 在 (function(modules) {...})() 中,我们先调用 require(0) 163 | // 理由很简单,因为我们的主模块永远是排在第一位的 164 | // 紧接着,在我们的 require 函数中,我们拿到外部传进来的 modules,利用我们一直在说的全局数字 id 获取我们的模块 165 | // 每个模块获取出来的就是一个二维元组 166 | // 然后,我们要制造一个 `子require` 167 | // 这么做的原因是我们在文件中使用 require 时,我们一般 require 的是地址,而顶层的 require 函数参数时 id 168 | // 不要担心,我们之前的 idMapping 在这里就用上了,通过用户 require 进来的地址,在 idMapping 中找到 id 169 | // 然后递归调用 require(id),就能够实现模块的自动倒入了 170 | // 接下来制造一个 const newModule = {exports: {}}; 171 | // 运行我们的函数 fn(childRequire, newModule, newModule.exports);,将应该丢进去的丢进去 172 | // 最后 return newModule.exports 这个模块的 exports 对象 173 | const wrap = ` 174 | (function(modules) { 175 | function require(id) { 176 | const [fn, idMapping] = modules[id]; 177 | function childRequire(filename) { 178 | return require(idMapping[filename]); 179 | } 180 | const newModule = {exports: {}}; 181 | fn(childRequire, newModule, newModule.exports); 182 | return newModule.exports 183 | } 184 | require(0); 185 | })({${modules}});` // 注意这里需要给 modules 加上一个 {} 186 | return wrap 187 | } 188 | 189 | // 这是一个 loader 的最简单实现 190 | function loader(filename, code) { 191 | if (/index/.test(filename)) { 192 | console.log('this is loader ') 193 | } 194 | return code 195 | } 196 | 197 | // 最后我们导出我们的 bundler 198 | module.exports = entry => { 199 | const graph = parseGraph(entry) 200 | const bundle = build(graph) 201 | return bundle 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roid 2 | 3 | roid 是一个极其简单的打包软件,使用 node.js 开发而成,看完本文,你可以实现一个非常简单的,但是又有实际用途的前端代码打包工具。 4 | 5 | 如果不想看教程,直接看代码的(全部注释):[点击地址](https://github.com/Foveluy/roid/blob/master/src/roid.js) 6 | 7 | ## 为什么要写 roid ? 8 | 9 | 我们每天都面对前端的这几款编译工具,但是在大量交谈中我得知,并不是很多人知道这些打包软件背后的工作原理,因此有了这个 project 出现。诚然,你并不需要了解太多编译原理之类的事情,如果你在此之前对 node.js 极为熟悉,那么你对前端打包工具一定能非常好的理解。 10 | 11 | 弄清楚打包工具的背后原理,有利于我们实现各种神奇的自动化、工程化东西,比如表单的双向绑定,自创 JavaScript 语法,又如蚂蚁金服 ant 中大名鼎鼎的 import 插件,甚至是前端文件自动扫描载入等,能够极大的提升我们工作效率。 12 | 13 | 不废话,我们直接开始。 14 | 15 | ## 从一个自增 id 开始 16 | 17 | ```js 18 | const { readFileSync, writeFileSync } = require('fs') 19 | const path = require('path') 20 | const traverse = require('babel-traverse').default 21 | const { transformFromAst, transform } = require('babel-core') 22 | 23 | let ID = 0 24 | 25 | // 当前用户的操作的目录 26 | const currentPath = process.cwd() 27 | ``` 28 | 29 | `id`:全局的自增 `id` ,记录每一个载入的模块的 `id` ,我们将所有的模块都用唯一标识符进行标示,因此自增 `id` 是最有效也是最直观的,有多少个模块,一统计就出来了。 30 | 31 | ## 解析单个文件模块 32 | 33 | ```js 34 | function parseDependecies(filename) { 35 | const rawCode = readFileSync(filename, 'utf-8') 36 | 37 | const ast = transform(rawCode).ast 38 | 39 | const dependencies = [] 40 | 41 | traverse(ast, { 42 | ImportDeclaration(path) { 43 | const sourcePath = path.node.source.value 44 | dependencies.push(sourcePath) 45 | } 46 | }) 47 | 48 | // 当我们完成依赖的收集以后,我们就可以把我们的代码从 AST 转换成 CommenJS 的代码 49 | // 这样子兼容性更高,更好 50 | const es5Code = transformFromAst(ast, null, { 51 | presets: ['env'] 52 | }).code 53 | 54 | // 还记得我们的 webpack-loader 系统吗? 55 | // 具体实现就是在这里可以实现 56 | // 通过将文件名和代码都传入 loader 中,进行判断,甚至用户定义行为再进行转换 57 | // 就可以实现 loader 的机制,当然,我们在这里,就做一个弱智版的 loader 就可以了 58 | // parcel 在这里的优化技巧是很有意思的,在 webpack 中,我们每一个 loader 之间传递的是转换好的代码 59 | // 而不是 AST,那么我们必须要在每一个 loader 进行 code -> AST 的转换,这样时非常耗时的 60 | // parcel 的做法其实就是将 AST 直接传递,而不是转换好的代码,这样,速度就快起来了 61 | const customCode = loader(filename, es5Code) 62 | 63 | // 最后模块导出 64 | return { 65 | id: ID++, 66 | code: customCode, 67 | dependencies, 68 | filename 69 | } 70 | } 71 | ``` 72 | 73 | 首先,我们对每一个文件进行处理。因为这只是一个简单版本的 `bundler` ,因此,我们并不考虑如何去解析 `css` 、`md` 、`txt` 等等之类的格式,我们专心处理好 `js` 文件的打包,因为对于其他文件而言,处理起来过程不太一样,用文件后缀很容易将他们区分进行不同的处理,在这个版本,我们还是专注 `js`。 74 | 75 | `const rawCode = readFileSync(filename, 'utf-8')` 函数注入一个 filename 顾名思义,就是文件名,读取其的文件文本内容,然后对其进行 AST 的解析。我们使用 `babel` 的 `transform` 方法去转换我们的原始代码,通过转换以后,我们的代码变成了抽象语法树( `AST` ),你可以通过 https://astexplorer.net/, 这个可视化的网站,看看 `AST` 生成的是什么。 76 | 77 | 当我们解析完以后,我们就可以提取当前文件中的 `dependencies`,`dependencies` 翻译为依赖,也就是我们文件中所有的 `import xxxx from xxxx`,我们将这些依赖都放在 `dependencies` 的数组里面,之后统一进行导出。 78 | 79 | 然后通过 `traverse` 遍历我们的代码。`traverse` 函数是一个遍历 `AST` 的方法,由 `babel-traverse` 提供,他的遍历模式是经典的 `visitor` 模式 80 | ,`visitor` 模式就是定义一系列的 `visitor` ,当碰到 `AST` 的 `type === visitor` 名字时,就会进入这个 `visitor` 的函数。类型为 `ImportDeclaration` 的 AST 节点,其实就是我们的 `import xxx from xxxx`,最后将地址 push 到 dependencies 中. 81 | 82 | 最后导出的时候,不要忘记了,每导出一个文件模块,我们都往全局自增 `id` 中 `+ 1`,以保证每一个文件模块的唯一性。 83 | 84 | ## 解析所有文件,生成依赖图 85 | 86 | ```js 87 | function parseGraph(entry) { 88 | // 从 entry 出发,首先收集 entry 文件的依赖 89 | const entryAsset = parseDependecies(path.resolve(currentPath, entry)) 90 | 91 | // graph 其实是一个数组,我们将最开始的入口模块放在最开头 92 | const graph = [entryAsset] 93 | 94 | for (const asset of graph) { 95 | if (!asset.idMapping) asset.idMapping = {} 96 | 97 | // 获取 asset 中文件对应的文件夹 98 | const dir = path.dirname(asset.filename) 99 | 100 | // 每个文件都会被 parse 出一个 dependencise,他是一个数组,在之前的函数中已经讲到 101 | // 因此,我们要遍历这个数组,将有用的信息全部取出来 102 | // 值得关注的是 asset.idMapping[dependencyPath] = denpendencyAsset.id 操作 103 | // 我们往下看 104 | asset.dependencies.forEach(dependencyPath => { 105 | // 获取文件中模块的绝对路径,比如 import ABC from './world' 106 | // 会转换成 /User/xxxx/desktop/xproject/world 这样的形式 107 | const absolutePath = path.resolve(dir, dependencyPath) 108 | 109 | // 解析这些依赖 110 | const denpendencyAsset = parseDependecies(absolutePath) 111 | 112 | // 获取唯一 id 113 | const id = denpendencyAsset.id 114 | 115 | // 这里是重要的点了,我们解析每解析一个模块,我们就将他记录在这个文件模块 asset 下的 idMapping 中 116 | // 之后我们 require 的时候,能够通过这个 id 值,找到这个模块对应的代码,并进行运行 117 | asset.idMapping[dependencyPath] = denpendencyAsset.id 118 | 119 | // 将解析的模块推入 graph 中去 120 | graph.push(denpendencyAsset) 121 | }) 122 | } 123 | 124 | // 返回这个 graph 125 | return graph 126 | } 127 | ``` 128 | 129 | 接下来,我们对模块进行更高级的处理。我们之前已经写了一个 `parseDependecies` 函数,那么现在我们要来写一个 `parseGraph` 函数,我们将所有文件模块组成的集合叫做 `graph`(依赖图),用于描述我们这个项目的所有的依赖关系,`parseGraph` 从 `entry` (入口) 出发,一直手机完所有的以来文件为止. 130 | 131 | 在这里我们使用 `for of` 循环而不是 `forEach` ,原因是因为我们在循环之中会不断的向 `graph` 中,`push` 进东西,`graph` 会不断增加,用 `for of` 会一直持续这个循环直到 `graph` 不会再被推进去东西,这就意味着,所有的依赖已经解析完毕,`graph` 数组数量不会继续增加,但是用 `forEach` 是不行的,只会遍历一次。 132 | 133 | 在 `for of` 循环中,`asset` 代表解析好的模块,里面有 `filename` , `code` , `dependencies` 等东西 `asset.idMapping` 是一个不太好理解的概念,我们每一个文件都会进行 `import` 操作,`import` 操作在之后会被转换成 `require` 每一个文件中的 `require` 的 `path` 其实会对应一个数字自增 `id`,这个自增 `id` 其实就是我们一开始的时候设置的 `id`,我们通过将 `path-id` 利用键值对,对应起来,之后我们在文件中 `require` 就能够轻松的找到文件的代码,解释这么啰嗦的原因是往往模块之间的引用是错中复杂的,这恰巧是这个概念难以解释的原因。 134 | 135 | ## 最后,生成 bundle 136 | 137 | ```js 138 | function build(graph) { 139 | // 我们的 modules 就是一个字符串 140 | let modules = '' 141 | 142 | graph.forEach(asset => { 143 | modules += `${asset.id}:[ 144 | function(require,module,exports){${asset.code}}, 145 | ${JSON.stringify(asset.idMapping)}, 146 | ],` 147 | }) 148 | 149 | const wrap = ` 150 | (function(modules) { 151 | function require(id) { 152 | const [fn, idMapping] = modules[id]; 153 | function childRequire(filename) { 154 | return require(idMapping[filename]); 155 | } 156 | const newModule = {exports: {}}; 157 | fn(childRequire, newModule, newModule.exports); 158 | return newModule.exports 159 | } 160 | require(0); 161 | })({${modules}});` // 注意这里需要给 modules 加上一个 {} 162 | return wrap 163 | } 164 | 165 | // 这是一个 loader 的最简单实现 166 | function loader(filename, code) { 167 | if (/index/.test(filename)) { 168 | console.log('this is loader ') 169 | } 170 | return code 171 | } 172 | 173 | // 最后我们导出我们的 bundler 174 | module.exports = entry => { 175 | const graph = parseGraph(entry) 176 | const bundle = build(graph) 177 | return bundle 178 | } 179 | ``` 180 | 181 | 我们完成了 graph 的收集,那么就到我们真正的代码打包了,这个函数使用了大量的字符串处理,你们不要觉得奇怪,为什么代码和字符串可以混起来写,如果你跳出写代码的范畴,看我们的代码,实际上,代码就是字符串,只不过他通过特殊的语言形式组织起来而已,对于脚本语言 JS 来说,字符串拼接成代码,然后跑起来,这种操作在前端非常的常见,我认为,这种思维的转换,是拥有自动化、工程化的第一步。 182 | 183 | 我们将 graph 中所有的 asset 取出来,然后使用 node.js 制造模块的方法来将一份代码包起来,我之前做过一个《庖丁解牛:教你如何实现》node.js 模块的文章,不懂的可以去看看,https://zhuanlan.zhihu.com/p/34974579 184 | 185 | 在这里简单讲述,我们将转换好的源码,放进一个 `function(require,module,exports){}` 函数中,这个函数的参数就是我们随处可用的 `require`,`module`,以及 `exports`,这就是为什么我们可以随处使用这三个玩意的原因,因为我们每一个文件的代码终将被这样一个函数包裹起来,不过这段代码中比较奇怪的是,我们将代码封装成了 `1:[...],2:[...]`的形式,我们在最后导入模块的时候,会为这个字符串加上一个 `{}`,变成 `{1:[...],2:[...]}`,你没看错,这是一个对象,这个对象里用数字作为 `key`,一个二维元组作为值: 186 | 187 | - [0] 第一个就是我们被包裹的代码 188 | - [1] 第二个就是我们的 `mapping` 189 | 190 | 马上要见到曙光了,这一段代码实际上才是模块引入的核心逻辑,我们制造一个顶层的 `require` 函数,这个函数接收一个 `id` 作为值,并且返回一个全新的 `module` 对象,我们倒入我们刚刚制作好的模块,给他加上 `{}`,使其成为 `{1:[...],2:[...]}` 这样一个完整的形式。 191 | 192 | 然后塞入我们的立即执行函数中`(function(modules) {...})()`,在 `(function(modules) {...})()` 中,我们先调用 `require(0)`,理由很简单,因为我们的主模块永远是排在第一位的,紧接着,在我们的 `require` 函数中,我们拿到外部传进来的 `modules`,利用我们一直在说的全局数字 `id` 获取我们的模块,每个模块获取出来的就是一个二维元组。 193 | 194 | 然后,我们要制造一个 `子require`,这么做的原因是我们在文件中使用 `require` 时,我们一般 `require` 的是地址,而顶层的 `require` 函数参数时 `id` 195 | 不要担心,我们之前的 `idMapping` 在这里就用上了,通过用户 `require` 进来的地址,在 `idMapping` 中找到 `id`。 196 | 197 | 然后递归调用 `require(id)`,就能够实现模块的自动倒入了,接下来制造一个 `const newModule = {exports: {}};`,运行我们的函数 `fn(childRequire, newModule, newModule.exports);`,将应该丢进去的丢进去,最后 `return newModule.exports` 这个模块的 `exports` 对象。 198 | 199 | 这里的逻辑其实跟 node.js 差别不太大。 200 | 201 | ## 最后写一点测试 202 | 203 | 测试的代码,我已经放在了仓库里,想测试一下的同学可以去仓库中自行提取。 204 | 205 | 打满注释的代码也放在仓库了,[点击地址](https://github.com/Foveluy/roid/blob/master/src/roid.js) 206 | 207 | ```bash 208 | git clone https://github.com/Foveluy/roid.git 209 | npm i 210 | node ./src/_test.js ./example/index.js 211 | ``` 212 | 213 | 输出 214 | 215 | ```bash 216 | this is loader 217 | 218 | hello zheng Fang! 219 | welcome to roid, I'm zheng Fang 220 | 221 | if you love roid and learnt any thing, please give me a star 222 | https://github.com/Foveluy/roid 223 | ``` 224 | 225 | ## 参考 226 | 1. https://github.com/blackLearning/blackLearning.github.io/issues/23 227 | 2. https://github.com/ronami/minipack 228 | --------------------------------------------------------------------------------