封面

从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相关用法

目录结构


├── 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文件锁

依赖库介绍

核心框架

"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",

UI相关

"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",

环境相关

"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",

状态管理

"@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框架

"tailwindcss-classnames": "^2.2.3",
"autoprefixer": "^10.3.3",
"postcss": "^8.3.6",
"tailwindcss": "^2.2.9",
"variables": "^1.0.1"

eslint相关

"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",

网络相关

"redaxios": "^0.4.1",
"qs": "^6.10.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 ."

完整package.json

{
"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

NODE_ENV = local
VITE_BUILD_ENV = development
VITE_HOST = portal.local.g123.jp
VITE_PORT = 10086
VITE_BASE_URL = ./
VITE_OUTPUT_DIR = dist
VITE_APP_BASE_API = https://mock.local.g123.jp

vite配置

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

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

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

ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<App/>
</Provider>
</BrowserRouter>
</React.StrictMode>,
document.getElementById('root')
);

fileService

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中获取数据

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

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

国际化核心实现

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

路由

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到本地进行开发

文章目录
  1. 1. 依赖库介绍
    1. 1.0.1. 核心框架
    2. 1.0.2. 国际化
    3. 1.0.3. UI相关
    4. 1.0.4. 环境相关
    5. 1.0.5. 状态管理
    6. 1.0.6. css框架
    7. 1.0.7. eslint相关
    8. 1.0.8. 网络相关
  • 2. scripts
  • 3. 完整package.json
  • 4. 多环境
  • 5. vite配置
  • 6. 状态管理
    1. 6.0.1. slice
    2. 6.0.2. store
    3. 6.0.3. 使用store
    4. 6.0.4. fileService
  • 7. 国际化
  • 8. 在assets/i18n下新建翻译文件
    1. 8.0.1. 国际化核心实现
    2. 8.0.2. 在入口文件加载i18n
  • 9. 路由
  • 10. 如何使用


  • twitter分享


    如果想及时收到回复,可在 订阅中心Participating中勾选Email

    Fork me on GitHub