webpack原理
webpack流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :
- 初始化参数:读取webpack配置参数,从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
 - 开始编译:启动webpack,创建compiler对象。用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
 - 确定入口:从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树。
 - 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
 - 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
 - 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
 - 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
 
打包器
https://mp.weixin.qq.com/s/PeSbdScrvsO0ctECGGY1bQ
https://mp.weixin.qq.com/s/gW_2sDfX5o4wamoiZMsxCw
所谓打包器,就是前端开发人员用来将 JavaScript 模块打包到一个可以在浏览器中运行的优化的 JavaScript 文件的工具,例如 webapck、rollup、gulp 等。
<html><script src="/src/entry.js"></script><script src="/src/message.js"></script><script src="/src/hello.js"></script><script src="/src/name.js"></script></html>
主要工作
模块化 在 HTML 引入时,我们需要注意这 4 个文件的引入顺序(如果顺序出错,项目就会报错),如果将其扩展到具有实际功能的可用的 web 项目中,那么可能需要引入几十个文件,依赖关系更是复杂。打包器需要管理模块依赖关系
打包 当浏览器打开该网页时,每个 js 文件都需要一个单独的 http 请求,即 4 个往返请求,才能正确的启动你的项目。
实现思路
1. 解析入口文件,获取所有的依赖项
首先我们唯一确定的是入口文件的地址,通过入口文件的地址可以
- 获取其文件内容
 - 获取其依赖模块的相对地址
 
由于依赖模块的引入是通过相对路径(import './message.js'),所以,我们需要保存入口文件的路径,结合依赖模块的相对地址,就可以确定依赖模块绝对地址,读取它的内容。
如何在依赖关系中去表示一个模块,以方便在依赖图中引用
将模块表示为:
- code: 文件解析内容,注意解析后代码能够在当前以及旧浏览器或环境中运行;
 - dependencies: 依赖数组,为所有依赖模块路径(相对)路径;
 - filename: 文件绝对路径,当 
import依赖模块为相对路径,结合当前绝对路径,获取依赖模块路径; 
其中 filename(绝对路径) 可以作为每个模块的唯一标识符,通过 key: value 形式,直接获取文件的内容一依赖模块:
// 模块'src/entry': {code: '', // 文件解析后内容dependencies: ["./message.js"], // 依赖项}
2. 递归解析所有的依赖项,生成一个依赖关系图
我们已经确定了模块的表示,那怎么才能将这所有的模块关联起来,生成一个依赖关系图,通过这个依赖关系可以直接获取所有模块的依赖模块、依赖模块的代码、依赖模块的来源、依赖模块的依赖模块。
如何去维护依赖文件间的关系
现在对于每一个模块,可以唯一表示的就是 filename ,而我们在由入口文件递归解析时,我们可以获取到每个文件的依赖数组 dependencies ,也就是每个依赖项的相对路径,所以我们需要定义一个:
// 关联关系let mapping = {}
用来在运行代码时,由 import 相对路径映射到 import 绝对路径。
所以我们模块可以定义为[filename: {}]:
// 模块'src/entry': {code: '', // 文件解析后内容dependencies: ["./message.js"], // 依赖项mapping:{"./message.js": "src/message.js"}}
则依赖关系图为:
// graph 依赖关系图let graph = {// entry 模块"src/entry.js": {code: '',dependencies: ["./src/message.js"],mapping:{"./message.js": "src/message.js"}},// message 模块"src/message.js": {code: '',dependencies: [],mapping:{},}}
当项目运行时,通过入口文件成功获取入口文件代码内容,运行其代码,当遇到 import 依赖模块时,通过 mapping 映射其为绝对路径,就可以成功读取模块内容。
并且每个模块的绝对路径 filename 是唯一的,当我们将模块接入到依赖图 graph 时,仅仅需要判断 graph[filename] 是否存在,如果存在就不需要二次加入,剔除掉了模块的重复打包。
3. 使用依赖图,返回一个可以在浏览器运行的 JavaScript 文件
现今,可立即执行的代码形式,最流行的就是 IIFE(立即执行函数),它同时能够解决全局变量污染的问题。
I
所谓 IIFE,就是在声明市被直接调用的匿名函数,由于 JavaScript 变量的作用域仅限于函数内部,所以你不必考虑它会污染全局变量。
(function(man){function log(name) {console.log(`hello ${name}`);}log(man.name)})({name: 'bottle'});// hello bottle
4. 输出到 dist/bundle.js
fs.writeFile 写入 dist/bundle.js 即可。
我们看下最后打包出来的bundle
(function(modules) {// 模拟 require 语句function __webpack_require__() {}// 执行存放所有模块数组中的第0个模块__webpack_require__(0);})([/*存放所有模块的数组 依赖图*/])
(function(modules) { //// The module cachevar installedModules = {};function __webpack_require__(moduleId) {// ...省略细节}// 入口文件return __webpack_require__(__webpack_require__.s = "./src/index.js");})({"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),"./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__) {})});
- IIFE立即执行函数,保证bundle在浏览器中立即执行
 - 入参是依赖图,从入口出发的依赖关系,依赖图的基本结构,moduleID,code
 - 将依赖图传入立即执行函数中,通过webpack_require模拟require,抹平不同模块规范差异,同时实现了缓存模块的机制
 
Loader 就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多个翻译员翻译。
- 单一原则: 每个 Loader 只做一件事;
 - 链式调用: Webpack 会按顺序链式调用每个 Loader;
 - 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;
 
为什么链式调用从后往前顺序? compose = (f,g) => (…args) => f(g(…args))
以处理 SCSS 文件为例:
- SCSS 源代码会先交给 
sass-loader把 SCSS 转换成 CSS; - 把 
sass-loader输出的 CSS 交给css-loader处理,找出 CSS 中依赖的资源、压缩 CSS 等; - 把 
css-loader输出的 CSS 交给style-loader处理,转换成通过脚本加载的 JavaScript 代码; 
一个单纯的loader如下,没有对源码做任何处理
module.exports = function(source) {// source 为 compiler 传递给 Loader 的一个文件的原内容// 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换return source;};
loader中的api
webpack给loader注入this,this上挂载了很多属性和方法 loader 存在运行上下文 context,表示在 loader 内使用 this 可以访问的一些方法或属性
this.callback是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。
module.exports = function(source) {// 通过 this.callback 告诉 Webpack 返回的结果this.callback(null, source, sourceMaps);// 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,// 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中return;};this.callback(// 当无法转换原内容时,给 Webpack 返回一个 Errorerr: Error | null,// 原内容转换后的内容content: string | Buffer,// 用于把转换后的内容得出原内容的 Source Map,方便调试sourceMap?: SourceMap,// 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,// 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能abstractSyntaxTree?: AST);
异步
module.exports = function(source) {// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果var callback = this.async();someAsyncOperation(source, function(err, result, sourceMaps, ast) {// 通过 callback 返回异步执行后的结果callback(err, result, sourceMaps, ast);});};
this.cacheable:是否开启缓存this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是/src/main.js,则this.context就等于/src。this.resource:当前处理文件的完整请求路径,包括querystring,例如/src/main.js?name=1。this.resourcePath:当前处理文件的路径,例如/src/main.js。this.resourceQuery:当前处理文件的querystring。this.target:等于 Webpack 配置中的 Target。this.loadModule:但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过this.loadModule(request: string, callback: function(err, source, sourceMap, module))去获得request对应文件的处理结果。this.resolve:像require语句一样获得指定文件的完整路径,使用方法为resolve(context: string, request: string, callback: function(err, result: string))。this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为addDependency(file: string)。this.addContextDependency:和addDependency类似,但addContextDependency是把整个目录加入到当前正在处理文件的依赖中。使用方法为addContextDependency(directory: string)。this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为clearDependencies()。this.emitFile:输出一个文件,使用方法为emitFile(name: string, content: Buffer|string, sourceMap: {...})。
在定义一个 loader 函数时,可以导出一个 pitch 方法,这个方法会在 loader 函数执行前执行。
- remainingRequest: 后面的 loader + 资源路径,loadername! 的语法
 - precedingRequest: 资源路径
 - metadata: 和普通的 loader 函数的第三个参数一样,辅助对象
 


手动实现
让我们来手动实现loader吧!
package.json
"loader":"npx webpack --config webpack-loader-test.js"
{test: /\.js$/,exclude: /(node_modules)/,// use: [// path.resolve(__dirname,'loaders/b-loader.js'),// path.resolve(__dirname,'loaders/a-loader.js')// ]use:[path.resolve(__dirname,'loaders/upper-loader.js')]},
module.exports = function(source){return source.replace(/const/g,'let')}
项目中实践的loader
/*** 这是一个 Webpack 插件,实现了对 async(require('资源'), '分组') 语法的支持* devtools 目前使用的 Webpack 1.x 动态引入依赖的语法较为繁琐,所以做了一种简写*/var template = `(new Promise(function (resolve) {require.ensure([], function () {var module = require($1);resolve(module.__esModule ? module.default : module);}, $2);}))`.replace(/\s+/g, ' ');module.exports = function (content) {this.cacheable && this.cacheable();return content.replace(/async\s*\(\s*require\s*\((\s*[^)]*)\s*\)\s*,\s*([^)]*)\)/g, template);};
/*** 这是一个 Webpack 插件,效果为在资源内容的开头加一行资源路径的注释* 主要用于在调试时快速定位 Webpack 插入的 <style> 标签来自哪个 CSS 文件*/var cwd = process.cwd();var getRelativePath = function (path) {return path.replace(cwd, '.').replace(/.*\/node_modules\//, '');};module.exports = function(content) {this.cacheable && this.cacheable();var path = getRelativePath(this.resourcePath);return '/* ' + path + ' */\n' + content;};

/*** 这是一个 Webpack 插件,用于改变react的引入包* 通过判断 路径是否含有 'mtd' 跟 是否引入了 @ss/mtd-react 来判断引入react16 还是 react15* (content.includes('from \'react\'') || content.includes('from \"react\"') || content.includes('require(\'react\')') || content.includes('require(\"react\")')))*/module.exports = function (content) {const entry = this._compilation.options.entry || [];const reactReg = RegExp(/(from\s+|require\()[\'|\"]react[\'|\"|\/]/, 'g');const mtdReactReg = RegExp(/(from\s+|require\()[\'|\"](\@ss\/mtd\-react|\@block\/plug)[\'|\"|\/]/, 'g');// utils需要替换reactif ((this.resourcePath.includes('node_modules\/mtf.utils') || !this.resourcePath.includes('node_modules')) &&entry[0].split('.js')[0] != this.resource.split('.js')[0] &&!this.resourcePath.includes('mtd') &&!content.match(mtdReactReg) &&content.match(reactReg)) {this.cacheable && this.cacheable();return content.replace(/from\s+[\'|\"]react[\'|\"]/g, 'from \'@mtfe/react\'').replace(/require\([\'|\"]react[\'|\"]\)/g, 'require(\'@mtfe/react\')').replace(/from\s+\"react\//g, 'from \"@mtfe/react/').replace(/from\s+\'react\//g, 'from \'@mtfe/react/').replace(/require\(\'react\//g, 'require(\'@mtfe/react\/').replace(/require\(\"react\//g, 'require(\"@mtfe/react\/');}return content;};
手动实现plugin
Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
plugin流程
一个最基础的 Plugin 的代码是这样的:
class BasicPlugin{// 在构造函数中获取用户给该插件传入的配置constructor(options){}// Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象apply(compiler){compiler.plugin('compilation',function(compilation) {})}}// 导出 Pluginmodule.exports = BasicPlugin;
在使用这个 Plugin 时,相关配置代码如下:
const BasicPlugin = require('./BasicPlugin.js');module.export = {plugins:[new BasicPlugin(options),]}
- Webpack 启动后,在读取配置的过程中会先执行 
new BasicPlugin(options)初始化一个BasicPlugin获得其实例。 - 在初始化 
compiler对象后,再调用basicPlugin.apply(compiler)给插件实例传入compiler对象。 - 插件实例在获取到 
compiler对象后,就可以通过compiler.plugin(事件名称, 回调函数)监听到 Webpack 广播出来的事件。 并且可以通过compiler对象去操作 Webpack。 
Compiler 和 Compilation
在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:
- Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 
options,loaders,plugins这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例; - Compilation 代表了一次资源版本构建对象, 包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
 
Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
事件流
Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 
插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。
Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。
在 webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 钩子 改变输出结果。在每个生命周期点,webpack 会运行所有注册的插件,并提供当前 webpack 编译状态信息。主要就是挂载在 compiler 和 compilation 对象提供的各种钩子上进行调用。
Compiler 和 Compilation 都继承自 Tapable tap,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:
/*** 广播出事件* event-name 为事件名称,注意不要和现有的事件重名* params 为附带的参数*/compiler.apply('event-name',params);/*** 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。* 同时函数中的 params 参数为广播事件时附带的参数。*/compiler.plugin('event-name',function(params) {});
同理,compilation.apply 和 compilation.plugin 使用方法和上面一致。
在开发插件时,你可能会不知道该如何下手,因为你不知道该监听哪个事件才能完成任务。
在开发插件时,还需要注意以下两点:
- 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
 - 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
 - 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:
 
compiler.plugin('emit',function(compilation, callback) {// 支持处理逻辑// 处理完毕后执行 callback 以通知 Webpack// 如果不执行 callback,运行流程将会一直卡在这不往下执行callback();});

具体参见  compiler钩子api  compliation钩子
entryOption/afterPlugins/beforeCompile/emit/compilation
class BasicPlugin {constructor(options) {this.options = options;}apply(compiler) {const options = this.options;compiler.hooks.emit.tap("BasicPlugin", (complication) => {let str;// 在emit钩子上 获取打包后的文件 main.jsfor (let file in complication.assets) {console.log(file, complication.assets[file].size());str += `文件:${file} 大小${complication.assets[file]["size"]()}\n`;}// 通过compilation.assets可以获取打包后静态资源信息,同样也可以写入资源// 在打包输出后dist文件中有fileSize.md文件complication.assets["fileSize.md"] = {source: () => str,size: () => str.length};});}}module.exports = BasicPlugin;

项目中实践plugin
const RawSource = require("webpack-sources");var HtmlLibraryPlugin = function (libraries, libraryManifest) {this.libraries = libraries;this.libraryManifest = libraryManifest;};HtmlLibraryPlugin.prototype.apply = function (compiler) {compiler.plugin('emit', function (compilation, callback) {var publicPath = compiler.options.output.publicPath;var libraryTags = this.libraries.map(function (library) {return '<script src="' + publicPath + this.libraryManifest[library] + '"></script>';}.bind(this)).join('\n');var html = compilation.assets['index.html'].source();html = html.replace(/<script /, libraryTags + '\n<script ');compilation.assets['index.html'] = new RawSource(html);callback();}.bind(this));};module.exports = HtmlLibraryPlugin;
const LxConfig = require('./template/LxConfig');const OwlConfig = require('./template/OwlConfig');/*** 在html中增加逻辑* @param {*} LxOptions 灵犀配置信息* @param {*} OwlOption Owl的配置信息* @param {*} NODE_ENV 当下环境变量*/let BeforeHtmlProcessing = function (LxOptions = null, OwlOption = null, NODE_ENV = 'development') {this.LxOptions = LxOptions;this.OwlOption = OwlOption;// compiler.options.mode也有环境变量信息,但这个量是给webpack编译用的// 如:在test环境中,我们需要webpack以prod方式编译(近似于线上),但我们加载的代码逻辑需要是dev逻辑,所以需要使用当前 NODE_ENV。this.NODE_ENV = NODE_ENV;}BeforeHtmlProcessing.prototype.apply = function (compiler) {compiler.hooks.compilation.tap('BeforeHtmlProcessing', (compilation) => {compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tap('BeforeHtmlProcessing',data => {// head头部引入Owl预采集模块,需要在head所有静态资源之前引入if (this.OwlOption) data.html = data.html.replace(/\<head\>/, (rs) => rs + OwlConfig.owlPreProccess());// head尾部引入 灵犀SDK 和 OwlSDKdata.html = data.html.replace(/\<\/head\>/, (result) => {const lxSdk = this.LxOptions ? LxConfig.LxSDK(this.LxOptions, this.NODE_ENV) : '';const owlSdk = this.OwlOption ? OwlConfig.owlSDK(this.NODE_ENV) : '';return lxSdk + owlSdk + result;});// body头中 引入OwlStartif (this.OwlOption) data.html = data.html.replace(/\<body\>/, (rs) => rs += OwlConfig.owlStart(this.OwlOption, this.NODE_ENV));})})}module.exports = BeforeHtmlProcessing;
// 通过script src注入js// catInitconst catInit = require('./../template/CatInit');// catSDK TODO crossorigin="anonymous"const prodOwl = "//www.dpfile.com/app/owl/static/owl_latest.js";const devOwl = "//s1.51ping.com/app/owl/static/owl_latest.js";let BeforeHtmlGeneration = function(options) {options = options || {};this.options = options;}BeforeHtmlGeneration.prototype.apply = function (compiler) {compiler.hooks.compilation.tap('BeforeHtmlGeneration', (compilation) => {console.log('compilation', compiler.options.mode);compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tap('BeforeHtmlGeneration',htmlPluginData => {htmlPluginData.assets.js.unshift(catInit);compiler.options.mode === 'production' ? htmlPluginData.assets.js.unshift(prodOwl) : htmlPluginData.assets.js.unshift(devOwl)})})}module.exports = BeforeHtmlGeneration;
/*** 灵犀初始化配置 及 SDK 引入* @param {*} LxOptions 灵犀配置信息* @param {*} NODE_ENV 环境变量*/const LxSDK = (LxOptions, NODE_ENV = 'development') => {const { category = 'ead', appnm, cid, bid, autopv = 'off' } = LxOptions;const isDev = NODE_ENV !== 'production';if (!cid) throw Error("Lingxi configuration information 'cid' is required, \n please check if the 'lxconfig.cid' configuration is correct.\n(灵犀配置信息 cid 是必须项,请查看 LxConfig.cid 配置是否正确)");const appnmMeta = appnm ? `<meta name="lx:appnm" content="${appnm}">` : '';const bidMeta = bid ? `<meta name="lx:bid" content="${bid}">` : '';// 线上: src="//lx.meituan.net/analytics.js"// 线下: src="//analytics.fetc.st.sankuai.com/analytics.js"const LxSdkHost = "lx.meituan.net"; // 灵犀的线下地址只是灵犀sdk做灰度用的,上报上去都是一个仓库,对业务没用return `<!---------- 接入灵犀 ----------><meta name="lx:category" content="${category}">${appnmMeta}<meta name="lx:cid" content="${cid}">${bidMeta}<meta name="lx:autopv" content="${autopv}" /><meta name="lx:mvDelay" content="2" /><link rel="dns-prefetch" href="//lx.meituan.net" /><link rel="dns-prefetch" href="//wreport.meituan.net" /><link rel="dns-prefetch" href="//report.meituan.com" /><script type="text/javascript">!(function (win, doc, ns) {var cacheFunName = '_MeiTuanALogObject';win[cacheFunName] = ns;if (!win[ns]) {var _LX = function () {_LX.q.push(arguments);return _LX;};_LX.q = _LX.q || [];_LX.l = +new Date();win[ns] = _LX;}})(window, document, 'LXAnalytics');LXAnalytics('config','isDev', ${isDev}); // 在初始化时设置isDev为true,以使灵犀数据上报至线下环境,默认为false,即默认上报至线上环境LXAnalytics("config", "useQR", true); // 开发快速上报功能,满足即时数据分析的需求LXAnalytics("config", "onWebviewAppearAutoPV", false); // 关闭Webview容器显示/隐藏时的自动PV/PD</script><!---------- 灵犀 SDK ----------><script type="text/javascript" src="//${LxSdkHost}/analytics.js" async> </script>`;}module.exports = { LxSDK };

- loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中,运行在打包前。
 - plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事,可以作用在 webpack 的整个流程中。
 
热更新原理
https://mp.weixin.qq.com/s/OQUGvdJi6wJNod0VWTDkEg
https://mp.weixin.qq.com/s/oXzsXIumOmg45SOOCsevQQ
简单来说就是:hot-module-replacement-plugin 包给 webpack-dev-server 提供了热更新的能力,它们两者是结合使用的,单独写两个包也是出于功能的解耦来考虑的。
1)webpack-dev-server(WDS)的功能提供 bundle server的能力,就是生成的 bundle.js 文件可以通过 localhost://xxx 的方式去访问,另外 WDS 也提供 livereload(浏览器的自动刷新)。
2)hot-module-replacement-plugin 的作用是提供 HMR 的 runtime,并且将 runtime 注入到 bundle.js 代码里面去。一旦磁盘里面的文件修改,那么 HMR server 会将有修改的 js module 信息发送给 HMR runtime,然后 HMR runtime 去局部更新页面的代码。因此这种方式可以不用刷新浏览器。
HMR主要实现:
- 如何在服务端文件变化之后通知客户端?使用socket Websocket
 - 如何判断文件是否发生变化?客户端和服务端都保存
chunk,比较是否发生变化 
websocket最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种
协议标识符是ws(如果加密,则为wss),服务器网址就是 URL
- HMR Runtime 通过 HotModuleReplacementPlugin 已经注入到我们 chunk 中了
 - 除了开启一个 Bundle Server,还开启了 HMR Server,主要用来和 HMR Runtime 中通信
 - 在编译结束的时候,通过 compiler.hooks.done,监听并通知客户端
 - 客户端接收到之后,就会调用 module.hot.check 等,发起 http 请求去服务器端获取新的模块资源解析并局部刷新页面
 

webpack —watch启动监听模式之后,webpack第一次编译项目,将结果存储在内存文件系统,相比较磁盘文件读写方式内存文件管理速度更快,webpack服务器通知浏览器加载资源,浏览器获取的静态资源除了JS code内容之外,还有一部分通过webpack-dev-server注入的的 HMR runtime代码,作为浏览器和webpack服务器通信的客户端( webpack-hot-middleware提供类似的功能)
文件系统中一个文件(或者模块)发生变化,dev-server 的中间件 webpack-dev-middleware 和 webpack 交互
- webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中,webpack监听到文件变化对文件重新编译打包,每次编译生成唯一的hash值,根据变化的内容生成两个补丁文件:说明变化内容的manifest(文件格式是hash.hot-update.json,包含了hash和chundId用来说明变化的内容)和chunk js(hash.hot-update.js)模块。
 - dev-server 监听配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新
 
hrm-server通过websocket将manifest推送给浏览器
浏览器端hmr runtime【HotModuleReplacement.runtime 是客户端 HMR 的中枢】根据manifest的hash和chunkId向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过请求,获取到最新的模块代码
HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用
异步加载原理
异步加载的核心其实是使用类jsonp的方式,通过动态创建script的方式实现异步加载
将 异步加载的的模块单独导出一个.js 文件,在使用该模块的时候,创建一个 script 对象,加入到 document.head 对象中,浏览器会自动发起请求,请求这个 js 文件,写个回调函数,让请求到的 js 文件做一些业务操作
将 import() 转换成模拟 JSONP 去加载动态加载的 chunk 文件
浏览器不支持import语法和import()语法,那webpack是怎么处理的呢?
output: {filename: "[name].[hash:8].js",path: path.resolve(__dirname, "AsyncDist"),chunkFilename: "bundle-[name]-[chunkhash:8].js",jsonpFunction: "webpackJsonp_DEMO_ASYNC"},// =========入口文件 =============import INDEX3 from './index3';function component() {setTimeout(() => {import(/* webpackChunkName: 'lodash'*/ "lodash").then(function (lodash) {console.log("异步加载 lodash");});}, 3000);element.innerHTML = "Hello Webpack,我要好好学习,天天向上啊";return element;}document.body.appendChild(component());
其中,jsonpFunction是定义异步加载的模块数组
/ webpackChunkName: ‘lodash’/  定义异步加载的bundle名称
打包后生成的文件
bundle-vendors~lodash-22b637c2.js 异步加载的包
index.html
main.7ca2ed1c.js   主入口bundle
先看bundle-vendors~lodash-22b637c2.js,可以看到全局定义了
jsonpFunction:webpackJsonp_DEMO_ASYNC数组,来存放异步加载的包
- 异步加载的文件中存放的需要安装的模块对应的 Chunk Names,vendors~lodash
 - 异步加载的文件中存放的需要安装的模块列表
(window["webpackJsonp_DEMO_ASYNC"] = window["webpackJsonp_DEMO_ASYNC"] || []).push([["vendors~lodash"],{"./node_modules/lodash/lodash.js":(function(module, exports, __webpack_require__) {...})})
 
再看main.7ca2ed1c.js , 主要结构:
- IIFE立即执行函数,保证bundle在浏览器中立即执行
 - 入参是依赖图,从入口出发的依赖关系,依赖图的基本结构,moduleID,code
 - 将依赖图传入立即执行函数中,通过webpack_require模拟require,将入口文件返回
 
在依赖图中可以看到,函数入参module对应于当前模块的相关状态(是否加载完成、导出值、id 等)、webpack_exports就是当前模块的导出(就是 export)、webpack_require就是入口 chunk 的webpack_require函数,用于import其它代码
同步import被转化成webpack_require,异步import()被转化成webpack_require.e,返回的是一个promise
(function(modules){function webpackJsonpCallback(data) {};// 缓存已经加载过的module。无论是同步还是异步加载的模块都会进入该缓存var installedModules = {};// 记录chunk的状态位// 值:0 表示已加载完成。var installedChunks = { main: 0};//获取资源地址function jsonpScriptSrc(chunkId) {};//同步importfunction __webpack_require__(moduleId) {}// 用于加载异步import的方法__webpack_require__.e = function requireEnsure(chunkId) {//...}return __webpack_require__((__webpack_require__.s = "./src/async.js"));})({"./src/async.js":(function(module, __webpack_exports__, __webpack_require__) {eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _index3__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index3 */ \"./src/index3.js\");\n\n\nfunction component() {\nsetTimeout(() => {\n __webpack_require__.e(/*! import() | lodash */ \"vendors~lodash\").then(__webpack_require__.t.bind(null, /*! lodash */ \"./node_modules/lodash/lodash.js\", 7)).then(function (lodash) {\n console.log(\"异步加载 lodash\");\n });\n}, 3000);\n element.innerHTML = \"Hello Webpack,我要好好学习,天天向上啊\";\n return element;\n}\n\ndocument.body.appendChild(component());\n\n\n\n//# sourceURL=webpack:///./src/async.js?");}),"./src/index3.js":(function(module, __webpack_exports__, __webpack_require__) {eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"INDEX3\", function() { return INDEX3; });\nconst INDEX3 = 'index 3';\n\n\n\n//# sourceURL=webpack:///./src/index3.js?");})})
下面一个一个来看到底怎么实现的?
jsonpScriptSrc的主要作用是通过publicPath+chunkId的方式获取到异步加载模块的url地址。
function jsonpScriptSrc(chunkId) {return (__webpack_require__.p +"bundle-" +({ "vendors~lodash": "vendors~lodash" }[chunkId] || chunkId) +"-" +{ "vendors~lodash": "22b637c2" }[chunkId] +".js");
webpack_require是webpack的核心,webpack通过webpack_require引入模块。
 webpack_require对require包裹了一层,主要功能是加载 js 文件。
//立即执行函数传入参数的的key值function __webpack_require__(moduleId) {//如果需要加载的模块已经被加载过,就直接从内存缓存中返回if (installedModules[moduleId]) {return installedModules[moduleId].exports;}//如果缓存中不存在需要加载的模块,就新建一个模块,并把它存在缓存中var module = (installedModules[moduleId] = {i: moduleId, // 模块在数组中的 indexl: false, // 该模块是否已经加载完毕exports: {} // 该模块的导出值});// 从 modules 中获取 index 为 moduleId 的模块对应的函数// 再调用这个函数,同时把函数需要的参数传入modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);// 把这个模块标记为已加载module.l = true;// Return the exports of the modulereturn module.exports;}
这个函数接收一个moduleId,对应于立即执行函数传入参数的key值。若一个模块之前已经加载过,直接返回这个模块的导出值;若这个模块还没加载过,就执行这个模块,将它缓存到installedModules相应的moduleId为 key 的位置上,然后返回模块的导出值。所以在 webpack 打包代码中,import一个模块多次,这个模块只会被执行一次。还有一个地方就是,在 webpack 打包模块中,默认import和require是一样的,最终都是转化成__webpack_require__。
webpack_require.e 异步加载核心。异步加载的核心其实是使用类jsonp的方式,通过动态创建script的方式实现异步加载
// 记录chunk的状态位// 值:0 表示已加载完成。// undefined : chunk 还没加载// null :chunk preloaded/prefetched// Promise : chunk正在加载__webpack_require__.e = function requireEnsure(chunkId) {var promises = [];// 判断当前chunk是否已经安装,如果已经使用var installedChunkData = installedChunks[chunkId];// installedChunkData没有加载if (installedChunkData !== 0) {//installedChunkData正在加载if (installedChunkData) {promises.push(installedChunkData[2]);} else {//installedChunkData 为空,表示该 Chunk 还没有加载过,去加载该 Chunk 对应的文件var promise = new Promise(function(resolve, reject) {installedChunkData = installedChunks[chunkId] = [resolve, reject];});promises.push((installedChunkData[2] = promise));// 通过 DOM 操作,往 HTML head 中插入一个 script 标签去异步加载 Chunk 对应的 JavaScript 文件var script = document.createElement("script");var onScriptComplete;script.charset = "utf-8";script.timeout = 120;if (__webpack_require__.nc) {script.setAttribute("nonce", __webpack_require__.nc);}// 文件的路径为配置的 publicPath、chunkId 拼接而成script.src = jsonpScriptSrc(chunkId);// create error before stack unwound to get useful stacktrace latervar error = new Error();// 当脚本加载完成,执行对应回调onScriptComplete = function(event) {// 避免IE的内存泄漏script.onerror = script.onload = null;clearTimeout(timeout);// 去检查 chunkId 对应的 Chunk 是否安装成功,安装成功时才会存在于 installedChunks 中var chunk = installedChunks[chunkId];if (chunk !== 0) {if (chunk) {var errorType =event && (event.type === "load" ? "missing" : event.type);var realSrc = event && event.target && event.target.src;error.message ="Loading chunk " +chunkId +" failed.\n(" +errorType +": " +realSrc +")";error.name = "ChunkLoadError";error.type = errorType;error.request = realSrc;chunk[1](error);}installedChunks[chunkId] = undefined;}};// 设置异步加载的最长超时时间var timeout = setTimeout(function() {onScriptComplete({ type: "timeout", target: script });}, 120000);// 在 script 加载和执行完成时回调script.onerror = script.onload = onScriptComplete;document.head.appendChild(script);}}return Promise.all(promises);};
webpackJsonpCallback 实现异步回调,主要作用是每个异步模块加载并安装。 webpack 会安装对应的 webpackJsonp 文件。
var jsonpArray = (window["webpackJsonp_DEMO_ASYNC"] = window["webpackJsonp_DEMO_ASYNC"] || []);var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);// 重写数组 push 方法,重写之后,每当webpackJsonp.push的时候,就会执行webpackJsonpCallback代码jsonpArray.push = webpackJsonpCallback;jsonpArray = jsonpArray.slice();for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
function webpackJsonpCallback(data) {//chunkIds 异步加载的文件中存放的需要安装的模块对应的 Chunk ID// moreModules 异步加载的文件中存放的需要安装的模块列表var chunkIds = data[0];var moreModules = data[1];//循环去判断对应的chunk是否已经被安装,如果,没有被安装就吧对应的chunk标记为安装。var moduleId,chunkId,i = 0,resolves = [];for (; i < chunkIds.length; i++) {chunkId = chunkIds[i];if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&installedChunks[chunkId]) {// 此处的resolves push的是在__webpack_require__.e 异步加载中的 installedChunks[chunkId] = [resolve, reject];的resolveresolves.push(installedChunks[chunkId][0]);}installedChunks[chunkId] = 0;}for (moduleId in moreModules) {if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {modules[moduleId] = moreModules[moduleId];}}if (parentJsonpFunction) parentJsonpFunction(data);while (resolves.length) {// 执行异步加载的所有 promise 的 resolve 函数resolves.shift()();}}

模块循环加载
https://mp.weixin.qq.com/s/gW_2sDfX5o4wamoiZMsxCw
References:
异步加载原理:https://juejin.cn/post/6844904033681948686#heading-2
https://github.com/sisterAn/minipack/blob/master/index.js
https://juejin.cn/post/6844904038543130637#heading-11
WDS:https://github.com/liangklfangl/webpack-dev-server
热更新源码解读  https://juejin.cn/post/6844904008432222215#heading-14
https://mp.weixin.qq.com/s/PeSbdScrvsO0ctECGGY1bQ
https://mp.weixin.qq.com/s/2-zNlGrKUngWdQNvlcgESw
https://mp.weixin.qq.com/s/oXzsXIumOmg45SOOCsevQQ 热更新原理
[
](https://mp.weixin.qq.com/s/PeSbdScrvsO0ctECGGY1bQ)
附
1.miniWebpack
const fs = require('fs')const path = require('path')// babel解析器(JavaScript 解析器)const babelParser = require('@babel/parser')// 和 babel 解析器配合使用,来遍历及更新每一个子节点const traverse = require('@babel/traverse').defaultconst {transformFromAst} = require('@babel/core');// 获取配置文件const config = require('./minipack.config')// 入口const entry = config.entry// 出口const output = config.output/*** 解析文件内容及其依赖,* 期望返回:* dependencies: 文件依赖模块* code: 文件解析内容* @param {string} filename 文件路径*/function createAsset(filename) {// 读取文件内容const content = fs.readFileSync(filename, 'utf-8')// 使用 @babel/parser(JavaScript解析器)解析代码,生成 ast(抽象语法树)const ast = babelParser.parse(content, {sourceType: "module"})// 从 ast 中获取所有依赖模块(import),并放入 dependencies 中const dependencies = []traverse(ast, {// 遍历所有的 import 模块,并将相对路径放入 dependenciesImportDeclaration: ({node}) => {dependencies.push(node.source.value)}})// 获取文件内容const {code} = transformFromAst(ast, null, {presets: ['@babel/preset-env'],})// 返回结果return {dependencies,code,}}/*** 从入口文件开始,获取整个依赖图* @param {string} entry 入口文件*/function createGraph(entry) {// 从入口文件开始,解析每一个依赖资源,并将其一次放入队列中const mainAssert = createAsset(entry)const queue = {[entry]: mainAssert}/*** 递归遍历,获取所有的依赖* @param {*} assert 入口文件*/function recursionDep(filename, assert) {// 跟踪所有依赖文件(模块唯一标识符)assert.mapping = {}// 由于所有依赖模块的 import 路径为相对路径,所以获取当前绝对路径const dirname = path.dirname(filename)assert.dependencies.forEach(relativePath => {// 获取绝对路径,以便于 createAsset 读取文件const absolutePath = path.join(dirname, relativePath)// 与当前 assert 关联assert.mapping[relativePath] = absolutePath// 依赖文件没有加入到依赖图中,才让其加入,避免模块重复打包if (!queue[absolutePath]) {// 获取依赖模块内容const child = createAsset(absolutePath)// 将依赖放入 queue,以便于 for 继续解析依赖资源的依赖,直到所有依赖解析完成,这就构成了一个从入口文件开始的依赖图queue[absolutePath] = childif (child.dependencies.length > 0) {// 继续递归recursionDep(absolutePath, child)}}})}// 遍历 queue,获取每一个 asset 及其所以依赖模块并将其加入到队列中,直至所有依赖模块遍历完成for (let filename in queue) {let assert = queue[filename]recursionDep(filename, assert)}// 返回依赖图return queue}/*** 打包(使用依赖图,返回一个可以在浏览器运行的包)* 所以返回一个立即执行函数 (function() {})()* 这个函数只接收一个参数,包含依赖图中所有信息** 遍历 graph,将每个 mod 以 `key: value,` 的方式加入到 modules,* 其中key 为 filename, 模块的唯一标识符,value 为一个数组, 它包含:* function(require, module, exports){${mod.code}}* ${JSON.stringify(mod.mapping)}** 其中:function(require, module, exports){${mod.code}}* 使用函数包装每一个模块的代码 mode.code,防止 mode.code 污染全局变量或其它模块* 并且模块转化后运行在 common.js 系统,它们期望有 require, module, exports 可用** 其中:${JSON.stringify(mod.mapping)} 是模块间的依赖关系,当依赖被 require 时调用* 例如:{ './message.js': 1 }** @param {array} graph 依赖图*/function bundle(graph) {let modules = ''for (let filename in graph) {let mod = graph[filename]modules += `'${filename}': [function(require, module, exports) {${mod.code}},${JSON.stringify(mod.mapping)},],`}// 注意:modules 是一组 `key: value,`,所以我们将它放入 {} 中// 实现 立即执行函数// 首先实现一个 require 函数,require('${entry}') 执行入口文件,entry 为入口文件绝对路径,也为模块唯一标识符// require 函数接受一个 id(filename 绝对路径) 并在其中查找它模块我们之前构建的对象.// 通过解构 const [fn, mapping] = modules[id] 来获得我们的函数包装器和 mappings 对象.// 由于一般情况下 require 都是 require 相对路径,而不是id(filename 绝对路径),所以 fn 函数需要将 require 相对路径转换成 require 绝对路径,即 localRequire// 注意:不同的模块 id(filename 绝对路径)时唯一的,但相对路径可能存在相同的情况//// 将 module.exports 传入到 fn 中,将依赖模块内容暴露处理,当 require 某一依赖模块时,就可以直接通过 module.exports 将结果返回const result = `(function(modules) {function require(moduleId) {const [fn, mapping] = modules[moduleId]function localRequire(name) {return require(mapping[name])}const module = {exports: {}}fn(localRequire, module, module.exports)return module.exports}require('${entry}')})({${modules}})`return result}/*** 输出打包* @param {string} path 路径* @param {string} result 内容*/function writeFile(path, result) {// 写入 ./dist/bundle.jsfs.writeFile(path, result, (err) => {if (err) throw err;console.log('文件已被保存');})}// 获取依赖图const graph = createGraph(entry)// 打包const result = bundle(graph)// 输出fs.access(`${output.path}/${output.filename}`, (err) => {if(!err) {writeFile(`${output.path}/${output.filename}`, result)} else {fs.mkdir(output.path, { recursive: true }, (err) => {if (err) throw err;writeFile(`${output.path}/${output.filename}`, result)});}})



