• 搭建简易版cli


    1. vite的 create-app CLI整体架构

     我们可以看到除了我们常用的npm库enquirer(命令行提示)外,还用到了minimist和kolorist这两个库。接下来,

    • 将对create-app CLI中用到的库进行介绍
    • 逐步拆解、分析create-app CLI源码

    2. create-app CLI中用到的库

    2.1 enquirer 可交互的(对话式的)命令行提示

    2.1.1 获取单一值

    const { prompt } = require('enquirer');
     
    const response = await prompt({
      type: 'input',
      name: 'username',
      message: 'What is your username?'
    });
     
    console.log(response); // { username: 'jonschlinkert' }

    通过这个例子,我们可以简单了解一个简单的enquirer实例的用法。关于prompt的属性,type, name, message是必需参数。详见:https://www.npmjs.com/package/enquirer#prompt-options

    2.1.2 内置prompt

    enquirer提供了一系列内置prompt 

    以 Form Prompt 为例,我们可以通过选择合适类型的内置prompt,得到相应的用户输入。

    const { Form } = require('enquirer');
     
    const prompt = new Form({
      name: 'user',
      message: 'Please provide the following information:',
      choices: [
        { name: 'firstname', message: 'First Name', initial: 'Jon' },
        { name: 'lastname', message: 'Last Name', initial: 'Schlinkert' },
        { name: 'username', message: 'GitHub username', initial: 'jonschlinkert' }
      ]
    });
     
    prompt.run()
      .then(value => console.log('Answer:', value))
      .catch(console.error);

    2.2 minimist 轻量级的用于解析命令行参数的工具。

    与常用的命令行解析工具commander相比, minimist更加轻量, commander(7.1.0)的大小为144kb, 而enquirer(1.2.5)的大小只有32.4kb.

    (minimist命令行参数解析,解析后以对象的形式进行访问)

    var args = require('minimist')(process.argv.slice(2));
    
    console.log(args.hello);
    $ node test.js --hello=world
    // world
    $ node test.js --hello world
    // world

    _参数

    var args = require('minimist')(process.argv.slice(2), {
        boolean: ["hello"]   // hello只能被解析为true或者false
    });
    
    console.log(args.hello);
    console.log(args._);
    $ node test.js --hello world
    // true
    // [ 'world' ]  // 可以从argv._中读取传入的参数值
    // src/print.js
    var
    argv = require('minimist')(process.argv.slice(0)); console.log(argv);

    命令行直接运行:

    node src/print.js

    结果:

    { _:
       [ '/Users/cecelia/.nvm/versions/node/v10.21.0/bin/node',
         '/Users/cecelia/lesson1/src/print.js' ] }

    命令行运行:

    node src/print.js hello mama 

     结果:

    { _:
       [ '/Users/cecelia/.nvm/versions/node/v10.21.0/bin/node',
         '/Users/cecelia/lesson1/src/print.js',
         'hello',
         'mama' ] }

    所以如果想从命令行拿到传入的两个参数,可以对argv稍加处理

    var argv = require('minimist')(process.argv.slice(2));
    console.log(argv._.join(', '));

    得到:

    hello, mama

    如果想直接将数据的值传给对应的属性名,可以在命令行运行:

    node src/print.js -a alex -b bama -def --boom=beef

    得到:

    { _: [],
    a: 'alex',
    b: 'bama',
    d: true,
    e: true,
    f: true,
    boom: 'beef' }

    2.3 kolorist

    kolorist 是一个轻量级的使命令行输出带有色彩的工具。并且,说起这类工具,我想大家很容易想到的就是 chalk。不过相比较 chalk 而言,两者包的大小差距并不明显,kolorist为 49.9 kB,chalk(4.1.0)为 33.6 kB。不过 kolorist 可能较为小众,npm 的下载量大大不如后者 chalk,相应地 chalk 的 API 也较为详尽。
    const { red, cyan } = require('kolorist');
    
    console.log(red(`Error: something failed in ${cyan('my-file.js')}.`));

    3. 拆解分析create-app CLI源码

    在创建CLI时,我们通常把命令放在package.json的bin中。create-app CLI 对应的文件根目录下该文件的 bin 配置是这样:
    // pacakges/create-app/package.json
    "bin": {
      "create-app": "index.js",
      "cva": "index.js"
    }

    可以看到create-app的命令就是在这里被注册的,它指向了根目录下的index.js文件。在上面的配置中,我们看到还注册了另外一条命令cva,同样指向index.js,即:运行cva与create-app是等效的。

    下面我们来看下index.js中的实现:

    3.1 依赖引入

    const fs = require('fs')
    const path = require('path')
    const argv = require('minimist')(process.argv.slice(2))
    const { prompt } = require('enquirer')
    const {
      yellow,
      green,
      cyan,
      magenta,
      lightRed,
      stripColors
    } = require('kolorist')

    了解node的同学知道,fs和path是node的内置模块,前者用于与文件相关的功能,后者用于路径相关的操作。

    除此之外,引入了我们上述的三个库,enquirer, minimist, kolorist

    3.2 定义项目模板(含颜色)和文件

    const TEMPLATES = [
      yellow('vanilla'),
      green('vue'),
      green('vue-ts'),
      cyan('react'),
      cyan('react-ts'),
      magenta('preact'),
      magenta('preact-ts'),
      lightRed('lit-element'),
      lightRed('lit-element-ts')
    ]
    TEMPLATES中定义了不同的模板,并给予不同的模板以不同的颜色。
    此外,由于 .gitignore 文件的特殊性,每个项目模版下都是先创建 _gitignore 文件,在后续创建项目的时候再替换掉该文件的命名(替换为 .gitignore)。所以,CLI 会预先定义一个对象来存放需要重命名的文件:
    const renameFiles = {
      _gitignore: '.gitignore'
    }

    3.3 相关工具函数

    copy 函数:用于文件或文件夹复制,将src复制到dest。首先,判断 src 的stat,如果是文件夹(stat.isDirectory()返回true时),进行的是文件夹的复制;否则,将进行文件复制。

    function copy(src, dest) {
      const stat = fs.statSync(src)
      if (stat.isDirectory()) {
        copyDir(src, dest)
      } else {
        fs.copyFileSync(src, dest)
      }
    }

    copyDir 函数:用于文件夹的复制。首先创建文件夹srcDir, 然后通过枚举的方式,将destDir中的每一个文件/文件夹复制到srcDir中

    function copyDir(srcDir, destDir) {
      fs.mkdirSync(destDir, { recursive: true })
      for (const file of fs.readdirSync(srcDir)) {
        const srcFile = path.resolve(srcDir, file)
        const destFile = path.resolve(destDir, file)
        copy(srcFile, destFile)
      }
    }

    emptyDir 函数:用于清空文件夹。首先判断下给出的路径dir是否存在,如果不存在直接返回;若存在则枚举文件夹下的每一个文件/文件夹。当为文件时,调用fs.unlinkSync删除文件;当为文件夹时,递归调用emptyDir函数清空文件夹下的每个文件,然后再调用fs.rmdirSync删除该文件夹。

    function emptyDir(dir) {
      if (!fs.existsSync(dir)) {
        return
      }
      for (const file of fs.readdirSync(dir)) {
        const abs = path.resolve(dir, file)
        // baseline is Node 12 so can't use rmSync :(
        if (fs.lstatSync(abs).isDirectory()) {
          emptyDir(abs)
          fs.rmdirSync(abs)
        } else {
          fs.unlinkSync(abs)
        }
      }
    }

    4. 核心函数

    4.1 基础依赖引入

    我们会使用create-app my-project来创建项目(间接定义了目录)。

    我们在上面讲minimist提到过 argv._ 是一个读取命令行参数的数组,这里,argv._[0] 代表 create-app 后的第一个参数(my-project),如果没有读到这个参数的值,就会通过命令行提示(enquirer prompt)的方式,让你输入或直接回车使用默认值vite-project。然后,通过 path.join 函数构建的完整文件路径root。接下来,在命令行中会输出提示,告述你脚手架(Scaffolding)项目创建的文件路径。

    let targetDir = argv._[0]
      if (!targetDir) {
        /**
         * @type {{ name: string }}
         */
        const { name } = await prompt({
          type: 'input',
          name: 'name',
          message: `Project name:`,
          initial: 'vite-project'
        })
        targetDir = name
      }
    
      const root = path.join(cwd, targetDir)
      console.log(`
    Scaffolding project in ${root}...`)

    接下来,会判断root是否存在:不存在会创建新的目录

     if (!fs.existsSync(root)) {
        fs.mkdirSync(root, { recursive: true })
      } else {
        // 文件夹存在时
      }

    反之,若文件夹存在,会进一步判断文件夹下是否存在文件。当存在文件,即: if (existing.length) 的结果为true,会提示是否清空已有文件夹下的内容。命令行输入 Y , 会清空文件夹;输入N, 不清空该文件夹,同时整个 CLI 的执行会退出。

    const existing = fs.readdirSync(root)
        if (existing.length) {
          /**
           * @type {{ yes: boolean }}
           */
          const { yes } = await prompt({
            type: 'confirm',
            name: 'yes',
            initial: 'Y',
            message:
              `Target directory ${targetDir} is not empty.
    ` +
              `Remove existing files and continue?`
          })
          if (yes) {
            emptyDir(root)
          } else {
            return
          }
        }

    4.2 确定项目模板

    在创建好项目文件夹后,CLI 会获取 --template(或--t) 选项

    npm init @vitejs/app --template 文件夹名

    如果没有--template或者--t,会通过提示让用户选择一个模板。

      let template = argv.t || argv.template
      if (!template) {
        /**
         * @type {{ t: string }}
         */
        const { t } = await prompt({
          type: 'select',
          name: 't',
          message: `Select a template:`,
          choices: TEMPLATES
        })
        template = stripColors(t)
      }
    由于,TEMPLATES 中只是定义了模版的类型,对比起 packages/create-app 目录下的项目模版文件夹命名有点差别(缺少 template 前缀),所以需要给 template 拼接前缀和构建完整目录:
      const templateDir = path.join(__dirname, `template-${template}`)

    4.3 写入项目目录

    读取 templateDir 目录下的文件名,除package.json外,依次写入。

     const files = fs.readdirSync(templateDir)
      for (const file of files.filter((f) => f !== 'package.json')) {
        write(file)
      }

    上述过程用到了write函数。

    write 函数则接受两个参数 filecontent,其具备两个能力:

    • 对指定的文件 file 写入指定的内容 content,调用 fs.writeFileSync 函数来实现将内容写入文件

    • 复制模版文件夹下的文件到指定文件夹下,调用前面介绍的 copy 函数来实现文件的复制

    在write函数中,首先对即将写入的文件名进行判断,是否是先前定义的 renameFiles 中的属性名(_gitignore), 若是,则替换文件名,并进行路径拼接;否则无需替换,直接拼接。然后,进行内容写入。如果没有传入content, 则将模板目录下的相应的文件复制过来。

     const write = (file, content) => {
        const targetPath = renameFiles[file]
          ? path.join(root, renameFiles[file])
          : path.join(root, file)
        if (content) {
          fs.writeFileSync(targetPath, content)
        } else {
          copy(path.join(templateDir, file), targetPath)
        }
      }
    在写入模版内的这些文件后,CLI 就会处理 package.json 文件。之所以单独处理 package.json 文件的原因是每个项目模版内的 package.jsonname 都是写死的,而当用户创建项目后,name 都应该为该项目的文件夹命名。这个过程对应的代码会是这样:
      const pkg = require(path.join(templateDir, `package.json`))
      pkg.name = path.basename(root)
      write('package.json', JSON.stringify(pkg, null, 2))

    其中,path.basename 函数则用于获取一个完整路径的最后的文件夹名。

    当这一切都完成后,命令行输出,依赖安装和启动命令提示。(要判断一下使用的是npm还是yarn, 还要判断下root是否是当前工作路径,并视情况予以输入提示)
     const pkgManager = /yarn/.test(process.env.npm_execpath) ? 'yarn' : 'npm'
    
      console.log(`
    Done. Now run:
    `)
      if (root !== cwd) {
        console.log(`  cd ${path.relative(cwd, root)}`)
      }
      console.log(`  ${pkgManager === 'yarn' ? `yarn` : `npm install`}`)
      console.log(`  ${pkgManager === 'yarn' ? `yarn dev` : `npm run dev`}`)
      console.log()
    }

    源码地址: https://github.com/vitejs/vite/blob/main/packages/create-app/index.js

    参考:https://juejin.cn/post/6926648505008128008#heading-7

  • 相关阅读:
    ASP.NET Core
    ASP.NET Core
    ASP.NET Core
    ASP.NET Core
    通用查询设计思想(2)- 基于ADO.Net的设计
    API接口通讯参数规范(2)
    Centos7 安装selenium(python3.7 centos7 )
    mysql (create temporary table table_name )临时表创建
    C# 字符串 String、StringBuffer和StringBuilder的区别
    安装Elasticsearch
  • 原文地址:https://www.cnblogs.com/ceceliahappycoding/p/14405438.html
Copyright © 2020-2023  润新知