⚠️ 本文为掘金社区首发签约文章,未获授权禁止转载
前言
前端工程化的实战目前已经到了第三篇,在前两篇中已经分别搭建了一个简单的 React 脚手架与一个初步可用的 CLI 工具。
在基础脚手架搭建完毕之后,我们就可以着手于模板管理的开发,毕竟在真实环境中,一套脚手架是远远不能满足需求的,我们会面对各式各样的业务场景,而对于这些场景都可能会有定制的脚手架出现。
那么我们该如何管理这些脚手架呢?
功能设计
如果每一次新脚手架的开发或者模板的更新都需要重新更新一次 CLI 的话,虽然成本不高,但是开发模板的同学需要通知 CLI 开发的同学去升级,使用模板的同学又需要在等 CLI 开发完毕才能使用,中间交流沟通的成本就增加了。
其次,对于业务开发同学来说,可能只需要一类或者几类的模板,那么如果 CLI 是一个大而全的模板集合,对这些同学来说,快速选择模板创建项目反而也是一个负担,因为要在很多模板中选择自己想要的也是很花费时间。
所以我们的目的是设计一款拥有自定义配置与可升级模板功能的 CLI 工具。
既然是自定义配置,那么就需要用户可以在本地手动添加、删除、更新自己常用的模板信息,同时需要可以动态的拉取这些模板而不是一直下载下来就是本地的旧版本。
根据需求,可简单设计一下我们 CLI 的模板功能概要:
- 需要保存模板来源的地址
- 根据用户的选择拉取不同的模板代码
- 将模板保存在本地
实战开发
那么根据上面的设计思路,我们可以一步步开发所需要的功能
本地保存模板地址功能
第一步,如果需要将模板的一些信息保存在本地的话,我们需要一个对话型的交互,引导用户输入我们需要的信息,所以可以选择 inquirer 这个工具库。
一般拉取代码的话,我们需要知道用户输入的模板地址(通过 URL 拉取对应模板的必须条件)、模板别名(方便用户做搜索)、模板描述(方便用户了解模板信息)
这样需要保存的模板信息有地址、别名与描述,后续可以方便我们去管理对应的模板。示例代码如下:
import inquirer from 'inquirer';
import { addTpl } from '@/tpl'
const promptList = [
{
type: 'input',
message: '请输入仓库地址:',
name: 'tplUrl',
default: 'https://github.com/boty-design/react-tpl'
},
{
type: 'input',
message: '模板标题(默认为 Git 名作为标题):',
name: 'name',
default({ tplUrl }: { tplUrl: string }) {
return tplUrl.substring(tplUrl.lastIndexOf('/') + 1)
}
},
{
type: 'input',
message: '描述:',
name: 'desc',
}
];
export default () => {
inquirer.prompt(promptList).then((answers: any) => {
const { tplUrl, name, desc } = answers
addTpl(tplUrl, name, desc)
})
}
通过 inquirer
已经拿到了对应的信息,但由于会有电脑重启等各种情况发生,所以数据存在缓存中是不方便的,这种 CLI 工具如果使用数据库来存储也是大材小用,所以可以将信息直接已经以 json 文件的方式存储在本地。
示例代码如下:
import { loggerError, loggerSuccess, getDirPath } from '@/util'
import { loadFile, writeFile } from '@/util/file'
interface ITpl {
tplUrl: string
name: string
desc: string
}
const addTpl = async (tplUrl: string, name: string, desc: string) => {
const cacheTpl = getDirPath('../cacheTpl')
try {
const tplConfig = loadFile<ITpl[]>(`${cacheTpl}/.tpl.json`)
let file = [{
tplUrl,
name,
desc
}]
if (tplConfig) {
const isExist = tplConfig.some(tpl => tpl.name === name)
if (isExist) {
file = tplConfig.map(tpl => {
if (tpl.name === name) {
return {
tplUrl,
name,
desc
}
}
return tpl
})
} else {
file = [
...tplConfig,
...file
]
}
}
writeFile(cacheTpl, '.tpl.json', JSON.stringify(file, null, "\t"))
loggerSuccess('Add Template Successful!')
} catch (error) {
loggerError(error)
}
}
export {
addTpl,
}
这里我们需要对是否保存还是更新模板做一个简单的流程判断:
- 判断当前是否存在 tpl 的缓存文件,如果已存在缓存文件,那么需要跟当前的模板信息合并,如果不存在的话则需要创建文件,将获取的信息保存进去。
- 如果当前已存在缓存文件,需要根据
name
判断是已经被缓存了,如果被缓存了的话,则根据name
来更新对应的模板信息。
接下来,我们来演示一下,使用的效果。
根据之前的操作,构建完 CLI 之后,运行 fe-cil add tpl
可以得到如下的结果:
那么在对应的路径可以看到已经将这条模板信息缓存下来了。
如上,我们已经完成一个简单的本地对模板信息添加与修改功能,同样删除也是类似的操作,根据自己的实际需求开发即可。
下载模板
在保存了模板之后,我们需要选择对应的模板下载了。
下载可以使用 download-git-repo 作为 CLI 下载的插件,这是一款非常好用的插件,支持无 clone 去下载对应的模板,非常适合我们的项目。
同样在下载模板的时候,我们需要给用户展示当前的保存好的模板列表,这里同样需要使用到 inquirer
工具。
- 使用
inquirer
创建 list 选择交互模式,读取本地模板列表,让用户选择需要的模板
export const selectTpl = () => {
const tplList = getTplList()
const promptList = [
{
type: 'list',
message: '请选择模板下载:',
name: 'name',
choices: tplList && tplList.map((tpl: ITpl) => tpl.name)
},
{
type: 'input',
message: '下载路径:',
name: 'path',
default({ name }: { name: string }) {
return name.substring(name.lastIndexOf('/') + 1)
}
}
];
inquirer.prompt(promptList).then((answers: any) => {
const { name, path } = answers
const select = tplList && tplList.filter((tpl: ITpl) => tpl.name)
const tplUrl = select && select[0].tplUrl || ''
loadTpl(name, tplUrl, path)
})
}
- 使用
download-git-repo
下载对应的模板
export const loadTpl = (name: string, tplUrl: string, path: string) => {
download(`direct:${tplUrl}`, getCwdPath(`./${path}`), (err: string) => {
if (err) {
loggerError(err)
} else {
loggerSuccess(`Download ${name} Template Successful!`)
}
})
}
但是问题来了,如果选择 direct
的模式,那么下载的是一个 zip 的地址,而不是正常的 git 地址,那么我们上述的地址就无效了,所以在正式下载代码之前需要对地址做一层转换。
首先看拉取规则,正常的 git 地址是 https://github.com/boty-design/react-tpl
,而实际在 github 中下载的地址则是 https://codeload.github.com/boty-design/react-tpl/zip/refs/heads/main
,可以看到对比正常的 github 链接的话,域名跟链接都有所改变,但是一定有项目名跟团队名,所以我们在存储的时候可以将 boty-design/react-tpl
拆出来,后期方便我们组装。
const { pathname } = new URL(tplUrl)
if (tplUrl.includes('github.com')) {
reTpl.org = pathname.substring(1)
reTpl.downLoadUrl = 'https://codeload.github.com'
}
如上述代码,解析 tplUrl
拿到的 pathname
就是我们需要的信息,再 dowload 模板的时候,重新组装下载链接即可。
如上图所示,我们可以将公共的模板下载到本地,方便同学正常开发了,但是此时还有一个问题,那就是上面的分支是 main 分支,不是每一个模板都有这个分支,可控性太差,那么我们怎么拿到项目所有的分支来选择性下载呢。
Github Api
在 Github 中对于开源、不是私有的项目,可以省去授权 token 的步骤,直接使用 Github Api 获取到对应的信息。
所以针对上面提到问题,我们可以借助 Github Api 提供的能力来解决。
获取分支的链接是 https://api.github.com/repos/boty-design/react-tpl/branches
,在开发之前我们可以使用 PostMan 来测试一下是否正常返回我们需要的结果。
如上可以看到,已经能通过 Github Api 拿到我们想要的分支信息了。
针对上述的问题,我们需要的是控制频率、使用带条件的请求或者使用 token 请求 Github Api 的方式来规避,但是鉴于模板来说,一般请求频率也不会很高,只是我在开发的时候需要不断的请求来测试,才会出现这种问题,各位同学有兴趣的话可以自己试试其他的解决方案。
分支代码优化
在预研完 Github Api 之后,接下来就需要对拿到的信息做一层封装,例如只有一条分支的时候用户可以直接下载模板,如果请求到多条分支的时候,则需要显示分支让用户自由选择对应的分支下载模板,整体的业务流程图如上所示。
主要逻辑代码如下:
export const selectTpl = async () => {
const prompts: any = new Subject();
let select: ITpl
let githubName: string
let path: string
let loadUrl: string
try {
const onEachAnswer = async (result: any) => {
const { name, answer } = result
if (name === 'name') {
githubName = answer
select = tplList.filter((tpl: ITpl) => tpl.name === answer)[0]
const { downloadUrl, org } = select
const branches = await getGithubBranch(select) as IBranch[]
loadUrl = `${downloadUrl}/${org}/zip/refs/heads`
if (branches.length === 1) {
loadUrl = `${loadUrl}/${branches[0].name}`
prompts.next({
type: 'input',
message: '下载路径:',
name: 'path',
default: githubName
});
} else {
prompts.next({
type: 'list',
message: '请选择分支:',
name: 'branch',
choices: branches.map((branch: IBranch) => branch.name)
});
}
}
if (name === 'branch') {
loadUrl = `${loadUrl}/${answer}`
prompts.next({
type: 'input',
message: '下载路径:',
name: 'path',
default: githubName
});
}
if (name === 'path') {
path = answer
prompts.complete();
}
}
const onError = (error: string) => {
loggerError(error)
}
const onCompleted = () => {
loadTpl(githubName, loadUrl, path)
}
inquirer.prompt(prompts).ui.process.subscribe(onEachAnswer, onError, onCompleted);
const tplList = getTplList() as ITpl[]
prompts.next({
type: 'list',
message: '请选择模板:',
name: 'name',
choices: tplList.map((tpl: ITpl) => tpl.name)
});
} catch (error) {
loggerError(error)
}
}
上述代码,我们可以看到使用了 RXJS 来动态的渲染交互问题,因为存在一些模板的项目分支只有一个的情况。如果我们每次都需要用户都去选择分支是有多余累赘的,所以固定的问题式交互已经不适用了,我们需要借助 RXJS 动态添加 inquirer 问题,通过获取的分支数量来判断是否出现选择分支这个选项,提高用户体验。
写在最后
在第一篇 企业级 CLI 开发 中,我们一起搭建了一个初步的 CLI 架子,提供了构建、Eslint 校验等基础功能。
在第二篇 自定义 React 脚手架 & CLI 升级 中,我们一起搭建一个简单的 React 脚手架,以及使用 CLI 去接管模板的 dev 模块,并且提供了拓展构建配置等功能,完成一个基础的 CLI 模板工具的整合
在这一篇中,我们将上一篇的脚手架作为模板,对 CLI 进行了更进一步的改造,使得 CLI 可以让用户自主的配置符合自己需求与习惯的模板。
经过这三篇文章,CLI 已经具备了下述这些功能:
CLI 命令 | 功能 | fe-cli eslint | 对当前项目进行 Eslint 校验 | fe-cli webpack | 使用 Webapck 当前项目进行构建 | fe-cli rollup | 使用 Rollup 当前项目进行构建 | fe-cli git init | 本地初始化 git 项目(当前支持 GitLab 部分功能) | fe-cli add tpl | 自定义添加模板 | fe-cli init tpl | 将添加的模板初始化到本地 |
---|
整个 CLI 将根据第一篇的需求设计,逐步打造一款通用性可改造的工具,可以给同学们做一个实用性参考。
在下一篇,CLI 将会围绕工具类的模块进行开发。
所有的项目代码已经上传至项目地址,有兴趣的同学可以拉取参考,后续所有专栏的相关的代码都会统一放在 BOTY DESIGN 中。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!