原文地址
这是“在浏览器中打包 TypeScript 系列”的第 2 部分。
第 1 部分:ES 模块和导入映射import maps
打包和转译( Bundling & Transpiling )
毫无疑问,打包和转译对于 Web 开发至关重要。在深入讨论该主题之前,让我们重申一下什么是打包和转译。
打包是将所有源文件和依赖项合并到单个文件中的过程。
转译是分析每个源文件并修改它以便所有浏览器都可以执行它的过程。这一步允许我们使用最新的 JS/TS 功能,而不必担心是否所有浏览器都支持。
Babel 是最著名的转译工具。
Webpack、Rollup 都是打包工具。
目前,需要先预先进行转译和打包,然后在浏览器中导入的打包后的文件。稍后在内容,我将展示如何在浏览器中完全进行打包和转译。
ESBuild
ESBuild是一个可以转译和打包 JavaScript/TypeScript 项目的单个可执行文件。
ESBuild 并不是唯一可以在浏览器中运行的工具。 SWC 还有一个 WASM 选项。 Rollup 也有浏览器版本。
为什么要在浏览器中打包 ?
您可能正在构建一项服务,用户可以在其中构建代码或自定义现有组件。
用例#1:代码编辑器
假设您正在构建一个在线代码编辑器。用户可以编写 TypeScript 代码并执行它。您有两个选择:A)将 TypeScript 代码发送到服务器,对其进行编译并以浏览器可以执行的 JS 代码进行响应;或 B) 在浏览器中编译 TypeScript 代码并执行它。如果可能,选项 (B) 会更容易,并且无需维护服务器池来执行不受信任的代码。
用例 #2:自定义 Web 组件
我的个人用例是 Bolik.net 服务。 Bolik 允许用户创建自定义 Web 组件。收到用户配置后,我编译 Web 组件源并生成用户可以在网站上导入的单个包。
目前 Bolik 还没有在浏览器中构建 Web 组件。不过,我将在这篇博文中表明这是可能的。
浏览器中的操作方法
与直接在计算机上运行相比,在浏览器中捆绑和打包 略有不同。虽然 WASM 可以在浏览器中运行二进制文件,但仍然存在一个主要区别:文件系统。所有工具都假设它们可以从本地文件系统读取文件。这种情况在浏览器中会并不满足。让我们看看如何规避它。
在浏览器中转译
转译(编译)代码是我们能做的最简单的步骤。 ESBuild 让它变得非常简单。我们只需要导入 ESBuild 库,初始化 WASM 模块,然后就可以转译 TypeScript 了。
const { default: esbuild } = await import("https://esm.sh/esbuild-wasm@0.18.11/");
await esbuild.initialize({
wasmURL: "https://esm.sh/esbuild-wasm@0.18.11/esbuild.wasm",
});
const res = await esbuild.transform(`
let a: number = 2;
`, {
loader: "ts",
});
// Prints: let a = 2;
console.log(res.code);
就像这样,我们将 TypeScript 代码转换为 JavaScript。
ESBuild wasm 模块约为 10MB,因此初始化设置确实需要一些时间。
在浏览器中打包
现在让我们尝试打包 。
const { default: esbuild } = await import("https://esm.sh/esbuild-wasm@0.18.11/");
await esbuild.initialize({
wasmURL: "https://esm.sh/esbuild-wasm@0.18.11/esbuild.wasm",
});
const res = await esbuild.build({
bundle: true,
write: false,
entryPoints: ['main.ts'],
});
这将失败,因为浏览器无权访问文件系统,因此无法读取 maint.ts 。那我们怎样才能让它发挥作用呢?
幸运的是,ESBuild 支持插件。好的,但是编写插件对我们有什么帮助呢?浏览器无法访问本地文件系统,但可以发送HTTP请求!我们的插件可以从服务器获取文件,而不是读取本地文件。
function withBrowserResolver() {
return {
name: "browser-resolver",
async setup(build) {
// Intercept import paths starting with "https://" or "http://" so
// esbuild doesn't attempt to map them to a file system location.
build.onResolve({ filter: /^http[s]{0,1}:\/\// }, (args) => ({
path: args.path,
namespace: "http-url",
}));
// We also want to intercept all import paths inside downloaded
// files and resolve them against the original URL. All of these
// files will be in the "http-url" namespace. Make sure to keep
// the newly resolved URL in the "http-url" namespace so imports
// inside it will also be resolved as URLs recursively.
build.onResolve({ filter: /.*/, namespace: "http-url" }, (args) => ({
path: new URL(args.path, args.importer).toString(),
namespace: "http-url",
}));
// When a URL is loaded, we want to actually download the content
// from the internet.
build.onLoad({ filter: /.*/, namespace: "http-url" }, async (args) => {
const url = new URL(args.path);
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch ${url}: status=${res.statusText}`);
}
const body = await res.text();
return {
contents: body,
// ESBuild can't get extension from a URL so it falls back to js loader.
loader: resolveLoader(url),
};
});
},
};
}
function resolveLoader(url) {
if (url.pathname.endsWith(".ts")) {
return "ts";
}
if (url.pathname.endsWith(".tsx")) {
return "tsx";
}
return undefined;
}
现在我们可以使用它了
const { default: esbuild } = await import("https://esm.sh/esbuild-wasm@0.18.11/");
await esbuild.initialize({
wasmURL: "https://esm.sh/esbuild-wasm@0.18.11/esbuild.wasm",
});
const res = await esbuild.build({
bundle: true,
write: false,
entryPoints: [`${location.protocol}//${location.host}/bundler-demo/main.ts`],
loader: {},
plugins: [withBrowserResolver()],
});
console.log(res.outputFiles[0].text);
它是如何工作的?这需要解释一下。
-
首先我们定义了一个 ESBuild 插件来支持通过 HTTP 请求获取文件。
-
该插件会覆盖以 http:// 或 https:// 开头的每个 ES 模块导入。
-
如果导入的模块想要加载更多文件,那么我们的插件也将能够导入这些依赖项。
-
然后我们要求 ESBuild 打包 我们的项目。
-
请注意,我们的入口点现在是一个 URL。我在此网站上托管了几个由 ESBuild 获取和捆绑的演示文件。
-
在浏览器中执行
我们通常不想只是打包 代码。我们想要对其进行执行,看看它是否有效以及它是如何工作的。幸运的是,我们也可以做到这一点。正如我在第 1 部分中提到的,我们可以通过导入字符串来执行构建的模块:
// Define our module
const code = `export const name = 'James Bond';`;
// Create a URL object
const blob = new Blob([code], { type: "text/javascript" });
const url = URL.createObjectURL(blob);
// Import
const module = import(url);
URL.revokeObjectURL(url); // Garbage collect
// Use imported module
const { name } = await module;
console.log(`Hi from JS ${name}!`);
您可以尝试在线 ESBuild。如下图所示: