跳转到内容

CEM 分析器插件

CEM(custom-elements.json)是一份标准化的描述文件,用来说明一个包所定义的元素 (它们的标签、属性、成员、事件和插槽),使得工具无需执行代码就能读取你的组件信息。 更多内容参见 custom-elements-manifest.open-wc.org 了解该分析器及其插件 API,或参见该文件格式本身的模式与规范

CEM 分析器(@custom-elements-manifest/analyzer)默认会将 static props 读作 一个单独的静态字段,不会为其生成任何属性。这导致 Storybook 和代码编辑器自动补全 这类工具无法读到任何有用信息。

使用我们的 web-component-base/cem-plugin,CEM 分析器会将每个 prop 展开为一个 带类型的清单属性,以及一个与之匹配的公共字段。以下步骤将指导你安装 CEM 分析器 并进行配置,使工具能够正常处理你的 wcb 自定义元素。

Terminal window
npm i -D @custom-elements-manifest/analyzer
custom-elements-manifest.config.mjs
import { wcbStaticProps } from 'web-component-base/cem-plugin'
export default {
globs: ['src/**/*.{js,ts}'],
outdir: '.',
plugins: [wcbStaticProps()],
}

然后运行分析器:

Terminal window
npx cem analyze

给定一个组件:

src/cozy-button.ts
import { WebComponent, html } from 'web-component-base'
type CozyButtonProps = {
variant: 'primary' | 'ghost'
disabled: boolean
maxCount: number
}
export class CozyButton extends WebComponent<CozyButtonProps> {
static props: CozyButtonProps = {
variant: 'primary',
disabled: false,
maxCount: 3,
}
static shadowRootInit = { mode: 'open' }
static styles = ':host { display: inline-block }'
get template() {
return html`<button>${this.props.variant}</button>`
}
}
customElements.define('cozy-button', CozyButton)

custom-elements.json 中会为每个 prop 新增一个带类型的属性以及一个匹配的公共字段:

属性类型字段默认值
variantstringvariant'primary'
disabledbooleandisabledfalse
max-countnumbermaxCount3

……并且 propsshadowRootInitstylesstrictPropsobservedAttributestemplate 会从公共接口中被剔除。

有两个细节值得了解:

  • 类型来自默认值的字面量true/falseboolean,数字 → number, 对象/数组 → object,其余一律 → string。TypeScript 的类型标注不会被参考, 因此 variant 的联合类型在清单中仍然会体现为 string
  • 属性名来自 wcb 自身的 getKebabCase,与 observedAttributes 所使用的 是同一个函数,因此清单中的名称不会与组件实际观察的内容产生偏差。

Storybook 的 web-components 渲染器会根据 Custom Elements Manifest 构建 autodocs 和控件(controls)

.storybook/preview.js
import { setCustomElementsManifest } from '@storybook/web-components-vite'
import manifest from '../custom-elements.json'
setCustomElementsManifest(manifest)
export default { tags: ['autodocs'] }

将一个 story 绑定到标签名,Storybook 就会推断出其余部分,为 variant 生成 文本输入框,为 disabled 生成开关,为 maxCount 生成数字输入框:

cozy-button.stories.js
import { html } from 'lit'
import '../src/cozy-button.ts'
export default {
title: 'Cozy/Button',
component: 'cozy-button', // ← 无需 argTypes
render: ({ variant, disabled, maxCount }) => html`
<cozy-button
variant=${variant}
?disabled=${disabled}
max-count=${maxCount}
></cozy-button>
`,
}
export const Default = {
args: { variant: 'primary', disabled: false, maxCount: 3 },
}

完整可运行的配置示例,参见 wcb 仓库中的 storybook/,它针对 demo 组件运行了这套配置。

一旦 custom-elements.json 存在,编辑器就可以为你的组件提供标签名和属性自动补全, 这些补全依据的正是插件所读取的同一份 static props,因此提示内容不会与代码产生偏差。

首先,在你的 package.json 中声明该清单文件。路线 2 中的语言服务器 会通过这个字段发现它,这也是其他依赖清单的工具会去查找的生态系统约定:

{
"customElements": "custom-elements.json"
}

VS Code 内置的 HTML 语言服务读取的是它自己的 custom data 格式。第二个分析器插件可以将清单转换为该格式,因此这两个文件可以由同一次 cem analyze 运行生成:

Terminal window
npm i -D cem-plugin-vs-code-custom-data-generator
custom-elements-manifest.config.mjs
import { wcbStaticProps } from 'web-component-base/cem-plugin'
import { generateCustomData } from 'cem-plugin-vs-code-custom-data-generator'
export default {
globs: ['src/**/*.{js,ts}'],
outdir: '.',
plugins: [wcbStaticProps(), generateCustomData()],
}
.vscode/settings.json
{
"html.customData": ["./vscode.html-custom-data.json"]
}

重启 VS Code 后,在 HTML 文件中输入 <cozy- 即可自动补全,variant / disabled / max-count 会作为属性被提供,并且在悬停时显示其类型和默认值。

注意事项: html.customData 只适用于 .html 文件。wcb 组件是在 .js / .ts 内部的 html 标签模板中编写标记的,这些不会经过 HTML 语言服务。这条路径 适用于那些直接编写纯 HTML 页面来使用你的组件的人,而不会在你自己的模板内生效。

路线 2:语言服务器扩展,适用于标签模板

Section titled “路线 2:语言服务器扩展,适用于标签模板”

要在标签模板内部获得同样的补全,你需要一个能理解标签模板的扩展。目前最新的选择是 Custom Elements Manifest Language Serverpwrs.cem-language-server-vscode)。它可以在 JS 和 TS 的模板字面量内为标签名和属性 提供自动补全,为属性和默认值添加悬停文档,并通过上面提到的 customElements 字段 发现清单文件,无需任何 .vscode/settings.json 配置。

还有两个替代方案,同样值得了解它们的现状:

  • wc-toolkit/wc-language-server:支持 VS Code 和 JetBrains,同样以清单文件为驱动。官方将其自称为 alpha 和实验性
  • Matsuuu.custom-elements-language-server-project:在较旧的文章中最常见到的一个。 它处于 alpha 阶段,且其仓库已于 2026 年 1 月被归档,因此建议优先选择上面两者之一。

要将你的组件发布到 npm?分析器会为每个模块的 path 打上它所扫描到的确切文件路径—— 因此如果 globs: ['src/**/*.ts'],那么 custom-elements.json 中的每个 path 都会是 一个 .ts 源文件。而大多数包只发布它们构建后的 dist/ 产物(而且运行时本来也无法 import 一个 .ts 文件),因此读取该清单的消费者最终解析出的路径可能根本不在 发布的压缩包中。

wcbStaticProps() 之后添加 distPaths(),即可将这些路径重写为构建后的产物路径。 零配置情况下,它会将 src/ 映射为 dist/,并将 TypeScript 扩展名映射为其编译后的 JS 形式(.ts.js.mts.mjs.cts.cjs)。其他扩展名会原样通过, 因此 .js 源文件只会替换其目录部分:

custom-elements-manifest.config.mjs
import { wcbStaticProps, distPaths } from 'web-component-base/cem-plugin'
export default {
globs: ['src/**/*.ts'],
outdir: '.',
plugins: [wcbStaticProps(), distPaths()],
}

如果目录结构不同,可以重写 rootDir / outDirdistPaths({ rootDir: 'lib', outDir: 'dist/esm' })。 请在构建之后运行 cem analyze,以确保重写路径所指向的 dist/ 文件确实存在。 关于 ext 扩展名重映射,参见 API 参考。如果不需要 发布清单文件——例如只是在本地针对自己的源码使用 Storybook 或编辑器——则完全不需要这些配置。