My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
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数据后再执行。

My Little World

复杂状态管理

发表于 2021-11-22

React 的开发其实就是复杂应用程序状态的管理和开发

复杂状态管理的两个原则

原则一:保证状态最小化

在定义一个新的状态之前,都要再三拷问自己:
这个状态是必须的吗?是否能通过计算得到呢?
在得到肯定的回答后,再去定义新的状态,
就能避免大部分多余的状态定义问题,也就能在简化状态管理的同时,保证状态的一致性

比如根据关键字搜索的场景,搜索结果可以根据关键和原始数据计算得到,可以使用useMemo保存过滤结果,
而不是再为过滤结果声明一个state

原则二:避免中间状态,确保唯一数据源

比如在需要根据URL查询数据的场景,查询条件在url,同时页面有配置条件的交互
那么直接将 URL 作为唯一的数据来源,状态的读取和修改都是对 URL 直接进行操作,
而不是通过一个中间的状态保存条件
(URL 变化时,同步查询关键字到 State;State 变化时,同步查询关键字到输入框; 用户在输入框输入的时候,同步关键字到 URL 和 State),
这样就简化了状态的管理,保证了状态的一致性

My Little World

redux的基本使用

发表于 2021-11-17

Redux 的三个基本概念

Redux 引入的概念其实并不多,主要就是三个:State、Action 和 Reducer。

其中 State 即 Store,一般就是一个纯 JavaScript Object。
Action 也是一个 Object,用于描述发生的动作。
而 Reducer 则是一个函数,接收 Action 和 State 并作为参数,通过计算得到新的 Store。

如何在 React 中使用 Redux

如何建立 Redux 和 React 的联系

主要是两点:

  1. React 组件能够在依赖的 Store 的数据发生变化时,重新 Render;
  2. 在 React 组件中,能够在某些时机去 dispatch 一个 action,从而触发 Store 的更新

答案:
借助react-redux 这样一个工具库,工具库的作用就是建立一个桥梁,让 React 和 Redux 实现互通。

在 react-redux 的实现中,为了确保需要绑定的组件能够访问到全局唯一的 Redux Store,
利用了 React 的 Context 机制去存放 Store 的信息。
通常我们会将这个 Context 作为整个 React 应用程序的根节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import ReactDOM from 'react-dom'

import { Provider } from 'react-redux'
import store from './store'

import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)

Hooks 的本质就是提供了让 React 组件能够绑定到某个可变的数据源的能力。
在这里,当 Hooks 用到 Redux 时可变的对象就是 Store,而 useSelector 则让一个组件能够在 Store 的某些数据发生变化时重新 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


import React from 'react'
import { useSelector, useDispatch } from 'react-redux'

export function Counter() {
// 从 state 中获取当前的计数值
const count = useSelector(state => state.value)

// 获得当前 store 的 dispatch 方法
const dispatch = useDispatch()

// 在按钮的 click 时间中去分发 action 来修改 store
return (
<div>
<button
onClick={() => dispatch({ type: 'counter/incremented' })}
>+</button>
<span>{count}</span>
<button
onClick={() => dispatch({ type: 'counter/decremented' })}
>-</button>
</div>
)
}

使用 Redux 处理异步逻辑

借助中间件机制

middleware 可以让你提供一个拦截器在 reducer 处理 action 之前被调用。
在这个拦截器中,你可以自由处理获得的 action。
无论是把这个 action 直接传递到 reducer,或者构建新的 action 发送到 reducer,都是可以的

Middleware 正是在 Action 真正到达 Reducer 之前提供的一个额外处理 Action 的机会

Redux 提供了 redux-thunk 这样一个中间件,它如果发现接受到的 action 是一个函数,那么就不会传递给 Reducer,
而是执行这个函数,并把 dispatch 作为参数传给这个函数,从而在这个函数中你可以自由决定何时,如何发送 Action。

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
// 在创建 Redux Store 时指定了 redux-thunk 这个中间件
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from './reducer'

const composedEnhancer = applyMiddleware(thunkMiddleware)
const store = createStore(rootReducer, composedEnhancer)

//dispatch action 时就可以 dispatch 一个函数用于来发送请求

function fetchData() {
return dispatch => {
dispatch({ type: 'FETCH_DATA_BEGIN' });
fetch('/some-url').then(res => {
dispatch({ type: 'FETCH_DATA_SUCCESS', data: res });
}).catch(err => {
dispatch({ type: 'FETCH_DATA_FAILURE', error: err });
})
}
}

// dispatch action 时就可以 dispatch 一个函数用于来发送请求
import fetchData from './fetchData';

function DataList() {
const dispatch = useDispatch();
// dispatch 了一个函数由 redux-thunk 中间件去执行
dispatch(fetchData());
}

通过这种方式,我们就实现了异步请求逻辑的重用。那么这一套结合 redux-thunk 中间件的机制,我们就称之为异步 Action.

dispatch时可以传入其他参数

1
2
3
4
5
6
7
8
9
10
11
12

function counterReducer(state = initialState, action) {
switch (action.type) {
case 'counter/incremented': return {value: state.value + action.payload}
case 'counter/decremented': return {value: state.value - action.payload}
default: return state
}
}
const incrementAction = { type: 'counter/incremented', payload: 10 };
store.dispatch(incrementAction); // 计数器加 10
const decrementAction = { type: 'counter/decremented', payload: 10 };
store.dispatch(decrementAction); // 计数器减 10
1…567…27
YooHannah

YooHannah

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