├── images ├── 1.png ├── 2.png ├── 3.png ├── chunks.png ├── replace.png ├── sketch.png ├── generated.png ├── contentbase.png └── dependency.png ├── README.md ├── 课程内容简介.md ├── 04-webpack-dev-server基本使用.md ├── 10-webpack结合react-router实现按需加载.md ├── 07-webpack常见插件原理分析.md ├── 12-以node方式集成webpack和webpack-dev-server打包.md ├── 08-教你写一个webpack插件.md ├── 01-webpack核心概念.md ├── 03-webpack-dev-server核心概念.md ├── 11-webpack2的tree-shaking深入分析.md ├── 02-webpack基本使用.md ├── 05-webpack的HMR原理分析.md ├── 09-教你写一个webpack的loader.md └── 06-webpack中的compiler和compilation对象.md /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/3.png -------------------------------------------------------------------------------- /images/chunks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/chunks.png -------------------------------------------------------------------------------- /images/replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/replace.png -------------------------------------------------------------------------------- /images/sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/sketch.png -------------------------------------------------------------------------------- /images/generated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/generated.png -------------------------------------------------------------------------------- /images/contentbase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/contentbase.png -------------------------------------------------------------------------------- /images/dependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangklfangl/webpack-core-usage/HEAD/images/dependency.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 前言 2 | 本系列 **Webpack** 课程成册于一年前,并在[gitchat](https://gitbook.cn/gitchat/column/59f57e2549cd43306135e255)上取得了不错的反响,成功帮助不少想深入了解 **Webpack** 的同学。现将其开源出来,欢迎阅读。课程篇幅较长,文中错误在所难免,也恳请同行不吝指出。 3 | 4 | ### 课程简介 5 | 本课程是为有一定了解或想深入了解Webpack打包原理的读者定制的。 6 | 7 | 内容从 **Webpack** 的基本概念和使用逐步深入到核心,如 **Loader** 和 **Plugin** 的书写,以及 **Compiler** 和 **Compilation** 对象分析;同时也涵盖了 **HMR** 的实现原理及 **Tree-shaking**、按需加载等高级知识点。 8 | 9 | 通过本课程,你可以深入的解和使用 **Webpack**,并能够按照项目需求快速开发一个适合于自身项目的打包工具。 10 | 11 | ### 你可以学到什么? 12 | > 1.Webpack 的核心概念 13 | 14 | > 2.Webpack 基本使用 15 | 16 | > 3.webpack-dev-server 核心概念 17 | 18 | > 4.webpack-dev-server 基本使用 19 | 20 | > 5.Webpack 的 HMR 原理分析 21 | 22 | > 6.Webpack 中的 Compiler 和 Compilation 对象 23 | 24 | > 7.Webpack 常见插件原理分析 25 | 26 | > 8.写一个 Webpack 插件 27 | 28 | > 9.写一个 Webpack 的 loader 29 | 30 | > 10.Webpack 结合 react-router 实现按需加载 31 | 32 | > 11.Webpack 2 的 Tree-shaking 深入分析 33 | 34 | > 12.以 Node 方式集成 Webpack 和 webpack-dev-server 打包 35 | 36 | 其实现在基于 **Webpack** 的打包工具非常成熟,读者可以在 **Github** 或者 **npm** 中轻松地找到需要的脚手架。但我见过很多同学虽能够正常地使用 **Webpack**,对 **Webpack** 的配置也十分了解,可当遇到问题时依然不知所措。 37 | 38 | 通过本系列课程,你可以深入地了解和使用 **Webpack**,并能够按照项目需求快速开发一个适合于自身项目的打包工具,在开发中做到得心应手。 39 | -------------------------------------------------------------------------------- /课程内容简介.md: -------------------------------------------------------------------------------- 1 | #### 1.前端必不可少的脚手架 2 | 3 | 对于打包工具的熟悉程度渐渐的也已经成为衡量前端开发工程师水平的一个重要指标。记得在校招面试的时候就有问各种打包工具的问题,如对于gulp,grunt,webpack的熟悉程度,各种打包工具的特点以及优缺点等。而当我们逐渐融入到一个特定的团队中,一般都有有现成的脚手架提供给我们使用,而对于脚手架本身的关注程度也会慢慢降低。那是否就意味着,我们不需要掌握脚手架的相关知识了呢?其实不然,个人认为有以下几个理由: 4 | 5 | (1)任何脚手架都有一定的适用场景,但是同时也有边界,如果你不小心跨域了这个边界,那么你很可能遇到意想不到的问题,比如bug。此时,如果你对脚手架的原理有一定的了解,那么也能够更快的定位问题。 6 | 7 | (2)任何一个脚手架都不可能是完美的,都会存在一个优化的阶段,如果你只是用它,而不去了解它,优化它,那么本身就是一个`追求完美的工程师`不应该具有的态度。况且,对于工程师来说,只是会用而不知道其原理本身就是一个笑话。 8 | 9 | #### 2.课程内容 10 | 本课程是基于你对webpack有一定的了解,或者是想深入了解webpack打包原理。如果你只是想了解如何使用webpack,那么网上的大部分资料已经足够了。现在对本课程做一个概括,主要内容包含以下部分: 11 | - webpack的核心概念 12 | 13 | 在本章节,我会首先通过一个`依赖图谱`的例子来展开,详细的论述webpack的loader,plugin,entry,output等核心概念。结合webpack2官网的说明以及日常开发实践经验进行深入的分析。我会使用完整的实例让你对webpack核心概念有深入的理解,什么是chunk?什么是common chunk?什么是hotUpdated chunk?externals ?libraryTarget?library等一系列疑问在本章节我都会给你答案。 14 | - webpack基本使用 15 | 16 | 本章节从webpack的基本使用出发,但是又不止于基本使用。我会结合7个实例代码来深入的分析webpack与CommonChunkPlugin结合后的打包实践与原理。同时对于CommonChunkPlugin的各种配置都会使用具体的实例来深入讲解。通过本章节的学习,你不仅会使用webpack,而且会知道如何更好的使用webpack。 17 | - webpack-dev-server核心概念 18 | 19 | 本章节会深入分析与webpack-dev-server相关的概念,比如proxy代理,HMR原理,contentBase,publicPath,lazyload,filename等诸多配置的详细讲解。通过深入的了解这部分内容,不仅可以了解优化的点,同时也能更好的解决真实项目开发中可能遇到的问题。 20 | - webpack-dev-server基本使用 21 | 22 | 本章节主要教你如何在项目中使用webpack-dev-server,并深入的分析了webpack-dev-server的iframe模式与inline模式的区别。网上关于两者的区别大都来自于官网的翻译,在本章节,我会结合具体的实例来进行分析。 23 | - webpack的HMR原理分析 24 | 25 | 结合webpack-dev-server来深入分析webpack实现HMR的原理。在本章节中,我不仅会告诉你webpack实现HMR的原理,同时也会告诉你如何让你写出支持HMR的代码,从而可以让你深入的了解HMR。这其中会包含你常见的:decline函数,accept函数,dispose函数,status函数,apply等函数分析,同时也会详细的告诉你webpack与HMR的相关的配置信息。使得你可以在以后使用webpack的时候得心应手。 26 | - webpack中的compiler和compilation对象 27 | 28 | compilation和compiler对象是写webpack插件的核心内容,在本章节,我不仅会详细讲述两者的作用以及如何在插件中使用它们,同时我也会告诉你在webpack插件书写中你经常使用到的方法或者属性。通过本章节,你不仅能了解什么是模块,什么是依赖模块,什么是chunk,什么是资源等等,你也能知道如何根据具体场景来使用这些资源。 29 | 30 | - webpack常见插件原理分析 31 | 32 | 在本章节,我会将关注点放在webpack的两个插件的原理上。包括CommonChunkPlugin和PrepackWebpackPlugin,通过这两个插件来加深你对上面知识的理解。从而为下文写一个自己的webpack插件听提供必要的知识。 33 | 34 | - 教你写一个webpack插件 35 | 36 | webpack插件是扩展webpack基础功能的主要渠道,在本章节,我会教你如何写一个webpack插件。 37 | 38 | - 教你写一个webpack的loader 39 | 40 | 在本章节,我会使用一个markdown文件处理的loader来教你如何写webpack的loader。 41 | 42 | - webpack结合react-router实现按需加载 43 | 44 | 在上面的章节中,我讲到了如何使用require.ensure来动态产生独立的chunk的问题,在本章节我会使用react-router的例子来讲解如何使用webpack的这种特性。通过动态按需加载的特性能够减少你页面首次加载的时长,配合单页面应用绝对是页面优化的首选。 45 | 46 | - webpack2的tree-shaking深入分析 47 | 48 | tree-shaking是webpack2引入的新的特性,本章节我会详细描述如何使用tree-shaking以及tree-shaking的原理和适用范围。本章节内容会包含具体的实例,所以你一定能够很好的了解这种新特性。 49 | 50 | - 以node方式集成webpack和webpack-dev-server打包 51 | 52 | 在本章节,我将使用一个很好的例子来教你如何基于webpack,webpack-dev-server来写一个自己的打包工具并适应具体的业务场景。通过本章节的内容,你将能很好的将上面章节的内容做一个串联,同时也能更好的理解webpack。 53 | 54 | #### 3.写给读者 55 | 其实现在基于webpack的打包工具都已经非常成熟,所以你可以随意的在github或者npm中找到你需要的脚手架。但是,就像我文章开头所说,只有你了解了webpack的核心原理,才能在开发中做到得心应手。我见过很多同学,能够正常的使用webpack,对很多webpack的配置也能够理解,但是当遇到问题的时候往往不知所措。通过本系列课程,我会让你摆脱现状,更好的理解webpack原理,而不会知其然不知其所以然。 56 | -------------------------------------------------------------------------------- /04-webpack-dev-server基本使用.md: -------------------------------------------------------------------------------- 1 | #### 1.本章概述 2 | 在本章节,我们将简要的对webpack-dev-server的基本使用做一个演示。通过这个章节,你可以学会如何在自己的项目中使用webpack-dev-server。 3 | 4 | #### 2.webpack-dev-server使用 5 | 第一步:安装webpack-dev-server 6 | ```js 7 | npm install webpack-dev-server -g 8 | ``` 9 | 第二步:在项目根目录下配置webpack.config.js 10 | ```js 11 | var path = require("path"); 12 | module.exports = { 13 | entry: { 14 | app: ['./src/main.js'] 15 | }, 16 | output: { 17 | path: path.resolve(__dirname, "public"), 18 | publicPath: "", 19 | filename: "bundle.js" 20 | } 21 | }; 22 | ``` 23 | 其中里面的各项配置,通过前面的章节你应该能够理解,此处不再赘述,如果不懂,可以仔细阅读前面的章节。 24 | 25 | 第三步:配置package.json中的scripts部分 26 | ```js 27 | "scripts": { 28 | "build": "webpack", 29 | "start": "webpack-dev-server --inline --hot --port 3000 --content-base public", 30 | "test": "echo \"Error: no test specified\" && exit 1" 31 | }, 32 | ``` 33 | 其中对于script部分不太了解的可以查看我写的[package.json中的scripts部分深入讲解](https://github.com/liangklfangl/npm-command#4packagejson中script部分深入讲解)。通过配置scripts你可以使用如下命令: 34 | ```js 35 | npm start 36 | //或者 37 | npm run start 38 | ``` 39 | 来替换掉: 40 | ```js 41 | webpack-dev-server --inline --hot --port 3000 --content-base public 42 | ``` 43 | 当然,如果你不想做替换,依然可以在目录下运行你全局安装的webpack-dev-server命令。此时需要的所有配置都已经完成了,启动你的*npm run start*就会启动一个Express服务器,端口号是3000,同时支持HMR,contentBase为我们指定的/public。 44 | 45 | #### 3.webpack-dev-server的inline模式与iframe模式 46 | 上面的打包实例中你看到我在cli中传入了*--inline*,表示这是内联的打包方式,那么什么是内联的打包方式呢? 47 | ##### 3.1 iframe模式 48 | 我们首先看看Express服务器是如何处理iframe模式的: 49 | ```js 50 | app.get('/webpack-dev-server/*', (req, res) => { 51 | res.setHeader('Content-Type', 'text/html'); 52 | fs.createReadStream(path.join(__dirname, '..', 'client', 'live.html')).pipe(res); 53 | }); 54 | ``` 55 | 所以当我们在URL中加入webpack-dev-server的路径以后,我们就会返回live.html,我们再看看live.html的内容: 56 | ```js 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ``` 69 | 其直接返回了*/__webpack_dev_server__/live.bundle.js*。我们看看这个请求Express服务器返回的资源内容: 70 | ```js 71 | app.get('/__webpack_dev_server__/live.bundle.js', (req, res) => { 72 | res.setHeader('Content-Type', 'application/javascript'); 73 | fs.createReadStream(path.join(__dirname, '..', 'client', 'live.bundle.js')).pipe(res); 74 | }); 75 | ``` 76 | 所以我们最终返回的还是client/live.bundle.js,我们就来看看live.bundle.js的具体内容。首先它会在页面中创建一个iframe并注册了客户端代码: 77 | ```js 78 | 79 | const onSocketMsg = { 80 | hot() { 81 | hot = true; 82 | iframe.attr('src', contentPage + window.location.hash); 83 | }, 84 | invalid() { 85 | okness.text(''); 86 | status.text('App updated. Recompiling...'); 87 | header.css({ 88 | borderColor: '#96b5b4' 89 | }); 90 | $errors.hide(); 91 | if (!hot) iframe.hide(); 92 | }, 93 | hash(hash) { 94 | currentHash = hash; 95 | }, 96 | 'still-ok': function stillOk() { 97 | okness.text(''); 98 | status.text('App ready.'); 99 | header.css({ 100 | borderColor: '' 101 | }); 102 | $errors.hide(); 103 | if (!hot) iframe.show(); 104 | }, 105 | ok() { 106 | okness.text(''); 107 | $errors.hide(); 108 | reloadApp(); 109 | }, 110 | warnings() { 111 | okness.text('Warnings while compiling.'); 112 | $errors.hide(); 113 | reloadApp(); 114 | }, 115 | errors(errors) { 116 | status.text('App updated with errors. No reload!'); 117 | okness.text('Errors while compiling.'); 118 | $errors.text(`\n${stripAnsi(errors.join('\n\n\n'))}\n\n`); 119 | header.css({ 120 | borderColor: '#ebcb8b' 121 | }); 122 | $errors.show(); 123 | iframe.hide(); 124 | }, 125 | close() { 126 | status.text(''); 127 | okness.text('Disconnected.'); 128 | $errors.text('\n\n\n Lost connection to webpack-dev-server.\n Please restart the server to reestablish connection...\n\n\n\n'); 129 | header.css({ 130 | borderColor: '#ebcb8b' 131 | }); 132 | $errors.show(); 133 | iframe.hide(); 134 | } 135 | }; 136 | socket('/sockjs-node', onSocketMsg); 137 | ``` 138 | 这样,当webpack的资源发生变化以后可以通过websocket通知到我们这个主页面。我们看看主页面最重要的一个方法,即*reloadApp*方法,方法内容如下: 139 | ```js 140 | function reloadApp() { 141 | //如果开启了HMR功能 142 | if (hot) { 143 | status.text('App hot update.'); 144 | try { 145 | iframe[0].contentWindow.postMessage(`webpackHotUpdate${currentHash}`, '*'); 146 | } catch (e) { 147 | console.warn(e); // eslint-disable-line 148 | } 149 | iframe.show(); 150 | } else { 151 | status.text('App updated. Reloading app...'); 152 | header.css({ 153 | borderColor: '#96b5b4' 154 | }); 155 | try { 156 | let old = `${iframe[0].contentWindow.location}`; 157 | if (old.indexOf('about') === 0) old = null; 158 | iframe.attr('src', old || (contentPage + window.location.hash)); 159 | if (old) { 160 | //强制刷新 161 | iframe[0].contentWindow.location.reload(); 162 | } 163 | } catch (e) { 164 | iframe.attr('src', contentPage + window.location.hash); 165 | } 166 | } 167 | } 168 | }); 169 | ``` 170 | 这个主页面接受到事件以后,通过postMessage将当前打包的hash值发送到我们的内部的iframe。如果开启了HMR,那么iframe会检查资源更新,如果没有开启HMR,那么强制iframe进行刷新。上面讲了很多原理的知识,我们下面给出一个日常实例: 171 | 172 | 我们的页面被嵌套在一个iframe中,当资源改变的时候会重新加载。只需要在路径中加入webpack-dev-server就可以了,不需要其他的任何处理: 173 | ```js 174 | http://localhost:8080/webpack-dev-server/index.html 175 | ``` 176 | 从而在页面中就会产生如下的一个iframe标签并注入css/js/DOM: 177 | 178 | ![](https://github.com/liangklfangl/webpack-dev-server/blob/master/iframe.png) 179 | 180 | 这个主页面会请求live.bundle.js ,其中里面会新建一个Iframe ,你的应用就被注入到了这个 iframe 当中。同时live.bundle.js中含有socket.io的client代码,这样它就能和 webpack-dev-server建立的 http server 进行 websocket 通讯了,并根据返回的信息完成相应的动作。 181 | 182 | 总之,因为我们的http://localhost:8080/webpack-dev-server/index.html访问的时候加载了live.bundle.js,其具有websocket的client代码,所以当websocket-dev-server服务端代码发生变化的时候会通知到这个页面,这个页面只是需要重新刷新iframe中的页面就可以了。该模式有如下作用: 183 |
184 | No configuration change needed.(不需要修改配置文件)
185 | Nice information bar on top of your app.(在app上面有information bar)
186 | URL changes in the app are not reflected in the browser’s URL bar.(在app里面的URL改变不会反应到浏览器的地址栏中)
187 | 
188 | ##### 3.2 inline mode 189 | webpack-dev-server的客户端入口被添加到文件中,用于自动刷新页面。其中在cli中输入的是: 190 | ```js 191 | webpack-dev-server --inline --content-base ./build 192 | ``` 193 | 此时在页面中输出的内容中看不到插入任何的js代码: 194 | 195 | ![](https://github.com/liangklfangl/webpack-dev-server/blob/master/inline.png) 196 | 197 | 但是在控制台中可以清楚的知道页面的重新编译等信息: 198 | 199 | ![](https://github.com/liangklfangl/webpack-dev-server/blob/master/reload.png) 200 | 201 | 该模式有如下作用: 202 |
203 | Config option or command line flag needed.(webpack配置或者命令行配置)
204 | Status information in the console and (briefly) in the browser’s console log.(状态信息在浏览器的console.log中)
205 | URL changes in the app are reflected in the browser’s URL bar(URL的改变会反应到浏览器的地址栏中).
206 | 
207 | 每一个模式都是支持Hot Module Replacement的,在HMR模式下,每一个文件都会被通知内容已经改变而不是重新加载整个页面。因此,在HMR执行的时候可以加载更新的模块,从而把他们注册到运行的应用里面。不管是inline模式还是iframe模式,我们都是通过socketjs来连接客户端代码和webpack-dev-server的服务端代码,其原理都是一样的。 208 | 209 | ### 3.本章小结 210 | 该本章节中,我们讲解了如何在你的项目中使用webpack-dev-server来启动一个Express服务器,同时也分析了webpack-dev-server本身具有的inline模式和iframe模式。并深入了讲解了iframe模式的原理,希望你能对webapck-dev-server有一个更加深入的了解。文中提到的示例代码你可以[点击这里下载](https://github.com/liangklfang/webpack-dev-server-demo)。 211 | 212 | -------------------------------------------------------------------------------- /10-webpack结合react-router实现按需加载.md: -------------------------------------------------------------------------------- 1 | ### 1.本章概述 2 | 对于大型的 web 应用来说,如果我们将所有的代码都放在一个文件中,然后一次性加载,这对于页面的性能来说可能存在问题,特别是当很多代码需要满足一定的条件才需要加载的情况下。webpack可以允许将我们的代码分割成为不同的 chunk,然后按需加载这些 chunk,这种特性就是我们常说的 code splitting。在本章节我会主要论述 Webpack 与 React-Router 一起实现按需加载的内容。其中包括如何针对开发环境和生产环境配置不同的 webpack.config.js 内容以及 webpack 按需加载的表现,通过本章节的学习你应该能够学会如何实现按需加载,以及如何使用该特性提升首页加载性能。好了,下面我们开始本章节的正文。 3 | 4 | ### 2.开发环境搭建 5 | #### 2.1 配置webpack.config.js 6 | 我们配置如下的 webpack.config.js: 7 | ```js 8 | //webpack.config.js 9 | const webpack = require('webpack'); 10 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 11 | const path = require('path'); 12 | module.exports = env => { 13 | const ifProd = plugin => env.prod ? plugin : undefined; 14 | const removeEmpty = array => array.filter(p => !!p); 15 | return { 16 | entry: { 17 | app: path.join(__dirname, '../src/'), 18 | vendor: ['react', 'react-dom', 'react-router'], 19 | }, 20 | output: { 21 | filename: '[name].[hash].js', 22 | path: path.join(__dirname, '../build/'), 23 | }, 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.(js)$/, 28 | exclude: /node_modules/, 29 | loader: 'babel-loader', 30 | query: { 31 | cacheDirectory: true, 32 | }, 33 | }, 34 | ], 35 | }, 36 | plugins: removeEmpty([ 37 | new webpack.optimize.CommonsChunkPlugin({ 38 | name: 'vendor', 39 | minChunks: Infinity, 40 | filename: '[name].[hash].js', 41 | }), 42 | new HtmlWebpackPlugin({ 43 | template: path.join(__dirname, '../src/index.html'), 44 | filename: 'index.html', 45 | inject: 'body', 46 | hash:true 47 | }), 48 | ifProd(new webpack.optimize.DedupePlugin()), 49 | ifProd(new webpack.optimize.UglifyJsPlugin({ 50 | compress: { 51 | 'screw_ie8': true, 52 | 'warnings': false, 53 | 'unused': true, 54 | 'dead_code': true, 55 | }, 56 | output: { 57 | comments: false, 58 | }, 59 | sourceMap: false, 60 | })), 61 | ]), 62 | }; 63 | }; 64 | ``` 65 | 首先你应该关注如下的方法: 66 | ```js 67 | const ifProd = plugin => env.prod ? plugin : undefined; 68 | ``` 69 | 这个方法表示,只有在生产模式下才会添加特定的插件,如果不是在生产模式下,那么给插件就不需要添加,比如上面的 UglifyJsPlugin 插件压缩代码,在开发模式下是不需要的,只有在项目上线以后才需要将我们的 js/css 代码进行压缩。假如我们现在处于开发阶段,那么我们一般是需要启动 webpack-dev-server 的,我们看看如何对 webpack-dev-server进行配置: 70 | ```js 71 | //webpack-dev-server.js 72 | const webpack = require('webpack'); 73 | const WebpackDevServer = require('webpack-dev-server'); 74 | const webpackConfig = require('./webpack.config'); 75 | const path = require('path'); 76 | const env = {dev: process.env.NODE_ENV }; 77 | const devServerConfig = { 78 | contentBase: path.join(__dirname, '../build/'), 79 | historyApiFallback: { disableDotRule: true }, 80 | stats: { colors: true } 81 | }; 82 | const server = new WebpackDevServer(webpack(webpackConfig(env)), devServerConfig); 83 | server.listen(3000, 'localhost'); 84 | ``` 85 | 注意:上面直接调用 webpack 方法会得到一个 Compiler 对象,我们的 webpack-dev-server 的很多功能,比如 HMR 都是基于这个对象来完成的,包括从内存中拿到资源来处理请求(参考 webpack-dev-server 的 lazyload 部分)。此时我们直接调用 listen 方法来完成 webpack-dev-server 的启动。 86 | 87 | #### 2.2 配置package.json的script 88 | 通过配置 package.json 中的 script 部分,可以使得我们更加容易启动 cli 命令,比如我们配置的命令如下: 89 | ```js 90 | "scripts": { 91 | "start": "NODE_ENV=development node webpack/webpack-dev-server --env.dev", 92 | "build": "rm -rf build/* | NODE_ENV=production webpack --config webpack/webpack.config.js --progress --env.prod" 93 | }, 94 | ``` 95 | 此时我们可以通过下面简单的命令来替换复杂的webpack命令(参数很长): 96 | ```js 97 | npm start 98 | //或者npm run start 99 | npm run build 100 | //相当于rm -rf build/* | NODE_ENV=production webpack --config webpack/webpack.config.js --progress --env.prod 101 | ``` 102 | 此时你应该注意到了,在特定的命令后面,比如 start 命令后面会有 env.dev ,而 build 后会存在 env.prod, 所以,我们可以在 webpack.config.js 中通过 *env* 对象判断当前所处的环境,从而在生产模式下添加特定的 webpack 插件,比如上面说的 UglifyJsPlugin 或者 DedupePlugin 等等。此时运行*npm start*就可以启动服务器,在浏览器中打开*http://localhost:3000*就可以看到当前的页面了。 103 | 104 | #### 2.3 入口文件分析 105 | 在 Webpack 中一个重要的概念就是入口文件,通过入口文件我们可以构建前面章节所说的模块依赖图谱。我们看看上面的入口文件的内容: 106 | ```js 107 | //src/index.js 108 | import React from 'react'; 109 | import { render } from 'react-dom'; 110 | import Root from './root'; 111 | render(, document.getElementById('App')); 112 | ``` 113 | 而 root.js 中的内容如下: 114 | ```js 115 | import React from 'react'; 116 | import Router from 'react-router/lib/Router'; 117 | import browserHistory from 'react-router/lib/browserHistory'; 118 | import routes from './routes'; 119 | const Root = () => ; 120 | export default Root; 121 | ``` 122 | 此时,在 Router 组件中的 routes 配置就是指的前端路由对象,而这也是 webpack 结合 react-router 实现按需加载的核心代码,我们看看他真实的代码结构: 123 | ```js 124 | import Core from './components/Core'; 125 | function errorLoading(error) { 126 | throw new Error(`Dynamic page loading failed: ${error}`); 127 | } 128 | function loadRoute(cb) { 129 | return module => cb(null, module.default); 130 | } 131 | export default { 132 | path: '/', 133 | component: Core, 134 | indexRoute: { 135 | getComponent(location, cb) { 136 | System.import('./components/Home') 137 | .then(loadRoute(cb)) 138 | .catch(errorLoading); 139 | }, 140 | }, 141 | childRoutes: [ 142 | { 143 | path: 'about', 144 | getComponent(location, cb) { 145 | System.import('./components/About') 146 | .then(loadRoute(cb)) 147 | .catch(errorLoading); 148 | }, 149 | }, 150 | { 151 | path: 'users', 152 | getComponent(location, cb) { 153 | System.import('./components/Users') 154 | .then(loadRoute(cb)) 155 | .catch(errorLoading); 156 | }, 157 | }, 158 | { 159 | path: '*', 160 | getComponent(location, cb) { 161 | System.import('./components/Home') 162 | .then(loadRoute(cb)) 163 | .catch(errorLoading); 164 | }, 165 | }, 166 | ], 167 | }; 168 | ``` 169 | 注意:上面的例子使用的是 react-router 的对象配置方式,他的作用和下面的配置是一样的: 170 | ```js 171 | 172 | 173 | 174 | 175 | 176 | 177 | ``` 178 | 其中,最重要的代码就是上面看到的 System.import,它和 require.ensure 方法是一样的,这部分内容在[webpack1](http://webpack.github.io/docs/code-splitting.html#es6-modules)中就已经引入了。比如上面的配置: 179 | ```js 180 | { 181 | path: 'users', 182 | getComponent(location, cb) { 183 | System.import('./components/Users') 184 | .then(loadRoute(cb)) 185 | .catch(errorLoading); 186 | }, 187 | } 188 | ``` 189 | 表示如果路由满足*/users*的时候就会动态加载 components 下的 Users 组件,而且 Users 组件的内容是不会和入口文件打包在一起的,而是会单独打包到一个独立的 chunk 中的,只有这样才能实现按需加载的功能。而且针对上面的loadRoute方法也做一个说明: 190 | ```js 191 | function loadRoute(cb) { 192 | return module => cb(null, module.default); 193 | } 194 | ``` 195 | 其中加载 *module.default* 是因为 ES6 的模块机制导致的,你可以查看导出的模块内容: 196 | ```js 197 | //Users.js 198 | import React from 'react'; 199 | const Users = () =>
Users
; 200 | export default Users; 201 | ``` 202 | 其实是通过 *export default* 来完成的,如果引入了babel-plugin-add-module-export 就不需要这样处理了,你可以参考我写的[__esModule是什么?](https://github.com/liangklfangl/react-article-bucket/tree/master/es6/es6-QA)。如果要将上面的代码 users 路由修改为 require.ensure 加载,可以使用如下方式: 203 | ```js 204 | { 205 | require.ensure([], require => { 206 | cb(null, require('./components/Users').default) 207 | },'users'); 208 | }} /> 209 | 210 | ``` 211 | 注意 require.ensure的签名如下: 212 | ```js 213 | require.ensure(dependencies, callback, chunkName) 214 | ``` 215 | 所以,我们通过第三个参数可以指定该 chunk 的名称,如果不指定该 chunk 的名称,你将获得下面的 0.xx, 1.xx这种 webpack 自动分配的 chunk 名称。 216 | 217 | #### 2.4 依赖图谱分析 218 | 当你使用了 code splitting 特性,你可以使用很多工具来查看每一个 chunk 中都包含了什么特定的模块,比如我常用的这个[ webpack官方分析工具](https://github.com/webpack/analyse)。下面是我使用了这个工具查看本章节例子中的 stats.json 得到的依赖图谱。 219 | 220 | ![](./images/sketch.png) 221 | 222 | 通过这个图谱,你可以看到很多内容。比如其中的 entry 因为含有 webpack 的特定加载环境,所以需要在所有的 chunk 加载之前加载,这部分内容在前面章节也已经讲过; 而我们的 initial 部分表示在 webpack.config.js 中配置的入口文件。其他的 id 为0/1/2 的 chunk 就是动态产生的 chunk, 比如通过 System.import 或者 require.ensure 产生的动态的模块。 还有一点就是其中的 names 列,因为我们调用 require.ensure 的时候并没有指定当前的 chunk 的名称,即第三个参数,所以 names 就是为空数组。而且很多如 assets, modules, warnings, errors 等信息都可以在这个页面进行查看。此处不再赘述。当然,你也可以使用[第三方的工具](https://webpack.js.org/guides/code-splitting/#bundle-analysis)来查看我们的 stats.json 的内容。 223 | 224 | ### 3.按需加载的表现 225 | 当页面初始加载的时候你会看到下面的内容: 226 | 227 | ![](./images/1.png) 228 | 229 | 其中 vendor.js 应该很好理解,就是包含上面配置的框架代码: 230 | ```js 231 | vendor: ['react', 'react-dom', 'react-router'], 232 | ``` 233 | 这部分如果你不理解,你可以回到前面章节进行复习。而 app.js 就是入口文件内容,即不包含动态加载的模块的内容。而另外一个 *0.xxxx*的 chunk 就是我们上面配置的: 234 | ```js 235 | indexRoute: { 236 | getComponent(location, cb) { 237 | System.import('./components/Home') 238 | .then(loadRoute(cb)) 239 | .catch(errorLoading); 240 | }, 241 | } 242 | ``` 243 | 因为 indexRoute 表示默认初始化的子组件。而当你访问*localhost:3000/about*的时候将会看到下面的内容: 244 | 245 | ![](./images/2.png) 246 | 247 | 其中 *2.xx* 的内容就是上面配置的*about*组件的内容: 248 | ```js 249 | { 250 | path: 'about', 251 | getComponent(location, cb) { 252 | System.import('./components/About') 253 | .then(loadRoute(cb)) 254 | .catch(errorLoading); 255 | }, 256 | } 257 | ``` 258 | 而当你访问*localhost:3000/users*的时候将会看到下面的内容: 259 | 260 | ![](./images/3.png) 261 | 262 | 其中*1.xx*就是上面配置的如下内容: 263 | ```js 264 | { 265 | path: 'users', 266 | getComponent(location, cb) { 267 | System.import('./components/Users') 268 | .then(loadRoute(cb)) 269 | .catch(errorLoading); 270 | }, 271 | }, 272 | ``` 273 | 所以,按需加载的表现就是:当访问特定的路由的时候才会加载特定的模块,而不会将所有的代码一股脑的一次性全部加载进来。这对于优化首页加载的速度是很好的方案。同时,如果在上面你运行的是 *npm run start*,那么你在相应的目录下会看不到输出的文件,这是因为此时启动的是 webpack-dev-server,而 webpack-dev-server 会将输出的内容直接写出到内存中,而不是写到具体的文件系统中。但是如果你运行的是 *npm run build* 你会发现文件会写到文件系统中。我们看看在 webpack 中如何指定自己的输出结果到底是内存还是具体的文件系统: 274 | ```js 275 | const MemoryFS = require("memory-fs"); 276 | const webpack = require("webpack"); 277 | const fs = new MemoryFS(); 278 | const compiler = webpack({ /* options*/ }); 279 | compiler.outputFileSystem = fs; 280 | compiler.run((err, stats) => { 281 | // Read the output later: 282 | const content = fs.readFileSync("..."); 283 | }); 284 | ``` 285 | 通过这个 webpack 的官方实例你就会发现,其实只要我们指定了 compiler.outFileSystem 为 MemoryFS 实例,那么我们在 run/watch等方法中就可以通过相应的方法从内存中读取文件而不是文件系统了。这种方式在开发模式下还是很有用的,但是在生产模式下建议不要使用。 286 | 287 | ### 4.本章总结 288 | 通过本章节的学习,你应该对于 webpack+react-router 实现按需加载方案有了一个总体的认识。其实现的核心是通过 require.ensure 或者 System.import 来完成的。其中,本实例的完整代码你可以[点击这里查看](https://github.com/liangklfangl/webpack-code-splitting)。 289 | -------------------------------------------------------------------------------- /07-webpack常见插件原理分析.md: -------------------------------------------------------------------------------- 1 | #### 1.本章说明 2 | 主要讲解一下 Webpack 几个稍微简单的一点的插件原理,通过本章节的学习,你对前面的知识应该会有一个更加深入的理解。 3 | 4 | #### 2.prepack-webpack-plugin的说明 5 | 今年facebook开源了一个[prepack](https://github.com/facebook/prepack),当时就很好奇。它到底和webpack之间的关系是什么?于是各种google,最后还是去官网上看了下各种例子。例子都很好理解,但是对于其和webpack的关系还是有点迷糊。最后找到了一个好用的插件,即[prepack-webpack-plugin](https://github.com/gajus/prepack-webpack-plugin),这才恍然大悟~ 6 | 7 | #### 2.1 解析prepack-webpack-plugin源码 8 | 下面我们直接给出这个插件的apply源码,因为webpack的plugin的所有逻辑都是在apply方法中处理的。内容如下: 9 | ```js 10 | import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers'; 11 | import { 12 | RawSource 13 | } from 'webpack-sources'; 14 | import { 15 | prepack 16 | } from 'prepack'; 17 | import type { 18 | PluginConfigurationType, 19 | UserPluginConfigurationType 20 | } from './types'; 21 | const defaultConfiguration = { 22 | prepack: {}, 23 | test: /\.js($|\?)/i 24 | }; 25 | export default class PrepackPlugin { 26 | configuration: PluginConfigurationType; 27 | constructor (userConfiguration?: UserPluginConfigurationType) { 28 | this.configuration = { 29 | ...defaultConfiguration, 30 | ...userConfiguration 31 | }; 32 | } 33 | apply (compiler: Object) { 34 | const configuration = this.configuration; 35 | compiler.plugin('compilation', (compilation) => { 36 | compilation.plugin('optimize-chunk-assets', (chunks, callback) => { 37 | for (const chunk of chunks) { 38 | const files = chunk.files; 39 | //chunk.files获取该chunk产生的所有的输出文件,记住是输出文件 40 | for (const file of files) { 41 | const matchObjectConfiguration = { 42 | test: configuration.test 43 | }; 44 | if (!ModuleFilenameHelpers.matchObject(matchObjectConfiguration, file)) { 45 | // eslint-disable-next-line no-continue 46 | continue; 47 | } 48 | const asset = compilation.assets[file]; 49 | //获取文件本身 50 | const code = asset.source(); 51 | //获取文件的代码内容 52 | const prepackedCode = prepack(code, { 53 | ...configuration.prepack, 54 | filename: file 55 | }); 56 | //所以,这里是在webpack打包后对ES5代码的处理 57 | compilation.assets[file] = new RawSource(prepackedCode.code); 58 | } 59 | } 60 | callback(); 61 | }); 62 | }); 63 | } 64 | } 65 | ``` 66 | 首先对于webpack各种钩子函数时机不了解的可以[点击这里](https://github.com/liangklfangl/webpack-compiler-and-compilation)。如果对于webpack中各个对象的属性不了解的可以[点击这里](https://github.com/liangklfangl/webpack-common-sense)。接下来我们对上面的代码进行简单的剖析: 67 | 68 | (1)首先看for循环的前面那几句 69 | ```js 70 | const files = chunk.files; 71 | //chunk.files获取该chunk产生的所有的输出文件,记住是输出文件 72 | for (const file of files) { 73 | //这里我们只会对该chunk包含的文件中符合test规则的文件进行后续处理 74 | const matchObjectConfiguration = { 75 | test: configuration.test 76 | }; 77 | if (!ModuleFilenameHelpers.matchObject(matchObjectConfiguration, file)) { 78 | // eslint-disable-next-line no-continue 79 | continue; 80 | } 81 | } 82 | ``` 83 | 我们这里也给出ModuleFilenameHelpers.matchObject的代码: 84 | ```js 85 | //将字符串转化为regex 86 | function asRegExp(test) { 87 | if(typeof test === "string") test = new RegExp("^" + test.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")); 88 | return test; 89 | } 90 | ModuleFilenameHelpers.matchPart = function matchPart(str, test) { 91 | if(!test) return true; 92 | test = asRegExp(test); 93 | if(Array.isArray(test)) { 94 | return test.map(asRegExp).filter(function(regExp) { 95 | return regExp.test(str); 96 | }).length > 0; 97 | } else { 98 | return test.test(str); 99 | } 100 | }; 101 | ModuleFilenameHelpers.matchObject = function matchObject(obj, str) { 102 | if(obj.test) 103 | if(!ModuleFilenameHelpers.matchPart(str, obj.test)) 104 | return false; 105 | //获取test,如果这个文件名称符合test规则返回true,否则为false 106 | if(obj.include) 107 | if(!ModuleFilenameHelpers.matchPart(str, obj.include)) return false; 108 | if(obj.exclude) 109 | if(ModuleFilenameHelpers.matchPart(str, obj.exclude)) return false; 110 | return true; 111 | }; 112 | ``` 113 | 这几句代码是一目了然的,如果这个产生的文件名称符合test规则返回true,否则为false。 114 | 115 | (2)我们继续看后面对于符合规则的文件的处理 116 | ```js 117 | //如果满足规则我们继续处理~ 118 | const asset = compilation.assets[file]; 119 | //获取编译产生的资源 120 | const code = asset.source(); 121 | //获取文件的代码内容 122 | const prepackedCode = prepack(code, { 123 | ...configuration.prepack, 124 | filename: file 125 | }); 126 | //所以,这里是在webpack打包后对ES5代码的处理 127 | compilation.assets[file] = new RawSource(prepackedCode.code); 128 | ``` 129 | 其中asset.source表示的是模块的内容,你可以[点击这里](https://github.com/liangklfangl/webpack-common-sense)查看。假如我们的模块是一个html,内容如下: 130 | ```html 131 |
{{text}}
132 | ``` 133 | 最后打包的结果为: 134 | ```js 135 | module.exports = "
{{text}}
";' } 136 | ``` 137 | 这也是为什么我们会有下面的代码: 138 | ```js 139 | compilation.assets[basename] = { 140 | source: function () { 141 | return results.source; 142 | }, 143 | //source是文件的内容,通过fs.readFileAsync完成 144 | size: function () { 145 | return results.size.size; 146 | //size通过 fs.statAsync(filename)完成 147 | } 148 | }; 149 | return basename; 150 | }); 151 | ``` 152 | 前面两句代码我们都分析过了,我们继续看下面的内容: 153 | ```js 154 | const prepackedCode = prepack(code, { 155 | ...configuration.prepack, 156 | filename: file 157 | }); 158 | //所以,这里是在webpack打包后对ES5代码的处理 159 | compilation.assets[file] = new RawSource(prepackedCode.code); 160 | ``` 161 | 此时才真正的对webpack打包后的代码进行处理,prepack的nodejs用法可以[查看这里](https://prepack.io/getting-started.html)。最后一句代码其实就是操作我们的输出资源,在输出资源中添加一个文件,文件的内容就是prepack打包后的代码。其中webpack-source的内容你可以[点击这里](https://github.com/webpack/webpack-sources)。按照官方的说明,该对象可以获取源代码,hash,内容大小,sourceMap等所有信息。我们给出对RowSourceMap的说明: 162 |
163 | RawSource
164 | Represents source code without SourceMap.
165 | new RawSource(sourceCode: String)
166 | 
167 | 168 | 很显然,就是显示源代码而不包含sourceMap。 169 | 170 | #### 2.2 prepack-webpack-plugin总结 171 | 所以,从我的理解来说,prepack作用于webpack的时机在于:将源代码转化为ES5以后。从上面的html的编译结果就可以知道了,至于它到底做了什么,以及如何做的,还请查看[官网](https://prepack.io/getting-started.html) 172 | 173 | 174 | ### 3.BannerPlugin插件分析 175 | 我们现在讲述一下BannerPlugin内部的原理。他的主要用法如下: 176 | ```js 177 | { 178 | banner: string, 179 | // the banner as string, it will be wrapped in a comment 180 | raw: boolean, 181 | //如果配置了raw,那么banner会被包裹到注释当中 182 | entryOnly: boolean, 183 | //如果设置为true,那么banner仅仅会被添加到入口文件产生的chunk中 184 | test: string | RegExp | Array, 185 | include: string | RegExp | Array, 186 | exclude: string | RegExp | Array, 187 | } 188 | ``` 189 | 我们看看他的内部代码: 190 | ```js 191 | "use strict"; 192 | const ConcatSource = require("webpack-sources").ConcatSource; 193 | const ModuleFilenameHelpers = require("./ModuleFilenameHelpers"); 194 | //'This file is created by liangklfangl' =>/*! This file is created by liangklfangl */ 195 | function wrapComment(str) { 196 | if(!str.includes("\n")) return `/*! ${str} */`; 197 | return `/*!\n * ${str.split("\n").join("\n * ")}\n */`; 198 | } 199 | class BannerPlugin { 200 | constructor(options) { 201 | if(arguments.length > 1) 202 | throw new Error("BannerPlugin only takes one argument (pass an options object)"); 203 | if(typeof options === "string") 204 | options = { 205 | banner: options 206 | }; 207 | this.options = options || {}; 208 | //配置参数 209 | this.banner = this.options.raw ? options.banner : wrapComment(options.banner); 210 | } 211 | apply(compiler) { 212 | let options = this.options; 213 | let banner = this.banner; 214 | compiler.plugin("compilation", (compilation) => { 215 | compilation.plugin("optimize-chunk-assets", (chunks, callback) => { 216 | chunks.forEach((chunk) => { 217 | //入口文件都是默认首次加载的,即isInitial为true,和require.ensure按需加载是完全不一样的 218 | if(options.entryOnly && !chunk.isInitial()) return; 219 | chunk.files 220 | .filter(ModuleFilenameHelpers.matchObject.bind(undefined, options)) 221 | //只要满足test正则表达式的文件才会被处理 222 | .forEach((file) => 223 | compilation.assets[file] = new ConcatSource( 224 | banner, "\n", compilation.assets[file] 225 | //在原来的输出文件头部添加我们的banner信息 226 | ) 227 | ); 228 | }); 229 | callback(); 230 | }); 231 | }); 232 | } 233 | } 234 | module.exports = BannerPlugin; 235 | ``` 236 | 237 | ### 4.EnvironmentPlugin插件分析 238 | 该插件的使用方法如下: 239 | ```js 240 | new webpack.EnvironmentPlugin(['NODE_ENV', 'DEBUG']) 241 | ``` 242 | 此时相当于以以下方式使用DefinePlugin插件 243 | ```js 244 | new webpack.DefinePlugin({ 245 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 246 | 'process.env.DEBUG': JSON.stringify(process.env.DEBUG) 247 | }) 248 | ``` 249 | 当然,该插件也可以传入一个对象: 250 | ```js 251 | new webpack.EnvironmentPlugin({ 252 | NODE_ENV: 'development', 253 | // use 'development' unless process.env.NODE_ENV is defined 254 | DEBUG: false 255 | }) 256 | ``` 257 | 假如我们有如下的entry文件: 258 | ```js 259 | if (process.env.NODE_ENV === 'production') { 260 | console.log('Welcome to production'); 261 | } 262 | if (process.env.DEBUG) { 263 | console.log('Debugging output'); 264 | } 265 | ``` 266 | 如果我们执行*NODE_ENV=production webpack*命令,那么你会发现输出文件为如下内容: 267 | ```js 268 | if ('production' === 'production') { // <-- 'production' from NODE_ENV is taken 269 | console.log('Welcome to production'); 270 | } 271 | if (false) { // <-- default value is taken 272 | console.log('Debugging output'); 273 | } 274 | ``` 275 | 那么上面讲述了这个插件如何使用,那么我们看看他的内部原理是什么? 276 | ```js 277 | "use strict"; 278 | const DefinePlugin = require("./DefinePlugin"); 279 | //1.EnvironmentPlugin内部直接调用DefinePlugin 280 | class EnvironmentPlugin { 281 | constructor(keys) { 282 | this.keys = Array.isArray(keys) ? keys : Object.keys(arguments); 283 | } 284 | apply(compiler) { 285 | //2.这里直接使用compiler.apply方法来执行我们的DefinePlugin插件 286 | compiler.apply(new DefinePlugin(this.keys.reduce((definitions, key) => { 287 | const value = process.env[key]; 288 | //获取process.env中的参数 289 | if(value === undefined) { 290 | compiler.plugin("this-compilation", (compilation) => { 291 | const error = new Error(key + " environment variable is undefined."); 292 | error.name = "EnvVariableNotDefinedError"; 293 | //3.我们可以往compilation.warning里面填充我们自己的编译warning信息 294 | compilation.warnings.push(error); 295 | }); 296 | } 297 | definitions["process.env." + key] = value ? JSON.stringify(value) : "undefined"; 298 | //4.将我们的所有的key都封装到process.env上面了并返回(注意这里是向process.env上赋值) 299 | return definitions; 300 | }, {}))); 301 | } 302 | } 303 | module.exports = EnvironmentPlugin; 304 | ``` 305 | 306 | ### 5.MinChunkSizePlugin插件分析 307 | 这个插件的作用在于,如果产生的某个 Chunk 的大小小于阈值,那么直接和其他的 Chunk 合并,其主要使用方法如下: 308 | ```js 309 | new webpack.optimize.MinChunkSizePlugin({ 310 | minChunkSize: 10000 311 | }) 312 | ``` 313 | 我们看看他的内部原理是如何实现的: 314 | ```js 315 | class MinChunkSizePlugin { 316 | constructor(options) { 317 | if(typeof options !== "object" || Array.isArray(options)) { 318 | throw new Error("Argument should be an options object.\nFor more info on options, see https://webpack.github.io/docs/list-of-plugins.html"); 319 | } 320 | this.options = options; 321 | } 322 | apply(compiler) { 323 | const options = this.options; 324 | const minChunkSize = options.minChunkSize; 325 | compiler.plugin("compilation", (compilation) => { 326 | compilation.plugin("optimize-chunks-advanced", (chunks) => { 327 | let combinations = []; 328 | chunks.forEach((a, idx) => { 329 | for(let i = 0; i < idx; i++) { 330 | const b = chunks[i]; 331 | combinations.push([b, a]); 332 | } 333 | }); 334 | const equalOptions = { 335 | chunkOverhead: 1, 336 | // an additional overhead for each chunk in bytes (default 10000, to reflect request delay) 337 | entryChunkMultiplicator: 1 338 | //a multiplicator for entry chunks (default 10, entry chunks are merged 10 times less likely) 339 | //入口文件乘以的权重,所以如果含有入口文件,那么更加不容易小于minChunkSize,所以入口文件过小不容易被集成到别的chunk中 340 | }; 341 | combinations = combinations.filter((pair) => { 342 | return pair[0].size(equalOptions) < minChunkSize || pair[1].size(equalOptions) < minChunkSize; 343 | }); 344 | //对数组中元素进行删选,至少有一个chunk的值是小于minChunkSize的 345 | combinations.forEach((pair) => { 346 | const a = pair[0].size(options); 347 | const b = pair[1].size(options); 348 | const ab = pair[0].integratedSize(pair[1], options); 349 | //得到第一个chunk集成了第二个chunk后的文件大小 350 | pair.unshift(a + b - ab, ab); 351 | //这里的pair是如[0,1],[0,2]等这样的数组元素,前面加上两个元素:集成后总体积的变化量;集成后的体积 352 | }); 353 | //此时combinations的元素至少有一个的大小是小于minChunkSize的 354 | combinations = combinations.filter((pair) => { 355 | return pair[1] !== false; 356 | }); 357 | if(combinations.length === 0) return; 358 | //如果没有需要优化的,直接返回 359 | combinations.sort((a, b) => { 360 | const diff = b[0] - a[0]; 361 | if(diff !== 0) return diff; 362 | return a[1] - b[1]; 363 | }); 364 | //按照我们的集成后变化的体积来比较,从大到小排序 365 | const pair = combinations[0]; 366 | //得到第一个元素 367 | pair[2].integrate(pair[3], "min-size"); 368 | //pair[2]是我们的chunk,pair[3]也是chunk 369 | chunks.splice(chunks.indexOf(pair[3]), 1); 370 | //从chunks集合中删除集成后的chunk 371 | return true; 372 | }); 373 | }); 374 | } 375 | } 376 | module.exports = MinChunkSizePlugin; 377 | ``` 378 | 下面说明一下具体的其中主要的代码: 379 | ```js 380 | var combinations = []; 381 | var chunks=[0,1,2,3] 382 | chunks.forEach((a, idx) => { 383 | for(let i = 0; i < idx; i++) { 384 | const b = chunks[i]; 385 | combinations.push([b, a]); 386 | } 387 | }); 388 | ``` 389 | 变量combinations是组合形式,把自己和前面比自己小的元素组合成为一个元素。之所以是选择比自己的小的情况是为了减少重复的个数,如[0,2]和[2,0]必须只有一个。 390 | 391 | #### 6.本章小结 392 | 在本章节,我们主要讲了几个稍微简单一点的 Webpack 的 Plugin , 如果你对于 Plugin 的原理比较感兴趣,我想前面我介绍的那些基础知识已经够用了。至于很多复杂的 Plugin 就需要在平时开发的时候多关注和学习了。更多 Webpack 插件的分析你也可以[点击这里](https://github.com/liangklfang/webpack/tree/master/lib/optimize), 而至于插件本身的用法,我觉得[官网](https://webpack.js.org/plugins/min-chunk-size-plugin/)就已经足够了。 393 | -------------------------------------------------------------------------------- /12-以node方式集成webpack和webpack-dev-server打包.md: -------------------------------------------------------------------------------- 1 | ### 1.本章概述 2 | 到了本章节,本系列课程已经接近尾声了。在前面的章节中,我们论述了 webpack 的核心概念和使用, webpack-dev-server 的核心概念和使用, webpack 插件的编写, webpack 的 loader 的编写等等一系列 webpack 的核心内容。在本章节,我们主要讲述如何在此基础上写一个自己的打包工具,下面我们开始正文内容。 3 | 4 | ### 2.webpack的三种打包策略 5 | #### 2.1 webpack的一次性打包模式 6 | 我们前面已经讲过,调用 Compiler 对象的 run 方法可以开始 webpack 的打包过程,所以一次性打包的代码是极好编写的,你只需要执行下面的代码即可: 7 | ```js 8 | //defaultWebpackConfig表示webpack的配置,注意需要入口文件 9 | const compiler = webpack(defaultWebpackConfig); 10 | compiler.run(doneHandler); 11 | //调用run方法开始打包并监听打包结果 12 | function doneHandler(err, stats) { 13 | if(stats.hasErrors()){ 14 | printErrors(stats.compilation.errors,true); 15 | } 16 | const warnings =stats.warnings && stats.warnings.length==0; 17 | if(stats.hasWarnings()){ 18 | printErrors(stats.compilation.warnings); 19 | } 20 | console.log("Compilation finished!\n"); 21 | } 22 | function printErrors(errors,isError=false) { 23 | console.log("Compilation Errors or Warnings as follows:\n"); 24 | const strippedErrors = errors.map(function(error) { 25 | return stripAnsi(error); 26 | }); 27 | for(let i = 0; i < strippedErrors.length; i++) 28 | isError ? console.error(strippedErrors[i]) : console.warn(strippedErrors[i]); 29 | } 30 | ``` 31 | 其中在 doneHandler 方法中得到的 Stats 在前面的 Compiler 和 Compilation 章节已经讲过,你可以再去看看这部分的内容。下面给出一些配置,通过这些配置你可以进一步细粒度的控制前面说的 Stats 的展示内容: 32 | ```js 33 | stats: { 34 | // fallback value for stats options when an option is not defined (has precedence over local webpack defaults) 35 | all: undefined, 36 | //添加assets资源信息,和chunks区别前面章节已经说过 37 | assets: true, 38 | //通过一个字段来对assets资源排序,而!field表示反向排序 39 | assetsSort: "field", 40 | // Add information about cached (not built) modules 41 | //添加哪些模块是缓存的 42 | cached: true, 43 | //显示缓存的assets,如果设置为false表示只显示那些缓存的输出资源 44 | cachedAssets: true, 45 | //添加children的信息 46 | children: true, 47 | //添加chunks的信息 48 | chunks: true, 49 | //将编译的模块信息添加到chunk的信息中 50 | chunkModules: true, 51 | //添加chunk的来源信息 52 | chunkOrigins: true, 53 | //通过一个字段来对chunks资源排序,而!field表示反向排序 54 | chunksSort: "field", 55 | //模块解析的context目录 56 | context: "../src/", 57 | // 和`webpack --colors`命令一致 58 | colors: true, 59 | //显示某一个模块和入口模块的距离(层级) 60 | depth: false, 61 | //显示某一个相应的bundle的入口文件 62 | entrypoints: false, 63 | //添加--env信息 64 | env: false, 65 | //添加errors信息 66 | errors: true, 67 | //添加错误的相信信息 68 | errorDetails: true, 69 | // Exclude assets from being displayed in stats 70 | // This can be done with a String, a RegExp, a Function getting the assets name 71 | // and returning a boolean or an Array of the above. 72 | excludeAssets: "filter" | /filter/ | (assetName) => ... return true|false | 73 | ["filter"] | [/filter/] | [(assetName) => ... return true|false], 74 | // Exclude modules from being displayed in stats 75 | // This can be done with a String, a RegExp, a Function getting the modules source 76 | // and returning a boolean or an Array of the above. 77 | excludeModules: "filter" | /filter/ | (moduleSource) => ... return true|false | 78 | ["filter"] | [/filter/] | [(moduleSource) => ... return true|false], 79 | // See excludeModules 80 | exclude: "filter" | /filter/ | (moduleSource) => ... return true|false | 81 | ["filter"] | [/filter/] | [(moduleSource) => ... return true|false], 82 | //增加编译的哈希值 83 | hash: true, 84 | //添加最多显示的模块数量的限制 85 | maxModules: 15, 86 | //添加编译的模块信息 87 | modules: true, 88 | //通过指定的字段对模块进行排序,你可以使用 `!field` 来反转排序。默认是按照 `id` 排序。 89 | modulesSort: "field", 90 | //显示模块依赖以及warning/errors产生的原因(2.5.0以后引入) 91 | moduleTrace: true, 92 | //当文件大小超过`performance.maxAssetSize`指定的值以后输出提示信息 93 | performance: true, 94 | // Show the exports of the modules 95 | providedExports: false, 96 | //添加publicPath的信息 97 | publicPath: true, 98 | //添加某一个模块被引入的原因 99 | reasons: true, 100 | //添加模块的源代码 101 | source: true, 102 | //添加模块的时间信息 103 | timings: true, 104 | //显示哪一个模块的exports属性被使用 105 | usedExports: false, 106 | //添加webpack版本信息 107 | version: true, 108 | //添加warning信息 109 | warnings: true, 110 | //webpack 2.4.0以后引入,通过这个函数可以过滤输出的warning信息。可以是String,Regexp,或者函数,该函数可以返回一个boolean值。当然该值也可以同时指定String,Regexp,或者函数,将返回第一个匹配的值 111 | warningsFilter: "filter" | /filter/ | ["filter", /filter/] | (warning) => ... return true|false 112 | }; 113 | ``` 114 | 而具体的配置你可以通过传入 stats.toString/stats.toJson 方法来完成: 115 | ```js 116 | var webpack = require("webpack"); 117 | webpack({ 118 | //webpack配置 119 | }, function(err, stats) { 120 | if (err) { throw new gutil.PluginError('webpack:build', err); } 121 | gutil.log('[webpack:build]', stats.toString({ 122 | chunks: false, 123 | colors: true 124 | })); 125 | }); 126 | ``` 127 | 128 | #### 2.2 webpack的watch模式 129 | webpack 的 watch 模式表示当 webpack 的打包完成以后会继续监听文件的变化,从而重新打包。其相对于上面的一次性打包方式不同之处在于完成打包后并不是立即退出,而是继续监听依赖的文件的变化。其代码就是直接调用下面的* compiler.watch *方法: 130 | ```js 131 | //defaultWebpackConfig表示webpack的配置,注意需要入口文件 132 | const compiler = webpack(defaultWebpackConfig); 133 | compiler.watch(delay, doneHandler); 134 | //调用watch方法开始打包并监听打包结果 135 | function doneHandler(err, stats) { 136 | //stats.hasErrors()表示是否有errors 137 | if(stats.hasErrors()){ 138 | printErrors(stats.compilation.errors,true); 139 | } 140 | //stats.hasWarnings()表示是否有warnings 141 | if(stats.hasWarnings()){ 142 | printErrors(stats.compilation.warnings); 143 | } 144 | console.log("Compilation finished!\n"); 145 | } 146 | function printErrors(errors,isError=false) { 147 | console.log("Compilation Errors or Warnings as follows:\n"); 148 | const strippedErrors = errors.map(function(error) { 149 | return stripAnsi(error); 150 | }); 151 | for(let i = 0; i < strippedErrors.length; i++) 152 | isError ? console.error(strippedErrors[i]) : console.warn(strippedErrors[i]); 153 | } 154 | ``` 155 | 其实上面的 watch 方法还可以接收第二个参数,比如下面的例子: 156 | ```js 157 | compiler.watch({ // watch options: 158 | aggregateTimeout: 300, // wait so long for more changes 159 | poll: true // use polling instead of native watchers 160 | // pass a number to set the polling interval 161 | }, function(err, stats) { 162 | // ... 163 | }); 164 | ``` 165 | - aggregateTimeout 166 | 该参数表示当一个文件发生变化以后,不是立即开始一轮新的编译,而是会等待 aggregateTimeout 毫秒。这样 webpack 就可以将很多文件的变化放在一次编译中完成。该参数的默认值为300ms。 167 | 168 | - poll 169 | 该参数表示轮询。他会每隔一定时间去检查文件是否发生了变化,如果发生了变化就会重新编译。你可以通过下面的方式来指定: 170 | ```js 171 | poll: 1000 172 | ``` 173 | 上面的配置表示每隔1s会监听文件的变化并重新打包。注意:我们的 Watching 模式对于网络文件系统是不适用的,如果 Watching 模式不适用你可以使用下我们的轮询。 174 | 175 | - ignored 176 | 在很多系统中,监听所有文件变化会消耗大量的 CPU 和内存占用,因此很多情况下我们会排除一些文件的监听,比如常见的 node_modules 文件夹。此时,你可以使用这里的 ignored 配置。 177 | ```js 178 | ignored: /node_modules/ 179 | ``` 180 | 当然,你也可以使用下面这种深度匹配策略: 181 | ```js 182 | ignored: "files/**/*.js" 183 | ``` 184 | 此时不再监听 files 文件夹下的任何以 *.js* 结尾的文件的变化。调用 watch 方法后会得到一个 Watching 对象,该对象上也含有很多常用的方法。比如: 185 | 186 | - close方法 187 | 通过调用 Watching 对象的这个方法,我们会结束文件的监听操作。注意:如果当前的 Watching 对象没有调用 close/Invalidate ,那么不允许调用新的一轮的打包,比如 watch 或者 run 方法。其调用方式如下: 188 | ```js 189 | watching.close(() => { 190 | console.log("Watching Ended."); 191 | }); 192 | ``` 193 | 194 | - Invalidate方法 195 | 调用该方法表示本轮编译失效,但是并不会直接退出当前的文件监听。调用方式如下: 196 | ```js 197 | watching.invalidate(); 198 | ``` 199 | 200 | 其实上面的这个 close 方法和 Invalidate 方法在 [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) 插件中很常用,而我们的 webpack-dev-server 内部也是直接封装了 webpack-dev-middleware ,如下: 201 | ```js 202 | this.sockets = []; 203 | this.contentBaseWatchers = []; 204 | const webpackDevMiddleware = require('webpack-dev-middleware'); 205 | this.middleware = webpackDevMiddleware(compiler, options); 206 | //方法1:webpack-dev-server的middleware方法 207 | middleware: () => { 208 | app.use(this.middleware); 209 | } 210 | //方法2:webpack-dev-server直接调用webpack-dev-middleware的invalidate方法 211 | Server.prototype.invalidate = function () { 212 | if (this.middleware) this.middleware.invalidate(); 213 | }; 214 | //方法3:webpack-dev-server直接调用webpack-dev-middleware的close方法 215 | Server.prototype.close = function (callback) { 216 | this.sockets.forEach((sock) => { 217 | sock.close(); 218 | }); 219 | this.sockets = []; 220 | this.contentBaseWatchers.forEach((watcher) => { 221 | watcher.close(); 222 | }); 223 | this.contentBaseWatchers = []; 224 | this.listeningApp.kill(() => { 225 | this.middleware.close(callback); 226 | }); 227 | }; 228 | Server.prototype._watch = function (watchPath) { 229 | const watcher = chokidar.watch(watchPath).on('change', () => { 230 | //如果文件变化,那么通知所有的this.sockets集合中的socket,通知类型为'content-changed' 231 | this.sockWrite(this.sockets, 'content-changed'); 232 | //客户端会通过下面的方式进行监听 233 | // 'content-changed': function contentChanged() { 234 | // log.info('[WDS] Content base changed. Reloading...'); 235 | // self.location.reload(); 236 | // } 237 | }); 238 | this.contentBaseWatchers.push(watcher); 239 | }; 240 | Server.prototype.sockWrite = function (sockets, type, data) { 241 | sockets.forEach((sock) => { 242 | sock.write(JSON.stringify({ 243 | type, 244 | data 245 | })); 246 | }); 247 | }; 248 | ``` 249 | 上面看了 webpack-dev-server 是如何使用 webpack-dev-middleware 的,下面我们看看 webpack-dev-middleware 具体开放的 API : 250 | ```js 251 | var webpackDevMiddlewareInstance = webpackMiddleware(/* see example usage */); 252 | app.use(webpackDevMiddlewareInstance); 253 | //10s以后不再监听文件的变化 254 | setTimeout(function(){ 255 | webpackDevMiddlewareInstance.close(); 256 | }, 10000); 257 | ``` 258 | 上面这个例子展示了某一个时间后我们不再监听文件的变化。 259 | ```js 260 | var compiler = webpack(/* see example usage */); 261 | var webpackDevMiddlewareInstance = webpackMiddleware(compiler); 262 | app.use(webpackDevMiddlewareInstance); 263 | setTimeout(function(){ 264 | // After a short delay the configuration is changed 265 | // in this example we will just add a banner plugin: 266 | compiler.apply(new webpack.BannerPlugin('A new banner')); 267 | // Recompile the bundle with the banner plugin: 268 | webpackDevMiddlewareInstance.invalidate(); 269 | }, 1000); 270 | ``` 271 | 而这个例子展示了,*假如*我们的配置文件发生变化以后,我们可以通过调用 invalidate 方法重新开始打包工作(这个功能很常用,所以在[wcf](https://github.com/liangklfangl/wcf/blob/master/src/webpackWatch.js#L25)中当文件变化以后不应该是直接退出)。关于 [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware)的更多用法你可以官网查看。通过这个例子你可以知道了我们的 webpack-dev-middleware具有如下的构造函数: 272 | ```js 273 | module.exports = function(compiler, options) { 274 | } 275 | ``` 276 | 他会接受一个 Compiler 对象和用户配置作为参数,同时返回了一个中间件,这个中间件的签名为 *webpackDevMiddleware(req, res, next)* 类型,因此可以直接作为 Express 服务器的中间件使用( webpack-dev-server 本身就是一个 Express 服务器)。而其内部其实就是直接调用 Watching 对象的 close 或者 invalidate 方法而已,比如下面的 close 方法和 invalidate 方法: 277 | ```js 278 | close: function(callback) { 279 | callback = callback || function() {}; 280 | if(context.watching) context.watching.close(callback); 281 | else callback(); 282 | } 283 | ``` 284 | 下面是调用 Watching 方法的 invalidate 方法: 285 | ```js 286 | invalidate: function(callback) { 287 | callback = callback || function() {}; 288 | if(context.watching) { 289 | share.ready(callback, {}); 290 | context.watching.invalidate(); 291 | } else { 292 | callback(); 293 | } 294 | }, 295 | ``` 296 | 更多关于 [webpack-dev-server](https://webpack.js.org/configuration/dev-server/#devserver) 用法的内容你可以查看官网。 297 | 298 | #### 2.3 webpack-dev-server模式 299 | webpack-dev-server 模式需要我们首先添加 webpack 的 HMR 功能依赖的入口模块,并设置 server 启动的域名和端口号。接着,我们需要调用 webpack 方法获取到 Compiler 对象,接着把这个对象传入到我们的 webpack-dev-server 的实例中。这样,我们的服务器就可以监听文件的变化,并将打包的消息实时传递给前端页面实现 HMR 自动刷新。 300 | ```js 301 | function startDevServer(wpOpt, options) { 302 | addDevServerEntrypoints(wpOpt, options); 303 | //第一步:添加webpack-dev-server的入口文件 304 | let compiler; 305 | try { 306 | compiler = webpack(wpOpt); 307 | } catch (e) { 308 | console.log("webpack compile error!"); 309 | if (e instanceof webpack.WebpackOptionsValidationError) { 310 | console.error(colorError(options.stats.colors, e.message)); 311 | process.exit(1); 312 | } 313 | throw e; 314 | } 315 | //创建访问需要构建的域名 316 | const uri = 317 | createDomain(options) + 318 | (options.inline !== false || options.lazy === true 319 | ? "/" 320 | : "/webpack-dev-server/"); 321 | let server; 322 | try { 323 | //第二步:获取Compiler对象并传入webpack-dev-server 324 | server = new WebpackDevServer(compiler, options); 325 | } catch (e) { 326 | const OptionsValidationError = require("webpack-dev-server/lib/OptionsValidationError"); 327 | if (e instanceof OptionsValidationError) { 328 | console.error(colorError(options.stats.colors, e.message)); 329 | process.exit(1); 330 | } 331 | throw e; 332 | } 333 | server.listen(options.port, options.host, function(err) { 334 | if (err) throw err; 335 | reportReadiness(uri, options); 336 | }); 337 | } 338 | ``` 339 | - 第一步:添加 webpack-dev-server 的入口文件 340 | 341 | 通过添加 "webpack/hot/only-dev-server" 或者 "webpack/hot/dev-server" 可以启动 HMR 等高级功能,这部分内容在前面章节已经详细论述过了。 342 | ```js 343 | function createDomain(options) { 344 | const protocol = options.https ? "https" : "http"; 345 | return options.public ? `${protocol}://${options.public}` : url.format({ 346 | protocol: protocol, 347 | hostname: options.host, 348 | port: options.socket ? 0 : options.port.toString() 349 | }); 350 | }; 351 | module.exports = function addDevServerEntrypoints(webpackOptions, devServerOptions) { 352 | if(devServerOptions.inline !== false) { 353 | const domain = createDomain(devServerOptions); 354 | //创建启动的http服务器 355 | const devClient = [`${require.resolve("wds-hack")}?${domain}`]; 356 | if(devServerOptions.hotOnly) 357 | devClient.push("webpack/hot/only-dev-server"); 358 | else if(devServerOptions.hot) 359 | devClient.push("webpack/hot/dev-server"); 360 | [].concat(webpackOptions).forEach(function(wpOpt) { 361 | if(typeof wpOpt.entry === "object" && !Array.isArray(wpOpt.entry)) { 362 | Object.keys(wpOpt.entry).forEach(function(key) { 363 | wpOpt.entry[key] = devClient.concat(wpOpt.entry[key]); 364 | }); 365 | } else { 366 | wpOpt.entry = devClient.concat(wpOpt.entry); 367 | } 368 | }); 369 | } 370 | }; 371 | ``` 372 | 373 | - 第二步:获取Compiler对象并传入webpack-dev-server 374 | ```js 375 | import WebpackDevServer from "webpack-dev-server/lib/Server"; 376 | //wpOpt表示webpack配置 377 | var compiler = webpack(wpOpt); 378 | var server = new WebpackDevServer(compiler, options); 379 | server.listen(options.port, options.host, function(err) { 380 | if (err) throw err; 381 | reportReadiness(uri, options); 382 | }); 383 | } 384 | //启动服务器 385 | ``` 386 | 通过上面的代码,我们就在 nodejs 中正常启动了 webpack-dev-server。而至于其他内容,通过上面的 watch 模式的分析你应该已经知道了,这里就不再说了。 387 | 388 | ### 3.本章小结 389 | 通过本章节的学习,你对于 webpack 的 watch 模式,webpack-dev-server 模式,和一次性打包模式的代码编写已经有了一个大概的认识。本章节的完整代码你可以[在这里](https://github.com/liangklfangl/wcf)获取,该脚手架虽然比较简单,但是牵涉的内容还是很多的,只要你弄懂了里面的内容,写一个自己的打包脚手架已经不是难事。而对于 webpack-dev-middleware 如果有更深的兴趣,你可以阅读我的[ webpack-dev-middle 源码分析](https://github.com/liangklfang/webpack-dev-middleware)文章,比如里面就包含了 webpack-dev-server 为什么可以将资源编译到内存中的分析: 390 | ```js 391 | webpackDevMiddleware.fileSystem = context.fs; 392 | setFs: function(compiler) { 393 | //compiler.outputPath必须提供一个绝对路径,其就是我们在output.path中配置的内容 394 | if(typeof compiler.outputPath === "string" && !pathIsAbsolute.posix(compiler.outputPath) && !pathIsAbsolute.win32(compiler.outputPath)) { 395 | throw new Error("`output.path` needs to be an absolute path or `/`."); 396 | } 397 | // store our files in memory 398 | var fs; 399 | var isMemoryFs = !compiler.compilers && compiler.outputFileSystem instanceof MemoryFileSystem; 400 | //是否是MemoryFileSystem实例 401 | if(isMemoryFs) { 402 | fs = compiler.outputFileSystem; 403 | } else { 404 | fs = compiler.outputFileSystem = new MemoryFileSystem(); 405 | } 406 | context.fs = fs; 407 | } 408 | ``` 409 | 。最后,非常感谢你对于本系列课程的支持,如果有任何不对的地方欢迎在读者圈给我留言,我会及时修改。当然,如果有任何疑问,也欢迎讨论,共同进步! 410 | -------------------------------------------------------------------------------- /08-教你写一个webpack插件.md: -------------------------------------------------------------------------------- 1 | ### 1.本章概述 2 | 通过前面章节的内容,你对于 Webpack 的插件应该已经不陌生了,而且,对于 Webpack 很多高级的知识点应该都有了一定的了解。这包括 Webpack中的 Compiler 和 Compilation 对象,以及 Webpack 的插件原理。在本章节,我主要以官网提供的例子 FileListPlugin/HelloWorldPlugin 来说明如何写一个插件,而这部分内容在前面你应该已经都有了深入的了解,因此你应该会很轻松。同时,在本章节我也会给出 Webpack 中不同插件的类型与区别。但是,如果你想要写一个自己的 Webpack 的复杂插件,那么除了前面的内容以外,也要注意日常的积累。好了,下面开始我们的正文。 3 | 4 | ### 2.如何写一个 Webpack 的插件 5 | Webpack 的插件机制将 Webpack 引擎的能力暴露给了开发者。使用 Webpack 内置的各种打包阶段钩子函数使得开发者能够引入他们自己的打包流程。写一个 Webpack 插件往往比写一个 Loader 复杂,因为你需要了解 Webpack 内部很多细节的部分。 6 | #### 2.1 如何创建一个 Webpack 的插件 7 | 通过前面的章节你应该了解了,一个 Webpack 的插件其实包含以下几个条件: 8 | 9 | - 一个 js 命名函数 10 | - 在他的原型链上存在一个 apply 方法 11 | - 为该插件指定一个 Webpack 的事件钩子函数 12 | - 使用 Webpack 内部的实例对象( Compiler 或者 Compilation )具有的属性或者方法 13 | - 当功能完成以后,我们需要执行 Webpack 的回调函数 14 | 15 | 比如下面的函数就具备了上面的条件,所以他是可以作为一个 Webpack 插件的: 16 | ```js 17 | function MyExampleWebpackPlugin() { 18 | }; 19 | MyExampleWebpackPlugin.prototype.apply = function(compiler) { 20 | //我们主要关注compilation阶段,即webpack打包阶段 21 | compiler.plugin('compilation', function(compilation , callback) { 22 | console.log("This is an example plugin!!!"); 23 | //当该插件功能完成以后一定要注意回调callback函数 24 | callback(); 25 | }); 26 | }; 27 | ``` 28 | 29 | #### 2.2 Compiler和Compilation实例 30 | 在前面的章节,我已经深入的讲解了这部分的内容,我们下面总结性的给出两个对象的作用。 31 | 32 | - *Compiler*对象 33 | 这个 Compiler 对象代表了 Webpack 完整的可配置的的环境。这个对象在 Webpack 启动的时候会被创建,同时该对象也会被传入一些可控的配置,比如:Options, Loaders, Plugins。当插件被实例化的时候,会收到一个 Compiler 对象,通过这个对象你可以访问 Webpack 的内部环境。 34 | 35 | - *Compilation*对象 36 | 该对象在每次文件变化的时候都会被创建,因此会重新产生新的打包资源。我们的 Compilation 对象表示本次打包的模块,编译的资源,文件改变和监听的依赖文件的状态。而且该对象也会提供很多的回调点,我们的插件可以使用它来完成特定的功能。 而提供的钩子函数在前面的章节已经讲过了,此处不再赘述 37 | 38 | #### 2.3 Hello World插件 39 | 比如下面是我们写的一个插件: 40 | ```js 41 | //插件内部可以接受到该插件的配置参数 42 | function HelloWorldPlugin(options) { 43 | } 44 | HelloWorldPlugin.prototype.apply = function(compiler) { 45 | //此处利用了Compiler提供的done钩子函数,作用前面已经说过 46 | compiler.plugin('done', function() { 47 | console.log('Hello World!'); 48 | }); 49 | }; 50 | module.exports = HelloWorldPlugin; 51 | ``` 52 | 那么在 Webpack 配置文件中你就可以通过下面的方式来进行配置: 53 | ```js 54 | var HelloWorldPlugin = require('hello-world'); 55 | //已经发布到NPM 56 | var webpackConfig = { 57 | plugins: [ 58 | new HelloWorldPlugin({options: true}) 59 | ] 60 | }; 61 | ``` 62 | 我们前面已经说过,Webpack 插件最重要的就是 Compilation 和 Compiler 对象。我们看看在插件里面如何使用我们的 Compilation 对象: 63 | ```js 64 | function HelloCompilationPlugin(options) {} 65 | 66 | HelloCompilationPlugin.prototype.apply = function(compiler) { 67 | //使用Compiler对象的compilation钩子函数就可以获取Compilation对象 68 | compiler.plugin("compilation", function(compilation) { 69 | //使用Compilation注册回调 70 | compilation.plugin("optimize", function() { 71 | console.log("Assets are being optimized."); 72 | }); 73 | }); 74 | }; 75 | module.exports = HelloCompilationPlugin; 76 | ``` 77 | 78 | #### 2.4 异步插件 79 | 上面你看到的 HelloWorld 插件是同步的,还有一种插件是异步的,我们看看异步插件如何编写: 80 | ```js 81 | function HelloAsyncPlugin(options) {} 82 | 83 | HelloAsyncPlugin.prototype.apply = function(compiler) { 84 | compiler.plugin("emit", function(compilation, callback) { 85 | // Do something async... 86 | setTimeout(function() { 87 | console.log("Done with async work..."); 88 | callback(); 89 | }, 1000); 90 | 91 | }); 92 | }; 93 | module.exports = HelloAsyncPlugin; 94 | ``` 95 | 从这里你可以看出,异步插件和同步插件最大的不同在于,异步插件会传入一个 callback 参数,当你的插件完成的相应的功能以后,你必须回调这个 *callback* 函数。 96 | 97 | 当访问到 Webpack 的 Compiler 和 每次产生的 Compilation 对象的时候,你可以使用 Webpack 的引擎来完成任何事情。你可以重新处理已经存在的文件,创建自己的派生文件(你想要多产生的文件),或者对将要产生的资源进行修改(HtmlWebpackPlugin)等等。比如我们在前面章节就已经讲述的下面的实例,该实例就是有效的利用了 Compiler 的文件输出 emit 阶段产生我们自己需要的文件: 98 | ```js 99 | function FileListPlugin(options) {} 100 | FileListPlugin.prototype.apply = function(compiler) { 101 | compiler.plugin('emit', function(compilation, callback) { 102 | var filelist = 'In this build:\n\n'; 103 | //compilation.assets和compilation.chunks前面已经说过 104 | for (var filename in compilation.assets) { 105 | filelist += ('- '+ filename +'\n'); 106 | } 107 | //在compilation.assets中添加我们需要的资源 108 | compilation.assets['filelist.md'] = { 109 | source: function() { 110 | return filelist; 111 | }, 112 | size: function() { 113 | return filelist.length; 114 | } 115 | }; 116 | callback(); 117 | }); 118 | }; 119 | module.exports = FileListPlugin; 120 | ``` 121 | 122 | ### 3. Webpack 的插件类型 123 | 插件可以根据它注册的事件分成不同的类型。每一个特定的钩子函数决定了他会被如何执行,比如插件可以分为如下的类型: 124 | 125 | - 同步插件 126 | 127 | 此时我们的 Tapable 实例通过下面的方式来执行插件 128 | ```js 129 | applyPlugins(name: string, args: any...) 130 | //或者 131 | applyPluginsBailResult(name: string, args: any...) 132 | ``` 133 | 这意味着每一个插件的回调函数将会被按照顺序*依次执行(观察者模式)*,并传入特定的参数*args*,这是插件的最简单的格式。很多有用的钩子函数比如`"compile"`, `"this-compilation"`都期望每一个插件同步执行。下面给出 Webpack 对于 compile 这个钩子函数的执行方式: 134 | ```js 135 | Compiler.prototype.compile = function(callback) { 136 | self.applyPluginsAsync("before-compile", params, function(err) { 137 | self.applyPlugins("compile", params); 138 | //1.执行compile阶段,同步执行插件的方式 139 | var compilation = self.newCompilation(params); 140 | self.applyPluginsParallel("make", compilation, function(err) { 141 | compilation.finish(); 142 | compilation.seal(function(err) { 143 | self.applyPluginsAsync("after-compile", compilation, function(err) { 144 | }); 145 | }); 146 | }); 147 | }); 148 | }; 149 | ``` 150 | 151 | - 瀑布流插件 152 | 153 | 这种类型的插件通过下面的方法来执行: 154 | ```js 155 | applyPluginsWaterfall(name: string, init: any, args: any...) 156 | ``` 157 | 此时,每一个插件都会将前一个插件的返回值作为参数输入,并传入自己的参数,这种插件必须考虑插件的执行顺序。第一个插件传入的第二个参数值为*init*,而最后一个插件的返回值作为applyPluginsWaterfall的返回值。这种插件的模式常用于 Webpack 的模板,比如 158 | ModuleTemplate, ChunkTemplate。比如 ModuleTemplate 下就使用了如下的内容: 159 | ```js 160 | const Template = require("./Template"); 161 | module.exports = class ModuleTemplate extends Template { 162 | constructor(outputOptions) { 163 | super(outputOptions); 164 | } 165 | render(module, dependencyTemplates, chunk) { 166 | const moduleSource = module.source(dependencyTemplates, this.outputOptions, this.requestShortener); 167 | const moduleSourcePostModule = this.applyPluginsWaterfall("module", moduleSource, module, chunk, dependencyTemplates); 168 | const moduleSourcePostRender = this.applyPluginsWaterfall("render", moduleSourcePostModule, module, chunk, dependencyTemplates); 169 | //1.必须考虑插件的执行顺序 170 | return this.applyPluginsWaterfall("package", moduleSourcePostRender, module, chunk, dependencyTemplates); 171 | } 172 | updateHash(hash) { 173 | hash.update("1"); 174 | this.applyPlugins("hash", hash); 175 | } 176 | }; 177 | ``` 178 | 179 | - 异步插件 180 | 181 | 如果插件会被异步执行,那么应该使用下面的方式来完成: 182 | ```js 183 | applyPluginsAsync(name: string, args: any..., callback: (err?: Error) -> void) 184 | ``` 185 | 此时我们的插件处理函数调用的时候会传入args和签名为*(err?: Error) -> void*的回调函数。我们的处理函数将会按照*注册时候的顺序*被执行。而回调函数callback将会在所有的处理函数被调用以后调用。这种模式常常用于如"emit", "run"等钩子函数。比如下面的 Compiler 的 run 方法的具体逻辑。 186 | ```js 187 | //1.这里是异步的执行逻辑 188 | self.applyPluginsAsync("run", self, function(err) { 189 | if(err) return callback(err); 190 | self.readRecords(function(err) { 191 | if(err) return callback(err); 192 | //2.调用compile的回调函数 193 | self.compile(function onCompiled(err, compilation) { 194 | //其他代码逻辑 195 | }); 196 | }); 197 | }); 198 | ``` 199 | 200 | - 异步瀑布流插件 201 | 202 | 此时所有的插件将会被异步执行,同时遵循瀑布流的方式。此时以下面的方式来调用: 203 | ```js 204 | applyPluginsAsyncWaterfall(name: string, init: any, callback: (err: Error, result: any) -> void) 205 | ``` 206 | 此时插件的回调函数在调用的时候传入当前的值,回调函数被调用的时候会有如下的签名*(err: Error, nextValue: any) -> void*。如果回调函数被调用了,那么*nextValue*就会成为下一个处理函数的当前值。第一个处理函数的当前值为*init*。当所有的处理函数都执行以后,回调函数会传入最后一个插件的返回值。如果任何一个处理函数传入了一个*err*,那么回调函数将会传入错误参数*err*,此时余下的所有的处理函数都不会被执行。这种模式常常用于如"before-resolve"或者 "after-resolve"。 207 | 208 | - 异步序列化插件 209 | 210 | 这种模式和异步相同,但是区别在于只要一个插件失败,那么后续的插件都不会被调用。如果某一个插件传入了 Error 对象,那么回调函数的第一个参数将会是该 Error 对象,而且后续所有的回调函数都不会执行。这类插件的调用方式如下: 211 | ```js 212 | applyPluginsAsyncSeries(name: string, args: any..., callback: (err: Error, result: any) -> void) 213 | ``` 214 | 比如在 Compiler 的 seal 方法中将会有如下的调用逻辑: 215 | ```js 216 | self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) { 217 | //"optimize-tree"是异步的,但是只要有一个插件失败那么后续都不会执行 218 | ); 219 | ``` 220 | 221 | - 平行插件 222 | 此时通过下面的方式来执行插件: 223 | ```js 224 | applyPluginsParallel(name: string, args: any..., callback: (err?: Error) -> void) 225 | applyPluginsParallelBailResult(name: string, args: any..., callback: (err: Error, result: any) -> void) 226 | ``` 227 | 比如在 Compiler 的 compile 方法中将执行下面的代码: 228 | ```js 229 | self.applyPluginsParallel("make", compilation, function(err) { 230 | compilation.finish(); 231 | compilation.seal(function(err) { 232 | //其他逻辑 233 | }); 234 | }); 235 | ``` 236 | 这个回调函数只有在*所有的插件注册的该钩子函数的回调没有抛出错误的时候*才会执行。如果任意一个插件注册的回调函数抛出了一个错误,那么这个 callback 也会被执行,同时传入该错误对象,同时其他插件注册的处理函数将会直接忽略。 237 | 238 | 239 | ### 4.Webpack插件调用顺序 240 | Webpack 的源码中你经常会看到上面说的执行插件注册的方法,我们给出下面的seal方法的部分代码: 241 | ```js 242 | seal(callback) { 243 | self.applyPlugins0("seal"); 244 | self.applyPlugins0("optimize"); 245 | while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) || 246 | self.applyPluginsBailResult1("optimize-modules", self.modules) || 247 | self.applyPluginsBailResult1("optimize-modules-advanced", self.modules)); 248 | self.applyPlugins1("after-optimize-modules", self.modules); 249 | //这里是optimize module 250 | while(self.applyPluginsBailResult1("optimize-chunks-basic", self.chunks) || 251 | self.applyPluginsBailResult1("optimize-chunks", self.chunks) || 252 | self.applyPluginsBailResult1("optimize-chunks-advanced", self.chunks)); 253 | //这里是optimize chunk 254 | self.applyPlugins1("after-optimize-chunks", self.chunks); 255 | //这里是optimize tree 256 | self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) { 257 | self.applyPlugins2("after-optimize-tree", self.chunks, self.modules); 258 | const shouldRecord = self.applyPluginsBailResult("should-record") !== false; 259 | self.applyPlugins2("revive-modules", self.modules, self.records); 260 | self.applyPlugins1("optimize-module-order", self.modules); 261 | self.applyPlugins1("advanced-optimize-module-order", self.modules); 262 | self.applyPlugins1("before-module-ids", self.modules); 263 | self.applyPlugins1("module-ids", self.modules); 264 | self.applyModuleIds(); 265 | self.applyPlugins1("optimize-module-ids", self.modules); 266 | self.applyPlugins1("after-optimize-module-ids", self.modules); 267 | self.sortItemsWithModuleIds(); 268 | self.applyPlugins2("revive-chunks", self.chunks, self.records); 269 | self.applyPlugins1("optimize-chunk-order", self.chunks); 270 | self.applyPlugins1("before-chunk-ids", self.chunks); 271 | self.applyChunkIds(); 272 | self.applyPlugins1("optimize-chunk-ids", self.chunks); 273 | self.applyPlugins1("after-optimize-chunk-ids", self.chunks); 274 | self.sortItemsWithChunkIds(); 275 | if(shouldRecord) 276 | self.applyPlugins2("record-modules", self.modules, self.records); 277 | if(shouldRecord) 278 | self.applyPlugins2("record-chunks", self.chunks, self.records); 279 | self.applyPlugins0("before-hash"); 280 | self.createHash(); 281 | self.applyPlugins0("after-hash"); 282 | if(shouldRecord) 283 | self.applyPlugins1("record-hash", self.records); 284 | self.applyPlugins0("before-module-assets"); 285 | self.createModuleAssets(); 286 | if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) { 287 | self.applyPlugins0("before-chunk-assets"); 288 | self.createChunkAssets(); 289 | } 290 | self.applyPlugins1("additional-chunk-assets", self.chunks); 291 | self.summarizeDependencies(); 292 | if(shouldRecord) 293 | self.applyPlugins2("record", self, self.records); 294 | self.applyPluginsAsync("additional-assets", err => { 295 | if(err) { 296 | return callback(err); 297 | } 298 | self.applyPluginsAsync("optimize-chunk-assets", self.chunks, err => { 299 | if(err) { 300 | return callback(err); 301 | } 302 | self.applyPlugins1("after-optimize-chunk-assets", self.chunks); 303 | self.applyPluginsAsync("optimize-assets", self.assets, err => { 304 | if(err) { 305 | return callback(err); 306 | } 307 | self.applyPlugins1("after-optimize-assets", self.assets); 308 | if(self.applyPluginsBailResult("need-additional-seal")) { 309 | self.unseal(); 310 | return self.seal(callback); 311 | } 312 | return self.applyPluginsAsync("after-seal", callback); 313 | }); 314 | }); 315 | }); 316 | }); 317 | } 318 | ``` 319 | 而各个钩子函数执行的顺序你可以查看下面的内容: 320 | ```js 321 | 'before run' 322 | 'run' 323 | compile:func//调用compile函数 324 | 'before compile' 325 | 'compile'//(1)compiler对象的第一阶段 326 | newCompilation:object//创建compilation对象 327 | 'make' //(2)compiler对象的第二阶段 328 | compilation.finish:func 329 | "finish-modules" 330 | compilation.seal 331 | "seal" 332 | "optimize" 333 | "optimize-modules-basic" 334 | "optimize-modules-advanced" 335 | "optimize-modules" 336 | "after-optimize-modules"//首先是优化模块 337 | "optimize-chunks-basic" 338 | "optimize-chunks"//然后是优化chunk 339 | "optimize-chunks-advanced" 340 | "after-optimize-chunks" 341 | "optimize-tree" 342 | "after-optimize-tree" 343 | "should-record" 344 | "revive-modules" 345 | "optimize-module-order" 346 | "advanced-optimize-module-order" 347 | "before-module-ids" 348 | "module-ids"//首先优化module-order,然后优化module-id 349 | "optimize-module-ids" 350 | "after-optimize-module-ids" 351 | "revive-chunks" 352 | "optimize-chunk-order" 353 | "before-chunk-ids"//首先优化chunk-order,然后chunk-id 354 | "optimize-chunk-ids" 355 | "after-optimize-chunk-ids" 356 | "record-modules"//record module然后record chunk 357 | "record-chunks" 358 | "before-hash" 359 | compilation.createHash//func 360 | "chunk-hash"//webpack-md5-hash 361 | "after-hash" 362 | "record-hash"//before-hash/after-hash/record-hash 363 | "before-module-assets" 364 | "should-generate-chunk-assets" 365 | "before-chunk-assets" 366 | "additional-chunk-assets" 367 | "record" 368 | "additional-assets" 369 | "optimize-chunk-assets" 370 | "after-optimize-chunk-assets" 371 | "optimize-assets" 372 | "after-optimize-assets" 373 | "need-additional-seal" 374 | unseal:func 375 | "unseal" 376 | "after-seal" 377 | "after-compile"//(4)完成模块构建和编译过程(seal函数回调) 378 | "emit"//(5)compile函数的回调,compiler开始输出assets,是改变assets最后机会 379 | "after-emit"//(6)文件产生完成 380 | ``` 381 | 382 | ### 5.本章小结 383 | 在本章节,我们给出了不同种类的 Webpack 的插件类型与区别,同时给出了 Webpack 中各个钩子函数调用的顺序与时机,也给出了官网的一个关于 HelloWorld/FileListPlugin 插件的用法。虽然这个例子比较简单,但是可以对前面的内容做一个总结和回顾。如果你需要写出复杂的插件,通过前面章节的论述应该也不难。如果你对本章节的内容有疑问,你也可以查看我写的[Compiler 和 Compilation](https://github.com/liangklfangl/webpack-compiler-and-compilation)文章。 384 | -------------------------------------------------------------------------------- /01-webpack核心概念.md: -------------------------------------------------------------------------------- 1 | ### 1.webpack的基础作用与打包实例 2 | webpack是一个最新的js模块管理器。当webpack处理你的应用的时候,它会递归的创建一个依赖图谱,这个图谱会详细的列出你应用需要的每一个模块,最终将这些模块打包到几个小的输出文件中。当然一般只是一个文件(采用*webpack-common-chunk*插件除外),最终这个文件将会通过浏览器来加载。这里的核心概念是图谱,我这里在开始webpack的核心概念介绍之前,先给出一个图谱的例子,希望对你下面理解webpack有一定的帮助。 3 | 4 | 假如,我们有如下的webpack配置: 5 | ```js 6 | var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); 7 | module.exports = { 8 | entry: { 9 | main: process.cwd()+'/example3/main.js', 10 | main1: process.cwd()+'/example3/main1.js', 11 | common1:["jquery"], 12 | common2:["vue"] 13 | }, 14 | output: { 15 | path: process.cwd()+'/dest/example3', 16 | filename: '[name].js' 17 | }, 18 | plugins: [ 19 | new CommonsChunkPlugin({ 20 | name: ["chunk",'common1','common2'], 21 | minChunks:2 22 | 23 | }) 24 | ] 25 | }; 26 | ``` 27 | 而且各个模块的依赖关系如下图: 28 | 29 | ![](./images/dependency.png) 30 | 31 | 此时我们可以看一下上文说到的模块依赖图谱: 32 | 33 | ![](https://github.com/liangklfangl/commonchunkplugin-source-code/blob/master/5.png?raw=true) 34 | 35 | 在这个图谱中你可以清楚的看到各个模块的依赖关系。如main.js和main1.js的模块id分别为3,2,而且它们的父级模块的id为1,即chunk.js,而chunk.js的父级模块id为0,即common1.js,最后就是我们的common1.js的父级模块的id为4,即我们的common2.js。也就是说,如果我们使用了webpack-common-chunk插件,那么我们会产生多个输出资源文件,而且输出资源的name是通过output.filename配置来指定的。不过有几个注意点你需要了解下: 36 | 37 | #### 1.1 webpack-common-chunk抽取公共模块的逻辑 38 | 我们上面的例子入口文件的配置是如下形式: 39 | ```js 40 | entry: { 41 | main: process.cwd()+'/example3/main.js', 42 | main1: process.cwd()+'/example3/main1.js', 43 | common1:["jquery"], 44 | common2:["vue"] 45 | } 46 | ``` 47 | 而且output的配置为: 48 | ```js 49 | output: { 50 | path: process.cwd()+'/dest/example3', 51 | filename: '[name].js' 52 | }, 53 | ``` 54 | 此时,我们至少输出4个文件,分别为main.js,main1.js,common1.js,common2.js,但是我们在webpack中又配置了webpack-common-chunk这个插件: 55 | ```js 56 | new CommonsChunkPlugin({ 57 | name: ["chunk",'common1','common2'], 58 | minChunks:2 59 | //这个配置表示,如果一个模块的依赖次数至少为2次才会被抽取到公共模块中 60 | }) 61 | ``` 62 | 这个插件首先会将main.js和main1.js中出现*两次以上*模块依赖次数的模块单独提取出来,如上图中的chunk1.js和chunk2.js(在main.js和main1.js中都被引用了),将他们的代码抽取到chunk.js中;然后我们将chunk.js中被依赖两次以上的模块抽取到common1.js中;接着,我们继续将common1.js中被依赖两次以上的代码抽取到common2.js中。最后,我们将会发现: 63 |
 64 | (1)main1.js中只含有jquery.js
 65 | (2)main2.js中只含有vue.js
 66 | (3)chunk.js中含有main1.js和main2.js的公共模块,即chunk1.js和chunk2.js的内容
 67 | (4)common1.js中只含有jquery代码,因为chunk.js中不含有依赖两次以上的模块
 68 | (5)common2.js中只含有vue.js代码,因为common1.js中不含有依赖两次以上的模块
 69 | 
70 | 71 | #### 1.2 模块加载顺序 72 | 73 | 经过webpack-common-chunk处理后的代码有一点要注意,即抽取层级越高的代码应该越先加载。具体的含义可以通过上面的例子来说明下。比如,上面的common-chunk-plugin的配置如下: 74 | ```js 75 | new CommonsChunkPlugin({ 76 | name: ["chunk",'common1','common2'], 77 | minChunks:2 78 | //这个配置表示,如果一个模块的依赖次数至少为2次才会被抽取到公共模块中 79 | }) 80 | ``` 81 | 这样我们应该最先加载common2.js,然后是common1.js,chunk.js,最后才是我们的index.js。所以,我们经常可以看到下面的html模板: 82 | ```html 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ``` 94 | 之所以是这样,其实很好理解。原因之一在于:我们将下级模块公共的代码已经抽取到其他文件中了,这样,如果我们不预先加载公共模块而先加载其他模块,那么就会出现模块找不到的报错信息。原因之二:假如你去看过我们打包后的[common2.js](https://github.com/liangklfangl/commonsChunkPlugin_Config/blob/master/dest/example3/common2.js#L1),你可能会看到如下的代码: 95 | ```js 96 | /******/ (function(modules) { // webpackBootstrap 97 | /******/ // install a JSONP callback for chunk loading 98 | /******/ var parentJsonpFunction = window["webpackJsonp"]; 99 | /******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) { 100 | /******/ // add "moreModules" to the modules object, 101 | /******/ // then flag all "chunkIds" as loaded and fire callback 102 | /******/ var moduleId, chunkId, i = 0, callbacks = []; 103 | /******/ for(;i < chunkIds.length; i++) { 104 | /******/ chunkId = chunkIds[i]; 105 | /******/ if(installedChunks[chunkId]) 106 | /******/ callbacks.push.apply(callbacks, installedChunks[chunkId]); 107 | /******/ installedChunks[chunkId] = 0; 108 | /******/ } 109 | /******/ for(moduleId in moreModules) { 110 | /******/ modules[moduleId] = moreModules[moduleId]; 111 | /******/ } 112 | /******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules); 113 | /******/ while(callbacks.length) 114 | /******/ callbacks.shift().call(null, __webpack_require__); 115 | /******/ if(moreModules[0]) { 116 | /******/ installedModules[0] = 0; 117 | /******/ return __webpack_require__(0); 118 | /******/ } 119 | /******/ }; 120 | 121 | /******/ // The module cache 122 | /******/ var installedModules = {}; 123 | 124 | /******/ // object to store loaded and loading chunks 125 | /******/ // "0" means "already loaded" 126 | /******/ // Array means "loading", array contains callbacks 127 | /******/ var installedChunks = { 128 | /******/ 1:0 129 | /******/ }; 130 | 131 | /******/ // The require function 132 | /******/ function __webpack_require__(moduleId) { 133 | 134 | /******/ // Check if module is in cache 135 | /******/ if(installedModules[moduleId]) 136 | /******/ return installedModules[moduleId].exports; 137 | 138 | /******/ // Create a new module (and put it into the cache) 139 | /******/ var module = installedModules[moduleId] = { 140 | /******/ exports: {}, 141 | /******/ id: moduleId, 142 | /******/ loaded: false 143 | /******/ }; 144 | 145 | /******/ // Execute the module function 146 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 147 | 148 | /******/ // Flag the module as loaded 149 | /******/ module.loaded = true; 150 | 151 | /******/ // Return the exports of the module 152 | /******/ return module.exports; 153 | /******/ } 154 | 155 | /******/ // This file contains only the entry chunk. 156 | /******/ // The chunk loading function for additional chunks 157 | /******/ __webpack_require__.e = function requireEnsure(chunkId, callback) { 158 | /******/ // "0" is the signal for "already loaded" 159 | /******/ if(installedChunks[chunkId] === 0) 160 | /******/ return callback.call(null, __webpack_require__); 161 | 162 | /******/ // an array means "currently loading". 163 | /******/ if(installedChunks[chunkId] !== undefined) { 164 | /******/ installedChunks[chunkId].push(callback); 165 | /******/ } else { 166 | /******/ // start chunk loading 167 | /******/ installedChunks[chunkId] = [callback]; 168 | /******/ var head = document.getElementsByTagName('head')[0]; 169 | /******/ var script = document.createElement('script'); 170 | /******/ script.type = 'text/javascript'; 171 | /******/ script.charset = 'utf-8'; 172 | /******/ script.async = true; 173 | /******/ script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"common1","2":"main","3":"main1","4":"chunk"}[chunkId]||chunkId) + ".js"; 174 | /******/ head.appendChild(script); 175 | /******/ } 176 | /******/ }; 177 | 178 | /******/ // expose the modules object (__webpack_modules__) 179 | /******/ __webpack_require__.m = modules; 180 | 181 | /******/ // expose the module cache 182 | /******/ __webpack_require__.c = installedModules; 183 | 184 | /******/ // __webpack_public_path__ 185 | /******/ __webpack_require__.p = ""; 186 | 187 | /******/ // Load entry module and return exports 188 | /******/ return __webpack_require__(0); 189 | /******/ }) 190 | ``` 191 | 最顶级的输出文件中会包含webpack加载其他模块的公共代码,你可以理解为*加载器*,如果不先加载顶级的模块(上面的例子是common2.js),那么就无法通过它来加载其他的模块,因此也会报错。这里提到的例子代码你可以[点击这里](https://github.com/liangklfangl/commonsChunkPlugin_Config/tree/master/example3)来仔细阅读。 192 | 193 | 好了,上面讲了一个复杂的例子,如果你不懂也没关系,在webpack常见插件部分我会更加详细的分析。我们下面看看一些webpack的基础概念。 194 | 195 | ### 2.webpack的基础概念 196 | #### 2.1 webpack的入口文件 197 | 上面说过,webpack会构建一个模块的依赖图谱,而构建这个图谱的起点就是我们说的入口文件。webpack相当于从这个起点来绘制类似于上面的整个图谱。通过这个图谱,我们可以知道哪些模块会被打包到最终的文件中,而那些没有出现在图谱中的模块将会被忽略。在webpack中我们使用*entry*这个配置参数来指定我们的webpack入口文件,比如我们上面的例子: 198 | ```js 199 | entry: { 200 | main: process.cwd()+'/example3/main.js', 201 | main1: process.cwd()+'/example3/main1.js', 202 | common1:["jquery"], 203 | common2:["vue"] 204 | } 205 | ``` 206 | 这也就是告诉我们,我们的入口文件总共是4个,那么我们最终会构建出四个不同的文件依赖图谱。当然,这种配置常用于单页面应用,一般只有一个入口文件。这个配置可以允许我们使用CommonChunkPlugin,然后将那些公共的模块抽取到vendor.js(名字可以自定义)中。这种形式,通过上面的例子你应该已经了解到了。 207 | 208 | 当然,上面这种配置依然可以应用于*多页面*应用,比如下面的例子: 209 | ```js 210 | const config = { 211 | entry: { 212 | pageOne: './src/pageOne/index.js', 213 | pageTwo: './src/pageTwo/index.js', 214 | pageThree: './src/pageThree/index.js' 215 | } 216 | }; 217 | ``` 218 | 我们告诉webpack需要三个不同的依赖图谱。因为在多页面应用中,当你访问一个URL的时候,实际上你需要的是一个html文档,我们只需要将打包后的资源插入到你的html文档中就可以了。比如上面的配置,我们需要分别读取pageOne,pageTwo,pageThree下的index.js作为入口文件,然后结合我们的html模板打包一次,并将打包后的html以及输出资源结合起来输出到特定的文件路径下,这样当我们访问固定的html的时候就可以了。因此,webpack的单页面打包和多页面的打包其实原理是完全一致的。 219 | 220 | #### 2.2 webpack的output常见配置 221 | ##### 2.2.1 output.path 222 | 当我们的资源打包后,我们需要指定特定的输出路径,这就是output需要完成的事情。此时,我们可以通过output配置来完成,比如我们上面的例子: 223 | ```js 224 | entry: { 225 | main: process.cwd()+'/example3/main.js', 226 | main1: process.cwd()+'/example3/main1.js', 227 | common1:["jquery"], 228 | common2:["vue"] 229 | }, 230 | output: { 231 | path: process.cwd()+'/dest/example3', 232 | filename: '[name].js' 233 | }, 234 | ``` 235 | 我们指定了输出路径为*process.cwd()+'/dest/example3'*,相当于告诉webpack所有的输出文件全部放到这个目录下。 236 | 237 | ##### 2.2.2 output.filename 238 | 上面的文件名称我们指定为*'[name].js'*,其中[name]和entry中的key保持一致。其中filename的配置还是比较多的,不仅可以使用name,还可以使用id,hash,chunkhash等。 239 |
240 | [name]:这个模块的名称,就是entry中指定的key
241 | [id]:这个模块的id,由webpack来分配,通过上面的依赖图谱你可以看到
242 | [hash]:每次webpack完成一个打包都会生成这个hash
243 | [chunkhash]:webpack每生成一个文件就叫一个chunk,这个chunk本身的hash
244 | 
245 | 其中这里的hash如果不懂,可以查看[webpack-dev-server](https://github.com/liangklfangl/webpack-dev-server)的"深入源码分析"部分,这里有详细的论述。 246 | 247 | ##### 2.2.3 publichPath与output.path 248 | 打包好的bundle在被请求的时候,其路径是相对于你配置的`publicPath`来说的。因为我理解的publicPath相当于虚拟路径,其映射于你指定的`output.path`。假如你指定的publicPath为 "/assets/",而且output.path为"build",那么相当于虚拟路径"/assets/"对应于"build"(前者和后者指向的是同一个位置),而如果build下有一个"index.css",那么通过虚拟路径访问就是`/assets/index.css`。比如我们有一个如下的配置: 249 | ```js 250 | module.exports = { 251 | entry: { 252 | app: ["./app/main.js"] 253 | }, 254 | output: { 255 | path: path.resolve(__dirname, "build"), 256 | publicPath: "/assets/", 257 | //此时相当于/assets/路径对应于build目录,是一个映射的关系 258 | filename: "bundle.js" 259 | } 260 | } 261 | ``` 262 | 那么我们要访问编译后的资源可以通过localhost:8080/assets/bundle.js来访问。如果我们在build目录下有一个html文件,那么我们可以使用下面的方式来访问js资源 263 | ```html 264 | 265 | 266 | 267 | 268 | Document 269 | 270 | 271 | 272 | 273 | 274 | ``` 275 | ##### 2.2.4 hotUpdateChunkFilename vs hotUpdateMainFilename 276 | 我这里提供了一个[例子](https://github.com/liangklfangl/wcf),当你修改了test目录下的文件的时候,比如修改了scss文件,此时你会发现在页面中多出了一个script元素,内容如下: 277 | ```html 278 | 279 | ``` 280 | 当你打开它你会看到: 281 | ```js 282 | webpackHotUpdate(0,{ 283 | /***/ 15: 284 | /***/ (function(module, exports, __webpack_require__) { 285 | exports = module.exports = __webpack_require__(46)(); 286 | // imports 287 | // module 288 | exports.push([module.i, "html {\n border: 1px solid yellow;\n background-color: pink; }\n\nbody {\n background-color: lightgray;\n color: black; }\n body div {\n font-weight: bold; }\n body div span {\n font-weight: normal; }\n", ""]); 289 | // exports 290 | 291 | /***/ }) 292 | }) 293 | //# sourceMappingURL=0.188304c98f697ecd01b3.hot-update.js.map 294 | ``` 295 | 从内容你也可以看出,只是将我们修改的模块push到exports对象中!而hotUpdateChunkFilename就是为了让你能够执行script的src中的值!而同样的hotUpdateMainFilename是一个json文件用于指定哪些模块发生了变化,在output目录下。 296 | 297 | ##### 2.2.5 externals vs libraryTarget vs library 298 | 假如我们需要完成下面的两个需求: 299 |
300 | 1.我们的模块依赖于jQuery,但是我们不希望jQuery打包到最后的文件中去
301 | 2.我们的模块要存在于全局的变量Foo上面
302 | 
303 | 那么我们需要将webpack配置如下: 304 | ```js 305 | module.exports = { 306 | entry: 307 | { 308 | main:process.cwd()+'/example1/main.js', 309 | }, 310 | output: { 311 | path:process.cwd()+'/dest/example1', 312 | filename: '[name].js', 313 | // export itself to a global var 314 | libraryTarget: "var", 315 | // name of the global var: "Foo" 316 | library: "Foo" 317 | }, 318 | externals: { 319 | // require("jquery") is external and available 320 | // on the global var jQuery 321 | "jquery": "jQuery" 322 | }, 323 | plugins: [ 324 | new CommonsChunkPlugin({ 325 | name:"chunk", 326 | minChunks:2 327 | }), 328 | new HtmlWebpackPlugin() 329 | ] 330 | }; 331 | ``` 332 | 其中external配置表示我们的模块中的require('jquery')中的jquery来自于window.jQuery,也就是来自于全局对象jQuery,而不要单独打包到我们的入口文件的bundle中,在页面中我们通过script标签来引入! 333 | ```js 334 | externals: { 335 | // require("jquery") is external and available 336 | // on the global var jQuery 337 | "jquery": "jQuery" 338 | } 339 | ``` 340 | 下面我们详细的分析下libraryTarget和library相关内容 341 |
342 | library:在output中配置,可以指定你的库的名称
343 | libraryTarget:指定你的模块输出类型,可以是commonjs,AMD,script形式,UMD模式
344 | 
345 | 例子1:其中我们的libraryTarget设置为var,而library设置为'Foo'。也就是表示我们把入口文件打包的结果封装到变量Foo上面(以下例子的external全部是一样的,见上面的webpack.config.js文件) 346 | ```js 347 | output: { 348 | path:process.cwd()+'/dest/example1', 349 | filename: '[name].js', 350 | // export itself to a global var 351 | libraryTarget: "var", 352 | // name of the global var: "Foo" 353 | library: "Foo" 354 | } 355 | ``` 356 | 我们看看打包的结果: 357 | ```js 358 | var Foo = 359 | webpackJsonpFoo([0,1],[ 360 | /* 0 */ 361 | /***/ function(module, exports, __webpack_require__) { 362 | var jQuery = __webpack_require__(1); 363 | var math = __webpack_require__(2); 364 | function Foo() {} 365 | // ... 366 | module.exports = Foo; 367 | /***/ }, 368 | /* 1 */ 369 | /***/ function(module, exports) { 370 | module.exports = jQuery 371 | /***/ }, 372 | /* 2 */ 373 | /***/ function(module, exports) { 374 | 375 | console.log('main1'); 376 | 377 | /***/ } 378 | ]); 379 | ``` 380 | 从结果分析我们的目的,我们的入口文件的bunle被打包成为一个变量,变量名就是library指定的Foo。而且我们externals中指定的jQuery也被打包成为一个模块,但是这个模块是没有jQuery源码的,他的模块内容很简单,就是引用window.jQuery: 381 | ```js 382 | /* 1 */ 383 | /***/ function(module, exports) { 384 | module.exports = jQuery; 385 | /***/ }, 386 | ``` 387 | 关于externals vs libraryTarget vs library还有不懂的地方可以仔细阅读我写的[webpack中的externals vs libraryTarget vs library深入分析](https://github.com/liangklfangl/webpack-external-library)文章。 388 | 389 | #### 2.3 webpack的Loader 390 | 因为浏览器并非能够识别所有的文件类型(比如scss/less/typescript),因此在浏览器加载特定类型的资源之前我们需要对资源本身进行处理。webpack将所有的文件类型都当做一个模块,比如css,html,scss,jpg等,但是webpack本身只能识别javascript文件。因此,我们需要特定的loader将我们的文件转化为javascript模块,同时将这个模块添加到依赖图谱中。 391 | 392 | webpack中loader具有如下的作用: 393 |
394 | 1.识别特定的文件类型应该被那个loader处理
395 | 2.转化特定的文件以便将它添加到依赖图谱中,并最终添加到打包后的输出文件中
396 | 
397 | 我们下面给出一个例子: 398 | ```js 399 | const path = require('path'); 400 | const config = { 401 | entry: './path/to/my/entry/file.js', 402 | output: { 403 | path: path.resolve(__dirname, 'dist'), 404 | filename: 'my-first-webpack.bundle.js' 405 | }, 406 | module: { 407 | rules: [ 408 | { test: /\.txt$/, use: 'raw-loader' } 409 | ] 410 | } 411 | }; 412 | 413 | module.exports = config; 414 | ``` 415 | 上面这个例子相当于告诉webpack,当你遇到*require()/import*一个后缀名为*txt*的文件的时候就要使用raw-loader来处理,但是你必须保证在node_modules中已经安装了这个raw-loader,否则报错。而且,在文件路径嵌套很深的情况下,建议使用如下方式: 416 | ```js 417 | rules: [ 418 | { test: /\.txt$/, use: require.resolve('raw-loader') } 419 | ] 420 | ``` 421 | 否则可能即使安装了也找不到相应的loader,特别是当npm3出现以后。 422 | 423 | #### 2.4 webpack的Plugin 424 | 上面我们说的loader是在特定文件类型的基础上完成它的处理的,即它是针对于特定的文件类型,并把它转化为javascript中的模块。而我们的*plugin*用于在编译和打包过程中对所有的模块执行特定的操作。而且我们的webpack插件系统是非常强大的和可配置的。 425 | 426 | 为了使用插件,你只需要*require*它并把它添加到配置文件的*plugins*数组中。比如下面的例子: 427 | ```js 428 | plugins: [ 429 | //注意:这里使用了webpack-common-chunk,所以会产生两个文件,即common.js和index.js(名字自由设置) 430 | new CommonsChunkPlugin({ 431 | name: ["chunk",'common1','common2'], 432 | minChunks:2 433 | 434 | }) 435 | ] 436 | ``` 437 | 438 | ### 3.本章小结 439 | 该本章节中,我们通过一个CommonChunkPlugin的例子引入了打包后*模块图谱*的概念,并对CommonChunkPlugin的打包原理进行了分析。同时,我们介绍了webpack中四大核心概念,即入口文件,输出文件,Loader,Plugin等,并对输出文件中很多容易混淆的概念进行了深入的讲解。通过本章节,你对webpack本身应该会有一个初略的了解,后续我们会针对Loader,Plugin等核心内容进行进一步的剖析。 440 | -------------------------------------------------------------------------------- /03-webpack-dev-server核心概念.md: -------------------------------------------------------------------------------- 1 | ### 1.本章概述 2 | 在这个章节,主要会讲解一些webpack与webpack-dev-server结合的内容。包括webpack-dev-server中一些常见的容易混淆的知识点,以及webpack-dev-server一些原理性相关的东西。 3 | ### 2.webpack-dev-server核心概念 4 | #### 2.1 webpack的ContentBase vs publicPath vs output.path 5 | webpack-dev-server会使用当前的路径作为请求的资源路径(所谓`当前的路径`就是你运行webpack-dev-server这个命令的路径,如果你对webpack-dev-server进行了包装,比如[wcf](https://github.com/liangklfangl/wcf),那么当前路径指的就是运行wcf命令的路径,一般是项目的根路径),但是你可以通过指定content base来修改这个默认行为: 6 | ```js 7 | webpack-dev-server --content-base build/ 8 | ``` 9 | 这样webpack-dev-server就会使用`build目录`下的资源来处理静态资源的请求,比如`css/图片等`。content-base一般不要和publicPath,output.path混淆掉。其中content-base表示`静态资源`的路径是什么,比如下面的例子: 10 | ```html 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
这里要插入js内容
19 | 20 | 21 | ``` 22 | 在作为html-webpack-plugin的template以后,那么上面的`index.css`路径到底是什么?是相对于谁来说?上面我已经强调了:如果在没有指定content-base的情况下就是相对于`当前路径`来说的,所谓的当前路径就是在运行webpack-dev-server目录来说的,所以假如你在项目根路径运行了这个命令,那么你就要保证在项目根路径下存在该index.css资源,否则就会存在html-webpack-plugin的404报错。当然,为了解决这个问题,你可以将content-base修改为和html-webpack-plugin的html模板一样的目录。 23 | 24 | 上面讲到content-base只是和静态资源的请求有关,那么我们将其和`publicPath`和`output.path`做一个区分: 25 | 26 | 首先:假如你将output.path设置为`build`(这里的build和content-base的build没有任何关系,请不要混淆),你要知道webpack-dev-server实际上并没有将这些打包好的bundle写到这个目录下,而是存在于内存中的,但是我们可以`假设`(注意这里是假设)其是写到这个目录下的 27 | 28 | 然后:这些打包好的bundle在被请求的时候,其路径是相对于你配置的`publicPath`来说的,我的理解publicPath相当于虚拟路径,其映射于你指定的`output.path`。假如你指定的publicPath为 "/assets/",而且output.path为"build",那么相当于虚拟路径"/assets/"对应于"build"(前者和后者指向的是同一个位置),而如果build下有一个"index.css",那么通过虚拟路径访问就是`/assets/index.css`。 29 | 30 | 最后:如果某一个内存路径(文件写在内存中)已经存在特定的bundle,而且编译后内存中有新的资源,那么我们也会使用新的内存中的资源来处理该请求,而不是使用旧的bundle!比如我们有一个如下的配置: 31 | ```js 32 | module.exports = { 33 | entry: { 34 | app: ["./app/main.js"] 35 | }, 36 | output: { 37 | path: path.resolve(__dirname, "build"), 38 | publicPath: "/assets/", 39 | //此时相当于/assets/路径对应于build目录,是一个映射的关系 40 | filename: "bundle.js" 41 | } 42 | } 43 | ``` 44 | 45 | 那么我们要访问编译后的资源可以通过localhost:8080/assets/bundle.js来访问。如果我们在build目录下有一个html文件,那么我们可以使用下面的方式来访问js资源 46 | 47 | ```html 48 | 49 | 50 | 51 | 52 | Document 53 | 54 | 55 | 56 | 57 | 58 | ``` 59 | 60 | 此时你会看到控制台输出如下内容: 61 | 62 | ![](./images/contentbase.png) 63 | 64 | 主要关注下面两句输出: 65 |
 66 | Webpack result is served from /assets/
 67 | Content is served from /users/…./build
 68 | 
69 | 之所以是这样的输出结果是因为我们设置了contentBase为build,因为我们运行的命令为`webpack-dev-server --content-base build/`。所以,一般情况下:如果在html模板中不存在对外部相对资源的引用,我们并不需要指定content-base,但是如果存在对外部相对资源css/图片的引用,我们可以通过指定content-base来设置默认静态资源加载的路径,除非你所有的静态资源全部在`当前目录下`。 70 | 71 | #### 2.2 webpack-dev-server热加载(HMR) 72 | 为我们的webpack-dev-server开启HMR模式只需要在命令行中添加--hot,他会将HotModuleReplacementPlugin这个插件添加到webpack的配置中去,所以开启HotModuleReplacementPlugin最简单的方式就是使用inline模式。在inline模式下,你只需要在命令行中添加--inline --hot就可以自动实现。这时候webpack-dev-server就会自动添加webpack/hot/dev-server入口文件到你的配置中去,你只是需要访问下面的路径就可以了http://«host»:«port»/«path»。在控制台中你可以看到如下的内容: 73 | 74 | ![](./images/replace.png) 75 | 76 | 其中以[HMR]开头的部分来自于webpack/hot/dev-server模块,而`以[WDS]开头的部分来自于webpack-dev-server的客户端`。下面的部分来自于webpack-dev-server/client/index.js内容,其中的log都是以[WDS]开头的: 77 | 78 | ```js 79 | function reloadApp() { 80 | if(hot) { 81 | log("info", "[WDS] App hot update..."); 82 | window.postMessage("webpackHotUpdate" + currentHash, "*"); 83 | } else { 84 | log("info", "[WDS] App updated. Reloading..."); 85 | window.location.reload(); 86 | } 87 | } 88 | ``` 89 | 而在我们的webpack/hot/dev-server中的log都是以[HMR]开头的(他是来自于webpack本身的一个plugin): 90 | ```js 91 | if(!updatedModules) { 92 | console.warn("[HMR] Cannot find update. Need to do a full reload!"); 93 | console.warn("[HMR] (Probably because of restarting the webpack-dev-server)"); 94 | window.location.reload(); 95 | return; 96 | } 97 | ``` 98 | 那么我们如何在nodejs中使用HMR功能呢?此时需要修改三处配置文件: 99 |
100 | 第一:添加一个webpack的入口点,也就是webpack/hot/dev-server
101 | 第二:添加一个new webpack.HotModuleReplacementPlugin()到webpack的配置中
102 | 第三:添加hot:true到webpack-dev-server配置中,从而在服务端启动HMR(可以在cli中使用webpack-dev-server --hot)
103 | 
104 | 比如下面的代码就展示了webpack-dev-server为了实现HMR是如何处理我们的入口文件的。 105 | ```js 106 | if(options.inline) { 107 | var devClient = [require.resolve("../client/") + "?" + protocol + "://" + (options.public || (options.host + ":" + options.port))]; 108 | //将webpack-dev-server的客户端入口添加到的bundle中,从而达到自动刷新 109 | if(options.hot) 110 | devClient.push("webpack/hot/dev-server"); 111 | //这里是webpack-dev-server中对hot配置的处理 112 | [].concat(wpOpt).forEach(function(wpOpt) { 113 | if(typeof wpOpt.entry === "object" && !Array.isArray(wpOpt.entry)) { 114 | Object.keys(wpOpt.entry).forEach(function(key) { 115 | wpOpt.entry[key] = devClient.concat(wpOpt.entry[key]); 116 | }); 117 | } else { 118 | wpOpt.entry = devClient.concat(wpOpt.entry); 119 | } 120 | }); 121 | } 122 | ``` 123 | 满足上面三个条件的nodejs使用方式如下: 124 | ```js 125 | var config = require("./webpack.config.js"); 126 | config.entry.app.unshift("webpack-dev-server/client?http://localhost:8080/", "webpack/hot/dev-server"); 127 | //条件一(添加了webpack-dev-server的客户端和HMR的服务端) 128 | var compiler = webpack(config); 129 | var server = new webpackDevServer(compiler, { 130 | hot: true //条件二(--hot配置,webpack-dev-server会自动添加HotModuleReplacementPlugin),条件三 131 | ... 132 | }); 133 | server.listen(8080); 134 | ``` 135 | #### 2.3 webpack-dev-server启动proxy代理 136 | webpack-dev-server使用[http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware)去把请求代理到一个外部的服务器,配置的样例如下: 137 | ```js 138 | proxy: { 139 | '/api': { 140 | target: 'https://other-server.example.com', 141 | secure: false 142 | } 143 | } 144 | // In webpack.config.js 145 | { 146 | devServer: { 147 | proxy: { 148 | '/api': { 149 | target: 'https://other-server.example.com', 150 | secure: false 151 | } 152 | } 153 | } 154 | } 155 | // Multiple entry 156 | proxy: [ 157 | { 158 | context: ['/api-v1/**', '/api-v2/**'], 159 | target: 'https://other-server.example.com', 160 | secure: false 161 | } 162 | ] 163 | ``` 164 | 这种代理在很多情况下是很重要的,比如你可以把一些静态文件通过本地的服务器加载,而一些API请求全部通过一个远程的服务器来完成。还有一个情景就是在两个独立的服务器之间进行请求分割,如一个服务器负责授权而另外一个服务器负责应用本身。下面我给出日常开发中遇到的一个例子: 165 | 166 | (1)我有一个请求是通过相对路径来完成的,比如地址是"/msg/show.htm"。但是,在日常和生产环境下前面会加上不同的域名,比如日常是you.test.com而生产环境是you.inc.com。 167 | 168 | (2)那么比如我现在想在本地启动一个webpack-dev-server,然后通过webpack-dev-server来访问日常的服务器,而且日常的服务器地址是11.160.119.131,所以我就会通过如下的配置来完成: 169 | ```js 170 | devServer: { 171 | port: 8000, 172 | proxy: { 173 | "/msg/show.htm": { 174 | target: "http://11.160.119.131/", 175 | secure: false 176 | } 177 | } 178 | } 179 | ``` 180 | 此时当你请求"/msg/show.htm"的时候,其实请求的真是URL地址为"http"//11.160.119.131/msg/show.htm"。 181 | 182 | (3)在开发环境中遇到一个问题,那就是:如果我本地的devServer启动的地址为:"http://30.11.160.255:8000/" 或者常见的"http://0.0.0.0:8000/" ,那么真实的服务器会返回一个URL要求我登录,但是,将你的本地devServer启动到localhost上就不存在这个问题了(一个可能的原因在于localhost种上了后端需要的cookie,而其他的域名没有种上cookie,导致代理服务器访问日常服务器的时候没有相应的cookie,从而要求权限验证)。其中指定localhost的方式你可以通过[wcf](https://github.com/liangklfangl/wcf)来完成,因为wcf默认可以支持ip或者localhost方式来访问。当然你也可以通过添加下面的代码来完成: 183 | ```js 184 | devServer: { 185 | port: 8000, 186 | host:'localhost', 187 | proxy: { 188 | "/msg/show.htm": { 189 | target: "http://11.160.119.131/", 190 | secure: false 191 | } 192 | } 193 | } 194 | ``` 195 | 196 | (4)我想在此说一下webpack-dev-server的原理是什么?你可以通过查看这个[反向代理为何叫反向代理?](https://www.zhihu.com/question/24723688)这个文章来了解基础知识。其实正向代理和反向代理用一句话来概括就是:"正向代理隐藏了真实的客户端,而反向代理隐藏了真实的服务器"。而我们的webpack-dev-server其实扮演了一个代理服务器的角色,服务器之间通信不会存在前端常见的同源策略,这样当你请求我们的webpack-dev-server的时候,他会从真实的服务器中请求数据,然后将数据发送给你的浏览器。 197 |
198 |   (1)browser => localhost:8080(webpack-dev-server无代理) => http://you.test.com
199 |   (2)browser => localhost:8080(webpack-dev-server有代理) => http://you.test.com
200 | 
201 | 上面的第一种情况就是没有代理的情况,在我们的localhost:8080的页面通过前端策略去访问http://you.test.com 会存在同源策略,即第二步是通过前端策略去访问另外一个地址的。但是对于第二种情况,我们的第二步其实是通过代理去完成的,即服务器之间的通信,不存在同源策略问题。而我们变成了直接访问代理服务器,代理服务器返回一个页面,对于*页面中*某些满足特定条件前端请求(proxy,rewrite配置)全部由代理服务器来完成,这样同源问题就通过代理服务器的方式得到了解决。 202 | 203 | (5)上面讲述的是target是ip的情况,如果target要指定为域名的方式,可能需要绑定host。比如下面是我绑定的host: 204 | ```js 205 | 11.160.119.131 youku.min.com 206 | ``` 207 | 那么我下面的proxy配置就可以采用域名了: 208 | ```js 209 | devServer: { 210 | port: 8000, 211 | proxy: { 212 | "/msg/show.htm": { 213 | target: "http://youku.min.com/", 214 | secure: false 215 | } 216 | } 217 | } 218 | ``` 219 | 这和target绑定为IP地址的效果是完全一致的。总结一句话:"target指定了满足特定URL的请求应该对应到哪台主机上,即代理服务器应该访问的真实主机地址"。 220 | 221 | 其实proxy还可以通过配置一个*bypass*函数的返回值视情况绕开一个代理。这个函数可以查看http请求和响应以及一些代理的选项。它返回要么是false要么是一个URL的path,这个path将会用于处理请求而不是使用原来代理的方式完成。下面例子的配置将会忽略来自于浏览器的HTTP请求,他和historyApiFallback配置类似。浏览器请求可以像往常一样接收到HTML文件,但是API请求将会被代理到另外的服务器: 222 | ```js 223 | proxy: { 224 | '/some/path': { 225 | target: 'https://other-server.example.com', 226 | secure: false, 227 | bypass: function(req, res, proxyOptions) { 228 | if (req.headers.accept.indexOf('html') !== -1) { 229 | console.log('Skipping proxy for browser request.'); 230 | return '/index.html'; 231 | } 232 | } 233 | } 234 | } 235 | ``` 236 | 对于代理的请求也可以通过提供一个函数来重写,这个函数可以查看或者改变http请求。下面的例子就会重写HTTP请求,其主要作用就是移除URL前面的/api部分。 237 | ```js 238 | proxy: { 239 | '/api': { 240 | target: 'https://other-server.example.com', 241 | pathRewrite: {'^/api' : ''} 242 | } 243 | } 244 | ``` 245 | 其中pathRewrite配置来自于http-proxy-middleware。更多配置你可以查看[http-proxy-middleware官方文档](https://github.com/chimurai/http-proxy-middleware) 246 | #### 2.4 historyApiFallback选项 247 | 当你使用HTML5的history API的时候,当404出现的时候你可能希望使用index.html来作为请求的资源,这时候你可以使用这个配置:historyApiFallback:true。然而,如果你修改了output.publicPath,你就需要指定重定向的URL,你可以使用historyApiFallback.index选项。 248 | 249 | ```js 250 | // output.publicPath: '/foo-app/' 251 | historyApiFallback: { 252 | index: '/foo-app/' 253 | } 254 | ``` 255 | 256 | 使用rewrite选项你可以重新设置静态资源 257 | 258 | ```js 259 | historyApiFallback: { 260 | rewrites: [ 261 | // shows views/landing.html as the landing page 262 | { from: /^\/$/, to: '/views/landing.html' }, 263 | // shows views/subpage.html for all routes starting with /subpage 264 | { from: /^\/subpage/, to: '/views/subpage.html' }, 265 | // shows views/404.html on all other pages 266 | { from: /./, to: '/views/404.html' }, 267 | ], 268 | }, 269 | ``` 270 | 使用disableDotRule来满足一个需求,即如果一个资源请求包含一个`.`符号,那么表示是对某一个特定资源的请求,也就满足dotRule。我们看看[connect-history-api-fallback](https://github.com/liangklfang/connect-history-api-fallback)内部是如何处理的: 271 | ```js 272 | if (parsedUrl.pathname.indexOf('.') !== -1 && 273 | options.disableDotRule !== true) { 274 | logger( 275 | 'Not rewriting', 276 | req.method, 277 | req.url, 278 | 'because the path includes a dot (.) character.' 279 | ); 280 | return next(); 281 | } 282 | rewriteTarget = options.index || '/index.html'; 283 | logger('Rewriting', req.method, req.url, 'to', rewriteTarget); 284 | req.url = rewriteTarget; 285 | next(); 286 | }; 287 | ``` 288 | 也就是说,如果是对绝对资源的请求,也就是满足dotRule,但是disableDotRule(disable dot rule file request)为false,表示我们会自己对满足dotRule的资源进行处理,所以不用定向到index.html中!如果disableDotRule为true表示我们不会对满足dotRule的资源进行处理,所以直接定向到index.html! 289 | ```js 290 | history({ 291 | disableDotRule: true 292 | }) 293 | ``` 294 | 295 | #### 2.5 webpack-dev-server更多配置 296 | 更多配置的内容请查看下面的注释,我只会重点讲解filename和lazy配置: 297 | ```js 298 | var server = new WebpackDevServer(compiler, { 299 | contentBase: "/path/to/directory", 300 | //content-base配置 301 | hot: true, 302 | //开启HMR,由webpack-dev-server发送"webpackHotUpdate"消息到客户端代码 303 | historyApiFallback: false, 304 | //单页应用404转向index.html 305 | compress: true, 306 | //开启资源的gzip压缩 307 | proxy: { 308 | "**": "http://localhost:9090" 309 | }, 310 | //代理配置,来源于http-proxy-middleware 311 | setup: function(app) { 312 | //webpack-dev-server本身是Express服务器你可以添加自己的路由 313 | // app.get('/some/path', function(req, res) { 314 | // res.json({ custom: 'response' }); 315 | // }); 316 | }, 317 | //为Express服务器的express.static方法配置参数http://expressjs.com/en/4x/api.html#express.static 318 | staticOptions: { 319 | }, 320 | //在inline模式下用于控制在浏览器中打印的log级别,如`error`, `warning`, `info` or `none`. 321 | clientLogLevel: "info", 322 | //不在控制台打印任何log 323 | quiet: false, 324 | //不输出启动log 325 | noInfo: false, 326 | //webpack不监听文件的变化,每次请求来的时候重新编译 327 | lazy: true, 328 | //文件名称 329 | filename: "bundle.js", 330 | //webpack的watch配置,每隔多少秒检查文件的变化 331 | watchOptions: { 332 | aggregateTimeout: 300, 333 | poll: 1000 334 | }, 335 | //output.path的虚拟路径映射 336 | publicPath: "/assets/", 337 | //设置自定义http头 338 | headers: { "X-Custom-Header": "yes" }, 339 | //打包状态信息输出配置 340 | stats: { colors: true }, 341 | //配置https需要的证书等 342 | https: { 343 | cert: fs.readFileSync("path-to-cert-file.pem"), 344 | key: fs.readFileSync("path-to-key-file.pem"), 345 | cacert: fs.readFileSync("path-to-cacert-file.pem") 346 | } 347 | }); 348 | server.listen(8080, "localhost", function() {}); 349 | // server.close(); 350 | ``` 351 | 上面其他配置中,除了filename和lazy外都是容易理解的,那么我们下面继续分析下*lazy*和*filename*的具体使用场景。我们知道,在lazy阶段webpack-dev-server不是调用compiler.watch方法,而是等待请求到来的时候我们才会编译。源代码如下: 352 | ```js 353 | startWatch: function() { 354 | var options = context.options; 355 | var compiler = context.compiler; 356 | // start watching 357 | if(!options.lazy) { 358 | var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback); 359 | context.watching = watching; 360 | //context.watching得到原样返回的Watching对象 361 | } else { 362 | //如果是lazy,表示我们不是watching监听,而是请求的时候才编译 363 | context.state = true; 364 | } 365 | } 366 | ``` 367 | 调用rebuild的时候会判断context.state。每次重新编译后在compiler.done中会将context.state重置为true! 368 | ```js 369 | rebuild: function rebuild() { 370 | //如果没有通过compiler.done产生过Stats对象,那么我们设置forceRebuild为true 371 | //如果已经有Stats表明以前build过,那么我们调用run方法 372 | if(context.state) { 373 | context.state = false; 374 | //lazy状态下context.state为true,重新rebuild 375 | context.compiler.run(share.handleCompilerCallback); 376 | } else { 377 | context.forceRebuild = true; 378 | } 379 | }, 380 | ``` 381 | 下面是当请求到来的时候我们调用上面的rebuild继续重新编译: 382 | ```js 383 | handleRequest: function(filename, processRequest, req) { 384 | // in lazy mode, rebuild on bundle request 385 | if(context.options.lazy && (!context.options.filename || context.options.filename.test(filename))) 386 | share.rebuild(); 387 | //如果filename里面有hash,那么我们通过fs从内存中读取文件名,同时回调就是直接发送消息到客户端!!! 388 | if(HASH_REGEXP.test(filename)) { 389 | try { 390 | if(context.fs.statSync(filename).isFile()) { 391 | processRequest(); 392 | return; 393 | } 394 | } catch(e) { 395 | } 396 | } 397 | share.ready(processRequest, req); 398 | //回调函数将文件结果发送到客户端 399 | }, 400 | ``` 401 | 其中processRequest就是直接把编译好的资源发送到客户端: 402 | ```js 403 | function processRequest() { 404 | try { 405 | var stat = context.fs.statSync(filename); 406 | //获取文件名 407 | if(!stat.isFile()) { 408 | if(stat.isDirectory()) { 409 | filename = pathJoin(filename, context.options.index || "index.html"); 410 | //文件名 411 | stat = context.fs.statSync(filename); 412 | if(!stat.isFile()) throw "next"; 413 | } else { 414 | throw "next"; 415 | } 416 | } 417 | } catch(e) { 418 | return goNext(); 419 | } 420 | // server content 421 | // 直接访问的是文件那么读取,如果是文件夹那么要访问文件夹 422 | var content = context.fs.readFileSync(filename); 423 | content = shared.handleRangeHeaders(content, req, res); 424 | res.setHeader("Access-Control-Allow-Origin", "*"); 425 | // To support XHR, etc. 426 | res.setHeader("Content-Type", mime.lookup(filename) + "; charset=UTF-8"); 427 | res.setHeader("Content-Length", content.length); 428 | if(context.options.headers) { 429 | for(var name in context.options.headers) { 430 | res.setHeader(name, context.options.headers[name]); 431 | } 432 | } 433 | // Express automatically sets the statusCode to 200, but not all servers do (Koa). 434 | res.statusCode = res.statusCode || 200; 435 | if(res.send) res.send(content); 436 | else res.end(content); 437 | } 438 | } 439 | 440 | ``` 441 | 所以,在lazy模式下如果我们没有指定文件名filename,即每次请求的是那个webpack输出文件(chunk),那么我们每次都是会重新rebuild的!但是如果指定了文件名,那么只有访问该文件名的时候才会rebuild! 442 | 443 | ### 3.本章小结 444 | 该本章节中,我们深入讲解了webpack-dev-server中一些核心的概念,如contentBase,publicPath,proxy代理,historyApiFallback,lazyLoad等。通过这个章节,你对于webpack-dev-server应该有了一个深入的认识,即webpack-dev-server本身只是一个Express服务器,只是他将资源请求与webpack的打包过程联系起来。 445 | -------------------------------------------------------------------------------- /11-webpack2的tree-shaking深入分析.md: -------------------------------------------------------------------------------- 1 | #### 1.本章概述 2 | 在本章节,我们通过一个引入 ladash 特定模块的实例来展示了 tree-shaking 在 Webpack 中的重要作用。通过合理的使用 tree-shaking 功能可以有效的减少打包后文件的大小,通过本实例我们也可以知道 tree-shaking 的作用条件和范围。这样对于 webpack 优化策略你又掌握了一部分核心知识。 3 | 4 | #### 2.我们是否需要引入tree-shaking? 5 | 下面是ladash中对外导出的对象: 6 | ```js 7 | lodash.isFunction = isFunction; 8 | lodash.isInteger = isInteger; 9 | lodash.isLength = isLength; 10 | lodash.isMap = isMap; 11 | lodash.isMatch = isMatch; 12 | lodash.isMatchWith = isMatchWith; 13 | lodash.isNaN = isNaN; 14 | lodash.isNative = isNative; 15 | lodash.isNil = isNil; 16 | lodash.isNull = isNull; 17 | lodash.isNumber = isNumber; 18 | lodash.isObject = isObject; 19 | lodash.isObjectLike = isObjectLike; 20 | lodash.isPlainObject = isPlainObject; 21 | lodash.isRegExp = isRegExp; 22 | lodash.isSafeInteger = isSafeInteger; 23 | lodash.isSet = isSet; 24 | lodash.isString = isString; 25 | lodash.isSymbol = isSymbol; 26 | lodash.isTypedArray = isTypedArray; 27 | lodash.isUndefined = isUndefined; 28 | lodash.isWeakMap = isWeakMap; 29 | lodash.isWeakSet = isWeakSet; 30 | ``` 31 | 这是为什么我们可以通过如下方式引入方法的原因: 32 | ```js 33 | import { concat, sortBy, map, sample } from 'lodash'; 34 | //lodash其实是一个对象 35 | ``` 36 | 但是还有一种常见的方法就是只引入我们需要的函数,如下: 37 | ```js 38 | import sortBy from 'lodash/sortBy'; 39 | import map from 'lodash/map'; 40 | import sample from 'lodash/sample'; 41 | ``` 42 | 之所以可以通过这种方法引用是因为在lodash的npm包中,我们每一个方法都对应于一个独立的文件,并导出了该方法,如下面就是sortBy.js方法的源码: 43 | ```js 44 | var sortBy = baseRest(function(collection, iteratees) { 45 | if (collection == null) { 46 | return []; 47 | } 48 | var length = iteratees.length; 49 | if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) { 50 | iteratees = []; 51 | } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) { 52 | iteratees = [iteratees[0]]; 53 | } 54 | return baseOrderBy(collection, baseFlatten(iteratees, 1), []); 55 | }); 56 | module.exports = sortBy; 57 | ``` 58 | 注意一点就是:通过后者来导入我们需要的文件比前者全部导入的文件要小的多。上面我已经说了原因,即后者将每一个方法都存放在一个独立的文件中,从而可以按需导入,所以文件也就比较小了。具体你可以[查看这里](https://lacke.mn/reduce-your-bundle-js-file-size/)来学习如何减少bundle.js的大小。当然,如果你使用了webpack3的tree-shaking,那么就不需要考虑这个情况了。tree-shaking会让没用的代码在打包的时候直接被剔除。但是,请注意,tree-shaking 的功能要生效必须满足一定的条件,即必须是 ES6 模块。 59 | 60 | 61 | #### 3.webpack引入tree-shaking功能 62 | ##### 3.1 webpack如何使用tree-shaking 63 | 为了让 webpack2 支持tree-shaking功能,我们需要对[wcf的 babel 配置进行修改](https://github.com/liangklfangl/wcf/blob/master/src/getBabelDefaultConfig.js),其中修改最重要的一点就是去掉babel-preset-es2015 ,而采用 plugin 处理。在 plugin 处理的时候还需要去掉下面的插件: 64 | ```js 65 | require.resolve("babel-plugin-transform-es2015-modules-amd"), 66 | //转化为amd格式,define类型 67 | require.resolve("babel-plugin-transform-es2015-modules-commonjs"), 68 | //转化为commonjs规范,得到:exports.default = 42,export.name="罄天" 69 | require.resolve("babel-plugin-transform-es2015-modules-umd"), 70 | //umd规范 71 | ``` 72 | 采用 babel-plugin-transform-es2015-modules-commonjs 以后,我们的代码: 73 | ```js 74 | //imported.js 75 | export function foo() { 76 | return 'foo'; 77 | } 78 | export function bar() { 79 | return 'bar'; 80 | } 81 | //下面是index.js 82 | import {foo} from './imported'; 83 | let elem = document.getElementById('output'); 84 | elem.innerHTML = `Output: ${foo()}`; 85 | ``` 86 | 会被webpack转化为如下的形式: 87 | ```js 88 | Object.defineProperty(exports, "__esModule", { 89 | value: true 90 | }); 91 | exports.foo = foo; 92 | exports.bar = bar; 93 | //都转化为commonjs规范了 94 | function foo() { 95 | return 'foo'; 96 | } 97 | function bar() { 98 | return 'bar'; 99 | } 100 | /***/ }), 101 | /* 1 */ 102 | /***/ (function(module, exports, __webpack_require__) { 103 | 104 | "use strict"; 105 | var _imported = __webpack_require__(0); 106 | 107 | var elem = document.getElementById('output'); 108 | elem.innerHTML = 'Output: ' + (0, _imported.foo)(); 109 | /***/ }) 110 | /******/ ]); 111 | ``` 112 | 所以,我们没有用到的bar方法也被引入了。而如果引入babel-plugin-transform-es2015-modules-amd,我们的打包代码就会得到如下的内容: 113 | ```js 114 | /******/ ([ 115 | /* 0 */ 116 | /***/ (function(module, exports, __webpack_require__) { 117 | var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [exports], __WEBPACK_AMD_DEFINE_RESULT__ = function (exports) { 118 | 'use strict'; 119 | Object.defineProperty(exports, "__esModule", { 120 | value: true 121 | }); 122 | exports.foo = foo; 123 | exports.bar = bar; 124 | //我们的没有用到的bar方法也被导出了 125 | function foo() { 126 | return 'foo'; 127 | } 128 | function bar() { 129 | return 'bar'; 130 | } 131 | }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), 132 | __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); 133 | /***/ }), 134 | /* 1 */ 135 | /***/ (function(module, exports, __webpack_require__) { 136 | 137 | var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(0)], __WEBPACK_AMD_DEFINE_RESULT__ = function (_imported) { 138 | 'use strict'; 139 | 140 | var elem = document.getElementById('output'); 141 | elem.innerHTML = 'Output: ' + (0, _imported.foo)(); 142 | }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), 143 | __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); 144 | /***/ }) 145 | /******/ ]); 146 | ``` 147 | 而如果引入 babel-plugin-transform-es2015-modules-umd 也会面临同样的问题,所以我们应该去掉上面三个插件,即不再使用 `amd/cmd/umd` 规范打包,而使用我们的 ES6 原生模块打包策略。让 ES6 模块不受 Babel 预设(preset)的影响。Webpack 认识 ES6 模块,只有当保留 ES6 模块语法时才能够应用 tree-shaking。如果将其转换为 CommonJS 语法,Webpack 不知道哪些代码是使用过的,哪些不是(就不能应用 tree-shaking 了)。最后,Webpack将把它们转换为 CommonJS语法。最终得到的 babel 默认配置就是如下的内容: 148 | ```js 149 | function getDefaultBabelConfig() { 150 | return { 151 | cacheDirectory: tmpdir(), 152 | //We must set! 153 | presets: [ 154 | require.resolve('babel-preset-react'), 155 | // require.resolve('babel-preset-es2015'), 156 | //(1)这个必须去掉 157 | require.resolve('babel-preset-stage-0'), 158 | ], 159 | plugins: [ 160 | require.resolve("babel-plugin-transform-es2015-template-literals"), 161 | require.resolve("babel-plugin-transform-es2015-literals"), 162 | require.resolve("babel-plugin-transform-es2015-function-name"), 163 | require.resolve("babel-plugin-transform-es2015-arrow-functions"), 164 | require.resolve("babel-plugin-transform-es2015-block-scoped-functions"), 165 | require.resolve("babel-plugin-transform-es2015-classes"), 166 | //这里会转化class 167 | require.resolve("babel-plugin-transform-es2015-object-super"), 168 | require.resolve("babel-plugin-transform-es2015-shorthand-properties"), 169 | require.resolve("babel-plugin-transform-es2015-computed-properties"), 170 | require.resolve("babel-plugin-transform-es2015-for-of"), 171 | require.resolve("babel-plugin-transform-es2015-sticky-regex"), 172 | require.resolve("babel-plugin-transform-es2015-unicode-regex"), 173 | require.resolve("babel-plugin-syntax-object-rest-spread"), 174 | require.resolve("babel-plugin-transform-es2015-parameters"), 175 | require.resolve("babel-plugin-transform-es2015-destructuring"), 176 | require.resolve("babel-plugin-transform-es2015-block-scoping"), 177 | require.resolve("babel-plugin-transform-es2015-typeof-symbol"), 178 | [ 179 | require.resolve("babel-plugin-transform-regenerator"), 180 | { async: false, asyncGenerators: false } 181 | ], 182 | // require.resolve("babel-plugin-add-module-exports"), 183 | // 交给webpack2处理,可以删除 184 | require.resolve("babel-plugin-check-es2015-constants"), 185 | require.resolve("babel-plugin-syntax-async-functions"), 186 | require.resolve("babel-plugin-syntax-async-generators"), 187 | require.resolve("babel-plugin-syntax-class-constructor-call"), 188 | require.resolve("babel-plugin-syntax-class-properties"), 189 | require.resolve("babel-plugin-syntax-decorators"), 190 | require.resolve("babel-plugin-syntax-do-expressions"), 191 | require.resolve("babel-plugin-syntax-dynamic-import"), 192 | require.resolve("babel-plugin-syntax-exponentiation-operator"), 193 | require.resolve("babel-plugin-syntax-export-extensions"), 194 | require.resolve("babel-plugin-syntax-flow"), 195 | require.resolve("babel-plugin-syntax-function-bind"), 196 | require.resolve("babel-plugin-syntax-jsx"), 197 | require.resolve("babel-plugin-syntax-trailing-function-commas"), 198 | require.resolve("babel-plugin-transform-async-generator-functions"), 199 | require.resolve("babel-plugin-transform-async-to-generator"), 200 | require.resolve("babel-plugin-transform-class-constructor-call"), 201 | require.resolve("babel-plugin-transform-class-properties"), 202 | require.resolve("babel-plugin-transform-decorators"), 203 | require.resolve("babel-plugin-transform-decorators-legacy"), 204 | require.resolve("babel-plugin-transform-do-expressions"), 205 | require.resolve("babel-plugin-transform-es2015-duplicate-keys"), 206 | require.resolve("babel-plugin-transform-es2015-spread"), 207 | require.resolve("babel-plugin-transform-exponentiation-operator"), 208 | require.resolve("babel-plugin-transform-export-extensions"), 209 | // require.resolve("babel-plugin-transform-es2015-modules-amd"), 210 | // require.resolve("babel-plugin-transform-es2015-modules-commonjs"), 211 | // require.resolve("babel-plugin-transform-es2015-modules-umd"), 212 | // (2)去掉这个 213 | require.resolve("babel-plugin-transform-flow-strip-types"), 214 | require.resolve("babel-plugin-transform-function-bind"), 215 | require.resolve("babel-plugin-transform-object-assign"), 216 | require.resolve("babel-plugin-transform-object-rest-spread"), 217 | require.resolve("babel-plugin-transform-proto-to-assign"), 218 | require.resolve("babel-plugin-transform-react-display-name"), 219 | require.resolve("babel-plugin-transform-react-jsx"), 220 | require.resolve("babel-plugin-transform-react-jsx-source"), 221 | require.resolve("babel-plugin-transform-runtime"), 222 | require.resolve("babel-plugin-transform-strict-mode"), 223 | ] 224 | }; 225 | } 226 | ``` 227 | 具体文件内容你可以点击[wcf](https://github.com/liangklfangl/wcf/blob/master/src/getBabelDefaultConfig.js)打包 babel 配置。当然你也可以使用下面方式告诉 babel 预设不转换模块: 228 | ```js 229 | { 230 | "presets": [ 231 | ["env", { 232 | "loose": true, 233 | "modules": false 234 | }] 235 | ] 236 | } 237 | ``` 238 | 这种方式要简单的多。但是,正如[如何在 Webpack 2 中使用 tree-shaking](https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651226843&idx=1&sn=8ce859bb0ccaa2351c5f8231cc016052&chksm=bd495b5f8a3ed249bb2d967e27f5e0ac20b42f42698fdfd0d671012782ce0074a21129e5f224&mpshare=1&scene=1&srcid=08241S5UYwTpLwk1N2s51tXG&key=adf9313632dd72f547280f783810492f9adb79ab0d4163835d8f16b9ef1ba0b666c3253ebf73fcbd10842f39091c3775a8bcb7ebf2f1613b0baadc517bd3a3f871c02aa3495fa42b3e960fd7f99357e0&ascene=0&uin=MTkwNTY4NjMxOQ%3D%3D&devicetype=iMac+MacBookAir7%2C2+OSX+OSX+10.12+build(16A323)&version=12020810&nettype=WIFI&fontScale=100&pass_ticket=Kwkar2P9YwiWaPYmrcaqYmEqAigrP8I305SDCp6p05cCbna5znl6Uz%2FMx75BskRL)文章本身所说,这种方式会存在副作用,即无法移除多余的类声明。在使用 ES6 语法定义类时,类的成员函数会被添加到属性 prototype ,没有什么方法能完全避免这次赋值,所以 webpack 会`认为我们添加到prototype 上方法的操作也是对类的一种使用,导致无法移除多余的类声明`,编译过程阻止了对类进行 tree-shaking ,它仅对函数起作用。UglifyJS 不能够分辨它仅仅是类声明,还是其它有副作用的操作,因为UglifyJS 不能做控制流分析。 239 | 240 | ##### 3.2 webpack的tree-shaking标记 vs rollup标记区别 241 |
242 | 移除未使用代码(Dead code elimination) vs 包含已使用代码(live code inclusion)
243 | 
244 | Webpack 仅仅标记未使用的代码而不移除,并且不将其导出到模块外。它拉取所有用到的代码,将剩余的(未使用的)代码留给像 UglifyJS 这类压缩代码的工具来移除。UglifyJS 读取打包结果,在压缩之前移除未使用的代码。而 Rollup 不同,它的打包结果只包含运行应用程序所必需的代码。打包完成后的输出并没有未使用的类和函数,压缩仅涉及实际使用的代码。 245 | 246 | ##### 3.3 基于babel-minify-webpack-plugin(即babili-webpack-plugin)移除多余的类声明 247 | [babel-minify-webpack-plugin](https://github.com/webpack-contrib/babel-minify-webpack-plugin)能将 ES6 代码编译为 ES5,移除未使用的类和函数,这就像 UglifyJS 已经支持 ES6 一样。babel-minify 会在编译前`删除未使用的代码`。在编译为 ES5 之前,很容易找到未使用的类,因此tree-shaking 也可以用于类声明,而不再仅仅是函数。如果你去看 babili-webpack-plugin 的代码,你会看到下面两句: 248 | ```js 249 | import { transform } from 'babel-core'; 250 | import babelPresetMinify from 'babel-preset-minify'; 251 | ``` 252 | 首先是就是[babel-preset-minify](https://github.com/babel/minify/blob/master/packages/babel-preset-minify/src/index.js),你可以看到他内部会调用如 babel-plugin-minify-dead-code-elimination , babel-plugin-minify-type-constructors 等来判断哪些代码没有被引用,进而可以在代码没有被编译为 ES5 之前把它移除掉。而 babel-core 就是负责把处理后的 ES6 代码继续编译为 ES5 代码。 253 | 254 | 所以,我们只需用 babel-minify-webpack-plugin 替换 UglifyJS ,然后删除 babel-loader (该 plugin 自己会处理 ES6 代码,但是 jsx 处理需要自己添加 preset ) 即可。另一种方式是将[babel-preset-minify](https://github.com/babel/minify/tree/master/packages/babel-preset-minify)作为 Babel 的预设,仅使用 babel-loader(移除 UglifyJS 插件,因为 babel-preset-minify 已经压缩完成)。推荐使用第一种(插件的方式),因为当编译器不是 Babel(比如 Typescript)时,它也能生效。 255 | ```js 256 | module: { 257 | rules: [] 258 | }, 259 | plugins: [ 260 | new BabiliPlugin() 261 | //替代uglifyjs,它可以移除es6的多余类声明 262 | ] 263 | ``` 264 | 我们需要将 ES6+ 代码传给 babel-minify ,否则它不会移除(未使用的)类。所以,这种方式就要求所有的第三方包都必须有es6的代码发布,否则无法移除。 265 | 266 | ##### 3.4 目前[wcf](https://github.com/liangklfangl/wcf)没有引入babili-webpack-plugin 267 | 这种情况下我们依然会对类的代码打包成为ES5,然后交给我们的uglifyjs处理,比如下面的例子: 268 | ```js 269 | //imported.js 270 | export function foo() { 271 | return 'foo'; 272 | } 273 | export function bar() { 274 | return 'bar'; 275 | } 276 | export function ql(){ 277 | return 'ql' 278 | } 279 | export class Test{ 280 | toString(){ 281 | return 'test'; 282 | } 283 | } 284 | export class Test1{ 285 | toString(){ 286 | return 'test1'; 287 | } 288 | } 289 | //index.js 290 | import {foo} from './imported'; 291 | let elem = document.getElementById('app'); 292 | elem.innerHTML = `Output: ${foo()}`; 293 | ``` 294 | 打包后的结果如下: 295 | ```js 296 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 297 | "use strict"; 298 | /* harmony export (immutable) */ 299 | __webpack_exports__["a"] = foo; 300 | /* unused harmony export bar */ 301 | /* unused harmony export ql */ 302 | /* unused harmony export Test */ 303 | /* unused harmony export Test1 */ 304 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck__ = __webpack_require__(8); 305 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck__); 306 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass__ = __webpack_require__(9); 307 | /* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass__); 308 | function foo() { 309 | return 'foo'; 310 | } 311 | function bar() { 312 | return 'bar'; 313 | } 314 | function ql() { 315 | return 'ql'; 316 | } 317 | var Test = function () { 318 | function Test() { 319 | __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck___default()(this, Test); 320 | } 321 | __WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass___default()(Test, [{ 322 | key: 'toString', 323 | value: function toString() { 324 | return 'test'; 325 | } 326 | }]); 327 | 328 | return Test; 329 | }(); 330 | var Test1 = function () { 331 | function Test1() { 332 | __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck___default()(this, Test1); 333 | } 334 | 335 | __WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass___default()(Test1, [{ 336 | key: 'toString', 337 | value: function toString() { 338 | return 'test1'; 339 | } 340 | }]); 341 | return Test1; 342 | }(); 343 | }) 344 | ``` 345 | 此时通过查看 `harmony export` 部分,我们知道 webpack 导出的仅仅是我们用到的 foo 模块而已,而其他的不管是多余的函数声明还是多余的类声明都是被标记为无用代码(`'unused'`)。我以为,通过这种方式打包,经过uglifyjs处理就会将类 Test1,Test2 的代码移除,其实事实并不是这样,经过 uglifyjs 处理后多余的函数是没有了,但是多余的类声明打包成的函数代码依然存在!依然存在!依然存在! 346 | 347 | 终极解决方法:[使用babel-minify-webpack-plugin](https://github.com/webpack-contrib/babel-minify-webpack-plugin),即babili-webpack-plugin。完整实例代码可以[参考这里](https://github.com/blacksonic/babel-webpack-tree-shaking),而目前[wcf](https://github.com/liangklfangl/wcf)没有采用这种策略,所以多余的 class 是无法去除的。目前,我觉得这种策略是可以接受的,因为我们第三方发布的包很少是使用 class 发布的,而都是编译为 ES5 代码后发布的,所以通过 uglifyjs 这种策略已经足够了。 348 | 349 | 当然,你也可以使用[babel-preset-minify](https://github.com/babel/minify/tree/master/packages/babel-preset-minify)来将代码压缩作为你的预设,我觉的这种方式在独立封装自己的打包工具的时候比较有用,他是所有 babel 代码压缩插件的集合。 350 | 351 | ### 4.tree-shaking的局限性 352 | 这一部分都是自己的理解,但是是基于这样一个事实: 353 | ```js 354 | import {sortBy} from "lodash"; 355 | ``` 356 | 我通过 import 引入 sortBy 方法以后,以为仅仅是引入了该方法而已,但是实际上把 concat 等函数都引入了。因为 import 是基于 ES6 的静态语法分析,而我们的 lodash 第三方包导出的时候并不是基于 ES6 的 import/export 机制,代码如下: 357 | ```js 358 | var _ = runInContext(); 359 | if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { 360 | root._ = _; 361 | define(function() { 362 | return _; 363 | }); 364 | } 365 | else if (freeModule) { 366 | // Export for Node.js. 367 | (freeModule.exports = _)._ = _; 368 | // Export for CommonJS support. 369 | freeExports._ = _; 370 | } 371 | else { 372 | // Export to the global object. 373 | root._ = _; 374 | } 375 | }.call(this)); 376 | ``` 377 | 所以,实际上 tree-shaking 是无法完成的!所以,上面说的 babel-minify-webpack-plugin 其实在这里根本不能起作用,因为他的第一步就是去掉 ES6 中没有用到的代码然后打包成为 ES5 ,但是这里的代码压根就不是 ES6,所以它也就没有魔力了。对第三方包来说也是,应当使用 ES6 模块。幸运的是,越来越多的包作者同时发布 CommonJS 格式和ES6格式的模块。ES6 模块的入口由 package.json的字段 module 指定。 378 | 379 | 对 ES6 模块,未使用的函数会被移除,但 class 并不一定会。只有当包内的 class 定义也为 ES6 格式时,babili-webpack-plugin才能将其移除。很少有包能够以这种格式发布,但有的做到了(比如说 lodash 的 lodash-es)(本段文字来自于[如何在 Webpack 2 中使用 tree-shaking](https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651226843&idx=1&sn=8ce859bb0ccaa2351c5f8231cc016052&chksm=bd495b5f8a3ed249bb2d967e27f5e0ac20b42f42698fdfd0d671012782ce0074a21129e5f224&mpshare=1&scene=1&srcid=08241S5UYwTpLwk1N2s51tXG&key=adf9313632dd72f547280f783810492f9adb79ab0d4163835d8f16b9ef1ba0b666c3253ebf73fcbd10842f39091c3775a8bcb7ebf2f1613b0baadc517bd3a3f871c02aa3495fa42b3e960fd7f99357e0&ascene=0&uin=MTkwNTY4NjMxOQ%3D%3D&devicetype=iMac+MacBookAir7%2C2+OSX+OSX+10.12+build(16A323)&version=12020810&nettype=WIFI&fontScale=100&pass_ticket=Kwkar2P9YwiWaPYmrcaqYmEqAigrP8I305SDCp6p05cCbna5znl6Uz%2FMx75BskRL)),你可以可以参考[如何评价 Webpack 2 新引入的 Tree-shaking 代码优化技术?](https://www.zhihu.com/question/41922432)尤雨溪的回答: 380 |

381 | ES6的模块设计虽然使得灵活性不如 CommonJS 的 require ,但却保证了 ES6 modules 的依赖关系是确定 (deterministic) 的,和运行时的状态无关,从而也就保证了 ES6 modules 是可以进行可靠的静态分析的。对于主要在服务端运行的 Node 来说,所有的代码都在本地,按需动态 require 即可,但对于要下发到客户端的 web 代码而言,要做到高效的按需使用,不能等到代码执行了才知道模块的依赖,必须要从模块的静态分析入手。这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS。 382 |

383 | 384 | 所以,我们在引入一个lodash模块的时候应该使用下面的模式: 385 | ```js 386 | import sortBy from 'lodash/sortBy'; 387 | ``` 388 | 389 | ### 5.本章小结 390 | 在本章节,我们通过 lodash 的一个简单实例说明了 webpack 引入 tree-shaking 的重要意义。同时,我们也通过例子说明了如何在 webpack2 中引入 tree-shaking,以及要实现 tree-shaking需要满足的条件。通过本章节的学习,你不仅能够了解如何实现 tree-shaking,也将知道如何优化自己的代码,以及如何为未来 npm 包的优化做出选择。 391 | -------------------------------------------------------------------------------- /02-webpack基本使用.md: -------------------------------------------------------------------------------- 1 | #### 1.本章概述 2 | 在本章节,我们将简要的对webpack的基本使用做一个演示。通过本章节的学习,你应该能在你的项目中快速配置一个webpack打包文件并完成项目的打包工作。在本章节,我们不牵涉到webpack中那些深入的知识点,后续我会针对这些知识点做更加细致的讲解。但是,正如在上一个章节看到的那样,我很早就在webpack配置中引入了CommonChunkPlugin,因此在本章节,我将在webpack配置文件中继续使用这个插件。希望你能对该插件引起足够的重视,并学会如何在自己的项目中使用它。 3 | 4 | 其中CommonChunkPlugin是webpack用于创建一个独立的文件,即所谓的common chunk。这个chunk会包含多个入口文件中共同的模块。通过将多个入口文件公共的模块抽取出来可以在特定的时间进行缓存,这对于提升页面加载速度是很好的优化手段。 5 | #### 2.webpack打包例子讲解 6 | ##### 2.1 CommonChunkPlugin参数详解 7 | 开始具体的例子之前我们看下这个插件支持的配置和详细含义。同时,我们也给出官网描述的几个例子: 8 | ```js 9 | { 10 | name: string, // or 11 | names: string[], 12 | // The chunk name of the commons chunk. An existing chunk can be selected by passing a name of an existing chunk. 13 | // If an array of strings is passed this is equal to invoking the plugin multiple times for each chunk name. 14 | // If omitted and `options.async` or `options.children` is set all chunks are used, otherwise `options.filename` 15 | // is used as chunk name. 16 | // When using `options.async` to create common chunks from other async chunks you must specify an entry-point 17 | // chunk name here instead of omitting the `option.name`. 18 | filename: string, 19 | //指定该插件产生的文件名称,可以支持output.filename中那些支持的占位符,比如[hash],[chunkhash],[id]等。如果忽略这个这个属性,那么原始的文件名称不会被修改(一般是output.filename或者output.chunkFilename,你可以查看compiler和compilation部分第一个例子)。但是这个配置不允许和`options.async`一起使用 20 | minChunks: number|Infinity|function(module, count) boolean, 21 | //至少有minChunks的chunk都包含指定的模块,那么该模块就会被移出到common chunk中。这个数值必须大于等于2,并且小于等于没有使用这个插件应该产生的chunk数量。如果你传入`Infinity`,那么只会产生common chunk,但是不会有任何模块被移到这个chunk中(没有一个模块会被依赖无限次)。通过提供一个函数,你也可以添加自己的逻辑,这个函数会被传入一个参数表示产生的chunk数量 22 | chunks: string[], 23 | // Select the source chunks by chunk names. The chunk must be a child of the commons chunk. 24 | // If omitted all entry chunks are selected. 25 | children: boolean, 26 | // If `true` all children of the commons chunk are selected 27 | deepChildren: boolean, 28 | // If `true` all descendants of the commons chunk are selected 29 | 30 | async: boolean|string, 31 | // If `true` a new async commons chunk is created as child of `options.name` and sibling of `options.chunks`. 32 | // It is loaded in parallel with `options.chunks`. 33 | // Instead of using `option.filename`, it is possible to change the name of the output file by providing 34 | // the desired string here instead of `true`. 35 | minSize: number, 36 | //所有被移出到common chunk的文件的大小必须大于等于这个值 37 | } 38 | ``` 39 | 上面的filename和minChunks已经在注释中说明了,下面我们重点说一下其他的属性。 40 | 41 | - children属性 42 | 43 | 其中在webpack中很多chunk产生都是通过require.ensure来完成的。我们看看下面的例子: 44 | ```js 45 | //main.js为入口文件 46 | if (document.querySelectorAll('a').length) { 47 | require.ensure([], () => { 48 | const Button = require('./Components/Button').default; 49 | const button = new Button('google.com'); 50 | button.render('a'); 51 | }); 52 | } 53 | if (document.querySelectorAll('h1').length) { 54 | require.ensure([], () => { 55 | const Header = require('./Components/Header').default; 56 | new Header().render('h1'); 57 | }); 58 | } 59 | ``` 60 | 此时会产生三个chunk,分别为main和其他两个通过require.ensure产生的chunk,比如0.entry.chunk.js和1.entry.chunk.js。如果我们配置了多个入口文件(假如还有一个main1.js),那么这些动态产生的chunk中可能也会存在相同的模块(此时main1,main会产生四个动态chunk)。而这个children配置就是为了这种情况而产生的。通过配置children,我们可以将动态产生的这些chunk的公共的模块也抽取出来。但是,很显然,以前是动态加载的文件现在都必须在页面初始的时候就加载完成,那么对于初始加载肯在时间上有一定的副作用。但是存在一种情况,比如进入主页面后,我们需要加载路由A,路由B....等一系列的文件(网站的核心模块都要提前一次性加载),那么我们把路由A,路由B...这些公共的模块提取到公有模块中,然后和入口文件一起加载,在性能上还是有优势的。下面是官网提供的一个例子: 61 | ```js 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | // names: ["app", "subPageA"] 64 | // (choose the chunks, or omit for all chunks) 65 | children: true, 66 | // (select all children of chosen chunks) 67 | // minChunks: 3, 68 | // (3 children must share the module before it's moved) 69 | }) 70 | ``` 71 | 对于common-chunk-plugin不太明白的,可以[查看这里](https://github.com/liangklfangl/commonsChunkPlugin_Config#将公共业务模块与类库或框架分开打包)。 72 | 73 | - async 74 | 75 | 上面这种children的方案会增加初始加载的时间,这种async的方式相当于创建了一个异步加载的common-chunk,其包含我们require.ensure动态产生的chunk中的公共模块。这样,当你访问特定路由的时候,我们会动态的加载这个common chunk,以及你特定路由包含的业务代码。下面也是官网给出的一个实例: 76 | ```js 77 | new webpack.optimize.CommonsChunkPlugin({ 78 | name: "app", 79 | // or 80 | names: ["app", "subPageA"] 81 | // the name or list of names must match the name or names 82 | // of the entry points that create the async chunks 83 | children: true, 84 | // (use all children of the chunk) 85 | async: true, 86 | // (create an async commons chunk) 87 | minChunks: 3, 88 | // (3 children must share the module before it's separated) 89 | }) 90 | ``` 91 | 92 | - names 93 | 94 | 该参数用于指定common chunk的名称。如果指定的chunk名称在entry中有配置,那么表示选择特定的chunk。如果指定的是一个数组,那么相当于按照名称的顺序多次执行common-chunk-plugin插件。如果没有指定name属性,但是指定了`options.async` 或者`options.children`,那么表示抽取所有的chunk的公共模块,包括通过require.ensure动态产生的模块。其他情况下我们使用`options.filename`来作为chunk的名称。注意:如果你指定了`options.async`来创建一个异步加载的common chunk,那么你必须指定一个入口chunk名称,而不能忽略option.name参数。你可以[点击这个例子](https://github.com/liangklfangl/commonsChunkPlugin_Config#将公共业务模块与类库或框架分开打包)查看。 95 | 96 | - chunks 97 | 98 | 通过chunks参数来选择来源的chunk。这些chunk必须是common-chunk的子级chunk。如果没有指定,那么默认选中所有的入口chunk。下面给出一个例子: 99 | ```js 100 | var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); 101 | module.exports = { 102 | entry: { 103 | main: process.cwd()+'/example6/main.js', 104 | main1: process.cwd()+'/example6/main1.js', 105 | jquery:["jquery"] 106 | }, 107 | output: { 108 | path: process.cwd() + '/dest/example6', 109 | filename: '[name].js' 110 | }, 111 | plugins: [ 112 | new CommonsChunkPlugin({ 113 | name: "jquery", 114 | minChunks:2, 115 | chunks:["main","main1"] 116 | }) 117 | ] 118 | }; 119 | ``` 120 | 此时你会发现在我们的jquery.js的最后会打包进来我们的chunk1.js和chunk2.js 121 | ```js 122 | /* 2 */ 123 | /***/ function(module, exports, __webpack_require__) { 124 | __webpack_require__(3); 125 | var chunk1=1; 126 | exports.chunk1=chunk1; 127 | 128 | /***/ }, 129 | /* 3 */ 130 | /***/ function(module, exports) { 131 | var chunk2=1; 132 | exports.chunk2=chunk2; 133 | 134 | /***/ } 135 | ``` 136 | 关于chunks配置的使用你可以[点击这里查看](https://github.com/liangklfangl/commonsChunkPlugin_Config#参数chunks)。所以,chunks就是用于指定从哪些chunks来抽取公共的模块,而chunks的名称一般都是通过entry来指定的,比如上面的entry为: 137 | ```js 138 | entry: { 139 | main: process.cwd()+'/example6/main.js', 140 | main1: process.cwd()+'/example6/main1.js', 141 | jquery:["jquery"] 142 | }, 143 | ``` 144 | 而chunks指定的为: 145 | ```js 146 | chunks:["main","main1"] 147 | ``` 148 | 表明从main,main1两个入口chunk中来找公共的模块。 149 | 150 | 151 | - deepChildren 152 | 153 | 如果将该参数设置为true,那么common-chunk下的所有的chunk都会被选中,比如require.ensure产生的chunk的子级chunk。从这些chunks中来抽取公共的模块。 154 | 155 | - minChunks为函数 156 | 157 | 你可以给*minChunks*传入一个函数。CommonsChunkPlugin将会调用这个函数并传入module和count参数。这个module参数用于指定某一个chunks中所有的模块,而这个chunk的名称就是上面你配置的name/names参数。这个module是一个[NormalModule](https://github.com/webpack/webpack/blob/master/lib/NormalModule.js)实例,他有如下的常用属性: 158 | 159 | *module.context*:表示存储文件的路径,比如'/my_project/node_modules/example-dependency' 160 | 161 | 162 | *module.resource*: 表示被处理的文件名称。比如'/my_project/node_modules/example-dependency/index.js' 163 | 164 | 而我们的count参数表示指定的模块出现在多少个chunk中。这个函数对于你细粒度的操作CommonsChunk插件还是很有用的。你可自己决定将那些模块放在指定的common chunk中,下面是官网给出的一个例子: 165 | ```js 166 | new webpack.optimize.CommonsChunkPlugin({ 167 | name: "my-single-lib-chunk", 168 | filename: "my-single-lib-chunk.js", 169 | minChunks: function(module, count) { 170 | //如果一个模块的路径中存在somelib部分,而且这个模块出现在3个独立的chunk或者entry中,那么它就会被抽取到一个独立的chunk中,而且这个chunk的文件名称为"my-single-lib-chunk.js",而这个chunk本身的名称为"my-single-lib-chunk" 171 | return module.resource && (/somelib/).test(module.resource) && count === 3; 172 | } 173 | }); 174 | ``` 175 | 而官网下面的例子详细的展示了如何将node_modules下引用的模块抽取到一个独立的chunk中: 176 | ```js 177 | new webpack.optimize.CommonsChunkPlugin({ 178 | name: "vendor", 179 | minChunks: function (module) { 180 | // this assumes your vendor imports exist in the node_modules directory 181 | return module.context && module.context.indexOf("node_modules") !== -1; 182 | } 183 | }) 184 | ``` 185 | 因为node_module下的模块一般都是来源于第三方,所以在本地很少修改,通过这种方式可以将第三方的模块抽取到公共的chunk中。 186 | 187 | 还有一种情况就是,如果你想把应用的css/scss和vendor的css(第三方类库的css)抽取到一个独立的文件中,那么你可以使用下面的minChunk函数,同时配合ExtractTextPlugin来完成。 188 | ```js 189 | new webpack.optimize.CommonsChunkPlugin({ 190 | name: "vendor", 191 | minChunks: function (module) { 192 | // This prevents stylesheet resources with the .css or .scss extension 193 | // from being moved from their original chunk to the vendor chunk 194 | if(module.resource && (/^.*\.(css|scss)$/).test(module.resource)) { 195 | return false; 196 | } 197 | return module.context && module.context.indexOf("node_modules") !== -1; 198 | } 199 | }) 200 | ``` 201 | 这个例子在抽取node_modules下的模块的时候做了一个限制,即明确指定node_modules下的scss/css文件不会被抽取,所以我们最后生成的vendor.js不会包含第三方类库的css/scss文件,而只包含其中的js部分。 同时通过配置ExtractTextPlugin就可以将我们应用的css和第三方应用的css抽取到一个独立的css文件中,从而达到css和js分离。 202 | 203 | 其中CommonsChunkPlugin插件还有一个更加有用的配置,即用于将webpack打包逻辑相关的一些文件抽取到一个独立的chunk中。但是此时配置的name应该是entry中不存在的,这对于线上缓存很有作用。因为如果文件的内容不发生变化,那么chunk的名称不会发生变化,所以并不会影响到线上的缓存。比如下面的例子: 204 | ```js 205 | new webpack.optimize.CommonsChunkPlugin({ 206 | name: "manifest", 207 | //这个name必须不在entry中 208 | minChunks: Infinity 209 | }) 210 | ``` 211 | 但是你会发现我们抽取manifest文件和配置vendor chunk的逻辑不一样,所以这个插件需要配置两次: 212 | ```js 213 | [ 214 | new webpack.optimize.CommonsChunkPlugin({ 215 | name: "vendor", 216 | minChunks: function(module){ 217 | return module.context && module.context.indexOf("node_modules") !== -1; 218 | } 219 | }), 220 | new webpack.optimize.CommonsChunkPlugin({ 221 | name: "manifest", 222 | minChunks: Infinity 223 | }), 224 | ] 225 | ``` 226 | 你可能会好奇,假如我们有如下的配置: 227 | ```js 228 | module.exports = { 229 | entry: './src/index.js', 230 | plugins: [ 231 | new CleanWebpackPlugin(['dist']), 232 | new HtmlWebpackPlugin({ 233 | title: 'Caching' 234 | }) 235 | ], 236 | output: { 237 | filename: '[name].[chunkhash].js', 238 | path: path.resolve(__dirname, 'dist') 239 | } 240 | }; 241 | ``` 242 | 那么如果entry中文件的内容没有发生变化,你运行webpack命令多次,那么最后生成的文件名称应该是一样的,为什么会重新通过CommonsChunkPlugin来生成一个manifest文件呢?这个官网也有明确的说明: 243 | 244 | 因为webpack在入口文件中会包含特定的样板文件,特别是runtime文件和manifest文件。而最后生成的文件的名称到底是否一致还与webpack的版本有关,在新版本中可能不存在这个问题,但是在老版本中可能会存在,所以为了安全起见我们一般都会使用它。那么问题又来了,样板文件和runtime文件指的是什么?你可以查[我的这个例子](https://github.com/liangklfangl/commonsChunkPlugin_Config#将公共业务模块与类库或框架分开打包),把关注点放在文中说的为什么要提前加载最后一个chunk的问题上。下面我们就这部分做一下深入的分析: 245 | 246 | - Runtime 247 | 248 | 当你的代码在浏览器中运行的时候,webpack使用Runtime和manifest来处理你应用中的模块化关系。其中包括在模块存在依赖关系的时候,加载和解析特定的逻辑,而解析的模块包括已经在浏览中加载完成的模块和那些需要懒加载的模块本身。 249 | 250 | - Manifest 251 | 252 | 一旦你的应用程序中,如index.html文件、一些 bundle 和各种静态资源被加载到浏览器中,会发生什么?你精心安排的 /src 目录的文件结构现在已经不存在,所以webpack如何管理所有模块之间的交互呢?这就是 manifest数据用途的由来…… 253 | 254 | 当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest",当完成打包并发送到浏览器时,会在运行时通过Manifest来解析和加载模块。无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用manifest中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。比如我提供的[这个例子](https://github.com/liangklfangl/commonsChunkPlugin_Config#单入口文件时候不能把引用多次的模块打印到commonchunkplugin中),其中入口文件中有main.js,入口文件中加载chunk1.js和chunk2.js,而最后你看到的就是下面转化为__webpack_require__后的内容: 255 | ```js 256 | webpackJsonp([0,1],[ 257 | /* 0 */ 258 | /***/ function(module, exports, __webpack_require__) { 259 | __webpack_require__(1); 260 | __webpack_require__(2); 261 | /***/ }, 262 | /* 1 */ 263 | /***/ function(module, exports, __webpack_require__) { 264 | 265 | __webpack_require__(2); 266 | var chunk1=1; 267 | exports.chunk1=chunk1; 268 | /***/ }, 269 | /* 2 */ 270 | /***/ function(module, exports) { 271 | var chunk2=1; 272 | exports.chunk2=chunk2; 273 | /***/ } 274 | ]); 275 | ``` 276 | 而manifest文件的作用就是在运行的时候通过__webpack_require__后的模块标识(Module identifier)来加载指定的模块内容。比如[manifest例子](https://github.com/liangklfangl/commonsChunkPlugin_Config/blob/master/dest/example8/manifest.json)生成的manifest.json文件内容是如下的格式: 277 | ```js 278 | { 279 | "common.js": "common.js", 280 | "main.js": "main.js", 281 | "main1.js": "main1.js" 282 | } 283 | ``` 284 | 这样就可以在源文件和目标文件之间有一个映射关系,而这个映射关系本身依然存在于我们打包后的输出目录,而不会因为src目录消失了而不知道具体的模块对应关系。而至于其中moduleId等的对应关系是由webpack自己维护的,通过打包后的[可视化](https://github.com/liangklfangl/commonchunkplugin-source-code/raw/master/7.png)你可以了解。 285 | 286 | ##### 2.2 CommonChunkPlugin无法抽取单入口文件公共模块 287 | 上面讲了webpack官网提供的例子以及原理分析,下面我们通过自己构造的几个例子来深入理解上面的概念。假如我们有如下的webpack配置文件: 288 | ```js 289 | var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); 290 | module.exports = { 291 | entry: 292 | { 293 | main:process.cwd()+'/example1/main.js', 294 | }, 295 | output: { 296 | path:process.cwd()+'/dest/example1', 297 | filename: '[name].js' 298 | }, 299 | devtool:'cheap-source-map', 300 | plugins: [ 301 | new CommonsChunkPlugin({ 302 | name:"chunk", 303 | minChunks:2 304 | }) 305 | ] 306 | }; 307 | ``` 308 | 下面是我们的入口文件内容: 309 | ```js 310 | //main.js 311 | require("./chunk1"); 312 | require("./chunk2"); 313 | console.log('main1.'); 314 | ``` 315 | 其中chunk1.js内容如下: 316 | ```js 317 | require("./chunk2"); 318 | var chunk1=1; 319 | exports.chunk1=chunk1; 320 | ``` 321 | 而chunk2.js内容如下: 322 | ```js 323 | var chunk2=1; 324 | exports.chunk2=chunk2; 325 | ``` 326 | 我们引入了CommonsChunkPlugin,并将那些引入了两次以上的模块输出到chunk.js中。那么你肯定会认为,因为chunk2.js被引入了两次,那么它肯定会被插件抽取到chunk.js中,但是实际上并不是这样。你可以查看*main.js*,他的内容如下: 327 | ```js 328 | webpackJsonp([0,1],[ 329 | /* 0 */ 330 | /***/ function(module, exports, __webpack_require__) { 331 | __webpack_require__(1); 332 | __webpack_require__(2); 333 | /***/ }, 334 | /* 1 */ 335 | /***/ function(module, exports, __webpack_require__) { 336 | __webpack_require__(2); 337 | var chunk1=1; 338 | exports.chunk1=chunk1; 339 | 340 | /***/ }, 341 | /* 2 */ 342 | /***/ function(module, exports) { 343 | var chunk2=1; 344 | exports.chunk2=chunk2; 345 | 346 | /***/ } 347 | ]); 348 | ``` 349 | 通过这个例子我需要告诉你:'单入口文件时候不能把引用多次的模块打印到CommonChunkPlugin中'。 350 | 351 | ##### 2.3 CommonChunkPlugin抽取多入口文件公共模块 352 | 假如我们有如下的webpack配置文件: 353 | ```js 354 | var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); 355 | module.exports = { 356 | entry: 357 | { 358 | main:process.cwd()+'/example2/main.js', 359 | main1:process.cwd()+'/example2/main1.js', 360 | }, 361 | output: { 362 | path:process.cwd()+'/dest/example2', 363 | filename: '[name].js' 364 | }, 365 | plugins: [ 366 | new CommonsChunkPlugin({ 367 | name:"chunk", 368 | minChunks:2 369 | }) 370 | ] 371 | }; 372 | ``` 373 | 其中main1.js内容如下: 374 | ```js 375 | require("./chunk1"); 376 | require("./chunk2"); 377 | ``` 378 | 而main.js内容如下: 379 | ```js 380 | require("./chunk1"); 381 | require("./chunk2"); 382 | ``` 383 | 而chunk1.js内容如下: 384 | ```js 385 | require("./chunk2"); 386 | var chunk1=1; 387 | exports.chunk1=chunk1; 388 | ``` 389 | 而chunk2.js内容如下: 390 | ```js 391 | var chunk2=1; 392 | exports.chunk2=chunk2; 393 | ``` 394 | 此时,很显然我们采用的是多入口文件模式,在相应的目录下会生成main.js和main1.js,以及chunk.js,而chunk.js中抽取的是main.js和main1.js中被引入了两次以上的模块,很显然chunk1.js和chunk2.js都会被引入到chunk.js中,下面是chunk.js中的部分代码: 395 | ```js 396 | /******/ ([ 397 | /* 0 */, 398 | /* 1 */ 399 | /***/ function(module, exports, __webpack_require__) { 400 | __webpack_require__(2); 401 | var chunk1=1; 402 | exports.chunk1=chunk1; 403 | /***/ }, 404 | /* 2 */ 405 | /***/ function(module, exports) { 406 | var chunk2=1; 407 | exports.chunk2=chunk2; 408 | 409 | /***/ } 410 | /******/ ]); 411 | ``` 412 | 413 | ##### 2.4 CommonChunkPlugin分离业务代码与框架代码 414 | 假如我们有如下的webpack配置内容,同时chunk1,chunk2,main1,main的内容和上面保持一致。 415 | ```js 416 | var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); 417 | module.exports = { 418 | entry: { 419 | main: process.cwd()+'/example3/main.js', 420 | main1: process.cwd()+'/example3/main1.js', 421 | common1:["jquery"], 422 | //只含有jquery.js 423 | common2:["vue"] 424 | //只含有vue.js和加载器代码 425 | }, 426 | output: { 427 | path: process.cwd()+'/dest/example3', 428 | filename: '[name].js' 429 | }, 430 | plugins: [ 431 | new CommonsChunkPlugin({ 432 | name: ["chunk",'common1','common2'], 433 | minChunks:2 434 | //引入两次以及以上的模块 435 | }) 436 | ] 437 | }; 438 | ``` 439 | 按照CommonsChunkPlugin的抽取公共代码的逻辑,我们会有如下的结果: 440 |
441 | 1.chunk.js中保存的是main.js和main1.js的公共代码,即chunk1.js和chunk2.js 
442 | 2.common1.js中只有jquery.js
443 | 3.common2.js中只有vue.js,但是必须含有webpack的加载器代码 
444 | 
445 | 其实道理很简单,我们的chunk.js中只有chunk1.js和chunk2.js,而不存在被引入了两次的模块,最多引入次数的就是chunk2.js,所以common1.js只含有jquery.js。但是,正如前文所说,我们的common2.js必须最先加载。 446 | 447 | ##### 2.5 minChunks为Infinity配置 448 | 假如我们的webpack配置如下: 449 | ```js 450 | var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); 451 | module.exports = { 452 | entry: { 453 | main: process.cwd()+'/example5/main.js', 454 | main1: process.cwd()+'/example5/main1.js', 455 | jquery:["jquery"] 456 | //minChunks: Infinity时候框架代码依然会被单独打包成一个文件 457 | }, 458 | output: { 459 | path: process.cwd() + '/dest/example5', 460 | filename: '[name].js' 461 | }, 462 | plugins: [ 463 | new CommonsChunkPlugin({ 464 | name: "jquery", 465 | minChunks:2//被引用两次及以上 466 | }) 467 | ] 468 | }; 469 | ``` 470 | 上面的文件输出将会是如下内容: 471 |
472 | 1.main.js包含去掉的公共代码部分
473 | 2.main1.js包含去掉的公共代码部分
474 | 3.main1.js和main2.js的公共代码将会被打包到jquery.js中,即jquery.js包含jquery+公共的业务代码
475 | 
476 | 477 | 其实,这个配置稍微晦涩难懂一点,假如我们将上面的minChunks配置修改为"Infinity",那么结果将截然不同: 478 |
479 | 1.main.js原样打包
480 | 2.main1.js原样打包
481 | 3.jquery包含jquery.js和webpack模块加载器    
482 | 
483 | 因为将minChunks设置为Infinity,也就是无穷大,那么main.js和main1.js中不存在任何模块被依赖的次数这么大,因此chunk.js和chunk1.js都不会被抽取出来。 484 | 485 | ##### 2.6 chunks指定那些入口文件中的公共模块会被抽取 486 | 我们继续修改webpack配置如下: 487 | ```js 488 | var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); 489 | module.exports = { 490 | entry: { 491 | main: process.cwd()+'/example6/main.js', 492 | main1: process.cwd()+'/example6/main1.js', 493 | jquery:["jquery"] 494 | }, 495 | output: { 496 | path: process.cwd() + '/dest/example6', 497 | filename: '[name].js' 498 | }, 499 | plugins: [ 500 | new CommonsChunkPlugin({ 501 | name: "jquery", 502 | minChunks:2, 503 | chunks:["main","main1"] 504 | //main.js和main1.js中都引用的模块才会被打包的到公共模块 505 | }) 506 | ] 507 | }; 508 | ``` 509 | 此时我们的chunks设置为*["main","main1"]*,表示只有main.js和main1.js中都引用的模块才会被打包的到公共模块,而且必须是依赖次数为2次以上的模块。因此结果将会如下: 510 |
511 | 1.jquery.js中包含main1.js和main.js中公共的模块,即chunk1.js和chunk2.js,以及jquery.js本身
512 | 2.main1.js表示是去掉公共模块后的文件内容
513 | 3.main.js表示是去掉公共模块后的文件内容    
514 | 
515 | 我们也可以通过查看打包后的jquery.js看到结果验证,即jquery.js包含了jquery.js本身以及公共的业务代码: 516 | ```js 517 | /* 2 */ 518 | /***/ function(module, exports, __webpack_require__) { 519 | __webpack_require__(3); 520 | var chunk1=1; 521 | exports.chunk1=chunk1; 522 | 523 | /***/ }, 524 | /* 3 */ 525 | /***/ function(module, exports) { 526 | var chunk2=1; 527 | exports.chunk2=chunk2; 528 | 529 | /***/ } 530 | ``` 531 | 532 | #### 3.本章小结 533 | 本章节主要通过7个例子展示了webpack配合CommonsChunkPlugin的打包结果,但是为什么结果是这样,我会在webpack常见插件原理分析章节进行深入的剖析。本章节所有的例子代码你可以[点击这里](https://github.com/liangklfangl/commonsChunkPlugin_Config)查看。文中的配置都是参考webpack2的,如果你使用的是webpack1,请升级。如果需要查看上面的例子的运行结果,请执行下面的命令: 534 | ```js 535 | npm install webpack -g 536 | git clone https://github.com/liangklfangl/commonsChunkPlugin_Config.git 537 | webpack 538 | //修改webpack.config.js并运行webpack命令 539 | ``` 540 | 541 | -------------------------------------------------------------------------------- /05-webpack的HMR原理分析.md: -------------------------------------------------------------------------------- 1 | #### 1.本章概述 2 | 本章节我们将会详细论述webpack对于HMR的支持,当然因为更多的涉及原理的东西,所以代码也比较多。如果你有任何不懂的地方,记得在读者圈给我提问,我会及时回答。 3 | 4 | #### 2.webpack 是如何实现 HMR 的以及实现的原理如何 5 | 6 | ##### 2.1 webpack的HMR的实现 7 | 其实webpack实现HMR是依赖于webpack-dev-server的,webpack官方文档也写的非常清楚,我们只需要参考它的做法来完成即可,首先假如我们有如下的webpack.config.js配置文件: 8 | ```js 9 | const path = require('path'); 10 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 11 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 12 | const webpack = require('webpack'); 13 | module.exports = { 14 | entry: { 15 | app: './src/index.js' 16 | }, 17 | devtool: 'inline-source-map', 18 | devServer: { 19 | contentBase: './dist', 20 | hot: true 21 | //支持HMR 22 | }, 23 | plugins: [ 24 | new webpack.NamedModulesPlugin(), 25 | new webpack.HotModuleReplacementPlugin() 26 | //该插件来自于webpack本身的支持 27 | ], 28 | output: { 29 | filename: '[name].bundle.js', 30 | path: path.resolve(__dirname, 'dist') 31 | } 32 | }; 33 | ``` 34 | 很显然,我们的入口文件中只有app.js,同时在webpack的devServer配置中我们设置了hot为true,而且在webpack的plugin中我们添加了new webpack.HotModuleReplacementPlugin()这个插件。 35 | 假如我们需要在index.js中实现HMR,我们写一个index.js的代码实例: 36 | ```js 37 | import printMe from './print.js'; 38 | if(module.hot){ 39 | //此时accept方法第一个参数表示只有当print.js发生改变我们才会热加载 40 | module.hot.accept('./print.js', function() { 41 | console.log('Accepting the updated printMe module!'); 42 | printMe(); 43 | }) 44 | } 45 | ``` 46 | 而我们的print.js为如下内容: 47 | ```js 48 | export default function printMe() { 49 | console.log('I get called from print.js!'); 50 | } 51 | ``` 52 | 此时,当你修改print.js的时候,我们的index.js也会重新加载,而且在控制台中也会输出如下内容: 53 |
 54 |  [HMR] Waiting for update signal from WDS... main.js:4395 [WDS] Hot
 55 |  Module Replacement enabled.
 56 |  + 2main.js:4395 [WDS] App updated. Recompiling...
 57 |  + main.js:4395 [WDS] App hot update...
 58 |  + main.js:4330 [HMR] Checking for updates on the server...
 59 |  + main.js:10024 Accepting the updated printMe module!
 60 |  + 0.4b8ee77….hot-update.js:10 Updating print.js...
 61 |  + main.js:4330 [HMR] Updated modules:
 62 |  + main.js:4330 [HMR]  - 20
 63 |  + main.js:4330 [HMR] Consider using the NamedModulesPlugin for module names.
 64 |  
65 | 66 | 这就是HMR热加载的方式,而且它不是采用完全reload页面的方式,所以在开发中是一个很重要的特性。当然,只有模块本身支持热加载并开启了webpack的热加载特性以后才会支持,这一点一定要注意。 67 | 68 | ##### 2.2 webpack的HMR的实现原理 69 | 看看下面的方法你就知道了,在hot模式下,我们的entry最后都会被添加两个文件,而这两个文件和你配置的entry一起作为webpack最终的入口文件,进而构建最终的模块依赖图谱。 70 | ```js 71 | module.exports = function addDevServerEntrypoints(webpackOptions, devServerOptions) { 72 | if(devServerOptions.inline !== false) { 73 | //表示是inline模式而不是iframe模式 74 | const domain = createDomain(devServerOptions); 75 | const devClient = [`${require.resolve("../../client/")}?${domain}`]; 76 | //客户端内容 77 | if(devServerOptions.hotOnly) 78 | devClient.push("webpack/hot/only-dev-server"); 79 | else if(devServerOptions.hot) 80 | devClient.push("webpack/hot/dev-server"); 81 | //配置了不同的webpack而文件到客户端文件中 82 | [].concat(webpackOptions).forEach(function(wpOpt) { 83 | if(typeof wpOpt.entry === "object" && !Array.isArray(wpOpt.entry)) { 84 | //这里是我们自己在webpack.config.js中配置的entry对象,对entry中的每一个入口文件都添加我们webpack/hot/only-dev-server或者webpack/hot/dev-server用于实现HMR 85 | Object.keys(wpOpt.entry).forEach(function(key) { 86 | wpOpt.entry[key] = devClient.concat(wpOpt.entry[key]); 87 | }); 88 | } else if(typeof wpOpt.entry === "function") { 89 | wpOpt.entry = wpOpt.entry(devClient); 90 | //如果entry是一个函数那么我们把devClient数组传入函数,由开发者自己构建自己的entry,但是只有在HMR开启的情况下适用 91 | } else { 92 | wpOpt.entry = devClient.concat(wpOpt.entry); 93 | //如果用户的entry是数组,那么我们直接将webpack/hot/only-dev-server或者webpack/hot/dev-server传入用于实现HMR 94 | } 95 | }); 96 | } 97 | }; 98 | ``` 99 | 请仔细理解上面的注释,因为它蕴含着在HMR模式下,webpack-dev-server对于我们自己配置的entry的一种进一步处理。下面我们将会进一步深入分析webpack/hot/only-dev-server和webpack/hot/dev-server,看看他们是如何实现HMR的。我们来看看"webpack/hot/only-dev-server"的文件内容,它是实现HMR的关键: 100 | ```js 101 | if(module.hot) { 102 | var lastHash; 103 | var upToDate = function upToDate() { 104 | return lastHash.indexOf(__webpack_hash__) >= 0; 105 | //(1)如果两个hash相同那么表示没有更新,其中lastHash表示上一次编译的hash,记住是compilation的hash 106 | //只有在HotModuleReplacementPlugin开启的时候存在。任意文件变化后compilation都会发生变化 107 | }; 108 | //(2)下面是检查更新的模块 109 | var check = function check() { 110 | module.hot.check().then(function(updatedModules) { 111 | //(2.1)没有更新的模块直接返回,通知用户无需HMR 112 | if(!updatedModules) { 113 | console.warn("[HMR] Cannot find update. Need to do a full reload!"); 114 | console.warn("[HMR] (Probably because of restarting the webpack-dev-server)"); 115 | return; 116 | } 117 | //(2.2)开始更新 118 | return module.hot.apply({ 119 | ignoreUnaccepted: true, 120 | //和accept函数指定热加载那些模块 121 | ignoreDeclined: true, 122 | //decline表示不支持这个模块热加载 123 | ignoreErrored: true, 124 | //error表示出错的模块 125 | onUnaccepted: function(data) { 126 | console.warn("Ignored an update to unaccepted module " + data.chain.join(" -> ")); 127 | }, 128 | onDeclined: function(data) { 129 | console.warn("Ignored an update to declined module " + data.chain.join(" -> ")); 130 | }, 131 | onErrored: function(data) { 132 | console.warn("Ignored an error while updating module " + data.moduleId + " (" + data.type + ")"); 133 | } 134 | //(2.2.1)renewedModules表示哪些模块已经更新了 135 | }).then(function(renewedModules) { 136 | //(2.2.2)如果有模块没有更新完成,那么继续检查 137 | if(!upToDate()) { 138 | check(); 139 | } 140 | //(2.2.3)更新的模块updatedModules,renewedModules表示哪些模块已经更新了 141 | require("./log-apply-result")(updatedModules, renewedModules); 142 | //通知已经热加载完成 143 | if(upToDate()) { 144 | console.log("[HMR] App is up to date."); 145 | } 146 | }); 147 | }).catch(function(err) { 148 | //(2.3)更新异常,输出HMR信息 149 | var status = module.hot.status(); 150 | if(["abort", "fail"].indexOf(status) >= 0) { 151 | console.warn("[HMR] Cannot check for update. Need to do a full reload!"); 152 | console.warn("[HMR] " + err.stack || err.message); 153 | } else { 154 | console.warn("[HMR] Update check failed: " + err.stack || err.message); 155 | } 156 | }); 157 | }; 158 | var hotEmitter = require("./emitter"); 159 | //(3)emitter模块内容,也就是导出一个events实例 160 | /* 161 | var EventEmitter = require("events"); 162 | module.exports = new EventEmitter(); 163 | */ 164 | hotEmitter.on("webpackHotUpdate", function(currentHash) { 165 | lastHash = currentHash; 166 | //(3.1)表示本次更新后得到的hash值 167 | if(!upToDate()) { 168 | //(3.1.1)有更新 169 | var status = module.hot.status(); 170 | if(status === "idle") { 171 | console.log("[HMR] Checking for updates on the server..."); 172 | check(); 173 | } else if(["abort", "fail"].indexOf(status) >= 0) { 174 | console.warn("[HMR] Cannot apply update as a previous update " + status + "ed. Need to do a full reload!"); 175 | } 176 | } 177 | }); 178 | console.log("[HMR] Waiting for update signal from WDS..."); 179 | } else { 180 | throw new Error("[HMR] Hot Module Replacement is disabled."); 181 | } 182 | ``` 183 | 上面看到了log-apply-result模块,我们看到这个模块是在所有的内容已经更新完成后调用的,下面继续看一下它到底做了什么事情: 184 | ```js 185 | module.exports = function(updatedModules, renewedModules) { 186 | //(1)renewedModules表示哪些模块需要更新,剩余的模块unacceptedModules表示,哪些模块由于 ignoreDeclined,ignoreUnaccepted配置没有更新 187 | var unacceptedModules = updatedModules.filter(function(moduleId) { 188 | return renewedModules && renewedModules.indexOf(moduleId) < 0; 189 | }); 190 | //(2)unacceptedModules表示该模块无法HMR,打印log 191 | if(unacceptedModules.length > 0) { 192 | console.warn("[HMR] The following modules couldn't be hot updated: (They would need a full reload!)"); 193 | unacceptedModules.forEach(function(moduleId) { 194 | console.warn("[HMR] - " + moduleId); 195 | }); 196 | } 197 | //(2)没有模块更新,表示模块是最新的 198 | if(!renewedModules || renewedModules.length === 0) { 199 | console.log("[HMR] Nothing hot updated."); 200 | } else { 201 | console.log("[HMR] Updated modules:"); 202 | //(3)打印那些模块被热更新。每一个moduleId都是数字那么建议使用NamedModulesPlugin(webpack2建议) 203 | renewedModules.forEach(function(moduleId) { 204 | console.log("[HMR] - " + moduleId); 205 | }); 206 | var numberIds = renewedModules.every(function(moduleId) { 207 | return typeof moduleId === "number"; 208 | }); 209 | if(numberIds) 210 | console.log("[HMR] Consider using the NamedModulesPlugin for module names."); 211 | } 212 | }; 213 | ``` 214 | 215 | 所以"webpack/hot/only-dev-server"的文件内容就是检查哪些模块更新了(通过webpackHotUpdate事件完成,而该事件依赖于`compilation`的hash值),其中哪些模块更新成功,而哪些模块由于某种原因没有更新成功。至于模块什么时候接受到需要更新是和webpack的打包过程有关的,这里也给出触发更新的时机: 216 | 217 | ```js 218 | ok: function() { 219 | sendMsg("Ok"); 220 | if(useWarningOverlay || useErrorOverlay) overlay.clear(); 221 | if(initial) return initial = false; 222 | reloadApp(); 223 | }, 224 | warnings: function(warnings) { 225 | log("info", "[WDS] Warnings while compiling."); 226 | var strippedWarnings = warnings.map(function(warning) { 227 | return stripAnsi(warning); 228 | }); 229 | sendMsg("Warnings", strippedWarnings); 230 | for(var i = 0; i < strippedWarnings.length; i++) 231 | console.warn(strippedWarnings[i]); 232 | if(useWarningOverlay) overlay.showMessage(warnings); 233 | 234 | if(initial) return initial = false; 235 | reloadApp(); 236 | }, 237 | function reloadApp() { 238 | //(1)如果开启了HMR模式 239 | if(hot) { 240 | log("info", "[WDS] App hot update..."); 241 | var hotEmitter = require("webpack/hot/emitter"); 242 | hotEmitter.emit("webpackHotUpdate", currentHash); 243 | //重新启动webpack/hot/emitter,同时设置当前hash,通知上面的webpack-dev-server的webpackHotUpdate事件,告诉它打印那些模块的更新信息 244 | if(typeof self !== "undefined" && self.window) { 245 | // broadcast update to window 246 | self.postMessage("webpackHotUpdate" + currentHash, "*"); 247 | } 248 | } else { 249 | //(2)如果不是Hotupdate那么我们直接reload我们的window就可以了 250 | log("info", "[WDS] App updated. Reloading..."); 251 | self.location.reload(); 252 | } 253 | } 254 | 255 | ``` 256 | 也就是说当客户端(*打包到我们的entry中的webpack-dev-server提供的websocket的客户端代码*)接受到服务器(*webpack-dev-server接受到webpack提供的compiler对象可以知道webpack什么时候打包完成,通过webpack-dev-server提供的websocket服务端代码通知websocket客户端*)发送的ok和warning信息的时候会要求更新。如果支持HMR的情况下就会要求检查更新,同时发送过来的还有服务器端本次编译的*compilation的hash*值。如果不支持HMR,那么我们要求刷新页面。我们继续深入一步,看看服务器什么时候发送'ok'和'warning'消息: 257 | ```js 258 | Server.prototype._sendStats = function(sockets, stats, force) { 259 | if(!force && 260 | stats && 261 | (!stats.errors || stats.errors.length === 0) && 262 | stats.assets && 263 | stats.assets.every(function(asset) { 264 | return !asset.emitted; 265 | //(1)每一个asset都是没有emitted属性,表示没有发生变化。如果发生变化那么这个assets肯定有emitted属性 266 | }) 267 | ) 268 | return this.sockWrite(sockets, "still-ok"); 269 | //(1)将stats的hash写给socket客户端 270 | this.sockWrite(sockets, "hash", stats.hash); 271 | //设置hash 272 | if(stats.errors.length > 0) 273 | this.sockWrite(sockets, "errors", stats.errors); 274 | else if(stats.warnings.length > 0) 275 | this.sockWrite(sockets, "warnings", stats.warnings); 276 | else 277 | this.sockWrite(sockets, "ok"); 278 | } 279 | ``` 280 | 也就是说更新是通过上面这个方法完成的,我们看看上面这个方法什么时候调用就可以了: 281 | ```js 282 | compiler.plugin("done", function(stats) { 283 | //clientStats表示需要保存stats中的那些属性,可以允许配置,参见webpack官网 284 | this._sendStats(this.sockets, stats.toJson(clientStats)); 285 | this._stats = stats; 286 | }.bind(this)); 287 | ``` 288 | 是不是豁然开朗了,也就是每次compiler的'done'钩子函数被调用的时候就会要求客户端去检查模块更新,如果客户端不支持HMR,那么就会全局加载。整个过程就是:`webpack-dev-server在用户的入口文件中添加热加载的客户端websocket代码=>webpack-dev-server拿到webpack的compiler对象=>判断是否需要更新=>通过websocket服务端代码通知客户端,并发送compilation的hash值=>客户端判断compilation的hash值是否发生变化并实现热加载以及log打印`。而有一点你需要弄清楚,那就是:我们的webpack-dev-server必须拿着webpack提供的compiler对象才行,具体你可以查看我对webpack-dev-server的一个[封装实例](https://github.com/liangklfangl/wcf/blob/master/src/devServer.js#L136)。 289 | 290 | 接下来我们来看看"webpack/hot/dev-server": 291 | ```js 292 | if(module.hot) { 293 | var lastHash; 294 | //__webpack_hash__是每次编译的hash值是全局的 295 | var upToDate = function upToDate() { 296 | return lastHash.indexOf(__webpack_hash__) >= 0; 297 | }; 298 | var check = function check() { 299 | module.hot.check(true).then(function(updatedModules) { 300 | //检查所有要更新的模块,如果没有模块要更新那么回调函数就是null 301 | if(!updatedModules) { 302 | console.warn("[HMR] Cannot find update. Need to do a full reload!"); 303 | console.warn("[HMR] (Probably because of restarting the webpack-dev-server)"); 304 | window.location.reload(); 305 | return; 306 | } 307 | //如果还有更新 308 | if(!upToDate()) { 309 | check(); 310 | } 311 | require("./log-apply-result")(updatedModules, updatedModules); 312 | //已经被更新的模块都是updatedModules 313 | if(upToDate()) { 314 | console.log("[HMR] App is up to date."); 315 | } 316 | 317 | }).catch(function(err) { 318 | var status = module.hot.status(); 319 | //如果报错直接全局reload 320 | if(["abort", "fail"].indexOf(status) >= 0) { 321 | console.warn("[HMR] Cannot apply update. Need to do a full reload!"); 322 | console.warn("[HMR] " + err.stack || err.message); 323 | window.location.reload(); 324 | } else { 325 | console.warn("[HMR] Update failed: " + err.stack || err.message); 326 | } 327 | }); 328 | }; 329 | var hotEmitter = require("./emitter"); 330 | //获取MyEmitter对象 331 | hotEmitter.on("webpackHotUpdate", function(currentHash) { 332 | lastHash = currentHash; 333 | if(!upToDate() && module.hot.status() === "idle") { 334 | //调用module.hot.status方法获取状态 335 | console.log("[HMR] Checking for updates on the server..."); 336 | check(); 337 | } 338 | }); 339 | console.log("[HMR] Waiting for update signal from WDS..."); 340 | } else { 341 | throw new Error("[HMR] Hot Module Replacement is disabled."); 342 | } 343 | ``` 344 | 两者的主要代码区别在于check函数的调用方式: 345 | 346 |
347 | check([autoApply], callback: (err: Error, outdatedModules: Module[]) => void
348 | 
349 | 350 | 如果我们的autoApply设置为true,那么我们回调函数传入的就是所有被自己[dispose处理](https://webpack.js.org/api/hot-module-replacement/#dispose-or-adddisposehandler-)过的模块,同时apply方法也会自动调用,而不需要向`webpack/hot/only-dev-server`一样手动调用`module.hot.apply`。如果auApply设置为false,那么所有的模块更新都会通过手动调用apply来完成。而所说的被自己dispose处理就是通过如下的方式来完成的: 351 | ```js 352 | if (module.hot) { 353 | module.hot.accept(); 354 | //支持热更新 355 | //当前模块代码更新后的回调,常用于移除持久化资源或者清除定时器等操作,如果想传递数据到更新后的模块,可以通过传入data参数,后续参数可以通过module.hot.data获取 356 | module.hot.dispose(() => { 357 | window.clearInterval(intervalId); 358 | }); 359 | } 360 | ``` 361 | 而一般我们调用webpack-dev-server只会添加--hot而已,即内部不需要调用apply,而传入的都是被dispose处理过的模块: 362 | ```js 363 | if(devServerOptions.hotOnly) 364 | devClient.push("webpack/hot/only-dev-server"); 365 | else if(devServerOptions.hot) 366 | devClient.push("webpack/hot/dev-server"); 367 | ``` 368 | 369 | 不管是那种方式,webpack-dev-server实现热加载都具有如下的流程: 370 | 371 | ![](http://images.gitbook.cn/5f2d93b0-b0ae-11e7-a56a-2b0687e97e1c) 372 | 373 | 374 | ##### 2.3 如何写出支持HMR的代码 375 | 376 | 这里就是一个例子,你也可以查看[这个仓库](https://github.com/liangklfangl/wcf),然后克隆下来,执行下面命令(注意,这个仓库的代码已经发布到npm的[webpackcc](https://www.npmjs.com/package/webpackcc)): 377 | ```js 378 | npm install webpackcc -g 379 | npm run test 380 | ``` 381 | 你就会发现访问localhost:8080的时候代码是可以支持HMR(你可以修改test目录下的所有的文件),而不会出现页面刷新的情况。下面也给出实例代码: 382 | ```js 383 | //time.js 384 | let moduleStartTime = getCurrentSeconds(); 385 | //(1)得到当前模块加载的时间,是该模块的一个全局变量,首次加载模块的时候获取到,热加载 386 | // 时候会重新赋值 387 | function getCurrentSeconds() { 388 | return Math.round(new Date().getTime() / 1000); 389 | } 390 | export function getElapsedSeconds() { 391 | return getCurrentSeconds() - moduleStartTime; 392 | } 393 | //(2)开启HMR,如果添加HotModuleReplacement插件,webpack-dev-server添加--hot 394 | if (module.hot) { 395 | const data = module.hot.data || {}; 396 | //(3)如果module.hot.dispose将当前的数据放到了data中可以通过module.hot.data获取 397 | if (data.moduleStartTime) 398 | moduleStartTime = data.moduleStartTime; 399 | //(4)我们首次会将当前模块加载的时间传递到热加载后的模块中,从而热加载后的moduleStartTime 400 | // 会一直是首次加载模块的时间 401 | module.hot.dispose((data) => { 402 | data.moduleStartTime = moduleStartTime; 403 | }); 404 | } 405 | ``` 406 | 在time.js中我们会在每次热加载的时候保存模块首次加载的时间,这是实现热加载后页面time不改变的关键代码。下面再给出index.js的代码: 407 | ```js 408 | import * as dom from './dom'; 409 | import * as time from './time'; 410 | import pulse from './pulse'; 411 | require('./styles.scss'); 412 | const UPDATE_INTERVAL = 1000; // milliseconds 413 | const intervalId = window.setInterval(() => { 414 | dom.writeTextToElement('upTime', time.getElapsedSeconds() + ' seconds'); 415 | dom.writeTextToElement('lastPulse', pulse()); 416 | }, UPDATE_INTERVAL); 417 | // Activate Webpack HMR 418 | if (module.hot) { 419 | module.hot.accept(); 420 | // dispose handler 421 | module.hot.dispose(() => { 422 | window.clearInterval(intervalId); 423 | }); 424 | } 425 | ``` 426 | 你可能有这样的疑问:"如果我们修改index.js后,我们页面的时间是否就会刷新呢?"答案是:`"不会"`!这是因为:当你改变了index.js的代码,虽然我们会调用clearInterval,但是该模块也是支持热加载的,所以热加载后又会执行window.setInterval,而我们time.js返回的依然是正确的时间。 427 | 428 | 关于module.hot.dispose有一点需要注意: 429 | ```js 430 | module.hot.dispose(function(){ 431 | console.log('1'); 432 | window.clearInterval(intervalId); 433 | }) 434 | ``` 435 | 假如在修改index.js之前,我们的代码如上,此时我们修改代码为如下: 436 | ```js 437 | module.hot.dispose(function(){ 438 | console.log('2'); 439 | window.clearInterval(intervalId); 440 | }) 441 | ``` 442 | 此时你会发现打印出来的结果为1而不是2,即打印的结果是HMR完成之前的代码。这可能是因为这个函数是为了清除持久化资源或者清除定时器等操作而设计的。完整的代码逻辑你一定要[查看这里](https://github.com/liangklfangl/wcf/blob/master/test/index.js)并运行一下,这样可能更好的了解HMR的逻辑。 443 | 444 | #### 2.4 HMR牵涉到其他函数与概念 445 | 446 | ##### 2.4.1 accept函数 447 | ```js 448 | accept(dependencies: string[], callback: (updatedDependencies) => void) => void 449 | accept(dependency: string, callback: () => void) => void 450 | ``` 451 | 此时表示,我们这个模块支持HMR,任何其依赖的模块变化都会被捕捉到。当依赖的模块更新后回调函数被调用。当然,如果是下面这种方式: 452 | ```js 453 | accept([errHandler]) => void 454 | ``` 455 | 那么表示我们接受当前模块`所有`依赖的模块的代码更新,而且这种更新不会冒泡到父级中去。这当我们模块没有导出任何东西的情况下有用(因为没有导出,所以也就没有父级调用)。 456 | 457 | ##### 2.4.2 decline函数 458 | 上面的例子中我们的dom.js是如下方式写的: 459 | ```js 460 | import $ from 'jquery'; 461 | export function writeTextToElement(id, text) { 462 | $('#' + id).text(text); 463 | } 464 | if (module.hot) { 465 | module.hot.decline('jquery');//不接受jquery更新 466 | } 467 | ``` 468 | 其中decline方法签名如下: 469 | ```js 470 | decline(dependencies: string[]) => void 471 | decline(dependency: string) => void 472 | ``` 473 | 这表明我们不会接受特定模块的更新,如果该模块更新了,那么更新失败同时失败代码为"decline"。而上面的代码表明我们不会接受jquery模块的更新。当然也可以是如下模式: 474 | ```js 475 | decline() => void 476 | ``` 477 | 这表明我们当前的模块是不会更新的,也就是不会HMR。如果更新了那么错误代码为"decline"; 478 | 479 | ##### 2.4.3其中dispose函数 480 | 函数签名如下: 481 | 482 | ```js 483 | dispose(callback: (data: object) => void) => void 484 | addDisposeHandler(callback: (data: object) => void) => void 485 | ``` 486 | 这表示我们会添加一个一次性的处理函数,这个函数在当前模块更新后会被调用。此时,你需要移除或者销毁一些持久的资源,如果你想将当前的状态信息转移到更新后的模块中,此时可以添加到data对象中,以后可以通过module.hot.data访问。如下面的time.js例子用于保存指定模块实例化的时间,从而防止模块更新后数据丢失(刷新后还是会丢失的)。 487 | ```js 488 | if (module.hot) { 489 | module.hot.accept(); 490 | // dispose handler 491 | module.hot.dispose(() => { 492 | window.clearInterval(intervalId); 493 | //这是更新之前的模块的intervalId,而不是更新后的新的模块 494 | }); 495 | } 496 | ``` 497 | ##### 2.4.4 HMR的module.hot.status() => string 498 | 该函数可以获取到HMR当前所处的状态,可以是*idle, check, watch, watch-delay, prepare, ready, dispose, apply, abort or fail*中的任意个: 499 | - idle 500 | 这个状态表明,当然HMR处于空闲状态,可以调用check。调用后状态为check 501 | - check 502 | HMR在检查模块更新。如果没有模块更新,那么重新回到idle状态。如果有更新那么会依次经过prepare,dispose,apply然后重新回到idle状态 503 | - watch 504 | HMR当前处于监听模式,他可以自动接收到更新。如果接受到更新,那么就会进入*watch-delay*模式,然后等待机会开始更新操作。如果开始更新,那么会依次经过prepare, dispose 和 apply状态。如果在更新的时候又监听到文件更新,那么重新回到watch或者watch-delay状态 505 | - prepare 506 | 表明HMR在准备更新。比如在下载一些资源,如webpack更新后的*资源下载*过程。 507 | - ready 508 | 可以开始更新了,需要手动调用apply方法去继续更新操作 509 | - dispose 510 | HMR在调用模块自己的dispose方法,并开始更新后的模块替换操作 511 | - apply 512 | HMR在调用*被替换后(dispose)*的模块的父级模块的accept方法,当然模块自己必须能够被dispose 513 | - abort 514 | 更新无法被进一步apply,但是文件处于更新之前的一致状态 515 | - fail 516 | 在更新过程中抛出了异常,当前的文件状态处于不一致状态。系统需要重启 517 | 518 | 上面的源码分析中你也看到了调用方式: 519 | ```js 520 | module.hot.status() 521 | ``` 522 | 523 | ##### 2.4.5 HMR的apply方法 524 | 其中调用的方式如下: 525 | ```js 526 | module.hot.apply(options).then(outdatedModules => { 527 | // outdated modules... 528 | }).catch(error => { 529 | // catch errors 530 | }); 531 | ``` 532 | 其中options可以包含下面的这些参数: 533 |
534 |  1.ignoreUnaccepted表示调用accept时没有指定的模块。如果accpet没有参数,接受任何模块更新
535 |  2.ignoreDeclined表示调用decline明确指定不需要检查的模块
536 |  3.ignoreErrored忽略在调用accept时候抛出的错误
537 |  4.onDeclined函数,接受那些decline指定的模块
538 |  5.onUnaccepted函数,接受accept中没有指定的模块
539 |  6.onAccepted函数,接受accept中指定的模块
540 |  7.onDisposed函数,接受那些被dispose的模块
541 |  8.onErrored函数,接受那些出错的模块   
542 | 
543 | 每一个函数接受到的参数为如下类型: 544 | ```js 545 | { 546 | type: "self-declined" | "declined" | 547 | "unaccepted" | "accepted" | 548 | "disposed" | "accept-errored" | 549 | "self-accept-errored" | "self-accept-error-handler-errored", 550 | moduleId: 4, 551 | // The module in question. 552 | dependencyId: 3, 553 | // For errors: the module id owning the accept handler. 554 | chain: [1, 2, 3, 4], 555 | // For declined/accepted/unaccepted: the chain from where the update was propagated. 556 | // 这个chain表示更新冒泡的顺序 557 | parentId: 5, 558 | // For declined: the module id of the declining parent 559 | outdatedModules: [1, 2, 3, 4], 560 | // For accepted: the modules that are outdated and will be disposed 561 | outdatedDependencies: { 562 | // For accepted: The location of accept handlers that will handle the update 563 | 5: [4] 564 | }, 565 | error: new Error(...), 566 | // For errors: the thrown error 567 | originalError: new Error(...) 568 | // For self-accept-error-handler-errored: 569 | // the error thrown by the module before the error handler tried to handle it. 570 | } 571 | ``` 572 | ##### 2.4.6 hotUpdateChunkFilename vs hotUpdateMainFilename 573 | 当你修改了test目录下的文件的时候,比如修改了scss文件,此时你会发现在页面中多出了一个script元素,内容如下: 574 | ```html 575 | 576 | ``` 577 | 其中内容是: 578 | ```js 579 | webpackHotUpdate(0,{ 580 | /***/ 15: 581 | /***/ (function(module, exports, __webpack_require__) { 582 | exports = module.exports = __webpack_require__(46)(); 583 | // imports 584 | // module 585 | exports.push([module.i, "html {\n border: 1px solid yellow;\n background-color: pink; }\n\nbody {\n background-color: lightgray;\n color: black; }\n body div {\n font-weight: bold; }\n body div span {\n font-weight: normal; }\n", ""]); 586 | // exports 587 | 588 | /***/ }) 589 | }) 590 | //# sourceMappingURL=0.188304c98f697ecd01b3.hot-update.js.map 591 | ``` 592 | 从内容你也可以看出,只是将我们修改的模块push到exports对象中!而hotUpdateChunkFilename就是为了让你能够执行script的src中的值的!而同样的hotUpdateMainFilename是一个json文件用于指定哪些模块发生了变化,在output目录下。 593 | 594 | ##### 2.5 less/scss/css的热加载 595 | 要实现less/scss/css的热加载是非常容易的,我们可以直接使用style-loader来完成(在开发模式下,生产模式下不建议使用)。比如在开发模式下对于css的加载可以配置如下的loader: 596 | ```js 597 | module: { 598 | rules: [ 599 | { 600 | test: /\.css$/, 601 | use: ['style-loader', 'css-loader'] 602 | } 603 | ] 604 | } 605 | ``` 606 | 对于style-loader热加载的你可以直接[点击这里](https://github.com/webpack-contrib/style-loader/blob/master/index.js#L24),其中原理上面都说过了,如果不懂,请仔细阅读上面的HMR的部分。而至于less/scss因为最终都会打包成为css,所以其实和css的热加载是一样的道理。 607 | 608 | -------------------------------------------------------------------------------- /09-教你写一个webpack的loader.md: -------------------------------------------------------------------------------- 1 | ### 1.本章概述 2 | 通过前面章节的内容,你对于 Webpack 的基础知识应该有了一个总体的了解。下面我将以一个具体的实例来教你如何写一个 Webpack 的 Loader。这个 Loader 本身复用性并不高,他是我在开发中遇到的一个实际问题。通过这个例子的论述,我想你对于如何写一个 Webpack 的 Loader应该会有一个整体的把握。下面我们开始本章节的内容 3 | 4 | ### 2.写一个Webpack的Loader 5 | 假如我们如下的markdown文件,文件主要内容为如下(完整内容点击[这里](https://github.com/liangklfangl/astexample/blob/master/demo.md)): 6 | ```jsx 7 | import { Button } from 'antd'; 8 | ReactDOM.render( 9 |
10 | 11 | 12 | 13 | 14 |
15 | , mountNode); 16 | ``` 17 | 此时,我们希望使用 Webpack 的 Loader来加载这个markdown文件的内容。那么很显然,我们就是要写一个相应的Loader,比如我们在webpack.config.js中添加如下的配置: 18 | ```js 19 | module.exports = { 20 | module:{ 21 | rule:[ 22 | { 23 | test: /\.md$/, 24 | loaders: [ 25 | require.resolve("babel-loader"), 26 | require.resolve("./markdownDemoLoader.js") 27 | ] 28 | }] 29 | } 30 | } 31 | ``` 32 | 其中markdownDemoLoader.js就是我们需要完成的 Webpack 的 Loader。在这个Loader中我们有如下的代码: 33 | ```js 34 | const loaderUtils = require('loader-utils'); 35 | const Grob = require('grob-files'); 36 | const {p2jsonml} = require('./utils/pc2jsonml'); 37 | const transformer = require('./utils/transformer'); 38 | const Smangle = require('string-mangle'); 39 | const generator = require('babel-generator').default; 40 | const pwd = process.cwd(); 41 | const fs = require('fs'); 42 | const path = require('path'); 43 | const util = require('util'); 44 | /** 45 | *第一个参数是markdown的内容 46 | */ 47 | module.exports = function markdown2htmlPreview (content){ 48 | //缓存该模块 49 | if (this.cacheable) { 50 | this.cacheable(); 51 | } 52 | const loaderIndex = this.loaderIndex; 53 | //打印this可以得到所有的信息,这里得到md处理文件的loader数组中,当前loader所在的下标 54 | const query = loaderUtils.getOptions(this); 55 | const lang = query&&query.lang || 'react-demo'; 56 | //获取Loader的query字段 57 | const processedjsonml=Smangle.stringify(p2jsonml(content)); 58 | //得到jsonml 59 | const astProcessed = `module.exports = ${processedjsonml}`; 60 | //每一个Loader导出的内容都是module.exports 61 | const res = transformer(astProcessed,lang); 62 | //将得到的jsonml内容进一步的处理 63 | const inputAst = res.inputAst; 64 | const imports = res.imports; 65 | for (let k = 0; k < imports.length; k++) { 66 | inputAst.program.body.unshift(imports[k]); 67 | const code = generator(inputAst, null, content).code; 68 | //回到ES6代码 69 | const processedCode= 'const React = require(\'react\');\n' + 70 | 'const ReactDOM = require(\'react-dom\');\n'+ 71 | code; 72 | return processedCode; 73 | } 74 | } 75 | ``` 76 | 我们现在看看Loader编写中常用的方法: 77 | ```js 78 | if (this.cacheable) { 79 | this.cacheable(); 80 | } 81 | ``` 82 | 我们知道Loader加载的结果默认是缓存的,如果你不想缓存可以使用*this.cacheable(false);*去阻止缓存。一个可以缓存的Loader必须满足一定的条件,即当输入和模块依赖关系没有发生变化的情况下,输出默认是确定的。也就是说,这个模块除了通过*this.addDependency*添加的模块依赖以外没有任何其他的模块依赖。 83 | 84 | ```js 85 | const loaderIndex = this.loaderIndex; 86 | ``` 87 | 这个表示当前Loader在加载特定文件的时候所在的下标。 88 | 89 | 在上面这个Loader中,我们首先原样传入markdown文件的内容,然后将它转化为jsonml,我们看看上面的jsonml.js的内容: 90 | ```js 91 | const markTwain = require('mark-twain'); 92 | const path = require('path'); 93 | function p2jsonml(fileContent){ 94 | const markdown = markTwain(fileContent); 95 | return markdown; 96 | }; 97 | module.exports = { 98 | p2jsonml 99 | } 100 | ``` 101 | 转化为jsonml格式以后,我们将会得到如下的内容: 102 | ```js 103 | module.exports = { "content": [ "article", [ "h3", "1.mark-twain解析出来的无法解析成为ast" ], [ "pre", { "lang": "jsx" }, [ "code", "import { Button } from 'antd';\nReactDOM.render(\n 104 | 105 | \n 206 | 208 |
209 | 211 | 213 | ; 214 | }], 215 | "meta": {} 216 | }; 217 | ``` 218 | 此时的模块依然是ES6格式与jsx混合的代码,我们需要进一步配合babel来处理将它转化为ES5代码,所以我们的webpack.config.js中才会在该插件后引入babel-loader来对代码进行进一步的打包。那么你可能会想,就算babel打包后,得到上面这样的代码会有什么用?我给你看看,在前端我是如何将这样的代码转化为React类型的: 219 | ```js 220 | import ReactDOM from "react-dom"; 221 | import React from "react"; 222 | const content = require('../../demos/basic.md'); 223 | const converters = [ 224 | [ 225 | function(node) { return typeof node === 'function'; }, 226 | function(node, index) { 227 | return React.cloneElement(node(), { key: index }); 228 | } 229 | ] 230 | ]; 231 | //(2)converters可以引入一个库来完成 232 | const JsonML = require('jsonml.js/lib/utils'); 233 | const toReactComponent = require('jsonml-to-react-component'); 234 | ReactDOM.render(toReactComponent(content.content,converters), document.getElementById('react-content')); 235 | 236 | ``` 237 | 是不是很容易理解了,我们Loader处理后的代码,最后会被我原样转化为React的组件并在页面中展示,当然这个过程必须经过[jsonml-to-react-element](https://github.com/benjycui/jsonml-to-react-element)的转化。所以说,我们的Loader完成了markdown文件类型到我们最后的javascript模块的转化。这就是 Webpack 中 Loader 的强大作用。 238 | 239 | ### 3.Webpack的Loader常见配置 240 | 在 Webpack 中 Loader 就是一个模块,该模块导出一个函数。我们的 Webpack 的 Loader 机制会调用这些函数,并将前一个函数的处理结果传递给下一个处理函数,而第一个函数接受到的就是文件的原始内容,比如上面的这个例子就是 markdown 文件的原样内容(与通过 Nodejs 中 fs 模块读取的内容一致)。在这个函数中的 *this* 对象会有各种有用的方法,你可以通过这些方法将 Loader 的调用形式转化为异步的(this.async方法),或者得到该 Loader 的配置参数等等。 241 | 242 | 第一个 Loader 会被传入文件的原始内容,最后的一个 Loader 必须返回一个结果,这个结果可以是 String 或者 Buffer 类型,他们代表 JavaScript 模块的源代码。同时,一个可选的返回值就是 SourceMap 。如果只要返回一个值,那么可以是同步模式,如果需要返回多个值,那么必须调用*this.callback()*方法。在异步模式下,*this,async()*必须调用来通知 Webpack 的 Loader 执行器等待异步返回的结果。它返回*this.callback()*,同时该 Loader 必须返回 undefined同时调用该回调。 243 | 244 | #### 3.1 同步的Loader 245 | ```js 246 | module.exports = function(content) { 247 | return someSyncOperation(content); 248 | }; 249 | ``` 250 | 下面是同步的Loader并返回多个值: 251 | ```js 252 | module.exports = function(content) { 253 | this.callback(null, someSyncOperation(content), sourceMaps, ast); 254 | //如果要返回多个值必须通过this.callback方法 255 | return; 256 | // always return undefined when calling callback() 257 | // 当调用this.callback时候必须返回undefined 258 | }; 259 | ``` 260 | #### 3.2 异步Loader 261 | ```js 262 | module.exports = function(content) { 263 | var callback = this.async(); 264 | //*this,async()*必须调用来通知Webpack的Loader执行器等待异步返回的结果 265 | someAsyncOperation(content, function(err, result) { 266 | //必须返回undefined或者回调该callback函数 267 | if(err) return callback(err); 268 | callback(null, result); 269 | }); 270 | }; 271 | 272 | ``` 273 | 下面是异步的Loader并返回多个值的情况: 274 | ```js 275 | module.exports = function(content) { 276 | var callback = this.async(); 277 | //异步Loader 278 | someAsyncOperation(content, function(err, result, sourceMaps, ast) { 279 | if(err) return callback(err); 280 | callback(null, result, sourceMaps, ast); 281 | }); 282 | }; 283 | ``` 284 | 285 | #### 3.3 "Raw" Loader 286 | 默认情况下,源文件的内容会被转化为UTF-8的字符串并传给我们的 Loader 。通过设置 *raw* 这个标志,那么我们的 Loader 会接受到一个 Buffer 对象。每一个Loader 都允许将他的结果以 String 或者 Buffer 的类型传递给下一个 Loader ,而 Webpack 可以将它在两者之间正常转化: 287 | ```js 288 | module.exports = function(content) { 289 | assert(content instanceof Buffer); 290 | return someSyncOperation(content); 291 | // return value can be a `Buffer` too 292 | // This is also allowed if loader is not "raw" 293 | }; 294 | module.exports.raw = true; 295 | ``` 296 | 297 | #### 3.4 Pitching Loader 298 | 我们的Loader默认都是从右边向左边执行的,但是在很多情况下,我们可能并不关心前一个Loader的执行结果或者输入资源。我们仅仅关系元数据,我们的Loader上的*pitch*方法就是在 Loader 被调用之前从左边往右边执行的。如果某一个Loader的pitch方法输出一个结果,那么打包过程就是逆转,同时跳过其他的Loader,并继续执行左侧的 Loader(左侧的Loader最后执行)。同时*data*可以在 pitch 方法和常规调用之间传递: 299 | ```js 300 | module.exports = function(content) { 301 | return someSyncOperation(content, this.data.value); 302 | }; 303 | module.exports.pitch = function(remainingRequest, precedingRequest, data) { 304 | if(someCondition()) { 305 | // fast exit 306 | // 此时允许我们只执行左侧的Loader而忽略后续的Loader 307 | return "module.exports = require(" + JSON.stringify("-!" + remainingRequest) + ");"; 308 | } 309 | data.value = 42; 310 | }; 311 | ``` 312 | 比如我们的[style-loader](https://github.com/liangklfang/style-loader/blob/master/index.js#L8)就指定了该pitch方法。 313 | 314 | ### 4.Webpack的Loader配置 315 | 一个 Webpack 的 Loader 的上下文表示在 Loader 中的 this 对象具有的那些属性,假如有如下的例子: 316 | ```js 317 | require("./loader1?xyz!loader2!./resource?rrr"); 318 | ``` 319 | 假如我们在*/abc/file.js*这个文件中调用了上面的 require 方法。我们分析下常用的属性: 320 | 321 | - this.version 322 | 323 | 这表示 Loader 的 API 的版本。当前版本是2,该参数可用于向后兼容。使用 this.version 你可以指定自定义逻辑。 324 | 325 | - this.context 326 | 327 | 表示当前模块所在的目录。通过这个参数你可以获取该目录下的其他内容。在上面的例子中就是*/abc*这个目录。 328 | 329 | - this.request 330 | 331 | 已经解析后的请求字符串,比如上面的例子就是*"/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr"*。即,特定的Loader 已经转化为绝对路径。 332 | 333 | - this.query 334 | 335 | 如果一个 Loader 配置了 Options 对象,那么该参数就是指向这个对象。如果该 Loader 没有 Options 参数,但是配置了 query 字符串,那么该参数就是查询字符串,并以?开头。比如开头的例子可以通过 Options 来配置 Loader 具备的参数: 336 | ```js 337 | module.exports = { 338 | module:{ 339 | rule:[ 340 | { test: /\.md$/, use:[{ 341 | loader:'babel-loader' 342 | },{ 343 | loader:"./markdownDemoLoader.js", 344 | options:{ 345 | //指定该Loader的Options参数 346 | } 347 | }] 348 | }] 349 | } 350 | } 351 | ``` 352 | - this.callback 353 | 354 | 使用这个函数可以给我们的 Loader 返回多个结果,可以在同步或者异步的情况下调用。默认的参数类型是如下格式: 355 | ```js 356 | this.callback( 357 | err: Error | null, 358 | content: string | Buffer, 359 | sourceMap?: SourceMap, 360 | abstractSyntaxTree?: AST 361 | ); 362 | ``` 363 | 第一个参数可以是 Error 对象或者null;第二个参数是一个String或者Buffer;第三个参数可选,表示可以被该模块解析的SourceMap;第四个参数也是可选的,是一个AST抽象语法树,该参数Webpack本身会忽略,但是在不同的Loader之间共享AST可以提升打包的速度。调用该方法后必须返回undefined!关于抽象语法树的内容你可以继续阅读[这里](https://github.com/liangklfangl/astexample)的内容。 364 | 365 | - this.async 366 | 367 | 调用该方法相当于告诉我们的 Loader 执行器我们需要调用异步的结果,返回的内容就是*this.callback*。比如下面的例子: 368 | ```js 369 | module.exports = function(content) { 370 | var callback = this.async(); 371 | //*this,async()*必须调用来通知Webpack的Loader执行器等待异步返回的结果 372 | someAsyncOperation(content, function(err, result) { 373 | //必须返回undefined或者回调该callback函数 374 | if(err) return callback(err); 375 | callback(null, result); 376 | }); 377 | }; 378 | ``` 379 | 380 | - this.data 381 | 382 | 表示在 Loader 的 pitch 方法和正常打包阶段共享的数据。 383 | 384 | - this.cacheable 385 | 386 | 默认情况下,每一个Loader加载的结果都是可以缓存的。你可以在调用该方法的时候传入false显示要求Loader不要缓存结果。一个可以缓存的Loader必须满足一定的条件,即当输入和依赖关系没有发生变化的情况下,输出必须是确定的。也就是说,该Loader除了*this.addDependency*指定的依赖以外,不能有其他的依赖模块。 387 | 388 | - this.loaders 389 | 390 | 表示一个Loader数组,在pitch阶段是可以修改的。如: 391 | ```js 392 | loaders = [{request: string, path: string, query: string, module: function}] 393 | ``` 394 | 比如下面的例子: 395 | ```js 396 | [ 397 | { 398 | request: "/abc/loader1.js?xyz", 399 | path: "/abc/loader1.js", 400 | query: "?xyz", 401 | module: [Function] 402 | }, 403 | { 404 | request: "/abc/node_modules/loader2/index.js", 405 | path: "/abc/node_modules/loader2/index.js", 406 | query: "", 407 | module: [Function] 408 | } 409 | ] 410 | ``` 411 | 412 | - this.loaderIndex 413 | 414 | 表示当前Loader所在Loaders数组中的下标,比如上面的例子中*loader1*就是0,而*loader2*就是1。 415 | 416 | - this.resource 417 | 418 | Loader加载的资源部分,包含query字段。如上面的例子就是: 419 | 420 | ```js 421 | "/abc/resource.js?rrr" 422 | ``` 423 | 424 | - this.resourcePath 425 | 426 | 表示资源文件本身,比如上面的例子就是*"/abc/resource.js"*。 427 | 428 | - this.resourceQuery 429 | 430 | 表示资源的query部分。比如上面的例子就是* "?rrr"*。 431 | 432 | - this.target 433 | 434 | 表示将当前代码打包成的文件格式。可以是"web"或者"node"。 435 | 436 | - this.webpack 437 | 438 | 如果当前模块是被Webpack打包,那么值就是true。 439 | 440 | - this.sourceMap 441 | 442 | 表示是否应该产生sourceMap。因为产生sourceMap的花销是很昂贵的,所以你需要确定是否有必要产生。 443 | 444 | - this.emitWarning 445 | 446 | 产生一个警告消息。 447 | 448 | - this.emitError 449 | 450 | 产生一个错误信息。 451 | 452 | - this.loadModule 453 | 454 | 比如下面的形式: 455 | ```js 456 | loadModule(request: string, callback: function(err, source, sourceMap, module)) 457 | ``` 458 | 解析一个特定的加载请求成为对某一个模块的加载,同时使用产生的资源,sourceMap,模块实例(同行是NormalModule)来调用所有的Loader和回调函数。这个函数可以用于获取其他模块的内容并产生结果 459 | 460 | - this.resolve 461 | 其中使用方法如下: 462 | ```js 463 | resolve(context: string, request: string, callback: function(err, result: string)) 464 | ``` 465 | 相当于通过require方法来加载一个模块。 466 | 467 | - this.addDependency 468 | 469 | 可以通过下面的方法来完成 470 | ```js 471 | addDependency(file: string) 472 | dependency(file: string) // shortcut 473 | ``` 474 | 将某一个文件作为该Loader的依赖,进而使得该文件任何变化可以被监听。比如[html-loader](https://github.com/webpack-contrib/html-loader)使用该技术来查看解析的html文件中依赖的其他含有*src*和*src-set*属性的资源,然后为这些属性添加url。 475 | 476 | - this.addContextDependency 477 | 478 | 将某一个目录作为Loader的依赖。用法如下: 479 | ```js 480 | addContextDependency(directory: string) 481 | ``` 482 | 此时,如果目录中资源发生变化,那么Loader本身的输出将会更新,上一次加载的资源的缓存失效。 483 | 484 | - this.clearDependencies 485 | 486 | 移除 Loader 所有的依赖。甚至自己和其它 Loader 的初始依赖。考虑使用 pitch。用法如下: 487 | ```js 488 | clearDependencies() 489 | ``` 490 | 但是,建议在pitch方法中完成这个功能。 491 | 492 | - emitFile 493 | 494 | 用于输出一个文件。这是 webpack 特有的方法。用法如下: 495 | ```js 496 | emitFile(name: string, content: Buffer|string, sourceMap: {...}) 497 | ``` 498 | 你可以查看[file-loader](https://github.com/webpack-contrib/file-loader/blob/master/src/index.js#L66)是如何输出一个特定的文件的。 499 | 500 | - this.fs 501 | 502 | 使用这个属性可以获取到*Compilation*实例上的*inputFileSystem*属性,其实际上是一个CachedInputFileSystem。其主要属性如下: 503 | ```js 504 | CachedInputFileSystem { 505 | fileSystem: NodeJsInputFileSystem {}, 506 | //NodeJsInputFileSystem 507 | _statStorage: 508 | //_statStorage属性,保存加载的所有的模块资源 509 | Storage { 510 | duration: 60000, 511 | running: {}, 512 | data: 513 | { '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main.js': [Object], 514 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main.js.js': [Object], 515 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main.js.json': [Object], 516 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main1.js': [Object], 517 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main1.js.js': [Object], 518 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/main1.js.json': [Object], 519 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/node_modules': [Object], 520 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules': [Object], 521 | '/Users/qinliang.ql/Desktop/node_modules': [Object], 522 | '/Users/qinliang.ql/node_modules': [Object], 523 | '/Users/node_modules': [Object], 524 | '/node_modules': [Object], 525 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk1': [Object], 526 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk1.js': [Object], 527 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk1.json': [Object], 528 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk2': [Object], 529 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk2.js': [Object], 530 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/chunk-module-assets/chunk2.json': [Object], 531 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/map.png': [Object], 532 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/map.png.js': [Object], 533 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/map.png.json': [Object], 534 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue': [Object], 535 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue.js': [Object], 536 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue.json': [Object], 537 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery': [Object], 538 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery.js': [Object], 539 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery.json': [Object], 540 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/_url-loader@0.6.2@url-loader/index.js': [Object], 541 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/_url-loader@0.6.2@url-loader/index.js.js': [Object], 542 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/_url-loader@0.6.2@url-loader/index.js.json': [Object], 543 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/index': [Object], 544 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/index.js': [Object], 545 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/index.json': [Object], 546 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/index': [Object], 547 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/index.js': [Object], 548 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/index.json': [Object], 549 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/dist/vue.runtime.common.js': [Object], 550 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/dist/vue.runtime.common.js.js': [Object], 551 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/vue/dist/vue.runtime.common.js.json': [Object], 552 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/dist/jquery.js': [Object], 553 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/dist/jquery.js.js': [Object], 554 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/jquery/dist/jquery.js.json': [Object], 555 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/process/browser.js': [Object], 556 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/process/browser.js.js': [Object], 557 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/process/browser.js.json': [Object], 558 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/webpack/buildin/global.js': [Object], 559 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/webpack/buildin/global.js.js': [Object], 560 | '/Users/qinliang.ql/Desktop/commonsChunkPlugin_Config/node_modules/webpack/buildin/global.js.json': [Object] }, 561 | levels: 562 | [ [Object] ], 563 | count: 48, 564 | interval: 565 | Timeout { 566 | _called: false, 567 | _idleTimeout: 530, 568 | _idlePrev: [Object], 569 | _idleNext: [Object], 570 | _idleStart: 685, 571 | _onTimeout: [Function: bound ], 572 | _timerArgs: undefined, 573 | _repeat: 530 }, 574 | needTickCheck: false, 575 | nextTick: null, 576 | passive: false, 577 | tick: [Function: bound ] }, 578 | //_readdirStorage 579 | _readdirStorage: 580 | Storage { 581 | duration: 60000, 582 | running: {}, 583 | data: {}, 584 | levels: 585 | [], 586 | count: 0, 587 | interval: null, 588 | needTickCheck: false, 589 | nextTick: null, 590 | passive: true, 591 | tick: [Function: bound ] }, 592 | //_readFileStorage 593 | _readFileStorage: 594 | Storage { 595 | duration: 60000, 596 | running: {}, 597 | data: 598 | { 599 | //已经删除 600 | }, 601 | levels: 602 | [ [Object]], 603 | count: 32, 604 | interval: 605 | Timeout { 606 | _called: false, 607 | _idleTimeout: 530, 608 | _idlePrev: [Object], 609 | _idleNext: [Object], 610 | _idleStart: 678, 611 | _onTimeout: [Function: bound ], 612 | _timerArgs: undefined, 613 | _repeat: 530 }, 614 | needTickCheck: false, 615 | nextTick: null, 616 | passive: false, 617 | tick: [Function: bound ] }, 618 | //_statStorage与_readdirStorage,_readFileStorage 619 | _readJsonStorage: 620 | Storage { 621 | duration: 60000, 622 | running: {}, 623 | data: 624 | { 625 | //已经删除 626 | }, 627 | levels: 628 | [ [Object] ], 629 | count: 23, 630 | interval: 631 | Timeout { 632 | _called: false, 633 | _idleTimeout: 530, 634 | _idlePrev: [Object], 635 | _idleNext: [Object], 636 | _idleStart: 679, 637 | _onTimeout: [Function: bound ], 638 | _timerArgs: undefined, 639 | _repeat: 530 }, 640 | needTickCheck: false, 641 | nextTick: null, 642 | passive: false, 643 | tick: [Function: bound ] }, 644 | //_readlinkStorage 645 | _readlinkStorage: 646 | Storage { 647 | duration: 60000, 648 | running: {}, 649 | data: 650 | { 651 | //已经删除 652 | }, 653 | levels: 654 | [ [Object] ], 655 | count: 55, 656 | interval: 657 | Timeout { 658 | _called: false, 659 | _idleTimeout: 530, 660 | _idlePrev: [Object], 661 | _idleNext: [Object], 662 | _idleStart: 684, 663 | _onTimeout: [Function: bound ], 664 | _timerArgs: undefined, 665 | _repeat: 530 }, 666 | needTickCheck: false, 667 | nextTick: null, 668 | passive: false, 669 | tick: [Function: bound ] }, 670 | _stat: [Function: bound bound ], 671 | _statSync: [Function: bound bound ], 672 | _readdir: [Function: bound readdir], 673 | _readdirSync: [Function: bound readdirSync], 674 | _readFile: [Function: bound bound readFile], 675 | _readFileSync: [Function: bound bound ], 676 | _readJson: [Function: bound ], 677 | _readJsonSync: [Function: bound ], 678 | _readlink: [Function: bound bound ], 679 | _readlinkSync: [Function: bound bound ] } 680 | ``` 681 | 所以该对象其实就包含了_statStorage,_readdirStorage,_readFileStorage,_readJsonStorage,_readlinkStorage等几个存储相关的字段。而至于compiler.outputFileSystem你可以查看[webpack-dev-middleware](https://github.com/liangklfang/webpack-dev-middleware#4该插件的compileroutputfilesystem是一个memoryfilesystem实例)是如何使用它来将输出资源保存到内存中而不是文件系统中的。上面这个输出实例来自于[这个文件](https://github.com/liangklfangl/commonsChunkPlugin_Config/blob/master/chunk-module-assets/FileListPlugin.js),你可以自己运行并查看结果。 682 | 683 | ### 5.本章总结 684 | 本章节,我们通过一个 markdown 的 loader 的具体事例展了如何写一个 Webpack 的 loader。同时也给出了 loader 常见的配置和用法。通过本章节的学习,你应该能够写一个基础的 Webpack 的 loader。本章节的完整实例代码你可以查看[Webpack 操作 AST](https://github.com/liangklfangl/astexample),但是因为这个 Loader 牵涉到了如何操作我们的 AST 语法树,如果你对于这部分内容比较陌生,那么你可以查看我推荐给你的这个文章。 685 | -------------------------------------------------------------------------------- /06-webpack中的compiler和compilation对象.md: -------------------------------------------------------------------------------- 1 | ##### 本章概述 2 | 在本章节我会首先通过具体的实例向你展示我们的compilation对象具有的常用属性,并告诉你这些属性具体的含义。其中包含chunks,assets,modules等等。最后,我会深入的分析compiler和compilation对象具有的各种钩子方法。通过本章节的学习,你将会了解webpack中module,chunk等重要的概念,并对webpack打包过程有一个宏观的把握。 3 | 4 | ##### 1.webpack的compilation常用内容 5 | ##### 1.1 chunk的内容 6 | 为了更好的理解下面的概念,我先给大家看看webpack中所谓的chunk都包含哪些内部属性,希望能对大家理解chunk有一定的帮助,下面展示的就是一个chunk的实例。 7 | ```js 8 | compilation.getStats().toJson().chunks 9 | //获取compilation所有的chunks: 10 | ``` 11 | chunks所有的内容如下: 12 | ```js 13 | [ { id: 0, 14 | //chunk id 15 | rendered: true, 16 | //https://github.com/liangklfangl/commonchunkplugin-source-code 17 | initial: false, 18 | //require.ensure产生的chunk,非initial 19 | //initial表示是否是在页面初始化就需要加载的模块,而不是按需加载的模块 20 | entry: false, 21 | //是否含有webpack的runtime环境,通过CommonChunkPlugin处理后,runtime环境被提到最高层级的chunk 22 | recorded: undefined, 23 | extraAsync: false, 24 | size: 296855, 25 | //chunk大小,比特 26 | names: [], 27 | //require.ensure不是通过webpack配置的,所以chunk的names是空 28 | files: [ '0.bundle.js' ], 29 | //该chunk产生的输出文件,即输出到特定文件路径下的文件名称 30 | hash: '42fbfbea594ba593e76a', 31 | //chunk的hash,即chunkHash 32 | parents: [ 2 ], 33 | //父级chunk的id值 34 | origins: [ [Object] ] 35 | //该chunk是如何产生的 36 | }, 37 | { id: 1, 38 | rendered: true, 39 | initial: false, 40 | entry: false, 41 | recorded: undefined, 42 | extraAsync: false, 43 | size: 297181, 44 | names: [], 45 | files: [ '1.bundle.js' ], 46 | hash: '456d05301e4adca16986', 47 | parents: [ 2 ], 48 | origins: [ [Object] ] }, 49 | { id: 2, 50 | rendered: true, 51 | initial: true, 52 | entry: false, 53 | recorded: undefined, 54 | extraAsync: false, 55 | size: 687, 56 | names: [ 'main' ], 57 | files: [ 'bundle.js' ], 58 | hash: '248029a0cfd99f46babc', 59 | parents: [ 3 ], 60 | origins: [ [Object] ] }, 61 | { id: 3, 62 | rendered: true, 63 | initial: true, 64 | entry: true, 65 | recorded: undefined, 66 | extraAsync: false, 67 | size: 0, 68 | names: [ 'vendor' ], 69 | files: [ 'vendor.bundle.js' ], 70 | hash: 'fbf76c7c330eaf0de943', 71 | parents: [], 72 | origins: [] } ] 73 | ``` 74 | 而上面的每一个chunk还有一个origins参数,其含有的内容如下,它描述了某一个chunk是如何产生的: 75 | ```js 76 | { 77 | "loc": "", // Lines of code that generated this chunk 78 | "module": "(webpack)\\test\\browsertest\\lib\\index.web.js", // Path to the module 79 | "moduleId": 0, // The ID of the module 80 | "moduleIdentifier": "(webpack)\\test\\browsertest\\lib\\index.web.js", // Path to the module 81 | "moduleName": "./lib/index.web.js", // Relative path to the module 82 | "name": "main", // The name of the chunk 83 | "reasons": [ 84 | // A list of the same `reasons` found in module objects 85 | ] 86 | } 87 | ``` 88 | 如果对于chunk中某些属性不懂的,可以点击我这里对于[webpack-common-chunk-plugin的分析](https://github.com/liangklfangl/commonchunkplugin-source-code)。下面是给出的几个关于chunks的例子: 89 | 90 | 例1:html-webpack-plugin中就使用到了多个chunks属性,如names,initial等 91 | ```js 92 | //该chunk要被选中的条件是:有名称,不是懒加载,在includedChunks中但是不在excludedChunks中 93 | HtmlWebpackPlugin.prototype.filterChunks = function (chunks, includedChunks, excludedChunks) { 94 | return chunks.filter(function (chunk) { 95 | var chunkName = chunk.names[0]; 96 | // This chunk doesn't have a name. This script can't handled it. 97 | //通过require.ensure产生的chunk不会被保留,names是一个数组 98 | if (chunkName === undefined) { 99 | return false; 100 | } 101 | // Skip if the chunk should be lazy loaded 102 | //如果是require.ensure产生的chunk直接忽略 103 | if (!chunk.initial) { 104 | return false; 105 | } 106 | // Skip if the chunks should be filtered and the given chunk was not added explicity 107 | //这个chunk必须在includedchunks里面 108 | if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) { 109 | return false; 110 | } 111 | // Skip if the chunks should be filtered and the given chunk was excluded explicity 112 | //这个chunk不能在excludedChunks中 113 | if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) { 114 | return false; 115 | } 116 | // Add otherwise 117 | return true; 118 | }); 119 | }; 120 | ``` 121 | 122 | 例2:通过id对chunks进行排序 123 | 124 | ```js 125 | /** 126 | * Sorts the chunks based on the chunk id. 127 | * 128 | * @param {Array} chunks the list of chunks to sort 129 | * @return {Array} The sorted list of chunks 130 | * entry chunk在前,两个都是entry那么id大的在前 131 | */ 132 | module.exports.id = function (chunks) { 133 | return chunks.sort(function orderEntryLast (a, b) { 134 | if (a.entry !== b.entry) { 135 | return b.entry ? 1 : -1; 136 | } else { 137 | return b.id - a.id; 138 | } 139 | }); 140 | }; 141 | ``` 142 | 143 | 例3:通过chunk.parents(全部是parentId数组)来获取拓排序 144 | ```js 145 | /* 146 | Sorts dependencies between chunks by their "parents" attribute. 147 | This function sorts chunks based on their dependencies with each other. 148 | The parent relation between chunks as generated by Webpack for each chunk 149 | is used to define a directed (and hopefully acyclic) graph, which is then 150 | topologically sorted in order to retrieve the correct order in which 151 | chunks need to be embedded into HTML. A directed edge in this graph is 152 | describing a "is parent of" relationship from a chunk to another (distinct) 153 | chunk. Thus topological sorting orders chunks from bottom-layer chunks to 154 | highest level chunks that use the lower-level chunks. 155 | 156 | @param {Array} chunks an array of chunks as generated by the html-webpack-plugin. 157 | It is assumed that each entry contains at least the properties "id" 158 | (containing the chunk id) and "parents" (array containing the ids of the 159 | parent chunks). 160 | @return {Array} A topologically sorted version of the input chunks 161 | 因为最上面的通过commonchunkplugin产生的chunk具有webpack的runtime,所以排列在前面 162 | */ 163 | module.exports.dependency = function (chunks) { 164 | if (!chunks) { 165 | return chunks; 166 | } 167 | // We build a map (chunk-id -> chunk) for faster access during graph building. 168 | // 通过chunk-id -> chunk这种Map结构更加容易绘制图 169 | var nodeMap = {}; 170 | chunks.forEach(function (chunk) { 171 | nodeMap[chunk.id] = chunk; 172 | }); 173 | // Next, we add an edge for each parent relationship into the graph 174 | var edges = []; 175 | chunks.forEach(function (chunk) { 176 | if (chunk.parents) { 177 | // Add an edge for each parent (parent -> child) 178 | chunk.parents.forEach(function (parentId) { 179 | // webpack2 chunk.parents are chunks instead of string id(s) 180 | var parentChunk = _.isObject(parentId) ? parentId : nodeMap[parentId]; 181 | // If the parent chunk does not exist (e.g. because of an excluded chunk) 182 | // we ignore that parent 183 | if (parentChunk) { 184 | edges.push([parentChunk, chunk]); 185 | } 186 | }); 187 | } 188 | }); 189 | // We now perform a topological sorting on the input chunks and built edges 190 | return toposort.array(chunks, edges); 191 | }; 192 | 193 | ``` 194 | 通过这种方式可以把各个chunk公有的模块排列在前面,从而提前加载,这是合理的! 195 | ##### 1.2 assets内容 196 | ```js 197 | compilation.getStats().toJson().assets 198 | // 获取compilation所有的assets: 199 | ``` 200 | assets内部结构如下: 201 | 202 |
203 |  [ { name: '0.bundle.js',
204 |     size: 299109,
205 |     chunks: [ 0, 3 ],
206 |     
207 |     chunkNames: [],
208 |     emitted: undefined,
209 |     isOverSizeLimit: undefined },
210 |   { name: '1.bundle.js',
211 |     size: 299469,
212 |     chunks: [ 1, 3 ],
213 |     chunkNames: [],
214 |     emitted: undefined,
215 |     isOverSizeLimit: undefined },
216 |   { name: 'bundle.js',
217 |     
218 |     size: 968,
219 |     
220 |     chunks: [ 2, 3 ],
221 |     
222 |     chunkNames: [ 'main' ],
223 |     
224 |     emitted: undefined,
225 |     
226 |     isOverSizeLimit: undefined },
227 |   { name: 'vendor.bundle.js',
228 |     size: 5562,
229 |     chunks: [ 3 ],
230 |     chunkNames: [ 'vendor' ],
231 |     emitted: undefined,
232 |     isOverSizeLimit: undefined }]
233 | 
234 | 235 | 上次看到webpack-dev-server源码的时候看到了如何判断该assets是否变化的判断,其就是通过上面的emitted来判断的: 236 | 237 | ```js 238 | compiler.plugin("done", function(stats) { 239 | this._sendStats(this.sockets, stats.toJson(clientStats)); 240 | //clientStats表示客户端stats要输出的内容过滤 241 | this._stats = stats; 242 | }.bind(this)); 243 | Server.prototype._sendStats = function(sockets, stats, force) { 244 | if(!force && 245 | stats && 246 | (!stats.errors || stats.errors.length === 0) && 247 | stats.assets && 248 | stats.assets.every(function(asset) { 249 | return !asset.emitted; 250 | //每一个asset都是没有emitted属性,表示没有发生变化。如果发生变化那么这个assets肯定有emitted属性。所以emitted属性表示是否又重新生成了一遍assets资源 251 | }) 252 | ) 253 | return this.sockWrite(sockets, "still-ok"); 254 | this.sockWrite(sockets, "hash", stats.hash); 255 | //正常情况下首先发送hash,然后发送ok 256 | if(stats.errors.length > 0) 257 | this.sockWrite(sockets, "errors", stats.errors); 258 | else if(stats.warnings.length > 0) 259 | this.sockWrite(sockets, "warnings", stats.warnings); 260 | else 261 | //发送hash后再发送ok 262 | this.sockWrite(sockets, "ok"); 263 | } 264 | ``` 265 | 我们下面分析下什么是assets?其实每一个asset表示输出到webpack输出路径的具体的文件,包括的主要属性如下,我们做下说明: 266 | ```js 267 | { 268 | "chunkNames": [], 269 | // The chunks this asset contains 270 | //这个输出资源包含的chunks名称。对于图片的require或者require.ensure动态产生的chunk是不会有chunkNames的,但是在entry中配置的都是会有的 271 | "chunks": [ 10, 6 ], 272 | // The chunk IDs this asset contains 273 | //这个输出资源包含的chunk的ID。通过require.ensure产生的chunk或者entry配置的文件都会有该chunks数组,require图片不会有 274 | "emitted": true, 275 | // Indicates whether or not the asset made it to the `output` directory 276 | //使用这个属性标识这个assets是否应该输出到output文件夹 277 | "name": "10.web.js", 278 | // The `output` filename 279 | //表示输出的文件名 280 | "size": 1058 281 | // The size of the file in bytes 282 | //输出的这个资源的文件大小 283 | } 284 | ``` 285 | 所以你经常会看到通过下面的方式来为compilation.assets添加内容: 286 | ```js 287 | compilation.assets['filelist.md'] = { 288 | source: function() { 289 | return filelist; 290 | }, 291 | size: function() { 292 | return filelist.length; 293 | } 294 | }; 295 | ``` 296 | 此时你将看到我们的输出文件夹下将会多了一个输出文件filelist.md。这里你可能还有一个疑问,我们的assets和chunk有什么区别?你可以[运行并查看我的这个例子](https://github.com/liangklfangl/commonsChunkPlugin_Config/tree/master/chunk-module-assets),你会看到我们的dest目录下有如下的文件列表生成: 297 | 298 | ![](./images/generated.png) 299 | 300 | 我们可以发现assets除了包含那些chunks内容以外,还包含那些模块中对图片等的引用等。这个你可以[查看我们的filelist.md](https://github.com/liangklfangl/commonsChunkPlugin_Config/blob/master/dest/chunk-module-assets/filelist.md)文件内容。结合这个例子,我们深入分析下上面说到的那些属性,我们在这个Plugin中输出下面的内容: 301 | ```js 302 | const assets = compilation.getStats().toJson().assets; 303 | assets.forEach(function(asset,i){ 304 | console.log('asset.name====',asset.name); 305 | console.log('asset.chunkNames====',asset.chunkNames); 306 | console.log('asset.chunks====',asset.chunks); 307 | console.log("----------------"); 308 | }); 309 | ``` 310 | 此时你会看到如下的输出结果: 311 | ```js 312 | asset.name==== 12f93f748739150b542661e8677d0870.png 313 | asset.chunkNames==== [] 314 | asset.chunks==== [] 315 | ---------------- 316 | asset.name==== 0.js 317 | asset.chunkNames==== [] 318 | asset.chunks==== [ 0 ] 319 | ---------------- 320 | asset.name==== 1.js 321 | asset.chunkNames==== [] 322 | asset.chunks==== [ 1 ] 323 | ---------------- 324 | asset.name==== main1.js 325 | asset.chunkNames==== [ 'main1' ] 326 | asset.chunks==== [ 2 ] 327 | ---------------- 328 | asset.name==== main.js 329 | asset.chunkNames==== [ 'main' ] 330 | //注意:这里的chunkName只会包含main,而不会包含require.ensure产的chunk名称 331 | asset.chunks==== [ 3 ] 332 | ``` 333 | 通过输出我们可以知道,每一个assets都会有一个name属性,而chunks表示该输出资源包含的chunk的ID值列表。而chunkNames表示该输出资源包含的chunk的名称,但是不包含require.ensure产生的chunk的名称,除非require.ensure的时候自己指定了。比如下面的例子: 334 | ```js 335 | require.ensure([],function(require){ 336 | require('vue'); 337 | },"hello") 338 | //此时chunkName就是是['hello'] 339 | ``` 340 | 341 | ##### 1.3 获取stats中所有的modules 342 | ```js 343 | compilation.getStats().toJson().modules 344 | // 获取compilation所有的modules: 345 | ``` 346 | 347 | modules内部结构如下: 348 | 349 | ```js 350 | { id: 10, 351 | //该模块的id,和`module.id`一样 352 | identifier: 'C:\\Users\\Administrator\\Desktop\\webpack-chunkfilename\\node_ 353 | odules\\html-loader\\index.js!C:\\Users\\Administrator\\Desktop\\webpack-chunkf 354 | lename\\src\\Components\\Header.html', 355 | //webpack内部使用这个唯一的ID来表示这个模块 356 | name: './src/Components/Header.html', 357 | //模块名称,已经转化为相对于根目录的路径 358 | index: 10, 359 | index2: 8, 360 | size: 62, 361 | cacheable: true, 362 | //表示这个模块是否可以缓存,调用this.cacheable() 363 | built: true, 364 | //表示这个模块通过Loader,Parsing,Code Generation阶段 365 | optional: false, 366 | //所以对该模块的加载全部通过try..catch包裹 367 | prefetched: false, 368 | //表示该模块是否是预加载的。即,在第一个import,require调用之前我们就开始解析和打包该模块。https://webpack.js.org/plugins/prefetch-plugin/ 369 | chunks: [ 0 ], 370 | //该模块在那个chunk中出现 371 | assets: [], 372 | //该模块包含的所有的资源文件集合 373 | issuer: 'C:\\Users\\Administrator\\Desktop\\webpack-chunkfilename\\node_modu 374 | es\\eslint-loader\\index.js!C:\\Users\\Administrator\\Desktop\\webpack-chunkfil 375 | name\\src\\Components\\Header.js', 376 | //是谁开始本模块的调用的,即模块调用发起者 377 | issuerId: 1, 378 | //发起者的moduleid 379 | issuerName: './src/Components/Header.js', 380 | //发起者相对于根目录的路径 381 | profile: undefined, 382 | failed: false, 383 | //在解析或者处理该模块的时候是否失败 384 | errors: 0, 385 | //在解析或者处理该模块的是否出现的错误数量 386 | warnings: 0, 387 | //在解析或者处理该模块的是否出现的警告数量 388 | reasons: [ [Object] ], 389 | usedExports: [ 'default' ], 390 | providedExports: null, 391 | depth: 2, 392 | source: 'module.exports = "
{{text}}
";' } 393 | //source是模块内容,但是已经变成了字符串了 394 | ``` 395 | 而每一个模块都包含一个Reason对象表示该模块为什么会出现在依赖图谱中,这个Reason对象和上面说的chunk的origins类似,其内部签名如下: 396 | ```js 397 | { 398 | "loc": "33:24-93", 399 | // Lines of code that caused the module to be included 400 | "module": "./lib/index.web.js", 401 | // Relative path to the module based on context 402 | "moduleId": 0, 403 | // The ID of the module 404 | "moduleIdentifier": "(webpack)\\test\\browsertest\\lib\\index.web.js", 405 | // Path to the module 406 | "moduleName": "./lib/index.web.js", 407 | // A more readable name for the module (used for "pretty-printing") 408 | "type": "require.context", 409 | // The type of request used 410 | "userRequest": "../../cases" 411 | // Raw string used for the `import` or `require` request 412 | } 413 | ``` 414 | 415 | #### 2.webpack中的Compiler对象 416 | ##### 2.1 在插件中使用compiler实例 417 | webpack的*Compiler*模块是webpack主要引擎,通过它可以创建一个compilation实例,而且你所有通过cli或者webpack的API或者webpack的配置文件传入的配置都会作为参数来构建一个compilation实例。你可以通过*webpack.Compiler*来访问它。webpack通过实例化一个compiler对象,然后调用它的run方法来开始一次完整的编译过程,下面的例子演示如何使用Compiler对象,其实webpack内部也是这样处理的: 418 | ```js 419 | // Can be imported from webpack package 420 | import {Compiler} from 'webpack'; 421 | // Create a new compiler instance 422 | const compiler = new Compiler(); 423 | // Populate all required options 424 | compiler.options = {...}; 425 | // Creating a plugin. 426 | class LogPlugin { 427 | apply (compiler) { 428 | compiler.plugin('should-emit', compilation => { 429 | console.log('should I emit?'); 430 | return true; 431 | }) 432 | } 433 | } 434 | // Apply the compiler to the plugin 435 | new LogPlugin().apply(compiler); 436 | /* Add other supporting plugins */ 437 | // Callback to be executed after run is complete 438 | const callback = (err, stats) => { 439 | console.log('Compiler has finished execution.'); 440 | // Display stats... 441 | }; 442 | // call run on the compiler along with the callback 443 | //compiler.Compiler上的watch方法用于开启一次监听模式的编译,但是在正常的编译方法中不会调用(callback); 444 | ``` 445 | 我们的Compiler对象本身是一个[Tapable](https://github.com/webpack/tapable)实例,集成了Tapable类的一些功能。大多数用户看到的插件都是注册到Compiler实例上的,我们的Compiler作用可以浓缩为下面几点: 446 | 447 | - 一般只有一个父Compiler,而子Compiler可以用来处理一些特殊的事件,比如htmlWebpackplugin 448 | - 创建Compiler的难点在于传入所有相关的配置 449 | - webpack使用[WebpackOptionsDefaulter](https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsDefaulter.js)和[WebpackOptionsApply](https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js)来默认为Compiler设置参数 450 | - Compiler对象其实只是一个函数,其提供的功能非常有限,只是负责生命周期相关的部分。它将加载/打包/写文件等工作分配给不同的插件 451 | - 通过new LogPlugin(args).apply(compiler)用于在Compiler的声明周期方法中注册特定的钩子函数 452 | - Compiler提供的run方法用于开始整个webpack的打包过程。如果打包完成,就会调用callback回调函数,这个方法可以用来获取编译的信息 453 | 454 | 每一个plugin都需要在原型链上面有用apply方法,这个apply方法中会被传入Compiler对象实例: 455 | ```js 456 | //MyPlugin.js 457 | function MyPlugin() {}; 458 | MyPlugin.prototype.apply = function (compiler) { 459 | //now you have access to all the compiler instance methods 460 | } 461 | module.exports = MyPlugin; 462 | ``` 463 | 当然,你也可以使用下面这种方式: 464 | ```js 465 | //MyFunction.js 466 | function apply(options, compiler) { 467 | //now you have access to the compiler instance 468 | //and options 469 | } 470 | //this little trick makes it easier to pass and check options to the plugin 471 | module.exports = function(options) { 472 | if (options instanceof Array) { 473 | options = { 474 | include: options 475 | }; 476 | } 477 | 478 | if (!Array.isArray(options.include)) { 479 | options.include = [ options.include ]; 480 | } 481 | return { 482 | apply: apply.bind(this, options) 483 | }; 484 | }; 485 | ``` 486 | ##### 2.2 compiler上的钩子函数 487 | - run方法 488 | Compiler上的run方法用于开始一次编译过程,但是在"监听"模式下不会调用 489 | - watch-run 490 | Compiler上的watch方法用于开启一次监听模式的编译,但是在正常的模式下不会调用,只在watch模式下使用 491 | - compilation(c: Compilation, params: Object) 492 | 此时compilation实例对象已经被创建。此时你的插件可以使用该钩子函数来获取一个*Compilation*实例对象。此时的params对象也包含很多有用的引用 493 | - normal-module-factory(nmf: NormalModuleFactory) 494 | 此时NormalModuleFactory对象被创建,此时你的插件可以使用这个钩子函数获取NormalModuleFactory实例。比如下面的例子: 495 | ```js 496 | compiler.plugin("normal-module-factory", function(nmf) { 497 | nmf.plugin("after-resolve", function(data) { 498 | data.loaders.unshift(path.join(__dirname, "postloader.js")); 499 | //添加一个loader 500 | }); 501 | }); 502 | ``` 503 | - context-module-factory(cmf: ContextModuleFactory) 504 | 此时一个ContextModuleFactory对象被创建,我们的插件可以使用该钩子函数获取ContextModuleFactory对象实例 505 | - compile(params) 506 | 此时我们的Compiler对象开始编译,这个钩子函数不管在正常模式还是监听模式下都会被调用。此时我们的插件可以修改params对象 507 | ```js 508 | compiler.plugin("compile", function(params) { 509 | //you are now in the "compile" phase 510 | }); 511 | ``` 512 | - make(c: Compilation) 513 | 此时我们的插件可以添加入口文件或者预加载的模块。可以通过调用compilation上的addEntry(context, entry, name, callback)或者prefetch(context, dependency, callback)来完成。 514 | - after-compile(c: Compilation) 515 | 此时,我们的compile阶段已经完成了,很多模块已经被密封(sealed),下一个步骤就是开始输出产生的文件。 516 | - emit(c: Compilation) 517 | 我们的Compiler开始输出文件。此时我们的插件是添加输出文件到compilation.assets的最后时机 518 | - after-emit(c: Compilation) 519 | 我们的Compiler已经输出了所有的资源 520 | - done(stats: Stats) 521 | 打包已经完成 522 | - failed(err: Error) 523 | Compiler对象在watch模式,compilation失败即打包失败 524 | - invalid() 525 | Compiler对象在watch模式,而且文件已经发生变化。下options.watchDelay时间后将会重新编译 526 | - after-plugins() 527 | 所有从options中抽取出来的插件全部被添加到compiler实例上了 528 | - after-resolvers() 529 | 所有从options中抽取出来的插件全部被添加到resolver上了 530 | #### 2.Watching对象 531 | Compiler本身支持"监听"模式,在该模式下,如果文件发生变化那么webpack会自动重新编译。如果在watch模式下,我们的webpack会调用一些特定的方法,比如 "watch-run", "watch-close"和"invalid"。这些钩子函数常用于开发模式,一般和webpack-dev-server等配合,这样每次文件变化,开发者不用手动编译。 532 | 533 | #### 3.Compilation对象 534 | 我们的Compilation对象继承于Compiler。compiler.compilation是打包过程中的compilation对象。这个属性可以获取到所有的模块和依赖,但是大多数都是循环引用的模式。在compilation阶段,模块被加载,封存,优化,chunked,hased和存储。这些都是compilation对象上的主要声明周期方法: 535 | ```js 536 | compiler.plugin("compilation", function(compilation) { 537 | //the main compilation instance 538 | //all subsequent methods are derived from compilation.plugin 539 | }) 540 | ``` 541 | 下面介绍主要的声明周期方法: 542 | 543 | - normal-module-loader 544 | 这个loader是一个函数,用于依次加载所有依赖图谱中出现的模块 545 | ```js 546 | compilation.plugin('normal-module-loader', function(loaderContext, module) { 547 | //this is where all the modules are loaded 548 | //one by one, no dependencies are created yet 549 | //没有创建依赖 550 | }); 551 | ``` 552 | - seal 553 | 在这个阶段你不会再获取到任何没有加载过的模块 554 | ```js 555 | compilation.plugin('seal', function() { 556 | //you are not accepting any more modules 557 | //no arguments 558 | }); 559 | ``` 560 | - optimize 561 | 此时用于优化本次的打包过程 562 | ```js 563 | compilation.plugin('optimize', function() { 564 | //webpack is begining the optimization phase 565 | // no arguments 566 | }); 567 | ``` 568 | - optimize-tree(chunks, modules) 569 | 用于优化模块或者chunks依赖图谱 570 | ```js 571 | compilation.plugin('optimize-tree', function(chunks, modules) { 572 | }); 573 | ``` 574 | - optimize-modules(modules: Module[]) 575 | 用于优化模块 576 | ```js 577 | compilation.plugin('optimize-modules', function(modules) { 578 | //handle to the modules array during tree optimization 579 | }); 580 | ``` 581 | - after-optimize-modules(modules: Module[]) 582 | 所有的模块已经优化完毕 583 | - optimize-chunks(chunks: Chunk[]) 584 | 优化所有的chunks 585 | ```js 586 | //optimize chunks may be run several times in a compilation 587 | compilation.plugin('optimize-chunks', function(chunks) { 588 | //unless you specified multiple entries in your config 589 | //there's only one chunk at this point 590 | chunks.forEach(function (chunk) { 591 | //chunks have circular references to their modules 592 | chunk.modules.forEach(function (module){ 593 | //module.loaders, module.rawRequest, module.dependencies, etc. 594 | }); 595 | }); 596 | }); 597 | ``` 598 | - after-optimize-chunks(chunks: Chunk[]) 599 | 600 | chunks优化完毕 601 | - revive-modules(modules: Module[], records) 602 | 603 | 从记录中重新加载模块信息 604 | - optimize-module-order(modules: Module[]) 605 | 606 | 根据模块的重要性来对模块排序。最重要的排列在前面,并得到最小的id 607 | - optimize-module-ids(modules: Module[]) 608 | 609 | 优化模块的id 610 | - after-optimize-module-ids(modules: Module[]) 611 | 612 | 模块id优化完毕 613 | - record-modules(modules: Module[], records) 614 | 615 | 将模块信息重新保存到records中 616 | - revive-chunks(chunks: Chunk[], records) 617 | 618 | 将chunk信息从records中加载出来 619 | - optimize-chunk-order(chunks: Chunk[]) 620 | 621 | 根据chunk的重要性来对chunk排序。最重要的排列在前面,并得到最小的id 622 | - optimize-chunk-ids(chunks: Chunk[]) 623 | 624 | 优化chunk的id 625 | - after-optimize-chunk-ids(chunks: Chunk[]) 626 | 627 | chunk的id优化完毕 628 | - record-chunks(chunks: Chunk[], records) 629 | 630 | 将chunk的信息重新保存到records中 631 | - before-hash 632 | 633 | 此时本次编译还没有产生hash 634 | - after-hash 635 | 636 | 此时,我们本次编译已经产生hash 637 | - before-chunk-assets 638 | 639 | 此时,特定的chunk的资源还没有被创建 640 | - additional-chunk-assets(chunks: Chunk[]) 641 | 642 | 为特定的chunk创建额外的资源 643 | - record(compilation, records) 644 | 645 | 将我们的compilation的打包信息保存到records中 646 | - optimize-chunk-assets(chunks: Chunk[]) 647 | 648 | 优化每一个chunk依赖的资源文件。我们的资源文件全部保存在this.assets(this指向compilation)中,但是并不是所有文件都会是一个chunk,每一个chunk都会有特定的files属性,这个files属性指向的是这个chunk创建的所有的文件资源。所有额外的chunk资源都保存在*this.additionalChunkAssets*属性中。下面的例子展示为每一个chunk添加banner信息: 649 | ```js 650 | compilation.plugin("optimize-chunk-assets", function(chunks, callback) { 651 | chunks.forEach(function(chunk) { 652 | chunk.files.forEach(function(file) { 653 | compilation.assets[file] = new ConcatSource("\/**Sweet Banner**\/", "\n", compilation.assets[file]); 654 | }); 655 | }); 656 | callback(); 657 | }); 658 | ``` 659 | 那么上面的this.additionalChunkAssets指的是什么呢,我们看看下面的例子: 660 | ```js 661 | compiler.plugin('compilation', function(compilation) { 662 | compilation.plugin('additional-assets', function(callback) { 663 | download('https://img.shields.io/npm/v/webpack.svg', function(resp) { 664 | if(resp.status === 200) { 665 | //下载文件添加到assets中 666 | compilation.assets['webpack-version.svg'] = toAsset(resp); 667 | callback(); 668 | } else { 669 | callback(new Error('[webpack-example-plugin] Unable to download the image')); 670 | } 671 | }) 672 | }); 673 | }); 674 | ``` 675 | 所有*this.additionalChunkAssets*其实指的就是外部依赖的文件,比如网络资源文本,而不是本地的文件。 676 | 677 | - after-optimize-chunk-assets(chunks: Chunk[]) 678 | 所有chunk产生的资源都被优化过了。下面的例子展示每一个chunk都有哪些内容: 679 | ```js 680 | var PrintChunksPlugin = function() {}; 681 | PrintChunksPlugin.prototype.apply = function(compiler) { 682 | compiler.plugin('compilation', function(compilation, params) { 683 | compilation.plugin('after-optimize-chunk-assets', function(chunks) { 684 | console.log(chunks.map(function(c) { 685 | return { 686 | id: c.id, 687 | name: c.name, 688 | includes: c.modules.map(function(m) { 689 | return m.request; 690 | }) 691 | }; 692 | })); 693 | }); 694 | }); 695 | }; 696 | ``` 697 | - optimize-assets(assets: Object{name: Source}) 698 | 699 | 优化所有的assets资源,所有的资源都保存在this.assets中 700 | - after-optimize-assets(assets: Object{name: Source}) 701 | 702 | 所有的assets资源被优化完毕 703 | - build-module(module) 704 | 705 | 某一个模块的编译还没有开始 706 | ```js 707 | compilation.plugin('build-module', function(module){ 708 | console.log('build module'); 709 | console.log(module); 710 | }); 711 | ``` 712 | - succeed-module(module) 713 | 714 | 模块打包成功 715 | ```js 716 | compilation.plugin('succeed-module', function(module){ 717 | console.log('succeed module'); 718 | console.log(module); 719 | }); 720 | ``` 721 | - failed-module(module) 722 | 723 | 模块打包失败 724 | ```js 725 | compilation.plugin('failed-module', function(module){ 726 | console.log('failed module'); 727 | console.log(module); 728 | }); 729 | ``` 730 | - module-asset(module, filename) 731 | 732 | 所有模块的资源被添加到compilation中 733 | - chunk-asset(chunk, filename) 734 | 735 | 所有的chunk中的资源被添加到compilation中 736 | 737 | 关于上面提到的compiler和compilation的钩子函数,你可以继续阅读[我的这篇文章](https://github.com/liangklfangl/webpack-compiler-and-compilation)。 738 | 739 | 740 | #### 4.MultiCompiler 741 | MultiCompiler允许webpack在不同的Compiler中运行多个配置文件。比如在webpack方法中第一个参数是一个数组,那么webpack就会启动多个compiler实例对象,这样只有当所有的compiler完成执行后才会运行callback方法: 742 | ```js 743 | var webpack = require('webpack'); 744 | var config1 = { 745 | entry: './index1.js', 746 | output: {filename: 'bundle1.js'} 747 | } 748 | var config2 = { 749 | entry: './index2.js', 750 | output: {filename:'bundle2.js'} 751 | } 752 | //第一个参数为数组 753 | webpack([config1, config2], (err, stats) => { 754 | process.stdout.write(stats.toString() + "\n"); 755 | }) 756 | ``` 757 | 对于webpack提供的事件钩子函数你可以查看[这里](https://webpack.js.org/api/compiler/#event-hooks),比如下面的例子展示了异步的事件处理,其中emit方法表示准备将资源输出到output目录: 758 | ```js 759 | compiler.plugin("emit", function(compilation, callback) { 760 | // Do something async... 761 | setTimeout(function() { 762 | console.log("Done with async work..."); 763 | callback(); 764 | }, 1000); 765 | }); 766 | ``` 767 | 768 | --------------------------------------------------------------------------------