предисловие
Первоначально опубликовано в моемблог, добро пожаловать, обратите внимание~
Поздравляем всех с китайским Новым 2019 годом! Эта статья длинная, вам нужно набраться терпения, чтобы прочитать ее~
Некоторое время назад я использовалelectron-vueРазработано кросс-платформенное (в настоящее время поддерживающее три основные настольные операционные системы) бесплатное приложение для загрузки изображений с открытым исходным кодом——PicGo, я наступил на много ям в процессе разработки, причем не только со стороны бизнес-логики самого приложения, но и со стороны самого электрона. В процессе разработки этого приложения я многому научился. Поскольку я также начал изучать электрон с нуля, такой большой опыт также должен вдохновить и дать инструкции начинающим ученикам, которые хотят изучать разработку электронов. Поэтому я написал практический опыт разработки Электрона и объяснил его с точки зрения наиболее близкой к разработке реальных инженерных проектов. Надеюсь помочь всем.
Ожидается, что несколькосерия статейИли расширить с точки зрения:
- Начало работы с электрон-вью
- Простая разработка основного процесса и процесса визуализации
- Представляем базу данных JSON на основе Lodash - lowdb
- Некоторые меры кроссплатформенной совместимости
- Кстати CI релизы и обновления
- Разработка системы плагинов - часть CLI
- Разработка системы плагинов - часть графического интерфейса
- Думаю снова написать...
иллюстрировать
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:
можно увидеть, кромеRenderer
Отрисовка интерфейса, большинство функций обеспечиваетMain
обеспечивается процессом. Для PicGo его нижний уровень должен быть системой процесса загрузки, как показано ниже:
- Ввод (ввод): принять ввод извне, по умолчанию используется путь или полная информация об изображении base64.
- Transformer: преобразуйте ввод в объект, который может быть загружен загрузчиком (включая размер изображения, base64, имя изображения и т. д.).
- Загрузчик: загрузите вывод из конвертера в указанное место,Загрузчиком по умолчанию будет SM.MS.
- Вывод (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 может создать следующий индикатор выполнения загрузки:
системное уведомление
Опубликовать, если что-то пошло не так с загрузкой или если есть какая-то информация, которую необходимо сообщить пользователю через уведомление на уровне системы.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, двух вышеперечисленных частей в основном достаточно:
- Класс Lifecyle отвечает за весь жизненный цикл
- Класс LifecylePlugins отвечает за регистрацию и вызов плагинов.
Но хорошая система плагинов CLI нуждается как минимум в следующих частях (по крайней мере, я так думаю):
- Может быть вызван из командной строки
- Возможность чтения конфигурационных файлов для дополнительной настройки
- Установка плагинов в один клик из командной строки
- Полная настройка плагина в командной строке
- Дружественная информационная подсказка журнала
Здесь вы можете обратиться к инструменту vue-cli3.
Поэтому нам нужно хотя бы следующие запчасти:
- Классы, связанные с операциями командной строки
- Связанные операции с файлом конфигурации
- Классы для установки плагинов, удаления, обновления и других связанных операций
- Плагины загружают связанные классы
- Классы, связанные с выводом информации журнала
Вышеупомянутые части не особенно сильно связаны с самим классом жизненного цикла, поэтому вам не нужно помещать их все в класс жизненного цикла для реализации.
Родственник, мы выпали из одногоCore
В качестве ядра вышеперечисленные классы включены в этот основной класс, который отвечает за регистрацию команд командной строки, загрузку плагинов, оптимизацию информации журнала, вызов жизненного цикла и т. д.
Наконец, этот базовый класс доступен для использования пользователями или разработчиками. Это ядро PicGo-CorePicGo.tsреализация.
Сама реализация PicGo не сложна, она просто вызывает методы вышеприведенных экземпляров класса.
Но обратите внимание, что здесь есть кое-что, о чем раньше не упоминалось. В дополнение к основным подклассам PicGo от PicGo-Core, в основномconstructor
Фаза функции сборки будет проходить в файле с именемctx
параметр. Что это за параметр? Этот параметр является самим классом PicGo.this
. проходя вthis
, подклассы PicGo-Core также могут использовать методы, предоставляемые базовым классом PicGo.
НапримерLogger
Класс реализует красивый вывод журнала командной строки:
Затем в других подклассах вы хотите вызватьLogger
Метод также прост:
ctx.log.success('Hello world!')
вctx
Как мы уже говорили выше, собственный PicGothis
указатель.
Далее мы представим конкретную реализацию каждого класса.
Классы, связанные с выводом журнала
Начнем с этого класса, потому что это самый простой и наименее навязчивый класс. С ним все в порядке, но это вишенка на торте.
Библиотека PicGo для украшения вывода журнала:chalk, его роль заключается в выводе цветного текста командной строки:
Он также очень прост в использовании:
const log = chalk.green('Success')
console.log(log) // 绿色字体的Success
Мы намерены реализовать 4 типа вывода: успех, предупреждение, информация и ошибка:
Итак, создайте следующий класс:
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') // 提供配置文件就用所提供的配置文件
Имея на руках файл конфигурации, нам нужно реализовать только три основные операции:
- Начальная конфигурация
- чтение конфигурации
- Конфигурация записи (конфигурация записи включает создание, обновление, удаление и т. д.)
Начальная конфигурация
Вообще говоря, наша система будет иметь некоторые конфигурации по умолчанию, и 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
, затем назовите его операционным классом подключаемого модуля. Этот класс имеет три основные цели:
- пройти через
npm
Установить плагин - установить - пройти через
npm
Удалить плагин - удалить - пройти через
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
метод. Нам понадобятся следующие шаги:
- пройти первым
package.json
чтобы найти все законные плагины - пройти через
require
загрузить плагин - Поддержание
picgoPlugins
Настройте, чтобы определить, отключен ли плагин - Выявляется при выполнении плагинов, которые не отключены
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
}
}
Мы уже знаем о процессе запуска плагина из предыдущей статьи:
- Сначала запустите жизненный цикл
- При переходе к определенному жизненному циклу, например здесь
beforeTransform
, то на этом этапе получитьbeforeTransformPlugins
эти плагины -
beforeTransformPlugins
Эти плагины предоставляютсяctx.helper.beforeTransformPlugins.register
метод зарегистрирован и доступен черезctx.helper.beforeTransformPlugins.getList()
Получать - После получения плагина каждый будет называться
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
Предусмотренный интерактивный интерфейс:
Если вы использовали что-то вроде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Найдено в репозитории проекта, добро пожаловать в звезду~ Если эта статья поможет вам, это будет мое самое счастливое место. Если вам нравится, пожалуйста, следуйте за мнойблогтак же какСтатьи из этой сериипоследующий прогресс.
Примечание: фотографии в этой статье принадлежат моей личной работе, если не указано иное, если вам нужно перепечатать, пожалуйста, отправьте личное сообщение
использованная литература
Спасибо за эти качественные статьи:
- Разработка интерфейса командной строки (CLI) с помощью Node.js
- Практика написания CLI в Node.js
- Механизм модуля Node.js
- Проектирование и реализация подключаемой системы внешнего интерфейса
- Анализ механизма плагинов Hexo
- Как реализовать простое расширение плагина
- Публикация и поддержка модулей TypeScript с помощью NPM
- пример пакета typescript npm
- Публикация пакетов npm через travis-ci
- Dynamic load module in plugin from local project node_modules folder
- Следуйте старому драйверу, чтобы играть в командную строку Node.
- А тем хорошим статьям, которые не успел записать, спасибо!