由于工作职责长期处于基于数据渲染页面的纯前端工作,对后端世界的概念了解甚少,所以基于node实现一个服务端内核,拓展后端视野。
后端项目是怎么启动的?
当一个后端项目,通过某种方法启动,到服务监听端口,正常执行请求的处理,返回数据。整个过程是怎么启动起来的呢?请看下方的简易流程图
环境变量的读取
使用node启动的时候,通过cross-env包兼容wndows和linux环境变量的注入,例如:cross-env NODE_ENV=development node xxx.js
module.exports = (app) => {
return {
// 判断当前环境是否开发环境
isDevelopment() {
return process.env.NODE_ENV === "development";
},
// 判断当前环境是否测试环境
isTest() {
return process.env.NODE_ENV === "test";
},
// 判断当前环境是否生产环境
isProduction() {
return process.env.NODE_ENV === "production";
},
// 获取当前环境
get() {
return process.env.NODE_ENV ?? "development";
},
};
};
Loader实现
controller Loader实现
const path = require("path");
const glob = require("glob");
const { sep } = path;
/**
* 把多个中间件函数注册到app.controller
* @param {object} app koa 实例
*
* 文件系统:app/controller
* | custom-model
* | xxxx.js
* | custom-controller.js
* 结果: app.controller.customModel.customController
*/
module.exports = (app) => {
// 获取app/controller目录
const controllerPath = path.join(app.businessPath, `controller`);
// 获取所有文件
const fileList = glob.sync(path.join(controllerPath, "**", "*.js"));
const controller = {};
// 遍历所有文件
fileList.forEach((filePath) => {
let tempController = controller;
// 从路径中截取合法的子路径
let relPath = path.relative(controllerPath, filePath);
// 将 - 或者 _ 转换成驼峰
relPath = relPath.replace(/[-_](\w)/gi, (_, letter) =>
letter.toUpperCase(),
);
const parts = relPath.split(sep);
let filename = parts.pop();
filename = path.parse(filename).name;
const names = [...parts, filename];
// 创建嵌套对象指向
for (let i = 0, len = names.length; i < len; ++i) {
if (i == len - 1) {
const Controller = require(filePath)(app);
tempController[names[i]] = new Controller();
} else {
if (!tempController[names[i]]) {
tempController[names[i]] = {};
}
tempController = tempController[names[i]];
}
}
});
// 挂载到app.controller中
app.controller = controller;
};
config Loader实现
用于合并多个环境的配置
const path = require("path");
/**
* 把多个config合并成一个
* @param {object} app koa 实例
*
* 文件系统:config
* | config.development.js
* | config.default.js
* | config.test.js
* | config.production.js
*
* 结果: app.config = { ...config.default, ...config.[env] }
*/
module.exports = (app) => {
// 获取config目录路径
const configPath = path.join(app.baseDir, `config`);
// 获取默认的config配置
let defaultConfig = {};
try {
defaultConfig = require(path.join(configPath, "config.default.js"))(app);
} catch (err) {
console.error(`[default config load exception] ${err.message}`);
}
// 获取环境配置
let envConfig = {};
const { isDevelopment, isTest, isProduction } = app.$env;
try {
if (isDevelopment()) {
envConfig = require(path.join(configPath, `config.development.js`))(
app,
);
} else if (isTest()) {
envConfig = require(path.join(configPath, `config.test.js`))(
app,
);
} else if (isProduction()) {
envConfig = require(
path.join(configPath, `config.production.js`),
)(app);
}
} catch (err) {
console.error(`[env config load exception] ${err.message}`);
}
// 合并配置
app.config = {
...defaultConfig,
...envConfig,
};
};
其余Loader实现
这些都是相似的代码逻辑实现。暂不过多说明了...
解析引擎实现
核心就是通过多个loader解析对应的模块文件,挂载到运行时app实例中
// elpis-core/index.js
module.exports = {
/**
* 启动项目
* @param {object} options 项目配置
* @param {string} options.name 项目名
*/
start(options = {}) {
const app = new Koa();
// 项目配置
app.options = options;
// 基础路径
app.baseDir = process.cwd();
// 应用路径
app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
// 环境变量加载器
app.$env = env(app);
console.log(`-- [start] env: ${app.$env.get()}`);
// 配置加载器
configLoader(app);
console.log(`-- [start] config loader done--`);
// 拓展加载器
extendLoader(app);
console.log(`-- [start] extend loader done--`);
// 服务加载器
serviceLoader(app);
console.log(`-- [start] service loader done--`);
// middleware加载器
middlewareLoader(app);
console.log(`-- [start] middleware loader done--`);
// 路由参数配置加载器
routerSchema(app);
console.log(`-- [start] router schema loader done--`);
// 控制器加载器
controllerLoader(app);
console.log(`-- [start] controller loader done--`);
// 前置中间件注册
try {
require(path.resolve(app.businessPath, `.${sep}pre-middleware.js`))(app);
console.log(`-- [start] global middleware loader done--`);
} catch (err) {
console.error(`-- [global middleware exception] ${err.message}--`);
}
// 路由加载器
routerLoader(app, require(path.join(app.businessPath, `post-router.js`)));
console.log(`-- [start] router loader done--`);
// 启动服务
try {
const port = process.env.PORT || 8080;
const host = process.env.HOST || "0.0.0.0";
app.listen(port, host);
console.log(`${app.options.name} Server listening on ${host}:${port}`);
} catch (error) {
console.error(error);
}
},
};
执行效果
尾言
以上就完成了一个服务端内核的实现。当调用start()方法的时候,会使用elpis-core/下的各个loader,按顺序将模块目录下的文件加载到内存中,并挂载到app实例中,然后启动服务。应用层就遵守[约定大于配置]的规则,根据业务自行组织了