Skip to content

Commit 4c88776

Browse files
committed
feat: init project
1 parent 376abcc commit 4c88776

22 files changed

+1036
-1
lines changed

.eslintrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
extends: 'eslint-config-bce',
3+
rules: {
4+
class-methods-use-this: 0
5+
}
6+
}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
yarn.lock

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020 大鱼
3+
Copyright (c) 2020 [email protected]
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

lib/components/core/errorHandler.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* 错误处理中间件
3+
*/
4+
5+
const util = require('util');
6+
7+
module.exports = function createErrorHandler(config) {
8+
const development = (config || {}).env === 'development';
9+
return function errorHandler(ctx, next) {
10+
return next().catch(e => {
11+
const status = e.status || 500;
12+
// 只处理500及以上的异常
13+
if (status < 500) {
14+
throw e;
15+
}
16+
17+
global.console.error(e);
18+
ctx.app.emit('error', e, ctx);
19+
ctx.status = status;
20+
21+
const message = development ?
22+
'<pre>' + util.inspect(e) + '\n' + (e.stack || '') + '</pre>' :
23+
'Internel Server Error';
24+
25+
if (ctx.is('application/json') ||
26+
!ctx.accepts('html') && ctx.accepts('application/json')) {
27+
ctx.body = { success: false, message };
28+
} else {
29+
ctx.body = message;
30+
}
31+
});
32+
};
33+
};

lib/components/core/index.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const assert = require('assert');
2+
const { sortBy } = require('ramda');
3+
const debug = require('debug')('avada:core');
4+
const createErrorHandler = require('./errorHandler');
5+
6+
7+
module.exports = function CoreComponent(app, settings) {
8+
setupConfig(app, settings);
9+
10+
const starter = hookUse(app);
11+
12+
const errorHandler = createErrorHandler(app);
13+
app.use(errorHandler, { level: 3 });
14+
15+
return starter;
16+
};
17+
18+
19+
function setupConfig(app, settings) {
20+
const appConfig = {
21+
env: settings.env,
22+
...settings.app
23+
};
24+
25+
const props = {
26+
get() {
27+
return appConfig;
28+
}
29+
};
30+
31+
Object.defineProperty(app, 'config', props);
32+
Object.defineProperty(app.context, 'config', props);
33+
}
34+
35+
36+
function hookUse(app) {
37+
const list = [];
38+
39+
const use = app.use;
40+
app.use = (middleware, options) => {
41+
assert(
42+
typeof middleware === 'function',
43+
'middleware should be typeof function'
44+
);
45+
46+
options = { level: 3, ...options };
47+
list.push({ middleware, options });
48+
};
49+
50+
return () => {
51+
const sorted = sortBy(item => item.options.level, list);
52+
const oriUse = fn => use.call(app, fn);
53+
useMiddlewares(oriUse, sorted);
54+
};
55+
}
56+
57+
58+
function useMiddlewares(use, list) {
59+
for (const item of list) {
60+
const mw = item.middleware;
61+
const opts = item.options;
62+
debug('use %s, %o', mw.$name || mw.name, opts);
63+
use(mw);
64+
}
65+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const debug = require('debug')('avada:navigator/Controller');
2+
3+
module.exports = function Controller() {
4+
const map = new Map();
5+
6+
const add = (name, controller) => {
7+
if (map.has(name)) {
8+
global.console.error(`controller already exists: ${name}`);
9+
return;
10+
}
11+
debug('add controller: %s', name);
12+
map.set(name, controller);
13+
};
14+
15+
const has = (module, action) => {
16+
const controller = map.get(module);
17+
return controller && typeof controller[action] === 'function';
18+
};
19+
20+
const invoke = (module, action, ctx) => {
21+
const controller = map.get(module);
22+
debug('invoke %s[%s]', module, action);
23+
return controller[action](ctx);
24+
};
25+
26+
return { add, has, invoke };
27+
};

lib/components/navigator/index.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const fs = require('fs');
2+
const pathUtil = require('path');
3+
const loadModule = require('../../utils/loadModule');
4+
const Controller = require('./Controller');
5+
6+
7+
module.exports = function NavigatorComponent(app, settings) {
8+
const appRoot = settings.applicationRoot;
9+
const config = settings.navigator || {};
10+
const controllerRoot = config.controllerRoot || pathUtil.join(appRoot, 'controllers');
11+
12+
const controller = new Controller();
13+
app.controller = controller.add.bind(controller);
14+
15+
setupControllers(controller, { controllerRoot });
16+
17+
const navigator = createNavigator({ controller });
18+
app.use(navigator, { level: 4 });
19+
};
20+
21+
22+
function setupControllers(controller, { controllerRoot }) {
23+
const list = fs.readdirSync(controllerRoot);
24+
for (const name of list) {
25+
const path = pathUtil.join(controllerRoot, name);
26+
const mod = loadModule(path);
27+
if (mod) {
28+
controller.add(name, mod);
29+
}
30+
}
31+
}
32+
33+
34+
function createNavigator({ controller }) {
35+
return function Navigator(ctx, next) {
36+
const { route } = ctx;
37+
if (!route) {
38+
return next();
39+
}
40+
41+
if (!controller.has(route.module, route.action)) {
42+
global.console.error(`action not found: ${route.module}[${route.action}]`);
43+
return next();
44+
}
45+
46+
const query = {
47+
...route.query,
48+
...ctx.query
49+
};
50+
ctx.originalQuery = ctx.query;
51+
ctx.query = query;
52+
53+
return controller.invoke(route.module, route.action, ctx);
54+
};
55+
}

lib/components/router/Router.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const debug = require('debug')('avada:Router');
2+
const pathToRegexp = require('../../utils/pathToRegexp');
3+
4+
5+
const rNum = /^\d+$/;
6+
7+
module.exports = function Router() {
8+
const routes = [];
9+
10+
/**
11+
* 添加路由规则
12+
*
13+
* @param {String|RegExp} pattern - 用于url匹配的规则
14+
* 目前路由匹配是使用`path-to-regexp`这个库来实现的
15+
* 它可以很方便地将路径样式的字符串转换成正则表达式
16+
*
17+
* @param {String|Object} rule - 定位的规则
18+
* @param {Object} options - 额外的选项
19+
* - method {String|Array} 只允许指定method
20+
*/
21+
const add = (pattern, rule, options = {}) => {
22+
const keys = [];
23+
const regexp = pathToRegexp(pattern, keys);
24+
const queries = keys.map((key, index) => {
25+
const name = key.name;
26+
return rNum.test(name) ? null : { index: index + 1, name };
27+
}).filter(v => v);
28+
29+
rule = typeof rule === 'string' ? parseRule(rule) : rule;
30+
const item = { regexp, queries, rule, options };
31+
debug('add %o', item);
32+
routes.push(item);
33+
};
34+
35+
/**
36+
* 将path路由成RouteInfo
37+
*
38+
* @param {String} path - 路径
39+
* @param {Object} request - 选项
40+
* - method 当前请求method
41+
*
42+
* @return {Object|null} - 路由信息
43+
* - module {String}
44+
* - action {String}
45+
* - query {Object}
46+
*/
47+
const route = (path, request) => {
48+
if (routes.length === 0) {
49+
return null;
50+
}
51+
for (const item of routes) {
52+
// 对规则进行一次正则匹配,成功后将不对后续的路由进行匹配
53+
if (verifyRequest(item, request)) {
54+
const match = item.regexp.exec(path);
55+
if (match) {
56+
const result = createRouteInfo(item, match);
57+
debug('route success: %o', result);
58+
return result;
59+
}
60+
}
61+
}
62+
return null;
63+
};
64+
65+
return { add, route };
66+
};
67+
68+
/*
69+
* 测试是否允许访问
70+
* - method是否匹配
71+
*/
72+
function verifyRequest(item, request) {
73+
const method = item.options.method;
74+
if (!method || !request) {
75+
return true;
76+
}
77+
78+
const current = request.method.toLowerCase();
79+
if (typeof method === 'string') {
80+
return current === method;
81+
}
82+
83+
// for Array
84+
return method.indexOf(current) !== -1;
85+
}
86+
87+
function parseRule(rule) {
88+
const parts = rule.split('#');
89+
return { module: parts[0], action: parts[1] };
90+
}
91+
92+
function createRouteInfo(item, match) {
93+
const { rule, queries } = item;
94+
const paramQuery = queries.reduce((acc, { name, index }) => {
95+
acc[name] = match[index];
96+
return acc;
97+
}, {});
98+
99+
const query = rule.query ? { ...paramQuery, ...rule.query } : paramQuery;
100+
return { module: rule.module, action: rule.action, query };
101+
}

0 commit comments

Comments
 (0)