Compiler 和 Compilation 的区别
webpack
打包离不开 Compiler
和 Compilation
,它们两个分工明确,理解它们是我们理清 webpack
构建流程重要的一步。
Compiler
负责监听文件和启动编译 它可以读取到 webpack
的 config 信息,整个 Webpack
从启动到关闭的生命周期,一般只有一个 Compiler 实例,整个生命周期里暴露了很多方法,常见的 run
,make
,compile
,finish
,seal
,emit
等,我们写的插件就是作用在这些暴露方法的 hook 上
Compilation
负责构建编译。每一次编译(文件只要发生变化,)就会生成一个 Compilation
实例,Compilation
可以读取到当前的模块资源,编译生成资源,变化的文件,以及依赖跟踪等状态信息。同时也提供很多事件回调给插件进行拓展。
完成编译
- 输出资源:(seal 阶段)
在编译完成后,调用 compilation.seal
方法封闭,生成资源,这些资源保存在 compilation.assets
, compilation.chunk
,然后便会调用 emit
钩子,根据 webpack config
文件的 output
配置的 path
属性,将文件输出到指定的 path
.
- 输出完成:
done/failed
阶段
done
成功完成一次完成的编译和输出流程。failed
编译失败,可以在本事件中获取到具体的错误原因 在确定好输出内容后, 根据配置确定输出的路径和文件名, 把文件内容写入到文件系统。
emitAssets(compilation, callback) { const emitFiles = (err) => { //...省略一系列代码 // afterEmit:文件已经写入磁盘完成 this.hooks.afterEmit.callAsync(compilation, (err) => { if (err) return callback(err) return callback() }) } // emit 事件发生时,可以读取到最终输出的资源、代码块、模块及其依赖,并进行修改(这是最后一次修改最终文件的机会) this.hooks.emit.callAsync(compilation, (err) => { if (err) return callback(err) outputPath = compilation.getPath(this.outputPath, {}) mkdirp(this.outputFileSystem, outputPath, emitFiles) }) }
然后,我们来看一下 webpack 打包好的代码是什么样子的。
webpack 输出文件代码分析
未压缩的 bundle.js 文件结构一般如下:
;(function (modules) { // webpackBootstrap // 缓存 __webpack_require__ 函数加载过的模块,提升性能, var installedModules = {} /** * Webpack 加载函数,用来加载 webpack 定义的模块 * @param {String} moduleId 模块 ID,一般为模块的源码路径,如 "./src/index.js" * @returns {Object} exports 导出对象 */ function __webpack_require__(moduleId) { // 重复加载则利用缓存,有则直接从缓存中取得 if (installedModules[moduleId]) { return installedModules[moduleId].exports } // 如果是第一次加载,则初始化模块对象,并缓存 var module = (installedModules[moduleId] = { i: moduleId, l: false, exports: {}, }) // 把要加载的模块内容,挂载到module.exports上 modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ) module.l = true // 标记为已加载 // 返回加载的模块,直接调用即可 return module.exports } // 在 __webpack_require__ 函数对象上挂载一些变量及函数 ... __webpack_require__.m = modules __webpack_require__.c = installedModules __webpack_require__.d = function (exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter, }) } } __webpack_require__.n = function (module) { var getter = module && module.__esModule ? function getDefault() { return module['default'] } : function getModuleExports() { return module } __webpack_require__.d(getter, 'a', getter) return getter } // __webpack_require__对象下的r函数 // 在module.exports上定义__esModule为true,表明是一个模块对象 __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 = './src/index.js')) })({ './src/index.js': function ( module, __webpack_exports__, __webpack_require__ ) { 'use strict' eval( '__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ "./src/moduleA.js");\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_moduleA__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./moduleB */ "./src/moduleB.js");\n/* harmony import */ var _moduleB__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_moduleB__WEBPACK_IMPORTED_MODULE_1__);\n\n\n\n//# sourceURL=webpack:///./src/index.js?' ) }, './src/moduleA.js': function (module, exports) { eval('console.log("moduleA")\n\n//# sourceURL=webpack:///./src/moduleA.js?') }, './src/moduleB.js': function (module, exports) { // 代码字符串可以通过eval 函数运行 eval('console.log("moduleB")\n\n//# sourceURL=webpack:///./src/moduleB.js?') }, })
上述代码的实现了⼀个 webpack_require
来实现⾃⼰的模块化把代码都缓存在 installedModules
⾥,代码⽂件以对象传递进来,key
是路径,value
是包裹的代码字符串,并且代码内部的 require
,都被替换成了 webpack_require
,代码字符串可以通过 eval 函数去执行。
bundle.js
能直接运行在浏览器中的原因在于输出的文件中通过 webpack_require 函数定义了一个可以在浏览器中执行的加载函数来模拟 Node.js
中的 require
语句。
总结一下,生成的 bundle.js
只包含一个立即调用函数(IIFE),这个函数会接受一个对象为参数,它其实主要做了两件事:
- 定义一个模块加载函数
webpack_require。
- 使用加载函数加载入口模块
"./src/index.js"
,从入口文件开始递归解析依赖,在解析的过程中,分别对不同的模块进行处理,返回模块的exports
。
所以我们只需要实现 2 个功能就可以实现一个简单的仿 webpack
打包 js
的编译工具
- 从入口开始递归分析依赖
- 借助依赖图谱来生成真正能在浏览器上运行的代码
实现一个简单的 webpack
接下来从 0 开始实践一个 Webpack
的雏形,能够让大家更加深入了解 Webpack
const fs = require('fs') const path = require('path') const parser = require('@babel/parser') //解析成ast const traverse = require('@babel/traverse').default //遍历ast const { transformFromAst } = require('@babel/core') //ES6转换ES5 module.exports = class Webpack { constructor(options) { const { entry, output } = options this.entry = entry this.output = output this.modulesArr = [] } run() { const info = this.build(this.entry) this.modulesArr.push(info) for (let i = 0; i < this.modulesArr.length; i++) { // 判断有依赖对象,递归解析所有依赖项 const item = this.modulesArr[i] const { dependencies } = item if (dependencies) { for (let j in dependencies) { this.modulesArr.push(this.build(dependencies[j])) } } } //数组结构转换 const obj = {} this.modulesArr.forEach((item) => { obj[item.entryFile] = { dependencies: item.dependencies, code: item.code, } }) this.emitFile(obj) } build(entryFile) { const conts = fs.readFileSync(entryFile, 'utf-8') const ast = parser.parse(conts, { sourceType: 'module', }) // console.log(ast) // 遍历所有的 import 模块,存入dependecies const dependencies = {} traverse(ast, { // 类型为 ImportDeclaration 的 AST 节点, // 其实就是我们的 import xxx from xxxx ImportDeclaration({ node }) { const newPath = './' + path.join(path.dirname(entryFile), node.source.value) dependencies[node.source.value] = newPath // console.log(dependencies) }, }) // 将转化后 ast 的代码重新转化成代码 // 并通过配置 @babel/preset-env 预置插件编译成 es5 const { code } = transformFromAst(ast, null, { presets: ['@babel/preset-env'], }) return { entryFile, dependencies, code, } } emitFile(code) { //生成bundle.js const filePath = path.join(this.output.path, this.output.filename) const newCode = JSON.stringify(code) const bundle = `(function(modules){ // moduleId 为传入的 filename ,即模块的唯一标识符 function require(moduleId){ function localRequire(relativePath){ return require(modules[moduleId].dependencies[relativePath]) } var exports = {}; (function(require,exports,code){ eval(code) })(localRequire,exports,modules[moduleId].code) return exports; } require('${this.entry}') })(${newCode})` fs.writeFileSync(filePath, bundle, 'utf-8') } }
调用
let path = require('path') let { resolve } = path let webpackConfig = require(path.resolve('webpack.config.js')) let Webpack = require('./myWebpack.js') const defaultConfig = { entry: 'src/index.js', output: { path: resolve(__dirname, '../dist'), filename: 'bundle.js', }, } const config = { ...defaultConfig, ...webpackConfig, } const options = require('./webpack.config') new Webpack(options).run()
输入到浏览器看一下执行结果
参考:
- 快狗打车:实现一个简单的
webpack
tapable
详解+webpack
流程- 本文调试以及打断点用到的源码