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

news/2024/9/18 17:28:09/ 标签: webpack, 前端, node.js

文章目录

    • 自动清理构建目录产物
    • 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__573">进⼀步分析 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 />);

webpackESLint_795">在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 如何执⾏落地?

webpack__CICD__890">⽅案⼀: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__911">⽅案⼆: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_939">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');

webpackSSR_1096">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__1367">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_1498">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.ppmy.cn/news/1526724.html

相关文章

【C++】——list

文章目录 list介绍和使用list注意事项 list模拟实现list和vector的不同 list介绍和使用 在C中&#xff0c;list是一个带头双向链表 list注意事项 迭代器失效 删除元素&#xff1a;当使用迭代器删除一个元素时&#xff0c;指向该元素的迭代器会失效&#xff0c;但是不会影响其他…

【开发语言】写程序的两大基本原则(PO和NT原则)

PO&#xff08;Prioritize Operability&#xff09;原则 定义&#xff1a;确保程序能够正常运行&#xff0c;没有基本的语法错误&#xff0c;能够在预定的环境中执行其基本功能。 应用&#xff1a; 代码编写&#xff1a;在编写代码时&#xff0c;始终遵循所选编程语言的语法…

3. 轴指令(omron 机器自动化控制器)——>MC_MoveAbsolute

机器自动化控制器——第三章 轴指令 4 MC_MoveAbsolute变量▶输入变量▶输入输出变量▶输入输出变量 功能说明▶指令详情▶时序图▶重启运动指令▶多重启动运动指令▶异常 示例程序1▶参数设定▶动作示例▶梯形图▶结构文本(ST) 示例程序2▶参数设定▶动作示例▶梯形图▶结构文…

实现CPU压力测试工具的C语言实现

实现CPU压力测试工具的C语言实现 一、背景与需求二、伪代码设计三、C语言实现四、编译和运行五、注意事项在软件开发和系统维护中,CPU压力测试是一项重要任务,用于评估系统的稳定性和性能。本篇文章将详细介绍如何使用C语言结合伪代码实现一个简单的CPU压力测试工具。 一、…

数据库语言、SQL语言、数据库系统提供的两种语言

1.数据库语言 数据库语言有很多种&#xff0c;其中一种是SQL语言。 2. SQL语言 【几乎所有的关系数据库系统都使用SQL语言。】 SQL语言中包含很多不同的部分&#xff0c;有&#xff1a; &#xff08;1&#xff09;DDL语言&#xff08;Data definition language&#xff09;…

多输入多输出 | Matlab实现SSA-BP麻雀搜索算法优化BP神经网络多输入多输出预测

多输入多输出 | Matlab实现SSA-BP麻雀搜索算法优化BP神经网络多输入多输出预测 目录 多输入多输出 | Matlab实现SSA-BP麻雀搜索算法优化BP神经网络多输入多输出预测预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 Matlab实现SSA-BP麻雀搜索算法优化BP神经网络多输…

C/C++语言基础--从C到C++的不同(上)

本专栏目的 更新C/C的基础语法&#xff0c;包括C的一些新特性 前言 之前更新的C语言&#xff0c;感谢大家的点赞收藏关注&#xff0c;接下来我们逐步也开始更新C&#xff1b;C语言后面也会继续更新知识点&#xff0c;如内联汇编&#xff1b;本人现在正在写一个C语言的图书管理系…

深度学习自编码器 - 随机编码器和解码器篇

序言 在深度学习领域&#xff0c;自编码器作为一种无监督学习技术&#xff0c;凭借其强大的特征表示能力&#xff0c;在数据压缩、去噪、异常检测及生成模型等多个方面展现出独特魅力。其中&#xff0c;随机编码器和解码器作为自编码器的一种创新形式&#xff0c;进一步拓宽了…

CORS跨域请求共享

参考文章: https://xz.aliyun.com/t/12001?time__1311GqGxRGiti%3Dd052x%2BxCwx7qGIxpbDulE%3DoD https://blog.csdn.net/weixin_46622976/article/details/128452494 跨域资源共享 自己的理解&#xff0c;一般来讲&#xff0c;我们使用未授权的接口漏洞&#xff0c;都是因…

Ruoyi Cloud K8s 部署

本文视频版本:https://www.bilibili.com/video/BV1xF4Se3Esv 参考 https://blog.csdn.net/Equent/article/details/137779505 https://blog.csdn.net/weixin_48711696/article/details/138117392 https://zhuanlan.zhihu.com/p/470647732 https://gitee.com/y_project/Ruo…

反射的应用

1、获取Class类对象 //1、Class.forName(类的全路径) Class<?> aClass1 Class.forName("com.itheima.d2_reflect.Student"); //2、类.class Class<Student> aClass2 Student.class; //3、对象.getClass() Class<? extends Student> aClass3 …

OpenHarmony鸿蒙( Beta5.0)RTSPServer实现播放视频详解

鸿蒙开发往期必看&#xff1a; 一分钟了解”纯血版&#xff01;鸿蒙HarmonyOS Next应用开发&#xff01; “非常详细的” 鸿蒙HarmonyOS Next应用开发学习路线&#xff01;&#xff08;从零基础入门到精通&#xff09; “一杯冰美式的时间” 了解鸿蒙HarmonyOS Next应用开发路…

Java学习线路(2024版)

Java 作为一门成熟、强大且灵活的编程语言&#xff0c;广泛应用于企业级开发、Web开发、移动开发、大数据等领域。随着技术的不断演进&#xff0c;Java 生态系统不断扩展&#xff0c;学习路径也随之更新。如果你想全面掌握 Java&#xff0c;从基础开始到精通&#xff0c;再到最…

【Elasticsearch系列六】系统命令API

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

一起对话式学习-机器学习02——机器学习方法三要素

【一】核方法 首先补充一下核方法&#xff0c;这应是机器学习分类中的内容。 什么是核方法呢&#xff1f;听起来很高级&#xff0c;但理解很简单&#xff1a; 官方定义&#xff1a;核方法是使用核函数表示和学习非线性模型的一种机器学习方法&#xff0c;可以用于监督学习和非监…

初学Linux(学习笔记)

初学Linux&#xff08;学习笔记&#xff09; 前言 本文跳过了Linux前期的环境准备&#xff0c;直接从知识点和指令开始。 知识点&#xff1a; 1.目录文件夹&#xff08;Windows&#xff09; 2.文件内容属性 3.在Windows当中区分文件类型是通过后缀&#xff0c;而Linux是通过…

C++ ——string的模拟实现

目录 前言 浅记 1. reserve&#xff08;扩容&#xff09; 2. push_back&#xff08;尾插&#xff09; 3. iterator&#xff08;迭代器&#xff09; 4. append&#xff08;尾插一个字符串&#xff09; 5. insert 5.1 按pos位插入一个字符 5.2 按pos位插入一个字符串 …

JVM面试真题总结(九)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 描述CMS垃圾收集的工作过程 CMS&#xff08;Concurrent Mark Swee…

Eclipse 数据空间组件(EDC)项目介绍

探索数据共享新时代——Eclipse 数据空间组件&#xff08;EDC&#xff09;项目介绍 在当今数字经济的背景下&#xff0c;数据成为了新的“石油”。但与石油不同的是&#xff0c;数据可以无限共享且再生。然而&#xff0c;如何在保持数据主权的前提下实现高效、安全的数据共享&…

Redis 字典的哈希函数和 rehash 操作详解

Redis 字典的哈希函数和 rehash 操作详解 在 Redis 中,字典(Hash Table)是一种重要的数据结构,用于存储键值对。下面解释 Redis 字典的哈希函数和 rehash 操作。 一、哈希函数 作用 Redis 的字典使用哈希函数将键转换为一个整数索引,这个索引用于确定键值对在哈希表中的…