文章目录
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 三、TS 应用:JS神助攻 - 强类型
- 四、JWT、用户认证与异步请求
- 1~5
- 6~10
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 1.安装与使用 antd 组件库
- 安装 antd
- 安装 craco
- antd 组件替换原生组件
- 2.CSS-in-JS
- (1)传统CSS的缺陷
- ①缺乏模块组织
- ②缺乏作用域
- ③隐式依赖,让样式难以追踪
- ④没有变量
- ⑤CSS选择器与HTML元素耦合
- (2)Emotion 介绍
- 3.emotion & 登录注册页美化
- 全局样式
- 安装 emotion
- 原生标签使用 emotion
- antd 标签使用 emotion
- 进一步美化
学习内容来源: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、用户认证与异步请求
1~5
- 【实战】四、 JWT、用户认证与异步请求(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(四)
6~10
- 【实战】四、 JWT、用户认证与异步请求(下) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(五)
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
antd + emotion
1.安装与使用 antd 组件库
官网:Ant Design - 一套企业级 UI 设计语言和 React 组件库
安装 antd
# 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'
复制
安装 craco
为对 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
重新启动项目
antd 组件替换原生组件
- 先修改登录页面
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<Project>.columns?: ColumnsType<Project> | undefined
TS
的类型推断起作用了:
- 通过
list
的值类型为Project[]
,推断出dataSource?: RcTableProps<RecordType>['data']
中data
类型为Project[]
- 推断出
dataSource?: RcTableProps<RecordType>['data']
中RecordType
类型为Project[]
- 推断出
columns
类型为(property) TableProps<Project>.columns?: ColumnsType<Project> | 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> ); };
复制
- 尝试功能,正常!
2.CSS-in-JS
以下部分是课件原文:
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; ` <Title> 我是啥颜色? </Title> ④没有变量
传统的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( <div css={{ backgroundColor: 'hotpink', '&:hover': { color: 'lightgreen' } }} > This has a hotpink background. </div> ) 这种写法比起React自带的style的写法功能更强大,比如可以处理级联、伪类等style处理的不了的情况
3.emotion & 登录注册页美化
全局样式
- 编辑
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
中添加
安装 emotion
npm i @emotion/react @emotion/styled --force
复制
原生标签使用 emotion
编辑 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>, {}>
antd 标签使用 emotion
继续编辑 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
本次美化成果:
部分引用笔记还在草稿阶段,敬请期待。。。