webpack是近几年前端比较流行的打包工具,基本上是目前所有前端都必须要掌握的开发利器。不过,光停留在使用工具的阶段上,是难以得到成长的。因此,这篇文章将带大家手把手自己实现webpack的核心功能。
其实,webpack的核心功能是分为如下几个步骤:
接下来,我们以一个简单的例子作为开始,作为测试用例。
先建立一个入口main.js
文件:
import moduleB from "./moduleB"; console.log(moduleB());
接着,建立moduleA和moduleB两个模块:
export default { getName: () => { return "scq000"; } }
模块B代码如下:
import moduleA from "./moduleA"; console.log(moduleA());
其中模块B引用模块A,这样我们就实现了一个简单的demo例子。
有了这个测试例子,接着我们就可以来实现我们自定义的webpack功能。
先创建一个myWebpack.js
的文件,我们可以通过一个createAsset
函数读取main.js
入口文件,并生成一个最终可以直接在浏览器上运行的代码。
第一步,是要先读取文件内容,这个实现起来比较简单:
const fs = require('fs'); function createAsset(filename) { const content = fs.readFileSync(filename, 'utf8'); console.log(content); } createAsset('./example/main.js');
可以在命令行中输入如下命令进行查看:
node myWebpack.js | js-beautify | highlight
接着,我们就需要根据源码文件,进行解析。这里,我们可以借助AST Explorer来生成代码的AST语法树,然后找ImportDeclaration
语句,先打印出来看看效果:
const fs = require('fs'); const babylon = require('baylon'); const traverse = require('babel-traverse').default; function createAsset(filename) { const content = fs.readFileSync(filename, 'utf8'); // 根据源码内容生成语法书 const ast = babylon.parse(content, { sourceType: 'module' }); // traverse方法,用来操作语法树 traverse(ast, { ImportDeclaration: ({node}) => { console.log(node); } }); }
接着,我们就要根据找到的import语句,将对应的依赖模块放入到dependencies
数组中去,并给它赋予id。修改代码如下:
const fs = require('fs'); const babylon = require('babylon'); const traverse = require('babel-traverse').default; let ID = 0; function createAsset(filename) { const content = fs.readFileSync(filename, 'utf8'); const ast = babylon.parse(content, { sourceType: 'module' }); const dependencies = []; traverse(ast, { ImportDeclaration: ({node}) => { dependencies.push(node.source.value); } }); const id = ID++; return { id, filename, dependencies } }; const result = createAsset('./main.js'); console.log(result);
打印出来,可以看到如下结果:
有了上面的基础,我们就能根据入口文件递归生成依赖图了:
function createGraph(entry) { const mainAsset = createAsset(entry); const queue = [mainAsset]; for(const asset of queue) { const dirname = path.dirname(asset.filename); asset.mapping = {}; asset.dependencies.forEach(relativePath => { const absolutePath = path.join(dirname, relativePath); const child = createAsset(absolutePath); asset.mapping[relativePath] = child.id; queue.push(child); }); } console.log(queue); } createGraph('./main.js');
最后一步,就是需要根据获得的模块依赖信息,合并模块并生成可执行的文件。
为了保证代码可在浏览器上成功运行,这里,我们需要借助babel
工具进行代码的转换。
先安装依赖:
npm install babel-core babel-preset-env
接着,在createAsset
方法中实现如下代码:
const {code} = babel.transformFromAst(ast, null, { presets: ['env'] });
然后再将转换后的代码放入依赖图中,便于后续拼接。
最终依赖图结果如下:
所有准备工作完成后,开始实现bundle方法。在bundle方法里,主要是根据依赖图信息,将所有的模块组装到一个字符串中去,最后再输出成文件就可以了。
function bundle(graph) { let modules = ''; // 遍历graph, 生成代码 graph.forEach(mod => { modules += `${mod.id}: [ function (require, module, exports) { ${mod.code} }, ]` }) //组装模块 const result = `(function() { })({${modules}})`; return result; }
接着我们需要自己去实现require语句:
const modMap = JSON.stringify(mod.mapping); modules += `${mod.id}: [ function (require, module, exports) { ${mod.code} }, ${modMap} ]`;
将modules作为参数传给该立即执行函数,然后直接执行第一个模块就可以了:
(function(modules) { function require(id) { const [fn, mapping] = modules[id]; function localRequire(relativePath) { return require(mapping[relativePath]); } const module = { exports: {} }; fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}})
最终,完整代码如下:
function bundle(graph) { let modules = ''; // 遍历graph, 生成代码 graph.forEach(mod => { const modMap = JSON.stringify(mod.mapping); modules += `${mod.id}: [ function (require, module, exports) { ${mod.code} }, ${modMap} ],`; }); //组装模块 const result = `(function(modules) { function require(id) { const [fn, mapping] = modules[id]; function localRequire(relativePath) { return require(mapping[relativePath]); } const module = { exports: {} }; fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}})`; return result; }
至此,我们完成了最基础的打包工作,可以在命令行中执行一下试试效果:
node myWebpack.js > build.js && node build.js