nestjs项目快速搭建和部署

173 阅读3分钟

中文文档

一、创建项目工程

$ npm i -g @nestjs/cli
# nest new <project name>
$ nest new platform-server

二、项目工程基本配置

1. prettierrc配置

  • .prettierrc
{
    "singleQuote": true,
    "trailingComma": "all",
    "printWidth": 200,
    "tabWidth": 4,
    "endOfLine": "crlf",
    "useTabs": true
}
  • .vscode/settings.json
{
    "editor.formatOnSave": true, // Ctrl + S 保存代码自动格式化
    "editor.tabSize": 4,
    "prettier.printWidth": 1000, // 行內匹配代码数
    "prettier.bracketSameLine": true, // 超出后是否单独一行
    "prettier.tabWidth": 4,
    "prettier.useEditorConfig": false,
    "prettier.semi": true, // 是否带分号
    "prettier.singleQuote": true, // 是否为单引
    "prettier.proseWrap": "never",
    "prettier.endOfLine": "crlf",
    "prettier.requireConfig": true,
    "prettier.useTabs": true,
    "[jsonc]": {
        "editor.defaultFormatter": "vscode.json-language-features"
    },
    "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[vue]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[json]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[css]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[scss]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "[javascript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    },
    "stylelint.validate": [
        "css",
        "less",
        "scss",
        "vue"
    ],
    // 开启eslint检测
    "eslint.enable": true,
    "i18n-ally.localesPaths": [
        "src/i18n",
        "src/i18n/lang"
    ],
    "vite.autoStart": false,
    "vue.codeActions.enabled": false,
    "git.ignoreLimitWarning": true
}
  • .vscode/extensions.json
{
    "recommendations": [
        "esbenp.prettier-vscode",
    ],
}

2. eslint配置

  • eslint.config.mjs
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';

/** @type {import('eslint').Linter.Config[]} */
export default [
    { languageOptions: { globals: globals.browser } },
    pluginJs.configs.recommended,
    ...tseslint.configs.recommended,
    {
        files: ['**/*.{js,mjs,cjs,ts}'],
        rules: {
            '@typescript-eslint/no-explicit-any': 'off',
            '@typescript-eslint/interface-name-prefix': 'off',
            '@typescript-eslint/explicit-function-return-type': 'off',
            '@typescript-eslint/explicit-module-boundary-types': 'off',
        },
        ignores: ['node_modules/', 'build/', 'dist/'],
    },
];

3. tsconfig.json配置

{
    "compilerOptions": {
        "module": "commonjs",
        "declaration": true,
        "removeComments": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "allowSyntheticDefaultImports": true,
        "target": "ES2021",
        "sourceMap": true,
        "outDir": "./dist",
        "baseUrl": "./",
        "incremental": true,
        "skipLibCheck": true,
        "strictNullChecks": false,
        "noImplicitAny": false,
        "strictBindCallApply": false,
        "forceConsistentCasingInFileNames": false,
        "noFallthroughCasesInSwitch": false,
        // ☆☆☆☆☆ 可以使用import xx from 'xx'的形式引入模块,兼容CommonJS 和 ES6 模块
        "esModuleInterop": true,
        "paths": {
            "@/*": [
                "src/*"
            ]
        }
    },
    "include": [
        "src/**/*",
        "./types/**/*.d.ts",
        "test/**/*"
    ]
}

4. tsconfig.build.json配置

{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "sourceMap": false,
        "module": "commonjs",
    },
    "exclude": [
        "node_modules",
        "test",
        "dist",
        "build",
        "**/*spec.ts"
    ]
}

5. nest-cli.json配置

{
    "$schema": "https://json.schemastore.org/nest-cli",
    "collection": "@nestjs/schematics",
    "sourceRoot": "src",
    "compilerOptions": {
        "deleteOutDir": true,
        "builder": "tsc"
    }
}

6. package.json配置

  • 安装的依赖包根据项目具体需求
{
    "name": "XXX",
    "version": "0.0.1",
    "description": "XXX",
    "author": "huangteng",
    "private": true,
    "scripts": {
        "build": "nest build && node scripts/esbuild.config.mjs",
        "format": "prettier --write "src/**/*.ts" "test/**/*.ts"",
        "start": "nest start",
        "start:dev": "nest start --watch",
        "start:debug": "nest start --debug --watch",
        "start:prod": "node dist/main",
        "lint": "eslint "{src,apps,libs,test}/**/*.ts" --fix",
        "test": "jest",
        "test:watch": "jest --watch",
        "test:cov": "jest --coverage",
        "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
        "test:e2e": "jest --config ./test/jest-e2e.json",
        "prettier": "prettier --config .prettierrc -l -w "./src/**/*.{ts,js,json}"",
        "prepare": "git init && concurrently "pnpm run postinstall"",
        "postinstall": "simple-git-hooks",
        "build:prod": "set "NODE_ENV=production" && rollup -c scripts/rollup.build.prod.mjs"
    },
    "dependencies": {
        "@nestjs/axios": "^3.1.3",
        "@nestjs/cache-manager": "^2.3.0",
        "@nestjs/common": "^10.4.15",
        "@nestjs/core": "^10.4.15",
        "@nestjs/jwt": "^10.2.0",
        "@nestjs/mapped-types": "^2.1.0",
        "@nestjs/passport": "^10.0.3",
        "@nestjs/platform-express": "^10.4.15",
        "@nestjs/schedule": "^4.1.2",
        "@nestjs/serve-static": "^5.0.2",
        "@nestjs/swagger": "^8.1.1",
        "@nestjs/throttler": "^6.4.0",
        "@nestjs/typeorm": "^10.0.2",
        "axios": "^1.7.9",
        "class-transformer": "^0.5.1",
        "class-validator": "^0.14.1",
        "compression": "^1.7.5",
        "crypto-js": "^4.2.0",
        "dayjs": "^1.11.13",
        "express": "^4.21.2",
        "express-session": "^1.18.1",
        "geoip-lite": "^1.4.10",
        "helmet": "^8.0.0",
        "ioredis": "^5.5.0",
        "multer": "1.4.5-lts.1",
        "mysql2": "^3.12.0",
        "pg": "^8.13.1",
        "reflect-metadata": "^0.2.2",
        "rxjs": "^7.8.1",
        "svg-captcha": "^1.4.0",
        "typeorm": "^0.3.20",
        "uuid": "^11.0.5"
    },
    "devDependencies": {
        "@eslint/js": "^9.20.0",
        "@nestjs/cli": "^10.4.9",
        "@nestjs/schematics": "^10.2.3",
        "@nestjs/testing": "^10.4.15",
        "@rollup/plugin-alias": "^5.1.1",
        "@rollup/plugin-commonjs": "^28.0.2",
        "@rollup/plugin-json": "^6.1.0",
        "@rollup/plugin-node-resolve": "^15.3.1",
        "@rollup/plugin-replace": "^6.0.2",
        "@rollup/plugin-terser": "^0.4.4",
        "@rollup/plugin-typescript": "^12.1.2",
        "@rollup/plugin-url": "^8.0.2",
        "@types/compression": "^1.7.5",
        "@types/crypto-js": "^4.2.2",
        "@types/express": "^5.0.0",
        "@types/express-session": "^1.18.1",
        "@types/geoip-lite": "^1.4.4",
        "@types/jest": "^29.5.14",
        "@types/multer": "^1.4.12",
        "@types/node": "^20.17.17",
        "@types/supertest": "^6.0.2",
        "@types/uuid": "^10.0.0",
        "@typescript-eslint/eslint-plugin": "^8.23.0",
        "@typescript-eslint/parser": "^8.23.0",
        "concurrently": "^9.1.2",
        "esbuild": "^0.24.2",
        "eslint": "^9.20.0",
        "eslint-config-prettier": "^9.1.0",
        "eslint-plugin-prettier": "^5.2.3",
        "globals": "^15.14.0",
        "jest": "^29.7.0",
        "lint-staged": "^15.4.3",
        "prettier": "^3.4.2",
        "rollup": "^4.34.6",
        "rollup-plugin-copy": "^3.5.0",
        "simple-git-hooks": "^2.11.1",
        "source-map-support": "^0.5.21",
        "supertest": "^7.0.0",
        "ts-jest": "^29.2.5",
        "ts-loader": "^9.5.2",
        "ts-node": "^10.9.2",
        "tsconfig-paths": "^4.2.0",
        "typescript": "^5.7.3",
        "typescript-eslint": "^8.23.0"
    },
    "simple-git-hooks": {
        "pre-commit": "pnpm lint-staged"
    },
    "lint-staged": {
        "*.{ts,tsx,js,jsx,json}": [
            "eslint --fix"
        ]
    },
    "jest": {
        "moduleFileExtensions": [
            "js",
            "json",
            "ts"
        ],
        "rootDir": "src",
        "testRegex": ".*\.spec\.ts$",
        "transform": {
            "^.+\.(t|j)s$": "ts-jest"
        },
        "collectCoverageFrom": [
            "**/*.(t|j)s"
        ],
        "coverageDirectory": "../coverage",
        "testEnvironment": "node"
    }
}

7. 创建scripts打包构建的脚步文件

  • esbuild构建项目,保持项目工程的目录结构(不构建为单文件),只打包源码。
  • scripts/esbuild.config.mjs
import { build } from 'esbuild';
import fs from 'fs';
import path from 'path';
// 定义输出目录
const outDir = 'build';
const outputDir = path.resolve(outDir);
// 确保输出目录存在
if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
} else {
    // 目录存在,先清空目录内容
    const files = fs.readdirSync(outputDir);
    files.forEach((file) => {
        const filePath = path.join(outputDir, file);
        const stat = fs.statSync(filePath);
        if (stat.isDirectory()) {
            // 如果是目录,递归删除
            fs.rmSync(filePath, { recursive: true });
        } else {
            // 如果是文件,删除文件
            fs.unlinkSync(filePath);
        }
    });
}
// 复制文件的实用函数
const copyFile = (src, dest) => {
    if (!fs.existsSync(src)) return;
    const stat = fs.statSync(src);
    if (stat.isDirectory()) {
        if (!fs.existsSync(dest)) {
            fs.mkdirSync(dest, { recursive: true });
        }
        fs.readdirSync(src).forEach((file) => {
            copyFile(path.join(src, file), path.join(dest, file));
        });
    } else {
        fs.copyFileSync(src, dest);
    }
};
const copyStaticFiles = () => {
    // 复制目标文件
    const targets = [
        // { src: 'public', dest: path.join(outputDir, 'public') },
        { src: 'localstorage', dest: path.join(outputDir, 'localstorage') },
        { src: 'docker-compose.yml', dest: path.join(outputDir, 'docker-compose.yml') },
        { src: 'Dockerfile', dest: path.join(outputDir, 'Dockerfile') },
    ];
    // 执行复制
    targets.forEach(({ src, dest }) => {
        copyFile(path.resolve(src), dest);
    });
    // 处理 package.json
    const packageJsonPath = path.resolve('package.json');
    if (fs.existsSync(packageJsonPath)) {
        const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
        const packageJson = { ...pkg };
        delete packageJson.scripts;
        delete packageJson['simple-git-hooks'];
        delete packageJson['lint-staged'];
        delete packageJson.jest;
        delete packageJson['devDependencies'];
        const packageJsonContent = JSON.stringify(packageJson, null, 4);
        fs.writeFileSync(path.join(outputDir, 'package.json'), packageJsonContent, 'utf-8');
    }
    console.log('文件复制完成!');
};

build({
    entryPoints: ['./dist/**/*.js'],
    outdir: `./${outDir}/dist`,
    bundle: false, // false不打包为单文件
    minify: true, // 压缩代码
    minifyIdentifiers: false, // 禁用标识压缩
    minifySyntax: false, //禁用语法压缩
    sourcemap: false,
    keepNames: true, // 保留所有标识符的原始名称,处理关联查询报错问题
    platform: 'node',
    format: 'cjs',
    target: 'es2018',
    define: {
        // 'process.env.NODE_ENV': '"production"',
    },
})
    .then(() => {
        copyStaticFiles();
        console.log('Build complete');
    })
    .catch(() => {
        console.log('Build error');
    });
  • rollup打包构建,打包为单文件
  • scripts/rollup.build.prod.mjs
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import terser from '@rollup/plugin-terser';
import url from '@rollup/plugin-url';
import copy from 'rollup-plugin-copy';
import replace from '@rollup/plugin-replace';
import path from 'path';
import fs from 'fs';

// 定义输出目录
const outDir = 'build-prod';
const outputDir = path.resolve(outDir);
// 确保输出目录存在
if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
} else {
    // 目录存在,先清空目录内容
    const files = fs.readdirSync(outputDir);
    files.forEach((file) => {
        const filePath = path.join(outputDir, file);
        const stat = fs.statSync(filePath);
        if (stat.isDirectory()) {
            // 如果是目录,递归删除
            fs.rmSync(filePath, { recursive: true });
        } else {
            // 如果是文件,删除文件
            fs.unlinkSync(filePath);
        }
    });
}

const files_config = [
    {
        fileName: 'Dockerfile',
        content: [
            'FROM node:20.18.0',
            'WORKDIR /app',
            '',
            '# 设置时区为 Asia/Shanghai',
            'ENV TZ=Asia/Shanghai',
            '',
            'COPY package*.json ./',
            'RUN npm config set registry https://registry.npmmirror.com',
            '',
            'RUN npm install -g pnpm',
            '',
            'RUN npm install -g pm2',
            '',
            'RUN pnpm install --prod',
            '',
            'COPY . .',
            '',
            'CMD ["pm2-runtime", "start", "dist/main.js"]',
        ].join('\n'),
    },
    {
        fileName: 'docker-compose.yml',
        content: [
            'version: "3"',
            '',
            'services:',
            '  farmpack-service:',
            '    image: farmpack-service',
            '    build:',
            '      context: .',
            '      dockerfile: Dockerfile',
            '    ports:',
            '      - "3100:3100"',
            '    volumes:',
            '      - ./localstorage:/app/localstorage',
            '      - ./public:/app/public',
            '      - ./logs:/app/logs',
            '    environment:',
            '      - PORT=3100',
            '      - NODE_ENV=production',
        ].join('\n'),
    },
];
files_config.forEach((target) => {
    fs.writeFileSync(path.join(outputDir, target.fileName), target.content, 'utf-8');
    console.log(`✅ ${target.fileName} 文件生成完成!`);
});
// 处理 package.json
const packageJsonPath = path.resolve('package.json');
if (fs.existsSync(packageJsonPath)) {
    const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
    const packageJson = { ...pkg };
    delete packageJson.scripts;
    delete packageJson['simple-git-hooks'];
    delete packageJson['lint-staged'];
    delete packageJson.jest;
    delete packageJson['devDependencies'];
    const packageJsonContent = JSON.stringify(packageJson, null, 4);
    fs.writeFileSync(path.join(outputDir, 'package.json'), packageJsonContent, 'utf-8');
    console.log('✅ package.json 文件复制完成!');
}

export default {
    input: './dist/main.js',
    output: {
        format: 'cjs',
        dir: outputDir + '/dist',
        entryFileNames: 'main.js',
    },
    plugins: [
        nodeResolve({
            preferBuiltins: true, // 确保 Node.js 内置模块被处理
        }),
        commonjs(),
        json(),
        replace({
            preventAssignment: true,
            // 'process.env.NODE_ENV': JSON.stringify('production'),
        }),
        terser({
            compress: {
                warnings: false,
                drop_console: false,
                drop_debugger: true,
            },
            format: {
                comments: false, 
                beautify: false, 
            },
            mangle: false, // 不混淆变量名
        }),
        url({
            limit: 0,
            include: ['**/*.ttf'],
            fileName: '[name][extname]',
            destDir: path.resolve(outDir, 'public'),
            publicPath: './',
            emitFiles: true,
        }),
        copy({
            targets: [
                {
                    src: 'public/*',
                    dest: path.resolve(outDir, 'public'),
                },
                {
                    src: 'templates/*',
                    dest: path.resolve(outDir, 'templates'),
                },
                {
                    src: 'localstorage/*',
                    dest: path.resolve(outDir, 'localstorage'),
                },
            ],
        }),
    ],
    external: [
        '@nestjs/common',
        '@nestjs/core',
        '@nestjs/jwt',
        '@nestjs/mapped-types',
        '@nestjs/passport',
        '@nestjs/platform-express',
        '@nestjs/typeorm',
        '@nestjs/serve-static',
        '@nestjs/swagger',
        'class-transformer',
        'class-validator',
        'crypto-js',
        'dayjs',
        'express-session',
        'mysql2',
        'reflect-metadata',
        'rxjs',
        'svg-captcha',
        'typeorm',
        'geoip-lite',
    ],
    onwarn: (warning, warn) => {
        if (warning.code === 'CIRCULAR_DEPENDENCY') {
            // 忽略关于 '(!) Circular dependency' 的警告
            return;
        }
        warn(warning);
    },
};

8. 创建全局ts类型声明的扩展(根据tsconfig.json的配置创建对应的目录)

  • types/global.d.ts
declare global {
    interface QueryPageInterface {
        pageIndex: number;
        pageSize: number;
    }
    interface ResponsePageInterface<T = any> {
        records: T[];
        total: number;
        pageIndex?: number;
        pageSize?: number;
    }
    interface ResponseIngerface<T = any> {
        code: number;
        message?: string;
        data?: T;
    }
}

export {};
  • types/express.d.ts
import type { Request as Req } from 'express';

declare global {
    namespace Express {
        namespace Multer {
            interface File {
                _path?: string;
            }
        }
    }
}

declare module 'express' {
    interface Request extends Req {
        userInfo?: any;
        isAdmin?: boolean;
        isSuperAdmin?: boolean;
    }
}

declare module '@nestjs/common' {
    interface Request extends Req {
        userInfo?: any;
        isAdmin?: boolean;
        isSuperAdmin?: boolean;
    }
}

三、项目工程基本目录结构

1. 整体工程目录结构

│  .gitignore
│  .prettierrc
│  docker-compose.yml
│  Dockerfile
│  ecosystem.config.js
│  eslint.config.mjs
│  nest-cli.json
│  package.json
│  pnpm-lock.yaml
│  README.md
│  tsconfig.build.json
│  tsconfig.json
│  
├─.vscode
│      extensions.json
│      settings.json
│
├─scripts
│      esbuild.config.mjs
│      rollup.build.prod.mjs
│ 
├─src
│  │  app.module.ts
│  │  main.ts
│  │  swagger.ts
│  │	... 
├─test
│      app.e2e-spec.ts
│      jest-e2e.json
│      
└─types
        express.d.ts
        global.d.ts

2. 源码src下目录结构

│  app.module.ts
│  main.ts
│  swagger.ts
│  
├─common
│  ├─decorators
│  │      public.decorator.ts
│  │      tenant.decorator.ts
│  │      
│  ├─exceptions
│  ├─filters
│  │      catch.filter.ts
│  │      
│  ├─interceptors
│  │      http-status.interceptor.ts
│  │      tenant.intercepter.ts
│  │      
│  ├─middleware
│  │      permission.middleware.ts
│  │      request-logger.middleware.ts
│  │      
│  ├─pipes
│  │      file.validation.pipe.ts
│  │      remove-unknown-filds.pipe.ts
│  │      
│  └─utils
│          crypto-js.ts
│          jwt-auth.guard.ts
│          methods.ts
│          multer-file.ts
│          redis.ts
│          response.ts
│          tools.ts
│          
├─config
│      enums.ts
│      env.ts
│
└─modules
    ├─common
    │  │  common.controller.ts
    │  │  common.module.ts
    │  │  common.service.ts  
    │  │	...
    │  ...         
    └─system
        │  system.module.ts
        │  
        ├─dept
        │  │  dept.controller.ts
        │  │  dept.module.ts
        │  │  dept.service.ts
        │  │  
        │  ├─dto
        │  │      create-dept.dto.ts
        │  │      update-dept.dto.ts
        │  │      
        │  └─entities
        │          dept.entity.ts
        │          
        ├─file
        │  │  file.controller.ts
        │  │  file.module.ts
        │  │  file.service.ts
        │  │  
        │  ├─dto
        │  │      create-file.dto.ts
        │  │      update-file.dto.ts
        │  │      
        │  └─entities
        │          file.entity.ts
        │          
        ├─menu
        │  │  menu.controller.ts
        │  │  menu.module.ts
        │  │  menu.service.ts
        │  │  
        │  ├─dto
        │  │      create-menu.dto.ts
        │  │      update-menu.dto.ts
        │  │      
        │  └─entities
        │          menu.entity.ts
        │          
        ├─region
        │  │  region.controller.ts
        │  │  region.module.ts
        │  │  region.service.ts
        │  │  
        │  ├─dto
        │  │      create-region.dto.ts
        │  │      update-region.dto.ts
        │  │      
        │  └─entities
        │          region.entity.ts
        │          
        ├─role
        │  │  role.controller.ts
        │  │  role.module.ts
        │  │  role.service.ts
        │  │  
        │  ├─dto
        │  │      create-role.dto.ts
        │  │      update-role.dto.ts
        │  │      
        │  └─entities
        │          role.entity.ts
        │          
        ├─tenant
        │  │  tenant.controller.ts
        │  │  tenant.module.ts
        │  │  tenant.service.ts
        │  │  
        │  ├─dto
        │  │      create-tenant.dto.ts
        │  │      update-tenant.dto.ts
        │  │      
        │  └─entities
        │          tenant.entity.ts
        │          
        ├─tlog
        │  │  tlog.controller.ts
        │  │  tlog.module.ts
        │  │  tlog.service.ts
        │  │  
        │  ├─dto
        │  │      create-tlog.dto.ts
        │  │      update-tlog.dto.ts
        │  │      
        │  └─entities
        │          tlog.entity.ts
        │          
        └─user
            │  user.controller.ts
            │  user.module.ts
            │  user.service.ts
            │  
            ├─dto
            │      user.dto.ts
            │      
            └─entities
                    user.entity.ts

四、项目基础功能及源码

1. 主入口main.ts文件

import { NestFactory } from '@nestjs/core';
import compression from 'compression';
import { AppModule } from './app.module';
import session from 'express-session';
import helmet from 'helmet';
import express from 'express';
import { CatchExceptionsFilter } from './common/filters/catch.filter';
import { session_secret } from './config/env';
import { SwaggerApp } from './swagger';

async function bootstrap() {
    const app = await NestFactory.create(AppModule, {
        // 'log' | 'error' | 'warn' | 'debug' | 'verbose' | 'fatal'
        logger: ['error', 'warn', 'debug', 'verbose'],
    });
    // 设置请求体的大小限制为 50MB
    app.use(express.json({ limit: '50mb' }));
    app.use(express.urlencoded({ limit: '50mb', extended: true }));
    app.use(
        helmet({
            frameguard: {
                action: 'sameorigin',
            },
            hidePoweredBy: true, // 隐藏 X-Powered-By 标头,避免泄露技术栈信息。
            noSniff: true, // 防止浏览器猜测响应的 MIME 类型。
            xssFilter: true, // 防止跨站脚本攻击(XSS)。
            crossOriginEmbedderPolicy: true, // 防止跨源嵌入(COEP)。
            crossOriginOpenerPolicy: true, // 防止跨源打开(COOP)。
            crossOriginResourcePolicy: true, // 防止跨源资源共享(CORS)。
            contentSecurityPolicy: false, // 禁用 CSP,才可以通过ip访问@nestjs/swagger
        }),
    );
    app.use(
        compression({
            filter: (req, res) => {
                if (req.headers['x-no-compression']) {
                    // 不要压缩
                    return false;
                }
                // gzip压缩指定文件类型
                const contentType = (res.getHeader('Content-Type') || '') as string;
                const compressibleTypes = [
                    'application/json',
                    'application/javascript',
                    'text/css',
                    'image/jpeg',
                    'image/png',
                    'image/gif',
                    'image/webp',
                    'font/woff',
                    'font/woff2',
                    'application/font-woff',
                    'application/font-woff2',
                    'font/ttf',
                    'application/font-ttf',
                    'font/otf',
                    'image/svg+xml',
                    'application/vnd.ms-fontobject',
                ];
                return compressibleTypes.some((type) => contentType.includes(type));
            },
        }),
    );
    app.use(
        session({
            secret: session_secret,
            resave: true, 
            saveUninitialized: false,
            rolling: true, 
            cookie: {
                maxAge: 1000 * 60 * 5, // 5分钟时间
                httpOnly: true,
            },
        }),
    );
    app.useGlobalFilters(new CatchExceptionsFilter());
    SwaggerApp(app);
    await app.listen(process.env.PORT ?? 3000);
    console.log(`Application is running on: ${await app.getUrl()}`);
    console.log(`NODE_ENV: ${process.env.NODE_ENV || 'development'}`);
}
bootstrap();

2. app.module.ts 文件

import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { JwtModule } from '@nestjs/jwt';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { jwt_secret, jwt_expires_in, DATABASE_CONFIG, PG_DB_CONFIG, PUBLIC_DIR, STORAGE_FILE_DIR } from './config/env';
import { DB_CONNECTION_NAME_ENUM } from './config/enums';
import { JwtAuthGuard } from './common/utils/jwt-auth.guard';
import { HttpStatusInterceptor } from './common/interceptors/http-status.interceptor';
import { RequestLoggerMiddleware } from './common/middleware/request-logger.middleware';
import { OauthModule } from './modules/oauth/oauth.module';
import { SystemModule } from './modules/system/system.module';

@Module({
    imports: [
        TypeOrmModule.forRoot({
            type: 'mysql',
            ...DATABASE_CONFIG['xxx'],
            legacySpatialSupport: false, // true不支持空间类型
            // // synchronize: true, 
            autoLoadEntities: true, 
        }),
        TypeOrmModule.forRoot({
            name: DB_CONNECTION_NAME_ENUM.POSTGRES_IOT,
            type: 'postgres',
            host: PG_DB_CONFIG.host,
            port: PG_DB_CONFIG.port,
            database: PG_DB_CONFIG.database,
            username: PG_DB_CONFIG.username,
            password: PG_DB_CONFIG.password,
            autoLoadEntities: true, 
        }),
        JwtModule.register({
            global: true,
            secret: jwt_secret,
            signOptions: { expiresIn: jwt_expires_in },
        }),
        ServeStaticModule.forRoot({
            rootPath: join(__dirname, '..', PUBLIC_DIR),
            serveRoot: '/static静态资源',
            serveStaticOptions: {
                cacheControl: true,
            },
        }),
        ThrottlerModule.forRoot([
            {
                ttl: 60 * 1000,
                limit: 100,
            },
        ]),
        OauthModule,
        SystemModule,
    ],
    controllers: [],
    providers: [
        {
            provide: APP_INTERCEPTOR,
            useClass: HttpStatusInterceptor,
        },
        {
            provide: APP_GUARD,
            useClass: JwtAuthGuard,
        },
        {
            provide: APP_GUARD,
            useClass: ThrottlerGuard,
        },
    ],
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(RequestLoggerMiddleware)
            .exclude(
                {
                    path: '/oauth/captcha',
                    method: RequestMethod.ALL,
                },
                '/system/tlog/(.*)',
                '/iot/(.*)',
                '/static/(.*)',
            )
            .forRoutes('*');
    }
}

3. 常用工具与方法

  • crypto-js和crypto
import CryptoJS from 'crypto-js';
import crypto from 'crypto';

const key_str = 'xxx';
// 加密
export const Encrypt = function (val: string) {
    const key = CryptoJS.enc.Utf8.parse(key_str).toString();
    const srcs = CryptoJS.enc.Utf8.parse(val);
    const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
    return encrypted.toString();
};
// 解密
export const Decrypt = function (val: string) {
    const key = CryptoJS.enc.Utf8.parse(key_str).toString();
    const decrypted = CryptoJS.AES.decrypt(val, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
    return decrypted.toString(CryptoJS.enc.Utf8);
};
/**
 * bufer数据加密
 * @returns
 */
export const cryptoBuffer = () => {
    const encryptionKey = 'xxx';//32位
    const iv = crypto.randomBytes(16);
    const Encrypt = function (fileBuffer: Buffer) {
        const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
        const encryptedBuffer = Buffer.concat([cipher.update(fileBuffer), cipher.final()]);
        return encryptedBuffer;
    };
    return {
        iv,
        Encrypt,
    };
};
  • jwt认证拦截
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { jwt_secret } from '@/config/env';
import { IS_PUBLIC_KEY } from '@/common/decorators/public.decorator';
import { Reflector } from '@nestjs/core';

@Injectable()
    export class JwtAuthGuard implements CanActivate {
        constructor(
            private jwtService: JwtService,
            private reflector: Reflector,
        ) {}

        async canActivate(context: ExecutionContext): Promise<boolean> {
            const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [context.getHandler(), context.getClass()]);
            if (isPublic) {
                return true;
            }
            const request = context.switchToHttp().getRequest();
            const token = this.extractTokenFromHeader(request);
            if (!token) {
                throw new UnauthorizedException();
            }
            try {
                const payload = await this.jwtService.verifyAsync(token, {
                    secret: jwt_secret,
                });
                request['userInfo'] = payload;
            } catch {
                throw new UnauthorizedException();
            }
            return true;
        }

        private extractTokenFromHeader(request: Request): string | undefined {
            const [type, token] = request.headers.authorization?.split(' ') ?? [];
            return type === 'Bearer' ? token : undefined;
        }
    }
  • 常用基础方法
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);

/**
 * 数据库中的时间转换为本地时间
 * @param date
 * @param format
 * @returns
 */
export const utcTransformLocal = (date: dayjs.ConfigType, format = 'YYYY-MM-DD HH:mm:ss') => {
    if (!date) return null;
    return dayjs.utc(date).local().format(format);
};
/**
 * 格式化时间
 * @param {*} date
 * @param {*} format
 * @returns
 */
export const formatDate = (date: Date, format = 'YYYY-MM-DD HH:mm:ss'): string => {
    let time = date;
    if (!time) time = new Date();
    return dayjs(time).format(format);
};
/**
 * 格式时间 YYYYMMDDHHmmss
 * @param date
 * @returns
 */
export const compactDatetimeFormat = (date: Date): string => {
    let time = date;
    if (!time) time = new Date();
    return formatDate(time, 'YYYYMMDDHHmmss');
};
/**
 * 解析时间 YYYYMMDDHHmmss -> YYYY-MM-DD HH:mm:ss
 * @param dateString
 * @param format
 * @returns
 * */
export const parseCompactDatetimeFormat = (dateString: string, format = 'YYYY-MM-DD HH:mm:ss'): string => {
    const parsedDate = dayjs(dateString, 'YYYYMMDDHHmmss');
    return parsedDate.format(format);
};
/**
 * 常规列表数据转数结构
 * @param list
 * @param rowkey
 * @param relationKey
 * @returns
 */
export const arrayTransformTree = (list: any[], rowkey = 'id', relationKey = 'parentId', rootValue = 0) => {
    const nodeMap = {};
    const roots = [];
    list.forEach((item) => {
        const id = item[rowkey];
        const parentId = item[relationKey];
        nodeMap[id] = nodeMap[id] || { children: [] };
        Object.assign(nodeMap[id], {
            ...item,
            [rowkey]: id,
            [relationKey]: parentId,
        });
        const node = nodeMap[id];
        if (parentId === null || parentId === rootValue) {
            roots.push(node);
        } else {
            nodeMap[parentId] = nodeMap[parentId] || { children: [] };
            nodeMap[parentId].children.push(node);
        }
    });
    return roots;
};
/**
 * 权限菜单树结构,增加权限按钮
 * @param list
 * @param btns
 * @param rowkey
 * @param relationKey
 * @returns
 */
export const arrayTransformPermissionMenuTree = (list: any[], btns = {}, rowkey = 'id', relationKey = 'parentId') => {
    const nodeMap = {};
    const roots = [];
    list.forEach((item) => {
        const id = item[rowkey];
        const parentId = item[relationKey];
        nodeMap[id] = nodeMap[id] || { children: [] };
        Object.assign(nodeMap[id], {
            ...item,
            [rowkey]: id,
            [relationKey]: parentId,
        });
        const node = nodeMap[id];
        if (btns[id]) {
            node.permissionBtns = btns[id];
        }
        if (parentId === null || parentId === 0) {
            roots.push(node);
        } else {
            nodeMap[parentId] = nodeMap[parentId] || { children: [] };
            nodeMap[parentId].children.push(node);
        }
    });
    const loopLevel = (list, level = 1) => {
        list.forEach((item) => {
            item.level = level;
            if (item.children && item.children.length > 0) {
                loopLevel(item.children, level + 1);
            }
        });
        return list;
    };
    return loopLevel(roots);
};
/**
 * 将wkt格式的点坐标解析为经纬度
 * @param wkt
 * @returns
 */
export const parseWktPoint = (wkt: string): [number, number] => {
    if (!wkt) return;
    const regex = /^POINT((-?\d+.\d+) (-?\d+.\d+))$/;
    const match = wkt.match(regex);
    if (match) {
        const longitude = parseFloat(match[1]);
        const latitude = parseFloat(match[2]);
        return [longitude, latitude];
    } else {
        throw new Error('Invalid WKT format');
    }
};
/**
 * 将wkt格式的多边形坐标解析为经纬度数组
 * @param wkt
 * @returns
 */
export const parseWktPolygon = (wkt: string): [number, number][] => {
    const regex = /^POLYGON(((.*)))$/;
    const match = wkt.match(regex);
    if (match) {
        const rings = match[1].split(',').map((lnglat) => lnglat.split(' ').map((coord) => parseFloat(coord)) as [number, number]);
        const start = rings[0];
        const end = rings[rings.length - 1];
        // 判断是否闭合, 若闭合则删除最后一个坐标【只是返回前端时做处理】
        if (start[0] === end[0] && start[1] === end[1]) {
            rings.pop();
        }
        const paths: [number, number][] = rings;
        return paths;
    } else {
        throw new Error('Invalid WKT format for POLYGON');
    }
};
  • 文件处理
import fs from 'fs';
import path from 'path';
import { v4 as UUID } from 'uuid';
import { diskStorage } from 'multer';
import { STORAGE_FILE_DIR } from '@/config/env';
import { formatDate } from '@/common/utils/methods';

/**
 * 获取文件路径
 * @param type
 * @param filename
 * @returns
 */
export const getResourceFilePath = (type: string, filename: string, ext: string) => {
    const filePath = path.join(STORAGE_FILE_DIR, type, filename + ext);
    if (!fs.existsSync(filePath)) {
        return '';
    }
    return filePath;
};
export const getFileFullPath = (filePath: string) => {
    const fullPath = path.resolve(process.cwd(), STORAGE_FILE_DIR, filePath);
    return fullPath;
};

/**
 * 自动创建文件目录
 * @param dir
 * @returns
 */
export const mkdirSync = (dir: string) => {
    const dirname = path.resolve(process.cwd(), dir);
    if (fs.existsSync(dirname)) {
        return true;
    } else {
        if (mkdirSync(path.dirname(dirname))) {
            fs.mkdirSync(dirname, { recursive: true });
            return true;
        }
    }
    return false;
};
/**
 * 生成uuid 文件名
 * @param ext
 * @returns
 */
export const formatFileNameToUUID = (ext = '') => {
    const uuid = UUID().replace(/-/g, '');
    const filename = formatDate(new Date(), 'YYYY-MM-DD~HH-mm') + '__' + uuid + ext;
    return filename;
};
/**
 * 根据文件类型,存储文件到类型目录下
 * @returns
 */
export const diskStorageFileType = () => {
    const storate = diskStorage({
        destination: (req, file, callback) => {
            const mimeType = file.mimetype;
            const time = formatDate(new Date(), 'YYYY-MM-DD');
            let root_dir = `${STORAGE_FILE_DIR}/uploads`;
            if (mimeType.startsWith('image/')) {
                root_dir += '/images';
            } else if (mimeType === 'application/pdf') {
                root_dir += '/pdfs';
            } else if (mimeType === 'application/msword') {
                root_dir += '/word';
            } else if (mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
                root_dir += '/word';
            } else if (mimeType === 'application/vnd.ms-excel') {
                root_dir += '/excel';
            } else if (mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
                root_dir += '/excel';
            } else if (mimeType === 'application/vnd.ms-powerpoint') {
                root_dir += '/ppt';
            }
            root_dir += `/${time}`;
            mkdirSync(root_dir);
            callback(null, root_dir);
        },
        filename: (req, file, callback) => {
            const originalname = Buffer.from(file.originalname, 'latin1').toString('utf-8');
            const ext = path.extname(originalname);
            const fileName = formatFileNameToUUID(ext);
            callback(null, fileName);
        },
    });
    return storate;
};
/**
 * 存储文件到指定目录
 * @param dir
 * @returns
 */
export const diskStorageFile = (dir: string) => {
    const storate = diskStorage({
        destination: (req, file, callback) => {
            const params = req.params;
            const fileType = params.fileType || 'temp';
            const root_dir = `${STORAGE_FILE_DIR}/${dir}/${fileType}`;
            mkdirSync(root_dir);
            callback(null, root_dir);
        },
        filename: (req, file, callback) => {
            const originalname = Buffer.from(file.originalname, 'latin1').toString('utf-8');
            const ext = path.extname(originalname);
            const fileName = formatFileNameToUUID(ext);
            callback(null, fileName);
        },
    });
    return storate;
};
/**
 * 移动文件到指定目录
 * @param filePath
 * @param dir
 * @returns
 */
export const cutFileToDisk = (filePath: string, dir: string, fileName?: string) => {
    const ext = path.extname(filePath);
    if (!fileName) {
        fileName = formatFileNameToUUID(ext);
    }
    const root_dir = `${STORAGE_FILE_DIR}/${dir}`;
    mkdirSync(root_dir);
    const fullPath = path.resolve(process.cwd(), filePath);
    if (fs.existsSync(fullPath)) {
        fs.copyFileSync(fullPath, path.resolve(root_dir, fileName));
        fs.unlinkSync(fullPath);
    }
    return `${dir}/${fileName}`;
};
/**
 * 删除文件
 * @param filePath
 */
export const unlinkFile = (filePath: string) => {
    const fullPath = path.resolve(process.cwd(), STORAGE_FILE_DIR, filePath);
    if (fs.existsSync(fullPath)) {
        fs.unlinkSync(fullPath);
    }
};

五、打包构建及部署

使用docker部署到生产环境

# 打包单文件部署
$ pnpm run build:prod

执行rollup.build.prod.mjs脚步文件,自动打包复制静态资源文件和生成Dockerfile和docker-compsoe.yml文件

# 构建
$ docker-compose build
# 启动
$ docker-compose up -d