解决方案:动态加载独立编译的Webpack 2包

Jör*_*ter 48 javascript webpack

我想分享如何捆绑一个充当插件主机的应用程序以及如何动态加载已安装的插件.

  1. 应用程序和插件都与Webpack捆绑在一起
  2. 应用程序和插件是独立编译和分发的.

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

这里描述的解决方案基于@ 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包含在"加载器功能"中.服务器端没有填充.

构建一个插件

插件的构建配置使用Webpack 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'
  },
  ...
}
Run Code Online (Sandbox Code Playgroud)

插件开发人员可以为插件选择一个唯一的ID(或"URI"),并使其在插件配置中可用.在这里我使用变量pluginURI:

// unique plugin ID (using dots for namespacing)
var pluginUri = 'com.companyX.pluginY'
Run Code Online (Sandbox Code Playgroud)

对于该library选项,您还必须为插件指定唯一名称.生成JSONP加载器函数时,Webpack将使用此名称.我从插件URI派生函数名称:

// transform plugin URI into a valid function name
var pluginIdent = "_" + pluginUri.replace(/\./g, '_')
Run Code Online (Sandbox Code Playgroud)

请注意,当library设置该选项时,Webpack会output.jsonpFunction自动为该选项派生值.

构建插件时,Webpack会生成3个分发文件:

dist/js/manifest.js
dist/js/vendor.js
dist/js/main.js
Run Code Online (Sandbox Code Playgroud)

需要注意的是vendor.jsmain.js被包裹在JSONP装载功能,其名称来自取output.jsonpFunction,并output.library分别.

您的后端服务器必须提供每个已安装插件的分发文件.例如,我的后端服务器dist/将插件URI下的插件目录的内容作为第一个路径组件:

/com.companyX.pluginY/js/manifest.js
/com.companyX.pluginY/js/vendor.js
/com.companyX.pluginY/js/main.js
Run Code Online (Sandbox Code Playgroud)

这就是为什么在示例插件配置中publicPath设置的原因'/' + pluginUri + '/'.

注意:分发文件可以作为静态资源提供.后端服务器不需要进行任何填充("P" JSONP).分发文件在构建时已由Webpack"填充".

加载插件

主应用程序应该从后端服务器检索已安装的插件(URI)列表.

// retrieved from server
var pluginUris = [
  'com.companyX.pluginX',
  'com.companyX.pluginY',
  'org.organizationX.pluginX',
]
Run Code Online (Sandbox Code Playgroud)

然后加载插件:

loadPlugins () {
  pluginUris.forEach(pluginUri => loadPlugin(pluginUri, function (exports) {
    // the exports of the plugin's main file are available in `exports`
  }))
}
Run Code Online (Sandbox Code Playgroud)

现在,应用程序可以访问插件的导出.此时,加载独立编译插件的原始问题基本解决了:-)

一个插件是由加载其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')
    )
  )
}
Run Code Online (Sandbox Code Playgroud)

回调调用的工作原理是定义一个全局函数,其名称等于output.library插件配置中的名称.应用程序从中获取该名称pluginUri(就像我们在插件配置中所做的那样).

function installMainCallback (pluginUri, mainCallback) {
  var _pluginIdent = pluginIdent(pluginUri)
  window[_pluginIdent] = function (exports) {
    delete window[_pluginIdent]
    mainCallback(exports)
  }
}
Run Code Online (Sandbox Code Playgroud)

通过动态创建<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)
}
Run Code Online (Sandbox Code Playgroud)

帮手:

function pluginIdent (pluginUri) {
  return '_' + pluginUri.replace(/\./g, '_')
}

function pluginChunk (pluginUri, name) {
  return '/' + pluginUri + '/js/' + name + '.js'
}
Run Code Online (Sandbox Code Playgroud)