ARTICLE DETAIL

资讯详情

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

Docsify 原生 ESM + TypeScript 类型消费指南:解读 test/consume-types 参考示例

Docsify 原生 ESM + TypeScript 类型消费指南:解读 test/consume-types 参考示例 Docsify 原生 ESM TypeScript 类型消费指南解读 test/consume-types 参考示例【免费下载链接】docsify A magical documentation site generator.项目地址: https://gitcode.com/gh_mirrors/do/docsify本指南围绕 docsify 仓库中的 test/consume-types 示例工程展开讲解如何在不使用任何构建工具无 bundler、无 npm script 打包的前提下通过浏览器原生 ESMscript typeimportmapscript typemodule直接以模块方式消费 docsify并用 TypeScript 类型检查证明其类型定义.d.ts真实可用。读完本文你将掌握 importmap 依赖映射、CommonJS 库转 ESM 的包装技巧、Service Worker 修补非标准模块路径等一套可复用的「无构建工具前端工程」实战方案。这个示例工程要解决什么问题docsify 是一个「神奇」的文档站点生成器官方推荐的常规用法是在index.html里用普通script标签全局引入构建产物IIFE 格式的dist/docsify.js配合window.$docsify全局配置对象完成初始化。但很多下游项目希望以现代 ESM 模块的方式集成 docsify像import { Docsify } from docsify这样按需导入并能在 IDE 中获得完整的类型提示。test/consume-types示例就是为了验证这条消费路径而存在它通过浏览器原生 ESM直接 import docsify 的源码模块用TypeScript 类型检查tsc --noEmit证明 docsify 发布的类型定义是真实可用的验证下游项目通过 ESM 消费 docsify 时IDE 的Go to Definition转到定义能直接跳转到 docsify 源码而非仅仅停在声明文件上。示例工程的关键文件如下文件职责index.html承载importmap依赖映射与入口脚本加载example.jsESM 方式导入并实例化Docsify含类型验证用例prism.js将 CommonJS 的 PrismJS 包装为 ESM 模块sw.jsService Worker修补 node_modules 中的非标准模块路径register-sw.jsService Worker 注册与激活协调逻辑tsconfig.jsonTypeScript 严格检查配置package.json类型检查与本地服务脚本无构建工具importmap 如何映射 docsify 及其依赖整个示例「不依赖构建工具」靠的是浏览器原生的 importmap 机制。在 index.html 中script typeimportmap告诉浏览器去哪找docsify及它的运行时依赖script typeimportmap { imports: { docsify: /node_modules/docsify/src/core/Docsify.js, docsify/: /node_modules/docsify/, prismjs: /prism.js, prismjs/: /node_modules/prismjs/, marked: /node_modules/marked/lib/marked.esm.js, marked/: /node_modules/marked/, tinydate: /node_modules/tinydate/dist/tinydate.mjs, common-tags: /node_modules/common-tags/es/index.js } } /script几个值得注意的映射细节docsify指向源码而非构建产物映射到 src/core/Docsify.js即直接消费 docsify 的源码模块。这样做的好处是Go to Definition时能直接看到真实实现IDE 会通过sourceMap/ declaration 关联回源码。同时包尾的docsify/前缀映射保证了docsify/子路径导入如docsify/xxx都能落到/node_modules/docsify/目录下。prismjs指向自定义包装文件/prism.jsPrismJS 是 CommonJS 格式原生 ESM 无法直接解析因此 importmap 将prismjs单独映射到 prism.js 这个 ESM 包装器详见下文。marked、tinydate、common-tags是 docsify 运行时的其他第三方依赖importmap 将其分别指向 node_modules 中各自的 ESM 入口文件如marked/lib/marked.esm.js、tinydate/dist/tinydate.mjs。index.html还顺带通过link relstylesheet引入了 docsify 的主题样式link relstylesheet href/node_modules/docsify/dist/themes/core.css /并在末尾以typemodule加载入口脚本script src./example.js typemodule/script入口脚本await import 的顺序为何如此重要example.js 是消费 docsify 的演示入口核心流程如下import ./register-sw.js; // 先注册 Service Worker // FIXME hack: 先导入 prismjs确保全局 Prism 已就绪 await import(prismjs); // 再导入 Docsify否则会运行时错误 const { Docsify } await import(docsify); const d new Docsify({ el: #app, name: Vanilla ESM TypeScript Example, themeColor: deeppink, hideSidebar: false, collapseSidebarGroups: true, collapsibleSidebarGroups: true, // ts-expect-error invalid property to test that type checking works blahblah: 123, }); console.log(d); // ts-expect-error global types not available to ESM window.Docsify;为什么用await import()而不是静态import这是整个示例中最「tricky」的一点。docsify 源码如 src/core/Docsify.js 顶部存在import prism from prismjs以及编译器中对prismjs/components/prism-markup-templating.js的深层依赖而这些依赖假定Prism 已作为全局变量存在。但「Prism 是否是全局」无法被 ES Module 系统的静态分析捕获——如果用静态import声明 docsify模块图会在prism.js执行完之前就被求值从而触发运行时错误。因此示例刻意采用await import()动态导入先await import(prismjs)确保包装器执行完毕并挂好window.Prism全局变量再await import(docsify)此时 Prism 全局已就绪docsify 的模块图可以安全求值。类型验证的两处点睛之笔ts-expect-error invalid property to test that type checking works配合blahblah: 123docsify 的配置类型定义 DocsifyConfig 并不包含blahblah属性如果类型系统失效这一行会报「非预期错误」tsc --noEmit就会失败——它反向证明了配置项的类型检查是严格生效的。ts-expect-error global types not available to ESM配合window.Docsify说明通过 ESM 消费时docsify 的全局变量形态window.Docsify对 ESM 模块并不可见全局类型只在经典脚本场景通过 globals.ts 声明这也是 ESM 消费方式与传统script全局方式的核心差异。把 CommonJS 包装成 ESMprism.js 的完整思路prism.js 是一个用 ESM 骗过 CommonJS的经典包装器完整代码如下// 拉取 PrismJS 的 CommonJS 源码文本 const __prismCjs await fetch(/node_modules/prismjs/prism.js).then(res res.text(), ); // 模拟 CommonJS 环境 const module { exports: {} }; const exports module.exports; // 在 module.exports 处于作用域内的情况下执行 CommonJS 代码 eval(__prismCjs); // 导出 Prism 对象 const _Prism module.exports; export default _Prism; // 同时把 Prism 挂到全局因为 docsify 的编译器会从 // prismjs/components/prism-markup-templating.js 导入并依赖全局 Prism window.Prism _Prism;它的工作原理分四步fetch获取源码文本通过网络请求读取 node_modules 中 PrismJS 的 CommonJS 文件内容以字符串形式拿到伪造 CJS 环境构造module { exports: {} }与exports变量让 CommonJS 代码的module.exports ...有处可写eval执行在伪造环境下运行源码字符串PrismJS 的导出内容落入module.exports双重导出既export default _Prism让 ESM 侧拿到 Prism 对象又window.Prism _Prism补齐 docsify 依赖的全局变量。代码中的ts-expect-error FIXME get rid of this ugly global dependency hack in Docsify注释直白地承认这是丑陋的全局依赖 hack也印证了 docsify 内部见 render/compiler 对 prism 的引用链路确实存在对全局 Prism 的隐式依赖——这正是该示例存在的意义之一暴露并绕开这些历史包袱。Service Worker修补 node_modules 的非标准模块路径即便有了 importmap浏览器原生 ESM 仍要求导入路径带明确文件扩展名而部分第三方库包括 docsify 依赖图里的模块存在import some-lib/foo/bar无扩展名或import some-lib/foo/bar/以斜杠结尾、隐含 index.js这类非标准导入路径。原生浏览器解析会直接 404。sw.js 用 Service Worker 在请求层面解决这个问题。核心 fetch 逻辑如下仅处理/node_modules/路径下的请求其余请求原样放行scope.addEventListener(fetch, event { const url new URL(event.request.url); // 非 node_modules 路径直接放行 if (!url.pathname.startsWith(/node_modules/)) { event.respondWith(fetch(url.href)); return; } const parts url.pathname.split(/); const fileName parts.pop(); const ext fileName.includes(.) ? fileName.split(.).pop() : ; // 情形一some-lib/foo/bar无扩展名——先试 .js失败再试 /index.js if (fileName ! ext ) { event.respondWith( new Promise(async resolve { try { const response await tryJs(); const mimeType response.headers.get(Content-Type) || ; if (response.ok mimeType.includes(javascript)) { resolve(response); } else { throw new Error(Not JS); } } catch { resolve(await tryIndexJs()); } }), ); return; } // 情形二some-lib/foo/bar/以斜杠结尾——追加 index.js if (fileName ) { const tryIndexJs new URL(url); tryIndexJs.href index.js; event.respondWith(fetch(tryIndexJs)); return; } // 其他情况正常请求 event.respondWith(fetch(url)); });补丁策略总结为一张表请求路径形态处理策略some-lib/foo/bar无扩展名先试bar.js若响应非 JS404 或 MIME 不符再试bar/index.jssome-lib/foo/bar/斜杠结尾直接追加index.js其他已有扩展名等原样fetch非/node_modules/路径原样fetch不干预同时 sw.js 在install/activate阶段分别调用skipWaiting()与clients.claim()保证新 Service Worker 立即生效便于测试场景下始终使用最新补丁逻辑。注册与激活协调register-sw.js 的细节register-sw.js 处理 Service Worker 的注册与「旧 worker → 新 worker」的切换协调。它写得比较啰嗦但每个分支都有明确目的以type: module、updateViaCache: none注册/sw.js作用域为/若已存在 waiting worker等待其statechange到activated后再继续若激活的 worker 尚未控制当前页面则queueReload()触发一次刷新首载时无 controller等待首次controllerchange监听updatefound捕获新安装的 worker 状态变化同样在必要时排队刷新最后await registration.update()强制检查更新并await navigator.serviceWorker.ready等待首个活跃 worker。queueReload()通过reloadQueued标志位保证只刷新一次避免多个controllerchange事件叠加导致循环刷新。注释中Continue app bootstrap here表明只有 Service Worker 就绪并接管页面后应用主体docsify 启动才应继续。两条命令类型检查与本地预览package.json 提供了两条命令命令作用底层实现npm run typecheck对整个示例工程做 TypeScript 类型检查tsc --noEmitnpm run serve在http://localhost:5500启动静态服务验证原生 ESM 用法可用five-server . --openfalse --ignorePatternnode_modules依赖方面docsify: file:../../——以本地文件协议方式把仓库根目录即 docsify 自身作为依赖安装从而直接消费当前源码与类型定义开发依赖five-server轻量静态服务器与typescript^7.0.2通过overrides锁定body-parser、path-to-regexp、qs、ws等传递依赖的版本规避上游冲突。tsconfig.json 的关键选项tsconfig.json 中与检查 JS 代码的类型直接相关的选项{ compilerOptions: { allowJs: true, checkJs: true, module: esnext, moduleResolution: bundler, target: esnext, strict: true, noEmit: true, lib: [DOM, ESNext, WebWorker], skipLibCheck: true, skipDefaultLibCheck: true } }allowJs checkJs允许且强制检查.js文件示例入口是example.jsmoduleResolution: bundler匹配无构建工具、靠 importmap 运行时解析的消费场景无需打包器也能让 TS 正确解析docsify包导出lib中加入WebWorker让sw.js/register-sw.js中的ServiceWorkerGlobalScope、navigator.serviceWorker类型可用strict: truenoEmit: true严格模式下只做类型检查不产出任何编译文件。类型从何而来docsify 的模块化导出与类型声明test/consume-types能拿到完整类型前提是 docsify 本身发布了模块入口与类型声明。仓库根 package.json 中的关键字段type: module, main: dist/docsify.js, types: dist/docsify.module.d.ts, exports: { .: { types: ./dist/docsify.module.d.ts, import: ./dist/docsify.module.js }, ./*: ./* }exports[.]的types条件指向 ESM 产物的声明文件dist/docsify.module.d.tsimport条件指向dist/docsify.module.js——这正是 TS 在moduleResolution: bundler下解析docsify包类型与 ESM 入口的依据dist/docsify.module.js由 rollup.config.js 中的docsifyEsmConfig构建输入src/core/module.js、输出格式es类型声明文件本身由src/core/globals.ts、src/core/modules.ts等类型源码在构建期生成globals.ts声明了Window.$docsify、Window.Docsify、Prism等全局形状modules.ts则声明了*.css模块让 CSS 导入具备类型。因此示例中const { Docsify } await import(docsify)拿到的Docsify是 src/core/Docsify.js 导出的类——它通过多层混入Fetch(Events(Render(VirtualRoutes(Router(Lifecycle(Object)))))组合了路由、渲染、数据获取、事件、虚拟路由等能力构造时依次执行initLifecycle→initPlugin→callHook(init)→initRouter→initRender→initEvent→initFetch→callHook(mounted)的完整初始化链。读者可以在 IDE 中对Docsify按Go to Definition验证是否如示例所述直达这一源码文件。延展如何将这套方案迁移到自己的项目如果你希望在自己的站点里复刻这种「无构建工具 原生 ESM 类型安全」的 docsify 集成方式可以按以下步骤操作安装依赖npm install docsify确保node_modules/docsify中存在模块产物与类型声明配置 importmap参照 index.html 把docsify、prismjs、marked等映射到 node_modules 对应文件PrismJS 需用 prism.js 这类包装器转为 ESM 并挂全局部署 Service Worker注册 sw.js 的补丁逻辑拦截/node_modules/下无扩展名与目录式导入动态导入启动在入口模块中先await import(prismjs)再await import(docsify)并new Docsify({ el: #app, ... })接入类型检查复制 tsconfig.json 的allowJs/checkJs/moduleResolution: bundler配置运行npm run typecheck验证类型链路。也可以参考仓库中其他与模块消费相关的测试如 test/e2e/module.test.js进一步了解 docsify ESM 形态的验证方式。小结test/consume-types表面上是 docsify 仓库里的一个示例目录实际上浓缩了一套完整的「现代 ESM 消费老旧库」工程方法论importmap 做静态依赖映射、包装文件做 CJS→ESM 格式转换、Service Worker 做模块路径补丁、await import()控制初始化时序、TS 严格检查反向验证类型定义。同时它也清晰展示了 docsify 作为可编程模块的一面——Docsify类、docsify包导出util、dom、marked、Compiler、slugify、get、version见 src/core/Docsify.js都是 ESM 消费者可以直接使用的公共 API。无论你是想无构建工具地集成 docsify还是想了解浏览器原生 ESM 与 CommonJS 生态共存的通用解法这个示例都值得逐文件细读。【免费下载链接】docsify A magical documentation site generator.项目地址: https://gitcode.com/gh_mirrors/do/docsify创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表