webpack摸索笔记
上一个链接,入门webpack看这篇文章最好:https://segmentfault.com/a/1190000006178770
1、先安装好node
2、建个项目文件
3,、window+r,打开终端 初始化这个项目
npm init -y
3.1、本地安装webpack(官方建议本地安装)
npm install --save-dev webpack
3.2、如果用webpack4+版本,还需要安装webpack-cli
npm install --save-dev webpack-cli
4、根目录下创建src/index.html 引入main.js 这个main.js不用创建,等会儿自己会生dist文件下成(奇怪),而且默认的入口文件是src下的index.js.
5、运行一下npx webpack,后,发现在dist文件下出现了main.js.都是大包过后的代码(奇怪)
4/5出现的奇怪现象,都是在未自定义配置文件时出现的情况,猜测是webpack4+版本将会默认设置一份配置文件,放在了我看不到的地方。
接下来我们设置自己的一个webpack配置文件,用来管理webpack打包
在跟目录下创建一个webpack.config.js文件
里面可以配置文件的入口,和出口!!!
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
以上代码表示,入口文件是index.js,出口文件是bundle.js,这个bunld.js文件同样不用自己创建,只要在这配置好了,打包命令执行后会自动创建;
切记将index.html中script标签引入的js由main.js该成bundle.js,因为现在的出口文件是bundle.js
现在,让我们通过新配置文件再次执行构建:
npx webpack --config webpack.config.js
NPM 脚本(NPM Scripts)
考虑到用 CLI 这种方式来运行本地的 webpack 不是特别方便,我们可以设置一个快捷方式。在 package.json 添加一个 npm 脚本(npm script):
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev":"webpack"
},
现在,可以使用 npm run dev
命令,来替代我们之前使用的 npx
命令。
加载 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'
]
}
]
}
在src下创建一个style.css文件,写上一下样式
在src下的index.js中通过import引入这个css
import './style.css';
运行一下打包命令npm run dev 发现样式生效了
加载图片
假想,现在我们正在下载 CSS,但是我们的背景和图标这些图片,要如何处理呢?使用 file-loader,我们可以轻松地将这些内容混合到 CSS 中:
在webpack.config.js文件的rules数组下,添加这一项配置
{//加载图片(js或css中引入图片时)
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options:{
name: '[path][name].[ext]',//配置自定义文件模板
outputPath: '.',//配置打包后的输出目录(.代表在dist目录中生成)
}
}
]
},
下载包
npm install --save-dev file-loader
在src下加一个名为icon的图片
在style.css里加一个背景样式,用上这个图片
在index.js文件里引入这张图
import Icon from './icon.png';
就可以在这个js里用这张图片了
执行一下打包命令npm run dev,就可以看到效果了!!!
加载字体
那么,像字体这样的其他资源如何处理呢?file-loader 和 url-loader 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,包括字体。让我们更新 webpack.config.js
来处理字体文件:
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
}
在网上下载个字体文件加到src文件下,在style.css引入(这只是一个简单的引入方式)
@font-face {
font-family: 'webfont';
src:url('./fontname.ttf') format('truetype');
}
.hello {
color: red;
font-family: 'webfont' !important;
background: url('./icon.png') no-repeat;
}
打包运行 npm run dev会发现字体变了
加载数据
此外,可以加载的有用资源还有数据,如 JSON 文件,CSV、TSV 和 XML。类似于 NodeJS,JSON 支持实际上是内置的,也就是说 import Data from './data.json'
默认将正常运行。要导入 CSV、TSV 和 XML,你可以使用 csv-loader 和 xml-loader。让我们处理这三类文件:
npm install --save-dev csv-loader xml-loader
更改webpack.config.js文件
{
test: /\.(csv|tsv)$/,
use: [
'csv-loader'
]
},
{
test: /\.xml$/,
use: [
'xml-loader'
]
}
在src中加入xml文件(data.xml)
<?xml version="1.0" encoding="UTF-8"?> <note> <to>Mary</to> <from>John</from> <heading>Reminder</heading> <body>Call Cindy on Tuesday</body> </note>
在src/index.js里导入xml文件(其它的文件也可以导入)
import Data from './data.xml';
console.log(Data);
npm run dev 运行一下,会在页面的控制台看到xml文件内容
回退处理
为了更好的 往下探索,我需要将一下文件删除
/src
- |- data.xml
- |- my-font.woff
- |- my-font.woff2
- |- icon.png
- |- style.css
webpack.config.js的module模块删除
- 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'
- ]
- },
- {
- test: /\.(csv|tsv)$/,
- use: [
- 'csv-loader'
- ]
- },
- {
- test: /\.xml$/,
- use: [
- 'xml-loader'
- ]
- }
- ]
- }
src/index.js 恢复到原来代码
import _ from 'lodash';
function component() {
var element = document.createElement('div');
element.innerHTML = _.join(['hello', 'world'], ' ');
return element;
}
document.body.appendChild(component());
管理输出
到目前为止,我们在 index.html
文件中手动引入所有资源,然而随着应用程序增长,并且一旦开始对文件名使用哈希(hash)]并输出多个 bundle,手动地对 index.html
文件进行管理,一切就会变得困难起来。然而,可以通过一些插件,会使这个过程更容易操控。
其实撸到这,自己就大致了解webpack的模式了,加插件,改配置。。。后面的大家可以自己撸了,奉上中文文档地址
想了想还是再写写吧,写理解与思路,不详细上代码了
我们可以将文件入口设置成多文件入口,(需要在webpack.config.js)里配置一下,
多文件入口配置好之后,dist文件中会生成对应的打包好的文件,我们之前是将这些打包好的文件固定名字引入index.html文件的,但是当我们新增入口文件,或改变入口文件名时,而index.html还是引用的原来的打包好得文件名,手动去改,势必会比较麻烦,所以我们需要借用HtmlWebpackPlugin插件来解决这个问题;
npm install --save-dev html-webpack-plugin
上诉是安装这个插件
下面更改webpack.config.js配置
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { app: './src/index.js', print: './src/print.js' }, plugins: [ new HtmlWebpackPlugin({ title: 'Output Management',
template: 'index.html',//配置html模板
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
在我们构建之前,你应该了解,虽然在 dist/
文件夹我们已经有 index.html
这个文件,然而 HtmlWebpackPlugin
还是会默认生成 index.html
文件。这就是说,它会用新生成的 index.html
文件,把我们的原来的替换。(好高大上!!!!),这样我们无论新增还是更改入口文件的名字,这个新生成的index.html都会将打包好的js文件对应加载进去的。
清理 /dist
文件夹
我们每次执行打包命令,都会在dist文件下生成打包内容,看着很乱,所以在打包命令前,将dist文件清理一下,会让dist保存的都是有用文件;
构建开发环境
使用 source map
当 webpack 打包源代码时,可能会很难追踪到错误和警告在源代码中的原始位置。例如,如果将三个源文件(a.js
, b.js
和 c.js
)打包到一个 bundle(bundle.js
)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会简单地指向到 bundle.js
。这并通常没有太多帮助,因为你可能需要准确地知道错误来自于哪个源文件。
为了更容易地追踪错误和警告,JavaScript 提供了 source map 功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js
,source map 就会明确的告诉你。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
devtool: 'inline-source-map',
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
在print.js文件中生成一个错误(故意将console写错)
export default function printMe() {
}
然后进行打包命令,打开index.html页面点击按钮,浏览器控制台会显示出错误在哪个js文件,这样就很好定位错误了;
选择一个开发工具(观察者模式)
每次要编译代码时,手动运行 npm run build
就会变得很麻烦
webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码,俗称观察者模式!!!(下面提供三种方法实现)
1、webpack's Watch Mode
2、webpack-dev-server
3、webpack-dev-middleware
多数场景中,你可能需要使用 webpack-dev-server
,但是不妨探讨一下以上的所有选项。
使用观察模式
你可以指示 webpack "watch" 依赖图中的所有文件以进行更改。如果其中一个文件被更新,代码将被重新编译,所以你不必手动运行整个构建。
我们添加一个用于启动 webpack 的观察模式的 npm script 脚本:
package.json文件
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
"build": "webpack",
"dev": "webpack"
},
现在,你可以在命令行中运行 npm run watch
,就会看到 webpack 编译代码,然而却不会退出命令行。这是因为 script 脚本还在观察文件。
这时该一下print.js里的代码,不需要运行npm run dev只需要刷新一下页面,内容就变过来了,这就是watch模式。
使用 webpack-dev-server(经常使用)
webpack-dev-server
为你提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading)。让我们设置以下:
npm install --save-dev webpack-dev-server
修改配置文件,告诉开发服务器(dev server),在哪里查找文件:
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { app: './src/index.js', print: './src/print.js' }, devtool: 'inline-source-map', devServer: {//使用观察者模式(启动一个服务器,当文件有变动时,页面立即改变)
},
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, '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",
"dev": "webpack"
},
现在,我们可以在命令行中运行 npm start
,就会看到浏览器自动加载页面。如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码。试一下!
会自动弹出一个url为localhost:8080的页面,此时更改任何一个代码,都会实时的反馈到页面上
使用 webpack-dev-middleware
ebpack-dev-middleware
是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server
在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。接下来是一个 webpack-dev-middleware 配合 express server 的示例。
首先,安装 express
和 webpack-dev-middleware
:
npm install --save-dev express webpack-dev-middleware
接下来我们需要对 webpack 的配置文件做一些调整,以确保中间件(middleware)功能能够正确启用:
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist'
},
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
}
};
publicPath
也会在服务器脚本用到,以确保文件资源能够在 http://localhost:3000
下正确访问,我们稍后再设置端口号。下一步就是设置我们自定义的 express
服务:
首先在根目录加一个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);
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
// Serve the files on port 3000.
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
现在,添加一个 npm script,以使我们更方便地运行服务:
package.json
{
"name": "module",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
"start": "webpack-dev-server --open",
"server": "node server.js",
"build": "webpack",
"dev": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^2.1.1",
"csv-loader": "^3.0.2",
"express": "^4.16.4",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"lodash": "^4.17.11",
"style-loader": "^0.23.1",
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0",
"webpack-dev-middleware": "^3.6.2",
"webpack-dev-server": "^3.3.1",
"xml-loader": "^1.2.1"
},
"dependencies": {}
}
现在,在你的终端执行 npm run server
,将会启动这个服务器
现在,打开浏览器,跳转到 http://localhost:3000
,你应该看到你的webpack 应用程序已经运行!
调整文本编辑器
使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。
要在一些常见的编辑器中禁用此功能,请查看以下列表:
- Sublime Text 3 - 在用户首选项(user preferences)中添加
atomic_save: "false"
。 - IntelliJ - 在首选项(preferences)中使用搜索,查找到 "safe write" 并且禁用它。
- Vim - 在设置(settings)中增加
:set backupcopy=yes
。 - WebStorm - 在
Preferences > Appearance & Behavior > System Settings
中取消选中 Use"safe write"
。
模块热替换
模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。本页面重点介绍实现,而概念页面提供了更多关于它的工作原理以及为什么它有用的细节。
启用此功能实际上相当简单。而我们要做的,就是更新 webpack-dev-server 的配置,和使用 webpack 内置的 HMR 插件。我们还要删除掉 print.js
的入口起点,因为它现在正被 index.js
模块使用。
如果你使用了 webpack-dev-middleware
而没有使用 webpack-dev-server
,请使用 webpack-hot-middleware
package 包,以在你的自定义服务或应用程序上启用 HMR。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: {
app: './src/index.js'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
hot: true
},
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management'
}),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/'
}
};
注意,我们还添加了 NamedModulesPlugin
,以便更容易查看要修补(patch)的依赖。在起步阶段,我们将通过在命令行中运行 npm start
来启动并运行 dev server。
现在,我们来修改 index.js
文件,以便当 print.js
内部发生变更时可以告诉 webpack 接受更新的模块。
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', 'world'], ' ');
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.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}
更改 print.js
中 console.log
的输出内容,你将会在浏览器中看到相应的改变。(控制台上能看到响应的改变,但点击按钮时弹出的还是之前的代码,必须刷新一下页面才能更新过来,奇怪)
通过 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');
});
运行一下npm start
问题
模块热替换可能比较难掌握。为了说明这一点,我们回到刚才的示例中。如果你继续点击示例页面上的按钮,你会发现控制台仍在打印这旧的 printMe
功能。
这是因为按钮的 onclick
事件仍然绑定在旧的 printMe
函数上。
为了让它与 HMR 正常工作,我们需要使用 module.hot.accept
更新绑定到新的 printMe
函数上:
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', 'world'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}
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!');
document.body.removeChild(element);
element = component(); // 重新渲染页面后,component 更新 click 事件处理
document.body.appendChild(element);
})
}
此时,再更改print.js里的内容时,页面会即使显示出 对象的更改,点击按钮时,console出来的是最新更改的值
这只是一个例子,但还有很多其他地方可以轻松地让人犯错。幸运的是,存在很多 loader(其中一些在下面提到),使得模块热替换的过程变得更容易。
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
配置rules
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
之后添加个样式表,更改样式,页面会实时改变
其他代码和框架
社区还有许多其他 loader 和示例,可以使 HMR 与各种框架和库(library)平滑地进行交互……
- React Hot Loader:实时调整 react 组件。
- Vue Loader:此 loader 支持用于 vue 组件的 HMR,提供开箱即用体验。
- Elm Hot Loader:支持用于 Elm 程序语言的 HMR。
- Redux HMR:无需 loader 或插件!只需对 main store 文件进行简单的修改。
- Angular HMR:No loader necessary! A simple change to your main NgModule file is all that's required to have full control over the HMR APIs.没有必要使用 loader!只需对主要的 NgModule 文件进行简单的修改,由 HMR API 完全控制。
如果你知道任何其他 loader 或插件,能够有助于或增强模块热替换(Hot Module Replacement),请提交一个 pull request 以添加到此列表中!
tree shaking(移除javascript中多余代码)
新的 webpack 4 正式版本,扩展了这个检测能力,通过 package.json
的 "sideEffects"
属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
添加一个通用模块
在我们的项目中添加一个新的通用模块文件 src/math.js
,此文件导出两个函数:
src/math.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
接着,更新入口脚本,使用其中一个新方法,并且为了简单,将 lodash
删除:
src/index.js
import { cube } from './math.js';
function component() {
var element = document.createElement('pre');
element.innerHTML = [
'Hello webpack!',
'5 cubed is equal to ' + cube(5)
].join('\n\n');
return element;
}
document.body.appendChild(component());
注意,我们并未从 src/math.js
模块中 import
导入 square
方法。这个功能是所谓的“未引用代码(dead code)”,也就是说,应该删除掉未被引用的 export
。现在让我们运行我们的npm 脚本 npm run build
,并检查输出的 bundle:
bundle里依然会存在square
方法,下面会解决这个问题
将文件标记为无副作用(side-effect-free)
在一个纯粹的 ESM 模块世界中,识别出哪些文件有副作用很简单。然而,我们的项目无法达到这种纯度,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。
这种方式是通过 package.json 的 "sideEffects"
属性来实现的。
{
"name": "your-project",
"sideEffects": false
}
如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false
,来告知 webpack,它可以安全地删除未用到的 export 导出。
「副作用」的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。
如果你的代码确实有一些副作用,那么可以改为提供一个数组:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js"
]
}
数组方式支持相关文件的相对路径、绝对路径和 glob 模式。它在内部使用 micromatch。
注意,任何导入的文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader
并导入 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
]
}
最后,还可以在 module.rules
配置选项 中设置 "sideEffects"
。
压缩输出
通过如上方式,我们已经可以通过 import
和 export
语法,找出那些需要删除的“未使用代码(dead code)”,然而,我们不只是要找出,还需要在 bundle 中删除它们。为此,我们将使用 -p
(production) 这个 webpack 编译标记,来启用 uglifyjs 压缩插件。
注意,--optimize-minimize
标记也会在 webpack 内部调用 UglifyJsPlugin
。
从 webpack 4 开始,也可以通过 "mode"
配置选项轻松切换到压缩输出,只需设置为 "production"
。
注意,也可以在命令行接口中使用 --optimize-minimize
标记,来使用 UglifyJSPlugin
。
准备就绪后,然后运行另一个命令 npm run build
,看看输出结果有没有发生改变。
你发现 dist/bundle.js
中的差异了吗?显然,现在整个 bundle 都已经被精简过,但是如果仔细观察,则不会看到 square
函数被引入,但会看到 cube
函数的修改版本(function r(e){return e*e*e}n.a=r
)。现在,随着 tree shaking 和代码压缩,我们的 bundle 减小几个字节!虽然,在这个特定示例中,可能看起来没有减少很多,但是,在具有复杂的依赖树的大型应用程序上运行时,tree shaking 或许会对 bundle 产生显著的体积优化。
结论
为了学会使用 tree shaking,你必须……
- 使用 ES2015 模块语法(即
import
和export
)。 - 在项目
package.json
文件中,添加一个 "sideEffects" 入口。 - 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如
UglifyJSPlugin
)。
你可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
生产环境构建
配置
开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
虽然,以上我们将生产环境和开发环境做了略微区分,但是,请注意,我们还是会遵循不重复原则(Don't repeat yourself - DRY),保留一个“通用”配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge
的工具。通过“通用”配置,我们不必在环境特定(environment-specific)的配置中重复代码。
我们先从安装 webpack-merge
开始,并将之前指南中已经成型的那些代码再次进行分离:
npm install --save-dev webpack-merge
我们需要将webpack.config.js删除,新建webpack.common.js、webpack.dev.js、webpack.prod.js
webpack.common.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js'
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Production'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
contentBase: './dist'
}
});
webpack.prod.js
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin()
]
});
现在,在 webpack.common.js
中,我们设置了 entry
和 output
配置,并且在其中引入这两个环境公用的全部插件。在 webpack.dev.js
中,我们为此环境添加了推荐的 devtool
(强大的 source map)和简单的 devServer
配置。最后,在 webpack.prod.js
中,我们引入了之前在 tree shaking 指南中介绍过的 UglifyJSPlugin
。
注意,在环境特定的配置中使用 merge()
很容易地包含我们在 dev
和 prod
中的常见配置。webpack-merge
工具提供了多种合并(merge)的高级功能,但是在我们的用例中,无需用到这些功能。
现在,我们把 scripts
重新指向到新配置。我们将 npm start
定义为开发环境脚本,并在其中使用 webpack-dev-server
,将 npm run build
定义为生产环境脚本:
{
"name": "module",
"sideEffects": [
"./src/some-side-effectful-file.js",
"*.css"
],
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"start": "webpack-dev-server --open --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^2.1.1",
"csv-loader": "^3.0.2",
"express": "^4.16.4",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"lodash": "^4.17.11",
"style-loader": "^0.23.1",
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0",
"webpack-dev-middleware": "^3.6.2",
"webpack-dev-server": "^3.3.1",
"webpack-merge": "^4.2.1",
"xml-loader": "^1.2.1"
},
"dependencies": {}
}
随意运行这些脚本(npm run start npm run build),然后查看输出结果的变化,
发现在开发环境下dist文件是空的,生产环境下dist文件有生成的代码
然后我们继续添加一些生产环境配置
Minification(压缩)
注意,虽然 UglifyJSPlugin
是代码压缩方面比较好的选择,但是还有一些其他可选择项。以下有几个同样很受欢迎的插件:
如果决定尝试以上这些,只要确保新插件也会按照 tree shake 指南中所陈述的,具有删除未引用代码(dead code)的能力足矣。
source map
我们鼓励你在生产环境中启用 source map,因为它们对调试源码(debug)和运行基准测试(benchmark tests)很有帮助。虽然有如此强大的功能,然而还是应该针对生成环境用途,选择一个构建快速的推荐配置(具体细节请查看 devtool
)。对于本指南,我们将在生产环境中使用 source-map
选项,而不是我们在开发环境中用到的
inline-source-map
:
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'source-map',
plugins: [
new UglifyJSPlugin({
sourceMap: true
})
]
});
避免在生产中使用 inline-***
和 eval-***
,因为它们可以增加 bundle 大小,并降低整体性能。
指定环境
许多 library 将通过与 process.env.NODE_ENV
环境变量关联,以决定 library 中应该引用哪些内容。例如,当不处于生产环境中时,某些 library 为了使调试变得容易,可能会添加额外的日志记录(log)和测试(test)。其实,当使用 process.env.NODE_ENV === 'production'
时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin
为所有的依赖定义这个变量:
webpack.prod.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'source-map',
plugins: [
new UglifyJSPlugin({
sourceMap: true
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
});
技术上讲,NODE_ENV
是一个由 Node.js 暴露给执行脚本的系统环境变量。通常用于决定在开发环境与生产环境(dev-vs-prod)下,服务器工具、构建脚本和客户端 library 的行为。然而,与预期不同的是,无法在构建脚本 webpack.config.js
中,将 process.env.NODE_ENV
设置为 "production"
,请查看 #2537。因此,例如 process.env.NODE_ENV === 'production' ? '[name].[hash].bundle.js' : '[name].bundle.js'
这样的条件语句,在 webpack 配置文件中,无法按照预期运行。
如果你正在使用像 react
这样的 library,那么在添加此 DefinePlugin 插件后,你应该看到 bundle 大小显著下降。还要注意,任何位于 /src
的本地代码都可以关联到 process.env.NODE_ENV 环境变量,所以以下检查也是有效的:
src/index.js
import { cube } from './math.js';
if (process.env.NODE_ENV !== 'production') {
console.log('Looks like we are in development mode!');
}
function component() {
var element = document.createElement('pre');
element.innerHTML = [
'Hello webpack!',
'5 cubed is equal to ' + cube(5)
].join('\n\n');
return element;
}
document.body.appendChild(component());
Split CSS
正如在管理资源中最后的 加载 CSS 小节中所提到的,通常最好的做法是使用 ExtractTextPlugin
将 CSS 分离成单独的文件。在插件文档中有一些很好的实现例子。disable
选项可以和 --env
标记结合使用,以允许在开发中进行内联加载,推荐用于热模块替换和构建速度。
CLI 替代选项
以上描述也可以通过命令行实现。例如,--optimize-minimize
标记将在后台引用 UglifyJSPlugin
。和以上描述的 DefinePlugin
实例相同,--define process.env.NODE_ENV="'production'"
也会做同样的事情。并且,webpack -p
将自动地调用上述这些标记,从而调用需要引入的插件。
这些简便方式虽然都很不错,但是我们通常建议只使用配置方式,因为在这两种场景中下,配置方式能够更好地帮助你了解自己正在做的事情。配置方式还可以让你更方便地控制这两个插件中的其他选项。
代码分离
代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
有三种常用的代码分离方法:
- 入口起点:使用
entry
配置手动地分离代码。 - 防止重复:使用
CommonsChunkPlugin
去重和分离 chunk。 - 动态导入:通过模块的内联函数调用来分离代码。
入口起点(entry points)
这是迄今为止最简单、最直观的分离代码的方式。不过,这种方式手动配置较多,并有一些陷阱,我们将会解决这些问题。先来看看如何从 main bundle 中分离另一个模块:
先在src文件下简历一个another-module.js文件
another-module.js
import _ from 'lodash';//lodash是一个工具库(含有很多集成的方法)
console.log(
_.join(['Another', 'module', 'loaded!'], ' ')
);
webpack.config.js
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
plugins: [
new HTMLWebpackPlugin({
title: 'Code Splitting'
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
我们在package.json里再添加一个script脚本,用来专门执行使用webpack.config.js的配置文件的运行脚本(自定义)
"scripts": {
"start": "webpack-dev-server --open --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
"common":"webpack --open --config webpack.config.js"
},
这样就可以运行npm run common命令来运行基于webpack.config.js文件的脚本了
看一下终端(cmd)的情况。。。(略)
正如前面提到的,这种方法存在一些问题:
- 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
- 这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。
以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js
中也引入过 lodash
,这样就在两个 bundle 中造成重复引用。接着,我们通过使用 CommonsChunkPlugin
来移除重复的模块。
防止重复(prevent duplication)
CommonsChunkPlugin
插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将之前的示例中重复的 lodash
模块去除:
以上CommonsChunkPlugin插件已经在webpack4+被屏弃了,使用以下设置方式就好
webpack.config.js
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
plugins: [
new HTMLWebpackPlugin({
title: 'Code Splitting'
}),
],
optimization: {//提取重复引用模块
splitChunks: {
name: 'common'
}
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
现在应该可以看出,index.bundle.js
中已经移除了重复的依赖模块。需要注意的是,CommonsChunkPlugin 插件将 lodash
分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。执行 npm run common
查看效果:
以下是由社区提供的,一些对于代码分离很有帮助的插件和 loaders:
ExtractTextPlugin
: 用于将 CSS 从主应用程序中分离。bundle-loader
: 用于分离代码和延迟加载生成的 bundle。promise-loader
: 类似于bundle-loader
,但是使用的是 promises。
CommonsChunkPlugin
插件还可以通过使用显式的 vendor chunks 功能,从应用程序代码中分离 vendor 模块。
动态导入(dynamic imports)
当涉及到动态代码拆分时,webpack 提供了两个类似的技术。对于动态导入,第一种,也是优先选择的方式是,使用符合 ECMAScript 提案 的 import()
语法。第二种,则是使用 webpack 特定的 require.ensure
。让我们先尝试使用第一种……
import()
调用会在内部用到 promises。如果在旧有版本浏览器中使用 import()
,记得使用 一个 polyfill 库(例如 es6-promise 或 promise-polyfill),来 shim Promise
在我们开始本节之前,先从配置中移除掉多余的 entry
和 CommonsChunkPlugin
,因为接下来的演示中并不需要它们:
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js'
},
plugins: [
new HTMLWebpackPlugin({
title: 'Code Splitting'
}),
],
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
注意,这里使用了 chunkFilename
,它决定非入口 chunk 的名称。想了解 chunkFilename
更多信息,请查看 output 相关文档。接着,更新我们的项目,移除掉那些现在不会用到的文件:
下来删除掉src中的another-module.js文件
现在,我们不再使用静态导入 lodash
,而是通过使用动态导入来分离一个 chunk:
src/index.js
// import _ from 'lodash';//删除静态引入lodash的方式,接下来动态导入
function getComponent() {
// var element = document.createElement('pre');
// element.innerHTML = [
// 'Hello webpack!',
// '5 cubed is equal to ' + cube(5)
// ].join('\n\n');
return import(/* webpackChunkName: "lodash" */ 'lodash').then(_=>{
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}).catch(error=>'An error occurred while loading the component');
}
// document.body.appendChild(component());
getComponent().then(component => {
document.body.appendChild(component);
})
注意,在注释中使用了 webpackChunkName
。这样做会导致我们的 bundle 被命名为 lodash.bundle.js
,而不是 [id].bundle.js
。想了解更多关于 webpackChunkName
和其他可用选项,请查看 import()
相关文档。让我们执行 webpack,查看 lodash
是否会分离到一个单独的 bundle:
我们发现,dist目录下多了一个vendors~lodash.bundle.js文件,这是动态导入的结果
由于 import()
会返回一个 promise,因此它可以和 async
函数一起使用。但是,需要使用像 Babel 这样的预处理器和Syntax Dynamic Import Babel Plugin。下面是如何通过 async
函数简化代码:
src/index.js
async function getComponent() {
// return import(/* webpackChunkName: "lodash" */ 'lodash').then(_=>{
// var element = document.createElement('div');
// element.innerHTML = _.join(['Hello', 'webpack'], ' ');
// return element;
// }).catch(error=>'An error occurred while loading the component');
var element = document.createElement('div');
const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
getComponent().then(component => {
document.body.appendChild(component);
})
bundle 分析(bundle analysis)
如果我们以分离代码作为开始,那么就以检查模块作为结束,分析输出结果是很有用处的。官方分析工具 是一个好的初始选择。下面是一些社区支持(community-supported)的可选工具:
- webpack-chart: webpack 数据交互饼图。
- webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
- webpack-bundle-analyzer: 一款分析 bundle 内容的插件及 CLI 工具,以便捷的、交互式、可缩放的树状图形式展现给用户。
懒加载(按需加载)
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
示例
我们在代码分离中的例子基础上,进一步做些调整来说明这个概念。那里的代码确实会在脚本运行的时候产生一个分离的代码块 lodash.bundle.js
,在技术概念上“懒加载”它。问题是加载这个包并不需要用户的交互 -- 意思是每次加载页面的时候都会请求它。这样做并没有对我们有很多帮助,还会对性能产生负面影响。
我们试试不同的做法。我们增加一个交互,当用户点击按钮的时候用 console 打印一些文字。但是会等到第一次交互的时候再加载那个代码块(print.js
)。为此,我们返回到代码分离的例子中,把 lodash
放到主代码块中,重新运行代码分离中的代码 final Dynamic Imports example。
在src下简历一个print.js
console.log('The print.js module has loaded! See the network tab in dev tools...');
export default () => {
console.log('Button Clicked: Here\'s "some text"!');
}
src/index.js
import _ from 'lodash';
function component() {
var element = document.createElement('div');
var button = document.createElement('button');
var br = document.createElement('br');
button.innerHTML = 'Click me and look at the console!';
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.appendChild(br);
element.appendChild(button);
// Note that because a network request is involved, some indication
// of loading would need to be shown in a production-level site/app.
button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
var print = module.default;
print();
});
return element;
}
document.body.appendChild(component());
注意当调用 ES6 模块的 import()
方法(引入模块)时,必须指向模块的 .default
值,因为它才是 promise 被处理后返回的实际的 module
对象。
现在运行 npm run common 来验证一下我们的懒加载功能:
框架
许多框架和类库对于如何用它们自己的方式来实现(懒加载)都有自己的建议。这里有一些例子:
缓存
以上,我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist
目录,然后把打包后的内容放置在此目录中。只要 /dist
目录中的内容部署到服务器上,客户端(通常是浏览器)就能够访问网站此服务器的网站及其资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。
此指南的重点在于通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。
通过使用 output.filename
进行文件名替换,可以确保浏览器获取到修改后的文件。[hash]
替换可以用于在文件名中包含一个构建相关(build-specific)的 hash,但是更好的方式是使用 [chunkhash]
替换,在文件名中包含一个 chunk 相关(chunk-specific)的哈希。
让我们使用起步 中的示例,以及管理输出 中的 plugins
来作为项目的基础,所以我们不必手动处理维护 index.html
文件:
webpack.config.js
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js'
},
plugins: [
new HTMLWebpackPlugin({
title: 'Caching'
}),
],
output: {
filename: '[name].[chunkhash].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
使用此配置,然后运行我们的构建脚本 npm run common
,应该产生以下输出:。。。dist文件下打包出来的文件名字中包含hash值
可以看到,bundle 的名称是它内容(通过 hash)的映射。如果我们不做修改,然后再次运行构建,我们以为文件名会保持不变。然而,如果我们真的运行,可能会发现情况并非如此:(译注:这里的意思是,如果不做修改,文件名可能会变,也可能不会。)
这也是因为 webpack 在入口 chunk 中,包含了某些样板(boilerplate),特别是 runtime 和 manifest。(译注:样板(boilerplate)指 webpack 运行时的引导代码)
输出可能会因当前的 webpack 版本而稍有差异。新版本不一定有和旧版本相同的 hash 问题,但我们以下推荐的步骤,仍然是可靠的。
提取模板(Extracting Boilerplate)
就像我们之前从代码分离了解到的,CommonsChunkPlugin
可以用于将模块分离到单独的文件中。然而 CommonsChunkPlugin
有一个较少有人知道的功能是,能够在每次修改后的构建结果中,将 webpack 的样板(boilerplate)和 manifest 提取出来。通过指定 entry
配置中未用到的名称,此插件会自动将我们需要的内容提取到单独的包中:
webpack.config.js(因为CommonsChunkPlugin在webpack4+已经屏弃,所以用下面代码替换,
)
const path = require('path'); const HTMLWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { entry: { index: './src/index.js' }, plugins: [ new CleanWebpackPlugin(), new HTMLWebpackPlugin({ title: 'Caching' }) ], optimization: {//提取重复引用模块 splitChunks: { name: 'manifest
' }
},
output: {
filename: '[name].[chunkhash].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
让我们再次构建,然后查看提取出来的 manifest
bundle:(运行一下npm run common)
将第三方库(library)(例如 lodash
或 react
)提取到单独的 vendor
chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用客户端的长效缓存机制,可以通过命中缓存来消除请求,并减少向服务器获取资源,同时还能保证客户端代码和服务器端代码版本一致。这可以通过使用新的 entry(入口)
起点,以及再额外配置一个 CommonsChunkPlugin
实例的组合方式来实现:
webpack.config.js
const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js',
vendor:[
'lodash'
]
},
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
title: 'Caching'
})
],
optimization: {//提取重复引用模块
splitChunks: {
name: 'vendor',
},
runtimeChunk: {//必须保证vendor在manifest之前 用来提取模板
name: 'manifest'
}
},
output: {
filename: '[name].[chunkhash].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
模块标识符(Module Identifiers)
我们向项目中再添加一个模块src/ print.js
:
export default function print(text) {
console.log(text);
};
src/index.js
import _ from 'lodash';
import Print from './print';
function component() {
var element = document.createElement('div');
// lodash 是由当前 script 脚本 import 导入进来的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.onclick = Print.bind(null, 'Hello webpack!');
return element;
}
document.body.appendChild(component());
再次运行构建,然后我们期望的是,只有 main
bundle 的 hash 发生变化,然而……
我们可以看到这三个文件的 hash 都变化了。这是因为每个 module.id
会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。因此,简要概括:
main
bundle 会随着自身的新增内容的修改,而发生变化。vendor
bundle 会随着自身的module.id
的修改,而发生变化。manifest
bundle 会因为当前包含一个新模块的引用,而发生变化。
第一个和最后一个都是符合预期的行为 -- 而 vendor
的 hash 发生变化是我们要修复的。幸运的是,可以使用两个插件来解决这个问题。第一个插件是 NamedModulesPlugin
,将使用模块的路径,而不是数字标识符。虽然此插件有助于在开发过程中输出结果的可读性,然而执行时间会长一些。第二个选择是使用 HashedModuleIdsPlugin
,推荐用于生产环境构建:
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js',
vendor:[
'lodash'
]
},
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
title: 'Caching'
}),
new webpack.HashedModuleIdsPlugin()
],
optimization: {//提取重复引用模块
splitChunks: {
name: 'vendor',
},
runtimeChunk: {//必须保证vendor在manifest之前 用来提取模板
name: 'manifest'
}
},
output: {
filename: '[name].[chunkhash].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
再运行一下。我们可以看到,这两次构建中,vendor
bundle 的文件名称,不会发生变化了。
结论
缓存从凌乱变得清晰直接。然而以上预先演示,只能帮助你在部署一致性(deploying consistent)和资源可缓存(cachable assets)方面,有个好的开始。想要了解更多信息,请查看以下的进一步阅读部分
创建 library(库)
除了打包应用程序代码,webpack 还可以用于打包 JavaScript library。以下指南适用于希望流水线化(streamline)打包策略的 library 作者。
创建一个 library(library:文库)
假设你正在编写一个名为 webpack-numbers
的小的 library,可以将数字 1 到 5 转换为文本表示,反之亦然,例如将 2 转换为 'two'。
基本的项目结构可能如下所示:
+ |- webpack.config.js
+ |- package.json
+ |- /src
+ |- index.js
+ |- ref.json
初始化 npm,安装 webpack 和 lodash:
src/ref.json
[{
"num": 1,
"word": "One"
}, {
"num": 2,
"word": "Two"
}, {
"num": 3,
"word": "Three"
}, {
"num": 4,
"word": "Four"
}, {
"num": 5,
"word": "Five"
}, {
"num": 0,
"word": "Zero"
}]
src/index.js
import _ from 'lodash';
import numRef from './ref.json';
export function numToWord(num) {
return _.reduce(numRef, (accum, ref) => {
return ref.num === num ? ref.word : accum;
}, '');
};
export function wordToNum(word) {
return _.reduce(numRef, (accum, ref) => {
return ref.word === word && word.toLowerCase() ? ref.num : accum;
}, -1);
};
该 library 的使用方式如下:
// ES2015 模块引入
import * as webpackNumbers from 'webpack-numbers';
// CommonJS 模块引入
var webpackNumbers = require('webpack-numbers');
// ...
// ES2015 和 CommonJS 模块调用
webpackNumbers.wordToNum('Two');
// ...
// AMD 模块引入
require(['webpackNumbers'], function ( webpackNumbers) {
// ...
// AMD 模块调用
webpackNumbers.wordToNum('Two');
// ...
});
用户还可以通过 script 标签来加载和使用此 library:
<!doctype html> <html> ... <script src="https://unpkg.com/webpack-numbers"></script> <script> // ... // 全局变量 webpackNumbers.wordToNum('Five') // window 对象中的属性 window.webpackNumbers.wordToNum('Five') // ... </script> </html>
注意,我们还可以通过以下配置方式,将 library 暴露:
- global 对象中的属性,用于 Node.js。
this
对象中的属性。
完整的 library 配置和相关代码请参阅 webpack library 示例。
基本配置
现在,让我们以某种方式打包这个 library,能够实现以下几个目标:
- 不打包
lodash
,而是使用externals
来 require 用户加载好的 lodash。 - 设置 library 的名称为
webpack-numbers
. - 将 library 暴露为一个名为
webpackNumbers
的变量。 - 能够访问其他 Node.js 中的 library。
此外,用户应该能够通过以下方式访问 library:
- ES2015 模块。例如
import webpackNumbers from 'webpack-numbers'
。 - CommonJS 模块。例如
require('webpack-numbers')
. - 全局变量,当通过
script
脚本引入时
我们可以从这个基本的 webpack 配置开始:
var path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js'
}
};
外部化 lodash
现在,如果执行 webpack
,你会发现创建了一个非常巨大的文件。如果你查看这个文件,会看到 lodash 也被打包到代码中。在这种场景中,我们更倾向于把 lodash
当作 peerDependency
。也就是说,用户应该已经将 lodash
安装好。因此,你可以放弃对外部 library 的控制,而是将控制权让给使用 library 的用户。
这可以使用 externals
配置来完成:
webpack.config.js
var path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js'
},
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
};
这意味着你的 library 需要一个名为 lodash
的依赖,这个依赖在用户的环境中必须存在且可用。
运行一下npm run dev(这个命令需要在packge.json中配置),我们发现,打包后,lodash没有被打包到文件里,打包后的代码变的很少!!!
注意,如果你计划只是将 library 用作另一个 webpack bundle 中的依赖模块,则可以将 externals
指定为数组。
外部扩展的限制
对于从一个依赖目录中,调用多个文件的 library:
import A from 'library/one';
import B from 'library/two';
// ...
无法通过在 externals 中指定 library
目录的方式,将它们从 bundle 中排除。你需要逐个排除它们,或者使用正则表达式排除。
externals: [
'library/one',
'library/two',
// Everything that starts with "library/"
/^library\/.+$/
]
暴露 library
对于用途广泛的 library,我们希望它能够兼容不同的环境,例如 CommonJS,AMD,Node.js 或者作为一个全局变量。为了让你的 library 能够在各种用户环境(consumption)中可用,需要在 output
中添加 library
属性:
webpack.config.js
var path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: 'webpackNumbers'
},
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
};
注意,library
设置绑定到 entry
配置。对于大多数库,指定一个入口起点就足够了。虽然构建多个库也是也可以的,然而还可以直接通过将主入口脚本(index script)暴露部分导出,来作为单个入口起点则相对简单。不推荐使用数组
作为库的 entry
。
当你在 import 引入模块时,这可以将你的 library bundle 暴露为名为 webpackNumbers
的全局变量。为了让 library 和其他环境兼容,还需要在配置文件中添加 libraryTarget
属性。这是可以控制 library 如何以不同方式暴露的选项。
webpack.config.js
var path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'webpack-numbers.js',
library: 'webpackNumbers',
libraryTarget: 'umd'
},
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_'
}
}
};
可以通过以下方式暴露 library:
- 变量:作为一个全局变量,通过
script
标签来访问(libraryTarget:'var'
)。 - this:通过
this
对象访问(libraryTarget:'this'
)。 - window:通过
window
对象访问,在浏览器中(libraryTarget:'window'
)。 - UMD:在 AMD 或 CommonJS 的
require
之后可访问(libraryTarget:'umd'
)。
如果设置了 library
但没设置 libraryTarget
,则 libraryTarget
默认为 var
,详细说明请查看 output 配置文档。查看 output.libraryTarget
,以获取所有可用选项的详细列表。
在 webpack 3.5.5 中,使用 libraryTarget: { root:'_' }
将无法正常工作(参考 issue 4824) 所述)。然而,可以设置 libraryTarget: { var: '_' }
来将 library 作为全局变量。
最终步骤
循生产环境指南中的步骤,来优化生产环境下的输出。那么,我们还需要通过设置 package.json
中的 main
字段,添加生成 bundle 的文件路径。
{
"name": "library",
"version": "1.0.0",
"description": "",
"main": "dist/webpack-numbers.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev":"webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"lodash": "^4.17.11",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0"
}
}
或者,按照这里的指南添加为标准模块:
{
"name": "library",
"version": "1.0.0",
"description": "",
"main": "dist/webpack-numbers.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev":"webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"lodash": "^4.17.11",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0"
},
"module": "src/index.js"
}
键(key) main
是指 package.json
标准,以及 module
是 一个提案,此提案允许 JavaScript 生态系统升级使用 ES2015 模块,而不会破坏向后兼容性。
module
属性应指向一个使用 ES2015 模块语法的脚本,但不包括浏览器或 Node.js 尚不支持的其他语法特性。这使得 webpack 本身就可以解析模块语法,如果用户只用到 library 的某些部分,则允许通过 tree shaking 打包更轻量的包。
现在你可以将其发布为一个 npm 包,并且在 unpkg.com 找到它并分发给你的用户。
shimming(修补,垫片)
webpack
编译器(compiler)能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些第三方的库(library)可能会引用一些全局依赖(例如 jQuery
中的 $
)。这些库也可能创建一些需要被导出的全局变量。这些“不符合规范的模块”就是 shimming 发挥作用的地方。
我们不推荐使用全局的东西!在 webpack 背后的整个概念是让前端开发更加模块化。也就是说,需要编写具有良好的封闭性(well contained)、彼此隔离的模块,以及不要依赖于那些隐含的依赖模块(例如,全局变量)。请只在必要的时候才使用本文所述的这些特性。
shimming 另外一个使用场景就是,当你希望 polyfill 浏览器功能以支持更多用户时。在这种情况下,你可能只想要将这些 polyfills 提供给到需要修补(patch)的浏览器(也就是实现按需加载)。
shimming 全局变量
创建一个项目,包含src/index.js和dist文件
还记得我们之前用过的 lodash
吗?出于演示的目的,让我们把这个模块作为我们应用程序中的一个全局变量。要实现这些,我们需要使用 ProvidePlugin
插件。
使用 ProvidePlugin
后,能够在通过 webpack 编译的每个模块中,通过访问一个变量来获取到 package 包。如果 webpack 知道这个变量在某个模块中被使用了,那么 webpack 将在最终 bundle 中引入我们给定的 package。让我们先移除 lodash
的 import
语句,并通过插件提供它:
src/index.js
// import _ from 'lodash';//删除掉
function component() {
var element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.ProvidePlugin({
_: 'lodash'
})
]
};
本质上,我们所做的,就是告诉 webpack……
如果你遇到了至少一处用到 lodash
变量的模块实例,那请你将 lodash
package 包引入进来,并将其提供给需要用到它的模块。
如果我们 run build,将会看到同样的输出:。。。。略
我们还可以使用 ProvidePlugin
暴露某个模块中单个导出值,只需通过一个“数组路径”进行配置(例如 [module, child, ...children?]
)。所以,让我们做如下设想,无论 join
方法在何处调用,我们都只会得到的是 lodash
中提供的 join
方法。
src/index.js
// import _ from 'lodash';//删除掉
function component() {
var element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.ProvidePlugin({
// _: 'lodash'
join: ['lodash', 'join']
})
]
};
这样就能很好的与 tree shaking 配合,将 lodash
库中的其他没用到的部分去除。
细粒度 shimming
一些传统的模块依赖的 this
指向的是 window
对象。在接下来的用例中,调整我们的 index.js
:
当模块运行在 CommonJS 环境下这将会变成一个问题,也就是说此时的 this
指向的是 module.exports
。在这个例子中,你可以通过使用 imports-loader
覆写 this
:
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?this=>window'
}
]
},
plugins: [
new webpack.ProvidePlugin({
// _: 'lodash'
join: ['lodash', 'join']
})
]
};
全局 exports
让我们假设,某个库(library)创建出一个全局变量,它期望用户使用这个变量。为此,我们可以在项目配置中,添加一个小模块来演示说明:
在src中添加一个globals.js文件
src/globals.js
var file = 'blah.txt';
var helpers = {
test: function() { console.log('test something'); },
parse: function() { console.log('parse something'); }
}
你可能从来没有在自己的源码中做过这些事情,但是你也许遇到过一个老旧的库(library),和上面所展示的代码类似。在这个用例中,我们可以使用 exports-loader
,将一个全局变量作为一个普通的模块来导出。例如,为了将 file
导出为 file
以及将 helpers.parse
导出为 parse
,做如下调整:
npm install imports-loader --sace -dev
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?this=>window'
},
{
test: require.resolve('./src/globals.js'),
use: 'exports-loader?file,parse=helpers.parse'
}
]
},
plugins: [
new webpack.ProvidePlugin({
// _: 'lodash'
join: ['lodash', 'join']
})
]
};
现在从我们的 entry 入口文件中(即 src/index.js
),我们能 import { file, parse } from './globals.js';
,然后一切将顺利进行。
加载 polyfills(填充物)
目前为止我们所讨论的所有内容都是处理那些遗留的 package 包,让我们进入到下一个话题:polyfills。
有很多方法来载入 polyfills。例如,要引入 babel-polyfill
我们只需要如下操作:
npm install --save babel-polyfill
然后使用 import
将其添加到我们的主 bundle 文件:
src/index.js
import 'babel-polyfill';
function component() {
var element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert('Hmmm, this probably isn\'t a great idea...')
return element;
}
document.body.appendChild(component());
注意,我们没有将 import
绑定到变量。这是因为只需在基础代码(code base)之外,再额外执行 polyfills,这样我们就可以假定代码中已经具有某些原生功能。
polyfills 虽然是一种模块引入方式,但是并不推荐在主 bundle 中引入 polyfills,因为这不利于具备这些模块功能的现代浏览器用户,会使他们下载体积很大、但却不需要的脚本文件。
让我们把 import
放入一个新文件,并加入 whatwg-fetch
polyfill:
npm install --save whatwg-fetch
src/index.js
// import 'babel-polyfill';//删除掉
function component() {
var element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert('Hmmm, this probably isn\'t a great idea...')
return element;
}
document.body.appendChild(component());
在src下加一个ployfills.js文件
src/polyfills.js
import 'babel-polyfill';
import 'whatwg-fetch';
webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
polyfills: './src/polyfills.js',
index: './src/index.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?this=>window'
},
{
test: require.resolve('./src/globals.js'),
use: 'exports-loader?file,parse=helpers.parse'
}
]
},
plugins: [
new webpack.ProvidePlugin({
// _: 'lodash'
join: ['lodash', 'join']
})
]
};
如此之后,我们可以在代码中添加一些逻辑,根据条件去加载新的 polyfills.bundle.js
文件。你该如何决定,依赖于那些需要支持的技术以及浏览器。我们将做一些简单的试验,来确定是否需要引入这些 polyfills:
dist/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Getting Started</title> <script> var modernBrowser = ( 'fetch' in window && 'assign' in Object ); if ( !modernBrowser ) { var scriptElement = document.createElement('script'); scriptElement.async = false; scriptElement.src = '/polyfills.bundle.js'; document.head.appendChild(scriptElement); } </script> </head> <body> </body> </html>
现在,我们能在 entry 入口文件中,通过 fetch
获取一些数据:
src/index.js
// import 'babel-polyfill';//删除掉
function component() {
var element = document.createElement('div');
element.innerHTML = join(['Hello', 'webpack'], ' ');
// Assume we are in the context of `window`
this.alert('Hmmm, this probably isn\'t a great idea...')
return element;
}
document.body.appendChild(component());
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(json => {
console.log('We retrieved some data! AND we\'re confident it will work on a variety of browser distributions.')
console.log(json)
})
.catch(error => console.error('Something went wrong when fetching this data: ', error))
当我们开始执行构建时,polyfills.bundle.js
文件将会被载入到浏览器中,然后所有代码将正确无误的在浏览器中执行。请注意,以上的这些设定可能还会有所改进,我们只是对于如何解决「将 polyfills 提供给那些需要引入它的用户」这个问题,向你提供一个很棒的想法。
深度优化
。。。
渐进式网络应用程序
搭一个简单的服务器
npm install http-server --save-dev
package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack",
"build": "webpack",
"start": "http-server dist"
},
如果你之前没有操作过,请运行命令 npm run build
来构建你的项目。然后运行命令 npm start
。服务器就搭好了
如果你打开浏览器访问 http://localhost:8080
(即 http://127.0.0.1
),你应该会看到在 dist
目录创建出服务,并可以访问 webpack 应用程序。如果停止服务器然后刷新,则 webpack 应用程序不再可访问。
这就是我们最终要改变的现状。在本章结束时,我们应该要实现的是,停止服务器然后刷新,仍然可以查看应用程序正常运行。
添加 Workbox
添加 workbox-webpack-plugin 插件,并调整 webpack.config.js
文件:
npm install workbox-webpack-plugin --save-dev
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Progressive Web Application'
}),
new WorkboxPlugin.GenerateSW({
// 这些选项帮助 ServiceWorkers 快速启用
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true
})
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
有了 Workbox,我们再看下执行 npm run build
时会发生什么。。。
现在你可以看到,生成了 2 个额外的文件:sw.js
和体积很大的 precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
。sw.js
是 Service Worker 文件,precache-manifest.b5ca1c555e832d6fbf9462efd29d27eb.js
是 sw.js
引用的文件,所以它也可以运行。可能在你本地生成的文件会有所不同;但是你那里应该会有一个 sw.js
文件。
所以,值得高兴的是,我们现在已经创建出 Service Worker 的高兴点。接下来该做什么?
注册我们的 Service Worker
我们将注册 Service Worker,使其出场并开始表演。通过添加以下注册代码来完成此操作:
index.js
import _ from 'lodash';
import printMe from './print.js';
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
再次运行 npm build build
来构建包含注册代码版本的应用程序。然后用 npm start
启动服务。访问 http://localhost:8080
并查看 console 控制台。在那里你应该看到:SW registered
现在来进行测试。停止服务器并刷新页面。如果浏览器能够支持 Service Worker,你应该可以看到你的应用程序还在正常运行。然而,服务器已经停止了服务,此刻是 Service Worker 在提供服务。
结论
你已经使用 Workbox 项目构建了一个离线应用程序。开始进入将 web app 改造为 PWA 的旅程。你现在可能想要考虑下一步做什么。在这里的谷歌文档中可以找到一些不错的资源。
TypeScript(介绍了webpack是如何与typescript集成的)略
。。。。。