How to create JavaScript web sdk

How to create JavaScript web sdk

February 11, 2024

·

11 min read

去年在公司做了一個滿好玩的專案,要建立一個 JavaScript tracking sdk,透過客戶的 google tag manager 安裝,收集客戶的 user 使用資料,能做到部署 code 後,每個客戶都自動更新成最新的版本,額外要做到版本控制,讓部分客戶選擇安裝舊版的 tracking sdk,uglify code 不讓邏輯被輕易了解,至於後面業務細節就不提太多了。

後面會分享 JavaScript sdk 我是如何規劃、建立這個專案的,然後在把整個想法實作出來。

Planning

先從最基本的 coding tool 來想,現況前端都會要求程式碼要有型別,日後維運會方便很多,所以要有 TypeScript,再來是 bundler,打包程式碼並且 minify 加上 uglify,能減少 code file size,不要讓 code 業務邏輯輕易被看光,這邊我選用 rollup,理由是搭配 typescript setup 容易,有 tree shaking 來減低打包檔案大小,提供的 plugin 也足夠使用。

再來就是 deploy 要 deploy 到哪裡,這邊選擇相對多,可以是 s3、gcs 或是 github hosting,公司專案我是選用 gcs,因為專案都是用 google cloud 的,另外用 cloud storage 還可以額外 setup meta header。

還有 CI/CD 要能自動化 deploy,避免手動作業流程,再來還有 javascript 搭配 cdn,提高我們 file 被載入的速度,順便還可以處理 custom domain。

也要有清除 cdn cache 的機制,當更新版本後,能讓客戶快速取得最新版本的 file。

所以這邊我需要有 bundler、build version control、CI/CD deploy、hosting javascript file、CDN setup。

createlibrary flow
createlibrary flow

Develop Setup

這邊就不寫上介紹,直接列出步驟,主要都是設定 project 的工具,從最基本的 eslint、prettier、husky、jest、typescript 這些,可以直接看我的 sample project 是怎樣設定的

  • Basic setup project (install develop environment)
mkdir create-javaScript-sdk
cd create-javaScript-sdk
npm init

// setup eslint. select env browser, npm and typescript
npm init @eslint/config

// install lint-staged
npm install -D prettier lint-staged jest

// install husky
npx husky init

// install rollup typescript
npm install -D typescript

// init typescript config
npx tsc --init

// install jest
npm install -D jest
npm init jest@latest
npm install -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript @jest/globals ts-jest jest-environment-jsdom ts-node

其餘設定 husky (.husky/pre-commit)、jest 跟 prettier 的 config,再麻煩幫我看 git repo 了,篇幅真的有點太長,我不想花太多篇幅在寫設定專案。官方插件都查得到教學,工具依序是 typescript、eslint、prettier、babel、lint-staged、husky、jest。比較不一樣會有個人化的會是 linter、prettier,在各自修改自己喜歡的 coding style。

改完之後可以 npm lint-staged 看看能不能正常跑 linter,通常都會卡在 jest 處理不了 typescript,解法就是 jest transform 要設定 '^.+\\.ts?$': 'ts-jest'。建議直接 clone 下面 sample repo 比較快XDD,我整個手動一個一個安裝,跑完加上 debug 也花了一點時間,另外如果我 repo 上的插件太舊再記得 npm update

Github Repo sample

Bundle Setup

這邊我們是選用 rollup,因為實際 build 出來的 file size 遠比 webpack 還小很多,開發 plugin 也足夠讓我們使用。

  • install bundler rollup
// install rollup
npm install -D rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-babel @rollup/plugin-terser rollup-plugin-version-injector
  • Add script on package.json
script: {
  ...
  "build-dev": "rollup -c -w",
  "build": "rollup -c --prod",
}

我在設定卡了滿久的時間,大概 1、2 小時,不知道怎設定 rollup config 讓他吃到 config.ts,就算強制用 rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript 反而會卡在 tsconfig 各種沒有 apply 到…,索性直接把 file type 改成 mjs,rollup 就會自己 apply 到了。

網路上也滿多人也在 rollup version 4 上遇到一樣問題,也沒看到什麼很好的解法。

Error: [!] TypeError: Unknown file extension ".ts" for /Users/ian/Documents/create-javaScript-sdk/rollup.config.ts

強烈建議大家要用 format: iife,這是實際踩過的血淚經驗,iife 是立即函示的意思,這代表會讓我們作用域封裝起來,確保我們在這個 sdk 內用得的變數都是內部變數,而不會被外部所覆蓋污染。因為 bundle 後,變數、function name 都會被 uglify 變成 a, b, c,這就很容易跟其他人的 bundle file 衝突到。

rollup-plugin-version-injector 則是幫我們把 version inject 到 file 上,方便我們檢查檔案的版本,@rollup/plugin-terser 則是 minify code 減少 file size。

都完成之後就可以嘗試 npm run build 來檢查是否有建立 file 到 dist folder 內。

  • create rollup.config.mjs
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import rollupTypescript from 'rollup-plugin-typescript2';
import babel from '@rollup/plugin-babel';
import { DEFAULT_EXTENSIONS } from '@babel/core';
import terser from '@rollup/plugin-terser';
import versionInjector from 'rollup-plugin-version-injector';

import pkg from './package.json' assert { type: 'json' };

const folder = './dist/';

const config = (prod) => {
  return {
    input: 'src/index.ts',
    output: [
      {
        file: folder + pkg.main.replace('.js', `-dev.js`),
        format: 'iife',
        strict: false,
      },
      ...[
        prod && {
          file: folder + pkg.main,
          format: 'iife',
          strict: false,
        },
        prod && {
          file: folder + pkg.main.replace('.js', `-v${pkg.version}.js`),
          format: 'iife',
          strict: false,
        },
      ].filter((v) => v),
    ],
    plugins: [
      resolve(),
      commonjs(),
      rollupTypescript(),
      babel({
        exclude: 'node_modules/**',
        extensions: [...DEFAULT_EXTENSIONS, '.ts'],
        presets: ['@babel/preset-env'],
      }),
      terser(),
      versionInjector(),
    ],
  };
};

export default (args) => {
  const prod = !!args.prod;
  return config(prod);
};

Github Repo rollup

CI/CD Setup

前面就大致上完成了開發環境的設定了,剩下其實都滿簡單的,我們再來是 CI/CD 的設定,我們就直接以 github action 來當範例好了,我們希望能在 push release tag 的時候去 build file,並且 upload 到 cloud storage,為了方便大家直觀這邊就直接用 github hosting js file。

首先 create .github/workflows/deploy.yml,開始設定我們的 github action yaml,這邊就直接用 gh-pages 來 upload dist files 到另一個 github repo。

package.json 也要在 script 加上 deploy:ci: gh-pages -d dist -r https://[email protected]/ianccy/sample-javascript-sdk.git

ps.記得到 github Setting 建立一個 secret token,然後設定到 repo 的 setting 上面。

這邊我也卡了一下,原本我是想嘗試用 github action,不過多半都是專注在 upload assets 到指定的 repo,這跟我們需求差滿多的,我們也需要同時依賴 github hosting resource。

  • deploy.yml
name: release the javascript sdk

on:
  release:
    types: [created]

env:
  GH_TOKEN: ${{ secrets.GH_TOKEN }}

jobs:
  release-code:
    runs-on: ubuntu-latest
    steps:
      - name: GitHub Config
        run: |
          git config --global user.email "[email protected]"
          git config --global user.name "ianccy"
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          registry-url: https://registry.npmjs.org/
      - name: Install dependencies
        run: npm install
      - name: Unit-Test
        run: npm run test
      - name: Build Code
        run: npm run build
      - name: Upload to GitHub Repository
        run: npm run deploy:ci
  • add deploy:ci on package.json
  script: {
    ...
    "deploy:ci": "gh-pages -d dist -r https://[email protected]/ianccy/sample-javascript-sdk.git"
  }

接下來設定 custom domain

再來到 sample-javascript-sdk repo 去設定 custom domain,我是用 js-sdk-sample.iannccy.com,接下來到 cloudflare 去設定 dns 指向 github,幾乎就可以說是大功告成哩。

createlibrary cname
createlibrary cname

JavaScript SDK

再來補上 purge cache 在 CD flow,確保我們每一次部署上去的 file 都會 purge cdn cache,確保客戶可以盡快拿到最新版的 js file。

同時需要設定對應的 secrets 到 github repo 上。 github action guide

  • deploy.yml
      - name: Purge cache
        uses: jakejarvis/cloudflare-purge-action@master
        env:
          CLOUDFLARE_ZONE: ${{ secrets.CLOUDFLARE_ZONE }}
          CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
          CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
          CLOUDFLARE_KEY: ${{ secrets.CLOUDFLARE_KEY }}
          PURGE_URLS: '["https://js-sdk-sample.ianccy.com/index.js"]'

Push Tag and Create Release with Tag

接下來就是 create tag ,再到 github repo 去建立 release,就會觸發 github action 執行 CI/CD 了。可以點開 action 來查看每個步驟是不是有正確執行完畢。

github action result

github repo

心得

起初是想寫我怎樣規劃設計這整個架構,也包含怎儲存資料,但礙於一些 nda 因素,不方便講太多細節,再說我覺得每一個步驟怎實作也很重要,兩者相衡之下,就選擇介紹 setup 架構了。因為我當時在網路上找不到相關教學,每個步驟都是零散的拼湊出來的…,我當時大概花了 2~3 天規劃然後寫完,想拋磚引玉寫一篇 setup 的文章、sample repo。

至於我是如何選擇工具,其實想法很簡單,費用成本 > 效能 > 維運,我是選擇用 gcs,可以讓我客製化 meta header 很方便,再來搭配 cloudflare cdn cache 幾乎沒什麼成本,不選擇 webpack 是因為效能差異太大了,真的是盡可能減少 file size。維運問題上,rollup 可能雷相對多一點點,社群相對小一點,但這就是取捨了,rollup 的優點遠大於缺點。

整個來說設定 repo 開發環境跟 github cloudflare 之間連動比較麻煩。

一樣有問題歡迎詢問,打錯的地方也歡迎更正我。

最後 murmur 一下,最近都在學 python 還有後端,所以 blog 文章嚴重拖稿中…,待寫文章堆積如山。然後久久沒寫 blog 文筆超爛 QQ。