【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 引⼊

    ...

如何将库暴露出去?

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

相关推荐
HUMHSX22 分钟前
Vue 项目启动全流程解析:从入口文件到全局指令注册与页面渲染
前端·javascript·vue.js
有颜有货34 分钟前
PMC生产排产的4种算法,一次讲清
java·服务器·前端
小虎牙00736 分钟前
Android kotlin图片库Coil源码详解
android·前端
随风一样自由1 小时前
【前端领域】前端开发核心应用场景与落地实践
前端·前端框架
an317421 小时前
弹窗数据流设计的两种高阶架构实践
前端·vue.js·架构
谢尔登1 小时前
【React】 状态管理方案
前端·react.js·前端框架
用户2136610035722 小时前
Vue商品详情与放大镜组件
前端·javascript
半个落月2 小时前
从Tapas小Demo理清localStorage、事件与this
前端·javascript
李明卫杭州2 小时前
Vue2 中 v-model 处理不同数据结构的技巧
前端·javascript·vue.js
李明卫杭州2 小时前
使用 computed 处理 v-model 复杂数据结构
前端·javascript·vue.js