我想分享如何捆绑一个充当 plugin host 的应用程序以及它如何加载已安装的插件 dynamically .

  • 应用程序和插件都与Webpack捆绑在一起

  • 应用程序和插件是独立编译和分发的 .

网上有几个人正在寻找这个问题的解决方案:

这里描述的解决方案基于@ 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.libraryoutput.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.jsmain.js 包含在JSONP加载程序函数中,其名称分别取自 output.jsonpFunctionoutput.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.jsvendor.jsmain.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'
}