天马,跨终端搭建页面,你说了才算

8,921 阅读32分钟

前端早早聊大会,前端成长的新起点,与掘金联合举办。 加微信 codingdreamer 进大会专属内推群,赢在新的起跑线。


第十四届|前端成长晋升专场,8-29 即将直播,9 位讲师(蚂蚁金服/税友等),点我上车👉 (报名地址):


正文如下

本篇为第三届 - 前端页面搭建专场步天讲师的分享


Hello,大家好。

今天我代表淘系前端搭建服务团队,来分享淘系在经历十多年的可视化搭建探索后,沉淀出来的面向阿里经济体的搭建服务,也是今年经济体前端委员会四大方向之一,后面也会重点讲下一些比较核心的围绕可搭建模块的设计。

个人介绍

在正式开始之前,我还是先介绍一下,我目前负责淘系搭建服务团队,花名叫步天,11 年加入阿里巴巴,最早负责过淘宝口碑网的外卖业务,后面因为公司业务调整,负责过云 os 的云应用开发,后面来到天猫、淘宝,负责过天猫的头尾、首页以及一些行业业务,也重点参与了天猫前端模块化技术体系 MAP 的建设,之前也在天猫前端知乎专栏上做过分享。后面结合当时无线化和跨终端的业务诉求,和团队同学一起设计了一套解决天猫营销大促的搭建解决方案,这套方案也从上线那一年开始,支撑了后面的每一次双 11 大促会场。由于双 11 的场景足够复杂,参与的开发者和非研发同学足够多,搭建平台在不断迭代中越来越成熟,包括也进行了服务场景的拆分,墨冥同学之前说的方舟,就是淘系面向营销活动设计的搭建解决方案。随着集团越来越多的业务逐渐进来使用这套体系,为了提供更好且更适合业务场景的搭建能力,抽象了一套统一的搭建服务,协助业务能够快速构建一套符合自己业务场景的搭建解决方案。

搭建的定义

因为今天的主题是搭建,实际上搭建的概念还是比较宽泛的,前面的主题有面向中后台的搭建,也有比较横向的智能化搭建,我这里讲的搭建,相对会更偏向于消费者端产品的搭建,主要用户也是非技术同学。为了让大家更有体感,我在 PPT 里展示了一些用搭建系统产出的页面,包括聚划算、天猫超市、天猫国际等手淘里入口比较明显的业务,实际上,因为一个模块本身包含的内容是由开发者决定,所以复杂场景下,包括双 11 的互动游戏,也是可以用模块来承载的。


搭建解决了什么问题

搭建本质上还是在解决效率问题,把原本开发者的一些工作量,以一种相对比较合理的方式,转移到了非技术同学。在讲搭建的设计之前,可以回到一个比较原始的问题,作为一个开发者,我们是如何来提效的,通过这个效率问题的探讨,来看我们应该怎么设计一个搭建系统。



当然这些是我自己总结的,可能也没有那么准确,主要有 6 个因素:

  1. 正确的人做正确的事情:作为一个开发者,一般都会期望自己做一些比较擅长的技术方向,比如有些同学擅长性能优化,有些同学擅长可用性无障碍,有些同学擅长 PM,与业务方沟通和协调。正确的人做正确的事情,既是对项目本身的保障,也是对开发者来说一个更合适的选择。
  2. 工程工具:前端其实和工程的关系还是非常大的,特别是前端框架、技术方案迭代非常快,对工程体系的要求也非常高。从早期的 YUI compressor,到 uglifyjs,再到 gulp、webpack 的工作流整合。工程背后还有部署、安全、测试、体验等更多方方面面,在提升开发者工作效率的同时,也是在提升对外交付代码的质量。
  3. 代码质量:代码质量也还是一个非常大的命题,包括人工的 code review, eslint 的代码检查,以及各种单元测试、集成测试的方案。一方面是避免代码因为 bug 或者其他原因反复修改浪费时间,另一方面也是能够在多人协作的时候,能够更好地相互维护和理解代码本身。
  4. 组件生态:这个就不用多说,好的组件生态,可以让开发者更加关注与解决目前业务的问题,而不是需要大家都来造一些低级的轮子。
  5. 提升复用:大部分开发者都知道组件化可以提升复用度,提升大家的效率,但我这里为啥在已经讲了组件生态之后,还是提到了提升代码复用这个事情。因为写一个通用的业务组件确是很难的,你要考虑我给哪些业务用,这些业务之间的后端系统有哪些差异(字段怎么设计),设计风格有哪些差异,交互上是不是需要支持多种模式等等。
  6. 性能&监控&埋点:性能和监控很像是跑马拉松的最后一公里(通常说马拉松最后一公里最危险),很多时候我们就非常容易在最后一公里出岔子。上线发现因为页面打开太慢,转化率下降,或者上线发现忘记埋点了,不知道做的功能有多少人体验,使用情况如何等等,相信大家也有或者经常遇到这样的案例。这些背后也会和工程、组件生态都相关。

交换角色

当开发者把上面的事情都做了一遍之后,接下来又会面临一个问题,就是人不够,一个前端对应五个开发,然后对应二十几个运营,算是比较正常的事情。人越是不够,就越被资源化。这个时候,我们就会想到,是不是可以让更了解用户的非技术同学,直接可以自主产出需要的页面,而不需要前端同学从头跟进到尾实现一遍。



那么,和上面说的一样,解决效率的 6 种方式也会有一一对应的手段,背后也就是搭建设计的一些思路:

  1. 需要一个面向非技术同学设计的搭建平台,原则上文档、视频、培训后,就可以业务自主运作了。
  2. 虽然是非技术同学在生产页面,但是最基础的回滚、灰度发布能力,还是要具备,确保有一定的应急和安全发布的能力。
  3. 非技术同学搭建依赖的物料应该是版本化的,不然开发者提交一个有 bug 的模块,用到这个模块的非技术同学的搭建操作流就会被阻断,这是一件非常让人困扰的事情。
  4. 要提升页面产能,一定数量的模块是需要的,比如一次活动可能就需要 20+ 的模块。而随着前面提到的智能化,我们也可以让非技术同学通过视觉稿/拖拽的方式来产出更多的物料。
  5. 业务之间一定是存在互通和合作的,那么这些物料也应该可以跨业务流通和复用。
  6. 性能、监控、埋点这些偏技术的概念,应当尽量自动化。

搭建的名词和概念

前面说了比较多对于搭建系统设计的思考,为了帮助大家能够更好地理解后面的内容,首先把一些名词做一下对齐:

  1. 模块:非技术同学搭建页面依赖的最小单位。
  2. 页面搭建:从模块到页面的组合过程。
  3. 数据投放:数据的变化频率远高于页面,所以单独提取出数据投放的概念。
  4. 天马搭建服务的上层搭建应用,大部分对应的数据投放能力都是非常复杂的,包括数据的定时生效,个性化、定向投放、自动调优,并不是一份静态可以和页面打包在一起的数据。这也就是数据能够单独调整的必要性所在。

搭建的设计


  1. 从非技术同学作为搭建主要用户的角度来思考,以及无线化场景下,手机屏幕的特征,一维存储的模块列表是比较友好的。这个设计也对搭建服务本身带来了很大的简化,整个页面结构就是一维数组,每次操作都可以转变成一次简单的数组操作。当然,一维的存储不代表一维的展示,开发者依然可以在展示的时候,通过一些父子关系,来把一维的存储结构转变为树状结构。目前我们是判断把复杂度给开发者,简单的设计给到非技术同学,还是一个比较合适的方式。并且一维的页面结构也可以更好地融合后端的数据,无论是搭建的智能化自动化还是面向端侧的个性化,都可以结合的更好
  2. 搭建结果可以跨终端访问也是和阿里的一些业务特征有关,因为无线化的战略,至少在消费者端,公司在无线上的投入比桌面端大很多(主要是消费者侧),那么如果能搭建无线页面,然后桌面端自动生成,或者反一下,对搭建用户来说可以省掉很多时间。特别是当下极端一些的场景,用户搭建一个无线端页面,需要同时额外生成 pc、weex、小程序的版本。当然,这个只是默认选项,搭建用户还是可以选择只搭无线,或者只搭桌面端,或者都搭但是不是自动同步。

天马搭建服务

基于上面这些定义,最后我们交付了天马搭建服务,服务覆盖了从模块研发到托管,用户搭建到上线的流程。同时基于这些服务,淘系内部也提供了面向大促营销的搭建产品,面向通用场景的搭建产品等等。

从数据上来说,目前天马的搭建服务支撑了十几个 BU,包括已接入和正在接入的 BU,覆盖了国内及国际化的场景,也随着海外的业务部署多个国家,覆盖亚欧美三大板块。前面提到的方舟就是天马搭建服务的重要用户。

同时通过这十几个 BU 的搭建应用,服务了阿里经济体近 30% 的员工,对应创建的页面数也过了百万。而面向商家的搭建应用则创建了更多页面。

天马也联合这些上层 BU 一起制定了阿里巴巴前端模块规范及对应的阿里巴巴模块中心,基于更多明确的约定和共识,让跨BU的模块流通也成为可能。

架构大图



回到天马本身的话,核心设计了 3 个分层:

  1. 基础服务层:负责基础搭建数据的管理,比如模块、页面、用户、管理等模型的增删改查。这一层设计的足够通用,简单,不感知业务逻辑,也尽量少对外依赖,确保一个个功能单元足够颗粒化。
  2. 能力层:因为如果我们只提供一个基础服务层,就会遇到一个问题,上层搭建应用接入的时候非常复杂,处理用户的一个操作,背后可能需要调用到基础服务层的多个接口,这些操作组合还是比较容易出错的,所以在能力层对这些基础服务进行了组合,降低接入成本。另一方面,有了能力层,和大量外部系统的交互就可以在这里完成,比如和小程序后台、性能检测等平台对接。这一层的产物包括了 API 接口,搭建的脚手架,模块管理平台等等。
  3. 研发服务层:研发能力主要分为核心的构建器、提升模块研发体验的可视化研发插件、面向不同场景的初始化脚手架、以及配套的研发文档。文档也是重要功能,不管是面向开发者还是其他非技术用户。

模块的设计

可搭建模块的定义

前面有提到,模块是搭建的最小可用单位,其实中后台搭建也是一样的,只是天马设计的模块是简化了搭建过程,屏蔽了更多技术侧带来的复杂度。

模块本身,除了定义之外还有几个重要的设计:

  1. 跨终端:减少重复工作
  2. 扁平化:暴露的接口足够简单清晰
  3. 面向标准数据研发:提升模块的流通能力

跨终端

最早在 12 年的时候,天猫就已经有了跨终端的概念。这里不严格定义来区分终端、容器等等,先用比较简单的概念,称为终端,也就是目标的运行环境。终端包括桌面版 chrome、移动端 safari、tv 盒子的 UC 浏览器、手机淘宝的 webview/weex 容器、支付宝小程序容器甚至到服务端的 ssr 渲染引擎等等。

为什么不用响应式?响应式只是跨终端的一种解决方案,响应式解决不了代码运行在服务端的问题,并且响应式本身也过于注重效率,而不是去面对本质上的差异。



桌面端端导航模块、无线端导航模块:
比如图里的导航模块,无线端是一个 tab ,而在桌面端是一个随屏滚动且悬浮的模块,这是一个交互差异的案例,而实际上,更多还有内容、业务逻辑上的差异,所以不必拘泥于响应式,该写两套逻辑就写。
当然,跨终端是模块的能力,如果我的模块就是只面向一个端服务,就可以只写一个端。

扁平化

因为有跨终端,也就是意味着模块可能有多个出口。扁平化可以简化出口复杂度,搭建系统可以通过简单的约定来获取到对应端内的文件,不需要额外有一个配置文件来指定哪个端用哪个文件,用规范约定替代繁杂的配置。

比如下面就是一些模块跨终端的范例,扁平化也很好理解,就是构建后的产物,没有目录嵌套,直接在根目录展示:

  • index.js
  • index-pc.js
  • index-tv.js
  • index-weex.js
  • index-es6.js
  • index-ssr.js
  • index-miniapp.js


简单的层级对于模块易用性帮助还是比较大的,同时也方便类似 Rax 这样的一次开发多端运行的方案落地。



rax.js.org/

面向标准数据研发

数据标准化算是三个设计里比较容易让人产生困扰的点,但实际上,大家在日常研发中,或多或少都在做着相关的事情。比如如何校验一个表单,如何和后端一起定义一个新的数据接口,以及现在比较流行的 TypeScript 也是在定义数据格式。

我们把这个概念单独抽离出来,形成一个符合 json schema 规范的 schema.json 数据描述,就是判断需要有一个这样的定义,来描述模块接受哪些入参。这些入参内部,也做了更多的约定,比如如何让模块能够换肤(和中后台换肤的机制有比较大的差异),如何能够让模块能够接受一些配置,以及如何给模块传递核心渲染需要的数据。

这几个字段也是一个推荐约定,算是搭建的一些最佳实践。原则上,作为非技术同学,更多还是需要关注数据本身,而配置和主题之类的UI属性,应该更智能和自动化,比如双 11 会场都会有统一的主题,就不需要运营同学一个一个页面设置。

这些设计背后也就是我们期望开发者模块尽量脱离业务场景,尽量少的与特定的后端接口关联,把模块写的更像一个纯做渲染的组件,这样模块的流通能力才能得到保障。同时也期望运营同学只需要关注自己真正需要关注的部分,而不要变成替研发同学承担一些工作量。

{
  "type": "object",
  "properties": {
    "$attr": {
      "type": "object",
      "properties": {
        "hidden": {
          "type": "boolean"
        }
      }
    },
    "$theme": {
      "type": "object",
      "properties": {
        "themeColor": {
          "type": "string"
        }
      }
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          “itemId”: {
            "type": "string"
          }
        }
      }
    }
  }
}


不过,这样的开发者设计还是会比较理想,schema 也只能覆盖首次渲染依赖的数据,更多交互后的请求,以及那些和特定场景相关的逻辑,看模块需要复用的范围,可以把这部分逻辑外置,比如抽出来放到一个公共组件里,或者可以放到页面级,不同的页面可以共享一套页面初始化逻辑,同时页面也可以给模块传很多公共的方法。

数据标准化

继续展开讲面向标准数据研发,因为搭建本身的特殊性,以及模块对应的数据不会只是简单的进行表单投放,特别是在个性化普及的今天,大部分模块背后,不仅仅是静态数据,而是一些动态数据服务,这些接口可能会来自于公司大大小小各种不同的系统。对于模块开发者而言,我定义的数据描述应该面向哪个接口?同样都是商品接口,A 应用和B应用接口返回的字段,一个是下划线风格,一个是驼峰怎么办。

数据标准化解决的就是这个问题,我们应该面向一个标准的数据进行研发。这个标准数据就是基于目前最底层的这些系统,商品库、用户库等等,统一命名规范后的结果。大家都遵守这个规范来给模块传递数据就可以了。

但实际情况比这个还要复杂,比如有一个模块,有一行文字描述,部分场景下显示的是商品标题,部分场景下显示的是商家写的宣传文案,UI 本身是有二义性的。这个时候,我们在数据描述里就会定义一个叫 title 的字段,具体这个 title 对应实际是 itemTitle 还是 itemDescription,就要看实际的场景。

最后也就是说一个数据接口,给到前端渲染前,实际可能会经过两次标准化,一次领域模型的标准化,确保字段是没有二义性的,然后再是一次 VO 的标准化,再基于视图的需求,映射到可能有二义性的模块展示上。

通常我们会要求后端同学来做好领域模型的标准化,通过 SDK 或者统一有一个数据出口服务,然后前端在 FaaS 或者维护一个类似网关的应用进行视图模型的标准化。


schema 其他用途

搭建系统,除了搭建之外,还有一个非常重要的操作就是投放数据。投放数据操作可能是手动填写表单,也可能是一些商品选择器等等。这些表单都是可以通过类似 json schema form 的方案来生成的,也就是开发者只要写这样一份 schema 描述就可以了。相关的内容,前面洛尘也有比较详细的介绍,大概的思路都差不多。阿里也开源了一套现成的方案。



github.com/alibaba/for…

如何编写模块代码

前面有提到,我们还是比较推荐模块只是做渲染,尽量少的和特定的场景绑定。那么简化一下模块开发,就是:

  1. 定义我需要什么格式的数据
  2. 准备好一份符合格式描述的 mock 数据
  3. 写一段逻辑,输入 mock 数据,返回渲染结果


这个过程其实很像日常业务开始的时候,需要先和后端同学约定字段,先 mock 开发,然后再与后端接口联调。
以下是一个 rax 模块的范例:

import { createElement } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';

export default function Mod(props) {
  let defaultTheme = {
    themeColor: '#fff'
  };
  let defaultAttr = {
    hidden: false
  };
  let {
    items = [],
    $theme: {themeColor} = defaultTheme,
    $attr: {hidden} = defaultAttr,
  } = props;

  return (
    <View className="mod" style={{
      backgroundColor: themeColor
    }}>
      {
        hidden !== 'true' ? <Text>欢迎使用天马模块!</Text> : null
      }
      <View className="keys">
        {
          items.map(element => {
            return (<Text>{element.key}</Text>);
          })
        }
      </View>
    </View>
  );
}


当然,一定会有部分模块,更复杂,比如有点赞、关注等不只是一次渲染能解决的事情,这部分一方面是可以组件化的,写模块的开发者并不需要关心到这部分逻辑,还是只需要传递一些用户信息给到组件就行。另一角度考虑,如果一个点赞接口就有 N 份实现,可以由网关或者 SDK 抹平,或者在公司维度,后端服务设计是不是也有问题,是不是推进统一一个服务会更好?

如何研发模块



模块的研发链路其实和正常开发一个 npm 包差别不大,基于很多约定,我们提供了一些便捷的脚手架,以及有支持插件化的构建器,可以提供给各种不同但有限的 DSL 模块进行构建操作。

同时由于开发者有 ISV、外包、内部员工,可视化研发还是非常重要的环节,尽量把那些和开发一个 npm 模块有差异的点,都通过可视化研发的方式抹平。我们也提供了包括本地模块管理、调试、预览、schema 编辑器等能力,以及代码扫描、资源存储等发布流程的保障。

如何管理模块



每个模块从非技术同学视角是一个独立的产品,而从技术同学视角是一个完整的应用。

当开发者提交一个模块之后,模块的设计师或者产品就可以来 review 和审核对应的模块版本,确保模块质量符合预期,在审核的时候,也可以搭配一些自动化测试和性能检测方案,作为审核参考。

然后业务管理员就可以圈选模块,确定业务域内可以用哪些模块,一方面也可以把那些不是一个技术栈或者存在核心组件大版本不兼容的模块做一下隔离,避免非技术同学使用的时候,遇到不可用的情况。

接下来运营就可以选择这些已经圈选好的模块,进行模块搭建和数据投放,最后给到终端渲染。

如何运行模块


服务端

因为服务端也是模块的目标运行环境,目前在服务端运行模块的方式主要有比较老派的纯模板渲染方式(需要开发者单独写一个模板文件用于生成 html ),以及现在逐渐普及的 SSR 方案。前者足够简单且有确定性,后者面向未来但是需要有足够多稳定性的保障。

客户端

前面也有提到的,为了让模块研发足够简单且保证流通性,页面级需要承担更多包括数据请求、页面容器初始化等操作。

数据请求逻辑是页面逻辑中非常核心的部分,现在会有一些数据驱动 UI 展示的概念,特别是接口合并、分页分屏、容灾打底这些非常重要的功能。分页分屏决定了首屏需要展示哪些内容,请求哪些数据,然后接口合并负责减少请求数,加速首屏展示,然后容灾打底确保最后一定是可以有内容展示给用户,即使有各种网络、服务的问题。

而页面容器渲染,主要还是包括滚动容器的初始化,多维模块列表的渲染。比如把一维的模块列表渲染成多 tab 的父子关系,以及最后需要单独初始化一个个模块。

这里面有大量的细节和技术点,特别是面向用户体验的优化,前面墨冥同学的分享有提到,我这边就不赘述了。

模块的依赖和分析

到这里,上面的大部分内容和中后台搭建还是比较类似的。接下来就是消费者端比较有特点的地方了。中后台搭建通常都只需要在 npm 包组件的基础上,加上一个 schema 描述,就可以用于在搭建系统中生成对应的表单配置了。

但是消费者端的不行,每个模块需要单独进行打包,背景是这样的:

  1. 某次活动,大概用到了 100+ 的模块,搭建出了 1000+ 的页面。然后有个功能需要在短时间内对特定几个模块进行版本升级操作。如果每个页面都需要构建才能生效,短时间内进行大量构建的可操作性是比较低的。
  2. 在个性化、千人前面普及后,页面的展示是由数据来驱动的,如果用传统的构建方案,无法准确做到首屏只加载首屏模块,因为首屏本身包含哪个模块不是由 bundle 决定,而是由数据决定的。




因为搭建的最小单位是模块,且业务上有大量动态性的要求,比如某一天 10 点需要升级 1000 个页面的其中 5 个模块的版本,把这 1000 个页面进行重新构建发布操作性较低,所以组装模块的过程是通过线上渲染服务计算 assets combo uri 实现的,只要在操作后台点击一下模块升级,这 1000 个页面会自动更新模块版本而不需要重新走一次构建逻辑。这也意味着每个模块需要单独打包,给出一个已经可以在浏览器上运行的 web 版本。

但是由于每个模块单独打包,如果啥都不做,会造成依赖重复加载的问题。那么就需要把模块的依赖 dependecies 都 external 掉(也支持主动选择部分打包),为了确保不重复加载依赖模块造成页面脚本体积不可控,引入了 seed 描述依赖的机制,这份文件会比较类似 SystemJs 里的 importMap,只是 SystemJs 是动态获取依赖,而 seed 描述里因为有 requires 依赖描述,就可以利用 CDN combo,合并请求依赖的组件。选择这个格式的原因一方面是描述的能力比较强,另一方面也是历史向前兼容原因,用过 KISSY 的同学会发现这个描述和 KISSY seed 描述是类似的。

关于依赖去重,webpack 也提供了类似的方案,比如 external、DLL 之类的方式,但是对于依赖嵌套处理能力是不足的。

{
  "modules": {
    "@ali/pmod-ark-butian-test/index": {
      "requires": [
        "@ali/rax-pkg-rax/index",
        "@ali/rax-pkg-rax-view/index",
        "@ali/rax-pkg-rax-text/index"
      ]
    }
  },
  "packages": {
    "@ali/rax-pkg-rax": {
      "path": "//g.alicdn.com/rax-pkg/rax/1.0.15/",
    },
    "@ali/rax-pkg-rax-view": {
      "path": "//g.alicdn.com/rax-pkg/rax-view/1.0.1/",
    },
    "@ali/rax-pkg-rax-text": {
      "path": "//g.alicdn.com/rax-pkg/rax-text/1.0.2/",
    },
    "@ali/pmod-module-test": {
      "path": "//g.alicdn.com/pmod/module-test/0.0.9/",
    }
  }
}


这样的描述对于开源组件的使用会有一些限制,比如 npm 上现成的组件,是需要走一个流程提交到天马的系统里的,确保能够生成对应的 seed 描述以及发布资源到 CDN,能够正常引用到。当然也可以不走这个流程,那么依赖的 npm 组件就会被打包到搭建模块的 bundle 里,因为页面级没有构建过程,就没办法做依赖去重了。

光有一个描述肯定不够,核心还需要确定一个策略,模块依赖同个 npm 包的不同版本,应该如何选择。npm 安装的方式是兼容版本取最大,不兼容或者指定版本的时候安装多份的策略。web 上的策略也是类似的,只是内部的研发更可控,所以把这个策略做了更多的简化(以 x,y,z 版本为例):

  1. x 位大版本可以共存(也可以选择不共存)
  2. y,z位版本变化都是向前兼容的,会自动取兼容下的最新,即使指定了版本。


从 web 和用户侧的角度考虑,加载大量同组件的不同版本只会造成页面体积的膨胀,带来带宽、流量的浪费,以及用户侧较差的体验。

本质上,就是把原本 webpack 帮开发者做的内部依赖管理,提取出来,模块级别构建,在页面级统一进行管理。

模块的应用

一个模块发布后,会同时同步到 CDN 和 npm 上,CDN 版本给到纯浏览器和服务端使用,tnpm 部分给到小程序和源码页面等其他有页面级构建能力的场景使用。



目前淘系选择了 Rax 作为统一 DSL,基于上面的搭建设计,加上 Rax 本身一次开发多端运行的能力,就可以实现我只需要写一份无线端 web 的代码,分别转出 weex、小程序的版本,这样我的模块投放到 webview 里就是 web 模式,投放到小程序里就是原生小程序,投放到 weex 就可以以 weex 形式渲染。

社区方案的差异

由于解决问题的特殊性,这套方案和社区也有出现一些差异。

简单说只是在社区的基础上,加了一些约定和文件,在依赖分析和处理上虽然有差异,但实际上背后的思路是相通的。

天马搭建模块社区组件方案
构建规范cmd/umd/未构建版本(发布到npm)页面级打包,组件不需要构建
依赖声明package.json dependeciespackage.json dependencies
依赖分析seed.jsonwebpack dependecy
运行时自研 js loaderwebpack 自动生成
动态加载方案require(dependencies, Callback?)
require.async(dependencies, Callback?)
require.ensure(dependencies, Callback?)
require(dependencies, Callback?)
dynamic import
exports 名称@ali/gitGroupName{gitGroupName}-{gitName}/indexwebpack 自动生成 id
schema格式json-schema,保持和集团 formily 方案一直无限制
mock天马 mock 数据存储在 src/mock.json
对应组件依赖的 props 模拟数据
无限制

跨终端的缓存方案

模块是支持跨终端的,那页面也肯定是跨终端的。而这背后是需要统一的终端识别架构来支撑的。目前搭建产物的页面都是托管在一套缓存+源站架构下的,不同的端会有一份对应的缓存副本,避免每次访问都需要重新渲染。当然由于缓存副本的数量需要控制,所以不是所有的 app 或者浏览器都会被识别,这里会做一些取舍。

为了确保终端识别逻辑的一致性,CDN 解析 UA 的规则是和运行时组件保持一致的。

基于这样一套架构,我们也可以实现运营只需要投放一个地址或者二维码,在不同的端就有不同的展现方式。


未来展望

webpack 5

其实我自己还是觉得这套 seed 机制,因为没有像 webpack 那样把依赖关系隐藏起来,还是有一定的学习成本,并且也容易造成一些问题(当然 webpack 也有他自己的复杂和学习成本 ),自己有时候还是会有点怀疑人生,是不是应该逐渐切换到页面级构建的方案,所以那些页面量不大,且没有淘系这样相对比较极端的批量更新诉求下,天马搭建服务也支持了离线构建的方式,这个方案就更贴近 react 源码app的开发,可以做更多的构建优化。

webpack 5 也带来了一些更多的思路,新增了 “Module Federation” 插件,就是在原本页面级构建,仓库内外置依赖(external)、内部依赖独立(DLL)、拆包(splitChunks)的能力上,再加了一个选项,可以按需引用另外一个仓库的构建结果。具体使用方式是新增了一个插件,插件配置描述了依赖哪些命名空间(也就是对应依赖的工程,对应 seed 的 package),以及哪些内部的模块和具体的文件位置,还有共享的外置依赖,避免重复打包(对应 module 配置)。

基于这样一个模式,模块级打包就是一个更自然的事情,当然把多个模块打包在一个工程里,本质上依然是模块级打包,因为对外出口是独立的。同时真正在业务中使用,还是会一样遇到之前提到的类似的工程复杂度问题。

  1. 公共依赖,如果存在版本冲突如何处理,特别是在 web 运行时上实践大版本共存,其他版本变化兼容取最新,还是需要很多约定和约束的。如果大版本需要共存,在webpack 里可以映射两个不同的id,应该也可以解决。
  2. SSR 的动态化方案,还没有仔细看,感觉落地起来会比较复杂,可能需要多份 webpack 配置,打包多个版本,毕竟去重对于服务端运行来说,意义没有那么大,动态依赖太深反倒会增加复杂度。目前天马模块的每个端也是独立执行打包逻辑的,方案也类似。
  3. 如何更自动地生成合适的 HTML 文件,这部分还需要更多的探索。可能需要把所有工程的入口文件都放到页面上,不管用不用。

浏览器构建



而对于淘系自己来说,动态化是一个重要的诉求,我自己的设想是,如果今天我们可以不用打包模块,直接把源文件发布到 CDN,把 CDN 当做目录,直接在浏览器上跑一个类似 webpack 的能力,就可以在保留动态性的同学,也不会带来额外的复杂度(复杂度都在方案本身了,对开发者来说,就不需要了解太多)。

SystemJs 已经做了一部分类似的工作。为什么这只是一个展望呢,要这么做,还是需要面临一些问题:

  1. 浏览器的性能是否足够做这样的编译,特别是目前在 webpack 本身编译就是一件耗时的事情,把这部分时间扔给用户侧还是有点可怕的,当然也可以限制开发者尽量少些一些编译耗时的代码,以及只编译用到的功能,总之还是需要在用户体验和研发效率之间做权衡。
  2. 远程文件系统在网络上的时间消耗,不用 CDN combo 请求数过多,用 CDN combo 跨页面共享比较难。虽然可以设计很多缓存机制,但是首次依然是个问题,并且绕开浏览器本身的缓存机制,做一套文件缓存还是会有很多其他问题,特别是在无线端 app webview 内,缓存空间是非常有限的,还要设计一套优先级策略完备的缓存机制。
  3. 包管理的复杂度,当前的 seed 机制已经做了类似的事情,不会带来太多包管理方式上的变化。目前没有用 SystemJs 的原因就是依赖复杂度太高,SystemJs 还没办法解决,后续我们也会看下是否可以通过 SystemJs 插件的方式扩展解决。


面向未来考虑,对于开发者来说,写的代码能够更自然且简单的运行,理解成本和维护成本都会降低很多。

欢迎加入淘系前端

  1. 招聘关键词:可视化搭建 / 极客 / 前端工程 / Node.js / web 标准 / 性能 / 提效
  2. 投递简历:butian.wth@alibaba-inc.com
  • 或者加微信



当然在搭建和前端模块化上有更多的想法,也可以一起交流。

一起来创造双11


参考资料