placefill
v1.0.0
Published
Lightweight placeholder filling for strings, objects, and arrays.
Maintainers
Readme
placefill
轻量的占位符填充 SDK,面向字符串、对象和数组。
placefill 解决的是“把数据填进模板”这件事。它不是完整模板引擎,也不会把 HTML 解析成 AST。
为什么用 placefill
- 深度处理对象和数组中的字符串
- 支持
user.name、items.0、matrix.1.2这类路径 - 对纯占位符字符串保留原始值类型,比如
{{count}} - 支持受控的同步函数执行
- 支持轻量标签模板,适合富文本片段拼装
- 未注册标签会透明保留,同时继续处理已知子标签和占位符
- 零运行时依赖
适合的场景
适合在生产环境里做可预测、低复杂度的模板填充:
- 事务消息:订单通知、客服工单摘要、营销文案
- 对象 payload 生成:邮件 payload、推送 payload、内部配置对象
- CMS / 内容拼装:文章片段、CTA 模块、本地化富文本片段
- 编译一次多次渲染:i18n 片段、服务端重复渲染、模板注册表
不适合的场景
下面这些需求不建议用 placefill:
- 条件判断、循环、异步表达式、任意表达式求值
- 完整 HTML 解析或 AST 变换
- 完整组件系统或模板语言
安装
# 发布到 npm 前,可先用本地 tarball 安装
npm pack
npm install ./placefill-0.1.0.tgz
# or
pnpm add ./placefill-0.1.0.tgz
# or
bun add ./placefill-0.1.0.tgz
# 发布到 npm 后
npm install placefill
# or
pnpm add placefill
# or
bun add placefill运行环境
placefill 面向能运行 ES2020 产物的现代浏览器、Node.js 和 edge runtime。
- 零运行时依赖
- Node.js
>=16.20 - 适合前端应用、服务端 handler、worker 和 CLI 脚本
- API 保持同步、可预测,便于排障和压测
快速开始
import { fill } from "placefill";
const result = fill(
{
title: "Hello {{user.name}}",
firstItem: "{{items.0}}",
},
{
user: { name: "Lin" },
items: ["A", "B"],
},
);
console.log(result.value);
// {
// title: "Hello Lin",
// firstItem: "A"
// }编译一次,多次渲染
同一个模板会重复渲染时,优先使用 compile。
import { compile } from "placefill";
const template = compile('<Link href="/orders/{{order.id}}">{{user.name}}</Link>', {
tags: {
Link: '<a href="{{attrs.href}}">{{children}}</a>',
},
});
console.log(template.render({ order: { id: "A-100" }, user: { name: "Lin" } }).value);
console.log(template.render({ order: { id: "A-101" }, user: { name: "Qin" } }).value);
// <a href="/orders/A-100">Lin</a>
// <a href="/orders/A-101">Qin</a>这类场景尤其适合 compile:
- 服务端重复渲染
- i18n 片段复用
- 启动时加载模板注册表
- 富文本模板按不同数据多次渲染
何时用 fill,何时用 compile
按调用模式选最简单的入口:
fill(objectOrArray, values):适合一次性的深度 payload 组装fill(string, values):适合简单的单次字符串渲染;重复渲染同类字符串时可通过cacheSize复用共享编译缓存compile(template).render(values):适合热路径、重复渲染、富文本标签模板,以及启动后长期复用的模板注册表
如果标签模板里用了函数,又希望 fill(string, ...) 命中缓存,建议提供稳定的 cacheKey。
富文本与标签模板
import { fill } from "placefill";
const result = fill(
'欢迎 <highlight>{{user.name}}</highlight>,查看 <link href="/orders/{{order.id}}">订单</link>',
{ user: { name: "Lin" }, order: { id: "A-100" } },
{
tags: {
highlight: "<strong>{{children}}</strong>",
link: '<a href="{{attrs.href}}">{{children}}</a>',
},
},
);
console.log(result.value);
// 欢迎 <strong>Lin</strong>,查看 <a href="/orders/A-100">订单</a>标签行为说明:
- 标签名大小写敏感
- 支持
MyComp、ui.slot、x:label、my-comp这类标签名 - 未注册标签会原样保留
- 标签模板里的
{{children}}和{{attrs.xxx}}固定使用双大括号 - 如果标签片段本身有错,异常片段会原样保留,同一字符串里其他合法占位符仍会继续处理
返回结果
fill 和 compile().render() 都返回结构化结果:
| 字段 | 说明 |
| -------- | ------------------------------------------------ |
| value | 渲染结果 |
| ok | 没有 error 时为 true |
| issues | 收集到的 warning 和 error |
| stats | 占位符数量、替换数量、告警、错误、耗时等统计信息 |
支持的占位符格式
默认仅识别双大括号:
fill("Hello {{name}}", { name: "Lin" });double_braces 保持向后兼容,仍支持 {{full name}}、{{user:name}}、{{用户}} 这类直取 key。
其他格式需要显式启用:
fill("Hello {name}", { name: "Lin" }, { format: "single_braces" });
fill("Hello ${name}", { name: "Lin" }, { format: "dollar_braces" });
fill("Hello %name%", { name: "Lin" }, { format: "percent" });对于 single_braces、dollar_braces 和 percent,占位符限制为 path-like key,例如 {user.name}、${order.id}、%campaign.slug%。
真实场景示例
仓库里的 examples 目录包含了一组更接近真实业务的案例:
order-confirmation-payload.ts: 订单确认通知 payloadshipment-status-rich-text.ts: 带标签和属性的物流状态富文本single-braces-notification.ts: 使用single_braces格式渲染的通知对象模板campaign-banner-i18n.ts: 编译一次并针对不同人群多次渲染营销横幅support-ticket-summary.ts: 展示 badge 文本和动作路由的客服工单摘要cms-article-fragment.ts: CMS 文章片段,覆盖属性值包含>product-card-schema.ts: 商品卡片对象模板,覆盖原始值类型保留invoice-reminder-email.ts: 带默认值回退的账单提醒邮件onboarding-checklist-message.ts: 带嵌套列表和链接的 onboarding 消息unknown-tag-passthrough.ts: 未知标签透明透传dynamic-signature-block.ts: 在模板中执行同步函数生成签名块
本地运行:
pnpm run examples
pnpm dlx tsx examples/campaign-banner-i18n.ts配置选项概览
| 选项 | 类型 | 默认值 | 说明 |
| ------------------------- | ------------------------------------- | --------------- | -------------------------------------- |
| format | PlaceholderFormat | double_braces | 占位符语法 |
| defaultValue | string \| number \| boolean \| null | 未设置 | 缺失值的替代值 |
| onError | collect \| throw | collect | 错误处理策略 |
| executeFunctions | boolean | false | 是否执行同步占位符函数 |
| tags | Record<string, TagTemplate> | {} | 标签模板注册表 |
| maxDepth | number | 100 | 对象 / 数组遍历最大深度 |
| maxTagDepth | number | 100 | 标签嵌套最大深度 |
| treatUndefinedAsMissing | boolean | false | 是否将 undefined 视为缺失 |
| cacheSize | number | 200 | fill(string, ...) 的共享编译缓存大小 |
| cacheKey | string | 未设置 | 函数标签场景下的显式缓存 key |
补充说明:
values必须是普通对象,否则会抛FillError- 纯占位符字符串如
{{count}}可以直接返回数字、布尔、数组、对象、Date或函数结果 - 占位符函数仅支持同步执行
兼容性
- 同时提供 ESM 和 CJS 两种产物
- 打包目标为
ES2020 - 内置类型声明
- 面向当前 Node.js 版本和 evergreen 浏览器 + 现代打包器
- 不提供 UMD 或全局变量形式的浏览器构建
对 Node.js 用户:
- 包导出同时支持
import和require prepublishOnly会执行check、test和pack
对浏览器用户:
- 通过 bundler 或原生模块链路消费 ESM 产物
- 运行时无依赖,发布产物默认面向现代运行时
- 如果要兼容更老的浏览器,需要由使用方自己的构建链路负责转译和 polyfill
基准测试
基准文件位于 tests/bench/fill.bench.ts。分组方式按用户调用模式,而不是按内部函数名;同时使用 vitest bench 的 warmup、多轮采样、轮转数据集,并直接复用仓库里的 examples 语义,避免只看理想化的单 token 微基准。
当前包含三层基准:
layer: micro:低噪声回归基准,覆盖短字符串、默认值回退、富文本标签、对象 payloadlayer: realistic one-shot:低复用场景,每次迭代都显式计入 compile 成本,覆盖单大括号通知、订单确认 payload、账单提醒邮件layer: realistic repeat:固定模板反复渲染场景,覆盖客服工单摘要、CMS 文章片段、营销横幅
本地运行:
pnpm run bench结果解读建议:
- 单次组比较的是
fill(...)和compile(...).render(...),其中compile成本会在每次迭代里计算进去 - 重复字符串组比较
fill(cache off)、fill(shared cache)和compile.render(precompiled) - 重复对象组只比较
fill(...)和compile.render(...),因为共享缓存只对字符串模板生效
开发
pnpm run check
pnpm test
pnpm build
pnpm run bench
pnpm run examples许可证
MIT
