如何實現腳手架開發

經過學習慕課Web前端架構課程的筆記記錄。前端

腳手架簡介

什麼是腳手架?

腳手架本質是一個操做系統的客戶端,它經過命令行執行,就好比:vue

vue create vue-test-app
複製代碼

上面這條命令由3個部分組成:node

  • 主命令:vue
  • command:create
  • command的param:vue-test-app

腳手架的執行原理

5fda202309d65fff16991502.png

腳手架的執行原理以下:react

  • 在終端輸入vue create vue-test-app
  • 終端解析出vue命令
  • 終端在環境變量中找到vue命令
  • 最終根據vue命令連接到實際文件vue.js
  • 終端利用node執行vue.js
  • vue.js解析command/options
  • vue.js執行command
  • 執行完畢,退出執行

腳手架的實現原理

爲何全局安裝@vue/cli後會添加的命令爲vue?

首先咱們經過which vue找到vue的目錄/usr/local/bin/vue,而後進入到/usr/local/bin目錄下,查看下面全部內容,其中有一條這樣的連接vue -> ../lib/node_modules/@vue/cli/bin/vue.js,vue軟鏈接到全局安裝目錄,去執行vue.js,這種綁定關係是在那肯定的呢?咱們進入到../lib/node_modules/@vue/cli/,查看package.json,咱們看到"bin": { "vue": "bin/vue.js" },,這個bin當中配置的就是咱們在操做系統中安裝完成後軟鏈接的名稱以及指向的實際文件git

全局安裝@vue/cli時發生了什麼?

npm install -g @vue/cli
複製代碼

首先npm把咱們當前的包下載到node_modules目錄裏面,若是是全局安裝的node,它可能存在/uer/lib/目錄下,當把這個包徹底下載完畢以後,它會去解析package.json裏面的bin,若是說bin下面有配置,它就會在咱們node安裝目錄下的bin目錄裏面建立一個軟鏈接.github

執行vue命令時發生了什麼?爲何vue指向了js文件,咱們卻能夠直接經過vue命令去執行它?

第一個問題:執行vue的時候,咱們操做系統會經過which vue去找到bin目錄下的文件執行,也就是說去環境變量中找vue是否被註冊,註冊了就執行.mongodb

第二個問題:由於在文件上方加入了!/usr/bin/env node環境變量,這個環境變量可讓咱們經過固定的命名去對它進行執行vuex

擴展一下,下面兩種寫法的區別:vue-cli

#!/usr/bin/env node
  #!/usr/bin/node
複製代碼

第一種是在環境變量中查找node數據庫

第二種是直接執行/usr/bin/目錄下的node

腳手架的做用

開發腳手架的核心目標是: 提高前端研發效能

核心價值

  • 自動化:項目重複代碼拷貝/git操做/發佈上線操做
  • 標準化:項目建立/git flow/發佈流程/回滾流程
  • 數據化:研發過程系統化、數據化,使得研發過程可量化

腳手架開發難點解析

  • 分包:將複雜的系統拆分紅若干個模塊
  • 命令註冊:
vue create
vue add
vue invoke
複製代碼
  • 參數解析:
    • options全稱:--version--help
    • options簡寫:-V-h
    • 帶params的options: -path xxxx
  • 幫助文檔
  • 命令行交互
  • 日誌打印
  • 命令行文字變色
  • 網絡通訊:HTTP/WebSocket
  • 文件處理

等等...

腳手架本地link標準流程

連接本地腳手架:

cd your-cli-dir
npm link
複製代碼

連接本地庫文件:

cd your-cli-dir
npm link
cd your-cli-dir
npm link your-lib
複製代碼

取消連接本地庫文件:

cd your-cli-dir
npm unlink
cd your-cli-dir
# link存在
npm unlink your-lib
# link不存在
rm -rf node_modules
npm install -S your-lib
複製代碼

理解npm link:

  • npm link your-lib: 將當前項目中的node_modules下指定的庫文件連接到node全局node_modules下的庫文件
  • npm link: 將當前項目連接到node全局node_modules中做爲一個庫文件,並解析bin配置建立可執行文件

理解npm unlink:

  • npm unlink: 將當前項目從node全局node_modules中移除
  • npm unlink your-lib: 將當前項目中的庫文件依賴移除

原生腳手架開發痛點分析

  • 痛點一:重複操做
    • 多Package本地link
    • 多Package依賴安裝
    • 多Package單元測試
    • 多Package代碼提交
    • 多Package代碼發佈
  • 痛點二:版本一致性問題
    • 發佈時版本一致性
    • 發佈後相互依賴版本升級

分析可痛點,那麼就會有解決辦法,那就是經過Lerna來管理多Package。

Lerna簡介

Lerna是一個優化基於git+npm的多package項目的管理工具,使用Lerna管理的大型項目有:babel,vue-cli,craete-react-app等等。

實現原理

  • 經過import-local優先調用本地lerna命令
  • 經過Yargs生成腳手架,先註冊全局屬性,再註冊命令,最後經過parse方法解析參數
  • lerna 命令註冊時須要傳入builder和handler兩個方法,builder方法用於註冊命令專屬的options,handler用來處理命令業務的邏輯
  • lerna經過配置npm本地依賴的方法來進行本地開發,具體寫法是在package.json的依賴中寫入:file:your-local-module-path,在lerna public的時候自動將該路徑替換

優點

  • 大幅減小重複操做
  • 提高操做的標準化

learn開發腳手架流程

5fda20d609a8a01307221197.png

腳手架開發

在開發腳手架以前,咱們先了解下腳手架開發的流程圖。

腳手架架構圖

腳手架設計圖.png

腳手架拆包策略

  • 核心流程:core
  • 命令:commands
    • 初始化
    • 發佈
    • 清除緩存
  • 模型層:models
    • Command命令
    • Project項目
    • Component組件
    • Npm模塊
    • Git倉庫
  • 支持模塊:utils
    • Git操做
    • 雲構建
    • 工具方法
    • API請求
    • Git API

1620572867252.jpg

命令執行流程

  • 準備階段

core準備階段.png

  • 命令註冊

core命令階段.png

  • 命令執行

5fe4a3a408c7620016001303.jpeg

準備階段

  • 檢查版本號
// 檢查版本
function checkPkgVersion() {
    log.info('cli', pkg.version);
}
複製代碼
  • 檢查node版本
// 檢查node版本
checkNodeVersion() {
    //第一步,獲取當前Node版本號
    const currentVersion = process.version;
    const lastVersion = LOWEST_NODE_VERSION;
    //第二步,對比最低版本號
    if (!semver.gte(currentVersion, lastVersion)) {
        throw new Error(colors.red(`roy-cli-dev 須要安裝v${lastVersion}以上版本的Node.js`));
    }
}
複製代碼
  • 檢查root權限
// 檢查root啓動
function checkRoot() {
    //使用後,檢查到root帳戶啓動,會進行降級爲用戶帳戶
    const rootCheck = require('root-check');
    rootCheck();
}
複製代碼
  • 檢查用戶主目錄
// 檢查用戶主目錄
function checkUserHome() {
    if (!userHome || !pathExists(userHome)) {
        throw new Error(colors.red('當前登陸用戶主目錄不存在!!!'));
    }
}
複製代碼
  • 檢查入參
// 檢查入參
function checkInputArgs() {
    const minimist = require('minimist');
    args = minimist(process.argv.slice(2));
    checkArgs();
}

function checkArgs() {
    if (args.debug) {
        process.env.LOG_LEVEL = 'verbose';
    } else {
        process.env.LOG_LEVEL = 'info';
    }
    log.level = process.env.LOG_LEVEL;
}
複製代碼
  • 檢查環境變量
// 檢查環境變量
function checkEnv() {
    const dotenv = require('dotenv');
    const dotenvPath = path.resolve(userHome, '.env');
    if (pathExists(dotenvPath)) {
        config = dotenv.config({
            path: dotenvPath
        });
    }
    createDefaultConfig();
    log.verbose('環境變量', process.env.CLI_HOME_PATH);
}

function createDefaultConfig() {
    const cliConfig = {
        home: userHome
    }
    if (process.env.CLI_HOME) {
        cliConfig['cliHome'] = path.join(userHome, process.env.CLI_HOME);
    } else {
        cliConfig['cliHome'] = path.join(userHome, constants.DEFAULT_CLI_HOME);
    }
    process.env.CLI_HOME_PATH = cliConfig.cliHome;
}
複製代碼
  • 檢查是不是最新版本
// 檢查是不是最新版本,是否須要更新
async function checkGlobalUpdate() {
    //1.獲取當前版本號和模塊名
    const currentVersion = pkg.version;
    const npmName = pkg.name;
    //2.調用npm API,獲取全部版本號
    const { getNpmSemverVersion } = require('@roy-cli-dev/get-npm-info');
    //3.提取全部版本號,比對哪些版本號是大於當前版本號
    const lastVersion = await getNpmSemverVersion(currentVersion, npmName);
    if (lastVersion && semver.gt(lastVersion, currentVersion)) {
        //4.獲取最新的版本號,提示用戶更新到該版本
        log.warn(colors.yellow(`請手動更新${npmName},當前版本:${currentVersion},最新版本:${lastVersion} 更新命令:npm install -g ${npmName}`))
    }
}
複製代碼

命令註冊

註冊init階段

//命名的註冊
function registerCommand() {
    program
        .name(Object.keys(pkg.bin)[0])
        .usage('<command> [options]')
        .version(pkg.version)
        .option('-d, --debug', '是否開啓調試模式', false)
        .option('-tp, --targetPath <targetPath>', '是否指定本地調試文件路徑', '');

    program
        .command('init [projectName]')
        .option('-f, --force', '是否強制初始化項目')
        .action(init); //init 單獨解析一個命令 exec動態加載模塊


    //開啓debug模式
    program.on('option:debug', function () {
        if (program.debug) {
            process.env.LOG_LEVEL = 'verbose';
        } else {
            process.env.LOG_LEVEL = 'info';
        }
        log.level = process.env.LOG_LEVEL;
        log.verbose('test');
    });

    //指定targetPath
    program.on('option:targetPath', function () {
        process.env.CLI_TARGET_PATH = program.targetPath;
    });

    //對未知命令的監聽
    program.on('command:*', function (obj) {
        const availabelCommands = program.commands.map(cmd => cmd.name());
        log.verbose(colors.red('未知命令:' + obj[0]));
        if (availabelCommands.length > 0) {
            log.verbose(colors.blue('可用命令:' + availabelCommands.join(',')));
        }
    })

    program.parse(process.argv);
    //用戶沒有輸入命令的時候
    if (program.args && program.args.length < 1) {
        program.outputHelp();
        console.log();
    }
}
複製代碼

當前架構圖

經過準備階段和命令初始化init階段,咱們建立了以下一些package: 5fe4a37908dd3d1b13720561.jpeg

這樣的架構設計已經能夠知足通常腳手架需求,可是有如下兩個問題:

1.cli安裝速度慢:全部的package都集成在cli裏,所以當命令較多時,會減慢cli的安裝速度

2.靈活性差:init命令只能使用@roy-cli-dev/init包,對於集團公司而言,每一個bu的init命令可能都各不相同,可能須要實現init命令動態化,如:

  • 團隊A使用@roy-cli-dev/init做爲初始化模板
  • 團隊B使用本身開發的@roy-cli-dev/my-init做爲初始化模板
  • 團隊C使用本身開發的@roy-cli-dev/your-init做爲初始化模板

這時對咱們的架構設計就提出了挑戰,要求咱們可以動態加載init模塊,這將增長架構的複雜度,但大大提高腳手架的可擴展性,將腳手架框架和業務邏輯解耦

腳手架架構優化

jiaoshoujiayouhua.png

命令執行階段

const SETTINGS = {
    init: "@roy-cli-dev/init",
}

const CACHE_DIR = 'dependencies/';

async function exec() {
    let targetPath = process.env.CLI_TARGET_PATH;
    const homePath = process.env.CLI_HOME_PATH;
    let storeDir = '';
    let pkg;
    log.verbose('targetPath', targetPath);
    log.verbose('homePath', homePath);
    const cmdObj = arguments[arguments.length - 1];
    const cmdName = cmdObj.name();
    const packageName = SETTINGS[cmdName];
    const packageVersion = 'latest';

    if (!targetPath) {//是否執行本地代碼
        //生成緩存路徑
        targetPath = path.resolve(homePath, CACHE_DIR);
        storeDir = path.resolve(targetPath, 'node_modules');
        log.verbose(targetPath, storeDir);
        //初始化Package對象
        pkg = new Package({
            targetPath,
            storeDir,
            packageName,
            packageVersion
        });
        //判斷Package是否存在
        if (await pkg.exists()) {
            //更新package
            await pkg.update()
        } else {
            //安裝package
            await pkg.install();
        }
    } else {
        pkg = new Package({
            targetPath,
            packageName,
            packageVersion
        });
    }
    //獲取入口文件
    const rootFile = pkg.getRootFile();
    if (rootFile) {//判斷入口文件是否存在
        try {
            //在當前進程中調用
            // require(rootFile).call(null, Array.from(arguments));
            //在node子進程中調用
            const args = Array.from(arguments);
            const cmd = args[args.length - 1];
            const o = Object.create(null);
            Object.keys(cmd).forEach(key=>{
                if (cmd.hasOwnProperty(key) && !key.startsWith('_') && key !== 'parent') {
                    o[key] = cmd[key];
                }
            })
            args[args.length - 1] = o;
            const code = `require('${rootFile}').call(null, ${JSON.stringify(args)})`;
            const child = spawn('node',['-e',code],{
                cwd:process.cwd(),
                stdio:'inherit'
            });
            //執行產生異常
            child.on('error',e=>{
                log.error(e.message);
                process.exit(1);
            });
            //執行完畢 正常退出
            child.on('exit',e=>{
                log.verbose('命令執行成功:'+e);
                process.exit(e);
            })
        } catch (e) {
            log.error(e.message);
        }

    }


    //1.targetPath -> modulePath
    //2.modulePath -> Package(npm模塊)
    //3.Package.getRootFile(獲取入口文件)
    //4.Package.update/Package.install
}
複製代碼

腳手架項目建立功能設計

首先咱們要思考下腳手架項目建立爲了什麼:

  • 可擴展性:可以快速複用到不一樣團隊,適應不一樣團隊之間的差別
  • 低成本:在不改動腳手架源碼的狀況下,可以新增模板,且新增模板的成本很低
  • 高性能:控制存儲空間,安裝時充分利用Node多進程提高安裝性能

建立項目功能架構設計圖

總體過程分爲三個階段:

  • 準備階段

prepare.png

  • 下載模塊

downloadTemplate.png

  • 安裝模塊

installTemplate.png

準備階段

準備階段的核心工做就是:

  • 確保項目的安裝環境
  • 確認項目的基本信息

下載模塊

下載模塊是利用已經封裝Package類快速實現相關功能

安裝模塊

安裝模塊分爲標準模式和自定義模式:

  • 標準模式下,將經過ejs實現模塊渲染,並自動安裝依賴並啓動項目
  • 自定義模式下,將容許用戶主動去實現模塊的安裝過程和後續啓動過程

核心代碼以下:

class InitCommand extends Command {
    init() {
        this.projectName = this._argv[0] || '';
        this.force = this._cmd.force;
        log.verbose(this._argv);
        log.verbose('projectName', this.projectName);
        log.verbose('force', this.force);
    }
    async exec() {
        try {
            //1.準備階段
            const projectInfo = await this.prepare();
            if (projectInfo) {
                //2.下載模板
                log.verbose('projectInfo', projectInfo);
                this.projectInfo = projectInfo
                await this.downloadTemplate();
                //3.安裝模板
                await this.installTemplate();
            }
        } catch (e) {
            log.error(e.message);
            if (process.env.LOG_LEVEL === 'verbose') {
                console.log(e);
            }
        }
    }

    async installTemplate() {
        log.verbose('templateInfo', this.templateInfo);
        if (this.templateInfo) {
            if (!this.templateInfo.type) {
                this.templateInfo.type = TEMPLATE_TYPE_NORMAL
            }
            if (this.templateInfo.type === TEMPLATE_TYPE_NORMAL) {
                //標準安裝 
                await this.installNormalTemplate();
            } else if (this.templateInfo.type === TEMPLATE_TYPE_CUSTOM) {
                //自定義安裝
                await this.installCustomTemplate();
            } else {
                throw new Error('沒法失敗項目模板類');
            }

        } else {
            throw new Error('項目模板信息不存在');
        }
    }
    checkCommand(cmd) {
        if (WHITE_COMMAND.includes(cmd)) {
            return cmd;
        }
        return null;
    }

    async execCommand(command, errMsg) {
        let ret;
        if (command) {
            const cmdArray = command.split(' ');
            const cmd = this.checkCommand(cmdArray[0]);
            if (!cmd) {
                throw new Error('命令不存在!命令:' + command);
            }
            const args = cmdArray.slice(1);
            ret = await execAsync(cmd, args, {
                stdio: 'inherit',
                cwd: process.cwd(),
            })
        }
        if (ret !== 0) {
            throw new Error(errMsg)
        }
    }

    async ejsRender(options) {
        const dir = process.cwd();
        const projectInfo = this.projectInfo;
        return new Promise((resolve, reject) => {
            glob('**', {
                cwd: dir,
                ignore: options.ignore || '',
                nodir: true,
            }, (err, files) => {
                if (err) {
                    reject(err);
                }
                Promise.all(files.map(file => {
                    const filePath = path.join(dir, file);
                    return new Promise((resolve1, reject1) => {
                        ejs.renderFile(filePath, projectInfo, {}, (err, result) => {
                            console.log(result);
                            if (err) {
                                reject1(err);
                            } else {
                                fse.writeFileSync(filePath, result);
                                resolve1(result);
                            }
                        })
                    });
                })).then(() => {
                    resolve();
                }).catch(err => {
                    reject(err);
                });
            })
        })
    }

    async installNormalTemplate() {
        //拷貝模板代碼直當前目錄
        let spinner = spinnerStart('正在安裝模板');
        log.verbose('templateNpm', this.templateNpm)
        try {
            const templatePath = path.resolve(this.templateNpm.cachFilePath, 'template');
            const targetPath = process.cwd();
            fse.ensureDirSync(templatePath);//確保當前文件存不存在,不存在會建立
            fse.ensureDirSync(targetPath);
            fse.copySync(templatePath, targetPath);//把緩存目錄下的模板拷貝到當前目錄
        } catch (e) {
            throw e;
        } finally {
            spinner.stop(true);
            log.success('模板安裝成功');
        }
        const templateIgnore = this.templateInfo.ignore || [];
        const ignore = ['**/node_modules/**', ...templateIgnore];
        await this.ejsRender({ ignore });
        //依賴安裝
        const { installCommand, startCommand } = this.templateInfo
        await this.execCommand(installCommand, '依賴安裝過程當中失敗');
        //啓動命令執行
        await this.execCommand(startCommand, '啓動執行命令失敗');
    }
    async installCustomTemplate() {
        //查詢自定義模板的入口文件
        if (await this.templateNpm.exists()) {
            const rootFile = this.templateNpm.getRootFile();
            if (fs.existsSync(rootFile)) {
                log.notice('開始執行自定義模板');
                const options = {
                    ...this.options,
                    cwd:process.cwd(),
                }
                const code = `require('${rootFile}')(${JSON.stringify(options)})`;
                log.verbose('code',code);
                await execAsync('node',['-e', code], { stdio: 'inherit', cwd: process.cwd()});
                log.success('自定義模板安裝成功');
            } else {
                throw new Error('自定義模板入口文件不存在');
            }
        }
    }

    async downloadTemplate() {
        //1. 經過項目模板API獲取項目模板信息
        //1.1 經過egg.js搭建一套後端系統
        //1.2 經過npm存儲項目模板
        //1.3 將項目模板信息存儲到mongodb數據庫中
        //1.4 經過egg.js獲取mongodb中的數據而且經過API返回
        const { projectTemplate } = this.projectInfo;
        const templateInfo = this.template.find(item => item.npmName === projectTemplate);
        const targetPath = path.resolve(userHome, '.roy-cli-dev', 'template');
        const storeDir = path.resolve(userHome, '.roy-cli-dev', 'template', 'node_modules');
        const { npmName, version } = templateInfo;
        this.templateInfo = templateInfo;
        const templateNpm = new Package({
            targetPath,
            storeDir,
            packageName: npmName,
            packageVersion: version
        })
        if (! await templateNpm.exists()) {
            const spinner = spinnerStart('正在下載模板...');
            await sleep();
            try {
                await templateNpm.install();
            } catch (e) {
                throw e;
            } finally {
                spinner.stop(true);
                if (templateNpm.exists()) {
                    log.success('下載模板成功');
                    this.templateNpm = templateNpm;
                }
            }
        } else {
            const spinner = spinnerStart('正在更新模板...');
            await sleep();
            try {
                await templateNpm.update();
            } catch (e) {
                throw e;
            } finally {
                spinner.stop(true);
                if (templateNpm.exists()) {
                    log.success('更新模板成功');
                    this.templateNpm = templateNpm;
                }
            }
        }
    }


    async prepare() {
        // 判斷項目模板是否存在
        const template = await getProjectTemplate();
        if (!template || template.length === 0) {
            throw new Error('項目模板不存在');
        }
        this.template = template;
        //1.判斷當前目錄是否爲空
        const localPath = process.cwd();
        if (!this.isDirEmpty(localPath)) {
            let ifContinue = false;
            if (!this.force) {
                //詢問是否繼續建立
                ifContinue = (await inquirer.prompt({
                    type: 'confirm',
                    name: 'ifContinue',
                    default: false,
                    message: '當前文件夾不爲空,是否繼續建立項目?'
                })).ifContinue;
                if (!ifContinue) {
                    return;
                }
            }
            //2.是否啓動強制更新
            if (ifContinue || this.force) {
                //給用戶二次確認
                const { confirmDelete } = await inquirer.prompt({
                    type: 'confirm',
                    name: 'confirmDelete',
                    default: false,
                    message: '是否確認清空當前目錄下的文件?',
                })
                if (confirmDelete) {
                    //清空當前目錄
                    fse.emptyDirSync(localPath)
                }
            }
        }
        return this.getProjectInfo();

        //3.選擇建立項目或組件
        //4.獲取項目得基本信息

    }
    async getProjectInfo() {

        function isValidName(v) {
            return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v);
        }

        let projectInfo = {};
        let isProjectInfoValid = false;
        if (isValidName(this.projectName)) {
            isProjectInfoValid = true;
            projectInfo.projectName = this.projectName;
        }
        
        //1.選擇建立項目或組件
        const { type } = await inquirer.prompt({
            type: 'list',
            name: 'type',
            message: '請選擇初始化類型',
            default: TYPE_PROJECT,
            choices: [{
                name: '項目',
                value: TYPE_PROJECT
            }, {
                name: '組件',
                value: TYPE_COMPONENT
            }]
        });
        log.verbose('type', type);
        this.template = this.template.filter(template => {
            return template.tag.includes(type);
        })
        const title = type === TYPE_PROJECT ? '項目' : '組件';
        //2.獲取項目的基本信息
        const projectNamePrompt = {
            type: 'input',
            name: 'projectName',
            message: `請輸入${title}的名稱`,
            default: '',
            validate: function (v) {
                const done = this.async();
                setTimeout(function () {
                    //1.輸入的首字符必須爲英文字符
                    //2.尾字符必須爲英文或數字,不能爲字符
                    //3.字符僅運行"-_"
                    //\w = a-zA-Z0-9 *表示0個或多個
                    if (!isValidName(v)) {
                        done(`請輸入合法的${title}名稱`);
                        return;
                    }
                    done(null, true);
                }, 0);
            },
            filter: function (v) {
                return v;
            }
        }
        let projectPrompt = [];
        if (!isProjectInfoValid) {
            projectPrompt.push(projectNamePrompt);
        }
        projectPrompt.push({
            input: 'input',
            name: 'projectVersion',
            message: `請輸入${title}版本號`,
            default: '1.0.0',
            validate: function (v) {
                const done = this.async();
                setTimeout(function () {
                    //1.輸入的首字符必須爲英文字符
                    //2.尾字符必須爲英文或數字,不能爲字符
                    //3.字符僅運行"-_"
                    //\w = a-zA-Z0-9 *表示0個或多個
                    if (!(!!semver.valid(v))) {
                        done('請輸入合法的版本號');
                        return;
                    }
                    done(null, true);
                }, 0);
            },
            filter: function (v) {
                if (!!semver.valid(v)) {
                    return semver.valid(v);
                } else {
                    return v;
                }
            }
        }, {
            type: 'list',
            name: 'projectTemplate',
            message: `請選擇${title}模板`,
            choices: this.createTemplateChoices()
        });
        if (type === TYPE_PROJECT) {
            const project = await inquirer.prompt(projectPrompt);
            projectInfo = {
                ...projectInfo,
                type,
                ...project
            }
        } else if (type === TYPE_COMPONENT) {
            const descriptionPrompt = {
                input: 'input',
                name: 'componentDescription',
                message: '請輸入組件描述信息',
                default: '',
                validate: function (v) {
                    const done = this.async();
                    setTimeout(function () {
                        //1.輸入的首字符必須爲英文字符
                        //2.尾字符必須爲英文或數字,不能爲字符
                        //3.字符僅運行"-_"
                        //\w = a-zA-Z0-9 *表示0個或多個
                        if (!v) {
                            done('請輸入組件描述信息');
                            return;
                        }
                        done(null, true);
                    }, 0);
                }
            }
            projectPrompt.push(descriptionPrompt);
            const component = await inquirer.prompt(projectPrompt);
            projectInfo = {
                ...projectInfo,
                type,
                ...component
            }
        }
        //return 項目的基本信息(object)
        if (projectInfo.projectName) {
            projectInfo.className = require('kebab-case')(projectInfo.projectName).replace(/^-/, '');
        }
        if (projectInfo.projectVersion) {
            projectInfo.version = projectInfo.projectVersion;
        }
        if (projectInfo.componentDescription) {
            projectInfo.description = projectInfo.componentDescription;
        }
        return projectInfo;
    }

    isDirEmpty(localPath) {
        let fileList = fs.readdirSync(localPath);
        //文件過濾的邏輯
        fileList = fileList.filter(file => (
            !file.startsWith('.') && ['node_modules'].indexOf(file) < 0
        ));

        return !fileList || fileList.length <= 0;
    }
    createTemplateChoices() {
        return this.template.map(item => ({
            value: item.npmName,
            name: item.name
        }))
    }
}

function init(argv) {
    // console.log('init',projectName,cmdObj.force,process.env.CLI_TARGET_PATH);
    return new InitCommand(argv);
}


module.exports = init;
module.exports.InitCommand = InitCommand;
複製代碼

至此咱們完成了腳手架開發以及經過腳手架建立項目。

如何經過Yargs來開發腳手架?

  • 腳手架分爲三部分構成(vue create vuex)

    • bin:主命令在package.json中配置bin屬性,npm link本地安裝
    • command:命令
    • options:參數(boolean/string/number)
    • 文件頂部增長#!/usr/bin/env node,這行命令的用途時告訴操做系統要在環境變量當中查詢到node命令,經過node命令來執行文件
  • 腳手架初始化流程

    • 構造函數:Yargs() (經過Yargs構造函數的調用去生成一個腳手架)
    • 經常使用方法:
      • Yargs.options (註冊腳手架的屬性)
      • Yargs.option
      • Yargs.group (將腳手架屬性進行分組)
      • Yargs.demandCommand (規定最少傳幾個command)
      • Yargs.recommendCommands (在輸入錯誤command之後能夠給你推薦最接近的正確的command)
      • Yargs.strict (開啓之後能夠報錯提示)
      • Yargs.fail (監聽腳手架的異常)
      • Yargs.alias (起別名)
      • Yargs.wrapper (命令行工具的寬度)
      • Yargs.epilogus (命令行工具底部的提示)
  • 腳手架參數解析方法

    • hideBin(process.argv)
    • Yargs.parse(argv, options)
  • 命令註冊方法

    • Yargs.command(command,describe, builder, handler)
    • Yargs.command({command,describe, builder, handler})

Node.js模塊路徑解析流程

  • Node.js項目模塊路徑解析是經過require.resolve方法來實現的
  • require.resolve就是經過Module._resolveFileName方法實現的
  • require.resolve實現原理:
    • Module._resolveFileName方法核心流程有3點:
      • 判斷是否爲內置模塊
      • 經過Module._resolveLookupPaths方法生成node_modules可能存在的路徑
      • 經過Module._findPath查詢模塊的真實路徑
    • Module._findPath核心流程有4點:
      • 查詢緩存(將request和paths經過\x00(空格)合併成cacheKey)
      • 遍歷paths,將path與request組成文件路徑basePath
      • 若是basePath存在則調用fs.realPathSync獲取文件真實路徑
      • 將文件真實路徑緩存到Module._pathCache(key就是前面生成的cacheKey)
    • fs.realPathSync核心流程有3點:
      • 查詢緩存(緩存的key爲p,即Module._findPath中生成的文件路徑)
      • 從左往右遍歷路徑字符串,查詢到/時,拆分路徑,判斷該路徑是否爲軟鏈接,若是是軟鏈接則查詢真實連接,並生成新路徑p,而後繼續日後遍歷,這裏有1個細節須要注意:
        • 遍歷過程當中生成的子路徑base會緩存在knownHard和cache中,避免重複查詢
      • 遍歷完成獲得模塊對應的真實路徑,此時會將原路徑original做爲key,真實路徑做爲value,保存到緩存中
  • require.resolve.paths等價於Module._resolveLoopupPaths,該方法用於獲取全部的node_modules可能存在的路徑
  • require.resolve.paths實現原理:
相關文章
相關標籤/搜索