vue-cli ui 插件機(jī)制詳解

前言

在使用vue-cli的項(xiàng)目中涮毫,我們可以使用vue ui來(lái)打開(kāi)一個(gè)可視化的項(xiàng)目管理器剖笙,在其中可以操作項(xiàng)目,運(yùn)行任務(wù)肌似,安裝插件讯赏、依賴等垮兑。
除了官方提供的默認(rèn)功能之外,我們還可以通過(guò)ui插件來(lái)進(jìn)行拓展漱挎。
ui插件的寫(xiě)法是這樣的:

module.exports = api => {
  api.describeTask({
    match: /vue-cli-service serve(\s+--\S+(\s+\S+)?)*$/,
    description: 'org.vue.vue-webpack.tasks.serve.description',
    link: 'https://cli.vuejs.org/guide/cli-service.html#vue-cli-service-serve',
    icon: '/public/webpack-logo.png',
    prompts: [
     // ...
    ],
    onBeforeRun: ({ answers, args }) => {
     // ...
    }
  });
  api.addView({
    // ...
  })
}

一個(gè)UI插件其實(shí)就是個(gè)函數(shù)系枪,這個(gè)函數(shù)接收api對(duì)象作為參數(shù),api對(duì)象是PluginApi的實(shí)例识樱。

正文

從打開(kāi)一個(gè)項(xiàng)目開(kāi)始發(fā)生了什么嗤无?

我們?cè)谑褂每梢暬?yè)面打開(kāi)一個(gè)項(xiàng)目的時(shí)候,客戶端會(huì)發(fā)起一個(gè)mutation類(lèi)型的graphql查詢怜庸。apollo-server接收到之后就會(huì)進(jìn)入相對(duì)應(yīng)的resolver当犯,在resolver中調(diào)用函數(shù)去打開(kāi)項(xiàng)目。

module.exports = {
  Mutation: {
    projectOpen: (root, { id }, context) => projects.open(id, context),
  }
}

我們看一下project.open做了什么:

async function open (id, context) {
  const project = findOne(id, context)

  // ...
  lastProject = currentProject
  currentProject = project
  cwd.set(project.path, context)

  // 加載插件
  await plugins.list(project.path, context)

  // ...

  return project
} 

也就是先拿到該項(xiàng)目的信息割疾,設(shè)置cwd嚎卫,調(diào)用plugins.list方法,把信息設(shè)置到db中之后將項(xiàng)目信息返回給前端宏榕。
這里拓诸,重點(diǎn)就在plugin.list方法。
我們看一下他是如何實(shí)現(xiàn)的:

async function list (file, context, { resetApi = true, lightApi = false, autoLoadApi = true } = {}) {
  let pkg = folders.readPackage(file, context)
  let pkgContext = cwd.get()
  // 從package.json里加載插件信息
  if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
    pkgContext = path.resolve(cwd.get(), pkg.vuePlugins.resolveFrom)
    pkg = folders.readPackage(pkgContext, context)
  }
  pkgStore.set(file, { pkgContext, pkg })

  let plugins = []
  // 重依賴中查找插件
  plugins = plugins.concat(findPlugins(pkg.devDependencies || {}, file))
  plugins = plugins.concat(findPlugins(pkg.dependencies || {}, file))

  // 將cli-service放到最頂層
  const index = plugins.findIndex(p => p.id === CLI_SERVICE)
  if (index !== -1) {
    const service = plugins[index]
    plugins.splice(index, 1)
    plugins.unshift(service)
  }

  pluginsStore.set(file, plugins)
  // 如果條件滿足就去重置插件api麻昼,首次打開(kāi)項(xiàng)目會(huì)進(jìn)入該語(yǔ)句
  if (resetApi || (autoLoadApi && !pluginApiInstances.has(file))) {
    await resetPluginApi({ file, lightApi }, context)
  }
  return plugins
}

在這里奠支,它會(huì)從兩個(gè)地方查找插件:

  • package.json的vuePlugins字段
  • 依賴

查到之后將其存入map中進(jìn)行緩存,供后續(xù)使用抚芦。
如果是首次加載倍谜,或者重置時(shí)會(huì)進(jìn)入resetPluginApi方法。插件的加載和調(diào)用就是在這里完成叉抡。
我們看一下具體實(shí)現(xiàn):

function resetPluginApi ({ file, lightApi }, context) {
  return new Promise((resolve, reject) => {
    const widgets = require('./widgets')
    let pluginApi = pluginApiInstances.get(file)
    let projectId
    // 做一些清理工作
    if (pluginApi) {
      // ...
    }
    if (!lightApi) {
      // ...
    }

    setTimeout(async () => {
      const projects = require('./projects')
      const project = projects.findByPath(file, context)
      //  ...
      const plugins = getPlugins(file)

      // ...

      pluginApi = new PluginApi({
        plugins,
        file,
        project,
        lightMode: lightApi
      }, context)
      pluginApiInstances.set(file, pluginApi)

      // 運(yùn)行插件api

      // 運(yùn)行默認(rèn)ui插件
      runPluginApi(path.resolve(__dirname, '../../'), pluginApi, context, 'ui-defaults')
      // 運(yùn)行在上面獲取到的依賴中的插件
      plugins.forEach(plugin => runPluginApi(plugin.id, pluginApi, context))
      // 運(yùn)行package.json中vuePlugins字段中的插件
      const { pkg, pkgContext } = pkgStore.get(file)
      if (pkg.vuePlugins && pkg.vuePlugins.ui) {
        const files = pkg.vuePlugins.ui
        if (Array.isArray(files)) {
          for (const file of files) {
            runPluginApi(pkgContext, pluginApi, context, file)
          }
        }
      }
      // 添加客戶端附加組件
      pluginApi.clientAddons.forEach(options => {
        clientAddons.add(options, context)
      })
      // 添加視圖
      for (const view of pluginApi.views) {
        await views.add({ view, project }, context)
      }
      // 注冊(cè)組件
      for (const definition of pluginApi.widgetDefs) {
        await widgets.registerDefinition({ definition, project }, context)
      }

      if (projectId !== project.id) {
        callHook({
          id: 'projectOpen',
          args: [project, projects.getLast(context)],
          file
        }, context)
      } else {
        callHook({
          id: 'pluginReload',
          args: [project],
          file
        }, context)

        // View open hook
        const currentView = views.getCurrent()
        if (currentView) views.open(currentView.id)
      }

      // Load widgets for current project
      widgets.load(context)

      resolve(true)
    })
  })
}

可以看到尔崔,resetPluginApi函數(shù),進(jìn)入之后:

  • 拿到一些相關(guān)信息
  • 做一些清理工作
  • 創(chuàng)建PluginApi實(shí)例
  • 運(yùn)行在三個(gè)地方定義的api(默認(rèn)褥民、依賴季春、vuePlugins字段),運(yùn)行時(shí)將PluginApi實(shí)例傳入(回想一下插件的寫(xiě)法)消返。
  • 插件在運(yùn)行時(shí)载弄,就會(huì)調(diào)用PluginApi實(shí)例上的方法耘拇,在該示例上添加一系列的信息。在需要時(shí)即可直接拿出來(lái)進(jìn)行消費(fèi)侦锯。
  • 消費(fèi)PluginApi實(shí)例上的信息驼鞭,添加視圖、附加組件等尺碰,調(diào)用一些鉤子函數(shù),加載一些插件译隘。

到此為止亲桥,插件的加載過(guò)程完畢。
彎彎繞繞這么多固耘,但總的來(lái)說(shuō)题篷,插件的加載過(guò)程可以抽象成一下的一個(gè)圖:


插件加載過(guò)程.png

也就是:

  • 導(dǎo)入(打開(kāi))一個(gè)項(xiàng)目時(shí)區(qū)緩存項(xiàng)目信息
  • 加載插件信息
  • 創(chuàng)建插件實(shí)例
  • 運(yùn)行插件
  • 使用插件實(shí)例上的信息

以上就是vue-cli ui插件機(jī)制的實(shí)現(xiàn)過(guò)程。

附錄

PluginApi的定義

const path = require('path')
// Connectors
const logs = require('../connectors/logs')
const sharedData = require('../connectors/shared-data')
const views = require('../connectors/views')
const suggestions = require('../connectors/suggestions')
const progress = require('../connectors/progress')
const app = require('../connectors/app')
// Utils
const ipc = require('../util/ipc')
const { notify } = require('../util/notification')
const { matchesPluginId } = require('@vue/cli-shared-utils')
const { log } = require('../util/logger')
// Validators
const { validateConfiguration } = require('./configuration')
const { validateDescribeTask, validateAddTask } = require('./task')
const { validateClientAddon } = require('./client-addon')
const { validateView, validateBadge } = require('./view')
const { validateNotify } = require('./notify')
const { validateSuggestion } = require('./suggestion')
const { validateProgress } = require('./progress')
const { validateWidget } = require('./widget')

/**
 * @typedef SetSharedDataOptions
 * @prop {boolean} disk Don't keep this data in memory by writing it to disk
 */

/** @typedef {import('../connectors/shared-data').SharedData} SharedData */

class PluginApi {
  constructor ({ plugins, file, project, lightMode = false }, context) {
    // Context
    this.context = context
    this.pluginId = null
    this.project = project
    this.plugins = plugins
    this.cwd = file
    this.lightMode = lightMode
    // Hooks
    this.hooks = {
      projectOpen: [],
      pluginReload: [],
      configRead: [],
      configWrite: [],
      taskRun: [],
      taskExit: [],
      taskOpen: [],
      viewOpen: []
    }
    // Data
    this.configurations = []
    this.describedTasks = []
    this.addedTasks = []
    this.clientAddons = []
    this.views = []
    this.actions = new Map()
    this.ipcHandlers = []
    this.widgetDefs = []
  }

  /**
   * Register an handler called when the project is open (only if this plugin is loaded).
   *
   * @param {function} cb Handler
   */
  onProjectOpen (cb) {
    if (this.lightMode) return
    if (this.project) {
      cb(this.project)
      return
    }
    this.hooks.projectOpen.push(cb)
  }

  /**
   * Register an handler called when the plugin is reloaded.
   *
   * @param {function} cb Handler
   */
  onPluginReload (cb) {
    if (this.lightMode) return
    this.hooks.pluginReload.push(cb)
  }

  /**
   * Register an handler called when a config is red.
   *
   * @param {function} cb Handler
   */
  onConfigRead (cb) {
    if (this.lightMode) return
    this.hooks.configRead.push(cb)
  }

  /**
   * Register an handler called when a config is written.
   *
   * @param {function} cb Handler
   */
  onConfigWrite (cb) {
    if (this.lightMode) return
    this.hooks.configWrite.push(cb)
  }

  /**
   * Register an handler called when a task is run.
   *
   * @param {function} cb Handler
   */
  onTaskRun (cb) {
    if (this.lightMode) return
    this.hooks.taskRun.push(cb)
  }

  /**
   * Register an handler called when a task has exited.
   *
   * @param {function} cb Handler
   */
  onTaskExit (cb) {
    if (this.lightMode) return
    this.hooks.taskExit.push(cb)
  }

  /**
   * Register an handler called when the user opens one task details.
   *
   * @param {function} cb Handler
   */
  onTaskOpen (cb) {
    if (this.lightMode) return
    this.hooks.taskOpen.push(cb)
  }

  /**
   * Register an handler called when a view is open.
   *
   * @param {function} cb Handler
   */
  onViewOpen (cb) {
    if (this.lightMode) return
    this.hooks.viewOpen.push(cb)
  }

  /**
   * Describe a project configuration (usually for config file like `.eslintrc.json`).
   *
   * @param {object} options Configuration description
   */
  describeConfig (options) {
    if (this.lightMode) return
    try {
      validateConfiguration(options)
      this.configurations.push({
        ...options,
        pluginId: this.pluginId
      })
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'describeConfig' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /**
   * Describe a project task with additional information.
   * The tasks are generated from the scripts in the project `package.json`.
   *
   * @param {object} options Task description
   */
  describeTask (options) {
    try {
      validateDescribeTask(options)
      this.describedTasks.push({
        ...options,
        pluginId: this.pluginId
      })
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'describeTask' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /**
   * Get the task description matching a script command.
   *
   * @param {string} command Npm script command from `package.json`
   * @returns {object} Task description
   */
  getDescribedTask (command) {
    return this.describedTasks.find(
      options => typeof options.match === 'function' ? options.match(command) : options.match.test(command)
    )
  }

  /**
   * Add a new task independently from the scripts.
   * The task will only appear in the UI.
   *
   * @param {object} options Task description
   */
  addTask (options) {
    try {
      validateAddTask(options)
      this.addedTasks.push({
        ...options,
        pluginId: this.pluginId
      })
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'addTask' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /**
   * Register a client addon (a JS bundle which will be loaded in the browser).
   * Used to load components and add vue-router routes.
   *
   * @param {object} options Client addon options
   *   {
   *     id: string,
   *     url: string
   *   }
   *   or
   *   {
   *     id: string,
   *     path: string
   *   }
   */
  addClientAddon (options) {
    if (this.lightMode) return
    try {
      validateClientAddon(options)
      if (options.url && options.path) {
        throw new Error('\'url\' and \'path\' can\'t be defined at the same time.')
      }
      this.clientAddons.push({
        ...options,
        pluginId: this.pluginId
      })
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'addClientAddon' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /* Project view */

  /**
   * Add a new project view below the builtin 'plugins', 'config' and 'tasks' ones.
   *
   * @param {object} options ProjectView options
   */
  addView (options) {
    if (this.lightMode) return
    try {
      validateView(options)
      this.views.push({
        ...options,
        pluginId: this.pluginId
      })
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'addView' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /**
   * Add a badge to the project view button.
   * If the badge already exists, add 1 to the counter.
   *
   * @param {string} viewId Project view id
   * @param {object} options Badge options
   */
  addViewBadge (viewId, options) {
    if (this.lightMode) return
    try {
      validateBadge(options)
      views.addBadge({ viewId, badge: options }, this.context)
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'addViewBadge' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /**
   * Remove 1 from the counter of a badge if it exists.
   * If the badge counter reaches 0, it is removed from the button.
   *
   * @param {any} viewId
   * @param {any} badgeId
   * @memberof PluginApi
   */
  removeViewBadge (viewId, badgeId) {
    views.removeBadge({ viewId, badgeId }, this.context)
  }

  /* IPC */

  /**
   * Add a listener to the IPC messages.
   *
   * @param {function} cb Callback with 'data' param
   */
  ipcOn (cb) {
    const handler = cb._handler = ({ data, emit }) => {
      if (data._projectId) {
        if (data._projectId === this.project.id) {
          data = data._data
        } else {
          return
        }
      }
      // eslint-disable-next-line standard/no-callback-literal
      cb({ data, emit })
    }
    this.ipcHandlers.push(handler)
    return ipc.on(handler)
  }

  /**
   * Remove a listener for IPC messages.
   *
   * @param {any} cb Callback to be removed
   */
  ipcOff (cb) {
    const handler = cb._handler
    if (!handler) return
    const index = this.ipcHandlers.indexOf(handler)
    if (index !== -1) this.ipcHandlers.splice(index, 1)
    ipc.off(handler)
  }

  /**
   * Send an IPC message to all connected IPC clients.
   *
   * @param {any} data Message data
   */
  ipcSend (data) {
    ipc.send(data)
  }

  /**
   * Get the local DB instance (lowdb)
   *
   * @readonly
   */
  get db () {
    return this.context.db
  }

  /**
   * Display a notification in the user OS
   * @param {object} options Notification options
   */
  notify (options) {
    try {
      validateNotify(options)
      notify(options)
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'notify' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /**
   * Indicates if a specific plugin is used by the project
   * @param {string} id Plugin id or short id
   */
  hasPlugin (id) {
    return this.plugins.some(p => matchesPluginId(id, p.id))
  }

  /**
   * Display the progress screen.
   *
   * @param {object} options Progress options
   */
  setProgress (options) {
    if (this.lightMode) return
    try {
      validateProgress(options)
      progress.set({
        ...options,
        id: '__plugins__'
      }, this.context)
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'setProgress' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /**
   * Remove the progress screen.
   */
  removeProgress () {
    progress.remove('__plugins__', this.context)
  }

  /**
   * Get current working directory.
   */
  getCwd () {
    return this.cwd
  }

  /**
   * Resolves a file relative to current working directory
   * @param {string} file Path to file relative to project
   */
  resolve (file) {
    return path.resolve(this.cwd, file)
  }

  /**
   * Get currently open project
   */
  getProject () {
    return this.project
  }

  /* Namespaced */

  /**
   * Retrieve a Shared data instance.
   *
   * @param {string} id Id of the Shared data
   * @returns {SharedData} Shared data instance
   */
  getSharedData (id) {
    return sharedData.get({ id, projectId: this.project.id }, this.context)
  }

  /**
   * Set or update the value of a Shared data
   *
   * @param {string} id Id of the Shared data
   * @param {any} value Value of the Shared data
   * @param {SetSharedDataOptions} options
   */
  async setSharedData (id, value, { disk = false } = {}) {
    return sharedData.set({ id, projectId: this.project.id, value, disk }, this.context)
  }

  /**
   * Delete a shared data.
   *
   * @param {string} id Id of the Shared data
   */
  async removeSharedData (id) {
    return sharedData.remove({ id, projectId: this.project.id }, this.context)
  }

  /**
   * Watch for a value change of a shared data
   *
   * @param {string} id Id of the Shared data
   * @param {function} handler Callback
   */
  watchSharedData (id, handler) {
    sharedData.watch({ id, projectId: this.project.id }, handler)
  }

  /**
   * Delete the watcher of a shared data.
   *
   * @param {string} id Id of the Shared data
   * @param {function} handler Callback
   */
  unwatchSharedData (id, handler) {
    sharedData.unwatch({ id, projectId: this.project.id }, handler)
  }

  /**
   * Listener triggered when a Plugin action is called from a client addon component.
   *
   * @param {string} id Id of the action to listen
   * @param {any} cb Callback (ex: (params) => {} )
   */
  onAction (id, cb) {
    let list = this.actions.get(id)
    if (!list) {
      list = []
      this.actions.set(id, list)
    }
    list.push(cb)
  }

  /**
   * Call a Plugin action. This can also listened by client addon components.
   *
   * @param {string} id Id of the action
   * @param {object} params Params object passed as 1st argument to callbacks
   * @returns {Promise}
   */
  callAction (id, params) {
    const plugins = require('../connectors/plugins')
    return plugins.callAction({ id, params }, this.context)
  }

  /**
   * Retrieve a value from the local DB
   *
   * @param {string} id Path to the item
   * @returns Item value
   */
  storageGet (id) {
    return this.db.get(id).value()
  }

  /**
   * Store a value into the local DB
   *
   * @param {string} id Path to the item
   * @param {any} value Value to be stored (must be serializable in JSON)
   */
  storageSet (id, value) {
    log('Storage set', id, value)
    this.db.set(id, value).write()
  }

  /**
   * Add a suggestion for the user.
   *
   * @param {object} options Suggestion
   */
  addSuggestion (options) {
    if (this.lightMode) return
    try {
      validateSuggestion(options)
      suggestions.add(options, this.context)
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'addSuggestion' options are invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid options: ${e.message}`))
    }
  }

  /**
   * Remove a suggestion
   *
   * @param {string} id Id of the suggestion
   */
  removeSuggestion (id) {
    suggestions.remove(id, this.context)
  }

  /**
   * Register a widget for project dashboard
   *
   * @param {object} def Widget definition
   */
  registerWidget (def) {
    if (this.lightMode) return
    try {
      validateWidget(def)
      this.widgetDefs.push({
        ...def,
        pluginId: this.pluginId
      })
    } catch (e) {
      logs.add({
        type: 'error',
        tag: 'PluginApi',
        message: `(${this.pluginId || 'unknown plugin'}) 'registerWidget' widget definition is invalid\n${e.message}`
      }, this.context)
      console.error(new Error(`Invalid definition: ${e.message}`))
    }
  }

  /**
   * Request a route to be displayed in the client
   */
  requestRoute (route) {
    app.requestRoute(route, this.context)
  }

  /**
   * Create a namespaced version of:
   *   - getSharedData
   *   - setSharedData
   *   - onAction
   *   - callAction
   *
   * @param {string} namespace Prefix to add to the id params
   */
  namespace (namespace) {
    return {
      /**
       * Retrieve a Shared data instance.
       *
       * @param {string} id Id of the Shared data
       * @returns {SharedData} Shared data instance
       */
      getSharedData: (id) => this.getSharedData(namespace + id),
      /**
       * Set or update the value of a Shared data
       *
       * @param {string} id Id of the Shared data
       * @param {any} value Value of the Shared data
       * @param {SetSharedDataOptions} options
       */
      setSharedData: (id, value, options) => this.setSharedData(namespace + id, value, options),
      /**
       * Delete a shared data.
       *
       * @param {string} id Id of the Shared data
       */
      removeSharedData: (id) => this.removeSharedData(namespace + id),
      /**
       * Watch for a value change of a shared data
       *
       * @param {string} id Id of the Shared data
       * @param {function} handler Callback
       */
      watchSharedData: (id, handler) => this.watchSharedData(namespace + id, handler),
      /**
       * Delete the watcher of a shared data.
       *
       * @param {string} id Id of the Shared data
       * @param {function} handler Callback
       */
      unwatchSharedData: (id, handler) => this.unwatchSharedData(namespace + id, handler),
      /**
       * Listener triggered when a Plugin action is called from a client addon component.
       *
       * @param {string} id Id of the action to listen
       * @param {any} cb Callback (ex: (params) => {} )
       */
      onAction: (id, cb) => this.onAction(namespace + id, cb),
      /**
       * Call a Plugin action. This can also listened by client addon components.
       *
       * @param {string} id Id of the action
       * @param {object} params Params object passed as 1st argument to callbacks
       * @returns {Promise}
       */
      callAction: (id, params) => this.callAction(namespace + id, params),
      /**
       * Retrieve a value from the local DB
       *
       * @param {string} id Path to the item
       * @returns Item value
       */
      storageGet: (id) => this.storageGet(namespace + id),
      /**
       * Store a value into the local DB
       *
       * @param {string} id Path to the item
       * @param {any} value Value to be stored (must be serializable in JSON)
       */
      storageSet: (id, value) => this.storageSet(namespace + id, value),
      /**
       * Add a suggestion for the user.
       *
       * @param {object} options Suggestion
       */
      addSuggestion: (options) => {
        options.id = namespace + options.id
        return this.addSuggestion(options)
      },
      /**
       * Remove a suggestion
       *
       * @param {string} id Id of the suggestion
       */
      removeSuggestion: (id) => this.removeSuggestion(namespace + id),
      /**
       * Register a widget for project dashboard
       *
       * @param {object} def Widget definition
       */
      registerWidget: (def) => {
        def.id = namespace + def.id
        return this.registerWidget(def)
      }
    }
  }
}

module.exports = PluginApi

如上所示厅目,就是一些供插件使用的方法和一些數(shù)組番枚,以及一個(gè)訂閱發(fā)布模式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末损敷,一起剝皮案震驚了整個(gè)濱河市葫笼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拗馒,老刑警劉巖路星,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異诱桂,居然都是意外死亡洋丐,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)挥等,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)友绝,“玉大人,你說(shuō)我怎么就攤上這事肝劲∏停” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵涡相,是天一觀的道長(zhǎng)哲泊。 經(jīng)常有香客問(wèn)我,道長(zhǎng)催蝗,這世上最難降的妖魔是什么切威? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮丙号,結(jié)果婚禮上先朦,老公的妹妹穿的比我還像新娘缰冤。我一直安慰自己,他們只是感情好喳魏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布棉浸。 她就那樣靜靜地躺著,像睡著了一般刺彩。 火紅的嫁衣襯著肌膚如雪迷郑。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天创倔,我揣著相機(jī)與錄音嗡害,去河邊找鬼。 笑死畦攘,一個(gè)胖子當(dāng)著我的面吹牛霸妹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播知押,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼叹螟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了台盯?” 一聲冷哼從身側(cè)響起罢绽,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎爷恳,沒(méi)想到半個(gè)月后有缆,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡温亲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年棚壁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片栈虚。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡袖外,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出魂务,到底是詐尸還是另有隱情曼验,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布粘姜,位于F島的核電站鬓照,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏孤紧。R本人自食惡果不足惜豺裆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望号显。 院中可真熱鬧臭猜,春花似錦躺酒、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至次屠,卻和暖如春园匹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劫灶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工偎肃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人浑此。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像滞详,于是被迫代替她去往敵國(guó)和親凛俱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容