者 | Lakindu Hewawasam
译者 | 许学文
策划 | 丁晓昀
如果你在开发工作中使用的是 React 框架,那么首当其冲要学习的就是思考如何设计组件。组件设计并非简单地将多个组件合成一个集合,而是需要思考如何设计更小、复用性更强的组件。例如,思考下面这张组件图:
简化的组件图
图中有三个组件,分别是:
如图所示,Typography 组件同时被 Footer 和 Sizeable Box 组件使用。通常我们以为这样,就能构建一个简单、易维护和易排除错误的应用了。但其实只是这样思考组件的设计是远远不够的。
如果你知道如何从组件的视角思考问题,就可以通过在 React 组件中使用设计模式来提高代码的整体模块性、可扩展性和可维护性。
因此,下面这五种设计模式,是你在使用 React 时必须要掌握的。
首先,在使用 React 时候,请尝试为应用设计基础组件。
基础UI组件,就是一个具备默认行为且支持定制化的组件。
例如,每个应用都会通过基础的样式、按钮设计或者基础的排版,来实现应用在视觉和交互上的一致性。这些组件的设计特点是:
通过一个 Button 组件就能很好地说明基础组件模式的实现。示例如下:
现在你就可以利用基础组件模式进行设计,使组件的使用者可以改变其行为。请参考我基于基础组件模式完成的Button组件,示例代码如下:
import React, { ButtonHTMLAttributes } from 'react';
// 按钮组件的形态:实心或者空心
type ButtonVariant='filled' | 'outlined';
export type ButtonProps={
/**
* the variant of the button to use
* @default 'outlined'
*/
variant?: ButtonVariant;
} & ButtonHTMLAttributes<HTMLButtonElement>;;
const ButtonStyles: { [key in ButtonVariant]: React.CSSProperties }={
filled: {
backgroundColor: 'blue', // Change this to your filled button color
color: 'white',
},
outlined: {
border: '2px solid blue', // Change this to your outlined button color
backgroundColor: 'transparent',
color: 'blue',
},
};
export function Button({ variant='outlined', children, style, ...rest }: ButtonProps) {
return (
<button
type='button'
style={{
...ButtonStyles[variant],
padding: '10px 20px',
borderRadius: '5px',
cursor: 'pointer',
...style
}} {...rest}>
{children}
</button>
);
}
复制代码
仔细观察代码会发现,这里 Button 组件的 props 类型合并了原生 HTML 中 button 标签属性的全部类型。这意味着,使用者除了可以为 Button 组件设置默认配置外,还可以设置诸如 onClick、aria-label 等自定义配置。这些自定义配置会通过扩展运算符传递给 Button 组件内部的 button 标签。
通过不同的上下文设置,可以看到不同的 Button 组件的形态,效果截图如下图。
这个可以查看具体设置:
https://bit.cloud/lakinduhewa/react-design-patterns/base/button/~compositions
基础组件在不同上下文中的使用效果
通过不同的上下文,你可以设定组件的行为。这可以让组件成为更大组件的基础。
在成功创建了基础组件后,你可能会希望基于基础组件创建一些新的组件。
例如,你可以使用之前创建的 Button 组件来实现一个标准的 DeleteButton 组件。通过在应用中使用该 DeleteButton,可以让应用中所有删除操作在颜色、形态以及字体上保持一致。
不过,如果出现重复组合一组组件来实现相同效果的现象,那么你可以考虑将它们封装到一个组件中。
下面,让我们来看看其中一种实现方案:
https://bit.cloud/lakinduhewa/react-design-patterns/composition/delete-button
使用组合模式创建组件
如上面的组件依赖图所示,DeleteButton 组件使用基础的 Button 组件为所有与删除相关的操作提供标准的实现。下面是基本代码实现:
// 这里引入了,基础按钮组件和其props
import { Button, ButtonProps } from '@lakinduhewa/react-design-patterns.base.button';
import React from 'react';
export type DeleteButtonProps={} & ButtonProps;
export function DeleteButton({ ...rest }: DeleteButtonProps) {
return (
<Button
variant='filled'
style={{
background: 'red',
color: 'white'
}}
{...rest}
>
DELETE
</Button>
);
}
复制代码
我们使用基于模式一创建的 Button 组件来实现的 DeleteButton 组件的效果如下:
现在我们可以在应用中使用统一的删除按钮。此外,如果你使用类似 Bit 的构建系统进行组件的设计和构建,那么当 Button 组件发生改变时,可以让CI服务自动将此改变传递到DeleteButton组件上,就像下面这样(当 Button 组件从 0.0.3 升级到了 0.0.4,那么 CI 服务会自动触发,将 DeleteButton 组件从 0.0.1 升级到 0.0.2):
Bit 上的一个 CI 构建
React Hooks 是React v16就推出来的特性,它不依赖类组件实现状态管理、负效应等概念。简而言之,就是你可以通过利用 Hooks API 摆脱对类组件的使用需求。useSate 和 useEffect 是最广为人知的两个 Hooks API,但本文不打算讨论它们,我想重点讨论如何利用 Hooks 来提高组件的整体可维护性。
例如,请考虑下面这个场景:
基于上面的案例,你可能会像下面这样将 API 逻辑直接写在函数组件中:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const BlogList=()=> {
const [blogs, setBlogs]=useState([]);
const [isLoading, setIsLoading]=useState(true);
const [error, setError]=useState(null);
useEffect(()=> {
axios.get('https://api.example.com/blogs')
.then(response=> {
setBlogs(response.data);
setIsLoading(false);
})
.catch(error=> {
setError(error);
setIsLoading(false);
});
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Blog List</h2>
<ul>
{blogs.map(blog=> (
<li key={blog.id}>{blog.title}</li>
))}
</ul>
</div>
);
};
export default BlogList;
复制代码
这样写,组件也能正常工作。它将会获取博客文章列表并且渲染在 UI 上。但是,这里将 UI 逻辑和 API 逻辑混在一起了。
理想情况下,React 组件应该不需要关系如何获取数据。而只需要关心接收一个数据数组,然后将其呈现在 DOM 上。
因此,实现这一目标的最佳方法是将 API 逻辑抽象到 React Hook 中,以便在组件内部进行调用。这样做就可以打破 API 调用与组件之间的耦合。通过这种方式,就可以在不影响组件的情况下,修改底层的数据获取逻辑。
其中一种实现方式如下。
import { useEffect, useState } from 'react';
import { Blog } from './blog.type';
import { Blogs } from './blog.mock';
export function useBlog() {
const [blogs, setBlogs]=useState<Blog[]>([]);
const [loading, setLoading]=useState<boolean>(false);
useEffect(()=> {
setLoading(true);
// 注意:这里的setTimeout非实际需要,只是为了模拟API调用
setTimeout(()=> {
setBlogs(Blogs);
setLoading(false);
}, 3000);
}, []);
return { blogs, loading }
}
复制代码
如上代码所示,useBlog hook 获取博客列表数据,然后赋值给状态变量,最后通过导出变量给到消费者(BlogList 组件)使用:
Hook 效果
import React from 'react';
// 引入上面封装的 useBlog hook
import { useBlog } from '@lakinduhewa/react-design-patterns.hooks.use-blog';
export function BlogList() {
const { blogs, loading }=useBlog();
if (loading) {
return (
<p>We are loading the blogs...</p>
)
}
return (
<ul>
{blogs.map((blog)=> <ol
key={blog.id}
>
{blog.title}
</ol>)}
</ul>
);
}
复制代码
BlogList 组件效果
通过调用 useBlog 和使用其导出的状态变量,我们在 BlogList 组件中使用了 Hooks。如此,相对于之前,我们可以减少大量代码,并以最少的代码和精力维护两个组件。
此外,当你使用类似 Bit 这样的构建系统时(就像我一样),只需将 useBlog 组件导入本地开发环境,然后再修改完成之后重新推送回 Bit Cloud。Bit Cloud 的构建服务器可以依托依赖树将此修改传递给整个应用。因此如果只执行一些简单修改,甚至不需要访问整个应用。
此模式的核心是解决组件状态共享。我们都曾是 props 下钻式传递的受害者。但如果你还没有经历过,那这里简单解释下:“props 下钻式传递”就是当你在组件树中进行 props 传递时,这些 props 只会在最底层组件中被使用,而中间层的组件都不会使用该 props。例如,看看下面这张图:
props 下钻式传递
从 BlogListComponent 一直向下传递一个 isLoading 的 props 到 Loader。但是,isLoading 只在 Loader 组件中使用。因此,在这种情况下,组件不但会引入不必要的 props,还会有性能开销。因为当 isLoading 发生变化时,即使组件没有使用它,React 依然会重新渲染你的组件树。
因此,解决方案之一就是通过利用 React Context 来使用 React Context Provider 模式。React Context 是一组组件的状态管理器,通过它,你可以为一组组件创建特定的上下文。通过这种方式,你可以在上下文中定义和管理状态,让不同层级的组件都可以直接访问上下文,并按需使用 props。这样就可以避免 props 下钻式传递了。
主题组件就是该模式的一个常见场景。例如,你需要在应用程序中全局访问主题。但将主题传递到应用中的每个组件并不现实。你可以创建一个包含主题信息的 Context,然后通过 Context 来设置主题。看一下我是如何通过React Context实现主题的,以便更好地理解这一点:
https://bit.cloud/lakinduhewa/react-design-patterns/contexts/consumer-component
import { useContext, createContext } from 'react';
export type SampleContextContextType={
/**
* primary color of theme.
*/
color?: string;
};
export const SampleContextContext=createContext<SampleContextContextType>({
color: 'aqua'
});
export const useSampleContext=()=> useContext(SampleContextContext);
复制代码
在 Context 中定义了一种主题颜色,它将在所有实现中使用该颜色来设置字体颜色。接下来,我还导出了一个 hook——useSampleContext,该 hook 让消费者可以直接使用 Context。
只是这样还不行,我们还需要定义一个 Provider。Provider 是回答 "我应该与哪些组件共享状态?"问题的组件。Provider的实现示例如下:
import React, { ReactNode } from 'react';
import { SampleContextContext } from './sample-context-context';
export type SampleContextProviderProps={
/**
* primary color of theme.
*/
color?: string,
/**
* children to be rendered within this theme.
*/
children: ReactNode
};
export function SampleContextProvider({ color, children }: SampleContextProviderProps) {
return <SampleContextContext.Provider value={{ color }}>{children}</SampleContextContext.Provider>
}
复制代码
Provider 在管理初始状态和设置 Context 可访问状态的组件方面起着至关重要的作用。
接下来,你可以创建一个消费者组件来使用状态:
消费者组件
最后一个想和大家分享的是条件渲染模式。今天,人人都知道 React 中的条件渲染。它通过条件判断来选择组件进行渲染。
但在实际使用中我们的用法常常是错误的:
// ComponentA.js
const ComponentA=()=> {
return <div>This is Component A</div>;
};
// ComponentB.js
const ComponentB=()=> {
return <div>This is Component B</div>;
};
// ConditionalComponent.js
import React, { useState } from 'react';
import ComponentA from './ComponentA';
import ComponentB from './ComponentB';
const ConditionalComponent=()=> {
const [toggle, setToggle]=useState(true);
return (
<div>
<button onClick={()=> setToggle(!toggle)}>Toggle Component</button>
{toggle ? <ComponentA /> : <ComponentB />}
</div>
);
};
export default ConditionalComponent;
复制代码
你是否注意到,这里我们将基于条件的逻辑耦合到了 JSX 代码片段中。通常,你不应该在 JSX 代码中中添加任何与计算相关的逻辑,而只将与 UI 渲染相关的内容放在其中。
解决这个问题的方法之一是使用条件渲染组件模式。创建一个可重用的 React 组件,该组件可以根据条件渲染两个不同的组件。它的实现过程如下:
import React, { ReactNode } from 'react';
export type ConditionalProps={
/**
* the condition to test against
*/
condition: boolean
/**
* the component to render when condition is true
*/
whenTrue: ReactNode
/**
* the component to render when condition is false
*/
whenFalse: ReactNode
};
export function Conditional({ condition, whenFalse, whenTrue }: ConditionalProps) {
return condition ? whenTrue : whenFalse;
}
复制代码
我们创建了一个可以按条件渲染两个组件的组件。当我们将其集成到其他组件中时,会使代码更简洁,因为无需在 React 组件中加入复杂的渲染逻辑。你可以像下面这样使用它:
export const ConditionalTrue=()=> {
return (
<Conditional
condition
whenFalse="You're False"
whenTrue="You're True"
/>
);
}
export const ConditionalFalse=()=> {
return (
<Conditional
condition={false}
whenFalse="You're False"
whenTrue="You're True"
/>
);
}
复制代码
实际的输入如下:
掌握这五种设计模式,为 2024 年做好充分准备,构建出可扩展和可维护的应用吧。
如果你想详细深入本文中讨论的模式,请随时查看我在Bit Cloud的空间:
https://bit.cloud/lakinduhewa/react-design-patterns
感谢你的阅读!
原文链接:2024年,你应该知道的5种React设计模式_架构/框架_InfoQ精选文章
日常开发中,我们有一些组件的样式是重复的。
例如,Text 组件的 width、height 都是一样的。
@Entry
@Component
struct Index {
build() {
Column() {
Text("春明不觉晓")
.width(100)
.height(50)
Text("处处闻啼鸟")
.width(100)
.height(50)
Text("夜来风雨声")
.width(100)
.height(50)
Text("花落知多少")
.width(100)
.height(50)
}
.width('100%')
}
}
那么,我们可以将这些重复的样式抽出来,做成一个公共样式。
公共样式是一个函数,格式为:@Style 样式名称() { 样式描述 }
例如这里的 @Style textSize(){}。
@Entry
@Component
struct Index {
@Styles textSize(){
.width(100)
.height(50)
}
build() {
Column() {
Text("春明不觉晓")
.width(100)
.height(50)
Text("处处闻啼鸟")
.width(100)
.height(50)
Text("夜来风雨声")
.width(100)
.height(50)
Text("花落知多少")
.width(100)
.height(50)
}
.width('100%')
}
}
接下来,我们就可以使用 textSize 去替换掉那些重复的样式。
@Entry
@Component
struct Index {
@Styles textSize(){
.width(100)
.height(50)
}
build() {
Column() {
Text("春明不觉晓")
.textSize()
Text("处处闻啼鸟")
.textSize()
Text("夜来风雨声")
.textSize()
Text("花落知多少")
.textSize()
}
.width('100%')
}
}
运行结果是不变的:
使用公共样式可以帮我们减少很多重复的代码。
推荐大家在日常开发中多多使用 @Style 定义公共样式。
注意:当前 @Styles 仅支持通用属性和通用事件。
通用属性文档地址:
https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-universal-attributes-size-0000001428061700-V3
通用事件文档地址:
https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-universal-events-click-0000001477981153-V3
还有一点需要注意的是,@Styles方法不支持参数。下列操作是不允许的:
// 错误写法,不允许有参数
@Styles textSize(message: string){
.width(100)
.height(50)
}
至此,@Style 基本使用方法介绍完毕。
者|Next.js 团队
译者|无明
出处丨前端之巅
在经过 26 次金丝雀发布和 340 万次下载之后,近日,我们正式发布了 Next.js 7.0,新功能包括:
Next.js 的主要目标之一是提供最佳的性能和开发者体验。最新版本为构建和调试管道带来了很多重大改进。
得益于 Webpack 4 和 Babel 7,以及我们对代码库做出的很多改进和优化,Next.js 现在在开发过程中的启动速度提高了 57%。
我们新增了增量编译缓存,让变更代码的构建速度快了 40%。
以下是我们收集的一些示例数据:
因为使用了 webpackbar,在开发和构建的同时可以看到更好的实时反馈:
使用 react-error-overlay 更好地报告错误
准确地渲染错误对于良好的开发和调试体验来说是至关重要的。到目前为止,我们可以渲染错误消息和堆栈跟踪信息。我们在此基础上更进一步,我们使用 react-error-overlay 来丰富堆栈跟踪信息:
这是之前和之后的错误显示比较:
另外,借助 react-error-overlay,你只需单击特定代码块就可以轻松打开文本编辑器。
从发布第一个版本以来,Next.js 一直使用 Webpack 来打包代码和重用丰富的插件。Next.js 现在使用了最新的 Webpack 4,其中包含很多改进和 bug 修复。
另一个新功能是支持 WebAssembly,Next.js 甚至可以进行 WebAssembly 服务器渲染。
这里有一个例子:
https://github.com/zeit/next.js/tree/canary/examples/with-webassembly
因为使用了 Webpack 4,我们引入了一种从捆绑包中提取 CSS 的新方法,这个插件叫作 mini-extract-css-plugin(https://github.com/webpack-contrib/mini-css-extract-plugin)。
mini-extract-css-plugin 提供了 @zeit/next-css、@zeit/next-less、@zeit/next-sass 和 @zeit/next-stylus。
这些 Next.js 插件的新版本解决了与 CSS 导入相关的 20 个问题,例如,现在支持 import() 动态导入 CSS:
// components/my-dynamic-component.js import './my-dynamic-component.css' export default ()=> <h1>My dynamic component</h1> // pages/index.js import dynamic from 'next/dynamic' const MyDynamicComponent=dynamic(import('../components/my-dynamic-component')) export default ()=> <div> <MyDynamicComponent/> </div>
一个重大改进是现在不再需要在 pages/_document.js 中添加一下内容:
<link rel="stylesheet" href="/_next/static/style.css" />
Next.js 会自动注入这个 CSS 文件。在生产环境中,Next.js 还会自动向 CSS URL 中添加内容哈希,当文件发生变更时,最终用户就不会得到旧文件,并且能够获得不可变的永久缓存。
简而言之,要在 Next.js 项目中支持导入.css 文件,只需要在 next.config.js 中注册 withCSS 插件:
const withCSS=require('@zeit/next-css') module.exports=withCSS({/* my next config */})
从版本 3 开始,Next.js 就通过 next/dynamic 来支持动态导入。
作为这项技术的早期采用者,我们必须自己编写解决方案来处理 import()。
因此,Next.js 逐渐缺失 Webpack 后来引入的一些功能,包括 import()。
例如,无法手动命名和捆绑某些文件:
import(/* webpackChunkName: 'my-chunk' */ '../lib/my-library')
另一个例子是在 next/dyanmic 模块之外使用 import()。
从 Next.js 7 开始,我们不再直接使用默认的 import(),Next.js 为我们提供了开箱即用的 import() 支持。
这个变更也是完全向后兼容的。使用动态组件非常简单:
import dynamic from 'next/dynamic' const MyComponent=dynamic(import('../components/my-component')) export default ()=> { return <div> <MyComponent /> </div> }
这段代码的作用是为 my-component 创建一个新的 JavaScript 文件,并只在渲染< MyComponent/>时加载它。
最重要的是,如果没有进行渲染,< script>标记就不会出现在初始 HTML 文档中。
为了进一步简化我们的代码库并利用优秀的 React 生态系统,在 Next.js 7 中,我们使用 react-loadable 重写了 next/dynamic 模块。这为动态组件引入了两个很棒的新特性:
Next.js 6 中就已经引入了 Babel 7 测试版。后来 Babel 7 稳定版本发布,现在,Next.js 7 正在使用这个新发布的稳定版 Babel 7。
一些主要特性:
如果你的 Next.js 项目中没有自定义 Babel 配置,那么就不存在重大变更。
但如果你具有自定义 Babel 配置,则必须将相应的自定义插件 / 预设升级到最新版本。
如果你从 Next.js 6 以下的版本升级,可以使用 babel-upgrade 工具。
除了升级到 Babel 7 之外,当 NODE_ENV 被设置为 test 时,Next.js Babel 预设(next/babel)现在默认将 modules 选项设置为 commonjs。
这个配置选项通常是在 Next.js 项目中创建自定义.babelrc 的唯一理由:
{ "env": { "development": { "presets": ["next/babel"] }, "production": { "presets": ["next/babel"] }, "test": { "presets": [["next/babel", { "preset-env": { "modules": "commonjs" } }]] } } }
使用 Next.js 7,这将变成:
{ "presets": ["next/babel"] }
现在可以删除.babelrc,因为在没有 Babel 配置时,Next.js 将自动使用 next/babel。
Next.js 在预渲染 HTML 时会将页面内容放在< html>、< head>、< body>结构中,并包含页面所需的 JavaScript 文件。
这个初始载荷之前约为 1.62kB。在 Next.js 7 中,我们优化了初始 HTML 载荷,现在为 1.5kB,减少了 7.4%,让页面变得更加精简。
我们主要通过以下几种方式来缩小文件:
在 Next.js 5 中,我们引入了 assetPrefix 支持,让 Next.js 可以自动从某个位置(通常是 CDN)加载资源。如果你的 CDN 支持代理,可以使用这种办法。你可以像这样请求资源:
https://cdn.example.com/_next/static/<buildid>/pages/index.js
通常,CDN 先检查缓存中是否包含这个文件,否则直接从源中请求文件。
不过,某些解决方案需要将目录直接预先上传到 CDN 中。这样做的问题在于 Next.js 的 URL 结构与.next 文件夹中的文件夹结构不匹配。例如我们之前的例子:
https://cdn.example.com/_next/static/<buildid>/pages/index.js // 映射到: .next/page/index.js
在 Next.js 7 中,我们改变了.next 的目录结构,让它与 URL 结构相匹配:
https://cdn.example.com/_next/static/<buildid>/pages/index.js // 映射到: .next/static/<buildid>/pages/index.js
尽管我们建议使用代理类型的 CDN,但新结构也允许不同类型 CDN 的用户将.next 目录上传到 CDN。
我们也引入了 styled-jsx 3,Next.js 的默认 CSS-in-JS 解决方案,现在已经为 React Suspense 做好了准备。
如果一个组件不属于当前组件作用域的一部分,那么该如何设置这个子组件的样式呢?例如,如果你将一个组件包含在父组件中,并只有当它被用在父组件中时才需要特定的样式:
const ChildComponent=()=> <div> <p>some text</p> </div> export default ()=> <div> <ChildComponent /> <style jsx>{` p { color: black } `}</style> </div>
上面的代码试图选择 p 标签,但其实不起作用,因为 styled-jsx 样式被限定在当前组件,并没有泄漏到子组件中。解决这个问题的一种方法是使用:global 方法,将 p 标记的前缀移除。但这样又引入了一个新问题,即样式泄露到了整个页面中。
在 styled-jsx 3 中,通过引入一个新的 API css.resolve 解决了这个问题,它将为给定的 syled-jsx 字符串生成 className 和< style>标签(styles 属性):
import css from 'styled-jsx/css' const ChildComponent=({className})=> <div> <p className={className}>some text</p> </div> const { className, styles }=css.resolve`p { color: black }` export default ()=> <div> <ChildComponent className={className} /> {styles} </div>
这个新 API 可以将自定义样式传给子组件。
由于这是 styled-jsx 的主要版本,如果你使用了 styles-jsx/css,那么在捆绑包大小方面有一个重大变化。在 styled-jsx 2 中,我们将生成外部样式的“scoped”和“global”版本,即使只使用“scoped”版本,我们也会将“global”版本包含在内。
使用 styled-jsx 3 时,全局样式必须使用 css.global 而不是 css,这样 styled-jsx 才能对包大小进行优化。
App 和 Page 之间的 React Context(服务器端渲染)
从 Next.js 7 开始,我们支持 pages/_app.js 和页面组件之间的 React Context API。
以前,我们无法在服务器端的页面之间使用 React 上下文。原因是 Webpack 保留了内部缓存模块而不是使用 require.cache,我们开发了一个自定义 Webpack 插件来改变这种行为,以便在页面之间共享模块实例。
这样我们不仅可以使用新的 React 上下文,在页面之间共享代码时还能减少 Next.js 的内存占用。
从 Next.js 首次发布以来,就已获得相当多的用户,从财富 500 强公司到个人博客。我们非常高兴看到 Next.js 的采用量一直在增长。
目前,超过 12,500 个被公开索引的域名在使用 Next.js。我们有超过 500 名贡献者,他们至少提交过一次代码。在 GitHub 上,这个项目已经有 29,000 个 star。自首次发布以来,已提交了大约 2200 个拉取请求。
Next.js 社区在 spectrum.chat/next-js 上拥有近 2000 名成员。
英文原文
https://nextjs.org/blog/next-7
*请认真填写需求信息,我们会在24小时内与您取得联系。