苏格网站建设,亚马逊站外推广网站怎么做,个人博客网站怎么赚钱,查询公司的网站备案奇技指南本文内容主要是我之前分享的文字版#xff0c;若想看重点的话可以看之前的Slide: https://ppt.baomitu.com/d/75fc979a本文作者奇舞团前端开发工程师李喆明。需求描述由于作者所在的业务是资讯内容类业务#xff0c;因而在业务中会经常碰到如下场景#xff1a;有一个… 奇技指南本文内容主要是我之前分享的文字版若想看重点的话可以看之前的Slide: https://ppt.baomitu.com/d/75fc979a本文作者奇舞团前端开发工程师李喆明。需求描述由于作者所在的业务是资讯内容类业务因而在业务中会经常碰到如下场景有一个内容列表列表中需要按照一定的规则插入广告。除了获取广告数据广告展现和点击后需要有打点上报逻辑。正常来说会这么写import React from react;export default class extends React.Component { state {newsData: [], adData: []}; constructor() { this.getNewsData(); } getNewsData() { const newsData [...]; this.setState({newsData}); this.getAdData(newsData.length / 2); //根据新闻数和插入规则换算广告请求数 } getAdData() { const adData [...]; this.setState({adData}); } render() { const {newsData, adData} this.state; const comps []; for(let i 0; i newsData.length; i) { // 根据插入规则判断当前新闻卡片后是否要插入广告 comps.push(NewsCard {...newsData[i]} key{news-${i}} /); if(i % 2) { comps.push(AdCard {...adData[i/2]} key{ad-${i}} /); } } return (div{comps}/div); }}class AdCard extends React.Component { componentDidMount() { observe(this.dom, () {}); } onClick () {}; onMouseUp () {}; onMouseDown () {}; getDOM dom this.dom dom; render() { return div ref{this.getDOM} onMouseUp{this.onMouseUp} onMouseDown{this.onMouseDown} onClick{this.onClick} {this.props.title}/div }}逻辑非常的简单getNewsData() 拿到资讯列表数据之后计算需要请求的广告数调用 getAdData()请求广告数据最后根据插入规则将资讯和内容渲染到列表中。广告使用自定义组件渲染使用 Intersection Observe API 实现广告曝光打点监听 DOM 对应的点击时间实现广告点击打点。如果说只有一个组件是这样的还好说但是从上图可以看出我们有大量的内容广告混排场景。整体的逻辑和刚才说的都是一样的唯一的区别是不同的列表对应不一样的显现形式。在这种情况下如何设计一个既能将通用逻辑提取又能满足各个模块的自定义需求的通用模块就成了我们必须考虑的事情了。React 组件设计模式在具体讨论方案之前我们先简单的了解一下常见的 React 组件设计模式。基本上分为以下几种方案Context 模式组合组件继承模式容器组件和展示组件Render PropsHoc 高阶组件其中 Context 模式多用来在多层嵌套组件中进行跨组件的数据传递针对我们当前组件层级不多的情况用处不是非常大这里就不多表。我们来看看剩下的几个模式各自有什么优缺点最终来评估下是否能应用到我们的场景中。组合组件组合组件是通过模块化组件构建应用的模式它是 React 模块化开发的基础。除去普通的按照正常的业务功能进行模块拆分还有就是将配置和逻辑进行解耦的组合组件方式。例如下面的组合方式就是利用类似 Vue 的 slot 方式将配置通过子组件的形式与组件进行组合是的组件配置更优雅。Modal Modal.TitleModal TitleModal.Title Modal.ContentModal ContentModal.Content Modal.Footer buttonOKbutton Modal.FooterModal又如下面的下拉选择组件通过将 和 进行组合即达到了组件化配置的目的又达到了通用方法的复用。同时将点击操作在 组件中直接传递下去方便了点击后直接修改选择状态。export default function(props) { return React.Children.map(props.children, child React.cloneElement(child, {onClick() { console.log(click) }} ));}Select OptionClick Me!Option OptionClick Me!OptionSelect继承模式继承模式是使用类继承的方式对组件代码进行复用。在面向对象编程模式中继承是一种非常简单且通用的代码抽象复用方式。如果大部分逻辑相同只是一些细节不一致只要简单的将不一致的地方抽成成员方法继承的时候复写该成员方法即可达到简单的组件复用。不过我们知道 JS 中的继承本质上还是通过原型链实现的语法糖所以在一些场景使用上没有其它语言的继承那么方便例如无法直接实现多继承多继承后的跨层级方法调用比较麻烦适合简单的逻辑复用。另外通过继承方式会将父类中的所有方法都继承过来不小心的话非常容易继承到不需要的功能。容器组件和展示组件展示组件和容器组件是将数据逻辑和渲染逻辑进行拆分从而降低组件复杂度的模式。使用容器组件可以把最开始的代码改写成如下的形式。这样做最大的好处是渲染层可以抽离成无状态组件它不需要关心数据的获取逻辑直接通过 props获取数据渲染即可针对展示组件能实现很好的复用。class NewsList extends React.Component { state {newsData: [], adData: []}; constructor() { this.getNewsData(); } getNewsData() { this.getAdData(newsData.length / 2) } getAdData() {} render() { return List news{this.state.newsData} ad{this.state.adData} / }}function List({news, ad}) { const {newsData, adData} this.state; const comps []; for(let i 0; i newsData.length; i) { comps.push(NewsCard {...newsData[i]} key{news-${i}} /); if(i % 2) { comps.push(AdCard {...adData[i/2]} key{ad-${i}} /); } } return (div{comps}/div);}但是我们也可以看到即使我们把渲染逻辑拆分出去了本身组件的数据逻辑还是非常的复杂没有做到很好的拆分。同时容器组件和展示组件存在耦合关系所以无法很好的对逻辑组件进行复用。Render Props术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术它的本质实际上是通过一个函数 prop 将数据传递到其它组件的方式所以按照这个逻辑我们又可以将刚才的代码简单的改写一下。class NewsList extends React.Component { state {newsData: [], adData: []}; constructor() { this.getNewsData(); } getNewsData() { this.getAdData(newsData.length / 2) } getAdData() {} render() { return this.props.render(this.state) }}function List({news, ad}) { const {newsData, adData} this.state; const comps []; for(let i 0; i newsData.length; i) { comps.push(NewsCard {...newsData[i]} key{news-${i}} /); if(i % 2) { comps.push(AdCard {...adData[i/2]} key{ad-${i}} /); } } return (div{comps}/div);}NewsList render{({newsData, adData}) List news{newsData} ad{adData} /可以看到通过一个函数调用我们将数据逻辑和渲染逻辑进行解耦解决了之前数据逻辑无法复用的问题。不过通过函数回调的形式将数据传入如果想要把逻辑拆分(例如资讯数据获取与广告数据获取逻辑拆分)会变得比较麻烦让我想起了被 callback 支配的恐惧。同时由于render 的值为一个匿名函数每次渲染 的时候都会重新生成而这个匿名函数执行的时候会返回一个 组件这个本质上每次执行也是一个“新”的组件。所以 Render Props 使用不当的话会非常容易造成不必要的重复渲染。HoC 组件React 里还有一种使用比较广泛的组件模式就是 HoC 高阶组件设计模式。它是一种基于 React 的组合特性而形成的设计模式它的本质是参数为组件返回值为新组件的函数。我们来看看刚才的代码使用 HoC 组件修改后会变成什么样子。function withNews(Comp) { return class extends React.Component { state {newsData: []}; constructor() { this.getNewsData(); } render() { return Comp {...this.props} news{this.state.newsData} / } }}function withAd(Comp) { return class extends React.Component { state {adData: []}; componentWillReceiveProps(nextProps) { if(this.props.news.length) { this.getAdData(); } } render() { return Comp {...this.props} ad{this.state.adData} / } }}const ListWithNewsAndAd withAd(withNews(List));可以看到这次改动最激动的地方在于我们第一次把数据逻辑进行了拆分这也是高阶组件的魅力它不局限于 UI 复用使得代码复用更加自由(当然 Render Props 也是可以实现的)。当然这种模式也并不是完美的它也有它的缺点。我们可以看到它的本质是通过 props 在高阶组件中将多个数据传入到子组件中非常类似 mixin 的形式。所以它也会有 mixin 的缺点那就是属性名冲突的问题。由于不同的高阶组件由不同的开发者开发内部会传递什么样的属性名到子组件中就成了未知数。同时多层组件的嵌套导致组件层级过多在性能和调试上都会带来问题。初版实现了解完这些设计模式之后我们再回头来看看我们的需求。通过观察了解不同的组件中的共同部分之后我们可以将这种类型的组件抽象为如下描述“在一个内容列表中按照一定规则插入一定数量的和内容一致的一定样式的广告组件”。在这段描述中存在着三个不定因素一定规则不同的组件插入广告的逻辑是不一样的一定数量不同的组件由于资讯内容的不同插入逻辑的不同导致需要的广告数量也是不一样的一定样式不同的组件由于资讯内容样式不同所以广告的样式自然也不相同除却以上三个因素之外广告其它的逻辑广告数据的获取以及广告的曝光和点击打点等都是通用的。最后我们将广告组件的逻辑顺着之前了解的设计模式抽离成三个部分广告数据的获取广告模块的渲染 Base 模块广告模块的插入由具体业务处理import React from react;import Mediav from q/mediav;export default class extends React.Component { state {newsData: []}; constructor() { this.getNewsData(); } render() { const comps []; for(let i 0; i newsData.length; i) { comps.push(NewsCard {...newsData[i]} key{news-${i}} /); if(i % 2) { comps.push(AdCard key{ad-${i}} /); } } return (Mediav.Provider idxxx{comps}/Mediav.Provider); }}class AdCard extends Mediav.Item { render() { if(!this.props.type) { return null; } const {title} this.props; return (div ref{this.getDOM} onClick{this.onClick} onMouseUp{this.onMouseUp} onMouseDown{this.onMouseDown} {title}/div); }}通过容器组件 对数据获取逻辑进行封装通过遍历子组件找到 组件的示例个数来告知需要请求的广告数量。请求到广告后通过 Props 注入的形式传入到渲染组件中。而渲染组件 继承自 一方面能告诉容器组件它是广告组件的插槽同时还能抽离广告曝光打点和点击打点等通用逻辑进行复用。在用户自定义的 组件中我们可以自定义不同模块的广告组件的渲染样式最终完成了一套广告组件的渲染。不过这样实现还是有一些不足的地方。广告曝光检测需要依赖原生 DOM而 Ref 使用forwardRef() 在组件间传递稍微有点复杂所以最后采用了继承模式进行公共方法的抽离。子组件继承后自行绑定父类的一些方法即可在这点上理解起来有点晦涩看起来总像是绑定了一些“不存在”的方法。React Hooks针对上面提出的问题有没有什么方法可以解决呢最终我想到了 Hooks 的方案通过使用 Hooks 改写后能完美的解决这个问题。我们先简单的了解下什么是 Hooks它允许我们在不编写 class 的情况下使用 state 和 React 生命周期等相关特性。const {useState, useEffect} React;function App() { const [count, setCount] useState(0); useEffect(() { const interval setInterval(() setCount(count 1), 1000); return () clearInterval(interval); }); return span{count}/span;}ReactDOM.render(App /, document.getElementById(app));可以看到它使用 useState 提供了 state使用useEffect来做一些需要在声明周期中执行的方法。使用 useEffect代理了原来生命周期的概念后让代码理解起来更加简单。当然这不是 Hooks 厉害的地方它最厉害的地方是支持自定义 Hooks通过自定义 Hooks 你能对逻辑进行统一的封装。针对一个数据获取的逻辑我们需要定义state然后在初始化的时候去获取数据当 id 发生变化后我们需要重新获取数据。class User extends React.Component { state { user: {} } constructor(...args) { super(...args); const {name} this.props; this.getUserInfo(name) } componentWillReceiveProps(nextProps) { if(this.props.name nextProps.name) { return; } this.getUserInfo(nextProps.name); } async getUserInfo(name) { const user await fetch(url, {name}); this.setState({user}); } render() { return div{this.state.user.name}/div }}可以看到我们获取用户信息的这个逻辑要实现需要在组件的各种地方写逻辑代码一多之后非常容易造成需要各种跳行来查看某个数据逻辑的流程。而通过自定义 Hooks 我们能够将实现这个业务逻辑的代码全部整合到一处最终达到业务逻辑的复用。function useUserInfo(name) { const [user, setUser] useState({}); useEffect(() { fetch(url, {name}).then(user setUser(user)); }, [name]); return user;}function User({name}) { const user useUserInfo(name); return div{user.name}/div}我们可以从下面的视频中一窥 Hooks 的魅力同颜色的表示是同一个业务逻辑最终同颜色的代码都被归置到一处实现了逻辑的解耦。via: https://twitter.com/prchdk/status/1056960391543062528使用 Hooks 改进那 Hooks 是否能应用于我们的业务场景中呢通过我们之前的分析我们知道实际上我们的目的就是为了抽离出广告数据获取以及广告的曝光和点击打点这两个通用的业务逻辑出来。所以 Hooks 针对逻辑的封装正好可以为我们所用。import {useState, useEffect, useRef} from react;import {useFetchMediav, useMediavEvent} from q/mediav;function App() { const [newsData, setNewsData] useState([]); const [adData] useFetchMediav({id: xxx, length: newsData.length / 2}); useEffect(() { const newsData [...]; setNewsData(newsData); }, []); const comps []; for(let i 0; i newsData.length; i) { comps.push(NewsCard {...newsData[i]} key{news-${i}} /); if(i % 2) { comps.push(AdCard data{adData[Math.floor(i/2)]} key{ad-${i}} /); } } return (div{comp}/div);}function AdCard({data}) { const ref useRef(null); const bind useMediavEvent(ref, data); return (div classNamegg ref{ref} {...bind}{data.title}/div);}使用useFetchMediav() 获取广告数据通过 props 传入到 组件中通过 useMediavEvent() 获取打点相关的方法并绑定到对应的元素上。使用 Hooks 修改之后的代码不仅复用性提高了整体代码的逻辑也变的更加可阅读起来。后记当然 Hooks 本身也不是没有缺点。为了在无状态的函数组件中创造去有状态的 Hooks势必是需要通过副作用将每个 Hooks 缓存在组件中的。而我们没有指定 id 之类的东西React 是如何区分每一个 Hooks 的呢答案就是通过调用顺序。内部通过数组(链表)根据调用顺序依次记录。为了遵守这个规则Hooks 要求我们不能在if等会动态执行的地方进行 Hooks 的定义因为这样有可能会导致 Hooks 执行顺序发生变化。其次useEffect() 合并了多个生命周期某些 Effect 需要在哪些生命周期执行以及如何控制其仅在这些生命周期执行这些都对开发者带来了更大的挑战。稍微处理不当的话很可能会造成页面的性能问题。参考资料React Today and Tomorrow and 90% Cleaner React With Hooks你想知道的React组件设计模式这里都有(上)你想知道的React组件设计模式这里都有(下)关注我们界世的你当不只做你的肩膀无 360官方技术公众号 技术干货|一手资讯|精彩活动空·