项目学习:抖音 “哲玄前端”,《大前端全栈实践课》,复盘真的很重要!!!
1️⃣ eplis-core内核引擎设计
-
首先,需要规范目录结构。通过app目录下的各种配置,比如
middleware,controller,router,service,extend,config等规范对应目录下的文件内容。通过对应的各种loader实现elpis-core。 -
elpis-core其实是一个非常轻量的egg.js内核,内核奉行约定优于配置的设计理念。了解elpis-core的目录结构:
-
中间件-
洋葱圈模型,遵循先入后出的原则。三层接入层,业务层,服务层。通过接入层(router,router-schema,middleware中间件),进入业务层,进行业务逻辑处理(通过controller处理器进行页面渲染ssr,读取配置config,extend调用各种拓展,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:
middlewareLoader,routerSchemaLoader,controllerLoader,serviceLoader,extendLoader(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 通过查看执行对应环境启动命令后,打印环境变量的内容,来检验环境配置是否成功。
3️⃣ eplis-core引擎内核应用
上一步配置已经实现解析器(eplis-core),现在我们需要通过eplis-core来渲染页面,如下图例:通过浏览器输入url地址,eplis-core解析对页面请求/api请求,通过层层中间件(洋葱圈模型)到对业务逻辑处理,最终对对应事件做出响应。
在全局中间件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>
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:
Ajv的使用:
ajv用于判断传进来的对象,是否个规则匹配.例如下面传入的price是string类型,而不是对应json-schema定义的number类型会报错:
json { "productId":1, "productName":"A green door", "price":"dsadx", "tag":["home","green"] }
使用Ajv和json-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 里。(推荐)
结果查看附件:
结论:
当前结构下,参数校验不能作为全局中间件获取到 ctx.params,只能在 controller 或路由 handler 里实现参数校验。这是 Koa 洋葱模型和注册顺序共同决定的