阅读 1773

【Webpack5实践】搭建好用React 开发环境(The Last)

前戏

Hi~ o(* ̄▽ ̄*)ブ这是 Webpack5 搭建 React nice的开发环境的最后一篇。由于上一篇主要拿来试水讲的可能比较浅,细节考虑的不是很到位,那么通过这篇来完善一下吧。干货还是可以的看完会有收获,建议边看边喝水。最近也是忙着搞埋点,我也不想咕咕咕(放各位鸽子),所以我周末的时候写了一下。好的,那么开始进入正题,现贴上一篇的文章的 传送门大家可以回顾一下,以及完整的源码

本篇的流程规划

graph TD
开发环境配置完善 --> 生产环境配置完善 --> 优化的思路

1BF85B5D134BA3B5926A9DF4BE61E91B.png

开发配置完善

Babel 转译器和插件

首先先完善一下之前 loader 的配置

下面是原先的处理js、jsx loader配置

{
    test: /\.(js|jsx)$/,
    exclude: /node_modules/,
    use: [
        {
            loader: "babel-loader",
            options: {
                presets: [
                    "@babel/preset-env",
                    "@babel/preset-react",
                ],
            },
        },
    ],
},
复制代码

Babel presets 配置

Babel 应该有知道吧 是一个 JavaScript 编译器 官方的定义,它的作用的是让低版本浏览器使用 ES 上新的语法和新的数据类型, 将高版本的 ES 语法和 API 转换成现有浏览器可以运行的代码, 起转译作用。

安装

babel-loader @babel/core @babel/preset-env
复制代码

这个三个算的上 webpack 中的必不可少的存在

babel-loader

这个包允许使用 BabelWebpack 编译 JavaScript 文件 babel-loader

@babel/core

它是 Babel 核心库,提供了很多转译源文件的 API,它需要插件才能转译本身不会转译

import { transformSync } from "@babel/core";

function babelLoader(source, options) {
    //  var options= {
    //             presets: [
    //                 "@babel/preset-env",
    //                 "@babel/preset-react",
    //             ],
    //         },
    var result = transformSync(source, options);
    return this.callback(null, result.code, result.map, result.ast);
}
module.exports = babelLoader;
复制代码
  • source 需要的转译源文件或者是上一个 loader 转译过的结果
  • options 就是配置 loader 中传的 options 参数
  • transformSync 同步转译传入的代码,返回转转译后代码sourceMap 映射和 AST 对象。
@babel/preset-env

babel/preset-env 是语法转译器也可以叫预设,但是它只转换新的 ES 语法。而不转换新的 ES API,比如 Iterator, Generator, Set, Maps, Proxy, Reflect,Symbol,Promise,而对与这些新的 API 可以通过 babel-profill 转译,让浏览器实现 新 API 的功能 但是 babek-profill 已经不建议使用了建议使用 core-js

⚠️ As of Babel 7.4.0, this package has been deprecated in favor of directly including core-js/stable (to polyfill ECMAScript features) and regenerator-runtime/runtime (needed to use transpiled generator functions):

npm i core-js -S
复制代码

配置如下

{
    test: /\.(js|jsx)$/,
    exclude: /node_modules/,
    use: [
        {
            loader: 'babel-loader',
            options: {
                presets: [['@babel/preset-env', {
                    useBuiltIns: 'entry',
                    corejs: '3.9.1',
                    targets: {
                        chrome: '60',
                    },
                }], '@babel/preset-react'],
            },
        },
    ],
},
复制代码
@babel/preset-env 参数
  • useBuiltIns: "usage"| "entry"| false,默认为 false, 这里讲一讲 usage 其他参数的具体看官方描述传送门

  • usage 会根据配置的浏览器兼容,和只对你用到的 API 来进行 polyfill,实现按需添加补丁

  • targets:

// 对市场份额 >0.25% 做兼容
{
  "targets": "> 0.25%, not dead"
}
// 对要支持的最低环境版本的对象 做兼容
{
  "targets": {
    "chrome": "58",
    "ie": "11"
  }
}
复制代码

当未指定目标时,它的行为类似:preset-env 将所有 ES2015-ES2020 代码转换为与 ES5 兼容。不建议直接使用以下 preset-env 这种方式,因为它没有利用针对特定环境/版本的功能

{
  "presets": ["@babel/preset-env"]
}
复制代码

由于自@babel/polyfill7.4.0 起已弃用,因此建议您 core-js 通过该 corejs 选项直接添加和设置版本

  • corejs: '3.9.1' 这个'3.9.1' 是 core-js 版本号

@babel/preset-react

React 插件的 Babel 预设, JSXReact.createElement()来调用的,主要在转译 react 代码的时候使用。

  • 这是一段 jsx 代码
<div className="wrap" style={{ color: "#272822" }}>
    <span>一起学习</span>React
</div>
复制代码
  • 经过 babel/preset-react 转移器转译成:
React.createElement(
    "div",
    {
        className: "wrap",
        style: {
            color: "#272822",
        },
    },
    React.createElement("span", null, "一起学习"),
    "React"
);
复制代码

babel plugin 配置

  • @babel/plugin-syntax-dynamic-import 支持动态加载 import,@babel/preset-env 不支持动态 import 语法转译。

Currently, @babel/preset-env is unaware that using import() with Webpack relies on Promise internally. Environments which do not have builtin support for Promise, like Internet Explorer, will require both the promise and iterator polyfills be added manually.

  • @babel/plugin-proposal-decorators 把类和对象的装饰器编译成 ES5 代码
  • @babel/plugin-proposal-class-properties 转换静态类属性以及使用属性初始值化语法声明的属性

配置转译所需要的插件。使用插件的顺序是按照插件在数组中的顺序依次调用的

现在 babel-loader 参数比较臃肿可以提到 .babelrc.js 文件中

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                useBuiltIns: "entry",
                corejs: "3.9.1",
                targets: {
                    chrome: "58",
                    ie: "11",
                },
            },
        ],
        [
            "@babel/preset-react",
            {
                development: process.env.NODE_ENV === "development",
            },
        ],
    ],
    plugins: [
        ["@babel/plugin-proposal-decorators", { legacy: true }],
        ["@babel/plugin-proposal-class-properties", { loose: true }],
        "@babel/plugin-syntax-dynamic-import",
    ],
};
复制代码

eslint 配置

目前 eslist 推荐使用 eslint-webpack-plugin插件,因为 eslint-loader 即将废弃

⚠️ The loader eslint-loader will be deprecated soon

安装

npm i
eslint
eslint-webpack-plugin
eslint-config-airbnb-base
eslint-plugin-import -D
复制代码
  • eslint >= 7 (版本)

  • eslint-config-airbnb-base 支持所有 es6+的语法规范,需要 eslint 和 eslint-plugin-import 一起使用

  • eslint-plugin-import 用于支持 eslint-config-airbnb-base 做导入/导出语法的检查

webpack.dev.js

 new ESLintPlugin({
    fix: true, // 启用ESLint自动修复功能
    extensions: ['js', 'jsx'],
    context: paths.appSrc, // 文件根目录
    exclude: '/node_modules/',// 指定要排除的文件/目录
    cache: true, //缓存
}),
复制代码

此外有了 ES 的语法规范 还需要 react jsx 的的语法规法,

npm i eslint-plugin-react -D
// 在eslint config 拓展预设中 配置 react
extends: [
    "plugin:react/recommended", // jsx 规范支持
    "airbnb-base", // 包含所欲ES6+ 规范
],

// 或者 在插件中设置

"plugins": [
    "react"
  ]
复制代码

同时在根目录配置 .eslintrc.js文件

.eslintrc.js

module.exports = {
    env: {
        browser: true,
        es2021: true,
    },
    extends: [
        "airbnb-base", // 包含所欲ES6+ 规范
        "plugin:react/recommended", // react jsx 规范支持
    ],
    parserOptions: {
        ecmaFeatures: {
            jsx: true,
        },
        ecmaVersion: 12,
        sourceType: "module",
    },
    plugins: [],
    rules: {
        "consistent-return": 0, // 箭头函数不强制return
        semi: 0,
        "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
        "react/jsx-uses-react": "error", // 防止react被错误地标记为未使用
        "react/jsx-uses-vars": "error",
        "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx"] }],
        "react/jsx-key": 2, // 在数组或迭代器中验证JSX具有key属性
        "import/no-dynamic-require": 0,
        "import/no-extraneous-dependencies": 0,
        "import/no-named-as-default": 0,
        // 'import/no-unresolved': 2,
        "import/no-webpack-loader-syntax": 0,
        "import/prefer-default-export": 0,
        "arrow-body-style": [2, "as-needed"], // 箭头函数
        "class-methods-use-this": 0, // 强制类方法使用 this
        // 缩进Indent with 4 spaces
        indent: ["error", 4, { SwitchCase: 1 }], // SwitchCase冲突 闪烁问题
        // Indent JSX with 4 spaces
        "react/jsx-indent": ["error", 4],
        // Indent props with 4 spaces
        "react/jsx-indent-props": ["error", 4],
        "no-console": 0, // 不禁用console
        "react/jsx-props-no-spreading": 0,
        "import/no-unresolved": [
            2,
            {
                ignore: ["^@/"], // @ 是设置的路径别名
            },
        ],
    },
    //如果在webpack.config.js中配置了alias 并且在import时使用了别名需要安装eslint-import-resolver-webpack
    settings: {
        "import/resolve": {
            webpack: {
                config: "config/webpack.dev.js",
            },
        },
    },
};
/*
"off"或者0    //关闭规则关闭
"warn"或者1    //在打开的规则作为警告(不影响退出代码)
"error"或者2    //把规则作为一个错误(退出代码触发时为1)
*/
复制代码

也可以把 eslint 配置 放在 package.json,跟下面这样但是内容有点多为了减少耦合性还是放根目录吧 package.json

"eslintConfig": {
    "extends": ["plugin:react/recommended","airbnb-base"],
    ...省略
}

复制代码

智能感知 import 别名导入文件

默认情况下在Vscode 通过webpack.resolve.alias 配置的别名,在import 导入是没有路径提示的 为了使用别名导入模块有更好的体验在根部目录添加一个 jsconfig.json 文件

jsconfig.json


{
    "compilerOptions": {
        "baseUrl": "./src",// 基本目录,用于解析非相对模块名称
        "paths": {
            "@/*": ["./*"] //指定要相对于 baseUrl 选项计算别名的路径映射
        },
      "experimentalDecorators": true //为ES装饰器提案提供实验支持
    },
    "exclude": ["node_module"]
}

复制代码

这个别名应该与 webpack resolve 中的别名一致

webpack.common.js

resolve: {
    modules: [paths.appNodeModules],
    extensions: ['.js', '.jsx', '*'],
    mainFields: ['browser', 'jsnext:main', 'main'],
    alias: {
        moment$: 'moment/moment.js',
        '@/src': paths.appSrc,

    },
},
复制代码

效果:

image.png

环境配置完善

环境变量配置可以分为 node 环境配置 和 模块环境配置,两者都是单独设置无法共享

node 全局变量

通过 cross-env 可以设置 node 环境的全局变量区别开发模式还是生产模式

⚠️ 在 ESM 下无效的

npm i cross-env -D
复制代码

package.json

 "scripts": {
    "build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
    "start": "cross-env NODE_ENV=development node server",
    ...省略
  },
复制代码

.eslintrc.js

配置过 node 的环境全局变量后就可以 通过 process.env.NODE_ENV 获取到值

 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // 如果是生产环境就不允许debugger
复制代码
DefinePlugin 用来设置模块内的全局变量

这个是 webpack 自带的一个插件,可以在任意模块内通过 process.env.NODE_ENV 获取到值

 new webpack.DefinePlugin({
    NODE_ENV: isEnvProduction && JSON.stringify('production'), // 设置全局
}),
复制代码
const App = () => {
    console.log(process.env.NODE_ENV); // development

    return (
        <div>
            <Index />
            1133366
        </div>
    );
};
复制代码
IgnorePlugin
  • IgnorePlugin 用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去
new webpack.IgnorePlugin(/^\.\/locale/, /moment$/);
复制代码

生产环境配置完善

抽离 css

默认 打包是将 样式注入到 js 文件中运行性添加到 head style 标签,这样在开发模式下比较方便,但是生产环境建议将 css 抽离出来成为单独文件,这样如果应用代码发生变化,浏览器只能获取更改的 JS 文件,而提取的 css 文件则可以进行单独缓存.

image.png 安装

npm i -D mini-css-extract-plugin
复制代码
  • 该插件将 CSS 提取到单独的文件中。它为每个包含 CSS 的 JS 文件创建一个 CSS 文件,他为 style-loader 不能一起用,所以让它生产模式才生效。

  • 将 loader 与 plugin 添加到的 webpack 配置文件中

webpack.commom.js

const isEnvProduction = options.mode === 'production';

 {
    test: sassRegex,
    exclude: sassModuleRegex,
    use: [
        isEnvProduction ? MiniCssExtractPlugin.loader : 'style-loader',
        {
            loader: 'css-loader',
            options: {
                importLoaders: 1,
            },
        },
        'postcss-loader',
        'sass-loader',
    ],
},

复制代码

webpack.prod.js


plugins: [
    ...省略
    new MiniCssExtractPlugin({
        filename: 'css/[name].[contenthash:8].css', //输出的 CSS 文件的名称
        chunkFilename: 'css/[name].[contenthash:8].chunk.css',// 非入口的 css chunk 文件名称
        ignoreOrder: true, // 忽略有关顺序冲突的警告
    }),
],
复制代码

压缩 css

对 Webpack 5 Optimize CSS Assets Webpack Plugin 不推荐使用了,用 webpack 的 css-minimizer-webpack-plugin

⚠️ For webpack v5 or above please use css-minimizer-webpack-plugin instead.

这个插件使用 cssnano 优化和压缩 CSS。就像 optimize-css-assets-webpack-plugin 一样,但在 source maps 和 assets 中使用查询字符串会更加准确,支持缓存和并发模式下运行。

安装


npm install image-webpack-loader --save-dev
复制代码

这将只在生产模式下启用 CSS 压缩优化,如果需要在开发模式下使用, 可以设置 optimization.minimize 选项为 true

webpack.common.js

 optimization: {
    minimize: isEnvProduction, //是否是生产环境
    minimizer: [
        new CssMinimizerPlugin({
            parallel: true, // 开启多进程并发执行,默认 os.cpus().length - 1
        }),
        new TerserPlugin()
    ],
},
复制代码

压缩后 image.png

压缩 js

terser-webpack-plugin 使用 terser 适用于 ES6+ 的 JavaScript 解析器 来压缩 js 文件,是 Webpack 5 内置的 webpack4 是需要单独安装的

webpack.common.js

const TerserPlugin = require('terser-webpack-plugin')
...
 optimization: {
    minimize: isEnvProduction,
    minimizer: [
        ...
        new TerserPlugin({
            parallel: true, // 开启多进程并发执行
        }),
    ],
},
复制代码

压缩图片

压缩图片也是平时打包优化的一重要环节

image-webpack-loader 可以帮助我们对图片进行压缩和优化,但是安装这这个遇到了一些坑,Cannot find module 'gifsicle,安装 gif 的时候报错了,image-minimizer-webpack-plugin 也一样安装不了gif插件,不过科学上网可以安装

image-webpack-loader

安装

npm i -D image-webpack-loader
复制代码

image-webpack-loader 安装对应的 Issues

cnpm 是可以下载的但是 cnpm 相对 webpack 5 规则是不规范的下载工具 或者是 用 image-webpack-loader 低版本来试试,但是一版本会有一些 bug 我试了 yarn 和 npm 都遇到了Cannot find module 'gifsicle'

如果你成功使用了image-webpack-loader,可以使用一下配置


 {
    test: /\.(gif|png|jpe?g|svg|webp)$/i,
    type: 'asset',
    parser: {
        dataUrlCondition: {
            maxSize: imageInlineSizeLimit, // 4kb
        },
    },
    use: [
        {
            loader: 'image-webpack-loader',
            options: {
                mozjpeg: {
                    progressive: true,
                    quality: 65,
                },
                optipng: {
                    enabled: false,
                },
                pngquant: {
                    quality: '65-90',
                    speed: 4,
                },
                gifsicle: {
                    interlaced: false,
                },
                webp: {
                    quality: 75,
                },
            },
        },
    ],
},
复制代码
  • mozjpeg —压缩 JPEG 图像
  • optipng —压缩 PNG 图像
  • pngquant —压缩 PNG 图像
  • svgo —压缩 SVG 图像
  • gifsicle —压缩 GIF 图像

压缩前

image.png

压缩后

image.png

image-minimizer-webpack-plugin

这两个选一个就可以了 安装

npm install image-minimizer-webpack-plugin --save-dev
复制代码

可以用两种模式优化图像:

  • 无损(无质量损失)
  • 有损(质量下降)

具体选择官方文档已经给出来了

imagemin 插件进行无损优化

npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --save-dev
复制代码

imagemin 插件用于有损优化

npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo --save-dev
复制代码

imagemin-gifsicle 一样也是安装不了,

如果安装成功可以使用一下配置

webpack.prod.js

new ImageMinimizerPlugin({
    minimizerOptions: {
        plugins: [
            ['gifsicle', { interlaced: true }],
            ['jpegtran', { progressive: true }],
            ['optipng', { optimizationLevel: 5 }],
            [
                'svgo',
                {
                    plugins: [
                        {
                            removeViewBox: false,
                        },
                    ],
                },
            ],
        ],
    },
}),
复制代码

优化的思路

优化 方向分两个 一个是时间(速度)和空间(体积) webpack 不管模块规范 打包出来统一都是 webpack_require, 如果开发模式可以支持 ESM 方案就好了好了速度就很快,这个只是个人痴想。

现在这种优化手段也比较多,或者也可以尝试 其他工具 Vite、Snowpack,但目前阶段还成熟,真的用到项目中的可能还比较少, 坑比较多,我也是几个页面在很小项目中使用了一下 Vite,但是工具本身这确实很 nice,期待其发展。 Webpack 优化我就提提思路,然后很多博客都有提及,可以去看看他们的。

不管做什么都需要有明确的目标,优化也是如此,需要明白那里最耗时间,就从哪里入手,对症下药。 首先使用 speed-measure-webpack-plugin 可以分析打包各个步骤的时间

这个包在我这边使用不成功,在 issue 中也有一些人遇到了这个问题 issue

npm i speed-measure-webpack-plugin -D
复制代码

webpack-bundle-analyzer 分析打包出的文件包含哪些,大小占比如何,模块包含关系,依赖项,文件是否重复,压缩后大小如何

npm i webpack-bundle-analyzer -D
复制代码

编译时间优化

缩小文件查找范围
  • resolve
resolve: {
    modules: [paths.appNodeModules],
    extensions: ['.js', '.jsx', '*'],
    mainFields: ['browser', 'jsnext:main', 'main'],
    alias: {
        moment$: 'moment/moment.js',
        '@/src': paths.appSrc,
    },
},
复制代码
  • rule.oneOf

在 loader 解析的时候对于 rules 中的所有规则都会遍历一遍,如果使用 oneOf 就可以解决该问题,只要能匹配一个即可退出,类似 Array.find 找到对的就返回不会继续找了

module.exports = {
  module: {
    rules: [
      {
        oneOf: [
          ...,
        ]
      }
    ]
  }
}
复制代码
  • external

  • 多进程处理

缓存
  • 持久化缓存
  • babel-loader 开启缓存

体积优化

  • css html image js 压缩

  • tree-sharking 去除无用代码

    在打包的时候去除没有用到的代码,webpack4 版本的 tree-shaking 比较简单,主要是找一个 import 进来的变量是否在这个模块内出现过,webpack5 可以进行根据作用域之间的关系来进行优化

    tree-shaing 依赖 ESM,如果不是 ESM 就不支持,比如 commonjs 就不行,主要是 EMS 是静态依赖分析编译的时候就能判断出他的依赖项,而 require 是运行是加载,不不知道它是如何依赖的的就很蛋疼。

  • splitChunks 分包

问题

在上一篇掘友反馈webpack-dev-server 起服务时遇到了问题,就是页面报错后,重新修复了页面不会刷新,我试了一下确实存在。这个具体原因我也没有找到,但是配置了 eslint 可以在它重新编译的时候恢复热更新,可以试下.

总结

那么到这里整个搭建就结束了^_^有部分知识点我可能略过没有写,具体细节可以源码。然后这篇应该是 webpack 最后一篇了。后面可能会写一些比较有意思的 Webpack 插件。如果在调试 Webpack5 的时候遇到问题可以在评论区探讨。最后,希望大家看了都有收获,下篇见...

文章分类
前端
文章标签