系列文章: 前端工程師的 Modern Web 實踐之道 - Day 3
預計閱讀時間: 10 分鐘
難度等級: ⭐⭐⭐☆☆
在前一篇文章中,我們建立了現代化的開發環境基礎。今天我們將深入探討包管理器的選擇策略,這個決定將直接影響你的開發效率、專案性能和團隊協作體驗。
在現代前端開發中,一個典型專案的 node_modules 可能包含數百個相依套件,佔用數百 MB 的硬碟空間。每當我們執行 npm install 時,背後發生的不只是檔案下載,還涉及:
// package.json - npm 專案配置
{
  "name": "modern-web-project",
  "version": "1.0.0",
  "dependencies": {
    "react": "^18.2.0",
    "typescript": "^5.0.0"
  },
  "engines": {
    "npm": ">=8.0.0",
    "node": ">=16.0.0"
  }
}
技術特點:
# .yarnrc.yml - Yarn 3 配置
nodeLinker: pnp  # 使用 Plug'n'Play 模式
enableGlobalCache: true
compressionLevel: mixed
技術創新:
// .pnpmfile.cjs - pnpm 配置範例
module.exports = {
  hooks: {
    readPackage(pkg, context) {
      // 自訂相依性解析邏輯
      if (pkg.name === 'some-package') {
        pkg.dependencies = {
          ...pkg.dependencies,
          'optimized-dep': '^2.0.0'
        }
      }
      return pkg
    }
  }
}
核心優勢:
// bun.sh 安裝與使用
$ curl -fsSL https://bun.sh/install | bash
$ bun install  # 套件安裝
$ bun run dev  # 執行腳本
$ bun build   # 應用程式建置
# bunfig.toml - Bun 設定檔
[install]
# 設定 npm 註冊表
registry = "https://registry.npmjs.org/"
# 啟用快取
cache = true
# 安裝時自動信任憑證
auto = true
[install.scopes]
# 私有套件範圍設定
"@mycompany" = "https://npm.internal.com/"
革命性特點:
讓我們用一個實際的 React + TypeScript 專案來測試各個包管理器的表現:
// 測試專案 package.json
{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "@types/react": "^18.0.0",
    "typescript": "^5.0.0",
    "vite": "^4.0.0",
    "tailwindcss": "^3.2.0",
    "framer-motion": "^10.0.0",
    "react-query": "^3.39.0",
    "zustand": "^4.3.0"
  }
}
#!/bin/bash
# benchmark.sh - 包管理器性能測試
test_manager() {
    local manager=$1
    local project_path="./test-${manager}"
    echo "Testing $manager..."
    # 清理環境
    rm -rf $project_path
    mkdir $project_path
    cd $project_path
    # 複製 package.json
    cp ../package.json .
    # 測量安裝時間
    start_time=$(date +%s%N)
    case $manager in
        "npm")
            npm install --no-audit --no-fund
            ;;
        "yarn")
            yarn install --frozen-lockfile
            ;;
        "pnpm")
            pnpm install --frozen-lockfile
            ;;
        "bun")
            bun install
            ;;
    esac
    end_time=$(date +%s%N)
    duration=$(( (end_time - start_time) / 1000000 ))
    # 測量硬碟使用量
    disk_usage=$(du -sh node_modules 2>/dev/null | cut -f1)
    echo "$manager: ${duration}ms, Disk: $disk_usage"
    cd ..
}
# 執行測試
for manager in npm yarn pnpm bun; do
    test_manager $manager
done
// 測試結果處理腳本
const benchmarkResults = {
  npm: {
    installTime: 45230,  // ms
    diskUsage: '156MB',
    features: ['npx', 'workspaces', 'audit']
  },
  yarn: {
    installTime: 32100,  // ms
    diskUsage: '142MB',
    features: ['pnp', 'zero-installs', 'berry']
  },
  pnpm: {
    installTime: 28750,  // ms
    diskUsage: '89MB',   // 符號連結節省空間
    features: ['strict-isolation', 'monorepo', 'shamefully-hoist']
  },
  bun: {
    installTime: 18920,  // ms - 最快速度
    diskUsage: '145MB',  // 傳統 node_modules 結構
    features: ['native-runtime', 'zero-config-ts', 'built-in-bundler', 'web-api']
  }
};
function analyzeResults(results) {
  const fastest = Object.entries(results)
    .sort(([,a], [,b]) => a.installTime - b.installTime)[0];
  const mostEfficient = Object.entries(results)
    .sort(([,a], [,b]) =>
      parseInt(a.diskUsage) - parseInt(b.diskUsage))[0];
  console.log(`Fastest: ${fastest[0]} (${fastest[1].installTime}ms)`);
  console.log(`Most efficient: ${mostEfficient[0]} (${mostEfficient[1].diskUsage})`);
}
analyzeResults(benchmarkResults);
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'tools/*'
// apps/web/package.json
{
  "name": "@myorg/web",
  "dependencies": {
    "@myorg/ui-components": "workspace:*",
    "@myorg/shared-utils": "workspace:^1.0.0"
  }
}
// 跨套件依賴管理腳本
// scripts/sync-versions.js
const fs = require('fs');
const path = require('path');
const glob = require('glob');
function syncWorkspaceVersions() {
  const workspaces = glob.sync('./packages/*/package.json');
  const versions = new Map();
  // 收集所有內部套件版本
  workspaces.forEach(packagePath => {
    const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
    versions.set(pkg.name, pkg.version);
  });
  // 更新跨套件相依性
  workspaces.forEach(packagePath => {
    const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
    let updated = false;
    ['dependencies', 'devDependencies'].forEach(depType => {
      if (pkg[depType]) {
        Object.keys(pkg[depType]).forEach(depName => {
          if (versions.has(depName) &&
              pkg[depType][depName].startsWith('workspace:')) {
            pkg[depType][depName] = `workspace:^${versions.get(depName)}`;
            updated = true;
          }
        });
      }
    });
    if (updated) {
      fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
      console.log(`Updated ${pkg.name}`);
    }
  });
}
syncWorkspaceVersions();
// package.json - Bun workspaces 設定
{
  "name": "my-monorepo",
  "workspaces": ["apps/*", "packages/*"],
  "devDependencies": {
    "bun-types": "latest"
  }
}
// bun 專案中的 TypeScript 設定範例
// apps/web/src/index.ts
import { apiClient } from '@myorg/shared-api';
import { Button } from '@myorg/ui-components';
// Bun 原生支援 TypeScript,無需額外設定
const app = {
  async start() {
    const data = await apiClient.fetchUsers();
    console.log('Users loaded:', data.length);
  }
};
// 使用 Bun 的內建 Web API
export default {
  port: 3000,
  fetch(request: Request) {
    return new Response("Hello from Bun!");
  },
};
# Bun monorepo 常用指令
$ bun install                    # 安裝所有依賴
$ bun --filter web run dev      # 在特定 workspace 執行指令
$ bun run build --filter @myorg/ui-components  # 建置特定套件
# .npmrc - 企業級安全配置
registry=https://registry.npmjs.org/
audit-level=moderate
fund=false
package-lock-only=true
# 私有套件配置
@mycompany:registry=https://npm.internal.com/
//npm.internal.com/:_authToken=${NPM_INTERNAL_TOKEN}
# 安全性設定
ignore-scripts=true  # 禁止自動執行安裝腳本
# .github/workflows/security-audit.yml
name: Security Audit
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install pnpm
        run: npm install -g pnpm
      - name: Security Audit
        run: |
          pnpm audit --audit-level moderate
          pnpm licenses list --json > licenses.json
      - name: Check License Compliance
        run: |
          node scripts/check-licenses.js
// scripts/check-licenses.js - 授權合規檢查
const fs = require('fs');
const allowedLicenses = [
  'MIT', 'Apache-2.0', 'BSD-2-Clause',
  'BSD-3-Clause', 'ISC', 'CC0-1.0'
];
const prohibitedLicenses = [
  'GPL-2.0', 'GPL-3.0', 'AGPL-3.0', 'LGPL-2.1', 'LGPL-3.0'
];
function checkLicenses() {
  const licenses = JSON.parse(fs.readFileSync('licenses.json', 'utf8'));
  const violations = [];
  Object.entries(licenses).forEach(([packageName, info]) => {
    const license = info.license;
    if (prohibitedLicenses.includes(license)) {
      violations.push({
        package: packageName,
        license,
        severity: 'error'
      });
    } else if (!allowedLicenses.includes(license)) {
      violations.push({
        package: packageName,
        license,
        severity: 'warning'
      });
    }
  });
  if (violations.length > 0) {
    console.log('License violations found:');
    violations.forEach(v => {
      console.log(`${v.severity.toUpperCase()}: ${v.package} (${v.license})`);
    });
    const errors = violations.filter(v => v.severity === 'error');
    if (errors.length > 0) {
      process.exit(1);
    }
  } else {
    console.log('All licenses compliant ✅');
  }
}
checkLicenses();
.nvmrc 和 package.json 中明確指定工具版本.gitignore