打包非 JavaScript 静态资源详情

 更新时间:2021年10月21日 16:00  点击:1711 作者:Eli Gao

本文翻译自 https://web.dev/bundling-non-js-resources/,原文未做修改

假设你正在开发一个网络应用程序。在这种情况下,你很可能不仅要处理 JavaScript 模块,还要处理各种其他资源--Web Workers(它也是 JavaScript ,但它拥有一套独立的构建依赖图)、图片、CSS、字体、WebAssembly 模块等等。

一种可行的加载静态资源的办法是在 HTML 中直接引用它们,但通常它们在逻辑上是与其他可重用的组件耦合的。例如,自定义下拉菜单的 CSS 与它的 JavaScript 部分相联系,图标图像与工具栏组件相关,而 WebAssembly 模块与它的 JavaScript 胶水相依赖。在这些情况下,有种更加方便快捷的办法是直接从它们的 JavaScript 模块中引用资源,并在加载相应的组件时动态地加载它们。

然而,大多数大型项目的构建系统都会对内容进行额外的优化和重组--例如打包和最小化(minimize)。构建系统不能执行代码并预测执行的结果是什么,也没理由去遍历判断 JavaScript 中每一个可能的字符串是否是一个资源 URL。那么,如何才能让它们 "看到 "那些由 JavaScript 组件加载的动态资源,并将它们包含在构建产物中呢?

1、打包工具中的自定义导入

一种常见的方法是利用已有的静态导入语法。有些打包工具可能会通过文件扩展名来自动检测格式,而有些其他打包工具则允许插件使用自定义的 URL Scheme比如下面的例子:

// 普通 JavaScript 导入
import { loadImg } from './utils.js';

// 特殊 "URL 导入" 的静态资源
import imageUrl from 'asset-url:./image.png';
import wasmUrl from 'asset-url:./module.wasm';
import workerUrl from 'js-url:./worker.js';

loadImg(imageUrl);
WebAssembly.instantiateStreaming(fetch(wasmUrl));
new Worker(workerUrl);

当一个打包工具插件发现一个导入项带有它所识别的扩展名或 URL Scheme(上面的例子中的 asset-url: 和 js-url: )时,它会将引用的资源添加到构建图中,将其复制到最终目的地,执行适用于资源类型的优化,并返回最终的 URL,以便在运行时使用。

这种方法的好处是:重用 JavaScript 导入语法,保证所有的 URL 都是静态的相对路径,这使得构建系统很容易定位这种依赖关系。

然而,它有一个明显的缺点:这种代码不能直接在浏览器中工作,因为浏览器不知道如何处理那些自定义的导入方案或扩展名。当然,如果你可以控制所有的代码,并且本来就要依靠打包工具进行开发,这听起来还不错。然而为了减少麻烦,直接在浏览器中使用 JavaScript 模块的情况越来越普遍(至少在开发过程中是这样)。一个小 demo 可能根本就不需要打包工具,即使在生产中也不需要。

2、浏览器和打包工具中通用的导入语法

如果你正在开发一个可重用的组件,你会希望它在任何环境下都能发挥作用,无论它是直接在浏览器中使用还是作为一个更大的应用程序的一部分预先构建。大多数现代的打包工具都接受下面这个JavaScript 模块导入语法:

new URL('./relative-path', import.meta.url)

它看着像是一种特殊的语法,然而它确实是一种有效的 JavaScript 表达式,可以直接在浏览器中使用,也可以被打包工具静态地检测出来并加以处理。

使用这个语法,前面的例子可以改写为:

// regular JavaScript import
import { loadImg } from './utils.js';
loadImg(new URL('./image.png', import.meta.url));
WebAssembly.instantiateStreaming(
  fetch(new URL('./module.wasm', import.meta.url)),
  { /* … */ }
);
new Worker(new URL('./worker.js', import.meta.url));


让我们分析一下它是什么原理: new URL(...) 构造函数会基于第二个参数里的绝对URL,解析出第一个参数中相对 URL 所对应的 URL。在我们的例子中,第二个参数是 import.meta.url [1] ,它是当前 JavaScript 模块的 URL ,所以第一个参数可以是相对于它的任何路径。

它的优点和劣势都类似于 动态导入。虽然可以使用 import(...) 导入内容,如 import(someUrl) ,但打包工具会特殊处理带有静态 URL import('./some-static-url.js') 的导入方式:把它作为一种在编译时预处理已知依赖关系的导入方式,把 代码分块 并动态加载。

同样,你可以使用 new URL(...) ,如 new URL(relativeUrl, customAbsoluteBase) ,然而 new URL('...', import.meta.url) 语法可以明确地告诉打包工具预处理依赖,并将其与主 JavaScript 资源打包在一起。

3、模棱两可的相对URL

你可能会想,为什么打包工具不能检测到其他常见的语法--例如,没有 new URL 包装的 fetch('./module.wasm') ?

原因是,与 import 关键字不同,任何动态请求都是相对于文档本身的,而不是相对于当前的JavaScript文件进行解析。比方说,我们有以下结构:

index.html:

<script src="src/main.js" type="module"></script>


src/

main.js

module.wasm

如果你想从 main.js 中加载 module.wasm ,你的第一反应可能是使用 fetch('./module.wasm') 这样的相对路径引用。

然而,fetch不知道它所执行的 JavaScript 文件的 URL,相反,它是相对于文档来解析 URL 的。因此, fetch('./module.wasm') 最终会试图加载 http://example.com/module.wasm ,而不是预期的 http://example.com/src/module.wasm ,从而造成失败(运气更不好的情况下,还可能默默地加载一个与你预期不同的资源)。

通过将相对的URL包装成 new URL('...', import.meta.url) ,你可以避免这个问题,并保证任何提供的URL在传递给任何loader之前都是相对于 当前 JavaScript 模块的 URL(import.meta.url) 解析的。

只要用 fetch(new URL('./module.wasm', import.meta.url)) 代替 fetch('./module.wasm') ,就可以成功地加载预期的 WebAssembly 模块,同时给打包工具一个在构建时找到这些相对路径的可靠方法。

4、工具链中的支持

4.1 打包工具

下面这些打包工具已经支持 new URL 语法:

  • Webpack v5
  • Rollup (通过插件支持: @web/rollup-plugin-import-meta-assets  支持通用资源,而 @surma/rollup-plugin-off-main-thread 支持 Workers.)
  • Parcel v2 (beta) (译者注:在本译文发布时,Parcel V2已经正式发布:https://parceljs.org/blog/v2)
  • Vite

5、 WebAssembly

当使用 WebAssembly 时,你通常不会手动加载 Wasm 模块,而是导入由工具链发出的 JavaScript 胶水代码。下面的工具链可以替你生成 new URL(...) 语法:

5.1  通过Emscripten编译的C/C++

当使用 Emscripten 工具链时,我们可以通过以下选项要求它输出 ES6 模块胶水代码,而非普通 JS 代码:

$ emcc input.cpp -o output.mjs
## 如果你不想用mjs扩展名:
$ emcc input.cpp -o output.js -s EXPORT_ES6


当使用这个选项时,输出的胶水代码将使用new URL(..., import.meta.url) 语法,这样打包工具可以自动找到相关的 Wasm 文件。

通过添加 -pthread 参数,这个语法也可以支持 WebAssembly 线程的编译

$ emcc input.cpp -o output.mjs -pthread
## 如果你不想用mjs扩展名:
$ emcc input.cpp -o output.js -s EXPORT_ES6 -pthread


在这种情况下,生成的Web Worker将以同样的方式被引用,并且也能被打包工具和浏览器正确加载。

5.2 通过 wasm-pack / wasm-bindgen 编译的 Rust

wasm-pack  --WebAssembly 的主要 Rust 工具链,也有几种输出模式。

默认情况下,它将输出一个依赖于 WebAssembly ESM 集成提议 的 JavaScript 模块。在写这篇文章的时候,这个提议仍然是实验性的,只有在使用 Webpack 打包时,输出才会有效。

或者,我们可以通过 -target web 参数要求 wasm-pack 通过输出一个与浏览器兼容的 ES6 模块:

$ wasm-pack build --target web


输出将使用前面所说的 new URL(..., import.meta.url) 语法,而且 Wasm 文件也会被打包工具自动发现。

如果你想通过 Rust 使用 WebAssembly 线程,这就有点复杂了。请查看指南的 相应部分 [13] 以了解更多。

简而言之,你不能使用任意的线程 API,但如果你使用 Rayon [14] ,你可以试试 wasm-bingen-rayon [15] 适配器,这样它就可以生成 Web 上可以运行的 Worker 。 wasm-bindgen-rayon 使用的 JavaScript 胶水 也包括 [16] new URL (...)语法,因此 Workers 也能被打包工具发现和引入。

6、未来的导入方式

6.1  import.meta.resolve

有一个潜在的未来改进是专门的 import.meta.resolve(...) 语法。它将允许以一种更直接的方式解析相对于当前模块的内容,而不需要额外的参数。

// 现在的语法
new URL('...', import.meta.url)

// 未来的语法
await import.meta.resolve('...')

它还能与导入依赖图(import maps)还有自定义解析器更好地整合,因为它和 import 语法通过同一个模块解析系统处理。这对打包工具来说也是一个更可靠的信号,因为它是一个静态语法,不依赖于像 URL 这样的运行时 API 。

import.meta.resolve 已经作为一个 实验性功能 在 Node.js 中实现了,但是关于它在 Web 上应该如何工作 还有一些问题没有定论。

6.2 导入断言

导入断言(import assertions)是一项新功能,允许导入 ECMAScript 模块以外的类型,不过现在只支持JSON 类型。

foo.json

{ "answer": 42 }


main.mjs

import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42


(译者注:关于这个不太符合直觉的语法选择也有点意思 https://github.com/tc39/proposal-import-assertions/issues/12)

它们也可能被打包工具使用,并取代目前由new URL语法所支持的场景,但导入断言中的类型需要一个一个被支持,目前被支持的只有 JSONCSS 模块即将被支持,但其他类型的资源导入仍然需要一个更通用的解决方案。

要想了解更多关于这个功能的信息,请查看 v8.dev上的功能解释 [19] 。

7、小结

正如你所看到的,有各种方法可以在网络上包含非 JavaScript 资源,但它们有各自的优缺点,而且都不能同时在所有工具链中工作。一些未来的提议可能会让我们用专门的语法来导入这些资源,但我们还没有走到这一步。

在那一天到来之前, new URL(..., import.meta.url) 语法是最有希望的解决方案,并且今天已经可以在浏览器、各种捆绑器和 WebAssembly 工具链中工作。

到此这篇关于打包非 JavaScript 静态资源详情的文章就介绍到这了,更多相关打包非 JavaScript 静态资源内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

8、参考资料

[1];import.meta.url: https://v8.dev/features/modules#import-meta

[2];动态导入: https://v8.dev/features/dynamic-import

[3]:代码分块: https://web.dev/reduce-javascript-payloads-with-code-splitting/

[4]:Webpack v5: https://webpack.js.org/guides/asset-modules/#url-assets

[5]:Rollup: https://rollupjs.org/

[6]:@web/rollup-plugin-import-meta-assets: https://modern-web.dev/docs/building/rollup-plugin-import-meta-assets/

[7]:@surma/rollup-plugin-off-main-thread: https://github.com/surma/rollup-plugin-off-main-thread

[8]:Parcel v2 (beta): https://v2.parceljs.org/languages/javascript/#url-dependencies

[9]:Vite: https://vitejs.dev/guide/assets.html#new-url-url-import-meta-url

[10]:WebAssembly: https://web.dev/webassembly-threads/#c

[11]:wasm-pack: https://github.com/rustwasm/wasm-pack

[12]:WebAssembly ESM 集成提议: https://github.com/WebAssembly/esm-integration

[13]:相应部分: https://web.dev/webassembly-threads/#rust

[14]:Rayon: https://github.com/rayon-rs/rayon

[15]:wasm-bingen-rayon: https://github.com/GoogleChromeLabs/wasm-bindgen-rayon

[16]:也包括: https://github.com/GoogleChromeLabs/wasm-bindgen-rayon/blob/4cd0666d2089886d6e8731de2371e7210f848c5d/demo/index.js#L26

[17]:实验性功能: https://nodejs.org/api/esm.html#esm_import_meta_resolve_specifier_parent

[18]:还有一些问题没有定论: https://github.com/WICG/import-maps/issues/79

[19]:v8.dev上的功能解释: https://v8.dev/features/import-assertions

原文出处:https://www.tuicool.com/articles/7JRzeij

[!--infotagslink--]

相关文章

  • 使用PHP+JavaScript将HTML页面转换为图片的实例分享

    这篇文章主要介绍了使用PHP+JavaScript将HTML元素转换为图片的实例分享,文后结果的截图只能体现出替换的字体,也不能说将静态页面转为图片可以加快加载,只是这种做法比较interesting XD需要的朋友可以参考下...2016-04-19
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • 关于JavaScript中name的意义冲突示例介绍

    在昨天的《Javascript权威指南》学习笔记之十:ECMAScript 5 增强的对象模型一文中,对于一段代码的调试出现了一个奇怪现象,现将源代码贴在下面: 复制代码 代码如下: <script type="text/javascript"> function Person(){}...2014-05-31
  • javascript自定义的addClass()方法

    复制代码 代码如下: //element:需要添加新样式的元素,value:新的样式 function addClass(element, value ){ if (!element.className){ element.className = value; }else { newClassName = element.className; newClas...2014-05-31
  • 详解如何将c语言文件打包成exe可执行程序

    这篇文章主要介绍了详解如何将c语言文件打包成exe可执行程序,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-25
  • JavaScript中的this关键字使用方法总结

    在javascritp中,不一定只有对象方法的上下文中才有this, 全局函数调用和其他的几种不同的上下文中也有this指代。 它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下...2015-03-15
  • JavaScript中逗号运算符介绍及使用示例

    有一道js面试题,题目是这样的:下列代码的执行结果是什么,为什么? 复制代码 代码如下: var i, j, k; for (i=0, j=0; i<10, j<6; i++, j++) { k = i+j; } document.write(k); 答案是显示10,这道题主要考察JavaScript的逗...2015-03-15
  • javascript的事件触发器介绍的实现

    事件触发器从字面意思上可以很好的理解,就是用来触发事件的,但是有些没有用过的朋友可能就会迷惑了,事件不是通常都由用户在页面上的实际操作来触发的吗?这个观点不完全正确,因为有些事件必须由程序来实现,如自定义事件,jQue...2014-06-07
  • 详解javascript数组去重问题

    首先,我想到的是另建一个结果数组,用来存储原始数组中不重复的数据。遍历原始数组依次跟结果数组中的元素进行比较,检测是否重复。于是乎,我写出了如下代码A: Array.prototype.clearRepetitionA = function(){ var resul...2015-11-08
  • ActiveX控件与Javascript之间的交互示例

    1、ActiveX向Javascript传参 复制代码 代码如下: <script language="javascript" for="objectname" event="fun1(arg)"> fun2(arg); </script> objectname为ActiveX控件名,通过<object>标签里的id属性设定,如下; 复制...2014-06-07
  • Javascript类型转换的规则实例解析

    这篇文章主要介绍了Javascript类型转换的规则实例解析,涉及到javascript类型转换相关知识,对本文感兴趣的朋友一起学习吧...2016-02-27
  • JavaScript获取浏览器信息的方法

    Window有navigator对象让我们得知浏览器的全部信息.我们可以利用一系列的API函数得知浏览器的信息.JavaScript代码如下:function message(){ txt = "<p>浏览器代码名: " + navigator.appCodeName + "</p>";txt+= "<p>...2015-11-24
  • 详解JavaScript操作HTML DOM的基本方式

    通过 HTML DOM,可访问 JavaScript HTML 文档的所有元素。 HTML DOM (文档对象模型) 当网页被加载时,浏览器会创建页面的文档对象模型(Document Object Model)。 HTML DOM 模型被构造为对象的树: 通过可编程的对象模型,Java...2015-10-23
  • 跟我学习javascript的最新标准ES6

    虽然ES6都还没真正发布,但已经有用ES6重写的程序了,各种关于ES789的提议已经开始了,这你敢信。潮流不是我等大众所能追赶的。潮流虽然太快,但我们不停下学习的步伐,就不会被潮流丢下的,下面来领略下ES6中新特性,一堵新生代JS...2015-11-24
  • javascript设计模式之解释器模式详解

    神马是“解释器模式”?先翻开《GOF》看看Definition:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。在开篇之前还是要科普几个概念: 抽象语法树: 解释器模式并未解释如...2014-06-07
  • JavaScript操作URL的相关内容集锦

    ---恢复内容开始---1.location.href.....(1)self.loction.href="http://www.cnblogs.com/url" window.location.href="http://www.cnblogs.com/url" 以上两个用法相同均为在当前页面打开URL页面 (2)this.locati...2015-10-30
  • javascript实现tab切换的四种方法

    tab切换在网页中很常见,故最近总结了4种实现方法。 首先,写出tab的框架,加上最简单的样式,代码如下: <!DOCTYPE html> <html> <head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><style> *{ pa...2015-11-08
  • 基于JavaScript如何实现私有成员的语法特征及私有成员的实现方式

    前言在面向对象的编程范式中,封装都是必不可少的一个概念,而在诸如 Java,C++等传统的面向对象的语言中, 私有成员是实现封装的一个重要途径。但在 JavaScript 中,确没有在语法特性上对私有成员提供支持, 这也使得开发人员使...2015-10-30
  • JavaScript预解析,对象详解

    这篇文章主要介绍了JavaScript预解析,对象的的相关资料,小编觉得这篇文章写的还不错,需要的朋友可以参考下,希望能够给你带来帮助...2021-11-10
  • javascript实现数独解法

    生生把写过的java版改成javascript版,第一次写,很不专业,见谅。唉,我是有多闲。复制代码 代码如下: var Sudoku = { init: function (str) { this.blank = []; this.fixed = []; this.cell =...2015-03-15