news 2026/10/15 12:05:38

Choices.js 贡献开发指南:代码风格约束、完整测试体系与 Rollup 构建标志详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Choices.js 贡献开发指南:代码风格约束、完整测试体系与 Rollup 构建标志详解
  • 前端
  • UI组件

【免费下载链接】Choices

A vanilla JS customisable select box/text input plugin ⚡️

项目地址:https://gitcode.com/gh_mirrors/ch/Choices
点击查看免费下载

本指南以仓库根目录的 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 中的真实底层实现:

TaskUsage(原始说明)底层实现(package.json)
npm run startFire up local server for developmentrun-p js:watch css:watch(并行启动 JS 与 SCSS 监听)
npm run test:unitRun sequence of tests oncevitest run
npm run test:unit:watchFire up test server and re-test on file changenpm run test:unit -- --watch --inspect=5556
npm run test:e2eRun sequence of e2e tests (with local server)run-s playwright:cli(串行执行 Playwright CLI 测试)
npm run testRun both unit and e2e testsrun-s test:unit test:e2e:all
npm run playwright:guiRun Playwright e2e tests (GUI)playwright test --ui --project=chromium
npm run playwright:cliRun Playwright e2e tests (CLI)playwright test --project=chromium
npm run js:buildCompile Choices to an uglified JavaScript filerollup ... --environment WITH_D_TS_FILES:1 && mv public/assets/scripts/src public/types/
npm run css:watchWatch SCSS files for changes. On a change, run build processnodemon -e scss -x "npm run css:build"
npm run css:buildCompile, minify and prefix SCSS files to CSSrun-s css:sass css:prefix css:min

几点值得注意的细节:

  1. start是开发模式的入口。它同时拉起两条 watch 链路:js:watch使用 Rollup 的-w参数监听源码变更并重新打包,css:watch使用 nodemon 监听scss文件变化。e2e 测试也依赖它——playwright.config.ts 中的webServer.command正是npm run start,默认访问http://localhost:3001。

  2. 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。

  3. 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:basic

CHOICES_SEARCH_KMP:"1"/"0"

Usage:High performanceindexOf-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:1

CHOICES_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 ⚡️

项目地址:https://gitcode.com/gh_mirrors/ch/Choices
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 12:05:32

CodeGraph实操:从零剖析大型工程的模块依赖与循环依赖

1. CodeGraph帮我解决了什么真实痛点1.1 从"读代码头痛"到"先看地图"先说个我亲身经历的场景。半年前我接手了一个为公司内部运营系统做维护的老项目&#xff0c;PHP为主&#xff0c;掺杂着几段Node.js写的命令行脚本&#xff0c;代码量大概十多万行。前任…

作者头像 李华
网站建设 2026/10/15 12:05:19

C++调用C# dll实战:C++/CLI桥接方案与避坑指南

简介&#xff1a;本资源面向需要在C工程中复用C#类库的开发者&#xff0c;聚焦跨语言互操作这一常见难题&#xff0c;提供一套可在Visual Studio 2010环境下直接运行的完整示例。内容围绕C#端定义公共类与导出方法、C端通过P/Invoke与GetProcAddress动态获取函数地址并调用展开…

作者头像 李华
网站建设 2026/10/15 12:05:17

STM32循迹避障小车代码实战:从传感器采样到电机差速的完整工程

简介&#xff1a;这份资源是面向嵌入式初学者与电子竞赛选手的STM32循迹避障小车完整工程代码&#xff0c;围绕路径跟踪与障碍物避障两大核心功能展开&#xff0c;适合已掌握C语言基础、希望从外设驱动进阶到综合项目实战的开发者。压缩包共223个文件&#xff0c;约7.83MB&…

作者头像 李华
网站建设 2026/10/15 12:04:19

参考资源如何成为团队知识库的基石?从筛选到维护

我翻出那本早年的技术书&#xff0c;"第32章&#xff1a;参考资源"几个字印在目录最后&#xff0c;很多读者都直接跳过它。做了多年开发后&#xff0c;我反而越来越看重这类章节——项目做不下去的时候&#xff0c;最后能拉你一把的不是某个高深算法&#xff0c;而是…

作者头像 李华
网站建设 2026/10/15 12:04:14

C#网络调试助手:TCP/UDP底层通信实时可视化工具

简介&#xff1a;这是一份面向C#初学者与.NET网络开发者的实战型调试工具源码&#xff0c;专为掌握TCP/UDP通信原理及Socket编程而设计。资源提供完整的网络调试助手可视化实现&#xff0c;涵盖连接管理、数据收发、日志记录与异常处理等核心功能&#xff0c;可直接用于协议验证…

作者头像 李华
网站建设 2026/10/15 12:04:09

U8 Cloud持久层红皮书:从JDBC封装到ORM映射全解析

简介&#xff1a;《用友U8 cloud V1.0-持久层技术红皮书》面向用友U8C开发工程师、ERP项目实施与运维人员&#xff0c;聚焦云ERP平台中的数据访问与持久化环节&#xff0c;系统讲解U8 cloud数据访问层的高性能、灵活性、安全性和易维护性设计&#xff0c;以及JDBC FrameWork中J…

作者头像 李华