其实,我只在四年前完整的自己搭建过react项目,后来都基于umijs一键一把梭了。最近闲得慌,再加上最近一两年react项目开发的少,知识点感觉跟不上了,就想着复盘一下,随从项目搭建开始...
你可以学到什么?
-
如何使用 vite 搭建项目
-
如何集成与使用 web-localstorage-plus
-
如何集成与使用 react-router6
-
如何集成与使用 redux
-
如何集成与使用 ant-design
-
如何封装与使用 umi-request
-
如何借力 eslint 和 prettier 保证代码质量
-
如何借力 commitlint 规范git提交信息
1.按提示创建项目
- 运行vite
yarn create vite
- 输入自定义的项目名称
name: › your-project-name
- 选择你想要的技术框架
? Select a framework: › - Use arrow-keys. Return to submit.
Vanilla
Vue
❯ React
Preact
Lit
Svelte
Others
- 选择ts模板
? Select a variant: › - Use arrow-keys. Return to submit.
TypeScript
❯ TypeScript + SWC
JavaScript
JavaScript + SWC
5.按提示安装并运行项目
Done. Now run:
cd react-blob
yarn
yarn dev
6.优化项目结构
打开工程可以看到,vite默认了部分页面和配置,大多数其实都是无用的,需要进行下修剪。此处省略过程,有需要的可以(拉取源码)[https://github.com/supanpanCn/svite]查看
2.配置vite.config.ts
这里和vite+vue工程化配置是一样的,此处不再赘述,感兴趣的可直接跳转查看
3.集成web-storage-plus
对于需要使用到持久缓存的地方,localstorage
是优选的方案,不过原生接口比较难用,而该npm包对其进行了二次封装,使其支持了命名空间、过期时间、监听变化、批量操作等特性,笔者的项目里一致都在用,文档看这里:传送门
- 安装
yarn add web-localstorage-plus@next
- 在
main.tsx
中引入并初始化根存储
import createStorage from 'web-localstorage-plus'
createStorage({
// 根命名空间
rootName:'spp-storage',
// 是否禁用原生localstorage
noUseLocalStorage:false
})
- 在xxx.tsx文件中引入并使用
import { useStorage } from "web-localstorage-plus";
function Storage() {
const storage = useStorage();
storage.setItem("name", "spp", "author");
storage.setItem("age", 29, "author");
return <>learn storage</>;
}
export default Storage;
- 存储结果如下
4.集成react-router
- 安装依赖
yarn add react-router-dom
- 配置路由表
在src目录下新建router文件夹,并在router下新建index.ts文件,内容如下
import { createHashRouter } from "react-router-dom";
import User from "../pages/user";
const router = createHashRouter([
{
path: "/",
},
{
path: "/user",
Component: User,
},
]);
export default router;
- 路由出口
找到App.tsx文件,引入并注入路由表
import Storage from './pages/storage';
import { RouterProvider } from 'react-router-dom';
import router from './router'
function App() {
return (
<>
<RouterProvider router={router}/>
<br />
<Storage/>
</>
)
}
export default App
- 预览
5.集成redux
- 安装
redux
yarn add @reduxjs/toolkit react-redux
- 创建根
store
在根目录下新建store/index.ts
文件
import { configureStore } from '@reduxjs/toolkit'
export default configureStore({
reducer: {
}
})
- 拆分
reducer
在store
下新建xxx.ts
,笔者这里为calculate.ts
import { createSlice } from '@reduxjs/toolkit'
export const counterSlice = createSlice({
name: 'calculate',
initialState: {
value: 0
},
reducers: {
increment: state => {
state.value += 1
},
}
})
export const { increment } = counterSlice.actions
export default counterSlice.reducer
将定义的calculate.ts
导入到index.ts
中并设置
import calculateReducer from './calculate'
export default configureStore({
reducer: {
calculate:calculateReducer
}
})
export { increment } from './calculate'
- 全局注入
在main.ts
中注入store
import store from "./store";
import { Provider } from "react-redux";
ReactDOM.createRoot(...).render(
...
<Provider store={store}>
...
</Provider>
...
);
- 在
xxx.tsx
组件页面中使用
可以看到,报了TypeScript
相关的类型错误
这需要返回store/index.ts
中导出类型
export type Store = ReturnType<typeof store.getState>
并在xxx.tsx
组件内导入类型并作为泛型传入
import { ...,Store } from "../store";
... useSelector<Store>(...);
- 预览
- 支持异步
一般使用第三方中间件redux-saga
或redux-thunk
,不过笔者从工作开始,就没在状态程序中写过异步,感觉不甚重要,此处就不引入了
6.集成antd
- 安装
yarn add antd
- 使用
import { DatePicker } from 'antd'
function Atd() {
return <>
<DatePicker/>
</>;
}
export default Atd;
- 预览
- 全局配置
预览中可以看到,默认语言是英文
找到app.ts
文件,修改如下
再次预览,就变成中文了
image.png7.集成umi-request
- 安装
yarn add umi-request
- 配置
在根目录下新建api
文件夹,在api
下新建index.ts
文件,此处统一进行配置。其实和axios
一样,都是设置下拦截器统一处理请求的发起和接收
一般来说,在请求发起时拦截,是为了统一增加字段,比如token
。又或者,改变url
前缀,毕竟同一个项目可能需要向不同的后端服务发起请求;在响应接收阶段,则是统一输出,以使业务代码接收统一
import { extend, type ResponseError } from "umi-request";
import { stringify } from "qs";
import { message } from "antd";
const errorHandler = (
err: ResponseError & {
description?: string;
}
) => {
message.destroy();
message.error(err.description || "接口请求失败,请稍后再试...");
return {
code: -1,
};
};
// 拦截错误
const Request = extend({
timeout: 30000,
errorHandler,
});
// 拦截请求
Request.interceptors.request.use((url) => {
return {
url,
};
});
// 拦截响应
Request.interceptors.response.use((response) => {
return new Promise(function (resolve, reject) {
response.text().then((res) => {
let resData;
try {
resData = JSON.parse(res);
} catch (err) {
resData = { code: -1 };
}
if (resData.responseCode === 200) {
resolve(resData);
} else {
reject(resData);
}
});
});
});
export default Request;
// 序列化函数--辅助用
export function stringifyWithTrim(params = {}) {
function encoder(str:unknown, defaultEncoder:(str: unknown, defaultEncoder?: unknown, charset?: string) => string) {
if (typeof str === "string") {
return defaultEncoder(str.trim());
}
return defaultEncoder(str);
}
return stringify(params, { encoder });
}
- 使用
在api
文件夹下按模块新建并引入index.ts
中的导出模块进行接口请求即可,一般对于get
请求还需要对参数进行序列化
import request,{stringifyWithTrim} from "./index.ts";
export default {
post(params){
return request(url, {
method: "POST",
params,
})
},
get(query){
return request(`url?${stringifyWithTrim(query)}`)
}
}
8.css隔离
样式方案有很多,你也可以使用less
、scss
等预处理器。笔者这里以模块化方式举例
- css模块
在根目录下创建style
文件夹,并新建xxx.module.css
.wrapper {
color: red;
}
- 使用
首先,安装classnames
yarn add classnames
接着导入并使用
import classnames from "classnames";
import CSS from "../style/css.module.css";
function Css() {
const klass = classnames({
[CSS.wrapper]: true,
});
return <div className={klass}>css</div>;
}
export default Css;
- 预览
9.代码质量与提交规范
这里的完整步骤可以参考vite+vue工程化配置
首先,安装相关依赖
yarn add lint-staged husky @commitlint/config-conventional @commitlint/cli -D
然后,修改package.json
"scripts": {
"prepare": "husky install",
"check": "lint-staged"
},
"lint-staged": {
"*.{tsx,ts}": [
"npm run lint"
]
}
接着,注册husky
相关钩子
npx husky add .husky/pre-commit "npm run check"
git add .husky/pre-commit
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'
再然后,在根目录下定义commitlint.config.cjs
配置文件
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feature", // 迭代功能
"conf", // 修改构建配置
"fixbug", // 修复bug
"refactor", // 代码重构
"optimize", // 代码优化
"style", // 仅修改样式文件
"docs", // 文档补充说明
],
],
"header-max-length": [0, "always", 72], //限制最长72
},
};
最后,测试下。可以成功拦截
image.png
网友评论