《⚙️ Elpis 实战笔记 #1 | Node.js 服务端内核引擎详解》

76 阅读7分钟

项目学习:抖音 “哲玄前端”,《大前端全栈实践课》,复盘真的很重要!!!

1️⃣ eplis-core内核引擎设计

  • 首先,需要规范目录结构。通过app目录下的各种配置,比如middleware,controller,router,service,extend,config等规范对应目录下的文件内容。通过对应的各种loader实现elpis-core。

  • elpis-core其实是一个非常轻量的egg.js内核,内核奉行约定优于配置的设计理念。了解elpis-core的目录结构:

Desktop.png

  • 中间件-洋葱圈模型,遵循先入后出的原则。三层接入层,业务层,服务层。通过接入层(router,router-schema,middleware中间件),进入业务层,进行业务逻辑处理(通过controller处理器进行页面渲染ssr,读取配置configextend调用各种拓展,env环境分发),再根据服务层service进行数据库读写,日志查询,调动外部服务等等。

  • 通过承接用户app目录下的文件,实现解析器(服务引擎elpis-koa),可以支持这样运行(API请求和页面请求通过洋葱圈模型)的效果。

2️⃣ elpis-core 引擎内核实现

入口文件配置

入口启动文件实现(index.js):

const ElpisCore = require('./elpis-core')

// 启动项目
ElpisCore.start({
	name:'Eplis',
	homePage:'/'
});

elpis-core启动文件elpis-core/index.js,基础配置项目:

const Koa = require('koa');
const path = require('path');
const { sep } = path;//兼容不同操作系统上的斜杠

module.exports = {
    /**
     * 启动项目
     * @param  options 项目配置
     */
       start(options = {}) {
        // koa实例
        const app = new Koa();

        // 应用配置
        app.options = options;

        // 基础路径
        app.baseDir = process.cwd();

        // 业务文件路径
        app.businessPath = path.resolve(app.baseDir,`.${sep}app`);
        
         // 启动服务
        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 (e) {
            console.error(e)
        }
    }
}

环境配置

初始化环境配置(用于区分本地,测试,生产环境):

eplis-core/env.js

module.exports = (app) => {
    return {
        // 判断是否本地环境
        isLocal() {
            return process.env._ENV === 'local';
        },

        // 判断是否测试环境
        isBeta() {
            return process.env._ENV === 'beta';
        },

        // 判断是否生产环境
        isProduction(){
            return process.env._ENV === 'production';
        },

        // 获取当前环境
        get() {
            return process.env._ENV ?? 'local';
        }

    }
}

eplis-core/index.js

const env = require('./env');

module.exports = {
        start(options = {}) { // koa实例 
        const app = new Koa(); // 应用配置 
        ......
        // 初始化环境配置
        app.env = env(); 
        console.log(`-- [start] env: ${app.env.get()} --`)
        ......
}

Loader配置

各种Loader启动+引入全局中间件middleware.js

middlewareLoaderrouterSchemaLoadercontrollerLoaderserviceLoaderextendLoader(extend不同于其他加载器直接挂载到app),routerLoader用于获取app目录下的文件并挂载到对应的loader上,configLoader用于获取对应环境配置,middleware.js用于挂载全局中间件,除了app/middleware/xxx.js,用户可以配置自己的中间件到app/middleware.js

elpis 
|-- elpis-core // 引擎内核 
| |-- loader 
| | |-- config // 配置区分(开发/测试/生产)环境,读取不同文件配置
| | |-- controller //负责接收路由分发的请求,处理业务逻辑,调用 service 层,返回统一结构响应
| | |-- extend //拓展方法
| | |-- middleware // 中间件
| | |-- router-schema //API 规则进行约束
| | |-- router //提取路由
| | |-- service // 封装具体的数据操作、外部接口调用
| |-- env.js //环境获取 
| |-- index.js 
|-- index.js // 入口文件
|-- package.json // 入口文件
 

完整启动文件配置eplis-core/index.js:

const Koa = require('koa');
const path = require('path');
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  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 --`)

        // 注册全局中间件
       try{
        require(`${app.businessPath}${sep}middleware.js`)(app);
        console.log(`-- [start] load globalMiddleware done --`)
       } catch (e) {
        console.log(e)
        console.log('[exception] there is no global middleware file')
       }

        // 注册 router路由
        routerLoader(app);
        console.log(`-- [start] load router done --`)

        // 启动服务
        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 (e) {
            console.error(e)
        }
    }

}

完整目录结构:

elpis 
|-- app // 文件目录 
| |-- controller // 业务逻辑处理 
| |-- extend // 全局拓展 
| |-- middleware // 中间件 
| |-- public // 静态资源 
| |-- router // 路由 
| |-- router-schema // 路由校验规则 
| |-- service // api及数据处理 
| |-- middlewares.js // 全局中间件 
|-- config // 环境配置文件 
| |-- -config.beta.js //测试环境 
| |-- -config.default.js //默认环境 
| |-- -config.local.js //本地环境 
| |-- -config.prod.js //生产环境 
|-- elpis-core // 引擎内核 
| |-- loader 
| | |-- config 
| | |-- controller 
| | |-- extend 
| | |-- middleware 
| | |-- router-schema 
| | |-- router 
| | |-- service 
| |-- env.js //环境获取 
| |-- index.js 
|-- index.js // 入口文件
|-- package.json // 入口文件

配置package.josn

package.json

  "scripts": {
    "lint": "eslint --quiet --ext js,vue .",
    "dev":"set _ENV=local && nodemon ./index.js",//win环境下 执行本地环境
    "beta":"set _ENV=beta && node ./index.js",//执行测试环境
    "prod":"set _ENV=production && node ./index.js",//执行开发环境
    "dev:mac":"_ENV='local' nodemon ./index.js",//mac环境下 执行本地环境
    "beta:mac":"_ENV='beta' node ./index.js",
    "prod:mac":"_ENV='production' node ./index.js",
  },

通过Loader/config.js获取app/config目录下的配置,通过切换的环境app/config/config.(local/beta/prod).js、去覆盖默认环境的内容app/config/config.default.js

执行npm run dev/beta/prod 通过查看执行对应环境启动命令后,打印环境变量的内容,来检验环境配置是否成功。

image.png

3️⃣ eplis-core引擎内核应用

上一步配置已经实现解析器(eplis-core),现在我们需要通过eplis-core来渲染页面,如下图例:通过浏览器输入url地址,eplis-core解析对页面请求/api请求,通过层层中间件(洋葱圈模型)到对业务逻辑处理,最终对对应事件做出响应。

image.png

在全局中间件app/middleware.js配置

koa-nujunks-2(模板渲染引擎)

const path = require('path');
module.exports = (app) => {
     // 模板渲染引擎
    const koaNunjucks = require('koa-nunjucks-2');
    app.use(koaNunjucks({
        ext: 'tpl',//模板后缀
        path: path.resolve(process.cwd(), './app/public'),//模板路径
        nunjucksConfig: {
            noCache: true,//不缓存模板
            trimBlocks: true//  开启块级缓存
        }
    }))
}

app/public目录下,新建output目录,且新建entry.page1.tpl,entry.page2.tpl;再配置页面对应的路由app/router/xxx.js,路由内需要控制器app/controller/xx.js

//app/router/view.js
module.exports = (app,router) => {
    const {view:ViewController} = app.controller;

    // 用户输入 http://ip:port/view/page1 能渲染对应的页面
    router.get('/view/:page', ViewController.renderPage.bind(ViewController));
}

//app/controller/view.js
module.exports = (app) => {
   return class ViewController {
    /**
     * 渲染页面
     * @param {object} ctx koa 上下文
     */
    async renderPage(ctx){
        await ctx.render(`output/entry.${ctx.params.page}`,{
            name: app.options?.name,
            env: app.env.get(),
            options: JSON.stringify(app.options)
        })
    }
   }
};

//app/public/output/entry.page1.tpl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{name}}</title>
    <link href="/static/normalize.css" rel="stylesheet">
    <link href="/static/logo.png" rel="icon" type="image/x-icon">
</head>

<body style="margin: 0;">
    <h1 style="color:red;">Page1</h1>
    <input id ="env" value="{{env}}">
    <input id ="options" value="{{options}}">
</body>
<script type="text/javascript">
 try {
        window.env = document.getElementById('env').value;
        const options = document.getElementById('options').value;
        window.options = JSON.parse(options);
    } catch (e) {
        console.log(e)
    }
</script>

</html>

//app/public/output/entry.page2.tpl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>eplis</title>
    <link href="/static/normalize.css" rel="stylesheet">
    <link href="/static/logo.png" rel="icon" type="image/x-icon">
</head>

<body style="margin: 0;">
    <h1 style="color:blue;">Page2</h1>
</body>
<script type="text/javascript">
</script>

</html>

image.png

koa-static(获取静态目录)

往中间件配置koa-static,否则tpl的<link href="/static/logo.png" rel="icon" type="image/x-icon">图片资源无法访问

const path = require('path');

module.exports = (app) => {
    
    // 配置静态根目录
    const koaStatic = require('koa-static');
    app.use(koaStatic(path.resolve(process.cwd(), './app/public')));
    
    // 模板渲染引擎
    ....
 }

koa-bodyparser(解析body中间件)

配置koa-bodyparser解析body中间件,用于执行ajax请求,接收请求参数。实现服务请求响应。

const path = require('path');
module.exports = (app) =>{
    //配置静态根目录
    ....
    //模板渲染引擎
    ...
     // 引入 ctx.body 解析中间件
    const bodyParser = require('koa-bodyparser');
    app.use(bodyParser({
        formLimit:'1000mb',
        enableTypes:['json','form','text'],
    }));
}

增加可维护性,安全性,拓展性

抽离公共方法,新建controller、service的基类base.js

如果后续controller方法变多,会有很多重复性的代码,比如返回状态码,数据和提示信息。所以设计controller的返回是一个class类,设置成对象方便继承,此处创建基类base.js.

通过基类公共方法获取成功和失败的返回结构:app/controller/base.js

module.exports = (app) => class BaseController {
    /**
     * controller 基类
     * 统一收拢 controller 公共方法
     */
    constructor(){
        this.app = app;
        this.config = app.config;
    }

    /**
     * API 处理成功时统一返回结构
     * @params {object} ctx 上下文
     * @params {object} data 返回数据
     * @params {object} metadata 附加数据
     */

    success(ctx,data={},metadata={}){
        ctx.status = 200;
        ctx.body = {
            success:true,
            data,
            metadata
        }
    }

    /**
     * API 处理失败时统一返回结构
     * @params {object} ctx 上下文
     * @params {object} message 错误信息
     * @params {object} code错误码
     */
    fail(ctx,message,code){
        ctx.body = {
            success:false,
            message,
            code
        }
    }
}

app/controller/project.js

module.exports = (app) => {
    const BaseController = require('./base')(app);
   return class ProjectController extends BaseController{
    /**
     * 获取项目列表
     * @parma {object} ctx koa 上下文
     */
    async getList(ctx){
        const {project:ProjectService} = app.service;
        const projectList = await ProjectService.getList();
        this.success(ctx,projectList)
    }
   }
};

同理service处也是设置基类base.js

app/service/base.js

const superagent = require('superagent');
module.exports = (app) => class BaseService {
    /**
     * service 基类
     * 统一收拢 service 公共方法
     */
    constructor(){
        this.app = app;
        this.config = app.config;
        this.curl = superagent;
    }
}

app/service/project.js

module.exports = (app) => {
    const BaseService = require('./base')(app);
    return class ProjectService extends BaseService {
        async getList(){
            return [
                {name:'project1',desc:'project1 desc'},
                {name:'project2',desc:'project2 desc'},
                {name:'project3',desc:'project3 desc'}
            ]
        }
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{name}}</title>
    <link href="/static/normalize.css" rel="stylesheet">
    <link href="/static/logo.png" rel="icon" type="image/x-icon">
</head>

<body style="margin: 0;">
    <h1 style="color:red;">Page1</h1>
    <input id ="env" value="{{env}}">
    <input id ="options" value="{{options}}">
    <button onclick="handleClick()">发送请求</button>
</body>
<script type="text/javascript">
 try {
        window.env = document.getElementById('env').value;
        const options = document.getElementById('options').value;
        window.options = JSON.parse(options);
    } catch (e) {
        console.log(e)
    }
    const handleClick = () => {
        axios.request({
            method:'post',
            url:'/api/project/list',
            data:{a:1,b:2,c:3}
        })
    }
</script>

</html>

log4js(配置日志)

新增extend/logger.js,在需要的页面调用logger方法,输出日志会在logs目录下,写法为 app.logger.info()

const log4js = require('log4js');

/**
 * 日志工具
 * 外部调用 app.logger.info logger.error
 */

module.exports = (app) => {
    let logger;

    if (app.env.isLocal()) {
        // 打印控制台即可
        logger = console;
    } else {
        // 把日志输出并落地到磁盘(日志落盘)
        log4js.configure({
            appenders: {
                console: {
                    type: 'console'
                },
                // 日志文件切分
                dataFile: {
                    type: 'dateFile',
                    filename: './logs/application.log',
                    pattern: '.yyyy-MM-dd',
                }
            },
            categories:{
                default:{
                    appenders: ['console','dataFile'],
                    level: 'trace'
                }
            }
        });

        logger = log4js.getLogger();
    }
    return logger;
}

配置app目录的middleware中间件

在全局中间件引入app目录下的中间件

const path = require('path');
module.exports = (app) =>{
    //配置静态根目录
    ....
    //模板渲染引擎
    ...
     // 引入 ctx.body 解析中间件
   ...
   // 异常捕获
    app.use(app.middlewares.errorHandler);

    // 签名合法性
    app.use(app.middlewares.apiSignVerify);

    // 引入 API 参数校验
    app.use(app.middlewares.apiParmasVerify);
}

异常错误处理

app/middleware/error-handler.js

module.exports = (app) => {
    return async (ctx,next) => {
        try{
            await next()
        }catch(e){
            //异常处理
        }
}
/**
 * 运行时异常错误处理,兜底所有异常
 * @param {[object]} app koa 实例
 */
module.exports = (app) => {
    return async (ctx,next) => {
        try{
            await next();
        }catch(err){
            // 异常处理
            const {status,message,detail} = err;

            app.logger.info(JSON.stringify(err));
            app.logger.error(`[-- exception --]:`,err);
            app.logger.error(`[-- exception --]:`,status,message,detail);

            if(message && message.indexOf('template not found') > -1){
                // 页面重定向
                ctx.status = 302;//临时重定向
                ctx.redirect(`${app.options?.homePage}`);
                return;
            }

            const resBody = {
                success:false,
                code:50000,
                message:'网络异常 请稍后重试'
            }

            ctx.status = 200;
            ctx.body = resBody;

        }
    }
}

API 签名合法性校验

app/middleware/api-sign-verify.js

const md5 = require('md5')
/**
 * API 签名合法性校验
 */
module.exports = (app) => {
  return async (ctx,next) => {
    // 只对API请求做校验
    if(ctx.path.indexOf('/api') < 0){
        return await next();
    }

    const {path,method} = ctx;
    const {headers} = ctx.request;
    const {s_sign:sSign,s_t:st} = headers;

    const signKey = '***********';//设置自己想要设置的秘钥内容
    const signature = md5(`${signKey}_${st}`);
    app.logger.info(`[${method} ${path}] signature: ${signature}`);//打印请求方法,签名

    if(!sSign || !st || signature !== sSign.toLowerCase() || Date.now() - st > 600000){
        
        ctx.status = 200;
        ctx.body = {
            success:false,
            message:'signature not correct or api timeout!!',
            code:445
        }
        return ;
    }

    await next();  
  }
}

验证签名正确才能成功获取请求:

<script>
 //...
 const handleClick = () => {
 const signKey = '***********';//同后端配置的signKey
 const st = Date.now();
 axios.request({
        method:'post',
        url:'/api/project/list',
        data:{a:1,b:2,c:3},
        headers:{s_t:st,s_sign:md5(`${signKey}_${st}`)}
     });
  }
</script>
API 参数校验

通过json-shcema配置接口的参数类型和描述,参考比如productId是一个int类型,描述文本是description的内容,json-schema需要配套搭配ajv

image.png Ajv的使用:

image.png

ajv用于判断传进来的对象,是否个规则匹配.例如下面传入的price是string类型,而不是对应json-schema定义的number类型会报错:

json { "productId":1, "productName":"A green door", "price":"dsadx", "tag":["home","green"] }

使用Ajvjson-schema对api参数进行校验:app/middleware/api-parmas-verify.js

const Ajv = require('ajv');
const ajv = new Ajv();
/**
 * API 参数校验
 */
module.exports = (app) => {
    const $schema = 'http://json-schema.org/draft-07/schema#';

    return async (ctx, next) => {
        // 只对API请求做校验
        if (ctx.path.indexOf('/api') < 0) {
            return await next();
        }

        // 获取请求参数
        const { body, query, headers } = ctx.request;
        const { params, path, method } = ctx;

        app.logger.info(`[${method} ${path}] body:${JSON.stringify(body)}`);
        app.logger.info(`[${method} ${path}] query:${JSON.stringify(query)}`);
        app.logger.info(`[${method} ${path}] params:${JSON.stringify(params)}`);
        app.logger.info(`[${method} ${path}] headers:${JSON.stringify(headers)}`);

        const schema = app.routerSchema[path]?.[method.toLowerCase()];

        if (!schema) {
            return await next();
        }

        let valid = true;

        // ajv 校验
        let validate;

        // 校验 headers
        //valid为true且存在headers
        if (valid && headers && schema.headers) {
             schema.headers.$schema = $schema;//指定草案
            validate = ajv.compile(schema.headers);//编译
            valid = validate(headers);//校验
        }

        // 校验 body
        if (valid && body && schema.body) {
            schema.body.$schema = $schema;
            validate = ajv.compile(schema.body);
            valid = validate(body);
        }

        // 校验 query
        if (valid && query && schema.query) {
            schema.query.$schema = $schema;
            validate = ajv.compile(schema.query);
            valid = validate(query);
        }

        // 校验 params
        if (valid && params && schema.params) {
            schema.params.$schema = $schema;
            validate = ajv.compile(schema.params);
            valid = validate(params);
        }
        
        //用户传过来的校验和schema的不匹配,报错:
        if (!valid) {
            ctx.status = 200;
            ctx.body = {
                success: false,
                message: `request validate fail: ${ajv.errorsText(validate.errors)}`,
                code: 442
            }
            return;
        }

        await next();
    }
}

验证,在app/router-schema新建project.js,且proj_key参数为string类型且必须

module.exports = {
    '/api/project/list':{
        get:{
            query:{
                type:'object',
                properties:{
                    proj_key:{
                        type:'string',
                    }
                },
                required:['proj_key']
            },
        }
    }
}

app/controller/project.js中,如果请求参数缺少proj_key或者类型不匹配会报错

module.exports = (app) => {
    const BaseController = require('./base')(app);
   return class ProjectController extends BaseController{
    /**
     * 获取项目列表
     * @parma {object} ctx koa 上下文
     */
    async getList(ctx){
        const {proj_key:projKey} = ctx.request.query;
        console.log('projKey',projKey);

        const {project:ProjectService} = app.service;
        const projectList = await ProjectService.getList();
        this.success(ctx,projectList)
    }
   }
};

<script>
 //...
 const handleClick = () => {
 const signKey = '***********';//同后端配置的signKey
 const st = Date.now();
 axios.request({
        method:'get',
        url:'/api/project/list',
        parmas:{proj_key:'test'},
        headers:{s_t:st,s_sign:md5(`${signKey}_${st}`)}
     });
  }
</script>

4️⃣ 章节总结

1. 设计分层与加载流程

  • 磁盘文件层:项目结构清晰,按 router、controller、service、middleware、config 等目录分离,便于维护和扩展。
  • 解析器(Loader)层:启动时通过各类 loader(如 router-loader、controller-loader、service-loader 等)自动扫描并加载对应模块,动态组装应用内存结构,提升了自动化和可插拔能力。
  • 运行时(内存)层:所有业务、路由、中间件、服务等都已注册到 Koa 实例,形成完整的运行时环境。

2. 洋葱圈模型的设计理解

  • 中间件链式处理:Koa 的中间件采用“洋葱圈”模型,所有请求会按注册顺序依次进入每一层中间件(如日志、鉴权、参数校验、错误处理等),每层可决定是否继续传递或拦截处理。
  • 业务解耦:中间件负责通用逻辑,controller/service 只关注业务本身,极大提升了代码复用性和可维护性。
  • 灵活扩展:可随时插拔新的中间件或调整顺序,满足不同业务场景需求。

3. 业务分层与职责

  • Controller 层:负责接收路由分发的请求,处理业务逻辑,调用 service 层,返回统一结构响应(如 success/fail 方法)。
  • Service 层:封装具体的数据操作、外部接口调用等,controller 只需关注业务流程。
  • Extend/Config/SSR:通过 extend-loader/config-loader/SSR 等机制,controller 可灵活获取扩展方法、配置项、页面渲染能力。

4. 路由与参数校验

  • router-schema:通过 router-schema-loader 加载路由参数校验规则,结合中间件实现自动化校验,提升 API 安全性和规范性。
  • 路由分发:router-loader 动态注册所有路由,支持 API 和页面请求统一分发。

5. 总结

  • 优点:高度自动化、分层清晰、扩展性强、易于维护,充分利用 Koa 洋葱圈模型实现灵活的中间件链式处理。
  • 适用场景:适合中大型 Node.js/Koa 项目,支持多业务线、复杂中间件链、自动化 API 校验和页面渲染。

5️⃣ 问题拓展

1.loader 加载顺序是否可以进一步优化?

const Koa = require('koa');
const path = require('path');
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  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 --`)

        // 注册全局中间件
       try{
        require(`${app.businessPath}${sep}middleware.js`)(app);
        console.log(`-- [start] load globalMiddleware done --`)
       } catch (e) {
        console.log('[exception] there is no global middleware file'+ e)
       }

        // 注册 router路由
        routerLoader(app);
        console.log(`-- [start] load router done --`)

        // 启动服务
        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 (e) {
            console.error(e)
        }
    }

}

可以进一步优化 loader 加载顺序,让依赖关系更清晰、初始化更高效。推荐顺序如下:

  • 加载 config(优先加载配置,后续 loader 可用到配置项)
  • 加载 extend(扩展 Koa 实例,方便后续 loader 使用扩展方法/属性)
  • 加载 middleware(注册基础/全局中间件)
  • 加载 controller/service(controller 依赖 service,service 可提前加载)
  • 加载 routerSchema(路由参数校验规则,供路由和中间件使用)
  • 加载 router(最后注册路由,确保所有依赖已初始化)
    示例顺序: configLoader(app) --> extendLoader(app) --> middlewareLoader(app)--> serviceLoader(app)-->controllerLoader(app)-->routerSchemaLoader(app)-->routerLoader(app)

理由:

  • config/extend 优先,保证后续 loader 能用到扩展和配置。
  • middleware 早注册,controller/service/router 可用到全局中间件。
  • service 先于 controller,controller 依赖 service。
  • routerSchema 在 router 前,路由和参数校验可用。
  • router 最后注册,确保所有依赖已初始化。
    这样顺序更合理,依赖更清晰,项目启动更健壮。

2.app/middleware/error-handler.js,这里为什么用 302,而不是 301 ? 301 302 303 具体有什么不一样?

这里用 302 是因为它表示“临时重定向”,即当前请求临时跳转到其他页面,之后还可能回到原页面。302适合页面临时不可用或临时跳转,等新页面上线后,用户可以直接访问新页面。301适合永久迁移,原地址以后都不会再用。新页面上线后,用户访问不到新页面还是之前跳转结果。 区别如下:

  • 301 Moved Permanently:永久重定向,浏览器和搜索引擎会记住跳转,后续请求都直接跳转到新地址。
  • 302 Found:临时重定向,浏览器不会记住,之后还会访问原地址。
  • 303 See Other:重定向到另一个地址,通常用于 POST 请求后跳转到 GET 页面。

一般页面或模板未找到时用 302,表示只是临时跳转。301 用于域名、路径永久迁移。303 用于表单提交后跳转.

3.app/middleware/api-parmas-verify.js,思考一下这里能否获取 params,如果不行,为什么不行?能如何解决?

这里是获取不到params的。因为在配置的洋葱圈模型执行顺序中,index.js先执行了middleware后执行路由。

原因:

  • 在 index.js 里,先注册了全局中间件(包括参数校验),后注册路由。
  • Koa 的洋葱模型决定了:只有在路由挂载之后注册的中间件,才能在路由匹配后拿到 ctx.params。
  • 所以在全局中间件里,参数校验执行时,ctx.params 还没有被 Koa-router赋值,结果就是 undefined。

如何解决:

  • 参数校验中间件必须在路由挂载之后注册,才能获取到 ctx.params。
  • 如果顺序不能改,只能把参数校验逻辑放到 controller 层或每个路由 handler 里。(推荐)
    结果查看附件:

params.png 结论: 当前结构下,参数校验不能作为全局中间件获取到 ctx.params,只能在 controller 或路由 handler 里实现参数校验。这是 Koa 洋葱模型和注册顺序共同决定的