Stories

Detail Return Return

👋 一起寫一個基於虛擬模塊的密鑰管理 Rollup 插件吧(二) - Stories Detail

上一章 我們詳細介紹了為什麼需要一個自動化的密鑰管理方案,以及如何利用虛擬模塊機制設計並實現一個適用於 Rollup 的密鑰管理插件。

我們從需求出發,將核心的密鑰拆分還原算法獨立為 crypto-splitter 模塊,再通過 rollup-plugin-crypto-key 模塊將其接入 Rollup / Vite 的構建流程。用户只需簡單配置明文密鑰,就能實現在業務代碼中像導入普通模塊一樣獲取密鑰,同時保證構建產物中不暴露密鑰明文。

方案探索

回到上一章末尾提出的問題,我們需要讓插件提供對 TypeScript 的支持,以提升用户的開發體驗。

那麼如何為一個模塊提供類型定義呢?

查閲 TypeScript 文檔 可以知道,如果需要為一個 JavaScript 模塊提供類型定義,應該使用 .d.ts 文件讓 TypeScript 的類型系統知道這個模塊裏有什麼內容。例如:

declare module "virtual:crypto-key" {
  export const KEY1: string;
  export const KEY2: string;
}

大家可以試試手動創建這樣一個 crypto-key.d.ts 文件,並且讓 tsconfig.json 包含它,不出意外的話 TypeScript 的報錯信息就消失了,並且 IDE 也能正確提示模塊的內容。

真是一個完美的解決方案,那麼本章的內容就到此結束吧!

讓用户自己手動創建 .d.ts 文件固然能夠解決問題,但是大家都知道手動操作既低效又容易出錯,也違背了我們這個插件的初衷。

所以,我們的插件需要能夠根據密鑰配置自動生成 .d.ts 文件,接下來就一起實現這個功能吧!

編碼實現

既然要生成文件,就需要使用到文件相關的 API,大家第一時間想到的應該是 node:fs 模塊,但是今天我們使用 fs-extra 插件來完成。

先簡單介紹一下 fs-extra 插件:

fs-extra adds file system methods that aren't included in the native fs module and adds promise support to the fs methods. It also uses graceful-fs to prevent EMFILE errors. It should be a drop in replacement for fs.

fs-extranode:fs 模塊的增強版,它在完全兼容 node:fs 的基礎上,提供了更多常用且方便的文件系統操作方法,並且它的所有方法同時支持 callbackPromise

然後是我們稍後會使用到的幾個方法:

  • ensureFile(file: string)

    確保目標文件存在。如果目標文件不存在,則會自動創建文件及其父目錄。如果目標文件已經存在,則不會做任何修改。

  • outputFile(file: string, data: string | NodeJS.ArrayBufferView)

    幾乎與 fs.writeFile 相同(即會覆蓋文件)。不同之處在於,如果父目錄不存在,則會自動創建。

準備好了前置知識,就可以開始正式編碼啦!

import type { Plugin } from "rollup";
import { getCode } from "./code";
+ import { writeDeclaration } from "./declaration";

export interface Options {
+ /**
+  * 生成類型聲明文件,支持布爾值或者文件路徑
+  */
+ dts?: boolean | string;
  keys?: Record<string, string>;
}

const VIRTUAL_MODULE_ID = "virtual:crypto-key";
const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;

export default function cryptoKey(options: Options = {}): Plugin {
  const {
    keys = {},
+   dts = false
  } = options;

+ if (dts) {
+   writeDeclaration(keys, {
+     moduleId: VIRTUAL_MODULE_ID,
+     dts
+   });
+ }

  return {
    name: "crypto-key",
    resolveId(source) {
      if (source !== VIRTUAL_MODULE_ID) {
        return null;
      }

      return RESOLVED_VIRTUAL_MODULE_ID;
    },
    load(id) {
      if (id !== RESOLVED_VIRTUAL_MODULE_ID) {
        return null;
      }

      return getCode(keys);
    }
  };
}

我們添加了 dts 配置項用於控制類型聲明文件的生成,支持傳入布爾值或者文件路徑。如果 dtstrue 或者字符串路徑,插件就會調用 writeDeclaration 方法生成 .d.ts 文件到指定路徑。

接下來我們將着手 writeDeclaration 方法的實現:

// declaration.ts

import { ensureFile, outputFile } from "fs-extra";

interface DeclarationOptions {
  /**
   * 模塊名
   */
  moduleId: string;
}

interface WriteDeclarationOptions extends DeclarationOptions {
  /**
   * 聲明文件生成路徑
   * 
   * - true:默認文件路徑
   * - 字符串:自定義文件路徑
   */
  dts: true | string;
}

/**
 * 根據 dts 參數獲取聲明文件生成路徑
 */
function getDeclarationPath(dts: true | string): string {
  if (dts === true) {
    // 使用默認文件路徑
    return "crypto-key.d.ts";
  }

  // 使用傳入的自定義文件路徑
  return dts;
}

/**
 * 根據密鑰表生成聲明文件的內容
 */
function getDeclarationCode(keys: Record<string, string>, options: DeclarationOptions): string {
  return `declare module "${options.moduleId}" {
${
  Object.keys(keys)
    .map((it) => {
      return `  export const ${it}: string;`;
    })
    .join("\n")
}
}`;
}

/**
 * 根據密鑰表生成類型聲明文件到磁盤
 */
export async function writeDeclaration(
  keys: Record<string, string>,
  options: WriteDeclarationOptions
): Promise<void> {
  // 獲取聲明文件路徑
  const path = getDeclarationPath(options.dts);

  // 確保聲明文件存在
  await ensureFile(path);

  // 寫入聲明文件內容
  await outputFile(path, getDeclarationCode(keys, {
    moduleId: options.moduleId
  }));
}

首先,根據傳入的 dts 參數獲取文件生成路徑,如果為 true 就在根目錄生成 crypto-key.d.ts 文件,否則根據傳入的自定義路徑生成。然後,遍歷密鑰映射表逐一生成 export const 語句到 declare module 塊中併合成類型聲明文件內容。最後,將類型聲明文件實際寫入到磁盤,也就完成了我們期望的目標。

插件使用

又到了激動人心的時刻,開始體驗我們為插件添加的新功能吧!

// vite.config.(js|ts)

import CryptoKey from "rollup-plugin-crypto-key";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    CryptoKey({
+     dts: true,
      keys: {
        DEMO_KEY1: "iamxiaohe",
        DEMO_KEY2: "ilovexiaohe"
      }
    })
  ]
});

只需要在配置項中添加上 dts 的配置,插件就可以自動完成類型聲明文件的生成工作。然後讓 tsconfig.json 包含聲明文件,TypeScript 的類型系統就能準確知道 virtual:crypto-key 模塊中的內容,IDE 也能順利完成代碼提示功能啦!

下面以默認路徑為例:

// tsconfig.json

{
  "include": [
    // ...
+   "crypto-key.d.ts"
  ]
}

至此,我們成功為插件實現了對 TypeScript 的支持,讓用户的開發體驗得到了保障!讓我們一起為自己點個贊 👍 吧!

源碼

插件的完整代碼可以在 virtual-crypto-key 倉庫中查看。贈人玫瑰,手留餘香,如果對你有幫助可以給我一個 ⭐️ 鼓勵,這將是我繼續前進的動力,謝謝大家 🙏!

下一步

現在我們的插件已經可以在兼容 Rollup 的環境(Rollup、Rolldown、Vite)中順利使用,雖然 Vite 現在是大勢所趨,越來越多的項目都基於 Vite 開發,但是仍然有大量的項目使用其他的構建工具(Webpack、Rspack、Esbuild 等),那麼我們能不能同時支持更多的構建工具呢?

可是,如果為每種構建工具都單獨去寫一個插件,這樣會在一定程度上增加工作量和維護成本,所以有沒有一種工具可以讓我們的插件使用一套代碼就能夠適用於多種構建工具的插件系統呢?

Unjs 團隊的 Unplugin 項目就實現了這樣一個統一的插件系統,能夠同時支持 Vite、Rollup、Webpack、Esbuild 等構建工具。

為了讓我們的插件能夠被更多的用户使用,所以下一章我們將會一起將插件遷移到 Unplugin 以支持更多的構建工具!

user avatar tianmiaogongzuoshi_5ca47d59bef41 Avatar alibabawenyujishu Avatar aqiongbei Avatar yelloxing Avatar xiaoxxuejishu Avatar guixiangyyds Avatar wszgrcy Avatar libubai Avatar ldh-blog Avatar Poetwithapistol Avatar laggage Avatar shuyuanutil Avatar
Favorites 56 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.