文章目录
- 自动清理构建目录产物
- PostCSS插件autoprefixer自动补齐CSS3前缀
- 移动端CSS px自动转换成rem
- 静态资源内联
- 多页面应用打包通用方案
- 使用sourcemap
- 提取页面公共资源
- 基础库分离
- 利⽤ SplitChunksPlugin 进⾏公共脚本分离
- 利⽤ SplitChunksPlugin 分离基础包
- 利⽤ SplitChunksPlugin 分离⻚⾯公共⽂件
- Tree Shaking(摇树优化)的使用和原理分析
- 基础介绍
- DCE (Dead code elimination)
- Tree-shaking 原理
- Scope Hoisting使用和原理分析
- 背景:构建后的代码存在⼤量闭包代码
- 模块转换分析
- 进⼀步分析 webpack 的模块机制
- scope hoisting 原理
- scope hoisting 使⽤
- 代码分割和动态import
- 代码分割的意义
- 懒加载 JS 脚本的⽅式
- 如何使⽤动态 import?
- 在webpack中使用ESLint
- 行内优秀的eslint规范
- 制定团队的 ESLint 规范
- ESLint 如何执⾏落地?
- ⽅案⼀:webpack 与 CI/CD 集成
- ⽅案⼆:webpack 与 ESLint 集成
- webpack打包组件和基础库
- ⽀持的使⽤⽅式
- 如何将库暴露出去?
- 使用TerSerPlugin插件对 .min 压缩
- 根据环境设置⼊⼝⽂件
- webpack实现SSR打包
- ⻚⾯打开过程
- 服务端渲染 (SSR) 是什么?
- 浏览器和服务器交互流程
- 客户端渲染 vs 服务端渲染
- SSR 代码实现思路
- webpack ssr 打包存在的问题
- 如何解决样式不显示的问题?
- ⾸屏数据如何处理?
- 优化构建时命令行的显示日志
- 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 cachevar installedModules = {};// The require functionfunction __webpack_require__(moduleId) {// Check if module is in cacheif (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 functionmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);// Flag the module as loadedmodule.l = true;// Return the exports of the modulereturn 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.jsfunction helloworld() {return 'Hello webpack';}// CONCATENATED MODULE: ./common/index.jsfunction common() {return "common module";}// CONCATENATED MODULE: ./src/index/index.jsdocument.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 variablelargeNumber.add('999', '1');// Property in the window objectwindow. 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 - 1let j = b.length - 1let carry = 0let ret = ''while (i >= 0 || j >= 0) {let x = 0let y = 0let sumif (i >= 0) {x = a[i] - '0'i--}if (j >= 0) {y = b[j] - '0'j--}sum = x + y + carryif (sum >= 10) {carry = 1sum -= 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
就是上面代码配置的中断处理逻辑。