打造前端团队的 Vue CLI 工具

1,949 阅读9分钟

主要功能包括基础组件,通用业务组件,网络请求库,Proxy跨域代理,Auth权限,Router路由unit,e2e测试,eslint代码规范,Mock data本地模拟后台数据,图表库,webpack打包工具

很多小伙伴一直很纠结什么是脚手架?其实核心功能就是创建项目初始文件,那问题又来了,市面上的脚手架不够用?为什么还要自己写?
只要提到脚手架你就会想到,vue-clicreate-react-appdva-cli ... 他们的特点不用多说那就是专一! 但是在公司中你会发现有以下一系列的问题!

  • 业务类型多
  • 多次造轮子,项目升级等问题
  • 公司代码规范,无法统一

很多时候我们开发时需要新建项目,把已有的项目代码复制一遍,保留基础能力。(但是这个过程非常琐碎而又耗时)。那我们可以自己定制化模板,自己实现一个属于自己的脚手架。来解决这些问题。


1、必备模块

我们先从大家众所周知的vue-cli入手,先来看看他用了哪些npm包来实现的

  • commander :参数解析 --help其实就借助了他~
  • inquirer :交互式命令行工具,有他就可以实现命令行的选择功能
  • download-git-repo :在git中下载模板
  • chalk :粉笔帮我们在控制台中画出各种各样的颜色
  • metalsmith :读取所有文件,实现模板渲染
  • consolidate :统一模板引擎

2、安装依赖npm

npm link  //在命令行中使用zhu-cli命令,并且执行main.js文件

npm i commander  //安装commander包 自动生成help 解析选项参数
npm i axios  //ajax数据
npm i ora inquirer  //loading的样式 选择模板
npm i download-git-repo //选择好项目模板名称和对应的版本,直接下载
npm i ncp     //实现文件的拷贝功能
npm i metalsmith ejs consolidate    //遍历文件夹  借用ejs模板  使用多种模板引擎统一
或
yarn add commander
yarn add axios
yarn add ora inquirer
yarn add download-git-repo
yarn add ncp
yarn metalsmith ejs consolidate

yarn add commander axios ora inquirer ncp metalsmith ejs consolidate
  • 新建bin文件夹和www文件 bin/www
  • 新建src文件夹和main.js文件 src/main.js
  • 初始化 npm init -y
  • npm install babel-cli babel-env -D
//.babelrc文件
{
  "presets": [
    [
      "env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}
  • src/main.js文件
console.log('hello!');
  • 修改package.json文件

置在命令下执行zhu-cli时调用bin目录下的www文件

"scripts": {
 "compile": "babel src -d dist",//只能生成一次
  "watch": "npm run compile -- -watch",//持续生成
},
"bin":{//bin字段,设置脚手架的入口
  "zf-cli":"./bin/www"
},
  • npm run compile 生成相对应的dis文件夹的文件 npm run watch [常用这个命令]
  • bin/www文件
#!/usr/bin/env node
//使用main作为入口文件,并且以node环境执行此文件
require('../dist/main.js')
  • npm link 链接包到全局下使用,可以在命令行中使用zf-cli命令,并且执行main.js文件。

3、创建文件夹

├── bin
│ └── zf # 全局命令执行的根文件
├── src # 源代码
│ ├── heiper
│ ├── utils # 存放工具方法
│ ├── config.js
│ ├── init.js
│ ├── install.js
│ ├── list.js
│ ├── index.js # 入口文件
│ └── zf.js
├── .eslintrc.js # eslint 配置项
├── ..eslintignore # eslint 忽视文件
├── .babelrc # babel-loader 配置
└── package.json # package.json

4、解析命令行参数

commander:The complete solution for node.js command-line interfaces
先吹一波commander,commander可以自动生成help,解析选项参数!
像这样 vue-cli --help!
像这样 vue-cli create <project-namne>

4.1、使用commander

npm install commander

1
main.js就是我们的入口文件

const program = require('commander');
program.version('0.0.1')
  .parse(process.argv); // process.argv就是用户在命令行中传入的参数


执行zf-cli --help 是不是已经有一提示了!
这个版本号应该使用的是当前cli项目的版本号,我们需要动态获取,并且为了方便我们将常量全部放到util下的constants文件夹中

const { name, version } = require('../../package.json');
module.exports = {
  name,
  version,
};

这样我们就可以动态获取版本号

const program = require('commander');
const { version } = require('./utils/constants');
program.version(version)
  .parse(process.argv);

4.2、配置命令

在命令行中输入 zf-cli --help 中打印出帮助信息
下载包如代码所示

npm install commander

新建src/utils/constants.js文件

import { version } from "../../package.json";
//当前package.json的版本号
export const VERSION = version;

src/main.js文件

import program from "commander";
import {  VERSION  } from "./utils/constants";
//多种功能命令
let actionMap={
    install:{//配置命令的名字
        alias:'i',//命令别的名称
        description:'install template', //命令对应的描述
        examples:[//命令对应的模板
            'zf-cli i',
            'zf-cli install'
        ]
    },
    config:{
        alias:'c',
        description:'config .zfclirc',
        examples:[
            'zf-cli config set <k> <v>',
            'zf-cli config get <k>',
            'zf-cli config remove <k>'
        ]
    },
    '*':{
        description:'not found',
        examples:[]
    }
}
Object.keys(actionMap).forEach(action=>{
    program.command(action)
        .description(actionMap[action].description)
        .alias(actionMap[action].alias)
        .action(()=>{
            console.log(action)
        })
})
function help(){
    // console.log('123')//把example显示出去
    console.log('\r\n  '+'how to use command');
    Object.keys(actionMap).forEach(action=>{
        actionMap[action].examples.forEach(example=>{
            console.log('  - '+example)
        })
    })
}
program.on('-h',help);
program.on('--help',help);
program.version(VERSION, '-v --version').parse(process.argv);

新建src/index.js文件

let apply = () => {}
export default apply;

src/main.js文件

...
Object.keys(actionMap).forEach(action=>{
    program.command(action)
        .description(actionMap[action].description)
        .alias(actionMap[action].alias)
        .action(()=>{
            //console.log(action)
           //判断一下当前用的是什么操作
            if(action === 'config'){
                //实现更改配置文件
                //console.log(process.argv)//数组
                main(action,process.argv.slice(3));
            }else if(action === 'install'){
            }
            main()
        })
        })
})
...

src/index.js文件

//命令行的命令名称拿到后  这个是主流程控制的地方
import { betterRequire } from './utils/common';//动态加载文件
import { resolve } from 'path'
let apply = (action, ...args) => {
    // console.log(action, args)
    //babel-env  export default=>module.exports={default:xxx}
    betterRequire(resolve(__dirname, `./${action}`))(...args) //默认导出
}
export default apply;

新建src/utils/common.js文件

export const betterRequire = (absPath) => {//两种引入方式
    let module = require(absPath);
    if(module.default){
        return module.default;
    }
    return module;
}

4.3、编写help命令

监听help命令打印帮助信息

program.on('--help', () => {
  console.log('Examples');
  Object.keys(actionsMap).forEach((action) => {
    (actionsMap[action].examples || []).forEach((example) => {
      console.log(`  ${example}`);
    });
  });
});

4.4、create命令

create命令的主要作用就是去git仓库中拉取模板并下载对应的版本到本地,如果有模板则根据用户填写的信息渲染好模板,生成到当前运行命令的目录下~

action(() => { // 动作
  if (action === '*') { // 如果动作没匹配到说明输入有误
    console.log(acitonMap[action].description);
  } else { // 引用对应的动作文件 将参数传入
    require(path.resolve(__dirname, action))(...process.argv.slice(3));
  }
}

根据不同的动作,动态引入对应模块的文件
执行zf-cli create project,可以打印出 project

5、config命令

新建config.js 主要的作用其实就是配置文件的读写操作,当然如果配置文件不存在需要提供默认的值,先来编写常量
constants.js的配置

// 本机的home目录
// 找到用户的根目录
const HOME = process.env[process.platform === "win32" ? "USERPROFILE" : "HOME"];
//console.log(process.platform)//win32  node运行的操作系统的环境时显示内核相关的信息
//process.env.USERPROFILE  //当前目录下配置的文件

export const VERSION = version;
export const RC = `${HOME}/.zfrc`;
// 下载目录
export const DOWNLOAD = `${HOME}/.zf`;

//RC配置下载(模板)的地方
//给github的api来用的
export const DEFAULTS = {
  registry: "chef-template",
  type: "orgs", // ['orgs', 'users']
};

编写config.js

const fs = require('fs');
const { defaultConfig, configFile } = require('./util/constants');
module.exports = (action, k, v) => {
  if (action === 'get') {
    console.log('获取');
  } else if (action === 'set') {
    console.log('设置');
  }
  // ...
};

一般rc类型的配置文件都是ini格式也就是:

repo=zhu-cli
register=github

下载 ini 模块解析配置文件

npm i ini

这里的代码很简单,无非就是文件操作了

const fs = require('fs');
const { encode, decode } = require('ini');
const { defaultConfig, configFile } = require('./util/constants');
const fs = require('fs');
const { encode, decode } = require('ini');
const { defaultConfig, configFile } = require('./util/constants');
module.exports = (action, k, v) => {
  const flag = fs.existsSync(configFile);
  const obj = {};
  if (flag) { // 配置文件存在
    const content = fs.readFileSync(configFile, 'utf8');
    const c = decode(content); // 将文件解析成对象
    Object.assign(obj, c);
  }
  if (action === 'get') {
    console.log(obj[k] || defaultConfig[k]);
  } else if (action === 'set') {
    obj[k] = v;
    fs.writeFileSync(configFile, encode(obj)); // 将内容转化ini格式写入到字符串中
    console.log(`${k}=${v}`);
  } else if (action === 'getVal') { 
    return obj[k];
  }
};

getVal这个方法是为了在执行create命令时可以获取到配置变量

const config = require('./config');
const repoUrl = config('getVal', 'repo');

这样我们可以将create方法中所有的zb-cli全部用获取到的值替换掉啦!

6、编写install命令

下载的包

npm install ini 
npm install ora inquirer
npm install download-git-repo

新建src/config.js文件

//专门管理.zfclirc文件(当前的用户目录下)
//zf-cli config set key value
import {get,set,remove,getAll} from './utils/rc.js';
let config =async (action, k, v) => {
    // console.log(action, k, v)
    switch (action) {
        case 'get':
            if (k) {
               let key=await get(k);
               //console.log(key)
            } else {
                let obj=await getAll();
                Object.keys(obj).forEach(key=>{
                    console.log(`${key}=${obj[key]}`);
                })
            }
            break;
        case 'set':
            set(k, v);
            break;
        case 'remove':
            remove(k);
            break;
    }
}
export default config;

src/utils/constants.js文件

//找到用户的根  目录
const HOME = process.env[process.platform === 'win32' ? "USERPROFILE" : "HOME"];
//console.log(process.platform)//win32  node运行的操作系统的环境时显示内核相关的信息
//process.env.USERPROFILE  //当前目录下配置的文件
export const RC = `${HOME}/.zfclirc`;
//RC配置下载(模板)的地方
//给github的api来用的
export const DEFAULTS = {
    registry: 'zhufeng-cli',
    type: 'orgs'
}

新建src/utils/rc.js文件

import {RC, DEFAULTS} from './constants.js'
//RC是配置文件 DEFAULT是默认配置
//promisify:异步函数promise化
import {decode,encode} from 'ini'//格式分析和序列化
import { promisify } from 'util';
import fs from 'fs';
import { exit } from 'process';
let exists = promisify(fs.exists);//测试某个路径下的文件是否存在
let readFile = promisify(fs.readFile);
let writeFile = promisify(fs.writeFile);
export let get = async (k) => {
    //console.log(k)
    let has = await exists(RC);
    let opts;
    if (has) {
        opts = await readFile(RC, 'utf8');
        opts = decode(opts);
        return opts[k];
    }
    return '';
}
export let set = async (k, v) => {
    let has = await exists(RC);
    let opts;
    if (has) {
        opts = await readFile(RC, 'utf8');
        opts = decode(opts);
        Object.assign(opts, { [k]: v });
    }else{
        opts=Object.assign(DEFAULTS,{[k]:v})
    }
    await writeFile(RC,encode(opts),'utf8');
}
export let remove = async (k) => {
    let has=await exists(RC);
    let opts;
    if(has){
        opts=await readFile(RC,'utf8');
        opts=decode(opts);
        delete opts[k];
        await writeFile(RC,encode(opts),'utf8')
    }
}
export let getAll = async () => {
    let has=await exists(RC);
    let opts;
    if(has){
        opts=await readFile(RC,'utf8');
        opts=decode(opts);
        return opts;
    }
    return {}
}

新建src/install.js文件

//下载模板 选择模板使用
//用过配置文件 获取模板信息(有哪些模板)
import { repoList, tagList, downloadLocal,} from './utils/git';
import ora from 'ora';//进度条
import inquirer from 'inquirer';//命令交互
let install = async () => {
    let loading = ora('fetching template ......');
    loading.start()
    let list = await repoList();
    loading.succeed();
    list = list.map(({name}) => name);
    //console.log(list);
    let answer = await inquirer.prompt([{
        type: 'list',
        name: 'project',
        choices: list,
        questions: 'pleace choice template'
    }]);
    // console.log(answer.project);
    //拿到当前项目
    let project = answer.project;
    //获取当前的项目的版本号
    loading = origin('fetching tag ......');
    loading.start();
    list = await tagList(project);
    loading.succeed();
    list = list.map(({name}) => name);
    let tag=answer.tag;
    //下载文件(先下载到缓存区文件中)
    //zf-cli init
    //下载中...
    loading=ora('download project ......');
    loading.start();
    //console.log(project,tag);
    await downloadLocal(project,tag);
    loading.succeed();
}
export default install;
src/utils.js文件
//下载目录
export const DOWNLOAD=`${HOME}/.template`;
新建src/utils/git.js文件
import request from 'request';
import { getAll } from './rc'
import downLoadGit from 'download-git-repo';
import {DOWNLOAD} from './constants'
let fetch = async () => {
    return new Promise((resolve, reject) => {
        let config = {
            url,
            method: 'get',
            headers: {
                'user-agent': 'xxx'
            }
        }
        request(config, (err, response, body) => {
            if (err) {
                reject(err);
            }
            resolve(JSON.parse(body))
        })
    })
}


//链接地址:https://api.github.com/repos/zhufeng-cli/vue-template/tags 版本
export let tagList = async (repo) => {
    let config = await getAll();
    let api = `https://api.github.com/repos/${config.registry}/${repo}/tags`;
    return await fetch(api)
}
//链接地址:https://api.github.com/orgs/zhufeng-cli/repos 项目
export let repoList = async () => {
    let config = await getAll();
    let api = `https://api.github.com/${config.type}/${config.registry}/repos`;
    return await fetch(api);
}
export let download = async (src, dest) => {
    return new Promise((resolve, reject) => {
        downLoadGit(src, dest, (err) => {
            if (err) {
                reject(err);
            }
            resolve();
        })
    })
}
//下载到本地
export let downloadLocal = async (project, version) => {
    let config=await getAll()
    let api =`${config.registry}/${project}`;
    if(version){
        api += `#${version}`;
    }
    return await download(api,DOWNLOAD+'/'+project);
}

我们看到的命令行中选择的功能基本都是基于inquirer实现的,可以实现不同的询问方式

新建src/utils.js git.js

npm i download-git-repo
const { promisify } = require('util');
const downLoadGit = require('download-git-repo');
downLoadGit = promisify(downLoadGit);

node中已经帮你提供了一个现成的方法,将异步的api可以快速转化成promise的形式~
下载前先找个临时目录来存放下载的文件,来~继续配置常量

const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.template`;

这里我们将文件下载到当前用户下的.template文件中,由于系统的不同目录获取方式不一样,process.platform 在windows下获取的是 win32 我这里是mac 所有获取的值是 darwin,在根据对应的环境变量获取到用户目录

const download = async (repo, tag) => {
  let api = `zf-cli/${repo}`; // 下载项目
  if (tag) {
    api += `#${tag}`;
  }
  const dest = `${downloadDirectory}/${repo}`; // 将模板下载到对应的目录中
  await downLoadGit(api, dest);
  return dest; // 返回下载目录
};
// 下载项目
const target = await wrapFetchAddLoding(download, 'download template')(repo, tag);

如果对于简单的项目可以直接把下载好的项目拷贝到当前执行命令的目录下即可。

新建src/utils.js common.js
安装ncp可以实现文件的拷贝功能

npm i ncp


像这样:

let ncp = require('ncp'); 
ncp = promisify(ncp);
// 将下载的文件拷贝到当前执行命令的目录下
await ncp(target, path.join(path.resolve(), projectName));

7、npm发布

7.1、第一次发包

**
发布包之前你首先要有一个npm的账号:npm login
输入你创建的账号、密码和邮箱--->登陆

7.2、非第一次发布包

**
终端输入npm adduser 提示输入账号,密码和邮箱,然后提示创建成功
npm adduser成功的时候默认你已经登陆了,所以不需要再接着npm login

7.3、发布流程

  • 进入项目目录下登录 npm login
  • 通过npm publish发包 包的名称和版本就是项目里的package.json里的name和version
  • 到npm的搜索里可以找到被发布的App了

7.4、注意的点

  • 不能和已有包的名字重名,可以在发包前通过npm的搜索引擎查找是否有已存在相同名称的包
  • npm对包名的限制:不能有大写字母/空格/下划线
  • 项目里有私密部分不想发布到npm上可以写入.gitignore或.npmignore中,上传就会被忽略了。

8、源码