前言在实际开发过程中,从头搭建一个项目结构是件很头疼的事情,于是各种脚手架工具应运而生。笔者使用较多的是yoeman,express-generator和vue-cli就是其中之一。它们功能丰富,但核心功能是快速构建完整的项目结构。开发者只需要在生成的项目结构的基础上进行开发,非常简单高效。作为一个不摆弄死星的人,在熟悉了使用方法之后,他开始琢磨它们的原理。在仔细研究了文档和源码后,我终于摸清了它的核心原理,并基于这个原理搭建了一个名为SCION的脚手架。下面就让我们以SCION为例,从零开始搭建属于自己的脚手架工具吧!核心原理是yoeman需要提供yoeman-generator来构建项目。yoeman-generator本质上是一个具有完整文件结构的项目模板。用户需要手动将这些生成器下载到本地,然后yoeman会自动根据这些生成器生成各种项目。vue-cli提供了相当多的选项和设置功能,但其实质是将不同的模板从远程仓库拉取到本地,而不是一些“本地生成”的黑科技。从这个角度来看,思路是有的——先创建不同的示例项目,然后脚手架会根据用户的指令引用示例项目生成实际的项目。样板项目可以构建到脚手架中或部署到远程存储库中。为了更广泛的应用范围,SCION采用了第二种方式。技术选择node.js:整个脚手架工具的基础部分,推荐使用最新版本。es6:新版node.js对es6的支持非常高。使用es6可以大大提高开发效率和开发体验。Commander:TJ大神开发的一款工具,可以更好的组织和处理命令行输入。co:TJ大神开发的异步流程控制工具,写异步代码更舒服。co-prompt:还是TJ大神的杰作。。。传统的命令行只能一次性在一行输入所有的参数和选项。使用该工具可以自动提供提示信息并逐步接收用户输入。体验类似于npminit一步一步输入参数的过程。整体结构为国际惯例。在开始开发之前,首先要了解整体结构。看图:先了解模板的概念。模板是项目的模板,包含项目的完整结构和信息。模板信息存储在名为templates.json的文件中。用户可以通过命令行添加、删除和列出templates.json。通过选择不同的模板,SCION会自动从远程仓库中拉取对应的模板到本地,完成项目的搭建。整个脚手架最终的文件结构如下:==================|__bin|__scion|__command|__add.js|__delete.js|__init.js|__list.js|__node_modules|__package.json|__templates.json入口文件首先创建项目,在package.json中写入依赖,执行npminstall:"dependencies":{"chalk":"^1.1.3","co":"^4.6。0","co-prompt":"^1.0.0","commander":"^2.9.0"}在根目录下创建一个\bin文件夹,在里面创建一个没有后缀的scion文件。这个bin\scion文件是整个脚手架的入口文件,所以我们先写。首先是一些初始化代码:#!/usr/bin/envnode--harmony'usestrict'//定义脚手架进程的文件路径.env.NODE_PATH=__dirname+'/../node_modules/'constprogram=require('commander')//定义当前版本program.version(require('../package').version)//定义使用方法program.usage('')从前面的架构图可以看出,脚手架支持用户输入4种不同的命令。现在来写处理这4个命令的方法:program.command('add').description('Addanewtemplate').alias('a').action(()=>{require('../command/add')()})program.command('list').description('Listallthetemplates').alias('l').action(()=>{require('../command/list')()})program.command('init').description('Generateanewproject').alias('i').action(()=>{require('../command/init')()})program.command('delete').description('Deletatetemplate').alias('d').action(()=>{require('../command/delete')()})commander的具体用法在这里如果你不展开,可以直接上官网看详细文档。***不要忘记处理参数并提供帮助信息:program.parse(process.argv)if(!program.args.length){program.help()}完整代码请看这里。使用node运行这个文件,看到输出如下,证明入口文件已经写入。Usage:scionCommands:add|aAddanewtemplatelist|lListallthetemplatesinit|iGenerateanewprojectdelete|dDeleteatetemplateOptions:-h,--helpoutputusageinformation-V,--versionoutputtheversionnumber处理用户输入在项目根目录下创建一个\command文件夹,专门用来存放命令处理文档。在根目录下创建一个templates.json文件,写入如下内容存放模板信息:{"tpl":{}}将模板添加到\command中,新建一个add.js文件:'usestrict'constco=require('co')constprompt=require('co-prompt')constconfig=require('../templates')constchalk=require('chalk')constfs=require('fs')module.exports=()=>{co(function*(){//逐步接收用户输入的参数lettplName=yieldprompt('Templatename:')letgitUrl=yieldprompt('Githttpslink:')letbranch=yieldprompt('Branch:')//避免添加if(!config.tpl[tplName]){config.tpl[tplName]={}config.tpl[tplName]['url']=gitUrl.replace(/[\u0000-\u0019]/g,'')//过滤器unicodeCharacterconfig.tpl[tplName]['branch']=branch}else{console.log(chalk.red('Templatehasalreadyexisted!'))process.exit()}//将模板信息写入templates.jsonfs.writeFile(__dirname+'/../templates.json',JSON.stringify(config),'utf-8',(err)=>{if(err)console.log(err)console.log(chalk.green('Newtemplateadded!\n'))康索le.log(chalk.grey('Thelasttemplatelistis:\n'))console.log(config)console.log('\n')process.exit()})})}deletetemplate同上是的,创建删除。\command文件夹下的js文件:'usestrict'constco=require('co')constprompt=require('co-prompt')constconfig=require('../templates')constchalk=require('chalk')constfs=require('fs')模块.exports=()=>{co(function*(){//接收用户输入的参数lettplName=yieldprompt('Templatename:')//删除对应的模板if(config.tpl[tplName]){config.tpl[tplName]=undefined}else{console.log(chalk.red('Templatedoesnotexist!'))process.exit()}//写入template.jsonfs.writeFile(__dirname+'/../templates.json',JSON.stringify(config),'utf-8',(err)=>{if(err)console.log(err)console.log(chalk.green('Templatedeleted!'))console.log(chalk.grey('最后一个templatelist是:\n'))console.log(config)console.log('\n')process.exit()})})}ListtemplatesCreatelist.jsfile:'usestrict'constconfig=require('../templates')module.exports=()=>{console.log(config.tpl)process.exit()}构建项目现在来到我们最重要的部分——构建项目同样,在\command目录中创建一个名为init.js的新文件:'usestrict'constexec=require('child_process').execconstco=require('co')constprompt=require('co-prompt')constconfig=require('../templates')constchalk=require('chalk')module.exports=()=>{co(function*(){//处理用户输入lettplName=yieldprompt('Templatename:')letprojectName=yieldprompt('项目名称:')letgitUrlletbranchif(!config.tpl[tplName]){console.log(chalk.red('\n×Templatedoesnotexit!'))process.exit()}gitUrl=config.tpl[tplName].urlbranch=config.tpl[tplName].branch//git命令,远程拉取项目并自定义项目名white('\nStartgenerating...'))exec(cmdStr,(error,stdout,stderr)=>{if(error){console.log(error)process.exit()}console.log(chalk.green('\n√生成完成!'))console.log(`\ncd${projectName}&&npminstall\n`)process.exit()})})}可以看到这部分代码也是very很简单,关键语句是letcmdStr=`gitclone${gitUrl}${projectName}&&cd${projectName}&&gitcheckout${branch}`它的作用是从远程仓库克隆到自定义目录,并切换到对应的分支,熟悉git命令的同学应该明白,不熟悉git命令的同学该明白了补课吧!全局使用为了全局使用,我们需要在package.json中设置:"bin":{"scion":"bin/scion"},本地调试时,在根目录下执行npmlink绑定scion命令toGlobally,以后可以直接用scion作为命令启动,不用敲nodescion之类的长命令。现在我们的脚手架工具已经搭好了,下面我们一起来试试吧!使用测试添加|a添加模板命令init|我生成项目命令删除|d删除模板命令和列表|llisttemplate命令,大功告成!现在我们整个脚手架工具已经搭建完成。以后只需要知道模板的githttps地址和分支就可以持续添加到SCION中了。团队协作,只需要共享SCION的templates.json文件即可。后记看起来并不复杂,但真正从头开始构建它却花了很多心思。最大的问题是一开始并不知道如何像npminit一样一步步处理用户输入。我只知道如何用命令行带上所有参数。这种用户体验真的很糟糕。研究了vue-cli和yoeman也没找到对应的代码,只好不断google,***终于找到一篇文章,可以用co和co-prompt这两个工具实现,膜拜一下万能的TJ又是大神,也希望有朋友能告诉我vue-cli是怎么实现的。这种脚手架只有最基本的功能,远远达不到市场同类产品的高度。以后慢慢补上。总之,在完成SCION的过程中,我真的学到了很多东西。谢谢阅读。