【Node.js】工具链与工程化
个人主页:Guiat
归属专栏:node.js
文章目录
- 1. Node.js 工具链概述
- 1.1 工具链的作用
- 1.2 Node.js 工具链全景
- 2. 包管理与依赖管理
- 2.1 npm (Node Package Manager)
- 2.2 yarn
- 2.3 pnpm
- 2.4 锁文件与依赖管理
- 2.5 工作空间与 Monorepo
- 3. 构建工具与打包
- 3.1 Webpack
- 3.2 Rollup
- 3.3 esbuild
- 3.4 Babel
- 3.5 TypeScript 配置
- 3.6 构建工具比较
- 4. 代码质量与规范
- 4.1 ESLint
- 4.2 Prettier
- 4.3 Git Hooks 与 Husky
- 4.4 代码风格规范工作流
- 4.5 TypeScript 检查
- 5. 测试框架与测试策略
- 5.1 Jest 测试框架
- 5.2 API 测试与 Supertest
- 5.3 单元测试与模拟
- 5.4 测试覆盖率与报告
- 5.5 持续集成与测试
- 6. 自动化与 CI/CD 流程
- 6.1 GitHub Actions
- 6.2 Docker 与容器化
正文
1. Node.js 工具链概述
Node.js 工具链是一系列用于开发、测试、构建和部署 Node.js 应用程序的工具集合。完善的工具链可以显著提高开发效率,确保代码质量,简化部署流程。
1.1 工具链的作用
- 自动化开发工作流程
- 提高代码质量和一致性
- 简化依赖管理
- 优化构建过程
- 加速测试和部署
- 提高项目可维护性
1.2 Node.js 工具链全景
2. 包管理与依赖管理
2.1 npm (Node Package Manager)
npm 是 Node.js 的默认包管理器,提供了安装、更新和管理依赖的功能。
# 初始化新项目
npm init -y# 安装依赖包
npm install express# 安装开发依赖
npm install --save-dev jest# 全局安装包
npm install -g nodemon# 更新依赖
npm update# 运行脚本
npm run start
package.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 开发的包管理器,提供了更快的安装速度和更好的依赖解决方案。
# 初始化项目
yarn init -y# 安装依赖
yarn add express# 安装开发依赖
yarn add --dev jest# 全局安装
yarn global add nodemon# 更新依赖
yarn upgrade# 运行脚本
yarn start
2.3 pnpm
pnpm 是一个快速、节省磁盘空间的包管理器,采用了内容寻址存储方式。
# 安装 pnpm
npm install -g pnpm# 初始化项目
pnpm init# 安装依赖
pnpm add express# 安装开发依赖
pnpm add -D jest# 更新依赖
pnpm update# 运行脚本
pnpm start
2.4 锁文件与依赖管理
graph TDA[依赖管理] --> 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:#66CDAAstyle B fill:#87CEFAstyle C fill:#87CEFAstyle D fill:#87CEFA
语义化版本控制:
- 主版本号:不兼容的 API 更改
- 次版本号:向后兼容的功能性新增
- 修订版本号:向后兼容的问题修正
前缀含义:
^
:允许升级到任何保持主版本相同的版本~
:允许升级到任何保持主版本和次版本相同的版本- 无前缀:精确匹配版本
2.5 工作空间与 Monorepo
// package.json (workspaces 示例)
{"name": "monorepo-project","private": true,"workspaces": ["packages/*"],"scripts": {"start": "node scripts/start.js","test": "jest"}
}
// 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、图像等资源。
// 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 库构建,特别适合生成更小、更高效的包。
// 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 打包器和压缩器。
// 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 代码转换为向后兼容的版本。
// 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 配置
// 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 代码中的问题。
// .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 是一个代码格式化工具,确保代码风格统一。
// .prettierrc
{"semi": true,"singleQuote": true,"tabWidth": 2,"printWidth": 100,"trailingComma": "es5","arrowParens": "avoid","endOfLine": "lf"
}
与 ESLint 集成:
npm install --save-dev eslint-config-prettier eslint-plugin-prettier
// .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 生命周期的关键点上运行脚本。
# 安装 Husky
npm install --save-dev husky# 启用 Git hooks
npx husky install# 添加 pre-commit hook
npx husky add .husky/pre-commit "npm run lint-staged"
// package.json
{"scripts": {"prepare": "husky install"},"lint-staged": {"*.js": ["eslint --fix","prettier --write"],"*.{json,md}": ["prettier --write"]}
}
4.4 代码风格规范工作流
4.5 TypeScript 检查
# 安装 TypeScript
npm install --save-dev typescript @types/node# 生成 tsconfig.json
npx tsc --init
与 ESLint 集成:
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
// .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 项目。
// 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/']
};
示例测试文件:
// 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
// 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: 'test@example.com',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: 'test@example.com',password: 'password123'};// 创建第一个用户await request(app).post('/api/users').send(userData);// 尝试创建重复的用户const response = await request(app).post('/api/users').send({username: 'anotheruser',email: 'test@example.com', // 相同的邮箱password: 'password456'});expect(response.status).toBe(400);expect(response.body).toHaveProperty('error');});});
});
5.3 单元测试与模拟
// 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: 'test@example.com',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: 'test@example.com',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 测试覆盖率与报告
# 运行测试并生成覆盖率报告
npm test -- --coverage
5.5 持续集成与测试
# .github/workflows/test.yml
name: Teston:push:branches: [ main, develop ]pull_request:branches: [ main, develop ]jobs:test:runs-on: ubuntu-latestservices:mongodb:image: mongo:4.4ports:- 27017:27017strategy:matrix:node-version: [16.x, 18.x]steps:- uses: actions/checkout@v3- name: Use Node.js ${{ matrix.node-version }}uses: actions/setup-node@v3with:node-version: ${{ matrix.node-version }}cache: 'npm'- name: Install dependenciesrun: npm ci- name: Run lintingrun: npm run lint- name: Run tests with coveragerun: npm test -- --coverageenv:MONGO_URI_TEST: mongodb://localhost:27017/test-db- name: Upload coverage to Codecovuses: codecov/codecov-action@v3with:token: ${{ secrets.CODECOV_TOKEN }}
6. 自动化与 CI/CD 流程
6.1 GitHub Actions
# .github/workflows/ci.yml
name: CI/CDon:push:branches: [ main ]pull_request:branches: [ main ]jobs:build:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- name: Use Node.jsuses: actions/setup-node@v3with:node-version: '18.x'cache: 'npm'- name: Install dependenciesrun: npm ci- name: Lintrun: npm run lint- name: Testrun: npm test- name: Buildrun: npm run build- name: Upload build artifactsuses: actions/upload-artifact@v3with:name: buildpath: distdeploy:needs: buildif: github.event_name == 'push' && github.ref == 'refs/heads/main'runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- name: Download build artifactsuses: actions/download-artifact@v3with:name: buildpath: dist- name: Set up Docker Buildxuses: docker/setup-buildx-action@v2- name: Login to Docker Hubuses: docker/login-action@v2with:username: ${{ secrets.DOCKER_HUB_USERNAME }}password: ${{ secrets.DOCKER_HUB_TOKEN }}- name: Build and push Docker imageuses: docker/build-push-action@v4with:context: .push: truetags: username/my-node-app:latest- name: Deploy to productionuses: appleboy/ssh-action@masterwith:host: ${{ secrets.SSH_HOST }}username: ${{ secrets.SSH_USERNAME }}key: ${{ secrets.SSH_PRIVATE_KEY }}script: |cd /path/to/productiondocker-compose pulldocker-compose up -d
6.2 Docker 与容器化
# Dockerfile
FROM node:18-alpine as builderWORKDIR /appCOPY package*.json ./
RUN npm ciCOPY . .
RUN npm run buildFROM node:18-alpineWORKDIR /appCOPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modulesENV NODE_ENV=production
ENV PORT=3000EXPOSE 3000
CMD ["node", "dist/index.js"]
结语
感谢您的阅读!期待您的一键三连!欢迎指正!