内容介绍
本专题假设你已经具备了基本的 HTML 5 、Javascript(ES6) 和 CSS 3 知识。本专题是为了补全 AXUM 全栈开发所需要的技术栈,而不是从零开始的前端课程,如果你还不具备以上知识,请通过其它途径学习完成之后,再来学习本专题。React: 简介与安装
本章将通过编程世界通用的“Hello World”程序,告诉你:创建 React 应用的几种方法、虚拟DOM、JSX以及将 React 挂载到真实 DOM 的步骤。- 支持试读
React:类式组件、函数式组件及state和props
本章将讨论 React 的类式组件、函数式组件以及 React 两个最重要的属性:`state` 和 `props`。 React: 组件的生命周期
本章将讨论 React 组件的常用生命周期。- 支持试读
React: 事件处理、表单处理及受控组件与非受控组件
本章讨论 React 的事件处理,以及表单处理时涉及两个策略:受控组件与非受控组件。 React: 流程控制与key
本章将讨论 React(JSX) 的条件判断和循环。React: 自定义组件及组件通讯
本章我们将正式开启 React 组件之路。虽然我们之前章节的都叫组件,但整个应用只有一个组件,略显单薄。同时,我们还将讨论组件之间如何进行通讯。React: 路由
本章将讨论 React 的路由:通过路由,你可以制作出“多页面”的系统。React: 状态共享
本章将通过一个非常典型的案例来讨论 React 的状态共享:只有登录用户才能看到某些内容。早期的 React 完全依靠其生态中的 redux 等第三方库来实现状态共享;现在 React 提供了官方的 `Context` 来实现这一目的。React: SEO挑战、服务端渲染及本地存储
经过紧张的学习,React 课程终于暂告一个段落了。本章将是一个相对轻松的内容,我们一起探讨一下 React 应用的 SEO 以及为什么需要服务端渲染,同时对 React 课程做个简单的小结。NextJS: 简介与安装
NextJS 是一个 React 框架,它提供了很多有用的功能把 React 的力量发挥地淋漓尽致。本章我们将开始 NextJS 之旅,首先自然是安装它,然后来一个「你好,NextJS」NextJS: 渲染模式和数据获取
趁你现在对我们刚刚讨论的有关 React 和 SEO 的问题还保持有较强的印象,我们先来讨论 NextJS 是如何利用多种渲染模式来应用 SEO 挑战的。NextJS: 内置组件及自动路由
NextJS 既然是 React 的框架,自然提供了一些内置组件来扩展 React。本章将介绍几个常用的 NextJS 内置组件,同时也将介绍 NextJS 的路由系统。NextJS: 开发博客系统
本章我们将使用 NextJS 开发一个小型的博客系统,你将学习到如何从远程服务器获取数据以及数据过滤、NextJS 常用组件的用法、NextJS 的自动路由等功能。Tailwind:简介与响应式设计的基本原则
TailwindCSS 是一款响应式的、移动设备优先的 CSS 工具类框架。本章对响应式布局、移动设备优先、断点等基本概念进行简要说明;并讨论几种安装 tailwind 的方式。Tailwind: 通过小示例体验它
本章通过两个小示例来体验一下 tailwind 的魔力。Tailwind: 默认配置
Tailwind 定义了一系列变量,比如颜色、大小、间距等。本章将介绍 tailwind 的一些默认设置。Tailwind: 撸一个按钮
本章将带你使用 tailwind 撸一个按钮Tailwind: 使用 flex 和 grid 进行响应式布局
本章我们将讨论使用 `flex` 和 `grid` 进行响应式布局,以及为什么不建议再使用 `float` 进行布局。- 支持试读
Tailwind: 撸一个带图标和动画效果的下拉框
本章将使用 tailwind 实现一个没有任何 Javscript 代码的纯 CSS 的下拉框,把应用到导航栏、菜单栏时,也被称为下拉菜单。同时我们将讨论如何在 tailwind 中使用图标,包括图标的进化史:从字体文件到SVG。 Tailwind: 撸一个报价卡片
本章我们将使用 tailwind 撸一个报价卡片。Tailwind: 撸一个响应式的纯CSS导航栏
本章将使用 tailwind 撸一个响应式的、纯 CSS 的导航栏。Tailwind: 集成到React/NextJS
本章将介绍如何将 Tailwind 集成到 React 或 NextJS 项目中。Tailwind: 复用
本章通过将之前撸的按钮改成 NextJS 版,进而讨论 Tailwind 的复用原则。Tailwind: 配置和插件
本章将讨论如何配置 tailwind,以及几个 tailwind 官方插件。Tailwind: 制作响应式博客
本章我们使用 tailwind 将之前课程中 NextJS 迷你博客改造为响应式的布局。
React: 状态共享
本章将通过一个非常典型的案例来讨论 React 的状态共享:只有登录用户才能看到某些内容。早期的 React 完全依靠其生态中的 redux 等第三方库来实现状态共享;现在 React 提供了官方的 Context
来实现这一目的。
本章将重点讨论 Context
,由于 redux 无论在 API 设计还是设计理念上和 Context
都非常相似,所以只在本章结束部分给出示例代码的链接。
本章讨论的是 hook 形式的 Context 和 redux
本站不对诸如「Context 会代替 redux吗 / 项目里用 Context 还是 redux」等问题进行回答。我只能说,你甚至可以将 Context 和 redux 整合在一起使用。
本章讨论的是 hook 形式的 Context 和 redux
本站不对诸如「Context 会代替 redux吗 / 项目里用 Context 还是 redux」等问题进行回答。我只能说,你甚至可以将 Context 和 redux 整合在一起使用。
快速体验 Context
你可以点击这里即时查看效果。
const UserContext = createContext();
使用createContext()
创建一个 Context,并赋值给变量UserContext
<UserContext.Provider value="axum.rs">
- 使用
UserContext.Provider
包裹需要访问共享数据的组件 - 通过
value
传递共享的数据
- 使用
- 在其它组件中,使用
const user = useContext(UserContext);
来访问共享的数据 - 本例在一个文件中定义了多个组件,这并没什么奇怪的,毕竟从语言层面上说,它们只是JS函数
- 使用
UserContext.Provider
包裹需要访问共享数据的组件 - 通过
value
传递共享的数据
是的,你会看到并没有通过 props
来进行通讯。
实现 Context 的读写操作
建议你点击打开这里,先体验一下效果:在Foo
组件的文本框里改变共享的数据,Bar
组件能实时感知并显示出来。
在上面的例子里,我们猜测,实现数据共享的根源在于,我们使用了 <UserContext.Provider>
组件。回顾之前所学内容:props
可以传递包括函数在内的 JS 表达式,但 <UserContext.Provider>
是由 React 提供的,只能接收包括 value
和 children
在内的,为数不多的 props
。
我们能不能自己对 <UserContext.Provider>
进行封装,让它能接受更多的 props
——包括修改共享数据的函数。顺着这个思路,有了以下代码:
import React, { createContext, useContext, useState } from 'react';
const UserContext = createContext();
export default function App() {
return (
<UserContextProvider value={{ user: 'axum.rs' }}>
<Foo />
<Bar />
</UserContextProvider>
);
}
function UserContextProvider({ value, children }) {
const [user, setUser] = useState(value.user);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
function Foo() {
const ctx = useContext(UserContext);
return (
<div>
Foo:{' '}
<input value={ctx.user} onChange={(e) => ctx.setUser(e.target.value)} />
</div>
);
}
function Bar() {
const ctx = useContext(UserContext);
return <div>Bar: {ctx.user}</div>;
}
-
<UserContextProvider value={{ user: 'axum.rs' }}>
- 我们不再直接使用
UserContext.Provider
,而是使用自己封装的UserContextProvider
(名字很像,偷懒的把中间的.
去掉就好了) - 我们给
value
传递的不再是简单的字符串,而是一个 JS 简单对象
- 我们不再直接使用
-
来看
UserContextProvider
的封装function UserContextProvider({ value, children }) { const [user, setUser] = useState(value.user); return ( <UserContext.Provider value={{ user, setUser }}> {children} </UserContext.Provider> ); }
看不懂?其实很简单——只要你能打破思维限制
- 参数:我们从
props
里解构了value
和childen
const [user, setUser] = useState(value.user);
- 我们创建了一个用于维护用户信息的状态,以及设置它的函数
- 使用
props
传递过来的value.user
作为该状态的默认值
<UserContext.Provider value={{ user, setUser }}>
:我们在返回值里调用原始的UserContext.Provider
- 并且给它的
value
传递了更丰富的内容:原样返回从props
接收到的user
,以及设置状态的函数
- 并且给它的
- 参数:我们从
-
在
Foo
组件里const ctx = useContext(UserContext);
同样,我们还是先使用这个 Context<input value={ctx.user} onChange={(e) => ctx.setUser(e.target.value)} />
- 将 Context 里的
user
作为<input>
的值 - 在
onChange
的处理函数中,我们把<input>
的值,通过 Context 提供的setUser
方法写入到共享数据里
- 将 Context 里的
<UserContextProvider value={{ user: 'axum.rs' }}>
- 我们不再直接使用
UserContext.Provider
,而是使用自己封装的UserContextProvider
(名字很像,偷懒的把中间的.
去掉就好了) - 我们给
value
传递的不再是简单的字符串,而是一个 JS 简单对象
来看 UserContextProvider
的封装
function UserContextProvider({ value, children }) {
const [user, setUser] = useState(value.user);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
看不懂?其实很简单——只要你能打破思维限制
- 参数:我们从
props
里解构了value
和childen
const [user, setUser] = useState(value.user);
- 我们创建了一个用于维护用户信息的状态,以及设置它的函数
- 使用
props
传递过来的value.user
作为该状态的默认值
<UserContext.Provider value={{ user, setUser }}>
:我们在返回值里调用原始的UserContext.Provider
- 并且给它的
value
传递了更丰富的内容:原样返回从props
接收到的user
,以及设置状态的函数
- 并且给它的
- 我们创建了一个用于维护用户信息的状态,以及设置它的函数
- 使用
props
传递过来的value.user
作为该状态的默认值
- 并且给它的
value
传递了更丰富的内容:原样返回从props
接收到的user
,以及设置状态的函数
在 Foo
组件里
const ctx = useContext(UserContext);
同样,我们还是先使用这个 Context<input value={ctx.user} onChange={(e) => ctx.setUser(e.target.value)} />
- 将 Context 里的
user
作为<input>
的值 - 在
onChange
的处理函数中,我们把<input>
的值,通过 Context 提供的setUser
方法写入到共享数据里
- 将 Context 里的
- 将 Context 里的
user
作为<input>
的值 - 在
onChange
的处理函数中,我们把<input>
的值,通过 Context 提供的setUser
方法写入到共享数据里
案例说明
本章的案例是:只有登录用户才能访问用户中心,页面如下:
组件如下:
组件 | 文件名 | 说明 |
---|---|---|
Navbar | components/Navbar.jsx | 全局顶部导航 |
Container | components/Container.jsx | 通用布局容器 |
初始页面
我建议你现在先点开本节代码axum-rs-react-context-1进行体验一下,因为在引入 Context 之前,我们已经引入了一些新知识。
-
tailwind:为了便于页面布局,我们引入了 tailwind,这也是本专题的子专题之一,你现在不用管它。我们是通过 JSX 里的
className
来使用 tailwind 的,所以当你浏览代码时,可以不用关注这些 CSS 样式。 -
打开
src/routeTable.jsx
,你可以看到熟悉的代码——但又不完全熟悉//... import { Navigate } from 'react-router-dom'; //... const routeTable = [ //... { path: '/user', element: <User />, children: [ { path: 'index', element: <UserHome />, }, { path: 'profile', element: <UserProfile />, }, { path: 'order', element: <UserOrder />, }, { path: '', element: <Navigate to="index" />, }, ], }, ]; //...
/user
路由- 它不再像上一章的
/news
那样,只是提供前缀。它除了提供前缀,还有另一个功能,那就是提供整个“用户中心”的布局,所以它由两部分组成:菜单和匹配嵌套在它里面的子路由(请看下面对该文件的分析) - 它还有一个
path: ''
的子路由- 首先,所有子路由有都有明确的
path
,而在全局导航里,“用户中心”的地址是/user
,所以在全局导航里点击“用户中心”实际匹配的是这个子路由 - 由于它并不提供有实际意义的功能,所以使用react-router提供的
<Navigate>
组件跳转到index
(完整的路由是/user/index
)
- 首先,所有子路由有都有明确的
- 它不再像上一章的
-
打开
src/pages/User/index.jsx
import React from 'react'; import { Outlet, NavLink } from 'react-router-dom'; export default function User() { return ( <div className="grid grid-cols-2 gap-3"> <div className="flex flex-col space-y-4 border-r"> <NavLink to="index">用户首页</NavLink> <NavLink to="profile">个人资料</NavLink> <NavLink to="order">我的订单</NavLink> <NavLink to="/logout">退出登录</NavLink> </div> <div> <Outlet /> </div> </div> ); }
- 正如上面所说,
User/index.jsx
是给整个用户中心提供布局的,所以它有两部分:一部分是由<NavLink>
组成的导航菜单,另一部分是适配子路由页面 - react-router 提供的
<Outlet>
派上用场了——适配嵌套路由中的子路由
- 正如上面所说,
-
打开
src/components/Container.jsx
import React from 'react'; export default function Container({ children }) { return <div className="container max-w-lg mx-auto p-3">{children}</div>; }
- 这个组件很简单:给子组件提供一个容器
- 这是tailwind里的一个重要思想:使用组件来复用,而不是使用 CSS 的类
-
打开
src/App.jsx
import React from 'react'; import { useRoutes } from 'react-router-dom'; import Navbar from './components/Navbar'; import Container from './components/Container'; import routeTable from './routeTable'; function App() { const routes = useRoutes(routeTable); return ( <> <header className="bg-gray-100 shadow-md"> <Container> <Navbar /> </Container> </header> <main> <Container>{routes}</Container> </main> </> ); } export default App;
<>
是什么鬼?- 我们说过,一个 VDOM 必须只有一个根元素,在之间的章节里,为了满足这一规范,我们不得不使用
<div>
来包裹多个组件,这样的结果是,在真实DOM里一大堆无用的div
- 我们可以用
<>
来包裹多个组件,并且在真实的DOM里,它会自动消失——只是为了满足 VDOM 对只有一个根元素的要求
- 我们说过,一个 VDOM 必须只有一个根元素,在之间的章节里,为了满足这一规范,我们不得不使用
<React.Fragment>
是另外一个和<>
类似的组件,不同之处在于,<React.Fragment>
可以指定key
,而<>
什么属性都不能指定
tailwind:为了便于页面布局,我们引入了 tailwind,这也是本专题的子专题之一,你现在不用管它。我们是通过 JSX 里的 className
来使用 tailwind 的,所以当你浏览代码时,可以不用关注这些 CSS 样式。
//...
import { Navigate } from 'react-router-dom';
//...
const routeTable = [
//...
{
path: '/user',
element: <User />,
children: [
{
path: 'index',
element: <UserHome />,
},
{
path: 'profile',
element: <UserProfile />,
},
{
path: 'order',
element: <UserOrder />,
},
{
path: '',
element: <Navigate to="index" />,
},
],
},
];
//...
/user
路由- 它不再像上一章的
/news
那样,只是提供前缀。它除了提供前缀,还有另一个功能,那就是提供整个“用户中心”的布局,所以它由两部分组成:菜单和匹配嵌套在它里面的子路由(请看下面对该文件的分析) - 它还有一个
path: ''
的子路由- 首先,所有子路由有都有明确的
path
,而在全局导航里,“用户中心”的地址是/user
,所以在全局导航里点击“用户中心”实际匹配的是这个子路由 - 由于它并不提供有实际意义的功能,所以使用react-router提供的
<Navigate>
组件跳转到index
(完整的路由是/user/index
)
- 首先,所有子路由有都有明确的
- 它不再像上一章的
- 它不再像上一章的
/news
那样,只是提供前缀。它除了提供前缀,还有另一个功能,那就是提供整个“用户中心”的布局,所以它由两部分组成:菜单和匹配嵌套在它里面的子路由(请看下面对该文件的分析) - 它还有一个
path: ''
的子路由- 首先,所有子路由有都有明确的
path
,而在全局导航里,“用户中心”的地址是/user
,所以在全局导航里点击“用户中心”实际匹配的是这个子路由 - 由于它并不提供有实际意义的功能,所以使用react-router提供的
<Navigate>
组件跳转到index
(完整的路由是/user/index
)
- 首先,所有子路由有都有明确的
- 首先,所有子路由有都有明确的
path
,而在全局导航里,“用户中心”的地址是/user
,所以在全局导航里点击“用户中心”实际匹配的是这个子路由 - 由于它并不提供有实际意义的功能,所以使用react-router提供的
<Navigate>
组件跳转到index
(完整的路由是/user/index
)
打开 src/pages/User/index.jsx
import React from 'react';
import { Outlet, NavLink } from 'react-router-dom';
export default function User() {
return (
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col space-y-4 border-r">
<NavLink to="index">用户首页</NavLink>
<NavLink to="profile">个人资料</NavLink>
<NavLink to="order">我的订单</NavLink>
<NavLink to="/logout">退出登录</NavLink>
</div>
<div>
<Outlet />
</div>
</div>
);
}
- 正如上面所说,
User/index.jsx
是给整个用户中心提供布局的,所以它有两部分:一部分是由<NavLink>
组成的导航菜单,另一部分是适配子路由页面 - react-router 提供的
<Outlet>
派上用场了——适配嵌套路由中的子路由
打开 src/components/Container.jsx
import React from 'react';
export default function Container({ children }) {
return <div className="container max-w-lg mx-auto p-3">{children}</div>;
}
- 这个组件很简单:给子组件提供一个容器
- 这是tailwind里的一个重要思想:使用组件来复用,而不是使用 CSS 的类
打开src/App.jsx
import React from 'react';
import { useRoutes } from 'react-router-dom';
import Navbar from './components/Navbar';
import Container from './components/Container';
import routeTable from './routeTable';
function App() {
const routes = useRoutes(routeTable);
return (
<>
<header className="bg-gray-100 shadow-md">
<Container>
<Navbar />
</Container>
</header>
<main>
<Container>{routes}</Container>
</main>
</>
);
}
export default App;
<>
是什么鬼?- 我们说过,一个 VDOM 必须只有一个根元素,在之间的章节里,为了满足这一规范,我们不得不使用
<div>
来包裹多个组件,这样的结果是,在真实DOM里一大堆无用的div
- 我们可以用
<>
来包裹多个组件,并且在真实的DOM里,它会自动消失——只是为了满足 VDOM 对只有一个根元素的要求
- 我们说过,一个 VDOM 必须只有一个根元素,在之间的章节里,为了满足这一规范,我们不得不使用
<React.Fragment>
是另外一个和<>
类似的组件,不同之处在于,<React.Fragment>
可以指定key
,而<>
什么属性都不能指定
使用 Context 来控制访问权限
建议你打开本节代码:axum-rs-react-context-2先体验结果,并在编辑器里查看文件。
由于文件较多,请通过上面的链接打开完整的源码,这里只对重要的代码进行讲解
由于文件较多,请通过上面的链接打开完整的源码,这里只对重要的代码进行讲解
封装 UserContext
// src/contexts/UserContext.jsx
import React, { createContext, useContext, useState } from 'react';
export const UserContext = createContext();
export function UserContextProvider({ value, children }) {
const [user, setUser] = useState(value.user);
const getUser = () => {
return user;
};
return (
<UserContext.Provider value={{ getUser, setUser }}>
{children}
</UserContext.Provider>
);
}
export const defaultValue = { user: null, setUser: null };
export const UserContext = createContext();
创建UserContext
并导出<UserContext.Provider value={{ getUser, setUser }}>
暴露getUser
和setUser
两个函数给外部
App
组件
function App() {
const routes = useRoutes(routeTable);
return (
<UserContextProvider value={defaultValue}>
<header className="bg-gray-100 shadow-md">
<Container>
<Navbar />
</Container>
</header>
<main>
<Container>{routes}</Container>
</main>
</UserContextProvider>
);
}
- 使用我们自定义的
UserContextProvider
包裹所有子组件
导航栏
// ...
import { UserContext } from '../contexts/UserContext';
export default function Navbar() {
const { getUser } = useContext(UserContext);
return (
{/*...*/}
<ul className="flex space-x-4 justify-end items-center">
{getUser() ? (
<li>
<NavLink to="/logout">退出登录</NavLink>
</li>
) : (
<li>
<NavLink to="/login">登录</NavLink>
</li>
)}
</ul>
{/*...*/}
);
}
- 从
UserContext
读取用户信息,如果有用户信息,显示“退出登录”,反之显示“登录”。
用户中心
//...
export default function User() {
const { getUser } = useContext(UserContext);
if (!getUser()) {
return <Navigate to="/login" />;
}
return (
<div className="grid grid-cols-2 gap-3">
{/* ... */}
</div>
);
}
- 从
UserContext
读取用户信息,如果没有,跳转到登录页面
登录
import React, { useContext, useState } from 'react';
import { UserContext } from '../contexts/UserContext';
import { useNavigate } from 'react-router-dom';
export default function Login() {
const [formData, setFormData] = useState({ username: '', password: '' });
const { setUser } = useContext(UserContext);
const navigate = useNavigate();
const onChangeHandler = (el) => {
return (e) => {
setFormData((current) => ({ ...current, [el]: e.target.value }));
};
};
const onSubmitHandler = (e) => {
e.preventDefault();
if (
!(formData.username === 'AXUM中文网' && formData.password === 'axum.rs')
) {
alert('用户名或密码错误');
return;
}
setUser(formData.username);
navigate('/user');
return;
};
return (
<div className="border rounded-md mx-auto bg-gray-100 p-6">
<form className="flex flex-col space-y-4" onSubmit={onSubmitHandler}>
<div>
<label>
账号:{' '}
<input
className="border px-3 py-2 rounded"
placeholder="输入你的用户名"
value={formData.username}
onChange={onChangeHandler('username')}
required
/>
</label>
</div>
<div>
<label>
密码:{' '}
<input
className="border px-3 py-2 rounded"
placeholder="输入你的密码"
type="password"
onChange={onChangeHandler('password')}
required
/>
</label>
</div>
<div>
<button className="border px-8 py-2 rounded bg-blue-600 text-gray-200 hover:bg-blue-700">
登录
</button>
</div>
</form>
<div>{formData.username}</div>
<div>{formData.password}</div>
</div>
);
}
- 注意这里的受控组件的绑定是怎么写的,这是一个高阶函数的写法
- 如果用户名和密码正确,调用
setUser
将用户名写入UserContext
,并跳转的用户中心
退出登录
import React, { useContext } from 'react';
import { UserContext } from '../contexts/UserContext';
import { useNavigate } from 'react-router-dom';
export default function Logout() {
const { setUser } = useContext(UserContext);
const navigate = useNavigate();
function onClickHandler() {
setUser(null);
navigate('/user');
return;
}
return (
<div className="border rounded-md mx-auto text-center bg-gray-100 p-3">
<p className="py-24">确定退出本次登录?</p>
<button
className="border px-8 py-2 rounded bg-rose-600 text-gray-200 hover:bg-rose-700"
onClick={onClickHandler}
>
确定退出
</button>
</div>
);
}
- 清空已登录用户的信息,并跳转到用户中心(实际可以直接改成跳转到登录页面)
redux 的用法请参考
https://react-redux.js.org/api/hooks