文章目录
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 三、TS 应用:JS神助攻 - 强类型
- 四、JWT、用户认证与异步请求
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 1~3
- 4.用Grid和Flexbox布局优化项目列表页面
- 5.使用 emotion 自定义样式组件
- 6.完善项目列表页面样式
- 7.遗留问题处理
学习内容来源: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 应用:实现项目列表
- 【实战】 二、React 与 Hook 应用:实现项目列表 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二)
三、TS 应用:JS神助攻 - 强类型
- 【实战】三、 TS 应用:JS神助攻 - 强类型 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(三)
四、JWT、用户认证与异步请求
- 【实战】四、 JWT、用户认证与异步请求(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(四)
- 【实战】四、 JWT、用户认证与异步请求(下) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(五)
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
1~3
- 【实战】 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(六)
4.用Grid和Flexbox布局优化项目列表页面
编辑 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
5.使用 emotion 自定义样式组件
区别于
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``;
复制
6.完善项目列表页面样式
编辑 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
的引入
本次美化成果:
7.遗留问题处理
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( // <React.StrictMode> <AppProvider> <DevTools/> <App /> </AppProvider> // </React.StrictMode> ); }); ...
复制
修改 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>; };
复制
部分引用笔记还在草稿阶段,敬请期待。。。