自定义脚手架工具(类cli)

发布于:2023-02-02 ⋅ 阅读:(563) ⋅ 点赞:(0)

前言

在前端开发中,我们所熟知的脚手架工具 vue 有 vue-cli,react 有 create-react-app,根据选择项来生成新项目模板。其模板满足于大多数小型的项目或个人项目,但对企业来说,官方提供的脚手架不够定制化,生成项目之后仍需要添加很多配置或需导入很多已成熟的模块,离生成即用还有一定的距离。

开发者可以通过定制自己的cli工具来进行处理,做一个简单的cli工具,集成自己预设的模块,根据模板来生成,后续只需维护脚手架的更新。

Demo目录

└───demo-mycli/.....................Demo目录
    ├───lib/............................lib目录
    |	└───core/...........................指令目录
    │       └───create.js.......................create指令
    │   └───utils/..........................工具目录
    │       └───action.js.......................action代码
    │       └───inquirer.js.....................inquirer代码
    │       └───ora.js..........................ora代码
    │       └───templateCreate.js...............templateCreate代码
    │       └───terminal.js.....................terminal代码
    ├───template/.......................模板文件
    │   └───src/
    │       └───baseConfigComponents/
    │       └───loginComponents/
    │       └───queryComponent/
    │   └───vite.config.js
    ├───components/.....................ejs文件目录
    │   └───package.ejs.....................需写入的package文件
    ├───universe/.......................备份文件夹
    ├───index.js........................index文件(梦开始的地方)
    └───package.json....................模块包配置文件

index.js

创建空文件夹demo-mycli,创建index.js文件,在首行写上
#!/usr/bin/env node

若是有使用过Linux或者Unix的前端开发者,对于Shebang应该不陌生,它是一个符号的名称,#!。这个符号通常在Unix系统的基本中第一行开头中出现,用于指明这个脚本文件的解释程序。了解了Shebang之后就可以理解,增加这一行是为了指定用node执行脚本文件。

package.json

通过命令行npm init或者yarn init创建一个package.json文件。

package.json文件中再加一项bin属性,指向index.js

"bin": {
    "mycli": "index.js"
},

cli各种工具库

为了实现脚手架的功能,我们需要借助各种库:

commander

npm install commander

编写代码来描述你的命令行界面。 Commander 负责将参数解析为选项和命令参数,为问题显示使用错误,并实现一个有帮助的系统。

inquirer

npm install inquirer

inquirer 的主要作用是询问,有input 输入框输入,number 数字输入,confirm yes/no确认,list/rawlist 列表选择(单选),expand展开,checkbox多选,password密码输入,editor在临时文件上启动用户首选编辑器的实例。

ora

npm install ora

相当于命令行终端中的进度条,用它来表示动作的开始和结束,加载中等状态。

ejs

npm install ejs

ejs 是一套简单的模板语言,帮你利用普通的 JavaScript 代码生成 HTML 页面。

至此,该demo的package.json文件代码为

{
  "name": "demo-mycli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    "mycli": "index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "commander": "^9.4.0",
    "ejs": "^3.1.6",
    "inquirer": "^8.2.0",
    "ora": "^5.4.1"
  }
}

在demo-mycli终端使用npm install拉取依赖

在demo-mycli终端使用npm link链接mycli成全局命令

自定义create指令

现在我们开始编写自定义create指令

index.js调用指令

需在尾行写program.parse(process.argv)来执行command语句

index.js页面代码:

#!/usr/bin/env node
const program = require('commander')

// 调用自定义指令
const createCommandsPage = require('./lib/core/create')
createCommandsPage.createCommands()

program.parse(process.argv)

create.js编写指令

commander的基础用法:

const program = require('commander')

program
    .command('name') // 自定义指令名称
    .description('description')  // 该指令的文字描述
    .action(function) // 指令的function执行代码

create.js页面代码:

const program = require('commander')

const { createProjectAction } = require('../utils/actions')
const createCommands = ()  => {
    program
        .command('create')
        .description('以模板为基础创建新的项目')
        .action(createProjectAction)
}

module.exports = {
    createCommands
}

actions.js编写指令执行代码

inquirer的基础用法:

const inquirer = require('inquirer');

inquirer.prompt([
    {
        type:"confirm",
        name:"firut",  
        message:"你喜欢吃榴莲吗?"  
    },
    {
        type:"input",
        name:"food",  
        message:"告诉我你喜欢吃什么?"  
    }
]).then((answers) => {
    console.log(answers);
})

ora的基础用法:

const ora = require('ora')

const loadingCreate = ora('Loading unicorns')
loadingCreate.color = 'green'
loadingCreate.text = '正在创建中'
loadingCreate.start()

ejs的基础用法:

let template = ejs.compile(str, options);
template(data);
// => 输出渲染后的 HTML 字符串

ejs.render(str, data, options);
// => 输出渲染后的 HTML 字符串

ejs.renderFile(filename, data, options, function(err, str){
    // str => 输出渲染后的 HTML 字符串
});

执行终端命令相关const {spawn} = require('child_process')

拉取模板至目标文件主流有两种方式:

  1. 从远程仓库拉取,网络可查资料大多数使用的是download-git-repo

  2. 从模板文件夹中拉取

由于download-git-repo并不支持按模块拉取,功能较为有限,该demo采用第二种方式。
inquirer.js页面代码:

const createInformation = [
    {
        type: 'input',
        name: 'createName',
        message: '请输入项目名称',
    },{
        type: "checkbox",
        name: "compList",
        message: "请选择你所需要的模块",
        choices: [
            { name: 'baseConfigComponents', value: 'baseConfigComponents' },
            { name: 'loginComponents', value: 'loginComponents' },
            { name: 'queryComponent', value: 'queryComponent' },
        ],
    },
]

module.exports = {
    createInformation,
}

ora.js页面代码:

const ora = require('ora')

const loadingCreate = ora('Loading unicorns')
loadingCreate.color = 'green'

module.exports = {
    loadingCreate,
}

terminal.js页面代码:

const {spawn} = require('child_process')

const commandSpawn = (...args) => {
    return new Promise((resolve,reject) => {
        const childProcess = spawn(...args)
        // 显示子进程的控制台打印信息
        childProcess.stdout.pipe(process.stdout)
        childProcess.stderr.pipe(process.stderr)
        childProcess.on("close",() => {
            resolve()
        })
    })
}

module.exports = {
    commandSpawn
}

templateCreate.js页面代码:

const ejs = require('ejs')
const fs = require("fs");
const path = require("path");

const packageCreate = (name) => {
    let templateCode = fs.readFileSync(path.join(__dirname,'../../components/package.ejs'))
    let template = ejs.render(templateCode.toString(),{
        name:name,
    })
    return template;
}

module.exports = {
    packageCreate,
}

package.ejs页面代码:

{
  "name":"<%= name %>",
  "version": "3.0.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve --mode development",
    "dev": "vue-cli-service build --mode developmentBuild",
    "build": "node build.js"
  },
  "dependencies": {
    "vue-router": "^3.2.0",
    "vuex": "^3.4.0",
  },
  "devDependencies": {
    "babel-eslint": "^10.1.0",
  },
  "lint-staged": {
    "*.{js,jsx,vue}": [
      "vue-cli-service lint",
      "git add"
    ]
  }
}

自此,我们在actions.js文件中引用所需代码

actions.js页面代码:

const { commandSpawn } = require('./terminal')
const { createInformation } = require('./inquirer')
var inquirer = require('inquirer')
const { loadingCreate } = require('./ora')
const fs = require("fs");
const stat = fs.stat;
const path = require("path");
const { packageCreate } = require('./templateCreate')

// 工具当前目录
const allDir = path.join(__dirname, "../../template");
const componentsDir = path.join(__dirname, "../../template/src");

const createProjectAction = () => {
    inquirer.prompt(createInformation).then(async(res) => {
        // console.log(res.createName,res.compList)
        loadingCreate.start('项目正在努力创建中')
        // 预创建总文件夹
        mkdirfs(res.createName)
        // 复制不可选的所有文件
        fn(allDir,res.createName)
        // 拉取所选择模块
        if(res.compList.length > 0) {
            res.compList.forEach(item => {
                // console.log(`${componentsDir}/${item}`)
                exists(`${componentsDir}/${item}`, `${res.createName}/src/${item}`, copy);
            })
        }
        // 创建并写入需按配置更改内容的文件
        fs.writeFileSync(`${res.createName}/package.json`, packageCreate(res.createName));

        loadingCreate.succeed('项目完成创建')
        // 判断系统平台
        loadingCreate.start('正在努力拉取依赖')
        const command = process.platform === 'win32'?'npm.cmd':'npm'
        // 执行npm install
        await commandSpawn(command,['install'],{cwd:`./${res.createName}/`})
        loadingCreate.succeed('项目已完成创建并成功拉取依赖')
    })
}
function mkdirfs(name){
    fs.mkdir(`./${name}`,(err)=>{
        if (err) {
            console.log("name文件夹创建失败");
            return;
        }
    })
    fs.mkdir(`./${name}/src`,(err)=>{
        if (err) {
            console.log("src文件夹创建失败");
            return;
        }
    })
}
function fn(src,dst){
    fs.readdir(src, function (err, paths) {
        if (err) {
            throw err;
        }
        paths.forEach(function (path) {
            if(path == `src`){
                return
            }else{
                var _src = src + "/" + path,
                _dst = dst + "/" + path,
                readable,
                writable;
                stat(_src, function (err, st) {
                    if (err) {
                        throw err;
                    }
                    if (st.isFile()) {
                    readable = fs.createReadStream(_src);
                    writable = fs.createWriteStream(_dst);
                    readable.pipe(writable);
                    }
                    else if (st.isDirectory()) {
                        exists(_src, _dst, copy);
                    }
                });
            }
        });
    });
}
var exists = function (src, dst, callback) {
    fs.exists(dst, function (exists) {
        if (exists) {
            callback(src, dst);
        }
        else {
            fs.mkdir(dst, function () {
                callback(src, dst);
            });
        }
    });
};
var copy = function (src, dst) {
    fs.readdir(src, function (err, paths) {
        if (err) {
            throw err;
        }
        paths.forEach(function (path) {
            var _src = src + "/" + path,
                _dst = dst + "/" + path,
                readable,
                writable;
            stat(_src, function (err, st) {
                if (err) {
                    throw err;
                }
                if (st.isFile()) {
                readable = fs.createReadStream(_src);
                writable = fs.createWriteStream(_dst);
                readable.pipe(writable);
                }
                else if (st.isDirectory()) {
                    exists(_src, _dst, copy);
                }
            });
        });
    });
};

module.exports = {
    createProjectAction,
}

自此,demo编写完成。

实现

重申:

在demo-mycli终端使用npm install拉取依赖

在demo-mycli终端使用npm link链接mycli成全局命令

在终端中输入

mycli create

回车
在这里插入图片描述
回车

即成功创建了名为demo01的包含baseConfigComponents、queryComponent模块的项目文件并执行npm install命令拉取了依赖。

尾声

故乡遥,何日去?家住吴门,久作长安旅。


网站公告

今日签到

点亮在社区的每一天
去签到