- 构建工具
- 前端
【免费下载链接】brunch
🍴 Web applications made easy. Since 2011.
本文是 Brunch.io 官方指南(见 packages/brunch-guide/content/fr/README.md)系列的一部分,聚焦"如何将 Brunch 接入一个已存在的项目"。如果你正从 Grunt、Gulp 或其他构建工具迁移而来,面对的是一个早已成型的代码库而非按 Brunch 约定从零搭建的新项目,那么本指南将围绕五个关键决策点展开:源码在哪里、用什么语言、构建产物放哪里、源文件如何映射到目标文件、以及是否启用模块化包装。读完后,你将能写出第一份真正服务于存量项目的brunch-config,并理解paths、conventions、files、modules这些核心配置项在 Brunch 内部的真实运作方式。
迁移前要回答的五个问题
把 Brunch 交给一个既有项目之前,先冷静回答以下五个问题,它们分别对应brunch-config中的一组具体配置:
- 源码文件在哪里?—— 决定
paths.watched的值,即 Brunch 要构建并监视的根目录集合。 - 这些源码用什么语言编写?—— 决定你需要安装哪些 Brunch 插件(sass-brunch、stylus-brunch、jade-brunch 等)。
- 构建产物输出到哪个目录?—— 决定
paths.public的值。 - 源文件到目标文件的映射关系是什么?—— 决定
files配置中javascripts、stylesheets、templates三个小节的结构与joinTo规则。 - 要不要把应用 JS 包装成模块?—— 决定
modules.wrapper与modules.definition的设置。
下面逐一深入。
问题一:源码在哪 ——paths.watched与相关约定
paths.watched用于描述构建所依赖的根路径集合。其默认值为['app', 'test', 'vendor'](见 lib/utils/config-validate.js 中的 schema 定义),但绝大多数存量项目都需要显式修改它——你的源码可能分布在src/、lib/或若干个平级目录中,而默认的app目录根本不存在。
围绕源码位置,还有两个conventions配置需要一并考虑:
conventions.assets:指定哪些目录下的文件会被"原样复制"(copy-paste)到输出目录,而不经过任何编译或打包。默认匹配规则为/assets\//(见 lib/utils/config-validate.js),也就是说任何路径中包含assets/的文件都会被当作静态资源直接搬运。conventions.vendor:指定哪些目录中的 JS不应被包装成模块,默认匹配规则为/(^node_modules|vendor)\//(见 lib/utils/config-validate.js)。需要注意:如果通过 Bower 引入组件,Brunch 对这些组件永远不做模块包装,即使它们不在vendor目录中。
从实现上看,这两条约定在配置加载时会被归一化为可复用的判定函数。在 lib/utils/config.js 的normalizeConfig中,每个约定都通过anymatch编译成conventions[key]检查器;其中vendor约定直接使用该检查器,而其他约定则额外排除了 npm 包路径(!deppack.isNpm(path) && fn(path))。这些检查器随后被 lib/fs_utils/file_list.js 用来决定一个文件被归入assets(静态资源)还是files(源码),并最终决定其是否参与模块包装。
示例:假设你的存量项目源码放在src/,第三方库在lib/third_party/:
// brunch-config.js module.exports = { paths: { watched: ['src', 'lib/third_party'] }, conventions: { assets: /assets\//, vendor: /third_party\// } };问题二:用什么语言 —— 按需混装插件
问题二决定你需要哪些Brunch 插件。核心思想是:同一类源码可以混用多种语言及其插件。例如:
- 使用 Bootstrap 时,作者倾向于直接用它的 SASS 源码,这样可以通过修改
_variables.scss轻松定制主题; - 而自己的样式则偏爱 Stylus;
- 于是
sass-brunch与stylus-brunch常常同时安装、同时工作。
如果应用采用客户端 MVC 架构,通常会把模板独立成文件,这时可用jade-brunch或dust-linkedin-brunch之类的模板插件,把模板透明地编译成"导出唯一渲染函数"的模块——该函数接收一个 presenter(即 view model)对象作为参数,并同步返回 HTML 字符串。
插件混装之所以可行,源于 Brunch 的管道式编译设计。在 lib/fs_utils/pipeline.js 中,nextCompiler会按compiler.pattern依次匹配文件路径并逐个调用compiler.compile(file),直到没有插件能处理为止;换言之,多个编译器可以串联作用于同一份源码,也可以按路径各司其职。每种源码类型(如sass、stylus、jade)对应一个实现了compile方法、声明了pattern的插件对象。
问题三:构建产物输出到哪 ——paths.public
paths.public指定构建产物的输出目录,默认值为'public'(见 lib/utils/config-validate.js)。有两点值得注意:
- 该目录不必在构建前存在,Brunch 在写出文件时会自动创建目录(见 lib/fs_utils/generate.js 中
writeFile对父目录的递归创建逻辑)。 - 所有目标文件路径(即
joinTo的键)都是相对于paths.public解析的。
从源码看,lib/utils/config.js 中的setConfigDefaults会把paths.public与paths.root拼接成绝对路径,并将其同步到config.server.publicPath,保证内置服务器在overrides生效后仍指向正确的输出目录(见 lib/utils/config.js)。
问题四:源到目标的映射 ——files与joinTo
files配置是迁移中最核心、最灵活的部分,最多包含三个小节:
javascripts:所有最终产出为 JS 的文件(模板预编译除外);stylesheets:所有最终产出为 CSS 的文件;templates:所有模板预编译产物——每个模板被预编译成一个渲染函数,接收 presenter(view model)对象、返回 HTML;通常其目标与javascripts的核心目标相同。
每个小节至少包含一个joinTo属性。joinTo的值可以非常简单,也可以相当高级:
形式一:单个字符串
如果只提供一个文件路径字符串,该小节所有候选文件都会被拼接到这一个文件中:
files: { stylesheets: { joinTo: 'stylesheets/app.css' } }形式二:对象(多目标 + 过滤)
如果提供一个对象,则键是目标文件路径,值是 anymatch 匹配集,用于决定哪些源文件进入哪个目标。匹配值可以是:
| 类型 | 说明 | 示例 |
|---|---|---|
String | 精确匹配 Brunch 眼中的文件路径(见下文"模块名从哪来"对路径形态的说明) | 'app/init.js' |
| 正则表达式 | 匹配文件路径,特别适合路径前缀 | /^app\//、/^vendor\// |
| 谓词函数 | 接收精确路径,同步返回布尔值决定是否收录 | path => path.endsWith('.coffee') |
| 数组 | 上述任意类型的混合 | ['app/', /^vendor\//] |
下面是一个真实项目(Chaplain 骨架)的完整示例,见 packages/skeletons/brunch-with-chaplin-js/brunch-config.js:
exports.config = { files: { javascripts: { joinTo: { 'javascripts/app.js': /^app/, 'javascripts/vendor.js': /^(?!app)/ } }, stylesheets: { joinTo: 'stylesheets/app.css' }, templates: { joinTo: 'javascripts/app.js' } } };可以看到:javascripts被拆成app.js(以app开头的文件)与vendor.js(其余文件)两个目标;templates与核心 JS 共享同一个目标javascripts/app.js;stylesheets则使用最简单的字符串形式。
从源码看,joinTo在配置加载时会被统一归一化。在 lib/utils/config.js 的normalizeJoinConfig中:
- 字符串形式的
joinTo会被转换为{[目标路径]: () => true},即"所有文件都进这一个目标"; - 对象形式则对每个目标值调用
anymatch(checker)编译成匹配函数。
此外,lib/utils/config.js 中的createJoinConfig还隐含一条规则:如果javascripts定义了joinTo而templates没有定义,templates会自动继承javascripts.joinTo——这正是上面示例中模板可以省略、也可以显式写出同一个目标的底层原因。
还有一个补充机制值得一提:config.files.javascripts.entryPoints可以把joinTo视为一种"入口点"的特殊情形,让某个入口文件单独打包(见 lib/utils/config.js),但入口文件必须位于paths.watched之内,否则会报错。
问题五:要不要模块化 ——modules.wrapper与modules.definition
第五个问题在作者看来根本不该是问题:当然要用模块。默认情况下 Brunch 采用CommonJS(这既便于编写同构 JS,也为日后迁移到原生 ES6 模块铺路)。
模块化由两个配置联合控制(默认值见 lib/utils/config-validate.js):
modules.wrapper:决定单个源文件如何被包装成模块(默认'commonjs');modules.definition:决定模块系统的运行时定义代码如何注入输出文件(默认'commonjs')。
可用的取值:
| 取值 | 效果 |
|---|---|
'commonjs'(默认) | 每个模块被包装为require.register("模块名", function(exports, require, module) { ... });,并在输出头部注入 CommonJS 的 require 定义 |
false | 关闭包装/定义,文件内容原样输出——适合"想回到石器时代"的纯拼接场景 |
'amd' | 使用 AMD 模块包装 |
| 自定义函数 | 为更小众的模块系统提供完全定制化的包装与定义逻辑 |
从源码看,lib/utils/modules.js 中的getWrapperFn明确给出了commonjs与false两种内置包装器的实现:前者生成require.register(...)的前缀与后缀,后者直接返回原数据(即不包装)。normalizeWrapper还会先对路径做规范化,再调用nameCleaner得到最终模块名(lib/utils/modules.js)。运行时定义则由 lib/utils/modules.js 的normalizeDefinition决定:commonjs返回commonjs-require-definition包(见 packages/subdependencies/commonjs-require-definition/require.js),false则返回空字符串。
需要提醒的是:模块包装只对"非 vendor、非 helper"的 JS 生效。在 lib/fs_utils/source_file.js 中,_shouldBeWrapped要求文件isJS && !isVendor;换言之,命中conventions.vendor的文件即使开启了 CommonJS 也不会被包装。
模块名从哪来 —— 默认命名算法与nameCleaner
使用模块化后,最后一个重要话题是模块名。默认情况下 Brunch 的模块命名算法分三步:
- 取文件相对于监视根目录的精确路径,例如
"app/application.js"; - 去掉扩展名,得到
"app/application"; - 如果只有一个参与模块包装的监视路径(默认只有
"app"),则去掉该前缀,得到"application";但如果存在多个参与包装的监视路径,前缀会保留。
如果你不满意默认命名,可以通过modules.nameCleaner提供一个自定义的模块名计算函数。默认的nameCleaner实现是path => path.replace(/^app\//, '')(见 lib/utils/config-validate.js),与上述第三步的行为完全吻合。
实战示例:保留长文件名、输出短模块名
假设你的存量项目把第三方库放在app/externals/目录,文件名携带版本与语言信息(如jquery-1.11.2-min.js、moment-2.2.1-fr.js),但你希望模块名保持简洁通用(如"jquery"、"moment")。此时可以在brunch-config.js中这样写:
module.exports = { modules: { nameCleaner(path) { return path // 去掉 app/ 与 app/externals/ 前缀 .replace(/^app\/(?:externals\/)?/, '') // 去掉形如 -1.11.2 的版本号后缀 .replace(/-\d+(?:\.\d+)+/, '') // 把 -fr. 语言后缀处理成 . .replace('-fr.', '.') } } };这个函数的输入正是"从监视根目录开始的相对路径 + 扩展名",它在 lib/utils/config.js 的normalizeConfig中被传入wrappers.normalizeWrapper,并在 lib/utils/modules.js 中应用于每一个被包装的模块路径,最终出现在require.register("jquery", ...)的模块名位置上。
验证与调试:配置加载路径一览
理解上述配置如何进入运行时,有助于排查迁移中的问题。在 lib/utils/config.js 的loadConfig中,完整流程大致是:
- 读取项目根目录的
package.json; - 按优先级加载配置:
package.json中的brunch字段 →brunch-config.js→ 默认配置(见tryToLoad,lib/utils/config.js); - 用 skemata schema 校验配置并填充默认值(见 lib/utils/config-validate.js);
- 应用
overrides(按NODE_ENV/BRUNCH_ENV等环境切换,例如 test/fixtures/config-with-overrides.js 展示了按环境切换paths.public的用法,对应测试见 test/config.js); - 归一化
joinTo与conventions,生成config._normalized; - 加载插件并开始构建。
迁移存量项目时若发现文件没有按预期进入某个目标,优先检查三处:paths.watched是否覆盖了真实源码目录、conventions.vendor是否误伤了应参与包装的文件、以及joinTo的正则是否与实际路径形态(相对监视根目录)吻合。
小结
将 Brunch 接入存量代码库,本质上就是用一份brunch-config明确回答五个问题:源码位置(paths.watched)、语言与插件(sass-brunch、stylus-brunch、jade-brunch等)、输出目录(paths.public)、源到目标的映射(files.javascripts/files.stylesheets/files.templates及joinTo的字符串、正则、函数、数组四种匹配形式)、以及模块化策略(modules.wrapper/modules.definition/modules.nameCleaner)。这五组配置的组合能力,足以让 Brunch 在不重构既有目录结构的前提下接管任何历史项目的构建;而joinTo的 anymatch 匹配与模块名的自定义清洗,则是让迁移过程"既保目录原貌、又得模块整洁"的两个关键杠杆。
「上一篇:模板初体验 • 下一篇:开发与生产构建」
- 构建工具
- 前端
【免费下载链接】brunch
🍴 Web applications made easy. Since 2011.
相关推荐
Terratest v2 导入路径迁移完全指南:import map 全量映射表与源码级解读
Terratest v2 导入路径迁移完全指南:import map 全量映射表与源码级解读 Terratest v2 将原本单一的 github.com/gr
测试开发工具DevOps质量保障未来已来:ElasticDL与DLRover的技术演进与自动扩展训练展望
未来已来:ElasticDL与DLRover的技术演进与自动扩展训练展望 在当今AI大模型时代,分布式深度学习训练已成为技术发展的必然趋势。 ElasticDL
大模型人工智能NLP本地部署微调模型推理服务notepad-- 代码折叠:3 步把 5000 行文件折成一张目录
notepad 代码折叠:3 步把 5000 行文件折成一张目录 周一 9:40,你盯着一份 4800 行的 .cpp 文件,滚轮翻了 20 分钟,回答不出一个
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考