抛开脚手架,徒手搭建 react 项目,webpack打包优化篇(四)

510 阅读10分钟

[《抛开脚手架,徒手搭建 react 项目(一)》](juejin.cn/post/739816… ")我们用webpack搭建了一个react项目,引入了typescript,用Babel编译,还用webpack-merge拆分了我们的配置文件。

《抛开脚手架,徒手搭建 react 项目(二)》我们在项目里面引入了eslint,还介绍了打开服务的五种方法。尤其重要的是 nginx,要你在本地体验一把线上奔跑的感觉。

《抛开脚手架,徒手搭建 react 项目(三)》我们在项目里面引入了 stylelint、还有husky相关工具,轻松实现git commit的时候,对代码格式和提交信息做具体的检测。

《抛开脚手架,徒手搭建 react 项目,webpack打包优化篇(四)》亲手测试并整理了六点减少打包时间的手段。

抛开脚手架,徒手搭建 react 项目,webpack打包优化篇(五) 亲手测试并整理了六点能够缩减打包体积的手段

前端工程相关的内容都有哪些?

image.png

关于webpasck的优化

image.png

1.文件配置说明

这张图,清楚的告诉我们,webpack在什么阶段调用什么配置项,咱们可以借用这张图重新认识下webpack。

entry:是webpack的打包入口 output:是webpack的打包出口,就是把打包好的文件放到哪里去。 一般配置如下:

const path = require('path')

module.exports = {
  entry: {
    app: path.resolve(__dirname, '../../src/app.js'),
  },
  output: {
    filename: 'js/[name].[hash:8].js',
    path: path.resolve(__dirname, '../../dist'),
  },
}

解释下filename: 'bundle.js',直接规定好打包后的文件名字是bundle.js

  • 你也可以写成这样,filename: '[name].js',打包的入口名字叫什么,打包后的名字就叫什么。

  • 你也可以写成这样,filename: '[id].js',每个包都有一个唯一id,我们就用这个id命名。

  • 你也可以写成这样,filename: '[contenthash].js',每个包都有一个缓存的hash值,我们就用这个hash命名。

webpack的hash值,很有用,对理解webpack-dev-server很有用,因为它的hot更新就是利用hash做的

**Hash 值类型:**

-   `[hash]`:与整个项目的构建相关的哈希值。不论哪个文件有变化,整个项目构建的哈希值都会改变。
-   `[chunkhash]`:与 Webpack 打包过程中生成的 chunk 相关的哈希值。同一 chunk 内文件没有变化时,[chunkhash] 是不变的。它可以用来优化浏览器缓存,因为不同的文件通常被打包进不同的 chunk。
-   `[contenthash]`:与文件内容直接相关的哈希值。只有文件内容改变了,`[contenthash]` 才会改变。这在使用如 css-extract-plugin 分离 CSS 文件时非常有用,因为你可能只希望在 CSS 文件的内容实际发生变化时才改变文件名。
-   `[modulehash]`:与单个模块相关的哈希值。
-   `[fullhash]`Webpack 5 引入的哈希值,用来代替旧版本中的 [hash]。

2.环境配置说明

由于不同操作系统设置获取环境变量的方式是不一样的,在 Mac 上,我们通常使用 export NODE_ENV=development ,而在Windows上,我们使用的是 set NODE_ENV=development 为了抹平系统差异带来的麻烦,我请cross-env 出手解决。

npm i cross-env -D

package.json里面添加:

 "start": "cross-env NODE_ENV=development webpack",

在webpack.config.js里面我们拿一下环境变量看看

const env = process.env.NODE_ENV;
console.log(env, 999);
const isDev = process.env.NODE_ENV !== 'production';

image.png

3.Source Map,devtool 配置说明

  • eval:每个模块会用 eval() 执行,并且在末尾追加一个 //@ sourceMappingURL 注释,最快。
  • cheap-eval-source-map:生成较快,并且可以映射到行号。
  • cheap-module-eval-source-map:生成较快,并且会生成 Source Map 到模块级别。
  • eval-source-map:转换(transpile)每个模块,并在一个 DataUrl 中提供 Source Map,提供质量更好但速度较慢的 Source Map。
  • cheap-source-map:不包含列信息的 Source Map,不包括 loader 的 Source Map。
  • cheap-module-source-map:不包含列信息,但是加载器(loader)的 Source Map 会被简化为每行一个映射(mapping)。
  • inline-source-map:生成一个完整的 Source Map,并以 DataUrl 的形式追加到输出文件末尾。
  • source-map:生成一个独立的 Source Map 文件,提供完整的映射信息,通常用于生产环境中。
  • hidden-source-map:创建 Source Map,但不在打包文件中引用,错误信息不会显示原始代码的位置信息,只会显示构建后代码的位置信息。
  • nosources-source-map:创建 Source Map,但不包含 sourcesContent(原始代码内容)。 这些选项提供了不同层次的 Source Map 质量和构建性能之间的权衡。

一般情况下,在开发环境中,你可能会选择一个构建速度快的 Source Map 选项,比如 eval-source-map,以提高构建和重构建的速度。在生产环境中,为了获取好的源映射,通常会采用 source-map 或 hidden-source-map 这种完整但较慢的选项。

开发环境

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-source-map',
})

生产环境

module.exports = merge(common, {
  mode: 'production',
  devtool: false,
})

如果你想调试生产环境的代码,请看这个文章# 作为前端开发,如何调试线上代码?

配置更新日志changelog

安装

 npm i conventional-changelog-cli -D

在package.json里面配置


  "scripts": {
  ...
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
  },

执行命令

 npm run changelog

生成文件

image.png

从此,你可以把你要提交的内容全部写在这个文件里面,以后我们要看看之前哪个分支做了什么事情,就一目了然了。从此你就可以快乐的回退代码了。

显示打包进度

安装

npm i progress-bar-webpack-plugin -D

配置webpack.config.js

const ProgressBarPlugin = require('progress-bar-webpack-plugin')


new ProgressBarPlugin({      complete: '█',    }),

执行命令npm run build

image.png

webpack性能优化

我对两层面分别做出6个总共12个性能优化建议,为了方便记忆都使用四字真言概括。⏱表示减少打包时间,📦表示减少打包体积

  • 减少打包时间缩减范围缓存副本定向搜索提前构建并行构建可视结构
  • 减少打包体积分割代码摇树优化动态垫片按需加载作用提升压缩资源

1缩减范围

利用loader的include/exclude缩小搜索范围

image.png

2.利用缓存

很多loader和Plugin都会又自己的缓存配置项,目的就是在在下次打包的时候,像打补丁一样,谁没有编译过,就编译谁,以前编译过的文件复用就好。比如babel-loader的cache,还有eslint-webpack-Plugin的cache,都是缓存

image.png

image.png

3.定向搜索

resolve提高文件的搜索速度的配置项, 一般我们的别名,后缀都在这里配置。

extension可以设置扩展名,当我们写import App from './App'的时候,它自动会去找对应扩展名是[".js", ".ts", ".jsx", ".tsx", ".json", ".vue"]的文件。

alias可以设置别名,当我们写 import getName from '@/utils/user'的时候,它会把@帮我解析成src//utils/user

image.png

4.提前构建

webpack 插件文档

webpack5 DllPlugin|DllReferencePlugin

DefinePlugin主要功能

  • 定义环境变量:可以根据不同的环境(如开发环境、生产环境)定义不同的变量,以便在代码中进行相应的处理。
  • 配置参数动态化:将一些配置参数定义为全局变量,方便在代码中进行读取和使用。
  • 条件编译:通过定义不同的变量来实现条件编译,从而减少不必要的代码执行。

说白了,他就是给webpack定义全局变量的工具,比如定义下面这个


new webpack.DefinePlugin({
  ENV: JSON.stringify(process.env.NODE_ENV),
});

在src/index.js下面

image.png

测试

image.png

打包如果出现这个提示:

image.png

解决办法:

  performance: {
    maxEntrypointSize: 50000000,
    maxAssetSize: 30000000,
  },

DllPlugin 和 DllReferencePlugin

DLLPlugin能把第三方库代码分离开,并且每次文件更改的时候,它只会打包该项目自身的代码。所以打包速度会更快。

首先需要新建一个webpack.dll.config.js文件。webpack.dll.config.js作用是把所有的第三方库依赖打包到一个bundle的dll文件里面,还会生成一个名为 manifest.json文件。

manifest.json的作用是用来让 DllReferencePlugin 映射到相关的依赖上去的。

DllReferencePlugin作用是把刚刚在webpack.dll.config.js中打包生成的dll文件引用到需要的预编译的依赖上来。

因为webpack.dll.config.js中打包后比如会生成 vendor.dll.js文件和vendor-manifest.json文件,vendor.dll.js文件包含所有的第三方库文件,vendor-manifest.json文件会包含所有库代码的一个索引,当在使用webpack.config.js文件打包DllReferencePlugin插件的时候,会使用该DllReferencePlugin插件读取vendor-manifest.json文件,看看是否有该第三方库。vendor-manifest.json文件就是有一个第三方库的一个映射而已

第一次使用 webpack.dll.config.js 文件会对第三方库打包,打包完成后就不会再打包它了,然后每次运行 webpack.config.js文件的时候,都会打包项目中本身的文件代码,当需要使用第三方依赖的时候,会使用 DllReferencePlugin插件去读取第三方依赖库。所以说它的打包速度会得到一个很大的提升。

配置

1.先创建webpack.dll.conf.js
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
  entry: {
    // 项目中用到该两个依赖库文件
    vendor: ['react', 'react-dom', 'antd', 'lodash-es'],
  },
  output: {
    filename: '[name].dll.js',   // 文件名称
    path: path.resolve(__dirname, 'public'),// 将输出的文件放到public目录下
    /*
     存放相关的dll文件的全局变量名称,比如对于react来说的话就是 _dll_react, 在前面加 _dll
     是为了防止全局变量冲突。
    */
    library: '_dll_[name]'
  },
  plugins: [
    new DllPlugin({
      /*
       该插件的name属性值需要和 output.library保存一致,该字段值,也就是输出的 manifest.json文件中name字段的值。
       比如在react.manifest文件中有 name: '_dll_react'
      */
      name: '_dll_[name]',
      path: path.join(__dirname, 'public', '[name].manifest.json')/* 生成manifest文件输出的位置和文件名称 */
    }),
  ]
};

2.配置命令
"dll": " webpack --config webpack.dll.config.js"
3.执行命令:npm run dll

image.png

4.使用DllReferencePlugin

webpack.config.js里面配置

const { DefinePlugin, DllReferencePlugin } = require("webpack");

    new DllReferencePlugin({
      manifest: require('./public/vendor.manifest.json')
    }),
5.AddAssetHtmlPlugin

配置以后,我发现public的包和dist包,不在一起,生成的index.htmldist里面,总不能在dist里面引用public包里面的东西吧,一看就不合理,最好的办法就是把public里面的东西拷贝到dist里面。手动拷贝,肯定是所有程序员最厌恶的事情,万一错了,忘了,咋办?所以出现了一个工具是add-asset-html-webpack-plugin,它会做两件事:1.帮我们把public里面的文件拷贝到dist里面,2.把dist里面拷贝过来的js添加到html文件里面。

webpack.config.js文件里面

const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

......
  plugins: [
    !isDev && new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './index.html', // 指定一个HTML模板文件
      filename: './index.html', // 输出的HTML文件名,默认是index.html
      inject: true, // 允许插件修改哪些内容,true将脚本添加到body元素的末尾
      cache: true,
    }),
    new StylelintPlugin({
      files: ["src/**/*.{scss,css,less}"], // 指定要检查的文件
      fix: true, // 是否自动修复一些样式错误
      cache: true
    }),
    new EslintPlugin({ cache: true }),
    new ProgressBarPlugin({ complete: '█', }),
    new DefinePlugin({
      'ENV': JSON.stringify('production')//定义全局变量
    }),
    new DllReferencePlugin({
      manifest: require('./public/vendor.manifest.json')
    }),
    // 因为我们生成的缓存代码是在dll文件夹,我们要借助以下插件把dll下的文件帮运到dist文件夹下面
    new AddAssetHtmlPlugin({
      publicPath: ".",
      outputPath: "../dist",
      filepath: path.resolve(__dirname, "./public/vendor.dll.js")
    }),
  ],

添加命令

    "dev": "npx cross-env NODE_ENV=development webpack-dev-server ",
    "build": "npx cross-env NODE_ENV=production webpack",
    "lint": "eslint --fix \"./src/**/*.{js,jsx,ts,tsx}\"",
    "prepare": "husky",
    "lint:lint-staged": "lint-staged",
    "commit": "git-cz",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "dll": "npx cross-env NODE_ENV=production webpack --config webpack.dll.config.js"
6.测试

执行npm run dll 后再执行 npm run build 后你会发现

image.png

执行npm run dev

image.png

5.并行构建

配置Thread将Loader单进程转换为多进程

先来看下thread-loader吧,这个也是webpack4官方所推荐的。HappyPack的作者表示已不再维护此项目,这个可以在github仓库看到。所以happyPack就不要用了,直接选择thread-loader。

image.png

安装

npm i thread-loader -D

配置

const Os = require("os");
....
 {
        test: /\.(js|jsx|tsx|ts)$/,
        include: path.resolve(__dirname, 'src'),
        exclude: path.resolve(__dirname, 'node_modules'),
        use: [{
          loader: "thread-loader",
          options: { workers: Os.cpus().length }
        }, {
          loader: 'babel-loader',
          options: {
            presets: [
              "@babel/preset-env",
              "@babel/preset-react",
            ],
            cacheDirectory: true
          }
        }]

   },

执行npm run build

image.png

如果文件多的话,效果会更加明显!

6.可视结构

打包速度分析
npm i speed-measure-webpack-plugin -D

配置


const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");

const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  plugins: [
    new MyPlugin(),
    new MyOtherPlugin()
  ]
});

image.png

执行npm run build

image.png

打包体积分析

安装

npm i webpack-bundle-analyzer -D

image.png