【Node.js】工具链与工程化

个人主页:Guiat
归属专栏:node.js

文章目录

  • [1. Node.js 工具链概述](#1. Node.js 工具链概述)
    • [1.1 工具链的作用](#1.1 工具链的作用)
    • [1.2 Node.js 工具链全景](#1.2 Node.js 工具链全景)
  • [2. 包管理与依赖管理](#2. 包管理与依赖管理)
    • [2.1 npm (Node Package Manager)](#2.1 npm (Node Package Manager))
    • [2.2 yarn](#2.2 yarn)
    • [2.3 pnpm](#2.3 pnpm)
    • [2.4 锁文件与依赖管理](#2.4 锁文件与依赖管理)
    • [2.5 工作空间与 Monorepo](#2.5 工作空间与 Monorepo)
  • [3. 构建工具与打包](#3. 构建工具与打包)
    • [3.1 Webpack](#3.1 Webpack)
    • [3.2 Rollup](#3.2 Rollup)
    • [3.3 esbuild](#3.3 esbuild)
    • [3.4 Babel](#3.4 Babel)
    • [3.5 TypeScript 配置](#3.5 TypeScript 配置)
    • [3.6 构建工具比较](#3.6 构建工具比较)
  • [4. 代码质量与规范](#4. 代码质量与规范)
    • [4.1 ESLint](#4.1 ESLint)
    • [4.2 Prettier](#4.2 Prettier)
    • [4.3 Git Hooks 与 Husky](#4.3 Git Hooks 与 Husky)
    • [4.4 代码风格规范工作流](#4.4 代码风格规范工作流)
    • [4.5 TypeScript 检查](#4.5 TypeScript 检查)
  • [5. 测试框架与测试策略](#5. 测试框架与测试策略)
    • [5.1 Jest 测试框架](#5.1 Jest 测试框架)
    • [5.2 API 测试与 Supertest](#5.2 API 测试与 Supertest)
    • [5.3 单元测试与模拟](#5.3 单元测试与模拟)
    • [5.4 测试覆盖率与报告](#5.4 测试覆盖率与报告)
    • [5.5 持续集成与测试](#5.5 持续集成与测试)
  • [6. 自动化与 CI/CD 流程](#6. 自动化与 CI/CD 流程)
    • [6.1 GitHub Actions](#6.1 GitHub Actions)
    • [6.2 Docker 与容器化](#6.2 Docker 与容器化)

正文

1. Node.js 工具链概述

Node.js 工具链是一系列用于开发、测试、构建和部署 Node.js 应用程序的工具集合。完善的工具链可以显著提高开发效率,确保代码质量,简化部署流程。

1.1 工具链的作用

  • 自动化开发工作流程
  • 提高代码质量和一致性
  • 简化依赖管理
  • 优化构建过程
  • 加速测试和部署
  • 提高项目可维护性

1.2 Node.js 工具链全景

Node.js工具链 包管理工具 构建工具 测试框架 代码质量工具 版本控制 CI/CD工具 文档工具 部署与监控 npm yarn pnpm Webpack Rollup Parcel esbuild Vite Jest Mocha Jasmine AVA Cypress ESLint Prettier TypeScript Husky SonarQube Git GitHub/GitLab Conventional Commits GitHub Actions Jenkins CircleCI Travis CI JSDoc Swagger/OpenAPI Docusaurus Docker PM2 Kubernetes Grafana/Prometheus

2. 包管理与依赖管理

2.1 npm (Node Package Manager)

npm 是 Node.js 的默认包管理器,提供了安装、更新和管理依赖的功能。

bash 复制代码
# 初始化新项目
npm init -y

# 安装依赖包
npm install express

# 安装开发依赖
npm install --save-dev jest

# 全局安装包
npm install -g nodemon

# 更新依赖
npm update

# 运行脚本
npm run start

package.json 详解:

json 复制代码
{
  "name": "my-node-app",
  "version": "1.0.0",
  "description": "A Node.js application with complete toolchain",
  "main": "src/index.js",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "build": "webpack --config webpack.config.js",
    "test": "jest --coverage",
    "lint": "eslint src/**/*.js",
    "format": "prettier --write \"src/**/*.{js,json}\"",
    "prepare": "husky install"
  },
  "keywords": ["node", "express", "api"],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.5.0",
    "dotenv": "^16.3.1",
    "jsonwebtoken": "^9.0.1"
  },
  "devDependencies": {
    "jest": "^29.6.4",
    "eslint": "^8.48.0",
    "prettier": "^3.0.3",
    "nodemon": "^3.0.1",
    "webpack": "^5.88.2",
    "webpack-cli": "^5.1.4",
    "husky": "^8.0.3",
    "lint-staged": "^14.0.1"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

2.2 yarn

Yarn 是 Facebook 开发的包管理器,提供了更快的安装速度和更好的依赖解决方案。

bash 复制代码
# 初始化项目
yarn init -y

# 安装依赖
yarn add express

# 安装开发依赖
yarn add --dev jest

# 全局安装
yarn global add nodemon

# 更新依赖
yarn upgrade

# 运行脚本
yarn start

2.3 pnpm

pnpm 是一个快速、节省磁盘空间的包管理器,采用了内容寻址存储方式。

bash 复制代码
# 安装 pnpm
npm install -g pnpm

# 初始化项目
pnpm init

# 安装依赖
pnpm add express

# 安装开发依赖
pnpm add -D jest

# 更新依赖
pnpm update

# 运行脚本
pnpm start

2.4 锁文件与依赖管理

graph TD A[依赖管理] --> B[锁文件] A --> C[语义化版本] A --> D[monorepo] B --> B1[package-lock.json] B --> B2[yarn.lock] B --> B3[pnpm-lock.yaml] C --> C1[主版本.次版本.修订版本] C --> C2[^主版本.次版本.修订版本] C --> C3[~主版本.次版本.修订版本] D --> D1[Lerna] D --> D2[Nx] D --> D3[Turborepo] style A fill:#66CDAA style B fill:#87CEFA style C fill:#87CEFA style D fill:#87CEFA

语义化版本控制:

  • 主版本号:不兼容的 API 更改
  • 次版本号:向后兼容的功能性新增
  • 修订版本号:向后兼容的问题修正

前缀含义:

  • ^:允许升级到任何保持主版本相同的版本
  • ~:允许升级到任何保持主版本和次版本相同的版本
  • 无前缀:精确匹配版本

2.5 工作空间与 Monorepo

json 复制代码
// package.json (workspaces 示例)
{
  "name": "monorepo-project",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "start": "node scripts/start.js",
    "test": "jest"
  }
}
javascript 复制代码
// Lerna 配置文件 (lerna.json)
{
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true,
  "packages": ["packages/*"],
  "command": {
    "publish": {
      "conventionalCommits": true,
      "message": "chore(release): publish"
    }
  }
}

3. 构建工具与打包

3.1 Webpack

Webpack 是一个静态模块打包器,可以处理 JavaScript、CSS、图像等资源。

javascript 复制代码
// webpack.config.js 基本配置
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  target: 'node',
  mode: process.env.NODE_ENV || 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    clean: true
  },
  externals: [nodeExternals()],
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.json$/,
        loader: 'json-loader',
        type: 'javascript/auto'
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  optimization: {
    minimize: process.env.NODE_ENV === 'production'
  },
  devtool: process.env.NODE_ENV === 'production' ? 'source-map' : 'eval-source-map'
};

3.2 Rollup

Rollup 专注于 JavaScript 库构建,特别适合生成更小、更高效的包。

javascript 复制代码
// rollup.config.js 基本配置
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import json from '@rollup/plugin-json';
import { terser } from 'rollup-plugin-terser';

export default {
  input: 'src/index.js',
  output: [
    {
      file: 'dist/bundle.cjs.js',
      format: 'cjs',
      sourcemap: true
    },
    {
      file: 'dist/bundle.esm.js',
      format: 'esm',
      sourcemap: true
    }
  ],
  plugins: [
    resolve({
      preferBuiltins: true
    }),
    commonjs(),
    json(),
    babel({
      babelHelpers: 'bundled',
      exclude: 'node_modules/**'
    }),
    process.env.NODE_ENV === 'production' && terser()
  ],
  external: ['express', 'mongoose', 'dotenv']
};

3.3 esbuild

esbuild 是一个极快的 JavaScript 打包器和压缩器。

javascript 复制代码
// esbuild.config.js
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  platform: 'node',
  target: ['node16'],
  outfile: 'dist/bundle.js',
  minify: process.env.NODE_ENV === 'production',
  sourcemap: true,
  external: ['express', 'mongoose', 'dotenv']
}).catch(() => process.exit(1));

3.4 Babel

Babel 是一个 JavaScript 编译器,用于将现代 JavaScript 代码转换为向后兼容的版本。

javascript 复制代码
// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        node: '16'
      }
    }]
  ],
  plugins: [
    '@babel/plugin-transform-runtime',
    '@babel/plugin-proposal-optional-chaining',
    '@babel/plugin-proposal-nullish-coalescing-operator'
  ]
};

3.5 TypeScript 配置

json 复制代码
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

3.6 构建工具比较

工具 优势 适用场景
Webpack 生态丰富,功能强大,社区支持好 复杂应用,需要处理多种资源
Rollup 生成体积小的包,输出多种格式 库开发,需要 ESM 和 CJS 双格式
esbuild 极快的构建速度 开发环境快速构建,简单项目
Parcel 零配置,开箱即用 快速原型开发,不想编写配置
Vite 快速开发服务器,模块热替换 现代应用开发,优先考虑开发体验

4. 代码质量与规范

4.1 ESLint

ESLint 是一个静态代码分析工具,用于识别和报告 JavaScript 代码中的问题。

javascript 复制代码
// .eslintrc.js
module.exports = {
  env: {
    node: true,
    es2021: true,
    jest: true
  },
  extends: [
    'eslint:recommended',
    'plugin:node/recommended'
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module'
  },
  rules: {
    'indent': ['error', 2],
    'linebreak-style': ['error', 'unix'],
    'quotes': ['error', 'single'],
    'semi': ['error', 'always'],
    'no-unused-vars': ['warn', { 'argsIgnorePattern': '^_' }],
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'node/no-unsupported-features/es-syntax': [
      'error',
      { 'ignores': ['modules'] }
    ]
  }
};

4.2 Prettier

Prettier 是一个代码格式化工具,确保代码风格统一。

json 复制代码
// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "printWidth": 100,
  "trailingComma": "es5",
  "arrowParens": "avoid",
  "endOfLine": "lf"
}

与 ESLint 集成:

bash 复制代码
npm install --save-dev eslint-config-prettier eslint-plugin-prettier
javascript 复制代码
// .eslintrc.js with Prettier integration
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:node/recommended',
    'plugin:prettier/recommended'
  ],
  // other configurations...
};

4.3 Git Hooks 与 Husky

Husky 可以帮助在 Git 生命周期的关键点上运行脚本。

bash 复制代码
# 安装 Husky
npm install --save-dev husky

# 启用 Git hooks
npx husky install

# 添加 pre-commit hook
npx husky add .husky/pre-commit "npm run lint-staged"
json 复制代码
// package.json
{
  "scripts": {
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.js": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md}": [
      "prettier --write"
    ]
  }
}

4.4 代码风格规范工作流

失败 通过 完成 开发人员提交代码 Husky拦截 lint-staged运行 ESLint检查 显示错误 Prettier格式化 提交代码

4.5 TypeScript 检查

bash 复制代码
# 安装 TypeScript
npm install --save-dev typescript @types/node

# 生成 tsconfig.json
npx tsc --init

与 ESLint 集成:

bash 复制代码
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
javascript 复制代码
// .eslintrc.js for TypeScript
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended'
  ],
  plugins: ['@typescript-eslint'],
  // other configurations...
};

5. 测试框架与测试策略

5.1 Jest 测试框架

Jest 是一个零配置的 JavaScript 测试框架,适用于大多数 JavaScript 项目。

javascript 复制代码
// jest.config.js
module.exports = {
  testEnvironment: 'node',
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.{js,ts}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{js,ts}',
    '!src/**/index.{js,ts}'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  testMatch: [
    '**/__tests__/**/*.test.[jt]s?(x)',
    '**/?(*.)+(spec|test).[jt]s?(x)'
  ],
  testPathIgnorePatterns: [
    '/node_modules/',
    '/dist/'
  ]
};

示例测试文件:

javascript 复制代码
// src/utils/calculator.test.js
const { add, subtract, multiply, divide } = require('./calculator');

describe('Calculator', () => {
  describe('add', () => {
    test('adds two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('adds a positive and a negative number', () => {
      expect(add(2, -3)).toBe(-1);
    });
  });

  describe('subtract', () => {
    test('subtracts two positive numbers', () => {
      expect(subtract(5, 3)).toBe(2);
    });

    test('subtracts a negative from a positive number', () => {
      expect(subtract(5, -3)).toBe(8);
    });
  });

  describe('multiply', () => {
    test('multiplies two positive numbers', () => {
      expect(multiply(2, 3)).toBe(6);
    });

    test('multiplies a positive and a negative number', () => {
      expect(multiply(2, -3)).toBe(-6);
    });
  });

  describe('divide', () => {
    test('divides two positive numbers', () => {
      expect(divide(6, 3)).toBe(2);
    });

    test('throws an error when dividing by zero', () => {
      expect(() => divide(6, 0)).toThrow('Cannot divide by zero');
    });
  });
});

5.2 API 测试与 Supertest

javascript 复制代码
// src/app.test.js
const request = require('supertest');
const app = require('./app');
const mongoose = require('mongoose');

// 连接测试数据库
beforeAll(async () => {
  await mongoose.connect(process.env.MONGO_URI_TEST);
});

// 断开数据库连接
afterAll(async () => {
  await mongoose.connection.close();
});

// 清理测试数据
afterEach(async () => {
  await mongoose.connection.db.dropDatabase();
});

describe('User API', () => {
  describe('POST /api/users', () => {
    test('should create a new user', async () => {
      const userData = {
        username: 'testuser',
        email: '[email protected]',
        password: 'password123'
      };

      const response = await request(app)
        .post('/api/users')
        .send(userData);

      expect(response.status).toBe(201);
      expect(response.body).toHaveProperty('user');
      expect(response.body.user).toHaveProperty('id');
      expect(response.body.user.username).toBe(userData.username);
      expect(response.body.user.email).toBe(userData.email);
      expect(response.body.user).not.toHaveProperty('password');
    });

    test('should not create user with duplicate email', async () => {
      const userData = {
        username: 'testuser',
        email: '[email protected]',
        password: 'password123'
      };

      // 创建第一个用户
      await request(app)
        .post('/api/users')
        .send(userData);

      // 尝试创建重复的用户
      const response = await request(app)
        .post('/api/users')
        .send({
          username: 'anotheruser',
          email: '[email protected]', // 相同的邮箱
          password: 'password456'
        });

      expect(response.status).toBe(400);
      expect(response.body).toHaveProperty('error');
    });
  });
});

5.3 单元测试与模拟

javascript 复制代码
// src/services/userService.test.js
const userService = require('./userService');
const User = require('../models/User');

// 模拟 User 模型
jest.mock('../models/User');

describe('User Service', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('createUser', () => {
    test('should create and return a new user', async () => {
      // 准备测试数据
      const userData = {
        username: 'testuser',
        email: '[email protected]',
        password: 'password123'
      };

      // 模拟 User.create 方法
      User.create.mockResolvedValue({
        _id: '507f1f77bcf86cd799439011',
        username: userData.username,
        email: userData.email,
        createdAt: new Date().toISOString()
      });

      // 调用被测试的方法
      const user = await userService.createUser(userData);

      // 断言 User.create 被正确调用
      expect(User.create).toHaveBeenCalledWith(expect.objectContaining({
        username: userData.username,
        email: userData.email
      }));

      // 断言返回值
      expect(user).toHaveProperty('_id');
      expect(user.username).toBe(userData.username);
      expect(user.email).toBe(userData.email);
    });

    test('should throw an error if user creation fails', async () => {
      // 准备测试数据
      const userData = {
        username: 'testuser',
        email: '[email protected]',
        password: 'password123'
      };

      // 模拟 User.create 抛出错误
      const errorMessage = 'Failed to create user';
      User.create.mockRejectedValue(new Error(errorMessage));

      // 断言方法抛出错误
      await expect(userService.createUser(userData))
        .rejects.toThrow(errorMessage);
    });
  });
});

5.4 测试覆盖率与报告

bash 复制代码
# 运行测试并生成覆盖率报告
npm test -- --coverage

测试策略 单元测试 集成测试 端到端测试 性能测试 Jest Supertest Cypress k6

5.5 持续集成与测试

yaml 复制代码
# .github/workflows/test.yml
name: Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      mongodb:
        image: mongo:4.4
        ports:
          - 27017:27017

    strategy:
      matrix:
        node-version: [16.x, 18.x]

    steps:
    - uses: actions/checkout@v3
    
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run linting
      run: npm run lint
    
    - name: Run tests with coverage
      run: npm test -- --coverage
      env:
        MONGO_URI_TEST: mongodb://localhost:27017/test-db
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        token: ${{ secrets.CODECOV_TOKEN }}

6. 自动化与 CI/CD 流程

6.1 GitHub Actions

yaml 复制代码
# .github/workflows/ci.yml
name: CI/CD

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    
    - name: Use Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18.x'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Lint
      run: npm run lint
    
    - name: Test
      run: npm test
    
    - name: Build
      run: npm run build
    
    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: build
        path: dist

  deploy:
    needs: build
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Download build artifacts
      uses: actions/download-artifact@v3
      with:
        name: build
        path: dist
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_HUB_USERNAME }}
        password: ${{ secrets.DOCKER_HUB_TOKEN }}
    
    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: username/my-node-app:latest
    
    - name: Deploy to production
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USERNAME }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /path/to/production
          docker-compose pull
          docker-compose up -d

6.2 Docker 与容器化

dockerfile 复制代码
# Dockerfile
FROM node:18-alpine as builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:18-alpine

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules

ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000
CMD ["node", "dist/index.js"]

结语

感谢您的阅读!期待您的一键三连!欢迎指正!

相关推荐
MonkeyKing_sunyuhua6 小时前
Ubuntu 22.04上升级npm版本
linux·ubuntu·node.js
kongxx6 小时前
升级node@22后运行npm install报错 distutils not found
npm·node.js
Clang's Blog7 小时前
如何在 Mac M4 芯片电脑上卸载高版本的 Node.js
macos·node.js·vim
MonkeyKing_sunyuhua7 小时前
Ubuntu 22.04上升级Node.js版本
linux·ubuntu·node.js
哎哟喂_!7 小时前
深入掌握Node.js HTTP模块:从开始到放弃
开发语言·前端·javascript·http·node.js
achene_ql8 小时前
基于 WebRTC 的一对一屏幕共享项目(一)——项目简介
javascript·websocket·node.js·webrtc·html5
奋斗的袍子00710 小时前
Node.js多版本安装工具NVM详细使用教程
node.js·nvm
GUIQU.10 小时前
【Node.js】全栈开发实践
前端·node.js·全栈开发
码农捻旧19 小时前
Node.js Express 项目现代化打包部署全指南
javascript·node.js·github·express