从0到1使用webpack搭建react脚手架

背景

好多前端童鞋工作多年依然不会使用webpack搭建react脚手架,本文就介绍下如何从零开始搭建一个属于你自己的前端脚手架,提高自己的工程化实力,同时也提高团队的开发效率。

一、基础配置

目标:可以启动最简单的react项目

初始化项目、安装依赖

  1. 初始化项目 npm init -y
  2. 然后按下方的目录结构创建文件
javascript 复制代码
├── config
|   ├── webapack.base.js # 公共配置
|   ├── webpack.dev.js  # 开发环境配置
|   └── webpack.prod.js # 打包环境配置
├── public
│   └── index.html # html模板
├── src
|   ├── App.tsx 
│   └── index.tsx # react应用入口页面
├── tsconfig.json  # ts配置
└── package.json

安装依赖

javascript 复制代码
// 1.安装webpack
pnpm i webpack webpack-cli -D

// 2.安装react和类型依赖
pnpm i react react-dom 
pnpm i @types/react @types/react-dom -D

添加html, App, index, tsconfig

  1. 添加public/index.html内容
javascript 复制代码
<!DOCTYPE html>
<html lang="en">
<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="./favicon.ico ">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
  1. 添加tsconfig.json内容
javascript 复制代码
{
  "compilerOptions": {
    "target": "es5",
    // 指定要包含在编译中的 library  
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    // 允许 ts 编译器编译 js 文件  
    "allowJs": true,
    // 跳过类型声明文件的类型检查  
    "skipLibCheck": true,
    // es 模块 互操作,屏蔽 ESModule 和 CommonJS 之间的差异  
    "esModuleInterop": true,
    // 允许通过 import x from 'y' 即使模块没有显式指定 default 导出  
    "allowSyntheticDefaultImports": true,
    // 开启严格模式  
    "strict": true,
    // 对文件名称强制区分大小写  
    "forceConsistentCasingInFileNames": true,
    // 为 switch 语句启用错误报告  
    "noFallthroughCasesInSwitch": true,
    // 生成代码的模块化标准  
    "module": "esnext",
    // 模块解析(查找)策略  
    "moduleResolution": "node",
    // 允许导入扩展名为.json的模块  
    "resolveJsonModule": true,
    // 是否将没有 import/export 的文件视为旧(全局而非模块化)脚本文件  
    "isolatedModules": true,
    // 编译时不生成任何JS文件(只进行类型检查)  
    "noEmit": true,
    // 指定将 JSX 编译成什么形式  
    "jsx": "react-jsx"
  },
  // 指定允许ts处理的文件目录  
  "include": [
    "src"
  ]
}
  1. 添加src/App.tsx
javascript 复制代码
import React from 'react';

export default function App() {
  return (
    <div>App</div>
  )
}
  1. 添加index.tsx
javascript 复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />)

配置webpack

webpack公共配置

  1. 配置入口出口
javascript 复制代码
// config/webpack.base.js
const path = require('path');

module.exports = {
  entry: path.resolve(__dirname, '../src/index.tsx'),
  output: {
    path: path.resolve(__dirname, '../dist'), // 打包后的文件存放的位置, 必须是绝对路径
    filename: 'js/[name].[chunkhash:8].js', // [name]格式化字符串,之前是啥名,现在还是啥名  2.[chunkhash:8]指定hash值,解决缓存问题
    clean: true, // 每次打包前清空dist目录
    publicPath: '/',  // 资源引用路径,若不配置,刷新页面,页面空白 如:/goods/detail ->/goods/js/main.js,找不到资源
  },
}
  1. 配置loader解析ts, jsx
javascript 复制代码
pnpm i babel-loader @babel/core @babel/preset-env core-js @babel/preset-react @babel/preset-typescript -D
  1. babel-loader: 使用 babel 加载最新js代码并将其转换为 ES5
  2. @babel/corer: babel 编译的核心包
  3. @babel/preset-env: babel 编译的预设,可以转换目前最新的js标准语法
  4. core-js: 使用低版本js语法模拟高版本的库,也就是垫片

webpack.base添加module.rules

javascript 复制代码
module.exports = {
 module: {
    rules: [
      {
        test: /\.(ts|tsx|js|jsx)$/, use: ['babel-loader'],
      },
    ]
  },
};

为了避免webpack配置文件过大,将babel-loader配置抽离到babel.config.js ,,使用js 作为配置文件,可用process.env.NODE_ENV来区分是开发、打包模式。

在/babel.config.js中添加

javascript 复制代码
module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      '@babel/preset-env',  // 兼容高版本js
      '@babel/preset-react',
      '@babel/preset-typescript'
    ],
  }
};
  1. 配置路径别名, 省略文件后缀名

**extensions **作用:在引入模块时不带文件后缀时

javascript 复制代码
module.exports = {
  // ...
  resolve: {
    extensions: ['.js', '.tsx', '.ts', '.json'],  // 解析模块时,可以省略的扩展名
  },
}
  1. 添加html-webpack-plugin

作用:处理html的生成和管理,自动引入js,css。

  1. webpack-dev-server开发服务器,默认会找 /public 文件夹中的index.html,

先装包pnpm i html-webpack-plugin -D

javascript 复制代码
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../', 'public/index.html'), // 指定template后div里的东西不会被删除
      inject: true, // 自动注入静态资源, 2可指定js插入位置,如body前
      title: 'react-webpack', // 设置页面title
      // favicon: path.resolve(__dirname, '../', 'public/favicon.ico'), // 设置页面图标
      // filename: 'aaa.html', // 打包后的文件名, 默认index.html
    }),
  ],
}

为项目配置标题和网站图标

在HtmlWebpackPlugin配置title和favicon后,index.html也需要配置

javascript 复制代码
// 网站图标
<link rel="icon" href="./favicon.ico ">
// 页面标题
<title><%= htmlWebpackPlugin.options.title %></title>

基础的公共配置完毕,接着配置开发、打包环境

webpack开发环境配置

开发是需要在内存中打包,速度快。

  1. 配置webpack.dev.js

先装包pnpm i webpack-dev-server webpack-merge -D

配置webpack.dev.js

javascript 复制代码
const path = require('path');
const { merge } = require('webpack-merge');
const base = require('./webpack.base.js');

module.exports = merge(base, {
  mode: 'development', // development:开发环境,内存打包  production:生产环境,硬盘打包
  devtool: 'eval-cheap-module-source-map', // 生成map文件,方便调试
  devServer: {
    open: false, // 自动打开浏览器
    port: 3344,
    hot: true, // 热更新
    historyApiFallback: true, // 解决history路由404问题
    compress: false, // gzip压缩,开发环境不开启,提升热更新速度
    static: {
      directory: path.join(__dirname, "../public"), //托管静态资源public文件夹
    },
  },
});
  1. package.json添加启动脚本
javascript 复制代码
"scripts": {
  "start": "webpack serve -c ./config/webpack.dev.js",
}

至此,可看到项目启动起来了

webpack打包环境配置

  1. 处理webpack.prod.js
javascript 复制代码
const path = require('path');
const { merge } = require('webpack-merge')
const base = require('./webpack.base.js')

module.exports = merge(base, {
  mode: 'production', // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
})
  1. package.jsonscripts 中添加build打包命令
javascript 复制代码
"scripts": {
  "build": "webpack -c ./config/webpack.prod.js",
},
  1. 如何使用浏览器查看打包后的项目

打包后的dist 文件可以在本地借助node 服务器serve 打开,全局安装serve

javascript 复制代码
npm i serve -g

然后在项目根目录命令行执行serve -s dist,就可以启动打包后的项目了

至此,简单版的react-cli 配置完毕。

二、进阶配置

展示打包进度-ProgressPlugin

ProgressPlugin是自带的,如果要美颜版,先装包pnpm i webpackbar -D

javascript 复制代码
// webpack.base.js
const WebpackBar = require('webpackbar'); 

module.exports = {
  plugins: [
    // 2.开美颜
    new WebpackBar({
      color: "#85d", // 默认green,进度条颜色支持HEX
      basic: false, // 默认true,启用一个简单的日志报告器
      profile: false, // 默认false,启用探查器。
    })
  ]
}

自带的方式-一般不用

javascript 复制代码
const { ProgressPlugin } = require('webpack');

module.exports = {
  plugins: [
    // 1.基础版
    new ProgressPlugin({})
  ]
}

将某插件配置到全局-不用再引React

  1. 将某个插件全局化,比如react,这样组件中就不用导入了。
  2. webpack自带插件ProvidePlugin来处理

背景

解决方案:配置webpack.base

javascript 复制代码
const { ProvidePlugin } = require('webpack');

module.exports = {
  plugins: [
    new ProvidePlugin({
      React: path.resolve(__dirname, '../', 'node_modules/react/index.js'),
    })
  ]
}

配置路径别名

  1. 处理webpack.base.js
jsx 复制代码
module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, '../', 'src'),
    },
  }
}
  1. 处理tsconfig.json, 添加baseUrl,paths
javascript 复制代码
{
  "compilerOptions": {
    // ...
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
  }
}

复制public文件夹资源

背景:组件中用public的图片,打包后不显示

原因:打包时不会将public的资源放入dist

解决方案:用插件将public资源复制到dist

  1. 先装包 pnpm i copy-webpack-plugin -D
  2. 配置webpack.prod.js
jsx 复制代码
const CopyPlugin = require('copy-webpack-plugin');
module.exports = merge(base, {
  plugins: [
    // 复制public文件夹
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'), // 复制public下文件
          to: path.resolve(__dirname, '../dist'), // 复制到dist目录中
          filter: source => {
            return !source.includes('index.html') // 忽略index.html
          }
        },
      ],
    }),
  ],
});

配置sourcemap

作用:控制台报错的位置和源码中的位置一致(打包后行数映射到打包前的行数) 。打包后会多个map文件

在开发、生产环境中配置sourcemap

  1. 开发环境
javascript 复制代码
// webpack.dev.js
module.exports = {
  mode: 'development', // development:开发环境,内存打包  production:生产环境,硬盘打包
  devtool: 'eval-cheap-module-source-map', // 生成map文件,方便调试
}
  1. 生产环境
javascript 复制代码
// webpack.prod.js
module.exports = {
  mode: 'development', // development:开发环境,内存打包  production:生产环境,硬盘打包
  devtool: 'source-map', // 生成map文件,方便调试
}

处理样式

  1. 从后往前处理,当webpck遇到css时,先用css-loader加载解析返回内容给style-loader
  2. style-loader: 将css作为内部样式插head标签中
  3. 用mini-css-extract-plugin抽离css,一般只在生产环境配置
  4. 总结:开发时用内部样式,上线时用外部样式。
  1. 先装包
javascript 复制代码
pnpm i css-loader style-loader mini-css-extract-plugin -D  
  1. 配置webpack.base.js
javascript 复制代码
module.exports = {
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
    ]
  }
}
  1. 上线时抽离css,配置webpack.prod.js
javascript 复制代码
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const prodConfig = {
  mode: 'production',
  module: {
    rules: [
      // 1. 使用MiniCssExtractPlugin.loader代替style-loader
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] },
    ]
  },
  plugins: [
    // 2. 提取css
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
    }),
  ],
};

module.exports = merge(base, prodConfig);

❌处理less

prod里需要重复配,待解决

javascript 复制代码
pnpm i less less-loader -D 
javascript 复制代码
// webpack.base.js
{ test: /\.(css|less)$/, use: ['style-loader', 'css-loader', 'less-loader'] },

// webpack.prod.js
{ test: /\.(css|less)$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] },

压缩css

  1. 抽离css后,不会自动压缩,需要处理
  1. 装包pnpm i css-minimizer-webpack-plugin -D
  2. 配置webpack.prod.js
javascript 复制代码
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = merge(base, {
  // ...
  optimization: {
    minimizer: [ 
      // 压缩css
      new CssMinimizerPlugin(),
    ],
  },
});

压缩js

  1. webpack在mode=prod时默认会压缩js, 但是手动压缩css后,默认压缩js会失效

解决方案

  1. 装包 pnpm i terser-webpack-plugin -D
  2. 配置webpack.prod.js
javascript 复制代码
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(base, {
  // ...
  optimization: {
    minimizer: [ 
      // 压缩css
      new CssMinimizerPlugin(),
      // 压缩js
      new TerserPlugin({
        parallel: true, // 开启多线程压缩
        terserOptions: {
          compress: {
            // pure_funcs: ["console.log"] // 删除console.log
          }
        }
      }),
    ],
  },
});

处理css3前缀兼容

  1. 先装包
javascript 复制代码
pnpm i postcss-loader autoprefixer -D
  1. 修改webpack.base.js
javascript 复制代码
module.exports = {
  // ...
  module: { 
    rules: [
      // ...
      {
        test: /.(css|less)$/, //匹配 css和less 文件
        use: [
          // ...
          'postcss-loader',
          'less-loader',
        ]
      },
    ]
  },
  // ...
}
  1. 配置postcss

根目录新建postcss.config.js,postcss会自动读取配置

javascript 复制代码
module.exports = {
  plugins: ['autoprefixer']
}
  1. 配置要兼容的浏览器和版本
javascript 复制代码
// package.json
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }

🚩处理css module

注意:将css-loader,从v7 => "^6.0.0",否则报错

方案1 - react-css-modules

提取lessModule还是有问题,临时解决方案:不提取cssModule

  1. 先装包 pnpm i babel-plugin-react-css-modules postcss-less -D
  2. 配置webpack.base.js
javascript 复制代码
module.exports = {
  rules: [
    {
      test: /\.(ts|tsx|js|jsx)$/,
      use: [
        'thread-loader',
        {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
            plugins: [
              [
                'react-css-modules',
                {
                  exclude: 'node_modules',
                  filetypes: { '.less': { syntax: 'postcss-less' } },
                  generateScopedName: '[local]-[hash:base64:5]',
                },
              ].filter(Boolean),
            ],
          },
        }
      ],
      include: [path.resolve(__dirname, '../src')]
    },
    {
      test: /\.less$/,
      use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
    },
  ]
}
  1. 配置webpack.prod.js
javascript 复制代码
module.exports = merge(base, {
  plugins: [
    new MiniCssExtractPlugin({
      ignoreOrder: true,
      filename: 'css/[name].[contenthash:8].css',
    })
  ],
  module: {
    rules: [
      {
        test: /\.less$/,
        // 注意:提取less时排除 module.less
        exclude: /\.module\.less$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'],
      },
    ]
  },
}

成功。

❌方案2 - css-loader

  1. 普通的css又不生效, 普通less和module.less分开处理 - 参考cra

  2. prod不做处理,即

  3. 配置webpack.base.js

javascript 复制代码
module.exports = {
  module: 
    rules: [
      {
        test: /\.(css|less)$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                auto: true,
                localIdentName: '[local]-[hash:base64:5]'
              }
            },
          },
          'postcss-loader', 'less-loader',
        ],
        // sideEffects: true,
      },
    ]
}
  1. 处理webpack.prod.js,注意:处理普通less文件时要排除less.module文件。
javascript 复制代码
module.exports = merge(base, {
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
    })
  ],
  module: {
    rules: [
      {
        test: /\.less$/,
        exclude: /\.module\.less$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'],
      }
    ]
  },
})

保留旧的方案:排除module.less

javascript 复制代码
module.exports = {
  module: { reules: [
    // 处理普通less文件, 排除less.module文件
    { test: /\.less$/, exclude: /\.module\.less$/, use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader',] },
    // 处理普通less.module文件
    {
      test: /\.module\.less$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            // 为样式指定名称, local原样式名,还可加[path][name]
            modules: { localIdentName: '[local]-[hash:base64:5]' }
          },
        },
        'postcss-loader',
        'less-loader',
      ],
    },
  ]}
}

处理react热更新

  1. form修改文案,会热更新-浏览器自动刷新 ,input中的状态丢失
  2. 期望:浏览器不刷新的热更新,保留form状态。
  1. 先装包pnpm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D
  2. 配置webpack.dev.js
jsx 复制代码
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = merge(base, {
  plugins: [
    new ReactRefreshWebpackPlugin(), // react热更新
  ]
});

处理图片

一、webpack5已内置图片处理

  1. 添加静态文件声明来处理找不到模块或其类型声明的报错。

二、webpack4的方案:已淘汰(需装包)

  1. file-loader,将图片整到内存中,build时可看到图片,但是名字变了。
  2. url-loader,将图片转化为base64,build时看不到图片,打到了js中
  1. 配置webpack.base.js
jsx 复制代码
  module: {
    rules: [
      // 处理图片-webpack5,类似file-loader, 可通过generator配置图片的位置和名字
      { test: /\.(png|jpg|jpeg|gif|svg|webp)$/, type: 'asset/resource', generator: { filename: 'img/[name].[contenthash:8][ext]' } },
      // 处理图片-webpack4-已淘汰
      // { test: /\.(png|jpg|jpeg|gif|svg|webp)$/, use: 'file-loader' }
      // { test: /\.(png|jpg|jpeg|gif|svg|webp)$/, use: 'url-loader' }
    ]
  }
  1. 添加类型声明文件 src/index.d.ts
javascript 复制代码
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'
  1. 配置tsconfig.json
javascript 复制代码
// 不用ts处理的文件目录 
{
  compilerOptions: ...,
  "exclude": [
    "node_modules",
    "dist",
    "static"
  ],
}

处理字体和媒体文件

处理webpack.base.js

jsx 复制代码
// webpack.base.js
module.exports = {
  module: {
    rules: [
      // ...
      {
        test:/.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
        type: "asset", // type选择asset
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 小于10kb转base64位
          }
        },
        generator:{ 
          filename:'static/fonts/[name][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][ext]', // 文件输出目录和命名
        },
      },
    ]
  }
}

三、优化构建速度

❌分析打包速度

使用less后,分析打包速度报错。

  1. 装包pnpm i speed-measure-webpack-plugin -D
  2. 新建config/webpack.analy.js, 这样不会影响正常的开发、线上
jsx 复制代码
const prodConfig = require('./webpack.prod.js') // 引入打包配置
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); // 引入webpack打包速度分析插件
const smp = new SpeedMeasurePlugin(); // 实例化分析插件
const { merge } = require('webpack-merge') // 引入合并webpack配置方法

// 使用smp.wrap方法,把生产环境配置传进去,由于后面可能会加分析配置,所以先留出合并空位
module.exports = smp.wrap(merge(prodConfig, {

}))
  1. 配置打包命令
jsx 复制代码
  "scripts": {
    "start": "webpack serve  -c ./config/webpack.dev.js",
    "analy": "webpack -c ./config/webpack.analy.js"
  },
  1. 结果 - 使用less报错


缩小loader作用范围

  1. 一般第三库都是已经处理好的, 不需要再用loader去解析,可节省时间。

配置webpack.base.js

jsx 复制代码
const path = require('path')
module.exports = {
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/,
        // 缩小范围
        include: [path.resolve(__dirname, '../src')], 只对项目src文件的ts,tsx进行loader解析
      }
    ]
  }
}

🚩开启缓存

  1. webpack5内置缓存插件,可缓存生成的webpack模块和chunk,提速90%
  2. 缓存位置:node_modules/.cache/webpack,区分development和production缓存
jsx 复制代码
// webpack.base.js
// ...
module.exports = {
  // ...
  cache: {
    type: 'filesystem', // 使用文件缓存
  },
}

开启多线程loader

  1. 由于thread-loader不支持抽离css插件MiniCssExtractPlugin.loader(下面会讲),所以这里只配置了多进程解析js
  2. loader 放其他 loader 之前。放在此后的 loader 会在一个独立的 worker 池中运行
  3. 注意:开启多线程需要启动时间, 约600ms左右,所以适合规模大的项目。
  1. 先装包 pnpm i thread-loader -D
  2. 配置webpack.base.js
jsx 复制代码
module.exports = {
  module: {
    rules: [
      {
        test: /.(ts|tsx)$/,
        use: ['thread-loader', 'babel-loader']
      }
    ]
  }
}

其他优化配置

除了上面的配置外,webpack还提供了其他的一些优化方式,本次搭建没有使用到,所以只简单罗列下:

  • externals: 外包拓展,打包时会忽略配置的依赖,会从上下文中寻找对应变量;
  • module.noParse: 匹配到设置的模块,将不进行依赖解析,适合jquery,boostrap这类不依赖外部模块的包;
  • ignorePlugin: 可以使用正则忽略一部分文件,常在使用多语言的包时可以把非中文语言包过滤掉;

四、优化构建结果

分析打包体积

  1. pnpm start和build时都会自动打开 包体积分析页面 http://127.0.0.1:8888/
  2. 不用时注释掉 **new BundleAnalyzerPlugin() **即可
  1. 装包pnpm install webpack-bundle-analyzer -D
  2. 配置webpack.base.js
javascript 复制代码
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

🚩提取第三方包和公共模块

  1. cacheGroups可继承外边的属性
  2. 注意优先级

配置webpack.prod.js

javascript 复制代码
module.exports = {
  // ...
  optimization: {
    // ...
    // 提取第三方包和公共模块
    splitChunks: { 
      chunks: 'all',
      minSize: 10, // 提取代码体积大于10就提取出来
      cacheGroups: {
        vendors: { // 提取node_modules代码
          name: 'vendors', // 名称.hash.js
          test: /[\\/]node_modules[\\/]/,  // 只匹配node_modules里面的模块
          minChunks: 1, // 只要使用一次就提取出来
          priority: -10, // 注意优先级,如果过高下边不生效
          reuseExistingChunk: true,
        },
        commons: { // 提取页面公共代码
          name: 'commons', // 提取文件命名为commons
          minChunks: 2, // 只要使用两次就提取出来
          priority: -20, // 注意优先级,如果过高下边不生效
          reuseExistingChunk: true,
        },
      }
    }, 
  }
}

如何抽离react、antd, 接上方

javascript 复制代码
      cacheGroups: {
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)/,
          name: 'react',
          reuseExistingChunk: true,
        },
        antd: {
          name: 'antd',
          test: /[\\/]node_modules[\\/](antd)/,
          reuseExistingChunk: true,
        },
      }

清理无用的css

有风险,暂且不用

组件懒加载,样式懒加载

  1. 提取第三方包和公共模块
  2. react 再使用 lazy和Suspense即可做到组件懒加载
  3. 如何懒加载css呢,看下方
javascript 复制代码
import React, { lazy, Suspense, useState } from 'react'
const LazyDemo = lazy(() => import('@/components/LazyDemo')) // 使用import语法配合react的Lazy动态引入资源  
export default function App() {
  const [show, setShow] = useState(false)

  // 点击事件中动态引入css, 设置show为true  
  const onClick = () => {
    import("./app.css")
    setShow(true)
  }

  return (
    <>
      <h2 onClick={onClick}>展示</h2>
      {show && <Suspense fallback={<span>加载中</span>}><LazyDemo /></Suspense>}
    </>
  )
}

预加载

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

**rel的属性值**

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

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

语法:

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

详细代码测试

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

javascript 复制代码
// 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

javascript 复制代码
import React, { lazy, Suspense, useState } from 'react'

const PreFetchDemo = lazy(() => import(
  /* webpackChunkName: "PreFetchDemo" */
  /*webpackPrefetch: true*/
  '@/components/PreFetchDemo'
))

const PreloadDemo = lazy(() => import(
  /* webpackChunkName: "PreloadDemo" */
  /*webpackPreload: true*/
  '@/components/PreloadDemo'
  ))

function App() {
  const [ show, setShow ] = useState(false)
  
  return (
    <>
      <h3 onClick={() => setShow(true)}>预加载:点击后加载{show ? 1 : 0}</h3>
      { show && (
        <>
          <Suspense fallback={null}><PreloadDemo /></Suspense>
          <Suspense fallback={null}><PreFetchDemo /></Suspense>
        </>
       )}
    </>
  )
}
export default App

结果

  1. 点击显示按钮前,只看到了提前加载prefetch文件,没有看到preload
  2. 使用lazy,点击显示后回去加载js文件

开启gzip压缩

  1. 可压缩掉70%的体积
  2. 其他知识:nginx可开启gzip压缩,但只在nginx开,每次请求时都对资源进行压缩,压缩需要时间和占用服务器cpu资源,更好的方式是前端在打包的时候直接生成gzip资源,服务器接收到请求,可以直接把对应压缩好的gzip文件返回给浏览器,节省时间和cpu。
  1. 装包 pnpm i compression-webpack-plugin -D
  2. 配置 webpack.prod.js
javascript 复制代码
const CompressionPlugin = require('compression-webpack-plugin')

module.exports = merge(base, {
  plugins: [
    // 开启gzip压缩
    new CompressionPlugin({
      test: /.(js|css)$/, // 只生成css,js压缩文件
      filename: '[path][base].gz', // 文件命名
      algorithm: 'gzip', // 压缩格式,默认是gzip
      test: /.(js|css)$/, // 只生成css,js压缩文件
      threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
      minRatio: 0.8 // 压缩率,默认值是 0.8
    })
  ]
})

合理配置打包文件hash

  1. webpack的hash有3种类
    1. hash: 跟整个项目相关,所有文件共用一个hash。一个文件改动,所有文件的hash都会动。
    2. chunkhash: 影响范围:文件+依赖,依赖、文件变,则对应都变。-小连坐。
    3. contenthash: 只与文件自身有关。
  2. js-chunkhash
  3. css和图片-contenthash

按需加载

  1. 默认已按需加载,antd只用button 37k,多用几个到100+k
  2. lodash: 使用lodash-es来代替loadsh,只用cloneDeep, 打包后只有13k。

四、项目规范

可参考之前打文章。

五、进阶

区分开发、测试、线上环境

简洁版 - 用于小项目

crossEnv + DefinePlugin,就可以在代码中使用

  1. 先装包pnpm i cross-env dotenv-webpack -D
  2. 在package.json 的命令中 添加 env变量
jsx 复制代码
"scripts": {
  "start": "cross-env NODE_ENV=development webpack serve -c ./config/webpack.dev.js",
  "start:test": "cross-env NODE_ENV=test webpack serve -c ./config/webpack.dev.js",
  "start:prod": "cross-env NODE_ENV=production webpack serve  -c ./config/webpack.dev.js",
  "build:test": "cross-env NODE_ENV=test webpack -c ./config/webpack.prod.js",
  "build:prod": "cross-env NODE_ENV=production webpack -c ./config/webpack.prod.js",
}
  1. 在webpack.base.js中添加 DefinePlugin,注意Json中的 process.env.xxx,和命令中的名字保持一致
jsx 复制代码
const { DefinePlugin } = require('webpack');

module.exports = {
  plugins: [
    new DefinePlugin({ 
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 
    }),
  ]
}
  1. 使用,随便找个组件测试
jsx 复制代码
console.log('==http:NODE_ENV,', process.env.NODE_ENV);

使用dotenv配置文件 - 大项目

dotenv-webpack 整合了dotenv和DefinePlugin,所以不再DefinePlugin。

  1. 装包pnpm i dotenv-webpack -D
  2. 在webpack.base.js中添加 Dotenv
jsx 复制代码
const Dotenv = require('dotenv-webpack');

module.exports = {
  plugins: [
    new Dotenv({ path: path.resolve(__dirname, '..', `.env.${process.env.NODE_ENV}`) }),
  ]
}
  1. env文件
jsx 复制代码
// .env.development
REACT_APP_ENV=development
REACT_APP_BASEURL=/api

// .env.test
REACT_APP_ENV=test
REACT_APP_BASEURL=/api/test

// .env.production
REACT_APP_ENV=production
REACT_APP_BASEURL=/api/production

六、问题

css_module不生效

  1. 版本问题,将css-loader,从v7 => "^5.2.7"
  2. 普通的css又不生效, 普通less和module.less分开处理 - 参考cra

二级页面不展示

背景:刷新页面,空白

解决方案:在webpack.base的output添加 publicPath: '/', 即可

❌如何解决process报错的问题

  1. 背景:组件中单独打印process会报错

console.log('process: ', process); // 报错

** console.log('====http:NODE_ENV,', process.env.NODE_ENV); // 不报错,应为用了dotenv**
console.log('process.ENV,', process.env.NODE_ENV); // 不报错

注意版本的坑

  1. 处理跨域:webpack-dev-server 需要降级 "4.15.1",
  2. css_module不生效,将css-loader,从v7 => "^5.2.7"
相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js