前言
时至2021年,nodejs在web开发领域有着举足轻重的地位,常被应用于各种业务中间件开发以及构建工具插件开发,其web框架如express、koa、fastify、egg、midway、nest等也有一定的应用空间。本文将结合常见mvc框架特性,重点阐述nodejs在web mvc框架领域中的应用价值。希望对有兴趣自己封装框架的童鞋有所帮助!
正文
常见mvc框架核心功能如下:1.请求/响应的统一处理机制;2.静态资源映射;3.http请求分发。本文将对以上3个问题点进行阐述,分别讲述解决方案!
1 基于nodejs http模块实现
由于原生node http模块不具备任何特性,因此上述1、2、3点都需要处理!
1.1 请求/响应的统一处理机制
这个问题利用http模块拦截机制实现,拦截原型如下:
function createServer(requestListener?: RequestListener): Server;
function createServer(options: ServerOptions, requestListener?: RequestListener): Server;
type RequestListener = (req: IncomingMessage, res: ServerResponse) => void;
createServer函数接受一个请求监听器,该监听器包含了请求和响应对象,可以定义拦截器对请求响应的统一处理,如下进行了简单跨域处理:
export default function interceptor(req, res) {
console.log(`request ${req.url} is targeted!`.green.bold);
// 跨域处理
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "*");
res.setHeader("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
}
1.2 静态资源映射
在对静态资源映射前,首先需要区分接口请求和资源请求,如接口请求则转发给业务代码进行处理,资源请求则直接读取本地文件返回即可。因此,需要解决以下问题:
(1)区分接口请求和资源请求
简单区分可以采用url前缀进行区分,以/api开头的为接口请求,否则为资源请求。
export default async function(req, res) {
try {
if (req.url.startsWith('/api')) { //接口请求
await dispatchActions(req, res)
} else { //资源请求
...
}
} catch (error) {
console.error(error);
res.statusCode = 500;
res.end('服务器异常');
}
}
资源请求又可细分为后端资源请求和前端资源请求,因此需要添加响应配置项进行处理。
let path;
const urlObj = new URL(req.url, globalThis.$HOST);
if (req.url.startsWith('upload')) { // 后端资源请求
path = join(cwd(), urlObj.pathname);
} else { // 前端资源请求
let pathname = urlObj.pathname === '/' ? '/index.html' : urlObj.pathname;
path = join(cwd(), config.views, pathname);
}
(2)对资源请求设置适当的ContentType
Content-Type对照表可以获取常用文件后缀以及对应的contentType,通过工具类处理响应即可。
async function setContentType(path, res) {
try {
let contentType;
if (path.includes('.')) {
const suffix = '.' + path.split('.')[1];
metaData[suffix] ? (contentType = metaData[suffix]) : (contentType = metaData['.*']);
}
if (contentType === 'text/html' || contentType === 'text/plain') {
contentType += ';charset=utf-8';
}
if (contentType) {
res.setHeader('Content-Type', contentType);
}
} catch (error) {
console.error(error)
}
}
1.3 http请求分发
(1) 编写请求分发函数
async function dispatchActions(req, res) {
const urlObj = new URL(req.url, globalThis.$HOST);
const api = urlObj.pathname.split('/api')[1];
if (getControllerExport()[api]) {
await getControllerExport()[api](req, res);
} else {
res.statusCode = 404;
res.end('not found');
}
}
export function setControllerExport(data) {
globalThis.$CONTROLLER = data;
}
export function getControllerExport() {
return globalThis.$CONTROLLER;
}
(2) 仿照webpack require.context功能实现读取同一文件下的其他文件的默认导出
// controll/index.mjs
async function autoImpotController(dir, exclude) {
let result = {};
const files = await readdir(dir);
if (files?.length) {
for (let i = 0; i < files.length; i++) {
if (exclude !== files[i]) {
const module = await import('./' + files[i]);
result = {...result, ...module.default};
}
}
}
setControllerExport(result);
}
const fileUrl = import.meta.url;
const dir = dirname(fileURLToPath(fileUrl));
const exclude = basename(fileURLToPath(fileUrl));
autoImpotController(dir, exclude);
(3) 编写controller
// controll/user.mjs
const base="/user";
const controllers = {
getUsername : async function (req, res) {
try {
res.setHeader('Content-Type', 'application/json;charset=utf-8');
res.end(getJsonResult(true, 'getUsername'));
} catch (error) {
throw new Error(error);
}
}
}
const ept = {};
Object.keys(controllers).forEach(controller => {
ept[`${base}/${controller}`] = controllers[controller];
})
export default ept;
1.4 基于注解的controller开发
经过查询资料发现,基于typescript的注解开发处于实验阶段,而ecmascript注解提案处于第二阶段,详见github.com/tc39/propos… ,本文暂时不作探究,感兴趣的童鞋可以自行研究下!最终的controller写法应类似:
@controller({baes: 'uesr'})
class UserController {
@Request({path: '/user', method: 'GET', contentType: 'application/json;charset=utf-8'})
async getUsername() {
try {
res.setHeader('Content-Type', 'application/json;charset=utf-8');
res.end(getJsonResult(true, 'getUsername'));
} catch (error) {
throw new Error(error);
}
}
}
2 基于express模块实现
express已经内置对静态资源的处理,详见express static函数,而且对于请求也进行了统一处理,只需要简单封装即可直接使用。
2.1 实现controller自动导入并关联express
//controller/index.mjs
const fileUrl = import.meta.url;
const dir = dirname(fileURLToPath(fileUrl));
const exclude = basename(fileURLToPath(fileUrl));
const controller = await autoImpotController(dir, exclude);
export default controller;
...
//controller/test.mjs
export default {
get: {
async test(req, res) {
res.send('ok');
},
},
post: {},
put: {},
delete: {}
}
...
//util.mjs
const currentDir = dirname(fileURLToPath(import.meta.url));
export async function autoImpotController(dir, exclude) { //dir 为controller目录所在绝对路径, exclude为index.mjs
let result = [];
const files = await readdir(dir);
if (files && files.length) {
for (let i = 0; i < files.length; i++) {
if (exclude !== files[i]) {
const module = await import(join(relative(currentDir, dir) , files[i]).replace(/\\/g, '/'));
for (let method in module.default) {
for (let controller in module.default[method]) {
result.push({
method, //方法名 get post put delete ...
url: `/${files[i].split('.')[0]}/${controller}`, //请求url 为 文件名 + 对应函数名,如/test/test
handler: module.default[method][controller] // 处理函数
})
}
}
}
}
}
return result;
}
...
// 入口文件index.mjs
const app = new Express();
if (controller && controller.length) {
controller.forEach(item => app[item.method](item.url, item.handler))
}
...
2.2 mongoose集成
本文采用了mongodb作为数据库进行开发,为了进行规范化开发,本文单独抽离出schema/model的操作作为dao层,由于作简单演示,service层暂时不做。 (1)引入mongoose库到入口文件
// mongoose.mjs
import mongoose from 'mongoose';
try {
await mongoose.connect('mongodb://su:123@localhost:27017,localhost:27018,localhost:27019/test?replicaSet=rs0');
} catch (error) {
console.error(error);
}
export default mongoose;
(2) 数据库设计,定义schema,并导出model
//dao/user.mjs
import mongoose from '../mongoose.mjs';
const schema = new mongoose.Schema({
name: String,
age: {
type: Number,
default: 20
},
hobby: String,
});
// 定义shema methods or static methods
export default mongoose.model('User', schema);
(3) 直接在controller中使用,实现增删改查
// controller/test.mjs
import User from '../dao/user.mjs';
export default {
get: {
async getUsers(req, res) {
try {
const data = await User.find();
res.status(200).json({data});
} catch (error) {
console.error(error);
res.status(500).json({error: JSON.stringify(error || '服务器异常')});
}
},
},
post: {
async addUsers(req, res) {
try {
const total = await User.count();
const { num = 1, name="peter", hobby= "pingpong" } = req.body;
const list = Array.from({length: num}).map((_, i) => ({
name: `${name}${total + i + 1}`,
hobby: `${hobby}${total + i + 1}`,
age: Math.round(20 + Math.random()*10)
}))
const result = await User.insertMany(list);
res.status(200).json({msg:'创建成功', data: result});
} catch (error) {
console.error(error);
res.status(500).json({error: error && error.message || '服务器异常'});
}
}
},
put: {
async updateUser(req, res) {
try {
const { id, name, hobby, age} = req.body;
const query = { _id: id };
const setter = {};
if (name) {
setter.name = name;
}
if (hobby) {
setter.hobby = hobby;
}
if (age) {
setter.age = age;
}
if (!id || !Object.keys(setter)) {
throw new Error('参数缺失');
}
const result = await User.update(query, setter);
res.status(200).json({msg:'修改成功', data: result});
} catch (error) {
console.error(error);
}
}
},
delete: {
async delUsersByName(req, res) {
try {
const { name } = req.query;
if (!name) {
res.status(200).json({msg:'删除成功', data: 0});
return;
}
const result = await User.deleteMany({name: new RegExp(name)});
res.status(200).json({msg:'删除成功', data: result});
} catch (error) {
console.error(error);
res.status(500).json({error: error && error.message || '服务器异常'});
}
}
}
}
(4) 接口测试,推荐使用vscode REST Client插件,现奉上接口测试文件
# api.rest
# 批量新增
# POST http://localhost:3030/test/addUsers HTTP/1.1
# content-type: application/json
# {
# "name": "liming",
# "hobby": "football",
# "num": 10
# }
# 批量删除
# DELETE http://localhost:3030/test/delUsersByName?name=liming HTTP/1.1
# 查询列表
# GET http://localhost:3030/test/user?name=Hen HTTP/1.1
# 更新列表
PUT http://localhost:3030/test/updateUser HTTP/1.1
content-type: application/json
{
"id": "614301c49171f36ac7ad337c",
"name": "钱大妈"
}
3 变更记录
2021年8月20日,增加小节2.1
2021年9月17日,增加小节2.2