【中高級前端必備】手摸手教你擼一個腳手架

腳手架

vue-cli, create-react-appreact-native-cli 等都是很是優秀的腳手架,經過腳手架,咱們能夠快速初始化一個項目,無需本身從零開始一步步配置,有效提高開發體驗。儘管這些腳手架很是優秀,可是未必是符合咱們的實際應用的,咱們能夠定製一個屬於本身的腳手架(或公司通用腳手架),來提高本身的開發效率。javascript

腳手架的做用前端

  • 減小重複性的工做,不須要複製其餘項目再刪除無關代碼,或者從零建立一個項目和文件。
  • 能夠根據交互動態生成項目結構和配置文件。
  • 多人協做更爲方便,不須要把文件傳來傳去。

本項目完整代碼請戳: github.com/YvetteLau/B…vue

實現的功能

在開始以前,咱們須要明確本身的腳手架須要哪些功能。vue init template-name project-namecreate-react-app project-name。咱們此次編寫的腳手架(eos-cli)具有如下能力(腳手架的名字愛叫啥叫啥,我選用了Eos黎明女神):java

  • eos init template-name project-name 根據遠程模板,初始化一個項目(遠程模板可配置)
  • eos config set <key> <value> 修改配置信息
  • eos config get [<key>] 查看配置信息
  • eos --version 查看當前版本號
  • eos -h

你們能夠自行擴展其它的 commander,本篇文章旨在教你們如何實現一個腳手架。node

本項目完整代碼請戳(建議先clone代碼): github.com/YvetteLau/B…react

效果展現git

初始化一個項目github

修改.eosrc文件,從 vuejs-template 下載模板vue-cli

須要使用的第三方庫

  • @babel/cli @babel/core @babel/preset-env: 語法轉換
  • commander: 命令行工具
  • download-git-repo: 用來下載遠程模板
  • ini: 格式轉換
  • inquirer: 交互式命令行工具
  • ora: 顯示loading動畫
  • chalk: 修改控制檯輸出內容樣式
  • log-symbols: 顯示出 √ 或 × 等的圖標

關於這些第三方庫的說明,能夠直接npm上查看相應的說明,此處不一一展開。shell

初始化項目

建立一個空項目(eos-cli),使用 npm init 進行初始化。

安裝依賴

npm install @babel/cli @babel/core @babel/preset-env chalk commander download-git-repo ini inquirer log-symbols ora -D
複製代碼

目錄結構

├── bin
│   └── www             //可執行文件
├── dist
    ├── ...             //生成文件
└── src
    ├── config.js       //管理eos配置文件
    ├── index.js        //主流程入口文件
    ├── init.js         //init command
    ├── main.js         //入口文件
    └── utils
        ├── constants.js //定義常量
        ├── get.js       //獲取模板
        └── rc.js        //配置文件
├── .babelrc             //babel配置文件
├── package.json
├── README.md

複製代碼

babel 配置

開發使用了ES6語法,使用 babel 進行轉義,

.bablerc

{
    "presets": [
        [
            "@babel/env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}
複製代碼

eos 命令

node.js 內置了對命令行操做的支持,package.json 中的 bin 字段能夠定義命令名和關聯的執行文件。在 package.json 中添加 bin 字段

package.json

{
    "name": "eos-cli",
    "version": "1.0.0",
    "description": "腳手架",
    "main": "index.js",
    "bin": {
        "eos": "./bin/www"
    },
    "scripts": {
        "compile": "babel src -d dist",
        "watch": "npm run compile -- --watch"
    }
}
複製代碼

www 文件

行首加入一行 #!/usr/bin/env node 指定當前腳本由node.js進行解析

#! /usr/bin/env node
require('../dist/main.js');
複製代碼

連接到全局環境

開發過程當中爲了方便調試,在當前的 eos-cli 目錄下執行 npm link,將 eos 命令連接到全局環境。

啓動項目

npm run watch
複製代碼

處理命令行

利用 commander 來處理命令行。

main

import program from 'commander';
import { VERSION } from './utils/constants';
import apply from './index';
import chalk from 'chalk';

/** * eos commands * - config * - init */

let actionMap = {
    init: {
        description: 'generate a new project from a template',
        usages: [
            'eos init templateName projectName'
        ]
    },
    config: {
        alias: 'cfg',
        description: 'config .eosrc',
        usages: [
            'eos config set <k> <v>',
            'eos config get <k>',
            'eos config remove <k>'
        ]
        
    },
    //other commands
}

// 添加 init / config 命令
Object.keys(actionMap).forEach((action) => {
    program.command(action)
    .description(actionMap[action].description)
    .alias(actionMap[action].alias) //別名
    .action(() => {
        switch (action) {
            case 'config': 
                //配置
                apply(action, ...process.argv.slice(3));
                break;
            case 'init':
                apply(action, ...process.argv.slice(3));
                break;
            default:
                break;
        }
    });
});

function help() {
    console.log('\r\nUsage:');
    Object.keys(actionMap).forEach((action) => {
        actionMap[action].usages.forEach(usage => {
            console.log(' - ' + usage);
        });
    });
    console.log('\r');
}
program.usage('<command> [options]');
// eos -h 
program.on('-h', help);
program.on('--help', help);
// eos -V VERSION 爲 package.json 中的版本號
program.version(VERSION, '-V --version').parse(process.argv);

// eos 不帶參數時
if (!process.argv.slice(2).length) {
    program.outputHelp(make_green);
}
function make_green(txt) {
    return chalk.green(txt); 
}
複製代碼

下載模板

download-git-repo 支持從 Github、Gitlab 下載遠程倉庫到本地。

get.js

import { getAll } from './rc';
import downloadGit from 'download-git-repo';

export const downloadLocal = async (templateName, projectName) => {
    let config = await getAll();
    let api = `${config.registry}/${templateName}`;
    return new Promise((resolve, reject) => {
        //projectName 爲下載到的本地目錄
        downloadGit(api, projectName, (err) => {
            if (err) {
                reject(err);
            }
            resolve();
        });
    });
}
複製代碼

init 命令

命令行交互

在用戶執行 init 命令後,向用戶提出問題,接收用戶的輸入並做出相應的處理。命令行交互利用 inquirer 來實現:

inquirer.prompt([
    {
        name: 'description',
        message: 'Please enter the project description: '
    },
    {
        name: 'author',
        message: 'Please enter the author name: '
    }
]).then((answer) => {
    //...
});
複製代碼

視覺美化

在用戶輸入以後,開始下載模板,這時候使用 ora 來提示用戶正在下載模板,下載結束以後,也給出提示。

import ora from 'ora';
let loading = ora('downloading template ...');
loading.start();
//download
loading.succeed(); //或 loading.fail();
複製代碼

init.js

import { downloadLocal } from './utils/get';
import ora from 'ora';
import inquirer from 'inquirer';
import fs from 'fs';
import chalk from 'chalk';
import symbol from 'log-symbols';

let init = async (templateName, projectName) => {
    //項目不存在
    if (!fs.existsSync(projectName)) {
        //命令行交互
        inquirer.prompt([
            {
                name: 'description',
                message: 'Please enter the project description: '
            },
            {
                name: 'author',
                message: 'Please enter the author name: '
            }
        ]).then(async (answer) => {
            //下載模板 選擇模板
            //經過配置文件,獲取模板信息
            let loading = ora('downloading template ...');
            loading.start();
            downloadLocal(templateName, projectName).then(() => {
                loading.succeed();
                const fileName = `${projectName}/package.json`;
                if(fs.existsSync(fileName)){
                    const data = fs.readFileSync(fileName).toString();
                    let json = JSON.parse(data);
                    json.name = projectName;
                    json.author = answer.author;
                    json.description = answer.description;
                    //修改項目文件夾中 package.json 文件
                    fs.writeFileSync(fileName, JSON.stringify(json, null, '\t'), 'utf-8');
                    console.log(symbol.success, chalk.green('Project initialization finished!'));
                }
            }, () => {
                loading.fail();
            });
        });
    }else {
        //項目已經存在
        console.log(symbol.error, chalk.red('The project already exists'));
    }
}
module.exports = init;
複製代碼

config 配置

eos config set registry vuejs-templates
複製代碼

config 配置,支持咱們使用其它倉庫的模板,例如,咱們可使用 vuejs-templates 中的倉庫做爲模板。這樣有一個好處:更新模板無需從新發布腳手架,使用者無需從新安裝,而且能夠自由選擇下載目標。

config.js

// 管理 .eosrc 文件 (當前用戶目錄下)
import { get, set, getAll, remove } from './utils/rc';

let config = async (action, key, value) => {
    switch (action) {
        case 'get':
            if (key) {
                let result = await get(key);
                console.log(result);
            } else {
                let obj = await getAll();
                Object.keys(obj).forEach(key => {
                    console.log(`${key}=${obj[key]}`);
                })
            }
            break;
        case 'set':
            set(key, value);
            break;
        case 'remove':
            remove(key);
            break;
        default:
            break;
    }
}

module.exports = config;
複製代碼

rc.js

.eosrc 文件的增刪改查

import { RC, DEFAULTS } from './constants';
import { decode, encode } from 'ini';
import { promisify } from 'util';
import chalk from 'chalk';
import fs from 'fs';

const exits = promisify(fs.exists);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

//RC 是配置文件
//DEFAULTS 是默認的配置
export const get = async (key) => {
    const exit = await exits(RC);
    let opts;
    if (exit) {
        opts = await readFile(RC, 'utf8');
        opts = decode(opts);
        return opts[key];
    }
    return '';
}

export const getAll = async () => {
    const exit = await exits(RC);
    let opts;
    if (exit) {
        opts = await readFile(RC, 'utf8');
        opts = decode(opts);
        return opts;
    }
    return {};
}

export const set = async (key, value) => {
    const exit = await exits(RC);
    let opts;
    if (exit) {
        opts = await readFile(RC, 'utf8');
        opts = decode(opts);
        if(!key) {
            console.log(chalk.red(chalk.bold('Error:')), chalk.red('key is required'));
            return;
        }
        if(!value) {
            console.log(chalk.red(chalk.bold('Error:')), chalk.red('value is required'));
            return;
        }
        Object.assign(opts, { [key]: value });
    } else {
        opts = Object.assign(DEFAULTS, { [key]: value });
    }
    await writeFile(RC, encode(opts), 'utf8');
}

export const remove = async (key) => {
    const exit = await exits(RC);
    let opts;
    if (exit) {
        opts = await readFile(RC, 'utf8');
        opts = decode(opts);
        delete opts[key];
        await writeFile(RC, encode(opts), 'utf8');
    }
}
複製代碼

發佈

npm publish 將本腳手架發佈至npm上。其它用戶能夠經過 npm install eos-cli -g 全局安裝。 便可使用 eos 命令。

項目地址

本項目完整代碼請戳: github.com/YvetteLau/B…

編寫本文,雖然花費了必定時間,可是在這個過程當中,我也學習到了不少知識,謝謝各位小夥伴願意花費寶貴的時間閱讀本文,若是本文給了您一點幫助或者是啓發,請不要吝嗇你的贊和Star,您的確定是我前進的最大動力。 github.com/YvetteLau/B…

參考文章:

[1] npm依賴文檔(www.npmjs.com/package/dow…)

感謝指出:

增長參考文章:[簡單搭建前端腳手架 ICE] (link.juejin.im/?target=htt…)

關注公衆號,加入技術交流羣

相關文章
相關標籤/搜索