橙某人

V1

2022/09/15阅读:18主题:萌绿

硬刚VueCli3源码系列二 - 创建入口文件create.js、确定项目名称和目录、创建核心文件Creator.js

写在开头

VueCli3源码系列 从发布第一篇文章到现在已经是时隔一月之久了,唉⊙﹏⊙|||,真的是挺忙的(也有一些懒的成分),真的佩服一些大佬能做到日更或者周更,写文真的是不易啊。

不过,咱们不多说了,赶紧来开始本章的内容,讲完小编还得赶着去写 Bug 呢。

我们接着上一篇文章 硬刚VueCli3源码系列一 - 从零搭建项目架构,项目准备、创建项目软链接、初创create命令 的内容来说,上一篇文章,我们已经初步创建完项目的基本目录结构了。

预备知识

inquirer模块

inquirer 模块是一个用户能与命令行交互的工具。

啥意思呢?不用着急,我们直接来看下面的例子你就明白了。

先下载安装:

npm install inquirer

新建测试文件 test-inquirer.js 文件:

const inquirer = require('inquirer')

async function fn({
    // 异步过程
    let res = await inquirer.prompt([
        {
            type'input'// 输入框
            name'project'// 获取结果的key
            message'项目名称'// 描述
            default'我是默认名称',  // 默认值
        },
        {
            type'list'// 单选
            name'type',
            message'项目类型',
            default'vue'// 默认选项
            choices: [ // 选项
                { name'vue'value'vue' },
                { name'react'value'react' },
                { name'jq'value'jq' },
            ]
        }
    ]);
    console.log(res); // { project: '', type: '' }
}

fn()

我们执行这个文件,会有依次这些交互问答:

image.png
image.png
image.png
image.png
image.png
image.png

相信看完这个例子你明白 inquirer 模块的作用了,是不是很简单(✪ω✪),它还有很多其他的交互组件:

const input = {
  type'input',
  name: '输入框',
  message: 'input',
}
const number = {
  type'number',
  name: '数字输入框',
  message: 'number',
}
const password = {
  type'password',
  name: '密码框',
  message: 'password',
}
const list = {
  type'list',
  name: '单选',
  message: 'list',
  choices: [
    { name: '1', value: 1 },
    { name: '2', value: 2 },
    { name: '3', value: 3 }
  ]
}
const rawlist = {
  type'rawlist',
  name: '列表',
  message: 'rawlist',
  choices: [
    { name: '1', value: 1 },
    { name: '2', value: 2 },
    { name: '3', value: 3 }
  ]
}
const checkbox = {
  type'checkbox',
  name: '多选',
  message: 'checkbox',
  choices: [
    { name: '1', value: 1 },
    { name: '2', value: 2 },
    { name: '3', value: 3 }
  ]
}
const confirm = {
  type'confirm',
  name: '判断',
  message: 'confirm',
}
const editor = {
  type'editor',
  name: '编辑器',
  message: 'editor',
}

这里列举一些常用的组件,更多组件和用法就自行查看 官方文档 啦。

文件绝对路径与上级目录名称获取

新建测试 path-relative.js 测试文件:

const path = require('path');
const cwd = process.cwd();

console.log(cwd); // 文件所在的绝对路径
console.log(path.relative('../', cwd)); // 文件的上级目录名称
console.log(path.relative('../../', cwd)); // 文件的上两级目录名称
image.png
image.png

path.relative() 能帮助获取到当前目录的名称。

创建入口文件create.js

我们先来改造一下 bin/vue.js 文件,让它引进入口文件 create.js

#!/usr/bin/env node
const program = require('commander');

program
  .version(`juejin-vue-cli ${require('../package').version}`'-v, --version')
  .usage('命令错误,您可以输入 juejin-vue-cli -h 命令查看具体命令'); 

program
  .command('create <app-name>')
  .description('这是一条初始化项目的命令'
  .option('-d, --default''忽略提示符并使用默认预设选项'// 添加 create 命令的一个可选项
  .option('-p, --preset <presetName>''Skip prompts and use saved or remote preset')
  .option('-i, --inlinePreset <json>''忽略提示符并使用内联的 JSON 字符串预设选项')
  .action((name, cmd) => { 
    const options = cleanArgs(cmd); // 获取 create 命令的其他一些可选项
    require('../lib/create')(name, options); // 入口文件
});

/**
 * 把当前命令的其他 option() 参数转成对象接收, 方便处理
 * @param {*} cmd: commander对象信息
 */

function cleanArgs (cmd{
  const args = {}
  // cmd.options 存放着该条命令配置的其他 option() 参数配置信息
  cmd.options.forEach(o => {
    // o.long是 全称 选项标识: --default
    const key = camelize(o.long.replace(/^--/''));
    if (typeof cmd[key] !== 'function' && typeof cmd[key] !== 'undefined') {
      args[key] = cmd[key]
    }
  })
  return args
}

/**
 * 如果是 --verson-git 那么下划线转成驼峰的形式返回
 * @param {*} str
 */

function camelize (str{
  // \w: 查找数字、字母及下划线。
  return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : '');
}
// 做一个简单的提示
if(process.argv.length <= 2) {
  console.log('请输入相关命令操作,可以输入提供帮助:junjin-vue-cli -h');
}

program.parse(process.argv);

上面的改动虽然很多,但不要懵,实际它就做了一件事情,也就是接收 create 命令的 一些可选项,然后引入核心文件 create.js 并执行它,再把可选项参数传递进去。

这里涉及了一个 可选项 知识,如果你熟读文档应该知道,create 命令可不单单只有这种形式:
vue create projectName
它还可以这样子:
vue create projectName -d
vue create projectName --default (单词全称写法)
vue create projectName -i

更多选项就对照下图查看,上面代码中我们预设了 -d-i 选项,这两个后续我们会做一个完整的实现。

image.png
image.png

这里你可以再次打印 cmd 对象看看:

image.png
image.png

可以看到,在 options 属性上会储存这些选项的信息。

然后你可以执行 juejin-vue-cli create projectName -d 命令,查看 const options = cleanArgs(cmd); 语句的处理结果:

image.png
image.png

如果你的参数是 -i 那结果会是这样:{ inlinePreset: true }

好了,讲了那么多,下面我们就该来创建 create.js 文件了:

// 内部具体逻辑会使用到 await 所以我们把 async 先加上。
async function create (projectName, options{

}

module.exports = (...args) => {
  // 执行create()方法, 注意async会使方法返回一个Promise
  return create(...args).catch(err => {
    console.log('错误信息:', err);
  })
}

从前面的引入 require('../lib/create')(name, options); 使用,我们可以知道 create.js 文件默认导出一个函数。

确定项目名称和项目目录

下面,我们来编写 create.js 文件的具体逻辑,这个文件主要会做一件事情,就是确定项目的最终名称目录

// create.js
const path = require('path');
const fs = require('fs-extra');
const inquirer = require('inquirer');
const { exit } = require('@vue/cli-shared-utils/lib/exit');

async function create (projectName, options{
  // 获取当前执行命令的绝对路径
  const cwd = options.cwd || process.cwd();
  const inCurrent = projectName === '.';
  // path.relative('../', cwd)能帮助获取到当前目录的名称: http://nodejs.cn/api/path.html#pathrelativefrom-to
  const name = inCurrent ? path.relative('../', cwd) : projectName;
  const targetDir = path.resolve(cwd, projectName || '.');
    // 判断文件名是否存在
    if (fs.existsSync(targetDir)) {
        if (inCurrent) {
            const { ok } = await inquirer.prompt([
              {
                name'ok',
                type'confirm',
                message`是否直接在当前目录下创建项目?`
              }
            ]);
            if (!ok) return;
        }else {
            console.log('创建的项目名已存在-逻辑处理');
            exit(1); // Vue封装的退出命令行工具的方法, 本质是执行process.exit(code)
        }
    }
    console.log(name, targetDir)
}

module.exports = (...args) => {
  return create(...args).catch(err => {
    console.log('错误信息:', err);
  })
}

编写了怎么多行代码后,它们的作用是什么呢?你可以直接阅读代码和注释相信也能猜个八九不离十了,或者,也可以看下图执行三条命令后得到的结果。

image.png
image.png
image.png
image.png

哦,对了(T▽T),要执行命令,你还得下载相关的依赖,如下:

npm install inquirer@6.0.0 fs-extra@7.0.1 @vue/cli-shared-utils@3.12.1

这里值得注意的是 @vue/cli-shared-utils 包,这是 Vue 的工具包,里面写了很多工具方法,很多是值得我们去研究和学习的,感兴趣的小伙伴可以细细导读看看。

image.png
image.png

创建核心文件Creator.js

我们继续看编写 create.js 文件:

// create.js
...
const { getPromptModules } = require('./util/createTools'); // 引入新文件
const Creator = require('./Creator'); // 引入新文件

async function create (projectName, options{
  ...
  console.log(name, targetDir);
  /*
  * name: 项目最终的项目名
  * targetDir: 项目最终的目录
  * 第三个参数: 问答选项与选项文件的映射
  */

  const creator = new Creator(name, targetDir, getPromptModules());
  await creator.create(options); // 核心方法
}

module.exports = (...args) => {
  return create(...args).catch(err => {
    console.log('错误信息:', err);
  })
}

上面我们引入核心文件,并实例化文件中的对象,下面我们对应的来创建这些文件。

创建 Creator.js 文件:

module.exports = class Creator {
  constructor (name, context, promptModules) {}
  async create(cliOptions = {}, preset = null) {}
}

创建 ./util/createTools.js 文件:

/**
 * 这是命令操作时, 脚手架提供的选项与具体选项文件的映射
 * @returns
 */

 exports.getPromptModules = () => {
  return [
    // 'babel',
    // 'typescript',
    // 'pwa',
    'router',
    'vuex',
    // 'cssPreprocessors',
    // 'linter',
    // 'unit',
    // 'e2e'
  ].map(file => require(`../promptModules/${file}`))
}

createTools.js 文件是一个选项与选项文件之间的映射文件。

当我们执行 create 命令后,选择 Manuall 选项后,会有一些选项可以供选择。假如你选择了 Router 选项,那么后续还会让询问你路由是使用 history 还是 hash 模式,Vue 把这每一个选项的后续操作都单独写在一个文件中,方便分类管理,它们都存放在 promptModules 文件夹内。

image.png
image.png
image.png
image.png

上面我们先只保留 routervuex 两个选项,并对应创建 ../promptModules/router.js../promptModules/vuex.js 文件。

image.png
image.png

做完这些后,你可以执行一下 juejin-vue-cli create test 命令,没有报错就是对的。

那么本章就先写到这里了,下一篇文章我们再来具体完善 Creator.js 文件的逻辑。

image.gif
image.gif



至此,本篇文章就写完啦,撒花撒花。

image.png
image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。

分类:

前端

标签:

Vue.js

作者介绍

橙某人
V1

打杂工 | 广州郊区建筑工地