假如 Web 当初不支持动态化
楔子
Web 生而具有极其灵活的动态化基础能力,诸如:
-
动态插入
script
标签执行任意脚本逻辑 -
动态插入
style
标签引入任何 CSS 样式规则 -
通过
iframe
标签嵌入整站 -
以上标签均可直接加载网络资源
-
承载这些内容的 Web 页面部署在远程服务器,可随时动态更新,并且能立即生效
一直以来的探索和实践似乎只是在不断地发掘动态化能力的工程价值,为其寻找更合适的应用场景,比如早期的frameset
,如今的微前端/微应用
而移动端正好相反,生而具有许多灵活性限制:
-
原生不支持动态执行逻辑代码
-
构成移动应用程序的关键资源大都要打入安装包中(动态库例外)
-
应用程序安装在用户设备上,安装包更新需经应用商店审核,用户重新安装才能生效
移动业务的发展不断地对动态化能力提出更高的要求,但苦于缺少动态化的基础能力,所以一直在探索更灵活的技术方案,像早期的热修复/热更新,到如今的小程序
实际上,二者在动态化技术能力上所要解决的工程问题是一致的,比如动态加载依赖库、视图组件、甚至整个应用。所以不妨开个脑洞,假定 Web 不支持动态化,以 Native 的业务诉求来推演 Web 动态化技术的发展轨迹
伊始:原生 WebAssembly
0061 736d 0100 0000 0187 8080 8000 0160 027f 7f01 7f03 8280 8080 0001 0004 8480 8080 0001 7000 0005 8380 8080 0001 0001 0681 8080 8000 0007 9080 8080 0002 066d 656d 6f72 7902 0003 6763 6400 000a ab80 8080 0001 a580 8080 0001 017f 0240 2000 450d 0003 4020 0120 0022 026f 2100 2002 2101 2000 0d00 0b20 020f 0b20 010b
从前,Web 应用程序只能被打包成这种wasm的二进制格式,发布到各大浏览器应用商店。期间,不仅要等待数天的审核,通过之后还要等用户主动安装更新,等到新版本真正“生效”(覆盖大多数用户),可能已经是数月之后了
版本更迭慢,无论是战略性的重要功能还是十万火急的问题修复,都无法及时触达用户。即便线上着火了,最快速的救火方案也要几天甚至几周之后才能起到作用
为了能够更快地修复问题、降低风险,热修复方案的探索就此展开
浪花:为热修复引入脚本语言 JavaScript
热修复意味着要加载并运行(安装包之外的)逻辑代码,所以有人直接从 WebAssembly 模块加载机制入手,研究出了一些 Hook 方案,能够动态地换掉某些模块/文件
也有人沿着这个方向走得更远,权衡时效性、性能、兼容性与稳定性,通过编译插桩、工程配套设施、运行时框架等手段解决了模块依赖、版本管理、差量更新等问题,将应用程序的各个功能模块插件化
还有人另辟蹊径,引入轻量级的脚本语言运行时(如 JavaScript 引擎),并在浏览器原生 WebAssembly 与 JavaScript 世界之间架起一座桥梁,允许通过 JavaScript 调用原生的系统平台能力,从而扩展出了动态化的基础能力
动态化漾起了一道波纹,紧接着是呼啸而来的动态更新浪潮
海啸:基于 JavaScript 的动态更新
往动态化方向迈出第一步之后,离全面动态化的大好前景也就一步之遥了:
Any application that can be written in JavaScript, will eventually be written in JavaScript. —— Jeff Atwood
(摘自The Principle of Least Power)
全面动态化意味着要:
-
将应用程序中所有能够动态化的部分全都迁由 JavaScript 实现
-
将庞大的 JavaScript 代码按功能模块组织起来,并管理好功能模块之间的依赖关系
从而实现以功能模块为单位的快速迭代,相当于将热修复技术应用到问题修复之外的需求迭代上,既不用发版,免去了审核周期,也不需要等待用户主动安装,新功能得以动态发布并迅速覆盖到活跃用户
堤坝:容器概念形成
随着动态化程度的不断提升,JavaScript 在应用程序中的占比越来越高,最终仅剩余无法动态化(或没有必要动态化)的部分仍由 WebAssembly 实现,包括:
-
系统平台能力桥接
-
基础 UI 控件、交互能力
-
视图层框架(历史栈管理、生命周期支持等)
-
特定业务领域能力(例如多媒体内容生产、IM SDK 等)
-
通信机制(广播、状态共享等)
这些部分形成了容器(原生外壳),相当于运行在浏览器中的一个动态化运行时,在容器圈定的能力范围内,业务能够充分利用动态优势,实现快速修复、快速发布、快速触达、快速迭代
但随容器概念一同出现的,除了赋能业务跑得更快之外,还有动态业务与容器之间的依赖问题:
-
如何解除二者之间的强耦合,如路由、混合视图容器等场景?
-
如何识别出二者之间的依赖关系?
-
如何保障依赖关系是可控的,比如禁止将依赖新能力的动态业务发布到旧容器中?
通过工程配套设施将依赖管束起来之后,接下来的首要问题是想办法保证动态业务所依赖的底层容器的可靠性
边界:HTML、JavaScript、CSS 构成容器标准
隔离变化的惯用手段是加一层抽象,将变化的部分置于抽象层之下:
-
BOM API:对系统平台、视图层框架能力以及通信机制的抽象
-
Native Module API:对特定业务领域能力的抽象
-
DOM API:对基础视图渲染能力的抽象
-
JS API:对 JavaScript 运行时的抽象
-
CSS:对样式、布局能力的抽象
-
HTML:对基础 UI 控件、交互能力的抽象
抽象出的这些标准确立了稳固的容器边界,边界之内,动态业务能够肆意发挥,边界之下,容器同样能够不断精进、丰富容器能力,将边界拓宽。同时,具有标准定义的 API 能够以结构化的形式维护起来,对于开发体验大有裨益
云海:浏览器支持加载网络资源
另一方面,在标准化的过程中,一些动态化业务实践也沉淀到了容器之中,例如:
-
动态脚本:
script
支持加载网络资源 -
动态样式:
style
支持加载网络资源 -
动态路由:浏览器支持直接通过 URL 载入、或通过
iframe
嵌入网络应用程序
虽然从热修复开始就能够从CDN拉取 JS 文件,运行时动态解释执行了,但容器标准不仅对这种方式提供了便捷支持,还将动态化的基础能力从逻辑扩大到了视图、样式、静态资源等等
至此,动态化最关键的基础能力已经完备了。迁至 JavaScript 的功能模块甚至能够进一步部署到云端,实现离线集成、在线托管两种模式的灵活切换
一色:同步、异步模式切换自如
完备的动态化基础能力解锁了许多新玩法,例如:
将业务模块(bundle)进一步拆分成功能模块(chunk),并将非核心模块异步出去,实现动态按需加载,例如第三方 JS SDK、jQuery 插件、以及分享/评论/城市选择等重磅组件
对于内容呈现的偏静态场景,还可以通过 SSR 在服务端完成(大部分)页面渲染工作,加快首屏内容展现
另一方面,Hydration、lazy 组件、Suspense 等运行时特性使得在线的动态部分能够与离线的非动态部分充分融合,实现更细粒度的业务动态化,让在线托管真正成为一种部署选项
与此同时,动态业务自身的组件化程度也在不断加深,前端开发的核心工作从页面、模块开发转向了组件、编排逻辑开发
流云:数据驱动的前端应用程序
组件体系趋向成熟之后,一个由来已久的概念终于彻底浮出水面——数据驱动
从前后端分层的数据协议,逐渐演变成数据驱动,这里的数据包括 3 部分:
-
后端业务域数据
-
前端状态数据
-
(基于后端业务域数据的)前端衍生数据
将这些数据填入业务组件,即可渲染出完整的功能模块(无论是在客户端还是服务端),再将其放置到视图容器中合适的坑位里,就完成了一次组件级的“发布”过程
这种模式涉及 5 个重要环节:
-
业务数据(包括后端业务域数据和前端衍生数据)的生产
-
业务组件(包括前端状态数据)的生产和维护
-
组件的渲染(
业务数据 + 业务组件 = 功能模块
) -
坑位的生产
-
功能模块的投放
其中,业务组件、坑位是进一步动态化的关键,可分为 4 个阶段:
-
一个萝卜一个坑:静态业务组件 + 静态坑位
-
一个萝卜到处扔:静态业务组件 + 动态坑位
-
多个萝卜轮番扔:动态业务组件 + 静态坑位
-
多个萝卜到处扔:动态业务组件 + 动态坑位
要达到多个萝卜到处扔的组件级动态化终极目标,就要求能够动态发布业务组件、动态发布坑位
交融:动态业务组件 + 动态坑位
从端和云的视角来看,业务组件也可以看作数据(云)的一部分,相比之下坑位与端的关联更为紧密,而动态化的唯一手段就是将端侧的东西搬到云上去,所以要解决的关键问题是如何实现坑位的动态化
有 2 个思路:
-
干掉坑位的概念:将坑位的概念从组件级扩展到页面级,一个页面容器(一个 URL)即一个坑位
-
将坑位组件化:提供标准的坑位组件,就像
iframe
页面是一种天然的动态坑位,可打开一个新的页面容器加载任意 URL
对于除页面之外的其它布局容器,如对话框、消息条、Banner 位、腰封等等,可以将坑位标准化成容器组件,与业务组件一并动态发布,将坑位的租赁关系维护在服务端,作为数据驱动的数据之一
至此,前后端分层的界限几经重新定义,终于迎来了 JSP/PHP 融合数据与模板的黄金年代……