Koa800

基于koa框架, 可扩展、可配置的中间件集合, 以及快速定制项目结构的脚手架工具

Posted on April 6, 2017

开源版本: https://github.com/zhongfox/koa800

1. 要解决的问题

  1. 脚手架文件太多:
    • 干扰业务代码
    • 无法方便的进行更新和升级
    • 随着时间推移, 项目结构和脚手架文件在不同项目中逐渐变得不统一
  2. 中间件的管理:
    • 相同的中间件需要在不同项目中去引用和配置, 重复的工作量, 重复的代码(违反DRY)
    • 不便于统一的更新和升级

2. 背景

微服务是当前比较流行的系统架构模式, 各个应用的专注于提供特定的服务. 在框架选择上, 轻量级、 支持中间件选配的koa框架是一个不错的选择.

koa 的使用非常灵活便捷, 它没有提供模板文件, 也没有要求项目结构标准, 这让开发者有很大的自由度; 不过对于同一个团队下的多个koa应用来说, 这会是一个问题: 多个项目在脚手架结构上的不统一, 将会对跨项目维护、中间件升级等造成麻烦.

解决以上问题, 有的团队会采用代码规范/项目结构规范来作为约束, 对于生命周期较长的多应用系统, 规范不一定有很好的效果, 规范毕竟是靠人来执行.

koa800提供另外一种方案: 将各项目的结构标准、共用的中间件依赖等, 提取到npm包中, 对于那些部分项目使用的中间件, 也将其提取到npm包中, 使用者可以对这部分特性进行简单的声明选择. 这样一来, 项目结构和中间件都在koa800中进行控制和装配, 实现了强制的项目标准.

同时, koa800还提供了项目脚手架的创建和更新功能.


3. Koa800 介绍

koa800提供了2个功能:

  1. Scaffold Generator: 创建、更新项目脚手架, 可配置需要特性. 发生在执行setup脚本时.
  2. Runtime Koa Wrapper: 统一管理项目中间件, 可配置需要特性. 发生在项目运行时.

Scaffold Generator和Koa Wrapper都是通过Feature进行管理.


3.1 Scaffold Generator

让我们用koa800来初始化一个新项目:

% mkdir koa800_demo % cd koa800_demo

然后用npm init创建项目package.json

现在你需要做的是将koa800加入项目依赖:

% npm install koa800 –save

现在该项目的package.json大概是这个样子的:

{
  "name": "koa800_demo",
  "version": "1.0.0",
  "dependencies": {
    "koa800": "^1.0.0"
  }
}

执行koa800提供的setup脚本更新项目脚手架:

% ./node_modules/.bin/setup

执行完毕后, 将创建类似如下的项目结构:

├── app/
│   └── controllers/
├── app.js
├── bin/
│   └── www
├── config/
│   ├── environments/
│   │   ├── development.js
│   │   └── production.js
│   ├── errors.js
│   ├── routes/
│   │   └── index.js
│   └── settings.js
└── package.json

此时查看package.json, 内容也有更新, 内容大概如下:

{
  "name": "koa800_demo",
  "version": "1.0.0",
  "dependencies": {
    "koa800": "^1.0.0"
  },
  "scripts": {
    "start": "node ./bin/www",
    "c": "node ./node_modules/.bin/c",
    "setup": "node ./node_modules/.bin/setup"
  },
  "koa800Config": {
    "eslint": false,
    "jsonp": false,
    "memcached": false,
    "redis": false,
    "prepare_commit_msg": false,
    "auto_routes": false
  }
}

其中, koa800Config是用于声明可选feature的属性, 默认情况下, 这里列出了所有可选的features, 但设为了false, 表示不启用, 用户可以根据项目实际需要对某些feature进行开启.

在每次修改了koa800Config中的feature设置后, 需要再次执行如上的setup脚本, setup脚本会根据koa800Config中的配置去调整当前项目的脚手架和中间件.


3.2 Runtime Koa Wrapper

在上一小节中, 通过setup生成的项目入口文件是app.js, 该文件内容如下:

'use strict';
let app = require('koa800')();
module.exports = app;

可以看到, 项目本身没有直接依赖和使用koa, 而是使用的koa800, koa800作为一个包裹器, 引用了koa, 同时引入并正确配置了其他一些依赖的中间件. 同脚手架生成器一样, 用户也可以在package.json的koa800Config属性中去配置需要的依赖feature.


4. 约定

koa800 的主要目的是减少项目之间的重复的项目结构代码, 保证项目的规范和统一, 并能在今后方便的进行整体调整. koa800想要避免的重复代码包括:

  • 项目组织和加载的代码
  • 对相同中间件的引用和配置的代码
  • CI类, 工具类, 编译打包部署等代码

koa800创建的脚手架有如下约定:

├── app/
│   └── controllers/       控制器代码
├── app.js                 app的入口文件
├── bin/
│   └── www                项目启动文件
├── config/                项目配置文件目录
│   ├── environments/          具体环境的个性配置文件目录
│   │   ├── development.js
│   │   └── production.js
│   ├── errors.js
│   ├── routes/            路由目录
│   │   ├── index.js       任意域名路由
│   │   └── search.js      子域名路由, 子域名和文件名一致
│   └── settings.js        基础配置文件, 低优先级
│   └── settings.local.js  全局特殊配置文件, 高优先级
└── package.json

这些约定是由各feature的setup流程进行控制的.


5. Feature

Feature 是脚手架构建和中间件装配的基本单元, Feature分为Required和Optional2个类别, 目前的Feature如下:

Required Features 描述 Scaffold Generator Runtime
base 基础特性 生成项目基础脚手架, 引入基础依赖 引入并配置项目基本的中间件
controllers 控制器 生成控制器目录app/controllers/ 加载控制器, 挂载到app.controllers
errors 错误列表 生成错误列表模块config/errors.js 加载错误列表, 挂载到app.errors
settings 项目配置管理 生成多级配置管理文件 按照优先级解析settings, 挂载到app.settings
start_hooks 应用启动的前置注册管理 不涉及 处理app.start的前置操作
Optional Features 描述 Scaffold Generator Runtime
auto_routes 自动路由配置 引入路由和子域名的依赖 加载config/routes/中的路由, 并自动匹配子域名
eslint 引入eslint 将.git/hook/pre-commit链接到指定脚本 不涉及
jsonp 引入jsonp支持 引入jsonp依赖, 并提供check referer 加载jsonp中间件
memcached 引入memcached 引入memcached依赖 将memcached链接挂载到app.settings.memcached
prepare_commit_msg 在git commit消息中自动集成分支信息 将.git/hooks/prepare-commit-ms链接到指定脚本 不涉及
redis 引入redis 引入redis依赖 将redis链接挂载到app.redis

以下是部分feature说明:


base

feature base 按照约定创建了项目基本的结构, 同时提供了一些基础的API和脚本:

API

  • app.requireModule(relativePathFromRoot): 参数是相对于app.root的相对路径, 这个API让用户不用考虑当前脚本的位置和目标模块的层级关系, 非常方便.

  • app.isDevelopment() -> bool app.isProduction() -> bool 用于判断当前执行环境的语法糖.

  • app.netFT: 网络请求降级API

脚本

  • npm run setup: 按照当前配置的koa800Config更新项目脚手架
  • npm run c: 提供了koa800 console功能, 方便调试

console

console 演示

除此之外, base feature还配置了日志, 设置了favicon.ico, 处理了Post 参数等问题.


eslint

在node.js 项目中, 我们引入eslint作为代码质量控制工具. 并在git commit 时, 利用 pre-commit hook 对修改的文件自动执行eslint, 强制保证了入库代码的质量.

在 pre-commit 执行eslint检测到代码错误时, 可以配置要求阻止当前commit(默认), 也可以配置仅作为警告输出, commit仍然成功.

eslint

eslint 演示

eslint 支持的配置项:

选项 含义
false 关闭 auto eslint
true/error 开启 auto eslint, 如eslint失败, 将阻止 git commit
warning 开启 auto eslint, 如eslint失败, 仅输出错误消息, 允许 git commit

prepare_commit_msg

对多个开发共同维护的git仓库, 分析某个commit的作者、来源分支或者来源工单, 是一个常见的场景, 不过git commit 中只能查到作者信息, 不能很方便的追踪到来源分支. 在某些团队中是要求在commit message中手动加上当前分支名, 以满足未来分析需要, 这个工作其实可以通过git prepare-commit-msg 来自动实现.

当在koa800中开启prepare_commit_msg选项后, 每次用户commit代码, 会自动把当前分支名信息记录到commit message中.

prepare_commit_msg

prepare_commit_msg 演示

prepare_commit_msg 支持的配置项:

选项 含义
false 关闭 prepare_commit_msg
true 开启 prepare_commit_msg

settings

配置管理是项目工程结构中重要一环, koa800 的 settings feature 为项目提供了多级的配置管理:

  1. 第一优先级:

    config/settings.local.js

    该文件不应该在git版本库, 用于开发测试或者部署环境的特殊配置, 优先级最高.

  2. 第二优先级:

    config/environments/development.jsconfig/environments/production.js

    koa800会根据env的值去加载config/environments/中的同名js配置文件, 该目录中的配置用于不同运行环境的差异化配置.

    环境配置文件应该放入版本库.

  3. 最低优先级:

    config/settings.js

    该文件包含项目基础配置, 优先级最低, 应该放入git版本库.

settings属于Required feature, 无需在koa800Conifg中配置.


start_hooks

某些应用在启动web 服务前, 需要处理一些前置操作, 如服务注册, 配置同步, 获取静态资源hash值等. 如果任由这些需求在各个微服务中处理, 会有大量的重复代码. 该feature 提供的API app.beforeStart 允许用户在app.start前注册前置操作:

app.beforeStart(action)参数action 可以是以下值:

  • 一个 Promise 实例, koa800会保证此promise resolve后, 才启动web 服务

  • 一个 function, 且该function的返回值是一个Promise实例, koa800会在启动web服务前执行该function, 并且会保证此promise resolve后, 才启动web 服务.

  • 一个 function, 返回值并不是Promise实例, koa800会在启动web服务前执行该function.

app.beforeStart 允许级联调用, 如:

app.beforeStart(syncConfiguration).beforeStart(serviceDiscovery).beforeStart(serviceRegistration)

start_hooks属于Required feature, 无需在koa800Conifg中配置.


auto_routes

web应用的http 路由通常由两部分组成: 子域名匹配和path匹配, koa-sub-domainkoa-router 可以实现以上功能.

如果不使用koa800, 用户需要自行配置子域名路由, 如:

app.use(app.requireModule('config/routes').routes());                          // 泛域名路由
app.use(subdomain('last', app.requireModule('config/routes/last').routes()));  // last 子域名路由
app.use(subdomain('new', app.requireModule('config/routes/new').routes()));    // new 子域名路由
app.use(subdomain('top', app.requireModule('config/routes/top').routes()));    // top 子域名路由

koa800提倡约定大于配置, 只要代码按照约定组织, koa800 会完整这些重复而固定的工作, 减少用户需要编写的代码行.

在koa800, 只要将路由文件按照约定放入config/routes中, koa800 会自动加载路由, 并按照文件名识别子域名:

config
└── routes
    ├── index.js  泛域名路由
    ├── last.js   last 子域名路由
    ├── new.js    new 子域名路由
    └── top.js    top 子域名路由

auto_routes 支持的配置项:

选项 含义
false 关闭 auto_routes
true 开启 auto_routes

jsonp

koa-safe-jsonp 提供了koa jsonp支持, koa800将其做了简单的封装.

jsonp存在较高的安全风险, referer 检查是jsnop风险防范的常见手段, koa800的jsonp feature 提供了referer 检测的验证action, 该filter位于app/controllers/filters/check_referer.js

使用方式:

在路由中加上前置filter: filters.checkReferer:

router.get(‘/user_info’, app.controllers.filters.checkReferer, app.controllers.user.info);

可以在app/controllers/filters/check_referer.js中配置特定的refererMatcher.

jsonp 支持的配置项:

选项 含义
false 关闭 jsonp
true 开启 jsonp
配置对象 开启 jsonp, 并将配置对象传递给koa-safe-jsonp

配置对象中可以配置callbacklimit, 详见koa-safe-jsonp

jsonp(app, {
  callback: '_callback', // default is 'callback'
  limit: 50, // max callback name string length, default is 512
});

6. 开发者指南

koa800提供的所有特性单元是Feature, 开发者可以通过编写Feature按需扩展koa800的功能.

在koa800中Feature类定义如下:

class Feature {
  constructor(properties) {
    this.run          = properties.run;
    this.setup        = properties.setup;
    this.packageJson  = properties.packageJson;
    this.scaffold     = properties.scaffold;
    this.dependencies = properties.dependencies;
  }
}

属性说明:

  • run: 该feature在运行时对app的增强操作
  • setup: 该feature在脚手架更新过程中的操作, 该属性期望是一个生成器函数
  • packageJson: 该feature在脚手架更新过程中, 对项目package.json的补充, 比如引入scripts, dependencies等
  • scaffold: 该feature在脚手架更新过程需要同步的文件列表
  • dependencies: 该feature依赖的其他feature

Scaffold Generator 各feature的处理流程:

  1. 处理dependencies features
  2. 更新scaffold文件列表
  3. 更新packageJson内容
  4. 执行setup

运行时, 各feature的执行流程:

  1. 依次执行feature.run

单元测试

目前koa800测试覆盖率>80%, 开发者在新增、更新feature时, 务必补充单元测试.