学习内容来源: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 企业级项目(五)
antd + emotion
官网:Ant Design - 一套企业级 UI 设计语言和 React 组件库
# npm i antd
# yarn add antd
npm i antd --force
jira-dev-tool
依赖树中包含 antd,可尝试不安装直接使用- 鉴于
jira-dev-tool
长时间没有更新,依赖树有较多问题,建议清理node_modules
,执行npm i --force
重新安装依赖
src\index.tsx
中引入 antd.less
(一定要在 jira-dev-tool
之后引入,以便后续修改主题样式能够覆盖到 jira-dev-tool
)import { loadDevTools } from "jira-dev-tool";
import 'antd/dist/antd.less'
为对 create-react-app
进行自定义配置,需要安装 craco
和它的子依赖 craco-less
:
# npm i @craco/craco
# yarn add @craco/craco
npm i @craco/craco --force
npm i -D craco-less --force
https://4x.ant.design/docs/react/use-with-create-react-app-cn#高级配置
package.json
中脚本指令"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
+ "start": "craco start",
+ "build": "craco build",
+ "test": "craco test",
}
项目根目录下新建文件 craco.config.js
,复制文档中对应部分代码,并配置需要修改的主题变量:
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { '@primary-color': 'rgb(0, 82, 204)', '@font-size-base': '16px' },
javascriptEnabled: true,
},
},
},
},
],
};
npm start
重新启动项目src\unauthenticated-app\login.tsx
:import { useAuth } from "context/auth-context";
import { Form, Button, Input } from "antd"
export const Login = () => {
const { login, user } = useAuth();
const handleSubmit = (values: { username: string, password: string }) => {
login(values);
};
return (
<Form onFinish={handleSubmit}>
<Form.Item name='username' rules={[{ required: true, message: '请输入用户名' }]}>
<Input placeholder="用户名" type="text" id="username" />
</Form.Item>
<Form.Item name='password' rules={[{ required: true, message: '请输入密码' }]}>
<Input placeholder="密码" type="password" id="password" />
</Form.Item>
<Form.Item>
<Button htmlType="submit" type="primary">登录</Button>
</Form.Item>
</Form>
);
};
查看页面效果,并尝试 登录 功能
修改注册页面 src\unauthenticated-app\register.tsx
:
import { useAuth } from "context/auth-context";
import { Form, Button, Input } from "antd"
export const Register = () => {
const { register, user } = useAuth();
const handleSubmit = (values: { username: string, password: string }) => {
register(values);
};
return (
<Form onFinish={handleSubmit}>
<Form.Item name='username' rules={[{ required: true, message: '请输入用户名' }]}>
<Input placeholder="用户名" type="text" id="username" />
</Form.Item>
<Form.Item name='password' rules={[{ required: true, message: '请输入密码' }]}>
<Input placeholder="密码" type="password" id="password" />
</Form.Item>
<Form.Item>
<Button htmlType="submit" type="primary">注册</Button>
</Form.Item>
</Form>
);
};
从登录页切换到注册页,查看页面效果,并尝试 注册 功能
接下来修改 src\unauthenticated-app\index.tsx
:
import { useState } from "react";
import { Login } from "./login";
import { Register } from "./register";
import { Card, Button } from 'antd';
export const UnauthenticatedApp = () => {
const [isRegister, setIsRegister] = useState(false);
return (<Card style={{ display: 'flex', justifyContent: 'center' }}>
{isRegister ? <Register /> : <Login />}
<Button type='primary' onClick={() => setIsRegister(!isRegister)}>
切换到{isRegister ? "登录" : "注册"}
</Button>
</Card>
);
};
现在较之前页面好看多了
src\screens\ProjectList\components\List.tsx
(部分未改动省略):import { Table } from "antd";
import { User } from "./SearchPanel";
...
export const List = ({ users, list }: ListProps) => {
return <Table pagination={false} columns={[{
title: '名称',
dataIndex: 'name',
sorter: (a, b) => a.name.localeCompare(b.name)
}, {
title: '负责人',
render: (text, project) => <span>{users.find((user) => user.id === project.personId)?.name || "未知"}</span>
}]} dataSource={list}></Table>
};
- localeCompare 可排序中文字符
在引入
antd
的Table
后,先不给columns
属性赋值,而是先赋值dataSource
,然后将鼠标放于columns
上,这时便可见:
(property) TableProps
.columns?: ColumnsType | undefined
TS
的类型推断起作用了:
- 通过
list
的值类型为Project[]
,推断出dataSource?: RcTableProps
中['data'] data
类型为Project[]
- 推断出
dataSource?: RcTableProps
中['data'] RecordType
类型为Project[]
- 推断出
columns
类型为(property) TableProps
.columns?: ColumnsType | undefined
src\screens\ProjectList\components\SearchPanel.tsx
import { Form, Input, Select } from "antd";
...
export const SearchPanel = ({ users, param, setParam }: SearchPanelProps) => {
return (
<Form>
<Input
type="text"
value={param.name}
onChange={(evt) =>
setParam({
...param,
name: evt.target.value,
})
}
/>
<Select
value={param.personId}
onChange={value =>
setParam({
...param,
personId: value,
})
}
>
<Select.Option value="">负责人</Select.Option>
{users.map((user) => (
<Select.Option key={user.id} value={user.id}>
{user.name}
</Select.Option>
))}
</Select>
</Form>
);
};
以下部分是课件原文:
CSS-in-JS 不是指某一个具体的库,是指组织CSS代码的一种方式,代表库有 styled-component 和 emotion
(1)传统CSS的缺陷
①缺乏模块组织
传统的JS和CSS都没有模块的概念,后来在JS界陆续有了 CommonJS 和 ECMAScript Module,CSS-in-JS可以用模块化的方式组织CSS,依托于JS的模块化方案,比如:
// button1.ts import styled from '@emotion/styled' export const Button = styled.button` color: turquoise; `
// button2.ts import styled from '@emotion/styled' export const Button = styled.button` font-size: 16px; `
②缺乏作用域
传统的CSS只有一个全局作用域,比如说一个class可以匹配全局的任意元素。随着项目成长,CSS会变得越来越难以组织,最终导致失控。CSS-in-JS可以通过生成独特的选择符,来实现作用域的效果
const css = styleBlock => { const className = someHash(styleBlock); const styleEl = document.createElement('style'); styleEl.textContent = ` .${className} { ${styleBlock} } `; document.head.appendChild(styleEl); return className; }; const className = css(` color: red; padding: 20px; `); // 'c23j4'
③隐式依赖,让样式难以追踪
比如这个CSS样式:
.target .name h1 { color: red } body #container h1 { color: green }
doctype html> <html lang="en"> <body> <div id='container'> <div class='target'> <div class='name'> <h1>我是啥颜色?h1> div> div> div> body> html>
那么这个h1元素最终显式为什么颜色?加入你想要追踪这个影响这个h1的样式,怎么追踪?
而CSS-in-JS的方案就简单直接、易于追踪export const Title = styled.h1` color: green; `
我是啥颜色? ④没有变量
传统的CSS规则里没有变量,但是在 CSS-in-JS 中可以方便地控制变量
const Container = styled.div(props => ({ display: 'flex', flexDirection: props.column && 'column' }))
⑤CSS选择器与HTML元素耦合
.target .name h1 { color: red } body #container h1 { color: green }
doctype html> <html lang="en"> <body> <div id='container'> <div class='target'> <div class='name'> <h1>我是啥颜色?h1> div> div> div> body> html>
如果你想把
h1
改成h2
,必须要同时改动 CSS 和 HTML。而在CSS-in-JS中,HTML和CSS是结合在一起的,易于修改(2)Emotion 介绍
Emotion 是目前最受欢迎的 CSS-in-JS 库之一,它还对 React 作了很好的适应,可以方便地创建 styled component,也支持写行内样式:
/** @jsx jsx */ import { jsx } from '@emotion/react' render(
This has a hotpink background.)这种写法比起React自带的style的写法功能更强大,比如可以处理级联、伪类等style处理的不了的情况
src\App.css
清除原有样式,填入如下内容:html {
/* rem em */
/* em 相对于父元素的 font-size */
/* rem 相对于根元素的 font-size,r root */
/* 浏览器默认 font-size 16px */
/* 16px * 62.5% = 10px */
/* 1rem === 10px */
font-size: 62.5%;
}
html body #root .App {
min-height: 100vh;
}
删掉文件 src\index.css
并去掉在 src\index.tsx
中的引用,后续全局样式都在 src\App.css
中添加
npm i @emotion/react @emotion/styled --force
编辑 src\unauthenticated-app\index.tsx
(部分原有内容省略)
...
import { Card, Button } from "antd";
import styled from "@emotion/styled";
export const UnauthenticatedApp = () => {
...
return (
<Container>
<Card>
...
</Card>
</Container>
);
};
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
justify-content: center;
`
相当于将
div
添加以css-[hashcode]
命名的class
并自定义样式后 封装为StyledComponent
类型的 自定义组件Container
(仅添加样式)const Container: StyledComponent<{ theme?: Theme | undefined; as?: React.ElementType<any> | undefined; }, React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>
继续编辑 src\unauthenticated-app\index.tsx
(部分原有内容省略)
...
import { Card, Button } from "antd";
import styled from "@emotion/styled";
export const UnauthenticatedApp = () => {
...
return (
<Container>
<ShadowCard>
...
</ShadowCard>
</Container>
);
};
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;
`
...
相当于将
Card
添加以css-[hashcode]
命名的class
并自定义样式后 封装为StyledComponent
类型的 自定义组件ShadowCard
(不仅添加样式,还将 CardProps 原有属性原封不动还原)const ShadowCard: StyledComponent<CardProps & React.RefAttributes<HTMLDivElement> & { theme?: Theme | undefined; }, {}, {}>
新建 src\assets
,将预置 svg 文件放入(left.svg、logo.svg、right.svg)
继续编辑 src\unauthenticated-app\index.tsx
(部分原有内容省略):切换文案修改并使用 link
类型 button
;添加 logo、标题和背景图
...
import { Card, Button, Divider } from "antd";
import styled from "@emotion/styled";
import left from 'assets/left.svg'
import logo from 'assets/logo.svg'
import right from 'assets/right.svg'
export const UnauthenticatedApp = () => {
...
return (
<Container>
<Header/>
<Background/>
<ShadowCard>
<Title>
{isRegister ? '请注册' : '请登录'}
</Title>
{isRegister ? <Register /> : <Login />}
<Divider/>
<Button type="link" onClick={() => setIsRegister(!isRegister)}>
切换到{isRegister ? "已经有账号了?直接登录" : "没有账号?注册新账号"}
</Button>
</ShadowCard>
</Container>
);
};
const Title = styled.h2`
margin-bottom: 2.4rem;
color: rgb(94, 108, 132);
`
const Background = styled.div`
position: absolute;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-attachment: fixed; // 背景图片是否会随着页面滑动
background-position: left bottom, right bottom;
background-size: calc(((100vw - 40rem) / 2) - 3.2rem), calc(((100vw - 40rem) / 2) - 3.2rem), cover;
background-image: url(${left}), url(${right});
`
const Header = styled.header`
background: url(${logo}) no-repeat center;
padding: 5rem 0;
background-size: 8rem;
width: 100%;
`
...
- background-image 使用多个图时,默认会有一个重叠关系(后来者居下),可以通过 巧妙的 size 计算和 position 使其达到想要的效果
美化登录页 src\unauthenticated-app\login.tsx
(部分原有内容省略):按钮宽度撑开,并导出供注册页使用
...
import { Form, Button, Input } from "antd";
import styled from "@emotion/styled";
export const Login = () => {
...
return (
<Form onFinish={handleSubmit}>
...
<Form.Item>
<LongButton htmlType="submit" type="primary">
登录
</LongButton>
</Form.Item>
</Form>
);
};
export const LongButton = styled(Button)`
width: 100%
`
美化注册页 src\unauthenticated-app\register.tsx
(部分原有内容省略):引入登录页导出的“长按钮”
...
import { Form, Input } from "antd";
import { LongButton } from "./login";
export const Register = () => {
...
return (
<Form onFinish={handleSubmit}>
...
<Form.Item>
<LongButton htmlType="submit" type="primary">
注册
</LongButton>
</Form.Item>
</Form>
);
};
tips:在 emotion 编写css, 若是发现代码没有高亮,则需要安装 vscode/webstrom 插件:
- vscode styled-components插件
- Ctrl + P, 输入
ext install vscode-styled-components
第一个即是- 或者手动点过去搜索
- https://github.com/styled-components/styled-components
部分引用笔记还在草稿阶段,敬请期待。。。