一步一步手寫一個本身前端腳手架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…

    相關文章
    相關標籤/搜索