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

[iceworks]iceworks adapter design #1935

Merged

Conversation

@chenbin92
Copy link
Collaborator

chenbin92 commented May 14, 2019

iceworks server 适配器插件模式

背景:由于 adapter 包含项目和工程两个维度,每个维度下功能较多,不可能将所有的功能都以一套接口实现且必须实现所有的接口,应该是可按需实现对应的接口进行适配,在此背景下需要对 adapter 进行插件模式的设计

adapter interface 定义

adapter 目录结构规范

.
├── index.ts
├── dependency
│   └──index.ts
├── dev
│   ├── const.ts
│   └── index.ts
└── page
    └── index.ts

按需配置

import * as path from 'path';

const config = {
  dependency: {
    enable: true,
    path: path.join(__dirname, './dependency'),
  },

  page: {
    enable: true,
    path: path.join(__dirname, './page'),
  },

  dev: {
    enable: true,
    path: path.join(__dirname, './dev'),
  },
};

export default config;

应用适配器插件

/**
 * load adpater
 * @param projectInfo Object
 */
const loadAdapter = (projectInfo) => {
  const adapters = {};
  for (const [key, value] of Object.entries(adapterConfig)) {
    // whether to enable
    if (!value.enable) return;

    // adapter must be an class or fn
    // TODO: fn support
    const adapterPath = require.resolve(value.path);
    const AdapterName = require(adapterPath);
    adapters[key] = new AdapterName.default(projectInfo);
  }

  return adapters;
};

export default loadAdapter;

示例

实现依赖功能

import * as EventEmitter from 'events';
import { IProjectDependencySchema } from '../interface';

export default class Dependency extends EventEmitter {
  public readonly projectPath: string;

  constructor(options) {
    super();
    this.projectPath = options.projectPath;
  }

  async getDependencies(): Promise<IProjectDependencySchema[]> {
    return [
      {
        package: 'icestore',
        dev: false,
        specifyVersion: '^0.1.0',
      },
    ];
  }
}
@chenbin92 chenbin92 changed the title [iceworks]feat iceworks server implement plugin pattern [WIP][iceworks]feat iceworks server implement plugin pattern May 14, 2019
@imsobear

This comment has been minimized.

Copy link
Collaborator

imsobear commented May 14, 2019

设计原则

  • 简洁简单
    • 每个方法都是无状态的,固定输入与输出,抛弃 Class 的写法
    • 不约定 adapter 的目录结构,未来有需求可以产出最佳实践如示例 2
  • 每个方法的第一个参数固定为上下文 ctx:当前项目信息、通用 API等

示例

以 ice-scripts-adapter 为例:

// lib/index.js

// 页面操作相关
export const page = {
  enable: true,
  get: (ctx) => {},
  delete: (ctx, pageName) => {},
  create: (ctx) => {}
};

// 工程命令相关
export const dev = {
  enable: true,
  start: (ctx, cliOptions) => {
    const process = spawn('xxxx');

    process.on('data', (data) => {
      ctx.emit('dev:output', data);
    });
    process.on('exit', (data) => {
      ctx.emit('dev:stop');
    });
  },
  stop: (ctx, pageName) => {
	const process = spawn('xxxx');
  },
};

export const dependency = {
  add: (ctx) => {},
  delete: (ctx) => {}
};

或者:

// lib/index.js
const page = require('./page');
const dev = require('./dev');
const dependency = require('./dependency');

export {
  page, dev, dependency
}
chenbin92 added 2 commits May 14, 2019
@chenbin92

This comment has been minimized.

Copy link
Collaborator Author

chenbin92 commented May 14, 2019

  1. 设计简洁简单,每个方法都是无状态的,固定输入与输出,抛弃 Class 的写法

设计简洁简单不一定要抛弃 Class 的写法, 函数式编程是一种可选的方式但不是必须的, 我们可以推荐使用 fn 的方式,但抛弃 Class 限制开发者不能写 Class 不太友好,也无法基于 ice-scripts adapter 实现继承

  1. 不约定 adapter 的目录结构,未来有需求可以产出最佳实践如示例 2

不是未来有需求,是现在我们自己实现 ice-scripts adapter 就有需求,比如实现 页面列表 就包括 CRUD 等,不可能放在一个 lib/index.js 去实现诸如 page, dev, dependency 等等的功能,肯定是需要分功能模块去实现。

  1. 上述方式二
// lib/index.js
const page = require('./page');
const dev = require('./dev');
const dependency = require('./dependency');

export {
  page, dev, dependency
}

方式二与下面约定配置的区别本质就是要不要帮开发者去 load 文件的过程并进行约定,并无其他太大差异:

const config = {
  dependency: {
    enable: true,
    path: path.join(__dirname, './dependency'),
  },

  page: {
    enable: true,
    path: path.join(__dirname, './page'),
  },

  dev: {
    enable: true,
    path: path.join(__dirname, './dev'),
  },
};

解释下为什么有这种设计:

  • 通过配置的组织方式在设社区是非常常见的一种方式,如 egg.js/midway.js/next.js 等,而 iceworks-server 本质上也是 Midway 应用,对文件进行约定配置也符合整体的设计
  • 通过配置在内部可以使用 key 当做 namespace 进行模块功能的隔离,在外层可以是 fn 也可以是 class 进行实现由用户自行决定,而不是在一个 adapter 上挂载 50+ 方法(未来还会扩展)
  1. 每个方法的第一个参数固定为上下文 ctx:当前项目信息、通用 API等

赞成 +1


综上:

  1. 按照模块功能分文件实现接口,优先使用对象组织的形式 😂(也可使用 Class)
// in adapter/page.ts

Class Page {
   async  get(ctx) { },
   async  get(ctx) { },
   async  get(ctx) { }
}

or


const Page = {}

page.get = async (ctx) =>{ }
page.delete = async (ctx) =>{ }
page.create= async (ctx) =>{ }

export default Page;
  1. adapter 入口文件如下
import page from './page';
import dev from './dev';
import dependency from './dependency';

// 对应约定好的 Interface,如 page、dev、dependency 等等(可按需实现)
const adapter = {
  page,
  dev,
  dependency
}

export default adapter

Interface 定义

export  interface IAdapter = {
  page: IPage,
  dev: IDev,
  dependency: Idependency,
}

export interface IPage = {
  // code
}
  1. 调用方式如下:
// namespace: page
adapter.page.xxx()

// namespace: dev
adapter.dev.xxx)

// namespace: dependency
adpater.dependency.xxx()
@chenbin92

This comment has been minimized.

Copy link
Collaborator Author

chenbin92 commented May 14, 2019

@imsobear 还需要考虑基于 ice-scripts adapter 的继承关系,也需要支持 Class 的写法

@imsobear

This comment has been minimized.

Copy link
Collaborator

imsobear commented May 15, 2019

@chenbin92

  • Class 的设计在这里我觉得没有意义,你的那个例子里应该还需要加一句 export default new Page()
  • 继承问题:对象字面量就是用 merge 的方式来实现,没 Class 看起来优雅,但是能力上是满足的
  • 继承问题我觉得可以考虑 iceworks 层面做掉,单个 adapter 不需要通过 npm 包依赖其他 adapter(会引入版本问题),单个 adapter 只需要声明下自己继承的是哪个 adapter(字符串):
const adapter = { page, dependency };
adapter.extends = 'ice-scripts-adapter';
export default adapter;
@imsobear

This comment has been minimized.

Copy link
Collaborator

imsobear commented May 15, 2019

这里的继承跟 OOP 里的继承不是一个概念哈,本身需求是:官方实现一个 ice-scripts-adapter,然后有的团队使用的是 ice-sciprts 但是目录结构跟我们默认的不一样,因此他需要在 ice-scripts-adapter 的基础上自定义一些方法

@wssgcg1213

This comment has been minimized.

Copy link
Collaborator

wssgcg1213 commented May 15, 2019

设计原则: 在满足需求前提下, 规范约束越少对开发者的束缚越少
参考: rax driver 的设计,约束是纯对象,每个方法的职责清晰,开发者可以用 export xxx 的方式 直接导出简单的driver driver-dom,也可以用 export default new Class 的方式进行复杂结构的组织driver-worker

@wssgcg1213

This comment has been minimized.

Copy link
Collaborator

wssgcg1213 commented May 15, 2019

这里的继承跟 OOP 里的继承不是一个概念哈,本身需求是:官方实现一个 ice-scripts-adapter,然后有的团队使用的是 ice-sciprts 但是目录结构跟我们默认的不一样,因此他需要在 ice-scripts-adapter 的基础上自定义一些方法

那应该是对adaper定制的能力吧,如果adapter里面的逻辑足够强通过配置也能搞定

@alvinhui

This comment has been minimized.

Copy link
Collaborator

alvinhui commented May 15, 2019

为什么会有这个 PR

先说一说背景,背景是在 Iceworks 3.x 的迭代过程中,我们发现:

  • Iceworks Adapter 中的规约越来越多,其 interface 的设计已经有 700 多行。详尽地罗列 API 后,我们发现这不利于 Adapter 实现者的阅读和理解,同时不禁反问:某个 Adapter 真的要实现完所有的 API 吗?
  • 在实现「项目依赖管理」、「项目页面管理」、「项目启动调试」的过程中,我们发现,这些功能都可以按照「功能」的维度进行划分归类且彼此之间是没有关联的。
  • 继续延伸,如果按照「功能模块」去划分,那么某一种 Adapter 其可能有「路由管理」、「Mock 管理」的实现,也可能没有。

所以我们需要解决的问题是:怎么去组织和规约 Adapter 让其可以按照功能划分模块实现,并被 Iceworks 所识别

Adapter 是什么

Adapter 到底是什么,它包含了哪些职责?

在 Iceworks 2.x 里,没有针对某个项目的「工程命令执行」和「项目文件操作」进行明确的封装,其对「工程」和「项目」的操作分散在了主进程和渲染进程的各处。 @noyobo 显然意识到了这一点,已经抽象了 iceworks-scaffolder 这个工具,以及 Iceworks 和模板之间的协议

Iceworks 以项目的维度覆盖前端开发的完整生命周期。这个生命周期里,包含了项目的生成、项目的本地开发、项目的发布三个主要阶段。

Iceworks 的项目生成依赖模板,而模板是通过 ice-devtools 开发的(《已有项目接入 Iceworks》

模板里包含了什么?

  • 包含了我们物料体系里模板、布局、页面、区块、组件的概念和实现;
  • 包含了菜单、路由等最佳实践;
  • 包含了本地开发的命令约定:start, build。

所以,我个人的理解,Adapter 对应着的是前端项目的最佳实践,可以说是对前端应用框架(对标 umijs)的一层封装适配。只是 ICE 体系里没有「前端应用框架」这样的概念和封装。

关于用类还是用对象

这里有两个维度:

  • 维度一是 Adapter 应该是个类还是个对象;
  • 维度二是 Adapter 的功能模块是个类还是个对象。

在 Adapter 的第一版设计中,是个类。我在 PR 中 没有详细介绍这样设计的理由。

类是面向对象中的概念,主要特征是抽象性、封装性和继承性。在第一版设计中,体现了其抽象性和封装性 —— Iceowrks 进行多项目的管理,Adapter 是项目的持有者,维持着项目的状态及可进行项目的操作:

  • 项目当前的「调试状态」是怎样的:启动中、运行中、还是无?
  • 通过怎样的一组 API 对项目进行管理调试?

这里其实一个 Iceworks 对应 (1-n) 多个项目。

综上所述

Adapter 约定

interface IAdapterModule extends EventEmitter {
  
}

interface IAdapterPage extends IAdapterModule {

}

interface IAdapterDependency extends IAdapterModule {

}

interface IAdapter {
  Page: IAdapterPage;
  Dependency: IAdapterDependency;
}

Adapter 实现

import * as EventEmitter from 'events';
import { IAdapterPage } from 'iceworks-adapter';

export default Class Page extends EventEmitter implements IAdapterPage {
  public async getAll() { },
  public async getOne() { },
  public async create() { },
  public async delete() { }
}
import Page from './page';
import Dependency from './dependency';
import { IAdapter } from 'iceworks-adapter';

export default  {
  Page
  Dependency
}

使用

普通:

const project = new Project(adapter);
await project.page.getAll();

事件:

const project = new Project(adapter);
await project.dev.start();

project.dev.on('data', () => {
  // ...
});
@chenbin92

This comment has been minimized.

Copy link
Collaborator Author

chenbin92 commented May 15, 2019

Adapter 本质上是某一类型前端项目的最佳实践的抽象,通过 Adapter 机制可以接入 iceworks 进行可视化的项目工程管理,定制专有的前端工作台

@chenbin92 chenbin92 changed the title [WIP][iceworks]feat iceworks server implement plugin pattern [WIP][iceworks]iceworks adapter design May 15, 2019
@chenbin92

This comment has been minimized.

Copy link
Collaborator Author

chenbin92 commented May 15, 2019

结论

Adapter Interface 设计

目录结构

按照 iceworks 的功能模块进行接口设计,目录结构如下:

接口定义

.
├── base.ts
├── build.ts
├── component.ts
├── configuration.ts
├── dependency.ts
├── dev.ts
├── index.ts
├── layout.ts
├── menu.ts
├── mock.ts
├── page.ts
├── router.ts
└── todo.ts

模块接口基类

import * as EventEmitter from 'events';

/**
 * 功能模块的基类
 */
export interface IBaseModule  {
  readonly projectName: string;
  readonly projectPath: string;
}

模块接口示例

所有模块接口优先使用诸如 getAllgetOnecreateupdatedelete 等方法名称进行定义

import { IBaseModule } from './base';

/**
 * 项目的路由
 */
export interface IRouter {
  /**
   * URL 路径
   */
  path: string;

  /**
   * 页面名
   */
  pageName: string;
}

// 继承基类
export interface IRouterModule extends IBaseModule {
  /**
   * 获取项目路由
   */
  getAll(): Promise<IRouter[]>;

  /**
   * 添加路由
   *
   * @param router 路由配置
   */
  create(router: IRouter): Promise<IRouter>;

  /**
   * 添加多个路由到项目
   *
   * @param routers 多个路由配置
   */
  creates(routers: IRouter[]): Promise<IRouter[]>;

  /**
   * 删除路由
   *
   * @param path 路由路径
   */
  delete(path: string): Promise<void>;

  /**
   * 更新路由
   *
   * @param router 路由配置
   */
  update(router: IRouter): Promise<IRouter>;
}

Adapter 入口设计

export * from './layout';
export * from './page';
export * from './menu';
...

Adapter 核心实现

loadAdapter

加载 Adapter, 现阶段只考虑 ice-scripts 工程的 adapter,后续支持从项目的 package.json 中获取 adapter 进行适配

import * as adapter from './adapter';
import camelCase from 'camelCase';

/**
 * load adapter
 * @param context the current project information
 */
const loadAdapter = (context) => {
  const mods = {};
  for (const [key, Mod] of Object.entries(adapter)) {
    mods[camelCase(key)] = new Mod(context);
  }

  return mods;
};

export default loadAdapter;

projectManager

项目管理器,负责将 adapter 的功能模块应用挂载到当前项目进行管理,包含当前项目的信息

class ProjectManager extends EventEmitter {

  async ready() {
   // 从缓存中获取所有的项目
    const projects = storage.get('projects');
    this.projects = await Promise.all(
      projects.map(async (project) => {
        return {
          //  调用 adapter,并将当前项目信息作为上下文传递给对应的功能模块
          ...loadAdapter(this),
        };
      })
    );
  }

  /**
   * Get current project
   */
  getCurrent() {
    const projectInfo = storage.get('project');
    return this.getProject(projectInfo.projectPath);
  }

 ...
}

Adapter 模块实现示例

实现页面列表模块

import { IPageModule } from '@interface';

// 实现 IPageModule 接口
export default class Page implements IPageModule {
  public readonly projectPath: string;

  public readonly projectName: string;

  async getAll() : Promise<any> {}

  async getOne(): Promise<any> {}

  async create(): Promise<any> {}

  async creates(): Promise<any> {}

  async delete(): Promise<any> {}

  async update(): Promise<any> {}

  async getBlocks(): Promise<any> {}

  async createBlock(): Promise<any> {}

  async createBlocks(): Promise<any> {}
}

Adapter 入口导出

import Page from './page';

export {
   Page,
   ...
}
@chenbin92 chenbin92 changed the title [WIP][iceworks]iceworks adapter design [iceworks]iceworks adapter design May 16, 2019
@chenbin92 chenbin92 requested a review from alvinhui May 16, 2019
pages.refresh(newProject.dataSource.folderPath);
dependencies.refresh(newProject.dataSource.folderPath);
await projects.remove(projectPath);
await project.refresh();

This comment has been minimized.

Copy link
@alvinhui

alvinhui May 16, 2019

Collaborator

下面的代码不应该删除

@alvinhui alvinhui merged commit bdfbb79 into iceworks/release-3.x May 16, 2019
4 checks passed
4 checks passed
WIP Ready for review
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
license/cla Contributor License Agreement is signed.
Details
@delete-merged-branch delete-merged-branch bot deleted the feat-iceworks-server-implement-plugin-pattern branch May 16, 2019
@alvinhui alvinhui mentioned this pull request May 28, 2019
14 of 14 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.