actionherojs 的插件机制
actionherojs 的插件机制是比较强大的,基于插件我们可以直接实现npm包的安装与卸载,同时利用提供的reload api 实现
模块功能的生效(grouparoo 就利用了这些特性)
创建一个插件
- 代码结构
可以使用actionherojs 的cli 创建
├── README.md
├── package.json
├── public
│ └── dalong
│ └── index.html
├── src
│ ├── actions
│ │ ├── createChatRoom.ts
│ │ ├── mydemo.ts
│ │ └── status.ts
│ ├── bin
│ ├── initializers
│ │ └── ah-dalong-ui.ts
│ ├── server.ts
│ ├── servers
│ └── tasks
├── tsconfig.json
└── yarn.lock
- 代码说明
plugin 实际上就是一个普通的actionherojs项目,只是有些功能我们不需要
以上代码很简单,就是一个简单的ui 组件, 注意actionhero 为peer的
package.json
{
"author": "dalongrong",
"name": "ah-dalong-plugin",
"description": "my actionhero project",
"version": "0.1.0",
"engines": {
"node": ">=8.0.0"
},
"dependencies": {
"ioredis": "latest",
"ioredis-mock": "latest",
"winston": "latest",
"ws": "latest"
},
"peerDependencies": {
"actionhero": "28.1.7"
},
"devDependencies": {
"@types/jest": "latest",
"@types/node": "latest",
"actionhero": "28.1.7",
"jest": "latest",
"prettier": "latest",
"ts-jest": "latest",
"ts-node-dev": "latest",
"type-fest": "latest",
"typescript": "latest"
},
"scripts": {
"postinstall": "npm run build",
"dev": "ts-node-dev --no-deps --transpile-only ./src/server",
"debug": "tsc && ts-node-dev --transpile-only --no-deps --inspect -- ./src/server ",
"start": "node ./dist/server.js",
"actionhero": "actionhero",
"test": "jest",
"pretest": "npm run build && npm run lint",
"build": "tsc --sourceMap false --declaration",
"lint": "prettier --check src/*/** __tests__/*/**",
"pretty": "prettier --write src/*/** __tests__/*/**"
},
"jest": {
"testEnvironment": "node",
"transform": {
"^.+\\\\.ts?$": "ts-jest"
}
}
}
initializers/ah-dalong-ui.ts 主要实现了简单的路由注册
import { Initializer, route} from "actionhero";
export class AHResqueUIInitializer extends Initializer {
constructor() {
super();
this.name = "ah-dalong-ui";
this.loadPriority = 99999999;
}
async initialize() {
/* ----- Route Injection ----- */
route.registerRoute(
"get",
"/:apiVersion/dalong",
"dalongrong"
);
}
}
actions/mydemo.ts
import {Action} from "actionhero"
module.exports = class MyDemoAction extends Action {
async run() {
return {name:"dalongrong"};
}
constructor(){
super()
this.name="dalongrong"
this.description="demoapp",
this.version=1
}
}
- 集成使用
yarn link 模式使用
actionhero 项目的config/plugins.ts
import { PluginConfig } from "actionhero";
const namespace = "plugins";
declare module "actionhero" {
export interface ActionheroConfigInterface {
[namespace]: ReturnType<typeof DEFAULT[typeof namespace]>;
}
}
export const DEFAULT: { [namespace]: () => PluginConfig } = {
[namespace]: () => {
return {
'dalong': { path: __dirname + '/../../node_modules/ah-dalong-plugin' }
}
}
};
- 访问效果
- 参考日志
- 参考机制
actionherojs 实际上 src/initializers/actions.ts 通过此加载的
for (const p of config.general.paths.action) {
let files = glob.sync(path.join(p, "**", "**/*(*.js|*.ts)"));
files = utils.ensureNoTsHeaderFiles(files);
for (const j in files) {
await api.actions.loadFile(files[j]);
}
}
// 包含plugin的模式
for (const plugin of Object.values(config.plugins)) {
if (plugin.actions !== false) {
const pluginPath: string = path.normalize(plugin.path);
// old style at the root of the project
let files = glob.sync(path.join(pluginPath, "actions", "**", "*.js"));
files = files.concat(
glob.sync(path.join(pluginPath, "dist", "actions", "**", "*.js"))
);
utils
.ensureNoTsHeaderFiles(files)
.forEach((f) => api.actions.loadFile(f)); // 基于import 动态加载插件里边的代码定义
}
}
对于静态内容,实际上是基于staticFile 的Initializer 解决的,使用了默认的public 路径
参考
async initialize() {
api.staticFile = {
searchLocations: [],
get: this.get,
searchPath: this.searchPath,
sendFile: this.sendFile,
fileLogger: this.fileLogger,
sendFileNotFound: this.sendFileNotFound,
checkExistence: this.checkExistence,
logRequest: this.logRequest,
};
// load in the explicit public paths first
if (config.general.paths) {
config.general.paths.public.forEach(function (p: string) {
api.staticFile.searchLocations.push(path.normalize(p));
});
}
// source the public directories from plugins
for (const plugin of Object.values(config.plugins as PluginConfig)) {
if (plugin.public !== false) {
const pluginPublicPath: string = path.normalize(
path.join(plugin.path, "public")
);
// 将public 放到searchLocations 中,当需要访问静态页面的时候通过searchLocations查找,然后sendFile
if (
fs.existsSync(pluginPublicPath) &&
api.staticFile.searchLocations.indexOf(pluginPublicPath) < 0
) {
api.staticFile.searchLocations.push(pluginPublicPath);
}
}
}
log(
"static files will be served from these directories",
"debug",
api.staticFile.searchLocations
);
}
一些问题
- 覆盖问题 an existing xxxx with the same name
xxxx
will be overridden by the file
如下
actionherojs 使用了插件优先的模式,本地程序的action 会被plugin 的覆盖,所以名称不重复很重要
说明
actionherojs 的插件机制,并不难,主要还是利用了import 动态加载的机制(ts 模式),对于commonjs 的模式为
let collection = await Promise.resolve().then(() => require(fullFilePath));
建议大家阅读下actionherojs 的源码,源码并不多,但是设计还是比较巧妙的,后边会对与reload 介绍下grouparoo,很多地方就使用到了这个而且很巧妙
很多地方使用了类似java classpath 查找的模式,实现了js 模块的动态加载
参考资料
https://www.actionherojs.com/tutorials/plugins
https://github.com/actionhero/ah-resque-ui