如何創(chuàng)建屬于自己的腳手架

原理

我們每次搭建項(xiàng)目都需要?jiǎng)?chuàng)建項(xiàng)目廊勃,然后進(jìn)行一系列的基礎(chǔ)配置懈贺,封裝基礎(chǔ)api ,配置webpack,浪費(fèi)了很多時(shí)間和精力坡垫,用自己封裝的腳手架模板就可以每次像創(chuàng)建vue,react那樣一樣簡(jiǎn)單梭灿,有能力的還可以去封裝自己的框架。
參考自beleve666大神的文章
源碼查看github

原理就:利用npm封裝插件冰悠,拉取我們?cè)趃ithub上托管的項(xiàng)目堡妒,拉取完畢后把依賴(lài)的.git .svn刪除掉 并用node.js更改json參數(shù)

1. 先上插件依賴(lài)

commander:完整的 node.js 命令行解決方案
download-git-repo:用于下載,git倉(cāng)庫(kù)拉取
inquirer:用戶(hù)與命令行交互的工具
ora:實(shí)現(xiàn)node.js 命令行環(huán)境的 loading效果溉卓, 和顯示各種狀態(tài)的圖標(biāo)等
log-symbols:提供帶顏色的符號(hào)
Chalk:改變文字背景顏色
node-notifier:通知插件
shelljs:可以調(diào)用命令行工具

2.先搭一個(gè)基礎(chǔ)npm包

npm包發(fā)布教程 這里就不多說(shuō)了

3.先創(chuàng)建基礎(chǔ)命令

我們需要將我們的項(xiàng)目做下改動(dòng)皮迟,首先在packge.json中添加如下內(nèi)容:

 "bin": {
        "template-ma": "index.js"
    },

"template-ma"是你自定義的命令,到這里還不能使用桑寨,必須使用npm link 鏈接到全局

3.創(chuàng)建入口文件

Index.js文件

#!/user/bin/env node
//你想要你的這個(gè)文件中的代碼用什么可執(zhí)行程序去運(yùn)行它
//!/usr/bin/env node會(huì)去環(huán)境設(shè)置尋找node目錄
const commander = require('commander');//引入插件
commander.command('init <name>') // 定義init子命令伏尼,<name>為必需參數(shù)可在action的function中接收,如需設(shè)置非必需參數(shù)尉尾,可使用中括號(hào)
    .option('-d, --dev', '獲取開(kāi)發(fā)版') // 配置參數(shù)爆阶,簡(jiǎn)寫(xiě)和全寫(xiě)中使用,分割
    .description('創(chuàng)建項(xiàng)目') // 命令描述說(shuō)明
    .action(function(name,option)){ 
          console.log(name,option)
        //這里可以對(duì)參數(shù)進(jìn)行操作 
      }
//這里通過(guò) command定義Init 子命令  當(dāng)使用 init <name> 的時(shí)候 name 會(huì)在action中接收,
// option中 是傳遞配置參數(shù)沙咏,也在action中接收 
//action接收一個(gè)回調(diào) action(function(name,option)){ 這里可以對(duì)參數(shù)進(jìn)行操作 }

//查詢(xún)版本號(hào)
commander.version(require('./package.json').version,'-v,-V,--version', '查看版本號(hào)')  

//這句話必須寫(xiě)在最后面   提供幫助  -h
commander.parse(process.argv);

這里可以運(yùn)行一下

template-ma test -d
打印
test
true

4.創(chuàng)建執(zhí)行函數(shù)

做一下拆分 把a(bǔ)ction函數(shù)提取到外部

//index.js
const initAction = require('./commands/init')
commander.command('init <name>') // 定義init子命令辨图,<name>為必需參數(shù)可在action的function中接收,如需設(shè)置非必需參數(shù)芭碍,可使用中括號(hào)
    .option('-d, --dev', '獲取開(kāi)發(fā)版') // 配置參數(shù)徒役,簡(jiǎn)寫(xiě)和全寫(xiě)中使用,分割
    .description('創(chuàng)建項(xiàng)目') // 命令描述說(shuō)明
    .action(initAction);
)

commands/init文件

const initAction = async (name, option) => {
}
module.exports = initAction;

5.拉取模板

我們需要在github上托管自己的腳手架模板
然后 我們需要用download-git-repo拉取模板
shelljs工具 進(jìn)行條件判斷

 //  檢查控制臺(tái)是否可以運(yùn)行`git `,
    if (!shell.which('git')) {
        console.log(symbols.error, '對(duì)不起窖壕,git命令不可用忧勿!');
        shell.exit(1);
    }
    //  驗(yàn)證輸入name是否合法
    if (fs.existsSync(name)) {
        console.log(symbols.warning,`已存在項(xiàng)目文件夾${name}杉女!`);
        return;
    }
    if (name.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {
        console.log(symbols.error, '項(xiàng)目名稱(chēng)存在非法字符!');
        return;
    }
    // 獲取option鸳吸,確定模板類(lèi)型(分支)
    if (option.dev) branch = 'develop';

然后是拉取模板

function clone (remote, name, option) {
//三個(gè)參數(shù)熏挎,拉取的路徑,名字 晌砾,分支
    const downSpinner = ora('正在下載模板...').start();
//ora用于輸出loading
    return new Promise((resolve, reject) => {
        download(remote, name, option, err => {
            if (err) {
                downSpinner.fail();
                console.log(symbols.error, chalk.red(err));
                //chalk改變文字顏色
                reject(err);
                return;
            };
            downSpinner.succeed(chalk.green('模板下載成功坎拐!'));
            resolve();
        });
    });
  await clone(`direct:${remote}#${branch}`, name, { clone: true });

清理文件

拉取下來(lái)后是和遠(yuǎn)程倉(cāng)庫(kù)關(guān)聯(lián)的,所以我們要把它們刪掉养匈,還有清除一些多余的文件

 const deleteDir = ['.git', '.gitignore', 'README.md', 'docs']; // 需要清理的文件
    const pwd = shell.pwd();
    deleteDir.map(item => shell.rm('-rf', pwd + `/${name}/${item}`));

根據(jù)用戶(hù)需求更改配置

這里用到inquirer可以與命令行進(jìn)行交互

我們可以設(shè)置幾個(gè)問(wèn)題

 const questions = [
    {
        type: 'input',
        message: '請(qǐng)輸入模板名稱(chēng):',
        name: 'name',
        validate(val) {
        if (!val) return '模板名稱(chēng)不能為空哼勇!';
        if (val.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) return '模板名稱(chēng)包含非法字符,請(qǐng)重新輸入';
        return true;
        }
    },
    {
        type: 'input',
        message: '請(qǐng)輸入模板關(guān)鍵詞(;分割):',
        name: 'keywords'
    },
    {
        type: 'input',
        message: '請(qǐng)輸入模板簡(jiǎn)介:',
        name: 'description'
    },
]

通過(guò)inquirer獲取到用戶(hù)輸入的內(nèi)容

const answers = await inquirer.prompt(questions);
    // 將用戶(hù)的配置打印呕乎,確認(rèn)一下是否正確
    console.log('------------------------');
    console.log(answers);

    let confirm = await inquirer.prompt([
        {
            type: 'confirm',
            message: '確認(rèn)創(chuàng)建积担?',
            default: 'Y',
            name: 'isConfirm'
        }
    ]);
 if (!confirm.isConfirm) return false;

我們拿到用戶(hù)輸入的值 就可以根據(jù)用戶(hù)的參數(shù) 進(jìn)行更改

//簡(jiǎn)單的讀取操作 就不過(guò)多描述了
 let jsonData= fs.readFileSync((path.join(__dirname,'./'), 'malunan/package.json'),function(err,data){
       console.log(err)
    })
    jsonData=JSON.parse(jsonData)
    for(item in answers){
        jsonData[item]=answers[item]
    }
    let obj=JSON.stringify(jsonData,null,'\t')
    let sss=fs.writeFileSync((path.join(__dirname,'./'), 'malunan/package.json'),obj,function(err,data){
        console.log(err,data)
    })

這樣我們就根據(jù)用戶(hù)的參數(shù),寫(xiě)入到了package.json文件 其他的用途 可自行開(kāi)發(fā)

自動(dòng)安裝依賴(lài)

const installSpinner = ora('正在安裝依賴(lài)...').start();
    if (shell.exec('npm install').code !== 0) {
        console.log(symbols.warning, chalk.yellow('自動(dòng)安裝失敗猬仁,請(qǐng)手動(dòng)安裝帝璧!'));
        installSpinner.fail(); // 安裝失敗
        shell.exit(1);
    }
    installSpinner.succeed(chalk.green('依賴(lài)安裝成功!'));

    //切入后臺(tái)的時(shí)候給用戶(hù)提示
    notifier.notify({
        title: 'YNCMS-template-cli',
        icon: path.join(__dirname, 'coulson.png'),
        message: ' ?(^?^●)? 恭喜湿刽,項(xiàng)目創(chuàng)建成功的烁!'
    });

    // 自動(dòng)打開(kāi)編輯器
    if (shell.which('code')) shell.exec('code ./');
    shell.exit(1);

這樣腳手架就搭建完畢了,可以在本地 template-ma init <name> 試一下效果

源碼

index.js

#!/usr/bin/env node

const commander = require('commander');
const initAction = require('./commands/init')
//查詢(xún)版本號(hào)
commander.version(require('./package.json').version,'-v,-V,--version', '查看版本號(hào)')  
commander.command('init <name>') // 定義init子命令诈闺,<name>為必需參數(shù)可在action的function中接收渴庆,如需設(shè)置非必需參數(shù),可使用中括號(hào)
    .option('-d, --dev', '獲取開(kāi)發(fā)版') // 配置參數(shù)买雾,簡(jiǎn)寫(xiě)和全寫(xiě)中使用,分割
    .description('創(chuàng)建項(xiàng)目') // 命令描述說(shuō)明
    .action(initAction);


//這句話必須寫(xiě)在最后面   提供幫助  -h
commander.parse(process.argv);

clone.js

// utils/clone.js
const download = require('download-git-repo');
const symbols = require('log-symbols');  // 用于輸出圖標(biāo)
const ora = require('ora'); // 用于輸出loading
const chalk = require('chalk'); // 用于改變文字顏色
module.exports = function (remote, name, option) {
    const downSpinner = ora('正在下載模板...').start();
    return new Promise((resolve, reject) => {
        download(remote, name, option, err => {
            if (err) {
                downSpinner.fail();
                console.log(symbols.error, chalk.red(err));
                reject(err);
                return;
            };
            downSpinner.succeed(chalk.green('模板下載成功把曼!'));
            resolve();
        });
    });
  };

init.js

// commands/init.js
const shell = require('shelljs');
const symbols = require('log-symbols');
const clone = require('../utils/clone.js');
const remote = 'https://github.com/MaLunan/components.git';
const fs =require('fs')
const ora = require('ora'); // 用于輸出loading
const chalk = require('chalk'); // 用于改變文字顏色
const notifier =require('node-notifier')
const path = require('path')
let branch = 'master';

const initAction = async (name, option) => {
    // 0. 檢查控制臺(tái)是否可以運(yùn)行`git `,
    if (!shell.which('git')) {
        console.log(symbols.error, '對(duì)不起漓穿,git命令不可用!');
        shell.exit(1);
    }
    // 1. 驗(yàn)證輸入name是否合法
    if (fs.existsSync(name)) {
        console.log(symbols.warning,`已存在項(xiàng)目文件夾${name}注盈!`);
        return;
    }
    if (name.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {
        console.log(symbols.error, '項(xiàng)目名稱(chēng)存在非法字符晃危!');
        return;
    }
    // 2. 獲取option,確定模板類(lèi)型(分支)
    if (option.dev) branch = 'develop';
    // 4. 下載模板
    await clone(`direct:${remote}#${branch}`, name, { clone: true });

    // 5. 清理文件
    const deleteDir = ['.git', '.gitignore', 'README.md', 'docs']; // 需要清理的文件
    const pwd = shell.pwd();
    deleteDir.map(item => shell.rm('-rf', pwd + `/${name}/${item}`));
    const inquirer = require('inquirer');
    // 定義需要詢(xún)問(wèn)的問(wèn)題
    const questions = [
    {
        type: 'input',
        message: '請(qǐng)輸入模板名稱(chēng):',
        name: 'name',
        validate(val) {
        if (!val) return '模板名稱(chēng)不能為空老客!';
        if (val.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) return '模板名稱(chēng)包含非法字符僚饭,請(qǐng)重新輸入';
        return true;
        }
    },
    {
        type: 'input',
        message: '請(qǐng)輸入模板關(guān)鍵詞(;分割):',
        name: 'keywords'
    },
    {
        type: 'input',
        message: '請(qǐng)輸入模板簡(jiǎn)介:',
        name: 'description'
    },
    // {
    //     type: 'list',
    //     message: '請(qǐng)選擇模板類(lèi)型:',
    //     choices: ['響應(yīng)式', '桌面端', '移動(dòng)端'],
    //     name: 'type'
    // },
    // {
    //     type: 'list',
    //     message: '請(qǐng)選擇模板分類(lèi):',
    //     choices: ['整站', '單頁(yè)', '專(zhuān)題'],
    //     name: 'category'
    // },
    // {
    //     type: 'input',
    //     message: '請(qǐng)輸入模板風(fēng)格:',
    //     name: 'style'
    // },
    // {
    //     type: 'input',
    //     message: '請(qǐng)輸入模板色系:',
    //     name: 'color'
    // },
    {
        type: 'input',
        message: '請(qǐng)輸入您的名字:',
        name: 'author'
    }
    ];
    // 通過(guò)inquirer獲取到用戶(hù)輸入的內(nèi)容
    const answers = await inquirer.prompt(questions);
    // 將用戶(hù)的配置打印,確認(rèn)一下是否正確
    console.log('------------------------');
    console.log(answers);

    let confirm = await inquirer.prompt([
        {
            type: 'confirm',
            message: '確認(rèn)創(chuàng)建胧砰?',
            default: 'Y',
            name: 'isConfirm'
        }
    ]);
    if (!confirm.isConfirm) return false;
    //根據(jù)用戶(hù)配置調(diào)整文件
   let jsonData= fs.readFileSync((path.join(__dirname,'./'), 'malunan/package.json'),function(err,data){
       console.log(err)
    })
    jsonData=JSON.parse(jsonData)
    for(item in answers){
        jsonData[item]=answers[item]
    }
    console.log(jsonData)
    console.log(path.join(__dirname,`${name}/`))
    let obj=JSON.stringify(jsonData,null,'\t')
    let sss=fs.writeFileSync((path.join(__dirname,'./'), 'malunan/package.json'),obj,function(err,data){
        console.log(err,data)
    })
    //自動(dòng)安裝依賴(lài)
    const installSpinner = ora('正在安裝依賴(lài)...').start();
    if (shell.exec('npm install').code !== 0) {
        console.log(symbols.warning, chalk.yellow('自動(dòng)安裝失敗鳍鸵,請(qǐng)手動(dòng)安裝!'));
        installSpinner.fail(); // 安裝失敗
        shell.exit(1);
    }
    installSpinner.succeed(chalk.green('依賴(lài)安裝成功尉间!'));

    //切入后臺(tái)的時(shí)候給用戶(hù)提示
    notifier.notify({
        title: 'YNCMS-template-cli',
        icon: path.join(__dirname, 'coulson.png'),
        message: ' ?(^?^●)? 恭喜偿乖,項(xiàng)目創(chuàng)建成功击罪!'
    });

    // 8. 打開(kāi)編輯器
    if (shell.which('code')) shell.exec('code ./');
    shell.exit(1);
};


module.exports = initAction;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市贪薪,隨后出現(xiàn)的幾起案子媳禁,更是在濱河造成了極大的恐慌,老刑警劉巖画切,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件竣稽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡霍弹,警方通過(guò)查閱死者的電腦和手機(jī)毫别,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)典格,“玉大人拧烦,你說(shuō)我怎么就攤上這事《奂疲” “怎么了恋博?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)私恬。 經(jīng)常有香客問(wèn)我债沮,道長(zhǎng),這世上最難降的妖魔是什么本鸣? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任疫衩,我火速辦了婚禮,結(jié)果婚禮上荣德,老公的妹妹穿的比我還像新娘闷煤。我一直安慰自己,他們只是感情好涮瞻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布鲤拿。 她就那樣靜靜地躺著,像睡著了一般署咽。 火紅的嫁衣襯著肌膚如雪近顷。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天宁否,我揣著相機(jī)與錄音窒升,去河邊找鬼。 笑死慕匠,一個(gè)胖子當(dāng)著我的面吹牛饱须,可吹牛的內(nèi)容都是我干的咱筛。 我是一名探鬼主播猾瘸,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼粗仓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼送浊!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起督怜,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤殴瘦,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后号杠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蚪腋,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年姨蟋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屉凯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡眼溶,死狀恐怖悠砚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情堂飞,我是刑警寧澤灌旧,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站绰筛,受9級(jí)特大地震影響枢泰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铝噩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一衡蚂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骏庸,春花似錦毛甲、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至硬猫,卻和暖如春补箍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啸蜜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辈挂,地道東北人衬横。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像终蒂,于是被迫代替她去往敵國(guó)和親蜂林。 傳聞我的和親對(duì)象是個(gè)殘疾皇子遥诉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容