Практика разработки Electron-vue 5 — часть CLI системы подключаемых модулей разработки

Electron

предисловие

Первоначально опубликовано в моемблог, добро пожаловать, обратите внимание~

Поздравляем всех с китайским Новым 2019 годом! Эта статья длинная, вам нужно набраться терпения, чтобы прочитать ее~

Некоторое время назад я использовалelectron-vueРазработано кросс-платформенное (в настоящее время поддерживающее три основные настольные операционные системы) бесплатное приложение для загрузки изображений с открытым исходным кодом——PicGo, я наступил на много ям в процессе разработки, причем не только со стороны бизнес-логики самого приложения, но и со стороны самого электрона. В процессе разработки этого приложения я многому научился. Поскольку я также начал изучать электрон с нуля, такой большой опыт также должен вдохновить и дать инструкции начинающим ученикам, которые хотят изучать разработку электронов. Поэтому я написал практический опыт разработки Электрона и объяснил его с точки зрения наиболее близкой к разработке реальных инженерных проектов. Надеюсь помочь всем.

Ожидается, что несколькосерия статейИли расширить с точки зрения:

  1. Начало работы с электрон-вью
  2. Простая разработка основного процесса и процесса визуализации
  3. Представляем базу данных JSON на основе Lodash - lowdb
  4. Некоторые меры кроссплатформенной совместимости
  5. Кстати CI релизы и обновления
  6. Разработка системы плагинов - часть CLI
  7. Разработка системы плагинов - часть графического интерфейса
  8. Думаю снова написать...

иллюстрировать

PicGoсостоит в том, чтобы принятьelectron-vueразработан, поэтому, если выvueТогда последующее исследование пройдет быстрее. Если ваш технологический стек других подобныхreact,angular, то чисто по этому туториалу, хотя построение рендерной стороны (которую можно понимать как страницу) может и не многому научиться, но основная сторона (основной процесс электрона) все же должна суметь усвоить соответствующие знания.

Если вы не читали предыдущую статью, вы можете начатьпредыдущая статьяСледуйте вместе.

Как я уже говорил, эту статью действительно трудно написать. Как построить систему плагинов я потратил пол года. Рассказать об этом в одной-двух статьях действительно непросто. Поэтому в тексте могут быть какие-то огрехи, а в дальнейшем будет полировка.

Система плагинов - Контейнеры

Я считаю, что многие люди обычно дают другие фреймворки, такие какVue,ReactилиWebpackПодождите, пока плагин будет написан. Мы можем назвать фреймворк, который предоставляет систему подключаемых модулей, «контейнером». Через API, предоставляемый контейнером, подключаемый модуль можно смонтировать в контейнере или получить доступ к жизненному циклу контейнера для реализации некоторых дополнительных настраиваемых функций. .

НапримерWebpackПо существу система процессов, которая проходит черезTapableПредоставляет множество хуков жизненного цикла, и плагины могут реализовывать конвейерные операции, обращаясь к этим хукам жизненного цикла, таким какbabelсерия плагиновES6код экранирован вES5;SASS,LESS,StylusСерия плагинов ставит предварительно обработанныеCSSКод компилируется в обычный браузер, распознаваемый браузеромCSSкод и т. д.

Мы хотим реализовать систему плагинов, которая по сути реализует такой контейнер. Этот контейнер и соответствующие плагины должны иметь следующие основные характеристики:

  • контейнер безсторонние плагинытакже можно получить доступРеализовать основные функции
  • Плагины независимы
  • Плагины настраиваются и управляются

Первый пункт должен быть легким для понимания. Какой смысл в системе плагинов, если она не работает без наличия сторонних плагинов? Однако, в отличие от подключаемых модулей сторонних производителей, многие системы подключаемых модулей имеют собственные встроенные подключаемые модули, такие какvue-cli,WebpackСерия встроенных плагинов. В это время некоторые функции самой системы плагинов будут реализованы встроенными плагинами.

Второй момент, независимость плагина означает, что сам плагин не будет работать, когда он запущен.инициативаВлияют на работу других плагинов. Конечно, плагин может зависеть от результатов других плагинов.

Третий момент заключается в том, что если плагин не может быть настроен и управляем, он столкнется с проблемами уже на этапе установки плагина. Таким образом, контейнер должен иметь хорошо продуманную запись для регистрации плагина.

В следующей части я совмещуPicGo-Coreа такжеPicGoПодробно объяснить, как устроены и реализованы система подключаемых модулей CLI и система подключаемых модулей GUI.

Система плагинов CLI

Обзор

По сути, подключаемая система CLI может рассматриваться как подключаемая система без GUI, то есть подключаемая система, работающая в командной строке или без визуального интерфейса. Почему нам нужно задействовать систему подключаемых модулей CLI при разработке системы подключаемых модулей Electron? Здесь нам нужно кратко рассмотреть структуру Electron:

Electron结构

можно увидеть, кромеRendererОтрисовка интерфейса, большинство функций обеспечиваетMainобеспечивается процессом. Для PicGo его нижний уровень должен быть системой процесса загрузки, как показано ниже:

PicGo-Core

  1. Ввод (ввод): принять ввод извне, по умолчанию используется путь или полная информация об изображении base64.
  2. Transformer: преобразуйте ввод в объект, который может быть загружен загрузчиком (включая размер изображения, base64, имя изображения и т. д.).
  3. Загрузчик: загрузите вывод из конвертера в указанное место,Загрузчиком по умолчанию будет SM.MS.
  4. Вывод (output): вывод результата загрузки, обычно можно получить результат в выводе imgUrl

Так что теоретически это должно быть в нижнем конце Node.js будет реализован. И электронRendererПроцесс просто реализует интерфейс GUI и вызывает API, предоставляемый системой процессов, реализованной базовой стороной Node.js. Подобно разделению передней и задней частей, когда мы обычно разрабатываем веб-страницы, но теперь эта задняя часть представляет собой систему подключаемых модулей, основанную на Node.js. Основываясь на этой идее, я началPicGo-Coreреализация.

Жизненный цикл

Вообще говоря, подключаемая система имеет свой собственный жизненный цикл, напримерVueимеютbeforeCreate,created,mountedтак далее,WebpackимеютbeforeRun,run,afterCompileи т.п. Это также является душой системы подключаемых модулей.Получая доступ к жизненному циклу системы, подключаемые модули получают больше степеней свободы.

Итак, мы можем сначала реализовать класс жизненного цикла. код может относиться кLifecycle.ts.

Процесс жизненного цикла может относиться к приведенной выше блок-схеме.

class Lifecycle {
  // 整个生命周期的入口
  async start (input: any[]): Promise<void> {
    try {
      await this.beforeTransform(input)
      await this.doTransform(input)
      await this.beforeUpload(input)
      await this.doUpload(input)
      await this.afterUpload(input)
    } catch (e) {
      console.log(e)
    }
  }

  // 获取原始输入,转换前
  private async beforeTransform (input) {
    // ...
  }

  // 将输入转换成Uploader可上传的格式
  private async doTransform (input) {
    // ...
  }
  
  // Uploader上传前
  private async beforeUpload (input) {
    // ...
  }
  
  // Uploader上传
  private async doUpload (input) {
    // ...
  }
  
  // Uploader上传完成后
  private async afterUpload (input) {
    // ...
  }
}

В реальном использовании мы можем передать:

const lifeCycle = new LifeCycle()
lifeCycle.start([...])

для запуска всего жизненного цикла процесса загрузки. Но пока мы не видели ничего, связанного с плагинами. Это необходимо для выполнения первого условия, которое мы сказали: контейнер не имеетсторонние плагинытакже можно получить доступРеализовать основные функции.

трансляция события

Много раз нам нужно каким-то образом передать некоторые события. Как и в модели публикации-подписки, она публикуется контейнером и подписывается плагином. В этот момент мы можем непосредственноLifecycleЭтот класс наследуется от Node.jsEventEmmit:

class Lifecycle extends EventEmitter {
  constructor () {
    super()
  }
  // ...
}

ТакLifecycleТакже естьEventEmitterизemitа такжеonметод. Для контейнеров нам просто нужноemitСобытие может исчезнуть.

например, вPicGo-CoreВнутри весь процесс загрузки будет транслировать события наружу, уведомляя плагин о том, на каком этапе он находится в данный момент, и отправляя текущий ввод или вывод в момент трансляции.

private async beforeTransform (input) {
 // ...
 this.emit('beforeTransform', input) // 广播事件
}

Плагины могут свободно выбирать для прослушивания события, которые они хотят слушать. Например, плагин хочет узнать результат после загрузки (псевдокод):

plugin.on('finished', (output) => {
  console.log(output) // 获取output
})

При разработке PicGo-Core есть несколько полезных событий. Я тоже хочу поделиться здесь, хоть и не во всех плагинах системы будут такие события, но в сочетании со своими и реальными потребностями проекта они иногда полезны.

событие прогресса

Обычно мы загружаем или загружаем файл, вы обратите внимание на что-то: Progress Bar. Точно так же также есть событие в ядре Picgo, называетсяuploadProgress, который сообщает пользователю о текущем ходе загрузки. Однако в PicGo-Core ход загрузки начинается сbeforeTransformТолько начал считать, для удобства расчета разделил на 5 фиксированных значений.

private async beforeTransform (input) {
  this.emit('uploadProgress', 0) // 转换前,进度0
}
private async doTransform (input) {
  this.emit('uploadProgress', 30) // 开始转换,进度30
}
private async beforeUpload (input) {
  this.emit('uploadProgress', 60) // 开始上传,进度60
}
private async afterUpload (input) {
  this.emit('uploadProgress', 100) // 上传完毕,进度100
}

Возвращает, если загрузка не удалась-1:

async start (input: any[]): Promise<void> {
 try {
   await this.beforeTransform(input)
   await this.doTransform(input)
   await this.beforeUpload(input)
   await this.doUpload(input)
   await this.afterUpload(input)
 } catch (e) {
   console.log(e)
   this.emit('uploadProgress', -1)
 }
}

Прослушивая это событие, PicGo может создать следующий индикатор выполнения загрузки:

progress-bar

системное уведомление

Опубликовать, если что-то пошло не так с загрузкой или если есть какая-то информация, которую необходимо сообщить пользователю через уведомление на уровне системы.notificationмероприятие. Прослушав это событие, вы можете вызвать системное уведомление для публикации. Плагины также могут публиковать это событие для PicGo для прослушивания. Как показано выше, уведомление в правом верхнем углу после успешной загрузки.

Жизненный цикл доступа

В предыдущей части говорилось о трансляции события в жизненном цикле, можно обнаружить, что трансляция события только отправляется независимо от результата. То есть PicGo-Core публикует только это событие, а есть ли мониторинг плагина, то вам не нужно заботиться о том, что вы делаете после мониторинга. (Как это немного похоже на UDP). Но на самом деле много раз нам нужно получить доступ к жизненному циклу, чтобы что-то сделать.

Возьмите процесс загрузки в качестве примера, если я хочу сжать изображение перед загрузкой, тогда слушайтеbeforeUploadСобытия не могут этого сделать. Потому чтоbeforeUploadДаже если вы сжали изображение в событии, я боюсь, что процесс загрузки уже завершен.emitЖизненный цикл продолжается как обычно после выхода события.

Поэтому нам необходимо реализовать в жизненном цикле контейнера функцию, которая может позволить плагину получить доступ к его жизненному циклу, а результат отправлять в следующий жизненный цикл только после выполнения действий плагина в контейнере. текущий жизненный цикл. Можно обнаружить, что есть действие, которое «ждет» выполнения плагина. Таким образом, PicGo-Core является самым простым и интуитивно понятным в использовании.asyncфункция подходитawaitждать".

Нам не нужно рассматривать, как регистрируется плагин, что будет рассмотрено позже. Давайте сначала реализуем, как заставить плагин обращаться к жизненному циклу.

Следующий жизненный циклbeforeUploadНапример:

private async beforeUpload (input) {
  this.ctx.emit('uploadProgress', 60)
  this.ctx.emit('beforeUpload', input)
  // ...
  await this.handlePlugins(beforeUploadPlugins.getList(), input) // 执行并「等待」插件执行结束
}

Вы можете видеть, что мы проходимawaitОжидание метода жизненного циклаhandlePlugins(Я объясню, как это сделать ниже) выполнение заканчивается. И список плагинов, которые мы запускаем, черезbeforeUploadPlugins.getList()(Как этого добиться, будет объяснено ниже), указывая, что они предназначены только дляbeforeUploadплагины для этого жизненного цикла. затем введитеinputвходящийhandlePluginsПусть это называют плагины.

Теперь давайте реализуем этоhandlePlugins:

private async handlePlugins (plugins: Plugin[], input: any[]) {
  await Promise.all(plugins.map(async (plugin: Plugin) => {
    await plugin.handle(input)
  }))
}

мы проходимPromise.allтак же какawait"ждать" выполнения всех плагинов. Здесь следует отметить, что каждый плагин PicGo должен реализоватьhandleспособ обеспечитьPicGo-Coreпередача. Как видите, здесь реализована вторая функция, о которой мы говорим:Плагины независимы.

Отсюда также видно, что мы проходимasyncа такжеawaitСоздает среду, которая может «ждать» завершения выполнения плагина. Это решает проблему невозможности получить доступ к жизненному циклу подключаемой системы, просто транслируя события.

Нет, подождите, тут еще вопрос.beforeUploadPlugins.getList()Откуда это? Выше приведен пример кода. Фактически, PicGo-Core резервирует пять различных подключаемых модулей в соответствии с различными жизненными циклами процесса загрузки:

  • beforeTransformPlugins
  • transformer
  • beforeUploadPlugins
  • uploader
  • afterUploadPlugins

Вызывается за 5 циклов загрузки. Хотя время вызова этих пяти подключаемых модулей различается, их реализация одинакова: используется один и тот же механизм регистрации, один и тот же метод используется для получения списка подключаемых модулей, получения информации о подключаемых модулях и т. д. Итак, давайте продолжим и реализуем класс плагина жизненного цикла.

Класс плагина жизненного цикла

Это ключевая часть системы плагинов.Этот класс реализует, как плагины должны быть зарегистрированы в нашей системе плагинов и как система плагинов их получает. Код этого блока может относиться кLifecyclePlugins.ts.

Вот реализация:

class LifecyclePlugins {

  // list就是插件列表。以对象形式呈现。
  list: {
    [propName: string]: Plugin
  }
  constructor () {
    this.list = {} // 初始化插件列表为{}
  }

  // 插件注册的入口
  register (id: string, plugin: Plugin): void {
    // 如果插件没有提供id,则不予注册
    if (!id) throw new TypeError('id is required!')
    // 如果插件没有handle的方法,则不予注册
    if (typeof plugin.handle !== 'function') throw new TypeError('plugin.handle must be a function!')
    // 如果插件的id重复了,则不予注册
    if (this.list[id]) throw new TypeError(`${this.name} duplicate id: ${id}!`)
    this.list[id] = plugin
  }

  // 通过插件ID获取插件
  get (id: string): Plugin {
    return this.list[id]
  }

  // 获取插件列表
  getList (): Plugin[] {
    return Object.keys(this.list).map((item: string) => this.list[item])
  }

  // 获取插件ID列表
  getIdList (): string[] {
    return Object.keys(this.list)
  }
}

export default LifecyclePlugins

Самое главное для плагиновregisterметод, который является точкой входа для регистрации плагина. пройти черезregisterПосле регистрации будетLifecycleВнутреннийlistкid:pluginНапишите этот плагин в форме. Обратите внимание, что PicGo-Core требует, чтобы каждый подключаемый модуль реализовывалhandleметод, который будет вызываться позже в жизненном цикле.

Вот псевдокод, иллюстрирующий, как должен быть зарегистрирован плагин:

beforeTransformPlugins.register('test', {
  handle (ctx) {
    console.log(ctx)
  }
})

Здесь мы зарегистрировалиidназываетсяtestплагин, этоbeforeTransformПлагин сцены, его роль — печатать поступающую информацию.

Затем в разных жизненных циклах вызовитеLifeCyclePlugins.getList()метод для получения списка плагинов, соответствующих этому жизненному циклу.

Извлечь основные классы

Если вы просто реализуете систему плагинов, которая может работать в проекте Node.js, двух вышеперечисленных частей в основном достаточно:

  1. Класс Lifecyle отвечает за весь жизненный цикл
  2. Класс LifecylePlugins отвечает за регистрацию и вызов плагинов.

Но хорошая система плагинов CLI нуждается как минимум в следующих частях (по крайней мере, я так думаю):

  1. Может быть вызван из командной строки
  2. Возможность чтения конфигурационных файлов для дополнительной настройки
  3. Установка плагинов в один клик из командной строки
  4. Полная настройка плагина в командной строке
  5. Дружественная информационная подсказка журнала

Здесь вы можете обратиться к инструменту vue-cli3.

Поэтому нам нужно хотя бы следующие запчасти:

  1. Классы, связанные с операциями командной строки
  2. Связанные операции с файлом конфигурации
  3. Классы для установки плагинов, удаления, обновления и других связанных операций
  4. Плагины загружают связанные классы
  5. Классы, связанные с выводом информации журнала

Вышеупомянутые части не особенно сильно связаны с самим классом жизненного цикла, поэтому вам не нужно помещать их все в класс жизненного цикла для реализации.

Родственник, мы выпали из одногоCoreВ качестве ядра вышеперечисленные классы включены в этот основной класс, который отвечает за регистрацию команд командной строки, загрузку плагинов, оптимизацию информации журнала, вызов жизненного цикла и т. д.

Наконец, этот базовый класс доступен для использования пользователями или разработчиками. Это ядро ​​PicGo-CorePicGo.tsреализация.

Сама реализация PicGo не сложна, она просто вызывает методы вышеприведенных экземпляров класса.

Но обратите внимание, что здесь есть кое-что, о чем раньше не упоминалось. В дополнение к основным подклассам PicGo от PicGo-Core, в основномconstructorФаза функции сборки будет проходить в файле с именемctxпараметр. Что это за параметр? Этот параметр является самим классом PicGo.this. проходя вthis, подклассы PicGo-Core также могут использовать методы, предоставляемые базовым классом PicGo.

НапримерLoggerКласс реализует красивый вывод журнала командной строки:

logger

Затем в других подклассах вы хотите вызватьLoggerМетод также прост:

ctx.log.success('Hello world!')

вctxКак мы уже говорили выше, собственный PicGothisуказатель.

Далее мы представим конкретную реализацию каждого класса.

Классы, связанные с выводом журнала

Начнем с этого класса, потому что это самый простой и наименее навязчивый класс. С ним все в порядке, но это вишенка на торте.

Библиотека PicGo для украшения вывода журнала:chalk, его роль заключается в выводе цветного текста командной строки:

Он также очень прост в использовании:

const log = chalk.green('Success')
console.log(log) // 绿色字体的Success

Мы намерены реализовать 4 типа вывода: успех, предупреждение, информация и ошибка:

logger

Итак, создайте следующий класс:

import chalk from 'chalk'
import PicGo from '../core/PicGo'

class Logger {
  level: {
    [propName: string]: string
  }
  ctx: PicGo
  constructor (ctx: PicGo) { // 将PicGo的this传入构造函数,使得Logger也能使用PicGo核心类暴露的方法
    this.level = {
      success: 'green',
      info: 'blue',
      warn: 'yellow',
      error: 'red'
    }
    this.ctx = ctx
  }

  // 实际输出函数
  protected handleLog (type: string, msg: string | Error): string | Error | undefined {
    if (!this.ctx.config.silent) { // 如果不是静默模式,静默模式不输出log
      let log = chalk[this.level[type]](`[PicGo ${type.toUpperCase()}]: `)
      log += msg
      console.log(log)
      return msg
    } else {
      return
    }
  }

  // 对应四种不同类型
  success (msg: string | Error): string | Error | undefined {
    return this.handleLog('success', msg)
  }

  info (msg: string | Error): string | Error | undefined {
    return this.handleLog('info', msg)
  }

  error (msg: string | Error): string | Error | undefined {
    return this.handleLog('error', msg)
  }

  warn (msg: string | Error): string | Error | undefined {
    return this.handleLog('warn', msg)
  }
}

export default Logger

позжеLoggerЭтот класс монтируется в основной класс PicGo:

import Logger from '../lib/Logger'
class PicGo {
  log: Logger
  constructor () {
    // ...
    this.log = new Logger(this) // 把this传入Logger,也就是Logger里的ctx
  }
  // ...
}

Таким образом, другие классы, смонтированные в базовом классе PicGo, могут использоватьctx.logдля вызова метода в журнале.

Относится к файлу конфигурации

Во многих случаях системы, которые мы пишем, или подключаемые модули требуют большей или меньшей настройки, прежде чем их можно будет использовать лучше. Напримерvue-cli3изvue.config.js,Напримерhexoиз_config.ymlи т.п. И PicGo не исключение. Его можно использовать напрямую по умолчанию, но если вы хотите сделать что-то еще, вам, естественно, нужно его настроить. Таким образом, файл конфигурации является очень важной частью системы плагинов.

Раньше я использовал его в версии PicGo для Electron.lowdbКак библиотека чтения и записи для файлов конфигурации JSON, опыт хороший. Для прямой совместимости с конфигурацией PicGo я все еще использую эту библиотеку при написании PicGo-Core. Что касается конкретного использования lowdb, я упоминал об этом в предыдущей статье.Если вам интересно, вы можете взглянуть на-портал.

Поскольку lowdb выполняет постоянную настройку, аналогичную MySQL, для нее требуется определенный файл JSON на диске в качестве носителя, поэтому он не может инициализировать конфигурацию путем создания объекта конфигурации. Итак, все разворачивается из этого конфигурационного файла:

PicGo-Core использует файл конфигурации по умолчанию:homedir()/.picgo/config.json, который будет использоваться, если при создании экземпляра PicGo не указан путь к файлу конфигурации. Если пользователь предоставляет определенный файл конфигурации, то будет использоваться предоставленный файл конфигурации.

Давайте реализуем процесс инициализации PicGo:

import fs from 'fs-extra'
class PicGo extends EventEmitter {
  configPath: string
  private lifecycle: Lifecycle
  // ...

  constructor (configPath: string = '') {
    super()
    this.configPath = configPath // 传入configPath
    this.init()
  }

  init () {
    if (this.configPath === '') { // 如果不提供配置文件路径,就使用默认配置
      this.configPath = homedir() + '/.picgo/config.json'
    }
    if (path.extname(this.configPath).toUpperCase() !== '.JSON') { // 如果配置文件的格式不是JSON就返回错误日志
      this.configPath = ''
      return this.log.error('The configuration file only supports JSON format.')
    }
    const exist = fs.pathExistsSync(this.configPath)
    if (!exist) { // 如果不存在就创建
      fs.ensureFileSync(`${this.configPath}`)
    }
    // ...
  }
  // ...
}

Затем при создании PicGo происходит следующее:

const PicGo = require('picgo')
const picgo = new PicGo() // 不提供配置文件就用默认配置文件

// 或者

const picgo = new PicGo('./xxx.json') // 提供配置文件就用所提供的配置文件

Имея на руках файл конфигурации, нам нужно реализовать только три основные операции:

  1. Начальная конфигурация
  2. чтение конфигурации
  3. Конфигурация записи (конфигурация записи включает создание, обновление, удаление и т. д.)

Начальная конфигурация

Вообще говоря, наша система будет иметь некоторые конфигурации по умолчанию, и PicGo не является исключением. Мы можем выбрать запись конфигурации по умолчанию в код или запись конфигурации по умолчанию в код. Поскольку файлы конфигурации PicGo должны быть постоянными, разумно записать некоторые ключевые конфигурации по умолчанию в файлы конфигурации.

Он будет использоваться при инициализации конфигурацииlowdbНекоторые знания о , которые не будут здесь расширяться:

import lowdb from 'lowdb'
import FileSync from 'lowdb/adapters/FileSync'

const initConfig = (configPath: string): lowdb.LowdbSync<any> => {
  const adapter = new FileSync(configPath, { // lowdb的adapter,用于读取配置文件
    deserialize: (data: string): Function => {
      return (new Function(`return ${data}`))()
    }
  })
  const db = lowdb(adapter) // 暴露出来的db对象

  if (!db.has('picBed').value()) { // 如果没有picBed配置
    db.set('picBed', { // 就生成一个默认图床为SM.MS的配置
      current: 'smms'
    }).write()
  }
  if (!db.has('picgoPlugins').value()) { // 同理
    db.set('picgoPlugins', {}).write()
  }

  return db // 将db暴露出去让外部使用
}

Затем на этапе инициализации PicGo вы можетеconfigPathПередайте, чтобы инициализировать конфигурацию и получить конфигурацию.

init () {
  // ...
  let db = initConfig(this.configPath)
  this.config = db.read().value() // 将配置文件内容存入this.config
}

чтение конфигурации

После инициализации конфигурации получить ее легко:

import { get } from 'lodash'
getConfig (name: string = ''): any {
  if (name) { // 如果提供了配置项的名字
    return get(this.config, name) // 返回具体配置项结果
  } else {
    return this.config // 否则就返回完整配置
  }
}

используется здесьlodashизgetСпособ, в основном для удобства приобретения:

Например, содержимое конфигурации имеет следующую длину:

{
  "a": {
    "b": true
  }
}

Обычно мы получаемa.bнужно:

let b = this.config.a.b

В случае встречиaЕсли он не существует, то приведенное выше предложение сообщит об ошибке. потому чтоaне существует, тоa.bто естьundefined.bКонечно, это будет неправильно. и использоватьlodashизgetМетод позволяет избежать этой проблемы и может быть легко получен:

let b = get(this.config, 'a.b')

еслиaне существует, то полученный результатbне сообщит об ошибке, ноundefined.

записать конфигурацию

С вышеуказанным предзнаменованием написание контента также очень просто. пройти черезlowdbПредоставленный интерфейс, конфигурация записи выглядит следующим образом:

const saveConfig = (configPath: string, config: any): void => {
  const db = initConfig(configPath)
  Object.keys(config).forEach((name: string) => {
    db.read().set(name, config[name]).write()
  })
}

Мы можем использовать:

saveConfig(this.configPath, { a: { b: true } })

или:

saveConfig(this.configPath, { 'a.b': true })

Приведенные выше два способа записи сгенерируют следующую конфигурацию:

{
  "a": {
    "b": true
  }
}

Видно, что последний явно более лаконичен. Это благодаря lodash в lowdbsetметод.

На данный момент мы завершили операции, связанные с конфигурационным файлом. На самом деле, эти операции могут быть инкапсулированы в класс.Когда PicGo-Core был впервые реализован, казалось, что там не так много сложных вещей, поэтому это был просто небольшой инструмент для вызова. Ключ, конечно, не в этом, а в том, что после реализации соответствующих операций конфигурационного файла, как ваша система, так и плагины этой системы могут извлечь из этого пользу. Система может предоставлять API-интерфейсы для операций, связанных с файлами конфигурации, для подключаемых модулей. Далее мы будем шаг за шагом улучшать эту систему плагинов.

Класс действия плагина

Пока я понятия не имею, как будет называться этот класс.В коде я написал следующее:pluginHandler, затем назовите его операционным классом подключаемого модуля. Этот класс имеет три основные цели:

  1. пройти черезnpmУстановить плагин - установить
  2. пройти черезnpmУдалить плагин - удалить
  3. пройти черезnpmОбновить плагин - обновить

использоватьnpmдля распространения плагинов, что является решением, которое выберет большинство систем плагинов Node.js. Ведь на основании отсутствия собственного магазина плагинов (типа VSCode),npmЭто естественный «магазин плагинов». Конечно выложил вnpmЕсть много других преимуществ, таких как возможность легко устанавливать, обновлять и удалять плагины, например, запуск с нулевой стоимостью для пользователей Node.js. Это тожеpluginHandlerчто делает этот класс.

pluginHandlerСвязанные идеи реализации исходят отfeflow, Спасибо.

Обычно, когда мы устанавливаем модуль npm, это очень просто:

npm install xxxx --save

Однако мы установили его в текущий каталог проекта. Поскольку PicGo представляет файл конфигурации, мы можем установить плагин непосредственно в каталог, где находится файл конфигурации, поэтому, если вы хотите удалить PicGo, просто поместите . Но слишком утомительно просить пользователей открывать путь, по которому находится файл конфигурации PicGo, для установки плагинов каждый раз. Тоже не элегантно.

И наоборот, если мы установили его глобальноpicgoПосле этого в любом уголке файловой системы просто пройтиpicgo install xxxустановить одинpicgoПлагину не нужно находить папку, в которой находится файл конфигурации PicGo, поэтому пользовательский интерфейс будет намного лучше. Здесь вы можете сравнитьvue-cli3Шаги по установке плагина.

Для того, чтобы добиться такого эффекта, нам нужно вызвать по кодуnpmэта команда. Так как же Node.js реализует вызовы командной строки через код?

Здесь мы можем использоватьcross-spawnДля достижения кроссплатформенности цель вызова командной строки через код.

spawnЭтот метод также является родным для Node.js (в дочернем_процессе), ноcross-spawnИсправлены некоторые кроссплатформенные проблемы. Использование такое же.

const spawn = require('cross-spawn')
spawn('npm', ['install', '@vue/cli', '-g'])

Как видите, его параметры передаются в виде массива.

И операцию плагина, которую мы хотим реализовать, помимо основной командыinstall,update,uninstallВ остальном все остальные параметры одинаковы. Итак, мы вытащилиexecCommandметоды для реализации общей логики, стоящей за ними:

execCommand (cmd: string, modules: string[], where: string, proxy: string = ''): Promise<Result> {
  return new Promise((resolve: any, reject: any): void => {
    // spawn的命令行参数是以数组形式传入
    // 此处将命令和要安装的插件以数组的形式拼接起来
    // 此处的cmd指的是执行的命令,比如install\uninstall\update
    let args = [cmd].concat(modules).concat('--color=always').concat('--save')
    const npm = spawn('npm', args, { cwd: where }) // 执行npm,并通过 cwd指定执行的路径——配置文件所在文件夹

    let output = ''
    npm.stdout.on('data', (data: string) => {
      output += data // 获取输出日志
    }).pipe(process.stdout)

    npm.stderr.on('data', (data: string) => {
      output += data // 获取报错日志
    }).pipe(process.stderr)

    npm.on('close', (code: number) => {
      if (!code) {
        resolve({ code: 0, data: output }) // 如果没有报错就输出正常日志
      } else {
        reject({ code: code, data: output }) // 如果报错就输出报错日志
      }
    })
  })
}

Ключевые части в основном прокомментированы в коде. Конечно, есть еще некоторые вещи, о которых нужно знать. Обратите внимание на это предложение:

const npm = spawn('npm', args, { cwd: where }) // 执行npm,并通过 cwd指定执行的路径——配置文件所在文件夹

внутри{cwd: where},этоwhereэто значение, которое будет передано извне, указывающее, что этоnpmКаталог, в котором будет выполняться команда. Это также самая важная часть для нас, чтобы сделать этот класс работы с подключаемым модулем - не требуя от пользователей активного открытия каталога, в котором находится файл конфигурации, для установки подключаемого модуля, подключаемый модуль PicGo можно легко установить в любом месте. система.

Далее реализуемinstallметод, так что два других могут быть аналогичны.

async install (plugins: string[], proxy: string): Promise<void> {
  plugins = plugins.map((item: string) => 'picgo-plugin-' + item)
   const result = await this.execCommand('install', plugins, this.ctx.baseDir, proxy)
   if (!result.code) {
     this.ctx.log.success('插件安装成功')
     this.ctx.emit('installSuccess', {
       title: '插件安装成功',
       body: plugins
     })
   } else {
     const err = `插件安装失败,失败码为${result.code},错误日志为${result.data}`
     this.ctx.log.error(err)
     this.ctx.emit('failed', {
       title: '插件安装失败',
       body: err
    })
  }
}

Не смотрите на много кода, ключ всего в одном предложенииconst result = await this.execCommand('install', plugins, this.ctx.baseDir, proxy), остальное - просто вывод журнала. Ну и плагин тоже установлен, как его загрузить?

класс загрузки плагина

Как было сказано выше, мы установим плагин в каталог, где находится файл конфигурации. Стоит отметить, что посколькуnpmфункция, если есть каталог с именемpackage.json, то такие операции, как установка плагинов, обновление плагинов и т. д., будут изменены одновременно.package.jsonдокумент. Итак, мы можем читатьpackage.jsonфайл, чтобы узнать, какие плагины PicGo находятся в текущем каталоге. Это также очень важная часть механизма загрузки плагинов Hexo.

pluginLoaderСвязанные идеи реализации исходят отhexo, Спасибо.

Что касается именования плагинов, у PicGo здесь есть ограничение (это также способ выбора многих систем плагинов), который должен быть назван в честьpicgo-plugin-начало. Это облегчает их распознавание классами загрузки плагинов.

Здесь есть небольшая дырочка. Если каталог, в котором находится наш файл конфигурации, неpackage.jsonЕсли вы выполните команду, чтобы выполнить плагин установки, появится сообщение об ошибке. Но мы не хотим, чтобы пользователи видели эту ошибку, поэтому я инициализирую插件加载类Когда , нам нужно судить, существует файл или нет, если его нет, то нам нужно его создать:

class PluginLoader {
  ctx: PicGo
  list: string[]
  constructor (ctx: PicGo) {
    this.ctx = ctx
    this.list = [] // 插件列表
    this.init()
  }

  init (): void {
    const packagePath = path.join(this.ctx.baseDir, 'package.json')
    if (!fs.existsSync(packagePath)) { // 如果不存在
      const pkg = {
        name: 'picgo-plugins',
        description: 'picgo-plugins',
        repository: 'https://github.com/Molunerfinn/PicGo-Core',
        license: 'MIT'
      }
      fs.writeFileSync(packagePath, JSON.stringify(pkg), 'utf8') // 创建这个文件
    }
  }
  // ...
}

Далее мы реализуем наиболее важныеloadметод. Нам понадобятся следующие шаги:

  1. пройти первымpackage.jsonчтобы найти все законные плагины
  2. пройти черезrequireзагрузить плагин
  3. ПоддержаниеpicgoPluginsНастройте, чтобы определить, отключен ли плагин
  4. Выявляется при выполнении плагинов, которые не отключеныregisterспособ реализации регистрации плагина
import PicGo from '../core/PicGo'
import fs from 'fs-extra'
import path from 'path'
import resolve from 'resolve'

load (): void | boolean {
  const packagePath = path.join(this.ctx.baseDir, 'package.json')
  const pluginDir = path.join(this.ctx.baseDir, 'node_modules/')
    // Thanks to hexo -> https://github.com/hexojs/hexo/blob/master/lib/hexo/load_plugins.js
  if (!fs.existsSync(pluginDir)) { // 如果插件文件夹不存在,返回false
    return false
  }
  const json = fs.readJSONSync(packagePath) // 读取package.json
  const deps = Object.keys(json.dependencies || {})
  const devDeps = Object.keys(json.devDependencies || {})
  // 1.获取插件列表
  const modules = deps.concat(devDeps).filter((name: string) => {
    if (!/^picgo-plugin-|^@[^/]+\/picgo-plugin-/.test(name)) return false
    const path = this.resolvePlugin(this.ctx, name) // 获取插件路径
    return fs.existsSync(path)
  })
  for (let i in modules) {
    this.list.push(modules[i]) // 把插件push进插件列表
    if (this.ctx.config.picgoPlugins[modules[i]] || this.ctx.config.picgoPlugins[modules[i]] === undefined) { // 3.判断插件是否被禁用,如果是undefined则为新安装的插件,默认不禁用
      try {
        this.getPlugin(modules[i]).register() // 4.调用插件的`register`方法进行注册
        const plugin = `picgoPlugins[${modules[i]}]`
        this.ctx.saveConfig( // 将插件设为启用-->让新安装的插件的值从undefined变成true
          {
            [plugin]: true
          }
        )
      } catch (e) {
        this.ctx.log.error(e)
        this.ctx.emit('notification', {
          title: `Plugin ${modules[i]} Load Error`,
          body: e
        })
      }
    }
  }
}
resolvePlugin (ctx: PicGo, name: string): string { // 获取插件路径
  try {
    return resolve.sync(name, { basedir: ctx.baseDir })
  } catch (err) {
    return path.join(ctx.baseDir, 'node_modules', name)
  }
}
getPlugin (name: string): any { // 通过插件名获取插件
  const pluginDir = path.join(this.ctx.baseDir, 'node_modules/')
  return require(pluginDir + name)(this.ctx) // 2.通过require获取插件并传入ctx
}

loadЭтот метод является наиболее важной частью загрузки всей системы плагинов. Это может быть непросто понять, просто взглянув на приведенные выше шаги и код. Мы используем конкретный пример плагина ниже, чтобы проиллюстрировать.

Предположим, я пишуpicgo-plugin-xxxплагин. Мой код выглядит следующим образом:

// 插件系统会传入picgo的ctx,方便插件调用picgo暴露出来的api
// 所以我们需要有一个ctx的参数用于接收来自picgo的api
module.exports = ctx => {

  // 插件系统会调用这个方法来进行插件的注册
  const register = () => {
    ctx.helper.beforeTransformPlugins.register('xxx', {
      handle (ctx) { // 调用插件的 handle 方法时也会传入 ctx 方便调用api
        console.log(ctx.output)
      }
    })
  }

  return {
    register
  }
}

Мы уже знаем о процессе запуска плагина из предыдущей статьи:

  1. Сначала запустите жизненный цикл
  2. При переходе к определенному жизненному циклу, например здесьbeforeTransform, то на этом этапе получитьbeforeTransformPluginsэти плагины
  3. beforeTransformPluginsЭти плагины предоставляютсяctx.helper.beforeTransformPlugins.registerметод зарегистрирован и доступен черезctx.helper.beforeTransformPlugins.getList()Получать
  4. После получения плагина каждый будет называтьсяbeforeTransformPluginsизhandleметод и передатьctxдля использования плагина

Обратите внимание на третий шаг выше,ctx.helper.beforeTransformPlugins.registerКогда вызывается этот метод? Ответ находится на этапе загрузки плагина, описанного в этом разделе,pluginLoaderвызывается для каждого плагинаregisterметод, затем в плагинеregisterметод, мы писали:

ctx.helper.beforeTransformPlugins.register('xxx', {
  handle (ctx) { // 调用插件的 handle 方法时也会传入 ctx 方便调用api
    console.log(ctx.output)
  }
})

То есть в это время,ctx.helper.beforeTransformPlugins.registerЭтот метод называется.

Поэтому перед началом жизненного цикла весь подключаемый модуль и подключаемые модули каждого жизненного цикла предварительно регистрируются. Поэтому, когда жизненный цикл начнет работать, просто пропуститеgetList()Вы можете получить зарегистрированный плагин для выполнения всего процесса.

Поэтому я использовал для бегаHexoОбъясняются проблемы, с которыми я столкнулся при создании блога. я установил некоторыеHexoПлагин, но я не знаю, почему он всегда не работает. Позже обнаружил, что он не использовался при установке--save, из-за чего они не пишутсяpackage.jsonзависимые поля. а такжеHexoПервым шагом в загрузке плагина является запускpackage.jsonПолучите список допустимых плагинов отсюда, если плагин недоступенpackage.jsonв, даже вnode_modulesЕсли есть, то не получится.

С плагином поговорим о том, как вызывать и настраивать его в командной строке.

Класс операций командной строки

Классы действий командной строки PicGo в основном зависят от двух библиотек:commander.jsа такжеInquirer.js. Эти две библиотеки также очень часто используются для приложений командной строки Node.js. Первый отвечает за синтаксический анализ командной строки и выполнение связанных команд. Последний отвечает за предоставление интерфейса командной строки для взаимодействия с пользователем.

Например, вы можете ввести:

picgo use uploader

на этот разcommander.jsЧтобы разобрать эту команду, скажите нам, что это время называетсяuseЭта команда, параметрыuploader, затем введитеInquirer.jsПредусмотренный интерактивный интерфейс:

Inquirer.js

Если вы использовали что-то вродеvue-cli3илиcreate-react-appПодобные инструменты командной строки должны быть знакомы с подобными ситуациями.

Во-первых, мы пишем класс операций командной строки, чтобы предоставить API другим частям команды регистрации, здесь исходный код может ссылаться наCommander.ts.

import PicGo from '../core/PicGo'
import program from 'commander'
import inquirer from 'inquirer'
import { Plugin } from '../utils/interfaces'
const pkg = require('../../package.json')

class Commander {
  list: {
    [propName: string]: Plugin
  }
  program: typeof program
  inquirer: typeof inquirer
  private ctx: PicGo

  constructor (ctx: PicGo) {
    this.list = {}
    this.program = program
    this.inquirer = inquirer
    this.ctx = ctx
  }
  // ...
}

export default Commander

Затем мы создаем его экземпляр в основном классе PicGo-Core:

import Commander from '../lib/Commander'
class PicGo extends EventEmitter {
  // ...
  cmd: Commander

  constructor (configPath: string = '') {
    super()
    this.cmd = new Commander(this)
    // ...
  }
  // ...

так что другие части могут использоватьctx.cmd.programзвонитьcommander.jsи использоватьctx.cmd.inquirerзвонитьInquirer.js.

В Интернете есть много руководств по использованию этих двух библиотек. Вот простой пример, начнем с самой основной функции PicGo — загрузки картинок из командной строки.

регистрация команд

Чтобы унифицировать с предыдущей структурой плагина, мы также записываем регистрацию команды вhandleв функции.

import PicGo from '../../core/PicGo'
import path from 'path'
import fs from 'fs-extra'

export default {
  handle: (ctx: PicGo): void => {
    const cmd = ctx.cmd
    cmd.program // 此处是一个commander.js实例
      .command('upload') // 注册命令 upload
      .description('upload, go go go') // 命令的描述
      .arguments('[input...]') // 命令的参数
      .alias('u') // 命令的别名 u
      .action(async (input: string[]) => { // 命令执行的函数
        const inputList = input // 获取输入的input
            .map((item: string) => path.resolve(item))
            .filter((item: string) => {
              const exist = fs.existsSync(item) // 判断输入的地址存不存在
              if (!exist) {
                ctx.log.warn(`${item} is not existed.`) // 如果不存在就返回警告信息
              }
              return exist
            })
        await ctx.upload(inputList) // 上传图片(调用生命周期的start函数)
      })
  }
}

Итак, если мы каким-то образом зарегистрируем команду:

import PicGo from '../../core/PicGo'
import upload from './upload'
// ...

export default (ctx: PicGo): void => {
  ctx.cmd.register('upload', upload) // 此处的注册逻辑跟lifecyclePlugins一致。
  // ...
}

Когда код написан здесь, вы можете почувствовать, что все готово. На самом деле мы в одном последнем шаге, нам не хватает записи для принятия введенных нами команд. Например, теперь, когда мы закончили написание команды и регистрацию команды, как нам использовать ее в командной строке?

Использование командной строки

В это время позвольте мне просто сказатьpackage.jsonдва поля вbinа такжеmain. вmainФайл, на который указывает поле, является вашимconst xxx = require('xxx')когда вы его получите. а такжеbinФайл, на который указывает поле, — это команда, которую вы можете ввести непосредственно в командной строке после глобальной установки.

Например, PicGo-CorebinПоля следующие:

// ...
"bin": {
  "picgo": "./bin/picgo"
},

Затем, если пользователь установил picgo глобально, он может пройтиpicgoЭта команда тоже для использования picgo. Аналогично установке@vue/cliПосле этого вы можете использоватьvueЭта команда такая же.

Итак, давайте посмотрим./bin/picgoчто ты сделал. исходный код вздесь.

#!/usr/bin/env node
const path = require('path')
const minimist = require('minimist')
let argv = minimist(process.argv.slice(2)) // 解析命令行
let configPath = argv.c || argv.config || '' // 查看是否提供了configPath
if (configPath !== true && configPath !== '') {
  configPath = path.resolve(configPath)
} else {
  configPath = ''
}
const PicGo = require('../dist/index')
const picgo = new PicGo(configPath) // 实例化picgo
picgo.registerCommands() // 注册命令

try {
  picgo.cmd.program.parse(process.argv) // 调用commander.js解析命令
} catch (e) {
  picgo.log.error(e)
  if (process.argv.includes('--debug')) {
    Promise.reject(e)
  }
}

Ключевая частьpicgo.cmd.program.parse(process.argv)Это предложение, это предложение вызываетcommander.jsРазрешитьprocess.argvТо есть команда командной строки и параметры.

Затем мы можем использовать его на этапе разработки./bin/picgo uploadТаким образом, команда вызывается, и в производственной среде, то есть после того, как пользователь установил ее глобально, ее можно передать черезpicgo uploadЭто вызывает команду.

Обработка элементов конфигурации

Как упоминалось ранее, элементы конфигурации являются важной частью системы плагинов. Различные системы плагинов по-разному обрабатывают элементы конфигурации. НапримерHexoпри условии_config.ymlдля конфигурации пользователя,vue-cli3при условииvue.config.jsдля конфигурации пользователя. PicGo также предоставляетconfig.jsonДля настройки пользователями, но исходя из этого, я хочу предоставить пользователям более удобный способ завершения настройки непосредственно в командной строке, без необходимости открывать этот файл конфигурации.

Например, мы можем выбрать текущую загруженную кровать изображения через командную строку:

$ picgo use
? Use an uploader (Use arrow keys)
  smms
❯ tcyun
  weibo
  github
  qiniu
  imgur
  aliyun
(Move up and down to reveal more choices)

Это взаимодействие в командной строке требует ранее упомянутогоInquirer.jsчтобы помочь нам достичь этого эффекта.

Его использование также очень просто, перейдите вprompts(можно понимать как массив вопросов), и тогда он вернет результат вопроса в виде объекта, мы обычно записываем этот результат какanswer.

Чтобы упростить этот процесс, PicGo нужен только подключаемый модуль для предоставленияconfigметод, этот метод просто возвращает действительныйpromptsмассив вопросов, то PicGo автоматически вызоветInquirer.jsвыполнить его и автоматически записать результат в файл конфигурации.

Например, встроенный в PicGoImgurфигурная кроватьconfigкод показывает, как показано ниже:

const config = (ctx: PicGo): PluginConfig[] => {
  let userConfig = ctx.getConfig('picBed.imgur')
  if (!userConfig) {
    userConfig = {}
  }
  const config = [
    {
      name: 'clientId',
      type: 'input',
      default: userConfig.clientId || '',
      required: true
    },
    {
      name: 'proxy',
      type: 'input',
      default: userConfig.proxy || '',
      required: false
    }
  ]
  return config // 这个config就是一个合法的prompts数组
}
export default {
  // ...
  config
}

Затем мы можем использовать код для вызова его в командной строке, исходный кодпортал:

Следующий код сокращен

import PicGo from '../../core/PicGo'
import { PluginConfig } from '../../utils/interfaces'

// 处理uploader的config数组,然后写入配置文件
const handleConfig = async (ctx: PicGo, prompts: PluginConfig, name: string): Promise<void> => {
  const answer = await ctx.cmd.inquirer.prompt(prompts)
  let configName = `picBed.${name}`
  ctx.saveConfig({
    [configName]: answer
  })
}

export default {
  handle: (ctx: PicGo): void => {
    const cmd: typeof ctx.cmd = ctx.cmd
    cmd.program
      .command('set') // 注册一个set命令
      .alias('config') // 别名 config
      .description('configure config of picgo')
      .action(async () => {
        try {
          let prompts = [ // prompts问题数组
            {
              type: 'list',
              name: 'uploader',
              choices: ctx.helper.uploader.getIdList(), // 获取Uploader列表
              message: `Choose a(n) uploader`,
              default: ctx.config.picBed.uploader || ctx.config.picBed.current
            }
          ]
          let answer = await ctx.cmd.inquirer.prompt(prompts) // 等待inquirer处理用户的输入
          const item = ctx.helper.uploader.get(answer.uploader) // 获取用户选择的uploader
          if (item.config) { // 如果uploader提供了config方法
            await handleConfig(ctx, item.config(ctx), answer.uploader) //处理该config方法暴露出的prompts数组
          }
          ctx.log.success('Configure config successfully!')
        } catch (e) {
          ctx.log.error(e)
          if (process.argv.includes('--debug')) {
            Promise.reject(e)
          }
        }
      })
  }
}

Выше приведена обработка конфигурации для метода конфигурации Uploader, и то же самое верно для других плагинов, поэтому я не буду повторяться. Таким образом, мы можем быстро настроить файл конфигурации через командную строку, а пользовательский опыт - ++.

Релиз системы плагинов

Сказав так много, мы все пишем подключаемые системы локально, как опубликовать их, чтобы другие могли их установить и использовать? Существует множество связанных статей о публикации модулей в npm, например, ссылка на этустатья. Здесь я хочу поговорить о том, как опубликовать программу, которую можно использовать как в командной строке, так и через, например.const picgo = require('picgo')Библиотека для использования вызовов API в проектах Node.js.

Вызовы CLI и API сосуществуют

На самом деле, это также упоминается в предыдущем разделе. Когда мы публикуем библиотеку npm, мы обычноpackage.jsonвнутреннийmainПоле указывает файл входа для этой библиотеки. Затем пользователь может пройтиconst picgo = require('picgo')Используется в проектах Node.js.

Если мы хотим, чтобы библиотека могла прописать команду после установки, то мы можемbinПоле указывает файл записи, которому соответствует эта команда. Например:

// ...
"bin": {
  "picgo": "./bin/picgo"
},

Таким образом, после глобальной установки мы зарегистрируем файл с именемpicgoкоманда.

Конечно на этот разbinа такжеmainВходные файлы обычно отличаются.binФайл ввода должен хорошо анализировать командную строку. Поэтому обычно мы будем использовать некоторую библиотеку разбора командной строки, такую ​​какminimistилиcommander.jsи т. д. для разбора аргументов из командной строки.

резюме

На данный момент мы в основном реализовали ключевую часть системы подключаемых модулей CLI. Затем в проекте Electron мы можемmainНаписанная нами система плагинов используется в процессе, а система плагинов приложения создается через API, предоставляемый этим плагином. В следующей статье будет подробно описано, как интегрировать систему подключаемых модулей CLI в Electron, реализовать систему подключаемых модулей GUI и добавить некоторые дополнительные механизмы, чтобы сделать систему подключаемых модулей GUI более гибкой и мощной.

Большая часть этой статьи разработана мнойPicGoВозникшие проблемы и ямы, на которые наступили. Возможно, за простыми предложениями в тексте кроются мои бесчисленные чтения и отладки. Надеюсь, эта статья даст вамelectron-vueРазвитие приносит некоторое вдохновение. Соответствующий код в тексте, вы можете найти его вPicGoа такжеPicGo-CoreНайдено в репозитории проекта, добро пожаловать в звезду~ Если эта статья поможет вам, это будет мое самое счастливое место. Если вам нравится, пожалуйста, следуйте за мнойблогтак же какСтатьи из этой сериипоследующий прогресс.

Примечание: фотографии в этой статье принадлежат моей личной работе, если не указано иное, если вам нужно перепечатать, пожалуйста, отправьте личное сообщение

использованная литература

Спасибо за эти качественные статьи:

  1. Разработка интерфейса командной строки (CLI) с помощью Node.js
  2. Практика написания CLI в Node.js
  3. Механизм модуля Node.js
  4. Проектирование и реализация подключаемой системы внешнего интерфейса
  5. Анализ механизма плагинов Hexo
  6. Как реализовать простое расширение плагина
  7. Публикация и поддержка модулей TypeScript с помощью NPM
  8. пример пакета typescript npm
  9. Публикация пакетов npm через travis-ci
  10. Dynamic load module in plugin from local project node_modules folder
  11. Следуйте старому драйверу, чтобы играть в командную строку Node.
  12. А тем хорошим статьям, которые не успел записать, спасибо!