webpack 打包原理
在 webpack.config.js
添加如下配置。
const path = require('path') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } }
通过基础的 webpack 配置最后会生成如下所示的打包文件
(function (modules) { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); return module.exports; } // 此处省略部分代码 return __webpack_require__(__webpack_require__.s = "./src/index.js"); }) ({ "./src/a.js": (function (module, exports) { eval("exports.a = 'a 模块'\r\n\n\n//# sourceURL=webpack:///./src/a.js?"); }), "./src/index.js": (function (module, exports, __webpack_require__) { eval("const a = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\nconsole.log(a)\r\n\n\n//# sourceURL=webpack:///./src/index.js?"); }) });
通过删减部分代码之后,我们很容易看出代码是通过一个自执行函数处理的
(function (modules) { // 此处省略部分代码 })({})
自执行函数的参数是一个对象,对象的属性名是路径,属性值是函数
{ "./src/a.js": (function (module, exports) { eval("exports.a = 'a 模块'\r\n\n\n//# sourceURL=webpack:///./src/a.js?"); }), "./src/index.js": (function (module, exports, __webpack_require__) { eval("const a = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\nconsole.log(a)\r\n\n\n//# sourceURL=webpack:///./src/index.js?"); }) }
我的原始打包文件是这样的依赖关系
src
a.js -> exports.a = 'a 模块'
index.js -> const a = require('./a');console.log(a);
通过原始文件和打包后的文件对比,可以知道 __webpack_require__
是 webpack 实现的 require
方法,用来加载模块,原理和 commonJS 原理十分类似。因为浏览器是不支持 commonJS 原理,commonJS 原理只能在 node 环境中用,直接在浏览器却无法运行,所以要 webpack 自己去实现模块加载。
这里所说的是打包之后的代码是运行在浏览器,打包这个过程是 node 运行的。
值得注意的是模块的路径打包之后都是相对于当前工作目录的路径,就是相对于运行打包命令的路径。
命令
如果我们要自己实现一个 webpack,就必须写一个 webpack 包,发布的 npm 官方网站中,才能使用 npx webpack
命令,可是在本地开发必须得边开发边验证我们自己写的代码是否正确,我们可以通过 npm link
把包链接到全局,这样就可以不用发布到 npm 官方网站了,就直接使用 npx webpack
命令。开发完成再发布即可。
下面是我的 webpack,名字叫 z-pack
包
{ "name": "z-pack", "version": "1.0.0", "description": "", "main": "index.js", "bin": { "z-pack": "./bin/z-pack.js" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
当在 CMD 中执行命令 z-pack
将会执行 ./bin/z-pack.js
的文件,bin 字段就是 npm 配置可执行文件的字段,是固定的字段名
#! /usr/bin/env node console.log('hello')
在代码的第一行,必须指定代码的运行环境为 node,之后再写 JS 代码。
原理
首先是找到配置文件,然后开始编译执行
#! /usr/bin/env node const path = require('path') const configPath = path.resolve('webpack.config.js') const config = require(configPath) const compiler = new Compiler(config) const Compiler = require('../lib/Compiler') compiler.run()
从入口模块开始创建依赖关系
class Compiler { constructor (config) { this.config = config this.modules = {} this.entry = config.entry // 工作目录 this.root = process.cwd() } buildModule(modulePath, inEntry) { } run () { // 从入口模块开始创建依赖关系 this.buildModule(path.resolve(this.root, this.entry), true) this.emitFile() } emitFile () { } }
计算路径
let fs = require('fs') let path = require('path') class Compiler { constructor (config) { this.config = config this.modules = {} this.entry = config.entry // 工作目录 this.root = process.cwd() } buildModule(modulePath, isEntry) { // 构建模块谱图 let source = this.getSource(modulePath) // 计算相当路径 -> ./src/index let moduleName = './' + path.relative(this.root, modulePath).replace(path.sep, '/'); // src/index 得到路径 ./src let parentName = path.dirname(moduleName) // 标注为主模块 if (isEntry) { this.entryId = moduleName } console.log(moduleName, parentName) } getSource(modulePath) { // 获取模块内容 let content = fs.readFileSync(modulePath, 'utf8') return content } run () { // 从入口模块开始创建依赖关系 this.buildModule(path.resolve(this.root, this.entry), true) this.emitFile() } emitFile () { } } module.exports = Compiler
通过语法树,将 require
替换 __webpack_require__
let fs = require('fs') let path = require('path') const babylon = require('babylon'); const traverse = require('babel-traverse').default; const generate = require('babel-generator').default; class Compiler { constructor (config) { this.config = config this.modules = {} this.entry = config.entry // 工作目录 this.root = process.cwd() } buildModule(modulePath, isEntry) { // 构建模块谱图 let source = this.getSource(modulePath) // 计算相当路径 -> ./src/index let moduleName = './' + path.relative(this.root, modulePath).replace(path.sep, '/'); // src/index 得到路径 ./src let parentName = path.dirname(moduleName) // 标注为主模块 if (isEntry) { this.entryId = moduleName } let {r, dependencies} = this.parse(source, parentName) this.modules[moduleName] = r dependencies.forEach(dep => { this.buildModule(path.join(this.root, dep), false) }) } parse (source, parentDir) { const ast = babylon.parse(source); let dependencies = []; traverse(ast, { CallExpression(p) { // 匹配所有的调用表达式 let node = p.node; if (node.callee.name === 'require') { node.callee.name = '__webpack_require__'; let value = node.arguments[0].value; let ext = path.extname(value); value = ext ? value : `${value}.js`; value = path.join(parentDir, value); value = './' + value.replace(path.sep, '/'); node.arguments[0].value = value; dependencies.push(value); } } }); let r = generate(ast); // this.hooks.parser.call(this); return { r: r.code, dependencies } } getSource(modulePath) { // 获取模块内容 let content = fs.readFileSync(modulePath, 'utf8') return content } run () { // 从入口模块开始创建依赖关系 this.buildModule(path.resolve(this.root, this.entry), true) console.log(this.modules, '模块') this.emitFile() } emitFile () { } } module.exports = Compiler
最后写入打包之后的文件
let fs = require('fs') let path = require('path') const babylon = require('babylon'); const traverse = require('babel-traverse').default; const generate = require('babel-generator').default; class Compiler { constructor (config) { this.config = config this.modules = {} this.entry = config.entry // 工作目录 this.root = process.cwd() } buildModule(modulePath, isEntry) { // 构建模块谱图 let source = this.getSource(modulePath) // 计算相当路径 -> ./src/index let moduleName = './' + path.relative(this.root, modulePath).replace(path.sep, '/'); // src/index 得到路径 ./src let parentName = path.dirname(moduleName) // 标注为主模块 if (isEntry) { this.entryId = moduleName } let {r, dependencies} = this.parse(source, parentName) this.modules[moduleName] = r dependencies.forEach(dep => { this.buildModule(path.join(this.root, dep), false) }) } parse (source, parentDir) { const ast = babylon.parse(source); let dependencies = []; traverse(ast, { CallExpression(p) { // 匹配所有的调用表达式 let node = p.node; if (node.callee.name === 'require') { node.callee.name = '__webpack_require__'; let value = node.arguments[0].value; let ext = path.extname(value); value = ext ? value : `${value}.js`; value = path.join(parentDir, value); value = './' + value.replace(path.sep, '/'); node.arguments[0].value = value; dependencies.push(value); } } }); let r = generate(ast); // this.hooks.parser.call(this); return { r: r.code, dependencies } } getSource(modulePath) { // 获取模块内容 let content = fs.readFileSync(modulePath, 'utf8') return content } run () { // 从入口模块开始创建依赖关系 this.buildModule(path.resolve(this.root, this.entry), true) console.log(this.modules, '模块') this.emitFile() } emitFile () { let ejs = require('ejs'); let templateStr = this.getSource(path.resolve(__dirname, './template.ejs')); let str = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules }) let { filename, path: p } = this.config.output; // 将内容写到文件中 // 资源文件库 this.assets = { [filename]: str } Object.keys(this.assets).forEach(key => { fs.writeFileSync(path.join(p, key), this.assets[key]); }) // this.hooks.emitFile.call(this); } } module.exports = Compiler
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 二叉查找法
下一篇: 不要相信一个熬夜的人说的每一句话
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论