自动清理构建目录产物
每次构建的时候不会清理目录,造成构建的输出目录 output 文件越来越多。
通过 npm scripts 清理构建目录
rm -rf ./dist && webpackrimraf ./dist && webpack
使用 clean-webpack-plugin
npm i clean-webpack-plugin -D
避免构建前每次都需要手动删除 dist。
默认会删除 output 指定的输出目录
webpack.prod.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// ...
plugins: [
// ...
new CleanWebpackPlugin()
]
}
测试环境也可以使用此插件。
自定补齐 CSS3 前缀
需求原因:各大浏览器写法并没有统一。

举个例子。
.box {
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
-o-border-radius: 10px;
border-radius: 10px;
}
使用 PostCSS 插件 autoprefixer 插件。根据 Can I Use 规则(https://caniuse.com/)。
安装
npm i postcss-loader autoprefixer -D
配置
index.less
.text {
font-size: 20px;
color: red;
display: flex;
}
postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')({
"overrideBrowserslist": [
"defaults",
"not ie < 11",
"last 2 versions",
"> 1%",
"iOS 7",
"last 3 iOS versions"
]
})
]
}
webpack.prod.js
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
'postcss-loader'
]
}
},
// ...
}
移动端 CSS px 自动转换成 rem
针对浏览器的分辨率。需要进行页面适配。
主要针对 ios 出很多新的设备,不同的设备的分辨率都不同。
设备适配常用的方法。
CSS 媒体查询实现响应式布局
缺陷:需要写多套适配样式代码。
@media screen and (max-width: 980px) {
.header {
width: 900px;
}
}
@media screen and (max-width: 480px) {
.header {
width: 400px;
}
}
@media screen and (max-width: 350px) {
.header {
width: 300px;
}
}
rem 是什么
W3C 对 rem 的定义:font-size of the root element。
rem 与 px 对比:
- rem 是相对单位
- px 是绝对单位
使用 px2rem-loader
px2rem-loader 将 px 转换成 rem。
npm i px2rem-loader -D
页面渲染时计算根元素的 font-size 值。
- 可以使用手淘的 lib-flexible 库
自动根据当前设备宽高计算根元素实际 font-size 的值。 - https://github.com/amfe/lib-flexible
npm i lib-flexible -S
px2rem-loader 只是以构建的手段将 px 单位转换成了 rem。但是 rem 和 px 的单位计算并不清楚,flexible.js 的作用就是动态的去计算不同设备下的 rem 相对 px 的单位,也就是计算跟元素 html 节点的 font-size 大小。 这个比较适合H5和Pad端的应用,PC端还是使用px比较适合。
配置
webpack.prod.js
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
'postcss-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75, // 1rem = 75px、适合 750 设计稿
remPrecesion: 8 // px => rem 小数点的位数
}
}
]
},
// ...
}
// ...
}
index.html
先手动引入 lib-flexible 库,后面再学习如何内联库到页面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script type="text/javascript">
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}
refreshRem();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}
})(window, window['lib'] || (window['lib'] = {}));
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>
其他问题
1. 统一转化 rem 时,有部分样式不想转化。
后面有 /*no*/ 这种注释语法会不进行 rem 的转换。
.page {
font-size: 12px; /*no*/
width: 375px; /*no*/
height: 40px;
}
2. 关于 手淘的lib-flexible库 与 vw 方法。
html: {
font-size: calc(100vw / 固定分辨率);
}
手淘的这个库有个好处,它会比较方便的解决手机端的1px问题。
3. 不推荐使用rem了么,现在项目里面用vw用的更多一些
针对兼容性来说,rem 兼容性更好。
4. px2rem-loader 会把第三方 ui 库的 px 也给转换
px2rem-loader 也是可以设置 exclude 的,可以把 node_modules 里面的模块 exclude 掉。
如果不设置 exclude,那么也可以使用 /no/的语法去设置某一行样式不进行 px2rem 的转换操作。
5. lib-flex一定要内里联进来吗,可以引入文件吗?
这个是必须要内联进来的,因为页面打开的时候就需要马上计算页面的根节点的 font-size 值。
如果不内联进来而是打包到了 js 里面去,那么样式解析的时候会有问题,可能存在样式闪动的情况。
6. vm布局和rem哪个适配好,都有什么不同
主要是兼容性上。rem兼容性更好,支持 android2.2 以上的机型。但是vm只支持 android4.4 和 ios8 以上的。
另外rem需要的计算需要在头部内联一个脚本,vm是纯css去实现的。如果不考虑兼容性,vm完全没问题。
7. 内联样式该如何转为rem?
内联的样式如果想转换成rem需要自己实现 loader 去解析 html 文件。
可以实现一个 loader,然后这个 loader 去匹配 html,然后将 html 里面的 px 都转换成 rem就可以。
8. 按照 750 的设计稿,直接就是10px写10px吗? 为什么看着比设计稿的要大?
如果设置的 remUnit 是 75,那么对于 750 的设计稿如果字体是 24px,就写 24px(实际上在 iphone 6是12px的大小)。
如果设置的 remUnit 是 37.5,那么对于 375 的设计稿如果字体是 12px,就写 12px(实际上在 iphone 6是12px的大小)。
看着比设计稿的要大这个需要以 iphone 6 为参照。
资源内联
资源内联的意义
代码层面
- 页面框架的初始化脚本
- 上报相关打点
- css 内联避免页面闪动
请求层面
- 小图片或者字体内联(url-loader)
HTML 内联
<%= require('raw-loader!./meta.html') %>
JS 内联
<script><%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %></script>
CSS 内联
方案一:借助 style-loader
{
loader: 'style-loader',
options: {
insertAt: 'top', // 样式插入到 <head>
singleton: true, // 将所有的 style 标签合并成一个
}
}
css-loader 将css转换成commonjs对象,然后css代码就在js里面了。 style-loader的作用是在js执行时,动态的创建style标签,然后将 css-loader 转换的样式插入到这个style标签里去的。 所以在打包之后的 html 文件中找不到内联的 css 文件,只能运行时看到,也就是在浏览器可以看到。
方案二:html-inline-css-webpack-plugin
将页面打包过程的产生的所有 CSS 提取成一个独立的文件,然后将这个 CSS 文件内联进 HTML head 里面。这里需要借助 mini-css-extract-plugin 和 html-inline-css-webpack-plugin 来实现 CSS 的内联功能。
module.exports = {
// ...
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new HtmlWebpackPlugin(),
new HTMLInlineCSSWebpackPlugin()
]
};
注:html-inline-css-webpack-plugin 需要放在 html-webpack-plugin 后面。
配置
npm i raw-loader@0.5.1 -D
安装的版本是 0.5.1 的版本,最新的版本导出模块的时候使用了 export default 语法, html 里面用的话有问题。 raw-loader 内联进去的样式或者脚本的压缩都是基于 html-webpack-plugin 进行处理的。 如果想压缩内联的文件,可以配置 html-webpack-plugin 的 minify参数,用于压缩 html 里面内联的内容。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<%= require('raw-loader!./meta.html') %>
<title>Document</title>
<script><%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
<%= %> 语法是 html-webpack-plugin 插件支持的 ejs 写法。
关于图片、字体内联等,可以参考 这篇文章。
多页面应用打包通用方案
多页面应用(MPA)概念
每一次页面跳转的时候,后台服务器都会返回一个新的 html 文档,这种类型的网站也就是多页网站,也叫做多页应用。
基本思路
每个页面对应一个 entry,一个 html-webpack-plugin。
缺点:每次新增或删除页面需要改 webpack 配置。
通用方案
动态获取 entry 和设置 html-webpack-plugin 数量。
利用 glob.sync。
entry: glob.sync(path.join(__dirname, './src/*/index.js'))
比如上述配置需要所有的文件都放在 src 目录下,每个目录的文件名称都是 index.js 文件。
配置
npm i glob -D
页面结构整理

webpack.prod.js
const path = require('path');
const glob = require('glob');
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/pages/*/index.js'))
entryFiles.map((entryFile) => {
const match = entryFile.match(/src\/pages\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.resolve(__dirname, `src/pages/${pageName}/index.html`),
filename: `${ pageName }.html`,
chunks: [ pageName ],
excludeChunks: ['node_modules'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
}),
)
});
return {
entry,
htmlWebpackPlugins
}
}
const { entry, htmlWebpackPlugins } = setMPA();
module.exports = {
entry,
output: {
filename: '[name][chunkhash:8].js',
path: path.resolve(__dirname, 'dist')
},
mode: "production",
// ...
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new CleanWebpackPlugin()
].concat(htmlWebpackPlugins)
}
使用 sourceMap
可以通过 source map 定位到源代码。
开发环境开启,线上环境关闭。线上排查问题的时候可以将 sourcemap 上传到错误监控系统。
source map 关键字
- eval:使用 eval 包裹模块代码
- source map:产生 .map 文件
- cheap:不包含列信息
- inline:将 .map 作为 DataURL 嵌入,不单独生成 .map 文件
- module:包含 loader 的 sourcemap
source map 类型
| devtool | 首次构建 | 二次构建 | 是否适合生产环境 | 可以定位的代码 |
|---|---|---|---|---|
| none | +++ | +++ | yes | 最终输出的代码 |
| eval | +++ | +++ | no | webpack 生成的代码(一个个的模块) |
| cheap-eval-source-map | * | ++ | no | 经过 loader 转换后的代码(只能看到行) |
| cheap-module-eval-source-map | o | ++ | no | 源代码(只能看到行) |
| eval-source-map | — | + | no | 源代码 |
| cheap-source-map | + | o | yes | 经过 loader 转换后的代码(只能看到行) |
| cheap-module-source-map | o | - | yes | 源代码(只能看到行) |
| inline-cheap-source-map | + | o | no | 经过 loader 转换后的代码(只能看到行) |
| inline-cheap-module-source-map | o | - | no | 源代码(只能看到行) |
| source-map | — | — | yes | 源代码 |
| inline-source-map | — | — | no | 源代码 |
| hidden-source-map | — | — | yes | 源代码 |
配置
webpack.dev.js
module.exports = {
mode: "development",
// ...
devServer: {
contentBase: './dist',
hot: true
},
devtool: 'source-map'
}
提取页面公共资源
基础库分离
1. 利用 html-webpack-externals-plugin 。
将 react、react-dom 基础包通过 cdn 引入,不打入 bundle 中。
npm i html-webpack-externals-plugin -D
webpack.prod.js
const path = require('path');
const glob = require('glob');
// ...
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const htmlWebpackExternalsPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/pages/*/index.js'))
entryFiles.map((entryFile) => {
const match = entryFile.match(/src\/pages\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
// ...
htmlWebpackExternalsPlugins.push(
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://11.url.cn/now/lib/16.2.0/react.min.js',
global: 'React'
},
{
module: 'react-dom',
entry: 'https://11.url.cn/now/lib/16.2.0/react-dom.min.js',
global: 'ReactDOM'
},
],
files: [`${pageName}.html`]
})
)
});
return {
entry,
htmlWebpackPlugins,
htmlWebpackExternalsPlugins
}
}
const { entry, htmlWebpackPlugins, htmlWebpackExternalsPlugins } = setMPA();
module.exports = {
// ...
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new CleanWebpackPlugin(),
].concat(htmlWebpackExternalsPlugins, htmlWebpackPlugins)
}
2. 利用 SplitChunksPlugin。
webpack4 内置的,替代 CommonsChunksPlugin 插件。
chunks 参数说明:
- async 异步引入的库进行分离(默认)
- initial 同步引入的库进行分离
- all 所有引入的库进行分离(推荐)
详细参数可以去 webpack 官网查看。
test 匹配出需要分离的包。
module.exports = {
// ....
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new CleanWebpackPlugin(),
].concat(htmlWebpackPlugins),
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
不需要安装手动 SplitChunksPlugin,内置插件。
公共文件分离
利用 SplitChunksPlugin 分离页面公共文件。
minChunks:设置最小引用次数为 2 次
minSize:分离包体积的大小
module.exports = {
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2
}
}
}
}
}
基础库和公共文件分离
搭配使用,使用 priority 权重属性。值越大,优先级越高.模块先打包到优先级高的组里。
module.exports = {
// ....
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new CleanWebpackPlugin(),
].concat(htmlWebpackPlugins),
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
vendors: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all',
priority: -10
},
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2,
priority: -20
}
}
}
}
}
其他相关配置可以参考下面。
module.exports = {
//...
optimization: {
splitChunks: {
// async:异步引入的库进行分离(默认), initial: 同步引入的库进行分离, all:所有引入的库进行分离(推荐)
chunks: 'async',
minSize: 30000, // 抽离的公共包最小的大小,单位字节
maxSize: 0, // 最大的大小
minChunks: 1, // 资源使用的次数(在多个页面使用到), 大于1, 最小使用次数
maxAsyncRequests: 5, // 并发请求的数量
maxInitialRequests: 3, // 入口文件做代码分割最多能分成3个js文件
automaticNameDelimiter: '~', // 文件生成时的连接符
automaticNameMaxLength: 30, // 自动自动命名最大长度
name: true, //让cacheGroups里设置的名字有效
cacheGroups: { //当打包同步代码时,上面的参数生效
vendors: {
test: /[\\/]node_modules[\\/]/, // 检测引入的库是否在node_modlues目录下的
priority: -10, // 值越大,优先级越高.模块先打包到优先级高的组里
filename: 'vendors.js'// 把所有的库都打包到一个叫vendors.js的文件里
},
default: {
minChunks: 2, // 上面有
priority: -20, // 上面有
reuseExistingChunk: true //如果一个模块已经被打包过了,那么再打包时就忽略这个上模块
}
}
}
}
}
Tree Shaking 使用和原理分析
tree shaking(摇树优化)。
1 个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件就会被打到 bundle 里面去。
tree shaking 就是只把用到的方法打入到 bundle,没用到的方法会在 unlify 阶段被删除掉。
使用
- webpack 默认支持,在 .babelrc 中设置 modules: false 即可。
- production mode 的情况下默认开启。
必须是 ES6 Module 的形式,CommonJS 的方式不支持
DCE
DCE(Dead code elimination),即死码消除。
它是一种编译最优化技术,它的用途是移除对程序运行结果没有任何影响的代码。
- 代码不会被执行,不可到达。
- 代码执行的结果不会被用到。
- 代码只会影响死变量(只写不读)。
if (false) {
console.log('这段代码永远不会执行');
}
原理
Tree-shaking 原理。
利用 ES6 模块特点。
- 只能作为模块顶层的语句出现;
- import 的模块名只能是字符串常量;
- import binding 是 immutable 的。
代码擦除:unglify 阶段删除无用代码。
Tree-shaking 生效还有一个要求,就是编写的代码不能有副作用。
副作用这个概念来源于函数式编程(FP),纯函数是没有副作用的,也不依赖外界环境或者改变外界环境。 纯函数的概念是:接受相同的输入,任何情况下输出都是一样的。 非纯函数存在副作用,副作用就是:相同的输入,输出不一定相同。或者这个函数会影响到外部变量、外部环境。 函数如果调用了全局对象或者改变函数外部变量,则说明这个函数有副作用。
配置
tree-shaking.js
export function a () {
return 'This is a func a';
}
export function b () {
return 'This is a func b';
}
index.js
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
// ...
import { a } from './tree-shaking';
class Search extends React.Component {
render () {
const funcA = a;
return (
<div className="text">
Hello React!!! { funcA } <img width={ 200 } height={ 200 } src={ avator } />
</div>
);
}
}
ReactDOM.render(
<Search />,
document.getElementById('app')
);
webpack.prod.js
// ...
module.exports = {
entry,
output: {
filename: '[name][chunkhash:8].js',
path: path.resolve(__dirname, 'dist')
},
mode: "production"
// ...
}
Scope Hoisting 使用和原理分析
现象:构建后的代码存在大量闭包代码。
// a.js
export default 'xxxx';
// b.js
import index from './a';
console.log(index);
大量函数闭包包裹代码,导致体积增大(模块越多越明显)。
运行代码时创建的函数作用域变多,内存开销变大。
被 webpack 转换后的模块会带上一层包裹。import 会被转换成 __webpack_require。
webpack_require
打包出来的是一个 IIFE(匿名闭包)。
modules 是一个数组,每一项是一个模块初始化函数。
__webpack_require 用来加载模块,返回 module.exports。
通过 WEBPACK_REQUEST_METHOD(0) 启动程序。
Scope hoisting 原理
原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突。
通过 scope hoisting 可以减少函数声明代码和内存开销。
使用
webpack mode 为 prodution 默认开启,必须是 ES6 语法,CJS 不支持。
实际上默认开启 new webpack.optimize.ModuleConcatenationPlugin()。
Scope hoisting 对模块的引用次数大于1次是不产生效果的,如果一个模块引用次数大于1次,那么这个模块的代码会被内联多次,从而增加了打包出来的 js bundle 的体积。
代码分割与动态 import
代码分割的意义
对于大的的 Web 应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被使用到。webpack 有一个功能就是将你的代码分割成 chunks(语块),当代码运行到需要它们的时候在进行加载。
适用场景:
- 抽离相同代码到一个共享块。
- 脚本懒加载,使得初始下载的代码更小。
懒加载 JS 脚本的方式
CommonJS:require.ensure
ES6:动态 import(目前还没有原生支持,需要 babel 转换)。
如何使用动态 import
安装 babel 插件
npm i @babel/plugin-syntax-dynamic-import -D
{
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
}
配置
babelrc.js
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
}
dynamic.js
import React from 'react';
export default () => <div>动态 import</div>;
index.js
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
// ...
class Search extends React.Component {
constructor () {
super(...arguments);
this.state = {
Text: null
};
}
// 动态加载组件
loadComponent () {
import('./dynamic').then((Text) => {
this.setState({
Text: Text.default
});
});
}
render () {
const { Text } = this.state;
return (
<div className="text">
<div>
Hello React!!!
</div>
{ Text ? <Text /> : null }
<dvi>
<img
width={ 200 }
height={ 200 }
src={ avator }
onClick={ this.loadComponent.bind(this) }
/>
</dvi>
</div>
);
}
}
ReactDOM.render(
<Search />,
document.getElementById('app')
);
使用 ESLint
行业中优秀的 ESLint 规范实践
Airbnb: eslint-config-airbnb、 eslint-config-airbnb-base
腾讯:
- alloyteam团队 eslint-config-alloy(https://github.com/AlloyTeam/eslint-config-alloy)
- ivweb 团队:eslint-config-ivweb(https://github.com/feflow/eslint-config-ivweb)
制定团队的 ESLint 规范
不重复造轮⼦,基于 eslint:recommend 配置并改进。
能够帮助发现代码错误的规则,全部开启。
帮助保持团队的代码⻛格统⼀,⽽不是限制开发体验。

ESLint 如何执行落地
和 CI/CD 系统集成,和 webpack 集成。
webpack 与 CI/CD 集成

本地开发阶段增加 precommit 钩子
安装 husky
npm i husky -D
增加 npm script,通过 lint-staged 增量检查修改的文件
"scripts": {
"precommit": "lint-staged"
},
"lint-staged": {
"linters": {
"*.{js,scss}": ["eslint --fix", "git add"]
}
}
webpack 与 ESLint 集成
使用 eslint-loader,构建时检查 JS 规范。推荐新项目,老项目不适合。
react 项目推荐使用 eslint-config-airbnb,非 react 项目推荐使用 eslint-config-airbnb-base。
npm i eslint eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y -D
npm i eslint-loader babel-eslint eslint-config-airbnb -D
.eslintrc.js
module.exports = {
"parser": "babel-eslint",
"extends": "airbnb",
"env": {
"browser": true,
"node": true
}
}
webpack.prod.js
module.exports = {
entry,
output: {
filename: '[name][chunkhash:8].js',
path: path.resolve(__dirname, 'dist')
},
mode: "production",
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader',
'eslint-loader'
]
},
// ...
]
}
}
webpack 打包组件和基础库 ★ ★ ★
webpack 除了可以用来打包应用,也可以用来打包 JS 库。
实现一个大整数加法库的打包。
- 需要打包压缩版和非压缩版本。
- 支持 AMD/CJS/ESM 模块引入。
Rollup 更适合打包组件和库。
库的目录结构和打包要求
打包输出的库名称:
- 未压缩版 large-number.js
- 压缩版 large-number.min.js
支持的使用方式
- 支持 ES Module
- 支持 CJS
- 支持 AMD
- 可以直接通过 script 引入
如何将库暴露出去
library:指定库的全局变量
libraryTarget:支持库引入的方式
配置
新建立一个项目。
npm i webpack webpack-cli -D
需要只对 .min 压缩。通过 include 设置只压缩 min.js 结尾的文件。
npm i terser-webpack-plugin -D
webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'none',
entry: {
'large-number': './src/index.js',
'largeg-number.min': './src/index.js'
},
output: {
filename: '[name].js',
library: 'largeNumber',
libraryTarget: 'umd',
libraryExport: 'default',
globalObject: 'this',
umdNamedDefine: true
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
include: /\.min\.js$/
})
]
}
}
src/index.js
export default function add (a, b) {
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
let ret = '';
while (i >= 0 || j>= 0) {
let x = 0,
y = 0,
sum = 0;
if (i >= 0) {
x = a[i] - '0';
i--;
}
if (j >= 0) {
y = b[j] - '0';
j--;
}
sum = x + y + carry;
if (sum >= 10) {
carry = 1;
sum -= 10;
} else {
carry = 0;
}
ret = sum + ret;
}
if (carry) {
ret = carry + ret;
}
return ret;
}
// add('999', '1');
// add('1', '999');
// add('123', '321');
// add('9999999999999999999999999999999999999999999999999999', '1');
还需要设置入口文件,主要是 package.json main 字段。
package.json
{
"name": "yueluo-large-number",
"version": "1.0.0",
"description": "大整数加法打包",
"main": "index.js",
"scripts": {
"build": "webpack",
"prepublish": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"terser-webpack-plugin": "^5.1.1",
"webpack": "^5.24.2",
"webpack-cli": "^4.5.0"
}
}
index.js
if (process.env.NODE_ENV === 'production') {
modules.exports = require('./dist/large-number.min.js');
} else {
modules.exports = require('./dist/large-number.js');
}
最后可以使用 npm 命令推到 npm 仓库中,然后在项目中安装测试。
webpack 实现 SSR 打包
SSR 是什么
渲染:HTML + CSS + JS + Data => 渲染后的 HTML。
服务端:
- 所有模板等资源都存储在服务端。
- 内网机器拉取数据更快。
- 一个 HTML 返回所有数据。
浏览器和服务器交互流程

客户端渲染和服务端渲染

总结:服务端渲染(SSR)的核心是减少请求。
SSR 优势
减少白屏时间。
对 SEO 优化好。
实现思路
服务端
- 使用 react-dom/server 的 renderToString 方法将 React 组件渲染成字符串。
- 服务端返回对应的模板。
客户端
- 打包出针对服务端的组件。
配置
npm i express -D
server/index.js
const express = require('express');
const { renderToString } = require('react-dom/server');
const SEARCH_SSR = require('../dist/search-server');
const server = (port) => {
const app = express();
app.use(express.static('dist'));
app.get('/search', (req, res) => {
const htmlStr = renderMarkUp(renderToString(SEARCH_SSR));
res.status(200).send(htmlStr);
});
app.listen(port, () => {
console.log('Server is running on port:', port);
});
}
server(process.env.PORT || 3000);
const renderMarkUp = (str) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app>${str}</div>
</body>
</html>
`;
}
src/search/index-index.js
'use strict';
const React = require('react');
require('../../common/index');
require('./index.less');
class Search extends React.Component {
render () {
return (
<div className="text">
<div>
Hello React Servcer Render!!!
</div>
</div>
);
}
}
module.exports = <Search />;
webpack.ssr.js
const path = require('path');
const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/pages/*/index-server.js'))
entryFiles.map((entryFile) => {
const match = entryFile.match(/src\/pages\/(.*)\/index-server\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
if (pageName) {
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.resolve(__dirname, `src/pages/${pageName}/index.html`),
filename: `${ pageName }.html`,
chunks: [ pageName ],
excludeChunks: ['node_modules'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
}),
);
}
});
return {
entry,
htmlWebpackPlugins
}
}
const { entry, htmlWebpackPlugins } = setMPA();
module.exports = {
entry,
output: {
filename: '[name]-server.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'umd',
globalObject: 'this',
umdNamedDefine: true
},
mode: "production",
// ....
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new CleanWebpackPlugin(),
].concat(htmlWebpackPlugins)
}
package.json
"scripts": {
// ...
"build:ssr": "webpack --config webpack.ssr.js"
},
打包存在的问题
浏览器的全局变量 (Node.js 中没有 document, window)
- 组件适配:将不兼容的组件根据打包环境进⾏适配。
- 请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios。
样式问题 (Node.js ⽆法解析 css)
- ⽅案⼀:服务端打包通过 ignore-loader 忽略掉 CSS 的解析。
- ⽅案⼆:将 style-loader 替换成 isomorphic-style-loader 。
如何解决样式不显示的问题
使用打包后的浏览器端 html 为模板。设置占位符,动态插入组件。
服务端请求会的数据,也可以使用占位符,嵌入到页面中。
server/index.js
const fs = require('fs');
const path = require('path');
const express = require('express');
const { renderToString } = require('react-dom/server');
const SEARCH_SSR = require('../dist/search-server');
const SEARCH_TEMPLATE = fs.readFileSync(path.resolve(__dirname, '../dist/search.html'), 'utf-8');
const DATA = require('./data.json');
const server = (port) => {
const app = express();
app.use(express.static('../dist'));
app.get('/search', (req, res) => {
const htmlStr = renderMarkUp(renderToString(SEARCH_SSR));
res.status(200).send(htmlStr);
});
app.listen(port, () => {
console.log('Server is running on port:', port);
});
}
server(process.env.PORT || 3000);
const renderMarkUp = (str) => {
const dataStr = JSON.stringify(DATA);
return SEARCH_TEMPLATE.replace('<!--HTML_PLACEHOLDER-->', str)
.replace('<!--INITIAL_DATA_PLACEHOLDER-->', `<script>window._initial_data=${dataStr}</script>`);
}
data.json
{
"code": 200,
"msg": "查询文章列表成功",
"articles": [
{
"articleId": "60373cba61495b7b5aada0db",
"title": "消息队列和事件循环机制",
"category": "BOM",
"comments": 0,
"pageviews": 6,
"likes": 0,
"desc": "从浏览器底层分析消息队列和事件循环机制。",
"img": "https://data.yueluo.club/bom.png",
"time": "2021-02-25",
"author": "月落"
},
{
"articleId": "60271e4cac51f0058d2ec33e",
"title": "输入网址按下回车发生了什么?",
"category": "网络",
"comments": 0,
"pageviews": 16,
"likes": 0,
"desc": "输入网址按下回车到底发生了什么?接下来为你揭晓。",
"img": "https://data.yueluo.club/network.png",
"time": "2021-02-13",
"author": "月落"
},
{
"articleId": "5fcc633afee352374cbbceb1",
"title": "小程序双线程模型",
"category": "微信小程序",
"comments": 0,
"pageviews": 31,
"likes": 0,
"desc": "腾讯是为了技术垄断才单独开发小程序体系?为什么小程序不用浏览器的线程模型?双线程模型和 JavaScript 单线程模型有什么区别?",
"img": "https://data.yueluo.club/mini.png",
"time": "2020-12-06",
"author": "月落"
}
// ...
],
"total": 76
}
src/search/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"><!--HTML_PLACEHOLDER--></div>
<!--INITIAL_DATA_PLACEHOLDER-->
</body>
</html>
优化构建时命令行的显示日志
展示一大堆日志,很多并不需要开发者关注。
统计信息 stats

webpack.prod.js
module.exports = {
// ...
stats: 'errors-only'
}
webpack.dev.js
module.exports = {
// ...
devServer: {
contentBase: './dist',
hot: true,
stats: 'errors-only'
}
}
这样输出也不是很友好,如果没有错误,啥都不展示。
如何优化命令行的构建日志
使用 firendly-errors-webpack-plugin。
- success: 构建成功的⽇志提示
- warning: 构建警告的⽇志提示
- error: 构建报错的⽇志提示
stats 设置成 errors-only。
npm i friendly-errors-webpack-plugin -D
webpack.prod.js
module.exports = {
entry,
output: {
filename: '[name][chunkhash:8].js',
path: path.resolve(__dirname, 'dist')
},
mode: "production",
// ...
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new CleanWebpackPlugin(),
new FriendlyErrorsWebpaclPlugin(),
].concat(htmlWebpackPlugins),
// ...
stats: 'errors-only'
}
webpack.dev.js
module.exports = {
entry,
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
mode: "development",
// ...
plugins: [
new CleanWebpackPlugin(),
new FriendlyErrorsWebpaclPlugin(),
].concat(htmlWebpackPlugins),
devServer: {
contentBase: './dist',
hot: true,
stats: 'errors-only'
},
devtool: 'source-map'
}
构建异常和中断处理
如何判断构建是否成功?
在 CI/CD 的 pipline 或者发布系统需要知道当前构建状态。
每次构建完成后输入 echo $? 获取错误码。
如果错误码不为 0 ,说明这个构建是失败的。
webpack4 之前的版本构建失败不会抛出错误码 (error code)。
Node.js 中的 process.exit 规范
- 0 表示成功完成,回调函数中,err 为 null
- ⾮ 0 表示执⾏失败,回调函数中,err 不为 null,err.code 就是传给 exit 的数字
如何主动捕获并处理构建错误?
compiler 在每次构建结束后会触发 done 这个 hook。
process.exit 主动处理构建报错。
webpack.prod.js
const path = require('path');
const glob = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
const FriendlyErrorsWebpaclPlugin = require('friendly-errors-webpack-plugin');
// ...
module.exports = {
entry,
output: {
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, 'dist')
},
mode: "production",
// ...
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
new CleanWebpackPlugin(),
new FriendlyErrorsWebpaclPlugin(),
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);
}
})
}
].concat(htmlWebpackPlugins),
// ...
stats: 'errors-only'
}
