предисловие
Что такое плагин? Плагин, также известный как Плагин или надстройка, надстройка и т. д., представляет собой программу, написанную в соответствии с определенным стандартным интерфейсом прикладной программы, так что она может расширять исходные несуществующие функции для системы. В то же время, если система поддерживает систему плагинов, она также имеет функции, которые могут быть настроены пользователями.
Основные причины, по которым приложение должно поддерживать плагины, следующие:
- Он может поддерживать сторонних разработчиков для расширения границ возможностей приложения.
- Новые функции могут быть расширены более легко.
- Благодаря дизайну подключаемых модулей размер кода (основного) приложения уменьшается.
Конечно, есть много других преимуществ.
Использование плагинов в компьютерах имеет долгую историю. Например, графическое программное обеспечение поддерживает обработку файлов изображений в различных форматах с помощью подключаемых модулей; почтовые клиенты используют подключаемые модули для шифрования и расшифровки сообщений электронной почты; редакторы или интегрированные среды разработки поддерживают различные языки программирования с помощью подключаемых модулей и так далее.
Во фронтенде также много примеров применения плагинов различными фреймворками и библиотеками, такими как webpack, vuex, dva, babel, PostCSS и т.д., у всех свой набор систем плагинов. Как веб-интерфейс, автор хотел бы кратко обсудить положение с использованием плагинов во внешнем интерфейсе, мышление и принципы плагинов, а также то, как реализовать пользовательскую систему плагинов.
Знакомство с плагином
Прежде чем анализировать дизайн подключаемого модуля, мы можем использовать следующие примеры, чтобы понять, как подключаемый модуль применяется в различных популярных интерфейсных средах и библиотеках.
webpack
В стандартной конфигурации плагинов webpack есть много знакомых нам плагинов, таких как HtmlWebpackPlugin, DllPlugin, ExtractTextWebpackPlugin и т. д., см. официальную документацию:plugins | webpack doc.
Настройка плагинов webpack очень проста, два плагина настраиваются следующим образом:
import webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
const config: webpack.Configuration = {
// ...
plugins: [
new webpack.BannerPlugin({
banner: '(c" ತ,_ತ)'
}),
new HtmlWebpackPlugin()
]
};
export default config;
В webpack основной обязанностью загрузчика является преобразование других модулей, отличных от JavaScript и JSON, в эффективно поддерживаемые модули, которые затем передаются приложению для разбора. Кроме того, плагин отвечает за выполнение большинства задач веб-пакета, таких как оптимизация упаковки, управление ресурсами и так далее. Как основная опора веб-пакета, плагин может в основном решать задачи, которые не может выполнить загрузчик, что показывает его силу.
Компилятор и компиляция в webpack в основном основаны наtapableЭта библиотека построена, а плагин построен на основе жизненного цикла первых двух.
Например, подобный компилятор имеет следующие хуки жизненного цикла (хуки): инициализировать, испускать, делать, компилировать, компилировать и т. д. около 28. Справочные документы:compiler-hooks | webpack. Каждый хук имеет свой тип, асинхронный, синхронный, последовательный, параллельный, страховой, водопадный и т.д.
У компиляции тоже есть хуки жизненного цикла: около 84 buildModule, rebootModule, failedModule, SuccessModule, seal и т. д. Логика триггера такая же, как и у хука компилятора, но имеет разные типы. Справочная документация:compliation-hooks | webpack.
Как написать плагин для вебпака, есть и официальные документы:write a plugin | webpack. Я не буду здесь вдаваться в подробности. Базовая форма плагина выглядит следующим образом:
class FirstWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync("FirstWebpackPlugin", (compilation, callback) => {
// ...
callback();
});
}
}
Плагины регистрируются с помощью tap, tapAsync, tapPromise и т. д. для указанных хуков, и webpack будет вызывать определенные хуки в разных жизненных циклах, а tapable будет выполнять функции, зарегистрированные плагинами.
Около 80% кода в webpack составляют плагины, поэтому webpack на самом деле является управляемым событиями компилятором на основе плагинов.
babel
Я полагаю, что большинство студентов знакомы с Babel. Babel — универсальный многоцелевой компилятор JavaScript. В нашем проекте часто есть файл с именем.babelrc. Поскольку babel — это просто компилятор, он ничего не делает по умолчанию, поэтому нам нужно написать .babelrc, чтобы проинструктировать Babel, что делать, что в основном определяется указанием пресетов и плагинов.
Как следует из названия, плагины — это плагины для Babel, а пресеты — это набор предустановленных плагинов.
Например, обычно в качестве предустановки устанавливается @babel/preset-env.Мы можем написать последний код синтаксиса JavaScript, который можно скомпилировать и преобразовать в соответствующий окончательный код в соответствии с нашей целевой средой.
Затем вы можете установить плагин @babel/plugin-transform-runtime, который может внедрить вспомогательный код инструмента babel в наши скомпилированные файлы, тем самым сэкономив объем скомпилированного кода. .babelrc следующим образом:
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}
Затем мы можем разобрать его с помощью инструмента cli, предоставленного babel:
npx babel --config-file=./.babelrc source.js --out-file compiled.js
Конечно, для Babel существует множество плагинов:babel plugins, охватывающий очень широкий диапазон, от ES2015 до ES2018, а также React, TypeScript и другие. Можно сказать, что современная разработка практически неотделима от Babel и его богатых плагинов.
Принцип работы плагина babel не сложен, Babel сам разбирает входной код в абстрактное синтаксическое дерево (AST), а затем компилирует и выводит целевой код, сам по себе, конечно, ничего не делает, так же, как:
const babel = code => code;
Однако, если внедряется подключаемый модуль, он будет изменять и корректировать AST в процессе промежуточного преобразования (преобразования) с помощью различных инструментов, предоставляемых babel, таких как babylon, babel-traversal, babel-types и т. д. и babel затем скомпилирует модифицированный AST для финального кода. Чтобы достичь конечных потребностей плагина. Схема выглядит следующим образом:
Базовый формат плагина babel выглядит следующим образом:
export default function FirstBabelPlugin({ types: t }) {
return {
name: "plugin-demo",
pre(state) {},
visitor: {
Identifier(path, state) {},
BinaryExpression(path, state) {},
// ...
},
post(state) {}
};
};
В качестве подключаемого модуля Babel, если тип узла AST, к которому необходимо получить доступ, например, Identifier, BinaryExpression и т. д., объявлен как функция, он может настраивать и изменять узел AST. В то же время также предусмотрены функции pre и post hook, которые могут выполняться до и после запуска плагина соответственно.
Конечно, если студенты заинтересованы, вы можете обратиться к этому официальному руководству:babel plugin handbook | Github, и перейдите на этот сайт, чтобы узнать:AST explorer, чтобы вы были знакомы с типом узла AST, вы можете написать свой собственный подключаемый модуль babel для удовлетворения конкретных потребностей.
dva
Как внешнее решение для управления потоком данных, dva, как и webpack и babel, также использует режим плагина. Установите пользовательский плагин в приложении dva следующим образом:
import { create } from "dva-core";
import createImmerPlugin from "dva-immer";
const app = create();
app.use(createImmerPlugin());
Нижний слой два зависит от два ядра. Дизайн плагина также включен, пожалуйста, обратитесь к документу:Plugin.js | dva-core.
Затем dva-core сам разработал несколько хуков, таких как onError, onReducer, onEffect, onHmr и так далее. По сравнению с webpack и babel механизм плагинов dva намного проще.
Плагин dva написан следующим образом:
export default function FirstDvaPlugin() {
return {
onEffect() {},
onReducer() {},
// ...
};
}
После того, как плагин зарегистрирован на dva через app.use(plugin), dva-core будет управлять всеми функциями хуков (простое понимание здесь состоит в том, что каждый хук соответствует массиву функций). Затем через plugin.apply в определенное время будет запущен определенный хук, тем самым пройдя и выполнив соответствующий массив функций.
Идеи дизайна плагинов
Просмотрите рассмотренный выше механизм подключаемых модулей webpack, babel и dva. Можно обнаружить, что дизайн плагина можно в основном разделить на три части: объявление хука, регистрация хука и вызов хука. Как показано на рисунке:
Декларация крюка
Фреймворк должен идентифицировать свои собственные внутренние ключевые события и узлы жизненного цикла. Этот жизненный цикл не обязательно линейный, он также может быть циклическим, точечным и так далее. Каждый хук жизненного цикла в основном соответствует различным сценариям фреймворка. Например, хук инициализации компилятора в веб-пакете означает, что он срабатывает после инициализации объекта компилятора.
Если фреймворк сам спроектировал все хуки, вы можете в основном увидеть бизнес-сценарий и потребовать направления решения самой фреймворка из дизайна хуков.
вызов на крючок
Конечно, хуки обязательно будут вызываться, а хуки, которые не вызываются, теоретически не имеют никакой ценности. Время и место вызова ловушки определяют требования к функциям ловушки.
Например, если есть хук onAppInit, представляющий хук инициализации приложения. Затем его следует вызывать при инициализации приложения, и местом вызова, вероятно, будет основная запись кода приложения.
В то же время для разных сценариев вы можете спроектировать, будет ли этот хук асинхронным или синхронным, параллельным или последовательным и т. д. Предполагая, что хук onAppInit позволит стороне стыковки заблокировать приложение и продолжить работу после завершения трех бизнес-специфических запросов A, B и C, тогда этот хук должен быть спроектирован как асинхронный параллельный хук и вызываться в запись инициализации приложения.
регистрация хука
Конкретные плагины для разных сценариев будут регистрировать определенные хуки (больше или равные одному хуку). Плагины объединяют хуки с разными характеристиками и вставляют в них определенные бизнес-коды. Хук автоматически регистрируется, как только фреймворк загружает плагин. При запуске к указанному хуку также будет выполняться соответствующий бизнес-код, подключенный к конкретному плагину.
обобщать
Дизайн, регистрация и вызов хуков фактически отделяют стабильность и нестабильность от дизайна кода. Ядро фреймворка относительно стабильно, потому что оно слабое или не имеет отношения к бизнесу. Плагин реализует определенный бизнес-код, который будет меняться в любое время с изменением бизнес-требований, поэтому он относительно нестабилен. При этом между плагинами и плагинами нет никакой связи, что равносильно развязке модулей в коде.
Поэтому до тех пор, пока вышеуказанное соотношение может быть удовлетворено, конструкция подключаемого модуля по существу не ограничивается какой-либо конкретной формой реализации.
Реализовать систему плагинов
Сказав так много, мы будем лично практиковаться в том, как писать код, реализовывать объявление хука, вызов и регистрацию, чтобы построить систему плагинов. Автор в основном представит два решения: нативную реализацию и реализацию с использованием базовой библиотеки webpack, соответственно, чтобы познакомить вас с дизайном подключаемых модулей.
собственная реализация
Этот код в основном относится к реализации исходного кода dva-core:
import invariant from 'invariant';
type Hook = (...args: any) => void;
type IKernelPlugin<T extends string | symbol> = Record<T, Hook[]>;
type IPlugin<T extends string | symbol> = Partial<Record<T, Hook | Hook[]>>;
class PluginSystem<K extends string> {
private hooks: IKernelPlugin<K>;
constructor(hooks: K[] = []) {
invariant(hooks.length, `plugin.hooks cannot be empty`);
this.hooks = hooks.reduce((memo, key) => {
memo[key] = [];
return memo;
}, {} as IKernelPlugin<K>);
}
use(plugin: IPlugin<K>) {
const { hooks } = this;
for (let key in plugin) {
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
hooks[key] = hooks[key].concat(plugin[key]);
}
}
}
apply(key: K, defaultHandler: Hook = () => {}) {
const { hooks } = this;
const fns = hooks[key];
return (...args: any) => {
if (fns.length) {
for (const fn of fns) {
fn(...args);
}
} else {
defaultHandler(...args);
}
};
}
get(key: K) {
const { hooks } = this;
invariant(key in hooks, `plugin.get: hook ${key} cannot be got`);
return hooks[key];
}
}
export default PluginSystem;
Приведенный выше код в принципе очень прост, так что разные хуки соответствуют массиву функций. Фреймворк может регистрировать плагины (то есть массив функций, соответствующий хуку, будет обновляться) через метод use в экземпляре PluginSystem, и запускать хук через метод apply.
Приведенный выше метод применения поддерживает только запуск синхронных функций, но его можно немного изменить для поддержки запуска асинхронных функций.
Конечно, после того, как система плагинов будет реализована, мы можем написать два плагина, связанных с бизнес-требованиями, в соответствии с дизайном интерфейса соответствующей системы плагинов:
function createLoggerPlugin(appId: string) {
return {
onAction(action: { type: string; params: any }) {
log(`Log from appId: ${appId}`, action.type, action.params);
}
};
}
function createAppInitPlugin() {
return {
onInit() {
log(`App init, do something`);
}
};
}
Как видите, этот плагин очень прост, по сути, он просто возвращает объект-хук через функцию.
После того, как у вас есть система плагинов и плагины, вы можете зарегистрировать соответствующие плагины в приложении.Примеры следующие:
import { log } from './util';
import Plugin from './Plugin';
type Hook = 'onInit' | 'onAction';
/**
* 基于原生插件构建的 App 示例
*/
(async function App() {
/**
* 初始化插件机制 - 钩子声明
*/
const system = new PluginSystem<Hook>(['onInit', 'onAction']);
const APP_ID = 'a57e41';
const appInitPlugin = createAppInitPlugin();
const loggerPlugin = createLoggerPlugin(APP_ID);
/**
* 插件声明、注册
*/
system.use(appInitPlugin);
system.use(loggerPlugin);
/**
* 插件钩子任意时机调用
*/
system.apply('onInit')();
system.apply('onAction')({
type: 'SET_AUTHOR_INFO',
params: { name: 'sulirc', email: 'ygj2awww@gmail.com' }
});
})();
На данный момент рамочный крючок, бизнес-плагин достигли гармоничной единой экосистемы.
Реализовано с помощью Tapable
Конечно, приведенная выше нативная реализация может быть относительно простой и не может соответствовать более сложным сценариям, таким как асинхронный, параллельный, каскадный, страховой и другие типы хуков, упомянутые выше.Если используется нативная реализация, стоимость и риск действительно будут высокими. .
К счастью, у нас есть основная библиотека зависимостей webpack: tapable. Он предоставляет множество типов перехватчиков, таких как SyncHook для синхронных перехватчиков, AsyncParallelHook для асинхронных параллельных перехватчиков, AsyncSeriesHook для асинхронных последовательных перехватчиков и так далее. Если вы хотите узнать больше о значении типов хуков, вы можете перейти кtapableREADME.
Так как tapable и так чрезвычайно мощен, делать на его основе избыточную инкапсуляцию излишне, поэтому предполагается, что фреймворк объявляет три хука onError, onAction и onInit следующим образом, как в примере кода:
import invariant from 'invariant';
import {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook,
HookMap
} from 'tapable';
export interface IPluginHooks {
onError: SyncHook;
onAction: AsyncParallelHook;
onInit: AsyncSeriesHook;
}
interface IPlugin {
apply: (hooks: IPluginHooks) => void;
}
class TapablePluginSystem {
hooks: IPluginHooks;
constructor(plugins: IPlugin[] = []) {
/**
* 钩子声明、注册
*/
this.hooks = {
onError: new SyncHook(['errMsg']),
onAction: new AsyncParallelHook(['action']),
onInit: new AsyncSeriesHook()
};
if (~plugins.length) {
plugins.forEach(plugin => this.use(plugin));
}
}
use(plugin: IPlugin) {
invariant(plugin.apply, 'plugin.apply cannot be undefined');
plugin.apply(this.hooks);
}
}
export default TapablePluginSystem;
После реализации фреймворка аналогичным образом объявляем два плагина следующим образом:
class LoggerPlugin {
private appId: string;
constructor(appId: string) {
this.appId = appId;
}
apply(hooks: IPluginHooks) {
const PluginType = 'LoggerPlugin';
hooks.onInit.tapPromise(PluginType, () => {
return fetch('LOGGER_INIT');
});
hooks.onAction.tapAsync(PluginType, (action, callback) => {
report(`Log action from appId: ${this.appId}`, action.type, action.params);
fetch('APP_INFO')
.then(() => callback())
.catch(err => callback(err));
});
}
}
class ReportPlugin {
private appId: string;
constructor(appId: string) {
this.appId = appId;
}
apply(hooks: IPluginHooks) {
const PluginType = 'ReportPlugin';
hooks.onError.tap(PluginType, errMsg => {
report(`Report error from appId: ${this.appId}:`, errMsg);
});
hooks.onAction.tapAsync(PluginType, (action, callback) => {
report(`Report action from appId: ${this.appId}:`, action.type, action.params);
fetch('APP_INFO')
.then(() => callback())
.catch(err => callback(err));
});
}
}
Стиль плагина очень похож на стиль плагина webpack, на самом деле принцип очень прост: фреймворк вызывает приложение экземпляра плагина, чтобы плагин мог успешно зарегистрировать хук.
Наконец, приложению необходимо вызвать хук в нужное время и в нужном месте, например:
/**
* 基于 Tapable 插件构建的 App 示例
*/
(async function TapableApp() {
const APP_ID = 'a57e41';
/**
* 插件声明、注册
*/
const plugins = [new LoggerPlugin(APP_ID), new ReportPlugin(APP_ID)];
const system = new TapablePluginSystem(plugins);
/**
* 插件钩子任意时机调用
*/
system.hooks.onInit.promise().then(() => {
log('onInit hooks complete');
});
system.hooks.onAction.callAsync(
{
type: 'SET_AUTHOR_INFO',
params: { name: 'sulirc', email: 'ygj2awww@gmail.com' }
},
(err: any) => {
if (err) {
console.error(err);
return;
}
log('onAction hooks complete');
}
);
// ...
system.hooks.onError.call('Fake Error');
log('onError hooks complete');
})();
После прочтения приведенного выше примера кода у вас появилось более глубокое понимание? Также возможно, что фреймворк-приложение, построенное на основе подключаемых модулей, является очень мощным.Сложное приложение с богатыми интерфейсами хуков может генерировать множество подключаемых модулей и даже сообщество на основе хуков.
Мышление: как применить это в бизнес-требованиях?
На данный момент автор считает, что его понимание плагинов все еще относительно поверхностно и недостаточно глубоко. Но только благодаря написанию этого сообщения в блоге автор больше осознал прелесть режима плагина.
Итак, вопрос в том, как успешно применить вышеуказанную модель к реальной производственной среде, если вы просто скажете «нет», в основном это можно рассматривать только как «гимнастику для мозга».
Как применить шаблон плагина в бизнес-требованиях?Я считаю, что на этот вопрос нет стандартного ответа, но я хочу высказать свои собственные мысли.
Прежде всего, в начале разработки приложения разработчик должен иметь долгосрочное видение и быть в состоянии знать, в каком бизнес-направлении находится приложение? Каковы возможные будущие направления развития? При этом также необходимо определить границы возможностей. После того, как заданы большие рамки и известны будущие бизнес-сценарии приложения, можно подробно перечислить общие бизнес-функции (конечно, я думаю, что также необходимо оставить определенный простор для фантазии, что зачастую является наиболее сложный момент).
Далее, это должен быть научный дизайн кода. Какой крючок для дизайна? А время срабатывания и местонахождение хука (код запускает проблемы управления временем и пространством)? Дизайн хуков тесно связан с бизнес-требованиями, а также с жизненным циклом приложения. Например, приложение React будет иметь ряд жизненных циклов, таких как componentWillMount и componentDidMount, Есть ли у приложения также такие этапы, как инициализация, выборка данных, рендеринг, обновление и уничтожение? Можно ли каждую стадию превратить в хуки, и какие типы хуков подходят для разных стадий? Обо всем стоит подумать.
Как с помощью хорошо разработанных хуков третьи лица могут подключиться к внедрению плагинов? Загружается ли он через конфигурацию или может поддерживать динамическую загрузку, а плагин разрешает динамическую выгрузку? Плагин требует контроля разрешений?
Все вышеперечисленное требует особого учета в конкретном проекте.
резюме
Когда я учился в старшей школе, мой учитель физики однажды сказал: «Первоклассник изучает идеи, второклассник изучает методы, а третьеклассник изучает темы». Изучение класса вещей, изучение идей часто является кратчайшим путем. Когда вы овладеете своим умом, вы часто будете делать выводы из одного случая и проводить аналогии.
Я надеюсь, что вы также сможете немного узнать о методах проектирования и принципах мышления, лежащих в основе подключаемого модуля, из обсуждения системы подключаемых модулей в этой статье. В будущем, независимо от того, с каким API подключаемого модуля фреймворка или библиотеки вы столкнетесь, вы сможете использовать и мыслить с более высокой точки зрения.
После того, как мы освоили идею плагинов, если мы столкнемся с подходящим сценарием, мы должны без колебаний спроектировать и внедрить систему плагинов для конкретного бизнеса, и приложение станет более расширяемым и мощным.Конечно, с расширением возможностей механизма подключаемых модулей приложения также будут обновлены с «Приложений» до «Платформ».
Выше, если эта статья сможет вдохновить и помочь всем, это будет честь для автора~