一步一步手写一个本身前端脚手架cli

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

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

1、必备知识、须要用到的模块:

用过vue-cli 或者react的脚手架的朋友,咱们知道咱们能够用脚手架快速生生一个相对性的vue或者react项目,建立步骤是例如vue:html

~~~~~~来回忆、或者复习下,不知道的同窗也能够看看,固然知道的能够跳过回忆贴直接到正式部分啊~~~~~~~~~~~~~~~~~~~~~~vue

在命令行中输入下面:固然是基于安装了node环境的: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的文件内容,react


      (图1-1)linux


      (图1-2)ios


    (图1-3)git


~~~~~~综合以上,咱们预计要设计的脚手架须要实现的功能能够:

  • 咱们能在命令行工具中 --实现能够在命令行中直接运行代码 
  • 根据模板初始化项目 lee-cli create project-name
  • 初始化配置文件 lee-cli config set repo repo-name
  • 实现能够用npm安装 npm install lee-cli -g

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

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

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

  • 业务类型多
  • 屡次造轮子,项目升级等问题
  • 公司代码规范,没法统一
    • 是统一各业务线的技术栈、制定规范,这样出现问题能够统一解决,升级和迭代均可以同步的进行
    • 提升效率,在统一的基础上,提供更多提升效率的工具,这个效率不仅是开发效率,是从开发到上线的全流程的效率,好比一些业务组件的封装,提升上线效率的发布系统,各类utils等

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

    来下边就开始手撸代码了,建立咱们本身的脚手架辣~~按照步骤来:(有浅及深)github

    2、建立项目

    一、初始化项目

      初始化咱们的项目,并安装咱们须要的模块,咱们能够一开始就安装全部上边所提到须要的模块,也能够在后边用到时在一个个安装

    建立一个本身的空文件夹(lee-cli)来存放咱们的项目,首先须要:vue-cli

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

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

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

    ├── 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 复制代码


    三、工程建立

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

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

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

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


    •  在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文件

    • 连接全局包:
    在进行上一步时,咱们在命令行中输入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" }既能够。

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

    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 fetchRepoList = async () => {
      // 获取当前组织中的全部仓库信息,这个仓库中存放的都是项目模板
      const { data } = await axios.get('https://api.github.com/orgs/lxy-cli/repos');  return data
    };
    
    module.exports = async (projectName) => {
      let repos = await fetchRepoList();
      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-2四、图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> 初步完成了。固然还须要不少要优化的内容,好比咱们在下载的项目中项目名重复,代码会被重复添加。还有不少请求错误没有监控。请求的仓库路径是写死的对好是也能够经过命令行输入拉取。


    5.项目发布

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

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


    来让咱们检验一下吧

    ----安装测试 

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


    后续再增长其余的指令,敬请期待。。。。

    github.com/lixiaoyanle…

    相关文章
    相关标签/搜索