webpack 用于编译 JavaScript 模块。一旦完成安装,你就可以通过 webpack 的 CLI 或 API 与其配合交互。
一、安装
要安装最新版本或特定版本,请运行以下命令之一:
$ npm install --save-dev webpack
$ npm install --save-dev webpack@<version>
如果你使用 webpack 4+ 版本,你还需要安装 CLI。
$ npm install --save-dev webpack-cli
全局安装
以下的 NPM 安装方式,将使 webpack 在全局环境下可用:
$ npm install --global webpack
不推荐全局安装 webpack。这会将你项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中,可能会导致构建失败。
二、起步
1)创建一个本地页面
1、首先我们创建一个目录,初始化 npm,然后 在本地安装 webpack,接着安装 webpack-cli(此工具用于在命令行中运行 webpack):
$ mkdir webpack-demo && cd webpack-demo //创建目录
$ npm init -y //创建package.json文件
$ npm install webpack webpack-cli --save-dev //安装 webpack-cli
2、现在我们将创建以下目录结构、文件和内容:
webpack-demo
|- package.json
+ |- /dist
+ |- index.html
+ |- /src
+ |- index.js
- “源”代码(/src)是用于书写和编辑的代码。
- “分发”代码(/dist)是构建后代码最小化和优化后的“输出”目录
- 要在 index.js 中打包 lodash 依赖,我们需要在本地安装 library:
$ npm install --save lodash
- src/index.js
import _ from 'lodash';
function component() {
var element = document.createElement('div');
// lodash 是由当前 script 脚本 import 导入进来的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
- index.html
<!doctype html>
<html>
<head>
<title>起步</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
3、执行
$ npx webpack
4、在浏览器中打开 index.html,如果一切访问都正常,你应该能看到以下文本:'Hello webpack'。
2)模块
- ES2015 中的 import 和 export 语句已经被标准化。虽然大多数浏览器还无法支持它们,但是 webpack 却能够提供开箱即用般的支持。
- webpack 不会更改代码中除 import 和 export 语句以外的部分。如果你在使用其它 ES2015 特性,请确保你在 webpack 的 loader 系统中使用了一个像是 Babel 的转译器。
3)使用一个配置文件
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
4)NPM 脚本(NPM Scripts)
在 package.json 添加一个 npm 脚本(npm script):
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
现在,可以使用 npm run dev 命令,来替代我们之前使用的 npx 命令。
5)结论
现在,你已经实现了一个基本的构建过程,此刻你的项目应该和如下类似:
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
|- /node_modules
如果你使用的是 npm 5,你可能还会在目录中看到一个 package-lock.json 文件。
三、管理资源
webpack 最出色的功能之一就是,除了 JavaScript,还可以通过 loader 引入任何其他类型的文件。
1)加载 CSS
为了从 JavaScript 模块中 import 一个 CSS 文件,你需要在 module 配置中 安装并添加 style-loader 和 css-loader:
$ npm install --save-dev style-loader css-loader
webpack.config.js中添加模块
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
webpack 根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的 loader。在这种情况下,以 .css 结尾的全部文件,都将被提供给 style-loader 和 css-loader。
我们尝试一下,通过在项目中添加一个新的 style.css 文件,并将其导入到我们的 index.js 中:
|- /src
|- style.css
|- index.js
src/style.css
.hello {
color: red;
}
src/index.js
import _ from 'lodash';
import './style.css'; //引入css
function component() {
var element = document.createElement('div');
// lodash 是由当前 script 脚本 import 导入进来的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello'); //添加节点
return element;
}
document.body.appendChild(component());
现在运行构建命令:
$ npm run build
浏览器中打开 index.html,你应该看到 Hello webpack 现在的样式是红色。
查看页面的 head 标签。它包含了我们在 index.js 中导入的 style 块元素。
2)加载图片
背景和图标这些图片,使用 file-loader,我们可以轻松地将这些内容混合到 CSS 中:
$ npm install --save-dev file-loader
webpack.config.js中添加
module: {
{
test: /\.(png|svg|jpg|gif)$/,
use: [ 'file-loader']
}
}
我们向项目添加一个图像,然后看它是如何工作的,你可以使用任何你喜欢的图像:
|- /src
|- icon.png
|- style.css
|- index.js
src/index.js
import _ from 'lodash';
import './style.css';
import Icon from './icon.png'; //引入图片
function component() {
var element = document.createElement('div');
// lodash 是由当前 script 脚本 import 导入进来的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
var myIcon = new Image(); //将图像添加到我们现有的 div
myIcon.src = Icon;
element.appendChild(myIcon);
return element;
}
document.body.appendChild(component());
src/style.css
.hello {
color: red;
background: url('./icon.png');
}
现在运行构建命令:
$ npm run build
如果一切顺利,在Hello webpack 文本旁边将显示 img 元素。
想要压缩和优化你的图像。查看 image-webpack-loader 和 url-loader,以了解更多关于如果增强加载处理图片功能。
3)加载字体
像字体这样的其他资源如何处理呢?file-loader 和 url-loader 可以接收并加载任何文件,这就是说,我们可以将它们用于任何类型的文件,包括字体。让我们更新 webpack.config.js 来处理字体文件:
webpack.config.js中添加:
module: {
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [ 'file-loader' ]
}
}
在项目中添加一些字体文件:
|- /src
|- my-font.woff
|- my-font.woff2
|- icon.png
|- style.css
|- index.js
通过一个 @font-face 声明引入。本地的 url(...) 指令会被 webpack 获取处理,就像它处理图片资源一样:
src/style.css
@font-face {
font-family: 'MyFont';
src: url('./my-font.woff2') format('woff2'),
url('./my-font.woff') format('woff');
font-weight: 600;
font-style: normal;
}
.hello {
color: red;
font-family: 'MyFont';
background: url('./icon.png');
}
现在运行构建命令:
$ npm run build
重新打开 index.html 看看我们的 Hello webpack 文本显示是否换上了新的字体。如果一切顺利,你应该能看到变化。
4)加载数据
此外,可以加载的有用资源还有数据JSON 支持实际上是内置的,也就是说 import Data from './data.json' 默认将正常运行。
四、管理输出
随着引入所有资源的增长,手动地对 index.html 文件进行管理,一切就会变得困难起来。然而,可以通过一些插件,会使这个过程更容易操控。
1)预先准备
首先,让我们调整一下我们的项目:
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
|- print.js
|- /node_modules
我们在 src/print.js 文件中添加一些逻辑:
src/print.js
export default function printMe() {
console.log('I get called from print.js!');
}
并且在 src/index.js 文件中使用这个函数:
src/index.js
import _ from 'lodash';
import printMe from './print.js';
function component() {
var element = document.createElement('div');
var 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;
}
document.body.appendChild(component());
我们还要更新 dist/index.html 文件,来为 webpack 分离入口做好准备:
dist/index.html
<!doctype html>
<html>
<head>
<title>Output Management</title>
<script src="./print.bundle.js"></script>
</head>
<body>
<script src="./app.bundle.js"></script>
</body>
</html>
现在调整配置。我们将在 entry 添加 src/print.js 作为新的入口起点(print),然后修改 output,以便根据入口起点名称动态生成 bundle 名称:
webpack.config.js
const path = require('path');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
现在运行构建命令:
$ npm run build
我们可以看到,webpack 生成 print.bundle.js 和 app.bundle.js 文件,这也和我们在 index.html 文件中指定的文件名称相对应。如果你在浏览器中打开 index.html,就可以看到在点击按钮时会发生什么。
2)清理 /dist 文件夹
在每次构建前清理 /dist 文件夹,是比较推荐的做法,因此只会生成用到的文件。clean-webpack-plugin 是一个比较普及的管理插件,让我们安装和配置下。
$ npm install 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: {
app: './src/index.js',
print: './src/print.js'
},
plugins: [
new CleanWebpackPlugin(), //新增
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
现在执行 npm run build,再检查 /dist 文件夹。如果一切顺利,你现在应该不会再看到旧的文件,只有构建后生成的文件!
五、开发
1)使用 source map
当 webpack 打包源代码时,可能会很难追踪到错误和警告在源代码中的原始位置。
为了更容易地追踪错误和警告,JavaScript 提供了 [source map]功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js
,source map 就会明确的告诉你。
我们使用 inline-source-map 选项,这有助于解释说明我们的目的。
webpack.config.js
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
devtool: 'inline-source-map', //使用 inline-source-map 选项
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Development'
})
],
现在,让我们来做一些调试,在 print.js 文件中生成一个错误:
src/print.js
export default function printMe() {
- console.log('I get called from print.js!');
+ cosnole.error('I get called from print.js!');
}
运行 npm run build
现在在浏览器打开最终生成的 index.html 文件,点击按钮,并且在控制台查看显示的错误。错误应该如下:
Uncaught ReferenceError: cosnole is not defined
at HTMLButtonElement.printMe (print.js:2)
我们可以看到,此错误包含有发生错误的文件(print.js)和行号(2)的引用。这是非常有帮助的,因为现在我们知道了,所要解决的问题的确切位置。
2)选择一个开发工具
每次要编译代码时,手动运行 npm run build 就会变得很麻烦。
webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:
- webpack's Watch Mode
- webpack-dev-server
- webpack-dev-middleware
多数场景中,你可能需要使用 webpack-dev-server,但是不妨探讨一下以上的所有选项。
3)观察模式
我们添加一个用于启动 webpack 的观察模式的 npm script 脚本:
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
+ "watch": "webpack --watch",
"build": "webpack"
},
现在,你可以在命令行中运行 npm run watch,就会看到 webpack 编译代码,然而却不会退出命令行。这是因为 script 脚本还在观察文件。
现在,修改保存文件并检查终端窗口。应该可以看到 webpack 自动重新编译修改后的模块!
4)使用 webpack-dev-server
webpack-dev-server
提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading)。让我们设置以下:
npm install --save-dev webpack-dev-server
修改配置文件,告诉开发服务器(dev server),在哪里查找文件:
webpack.config.js
devtool: 'inline-source-map',
+ devServer: {
+ contentBase: './dist'
+ },
以上配置告知 webpack-dev-server,在 localhost:8080 下建立服务,将 dist 目录下的文件,作为可访问文件。
让我们添加一个 script 脚本,可以直接运行开发服务器(dev server):
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
+ "start": "webpack-dev-server --open",
"build": "webpack"
},
现在,我们可以在命令行中运行 npm start,就会看到浏览器自动加载页面。如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码
5)使用 webpack-dev-middleware
webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。
同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。
首先,安装 express 和 webpack-dev-middleware:
npm install --save-dev express webpack-dev-middleware
webpack.config.js
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
+ publicPath: '/'
}
下一步就是设置我们自定义的 express 服务:
project
webpack-demo
|- package.json
|- webpack.config.js
+ |- server.js
|- /dist
|- /src
|- index.js
|- print.js
|- /node_modules
server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
//告诉express使用webpack-dev-middleware并使用webpack.config.js
//以配置文件为基础。
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
//服务监听3000端口。
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
现在,添加一个 npm script,以使我们更方便地运行服务:
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
"start": "webpack-dev-server --open",
+ "server": "node server.js",
"build": "webpack"
},
现在,在你的终端执行 npm run server
打开浏览器,跳转到 http://localhost:3000,你应该看到你的webpack 应用程序已经运行!
六、模块热替换
模块热替换功能会在应用程序运行过程中替换、添加或删除模块而无需重新加载整个页面。
它允许在运行时更新各种模块,而无需进行完全刷新。
1)启用 HMR
启用此功能实际上相当简单。而我们要做的,就是更新 webpack-dev-server的配置,和使用 webpack 内置的 HMR 插件。我们还要删除掉 print.js
的入口起点,因为它现在正被 index.js
模块使用。
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'
+ app: './src/index.js'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
+ hot: true
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Hot Module Replacement'
}),
+ new webpack.NamedModulesPlugin(),
+ new webpack.HotModuleReplacementPlugin()
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
- "start": "webpack-dev-server --open",
+ "start": "webpack-dev-server --hotOnly",
"build": "webpack"
},
在起步阶段,我们将通过在命令行中运行 npm start 来启动并运行 dev server。
修改 index.js 文件,以便当 print.js 内部发生变更时可以告诉 webpack 接受更新的模块。
index.js
document.body.appendChild(component());
+
+ if (module.hot) {
+ module.hot.accept('./print.js', function() {
+ console.log('Accepting the updated printMe module!');
+ printMe();
+ })
+ }
更改 print.js 中 console.log 的输出内容。
print.js
export default function printMe() {
- console.log('I get called from print.js!');
+ console.log('Updating print.js...')
}
你将会在浏览器中看到如下的输出。
console
[HMR] Waiting for update signal from WDS...
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module!
+ 0.4b8ee77….hot-update.js:10 Updating print.js...
+ main.js:4330 [HMR] Updated modules:
+ main.js:4330 [HMR] - 20
+ main.js:4330 [HMR] Consider using the NamedModulesPlugin for module names.
2)通过 Node.js API
当使用 webpack dev server 和 Node.js API 时,不要将 dev server 选项放在 webpack 配置对象(webpack config object)中。而是,在创建选项时,将其作为第二个参数传递。例如:
new WebpackDevServer(compiler, options)
想要启用 HMR,还需要修改 webpack 配置对象,使其包含 HMR 入口起点。webpack-dev-server
package 中具有一个叫做 addDevServerEntrypoints
的方法,你可以通过使用这个方法来实现。这是关于如何使用的一个小例子:
dev-server.js
const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');
const config = require('./webpack.config.js');
const options = {
contentBase: './dist',
hot: true,
host: 'localhost'
};
webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);
server.listen(5000, 'localhost', () => {
console.log('dev server listening on port 5000');
});
3)问题
模块热替换可能比较难掌握。为了说明这一点,我们回到刚才的示例中。如果你继续点击示例页面上的按钮,你会发现控制台仍在打印这旧的 printMe
功能。
这是因为按钮的 onclick
事件仍然绑定在旧的 printMe
函数上。
为了让它与 HMR 正常工作,我们需要使用 module.hot.accept
更新绑定到新的 printMe
函数上:
index.js
- document.body.appendChild(component());
+ let element = component(); // 当 print.js 改变导致页面重新渲染时,重新获取渲染的元素
+ 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(); // 重新渲染页面后,component 更新 click 事件处理
+ document.body.appendChild(element);
})
}
4)HMR 修改样式表
借助于 style-loader
的帮助,CSS 的模块热替换实际上是相当简单的。当更新 CSS 依赖模块时,此 loader 在后台使用 module.hot.accept
来修补(patch) <style>
标签。
所以,可以使用以下命令安装两个 loader :
npm install --save-dev style-loader css-loader
接下来我们来更新 webpack 的配置,让这两个 loader 生效。
webpack.config.js
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader']
+ }
+ ]
+ },
热加载样式表,与将其导入模块一样简单:
project
webpack-demo
| - package.json
| - webpack.config.js
| - /dist
| - bundle.js
| - /src
| - index.js
| - print.js
+ | - styles.css
styles.css
body {
background: blue;
}
index.js
import _ from 'lodash';
import printMe from './print.js';
+ import './styles.css';
将 body 上的样式修改为 background: red;,你应该可以立即看到页面的背景颜色随之更改,而无需完全刷新。
styles.css
body {
- background: blue;
+ background: red;
}
网友评论