记得我第一次上 webpack 官网的时候看了半天,最后总结是:这是个什么东西?!接下来的第二次第三次依然会有这种感觉,webpack 令人懵逼的文档对新手来说简直是噩梦。这篇文章针对 webpack 中令人疑惑的部分进行全面讲解,让你不再懵逼。
原文译自:rajaraodv@medium Webpack - The Confusing Parts
Webpack 是目前最火 React 和 Redux 打包工具,使用 Angular2 或者其他框架的人们可能也用了 Webpack 一段时间了。
当我第一次见到 Webpack 配置文件的时候,它看起来像火星文👽并且让人懵逼😱。玩了一段时间后我觉得 Webpack 仅仅是有不同的语法和可能导致初学者困惑的新思想。然而,正是这种新的思想才让它如此流行。
正因为 Webpack 的配置让初学者很困惑,所以我计划写几篇文章来帮助其他的初学者更容易地掌握它,并且能更好的使用它强大的特性。这是第一篇。
Webpack 核心思想
Webpack 的两个主要思想:
- 所有的一切都是模块(module) —— 就像 JS 可以模块化一样,所有的一切 (CSS, Images, HTML) 都可以是模块。就是这样,你可以
require('myjsfile.js')
或者require('myCSSfile.css')
。这意味着我们可以把任何部件分割成更小的可管理的模块用来复用等等。 - 按需加载 通常来说模块打包只能将你所有的模块打包成单个大的”bundle.js”文件。但是在现实世界中,”bundle.js”可能达到 10MB-15MB 导致过长的加载时间。所以 Webpack 有专门的功能用来分割你的代码并且生成多个打包文件,同样也能异步加载部分模块,所以你只需要”按需加载”即可。
接下来看看 Webpack 中真正令人困惑的部分。
1. 开发环境 VS 生产坏境
第一点要注意的就是 Webpack 有一大堆特性,很多特性仅在开发环境下可以使用,另外的一些仅在生产环境使用。还有的一些则是两种环境下都能使用。
开发环境和生产坏境下的配置样例
通常一个项目有两个 Webpack 配置文件来配置众多特性(一个用作开发坏境,一个用在生产坏境)
打包文件可以在 package.json
里这样写:
1 2 3 4 5 6 |
"scripts": { //npm run build 用来打包生产坏境的文件 "build": "webpack --config webpack.config.prod.js", //npm run dev 用来启动开发服务器和生成开发模式下的文件 "dev": "webpack-dev-server" } |
2. Webpack 命令行工具和 webpack-dev-server
Webpack 提供了两种操作方式:
- 命令行工具,也就是默认的方式。(安装 Webpack 时就下载好了)
- webpack-dev-server,基于 Node.js 服务器的工具。(需要单独安装)
Webpack 命令行工具(适用于生产环境)
命令行工具可以通过参数以及 webpack.config.js
配置文件来进行配置。
初学者一般是从命令行工具开始学起的,但以后你几乎只用它来打包生产环境的文件
用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
方式1: // 全局安装 npm i webpack -g // 在命令行中使用(默认读取 webpack.config.js) webpack 方式2: // 作为项目依赖安装 npm i webpack --save // 添加到 package.json 文件 "scripts": { "build": "webpack --config webpack.config.prod.js -p", ... } // 然后用下面的命令运行 npm run build |
webpack-dev-server (WDS, 适用于开发环境)
这是一个基于 Express 的 node.js 服务器,默认跑在 8080 端口上。服务器会在内部调用 webpack 命令。WDS 的好处是提供了一些额外的功能,例如在浏览器中”实时加载”,即当你修改了本地的代码浏览器自动加载保存过的代码并且不需要刷新。(又名 “Hot Module Replacement, HMR”)
用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
方式1: // 全局安装 npm i webpack-dev-server -g // 在命令行中使用 webpack-dev-server --inline --hot 方式2: // 添加到项目中 npm i webpack-dev-server --save // 在 package.json 中指定 "scripts": { "start": "webpack-dev-server --line --hot" // ... } // 用法 npm start ------------ 以上两种操作结束后打开浏览器到 http://localhost:8080 即可 |
命令行和 WDS 参数
--inline
和 --hot
仅仅是 WDS 的参数,对于打包文件并没有什么意义,其他的一些选项例如--hide-modules
则是命令行专属的参数。
WDS 命令行选项对比 webpack.config.js
中的对象参数
给 WDS 加选项可以通过以下两种方式:
- 在
webpack.config.js
中声明一个devServer
的对象 - 直接通过命令行参数
1 2 3 4 5 6 7 8 |
// 命令行参数 webpack-dev-server --hot --inline // 通过 webpack.config.js devServer: { inline: true, hot: true } |
我有时候发现 devServer 配置
hot: true, inline: true
不管用,所以我建议你直接通过命令行来加这两个参数。
1 2 3 4 |
// package.json scripts: { start: "webpack-dev-server --hot --inline" } |
译者注:同遇到这个问题,建议 hot 和 inline 始终直接加到命令行参数,其他的建议写在 devServer 配置里。
注:不要同时在 devServer 里配置
hot: true
和 命令行里配置hot: true
,不然会造成多加载一个入口,浏览器死循环。
hot
和 inline
这两个配置的作用
inline
选项用于开启页面的实时加载功能(Live reloading),hot
选项用于开启 热加载(HMR)
功能,这两个的区别就是 Livereload 是一次页面刷新,HMR 则是页面不刷新,直接替换更换过后的模块。如果你两个参数都开了,那会先尝试热加载,如果热加载不行则尝试刷新页面。
译者注: HMR 有什么卵用?当你要调试一个购票表单,第一步填写信息第二部选择票种,第三步付款。HMR 可以让你在改完代码后直接调试第三部而不是页面刷新,从第一步开始自己输信息。
1 2 3 4 5 6 7 8 9 10 |
// 当代码更新的时候,这三种选项都会打包新的文件,但是又有不同。 // 1. 不帮你刷新页面 webpack-dev-server // 2. 直接帮你刷新整个页面 webpack-dev-server --inline // 3. 仅仅刷新更新过的模块,如果需要的话再刷新整个页面 webpack-dev-server --inline --hot |
3. entry
入口配置,到底用字符串还是数组
入口(entry) 告诉 Webpack 你项目的代码从哪个文件开始,入口文件可以是一个字符串,也可以是一个数组或者对象。估计你该懵逼了,但其实不同的类型有不同的作用。
如果你只有一个入口(大多数情况都是),你使用这三种格式都是一样的。
不同的入口类型但输入是一样的
entry-Array 用数组来写入口配置
如果你有多个入口文件并且彼此之间没有依赖,你可以使用数组来写入口配置。
例如:你要将 googleAnalytics.js
加载到你的 HTML 文件,你可以像这样用 Webpack 将这个文件打包到所有依赖的最后:
entry-Object 用对象来写入口配置
现在,假设你有一个多页面的应用,不是单页面多个视图那种的。而是多个 HTML 文件,例如 (index.html 和 profile.html). 你通过写一个对象来告诉 Webpack 打包多个入口到多个输出。
下面的配置将会生成两个 JS 文件:indexEntry.js
和 profileEntry.js
文件,你可以在 index.html 和 profile.html 单独引用。
例子:
1 2 3 4 5 |
// profile.html <script src="dist/profileEntry.js"></script> // index.html <script src="dist/indexEntry.js"></script> |
注: entry 对象的 key 就是文件最后打包好的名字
entry-combination 混合使用数组和对象编写入口配置
你也可以同时使用数组和对象来编写入口,下面的例子将会生成3个文件:vendor.js
包含三个外部引用文件,一个 index.js
和一个 profile.js
。
4. 输入(output)配置 ——path
和 publicPath
的不同
output 告诉 Webpack 你想把打包好的文件放到哪,有两个让人懵逼的属性 path
和publicPath
。
path
简单的告诉 Webpack 应该把打包好的文件放到哪,publicPath
是用来告诉 Webpack 的插件在生产环境下如何更新 CSS HTML 中的文件 URL的。一图胜前言:
例如你在 CSS 里写了一个 ./test.png
用来在本地开发的时候引一张图片,但是在服务器环境下这张图片可能是在 CDN 上存放的(例如你的服务器跑在阿里云但图片在七牛上)。这意味着如果你不写 publicPath
的配置你就需要在发布到线上的手动改路径。
换言之你可以用 webpack 里的 publicPath
来干这事,所有的插件都会在打包到生产环境时根据这个配置自动更新路径。
1 2 3 4 5 6 7 8 |
// 开发环境下相对引用本地服务器上的图片 .image { background-image: url('./test.png'); } // 生产环境下服务器可能在阿里云但图片在七牛 .image { background-image: url('https://someCDN/test.png'); } |
5. Loaders 和 链式 Loaders (真的不知道 Loaders 要怎么翻译)
Loaders 是外部插件用来加载或者引入浏览器支持的几种文件的(例如 JS CSS 等等)。进一步来说 loaders 也允许你在 JS 文件里使用 require
或者 import
(ES6/ES2015) 来加载模块。
例如:你可以使用 babel-loader
将 JS 文件从 ES6/ES2015 转化成浏览器兼容的 ES5 格式:
1 2 3 4 5 6 |
module: { loaders: [{ test: /\.js$/, ←文件名正则,如果以 `.js` 结尾则使用该 loader exclude: /node_modules/, ←不检测 node_modules 下的文件 loader: 'babel' ←使用 babel 加载器 ('babel-loader' 的简写) }] |
链式 loader (从右向左运行)
一个文件要使用多个 loaders 可以写成链式加载,链式加载的规则是 从右向左运行,以”!”为分割。
例如:假设有一个 myCssFile.css
文件你想把文件中的内容插入到 HTML <style> CSS content<style>
标签里,可以使用 css-loader
和 style-loader
来达到目的。
1 2 3 4 5 |
module: { loaders: [{ test: /\.css$/, loader: 'style!css' <--(style-loader!css-loader 的缩写) }] |
一图胜千言:
工作流程:
- Webpack 从入口开始搜索 CSS 依赖,如果找到类似于
require('mycssfile.css')
这样的依赖,那就将mycssfile.css
文件中的内容通过css-loader
进行处理。 css-loader
将所有 CSS 以及 CSS 内的依赖(例如@import xxx.css
)写进一个 JSON 里,然后将内容传到style-loader
里。style-loader
拿到 JSON 后将内容处理并插入到 HTML 的<style>
标签内。
译者注:WTF 你为什么把 CSS 加载到 HTML Head 标签写成行内呢?就不能自己 host 一个 css 文件吗?原因是一个包就一个 bundle.js 多清爽,干嘛还要自己再托管一个 css 文件然后再 link rel 引入进去。
6. Loaders 自身的配置
loaders 自身可以通过传不同的参数进行配置。
下面的例子里,通过给 url-loader
配置参数使得当加载小于 1024 bytes 的图片时使用 base64 字符串的格式,我们可以将 1024 这个大小的配置通过两种方式传给 url-loader
。
7. .babelrc
配置
babel-loader
> 6.0 以后使用 presets
来配置如何将 ES6 的代码编译为 ES5 的代码 以及如何编译 React 的 JSX 到 JS。可以通过 query
选项来进行配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// webpack.config.js module: { loaders: [ { test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, loader: 'babel', query: { presets: ['react', 'es2015'] // 要使用的编译器 } } ] } |
然而在许多项目中 babel 的配置可能会很大,所以你可以在项目跟目录下建一个 .babelrc
来管理 babel 配置。babel-loader
会自动检测该文件是否存在并应用配置。
所以上面的例子可以改成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//webpack.config.js module: { loaders: [ { test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, loader: 'babel' } ] } //.bablerc { "presets": ["react", "es2015"] } |
8. 插件
插件是用来控制打包结果的第三方模块。
例如,(uglifyJSPlugin)[https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin] 用来将 bundle.js
压缩混淆,以减小大小优化加载速度。
类似的,(extract-text-webpack-plugin)[https://github.com/webpack/extract-text-webpack-plugin] 插件在内部调用 css-loader
和 style-loader
将所有 css 内容保存为一个 style.css
文件,并且将该文件插入到 HTML 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//webpack.config.js //将所有的 css 依赖导报进一个 styles.css 文件中 var ETP = require("extract-text-webpack-plugin"); module: { loaders: [ {test: /\.css$/, loader:ETP.extract("style-loader","css-loader") } ] }, plugins: [ new ExtractTextPlugin("styles.css") //Extract to styles.css file ] } |
注意:如果你只是想把 css 插入到 head 中的 style 标签里,可以不用那么麻烦而是像下面一样:
1 2 3 4 5 6 |
module: { loaders: [{ test: /\.css$/, loader: 'style!css' <--(short for style-loader!css-loader) }] } |
9. Loaders 和插件的对比
如你所见,loaders 针对单个文件进行在打包前或者打包时进行处理。
而插件则是在打包或者合成(chunk level)的时候,并且通常在打包文件的最后一个步骤。一些插件,像 (commonsChunksPlugins)[https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin] 能够进行更进一步修改,包括修改打包后的文件内容。
10. 文件后缀名依赖解决
许多 Webpack 的配置都只写了 ‘’ 在 resolve.extensions
中,空字符串的配置可以在引入文件的时候省略后面的文件名。
1 2 3 4 5 6 |
// 这样就可以 require('./myJSFile') 而不需要指定文件后缀 require('./myJSFile.js') { resolve: { extensions: ['', '.js', '.jsx'] } } |
原文到此结束,然而译者还想要加 One More Thing
11. 通过 webpack.config.babel.js 来使用 ES6 编写 Webpack 配置
如题,改改文件名,webpack 就会调用 babel-loader 来处理自己的配置文件
12. 在代码中使用 NODE_ENV 等变量
1 2 3 4 5 |
plugins: [ new webpack.DefinePlugin({ IS_PRODUCTION: JSON.stringify(IS_PRODUCTION), }), ] |
13. 编译目标平台选择
如果你的 bundle.js 要发布到 npm 上让别人通过 webpack 引入或者 browserify 或者其他 AMD/CMD 的方式引入,那就和打包到浏览器里运行时不太一样的。比如你可以设置外部依赖,使用 process.env 等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ library: 'xxx', libraryTarget: 'umd', externals: { react: { root: 'React', commonjs: 'react', commonjs2: 'react', amd: 'react', }, 'react-dom': { root: 'ReactDOM', commonjs: 'react-dom', commonjs2: 'react-dom', amd: 'react-dom', }, moment: { root: 'Moment', commonjs: 'moment', commonjs2: 'moment', amd: 'moment', }, } } |
转载请注明:有爱前端 » Webpack 一探究竟