学习内容来源:React + React Hook + TS 最佳实践-慕课网
相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:
项 | 版本 |
---|---|
react & react-dom | ^18.2.0 |
react-router & react-router-dom | ^6.11.2 |
antd | ^4.24.8 |
@commitlint/cli & @commitlint/config-conventional | ^17.4.4 |
eslint-config-prettier | ^8.6.0 |
husky | ^8.0.3 |
lint-staged | ^13.1.2 |
prettier | 2.8.4 |
json-server | 0.17.2 |
craco-less | ^2.0.0 |
@craco/craco | ^7.1.0 |
qs | ^6.11.0 |
dayjs | ^1.11.7 |
react-helmet | ^6.1.0 |
@types/react-helmet | ^6.1.6 |
react-query | ^6.1.0 |
@welldone-software/why-did-you-render | ^7.0.1 |
@emotion/react & @emotion/styled | ^11.10.6 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
- 【实战】 一、项目起航:项目初始化与配置 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(一)
- 【实战】 二、React 与 Hook 应用:实现项目列表 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二)
- 【实战】三、 TS 应用:JS神助攻 - 强类型 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(三)
- 【实战】四、 JWT、用户认证与异步请求(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(四)
- 【实战】四、 JWT、用户认证与异步请求(下) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(五)
1~3
- 【实战】 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(六)
编辑 src\authenticated-app.tsx
import styled from "@emotion/styled";
import { useAuth } from "context/auth-context";
import { ProjectList } from "screens/ProjectList";
/**
* grid 和 flex 各自的应用场景
* 1. 要考虑,是一维布局 还是 二维布局
* 一般来说,一维布局用flex,二维布局用grid
* 2. 是从内容出发还是从布局出发?
* 从内容出发:你先有一组内容(数量一般不固定),然后希望他们均匀的分布在容器中,由内容自己的大小决定占据的空间
* 从布局出发:先规划网格(数量一般比较固定),然后再把元素往里填充
* 从内容出发,用flex
* 从布局出发,用grid
*/
export const AuthenticatedApp = () => {
const { logout } = useAuth();
return (
<Container>
<Header>
<HeaderLeft>
<h3>Logo</h3>
<h3>项目</h3>
<h3>用户</h3>
</HeaderLeft>
<HeaderRight>
<button onClick={logout}>登出</button>
</HeaderRight>
</Header>
<Nav>Nav</Nav>
<Main>
<ProjectList />
</Main>
<Aside>Aside</Aside>
<Footer>Footer</Footer>
</Container>
);
};
const Container = styled.div`
display: grid;
grid-template-rows: 6rem 1fr 6rem; // 3行每行高度(fr 单位是一个自适应单位,表示剩余空间中所占比例)
grid-template-columns: 20rem 1fr 20rem;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
/* grid-gap: 10rem; // 每部分之间的间隔 */
height: 100vh;
`;
// grid-area 用来给 grid 子元素起名字
const Header = styled.header`
grid-area: header;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
`;
const HeaderLeft = styled.div`
display: flex;
align-items: center;
`;
const HeaderRight = styled.div``;
const Main = styled.main`
grid-area: main;
/* height: calc(100vh - 6rem); */
`;
const Nav = styled.nav`
grid-area: nav;
`;
const Aside = styled.aside`
grid-area: aside;
`;
const Footer = styled.footer`
grid-area: footer;
`;
grid 和 flex 各自的应用场景
1.要考虑,是一维布局 还是 二维布局
- 一般来说,一维布局用flex,二维布局用grid
2.是从内容出发还是从布局出发?
- 从内容出发:你先有一组内容(数量一般不固定),然后希望他们均匀的分布在容器中,由内容自己的大小决定占据的空间
- 从布局出发:先规划网格(数量一般比较固定),然后再把元素往里填充
- 从内容出发,用flex
- 从布局出发,用grid
- CSS Grid: Holy Grail Layout | DigitalOcean
区别于
react
的功能组件emotion
组件我们称其为 样式组件
新建 src\components\lib.tsx
(emotion 自定义样式组件库):
import styled from '@emotion/styled'
export const Row = styled.div<{
gap?: number | boolean,
butween?: boolean,
marginBottom?: number
}>`
display: flex;
align-items: center;
justify-content: ${props => props.butween ? 'space-between' : undefined };
margin-bottom: ${ props => props.marginBottom + 'rem' };
> * {
/* 直接子元素强制控制样式 */
margin-top: 0 !important;
margin-bottom: 0 !important;
margin-right: ${ props => typeof props.gap === 'number' ? props.gap + 'rem' : props.gap ? '2rem' : undefined };
}
`
上一节代码是为了学习 grid
,为实现后续效果,清除无用代码并使用自定义样式组件(src\authenticated-app.tsx
):
import styled from "@emotion/styled";
import { Row } from "components/lib";
import { useAuth } from "context/auth-context";
import { ProjectList } from "screens/ProjectList";
export const AuthenticatedApp = () => {
const { logout } = useAuth();
return (
<Container>
<Header butween={ true }>
<HeaderLeft gap={ true }>
<h2>Logo</h2>
<h2>项目</h2>
<h2>用户</h2>
</HeaderLeft>
<HeaderRight>
<button onClick={logout}>登出</button>
</HeaderRight>
</Header>
<Main>
<ProjectList />
</Main>
</Container>
);
};
const Container = styled.div`
display: grid;
grid-template-rows: 6rem 1fr;
height: 100vh;
`;
// grid-area 用来给 grid 子元素起名字
const Header = styled(Row)``;
const HeaderLeft = styled(Row)``;
const HeaderRight = styled.div``;
const Main = styled.main``;
编辑 src\screens\ProjectList\components\SearchPanel.tsx
(使用Form.Item
、 emotion
的 css
属性):
// /** @jsx jsx */
// import { jsx } from '@emotion/react'
/** @jsxImportSource @emotion/react */
...
export const SearchPanel = ({ users, param, setParam }: SearchPanelProps) => {
return (
<Form css={{ marginBottom: '2rem', '>*': '' }} layout="inline">
<Form.Item>
<Input placeholder='项目名' ... />
</Form.Item>
<Form.Item>
<Select>...</Select>
</Form.Item>
</Form>
);
};
在使用
emotion
的css
属性时 需要注意,由于React 17
的自动导入破坏了@emotion
自身运行时的支持,从而将导致emotion
的jsx
运行时导入后未使用,也就无法使用emotion
的css
属性将/** @jsx jsx */
改为/** @jsxImportSource @emotion/react */
即可
截止2023.05.04,官方文档中依旧是/** @jsx jsx */
的导入方式:Emotion – The css Prop
编辑 src\screens\ProjectList\index.tsx
(调整与外部间距):
...
export const ProjectList = () => {
...
return (
<Container>
<h1>项目列表</h1>
<SearchPanel users={users} param={param} setParam={setParam} />
<List users={users} list={list} />
</Container>
);
};
const Container = styled.div`
padding: 3.2rem
`
安装 dayjs 库:
npm i dayjs --force
截止 2020.9 moment 库已停止开发
编辑 src\screens\ProjectList\components\List.tsx
(表中新增部门和创建时间字段):
...
import dayjs from 'dayjs'
interface Project {
...
created: number;
}
...
export const List = ({ users, list }: ListProps) => {
return (
<Table
pagination={false}
columns={[
{
title: "名称",
...
},
{
title: "部门",
dataIndex: "organization"
},
{
title: "负责人",
...
},
{
title: "创建时间",
render: (text, project) => (
<span>
{project.created ? dayjs(project.created).format('YYYY-MM-DD') : '无'}
</span>
),
},
]}
dataSource={list}
></Table>
);
};
将预置 svg 文件(software-logo.svg) 放入src\assets
使用方式推荐 ReactComponent as SVG
编辑 src\authenticated-app.tsx
(添加 Logo、优化登出、header
添加底部阴影)(部分未修改内容省略):
...
import { ReactComponent as SoftwareLogo } from 'assets/software-logo.svg'
import { Button, Dropdown } from "antd";
import type { MenuProps } from 'antd';
export const AuthenticatedApp = () => {
const { logout, user } = useAuth();
const items: MenuProps['items'] = [{
key: 1,
label: '登出',
onClick: logout
}]
return (
<Container>
<Header between={true}>
<HeaderLeft gap={true}>
<SoftwareLogo width='18rem' color='rgb(38,132,255)'/>
<h2>项目</h2>
<h2>用户</h2>
</HeaderLeft>
<HeaderRight>
<Dropdown menu={{ items }}>
<Button type='link' onClick={e => e.preventDefault()}>
Hi, { user?.name }
</Button>
</Dropdown>
</HeaderRight>
</Header>
<Main>... </Main>
</Container>
);
};
const Container = styled.div`...`;
// grid-area 用来给 grid 子元素起名字
const Header = styled(Row)`
padding: 3.2rem;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1);
z-index: 1;
`;
...
Dropdown
的overlay
属性已被menu
属性取代,注意MenuProps
的引入
src\utils\index.ts
解开 @ts-ignore
"封印"的报错
...
export const isVoid = (val: unknown) => val === undefined || val === null || val === ''
export const cleanObject = (obj: { [key: string]: unknown }) => {
const res = { ...obj };
Object.keys(res).forEach((key) => {
const val = res[key];
if (isVoid(val)) {
delete res[key];
}
});
return res;
};
export const useMount = (cbk: () => void) =>
useEffect(() => {
// TODO 依赖项里加上callback 会造成无限循环,这个和 useCallback 以及 useMemo 相关
cbk();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
...
- object 类型涵盖很广(
function
、new RegExp('')
…),若只是想用键值对的形式可以使用上面所示的形式{ [key: string]: unknown }
- 若
val = res[key]
的值是false
或是false
的字面量,isFalsy
也会识别,然后就会有bug
,比如checked
,visible
等
安装另一个版本的 jira-dev-tool
(api
有更改):
npm i jira-dev-tool # --force (可能需要强制安装)
若有报错,可以将
node_modules
清空再装或是按照报错提示操作
修改项目入口文件 src\index.tsx
(部分未修改内容省略):
...
import { loadServer, DevTools } from "jira-dev-tool";
...
loadServer(() => {
root.render(
//
<AppProvider>
<DevTools/>
<App />
</AppProvider>
//
);
});
...
修改 src\context\index.tsx
(部分未修改内容省略):
...
import { QueryClient, QueryClientProvider } from 'react-query'
export const AppProvider = ({ children }: { children: ReactNode }) => {
return <QueryClientProvider client={new QueryClient()}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>;
};
部分引用笔记还在草稿阶段,敬请期待。。。