组件化的模式

为什么说组件化在前端,特别是基于 JS 开发的 React 框架中,有着非常重要的位置呢?
一是它可以达到和桌面应用类似的功能
二是这样节省了资源在我们的手机或是 PC 上的下载和存储
三是因为这样可以让我们随时随地访问我们需要的内容,只要有网络,输入一个 URL 便可以使用

在 React 中的组件化和我们通常了解的 Web Component 是有区别的。
Web Component 的时候,更多关注的是组件的封装和重用,也就是经典的面向对象的设计模式思想。React Component 更多关注的是,通过声明式的方式更好地让 DOM 和状态数据之间同步。
React 推出了一些不同的模式:上下文提供者渲染属性高阶组件和后来出现的 Hooks

我们可以大体将这些组件化的模式分为两类,一类是在 Hooks 出现之前的上下文提供者、渲染属性、高阶组件模式,一类是 Hooks 出现后带来的新模式。

经典模式

上下文提供者模式(Context Provider Pattern):通过创建上下文将数据传给多个组件的组件化方式。
它的作用是可以避免 prop-drilling,也就是避免将数据从父组件逐层下传到子组件的繁琐过程。
举个例子,假如我们有一个菜单,里面包含了一个列表和列表元素。通过以下代码,我们看到如果将数据一层层传递,就会变得非常繁琐。

function App() {
  const data = { ... }
  return (<Menu data={data} />);
}

var Menu = ({ data }) => <List data={data} />
var List = ({ data }) => <ListItem data={data} />
var ListItem = ({ data }) => <span>{data.listItem}</span> 

而通过 React.createContext,我们创建一个主题。之后通过 ThemeContext.Provider,我们可以创建一个相关的上下文。这样我们无需将数据一一传递给每个菜单里的元素,便可以让上下文中的元素都可以获取相关的数据。

var ThemeContext = React.createContext();

function App() {
  var data = {};
  return (
      <ThemeContext.Provider value = {data}>
        <Menu />
      </ThemeContext.Provider>  
  )
}

通过 React.useContext,可以获取元素上下文中的数据来进行读写。
渲染属性模式(Render Props Pattern)
比如在下面的价格计算器的例子中,我们想让程序根据输入的产品购买数量计算出价格。但是,在没有渲染属性的情况下,计算价格的组件并拿不到输入购买的数量,所以计算不出价格。

export default function App() {
  return (
    <div className="App">
      <h1>价格计算器</h1>
      <Input />
      <Amount />
    </div>
  );
}

function Input() {
  var [value, setValue] = useState("");
  return (
    <input type="text" 
      value={value} 
      placeholder="输入数量"
      onChange={e => setValue(e.target.value)} 
    />
  );
}

function Amount({ value = 0 }) {
  return <div className="amount">{value * 188}元</div>;
}

为了解决这个问题,我们就可以用到 render props,把 amount 作为 input 的子元素,在其中传入 value 参数。也就是说通过渲染属性,我们可以在不同的组件之间通过属性来共享某些数据或逻辑。

export default function App() {
  return (
    <div className="App">
      ...
      <Input>
        {value => (
          <>
            <Amount value={value} />
          </>
        )}
      </Input>
    </div>
  );
}

function Input() {
  ...
  return (
    <>
      <input ... />
      {props.children(value)}
    </>  
  );
}

function Amount({ value = 0 }) {
  ...  
}

高阶组件模式(HOC,Higher Order Components Pattern): 就是我们可以把一个组件作为参数传入,并且返回一个组件。
![图片]
那么它有什么应用呢?假设在没有高阶组件的情况下,我们想给一些按钮或文字组件增加一个圆边,可能要修改组件内的代码,而通过高阶函数,我们可以在原始的直角的文字框和按钮组件的基础上面包装一些方法来得到圆边的效果。在实际的应用中,它可以起到类似于“装饰器”的作用。它不仅让我们不需要对组件本身做修改,而且还可以让我们重复使用抽象出来的功能,避免代码冗余。

// 高阶函数
var enhancedFunction = higherOrderFunction(originalFunction);
// 高阶组件
var enhancedComponent = higherOrderComponent(originalComponent);

// 高阶组件作为装饰器
var RoundedText = withRoundCorners(Text);
var RoundedButton = withRoundCorners(Button);

Hooks 模式

Hooks 最直接的作用是可以用函数来代替 ES6 引入的新的 class 创建组件。
传统class方式创建

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        点击了{this.state.count}次。
      </button>
    );
  }
}

通过Hook方式创建

import React, { useState } from 'react';
function APP() {
  var [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>点击了{count}次。
      </button>
    </div>
  );
}

在这个例子中,你可以看到,我们刚才是通过用解构(destructure)的方式,创建了两个计数状态的变量,一个是 count,另外一个是 setCount。这样当我们将这两个值赋值给 userState(0) 的时候,它们会分别被赋值为获取计数和更新计数。

// 数组解构
var [count, setCount] = useState(0);

// 上等同于
var countStateVariable = useState(0); 
var count = countStateVariable[0]; 
var setCount = countStateVariable[1];

Hooks 另外的一个作用是可以让组件按功能解耦、再按相关性组合的功能
比如在没有 Hooks 的情况下,我们可能需要通过组件的生命周期来组合功能。如果我们用的是同一个组件的生命周期 componentDidMount 管理,那就会将不相干的功能聚合在了一起,而通过 useEffect 这样的一个 Hook,就可以把不相干的功能拆开,再根据相关性聚合在一起。
Hooks 还可以让逻辑在组件之间更容易共享

加载渲染模式

渲染模式
前端单页应用(SPA)带来了方便的同时,也会造成性能上问题,比如它的 FCP(First Contentful Paint,首次内容绘制时间)、 LCP(Largest Contentful Paint,最大内容绘制时间)、TTI(Time to Interactive,首次可交互时间) 会比较长。
前端渲染除了性能上的问题,还会造成 SEO 的问题。通常为了解决 SEO 的问题,一些网站会在 SPA 的基础上再专门生成一套供搜索引擎检索的后端页面。但是作为搜索的入口页面,后端渲染的页面也会被访问到,它最大的问题就是到第一字节的时间(TTFB)会比较长。

为了解决前端和后端渲染的问题,静态渲染(static rendering)的概念便出现了。静态渲染使用的是一种预渲染(pre-render)的方式。也是说在服务器端预先渲染出可以在 CDN 上缓存的 HTML 页面,当前端发起请求的时候,直接将渲染好了的文件发送给后端,通过这种方式,就降低了 TTFB。
静态渲染一般被称之为静态生成(SSG,static generation),而由此,又引出了静态渐进生成(iSSG,incremental static generation)的概念。
静态生成一般用于处理静态内容。而对于需要动态更新的页面就要用到静态渐进生成。iSSG 可以在 SSG 的基础上做到对增量页面的生成和存量部分的再生成。

虽然页面内容分为静态和动态之分,但有些页面总体上是不需要频繁交互的,如果静态页面需要相关的行为互动的情况下。SSG 就只能保证 FCP,但是很难保证 TTI 了。静态加载的元素赋予动态的行为。而在用户发起交互的时候,再水合的动作就是渐进式水合(progressive hydration)。
除了静态渲染和水合可以渐进外,后端渲染也可以通过 node 中的流(stream)做到后端渐进渲染(progressive SSR)。通过流,页面的内容可以分段传到前端,前端可以先加载先传入的部分。除了渐进式水合外,选择性水合可以利用 node stream 暂缓部分的组件传输,而将先传输到前端的部分进行水合,这种方式就叫做选择性水合(selective hydration)。

还有一种集大成的模式叫做岛屿架构(islands architecture):就好像我们在地理课学到的,所有的大陆都可以看作是漂流在海洋上的“岛屿”一样,这种模式把页面上所有的组件都看成是“岛屿”。把静态的组件视为静态页面“岛屿”,使用静态渲染;而对于动态的组件则被视为一个个的微件“岛屿”,使用后端加水合的方式渲染。
加载模式
配合上述的渲染模式,相应的也需要对应的加载模式,对于静态内容,就通过静态倒入;动态的内容则通过动态倒入。基于渐进的思想,我们也可以在部分内容活动到特定区域或者交互后,将需要展示的内容渐进地导入。被导入的内容可以通过分割打包(bundle splitting),根据路径(route based splitting)来做相关组件或资源的加载。

PRPL(Push Render, Pre-Cache, Lazy-load):PRPL 模式的核心思想是在初始化的时候,先推送渲染最小的初始化内容。之后在背后通过 service worker 缓存其它经常访问的路由相关的内容,之后当用户想要访问相关内容时,就不需要再请求,而直接从缓存中懒加载相关内容。
PRPL 的思想是如何实现的呢?
背景:相比 HTTP1.1,HTTP2 中提供的服务器推送可以一次把初始化所需要的资源以外的额外素材都一并推送给客户端,PRPL 就是利用到了 HTTP2 的这个特点。可是光有这个功能还不够,因为虽然这些素材会保存在浏览器的缓存中,但是不在 HTTP 缓存中,所以用户下次访问的时候,还是需要再次发起请求。
解决问题方案:PRPL 就用到了 service worker 来做到将服务器推送过来的内容做预缓存。同时它也用到了代码分割(code splitting),根据不同页面的路由需求将不同的组件和资源分割打包,来按需加载不同的内容。
还有一个我们需要注意的概念就是,pre-fetch 不等于 pre-load。pre-fetch 更多指的是预先从服务器端获取,目的是缓存后,便于之后需要的时候能快速加载。而预加载则相反,是加载特别需要在初始化时使用的素材的一种方式,比如一些特殊字体,我们希望预先加载,等有内容加载时能顺滑地展示正确样式的字体。

性能优化模式
摇树优化
摇树优化的作用是移除 JavaScript 上下文中未引用的代码(dead-code)。那为什么我们需要移除这些代码呢?因为这些未被使用的代码如果存在于最后加载的内容中,会占用带宽和内存,而如果它们并不会在程序执行中用到,那就可以被优化掉了。

虚拟列表优化
它名字中的“虚拟化”一词从何而来呢?这就有点像我们在量子力学里面提到的“薛定谔的猫”思想,就是我们眼前的事物只有在观测的一瞬间才会被渲染出来,在这里我们的世界就好像是“虚拟”的沙箱。而在虚拟列表中,我们同样也只关注于渲染窗口移动到的位置。这样就可以节省算力和相关的耗时。

在基于 React 的三方工具中,有支持虚拟列表优化的react-window 和react-virtualized。

此文章为2月Day6学习笔记,内容来源于极客时间《Jvascript进阶实战课》,大家共同进步💪💪

豪猪
4 声望4 粉丝

undefined