前言
对于前端开发来说,想了解和熟悉后端开发的世界观,比较友好的过渡方式是对 Node.js 进行学习,具体基础的学习这里就不展开啦。
在企业的开发中,一般都会通过框架来快速落地项目,但对于初学者来说,想更好的巩固基础,最好不要一上来就使用太上层的框架,因为很多方法框架都已经帮你封装好了,这会导致初学者对底层 Node.js 或逻辑交互的理解很不清晰,就不利于巩固学习。
因此为了更好的学习和理解,搭建了一个简易版的服务端框架 elpis-core,主要参考 Egg.js 的设计理念并基于 Koa.js 实现。
选择 Koa.js,是因为它不绑定任何的框架,干净简洁,小而精,更容易实现定制化,扩展性好。
设计思路
在开始 elpis-core 的讲解之前呢,我们先来看看 Egg.js 所创建的项目目录结构:
egg-project
├── package.json
├── app.js(可选)
├── agent.js(可选)
├── app
| ├── router.js
│ ├── controller
│ │ └── home.js
│ ├── service(可选)
│ │ └── user.js
│ ├── middleware(可选)
│ │ └── response_time.js
│ ├── schedule(可选)
│ │ └── my_task.js
│ ├── public(可选)
│ │ └── reset.css
│ ├── view(可选)
│ │ └── home.tpl
│ └── extend(可选)
│ ├── helper.js(可选)
│ ├── request.js(可选)
│ ├── response.js(可选)
│ ├── context.js(可选)
│ ├── application.js(可选)
│ └── agent.js(可选)
└── config
├── plugin.js
├── config.default.js
├── config.prod.js
├── config.test.js(可选)
├── config.local.js(可选)
└── config.unittest.js(可选)
Eggjs 官网有这样一句话:
Egg 奉行“约定优于配置”。
Egg.js 通过对目录的约定,来规范项目结构,减少了大量协作和沟通成本。我们来简化一下目录:
app/router.js用于配置 URL 路由规则app/controller/**用于解析用户的输入,处理后返回相应的结果app/service/**用于编写业务逻辑层,建议使用app/middleware/**用于编写中间件app/public/**用于放置静态资源app/extend/**用于框架的扩展config/config.{env}.js用于编写配置文件config/plugin.js用于配置需要加载的插件test/**用于单元测试app.js和agent.js用于自定义启动时的初始化工作
那么思考一下:
- 底层是如何将这些约定的目录进行统筹运作起来的呢?
- 为什么只要创建好这些目录和文件,就能开启一个接口服务呢?
这两个思考,就交给今天的主角 elpis-core 来解答吧。
解析器:elpis-core
要让约定好的目录运行起来,我们可以想到,是不是只要有一个解析器,来负责读取约定好的目录结构,并通过遍历来执行对应文件中的方法,就可以实现统筹运作了呀
而我们接下来要讲的 elpis-core 就是一个解析器,底层由 Koa.js 实现,在这个解析器中,我们封装了针对不同目录所对应的 Loader(加载、解析的意思):
- loader/config.js ➡️ app/config
- loader/controller.js ➡️ app/controller
- loader/extend.js ➡️ app/extend
- loader/middleware.js ➡️ app/middleware
- loader/router-schema.js ➡️ app/router-schema
- loader/router.js ➡️ app/router
- loader/service.js ➡️ app/service
以上就是 elpis-core 约定好的规范,每个 loader 负责去读取 app 中对应的文件目录,然后将执行后获取的类或方法,挂载到 koa 的实例上,这里就实现了将磁盘文件转化为内存(运行时)的过程了。
因此,有了这个 elpis-core,我们只需要关注 app 中文件的创建,相互之间共享着 app 上的属性,实现目录间的统筹运作。
目录参考
部分代码
const Koa = require('koa');
const path = require('path');
/**
* sep 兼容不同操作系统上的斜杠
* 例如:`./app` => `.${sep}app`
*/
const { sep } = path;
const env = require('./env');
const middlewareLoader = require('./loader/middleware');
const routerSchemaLoader = require('./loader/router-schema');
const routerLoader = require('./loader/router');
const controllerLoader = require('./loader/controller');
const serviceLoader = require('./loader/service');
const configLoader = require('./loader/config');
const extendLoader = require('./loader/extend');
module.exports = {
/**
* 启动项目
* @param {object} options 扩展配置
* options = {
* name // 项目名称
* homePage // 项目首页
* }
*/
start(options = {}) {
// Koa 实例
const app = new Koa();
// 应用配置
app.options = options;
// 基础路径
app.baseDir = process.cwd();
// 业务文件路径
app.businessPath = path.resolve(app.baseDir, `.${sep}app`);
// 初始化环境配置
app.env = env();
console.log(`-- [start] env: ${app.env.get()} --`);
// 加载 middleware
middlewareLoader(app);
console.log(`-- [start] load middleware done --`);
// 加载 routerSchema
routerSchemaLoader(app);
console.log(`-- [start] load routerSchema done --`);
// 加载 controller
controllerLoader(app);
console.log(`-- [start] load controller done --`);
// 加载 service
serviceLoader(app);
console.log(`-- [start] load service done --`);
// 加载 config
configLoader(app);
console.log(`-- [start] load config done --`);
// 加载 extend
extendLoader(app);
console.log(`-- [start] load extend done --`);
// 注册全局中间件(用户引用到的外部中间件)
// app/middleware.js
// 用户需要经历一系列验证中间件(洋葱圈模型),才能到达最终的数据返回
try {
require(`${app.businessPath}${sep}middleware.js`)(app);
console.log(`-- [start] load global Middleware done --`);
} catch (error) {
console.log('[Exception] there is no global middleware file.');
}
// 注册 rouer
routerLoader(app);
console.log(`-- [start] load router done --`);
// 启动服务
// trycatch 保证服务启动的健壮性
try {
const port = process.env.PORT || 8080;
const host = process.env.IP || '0.0.0.0';
app.listen(port, host);
console.log('Server running on port:', port);
} catch (error) {
console.log(error);
}
},
};
测试接口服务,以 SSR 渲染为例
- router 中定义路由:
- controller 中定义控制层来操作服务层 service,但这里只是获取渲染页面,就不演示 service 了:
- ctx 会将 public/dist 目录下的模板入口页面,返回给前端:
更多学习:抖音 "哲玄前端",《全栈实践课》