Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] egg-init refactor #2892

Open
atian25 opened this issue Aug 10, 2018 · 17 comments
Open

[RFC] egg-init refactor #2892

atian25 opened this issue Aug 10, 2018 · 17 comments
Assignees

Comments

@atian25
Copy link
Member

atian25 commented Aug 10, 2018

背景

目前的 egg-init 存在以下问题:

  • 脚手架逻辑集中化,全部在 egg-init 本身,作为全局命令,更新不方便。
  • 模板无法定制自己的逻辑,无法代码共享。
  • 没有 sub generator,如 egg-init add controller Test 这样的功能。
  • 脚手架只在项目初始化时用到,无法支撑升级功能,容易腐化和分裂。
  • 上层封装不方便,不支持 preset 。

方案

基础骨架

脱离 Egg 的独立骨架模块,common-boilerplate

  • 支持多级框架继承
  • 提供 TestUtils 测试辅助
  • 支持注册子命令
  • 骨架可以单独运行,不依赖 egg-bin 等引导工具
  • 提供生成骨架的骨架:boilerplate-boilerplate

目录结构:

./boilerplate-example
├── boilerplate
│   ├── lib
│   ├── test
│   ├── README.md
│   ├── _.eslintrc
│   ├── _.gitignore
│   ├── _package.json
│   └── index.js
├── test
│   └── index.test.js
├── index.js
├── README.md
└── package.json

骨架入口:

// index.js
const Boilerplate = require('common-boilerplate');

class MainBoilerplate extends Boilerplate {

  // 类似 egg 的方式来提供骨架路径,方便继承
  get [Symbol.for('boilerplate#root')]() {
    return __dirname;
  }

  // 交互式问答,基于 Inquirer,并对其进行扩展,方便测试
  initQuestions() {
    const questions = super.initQuestions();

    questions.push(
      {
        type: 'list',
        name: 'type',
        message: 'choose your type:',
        choices: [ 'simple', 'plugin', 'framework' ],
      }
    );
    return questions
  }
};

module.exports = MainBoilerplate;
module.exports.testUtils = Boilerplate.testUtils;

模板渲染

  • 内置支持简单的渲染, {{ name }}
  • 文件名也支持 {{name}}.test.js 形式
  • 通过覆盖 renderTemplate 方法可以支持 nunjucks 之类的渲染引擎
const nunjucks = require('nunjucks');

// could disable auto escape
nunjucks.configure({ autoescape: false });

class MainBoilerplate extends Boilerplate {
  async renderTemplate(tpl, locals) {
    return nunjucks.renderString(tpl, locals);
  }

  // custom your locals
  async initLocals() {
    const locals = await super.initLocals();
    locals.foo = 'bar';
    return locals;
  }
};

模板继承

// share.js
class ShareBoilerplate extends Boilerplate {
  // must provide your directory
  get [Symbol.for('boilerplate#root')]() {
    return __dirname;
  }
};

// child.js
class MainBoilerplate extends ShareBoilerplate {
  // must provide your directory
  get [Symbol.for('boilerplate#root')]() {
    return __dirname;
  }

  // example for ignore some files from parent
  async listFiles(...args) {
    const files = await super.listFiles(...args);
    files['github.png'] = undefined;
    return files;
  }
};

单元测试

扩展了 coffee,提供 CLI 的测试支持。

const testUtils = require('common-boilerplate').testUtils;

describe('test/index.test.js', () => {
  it('should work', () => {
    return testUtils.run()
      // .debug()
      .waitForPrompt()
      // answer to the questions
      .write('example\n')
      // emit `DOWN` key to select the second choise
      .choose(2)

      // expect README.md to be exists
      .expectFile('README.md')

      // check with `includes`
      .expectFile('README.md', 'this is a desc')

      // check with regex
      .expectFile('README.md', /desc/)

      // check whether contains
      .expectFile('package.json', { name: 'example' })

      // opposite assertion
      .notExpectFile('not-exist')
      .notExpectFile('README.md', 'sth')

      // see others at `coffee` docs
      .expect('stdout', /some console message/)
      .expect('stderr', /some error message/)
      .expect('code', 0)

      // don't forgot to call `end()`
      .end();
  });
});

egg-init

egg-init 极简化:

  • 引导工具
  • 全局安装,功能尽量简单,无需升级。
  • 继承 common-bin
  • 判断指定目录
    • 空目录:根据全局配置,提示用户可用的 boilerplate 列表(preset),安装并执行对应的骨架。
    • 非空目录:读取 package.jsonboilerplate 节点,执行对应的骨架。
  • 支持 preset ,可以不再需要封装 @ali/egg-init
$ egg-init --npm=tnpm 
$ egg-init --registry='https://registry.npmjs.org' 
$ egg-init --preset='egg-init-config' --type=simple
$ egg-init --package=egg-boilerplate-simple
$ egg-init --template=/path/to/boulerplate
$ egg-init add controller Test

配置文件

  • 启动时将读取配置文件,作为 argv 的默认值。
  • 读取顺序:package.jsonboilerplate  -> ~/.egg-init
  • 也可以命令行传递: egg-init --config=/path/to

项目配置: package.json

{
  "name": "egg-showcase",
  "boilerplate": {
    "name": "egg-boilerplate-simple",
    "version": "2.0.0",
    "npm": "tnpm",
    "registry": "https://registry.npmjs.org"
  }
}

全局配置: 支持 yml / json 等格式

# ~/.egg-init

npm: 'tnpm'
registry: 'https://registry.npmjs.org'
proxy: '127.0.1.1:8888'
preset:
  - @ali/egg-init-config
  - egg-init-config

egg-init-config

骨架列表集合,用于 --preset 参数。

仅需在 package.json 中包含 config.boilerplate 字段即可。

  • package - npm 包名
  • description - 描述
  • category - 分类,可选
{
  "name": "egg-init-config",
  "version": "1.3.0",
  "description": "egg init boilerplate config",
  "config": {
    "boilerplate": {
      "simple": {
        "package": "egg-boilerplate-simple",
        "description": "Simple egg app boilerplate"
      },
      "ts": {
        "package": "egg-boilerplate-ts",
        "description": "Simple egg && typescript app boilerplate",
        "category": "typescript"
      }
   }
}

伪代码

// egg-init
const { Command } = require('common-bin');

class EggInitCommand extends Command {
  * run({ argv, cwd }) {
    // 读取配置文件
    argv = this.normalize(argv);

    const dir = argv.dir;
    let boilerplateName = argv.type;
    let action;
    // 如果目标目录不存在,则视为初始化行为
    if (!fs.existSync(dir)) {
      // 安装 boilerplate
      this.npmInstall(boilerplateName, dir);
      action = 'init';
    } else {
      // 从 pkg 读取当前应用使用的骨架
      boilerplateName = this.getPkgInfo(dir, 'boilerplate.name');
      // egg-init add <type> <name>
      action = argv._[0];
    }
    // 执行 boilerplate
    const boilerplate = require(path.join(dir, 'node_modules', boilerplateName));
    yield boilerplate.run({ action, argv, cwd } );
  }
}

module.exports = EggInitCommand;

egg-boilerplate-base

提供一个 egg-boilerplate-base 基础骨架,方便开发者继承使用。

  • 默认注册常用的命令,开发者可以覆盖
    • add controller
    • add service
    • add config
    • add plugin
  • 提供模板渲染 (nunjucks)
  • 提供 helper (或者考虑仅推荐,不集成,开发者自行引入)
    • 提供 egg-ast-utils 辅助代码修改和升级
    • 提供 mrm-core 相关功能

子命令

还没想好怎么做。

是基于 common-bin 的 sub command 还是作为 boilerplate 的一个方法,如 addXX() ?

还有就是跟 egg-bin generator 有点相关,很多子命令其实更应该由插件来提供,如 addModel 之类的,它的模板应该是在对应的插件里面。

所以 egg-initegg-bin generator 是可以考虑联动的,譬如 addModel 的时候,是固定读插件里面的某个约定的文件,或者执行某个脚本。

egg-boilerplate-legacy

用于兼容旧版本的骨架,引导安装,实现旧版 egg-init 的安装逻辑。

egg-init 新版源码里面,判断用户选择的骨架是否符合新规范,不符合的话,安装 egg-boilerplate-legacy 并引导安装。

@atian25
Copy link
Member Author

atian25 commented Aug 10, 2018

@fengmk2
Copy link
Member

fengmk2 commented Aug 10, 2018

好赞!

@Runrioter
Copy link

Runrioter commented Aug 10, 2018

Egg当前确实需要这个

@popomore
Copy link
Member

Add nyc to package.json by default

  "nyc": {
    "check-coverage": true,
    "statements": 76,
    "branches": 44,
    "functions": 71,
    "lines": 76,
    "exclude": [
      "app/dal/dao/base"
    ]
  },

@popomore
Copy link
Member

  1. 全局命令建议用 egg
  2. 配置使用 ./egg/config,方便之后扩张目录
  3. 子命令和 generator 是两个维度的,generator 就是其中一个子命令
  4. 子命令放脚手架不是很好,不容易更新,还是跟着项目走,放 egg-bin 里,可以做个命令映射。

@popomore
Copy link
Member

应用代码升级的问题好像没说

@atian25
Copy link
Member Author

atian25 commented Aug 10, 2018

  • 上面提到,子命令跟着插件走
  • cli 可以考虑用 egg,统一调用 egg-init 和 egg-bin,后者其实我也想拆解掉。
  • 升级这块没啥想法,你补充下?

@popomore
Copy link
Member

egg-bin 跟着项目走比较好,插件是全局的?

@atian25
Copy link
Member Author

atian25 commented Aug 11, 2018

插件指的是 egg 插件,就像 generator 那样,可以在 app/scripts 目录里面支持 generator.js / boilerplate.js 的方式来执行子命令。

譬如 egg-mongooes 的 boilerplate 放在插件里面最合适

@atian25
Copy link
Member Author

atian25 commented Aug 24, 2018

@popomore

配置使用 ./egg/config,方便之后扩张目录

指的是 ~/.egg/config/init.yml ?

插件指的是 egg 插件,就像 generator 那样,可以在 app/scripts 目录里面支持 generator.js / boilerplate.js 的方式来执行子命令。譬如 egg-mongooes 的 boilerplate 放在插件里面最合适

这个有什么问题不?

@popomore
Copy link
Member

~/.egg/config.yml 就好了

@popomore
Copy link
Member

我觉得生成器作为单独命令比较好。

首先脚手架模版肯定是独立,放插件里有变成了鸡蛋问题。工具类不一定都适合放插件里,比如我们原来默认的 dev/test 属于哪个插件?放插件的可能是代码生成器和更新工具,这两个东西虽然场景不同,实属于同一个东西。

@atian25
Copy link
Member Author

atian25 commented Aug 25, 2018

你理解错我的意思了,egg-bin 拆分那个只是考虑如何扩展,可能是 egg 这个全局引导命令,做一个映射,跟之前没区别,不会放到插件里面,这个先不在这展开,回头再另开。

我指的是:

  • 某个插件提供的 generator 指令
  • 某个插件特有的模板,如 egg-hsf 的 template
  • 某个插件特有的模板更新 upgrade

这几个放到插件里面合适,因为他们是跟插件强相关的。而放到骨架里面就不太合适了,如 simple 这个骨架,不能把 hsf 的 template/ sub generator(egg-init add hsf --name=User) 放进去

@atian25
Copy link
Member Author

atian25 commented Aug 25, 2018

回到 RFC 本身,现在有两个问题我还没想清晰,需要讨论下:

  • sub generator 如何实现?
    • 通用的 controller 可以做到 egg-boilerplate-base ,并提供扩展机制,也允许子模板覆盖
    • 非通用的,如上面提到的 hsf,应该是模板放到插件里面,然后骨架这边做一个映射来调用 (如 app/generator/boilerplate.js)
  • upgrade 怎么做

@popomore
Copy link
Member

明白了,那没问题。

通用的生成可以放到框架?就是考虑在哪个层面的就放到哪个 loadUnit?但是这个就不要考虑继承和依赖了,太复杂,可以通过配置的方式扩展。

更新我觉得跟生成是一个思路,只是需要根据一个标准来更新,比如什么版本到什么版本会做什么更新,现在想想有点复杂。

@atian25
Copy link
Member Author

atian25 commented Aug 25, 2018

  • common-boilerplate
    • boilerplate-boilerplate
    • egg-boilerplate-base
      • egg-boilerplate-simple
        • egg-boilerplate-chair
      • egg-boilerplate-plugin
      • egg-boilerplate-framework
    • egg-boilerplate-simple-ts

大概是这样的继承关系, base 里面实现 controller/service 等通用的

ts 里面实现 ts 版通用的,(到时再看有没有必要来个 base-ts )

框架的,目前还不是很清晰,倾向于先放到独立的 boilerplate,未来有需要再看看是抽象还是集成到框架。

更新的感觉挺复杂的,还想不清晰,先搁置。 egg-boilerplate-legacy 倒是可以实践下,一方面支持安装旧模板,一方面模板开发者可以用它来升级

@atian25
Copy link
Member Author

atian25 commented Sep 27, 2018

补充下,有 npm init xxx 这个机制,可以注册 create-egg / create-egg-simple 这样的 npm 包,然后开发者就可以直接 npm init egg-simple 的方式调用执行了,可以作为一个简化的方式。

image

https://github.com/eggjs/create-egg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants