完整代码
一个简单的webpack结果模版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 (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__ ); module.l = true; return module.exports; } __webpack_require__.d = function(exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } }; __webpack_require__.r = function(exports) { if (typeof Symbol !== "undefined" && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); } Object.defineProperty(exports, "__esModule", { value: true }); }; __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; return __webpack_require__((__webpack_require__.s = "<%- entryId%>")); })({ <% for (let moduleName in modules) {%> "<%- moduleName%>": function(module, __webpack_exports__, __webpack_require__) { "use strict"; eval( `<%-modules[moduleName]%>` ); }, <% }%> });
简单来说, webpack主流程上就作了这么几件事情
编译一个入口文件(代码文件)
解析并改造代码如将 import、require转换成 __webpack_require__
收集依赖的模块并重复2
生成文件并导出上面的模版。
构建代码骨架 先搭建一个基本的类骨架,先不考虑1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Compiler{ constructor(config) { // 读取webpack.config.js并初始化 } buildModule(modulePath, isEntry) { // 构建模块 } emitFile() { // 渲染模版并生成文件。 } getSource(path) { //读取本地文件内容 } parse(source, parentPath) { // 将源代码解析成 ast语法树并处理 } run() { // 执行并创建模块的依赖关系 } } module.exports = Compiler
初始化 1 2 3 4 5 6 7 8 9 10 constructor(config) { this.config = config // 保存入口文件 this.entryId ; // 保存所有的模块依赖 this.modules = {} // 工作路径 this.root = process.cwd() this.entry = config.entry }
构建模块 构建模块的基本思路
读取代码文件
构建模块id:./path/[name].[ext]
解析代码、修改代码、收集依赖。
1.首先借助工具babylon将代码解析成ast语法树,方便处理
使用工具@babel/traverse遍历ast节点
检测require,取出require函数的第一个参数,让它做为当前模块的依赖
将require 函数名修改成__webpack_require__,将参数修改成webpack支持的./path/[name].[js]
将修改后的ast还原成代码
将代码和依赖列表返回
4.递归依赖列表,重复以上步骤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 let babylon = require('babylon') let traverse = require('@babel/traverse').default let babelTyeps = require('@babel/types') let generator = require('@babel/generator').default getSource(path) { let content = fs.readFileSync(path, 'utf8') return content } parse(source, parentPath) {// Ast 语法解析 // 将源代码解析成 ast语法树 let deps = [] let ast = babylon.parse(source) // 遍历ast节点 traverse(ast, { CallExpression(p) { let 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(parentPath, moduleName) // ./src/a.js deps.push(moduleName) // 替换node arguments 的值 node.arguments = [babelTyeps.stringLiteral(moduleName)] } } }) let sourceCode = generator(ast).code return { sourceCode, deps} } buildModule(modulePath, isEntry) { // 读取模块内容 let source = this.getSource(modulePath) // 构建模块Id let moduleName = './' + path.relative(this.root, modulePath) if (isEntry) { this.entryId = moduleName } // 对源代码进行改造,并返回依赖列表。 let {sourceCode, deps} = this.parse(source, path.dirname(moduleName)) this.modules[moduleName] = sourceCode // 递归加载依赖模块。 deps.forEach((dep) => { this.buildModule(path.join(this.root, dep), false) }) }
导出文件 基本思路,使用 ejs模版,将模块信息填充进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 emitFile() { // 渲染模版并生成文件。 let { output } = this.config let main = path.join(output.path, output.filename) let tpl = this.getSource('./src/tpl.ejs') let code = ejs.render(tpl, { entryId: this.entryId, modules: this.modules, }) this.assets = {} this.assets[main] = code; fs.writeFileSync(main, code) }
爆露一个执行方法 1 2 3 4 5 6 run() { // 执行并创建模块的依赖关系 this.buildModule(path.resolve(this.root, this.entry), true) // 发射 打包后的文件。 this.emitFile() }
扩展loader 以上就完成了主流程的代码,下面来扩张webpack的loader。 首先,loader所作的事情就是在加载文件时匹配hash,如果正则匹配则调用loader去处理它并返回处理后的结果。
那么,先改造一下 getSource
1 2 3 4 5 getSource(path) { let content = fs.readFileSync(path, 'utf8') // return content return this.progressLoaders(path, content) }
再来实现 this.progressLoaders方法。loader有几个注意事项:
loader 是一个数组并从下往上执行
每一个loader的执行结果都会返回给下一个待执行的loader
loader是一个可执行函数。
那么思路就很清晰了,只要在正则匹配之后依次执行数组里的loader即可。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 progressLoaders(modulePath, content){ let rules = this.config.module.rules rules.forEach((rule) => { const { test:match, use} = rule let len = use.length - 1 function normalLoader() { let p = use[len] let moduleName = p.loader ? p.loader : p let loader = require(moduleName) content = loader(content) } if (match.test(modulePath)) { do { normalLoader() len-- } while (len >= 0) } }) return content }
扩展plugins 插件即钩子,插件的实现就很简单了。只要在constructor的时候添初始化钩子,然后在合适的地方调用即可。
改造 constructor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 constructor(config) { this.config = config // 保存入口文件 this.entryId ; // 保存所有的模块依赖 this.modules = {} // 工作路径 this.root = process.cwd() this.entry = config.entry this.initHooks() this.progressPlugins() } initHooks() { // 配置钩子 this.hooks = { entryOption: new SyncHook(), compile: new SyncHook(), afterCompile: new SyncHook(), afterPlugins: new SyncHook(), run: new SyncHook(), emit: new SyncHook(), done: new SyncHook() } } progressPlugins() { // 初始化用户配置插件 // 挂载钩子函数 let { plugins } = this.config if (Array.isArray(plugins)) { plugins.forEach((plugin) => { plugin.apply(this) }) this.hooks.afterPlugins.call(this) } }
在合适的地方调用钩子
1 2 3 4 5 6 7 8 9 10 11 run() { // 执行并创建模块的依赖关系 this.hooks.run.call(this) this.hooks.compile.call(this) this.buildModule(path.resolve(this.root, this.entry), true) this.hooks.afterCompile.call(this) // 发射 打包后的文件。 this.emitFile() this.hooks.emit.call(this) this.hooks.done.call(this) }