前言

目前前端发展蒸蒸日上,工程化也越来越成熟。在这期间出现了很多优秀的框架和工具。与此同时伴随着与框架搭配使用的脚手架也呼之欲出。前端脚手架工具发展的日益强大,比如 vue-cli, create-react-app等等是在 vue, react开发搭建项目常用的脚手架。小编在看了 vue-cli3, vue-cli2的脚手架实现之后,心血来潮自己实现一个简易版的脚手架,下边我们一起来学习一下脚手架的实现流程。

实现思路

我认为 vue-cli3和 vue-cli2的实现区别有如下几点

  • 就是 vue-cli3不在从 git仓库下载模板,而是自己生成代码和创建文件和文件夹。
  • vue-cli3把 webpack的配置内置了,不在暴露出来,提供用户自定义的配置文件来自定义自己的配置;而 vue-cli2则是把配置完全暴露出来,可以任意修改。

本文我们这里是基于 vue-cli2的实现思路来一步步实现一个简单的 react版本脚手架。下边是小编整体的实现过程1、添加自己脚手架的命令(lbs)2、使用 commander工具为自己的 lbs命令添加解析参数,解析参数,添加自定义命令;附上官方文档 [commander文档](https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md) 3、使用 inquirer实现命令行和用户的交互(用户输入,选择);附上官方文档 [inquirer文档](https://www.npmjs.com/package/inquirer) 4、根据用户输入的项目名称,模板来下载,解压模板 5、修改模板里边的文件(package.json,index.html等) 6、为项目安装依赖,结束

开始撸代码

本文实现一个 lbs init[projectName]--force命令projectName: 输入的项目名称 --force: 定义的选项(当前目录存在输入的 [projectName]文件夹时候,是否强制覆盖)

添加脚手架命令(lbs)

创建项目这一步省略 利用 package.json的 bin项来指定自己定义的命令对应的可执行文件的位置,我们在 package.json,添加如下代码

"bin":{  "lbs": "./bin/lbs.js"}

然后创建 bin/lbs.js文件,添加测试代码:

#!/usr/bin/env nodeconsole.log("hello lbs-cli")

第一行是必须添加的,是指定这里用node解析这个脚本。默认找 /usr/bin目录下,如果找不到去系统环境变量查找。然后我们在任意目录下打开 cmd窗口,输入 lbs命令,你会发现找不到命令。其实我们还需要做一步操作,就是把本地项目全局安装一下,在当前项目下执行 npm install.-g,然后在 cmd下执行 lbs命令,你会发现会输出我们打印的字符串。到这里我们已经成功在系统里添加了自己定义的 lbs命令,那么我们怎么为lbs添加init,create,--version等等参数呢?

使用commander丰富我们的lbs命令

不熟悉commander的使用请看[commander文档](https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md)我们首先要安装一下插件,然后初步尝试一下为我们的 lbs命令添加版本查看的选项

const { program } = require("commander")const pkg = require("./../package.json")program.version(pkg.version,'-v --version')program.parse(process.argv)

此时我们在任意命令行执行 lbs-v或者 lbs--version,可以看到在控制台输出版本信息接下来为 lbs命令添加一个命令:

// projectName 是一个可选参数program.command('init [projectName]').description("初始化项目")   // 添加一个选项     .option('-f --force','如果存在输入的项目目录,强制删除项目目录') .action((projectName,cmd)=>{      // projectName 是我们输入的参数,     console.log(projectName)      // cmd是Command对象     console.log(cmd.force)  })

这里我们添加了一个init命令,支持一个可选参数和一个-f的可选选项 这时候我们执行一下

lbs init test -f

可以在控制台查看到我们输入的 test 和 cmd对象。可以在cmd中查找到存在force属性。如果执行 lbs init,输出如下如果执行 lbs init test,输出如下这里我们主要是获取这两个数据,如果你的命令还有其它的复杂功能,还可以扩展其它参数和选项。这里只是 command的一种使用方式,当我们为 command添加第二个描述参数,就意味着使用独立的可执行文件作为子命令,比如你的命令是 init那么你就需要创建一个 lbs-init脚本文件,这个文件负责执行你指定的命令,按照 lbs-${command}的方式创建脚本,我们创建 lbs-init.js文件把命令修改如下,为 command方法添加第二个参数

// projectName 是一个可选参数program.command('init [projectName]','init project')  .description("初始化项目")        // 添加一个选项.option('-f --force','如果存在输入的项目目录,强制删除项目目录') .action((projectName,cmd)=>{     // projectName 是我们输入的参数,    console.log(projectName)     // cmd是Command对象    console.log(cmd.force)  })

执行 lbs init,你会发现什么也没输出。因为这里不会执行到action方法,会去执行我们创建的 lbs-init.js这个空文件。所以什么也不会输出。这时候 lbs.js只需要定义 init命令就可以了。只需要这一行就足够了 program.command('init [projectName]','init project')然后在 lbs-init.js添加解析代码

const { program } = require("commander")let projectName;let force;// 指定解析的参数program.arguments('[projectName]') .description("初始化项目")  .option('-f --force','如果存在输入的项目目录,强制删除项目目录') .action((name,cmd)=>{     projectName = name;    force = cmd.force;});program.parse(process.argv);console.log(projectName,force)

重新执行 lbs init test-f发现数据都能获取。到这里我们已经可以为我们的 lbs init命令自定义参数和选项了,那么当用户只执行 lbs init命令,这时候我们就获取不到项目名称,我们怎么办呢?请往下看

使用inquirer实现命令行和用户的交互(用户输入,选择,问答)

这里我们需要安装 chalk, inquirer插件 chalk:主要是自定义颜色控制台输出创建一个logger.js工具类,主要是输出控制台信息

const chalk = require('chalk');exports.warn = function(message){    console.log(chalk.yellow(message));}exports.error = function(message){    console.log(chalk.red(message))}exports.info = function(message){    console.log(chalk.white(message))}exports.infoGreen = function(message){    console.log(chalk.green(message))}exports.exit = function(error){    if(error && error instanceof Error){        console.log(chalk.red(error.message))    }    process.exit(-1);}

这个库是我们可以和用户交互的工具;第一个问题是输入项目名称,第二个问题是让用户选择一个模板,这里的模板需要在github上准备好,我这里只准备了一个[lb-react-apps-template](https://github.com/liuboshuo/lb-react-apps-template),这个模板是基于[react-apps-template](https://github.com/liuboshuo/react-apps-template)这个项目重新建了一个git仓库。这个模板的具体实现可以可以看之前webpack的系列文章:[react+webpack4搭建前端项目](https://www.jianshu.com/p/04e436cf75ba),后边两个模板是不存在的

// 设置用户交互的问题const questions = [    {        type: 'input',        name:'projectName',        message: chalk.yellow("输入你的项目名字:")    },    {        type:'list',        name:'template',        message: chalk.yellow("请选择创建项目模板:"),        choices:[            {                name:"lb-react-apps-template",                value:"lb-react-apps-template"            },            {name:"template2",value:"tempalte2"},            {name:"template3",value:"tempalte3"}        ]    }];// 如果用户命令参数带projectName,只需要询问用户选择模板if(projectName){    questions.splice(0,1);}// 执行用户交互命令inquirer.prompt(questions).then(result=>{    if(result.projectName) {        projectName = result.projectName;    }    const templateName = result.template;    // 获取projectName templateName    console.log("项目名称:" + projectName)    console.log("模板名称:" + templateName)    if(!templateName || !projectName){        // 退出        logger.exit();    }    // 往下走    checkProjectExits(projectName,templateName); // 检查目录是否存在}).catch(error=>{    logger.exit(error);})

这里的 checkProjectExits下边会实现,可以先忽略。这时候我们执行 lbs init,可以看到成功获取到 projectName和 templateName接下来我们还需要判断用户输入的项目名称在当前目录是不是存在,在存在的情况下 1、如果用户执行的命令包含 --force,那么直接把存在的目录删除, 2、如果命令不包含 --force,那么需要询问用户是否需要覆盖。如果用户需要覆盖,那就直接删除存在的文件夹,不过用户不允许,那就直接退出添加 checkProjectExits检查目录存在的方法,代码如下

function checkProjectExits(projectName,templateName){    const currentPath = process.cwd();    // 获取项目的真实路径    const filePath = path.join(currentPath,`${projectName}`);     if(force){ // 强制删除        if(fs.existsSync(filePath)){            // 删除文件夹            spinner.logWithSpinner(`删除${projectName}...`)            deletePath(filePath)            spinner.stopSpinner(false);        }        // 开始下载模板        startDownloadTemplate(projectName, templateName)         return;    }    // 判断文件是否存在 询问是否继续    if(fs.existsSync(filePath)){         inquirer.prompt( {            type: 'confirm',            name: 'out',            message: `${projectName}文件夹已存在,是否覆盖?`        }).then(data=>{            // 用户不同意            if(!data.out){                 exit();            }else{                // 删除文件夹                spinner.logWithSpinner(`删除${projectName}...`)                deletePath(filePath)                spinner.stopSpinner(false);                // 开始下载模板                startDownloadTemplate(projectName, templateName)             }        }).catch(error=>{            exit(error);        })    }else{        // 开始下载模板        startDownloadTemplate(projectName, templateName)     }}function startDownloadTemplate(projectName,templateName){    console.log(projectName,templateName)}

我们这里用到了一个 spinner的工具类,新建 lib/spinner.js,主要是一个转菊花的动画提示,代码如下

const ora = require('ora')const chalk = require('chalk')const spinner = ora()let lastMsg = nullexports.logWithSpinner = (symbol, msg) => {  if (!msg) {    msg = symbol    symbol = chalk.green('✔')  }  if (lastMsg) {    spinner.stopAndPersist({      symbol: lastMsg.symbol,      text: lastMsg.text    })  }  spinner.text = ' ' + msg  lastMsg = {    symbol: symbol + ' ',    text: msg  }  spinner.start()}exports.stopSpinner = (persist) => {  if (!spinner.isSpinning) {    return  }  if (lastMsg && persist !== false) {    spinner.stopAndPersist({      symbol: lastMsg.symbol,      text: lastMsg.text    })  } else {    spinner.stop()  }  lastMsg = null}

我们新建 lib/io.js,实现 deletePath删除目录方法,如下

function deletePath (filePath){    if(fs.existsSync(filePath)){        const files = fs.readdirSync(filePath);        for(let index=0; index            const fileNmae = files[index];            const currentPath = path.join(filePath,fileNmae);            if(fs.statSync(currentPath).isDirectory()){                deletePath(currentPath)            }else{                fs.unlinkSync(currentPath);            }        }        fs.rmdirSync(filePath);    }}

可以创建 my-app文件夹,这时候可以测试一下 lbs initmy-app-f和 lbs init-f命令,查看my-app是否删除,执行 lbs init,根据一步步提示,输入已经存在的目录名称作为项目名称;选择模板,检查是否my-app文件夹被删除,如下

下载,解压模板

下载模板,需要我们根据选择的模板名称拼接github仓库相对应的zip压缩包的url,然后执行node的下载代码,(注意这里是把下载的zip压缩包下载到系统的临时目录)下载成功后把zip压缩包解压到用户输入项目名称的目录,解压成功后删除已下载的压缩包。这一个流程就结束了这其中下载利用 request插件,解压用到了 decompress插件,这两个插件需要提前安装一下,这两个插件有不熟悉使用的小伙伴可以提前熟悉一下相关使用重写上边的 startDownloadTemplate方法

function startDownloadTemplate(projectName,templateName){    // 开始下载模板    downloadTemplate(templateName, projectName , (error)=>{        if(error){            logger.exit(error);            return;        }        // 替换解压后的模板package.json, index.html关键内容        replaceFileContent(projectName,templateName)    })}function replaceFileContent(projectName,templateName){    console.log(projectName,templateName);}

新建 lib/download.js,实现 downloadTemplate下载模板的方法,代码如下

const request = require("request")const fs = require("fs")const path = require("path")const currentPath = process.cwd();const spinner = require("./spinner")const os = require("os")const { deletePath , unzipFile } = require("./io")exports.downloadTemplate = function (templateName,projectName,callBack){    // 根据templateName拼接github对应的压缩包url    const url = `https://github.com/liuboshuo/${templateName}/archive/master.zip`;    // 压缩包下载的目录,这里是在系统临时文件目录创建一个目录    const tempProjectPath = fs.mkdtempSync(path.join(os.tmpdir(), `${projectName}-`));    // 压缩包保存的路径    const file = path.join(tempProjectPath,`${templateName}.zip`);    // 判断压缩包在系统中是否存在    if(fs.existsSync(file)){        // 删除本地系统已存在的压缩包        fs.unlinkSync(file);     }        spinner.logWithSpinner("下载模板中...")    let stream = fs.createWriteStream(file);    request(url,).pipe(stream).on("close",function(err){          spinner.stopSpinner(false)        if(err){            callBack(err);            return;        }        // 获取解压的目录        const destPath = path.join(currentPath,`${projectName}`);        // 解压已下载的模板压缩包        unzipFile(file,destPath,(error)=>{            // 删除创建的临时文件夹            deletePath(tempProjectPath);            callBack(error);        });    })}

在 lib/io.js添加解压zip压缩包的方法,代码如下

const decompress = require("decompress");exports.unzipFile = function(file,destPath,callBack){    decompress(file,destPath,{        map: file => {            // 这里可以修改文件的解压位置,             // 例如压缩包中文件的路径是 ${destPath}/lb-react-apps-template/src/index.js   =》  ${destPath}/src/index.js            const outPath = file.path.substr(file.path.indexOf('/') + 1)            file.path = outPath            return file        }}    ).then(files => {        callBack()    }).catch(error=>{        callBack(error)    })}

这里可以执行以下 lbs initmy-app测试一下

修改项目中的模板文件(package.json,index.html等)

重写 replaceFileContent方法,这一步是把模板中的一些文件的内容修改以下,比如package.json的name,index.html的title值

function replaceFileContent(projectName,templateName){    const currentPath = process.cwd();    try{        // 读取项目的package.json        const pkgPath = path.join(currentPath,`${projectName}/package.json`);        // 读取内容        const pkg = require(pkgPath);        // 修改package.json的name属性为项目名称        pkg.name = projectName;        fs.writeFileSync(pkgPath,JSON.stringify(pkg,null,2));        const indexPath = path.join(currentPath, `${projectName}/index.html`);        let html = fs.readFileSync(indexPath).toString();        // 修改模板title为项目名称        html = html.replace(/(.*)/g,`${projectName}`)        fs.writeFileSync(indexPath,html);    }catch(error){        exit(error)    }    // 安装依赖    install(projectName)}function install(projectName){    console.log(projectName)}
安装依赖

重写 install方法,这里利用 child_process包创建一个 node的子进程来执行 npm install任务。注意这里要执行的命令 npm在不同系统有区别,在 window下执行的是 npm.cmd命令,在 linux和 mac执行的是 npm命令 有不熟悉 child_process使用的小伙伴可以深入学习一下,这是nodejs自带的一个包,非常有用,这里贴一下文档地址 child_process官方文档,这里利用 spawn方法执行系统命令,还可以使用 execFileSync方法来执行文件等等

const currentPath = process.cwd();const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'// 创建一个子进程执行npm install 任务const nodeJob = child_process.spawn(npm , ['install'], {    stdio: 'inherit', // 指定父子进程通信方式    cwd: path.join(currentPath,projectName)});// 监听任务结束,提示用户创建成功,接下来的操作nodeJob.on("close",()=>{    logger.info(`创建成功! ${projectName} 项目位于 ${path.join(currentPath,projectName)}`)    logger.info('')    logger.info('你可以执行以下命令运行开发环境')    logger.infoGreen(` cd ${projectName}       `);    logger.infoGreen(` npm run dev             `);})

执行 lbs init测试一下那么到这里一个简易版的脚手架已经完成!有什么疑问可以关注公众号私信哦~

vue脚手架实现选项卡_从零一步步实现一个前端脚手架相关推荐

  1. 从0搭建vue3组件库: 如何完整搭建一个前端脚手架?

    脚手架是为了保证各施工过程顺利进行而搭设的工作平台.按搭设的位置分为外脚手架.里脚手架:按材料不同可分为木脚手架.竹脚手架.钢管脚手架:按构造形式分为立杆式脚手架.桥式脚手架.门式脚手架.悬吊式脚手架 ...

  2. 前端工程师: 我用gup4.0搭建一个前端脚手架

    冬至时节, 程序员加油! 本文将会介绍如何使用gulp4来搭建项目脚手架,如果您还在使用gulp3或更老的版本,您也以通过本文的一些思想将之前的项目进行完善,更新.如果gulp不是你们团队的重点,也可 ...

  3. 手写一个合格的前端脚手架

    为什么我们需要一套脚手架 为什么我们需要一套脚手架,它能帮助我们解决哪些痛点问题. •前端项目配置越来越繁琐.耗时,重复无意义的工作•项目结构不统一.不规范•前端项目类型繁多,不同项目不同配置,管理成 ...

  4. 创建一个 dva 脚手架工程

    2019独角兽企业重金招聘Python工程师标准>>> 1.2 dva 安装 使用 dva-cli 命令行工具安装 dva.(本文假设已掌握 npm 基础知识) 安装 dva-cli ...

  5. 2021-10-27 Vue安装脚手架npm install -g @vue/cli命令失败_因为文件已存在

    这里写自定义目录标题 Vue安装脚手架npm install -g @vue/cli命令失败_因为文件已存在 Vue安装脚手架npm install -g @vue/cli命令失败_因为文件已存在 1 ...

  6. element中根据条件判断按钮是否禁用_从零动手封装一个通用的vue按钮组件

    我们在使用目前最主流的前端框架vue在开发过程中,组件是一个非常重要的组成部分,可以这么说,所有的vue应用,都是由一个一个的小组件拼装而成的. 正是由于vue组件如此重要,所以vue的生态中,也非常 ...

  7. springboot项目结构_从零搭建Spring Boot脚手架(1):开篇以及技术选型

    1. 前言 目前Spring Boot已经成为主流的Java Web开发框架,熟练掌握Spring Boot并能够根据业务来定制Spring Boot成为一个Java开发者的必备技巧,但是总是零零碎碎 ...

  8. java webpack web项目_零基础如何学习web前端,入门教程分享

    前端作为互联网时代直接触达用户的窗口,大到我们每天浏览到的网站,小到一次点击按钮的页面,前端无处不在.并且在产品的众多开发环节之中,最能让用户直观感受到的就是前端开发.因而前端行业的广阔发展前景也吸引 ...

  9. vue 源码详解(零):Vue 源码流程图

    vue 源码详解(零):Vue 源码流程图 最近在研究 Vue 的源码, 整理博客, 结果想到的.看到的内容实在是太多了, 不知道从何写起, 故整理了一个大致的流程图,根据这个顺序进行一一整理. 为了 ...

  10. 【vue案例】vue实现tab选项卡

    vue实现tab选项卡 文章目录 vue实现tab选项卡 一.效果图展示 二.静态页面结构 css html javascript 三.vue实现 1.将静态结构和样式重构为基于vue模板语法的形式 ...

最新文章

  1. 我在犹豫是不是该收集这几首MP3
  2. malloc一次性最大能申请多大内存空间
  3. python学习------文件处理
  4. 算法总结 -- 博弈论(PN图)
  5. PHP 常用数据库操作
  6. 【每日SQL打卡】​​​​​​​​​​​​​​​DAY 3丨删除重复的电子邮箱【难度简单】
  7. 2021中国企服企业规模化获客体系建设指南
  8. 剖析Caffe源码之Net---NetParameter参数
  9. centos7 下修改网络配置
  10. 上有天最高,自然较为小
  11. easyui-textbox锁定按钮不锁定_EU5几乎锁定年度销量冠军,为何北汽新能源却高兴不起来?...
  12. Blackman 窗函数
  13. [PKKS19] 《Revealing Scenes by Inverting Structure from Motion Reconstructions》(CVPR2019)阅读笔记(完)
  14. pda通用扫描app_手持终端PDA盘点机盘点软件盘点APP
  15. 苹果手机html5定位,苹果手机常去地点可以记录多长时间?
  16. html5分镜头脚本范例,分镜头脚本范例
  17. 带你使用JS-SDK自定义微信分享效果
  18. GraphicsLab Project之基于物理的着色系统(Physical based shading)-直接光照
  19. stack around xxx is corrupted
  20. mysql快速导出数据(带列名)

热门文章

  1. 51单片机课程设计:基于TCS230/3200的颜色复制显示器
  2. 给Metasploit安装无Lorcon2线支持模块
  3. 递归的Fibonacci在数羊
  4. springAOP 之 前置输出
  5. The type XXX is not API (restriction on required library 'D:\jdk-64\jre\lib\rt.jar')
  6. spring3: 表达式5.2 SpEL基础
  7. shell 命令管理tomcat
  8. Mac 上Dock中添加“最近打开过的项目”(Recent Applications)
  9. 【转】Nginx双机热备高可用解决方案【二】
  10. 第26周维生素市场最新动态