【webpack4系列】webpack进阶用法(三)

文章目录

    • 自动清理构建目录产物
    • 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-loaderpostcssautoprefixer插件。

其中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-airbnbeslint-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-eslinteslint-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 errorerrno 1就是上面代码配置的中断处理逻辑。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/425051.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

食品包装识别系统源码分享

食品包装识别检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

(1)LT9211学习笔记

文章目录 前言一、MIPI接口是什么&#xff1f;二、LT9211框图及应用1&#xff0c;作为MIPI发射机&#xff1a;2&#xff0c;作为双端口Lvds发射机3&#xff0c;作为TTL输出4&#xff0c;作为MIPI接收机&#xff1a;5&#xff0c;作为双端口Lvds接收机6&#xff0c;作为TTL输入 …

林草湿地址、导出echart为word

2.导出 // 导出exportDoc () {this.loading truelet arrRefs [this.$refs.endChart, this.$refs.processChart, this.$refs.officeEndChart]setTimeout(() > {Promise.all(arrRefs.map((canvasDom) > {return this.html2canvasHandle(canvasDom)})).then(res > {let…

组合数(模板)

1.杨辉三角求组合数&#xff0c;最高只能求几千内的组合数。 #include<bits/stdc.h> using namespace std; #define int long long int C[1005][1005]; signed main() {//求 1000 以内的组合数 for(int i0;i<1000;i){C[i][0]C[i][i]1;for(int j1;j<i;j){C[i][j]C[…

MATLAB入门教程

MATLAB安装教程可参考链接&#xff1a;matlab怎么安装 matlab安装教程-电脑软件-PHP中文网 1.MATLAB的工作环境 &#xff08;1&#xff09;命令窗(command window) 是对MATLAB进行操作的主要载体。默认情况下&#xff0c;启动MATLAB时就打开命令窗。MATLAB的所有所数…

微软九月补丁星期二发现了 79 个漏洞

微软将在2024 年 9 月补丁星期二修复 79 个漏洞。 微软有证据表明&#xff0c;发布的四个漏洞被野外利用和/或公开披露&#xff1b;所有四个漏洞均已在CISA KEV上列出。微软还在修补四个关键的远程代码执行 (RCE) 漏洞。 不同寻常的是&#xff0c;微软本月尚未修补任何浏览器…

Codes 开源研发项目管理平台——敏捷测试管理创新解决方案

前言 Codes 是国内首款重新定义 SaaS 模式的开源项目管理平台&#xff0c;支持云端认证、本地部署、全部功能开放&#xff0c;并且对30人以下团队免费。它通过整合迭代、看板、度量和自动化等功能&#xff0c;简化测试协同工作&#xff0c;使敏捷测试更易于实施。并提供低成本的…

自动排课管理系统(源代码+论文+开题报告)

一、题目摘要 题目简要说明&#xff1a; 选排课系统功能的设计上&#xff0c;选排课系统可以分为登录、排课和选课3个子系统。登录子系统区分排课者(也即系统的管理者)、教师和学生这三者的不同身份&#xff0c;给出不同的权限&#xff0c;在页面中根据身份判断其相应具有的功…

Scratch游戏-史诗忍者7免费下载

小虎鲸Scratch资源站-免费少儿编程Scratch作品源码,素材,教程分享网站! 作品描述&#xff1a; 在Scratch版本的《史诗忍者7》中&#xff0c;你需要穿越关卡&#xff0c;击败敌人并收集33个水果。通过灵活的操作和精准的攻击&#xff0c;逐步闯过重重难关。游戏中提供了丰富的技…

C++3D迷宫

目录 开头程序程序的流程图程序游玩的效果下一篇博客要说的东西 开头 大家好&#xff0c;我叫这是我58。 程序 #include <iostream> using namespace std; void printmaze(char strmaze[5][5][5]) {cout << "-----" << endl;int i 0;int ia 0…

Java后端框架---Spring

目录 一.Spring是什么&#xff1f; 二.Spring Hello World 搭建 三.XML配置bean管理 1.bean标签 2.依赖注入 3.依赖注入的补充 四.注解配置bean管理 1.开启注解扫描 2.使用注解对类进行配置 3.自动注入 五.面向切面编程AOP 1.概述 2.通知 六.spring事务管理 1.数据库…

网络原理(3)—— 应用层、传输层(TCP)

1. 应用层 日常开发中最常用到的一层&#xff0c;主要涉及到两种情况&#xff1a; 1) 使用现成的应用层协议 2) 自己定义应用层协议 1.1 自定义应用层协议的流程 1. 明确前后端交互过程中需要传递哪些信息 实例&#xff1a;开发一个外卖软件 打开软件&#xff0c;首先需要展…

Linux 文件 IO 管理(第一讲)

Linux 文件 IO 管理&#xff08;第一讲&#xff09; 回顾 C 语言文件操作&#xff0c;提炼理解新创建的文件为什么被放在可执行文件的同级目录下&#xff1f;上述 log.txt 何时被创建&#xff1f;又是谁在打开它&#xff1f;那文件没有被打开的时候在哪里&#xff1f;一个进程可…

css 个人喜欢的样式 速查笔记

起因&#xff0c; 目的: 记录自己喜欢的&#xff0c; 觉得比较好看的 css. 下次用的时候&#xff0c;直接复制&#xff0c;很方便。 1. 个人 html 模板&#xff0c; 导入常用的 link 设置英语字体: Noto导入默认的 css使用网络 icon 图标导入 Bootstrap css 框架 html <…

【Python】谷歌浏览器总是自动更新,使用selenium跟chromedriver版本不匹配怎么办?

我发现&#xff0c;我的电脑对谷歌浏览器的禁止自动更新无效&#xff0c;哪怕是在任务计划程序里&#xff0c;禁止谷歌浏览器更新&#xff0c;也没有用。而且有时候点开右上角的三个点&#xff0c;也会自动更新版本。 但是往往chromedriver的更新版本更不上浏览器的版本哇&…

RPC远程调用的序列化框架

序列化框架对比&#xff1a; 一、Java Serialiazer 字段serialVersionUID的作用是为了在序列化时保持版本的兼容性&#xff0c;即版本升级时反序列化仍保持对象的唯一性。 //序列化 ByteArrayOutputStream bout new ByteArrayOutputStream(); ObjectOutoutStream out new O…

测试-Gatling 与性能测试

Gatling 与性能测试详解 一、什么是性能测试&#xff1f; 性能测试是一种软件测试类型&#xff0c;旨在评估系统在负载下的响应时间、吞吐量和资源利用率等性能指标。通过性能测试&#xff0c;开发者和运维团队能够识别出系统的瓶颈、优化系统性能&#xff0c;并确保其在实际…

使用kkFileView的几个问题

经测试&#xff0c;可以正常的预览&#xff1a;pdf&#xff0c;png&#xff0c;pptx&#xff0c;psd&#xff0c;zip&#xff0c;word&#xff0c;dcm(医学片子 dr ct等) 部署发布&#xff08;Linux&#xff09; 一、上传到服务器 上传jar包&#xff08;或者官方社区提供的包…

【Linux】精通GDB:打造你的Linux调试超能力

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;C从入门到精通 目录 一&#xff1a; &#x1f525; 什么是GDB / CGDB 二&#xff1a; &#x1f525; CGDB的安装 &#x1f34a; Linux-centos 三&#xff1a; &#x1f525; cgdb的使用背景 &#…

深度剖析iOS渲染

iOS App 图形图像渲染的基本流程&#xff1a; 1.CPU&#xff1a;完成对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制。 2.GPU&#xff1a;GPU拿到CPU计算好的显示内容&#xff0c;完成纹理的渲染&#xff0c; 渲染完成后将渲…