egg-artisan: 提供一个基于egg的命令行运行模式。
发布于 5 年前 作者 zzzs 4219 次浏览 来自 分享

egg-artisan: 提供一个基于egg的命令行运行模式。

github: https://github.com/zzzs/egg-artisan

前言

目前 egg 是没有基于开发业务功能的命令行运行模式的,比如我想在命令行执行个命令来处理一些业务相关的事,像数据库处理,缓存操作,文件操作等。当然egg-binegg-scripts也是命令行模式,但它们主要是项目维护,集成等作用,不是基于开发功能的。在 GitHub 找了好久没找到,于是就有个实现该想法的念头。

简介

egg-artisan 是基于 common-bin 的,为什么是 common-bin ?只能说命令行包这块自己也就对 common-bin 比较了解(如果有更好或更适合的欢迎提出),但功能上已经足够用了。egg-artisanrun()方法里注入了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 like npm 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.

1.png

  • Show test.js command help
$ npm run artisan -- test -h
// The following is the same
// npm run artisan -- test help

2.png

  • 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

3.png

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 as test -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。

github: https://github.com/zzzs/egg-artisan

8 回复

好棒,希望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 的启动流程部分。

回到顶部