├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── alias.js ├── babel.js ├── check.js ├── config.js ├── controller.js ├── core ├── config.js ├── controller.js ├── plugin.js ├── responder.js ├── router.js └── service.js ├── docs ├── API.md ├── README.md ├── configuration.md ├── controller.md ├── guide.md ├── installation.md ├── plugin.md ├── router.md └── service.md ├── example ├── config │ └── default.js ├── index.js ├── public │ └── index.html └── server │ ├── controllers │ └── home.js │ └── templates │ └── home.jade ├── import.js ├── index.js ├── model.js ├── package.json ├── plugins ├── cookie.js ├── database.js ├── defaultHandler.js ├── errorHandler.js ├── logger.js ├── mongodb.js ├── oss.js ├── render.js ├── sentry.js └── static.js ├── react.js ├── render.js ├── renders ├── default.js ├── ejs.js ├── handlebars.js ├── jade.js └── nunjucks.js ├── response.js ├── router.js ├── services ├── accepts.js ├── body.js ├── cookies.js ├── headers.js ├── ip.js ├── params.js └── query.js ├── start.js ├── test └── index.js └── worker.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: song940 4 | patreon: song940 5 | open_collective: song940 6 | ko_fi: song940 7 | tidelift: npm/kanary 8 | custom: https://git.io/fjRcB 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.log 3 | yarn.lock 4 | 5 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | *.log 3 | yarn.lock 4 | 5 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 lsong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Kanary 2 | 3 | > The next full-featured javascript frameworks. 4 | 5 | [](https://npmjs.org/kanary) 6 | 7 | ### Installation 8 | 9 | ```bash 10 | $ npm install kanary 11 | ``` 12 | 13 | ### Example 14 | 15 | ```js 16 | require('kanary/start'); 17 | ``` 18 | 19 | ### Documents 20 | 21 | [docs](./docs) 22 | 23 | ### Benchmarks 24 | 25 | MacBook Pro (Retina, 15-inch, Mid 2015) 2.2 GHz Intel Core i7 / 16 GB 1600 MHz DDR3 26 | 27 | | Framework | Version | Router | Requests\/s | 28 | | :--- | --: | :-: | --: | 29 | | http | 10.13.0 | ✗ | 32534.93 | 30 | | express | 4.16.4 | ✓ | 13088.20 | 31 | | koa | 2.6.2 | ✗ | 22540.10 | 32 | | kelp | 2.0.1 | ✗ | 30707.83 | 33 | | kanary | 2.0.0 | ✓ | 22689.01 | 34 | | fastify | 1.13.0 | ✓ | 31248.66 | 35 | 36 | https://github.com/song940/kanary-benchmarks 37 | 38 | ### Contributing 39 | - Fork this Repo first 40 | - Clone your Repo 41 | - Install dependencies by `$ npm install` 42 | - Checkout a feature branch 43 | - Feel free to add your features 44 | - Make sure your features are fully tested 45 | - Publish your local branch, Open a pull request 46 | - Enjoy hacking <3 47 | 48 | ### MIT 49 | 50 | This work is licensed under the [MIT license](./LICENSE). 51 | 52 | --- 53 | -------------------------------------------------------------------------------- /alias.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | const { Module } = require('module'); 4 | const config = require('./config'); 5 | 6 | const findPath = Module._findPath; 7 | Module._findPath = (request, paths, isMain) => { 8 | for (const key in config.path) { 9 | const prefix = `@${key}/`; 10 | if (request.startsWith(prefix)) { 11 | request = request.replace(prefix, ''); 12 | request = path.join(config.path[key], request); 13 | break; 14 | } 15 | } 16 | return findPath(request, paths, isMain); 17 | }; -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | babelrc: false, 3 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 4 | presets: [ 5 | '@babel/preset-react', 6 | '@babel/preset-typescript' 7 | ], 8 | plugins: [ 9 | ['@babel/plugin-transform-modules-commonjs'], 10 | ["@babel/plugin-proposal-decorators", { legacy: true }], 11 | ] 12 | }); -------------------------------------------------------------------------------- /check.js: -------------------------------------------------------------------------------- 1 | // npm engines seems to be broken since npm v3.6.0. 2 | // https://github.com/npm/npm/issues/12486 3 | const semver = require('semver'); 4 | const { name, engines = {} } = require('./package'); 5 | // https://docs.npmjs.com/files/package.json#engines 6 | Object.keys(engines).forEach(engine => { 7 | const current = process.versions[engine]; 8 | const range = engines[engine]; 9 | if (!semver.satisfies(current, range)) { 10 | console.error(`${name} requires ${engine} version ${range}, but current is ${current}`); 11 | process.exit(1); 12 | } 13 | }); -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const config = require('kelp-config/config'); 3 | 4 | const root = process.cwd(); 5 | const $ = p => join(root, p); 6 | 7 | const defaults = { 8 | port: 3000, 9 | path: { 10 | root, 11 | public: $('public'), 12 | client: $('client'), 13 | server: $('server'), 14 | models: $('server/models'), 15 | plugins: $('server/plugins'), 16 | services: $('server/services'), 17 | templates: $('server/templates'), 18 | controllers: $('server/controllers'), 19 | }, 20 | routes: [], 21 | plugins: [], 22 | }; 23 | 24 | module.exports = config(defaults); 25 | -------------------------------------------------------------------------------- /controller.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | 3 | class Controller extends EventEmitter { 4 | 5 | } 6 | 7 | module.exports = Controller; -------------------------------------------------------------------------------- /core/config.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | 3 | module.exports = app => { 4 | app.config = config; 5 | return (req, res, next) => next(); 6 | }; -------------------------------------------------------------------------------- /core/controller.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const Controller = require('../controller'); 3 | 4 | const convert = actions => { 5 | if (!Array.isArray(actions)) 6 | return convert([actions]); 7 | return actions.reduce((controller, item, i) => { 8 | if (typeof item === 'object' && Object.keys(item).length === 0) 9 | throw new Error(`[kelp-next] controller is empty`); 10 | const { method = '*', url: path } = item; 11 | const action = 12 | item.name || 13 | path || 14 | item.controller && item.controller.name || 15 | `action-${i}`; 16 | // obj like controller 17 | controller[action] = item.controller ? item.controller : item; 18 | controller.__routes.push({ method, path, action }); 19 | return controller; 20 | }, { __routes: [] }); 21 | }; 22 | 23 | module.exports = app => { 24 | const { controllers: dir } = app.config.path; 25 | app.registerController = (name, ctrl) => { 26 | app.controllers = app.controllers || {}; 27 | if (Array.isArray(ctrl.__routes)) { 28 | ctrl.__routes.forEach(route => { 29 | // because decorator can NOT get their filename name. 30 | route.controller = name; 31 | // when `route.path` is undefined, defaults to 32 | // controller name and action name 33 | if (route.path === undefined) { 34 | route.path = `/${route.controller}/${route.action}`; 35 | } 36 | // console.log(route.path); 37 | return app.registerRoute(route); 38 | }); 39 | } 40 | return app.controllers[name] = ctrl; 41 | }; 42 | // controllers 43 | app.loadController = filename => { 44 | var controller = app.import(filename); 45 | if (controller.__proto__ === Controller) { 46 | controller = new controller(app); 47 | } else if (typeof controller === 'function') { 48 | controller = convert(controller); 49 | } else if (Array.isArray(controller)) { 50 | controller = convert(controller); 51 | } else if (typeof controller === 'object') { 52 | controller = convert(controller); 53 | } else { 54 | throw new TypeError(`[kelp-next] controller must be a function at ${filename}`); 55 | } 56 | const name = filename.replace(`${dir}/`, '').replace(/\..+$/, ''); 57 | return app.registerController(name, controller); 58 | }; 59 | glob(`${dir}/**/*.{js,ts}`, (err, files) => { 60 | if (err) return console.error('[kelp-next] load controller error:', err); 61 | files.forEach(filename => app.loadController(filename)); 62 | }); 63 | return async (req, res, next) => { 64 | if (!req.route) return next(); 65 | const { controller: c, action: a } = req.route; 66 | const controller = app.controllers[c]; 67 | if (!controller) throw new Error(`[kelp-next] controller "${c}" not found`); 68 | const action = controller[a]; 69 | if (!action) throw new Error(`[kelp-next] action "${c}#${a}" not found`); 70 | res.scope = await res.invoke(action, controller); 71 | return next(); 72 | } 73 | }; -------------------------------------------------------------------------------- /core/plugin.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = app => { 4 | return app.middleware = app.config.plugins.map(plugin => { 5 | let name, args = []; 6 | if (typeof plugin === 'string') 7 | name = plugin; 8 | if (Array.isArray(plugin)) 9 | [name, ...args] = plugin; 10 | name && (plugin = app.import(name, [ 11 | app.config.path.plugins, 12 | path.join(__dirname, '../plugins') 13 | ])); 14 | return plugin.apply(app, [app].concat(args)); 15 | }).filter(mw => typeof mw === 'function'); 16 | }; -------------------------------------------------------------------------------- /core/responder.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = app => { 3 | return async (req, res, next) => { 4 | res.scope !== void (0) && res.send(res.scope); 5 | return next(); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /core/router.js: -------------------------------------------------------------------------------- 1 | const { METHODS } = require('http') 2 | const routing = require('routing2'); 3 | 4 | const Router = app => { 5 | const { routes: rules } = app.config; 6 | app.routes = app.routes || []; 7 | app.router = METHODS.reduce((api, method) => { 8 | // app.router.get('/').to('home', 'index'); 9 | api[method.toLowerCase()] = path => { 10 | return { 11 | to(controller, action) { 12 | return app.registerRoute({ method, path, controller, action }) 13 | } 14 | }; 15 | }; 16 | return api; 17 | }, {}); 18 | app.registerRoute = route => { 19 | if (typeof route === 'string') 20 | route = routing.parseLine(route); 21 | app.routes.push(routing.create(route)); 22 | return route; 23 | }; 24 | // routes 25 | rules.forEach(app.registerRoute.bind(app)); 26 | return async (req, res, next) => { 27 | const { status, route } = routing.find(app.routes, req); 28 | res.statusCode = status; 29 | req.route = route; 30 | await next(); 31 | }; 32 | }; 33 | 34 | module.exports = Object.assign(Router, routing); -------------------------------------------------------------------------------- /core/service.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const injector = require('inject2'); 4 | const Import = require('../import'); 5 | 6 | module.exports = app => { 7 | const { services = {} } = app.config; 8 | const { services: dir } = app.config.path; 9 | app.import = Import; 10 | app.services = services; 11 | app.registerService = (name, service) => { 12 | app.services[name] = service; 13 | return service; 14 | }; 15 | app.loadService = filename => { 16 | const name = path.basename(filename).replace(/\..+$/, ''); 17 | const service = app.import(filename); 18 | return app.registerService(name, service); 19 | }; 20 | const readdir = dir => new Promise(resolve => { 21 | fs.readdir(dir, (err, files) => { 22 | resolve(err ? [] : files.map(f => path.join(dir, f))); 23 | }); 24 | }); 25 | Promise 26 | .all([ 27 | readdir(path.join(__dirname, '../services')), 28 | readdir(dir) 29 | ]) 30 | .then(files => [].concat.apply([], files)) 31 | .then(files => files.forEach(app.loadService.bind(app))) 32 | return async (req, res, next) => { 33 | res.invoke = injector(Object.assign({ 34 | // ... 35 | }, app.services, { req, res })); 36 | return next(); 37 | }; 38 | }; -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | 2 | ## API Reference 3 | 4 | + app.registerRoute 5 | + app.registerController -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Kanary Documents 2 | 3 | Kanary 是 [@kelpjs 项目](https://github.com/kelpjs) 的集合版本,它的目标是 “简单、快速、高效”。 4 | 5 | + [Get Started](./guide.md) 6 | + [Installation](./installation.md) 7 | + [Configuration](./configuration.md) 8 | + [Router](./router.md) 9 | + [Controller](./controller.md) 10 | + [Service](./service.md) 11 | + [Plugin/Middleware](./plugin.md) 12 | 13 | --- 14 | Kanary Project @ 2019 -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | 2 | ## Configuration 3 | 4 | 配置管理可以根据不同的运行环境改变 Kanary 的默认行为 5 | 6 | 默认配置: 7 | 8 | ```js 9 | const root = process.cwd(); 10 | 11 | export default { 12 | port: 3000, 13 | path: { 14 | root, 15 | public: path.join(root, 'public'), 16 | server: path.join(root, 'server'), 17 | models: path.join(root, 'server/models'), 18 | plugins: path.join(root, 'server/plugins'), 19 | services: path.join(root, 'server/services'), 20 | templates: path.join(root, 'server/templates'), 21 | controllers: path.join(root, 'server/controllers'), 22 | }, 23 | } 24 | ``` 25 | 26 | ### 自定义配置 27 | 28 | Kanary 会根据应用运行的环境 (NODE_ENV) 读取不同的配置文件 29 | 30 | 具体可以参考 [kelp-config](https://github.com/kelpjs/kelp-config) 的实现。 -------------------------------------------------------------------------------- /docs/controller.md: -------------------------------------------------------------------------------- 1 | ## Controller 2 | 3 | Kanary 中的 控制器 对于服务器来说非常重要,它是我们处理请求的关键。 4 | 5 | 目前支持以下类型: 6 | 7 | #### Function 函数 8 | 9 | ```js 10 | export default () => { 11 | return 'hello world'; 12 | }; 13 | ``` 14 | 15 | #### Object 对象 16 | 17 | ```js 18 | export default { 19 | method: 'get', 20 | url: '/:name?', 21 | controller(params) { 22 | const { name } = params; 23 | return `hello ${name}`; 24 | } 25 | }; 26 | 27 | ``` 28 | 29 | #### Array 数组 30 | 31 | 数组中可以包含上面两种类型 (Function and/or Object) 32 | 33 | ```js 34 | export default [ 35 | () => 'hello world 1', 36 | () => 'hello world 2', 37 | ]; 38 | ``` 39 | 40 | #### Class 类 41 | 42 | ```js 43 | import Controller from 'kanary/controller'; 44 | 45 | class Home extends Controller { 46 | async index(){ 47 | return 'hello world'; // 响应 48 | } 49 | } 50 | 51 | export default Home; 52 | ``` -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | Kanary 相比目前社区中其他 Web 框架来说要更简单易用。 4 | 5 | 为了能更顺畅地进行后面的内容,我们假设你已经安装好了 [Node.js](https://nodejs.org) 并且有一定的编程基础。 6 | 7 | [📺 Watch Video Guide on YouTube](https://youtu.be/fuoSU5hJmMM) 8 | 9 | 首先我们创建一个新的项目,名字就叫做 "kanary-app" 吧 10 | 11 | ```bash 12 | ~$ mkdir kanary-app 13 | ~$ cd $_ 14 | ~$ git init 15 | ~$ npm init -y 16 | ``` 17 | 18 | 要使用 Kanary 框架,我们首先需要安装它: 19 | 20 | ```bash 21 | ~$ npm i kanary --save 22 | ``` 23 | 24 | 根据你的网络连接情况,这可能需要一点时间,不过一般很快就会安装好了。 25 | 26 | ## 使用 27 | 28 | 现在我们可以开始写一些代码来让我们的服务运行起来 29 | 30 | 创建一个文件 index.js 写入下面的代码: 31 | 32 | ```js 33 | require('kanary/start'); 34 | ``` 35 | 36 | 然后运行我们的服务器: 37 | 38 | ```bash 39 | ~$ node index.js 40 | ``` 41 | 42 | 现在打开浏览器访问 http://localhost:3000 可以看到它已经运行起来了。 43 | 44 | 虽然它现在看起来有点简陋,但是它确实运行起来了。 45 | 46 | ## 小结 47 | 48 | 这篇文章中我们介绍了如何创建一个 Node.js 项目,并安装 Kanary 框架。 49 | 50 | 与社区中大部分框架不同,Kanary 不需要复杂的设置、也不需要编写很多代码,只需要一行就可以使我们的服务运行。 51 | 52 | 这种设计思想会在 Kanary 框架中大量体现,后面你会 [了解更多](./README.md) 。 -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Kanary 安装与大多数使用 NPM 托管代码的 Package 一样,只需要: 4 | 5 | ```bash 6 | ~$ npm i kanary --save 7 | ~$ # if you have "Yarn" ... 8 | ~$ yarn add kanary 9 | ``` -------------------------------------------------------------------------------- /docs/plugin.md: -------------------------------------------------------------------------------- 1 | ## Plugin / Middleware 2 | 3 | 插件 / 中间件 是 Kanary 中强大的扩展能力的体现,在插件中你可以对 Kanary 做扩展动作,也可以处理请求。 4 | 5 | 在 Kanary 中已经内置了大量 插件 / 中间件 可以做到开箱即用 6 | 7 | 插件会在项目启动时加载并执行一次 8 | 9 | 中间件会在每次请求时触发,并且有能力调度之后的中间件运行。 10 | 11 | ```js 12 | module.exports = (app, options) => { 13 | // plugin phase 14 | return async (req, res, next) => { 15 | // middleware phase 16 | await next(); 17 | }; 18 | }; 19 | ``` 20 | 21 | ### 如何编写插件 / 中间件 22 | 23 | ```js 24 | const AWS = require('aws-sdk'); 25 | 26 | module.exports = (app, options) => { 27 | const { endpoint, accessKey, secretKey } = options; 28 | app.s3 = new AWS.S3({ 29 | endpoint, 30 | accessKeyId: accessKey, 31 | secretAccessKey: secretKey 32 | }); 33 | return (req, res, next) => { 34 | req.s3 = app.s3; 35 | res.s3 = app.s3; 36 | return next(); 37 | }; 38 | }; 39 | ``` 40 | 41 | ### 内置插件 / 中间件 42 | 43 | + cookie 44 | + logger 45 | + render 46 | + static 47 | + database 48 | + mongodb -------------------------------------------------------------------------------- /docs/router.md: -------------------------------------------------------------------------------- 1 | ## Router 2 | 3 | 在 Kanary 中,所有路由都定义在 *app.routes* 数组中,当服务器收到请求时,**路由模块** 会查询请求匹配的路由规则并根据路由的描述调度 **控制器** 。 4 | 5 | 在 Kanary 中定义路由有几种方法: 6 | 7 | 1. 使用配置文件定义 8 | 2. 使用路由修饰器定义 9 | 3. 使用 [Kanary API](./API.md) 定义 10 | 11 | 下面我们依次介绍: 12 | 13 | ### 使用配置文件定义路由规则 14 | 15 | ```js 16 | exports.routes = [ 17 | 'get / => home#index' 18 | ]; 19 | ``` 20 | 21 | ### 使用修饰器定义路由 22 | 23 | ```js 24 | import { get } from 'kanary/router'; 25 | 26 | class Home { 27 | @get('/hi') 28 | async index(){ 29 | return 'hello world'; 30 | } 31 | } 32 | 33 | export default Home; 34 | ``` 35 | ### 使用 Kanary API 定义路由 36 | 37 | ```js 38 | module.exports = app => { 39 | app.router.get('/').to('home', 'index'); 40 | } 41 | ``` -------------------------------------------------------------------------------- /docs/service.md: -------------------------------------------------------------------------------- 1 | ## Service Injecting 2 | 3 | 依赖注入服务是 Kanary 中最有特色的功能之一,它能够让 [控制器](./controller.md) 处理请求时更加方便、高效。 4 | 5 | Kanary 中 Service 服务必须定义为 [Function 函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function), 该函数(即 "Service 服务") 可以依赖注入其他服务。并且返回任意类型的对象作为输出。 6 | 7 | ```js 8 | 9 | // ./server/services/demo.js 10 | 11 | module.exports = (req /* 注入 req 服务 */) => { 12 | // ... 13 | // 可以返回任意类型 14 | return {}; 15 | return 1; 16 | return () => {}; 17 | }; 18 | ``` 19 | 20 | 在 Controller 控制器中使用 Injecting 依赖注入 21 | 22 | ```js 23 | import Controller from 'kanary/controller'; 24 | 25 | class Home extends Controller { 26 | async index(demo){ 27 | return `hello ${demo}`; // 响应 28 | } 29 | } 30 | 31 | export default Home; 32 | ``` 33 | 34 | ### 内置的 Service 服务 35 | 36 | + req 37 | + res 38 | + query 39 | + params 40 | + headers 41 | + cookies 42 | + body 43 | + accepts -------------------------------------------------------------------------------- /example/config/default.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | routes: [ 3 | 'get /:name? => home#index' 4 | ], 5 | plugins: [ 6 | 'errorHandler', 7 | 'static', 8 | 'defaultHandler' 9 | ] 10 | }; -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | require('../start'); -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 |