承接上回,我们同事分享了node+koa基础搭建,那么本篇文章就继续由浅入深继续探究!!!本文主要涵盖内容:node接口化标准、fs文件模块示例、日志查询中间件、部署。下面就让我们一探究竟吧!
通过上次分享,我们已经得到了一个比较贴近于业务项目的node项目,大家可以先熟悉一下目录结构,对下面的文章阅读是有帮助的哦~
1. node接口化标准
无论是我们生活中还是在工作coding中,都有着一套流程,比如早上起来闹钟响了,我们要刷牙 穿衣服吃饭,最后坐地铁公交上班。
那么对于程序员来讲,每个负责的业务项目也是,比如在我搭建的node项目中,在utils/requestEnc.js文件 创建了Response类,里面有针对于请求的返回体&成功失败方法 在下面代码例子,可以看到我在返回体里封装了四部分,分别是code:代表请求状态;message:返回信息;data:业务数据,lid:唯一标识。还额外封装了几个静态方法,可以在请求后拿到结果通过code判断再次封装。
const uuid = (pid = '40.60:7001') => {
// 查询日志区分
return String(pid) + String(new Date().getTime());
};
class Response {
// 统一返回结构 data里面有什么
constructor(code,message,data,lid) {
this.code = code;
this.message = message;
this.data = data; //业务数据
this.lid = lid; // 唯一标识
}
// 请求的状态
// 200 成功
static success(data,message='请求成功啦啦-->>') {
return new Response(0,message,data,uuid);
}
//失败
static error(code,message,data=null){
return new Response(code,message,data,uuid);
}
// 无权访问
static permission(code=403,message='您无权访问哦-->>',data=null){
return new Response(code,message,data,uuid);
}
// 用户未登录
static notLoginIn(code=401,message='您未登录哦-->>',data=null){
return new Response(code,message,data,uuid);
}
}
exports.Response = Response;
2. fs文件模块示例
fs文件模块是Node.Js内置的文件系统模块,允许我们读取、写入、删除和操作文件以及目录,能够以标准的POSIX函数为模型的方式与文件系统进行交互。
接下来 ,开始进入到我们的实操喽,本着在业务开发中,我们平时操作文件也比较多,所以就以fs模块为例子,演示增删查操作~~,下图就是我们的fs页面,涉及的操作有:获取文章列表、上传文章、删除文章。
const getArticleList = async () => {
// 拿到当前文件存储的目录
try {
// 绝对路径
const directoryPath = path.resolve(__dirname, "../articles");
if (directoryPath) {
// sync同步事件 阻塞
const files = fs.readdirSync(directoryPath);
// 拿到所有的文件路径 join拼接
const filesPath = files.map((file) => path.join(directoryPath, file));
const fileInfo = filesPath.map((filePath) => ({
name: path.basename(filePath),
type: fs.statSync(filePath).isDirectory() ? "directory" : "file",
size: fs.statSync(filePath).size,
birthtime: fs.statSync(filePath).birthtime,
}));
return { data: fileInfo, code: 0 };
}
} catch (err) {
console.log(err, "read article error--->>>");
return { code: 500, data: "读取文件发生异常啦---->>>>" };
}
};
2. 上传功能:fse.copy传入虚拟路径和真实存放路径
const fse = require("fs-extra");
const writeArticleDetail = async (file, filepath) => {
try {
// 虚拟路径-》实际路径
await fse.copy(file.filepath, filepath);
return { data: "已经成功写入文件啦---->>>" };
} catch (err) {
console.log(err, "write article error--->>>");
return { code: 500, data: "啊哦,写入文件失败--->>>" };
}
};
4. 删除功能:通过传入fileName,进行fs.remove
const deleteArticleDetail = async (fsPath) => {
console.log(fsPath, "fsPath-->>>>");
try {
// 注意2:不返回任何值 undefined
if (!(await fse.exists(fsPath))) {
return { code: 500, data: "文件不存在-->>" };
}
await fse.remove(fsPath);
return { code: 0, data: "成功删除文件--->>" };
} catch (err) {
console.log(err, "delete article error--->>>");
return { code: 500, data: "删除失败啦--->>>" };
}
};
📝 例子中使用到的fs的api&坑点记录
3. 日志查询中间件
常用的日志查询中间件如下,该项目选择使用koa-log4为例;
koa-log4是可以自定义查询日志等级,自己定义日志输出格式,我们可以思考一下查询日志都需要哪些信息,可以自己根据想要的封装,我认为其中必不可少的应该要包括:请求方式、响应状态、请求url、域名、请求时间等等;
我在config/log-config中重新定义日志查询格式,在middleware中引入。
/config/log-config
/**
* 日志生成及格式化
*/
const log4j = require("koa-log4");
const msFn = () => {
const date = new Date();
const milliseconds = date.getMilliseconds().toString().padEnd(3, "0");
return `${milliseconds}`;
};
//服务错误日志
const formatError = (ctx, err, costTime) => {
const {
url,
response: { status },
request: {
header: { host },
method,
},
} = ctx;
const msg = ctx.body || {};
console.log(msg, "msgggggg");
const Header = JSON.stringify(ctx.request.header);
const SourceIp =
ctx.request.ip ||
ctx.request.headers["x-forwarded-for"] ||
ctx.request.connection;
return `.${msFn()}|${url}|${method}|status=${status}|host=${String(
host
)}|SourceIp=${SourceIp}|${
msg.lid || "60.40"
}}|${costTime}|${Header}|body=${msg}|error=${err}`;
};
//服务日常日志
const formatRes = (ctx, costTime) => {
// console.log(ctx.body.lid);
const {
url,
response: { status },
request: {
header: { host },
method,
},
} = ctx;
const msg = ctx.body;
const Header = JSON.stringify(ctx.request.header);
const SourceIp =
ctx.request.ip ||
ctx.request.headers["x-forwarded-for"] ||
ctx.request.connection;
return `.${msFn()}|${url}|${method}|status=${status}|host=${String(
host
)}|SourceIp=${SourceIp}|${
ctx.body.lid
}|${costTime}|${Header}|body=${JSON.stringify(msg || {})}`;
};
//服务code=500日志
const formatFatal = (ctx, costTime) => {
const {
url,
request: {
header: { host },
method,
},
} = ctx;
const msg = ctx.body;
const Header = JSON.stringify(ctx.request.header);
const SourceIp =
ctx.request.ip ||
ctx.request.headers["x-forwarded-for"] ||
ctx.request.connection;
return `.${msFn()}|${url}|${method}|status=${ctx.body.code}|host=${String(
host
)}|SourceIp=${SourceIp}|${
ctx.body.lid
}}|${costTime}|${Header}|body=${JSON.stringify(msg || {})}`;
};
const levels = {
trace: log4j.levels.TRACE,
debug: log4j.levels.DEBUG,
info: log4j.levels.INFO,
warn: log4j.levels.WARN,
//程度中等、可以被捕获到的 比如数据库查询失败、用户输入验证失败等等,不会阻止程序的运行 404
error: log4j.levels.ERROR,
//程度叫高、程序无法运行的错误,比如内存溢出等等,会阻止
fatal: log4j.levels.FATAL,
};
// log4j配置
log4j.configure({
// 指定输出文件类型和文件名
appenders: {
//常规输出
console: { type: "console" },
//日常日志
info: {
type: "dateFile", // 输出的类型
pattern: "yyyy-MM-dd.log", // 格式
filename: "logs/access.log", // 地址
layout: { type: "pattern", pattern: "fe-web|%d{yyyy-MM-dd hh:mm:ss}%m" },
},
//错误日志
error: {
type: "dateFile",
filename: "logs/server_error.log",
pattern: "yyyy-MM-dd.log",
layout: { type: "pattern", pattern: "fe-web|%d{yyyy-MM-dd hh:mm:ss}%m" },
},
//500日志
fatal: {
type: "dateFile",
filename: "logs/server_fatal.log",
pattern: "yyyy-MM-dd.log",
layout: { type: "pattern", pattern: "fe-web|%d{yyyy-MM-dd hh:mm:ss}%m" },
},
},
// 设置日志输出级别
categories: {
default: {
appenders: ["console"],
level: "debug",
},
info: {
appenders: ["info"],
level: "info",
},
error: {
appenders: ["error"],
level: "error",
},
fatal: {
appenders: ["fatal"],
level: "fatal",
},
},
});
// 抛出日志记录方法
/**
* 日志输出 level为debug
* @param { string } content
*/
const debug = (content) => {
let logger = log4j.getLogger("debug");
logger.level = levels.debug;
logger.debug(formatRes(content));
};
/**
* 日志输出 level为info
* @param { string } ctx
*/
const info = (ctx, costTime) => {
let logger = log4j.getLogger("info");
logger.level = levels.info;
logger.info(formatRes(ctx, costTime));
};
/**
* 日志输出 level为error
* @param { string } ctx
*/
const error = (ctx, err, costTime) => {
let logger = log4j.getLogger("error");
logger.level = levels.error;
logger.error(formatError(ctx, err, costTime));
};
/**
* 日志输出 level为fatal
* @param { string } ctx
*/
const fatal = (ctx, costTime) => {
let logger = log4j.getLogger("fatal");
logger.level = levels.fatal;
logger.fatal(formatFatal(ctx, costTime));
};
module.exports = {
debug,
info,
error,
fatal,
};
最后引入使用 切记:在app.use挂载的时候 日志中间件一定一定是最先挂载!!!! 洋葱模型,先进后出,我们要获取所有的接口日志
4. 部署
使用esbuild打包
- 确保安装node; node -v检查;
- 安装esbuild npm install esbuild;
- 配置esbuild文件;
- package.json 添加命令"build": "node build.js";
- 执行命令 npm run build;
- 查看输出文件
部署到服务器
首先申请机器,检查node版本是否匹配,如果我们本地使用node版本为14,机器使用的是16,我们不可能更改机器上线node版本,更改本地如果代价太大也不划算,这里我们可以使用docker进行环境隔离。node端口只能进行一次操作,我们可以安装pm2,进行进程管理。
Docker是一组平台即服务(PaaS)的产品。它基于操作系统层级的虚拟化技术,将软件与其依赖项打包为容器。托管容器的软件称为Docker引擎。Docker能够帮助开发者在轻量级容器中自动部署应用程序,并使得不同容器中的应用程序彼此隔离,高效工作。该服务有免费和高级版本。
#dockerFile文件编写
# 使用官方的 Node.js 作为基础镜像
FROM node:16-alpine
# 设置工作目录为 /app
WORKDIR /app
# 设置 npm 源为淘宝源
RUN npm config set registry https://registry.npmmirror.com
# 全局安装 pm2
RUN npm install -g pm2
# 将当前目录下的 dist、logs、public、articles 文件夹及内容复制到 /app 下
COPY dist /app/dist
COPY logs /app/logs
COPY public /app/public
COPY articles /app/articles
# 暴露端口(如果需要)
EXPOSE 7001
# 设置容器启动时执行的命令(使用 pm2 启动应用)
CMD ["pm2-runtime", "start", "dist/app.json"]
# docker build -t my-app .
# docker run -d \
# -p 17001:7001 \
# -v /home/logs/node-share:/app/logs \
# my-app
最后就可以在我们申请的机器上面,看到我们的请求和返回数据啦!!