Storybook for Ember:用 @storybook/ember 为 Ember 组件构建隔离开发、文档与测试环境
Storybook for Ember 是 Storybook 官方为 Ember 应用提供的框架预设(@storybook/ember),它把 UI 组件的开发环境从应用本体中剥离出来,让组件可以在没有路由、数据层和应用级依赖的情况下独立可视化、交互开发。读完本篇,你将掌握 Ember 预设的初始化方式、Ember 版 CSF3 story 的写法(hbs 模板 + args/context)、polyfill 注入机制的源码级原理,以及组件 API 文档(jsondoc)的提取链路。
一、它是什么:一个独立于应用运行的组件工作台
@storybook/ember 的官方定位写在 package.json 中:"Storybook for Ember: Develop, document, and test UI components in isolation"。README 对它的描述是:一个面向 Ember 组件的 UI 开发环境,让你可视化组件的各种状态并交互式地开发它们。
它的核心设计原则是 Storybook runs outside of your app——Storybook 运行在你的应用之外,因此开发组件时不必关心应用特定的依赖与运行要求(比如路由配置、后端接口、认证逻辑)。
当前仓库中该包的版本为 10.6.0-beta.1(见 package.json),peerDependencies 声明了明确的运行环境约束,这实际构成了使用前提:
ember-source:~3.28.1 || >=4.0.0babel-plugin-ember-modules-api-polyfill:^3.5.0babel-plugin-htmlbars-inline-precompile:^5.3.1- 依赖 React 16.8–19 用于渲染文档 UI
从 sandbox/ember-3-js 与 sandbox/ember-default-js 两个沙盒工程定义也可以看出,仓库的 CI 矩阵同时覆盖了 Ember 3.x 与默认(较新)版本的 Ember 应用,这与上面的 peer 范围吻合。
二、Getting Started:在 Ember 应用中初始化
README 给出的起步命令极其简单,在任意 Ember 应用目录下执行:
cd my-ember-app
npx storybook@latest init
init 命令会自动检测框架并安装 @storybook/ember,同时脚手架出 .storybook 配置与示例 story。示例 story 的模板就存放在本仓库中:template/cli/Button.stories.js。下面以这个模板为蓝本讲解 Ember story 的正确写法。
Ember 版 CSF3 story 的结构
Ember 预设的 story 与 React 版最大的差异在于:render 返回的不是 JSX,而是一个「模板 + 上下文」对象(或直接返回 hbs 编译产物)。模板的核心示例:
import { hbs } from 'ember-cli-htmlbars';
import { action } from 'storybook/actions';
import { fn } from 'storybook/test';
export default {
title: 'Example/Button',
render: (args) => ({
template: hbs`<button {{on "click" this.onClick}}>{{this.label}}</button>`,
context: args, // args 作为模板的渲染上下文
}),
argTypes: {
label: { control: 'text' },
},
tags: ['autodocs'], // 自动生成 Autodocs 页面
args: { onClick: fn() }, // fn() 支持交互测试(Play 函数断言)
};
export const Text = {
args: { label: 'Button' },
};
export const TextWithAction = {
render: () => ({
template: hbs`
<button {{on "click" this.onClick}}>
Trigger Action
</button>
`,
context: {
onClick: () => action('This was clicked')(),
},
}),
name: 'With an action',
};
这个模板里有几个关键实践点:
hbs标签来自ember-cli-htmlbars,它在 Babel 编译阶段被内联预编译为模板(见下文源码分析);render: (args) => ({ template, context: args })是 Ember 预设的标准形态:args(由 Controls 面板驱动)被作为模板渲染上下文注入,{{this.label}}等模板语法即可消费 args;- story 级
render覆盖:TextWithAction演示了如何为单个 story 重写模板与上下文,例如把点击回调替换成action(...)以便在 Actions 面板中观察调用; - 交互测试:
args: { onClick: fn() }使用storybook/test的fn工厂,配合@storybook/addon-vitest/测试运行时可对交互行为做断言; - story 间跳转:模板中还展示了
linkTo('example-button--docs')(来自@storybook/addon-links)在点击事件中跳转到其他 story 的用法。
渲染失败时的错误提示
如果 story 的 storyFn 没有返回 Ember 元素,渲染器会主动报错。从 client/preview/render.ts 的 renderToCanvas 可以看到错误信息给出的两种合法写法:
() => hbs('{{component}}'):直接返回hbs编译产物;() => { return { template: hbs\{{component}}`, context }`:返回模板对象。
这可以作为写 story 时的快速自检依据。
三、构建体系:webpack5 + Babel 的双层预设
@storybook/ember 是「框架预设」,它由三部分构成,对应 preset.js 对 dist/preset.js 的转发以及 src/preset.ts 的源码:
- 构建器绑定(
core预设属性):强制使用@storybook/builder-webpack5作为构建器(见 src/preset.ts#L43-L45)。也就是说 Ember 预设当前走的是 webpack 构建链路而非 Vite; webpackFinal定制:在基础配置之上追加一条 Babel 规则(见 src/preset.ts#L12-L41):- 匹配
.jsx/.tsx/.js/.ts等源文件(当typescript.skipCompiler为真时只匹配 JS/JSX); - 使用
babel-loader,cacheDirectory指向 Storybook 缓存目录(resolvePathInStorybookCache('babel'))以加速二次构建; - 通过
options.presets.apply('babel', {}, options)聚合所有预设(包括框架自带的 Babel 预设)产出的 Babel 配置; include限定为项目根目录,exclude排除node_modules与 Storybook 注入的虚拟模块;
- 匹配
- 框架 Babel 预设:
addons属性注册了 server/framework-preset-babel-ember.ts,这是理解 Ember 模板与 polyfill 处理的关键。
框架 Babel 预设:注入 htmlbars 内联预编译与模块 API polyfill
framework-preset-babel-ember.ts 向 Babel 配置追加了两个插件:
babel-plugin-htmlbars-inline-precompile:把hbs标签内的 HTMLBars 模板字符串在编译期预编译,并配置了对ember-cli-htmlbars、ember-cli-htmlbars-inline-precompile、htmlbars-inline-precompile三个模块导出的识别映射;babel-plugin-ember-modules-api-polyfill:为import { get } from '@ember/object'这类具名导入提供运行时兼容垫片,保证 ES module 写法与 Ember 3.x 全局对象 API 的互操作。
此外它还注册了 previewAnnotations,把编译产物 client/preview/config(dist 下)注入 preview 侧的入口列表,从而让渲染器配置在浏览器端生效。
四、Polyfill 机制详解(README 核心章节的源码印证)
README 中「Working with polyfills」一节是本包最具特色的配置能力:当 Ember 应用中已经使用了某些模板 polyfill(如 ember-named-blocks-polyfill),需要把这些 polyfill 同样接入 Storybook 的模板编译管线。README 给出的配置示例是:
// .storybook/main.js
import namedBlockPolyfill from 'ember-named-blocks-polyfill/lib/named-blocks-polyfill-plugin';
export default {
framework: {
name: '@storybook/ember',
options: {
polyfills: [namedBlockPolyfill],
}
},
// [...]
};
这段配置的实际生效链路可以从源码得到完整印证。在 framework-preset-babel-ember.ts#L10-L17 中:
function precompileWithPlugins(string: string, options: any) {
const precompileOptions: any = options;
if (emberOptions && emberOptions.polyfills) {
precompileOptions.plugins = { ast: emberOptions.polyfills };
}
return precompile(string, precompileOptions); // precompile 来自 ember-source 的模板编译器
}
可以推断出其工作原理:
- 框架 options(README 中的
framework.options,含polyfills数组)在预设解析阶段被归集为emberOptions(babel预设函数从options.presetsList中读取preset.emberOptions,见 同文件#L19-L29),并随即从 Babel 配置中删除该字段以避免污染 Babel 选项; precompileWithPlugins被作为自定义precompile函数传给babel-plugin-htmlbars-inline-precompile,于是每一个hbs模板在预编译时都会带上plugins: { ast: polyfills }——polyfill 以 AST 插件形式作用于模板 AST,这正是 ember-named-blocks-polyfill 这类模板 polyfill 的工作协议;- 最终效果:Storybook 里 story 模板的编译行为与你 Ember 应用自身的构建管线保持一致,named blocks 等语法在两侧行为统一。
注:README 示例中的框架名为
@storybook/ember;而当前源码的类型定义(src/types.ts#L13)将框架标识类型化为@storybook/ember-webpack5,说明框架命名在版本演进中发生了变化。配置时以你所安装版本的init脚手架产出为准。
五、组件 API 文档:基于 jsondoc 的 argTypes 提取
Ember 组件没有 React 那样的 PropTypes 或 TS 内省,Storybook 因此采用了一条基于预生成 JSON 文档的链路。client/preview/config.ts 中注册了两个文档提取器:
export const parameters = {
renderer: 'ember',
docs: {
story: { iframeHeight: '80px' },
extractArgTypes,
extractComponentDescription,
},
};
而 client/preview/jsondoc.ts 中的 extractArgTypes 实现逻辑是:
- 读取全局变量
__EMBER_GENERATED_DOC_JSON__(该变量在 types.ts 中声明,由 Ember 侧的文档生成管线——如 ember-doc-gen 类工具——在构建时注入); - 在 JSON 的
included列表中按attributes.name找到目标组件; - 把
attributes.arguments归约成 Storybook argTypes 结构,其中type.required通过tags中是否存在required标签判定,table.defaultValue与table.type分别填充默认值与类型摘要; extractComponentDescription则直接返回组件的attributes.description。
这意味着:只要你的 Ember 工程产出了组件 doc JSON,Autodocs 页面即可展示参数表与描述,无需手工维护 argTypes。
六、渲染管线:为每个 story 动态引导一个 Ember 实例
Ember 预设最精妙的部分在 client/preview/render.ts。preview 侧的入口由 framework-preset-babel-ember.ts#L54-L56 通过 previewAnnotations 注入,它做了三件事:
1. 引导 Ember 应用(不自动挂载)
function loadEmberApp() {
const config = global.require(`${global.STORYBOOK_NAME}/config/environment`);
return global.require(`${global.STORYBOOK_NAME}/app`).default.create({
autoboot: false,
rootElement: rootEl, // <div id="storybook-root">
...config.APP,
});
}
STORYBOOK_NAME 由 globals.ts 从 process.env.STORYBOOK_NAME 写入全局,指向你的应用入口模块。autoboot: false 让应用只创建不自动启动路由,rootElement 绑定到 Storybook 的画布容器。
2. 串行化的实例生命周期
render 函数(render.ts#L29-L69)用 lastPromise 串起所有渲染操作:切换 story 时先 destroy() 上一个 app instance,再 buildInstance() + boot() 新实例,从而避免并发引导导致的状态串扰;isRendering 标志保证同一时刻只有一个渲染流程在跑。
3. 以 component:story-mode 临时组件承载 story
每个 story 会被注册为一个名为 component:story-mode 的临时 Ember.Component:
instance.register(
'component:story-mode',
Ember.Component.extend({
layout: template || options, // storyFn 的返回值即模板
...context, // args 展开为组件属性
})
);
之后通过 instance.lookup('component:story-mode') 取出组件并 appendTo 到画布元素。这也解释了前文渲染错误提示中两种合法 story 返回值的含义:storyFn 的返回值会被直接当作 layout 使用。
七、版本、依赖与适用前提小结
结合本仓库当前状态(@storybook/ember@10.6.0-beta.1,beta 阶段),使用该预设需注意:
| 项目 | 说明 | 依据 |
|---|---|---|
| Ember 版本 | ember-source ~3.28.1 || >=4.0.0 |
package.json peerDependencies |
| 构建器 | 固定使用 @storybook/builder-webpack5(无 Vite 链路) |
src/preset.ts#L43-L45 |
| 必需 Babel 插件 | babel-plugin-htmlbars-inline-precompile、babel-plugin-ember-modules-api-polyfill |
package.json peerDependencies、framework-preset-babel-ember.ts#L33-L46 |
| 模板 polyfill | 通过 framework.options.polyfills 注入为模板 AST 插件 |
framework-preset-babel-ember.ts#L10-L17 |
| 组件文档 | 依赖 __EMBER_GENERATED_DOC_JSON__ 全局 doc JSON |
jsondoc.ts |
| CI 沙盒 | sandbox/ember-3-js、sandbox/ember-default-js |
sandbox/ember-3-js、sandbox/ember-default-js |
由于仓库当前处于 10.6.0-beta.1 的 pre-release 状态,生产使用建议以稳定发布版本为准,配置细节(尤其是 framework.name 的取值)以对应版本的脚手架输出为最终依据。
八、关键文件索引
| 关注点 | 路径 |
|---|---|
| 本框架说明文档 | code/frameworks/ember/README.md |
| 包元信息与 peer 依赖 | code/frameworks/ember/package.json |
| 预设入口(webpack 定制 + builder 绑定) | code/frameworks/ember/src/preset.ts |
| Babel 预设(htmlbars 预编译 + polyfill 注入) | code/frameworks/ember/src/server/framework-preset-babel-ember.ts |
| 渲染管线(Ember 应用引导与实例管理) | code/frameworks/ember/src/client/preview/render.ts |
| preview 参数与文档提取器注册 | code/frameworks/ember/src/client/preview/config.ts |
| jsondoc argTypes 提取 | code/frameworks/ember/src/client/preview/jsondoc.ts |
| 初始化脚手架示例 story | code/frameworks/ember/template/cli/Button.stories.js |
整体来看,@storybook/ember 的设计可以概括为三层:构建层用 webpack5 + 定制 Babel 规则复现 Ember 应用的模板编译管线(含 polyfill 注入);渲染层以「autoboot 关闭的完整 Ember 应用 + 每 story 独立实例」保证组件在真实 Ember 运行时中运行;文档层通过外部 doc JSON 弥补 Ember 缺少组件内省能力的短板。三层叠加,使得 Ember 开发者可以在完全脱离应用上下文的环境中开发、文档化和测试 UI 组件。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00