自己创建一个项目Cli脚手架

224 阅读2分钟

初始化项目

首先是先创建一个文件夹, 然后用npm初始化项目生成package.json

$ mkdir my-node-cli
$ cd my-node-cli
$ npm init # 生成 package.json 文件

package.json 里名字可以起项目想要的名字,比如我起的就是yodaoCli

main是你要执行的脚本

bin这个可以自定义脚本,比如我想要的框架执行是 yodao create 项目名 然后生成一个项目

那么就写成

"bin": {
  "yodaocli": "./index.js"
},

index.js是你要执行的内容,可在根目录新生成一个空js文件,然后里面写

#!/usr/bin/env node

console.log('测试cli11111')

#!/usr/bin/env node ,简单的理解,就是输入命令后,会有在一个新建的 shell 中执行指定的脚本,在执行这个脚本的时候,我们需要来指定这个脚本的解释程序是 node。

然后在执行npm link,把项目链接到本地,就可以直接用命令行执行 yodao打印出你的conosole了

配置命令行选项

这里使用的是commander.js

yarn add commander --save
// index.js 中
program.option('-ig,--initgit', 'init git');
console.log('Options: ', program.opts()); // 可以得到选项值

第一个命令

/** 主程序 **/
const { program } = require('commander');

program
    // .command()用于配置命令及参数,其中<>表示参数是必须的,[]表示参数是可选的;
    .command('create <name> [destination]')
    // .description()添加命令描述
    .description('create a project')
    // .action()用于添加操作函数,入参就是配置命令时候的参数
    .action((name, destination) => {
        handleCreate({ name, destination }, program.opts());
    });

program.option('-ig,--initgit', 'init git');

program.parse(process.argv);

创建用户交互

这里用户交互使用的是 inquirer.js, 根据官网的描述, 我们想要使用require方法引入的话,就不能使用v9以上的inquirer,所以这里推荐安装v8.0.0

image.png

/** 用户交互 **/
const inquirer = require('inquirer')
const handleCreate = (params, options) => {
    console.log(55555, params, options);
    inquirer
        // 用户交互
        .prompt([
            {
                type: 'input',
                name: 'author',
                message: 'author name?'
            },
            {
                type: 'list',
                name: 'template',
                message: 'choose a template',
                choices: ['tpl-1', 'tpl-2']
            }
        ])
        .then((answers) => {
            //根据回答以及选项,参数来生成项目文件
            console.log(111, answers)
            genFiles({ ...answers, ...params, ...options });
        })
        .catch((error) => {
            console.error(error);
        });
};

这里面answers就是上面的回答,是个对象形式。比如我会获得{author: 'yodao2', template: 'tpl-1'} 拿到了用户选择的配置以后,我们就可以初始化项目文件

按需生成项目文件

这里用到了metalsmith和path, 可以 yarn add metalsmith path然后编写生成文件的逻辑

/** 按需生成项目文件 **/
// 获得命令运行时所在的路径
const getCwd = () => process.cwd();
// 这里用到Metalsmith,可以很方便地复制文件到指定目录
const Metalsmith = require('metalsmith');
const path = require('path');

const genFiles = (options) => {
    // 模版的目录 options.template就是上面用户交互的时候选择的模板文件夹
    const templateSrc = path.resolve(__dirname, `./template/${options.template}`);
    // 项目指定生成目录,如果命令中没有有配置目录,则在当前命令运行的目录下生成以项目名称为名字的新目录
    const destination = options.destination
        ? path.resolve(options.destination)
        : path.resolve(getCwd(), options.name);

    Metalsmith(__dirname)
        .source(templateSrc)
        .destination(destination)
        .build((err) => {
            if (err) {
                console.error(err);
            }
        });
};
// .source()和.destination()分别配置复制源目录和目标目录,最好使用绝对路径

在根目录新建目录及文件

+-- template 
| +-- tpl-1 
  | +-- index1.js
| +-- tpl-2 
  | +-- index2.js

然后就可以执行命令yodao create yodao1,一路选择下来就能看到youdao1文件夹已经创建在命令行指定的目录,里面含有你想要复制的指定文件夹里面里的文件。

模板语言替换

首先你要在声明需要替换的文件,如renderPathList所写的文件名,数组里可以有多个文件。 安装模板语言依赖yarn add ejs

其次你要替换的文件里需要有模板语,比如在上面,你声明了项目名称,和作者名,你要复制的package.json里需要用模板语言声明:

"name": "<%= name %>",
"author": "<%= author %>",

然后就是在destination和build过程中做替换行为

/** 按需生成项目文件 **/
// 获得命令运行时所在的路径
const getCwd = () => process.cwd();
// 这里用到Metalsmith,可以很方便地复制文件到指定目录
const Metalsmith = require('metalsmith');
const path = require('path');
const ejs = require('ejs');

const genFiles = (options) => {
    // 模版的目录 options.template就是上面用户交互的时候选择的模板文件夹
    const templateSrc = path.resolve(__dirname, `./template/${options.template}`);
    // 项目指定生成目录,如果命令中没有有配置目录,则在当前命令运行的目录下生成以项目名称为名字的新目录
    const destination = options.destination
        ? path.resolve(options.destination)
        : path.resolve(getCwd(), options.name);

// 需要动态生成的文件, 是个数据,可以填多个
    const renderPathList = ['package.json']

    /** 修改模板文件 **/

    Metalsmith(__dirname)
        .source(templateSrc)
        .destination(destination)
        .use((files) => {
            Object.keys(files).forEach((key) => {
                // 指定的文件动态生成
                if (renderPathList.includes(key)) {
                    const file = files[key];
                    // 原内容
                    const str = file.contents.toString();
                    // 新内容
                    const newContents = ejs.render(str, options);
                    // 将新内容写到文件中
                    file.contents = Buffer.from(newContents);
                }
            });
        })
        .build((err) => {
            if (err) {
                console.error(err);
            }
        });
};
// .source()和.destination()分别配置复制源目录和目标目录,最好使用绝对路径

上传

上传前记得切换回npm源

npm config set registry https://registry.npmjs.org
npm login // 登录你在npm的账号
npm publish // 上传, 非首次上传记得改版本号

如果想上传到npm组,需要强制public

npm publish --access public

使用

npm install -g yodaocli
youdaocli create <项目名>

拓展阅读