20分钟盘一个提高你开发效率的自定义脚手架

子成君 441 0

[aru_53]

写在前面

前端是一个技术问题较少,工程问题较多的开发领域,没有一个框架可以满足所有的业务场景,为了让公司的前端开发流程自成体系,也为了避免重量级的前端框架,

我写了很多维护性,可用性较强的项目模板(包括uni-app,electron,vue3+vite,Chrome扩展等...),每次有新项目就clone这些项目模板在该基础上进行开发,

虽然vue-cli/create-react-app等脚手架已经提供了完整的项目架构,但在工程化的路上,初始化项目还远远不够...

能够在适合公司业务的现有模板上进行开发,可以省下不少时间和精力.

dt-cli(指下文中即将完成的cli小工具,dt是公司名缩写)可以减少开发人员在拉取模板仓库上的操作维度,只需一句命令行即可,

包括项目模板需要填写一些配置信息各种参数,仓库分支等可以在cli工具中写好以免同事弄错,拉取完成后自动安装项目依赖,爽歪歪[aru_5]

在开始之前,如果你不熟悉npm包的制作发布和废弃,可以查看我之前的文章: npm包的发布/迭代和删除/废弃

发布时确保你使用的为npm官方源

项目github地址(dt-cli-template分支为本次实例代码):https://github.com/zcjunblog/dt-cli/tree/master

项目npm地址: https://www.npmjs.com/package/@duotai/dt-cli

1.实现可执行命令

新建项目文件夹(这里以dt-cli为例),在根目录执行

npm init

填上项目描述名字和作者等

这里需要注意,如果你的项目名称比如我这里的dt-cli,已经被占用了,所以我采用的是以组织发布带命名空间@duotai的npm包

如果你的项目名称未冲突,可以无视这里:

带@的npm包

带有@的包表示该包是范围包。如果有一个包是@test/myPackage那么你可以发布一个@my/myPackage。如果是无范围的包,且存在一个myPackage的包,你发布的包名就不能是myPackage了。注册npm用户帐户或创建组织时,系统会授予与您的用户或组织名称匹配的范围。您可以将此作用域用作相关程序包的命名空间。范围使您可以创建与其他用户或组织创建的包同名的包,而不会发生冲突。

所以我init的package.json文件是这样的:

{
  "name": "@duotai/dt-cli",
  "version": "1.0.0",
  "description": "快速拉取项目模板的cli工具",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "bin": {
    "dt-cli": "./index.js"
  },
  "author": "dt-zhaozc",
  "license": "ISC",
}

name这里是带@/duotai的,组织需要你自行去你的npm账号创建,以获取可使用的命名空间

完成项目初始化后安装下面几个包

1.chalk,让你的 CLI 工具五颜六色;

2.figlet,让你的 CLI 显示一个漂亮的 logo;

3.commander,解析命令参数;

4.clear,清空控制台;

5.inquirer,交互式输入;
 npm install chalk figlet commander clear inquirer

之后在项目根目录创建index.js, 这里我给出一个简单的模板,可以帮你省去了一些简单步骤,具体看代码注释

之后在该文件上继续编写index.js:

#!/usr/bin/env node
// 指定运行环境为node
const chalk = require('chalk');
const clear = require('clear');
const figlet = require('figlet');
const { Command } = require('commander');

const inquirer = require('./lib/create');
const packageJson = require('./package.json');

// 清除控制台
clear();

// 大logo 自行改文字,不支持汉字
console.log(chalk.cyan(figlet.textSync('DUOTAI', {
    horizontalLayout: 'full'
})));

const program = new Command();
// 版本
program
    .version(packageJson.version)
    .description('Quickly create front-end projects')
    .usage('<commnad [options]>');

// 触发 --help 后打印一些信息
program.on('--help', () => {
    console.log();
    console.log(`  create by ${chalk.cyan('xx科技')} zhaozc.`);
    console.log(`  more click ${chalk.red('https://x4v.cn/')}`)
    console.log();
});

program.commands.forEach(c => c.on('--help', () => console.log()));

// 开始解析参数
program.parse(process.argv);

// 无任何命令时输出帮助信息
if (!process.argv.slice(2).length) {
    program.outputHelp();
}

将上述内容复制到你的index.js后,在package.json中定义为一个命令

  "bin": {
    "dt-cli": "./index.js"
  },

至此,已经实现了两个命令了: dt-cli --version 和dt-cli --help

此时在项目根目录调起终端,执行dt-cli(执行你写的指令),如果看到了一个大logo,那恭喜你,你第一步已经完成了,若报错请仔细阅读本文排查问题或拉取文中给出的dt-cli项目仓库源码进行对比

20分钟盘一个提高你开发效率的自定义脚手架

2.实现创建项目命令

commander具体的用法: 官方文档

index.js中增加如下内容:

program
  .command('create <app-name>')// 定义create子命令,<app-name>可在action中接收
  .description('create a new project') // 命令描述
  .option('-u, --uni', '拉取uni-app项目基础模板') // 配置参数,简写和全写用,分割
  .option('-v, --vue', '拉取桌面端vue项目基础模板')
  .option('-e, --electron', '拉取electron项目基础模板')
  .action((name, option) => {
    console.log('name', name)
    console.log('option.uni', option.uni) // 为true则说明携带参数为--uni 
  })

添加后输入dt-cli create my-app --uniconsole结果如下

name my-app
option.uni true

之后就是编写输入create命令需要执行的操作,开头说过了,我要做的就是: 拉代码[aru_34]

为了方便维护,将action的操作拆分到lib文件夹中

20分钟盘一个提高你开发效率的自定义脚手架

因为要从git拉取项目, 在编写create.js之前我们先利用download-git-repo封装一个clone方法。

这里需要再装三个库:

npm install ora log-symbols download-git-repo
// utils/clone.js
const download = require('download-git-repo');
const ora = require('ora'); // 用于输出loading
const symbols = require('log-symbols'); // 用于输出图标
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();
        });
    });
  };

之后编写create.js,  安装shelljs

npm i shelljs

写入下面的代码,代码逻辑很简单,看下去就明白了

const shell = require('shelljs');
const fs = require('fs');
const symbols = require('log-symbols');
const clone = require('../utils/clone.js');
const remotes = { // git克隆地址以及分支 格式:<host>:<userName>/<repo> <projectName> #branchName 
  uni: {host:'github.com', userName:'zcjunblog', repo: 'uni-app-uView-template', branch: 'master' },
  electron: {host:'github.com',userName:'zcjunblog', repo: 'vue-electron-element-template', branch: 'master' }
  // 根据index.js中写的create命令的option配置....
}
let remote = ''; // git地址

const createAction = async (name, option) => {
  // 0. 检查控制台是否可以运行`git `,
  if (!shell.which('git')) {
    console.log(symbols.error, 'git命令不可用!请先安装git:https://git-scm.com/downloads');
    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.uni) remote = remotes.uni
  // if (option.vue) remote = remotes.vue
  if (option.electron) remote = remotes.electron
  // 3. 下载模板
  await clone(`${remote.host}:${remote.userName}/${remote.repo}#${remote.branch}`, name, {clone: true}, (err) => {console.log('err',err)});


module.exports = createAction;

这里执行在项目根目录执行dt-cli create my-project --uni 进行测试(也可以npm link 到全局在其他文件夹测试),可以正常拉下代码,相对于同名文件夹,不合规范的项目文件夹名,自行测试即可

20分钟盘一个提高你开发效率的自定义脚手架

拉取下拉的项目是和远程仓库关联的,所以需要清理一下git文件及一些不需要的说明文件:

// lib/create.js
// 4. 清理文件
const deleteDir = ['.git', 'README.md', 'docs']; // 需要清理的文件
const pwd = shell.pwd();
deleteDir.map(item => shell.rm('-rf', pwd + `/${name}/${item}`));

第五步,拉取完项目模板后进入该文件夹修改package.json文件的信息

这里插入一小步,utils建立一个log.js 封装各颜色的消息提示

//utils/log.js
const chalk = require('chalk');
const ora = require('ora');
// 紫色log
function logMagent(...txt) {
    console.log(chalk.magenta(...txt));
}
// 普通log
function logNormal(...txt) {
    console.log(...txt);
}
// 信息类log
function logInfo(...txt) {
    ora().info(chalk.blue(...txt));
}
// 成功log
function logSuccess(...txt) {
    ora().succeed(chalk.green(...txt));
}
// 警告类log
function logWarn(...txt) {
    ora().warn(chalk.yellow(...txt));
}
// 错误log
function logError(...txt) {
    ora().fail(chalk.red(...txt));
}
// 下划线
function logUnderline(txt) {
    return chalk.underline.blueBright.bold(txt);
}
module.exports = {
    logMagent,
    logNormal,
    logInfo,
    logSuccess,
    logWarn,
    logError,
    logUnderline
};

修改package.json以及安装项目依赖:

安装cross-spawn通过它来同步执行npm install, 引入文件

const { logSuccess, logUnderline, logWarn } = require('../utils/log.js');
const chalk = require('chalk');
const spawn = require('cross-spawn');
  // 5. 写入配置文件
  shell.cd(name);
  const cfgSpinner = ora('正在写入配置信息...').start();
  let pkg = fs.readFileSync(`${pwd}/${name}/package.json`, 'utf8');
  pkg = JSON.parse(pkg);
  pkg.name = name;
  pkg.author = '';
  fs.writeFileSync(`${pwd}/${name}/package.json`, JSON.stringify(pkg), { encoding: 'utf8' });
  cfgSpinner.succeed(chalk.green('配置信息写入成功!'));
  // 6. 安装依赖
  const installSpinner = ora('正在安装项目依赖包... \n').start();
  try {
    spawn.sync('npm', ['install', '-depth', '0'], { stdio: 'inherit' });
  } catch (error) {
    logWarn('自动安装依赖包失败,请手动安装!');
    console.log(error);
    installSpinner.fail();
    shell.exit(1);
  }
  installSpinner.succeed(chalk.green('依赖包安装成功!'));
  logSuccess('\n       o(゚Д゚)っ! \n\n  项目创建成功,冲啊!  \n');

如果你还想在再省一步, 自动打开代码编辑器:

  // 7 打开编辑器
  if (shell.which('code')) shell.exec('code ./');
  shell.exit(0);

至此执行dt-cli create demo --electron  成功拉取代码安装依赖弹出代码编辑器
20分钟盘一个提高你开发效率的自定义脚手架

3.完善脚手架交互功能

上述过程中已经实现了一个脚手架的基本功能,但还不够,使用--uni,--electron这种方式强制要求开发人员输入对应的配置难以避免输错/忘记等尴尬事情的发生,

所以我们选择通过inquirer来完成与用户进行交互,通过勾选配置项来完成"选择基础模板"的过程

这个包文章开始就已经安装了,在create.js中引入

const inquirer = require('inquirer');

重新修改第二步为

  // 2. 获取option,根据参数确定模板类型
  // 用户选择模板
  const tempQuestions = [
    {
      type: 'list',
      message: '请选择项目模板:',
      choices: Object.keys(remotes),
      name: 'type'
    }
  ];
  const selectTemp = await inquirer.prompt(tempQuestions);
  // console.log(selectTemp.type)
  remote = remotes[selectTemp.type]

此时index.js中create子命令option参数项可以删除了

20分钟盘一个提高你开发效率的自定义脚手架

好了,文章到此结束,你可以到https://github.com/zcjunblog/dt-cli/tree/master录取这个cli模板进行二次开发,完善你所需的功能

参考文章:

https://blog.csdn.net/frontend_frank/article/details/108373526

https://www.bwrong.co/post/cli/

 

 

发表评论 取消回复
OwO 图片 链接 代码

分享