NPM 包開發與最佳化全面指南

Immerse發表於2024-10-28

前言

  • Hey, 我是 Immerse
  • 系列文章首發於【Immerse】,更多內容請關注該網站
  • 轉載說明:轉載請註明原文出處及版權宣告!

1. 理解 NPM 包的結構

1.1 package.json 檔案:包的核心

package.json檔案是 NPM 包的中央配置,定義了包的各個方面,從基本後設資料到複雜的釋出配置。

{
    "name": "my-awesome-package",
    "version": "1.0.0",
    "description": "一個令人驚歎的包",
    "main": "./dist/index.js",
    "module": "./dist/index.mjs",
    "types": "./dist/index.d.ts",
    "files": ["dist"],
    "scripts": {
        "build": "tsup src/index.ts --format cjs,esm --dts",
        "test": "jest"
    },
    "keywords": ["awesome", "package"],
    "author": "Your Name <you@example.com>",
    "license": "MIT",
    "dependencies": {
        "lodash": "^4.17.21"
    },
    "devDependencies": {
        "typescript": "^4.5.5",
        "tsup": "^5.11.13",
        "jest": "^27.4.7"
    }
}

讓我們詳細解析一些關鍵欄位:

  • nameversion:這兩個欄位組成了包在 NPM 登錄檔中的唯一識別符號。
  • mainmoduletypes:這些指定了不同模組系統和 TypeScript 支援的入口點。
  • files:這個陣列指定了釋出包時應該包含哪些檔案和目錄。
  • scripts:這些是常見任務(如構建和測試)的命令快捷方式。

1.2 理解包的入口點

現代 JavaScript 生態系統支援多種模組格式。您的包應該透過提供多個入口點來適應不同的環境。

  1. main:主要入口點,通常用於 CommonJS (CJS)模組。
  2. module:用於 ECMAScript (ESM)模組的入口點。
  3. browser:用於瀏覽器環境的入口點。
  4. types:TypeScript 型別宣告的入口點。

以下是一個包結構的示例:

my-awesome-package/
├── src/
│   ├── index.ts
│   └── utils.ts
├── dist/
│   ├── index.js        (CJS構建)
│   ├── index.mjs       (ESM構建)
│   ├── index.d.ts      (TypeScript宣告)
│   └── browser.js      (瀏覽器特定構建)
├── package.json
└── tsconfig.json

對應的package.json配置:

{
    "name": "my-awesome-package",
    "version": "1.0.0",
    "main": "./dist/index.js",
    "module": "./dist/index.mjs",
    "browser": "./dist/browser.js",
    "types": "./dist/index.d.ts",
    "exports": {
        ".": {
            "require": "./dist/index.js",
            "import": "./dist/index.mjs",
            "types": "./dist/index.d.ts"
        }
    }
}

2. 深入理解模組格式

2.1 CommonJS (CJS)

CommonJS 是 Node.js 的傳統模組格式。它使用require()進行匯入,使用module.exports進行匯出。

// mathUtils.js
function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

module.exports = {
    add,
    subtract,
};

// main.js
const mathUtils = require('./mathUtils');
console.log(mathUtils.add(5, 3)); // 輸出: 8

2.2 ECMAScript 模組 (ESM)

ESM 是 JavaScript 模組的現代標準,使用importexport語句。

// mathUtils.mjs
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

// main.mjs
import { add, subtract } from './mathUtils.mjs';
console.log(add(5, 3)); // 輸出: 8

2.3 通用模組定義 (UMD)

UMD 是一種允許模組在多種環境(CommonJS、AMD、全域性變數)中工作的模式。

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['exports'], factory);
    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
        // CommonJS
        factory(exports);
    } else {
        // 瀏覽器全域性變數
        factory((root.mathUtils = {}));
    }
})(typeof self !== 'undefined' ? self : this, function (exports) {
    exports.add = function (a, b) {
        return a + b;
    };
    exports.subtract = function (a, b) {
        return a - b;
    };
});

3. 高階包最佳化技術

3.1 Tree Shaking 和副作用

Tree shaking 是現代打包工具用來消除死程式碼的技術。要使您的包可以進行 tree shaking:

  1. 使用 ES 模組
  2. 避免副作用
  3. package.json中使用"sideEffects"欄位
{
    "name": "my-utils",
    "version": "1.0.0",
    "sideEffects": false
}

如果某些檔案確實有副作用:

{
    "name": "my-utils",
    "version": "1.0.0",
    "sideEffects": ["./src/polyfills.js", "*.css"]
}

3.2 程式碼分割和動態匯入

對於大型包,考慮使用程式碼分割,允許使用者只匯入他們需要的部分:

// heavyFunction.js
export function heavyFunction() {
    // ... 一些計算密集型操作
}

// main.js
async function doHeavyWork() {
    const { heavyFunction } = await import('./heavyFunction.js');
    heavyFunction();
}

3.3 條件匯出

使用條件匯出為不同的環境或匯入條件提供不同的入口點:

{
    "name": "my-package",
    "exports": {
        ".": {
            "import": "./dist/index.mjs",
            "require": "./dist/index.cjs",
            "browser": "./dist/browser.js"
        },
        "./utils": {
            "import": "./dist/utils.mjs",
            "require": "./dist/utils.cjs"
        }
    }
}

4. 版本管理和釋出

4.1 語義化版本控制 (SemVer)

語義化版本使用三部分版本號:主版本號.次版本號.修訂號

  • 主版本號:進行不相容的 API 更改時
  • 次版本號:以向後相容的方式新增功能時
  • 修訂號:進行向後相容的 bug 修復時
npm version patch -m "版本更新到 %s - 修復文件中的拼寫錯誤"
npm version minor -m "版本更新到 %s - 新增新的實用函式"
npm version major -m "版本更新到 %s - 更改API結構"

4.2 預釋出版本

對於預釋出版本,使用帶連字元的標籤:

  • latest: 最新線上版本
  • alpha: 內部測試版本
  • beta: 公開測試版本
  • rc: 發行候選版本

    • Tips: 可以將這些識別符號新增到版本號中,同時也可以新增額外版本:如:1.0.0-alpha.01.0.0-beta.11.0.0-rc.1
npm version prerelease --preid=alpha
# 1.0.0 -> 1.0.1-alpha.0

npm version prerelease --preid=beta
# 1.0.1-alpha.0 -> 1.0.1-beta.0

npm version prerelease --preid=rc
# 1.0.1-beta.0 -> 1.0.1-rc.0

4.3 使用標籤釋出

使用標籤釋出不同版本或預釋出版本:

npm publish --tag next
npm publish --tag beta

使用者可以安裝特定版本:

npm install my-package@next
npm install my-package@beta

5. 持續整合和部署 (CI/CD)

5.1 使用 GitHub Actions 進行自動釋出

建立一個.github/workflows/publish.yml檔案:

name: 釋出包

on:
    release:
        types: [created]

jobs:
    build:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - uses: actions/setup-node@v2
              with:
                  node-version: '14'
                  registry-url: 'https://registry.npmjs.org'
            - run: npm ci
            - run: npm test
            - run: npm run build
            - run: npm publish
              env:
                  NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

    publish-gpr:
        needs: build
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - uses: actions/setup-node@v2
              with:
                  node-version: '14'
                  registry-url: 'https://npm.pkg.github.com'
            - run: npm ci
            - run: npm publish
              env:
                  NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

這個工作流程將在您建立新版本時自動將您的包釋出到 NPM 和 GitHub Packages。

5.2 自動化版本更新

您可以在 CI/CD 管道中自動化版本更新。以下是使用 GitHub Action 的示例:

name: 更新版本

on:
    push:
        branches:
            - main

jobs:
    bump-version:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
              with:
                  fetch-depth: 0
            - uses: actions/setup-node@v2
              with:
                  node-version: '14'
            - name: 更新版本
              run: |
                  git config --local user.email "action@github.com"
                  git config --local user.name "GitHub Action"
                  npm version patch -m "更新版本到 %s [skip ci]"
            - name: 推送更改
              uses: ad-m/github-push-action@master
              with:
                  github_token: ${{ secrets.GITHUB_TOKEN }}
                  branch: ${{ github.ref }}

這個動作將在每次向主分支推送更改時自動更新包的修訂版本號。

6. 包開發最佳實踐

6.1 文件

良好的文件對於包的採用至關重要。考慮使用像 JSDoc 這樣的工具進行內聯文件:

/**
 * 將兩個數字相加。
 * @param {number} a - 第一個數字。
 * @param {number} b - 第二個數字。
 * @returns {number} a和b的和。
 */
function add(a, b) {
    return a + b;
}

6.2 測試

使用像 Jest 這樣的框架實現全面的測試:

// math.js
export function add(a, b) {
    return a + b;
}

// math.test.js
import { add } from './math';

test('1 + 2 應該等於 3', () => {
    expect(add(1, 2)).toBe(3);
});

6.3 程式碼檢查和格式化

使用 ESLint 進行程式碼檢查,使用 Prettier 進行程式碼格式化。以下是一個示例.eslintrc.js

module.exports = {
    env: {
        browser: true,
        es2021: true,
        node: true,
    },
    extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
    parser: '@typescript-eslint/parser',
    parserOptions: {
        ecmaVersion: 12,
        sourceType: 'module',
    },
    plugins: ['@typescript-eslint'],
    rules: {
        // 在這裡新增自定義規則
    },
};

以及一個.prettierrc檔案:

{
    "singleQuote": true,
    "trailingComma": "es5",
    "tabWidth": 2,
    "semi": true,
    "printWidth": 100
}

info.jpg

相關文章