基于逻辑复用的联合跨端思路与实践

简介: 跨端新思路助你业务研发事半功倍。
作者 | 诚文

image.png
聊到跨端,我们都会想到 Flutter、Taro、RN 等等,这些框架各自的优势、适用场景和实操就不赘述了。如果了解过的同学,会发现几个共同点:

1、主要聚焦在无线端的实现;
2、都使用一套 UI 层 DSL 语法的方式来进行跨端运行;
3、多少存在一些降级处理方案或兼容问题,最终落地到多端上运行常常不是最佳实践;
4、不同端共用一个工程,通过不同构建方式得到多端资源;
5、跨端方案生态自成闭环,和原端的生态独立;

所以目前提到跨端大多涉及的是无线端浏览器、小程序或APP内场景,都是以一套无线端 UI 层 DSL 向上构建业务应用。而实际项目中,我们很多业务模块都含有 PC 和无线端 (无线 Web + APP + 小程序)的场景,例如平台类型需要长期维护迭代的项目、或涉及业务核心链路的项目,业务层厚、逻辑复杂,需求迭代频繁,代码历史渊源较深,无法用搭建来承载,不得不分开独立维护,典型的比如 PC 浏览器、无线浏览器和 APP 内 Webview。而类似早期 Bootstrap 等自适应UI框架的方式又往往满足不了实际的业务场景、节奏以及高性能要求。

对于这类情况,想继续实现开发提效,业务逻辑复用、多端 UI 层适配就成为了一种可选的方案。今天这里来跟大家分享一些基于业务逻辑复用的联合跨端方案的思考与实践。

什么是业务逻辑复用的联合跨端?

针对我们目前 PC 浏览器、无线浏览器、APP 内 Webview 的多种场景,我们希望达到的最终目标是,所有端共用一个工程(包括 PC 端、M 站、App 和小程序),让业务逻辑层共用,UI 层自由选择,又能分端构建出接近原端的代码,各端逻辑、UI 解耦合可插拔管理。因为是通过自由组合的方式来联合多端的能力,我们目前称之为联合跨端方式。

怎么理解呢?比如现在有个业务模块存在多端页面,PC 和无线 Web 上逻辑基本一致,但 PC 和无线 Web 样式完全不同,一般是两个应用,技术栈也不一样,PC 用的 React + Fusion (一种 React UI 库),无线用的 Rax + Fusion Mobile (一种移动端 UI 库,两者大体上可以理解为 antd 和 antd mobile 的区别),我们希望能合到一个工程里面,一次开发,同时构建 PC 和无线两份资源发布,各端还是按照原来的技术体系运行。一种思路是响应式 UI 的自适应,但往往同一个 UI 适配带来后期的迭代维护成本往往更高,且实际的多端业务场景、节奏以及高性能要求也难达到要求。

我们先简单看下现有的跨端框架是怎样运行的。从业务工程的角度上,跨端框架在具体项目落地一般是这样的分层架构:
image.png
跨端项目工程通过统一的 UI DSL 向上构建业务 UI,然后绑定数据模型和业务逻辑,当业务逻辑涉及到多端区分时,一般通过 if-else 的方式进行分支处理。然后多个逻辑业务组件相互编排组合被入口文件引用,最终通过跨端构建器打包,编译出不同端运行的代码资源发布上线。

但随着项目的迭代,一些问题仍无法有效解决:

1、现有跨端 UI DSL 大多是基于现有某个规范的删减版或非原生端内的 DSL 规范,往往使用起来有诸多限制。开发过程中很多问题不可预期,不符合开发者通用认知;
2、打包资源可能含有多端冗余代码;业务逻辑层多端差异逻辑通过 if-else 一起打包,特定端运行时存在死代码,要移除得定制,对一线开发者要求提高。
3、UI DSL 一旦使用后面基本很难改变;而这时混合逻辑层已经耦合了多端不同分支处理代码,要改变历史包袱极其沉重。历史证明事实也常常如此。
4、目前跨端 UI DSL 主要局限在无线端;
5、跨端通用 Libs 和 DSL 维护成本极高;脱离原端技术栈后,基本都要重新实现或做抹平,而各端Universal Api抹平的成本极高,实际业务迭代中不断发现需要补充的差异点更是无穷无尽。

如何设计实现联合的跨端方案?

那么联合跨端方案怎样去做呢?我们在原有的业务项目分层架构中做一些改进。如下图,在主要几个需要区分端实现的关键节点上暴露适配层 (适配层和设计模式一样其实也是解决一层 if-else 的问题),然后由适配层来区分端的差异性,这里可包括 UI 差异性、业务逻辑差异性、和底层 Libs 差异性等等。然后由构建器去识别适配层关键参数做不同端的资源抽取打包。
image.png
**这样我们将原有不同端跨端处理的适配层暴露了出来,以前 Libs 适配层和 UI 适配层都是跨端框架封装实现的,而逻辑适配层跨端框架没有做,才会有我们后来广泛使用的多端 if-else 来区分端实现的混合业务逻辑。
**
根据这一思路,我们要解决的问题和过去有很大不同,总的来讲只是暴露合理的适配层让用户自己实现:

1、暴露适配层,交予开发者更灵活地控制,并让业务可以实现逻辑适配层。
2、涵盖 PC 端,不再设计固定的 UI DSL 层。因为涉及 PC 和无线,表现层完全不一样,后期维护工作无穷无尽,而目前独立端上的UI库已经做的这么完善了,为什么不直接用。
3、可以兼容到现有的跨端方案。现有跨端方案解决了无线端跨端问题,我们无需重复设计,其跨端 DSL 依然可以作为一种UI方式被适配层引入使用。
4、从业务逻辑层向 UI 层构建应用。可以看到,适配层可以将业务逻辑和基础库、UI 层做隔离约束,让我们更加专注业务逻辑开发维护,即领域模型的开发设计,多端 UI 形式只做为领域模型的展示层,且多端的 UI 是可插拔的。
5、不对现有框架语法造成限制或冲击。不对现有的技术体系做具体实现,比如 Libs 通用库等,我们依然可以按现有方式适配到特定端使用。

综合几点,我们的方案其实是:设计和暴露分端适配器与对应的构建器。增加适配层后,跨端能力变得可扩展了,当然实际上它是以适配的方式联合不同端的能力,所以我们把这种跨端设计称为联合跨端方案。联合跨端方案和现有的跨端方案是不冲突的,现有跨端方案完全可以作为一种UI层集成到联合跨端工程当中,这个上面也解释了。

如何做到最简实现?

此外,另一点要做的是,我们希望能把差异化管理当成一种普通的 API 来使用,不改变原有任何技术习惯,而不是动则搞个大而全的架构体系闭环来实现。让开发者5分钟内完全掌握。

要设计一个通用的适配器看起来很容易,我们开始想得也很多,底层 UI 的适配、网络库的适配、数据流工具适配、业务逻辑适配... 然后做统一的 Universal 库、Universal UI、Universal Logic 都有了,还可以让 Universal 单元之间自由编排组合等等。但是历史事实证明往往做的越多越全,后期维护的代价越大,越难继续下去。

为了尽可能降低使用成本和对现有技术的入侵,针对目前的所有场景分析,适配层 API 设计最终只保留了两种类型:业务代码逻辑的分端业务文件引入的分端,也就是动态区分代码逻辑和动态区分文件模块,其它的交给业务开发去灵活地实现。对此我们仅添加了 { moduels, useModules } 和 { imports, useImport } 两组可调用的 API。然后发现这两组抽象出的 API 实际是可以灵活的去覆盖所有适配场景的。

其中 { moduels, useModules } 用于解决对外提供同名接口或函数,在不同端实现不同的逻辑;{ imports, useImport } 则用于需要针对不同端引入不同外部模块文件的场景。

看下如何5分钟完全掌握。

useModule ( fnMap,platforms = [] ) : 关联 modules 对象。fnMap 中的方法被注册到 modules 中,并匹配对应的platforms生效,平台不匹配的代码构建时被移除。其中 fnMap 支持对象和函数 return 值的两种写法:

import { useModule, modules } from '@ali/union/src/xframe';

// 直接对象的写法
useModule({
    alert: (e) => {
        e.preventDefault();
        alert('这是一个无线H5环境的弹框');
    }
}, ['m']);

// 函数返回对象的写法
useModule(() => {
  return {
     alert: (e) => {
        e.preventDefault();
        alert('这是一个PC环境的弹框');
    }
  }
}, ['pc']);

modules.alert();

useImport ( importMap,platforms = [] ) : 关联 imports 对象,根据不同的平台,引入不同的文件模块来区分编译使用,platforms 表示 importMap 支持的哪些平台下会被引入。例如我们希望根据平台引入不同的React UI 组件:

import React from 'react';
import { imports, useImport } from '@ali/union/src/xframe';

useImport({
    View: './index/m'
}, ['m']);

useImport({
    View: './index/pc'
}, ['pc']);

export default imports.View;

那么编译后 PC 的代码最终等价于:

import React from 'react';
import View from './index/pc';

export default View;

无线 H5 端的代码会最终等价于:

import React from 'react';
import View from './index/m';

export default View;

useImport 主要用来解决不同平台技术栈差异性的问题,比如引入不同的文件模块执行等。这里相比于Dymanic Import 是有区别的,我们依然希望打包后的文件是整体的,Dynamic Import 则会分割文件,而且是运行时加载,分端差异性越多时,分割的碎片文件也可能越多。这些都不符合我们的预期。

介绍下实现原理,其实也比较简单。编译构建每次会传入一个平台或场景参数,然后在源代码 AST 处理时,根据 API 后面的参数,将不满足参数的其它节点移除掉。如果是 useImport,移除后,还要将注入的文件模块列表生成 import 节点注入到 AST 中,再进行后续的构建操作。

image.png
image.png
所以对比 UI 层 DSL 的跨端方式,联合的方式有很大的区别:
image.png
联合跨端方式并不是一种具体的跨端 DSL 实现,而是一套项目复杂业务逻辑和UI层的管理理念,它不参与任何端特性的判断与 UI 层实现,而是对现有跨端方式另一个方向的完善补充。此外对于同端上的同个业务模块的多场景差异化实现,这种思路也非常适合引入进行轻松地管理。

联合方式将差异化逻辑通过适配层暴露管理,用统一的方式来构建出多个端或多种场景下不同的产物。本身实现非常简单,但是要真正结合业务项目分层,做好复杂业务代码的管理却并不容易,现有的 UI DSL 跨端方式在复杂业务场景的逻辑管理上没有约束,而联合方式更致力于去解决这些业务逻辑和 UI 上的管理问题,同时联合不同能力做到跨端效果。

当然,目前实践下来 API 还是有些不太优雅的地方,比如 useImport 的使用方式,使用起来有点别扭,但暂时没有找到合适的方式,感觉应该可以更加自然些。读者们有好的建议欢迎推荐。

开发使用体验与实践

快速体验开发一个跨端应用

为了方便使用,联合跨端构建器与适配 API 已经抽象集成到了业务项目物料中,结合脚手架,我们可以快速创建一个跨端应用。

image.png
npm i 后,可以分别运行 npm run dev:m 和 npm run dev:pc 启动 m 端和 pc 端调试。打开应用调试demo链接,PC端和M站即可以同时基于不同的技术栈运行调试。另外这里的 m 和 pc 参数是可以自定义的,自己也可以根据需要增加更多端场景与构建命令。
image.png
构建发布也很简单,后面带上平台参数即可:

npm run dev:m // 调试无线
npm run dev:pc // 调试pc

npm run build:m // 构建无线
npm run build:pc // 构建pc

构建后 build 下面会生成 m/ 目录和 pc/ 目录,统一发布到 cdn。然后配合前端模板发布引入即可。

业务落地实践

实际业务当中,我们选择了国际站某个商品订单列表页面进行落地。订单列表页涉及的多端技术栈十分繁杂,需求变更常常涉及多端多个应用工程的修改。

落地过程中我们的目标是对 PC 端应用和无线端应用进行整合,随后废弃无线端工程应用,通过联合跨端方案复用 PC 端的逻辑、差异化管理 UI 层,做到了同一个应用里面能够构建出 PC 端资源和无线端 H5 资源。而项目中的数据处理逻辑和主要事件逻辑都做到复用。这样可以减少原有两个项目分开研发、调试、发布的麻烦。

因为技术体系的差异,PC 端是 React + Fusion 体系,无线 H5 上为 Rax + Meet,例如我们需要在入口文件React/Rax 的 render 调用时做一次适配区分。(一般推荐不同端逻辑区分文件管理)
image.png
image.png
运行两端调试命令,npm run dev:pc 和 npm run dev:m,即可同时编译调试 PC 端 React 代码和无线 H5 Rax 代码。业务逻辑一处修改,两端同时生效。当然如果 PC 和 无线都是 React 体系,则同样适用。
image.png
看下同一个应用工程下面两端的实际运行效果,样式正常,功能验证OK。
image.png
发布过程和原来一样,构建配置稍作修改。build 两次即可生成多端资源:
image.png
最终分端构建资源会打包到 build 目录下面,如果 package.json 中设置了默认使用 pc 资源,则 /build/ 一级目录是 PC 端的资源,无线 H5 构建的资源则放到 /build/m/ 下面,线上模板引用时更新资源 cdn 路径即可,原有的发布流程也不变。

分别看一下PC端和无线端各自加载的JS资源文件情况:
image.png
image.png
完成!

总结

最后联合跨端思路和现有DSL跨端方案并不是冲突的,两者可以结合使用来达到覆盖全端的效果。针对不同的场景,我们也应该选择合适的方案来使用。

当然未来会有更多可能性,随着智能化 UI 生成技术的成熟,UI 层的开发将最先变得不是瓶颈。过去我们一直关注 UI DSL 统一为目标,以跨端 UI 为基础向上构建应用,反而 DSL 层的维护耗费了更大精力;未来应该是业务逻辑(领域模型)实现为中心,各端 UI 仅作为 UI 层(表现层)的联合能力体现,而且是可插拔扩展的一层,适配层将作为常用开发方式来处理差异化的问题。同时针对 Flutter ,我们也在探索尝试一端逻辑跨 web 和Flutter 的复用方式,提升研发效率的同时,也让业务领域模型的 UI 表达更加高效纯粹。


image.png

相关文章
|
3月前
|
设计模式 架构师 数据建模
架构师必备底层逻辑:设计与建模的技术深度探索
【8月更文挑战第13天】在软件开发的浩瀚星海中,架构师如同星辰指引,他们不仅规划着系统的蓝图,更在底层逻辑上精雕细琢,确保系统的稳健与高效。其中,“设计与建模”作为架构师的核心能力之一,是连接业务需求与技术实现的桥梁。本文将深入探讨架构师在设计与建模过程中的关键思维与实践方法,为工作学习中的技术同仁提供一份宝贵的干货分享。
47 3
|
3月前
|
机器学习/深度学习 分布式计算 前端开发
构建前端防腐策略问题之前端代码会随着技术引擎的迭代而腐烂的问题如何解决
构建前端防腐策略问题之前端代码会随着技术引擎的迭代而腐烂的问题如何解决
|
4月前
|
运维 自然语言处理 监控
软件研发核心问题之在需求拆解过程中,“需要多少UI”的问题如何解决
软件研发核心问题之在需求拆解过程中,“需要多少UI”的问题如何解决
|
4月前
软件研发核心问题之在需求拆解过程中,“数据与UI如何关联”的问题如何解决
软件研发核心问题之在需求拆解过程中,“数据与UI如何关联”的问题如何解决
|
存储 缓存 NoSQL
概念、场景技术方案选择的理解
概念、场景技术方案选择的理解
56 0
|
存储 自然语言处理 算法
GaiaX开源解读 | 表达式作为逻辑动态化的基础,我们是如何设计的
GaiaX跨端模板引擎,是在阿里优酷、淘票票、大麦内广泛使用的Native动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX开源解读》,带大家看看过去三年GaiaX的发展过程。
347 0
|
存储 自然语言处理 算法
作为逻辑动态化的基础,GaiaX 表达式是如何设计的? | GaiaX 开源解读
GaiaX 跨端模板引擎,是在阿里文娱内广泛使用的 Native 动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX 开源解读》,带大家看看过去三年 GaiaX 的发展过程。 GaiaX 开源地址:https://github.com/alibaba/GaiaX
421 0
作为逻辑动态化的基础,GaiaX 表达式是如何设计的? | GaiaX 开源解读
|
Android开发 UED iOS开发
一个淘宝的bug,让我弄懂了它的底层逻辑和顶层设计
一个淘宝的bug,让我弄懂了它的底层逻辑和顶层设计
一个淘宝的bug,让我弄懂了它的底层逻辑和顶层设计
|
自然语言处理 架构师 项目管理
技术方案设计的方法
前段时间接手了一个还处于方案设计阶段的工作,我重新做了设计。觉得新方案比旧方案业务清晰明朗、解决了旧方案的缺陷。我就很高兴,跟同事聊这个事情。同事就问我是怎么想到这些的呢。 我说了一些细节的,但是没有把核心本质讲出来。我觉得这是个很难回答的问题。因为一个方案怎么更合适,主要因素包含业务理解、个人经验、思维逻辑。这3个要素一般都是靠经年累月的积累才获得的。从这些中提取出别人可以学习和使用的方法确实不是一会儿就能想出来的事情。
技术方案设计的方法
|
设计模式 Java 程序员
《重构:改善既有代码的设计》-学习笔记一(+实战解析)
《重构:改善既有代码的设计》-学习笔记一(+实战解析)
206 0
《重构:改善既有代码的设计》-学习笔记一(+实战解析)