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
我,做爲廣告團隊的開發,要麼是把廣告組件作成npm包交給對方;要麼是把廣告作成js放cdn交給對方。每一種方式都要找對方聯調和溝通,npm包的方式對方還要編譯到對方業務中,其成本和出錯的機率更大。最蛋疼的是,第二種cdn交付方式,每次通知對方後,對方須要去後臺配置一下我給他的js地址,而後他下發後,客戶端瀏覽器纔會真正的請求最新的js地址。web
工做模式畫成時序圖,以下:npm
其缺點比較明顯:json
如何可以減小依賴,下降溝通成本呢。其實最簡單的方法就是讓對方引入一個固定的js地址就行了。目標很明確!後端
即,我指望實現JSSDK,在不依賴頁面方的狀況下自更新?api
咱們最容易想到的方案即是:給對方一個固定js地址,每次咱們更新的廣告代碼,咱們就在此地址上更新js。但這樣的話,有幾個問題:
基於這種考量,我設計了一種 「小拖大」 的方案,這種方案放棄了20%的緩存能力,但能保留住80%的緩存能力。用20%的緩存放棄,換來開發效率極大的提高,對於廣告場景來講是比較適合的,由於廣告並非一個頁面中最核心的性能訴求,頁面最關鍵的是基本功能的性能和展現,其次纔是廣告的正常展現和渲染,所以廣告適當少許的延遲並不會有太大的影響。
如下是我對幾種方案的對比圖:
因而,一個新的更適應我當前場景的jssdk加載方案,其時序圖是這樣的:
文字描述一個完整的首次廣告請求以下:
new ArticleAd()
因爲cdn上的真正的廣告js是強緩存的,所以用戶在大部分狀況下,都將會使用本地緩存的廣告jssdk。惟一的缺點是 seed.js 是須要每次都發起請求的(因爲廣告不會每小時都在更新,所以這裏也能夠將 seed.js 設置爲強緩存1天或1小時)。
因爲 seed.js 核心代碼僅僅有不到100行,所以其體積微乎其微,加載時間也很是的快。
下面咱們來看整個架構中比較核心的 seed.js 是如何實現的。這裏要考慮以下一些問題:
種子js 並非一個靜態的 js,因爲它須要內置一個最新版本的資源映射表。所以他是由 server 端動態來生成的,咱們 server 端能夠採用 Node.js 配合模板引擎來實現。
webpack 中有個 動態 import 的能力,便可以讓咱們在代碼中書寫:
import('abc.js')
這樣的代碼。而後瀏覽器中加載時,會動態遠程加載並將abc.js的導出做爲本地webpack的一個模塊來使用。
這個思路就有點相似於咱們本文所述的 seed.js 要完成的功能,所以咱們來看看它webpack是如何實現動態 import 的:
其底層邏輯仍是比較簡單的。
可是個人seed.js並不想實現的那麼重,也不須要有 require loader 這樣的概念。
因而,我在此基礎上實現了一個更適用於本場景的簡單的版本。其大概邏輯以下:
該函數的功能是:當對方業務調用 generate('ArticleAd') 這樣的函數時,則意味是要建立並初始化一個 ArticleAd 的廣告,那麼seed.js須要去主動加載 ArticleAd 廣告的js資源,並完成初始化。
其中 _loadModule 函數會去 RES_MAP映射表中尋找資源地址,並完成js資源加載和內存緩存(防止屢次調用generate)
有了 seed.js 去負責加載真正的廣告js。那麼,咱們廣告開發者的工做只需關注在:如何開發一個真正的能夠被 seed.js 加載的 廣告sdk便可。
那麼,如何能讓真正的廣告sdk開發更有效率呢? 個人指望是這樣的:
我指望如上圖,每個廣告組件是一個標準的目錄結構。如上圖綠色部分是一個廣告組件,紅色部分是另一個廣告組件。每一個廣告組件都有固定的編寫模式和規範,包括:
其中main.js 會被webpack編譯,並打包成一個 bundle.js。而這個 bundle.js就是你所開發的廣告組件的sdk,他將被seed.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, '') }
文中開頭有提到,咱們的 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 流水線的話,就會更加簡便了:
最後咱們再來思考下廣告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 加載方式,從而最大限度的下降對用戶頁面的性能影響。