egg-artisan: 提供一个基于egg的命令行运行模式。
前言
目前 egg 是没有基于开发业务功能的命令行运行模式的,比如我想在命令行执行个命令来处理一些业务相关的事,像数据库处理,缓存操作,文件操作等。当然egg-bin
和egg-scripts
也是命令行模式,但它们主要是项目维护,集成等作用,不是基于开发功能的。在 GitHub 找了好久没找到,于是就有个实现该想法的念头。
简介
egg-artisan 是基于 common-bin 的,为什么是 common-bin ?只能说命令行包这块自己也就对 common-bin 比较了解(如果有更好或更适合的欢迎提出),但功能上已经足够用了。egg-artisan
在run()
方法里注入了this.ctx
,于是命令行也就有了操作 egg 项目内服务的能力,即能处理项目内业务相关的事。使用上,我们可以通过执行 npm run artisan xxx
来处理一些事。额外,也提供了app.runArtisan(command, argvs)
方法使得能够在程序内也能调用。
过程中学习到了一些内容:
- common-bin, yargs 的学习
- egg 的启动流程
- egg-schedule, egg-mock 的学习
以下是具体README,尝试了把纯英文,有疑问或建议欢迎指出,谢谢!
Install
$ npm i egg-artisan --save
Mount
// {app_root}/config/plugin.js
exports.artisan = {
enable: true,
package: 'egg-artisan',
};
Features
egg-artisan
provides a cli running mode for egg. In the root directory, you can do something by executing commands likenpm run artisan xxx
, such as operating file, manipulating database scripts, updating cache scripts, etc.
egg-artisan
based on common-bin(based on yargs), to provide more convenient usage, as detailed below.
Usage
egg-artisan
requires cli file to be stored in app/artisan
, as shown below, test.js
, clone.js
.
egg-project
├── app
│ ├── artisan
│ | ├── test.js
│ | └── clone.js
│ ├── controller
| ├── router.js
| | ...
├── package.json
├── config
├── test
├── app.js (可选)
├── ...
How to write command
Let’s take test.js as an example, for the file operation.
As you can see, the usage of the command is the same as that of common-bin
, because egg-artisan
extends common-bin
. In addition, egg-artisan
injected ths.ctx
into the run method, so you can get anonymous context with ths.ctx
.
You can see common-bin, http://yargs.js.org/docs for more detail.
// {app_root}/app/artisan/test.js
'use strict';
const Command = require('egg-artisan');
class TestCommand extends Command {
constructor(rawArgv) {
super(rawArgv);
this.yargs.usage('test command');
this.yargs.options({
a: {
type: 'string',
description: 'test argv: a description',
},
});
}
async run({ argv }) {
const aa = argv.a || '';
const bb = argv.b || '';
const cc = argv._.join(',');
await this.ctx.service.file.write(`argv: ${aa}${bb}${cc}`);
const con = await this.ctx.service.file.read();
console.log('argv', argv);
return con;
}
get description() {
return 'test description';
}
}
module.exports = TestCommand;
Add egg-artisan
to package.json scripts:
{
"scripts": {
"artisan": "egg-artisan"
}
}
Run the test command
- Show help, the following image has 2 custom commands:
test.js
,clone.js
.
$ npm run artisan
// The following is the same
// npm run artisan -- -h
// npm run artisan -- help
Why use
--
? you can see http://www.ruanyifeng.com/blog/2016/10/npm_scripts.html.
- Show
test.js
command help
$ npm run artisan -- test -h
// The following is the same
// npm run artisan -- test help
- Run
test.js
command
$ npm run artisan -- test -x=1 --type=2 a b
// The following is the same
// npm run artisan -- test -x=1 --type 2 a b
// npm run artisan -- test -x 1 --type 2 a b
Call the artisan command inside the project
egg-artisan
provides app.runArtisan(artisanCommand, [argvs])
for running some commands inside the project. app.runArtisan(artisanName, [argvs])
accepts two parameters:
- artisanCommand: Relative path or absolute path in the app/artisan directory, such as
test
,{app_root}/app/artisan/test
; You can also append parameters, such astest -x=1 --type=2
,{app_root}/app/artisan/test -x=1 --type=2
. - argvs: command argvs, will be parsed and appended to
artisanCommand
. support object, array, such as[ 'a', 'b' ]
,{ '--a': 1, '--b': 2 }
,{ a: true, '--b': 2 }
.
Example:
// {app_root}/app/controller/home.js
'use strict';
const BaseController = require('./base');
class HomeController extends BaseController {
async index() {
await this.app.runArtisan('test', { '-a': 1 })
}
module.exports = HomeController;
more usage, reference npm run artisan
:
npm run artisan -- test
app.runArtisan('test')
npm run artisan -- test -a=1
app.runArtisan('test -a=1')
app.runArtisan('test', { '-a': 1 })
npm run artisan -- test a b
app.runArtisan('test a b')
app.runArtisan('test', [ 'a', 'b' ])
npm run artisan -- test -a=1 --bb=2
app.runArtisan('test -a=1 --bb=2')
app.runArtisan('test', { '-a': 1, '--bb': 2 })
npm run artisan -- test -a=1 --bb=2 cc
app.runArtisan('test -a=1 --bb=2 cc')
app.runArtisan('test -a=1', { '--bb': 2, cc: true })
app.runArtisan('test', { '-a': 1, '--bb': 2, cc: true })
advanced usage
-
Combined
egg-schedule
// {app_root}/app/schedule/xxx.js const Subscription = require('egg').Subscription; class ClusterTask extends Subscription { static get schedule() { return { type: 'custom', }; } async subscribe(data) { await this.ctx.app.runArtisan('test', { '-a': 1 }); } }
结语
如有问题或有优化的点,欢迎pr, star。
好棒,希望egg 社区越来越完善。
这个是不是可以理解成,能够测试一些内部不属于http请求处理的业务?
@jeremy16601 谢谢,大家一起维护,定会越来越完善。
@HobaiRiku 确实是可以的,当然论测试,egg-mock 也是可以的,也是官网推荐的
感谢分享。
一开始我以为楼主是实现了这个 RFC:建立一个运行时给 worker 下发命令的机制
后面看了下 README 好像不太像,我感觉你其实是想手动跑一些 CMD 指令,然后希望能调用到 egg service 里面的一些逻辑,这点我们之前是考虑在上面那个 RFC 的方式实现的。
然后有 2 个地方感觉有点奇怪:(没细看,只是手机瞟了下)
- 自己写了简化版的 egg loader,这个有可能会导致最后的运行效果跟 egg 里面不一致,手动测试不一定就是真测试。
- 在 egg 应用里面反过来调用这些脚本,感觉有点奇怪。
@atian25 感谢回复。
这个确实不是实现那个 RFC,功能上和你描述的差不多,当时的初衷就是想通过手动执行下脚本来修复项目数据,后面为了程序能动态修复这个问题,所以在某个条件下会在程序里调用这些脚本,其实就是执行脚本里的 run()
, 这么看来场景还是有的,也就解释了你说的第二点奇怪。
对于第一点奇怪,运行效果上目前是可以一致的,只是未来 egg 不断迭代中如果相关底层有变化,这个插件也要紧跟着迭代。 另外“手动测试不一定就是真测试“具体是指哪方面,还没 get 到点:)
其实就是执行脚本里的 run(), 这么看来场景还是有的,也就解释了你说的第二点奇怪。
如果这样,其实应该换个角度来看:
- 这个目录的脚本,本身属于 egg 的一个约定扩展,是可以作为一个独立插件的范畴的
- 然后我们只需要有一种手段,可以通过 cli 来调用 egg 里面的某个 service 或扩展的东西,这个就是我提到的那个 RFC 的范畴了。
一个是 Loader 约定扩展,一个是 CLI,目前 2 个耦合在一起会让我觉得有点怪怪的。
对于第一点奇怪,运行效果上目前是可以一致的,只是未来 egg 不断迭代中如果相关底层有变化,这个插件也要紧跟着迭代。 另外“手动测试不一定就是真测试“具体是指哪方面,还没 get 到点:)
指的就是跟进迭代
其实你这里也可以不用自己写 loader,可以模仿 egg-mock 那样,真实的启动 egg,只不过是在一个进程里面,这样可以加速启动速度。
@atian25 恩恩,学习了。
其实这里就是参考 egg-mock 来实现的,一开始还直接通过下面代码来启动
const mock = require('egg-mock');
let app = mock.app();
await app.ready();
// ...
但后面感觉这个插件太沉重了,就从中剥离出 egg 的启动流程部分。