bootstrap-component-js
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBootstrap 6 component JavaScript
Bootstrap 6 组件 JavaScript 开发
Every Bootstrap plugin is a TypeScript module in that extends , which handles instance registration, config merging, and teardown. Follow the shared structure and a component gets , , config, namespaced events, and without writing any of it.
js/src/BaseComponentgetInstance()getOrCreateInstance()data-bs-*dispose()The sources are TypeScript, but the runtime patterns are the same ones v5 used with types layered on top. Bootstrap compiles them to JavaScript and emits its own declarations, so consumers need no .
@types/bootstrapFor the component's styles, see the skill.
bootstrap-component每个 Bootstrap 插件都是 目录下的 TypeScript 模块,继承自 ,后者负责实例注册、配置合并与销毁逻辑。遵循这套共享结构,组件无需额外编写代码即可获得 、、 配置、命名空间事件以及 等能力。
js/src/BaseComponentgetInstance()getOrCreateInstance()data-bs-*dispose()源码使用 TypeScript 编写,但运行时模式与 v5 保持一致,只是在其之上添加了类型层。Bootstrap 会将其编译为 JavaScript 并生成自带的类型声明文件,因此使用者无需安装 。
@types/bootstrap关于组件样式,请参考 技能。
bootstrap-componentWorkflow
工作流程
- Step 1: Scaffold the module
- Step 2: Define config options
- Step 3: Implement behavior and events
- Step 4: Wire the data API
- Step 5: Position with Floating UI (optional)
- Step 6: Wrap a third-party library (optional)
- Step 7: Clean up in dispose()
- Step 8: Register and test
- Step 9: Verify
- 步骤 1:搭建模块骨架
- 步骤 2:定义配置选项
- 步骤 3:实现交互行为与事件
- 步骤 4:连接 data API
- 步骤 5:使用 Floating UI 实现定位(可选)
- 步骤 6:封装第三方库(可选)
- 步骤 7:在 dispose() 中清理资源
- 步骤 8:注册与测试
- 步骤 9:验证
Step 1: Scaffold the module
步骤 1:搭建模块骨架
Create . Read first — it's the smallest complete example.
js/src/<name>.tsjs/src/alert.tsts
/**
* --------------------------------------------------------------------------
* Bootstrap widget.ts
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
/**
* Constants
*/
const NAME = 'widget'
const DATA_KEY = 'bs.widget'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_SHOW = 'show'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="widget"]'
/**
* Class definition
*/
class Widget extends BaseComponent {
// Getters
static override get NAME(): string {
return NAME
}
}
export default Widget- is the only required static.
NAMEderivesBaseComponent(DATA_KEY) andbs.<name>(EVENT_KEY) from it, stores the instance on the element with.bs.<name>, and disposes any previous instance bound to the same element.Data.set() - Keep the section comments (,
Constants,Class definition) and member grouping (Data API implementation,constructor,// Getters,// Public,// Private) — every module in// Staticlooks the same.js/src/ - Repo style: ES modules only, no semicolons, 2-space indent, single quotes, and on every member that overrides
overrideorBaseComponent.Config - Relative imports keep the ESM extension even though the file on disk is
.js, because.tssetstsconfig.json. Rolldown, Vite, and Vitest all map the specifier back to themoduleResolution: nodenextsource..ts - The compiler is strict in ways worth knowing before you write: wants type imports marked (
verbatimModuleSyntax), andimport EventHandler, { type BootstrapEvent } from './dom/event-handler.js'rules out enums, namespaces, and constructor parameter properties.erasableSyntaxOnly - Declare instance state as and assign it in the constructor, the way
protected declare _foo: Typedoes.toast.tsis type-only; a real field without an initializer would emitdeclareunder_foo = undefinedand wipe whatever the base constructor set.useDefineForClassFields - v6 has no and no
defineJQueryPlugin. Helpers live injs/src/util.ts; DOM wrappers injs/src/util/(js/src/dom/,event-handler,selector-engine,manipulator).data
创建 文件。建议先阅读 —— 这是最小的完整示例。
js/src/<name>.tsjs/src/alert.tsts
/**
* --------------------------------------------------------------------------
* Bootstrap widget.ts
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
/**
* Constants
*/
const NAME = 'widget'
const DATA_KEY = 'bs.widget'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_SHOW = 'show'
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="widget"]'
/**
* Class definition
*/
class Widget extends BaseComponent {
// Getters
static override get NAME(): string {
return NAME
}
}
export default Widget- 是唯一必需的静态属性。
NAME会基于它派生BaseComponent(DATA_KEY)和bs.<name>(EVENT_KEY),通过.bs.<name>将实例存储在元素上,并销毁绑定在同一元素上的旧实例。Data.set() - 保留分区注释(、
Constants、Class definition)和成员分组(Data API implementation、constructor、// Getters、// Public、// Private)——// Static下的所有模块都遵循统一格式。js/src/ - 仓库代码规范:仅使用 ES 模块、不写分号、2 空格缩进、单引号,所有重写 或
BaseComponent的成员都要添加Config关键字。override - 相对导入需要保留 ESM 的 后缀,尽管磁盘上的实际文件是
.js,这是因为.ts中设置了tsconfig.json。Rolldown、Vite 和 Vitest 都会将导入路径映射回moduleResolution: nodenext源文件。.ts - 编译器的严格规则值得提前了解:要求类型导入必须显式标记(例如
verbatimModuleSyntax),而import EventHandler, { type BootstrapEvent } from './dom/event-handler.js'禁止使用枚举、命名空间和构造函数参数属性。erasableSyntaxOnly - 实例状态需要用 声明,并在构造函数中赋值,参考
protected declare _foo: Type的写法。toast.ts仅用于类型标注;如果是没有初始值的真实字段,在declare规则下会被编译为useDefineForClassFields,从而覆盖基类构造函数设置的值。_foo = undefined - v6 版本不再提供 ,也没有
defineJQueryPlugin。工具函数存放在js/src/util.ts目录下;DOM 包装器存放在js/src/util/目录(包括js/src/dom/、event-handler、selector-engine、manipulator)。data
Step 2: Define config options
步骤 2:定义配置选项
Declare the config shape, the defaults, and the runtime types as module constants, then expose them as statics:
ts
type WidgetConfig = {
animation: boolean
delay: number
}
const Default: WidgetConfig = {
animation: true,
delay: 5000
}
const DefaultType = {
animation: 'boolean',
delay: 'number'
}
class Widget extends BaseComponent {
protected declare _config: WidgetConfig
protected declare _timeout: number | null
constructor(element?: string | Element | null, config?: Partial<WidgetConfig> | null) {
super(element, config)
this._timeout = null
}
// Getters
static override get Default(): WidgetConfig {
return Default
}
static override get DefaultType(): Record<string, string> {
return DefaultType
}
static override get NAME(): string {
return NAME
}
}
export default Widget
export type { WidgetConfig }- The config type does double duty: is typed with it, the constructor accepts a
Defaultof it since callers pass a subset, and redeclaringPartial<>narrowsprotected declare _config: WidgetConfig's loose config soBaseComponentis athis._config.delay.numberstays a plain string map — it drives the runtime check, not the compiler. Export the type; the type tests import it.DefaultType - Merge order, applied by (last wins):
Config._mergeConfigObj()→ the JSON inDefault→ individualdata-bs-configattributes → the object passed to the constructor. You don't write any of this.data-bs-* - names are camelCased when read, so
data-bs-*becomesdata-bs-auto-hide="false".autoHide: false - Every key in is validated and throws a
DefaultTypeon mismatch. Values are matched as regex, so unions work:TypeError,'boolean','(number|string)'.'(string|element|function)' - Normalize a value before validation by overriding — the usual place to resolve a selector string into an element or coerce a shorthand.
_configAfterMerge(config)
将配置结构、默认值和运行时类型声明为模块常量,然后作为静态属性暴露出去:
ts
type WidgetConfig = {
animation: boolean
delay: number
}
const Default: WidgetConfig = {
animation: true,
delay: 5000
}
const DefaultType = {
animation: 'boolean',
delay: 'number'
}
class Widget extends BaseComponent {
protected declare _config: WidgetConfig
protected declare _timeout: number | null
constructor(element?: string | Element | null, config?: Partial<WidgetConfig> | null) {
super(element, config)
this._timeout = null
}
// Getters
static override get Default(): WidgetConfig {
return Default
}
static override get DefaultType(): Record<string, string> {
return DefaultType
}
static override get NAME(): string {
return NAME
}
}
export default Widget
export type { WidgetConfig }- 配置类型有双重作用:会使用它进行类型标注,构造函数接受它的
Default类型(因为调用者只会传入部分配置),而重新声明Partial<>可以收窄protected declare _config: WidgetConfig中宽松的配置类型,让BaseComponent的类型为this._config.delay。number仍然是普通的字符串映射——它用于运行时检查,而非编译时。记得导出配置类型,类型测试会导入它。DefaultType - 配置合并顺序由 实现(后者优先):
Config._mergeConfigObj()→Default中的 JSON → 各个data-bs-config属性 → 传入构造函数的对象。这些逻辑都不需要你编写。data-bs-* - 属性在读取时会自动转为驼峰命名,因此
data-bs-*会变成data-bs-auto-hide="false"。autoHide: false - 中的每个键都会被校验,不匹配时会抛出
DefaultType。值通过正则匹配,因此支持联合类型:TypeError、'boolean'、'(number|string)'。'(string|element|function)' - 可以通过重写 在校验前对配置值进行标准化——通常用于将选择器字符串解析为元素,或是处理简写形式。
_configAfterMerge(config)
Step 3: Implement behavior and events
步骤 3:实现交互行为与事件
ts
class Widget extends BaseComponent {
// Public
show(): void {
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW)
if (showEvent.defaultPrevented) {
return
}
this._element.classList.add(CLASS_NAME_SHOW)
this._queueCallback(() => {
EventHandler.trigger(this._element, EVENT_SHOWN)
}, this._element, this._config.animation)
}
}- Events come in pairs: an infinitive fired before the action (, cancelable) and a past participle after it (
show.bs.widget). Always bail when the "before" event isshown.bs.widget.defaultPrevented - runs the callback after the CSS transition on
_queueCallback(callback, element, isAnimated)finishes, and skips it if the instance was disposed mid-transition. Pass the animation flag from config so non-animated components fire immediately.element - Add a payload when listeners need context: .
EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })returns atrigger(), anBootstrapEventwidened with the payload keys, so reading them back in a handler needs no cast.Event - Query with (
SelectorEngine,findOne,find,next,prev) instead of rawgetElementFromSelector, and read or write attributes withquerySelector.Manipulator - Bind internal listeners with using the namespaced constants, so
EventHandler.on(this._element, EVENT_MOUSEOVER, …)removes them in one call.dispose()
ts
class Widget extends BaseComponent {
// Public
show(): void {
const showEvent = EventHandler.trigger(this._element, EVENT_SHOW)
if (showEvent.defaultPrevented) {
return
}
this._element.classList.add(CLASS_NAME_SHOW)
this._queueCallback(() => {
EventHandler.trigger(this._element, EVENT_SHOWN)
}, this._element, this._config.animation)
}
}- 事件总是成对出现:动作触发前是动词原形的事件(,可取消),动作完成后是过去分词的事件(
show.bs.widget)。当“before”事件的shown.bs.widget为 true 时,必须终止后续逻辑。defaultPrevented - 会在
_queueCallback(callback, element, isAnimated)的 CSS 过渡结束后执行回调,如果实例在过渡过程中被销毁则会跳过执行。需要传入配置中的动画开关,这样无动画的组件会立即触发回调。element - 当监听器需要上下文信息时,可以添加 payload:。
EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget })返回trigger()类型,是扩展了 payload 键的BootstrapEvent对象,因此在事件处理函数中读取这些属性无需类型转换。Event - 使用 (
SelectorEngine、findOne、find、next、prev)进行元素查询,不要直接用getElementFromSelector;属性读写则使用querySelector。Manipulator - 内部监听器使用 绑定,并使用带命名空间的常量,这样
EventHandler.on(this._element, EVENT_MOUSEOVER, …)可以一次性移除所有监听器。dispose()
Step 4: Wire the data API
步骤 4:连接 data API
The data API goes at the bottom of the module, after the class, as an import side effect:
ts
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
event.preventDefault()
Widget.getOrCreateInstance(this).toggle()
})- Delegate from and namespace the event with
documentso it stays distinguishable from user listeners..data-api - Keep rather than an arrow when you need the delegated element:
functiontypes the callback'sEventHandler, sothisis the match forthis. Use an arrow and reach forSELECTOR_DATA_TOGGLEonly when you need the actual click target.event.target - Prefer the shared helpers in over hand-rolled handlers:
js/src/util/component-functions.ts- wires
enableDismissTrigger(Widget)to[data-bs-dismiss="widget"]. Pass a method name for anything else:hide().enableDismissTrigger(Alert, 'close') - handles the "trigger points at one or more targets via
eventActionOnPlugin(Widget, 'click', SELECTOR_DATA_TOGGLE, 'toggle')" case and skips disabled elements.data-bs-target
- Because these are side effects, the data API only exists once the module is imported. Anything importing a single component file gets that component's data API and nothing else.
data API 作为导入副作用放在模块底部、类定义之后:
ts
/**
* Data API implementation
*/
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
event.preventDefault()
Widget.getOrCreateInstance(this).toggle()
})- 从 进行事件委托,并给事件加上
document命名空间,以便与用户的监听器区分开。.data-api - 当你需要获取被委托的元素时,使用普通 而非箭头函数:
function已经为回调的EventHandler做了类型标注,因此this就是匹配this的元素。只有当你需要实际的点击目标时,才使用箭头函数并通过SELECTOR_DATA_TOGGLE获取。event.target - 优先使用 中的共享工具函数,而非手动编写处理函数:
js/src/util/component-functions.ts- 会将
enableDismissTrigger(Widget)绑定到[data-bs-dismiss="widget"]方法。如果是其他方法,传入方法名即可:hide()。enableDismissTrigger(Alert, 'close') - 用于处理“触发器通过
eventActionOnPlugin(Widget, 'click', SELECTOR_DATA_TOGGLE, 'toggle')指向一个或多个目标”的场景,并且会自动跳过禁用状态的元素。data-bs-target
- 由于这些是导入副作用,因此只有导入该模块后,对应的 data API 才会生效。只导入单个组件文件时,只会获得该组件的 data API,不会引入其他内容。
Step 5: Position with Floating UI (optional)
步骤 5:使用 Floating UI 实现定位(可选)
Menus, tooltips, and popovers position with , an optional peer dependency. Guard for it in the constructor before calling — legal here because the class declares its fields instead of initializing them:
@floating-ui/domsuper()ts
import { autoUpdate, computePosition } from '@floating-ui/dom'
class Widget extends BaseComponent {
protected declare _floatingCleanup: (() => void) | null
constructor(element?: string | Element | null, config?: Partial<WidgetConfig> | null) {
if (typeof computePosition === 'undefined') {
throw new TypeError('Bootstrap\'s widgets require Floating UI (https://floating-ui.com)')
}
super(element, config)
this._floatingCleanup = null
}
}Create the positioner when the element is shown and keep the cleanup function so it can be cancelled:
ts
class Widget extends BaseComponent {
// Private
protected _createFloating(): void {
this._updateFloatingPosition()
this._floatingCleanup = autoUpdate(this._element, this._floatingEl, () => this._updateFloatingPosition())
}
protected _disposeFloating(): void {
if (this._floatingCleanup) {
this._floatingCleanup()
this._floatingCleanup = null
}
}
}js/src/util/floating-ui.tsgetDefaultPlacement()-start-endparseResponsivePlacement()getResponsivePlacement()data-bs-placement="bottom-start md:top-end"createBreakpointListeners()disposeBreakpointListeners()菜单、tooltip 和 popover 等组件使用 实现定位,这是一个可选的 peer 依赖。在构造函数中调用 之前先做存在性检查——在这里是合法的,因为类只是声明了字段,没有初始化:
@floating-ui/domsuper()ts
import { autoUpdate, computePosition } from '@floating-ui/dom'
class Widget extends BaseComponent {
protected declare _floatingCleanup: (() => void) | null
constructor(element?: string | Element | null, config?: Partial<WidgetConfig> | null) {
if (typeof computePosition === 'undefined') {
throw new TypeError('Bootstrap\'s widgets require Floating UI (https://floating-ui.com)')
}
super(element, config)
this._floatingCleanup = null
}
}当元素显示时创建定位器,并保存清理函数以便后续取消:
ts
class Widget extends BaseComponent {
// Private
protected _createFloating(): void {
this._updateFloatingPosition()
this._floatingCleanup = autoUpdate(this._element, this._floatingEl, () => this._updateFloatingPosition())
}
protected _disposeFloating(): void {
if (this._floatingCleanup) {
this._floatingCleanup()
this._floatingCleanup = null
}
}
}js/src/util/floating-ui.tsgetDefaultPlacement()-start-endparseResponsivePlacement()getResponsivePlacement()data-bs-placement="bottom-start md:top-end"createBreakpointListeners()disposeBreakpointListeners()Step 6: Wrap a third-party library (optional)
步骤 6:封装第三方库(可选)
js/src/datepicker.tsvanilla-calendar-pro- Keep Bootstrap's config surface. Users set options; a private builder (
data-bs-*) translates them into the library's option shape._buildCalendarOptions() - Instantiate in a private initializer called from the constructor and hold the instance on a private field typed with the library's own type ().
protected declare _calendar: Calendar | null - Re-emit the library's callbacks as namespaced Bootstrap events so consumers only learn one API.
- Call the library's teardown in before
dispose(), and null the field.super.dispose() - Add the package to in
peerDependencies, guard for its absence, and document the extra install step on the component's docs page.package.json
Don't fork or vendor the library, and don't expose its instance as public API — anything users need should be a method or event on the Bootstrap class.
js/src/datepicker.tsvanilla-calendar-pro- 保持 Bootstrap 的配置接口不变。用户通过 设置选项,由私有构建函数(
data-bs-*)将其转换为第三方库的配置格式。_buildCalendarOptions() - 在构造函数调用的私有初始化方法中实例化第三方库,并将实例保存在私有字段中,字段类型使用库自身的类型()。
protected declare _calendar: Calendar | null - 将库的回调重新包装为带 Bootstrap 命名空间的事件,这样使用者只需要学习一套 API。
- 在 中先调用库的销毁方法,再执行
dispose(),并将字段置为 null。super.dispose() - 将该包添加到 的
package.json中,做缺失检查,并在组件的文档页面说明额外的安装步骤。peerDependencies
不要 fork 或把库的代码放到自己的仓库中,也不要将库的实例作为公共 API 暴露——用户需要的所有功能都应该以 Bootstrap 类的方法或事件形式提供。
Step 7: Clean up in dispose()
步骤 7:在 dispose() 中清理资源
ts
class Widget extends BaseComponent {
// Public
override dispose(): void {
clearTimeout(this._timeout!)
this._observer?.disconnect()
this._disposeFloating()
super.dispose()
}
}super.dispose()Datathis._elementEVENT_KEYMutationObserverResizeObserverdocumentwindowts
class Widget extends BaseComponent {
// Public
override dispose(): void {
clearTimeout(this._timeout!)
this._observer?.disconnect()
this._disposeFloating()
super.dispose()
}
}super.dispose()Datathis._elementEVENT_KEYMutationObserverResizeObserverdocumentwindowStep 8: Register and test
步骤 8:注册与测试
-
Add the export to:
js/src/index.tstsexport { default as Widget } from './widget.js'That barrel is the package entry point and feeds bothand the bundle. Nothing else needs updating: Rolldown builds fromdist/js/bootstrap.js,js/src/index.tsglobsbuild/build-plugins.mjsfor the per-plugin files injs/src/**/*.ts, and Vitest picks upjs/dist/.js/tests/unit/**/*.spec.js -
Extend both type tests.asserts the public surface against the sources;
js/tests/types/api.tsasserts the same surface against the shippedjs/tests/types/consumer.ts, imported through the package name the way a downstream project does. Add the class, its config type, and its methods to both — a declaration-emit bug passes the first and fails the second.js/dist/*.d.ts -
Add a spec at. Specs are plain JavaScript and run in a real Chromium through Vitest browser mode:
js/tests/unit/<name>.spec.jsjsimport Widget from '../../src/widget.js' import { clearFixture, getFixture } from '../helpers/fixture.js' describe('Widget', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) it('should expose the plugin name and data key', () => { expect(Widget.NAME).toEqual('widget') expect(Widget.DATA_KEY).toEqual('bs.widget') }) })- ,
describe,it, and the hooks are globals (expectinglobals: true), so don't import them. Importjs/tests/vitest.config.mtsfromviwhen you need spies or fake timers — that's the preferred API for new specs.vitest - maps the Jasmine API the older specs are written against onto Vitest, so
js/tests/vitest-setup.js,spyOn,jasmine.clock(),toHaveClass,toBeTrue, andtoBeFalsestill work. Two gotchas: the shimmedtoHaveSizestubs the method (addspyOnfor the real one), and Vitest 4 dropped the.and.callThrough()callback, so an asynchronous test returns a promise and resolves it from the listener.done - Cover the constructor with both a selector and an element, config coming from , each public method, both events including the cancelable one, the data API without manual instantiation, and
data-bs-*. Coverage counts every file indispose(), so a component with no spec drags the run below the thresholds injs/src(90% of statements, functions, and lines; 88% of branches) and fails the build.js/tests/vitest.config.mts
-
Document it on the component's docs page: add(or
js: optional) to the frontmatter,js: requiredin an<JsDataAttributes />section, and### Optionstables for options, methods, and events. Demo snippets live in<BsTable>and render withsite/src/assets/partials/snippets.js.<JsDocs name="live-widget" file="site/src/assets/partials/snippets.js" />
-
在中添加导出:
js/src/index.tstsexport { default as Widget } from './widget.js'这个 barrel 文件是包的入口点,同时为和打包产物提供内容。不需要修改其他文件:Rolldown 从dist/js/bootstrap.js构建,js/src/index.ts会通过 glob 匹配build/build-plugins.mjs来生成js/src/**/*.ts下的单插件文件,Vitest 则会自动加载js/dist/的测试用例。js/tests/unit/**/*.spec.js -
补充两类类型测试。基于源码校验公共接口;
js/tests/types/api.ts则通过包名导入发布的js/tests/types/consumer.ts文件,模拟下游项目的使用方式来校验同一套接口。需要把类、它的配置类型和方法都添加到这两个测试文件中——如果声明文件生成有 bug,第一个测试会通过,但第二个会失败。js/dist/*.d.ts -
在添加测试用例。测试用例使用普通 JavaScript 编写,通过 Vitest 浏览器模式在真实的 Chromium 中运行:
js/tests/unit/<name>.spec.jsjsimport Widget from '../../src/widget.js' import { clearFixture, getFixture } from '../helpers/fixture.js' describe('Widget', () => { let fixtureEl beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) it('should expose the plugin name and data key', () => { expect(Widget.NAME).toEqual('widget') expect(Widget.DATA_KEY).toEqual('bs.widget') }) })- 、
describe、it和各个钩子都是全局变量(expect中设置了js/tests/vitest.config.mts),因此不需要导入。需要间谍函数或假定时器时,从globals: true导入vitest——这是新测试用例推荐使用的 API。vi - 将旧测试用例使用的 Jasmine API 映射到 Vitest 上,因此
js/tests/vitest-setup.js、spyOn、jasmine.clock()、toHaveClass、toBeTrue和toBeFalse仍然可用。有两个注意点:垫片提供的toHaveSize会默认存根方法(如果要调用真实方法,需要添加spyOn);Vitest 4 移除了.and.callThrough()回调,因此异步测试需要返回 Promise,并在监听器中 resolve 它。done - 测试覆盖需要包括:构造函数同时支持选择器和元素传入、配置、每个公共方法、两类事件(包括可取消的那个)、无需手动实例化的 data API,以及
data-bs-*。覆盖率会统计dispose()下的所有文件,因此没有测试用例的组件会导致覆盖率低于js/src中的阈值(语句、函数、行覆盖率 90%;分支覆盖率 88%),从而导致构建失败。js/tests/vitest.config.mts
-
在组件的文档页面添加说明:在 frontmatter 中添加(或
js: optional),在js: required章节添加### Options,并用<JsDataAttributes />表格展示选项、方法和事件。演示代码片段存放在<BsTable>中,通过site/src/assets/partials/snippets.js渲染。<JsDocs name="live-widget" file="site/src/assets/partials/snippets.js" />
Step 9: Verify
步骤 9:验证
npm run js-lint- —
npm run js-typecheckovertsc --noEmitand the type tests.js/src - , or
npm run js-test-unitto add the two Rolldown integration bundles that prove the module is tree-shakeable. Playwright needs its browser once per machine (npm run js-test), andnpx playwright install chromiumruns the specs in a visible browser.npm run js-debug - — Rolldown compiles the sources to
npm run jsanddist/js, terser minifies, andjs/distemits thetscfiles. Check the bundle grew by roughly what you'd expect;.d.tsenforces size budgets.npm run bundlewatch - and exercise the component on its docs page, with the keyboard as well as the mouse.
npm start
npm run js-lint- — 对
npm run js-typecheck和类型测试执行js/src。tsc --noEmit - ,或者运行
npm run js-test-unit额外执行两个 Rolldown 集成打包测试,验证模块支持 tree-shaking。每台机器需要安装一次 Playwright 浏览器(npm run js-test),运行npx playwright install chromium可以在可见的浏览器中运行测试用例。npm run js-debug - — Rolldown 将源码编译到
npm run js和dist/js目录,terser 进行压缩,js/dist生成tsc文件。检查包体积的增长是否符合预期;.d.ts会强制执行体积限制。npm run bundlewatch - 运行 ,在组件的文档页面测试功能,同时验证键盘和鼠标操作。
npm start
In your own project
在你自己的项目中使用
package.jsonbootstrap/js/src/*.jsjs/dist/*.jsts
import BaseComponent from 'bootstrap/js/src/base-component.js'
import EventHandler from 'bootstrap/js/src/dom/event-handler.js'
class Widget extends BaseComponent {
static override get NAME(): string {
return 'widget'
}
}Two things to keep in mind. Importing a Bootstrap component module runs its data API as a side effect, which is what you want in an app entry point but not inside a library. And is derived as , so pick a that doesn't collide with a Bootstrap plugin or your instance will replace theirs on shared elements.
DATA_KEYbs.<name>NAMEpackage.jsonbootstrap/js/src/*.jsjs/dist/*.jsts
import BaseComponent from 'bootstrap/js/src/base-component.js'
import EventHandler from 'bootstrap/js/src/dom/event-handler.js'
class Widget extends BaseComponent {
static override get NAME(): string {
return 'widget'
}
}有两点需要注意。导入 Bootstrap 组件模块时会作为副作用执行其 data API,这在应用入口点是符合预期的,但在库中则不适合。另外 是基于 派生的,因此选择的 不要与 Bootstrap 内置插件冲突,否则你的实例会覆盖共享元素上的原有实例。
DATA_KEYbs.<name>NAME