ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Hasura graphql-engine 前端构建插件 unplugin-dynamic-asset-loader:开发期动态资源加载器生成机制解析

Hasura graphql-engine 前端构建插件 unplugin-dynamic-asset-loader:开发期动态资源加载器生成机制解析 后端API网关数据库GraphQL【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址https://gitcode.com/gh_mirrors/gr/graphql-engine点击查看免费下载导读unplugin-dynamic-asset-loader是 Hasura graphql-engine 仓库前端Nx monorepo中的一个构建期插件库其核心职责是在开发dev构建阶段扫描index.html提取其中引用的 JS/CSS 资源并自动生成一个名为assetLoader.js的资源加载器文件供运行时按需从任意 basePath 动态注入脚本与样式。生产构建不会执行该生成逻辑因此该插件专注于开发体验。读完本文你将理解该插件的实现原理HTML 资源提取、加载代码生成、webpack 编译钩子接入以及如何在 Nx 工作区中构建与测试它。原始说明文档见 frontend/libs/nx/unplugin-dynamic-asset-loader/README.md本文以其为骨架并结合该库源码与单元测试进行纵深展开。一、插件定位只服务于开发期原文档对插件的定位表述非常精炼This plugin create an assetLoader file at dev time. It does not do it in production builds.这句话包含了两个关键设计约束dev time 生成在开发构建阶段插件读取打包产物中的index.html解析其script与link relstylesheet标签生成assetLoader.js并发射emit进 webpack compilation生产构建不生成该逻辑并不参与生产环境的资源处理避免污染线上产物。从实现层面看这个约束体现在 unplugin.ts 中插件通过 webpack 的compilation.hooks.processAssets在Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE阶段资源处理较后期的阶段读取编译产物中的index.html再调用生成逻辑最后用compilation.emitAsset(assetLoader.js, new RawSource(...))将生成文件写入产物。选择这一阶段是为了确保其他插件已经把所有资源加入 compilationindex.html中的引用是最终形态。二、目录结构与核心文件该库位于 Nx 工作区frontend/libs/nx/unplugin-dynamic-asset-loader下结构如下frontend/libs/nx/unplugin-dynamic-asset-loader/ ├── src/ │ ├── index.ts # 库对外出口re-export 全部公共 API │ └── lib/ │ ├── assetLoader.ts # 核心逻辑HTML 解析与加载器文件生成 │ ├── assetLoader.spec.ts # Jest 单元测试 │ └── unplugin.ts # unplugin 插件定义webpack 集成 ├── CHANGELOG.md ├── README.md ├── jest.config.ts ├── package.json ├── project.json # Nx 项目配置build/test/lint/publish 目标 ├── tsconfig.json ├── tsconfig.lib.json └── tsconfig.spec.json其中 src/index.ts 对外导出./lib/unplugin与./lib/assetLoader两个模块的所有符号即插件实例与纯函数工具同时暴露。三、核心实现解析assetLoader.tsassetLoader.ts 是整个库的核心它包含三个纯函数全部可独立测试、无副作用。3.1 数据类型定义export type Asset { tag: string; // 原始 HTML 标签字符串outerHTML url: string; // 资源的 src / href 属性值 }; export type JsAsset Asset { type: js; jsModule?: boolean; // 是否为 script typemodule }; export type CssAsset Asset { type: css; }; export type Assets { js: JsAsset[]; css: CssAsset[]; };Assets将资源分为 JS 与 CSS 两类tag保留原始标签形态jsModule标记 ES module 脚本。3.2 extractAssets从 HTML 中提取资源export const extractAssets (html: string): Assets { const assets: Assets { js: [], css: [] }; const domParser new DomParser(); const parsedDocument domParser.parseFromString(html); parsedDocument.getElementsByTagName(script)?.forEach(element { if ( element.getAttribute(src) !element.getAttribute(src)?.startsWith(http) ) { assets.js.push({ tag: element.outerHTML, url: element.getAttribute(src) || not_found, jsModule: element.getAttribute(type) module, type: js, }); } }); parsedDocument .getElementsByAttribute(rel, stylesheet) ?.forEach(element { if ( element.getAttribute(href) !element.getAttribute(href)?.startsWith(http) ) { assets.css.push({ tag: element.outerHTML, url: element.getAttribute(href) || not_found, type: css, }); } }); // ...校验逻辑 return assets; };关键行为使用dom-parser库将 HTML 解析为 DOM只收集非http开头的相对/本地资源外链如 CDN 上的https://...一律跳过——因为动态加载器只需要处理本地打包产物JS 通过getElementsByTagName(script)收集并依据typemodule判断jsModuleCSS 通过getElementsByAttribute(rel, stylesheet)收集严格校验若 CSS 或 JS 资源为空则分别抛出No css assets found, there is an issue with the provided html.与No js assets found, there is an issue with the provided html.防止生成一个空壳加载器。3.3 generateDynamicLoadCalls生成加载调用语句export const generateDynamicLoadCalls (assets: Assets): string { const cssMap assets.css .map(it loadCss(basePath ${it.url});\n) .join(); const jsMap assets.js .map(it { if (it.jsModule) { return loadJs(basePath ${it.url}, module);\n; } return loadJs(basePath ${it.url});\n; }) .join(); return cssMap jsMap; };它为每个资源生成一行调用语句CSS 统一生成loadCss(basePath xxx.css);JS 若是 module 则生成loadJs(basePath xxx.js, module);否则loadJs(basePath xxx.js);调用顺序完全遵循 DOM 中的出现顺序这一点由单元测试明确覆盖见 assetLoader.spec.ts 中 should generate the css in the same order as the dom 等用例输出顺序为 CSS 在前、JS 在后。3.4 generateAssetLoaderFile拼装最终加载器文件export const generateAssetLoaderFile (assets: Assets): string { const loadedAssets generateDynamicLoadCalls(assets); return // THIS FILE IS GENERATED; DO NOT MODIFY BY HAND. const loadCss (url) { const linkElem document.createElement(link); linkElem.rel stylesheet; linkElem.charset UTF-8; linkElem.href url; document.body.append(linkElem); }; const loadJs (url, type) { const scriptElem document.createElement(script); scriptElem.charset UTF-8; scriptElem.src url; if (type) { scriptElem.type type } document.body.append(scriptElem); }; window.__loadConsoleAssetsFromBasePath (root) { const basePath root.endsWith(/) ? root : root /; ${loadedAssets}}; ; };生成的文件具备以下特征文件头声明// THIS FILE IS GENERATED; DO NOT MODIFY BY HAND.明确该文件为构建产物内联定义两个运行时工具函数loadCss与loadJs前者创建link relstylesheet并追加到document.body后者创建scriptmodule 类型时设置typemodule追加到document.body暴露全局函数window.__loadConsoleAssetsFromBasePath(root)调用方传入一个根路径函数自动补全末尾/root.endsWith(/) ? root : root /再拼接各资源的相对 URL 依次动态加载这种运行时传 basePath的设计使得同一份构建产物可以部署到任意子路径下由宿主页面按实际部署位置动态决定资源根目录——这正是 Hasura Console 在多种宿主环境如 Cloud 控制台、自托管等下复用前端资源的关键机制。四、unplugin 集成如何接入构建流程unplugin.ts 使用 unplugin 通用插件抽象封装了上述纯函数使其可面向 webpack以及其他构建工具提供统一入口const unplugin createUnplugin(label ({ name: pluginName, // unplugin-prefixed-name transformInclude(id) { return id.endsWith(index.html); }, buildStart() { /* this.addWatchFile(index.html); */ }, transform(code, id) { return code; }, watchChange(id) { /* ... */ }, webpack(compiler) { compiler.hooks.thisCompilation.tap(pluginName, compilation { compilation.hooks.processAssets.tap( { name: pluginName, stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE, }, assets { const indexHtmlSource compilation .getAsset(index.html) ?.source?.source(); const finalString getFileFromHtml((indexHtmlSource as string) ?? ); console.log(CREATING ASSET LOADER : , finalString); compilation.emitAsset(assetLoader.js, new RawSource(finalString)); } ); }); }, })); export const vitePlugin unplugin.vite; export const rollupPlugin unplugin.rollup; export const webpackPlugin unplugin.webpack; export const esbuildPlugin unplugin.esbuild;需要重点理解的实现细节为什么必须直接 tap webpack代码注释明确说明——index.html来自其他插件Angular/应用构建插件而非资源正常流转路径因此不能在 loader/transform 流程中处理只能在 webpack 编译阶段直接挂钩子processAssets PROCESS_ASSETS_STAGE_SUMMARIZE选用资源处理较晚的阶段确保此时index.html已被其他插件写入 compilation其中引用的资源路径是最终产物路径读取与回写通过compilation.getAsset(index.html).source.source()读取 HTML 字符串调用getFileFromHtml生成加载器内容最后compilation.emitAsset(assetLoader.js, new RawSource(...))把assetLoader.js作为新产物发射过滤 styles.jsgetFileFromHtml中有一行assets.js.filter(asset asset.url ! styles.js)用于剔除 webpack 样式的 JS 引导文件Angular 构建会产出styles.js这类同时承载样式加载的脚本避免重复加载跨构建工具导出通过 unplugin 抽象同时导出vitePlugin、rollupPlugin、webpackPlugin、esbuildPlugin四个实例但 webpack 分支是完整实现。文件内保留了若干注释掉的调试语句console.log、this.warn等说明该插件属于开发期内部工具以调试便利优先。五、单元测试行为契约的验证assetLoader.spec.ts 使用 Jest 对三个纯函数做了详尽的行为验证可作为插件功能契约的权威参考extractAssets 用例使用贴近真实 Angular Console 的exampleHtml测试数据包含 CDN 外链、内联脚本、module 脚本等混合场景能正确提取 CSS 标签link relstylesheet hrefstyles.css/能提取所有加载文件的 JS 标签且自动跳过https://外链与内联脚本测试数据中 4 个 module 脚本runtime.esm.js、polyfills.esm.js、vendor.esm.js、main.esm.js被全部提取正确识别typemodulejsModule为 true/false 两种场景均有断言无 CSS 或无 JS 时分别抛出对应错误。generateDynamicLoadCalls 用例分别验证 CSS 加载语句、JS 加载语句、module JS 加载语句的生成格式验证输出顺序与 DOM 顺序一致CSS 与 JS 各自保持顺序验证合并输出时 CSS 在前、JS 在后的组合顺序。generateAssetLoaderFile 用例以 inline snapshot 断言整个生成文件的完整内容包含文件头、loadCss/loadJs实现、__loadConsoleAssetsFromBasePath函数体以及 basePath 拼接逻辑。运行这些测试的命令为与 README 一致nx test nx-unplugin-dynamic-asset-loaderJest 配置见 jest.config.ts使用ts-jest转译 TS/JS测试文件覆盖moduleFileExtensions: [ts, js, html]覆盖率输出到coverage/libs/nx/unplugin-dynamic-asset-loader。六、构建与项目配置6.1 构建命令README 给出的构建命令为nx build nx-unplugin-dynamic-asset-loader对应 project.json 中的buildtarget执行器nrwl/js:swcSWC 快速编译入口libs/nx/unplugin-dynamic-asset-loader/src/index.ts输出dist/libs/nx/unplugin-dynamic-asset-loader附加产物libs/nx/unplugin-dynamic-asset-loader/*.mdREADME、CHANGELOG 会随包发布依赖类型声明为dependencies。6.2 其他 Nx targetslintnrwl/linter:eslint扫描**/*.ts与**/*.jsontestnrwl/jest:jest使用jest.config.tspassWithNoTests: truepublish通过node tools/scripts/publish.mjs nx-unplugin-dynamic-asset-loader {args.ver} {args.tag}发布并依赖build先构建。项目标签为scope:shared、type:feature、meta:package说明这是一个可发布、共享的前端特性包。package.json 声明包名为unplugin-dynamic-asset-loaderv0.0.1type: commonjs。七、典型应用场景Console 资源的按需挂载从测试数据的exampleHtml与生成的全局函数名__loadConsoleAssetsFromBasePath可以看出该插件服务于 Hasura Console 前端如console-ce的资源加载场景构建期插件把最终index.html中的本地 JS/CSS 引用提取出来生成assetLoader.js运行期宿主页面调用window.__loadConsoleAssetsFromBasePath(/some/base/path/)即可把 Console 的样式与脚本以动态link/script追加的方式挂载到当前文档从而实现把 Console 作为可嵌入资源、部署在任意子路径下的能力开发期生成、生产构建不生成的设计则保证了插件仅影响本地开发/调试流程不改变线上产物结构。结语unplugin-dynamic-asset-loader虽然是一份极简的 README 配一个精简实现但其背后是典型的构建期元编程思路在 dev 构建中从最终 HTML 反向提取资源清单再生成一段纯运行时加载代码。理解它的实现assetLoader.ts 的三个纯函数、unplugin.ts 的 webpack 钩子接入与测试契约assetLoader.spec.ts对于需要实现自定义资源加载器生成类需求的工程实践有直接参考价值。赞分享后端API网关数据库GraphQL【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址https://gitcode.com/gh_mirrors/gr/graphql-engine点击查看免费下载相关推荐WSA 停更之后你电脑上的安卓应用还能用吗WSABuilds 完整上手指南WSA 停更之后你电脑上的安卓应用还能用吗WSABuilds 完整上手指南 微软已于 2025 年 3 月 5 日结束对 Windows Subsystem开发工具Backstage 动态前端插件Dynamic Frontend Plugins基于 Module Federation 的运行时插件加载架构解析Backstage 动态前端插件Dynamic Frontend Plugins基于 Module Federation 的运行时插件加载架构解析 本篇技开发者门户后端前端Gutenberg Asset Loader 深入解析在浏览器端动态加载 WordPress 脚本与样式Gutenberg Asset Loader 深入解析在浏览器端动态加载 WordPress 脚本与样式 wordpress/asset loader 是后端前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表