一步一步手写一个自己前端脚手架cli工具

------**前言**---------------------------

脚手架是为了保证各施工过程顺利进行而搭设的工作平台。我们使用脚手架可以快速生成项目节约时间,提升开发效率,比如egg、vue、react都有脚手架可以快速生成一个框架项目,且可以定制不同的选项。

~~~我们预计要设计的脚手架工具需要实现的功能可以:

  •  实现可以在命令行中直接运行代码 
  • 实现可以用npm安装 npm install lee-cli -g
  • 根据模板初始化项目 lee-cli create project-name
  • 模板库代码拉取
  • 初始化配置文件 lee-cli config set repo repo-name

一、必备知识、需要用到的模块:

用过vue-cli 或者react的脚手架的朋友,我们知道我们可以用脚手架快速生生一个相对性的vue或者react项目,创建步骤是例如vue:

~~~~~~来回忆、或者复习下,不知道的同学也可以看看,当然知道的可以跳过回忆贴直接到正式部分啊~~~~~~~~~~~~~~~~~~~~~~

因为代码执行在node环境,所以需要提前安装了node,因为我们是从github上拉取的代码,你还需要知道一些git的知识~~~

在命令行中输入下面:当然是基于安装了node环境的:

# 1)全局安装 ,可以在任意目录下执行 vue 这条命令
npm install -g vue-cli  
# 2)选择一个工作目录,并且用cmd进入该目录,创建你一个项目名称为 first-vue-project 的 vue 项目
vue create first-vue-project
# 运行vue项目
cd first-vue-project
npm run serve
复制代码

在按照上边过程安装时我们可以看到,首先我们的脚手架需要可以在命令行能够执行,能够解析用户输入的参数,(比如如果没有安装vue的脚手架直接在命令行输入vue是不能执行的,还有就是 能够解析 create参数)且在回车时,出现了选择如下图:有default\Manually select features 两个可以选择项,选择后回车会出现loading效果、小图标等,安装成功后可以看到图1-3的文件内容,


      (图1-1)


      (图1-2)


    (图1-3)

----2、而他们的脚手架的实现方式也是正式我们这篇文章所借鉴的,那都需要什么模块呢?请看以下模块:

  • commander :参数解析 --help其实就借助了他~ 解析用户输入的命令
  • inquirer :交互式命令行工具,有他就可以实现命令行的选择功能
  • download-git-repo :拉取GitHub上的文件
  • chalk :帮我们在控制台中画出各种各样的颜色
  • ora:小图标 (loading、succeed、warn等)
  • metalsmith :读取所有文件,实现模板渲染
  • consolidate :统一模板引擎

----3、应用的场景、应用的好处

  • 业务类型多
  • 多次造轮子,项目升级等问题
  • 公司代码规范,无法统一
    • 是统一各业务线的技术栈、制定规范,这样出现问题可以统一解决,升级和迭代都可以同步的进行
    • 提高效率,在统一的基础上,提供更多提高效率的工具,这个效率不只是开发效率,是从开发到上线的全流程的效率,比如一些业务组件的封装,提高上线效率的发布系统,各种utils等
    • 脚手架工具方便我们快速的clone代码库到本地,而且还可以更具有用户的选择,引入用户所需要的插件,具有工程化思想
    • 使用脚手架工具,我们只要只需要简单的命令就可以从代码库中下载项目到我的本地,不用每次git clone url链接地址(这个地址可能记不住),或者每次重开项目都要拷贝到其他文件夹

    -----**正式部分**--------------------------------

    来下边就开始手撸代码了,创建我们自己的脚手架辣~~按照步骤来:(有浅及深)零基础构建一个CLi工具,~~~全栈架构师必备技能^_^

    二、创建项目

    1、初始化项目

      初始化我们的项目,并安装我们需要的模块,我们可以一开始就安装所有上边所提到需要的模块,也可以在后边用到时在一个个安装

    创建一个自己的空文件夹(lee-cli)来存放我们的项目,首先需要:

    在当前目录命令行中按步骤输入下边的命令:

    npm init -y # 初始化package.json
    npm install eslint husky --save-dev # eslint是负责代码校验工作,husky提供了git钩子功能
    npx eslint --init # 初始化eslint配置文件复制代码

    2、自制脚手架的目录结构:

    ├── bin
    │   └── www  // 全局命令执行的根文件
    ├── src
    │   ├── main.js // 入口文件
    │   └── utils   // 存放工具方法
    |       |___constants.js  //  存放用户所需要的常量
    |       |___common.js
    │   ├── create.js // create 命令所有逻辑
    │   ├── config.js // config 命令所有逻辑
    │── .huskyrc    // git hook
    │── .eslintrc.json // 代码规范校
    ├── package.json
    |__ README.md 复制代码


    3、工程创建

    • 3.1、 bin\www 文件 全局命令执行的根文件
         内容:

    #!/usr/bin/env node     
     // 此文件是一个可执行文件
    console.log("这是我创建的一个文件,目录:/bin/www");复制代码

    ~~~~~~ps: 代码讲解及可能错误总结

     1)#!/usr/bin/env node -> 我要用系统中的这个目录/user/bin/env的node环境来执行此文件,且需要注意必须放在文件开头。


    • 3.2、 在package.json 中添加如下配置:
    如果我们想在命令行工具中执行lee-cli,可以执行我们bin/www这个文件怎么做呢?

    "bin": {
        "lee-cli": "./bin/www"
    }复制代码

    没有写上边的代码我们在命令工具是无法执行我们自己定义的命令的:


    (图2-3-1)

    ~~~~~~ps: 代码讲解及可能错误总结

    1)package.json中bin:内部命令对应的可执行文件的路径。很多包都有一个或多个可执行的文件希望被放到PATH中。(实际上,就是这个功能让npm可执行的)。上边的代码表示使用在命令工具使用命令lee-cli 会调用bin/www文件

    • 3.3、链接全局包:
    在进行上一步时,我们在命令行中输入lee-cli命令方向还是出现上图提示,怎么办?我们还需要执行下边命令:

    npm link复制代码


    (图2-3-1)

    ~~~~~~ps: 代码讲解及可能错误总结:

    npm link

    1)使用npm link ,link将一个任意位置的npm包链接到全局执行环境,从而在任意位置使用命令行都可以直接运行该npm包。 npm link命令通过链接目录和可执行文件,实现npm包命令的全局可执行

    2)我们可以看到上边的图2-3-1,有报错,就是因为bin/www中的文件原因是:

    (图2-3-2)(图2-3-3)

    所以一定注意#!/usr/bin/env node  在文件的开头不能有空格。

    #!/usr/bin/env node 
    
    console.log("这是我创建的一个文件,目录:/bin/www");复制代码

    3)在执行npm link 如下报错:

    (图2-3-4)

    是因为我们在建bin/www文件时建的文件时有扩展名的比如www.js,但是我们在package.json中的代码 还是: "bin": { "lee-cli": "./bin/www" } 改为"bin": { "lee-cli": "./bin/www.js" }既可以。

    4、脚手架相关命令行 参数

    bin/www.js 文件中引入main.js,www文件中使用main作为入口文件 require('../src/main.js');

    #!/usr/bin/env node
    // 此文件是一个可执行文件
     require('../src/main.js');复制代码

    4.1 使用commander

    api:www.npmjs.com/package/com…

    commander中文api:github.com/tj/commande…

    使用commander 使用commander会自动生成help,解析参数。 例如,我们使用vue的脚手架那样,vue-cli --help

    1)安装模块

    npm i commander复制代码

    2)src/main.js初步内容:

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

    (图2-4-1)

    如上图2-4-1所示,执行lee-cli --help已经有提示输出了,而且lee-cli -V是我们在代码中设置的内容。

    3)动态获取版本号

    第二步的version是我们写死的当前的cli的版本号,我们需要动态获取,并且为了方便我们将常量全部放到util下的constants.js文件中

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

    main.js

    const program = require('commander');
    const { version } = require('./utils/constants');
    program.version(version)
      .parse(process.argv); // process.argv就是用户在命令行中传入的参数复制代码

    ~~~~~~ps: 代码讲解及可能错误总结:

    1)关于:process.argv :想了解更多可以看相关链接:nodejs.cn/api/process…

    process.argv 属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数, 其中:

    •  数组的第一个元素process.argv[0]——返回启动Node.js进程的可执行文件所在的绝对路径 
    • 第二个元素process.argv[1]——为当前执行的JavaScript文件路径 
    • 剩余的元素为其他命令行参数


        4.1.2 配置脚手架命令参数

    在前言中我们有复习到在用vue-cli创建项目时,使用命令vue create first-vue-project 

    这个create参数是固定表示创建项目的,还可以使用其他的指令如vue config set k v

    那我们就来实现commander来实现吧!

    在utils/common.js中

    // 根据我们想要实现的功能配置执行动作,遍历产生对应的命令
    const mapActions = {
        create: {
            alias: 'c', //别名
            description: '创建一个项目', // 描述
            examples: [ //用法
                'lee-cli create <project-name>'
            ]
        },
        config: { //配置文件
            alias: 'conf', //别名
            description: 'config project variable', // 描述
            examples: [ //用法
                'lee-cli config set <k> <v>',
                'lee-cli config get <k>'
            ] 
       },
        '*': {
            alias: '', //别名
            description: 'command not found', // 描述
            examples: [] //用法
                }}
    module.exports = {
        mapActions
    };复制代码

    在main.js中 增加内容如下:

    const { mapActions } = require('./utils/common');
    // Object.keys()
    Reflect.ownKeys(mapActions).forEach((action)=>{
        program.command(action) //配置命令的名字
            .alias(mapActions[action].alias) // 命令的别名
            .description(mapActions[action].description) // 命令对应的描述
            .action(() => {  //动作
                if(action === '*'){  //访问不到对应的命令 就打印找不到命令
                    console.log(mapActions[action].description); 
               }else{
                    console.log(action);
                    // 分解命令 到文件里 有多少文件 就有多少配置 create config
                     // lee-cli create project-name ->[node,lee-cli,create,project-name]
                    console.log(process.argv);
                }
            })});
    
    program.version(version)
      .parse(process.argv); // process.argv就是用户在命令行中传入的参数复制代码

    输出:lee-cli create my

       (图2-4-2)

    我们在看看 lee-cli --help


    (图2-4-3)

    发现已经有了我们配置的create config * 指令。

    ~~~~~~ps: 代码讲解及可能错误总结:

    1)Reflect.ownKeys()类似Object.keys()的功能。静态方法 Reflect.ownKeys()返回一个由目标对象自身的属性键组成的数组。Reflect.ownKeys()可以返回包含Symbol属性在内的自有属性。Object.keys()返回属性key,但不包括不可枚举的属性。可参考:cloud.tencent.com/developer/s…

    2)子命令command,可以使用 .command 为你的最高层命令指定子命令。在之前的代码我们可以简化一个create代码来看,

    program.command('create') //配置命令的名字
            .alias('c') // 命令的别名
            .description('创建一个项目') // 命令对应的描述
            .action(() => {
                 console.log('此处为create子命令');
            })复制代码

    (图2-4-4) 

    4.1.3 监听--help

    监听 --help命令,打印帮助信息,在之前配置命令的代码中我们可以看到examples,这个就是在告诉我们这个子指令的用法,我们可以通过--help查看案例。


    (图2-4-5)

    在main.js中增加代码:

    // 监听用户的help事件
    program.on('--help', () => {
        console.log('\nExamples:');
        Reflect.ownKeys(mapActions).forEach((action) => {
            mapActions[action].examples.forEach((example) => {
                console.log(` ${example}`);
            })
        })})复制代码

    在执行 lee-cli --help 查看)

    (图2-4-6)

    4.1.4 具体create命令所做的事情

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


    图(2-4-7)

    4.1.4.1 如何实现create的主要作用呢?

    为命令绑定一个操作处理程序(action handler),或者将命令单独写成一个可执行文件。

    上图(2-4-7)中是main.js配置指令的代码,我们知道当我们输入lee-cli create my 命令时可以输出信息,动作在action里边,因为我们可以配置很多子命令比如create\config\init...所以我们将具体的动作分发到不同的文件中,通过读取不同的文件名来调用相对于的内容。

    在main.js中增加代码

    .action(() => {
                if (action === '*') {
     //访问不到对应的命令 就打印找不到命令
                    console.log(mapActions[action].description);
                } else {
                    console.log(action);
                    // 分解命令 到文件里 有多少文件 就有多少配置 create config 
                    // lee-cli create project-name ->[node,lee-cli,create,project-name]
                    console.log(process.argv);
                    require(path.join(__dirname,action))(...process.argv.slice(3));
                }
            }复制代码

    在create.js中增加代码:

    module.exports =  (projectName) => {
        console.log(`此处是文件${projectName}`);
    }复制代码


    图(2-4-8)

    ~~~~~~ps: 代码讲解及可能错误总结:

    1)require(path.join(__dirname,action))(...process.argv.slice(3));

    在action中我们引入了create.js, 并且将我们在命令行中输入的项目名传入到create.js中。process.argv.slice(3)在之前也讲过process.argv返回的是一个数组,从图(2-4-8)也可以看出了。执行lee-cli c my 打印出来了 此处是文件my

    4.1.4.2 拉取项目

    此时我们就需要将我们git上的项目或者其他云上的项目拉取下来,获取仓库中的所有模板信息,这里就以git为例。

    1)安装axio模块

    npm i axios复制代码

    2)在create.js中增加代码

    const axios = require('axios');
    // 1).获取仓库列表
    const fetchReopLists = async () => {
      // 获取当前组织中的所有仓库信息,这个仓库中存放的都是项目模板
      const { data } = await axios.get('https://api.github.com/orgs/lxy-cli/repos');  return data
    };
    
    module.exports = async (projectName) => {
      let repos = await fetchReopLists();
      repos = repos.map((item) => item.name);
      console.log(repos);
      console.log(`此处是文件${projectName}`);
    };复制代码



    ~~~~~~ps: 代码讲解及可能错误总结:

    1)const { data } = await axios.get('https://api.github.com/orgs/lxy-cli/repos');

    此处代码使用 axios.get()来调用gitHub的仓库中存放的代码,这个地址怎么找呢?可以通过api: developer.github.com/v3/  .因为我的项目仓库是一个组织中有两个仓库项目,可以在相应的api中找相应的地址规则。匹配api: GET /orgs/:org/repos

    4.2使用inquirer 和 ora 

    我们使用inquirer来增加选择,比如我的组织中有两个仓库,选择其中一个来拉取,我们vue脚手架的时候也是有选择的,使用ora这个包,它用来在终端展示loading的图标,开始时展示的提示语,成功时状态。

    4.2.1 使用inquirer 一个用户与命令行交互的工具

    可参考:www.npmjs.com/package/inq…

    1)安装inquirer模块

    npm i inquirer复制代码

    2)使用inquirer案例解释

    var inquirer = require('inquirer')
    inquirer.prompt([
      {
        type: 'confirm',
        name: 'test',
        message: '你确定使用这个吗?',
        default: true
      }
    ]).then((answers) => {
      console.log('结果为:')
      console.log(answers)
    })复制代码

    执行命令lee-cli c my结果为:


    (图2-4-11)

    上边的图2-4-11 我们可以看到出现了 ? 这是一个测试项目?(Y/n) 我们可以选择 Y出现先 ,这是因为我们的参数 type: 'confirm',


    (图2-4-12)

    我们在代码中输出 inquirer.prompt()结果是一个promise,上边是用的default,如果我们想选择不同的仓库可以用choice,type:'list',

    3)在我们项目中改为:create.js 代码增加

    const { repo} = await inquirer.prompt([
            {
                type: 'list',
                name:'repo',
                message:'请选择一个你要创建的项目',
                choices: repos
            }
        ]);
    console.log(`我现在选择了那个仓库? ${repo}`);复制代码

    执行命令lee-cli c my结果为:


    (图2-4-13)

    上图中我们可以看到有多出现了 请选一个你要创建的项目,下边为我们的仓库列表通过上下键选择一个回车选中


    (图2-4-14)

    ~~~~~~ps: 代码讲解及可能错误总结:

    1) inquirer.prompt(参数):

     {
       // 表示提问的类型,下文会单独解释
       type: String, 
       // 在最后获取到的answers回答对象中,作为当前这个问题的键
       name: String, 
       // 打印出来的问题标题,如果为函数的话
       message: String|Function, 
       // 用户不输入回答时,问题的默认值。或者使用函数来return一个默认值。
       //假如为函数时,函数第一个参数为当前问题的输入答案。
       default: String|Number|Array|Function,
       // 给出一个选择的列表,假如是一个函数的话,第一个参数为当前问题的输入答案。
       //为数组时,数组的每个元素可以为基本类型中的值。 
       choices: Array|Function, 
      // 接受用户输入,并且当值合法时,函数返回true。当函数返回false时,
      //一个默认的错误信息会被提供给用户。
       validate: Function, 
      // 接受用户输入并且将值转化后返回填充入最后的answers对象内。
       filter: Function, 
    // 接受当前用户输入的answers对象,并且通过返回true或者false来决定是否当前的问题应该去问。
    //也可以是简单类型的值。
       when: Function|Boolean, 
    // 改变渲染list,rawlist,expand或者checkbox时的行数的长度。
       pageSize: Number, }复制代码

    4.2.2 使用ora 

    我们在下载gitHub仓库中的项目时,需要一定时间,如果没有loading效果时,我们会常常认为是出错或者什么原因,而进行错误动作,增加效果可以知道是正在下载。。。

    可参考:www.npmjs.com/package/ora

    1)安装模块

    npm i ora 复制代码

    2)ora相关代码案例

        const spinner = ora('Loading 测试中哈哈哈。。。').start();
        setTimeout(() => {
            spinner.color = 'red';
            spinner.text = 'Loading ora哈哈哈';
            // 成功
            spinner.succeed('拉取成功');
        }, 1000);复制代码

    (图2-4-15)(图2-4-16)

    3)在utils/common.js中增加相关代码

     const ora = require('ora');
    // 封装loading效果
     const fnLoadingByOra = async (fn, message) => {
        const spinner = ora(message);
        spinner.start();
        let result = await fn();
        spinner.succeed(); // 结束loading
        return result; }
    module.exports = {
        mapActions,
        fnLoadingByOra
    };复制代码

    4)改变create.js相关代码

    const { fnLoadingByOra } = require('./utils/common');
    module.exports =  async (projectName) => {
        let repos = await fnLoadingByOra(fetchReopLists, '正在链接你的仓库...');
        repos = repos.map((item) => item.name);
        // 使用inquirer 在命令行中可以交互
        const { repo} = await inquirer.prompt([
            {
                type: 'list',
                name:'repo',
                message:'请选择一个你要创建的项目',
                choices: repos 
           }
        ]);
        console.log(`我现在选择了那个仓库? ${repo}`);}复制代码


    (图2-4-17)

    4.3 获取版本信息

    我们在下载仓库时会有不同版本,比如你下载vue的时候也有1.0 、2.0等,所以我们需要或者仓库的不同版本来下载你所需要的版本。

    1)在utils/common.js中增加更改信息

     // 封装loading效果
     const fnLoadingByOra = (fn, message) => async (...argv) =>{
        const spinner = ora(message);
        spinner.start();
        let result = await fn(...argv);
        spinner.succeed(); // 结束loading
        return result; }//  获取仓库(repo)的版本号信息
    const getTagLists =  async (repo) =>{
       const {data} = await axios.get(`https://api.github.com/repos/lxy-cli/${repo}/tags`);
       return data;
    }
    module.exports = {
        mapActions,
        fnLoadingByOra,
        fetchReopLists,
        getTagLists
    };复制代码

    2)在create.js

    const inquirer = require('inquirer');
    const {    fnLoadingByOra,    fetchReopLists,    getTagLists} = require('./utils/common');
    module.exports =  async (projectName) => {
        let repos = await fnLoadingByOra(fetchReopLists, '正在链接你的仓库...')();
        repos = repos.map((item) => item.name);
        // 使用inquirer 在命令行中可以交互
        const { repo} = await inquirer.prompt([
            {
                type: 'list',
                name:'repo',
                message:'请选择一个你要创建的项目',
                choices: repos
            }
        ]);
        let tags = await fnLoadingByOra(getTagLists, `正在链接你的选择的仓库${repo}的版本号...`)(repo);
        tags = tags.map((item) => item.name);
        console.log(`我现在选择了那个仓库? ${repo}`);
        console.log(`仓库 ${repo}的版本信息列表:${tags}`);
    }复制代码


    (图2-4-18)

    3)增加选择版本信息

    在create.js

        let tags = await fnLoadingByOra(getTagLists, `正在链接你的选择的仓库${repo}的版本号...`)(repo);
        tags = tags.map((item) => item.name);
        const { tag } = await inquirer.prompt([{
            type: 'list',
            name: 'tag',
            message: '请选择一个该项目的版本下载',
            choices: tags
        }]);
        console.log(`我现在选择了那个仓库? ${repo}`);
        console.log(`仓库 ${repo}的版本信息列表:${tag}`);复制代码


    (图2-4-19)


    (图2-4-20)

    4.4 从GitHub下载项目到临时文件夹中

    我们需要使用模块download-git-repo真正的从GitHub上下载到本地临时文件夹中,需注意不同系统的地址环境不同
    1) 安装模块

    npm i download-git-repo复制代码

    2) 我们需要下载到本地的地址

    下载前先找个临时目录来存放下载的文件,来存放,以备后期使用,这样的好处是,如果我们之前下载这个版本的项目可以直接从这个存放的地址拿来(相当于缓存),如果项目中更新我们将临时目录中的文件覆盖也可以。

    2.1)在utils/constants.js 增加常量

     下载临时文件存放地址 因为不同的电脑平台临时存放地址不同

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

    const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']}/.myTemplate`;
    console.log(downloadDirectory);
    module.exports = {
        name,
        version,
        downloadDirectory
    };复制代码

    ~~~~~~ps: 代码讲解及可能错误总结:

    1)process.platform

    process.platform:列举node运行的操作系统的环境,只会显示内核相关的信息,如:linux2, darwin,而不是“Redhat ES3” ,“Windows 7”,“OSX 10.7”等。

    比如小编使用的电脑:


    (图2-4-21)

    2.2)获取用户目录

    根据对应的环境变量获取到用户目录,并且下载到临时文件夹中

    在utils\common.js中增加代码

    const { promisify } = require('util');
    const downloadGit = require('download-git-repo');downloadGit = promisify(downloadGit);// 将项目下载到当前用户的临时文件夹下
    const downDir = async (repo,tag)=>{
        console.log(tag, 'downDir方法');
       let project = `lxy-cli/${repo}`; //下载的项目
       if(tag){
          project += `#${tag}`;
       }
        //     c:/users/lee/.myTemplate
       let dest = `${downloadDirectory}/${repo}`;
     //把项目下载当对应的目录中
       console.log(dest, 'dest的内容。。。。。。。。。。');
       console.log(project, 'dest的内容。。。。。。。。。。');
       try {
            await downloadGit(project, dest);
         } catch (error) {
             console.log('错误了吗???\n');
             console.log(error);
         }
          return dest;
    }复制代码

    在create.js中增加代码并引入

    // 下载项目到临时文件夹 C:\Users\lee\.myTemplate
        const target = await fnLoadingByOra(downDir, '下载项目中...')(repo, tag);复制代码


    (图2-4-22)

    根据图2-4-22我们可以看出来当我们执行lee-cli c my 创建项目时,选择了vue-tempalte,选择下载版本v1.0我还我们下载到临时文件夹目录是:下载项目到临时文件夹 C:\Users\lee\.myTemplate 所以我们可以找到地址:C:\Users\lee\.myTempalte\vue-tempalte 打开可以看到下图(图2-4-23):对应我们github地址上的内容是一样的,如图(图2-4-24)


    (图2-4-23)


    (图2-4-24)

    ~~~~~~ps: 代码讲解及可能错误总结:

    1) promisify(downloadGit);

    这句代码我们知道是将downloadGit改为promise,因为download-git-repo不是promise,而我们在项目中都用async await需要我们自己包装为promise。

    4.5 将临时文件夹的项目复制到我们需要的目录

    此处需要使用模块ncp,安装ncp可以实现文件的拷贝功能

    1)安装模块

    npm i ncp复制代码

    2)在utils\common.js增加方法

    // 复制项目从临时文件到本地工作项目
    const copyTempToLoclhost = async (target, projectName) => {
            const resolvePath = path.join(path.resolve(), projectName);
            // 此处模拟如果仓库中有ask.js就表示是复杂的仓库项目
            if (!fs.existsSync(path.join(target, 'ask.js'))) {
                await ncp(target, resolvePath);
                fse.remove(target);
            }else{
                //复杂项目
                 // 1) 让用户填信息
                 await new Promise((resolve, reject) => {
                     MetalSmith(__dirname)
                         .source(target) // 遍历下载的目录
                         .destination(resolvePath) // 最终编译好的文件存放位置
                         .use(async (files, metal, done) => {
                             let args = require(path.join(target, 'ask.js'));
                             let res = await inquirer.prompt(args);
                             let met = metal.metadata();
                             // 将询问的结果放到metadata中保证在下一个中间件中可以获取到
                             Object.assign(met, res);
                            //  ask.js 只是用于 判断是否是复杂项目 且 内容可以定制复制到本地不需要
                             delete files['ask.js'];
                             done();
                         })
                         .use((files, metal, done) => {
                             const res = metal.metadata();
                            //  获取文件中的内容
                             Reflect.ownKeys(files).forEach(async (file) => {
                                //  文件是.js或者.json才是模板引擎
                                 if (file.includes('.js') || file.includes('.json')) {
                                     let content = files[file].contents.toString(); //文件内容
                                    //  我们将ejs模板引擎的内容找到 才编译
                                     if (content.includes('<%')) {
                                         content = await render(content, res);
                                         files[file].contents = Buffer.from(content); //渲染
                                     }
                                 }
                             })
                             done();
    
                         })
                         .build((err) => {
                             if (err) {
                                 reject();
    
                             } else {
                                 resolve();
                             }
                         })
    
                 });
    
            }
    }复制代码

    在create.js文件中增加代码

    await copyTempToLoclhost(target, projectName);复制代码


    (图2-4-25)

    我们跟上边一样的选择命令发现图2-4-25和图2-4-24、图2-4-23内容相同。

    4.6 编译模板

    当我们下载的模板是需要用户是需要选择定制的项目时,还需要编译模板

    因为此情况有很多种形式,我们在此模拟了一个需要用户在命令行选择用户输入内容的特定的复杂模板。在其中一个仓库中存放一个ask.js中放入可以用inquirer模块执行的命令行参数,来询问生成相应的package.json文件,因为里边用到ejs

    1)安装模块

    npm i metalsmith ejs consolidate复制代码

    metalsmith用于:www.npmjs.com/package/met…
    consolidate是一个模板引擎的结合体。包括了常用的jade和ejs。www.npmjs.com/package/con…

    2)代码

    utils\common.js增加相关代码

    // 复制项目从临时文件到本地工作项目
    const copyTempToLoclhost = async (target, projectName) => {
            const resolvePath = path.join(path.resolve(), projectName);
            // 此处模拟如果仓库中有ask.js就表示是复杂的仓库项目
            if (!fs.existsSync(path.join(target, 'ask.js'))) {
                await ncp(target, resolvePath);
                fse.remove(target);
            }else{
                //复杂项目
                 // 1) 让用户填信息
                 await new Promise((resolve, reject) => {
                     MetalSmith(__dirname)
                         .source(target) // 遍历下载的目录
                         .destination(resolvePath) // 最终编译好的文件存放位置
                         .use(async (files, metal, done) => {
                             let args = require(path.join(target, 'ask.js'));
                             let res = await inquirer.prompt(args);
                             let met = metal.metadata();
                             // 将询问的结果放到metadata中保证在下一个中间件中可以获取到
                             Object.assign(met, res);
                            //  ask.js 只是用于 判断是否是复杂项目 且 内容可以定制复制到本地不需要
                             delete files['ask.js'];
                             done();
                         })
                         .use((files, metal, done) => {
                             const res = metal.metadata();
                             Reflect.ownKeys(files).forEach(async (file) => {
                                 if (file.includes('.js') || file.includes('.json')) {
                                     const content = files[file].contents.toString(); //文件内容
                                     if (content.includes('<%')) {
                                         content = await render(content, res);
                                         files[file].contents = Buffer.from(content); //渲染
                                     }
                                 }
                             })
                             done();
                         })
                         .build((err) => {
                             if (err) {
                                 reject();
                             } else {
                                 resolve();
                             }
                         })
                 });
            }}
    
    复制代码

    create.js增加代码

    copyTempToLoclhost(filePath, projectName);复制代码


    (图2-4-26)


    (图2-4-27)


    (图2-4-28)

    总小结:

    至此呢,使用lee-cli create <project-name> 初步完成了。当然还需要很多要优化的内容,比如我们在下载的项目中项目名重复,代码会被重复添加。还有很多请求错误没有监控。请求的仓库路径是写死的,最好是也可以通过命令行选择下载不同的项目、组织,还不完全是一个通用的脚手架工具。

    参考价值:

    本代码中是针对自己的https://api.github.com/orgs/lxy-cli/repos的组织写的自定义的脚手架,参考价值是代码的思路分析,可将里边的链接地址更换为自己的,如果是简单的项目拷贝只需要更改链接地址,如果是负责的项目需要定制的话可以根据自己的逻辑修改,本项目中的src/utils/common.js 中的方法 copyTempToLoclhost 里边的复杂方法也是针对自己的项目编写的。当然后续还会优化的^_^^_^^_^


    5.项目发布

    我们的初步写了一个指令的脚手架那需要发布到npm上

    #当前项目下 切换到官网上
    nrm use npm
    
    npm addUser 或者 npm login #登录账号
    npm publish  #上传项目复制代码


    来让我们检验一下吧

    ----安装测试 

    npm unlink
    lee-cli
    npm i lee-cli -g复制代码


    后续再增加其他的指令,敬请期待。。。。

    想看完整代码:戳此github.com/lixiaoyanle…


    分类:
    前端