搭建体系的模块依赖关系与 import maps

简介: 我是淘系前端搭建服务团队的步天,目前负责建设天马搭建服务和斑马搭建平台,既提供通用的搭建服务,也提供直接可用的搭建产品。
作者:步天
来源: Alibaba F2E公众号

image.png

我是淘系前端搭建服务团队的步天,目前负责建设天马搭建服务和斑马搭建平台,既提供通用的搭建服务,也提供直接可用的搭建产品。在开始之前,先介绍一些下面会提到的名词,有助于更好的理解:

  1. 搭建:可以给到非技术同学,通过拖拽、配置的方式,产出页面来运作自己的业务
  2. 模块:搭建操作的最小单位
  3. 天马:淘系搭建服务团队建设的,面向阿里内部多个 BU 提供服务的搭建中台,可以协助业务快速构建一个业务搭建系统

我们在去年4月分享过《淘宝前端在搭建服务上的探索》,介绍很多淘系搭建在过去发生的变化,以及为什么会有天马这样的搭建服务。

今年的4月分享了《淘系前端搭建服务在2020年有哪些变化》,介绍了在搭建体系比较完整的情况下,我们在 2020 年又做了哪些事情来提升用户的体验。

在阅读本文之前,推荐先阅读以上两篇文章,对搭建体系有一些更多的了解。本篇文章会和前面两篇文章不同,会专注于讨论搭建体系下,模块依赖关系的处理以及未来如何标准化的思考上。

老生常谈的 Web 资源引用问题

虽然现代的浏览器性能已经基本不输原生客户端,但真正跑在浏览器上的页面却依然有比较多的体验问题,其中加载性能长时间处于最大问题之列。其中很大一部分原因是渲染页面时依赖的文件都需要远程下载,这个也是 Web 区分于 Native 最大的差异。

为了提升加载性能,从社区规范上,经历了几个阶段:

  1. 从纯 script 标签组织加载顺序到开始使用 CDN combo 功能。
  2. 从没有模块规范到出现了 CommonJS 模块规范,可以同步加载模块。
  3. 由于浏览器远程下载文件的特性,出现了 AMD 模块规范,可以声明异步的依赖关系。
  4. 异步加载性能依然受限于浏览器并发请求数、调试困难等问题,开始出现了打包成单文件的方案(Dojo、Webpack)
  5. 随着 Web 复杂度的上升,以及 ES Module 标准、import maps 规范出现,开始出现 bundless 的方案,解决开发时构建效率以及面向标准运行的问题。

而天马 seed 模块规范源自 KISSY,本身沿用了 AMD 的思路,最后采用了 CMD 的规范(因为产物生成逻辑更简单),在搭建的动态化背景下,保留了模块化开发和运行的方式。天马模块和市面上的大部分搭建、低代码体系不一样的地方:

  1. 模块化开发,统一并隐藏了基础的构建逻辑,公共依赖自动去重,开发者基本上不需要关注构建过程。
  2. 页面的发布没有构建过程,用户访问时渲染,支持大批量页面同时修改。
  3. 页面之间发布独立,模块版本更新可以细粒度到单个页面生效。

但这套规范运行5年下来,也遇到了比较多的问题,我们在接下来与 import maps 规范的对比中,来讲讲天马模块规范和社区标准衍进的差异。

seed 规范与 import maps

seed 是天马模块中描述依赖关系的文件,类似 Webpack Dependency、SystemJS depcache,在天马体系里,这个文件会给到渲染引擎以及浏览器端直接执行。seed 这个词来源于 YUI 体系,seed 规范本身沿用了很多 KISSY seed 的描述方式。我们内部自研了 feloader 加载器(这部分也继承了 KISSY loader 的实现)来支持 seed 格式的解析和模块的加载与执行。

引入和使用

seed

  1. 引入 feloader 加载器,会在全局环境下注册 require 和 define 方法。
  2. 配置依赖关系。
  3. 加载 seed 里配置过的模块。
<script src="feloader.js"><script/>
<script>
  require.config({
    "packages": {
      "example-1": {
        "path": "/example-1/"
      }
    }
  });
  require(['example-1/index'], function(Example) {
    // do something.
  });
</script>

import maps

  1. 声明 importmap 配置
  2. 用浏览器提供的 import 方法加载配置过的模块

注意:下面只是一个范例,展示了 import maps 的使用方式。目前规范下,因为暂无合适的配置合并策略,所以一个页面只能设置一次 importmap。

<script type="importmap">
{
  "imports": { ... },
  "scopes": { ... }
}
</script>
<script type="importmap" src="import-map.importmap"></script>
<script>
const im = document.createElement('script');
im.type = 'importmap';
im.textContent = JSON.stringify({
  imports: {
    'my-library': Math.random() > 0.5 ? '/my-awesome-library.mjs' : '/my-rad-library.mjs';
  }
});
document.currentScript.after(im);
</script>

<script type="module">
import 'my-library'; // will fetch the randomly-chosen URL
</script>

方案对比

设置方式

首先,seed 是一个相对灵活的设置方式,在脚本执行的任何阶段都可以动态修改模块的配置,模块在加载完成后,依然可以重置模块状态,并重新加载。
而 import maps 会更严格:

  1. importmap 文件需要用 script + type="importmap" 的方式加载,虽然是 json 文件,但是浏览器会自动解析,所以 importmap 文件需要在调用 import 的脚本之前加载,并且加载过程是阻塞性的。
  2. 一个页面只能加载一次 importmap,多 importmap 还在讨论中。在开始加载脚本之后,增加新的 importmap 文件会直接报错,并且无法生效。
  3. 只能用 type="importmap" 来加载 importmap 文件,MIME 类型需要是 application/importmap+json(需要支持 CSP,普通 JSON 文件不需要)。

在这些限制下,我们就无法和 Node.js 环境一样,每个外部库都可以自己管理自己的依赖。而是需要从完整页面的视角,统一管理页面上用到的所有依赖,外部库应该只负责 import 就可以了。也就是如果页面上加载了一个远程的 CDN 外部库,开发者还需要关注这个外部库依赖了哪些其他外部库,并在 importmap 文件里声明清楚。这部分对于开发者实操来说很难,毕竟大部分开发者还是喜欢拿来即用,并不关注外部库做了什么。

理论上,无论用 seed 还是 import maps,开发者都应当知道页面有哪些外部依赖,并合理升级。npm 模式提供了一个较为宽松的版本化依赖方式,但在 Web 上并不一定适用。其中最大的差异是,Node.js 应用是在打包部署的时候,就把版本确定下来了,在下一次重新部署前,依赖版本是不会变化的,而 Web 页面是在用户侧,每次访问的时候重新组织,语义化版本带到 Web 页面渲染的时候并不合适,带来了太多不确定性,所以个人觉得 Airpack 是很好的方案,但用起来可能比 npm 会有更多的问题。大部分同学还是习惯了,拿来即用

目前标准推荐用行内的方式加载 importmap 文件,性能最佳,如果用外链的方式,由于页面上的脚本都需要等待 import maps 配置完成才能执行,那么只能用 HTTP/2 Push 或者 Web Bundle 来节省下载耗时。其实从 Web 标准上,css 样式表也是推荐用行内的方式加载的,虽然 css 文件不阻塞资源加载,但依然会阻塞元素的展示,毕竟浏览器总不能先展示没有样式的 HTML 元素,然后等 css 文件加载完了再把样式重新渲染上去。

支持 Package

为了缩减依赖配置的体积,减少重复的路径声明,seed 格式是由 packages 和 modules 组成的。packages 用于声明模块的目录,modules 用于声明模块间的依赖关系。同时 packages 上可以配置更多的信息,比如 CDN combine 的时候,是否需要独立的 script 标签,这样可以控制 script src 地址的组合,实现跨页面的脚本缓存共享。

{
  "modules": {
    "example-1/index": {
      "requires": [
        "example-1/utils"
      ]
    }
  },
  "packages": {
    "example-1": {
      "path": "/example-1/"
    }
  }
}

import maps 规范也有 packages 的概念,用 “/” 结尾与模块进行区分。配置 packages 的方式可以支持子文件方式的 import。

<script type="importmap">
{
  "imports": {
    "moment": "/node_modules/moment/src/moment.js",
    "moment/": "/node_modules/moment/src/",
    "lodash": "/node_modules/lodash-es/lodash.js",
    "lodash/": "/node_modules/lodash-es/"
  }
}
</script>
<script type="module">
  import moment from "moment";
  import _ from "lodash";
  import localeData from "moment/locale/zh-cn.js";
  import fp from "lodash/fp.js";
</script>

作用域和大版本不兼容设置

由于前面 import maps 要求页面统一管理依赖,但是实际情况下,同一个外部库在页面上只有一个版本还是比较理想的情况。那么 import maps 是支持了通过设置作用域的方式,让不同文件可以加载的外部库的不同版本。

{
  "imports": {
    "querystringify": "/node_modules/querystringify/index.js"
  },
  "scopes": {
    "/node_modules/socksjs-client/": {
      "querystringify": "/node_modules/socksjs-client/querystringify/index.js"
    }
  }
}

seed 规范下目前还没有支持作用域,一方面是同个外部库不同版本加载两遍带来的体验问题,另一方面也是对开发者做好依赖管理的要求。目前对于 npm 上允许大版本(x位)不兼容的情况,我们的解决方案是把大版本号放到模块名上,实际使用的时候就是两个模块了。比作用域的方式更加简单,不过依赖工程链路来做模块名的转换,带来了一定的复杂度。

{
  "packages": {
    "example-1@1": {
      "version": "1.0.1",
      "path": "/example-1/1.0.1/"
    },
    "example-1@2": {
      "version": "2.0.2",
      "path": "/example-1/2.0.2/"
    }
  }
}

依赖模块的下载

seed 机制下,最重要的其实是依赖包的加载能力。一个模块可以声明依赖,类似 npm 的 dependencies,然后在加载模块的同时,会并发下载所有的依赖,包括依赖的依赖,确保模块可以正常执行。

比如在下面的例子里,加载 index 模块的同时会加载和执行 utils 和 tools,最后执行 index。

{
  "modules": {
    "example-1/index": {
      "requires": [
        "example-1/utils",
        "example-1/tools"
      ]
    }
  }
}

seed 机制支持了 CDN combo,多个依赖的下载合并到一个 script 请求后,大部分情况下速度会比多个并发更快一些。具体还是要看场景,毕竟不做 combo 的话,跨页面之间可以自然共享脚本缓存。

import maps 目前还没有支持类似的依赖下载方式,这样就意味着,存在串行下载依赖的情况,目前 Webpack Module Federation 也有类似的问题,不过至少不是一个一个串行加载的。

SystemJS 内部有增加类似 requires 的实现,目前还在讨论阶段,用法如下:

<script type="systemjs-importmap">
{
  "imports": {
    "dep": "/path/to/dep.js"
  },
  "depcache": {
    "/path/to/dep.js": ["./dep2.js"],
    "/path/to/dep2.js": ["./dep3.js"],
    "/path/to/dep3.js": ["./dep4.js"],
    "/path/to/dep4.js": ["./dep5.js"]
  }
}
</script>
<script>
setTimeout(() => {
  System.import('dep');
}, 10000);
</script>

因为提前声明了 depcache (requires),就可以提前并行加载所有依赖的文件,省去了串行等待环节,只要加载器支持就好了。

关于 seed next

回想前面几年,我们尝试了很多方案想把 seed 模块化的体系改造到更贴近 Bundle 的方式,包括合并多个页面等等偏模板的方式,让模块搭建的页面接近源码开发的页面。但有一些问题始终很难解决:

  1. 如果每次发布都需要打包,生效速度和页面数量上存在比较大的问题。
  2. 如果合并多个页面作为一个源码模板,那么首屏资源无法精准计算,影响用户体验。

绕了一圈之后,发现 import maps 还是带来了不一样的思路,模块化的方式还是可以长期在 Web 下推进下去的。

面向未来的话,一方面需要考虑将 CMD 规范升级到 ES Module 上,需要考虑好降级方案和 CommonJS 依赖的处理。另一方面更重要的是需要推进一套更加规范的依赖加载方式,这个加载方式需要结合 CDN combo,异步并发下载依赖,客户端 native cache,PWA,Server Push,甚至浏览器实验功能预测加载 JavaScript 文件等等,针对的不同的场景,给到不同的加载方案。渐进增强,平稳退化应该是前端的强项。目前这部分还是会继续在内部的 feloader 上拓展,未来会考虑和 SystemJS 有更多的结合。

资料参考

  • 《github/import-maps》
  • 《W3C import maps 草案》
  • 《SystemJS 支持的 import-maps》
  • 《import maps需要支持 CSP》
  • 《import maps 支持 depcache 提案》
  • 《KISSY config》
  • 《YUI Loader》
相关文章
|
JSON 前端开发 JavaScript
开源表单方案 Formily 的核心设计思路
Formily 是一个数据+协议驱动的表单解决方案,它站在Reactive响应式编程巨人的肩膀上,构建出了从基础表单到低代码领域的高性能通用基础能力,同时其配套的跨框架+跨终端组件生态体系,也能让用户更高效的开发日常业务表单,尽可能的减少了重复冗余的逻辑实现。本篇内容来自白玄在第十六届D2前端技术论坛的分享,将为你介绍如何在高复杂业务场景下提高我们的表单性能与表单开发效率。
5484 1
开源表单方案 Formily 的核心设计思路
|
监控 前端开发 Java
错误码如何设计才合理?
对于错误码的设计,不同的开发团队有不同的风格习惯。本文分享阿里文娱技术专家长统对于错误码的看法,希望从错误码使用的不同场景讨论得到一个合理的错误码规约,得到一个面向日志错误码标准和一个面向外部传递的错误码标准。
11659 3
错误码如何设计才合理?
|
数据采集 移动开发 文字识别
服务阿里 9 个APP|揭秘新奥创升级的质量演变
新奥创技术体系,是手机淘宝端搭载着星环中台的整个商业化研发体系,孵化出的面对无线电商领域的技术体系。过去一年在手淘完成了下单、详情、购物车三大业务域的改造,接下来还会在订单、手淘导购等领域进行技术升级。目前新奥创已经接入阿里内的9个 App,逐步成为阿里集团无线领域电商系的技术解决方案。 本文主要围绕新奥创技术体系的升级,剖析架构升级对测试保障带来的新的转变,也是新的机遇。
5526 0
服务阿里 9 个APP|揭秘新奥创升级的质量演变
|
机器学习/深度学习 人工智能 自然语言处理
从前端智能化看“低代码/无代码”
什么是低代码/无代码开发?业界对于低代码/无代码开发是否存在其他不同的理解?低代码开发和无代码开发之间的区别是什么?
从前端智能化看“低代码/无代码”
|
7月前
宜搭接入钉钉闪记,会议记录、拜访录入效率飙升!
钉钉闪记是一款实时记录并快速实现语音转文字的工具,特别适合处理冗长的会议或培训视频。通过与宜搭平台的无缝集成,用户可以一键获取会议的关键知识点和重要内容片段,自动生成结构清晰的会议纪要,极大提升了工作效率。无论是销售拜访还是头脑风暴,闪记都能显著减少整理会议资料的时间,为企业带来高效的办公体验。
540 12
|
人工智能 JavaScript 开发工具
【完全免费】VS Code 最好用的 12 款 AI 代码提示插件!!!
🎉 探索12款免费VSCode AI代码提示插件:Codeium、Codegeex、CodeFuse、TONGYI Lingma、Comate、iFlyCode、Fitten Code、Bito AI、Mintlify Doc Writer、Kodezi AI、aiXcoder、IntelliCode。这些插件提供智能补全、代码生成、注释、优化,支持多种语言,提升编程效率!🚀👩‍💻👨‍💻
16569 0
|
存储 数据库 索引
faiss 三种基础索引方式
faiss 三种基础索引方式
766 1
|
前端开发 JavaScript API
ahooks 3.0 来了!高质量可靠的 React Hooks 库
ahooks 3.0 来了!高质量可靠的 React Hooks 库
1187 0
|
自然语言处理 JavaScript 前端开发
低代码平台加载远端组件解决方案(1)——defineAsyncComponent
低代码平台加载远端组件解决方案(1)——defineAsyncComponent
810 0
|
移动开发 监控 前端开发
2023 年大淘宝 Web 端技术概览
2022 年,大淘宝前端团队进行了调整:重新组织生产关系,按业务线拆分整合进对应的业务技术团队,同时保留了大前端虚线组织,确保研发基建的一致性、技术的持续投入以及推进人员的成长。 整个变化涉及超过三百人的前端团队,经过了半年多的运转,整个团队在技术上也进行了对应的聚焦和收敛。 新的组织协作形态下,大淘宝 Web 领域的工程师们正在做哪些技术工作、有什么技术产品,特在 2023 年开年之际向行业前端同学进行分享。
4214 1
2023 年大淘宝 Web 端技术概览