Node.js API开发(Express)

  现代WEB开发基本都是单页应用,页面渲染在前端完成,服务端只提供数据。在单页应用基础上,服务端渲染会在页面首次请求时,把渲染好的页面返回,此时,服务端又分为真正的后端(提供数据)和前端的后端(处理静态站点生成和服务器端渲染)

  真正的服务端采用Route-Service-Data分层架构进行开发。Route层定义路由,接受请求,调用Service。Service层提供CRUD(create-read-update-delete)等服务,调用Data层。Data层操作数据库进行查询或存储,也称Model层。有人会把Route的处理委托给Controllers,Controllers调用Service。mkdir express-tut && cd express-tut && npm init -y新建项目,mkdir -p src/controllers src/routes src/services src/models && touch src/index.js 创建基本目录结构。npm install express。路由定义端点来响应客户端请求,基本语法为 app.METHOD(PATH, HANDLER),其中 METHOD 是小写的 HTTP 方法(如 get、post),PATH是服务器路径,HANDLER 是匹配时执行的函数。index.js

import express from 'express';
const app = express();

app.get('/', (req, res) => res.send('这是首页'))
app.post('/submit', (req, res) => res.send('收到 POST 请求'));

app.listen(8080, () => console.log('服务运行在8080端口'));

  当应用变大时,建议使用 express.Router 来创建模块化、可挂载的路由处理程序。app.use('/模块名', 路由实例),给路由实例添加get、post等方法,就能处理以模块名开头的http请求。

import users from './routes/users.route.js' // esm语法,import from 的文件必须带后缀.js
app.use('/users', users)  // 只处理/users开头的路由

  在routes目录下,新建 users.route.js

import express from 'express';
import { getAllUsers, getUserById, createUser, updateUser, deleteUser } from '../controllers/users.controller.js';

const router = express.Router(); // 创建路由实例

// 路由的路径会进行拼接,以/users开头,再拼接上router.get/post第一个参数路径。
router.get('/', getAllUsers); // 接受/users get请求,获取所有用户
router.get('/:userId', getUserById); // 接受/users/:userId get请求,获取指定用户
router.post('/', createUser) // 接受/users post请求, 创建新用户
router.put('/:userId', updateUser); // 接受/users/:userId put请求, 更新整个用户
router.delete('/:userId', deleteUser); // 接受/users/:userId delete请求, 删除用户

export default router; // 导出路由实例

  这就是RESTful API,将客户端能够访问的东西称为资源,比如一个用户,一篇博客都是资源。对于每一个资源,都可以增删改查,创建不同的路由。但当嵌套数据时,RESTful API变的有点复杂

GET  /uses/123/habits  // 获取用户123的所有习惯
POST /users/123/habits // 创建用户123的习惯
DELETE  users/123/habits/456 // 删除用户123的习惯456

  最好不要这么做,此时GraphQL出现了,直接发送一个query json给服务端,所以如果数据没有疯狂的关联(关系),如果不是高度嵌套的数据,使用RESTful API就可以。controllers目录下新建users.controller.js

import { getAll, getById, create, update, deleteById } from '../services/users.service.js';
export const getAllUsers = async (req, res) => {
  const users = await getAll();
  res.status(200).json(users);
}
export const getUserById = async (req, res) => {
  const userId = parseInt(req.params.userId);
  const user = await getById(userId);
  if (!user) return res.status(404).json({ message: '未找到用户' });
  res.status(200).json(user);
};
export const createUser = async (req, res) => {
  const { username, password } = req.body;
  const user = await create({ username, password });
  res.status(201).json({ success: true, data: user });
};
export const updateUser = async (req, res) => {
  const userId = parseInt(req.params.userId);
  const user = await update(userId, req.body);
  if (!user) return res.status(404).json({ message: '未找到用户' });
  res.status(200).json(user);
};
export const deleteUser = async (req, res) => {
  const userId = parseInt(req.params.userId);
  const user = await deleteById(userId);
  if (!user) return res.status(404).json({ message: '未找到用户' });
  res.status(200).json({ message: '用户成功删除' })
};

  service要调用数据库。关系型数据就是数据相互依赖,特别是围绕所有权,比如用户拥有数据片段。当想到关系时,就要想需要哪些数据,哪些数据拥有其他数据。为了得出这个结论,开发应用程序某个功能或不同部分(domain),先确定需要哪些数据,数据怎么相互关联?比如开发用户习惯追踪API,从所有权模型来看,一个用户拥有多个习惯,一对多的关系。关系型数据的数据,就是定义schema。数据库设计的基本理念是,最终会画出数据形状(schema)的图

image

   然后把图转换成命令来创建表。

image

   关系型数据库的核心是关系,建立关系,避免冗余。冗余的问题是,修改的时候,修改多处,容易犯错。这和程序中,共用的代码放到一个地方,修改一处,其他都改了。如果是复制粘贴,那可能某一处就漏改了。公用的代码,就是一个张表。

image

 

操作数据库通常使用ORM,假设使用drizze。npm i drizzle-orm pg,npm i -D drizzle-kit

image

  models目录新建schema.js

import { pgTable, varchar, text, timestamp, integer } from 'drizzle-orm/pg-core'

// Users table
export const users = pgTable('users', {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    username: varchar({ length: 50 }).notNull().unique(),
    password: varchar({ length: 255 }).notNull(),
    createdAt: timestamp('created_at').defaultNow().notNull(),
    updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

// Habits table
export const habits = pgTable('habits', {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    userId: integer('user_id')
        .references(() => users.id, { onDelete: 'cascade' }) // 这里是定义表结构的关联关系,数据库层面
        .notNull(),
    name: varchar({ length: 100 }).notNull(),
    description: text('description'),
    createdAt: timestamp('created_at').defaultNow().notNull(),
    updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

  依据schema,创建数据库的表。Drizzle提供了migration。数据库的migration就是数据库自己的版本控制。为什么进行版本控制?数据库本质是合同,保证所有的数据都是这个样子,客户端就会根据这个样子开发应用。如果改变了,客户端的代码也要进行改变,客户端怎么知道数据库变了,使用migration。它们的工作方式是迁移数据库的旧状态到建议的状态,无论建议的状态是什么或发生什么改变,不仅要迁移schema(合同结束了),还要迁移已经有的数据来符合现在的合同,这是schema migration,也是data migration。migration需要数据库的地址,由于地址是敏感信息,使用环境变量。npm i @dotenvx/dotenvx,项目根目录.env

DATABASE_URL=postgres://postgres:123456@localhost:5432/postgres

  根目录drizzle.config.js

import '@dotenvx/dotenvx/config'
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle',
  schema: './src/models/schema.js',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL,
  },
});

  然后

npx drizzle-kit generate
npx drizzle-kit migrate

  连接数据库 index.js

import '@dotenvx/dotenvx/config'
const PORT = process.env.PORT || 8080; // .env文件中添加PORT环境变量

app.listen(PORT, () => console.log(`服务运行在${PORT}端口`));

  models目录新建connection.js

import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import { remember } from '@epic-web/remember'
import * as schema from './schema.js'

const createPool = () => {
  return new Pool({ connectionString: process.env.DATABASE_URL })
}
const isProd = () => process.env.ENV === 'production'

const client = isProd ? createPool() : remember('dbPool', () => createPool())
export const db = drizzle({ client, schema })

  当使用数据库连接池时,开发环境由于watch模式,文件变化就会重启服务器,导致连接池一直重建,以前的连接池没有释放,内存泄漏,所以使用了remember(npm install @epic-web/remember)。需要判断环境,.env文件中再添加ENV=development。services目录users.service.js调用db

import { eq } from 'drizzle-orm';
import bcrypt from 'bcrypt'

import { db } from '../models/connection.js';
import { users } from '../models/schema.js'
export async function getAll() {
    const allUsers = await db.select({ id: users.id, username: users.username }).from(users);
    return allUsers;
};
export async function getById(userId) {
    const [user] = await db.select({ id: users.id, username: users.username }).from(users)
        .where(eq(users.id, userId))
    return user;
};
export async function create({ username, password }) {
    const hashedPassword = await bcrypt.hash(password, 10)
    const [newUser] = await db.insert(users)
        .values({ username, password: hashedPassword })
        .returning({ id: users.id, username: users.username });
    return newUser;
};
export async function update(userId, { username }) {
    const [updatedUser] = await db.update(users).set({ username, updatedAt: new Date() })
        .where(eq(users.id, userId))
        .returning({ id: users.id, username: users.username })
    return updatedUser;
}
export async function deleteById(userId) {
    const [deletedUser] = await db.delete(users).where(eq(users.id, userId))
        .returning({ id: users.id, username: users.username })
    return deletedUser;
}

  密码需要加密,使用了bcrypt( npm i bcrypt)。使用ESM模块开发,package.json中"type": "module"

  "type": "module",
  "scripts": {
    "dev": "dotenvx run -- node --watch ./src/index.js"
  },

   npm run dev启动服务器,进行测试,但服务器怎么接受传递过来的JSON数据呢?使用中间件, index.js

app.use(express.json());

  中间件就是两段代码之间的代码。服务器从接受请求到请求处理函数,从返回响应到客户端接受到响应中间执行的代码都是中间件。可以看到app.use没有提供path参数,path的意思是只要匹配到path这条路径,才执行它的回调函数。没有path,就表明所有请求都会走它的回调函数,也称全局中间件。全局中间件通常放到前面,后面的内容都会受益于该中间件。中间件只是普通的函数,有三个参数(req, res, next)。express.json()返回(req, res, next) => {}。在函数体中,调用next()就到下一个中间件,如果没有中间件,就到路由处理函数。如果不调用next,请求就结束了,此时不返回响应,请求就hanging了。中间件接受请求对象和响应对象,可以检查请求,修改和返回响应对象,提前返回。

import express from 'express';
const app = express()
// 中间件
const mv1 = (req, res, next) => {
    console.log('mv1 start.....')
    next()
    console.log('mv1 end....')
}

const mv2 = (req, res, next) => {
    console.log('mv2 start.....')
    next()
    console.log('mv2 end....')
}

app.use(mv1)
app.use(mv2)

app.get('/', (req, res) => {
    console.log('request start...')
    res.send('ok')
    console.log('request.end')
})

app.listen(3000)

// 当请求进来时,express从上到下(按书写顺序),依次调用中间件,
// 当返回数据时(res.send),还是会调用中间件(执行next()后面的语句),只不过是顺序相反
// mv1 start.....
// mv2 start.....
// request start...
// request.end
// mv2 end....
// mv1 end....

  打开postman

image

   对请求的所有输入都要进行验证,使用zod schema进行验证, npm i zod。路由实例的post等方法的第二个参数是验证中间件,mkdir src/middleware && touch src/middleware/validate.js

export const validate = (schema, key) => (req, res, next) => {
    // key是'body'或'query'或'params',body就是请求体,query就是请求参数
    const { success, error } = schema.safeParse(req[key])
    if (!success) return res.status(400).json({ message: '验证失败' })
    next()
}

  users.route.js

import { z } from 'zod';
import { validate } from '../middleware/validate.js';  const createUserSchema
= z.object({ username: z.string().min(3, '用户名必须至少包含3个字符').max(50, '用户名不能太长'), password: z.string().min(8, '密码必须至少包含 8 个字符'), }) router.post('/', validate(createUserSchema, 'body'), createUser)

  如果发生错误,也要正确处理。路由处理函数(get,post等方法的回调函数)返回Promise,如果Promise被拒绝或抛出错误,会自动调用 `next(error)`。next只接受error参数,如果被调用,就会找错误处理函数。Express内置了一个错误处理函数。错误处理函数接收四个参数(err, req, res, next)。如果自定义,一定要放到所有中间件的最后(app.listen前面)

app.use((error, request, response, next) => {
    if (response.headersSent) return next(error);  // 将处理权移交给默认的 Express 错误处理程序,避免发送两次
    console.error(err.stack);
    return response.status(500).send('Something broke!');
});

  认证和授权

  先了解三个概念:Identification,Authentication,Authorization。Identification声明你是谁(身份),比如输入用户名。Authentication(认证)验证所声明身份的过程,比如服务端验证密码对不对,携带的token正不正确,确认用户的身份。Authorization(授权)身份验证成功后,确定用户可以访问或执行哪些特定资源。一个实际的例子就是机场安检, Identification:您出示身份证件来表明身份。 身份认证:安检人员核对身份证件上的信息与您的面部特征是否匹配,以确认您的身份。 授权:您在登机口出示登机牌,获得登机许可。

  对于web开发,认证首先是登录,其次是验证cookie或jwt token。因为HTTP是无状态的,服务器不会保存前一次请求的任何信息,它只会根据当前接受到的请求信息作出响应,因此登录成功后,以后的每次请求还要带着用户信息。登录的前提是注册,先创建用户(createUser,先买飞机票)。对于服务与服务之间的认证,使用API-key,比如使用百度地图前,要注册API key。

  WEB认证(登录)成功后,携带用户信息的方式有两种,一种是使用cookie,登录成功,服务器返回cookie,浏览器存储它,在随后的请求中,浏览器都会把cookie通过Cookie字段放到请求头中。cookie不与特定页面相关联,而是与页面的域相关联,因此无论用户从站点哪个页面发出请求,请求头中都会带有cookie。

  cookie通常来自session。首次访问页面,服务器生成一个session对象,然后把session对象中的id,通过setCookie的方式,发送给客户端。认证成功后,身份信息存储到session对象(修改session)。客户端请求带着cookie(session id),服务端就知道它是属于那个session的,从session对象中获取到客户端信息。npm install express-session,index.js

import express from 'express';
import session from "express-session"

const app = express();
app.use(express.json());

app.use(session(
    {
        secret: "47f17f0325ef0f15ef889020bcf5de8e", // 用于session ID签名,防止session id 随意更改。
        resave: false, // 如果session没有被修改过,不用保存
        saveUninitialized: false, // 防止向尚未登录的匿名用户分配 Cookie
        cookie: {
            maxAge: 1000 * 60 * 60,
            httpOnly: true, // 防御 XSS 攻击
            secure: true, // 如果在生产环境中使用 HTTPS,请将其设置为 true。
            sameSite: 'none',
        }
    }
));
// 2. 认证中间件
const requireLogin = (req, res, next) => {
    if (req.session && req.session.userId) return next();
    return res.status(401).send('拒绝访问,请先登录');
};

app.post('/login', (req, res) => { // 登录
    const { username, password } = req.body;
    if (username === 'admin' && password === 'password123') {
        // req.session由express-session中间件动态注入到每个请求中的对象。修改该对象会指示Express将相应状态存储在服务器内存中
        req.session.userId = 1;
        req.session.username = username;
        return res.send(`Successfully logged in as ${req.session.username}`);
    }
    return res.status(400).send('无效用户名和密码');
});

app.get('/dashboard', requireLogin, (req, res) => { // 受保护的路由(需要身份验证防护)
    res.send(`欢迎来到您的私人仪表板, ${req.session.username}!`);
});

app.post('/logout', (req, res) => { // 登出(销毁会话数据)
    req.session.destroy((err) => {
        if (err) return res.status(500).send('Could not log out. Try again.');

        res.clearCookie('connect.sid'); // 清除默认的 express-session Cookie 名称
        return res.send('成功登出');
    });
});

app.listen(3000);

  一种是JWT token。认证成功,返回token,以后的请求,就把token放到请求头中(Authorization: Bearer 返回的token),服务器解析token中,获取到用户信息。npm i jose,index.js中,

import authRoutes from './routes/auth.route.js'
app.use('/auth', authRoutes)

  routes/auth.route.js

import { createSecretKey } from 'node:crypto';
import bcrypt from 'bcrypt';
import express from 'express';
import { SignJWT } from 'jose'; // 生成jwt
import { z } from 'zod';
import { eq } from 'drizzle-orm';

import { db } from "../models/connection.js";
import { users } from "../models/schema.js";

import { validate } from '../middleware/validate.js';
const generateToken = (payload) => {
    const secretKey = createSecretKey(process.env.JWT_SECRET, 'utf8'); // ENV文件中JWT_SECRET
    return new SignJWT(payload)
        .setProtectedHeader({ alg: 'HS256' })
        .setIssuedAt()
        .setExpirationTime('2h')
        .sign(secretKey);
};

const router = express.Router();
const loginSchema = z.object({ username: z.string(), password: z.string() })

router.post('/login', validate(loginSchema, 'body'), async (req, res) => {
    const { username, password } = req.body;
    const user = await db.query.users.findFirst({
        where: eq(users.username, username),
    })

    if (!user) return res.status(401).json({ error: '用户名或密码错误' });

    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) return res.status(401).json({ error: '用户名或密码错误' });

    const token = await generateToken({ id: user.id, username: user.username });
    return res.json({ token });
})

export default router;

  写一个认证token的中间件,middleware/auth.js

import { createSecretKey } from 'node:crypto';
import { jwtVerify } from 'jose'
export const verifyToken = async (token) => {
  const secretKey = createSecretKey(process.env.JWT_SECRET, 'utf-8')
  const { payload } = await jwtVerify(token, secretKey)

  return { id: payload.id,username: payload.username }
}
export const authenticateToken = async (req, res, next) => {
    try {
        const authHeader = req.headers['authorization']
        const token = authHeader && authHeader.split(' ')[1] // Bearer TOKEN

        if (!token) return res.status(401).json({ error: 'Access token required' })

        const payload = await verifyToken(token)
        req.user = payload
        next()
    } catch (err) {
        return res.status(403).json({ error: 'Invalid or expired token' })
    }
}

  保护路由,比如get

import { authenticateToken } from '../middleware/auth.js';
router.get('/', authenticateToken, getAllUsers);

  现在有双token,assessToken 和refresh Token,assessToken 用于请求数据,refresh token 用于请求认证中心,刷新assessToken。登录成功返回 acesstoken和refresh token,存储到客户端。使用accessToken 去请求api,当api 返回 token 失效时,再用refresh Token 去请求refreshToken api,它会同时返回acess token,再用access Token 去请求以前失败api

  授权的基础是授权策略,通常使用角色授权,给角色分配操作权限。程序只检查用户是否已被分配到有权执行特定操作的角色。比如 Users role 包括Alice, Bob, Admins 只有Alice,Users role 可以执行Add 操作,但Delete 操作只能Admins role 执行。

  添加日志记录,使用Morgan记录HTTP请求,使用Winston记录结构化应用日志,比如session ID等应用信息。npm i winston morgan, morgan使用比较简单,直接使用默认配置

import morgan from 'morgan';
app.use(morgan('combined')); // 或 app.use(morgan('dev'));

  或自定义格式

// 添加一个request ID token
morgan.token('request-id', (req) => req.headers['x-request-id'] || '-');
// 创建自定义格式来使用添加的 request ID token
const customFormat = ':request-id :method :url :status :response-time ms';
app.use(morgan(customFormat));

  winston 要创建一个logger对象,mkdir src/logs && touch src/logs/index.js

import winston from 'winston';

const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info', // 设置最低日志级别
    format: winston.format.combine( // formate日志的格式,默认是JSON格式
        winston.format.timestamp(),  // 格式加一个时间
        winston.format.json(),
        winston.format.printf(
            ({ timestamp, level, message }) =>
                `[${timestamp}] ${level.toUpperCase()}: ${message}`
        )
    ),
    transports: [ // transports: 日志存储的地方,默认控制台
        new winston.transports.Console(), // log to console
        new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), // 单独的error.log文件,其中仅包含错误消息
        new winston.transports.File({ filename: 'logs/combined.log' }) // 输出到combined.log文件
    ]
});

export default logger;

  日志最低级别,只有比它级别高的日志才会被输出。winston默认六个日志级别,优先级从高到低依次是error,warn,info,verbose, debug, silly。每一个级别都对应到logger对象上的一个方法,logger.error('error message'), error message 这个日志的级别就error,假设level设置成info, 只有比info级别高的日志才会被输出,比如 loggeer.warn('warn')就会被输出,logger.verbose的内容就不会被输出。级别和优先级决定哪些日志信息会被输出到transport。

  如果日志一直写入到同一个文件,文件就太大了。日志rotate来限制日志文件的大小,并根据预定义的条件创建新的日志文件。例如,每天创建一个新的日志文件,并自动删除超过一定时间(比如30天)的旧日志文件。npm install winston-daily-rotate-file

import 'winston-daily-rotate-file';

// 它是一个传输模块,可以将日志写入一个轮换文件,轮换条件可以根据日期或文件大小进行配置,
// 同时可以根据日志数量或经过的天数自动删除旧日志。
const fileRotateTransport = new winston.transports.DailyRotateFile({
    // level: 'error',
    filename: 'combined-%DATE%.log',
    datePattern: 'YYYY-MM-DD',
    maxFiles: '14d',
});

const logger = winston.createLogger({
   // ....
    transports: [
        new winston.transports.Console(), // log to console
        new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), // 单独的error.log文件,其中仅包含错误消息
        fileRotateTransport
    ]
});

  使用 Morgan记录HTTP请求,也可以将这些日志通过管道传输给 Winston,以实现统一的格式化和文件存储。index.js

import logger from './logs/index.js'
const morganStream = { // 移除 Morgan 末尾的换行符
    write: (message) => logger.info(message.trim())
};
app.use(morgan('combined', { stream: morganStream }));

  Debug(VS Code编辑器)

  VS Code 内置了Node.js的调试器(debugger)。打开项目的入口文件(index.js),单击一下,使其处于活动(active)状态,然后点击左侧边栏debug按钮,如果是第一次debug项目,显示如下

   点击Run and Debug按钮,VS Code 会根据你的活动文件自动选择debug环境,如果无法选择,就会弹窗, 选择Node.js,整个编辑器进入到debug状态。编辑器的底部,Debug Console 显示,成功运行index.js 文件,服务运行在8080端口。编辑器的上方,出现debug工具栏,最后一个方框按钮是停止debug。如果启动程序报错,编辑器没有进入到debug状态,报错信息也会显示在Debug Console 中。debug就是以debug的模式运行程序,怎么都要运行程序,所以debug之前,还要先把原服务器停掉,否则,会出现端口冲突。VS Code 帮我们创建了一个.vscode/launch.json文件,

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}/src/server.js"
        }
    ]
}

  再点debug按钮

  点击绿色三角,就可以启动它右侧下拉列表框中选中的debug程序。右侧下拉列表框列出了项目中所有配置的debug程序,列表项就是launch.json中配置中的name,如果项目中有多个debug,可以选择启动哪一个debug。再右侧的齿轮是设置debug信息,点击它可以打开launch.json文件,由于项目中只配置了一条debug,默认处于选中状态,点击左侧的三角形或按F5 启动debug。如果项目有多个配置信息,要先在下拉列表框中选择想要进行的debug,然后再点击start 或按F5。看回launch.json

  type: 就是创建lauch.json时选择的环境,告诉VS Code 启动什么样的程序,在这里是node, 表示运行node程序。

  request: 表示怎么启动调试器,它有两个选项,launch 和 attached.  launch 表示在编辑中启动程序,编辑器自动把调试器附在或连接到启动程序上。attach 表示,你已经在别的地方启动了要调试的程序,只是想利用VS Code的调试器,这时就要把调试器边接到已经启动的程序上(attach)。

  name 就是给这个debug信息命个名,好以后进行选择,因为在一个项目中,可能配置了好多debug信息。launch.json 左下角有一个Add Configuration... 按钮, 点击它,可以再添加一个配置信息,比如,mochai 单元测试。这时点击上图中的Lauch Program下拉列表框,可以看到多了一个选项

  skipFiles: 在进行debug的过程中,要跳过或忽略的文件。program是启动debug的时候,要执行的程序文件。

  总的来说,就是在编辑器中启动一个node的程序,运行项目根目录下的index.js文件,启动后把debugger连接到程序上,进入debug模式,debug的过程中要忽略掉node的内部文件,给这个配置命个名,叫Lauch Program。启动debug后,在编辑区的行号左边,点击一下,或定位到一行,按F9,就会添加断点,进行调试。在某行左边,右击一下,则会显示添加断点,添加logpoint. logpoint 不会阻止程序的运行,会把信息输出到控制台上。在添加logpoint 的时, 变量要用{} 包起来,和React中的{} 一样。

   这样debug有一点问题,就是当debug出问题,修改一下代码,就要重新启动debug。这时可以用nodemon --inspect src/index.js, 启动服务器,然后把debugger attach到nodemon运行的node程序上。nodemon --inspect src/index.js,控制台上 Debugger listening on ws://127.0.0.1:9229/6122e2b5-a165-434b-ac45-51657da66545,程序启动在了9229 端口上。在launch.json中点击Add configuration 按钮,选择Node.js attach添加配置,再补充两个配置 address和restart

{
            "name": "Attach",
            "port": 9229,
            "request": "attach",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "type": "node",
            "address": "localhost",
            "restart": true
        },  

  restart 决定node.js debugger是否自动启动当debug session 结束后,true表示自启动,re-attach到node程序上。启动debugger的时候,选择Attach,就可以了。在VS code中打断点,当有请求进入到服务器时,也会进入到断点。底部在debuger console,可以在里面执行简单的运算,计算程序中在中间步骤,比如:greetings.length

  除了上述几个配置项,还有很多其它配置项,也很有用,

  env: 配置环境变量,如果在程序中写了process.env.APIBase, 可以在launch.json中添加 “env”: {"APIBase", "localhost:8080"}

  envFile: .env文件的绝对路径,.env也是定义环境变量的, 如果项目中的环境变量都配在.env文件中,给envFile 配置.env文件的绝对路径:“envFile”: "${workspaceFolder}/.env"

  cwd: 设置当前工作目录,用来寻找其它文件或依赖。

  console: 用来设置使用哪一个控制台,默认是internalConsole,就是看到的debug控制台。但时debug控制台只能显示信息,不能输入信息,如果要debug的程序,需要从控制台中读取内容,就要改变console的选项,可以是VS Code的集成终端(integratedTerminal), 也可以是外部终端(externalTerminal)。到底使用哪一个外部终端,可以在VS Code 的setting中搜索terminal进行设置。

   Node.js 安全 使用helmet 设置http header.   使用he库 阻肯xss攻击,csrf 攻击,阻止csrf,要使用包csurf,就是发送post 请求时,添加csrf token。 

posted @ 2026-07-19 12:50  SamWeb  阅读(13)  评论(0)    收藏  举报