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 包,它提供两条集成路径:
- npm 安装 + 前端构建工具:
import { WorkerLinter } from 'harper.js',由 Vite/webpack 等打包处理 Wasm 资源; - 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的原始文本,再用GET以text/html直接返回——所以你在 Harper 官方文档的 "Using a CDN" 页里看到的示例,与仓库中这份 HTML 完全同源。
三、二进制变体:为什么 CDN 场景要用binaryInlined
harper.js发布 4 个二进制入口加 1 个主入口(见 package.json 的 exports):
| 入口 | 形态 | 典型场景 |
|---|---|---|
./binary | Wasm 以独立文件(URL)形式加载 | 有静态资源托管的构建项目 |
./slimBinary | 独立文件 + 压缩(依赖fflate解压) | 需要减小网络体积的场景 |
./binaryInlined | Wasm 以 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: false与sideEffects: false则保证产物可读且可被按需树摇。
slim变体额外依赖fflate(package.json dependencies),用于对 Wasm 做压缩传输、运行时解压;如果你的 CDN 场景对首屏体积敏感,可评估slimBinaryInlined。
四、WorkerLinter 源码剖析:请求队列与线程模型
WorkerLinter的构造函数与初始化流程位于 src/WorkerLinter/index.ts,核心设计有三点:
- 独立 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)完成初始化。 - 串行请求队列:所有方法(
lint、applySuggestion、getLintConfig……)最终都走私有rpc(procName, args),把请求压入requestQueue,由submitRemainingRequests()保证同一时刻只有请求在 worker 中执行(L242-L274)。这使序列化/反序列化状态(Serializer绑定同一 binary)保持一致。 - Node 环境不适用:源码注释明确 "This class will not work properly in Node. In that case, just use
LocalLinter.",所以服务端脚本(如 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/exportIgnoredLints、importWords/exportWords/clearWords; - 方言与统计:
setDialect/getDialect、summarizeStats、generateStatsFile; - 生命周期:
dispose()会终止 worker 并清空队列,页面卸载时建议调用。
其中Lint、Suggestion等类型直接再导出自harper-wasm(见 src/main.ts),LintKind枚举覆盖了Spelling、Grammar、Capitalization、Punctuation等 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 | 无 | 正则掩码,命中区域的文本不参与检查(可跳过代码、邮箱等片段) |
forceAllHeadings | false | 将整个文档视为标题序列 |
dedup | true | 去除重叠的检查结果 |
isolateEnglish | false | 过滤掉不太可能是英文的文本段,多语言内容混排时可用 |
此外harper.js还提供全局规则开关LintConfig(Record<string, boolean | null>,即"规则名 → 启用/禁用"),配合getLintConfig/setLintConfig可以让用户在前端自定义面板里逐项开关 Harper 内置规则;getStructuredLintConfig()返回带Bool/OneOfMany/Group三种设置的树形结构(src/main.ts),是构建"设置页"的推荐数据源。
六、实践清单与限制
- 版本必须显式固定:CDN 上
harper.js无"latest 构建缓存"保障,示例固定@2.4.0即是范例;升级时先在本地回归 lint 行为。 - 浏览器环境才用
WorkerLinter:从 WorkerLinter 源码注释 可确认它依赖 Web Worker,Node 中请改用LocalLinter。 - CDN 产物是纯 ESM:
vite.config.ts中formats: ['es']决定了dist/index.js只能被import消费,不支持require;sideEffects: false让打包场景下可以安全树摇。 - 内联体积的取舍:
binaryInlined把完整 Wasm 打进 JS,首次加载体积较大;若页面部署在自家 CDN 并能托管静态资源,可改用./binary(URL 形式)把 Wasm 交给浏览器缓存,仅 CDN 直连场景才需要内联变体。 - 检查是纯本地的:整个流程没有任何 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),仅供参考