前言为什么要写这篇文章?因为我一直在研究Strve。对很多事情。所以本文将为大家介绍一个更加方便灵活的命令行脚手架工具,以及如何在npm上发布。之前写过类似的开发命令行工具的文章,但核心思想是通过代码远程拉取Git仓库中的项目模板代码。有时候因为网速拉取失败,然后项目初始化失败。那么,还有比这更好的解决方案吗?那么这篇文章就到这里了。最近,很多项目都是使用Vite工具开发的。不得不佩服游老师惊人的编码能力,打造了这么好的开发工具,开发体验非常流畅。尤其是刚初始化项目的时候,只需要执行一行命令,不需要全局安装任何工具。然后,自定义并选择需要的模板来初始化项目,就大功告成了!这样的操作着实让我惊艳了一把!我在想,如果我把create-vite的思想应用到自己的脚手架工具上,是不是很好?好的!实战所以,废话少说,赶快打开ViteGitHub地址吧。https://github.com/vitejs找了半天终于找到了命令行工具的核心代码。https://github.com/vitejs/vite/tree/main/packages/create-vite你看到的是很多以template-开头的文件夹。打开几个看看。它们都是框架项目模板。好吧,你可以暂时把它放在一边。接下来,让我们打开index.js文件,看看里面有什么。我把代码列一下,大家可以简单看一下,不需要深究。#!/usr/bin/envnode//@ts-checkconstfs=require('fs')constpath=require('path')//通过定义thattheargs避免自动转换项目名称的编号//与选项无关(_)需要解析为字符串。参见#4606constargv=require('minimist')(process.argv.slice(2),{string:['_']})//eslint-disable-next-linenode/no-restricted-requireconstprompts=require('prompts')const{yellow,green,cyan,蓝色、洋红色、浅红色、红色}=要求('kolorist')constcwd=process.cwd()constFRAMEWORKS=[{name:'vanilla',color:yellow,variants:[{name:'vanilla',display:'JavaScript',color:yellow},{name:'vanilla-ts',display:'TypeScript',color:blue}]},{name:'vue',color:green,variants:[{name:'vue',display:'JavaScript',color:yellow},{name:'vue-ts',display:'TypeScript',color:blue}]},{name:'react',color:cyan,variants:[{name:'react',display:'JavaScript',color:yellow},{name:'react-ts',display:'TypeScript',color:blue}]},{name:'preact',color:magenta,variants:[{name:'preact',display:'JavaScript',color:yellow},{name:'preact-ts',display:'TypeScript',color:blue}]},{name:'lit',color:lightRed,variants:[{name:'lit',display:'JavaScript',color:yellow},{name:'lit-ts',display:'TypeScript',color:blue}]},{name:'svelte',color:red,variants:[{name:'svelte',display:'JavaScript',color:yellow},{name:'svelte-ts',display:'TypeScript',color:blue}]}]constTEMPLATES=FRAMEWORKS.map((f)=>(f.variants&&f.variants.map((v)=>v.name))||[f.name]).reduce((a,b)=>a.concat(b),[])constrenameFiles={_gitignore:'.gitignore'}asyncfunctioninit(){lettargetDir=argv._[0]lettemplate=argv.template||argv.tconstdefaultProjectName=!targetDir?'vite-project':targetDirletresult={}try{result=awaitprompts([{type:targetDir?null:'text',name:'projectName',message:'Projectname:',initial:defaultProjectName,onState:(state)=>(targetDir=state.value.trim()||defaultProjectName)},{type:()=>!fs.existsSync(targetDir)||isEmpty(targetDir)?null:'confirm',name:'overwrite',message:()=>(targetDir==='.'?'Currentdirectory':`Targetdirectory"${targetDir}"`)+`isnotempty.Removeexistingfilesandcontinue?`},{type:(_,{overwrite}={})=>{if(overwrite===false){thrownewError(red('?')+'Operationcancelled')}returnnull},name:'overwriteChecker'},{type:()=>(isValidPackageName(targetDir)?null:'text'),name:'packageName',message:'Packagename:',initial:()=>toValidPackageName(targetDir),validate:(dir)=>isValidPackageName(dir)||'Invalidpackage.jsonname'},{type:template&&TEMPLATES.includes(template)?null:'select',name:'framework',message:typeoftemplate==='string'&&!TEMPLATES.includes(template)?`"${template}"is'tavalidtemplate.Pleasechoosefrombelow:`:'Selectaframework:',initial:0,choices:FRAMEWORKS.map((framework)=>{constframeworkColor=framework.colorreturn{title:frameworkColor(framework.name),value:framework}})},{type:(framework)=>framework&&framework.variants?'select':null,name:'variant',message:'Selectavariant:',//@ts-ignorechoices:(framework)=>framework.variants.map((variant)=>{constvariantColor=variant.colorreturn{title:variantColor(variant.name),value:variant.name}})}],{onCancel:()=>{thrownewError(red('?')+'Operationcancelled')}})}catch(cancelled){console.log(cancelled.message)return}//userchoiceassociatedwithpromptsconst{framework,overwrite,packageName,variant}=resultconstroot=path.join(cwd,targetDir)if(overwrite){emptyDir(root)}elseif(!fs.existsSync(root)){fs.mkdirSync(root)}//determintemplatetemplate=variant||framework||templateconsole.log(`\nScaffoldingprojectin${root}...`)consttemplateDir=path.join(__dirname,`template-${template}`)constwrite=(file,content)=>{consttargetPath=renameFiles[file]?path.join(root,renameFiles[file]):path.join(root,file)if(content){fs.writeFileSync(targetPath,content)}else{copy(path.join(templateDir,file),targetPath)}}constfiles=fs.readdirSync(templateDir)for(constfileofffiles.filter((f)=>f!=='package.json')){write(file)}constpkg=require(path.join(templateDir,`package.json`))pkg.name=packageName||targetDirwrite('package.json',JSON.stringify(pkg,null,2))constpkgInfo=pkgFromUserAgent(process.env.npm_config_user_agent)constpkgManager=pkgInfo?pkgInfo.name:'npm'console.log(`\nDone.Nowrun:\n`)if(root!==cwd){console.log(`cd${path.relative(cwd,root)}`)}switch(pkgManager){case'yarn':console.log('yarn')console.log('yarndev')breakdefault:console.log(`${pkgManager}install`)console.log(`${pkgManager}rundev`)break}console.log()}functioncopy(src,dest){conststat=fs.statSync(src)if(stat.isDirectory()){copyDir(src,dest)}else{fs.copyFileSync(src,dest)}}functionisValidPackageName(projectName){return/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName)}functiontoValidPackageName(projectName){returnprojectName.trim().toLowerCase().replace(/\s+/g,'-').replace(/^[._]/,'').replace(/[^a-z0-9-~]+/g,'-')}functioncopyDir(src目录,destDir){fs.mkdirSync(destDir,{recursive:true})for(constfileoffs.readdirSync(srcDir)){constsrcFile=path.resolve(srcDir,file)constdestFile=path.resolve(destDir,file)copy(srcFile,destFile)}}functionisEmpty(path){returnfs.readdirSync(path).length===0}functionemptyDir(dir){if(!fs.existsSync(dir)){return}for(constfileoffs.readdirSync(dir)){constabs=path.resolve(dir,file)//baselineisNode12socan'tusermSync:(if(fs.lstatSync(abs).isDirectory()){emptyDir(abs)fs.rmdirSync(abs)}else{fs.unlinkSync(abs)}}}/***@param{string|undefined}userAgentprocess.env.npm_config_user_agent*@returnsobject|undefined*/functionpkgFromUserAgent(userAgent){if(!userAgent)returnundefinedconstpkgSpec=userAgent.split('')[0]constpkgSpecArr=pkgSpec.split('/')return{name:pkgSpecArr[0],version:pkgSpecArr[1]}}init().catch((e)=>{console.error(e)})见上想看更多代码?别慌!我们其实只用到其中的几个地方,大家可以放心继续往下看。这些代码都是Cr的核心代码吃维特。我们将看到常量FRAMEWORKS定义了一个数组对象。另外,数组对象是我们在初始化项目的时候需要选择安装的一些框架。因此,我们可以先cloneViteGithub项目,试试效果。然后,项目clone之后,我们找到/packages/create-vite文件夹,我们现在只关注这个文件夹。我用的是Yarn依赖管理工具,所以先用命令初始化依赖。yarn然后,我们可以先打开根目录下的package.json文件,我们会发现下面的命令。{"bin":{"create-vite":"index.js","cva":"index.js"}}我们可以在这里命名自己的模板,比如我们叫它demo,{"bin":{"create-demo":"index.js","cvd":"index.js"}}然后,我们这里先使用yarnlink命令在本地运行这个命令。然后运行创建演示命令。会显示一些交互的文字,会很眼熟,这是我们创建Vite项目时看到的。前面我们说要实现一个自己的项目模板,现在已经找到了核心。让我们开始吧!我们会看到根目录下有很多template-开头的文件夹,我们打开一个看看。比如模板-vue。原来模板都在这里!但是这些模板文件都是以template-开头的,有约定吗?因此,我们打算回头看看index.js文件。//determinetemplatetemplate=variant||framework||templateconsole.log(`\nScaffoldingprojectin${root}...`)consttemplateDir=path.join(__dirname,`template-${template}`)为真,所以所有模板必须从模板开始。然后,我们在根目录下新建一个template-demo文件夹,在里面放一个index.js文件作为示例模板。我们在执行初始化工程的时候,发现需要选择对应的模板,那么这些选项是从哪里来的呢?我们决定回头看看根目录下的index.js文件。你会发现有这样一个数组,就是我们要选择的框架模板。constFRAMEWORKS=[{name:'vanilla',color:yellow,variants:[{name:'vanilla',display:'JavaScript',color:yellow},{name:'vanilla-ts',display:'TypeScript',color:blue}]},{name:'vue',color:green,variants:[{name:'vue',display:'JavaScript',color:yellow},{name:'vue-ts',display:'TypeScript',color:blue}]},{name:'react',color:cyan,variants:[{name:'react',display:'JavaScript',color:yellow},{name:'react-ts',display:'TypeScript',color:blue}]},{name:'preact',color:magenta,variants:[{name:'preact',display:'JavaScript',color:yellow},{name:'preact-ts',display:'TypeScript',color:blue}]},{name:'lit',color:lightRed,variants:[{name:'lit',display:'JavaScript',color:yellow},{name:'lit-ts',display:'TypeScript',color:blue}]},{name:'svelte',color:red,variants:[{name:'svelte',display:'JavaScript',color:yellow},{name:'svelte-ts',display:'TypeScript',color:blue}]}]因此,可以在数组后面再添加一个对象。{name:'demo',color:red,variants:[{name:'demo',display:'JavaScript',color:yellow}]}好了,你会发现我这里会有一个color属性,有类似colorsValue属性值,是kolorist导出的常量。kolorist是一个将颜色输入标准输入/标准输出的小型库。我们之前在那些模板交互文本中看到了不同颜色的它们,这就是它的作用。const{yellow,green,cyan,blue,magenta,lightRed,red}=require('kolorist')我们也已经把模板对象添加到数组中了,接下来我们执行命令看看效果。你会发现多了一个demo模板,这正是我们想要的。让我们继续吧。我们会看到根目录下已经成功创建了demo1文件夹,里面有我们想要的demo模板。上图报错是因为我没有在demo模板上创建package.json文件,所以这里可以忽略。您可以在模板中创建一个package.json文件。虽然我们在本地成功创建了一个自己的模板,但是只能在本地创建。也就是说,如果换了电脑,就没有办法执行创建模板的命令了。所以,我们得想办法发布到云端,这里我们发布到npm。首先我们新建一个工程目录,删除其他模板,只保留自己的模板。另外,删除数组中的其他模板对象,保留一个自己的模板。我以我自己的模板create-strve-app为例。然后,我们打开package.json文件,需要修改一些信息。以create-strve-app为例:{"name":"create-strve-app","version":"1.3.3","license":"MIT","author":"maomincoding","bin":{"create-strve-app":"index.js","cs-app":"index.js"},"files":["index.js","template-*"],"main":"index.js","private":false,"keywords":["strve","strvejs","dom","mvvm","virtualdom","html","template","string","create-strve","create-strve-app"],"engines":{"node":">=12.0.0"},"repository":{"type":"git","url":"git+https://github.com/maomincoding/create-strve-app.git"},"bugs":{"url":"https://github.com/maomincoding/create-strve-app/问题"},"主页":"https://github.com/maomincoding/create-strve-app#readme","dependencies":{"kolorist":"^1.5.0","minimist":"^1.2.5","prompts":"^2.4.2"}}注意,每次发布前,version字段必须与上一次不同,否则发布失败。最后我们依次运行以下命令。切换到npmsourcenpmconfigsetregistry=https://registry.npmjs.org登录npm(如果已经登录过可以忽略这一步)npmlogin发布npmnpmpublish我们可以登录npm(https://www.npmjs.com/)查看是否发布成功!之后我们可以直接运行命令下载自定义模板,这个在我们复用模板的时候非常有用,既可以提高效率,也可以避免犯很多不必要的错误。结论另外,本文中的CreateStrveApp示例是一套快速构建Strve.js项目的命令行工具,如果你对此感兴趣,可以访问如下源码查看地址:https://github.com/maomincoding/create-strve-app熬夜两个多月,Strve.js生态已经初步搭建完成。以下为欢迎访问Strve.js最新文档地址。https://maomincoding.github.io/strvejs-doc/
