从0搭建一个react开发的脚手架

最近这一年一直在用vue写前端,虽然一直很想用react,但是因为业务所限不可能花大量的时候去用react重写,只有在新项目的时候才有可能重新做技术选项。公司之前的技术类型比较多,最近开始统一成前端react,后端go/node,这样互相交流起来比较容易。

为了快捷的开发新项目,我维护了一套react脚手架模板 ,开新项目的时候可以快速的以此模板开始业务逻辑,它的特性有:

  • vite作为构建工具,复杂度比webpack低,但速度却比webpack快很多
  • 支持hmr,修改代码页面无缝更新
  • 以typescript为开发语言,有类型约束不会写出难以查找的bug
  • 支持别名@到根目录,@@到components目录
  • 支持国际化,默认写了中文、英文、日语3种语言
  • 配置了eslint,使用yarn fix可以自动修复代码格式问题
  • 采用了tailwind作为开发的css样式框架,外加tailwind-classnames约束样式名字。
  • 配置了多环境,默认为local、stg、prod3个环境,如果有特殊需求可根据需要扩展
  • 状态管理采用redux-tool-kit,没有繁琐的redux流程(action、reducer等),一个slice文件就是一个业务模块。
  • 带一个todoList的demo,列举了react-router-dom相关用法

目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

├── envs                          多环境配置文件
└── src                           工程核心代码
    ├── assets                    资源文件
    │   ├── i18n                     国际化翻译文件
    │   ├── images                   图片资源
    │   └── styles                   全局样式
    ├── components                 组件
    │   ├── CHeader                  头部
    │   └── Common                 通用组件
    │       ├── CButton
    │       ├── CContainer
    │       ├── CFullLoading
    │       ├── CInput
    │       ├── CLink
    │       ├── CNavLink
    │       └── CPartialLoading
    ├── i18n                         国际化代码实现
    ├── redux                        状态管理(slice和store)
    ├── service                      业务服务(http交互)
    ├── utils                        通用方法
    └── views                        页面
        ├── Add                        添加todo
        ├── Home                       todoList
        ├── Login                      登陆页面
        ├── NotFound                   404
        └── Task                       taskList
├── tsconfig.json                     ts配置
├── vite.config.ts                    vite配置
├── .eslintrc.js                      eslint配置
├── README.md                         项目说明文件
├── index.html                        项目入口文件
├── node_modules                      项目依赖包
├── package.json                      项目依赖声明文件
├── .env                              本地环境(如果没有的话会使用envs/.env.local)
└── yarn.lock                         yarn文件锁

依赖库介绍

核心框架

1
2
3
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^5.2.0",

国际化

1
2
3
    "react-i18next": "^11.11.4",
    "i18next": "^20.4.0",
    "i18next-browser-languagedetector": "^6.1.2",

UI相关

1
2
3
4
    "react-loading": "^2.0.3",
    "@fortawesome/fontawesome-svg-core": "^1.2.36",
    "@fortawesome/free-solid-svg-icons": "^5.15.4",
    "@fortawesome/react-fontawesome": "^0.1.15",

环境相关

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    "vite": "^2.5.0",
    "@vitejs/plugin-react-refresh": "^1.3.6",
    "typescript": "^4.3.5",
    "cross-env": "^7.0.3",
    "@types/node": "^16.7.12",
    "@types/react": "^17.0.18",
    "@types/react-dom": "^17.0.9",
    "@typescript-eslint/eslint-plugin": "^4.29.2",
    "@typescript-eslint/parser": "^4.29.2",
    "@types/react-router-dom": "^5.1.8",

状态管理

1
2
3
4
5
    "@reduxjs/toolkit": "^1.6.1",
    "redux": "^4.1.1",
    "react-redux": "^7.2.4",
    "redux-thunk": "^2.3.0",
    "redux-devtools-extension": "^2.13.9"

css框架

1
2
3
4
5
    "tailwindcss-classnames": "^2.2.3",
    "autoprefixer": "^10.3.3",
    "postcss": "^8.3.6",
    "tailwindcss": "^2.2.9",
    "variables": "^1.0.1"

eslint相关

1
2
3
4
5
6
7
8
    "eslint-config-airbnb": "^18.2.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-import": "^2.24.0",
    "eslint-plugin-jsx-a11y": "^6.4.1",
    "eslint-plugin-react": "^7.24.0",
    "eslint-plugin-prettier": "^3.4.0",
    "eslint-plugin-react-hooks": "^4.2.0",
    "prettier": "^2.3.2",

网络相关

1
2
    "redaxios": "^0.4.1",
    "qs": "^6.10.1",

scripts

1
2
3
4
5
6
7
"dev": "cross-env NODE_ENV=local vite --host",  
"dev:stg": "cross NODE_ENV=stg vite",
"build": "vite build",
"build:stg": "cross-env vite build --mode test",
"build:prod": "cross-env vite build --mode production",
"serve": "vite preview",
"lint": "eslint --ext .jsx,.js,.ts,.tsx ."

完整package.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
  "name": "vite-react-teamplate",
  "version": "0.0.1",
  "scripts": {
    "dev": "cross-env NODE_ENV=local vite --host",
    "dev:stg": "cross NODE_ENV=stg vite",
    "build": "vite build",
    "build:stg": "cross-env vite build --mode test",
    "build:prod": "cross-env vite build --mode production",
    "serve": "vite preview",
    "lint": "eslint --ext .jsx,.js,.ts,.tsx ."
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-router-dom": "^5.2.0",
    "react-i18next": "^11.11.4",
    "i18next": "^20.4.0",
    "i18next-browser-languagedetector": "^6.1.2",
    "react-loading": "^2.0.3",
    "@fortawesome/fontawesome-svg-core": "^1.2.36",
    "@fortawesome/free-solid-svg-icons": "^5.15.4",
    "@fortawesome/react-fontawesome": "^0.1.15",
    "redaxios": "^0.4.1",
    "qs": "^6.10.1",
    "@reduxjs/toolkit": "^1.6.1",
    "redux": "^4.1.1",
    "react-redux": "^7.2.4",
    "redux-thunk": "^2.3.0",
    "tailwindcss": "^2.2.9",
    "variables": "^1.0.1"
  },
  "devDependencies": {
    "vite": "^2.5.0",
    "@vitejs/plugin-react-refresh": "^1.3.6",
    "typescript": "^4.3.5",
    "cross-env": "^7.0.3",
    "@types/node": "^16.7.12",
    "@types/react": "^17.0.18",
    "@types/react-dom": "^17.0.9",
    "@typescript-eslint/eslint-plugin": "^4.29.2",
    "@typescript-eslint/parser": "^4.29.2",
    "@types/react-router-dom": "^5.1.8",
    "redux-devtools-extension": "^2.13.9",
    "tailwindcss-classnames": "^2.2.3",
    "autoprefixer": "^10.3.3",
    "postcss": "^8.3.6",
    "prettier": "^2.3.2",
    "dotenv": "^10.0.0",
    "eslint": "^7.32.0",
    "eslint-config-airbnb": "^18.2.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-import": "^2.24.0",
    "eslint-plugin-jsx-a11y": "^6.4.1",
    "eslint-plugin-react": "^7.24.0",
    "eslint-plugin-prettier": "^3.4.0",
    "eslint-plugin-react-hooks": "^4.2.0"
  }
}

多环境

image-20210907151025364

1
2
3
4
5
6
7
NODE_ENV = local
VITE_BUILD_ENV = development
VITE_HOST = portal.local.xiaomo.info
VITE_PORT = 10086
VITE_BASE_URL = ./
VITE_OUTPUT_DIR = dist
VITE_APP_BASE_API = https://mock.local.xiaomo.info

vite配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { defineConfig
} from 'vite';
import reactRefresh from '@vitejs/plugin-react-refresh';
import * as path from 'path';
import { existsSync } from 'fs';

// Dotenv 是一个零依赖的模块,它能将环境变量中的变量从 .env 文件加载到 process.env 中
const dotenv = require('dotenv');
dotenv.config({
   path: existsSync('.env') ?
      '.env' : path.resolve('envs', `.env.${process.env.NODE_ENV}`)
});

// https://vitejs.dev/config/
export default defineConfig({
   plugins: [reactRefresh()],
   resolve: {
      alias: {
         '@@': path.resolve(__dirname),
         '@': path.resolve(__dirname, 'src'),
      }
   },
   server: {
      cors: true,
      port: process.env.VITE_PORT as unknown as number,
      hmr: {
         host: 'localhost',
         protocol: 'ws',
         port: process.env.VITE_PORT as unknown as number,
      }
   }
});

状态管理

image-20210907151509301

slice

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import fileService from '@/service/fileService';
import { File } from '@/models/File';

export const getFiles = createAsyncThunk(
	'files/getFiles',
	async () => {
		const response = await fileService.getFiles();
		const result = response.data;
		return result.data as File[];
	}
);

export interface FileState {
	loading: boolean,
	files: File[]
}

const initialState: FileState = {
	loading: false,
	files: []
};

const fileSlice = createSlice({
	name: 'files',
	initialState: initialState,
	reducers: {},
	extraReducers: (builder) => {
		builder.addCase(getFiles.fulfilled, (state: FileState, action) => {
			state.loading = false;
			state.files = action.payload;
		});

	}
});

export default fileSlice.reducer;

store

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { configureStore } from '@reduxjs/toolkit';
import fileReducer from '@/redux/fileSlice';

const store = configureStore({
	reducer: {
		files: fileReducer,
	}
});

export type RootState = ReturnType<typeof store.getState>

export default store;

使用store

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ReactDOM.render(
   <React.StrictMode>
      <BrowserRouter>
         <Provider store={store}>
            <App/>
         </Provider>
      </BrowserRouter>
   </React.StrictMode>,
   document.getElementById('root')
);

fileService

1
2
3
4
5
6
7
8
9
import { Response } from 'redaxios';
import api from '@/utils/api';
import { Result } from '@/models/Result';

export default class fileService {
   static async getFiles(): Promise<Response<Result>> {
      return await api.get('/api/v1/files');
   }
}

UI中获取数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export const CtwFiles = (): JSX.Element => {
   const dispatch = useDispatch();
   useEffect(() => {
      dispatch(getFiles());
   }, [dispatch]);

   const state = useSelector((state: RootState) => {
      return state.files;
   });

   return <CContainer
      loading={state.loading}
      classes={classnames('h-80')}
      component={<FileList files={state.files}/>}
   />;
};

详细代码请查看工程 https://github.com/reactZone/vite-react-ts-template

国际化

assets/i18n下新建翻译文件

image-20210907152148011

ja-JP.json

1
2
3
4
5
{
  "home": "ホーム",
  "welcome": "ホームへようこそ!",
  "Login": "ログイン"
}

国际化核心实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import LanguageDetector from 'i18next-browser-languagedetector';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import enUsTrans from '@/assets/i18n/en-us.json';
import zhCnTrans from '@/assets/i18n/zh-cn.json';
import jaJpTrans from '@/assets/i18n/ja-jp.json';

i18n
   .use(LanguageDetector) // 嗅探当前浏览器语言
   .use(initReactI18next) // init i18next
   .init({
      // 引入资源文件
      resources: {
         en: {
            translation: enUsTrans,
         },
         zh: {
            translation: zhCnTrans,
         },
         ja: {
            translation: jaJpTrans,
         },
      },
      // 选择默认语言,选择内容为上述配置中的key,即en/zh/ja
      fallbackLng: 'ja',
      debug: false,
      interpolation: {
         escapeValue: false, // not needed for react as it escapes by default
      },
   })
   .then((r) => console.log(r));

export default i18n;

在入口文件加载i18n

image-20210907152327173

路由

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const Home = lazy(() => import('@/views/Home'));
const Task = lazy(() => import('@/views/Task'));
const Add = lazy(() => import('@/views/Add'));
const Login = lazy(() => import('@/views/Login'));

const App = (): JSX.Element => {
   return (
      <Suspense fallback={<CFullLoading/>}>
         <Switch>
            <Route path="/" component={Home} exact/>
            <Route path="/home" component={Home}/>
            <Route path="/task" component={Task}/>
            <Route path="/add" component={Add}/>
            <Route path="/login" component={Login}/>
            <Route path="/404" component={NotFound}/>
            <Redirect to="/404"/>
         </Switch>
      </Suspense>
   );
};

如何使用

https://github.com/reactZone/vite-react-ts-template/generate 点击此链接创建属于自己的项目(需要事先登陆github账号)

image-20210907142954421

以此项目为模板创建一个新的项目,然后clone到本地进行开发

署名 - 非商业性使用 - 禁止演绎 4.0