From d0a46320888aed18fd6a1bf4bb2ca54c24220b35 Mon Sep 17 00:00:00 2001 From: GaoXiaochen Date: Sat, 16 Jul 2016 02:19:11 +0800 Subject: [PATCH] feat: first version (#1) close eggjs/egg#11 --- .eslintignore | 3 + .gitignore | 4 +- README.md | 73 +++++++- README.zh-CN.md | 73 +++++++- index.js | 3 + lib/base_loader.js | 309 +++++++++++++++++++++++++++++++ lib/config_loader.js | 116 ++++++++++++ lib/extend_loader.js | 129 +++++++++++++ lib/middleware_loader.js | 69 +++++++ lib/plugin_loader.js | 374 ++++++++++++++++++++++++++++++++++++++ lib/proxy_loader.js | 53 ++++++ lib/service_loader.js | 60 ++++++ lib/utils/class_loader.js | 76 ++++++++ lib/utils/index.js | 125 +++++++++++++ lib/utils/sequencify.js | 42 +++++ package.json | 15 +- 16 files changed, 1518 insertions(+), 6 deletions(-) create mode 100644 .eslintignore create mode 100644 index.js create mode 100644 lib/base_loader.js create mode 100644 lib/config_loader.js create mode 100644 lib/extend_loader.js create mode 100644 lib/middleware_loader.js create mode 100644 lib/plugin_loader.js create mode 100644 lib/proxy_loader.js create mode 100644 lib/service_loader.js create mode 100644 lib/utils/class_loader.js create mode 100644 lib/utils/index.js create mode 100644 lib/utils/sequencify.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..0abd3b49 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +test/fixtures +node_modules +coverage diff --git a/.gitignore b/.gitignore index 0db1fd44..211308ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -node_modules +/node_modules coverage .logs +test +npm-debug.log \ No newline at end of file diff --git a/README.md b/README.md index d3a7c05c..271da401 100644 --- a/README.md +++ b/README.md @@ -1 +1,72 @@ -# egg-logger \ No newline at end of file +# egg-loader + +[![NPM version][npm-image]][npm-url] +[![build status][travis-image]][travis-url] +[![Test coverage][codecov-image]][codecov-url] +[![David deps][david-image]][david-url] +[![Known Vulnerabilities][snyk-image]][snyk-url] +[![npm download][download-image]][download-url] + +[npm-image]: https://img.shields.io/npm/v/egg-loader.svg?style=flat-square +[npm-url]: https://npmjs.org/package/egg-loader +[travis-image]: https://img.shields.io/travis/eggjs/egg-loader.svg?style=flat-square +[travis-url]: https://travis-ci.org/eggjs/egg-loader +[codecov-image]: https://codecov.io/github/eggjs/egg-loader/coverage.svg?branch=master +[codecov-url]: https://codecov.io/github/eggjs/egg-loader?branch=master +[david-image]: https://img.shields.io/david/eggjs/egg-loader.svg?style=flat-square +[david-url]: https://david-dm.org/eggjs/egg-loader +[snyk-image]: https://snyk.io/test/npm/egg-loader/badge.svg?style=flat-square +[snyk-url]: https://snyk.io/test/npm/egg-loader +[download-image]: https://img.shields.io/npm/dm/egg-loader.svg?style=flat-square +[download-url]: https://npmjs.org/package/egg-loader + +egg 文件加载器 + +## 使用说明 + +```js +const app = koa(); +const Loader = require('egg-loader'); +const loader = new Loader({ + baseDir: '/path/to/app', + eggPath: '/path/to/framework', + app: app, +}); +loader.loadPlugin(); +loader.loadConfig(); +``` + +## API + +### options + +- baseDir: 应用根目录 +- eggPath: egg 本身的路径 +- plugins: 自定义插件配置 +- app: 任何基于 koa 实例化 + +### methods + +基础方式 + +- loadFile: 加载单文件, +- loadDirs: 获取需要加载的所有目录,按照 egg > 插件 > 框架 > 应用的顺序加载。 + +业务方法 + +- getAppname: 获取应用名 +- loadServerEnv: 加载环境变量 +- loadConfig: 加载: config +- loadPlugin: 加载插件 +- loadApplication: 加载 extend/application.js 到 app +- loadRequest: 加载 extend/request.js 到 app.request +- loadResponse: 加载 extend/response.js 到 app.response +- loadContext: 加载 extend/context.js 到 app.context +- loadHelper: 加载 extend/helper.js,到 app.Helper.prototype,需要定义 app.Helper 才会加载 +- loadService: 加载 app/service 到 app.service +- loadProxy: 加载 app/proxy 到 app.proxy +- loadMiddleware: 加载中间件 +- loadController: 加载 app/controller 到 app.controller +- loadAgent: 加载 agent.js 进行自定义 +- loadApp: 加载 app.js 进行自定义 + diff --git a/README.zh-CN.md b/README.zh-CN.md index dca515a7..271da401 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1 +1,72 @@ -# egg-loader \ No newline at end of file +# egg-loader + +[![NPM version][npm-image]][npm-url] +[![build status][travis-image]][travis-url] +[![Test coverage][codecov-image]][codecov-url] +[![David deps][david-image]][david-url] +[![Known Vulnerabilities][snyk-image]][snyk-url] +[![npm download][download-image]][download-url] + +[npm-image]: https://img.shields.io/npm/v/egg-loader.svg?style=flat-square +[npm-url]: https://npmjs.org/package/egg-loader +[travis-image]: https://img.shields.io/travis/eggjs/egg-loader.svg?style=flat-square +[travis-url]: https://travis-ci.org/eggjs/egg-loader +[codecov-image]: https://codecov.io/github/eggjs/egg-loader/coverage.svg?branch=master +[codecov-url]: https://codecov.io/github/eggjs/egg-loader?branch=master +[david-image]: https://img.shields.io/david/eggjs/egg-loader.svg?style=flat-square +[david-url]: https://david-dm.org/eggjs/egg-loader +[snyk-image]: https://snyk.io/test/npm/egg-loader/badge.svg?style=flat-square +[snyk-url]: https://snyk.io/test/npm/egg-loader +[download-image]: https://img.shields.io/npm/dm/egg-loader.svg?style=flat-square +[download-url]: https://npmjs.org/package/egg-loader + +egg 文件加载器 + +## 使用说明 + +```js +const app = koa(); +const Loader = require('egg-loader'); +const loader = new Loader({ + baseDir: '/path/to/app', + eggPath: '/path/to/framework', + app: app, +}); +loader.loadPlugin(); +loader.loadConfig(); +``` + +## API + +### options + +- baseDir: 应用根目录 +- eggPath: egg 本身的路径 +- plugins: 自定义插件配置 +- app: 任何基于 koa 实例化 + +### methods + +基础方式 + +- loadFile: 加载单文件, +- loadDirs: 获取需要加载的所有目录,按照 egg > 插件 > 框架 > 应用的顺序加载。 + +业务方法 + +- getAppname: 获取应用名 +- loadServerEnv: 加载环境变量 +- loadConfig: 加载: config +- loadPlugin: 加载插件 +- loadApplication: 加载 extend/application.js 到 app +- loadRequest: 加载 extend/request.js 到 app.request +- loadResponse: 加载 extend/response.js 到 app.response +- loadContext: 加载 extend/context.js 到 app.context +- loadHelper: 加载 extend/helper.js,到 app.Helper.prototype,需要定义 app.Helper 才会加载 +- loadService: 加载 app/service 到 app.service +- loadProxy: 加载 app/proxy 到 app.proxy +- loadMiddleware: 加载中间件 +- loadController: 加载 app/controller 到 app.controller +- loadAgent: 加载 agent.js 进行自定义 +- loadApp: 加载 app.js 进行自定义 + diff --git a/index.js b/index.js new file mode 100644 index 00000000..b562eb25 --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./lib/base_loader'); diff --git a/lib/base_loader.js b/lib/base_loader.js new file mode 100644 index 00000000..bcbccef5 --- /dev/null +++ b/lib/base_loader.js @@ -0,0 +1,309 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const isFunction = require('is-type-of').function; +const loading = require('loading'); +const interopRequire = require('interop-require'); +const debug = require('debug')('egg:loader'); + +/** + * Loader 基类 + */ +class EggLoader { + + /** + * @constructor + * @param {Object} options + * - {String} [baseDir] - 应用根目录 + * - {String} [eggPath] - 使用 egg-loader 的框架的路径,如 egg + * - {String} [customEgg] - 自定义入口框架的路径,每个异构技术 bu 都可以自定义自己的插件集合 + * - {Object} [plugins] - 自定义插件配置,测试用 + * - {Object} [app] - app 实例,如果是 Agent Worker 则传入 agent 实例,可为空 + * - {Logger} [logger] - logger 实例,默认是 console + */ + constructor(options) { + this.options = options || {}; + this.options.logger = this.options.logger || console; + this.app = this.options.app || {}; // master 没有 app + assert(fs.existsSync(this.options.baseDir), `${this.options.baseDir} not exists`); + + /** + * 读取 package.json + * @member {Object} EggLoader#pkg + */ + this.pkg = require(path.join(this.options.baseDir, 'package.json')); + + /** + * 初始化时传入,见 {@link EggLoader} + * @member {String} EggLoader#eggPath + */ + this.eggPath = fs.realpathSync(this.options.eggPath); + debug('Loaded eggPath %j', this.eggPath); + + /** + * 框架可以继承,从入口框架(CustomEgg)找到所有的框架的根目录 + * + * 需要通过配置 getter 来指定 eggPath 才能被加载到 + * + * ``` + * // lib/xx.js + * const egg = require('egg'); + * class XxApplication extends egg.Application { + * constructor(options) { + * super(options); + * } + * + * get [Symbol.for('egg#eggPath')]() { + * return path.join(__dirname, '..'); + * } + * } + * ``` + * @member {Array} EggLoader#frameworkPaths + */ + this.frameworkPaths = this.loadFrameworkPaths(); + debug('Loaded frameworkPaths %j', this.frameworkPaths); + + /** + * = this.eggPath + this.frameworkPaths + * @member {Array} EggLoader#eggPaths + */ + this.eggPaths = [ this.eggPath ].concat(this.frameworkPaths); + debug('Loaded eggPaths %j', this.eggPaths); + + /** + * 获取当前应用所在的机器环境,统一 serverEnv + * ``` + * serverEnv | 说明 + * --- | --- + * default | 默认环境 + * test | 交付测试 + * prod | 主站生产环境,包括预发,线上服务器 + * local | 本地开发环境,就是你的电脑本地启动 + * unittest | 单元测试环境,tnpm test, NODE_ENV=test + * ``` + * + * @member {String} EggLoader#serverEnv + */ + this.serverEnv = this.loadServerEnv(); + debug('Loaded serverEnv %j', this.serverEnv); + } + + /** + * 加载自定义的 app.js,**在 app.js 可做任何操作,但建议尽量减少此操作,做该做的事**。 + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @example + * ```js + * module.exports = function(app) { + * // 自定义 + * } + * ``` + */ + loadCustomApp() { + this.loadDirs() + .forEach(dir => this.loadFile(path.join(dir, 'app.js'))); + } + + /** + * 同 {@link EggLoader#loadCustomApp},但加载自定义的 agent.js + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @example + * ```js + * module.exports = function(agent) { + * // 自定义 + * } + * ``` + */ + loadCustomAgent() { + this.loadDirs() + .forEach(dir => this.loadFile(path.join(dir, 'agent.js'))); + } + + /** + * 加载 app/controller 目录下的文件 + * + * @param {Object} opt - loading 参数 + */ + loadController(opt) { + const app = this.app; + opt = Object.assign({ lowercaseFirst: true }, opt); + const controllerBase = path.join(this.options.baseDir, 'app/controller'); + + delete app.controller; + app.controller = {}; + + loading(controllerBase, opt).into(app, 'controller'); + app.coreLogger.info('[egg:loader] Controller loaded: %s', controllerBase); + } + + /** + * 加载指定文件,如果文件返回是函数则返回函数的调用结果,否则直接返回。 + * + * @param {String} filepath - 加载的文件路径 + * @param {Object} inject - 调用函数时的第一个参数,默认为 app + * @return {Object} - 返回加载文件的结果 + * @example + * ```js + * app.loader.loadFile(path.join(app.options.baseDir, 'config/router.js')); + * ``` + */ + loadFile(filepath) { + if (!fs.existsSync(filepath)) { + return null; + } + + let ret; + try { + ret = interopRequire(filepath); + } catch (err) { + err.message = `[egg-loader] load file ${filepath} error: ${err.message}`; + throw err; + } + // 可支持传入多个参数 + // function(arg1, args, ...) {} + let inject = Array.prototype.slice.call(arguments, 1); + if (inject.length === 0) inject = [ this.app ]; + return isFunction(ret) ? ret.apply(null, inject) : ret; + } + + /** + * 返回 egg 需要加载的目录 + * + * 1. 核心框架目录,目录为框架根目录下的 lib/core 目录,框架根目录来自 {@link EggLoader#eggPaths} + * 2. 已开启插件的根目录 + * 3. 应用根目录 + * + * @return {Array} 返回所有目录 + */ + loadDirs() { + // 做一层缓存 + if (this.dirs) { + return this.dirs; + } + + const dirs = this.dirs = []; + + // egg 本身路径,在 lib/core 目录下 + dirs.push(path.join(this.eggPath, 'lib/core')); + + // 插件目录,master 没有 plugin + if (this.orderPlugins) { + for (const plugin of this.orderPlugins) { + dirs.push(plugin.path); + } + } + + // egg 框架路径,在 lib/core 目录下 + for (const frameworkPath of this.frameworkPaths) { + dirs.push(path.join(frameworkPath, 'lib/core')); + } + + // 应用目录 + dirs.push(this.options.baseDir); + + debug('Loaded dirs %j', dirs); + return dirs; + } + + /** + * 获取环境变量 + * + * 1. 从 EGG_SERVER_ENV 获取,一般用于测试 + * 2. 从 `$baseDir/config/serverEnv` 读取,框架可根据实际情况自行设置 + * 3. 默认值 + * + * @return {String} serverEnv + * @see EggLoader#serverEnv + */ + loadServerEnv() { + let serverEnv = process.env.EGG_SERVER_ENV; + + const envPath = path.join(this.options.baseDir, 'config/serverEnv'); + if (fs.existsSync(envPath)) { + serverEnv = fs.readFileSync(envPath, 'utf8').trim(); + } + + if (!serverEnv) { + if (process.env.NODE_ENV === 'test') { + serverEnv = 'unittest'; + } else if (process.env.NODE_ENV === 'production') { + serverEnv = 'default'; + } else { + serverEnv = 'local'; + } + } + + return serverEnv; + } + + /** + * 获取 {@link EggLoader#frameworkPaths} + * @return {Array} 框架目录 + * @private + */ + loadFrameworkPaths() { + const eggPath = this.eggPath; + const frameworkPaths = []; + + // 遍历整个原型链,获取原型链上所有的 eggPath + // 越核心的优先级越高 + let proto = this.app; + while (proto) { + proto = Object.getPrototypeOf(proto); + if (proto) { + const eggPath = proto[Symbol.for('egg#eggPath')]; + addEggPath(eggPath); + } + } + + return frameworkPaths; + + function addEggPath(dirpath) { + if (dirpath) { + // 使用 fs.realpathSync 来找到最终路径 + const realpath = fs.realpathSync(dirpath); + if (frameworkPaths.indexOf(realpath) === -1 && realpath !== eggPath) { + frameworkPaths.unshift(realpath); + } + } + } + } + + /** + * 返回应用 appname,默认获取 pkg.name + * + * @return {String} appname + * @private + */ + getAppname() { + if (this.pkg.name) { + debug('Loaded appname(%s) from package.json', this.pkg.name); + return this.pkg.name; + } + throw new Error('Can not get appname from package.json'); + } + +} + +/** + * Mixin loader 方法到 BaseLoader,class 不支持多类继承 + * // ES6 Multiple Inheritance + * https://medium.com/@leocavalcante/es6-multiple-inheritance-73a3c66d2b6b + */ +const loaders = [ + require('./plugin_loader'), + require('./config_loader'), + require('./extend_loader'), + require('./proxy_loader'), + require('./service_loader'), + require('./middleware_loader'), +]; + +for (const loader of loaders) { + Object.assign(EggLoader.prototype, loader); +} + +module.exports = EggLoader; diff --git a/lib/config_loader.js b/lib/config_loader.js new file mode 100644 index 00000000..593fd815 --- /dev/null +++ b/lib/config_loader.js @@ -0,0 +1,116 @@ +'use strict'; + +const debug = require('debug')('egg:loader:config'); +const fs = require('fs'); +const path = require('path'); +const extend = require('extend'); +const getHomedir = require('./utils').getHomedir; + +module.exports = { + + /** + * config/config.js 加载类,封装加载逻辑, + * + * 加载时会合并 config.default.js 和 config.${env}.js + * + * config.middleware 和 config.proxy 的配置会被剔除 + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @method EggLoader#loadConfig + */ + loadConfig() { + const target = {}; + + const names = [ + 'config.default.js', + `config.${this.serverEnv}.js`, + ]; + + // 先加载一次应用配置,传给框架和插件的配置 + const appConfig = this._preloadAppConfig(); + + // egg config.default + // plugin config.default + // framework config.default + // egg config.{env} + // plugin config.{env} + // framework config.{env} + for (const filename of names) { + for (const dirpath of this.loadDirs()) { + const config = this._loadConfig(dirpath, filename, appConfig); + + if (!config) { + continue; + } + + debug('Loaded config %s/%s, %j', dirpath, filename, config); + extend(true, target, config); + } + } + + // 可以在 app.js 中操作 app.config.coreMiddleware 和 app.config.appMiddleware; + target.coreMiddleware = target.coreMiddlewares = target.coreMiddleware || []; + // 记录应用自定义中间件,后续可以根据此配置让插件在将中间件放在应用自定义中间件之前 + target.appMiddleware = target.appMiddlewares = target.middleware || []; + + /** + * 获取 `{baseDir}/config/config.{env}.js` 下的配置。 + * 包含以下配置: + * + * * `baseDir`: 应用文件基础目录, 如 `/home/admin/demoapp` + * * `pkg`: [package.json] 配置 + */ + this.config = target; + }, + + // 提前加载应用配置,可以传给其他配置 + _preloadAppConfig() { + const names = [ + 'config.default.js', + `config.${this.serverEnv}.js`, + ]; + const target = {}; + for (const filename of names) { + const config = this._loadConfig(this.options.baseDir, filename); + extend(true, target, config); + } + return target; + }, + + _loadConfig(dirpath, filename, extraInject) { + const pluginPaths = this.orderPlugins ? this.orderPlugins.map(plugin => plugin.path) : []; + const isPlugin = pluginPaths.indexOf(dirpath) > -1; + const isApp = dirpath === this.options.baseDir; + + let filepath = path.join(dirpath, 'config', filename); + // 兼容 config.js,config.js 和 config.default 是平级的 + if (filename === 'config.default.js' && !fs.existsSync(filepath)) { + filepath = path.join(dirpath, 'config/config.js'); + } + const name = this.getAppname(); + const config = this.loadFile(filepath, { + name, + baseDir: this.options.baseDir, + env: this.serverEnv, + HOME: getHomedir(), + pkg: this.pkg, + }, extraInject); + + if (!config) { + return null; + } + + // 插件和应用不允许配置 coreMiddleware + if (isPlugin || isApp) { + delete config.coreMiddleware; + } + // 框架和插件不运行配置 middleware 和 proxy 的属性,避免覆盖应用的 + if (!isApp) { + delete config.middleware; + delete config.proxy; + } + + return config; + }, + +}; diff --git a/lib/extend_loader.js b/lib/extend_loader.js new file mode 100644 index 00000000..109a4ec5 --- /dev/null +++ b/lib/extend_loader.js @@ -0,0 +1,129 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const interopRequire = require('interop-require'); +const utils = require('./utils'); +const debug = require('debug')('egg:extend:loader'); + +const loadExtend = Symbol('loadExtend'); + +module.exports = { + + /** + * 扩展 Agent.prototype 的属性 + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @method EggLoader#loadAgent + */ + loadAgent() { + this[loadExtend]('agent', this.app); + }, + + /** + * 扩展 Application.prototype 的属性 + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @method EggLoader#loadApplication + */ + loadApplication() { + this[loadExtend]('application', this.app); + }, + + /** + * 扩展 Request.prototype 的属性 + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @method EggLoader#loadRequest + */ + loadRequest() { + this[loadExtend]('request', this.app.request); + }, + + /** + * 扩展 Response.prototype 的属性 + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @method EggLoader#loadResponse + */ + loadResponse() { + this[loadExtend]('response', this.app.response); + }, + + /** + * 扩展 Context.prototype 的属性 + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @method EggLoader#loadContext + */ + loadContext() { + this[loadExtend]('context', this.app.context); + }, + + /** + * 扩展 app.Helper.prototype 的属性 + * + * 可加载路径查看 {@link EggLoader#loadDirs} + * @method EggLoader#loadHelper + */ + loadHelper() { + if (this.app && this.app.Helper) { + this[loadExtend]('helper', this.app.Helper.prototype); + } + }, + + /** + * 加载 extend 基类 + * + * @method loadExtend + * @param {String} name - 加载的文件名,如 app/extend/{name}.js + * @param {Object} proto - 最终将属性合并到 proto 上 + * @private + */ + [loadExtend](name, proto) { + // 获取需要加载的文件 + const filepaths = this.loadDirs() + .map(dir => { + let pluginExtendsPath = path.join(dir, 'app/extend'); + if (!fs.existsSync(pluginExtendsPath)) { + pluginExtendsPath = path.join(dir, 'app'); + } + return path.join(pluginExtendsPath, name); + }); + const mergeRecord = new Map(); + for (const filepath of filepaths) { + if (!utils.existsModule(filepath)) { + continue; + } + + let ext; + try { + ext = interopRequire(filepath); + } catch (err) { + err.message = `[egg-loader] load file ${require.resolve(filepath)} error: ${err.message}`; + throw err; + } + + const names = Object.getOwnPropertyNames(ext) + .concat(Object.getOwnPropertySymbols(ext)); + + if (names.length === 0) { + continue; + } + + for (const name of names) { + if (mergeRecord.has(name)) { + debug('Property: "%s" already exists in "%s",it will be redefined by "%s"', + name, mergeRecord.get(name), filepath); + } + + // Copy descriptor + const descriptor = Object.getOwnPropertyDescriptor(ext, name); + Object.defineProperty(proto, name, descriptor); + mergeRecord.set(name, filepath); + } + debug('merge %j to %s from %s', Object.keys(ext), name, filepath); + } + + }, +}; diff --git a/lib/middleware_loader.js b/lib/middleware_loader.js new file mode 100644 index 00000000..90cac940 --- /dev/null +++ b/lib/middleware_loader.js @@ -0,0 +1,69 @@ +'use strict'; + +const join = require('path').join; +const loading = require('loading'); +const is = require('is-type-of'); +const debug = require('debug')('egg:loader:middleware'); +const inspect = require('util').inspect; + +module.exports = { + /** + * 加载中间件,将中间件加载到 app.middleware,并根据 config 配置载入到上下文中 + * + * 中间件允许覆盖,优先级依次从上到下 + * + * 中间件的规范写法为,options 是同名配置获取的 + * ```js + * // app/middleware/status.js + * module.exports = function(options, app) { + * // options == app.config.status + * return function*(next) { + * yield next; + * } + * } + * ``` + * @method EggLoader#loadMiddleware + * @param {Object} opt - loading 参数 + */ + loadMiddleware(opt) { + + const app = this.app; + opt = Object.assign({ + // 加载中间件,但是不调用它 + call: false, + override: true, + lowercaseFirst: true, + }, opt); + const middlewarePaths = this.loadDirs().map(dir => join(dir, 'app/middleware')); + + delete app.middlewares; + loading(middlewarePaths, opt).into(app, 'middlewares'); + + app.coreLogger.info('Use coreMiddleware order: %j', this.config.coreMiddleware); + app.coreLogger.info('Use appMiddleware order: %j', this.config.appMiddleware); + + // 将中间件加载到 koa 中 + // 通过 app.config.coreMiddleware, app.config.appMiddleware 配置的顺序加载 + const middlewareNames = this.config.coreMiddleware.concat(this.config.appMiddleware); + debug('middlewareNames: %j', middlewareNames); + for (const name of middlewareNames) { + if (!app.middlewares[name]) { + throw new TypeError(`Middleware ${name} not found`); + } + + const options = this.config[name] || {}; + let mw = app.middlewares[name]; + mw = mw(options, app); + if (!is.generatorFunction(mw)) { + throw new TypeError(`Middleware ${name} must be a generator function, but actual is ${inspect(mw)}`); + } + mw._name = name; + app.use(mw); + debug('Use middleware: %s with options: %j', name, options); + app.coreLogger.info('[egg:loader] Use middleware: %s with options: %s', name, inspect(options, { depth: 4 })); + } + + app.coreLogger.info('[egg:loader] Loaded middleware from %j', middlewarePaths); + }, + +}; diff --git a/lib/plugin_loader.js b/lib/plugin_loader.js new file mode 100644 index 00000000..d0f56888 --- /dev/null +++ b/lib/plugin_loader.js @@ -0,0 +1,374 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const debug = require('debug')('egg:loader:plugin'); +const interopRequire = require('interop-require'); + +const sequencify = require('./utils/sequencify'); + +module.exports = { + + /** + * 根据配置加载插件,实现 loadPlugin() 接口 + * + * 插件配置来自三个地方 + * + * 1. 应用 config/plugin.js,优先级最高 + * 2. egg/lib/core/config/plugin.js,优先级次之。 + * 3. 插件本身的 package.json => eggPlugin 配置,优先级最低 + * + * 具体的插件配置类似 + * + * ```js + * { + * 'xxx-client': { + * enable: true, + * package: 'xxx-client', + * dep: [], + * env: [], + * }, + * // 简写 + * 'rds': false, + * // 自定义路径,优先级最高 + * 'depd': { + * enable: true, + * path: 'path/to/depd' + * } + * } + * ``` + * + * 根据配置从三个目录去加载插件,优先级依次降低 + * + * 1. $APP_BASE/node_modules/${package or name} + * 2. $EGG_BASE/node_modules/${package or name} + * 3. $EGG_BASE/lib/plugins/${package or name} + * + * 加载后可通过 `loader.plugins` 访问已开启的插件 + * + * ```js + * loader.plugins['xxx-client'] = { + * name: 'xxx-client', // 模块名,模块依赖配置使用这个名字 + * package: 'xxx-client', // 包名,加载插件时会尝试使用包名加载 + * enable: true, // 是否开启 + * path: 'path/to/xxx-client', // 插件路径 + * dep: [], // 依赖的模块 + * env: [ 'local', 'unittest' ], // 只在这两个环境下开启才有效 + * } + * ``` + * + * 如需要访问所有插件可调用 `loader.allPlugins` + * @method EggLoader#loadPlugin + */ + loadPlugin() { + // 读取 appPlugins,为应用配置 + const appPlugins = this.readPluginConfigs(path.join(this.options.baseDir, 'config/plugin.js')); + debug('Loaded app plugins: %j', Object.keys(appPlugins)); + + // 读取 eggPlugins,为框架和 egg 配置 + const eggPluginConfigPaths = this.eggPaths.map(eggPath => path.join(eggPath, 'lib/core/config/plugin.js')); + const eggPlugins = this.readPluginConfigs(eggPluginConfigPaths); + debug('Loaded egg plugins: %j', Object.keys(eggPlugins)); + + // 自定义插件配置,一般用于单元测试 + let customPlugins; + if (process.env.EGG_PLUGINS) { + try { + customPlugins = JSON.parse(process.env.EGG_PLUGINS); + } catch (e) { + debug('parse EGG_PLUGINS failed, %s', e); + } + } + + if (this.options.plugins) { + customPlugins = Object.assign({}, customPlugins, this.options.plugins); + } + + if (customPlugins) { + for (const name in customPlugins) { + this.normalizePluginConfig(customPlugins, name); + } + debug('Loaded custom plugins: %j', Object.keys(customPlugins)); + } + + // 合并所有插件 + this.allPlugins = {}; + extendPlugins(this.allPlugins, eggPlugins); + extendPlugins(this.allPlugins, appPlugins); + extendPlugins(this.allPlugins, customPlugins); + + // 过滤出环境不符的插件、以及被应用显示关闭的插件 + const enabledPluginNames = []; // 入口开启的插件列表,不包括被依赖的 + const plugins = {}; + const env = this.serverEnv; + for (const name in this.allPlugins) { + const plugin = this.allPlugins[name]; + + // 根据 path/package 获取真正的插件路径,两者互斥 + plugin.path = this.getPluginPath(plugin, this.options.baseDir); + + // 从 eggPlugin 更新插件信息 + this.mergePluginConfig(plugin); + + // 只允许符合服务器环境 env 条件的插件开启 + if (env && plugin.env.length && plugin.env.indexOf(env) === -1) { + debug('Disabled %j as env is %j, but got %j', name, plugin.env, env); + plugin.enable = false; + continue; + } + + // app 的配置优先级最高,切不允许隐式的规则推翻 app 配置 + if (appPlugins[name] && !appPlugins[name].enable) { + debug('Disabled %j as disabled by app', name); + continue; + } + + plugins[name] = plugin; + + if (plugin.enable) { + enabledPluginNames.push(name); + } + } + + // 获取开启的插件,并排序 + this.orderPlugins = this.getOrderPlugins(plugins, enabledPluginNames); + + // 将数组转换成对象 + const enablePlugins = {}; + for (const plugin of this.orderPlugins) { + enablePlugins[plugin.name] = plugin; + } + debug('Loaded plugins: %j', Object.keys(enablePlugins)); + + /** + * 获取 plugin 配置 + * @alias app.loader.plugins + * @see Plugin + * @member {Object} App#plugins + * @since 1.0.0 + */ + this.plugins = enablePlugins; + }, + + /* + * 读取 plugin.js 配置 + */ + readPluginConfigs(configPaths) { + if (!Array.isArray(configPaths)) { + configPaths = [ configPaths ]; + } + + const plugins = {}; + for (const configPath of configPaths) { + if (!fs.existsSync(configPath)) { + continue; + } + + const config = interopRequire(configPath); + + for (const name in config) { + this.normalizePluginConfig(config, name); + } + + // 拷贝一个新对象,不修改原来的对象 + extendPlugins(plugins, config); + } + + return plugins; + }, + + /* + * 标准化每个插件的配置项 + */ + normalizePluginConfig(plugins, name) { + const plugin = plugins[name]; + + // 布尔型为简写,将其标准化 + // plugin_name: false + if (typeof plugin === 'boolean') { + plugins[ name ] = { + name, + enable: plugin, + dep: [], + env: [], + }; + return; + } + + // 否则标准化每个配置 + if (!('enable' in plugin)) { + plugin.enable = true; // 如果没有配则默认开启 + } + plugin.name = name; + plugin.dep = plugin.dep || []; + plugin.env = plugin.env || []; + // path, package 不需要处理 + }, + + // 读取插件本身的配置信息,插件只支持以下字段 + // { + // "name": "", 插件本身定义的名字,必须和配置名(应用或框架定义的 config/plugin.js)一致 + // "dep": [], 插件申明的依赖 + // "env": "", 插件适用的环境 + // } + mergePluginConfig(plugin) { + let pkg; + let config; + // 从 pkg.eggPlugin 获取配置 + const pluginPackage = path.join(plugin.path, 'package.json'); + if (fs.existsSync(pluginPackage)) { + pkg = require(pluginPackage); + config = pkg.eggPlugin; + if (pkg.version) { + plugin.version = pkg.version; + } + } + + if (!config) { + return; + } + + if (config.name && config.name !== plugin.name) { + // TODO: 兼容提示,1.1 改成必须配置 name + // pluginName 为 config/plugin.js 配置的插件名 + // pluginConfigName 为 pkg.eggPath.name + this.options.logger.warn(`[egg:loader] pluginName(${plugin.name}) is different from pluginConfigName(${config.name})`); + } + for (const key of [ 'dep', 'env' ]) { + if (!plugin[key].length && Array.isArray(config[key])) { + plugin[key] = config[key]; + } + } + }, + + /** + * 获取所有已开启并排序后的插件列表 + * @param {Object} allPlugins 所有的插件 + * @param {Array} enabledPluginNames 插件列表 + * @return {Array} 插件列表 + * @private + */ + getOrderPlugins(allPlugins, enabledPluginNames) { + // 表示所有插件都未开启 + if (!enabledPluginNames.length) { + return []; + } + + const result = sequencify(allPlugins, enabledPluginNames); + debug('Got plugins %j after sequencify', result); + + // 如果 result.sequence 是空数组可能处理有问题 + if (!result.sequence.length) { + const err = new Error(`sequencify plugins has problem, missing: [${result.missingTasks}], recursive: [${result.recursiveDependencies}]`); + // 找出缺少的 plugins 被谁依赖了 + for (const missName of result.missingTasks) { + const requires = []; + for (const name in allPlugins) { + if (allPlugins[name].dep.indexOf(missName) >= 0) { + requires.push(name); + } + } + err.message += `\n\t>> Plugin [${missName}] is disabled or missed, but is required by [${requires}]`; + } + + err.name = 'PluginSequencifyError'; + throw err; + } + + // 打印被自动开启的插件 + const implicitEnabledPlugins = []; + const requireMap = {}; + result.sequence.forEach(name => { + // 统计插件被那些插件依赖,用于提示隐式开启的插件引用关系 + for (const depName of allPlugins[name].dep) { + if (!requireMap[depName]) { + requireMap[depName] = []; + } + requireMap[depName].push(name); + } + + if (!allPlugins[name].enable) { + // 如果计算结果未开启说明需要自动开启 + implicitEnabledPlugins.push(name); + allPlugins[name].enable = true; + } + }); + if (implicitEnabledPlugins.length) { + // Following plugins will be enabled implicitly. + // - configclient required by [hsfclient] + // - eagleeye required by [hsfclient] + // - diamond required by [hsfclient] + this.options.logger.info(`Following plugins will be enabled implicitly.\n${implicitEnabledPlugins.map(name => ` - ${name} required by [${requireMap[name]}]`).join('\n')}`); + } + + return result.sequence.map(name => allPlugins[name]); + }, + + // 获取插件真正的路径 + getPluginPath(plugin) { + // 如果指定了 path 则直接使用 + if (plugin.path) { + return plugin.path; + } + + // 根据 package/name 配置 + const name = plugin.package || plugin.name; + const lookupDirs = []; + + // 尝试在以下目录找到匹配的插件 + // -> {appname}/node_modules + // -> {framework}/node_modules + // -> {framework}/lib/plugins (plugin.name) + // -> egg/node_modules + // -> egg/lib/plugins (plugin.name) + // -> $CWD/node_modules + lookupDirs.push(path.join(this.options.baseDir, 'node_modules')); + + // 到 egg 中查找,优先从外往里查找 + for (let i = this.eggPaths.length - 1; i >= 0; i--) { + const eggPath = this.eggPaths[i]; + lookupDirs.push(path.join(eggPath, 'node_modules')); + lookupDirs.push(path.join(eggPath, 'lib/plugins')); + } + + // npm@3, 插件测试用例,还需要通过 $cwd/node_modules 目录获取 + lookupDirs.push(path.join(process.cwd(), 'node_modules')); + + for (let dir of lookupDirs) { + dir = path.join(dir, name); + if (fs.existsSync(dir)) { + return dir; + } + } + + throw new Error(`Can not find plugin ${name} in "${lookupDirs.join(', ')}"`); + }, + +}; + +// 将 plugin 合并到 target 中 +// 如果合并过程中,插件指定了 path/package,则将已存在的 path/package 删除。 +function extendPlugins(target, plugins) { + if (!plugins) { + return; + } + for (const name in plugins) { + const plugin = plugins[name]; + if (!target[name]) { + target[name] = {}; + } + if (plugin.path || plugin.package) { + delete target[name].path; + delete target[name].package; + } + for (const prop in plugin) { + if (plugin[prop] === undefined) { + continue; + } + if (target[name][prop] && Array.isArray(plugin[prop]) && !plugin[prop].length) { + continue; + } + target[name][prop] = plugin[prop]; + } + } +} diff --git a/lib/proxy_loader.js b/lib/proxy_loader.js new file mode 100644 index 00000000..71eee077 --- /dev/null +++ b/lib/proxy_loader.js @@ -0,0 +1,53 @@ +'use strict'; + +const join = require('path').join; +const loading = require('loading'); +const classLoader = Symbol('classLoader'); +const utils = require('./utils'); + +module.exports = { + + /** + * 加载 app/proxy 目录下的文件 + * + * 1. 加载应用 app/proxy + * 2. 加载插件 app/proxy + * + * @method EggLoader#loadProxy + * @param {Object} opt - loading 参数 + */ + loadProxy(opt) { + const app = this.app; + opt = Object.assign({ call: true, lowercaseFirst: true }, opt); + const arr = this.loadDirs().map(dir => join(dir, 'app/proxy')); + // load proxy classes to app.proxyClasses + delete app.proxyClasses; + loading(arr, opt).into(app, 'proxyClasses'); + + // this.proxy.demoQuery.getUser(uid) + Object.defineProperty(app.context, 'proxy', { + get() { + let loader = this[classLoader]; + if (!loader) { + this[classLoader] = loader = new this.app.ProxyClassLoader(this); + } + return loader; + }, + }); + + // { + // key1: { + // subkey1: SubProxy1, + // subkey2: { + // subkey21: SubProxy21, + // subkey22: SubProxy22, + // }, + // subkey3: SubProxy3, + // } + // } + app.ProxyClassLoader = utils.getClassLoader(app, 'proxy'); + + app.coreLogger.info('[egg:loader] Proxy loaded from %j', arr); + }, + +}; diff --git a/lib/service_loader.js b/lib/service_loader.js new file mode 100644 index 00000000..1804ba10 --- /dev/null +++ b/lib/service_loader.js @@ -0,0 +1,60 @@ +'use strict'; + +const path = require('path'); +const loading = require('loading'); +const utils = require('./utils'); +const classLoader = Symbol('classLoader'); + +module.exports = { + + /** + * 加载 app/service 目录下的文件 + * + * 1. 加载应用 app/service + * 2. 加载插件 app/service + * + * @method EggLoader#loadService + * @param {Object} opt - loading 参数 + */ + loadService(opt) { + const app = this.app; + opt = Object.assign({ call: false, lowercaseFirst: true }, opt); + const servicePaths = this.loadDirs().map(dir => { + const servicePath = path.join(dir, 'app/service'); + return servicePath; + }); + + // 载入到 app.serviceClasses + delete app.serviceClasses; + loading(servicePaths, opt).into(app, 'serviceClasses'); + + /** + * 可以访问到当前应用配置的所有 service, + * service 目录约定在 `${baseDir}/app/service`。 + * @since 1.0.0 + * @member Context#service + */ + Object.defineProperty(app.context, 'service', { + get() { + let loader = this[classLoader]; + if (!loader) { + this[classLoader] = loader = new this.app.ServiceClassLoader(this); + } + return loader; + }, + }); + + // { + // key1: { + // subkey1: SubService1, + // subkey2: { + // subkey21: SubService21, + // subkey22: SubService22, + // }, + // subkey3: SubService3, + // } + // } + app.ServiceClassLoader = utils.getClassLoader(app, 'service'); + }, + +}; diff --git a/lib/utils/class_loader.js b/lib/utils/class_loader.js new file mode 100644 index 00000000..38519568 --- /dev/null +++ b/lib/utils/class_loader.js @@ -0,0 +1,76 @@ +'use strict'; + +const is = require('is-type-of'); + +module.exports = function createClassLoader(classes, subClasses) { + class ClassLoader { + constructor(ctx) { + this.ctx = ctx; + this._cache = new Map(); + } + + _getInstance(classname) { + let instance = this._cache.get(classname); + if (!instance) { + const Class = classes[classname]; + if (typeof Class === 'function' && !is.generatorFunction(Class)) { + // module.exports = class SubService extends Serivce + instance = new Class(this.ctx); + } else { + // 兼容模式 + // module.exports = { ... } + instance = Class; + } + this._cache.set(classname, instance); + } + return instance; + } + + // 支持子节点类加载,目前只支持最多2级节点 + // 只能一次性将此节点下的类都实例化出来 + _getSubClassInstance(rootName) { + let obj = this._cache.get(rootName); + if (obj) { + return obj; + } + obj = {}; + const map = subClasses[rootName]; + for (const sub1 in map) { + const Class = map[sub1]; + if (typeof Class === 'function') { + obj[sub1] = new Class(this.ctx); + } else { + for (const sub2 in Class) { + const Class2 = Class[sub2]; + if (!obj[sub1]) { + obj[sub1] = {}; + } + obj[sub1][sub2] = new Class2(this.ctx); + } + } + } + this._cache.set(rootName, obj); + return obj; + } + } + + Object.keys(classes).forEach(function(classname) { + Object.defineProperty(ClassLoader.prototype, classname, { + get() { + return this._getInstance(classname); + }, + }); + }); + + if (subClasses) { + Object.keys(subClasses).forEach(function(rootName) { + Object.defineProperty(ClassLoader.prototype, rootName, { + get() { + return this._getSubClassInstance(rootName); + }, + }); + }); + } + + return ClassLoader; +}; diff --git a/lib/utils/index.js b/lib/utils/index.js new file mode 100644 index 00000000..4f889165 --- /dev/null +++ b/lib/utils/index.js @@ -0,0 +1,125 @@ +'use strict'; + +const is = require('is-type-of'); +const deprecate = require('depd')('egg-loader'); +const createClassLoader = require('./class_loader'); + +module.exports = exports = { + + /** + * 判断模块是否存在 + * @method Util#existsModule + * @param {String} path - 模块路径 + * @return {boolean} 如果模块存在则返回 `true`,否则返回 `false`。 + */ + existsModule(path) { + try { + require.resolve(path); + return true; + } catch (e) { + return false; + } + }, + + /** + * 获得 Home 目录,将会从环境变量 `HOME` 里面获取,如果没有,会返回 "/home/admin" + * @function getHomedir + * @return {String} 用户目录 + */ + getHomedir() { + return process.env.HOME || '/home/admin'; + }, + + // 遍历 + walk(app, classes, fn, path) { + path = path || []; + Object.keys(classes).forEach(key => { + let target = classes[key]; + const keys = [].concat(path, key); + + if (target === undefined || target === null) { + return undefined; + } + + // module.exports = class xxService extends Service {} + if (is.class(target)) { + return fn(target, keys); + } + + // 兼容模式: module.exports = function*() {} + if (is.generatorFunction(target)) { + return fn(target, keys); + } + + // 兼容模式: module.exports = function (app) {} + if (typeof target === 'function') { + // 自动调用一次 + target = target(app); + return fn(target, keys); + } + + // 判断是否是 exports.get = function* () {} 结构 + let hasGenerator = false; + for (const fnName in target) { + if (is.generatorFunction(target[fnName])) { + hasGenerator = true; + break; + } + } + + if (hasGenerator) { + return fn(target, keys); + } + + return module.exports.walk(app, target, fn, keys); + }); + }, + + /** + * 获取对应的 classloader + * @param {Object} app - app对象 + * @param {String} type - 要加载的类型, proxy / service + * @return {Function} 返回对应的 classloader + */ + getClassLoader(app, type) { + const targetClasses = app[type + 'Classes']; + const subClasses = {}; + + // hook to subServiceClasses / subProxyClasses, will be used in mm.mockService + const subClassesName = 'sub' + type[0].toUpperCase() + type.substring(1) + 'Classes'; + app[subClassesName] = subClasses; + + exports.walk(app, targetClasses, (target, keys) => { + const first = keys[0]; + if (keys.length === 1) { + targetClasses[first] = target; + return; + } + + if (keys.length > 3) { + deprecate(`不再支持超过 2 级子目录的 ${type} 加载,最长只到 ${first}.${keys[1]}.${keys[2]}`); + return; + } + + // 有两层或者三层的情况 + delete targetClasses[first]; + + // 最后一层的值是target + const last = keys.pop(); + + // keys为对象路径,首先依次查看subClasses下是否有,如果没有赋值空对象 + let classes = subClasses; + for (const key of keys) { + if (!classes[key]) { + classes[key] = {}; + } + classes = classes[key]; + } + + classes[last] = target; + }); + + return createClassLoader(targetClasses, subClasses); + }, + +}; diff --git a/lib/utils/sequencify.js b/lib/utils/sequencify.js new file mode 100644 index 00000000..fbd5665d --- /dev/null +++ b/lib/utils/sequencify.js @@ -0,0 +1,42 @@ +'use strict'; + +function sequence(tasks, names, results, missing, recursive, nest) { + names.forEach(function(name) { + if (results.indexOf(name) !== -1) { + return; // de-dup results + } + const node = tasks[name]; + if (!node) { + missing.push(name); + } else if (nest.indexOf(name) > -1) { + nest.push(name); + recursive.push(nest.slice(0)); + nest.pop(name); + } else if (node.dep.length) { + nest.push(name); + sequence(tasks, node.dep, results, missing, recursive, nest); // recurse + nest.pop(name); + } + results.push(name); + }); +} + +// tasks: object with keys as task names +// names: array of task names +module.exports = function(tasks, names) { + let results = []; // the final sequence + const missing = []; // missing tasks + const recursive = []; // recursive task dependencies + + sequence(tasks, names, results, missing, recursive, []); + + if (missing.length || recursive.length) { + results = []; // results are incomplete at best, completely wrong at worst, remove them to avoid confusion + } + + return { + sequence: results, + missingTasks: missing, + recursiveDependencies: recursive, + }; +}; diff --git a/package.json b/package.json index a27a3cc0..f027b7f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "egg-loader", - "version": "1.0.0", + "version": "0.0.1", "description": "egg loader", "main": "index.js", "scripts": { @@ -34,9 +34,18 @@ "egg-ci": "1", "eslint": "3", "eslint-config-egg": "3", + "koa": "1", + "koa-router": "4", "mm": "1", "should": "9", "supertest": "1" }, - "dependencies": {} -} + "dependencies": { + "debug": "^2.2.0", + "depd": "^1.1.0", + "extend": "^3.0.0", + "interop-require": "^1.0.0", + "is-type-of": "^1.0.0", + "loading": "^1.12.0" + } +} \ No newline at end of file