zoukankan      html  css  js  c++  java
  • 前端脚手架的那些事儿

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

    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

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

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

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

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

     "bin": {
        "yncms-template": "index.js"
      },

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

    PS E:WorkSpaceyncms-template-cli> yncms-template
    hello cli

    但是,目前这个命令只有我们自己电脑可以用,要想其他人也能安装使用,需要将它发布到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);

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

    PS E:WorkSpaceyncms-template-cli> yncms-template -h
    Usage: index [options]
    
    Options:
      -h, --help  output usage information

    版本(--version)

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

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

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

    PS E:WorkSpaceyncms-template-cli> yncms-template -V
    1.0.10
    PS E:WorkSpaceyncms-template-cli> yncms-template --version
    1.0.10

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

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

    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);
        });

    现在测试一下:

    PS E:WorkSpaceyncms-template-cli> yncms-template init test -d
    test
    true

    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();
            });
        });
      };
    // 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-9u4e00-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;

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

    这里拉取的项目是和远程仓库关联的,我们需要将其删掉(由于我们项目是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}`));

    来点个性化

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

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

    这里我们直接使用现成的轮子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-9u4e00-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;

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

    锦上添花

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

    • 为异步操作加上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('依赖安装成功!'));
    • 在操作成功或者失败给出图标提示,使用log-symbols
    • 可以给文字加点颜色,同理用现成的轮子Chalk
    • 在安装依赖或者其他耗时比较长的时候,用户可能会把终端切到后台,这时我们的操作完成后可以使用node-notifier发出系统通知给予用户提示。
    notifier.notify({
        title: 'YNCMS-template-cli',
        icon: path.join(__dirname, 'coulson.png'),
        message: ' 恭喜,项目创建成功!'
    });
    • 在创建项目的时候,我们可能会需要执行一些shell命令,可以使用shelljs来完成,例如我们要在项目创建结束后打开vscode并退出终端
    // 8. 打开编辑器
    if (shell.which('code')) shell.exec('code ./');
    shell.exit(1);

    电脑刺绣绣花厂 http://www.szhdn.com 广州品牌设计公司https://www.houdianzi.com

    结语

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

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

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

  • 相关阅读:
    Spring基础知识
    Hibernate基础知识
    Struts2基础知识
    在eclipse里头用checkstyle检查项目出现 File contains tab characters (this is the first instance)原因
    java后台获取cookie里面值得方法
    ckplayer 中的style.swf 中的 style.xml 中的修改方法
    java hql case when 的用法
    Windows下Mongodb安装及配置
    Mongodb中经常出现的错误(汇总)child process failed, exited with error number
    Mac 安装mongodb
  • 原文地址:https://www.cnblogs.com/xiaonian8/p/13808635.html
Copyright © 2011-2022 走看看