- 前端
- UI组件
【免费下载链接】Choices
A vanilla JS customisable select box/text input plugin ⚡️
本指南以仓库根目录的 CONTRIBUTING.md 为骨架,面向希望向 Choices.js 提交代码的开发者:你将掌握仓库约定的代码风格与 lint 要求、理解从单元测试(Vitest)到端到端测试(Playwright)的完整验证链路,并深入拆解npm run js:watch -- --environment ...这类构建标志背后的 Rollup 编译机制,最终能按规范提交可合入的 Pull Request。文中所有命令、配置与结论均与当前仓库(Choices.js v11.2.4)的实际源码一一对应。
贡献规范总览:以现有代码风格为准
CONTRIBUTING.md 开篇即点明本项目的贡献准则:
In lieu of a formal styleguide, take care to maintain the existing coding style ensuring there are no linting errors. Add unit tests for any new or changed functionality.
翻译过来就是:仓库没有单独下发一份正式的 styleguide 文档,贡献者必须以“维持现有代码风格、不产生任何 lint 错误”为第一原则;同时,任何新增或修改的功能都必须配套单元测试。这两条是进入后续所有开发、构建与测试流程的前提。
“不产生 lint 错误”在仓库中有实际的落地工具链支撑,见 package.json:
npm run lint:并行执行lint:js与lint:scss;npm run lint:js:eslint src/scripts test/scripts test-e2e,即对源码、单元测试与 e2e 测试三块代码统一做 ESLint 检查;npm run lint:scss:stylelint src/**/*.scss,对 src/styles 下的 SCSS 源文件做样式规范检查。
此外,仓库通过 scripts/lint-staged.config.js 在提交阶段强制这些检查:
*.js文件提交前执行eslint --fix --quiet,自动修复并校验;*.{ts,scss,yaml,yml,md,html,json,...}提交前统一走prettier --write格式化;- 对
src/scripts/**/*.js还会额外触发npm run test:unit,确保修改的脚本不破坏既有单元测试。
也就是说,风格规范不是“建议”,而是通过 ESLint + Prettier + Stylelint 三重关卡在每次提交时强制执行的工程约束。
NPM 任务全览:开发、测试与构建命令
CONTRIBUTING.md 用一张表格列出了贡献者最常用的 npm 任务,下表完整保留原始说明,并补充了各任务在 package.json 中的真实底层实现:
| Task | Usage(原始说明) | 底层实现(package.json) |
|---|---|---|
npm run start | Fire up local server for development | run-p js:watch css:watch(并行启动 JS 与 SCSS 监听) |
npm run test:unit | Run sequence of tests once | vitest run |
npm run test:unit:watch | Fire up test server and re-test on file change | npm run test:unit -- --watch --inspect=5556 |
npm run test:e2e | Run sequence of e2e tests (with local server) | run-s playwright:cli(串行执行 Playwright CLI 测试) |
npm run test | Run both unit and e2e tests | run-s test:unit test:e2e:all |
npm run playwright:gui | Run Playwright e2e tests (GUI) | playwright test --ui --project=chromium |
npm run playwright:cli | Run Playwright e2e tests (CLI) | playwright test --project=chromium |
npm run js:build | Compile Choices to an uglified JavaScript file | rollup ... --environment WITH_D_TS_FILES:1 && mv public/assets/scripts/src public/types/ |
npm run css:watch | Watch SCSS files for changes. On a change, run build process | nodemon -e scss -x "npm run css:build" |
npm run css:build | Compile, minify and prefix SCSS files to CSS | run-s css:sass css:prefix css:min |
几点值得注意的细节:
start是开发模式的入口。它同时拉起两条 watch 链路:js:watch使用 Rollup 的-w参数监听源码变更并重新打包,css:watch使用 nodemon 监听scss文件变化。e2e 测试也依赖它——playwright.config.ts 中的webServer.command正是npm run start,默认访问http://localhost:3001。CSS 构建是三条流水线:
css:sass用 sass 将 src/styles/base.scss 与 src/styles/choices.scss 编译为 public/assets/styles 下的 CSS;css:prefix用 postcss + autoprefixer 补齐浏览器前缀;css:min用 csso 产出base.min.css与choices.min.css。js:build不只是压缩 JS。它通过WITH_D_TS_FILES:1让 Rollup 在打包的同时生成 TypeScript 声明文件(declaration: true,见 scripts/rollup.config.mjs),随后把产物移入public/types/——这就是 package.json 中"types": "./public/types/src/index.d.ts"的来源。
向 Rollup 传递环境参数:--语法与同名覆盖规则
CONTRIBUTING.md 特别强调:通过--后跟常规的 rollup--environment参数来传值,同名参数以最后一个为准(The last one overrides any previous ones with the same name)。
npm run js:watch -- --environment WATCH_HOST:0.0.0.0这条命令把WATCH_HOST设为0.0.0.0,使开发服务器监听所有网卡而非默认的localhost,方便局域网内的浏览器访问。其读取端在 scripts/rollup.config.mjs 与 scripts/server.mjs:
- Rollup 配置通过
process.env.WATCH_HOST读取该变量; - scripts/server.mjs 中的
server()函数只有在WATCH_HOST存在时才注入rollup-plugin-dev开发服务器,端口默认WATCH_PORT || 3001,目录指向public; - playwright.config.ts 中 e2e 测试的
baseURL也正是http://localhost:3001/,与之一致。
这一套“npm 传参 → 环境变量 → Rollup 读取”的机制,是后面所有构建标志(Build Flags)生效的基础。
构建标志(Build Flags):三种搜索能力与 DOM 支持的编译期开关
构建标志是 Choices.js 贡献体系中最核心的机制。CONTRIBUTING.md 定义了三个可通过环境变量控制的编译期开关,其默认值与源码中的消费逻辑如下。
CHOICES_SEARCH_FUSE:"full"/"basic"/"null"
Usage:The level of integration with fuse.
fullis the entire fuse.js build,basicis fuse.js with just standard fuzzy searching.nullis a basic prefix string search with no fuse.js.
它控制与 fuse.js 的集成深度:
full:完整 fuse.js 构建(默认值);basic:仅 fuse.js 的标准模糊搜索;null:完全不引入 fuse.js,退化为最轻量的前缀字符串搜索。
对应源码证据非常清晰。在 src/scripts/interfaces/build-flags.ts 中,searchFuse直接读取process.env.CHOICES_SEARCH_FUSE;src/scripts/search/index.ts 的getSearcher根据标志选择实现类:
export function getSearcher<T extends object>(config: Options): Searcher<T> { if (searchFuse && !searchKMP) { return new SearchByFuse<T>(config); // full / basic } if (searchKMP) { return new SearchByKMP<T>(config); } return new SearchByPrefixFilter<T>(config); // null }full与basic都走 src/scripts/search/fuse.ts:前者import FuseFull from 'fuse.js',后者import FuseBasic from 'fuse.js/basic',二者共用同一个SearchByFuse类,按searchFuse === 'full'选择实例化哪个 fuse 版本;null走 src/scripts/search/prefix-filter.ts 的SearchByPrefixFilter,它对searchFields中的每个字段做toLowerCase().startsWith(needle)前缀匹配,完全不依赖 fuse.js。
用法示例(来自 CONTRIBUTING.md):
npm run js:watch -- --environment CHOICES_SEARCH_FUSE:basicCHOICES_SEARCH_KMP:"1"/"0"
Usage:High performance
indexOf-like search algorithm.
开启后使用一个高性能的、类似indexOf的 KMP(Knuth–Morris–Pratt)字符串匹配算法。标志的读取同样在 src/scripts/interfaces/build-flags.ts:searchKMP = process.env.CHOICES_SEARCH_KMP === '1'。其实现位于 src/scripts/search/kmp.ts:
- 先为搜索模式串计算“最长公共前后缀表”(LSP 表,
kmpSearch的前半部分); - 再扫描文本,失配时按 LSP 表回退(
j = lsp[j - 1]),从而把匹配复杂度控制在 O(n) 量级,避免朴素算法的重复比较; - 对每个 choice 的每个
searchFields字段执行该匹配,命中即返回结果。
注意一个优先级细节:在getSearcher中,searchKMP的优先级高于 fuse——只要CHOICES_SEARCH_KMP=1,即使 fuse 标志为full也会选择 KMP 实现。
npm run js:watch -- --environment CHOICES_SEARCH_KMP:1CHOICES_CAN_USE_DOM:"1"/"0"
Usage:Indicates if DOM methods are supported in the global namespace. Useful if importing into DOM or the e2e tests without a DOM implementation available.
该标志声明全局命名空间是否具备 DOM 能力。在 src/scripts/interfaces/build-flags.ts 中:
export const canUseDom: boolean = process.env.CHOICES_CAN_USE_DOM !== undefined ? process.env.CHOICES_CAN_USE_DOM === '1' : !!(typeof document !== 'undefined' && document.createElement);- 显式传入
1/0时以显式值为准; - 未传入时回退到运行时探测:检查
document是否存在且具备createElement。
这对“在无 DOM 环境下导入库”或“e2e 测试中注入构建产物”很有价值——通过显式置0,可以让库在 Node 等无 DOM 环境里安全加载。
npm run js:watch -- --environment CHOICES_CAN_USE_DOM:1标志背后的编译机制:@rollup/plugin-replace与 tree-shaking
构建标志不只是“配置项”,它们在编译期被真实替换,从而影响产物体积。在 scripts/rollup.config.mjs 中:
const buildFeatures = { CHOICES_SEARCH_FUSE: "full", // "basic" / "null" CHOICES_SEARCH_KMP: "0", // "1" CHOICES_CAN_USE_DOM: "1", // "0" }每个环境变量都被replace插件注入为process.env.X常量(值为"null"时会被转换为'false'),Rollup 据此在打包阶段做 tree-shaking——未启用的搜索实现(fuse / KMP / prefix)整段代码都不会进入最终产物。这正是 public/assets/scripts 下同时存在choices.js、choices.search-basic.js、choices.search-prefix.js、choices.search-kmp.js等变体的原因,也对应 package.json 中./search-basic、./search-prefix、./search-kmp、./search-none的 exports 入口——使用者可以按体积与搜索能力需求选择对应构建。
值得留意的是 src/scripts/interfaces/build-flags.ts 末尾的注释:
These are not directly used, as an exported object (even as const) will prevent tree-shake away code paths
即:这些标志变量必须被单独、直接地导出,而不是聚合在一个导出对象里,否则会阻碍 tree-shaking 把死代码摇掉——这是写构建相关源码时必须遵守的纪律。
测试驱动贡献:从 Vitest 单元测试到 Playwright 端到端测试
单元测试(npm run test:unit)
单元测试由 Vitest 驱动,配置见 vitest.config.ts:
environment: 'jsdom':在 jsdom 模拟的 DOM 环境里运行;setupFiles: ['./test/setupFiles/window-matchMedia.ts']:启动前注入window.matchMedia的 mock(见 test/setupFiles/window-matchMedia.ts);include: ['test/**/*.{test,spec}.?(c|m)[jt]s?(x)']:测试文件统一放在 test/scripts 下,与 src/scripts 的目录结构一一对应(components、reducers、actions、store、search 等)。
贡献者提交新功能时,应模仿现有测试的写法。以搜索模块为例,test/scripts/search/index.test.ts 对SearchByFuse(full / basic 两档)、SearchByKMP、SearchByPrefixFilter四套实现做了等价性验证:空输入返回空结果、前缀命中数量、后缀匹配行为、结果顺序等。注意其中prefix-filter场景下搜索label 9的“后缀”用例期望返回 0 条——因为前缀过滤器只做startsWith,这正好印证了不同构建标志在行为上的差异。
模板系统的测试在 test/scripts/templates.test.ts,它通过shimTemplates将 src/scripts/templates.ts 中的默认模板方法绑定到伪造的 Choices 实例上,再逐一断言每个模板产出的 DOM 结构、data 属性与 class。
端到端测试(npm run test:e2e)
e2e 测试基于 Playwright,配置见 playwright.config.ts:
- 测试目录为
./test-e2e,覆盖 select-one、select-multiple、text、性能与 demo 页面(如 test-e2e/tests/select-multiple-performance.spec.ts); - 内置 chromium / firefox / webkit 三个浏览器项目;
- 测试前由
webServer自动执行npm run start拉起本地服务(端口 3001); - 通过 test-e2e/bundle-test.ts 的
bundlefixture 注入被测脚本地址:CI 下用choices.min.js,本地开发用未压缩的choices.js,确保压缩与未压缩产物都被真实浏览器验证; - 截图快照存放在 test-e2e/screenshots,命名模板为
{projectName}-{platform}{ext}。
压缩产物的纯度约束:new与get必须无副作用
CONTRIBUTING.md 有一行容易忽略但极其重要的硬性要求:
For compatibility,
newandgetmust be pure (side effect free).
这行约束与压缩流程直接相关。在 scripts/rollup.config.mjs 中,非 ESM 输出会经过 terser 压缩,且压缩参数显式开启了:
terser({ compress: { passes: 2, pure_getters: true, pure_new: true, } })pure_getters: true:terser 假定读取 getter 没有副作用,从而可以安全删除“只读不用”的访问表达式;pure_new: true:terser 假定执行new构造没有副作用,允许在结果未被使用时整段丢弃。
因此,如果贡献者编写的代码里,getter 或构造函数带有隐式副作用(例如在 getter 中修改外部状态、在构造时触发 DOM 写入),压缩后的 min 产物就会与开发产物行为不一致——这正是“new和get必须无副作用”这条约定存在的根本原因。写代码时请保持构造函数与属性 getter 的纯粹性,避免在压缩后“翻车”。
提交 Pull Request:使用问题模板
最后,CONTRIBUTING.md 建议:当提交的 Pull Request 用于修复 bug 时,尽量使用以下三段式模板,帮助维护者快速复现与评审:
## This is the problem: ## Steps to reproduce: ## This is my solution:即:描述问题 → 给出可复现步骤 → 说明你的解决方案。配合前文的 lint 检查、单元测试与构建标志验证,一份合格的 PR 应当做到:无 lint 错误、新功能有配套单测、改动逻辑(尤其涉及搜索构建时)在对应标志组合下验证通过,且不违反new/get无副作用的纯度约束。
小结
Choices.js 的贡献流程可以浓缩为四条主线:风格上以现有代码为准并通过 ESLint/Prettier/Stylelint 强制把关;功能上任何改动必须伴随单元测试,并经过 Vitest + Playwright 双层验证;构建上通过-- --environment传入的三个构建标志在编译期裁剪搜索能力与 DOM 依赖;代码质量上保证new与get无副作用以兼容压缩优化。理解了这些机制,你既能为这个轻量级选择框插件贡献高质量代码,也能在需要时基于这套“多构建变体 + tree-shaking”模式,为自己的库设计类似的可插拔编译体系。
- 前端
- UI组件
【免费下载链接】Choices
A vanilla JS customisable select box/text input plugin ⚡️
相关推荐
开发者指南:Valentina测试体系、代码风格与开源贡献流程
开发者指南:Valentina测试体系、代码风格与开源贡献流程 Valentina(fashionmaker)是一款跨平台服装打版程序,本文带你完整了解它的开发
桌面应用图形学Kong 开源贡献指南:PR 规范、Commit 约定、测试体系与 Lua 代码风格
Kong 开源贡献指南:PR 规范、Commit 约定、测试体系与 Lua 代码风格 本文基于 Kong 仓库根目录的 CONTRIBUTING.md http
API网关后端LLM 网关微服务人工智能Tornado 源码贡献指南:从测试、代码风格到文档构建的完整开发工作流
Tornado 源码贡献指南:从测试、代码风格到文档构建的完整开发工作流 Tornado 是源自 FriendFeed 的 Python Web 框架与异步网络
后端Web框架异步编程WebSocket
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考