Harper.js CDN 实战:用 unpkg + 原生 ESM 零构建在浏览器中运行 Harper 语法检查
2026/9/14 11:16:07 网站建设 项目流程

Harper.js CDN 实战:用 unpkg + 原生 ESM 零构建在浏览器中运行 Harper 语法检查

【免费下载链接】harperOffline, privacy-first grammar checker. Fast, open-source, Rust-powered项目地址: https://gitcode.com/GitHub_Trending/har/harper

Harper 是一个离线、隐私优先的 Rust 语法检查引擎,其 Web 端封装harper.js可以将完整的 Rust/Wasm 引擎加载到浏览器中本地运行,不经过任何服务器。本文基于官方文档《Using a CDN》展开:讲解如何通过 unpkg CDN 以原生 ECMAScript 模块语法消费 Harper,给出可直接复制运行的最小完整示例,并结合packages/harper.js的源码剖析WorkerLinter、二进制变体与 Lint 选项的底层机制,读完即可在自己的 HTML 页面、编辑器插件或前端项目中集成一个纯本地的语法检查器。

一、背景:harper.js 的两种消费方式

harper.js的核心设计是"始终在设备上运行"(always runs on-device),没有服务端依赖、没有隐私顾虑(见 README)。作为 npm 包,它提供两条集成路径:

  1. npm 安装 + 前端构建工具import { WorkerLinter } from 'harper.js',由 Vite/webpack 等打包处理 Wasm 资源;
  2. CDN 直接引用(本文主题):无需任何构建步骤,直接在<script type="module">中通过 unpkg 的 URL 导入。

CDN 方式的前提是所有现代浏览器均支持原生 ESM 导入——这正是官方文档 Using a CDN 强调的关键前提。由于 Harper 的 Rust 核心编译为 WebAssembly,harper.js的发布产物中特意包含了几种"内联二进制"入口(后文详述),使得 CDN 场景下不需要额外的 Wasm 文件托管。

二、最小完整示例:单文件 HTML 集成

官方提供了一个纯 HTML 的参考实现 raw-web/index.html(该示例也可用microserver直接打开预览,见 示例说明)。完整代码如下:

<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <script type="module"> // We can import `harper.js` using native ECMAScript syntax. import { binaryInlined } from 'https://unpkg.com/harper.js@2.4.0/dist/binaryInlined.js'; import { WorkerLinter } from 'https://unpkg.com/harper.js@2.4.0/dist/index.js'; // Since we are working in the browser, we can use either `WorkerLinter`, which doesn't block the event loop, or `LocalLinter`, which does. const linter = new WorkerLinter({ binary: binaryInlined }); // Every time the `<textarea/>` received an input, we process it and update our list. async function onInput(e) { const lints = await linter.lint(e.target.value); const list = document.getElementById('errorlist'); // Clear previous results list.innerHTML = ''; for (const lint of lints) { const item = document.createElement('LI'); const text = document.createTextNode(lint.message()); item.appendChild(text); list.appendChild(text); } } const inputField = document.getElementById('maininput'); inputField.addEventListener('input', onInput); onInput({ target: inputField }); </script> <!--Make the page look good using SimpleCSS--> <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" /> </head> <body> <h1>Demo</h1> <p> This page is a simple example of using <code>harper.js</code> on a plain HTML page with a CDN. It isn't pretty, but it demonstrates the fundamentals of using Harper. Start typing in the text box below to start getting suggestions right in your browser. </p> <!--This is an intentional mistake to highlight the technology.--> <textarea id="maininput">This is an test</textarea> <h2>Errors</h2> <ul id="errorlist"> Loading... </ul> </body> </html>

示例要点拆解:

  • 两个 CDN 导入dist/binaryInlined.js提供 Wasm 二进制的内联形式,dist/index.js是主入口,导出WorkerLinter等 API。仓库根目录相对路径对应 package.json 的exports映射:.指向dist/index.js./binaryInlined指向dist/binaryInlined.js
  • 版本固定:示例中写死了harper.js@2.4.0。注意当前仓库内package.json的版本已是2.9.1,实际接入时应显式固定到你验证过的版本号,这是 CDN 用法最重要的稳定性实践。
  • new WorkerLinter({ binary: binaryInlined }):构造时传入内联二进制。注释明确说明浏览器中可用WorkerLinter(不阻塞事件循环)或LocalLinter(阻塞),示例选择了前者。
  • linter.lint(text)返回Promise<Lint[]>,每个Lint通过lint.message()拿到人类可读的问题描述;页面示例中的This is an test(应为a test)就是故意写错的触发用例。
  • 页面将<textarea>的每次input事件直接送检,实现"边输入边提示"的实时体验。

官方文档站通过 SvelteKit 把这份示例内嵌渲染:example/+server.ts 以?raw方式导入packages/harper.js/examples/raw-web/index.html的原始文本,再用GETtext/html直接返回——所以你在 Harper 官方文档的 "Using a CDN" 页里看到的示例,与仓库中这份 HTML 完全同源。

三、二进制变体:为什么 CDN 场景要用binaryInlined

harper.js发布 4 个二进制入口加 1 个主入口(见 package.json 的 exports):

入口形态典型场景
./binaryWasm 以独立文件(URL)形式加载有静态资源托管的构建项目
./slimBinary独立文件 + 压缩(依赖fflate解压)需要减小网络体积的场景
./binaryInlinedWasm 以 data URL 内联在 JS 中CDN / 单 HTML 文件
./slimBinaryInlined内联 + 压缩CDN / 单 HTML 文件,追求更小体积

binaryInlined的实现极其简洁,见 src/binaries/binaryInlined.ts:

import { default as binaryInlinedUrl } from 'harper-wasm/harper_wasm_bg.wasm?inline'; import { BinaryModuleImpl } from '../BinaryModule'; /** A version of the Harper WebAssembly binary stored inline as a data URL. */ export const binaryInlined = /*@__PURE__*/ BinaryModuleImpl.create(binaryInlinedUrl, 'full');

?inline是 Vite 的资源内联语法:构建时把整个harper_wasm_bg.wasm编译进 JS 的 data URL 中,浏览器执行这段 JS 即获得完整引擎,不需要二次请求 Wasm 文件。这正是 CDN 单文件集成成立的关键。构建侧的配置印证了这一点:vite.config.ts 以 library 模式构建,入口按上述 5 个文件一一对应,formats: ['es']保证产物是纯 ESM(这也是能在现代浏览器里import的前提),minify: falsesideEffects: false则保证产物可读且可被按需树摇。

slim变体额外依赖fflate(package.json dependencies),用于对 Wasm 做压缩传输、运行时解压;如果你的 CDN 场景对首屏体积敏感,可评估slimBinaryInlined

四、WorkerLinter 源码剖析:请求队列与线程模型

WorkerLinter的构造函数与初始化流程位于 src/WorkerLinter/index.ts,核心设计有三点:

  1. 独立 Web Worker 执行:类注释写明 "A Linter that spins up a dedicated web worker to do processing on a separate thread. Main benefit: this Linter will not block the event loop for large documents."。构造函数new WorkerLinter({ binary: binaryInlined })立即创建 worker,并等待其ready消息后再postMessage二进制 URL、方言(dialect)与 wasm 胶水层口味(glue flavor)完成初始化。
  2. 串行请求队列:所有方法(lintapplySuggestiongetLintConfig……)最终都走私有rpc(procName, args),把请求压入requestQueue,由submitRemainingRequests()保证同一时刻只有请求在 worker 中执行(L242-L274)。这使序列化/反序列化状态(Serializer绑定同一 binary)保持一致。
  3. Node 环境不适用:源码注释明确 "This class will not work properly in Node. In that case, just useLocalLinter.",所以服务端脚本(如 Node 里跑 lint 管道)应改用 LocalLinter。

除示例用到的lint外,WorkerLinter暴露的完整 API 面相当丰富,均可在同一实例上调用:

  • 检查:lint(text, options?)organizedLints(text, options?)(按规则分组的检查结果)、isLikelyEnglish(text)isolateEnglish(text)
  • 建议应用:applySuggestion(text, lint, suggestion)直接返回替换后的文本;
  • 规则配置:getLintConfig()/setLintConfig(config)getDefaultLintConfig()getStructuredLintConfig()(供 UI 动态渲染规则开关);
  • 忽略与词库:ignoreLint(s)importIgnoredLints/exportIgnoredLintsimportWords/exportWords/clearWords
  • 方言与统计:setDialect/getDialectsummarizeStatsgenerateStatsFile
  • 生命周期:dispose()会终止 worker 并清空队列,页面卸载时建议调用。

其中LintSuggestion等类型直接再导出自harper-wasm(见 src/main.ts),LintKind枚举覆盖了SpellingGrammarCapitalizationPunctuation等 20 余种问题类别,可用于前端按类别着色或过滤。

五、Lint 选项:lint(text, options)的第二个参数

示例只传了文本,但lint的第二个参数LintOptions(定义于 src/main.ts)提供了细粒度控制:

export interface LintOptions { /** The markup language that is being passed. Defaults to `markdown`. */ language?: 'plaintext' | 'markdown' | 'typst'; regex_mask?: string; /** Force the entirety of the document to be composed of headings. An undefined value is assumed to be false.*/ forceAllHeadings?: boolean; /** Remove overlapping lints. An undefined value is assumed to be true. */ dedup?: boolean; /** Ignore text that is unlikely to be English. An undefined value is assumed to be false. */ isolateEnglish?: boolean; }
字段默认说明
language'markdown'被检查文本的标记语言;对示例中的纯文本 textarea,传{ language: 'plaintext' }语义更精确
regex_mask正则掩码,命中区域的文本不参与检查(可跳过代码、邮箱等片段)
forceAllHeadingsfalse将整个文档视为标题序列
deduptrue去除重叠的检查结果
isolateEnglishfalse过滤掉不太可能是英文的文本段,多语言内容混排时可用

此外harper.js还提供全局规则开关LintConfigRecord<string, boolean | null>,即"规则名 → 启用/禁用"),配合getLintConfig/setLintConfig可以让用户在前端自定义面板里逐项开关 Harper 内置规则;getStructuredLintConfig()返回带Bool/OneOfMany/Group三种设置的树形结构(src/main.ts),是构建"设置页"的推荐数据源。

六、实践清单与限制

  1. 版本必须显式固定:CDN 上harper.js无"latest 构建缓存"保障,示例固定@2.4.0即是范例;升级时先在本地回归 lint 行为。
  2. 浏览器环境才用WorkerLinter:从 WorkerLinter 源码注释 可确认它依赖 Web Worker,Node 中请改用LocalLinter
  3. CDN 产物是纯 ESMvite.config.tsformats: ['es']决定了dist/index.js只能被import消费,不支持requiresideEffects: false让打包场景下可以安全树摇。
  4. 内联体积的取舍binaryInlined把完整 Wasm 打进 JS,首次加载体积较大;若页面部署在自家 CDN 并能托管静态资源,可改用./binary(URL 形式)把 Wasm 交给浏览器缓存,仅 CDN 直连场景才需要内联变体。
  5. 检查是纯本地的:整个流程没有任何 Harper 自有后端参与,linter.lint()的结果完全来自用户本机浏览器中的 Wasm 引擎,这与 Harper "privacy-first" 的定位一致。

小结

通过 unpkg CDN + 原生 ESM,只需两段import和一个<script type="module">,即可在零构建、零后端的纯 HTML 页面中运行 Harper 全量语法检查:binaryInlined解决 Wasm 交付,WorkerLinter解决线程与事件循环问题,lint(text, options)+LintConfig解决检查行为控制。所有关键实现都能在仓库中逐行对照——示例见 examples/raw-web/index.html,入口与导出见 src/main.ts,Worker 实现见 src/WorkerLinter/index.ts,构建配置见 vite.config.ts。

【免费下载链接】harperOffline, privacy-first grammar checker. Fast, open-source, Rust-powered项目地址: https://gitcode.com/GitHub_Trending/har/harper

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询