前提: 本篇文章是在学习了【抖音“哲玄前端”的《全栈实践课》】得出的一些个人总结,主要记录自己不明白或者深入去了解的点,具体实现可以前往抖音下单学习
第一章 - elpis-core内核开发
具体loader
elpis内核主要是编写各种loader并将其挂在到Koa实例上,其主要有
configLoader(配置项)
serviceLoader(服务层)
middlewareLoader(中间件)
routerSchemaLoader(路由检验)
controllerLoader(控制器)
extendLoader(其他拓展功能)
routerLoader(路由)
内核代码文件
elpis-core
| -- loader
| -- config.js (将config下的配置挂载到app.config)
| -- service.js (将service下的服务挂载到app.services)
| -- middleware.js (将各种中间件挂载到app.middlewares)
| -- router-schema.js (将config挂载到app.routerSchemas)
| -- controller.js (将控制器挂载到app.controllers)
| -- extend.js (将功能挂载到app)
| -- router.js (将定义的接口注册到koa-router)
| -- env.js (判断和和获取环境信息)
| -- index.js (创建koa实例并挂载各种loader在实例上)
env.js (判断和和获取环境信息)
module.exports = (app) => {
return {
// 判断当前环境是否为本地环境
isLocal() {
return process.env._ENV === 'local'
},
// 判断当前环境是否为测试环境
isBeta() {
return process.env._ENV === 'beta'
},
// 判断当前环境是否为生产环境
isProduction() {
return process.env._ENV === 'prod'
},
// 获取当前环境
get() {
return process.env._ENV ?? 'local'
}
}
}
index.js (创建koa实例并挂载各种loader在实例上)
这里得注意各个loader的挂载先后顺序,例如controllerLoader需要用到app.services,那就需要serviceLoader先于controllerLoad挂载
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:'my-project', // 项目名称
homepath: 'https://example.com', // 项目首页
}
*/
start(options = {}) {
const app = new Koa(); // 创建实例
// 应用配置
app.options = options
console.log('app.options: ', app.options);
// 设置基础路径
app.baseDir = process.cwd()
console.log('app.baseDir: ', app.baseDir);
// 设置基础路径
app.bussinessPath = path.resolve(app.baseDir, `.${sep}app`)
console.log('app.bussinessPath: ', app.bussinessPath);
// 初始化环境配置
app.env = env()
console.log(`-- [start] env: ${app.env.get()} --`);
configLoader(app) // 加载config
console.log(`-- config --`, app.config);
console.log(`-- [start] loaded config --`);
serviceLoader(app) // 加载service
console.log(`-- services --`, app.services);
console.log(`-- [start] loaded service --`);
middlewareLoader(app) // 加载middleware
console.log(`-- middlewares --`, app.middlewares);
console.log(`-- [start] loaded middleware --`);
routerSchemaLoader(app) // 加载routerSchema
console.log(`-- routerSchemas --`, app.routerSchemas);
console.log(`-- [start] loaded routerSchemas --`);
controllerLoader(app) // 加载controller
console.log(`-- controllers --`, app.controllers);
console.log(`-- [start] loaded controller --`);
extendLoader(app) // 加载extend
console.log(`-- extend --`, app.extend);
console.log(`-- [start] loaded extend --`);
// 注册全局中间件
try {
require(`${app.bussinessPath + sep}middleware.js`)(app)
console.log(`-- [start] loaded global middleware --`);
} catch (error) {
console.log('[Exception] there is no middleware file');
}
routerLoader(app) // 加载router
console.log(`-- [start] loaded router --`);
// 启动服务
try {
const port = process.env.PORT || 8080; // 获取端口号
const host = process.env.IP || '0.0.0.0'; // 获取ip地址
app.listen(port, host)
console.log(`Server running on port: ${port}`)
} catch (e) {
console.error(e);
}
}
}
工具:
path: 其中提供的sep是用来在不同的操作系统上(Windows是\,Linux 和 macOS是/)都能正确处理文件路径。
configLoader(配置项)
const path = require('path')
const { sep } = path
/**
* config loader
* @param {object} app Koa实例
*
* 配置区分 本地/测试/生产,通过 env 环境读取不同文件配置 env.config
* 通过 env.config 覆盖 default.config 加载到app.config中
*
* 目录下对应的 config 配置
* 默认配置 config/config.default.js
* 本地配置 config/config.local.js
* 测试配置 config/config.beta.js
* 生产配置 config/config.prod.js
*/
module.exports = (app) => {
// 获取config目录
const configPath = path.resolve(app.baseDir, `.${sep}config`)
// 获取默认config
let defaultConfig = {}
try {
defaultConfig = require(path.resolve(configPath, '.${sep}config.default.js'))
} catch (error) {
console.error('[Exception] there is no config.default file')
}
// 获取env config
let envConfig = {}
try {
envConfig = require(path.resolve(configPath, `.${sep}config.${app.env.get()}.js`))
} catch (error) {
console.error(`[Exception] there is no config.${app.env.get()} file`)
}
// 合并配置
app.config = Object.assign({}, defaultConfig, envConfig)
}
serviceLoader(服务层)、controllerLoader(控制器)、middlewareLoader(中间件)
serviceLoader、middlewareLoader、controllerLoader的实现思路是一样的,就不重复展示了
const path = require('path')
const { sep } = path
const glob = require('glob')
/**
* service loader
* @param {object} app Koa实例
*
* 加载所有的cservice, 可通过 `app.services.${目录}.${文件}` 来访问
*
例子:
app/service
|- auth
|- auth.js
==> app.services.auth.auth
*
*/
module.exports = (app) => {
const servicePath = path.resolve(app.bussinessPath, `.${sep}service`)
const serviceFiles = glob.sync(path.resolve(servicePath, `.${sep}**${sep}**.js`))
// 遍历所有文件目录并加载到app.services下
const services = {}
serviceFiles.forEach((serviceFile) => {
// 提取文件名称
let serviceName = path.resolve(serviceFile)
serviceName = serviceName.substring(servicePath.length + 1, serviceName.lastIndexOf('.'))
// 驼峰化
serviceName = serviceName.replace(/[_-][a-z]/ig, (match) => match.substring(1).toUpperCase())
let tempservice = services
const names = serviceName.split(sep)
for (let i = 0; i < names.length; i++) {
const name = names[i]
if (i === names.length - 1) {
const ServiceMoudle = require(path.resolve(serviceFile))(app)
tempservice[name] = new ServiceMoudle()
} else {
if (!tempservice[name]) {
tempservice[name] = {}
}
tempservice = tempservice[name]
}
}
})
app.services = services
}
routerSchemaLoader(路由检验)
将各路由的检验规则挂载到app.routerSchemas上,可用于检验路由或者参数是否符合规范
const path = require('path')
const { sep } = path
const glob = require('glob')
/**
* router-schema loader
* @param {object} app Koa实例
*
* 通过'json-schema' & 'ajv' 对API规则进行约束,配合 api-params-verify 中间件使用 实现路由参数校验。
*
例子:
app/router-schema/**.js
==> app.routerSchemas = {
'${api1}' : ${jsonSchema},
'${api2}' : ${jsonSchema},
'${api3}' : ${jsonSchema},
'${api4}' : ${jsonSchema},
}
*
*/
module.exports = (app) => {
// 读取router-schema目录下所有js文件,并加载到app.routerSchema中
const routerSchemaPath = path.resolve(app.bussinessPath, `.${sep}router-schema`)
const routerSchemaFiles = glob.sync(path.resolve(routerSchemaPath, `.${sep}**${sep}**.js`))
let routerSchemas = {}
routerSchemaFiles.forEach(file => {
console.log(file, 'routerSchemas file');
routerSchemas = {
...routerSchemas,
...require(path.resolve(file))
}
})
app.routerSchemas = routerSchemas
}
extendLoader(其他拓展功能)
将应用需要的功能挂载到app方便调用
const path = require('path')
const { sep } = path
const glob = require('glob')
/**
* extend loader
* @param {object} app Koa实例
*
* 加载所有的extend, 可通过 `app.${文件}` 来访问
*
例子:
app/extend
|- auth.js
==> app.auth
*
*/
module.exports = (app) => {
const extendPath = path.resolve(app.bussinessPath, `.${sep}extend`)
const extendFiles = glob.sync(path.resolve(extendPath, `.${sep}**${sep}**.js`))
// 遍历所有文件目录并加载到app.extends下
const extend = {}
extendFiles.forEach((extendFile) => {
// 提取文件名称
let extendName = path.resolve(extendFile)
extendName = extendName.substring(extendPath.length + 1, extendName.lastIndexOf('.'))
// 驼峰化
extendName = extendName.replace(/[_-][a-z]/ig, (match) => match.substring(1).toUpperCase())
// 过滤 app 已经存在的key
for (const key in app) {
if (key === extendName) {
console.log(`[extend load error] name:${extendName} is already exists in app`);
return
}
}
app[extendName] = require(path.resolve(extendFile))(app)
})
app.extend = extend
}
routerLoader(路由)
遍历router文件夹下的文件来注册文件中定义好的路由到koa-router
const KoaRouter = require('koa-router');
const path = require('path')
const { sep } = path
const glob = require('glob')
/**
* router loader
* @param {object} app Koa实例
*/
module.exports = (app) => {
// 找到文件路径
const routerPath = path.resolve(app.bussinessPath, `.${sep}router`)
// 实例化路由
const router = new KoaRouter()
// 注册所有路由
const routerFiles = glob.sync(path.resolve(routerPath, `.${sep}**${sep}**.js`))
routerFiles.forEach(file => {
require(path.resolve(file))(app, router)
})
// 路由兜底(保证系统的健壮性)
router.get('*', async (ctx, next) => {
ctx.status = 302
ctx.redirect(`${app?.options?.homePage ?? '/'}`)
})
// 注册路由
app.use(router.routes())
app.use(router.allowedMethods())
}
第二章 - 内核应用
对应的目录规范
| -- config
| -- config.default.js (存放默认配置)
| -- config.beta.js (存放测试环境配置)
| -- config.prod.js (存放生产环境配置)
| -- app
| -- service (存放各种服务文件)
| -- middleware (存放各种中间件文件,如本章节的签名合法性校验、参数检验、错误处理)
| -- router-schema (存放各种路由校验文件)
| -- controller (存放各种控制器文件)
| -- extend (存放各种拓展功能的文件,如本章节的日志功能)
| -- router (存放各种模块的路由文件)
| -- middleware.js (存放全局的middleware)
config相关应用代码 (配置项)
可以进行相关配置,通过app.config调用
module.exports = {
name: '何木杉皮',
}
service相关应用代码 (服务层)
可以编写相关服务代码(如调用数据库获取数据并返回),通过app.services调用对应的服务
| -- service
| -- base.js (编写服务基类)
| -- xxx.js (基于基类的各服务类,如本章节的project.js)
const superagent = require('superagent')
module.exports = (app) => class BaseService {
/**
* service基类
* 统一管理 service 公共方法
*/
constructor() {
this.app = app
this.config = app.config
this.curl = superagent
}
}
module.exports = (app) => {
const BaseService = require('./base.js')(app)
return class ProjectService extends BaseService {
async getList() {
// return await app.db.Project.find();
return [{ name: 'project1', desc: 'project1 description' }, { name: 'project2', desc: 'project2 description' }]
}
}
}
工具:
superagent: 轻量的,渐进式的 ajax api,类似于前端的axios,nodejs 第三方模块,专注于处理服务端/客户端的http请求
middleware相关应用代码 (中间件)
可以编写中间件代码(如签名合法性校验、参数校验、错误处理),在app/middleware.js中通过app.use(app.middlewares.xxx)调用
| -- middleware
| -- api-params-verify.js (编写参数校验中间件)
| -- api-sign-verify.js (编写签名合法性校验中间件)
| -- error-handler.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) => {
// 只对apiq请求进行签名验证
if (!ctx.path.startsWith('/api/')) {
return await next();
}
// 获取请求参数
const { query, body, headers } = ctx.request;
const { path, method } = ctx;
const params = ctx.params
app.logger.info(`method: ${method}, path: ${path}, body: ${JSON.stringify(body)}`)
app.logger.info(`method: ${method}, path: ${path}, query: ${JSON.stringify(query)}`)
app.logger.info(`method: ${method}, path: ${path}, params: ${JSON.stringify(params)}`)
app.logger.info(`method: ${method}, path: ${path}, headers: ${JSON.stringify(headers)}`)
let schema = null;
const routerSchemaKeys = Object.keys(app.routerSchema);
for (const key of routerSchemaKeys) {
const keys = [];
const regex = pathToRegexp(key, keys);
const match = regex.exec(path);
if (match) {
schema = app.routerSchema[key][method.toLowerCase()];
break;
}
}
let valid = true
let validate
// 检验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)
}
if (!valid) {
ctx.status = 200
ctx.body = {
code: 442,
success: false,
message: `request validation failed: ${ajv.errorsText(validate.errors)}`
}
return
}
await next()
}
}
const md5 = require('md5');
/**
* api 签名合法性校验
*/
module.exports = (app) => {
return async (ctx, next) => {
// 只对apiq请求进行签名验证
if (!ctx.path.startsWith('/api/')) {
return await next();
}
const { path, method } = ctx
const { headers } = ctx.request
const { s_sign: sSign, s_t: st } = headers
const signKey = 'xxxxxxxxxx'
const signature = md5(`${signKey}_${st}`)
app.logger.info(`method: ${method}, path: ${path}, signature: ${signature}`)
if (!st || !sSign || signature !== sSign.toLowerCase() || Date.now() - st > 600000) {
ctx.status = 200
ctx.body = {
code: 445,
success: false,
message: 'signature not correct or timeout'
}
return
}
await next()
}
}
/**
* 运行时异常错误处理,兜底所有异常
* @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.includes('template not found')) {
// 页面重定向
ctx.status = 302 //临时重定向 301是永久重定向
ctx.redirect(`${app.options?.homePage}`)
return
}
const resBody = {
success: false,
code: 50000,
message: '网络异常,请稍后重置'
}
ctx.status = 200
ctx.body = resBody
}
}
}
工具:
md5: 一款加密工具
用法:
const md5 = require('md5')
const secret = md5('要加密的字符串')
ajv: 一个用于验证 JSON 数据的库,它支持 JSON Schema 规范,这里主要是用来检验参数是否有问题
routerSchema相关应用代码 (路由检验)
可以编写相规范检验代码(如检验接口参数是否符合规范),通过app.routerSchema[xxx]获取对应的规则
| -- router-schame
| -- xxx.js (路由检验规则,如本章节的project.js)
module.exports = {
'/api/project/list/:id': {
post: {
body: {
type: 'object',
properties: {
page: {
type: 'integer'
},
page_size: {
type: 'integer'
}
},
required: ['page', 'page_size']
},
params: {
type: 'object',
properties: {
id: {
type: 'string'
},
},
required: ['id']
}
}
}
}
controller相关应用代码 (控制器)
可以编写中转代码(如处理服务层返回的数据并转发给路由层,根据路由渲染页面等),通过app.controllers.xxx.xxxController调用对应的控制器
| -- controlle
| -- base.js (编写控制器基类,编写一些公共方法,如api成功或失败的处理)
| -- xxx.js (基于基类的各控制器,如本章节的view.js、project.js)
module.exports = (app) => class BaseController {
/**
* controller基类
* 统一管理 controller 公共方法
*/
constructor() {
this.app = app
this.config = app.config
this.services = app.services
}
/**
* api 成功返回
* @param {object} ctx 上下文对象
* @param {object} data 返回数据
* @param {string} message 提示信息
* @param {number} metaData 附加数据
*/
success(ctx, data, message = '操作成功', metaData = null) {
ctx.status = 200
ctx.body = {
success: true,
data,
message,
metaData
}
}
/**
* api 失败返回
* @param {object} ctx 上下文对象
* @param {string} message 提示信息
* @param {number} code 错误码
*/
fail(ctx, message = '操作失败', code) {
ctx.status = 200
ctx.body = {
success: false,
message,
code
}
}
}
module.exports = (app) => {
const BaseController = require('./base')(app)
return class ProjectController extends BaseController {
/**
* 获取数据列表
* @param {object} ctx 上下文对象
*/
async getList(ctx) {
const { project: ProjectService } = this.services
const res = await ProjectService.getList()
this.success(ctx, res, '获取成功', null)
}
}
}
module.exports = (app) => {
return class ViewController {
/**
* 渲染页面
* @param {object} ctx 上下文对象
*/
async renderPage(ctx) {
await ctx.render(`output/entry.${ctx.params.page}`, {
title: app.options.name,
env: app.env.get(),
user: app.config.name,
});
}
}
}
extend相关应用代码 (其他拓展功能)
可以编写拓展应用功能的代码(如日志输出),通过app.xxxAaa使用相关功能
| -- extend
| -- logger.js (编写控制器基类,编写一些公共方法,如api成功或失败的处理)
const log4js = require('log4js');
/**
* 日志工具
* 外部调用 app.logger.log / app.logger.error / app.logger.info
*/
module.exports = (app) => {
let logger
if (app.env.isLocal()) {
logger = console
} else {
log4js.configure({
appenders: {
console: { type: 'console' },
// 日志文件切分
dateFile: {
type: 'dateFile',
filename: './logs/application.log',
pattern: '.yyyy-MM-dd',
},
},
categories: {
default: { appenders: ['console', 'dateFile'], level: 'trace' }
}
})
logger = log4js.getLogger()
}
return logger
}
工具:
log4js: log4js是一个Node.js 日志记录库,它允许你在 Node.js 应用程序中轻松地实现灵活、高效的日志记录
了解更多: zhuanlan.zhihu.com/p/22110802
router相关应用代码 (路由)
可以编写项目的路由代码(如文件路由、接口路由),通过 '地址+定义的接口' 访问,执行对应controller的回调
| -- router
| -- view.js (页面路由)
| -- xxx.js (业务接口,如本章节的project.js)
module.exports = (app, router) => {
const { project: projectController } = app.controllers;
router.post('/api/project/list', projectController.getList.bind(projectController));
}
module.exports = (app, router) => {
const { view: viewController } = app.controllers;
router.get('/view/:page', viewController.renderPage.bind(viewController));
}
middleware.js (存放全局的middleware)
给app实例挂载上app.middlewares的各种中间件,同时挂载服务需要的一些全局中间件
const path = require('path')
module.exports = (app) => {
// 配置静态文件根目录
app.use(require('koa-static')(path.resolve(process.cwd(), './app/public')))
// 配置模板引擎
const koaNunjucks = require('koa-nunjucks-2')
app.use(koaNunjucks({
ext: 'html',
path: path.resolve(process.cwd(), './app/public'),
nunjucksConfig: {
noCache: true,
trimBlocks: true
}
}))
// 引入 ctx.body 解析中间件
const bodyParser = require('koa-bodyparser')
app.use(bodyParser({
formLimit: '1000mb',
enableTypes: ['json', 'form', 'text']
}))
// 引入异常捕获中间件
app.use(app.middlewares.errorHandler)
// API 签名合法性校验
app.use(app.middlewares.apiSignVerify)
// API 参数校验
app.use(app.middlewares.apiParamsVerify)
}
工具:
koa-nunjucks-2: 轻量级的 Koa 中间件,用于集成 Nunjucks 模板引擎,提供了一个render方法,可以直接在 Koa 的上下文(ctx)中使用,用于渲染模板
koa-bodyparser: 一个用于解析 HTTP 请求主体(body)的中间件,适用于 Koa.js 框架,支持解析 JSON、表单 (form)和文本(text)类型的请求体
以上工具的具体用法可自行查找