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)的图

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

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

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

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

对请求的所有输入都要进行验证,使用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。

浙公网安备 33010602011771号