如何实现脚手架开发

经过学习慕课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实现原理:
相关文章
相关标签/搜索