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

文章目录

自动清理构建目录产物

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 cache
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    });

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }
  return __webpack_require__(0);
})([
  /* 0 */
  function (module, __webpack_exports__, __webpack_require__) {
    // 省略代码
  },
  /* 1 */
  function (module, __webpack_exports__, __webpack_require__) {
    // 省略代码
  },
  /* 2 */
  function (module, __webpack_exports__, __webpack_require__) {
    // 省略代码
  }
  /******/
]);

上述代码分析:

  • 打包出来的是⼀个 IIFE (匿名闭包)
  • modules 是⼀个数组,每⼀项是⼀个模块初始化函数
  • __webpack_require ⽤来加载模块,返回 module.exports
  • 通过 webpack_require(0) 启动程序

scope hoisting 原理

原理:将所有模块的代码按照引⽤顺序放在⼀个函数作⽤域⾥,然后适当的重命名⼀

些变量以防⽌变量名冲突。

优点:通过 scope hoisting 可以减少函数声明代码和内存开销。

优化前代码:

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helloworld", function() { return helloworld; });
function helloworld() {
  return 'Hello webpack';
}

/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "common", function() { return common; });
function common() {
  return "common module";
}

/***/ })

优化后代码:

(function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    // ESM COMPAT FLAG
    __webpack_require__.r(__webpack_exports__);
    
    // CONCATENATED MODULE: ./src/index/helloworld.js
    function helloworld() {
      return 'Hello webpack';
    }
    // CONCATENATED MODULE: ./common/index.js
    function common() {
      return "common module";
    }
    // CONCATENATED MODULE: ./src/index/index.js
    
    
    document.write(helloworld());

})

scope hoisting 使⽤

webpack mode 为 production 默认开启 ,必须是 ES6 语法,CJS 不⽀持。

由于mode为production来验证的话,默认会被压缩,我们可以设置为none,然后添加ModuleConcatenationPlugin来验证,示例代码:

const webpack = require("webpack");

module.exports = {
  // 其他代码省略
  mode: "none",
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

注:webpack4及以上mode为production的时候,默认内置了ModuleConcatenationPlugin

代码分割和动态import

代码分割的意义

对于⼤的 Web 应⽤来讲,将所有的代码都放在⼀个⽂件中显然是不够有效的,特别是当你的

某些代码块是在某些特殊的时候才会被使⽤到。webpack 有⼀个功能就是将你的代码库分割成

chunks(语块),当代码运⾏到需要它们的时候再进⾏加载。

适⽤的场景:

  • 抽离相同代码到⼀个共享块
  • 脚本懒加载,使得初始下载的代码更⼩

懒加载 JS 脚本的⽅式

  • CommonJS:require.ensure
  • ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换)

如何使⽤动态 import?

安装 babel 插件

npm i @babel/plugin-syntax-dynamic-import -D

ES6:动态 import(⽬前还没有原⽣⽀持,需要 babel 转换),在babelrc中添加:

{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}

代码分割的效果如图所示:

上面编译的圈红的,如果是动态加载的,那会生成一个以[number]_[chunkhash].js生成的文件名

以React示例代码,其中2_139fa159.js编译前(text.js)代码为:

import React from "react";

export default () => <div>动态 import</div>;

编译后的源码:

(window.webpackJsonp = window.webpackJsonp || []).push([
  [2],
  {
    12: function (n, e, t) {
      "use strict";
      t.r(e);
      var i = t(0),
        o = t.n(i);
      e.default = function () {
        return o.a.createElement("div", null, "动态 import");
      };
    }
  }
]);

入口文件示例代码:

import React from 'react';
import { createRoot } from 'react-dom/client';
import logo from './images/logo.png';
import './search.less';

class Search extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = {
      Text: null,
    };
  }

  loadComponent() {
    import('./text').then((Text) => {
      this.setState({
        Text: Text.default,
      });
    });
  }

  render() {
    const { Text } = this.state;
    return (
      <div className="search-text">
        {Text ? <Text /> : null}
        搜索文字的内容
        <img src={logo} alt="logo" onClick={this.loadComponent.bind(this)} />
      </div>
    );
  }
}

createRoot(document.getElementById('root')).render(<Search />);

在webpack中使用ESLint

行内优秀的eslint规范

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

第二种方式使用eslint-webpack-plugin替换eslint-loader

eslint-webpack-plugin 3.0 which works only with webpack 5. For the webpack 4, see the 2.x branch.

npm i eslint-webpack-plugin@2 -D

eslint-webpack-plugin详细参考地址:https://github.com/webpack-contrib/eslint-webpack-plugin

配置代码:

const ESLintPlugin = require("eslint-webpack-plugin");

module.exports = {
  mode: "production",
  plugins: [
    new ESLintPlugin({
      fix: true, // 启用ESLint自动修复功能
      extensions: ["js", "jsx"],
      context: path.join(__dirname, "src"), // 文件根目录
      exclude: ["/node_modules/"], // 指定要排除的文件/目录
      cache: true // 缓存
    })
  ]
};

制定团队的 ESLint 规范

  • 不重复造轮⼦,基于 eslint:recommend 配置并改进
  • 能够帮助发现代码错误的规则,全部开启
  • 帮助保持团队的代码⻛格统⼀,⽽不是限制开发体验

常用规则参考:https://eslint.nodejs.cn/docs/latest/rules/

ESLint 如何执⾏落地?

  • CI/CD 系统集成
  • webpack 集成
⽅案⼀:webpack 与 CI/CD 集成

本地开发阶段增加 precommit 钩⼦

安装 husky

npm install husky --save-dev

增加 npm script,通过 lint-staged 增量检查修改的⽂件

"scripts": {
    "precommit": "lint-staged"
},
"lint-staged": {
    "linters": {
        "*.{js,scss}": ["eslint --fix", "git add"]
    }
},
⽅案⼆:webpack 与 ESLint 集成

使⽤ eslint-loader或者eslint-webpack-plugin插件,构建时检查 JS 规范。

eslint-loader方式:

rules: [
  {
    test: /.js$/,
    use: [
      "babel-loader",
      "eslint-loader"
    ]
  }
]

eslint-webpack-plugin方式:

plugins: [
    new ESLintPlugin({
      fix: true, // 启用ESLint自动修复功能
      extensions: ["js", "jsx"],
      context: path.join(__dirname, "src"), // 文件根目录
      exclude: ["/node_modules/"], // 指定要排除的文件/目录
      cache: true // 缓存
    })
  ]

webpack打包组件和基础库

webpack 除了可以⽤来打包应⽤,也可以⽤来打包 js 库

示例:实现⼀个⼤整数加法库的打包

  • 需要打包压缩版和⾮压缩版本
  • ⽀持 AMD/CJS/ESM 模块引⼊

⽀持的使⽤⽅式

  • ⽀持 ES module

    import * as largeNumber from 'large-number';
    // ...
    largeNumber.add('999', '1');

  • ⽀持 CJS

    const largeNumbers = require('large-number');
    // ...
    largeNumber.add('999', '1');

  • ⽀持 AMD

    require(['large-number'], function (large-number) {
    // ...
    largeNumber.add('999', '1');
    });

  • 支持script 引⼊

    <!doctype html>

    <html> ... <script src="./large-number.min.js"></script> <script> // Global variable largeNumber.add('999', '1'); // Property in the window object window. largeNumber.add('999', '1'); </script> </html>

如何将库暴露出去?

  • library: 指定库的全局变量

  • libraryTarget: ⽀持库引⼊的⽅式

    module.exports = {
    mode: "production",
    entry: {
    "large-number": "./src/index.js",
    "large-number.min": "./src/index.js"
    },
    output: {
    filename: "[name].js",
    library: "largeNumber",
    libraryExport: "default",
    libraryTarget: "umd"
    }
    };

使用TerSerPlugin插件对 .min 压缩

通过 include 设置只压缩 min.js 结尾的⽂件,webpack4需要安装terser-webpack-plugin@4版本

npm i terser-webpack-plugin@4 -D

const TerSerPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'none',
  entry: {
    'large-number': './src/index.js',
    'large-number.min': './src/index.js'
  },
  output: {
    filename: '[name].js',
    library: 'largeNumber',
    libraryTarget: 'umd',
    libraryExport: 'default'
  },
  optimization: {
    minimize: true,
    minimizer: [
      new TerSerPlugin({
        include: /\.min\.js$/
      }),
    ]
  }
}

根据环境设置⼊⼝⽂件

在工程目录下新建index.js,并且把package.json 的 main 字段为设置为 index.js,其index.js如下:

if (process.env.NODE_ENV === "production") {
    module.exports = require("./dist/large-number.min.js");
} else {
    module.exports = require("./dist/large-number.js");
}

在package.json中添加命令:

 "scripts": {
    "prepublish": "webpack"
  },

最后通过npm publish发布到npm上。

大整数加法代码:

export default function add(a, b) {
  let i = a.length - 1
  let j = b.length - 1

  let carry = 0
  let ret = ''
  while (i >= 0 || j >= 0) {
    let x = 0
    let y = 0
    let sum

    if (i >= 0) {
      x = a[i] - '0'
      i--
    }

    if (j >= 0) {
      y = b[j] - '0'
      j--
    }

    sum = x + y + carry

    if (sum >= 10) {
      carry = 1
      sum -= 10
    } else {
      carry = 0
    }
    // 0 + ''
    ret = sum + ret
  }

  if (carry) {
    ret = carry + ret
  }

  return ret
}

// add('999', '1');

webpack实现SSR打包

⻚⾯打开过程

  • 开始加载
  • HTML加载成功,开始加载数据
  • 数据加载成功,渲染成功开始,加载图⽚资源
  • 图⽚加载成功,⻚⾯可交互

服务端渲染 (SSR) 是什么?

渲染: HTML + CSS + JS + Data -> 渲染后的 HTML

服务端:

  • 所有模板等资源都存储在服务端
  • 内⽹机器拉取数据更快
  • ⼀个 HTML 返回所有数据

浏览器和服务器交互流程

客户端渲染 vs 服务端渲染

总结:服务端渲染 (SSR) 的核⼼是减少请求

SSR 的优势:

  • 减少⽩屏时间
  • 对于 SEO 友好

SSR 代码实现思路

服务端

  • 使⽤ react-dom/server 的 renderToString ⽅法将React 组件渲染成字符串
  • 服务端路由返回对应的模板

安装express:

npm i express -D

服务端的代码示例server/index.js:

if (typeof window === "undefined") {
  global.window = {};
}

const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("../dist/search-server");

function server(port) {
  const app = express();
  app.use(express.static("dist"));

  app.get("/search", (req, res) => {
    const html = renderMarkup(renderToString(SSR));
    res.status(200).send(html);
  });

  app.listen(port, () => {
    console.log("server is running on port:" + port);
  });
}

server(process.env.PORT || 3000);

function renderMarkup(html) {
  return `
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <div id="root">${html}</div>
  </body>
  </html>
  `;
}

客户端

  • 打包出针对服务端的组件

客户端组件代码示例:

const React = require("react");
const logo = require("./images/logo.png");
require("./search.less");

class Search extends React.Component {
  constructor(...args) {
    super(...args);
    this.state = {
      Text: null
    };
  }

  loadComponent() {
    import("./text").then((Text) => {
      this.setState({
        Text: Text.default
      });
    });
  }

  render() {
    const { Text } = this.state;
    return (
      <div className="search-text">
        {Text ? <Text /> : null}
        搜索文字的内容
        <img src={logo} alt="logo" onClick={this.loadComponent.bind(this)} />
      </div>
    );
  }
}

module.exports = <Search />;

客户端编写webpack.ssr.js:

"use strict";

const path = require("path");
const webpack = require("webpack");
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");


const setMPA = () => {
  const entry = {};
  const htmlWebpackPlugins = [];
  const entryFiles = glob.sync(path.join(__dirname, "./src/*/index-server.js"));

  Object.keys(entryFiles).map((index) => {
    const entryFile = entryFiles[index];
    const match = entryFile.match(/src\/(.*)\/index-server\.js/);
    const pageName = match && match[1];
    if (pageName) {
      entry[pageName] = entryFile;
      htmlWebpackPlugins.push(
        new HtmlWebpackPlugin({
          template: path.join(__dirname, `src/${pageName}/index.html`),
          filename: `${pageName}.html`,
          chunks: [pageName],
          inject: true,
          minify: {
            html5: true,
            collapseWhitespace: true,
            preserveLineBreaks: false,
            minifyCSS: true,
            minifyJS: true,
            removeComments: false
          }
        })
      );
    }
  });

  return {
    entry,
    htmlWebpackPlugins
  };
};

const { entry, htmlWebpackPlugins } = setMPA();

module.exports = {
  entry: entry,
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name]-server.js",
    libraryTarget: "umd"
  },
  mode: "production",
  module: {
    rules: [
      {
        test: /.js$/,
        use: ["babel-loader"]
      },
      {
        test: /.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"]
      },
      {
        test: /.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "less-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  [
                    "autoprefixer",
                    {
                      overrideBrowserslist: ["last 2 version", ">1%", "ios 7"]
                    }
                  ]
                ]
              }
            }
          },
          {
            loader: "px2rem-loader",
            options: {
              remUnit: 75,
              remPrecision: 8
            }
          }
        ]
      },
      {
        test: /.(png|jpe?g|gif)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              esModule: false,
              name: "[name]_[hash:8].[ext]"
            }
          }
        ]
      },
      {
        test: /.(woff|woff2|eot|otf|ttf)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              esModule: false,
              name: "[name]_[hash:8].[ext]"
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name]_[contenthash:8].css"
    }),
    new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require("cssnano")
    }),
    new CleanWebpackPlugin()
  ].concat(htmlWebpackPlugins),
  devtool: "none"
};

配置package.json命令:

"scripts": {
    "build:ssr": "webpack --config webpack.ssr.js"
}

我们通过npm run build:ssr编译组件,通过node server/inde.js启动后台服务,启动成功后,我们可以通过http://localhost:3000/search访问。

webpack ssr 打包存在的问题

浏览器的全局变量 (Node.js 中没有 document, window)

  • 组件适配:将不兼容的组件根据打包环境进⾏适配
  • 请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios

样式问题 (Node.js ⽆法解析 css)

  • ⽅案⼀:服务端打包通过 ignore-loader 忽略掉 CSS 的解析
  • ⽅案⼆:将 style-loader 替换成 isomorphic-style-loader

如何解决样式不显示的问题?

使⽤打包出来的浏览器端 html 为模板,设置占位符,动态插⼊组件。

如图所示:

首先在客户端html模板中添加占位符,如下:

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Document</title>
</head>

<body>
  <div id="root"><!--HTML_PLACEHOLDER--></div>
</body>

</html>

然后服务端的server/index.js调整为:

if (typeof window === "undefined") {
  global.window = {};
}

const fs = require("fs");
const path = require("path");
const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("../dist/search-server");
const htmlTemplate = fs.readFileSync(path.join(__dirname, "../dist/search.html"), "utf-8");

function server(port) {
  const app = express();
  app.use(express.static("dist"));

  app.get("/search", (req, res) => {
    const html = renderMarkup(renderToString(SSR));
    res.status(200).send(html);
  });

  app.listen(port, () => {
    console.log("server is running on port:" + port);
  });
}

server(process.env.PORT || 3000);

function renderMarkup(str) {
  return htmlTemplate.replace("<!--HTML_PLACEHOLDER--", str);
}

客户端重新npm run build:ssr,然后再通过http://localhost:3000/search访问。

⾸屏数据如何处理?

在客户端html模板页面添加占位符,服务端获取数据,替换占位符。

客户端html模板:

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Document</title>
</head>

<body>
  <div id="root"><!--HTML_PLACEHOLDER--></div>
  <!--INITIAL_DATA_PLACEHOLDER-->
</body>

</html>

服务端server/index.js代码调整:

if (typeof window === "undefined") {
  global.window = {};
}

const fs = require("fs");
const path = require("path");
const express = require("express");
const { renderToString } = require("react-dom/server");
const SSR = require("../dist/search-server");
const htmlTemplate = fs.readFileSync(path.join(__dirname, "../dist/search.html"), "utf-8");
const mockData = require("./data.json");

function server(port) {
  const app = express();
  app.use(express.static("dist"));

  app.get("/search", (req, res) => {
    const html = renderMarkup(renderToString(SSR));
    res.status(200).send(html);
  });

  app.listen(port, () => {
    console.log("server is running on port:" + port);
  });
}

server(process.env.PORT || 3000);

function renderMarkup(str) {
  const dataStr = JSON.stringify(mockData);
  return htmlTemplate.replace("<!--HTML_PLACEHOLDER--", str).replace("<!--INITIAL_DATA_PLACEHOLDER-->", `<script src="text/javascript">window.__initial_data = ${dataStr}</script>`);
}

最后运行页面源码如图所示:

优化构建时命令行的显示日志

webpack构建统计信息 stats

如何优化命令⾏的构建⽇志

1、使⽤ friendly-errors-webpack-plugin

  • success: 构建成功的⽇志提示
  • warning: 构建警告的⽇志提示
  • error: 构建报错的⽇志提示

2、stats 设置成 errors-only

安装friendly-errors-webpack-plugin:

npm i friendly-errors-webpack-plugin -D

开发配置webpack.dev.js:

const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");

module.exports = {
  plugins: [
    new FriendlyErrorsWebpackPlugin()
  ],
  devServer: {
    contentBase: "./dist",
    hot: true,
    stats: "errors-only"
  }
};

生产配置webpack.prod.js:

const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");

module.exports = {
  plugins: [
    new FriendlyErrorsWebpackPlugin()
  ],
  stats: "errors-only"
};

构建异常和中断处理

如何判断构建是否成功?

  • 在 CI/CD 的 pipline 或者发布系统需要知道当前构建状态
  • 每次构建完成后输⼊ echo $? 获取错误码

webpack4 之前的版本构建失败不会抛出错误码 (error code)

Node.js 中的 process.exit 规范

  • 0 表示成功完成,回调函数中,err 为 null
  • ⾮ 0 表示执⾏失败,回调函数中,err 不为 null,err.code 就是传给 exit 的数字

如何主动捕获并处理构建错误?

  • compiler 在每次构建结束后会触发 done 这个 hook
  • process.exit 主动处理构建报错

在配置中可以添加如下代码,进行中断处理,例如错误上报等。

module.exports = {
  plugins: [
    function () {
      this.hooks.done.tap("done", (stats) => {
        if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf("--watch") == -1) {
          console.log("build error");
          process.exit(1); // 1表示错误码并退出
        }
      });
    }
  ]
};

结果示例:

上面的build errorerrno 1就是上面代码配置的中断处理逻辑。

相关推荐
dr李四维14 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~35 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ38 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z44 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4041 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
临枫5411 小时前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript