为什么要搭建脚手架?我个人经常写一些demo,或者写一个新项目的时候,要么copy之前的项目模板,要么自己建一个,好像比较麻烦,比较浪费时间,只好搭建一个能满足我需求的脚手架了。脚手架的效果这是一个基本的脚手架,初始化一个项目,输入项目名称,版本号等信息,然后从git仓库中拷贝一个你需要的项目模板。类似于vue的vue-cli或者react的create-react-app,但是这个比较简单。基本思路参考下图。执行命令的入口文件是lib文件夹,里面放着工程的主要文件。package.json并没有太多说明这个项目中使用的主要包。Commander:命令行工具download-git-repo:用于下载远程模板ora:显示加载Animationchalk:修改控制台输出内容样式log-symbols:显示√或×等图标Inquirer.js:命令交互Metalsmith:Processprojecttemplatehandlebars:模板引擎使用命令行工具commander.js修改bin执行package.json入口,"bin":{"lz":"./bin/www"},"lz"命令可以选择自己,然后在bin文件中添加一个名为www的文件,#!/usr/bin/envnoderequire('../lib/index.js');在哪里#!/usr/bin/envnode是不可缺少的,这个主要指定当前脚本被node.js解析,在lib中创建一个index.js文件,constprogram=require('commander')program.version('1.0.0').usage('[项目名称]').command('init','创建新项目').parse(process.argv);为了方便测试,先链接全局环境npmlink,执行next命令,体验lzinithello正常应该会报错,错误栈很可能是真正的www-init文件。这是因为commander支持git-style的子命令处理,可以根据子命令自动引出以特定格式命名的命令执行文件。文件名的格式为[command]-[subcommand],例如:macawhello=>macaw-hellomacawinit=>macaw-init所以我们执行www文件的init,所以在bin中创建一个www-init文件,在lib中创建一个init.js文件www-init#!/usr/bin/envnoderequire('../lib/init.js');init.js完整代码constprogram=require('commander')constpath=require('path')constfs=require('fs')constglob=require('glob')//npmiglob-Dconstdownload=require('../lib/download.js')constinquirer=require('inquirer')constchalk=require('chalk')const生成器=require('../lib/generator')constlogSymbols=require("log-symbols");program.usage('')//根据输入,获取项目名称letprojectName=process.argv[2];if(!projectName){//project-name必填//是相当于执行命令的--help选项,并显示帮助信息。这是指挥官程序内置的命令选项。help()return}constlist=glob.sync('*')//遍历当前目录letnext=undefined;letrootName=path.basename(process.cwd());if(list.length){//如果当前目录不为空if(list.some(n=>{constfileName=path.resolve(process.cwd(),n);constisDir=fs.statSync(fileName).isDirectory();returnprojectName===n&&isDir})){console.log(`项目${projectName}已经存在`);返回;}根名称=项目名称;next=Promise.resolve(projectName);}elseif(rootName===projectName){rootName='.';next=inquirer.prompt([{name:'buildInCurrent',message:'当前目录为Empty,目录名与项目名相同,是否直接在当前目录下新建项目?',type:'confirm',default:true}]).then(answer=>{returnPromise.resolve(answer.buildInCurrent?'.':projectName)})}else{rootName=projectName;next=Promise.resolve(projectName)}next&&go()functiongo(){next.then(projectRoot=>{if(projectRoot!=='.'){fs.mkdirSync(projectRoot)}返回下载(projectRoot).then(target=>{return{name:projectRoot,root:projectRoot,downloadTemp:target}})}).then(context=>{returninquirer.prompt([{name:'projectName',message:'项目名称',默认:context.name},{name:'projectVersion',message:'Projectversionnumber',default:'1.0.0'},{name:'projectDescription',message:'Projectintroduction',default:`Aprojectnamed${context.name}`}]).then(answers=>{return{...context,metadata:{...answers}}})}).then(context=>{//删除临时文件夹并将文件移动到目标目录returngenerator(context);}).then(context=>{//成功显示为绿色,给予正面反馈console.log(logSymbols.success,chalk.green('Createdsuccessfully:)'))console.log(chalk.green('cd'+context.root+'\nnpminstall\nnpmrundev'))}).catch(err=>{//红色表示失败,增强提示console.log(err);console.error(logSymbols.error,chalk.red(`Failedtocreate:${err.message}`))})}init.js做了什么?首先获取init之后输入的参数作为项目名。当然是判断项目名是否存在,然后进行相应的逻辑操作。通过download-git-repo工具下载仓库的模板,然后通过inquirer.js处理命令行交互,获取输入的名称、版本号信息,最后根据这些信息处理模板文件使用download-git-repo下载模板文件,在lib下创建download.js文件constdownload=require('download-git-repo')constpath=require("path")constora=require('ora')模块。exports=function(target){target=path.join(target||'.','.download-temp');returnnewPromise(function(res,rej){//这里可以根据具体模板地址设置下载注意,如果是git,url后面的分支不能忽略leturl='github:ZoeLeee/BaseLearnCli#bash';constspinner=ora(`下载工程模板,源地址:${url}`)spinner.start();download(url,target,{clone:true},function(err){if(err){download(url,target,{clone:false},function(err){if(err){spinner.fail();rej(err)}else{//下载的模板存放在临时路径.下载完成后可以向下通知临时路径进行后续处理spinner.succeed()res(target)}})}else{//下载的模板存放在临时路径下下载完成后,你可以通知这个spinner.succeed()res(target)}})})}后续处理的临时路径这里要注意下载地址的url,以及url的格式。不是gitclone的地址有个clone:false参数。如果只是个人使用的话,可以这样,相当于执行了gitclone的操作。如果将其提供给其他人,可能会出现错误。如果使用false,即直接使用http协议下载该模板。具体可以参考官网Documentation.inquirer.js处理命令交互比较简单。可以看到这里有init.js将获取到的输入信息传递下去进行处理。Metalsmith然后根据获得的信息渲染模板。首先,在不影响原有模板运行的情况下,我们在git仓库上创建一个package_temp.json,对应我们要交互的变量名{"name":"{{projectName}}","version":"{{projectVersion}}","description":"{{projectDescription}}","main":"./src/index.js","scripts":{"dev":"webpack-dev-server--config./config/webpack.config.js","build":"webpack--config./config/webpack.config.js--modeproduction"},"author":"{{author}}","license":"ISC","devDependencies":{"@babel/core":"^7.3.3","@babel/preset-env":"^7.3.1","@babel/preset-react":"^7.0.0","babel-loader":"^8.0.5","clean-webpack-plugin":"^1.0.1","css-loader":"^2.1.0","html-webpack-plugin":"^3.2.0","style-loader":"^0.23.1","webpack":"^4.28.2","webpack-cli":"^3.1.2","webpack-dev-server":"^3.2.0"},"dependencies":{"react":"^16.8.2","react-dom":"^16.8.2"}}在lib下创建生成器。js文件处理模板constMetalsmith=require('metalsmith')constHandlebars=require('handlebars')constremove=require("../lib/remove")constfs=require("fs")constpath=require("path")module.exports=function(context){让元数据=context.metadata;让src=上下文。下载温度;让dest='./'+context.root;if(!src){returnPromise.reject(newError(`Invalidsource:${src}`))}returnnewPromise((resolve,reject)=>{constmetalsmith=Metalsmith(process.cwd())。metadata(metadata).clean(false).source(src).destination(dest);//判断下载的工程中是否有templates.ignoreconstignoreFiletemplate=path.resolve(process.cwd(),path.join(src,'templates.ignore'));constpackjsonTemp=path.resolve(process.cwd(),path.join(src,'package_temp.json'));让package_temp_content;如果(fs.existsSync(ignoreFile)){//定义一个metalsmith插件,用于删除模板中忽略的文件metalsmith.use((files,metalsmith,done)=>{constmeta=metalsmith.metadata()//先渲染忽略文件,然后剪切逐行忽略文件,得到忽略列表constignores=Handlebars.compile(fs.readFileSync(ignoreFile).toString())(meta).split('\n').map(s=>s.trim().replace(/\//g,"\\")).filter(item=>item.length);//删除被忽略的文件for(letignorePatternofignores){if(files.hasOwnProperty(ignorePattern)){deletefiles[ignorePattern];}}done()})}metalsmith.use((files,metalsmith,done)=>{constmeta=metalsmith.metadata();package_temp_content=Handlebars.compile(fs.readFileSync(packjsonTemp).toString())(meta);done();})metalsmith.use((files,metalsmith,done)=>{constmeta=metalsmith.metadata()Object.keys(files).forEach(fileName=>{constt=files[fileName].contents.toString()if(fileName==="package.json")files[fileName].contents=newBuffer(package_temp_content);elsefiles[fileName].contents=newBuffer(Handlebars.compile(t)(元));})done()}).build(err=>{remove(src);err?reject(err):resolve(context);})})}插值HandlebarsRender给我们的package_temp.json,然后用渲染文件的内容替换原始package.json的内容。有时我们也需要输入和选择某些文件不要下载,所以我们在模板仓库中添加一个文件,命名为templates.ignore,然后,类似处理package_temp.json,先渲染这个文件的内容,找出需要忽略的文件并删除它们。最后,删除临时文件夹并将文件移动到项目文件中。这个项目快完成了。添加删除文件夹的功能,createremove.jsconstfs=require("fs");constpath=require("path");functionremoveDir(dir){letfiles=fs.readdirSync(dir)for(vari=0;i