从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"
相关推荐
不吃鱼的羊17 小时前
DaVinci配置NVM模块
前端·javascript·网络
excel17 小时前
为什么需要构建工具(Webpack / Vite 的本质)
前端
lang2015092817 小时前
Java SAX 流式解析全解:从原理到 EasyExcel 实战
java·前端·javascript
Rain50917 小时前
2.4. PostgreSQL 数据库连接与实战指南
前端·数据库·人工智能·后端·postgresql·数据分析
console.log('npc')17 小时前
Codex 桌面端接入 Headroom 压缩代理完整教程
前端·vscode
独泪了无痕17 小时前
Vue集成uuid生成唯一标识实践指南
前端·vue.js
yuanyxh1 天前
Mac 软件推荐
前端·javascript·程序员
万少1 天前
AtomCode开发微信小程序《谁去呀》 全流程
前端·javascript·后端
某人辛木1 天前
Web自动化测试
前端·python·pycharm·pytest
Kagol1 天前
Superpowers GSD gstack AgentSkills深度测评
前端·人工智能