手把手從0到1實現一個web工程通用腳手架工具

xmanlin發表於2021-10-18

前言

前端工程化是人們常常提到的東西,其目的基本上都是為了提高開發效率,降低成本以及保證質量。而腳手架工具則是前端工程化中很重要的環節,一個好用的web工程通用腳手架工具可以在很大程度上做到上面所提到的。

我們不僅要會用市面上很多成熟的腳手架,還要能根據實際的專案情況,去實現一些適合自己專案的腳手架。本文就將和大家一起實現一個基礎的通用腳手架工具,後續就可以隨意擴充了。

專案結構

專案的整體結構如下,後面我們會一步步編寫程式碼,最終實現整個腳手架工具。

xman-cli
├─ bin
│  └─ xman.js
├─ command
│  ├─ add.js
│  ├─ delete.js
│  ├─ init.js
│  └─ list.js
├─ lib
│  ├─ remove.js
│  └─ update.js
├─ .gitignore
├─ LICENSE
├─ package.json
├─ README.md
└─ templates.json

具體實現

初始化專案

可以用 npm init 進行建立,也可以根據下面列出的 package.json 進行修改。

{
  "name": "xman-cli",
  "version": "1.0.0",
  "description": "web通用腳手架工具",
  "bin": {
    "xman": "bin/xman.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/XmanLin/xman-cli.git"
  },
  "keywords": [
    "cli"
  ],
  "author": "xmanlin",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/XmanLin/xman-cli/issues"
  },
  "homepage": "https://github.com/XmanLin/xman-cli#readme",
  "dependencies": {
    "chalk": "^4.1.2",
    "clear": "^0.1.0",
    "clui": "^0.3.6",
    "commander": "^8.2.0",
    "figlet": "^1.5.2",
    "handlebars": "^4.7.7",
    "inquirer": "^8.1.5",
    "update-notifier": "^5.1.0"
  }
}

這裡提兩點:

  • bin欄位:可以自定義腳手架工具的命令,例如上面的xman,而xman後面的就是命令的執行指令碼。
  • 專案中的依賴後面會用到,用到的時候會介紹。

編寫bin/xman.js

要使得指令碼可執行,就需要在xman.js的最頂部新增以下程式碼:

#!/usr/bin/env node

編寫好後引入commander(node.js命令列介面的完整解決方案),可以點選連結或者到npm官網檢視具體API的用法,後面一些列的相關依賴都一樣。

#!/usr/bin/env node

const { program } = require('commander');

此時,我們可以定義當前腳手架的版本以及版本檢視的命令。

#!/usr/bin/env node

const { program } = require('commander');

program
    .version(require('../package').version, '-v, --version');
    
program.parse(process.argv); // 這裡是必要的

if (!program.args.length) {
    program.help();
}

在當前xman-cli目錄下,執行 npm link 後,就可以在本地對腳手架工具進行除錯了。

然後在當前目錄下執行

xman -v

就能看到我們定義的版本號了,也證明腳手架工具初步搭建成功。

版本顯示.png

利用腳手架工具初始化搭建專案

這個是腳手架工具的最核心的功能點,通過腳手架工具命令快速選擇拉取,事先在git倉庫中構建好基礎專案模板。我們可以根據實際需求,自定義專案模板,並在專案中制定相關的開發規範和約定。

首先在git上搭建好自己的基礎專案,這裡需要注意的是:在搭建基礎專案模板的時候,專案的 package.json中的 name 欄位要寫成下面這種形式:

{
    "name": "{{name}}",
}

至於為什麼要這樣寫,後面的程式碼中會有體現。

然後在根目錄下建立 templates.json:

{
    "templates": {
        "xman-manage": {
            "url": "https://github.com/XmanLin/xman-manage.git",
            "branch": "master"
        },
        "xman-web": {
            "url": "https://github.com/XmanLin/xman-web.git",
            "branch": "master"
        }
    }
}

以上 xman-managexman-web 分別代表不同的專案,可以根據實際情況自定義,url 為基礎專案的地址, branch為自動拉取時的分支。

接著在command資料夾(這個資料夾下會放後續一些列命令的實現邏輯)下建立init.js:

const fs = require('fs'); // node.js檔案系統
const exec = require('child_process').exec; // 啟動一個新程式,用來執行命令
const config = require('../templates'); // 引入定義好的基礎專案列表
const chalk = require('chalk'); // 給提示語新增色彩
const clear = require('clear'); // 清除命令
const figlet = require('figlet'); // 可以用來定製CLI執行時的頭部
const inquirer = require('inquirer'); // 提供互動式命令列
const handlebars = require('handlebars'); // 一種簡單的模板語言,可以自行百度一下
const clui = require('clui'); // 提供等待的狀態
const Spinner = clui.Spinner;
const status = new Spinner('正在下載...');
const removeDir = require('../lib/remove'); // 用來刪除檔案和資料夾

module.exports = () => {
    let gitUrl;
    let branch;
    clear();
    // 定製酷炫CLI頭部
    console.log(chalk.yellow(figlet.textSync('XMAN-CLI', {
        horizontalLayout: 'full'
    })));
    inquirer.prompt([
        {
            name: 'templateName',
            type: 'list',
            message: '請選擇你需要的專案模板:',
            choices: Object.keys(config.templates),
        },
        {
            name: 'projectName',
            type: 'input',
            message: '請輸入你的專案名稱:',
            validate: function (value) {
                if (value.length) {
                    return true;
                } else {
                    return '請輸入你的專案名稱';
                }
            },
        }
    ])
    .then(answers => {
        gitUrl = config.templates[answers.templateName].url;
        branch = config.templates[answers.templateName].branch;
        // 執行的命令,從git上克隆想要的專案模板
        let cmdStr = `git clone ${gitUrl} ${answers.projectName} && cd ${answers.projectName} && git checkout ${branch}`;
        status.start();
        exec(cmdStr, (error, stdou, stderr) => {
            status.stop();
            if (error) {
                console.log('發生了一個錯誤:', chalk.red(JSON.stringify(error)));
                process.exit();
            }
            const meta = {
                name: answers.projectName
            };
            // 這裡需要注意:專案模板的 package.json 中的 name 要寫成 "name": "{{name}}"的形式
            const content = fs.readFileSync(`${answers.projectName}/package.json`).toString();
            // 利用handlebars.compile來進行 {{name}} 的填寫 
            const result = handlebars.compile(content)(meta);
            fs.writeFileSync(`${answers.projectName}/package.json`, result);
            // 刪除模板自帶的 .git 檔案
            removeDir(`${answers.projectName}/.git`);
            console.log(chalk.green('\n √ 下載完成!'));
            console.log(chalk.cyan(`\n cd ${answers.projectName} && yarn \n`));
            process.exit();
        })
    })
    .catch(error => {
        console.log(error);
        console.log('發生了一個錯誤:', chalk.red(JSON.stringify(error)));
        process.exit();
    });
}

lib/remove.js

const fs = require('fs');
let path = require('path');

function removeDir(dir) {
    let files = fs.readdirSync(dir); //返回一個包含“指定目錄下所有檔名稱”的陣列物件
    for (var i = 0; i < files.length; i++) {
        let newPath = path.join(dir, files[i]);
        let stat = fs.statSync(newPath); // 獲取fs.Stats 物件
        if (stat.isDirectory()) {
            //判斷是否是資料夾,如果是資料夾就遞迴下去
            removeDir(newPath);
        } else {
            //刪除檔案
            fs.unlinkSync(newPath);
        }
    }
    fs.rmdirSync(dir); //如果資料夾是空的,就將自己刪除掉
};

module.exports = removeDir;

最後繼續在 xman.js 定義命令:

#!/usr/bin/env node

const { program } = require('commander');

...
    
program
    .command('init')
    .description('Generate a new project')
    .alias('i')
    .action(() => {
        require('../command/init')()
    });
    

...

隨便再找個資料夾下執行定義好的命令:

xman i

初始化專案.gif

開啟我們下載好的模板專案看看:

初始化後的demo.png

通過命令新增專案模板配置

現在我們能夠通過命令拉取構建專案了,但是如果以後有了新的專案模板了怎麼辦?難道每次都是手動去修改 templates.json 嗎。這當然是不合理的,所以接下來我們要實現通過命令新增專案模板。

首先在git倉庫裡面新建一個專案模板,隨便叫什麼,我這裡叫 xman-mobile ,然後開始編寫專案模板新增的邏輯和命令,新建command/add.js:

const config = require('../templates.json');
const chalk = require('chalk');
const fs = require('fs');
const inquirer = require('inquirer');
const clear = require('clear');

module.exports = () => {
    clear();
    inquirer.prompt([
        {
            name: 'templateName',
            type: 'input',
            message: '請輸入模板名稱:',
            validate: function (value) {
                if (value.length) {
                    if (config.templates[value]) {
                        return '模板已存在,請重新輸入';
                    } else {
                        return true;
                    }
                } else {
                    return '請輸入模板名稱';
                }
            },
        },
        {
            name: 'gitLink',
            type: 'input',
            message: '請輸入 Git https link:',
            validate: function (value) {
                if (value.length) {
                    return true;
                } else {
                    return '請輸入 Git https link';
                }
            },
        },
        {
            name: 'branch',
            type: 'input',
            message: '請輸入分支名稱:',
            validate: function (value) {
                if (value.length) {
                    return true;
                } else {
                    return '請輸入分支名稱';
                }
            },
        }
    ])
    .then(res => {
        config.templates[res.templateName] = {};
        config.templates[res.templateName]['url'] = res.gitLink.replace(/[\u0000-\u0019]/g, ''); // 過濾unicode字元
        config.templates[res.templateName]['branch'] = res.branch;
        fs.writeFile(__dirname + '/../templates.json', JSON.stringify(config), 'utf-8', (err) => {
            if (err) {
                console.log(err);
            } else {
                console.log(chalk.green('新模板新增成功!\n'));
            }
            process.exit();
        })
    })
    .catch(error => {
        console.log(error);
        console.log('發生了一個錯誤:', chalk.red(JSON.stringify(error)));
        process.exit();
    });
}

繼續在bin/xman.js中新增命令

#!/usr/bin/env node

const { program } = require('commander');

...

program
    .command('add')
    .description('Add a new template')
    .alias('a')
    .action(() => {
        require('../command/add')()
    });
    
...

執行 npm link --force ,然後再執行配置好的命令 xman a:

新增模板.gif

可以看到 templates.json 中,新的模板資訊已經被新增上了。

通過命令刪除專案模板配置

既然有新增,那就肯定有刪除命令了。同樣,新建command/delete.js:

const fs = require('fs');
const config = require('../templates');
const chalk = require('chalk');
const inquirer = require('inquirer');
const clear = require('clear');

module.exports = () => {
    clear();
    inquirer.prompt([
        {
            name: 'templateName',
            type: 'input',
            message: '請輸入要刪除的模板名稱:',
            validate: function (value) {
                if (value.length) {
                    if (!config.templates[value]) {
                        return '模板不存在,請重新輸入';
                    } else {
                        return true;
                    }
                } else {
                    return '請輸入要刪除的模板名稱';
                }
            },
        }
    ])
    .then(res => {
        config.templates[res.templateName] = undefined;
        fs.writeFile(__dirname + '/../templates.json', JSON.stringify(config), 'utf-8', (err) => {
            if (err) {
                console.log(err);
            } else {
                console.log(chalk.green('模板已刪除!'));
            }
            process.exit();
        });
    })
    .catch(error => {
        console.log(error);
        console.log('發生了一個錯誤:', chalk.red(JSON.stringify(error)));
        process.exit();
    });
}

繼續新增命令:

#!/usr/bin/env node

const { program } = require('commander');

...

program
    .command('delete')
    .description('Delete a template')
    .alias('d')
    .action(() => {
        require('../command/delete')()
    });

...

執行 npm link --force ,然後再執行配置好的命令 xman d。檢視 templates.json ,我們已經刪除了想要刪除的模板資訊。

通過命令快速檢視已有模板

一般來說我們不可能記住已經新增的所有模板,有時候需要去快速檢視。所以接下來我們將要實現一個簡單的快速檢視模板列表的命令:

新建command/list.js

const config = require('../templates');
const chalk = require('chalk');

module.exports = () => {
    let str = '';
    Object.keys(config.templates).forEach((item, index, array) => {
        if (index === array.length - 1) {
            str += item;
        } else {
            str += `${item} \n`;
        }
    });
    console.log(chalk.cyan(str));
    process.exit();

}

新增命令:

#!/usr/bin/env node

const { program } = require('commander');

...

program
    .command('list')
    .description('show temlpate list')
    .alias('l')
    .action(() => {
        require('../command/list')()
    });

...

執行 npm link --force ,然後再執行配置好的命令 xman l:

檢視模板列表.gif

通過命令檢查CLI版本是否是最新版本

一個通用的腳手架工具肯定不是自己一個人用的,使用的人可能需要知道CLI是不是有最新版本,所以也需要有檢查CLI版本的功能。

新建 bin/update.js:

const updateNotifier = require('update-notifier');  // 更新CLI應用程式的通知
const chalk = require('chalk');
const pkg = require('../package.json');

const notifier = updateNotifier({
    pkg,
    updateCheckInterval: 1000 * 60 * 60, // 預設為 1000 * 60 * 60 * 24(1 天)
})

function updateChk() {
    if (notifier.update) {
        console.log(`有新版本可用:${chalk.cyan(notifier.update.latest)},建議您在使用前進行更新`);
        notifier.notify();
    } else {
        console.log(chalk.cyan('已經是最新版本'));
    }
};

module.exports = updateChk;

新增命令:

#!/usr/bin/env node

const { program } = require('commander');

...

program
    .command('upgrade')
    .description("Check the js-plugin-cli version.")
    .alias('u')
    .action(() => {
        updateChk();
    });

...

執行 npm link --force ,然後再執行配置好的命令 xman u:

檢查版本.gif

到此,我們已經實現了一個基礎但很完整的web工程通用腳手架工具。大家可以根據自己的實際需求進行修改和擴充了。

總結

一個web工程通用腳手架的本質作用其實就是以下幾點:

  • 快速的建立基礎專案結構;
  • 提供專案開發的規範和約定;
  • 根據實際專案需求,定製不同的功能,來提高我們的效率。

相關文章