背景
好多前端童鞋工作多年依然不会使用webpack搭建react脚手架,本文就介绍下如何从零开始搭建一个属于你自己的前端脚手架,提高自己的工程化实力,同时也提高团队的开发效率。
一、基础配置
目标:可以启动最简单的react项目
初始化项目、安装依赖
- 初始化项目 npm init -y
- 然后按下方的目录结构创建文件
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
- 添加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>
- 添加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"
]
}
- 添加src/App.tsx
javascript
import React from 'react';
export default function App() {
return (
<div>App</div>
)
}
- 添加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公共配置
- 配置入口出口
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,找不到资源
},
}
- 配置loader解析ts, jsx
javascript
pnpm i babel-loader @babel/core @babel/preset-env core-js @babel/preset-react @babel/preset-typescript -D
- babel-loader: 使用 babel 加载最新js代码并将其转换为 ES5
- @babel/corer: babel 编译的核心包
- @babel/preset-env: babel 编译的预设,可以转换目前最新的js标准语法
- 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'
],
}
};
- 配置路径别名, 省略文件后缀名
**extensions **作用:在引入模块时不带文件后缀时
javascript
module.exports = {
// ...
resolve: {
extensions: ['.js', '.tsx', '.ts', '.json'], // 解析模块时,可以省略的扩展名
},
}
- 添加html-webpack-plugin
作用:处理html的生成和管理,自动引入js,css。
- 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开发环境配置
开发是需要在内存中打包,速度快。
- 配置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文件夹
},
},
});
- package.json添加启动脚本
javascript
"scripts": {
"start": "webpack serve -c ./config/webpack.dev.js",
}
至此,可看到项目启动起来了
webpack打包环境配置
- 处理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和压缩代码,以及其他优化
})
- 在package.json 的scripts 中添加build打包命令
javascript
"scripts": {
"build": "webpack -c ./config/webpack.prod.js",
},
- 如何使用浏览器查看打包后的项目
打包后的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
- 将某个插件全局化,比如react,这样组件中就不用导入了。
- webpack自带插件ProvidePlugin来处理
背景
解决方案:配置webpack.base
javascript
const { ProvidePlugin } = require('webpack');
module.exports = {
plugins: [
new ProvidePlugin({
React: path.resolve(__dirname, '../', 'node_modules/react/index.js'),
})
]
}
配置路径别名
- 处理webpack.base.js
jsx
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, '../', 'src'),
},
}
}
- 处理tsconfig.json, 添加baseUrl,paths
javascript
{
"compilerOptions": {
// ...
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
}
}
复制public文件夹资源
背景:组件中用public的图片,打包后不显示
原因:打包时不会将public的资源放入dist
解决方案:用插件将public资源复制到dist
- 先装包
pnpm i copy-webpack-plugin -D
- 配置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
- 开发环境
javascript
// webpack.dev.js
module.exports = {
mode: 'development', // development:开发环境,内存打包 production:生产环境,硬盘打包
devtool: 'eval-cheap-module-source-map', // 生成map文件,方便调试
}
- 生产环境
javascript
// webpack.prod.js
module.exports = {
mode: 'development', // development:开发环境,内存打包 production:生产环境,硬盘打包
devtool: 'source-map', // 生成map文件,方便调试
}
处理样式
- 从后往前处理,当webpck遇到css时,先用css-loader加载解析返回内容给style-loader
- style-loader: 将css作为内部样式插head标签中
- 用mini-css-extract-plugin抽离css,一般只在生产环境配置。
- 总结:开发时用内部样式,上线时用外部样式。
- 先装包
javascript
pnpm i css-loader style-loader mini-css-extract-plugin -D
- 配置webpack.base.js
javascript
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
]
}
}
- 上线时抽离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
- 抽离css后,不会自动压缩,需要处理
- 装包
pnpm i css-minimizer-webpack-plugin -D
- 配置webpack.prod.js
javascript
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = merge(base, {
// ...
optimization: {
minimizer: [
// 压缩css
new CssMinimizerPlugin(),
],
},
});
压缩js
- webpack在mode=prod时默认会压缩js, 但是手动压缩css后,默认压缩js会失效
解决方案
- 装包
pnpm i terser-webpack-plugin -D
- 配置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前缀兼容
- 先装包
javascript
pnpm i postcss-loader autoprefixer -D
- 修改webpack.base.js
javascript
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /.(css|less)$/, //匹配 css和less 文件
use: [
// ...
'postcss-loader',
'less-loader',
]
},
]
},
// ...
}
- 配置postcss
根目录新建postcss.config.js,postcss会自动读取配置
javascript
module.exports = {
plugins: ['autoprefixer']
}
- 配置要兼容的浏览器和版本
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。
- 先装包
pnpm i babel-plugin-react-css-modules postcss-less -D
- 配置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']
},
]
}
- 配置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
-
普通的css又不生效, 普通less和module.less分开处理 - 参考cra
-
prod不做处理,即
-
配置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,
},
]
}
- 处理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热更新
- form修改文案,会热更新-浏览器自动刷新 ,input中的状态丢失。
- 期望:浏览器不刷新的热更新,保留form状态。
- 先装包
pnpm i @pmmmwh/react-refresh-webpack-plugin react-refresh -D
- 配置webpack.dev.js
jsx
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = merge(base, {
plugins: [
new ReactRefreshWebpackPlugin(), // react热更新
]
});
处理图片
一、webpack5已内置图片处理
- 添加静态文件声明来处理找不到模块或其类型声明的报错。
二、webpack4的方案:已淘汰(需装包)
- file-loader,将图片整到内存中,build时可看到图片,但是名字变了。
- url-loader,将图片转化为base64,build时看不到图片,打到了js中
- 配置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' }
]
}
- 添加类型声明文件 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'
- 配置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后,分析打包速度报错。
- 装包
pnpm i speed-measure-webpack-plugin -D
- 新建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, {
}))
- 配置打包命令
jsx
"scripts": {
"start": "webpack serve -c ./config/webpack.dev.js",
"analy": "webpack -c ./config/webpack.analy.js"
},
- 结果 - 使用less报错
缩小loader作用范围
- 一般第三库都是已经处理好的, 不需要再用
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解析
}
]
}
}
🚩开启缓存
- webpack5内置缓存插件,可缓存生成的webpack模块和chunk,提速90%
- 缓存位置:node_modules/.cache/webpack,区分development和production缓存
jsx
// webpack.base.js
// ...
module.exports = {
// ...
cache: {
type: 'filesystem', // 使用文件缓存
},
}
开启多线程loader
- 由于
thread-loader
不支持抽离css
插件MiniCssExtractPlugin.loader
(下面会讲),所以这里只配置了多进程解析js
loader
放其他loader
之前。放在此后的loader
会在一个独立的worker
池中运行- 注意:开启多线程需要启动时间, 约
600ms
左右,所以适合规模大的项目。
- 先装包
pnpm i thread-loader -D
- 配置webpack.base.js
jsx
module.exports = {
module: {
rules: [
{
test: /.(ts|tsx)$/,
use: ['thread-loader', 'babel-loader']
}
]
}
}
其他优化配置
除了上面的配置外,webpack
还提供了其他的一些优化方式,本次搭建没有使用到,所以只简单罗列下:
- externals: 外包拓展,打包时会忽略配置的依赖,会从上下文中寻找对应变量;
- module.noParse: 匹配到设置的模块,将不进行依赖解析,适合
jquery
,boostrap
这类不依赖外部模块的包; - ignorePlugin: 可以使用正则忽略一部分文件,常在使用多语言的包时可以把非中文语言包过滤掉;
四、优化构建结果
分析打包体积
- pnpm start和build时都会自动打开 包体积分析页面 http://127.0.0.1:8888/
- 不用时注释掉 **new BundleAnalyzerPlugin() **即可
- 装包
pnpm install webpack-bundle-analyzer -D
- 配置webpack.base.js
javascript
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
🚩提取第三方包和公共模块
- cacheGroups可继承外边的属性
- 注意优先级
配置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
有风险,暂且不用
组件懒加载,样式懒加载
- 提取第三方包和公共模块
- react 再使用 lazy和Suspense即可做到组件懒加载
- 如何懒加载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
属性prefetch
与preload
,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
结果
- 点击显示按钮前,只看到了提前加载prefetch文件,没有看到preload
- 使用lazy,点击显示后回去加载js文件
开启gzip压缩
- 可压缩掉70%的体积
- 其他知识:nginx可开启gzip压缩,但只在nginx开,每次请求时都对资源进行压缩,压缩需要时间和占用服务器cpu资源,更好的方式是前端在打包的时候直接生成gzip资源,服务器接收到请求,可以直接把对应压缩好的gzip文件返回给浏览器,节省时间和cpu。
- 装包
pnpm i compression-webpack-plugin -D
- 配置 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
- webpack的hash有3种类
- hash: 跟整个项目相关,所有文件共用一个hash。一个文件改动,所有文件的hash都会动。
- chunkhash: 影响范围:文件+依赖,依赖、文件变,则对应都变。-小连坐。
- contenthash: 只与文件自身有关。
- js-chunkhash
- css和图片-contenthash
按需加载
- 默认已按需加载,antd只用button 37k,多用几个到100+k
- lodash: 使用lodash-es来代替loadsh,只用cloneDeep, 打包后只有13k。
四、项目规范
可参考之前打文章。
五、进阶
区分开发、测试、线上环境
简洁版 - 用于小项目
crossEnv + DefinePlugin,就可以在代码中使用
- 先装包
pnpm i cross-env dotenv-webpack -D
- 在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",
}
- 在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)
}),
]
}
- 使用,随便找个组件测试
jsx
console.log('==http:NODE_ENV,', process.env.NODE_ENV);
使用dotenv配置文件 - 大项目
dotenv-webpack 整合了dotenv和DefinePlugin,所以不再DefinePlugin。
- 装包
pnpm i dotenv-webpack -D
- 在webpack.base.js中添加 Dotenv
jsx
const Dotenv = require('dotenv-webpack');
module.exports = {
plugins: [
new Dotenv({ path: path.resolve(__dirname, '..', `.env.${process.env.NODE_ENV}`) }),
]
}
- 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不生效
- 版本问题,将css-loader,从v7 => "^5.2.7"
- 普通的css又不生效, 普通less和module.less分开处理 - 参考cra
二级页面不展示
背景:刷新页面,空白
解决方案:在webpack.base的output添加 publicPath: '/', 即可
❌如何解决process报错的问题
- 背景:组件中单独打印process会报错
console.log('process: ', process); // 报错
** console.log('====http:NODE_ENV,', process.env.NODE_ENV); // 不报错,应为用了dotenv**
console.log('process.ENV,', process.env.NODE_ENV); // 不报错
注意版本的坑
- 处理跨域:webpack-dev-server 需要降级 "4.15.1",
- css_module不生效,将css-loader,从v7 => "^5.2.7"