create-vue的cli分析模拟

60 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情

create-vue

使用

create-vue是生成vue模板项目的cli,我们可以使用

npm create vue@latest <packagName>

效果如下:

image.png 使用的体验就是非常的方便且高效,快速生成自己需要或者喜欢的项目模板,并且交互设计非常的人性化。 因此我也是抱着学习学习的态度来把这个开源的create-vue项目浅析一下。

项目结构

项目源码在vuejs/create-vue: (github.com),clone到本地分析一下。结构很简单

image.png index是主要的cli代码,template存储了模板,untils有一些根据函数,例如render(),我们主要看index.ts。

使用的几个库

zx库,方便前端执行shell的库,以.mjs为后缀的文件,可以直接通过zx ./script.mjs运行,它主要是相对于shell,更加适合前端人员,且非常方便快速,而且可以使用async/await,console.log()等同于echo,$"..."执行shell命令。 内置了question(提问),chalk(文本颜色)等库

prompts,类似于question,只是它能够提供默认值,让用户选择,更加的人性化。

minimist,解析命令行参数。

fs, node的服务器文件管理模块,能够对文件做出很多操作。

以上几个库就能写出非常漂亮且人性化的cli,我也是浅写了个demo来尝试一下(上面这几个库没有中文文档,本人英语也差,资料也少,所以demo很简单)

#!/usr/bin/env zx
// import { chalk, question } from "zx/globals";
import prompts from "prompts";
// import { chalk } from "zx/.";
try {
    const color = await question(`请选择一个风格: ${chalk.cyan("cyan")} | ${chalk.green("green")} | ${chalk.hex("#ffcccc").bold("pink")}: `, {
        choice: ["蓝色", "绿色", "天蓝"]
    });

    let log = () => { };
    switch (color) {
        case "cyan": log = (string) => (console.log(chalk.cyan(string)))
            break;
        case "green": log = (string) => (console.log(chalk.green(string)));
            break
        case "pink": log = (string) => (console.log(chalk.hex("#ffcccc").bold(string)))
            break
        default: console.log("default");
            break;
    }
    log(`现在使用${color}风格`);
    log("");
    (async () => {
        const response = await prompts([
            {
                type: 'number',
                name: 'value',
                message: chalk.green('How old are you?'),
                validate: value => value < 18 ? `Nightclub is 18+ only` : true
            },
            {
                name: 'sex',
                type: () => ('toggle'),
                // yoggle
                message: chalk.green("your sex?"),
                initial: false,
                active: '男',
                inactive: '女'
            },
            {
                name: 'name',
                type: 'text',
                message: chalk.green('your name?')
            },
        ]);
        const { value, sex, name } = response;
        log(`你好!${name},你的性别是${sex},年龄是${value}`); // => { value: 24 }
    })();
} catch (e) {
    console.log(e)
}

image.png

init()

可以看到init函数就是入口函数,提供minimist读取参数

  const cwd = process.cwd()
  //cwd是提供node的cwdapi获取路径
  const argv = minimist(process.argv.slice(2), {
  //获取命令行参数
    alias: {
      typescript: ['ts'],
      'with-tests': ['tests'],
      router: ['vue-router']
    },
    // all arguments are treated as booleans
    boolean: true
  })

根据minimist获取的已有参数生成一个prompts选择列表,选择完成或返回一个string数组,

    result = await prompts(
      [
      //name属性定义结果的属性名,message是问题,type是输入的类型
        {
          name: 'projectName',
          type: targetDir ? null : 'text',
          message: 'Project name:',
          initial: defaultProjectName,
          onState: (state) => (targetDir = String(state.value).trim() || defaultProjectName)
        },
        {
          name: 'shouldOverwrite',
          type: () => (canSkipEmptying(targetDir) || forceOverwrite ? null : 'confirm'),
          message: () => {
            const dirForPrompt =
              targetDir === '.' ? 'Current directory' : `Target directory "${targetDir}"`

            return `${dirForPrompt} is not empty. Remove existing files and continue?`
          }
        },
         //... 很多,省略
      ],
      {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        }
      }
    )

接着创建文件

  const root = path.join(cwd, targetDir)

  if (fs.existsSync(root) && shouldOverwrite) {
    emptyDir(root)
    //这里主要是处理是否已存在6路径,
  } else if (!fs.existsSync(root)) {
    fs.mkdirSync(root)
  }

往后就很单一了,创建基础模板,然后根据选择的添加项依次调用render()函数将各种选择渲染进基础模板。 我们再来看看renderTemplate函数

function renderTemplate(src, dest) {
 const stats = fs.statSync(src)

 if (stats.isDirectory()) {
   // skip node_module
   if (path.basename(src) === 'node_modules') {
     return
   }
   fs.mkdirSync(dest, { recursive: true })
   for (const file of fs.readdirSync(src)) {
     renderTemplate(path.resolve(src, file), path.resolve(dest, file))
   }
   //这里是假如说仍然是一个路径,那就render这个路径。
   return
 }

 const filename = path.basename(src)
 //经过上面操作,现在filename就一定是file

 //....后面做一些文件处理

 fs.copyFileSync(src, dest)
}

可以看到,通篇都是fs和path操作,这里不熟悉fs/path的api头都要疼死了!我浅说一下这两个模块。

首先是Stats对象,它是路径对象,而fs.statSync("path")就能够返回一个Status对象,我们可以调用对象身上的一些方法isFile() isDirectory(),很简单就是判断是文件夹还是文件。

path对象有许多方法,诸如获取相对/绝对路径,路径拼接等等。

结语

本次的文章到这里就结束啦!♥♥♥读者大大们认为写的不错的话点个赞再走哦 ♥♥♥

每天一个知识点,每天都在进步!♥♥