端动态化方案详细设计

前言

背景什么的就不说了,你们都懂!不懂的请百度!既然看到了这篇文章,说明你仍是对动态化有本身的诉求哒,那么但愿文章中的内容能够帮到你。javascript


技术选型

技术选型永远是项目肯定以后遇到的第一个难题,市面上能够解决项目问题的选型有不少,究竟是时髦驱动开发仍是热闹驱动开发嘞?其实你们在选型过程当中最应该关心的不是技术,而是项目。由于技术应该为项目服务,而不是项目为技术服务,分清楚权重以后,就很清晰了。接下来就是从项目入手,从项目入手须要从三个因素考虑:css

  1. 项目因素
  2. 团队因素
  3. 技术因素

项目因素

项目在不一样的阶段须要考虑的状况是彻底不同的。 html

Alt text

好比项目刚启动或者在基础功能铺设的阶段,那么关心就应该是快速试错需求快速更新迭代需求变动紧急运营活动频繁、其余的非功能性需求。在项目的扩张期也就是中期,可能会经历一次重构,将前期各类临时的解决方案统一进行升级和整改,以此增长系统的稳定性而且能够足够应对项目对内的产品类目、功能需求以及对外的市场和产品扩张。在项目的稳按期大多数的项目架构都已经定型,可是并非那么的"尽善尽美",会有不少历史遗留问题让人头疼,因此这个时候更须要有一个技术视野和能力很强的人来带领团队把项目的技术深度和高度达到一个更高的高度,固然,这个过程当中是十分考验开发人员的能力的。前端

棋局里有一句善弈者通盘无妙手,好的架构都是润物细无声的,任何需求和功能的变化均可以让开发人员十分便捷的完成,拥有足够的灵活性和反脆弱性,固然这里就不作过多讨论了。java

团队因素

选型是针对团队的考虑比重也是十分重要的,由于项目不是一我的在作,而是一群人。当你选定了某一项技术以后团队里确定存在对这个技术不熟悉的人。因此你须要考虑到团队成员的学习成本,另外在团队招纳新人的时候也会把该技术的要求添加到新人的技能列表里,若是你选的技术大部分人都不会甚至不知道那就很尴尬了,项目就会越作越死。ios

技术因素

通过前两个因素的综合考虑以后,就该考虑技术因素了。选定的技术方案或者解决方案技术程度度怎么样,是否是已经达到了stable的状态。web

  1. 文档和示例是否齐全?
  2. 遇到问题以后是否能够获得技术维护人员的第一时间解答?
  3. 遇到bug如何修复?

技术方案的稳定性怎么样,须要多少人力支持所谓的稳定性这也是须要考虑的地方。另外就是扩展性了,固然这个是相对于需求和功能的扩展来讲的。npm

最后,把备选的多个技术方案进行三个方面的多维度对比,就会获得一个比较满意的方案了。json


动态化的选型

咱们在评审前,找了三端(FE、IOS、安卓)的高工一块儿讨论了选型的问题,通过综合考虑,咱们选择了Weex和Hybrid两种方案。具体细节包括但不局限于技术选型适用场景功能边界切入点交互协议等方面,在这里不作赘述。小程序

选定方案以后,咱们从上述的三种因素基于团队当前的项目阶段进行综合对比,具体以下图。

Alt text

至于为何没有选Weex,缘由是咱们FE团队里的人都不太了解Weex(大可能是新人),并且深刻学习的成本太大(不要告诉我肯定项目彻底基于某个技术方案开发以后不须要深刻学习和掌握,那你不太适合这篇文章)。遇到阻塞性问题怎么解决咱们也不是颇有把握,毕竟咱们不是阿里系的。而使用Hybrid的话,这些问题就不须要考虑了。

你们都知道,javascript是单线程的,即使js引擎底层引入了非阻塞(non-blocking)的机制,也改变不了运行逻辑较多时页面卡顿的问题(webWorker不在讨论范围内)。因此高级点的Hybrid方案使用了多线程以此拆分前端的逻辑和视图。

关于异步非阻塞的区别请参考 asynchronous-vs-non-blocking

使用RAIL模型评估性能

RAIL 是一种以用户为中心的性能模型。每一个网络应用均具备与其生命周期有关的四个不一样方面,且这些方面以不一样的方式影响着性能:

Alt text

TL;DR

  • 以用户为中心;最终目标不是让您的网站在任何特定设备上都能运行很快,而是使用户满意。
  • 当即响应用户;在 100 毫秒之内确认用户输入。
  • 设置动画或滚动时,在 10 毫秒之内生成帧。
  • 最大程度增长主线程的空闲时间。
  • 持续吸引用户;在 1000 毫秒之内呈现交互内容。

最后基于上述的RAIL模型,咱们获得告终论:Hybrid并无比Weex的体验差不少,在可控范围内。好比小程序的体验效果

行业契合度

选择Hybrid以后,咱们有对比了行业的契合度。由于咱们的项目是一个相似与电商的项目,就是买东西的。因此仍是蛮符合的。

Alt text


适用场景

接下来介绍下Hybrid方案在项目功能内的使用场景,目前咱们的项目因为是处于初期阶段,因此功能较少,主要有如下四类:

Alt text

图中依次是:首页、二级页、详情页、单品详情页。依据于咱们的场景,除了我的中心、订单列表、收银台以外,Hybrid何以适用于项目的其余任何场景。

切入点

由于项目刚开始到目前为止,咱们三个端都是各自实现业务逻辑,因此在实现动态化方案的过程当中,不能一刀切。时间、人力、项目各类因素也不容许咱们这么作。所以咱们选择了一个切入点,循循渐进得完成咱们需求,就像是给一辆高速驾驶的汽车更换地盘同样。

项目肯定以后,咱们优先考虑了单品详情页做为咱们的技术切入点。具体缘由以下:

  1. 展现内容居多,没有复杂交互
  2. 频繁变化,每一个单品都有不一样的详情内容
  3. 交互简单,适合循循渐进定制Hybrid的各类协议和逻辑

后面咱们依次的迁移顺序为:单品详情页 -> 详情页 -> 二级页 -> 首页。


总体架构

上面聊了那么多,没多少技术的干货,如今开始介绍下总体架构的设计。在需求实现的过程当中咱们常常会发生统一套页面需求在WAP和Native端上都须要实现,为了解决这种需求带来的重复工做量的问题,咱们在设计时加入对宿主环境兼容的考虑。

Alt text

主要分为三层:

  1. 视图层
  2. 容器
  3. native / OS

视图层主要负责视图的展示,包括H5的页面和模板、业务的框架实现还有内嵌在视图层的bridge,若是视图是在APP的webView中,那么也会包含Native Activities控件。至于原生的Native Activities如何设计,后面会讲到。

容器就是视图层的执行环境,多是移动端浏览器,也多是App的webView。浏览器的话这里暂且不提,webView的话会提供一个Bridge Provider用来将端封装好的能力输出给视图层,通常使用API注入和Schema的方式实现。里面封装的都是Native级别的业务API和硬件设备的API。

最下面的就是Native的OS层,主要提供一切必要的基础能力,因为我对Native了解的并不深刻,因此这里暂不讨论。

因为视图层和容器的层隔离,让视图层不须要关心容器的实现,可是它们之间的bridge却必须得关心这个。以致于bridge如何兼容不一样的容器(wap浏览器、Native App),这是个值得深刻考虑的问题。

这是一个简单的分层架构。其中每一层都有着特定的角色和职能。架构里的层次是具体工做的高度抽象,它们都是为了实现某种特定的业务请求而存在的。还有一个突出特性是关注点分离,每层都只会处理本层的逻辑。从另外一方面说,分层隔离使得层与层之间都是相互独立的。架构中的每一层都必须符合最少知识原则,正由于这种高度独立,才使得咱们能够很好的兼容WAP浏览器和Native APP。


视图层设计

UI的本质是什么?是将从服务器获取数据状态(state),通过必定的操做使之展现出来。咱们能够用一个数学表达式来表现它们的关系:UI = f(state)。state是经过bridge或异步化接口获取的数据,UI就是用户看到的界面,对于Hybrid模式的来讲,真正关心的就是f这个函数到底如何实现。

咱们能够简单的把f往大了想,把它理解成为一个web容器,也就是Web Container。至于Container里怎么作?请看下图:

Alt text

前文咱们说过了,Hybrid中能够将视图层中的视图(View)与逻辑(Service)分开达到体验提高的目的。在Native App中通常是将一个页面拆成这两部分放在两个不一样WebView中,一个WebView放View部分,一个WebView放Service部分*(也就是说,每一个页面都须要2个WebView)*。

他们之间通过各自的Bridge对即将发送或刚接收到的数据进行包装,而后再通过封装在bridge中的tunnel进行数据交互,完成后续操做。不过,在wap浏览器中彻底不须要考虑这些,该怎么作就这么作。可是也会出现一个兼容问题,视图层和容器之间经过bridge交互,也就是说在wap浏览器(wap浏览器也是容器)中也须要存在bridge,不过这个bridge提供的是浏览器的能力。

bridge中存在一个叫作tunnel的东西,主要负责传递Service和View之间的数据和事件。在不一样的宿主环境中,tunnel的组成也不一样,在Native App中,tunnel是一个IPC的实现。在浏览器中,tunnel是一个发布订阅事件机制的实现。

在Service中会遇到数据本地存储的问题。数据的存储和获取统一经过bridge将操做内容发送给Native,而后Native根据不一样的操做内容进行处理,完毕以后再经过处理完毕以后的数据发送给Bridge,进而bridge再行通知Service。

data的传递

Service包含视图层中除了视图渲染以外的其余任何逻辑。它把获取到的state通过framework的API处理以后会生成一个视图元信息(View Metadata),视图元信息是对将要渲染视图的简单描述,经过它咱们能够预想到视图长什么样子。以后framework会把视图元信息经过bridge中的tunnel发送给View。

注意: tunnel发送数据的过程是异步的。好比小程序中的setData()方法。

View只包含视图层中的页面渲染。渲染对象主要包括两个:html以及须要展现的Native Activities控件。当Service发送过来的视图元信息中包含Native级别控件时,bridge会把该部分的视图元数据发送给Native。Native收到以后,就会根据元数据在视图层的WebView上展现原生控件*(注意:原生组件是Cover在WebView上的)*。当Service发送过来的数据为html的视图元数据时,会先根据视图元数据进行DOM Diff,而后根据生成的Patch对象来进行页面的渲染。

event的传递

View渲染完成以后,就会等待用户操做。View会将用户操做的事件区别对待:html的事件和Native控件事件。先说Native的事件,Native的事件WebView把控不了,须要Native在封装业务原生控件时多作注意,对控件可能遇到的事件作统一梳理。原生控件会经过Native框架把事件源和事件参数进行序列化,而后Native框架再将序列化后的事件数据经过bridge发送给Service。

若是是html的事件,View这边中bridge会经过js获取事件源和事件参数,而后统一进行序列化。而后在经过tunnel将序列化后的事件数据发送给Service。

数据的请求

Web Container中请求的数据主要分为两类:

  • 静态资源请求
  • 业务数据请求(异步化接口)

静态资源请求会直接经过webView对外发起请求,这里不作赘述。

除了静态资源请求以外的异步化接口请求咱们会经过Native进行代理,让Native帮咱们发送请求,而不是使用XMLHttpRequest对象进行请求。


Bridge设计

bridge层位于视图层和Native之间,负责连接双方,一个好的bridge设计,可让咱们在开发的过程当中事半功倍。

咱们对Bridge的关注点:

  1. 位于 js执行环境宿主环境之间,负责连接双方
  2. 兼容宿主环境*(wap、app)*差别性
  3. 适配不一样业务线提供的桥连*(注入API、schema协议)*能力
  4. 根据业务线单独配置桥连能力
  5. 编译阶段 解决宿主环境兼容能力

js执行环境多是wap浏览器,也多是Native中的WebView。宿主环境也多是浏览器和Native App。对接的业务方提供的桥连能力各不相同,统一套方案须要对接至少三种不一样的功能需求平台。而这些问题就是须要在Bridge中解决的。

上一节提到过『除了静态资源请求以外的异步化接口请求咱们会经过Native进行代理,让Native帮咱们发送请求』。至于为何要这样作主要缘由为:

  1. 接口鉴权问题
  2. 对数据进行更新粒度的控制

先说第一个鉴权问题,常规的作法是App用户登陆后,将用户的认证标识存在在webView的cookies中,而后WebView里的业务代码发送AJAX请求时就会将cookies携带到服务器完成用户鉴权。这种状况下若是服务器端校验用户token失败的话是没法第一时间让APP跳转到登陆窗口的。另外在WebView中发送了一个退出登陆的异步接口请求,这时APP也须要同步退出登陆。很显然,最好的办法就是让APP帮咱们代为发送异步化接口请求。这样咱们还能够利用上APP的持久化缓存能力来存储接口数据。

总体架构流程

Alt text

宿主环境差别性

在浏览器和Native APP的差别性方面,咱们总结了如下5点:

  1. 视图控件
  2. 数据存储
  3. 异步化接口请求
  4. 页面路由
  5. 页面历史管理

咱们会在有差别的功能上封装统一的API,以此减小FE开发人员在开发过程当中的兼容问题。

这里仅以异步化接口请求举例,咱们封装一个统一request方法。开发人员不须要关心本身写的代码将要在哪一个平台上运行。借助WebPack和Rollup等工具的tree shaking功能,咱们能够很好的完成差别化编译。

// tools.js
import Axios from 'Axios';
import bridgeRequest from '@/bridge/request.js';

export default {
	request: process.env.TARGET === 'app' ? bridgeRequest : Axios
}

// main.js
import {request} from 'tools';

request.get('http://www.test.com/test', {a: 1}).then(data => {
	console.log('this is test data -> ', data);
});
复制代码

在编译时咱们只须要指定target就能够作差别化编译了:

# 编译为app版本
$ npm run build --TARGET=app
# 编译为wap浏览器版本
$ npm run build --TARGET=browser
复制代码

桥连能力注入

咱们定制一个Bridge的标准接口,用来规范各类操做,好比Native的调起弹出层控件。业务方根据本身往WebView注入的API或schema协议,填写一个配置Json文件,而后注入到bridge中,该文件中声明了alert操做要访问的协议或方法以及参数名称。这样bridge在调用alert方法的时候就会根据json完成指定操做。

业务方只须要根据Bridge定义好的标准接口,注入本身的schema协议便可。

// system.schema.json
export default {
	alert: {
		schema: 'xxxx',
		params: {}
	},
	request: {
		schema: 'xxxx',
		params: {}
	}
}

// interactive,js
import schema from '@/schemas/system.schema.json';
// 注入业务方本身的alert schema
interactive.injectSchema(schema);
export default {
	alert(options) {
		return interactive.api.alert(options)
	}
}

// main.js
import {bridge} from '@/bridge/index.js';
import {alert} from '@/bridge/interactive.js';

// view层准备完毕
bridge.on('ready', () => {
	alert('这是一个alert!').then(data => {
		console.log(data.state ? '肯定' : '取消');
	}).catch(e => {
		console.log('调起alert失败');
	});
})
复制代码

Native层设计

因为我自己不是Native的开发人员因此这里就列一张Native的架构图,具体的大家本身看吧。

Alt text

注意: 这张图是我这个FE画的,被安卓的大佬吐槽说画的结构不清晰。大家将就着看吧!

到这里,咱们就把架构里最主要的三层:视图层、Bridge和Native层介绍完了。下面开始介绍功能设计,主要包括三个方面:原生组件交互、路由系统(统跳协议)、资源包的缓存与更新。


原生组件交互

原生组件与webView中用javascript实现的组件是不同的。它们是由Native直接在WebView之上渲染的原生控件,没法受到javascript影响,只会受到Native的控制和影响。对于WebView中的javascirpt代码来讲就是:超乎三界以外,不在五行之中

为何不能够所有使用WebView中的js组件哪?那就是WebView中前端组件的影响面太小,就跟唐朝末年的朝廷同样,政令不出长安城。好比Alert提示在显示状态下,不能够作其余交互操做,只能点击Alert的肯定和取消按钮。还有Header上左侧按钮的后退以及点击右侧Icon返回APP首页的操做等,这样的例子还能够往下举不少。因此遇到这种状况,就必须请原生的Native控件出马控场了。

咱们这里梳理了一下能够用到的Native级别控件:

  • Header
  • Footer TabBar
  • Alert Tip Confirm
  • Dialog
  • SelectBar

Alt text

『部分』原生组件的加载时机

那些老是须要在视图里第一时间展现(Header、TabBar等)的原生组件必须区别对待。不能在WebView加载完以后再去渲染那些原生控件,由于这样会出现因需渲染原生控件而对WebView从新计算大小致使Service中数据错误以及页面闪烁的问题,从而影响用户体验。

最好的方法就是把这类原生组件的视图元数据单独放在一个控制版本管理的json文件(下文有写到)中,而不是放在包含bundle内容的zip包中。这样Native就能够根据json文件中的视图元信息提早渲染好原生控件,而后加载WebView并执行javascript代码。


路由系统

在设计整个路由系统以前咱们有个前提条件,那就是每一个视图页面都是独立的一个WebView(其实包含两个,一个存放View逻辑,一个存放Service逻辑),而不是在同一个WebView中加载渲染多个页面。由于只有这样才能够完美的模拟原生应用的页面跳转的各类操做。这个必定要注意,若是你不注意你就不会理解下文到底在说什么!

咱们遇到的场景有如下几种:

跳转场景:

  1. Native to Native
  2. Native to WebView
  3. WebView to Native
  4. WebView to WebView

加载场景:

  1. 同页面加载(重定向)
  2. 跨页面加载

存在的问题:

  • 同时存在的WebView最大数量
  • 视图之间的参数传递
  • 视图历史栈管理

最后咱们商定的WebView能够同时存在的数量为9个,和微信小程序同样。当页面栈已经达到9个的时再打开新页面就会没法打开新页面。页面之间的参数传递统一使用querystring格式。历史栈的管理由Native统一实现。

历史栈的管理

咱们维护一个历史栈的目的就是让Native中的视图能够像浏览器的历史同样,进行前进和后退。惟一的不一样是,浏览器的历史存的是URL字符串,而咱们的历史栈存的是视图对象。每次Native APP打开都会从新从头记录,只会记录APP运行期间的历史,APP关闭后历史栈清空。

Alt text

逐级访问

正常的操做路径访问,会将每一级的视图存放在历史栈中。最多存入9级,超过9级则没法加载新页面。

重复打开页面

当最新的单品页新打开一个二级页时,即使这个二级页已经打开过,历史管理器也会在栈的顶部新打开一个二级页。注意,两个二级页是彻底独立的。不存在视图提高。

重定向

在最新的单品页重定向为二级页时,和上面的重复打开页面状况相似,都是将当前页重定向为二级页并渲染。注意,这两个二级页是彻底独立的。不存在视图提高。

后退

当点击Header左侧的后退按钮(一级一级后退)或者经过Hybrid Router API(能够多级后退)进行后退操做时,就是消费当前的历史管理栈。


资源包的缓存与更新

当全部的步骤都已经就绪以后,就该到这一步了,bundle资源的缓存和更新。这里咱们引入的分包加载的机制,并且分包的级别是以页面为纬度的,而不是功能。其实就是小程序的那套分包加载机制。

为了实现这套机制,咱们抛弃了WebView的缓存,和Native同窗一块儿开发并创建起了这套缓存机制。而且只缓存bundle资源(一个一个的zip包)。咱们规定每一个业务只有一个入口zip包,全部的子包zip都必须依赖入口zip包中的subConf.json进行更新和加载。

Alt text

只有在Native每次更新入口包是才会显示loading,除此以外都会显示入口包中携带的骨架图html。

Alt text

APP每次打开的时候都是先去服务器获取conf.json,conf.json中内容以下:

{
	"version": "v1.0.1",
	// 此内容仅为示例
	"skeletonURL": "https://pan.baidu.com/nt-static/hybrid/app.skeleton_v1.0.1.d3a938346f1ab825.html",
	// 须要下载的入口zip包 命名方式 {version}.{md5}.zip
	"zip": "https://issue.pcs.baidu.com/packages/bybrid/v1.0.1.d3a938346f1ab825.zip",
	// zip包的md5,根据此md5判断该zip包是否须要更新
	"md5": "d3a938346f1ab825",
	// 签名校验
	"signature": "342876ba19d34aba92f7536e42992a45",
	// 须要提早渲染的原生控件
	"header": {
		"title": "this is a title"
	},
	"tabBar": {
		{
			"text": "log",
			"icon": ""
		},
		{
			"text": "home",
			"icon": ""
		}
	}
}
复制代码

入口zip包解压完毕以后的目录结构为:

$ tree ./v1.0.1.d3a938346f1ab825 ./v1.0.1.d3a938346f1ab825 ├── app.bundle.css //样式文件 ├── app.bundle.js // js逻辑文件 ├── app.index.html // 入口html ├── app.skeleton_v1.0.1.d3a938346f1ab825.html // 骨架图,和conf.json中的skeletonURL一致 └── subConf.json // 子包的加载及校验配置 复制代码

subConf.json中的内容:

{
	// 和conf.json一致
	"signature": "342876ba19d34aba92f7536e42992a45",
	// 子包入口
	"subRoutes": [
		{
			// 入口的路由
			"routes": ["/go/to/path/1", "/go/to/path/2"],
			// 骨架图URL
			"skeletonURL": {
				"/go/to/path/1": "https://pan.baidu.com/nt-static/hybrid/app.skeleton_v1.0.1.bfa31a2ae5f55a7f.html"
			},
			"zip": "https://issue.pcs.baidu.com/packages/bybrid/v1.0.1.bfa31a2ae5f55a7f.zip"
			"md5": "bfa31a2ae5f55a7f"
		},
		{
			"routes": ["/go/to/path/3"],
			"skeletonURL": {}
			"zip": "https://issue.pcs.baidu.com/packages/bybrid/v1.0.1.7af5f492a74499e7.zip",
			"md5": "7af5f492a74499e7"
		}
	]
}
复制代码

最后

本文主要介绍了Hybrid的总体架构的三层和功能设计的三个点,基本涵盖了端动态化方向的所有要点。但愿本文能够帮到你。

相关文章
相关标签/搜索