工厂流水线生产的东西用久了,总想着自己手工是否也能做出来,就如同工艺品和艺术品一般,虽然效果相似,但艺术品往往比工艺品更有韵味。
作为一名前端工程师,总是用一些脚手架来快速搭建新项目的基本结构,因此今天尝试着一步步搭建一个 React 的项目环境,看看需要处理哪些问题,查漏补缺!
一、需求分析
首先分析我们的诉求:
- 应用级别的项目,是需要支持打包构建。
- 需要考虑兼容性,支持代码 pollyfill。
- 支持 React 框架下的开发环境。
- 支持代码类型提示。
- 支持前端路由。
- 支持前端状态管理。
- 代码规范、自动格式化、Git 提交规范。
- 基础的 UI 组件库。
针对上面的诉求,其实也是绝大部分项目都会需要,因此也有了常见的解决方案:
- Webpack 5。
- Babel。
- React 17、React-dom。
- TypeScript。
- React-router-dom v6。
- Redux、React-redux。
- ESlint、Prettier、Lint-staged、Husky、@commitlint。
- Arco Design。
二、项目打包构建
因为是 2022 年了,所以我们的项目所有依赖项全部用最新的工具库版本,搞起来!
首先是把项目的基本构建能力搭建好,让项目先跑起来!
1、 初始化 package.json
mkdir webpack-react cd webpack-react npm init --y git init
然后稍微改改 package.json 文件如下:
{ "name": "webpack-react", "private": true, "version": "0.1.0", "description": "一个基于 Webpack 构建的 React开发环境", "main": "index.js", "scripts": { "dev": "", "build": "", "preinstall": "npx only-allow yarn" }, "keywords": [], "author": "DYBOY", "license": "ISC" }
由于没有安装一些三方库,所以该文件还比较“简陋”,所以接下来逐个安装模块,配置环境!
2、安装配置 React 和 Typescript
根据需求,我们先安装一些必要的模块。
首先是 React 的基本模块。
yarn add react react-dom yarn add @types/react @types/react-dom
然后是 TypeScript 类型模块。
yarn add typescript -D
有了 TypeScript,就可以直接通过 TS 生成一个 tsconfig.json 的配置文件
yarn tsc --init
根据需要,稍微改改后如下:
// tsconfig.json { "compilerOptions": { "target": "ES2015", "lib": ["DOM", "ES2015"], "jsx": "react-jsx", "experimentalDecorators": true, "emitDecoratorMetadata": true, "module": "ESNext", "rootDir": "./src", "moduleResolution": "node", "baseUrl": ".", "paths": { "@/*": ["src/*"] }, "resolveJsonModule": true, "allowJs": true, "outDir": "./dist", "removeComments": true, "isolatedModules": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }, "include": ["src/"] }
*关于 tsconfig.json 文件的配置解析可以参阅:《会写 TypeScript 但你真的会 TS 编译配置吗?[1]》。
此时可以创建文件和文件夹,有一个初步的项目结构。
项目结构
其中:
- dist/: 是用于存储打包的文件。
- public/: 是用于存放打包的模板入口 HTML 文件。
- src/: 是用于开发人员主要编码的文件夹。
- .gitignore: 用于配置 Git 忽略哪些文件或文件夹。
- tsconfig.json: TypeScript 的项目配置文件。
- yarn.lock: 依赖模块的版本信息,用于保证开发环境一致性。
此时就可以简单的写支持 TS 和 React 的应用了。
3、 Webpack 相关
因为是一个项目,我们需要通过构建工具,帮助我们快速的实现打包,以及开发环境下的预览,因此第二步就是安装和配置 Webpack。
yarn add webpack webpack-cli webpack-dev-server webpack-merge -D
后两个模块分别是用于开启开发时的本地 HTTP 服务,和用于 Merge webpack 配置的工具函数。
(1) Webpack 配置文件结构
首先,先完善 package.json 中的 scripts(开发指令和构建指令):
+ "dev": "cross-env NODE_ENV=development webpack serve -c scripts/webpack.dev.js", + "build": "yarn ts:checker && cross-env NODE_ENV=production webpack -c scripts/webpack.prod.js", + "ts:checker": "tsc --noEmit",
同时安装一下 cross-env,该模块主要是用于支持在不同的操作系统下保证环境变量正确。
yarn add cross-env -D
通过指令,我们需要三个 Webpack 的配置文件:
(2) webpack.common.js 通用配置
这是公共的 Webpack 配置,主要配置了如下几个地方。
const path = require("path"); const chalk = require("chalk"); const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const ProgressBarPlugin = require("progress-bar-webpack-plugin"); const pkgJSON = require("../package.json"); console.log("process.env.NODE_ENV: ", process.env.NODE_ENV); module.exports = { entry: path.resolve(__dirname, "../src/index.tsx", output: { filename: "[name].[hash:8].js", path: path.resolve(__dirname, "../dist"), publicPath: "/", clean: true, }, resolve: { extensions: [".ts", ".tsx", ".js"], alias: { "@": path.resolve(__dirname, "../src"), }, }, module: { rules: [ { test: /\.tsx?$/, use: ["ts-loader"], exclude: /node_modules/, }, { test: /\.(jpe?g|png|svg|gif)$/i, type: "asset", parser: { dataUrlCondition: { maxSize: 25 * 1024, // 25kb }, }, generator: { filename: "assets/imgs/[name].[hash:8][ext]", }, }, ], }, plugins: [ new webpack.DefinePlugin({ // 定义在代码中可以替换的一些常量 __DEV__: process.env.NODE_ENV === "development", }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../public/index.html"), title: pkgJSON.name, meta: { description: { type: "description", content: pkgJSON.description, }, }, minify: "auto", }), new ProgressBarPlugin({ format: ` :msg [:bar] ${chalk.green.bold(":percent")} (:elapsed s)`, }), ], };
个人有一个观点,开发环境和构建环境应该在配置上相似性需要寻找平衡,开发环境寻求的是热更新快,构建环境寻求的是兼容性好,且尽可能和开发环境看到效果相同!
针对缺失的模块还需要安装到开发依赖中:
# 支持 ts 和 tsx 文件的处理 yarn add ts-loader -D # 美化终端输出,安装特定版本是为了处理模块化包的问题 yarn add chalk@4.1.2 -D # 将 /public/index.html 作为模板入口文件打包 yarn add html-webpack-plugin -D # 美化 webpack 编译时候的进度条 yarn add progress-bar-webpack-plugin -D
(3) webpack.dev.js 开发配置
然后再配置下开发环境下的 Webpack 配置,主要是支持热更新、本地预览功能,以及一些和生产环境差异的配置。
const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); module.exports = merge(common, { mode: "development", // 开发模式 devServer: { hot: true, // 热更新 open: true, // 编译完自动打开浏览器 compress: false, // 关闭gzip压缩 port: 7878, // 开启端口号 historyApiFallback: true, // 支持 history 路由重定向到 index.html 文件 }, module: { // 插件的执行顺序从右到左 rules: [ { test: /\.(css|scss|sass)$/, use: [ "style-loader", "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [["autoprefixer"]], }, }, }, "sass-loader", ], // 排除 node_modules 目录 exclude: /node_modules/, }, ], }, stats: "errors-only", // Webpack 在编译的时候只输出错误日志,终端更清爽 });
这里增加了对 scss/css 文件的处理,因此还需要安装相关的模块:
# style-loader 将 css 注入到 HTML 的内联样式 # css-loader 用于加载 CSS 文件,转化 CSS 为 CommonJS yarn add style-loader css-loader -D # postcss 用于处理 CSS 兼容性 # autoprefixer 用于自动根据兼容需求增加 CSS 属性的前缀 yarn add postcss postcss-loader autoprefixer -D # sass 主要是用于支持 “CSS 编程” # sass-loader 会将 .scss 后缀文件编译成 CSS yarn add sass sass-loader -D
讲到了 CSS 自动前缀处理兼容性,因此可以将需要兼容浏览器版本的配置放到 package.json -> browserslist 属性下:
{ ... "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "defaults", "not ie < 11", "last 2 versions", "> 1%", "iOS 9", "last 3 iOS versions" ] } ... }
(4) webpack.prod.js 生产配置
针对 Webpack 的构建环境下(mode: "production")的配置,实际上在 Webpack 5 版本中默认就集成了很多优化,更多自定义诉求可以参考:Webpack Optimization[2] 配置。
const { merge } = require("webpack-merge"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const common = require("./webpack.common.js"); module.exports = merge(common, { mode: "production", optimization: { minimize: true, minimizer: [ "...", new TerserPlugin({ terserOptions: { format: { comments: false, }, }, extractComments: false, }), ], }, module: { rules: [ { test: /\.(css|scss|sass)$/, use: [ MiniCssExtractPlugin.loader, "css-loader", { loader: "postcss-loader", options: { postcssOptions: { plugins: [["autoprefixer"]], }, }, }, "sass-loader", ], exclude: /node_modules/, }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: "assets/css/[hash:8].css", }), ], });
需要安装依赖:
# 用于将 CSS 导出到单独文件 yarn add mini-css-extract-plugin -D # 用于做源代码压缩 yarn add terser-webpack-plugin -D
(5) 开发&构建
弄好了上面的 Webpack 配置,就可以实际的开发了。
/src/index.tsx 文件如下:
执行:yarn dev,会自动打开浏览器页面:http://localhost:7878/。
执行:yarn build,会将项目编译打包输出到 ./dist/ 文件夹下。
4、 Bable 处理兼容性
我们的项目可能会在各种浏览器中运行,为了尽可能兼容大多数用户的设备,因此引入 Babel 来统一处理兼容性。
在 webpack.common.js 配置文件中增加:
... rules: [ { test: /\.tsx?$/, use: [ + { + loader: "babel-loader", + options: { + presets: [ + [ + "@babel/preset-env", // 预制配置 + { + corejs: { + version: 3, + }, + useBuiltIns: "usage", // 按需引入 pollyfill + }, + ], + "@babel/preset-react", // React 环境 + ], + plugins: ["@babel/plugin-transform-runtime"], + }, + }, "ts-loader", ], exclude: /node_modules/, }, ... ], ...
放到 webpack.common.js 文件下也是为了考虑在开发环境下验证引入 pollyfill 的正确性。
同时还需要安装如下依赖:
# 安装 babel 核心和加载器 yarn add @babel/core babel-loader -D # core-js 中有各种各样的 pollyfill,用于提升兼容性 # https://github.com/zloirock/core-js yarn add core-js -D # 预制环境 yarn add @babel/preset-env @babel/preset-react -D # 统一的 pollyfill,打包时候加载到代码中,减少冗余代码 yarn add @babel/plugin-transform-runtime -D
三、路由 React-router-dom
前端的页面一般是多页面的,因此我们需要一个统一的路由来方便管理,这里用到了 react-router-dom v6[3] 版本。
多路由的使用方式基本相似,因此官方提炼出了 useRoutes 的 Hooks,用于便捷生成路由,相较于 V5 版本,确实方便太多了。
安装作为应用依赖:
yarn add react-router-dom
1、 统一管理的路由配置首先是配置
路由 /src/config/router.tsx 文件:
import { RouteObject } from "react-router-dom"; import HomePage from "@/pages/home"; const ROUTER_CONFIG: RouteObject[] = [ { path: "/", element: <HomePage />, }, { path: "*", element: <>404 Not Found!, }, ]; export { ROUTER_CONFIG };
之后如果新增任意页面,都可以在 /src/pages/ 文件夹下新增任,并且都可以放到 /src/config/router.tsx 文件来统一管理,嵌套路由同样适用,只需要根据 RouteObject 类型声明规范即可:
/** * A route object represents a logical route, with (optionally) its child * routes organized in a tree-like structure. */ export interface RouteObject { caseSensitive?: boolean; // 大小写敏感 children?: RouteObject[]; // 子路由 element?: React.ReactNode; // 组件 index?: boolean; // 在子路由中,默认为父级路由的首页 path?: string; // URL 路径 }
2、 项目中引入
然后在 /src/app.tsx 文件中使用 useRoutes() 并嵌入到应用中:
import { useRoutes } from "react-router-dom"; import { ROUTER_CONFIG } from "./config/router"; const App = () => { const appRoutesElement = useRoutes(ROUTER_CONFIG); return appRoutesElement; }; export default App;
最后在 /src/inde.tsx 使用 BrowserRouter 包裹 组件。
import { render } from "react-dom"; import { BrowserRouter } from "react-router-dom"; import App from "./app"; render( <BrowserRouter> <App /> BrowserRouter>, document.getElementById("root") );
此时的项目目录结构如下:
目录结构
如此就可以愉快的编写任意页面啦!
3、 [优化]延迟按需加载页面
虽然路由集中管理了,但是首屏加载的 js 文件太大,会使得白屏时间较长,增加了用户等待时间。
因此考虑延迟按需加载页面方式,使用 import() 和 React.lazy() 来主动优化。
新建一个通用组件 LazyWrapper 在 /src/components/lazy-wrapper/index.tsx 文件。
import { FC, lazy, Suspense } from "react"; interface LazyWrapperProps { /** 组件路径: 在 src/pages 目录下的页面路径,eg: /home => src/pages/home/index.tsx */ path: string; } /** * 懒加载组件包装器 */ const LazyWrapper: FC<LazyWrapperProps> = ({ path }) => { const LazyComponent = lazy(() => import(`/src/pages${path}`)); return ( <Suspense fallback={<div>loading...div>}> <LazyComponent /> Suspense> ); }; export default LazyWrapper;
此时修改 /config/router.tsx 路由配置文件:
效果如下: 当加载 Home 页面时,按需加载对应的组件。
另外由于拆包之后可能组件容易因网络抖动原因加载失败,所以还需要做自动重试拉取组件的方案,这里也不赘述了,参考之前写的文章:《性能优化竟白屏,难道真是我的锅?》
通过二次封装 Errorboundary 组件,实现组件加载失败自动重试,并针对错误上报日志,便于后期针对性优化。
四、状态管理 Redux
在一个应用中,自然是少不了全局状态管理,一般情况下如果状态比较简单,可以直接使用 React 的 useContext 和 useReducer Hooks 组合实现简单的全局状态管理。
但通常我们的项目应该是比较庞大复杂,为了提升后期可维护性,因此使用了 Redux 作为全局状态管理。
Redux 的另一大优势则是提供了 @reduxjs/toolkit[4] 辅助工具,使得状态管理更加简单。
安装:
# react-redux 是 redux 的 UI 桥接层 yarn add redux react-redux yarn add @reduxjs/toolkit
这里就不在赘述了,对于 Redux 的状态管理方案,可以参考之前写的文章:《用 Redux 做状态管理,真的很简单