文章目录
-
- 自动清理构建目录产物
- PostCSS插件autoprefixer自动补齐CSS3前缀
- [移动端CSS px自动转换成rem](#移动端CSS px自动转换成rem)
- 静态资源内联
- 多页面应用打包通用方案
- 使用sourcemap
- 提取页面公共资源
-
- 基础库分离
- [利⽤ SplitChunksPlugin 进⾏公共脚本分离](#利⽤ SplitChunksPlugin 进⾏公共脚本分离)
- [利⽤ SplitChunksPlugin 分离基础包](#利⽤ SplitChunksPlugin 分离基础包)
- [利⽤ SplitChunksPlugin 分离⻚⾯公共⽂件](#利⽤ SplitChunksPlugin 分离⻚⾯公共⽂件)
- [Tree Shaking(摇树优化)的使用和原理分析](#Tree Shaking(摇树优化)的使用和原理分析)
-
- 基础介绍
- DCE (Dead code elimination)
- [Tree-shaking 原理](#Tree-shaking 原理)
- [Scope Hoisting使用和原理分析](#Scope Hoisting使用和原理分析)
-
- 背景:构建后的代码存在⼤量闭包代码
- 模块转换分析
- [进⼀步分析 webpack 的模块机制](#进⼀步分析 webpack 的模块机制)
- [scope hoisting 原理](#scope hoisting 原理)
- [scope hoisting 使⽤](#scope hoisting 使⽤)
- 代码分割和动态import
-
- 代码分割的意义
- [懒加载 JS 脚本的⽅式](#懒加载 JS 脚本的⽅式)
- [如何使⽤动态 import?](#如何使⽤动态 import?)
- 在webpack中使用ESLint
-
- 行内优秀的eslint规范
- [制定团队的 ESLint 规范](#制定团队的 ESLint 规范)
- [ESLint 如何执⾏落地?](#ESLint 如何执⾏落地?)
-
- [⽅案⼀:webpack 与 CI/CD 集成](#⽅案⼀:webpack 与 CI/CD 集成)
- [⽅案⼆:webpack 与 ESLint 集成](#⽅案⼆:webpack 与 ESLint 集成)
- webpack打包组件和基础库
-
- ⽀持的使⽤⽅式
- 如何将库暴露出去?
- [使用TerSerPlugin插件对 .min 压缩](#使用TerSerPlugin插件对 .min 压缩)
- 根据环境设置⼊⼝⽂件
- webpack实现SSR打包
-
- ⻚⾯打开过程
- [服务端渲染 (SSR) 是什么?](#服务端渲染 (SSR) 是什么?)
- 浏览器和服务器交互流程
- [客户端渲染 vs 服务端渲染](#客户端渲染 vs 服务端渲染)
- [SSR 代码实现思路](#SSR 代码实现思路)
- [webpack ssr 打包存在的问题](#webpack ssr 打包存在的问题)
- 如何解决样式不显示的问题?
- ⾸屏数据如何处理?
- 优化构建时命令行的显示日志
-
- [webpack构建统计信息 stats](#webpack构建统计信息 stats)
- 如何优化命令⾏的构建⽇志
- 构建异常和中断处理
自动清理构建目录产物
webpack4.x使用clean-webpack-plugin@3版本:
npm i clean-webpack-plugin@3 -D
webpack配置:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins: [
new CleanWebpackPlugin(),
]
PostCSS插件autoprefixer自动补齐CSS3前缀
需要安装postcss-loader
、postcss
、autoprefixer
插件。
其中webpack4.x需要安装postcss-loader@4。
npm i postcss-loader@4 postcss@8 autoprefixer -D
配置如下:
module.exports = {
module: {
rules: [
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'autoprefixer',
{
overrideBrowserslist: ['last 2 version', '>1%', 'ios 7']
}
]
]
}
}
}
]
}
]
}
}
移动端CSS px自动转换成rem
使用px2rem-loader,⻚⾯渲染时计算根元素的 font-size 值,可以使⽤⼿淘的lib-flexible
库,地址:https://github.com/amfe/lib-flexible
npm i px2rem-loader -D
npm i lib-flexible -S
配置:
module.exports = {
module: {
rules: [
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75,
remPrecision: 8
}
}
]
}
]
}
}
如果需要验证 把安装的lib-flexible
源码先拷贝到html头部head里面,代码如下:
<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>
静态资源内联
资源内联的意义:
- 代码层⾯:
- ⻚⾯框架的初始化脚本
- 上报相关打点
- css 内联避免⻚⾯闪动
- 请求层⾯:减少 HTTP ⽹络请求数
- ⼩图⽚或者字体内联 (url-loader)
具体实现:
安装raw-loader@0.5.1
版本
npm i raw-loader@0.5.1 -D
-
raw-loader 内联 html
<%= require('raw-loader!./meta.html') %>
-
raw-loader 内联 JS
<%= require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js') %>
示例,例如我们抽离meta通用的代码为一个meta.html,以及flexible.js插件都内联带html页面中。
meta.html示例代码:
<meta charset="UTF-8">
<meta name="viewport" content="viewport-fit=cover,width=device-width,initial-scale=1,user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta name="keywords" content="keywords content">
<meta name="name" itemprop="name" content="name content">
<meta name="apple-mobile-web-app-capable" content="no">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
模板index.html代码:
<!DOCTYPE html>
<html lang="en">
<head>
<%= require('raw-loader!./meta.html') %>
<title>Document</title>
<script type="text/javascript">
<%= require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js') %>
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
多页面应用打包通用方案
动态获取 entry 和设置 html-webpack-plugin 数量,使用glob插件的glob.sync
方法获取所有的entry。
glob.sync(path.join(__dirname, './src/*/index.js')),
安装glob,我们安装版本7的,其他版本对node有要求,并且使用方式有区别:
npm i glob@7 -D
配置:
const glob = require("glob");
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"));
Object.keys(entryFiles).map((index) => {
const entryFile = entryFiles[index];
const match = entryFile.match(/src\/(.*)\/index\.js/);
console.log(match);
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: false
}
})
);
});
return {
entry,
htmlWebpackPlugins
};
};
const { entry, htmlWebpackPlugins } = setMPA();
module.exports = {
entry: entry,
output: {
path: path.join(__dirname, "dist"),
filename: "[name]_[chunkhash:8].js"
},
mode: "production",
plugins: [
// 省略其他插件
].concat(htmlWebpackPlugins)
};
使用sourcemap
作⽤:通过source map
定位到源代码
sourcemap参考文章:http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html
source map 关键字:
- eval: 使⽤eval包裹模块代码
- source map: 产⽣.map⽂件
- cheap: 不包含列信息
- inline: 将.map作为DataURI嵌⼊,不单独⽣成.map⽂件
- module:包含loader的sourcemap
source map 类型:
一般开发环境配置:
module.exports = {
// 其他代码省略
devtool: "source-map"
};
生产环境配置:
module.exports = {
// 其他代码省略
devtool: "none"
};
提取页面公共资源
基础库分离
将 react、react-dom、vue、Jquery等基础包通过 cdn 引⼊,不打⼊ bundle 中。
使⽤ html-webpack-externals-plugin
。
npm i html-webpack-externals-plugin -D
html-webpack-externals-plugin插件参考地址:https://www.npmjs.com/package/html-webpack-externals-plugin。
示例:
new HtmlWebpackExternalsPlugin({
externals: [
{
module: "react",
entry: "https://unpkg.com/react@18.2.0/umd/react.production.min.js",
global: "React"
},
{
module: "react-dom",
entry: "https://unpkg.com/react-dom@18/umd/react-dom.production.min.js",
global: "ReactDOM"
}
]
})
利⽤ SplitChunksPlugin 进⾏公共脚本分离
Webpack4 内置splitChunks的,替代CommonsChunkPlugin插件。
chunks 参数说明:
- async 分离异步加载的模块(默认)。
- initial 同步引⼊的库进⾏分离。
- all 所有引⼊的库进⾏分离(推荐)。
示例代码:
optimization: {
splitChunks: {
chunks: "async",
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: "~",
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}
}
}
}
利⽤ SplitChunksPlugin 分离基础包
例如要抽离除react、react-dom,配置代码如下:
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test: /(react|react-dom)/,
name: "vendors",
chunks: "all"
}
}
}
}
其中test属性标识匹配出需要分离的包。
抽离的基础文件要被模板文件引用,需要在html-webpack-plugin插件中配置chunks,示例代码:
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: ["vendors", pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
利⽤ SplitChunksPlugin 分离⻚⾯公共⽂件
-
minChunks: 设置最⼩引⽤次数为2次
-
minuSize: 分离的包体积的⼤⼩
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: "commons",
chunks: "all",
minChunks: 2
}
}
}
}
Tree Shaking(摇树优化)的使用和原理分析
基础介绍
一个模块可能有多个⽅法,只要其中的某个⽅法使⽤到了,则整个⽂件都会被打到
bundle ⾥⾯去,tree shaking
就是只把⽤到的⽅法打⼊ bundle ,没⽤到的⽅法会在
uglify
阶段被擦除掉。
uglify阶段:将 JavaScript代码进行压缩、混淆,并去除一些不必要的代码,从而减小文件体积。
webpack4及以上默认内置了,当mode为production
情况下默认开启。进行tree shaking条件是必须是 ES6 的语法,CJS 的⽅式不⽀持。
DCE (Dead code elimination)
DCE 解释就是死代码消除的意思。
- 代码不会被执⾏,不可到达
- 代码执⾏的结果不会被⽤到
- 代码只会影响死变量(只写不读)
示例:
if (false) {
console.log('这段代码永远不会执行');
}
如上所示代码,在uglify 阶段就会删除⽆⽤代码。
Tree-shaking 原理
利⽤ ES6 模块的特点:
- 只能作为模块顶层的语句出现
- import 的模块名只能是字符串常量
- import binding 是 immutable的(import引用的模块是不能修改的)
注:使用mode为production与none 来验证tree-shaking。
Scope Hoisting使用和原理分析
背景:构建后的代码存在⼤量闭包代码
如图所示:
这样会导致什么问题?
- ⼤量作⽤域包裹代码,导致体积增⼤(模块越多越明显)
- 运⾏代码时创建的函数作⽤域变多,内存开销变⼤
模块转换分析
示例:我们编写了一个模块,代码如下
import { helloworld } from "./helloworld";
import "../../common";
document.write(helloworld());
我们把webpack4中的mode 设置为none,看下编译结果,webpack会把编写的模块转换成模块初始化函数,代码如下:
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _helloworld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
document.write(Object(_helloworld__WEBPACK_IMPORTED_MODULE_0__["helloworld"])());
/***/ })
结果说明:
- 被 webpack 转换后的模块会带上⼀层包裹
- import 会被转换成 __webpack_require
当然上面两个import导入的模块编译为如下代码:
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helloworld", function() { return helloworld; });
function helloworld() {
return 'Hello webpack';
}
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "common", function() { return common; });
function common() {
return "common module";
}
/***/ })
进⼀步分析 webpack 的模块机制
(function (modules) {
// webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
});
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
return __webpack_require__(0);
})([
/* 0 */
function (module, __webpack_exports__, __webpack_require__) {
// 省略代码
},
/* 1 */
function (module, __webpack_exports__, __webpack_require__) {
// 省略代码
},
/* 2 */
function (module, __webpack_exports__, __webpack_require__) {
// 省略代码
}
/******/
]);
上述代码分析:
- 打包出来的是⼀个 IIFE (匿名闭包)
- modules 是⼀个数组,每⼀项是⼀个模块初始化函数
- __webpack_require ⽤来加载模块,返回 module.exports
- 通过 webpack_require(0) 启动程序
scope hoisting 原理
原理:将所有模块的代码按照引⽤顺序放在⼀个函数作⽤域⾥,然后适当的重命名⼀
些变量以防⽌变量名冲突。
优点:通过 scope hoisting
可以减少函数声明代码和内存开销。
优化前代码:
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helloworld", function() { return helloworld; });
function helloworld() {
return 'Hello webpack';
}
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "common", function() { return common; });
function common() {
return "common module";
}
/***/ })
优化后代码:
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// CONCATENATED MODULE: ./src/index/helloworld.js
function helloworld() {
return 'Hello webpack';
}
// CONCATENATED MODULE: ./common/index.js
function common() {
return "common module";
}
// CONCATENATED MODULE: ./src/index/index.js
document.write(helloworld());
})
scope hoisting 使⽤
webpack mode 为 production 默认开启 ,必须是 ES6 语法,CJS 不⽀持。
由于mode为production来验证的话,默认会被压缩,我们可以设置为none,然后添加ModuleConcatenationPlugin
来验证,示例代码:
const webpack = require("webpack");
module.exports = {
// 其他代码省略
mode: "none",
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
};
注:webpack4及以上mode为production的时候,默认内置了
ModuleConcatenationPlugin
代码分割和动态import
代码分割的意义
对于⼤的 Web 应⽤来讲,将所有的代码都放在⼀个⽂件中显然是不够有效的,特别是当你的
某些代码块是在某些特殊的时候才会被使⽤到。webpack 有⼀个功能就是将你的代码库分割成
chunks(语块),当代码运⾏到需要它们的时候再进⾏加载。
适⽤的场景:
- 抽离相同代码到⼀个共享块
- 脚本懒加载,使得初始下载的代码更⼩
懒加载 JS 脚本的⽅式
- CommonJS:require.ensure
- ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换)
如何使⽤动态 import?
安装 babel 插件
npm i @babel/plugin-syntax-dynamic-import -D
ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换),在babelrc中添加:
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
代码分割的效果如图所示:
上面编译的圈红的,如果是动态加载的,那会生成一个以[number]_[chunkhash].js生成的文件名
以React示例代码,其中2_139fa159.js
编译前(text.js)代码为:
import React from "react";
export default () => <div>动态 import</div>;
编译后的源码:
(window.webpackJsonp = window.webpackJsonp || []).push([
[2],
{
12: function (n, e, t) {
"use strict";
t.r(e);
var i = t(0),
o = t.n(i);
e.default = function () {
return o.a.createElement("div", null, "动态 import");
};
}
}
]);
入口文件示例代码:
import React from 'react';
import { createRoot } from 'react-dom/client';
import logo from './images/logo.png';
import './search.less';
class Search extends React.Component {
constructor(...args) {
super(...args);
this.state = {
Text: null,
};
}
loadComponent() {
import('./text').then((Text) => {
this.setState({
Text: Text.default,
});
});
}
render() {
const { Text } = this.state;
return (
<div className="search-text">
{Text ? <Text /> : null}
搜索文字的内容
<img src={logo} alt="logo" onClick={this.loadComponent.bind(this)} />
</div>
);
}
}
createRoot(document.getElementById('root')).render(<Search />);
在webpack中使用ESLint
行内优秀的eslint规范
- Airbnb:
eslint-config-airbnb
、eslint-config-airbnb-base
- alloyteam团队 eslint-config-alloy:https://github.com/AlloyTeam/eslint-config-alloy
eslint-config-airbnb:默认导出包含大多数ESLint规则,包括ECMAScript 6+和React。它需要eslint, eslint-plugin-import, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-jsx-a11y。请注意,它不会启用我们的React Hooks规则。
当然如果不需要React,那么可以参考使用eslint-config-airbnb-base
。
以使用eslint-config-airbnb
为例:
npm i eslint-config-airbnb eslint@7 eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y -D
注意:elsint要安装7.x版本。
再安装babel-eslint
、eslint-loader
。
npm i babel-eslint eslint-loader -D
eslint-loader详细参考地址:https://github.com/webpack-contrib/eslint-loader
根目录下新建.eslintrc.js
:
module.exports = {
"parser": "babel-eslint",
"extends": "airbnb",
"env": {
"browser": true,
"node": true
},
"rules": {
"indent": ["error", 4]
}
};
说明:
-
parser: 配置解析器
-
extends:扩展配置文件
在extends
属性中使用"eslint:recommended"
可以启用报告常见问题的核心规则子集(这些规则在 规则页 上用复选标记(推荐)标识)。module.exports = {
"extends": "eslint:recommended",
};
详细参考:https://eslint.nodejs.cn/docs/latest/use/configure/configuration-files
-
env:配置文件中指定环境(browser - 浏览器全局变量;node - Node.js 全局变量和 Node.js 作用域)
详细参考地址:https://eslint.nodejs.cn/docs/latest/use/configure/language-options#specifying-environments -
rules:配置规则
"off" 或 0 - 关闭规则
"warn" 或 1 - 打开规则作为警告(不影响退出代码)
"error" 或 2 - 打开规则作为错误(触发时退出代码为 1)
第二种方式使用eslint-webpack-plugin
替换eslint-loader
eslint-webpack-plugin 3.0 which works only with webpack 5. For the webpack 4, see the 2.x branch.
npm i eslint-webpack-plugin@2 -D
eslint-webpack-plugin详细参考地址:https://github.com/webpack-contrib/eslint-webpack-plugin
配置代码:
const ESLintPlugin = require("eslint-webpack-plugin");
module.exports = {
mode: "production",
plugins: [
new ESLintPlugin({
fix: true, // 启用ESLint自动修复功能
extensions: ["js", "jsx"],
context: path.join(__dirname, "src"), // 文件根目录
exclude: ["/node_modules/"], // 指定要排除的文件/目录
cache: true // 缓存
})
]
};
制定团队的 ESLint 规范
- 不重复造轮⼦,基于 eslint:recommend 配置并改进
- 能够帮助发现代码错误的规则,全部开启
- 帮助保持团队的代码⻛格统⼀,⽽不是限制开发体验
常用规则参考:https://eslint.nodejs.cn/docs/latest/rules/
ESLint 如何执⾏落地?
- CI/CD 系统集成
- webpack 集成
⽅案⼀:webpack 与 CI/CD 集成
本地开发阶段增加 precommit 钩⼦
安装 husky
npm install husky --save-dev
增加 npm script,通过 lint-staged 增量检查修改的⽂件
"scripts": {
"precommit": "lint-staged"
},
"lint-staged": {
"linters": {
"*.{js,scss}": ["eslint --fix", "git add"]
}
},
⽅案⼆:webpack 与 ESLint 集成
使⽤ eslint-loader或者eslint-webpack-plugin插件,构建时检查 JS 规范。
eslint-loader方式:
rules: [
{
test: /.js$/,
use: [
"babel-loader",
"eslint-loader"
]
}
]
eslint-webpack-plugin方式:
plugins: [
new ESLintPlugin({
fix: true, // 启用ESLint自动修复功能
extensions: ["js", "jsx"],
context: path.join(__dirname, "src"), // 文件根目录
exclude: ["/node_modules/"], // 指定要排除的文件/目录
cache: true // 缓存
})
]
webpack打包组件和基础库
webpack 除了可以⽤来打包应⽤,也可以⽤来打包 js 库
示例:实现⼀个⼤整数加法库的打包
- 需要打包压缩版和⾮压缩版本
- ⽀持 AMD/CJS/ESM 模块引⼊
⽀持的使⽤⽅式
-
⽀持 ES module
import * as largeNumber from 'large-number';
// ...
largeNumber.add('999', '1'); -
⽀持 CJS
const largeNumbers = require('large-number');
// ...
largeNumber.add('999', '1'); -
⽀持 AMD
require(['large-number'], function (large-number) {
// ...
largeNumber.add('999', '1');
}); -
支持script 引⼊
<!doctype html>
<html> ... <script src="./large-number.min.js"></script> <script> // Global variable largeNumber.add('999', '1'); // Property in the window object window. largeNumber.add('999', '1'); </script> </html>
如何将库暴露出去?
-
library: 指定库的全局变量
-
libraryTarget: ⽀持库引⼊的⽅式
module.exports = {
mode: "production",
entry: {
"large-number": "./src/index.js",
"large-number.min": "./src/index.js"
},
output: {
filename: "[name].js",
library: "largeNumber",
libraryExport: "default",
libraryTarget: "umd"
}
};
使用TerSerPlugin插件对 .min 压缩
通过 include 设置只压缩 min.js 结尾的⽂件,webpack4需要安装terser-webpack-plugin@4版本
npm i terser-webpack-plugin@4 -D
const TerSerPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'none',
entry: {
'large-number': './src/index.js',
'large-number.min': './src/index.js'
},
output: {
filename: '[name].js',
library: 'largeNumber',
libraryTarget: 'umd',
libraryExport: 'default'
},
optimization: {
minimize: true,
minimizer: [
new TerSerPlugin({
include: /\.min\.js$/
}),
]
}
}
根据环境设置⼊⼝⽂件
在工程目录下新建index.js,并且把package.json 的 main 字段为设置为 index.js,其index.js如下:
if (process.env.NODE_ENV === "production") {
module.exports = require("./dist/large-number.min.js");
} else {
module.exports = require("./dist/large-number.js");
}
在package.json中添加命令:
"scripts": {
"prepublish": "webpack"
},
最后通过npm publish
发布到npm上。
大整数加法代码:
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
let y = 0
let sum
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
}
// 0 + ''
ret = sum + ret
}
if (carry) {
ret = carry + ret
}
return ret
}
// add('999', '1');
webpack实现SSR打包
⻚⾯打开过程
- 开始加载
- HTML加载成功,开始加载数据
- 数据加载成功,渲染成功开始,加载图⽚资源
- 图⽚加载成功,⻚⾯可交互
服务端渲染 (SSR) 是什么?
渲染: HTML + CSS + JS + Data -> 渲染后的 HTML
服务端:
- 所有模板等资源都存储在服务端
- 内⽹机器拉取数据更快
- ⼀个 HTML 返回所有数据
浏览器和服务器交互流程
客户端渲染 vs 服务端渲染
总结:服务端渲染 (SSR) 的核⼼是减少请求
SSR 的优势:
- 减少⽩屏时间
- 对于 SEO 友好
SSR 代码实现思路
服务端
- 使⽤ react-dom/server 的 renderToString ⽅法将React 组件渲染成字符串
- 服务端路由返回对应的模板
安装express:
npm i express -D
服务端的代码示例server/index.js:
if (typeof window === "undefined") {
global.window = {};
}
const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("../dist/search-server");
function server(port) {
const app = express();
app.use(express.static("dist"));
app.get("/search", (req, res) => {
const html = renderMarkup(renderToString(SSR));
res.status(200).send(html);
});
app.listen(port, () => {
console.log("server is running on port:" + port);
});
}
server(process.env.PORT || 3000);
function renderMarkup(html) {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root">${html}</div>
</body>
</html>
`;
}
客户端
- 打包出针对服务端的组件
客户端组件代码示例:
const React = require("react");
const logo = require("./images/logo.png");
require("./search.less");
class Search extends React.Component {
constructor(...args) {
super(...args);
this.state = {
Text: null
};
}
loadComponent() {
import("./text").then((Text) => {
this.setState({
Text: Text.default
});
});
}
render() {
const { Text } = this.state;
return (
<div className="search-text">
{Text ? <Text /> : null}
搜索文字的内容
<img src={logo} alt="logo" onClick={this.loadComponent.bind(this)} />
</div>
);
}
}
module.exports = <Search />;
客户端编写webpack.ssr.js:
"use strict";
const path = require("path");
const webpack = require("webpack");
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/*/index-server.js"));
Object.keys(entryFiles).map((index) => {
const entryFile = entryFiles[index];
const match = entryFile.match(/src\/(.*)\/index-server\.js/);
const pageName = match && match[1];
if (pageName) {
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: false
}
})
);
}
});
return {
entry,
htmlWebpackPlugins
};
};
const { entry, htmlWebpackPlugins } = setMPA();
module.exports = {
entry: entry,
output: {
path: path.join(__dirname, "dist"),
filename: "[name]-server.js",
libraryTarget: "umd"
},
mode: "production",
module: {
rules: [
{
test: /.js$/,
use: ["babel-loader"]
},
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
},
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"less-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"autoprefixer",
{
overrideBrowserslist: ["last 2 version", ">1%", "ios 7"]
}
]
]
}
}
},
{
loader: "px2rem-loader",
options: {
remUnit: 75,
remPrecision: 8
}
}
]
},
{
test: /.(png|jpe?g|gif)$/,
use: [
{
loader: "file-loader",
options: {
esModule: false,
name: "[name]_[hash:8].[ext]"
}
}
]
},
{
test: /.(woff|woff2|eot|otf|ttf)$/,
use: [
{
loader: "file-loader",
options: {
esModule: false,
name: "[name]_[hash:8].[ext]"
}
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name]_[contenthash:8].css"
}),
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require("cssnano")
}),
new CleanWebpackPlugin()
].concat(htmlWebpackPlugins),
devtool: "none"
};
配置package.json命令:
"scripts": {
"build:ssr": "webpack --config webpack.ssr.js"
}
我们通过npm run build:ssr
编译组件,通过node server/inde.js
启动后台服务,启动成功后,我们可以通过http://localhost:3000/search
访问。
webpack ssr 打包存在的问题
浏览器的全局变量 (Node.js 中没有 document, window)
- 组件适配:将不兼容的组件根据打包环境进⾏适配
- 请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios
样式问题 (Node.js ⽆法解析 css)
- ⽅案⼀:服务端打包通过 ignore-loader 忽略掉 CSS 的解析
- ⽅案⼆:将 style-loader 替换成 isomorphic-style-loader
如何解决样式不显示的问题?
使⽤打包出来的浏览器端 html 为模板,设置占位符,动态插⼊组件。
如图所示:
首先在客户端html模板中添加占位符,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="root"><!--HTML_PLACEHOLDER--></div>
</body>
</html>
然后服务端的server/index.js调整为:
if (typeof window === "undefined") {
global.window = {};
}
const fs = require("fs");
const path = require("path");
const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("../dist/search-server");
const htmlTemplate = fs.readFileSync(path.join(__dirname, "../dist/search.html"), "utf-8");
function server(port) {
const app = express();
app.use(express.static("dist"));
app.get("/search", (req, res) => {
const html = renderMarkup(renderToString(SSR));
res.status(200).send(html);
});
app.listen(port, () => {
console.log("server is running on port:" + port);
});
}
server(process.env.PORT || 3000);
function renderMarkup(str) {
return htmlTemplate.replace("<!--HTML_PLACEHOLDER--", str);
}
客户端重新npm run build:ssr
,然后再通过http://localhost:3000/search
访问。
⾸屏数据如何处理?
在客户端html模板页面添加占位符,服务端获取数据,替换占位符。
客户端html模板:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="root"><!--HTML_PLACEHOLDER--></div>
<!--INITIAL_DATA_PLACEHOLDER-->
</body>
</html>
服务端server/index.js代码调整:
if (typeof window === "undefined") {
global.window = {};
}
const fs = require("fs");
const path = require("path");
const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("../dist/search-server");
const htmlTemplate = fs.readFileSync(path.join(__dirname, "../dist/search.html"), "utf-8");
const mockData = require("./data.json");
function server(port) {
const app = express();
app.use(express.static("dist"));
app.get("/search", (req, res) => {
const html = renderMarkup(renderToString(SSR));
res.status(200).send(html);
});
app.listen(port, () => {
console.log("server is running on port:" + port);
});
}
server(process.env.PORT || 3000);
function renderMarkup(str) {
const dataStr = JSON.stringify(mockData);
return htmlTemplate.replace("<!--HTML_PLACEHOLDER--", str).replace("<!--INITIAL_DATA_PLACEHOLDER-->", `<script src="text/javascript">window.__initial_data = ${dataStr}</script>`);
}
最后运行页面源码如图所示:
优化构建时命令行的显示日志
webpack构建统计信息 stats
如何优化命令⾏的构建⽇志
1、使⽤ friendly-errors-webpack-plugin
- success: 构建成功的⽇志提示
- warning: 构建警告的⽇志提示
- error: 构建报错的⽇志提示
2、stats 设置成 errors-only
安装friendly-errors-webpack-plugin:
npm i friendly-errors-webpack-plugin -D
开发配置webpack.dev.js:
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
module.exports = {
plugins: [
new FriendlyErrorsWebpackPlugin()
],
devServer: {
contentBase: "./dist",
hot: true,
stats: "errors-only"
}
};
生产配置webpack.prod.js:
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
module.exports = {
plugins: [
new FriendlyErrorsWebpackPlugin()
],
stats: "errors-only"
};
构建异常和中断处理
如何判断构建是否成功?
- 在 CI/CD 的 pipline 或者发布系统需要知道当前构建状态
- 每次构建完成后输⼊ echo $? 获取错误码
webpack4 之前的版本构建失败不会抛出错误码 (error code)
Node.js 中的 process.exit 规范
- 0 表示成功完成,回调函数中,err 为 null
- ⾮ 0 表示执⾏失败,回调函数中,err 不为 null,err.code 就是传给 exit 的数字
如何主动捕获并处理构建错误?
- compiler 在每次构建结束后会触发 done 这个 hook
- process.exit 主动处理构建报错
在配置中可以添加如下代码,进行中断处理,例如错误上报等。
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); // 1表示错误码并退出
}
});
}
]
};
结果示例:
上面的build error
与errno 1
就是上面代码配置的中断处理逻辑。