Webpack学习笔记
Webpack是用来在应用程序中构建JavaScript模块的工具,它能够快速的构造出应用程序的依赖图,并且按照正确的顺序将它们捆绑(bundling)在一起。Webpack还支持根据配置进行代码最优化。你可以通过命令行工具、API两种方式来使用Webpack。
Webpack通常被用来和Make、Grunt、Gulp、Browserify、Brunch之类的工具进行比较,实际上它们并不是同类型的工具。Make、Grunt、Gulp可以归类为任务运行器(task runner),而Webpack、Browserify、Brunch则可以归类为模块捆绑器(module bundler)。
任务运行器的作用是让一系列任务——代码检查、构建——变得容易。模块捆绑器则具有更特殊的目标:将一系列资产——例如JavaScript、CSS——交由捆绑器,它会将其输出为适合浏览器、最终用户使用的格式。
任务运行器可以和模块捆绑器协同工作, grunt-webpack、gulp-webpack就是集成两者的例子。Webpack用户也经常使用npm的Stage脚本作为任务运行器。
尽管Webpack的核心关注于捆绑,但是它的一些扩展类似于任务运行器。
- 支持开发者模式,可以热替换浏览器中的代码而不需要页面刷新
- 支持多种模块化机制:
- AMD的define、require
- CommonJS的exports、require、require.resolve
- 支持ES6模块
- 支持调试:SourceUrl、SourceMaps
- 支持基于uglify进行代码最小化
- 支持基于插件的扩展
- 使用异步I/O和多级缓存,性能优异,特别是增量编译时
- 支持按块(Chunk)加载,将整个依赖图拆分为多个Chunk加载,以提高响应速度
- 万物皆模块,任何静态资源都可以作为模块加载
配置文件不再是必须的。
内置ES6支持 Webpack2内置了对ES6模块的支持。ES6加载器通过 System.import 在运行时动态加载ES6模块。并且支持ES6、AMD、CommonJS在同一文件中混合使用 Webpack2支持以System.import为拆分点(splitpoint),把每个请求的ES6模块放到独立的块(Chunk)中 如果联合使用Babel,应该改用es2015-webpack这个预设,这样才能让Webpack来处理ES6模块。es2015预设会把ES6模块转换为CommonJS格式 |
术语 | 说明 | ||
Entry |
为了创建Bundle(即捆绑了所有依赖的、由HTML直接引用的JS文件),Webpack需要分析依赖图,所谓Entry就是分析此依赖图的入口点 一般入口点是启动应用时第一个加载的JS文件。默认值是 ./src |
||
Output | 一旦Webpack把所有资产捆绑到一起,它需要将其输出到工程的某个目录中供客户端使用。默认值是./dist | ||
Modules |
在模块化编程的术语中,所谓模块是独立的具有特定功能的块 在Webpack中,模块可以是:CoffeeScript、TypeScript、ESNext (Babel)、Sass、Less、Stylus等。模块之间可以具有依赖关系,声明依赖的方式包括:
|
||
Loaders |
Webpack把入口点所有直接/间接依赖的文件(CSS、HTML、SCSS、JPG等)都看作“模块”,然而Webpack本身只能理解JavaScript 所谓Loader(加载器),其用途就是把所有非JavaScript文件都转换为模块,同时加入到依赖图中。Loader本身是一个函数,它接受源文件作为入参,并返回转换后的结果。Loader的主要特性包括:
按照惯例,Loader命名一般均以-loader作为后缀,在配置文件中引用Loader时可以省略后缀 下面是Webpack 2.x配置Loader的示例:
在Webpack配置文件中,你需要参考上面的代码,在rules属性中来声明Loader:
|
||
Plugins |
Loader在文件级别操作,它转换的是单个文件。Plugin(插件)则用于更大范围的操作 —— 负责转换某些类型的模块。Webpack的插件系统非常强大和可扩展。插件可以做的事情包括:
要使用插件,你需要在webpack.config.js中require()它并在plugins中配置:
由于你可以基于不同意图多次使用同一种插件,因此每次都需要new一个插件实例 |
||
mode | 通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化
模式的区别:
|
在Webpack之前,确保合适版本的Node.js已经安装在你的系统中,当前LTS版本是推荐的选择。
要全局的使用webpack命令行,可以执行全局安装:
1 |
npm install webpack -g |
但是通常并不推荐(仅仅)这样做,因为这使得所有工程使用相同版本的ESLint。最好是针对工程进行本地安装:
1 |
npm install webpack --save-dev |
通好npm调用webpack时,总是会优先在本地模块目录中寻找Webpack。
执行下面的命令,创建一个示例工程:
1 2 3 4 5 |
mkdir WebpackStudy && cd WebpackStudy npm init -y npm install --save-dev webpack # 该示例使用下面这个库的功能 npm install --save lodash |
创建一个HTML:
1 2 3 4 5 6 7 8 9 10 11 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Webpack Study</title> <script src="https://unpkg.com/lodash@4.16.6" type="text/javascript"></script> </head> <body> <script src="index.js" type="text/javascript"></script> </body> </html> |
上面HTML使用的脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
function component() { var element = document.createElement( 'div' ); /* lodash提供了 _ 变量 */ element.innerHTML = _.map( [ 'Hello', 'webpack' ], function ( item ) { return item.toUpperCase(); } ).join( ' ' ); return element; } document.body.appendChild( component() ); |
这个示例中,index.js依赖于lodash,而index.html的script声明顺序,确保了此依赖关系得到满足,因此程序可以正常运行。
程序规模大了以后,很难用这种手工的方式来管理依赖。如果依赖不小心缺失或者script标签声明顺序不对,程序无法运行;如果包含了根本没用的依赖,则为客户端、服务器增加不必要的流量负担,无效依赖中的脚本还可能影响客户端性能。
使用Webpack来管理依赖,可以解决上面的问题。你需要为工程引入某种模块化机制,例如ES6模块化系统:
1 2 |
// 在文件开头添加 import _ from 'lodash'; |
同时修改index.html:
1 2 3 4 5 |
<!-- 添加:--> <script src="dist/bundle.js" type="text/javascript"></script> <!-- 删除:--> <script src="https://unpkg.com/lodash@4.16.6" type="text/javascript"></script> <script src="index.js" type="text/javascript"></script> |
改造以后,index.js通过import语句显式的声明对lodash的依赖,使用模块化的lodash还避免了全局名字空间的污染。
Webpack就是应用这里的import语句,来构建一个依赖图,并根据此依赖图,生成最优化的bundle,在bundle中脚本以正确的顺序被执行。如果import一个没有使用的依赖,它不会被包含在bundle中。
bundle的创建不会自动执行,因为分析需要消耗计算资源。我们可以通过命令行触发bundle的构建:
1 |
node_modules/.bin/webpack index.js dist/bundle.js |
输出文件bundle.js包含了运行页面的全部代码。但是转换后的代码并不能在浏览器中运行,这是因为Webpack 1.x不能原生的支持ES6 import语句,目前正在开发中的Webpack 2原生支持。
要转换为ES5兼容的代码,可以使用babel-loader加载器。
总是在命令行中指定参时比较麻烦,可以指定配置文件。当前目录下的 webpack.config.js 文件会自动被webpack命令读取。你也可以通过 --config 选项指定任意配置文件。
与上节webpack命令调用参数等价的配置文件为:
1 2 3 4 5 6 7 |
module.exports = { entry: './index.js', output: { filename: 'bundle.js', path: './dist' } } |
配置文件的用途不仅仅是指定入口点和输出文件位置,loader rules、plugins、resolve以及很多其它配置项,可以进一步增强bundle是其适用于生产环境。
要通过npm来调用Webpack,只需要将其webpack命令挂到某个Stage脚本下:
1 2 3 4 5 |
{ "scripts": { "build": "webpack" } } |
然后执行: npm run build 即可。 要通过npm命令行读取Webpack参数,可以使用 -- 中断npm本身的配置参数的读取:
1 2 |
# -- 后面的将作为Webpack参数,而不是npm配置参数 npm run build -- --colors |
下面是Webpack 2.1配置文件简要说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
path = require( "path" ); const config = { /** * 入口点,可以指定为字符串、对象或者数组 */ entry: "./app/entry", // 单入口点,注意相对于根目录需要 . 开头 entry: [ "./app/entry1", "./app/entry2" ], // 多入口点 entry: { a: "./app/entry-a", b: [ "./app/entry-b1", "./app/entry-b2" ] }, // 多入口点,键对Chunk进行命名,此名称对应output.filename的[name] /** * 输出,与Webpack生成的结果有关 */ output: { // 所有输出文件的Base目录 path: path.resolve( __dirname, "build" ), // string // 入口点Chunk的文件名模板,Chunk是代码的分割,Bundle可以是单个文件,也可以被分割为很多Chunks filename: "bundle.js", // 用于单入口点 filename: "[name].js", // 用于多入口点,name为入口点名称,默认名称main filename: "[chunkhash].js", // 用于长期缓存 // 输出Base目录,相对于HTML页面的URL。此URL会在HTML、CSS等文件的URL中使用 // 正确设置该目录,Webpack-dev-server需要用到 publicPath: "/assets/", publicPath: "", publicPath: "https://cdn.example.com/", library: "MyLibrary", // 导出的库的名称 // 库使用的模块系统 libraryTarget: "umd", // enum libraryTarget: "umd-module", // ES2015 module wrapped in UMD libraryTarget: "commonjs-module", // ES2015 module wrapped in CommonJs libraryTarget: "commonjs2", // exported with module.exports libraryTarget: "commonjs", // exported as properties to exports libraryTarget: "amd", // defined with AMD defined method libraryTarget: "this", // property set on this libraryTarget: "var", // variable defined in root scope }, // 模块选项 module: { // 用于模块的规则,配置加载器,解析器选项,等等 // 在Webpack 1.x中使用loaders而不是rules rules: [ { // 包含的目录或者文件,使用正则式 test: /\.jsx?$/, // 包含的目录或者文件,使用绝对路径 include: [ path.resolve( __dirname, "app" ) ], // 排除的目录或者文件,尽量避免使用 exclude: [ path.resolve( __dirname, "app/demo-files" ) ], issuer: { test, include, exclude }, // 即使被覆盖,仍然执行此规则 enforce: "pre", enforce: "post", // 应用到匹配文件的加载器。在Webpack2中不需要-loader后缀 loader: "babel-loader", // 传递给加载器的配置项 options: { presets: [ "es2015" ] }, }, { test: "\.html$", // 应用多个加载器,可以使用别名loaders use: [ "htmllint-loader", { loader: "html-loader", options: {} } ], // 如果当前规则匹配,则下列规则中第一个匹配的规则被应用 oneOf: [ /* rules */ ], // 如果当前规则匹配,则下列规则也被应用 rules: [ /* rules */ ], // 仅当所有条件满足时才匹配 resource: { and: [ /* conditions */ ] }, // 当任意条件满足时即匹配 resource: { or: [ /* conditions */ ] }, // 当任意条件满足时即匹配 resource: [ /* conditions */ ], // 当条件不满足时匹配 resource: { not: /* condition */ }, } ] }, // 用于解析模块请求,这些配置不用于加载器的解析 resolve: { // 从哪些目录寻找模块 modules: [ "node_modules", path.resolve( __dirname, "app" ) ], // 模块的扩展名 extensions: [ ".js", ".json", ".jsx", ".css" ], // 模块别名映射 alias: { "module": "new-module", // "module" -> "new-module" "module/path/file" -> "new-module/path/file" "only-module$": "new-module", // "only-module" -> "new-module" 但是only-module目录下的文件不映射 } }, // 通过为浏览器的开发者工具添加元数据信息,增强调试 devtool: "source-map", // enum devtool: "inline-source-map", // inlines SourceMap into orginal file devtool: "eval-source-map", // inlines SourceMap per module devtool: "hidden-source-map", // SourceMap without reference in original file devtool: "cheap-source-map", // cheap-variant of SourceMap without module mappings devtool: "cheap-module-source-map", // Webpack3 + Chrome 可以使用这个 devtool: "eval", // no SourceMap, but named modules. Fastest at the expense of detail // Webpack的Home目录,entry、module.rules.loader相对于此目录解析 context: __dirname, // 此Bundle的运行平台 target: "web", // enum target: "webworker", // WebWorker target: "node", // Node.js via require target: "async-node", // Node.js via fs and vm target: "node-webkit", // nw.js target: "electron-main", // electron, main process target: "electron-renderer", // electron, renderer process // 不去捆绑这些模块,而是在运行时动态加载 externals: ["react", /^@angular\//], externals: "react", // string (exact match) externals: /^[a-z\-]+($|\/)/, // Regex externals: { // object angular: "this angular", // this["angular"] react: { // UMD commonjs: "react", commonjs2: "react", amd: "react", root: "React" } }, externals: (request) => { /* ... */ return "commonjs " + request }, // 插件列表 plugins: [ // ... ], // 收集构建时的时间消耗信息 profile: true, // 启用或禁用缓存 cache: false, // 启用或禁用监控文件的变动 watch: true, // 监控选项 watchOptions: { aggregateTimeout: 1000, // 把多个文件变更在一个rebuild中处理,超时时间 poll: true, // 使用文件系统轮询,对于不支持变化通知的系统,例如nfs,必须启用 poll: 500, // 轮询间隔 }, } module.exports = config |
配置的细节请参考官方文档。
模块热替换(HMR - Hot Module Replacement)功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面时丢失的应用程序状态
- 只更新变更内容,以节省宝贵的开发时间
- 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式
在应用程序中,HMR这样实现模块的置换:
- 应用程序代码要求 HMR runtime 检查更新
- HMR runtime(异步)下载更新,然后通知应用程序代码
- 应用程序代码要求 HMR runtime 应用更新
- HMR runtime(同步)应用更新
你可以设置 HMR,以使此进程自动触发更新,或者你可以选择要求在用户交互时进行更新。
在编译器中,除了普通资源,编译器(compiler)需要发出 "update",以允许更新之前的版本到新的版本。"update" 由两部分组成:
- 更新后的 manifest(JSON)
- 一个或多个更新后的 chunk (JavaScript)
manifest 包括新的编译 hash 和所有的待更新 chunk 目录。每个更新 chunk 都含有对应于此 chunk 的全部更新模块(或一个 flag 用于表明此模块要被移除)的代码。编译器确保模块 ID 和 chunk ID 在这些构建之间保持一致。通常将这些 ID 存储在内存中(例如,使用 webpack-dev-server 时),但是也可能将它们存储在一个 JSON 文件中。
从模块的视角来看,HMR 是可选功能,只会影响包含 HMR 代码的模块。例如:通过 style-loader 为 style 样式追加补丁。为了运行追加补丁,style-loader 实现了 HMR 接口;当它通过 HMR 接收到更新,它会使用新的样式替换旧的样式。
类似的,当在一个模块中实现了 HMR 接口,你可以描述出当模块被更新后发生了什么。然而在多数情况下,不需要强制在每个模块中写入 HMR 代码。如果一个模块没有 HMR 处理函数,更新就会冒泡(bubble up)。这意味着一个简单的处理函数能够对整个模块树(complete module tree)进行更新。如果在这个模块树中,一个单独的模块被更新,那么整组依赖模块都会被重新加载。
在开发过程中,可以将 HMR 作为 LiveReload 的替代。webpack-dev-server 支持 hot 模式,在试图重新加载整个页面之前,热模式会尝试使用 HMR 来更新。
1 2 3 4 |
# entry,指定打包的入口点,可以使用<name>=<request>的形式指定多个入口点 # output,指定输出文件 # options,很多Webpack配置项被映射到命令行选项 webpack <entry> <output> <options> |
选项 | 说明 |
-d | 开发环境快捷选项,等价于:--debug --devtool source-map --output-pathinfo |
-p | 产品环境快捷选项,等价于:--optimize-minimize --optimize-occurrence-order |
--watch | 监控模式,监控所有依赖,当发生变化时自动重新编译 |
--config | 指定Webpack配置文件 |
--progress | 在stderr显示编译进度信息 |
--json | 输出JSON格式,而非默认的供人阅读的格式 |
--colors | 启用高亮 |
--no-color | 禁用高亮 |
--sort-modules-by --sort-chunks-by --sort-assets-by |
根据一个列来排序模块、块或者资产 |
--display-chunks | 显示模块到块的划分 |
--display-reasons | 显示一个模块被包含在结果中的原因 |
--display-error-details | 显示关于错误的详细信息 |
--display-modules | 显示隐藏模块。默认情况下如果模块位于node_modules", "bower_components", "jam", "components"等目录不会显示在命令输出中 |
--display-exclude | 手工排除显示到输出中的模块 |
该命令能够启动一个基于Express的服务器,以监控模式运行webpack。该命令由独立的包提供:
1 |
npm install webpack-dev-server --save-dev |
webpack-dev-server能够自动监控源文件的变化,进行编译(编译的结果仅仅放置在内存,文件系统看不到),然后基于Sockjs发送到客户端,导致页面自动刷新。命令示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
webpack-dev-server --progress --colors # 监听IP和端口 --port 8080 --host localhost # 可以指定Web根目录,默认为当前目录 --content-base build/ # 内联模式,一个简单的webpack-dev-server客户端被嵌入bundle,在代码变动时自动刷新 # 也可以在webpack.config.js中指定: devServer: { inline: true } --inline # 启用模块热替换 --hot # 生成Sourcemap -d |
很多IDE或者编辑器支持所谓Safe write特性并且默认启用,这会导致Webpack dev server无法监控文件变化,需要关闭。
以Intellij Webstorm为例: Settings ⇨ A & B ⇨ System Settings,取消勾选:Use "safe write" ...
要使用加载器,首先需要安装它们。例如:
1 2 3 |
# css-loader能够读取CSS文件 # style-loader能够将CSS文件插入到页面中 npm install css-loader style-loader |
要针对单个模块来链式调用加载器,可以参考如下语法:
1 2 3 4 5 6 7 8 |
// 使用!来分隔应用的加载器、模块名 require("!style!css!./style.css") // 使用当前目录中loader.js定义的加载器来转换dir/file.txt文件 require("./loader!./dir/file.txt"); // bootstrap模块中的less目录中的bootstrap.less文件,将被less加载器转换,结果传递给css加载器,css加载器的结果传递给style加载器 require("!style!css!less!bootstrap/less/bootstrap.less"); |
上面的方式比较麻烦,最好在配置文件中声明文件Pattern对应的加载器:
1 2 3 4 5 6 7 8 |
module: { loaders: [ // 对.css文件依次使用css、style加载器 { test: /\.css$/, loader: 'style!css' } // 另一种写法 { test: /\.css$/, loaders: ["style", "css"] }, ] } |
然后,require语句就可以简化为:
1 |
require( './style.css' ) |
可以通过webpack命令行来指定加载器:
1 2 3 |
# 对于.jade文件使用jade加载器 # 对于.css文件,使用css、style加载器 webpack --module-bind jade --module-bind 'css=style!css' |
对于大型Web应用来说,将所有代码捆绑到一个文件中是很低效的,会大大延长客户端的加载耗时。特别是,某些代码仅仅在特定的情况下在会被用到,更不应该在应用初始化时立即加载。
Webpack支持把代码分割(code splitting)为多个块(Chunks),并按需加载这些块。其它绑定器可能把Chunk称为layers、rollups或者fragments。
代码分割是可选的特性,你可以在代码中定义分割点(split points),Webpack会处理依赖、输出文件以及运行时加载的代码。
除了按需加载这个基本特性外,代码分割还支持抽取多个入口点的共享代码,将其独立到共享块(shared chunk)中。
AMD和CommonJS规范分别定义了按需加载的方法,这些方法可以被Webpack自动识别为分割点。
CommonJS按需加载方法如下:
1 2 3 4 5 6 7 8 9 |
// 在callback执行之前,确保所有dependencies已经同步的加载 // 注意:require.ensure仅仅加载模块文件,但是不会执行其中的代码 require.ensure(dependencies, callback); // 示例 require.ensure(["module-a", "module-b"], function() { // require则可能既加载文件,也执行模块代码 var a = require("module-a"); }); |
AMD按需加载方法如下:
1 2 3 4 5 6 |
// 异步加载dependencies,全部完毕后,执行回调 require(dependencies, callback); // require会加载模块并且执行其中的代码 require(["module-a", "module-b"], function(a, b) { // 回调的入参是加载模块的exports }); |
Webpack本身不支持ES6模块系统,但是你可以使用Babel之类的编译器,将ES6 import语句编译为AMD或者CommonJS的require调用。
ES6 import仅仅用于静态依赖分析,这就导致它只能接受字符串直接量表示的模块标识符。这是个严重的缺点,因为按需加载的模块,其名称往往在运行时才能确定。
所有在分割点引入的依赖,都会被分割到新的块中。如果这些依赖的代码本身也定义了分割点,则递归的生成新的块。
如果你在分割点回调函数中require新的依赖,则这些依赖会被捆绑到分割点生成的那个块中。
如果两个Chunk包含相同的模块,则它们会被合并为单个Chunk。这样,合并后的块就会有多个父块(Parent chunks)。
很多插件参与块优化:LimitChunkCountPlugin、MinChunkSizePlugin、AggressiveMergingPlugin
根据Webpack配置文件的target属性的不同,相应的运行时加载Chunk的逻辑会被添加到bundle中。例如对于target=web,Chunk会通过jsonp加载。
每个Chunk只会被加载一次,并行的加载请求会被合并为单个请求。
入口点块 Entry chunk | 这种块包含运行时代码、一系列的模块的代码。如果此块包含module 0则执行之,如果不包含module0则等待包含module0的块加载完毕然后执行之 |
普通块 Normal chunk |
这种块仅仅包含一系列模块代码,其结构依赖于Chunk加载方式,例如通过jsonp加载时目标模块被包裹在jsonp回调函数中 这种块包含一个列表,存放它fullfill的其它Chunks的ID |
初始块 Initial chunk | 一种特殊的普通块。但是会和入口点块一样在应用初始化时加载。这种块会在使用CommonsChunkPlugin插件时出现 |
如果想把应用程序分割为app.js和vendor.js,你可以在vendor.js中require厂商的所有代码,然后使用CommonsChunkPlugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var webpack = require("webpack"); module.exports = { entry: { app: "./app.js", vendor: ["jquery", "underscore", ], }, output: { filename: "bundle.js" } , plugins: [ new webpack.optimize.CommonsChunkPlugin(/* chunkName= */"vendor", /* filename= */"vendor.bundle.js") ] }; |
在HTML页面中需要同时引用:
1 2 |
<script src="vendor.bundle.js"></script> <script src="bundle.js"></script> |
Leave a Reply