webpack5从零开始搭建react + ts环境

215 阅读15分钟

1、初始化项目

首先手动新建一个项目文件夹 webpack-react-ts,然后在项目文件夹下执行

npm init -y

初始化 package.json 后,在项目目录下新增如下目录结构和文件

├── config
|   ├── webpack.common.js # 公共配置
|   ├── webpack.dev.js  # 开发环境配置
|   └── webpack.prod.js # 打包环境配置
├── public
│   └── index.html # html模板
├── src
|   ├── App.tsx 
│   └── index.tsx # react应用入口页面
├── tsconfig.json  # ts配置
├── .browserslistrc  # 浏览器适配信息
├── .prettierrc.js  # 代码格式化配置
├── typing.d.ts  # 定义ts文件
└── package.json

安装 webpack 依赖

npm i webpack webpack-cli -D

安装 react 依赖

npm i react react-dom -S

安装 react 类型依赖

npm i @types/react @types/react-dom -D

添加 public/index.html 内容

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    	<!--htmlWebpackPlugin.options.title 这暂时不用管,后面会讲到 -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

添加 tsconfig.json 内容

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react",
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["typing.d.ts", "./src",]
}

添加 .browserslistrc 内容
Browserslist是一个在不同的前端工具之间,共享目标浏览器和Node.js版本的配置
可以在项目根目录下新建 .borwserslistrc 文件,然后Browserslist工具就会根据这个条件查询需要适配的浏览器, 需要做适配的都会更具这个查出来的浏览器版本进行适配,比如postcss等等

>1%
last 2 version
not dead
ie 11

添加 .prettierrc.js 内容

module.exports = {
  printWidth: 130, // 一行的字符数,如果超过会进行换行
  tabWidth: 2, // 一个tab代表几个空格数,默认就是2
  useTabs: false, // 是否启用tab取代空格符缩进,.editorconfig设置空格缩进,所以设置为false
  semi: true, // 行尾是否使用分号,默认为true
  // singleQuote: true, // 字符串是否使用单引号
  trailingComma: "all", // 对象或数组末尾是否添加逗号 none| es5| all
  // jsxSingleQuote: true, // 在jsx里是否使用单引号,你看着办
  bracketSpacing: true, // 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
  arrowParens: "avoid", // 箭头函数如果只有一个参数则省略括号
};

添加 src/App.tsx 内容

import React from 'react'

function App() {
  return <h2>webpack5-react-ts</h2>
}
export default App

添加 src/index.tsx 内容

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = document.getElementById('root');
if(root) {
  createRoot(root).render(<App />)
}

2、配置 React + TS 环境

修改 webpack.common.js 文件 ,添加如下内容。
这里是 webpack 打包执行的入口文件,开发环境和生产环境的配置都会在这个文件内合并。这里也包含了一些公共配置

const { merge } = require('webpack-merge');
const prodConfig = require('./webpack.prod');
const devConfig = require('./webpack.dev');

const commonConfig = isProduction => {
  return {};
};
module.exports = function (env) {
  const isProduction = env.production;
  process.env.NODE_ENV = isProduction ? 'production' : 'development';

  const config = isProduction ? prodConfig : devConfig;
  const mergeConfig = merge(commonConfig(isProduction), config);

  return mergeConfig;
};

需要安装 webpack-merge

npm i webpack-merge -D

给 webpack.dev.js添加如下内容

const path = require('path');
module.exports = {}

给 webpack.prod.js 添加如下内容

const path = require('path');
module.exports = {}

2.1 webpack 公共配置

配置入口文件

修改 webpack.common.js 文件

const commonConfig = isProduction => {
  return {
    entry: path.resolve(process.cwd(), './src/index.tsx'), // ,
  };
};

配置出口文件

修改 webpack.common.js 文件 ,添加 output 选项内容

const commonConfig = isProduction => {
  return {
    // ...
    output: {
        filename: 'static/js/[name].[chunkhash:8].build.js', // 每个输出js的名称
        path: path.resolve(process.cwd(), './dist'), // 打包结果输出路径
        clean: true, // webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
        publicPath: '/' // 打包后文件的公共前缀路径
      },
  };
};

配置 loader 解析 ts 和 tsx

由于 webpack 默认只能识别 js 文件,不能识别 jsx 语法,需要配置 loader 解析。
@babel/preset-typescript来先ts语法转换为 js 语法 ,再借助预设 @babel/preset-react来识别jsx语法
安装 babel 核心模块 和 babel 预设

npm i  @babel/preset-react @babel/preset-typescript -D

修改 webpack.common.js文件

const commonConfig = isProduction => {
  return {
  	// ...
    module: {
      rules: [
        {
          test: /\.(js|ts)x?$/,
          include: [path.resolve(process.cwd(), "./src")], // 只对项目src文件的ts,tsx进行loader解析
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
          },
        },
      ],
    },
  };
};

在项目根目录下新建 babel.config.js 文件,将 babel 配置放在这里统一管理,写上如下预设代码

const presets = [
  ["@babel/preset-react"],
  ["@babel/preset-typescript"],
];

module.exports = {
  presets,
};

配置 resolve

extensions 配置文件名简写 extensions 是 webpack 的 resolve 解析配置下的选项,在引入模块时不带文件后缀时,会来该配置数组里面依次添加后缀查找文件,因为ts不支持引入以 .ts, tsx为后缀的文件,所以要在 extensions 中配置,而第三方库里面很多引入js文件没有带后缀,所以也要配置下js
修改 webpack.common.js文件,注意把高频出现的文件后缀放在前面

const commonConfig = isProduction => {
  return {
  	// ...
    resolve: {
      extensions: [".tsx", ".jsx", ".ts", ".js", ".json", ".wasm", ".mjs"]
    }
  };
};

alias 配置别名
webpack 支持设置别名,设置别名的可以让引用的地方减少路径复杂度
修改 webpack.common.js文件

const commonConfig = isProduction => {
  return {
  	// ...
    resolve: {
      alias: {
        "@": path.resolve(process.cwd(), "./src"),
      },
    }
};

修改 tsconfig.json, 添加baseUrl paths

{
  "compilerOptions": {
  	// ...
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  },

}

添加 html-webpack-plugin插件

webpack 需要把最终构建好的静态资源都引入到 html 中,这样才能在浏览器中运行。html-webpack-plugin 就是做这件事的,自动将打包好的文件引入html 文件中
安装依赖

npm i html-webpack-plugin -D

修改 webpack.common.js文件

const HtmlWebpackPlugin = require('html-webpack-plugin'); // html模板的plugin
const commonConfig = isProduction => {
  return {
  	// ...
    plugins: [
      new HtmlWebpackPlugin({
        title: "webpack-react-ts", // 模板文件里有用到title这个变量,将这里的变量名字注入到模板文件中
        template: path.resolve(process.cwd(), "./public/index.html"), // 模板文件的地址
        inject: true, // 自动注入静态资源
      }),
    ],
  };
};

2.2 webpack 开发环境配置

安装 webpack-dev-server

开发环境代码配置在 webpck-dev.js 中,需要借助 webpack-dev-server 在开发环境中启动服务器辅助开发。
安装依赖

npm i webpack-dev-server  -D

修改 webpack.dev.js 文件

const path = require('path');
module.exports = {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    hot: true, // 热更新HMR 后面会讲到react热替换的具体配置
    //hotOnly是当代码编译失败时,是否刷新整个页面,如果不希望重新刷新整个页面,可以设置hotOnly为true
    // hotOnly: true,
    host: '0.0.0.0', // 设置成0.0.0.0可以通过ip4地址访问到
    port: 8000, //port设置监听的端口,默认情况下是8080
    open: true, // 自动打开浏览器
    compress: false, // 是否开启压缩 gzip压缩,开发环境不开启,提升热更新速度
    proxy: {
      '/api': {
        // 表示的是代理到的目标地址
        target: 'http://123.207.32.32:8000',
        //默认情况下,我们的 /api 也会被写入到URL中,如果希望删除,可以使用pathRewrite
        pathRewrite: {
          '^/api': '',
        },
        //默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false
        secure: false,
        // 它表示是否更新代理后请求的headers中host地址【不太明白】
        changeOrigin: true,
      },
    },
    //historyApiFallback是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误
    historyApiFallback: true,
    static: {
      directory: path.resolve(process.cwd(), './public'), //托管静态资源public文件夹
    },
  }
};

package.json 添加 dev 脚本

// package.json
"scripts": {
  "start": "webpack server --config ./config/webpack.common.js --env development"
},

运行 以下命令可以在浏览器看到页面了

npm run start

2.3 配置打包环境

修改 webpack.prod.js 的代码

const path = require("path");
module.exports = {
  mode: "production",// 生产模式,会开启tree-shaking和压缩代码,以及其他优化
};

package.json 添加 build 打包命令脚本

 "scripts": {
    "build": "webpack --config ./config/webpack.common.js --env production"
  },

执行 npm run build ,最终打包在 dist 文件夹中

浏览器查看打包结果

安装 http-server

npm install --global http-server

然后在项目根目录下执行以下命令就可以启动打包后的项目了

http-server ./dist

到现在一个基础的支持react和ts的webpack5就配置好了,但只有这些功能是远远不够的,还需要继续添加其他配置。

3、基础功能配置

3.1 配置环境变量

环境变量按照作用来分为两种

  1. 区分是开发模式还是打包构建模式,可以用process.env.NODE_ENV,因为很多第三方包都用这个
  2. 区分项目业务环境开发、预发布、正式环境。可以用process.env.BASE_ENV

设置环境变量可以用 webpack 自带的--env 指令设置环境变量,然后用webpack.DefinePlugin这个内置插件为业务代码注入环境变量
修改 package.json 的 scripts 脚本为

"scripts": {
  "dev:dev": "webpack server --config ./config/webpack.common.js --env development BASE_ENV=dev",
  "dev:pre": "webpack server --config ./config/webpack.common.js --env development BASE_ENV=pre",
  "dev:prod": "webpack server --config ./config/webpack.common.js --env development BASE_ENV=prod",
  "start": "webpack server --config ./config/webpack.common.js --env development BASE_ENV=dev",
  "build:dev": "webpack --config ./config/webpack.common.js --env production BASE_ENV=dev",
  "build:pre": "webpack --config ./config/webpack.common.js --env production BASE_ENV=pre",
  "build:prod": "webpack --config ./config/webpack.common.js --env production BASE_ENV=prod"
},
  • developent/production 区分是开发模式还是打包构建模式
  • BASE_ENV=dev/BASE_ENV=pre/BASE_ENV=prod 区分项目业务环境开发、预发布、正式环境

修改 webpack.common.js 文件,如下

module.exports = function (env) {
  const isProduction = env.production;
  // 设置区分开发模式还是构建模式
  process.env.NODE_ENV = isProduction ? "production" : "development";
  // 设置区分业务环境
  process.env.BASE_ENV = env.BASE_ENV;

  const config = isProduction ? prodConfig : devConfig;
  const mergeConfig = merge(commonConfig(isProduction), config);

  console.log({BASE_ENV:process.env.BASE_ENV,NODE_ENV:process.env.NODE_ENV})
  return mergeConfig;
};

将当前的环境变量注入到业务代码,修改 webpack.common.js 文件,如下

const commonConfig = isProduction => {
  return {
  	// ...
    plugins: [
      new DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
        'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV)
      })
    ]
  }
}

在 src/index.tsx 下打印下这两个环境变量,检测是否注入到业务代码

// src/index.tsx
// ...
console.log('NODE_ENV', process.env.NODE_ENV)
console.log('BASE_ENV', process.env.BASE_ENV)

以上代码在 ts 环境中会报错,需要安装 @types/node 类型包

npm i @types/node -D

运行 npm run dev:dev 命令,然后在控制台查看是否有这两个变量

3.2 处理 css 和 less 文件

在 src 下新增 app.css

h2 {
  color: red;
  transform: translateY(100px);
}

在 src/App.tsx 中引入 app.css

import React from 'react'
import './app.css'

function App() {
  return <h2>webpack5-rea11ct-ts</h2>
}
export default App

执行打包命令npm run build:dev,会发现有报错, 因为webpack默认只认识js,是不识别css文件的,需要使用loader来解析css, 安装依赖

npm i style-loader css-loader -D
  • style-loader 把解析后的 css 从 js 中抽离,放到头部的 style 标签中
  • css-loader 解析 css 文件

因为解析 css 的配置在开发和打包构建都需要用到,所以修改 webpack.common.js 文件

const commonConfig = isProduction => {
  return {
  	// ...
    module: {
      rules: [
        {
          test: /\.css/,
          use: [
            { loader: 'style-loader' },
            {
              loader: 'css-loader',
              options: {
                // 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
                importLoaders: 2,
              },
            }
          ],
        }
      ]
    }
  }
}

loader执行顺序是从右往左,从下往上的,匹配到css文件后先用css-loader解析css, 最后借助style-loader把css插入到头部style标签中。
执行 npm run dev:dev 就可以看到样式生效了

3.3 支持 less 文件

项目开发中,为了更好的提升开发体验,一般会使用css超集less或者scss。对于这些超集也需要对应的loader来识别解析。这里选用 less 作为示范。需要安装依赖

npm i less-loader less -D
  • less-loader: 解析 less 文件代码,把 less 编译成css
  • less: less 的核心代码

修改 webpack.common.js 文件匹配 less 代码文件

const commonConfig = isProduction => {
  return {
  	// ...
    module: {
      rules: [
        {
          test: /\.less/,
          use: [
           { loader: 'style-loader' },
            {
              loader: 'css-loader',
              options: {
                // 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
                importLoaders: 1,
              }
            },
            {
              loader: 'less-loader'
            }
          ]
        }
      ]
    }
  };
};

新增 src/app.less 测试以下

#root {
  h2 {
    font-size: 20px;
    margin-left: 100px;
  }
}

在 App.tsx 中引入 app.less

import React from "react";
import './app.css'
import "./app.less"

function App() {
  return <h2>webpack5-react-ts</h2>;
}
export default App;

运行 npm run dev:dev 看页面效果

3.4 postcss 处理css兼容

这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀、css 样式的重置,但是实现这些工具,我们需要借助于 PostCSS 对应的插件,在 webpack 中使用 postcss 就是使用 postcss-loader 来处理的,然后使用 postcss-preset-env 这个插件处理 css,该插件默认内置了很多插件,比如:autoprefixer--添加前缀的插件
安装插件

npm install postcss-loader postcss-preset-env -D

在项目根目录下新建postcss.config.js文件,postcss-loader会读取这里的配置

module.exports = {
  plugins: [
    'postcss-preset-env'
  ]
}

在 webpack.common.js 中修改css less 兼容 css 处理,修改后如下

const commonConfig = isProduction => {
  return {
  	// ...
    module: {
      rules: [
        {
          test: /\.css/,
          use: [
            { loader: 'style-loader' },
            {
              loader: 'css-loader',
              options: {
                // 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
                importLoaders: 1,
              },
            },
            {
              loader: 'postcss-loader',
            },
          ],
        },
        {
          test: /\.less/,
          use: [
           { loader: 'style-loader' },
            {
              loader: 'css-loader',
              options: {
                // 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
                importLoaders: 2,
              }
            },
            {
              loader: 'postcss-loader',
            },
            {
              loader: 'less-loader'
            }
          ]
        }
      ]
    }
  };
};

配置完成后,需要有一份要兼容浏览器的清单,让postcss-loader知道要加哪些浏览器的前缀,之前新建的 .browserslistrc 文件就是这个作用

>1%   
last 2 version
not dead
ie 11

运行 npm run dev:dev 查看 css 前缀是否加上

3.5 babel 预设处理 js 兼容

现在js不断新增很多方便好用的标准语法来方便开发,甚至还有非标准语法比如装饰器,都极大的提升了代码可读性和开发效率。但前者标准语法很多低版本浏览器不支持,后者非标准语法所有的浏览器都不支持。需要把最新的标准语法转换为低版本语法,把非标准语法转换为标准语法才能让浏览器识别解析,而babel就是来做这件事的
安装依赖

npm i babel-loader @babel/core @babel/preset-env core-js -D
  • babel-loader: 使用 babel 加载最新js代码并将其转换为 ES5(上面已经安装过)
  • @babel/corer: babel 编译的核心包
  • @babel/preset-env: babel 编译的预设,可以转换目前最新的js标准语法
  • core-js: 使用低版本js语法模拟高版本的库,也就是垫片

修改 babel.config.js 文件如下

const presets = [
  [
    "@babel/preset-env",
    {
      // 设置兼容目标浏览器版本,这里可以不写,babel-loader会自动寻找上面配置好的文件.browserslistrc
      // "targets": {
      //  "chrome": 35,
      //  "ie": 9
      // },
      useBuiltIns: "usage", // 代码中需要哪些polyfill, 就引用相关的api
      corejs: 3, // 还需要配置corejs的
    },
  ],
  ["@babel/preset-react"],
  ["@babel/preset-typescript"],
];

module.exports = {
  presets,
};

3.6 复制 public 文件夹

一般public文件夹都会放一些静态资源,可以直接根据绝对路径引入,比如图片,css,js文件等,不需要webpack进行解析,只需要打包的时候把public下内容复制到构建出口文件夹中,可以借助copy-webpack-plugin插件,安装依赖

npm i copy-webpack-plugin -D

开发环境已经在 devServer 中配置了 static 托管了 public 文件夹,在开发环境使用绝对路径可以访问到public 下的文件,但打包构建时不做处理会访问不到,所以现在需要在打包配置文件 webpack.prod.js 中新增 copy 插件配置
修改 webpack.prod.js文件如下


const CopyWebpackPlugin = require("copy-webpack-plugin"); // 复制文件的plugin

module.exports = {
  // ...
  plugins: [
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(process.cwd(), "./public"), // 复制public下文件
          to: path.resolve(process.cwd(), "./dist"), // 复制到dist目录中
          globOptions: {
            // 表示需要忽略的文件,不用复制
            ignore: ["**/index.html"],
          },
        },
      ],
    }),
  ],
};

在 public 文件中新增一个 favicon.ico 图标文件,在 index.html 中引入测试

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

执行 npm run build:dev打包,就可以看到favicon.ico 已经被复制到 dist 目录下了
然后 再运行 http-server ./dist查看 favicon.ico 是否显示正确

3.7 处理图片文件

对于图片文件, webpack4 使用 file-loader 和 url-loade r来处理的,但 webpack5 不使用这两个 loader了,而是采用自带的 asset-module 来处理
修改 webpack.common.js, 添加图片解析配置

module.exports = {
  module: {
    rules: [
      // ...
     {
          test: /.(png|jpe?g|gif|svg)$/,
          // type: "asset/resource", file-loader的效果直接复制
          // type: "asset/inline", url-loader的效果直接转化为base64
          // type: "asset/source", 导出资源的源代码。之前通过使用 raw-loader 实现
          type: "asset", // 根据大小自动判断,例如一下配置了parser则根据这个大小是否转换成base64
          generator: {
            filename: "img/[name].[contenthash:6][ext]", // 自定义文件输出的名字和输出目录
          },
          parser: {
            dataUrlCondition: {
              maxSize: 10 * 1024, // 小于10kb的图片转化为base64
            },
          },
        }
    ]
  }
}

测试一下,准备一张小于 10kb 和一张大于 10kb 的图片,放在 src/assets/imgs 目录下,修改App.tsx

import React from "react";

import smallImg from './assets/imgs/5kb.png'
import bigImg from './assets/imgs/22kb.png'

import './app.css'
import "./app.less"

function App() {
  return <>
   <h2>webpack5-react-ts</h2>;

  <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
  </>
  }
 
export default App;

这时候会报错,找不到图片相应的类型声明,需要到 typing.d.ts 中增加图片类型声明

declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module '*.less';
declare module '*.css';

修改 app.less

#root {
  h2 {
    font-size: 20px;
    margin-left: 100px;
  }
  .smallImg {
    width: 69px;
    height: 75px;
    background: url("@/assets/imgs/5kb.png") no-repeat;
  }
  .bigImg {
    width: 232px;
    height: 154px;
    background: url("./assets/imgs/22kb.png") no-repeat;
  }
}

运行 npm run startnpm run build:dev http-server ./dist分别查看效果

3.8 处理字体和媒体文件

字体文件和媒体文件这两种资源处理方式和处理图片是一样的,只需要把匹配的路径和打包后放置的路径修改一下就可以了。修改 webpack.common.js 文件:

// webpack.common.js
const commonConfig = isProduction =>{
  module: {
    rules: [
      // ...
      {
        test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于10kb转base64位
          }
        },
        generator:{ 
          filename:'static/fonts/[name].[contenthash:6][ext]', // 文件输出目录和命名
        },
      },
      {
        test:/.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于10kb转base64位
          }
        },
        generator:{ 
          filename:'static/media/[name].[contenthash:6][ext]', // 文件输出目录和命名
        },
      },
    ]
  }
}

4、配置 react 模块热更新

借助 @pmmmwh/react-refresh-webpack-plugin 插件来实现,该插件又依赖于 react-refresh, 安装依赖

npm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D

配置 react 热更新插件,修改 webpack.dev.js 文件


const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
	// ...
  plugins: [new ReactRefreshWebpackPlugin()],
};

修改 babel.config.js 配置

const presets = [
  [
    "@babel/preset-env",
    {
      useBuiltIns: "usage", // 代码中需要哪些polyfill, 就引用相关的api
      corejs: 3, // 还需要配置corejs的
    },
  ],
  ["@babel/preset-react"],
  ["@babel/preset-typescript"],
];

const plugins = [];
const isProduction = process.env.NODE_ENV === "production";

// React HMR -> 模块的热替换 必然是在开发时才有效果
if (!isProduction) {
  // console.log(isProduction, typeof isProduction, "---------");
  plugins.push(["react-refresh/babel"]);
} else {
}

module.exports = {
  presets,
  plugins
};

修改 App.tsx 的内容

import React, { useState } from "react";

import smallImg from "./assets/imgs/5kb.png";
import bigImg from "./assets/imgs/22kb.png";

import "./app.css";
import "./app.less";

function App() {
  const [count, setCounts] = useState("");
  const onChange = (e: any) => {
    setCounts(e.target.value);
  };
  return (
    <>
      <h2>webpack5-react-ts2</h2>
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
      <div className="smallImg"></div>
      <div className="bigImg"></div>
      <p>受控组件</p>
      <input type="text" value={count} onChange={onChange} />
      

      <p>非受控组件</p>
      <input type="text" />
    </>
  );
}

export default App;

运行 npm run start 查看是否无刷新更新页面

5、优化构建速度

5.1 构建耗时分析

当进行优化的时候,肯定要先知道时间都花费在哪些步骤上了,而 speed-measure-webpack-plugin 插件可以帮我们做到,安装依赖

npm i speed-measure-webpack-plugin -D

为了不影响正常的开发/打包,新建一个 webpack.analy.js 分析耗时

const path = require("path");
const { merge } = require("webpack-merge");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); // 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin(); // 实例化分析插件

const { commonConfig } = require("./webpack.common");
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");

module.exports = function (env) {
  const isProduction = env.production;
  process.env.NODE_ENV = isProduction ? "production" : "development";

  const config = isProduction ? prodConfig : devConfig;
  const mergeConfig = merge(commonConfig(isProduction), config);
  /**
   * 分析各个loader、plugin的打包耗时,与MiniCssExtractPlugin有冲突
   */
  return smp.wrap(mergeConfig);
  // return mergeConfig;
};

同时需要修改 webpack.common.js 文件,在文件最末尾导出公共配置

module.exports.commonConfig = commonConfig;

在package.json 添加 scripts 脚本

 "scripts": {
    "dev:analy": "webpack --config ./config/webpack.analy.js --env development BASE_ENV=dev",
    "build:analy": "webpack --config ./config/webpack.analy.js --env production BASE_ENV=prod"
  },

执行 npm run build:analy 分析耗时 image.png

5.2 开启持久化缓存

webpack5 较于 webpack4,新增了持久化缓存、改进缓存算法等优化,通过配置 webpack 持久化缓存,来缓存生成的 webpack 模块和 chunk,改善下一次打包的构建速度,可提速 90% 左右,配置也简单,修改webpack.common.js

// webpack.common.js
// ...
const commonConfig = isProduction =>  {
  // ...
  cache: {
    type: 'filesystem', // 使用文件缓存
  },
}

通过开启持久化缓存,打包速度提神 80% image.png 缓存的存储位置在node_modules/.cache/webpack

5.3 开启多线程loader

webpack 的 loader 默认在单线程执行,现代电脑一般都有多核 cpu,可以借助多核 cpu 开启多线程 loader解析,可以极大地提升 loader 解析的速度, thread-loader 就是用来开启多进程解析loader的,安装依赖

npm i thread-loader -D

使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。

const commonConfig = isProduction => {
  return {
    module: {
      rules: [
        {
          test: /\.(js|ts)x?$/,
          include: [path.resolve(process.cwd(), "./src")], // 只对项目src文件的ts,tsx进行loader解析
          exclude: /node_modules/,
          use: [
            {
              loader: "thread-loader",
            },
            {
              loader: "babel-loader"
            }
          ]
        }
      ]
    }
  }
};

5.4 缩小 loader 作用范围

一般第三库都是已经处理好的,不需要再次使用loader去解析,可以按照实际情况合理配置loader的作用范围,来减少不必要的loader解析,节省时间,通过使用 include和exclude 两个配置项,可以实现这个功能,常见的例如:

  • include:只解析该选项配置的模块
  • exclude:不解该选项配置的模块,优先级更高
const commonConfig = isProduction => {
  return {
    module: {
      rules: [
        {
          test: /\.(js|ts)x?$/,
          include: [path.resolve(process.cwd(), "./src")], // 只对项目src文件的ts,tsx进行loader解析
          exclude: /node_modules/,
          use: [
            {
              loader: "thread-loader",
            },
            {
              loader: "babel-loader"
            }
          ]
        }
      ]
    }
  }
};

其他loader也是相同的配置方式,如果除src文件外也还有需要解析的,就把对应的目录地址加上就可以了,比如需要引入antd的css,可以把antd的文件目录路径添加解析css规则到include里面

5.5 缩小模块搜索范围

node 里面有三种模块

  • node 核心模块
  • node_modules 模块
  • 自定义文件模块

使用require和import引入模块时如果有准确的相对或者绝对路径,就会去按路径查询,如果引入的模块没有路径。会按照以下顺序查找

  1. 优先查询node核心模块
  2. 如果没有找到会去当前目录下node_modules中寻找,
  3. 如果没有找到会查从父级文件夹查找node_modules,一直查到系统node全局模块

这样会有两个问题,一个是当前项目没有安装某个依赖,但是上一级目录下node_modules或者全局模块有安装,就也会引入成功,但是部署到服务器时可能就会找不到造成报错,另一个问题就是一级一级查询比较消耗时间。可以告诉webpack搜索目录范围,来规避这两个问题


const commonConfig = isProduction => {
  return {
    resolve: {
  		extensions: [".tsx", ".jsx", ".ts", ".js", ".json", ".wasm", ".mjs"],
  		alias: {
    		"@": path.resolve(process.cwd(), "./src"),
  		},
      // 查找第三方模块只在本项目的node_modules中查找,提升模块查找的速度,提升构建速度
 		 modules: [path.resolve(process.cwd(), "./node_modules")], 
		}
  }
}
 

5.6 devtool 配置

开发过程中或者打包后的代码都是webpack处理后的代码,如果进行调试肯定希望看到源代码,而不是编译后的代码, source map就是用来做源码映射的,不同的映射模式会明显影响到构建和重新构建的速度, devtool选项就是webpack提供的选择源码映射方式的配置。
devtool的命名规则为 (inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map

  • inline 代码内通过 dataUrl 形式引入 SourceMap
  • hidden 生成 SourceMap 文件,但不使用
  • eval eval(...) 形式执行代码,通过 dataUrl 形式引入 SourceMap
  • nosources 不生成 SourceMap
  • cheap 只需要定位到行信息,不需要列信息
  • module 展示源代码中的错误位置

开发环境推荐 eval-cheap-module-source-map
修改 webpack.dev.js


module.exports = {
  devtool: "eval-cheap-module-source-map",
};

打包环境推荐:none(就是不配置devtool选项了,不是配置devtool: 'none')

5.7 显示构建速度

webpackbar 插件可以在打包的时候显示编译进度,安装依赖

npm i webpackbar -D

修改 webpack.common.js 文件

const WebpackBar = require("webpackbar"); // 显示编译进度
const commonConfig = isProduction => {
  return {
    plugins: [
      new WebpackBar({
        name: isProduction ? "正在打包" : "正在启动",
      })
  };
};

运行 npm run start 可以在控制台看见编译进度了

6、优化构建结果文件

6.1 webpack 包分析工具

webpack-bundle-analyzer是分析webpack打包后文件的插件。通过该插件可以对打包后的文件进行观察和分析,可以方便我们对不完美的地方针对性的优化,安装依赖

npm install webpack-bundle-analyzer -D

修改 webpack.prod.js

const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); // 打包分析

module.exports = {
 plugins: [
    new BundleAnalyzerPlugin(), //  打包分析
  ],
};

6.2 抽取css样式文件

在开发环境我们希望 css 嵌入在 style 标签里面,方便样式热替换,但打包时我们希望把 css 单独抽离出来,方便配置缓存策略。而插件 mini-css-extract-plugin 就是来帮我们做这件事的,安装依赖:

npm i mini-css-extract-plugin -D

修改 webpack.common.js ,根据环境变量设置开发环境使用 style-loader,打包环境抽离 css

const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 将css单独提取到独立的文件中
const commonConfig = isProduction => {
  return {
  	// ...
    module: {
      rules: [
        {
          test: /\.less/,
          use: [
          isProduction ? { loader: MiniCssExtractPlugin.loader } : { loader: "style-loader" },
            {
              loader: 'css-loader',
              options: {
                // 如果在css文件中映入了其他css文件,则需要用这个属性表示引入的该文件需要回退两个loader处理,即用less-loader处理
                importLoaders: 1,
              }
            },
            {
              loader: 'less-loader'
            }
          ]
        }
      ]
    }
  };
};

再修改 webpack.prod.js 打包时候抽取css

const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 将css提取到单独的文件
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: "static/css/[name].[contenthash:8].css",
    }),
  ],
};

配置完成后,开发模式会嵌入到 style 方便热气换,打包时候会抽离成单独的 css 文件
运行 npm run build:dev 验证 css 是否分离成单独文件

6.3 压缩 css 文件

上面配置了打包时把css抽离为单独css文件的配置,打开打包后的文件查看,可以看到默认css是没有压缩的,需要手动配置一下压缩css的插件。
可以借助css-minimizer-webpack-plugin来压缩css,安装依赖

npm i css-minimizer-webpack-plugin -D

修改 webpack.prod.js 文件,

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); // 对css代码进行压缩

module.exports = {
  plugins: [
    new CssMinimizerPlugin(), // 压缩css
  ],
};

再次执行 npm run build:prod 可以看到css 已经被压缩了

6.4 压缩 js 文件

项目维护的时候,一般只会修改一部分代码,可以合理配置文件缓存,来提升前端加载页面速度和减少服务器压力,而hash就是浏览器缓存策略很重要的一部分。webpack 打包的 hash 分三种:

  • hash:跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash:不同的入口文件进行依赖文件解析、构建对应的chunk,生成对应的哈希值,文件本身修改或者依赖文件修改, chunkhash 值会变化
  • contenthash:每个文件自己单独的 hash 值,文件的改动只会影响自身的 hash 值

hash是在输出文件时配置的,格式是 filename: "[name].[chunkhash:8][ext]",[xx] 格式是 webpack 提供的占位符, :8是生成hash的长度。

  • ext 文件后缀名
  • name 文件名
  • path 文件相对路径
  • folder 文件所在文件夹
  • hash 每次构建生成的唯一 hash 值
  • chunkhash 根据 chunk 生成 hash 值
  • contenthash 根据文件内容生成hash 值

因为js我们在生产环境里会把一些公共库和程序入口文件区分开,单独打包构建,采用 chunkhash 的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,可以继续使用浏览器缓存,所以 js 适合使用 chunkhash 。
css 和图片资源媒体资源一般都是单独存在的,可以采用 contenthash ,只有文件本身变化后会生成新 hash值。
之前的文件已经按照上面的 规则加了,这里不做过多示例,详情请查看上面的内容

6.5 代码分割第三方包和公共模块

一般第三方包的代码变化频率比较小,可以单独把 node_modules 中的代码单独打包, 当第三包代码没变化时,对应 chunkhash 值也不会变化,可以有效利用浏览器缓存,还有公共的模块也可以提取出来,避免重复打包加大代码整体体积, webpack 提供了代码分隔功能, 需要我们手动在优化项 optimization 中手动配置下代码分隔 splitChunks 规则。


module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors", // 提取文件命名为vendors,js后缀和chunkhash会自动加
          minChunks: 1, // 只要使用一次就提取出来
          chunks: "initial", // 只提取初始化就能获取到的模块,不管异步的
          minSize: 0, // 提取代码体积大于0就提取出来
          priority: 1, // 提取优先级为1
        },
        // react: {
        //   test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
        //   filename: "js/[id]_react.js",
        //   chunks: "all",
        // },
        commons: {
          // 提取页面公共代码
          name: "commons", // 提取文件命名为commons
          minChunks: 2, // 只要使用两次就提取出来
          chunks: "initial", // 只提取初始化就能获取到的模块,不管异步的
          minSize: 0, // 提取代码体积大于0就提取出来
        },
      },
    },
  },
};

配置完成后,可以看到 node_modules 的模块已经被抽离到 vendors.90e0c56b.build.js了,而业务代码则被抽离到 main.714f6a4c.build.js
image.png

修改业务代码再打包,此时 vendors.90e0c56b.build.js 没有变化,变化的只是 main 文件,这样发版后,浏览器就可以继续使用缓存中的 vendors.90e0c56b.build.js ,只需要重新请求 main.js 就可以了。 image.png

6.6 tree-shaking清理未引用js

Tree Shaking 的意思就是摇树,伴随着摇树这个动作,树上的枯叶都会被摇晃下来,这里的 tree-shaking 在代码中摇掉的是未使用到的代码,也就是未引用的代码。模式 mode 为production时就会默认开启tree-shaking 功能以此来标记未引入代码然后移除掉,测试一下。

6.7 tree-shaking清理未使用css

js 中会有未使用到的代码, css 中也会有未被页面使用到的样式,可以通过 purgecss-webpack-plugin 插件打包的时候移除未使用到的 css 样式,这个插件是和 mini-css-extract-plugin 插件配合使用的,在上面已经安装过,还需要glob来选择要检测哪些文件里面的类名和id还有标签名称, 安装依赖:

npm i purgecss-webpack-plugin glob-all -D

修改 webpack.prod.js
插件本身也提供了一些白名单 safelist 属性,符合配置规则选择器都不会被删除掉,比如使用了组件库 antd, purgecss-webpack-plugin 插件检测src文件下tsx文件中使用的类名和id时,是检测不到在 src 中使用 antd 组件的类名的,打包的时候就会把 antd 的类名都给过滤掉,可以配置一下安全选择列表,避免删除 antd组件库的前缀ant

const globAll = require("glob-all");
const PurgecssPlugin = require("purgecss-webpack-plugin"); // 消除css未使用的代码

module.exports = {
  plugins: [
    // 清理无用css(打包发现暂时有些没有用到的class样式没清理掉,原因待查找)
    new PurgecssPlugin({
      //表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;
      paths: globAll.sync([`${path.join(__dirname, "../src")}/**/*.tsx`, path.join(__dirname, "../public/index.html")]),
      //默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;
      safelist: function () {
        return {
          standard: ["body", "html",/^ant-/],
        };
      },
    }),

  ],
};

6.8 资源懒加载

webpack默认支持资源懒加载,只需要引入资源使用import语法来引入资源,webpack打包的时候就会自动打包为单独的资源文件,等使用到的时候动态加载。
以 react 为例,配合 lazy 和 Suspense 使用

6.9 资源预加载

上面配置了资源懒加载后,虽然提升了首屏渲染速度,但是加载到资源的时候会有一个去请求资源的延时,如果资源比较大会出现延迟卡顿现象,可以借助 link 标签的 rel 属性 prefetch 与 preload , link 标签除了加载 css 之外也可以加载 js 资源,设置 rel 属性可以规定 link 提前加载资源,但是加载资源后不执行,等用到了再执行。
rel的属性值

  • preload是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。
  • prefetch是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源,会在空闲时加载。

对于当前页面很有必要的资源使用 preload ,对于可能在将来的页面中使用的资源使用 prefetch。
webpack v4.6.0+ 增加了对预获取和预加载的支持,使用方式也比较简单,在 import 引入动态资源时使用webpack 的魔法注释

// 单个目标
import(
  /* webpackChunkName: "my-chunk-name" */ // 资源打包后的文件chunkname
  /* webpackPrefetch: true */ // 开启prefetch预获取
  /* webpackPreload: true */ // 开启preload预获取
  './module'
);

测试一下,在src/components目录下新建PreloadDemo.tsx, PreFetchDemo.tsx

// src/components/PreloadDemo.tsx
import React from "react";
function PreloadDemo() {
  return <h3>我是PreloadDemo组件</h3>
}
export default PreloadDemo

// src/components/PreFetchDemo.tsx
import React from "react";
function PreFetchDemo() {
  return <h3>我是PreFetchDemo组件</h3>
}
export default PreFetchDemo

修改 App.tsx

import React, { lazy, Suspense, useState } from "react";

import smallImg from "./assets/imgs/5kb.png";
import bigImg from "./assets/imgs/22kb.png";

// prefetch
const PreFetchDemo = lazy(
  () =>
    import(
      /* webpackChunkName: "PreFetchDemo" */
      /*webpackPrefetch: true*/
      "@/components/PreFetchDemo"
    ),
);
// preload
const PreloadDemo = lazy(
  () =>
    import(
      /* webpackChunkName: "PreloadDemo" */
      /*webpackPreload: true*/
      "@/components/PreloadDemo"
    ),
);

import "./app.css";
import "./app.less";

function App() {
  const [count, setCounts] = useState("");
  const [show, setShow] = useState(false);
  const onChange = (e: any) => {
    setCounts(e.target.value);
  };

  const onClick = () => {
    setShow(true);
  };
  return (
    <>
      <h2>webpack5-react-ts</h2>
      <img src={smallImg} alt="小于10kb的图片" />
      <img src={bigImg} alt="大于于10kb的图片" />
      <div className="smallImg"></div>
      <div className="bigImg"></div>
      <p>受控组件</p>
      <input type="text" value={count} onChange={onChange} />
      

      <p>非受控组件</p>
      <input type="text" />

      <button onClick={onClick}>展示</button>
      {show && (
        <>
          <Suspense fallback={null}>
            <PreloadDemo />
          </Suspense>
          <Suspense fallback={null}>
            <PreFetchDemo />
          </Suspense>
        </>
      )}
    </>
  );
}

export default App;

测试发现只有js资源设置prefetch模式才能触发资源预加载

使用pnpm 安装包,因为pnpm解决了幽灵依赖问题,如果用的pnpm的话,需要手动再安装以下依赖

pnpm add ansi-html-community  html-entities events core-js-pure error-stack-parser

6.10 打包时生成 gzip 文件

前端代码在浏览器运行,需要从服务器把html,css,js资源下载执行,下载的资源体积越小,页面加载速度就会越快。一般会采用gzip压缩,现在大部分浏览器和服务器都支持gzip,可以有效减少静态资源文件大小,压缩率在 70% 左右。
webpack可以借助compression-webpack-plugin 插件在打包时生成 gzip 文章,安装依赖

npm i compression-webpack-plugin -D

修改 webpack.prod.js

const CompressionPlugin = require("compression-webpack-plugin"); // webpack对文件的压缩
module.exports = {
  plugins: [
    new CompressionPlugin({
      test: /\.(css|js)$/i, //  匹配哪些文件需要压缩
      threshold: 0, // 设置文件从多大开始压缩
      minRatio: 0.8, // 至少的压缩比例
      algorithm: "gzip", // 采用的压缩算法
    }),
  ],
};

配置完成后再打包,可以看到打包后js的目录下多了一个 .gz 结尾的文件

7、总结

到目前为止已经使用webpack5把react18+ts的开发环境配置完成,并且配置比较完善的保留组件状态的热更新,以及常见的优化构建速度和构建结果的配置,完整代码已上传到 webpack-react-ts