1. 自动清理构建目录产物
在之前的示例中,每次构建之后,会在 dist
目录下新增文件,长期这样之后,会积攒许多的无用文件。
通过 npm scripts 删除构建目录(不够优雅)
该插件默认会删除 output 指定的输出目录
安装依赖: npm i clean-webpack-plugin -D
配置 webpack.config.js:
Copy const { CleanWebpackPlugin } = require('clean-webpack-plugin')
..
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
2. 自动补全 CSS3 前缀
浏览器渲染引擎(内核) 划分:
参考资料
使用 autoprefixer 插件,可以给某些属性(如 display: flex) 添加相应的浏览器前缀,使得浏览器支持某些属性。
安装依赖: npm i postcss-loader autoprefixer -D
配置 webpack.config.js
Copy module.exports = {
module: {
rules:[
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')()
// 以下这种传入浏览器的方式被废弃,改用在 package.json 中定义
// require('autoprefixer')({ browsers: ['last 2 version'] })
]
}
}
]
},
]
}
}
// 配置 package.json
"browserslist": [
"last 2 version",
">1%",
"ios 7"
]
3. 移动端 CSS px 转 rem
安装依赖: npm i px2rem-loader -D
配置 webpack.config.js
Copy {
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')()
]
}
},
{
loader: 'px2rem-loader',
options: {
remUnit: 75, // 1rem = 75px
remPrecision: 8 // 像素转换为rem之后,保留的小数点位数
}
}
]
}
此时,就能把所有引入的 css 中的像素值,转换为 rem 的值
注意,在测试的过程中,发现好像不能解析 less 语法,会报代码错误,有机会可以再次测试
此刻在 html 页面中引入 淘宝 flexible库 即可实现,移动端自适应问题。因为 屏幕大小变化时, flexible 库使得 rem 的值发生变化,而我们的样式经过 px2rem-loader 之后,所有的尺寸单位都是 rem 了,自然会做自适应。
4. 静态资源内联
对于有一些资源,如埋点js,flexible js 等,我们希望这些 js 首先被加载,而不是夹杂在 bundle.js 中,从而更快的得到加载。
另外,如果资源内联 html 模板中,那么会跟随 html 请求一并回来,减少 http 请求数量.
html片段(如 meta 信息) 和 js 内联 : 使用 raw-loader
raw-loader 实质上就是将文件内容读取出来,填充到 html 模板对应的位置。
css 内联 的两种方式:
将 css 的样式内联到 html 中:
Copy rules: [
{
test: /\.less$/,
use: [
loader: 'style-loader',
options: {
insertAt: 'top', // 样式插入到 head
singleton: true // 将所有的 style 标签合成一个标签
},
"css-loader",
"less-loader"
]
}
]
使用 html-inline-css-webpack-plugin (推荐)
将打包出来的 css 插入到 html 对应的位置。
安装依赖: npm i raw-loader@0.5.1 -D
在 html 模板中使用:
Copy <head>
// 以下代码 是 es6 中 ${} 的语法,读取文件内容填充到这个位置
${ require('raw-loader!./meta.html') }
<script>
// 针对 js 文件,可能内部使用了 es6 的语法,因此可以再用 babel-loader
${ require('raw-loader!babel-loader!../node_modules/xxx.js') }
</script>
<title>Docment</title>
</head>
5. 通用的多页面应用打包方案
每一次页面跳转的时候,后台服务器都会返回一个新的 html 文档,这种叫做多页面应用。
思路就是每个页面对应一个 entry,打包出一个 js 文件。同时有一个 html-webpack-plugin 生成对应的页面。
示例:
Copy entry: {
index: './src/index/index.js',
search: './src/search/index.js',
}
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname,'src/index/index.html')
}),
new HtmlWebpackPlugin({
template: path.join(__dirname,'src/search/index.html')
}),
]
于是我们可以约定如下的数据结构:
每个目录对应一个页面,目录中的 index.js 对应 entry, index.html 作为页面模板 。
Copy -src
----search // 目录
------index.js
------index.html
----index // 目录
------index.js
------index.html
利用 glob.sync
读取目录: entry: glob.sync(path.join(__dirname, './src/*/index.js'))
安装依赖: npm i glob -D
Copy // 设置 MPA, 产生 entry 和 htmlWebpackPlugin 的配置
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname,'./src/*/index.js'))
entryFiles.forEach(entryFile => {
const match = entryFile.match(/src\/(.*)\/index\.js$/)
const pageName = match && match[1];
entry[pageName] = entryFile;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
})
)
})
return {
entry,
htmlWebpackPlugins
}
}
const { entry, htmlWebpackPlugins } = setMPA()
// webpack.config.js
module.exports = {
entry: entry,
plugins: [
// 其余 plugins
].concat(htmlWebpackPlugins)
}
6. source map
参考资料
作用: 将压缩后的代码,通过 source map 定位到源代码。
开发环境开启,生产环境关闭,线上排查问题的时候可以将 source map 上传到错误监控系统。
webpack 中的 source map 类型:
将 .map 作为 DataUrl 嵌入,不单独生成 .map 文件
设置 webpack.config.js:
Copy module.exports = {
devtool: 'source-map'
}
Copy // webpack.config.js
module.exports = {
mode: 'development'
}
// app.js
function App() {
debugger;
return <div>app</div>
}
此时,development 模式下,不开启 sourcemap,那么在 chrome 调试是以下的代码:
function App() {
debugger;
return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: "red"
}, "hello world 3", react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("img", {
src: _logo_png__WEBPACK_IMPORTED_MODULE_2__["default"],
alt: ""
}));
}
而开启 source map 之后,会显示以下的代码:
function App() {
debugger;
return <div className='red'>
hello world 3
<img src={logo} alt=""/>
</div>
}
以上的测试在 mode 为 production
的时候不起效果,应该是 debugger 被移除了吧。
经过测试发现:
Copy import React from 'react'
import ReactDOM from 'react-dom';
import logo from './logo.png'
import './index.less'
function App() {
debugger;
console.log(1)
return <div className='red'>
hello world 3
<img src={logo} alt=""/>
</div>
}
ReactDOM.render(
<App />,
document.querySelector('#root')
)
以上代码,在 development 情况下,打包出来的 index.js 为 900多k,在 production 的情况下,打包出来为 128k.
7. 提取页面公共资源
思路: 将 react 和 react-dom 等基础包通过 cdn 引入,而不打入 bundle 中。
7.1 使用 html-webpack-extrnals-plugin
安装依赖: npm - html-webpack-extrnals-plugin
设置 webpack.config.js
Copy plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
// 这样当我们在代码中引入 react 的时候,就不会将 react 打包进 bundle
module: 'react',
// 该 entry 看着没啥用,因为最终还是要在 html 模板中添加 script 标签,但是这一项还是不能删除
entry: 'https://cdn.bootcss.com/react/16.10.2/umd/react.development.js',
// 因为 React 会在全局暴露 React 这个变量,所以使用 React
global: 'React'
},
{
module: 'react-dom',
entry: 'https://cdn.bootcss.com/react-dom/16.10.2/umd/react-dom.development.js',
global: 'ReactDOM'
}
]
}),
]
配置 html( 必须添加 ):
<script src="https://cdn.bootcss.com/react/16.10.2/umd/react.development.js"></script>
<script src="https://cdn.bootcss.com/react-dom/16.10.2/umd/react-dom.development.js"></script>
通过设置以上代码,打包出来的结果就不会包含 react, react-dom,体积大大减小。
7.2 使用 SplitChunksPlugin
通过该插件进行公共脚本分离,该插件是 webpack4 内置的,用于替代原来的 CommonChunksPlugin 插件。
chunks 参数说明:
async 异步引入的库进行分离(例如 动态 import,引入 react)
minChunks: 某个公共模块被引用的次数,如为 2 ,则如果该模块被引用 >= 2, 则会被认为 minChunks.
设置 webpack.config.js
Copy module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
// 将 vendor 引入到 html 中
chunks: [pageName,'vendors'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: true
}
})
],
// 分离基础包,如 react react-dom 外部代码
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(react|react-dom)/, // 将代码中引用的 react 和 react-dom, 抽取为一个公共的包,包名叫 vendors
name: 'vendors',
chunks: 'all'
}
}
}
}
// SplitChunksPlugin 还可以用于公共代码抽取(多个 entry 使用到了该文件,例如多页面的场景)的提取,例如的 helper, utils 等 :
// 例如多页面应用中有两个 entry,a.js 用于 a.html, b.js 用于 b.html 页面。
// a.js 和 b.js 最终都引入了 say.js,那么如果不抽取公共文件,a.bundle.js 和 b.bundle.js 最终都会有 say.js 的代码
// 同样适用于 react, react-dom 等情况,打出 common 包之后
optimization: {
splitChunks: {
// 不管包的大小,只要某个模块被引入,就会被提取。例如本例中的 say.js。
// 经过测试 say.js 打包出来假设为 1k,假设设置 minSize 为 2k,那么就不会生成 common.bundle.js
minSize: 0,
cacheGroups: {
commons: {
name: 'common', // 公共代码,被打包为 common.js
chunks: 'all',
minChunks: 2 // 该模块至少要被引入 2 次,才会被提取,在本例中, a.js 和 b.js 引入共两次. 假设只有 a.js 引入,或者设置 minChunks 为 3 ( 3 > 2) ,那么不会生成 common.bundle.js
}
}
}
}
}
以上公共代码,要在页面中看到效果,也要添到 HtmlWebpackPlugin 中的 chunks 数组中。
8. Tree Sharking
概念:一个模块可能有很多的方法,只要其中某个方法被使用到了(甚至只是引入某个函数,而函数没有调用 ),则整个文件都会被打到 bundle 中,tree sharking 就是只把用到的方法打入 bundle, 没用到的方法会在 uglify 阶段被擦除掉。
Copy import a from './a.js'
if(false) {
a(); // 也会被 tree sharking 掉
}
要求:
必须使用 es6 的 import / export 语法,不能使用 cjs 中的 module.exports / require 语法 。
因为 import 只能在代码顶层出现(不能在 if 中),因此在静态分析阶段,就能够知道那些方法被用到了 ,而不需要在执行阶段才能判定。
在 webpack 生产环境的情况下,(mode='production') 的情况下,会默认开启 tree sharking.
9. Scope Hoisting
Copy // hello.js
import {a} from './tree-sharking'
console.log('this is hello');
// tree-sharking.js
export function a() {
console.log('this is a')
}
export function b() {
console.log("this is b")
}
编译之后的代码:
Copy /******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
// 模块一: hello.js
console.log('this is hello');
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 模块二: tree-sharking
function a() {
console.log('this is a');
}
function b() {
console.log("this is b");
}
/***/ })
/******/ ]);
可以看到以上两个模块,都被函数包裹着:
Copy (function(module, __webpack_exports__, __webpack_require__) {
// 模块代码
/***/ })
会导致: 1. 大量闭包函数包裹着代码,导致代码体积增大(模块越多越明显) 2. 运行代码时创建的函数作用域变多,内存消耗变大
scope hoisting 原理: 将所有模块的代码按照 引用顺序 放在一个函数作用域里面,然后适当的重命名一些变量以防止变量名冲突。
要求: 必须是 es6 语法, cjs 语法不支持。
设置 webpack.config.js
Copy // 默认 mode 为 production 是已经开启了 scope hoisting
// 但是 production 情况下,代码会被压缩,因此还是手动引入该插件
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
编译结果:
Copy /***/ (function(module, __webpack_exports__, __webpack_require__) {
// 模块二: tree-sharking
function a() {
console.log('this is a');
}
function b() {
console.log("this is b");
}
// 模块一: hello.js
console.log('this is hello');
/***/ })
结果同上,最终两个模块会在一个函数包裹里面,这样就减少了闭包数量。
注意: 有些模块是经过 splitChunksPlugin 之后,可能依然会被函数包裹
10. 代码分割和动态 import
对于大的 WEB 应用来说,将所有的代码都放在一个文件中,显然是不够有效的。特别是你的某些代码块是在某些特殊的时候才会被使用到。
webpack 有一个功能就是将你的代码库分割成 chunks, 当代码运行到需要他们的时候再进行加载。
适用场景:
1. 抽离相同的代码到一个共享块 2. 脚本懒加载,使得初始化下载的代码更小
懒加载 JS 的两种方式:
CommonJS: require.ensure
ES6: 动态 import (没有原生支持,需要 babel 转换)
安装依赖: npm i @babel/plugin-syntax-dynamic-import -D
设置 .babelrc:
Copy {
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
}
Copy // say.js
import React from 'react';
function Say () {
return <div>动态 imoprt</div>
}
export default Say;
function App() {
const [Text,setText] = useState(null)
return <div className='red'>
hello world 3 {Text}
// 点击时,动态加载组件
<img src={logo} alt="" onClick={() => {
import('../../common/say').then(Text => {
setText(Text.default)
})
}}/>
</div>
}
当 webpack 打包发现代码里有动态 import() 的语法,就会 将被引入的组件单独打包出js ,当点击时,才会加载该组件的js。
11. Webpack 集成 Eslint
安装依赖:npm i eslint eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y -D
npm i eslint-loader -D
: 用于配合 babel-loader, 解析 js 文件
npm i babel-eslint -D
: eslint 的解析器
npm install eslint-config-airbnb -D
: eslint 的基础规则
设置 webpack.config.js
Copy module.exports = {
module: {
{
test: /\.js$/,
use: [
'babel-loader',
'eslint-loader', // 新增该项
],
},
}
}
设置 .eslintrc.js
12. webpack 打包组件和库
要求:
1. 需要打包压缩版和非压缩版本 2. 支持 AMD/CMD/ESM 模块引入,同时支持 script 的方式引入
如何将库暴露出去:
library: 指定库的全局变量,如 react 库在全局的变量是 React
libraryTarget: 支持库的引入方式
libraryExport: "default" // 设置为 default
Copy // 如果不设置 libraryExport,那么
import a from './a.js'
a.default.add();
// 设置为 default 之后
import a from './a.js'
a.add();
webpack.config.js
Copy module.exports = {
entry: {
'large-number': './src/index.js',
'large-number.min': './src/index.js',
},
output: {
filename: '[name].js',
library: 'largeNumber',
libraryTarget: 'umd', // 支持 AMD/CMD/ESM 和 script 的方式引入
libraryExport: 'default'
}
}
如何针对 .min 进行压缩:
设置 webpack.config.js
Copy module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({ // 使用该插件
include: '/\.min\.js/' // 针对该打包出来的文件
})
]
}
}
如何针对不同环境引入不同的包?
设置 package.json 中 main 字段为 index.js, 同时该 index.js 的内容为:
Copy if(process.env.NODE_ENV === 'production') {
module.exports = require('./dist/xxx.min.js')
} else {
module.exports = require('./dist/xxx.js')
}
13. webpack 实现 SSR 打包 (上)
SSR 优势:
1. SEO 友好 2. 减少白屏时间
实现思路:
服务端:
使用 react-dom/server 的 renderToString 方法将 React 组件渲染成字符串
客户端:
服务端的代码:
Copy // app.js
const express = require('express');
const { renderToString } = require('react-dom/server');
const SSR = require('../dist/search-server') // 打包出来的search页面组件
const renderMarkup = (str) => {
return `<!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>Document</title>
</head>
<body>
<div id="root">${str}</div>
</body>
</html>
`
}
const server = (port) => {
const app = express();
arr.get('/search',() => {
res.status(200).send( renderMarkup(renderToString(SSR)) )
})
app.listen(port, () => {
console.log('服务器启动成功')
})
}
客户端打包入口文件的代码:
Copy // index-server.js // 该文件打包出来给服务端使用
//
注意, node 端 app.js 中使用的是 module.exports 和 require 的语法
因此以下代码的 import 都要改为 require 语法
//
import React, { useState } from 'react'
import ReactDOM from 'react-dom'; // 移除该行
import logo from './logo.png'
import './index.less'
function App() {
const [Text, setText] = useState(null)
return (
<div className="red">
hello world 3
{Text}
<img
src={logo}
alt=""
onClick={() => {
import('../../common/say').then((Text) => {
setText(Text.default)
})
}}
/>
</div>
)
}
// 移除以下代码
ReactDOM.render(
<App />,
document.querySelector('#root'),
)
// 改为直接暴露
module.exports = <App />;
修改 webpack.ssr.js
Copy entry: {
引入 index-server.js,服务端入口文件
}
output: {
fileName: '[name]-sever.js' // 打包输出 -server.js 文件
libraryTarget: 'umd' // 输出 umd 包
}
打包命令: webpack --config webpack.ssr.js
webpack ssr 打包存在的一些问题:
1. 浏览器中的全局变量(nodejs 没有 window 和 document) 1. 组件适配,将不兼容的组件根据打包环境进行适配 2. 请求适配,将 fetch, ajax 请求改为 axios 或者 isomorphic -fetch
2. 样式问题(nodejs 无法解析 css) 1. 方案一: 服务端打包通过 ignore-loader 忽略 css 的解析 2. 方案二:将 style-loader 替换为 isomorphic-style-loader(但是必须采用 css in js 的语法)
报错:
1. window is undefined
解决办法:
在 app.js 添加以下代码:
Copy const express = require('express');
// 添加
if(typeof window === 'undefined') {
global.window = {}
}
const server = () => {
}
14. webpack 实现 SSR 打包 (下)
13 节中,我们针对 css 可以采用忽略 css 的做法,但是应该怎么引入 css 呢?
因为在打包出客户端模板时,css 被打包且内联进模板中了 ,因此我们需要的是将打包后的客户端模板作为基础的壳子,将服务端渲染的结果放进壳子中。
Copy 最初的模板
<html>
<!-- HTML_PLACEHOLDER -->
</html>
// 客户端打包之后的产物, 会将 css 引入到模板中
<html>
<link href='./xxxx.css' >
<!-- HTML_PLACEHOLDER -->
</html>
// 服务端读取客户端打包的产物,将 HTML_PLACEHOLDER 替换为 renderToString(SSR))
<html>
<link href='./xxxx.css' >
renderToString(SSR)) // 用 ssr 的结果替换原来的 HTML_PLACEHOLDER
</html>
同理,我们可以在初始的模板中,添加数据的 PLACEHOLDER:
Copy <html>
<!-- HTML_PLACEHOLDER -->
<!-- HTML_DATA_PLACEHOLDER --> // 添加数据入口,服务端渲染的时候用数据替换该模块
</html>
15. 优化构建时的命令行显示日志
统计信息 stats
设置 webpack.config.js
Copy module.exports = {
devServer: {
contentBase: './dist',
hot: true,
stats: 'errors-only', // dev-server 的 stats
},
stats: 'errors-only'
}
以上设置,只会在 webpack 编译出错的时候才会打印错误信息,对于成功的情况,不会打印任何信息,因此也是不太友好。
所以,可以使用 friendly-errors-webpack-plugin
插件,对于成功,警告,错误都有对应的错误提醒。
安装依赖: npm i friendly-errors-webpack-plugin -D
设置 webpack.config.js
Copy const FriendlyErrorsWebpacjPlugin = require('friendly-errors-webpack-plugin')
module.exports = {
plugins: [
new FriendlyErrorsWebpacjPlugin()
],
stats: 'errors-only' // 可以配合该参数一块使用
}
16. 构建异常和中断处理
在每次构建完成之后,输入 echo $?
可以获取错误码的信息。如果错误码不为 0 , 则代表当前构建是失败的。
Node.js 中的 process.exit 规范
1. 0 表示成功完成,回调函数中,err 为 null 2. 非0 表示执行失败,回调函数中,err 不为 null,err.code 为传给 exit的数字
compiler 在每次构建结束后会触发 done 这个 hook:
Copy module.exports = {
plugins: [
function () {
this.hooks.done.tap('done', (stats) => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') === -1) {
console.log('build error');
process.exit(1)
}
})
},
]
}
此时,如果你构建发生错误(例如引用了一个不存在的模块),就会打印 build error
这个错误信息。