react cli (开发模式配置)
开篇
废话不多说,我们直接上代码,使用webpack5搭建react 和 vue 脚手架,了解官方脚手架中做了什么事情,以及前端为什么需要打包工具。
入口
yarn init -y
安装webpack
yarn add webpack webpack-cli webpack-dev-server -D
安装 dependencies依赖
yarn add react react-dom react-router-dom antd
eslint
yarn add eslint
npx eslint --init
yarn add eslint-webpack-plugin -D
作用:让eslint和webpack结合起来,在 dev 环境和 build 环境都可以给出错误提示。
该插件使用 eslint 来查找和修复 JavaScript 代码中的问题。
在没有使用该插件之前,我们看到vscode编辑器里面会报错,但是我们dev启动的项目并没有报错,
当我们加上这个插件之后,再来看看效果。页面,编辑器,还有命令行都会有报错,所以用起来更加的安全了。
出口
mode
处理js、jsx
yarn add @babel/core @babel/cli @babel/preset-env babel-loader -D
编写loader处理.js|.jsx结尾的文件
到这,我们先来看看我们现在的文件目录结构。
命令行运行,就可以在浏览器中看见效果了。
.eslintrc.js
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
},
plugins: [
'react',
],
rules: {
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
},
};
config/webpack.dev.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: path.resolve(__dirname, '../src/main.js'),
module: {
rules: [
{
oneOf: [ // 表示当匹配到一个规则的时候,就不在匹配其他的了
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
include: path.resolve(__dirname, '../src'),
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开缓存
cacheCompression: false, // 关闭缓存压缩
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../public/index.html'),
}),
],
resolve: {
extensions: ['.js', '.jsx'],
},
mode: 'development',
devServer: {
open: true,
host: 'localhost',
port: 3000,
hot: true,
compress: true,
historyApiFallback: true,
},
};
.vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
"eslint.format.enable": true,
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
"prettier.enable": false
}
babel.config.js
module.exports = {
presets: ['@babel/preset-react'],
};
package.json,如果在运行的时候有包没安装的话,可在这个文件中找到,不用慌。
{
"name": "react-cli-study1",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "webpack server --config ./config/webpack.dev.js"
},
"devDependencies": {
"@babel/cli": "^7.17.10",
"@babel/core": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-react": "^7.17.12",
"babel-loader": "^8.2.5",
"eslint": "^7.32.0 || ^8.2.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.72.1",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.0"
},
"dependencies": {
"antd": "^4.20.6",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-router-dom": "^6.3.0"
}
}
src/main.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const root = createRoot(document.getElementById('app'));
root.render(<App />);
src/App.jsx
import React from 'react';
export default function App() {
return (
<h1>hello react</h1>
);
}
- eslint-webpack-plugin
yarn add cross-env -D
处理html
yarn add html-webpack-plugin -D
处理 css
yarn add postcss-loader postcss-preset-env css-loader style-loader less-loader sass-loader stylus-loader stylus less sass -D
config/webpack.dev.js
const getStyleLoaders = (preProcessor) => [
'style-loader',
'css-loader',
{
// 处理css兼容性问题
// 配合 browserslist 文件
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['postcss-preset-env'],
},
},
},
preProcessor,
].filter(Boolean);
{
test: /\.css$/,
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders('less-loader'),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders('sass-loader'),
},
{
test: /\.styl$/,
use: getStyleLoaders('stylus-loader'),
},
// 处理图片
{
test: /.(png|jpe?g|gif|svg)$/,
use: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // // 小于10kb的图片会被base64处理
},
},
},
// 处理字体
{
test: /\.(ttf|woff2?)$/,
type: 'asset/resource',
},
browserslist
根据提供的目标浏览器的环境来,智能添加css前缀,js的polyfill垫片,来兼容旧版本浏览器。避免不必要的兼容代码,以提高代码的编译质量。
共享使用browserslist的组件们:
| 组件名 | 功能vv |
|---|---|
| Autoprefixer | postcss添加css前缀组件 |
| bable-preset-env | 编译预设环境 智能添加polyfill垫片代码 |
| postcss-normalize | |
| 等等…… |
基础语法: 只要package.json配置了browserslist对象,需要的组件将自动匹配到并使用,也可以配置到具体的组件参数上
{ // package.json
"browserslist": [ // 注意:是一个数组对象
"> 1%",
"last 2 versions"
] }
或者
.browserslistrc
> 1%
last 2 versions
not dead
last 2 versions:CanIUse.com追踪的IE最新版本为11,向后兼容两个版本即为10、11
设置语法:通过浏览器过滤的思路实现
默认是兼容所有最近版本
支持的插件
Browserslist这个东西单独是没用的,(补充: 在vue官方脚手架中,browserslist字段会被 @babel/preset-env 和 Autoprefixer 用来确定需要转译的 JavaScript 特性和需要添加的 CSS 浏览器前缀。2019-7-22)下面的搭配的工具列表:
- Autoprefixer
- Babel
- postcss-preset-env
- eslint-plugin-compat
- stylelint-no-unsupported-browser-features
- postcss-normalize
了解更多请看这个list
或者npx browserslist
处理图片
devtool
devtool: "cheap-module-source-map",
optimization
devserver
devServer: {
open: true,
host: "localhost",
port: 3000,
hot: true,
compress: true,
historyApiFallback: true, // 解决react-router刷新404问题
}
resolve
webpack解析模块的配置
react cli (生产环境配置)
去掉dev-server、去掉HMR功能。
开启css js 压缩
开启图片压缩
css压缩提取
yarn add css-minimizer-webpack-plugin mini-css-extract-plugin -D
图片压缩
无损压缩
yarn add image-minimizer-webpack-plugin -D
yarn add imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo -D
react cli (合并配置)
// 合并配置
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
// 需要通过 cross-env 定义环境变量
const isProduction = process.env.NODE_ENV === "production";
const getStyleLoaders = (preProcessor) => {
return [
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor && {
loader: preProcessor,
options: preProcessor === 'less-loader' ? {
// antd的自定义主题
lessOptions: {
modifyVars: {
// 其他主题色:https://ant.design/docs/react/customize-theme-cn
"@primary-color": "#1DA57A", // 全局主色
},
javascriptEnabled: true
}
} : {},
}
].filter(Boolean);
};
module.exports = {
entry: "./src/main.js",
output: {
path: isProduction ? path.resolve(__dirname, "../dist") : undefined,
filename: isProduction
? "static/js/[name].[contenthash:10].js"
: "static/js/[name].js",
chunkFilename: isProduction
? "static/js/[name].[contenthash:10].chunk.js"
: "static/js/[name].chunk.js",
assetModuleFilename: "static/js/[hash:10][ext][query]",
clean: true,
},
module: {
rules: [
{
oneOf: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
},
},
},
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
},
{
test: /\.(jsx|js)$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: [
// "@babel/plugin-transform-runtime", // presets中包含了
!isProduction && "react-refresh/babel",
].filter(Boolean),
},
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
extensions: [".js", ".jsx"],
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
isProduction &&
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
!isProduction && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
optimization: {
// 是否需要压缩?
minimize: isProduction,
// 压缩的操作
minimizer: [
// 压缩css
new CssMinimizerPlugin(),
// 压缩js
new TerserWebpackPlugin(),
// 压缩图片
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],
// 代码分割配置
splitChunks: {
chunks: "all",
// 其他都用默认值
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
name: "chunk-react",
priority: 40
},
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: "chunk-antd",
priority: 30
},
libs: {
test: /[\\/]node_modules[\\/]/,
name: "chunk-libs",
priority: 20
}
}
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},
resolve: {
extensions: [".jsx", ".js", ".json"],
},
devServer: {
open: true,
host: "localhost",
port: 3000,
hot: true,
compress: true,
historyApiFallback: true,
},
mode: isProduction ? "production" : "development",
devtool: isProduction ? undefined : "cheap-module-source-map",
performance: false, // 关闭性能分析,提示速度
};
react cli (优化配置)
antd 自定义主题
类库单独打包
考虑到node_modules下面的代码打成一个包的时候文件体积会太大。所以将其分成几个包。
react react-dom react-router-dom 打成一个包
antd 打成一个包
剩下的打成一个包
runtimeChunk 的作用
我们先开启看看效果。
optimization: {
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
}
什么是运行时文件?
搜索文件得知
形如import('abc').then(res=>{})这种异步加载的代码,在webpack中即为运行时代码。
下面我们试一试最熟悉的路由懒加载,快速写下以下代码
main.js
import React from 'react';
import { createRoot } from "react-dom/client";
import './style/css/index.css'
import 'antd/dist/antd.less';
import { BrowserRouter } from 'react-router-dom'
import App from './App'
const root = createRoot(document.getElementById("app"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
App.jsx
import React, { lazy, Suspense } from "react";
import { Button } from "antd";
import { Routes, Route, Link } from "react-router-dom";
const About = React.lazy(() => import("./pages/About"));
const Home = lazy(() => import("./pages/Home"));
export default function App() {
return (
<>
<h1 className="h1-text-color">hello word 888</h1>
<Button type="primary">Primary Button</Button>
<Link to="/about">About</Link>
<Link to="/home">Home</Link>
<Routes>
<Route
path="/about"
element={
<Suspense fallback={<div>loading</div>}>
<About />
</Suspense>
}
/>
<Route
path="/home"
element={
<Suspense fallback={<div>loading</div>}>
<Home />
</Suspense>
}
/>
</Routes>
</>
);
}
同样先不开启runtimeChunk 、开启runtimeChunk、修改About.jsx文件。我们分别来对比结果
设置runtimeChunk是将包含
chunks 映射关系的 list单独从 app.js里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。设置runtimeChunk之后,webpack就会生成一个个 runtime~xxx.js的文件。然后每次更改所谓的运行时代码文件时,打包构建时app.js的hash值是不会改变的。如果每次项目更新都会更改app.js的hash值,那么用户端浏览器每次都需要重新加载变化的app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。
- 你以为这就完了?
1、查看下runtime~xxx.js文件内容:
(i.u = function (e) {
return "static/js/" + e + "." + { 229: "9acdd27ab0", 895: "147b4cdec2" }[e] + ".chunk.js";
}),
发现文件很小,且就是加载chunk的依赖关系的文件。虽然每次构建后app的hash没有改变,但是runtime~xxx.js会变啊。每次重新构建上线后,浏览器每次都需要重新请求它,它的 http 耗时远大于它的执行时间了,所以建议不要将它单独拆包,而是将它内联到我们的 index.html 之中。这边我们使用script-ext-html-webpack-plugin来实现。(也可使用html-webpack-inline-source-plugin,其不会删除runtime文件。)
这样重新打包,查看index.html文件
index.html中已经没有对runtime~xxx.js的引用了,而是直接将其代码写入到了index.html中,故不会在请求文件,减少http请求。
总结
runtimeChunk作用是为了线上更新版本时,充分利用浏览器缓存,使用户感知的影响到最低。