⚡【有手就行】轻松打造属于自己的Vue工程化脚手架工具__Vue.js
发布于 4 年前 作者 banyungong 1760 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

前言

为什么闲得蛋疼要自己做脚手架

  • 官方脚手架工具创建的项目不能马上进行搬砖?

  • 每次创建新项目就得复制粘贴旧项目的配置再CRUD?

  • 能不能自己造个劳资御用的个性化定制版脚手架?

以Vue举例来说,虽然vue-cli已经非常牛批,可以快速灵活的创建各种各样配置搭配的初始项目模板,但这对于能够开始搬砖还远远不够,我们还得在此基础上加上一个又一个的配置,并且这些配置散乱无章又大多互不关联,很容易这里少了一个配置那里配错了一个地方,要么就是直接copy旧项目的一些配置文件过来修修改改,属实有点烦琐了。

那么vue-cli不能创建再工程化一些的项目模板吗?

理论上是可以的,但所谓众口难调,不信你可以fork一下你小伙伴的项目,每个人实际开发的工程化习惯是不一样的,有的人喜欢把所有的component文件放在一起用业务模块划分文件夹,有的人喜欢分pages目录和components目录再进一步细分业务和通用组件等等。

总而言之,每个人根据自己的工作经验总结出来的工程化理解和习惯是具有差异性的。那么我们为什么不把自己的这套工程化经验和习惯抽象成一个或者是多个高度工程化的项目模板,再通过自己的脚手架工具去快速创建呢?

效果体验

全局安装

npm i channing-cli -g

创建项目

在希望创建的父目录路径下:

channing create demo-project-111

效果展示

创建Vue工程项目与mock-server项目

channing create demo-project-1
channing create mock-server

安装依赖和启动前端项目和mock-server服务

录视频有点手抖…

运行效果

实现思路/步骤拆解

难的不会,会了不难

说实话做之前对脚手架工具就是仰望高端玩家的视角,但做了之后发现实现一个简单易用的脚手架工具还真不难,甚至挺有意思的。

整体的实现步骤是这样的:

一、创建脚手架执行文件

首先创建这个脚手架的项目文件夹:比如叫channing2-cli。(因为channing-cli我已经发布过了,所以这里为了从头开始演示如何step by step地创造这个脚手架我暂且取名叫channing2-cli吧)。

然后创建一个 index.js 文件,里面长这样:

**注意:在使用Node开发命令行工具时,所执行的入口js脚本中头部必须加入 **#!/usr/bin/env node声明

接着通过命令 npm init -y 进行初始化并且生成 package.json文件,然后在这个文件中配置 bin 这个选项:

-y命令表示接受npm的一切默认参数设置(也可以不使用-y)。

然后使用 npm-link 命令 把这个文件映射到全局后,就可以在任意目录下的命令行中输入 channing2 执行我们的index.js脚本文件:

你好,靓仔~

到这里,我们已经成功将一个脚本文件映射到全局,也就是只要我们输入package.jsonbin配置的key值也就是channing2就可以执行我们channing2-cli文件夹下的index.js脚本了。

似乎有点内味了,接下来就用 commander.js 去为我们的 channing2指令添加参数并且解析,然后完成一系列的操作。

二、使用commander.js解析命令行指令参数

commander.js:完整的 node.js 命令行解决方案,灵感来自 Ruby 的 commander

安装

npm i commander

引入

index.js

const { program } = require('commander') 

然后在index.jsstart方法中调用program的一些api完成指令的创建和解析。

首先定义指令的版本号,用于channing2 -V的输出:

program.version(require('./package.json').version) // 输出版对应的版本号

这里我们直接将package.json中的版本号引入进来输出


现在创建一条create命令:

program
    .command('create <projectName>')
    .description('用于创建一个项目模板')
    .option("-T, --template [template]", "输入使用的模板名字")
    .action(function(projectName, options){
        let template = options.template || "vue-default-template";
        projectName = projectName || 'untitled';
        console.log(`成功创建项目:${projectName}`)
        console.log(`所使用的模板:${template}`);
    });

program.parse(process.argv);

description 的值用于在使用channing2 --hlep命令时看到对应的这条命令的描述信息:

options 则可以让我们输入一些选项参数,当然这是可选的,我们可以在指令后添加参数,然后根据输入的参数去进行不同的操作。

parse方法则是让命令行可以解析我们之前配置的命令。

在命令行中输入:

channing2 create demoProject -T vue-default-template

或者

channing2 create demoProject --template vue-default-template

执行结果为:

当然,这里只是模拟了创建的过程,并没有真正创建模板,后面我们会加入真正创建模板的操作。这里输入的template参数可以让我们通过自定义的一个映射关系(比如用Map)去映射到真正模板对应的git仓库地址,进入创建模板。

但是我怎么知道都有哪些模板呢?

我们可以再创建一条checkAll指令,去打印所有的模板名字,注意要放在program.parse(process.argv)之前

program
    .command('checkAll')
    .description('查看所有的模板')
    .action(function(){
        const templateList = [
            'vue-default-template',
            'vue-default-template-ts'
        ]
        templateList.forEach((temp,index) => {
            console.log(`(${index+1})  ${temp}`)
        })
    })

当我们在命令行中输入 channing2 checkAll

那么目前我们的index.js文件长这样:

#! /usr/bin/env node

const { program } = require('commander') // 引入


function start() {
    console.log('你好,靓仔')

    program.version(require('./package.json').version) // 输出版对应的版本号

    program
        .command('create <projectName>')
        .description('用于创建一个项目模板')
        .option("-T, --template [template]", "输入使用的模板名字")
        .action(function(projectName, options){
            let template = options.template || "vue-default-template";
            projectName = projectName || 'untitled';
            console.log(`成功创建项目:${projectName}`)
            console.log(`所使用的模板:${template}`);
        });

    program
        .command('checkAll')
        .description('查看所有的模板')
        .action(function(){
            const templateList = [
                'vue-default-template',
                'vue-default-template-ts'
            ]
            templateList.forEach((temp,index) => {
                console.log(`(${index+1})  ${temp}`)
            })
        })

    program.parse(process.argv);
}


start()

现在,我们已经知道了怎么通过在命令行输入命令来执行操作,我们核心的操作将在create这条配置的命令的action中去加入我们根据模板创建项目的逻辑,但现在还不急于将这个逻辑加入进来,我们先思考一个问题:

对于用户来说,用户不能够很清晰的知道有哪些模板可以选择,并且在输入模板名字这个方式交互上非常不友好,用户输错了怎么办,我们是不是又要对用户的输入做一系列的if-else判断。如果一开始就让用户通过选择的方式去选择可用的模板就完美了。

这个需求可以做!多亏 inquirer.js这个库,让我们可以在命令行交互上有了丰富的选择~~~

使用 inquirer.js 设计命令行交互

安装

npm i inquire

引入

const inquirer = require('inquirer')

使用

为了让代码结构更清晰,我们可以在index.js同级目录下创建一个inquirers.js 文件,将inquirer的相关逻辑提取到这个文件中,并将这些逻辑操作以function的形式暴露出去,这里这个function取名叫chooseTemplate,然后在index.js中引入并调用chooseTemplate方法。

index.js中:

const {chooseTemplate} = require('./inquirers')


 function start() {
   	// ......省略

    program
        .command('create <projectName>')
        .description('用于创建一个项目模板')
        .option("-T, --template [template]", "输入使用的模板名字")
        .action(async function(projectName, options){
            let template = options.template;
            projectName = projectName || 'untitled';

            if(!template){
                template = await chooseTemplate() // 注意这里是一个异步方法
            }


            console.log(`成功创建项目:${projectName}`)
            console.log(`所使用的模板:${template}`);
        });

   
    // ......省略
}


start()

inquirers.js中:

const inquirer = require('inquirer')

async function chooseTemplate(){
    const promptList = [
        {
            type: "list", // type决定交互的方式,比如当值为input的时候就是输入的形式,list就是单选,checkbox是多选...
            name: "template",
            message: "选择一个需要创建的工程化模板",
            choices: [
                {
                    name: "vue-default (js版本的vue全家桶工程化模板)",
                    value: "vue-template-default",
                },
                {
                    name: "mock-server (用于模拟接口数据的本地node服务模板)",
                    value: "mock-server",
                }
            ],
        },
    ];
    const answers = await inquirer.prompt(promptList);  // 执行命令行交互,并将交互的结果返回
    const {template} = answers
    console.log(`你选择的模板是:${template}`)
    return template  // 返回我们选择的模板
}

module.exports = {
    chooseTemplate
}

现在执行我们的 channing2 create demoProject 看看什么效果:

上下键选择模板,回车确认:

这样就可以通过上下选择,回车确认的方式去看到所有的模板并且选择我们所需要的模板了~

现在我们的整个交互流程已经完成了,那么接下来就开始最重要的环节:创建工程化模板和根据选择的模板创建项目

创建工程化模板并push到GitHub

利用现有的脚手架工具创建一个模板非常简单,以vue模板来举例,我们现在创建一个名字为vue-template-default的模板:

安装vue-cli

npm install -g @vue/cli

创建模板

vue create vue-template-default

根据指示执行即可。

模板工程化配置

在我们刚刚通过vue-cli创建的vue-template-default项目中补充我们工程化需要的配置,添加.gitignore文件,修改目录结构等等的操作。

最好确保项目中有.gitignore这个文件(具体内容可以参考:vue通用.gitignore文件

推送到GitHub远程仓库

首先,在自己的github中创建一个空仓库,命名就以模板的名字命名不易混乱。

vue-template-default项目路径下的控制台中依次执行以下命令:

倒数第二条命令中的仓库地址记得替换成自己刚刚创建的空仓库地址

git init
git add .
git commit -m "first commit"
git branch -M master
git remote add origin https://github.com/channing-cli/vue-template-default.git
git push -u origin master

现在已经创建好了模板并且推送到git仓库中了,接下来就使用download-git-repo这个插件去用我们准备好的模板在本地创建我们的工程化项目了。

使用 download-git-repo 下载模板

回到我们 channing2-cli的控制台中

安装

npm i download-git-repo

引入

const download = require('download-git-repo')

使用

program
    .command('create <projectName>')
    .description('用于创建一个项目模板')
    .option("-T, --template [template]", "输入使用的模板名字")
    .action(async function(projectName, options){
        let template = options.template;
        projectName = projectName || 'untitled';

        if(!template){
            template = await chooseTemplate() // 注意这里是一个异步方法
        }

        const downloadUrl = templateMap.get(template) // templateMap是一个引入的自定义Map

        download(downloadUrl, projectName,{clone: true} , error => {
            if(error){
                console.log(`创建项目失败:${projectName}`)
                console.log('失败原因:',error)
            }else {
                console.log(`成功创建项目:${projectName}`)
            }
        })

    });

核心就是调用download方法进行下载模板到本地。

其中downloadUrl,我这里用了一个templateMap去将选择的模板名字映射为下载地址,templateMap的定义在同级目录下的templateMap.js中:

const templateMap = new Map()

templateMap.set('vue-template-default',"https://github.com:ChanningHan/vue-template-default#master")
templateMap.set('mock-server',"https://github.com:channing-cli/mock-server#master")


module.exports = templateMap

需要注意的是这里downloadUrl的格式,可以理解为仓库地址 + #分支名,但需要将仓库地址中https://github.com后面的 '/ '换成 ‘:

否则你就会发现一直报128的错误~(我就被这坑过)

128错误的发生还出现在在相同目录下创建相同名字的项目

现在我们可以切到自己期望的目录下去执行我们创建项目的命令:

至此就完成了从命令行到创建自己定制化的初始工程项目的完整功能

功能上是没问题了,但是好像还有一个问题:这些命令行是不是长得太平平无奇有点单调了?并且这个下载的过程有点呆头呆脑,能不能整个骚气一点的?

下面就让我们用orachalk这两个库去美化我们的命令行输出~

使用ora 和 chalk 美化命令行

使用ora增加loading效果:

安装

npm install ora

引入

const ora = require('ora')

使用

index.js中:

program
    .command('create <projectName>')
    .description('用于创建一个项目模板')
    .option("-T, --template [template]", "输入使用的模板名字")
    .action(async function(projectName, options){
        let template = options.template;
        projectName = projectName || 'untitled';

        if(!template){
            template = await chooseTemplate() // 注意这里是一个异步方法
        }
    

        // 下载前提示loading
        const spinner = ora({
            text: '正在下载模板...',
            color: "yellow",
            spinner: {
                interval: 80,
                frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
            },
        });
        spinner.start();



        /**
         * @downloadUrl   注意所需要的格式,不要直接复制粘贴仓库地址
         *
         * @project       项目名称
         *
         */
        const downloadUrl = templateMap.get(template)
        download(downloadUrl, projectName,{clone: true} , error => {
            if(error){
                spinner.fail(`创建项目失败:${projectName}`)
                console.log('失败原因:',error.message)
            }else {
                spinner.succeed(`成功创建项目:${projectName}`)
            }
        })

    });

效果

这个小点点转的,爱了爱了

使用chalk改变命令行颜色

安装

npm install chalk

引入

const chalk = require('chalk');

使用

人狠话不多,全部一把梭

最终index.js

#! /usr/bin/env node

const { program } = require('commander') // 引入
const download = require('download-git-repo')
const templateMap = require('./templateMap')
const ora = require('ora')
const chalk = require('chalk');


const {chooseTemplate} = require('./inquirers')

 function start() {
    console.log(chalk.rgb(216, 27, 96)('\n 😈😈😈  雷猴啊, 靓仔~~'))
    console.log(chalk.cyanBright(' 🦄🦄🦄  靓仔正在使用channing2-cli命令行工具...\n'))

    program.version(require('./package.json').version) // 输出版对应的版本号

    program
        .command('create <projectName>')
        .description('用于创建一个项目模板')
        .option("-T, --template [template]", "输入使用的模板名字")
        .action(async function(projectName, options){
            let template = options.template;
            projectName = projectName || 'untitled';

            if(!template){
                template = await chooseTemplate() // 注意这里是一个异步方法
            }

            console.log(chalk.rgb(69, 39, 160)('你选择的模板是 👉'),chalk.bgRgb(69, 39, 160)(template))

            // 下载前提示loading
            const spinner = ora({
                text: '正在下载模板...',
                color: "yellow",
                spinner: {
                    interval: 80,
                    frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
                },
            });
            spinner.start();


            /**
             * @downloadUrl   注意所需要的格式,不要直接复制粘贴仓库地址
             *
             * @project       项目名称
             *
             */
            const downloadUrl = templateMap.get(template)
            download(downloadUrl, projectName,{clone: true} , error => {
                if(error){
                    spinner.fail(`下载失败 😭😭😭`)
                    console.log(chalk.bgRgb(220,0,8)(`  创建项目失败:${projectName} `),'😭😭😭')
                    console.log('🧐🧐🧐 失败原因:',chalk.bgRgb(220,0,8)(error.message))
                }else {
                    spinner.succeed(`下载完成:${projectName}`)
                    console.log('✌✌✌',chalk.rgb(69, 39, 160)('成功创建项目  👉  '),chalk.bgRgb(69, 39, 160)(projectName))
                }
            })

        });

    program
        .command('checkAll')
        .description('查看所有的模板')
        .action(function(){
            const templateList = [
                'vue-default-template',
                'vue-default-template-ts'
            ]
            templateList.forEach((temp,index) => {
                console.log(chalk.rgb(69, 39, 160)(`(${index+1})  ${temp}`))
            })
        })

    program.parse(process.argv);
}


start()

最终项目文件目录:

效果

成功

失败(比如重复创建、下载地址错误等原因会导致失败)

Gay里Gay气的

Finally~来到最后一步了:把自己一手一脚创建的脚手架工具发布到NPM上吧~

发布到 npm

  1. 在npm官网注册一个自己的账号(已有的请忽略)

  2. 在npm上搜一下看看自己即将发布的包是否已存在同名的包(自己的包名在package.json文件中可以查看修改)

  3. 万事俱备后打开控制台,输入npm login进行登录

  4. 登录成功后,在 channing2-cli项目文件夹路径下的控制台中输入npm publish命令进行发布:

  5. 在npm上查一下

大功告成~

写在最后

花了一整天功夫终于写完了(泪目😭😭😭)

第一次在论坛上分享博客,深深感受到原创内容创作者的呕心沥血,respect所有原创作者!🤞🤞🤞

做这个脚手架工具其实还有一个初衷:前不久用qiankun这个微前端解决方案的实现库重构了一个做了快两年的中后台系统(属实巨石应用),但是接入微应用的时候少不了一些必要的配置(暴露生命周期的hook之类的),每次做新应用起手都要花点时间去做接入配置,所以就想到自己造个脚手架工具去快速创建接入微应用的模板(然鹅我到现在还没来得及把微应用的模板放上去…🙃)。

做这个脚手架工具的时候顺便也总结了一下过去开发的一些工程化经验,抽象到了vue-template-default模板中,写这篇文章的时候只上了这个js版本的模板以及一个用koa2搭建的node本地服务模板,用于在日常开发中快速创建模拟后端的接口来进行前端开发。

总之总之,感谢超级具有耐心能够坚持看到这里的读者们🤞🤞🤞

所有的 模板 都放在 channing-cli 的同级目录下,欢迎大家提issue交流对于工程化的见解~

后续也会继续更新放置并且完善更多的模板,当然大🔥也可以提供优质模板,ok的话会放到脚手架工具中供大家使用。


喜欢的话点个star⭐吧 阿sir🙏

GitHub:channing-cli

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: ChanningHyl🙌 原文链接:https://juejin.im/post/6867331101552181262

回到顶部