TypeScript 實戰:製作 npm Package

前言
在開發專案時,經常會遇到跨專案共用邏輯或工具的情況,例如常用的 logger 或驗證工具。如果每次都從一個專案複製貼上到另一個專案,難免會出現版本不同步或維護困難的問題。為了解決這類問題,我們可以將這些共用邏輯獨立成為 npm 套件,以便在多個專案中重複使用。
以我們之前實作的前後端範例為例,後端已經實作了驗證 Task 資料的 schema。如果我們希望在前端發送請求前,也對使用者的輸入進行驗證,有許多解決方案,其中一個做法是將這些驗證邏輯封裝成 npm 套件,並且支援 CommonJS (CJS) 和 ECMAScript 模組 (ESM) 兩種格式,以確保前後端都能使用相同的工具。
初始化專案
首先初始專案起手式
npm init -y接著,安裝我們這次開發目標所需要的依賴套件。
npm i -D @types/node typescript rimraf
npm i --save-peer zod在這裡,我們安裝了 rimraf 作為開發依賴,用來在不同平台上統一刪除檔案或資料夾。此外,我們還將 zod 加入 peerDependencies,這表示我們預期使用這個套件的開發者自己會安裝 zod,並直接使用他們項目中的 zod 版本。
這樣的設置非常常見,例如當我們開發與 React 相關的套件時,我們也會將 React 安裝到 peerDependencies,讓使用者能自行管理 React 版本,而不會與我們的套件發生衝突。
TypeScript 配置
因為執行 TypeScript 編譯時,一次只能產生一個輸出。要產生 CommonJS 和 ESM 程式碼,就會需要設定針對這兩個的設定檔。
共用配置
首先,我們先配置共用的基本配置:
{
"compilerOptions": {
"lib": ["ESNext"],
"declaration": true,
"declarationDir": "./dist/types",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"baseUrl": ".",
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["dist", "node_modules"]
}該檔案指定了程式碼的位置、要排除的資料夾以及類型定義的輸出目錄等。
CommonJS 配置
針對 CommonJS,我們將輸出位置配置如下:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist/cjs",
"target": "ES2020"
}
}ESM 配置
針對 ESM,我們將輸出位置配置如下:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "ES6",
"outDir": "./dist/esm",
"target": "ES2016"
}
}Package 配置
設定完 TypeScript 後,我們來配置 package.json。
name:用來定義我們套件產生出來的名稱
main:我們 Node.js 主要執行的入口點
types:我們 Node.js 主要定義型別的檔案
files:選擇我們打包好的套件根資料夾
配置入口點
{
"name": "validator",
"version": "1.0.0",
"description": "",
"main": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts",
"files": [
"dist"
]
// ...
}配置模組匯出
由於我們要同時支援 CommonJS 和 ESM,所以需要設定模組的匯出方式:
{
// ...
"exports": {
".": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts"
}
}
// ...
}這裡我們為 ESM 模組使用 .mjs 副檔名,稍後將會撰寫腳本來替換編譯後的 .js 改為 .mjs。
撰寫 Scripts
接著,我們撰寫打包與建置的 scripts。特別說明一下,npm pack 可以將專案壓縮成 npm 套件,而 prepack 會在執行 pack 之前自動執行。
{
// ...
"scripts": {
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json && npm run rename:esm",
"build": "npm run build:cjs && npm run build:esm",
"clean": "rimraf dist",
"rename:esm": "/bin/zsh ./scripts/fix-mjs.sh",
"prepack": "npm run clean && npm run build"
}
}撰寫轉檔名腳本
新增一個檔案 fix-mjs.sh 並撰寫替換 .js 到 .mjs 的腳本
for file in ./dist/esm/*.js; do
echo "Updating $file contents..."
sed -i '' "s/\.js'/\.mjs'/g" "$file"
echo "Renaming $file to ${file%.js}.mjs..."
mv "$file" "${file%.js}.mjs"
done撰寫功能並打包
這邊就把原本寫在後端的驗證 schema 複製過來
import { z } from 'zod'
export const inputTaskSchema = z.object({
title: z.string().min(1, '請輸入任務標題'),
description: z.string().optional(),
status: z.enum(['new', 'active', 'completed']).default('new'),
storyPoint: z.number().optional(),
})完成後,執行打包指令
npm pack這時會看到專案內生成出一個 validator-1.0.0.tgz,結構上就是 <package.name>-<package.version>.tgz
接著,到前後端專案中,安裝本地套件
npm i ../package/validator-1.0.0.tgz這樣,前後端都可以使用同一套驗證邏輯了。
總結
通過這個文章,我們學習了如何:
- 設置 TypeScript 專案以支援 CJS 和 ESM。
- 配置 package.json 以支援多模組輸出。
- 使用腳本自動化構建過程。
- 打包並在其他專案中使用自製的 npm package。
這個方法允許我們創建可在前端和後端共用的模組,提高代碼重用性和一致性。
本篇程式碼變更可以看此 PR
相關文章
TypeScript 後記
終於完成連續三十天的鐵人賽文章撰寫!距離我第一次參加鐵人賽已經三年之久了。這次參加主要是因為朋友邀請一起參賽,同時我自己也想藉此機會深入了解 TypeScript 5 的新功能,強迫自己透過寫作來吸收知識。
TypeScript 實戰:前端表單與串接 API
在現代前端開發中,與後端進行資料交換是常見的需求。透過 HTTP 請求,我們可以將資料送出並取得結果。這篇文章將介紹如何使用 TypeScript 實作前端表單與串接 API,並展示如何將這些功能結合到 React 應用中。同時,文章還會介紹如何解決跨域問題(CORS),以便讓前後端能順利溝通。
TypeScript 實戰:前端泛型元件
今天我們將實作一個任務清單元件,並一步步重構它,使原本無法複用的元件,利用泛型變成更通用、更彈性的元件。這章節會著重在如何運用 TypeScript,不會過多討論 React 的原理。如果對 React 還不太熟悉,可以先參考官方[快速上手文件](https://react.dev/learn)。