创建react+ts的项目:
全局安装 react 脚手架(已经安装的忽略):npm i create-react-app -g
npx create-react-app 项目名 --template typescript
创建成功之后的一级目录
下面简单说一下每个文件夹里面的文件和修改了哪些内容
1:node_modules,不用多说,npm的依赖包。
2:public,不用多说,创建项目之后里面只留下 一个 favicon.ico 和一个入口 index.html 即可。
3:src,业务目录,基本上我们所有的业务代码,图片,css等都会在里面。
-------3.1:src下面的所有目录
--------------3.1.1:src/asstes,我们存放图片或者css以及其他静态资源
--------------3.1.2:src/asstes/styles/base.css,项目的初始化样式,这里我简单写了一点,可自行修改
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
height: 100%;
font-size: 14px;
user-select: none;
}
body{
font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif;
height: 100%;
color: #333;
}
a {
text-decoration: none;
color: #333;
outline: none;
}
i {
font-style: normal;
}
img {
max-width: 100%;
max-height: 100%;
vertical-align: middle;
}
ul {
list-style: none;
}
-------3.2:src/components,我们在项目中用到了公共组件,这里我只简单封装了一个鉴权路由组件
//自己封装的 history 路由跳转,在tsx和ts中都能使用。使用 react-router-dom 里面的 useHistory Hooks 只能在tsx函数组件中使用,不能在ts文件中使用,不太方便,所以这里自己封装。
import history from "@/utils/history";
// 自己封装的获取token的函数
import { hasToken } from "@//utils/storage";
import React from "react";
import { Redirect, Route } from "react-router-dom";
type AtahType = {
path: string; //传入的路由路径,必传
component: React.FC; //引入react中以及封装好的 组件 的类型声明,必传
};
function AuthRoute({ path, component: Com }: AtahType) {
return (
//引入 react-router-dom 中的 Route 组件
<Route
// 路由路径
path={path}
render={() => {
// 需要token的页面使用本组件,检查到有token,就正常访问
if (hasToken()) return <Com />;
else {
// 没有token,提示一下。这里随便写的alret,可自行修改其他方式提示(比如antd的轻提示)
alert("登录失效");
return (
// 路由重定向到 登录页面
<Redirect
to={{
pathname: "/Login",
// 附带 回跳 信息,有些业务需求 登录之后回到 上一个页面的时候可以使用(比如:在个人详情页,由于某些原因token失效,或者本地删除了token,那就会回到登录页,登录成功之后会回跳到 个人详情 页)
state: { from: history.location.pathname },
}}
/>
);
}
}}
/>
);
}
// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoAuthRoute = React.memo(AuthRoute);
// 默认导出
export default MemoAuthRoute;
-------3.3:src/pages,我们所有的组件
--------------3.3.1:src/pages/Home/index.tsx,简单声明一个组件
import React from "react";
function Home() {
return <div>Home</div>;
}
// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoHome = React.memo(Home);
// 默认导出
export default MemoHome;
--------------3.3.2:src/pages/Layout/index.tsx,简单声明一个组件(一般会把Layout作为二级路由页面,看各位需求)
import React from "react";
function Layout() {
return (
<div>
<h1>Layout页面</h1>
</div>
);
}
// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoLayout = React.memo(Layout);
// 默认导出
export default MemoLayout;
--------------3.3.3:src/pages/Home/Login.tsx,简单声明一个组件,包含了index.module.scss,模块化思想的css,下面会详细介绍
index.module.scss
.Login {
width: 500px;
height: 500px;
background-color: red;
// 使用 global 来声明 除了第一个盒子 会模块化自动随机生成 calss类名,global里面的类名和元素不会变更。这样在控制台调试样式的时候不会混乱
:global {
h1 {
background-color: aquamarine;
font-size: 40px;
}
}
}
index.tsx
import React from "react";
// 自己封装的 axios 初始基地址,会根据开发环境或者打包环境来自动改变
import { baseURL } from "@/utils/http";
// 导入模块化的css
import styles from "./index.module.scss";
function Login() {
return (
// 定义模块化css类名
<div className={styles.Login}>
<h1>登录页</h1>
<h2>全局变量:{baseURL}</h2>
</div>
);
}
// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoLogin = React.memo(Login);
// 默认导出
export default MemoLogin;
-------3.4:src/store,模块化仓库 redux 的所有文件
--------------3.4.1:src/store/index.tsx,仓库的总出口
// 导入 redux
import { applyMiddleware, legacy_createStore as createStore } from 'redux'
// 导入自己封装的 rootReducer
import rootReducer from './reducer'
// 导入调试工具和 异步的 redux(用来发送异步请求)
// 调试工具需要下载谷歌 扩展程序 我用的是 Redux DevTools 3.0.17
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
// 创建仓库实例
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))
// 声明 RootState,在使用仓库的时候用来使用
export type RootState = ReturnType<typeof store.getState>
// 声明 AppDispatch,在异步请求的时候来使用
export type AppDispatch = typeof store.dispatch
// 导出仓库实例
export default store
--------------3.4.2:src/store/action/login.ts,登录模块的action
// 导入总仓库和自己声明的 AppDispatch
import store, { AppDispatch } from ".."
// import http from "@/utils/http"
export const addNunAction = () => {
// 返回一个函数,用来处理异步操作
return async (dispatch: AppDispatch) => {
// 可以异步发送请求,这里我简单模拟一个异步操作
// const res = await http.get(``)
// console.log('----', res);
window.setTimeout(() => {
// 从仓库中获取之前的 num 值
const { num } = store.getState().loginStore
// 设置新的 num +1
dispatch({ type: 'login/addNum', payload: num + 1 })
}, 100);
}
}
--------------3.4.3:src/store/reducer/login.ts,登录模块的reducer
// 导入自己封装的 关于 LoginReducer 的ts声明文件
import { LoginType } from "@/types"
// 初始化状态
const initState: LoginType = {
num: 0,
active: 0,
}
// 定义 action 类型
type LoginActionType =
{ type: 'login/addNum', payload: number }
// 频道 reducer
export default function loginReducer(state = initState, action: LoginActionType) {
switch (action.type) {
case 'login/addNum':
return { ...state, num: action.payload }
default:
return state
}
}
--------------3.4.4:src/store/reducer/index.ts,reducer模块的总出口文件(用来合并reducer)
// 导入合并reducer的依赖
import { combineReducers } from 'redux'
// 导入 登录 模块的 reducer
import loginReducer from './login'
// 合并 reducer
const rootReducer = combineReducers({
loginStore: loginReducer,
})
// 默认导出
export default rootReducer
-------3.5:src/types,整个项目的声明文件
--------------3.5.1:src/types/store/login.d.ts,关于仓库/登录模块reducer的声明文件,这里我简单声明了一下,后面根据项目需求自己定义
export type LoginType = {
num: number;
active: number;
}
--------------3.5.2:src/types/declaration.d.ts,一些第三方包没有ts声明,scss或者less,以及图片 都需要在这里定义,避免ts类型检查的报错。这里我简单定义了一下,后面根据项目需求自己完善
declare module 'history'
declare module '*.scss';
--------------3.5.3:src/types/index.d.ts,拿到所有定义的类型声明文件,在这里统一获取,并且导出。方便后期使用的时候管理
//获取上面定义的 关于仓库/登录模块reducer的声明文件 并且导出。后面模块多了,也是在这里全部获取并且导出。
export * from './store/login'
-------3.6:src/utils,项目里面的所有工具的封装
--------------3.6.1:src/utils/history.ts,自己封装的 路由 history,在函数组件tsx和ts中都能使用
// createBrowserHistory 和 createHashHistory。
// 这里我们简单说一下 createBrowserHistory地址栏不带#; createHashHistory 带#
// 根据公司要求自己选择,createBrowserHistory在项目上线的时候需要服务器映射等处理。
import { createHashHistory } from 'history'
const history = createHashHistory()
export default history
--------------3.6.2:src/utils/http.ts,基于 axios 封装的请求实例
// 导入 axios
import axios from "axios";
// 导入自己封装的 history 用来跳转。useHistory 在单纯的ts文件里面无法使用
import history from "./history";
// 导入自己封装的获取token信息的函数,和删除本地存储token信息的函数
import { getTokenInfo, removeTokenInfo } from "./storage";
// 请求基地址,根据开发环境和打包环境来自己设置。一般打包环境都为空
export const baseURL = process.env.NODE_ENV === "development" ? "开发环境" : "";
// 创建 axios 实例
const http = axios.create({
baseURL,
// 请求超过5m没有回来即停止请求,并且抛出异常
timeout: 5000,
});
// 设置一个请求状态开关,判断 请求正在发送 和 所有请求都发送完毕
let axajInd = 0;
// 请求拦截器
http.interceptors.request.use(
function (config: any) {
// 开关数量+1,表示现在有一个请求在发送。
axajInd++;
//可以在这里添加业务代码,比如:显示 loding状态
// 检查到有token,为所有请求添加请求体。Bearer字段为 我这边后端的需求,各位看自己 后端需求来修改
const { token } = getTokenInfo();
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
},
function (err) {
return Promise.reject(err);
}
);
// 设置一个定时器id,防止多个请求同时发送,并且token失效的情况,多次执行 业务 代码
let timeId = -1;
// 响应拦截器
http.interceptors.response.use(
function (response) {
// 一个请求发送完毕
axajInd--;
if (axajInd === 0) {
// 所有请求发送完毕,可以在这里添加业务代码,比如:隐藏 loding状态
}
// token过期,这里看后台的返回值设置的为多少
if (response.data.code === 401) {
// 先清理定时器,在执行。防止多次执行。即页面防抖原理
clearTimeout(timeId);
timeId = window.setTimeout(() => {
// 删除本地token信息
removeTokenInfo();
// 这里的提示统一简单用的alert,自己根据需求更改。比如:antd的message
alert("登录失效!");
// 回到登录页面
history.push("/Login");
}, 200);
} else if (response.data.code === 0) {
// 请求成功,状态也成功(根据后端定义的状态值自己修改)
// 这里暂时不做处理,我一般会根据具体需求在组件中自己定义 message
} else {
//这里一般是请求成功,但是状态码有问题,直接返回后端的提示信息。这里的msg是我这边后端的字段,自己根据后端字段来修改
alert(response.data.msg);
}
return response.data;
},
async function (err) {
// 请求错误的时候,直接把 axajInd 归零,防止页面一直处于 loding 状态
axajInd = 0;
// 如果在上面定义了发送请求前显示 loding状态,这里需要隐藏 loding
// 如果因为网络原因,response没有,给提示消息
if (!err.response) {
alert("网络繁忙,请稍后重试");
} else {
// 网络没问题,后台返回了有数据
alert("错误");
}
return Promise.reject(err);
}
);
// 导出 axios 实例
export default http;
--------------3.6.3:src/utils/storage.ts,用户信息(token)的本地存储的封装
// ------------------------------------token的本地存储------------------------------------
// 用户 Token 的本地缓存键名,自己定义
const TOKEN_KEY = 'XXXXX'
/**
* 从本地缓存中获取 Token 信息
*/
export const getTokenInfo = (): any => {
return JSON.parse(localStorage.getItem(TOKEN_KEY) || '{}')
}
/**
* 将 Token 信息存入缓存
* @param {Object} tokenInfo 从后端获取到的 Token 信息
*/
export const setTokenInfo = (tokenInfo: any): void => {
localStorage.setItem(TOKEN_KEY, JSON.stringify(tokenInfo))
}
/**
* 删除本地缓存中的 Token 信息
*/
export const removeTokenInfo = (): void => {
localStorage.removeItem(TOKEN_KEY)
}
/**
* 判断本地缓存中是否存在 Token 信息
*/
export const hasToken = (): boolean => {
return Boolean(getTokenInfo().token)
}
-------3.7:src/App.tsx,App总组件,一个项目的总的初始组件
// 导入初始化样式
import "@/assets/styles/base.css";
// 导入 修改仓库的 dispatch,和获取仓库数据的 useSelector
import { useDispatch, useSelector } from "react-redux";
// 导入自己定义的 RootState 声明类型
import { RootState } from "./store";
// 导入自己定义的异步addNunAction
import { addNunAction } from "./store/action/login";
// 关于路由
import React from "react";
import { Router, Route, Switch, Redirect } from "react-router-dom";
// 导入自己封装的 history
import history from "./utils/history";
// 导入自己封装的 鉴权路由
import AuthRoute from "./components/AuthRoute";
// 使用 React.lazy 懒加载页面
const Layout = React.lazy(() => import("./pages/Layout"));
const Login = React.lazy(() => import("./pages/Login"));
const Home = React.lazy(() => import("./pages/Home"));
function App() {
const dispatch = useDispatch();
return (
<div>
<h1>App总组件</h1>
{/* 点击这个按钮 控制 dispatch 来异步 修改仓库的 num 变量*/}
<button onClick={() => dispatch(addNunAction())}>点击+1</button>
<br />
<div onClick={() => history.push("/Login")}>去登录页</div>
<br />
<div onClick={() => history.push("/Layout")}>去Layout页面</div>
<br />
<div onClick={() => history.push("/Home")}>去Home页面</div>
<hr />
<Son1 />
<hr />
<Son2 />
{/* 关于路由 */}
{/* 这里需要把自己封装的 history 给第一级总路由器,不然项目中的路由信息会链接不上*/}
<Router history={history}>
{/* 使用了 React.lazy 需要定义一个加载中组件,这里我简单写一个。后面根据需求自己修改更好看的 加载中 组件*/}
<React.Suspense fallback={<div>加载中...</div>}>
{/* 使用 Switch 包裹路由,匹配到一个之后就不会往下匹配 */}
<Switch>
{/* 使用路由重定向,回到登录页 */}
<Redirect exact path="/" to="Login" />
{/* 普通路由,没有鉴权功能 */}
<Route path="/Login" component={Login} />
{/* 普通路由,没有鉴权功能 */}
<Route path="/Layout" component={Layout} />
{/* 鉴权路由,没有token,访问改路由,会重定向到 登录页面 */}
<AuthRoute path="/Home" component={Home} />
{/* 当所有路由匹配不到的时候显示 自己定义的 404 页面,这里我暂时没有定义 */}
{/* <Route path='*' component={NotFound} /> */}
</Switch>
</React.Suspense>
</Router>
</div>
);
}
const Son1 = function Son1() {
// 从仓库中获取响应式数据 num
const { num } = useSelector((state: RootState) => state.loginStore);
return (
<>
<h2>子组件1</h2>
<p>存在仓库的数字:{num}</p>
</>
);
};
const Son2 = function Son2() {
// 从仓库中获取响应式数据 num
const { num } = useSelector((state: RootState) => state.loginStore);
return (
<>
<h2>子组件2</h2>
<p>存在仓库的数字:{num}</p>
</>
);
};
const MemoApp = React.memo(App);
export default MemoApp;
-------3.8:src/index.tsx,项目的入口,这里一般不要写太复杂的业务逻辑
// 导入 App,总组件
import App from './App'
// 导入仓库
import store from './store/index'
// 导入仓库的入口 函数(可以简单这么理解。react的所有数据都是单向流动,即从父级页面向下流动。所有需要把仓库信息在第一级里面定义)
import { Provider } from 'react-redux'
// 导入 react 里面定义的 转化dom的方法(简单里面为把 dom 转化成react需要的格式)
import { createRoot } from 'react-dom/client';
// 获取 public/index.html里面id为root的根元素
const container = document.getElementById('root') as HTMLElement;
// 转化成react需求的格式
const root = createRoot(container);
root.render(
// 仓库信息在根组件中定义
<Provider store={store}>
<App />
</Provider>
);
4:.gitignore,git忽略跟踪的文件,不必多说
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
5:config-overrides.js,配置类似vue里面的@表示src/根目录
const path = require('path')
const { override, addWebpackAlias } = require('customize-cra')
// 添加 @ 别名
const webpackAlias = addWebpackAlias({
'@': path.resolve(__dirname, 'src'),
})
// 导出要进行覆盖的 webpack 配置
module.exports = override(webpackAlias)
6:package-lock.json,大家都懂。依赖包的信息,大同小异
7:package.json,为了配置类似vue里面的@表示src/根目录,我修改了 scripts 的 调试命令。大家可以复制我的信息
{
"name": "demo",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.3",
"@types/react": "^18.0.24",
"@types/react-dom": "^18.0.8",
"axios": "^1.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.4",
"react-router-dom": "5.3",
"react-scripts": "5.0.1",
"redux": "^4.2.0",
"redux-devtools-extension": "^2.13.9",
"redux-thunk": "^2.4.1",
"sass": "^1.55.0",
"typescript": "^4.8.4",
"web-vitals": "^2.1.4"
},
"scripts": {
"dev": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/history": "^5.0.0",
"@types/react-router-dom": "^5.3.3",
"customize-cra": "^1.0.0",
"react-app-rewired": "^2.2.1"
},
"homepage": "."
}
8:path.tsconfig.json,也是为了配置类似vue里面的@表示src/根目录
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
9:README.md,git文档信息。有需求自己修改,我反正从来没动过。
10:tsconfig.json,ts的声明,这里面的信息比较杂,这里不深入展开,可以直接复制我的
{
"extends": "./path.tsconfig.json",
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
经过一番“辛苦”的配置,满心欢喜的 npm run dev。然后你就会发现。
这里的原因个人判断是 redux 和 react-redux 版本更替过程中的ts类型声明问题(个人推断,各位大佬有很好的理解或者解决方法也麻烦告诉小弟~)
解决办法1:
在使用 useDispatch 的时候 添加 any 泛型
即:原来是 const dispatch = useDispatch();
修改为:const dispatch = useDispatch<any>();
但是这样在每次使用的时候都要定义any,不太友好。
你也可以自己定义一个函数,然后调用自己的函数
const useUserDispatch = () => useDispatch<any>();
即:原来是 const dispatch = useDispatch();
修改为:const dispatch = useUserDispatch();
但个人认为也不太好,毕竟多了一层封装。
解决办法2:修改源代码ts类型声明
1.找到 import { useDispatch, useSelector } from "react-redux";
2.按住键盘的Ctrl+鼠标左键打开
3.找到 AnyAction,继续按住键盘的Ctrl+鼠标左键打开
4.进入 node_modules > redux > index.d.ts 找到(或者你直接搜索下面的代码)
export interface Action<T = any> {
type: T
}
5.加上一个小小的?,一切都解决了
export interface Action<T = any> {
type?: T
}
再次 npm run dev ,你就会看到一个很丑的页面。点击按钮 点击+1 就能完成异步的 active 控制仓库的响应式数据。然后根据业务需求来美美的开发吧。
分享 redux 的调试工具
考虑到不能翻墙的小伙伴,我这里准备了一个链接
谷歌-设置-扩展程序-打开开发者模式-解压下载的文件-然后整个拖进去
打开控制台,就可以看到仓库数据的每一步变化。讲道理,个人觉得比vue的调试工具好用得多
完结撒花
各位大佬有不同的想法或者本人写错的地方,欢迎指正。