强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Node.js 开发指南 / 第 25 章 · 实战项目

第 25 章 · 实战项目

25.1 项目一:全栈待办应用

技术栈

技术
前端React / Vue(可选)
后端Express + Prisma
数据库PostgreSQL
认证JWT
部署Docker + GitHub Actions

项目结构

todo-app/
├── src/
│   ├── config/
│   │   └── index.js
│   ├── api/
│   │   ├── routes/
│   │   │   ├── auth.js
│   │   │   ├── todos.js
│   │   │   └── index.js
│   │   ├── controllers/
│   │   ├── middlewares/
│   │   │   ├── auth.js
│   │   │   └── errorHandler.js
│   │   └── validators/
│   ├── services/
│   ├── repositories/
│   ├── utils/
│   ├── app.js
│   └── server.js
├── prisma/
│   └── schema.prisma
├── tests/
├── Dockerfile
└── docker-compose.yml

Prisma Schema

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  name      String
  todos     Todo[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Todo {
  id          Int      @id @default(autoincrement())
  title       String
  description String?
  completed   Boolean  @default(false)
  priority    String   @default("medium") // low, medium, high
  dueDate     DateTime?
  user        User     @relation(fields: [userId], references: [id])
  userId      Int
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

API 路由

// src/api/routes/todos.js
const { Router } = require('express');
const { authenticate } = require('../middlewares/auth');
const { validate } = require('../middlewares/validator');
const { todoSchema } = require('../validators/todo');
const todoController = require('../controllers/todoController');

const router = Router();

router.use(authenticate); // 所有路由都需要认证

router.get('/', todoController.getAll);
router.post('/', validate(todoSchema), todoController.create);
router.get('/:id', todoController.getById);
router.put('/:id', validate(todoSchema), todoController.update);
router.patch('/:id/toggle', todoController.toggle);
router.delete('/:id', todoController.delete);

module.exports = router;

Service 层

// src/services/todoService.js
const todoRepo = require('../repositories/todoRepo');
const { NotFoundError, ForbiddenError } = require('../utils/errors');

exports.getAll = async (userId, query) => {
  const { page = 1, limit = 20, completed, priority, search } = query;
  
  return todoRepo.findAll({
    where: {
      userId,
      ...(completed !== undefined && { completed: completed === 'true' }),
      ...(priority && { priority }),
      ...(search && { title: { contains: search, mode: 'insensitive' } }),
    },
    orderBy: { createdAt: 'desc' },
    skip: (Number(page) - 1) * Number(limit),
    take: Number(limit),
  });
};

exports.create = async (userId, data) => {
  return todoRepo.create({ ...data, userId });
};

exports.update = async (userId, todoId, data) => {
  const todo = await todoRepo.findById(todoId);
  if (!todo) throw new NotFoundError('待办事项');
  if (todo.userId !== userId) throw new ForbiddenError();
  return todoRepo.update(todoId, data);
};

exports.toggle = async (userId, todoId) => {
  const todo = await todoRepo.findById(todoId);
  if (!todo) throw new NotFoundError('待办事项');
  if (todo.userId !== userId) throw new ForbiddenError();
  return todoRepo.update(todoId, { completed: !todo.completed });
};

exports.delete = async (userId, todoId) => {
  const todo = await todoRepo.findById(todoId);
  if (!todo) throw new NotFoundError('待办事项');
  if (todo.userId !== userId) throw new ForbiddenError();
  return todoRepo.delete(todoId);
};

25.2 项目二:网页爬虫

技术栈

工具用途
axiosHTTP 请求
cheerioHTML 解析(类 jQuery)
puppeteer无头浏览器(动态页面)
p-limit并发控制

基本爬虫

npm install axios cheerio p-limit
// crawler.js
const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs/promises');

async function crawlArticle(url) {
  const { data: html } = await axios.get(url, {
    headers: {
      'User-Agent': 'Mozilla/5.0 (compatible; MyBot/1.0)',
    },
    timeout: 10000,
  });

  const $ = cheerio.load(html);

  return {
    url,
    title: $('h1').first().text().trim(),
    content: $('article').text().trim().substring(0, 5000),
    author: $('meta[name="author"]').attr('content') || '未知',
    date: $('time').attr('datetime') || null,
    images: $('article img')
      .map((_, el) => $(el).attr('src'))
      .get()
      .filter(Boolean),
  };
}

async function crawlSitemap(baseUrl) {
  const { data } = await axios.get(`${baseUrl}/sitemap.xml`);
  const $ = cheerio.load(data, { xmlMode: true });
  return $('url > loc')
    .map((_, el) => $(el).text())
    .get();
}

// 批量爬取(带并发控制)
const pLimit = require('p-limit');
const limit = pLimit(3); // 最多 3 个并发

async function crawlMultiple(urls) {
  const tasks = urls.map((url) =>
    limit(async () => {
      try {
        console.log(`爬取: ${url}`);
        const article = await crawlArticle(url);
        return article;
      } catch (err) {
        console.error(`失败: ${url} - ${err.message}`);
        return null;
      }
    })
  );

  const results = await Promise.all(tasks);
  return results.filter(Boolean);
}

// 主函数
async function main() {
  const urls = [
    'https://example.com/article/1',
    'https://example.com/article/2',
    'https://example.com/article/3',
  ];

  const articles = await crawlMultiple(urls);
  
  await fs.writeFile(
    'articles.json',
    JSON.stringify(articles, null, 2),
    'utf8'
  );

  console.log(`共爬取 ${articles.length} 篇文章`);
}

main().catch(console.error);

Puppeteer 爬虫(动态页面)

const puppeteer = require('puppeteer');

async function crawlDynamicPage(url) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
  });

  try {
    const page = await browser.newPage();
    
    // 设置视窗和 UA
    await page.setViewport({ width: 1280, height: 800 });
    await page.setUserAgent('Mozilla/5.0 (compatible; MyBot/1.0)');

    // 请求拦截(阻止图片/样式加载,提升速度)
    await page.setRequestInterception(true);
    page.on('request', (req) => {
      if (['image', 'stylesheet', 'font'].includes(req.resourceType())) {
        req.abort();
      } else {
        req.continue();
      }
    });

    await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });

    // 等待特定元素加载
    await page.waitForSelector('.article-content', { timeout: 5000 });

    // 提取数据
    const data = await page.evaluate(() => {
      return {
        title: document.querySelector('h1')?.textContent?.trim(),
        content: document.querySelector('.article-content')?.textContent?.trim(),
        images: Array.from(document.querySelectorAll('.article-content img'))
          .map(img => img.src),
      };
    });

    return data;
  } finally {
    await browser.close();
  }
}

爬虫礼仪

// robots.txt 解析
const robotsParser = require('robots-txt-parser');

async function canCrawl(url, userAgent) {
  const robots = await robotsParser.parse(url);
  return robots.isAllowed(url, userAgent);
}

// 爬虫配置
const config = {
  delay: 1000,        // 请求间隔(毫秒)
  maxRetries: 3,      // 最大重试次数
  timeout: 10000,     // 请求超时
  concurrency: 3,     // 并发数
  userAgent: 'MyBot/1.0 (contact@example.com)',
};

25.3 项目三:CLI 工具

技术栈

工具用途
commander命令行参数解析
inquirer交互式提示
chalk终端彩色输出
ora加载动画
cli-table3终端表格
npm install commander inquirer chalk ora cli-table3

项目结构

my-cli/
├── bin/
│   └── index.js           # 入口(#!/usr/bin/env node)
├── src/
│   ├── commands/
│   │   ├── init.js        # init 命令
│   │   ├── build.js       # build 命令
│   │   └── deploy.js      # deploy 命令
│   ├── utils/
│   │   ├── logger.js
│   │   └── config.js
│   └── index.js
├── package.json
└── README.md

package.json

{
  "name": "my-cli",
  "version": "1.0.0",
  "bin": {
    "my-cli": "./bin/index.js"
  },
  "type": "module",
  "engines": {
    "node": ">=20"
  }
}

CLI 实现

#!/usr/bin/env node
// bin/index.js
import { Command } from 'commander';
import chalk from 'chalk';
import { initCommand } from '../src/commands/init.js';
import { buildCommand } from '../src/commands/build.js';
import { deployCommand } from '../src/commands/deploy.js';

const program = new Command();

program
  .name('my-cli')
  .description('我的 CLI 工具')
  .version('1.0.0');

program
  .command('init')
  .description('初始化项目')
  .option('-t, --template <name>', '模板名称', 'default')
  .option('-y, --yes', '跳过确认', false)
  .action(initCommand);

program
  .command('build')
  .description('构建项目')
  .option('--minify', '压缩代码', false)
  .option('--sourcemap', '生成 sourcemap', false)
  .action(buildCommand);

program
  .command('deploy')
  .description('部署项目')
  .option('-e, --env <environment>', '部署环境', 'staging')
  .option('--dry-run', '模拟运行', false)
  .action(deployCommand);

program.parse();

交互式命令

// src/commands/init.js
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
import fs from 'fs/promises';
import path from 'path';

export async function initCommand(options) {
  console.log(chalk.bold('\n🚀 项目初始化\n'));

  let config = {};

  if (!options.yes) {
    config = await inquirer.prompt([
      {
        type: 'input',
        name: 'name',
        message: '项目名称:',
        default: 'my-project',
        validate: (input) => input.length > 0 || '请输入项目名称',
      },
      {
        type: 'list',
        name: 'template',
        message: '选择模板:',
        choices: ['default', 'typescript', 'express', 'monorepo'],
        default: options.template,
      },
      {
        type: 'checkbox',
        name: 'features',
        message: '选择特性:',
        choices: [
          { name: 'ESLint', checked: true },
          { name: 'Prettier', checked: true },
          { name: 'Docker', checked: false },
          { name: 'GitHub Actions', checked: false },
        ],
      },
    ]);
  }

  const spinner = ora('创建项目...').start();

  try {
    // 创建目录
    await fs.mkdir(config.name || 'my-project', { recursive: true });
    await fs.writeFile(
      path.join(config.name, 'package.json'),
      JSON.stringify({ name: config.name, version: '1.0.0' }, null, 2)
    );

    spinner.succeed(chalk.green('项目创建成功!'));
    console.log(`
${chalk.bold('下一步:')}
  cd ${config.name}
  npm install
  npm run dev
    `);
  } catch (err) {
    spinner.fail(chalk.red('创建失败'));
    console.error(err.message);
    process.exit(1);
  }
}

发布 CLI 工具

# 注册 npm 账号后
npm login

# 发布
npm publish

# 使用
npx my-cli init
# 或全局安装
npm install -g my-cli
my-cli init

25.4 项目四:微服务架构

架构设计

                    ┌──────────────┐
                    │   API 网关    │ :3000
                    │  (Express)   │
                    └──────┬───────┘
                           │
            ┌──────────────┼──────────────┐
            │              │              │
     ┌──────┴──────┐ ┌────┴──────┐ ┌─────┴─────┐
     │ 用户服务     │ │ 订单服务   │ │ 通知服务   │
     │ :3001       │ │ :3002     │ │ :3003     │
     └──────┬──────┘ └────┬──────┘ └─────┬─────┘
            │              │              │
     ┌──────┴──────┐ ┌────┴──────┐ ┌─────┴─────┐
     │  用户 DB    │ │  订单 DB   │ │  Redis    │
     └─────────────┘ └───────────┘ └───────────┘

Docker Compose 编排

# docker-compose.yml
version: '3.8'

services:
  api-gateway:
    build: ./services/gateway
    ports: ["3000:3000"]
    environment:
      USER_SERVICE_URL: http://user-service:3001
      ORDER_SERVICE_URL: http://order-service:3002
      NOTIFICATION_SERVICE_URL: http://notification-service:3003
    depends_on: [user-service, order-service]

  user-service:
    build: ./services/users
    environment:
      DATABASE_URL: postgresql://user:pass@user-db:5432/users
    depends_on:
      user-db: { condition: service_healthy }

  order-service:
    build: ./services/orders
    environment:
      DATABASE_URL: postgresql://user:pass@order-db:5432/orders
      USER_SERVICE_URL: http://user-service:3001
    depends_on:
      order-db: { condition: service_healthy }

  notification-service:
    build: ./services/notifications
    environment:
      REDIS_URL: redis://redis:6379
    depends_on: [redis]

  user-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: users
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes: [user-data:/var/lib/postgresql/data]
    healthcheck:
      test: pg_isready -U user -d users
      interval: 5s

  order-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: orders
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes: [order-data:/var/lib/postgresql/data]
    healthcheck:
      test: pg_isready -U user -d orders
      interval: 5s

  redis:
    image: redis:7-alpine

volumes:
  user-data:
  order-data:

服务间通信

// services/gateway/routes/index.js
const { Router } = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const router = Router();

// 代理到用户服务
router.use('/api/users', createProxyMiddleware({
  target: process.env.USER_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/users': '/users' },
}));

// 代理到订单服务
router.use('/api/orders', createProxyMiddleware({
  target: process.env.ORDER_SERVICE_URL,
  changeOrigin: true,
  pathRewrite: { '^/api/orders': '/orders' },
}));

module.exports = router;
// services/orders/services/userClient.js
const axios = require('axios');

class UserClient {
  constructor(baseUrl) {
    this.client = axios.create({
      baseURL: baseUrl,
      timeout: 5000,
    });
  }

  async getUser(userId) {
    try {
      const { data } = await this.client.get(`/users/${userId}`);
      return data;
    } catch (err) {
      if (err.response?.status === 404) return null;
      throw new Error(`用户服务不可用: ${err.message}`);
    }
  }
}

module.exports = new UserClient(process.env.USER_SERVICE_URL);

事件驱动通信

// 使用 Redis Pub/Sub 进行异步通信
const { createClient } = require('redis');

// 发布者(订单服务)
const publisher = createClient();
await publisher.connect();

async function createOrder(orderData) {
  const order = await db.orders.create(orderData);
  
  // 发布事件
  await publisher.publish('order:created', JSON.stringify({
    orderId: order.id,
    userId: order.userId,
    total: order.total,
    timestamp: Date.now(),
  }));

  return order;
}

// 订阅者(通知服务)
const subscriber = createClient();
await subscriber.connect();

await subscriber.subscribe('order:created', (message) => {
  const data = JSON.parse(message);
  console.log(`订单创建: ${data.orderId}`);
  sendNotification(data.userId, `您的订单 ${data.orderId} 已创建`);
});

25.5 学习路线图

入门(1-3 个月)
├── JavaScript 基础
├── Node.js 核心模块
├── Express 基础
└── 简单 REST API

进阶(3-6 个月)
├── 数据库(PostgreSQL/MongoDB)
├── 认证授权(JWT)
├── 测试(Jest)
├── Docker 基础
└── TypeScript 入门

高级(6-12 个月)
├── 微服务架构
├── 消息队列(RabbitMQ/Kafka)
├── CI/CD 流水线
├── 性能优化
├── 安全加固
└── Kubernetes 基础

专家(12+ 个月)
├── 分布式系统设计
├── 高并发架构
├── 开源贡献
└── 技术团队管理

注意事项

⚠️ 爬虫要遵守 robots.txt 和法律法规:不要对目标网站造成过大负载。

⚠️ CLI 工具要有良好的错误提示:用户看不懂的错误是糟糕的用户体验。

⚠️ 微服务不是银弹:小型项目使用单体架构更高效,当团队和业务规模增长时再考虑微服务。

⚠️ 实战中持续学习:每个项目都会遇到新问题,保持好奇心和学习习惯。

扩展阅读


上一章第 24 章 · 最佳实践

🎉 恭喜! 你已经完成了全部 25 章的 Node.js 开发指南。继续实践,不断精进!