当前位置: 首页 > 后端技术 > Node.js

搭建一个通用的脚手架

时间:2023-04-03 20:55:44 Node.js

2016年底,同事聊起脚手架。由于公司业务的多样性和前端的灵活性,我们不得不考虑更通用的脚手架。而不是随着前端技术的发展把时间花在配置上。于是chef-cli诞生了。2018年初,我们对过去一年的事情进行了整理和总结,对原有的脚手架项目-next-cli进行了重新增强,既满足了我们团队的需求,也满足了其他人的需求。project-next-cli的目标用户:公司业务复杂,但有一定积累的同学和爱折腾的团队)正在高速发展中,主要表现:备注:出现以下开发流程,请不要纠结出场顺序[捂脸]库/框架:jQuery,backbone,angular,react,vue模块化:commonjs,AMD(CMD),UMD,esmoduletaskmanager:npmscripts、grunt、gulp模块打包工具:r.js、webpack、rollup、browserifycss预处理器:Sass、Less、Stylus、Postcss静态检查器:flow/typescript测试工具:mocha、jasmine、jest、ava代码检测工具:eslint、jslint开发我们在实际开发的时候会遇到各种各样的业务需求(场景),根据需求和场景选择不同的技术栈。由于技术的进步和不同浏览器运行时的限制,我们不得不配置相应的环境等,导致我们无法满足业务需求。我画了一张图来展示业务、配置(环境)和技术之间的关系。前端配置工程师然后就清楚的看到了一个新的职业传播,前端配置工程师O(∩_∩)O~社区状态专用脚手架社区中有大量的特定框架,主要是针对某个目标任务定制的.比如下面的脚手架vue-clivue-cli,提供了使用vue开发webpack等模板,远程clone生成的pwa等文件。本文中的脚手架是指vue-cli的实现。dva-clidva-clidva开发的脚手架think-clithink-cli为thinkjs项目创建项目通用脚手架。详情请点这里查看yeoman添加生成器规则的开发初衷和目标。由于公司的形态,业务类型多样,前端技术开发的迭代,为了跟进社区发展,更好的完成以下目标而诞生。业务完成:专注、稳定、速度团队规范:代码规范、测试流程、发布流程沉淀:专人专职,持续稳定迭代更新,跟进时代好处:加班少,造轮子少,完成KPI,做更多有意义的事情准备依赖Github,根据GithubAPI实现,如下:获取项目curl-ihttps://api.github.com/orgs/project-scaffold/repos获取versioncurl-ihttps://api.github.com/repos/project-scaffold/cli/tags实现逻辑根据githubapi获取项目列表和版本号后,根据输入的名称,选择对应的版本即可下载到本地私有仓库,生成到执行目录。核心流程图如下:整体设计规范使用Node进行脚手架开发,版本选择>=6.0.0选择async/await开发,解决异步回调问题使用babel编译,使用ESLint规范代码功能,遵守单一职责原则,每个文件都是一个单独的模块来解决独立的问题。可以自由组合,实现复用。下面是最终的目录结构:├──LICENSE├──README.md├──bin│└──project├──package.json├──src│├──clear.js│├──config。js│├──helper││├──metalAsk.js││├──metalsimth.js││└──render.js│├──index.js│├──init.js│├──安装。js│├──list.js│├──project.js│├──search.js│├──uninstall.js│├──update.js│└──utils│├──betterRequire.js│├──check.js│├──copy.js│├──defs.js│├──git.js│├──loading.js│└──rc.js└──yarn.lock配置和主框架使用babel-preset-env保证版本兼容性{"presets":[["env",{"targets":{"node":"6.0.0"}}]]}使用eslint管理代码eslintdemo{"parserOptions":{"ecmaVersion":7,"sourceType":"module","ecmaFeatures":{"jsx":true}},"extends":"airbnb-base/legacy","rules":{"consistent-return":1,"prefer-destructuring":0,"no-mixed-spaces-and-tabs":0,"no-console":0,"no-tabs":0,"one-var":0,"no-unused-vars":2,"no-multi-spaces":2,"key-spacing":[2,{"beforeColon":false,"afterColon":true,"align":{"on":"colon"}}],"no-return-await":0},"env":{"node":true,"es6":true}}使用husky检测提交使用husky,定义git-hooks,规范git代码提交流程,这里只在package.json配置中做commit验证,如下:"husky":{"hooks":{"pre-commit":"npmrunlint"}}entry统一配置和入口,分发到不同的单个文件,执行输出核心代码functionregisterAction(command,type,typeMap){command.command(type).description(typeMap[type].desc).alias(typeMap[type].alias).action(async()=>{try{if(type==='help'){help();}elseif(type==='config'){awaitproject('config',...process.argv.slice(3));}else{awaitproject(type);}}catch(e){console.log(e);help();}});returncommand;}本地配置读写配置用于获取脚手架的基本设置,如注册表、类型等基本信息。useprojectconfigsetregistrykoajs#设置本地仓库下载源码projectconfiggetregistry#获取本地仓库设置的属性projectconfigdeleteregistry#删除本地设置逻辑判断本地设置文件存在的属性===>读写本地配置文件,格式为.ini。如果每一步数据为空/文件不存在,都会给出提示。核心代码switch(action){case'get':console.log(awaitrc(k));控制台日志('');返回真;case'set':awaitrc(k,v);返回真;case'remove':awaitrc(k,v,true);返回真;默认值:console.log(awaitrc());命令的执行逻辑。使用projectilogicGithubAPI下载===>获取项目列表===>选择项目===>获取项目版本号===>选择版本号===>下载到本地仓库获取项目列表https://api.github.com/orgs/p...获取标签列表如果每一步数据为空/文件不存在,会提示请求代码requestfunctionfetch(api){returnnewPromise((resolve,reject)=>{request({url:api,method:'GET',headers:{'User-Agent':`${ua}`}},(err,res,body)=>{如果(err){reject(err);return;}constdata=JSON.parse(body);if(data.message==='NotFound'){reject(newError(`${api}isnotfound`));}else{resolve(data);}});});}下载代码download-git-repoexportconstdownload=async(repo)=>{const{url,scaffold}=awaitgetGitInfo(repo);returnnewPromise((resolve,reject)=>{downloadGit(url,`${dirs.download}/${scaffold}`,(err)=>{if(err){reject(err);return;}resolve();});});};核心代码//获取github项目列表constrepos=awaitrepoList();choices=repos.map(({name})=>name);answers=awaitinquirer.prompt([{type:'list',name:'repo',message:'whichrepodoyouwanttoinstall?',choices}]);//选中的项目constrepo=answers.repo;//项目的版本号consttags=awaittagList(repo);如果(tags.length===0){version='';}else{选择=标签。地图(({名称})=>名称);答案=等待询问者。prompt([{type:'list',name:'version',message:'你想安装哪个版本?',choices}]);版本=答案.版本;}//下载awaitdownload([repo,version].join('@'));generateprojectusingprojectinit逻辑获取本地仓库列表===>选择一个本地项目===>输入基本信息===>编译生成临时文件===>复制重命名到目标目录if中间每一步数据为空/文件不存在如果生成的目录有重复,会有提示。核心代码//获取本地仓库物品constlist=awaitreaddir(dirs.download);//基本信息constanswers=awaitinquirer.prompt([{type:'list',name:'scaffold',message:'whichscaffolddoyouwanttoinit?',choices:list},{type:'input',name:'dir',message:'projectname',//必要的验证异步验证(输入){constdone=this.async();if(input.length===0){done('你必须输入项目名称');返回;}constdir=resolve(process.cwd(),输入);if(awaitexists(dir)){done('项目名已经存在,请改名');完成(空,真);}}]);constmetalsmith=awaitrc('metalsmith');if(metalsmith){consttmp=`${dirs.tmp}/${answers.scaffold}`;//拷贝一份到临时目录,编译生成awaitcopy(`${dirs.download}/${answers.scaffold}`,tmp);等待金属(answers.scaffold);等待复制(`${tmp}/${dirs.metalsmith}`,answers.dir);//删除临时目录awaitrmfr(tmp);}else{awaitcopy(`${dirs.download}/${answers.scaffold}`,答案.dir);}模板引擎编译实现核心代码如下://metalsmithlogicfunctionmetal(answers,tmpBuildDir){returnnewPromise((resolve,reject)=>{metalsmith.metadata(answers).source('./').destination(tmpBuildDir).clean(false).use(render()).build((err)=>{if(err){reject(err);return;}resolve(true);});});}//metalsmith渲染中间件实现functionrender(){returnfunction_render(files,metalsmith,next){constmeta=metalsmith.metadata();/*eslint禁用*/Object.keys(files).forEach(function(file){conststr=files[file].contents.toString();consolidate.swig.render(str,meta,(err,res)=>{if(err){returnnext(err);}files[file].contents=newBuffer(res);next();});})}}升级/降级版本使用项目更新逻辑获取本地仓库列表===>选择一个本地项目===>获取版本信息列表===>选择一个版本===>如果中间每一步数据都覆盖原版本文件如果为空/文件不存在,会给出提示,name:'scaffold',message:'whichscaffolddoyouwanttoupdate?',choices:list,asyncvalidate(input){constdone=this异步();if(input.length===0){done('你必须选择一个脚手架来更新版本,如果不更新,Ctrl+C');返回;完成(空,真);}}]);constrepo=answers.scaffold;//获取项目版本信息consttags=awaittagList(repo);如果(tags.length===0){version='';}else{choices=tags.map(({name})=>name);answers=awaitinquirer.prompt([{type:'list',name:'version',message:'你想安装哪个版本?',choices}]);版本=答案.版本;}//下载覆盖文件awaitdownload([repo,version].join('@'))search在远程github仓库中搜索项目列表使用项目搜索逻辑获取github项目列表===>输入搜索内容===>返回一个匹配列表如果中间每一步数据为空,给出提示核心代码constanswers=awaitinquirer.prompt([{type:'input',name:'search',message:'searchrepo'}]);如果(answers.search){letlist=awaitsearchList();list=list.filter(item=>item.name.indexOf(answers.search)>-1).map(({name})=>name);控制台日志('');if(list.length===0){console.log(`${answers.search}未找到`);}console.log(list.join('\n'));控制台日志('');}总结以上是这个通用脚手架的背景,对于用户和具体实现,这个脚手架还有一些可以优化的地方:不同的来源,不同的文件存储,支持离线功能。硬广:如果你觉得project-next-cli好用欢迎star和fork维护