从 0 搭建配置 React+TS 项目

3,847 阅读9分钟

前言

原文来自我的 个人博客

本章的源码可以通过脚手架 jw-cli 下载

npm i @yejiwei/cli -g

image.png

image.png

image.png

接着执行以下命令即可

cd react-app
npm install
npm start

1. 通过 Create-React-App 创建项目

  1. 创建一个 TypeScript 模版的 React 项目
npx create-react-app react-app --template typescript
  1. 运行项目
cd react-app
npm start
  1. 输入 localhost:3000 显示如下如即成功

image.png

2. 配置 CRACO

CRACO 全称 Create React App Configuration Override,取首字母即组成了工具名称。是为了无 eject 、可配置式的去修改 CRA 默认提供的工程配置,这样既能享受 CRA 带来的便利和后续升级,也能自己去自定义打包配置完成项目需要,一举两得。

  1. 从npm安装最新版本的包作为开发依赖项:
npm i -D @craco/craco
  1. 在项目的根目录中创建一个CRACO配置文件并配置:
  react-app
  ├── node_modules
+ ├── craco.config.js
  └── package.json
  1. 更新 package.json 的脚本部分中对 react 脚本的现有调用以使用 CRACO CLI
"scripts": {
-  "start": "react-scripts start"
+  "start": "craco start"
-  "build": "react-scripts build"
+  "build": "craco build"
-  "test": "react-scripts test"
+  "test": "craco test"
}
  1. 支持 TypeScript ,使用 CRACO 提供的类型包
npm i -D @craco/types
  1. craco.config.js 配置

因为不同的项目有不同的需求和业务,配置文件也会不同,根据自己需求配置即可,遇到问题可到找craco官方文档 查看

下面我以配置 less 和别名为例:

安装craco-less

npm install -D craco-less

如果上面 craco-less 因为版本原因报错,在命令后面加 @alpha

配置craco-less插件和别名

const path = require("path");
const CracoLessPlugin = require("craco-less");
const resolve = (pathname) => path.resolve(__dirname, pathname);

module.exports = {
  plugins: [
    /* less */
    {
      plugin: CracoLessPlugin,
    },
  ],
  webpack: {
    /* 别名 */
    alias: {
      "@": resolve("src"),
    },
  },
};
  1. tsconfig.jsoncompilerOptions 添加配置
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  1. 运行 npm run start ,项目能正常跑起来就OK。

3. 集成 EditorConfig 配置

EditorConfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。

# http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行尾的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

VSCode 需要安装一个插件:EditorConfig for VS Code

image.png

4. 使用 Prettier 工具

Prettier 是一款强大的代码格式化工具,支持 JavaScriptTypeScriptCSSSCSSLessJSXAngularVueGraphQLJSONMarkdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。

1.安装 Prettier

npm install prettier -D

2.配置 .prettierrc 文件:

  • useTabs:使用tab缩进还是空格缩进,选择false;
  • tabWidth:tab是空格的情况下,是几个空格,选择2个;
  • printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
  • singleQuote:使用单引号还是双引号,选择true,使用单引号;
  • trailingComma:在多行输入的尾逗号是否添加,设置为 none,比如对象类型的最后一个属性后面是否加一个,;
  • semi:语句末尾是否要加分号,默认值true,选择false表示不加;
{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": true,
  "trailingComma": "none",
  "semi": false
}

3.创建 .prettierignore 忽略文件

/dist/*
.local
.output.js
/node_modules/**

**/*.svg
**/*.sh

/public/*
  1. VSCode 需要安装 Prettier 的插件

image.png

  1. VSCode 中的配置
  • settings =>format on save => 勾选上

image.png

  • settings => editor default format => 选择 prettier

image.png

6.测试 Prettier 是否生效

  • 测试一:在代码中保存代码;
  • 测试二:配置一次性修改的命令;

在package.json中配置一个scripts:

"prettier": "prettier --write ."

5. 使用 ESLint 检测

  1. 安装 ESLint
npm install eslint -D
  1. 配置 ESLint
npx eslint --init

第一步选择如何使用 ESLint ,选第二个

image.png

第二部模块化选择 ESModule

image.png

第三步选择框架,根据实际情况选择 React

image.png

第四步选择是否 TypeScript ,根据实际情况选择

image.png

第五步选择代码运行的环境,两个可以同时选择

image.png

第六步,配置文件类型, 选 js

image.png

第七步,根据你上面的选择询问你要不要安装包,我上面选了React和TypeScript

image.png

  1. VSCode 需要安装 ESLint 插件:

image.png

  1. 解决 ESLintPrettier 冲突的问题:

安装插件:

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

添加 Prettier 插件:plugin:prettier/recommended

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended'
  ],
  overrides: [],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {
    '@typescript-eslint/no-var-requires': 'off',
    'prettier/prettier': 'warn',
    '@typescript-eslint/no-explicit-any': 'off'
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
}

  1. VSCode中eslint的配置
"eslint.alwaysShowStatus": true,
  1. 在package.json中配置一个scripts:
"lint": "eslint ."

6. Git Husky和 ESLint (可选)

虽然现在已经要求项目使用 ESLint 了,但是不能保证组员提交代码之前都将 ESLint 中的问题解决掉了:

  • 也就是希望保证代码仓库中的代码都是符合 ESLint 规范的;

  • 那么需要在组员执行 git commit 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复;

那么如何做到这一点呢?可以通过 Husky 工具:

  • husky 是一个 git hook 工具,可以帮助我们触发 git 提交的各个阶段:pre-commitcommit-msgpre-push

如何使用 husky 呢?

这里我们可以使用自动配置命令:

npx husky-init && npm install

这里会做三件事:

1.安装husky相关的依赖:

image.png

2.在项目目录下创建 .husky 文件夹:

image.png

3.在package.json中添加一个脚本:

image.png

4.接下来,我们需要去完成一个操作:在进行commit时,执行lint脚本:

image.png

这个时候我们执行 git commit 的时候会自动对代码进行 lint 校验。

7. Git Commit 规范 (可选)

7.1 代码提交风格

通常我们的 git commit 会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制。

image.png

但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:Commitizen

  • Commitizen 是一个帮助我们编写规范 commit message 的工具;

1.安装 Commitizen

npm install commitizen -D

2.安装 cz-conventional-changelog,并且初始化 cz-conventional-changelog

npx commitizen init cz-conventional-changelog --save-dev --save-exact

这个命令会帮助我们安装cz-conventional-changelog:

image.png

并且在package.json中进行配置:

image.png

这个时候我们提交代码需要使用 npx cz

  • 第一步是选择type,本次更新的类型
Type作用
feat新增特性 (feature)
fix修复 Bug(bug fix)
docs修改文档 (documentation)
style代码格式修改(white-space, formatting, missing semi colons, etc)
refactor代码重构(refactor)
perf改善性能(A code change that improves performance)
test测试(when adding missing tests)
build变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等)
ci更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等
chore变更构建流程或辅助工具(比如更改测试环境)
revert代码回退
  • 第二步选择本次修改的范围(作用域)

image.png

  • 第三步选择提交的信息

image.png

  • 第四步提交详细的描述信息

image.png

  • 第五步是否是一次重大的更改

image.png

  • 第六步是否影响某个open issue

image.png

我们也可以在scripts中构建一个命令来执行 cz:

image.png

7.2 代码提交验证

如果我们按照 cz 来规范了提交风格,但是依然有同事通过 git commit 按照不规范的格式提交应该怎么办呢?

  • 我们可以通过 commitlint 来限制提交;

1.安装 @commitlint/config-conventional 和 @commitlint/cli

npm i @commitlint/config-conventional @commitlint/cli -D

2.在根目录创建commitlint.config.js文件,配置commitlint

module.exports = {
  extends: ['@commitlint/config-conventional']
}

3.使用husky生成commit-msg文件,验证提交信息:

npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

8. 文件目录结构划分

对项目进行目录结构划分:

image.png

  react-app
+ |- /src
+   |- /assets 存放资源
+     |- /img   
+     |- /css   
+     |- /font   
+     |- /data   
+   |- base-ui  存放多个项目中都会用到的公共组件
+   |- components 存放这个项目用到的公共组件
+   |- hooks 存放自定义hook
+   |- views 视图
+   |- store 状态管理
+   |- router 路由
+   |- service 网络请求
+   |- utils 工具
+   |- global 全局注册、全局常量...
- |- App.css
- |- App.test.tsx
- |- index.css
- |- logo.svg
- |- reportWebVitals.ts
- |- setupTest.ts

App.tsx

import React from 'react'

function App() {
  return (
    <div className="App">
      <h1>React App</h1>
    </div>
  )
}

export default App

index.tsx

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 />)

9. CSS 重置

  1. 添加 common.less index.less reset.less
src/assets/css
+ |- common.less 公共样式
+ |- index.less
+ |- reset.less 自定义重置样式

reset.less

/* reset.css样式重置文件 */
/* margin/padding重置 */
body, h1, h2, h3, ul, ol, li, p, dl, dt, dd {
  padding: 0;
  margin: 0;
}

/* a元素重置 */
a {
  text-decoration: none;
  color: #333;
}

/* img的vertical-align重置 */
img {
  vertical-align: top;
}

/* ul, ol, li重置 */
ul, ol, li {
  list-style: none;
}

/* 对斜体元素重置 */
i, em {
  font-style: normal;
}

index.less

@import './reset.less';
@import './common.less';
  1. 安装 normalize.css
npm install normalize.css
  1. index.tsx 中引入 normalize.cssindex.less
...
import 'normalize.css'
import './src/assets/index.less'
...

10. 设置代码片段

为了方便开发,我们可以设置一份通用的 React 组件代码模板。

  1. 创建一份自己常用的模板
import React, { memo } from 'react'
import type { FC, ReactNode } from 'react'

interface IProps {
  children?: ReactNode
}

const Template: FC<IProps> = () => {
  return <div>Template</div>
}

export default memo(Template)
  1. 复制到 snippet-generator网站 ,生成对应vscode的配置

image.png

  1. vscode 中点击 文件->首选项->配置用户代码片段, 选择 typescriptreact.json 配置文件

image.png

  1. 将生成的代码 变量名修改为 ${1:Template} 复制进去,如下图所示,这样做的目的是当输入 tsreact 时,可以同时修改这三个变量名,而不用一个一个改了

image.png

  1. 之后只要在文件中输入 tsreact 即可创建模板

2.gif

11. 配置 React-Router

  1. 安装 react-router-dom
npm i react-router-dom 
  1. src/index.tsx 中导入 HashRouter 对App组件进行包裹
import { HashRouter } from 'react-router-dom'

root.render(
  <HashRouter>
    <App />
  </HashRouter>
)
  1. src/router/index.tsx 中配置路由映射表
import React, { lazy } from 'react'
import type { RouteObject } from 'react-router-dom'
import { Navigate } from 'react-router-dom'

/* 路由懒加载 */
const Home = lazy(() => import('@/views/home'))
const Mine = lazy(() => import('@/views/mine'))

const routes: RouteObject[] = [
  {
    path: '/',
    element: <Navigate to="/home" />
  },
  {
    path: '/home',
    element: <Home />
  },
  {
    path: '/mine',
    element: <Mine />
  }
]

export default routes
  1. App.tsx 中使用路由
import React, { Suspense } from 'react'
import { useRoutes, Link } from 'react-router-dom'
import routes from './router'

function App() {
  return (
    <div className="App">
      <div className="nav">
        <Link to="/home">菜单一</Link>
        <Link to="/mine">菜单二</Link>
      </div>
      <Suspense fallback="loading...">
        <div className="main">{useRoutes(routes)}</div>
      </Suspense>
    </div>
  )
}

export default App

12. 配置 Redux 状态管理

  1. 安装 react-redux@reduxjs/toolkit 两个包
npm install react-redux @reduxjs/toolkit
  1. src/store 中创建store

store/index.ts

import { configureStore } from '@reduxjs/toolkit'
import couterReducer from './modules/counter'
import {
  useSelector,
  TypedUseSelectorHook,
  useDispatch,
  shallowEqual
} from 'react-redux'

const store = configureStore({
  reducer: {
    counter: couterReducer
  }
})

type GetStateFnType = typeof store.getState
type IRootState = ReturnType<GetStateFnType>
type DispatchType = typeof store.dispatch

export const useAppSelector: TypedUseSelectorHook<IRootState> = useSelector
export const useAppDispatch: () => DispatchType = useDispatch
export const shallowEqualApp = shallowEqual
export default store

store/modules/counter.ts

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    count: 0
  },
  reducers: {
    incremented: (state) => {
      state.count += 1
    },
    decremented: (state) => {
      state.count -= 1
    }
  }
})

export const { incremented, decremented } = counterSlice.actions
export default counterSlice.reducer
  1. src/index.tsx 中提供store
import { Provider } from 'react-redux'
import store from './store'

root.render(
  <Provider store={store}>
    <HashRouter>
      <App />
    </HashRouter>
  </Provider>
)
  1. 在 App.tsx 测试使用

获取并且显示 state

import { useAppSelector, shallowEqualApp, useAppDispatch } from './store'
import { incremented, decremented } from '@/store/modules/counter'
...
  const { count } = useAppSelector(
    (state) => ({
      count: state.counter.count
    }),
    shallowEqualApp
  )

  const dispatch = useAppDispatch()
  function addCount() {
    dispatch(incremented())
  }
  function subCount() {
    dispatch(decremented())
  }
...
  return (
      ...
        <div>count:{count}</div>
        <button onClick={subCount}>-1</button>
        <button onClick={addCount}>+1</button>
      ...
  )
 

修改 state

import { useAppDispatch } from './store'
...
    const dispatch = useAppDispatch()
    function handleChangeMessage() {
    dispatch(changeMessage('哈哈哈哈哈哈'))
  }
...
    return (
        ...
        <button onClick={handleChangeMessage}>changeMessage</button>
        ...
    )

13. 环境配置

在根目录下添加两个文件用以配置不同环境下的环境变量 .env.development

REACT_APP_BASE_URL = 'www.development.com'

.env.production

REACT_APP_BASE_URL = 'www.production.com'

同时需要在 react-app-env.d.ts 声明变量类型

/// <reference types="react-scripts" />

declare namespace NodeJS {
  interface ProcessEnv {
    readonly REACT_APP_BASE_URL: string
  }
}

14. axios 网络请求封装

安装 axios

npm i axios@0.27.2

修改 service 的目录结构为

+ |- /config
+    |- index.ts
+ |- /request
+    |- index.ts
+    |- types.ts
+ |- index.ts

/service/config.ts

const BASE_URL = process.env.REACT_APP_BASE_URL
export const TIME_OUT = 1000
export { BASE_URL }

/service/request/index.ts

import axios from 'axios'
import type { AxiosInstance } from 'axios'
import type { RequestConfig } from './types'

// 拦截器: 蒙版Loading/token/修改配置

/**
 * 两个难点:
 *  1.拦截器进行精细控制
 *    > 全局拦截器
 *    > 实例拦截器
 *    > 单次请求拦截器
 *
 *  2.响应结果的类型处理(泛型)
 */

class Request {
  instance: AxiosInstance

  // request实例 => axios的实例
  constructor(config: RequestConfig) {
    this.instance = axios.create(config)

    // 每个instance实例都添加拦截器
    this.instance.interceptors.request.use(
      (config) => {
        // loading/token
        return config
      },
      (err) => {
        return err
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        return res.data
      },
      (err) => {
        return err
      }
    )

    // 针对特定的Request实例添加拦截器
    this.instance.interceptors.request.use(
      config.interceptors?.requestSuccessFn,
      config.interceptors?.requestFailureFn
    )
    this.instance.interceptors.response.use(
      config.interceptors?.responseSuccessFn,
      config.interceptors?.responseFailureFn
    )
  }

  // 封装网络请求的方法
  request<T = any>(config: RequestConfig<T>) {
    // 单次请求的成功拦截处理
    if (config.interceptors?.requestSuccessFn) {
      config = config.interceptors.requestSuccessFn(config)
    }

    // 返回Promise
    return new Promise<T>((resolve, reject) => {
      this.instance
        .request<any, T>(config)
        .then((res) => {
          // 单词响应的成功拦截处理
          if (config.interceptors?.responseSuccessFn) {
            res = config.interceptors.responseSuccessFn(res)
          }
          resolve(res)
        })
        .catch((err) => {
          reject(err)
        })
    })
  }

  get<T = any>(config: RequestConfig<T>) {
    return this.request({ ...config, method: 'GET' })
  }
  post<T = any>(config: RequestConfig<T>) {
    return this.request({ ...config, method: 'POST' })
  }
  delete<T = any>(config: RequestConfig<T>) {
    return this.request({ ...config, method: 'DELETE' })
  }
  patch<T = any>(config: RequestConfig<T>) {
    return this.request({ ...config, method: 'PATCH' })
  }
}

export default Request

/service/request/type.ts

import type { AxiosRequestConfig, AxiosResponse } from 'axios'

// 针对AxiosRequestConfig配置进行扩展
export interface Interceptors<T = AxiosResponse> {
  requestSuccessFn?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestFailureFn?: (err: any) => any
  responseSuccessFn?: (res: T) => T
  responseFailureFn?: (err: any) => any
}

export interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: Interceptors<T>
}

/service/index.ts

import { BASE_URL, TIME_OUT } from './config'
import Request from './request'

const request = new Request({
  baseURL: BASE_URL,
  timeout: TIME_OUT
})

export default request

15. 引入 styled-components

除了 styled-components本身之外还要安装它的类型声明

npm i -D styled-components @types/styled-components