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

news2024/11/15 3:45:27

文章目录

    • 自动清理构建目录产物
    • PostCSS插件autoprefixer自动补齐CSS3前缀
    • 移动端CSS px自动转换成rem
    • 静态资源内联
    • 多页面应用打包通用方案
    • 使用sourcemap
    • 提取页面公共资源
      • 基础库分离
      • 利⽤ SplitChunksPlugin 进⾏公共脚本分离
      • 利⽤ SplitChunksPlugin 分离基础包
      • 利⽤ SplitChunksPlugin 分离⻚⾯公共⽂件
    • Tree Shaking(摇树优化)的使用和原理分析
      • 基础介绍
      • DCE (Dead code elimination)
      • Tree-shaking 原理
    • Scope Hoisting使用和原理分析
      • 背景:构建后的代码存在⼤量闭包代码
      • 模块转换分析
      • 进⼀步分析 webpack 的模块机制
      • scope hoisting 原理
      • scope hoisting 使⽤
    • 代码分割和动态import
      • 代码分割的意义
      • 懒加载 JS 脚本的⽅式
      • 如何使⽤动态 import?
    • 在webpack中使用ESLint
      • 行内优秀的eslint规范
      • 制定团队的 ESLint 规范
      • ESLint 如何执⾏落地?
        • ⽅案⼀:webpack 与 CI/CD 集成
        • ⽅案⼆:webpack 与 ESLint 集成
    • webpack打包组件和基础库
      • ⽀持的使⽤⽅式
      • 如何将库暴露出去?
      • 使用TerSerPlugin插件对 .min 压缩
      • 根据环境设置⼊⼝⽂件
    • webpack实现SSR打包
      • ⻚⾯打开过程
      • 服务端渲染 (SSR) 是什么?
      • 浏览器和服务器交互流程
      • 客户端渲染 vs 服务端渲染
      • SSR 代码实现思路
      • webpack ssr 打包存在的问题
      • 如何解决样式不显示的问题?
      • ⾸屏数据如何处理?
    • 优化构建时命令行的显示日志
      • webpack构建统计信息 stats
      • 如何优化命令⾏的构建⽇志
    • 构建异常和中断处理

自动清理构建目录产物

webpack4.x使用clean-webpack-plugin@3版本:

npm i clean-webpack-plugin@3 -D

webpack配置:

const { CleanWebpackPlugin }  = require('clean-webpack-plugin')

plugins: [
    new CleanWebpackPlugin(),
 ]

PostCSS插件autoprefixer自动补齐CSS3前缀

需要安装postcss-loaderpostcssautoprefixer插件。

其中webpack4.x需要安装postcss-loader@4。

npm i postcss-loader@4 postcss@8 autoprefixer -D

配置如下:

module.exports = {
  module: {
    rules: [
     
      {
        test: /.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                plugins: [
                  [
                    'autoprefixer',
                    {
                      overrideBrowserslist: ['last 2 version', '>1%', 'ios 7']
                    }
                  ]
                ]
              }
            }
          }
        ]
      }
    ]
  }
}

移动端CSS px自动转换成rem

使用px2rem-loader,⻚⾯渲染时计算根元素的 font-size 值,可以使⽤⼿淘的lib-flexible库,地址:https://github.com/amfe/lib-flexible

npm i px2rem-loader -D
npm i lib-flexible -S

配置:

module.exports = {
  module: {
    rules: [
      {
        test: /.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
          {
            loader: 'px2rem-loader',
            options: {
              remUnit: 75,
              remPrecision: 8
            }
          }
        ]
      }
    ]
  }
}

如果需要验证 把安装的lib-flexible源码先拷贝到html头部head里面,代码如下:

<script type="text/javascript">
    
;(function(win, lib) {
    var doc = win.document;
    var docEl = doc.documentElement;
    var metaEl = doc.querySelector('meta[name="viewport"]');
    var flexibleEl = doc.querySelector('meta[name="flexible"]');
    var dpr = 0;
    var scale = 0;
    var tid;
    var flexible = lib.flexible || (lib.flexible = {});

    if (metaEl) {
        console.warn('将根据已有的meta标签来设置缩放比例');
        var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
        if (match) {
            scale = parseFloat(match[1]);
            dpr = parseInt(1 / scale);
        }
    } else if (flexibleEl) {
        var content = flexibleEl.getAttribute('content');
        if (content) {
            var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
            var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
            if (initialDpr) {
                dpr = parseFloat(initialDpr[1]);
                scale = parseFloat((1 / dpr).toFixed(2));
            }
            if (maximumDpr) {
                dpr = parseFloat(maximumDpr[1]);
                scale = parseFloat((1 / dpr).toFixed(2));
            }
        }
    }

    if (!dpr && !scale) {
        var isAndroid = win.navigator.appVersion.match(/android/gi);
        var isIPhone = win.navigator.appVersion.match(/iphone/gi);
        var devicePixelRatio = win.devicePixelRatio;
        if (isIPhone) {
            // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
            if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
                dpr = 3;
            } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
                dpr = 2;
            } else {
                dpr = 1;
            }
        } else {
            // 其他设备下,仍旧使用1倍的方案
            dpr = 1;
        }
        scale = 1 / dpr;
    }

    docEl.setAttribute('data-dpr', dpr);
    if (!metaEl) {
        metaEl = doc.createElement('meta');
        metaEl.setAttribute('name', 'viewport');
        metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
        if (docEl.firstElementChild) {
            docEl.firstElementChild.appendChild(metaEl);
        } else {
            var wrap = doc.createElement('div');
            wrap.appendChild(metaEl);
            doc.write(wrap.innerHTML);
        }
    }

    function refreshRem(){
        var width = docEl.getBoundingClientRect().width;
        if (width / dpr > 540) {
            width = 540 * dpr;
        }
        var rem = width / 10;
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }

    win.addEventListener('resize', function() {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
    }, false);
    win.addEventListener('pageshow', function(e) {
        if (e.persisted) {
            clearTimeout(tid);
            tid = setTimeout(refreshRem, 300);
        }
    }, false);

    if (doc.readyState === 'complete') {
        doc.body.style.fontSize = 12 * dpr + 'px';
    } else {
        doc.addEventListener('DOMContentLoaded', function(e) {
            doc.body.style.fontSize = 12 * dpr + 'px';
        }, false);
    }


    refreshRem();

    flexible.dpr = win.dpr = dpr;
    flexible.refreshRem = refreshRem;
    flexible.rem2px = function(d) {
        var val = parseFloat(d) * this.rem;
        if (typeof d === 'string' && d.match(/rem$/)) {
            val += 'px';
        }
        return val;
    }
    flexible.px2rem = function(d) {
        var val = parseFloat(d) / this.rem;
        if (typeof d === 'string' && d.match(/px$/)) {
            val += 'rem';
        }
        return val;
    }

})(window, window['lib'] || (window['lib'] = {}));
</script>

静态资源内联

资源内联的意义:

  • 代码层⾯:
    • ⻚⾯框架的初始化脚本
    • 上报相关打点
    • css 内联避免⻚⾯闪动
  • 请求层⾯:减少 HTTP ⽹络请求数
    • ⼩图⽚或者字体内联 (url-loader)

具体实现:

安装raw-loader@0.5.1版本

npm i raw-loader@0.5.1 -D
  • raw-loader 内联 html
<%= require('raw-loader!./meta.html') %>
  • raw-loader 内联 JS
<%= require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js') %>

示例,例如我们抽离meta通用的代码为一个meta.html,以及flexible.js插件都内联带html页面中。
meta.html示例代码:

<meta charset="UTF-8">
<meta name="viewport" content="viewport-fit=cover,width=device-width,initial-scale=1,user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta name="keywords" content="keywords content">
<meta name="name" itemprop="name" content="name content">
<meta name="apple-mobile-web-app-capable" content="no">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">

模板index.html代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <%= require('raw-loader!./meta.html') %>
  <title>Document</title>
  <script type="text/javascript">
    <%= require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js') %>
  </script>
</head>
<body>
<div id="root"></div>
</body>
</html>

多页面应用打包通用方案

动态获取 entry 和设置 html-webpack-plugin 数量,使用glob插件的glob.sync方法获取所有的entry。

glob.sync(path.join(__dirname, './src/*/index.js')),

安装glob,我们安装版本7的,其他版本对node有要求,并且使用方式有区别:

npm i glob@7 -D

配置:

const glob = require("glob");

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

  Object.keys(entryFiles).map((index) => {
    const entryFile = entryFiles[index];
    const match = entryFile.match(/src\/(.*)\/index\.js/);
    console.log(match);
    const pageName = match && match[1];

    entry[pageName] = entryFile;
    htmlWebpackPlugins.push(
      new HtmlWebpackPlugin({
        template: path.join(__dirname, `src/${pageName}/index.html`),
        filename: `${pageName}.html`,
        chunks: [pageName],
        inject: true,
        minify: {
          html5: true,
          collapseWhitespace: true,
          preserveLineBreaks: false,
          minifyCSS: true,
          minifyJS: true,
          removeComments: false
        }
      })
    );
  });

  return {
    entry,
    htmlWebpackPlugins
  };
};

const { entry, htmlWebpackPlugins } = setMPA();

module.exports = {
  entry: entry,
  output: {
    path: path.join(__dirname, "dist"),
    filename: "[name]_[chunkhash:8].js"
  },
  mode: "production",
  plugins: [
   // 省略其他插件
  ].concat(htmlWebpackPlugins)
};

使用sourcemap

作⽤:通过source map定位到源代码

sourcemap参考文章:http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html

source map 关键字:

  • eval: 使⽤eval包裹模块代码
  • source map: 产⽣.map⽂件
  • cheap: 不包含列信息
  • inline: 将.map作为DataURI嵌⼊,不单独⽣成.map⽂件
  • module:包含loader的sourcemap

source map 类型:
在这里插入图片描述

一般开发环境配置:

module.exports = {
  // 其他代码省略
  devtool: "source-map"
};

生产环境配置:

module.exports = {
  // 其他代码省略
  devtool: "none"
};

提取页面公共资源

基础库分离

将 react、react-dom、vue、Jquery等基础包通过 cdn 引⼊,不打⼊ bundle 中。

使⽤ html-webpack-externals-plugin

npm i html-webpack-externals-plugin -D

html-webpack-externals-plugin插件参考地址:https://www.npmjs.com/package/html-webpack-externals-plugin。

示例:

new HtmlWebpackExternalsPlugin({
  externals: [
    {
      module: "react",
      entry: "https://unpkg.com/react@18.2.0/umd/react.production.min.js",
      global: "React"
    },
    {
      module: "react-dom",
      entry: "https://unpkg.com/react-dom@18/umd/react-dom.production.min.js",
      global: "ReactDOM"
    }
  ]
})

利⽤ SplitChunksPlugin 进⾏公共脚本分离

Webpack4 内置splitChunks的,替代CommonsChunkPlugin插件。

chunks 参数说明:

  • async 分离异步加载的模块(默认)。
  • initial 同步引⼊的库进⾏分离。
  • all 所有引⼊的库进⾏分离(推荐)。

示例代码:

optimization: {
    splitChunks: {
      chunks: "async",
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: "~",
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        }
      }
    }
  }

利⽤ SplitChunksPlugin 分离基础包

例如要抽离除react、react-dom,配置代码如下:

optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /(react|react-dom)/,
          name: "vendors",
          chunks: "all"
        }
      }
    }
}

其中test属性标识匹配出需要分离的包。

抽离的基础文件要被模板文件引用,需要在html-webpack-plugin插件中配置chunks,示例代码:

new HtmlWebpackPlugin({
    template: path.join(__dirname, `src/${pageName}/index.html`),
    filename: `${pageName}.html`,
    chunks: ["vendors", pageName],
    inject: true,
    minify: {
      html5: true,
      collapseWhitespace: true,
      preserveLineBreaks: false,
      minifyCSS: true,
      minifyJS: true,
      removeComments: false
    }
  })

利⽤ SplitChunksPlugin 分离⻚⾯公共⽂件

  • minChunks: 设置最⼩引⽤次数为2次
  • minuSize: 分离的包体积的⼤⼩
optimization: {
    splitChunks: {
      minSize: 0,
      cacheGroups: {
        commons: {
          name: "commons",
          chunks: "all",
          minChunks: 2
        }
      }
    }
 }

Tree Shaking(摇树优化)的使用和原理分析

基础介绍

一个模块可能有多个⽅法,只要其中的某个⽅法使⽤到了,则整个⽂件都会被打到
bundle ⾥⾯去,tree shaking 就是只把⽤到的⽅法打⼊ bundle ,没⽤到的⽅法会在
uglify 阶段被擦除掉。

uglify阶段:将 JavaScript代码进行压缩、混淆,并去除一些不必要的代码,从而减小文件体积。

webpack4及以上默认内置了,当mode为production情况下默认开启。进行tree shaking条件是必须是 ES6 的语法,CJS 的⽅式不⽀持

DCE (Dead code elimination)

DCE 解释就是死代码消除的意思。

  • 代码不会被执⾏,不可到达
  • 代码执⾏的结果不会被⽤到
  • 代码只会影响死变量(只写不读)

示例:

if (false) {
    console.log('这段代码永远不会执行’);
}

如上所示代码,在uglify 阶段就会删除⽆⽤代码。

Tree-shaking 原理

利⽤ ES6 模块的特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable的(import引用的模块是不能修改的)

注:使用mode为production与none 来验证tree-shaking。

Scope Hoisting使用和原理分析

背景:构建后的代码存在⼤量闭包代码

如图所示:
在这里插入图片描述

这样会导致什么问题?

  • ⼤量作⽤域包裹代码,导致体积增⼤(模块越多越明显)
  • 运⾏代码时创建的函数作⽤域变多,内存开销变⼤

模块转换分析

示例:我们编写了一个模块,代码如下

import { helloworld } from "./helloworld";
import "../../common";

document.write(helloworld());

我们把webpack4中的mode设置为none,看下编译结果,webpack会把编写的模块转换成模块初始化函数,代码如下:

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

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _helloworld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);


document.write(Object(_helloworld__WEBPACK_IMPORTED_MODULE_0__["helloworld"])());

/***/ })

结果说明:

  • 被 webpack 转换后的模块会带上⼀层包裹
  • import 会被转换成 __webpack_require

当然上面两个import导入的模块编译为如下代码:

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

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

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

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

/***/ })

进⼀步分析 webpack 的模块机制

(function (modules) {
  // webpackBootstrap
  // The module 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规范

  • Airbnb: eslint-config-airbnbeslint-config-airbnb-base
  • alloyteam团队 eslint-config-alloy:https://github.com/AlloyTeam/eslint-config-alloy

eslint-config-airbnb:默认导出包含大多数ESLint规则,包括ECMAScript 6+和React。它需要eslint, eslint-plugin-import, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-jsx-a11y。请注意,它不会启用我们的React Hooks规则。

当然如果不需要React,那么可以参考使用eslint-config-airbnb-base

以使用eslint-config-airbnb为例:

npm i eslint-config-airbnb eslint@7 eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y -D

注意:elsint要安装7.x版本。

再安装babel-eslinteslint-loader

npm i babel-eslint eslint-loader -D

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

根目录下新建.eslintrc.js

module.exports = {
    "parser": "babel-eslint",
    "extends": "airbnb",
    "env": {
        "browser": true,
        "node": true
    },
    "rules": {
        "indent": ["error", 4]
    }
};

说明:

  • parser: 配置解析器
  • extends:扩展配置文件
    extends 属性中使用 "eslint:recommended" 可以启用报告常见问题的核心规则子集(这些规则在 规则页 上用复选标记(推荐)标识)。
module.exports = {
    "extends": "eslint:recommended",
};

详细参考:https://eslint.nodejs.cn/docs/latest/use/configure/configuration-files

  • env:配置文件中指定环境(browser - 浏览器全局变量;node - Node.js 全局变量和 Node.js 作用域)
    详细参考地址:https://eslint.nodejs.cn/docs/latest/use/configure/language-options#specifying-environments
  • rules:配置规则
"off" 或 0 - 关闭规则
"warn" 或 1 - 打开规则作为警告(不影响退出代码)
"error" 或 2 - 打开规则作为错误(触发时退出代码为 1)

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

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

npm i eslint-webpack-plugin@2 -D

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

配置代码:

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

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

制定团队的 ESLint 规范

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

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

ESLint 如何执⾏落地?

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

在这里插入图片描述

本地开发阶段增加 precommit 钩⼦

安装 husky

npm install husky --save-dev

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

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

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

eslint-loader方式:

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

eslint-webpack-plugin方式:

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

webpack打包组件和基础库

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

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

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

⽀持的使⽤⽅式

  • ⽀持 ES module
import * as largeNumber from 'large-number';
// ...
largeNumber.add('999', '1');
  • ⽀持 CJS
const largeNumbers = require('large-number');
// ...
largeNumber.add('999', '1');
  • ⽀持 AMD
require(['large-number'], function (large-number) {
// ...
largeNumber.add('999', '1');
});
  • 支持script 引⼊
<!doctype html>
<html>
...
<script src="./large-number.min.js"></script>
<script>
    // Global 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就是上面代码配置的中断处理逻辑。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

UVA1395 Slim Span(最小生成树)

*原题链接*(洛谷) 非常水的一道题。看见让求最小边权差值的生成树&#xff0c;很容易想到kruskal。 一个暴力的想法是以每条边为最小边跑一遍kruskal&#xff0c;然后统计答案。时间复杂度&#xff0c;再看题中很小的数据范围和3s的时限。最后还真就过了。 不过我天真的想了…

【机器学习】11——矩阵求导

机器学习11——矩阵求导 打公式不太好标注&#xff0c;全图警告&#xff01;&#xff01;&#xff01; 文章目录 机器学习11——矩阵求导1.1标量对向量1.2标量对矩阵2.1向量对标量2.2向量对向量2.3向量对矩阵 1.1标量对向量 1.2标量对矩阵 X是m*n的矩阵&#xff0c;不严谨&am…

代码随想录Day 44|leetcode题目:1143.最长公共子序列、1035.不相交的线、53. 最大子序和、392.判断子序列

提示&#xff1a;DDU&#xff0c;供自己复习使用。欢迎大家前来讨论~ 文章目录 题目题目一&#xff1a;1143.最长公共子序列解题思路&#xff1a; 题目二&#xff1a; 1035.不相交的线解题思路&#xff1a; 题目三&#xff1a;53. 最大子序和解题思路 题目四&#xff1a;392.判…

【白话树】之 二叉树

快速导航 一、二叉树的基本概念1、 二叉树定义2、常见术语3、基本操作1&#xff09;创建&#xff1a;2&#xff09;插入与删除&#xff1a; 4、常见类型1&#xff09;满二叉树&#xff08;完美二叉树&#xff09;2&#xff09;完全二叉树3&#xff09;完满二叉树4&#xff09;平…

Mysql InnoDB 存储引擎简介

InnoDB 存储引擎是 Mysql 的默认存储引擎&#xff0c;它是由 Innobase Oy 公司开发的 Mysql 为什么默认使用 InnoDB 存储引擎 InnoDB 是一款兼顾高可靠性和高性能的通用存储引擎 在 Mysql 5.5 版本之前&#xff0c;默认是使用 MyISAM 存储引擎&#xff0c;在 5.5 及其之后版…

WEB打点

目录 web打点概述打点流程打点中的问题getshell手法汇总web打点批量检测端口扫描POC扫描指纹识别漏洞扫描 手工检测开源框架漏洞通用框架漏洞基础web漏洞商用系统漏洞 WAF绕过waf分类常见waf拦截界面WAF绕过思路侧面绕&#xff1a;适合云WAF直面WAF web打点概述 打点流程 资产…

远程Linux网络连接( Linux 网络操作系统 04)

接下来我们准备开始进入Linux操作系统的第二个模块的学习&#xff0c;不过在学习之前我们需要对如下进行简单的配置&#xff0c;通过外接辅助软件MobaXterm来进行虚拟操作系统的访问。接下来的课程我们会一直在MobaXterm中进行命令和相关知识的学习。 一、准备阶段 1.1 软件 …

安装配置filebrowser

安装配置filebrowser ​ 这章就简单搞个工具用一下&#xff0c;这个工具就是一个像安卓软件一样的文件浏览器&#xff0c;可以设置用户权限啥的&#xff0c;挺好用的下面直接粘的安装步骤&#xff0c;注意一下配置别错了就行&#xff0c;json文件和命令配置要一样。访问效果放…

逻辑回归原理

本文主要介绍了逻辑回归的原理和应用&#xff0c;包括从线性回归到逻辑回归的转换、二元逻辑回归的模型和损失函数、优化方法以及正则化等内容。以下是内容的详细叙述&#xff1a; 1. 从线性回归到逻辑回归 线性回归模型&#xff1a;线性回归是找出输出特征向量Y与输入样本矩阵…

无线通信感知/雷达系统算法专业技术栈

无论是在工业界还是在学业界&#xff0c;无线通信感知一体化都是一个热门的方向&#xff0c;作为一个24届毕业生&#xff0c;刚好处于行业当中&#xff0c;就总结一下自己浅薄认知下&#xff0c;自己觉得已经掌握或者应该掌握的技术栈和专业能力&#xff0c;与大家共勉。 Rada…

CS61C 2020计算机组成原理Lab01-数字表示,溢出

1. Exercise 1 :See what you can C # 用gcc 来编译可执行文件如program.c gcc program.c # 就可以得到一个executable file named a.out. ./a.out# 如果想给这个可执行文件命名&#xff0c;则使用 -o gcc -o program program.c ./program# 使用-g 能得到一个 可执行程序的deb…

微服务保护之熔断降级

在微服务架构中&#xff0c;服务之间的调用是通过网络进行的&#xff0c;网络的不确定性和依赖服务的不可控性&#xff0c;可能导致某个服务出现异常或性能问题&#xff0c;进而引发整个系统的故障&#xff0c;这被称为 微服务雪崩。为了防止这种情况发生&#xff0c;常用的一些…

K8s 之控制器的定义及详细调用案例

什么是控制器 官方文档&#xff1a; https://v1-30.docs.kubernetes.io/zh-cn/docs/concepts/workloads/controllers/ 控制器也是管理pod的一种手段 自主式pod&#xff1a;pod退出或意外关闭后不会被重新创建控制器管理的 Pod&#xff1a;在控制器的生命周期里&#xff0c;始…

《深度学习》PyTorch 手写数字识别 案例解析及实现 <下>

目录 一、回顾神经网络框架 1、单层神经网络 2、多层神经网络 二、手写数字识别 1、续接上节课代码&#xff0c;如下所示 2、建立神经网络模型 输出结果&#xff1a; 3、设置训练集 4、设置测试集 5、创建损失函数、优化器 参数解析&#xff1a; 1&#xff09;para…

Cesium 计算3d凸包(ConvexHull)

Cesium 计算3d凸包(ConvexHull) Cesium 计算3d凸包(ConvexHull)

Unity实战案例全解析:PVZ 植物放置分析

前篇&#xff1a;Unity实战案例全解析&#xff1a;PVZ 植物卡片状态分析-CSDN博客 植物应该如何从卡牌状态转为实物&#xff1f; 其实就只需要考虑两个步骤加一个后续处理&#xff1a; 1.点击卡牌后就实例化 需要一个植物状态枚举&#xff0c;因为卡牌分为拿在手上和种植下…

Android 10.0 mtk平板camera2横屏预览旋转90度横屏保存圆形预览缩略图旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,在上一篇已经实现了横屏预览功能,然后发现横屏预览后,点击录像和照片下保存的圆形预览缩略图 依然是竖屏的,所以说同样需要…

需求导向的正则表达式

目录 re.sub 需求&#xff1a;把 1. 2.这些序号转成&#xff08;1&#xff09; &#xff08;2&#xff09; 需求&#xff1a;反过来&#xff0c;把(1)->1. ,&#xff08;2&#xff09;》2. 。 需求&#xff1a;把出现的 1 2 3都转成下标 进阶1&#xff01;只想让化学符…

Redis入门2

在java中操作Redis Redis的Java客户端 Redis 的 Java 客户端很多&#xff0c;常用的几种: Jedis Lettuce Spring Data Redis Spring Data Redis 是 Spring 的一部分&#xff0c;对 Redis 底层开发包进行了高度封装。 在 Spring 项目中&#xff0c;可以使用Spring Data R…

把项目部署到Linux系统上(如何在阿里云服务器上安装和配置SpringBoot+vue全栈开发环境)

项目部署上线 环境准备下载安装Linux系统和ssh连接工具背景知识安装虚拟机安装Linux系统选择installCentOS7按命令IP addr查看服务器IP地址&#xff0c;ens33网卡中会出现IP地址配置好后就可以查看了一个可远程连接Linux服务器的工具1.&#xff08;基于finalshell工具&#xff…