CloudBase Framework丨第一個 Deno 部署工具是如何打造的?

用心做開發發表於2020-08-26

雲端一體化部署工具 CloudBase Framework (簡稱 CBF)自開源釋出以來迭代迅速,不僅支援 Vue、React 等前端框架,也支援 Nuxt 等 SSR 框架,基於 Node 開發的應用如 Express、Koa 等也可以一鍵託管。除此之外,藉助底層 Serverless 雲應用的能力,也可以部署其他後端的應用(PHP、Java、Go 等),值得一提的是可以部署 Dart Server,可以配合 Flutter 實現 Dart 語言的雲端一體化,這也是國內雲廠商對 Dart 語言和生態的一大補充。

現在,CloudBase Framework 已支援部署Deno,可能是首個支援部署Deno的前後端一體化部署工具!下面就來介紹下 Deno 外掛的開發流程。

Deno是基於V8引擎和Rust語言所建立的JavaScriptTypeScript執行環境,由Node.js的原始開發者Ryan Dahl所創造,目前 github star 66.7k+。

來自 justjavac 大神的點贊

開發準備

雲開控制檯:https://console.cloud.tencent.com/tcb

當 CloudBase Framework 正式推出後,一直覺得 Deno 和雲開發應該是絕配,所以嘗試為其貢獻了 Deno 外掛與模板,並調研感受了下 Deno 開發過程。

相關產出:

開始著手 deno 外掛開發時,CloudBase Framework 外掛開發的文件暫缺,不過好在其他外掛程式碼清晰易懂,可以參考其他外掛進行開發。

考慮到 deno 執行狀態,應該就是需要打通容器部署環節,於是根據 CloudBase Framework 作者建議,參考了 framework-plugin-nodeframework-plugin-dart 兩款外掛的程式碼來進行開發。

整個 CloudBase Framework deno 外掛開發,主要需要編寫程式碼的檔案就 3 個:

調研基本示例

由於需要進行容器部署,所以在 dockerhub 找了個 docker image aredwood/deno 作為參考映象進行改造。來編寫 CloudBase Framework 外掛所需 的 Dockerfile 。

為方便驗證 Dockerfile 和 deno 應用如何整合,構建了一個簡單專案來驗證映象構建流程:deno-docker

deno 生態有一個類似 node koa 的應用框架 oak 直接使用它的官方示例,存為一個 entry.ts ,很快就完成了本地示例的搭建。執行示例也非常簡單 deno run entry.ts

外掛開發

接下來考慮如何部署的問題,開始開發 CloudBase Framework deno 外掛,src/index.ts 主要需要提供一個外掛類給 CloudBase Framework 命令列元件使用。這個類需要繼承自 @cloudbase/framework-core 的 Plugin。

參考其他外掛寫法,Plugin 是抽象類,需要自行實現抽象類的各個方法。其中在 build 方法中,需要構建中間產物,主要是編譯過後的 Dockerfile 和需要包裝到映象的檔案,然後通過 framework-plugin-container 提供 docker container 構建產物。

import { plugin as ContainerPlugin } from '@cloudbase/framework-plugin-container';
/*** code:other ***/
class DenoPlugin extends Plugin {
  /*** code: 初始化處理 ***/
  async build() {
    // 構建 deno 中間產物
    this.buildOutput = await this.denoBuilder.build(
      this.resolvedInputs.projectPath || '.',
      { /*** code: 給 buider 提供選項 ***/ }
    );

    // 提供 containerPlugin 物件
    const container = this.buildOutput.containers[0];
    this.containerPlugin = new ContainerPlugin(
      'container',
      this.api,
      resolveInputs(
        { localAbsolutePath: container.source },
        this.resolvedInputs
      )
    );

    // 構建 container 最終產物
    await this.containerPlugin.build();
  }
  /*** code: other ***/
}

而 deploy 方法看來主要是在部署之後,提供最終部署結果的日誌呈現。參考其他 2 個外掛,大部分程式碼改動主要用來做配置項的處理和日誌的區別,整體與其他外掛相比,改動不大。

class DenoPlugin extends Plugin {
  async deploy() {
    /*** code: 日誌處理 ***/
    // 實際部署能力呼叫
    await this.containerPlugin.deploy();
    await this.denoBuilder.clean();
    /*** code: 日誌處理 ***/
  }
}

src/builder.ts 中,主要擴充套件 Builder 類,提供中間產物構建方法。其中 build 方法,參考其他外掛,給出容器構建所需的固定返回即可。

import { Builder } from '@cloudbase/framework-core';
/*** code: other ***/
export class DenoBuilder extends Builder {
  /*** code: 初始化 ***/
  async build(localDir: string, options: BuilderBuildOptions) {
    /*** code: 選項處理,路徑處理 ***/
    // 生成中間產物需要呼叫的方法
    await Promise.all([
      this.generator.generate(
        path.join(__dirname, '../assets'),
        appDir,
        spec
      ),
      fs.copy(path.join(projectDir, localDir), appDir),
    ]);

    // 對於容器部署,是固定的返回
    return {
      containers: [
        {
          name: containerName,
          options: {},
          source: appDir,
        },
      ],
      routes: [
        {
          path: options.path,
          targetType: 'container',
          target: containerName,
        },
      ],
    };
  }
}

this.generator.generate 方法呼叫時,Dockerfile 會作為 ejs 模板被進行編譯,傳遞的選項將會作為編譯引數。結合這個能力,可以實現 docker image 的精細配置。

本地部署除錯

除錯 CloudBase Framework deno 外掛時,需參考 cloudebase-framework 貢獻指南 提供的本地除錯流程。

本地需要部署的程式碼,需要提供一個 cloudbaserc.json 作為部署配置。如果是開發模板,需要配置屬性 "envId": "{{envId}}"cloudbaserc.json 參考 CloudBase Framework 配置文件 來配置屬性。其中 inputs 屬性將作為引數傳遞給外掛。

以我個人模板除錯為例,外掛編寫完畢後,需要在外掛目錄執行 npm run build 編譯外掛程式碼。然後在 cloudbase-framework 根目錄執行 npm run link 實現外掛的本地指向。最後在模板目錄執行 CLOUDBASE_FX_ENV=dev cloudbase framework:deploy -e test-1gxe3u9377a09734 來進行部署。

test-1gxe3u9377a09734 為我個人的 envId,將會替換 cloudbaserc.json 中的 "{{envId}}" 部分。

deno 開發體驗

開發

deno 可以直接執行 typescript,示例程式碼跑在開發模式,報錯時可以直接看到清晰的呼叫棧,這彌補了 typescript 在 node 開發中的弊端。好感度 +1 !

部署

初次部署時經常碰到部署失敗,經過溝通與除錯,發現問題主要出在 docker image 編譯和 app 應用執行環節中,由於網路環境問題,部分遠端檔案未能成功載入或者快取。

再次審視 deno 專案介紹與說明,發現最佳實踐是進行本地打包(或者 ci 打包)後提供無依賴的入口檔案。

deno 提供了 deno bundle 命令,可以將程式碼打包為一個 js 檔案來執行。然後找到 denon 這個工具,直接解決了開發部署配置問題,其類似 nodemon 。舒服的是,包括 deno 應用的執行許可權,環境變數,都可以在它的配置檔案中配置。所以直接修改了 CloudBase Framework deno 外掛,使用 denon 來提供啟動應用能力。

使用先打包,後部署的方案後,雲開發部署 deno 應用的成功率大幅上升。

依賴

值得一提的是,雖然示例應用簡陋,但是依然能感受到 deno 打包執行流暢易用。好感度 +1!

脫離了 node_modules 這層設計,deno 內建的打包部署這方面的體驗遠超 node 開發。本地應用開發設計時,推薦使用固定版本的檔案引用方式,這樣可以避免依賴更新導致的應用 bug。

/* @see https://github.com/oakserver/oak/blob/main/application.ts */
import { reset } from "https://deno.land/std@0.62.0/fmt/colors.ts";

模板引擎

在使用 dejs 模板時,發現示例中的 cwd() 不能使用。

(async () => {
  const output = await renderFile(`${cwd()}/views/main.ejs`);
  await copy(output, stdout);
})();

需要改為 Deno.cwd()

(async () => {
  const output = await renderFile(`${Deno.cwd()}/views/main.ejs`);
  await copy(output, stdout);
})();

而巢狀模板程式碼直接報錯,只提示檔案未找到,卻並未給出更詳細提示。

<%- await include('views/header.ejs') %>
<h1>hello, world!</h1>
<%- await include('views/footer.ejs') %>

反覆除錯後發現,需改為:

<%- await include(`${Deno.cwd()}/views/header.ejs`) %>
<h1>hello, world!</h1>
<%- await include(`${Deno.cwd()}/views/footer.ejs`) %>

IO

在 deno 應用中,使用 fetch 方法獲取遠端資源時,該方法與瀏覽器規範實現一致,使用起來莫名親切。由於 deno 預設直接讀取了環境變數的 http_proxy,node 開發中碰到的內網代理配置問題,在 deno 開發中也不再存在。好感度 +1 !

總結

聯絡到 Deno 的願景是設計一款服務端執行的瀏覽器,忽然有了一些大膽的想法,想來在 SSR、測試、Web資源編輯與建立方面,Deno 未來可能會有一些獨到的優勢。

總體來說,即便 Deno 並非 Node 的替代者,依靠其順滑的開發部署體驗,未來極有可能分走 Node 相當一部分使用場景。而這個專案在 github 上的 star 數量,與社群參與人數的快速上漲,也證明其具有相當大的潛力。

Deno is coming!

Deno is coming!

參與貢獻

  • 積極參與 Issue 的討論,如答疑解惑、提供想法或報告無法解決的錯誤;
  • 撰寫和改進專案的文件;
  • 提交補丁優化程式碼;
  • 認領待辦任務中的事項。

相關文章