9

自定义 Webpack Target

 3 years ago
source link: https://blog.crimx.com/2020/03/29/%E8%87%AA%E5%AE%9A%E4%B9%89-webpack-target/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

由于浏览器扩展有特殊的权限限制,许多前端的开发工具都无法直接派上用场,如之前我解决了热更新分块自动填写到清单的问题。现在我们继续突破下个影响性能的问题:动态加载分块。

Webpack 支持 import() 自动分块并异步加载,这对于大型应用来说是非常有用的功能。虽然浏览器扩展的源文件都在本地,但对于大型应用来说静态加载依然会浪费了不少内存。那么为什么浏览器扩展不支持异步加载呢?这就需要理解 Webpack 是怎么处理的。

(如果只关心如何在浏览器扩展中使用,本文的内容已封装为 webpack-target-webextension 库。)

JSONP

当我们指定(或默认) Webpack Target 为 web 的时候,Webpack runtime 会以 JSONP 方式来加载异步块。那么什么是 JSONP?

JSONP 常用于跨域动态获取数据。如 a.comb.com 请求数据,

  • 首先生成一个回调函数名,如 myCallback
  • 创建全局函数 myCallback 实现加载数据的逻辑;
  • myCallback 作为参数构造请求链接,如 https://b.com/data?callback=myCallback
  • 通过支持跨域的 <script> 标签发起请求,<script src="https://b.com/data?callback=myCallback"></script>
  • 服务器将数据包裹到回调中返回, myCallback(...)
  • 浏览器加载脚本,myCallback 的逻辑被执行。

这种方式为什么在浏览器扩展中会失效呢?我们都知道一些浏览器扩展可以对用户的网页进行修改,如美化或者去广告。这些修改是通过一种叫 content script 类型的脚本实现。每个 content script 可以在作者指定的时机被植入到页面上。虽然 content script 可以修改 DOM,但是 content script 本身是运行在隔离的沙箱环境中的。这个环境可以让 content script 访问部分浏览器扩展 API。

所以当 Webpack 以 JSONP 方式加载异步块的时候,<script> 中的回调会在用户的脚本环境中执行,而扩展环境中的接收回调只能默默等待到超时。

不如来真的

主流浏览器早早就支持了原生的 import() ,那么有没有可能,我们不让 Webpack 生成 JSONP 而直接使用原生的 import()? CRIMX 说 yes!

在 Webpack 中,模块加载的逻辑通过 target 设置来调整。Webpack 4 中预设了几种常见的 target:

Option Description async-node 用于类 Node.js 环境 electron-main 用于 Electron 主进程 electron-renderer 用于 Electron 渲染进程 electron-preload 用于 Electron 渲染进程 node 用于类 Node.js 环境 node-webkit 用于 NWebKit 环境 web 用于类浏览器环境 webworker 用于 WebWorker

很可惜这几种都不支持原生 import(),也不适用浏览器扩展。在 Webpack 5 的预览中明确提到了对 es2015 的支持,同时提供了新的 module 设置。但是离 Webpack 5 正式发布以及生态跟上可能还有一段时间。

最后 target 还支持传入函数以自行实现逻辑。尽管 Webpack 的源码不是很好读,最后还是决定挑战一下,自定义实现一个针对浏览器扩展的 target

其实很简单

首先通过文档找到判断上面预设环境的位置。通过参考 web 的配置可以找到 JSONP 的实现在 JsonpMainTemplatePlugin.js 中。

其中异步块的加载分了三种方式,正常的,预加载的以及预读取的,对应 <script><link>preloadprefetch。全部改成 import() 即可。

其中注意计算块的路径,由于在 content script 中相对路径会根据当前页面计算,而我们需要根据扩展根来算路径。所以函数 jsonpScriptSrc 改为

if (needChunkOnDemandLoadingCode(chunk)) {
  extraCode.push(
    '',
    '// script path function',
    'function webextScriptSrc(chunkId) {',
    Template.indent([
      `var publicPath = ${mainTemplate.requireFn}.p`,
      `var scriptSrcPath = publicPath + ${getScriptSrcPath(
        hash,
        chunk,
        'chunkId'
      )};`,
      `if (!publicPath || !publicPath.includes('://')) {
        return (typeof chrome === 'undefined' ? browser : chrome).runtime.getURL(
          scriptSrcPath
        );
      } else {
        return scriptSrcPath;
      }`
    ]),
    '}'
  )
}

从而利用 runtime.getURL 来计算扩展资源路径。

可以通过 publicPath 来控制根路径。

注意去除 @babel/plugin-syntax-dynamic-import 等插件以免 import() 被转换掉。

Webpack 一些设置的默认值依赖 target 来判断,所以需要手动设置:

module.exports = {
  resolve: {
    mainFields: ['browser', 'module', 'main'],
    aliasFields: ['browser']
  },
  output: {
    globalObject: 'window'
  }
}

完整修改见这里


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK