如何简单搭建一个类似egg.js的框架?

前言:第一次学习搭建一个框架(虽然有点low),开始的时候感觉挺难的,经过好几天的艰苦奋战终于有了不小的收获。嘿嘿

介绍:

前段时间学习了一下Egg.js这个框架后感觉这个蛋框架用起来还是蛮方便的,只要在固定的文件夹里面写好相应的内容也就是 按照一套统一的约定 进行应用开发,这些内容就会生效。这也就是Egg的理念:约定优于配置

QQ图片20230105181122.png

在这些固定的文件夹里面写上相应的内容

这样的理念就方便了开发者之间的沟通,大大的提高了开发效率。在学习使用egg.js后,根据其理念我也学习搭建了一个类似的框架,以下是学习搭建的过程。

工具:

  • koa
  • koa-router

搭建思路:

egg.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 (可选)

以上是egg.js的部分约定目录,详情 点击这里。根据egg.js的特性:我们如果想要搭建一个类似的框架,就需要实现以下功能:

  1. 读取目录里面的文件内容
  2. 读取到各个文件夹里面的内容之后,让路由层与其产生联系

项目结构

├─ myMvc                     
    ├─config             配置项        
    ├─controller         解析用户的输入
    |  └──home.js
    ├─middleware         编写中间件         
    ├─model              模型   
    |  └──article.js
    ├─routes             配置 URL 路由规则 
    | └──index.js
    ├─service            编写业务逻辑层                    
    index.js             入口文件
    my-loader.js         加载文件:用来读取文件夹与文件
    my.js                封装的框架类

概述:声明一个框架类,将读取各个层文件的函数挂载到其构造方法上,然后让路由层与其产生关联。

搭建开始

这个框架是基于 koa 进行封装的,所以在入口文件 index.js 里先使用 koa 启动项目,然后再以此为基础对其进行封装。

// index.js 根入口文件
const my=require('./my')   // 引入框架的构造函数 ,在这个文件里面引入koa
const app=new my()  // 实例化

app.start(3000)

约定自己的约定

使用 egg.js 时,我们如果想要指定某个url进行匹配,只需要在 router.js 里面写上如下代码:

module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);   // 指定 根路径 返回某个结果
  router.get('/list', controller.list.list); // 指定 /list 路径 返回某个结果
};

egg.js约定了其指定路由匹配的方法:router.get('/', controller.home.index)而不再是koa-router的原始方法。所以由egg.js得到启示:我们自己定义一种 约定,只要程序员按照这种约定形式进行coding,我们自己封装的koa可以读得懂这种形式的代码,可以进行相应功能的实现,那么就可以实现一个简单的框架。就比如我们期望程序员可以在路由层这样进行路由匹配:

// routes/index.js
module.exports = app => ({   // 让路由层接受一个参数代表 class my对象
    'get /':app.$ctrl.home.index,   // 用户使用控制层里面的文件   
    'get /detail': app.$ctrl.home.detail
})

上面就是我们自己约定的路由请求方法,即使用 'get /':app.$ctrl.home.index 这种形式来进行 url 匹配。说白了就是

router .get('/', (ctx, next) => { ctx.body = 'Hello World!'; })

这个方法的封装。

怎么实现 ?

要搭建一个类似 egg.js 的框架实现其类似功能也就是这个框架可以识别出自己的约定,然后框架将这个约定进行一系列的处理自动的整理成koa路由匹配的方式。底层逻辑还是 koa-router 对象上添加路由和对应的处理函数,明白了这个那么搭建一个类似的就比较简单了。

第一步

首先就是要让自己的框架读的懂自己的约定: 'get /':app.$ctrl.home.index。所以就要有一个可以读取文件的功能,将约定好的文件名下面的文件都加载出来。

// 读取目录和文件的方法
function load(dir, cb) {  // 文件夹名字 执行的回调函数
    const url = path.resolve(__dirname, dir)  // 获取某个文件的绝对路径
    const files = fs.readdirSync(url) // 读取某个文件夹里面的所有文件

    files.forEach(filename => {
        filename = filename.replace('.js', '')  // 获取文件名 去除文件类型 .js
        const file = require(url + '/' + filename)   // 获取某个文件里抛出的内容y
        cb(filename, file)
    })
}

第二步

有了加载文件的功能后当然是要读取到路由文件夹里的内容,根据约定的'get /':app.$ctrl.home.index写法,读取到对应的 get 方法与对应 '/'路径,然后再整理成koa-router的方法就可以了。

const Router = require('koa-router')
// 加载路由
function initRouter(app) {
    const router = new Router()  // 获取一个router实例对象
    load('routes', (filename, routes) => {      // 加载到route文件夹下面的文件
        const prefix = filename === 'index' ? '' : `/${filename}`  // 路由前缀,如果文件名不是index就默认加上前缀

        routes = typeof routes === 'function' ? routes(app) : routes

        Object.keys(routes).forEach(key => {
            const [method, path] = key.split(' ') // ['get', '/']
            // console.log(`正在映射地址:${method.toLocaleUpperCase()} ${prefix}${path}`);
            router[method](prefix + path, async ctx => {   // 注册路由
                app.ctx = ctx
                await routes[key](app)
            })
        })
    })
    return router
}

第三步

除了要读取routes文件夹,还要读取其他文件夹,这里就将它们写在同一个文件里面

// my-loader.js
const fs = require('fs');
const path = require('path');
const Router = require('koa-router')

// 读取目录和文件的方法
function load(dir, cb) {  // 文件夹名字 执行的回调函数
    const url = path.resolve(__dirname, dir)  // 获取某个文件的绝对路径
    const files = fs.readdirSync(url) // 读取某个文件夹里面的所有文件

    files.forEach(filename => {
        filename = filename.replace('.js', '')  // 获取文件名 去除文件类型 .js
        const file = require(url + '/' + filename)   // 获取某个文件里抛出的内容
        cb(filename, file)
    })
}

// 加载路由
function initRouter(app) {
    const router = new Router()  // 获取一个router实例对象
    load('routes', (filename, routes) => {      // 加载到route文件夹下面的文件
        const prefix = filename === 'index' ? '' : `/${filename}`  // 路由前缀,如果文件名不是index就默认加上前缀

        routes = typeof routes === 'function' ? routes(app) : routes

        Object.keys(routes).forEach(key => {
            const [method, path] = key.split(' ') // ['get', '/']
            // console.log(`正在映射地址:${method.toLocaleUpperCase()} ${prefix}${path}`);
            router[method](prefix + path, async ctx => {   // 注册路由
                app.ctx = ctx
                await routes[key](app)
            })
        })
    })
    return router
}

// 初始化控制层
function initController(app) {  
    const controllers = {}
    load('controller', (filename, controller) => {   // 读取到控制层的文件
        controller = typeof controller === 'function' ? controller(app) : controller
        controllers[filename] = controller   // controller.filename 的内容是 controller
    })
    return controllers
}

// 初始化服务层
function initService() {
    const services = {}
    load('service', (filename, service) => {

        services[filename] = service
    })
    return services
}

// model层初始化
const Sequelize = require('sequelize')
function loadConfig(app) {
    load('config', (filename, conf) => {
        if (conf.db) {
            app.$db = new Sequelize(conf.db)  // 负责初始化 config 配置,$db就是配置好的

            // 加载模型,现在知道了要操作的数据库。将表映射到数据库
            app.$model = {}
            load('model', (filename, { schema, options }) => {
                app.$model[filename] = app.$db.define(filename, schema, options)  // $db 就是myMvc这个数据库
            })
            app.$db.sync()  // 同步模块,让映射表同步到数据库中
        }
    })
}

module.exports = {
    initRouter,
    initController,
    initService,
    loadConfig
}

第四步

实现类似 egg.js router.get('/', controller.home.index)这个方法。无论是egg.js还是我们自己搭建的框架其底层原理都是基于koa的原始方法进行封装的。先看看egg.js,它在实现这个方法的时候不需要在当前页面引入 /controller/home这个文件,而是从egg对象上解构出来 controller 这个属性,换句话说egg将 controller 挂载到了其实例对象上面。

QQ图片20230105232046.png

所以类似我们自己写的框架也要实现这种功能,我们就可以定义一个框架类 class my,将解析出来的文件放到其构造函数上,那么我们也只需要解构框架实例对象里面的各个文件对象然后调用相关方法就可以。
框架类:

// my.js
const Koa = require('koa')
const { initRouter, initController, initService, loadConfig } = require('./my-loader')

class my {   // 相当于egg这个对象
    constructor() {
        this.$app = new Koa()
        loadConfig(this)   
        this.$service = initService()   // 服务层要在控制层之前初始化
        this.$ctrl = initController(this)   // 初始化控制层,得到控制层的所有文件  传入框架实例对象进去 那么就可以访问到 $service
        this.$router = initRouter(this)   // 路由层   这里将整个实例对象传给这个函数,然后这个函数就能使用前面的控制层
        this.$app.use(this.$router.routes())   // 路由生效  那么路由里的方法就会带有 ctx
    }
    start(port) {
        this.$app.listen(port, () => {
            console.log(`我的服务已启动在:${port}`);
        })
    }
}

module.exports = my

小结: 搭建一个类似egg.js框架的大体思路大概就是这样子吧,总结一下就是在框架类构造函数上面挂载各个文件对象,然后把自己约定好路由的写法:'get /':app.$ctrl.home.index悄咪咪的转换成koa的路由方法,这样就可以实现类似egg.js的效果。

最后:本文是学习搭建类似egg.js框架的学习历程,有很多不完美的地方,还请大佬们赐教😊🤝

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容