前端学习之React项目实战Ⅰ
项目创建
使用 react-create-app
初始化项目后:
react-app-env.d
用于引入预先定义好的类型
reportWebVitals.ts
用于埋点上报;
setupTests.ts
用于配置单元测试,默认使用 testing-library
进行测试;
基础配置
在tsconfig.json
里,在compilerOptions
下添加配置:baseUrl: "./src"
;将引入模块的绝对路径设置为 ./scr;
添加插件包:Prettier,配置参考官方文档:
npm install --save-dev --save-exact prettier
- 创建配置文件
.prettierrc.json
和忽略文件.prettierignore
- 手动格式化命令:
npx prettier --write .
;
- 配置 Commit 时自动格式化:
Pre-commit Hook
,使用命令:npx mrm@2 lint-staged
,然后会在package.json
中配置对各种后缀名文件的支持;
- 因使用
react-create-app
初始化项目自带 ESLint ,需要安装eslint-config-prettier才能让它们更和谐地工作,之后在package.json --> eslintConfig --> extends
添加**”prettier”**,以覆盖原来的规则;
安装工具,确保每次提交的 Commit comment 符合一定的规范,否则提交失败:
npm install --save-dev @commitlint/config-conventional @commitlint/cli
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
npx husky add .husky/commit-msg \"npx --no -- commitlint --edit '$1'\"
- 查看对应提交规则:conventional-changelog/commitlint
Mock 数据的配置:使用 json-server,快速地创建 REST API:
- 安装:
npm install -g json-server
- 配置:新建文件夹
__json_server_mock__
,创建数据文件db.json
,填充数据;
- 使用:
json-server --watch __json_server_mock__/db.json
--save-dev/-D
:安装到开发环境依赖;
--save-optional/-O
:安装到可选环境依赖;
--save-exact/-E
:精确安装
工程列表
主页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| export const ProjectListPage = () => { const [param, setParam] = useState({ name: "", personID: "" }) const [list, setList] = useState([]) const [users, setUsers] = useState([])
useEffect(() => { fetch(`${apiUrl}/users`).then(async response => { if(response.ok){ setUsers(await response.json()) } }) }, []) // 只触发一次 return (<div> <SearchPanel param={param} setParam={setParam} users={users} setUsers={setUsers}/> <List list={list} users={users}/> </div>) }
|
搜索框和下拉选择框:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const SearchPanel = ({param,setParam,users,setUsers}) => { return ( <div> <input type="text" value={param.name} onChange={e => setParam({...param, name:e.target.value})}></input> <select value={param.personID} onChange={e => setParam({...param,personId: e.target.value})}> <option value={''}>负责人</option> { users.map(user => <option key={user.id} value={user.id}>{user.name}</option>) } </select> </div> ) }
|
工程列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const List = ({list,users}) => { return ( <table> <thead> <tr><th>名称</th><th>负责人</th></tr> </thead> <tbody> { list.map(project => (<tr key={project.id}> <td>{project.name}</td> <td>{users.find(user => user.id === project.personId)?.name || '未知'}</td> </tr>)) } </tbody> </table> ) }
|
接口配置
- 在
src
下,创建两个文件.env
,.env.development
,对应生产和开发环境;
- 开发环境下,添加内容:
REACT_APP_API_URL=http://localhost:3001
;
- 使用:
const apiUrl = process.env.REACT_APP_API_URL
;
工具函数
在请求数据时,如搜索框和下拉框当有一处为空时,会给后端造成疑惑,是请求该字段为空的数据还是忽略该数据,这样的情况最好在前端避免,需要写一些工具代码进行处理:
1 2 3 4 5 6 7 8 9 10 11 12
| export const isFalsy = (value) => value === 0 ? false : !value
export const cleanObject = (obj) => { const result = { ...obj } Object.keys(result).forEach(key => { const value = result[key] if (isFalsy(value)) { delete result[key] } }) return result }
|
参数查询
安装参数拼接处理的外部库:qs
;使用工具函数处理查询的数据:
1 2 3 4 5 6 7 8
| useEffect(() => { fetch(`${apiUrl}/projects?${qs.stringify(cleanObject(param))}`) .then(async response => { if(response.ok){ setList(await response.json()) } }) }, [param])
|
自定义 Hook
useMount
1 2 3 4 5 6
| export const useMount = (callback) => { useEffect(() => { callback() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) }
|
当我们需要一个useEffect
只在组件挂载时执行一次时,需要在其第二个参数中传入空数组,但这样语义性并不强,所以可以封装一个useMount
,传入需要执行的回调函数即可;
useDebounce
Debounce 也就是防抖动,是处理一些需要短暂延时后再进行的操作,例如搜索框的内容提示等;以下是类似的案例,对搜索框发送请求进行 debounce:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export const useDebounce = (value, delay) => { const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => { const timeout = setTimeout(() => { setDebouncedValue(value) }, delay) return () => { clearTimeout(timeout) } }, [value, delay])
return debouncedValue }
|
登录页面
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
| import React, { FormEvent } from "react";
export const LoginPage = () => { const handleSubmit = (e: FormEvent<HTMLFormElement>) => { const login = (param: { username: string; password: string }) => { fetch(`${apiUrl}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(param), }).then(async (response) => { if (response.ok) {} }); }; e.preventDefault(); const username = (e.currentTarget.elements[0] as HTMLInputElement).value; const password = (e.currentTarget.elements[1] as HTMLInputElement).value; login({ username, password }); };
return ( <form onSubmit={handleSubmit}> <div> <label htmlFor="username">用户名</label> <input type="text" id={"username"}></input> </div> <div> <label htmlFor="password">密码</label> <input type="password" id={"password"}></input> </div> <button type="submit">登录</button> </form> ); };
|
这里我们再继续使用 json server
作为后端,之后会使用另外的插件,由于 json server
仅支持 RestAPI 风格,对登录验证这样的业务,需要进行中间件注入;
json server 中间件
创建文件__json_server_mock__/middleware.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| module.exports = (req, res, next) => { if (req.method === 'POST' && req.path === '/login') { if (req.body.username === 'mahoo12138' && req.body.password === 'xm12345678') { return res.status(200).json({ user: { token: '12345' } }) } else { return res.status(400).json({ message: '用户名或密码错误' }) } } next() }
|
注入中间件,在启动代码中修改为如下代码:
1
| json-server --watch __json_server_mock__/db.json --port 3001 --middlewares ./__json_server_mock__/middleware.js
|
context 存储全局状态
首先,为了更好地专注地进行学习,将使用另一个专用的插件 jira-dev-tool 作为后端,使用npx imooc-jira-tool
进行安装,使用方法:
1 2 3 4 5 6 7 8 9 10
| import { loadDevTools } from "jira-dev-tool";
loadDevTools(() => { ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ); });
|
jira-dev-tool使用本地的 localStorage 作为存储,需要与之建立联系并操作 Token;真实开发过程中往往会使用第三方或自研的SDK,往往不需要下列代码:
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
| export const localStorageKey = "__auth_provider_token__";
export const getToken = () => window.localStorage.getItem(localStorageKey);
export const handleUserResponse = ({ user }: { user: User }) => { window.localStorage.setItem(localStorageKey, user.token || ""); return user; };
export const login = (param: { username: string; password: string }) => { return fetch(`${apiUrl}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(param), }).then(async (response) => { if (response.ok) { return handleUserResponse(await response.json()); } else { return Promise.reject(param); } }); };
export const logout = async () => window.localStorage.removeItem(localStorageKey);
|
之后,利用上述的代码,封装一个提供登录注册数据持久化的 ContextProvider ,且通过自定义的 useAuth 还能在任意子组件获取到该 Context :
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
| import * as auth from "auth-provider";
interface AuthForm { username: string; password: string; }
const AuthContext = React.createContext< | { user: User | null; login: (form: AuthForm) => Promise<void>; register: (form: AuthForm) => Promise<void>; logout: () => Promise<void>; } | undefined >(undefined);
AuthContext.displayName = "AuthContext";
export const AuthProvider = ({ children }: { children: ReactNode }) => { const [user, setUser] = useState<User | null>(null);
const login = (form: AuthForm) => auth.login(form).then((u) => setUser(u)); const register = (form: AuthForm) => auth.register(form).then(setUser); const logout = () => auth.logout().then(() => setUser(null));
return ( <AuthContext.Provider children={children} value={{ user, login, register, logout }} /> ); };
export const useAuth = () => { const context = React.useContext(AuthContext); if (!context) { throw new Error("useAuth 必须在 AuthProvider 中使用"); } return context; };
|
项目结构优化
1 2 3 4 5 6 7 8 9
| function App() { const { user } = useAuth(); return ( <div className="App"> {/* <ProjectListPage /> */} {user ? <AuthenticateApp /> : <UnAuthenticateApp />} </div> ); }
|
将页面分为登录和未登录后两个部分,登录后即是项目列表页面,未登录包括注册登录页面:
1 2 3 4 5 6 7 8 9
| export const UnAuthenticateApp = () => { const [isLogin, setIsLogin] = useState(false); return ( <div> {isLogin ? <LoginPage /> : <RegisterPage />} <button onClick={(e) => setIsLogin(!isLogin)}>切换</button> </div> ); };
|
页面样式
引入 Ant Design
使用 Emotion 模块
安装:npm i @emotion/react @emotion/styled
新增普通css组件
1 2 3 4 5 6 7 8 9
| // 引入emotion import styled from "@emotion/styled”; // 使用emotion 创建css组件 const Container = styled.div` display: flex; flex-direction: column; align-items: center; min-height: 100vh; `;
|
使用行内样式
首先,需要在代码文件顶端添加:/* @jsxImportSource @emotion/react */
,标识当前组件用了emotion行内样式;使用如下:
1 2 3
| <Form css={{ marginBottom: "2rem", ">*": "" }} layout={"inline"}> {} </Form>
|
给已存在组件加样式
1 2 3 4 5 6 7 8 9 10
| // Card 是antd已存在的组件 const ShadowCard = styled(Card)` width: 40rem; min-height: 56rem; padding: 3.2rem 4rem; border-radius: 0.3rem; box-sizing: border-box; box-shadow: rgba(0, 0, 0, 0.1) 0 0 10px; text-align: center; `;
|
配置基础全局样式
1 2 3 4 5 6 7 8 9
| html { font-size: 62.5%; } html body #root .App { min-height: 100vh; }
|