我想分享如何捆绑一个充当 plugin host 的应用程序以及它如何加载已安装的插件 dynamically .
-
应用程序和插件都与Webpack捆绑在一起
-
应用程序和插件是独立编译和分发的 .
网上有几个人正在寻找这个问题的解决方案:
-
Multi-project build and dynamically loading modules with webpack
-
How to expose objects from Webpack bundle and inject external libs into compiled bundle?
这里描述的解决方案基于@ sokra 2014年4月17日关于Webpack问题#118的评论,并略微调整以便与Webpack 2一起使用.https://github.com/webpack/webpack/issues/118
要点:
-
插件需要一个ID(或“URI”),它在后端服务器上注册,并且对应用程序是唯一的 .
-
为了避免每个插件的块/模块ID冲突,将使用单独的
JSONP
加载器函数来加载插件的块 . -
加载插件是由动态创建的
<script>
元素(而不是require()
)启动的,让主应用程序最终通过JSONP
回调消耗插件的导出 .
注意:您可能会发现Webpack的"JSONP"措辞具有误导性,因为实际上没有转移 JSON
但插件的Javascript包含在"loader function"中 . 服务器端没有填充 .
Building a plugin
一个插件's build configuration uses Webpack' s 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'
},
...
}
由插件开发人员为插件选择一个唯一的ID(或"URI")并使其在插件配置中可用 . 这里我使用变量 pluginURI
:
// unique plugin ID (using dots for namespacing)
var pluginUri = 'com.companyX.pluginY'
对于 library
选项,您还必须为插件指定唯一的名称 . 生成 JSONP
加载程序函数时,Webpack将使用此名称 . 我从插件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
.
您的后端服务器必须提供每个已安装插件的分发文件 . 例如,我的后端服务器将插件的URI下的插件 dist/
目录的内容作为第一个路径组件提供:
/com.companyX.pluginY/js/manifest.js
/com.companyX.pluginY/js/vendor.js
/com.companyX.pluginY/js/main.js
这就是为什么 publicPath
在示例插件配置中设置为 '/' + pluginUri + '/'
的原因 .
注意:分发文件可以作为静态资源提供 . 后端服务器不需要执行任何填充( JSONP
中的"P") . Webpack已经在构建时分发文件"padded" .
Loading plugins
主应用程序应该从后端服务器检索已安装的插件(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's 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)
}
帮手:
function pluginIdent (pluginUri) {
return '_' + pluginUri.replace(/\./g, '_')
}
function pluginChunk (pluginUri, name) {
return '/' + pluginUri + '/js/' + name + '.js'
}