原文首发于:Webpack 3,从入门到放弃css
Update (2017.8.27) : 关于
output.publicPath
、devServer.contentBase
、devServer.publicPath
的区别。以下:html
- output.publicPath: 对于这个选项,咱们无需关注什么绝对相对路径,由于两种路径均可以。咱们只须要知道一点:这个选项是指定 HTML 文件中资源文件 (字体、图片、JS文件等) 的
文件名
的公共 URL 部分的。在实际状况中,咱们首先会经过output.filename
或有些 loader 如file-loader
的name
属性设置文件名
的原始部分,webpack 将文件名
的原始部分和公共部分结合以后,HTML 文件就能获取到资源文件了。- devServer.contentBase: 设置静态资源的根目录,
html-webpack-plugin
生成的 html 不是静态资源。当用 html 文件里的地址没法找到静态资源文件时就会去这个目录下去找。- devServer.publicPath: 指定浏览器上访问全部 打包(bundled)文件 (在
dist
里生成的全部文件) 的根目录,这个根目录是相对服务器地址及端口的,比devServer.contentBase
和output.publicPath
优先。
Tips
若是你用过 webpack 且一直用的是 webpack 1,请参考 从v1迁移到v2 (v2 和 v3 差别不大) 对版本变动的内容进行适当的了解,而后再选择性地阅读本文。node
首先,这篇文章是根据当前最新的 webpack 版本 (即 v3.4.1) 撰写,较长一段时间内无需担忧过期的问题。其次,这应该会是一篇极长的文章,涵盖了基本的使用方法,有更高级功能的需求能够参考官方文档继续学习。再次,即便是基本的功能,也内容繁多,我尽量地解释通俗易懂,将我学习过程当中的疑惑和坑一一解释,若有纰漏,敬请雅正。再次,为了清晰有效地讲解,我会演示从零编写 demo,只要一步步跟着作,就会清晰许多。最后,官方文档也是个坑爹货!jquery
借用官方的说法:webpack
webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.git
简言之,webpack 是一个模块打包器 (module bundler),可以将任何资源如 JavaScript 文件、CSS 文件、图片等打包成一个或少数文件。es6
首先,定义已经说明了 webpack 能将多个资源模块打包成一个或少数文件,这意味着与以往的发起多个 HTTP 请求来得到资源相比,如今只须要发起少许的 HTTP 请求。github
Tips
想了解合并 HTTP 请求的意义,请见 这里。web
其次,webpack 能将你的资源转换为最适合浏览器的“格式”,提高应用性能。好比只引用被应用使用的资源 (剔除未被使用的代码),懒加载资源 (只在须要的时候才加载相应的资源)。再次,对于开发阶段,webpack 也提供了实时加载和热加载的功能,大大地节省了开发时间。除此以外,还有许多优秀之处之处值得去挖掘。不过,webpack 最核心的仍是打包的功能。正则表达式
webpack 是模块打包器(module bundler),把全部的模块打包成一个或少许文件,使你只需加载少许文件便可运行整个应用,而无需像以前那样加载大量的图片,css文件,js文件,字体文件等等。而gulp/grunt 是自动化构建工具,或者叫任务运行器(task runner),是把你全部重复的手动操做让代码来作,例如压缩JS代码、CSS代码,代码检查、代码编译等等,自动化构建工具并不能把全部模块打包到一块儿,也不能构建不一样模块之间的依赖图。二者来比较的话,gulp/grunt 没法作模块打包的事,webpack 虽然有 loader 和 plugin能够作一部分 gulp/grunt 能作的事,可是终究 webpack 的插件仍是不如 gulp/grunt 的插件丰富,能作的事比较有限。因而有人二者结合着用,将 webpack 放到 gulp/grunt 中用。然而,更好的方法是用 npm scripts 取代 gulp/grunt,npm 是 node 的包管理器 (node package manager),用于管理 node 的第三方软件包,npm 对于任务命令的良好支持让你最终省却了编写任务代码的必要,取而代之的,是老祖宗的几个命令行,仅靠几句命令行就足以完成你的模块打包和自动化构建的全部需求。
先来看看一个 webpack 的一个完备的配置文件,是 介样 的,固然啦,这里面有不少配置项是即便到这个软件被废弃你也用不上的:),因此无需担忧。
开始以前,请肯定你已经安装了当前 Node 的较新版本。
而后执行如下命令以新建咱们的 demo 目录:
$ mkdir webpack-demo && cd webpack-demo && npm init -y $ npm i --save-dev webpack $ mkdir src && cd src && touch index.js
咱们使用工具函数库 lodash 来演示咱们的 demo。先安装之:
$ npm i --save lodash
src/index.js
import _ from 'lodash'; function component() { const element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } document.body.appendChild(component());
Tips
import
和export
已是 ES6 的标准,可是仍未获得大多数浏览器的支持 (可喜的是, Chrome 61 已经开始默认支持了,见 ES6 modules),不过 webpack 提供了对这个特性的支持,可是除了这个特性,其余的 ES6 特性并不会获得 webpack 的特别支持,若有须要,须借助 Babel 进行转译 (transpile)。
而后新建发布版本目录:
$ cd .. && mkdir dist && cd dist && touch index.html
dist/index.html
<!DOCTYPE html> <html> <head> <title>webpack demo</title> </head> <body> <script src="bundle.js"></script> </body> </html>
如今,咱们运行 webpack 来打包 index.js
为 bundle.js
,本地安装了 webpack 后能够经过 node_modules/.bin/webpack
来访问 webpack 的二进制版本。
$ cd .. $ ./node_modules/.bin/webpack src/index.js dist/bundle.js # 第一个参数是打包的入口文件,第二个参数是打包的出口文件
咻咻咻,大体以下输出一波:
Hash: de8ed072e2c7b3892179 Version: webpack 3.4.1 Time: 390ms Asset Size Chunks Chunk Names bundle.js 544 kB 0 [emitted] [big] main [0] ./src/index.js 225 bytes {0} [built] [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] + 1 hidden module
如今,你已经获得了你的第一个打包文件 (bundle.js) 了。
像上面这样使用 webpack 应该是最挫的姿式了,因此咱们要使用 webpack 的配置文件来提升咱们的姿式水平。
$ touch webpack.config.js
webpack.config.js
const path = require('path'); module.exports = { entry: './src/index.js', // 入口起点,能够指定多个入口起点 output: { // 输出,只可指定一个输出配置 filename: 'bundle.js', // 输出文件名 path: path.resolve(__dirname, 'dist') // 输出文件所在的目录 } };
执行:
$ ./node_modules/.bin/webpack --config webpack.config.js # `--config` 制定 webpack 的配置文件,默认是 `webpack.config.js`
因此这里能够省却 --config webpack.config.js
。可是每次都要写 ./node_modules/.bin/webpack
实在让人不爽,因此咱们要动用 NPM Scripts。
package.json
{ ... "scripts": { "build": "webpack" }, ... }
Tips
在npm scripts
中咱们能够经过包名直接引用本地安装的 npm 包的二进制版本,而无需编写包的整个路径。
执行:
$ npm run build
一波输出后便获得了打包文件。
Tips
bulid
并非npm scripts
的内置属性,须要使用npm run
来执行脚本,详情见 npm run。
由于其余文件和 JS 文件类型不一样,要把他们加载到 JS 文件中就须要通过加载器 (loader) 的处理。
咱们须要安装两个 loader 来处理 CSS 文件:
$ npm i --save-dev style-loader css-loader
style-loader 经过插入 <style> 标签将 CSS 加入到 DOM 中,css-loader 会像解释 import/require() 同样解释 @import 和 url()。
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { // 如何处理项目中不一样类型的模块 rules: [ // 用于规定在不一样模块被建立时如何处理模块的规则数组 { test: /\.css$/, // 匹配特定文件的正则表达式或正则表达式数组 use: [ // 应用于模块的 loader 使用列表 'style-loader', 'css-loader' ] } ] } };
咱们来建立一个 CSS 文件:
$ cd src && touch style.css
src/style.css
.hello { color: red; }
src/index.js
import _ from 'lodash'; import './style.css'; // 经过`import`引入 CSS 文件 function component() { const element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); element.classList.add('hello'); // 在相应元素上添加类名 return element; } document.body.appendChild(component());
执行npm run build
,而后打开index.html
,就能够看到红色的字体了。CSS 文件此时已经被打包到 bundle.js 中。再打开浏览器控制台,就能够看到 webpack 作了些什么。
$ npm install --save-dev file-loader
file-loader 指示 webpack 以文件格式发出所需对象并返回文件的公共URL,可用于任何文件的加载。
webpack.config.js
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { // 增长加载图片的规则 test: /\.(png|svg|jpg|gif)$/, use: [ 'file-loader' ] } ] } };
咱们在当前项目的目录中以下增长图片:
webpack-demo |- package.json |- webpack.config.js |- /dist |- bundle.js |- index.html |- /src + |- icon.jpg |- style.css |- index.js |- /node_modules
src/index.js
import _ from 'lodash'; import './style.css'; import Icon from './icon.jpg'; // Icon 是图片的 URL function component() { const element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); element.classList.add('hello'); const myIcon = new Image(); myIcon.src = Icon; element.appendChild(myIcon); return element; } document.body.appendChild(component());
src/style.css
.hello { color: red; background: url(./icon.jpg); }
再npm run build
之。如今你能够看到单独的图片和以图片为基础的背景图了。
加载字体用的也是 file-loader。
webpack.config.js
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.(png|svg|jpg|gif)$/, use: [ 'file-loader' ] }, { // 增长加载字体的规则 test: /\.(woff|woff2|eot|ttf|otf)$/, use: [ 'file-loader' ] } ] } };
在当前项目的目录中以下增长字体:
webpack-demo |- package.json |- webpack.config.js |- /dist |- bundle.js |- index.html |- /src + |- my-font.ttf |- icon.jpg |- style.css |- index.js |- /node_modules
src/style.css
@font-face { font-family: MyFont; src: url(./my-font.ttf); } .hello { color: red; background: url(./icon.jpg); font-family: MyFont; }
运行打包命令以后即可以看到打包好的文件和发生改变的页面。
由于 webpack 对 JSON 文件的支持是内置的,因此能够直接添加。
src/data.json
{ "name": "webpack-demo", "version": "1.0.0", "author": "Sam Yang" }
src/index.js
import _ from 'lodash'; import './style.css'; import Icon from './icon.jpg'; import Data from './data.json'; // Data 变量包含可直接使用的 JSON 解析获得的对象 function component() { const element = document.createElement('div'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); element.classList.add('hello'); const myIcon = new Image(); myIcon.src = Icon; element.appendChild(myIcon); console.log(Data); return element; } document.body.appendChild(component());
关于其余文件的加载,能够寻求相应的 loader。
前面咱们只有一个输入文件,但现实是咱们每每有不止一个输入文件,这时咱们就须要输入多个入口文件并管理输出文件。咱们在 src 目录下增长一个 print.js 文件。
src/print.js
export default function printMe() { console.log('I get called from print.js!'); }
src/index.js
import _ from 'lodash'; import printMe from './print.js'; // import './style.css'; // import Icon from './icon.jpg'; // import Data from './data.json'; function component() { const element = document.createElement('div'); const btn = document.createElement('button'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); // element.classList.add('hello'); // const myIcon = new Image(); // myIcon.src = Icon; // element.appendChild(myIcon); // console.log(Data); btn.innerHTML = 'Click me and check the console!'; btn.onclick = printMe; element.appendChild(btn); return element; } document.body.appendChild(component());
dist/index.html
<!DOCTYPE html> <html> <head> <title>webpack demo</title> <script src="./print.bundle.js"></script> </head> <body> <!-- <script src="bundle.js"></script> --> <script src="./app.bundle.js"></script> </body> </html>
webpack.config.js
const path = require('path'); module.exports = { // entry: './src/index.js', entry: { app: './src/index.js', print: './src/print.js' }, output: { // filename: 'bundle.js', filename: '[name].bundle.js', // 根据入口起点名动态生成 bundle 名,可使用像 "js/[name]/bundle.js" 这样的文件夹结构 path: path.resolve(__dirname, 'dist') }, // ... };
Tips
filename: '[name].bundle.js'
中的[name]
会替换为对应的入口起点名,其余可用的替换请参见 output.filename。
如今能够打包文件了。可是若是咱们修改了入口文件名或增长了入口文件,index.html
是不会自动引用新文件的,而手动修改实在太挫。是时候使用插件 (plugin) 来完成这一任务了。咱们使用 HtmlWebpackPlugin 自动生成 html 文件。
loader 和 plugin,有什么区别?
loader (加载器),重在“加载”二字,是用于预处理文件的,只用于在加载不一样类型的文件时对不一样类型的文件作相应的处理。而 plugin (插件),顾名思义,是用来增长 webpack 的功能的,做用于整个 webpack 的构建过程。在 webpack 这个大公司中,loader 是保安大叔,负责对进入公司的不一样人员的处理,而 plugin 则是公司里不一样职位的职员,负责公司里的各类不一样业务,每增长一种新型的业务需求,咱们就须要增长一种 plugin。
安装插件:
$ npm i --save-dev html-webpack-plugin
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // entry: './src/index.js', entry: { app: './src/index.js', print: './src/print.js' }, output: { // filename: 'bundle.js', filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ // 插件属性,是插件的实例数组 new HtmlWebpackPlugin({ title: 'webpack demo', // 生成 HTML 文档的标题 filename: 'index.html' // 写入 HTML 文件的文件名,默认 `index.html` }) ], // ... };
你能够先把 dist 文件夹的index.html
文件删除,而后执行打包命令。咻咻咻,咱们看到 dist 目录下已经自动生成了一个index.html
文件,但即便不删除原先的index.html
,该插件默认生成的index.html
也会替换本来的index.html
。
此刻,当你细细观察 dist 目录时,虽然如今生成了新的打包文件,但本来的打包文件bundle.js
及其余不用的文件仍然存在在 dist 目录中,因此在每次构建前咱们须要晴空 dist 目录,咱们使用 CleanWebpackPlugin 插件。
$ npm i clean-webpack-plugin --save-dev
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { // entry: './src/index.js', entry: { app: './src/index.js', print: './src/print.js' }, output: { // filename: 'bundle.js', filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new CleanWebpackPlugin(['dist']) // 第一个参数是要清理的目录的字符串数组 ], // ... };
打包之,如今,dist 中只存在打包生成的文件。
webpack 提供了不少便于开发时使用的功能,来一一看看吧。
当你的代码被打包后,若是打包后的代码发生了错误,你很难追踪到错误发生的原始位置,这个时候,咱们就须要代码映射 (source map) 这种工具,它能将编译后的代码映射回原始的源码,你的错误是起源于打包前的b.js
的某个位置,代码映射就能告诉你错误是那个模块的那个位置。webpack 默认提供了 10 种风格的代码映射,使用它们会明显影响到构建 (build) 和重构建 (rebuild,每次修改后须要从新构建) 的速度,十种风格的差别能够参看 devtool。关于如何选择映射风格能够参看 Webpack devtool source map。这里,咱们为了准确显示错误位置,选择速度较慢的inline-source-map
。
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { devtool: 'inline-source-map', // 控制是否生成以及如何生成 source map // entry: './src/index.js', entry: { app: './src/index.js', print: './src/print.js' }, // ... };
如今来手动制造一些错误:
src/print.js
export default function printMe() { - console.log('I get called from print.js!'); + cosnole.log('I get called from print.js!'); }
打包以后打开index.html
再点击按钮,你就会看到控制台显示以下报错:
Uncaught ReferenceError: cosnole is not defined at HTMLButtonElement.printMe (print.js:2)
如今,咱们很清楚哪里发生了错误,而后轻松地改正之。
你必定有这样的体验,开发时每次修改代码保存后都须要从新手动构建代码并手动刷新浏览器以观察修改效果,这是很麻烦的,因此,咱们要实时加载代码。可喜的是,webpack 提供了对实时加载代码的支持。咱们须要安装 webpack-dev-server 以得到支持。
$ npm i --save-dev webpack-dev-server
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { devtool: 'inline-source-map', devServer: { // 检测代码变化并自动从新编译并自动刷新浏览器 contentBase: path.resolve(__dirname, 'dist') // 设置静态资源的根目录 }, // entry: './src/index.js', entry: { app: './src/index.js', print: './src/print.js' }, // ... };
package.json
{ ... "scripts": { "build": "webpack", "start": "webpack-dev-server --open" }, ... }
Tips
使用 webpack-dev-server 时,webpack 并无将全部生成的文件写入磁盘,而是放在内存中,提供更快的内存内访问,便于实时更新。
如今,能够直接运行npm start
(start
是 npm scripts 的内置属性,可直接运行),而后浏览器自动加载应用的页面,默认在localhost:8080
显示。
webpack 提供了对模块热替换 (或者叫热加载) 的支持。这一特性可以让应用运行的时候替换、增长或删除模块,而无需进行彻底的重载。想进一步地了解其工做机理,能够参见 Hot Module Replacement,但这并非必需的,你能够选择跳过机理部分继续往下阅读。
Tips
模块热替换(HMR)只更新发生变动(替换、添加、删除)的模块,而无需从新加载整个页面(实时加载,LiveReload),这样能够显著加快开发速度,一旦打开了 webpack-dev-server 的 hot 模式,在试图从新加载整个页面以前,热模式会尝试使用 HMR 来更新。
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); // 引入 webpack 便于调用其内置插件 module.exports = { devtool: 'inline-source-map', devServer: { contentBase: path.resolve(__dirname, 'dist'), hot: true, // 告诉 dev-server 咱们在用 HMR hotOnly: true // 指定若是热加载失败了禁止刷新页面 (这是 webpack 的默认行为),这样便于咱们知道失败是由于何种错误 }, // entry: './src/index.js', entry: { app: './src/index.js', // print: './src/print.js' }, // ... plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new CleanWebpackPlugin(['dist']), new webpack.HotModuleReplacementPlugin(), // 启用 HMR new webpack.NamedModulesPlugin() // 打印日志信息时 webpack 默认使用模块的数字 ID 指代模块,不便于 debug,这个插件能够将其替换为模块的真实路径 ], // ... };
Tips
webpack-dev-server 会为每一个入口文件建立一个客户端脚本,这个脚本会监控该入口文件的依赖模块的更新,若是该入口文件编写了 HMR 处理函数,它就能接收依赖模块的更新,反之,更新会向上冒泡,直到客户端脚本仍没有处理函数的话,webpack-dev-server 会从新加载整个页面。若是入口文件自己发生了更新,由于向上会冒泡到客户端脚本,而且不存在 HMR 处理函数,因此会致使页面重载。
咱们已经开启了 HMR 的功能,HMR 的接口已经暴露在module.hot
属性之下,咱们只须要调用 HMR API 便可实现热加载。当“被加载模块”发生改变时,依赖该模块的模块便能检测到改变并接收改变以后的模块。
src/index.js
import _ from 'lodash'; import printMe from './print.js'; // import './style.css'; // import Icon from './icon.jpg'; // import Data from './data.json'; function component() { const element = document.createElement('div'); const btn = document.createElement('button'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); // element.classList.add('hello'); // const myIcon = new Image(); // myIcon.src = Icon; // element.appendChild(myIcon); // console.log(Data); btn.innerHTML = 'Click me and check the console!'; btn.onclick = printMe; element.appendChild(btn); return element; } document.body.appendChild(component()); if(module.hot) { // 习惯上咱们会检查是否能够访问 `module.hot` 属性 module.hot.accept('./print.js', function() { // 接受给定依赖模块的更新,并触发一个回调函数来对这些更新作出响应 console.log('Accepting the updated printMe module!'); printMe(); }); }
npm start
之。为了演示效果,咱们作以下修改:
src/print.js
export default function printMe() { - console.log('I get called from print.js!'); + console.log('Updating print.js...'); }
咱们会看到控制台打印出的信息中含有如下几行:
index.js:33 Accepting the updated printMe module! print.js:2 Updating print.js... log.js:23 [HMR] Updated modules: log.js:23 [HMR] - ./src/print.js log.js:23 [HMR] App is up to date.
Tips
webpack-dev-server 在 inline mode (此为默认模式) 时,会为每一个入口起点 (entry) 建立一个客户端脚本,因此你会在上面的输出中看到有些信息重复输出两次。
可是当你点击页面的按钮时,你会发现控制台输出的是旧的printMe
函数输出的信息,由于onclick
事件绑定的还是原始的printMe
函数。咱们须要在module.hot.accept
里更新绑定。
src/index.js
import _ from 'lodash'; import printMe from './print.js'; // import './style.css'; // import Icon from './icon.jpg'; // import Data from './data.json'; // ... // document.body.appendChild(component()); var element = component(); document.body.appendChild(element); if(module.hot) { module.hot.accept('./print.js', function() { console.log('Accepting the updated printMe module!'); // printMe(); document.body.removeChild(element); element = component(); document.body.appendChild(element); }); }
Tips
uglifyjs-webpack-plugin 升级到 v0.4.6 时没法正确压缩 ES6 的代码,因此上面有些代码采用 ES5 以暂时方便后面的压缩,详见 #49。
模块热替换也能够用于样式的修改,效果跟控制台修改同样同样的。
src/index.js
import _ from 'lodash'; import printMe from './print.js'; import './style.css'; // import Icon from './icon.jpg'; // import Data from './data.json'; // ...
npm start
之,作以下修改:
/* ... */ body { background-color: yellow; }
能够发如今不重载页面的前提下咱们对样式的修改进行了热加载,棒!
咱们只须要运行webpack -p
(至关于 webpack --optimize-minimize --define process.env.NODE_ENV="'production'"
)这个命令,即可以自动构建生产版本的应用,这个命令会完成如下步骤:
UglifyJsPlugin
(webpack.optimize.UglifyJsPlugin) 压缩 JS 文件 (此插件和 uglifyjs-webpack-plugin 相同)LoaderOptionsPlugin
插件,这个插件是用来迁移的,见 document 值得一提的是,webpack -p
设置的process.env.NODE_ENV
环境变量,是用于编译后的代码的,只有在打包后的代码中,这一环境变量才是有效的。若是在 webpack 配置文件中引用此环境变量,获得的是 undefined,能够参见 #2537。可是,有时咱们确实须要在 webpack 配置文件中使用 process.env.NODE_ENV
,怎么办呢?一个方法是运行NODE_ENV='production' webpack -p
命令,不过这个命令在Windows中是会出问题的。为了解决兼容问题,咱们采用 cross-env 解决跨平台的问题。
$ npm i --save-dev cross-env
package.json
{ ... "scripts": { "build": "cross-env NODE_ENV=production webpack -p", "start": "webpack-dev-server --open" }, ... }
如今能够在配置文件中使用process.env.NODE_ENV
了。
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { // ... output: { // filename: 'bundle.js', // filename: '[name].bundle.js', filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js', // 在配置文件中使用`process.env.NODE_ENV` path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new CleanWebpackPlugin(['dist']), // new webpack.HotModuleReplacementPlugin(), // 关闭 HMR 功能 new webpack.NamedModulesPlugin() ], // ... };
Tips
[chunkhash]不能和 HMR 一块儿使用,换句话说,不该该在开发环境中使用 [chunkhash] (或者 [hash]),这会致使许多问题。详情见 #2393 和 #377。
build 之,咱们获得了生产版本的压缩好的打包文件。
有时咱们会须要为不一样的环境配置不一样的配置文件,能够选择 简易方法,这里咱们采用较为先进的方法。先准备一个基本的配置文件,包含了全部环境都包含的配置,而后用 webpack-merge 将它和特定环境的配置文件合并并导出,这样就减小了基本配置的重复。
$ npm i --save-dev webpack-merge
webpack.common.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { entry: { app: './src/index.js', print: './src/print.js' }, output: { path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new CleanWebpackPlugin(['dist']) ], module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.(png|svg|jpg|gif)$/, use: [ 'file-loader' ] }, { test: /\.(woff|woff2|eot|ttf|otf)$/, use: [ 'file-loader' ] } ] } };
webpack.dev.js
const path = require('path'); const webpack = require('webpack'); const Merge = require('webpack-merge'); const CommonConfig = require('./webpack.common.js'); module.exports = Merge(CommonConfig, { devtool: 'cheap-module-eval-source-map', devServer: { contentBase: path.resolve(__dirname, 'dist'), hot: true, hotOnly: true }, output: { filename: '[name].bundle.js' }, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('development') // 在编译的代码里设置了`process.env.NODE_ENV`变量 }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin() ] });
webpack.prod.js
const path = require('path'); const webpack = require('webpack'); const Merge = require('webpack-merge'); const CommonConfig = require('./webpack.common.js'); module.exports = Merge(CommonConfig, { devtool: 'cheap-module-source-map', output: { filename: '[name].[chunkhash].js' }, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }), new webpack.optimize.UglifyJsPlugin() ] });
package.json
{ ... "scripts": { "build": "cross-env NODE_ENV=production webpack -p", "start": "webpack-dev-server --open", "build:dev": "webpack-dev-server --open --config webpack.dev.js", "build:prod": "webpack --progress --config webpack.prod.js" }, ... }
如今只需执行npm run build:dev
或npm run build:prod
即可以获得开发版或者生产版了!
Tips
webpack 命令行选项见 Command Line Interface。
咱们先建立一个新文件:
$ cd src && touch another.js
src/another.js
import _ from 'lodash'; console.log(_.join(['Another', 'module', 'loaded!'], ' '));
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { // ... entry: { app: './src/index.js', // print: './src/print.js' another: './src/another.js' }, // ... };
cd .. && npm run build
之,咱们发现用入口分离的代码获得了两个大文件,这是由于两个入口文件都引入了lodash
,这很大程度上形成了冗余,在同一个页面中咱们只须要引入一个lodash
就能够了。
咱们使用 CommonsChunkPlugin 插件来将相同的部分提取出来放到一个单独的模块中。
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { // devtool: 'inline-source-map', // ... output: { // filename: 'bundle.js', filename: '[name].bundle.js', // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new CleanWebpackPlugin(['dist']), new webpack.optimize.CommonsChunkPlugin({ name: 'common' // 抽取出的模块的模块名 }), // new webpack.HotModuleReplacementPlugin(), // new webpack.NamedModulesPlugin() ], // ... };
build 之,能够看到结果中包含如下部分:
app.bundle.js 6.14 kB 0 [emitted] app another.bundle.js 185 bytes 1 [emitted] another common.bundle.js 73.2 kB 2 [emitted] common index.html 314 bytes [emitted]
咱们把lodash
分离出来了。
咱们还能够选择以动态引入的方式来实现代码分离,借助 import() 实现之。
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); // const webpack = require('webpack'); module.exports = { // ... entry: { app: './src/index.js', // print: './src/print.js' // another: './src/another.js' }, output: { // filename: 'bundle.js', filename: '[name].bundle.js', chunkFilename: '[name].bundle.js', // 指定非入口块文件输出的名字 // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new CleanWebpackPlugin(['dist']) // new webpack.optimize.CommonsChunkPlugin({ // name: 'common' // }), // new webpack.HotModuleReplacementPlugin(), // new webpack.NamedModulesPlugin() ], // ... };
src/index.js
// import _ from 'lodash'; import printMe from './print.js'; // import './style.css'; // import Icon from './icon.jpg'; // import Data from './data.json'; function component() { // 此函数原来的内容所有注释掉... return import(/* webpackChunkName: "lodash" */ 'lodash').then(function(_) { const element = document.createElement('div'); const btn = document.createElement('button'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); btn.innerHTML = 'Click me and check the console!'; btn.onclick = printMe; element.appendChild(btn); return element; }).catch(function(error) { console.log('An error occurred while loading the component') }); } // document.body.appendChild(component()); // var element = component(); // document.body.appendChild(element); // 本来热加载的部分所有注释掉... component().then(function(component) { document.body.appendChild(component); });
Tips
注意上面中的/* webpackChunkName: "lodash" */
这段注释,它并非无关紧要的,它能帮助咱们结合output.chunkFilename
把分离出的模块最终命名为lodash.bundle.js
而非[id].bundle.js
。
如今 build 之看看吧。
既然有了import()
,咱们能够选择在须要的时候才加载相应的模块,减小了应用初始化时加载大量暂不须要的模块的压力,这能让咱们的应用更高效地运行。
src/print.js
console.log('The print.js module has loaded! See the network tab in dev tools...'); export default function printMe() { // console.log('Updating print.js...'); console.log('Button Clicked: Here\'s "some text"!'); }
src/index.js
import _ from 'lodash'; // 其余引入注释... function component() { const element = document.createElement('div'); const btn = document.createElement('button'); element.innerHTML = _.join(['Hello', 'webpack'], ' '); // element.classList.add('hello'); // const myIcon = new Image(); // myIcon.src = Icon; // element.appendChild(myIcon); // console.log(Data); btn.innerHTML = 'Click me and check the console!'; // btn.onclick = printMe; element.appendChild(btn); btn.onclick = function() { import(/* webpackChunkName: "print" */ './print') .then(function(module) { const printMe = module.default; // 引入模块的默认函数 printMe(); }); }; return element; // 本来的动态引入注释... } document.body.appendChild(component()); // var element = component(); // document.body.appendChild(element); // 热加载部分注释 // component().then(function(component) { // document.body.appendChild(component); // });
构建之,控制台此时并没有输出,点击按钮,会看到控制台以下输出:
print.bundle.js:1 The print.js module has loaded! See the network tab in dev tools... print.bundle.js:1 Button Clicked: Here's "some text"!
说明 print 模块只在咱们点击时才引入了,すっげえ!
浏览器在初次加载网站时,会下载不少文件,为了较少下载大量资源的压力,浏览器会对资源进行缓存 (caching),这样浏览器即可以更迅速地加载网站,可是咱们须要在文件内容发生改变时更新文件。
咱们能够在输出文件名上下手脚:
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); // const webpack = require('webpack'); module.exports = { // ... output: { // filename: 'bundle.js', filename: '[name].[chunkhash].js', // chunkFilename: '[name].bundle.js', // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, // ... };
Tips
[chunkhash] 是内容相关的,只要内容发生了改变,构建后文件名的 hash 就会发生改变。
还有一个要点是提取出第三方库放到单独模块中,由于它们是不太可能频繁发生改变的,因此无需屡次加载这些模块,提取的方法用 CommonsChunkPlugin 插件,这个插件上文中提到过,指定入口文件名时它会提取改入口文件为单个文件,不指定则会提取 webpack 的运行时代码。
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { // ... entry: { app: './src/index.js', vendor: [ // 第三方库能够统一放在这个入口一块儿合并 'lodash' ] // print: './src/print.js' // another: './src/another.js' }, output: { // filename: 'bundle.js', filename: '[name].[chunkhash].js', chunkFilename: '[name].bundle.js', // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new CleanWebpackPlugin(['dist']), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' // 将 vendor 入口处的代码放入 vendor 模块 }), new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' // 将 webpack 自身的运行时代码放在 runtime 模块 }) // new webpack.HotModuleReplacementPlugin(), // new webpack.NamedModulesPlugin() ], // ... };
Tips
包含 vendor 的 CommonsChunkPlugin 实例必须在包含 runtime 的以前,不然会报错。
src/index.js
// import _ from 'lodash'; // ... // ...
若是咱们在 src 下新建一个文件h.js
,再在index.js
中引入它,保存,构建之,咱们发现有些没改变的模块的 hash 也发生了改变,这是由于加入h.js
后它们的module.id
变了,但这明显是不合理的。在开发环境,咱们能够用 NamedModulesPlugin 将 id 换成具体路径名。而在生产环境,咱们可使用 HashedModuleIdsPlugin。
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { // ... plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new webpack.HashedModuleIdsPlugin(), // 替换掉原来的`module.id` new CleanWebpackPlugin(['dist']), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }), new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' }) // new webpack.HotModuleReplacementPlugin(), // new webpack.NamedModulesPlugin() ], // ... };
再来执行刚才那波操做,就会发现无关修改的模块 hash 未变了。
Tips
你能够将 shim 简单理解为是用于兼容 API 的小型库。
使用 jQuery 时咱们习惯性地使用$
或jQuery
变量,每次都使用const $ = require(“jquery”)
引入的话太麻烦,若是能直接把这两个变量设置为全局变量岂不美滋滋?这样就能够在每一个模块中直接使用这两个变量了。为了兼容这一作法,咱们使用 ProvidePlugin 插件为咱们完成这一任务。
$ npm i --save jquery
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { // ... plugins: [ new HtmlWebpackPlugin({ title: 'webpack demo', filename: 'index.html' }), new webpack.ProvidePlugin({ // 设置全局变量 $: 'jquery', jQuery: 'jquery' }), new webpack.HashedModuleIdsPlugin(), new CleanWebpackPlugin(['dist']), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor' }), new webpack.optimize.CommonsChunkPlugin({ name: 'runtime' }) // new webpack.HotModuleReplacementPlugin(), // new webpack.NamedModulesPlugin() ], // ... };
src/print.js
console.log('The print.js module has loaded! See the network tab in dev tools...'); console.log($('title').text()); // 使用 jQuery export default function printMe() { // console.log('Updating print.js...'); console.log('Button Clicked: Here\'s "some text"!'); }
build,点击页面按钮,成功了。
另外,若是你须要在某些模块加载时设置该模块的全局变量,请看 这里。
终于写完了 :),也感谢你能耐心看到这里。webpack 这个工具的配置仍是有些麻烦的。可是呢,某人说这个东东前期会花比较多时间,后期会大大提升你的效率。因此呢,仍是拿下这个东东吧。有其余需求的话能够继续看官方的文档。遇到困难能够找:
我写好的 demo 文件放在了这里。