React18+Webpack5搭建React-cli(js+ts版本)

516 阅读6分钟

React18+Webpack5搭建React-cli

1. 主要目录解析

react-cli # 项目根目录
├── package.json
├── public
│   ├── favicon.ico # 网站页签图标
│   ├── index.html # 主页面
└── src
│   ├── page # 项目页面文件夹
│   ├── app.jsx # App组件
│   ├── main.js # 入口文件
└── config
    ├── webpack.config.js
    ├── webpack.dev.js
    ├── webpack.prod.js

2. 基本配置

module.exports = {
  // 入口
  entry: "",
  // 输出
  output: {},
  // 加载器
  module: {
    rules: [],
  },
  // 插件
  plugins: [],
  // 模式
  mode: "",
};

Webpack 是基于 Node.js 运行的,所以采用 Common.js 模块化规范

3. 安装webpack

yarn add webpack webpack-cli -D

4. 处理样式与加载器

安装依赖

yarn add less less-loader sass sass-loader style-loader css-loader -D
// 返回处理样式loader函数
const getStyleLoaders = (pre) => {
  return [
    'style-loader',
    'css-loader',
    {
      // 处理css兼容性问题
      // 配合package.json中browserslist来指定兼容性
      loader: 'postcss-loader',
    },
    pre,
  ].filter(Boolean);
};

module: {
  rules: [
    // 处理css
    {
      test: /\.css$/,
      use: getStyleLoaders(),
    },
    // 处理less
    {
      test: /\.less$/,
      use: getStyleLoaders('less-loader'),
    },
    // 处理sass
    {
      test: /\.s[ac]ss$/,
      use: getStyleLoaders('sass-loader'),
    },
 ],
},

在项目根目录创建 postcss.config.js 文件,并添加以下代码:

module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

Webpack 就会自动处理样式兼容性和添加前缀

5. 处理图片资源

module: {
  rules: [
    // 处理图片文件
    {
      test: /\.(png|jpe?g|gif|webpp)$/,
      type: 'asset',
      parser: {
        dataUrlCondition: {
          maxSize: 16 * 1024, // 小于16kb的图片会被base64处理
        },
      },
      generator: {
        // 将图片文件输出到 static/imgs 目录中
        // 将图片文件命名 [hash:8][ext][query]
        // [hash:8]: hash值取8位
        // [ext]: 使用之前的文件扩展名
        // [query]: 添加之前的query参数
        filename: 'static/imgs/[hash:8][ext][query]',
      },
    },
  ],
},

6. 处理其他资源

module: {
  rules: [
    {
      test: /\.(ttf|woff2?|map4|map3|avi)$/,
      type: 'asset/resource',
      generator: {
        filename: 'static/media/[hash:9][ext][query]',
      },
    },
  ],
},

7. 处理js、jsx资源

  1. Babel 是一个 JavaScript 编译器,可以将 ES6+转化成ES5。

安装依赖

yarn add babel-loader @babel/core @babel/preset-env @babel/preset-react -D

babel-loader是webpack和Babel之间的桥梁,它将Babel与webpack集成。

@babel/core是Babel的核心库。

@babel/preset-env@babel/preset-react是预设,用于指定Babel要如何转换代码

rules: [
  {
    // 处理JSX语法
    test: /\.jsx?$/,
    exclude: /node_modules/,
    use: {
      loader: "babel-loader",
      options: {
        presets: ["@babel/preset-env", "@babel/preset-react"]
      }
    }
  }
]
  1. 激活js的HMR

修改App.jsx,浏览器会自动刷新后再显示修改后的内容,我们需要在不刷新浏览器的前提下模块热更新,并且能够保留react组件的状态。

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

yarn add react-refresh @pmmmwh/react-refresh-webpack-plugin -D
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  // ...
  module: {
    rules: [
      {
        // 处理JSX语法
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
            plugins: ['react-refresh/babel'],
          },
        },
      },
    ]
  },
  plugins: [
    new ReactRefreshWebpackPlugin()
  ],
  devServer: {
    hot: true, // 开启HMR
    liveReload:true // 开启浏览器自动刷新功能
  }
};

8. 解析模块加载器选项

/* 解析模块加载器选项 */
resolve: {
  // 自动补全文件扩展名
  extensions: ['.jsx', '.js', '.json'],
},

9. 配置路径别名

resolve: {
  // ...
  // 配置路径别名
  alias: {
    '@src': path.resolve(__dirname, '/src'),
  },
},

在项目根目录下创建 jsconfig.json 文件,并添加以下内容

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

10. 自动拆分代码块splitChunks

optimization.splitChunks 是 Webpack 中用于代码分割的优化配置项.

optimization.runtimeChunk,可以将运行时代码块从代码块中拆分出来,生成一个独立的代码块。这样做有如下好处:

  • 代码体积更小:独立的运行时代码块可以被浏览器缓存,减少每个代码块的大小,降低网络传输的成本。
  • 缓存效果更佳:运行时代码块不受业务代码影响,只有当 Webpack 升级或配置发生变化时才会变化,因此可以更好地利用浏览器缓存。

通过适当地配置 optimization.runtimeChunk,可以实现更好的代码优化效果,提高页面加载速度。

/* 优化配置 */
optimization: {
  splitChunks: {
    chunks: 'all',
  },
  runtimeChunk: {
    // 这样可以为每个入口生成一个独立的运行时代码块
    name: (entrypoint) => `runtime~${entrypoint.name}.js`,
  },
},

11. 安装React相关依赖

yarn add react react-dom react-router-dom -S

/src/App.jsx

import React, { lazy, Suspense } from 'react';
import { Link, Route, Routes } from 'react-router-dom';
// import About from './page/About/About';
// import Home from './page/Home/Home';

const About = lazy(() => import(/* webpackChunkName: 'About' */'./page/About/About'));
const Home = lazy(() => import(/* webpackChunkName: 'Home' */'./page/Home/Home'));

const App = () => {
  return (
    <div>
      <h1>Hello React</h1>

      <ul>
        <li>
          <Link to="/home">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>

      <Suspense fallback={'loading......'}>
        <Routes>
          <Route path={"/"} element={<Home />} />
          <Route path={"/home"} element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </div>
  );
};

export default App;

/src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import APP from './App.jsx';

const root = ReactDOM.createRoot(document.getElementById('app'));

root.render(
  <BrowserRouter>
    <APP />
  </BrowserRouter>
);

/public/index.html

<!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="shortcut icon" href="favicon.ico" type="image/x-icon">
  <title>React Cli</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

12.配置devServer

/* 开发服务器 */
devServer: {
  host: 'localhost',
  port: 9527,
  open: true,
  hot: true, // 开启热更新
  // liveReload: false, // 禁用浏览器自动刷新功能
  historyApiFallback: true, // 解决前端路由刷新404问题
},

package.json

"scripts": {
  "start": "npm run dev",
  "dev": "webpack serve --config ./config/webpack.dev.js"
},

最后yarn start image.png

13.优化配置

13.1. 提取CSS成一个单独的文件

安装依赖:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  // ... 其他配置 ...

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:10].css',
      chunkFilename: 'static/css/[name].[contenthash:10].chunk.css',
    }),
  ]
};

13.2. 压缩CSS、JS

const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
// TerserWebpackPlugin不需要安装,webpack已集成
const TerserWebpackPlugin = require('terser-webpack-plugin');

module.exports = {
  // ... 其他配置 ...

  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },

  /* 优化配置 */
  optimization: {
    minimizer: [
      new CssMinimizerWebpackPlugin(), 
      new TerserWebpackPlugin()
    ],
  },
};

13.4. 压缩图片

yarn add image-webpack-loader imagemin-webpack-plugin -D
  • file-loader:将文件输出到指定的目录,并返回相对路径
  • url-loader:与file-loader类似,但可以将文件转换为Base64编码,减少请求次数
  • image-webpack-loader:加载和压缩图片
  • imagemin-webpack-plugin:在打包过程中进一步压缩图片
const path = require('path');
const ImageminPlugin = require('imagemin-webpack-plugin').default;

module.exports = {
  // ... 其他配置 ...

  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif)$/i,
        type: 'asset/resource', // 用asset模块类型处理图片
        generator: {
          filename: 'images/[name].[hash:8][ext]', // 输出文件名,使用哈希值
        },
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              disable: process.env.NODE_ENV === 'development', // 开发模式下不压缩图片
            },
          },
        ],
      },
    ],
  },

  plugins: [
    new ImageminPlugin({
      test: /\.(png|jpe?g|gif)$/i,
    }),
  ],
};

14.复制文件

const CopyPlugin = require("copy-webpack-plugin");
const path = require('path');
const ImageminPlugin = require('imagemin-webpack-plugin').default;

module.exports = {
  // ... 其他配置 ...
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          // 打包public文件夹
          from: path.resolve(__dirname, "../public"),
          to: path.resolve(__dirname, "../dist"),
          globOptions: {
            // 忽略index.html文件
            ignore: ["**/index.html"],
          },
        },
      ],
    }),
  ],
};

15. 打包优化optimization

  • minimize: 是否对打包后的代码进行压缩,默认值为 true。

  • minimizer: 配置压缩器,可以使用 UglifyJsPlugin、TerserPlugin 等插件来进行压缩。

  • splitChunks: 配置代码分离,可以将多个入口文件共同使用的代码提取出来,形成一个共享的代码块(chunk),从而减小整体的文件大小,提高页面加载速度。

    • chunks: 决定哪些块会被优化,默认值为 "async",表示只对异步加载的块进行优化。还可以设置为 "all",表示对所有块进行优化,或者设置为函数,以返回 true 或 false 来决定哪些块会被优化。
    • cacheGroups 是一个对象,其属性名是缓存组的名称,属性值是一个对象,包含如下属性:
      • test: 用于匹配符合要求的模块。
      • priority: 用于设置缓存组的优先级,值越大表示优先级越高。默认值为 0。
      • reuseExistingChunk: 如果当前的模块已经被打包到一个共享代码块中,可以设置为 true,表示重用这个共享代码块。默认值为 true。
      • enforce: 设置为 true,表示将模块强制打包到当前的缓存组中,而不是被其他的缓存组复用。默认值为 false。
  • runtimeChunk: 配置运行时代码的分离,可以将运行时代码提取出来,形成一个单独的代码块,避免重复打包。

optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      // react react-dom react-router-dom 一起打包成一个js文件
      react: {
        test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
        name: 'chunk-react',
        priority: 40, // 权重 优先级
      },
      // antd 单独打包
      antd: {
        test: /[\\/]node_modules[\\/]antd[\\/]/,
        name: 'chunk-antd',
        priority: 30,
      },
      // 剩下的node_modules单独打包
      libs: {
        test: /[\\/]node_modules[\\/]/,
        name: 'chunk-libs',
        priority: 20,
      },
    },
  },
  runtimeChunk: {
    name: (entrypoint) => `runtime~${entrypoint.name}.js`,
  },
  // 是否需要进行压缩
  minimize: isProduction,
  minimizer: [new CssMinimizerWebpackPlugin(), new TerserWebpackPlugin()],
},

16. 代码质量

16.1. 集成eslint+preitter

npm install eslint eslint-config-prettier eslint-plugin-prettier prettier -D

新建.eslintrc.js文件

module.exports = {
  env: {
    browser: true, // 允许在浏览器环境中使用全局变量,如document和window
    es2021: true, // 启用对 ES2021 的支持
    node: true, // 允许在Node.js环境中使用全局变量,如process和__dirname
  },
  extends: [
    'eslint:recommended', // 使用 ESLint 推荐的规则
    'plugin:react/recommended', // 使用 React 推荐的规则
    'plugin:react-hooks/recommended', // 使用 React Hooks 推荐的规则
    'plugin:prettier/recommended', // 使用 Prettier 推荐的规则
  ],
  ignorePatterns: ['/node_modules/**', '/build/**', '/dist/**'],
  overrides: [],
  // 指定解析器选项
  parserOptions: {
    // 启用 ECMAScript 模块
    sourceType: 'module',
    // 允许使用 import 语句
    ecmaVersion: 2021,
    // JSX 解析器配置
    ecmaFeatures: {
      jsx: true,
    },
  },
  plugins: ['react', 'react-hooks', 'prettier'],
  rules: {
    'no-unused-vars': 'warn', // 未使用变量的警告
    'no-undef': 'error', // 未定义变量的错误
    'no-console': 'warn', // 禁止使用 console 的警告
    'no-use-before-define': 'error', // 不允许在定义之前使用
    'react/jsx-no-undef': 'error', // 未定义 JSX 元素的错误
    'react/react-in-jsx-scope': 'off', // 不需要引入 React 就可以使用 JSX
    'react-hooks/rules-of-hooks': 'error', // 使用 Hooks 规则的错误
    'react-hooks/exhaustive-deps': 'warn', // useEffect 依赖项检查的警告
    'prettier/prettier': 'error', // 使用 Prettier 推荐的格式化规则
  },
};

新建.prettierrc.js文件

module.exports = {
  // 一行的字符数,如果超过会进行换行,默认为 80
  printWidth: 80,
  // 一个 tab 代表几个空格数,默认为 2
  tabWidth: 2,
  // 是否使用 tab 进行缩进,默认为 false,表示用空格进行缩减
  useTabs: false,
  // 行尾是否需要分号,默认为 true
  semi: true,
  // 是否使用单引号,默认为 false,使用双引号
  singleQuote: true,
  // jsx 是否使用单引号,默认为 false,使用双引号
  jsxSingleQuote: true,
  // 末尾是否需要逗号,有三个可选值 "<none|es5|all>"
  // "<none>" 代表不需要逗号
  // "<es5>" 代表ES5中需要逗号
  // "<all>" 代表所有对象最后一个属性添加逗号
  trailingComma: 'all',
  // 对象大括号直接是否有空格,默认为 true,效果:{ foo: bar }
  bracketSpacing: true,
  // jsx 标签的反尖括号是否在最后一行中间换行,默认为 false,效果:<br
  //   className=""
  // />
  jsxBracketSameLine: false,
  // 箭头函数,只有一个参数的时候,是否需要括号,默认为 avoid,有两个可选值 "avoid" 和 "always"
  arrowParens: 'avoid',
};
// 修复eslint
npx eslint --fix .
// 格式化代码
npx prettier --write .

16.2. 代码提交前检验并格式化代码

执行命令,生成.husky文件夹

npx mrm lint-staged

package.json中添加

"lint-staged": {
  "*.{js,jsx,ts,tsx}": "eslint --fix"
}

18. 切换为TS版本的React脚手架

18.1. 安装

yarn add typescript @babel/preset-typescript @types/react @types/react-dom -D

18.2. 修改webpack.config.js

module.exports = {
  // ... 其他配置 ...

  module: {
    rules: [
      {
        // 处理tsx语法
        test: /\.(ts|js)x?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
          },
        },
      },
    ],
  },
};

18.3. 删除jsconfig.json文件,添加tsconfig.json文件

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"]
    },
    // 指定TypeScript使用的模块系统。
    "module": "esnext",
    // 将ECMAScript目标版本设置为要编译的版本。
    "target": "es5",
    // 列出要在编译中包含的库文件。
    "lib": ["dom", "dom.iterable", "esnext"],
    // 是否生成源映射文件。
    "sourceMap": true,
    // 是否启用esModuleInterop编译选项。
    "esModuleInterop": true,
    // 是否允许编译器编译JavaScript文件。
    "allowJs": true,
    // 指定JSX输出格式。
    "jsx": "react-jsx",
    // 解析模块时要遵循的规则。
    "moduleResolution": "node",
    // 是否禁用输出。
    "noEmit": true,
    // 是否启用严格模式。
    "strict": true,
    // 是否启用独立模块编译模式。
    "isolatedModules": true,
    // 是否允许编译器解析JSON模块。
    "resolveJsonModule": true,
    // 是否启用noImplicitAny编译选项。
    "noImplicitAny": false
  },
  // 指定要包含在编译中的文件和目录。
  "include": ["src"]
}

18.4. 模块声明语句,告诉编译器如何处理特定类型的文件src/types/index.d.ts

declare module '*.svg' {
  const value: string;
  export default value;
}

declare module '*.bmp' {
  const value: string;
  export default value;
}

declare module '*.gif' {
  const value: string;
  export default value;
}

declare module '*.jpg' {
  const value: string;
  export default value;
}

declare module '*.jpeg' {
  const value: string;
  export default value;
}

declare module '*.png' {
  const value: string;
  export default value;
}

18.5. 配置eslint

module.exports = {
  parser: '@typescript-eslint/parser', // 指定 ESLint 解析器为 TypeScript
  settings: {
    react: {
      version: '18.2.0', // 指定react版本
    },
  },
  env: {
    browser: true, // 启用浏览器全局变量,例如 window 和 document
    es2021: true, // 启用 ES2021 功能,例如 async/await
    node: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended', // 启用推荐的 React 规则
    'plugin:@typescript-eslint/recommended', // 启用推荐的 TypeScript 规则
    'plugin:prettier/recommended', // 启用 Prettier 与 ESLint 的集成
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true, // 启用解析 JSX 语法
    },
    ecmaVersion: 12, // 指定 ECMAScript 版本
    sourceType: '', // 指定 ECMAScript 版本
  },
  plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier'], // 指定 ECMAScript 版本
  rules: {
    'no-undef': 'error', // 未定义变量的错误
    'no-console': 'warn', // 禁止使用 console 的警告
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-var-requires': 'off',
    'react/react-in-jsx-scope': 'off', // 关闭在 React 18 中不必要的警告
    'react/prop-types': 'off', // 关闭 prop-types 规则,如果你不使用 prop-types 库
    'react-hooks/rules-of-hooks': 'error', // 启用 React Hooks 规则
    'react-hooks/exhaustive-deps': 'warn', // 警告 React Hooks 中缺失的依赖项
    '@typescript-eslint/explicit-module-boundary-types': 'off', // 关闭函数需要显式返回类型的警告
    'prettier/prettier': 'error',
    eqeqeq: 'error', // 要求使用 === 和 !==
  },
  ignorePatterns: ['/node_modules/**', '/build/**', '/dist/**'],
  overrides: [
    {
      files: ['src/**/*.ts', 'src/**/*.tsx'],
      excludedFiles: 'node_modules/**',
      rules: {
        // 在这里添加只对src文件夹生效的规则
      },
    },
  ],
};

至此,大功告成

19. 拓展

..... 未完待续