编译产物剖析
(() => { // 模块依赖 var __webpack_modules__ = ({ "./src/index.js": ((module, __unused_webpack_exports, __webpack_require__) => { // 执行模块代码其中 同时执行__webpack_require__ 援用代码 eval(`const str = __webpack_require__("./src/a.js");console.log(str);`); }), "./src/a.js": ((module, __unused_webpack_exports, __webpack_require__) => { eval(`const b = __webpack_require__("./src/base/b.js");module.exports = 'a' + b;`); }), "./src/base/b.js": ((module, __unused_webpack_exports, __webpack_require__) => { eval(`module.exports = 'b';`); }), }); var __webpack_module_cache__ = {}; function __webpack_require__(moduleId) { // 获取_webpack_module_cache__ 是否有exports值 var cachedModule = __webpack_module_cache__[moduleId]; // 如果曾经有了,不必再执行模块代码 if (cachedModule !== undefined) { return cachedModule.exports; } var module = __webpack_module_cache__[moduleId] = { exports: {} }; // 依据moduleId 模块文件门路,找到模块代码并执行传入 module, module.exports, __webpack_require__ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); return module.exports; } // 执行入口文件代码 var __webpack_exports__ = __webpack_require__("./src/index.js"); })()
以上代码是通过精简过的,能够看到以下工具函数
- __webpack_modules__:是一个对象,它的值是所有模块的代码,key值对应是模块文件门路
- __webpack_module_cache__: 缓存exports的值
- __webpack_require__:加载模块代码,依据模块文件门路
- __webpack_exports__:模块对外裸露办法
通过以上工具办法,就能够在浏览器run起来;从源代码es6、es7 新个性新写法,都须要转成浏览器可辨认的代码;
如:
// es6import // es5 __webpack_require__
webpack通过自定义__webpack_require__、__webpack_exports__ ...,实现多个模块代码打包。
接下来将依照上述逻辑,来构建简易版的webpack,通过以下几个阶段
- 配置信息
- 依赖构建
- 生成模版代码
- 生成文件
配置信息
class Compiler { constructor(config) { // 获取配置信息 this.config = config; // 保留入口门路 this.entryId; // 模块依赖关系 this.modules = {}; // 入口门路 this.entry = config.entry; // 工作门路 this.root = process.cwd(); }
构建依赖
getSource(modulePath) { const rules = this.config.module.rules; let content = fs.readFileSync(modulePath, 'utf8'); return content; }buildModule(modulePath, isEntry) { // 拿到模块内容 const source = this.getSource(modulePath); // 模块id const moduleName = './' + path.relative(this.root, modulePath); if (isEntry) { this.entryId = moduleName; } // 解析源码须要把source 源码进行革新,返回一个依赖列表 const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src // 把相对路径和模块中的内容,对应起来 this.modules[moduleName] = sourceCode; dependencies.forEach((dep) => { // 递归加载模块 this.buildModule(path.join(this.root, dep), false) }) }
通过buildModule
解析源码,造成模块依赖this.modules[moduleName
;
- 找到模块源码
this.getSource(modulePath);
- 解析源码,转换ast,返回源码和模块依赖门路
this.parse(source, path.dirname(moduleName))
- 生成门路与模块代码对象:
this.modules[moduleName] = sourceCode
- 对模块中有依赖的文件,造成迭代调用
this.buildModule(path.join(this.root, dep), false)
从新执行以上办法
解析源码
parse(source, parentPatch) { // AST 解析语法树 const ast = babylon.parse(source); let dependencies = []; // 依赖数组 traverse(ast, { CallExpression(p) { const node = p.node; if (node.callee.name == 'require') { node.callee.name = '__webpack_require__'; let moduleName = node.arguments[0].value; // 模块名字 moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js moduleName = './' + path.join(parentPatch, moduleName); // src/a.js dependencies.push(moduleName); node.arguments = [t.stringLiteral(moduleName)]; } } }); const sourceCode = generator(ast).code; return { sourceCode, dependencies } }
解析模块源代码,替换require办法成__webpack_require__
,同时把文件门路也转换掉
代码生成模版
// ejs 模版代码(() => {var __webpack_modules__ = ({<%for(let key in modules){%> "<%-key%>": ((module, __unused_webpack_exports, __webpack_require__) => { eval(`<%-modules[key]%>`); }),<%}%>});var __webpack_module_cache__ = {};function __webpack_require__(moduleId) {var cachedModule = __webpack_module_cache__[moduleId];if (cachedModule !== undefined) {return cachedModule.exports;}var module = __webpack_module_cache__[moduleId] = {exports: {}};__webpack_modules__[moduleId](module, module.exports, __webpack_require__);return module.exports;}var __webpack_exports__ = __webpack_require__("<%-entryId%>");})();
将把this.modules
、this.entryId
数据,传入此模版中,生成可执行代码
生成文件
emitFile() { const {output} = this.config; const main = path.join(output.path, output.filename); // 模块字符串 let templateStr = this.getSource(path.join(__dirname, 'main.ejs')); // 生成代码 const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules}); this.assets = {}; this.assets[main] = code; // 将代码写入output文件夹/文件 fs.writeFileSync(main, this.assets[main]) }
loader
将援用资源,转换成模块 getSource(modulePath) { const rules = this.config.module.rules; let content = fs.readFileSync(modulePath, 'utf8'); for (let i = 0; i < rules.length; i++) { const rule = rules[i]; const {test,use} = rule; let len = use.length -1 if(test.test(modulePath)) { function normalLoader() { const loader = require(use[len--]); content = loader(content); if(len >= 0) { normalLoader(); } } normalLoader(); } } return content; }
依据门路获取源码,判断以后门路是否能匹配上loader文件test.test(modulePath)
,
如果能够匹配,将模块源码传入,loader办法中,再做其余转换 content = loader(content);
并且造成递归调用;
// 自定义loader// less-loaderconst {render} = require('less')function loader(source) { let css = ''; render(source,(err,c) => { css = c; }) css = css.replace(/\n/g,'\\n') return css;}module.exports = loader;// style-loaderfunction loader(source) { let style = ` let style = document.createElement('style') style.innerHTML = ${JSON.stringify(source)} document.head.appendChild(style); `; return style;}module.exports = loader;
配置文件
const path = require('path');module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle2.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.less$/, use:[ path.resolve(__dirname,'loader','style-loader'), // 后执行 path.resolve(__dirname,'loader','less-loader') // 先执行 ] } ] },}
plugin
从状态上看,插件通常是一个带有 apply 函数的类:
class SomePlugin { apply(compiler) { }}
apply 函数运行时会失去参数 compiler ,以此为终点能够调用 hook 对象注册各种钩子回调,
例如:compiler.hooks.make.tapAsync ,这外面 make 是钩子名称,tapAsync 定义了钩子的调用形式,
webpack 的插件架构基于这种模式构建而成,插件开发者能够应用这种模式在钩子回调中,插入特定代码
配置文件
const path = require('path');class P { constructor() { } apply(compiler) { // 获取compiler上办法,注册各个阶段回调 compiler.hooks.emit.tap('emit',function () { console.log('emit') }) }}module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle2.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new P() ]}
compiler.js
const {SyncHook} = require('tapable');class Compiler { constructor(config) { this.config = config; // 保留入口门路 this.entryId; // 模块依赖关系 this.modules = {}; // 入口门路 this.entry = config.entry; // 工作门路 this.root = process.cwd(); // 开始注册同步公布订阅 this.hooks = { entryOption:new SyncHook(), compile:new SyncHook(), afterCompile:new SyncHook(), afterPlugins:new SyncHook(), run:new SyncHook(), emit:new SyncHook(), done:new SyncHook() }; const plugins = this.config.plugins; // 拿到配置项里的plugin if(Array.isArray(plugins)) { plugins.forEach((plugin) => { // 调用plugin 中实例办法 apply,并传入整个Compiler 类 plugin.apply(this); }) } this.hooks.afterPlugins.call(); }
plugin 外围就是tapable
采纳公布/订阅的模式,先收集/订阅插件中所须要回调,在webpack生命周期中去执行,这样插件就能够在应用的机会,获取想要的上下文,从而进行干涉以及其余操作。
以上就是各个阶段要害外围代码局部
残缺代码
const path = require('path');const fs = require('fs');const babylon = require('babylon');const traverse = require('@babel/traverse').default;const t = require('@babel/types');const generator = require('@babel/generator').default;const ejs = require('ejs');const {SyncHook} = require('tapable');// babylon 解析 js 转换 ast// https://www.astexplorer.net/// @babel/travers// @babel/types// @babel/generatorclass Compiler { constructor(config) { this.config = config; // 保留入口门路 this.entryId; // 模块依赖关系 this.modules = {}; // 入口门路 this.entry = config.entry; // 工作门路 this.root = process.cwd(); this.hooks = { entryOption:new SyncHook(), compile:new SyncHook(), afterCompile:new SyncHook(), afterPlugins:new SyncHook(), run:new SyncHook(), emit:new SyncHook(), done:new SyncHook() }; const plugins = this.config.plugins; if(Array.isArray(plugins)) { plugins.forEach((plugin) => { plugin.apply(this); }) } this.hooks.afterPlugins.call(); } getSource(modulePath) { const rules = this.config.module.rules; let content = fs.readFileSync(modulePath, 'utf8'); for (let i = 0; i < rules.length; i++) { const rule = rules[i]; const {test,use} = rule; let len = use.length -1 if(test.test(modulePath)) { function normalLoader() { const loader = require(use[len--]); content = loader(content); if(len >= 0) { normalLoader(); } } normalLoader(); } } return content; } parse(source, parentPatch) { // AST 解析语法树 const ast = babylon.parse(source); let dependencies = []; // 依赖数组 traverse(ast, { CallExpression(p) { const node = p.node; if (node.callee.name == 'require') { node.callee.name = '__webpack_require__'; let moduleName = node.arguments[0].value; // 模块名字 moduleName = moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js moduleName = './' + path.join(parentPatch, moduleName); // src/a.js dependencies.push(moduleName); node.arguments = [t.stringLiteral(moduleName)]; } } }); const sourceCode = generator(ast).code; return { sourceCode, dependencies } } buildModule(modulePath, isEntry) { // 拿到模块内容 const source = this.getSource(modulePath); // 模块id const moduleName = './' + path.relative(this.root, modulePath); if (isEntry) { this.entryId = moduleName; } // 解析源码须要把source 源码进行革新,返回一个依赖列表 const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src // 把相对路径和模块中的内容,对应起来 this.modules[moduleName] = sourceCode; dependencies.forEach((dep) => { // 递归加载模块 this.buildModule(path.join(this.root, dep), false) }) } emitFile() { const {output} = this.config; const main = path.join(output.path, output.filename); let templateStr = this.getSource(path.join(__dirname, 'main.ejs')); const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules}); this.assets = {}; this.assets[main] = code; fs.writeFileSync(main, this.assets[main]) } run() { this.hooks.run.call(); this.hooks.compile.call(); // 执行,并且创立模块的依赖关系 this.buildModule(path.resolve(this.root, this.entry), true); this.hooks.afterCompile.call(); // 发射一个文件,打包后的文件 this.emitFile(); this.hooks.emit.call(); this.hooks.done.call(); }}module.exports = Compiler;
github链接:
https://github.com/NoahsDante...
如果对你有帮忙,点个start