My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

redis 学习的一些笔记

发表于 2022-08-09

基础常识
磁盘
寻址:ms
带宽: G/M
内存
寻址:ns(纳秒级)
带宽:byte/s
秒>毫秒>微秒>纳秒
磁盘寻址上比内存少了10w倍

I/O BUffer:成本问题
磁盘有磁道和扇区,一个扇区512byte
会造成索引成本增大
因此进行4K对齐,操作系统无论读多少都最少从磁盘里面拿4k出来

数据库的表很大,性能会下降吗?
如果表有索引,
那么对于增删改的操作肯定会变慢
查询速度
如果是1个或者少量查询依然很快
但如果是并发大的时候会受到硬盘带宽的影响,从而影响速度

数据在内存和磁盘中体积不一样

redis出现原因:
内存 ==> 贵
磁盘 ==> 慢
两个基础设施限制:
冯诺依曼体系的硬件制约 ===> 硬盘io带宽问题
以太网,tcp/ip的网络 ===> 不稳定

https://db-engines.com/en/

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

上述数据类型是指value的数据类型
memcache 和redis区别在于,memcache value没有类型
将返回value所有数据到client端,会受到server 网卡IO的限制,而且client要有解码的逻辑
redis因为value有类型,所以对于数据的请求根据不同情况直接调用相应类型方法返回少量数据即可
做到了计算向数据移动

redis是单进程,单线程,单实例的,通过epoll快速处理并发请求
epoll是同步非阻塞的多路复用机制
jvm: 一个线程成本是1MB
线程多了会增加调度成本,从而使CPU浪费,也会增加内存成本
BIO—>NIO(同步非阻塞)—>多路复用—>epoll(引入共享空间避免fd相关数据考来考去)

redis 对不同数据类型不同encoding类型的数值具有不同的方法
key有两个属性:
type标识值的类型
encoding标识值的编码类型
二者决定对值的操作可以使用哪些方法,从而加速计算

String 类型
字符串的相关操作:
set,get,append,setRange,getRange,strlen
适用场景:
使用内存存储的session,对象,小文件

数值的相关操作:
incr,decr
适用场景:
限流,计数
秒杀一般会用数据库

bitmap的相关操作:
setbit, bitcount,bitpos,bittop
使用场景:web, 离线数据
1.用户系统,统计用户登录天数,且窗口随机
key标识用户,每个用户准备365位,每一位表示当天是否登录,登录置一
setbit sean 1 1
setbit sean 7 1
setbit sean 364 1
STRLEN sean ===>46 每个用户只需要46个字节来存储这些信息
BITCOUNT sean -2 -1 计算多少天范围内一共登录了几天

  1. 电商做618活动,需要给活跃用户登录后送礼物,请问应该备货多少礼物
    假设这里活跃用户统计规则为20220901-20220903三天内登录的用户,三天内只要登录一次就算活跃用户
    key标识当天用户登录情况,将用户编号映射到二进制位的相应位置上,每一位二进制代表一位用户是否登录
    setbit 20220901 1 1
    setbit 20220902 1 1
    setbit 20220902 7 1
    bittop or destkey 20220901 20220902
    bitcount destkey 0 -1

  2. 存储oa权限信息

  3. 布隆过滤器,模块调用

List 类型相关操作
按不同放入顺序排列
栈:同向命令
队列:反向命令
数组
阻塞,单播队列
ltrim
使用场景:
数据共享,迁出
无状态

Hash类型
对filed进行数值计算
场景:
点赞,收藏,详情页
聚合场景

Set类型
无序去重集合
元素变多扩容,会触发rehash,造成原顺序颠倒不稳定
集合操作多
随机事件:
RANDMEMBER key count
正数:取出一个去重的结果集(不能超过已有集)
负数:取出一个带重复的结果集,一定满足你要的数量
如果是0,不返回
应用场景:
随机事件==>抽奖:
10个精品,参与人数> 10 时,传正数,得到10个随机不重复值
10个精品,参与人数< 10 时,传负数,得到10个可重复的值
中奖是否重复
Spop 不重复取出一个
推荐系统:
共同好友:交集
推荐好友:差集

Sorted set
排序
物理内存左小右大(根据分值从左到右从小到大)
不随命令发生变化
集合操作(并集,交集)权重、聚合指令
排序是怎么实现的。 ==> skip List 跳跃表
增删改查的速度
场景:
排行榜,
有序事件
评论+分页

redis持久化
持久化意味着性能会下降
两个指标
快照:rdb,恢复的速度快,但缺失的多
日志:aof, ,
满足完整性好,恢复速度变慢,===>采用不同日志策略避免
指令追加造成冗余量比较大 ===> 使用重写

redis 分布式集群
可用性:
单点故障可通过主从主备一变多集群构建镜像,需要同步
强一致性,会破坏使用性
弱一致性,可用性强,但同步一致性低(默认)
最终一致性,使用黑盒可靠集群做中间缓存,保证主从数据最终一致性
数据存储压力问题(装不下),采用分片式集群代理集群,也是一变多,但不需要同步

AkF拆分原则
根据业务划分数据到不同redis实例

My Little World

系统设计案例

发表于 2022-02-02

案例:用户将存储内容粘贴到站点,站点给用户返回一个短地址,用户通过短地址,可以访问之前粘贴的内容或者跳转之前的原始站点

功能

  1. 对于用户来说操作要简单,生成的短地址要简单,而且要唯一,不同用户即使内容一样也要生成唯一的短地址

  2. 时间有效性,从存储角度来说,不可能一直帮用户存储所有生成的短地址,不然存储会越来越大,所以通过设置短地址有效的访问时间,可以减少存储成本

技术

  1. 可用性(high Availability), 保障用户功能可用
  2. 低延时(low latency),用户拿到短地址或者通过短地址跳转其他网站时,重定向时间不宜过长
  3. 安全性(non guessable),不能被猜出来,用户在生成一定短地址时如果携带一些个人信息,不应体现在短地址中,否则会造成用户信息泄露
  4. 对于ins/微博/小红书之类的社交功能还要保障一致性,博主发的照片,follower看到的内容应该是一样的

容量负载能力假设

对一个用户来说,可以抽象出两个主要请求

  1. 请求生成短url,我们要把请求参数或者原始信息存储起来 inbound
  2. 请求访问url,把生成的url返回给用户使用,进行重定向 outbound

假设一个短地址按500byte大小存储

容量

假设一个月会有100万个新短地址生成,
那么5年会产生

100万5年12个月 ==> 约等于60亿个短地址

60亿*500byte ==> 3TB 会有需要3TB大小的容量存储

负载能力

假设一个月有100个用户,每个用户会进行100万次访问短地址进行重定向的操作

那么每秒钟会有
(100 100万)/(30D 24H * 3600s) ==> 约等于4000个短url 要给到用户
同时会有
4000/ 100 ==> 大概40个短url 需要被生成

那么服务所需要的带宽就可以计算出来

inbound : 40 500 byte 约等于 20kb/s
outbound: 4000
500 byte 约等于 20MB/s

API 数据库设计

api

假设会用到简单的增删
生成短url createURL(api-key, originUrl, expired-Date, userId)
删除url deleteURL(api-key,shortUrl)

DataBase

对于shortUrl

pk: hash
originUrl,
expired-date,
userId
…

对于user
pk: userId
name
…

生成shortUrl

假如计划生成一个6个字符的短url,使用base64的加密算法的话可以生成64 ^ 6 大约640个短url,

满足之前5年会产生60个亿的唯一性需求

整体逻辑

用户 —>request shortUrl generation —> app

app —> base64 encoding + 从key generation DB中拿一个nonUse 的key —> 得到shortUrl —>DB

DB —> app –>用户

其他

Cache

2-8原则

用存储的20% 的url做cache内容,可以满足80% 的访问需求

load balance

均衡流量

过期后的url处理,key的处理

分布式存储,分片

对于社交功能的newFeed的推送

pull / push / pull + push hybrid

My Little World

如何提升应用打开速度

发表于 2021-11-29

使用 import 语句,定义按需加载的起始模块

对于这个需求,ECMA Script 标准有一个提案,专门用于动态加载模块,语法是 import(someModule)。
注意,这里的 import 和我们一般用于引入模块的静态声明方式不同,比如 import something from ‘somemodule’ 。
但这里的 import 是作为一个函数动态运行的,这个 import() 函数会返回一个 Promise。
这样,在模块加载成功后,就可以在 Promise 的 then 回调函数中去使用这个模块了。

虽然这只是一个提案,并没有成为标准,但是 Webpack 等打包工具利用了这样的语法去定义代码的分包。

也就是说,Webpack 实现了这样的语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ProfilePage() {
// 定义一个 state 用于存放需要加载的组件
const [RealPage, setRealPage] = useState(null);

// 根据路径动态加载真正的组件实现
import('./RealProfilePage').then((comp) => {
setRealPage(Comp);
});
// 如果组件未加载则显示 Loading 状态
if (!RealPage) return 'Loading....';

// 组件加载成功后则将其渲染到界面
return <RealPage />
}

import() 这个语句完全是由 Webpack 进行处理的。

按需加载的实现原理:Webpack 利用了动态 import 语句,自动实现了整个应用的拆包。
而在实际开发中,其实并不需要关心 Webpack 是如何做到的,
而只需要考虑:该在哪个位置使用 import 语句去定义动态加载的拆分点。

总体需要采用的策略是:按业务模块为目标去做隔离,尽量在每个模块的起始页面去定义这个拆分点。

react-loadable,专门用于 React 组件的按需加载。

1
2
3
4
5
6
7
8
9
10
11
12
import Loadable from "react-loadable";


// 创建一个显示加载状态的组件
function Loading({ error }) {
return error ? 'Failed' : 'Loading';
}
// 创建加载器组件
const HelloLazyLoad = Loadable({
loader: () => import("./RealHelloLazyLoad"),
loading: Loading,
});

可以看到 Loadable 这个高阶组件主要就是两个 API。

loader:用于传入一个加载器回调,在组件渲染到页面时被执行。
在这个回调函数中,我们只需要直接使用 import 语句去加载需要的模块就可以了。

loading:表示用于显示加载状态的组件。在模块加载完成之前,加载器就会渲染这个组件。
如果模块加载失败,那么 react-loadable 会将 errors 属性传递给 Loading 组件,方便你根据错误状态来显示不同的信息给用户。

按需加载可以说是减少首屏加载时间最为有效的手段,它可以让用户在打开应用时,无需加载所有代码就能开始使用,从而提升用户体验。

使用 service worker 缓存前端资源

和浏览器自动的资源缓存机制相比,Service Worker 加上 Cache Storage 这个缓存机制,具有更高的准确性和可靠性。

因为它可以确保两点:
缓存永远不过期。你只要下载过一次,就永远不需要再重新下载,除非主动删除。
永远不会访问过期的资源。换句话说,如果发布了一个新版本,那么你可以通过版本化的一些机制,来确保用户访问到的一定是最新的资源。

My Little World

一些其他小问题

发表于 2021-11-29

1.
在 useEffect 中如果使用了某些变量,却没有在依赖项中指定,会发生什么呢?

依赖那里没有传任何参数的话,会每次render都执行。
依赖项有传值但是,有部分依赖没有传,那么没有传的那部分,数据即使变化也不会执行副作用。

对于这节课中显示的 Blog 文章的例子,我们在 useEffect 中使用了 setBlogContent 这样一个函数,本质上它也是一个局部变量,那么这个函数需要被作为依赖项吗?为什么?

函数应该是不会变化的,所以不需要监听。

2.
useState 其实也是能够在组件的多次渲染之间共享数据的,那么在 useRef 的计时器例子中,能否用 state 去保存 window.setInterval() 返回的 timer 呢?

可以,只是没有 useRef 更优,因为在更新 state 值后会导致重新渲染,而 ref 值发生变化时,是不会触发组件的重新渲染的,这也是 useRef 区别于 useState 的地方。

3.
componentWillUnmount 近似的实现:组件销毁和文章 id 变化时执行。那么在函数组件中如果要实现严格的 componentWillUnmount,也就是只在组件销毁时执行,应该如何实现?\

1
2
3
4
5
useEffect(() => {
return () => {
// 这里只会在组件销毁前(componentWillUnmount)执行一次
}
}, [])

4.
每次调用 useArticle 这个 Hook 的时候都会触发副作用去获取数据。
但是有时候,我们希望在有些组件自动获取,但有的组件中需要点击某个按钮才去获取数据,那么你会如何设计这个 Hook?(可能这道题有一点难度。)

useArticle Hook 可以提供一个参数,用来标记本地调用是否默认触发副作用去获取数据;对于点击按钮才触发请求的功能,可以在 Hook 中将获取数据的方法 retrn 出去,供外部自由调用。

  1. Hook 一般都是使用的 useState 保存了状态数据,也就意味着状态的范围限定在组件内部,组件销毁后,数据就没了。那么如果希望数据直接缓存到全局状态,应该如何做呢?

可以借助 redux,配合 useContext 等 api ,将状态数据存储至全局中。

My Little World

重新认识路由

发表于 2021-11-28

路由管理,就是让你的页面能够根据 URL 的变化进行页面的切换,这是前端应用中一个非常重要的机制

路由的核心逻辑就是根据 URL 路径这个状态,来决定在主内容区域显示什么组件。

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
// 自定义路由
const MyRouter = ({ children }) => {
const routes = _.keyBy(
children.map((c) => c.props),
"path",
);
const [hash] = useHash();
const Page = routes[hash.replace("#", "")]?.component;
// 如果路由不存在就返回 Not found.
return Page ? <Page /> : "Not found.";
};


const Route = () => null;

// 应用
function SamplePages {
return (
<div className="sample-pages">
{/* 定义了侧边导航栏 */}
<div className="sider">
<a href="#page1">Page 1</a>
<a href="#page2">Page 2</a>
<a href="#page3">Page 3</a>
<a href="#page4">Page 4</a>
</div>
<div className="exp-15-page-container">
{/* 定义路由配置 */}
<MyRouter>
<Route path="page1" component={Page1} />
<Route path="page2" component={Page2} />
<Route path="page3" component={Page3} />
<Route path="page4" component={Page4} />
</MyRouter>
</div>
</>
);
};

使用React Router

简单应用

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
// 从 react-router-dom 引入路由框架提供的一些组件
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
// 引入了两个课程示例页面
import Counter from "./01/Counter";
import UserList from "./01/UserList";


// 使用数组定义了页面组件和导航的标题,从而方便下面的渲染逻辑
const routes = [
["01 Counter", Counter],
["01 UserList", UserList],
];
function App() {
return (
<BrowserRouter>
<div className="app">
<ul className="sider">
{routes.map(([label]) => (
<li>
<Link to={`/${label.replace(" ", "/")}`}>{label}</Link>
</li>
))}
</ul>
<div id="pageContainer" className="page-container">
<Switch>
{routes.map(([label, Component]) => (
<Route key={label} path={`/${label.replace(" ", "/")}`}>
<Component />
</Route>
))}
{/* 定义一个默认的路由 */}
<Route path="/" exact>
<h1>Welcome!</h1>
</Route>
<Route path="*">Page not found.</Route>
</Switch>
</div>
</div>
</BrowserRouter>
);
}

相关组件作用

BrowserRouter:标识用标准的 URL 路径去管理路由,比如 /my-page1 这样的标准 URL 路径。
除此之外,还有 MemoryRouter,表示通过内存管理路由;
HashRouter,标识通过 hash 管理路由。
自己实现的例子其实就是用的 hash 来实现路由。

Link:定义一个导航链接,点击时可以无刷新地改变页面 URL,从而实现 React Router 控制的导航。

Route: 定义一条路由规则,可以指定匹配的路径、要渲染的内容等等。

Switch:在默认情况下,所有匹配的 Route 节点都会被展示,但是 Switch 标记可以保证只有第一个匹配到的路由才会被渲染。

react router

使用嵌套路由:实现二级导航页面

需要路由框架具备两个能力:

能够模糊匹配。比如 /page1/general 、/page1/profile 这样两个路由,需要都能匹配到 Page1 这样一个组件。
然后 Page1 内部呢,再根据 general 和 profile 这两个子路由决定展示哪个具体的页面。

Route 能够嵌套使用。在我们自定义 Route 的例子中,Route 组件仅用于收集路由定义的信息,不渲染任何内容。
如果需要路由能嵌套使用,那就意味着需要在 Route 下还能嵌套使用 Route。而这在 React Router 是提供支持的。

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
42
43

import { BrowserRouter, Route, Link } from "react-router-dom";


const Page1 = () => {
return (
<div className="exp-15-page1">
<div className="exp-15-page1-header">
<Link to="/page1/general">General</Link>
<Link to="/page1/profile">Profile</Link>
<Link to="/page1/settings">Settings</Link>
</div>
<div className="exp-15-page1-content">
<Route path="/page1/general">General Page</Route>
<Route path="/page1/profile">Profile Page</Route>
<Route path="/page1/settings">Settings Page</Route>
</div>
</div>
);
};
const Page2 = () => "Page 2";
const Page3 = () => "Page 3";


function NestedRouting() {
return (
<BrowserRouter>
<h1>Nested Routing</h1>
<div className="exp-15-nested-routing">
<div className="exp-15-sider">
<Link to="/page1">Page 1</Link>
<Link to="/page2">Page 2</Link>
<Link to="/page3">Page 3</Link>
</div>
<div className="exp-15-page-container">
<Route path="/page1"><Page1 /></Route>
<Route path="/page2"><Page2 /></Route>
<Route path="/page3"><Page3 /></Route>
</div>
</div>
</BrowserRouter>
);
}

在 URL 中保存页面状态(页面参数)

在url携带页面相关信息,将页面的一些状态存放到 URL 中,
一方面可以提升用户体验,另一方面也可以简化页面之间的交互。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

<Route path="/tabs-page/:activeTab" component={TabsPage} />

import { useCallback } from "react";
import { Tabs, Table } from "antd";
import { useHistory, useParams } from "react-router-dom";
import data from "../10/data";
import { useSearchParam } from "react-use";


const { TabPane } = Tabs;


export default () => {
// 通过 React Router 的 API 获取 activeTab 这个参数信息
const { activeTab = "users" } = useParams();
// 通过查询字符串获取当前的页码信息
const page = parseInt(useSearchParam("page"), 10) || 1;

// 通过 React Router 提供的 history 对象来操作 URL
const history = useHistory();
const handleTabChange = useCallback(
(tab) => history.push(`/15/TabsPage/${tab}`),
[history],
);
// 定义表格的翻页功能
const pagination = {
pageSize: 3,
current: page,
onChange: (p) => {
history.push(`/15/TabsPage/${activeTab}?page=${p}`);
},
};
return (
<div>
<h1>Tabs Page</h1>
<Tabs activeKey={activeTab} onChange={handleTabChange}>
<TabPane tab="Users" key="users">
<Table
dataSource={data}
columns={[
{ dataIndex: "name", title: "User Name" },
{ dataIndex: "city", title: "City" },
]}
pagination={pagination}
/>
</TabPane>
<TabPane tab="Jobs" key="jobs">
<Table
dataSource={data}
columns={[{ dataIndex: "job", title: "Job Title" }]}
pagination={pagination}
/>
</TabPane>
</Tabs>
</div>
);
}

注意

这里遵循了唯一数据源的原则,避免定义中间状态去存储 tab 和页码的信息,而是直接去操作 URL,这样可以让代码逻辑更加清晰和直观。

路由层面实现权限控制

利用前端路由的动态特性。路由是通过 JSX 以声明式的方式去定义的,这就意味着路由的定义规则是可以根据条件进行变化的,也就是所谓的动态路由。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65


import { useState } from "react";
import { Button } from "antd";
import { Route, Link } from "react-router-dom";


// 定义了两个示例页面组件
const Page1 = () => "Page 1";
const Page2 = () => "Page 2";


// 定义了一个组件用户展示未登录状态
const UnauthedPage = () => (
<span style={{ color: "red" }}>Unauthorized, please log in first.</span>
);
export default () => {
// 模拟用户是否登录的状态,通过一个按钮进行切换
const [loggedIn, setLoggedIn] = useState(false);

// 定义了两套路由,一套用于登录后,一套用于未登录状态
const routes = loggedIn
? [
{
path: "/15/RouterAuth",
component: Page1,
},
{
path: "/15/RouterAuth/page1",
component: Page1,
},
{
path: "/15/RouterAuth/page2",
component: Page2,
},
]
// 如果未登录,那么对于所有 /15/RouterAuth 开头的路径,显示未授权页面
: [{ path: "/15/RouterAuth", component: UnauthedPage }];


return (
<div>
<h1>Router Auth</h1>
<Button
type={loggedIn ? "primary" : ""}
onClick={() => setLoggedIn((v) => !v)}
>
{loggedIn ? "Log Out" : "Log In"}
</Button>


<div className="exp-15-router-auth">
<div className="exp-15-sider">
<Link to="/15/RouterAuth/page1">Page 1</Link>
<Link to="/15/RouterAuth/page2">Page 2</Link>
</div>
<div className="exp-15-page-container">
{/* */}
{routes.map((r) => (
<Route path={r.path} component={r.component} />
))}
</div>
</div>
</div>
);

代码中核心的机制就在于我们根据登录状态,创建了不同的路由规则,这样就能在源头上对权限进行集中控制,避免用户未经授权就访问某些受保护的页面。

同时呢,因为在相同的 URL 下进行了信息提示,那么也就更容易实现用户登录后还能返回原页面的功能。

My Little World

Hooks 在forms上的使用

发表于 2021-11-27

应用思想就是在这个 Hook 去维护整个表单的状态,并提供根据名字去取值和设值的方法,从而方便表单在组件中的使用。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 包含验证功能validators 的hooks, 处理表单状态管理
const useForm = (initialValues = {}, validators) => {
const [values, setValues] = useState(initialValues);
// 定义了 errors 状态
const [errors, setErrors] = useState({});

const setFieldValue = useCallback(
(name, value) => {
setValues((values) => ({
...values,
[name]: value,
}));

// 如果存在验证函数,则调用验证用户输入
if (validators[name]) {
const errMsg = validators[name](value);
setErrors((errors) => ({
...errors,
// 如果返回错误信息,则将其设置到 errors 状态,否则清空错误状态
[name]: errMsg || null,
}));
}
},
[validators],
);
// 将 errors 状态也返回给调用者
return { values, errors, setFieldValue };
};

// 应用

import { useCallback } from "react";
import useForm from './useForm';

export default () => {
const validators = useMemo(() => {
return {
name: (value) => {
// 要求 name 的长度不得小于 2
if (value.length < 2) return "Name length should be no less than 2.";
return null;
},
email: (value) => {
// 简单的实现一个 email 验证逻辑:必须包含 @ 符号。
if (!value.includes("@")) return "Invalid email address";
return null;
},
};
}, []);
// 使用 useForm 得到表单的状态管理逻辑
const { values, errors, setFieldValue } = useForm({}, validators);
// 处理表单的提交事件
const handleSubmit = useCallback(
(evt) => {
// 使用 preventDefault() 防止页面被刷新
evt.preventDefault();
console.log(values);
},
[values],
);
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name: </label>
<input
value={values.name || null}
onChange={(evt) => setFieldValue("name", evt.target.value)}
/>
</div>

<div>
<label>Email:</label>
<input
value={values.email || null}
onChange={(evt) => setFieldValue("email", evt.target.value)}
/>
</div>
<button type="submit">Submit</button>
</form>
);
};

把表单的状态管理单独提取出来,成为一个可重用的 Hook。
这样在表单的实现组件中,就只需要更多地去关心 UI 的渲染,而无需关心状态是如何存储和管理的,从而方便表单组件的开发。

Form 最为核心的机制就是我们将表单元素的所有状态提取出来,
这样表单就可以分为状态逻辑和 UI 展现逻辑,从而实现数据层和表现层的分离。

My Little World

合理组织项目文件结构

发表于 2021-11-26

按领域组织文件夹结构

每增加一个新的功能,整个应用程序的复杂度不应该明显上升。这样才能保证我们的应用程序始终可扩展,可维护。

软件复杂度的根源完全来自复杂的依赖关系

从业务功能去理解,依赖可以分为两种。
第一种是硬依赖。如果功能 A 的实现必须基于功能 B,也就是说没有功能 B,功能 A 就是不可运行的,那么我们可以说 A 硬依赖于 B。
比如对于博客系统,评论功能肯定是基于文章功能的,因为评论的对象就是文章,脱离了文章,评论功能就没有意义。

1
2
3
4
5
6
7
8
9
import CommentList from './CommentList';
function ArticleView() {
return (
<div className="article-view">
<MainContent />
<CommentList />
</div>
);
}

第二种是软依赖。如果功能 B 扩展了功能 A,也就是说,没有功能 B,功能 A 自身也是可以独立工作的,只是缺少了某些能力。
同样对于博客应用,文章管理是主要的功能,而评论功能则可以认为是增强了文章的功能。
照此来看,即使没有评论功能,文章功能也是可以独立运行的。这样就可以认为文章功能软依赖于评论功能。
我们应该让文章功能相关的代码,不要硬依赖于评论功能的代码。

在业务功能上是一个软依赖,但是在代码实现层面,却往往做成了硬依赖。
这就导致随着功能的不断增加,整个应用变得越来越复杂,最终降低了整体的开发效率

让模块之间的交互不再通过硬依赖。
解决办法就是
扩展点机制:在任何可能产生单点复杂度的模块中,通过扩展点的方式,允许其它模块为其增加功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在需要扩展的地方定义扩展点
function ArticleView({ id }) {
const { article } = useArticle(id);
return (
<div className="article-view">
<MainContent article={article} />
{/* 定义了一个名为 article.footer 的扩展点 */}
<Extension name="article.footer" args={article} />
</div>
);
}

// 在被扩展的组件里,将自己挂到扩展点上,这样就算被扩展组件直接删除,对需要扩展的功能组件来说也毫无影响
extensionEngine.register('article.footer', article => {
return <CommentList article={article} />
});

软件复杂度产生的根源,来自复杂的依赖关系。
随着功能的增加,系统复杂度也在不断增加,那么整个项目就会到达一个不可维护的状态。
所以我们首先需要从项目结构层面,去对复杂度做物理上的隔离,确保业务模块相关的代码都能在独立的文件夹中。
其次,我们要妥善地处理业务模块之间的依赖关系。
不仅需要在业务上区分硬依赖和软依赖。
同时呢,在技术的实现层面也要能做到模块的松耦合。
当然,上面的所有介绍要落实到实际的项目,还有很多细节问题需要考虑,
比如如何避免在单点模块定义所有的路由,如何避免一个导航菜单组件包含了所有业务功能的导航逻辑,等等。
总结来说,这里是整个隔离复杂度的思路,可以根据实际场景进行有针对性的思考,进而解决复杂度的问题。
同时更为重要的是,在进行实际项目开发,尤其是大型项目的架构设计时,一定要时刻有管理系统复杂度的意识,
不能只考虑功能是否实现,而不管复杂度,那样终究会导致系统越来越复杂,不断降低开发和维护的效率,甚至导致项目失败。

一个扩展点引擎

My Little World

使用hooks封装自定义事件

发表于 2021-11-25

事件处理

1.原生事件

只要原生 DOM 有的事件,在 React 中基本都可以使用,只是写法上采用骆驼体就可以了

2.是不是所有的回调函数都需要用 useCallback 进行封装呢?是不是简单的回调函数就可以不用封装了呢?

其实是否需要 useCallback ,和函数的复杂度没有必然关系,而是和回调函数绑定到哪个组件有关。
这是为了避免因组件属性变化而导致不必要的重新渲染。
如果你的事件处理函数是传递给原生节点,那么不写 callback,也几乎不会有任何性能的影响。
但是如果你使用的是自定义组件,或者一些 UI 框架的组件,那么回调函数还都应该用 useCallback 进行封装。

React 原生事件的原理:合成事件(Synthetic Events)

由于虚拟 DOM 的存在,在 React 中即使绑定一个事件到原生的 DOM 节点,事件也并不是绑定在对应的节点上,而是所有的事件都是绑定在根节点上。
然后由 React 统一监听和管理,获取事件后再分发到具体的虚拟 DOM 节点上。

在 React 17 之前,所有的事件都是绑定在 document 上的,
而从 React 17 开始,所有的事件都绑定在整个 App 上的根节点上,这主要是为了以后页面上可能存在多版本 React 的考虑。

具体来说,React 这么做的原因主要有两个。

第一,虚拟 DOM render 的时候, DOM 很可能还没有真实地 render 到页面上,所以无法绑定事件。
第二,React 可以屏蔽底层事件的细节,避免浏览器的兼容性问题。
同时呢,对于 React Native 这种不是通过浏览器 render 的运行时,也能提供一致的 API。

浏览器原生事件机制是冒泡模型。
无论事件在哪个节点被触发, React 都可以通过事件的 srcElement 这个属性,知道它是从哪个节点开始发出的,
这样 React 就可以收集管理所有的事件,然后再以一致的 API 暴露出来。

自定义事件

虽然自定义事件和原生事件看上去类似,但是两者的机制是完全不一样的:

原生事件是浏览器的机制;

而自定义事件则是纯粹的组件自己的行为,本质是一种回调函数机制。

Hooks 具备绑定任何数据源的能力, 通过分析事件中用到的数据,将数据进行抽离,从hooks角度去处理事件
可以实现定义一次,然后在任何组件中重复使用的效果
用hooks的思维去简化事件处理逻辑。

比如封装键盘输入事件
只要把键盘按键看做是一个不断变化的数据源,这样,就可以去实时监听某个 DOM 节点上触发的键盘事件了。

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
import { useEffect, useState } from "react";

// 使用 document.body 作为默认的监听节点
const useKeyPress = (domNode = document.body) => {
const [key, setKey] = useState(null);
useEffect(() => {
const handleKeyPress = (evt) => {
setKey(evt.keyCode);
};
// 监听按键事件
domNode.addEventListener("keypress", handleKeyPress);
return () => {
// 接触监听按键事件
domNode.removeEventListener("keypress", handleKeyPress);
};
}, [domNode]);
return key;
};

// 使用


import useKeyPress from './useKeyPress';

function UseKeyPressExample() => {
const key = useKeyPress();
return (
<div>
<h1>UseKeyPress</h1>
<label>Key pressed: {key || "N/A"}</label>
</div>
);
};
My Little World

应对复杂条件渲染场景

发表于 2021-11-24

容器模式:实现按条件执行 Hooks

把条件判断的结果放到两个组件之中,确保真正 render UI 的组件收到的所有属性都是有值的。

1
2
3
4
5
6
7
8
9
10
11

// 定义一个容器组件用于封装真正的 UserInfoModal
export default function UserInfoModalWrapper({
visible,
...rest, // 使用 rest 获取除了 visible 之外的属性
}) {
// 如果对话框不显示,则不 render 任何内容
if (!visible) return null;
// 否则真正执行对话框的组件逻辑
return <UserInfoModal visible {...rest} />;
}

在容器模式中可以看到,条件的隔离对象是多个子组件,这就意味着它通常用于一些比较大块逻辑的隔离。
所以对于一些比较细节的控制,其实还有一种做法,就是把判断条件放到 Hooks 中去。

1
2
3
4
5
6
7
8
9
10
11

function useUser(id) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 当 id 不存在,直接返回,不发送请求
if (!id) return
// 获取用户信息的逻辑
});
}

通过这样一个容器模式,我们把原来需要条件运行的 Hooks 拆分成子组件,
然后通过一个容器组件来进行实际的条件判断,从而渲染不同的组件,实现按条件渲染的目的。
这在一些复杂的场景之下,也能达到拆分复杂度,让每个组件更加精简的目的.

使用 render props 模式重用 UI 逻辑

render props 就是把一个 render 函数作为属性传递给某个组件,
由这个组件去执行这个函数从而 render 实际的内容

Hooks 是逻辑重用的第一选择。
不过在如今的函数组件情况下,Hooks 有一个局限,那就是只能用作数据逻辑的重用,
而一旦涉及 UI 表现逻辑的重用,就有些力不从心了,
而这正是 render props 擅长的地方。
所以,即使有了 Hooks,我们也要掌握 render props 这个设计模式的用法

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

import { Popover } from "antd";

function ListWithMore({ renderItem, data = [], max }) {
const elements = data.map((item, index) => renderItem(item, index, data));
const show = elements.slice(0, max);
const hide = elements.slice(max);
return (
<span className="exp-10-list-with-more">
{show}
{hide.length > 0 && (
<Popover content={<div style={{ maxWidth: 500 }}>{hide}</div>}>
<span className="more-items-wrapper">
and{" "}
<span className="more-items-trigger"> {hide.length} more...</span>
</span>
</Popover>
)}
</span>
);
}

// 这里用一个示例数据
import data from './data';

function ListWithMoreExample () => {
return (
<div className="exp-10-list-with-more">
<h1>User Names</h1>
<div className="user-names">
Liked by:{" "}
<ListWithMore
renderItem={(user) => {
return <span className="user-name">{user.name}</span>;
}}
data={data}
max={3}
/>
</div>
<br />
<br />
<h1>User List</h1>
<div className="user-list">
<div className="user-list-row user-list-row-head">
<span className="user-name-cell">Name</span>
<span>City</span>
<span>Job Title</span>
</div>
<ListWithMore
renderItem={(user) => {
return (
<div className="user-list-row">
<span className="user-name-cell">{user.name}</span>
<span>{user.city}</span>
<span>{user.job}</span>
</div>
);
}}
data={data}
max={5}
/>
</div>
</div>
);
};
My Little World

从 Hooks 的角度去组织异步请求

发表于 2021-11-23

要定义一个自己的 API Client

封装整个应用中异步请求的一些通过设置,以及统一处理,方便在 Hooks 中使用。

通常来说,会包括以下几个方面:

  1. 一些通用的 Header。比如 Authorization Token。
  2. 服务器地址的配置。前端在开发和运行时可能会连接不同的服务器,比如本地服务器或者测试服务器,此时这个 API Client 内部可以根据当前环境判断该连接哪个 URL。
  3. 请求未认证的处理。比如如果 Token 过期了,需要有一个统一的地方进行处理,这时就会弹出对话框提示用户重新登录。
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

import axios from "axios";

// 定义相关的 endpoint
const endPoints = {
test: "https://60b2643d62ab150017ae21de.mockapi.io/",
prod: "https://prod.myapi.io/",
staging: "https://staging.myapi.io/"
};

// 创建 axios 的实例
const instance = axios.create({
// 实际项目中根据当前环境设置 baseURL
baseURL: endPoints.test,
timeout: 30000,
// 为所有请求设置通用的 header
headers: { Authorization: "Bear mytoken" }
});

// 听过 axios 定义拦截器预处理所有请求
instance.interceptors.response.use(
(res) => {
// 可以假如请求成功的逻辑,比如 log
return res;
},
(err) => {
if (err.response.status === 403) {
// 统一处理未授权请求,跳转到登录界面
document.location = '/login';
}
return Promise.reject(err);
}
);

export default instance;

封装远程资源

将通过get方法获取的数据其实就是远程数据源,UI依赖远程数据源渲染

比起在组件内部直接发请求,只是把代码换了个地方,也就是写到了 Hook 里面

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

// useArticle
import { useState, useEffect } from "react";
import apiClient from "./apiClient";

// 将获取文章的 API 封装成一个远程资源 Hook
const useArticle = (id) => {
// 设置三个状态分别存储 data, error, loading
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 重新获取数据时重置三个状态
setLoading(true);
setData(null);
setError(null);
apiClient
.get(`/posts/${id}`)
.then((res) => {
// 请求成功时设置返回数据到状态
setLoading(false);
setData(res.data);
})
.catch((err) => {
// 请求失败时设置错误状态
setLoading(false);
setError(err);
});
}, [id]); // 当 id 变化时重新获取数据

// 将三个状态作为 Hook 的返回值
return {
loading,
error,
data
};
};

// 使用


import useArticle from "./useArticle";

const ArticleView = ({ id }) => {
// 将 article 看成一个远程资源,有 data, loading, error 三个状态
const { data, loading, error } = useArticle(id);
if (error) return "Failed.";
if (!data || loading) return "Loading...";
return (
<div className="exp-09-article-view">
<h1>
{id}. {data.title}
</h1>
<p>{data.content}</p>
</div>
);
};

有了这样一个 Hook,React 的函数组件几乎不需要有任何业务的逻辑,而只是把数据映射到 JSX 并显示出来就可以了,在使用的时候非常方便。

在项目中,可以把每一个 Get 请求都做成这样一个 Hook。
数据请求和处理逻辑都放到 Hooks 中,从而实现 Model 和 View 的隔离,
不仅代码更加模块化,而且更易于测试和维护。

这样做是为了保证每个 Hook 自身足够简单。

一般来说,为了让服务器的返回数据满足 UI 上的展现要求,通常需要进一步处理。
而这个对于每个请求的处理逻辑可能都不一样,通过一定的代码重复,能够避免产生太复杂的逻辑。

同时呢,某个远程资源有可能是由多个请求组成的,那么 Hooks 中的逻辑就会不一样,因为要同时发出去多个请求,组成 UI 展现所需要的数据。
所以,将每个 Get 请求都封装成一个 Hook ,也是为了让逻辑更清楚。

这个模式仅适用于 Get 请求的逻辑,对于其它类型,可以使用 useAsync 这样一个自定义的 Hook,
同样也是用 Hook 的思想,把请求的不同状态封装成了一个数据源供组件使用。

处理并发或串行请求

从状态变化的角度去组织异步调用。

函数组件的每一次 render,其实都提供了我们根据状态变化执行不同操作的机会,
就是利用这个机制,通过不同的状态组合,来实现异步请求的逻辑

利用状态的组合变化来实现并发和串行请求

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// useArticle 同上
// useUser
import { useState, useEffect } from "react";
import apiClient from "./apiClient";

export default (id) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 当 id 不存在,直接返回,不发送请求
if (!id) return;
setLoading(true);
setData(null);
setError(null);
apiClient
.get(`/users/${id}`)
.then((res) => {
setLoading(false);
setData(res.data);
})
.catch((err) => {
setLoading(false);
setError(err);
});
}, [id]);
return {
loading,
error,
data
};
};

// 应用

import { useState } from "react";
import CommentList from "./CommentList";
import useArticle from "./useArticle";
import useUser from "./useUser";
import useComments from "./useComments";

const ArticleView = ({ id }) => {
const { data: article, loading, error } = useArticle(id);
const { data: comments } = useComments(id);
const { data: user } = useUser(article?.userId);
if (error) return "Failed.";
if (!article || loading) return "Loading...";
return (
<div className="exp-09-article-view">
<h1>
{id}. {article.title}
</h1>
{user && (
<div className="user-info">
<img src={user.avatar} height="40px" alt="user" />
<div>{user.name}</div>
<div>{article.createdAt}</div>
</div>
)}
<p>{article.content}</p>
<CommentList data={comments || []} />
</div>
);
};

export default () => {
const [id, setId] = useState(1);
return (
<div className="exp-09-article-view-wrapper">
<ul>
<li onClick={() => setId(1)}>Article 1</li>
<li onClick={() => setId(2)}>Article 2</li>
<li onClick={() => setId(3)}>Article 3</li>
<li onClick={() => setId(4)}>Article 4</li>
<li onClick={() => setId(5)}>Article 5</li>
</ul>
<ArticleView id={id} />
</div>
);
};

useArticle 和 useComments属于并发请求;useArticle 和 useUser属于串行请求,

之所以useUser能在article数据变化时重新执行,是因为在useUser的hook里面用useEffect做了依赖

或者换个角度想,把三个钩子的逻辑全部都写在应用代码里,不再封装起来,明显可以看到,
因为useEffect的原因,可以使获取user信息的请求,在拿到article数据后再执行。

1…345…25
YooHannah

YooHannah

246 日志
1 分类
21 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces