脚手架有一个梦想:陪项目走到仓库归档。
随着开发生产活动的进行,我们有了越来越多的大小项目。这些项目大体上有相似的结构、配置,但是过去除了一些细节较多的通用配置(如 tsconfig 文件,eslint 规则等)我们没有维护一个专门的项目模板仓库,究其原因估计主要有两点:
- 项目配置相似,但是根据项目大小、运行环境,目录结构还是有区别。搞一个项目模板不一定有新建项目时复制粘贴来得快捷。
- 项目模板随时间始终会有一些大大小小的变化,意味着每次想要新建项目时,项目模板几乎总会是不够新的。
久而久之,根据项目创建的时间和维护的频率,我们甚至能在各个仓库的最新版本找到各个时期的项目结构和配置。
这里鸣谢 yarn.lock(有的同学可能需要鸣谢 package-lock.json),让我们基本上可以在两年后突然需要维护某个小项目时没有大的障碍。
但强迫症表示,他只要最新的!所以很久以前我就萌生了一个想法,搞一套“活”的项目模板工具:一个命令,就可以把已有项目内,与模板相关的内容全部更新,同时不破坏后续的修改。
这套工具就是 magicspace。
更新思路
magicspace 的思路其实很简单:提供一套可配置的模板项目生成机制,并把每次更新生成的版本作为一个新的 commit 提交到模板分支,再将模板分支合并到项目分支,按需解决冲突并完成合并,模板更新就完成了。至于冲突解决,那是 Git 和使用者的事情,magicspace 就不管了。
不过在实现上,所谓的模板分支并非始终存在,而是“隐藏”在了项目分支之中。magicspace 会识别 commit message 为 (magicspace-*)
的 commit,并在处理过程中从其中最近的一个切出分支。模板分支上的提交是 magicspace 自动完成的,修改合并分支时自动生成的 commit message 并不会影响 magicspace 的后续使用。
最初的方案没有使用 Git,核心是结构化的值,提供了一系列内置的合并选项(merge、override 等)让使用者组合相关内容。当时的实现要求使用者不能修改多数由模板维护的文件,而需要辗转修改相关模板配置后重新生成。实现的时候就觉得很蛋疼,实际使用起来也确实很蛋疼。于是当时 magicspace 就吃灰了两年。
模板编写
吸取了之前的一些经验,新的文件生成机制就做得非常轻量,一些常用的操作也仅仅是作为工具函数抽了出来,没有加入核心设计。
magicspace 模板中,每一个最终生成的文件对应一个 composable file 对象,模板可以继承,并通过提供相同路径的 composable 对象来编排文件内容。这个过程类似一个管道,原始内容通过多个 composable 加工,变成最后的文件。
以 JSON 文件举例,假定 composable module 文件名为 package.json.js
,默认对应的 composable file 输出路径则为 package.json
。如果当前模板继承的模板已经有相同的文件,那么我们可以通过以下代码为这个 JSON 文件添加一个名为 build
的脚本:
module.exports = {
type: 'json',
compose(data) {
data.scripts = {
...data.scripts,
build: 'tsc --build .',
};
},
};
一个更实际的例子:
https://github.com/makeflow/mufan-code-boilerplates/blob/master/typescript/src/composables/package.json.tsgithub.com如上方链接的例子,composable module 可以生成多个文件,也可以导出一个函数根据用户或模板配置的参数(boilerplate.json
)动态生成文件内容。
在 @magicspace/core
包中,提供了 text
、json
、copy
、handlebars
等简单封装的函数用于构建 composable 对象;@magicspace/utils
包中则提供了另一些工具函数,用于,比如:
extendObjectProperties
在特定的键前后扩展属性,如{after: '*build*'}
在包含build
的键后扩展属性。extendPackageScript
在脚本特定的命令前后增加命令(字符串修改)。
模板使用
以我们自用的模板为例:
yarn global add magicspace makeflow/mufan-code-boilerplates
其中 makeflow/mufan-code-boilerplates
是从 GitHub 仓库安装,包名实际为 @mufan/code-boilerplates
。
创建项目及配置文件
mkdir my-project
cd my-project
magicspace create @mufan/code-boilerplates/typescript
选择模板配置用例创建 .magicspace/boilerplate.json
,比如选择上方的“library”我们会得到如下模板配置文件:
{
"extends": "@mufan/code-boilerplates/typescript",
"options": {
"name": "awesome-library",
"license": "MIT",
"author": "Awesome Author",
"tsProjects": [
{
"name": "library"
},
{
"name": "test"
}
]
}
}
其中 extends
是一个字符串或数组,即要继承、使用的模板,解析规则和 Node.js require 类似。可用参数可以参考该模板的相关声明:
根据需要修改后初始化 Git 仓库并进行一次提交。
项目初始化
magicspace init
根据需要修改文件完成合并。
项目更新
由于 magicspace 模板是动态生成的,那么在如下几种情况可以对项目相关部分进行更新:
- 模板更新
- 模板配置更新(使用者的
.magicspace/boilerplate.json
) - 模板中动态请求的内容更新(比如我们自用模板中的包版本号)
magicspace update
同样也是更新后根据需要修改文件、解决冲突,并完成分支合并。如果想要中止更新,只需要执行 git merge --abort
即可。
关于冲突解决
由于模板分支本身和项目分支是平行的,受限于 magicspace 选择的分支合并的方案和 Git 的相关能力,很多时候更新都会产生需要手动解决的冲突:
但即便如此,我认为带来的好处依然是显著的,最坏的情况至少把有变化的内容通过冲突的形式展现出来,方便使用者取用。
复用现有模板
magicspace 提供了 postgenerate
的生命周期钩子,因此即便不使用它提供的 composable 机制,也可以直接利用这个生命周期钩子实现现有模板复用。
作为例子我增加了一个 @magicspace/boilerplate-url
模板, 利用这个钩子下载指定 URL 的模板:
{
"extends": "@magicspace/boilerplate-url",
"options": {
"url": "https://github.com/react-boilerplate/react-boilerplate/archive/master.zip",
"strip": 1
}
}
在 postgenerate
中我们可以直接使用模板包依赖中的命令,因此如果需要,我们完全可以将一些现有脚手架和 magicspace 组合使用。
Makeflow(makeflow.com)让团队经验可以像文档一样详细地记录在流程中,指导和验证工作实践。每一次经验的迭代都可以通过任务的执行自然“推送”到整个团队,消除工作流程从想法到实践、从实践到改进之间的多种障碍。大到产品迭代管理,小到监控报警处置:记录、实践、再记录,把每一次进步写入团队基因——延续、变化、可复制。