Я хотел бы поделиться, как связать приложение, которое действует как хост плагина, и как он может загружать установленные плагины динамически.
- Оба приложения и плагины связаны с Webpack
- Приложение и плагины скомпилированы и распределены независимо.
В сети есть несколько человек, которые ищут решение этой проблемы:
- Многопроектная сборка и динамическая загрузка модулей с помощью веб-пакета
- Загрузка готовых пакетов веб-пакетов во время выполнения
- Как выставить объекты из пакета Webpack и вставить внешние библиотеки в скомпилированный пакет?
- Динамическое требование | https://github.com/webpack/webpack/issues/118
Решение, описанное здесь, основано на комментарии @sokra от 17 апреля 2014 года по проблеме Webpack № 118 и слегка адаптирован для работы с Webpack 2. https://github.com/webpack/webpack/issues/118
Основные моменты:
-
Плагину нужен идентификатор (или "URI" ), с помощью которого он регистрируется на сервере backend и уникален для приложения.
-
Чтобы избежать столкновений идентификаторов блоков/модулей для каждого плагина, для загрузки блоков плагинов будут использоваться отдельные функции загрузчика
JSONP
. -
Загрузка плагина инициируется динамически создаваемыми элементами
<script>
(вместоrequire()
), и пусть основное приложение в конечном итоге потребляет экспорт плагинов через обратный вызовJSONP
.
Примечание. Вы можете обнаружить, что формулировка Webpack "JSONP" вводит в заблуждение, так как фактически передается JSON
, но Javascript плагина, завернутый в "функцию загрузчика". На стороне сервера нет прокладки.
Создание плагина
Конфигурация сборки плагинов использует параметры Webpack output.library
и output.libraryTarget
.
Пример конфигурации плагина:
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/' + pluginUri + '/',
filename: 'js/[name].js',
library: pluginIdent,
libraryTarget: 'jsonp'
},
...
}
Чтобы разработчик плагина мог выбрать уникальный идентификатор (или "URI" ) для плагина и сделать его доступным в конфигурации плагина. Здесь я использую переменную pluginURI
:
// unique plugin ID (using dots for namespacing)
var pluginUri = 'com.companyX.pluginY'
Для параметра library
вам также нужно указать уникальное имя для плагина. Webpack будет использовать это имя при создании функций загрузчика JSONP
. Я получаю имя функции из URI модуля:
// transform plugin URI into a valid function name
var pluginIdent = "_" + pluginUri.replace(/\./g, '_')
Обратите внимание, что при установке параметра library
Webpack автоматически получает значение для параметра output.jsonpFunction
.
При создании плагина Webpack генерирует 3 файла дистрибутива:
dist/js/manifest.js
dist/js/vendor.js
dist/js/main.js
Обратите внимание, что vendor.js
и main.js
завернуты в функции загрузчика JSONP, имена которых взяты из output.jsonpFunction
и output.library
соответственно.
Ваш сервер backend должен обслуживать файлы дистрибутива каждого установленного плагина. Например, мой серверный сервер обслуживает содержимое каталога плагинов dist/
под URI плагина в качестве компонента первого пути:
/com.companyX.pluginY/js/manifest.js
/com.companyX.pluginY/js/vendor.js
/com.companyX.pluginY/js/main.js
Вот почему publicPath
установлен в '/' + pluginUri + '/'
в конфигурации плагина примера.
Примечание. Файлы распространения могут использоваться как статические ресурсы. Бэкэнд-серверу не требуется выполнять какие-либо дополнения ( "P" в JSONP
). Файлы распространения "дополняются" Webpack уже во время сборки.
Загрузка плагинов
Предполагается, что основное приложение должно получить список установленных модулей (URI) с серверного сервера.
// retrieved from server
var pluginUris = [
'com.companyX.pluginX',
'com.companyX.pluginY',
'org.organizationX.pluginX',
]
Затем загрузите плагины:
loadPlugins () {
pluginUris.forEach(pluginUri => loadPlugin(pluginUri, function (exports) {
// the exports of the plugin main file are available in `exports`
}))
}
Теперь приложение имеет доступ к экспортированию плагинов. На данный момент исходная проблема загрузки независимого скомпилированного плагина в основном решена: -)
Плагин загружается путем загрузки его 3 кусков (manifest.js
, vendor.js
, main.js
) в последовательности. После загрузки main.js обратный вызов будет вызван.
function loadPlugin (pluginUri, mainCallback) {
installMainCallback(pluginUri, mainCallback)
loadPluginChunk(pluginUri, 'manifest', () =>
loadPluginChunk(pluginUri, 'vendor', () =>
loadPluginChunk(pluginUri, 'main')
)
)
}
Вызов обратного вызова работает, определяя глобальную функцию, имя которой равно output.library
, как в конфигурации плагина. Приложение выводит это имя из pluginURI
(как и в конфигурации плагина).
function installMainCallback (pluginUri, mainCallback) {
var _pluginIdent = pluginIdent(pluginUri)
window[_pluginIdent] = function (exports) {
delete window[_pluginIdent]
mainCallback(exports)
}
}
Кусок загружается динамическим созданием элемента <script>
:
function loadPluginChunk (pluginUri, name, callback) {
return loadScript(pluginChunk(pluginUri, name), callback)
}
function loadScript (url, callback) {
var script = document.createElement('script')
script.src = url
script.onload = function () {
document.head.removeChild(script)
callback && callback()
}
document.head.appendChild(script)
}
Helper:
function pluginIdent (pluginUri) {
return '_' + pluginUri.replace(/\./g, '_')
}
function pluginChunk (pluginUri, name) {
return '/' + pluginUri + '/js/' + name + '.js'
}