注册

前端工程化实战 - 可配置的模板管理

功能设计


如果每一次新脚手架的开发或者模板的更新都需要重新更新一次 CLI 的话,虽然成本不高,但是开发模板的同学需要通知 CLI 开发的同学去升级,使用模板的同学又需要在等 CLI 开发完毕才能使用,中间交流沟通的成本就增加了。


其次,对于业务开发同学来说,可能只需要一类或者几类的模板,那么如果 CLI 是一个大而全的模板集合,对这些同学来说,快速选择模板创建项目反而也是一个负担,因为要在很多模板中选择自己想要的也是很花费时间。


所以我们的目的是设计一款拥有自定义配置与可升级模板功能的 CLI 工具。


未命名文件.png


既然是自定义配置,那么就需要用户可以在本地手动添加、删除、更新自己常用的模板信息,同时需要可以动态的拉取这些模板而不是一直下载下来就是本地的旧版本。


根据需求,可简单设计一下我们 CLI 的模板功能概要:



  1. 需要保存模板来源的地址
  2. 根据用户的选择拉取不同的模板代码
  3. 将模板保存在本地

实战开发


那么根据上面的设计思路,我们可以一步步开发所需要的功能


本地保存模板地址功能


第一步,如果需要将模板的一些信息保存在本地的话,我们需要一个对话型的交互,引导用户输入我们需要的信息,所以可以选择 inquirer 这个工具库。



Inquirerjs 是一个用来实现命令行交互式界面的工具集合。它帮助我们实现与用户的交互式交流,比如给用户提一个问题,用户给我们一个答案,我们根据用户的答案来做一些事情,典型应用如 plop等生成器工具。



一般拉取代码的话,我们需要知道用户输入的模板地址(通过 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,
}

这里我们需要对是否保存还是更新模板做一个简单的流程判断:



  1. 判断当前是否存在 tpl 的缓存文件,如果已存在缓存文件,那么需要跟当前的模板信息合并,如果不存在的话则需要创建文件,将获取的信息保存进去。
  2. 如果当前已存在缓存文件,需要根据 name 判断是已经被缓存了,如果被缓存了的话,则根据 name 来更新对应的模板信息。

接下来,我们来演示一下,使用的效果。


根据之前的操作,构建完 CLI 之后,运行 fe-cil add tpl 可以得到如下的结果:


image.png


那么在对应的路径可以看到已经将这条模板信息缓存下来了。


image.png


如上,我们已经完成一个简单的本地对模板信息添加与修改功能,同样删除也是类似的操作,根据自己的实际需求开发即可。


下载模板


在保存了模板之后,我们需要选择对应的模板下载了。


下载可以使用 download-git-repo 作为 CLI 下载的插件,这是一款非常好用的插件,支持无 clone 去下载对应的模板,非常适合我们的项目。



download-git-repo 是一款下载 git repository 的工具库,它提供了简写与 direct:url 直接下载两种方式,同时也提供直接下载代码与 git clone 的功能,非常使用与方便。



同样在下载模板的时候,我们需要给用户展示当前的保存好的模板列表,这里同样需要使用到 inquirer 工具。



  1. 使用 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)
})
}


  1. 使用 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 模板的时候,重新组装下载链接即可。


image.png


image.png


如上图所示,我们可以将公共的模板下载到本地,方便同学正常开发了,但是此时还有一个问题,那就是上面的分支是 main 分支,不是每一个模板都有这个分支,可控性太差,那么我们怎么拿到项目所有的分支来选择性下载呢。


Github Api


在 Github 中对于开源、不是私有的项目,可以省去授权 token 的步骤,直接使用 Github Api 获取到对应的信息。


所以针对上面提到问题,我们可以借助 Github Api 提供的能力来解决。


获取分支的链接是 https://api.github.com/repos/boty-design/react-tpl/branches,在开发之前我们可以使用 PostMan 来测试一下是否正常返回我们需要的结果。


image.png


如上可以看到,已经能通过 Github Api 拿到我们想要的分支信息了。



如果出现了下述错误的话,没关系,只是 github 限制访问的频率罢了



image.png


针对上述的问题,我们需要的是控制频率、使用带条件的请求或者使用 token 请求 Github Api 的方式来规避,但是鉴于模板来说,一般请求频率也不会很高,只是我在开发的时候需要不断的请求来测试,才会出现这种问题,各位同学有兴趣的话可以自己试试其他的解决方案。


分支代码优化


未命名文件 (1).png


在预研完 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 问题,通过获取的分支数量来判断是否出现选择分支这个选项,提高用户体验。



链接:https://juejin.cn/post/6999397309180182564

0 个评论

要回复文章请先登录注册