San CLI UI ——不只是San CLI的GUI(原理篇)

266次阅读  |  发布于3年以前

本期继续 San CLI UI 主题,主要讲述 San CLI UI 实现原理、插件系统的实现,以及利用插件系统可以实现哪些个性化的定制功能。

前言

San CLI UI 的功能篇,我们已经介绍了 San CLI UI 的核心功能包括:项目管理、依赖管理、插件管理、任务管理、配置管理、仪表盘工具集等功能,那么这些功能是如何实现的?工具扩展如何集成的?请跟随我们的脚步深入了解。

阅读本篇有助于提升开发自定义插件时对 api 的使用理解。

首先将整个 San CLI UI 的实现中涉及到的技术点整理如下:

原理

整体架构

San CLI UI 的架构设计参考了 Vue CLI UI,在实现上整体可以分为三部分:client 端(也即浏览器端),server 端(服务端),底层数据存储。

对其中主要部分简要概括如下:

整个架构图的右侧是插件包示意图,插件内主要包含两部分:用于 San CLI UI 的 client 端加载的前端组件,以及用于 node 端读取的 ui.js 插件配置。

接下来我们来看一下 San CLI UI 的工作流程。

工作流程

如图所示 San CLI UI 数据流程,前后端的通讯主要通过 graphql 实现,而插件的加载主要通过插件系统。

San CLI UI 启动后,首先调用 plugin 初始化方法,加载全部的内置插件及用户开发的插件,在 client端通过 ClientAddonApi 注册并加载对应的组件,在 server端则通过 pluginManager 读取各个插件包内 ui.js 描述的插件配置,在操作中涉及到对项目的配置,则会操作本地项目进行读取并修改。

San CLI UI 中,通讯可概括为三个部分:在 client 和 server 端主要是通过基于 GraphQL 实现通讯;在 webpack 子进程间通过 I 方式进行通讯;在自定义插件中前端组件与 server端通讯则是通过 PluginAction 和 SharedData 机制。

接下来将主要介绍基于 GraphQL 的后端设计以及插件系统的设计。

基于 GraphQL 的 node 端设计

GraphQL 概述

首先来回答一个问题:什么是 GraphQL?

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data

在官方文档的定义中,GraphQL 是一种 API 的查询语言,也是一个使用现有数据来完成所有查询的运行时;它允许客户端能够准确地获得它需要的、没有冗余的数据;对于一个复杂数据,采用 REST API 需要进行多次请求,而对于 GraphQL 仅需一次请求即可获取全部数据,即使在移动端网速较慢的情况下,GraphQL 仍能保持很好的请求效率;GraphQL 的类型定义系统对接口进行了规范,为书写错误等提供友好的错误提示。

GraphQL 的优点:

GraphQL 的使用方式 GraphQL 的使用方式简单来说分三个步骤:

  1. 描述数据格式
  2. 请求所需的数据格式
  3. 得到可预测的结果

GraphQL 的核心概念

node 后端接口实现

接口的定义是前后端通信的一种约定,在使用 GraphQL 时,由 schema 实现,在 San CLI UI 中,整个服务端的目录如下:

server
├── api                       # plugin相关api
├── connectors                # 连接api具体实现
│      ├─ plugins.js
│      ├─ projects.js
│      ├─ configurations.js
│      └─ ...
├── resolver                   # 解析器
│      ├─ plugin.js
│      ├─ project.js
│      ├─ configuration.js
│      └─ ...
├── schema     # schema定义
│      ├─ plugin.js
│      ├─ project.js
│      ├─ configuration.js
│      └─ ...
├── main                       # GraphQL解析服务入口
├── utils                      # 工具类函数
├── modal                      # db及文件操作相关
└── index.js                   # 整个服务端入口

schema 定义与解析

GraphQL 服务端都有两个核心部分:Schema 和解析器 Resolver。

Schema 是可以通过 GraphQL 服务端获取的数据模型。它定义了允许客户端进行的查询,可以从服务端获取的数据类型,以及这些类型中的字段和之间的关系。以 folder 为例,Schema 定义如下:

extend type Query {
  folderCurrent: Folder
  foldersFavorite: [Folder]
  folderExists (file: String!): Boolean
}

extend type Mutation {
  folderOpen (path: String!): Folder
  folderSetFavorite (path: String!, favorite: Boolean!): Folder
  folderCreate(name: String!): Folder
}

type Folder {
  name: String!
  path: String!
  isPackage: Boolean
  isSanProject: Boolean
  favorite: Boolean
  children: [Folder]
  hidden: Boolean
}

Schema 告诉服务端允许客户端进行哪些查询,以及不同类型的关联方式,而解析器 Resolver 则告知每种类型的数据来自哪里。

const folders = require('../connectors/folders');
module.exports = {
    Query: {
        folderCurrent: (root, args, context) => folders.getCurrent(args, context),
        foldersFavorite: (root, args, context) => folders.listFavorite(context),
        folderExists: (root, {file}, context) => isDirectory(file)
    },
    ...
};

其中 connectors 负责执行具体的方法并返回结果。

class Folders {
    getCurrent(args, context) {
        const base = cwd.get();
        return generateFolder(base, context);
    }

    listFavorite(context) {
        return context.db.get('foldersFavorite').value().map(
            file => generateFolder(file.id, context)
        );
    }
    ...
}

San CLI UI 中,采用 apollo-client 与 apollo-server 实现 graphQL 通讯,在 client 端发起 query/mutate 请求后,由 apollo-server 进行解析并验证查询语法,验证通过后找到对应的 Resolver,并执行其 connectors 方法实现,将结果返回给 client 端。当订阅事件发生变化时,服务端推送对应的数据到 client 端,通过 subscribe 订阅的回调执行。

apollo 相关操作可参见:https://www.apollographql.com/docs/

插件系统设计

San CLI UI 插件

San CLI UI 中,仪表盘、配置管理、任务管理都是基于插件系统实现的,我们先来了解一下插件系统的相关概念。

插件系统的概念

定义

San CLI UI 插件是一个动态加载到 San CLI UI 中的 JS 包,能够为 San CLI UI 创建的项目添加额外的功能

插件的命名

为便于识别,插件包应以 san-cli-ui-<type>-<name> 作为的格式命名,这样做不仅便于 San CLI UI识别,同时便于其他开发者搜索发现。

npm 包基本结构:

如下所示,除满足一个 npm 包的基本要求外,每个插件需要包含一个 ui.js 文件,用于导出插件的相关配置信息

.
├── README.md
├── src
│    └── index.js // 组件注册
├── package.json
└── ui.js         // `San CLI UI` 集成(这里存放插件的配置信息)

其中最主要的两个文件:

San CLI UI 插件加载关键

San CLI UI 插件加载依赖于 server 端和 client 端的两个对象的配合,首先在 server 端加载插件的描述,将插件的 id 及路径返回到 client 端,client端负责加载并挂载组件到页面:

ui.js 文件配置

在每个安装好的San CLI UI插件里,San CLI UI都会尝试从其插件的根目录加载一个可选的ui.js文件。(也可以使用文件夹形式,例如 ui/index.js)。

ui.js 主要导出一个函数,函数会以 API 对象作为第一个参数:

module.exports = api => {
  // 在这里使用 API...
}

其中apiSan CLI UI传入,为PluginManager的实例,所有插件的扩展功能都是基于这个对象来实现,例如:api.registerConfig注册配置项、api.registerWidget注册 widget 部件、api.registerAddon注册插件 id 及加载路径等。

index.js

在用户自定义插件中,index 文件主要负责自定义组件的注册,以便后面加载并显示在 San CLI UI 中,例如欢迎部件的定义:

import Welcome from './components/welcome/welcome';
/* global ClientAddonApi */
if (window.ClientAddonApi) {
    ClientAddonApi.defineComponent('san.widgets.components.welcome', Welcome);
}

其中 ClientAddonApi 为 ClientAddon 的实例,在 San CLI UI 启动时,通过实例化 ClientAddon 将 ClientAddonApi 挂载至全局,供插件加载使用。

接下来将逐个讲解服务端对应的 PluginManager 对象及客户端对应的 ClientAddon 对象。

PluginManager 对象

PluginManager 是整个San CLI UI插件系统的基础,主要完成了两件事:1. 插件的加载及定义;2. 提供消息通讯。

上文提到,在 San CLI UI 加载依赖时,会尝试读取依赖包内的 ui.js 文件,并将 PluginManager 对象的实例 api 注入其中,因此以下插件的使用均基于 api 来调用。

1. 插件加载及定义

插件加载

通过api.registerAddon函数,开发者可以为自定义的组件指定 id 及加载路径(在 npm 包内的 ui.js 中),San CLI UI在插件加载时,会尝试从开发者指定的路径下加载插件定义,从而集成到San CLI UI对应位置,api 使用方式如下:

if (process.env.SAN_CLI_UI_DEV) { // 在开发模式下加载自定义端口文件
    api.registerAddon({
        id: 'san.widgets.client-addon.dev',
        url: 'http://localhost:8889/index.js'
    });
}
else {
    api.registerAddon({           // 在生产模式下加载npm包的路径
        id: 'san.widgets.client-addon',
        path: 'san-cli-ui-addon-widgets/dist'
    });
}

api.registerAddon仅实现了插件包的加载,而加载的插件显示在何处?插件的显示项以及数据操作逻辑则需要单独调用每个插件的 api 进行描述。

San CLI UI中可以注册的插件类型包括:widget 插件、配置插件、任务插件、自定义视图插件。

widget 插件

widget(部件)插件,指显示在「项目仪表盘」内的小部件,San CLI UI默认部件有:欢迎提示、运行任务、终止端口、新闻订阅。部件运行流程如图:

server端读取依赖包中的 ui.js 文件,通过文件内的api.registerAddonapi.registerWidget的描述可以得到插件的定义及所在视图,在 client 的仪表盘请求数据时,将读取的插件定义和路径返回给 client端,client端加载对应路径的 js,并通过 ClientAddonApi 将组件挂载到仪表盘视图,自定义组件和视图间的数据可以通过 sharedData 和 pluginAction 来传递。

通过 api.registerWidget 方法,开发者可实现自定义的部件,显示在仪表盘内。api 使用方式如下:

api.registerWidget({
    id: 'san.widgets.test', // 必选,唯一的 ID
    title: 'title', // 必选,组件的名称
    description: 'description',  // 必选,组件的描述
    icon: 'info-circle', // 必选,组件的icon,取值可选santd内的icon类型
    component: 'san.widgets.components.test-widget', // 必选,加载的动态组件,会用 ClientAddonApi 进行注册
    minWidth: 6, // 宽度
    minHeight: 1, // 高度
    maxWidth: 6,
    maxHeight: 6,
    defaultWidth: 5, // 必选
    defaultHeight: 2, // 必选
    openDetailsButton: false, // 可选
    defaultConfig: () => ({  // 可选,如果有prompt表单,返回默认配置
        hi: 'hello'
    }),
    async onConfigOpen() {  // 可选,返回表单配置
        return {
            prompts: [
                {
                    name: 'hi',
                    type: 'input',
                    message: '',
                    validate: input => !!input
                }
            ]
        };
    }
});
配置插件

配置插件主要用于在配置管理中,将项目中配置文件的修改变为可视化的表单操作,方便用户理解并修改配置项。目前San CLI UI内默认配置项包含san.config.jseslint的配置。

运行流程如图:

通过调用api.registerConfig可以更改项目的配置,此函数返回一个符合inquirer.prompts 格式的对象,San CLI UI内支持的 inquirer 类型有:checkbox、confirm、input、list、string。通过该对象生成表单,可在项目配置中显示并修改具体项目的配置。

api.registerConfig配置的内容与本地文件的对应关系如下(以san.config.js为例):

// ui.js
api.registerConfig({
    id: 'san.san-cli', // 唯一的配置 ID
    name: 'San CLI', // 展示名称
    description: 'configuration.san-cli.description', // 展示在名称下方的描述
    link: 'https://ecomfe.github.io/san-cli/#/config', // “更多信息 (More info)”链接
    files: { // 该配置所有可能的文件
        san: {
            js: ['san.config.js']
        }
    },
    icon: iconUrl, // 配置图标
    onRead: ({data}) => ({ // 在读取时调用。onRead钩子返回一个提示符列表
        prompts: [
            {
                name: 'publicPath',
                type: 'input',
                default: '/',
                value: data.san && data.san.publicPath,
                message: 'configuration.san-cli.publicPath.label',
                description: 'configuration.san-cli.publicPath.description',
                group: 'configuration.san-cli.groups.general',
                link: 'https://ecomfe.github.io/san-cli/#config'
            },
            ...
        ]
    }),
    onWrite: async ({api, prompts}) => { // 在写入时调用
        const sanData = {};
        for (const prompt of prompts) {
            sanData[prompt.id] = await api.getAnswer(prompt.id);
        }
        api.setData('san', sanData);
    }
});
// san.config.js
{
    assetsDir: STATIC_PRO,
    publicPath: '/',
    outputDir: 'dist',
    filenameHashing: isProduction,
    css: {
        sourceMap: isProduction,
        cssPreprocessor: 'less',
        extract: true
    },

    pages: {
        index: {
            entry: './pages/index.js',
            filename: 'index.html',
            template: './assets/index.html',
            title: '项目管理器 - `San CLI UI`',
            chunks: <span style="index">, 'vendors'</span>
        }
    },
    ...
}
// 读取到San CLI UI后
{
    san: {
        assetsDir: STATIC_PRO,
        publicPath: '/',
        outputDir: 'dist',
        filenameHashing: isProduction,
        css: {
            sourceMap: isProduction,
            cssPreprocessor: 'less',
            extract: true
        },
        pages: {
            index: {
                entry: './pages/index.js',
                filename: 'index.html',
                template: './assets/index.html',
                title: '项目管理器 - `San CLI UI`',
                chunks: <span style="index">, 'vendors'</span>
            }
        }
    ...
    }
}
任务插件

在项目任务中展示的任务项,生成自项目 package.json 文件的 scripts 字段。 San CLI UI 默认内置了san servesan buildsan inspect三个命令的增强任务,包括: startbuildanalyzerbuild:moderninspect几个任务。运行流程如图:

San CLI UI 任务管理中,基于--dashborad 命令扩展方式实现了 san servesan buildsan inspect 三个命令的可视化显示,整个插件包既是 San CLI 插件包也是 San CLI UI 插件包,整体分为两部分:

通过 api.registerTask 方法,实现任务的 “增强”,为任务增加额外的信息和显示,并能在对应的调用周期下实现附加功能。使用方式如下:

api.registerTask({
    // 匹配san serve
    match: /san(-cli\/index\.js)? serve(\s+--\S+(\s+\S+)?)*$/,
    description: 'task.description.serve',
    link: 'https://ecomfe.github.io/san-cli',
    icon: sanIcon,
    prompts: [
        {
            name: 'open',
            type: 'confirm',
            default: false,
            message: 'task.serve.open'
        },
        ...
    ],
    onBeforeRun: ({answers, args}) => {
        ...
    },
    onRun: () => {
        ...
    },
    onExit: () => {
        ...
    },
    views: [
        {
            id: 'san.cli-ui.views.dashboard',
            label: 'addons.dashboard.title',
            component: 'san.cli-ui.components.dashboard'
        },
        ...
    ],
    defaultView: 'san.cli-ui.views.dashboard'
});
自定义视图插件与自定义路由插件

开发者可以使用 api.registerView 创建自定义视图,结合使用 ClientAddonApi.addRoute 创建自定义路由跳转该视图,运行流程如下:

在 ui.js 通过 api.registerView 注册的视图,在服务端触发视图增加的 subscription 监听,将新增的页面路径及名称推送到客户端显示,而客户端组件加载时,已通过 ClientAddonApi.addRoute 将路由加载到 san-router,当点击跳转时,就如处理 San CLI UI 默认路由一般,跳转至对应自定义组件页面。使用方式如下:

api.registerView({
    id: 'san.cli-ui.views.dashboard',
    label: 'addons.dashboard.title',
    component: 'san.cli-ui.components.dashboard'
});

2. 消息通讯

ui.js 内注入的 api 对象提供了以下几种方式用于自定义插件间的通讯:

此外 San CLI UI 为开发者提供了工具函数:

以上就是 pluginManager 对象提供的能力,接下来我们来看看 index.js 中的负责组件注册及加载的 ClientAddon 对象

ClientAddon 对象

在插件包内,ClientAddon 实例化的对象 ClientAddonApi 主要完成两件事:

在插件内的使用方式如下:

import widgetdemo from './components/widget-demo';
import locales from './locales.json';

/* global ClientAddonApi */
if (window.ClientAddonApi) {
    // 扩展语言
    ClientAddonApi.addLocales(locales);
    // 推荐以类型前缀定义组件的唯一id:'san.widget'
    ClientAddonApi.defineComponent('san.widget.components.widget-demo', widgetdemo);
}

通过defineComponent将自定义组件加载到San CLI UI内,此时组件内可使用san-component增强的功能,如santd组件、$onPluginActionCalled等方法;通过addLocales将自定义组件的语言包加载到San CLI UI内,此时组件内可直接使用this.$t(key)的形式显示页面文案;通过ClientAddonApi.awaitComponent方法,在组件加载后,将组件挂载到页面对应位置。

所有 api 使用可参见:https://ecomfe.github.io/san-cli/#/ui/start

最后

感谢你阅读到了这里,以上便是《San CLI UI —— 不只是 San CLI 的 GUI(原理篇)》的全部内容。

本篇主要介绍了San CLI UI的整体架构,基于San + Node + Apollo GraphQL + Santd + san-router 实现,在工作流程部分重点介绍了插件系统的实现,并对San CLI UI可注册的插件类型及原理进行分析,包括:显示在仪表盘的 widget 部件注册、加入到配置管理的配置插件注册、用于任务增强的任务插件注册、以及扩展自定义视图的视图和路由插件注册,在具体加载机制上,重点介绍了插件包内用于 server端读取的ui.js和用于client端注册的index.js文件对象。下篇(实践篇)将通过一个插件开发的实例来演示插件的具体过程。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8