一种小拖大的jssdk加载方案

背景

jssdk 是在前端中完成某些业务功能的 JavaScript 函数库,一般由 sdk 的开发者开发完毕后,交给业务的页面来引入使用。例如:css

<head>
    <script src="//hm.baidu.com/hm.js?XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"></script>
</head>

在一些特殊的场景(例如联盟广告)下,咱们一般须要把一个 jssdk 地址 交付给另一个团队的页面来引入。对于大型广告联盟商来讲,通常是提供本身的联盟广告平台,在平台上,开发者能够申请到广告appid,并按照文档引入广告 sdk 到本身的页面中使用。一切比较顺利。html

但在公司内部,我所在的商业化部门并无造成如此成熟的平台。这时咱们采用的是比较原始的办法,我所在的商业化部门要把广告sdk开发完成后,部署到cdn。而后咱们部门将 cdn地址如 “http://a.b.c/12345.js” 告知对方业务部门的相关开发,让对方放置个人js到对方的页面当中。每次开发、测试、部署,咱们的js资源地址必然会发生变化,结果就是每次都要找对方部门沟通协调部署问题,部署成本巨大。在没有良好的机制协调下,每每会形成开发和测试发布成本上升,效率低端低下。前端

为了解决该问题,我在现状的基础上,设计了一种“小拖大”的jssdk加载方案,完全解决对对方部门的依赖。webpack

历史背景

先看下现状咱们的工做模式是怎样的:git

image.png

我,做为广告团队的开发,要么是把广告组件作成npm包交给对方;要么是把广告作成js放cdn交给对方。每一种方式都要找对方联调和沟通,npm包的方式对方还要编译到对方业务中,其成本和出错的几率更大。最蛋疼的是,第二种cdn交付方式,每次通知对方后,对方须要去后台配置一下我给他的js地址,而后他下发后,客户端浏览器才会真正的请求最新的js地址。web

工做模式画成时序图,以下:npm

image.png

其缺点比较明显:json

  • 架构上: 不符合如今分团队的开发模式
  • 流程上: 多了冗余的沟通,例如找后台同窗配置
  • 技术上: jssdk下发方式不够标准,不够原生,不够灵活

抛出问题

如何可以减小依赖,下降沟通成本呢。其实最简单的方法就是让对方引入一个固定的js地址就行了。目标很明确!后端

即,我指望实现JSSDK,在不依赖页面方的状况下自更新?api

技术方案

咱们最容易想到的方案即是:给对方一个固定js地址,每次咱们更新的广告代码,咱们就在此地址上更新js。但这样的话,有几个问题:

  • 咱们的广告组件便没有了版本的概念,回滚时只能回滚git
  • 咱们的广告jssdk,完全没有了缓存。若是咱们的jssdk体积增大,那么用户每次打开页面都要下载一个大js
  • 也没法利用cdn就近的优点

基于这种考量,我设计了一种 “小拖大” 的方案,这种方案放弃了20%的缓存能力,但能保留住80%的缓存能力。用20%的缓存放弃,换来开发效率极大的提高,对于广告场景来讲是比较适合的,由于广告并非一个页面中最核心的性能诉求,页面最关键的是基本功能的性能和展现,其次才是广告的正常展现和渲染,所以广告适当少许的延迟并不会有太大的影响。

如下是我对几种方案的对比图:image.png

因而,一个新的更适应我当前场景的jssdk加载方案,其时序图是这样的:

image.png

文字描述一个完整的首次广告请求以下:

  • 首先将咱们的 “种子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 的能力,便可以让咱们在代码中书写:

import('abc.js')

这样的代码。而后浏览器中加载时,会动态远程加载并将abc.js的导出做为本地webpack的一个模块来使用。

这个思路就有点相似于咱们本文所述的 seed.js 要完成的功能,所以咱们来看看它webpack是如何实现动态 import 的:
image.png

其底层逻辑仍是比较简单的。

实现简单版本

可是个人seed.js并不想实现的那么重,也不须要有 require loader 这样的概念。

  1. webpack 有chunk概念,对咱们来讲我用不到。
  2. 须要主调模块中写明被调组件名和哈希地址,他是在编译期实现进行代码分割。而个人seed.js但愿简化逻辑且不该该存在调用代码,且要支持后端任意动态新增组件。

因而,我在此基础上实现了一个更适用于本场景的简单的版本。其大概逻辑以下:

  1. 我在服务端会将当前的 "广告资源cdn地址映射表" 插入到下图的 RES_MAP 这个对象当中。

image.png

  1. 实现一个generate函数,等待对方业务调用

image.png
该函数的功能是:当对方业务调用 generate('ArticleAd') 这样的函数时,则意味是要建立并初始化一个 ArticleAd 的广告,那么seed.js须要去主动加载 ArticleAd 广告的js资源,并完成初始化。
其中 _loadModule 函数会去 RES_MAP映射表中寻找资源地址,并完成js资源加载和内存缓存(防止屡次调用generate)
image.png

如何给开发者屏蔽开发细节

有了 seed.js 去负责加载真正的广告js。那么,咱们广告开发者的工做只需关注在:如何开发一个真正的能够被 seed.js 加载的 广告sdk便可。

那么,如何能让真正的广告sdk开发更有效率呢? 个人指望是这样的:

image.png

我指望如上图,每个广告组件是一个标准的目录结构。如上图绿色部分是一个广告组件,红色部分是另一个广告组件。每一个广告组件都有固定的编写模式和规范,包括:

  • 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 来注册本身:

// 把当前组件注册到 seed.js 中
      (function(root) {
        if (root && root.tnfa && root.tnfa.cache && !root.tnfa.cache[{{comp-name}}.compName]) { // 最后一个条件是防止用户屡次调用屡次并行load,会把cache中的组件类替换掉
          root.tnfa.cache[{{comp-name}}.compName] = {{comp-name}}
        }
      })(window)`;

但是,总不能让广告组件的开发者每一个人都记得在 main.js 底部写上这样一段代码。所以,我使用 webpack 的 loader 来实现自动给 main.js chunk 添加桩代码,loader 的实现以下:

module.exports = function (source) {
  if (/src[\/\\]ads-comp[\\\/]([-\w])+[\/\\]main\.js/.test(this.resourcePath)) {
      // 若是是组件入口,则添加注册代码
      const code = `
      // 把当前组件注册到 seed.js 中
      (function(root) {
        if (root && root.tnfa && root.tnfa.cache && !root.tnfa.cache[{{comp-name}}.compName]) { // 最后一个条件是防止用户屡次调用屡次并行load,会把cache中的组件类替换掉
          root.tnfa.cache[{{comp-name}}.compName] = {{comp-name}}
        }
      })(window)`;
      // 分析组件名
      const regRes = /src[\/\\]ads-comp[\\\/]([-\w]+)[\/\\]main\.js/.exec(this.resourcePath)
      const compFolder = regRes[1]
      const result = source + `\n${code.replace(/{{comp-name}}/g, upcaseFirstLetter(compFolder))}`;
      return result
  }
  return source;
}

function upcaseFirstLetter(word) {
  return  word.replace(/((^\w)|(-\w))/g, function(m) {
      return m.toUpperCase()
  }).replace(/-/g, '')
}

经过 webpack 插件生成资源配置表

文中开头有提到,咱们的 seed.js 每次给用户返回时,都会将一个最新资源映射表放置到 seed.js 中的 RES_MAP 对象上。那么这个资源映射表是怎样造成的呢。

这里,咱们能够借助 webpack 插件来将每次开发广告的同窗编译或CI出来的最新 sdk 地址记录下来,并最终输出为一份资源映射表。

webpack 插件的实现代码以下:

const pluginName = 'genMetaJson'
const path = require('path')

class GenMetaJson {
  // apply 被 webpack compiler 在打包前调用,用于注册上咱们的插件处理逻辑吧
  apply(compiler) {
    // 注册相应事件的插件处理逻辑

    compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
      const filenames = Object.keys(compilation.assets).filter(name => {
        return path.extname(name) === '.js'
      })

      let meta = {}
      filenames.forEach(name => {
        const baseName = path.basename(name)
        const preName = baseName.slice(0, baseName.indexOf('.'))
        meta[preName] = name
      })

      console.log('result', meta)
      if (meta && Object.keys(meta)) {
        meta = JSON.stringify(meta, null, '\t')
        compilation.assets['meta.json'] = {
          source: function() {
            return meta
          },
          size() {
            return meta.length
          }
        }
      }
      callback()
    })
  }
}


module.exports = GenMetaJson

最终在meta.json中,咱们将会看到这样的结果:

{
    "ArticleAd": "article-ad.2df9c7eb94554e20.js",
    "VideoAd": "video-ad.5ea7aaf787bfe15c.js",
}

全部文件名,广告资源名,都是按照咱们广告组件开发的约定自动由webpack生成的。

至此,开发同窗只需在接到一个广告开发需求时,打开咱们的项目,新建一个对应的文件夹如 “my-ad”。按照约定建立响应的文件,开发过程当中使用 npm run comp:dev 预览。开发结束后走 CI,CI执行 npm run comp:build生成资源映射表。而后咱们将映射表配置到 seed.js server便可。

配合上 CI 流水线的话,就会更加简便了:

image.png

sdk的加载方式

最后咱们再来思考下广告jssdk交给对方页面引用时,最好是用何种方式引用呢?

咱们能够这样思考:对于业务来讲,页面的核心诉求是保证基本功能的使用。其次才是统计和广告等附加需求。

所以,在业界统计和广告jssdk一般尽可能采用异步的方式来加载,例如百度提供的异步加载方式:

<script>
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "//hm.baidu.com/hm.js?XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>

这种方式相似于script标签的 async 属性的功能,可让js脚本的加载和执行不阻塞当前脚本所在位置的html dom树构造和渲染。

所以,我也建议在咱们开发各种jssdk以后,交给用户使用时,能够建议对方使用相似上面这样的 async 加载方式,从而最大限度的下降对用户页面的性能影响。

相关文章
相关标签/搜索