618主会场的探索与实践

业务背景

会场类页面是每年618大促中不可或缺的一部分,主会场是其中最重要的一环,有着巨大的访问量。主会场所包含的内容非常丰富,几乎涵盖大促期间所有热门活动、会场及店铺的入口,进行高价值优惠券的发放工作,页面的性能以及稳定性直接影响到用户的购物体验,同时还要求开发人员可以快速,安全的响应多变的线上需求变更。这样的页面工作量很大,对代码质量要求高,接来下我就介绍一下经过多次大促积累沉淀下来的主会场架构是如何应对的。

技术选型

我们先从主会场的业务需求入手,主会场的业务最主要的任务是分流,页面中聚合了大量的广告坑位,以引导用户进行跳转为主,并没有非常复杂的交互逻辑。

首先我们选用了react作为ui层的框架,react提供的组件化能力保证了我们代码的灵活性,同时能够有所积累,react丰富的生态环境也为我们提供了更多的选择。主会场的数据量庞大,有着较为复杂的数据结构,我们选择redux搭配react-redux作为我们的状态管理库。因为运营上的需要,经常要进行广告坑位的样式变更或者数据源变更,这就要求我们的逻辑与ui层竟可能的剥离,所以在redux中间件的选择上我们毫不犹豫的选择了redux-saga。主会场作为移动端的页面,复杂的浏览器环境也是需要关注的点,我们使用了postcss的autoprefixer为我们的css进行的编译,babel对js进行编译,保证页面的兼容性。

使用这套架构进行开发,代码结构接近于传统的MVC架构,非常清晰:

  • 模型(model) redux的store中的数据我们并不做定制化处理,所以并不是最近比较流行的view-model,数据可以被多个组件使用
  • 视图(view) 广告坑位为展示组件,楼层则为与store连接的容器组件
  • 控制器(controller) redux-saga和reducer定义了action如何改变store,我们将所有的业务逻辑都写在了saga中,generator函数也为异步操作提供简洁,优雅的解决方案

代码实践

1.开发依赖

除了react全家桶之外,我们JMFE积累了一套非常完善的生态圈,有着成熟的基础,业务,react组件库,帮助我们专注于需求中的业务代码开发,并且有成形的监控和灾备方案,帮助我们监控页面性能,快速定位线上问题,保证页面的性能与稳定。

jmfe生态图

jmfe生态中丰富的jnmp包为开发带来了非常大的帮助。@jmfe/jm-common 中集成了大量的常用的api,覆盖了大部分的开发场景,变量类型判断,页面环境识别,url参数获等等取面面俱到。@jmfe/jm-webview 为我们提供简洁跨平台的webview交互api,让页面与webview的交互更加便捷。@jmfe/jm-service 集成了多种网络请求方式,Post,Get,JSONP 并且统一使用 Promise 实现,让我们可以根据业务场景自由选择,并且实现请求方式的无缝切换。jdfme的react组件库实现了会场页面中常见的组件,可以快速地进行开发。使用 Webpack,并结合了 ESLint 和 Babel 等来进行开发和编译打包,保证了代码的稳定性与兼容性。

2.组件结构

基于整个页面的产品模型来看,页面主要特点有:

  • 以楼层为单位基本
  • 楼层间无耦合,经常会有换位需求
  • 楼层中包含多种广告坑位

从架构图中我们可以看到页面树状的组件架构,在使用redux时比较常用的就是使用react-redux直接将RootComponent与redux连接,将store中的数据通过props一级一级向下传递,这样自顶向下的数据流看起来非常清晰,但是在实际开发中却存在很多问题。RootComponent将会有一个巨大的mapStateToProps和mapDispatchToProps,RootComponent中要写很多props向下传递,中间组件也要传递自身不使用的props,造成不必要的组件刷新,修改底层文件的props时要涉及到不需要变更的中转组件,这样会导致在需求发生变化时我们要多整条链路上的组件进行修改,不必要的组件刷新也会导致页面的性能下降。

从业务场景出发,经过不断的探索与实践,我们在主会场中找到了最佳的应对方法。将每个楼层都作为容器组件进行开发,使用react-redux的connect函数对FloorComponent进行修饰,监听需要的数据(mapStateToProps)以及注册整个楼层所需的Dispatch(mapDispatchToProps),并且将AdvertComponent需要的state和dispatch通过props传递给对应的广告坑位组件,同时react-redux的connect函数实现了对被修饰组件shouldComponentUpdate生命周期的优化,避免了不必要的刷新,提升页面性能。

FloorComponent作为项目中最小且具有的健全业务功能的组件,我们可以随意地把这个组件放在任意组件中使用,不用关心他的数据及行为,非常灵活,甚至可以嵌套使用,整个项目都是以这样的组件构建而成,就像是一块一块积木搭建而成的。

我们基于此进行代码结构的设计与实现,使用自底向上的方式开发,首先为展示组件广告坑位,广告坑位仅仅负责根据数据进行样式渲染,data和dispatch都来自props,可以被用在多个楼层中:

import React, { Component } from 'react'
import Image from '@jmfe/jmr-image'

const Banner = ({goodThingClick, pictureUrl}) => <div className="mod-banner-good-thing full-img place-holder-advert" onClick={ goodThingClick }>
  <Image pictureUrl={ pictureUrl }/>
</div>
复制代码

楼层组件中包含了多个已经开发完成的广告坑位,为了达到楼层间的解耦,我们需要将楼层组件使用react-redux提供的connect函数进行修饰,从store中取出我们需要的data和dispatch通过props传入广告坑位组件中:

import React, { Component } from 'react'
import './index.scss'
import { connect } from 'react-redux'
import { formatUrl } from './../../lib/imgForm'
import Banner from './banner'
function mapStateToProps(state) {
	return {
    bannerGoodThing:state.bannerGoodThing, 
  } 
}

function mapDispatchToProps(dispatch) {
	return {
		goodThingClick: (item, groupId) => dispatch({ type: 'click:bannerGoodThing', item, groupId}),    
	}
}
@connect(mapStateToProps, mapDispatchToProps)
export class BannerGoodThing extends Component {
  render() {
    const { bannerGoodThing,goodThingClick } = this.props
    const { list }  = bannerGoodThing
    if(list&&list[0]){
      return (
        <div>
          <Banner 
            goodThingClick={ goodThingClick.bind(this,list[0],bannerGoodThing.groupId) }
            pictureUrl={formatUrl(list[0].pictureUrl)}
          />
        </div>
    )
    }else{
      return null
    }
  }
}

export default BannerGoodThing

复制代码

最后我们将开发好的楼层放置到页面的根组件中,楼层之间没有耦合,我们可以方便地对组件进行换位,也可以方便地使用往年积累的组件,非常灵活。

import React, { Component } from 'react'
import PartTitle from './../part-title'
import BannerGoodThing from './../banner-good-thing'
import './index.scss'

export class MainPushGame extends Component {
  render() {
    return (
      <div className="mod-main-push-game-food">
        <PartTitle imgHeigh={19} imgWidth={162}  titleImg={require('./title-game.png')} />
        <BannerGoodThing />
      </div>
    )
  }
}

export default MainPushGame

复制代码

我们所有的业务逻辑都放置到了redux-saga中完成,UI组件dispatch的Aciton可以被reducer直接处理作用于store,或者经由redux-saga进行相关业务逻辑处理之后作用于store,UI层组件的呈现仅取决于store中的状态,通过redux和react-redux作为桥梁和UI层组件进行交互,逻辑与UI组件之间也实现了解耦。

这样的组件结构保证了我们的开发人员可以快速的响应常见的需求变更,例如切换数据源时我们仅仅重写redux-saga中的小部分逻辑就可以实现,在对UI组件的修改也不会影响到逻辑部分,耦合度较小的组件结构也使得多人开发变得非常轻松,以楼层,或者UI/逻辑为单位对工作进行拆分都十分方便。

3性能优化

页面的性能也是个前端非常关注的点,在代码优化的基础上,页面的资源是页面首屏时间的重要影响因素,我们做了以下几项工作,收获了不错的首屏时间:

  • 打包时对js与css进行压缩,服务器端打开GZIP
  • 资源文件放置在cdn上并使用link标签的 dns-prefetch 进行预热
  • 图片的懒加载,根据网络情况的对图片质量进行动态调整,根据浏览器特性进行webp优化

虽然页面的图片非常多,但是最终将页面首屏的资源总量下降到了900KB。

wifi情况下在chrome中首屏的时间为800ms,200ms就已经刷出背景色,效果非常棒。

4静态兜底

大促期间页面的灾备工作也非常重要,我们设想一个最极端的情况,所有的后台服务都挂掉了怎么办?我们还能不能让页面进行正常的展示?jmfe的生态中刚好有一套完善的页面静态兜底方案,可以让页面在后台服务全都挂掉的情况下依然正常展示页面。

我们在页面开发完成之后,进行接口的静态数据备份工作,使用接口注册模块注册接口的备份信息,在静态数据同步系统中就会在数据备份队列中加入这些接口的数据备份任务,并将备份信息返回给前端开发,我们只需要读取返回的配置文件就可以访问到在CDN服务器上的静态数据。由于我们所有的api请求都使用Promise作为返回,仅仅是在进行api请求时的配置有所不同,所以静态版本的页面和常规版本的页面可以使用同一套代码,我们通过webpack的插件DefinePlugin注入不同的全局变量,在编译打包的时候生成两个页面,不会增加额外的工作量。

我们在接口异常或者超时的情况下都会将页面重定向到静态页面中,静态页面的所有资源都在CDN上,所以即便是我们的后台服务都挂掉了依然可以保证页面的正常展示。

未来规划

经历了多次大促的洗礼,目前的主会场技术架构已经非常成熟,大促期间页面的性能,稳定性都非常值得信赖,同时可以让开发人员快速地响应需求变更,时间吃紧时也可以较为方便地增加人员进行支持,但是我们还可以做得更好,依然有很大的提升空间:

  • 更加优雅的埋点上报方式
  • 对页面的特殊场景进行抽象,开发可复用的组件
  • 会场项目的可配置快速生成工具,进一步减少开发人员的工作量,提升效率
  • 结合JDReact/taro实现主会场多端复用,提升用户体验

猜你喜欢

转载自juejin.im/post/5b5db5365188251ace75d4e6
今日推荐