也来盘盘前端脚手架的那些事儿

开发 前端
在上半年的一个项目中需要自定义一个脚手架,来帮助小伙伴们提高开发效率,统一代码输出质量,并解决一些使用上的问题。

[[286793]]

本来早就想写这篇文章的,由于有其他事情耽搁了(可能还是因为太懒),就拖到了现在,如果再不记下来,估计会抛到九霄云外了。

NodeJs的出现,让前端工程化的理念不断深入,正在向正规军靠近。先是带来了Gulp、Webpack等强大的构建工具,随后又出现了vue-cli和create-react-app等完善的脚手架,提供了完整的项目架构,让我们可以更多的关注业务,而不必在项目基础设施上花费大量时间。

但是,这些现成的脚手架未必就能满足我们的业务需求,也未必是最佳实践,这时我们就可以自己来开发一个脚手架。当然,这其实很简单,利用npm上现成的轮子就可以搞定,这里做个记录,仅当备忘,以抛砖引玉。

缘起

在上半年的一个项目中需要自定义一个脚手架,来帮助小伙伴们提高开发效率,统一代码输出质量,并解决一些使用上的问题,当然也是为了装装逼。

在使用脚手架方式构建之前,我们遇到了这几个问题:

  •  每个项目创建的时候,需要去Git仓库拉取项目模板或者拷贝之前的项目,这样做有两个问题
    •   从Git拉取项目后,由于部分人员是有推送权限的,如果他误操作将私有项目中的修改推送到了模板仓库,可能会破坏Git上的项目模板
    •   从之前项目拷贝就无法获取最新的项目模板,这就导致有些问题明明在最新的模板中已经修复,却在新项目中依然存在
  •  项目模板需要填写一些配置信息,开发人员很容易忘记填写

所以我们来解决这些问题,思路如下:

  •  从Git上拉取最新模板,最后干掉Git仓库信息,切断和远程仓库的关联
  •  在初始化时,通过问答方式强制让使用者输入配置信息,再根据配置信息生成配置文件,有点类似VueCli初始化项目那样。

当然,除了上述需求,我们还可以再做些额外工作:

  •  拉取完成后自动安装项目依赖,并打开编辑器
  •  提供帮助信息及常用命令查看
  •  发布到NPM,所有人员都可以直接全局安装使用
  •  ...

急急如律令

实现可执行模块

首先,我们需要创建一个项目,这里就叫yncms-template-cli, 项目结构如下: 

- commands  // 此文件夹用于放置自定义命令  
- utils  
- index.js  // 项目入口  
- readme.md 
  • 1.
  • 2.
  • 3.
  • 4.

为了测试,我们先在index.js放点内容: 

#!/usr/bin/env node  
// 必须在文件头添加如上内容指定运行环境为node  
console.log('hello cli'); 
  • 1.
  • 2.
  • 3.

对于一般的nodejs项目,我们直接使用node index.js就可以了,但是这里是脚手架,肯定不能这样。我们需要把项目发布到npm,用户进行全局安装,然后就可以直接使用我们自定义的命令,类似yncms-template这样。

所以,我们需要将我们的项目做下改动,首先在packge.json中添加如下内容: 

"bin": {  
   "yncms-template": "index.js"  
 }, 
  • 1.
  • 2.
  • 3.

这样就可以将yncms-template定义为一个命令了,但此时仅仅只能在项目中使用,还不能作为全局命令使用,这里我们需要使用npm link将其链接到全局命令,执行成功后在你的全局node_modules目录下可以找到相应文件。然后输入命令测试一下,如果出现如下内容说明第一步已经成功一大半了: 

PS E:\WorkSpace\yncms-template-cli> yncms-template  
hello cli 
  • 1.
  • 2.

但是,目前这个命令只有我们自己电脑可以用,要想其他人也能安装使用,需要将它发布到npm,大致流程如下:

  1.  注册一个npm账户,已有账户的可以跳过这一步
  2.  使用npm login登录,需要输入username、password、email
  3.  使用npm public发布

这一步比较简单,不多说,但是请注意如下几点:

  •  使用了nrm的需要先将源切换到npm官方源
  •  package.json中有几个字段需要完善:
    •   name为发布的包名,不能和npm已有的包重复
    •   version为版本信息,每次发布都必须要比线上的版本高
    •   homepage、bugs、repository也可以添加上,对应如下页面   

  • 在readme.md加入脚手架介绍及使用方法,方便他人使用。如果需要在文档中加入徽标,展示脚手架的下载次数之类的,可以在这里生成。

发布成功后,需要等待一会儿才可以在npm仓库搜索到。

创建命令

既然是脚手架,肯定不能只让它输出一段文字吧,我们还需要定义一些命令,用户在命令行输入这些命令和参数,脚手架会做出对应的操作。这里不需要我们自己去解析这些输入的命令和参数,有现成的轮子(commander)可以使用,完全可以满足我们的需要。

帮助(--help)

安装好commander后,我们将index.js中内容改为如下: 

#!/usr/bin/env node  
const commander = require('commander');  
// 利用commander解析命令行输入,必须写在所有内容最后面  
commander.parse(process.argv); 
  • 1.
  • 2.
  • 3.
  • 4.

这时,虽然我们没有定义任何命令,但是commander内部给我们定义了一个帮助命令--help(简写-h): 

PS E:\WorkSpace\yncms-template-cli> yncms-template -h  
Usage: index [options]  
Options:  
  -h, --help  output usage information 
  • 1.
  • 2.
  • 3.
  • 4.

版本(--version)

接下来,我们再创建一个查询版本的命令参数,在index.js增加如下内容: 

// 查看版本号  
commander.version(require('./package.json').version); 
  • 1.
  • 2.

这样,我们在命令行就可以查看版本号了: 

PS E:\WorkSpace\yncms-template-cli> yncms-template -V  
1.0.10  
PS E:\WorkSpace\yncms-template-cli> yncms-template --version  
1.0.10 
  • 1.
  • 2.
  • 3.
  • 4.

默认参数是大写V,如果需要改成小写,将上面内容做如下改动即可: 

// 查看版本号  
commander  
    .version(require('./package.json').version)  
    .option('-v,--version', '查看版本号');  
  • 1.
  • 2.
  • 3.
  • 4.
PS E:\WorkSpace\yncms-template-cli> yncms-template -h  
Usage: index [options]  
Options:  
  -V, --version  output the version number  
  -h, --help     output usage information 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

init子命令

接下来,我们来定义一个init命令,如yncms-template init test。

在index.js中增加如下内容: 

commander  
    .command('init <name>') // 定义init子命令,<name>为必需参数可在action的function中接收,如需设置非必需参数,可使用中括号  
    .option('-d, --dev', '获取开发版') // 配置参数,简写和全写中使用,分割  
    .description('创建项目') // 命令描述说明  
    .action(function (name, option) { // 命令执行操作,参数对应上面的设置的参数  
        // 我们需要执行的所有操作,都在这里完成  
        console.log(name);  
        console.log(option.dev);  
    }); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

现在测试一下: 

PS E:\WorkSpace\yncms-template-cli> yncms-template init test -d  
test  
true 
  • 1.
  • 2.
  • 3.

commander具体的用法,请自行查看官方文档

如此,一个自定义命令雏形就算完成了,然还有几件事情要做:

  •  实现init命令具体执行的操作,下面会有单独部分来说。
  •  为了方便维护,将命令action拆分到commands文件夹中

拉取项目

上面,我们定义了init命令,但是并没有达到初始化项目的目的,接下来我们就实现一下。

一般来说,项目模板有两种处理方式:

  •  将项目模板和本脚手架放在一起,好处是用户安装脚手架后,模板在本地,初始化会比较快;缺点是项目模板更新比较麻烦,因为和脚手架耦合在一起了
  •  将项目放置到单独的GIT仓库,好处是模板更新比较简单,因为是相互独立的,只需要维护模板自己的仓库即可,另外可以控制拉取权限,因为如果是私有项目,那么没有权限的人员是无法拉取成功的;缺点就是每次初始化都要去GIT拉取,可能会慢点,不过影响不大,所以建议选择此种方式

首先,我们利用download-git-repo封装一个clone方法,用于从git拉取项目。 

// utils/clone.js  
const download = require('download-git-repo');  
const symbols = require('log-symbols');  // 用于输出图标  
const ora = require('ora'); // 用于输出loading  
const chalk = require('chalk'); // 用于改变文字颜色  
module.exports = function (remote, name, option) {  
    const downSpinner = ora('正在下载模板...').start();  
    return new Promise((resolve, reject) => {  
        download(remote, name, option, err => {  
            if (err) { 
                 downSpinner.fail();  
                console.log(symbols.error, chalk.red(err));  
                reject(err);  
                return;  
            };  
            downSpinner.succeed(chalk.green('模板下载成功!'));  
            resolve();  
        });  
    });  
  };  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
// commands/init.js  
const shell = require('shelljs');  
const symbols = require('log-symbols');  
const clone = require('../utils/clone.js');  
const remote = 'https://gitee.com/letwrong/cli-demo.git' 
let branch = 'master' 
const initAction = async (name, option) => {  
    // 0. 检查控制台是否可以运行`git `,  
    if (!shell.which('git')) {  
        console.log(symbols.error, '对不起,git命令不可用!');  
        shell.exit(1);  
    }  
    // 1. 验证输入name是否合法  
    if (fs.existsSync(name)) {  
        console.log(symbols.warning,`已存在项目文件夹${name}!`);  
        return;  
    }  
    if (name.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {  
        console.log(symbols.error, '项目名称存在非法字符!');  
        return;  
    }  
    // 2. 获取option,确定模板类型(分支)  
    if (option.dev) branch = 'develop' 
    // 4. 下载模板  
    await clone(`direct:${remote}#${branch}`, name, { clone: true });  
};  
module.exports = initAction
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

测试一下,不出意外就可以成功拉取项目了。

这里拉取的项目是和远程仓库关联的,我们需要将其删掉(由于我们项目是svn管理,所以直接把.git文件夹删掉,如果使用git的话,可以git init初始化即可),清理掉一些多余文件: 

// commands/init.js  
// 5. 清理文件  
const deleteDir = ['.git', '.gitignore', 'README.md', 'docs']; // 需要清理的文件  
const pwd = shell.pwd();  
deleteDir.map(item => shell.rm('-rf', pwd + `/${name}/${item}`)); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

来点个性化

在上述过程中,我们实现了一个脚手架的基本功能,大致分为三个流程(拉取模板->创建项目->收尾清理),也解决了上面我项目中遇到的第一个问题。接下来,我们就来看下第二个问题如何解决。

解决的思路就是在创建项目的时候,就通过命令行强制要求开发人员输入对应的配置,然后自动写入配置文件,这样就可以有效避免忘记填写的尴尬。当然通过这种方式也可以实现根据用户的输入来动态初始化项目,达到个性化的目的。

这里我们直接使用现成的轮子inquirer就可以搞定,效果和VueCli创建项目一样,支持很多类型,比较强大,也比较简单,具体用法看官方文档就可以了。这里我直接上代码,在第4步(下载模板)前面增加如下: 

// init.js  
const inquirer = require('inquirer');  
// 定义需要询问的问题  
const questions = [  
  {  
    type: 'input',  
    message: '请输入模板名称:',  
    name: 'name',  
    validate(val) {  
      if (!val) return '模板名称不能为空!';  
      if (val.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) return '模板名称包含非法字符,请重新输入';  
      return true;  
    }  
  },  
  {  
    type: 'input',  
    message: '请输入模板关键词(;分割):', 
     name: 'keywords'  
  },  
  {  
    type: 'input',  
    message: '请输入模板简介:',  
    name: 'description'  
  },  
  {  
    type: 'list',  
    message: '请选择模板类型:',  
    choices: ['响应式', '桌面端', '移动端'],  
    name: 'type'  
  },  
  {  
    type: 'list',  
    message: '请选择模板分类:',  
    choices: ['整站', '单页', '专题'],  
    name: 'category'  
  },  
  {  
    type: 'input',  
    message: '请输入模板风格:',  
    name: 'style'  
  },  
  { 
     type: 'input',  
    message: '请输入模板色系:',  
    name: 'color'  
  },  
  {  
    type: 'input',  
    message: '请输入您的名字:', 
    name: 'author'  
  }  
];  
// 通过inquirer获取到用户输入的内容  
const answers = await inquirer.prompt(questions);  
// 将用户的配置打印,确认一下是否正确  
console.log('------------------------');  
console.log(answers);  
let confirm = await inquirer.prompt([  
    {  
        type: 'confirm',  
        message: '确认创建?',  
        default: 'Y',  
        name: 'isConfirm'  
    }  
]);  
if (!confirm.isConfirm) return false; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.

获取到用户输入的配置以后,就可以写入配置文件或者做个性化的处理了,这个太简单,我这里就不赘述了。

锦上添花

到这里,一个完全满足需求的脚手架就完成了,但是作为一个有追求的程序员,我们可以在界面和易用性上面再做点什么:

  •  为异步操作加上loding动画,可以直接使用ora  
const installSpinner = ora('正在安装依赖...').start();  
if (shell.exec('npm install').code !== 0) {  
    console.log(symbols.warning, chalk.yellow('自动安装失败,请手动安装!'));  
    installSpinner.fail(); // 安装失败  
    shell.exit(1);  
 
installSpinner.succeed(chalk.green('依赖安装成功!')); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  •  在操作成功或者失败给出图标提示,使用log-symbols    

  • 可以给文字加点颜色,同理用现成的轮子Chalk    

  • 在安装依赖或者其他耗时比较长的时候,用户可能会把终端切到后台,这时我们的操作完成后可以使用node-notifier发出系统通知给予用户提示。 
notifier.notify({  
    title: 'YNCMS-template-cli',  
    icon: path.join(__dirname, 'coulson.png'),  
    message: ' ♪(^∀^●)ノ 恭喜,项目创建成功!'  
}); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  •  在创建项目的时候,我们可能会需要执行一些shell命令,可以使用shelljs来完成,例如我们要在项目创建结束后打开vscode并退出终端 
// 8. 打开编辑器  
if (shell.which('code')) shell.exec('code ./');  
shell.exit(1); 
  • 1.
  • 2.
  • 3.

结语

到这里,会发现开发一个脚手架其实很简单,都是使用现成的轮子就可以搞定,不晓得哪位大牛说过玩NodeJS就是玩轮子。

除了上述方法,我们也可以直接通过大名鼎鼎的Yeoman来创建,不过个人觉得没必要,毕竟这玩意也不难。

一个好的脚手架应该是能够解决工作中遇到的问题,提高开发效率的。 

 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2017-01-04 09:47:38

联想企业网盘

2021-01-07 05:34:07

脚手架JDK缓存

2021-09-22 08:26:31

前端脚手架开源项目

2016-08-10 14:59:41

前端Javascript工具

2016-09-07 15:35:06

VueReact脚手架

2021-12-23 10:35:32

SpringCloud脚手架架构

2022-05-23 08:34:08

微前端微服务开发

2021-05-21 05:22:52

脚手架工具项目

2020-03-20 08:32:41

物联网脚手架传感器

2022-04-24 11:33:47

代码管理工程

2018-08-30 16:08:37

Node.js脚手架工具

2018-06-11 14:39:57

前端脚手架工具node.js

2023-11-21 17:36:04

OpenFeignSentinel

2020-12-17 12:43:43

前端gup4.0webpack

2014-08-15 09:36:06

2024-03-11 13:18:00

RustClap项目

2020-06-29 11:35:02

Spring BootJava脚手架

2022-01-14 14:09:11

脚手架代码自定义

2021-10-08 06:10:43

前端技术Vue

2017-07-21 09:56:46

Webpack3 Vue.js脚手架
点赞
收藏

51CTO技术栈公众号