一种小拖大的jssdk加载方案
背景
jssdk 是在前端中完成某些业务功能的 JavaScript 函数库,通常由 sdk 的开发者开发完毕后,交给业务的页面来引入使用。例如:
1 | <head> |
在一些特殊的场景(例如联盟广告)下,我们通常需要把一个 jssdk 地址 交付给另外一个团队的页面来引入。对于大型广告联盟商来说,一般是提供自己的联盟广告平台,在平台上,开发者可以申请到广告 appid,并按照文档引入广告 sdk 到自己的页面中使用。一切比较顺利。
但在公司内部,我所在的商业化部门并没有形成如此成熟的平台。这时我们采用的是比较原始的办法,我所在的商业化部门要把广告 sdk 开发完成后,部署到 cdn。然后我们部门将 cdn 地址如 “http://a.b.c/12345.js” 告知对方业务部门的相关开发,让对方放置我的 js 到对方的页面当中。每次开发、测试、部署,我们的 js 资源地址必然会发生变化,结果就是每次都要找对方部门沟通协调部署问题,部署成本巨大。在没有良好的机制协调下,往往会造成开发和测试发布成本上升,效率低端低下。
为了解决该问题,我在现状的基础上,设计了一种“小拖大”的 jssdk 加载方案,彻底解决对对方部门的依赖。
历史背景
先看下现状我们的工作模式是怎样的:
我,作为广告团队的开发,要么是把广告组件做成 npm 包交给对方;要么是把广告做成 js 放 cdn 交给对方。每一种方式都要找对方联调和沟通,npm 包的方式对方还要编译到对方业务中,其成本和出错的概率更大。最蛋疼的是,第二种 cdn 交付方式,每次通知对方后,对方需要去后台配置一下我给他的 js 地址,然后他下发后,客户端浏览器才会真正的请求最新的 js 地址。
工作模式画成时序图,如下:
其缺点比较明显:
- 架构上: 不符合现在分团队的开发模式
- 流程上: 多了冗余的沟通,例如找后台同学配置
- 技术上: jssdk 下发方式不够标准,不够原生,不够灵活
抛出问题
如何能够减少依赖,降低沟通成本呢。其实最简单的方法就是让对方引入一个固定的 js 地址就好了。目标很明确!
即,我期望实现 JSSDK,在不依赖页面方的情况下自更新?
技术方案
我们最容易想到的方案便是:给对方一个固定 js 地址,每次我们更新的广告代码,我们就在此地址上更新 js。但这样的话,有几个问题:
- 我们的广告组件便没有了版本的概念,回滚时只能回滚 git
- 我们的广告 jssdk,彻底没有了缓存。如果我们的 jssdk 体积增大,那么用户每次打开页面都要下载一个大 js
- 也无法利用 cdn 就近的优势
基于这种考量,我设计了一种 “小拖大” 的方案,这种方案放弃了 20%的缓存能力,但能保留住 80%的缓存能力。用 20%的缓存放弃,换来开发效率极大的提升,对于广告场景来说是比较适合的,因为广告并不是一个页面中最核心的性能诉求,页面最关键的是基本功能的性能和展示,其次才是广告的正常展示和渲染,因此广告适当少量的延迟并不会有太大的影响。
以下是我对几种方案的对比图:
于是,一个新的更适应我当前场景的 jssdk 加载方案,其时序图是这样的:
文字描述一个完整的首次广告请求如下:
- 首先将我们的 “种子 sdk” 地址放入对方业务页面(种子 sdk 将是一个固定不变的 jssdk 地址),
- 对方业务页面被用户打开后,会发起对种子 jssdk 的请求
- 种子 sdk 请求到达我方 sdk server 后,我方 sdkserver 实时生成一个 seed.js ,其中会放入当前各个广告组件最新版本的真实 cdn 地址的一个“资源映射表”
- 当页面收到服务端返回的 seed.js。页面中可以根据业务广告的需求,随意创建任何类型的广告。例如创建一个文中广告:
new ArticleAd()
- 此时,seed.js 发现业务要实例化一个 ArticleAd 的广告,则 seed.js 会查询资源映射表,找到 ArticleAd 的真正 cdn 地址并完成广告代码加载和初始化渲染
由于 cdn 上的真正的广告 js 是强缓存的,因此用户在大部分情况下,都将会使用本地缓存的广告 jssdk。唯一的缺点是 seed.js 是需要每次都发起请求的(由于广告不会每小时都在更新,因此这里也可以将 seed.js 设置为强缓存 1 天或 1 小时)。
由于 seed.js 核心代码仅仅有不到 100 行,因此其体积微乎其微,加载时间也非常的快。
种子 js 的实现
下面我们来看整个架构中比较核心的 seed.js 是如何实现的。这里要考虑如下一些问题:
- seed 如何实现异步加载组件和组件注册?
- 如何保证多次加载时避免重复加载?
- 组件加载完成后如何通知 seed 继续执行?
- seed 加载器如何知道 js 资源最新地址?
- 组件版本更新后如何第一时间更新页面?
- 如何方便页面调试?
种子 js 并不是一个静态的 js,由于它需要内置一个最新版本的资源映射表。因此他是由 server 端动态来生成的,我们 server 端可以采用 Node.js 配合模板引擎来实现。
参考下 webpack 的动态 import 原理
webpack 中有个 动态 import 的能力,即可以让我们在代码中书写:
1 | import("abc.js"); |
这样的代码。然后浏览器中加载时,会动态远程加载并将 abc.js 的导出作为本地 webpack 的一个模块来使用。
这个思路就有点类似于我们本文所述的 seed.js 要完成的功能,因此我们来看看它 webpack 是如何实现动态 import 的:
其底层逻辑还是比较简单的。
实现简单版本
但是我的 seed.js 并不想实现的那么重,也不需要有 require loader 这样的概念。
- webpack 有 chunk 概念,对我们来说我用不到。
- 需要主调模块中写明被调组件名和哈希地址,他是在编译期实现进行代码分割。而我的 seed.js 希望简化逻辑且不应该存在调用代码,且要支持后端任意动态新增组件。
于是,我在此基础上实现了一个更适用于本场景的简单的版本。其大概逻辑如下:
我在服务端会将当前的 “广告资源 cdn 地址映射表” 插入到下图的 RES_MAP 这个对象当中。
实现一个 generate 函数,等待对方业务调用
该函数的功能是:当对方业务调用 generate(‘ArticleAd’) 这样的函数时,则意味是要创建并初始化一个 ArticleAd 的广告,那么 seed.js 需要去主动加载 ArticleAd 广告的 js 资源,并完成初始化。
其中 _loadModule 函数会去 RES_MAP 映射表中寻找资源地址,并完成 js 资源加载和内存缓存(防止多次调用 generate)
如何给开发者屏蔽开发细节
有了 seed.js 去负责加载真正的广告 js。那么,我们广告开发者的工作只需关注在:如何开发一个真正的可以被 seed.js 加载的 广告 sdk 即可。
那么,如何能让真正的广告 sdk 开发更有效率呢? 我的期望是这样的:
我期望如上图,每一个广告组件是一个标准的目录结构。如上图绿色部分是一个广告组件,红色部分是另外一个广告组件。每个广告组件都有固定的编写模式和规范,包括:
- index.html 是本地调试的 demo 页面
- img 存放图片资源
- jsapi.js 放置工具函数
- main.js 是你广告 sdk 的执行入口
- style.scss 是样式代码
- template.art 是你广告 dom 的模板
其中 main.js 会被 webpack 编译,并打包成一个 bundle.js。而这个 bundle.js 就是你所开发的广告组件的 sdk,他将被 seed.js 加载并执行。
通过 webpack loader 生成主 js
问题来了。我们一个广告组件的 main.js 不可能平白无故就可以被 seed.js 加载执行,他需要有一定的配合才可以。就我目前的场景来说,我的广告 js 中的 main.js 需要如下的桩代码来完成主动向 seed.js 来注册自己:
1 | // 把当前组件注册到 seed.js 中 |
可是,总不能让广告组件的开发者每个人都记得在 main.js 底部写上这样一段代码。因此,我使用 webpack 的 loader 来实现自动给 main.js chunk 添加桩代码,loader 的实现如下:
1 | module.exports = function (source) { |
通过 webpack 插件生成资源配置表
文中开头有提到,我们的 seed.js 每次给用户返回时,都会将一个最新资源映射表放置到 seed.js 中的 RES_MAP 对象上。那么这个资源映射表是怎样形成的呢。
这里,我们可以借助 webpack 插件来将每次开发广告的同学编译或 CI 出来的最新 sdk 地址记录下来,并最终输出为一份资源映射表。
webpack 插件的实现代码如下:
1 | const pluginName = "genMetaJson"; |
最终在 meta.json 中,我们将会看到这样的结果:
1 | { |
所有文件名,广告资源名,都是按照我们广告组件开发的约定自动由 webpack 生成的。
至此,开发同学只需在接到一个广告开发需求时,打开我们的项目,新建一个对应的文件夹如 “my-ad”。按照约定创建响应的文件,开发过程中使用 npm run comp:dev
预览。开发结束后走 CI,CI 执行 npm run comp:build
生成资源映射表。然后我们将映射表配置到 seed.js server 即可。
配合上 CI 流水线的话,就会更加简便了:
sdk 的加载方式
最后我们再来思考下广告 jssdk 交给对方页面引用时,最好是用何种方式引用呢?
我们可以这样思考:对于业务来说,页面的核心诉求是保证基本功能的使用。其次才是统计和广告等附加需求。
因此,在业界统计和广告 jssdk 通常尽量采用异步的方式来加载,例如百度提供的异步加载方式:
1 | <script> |
这种方式类似于 script 标签的 async 属性的功能,可以让 js 脚本的加载和执行不阻塞当前脚本所在位置的 html dom 树构造和渲染。
因此,我也建议在我们开发各类 jssdk 之后,交给用户使用时,可以建议对方使用类似上面这样的 async 加载方式,从而最大限度的降低对用户页面的性能影响。