
橙某人
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()
我们执行这个文件,会有依次这些交互问答:



相信看完这个例子你明白 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)); // 文件的上两级目录名称

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
选项,这两个后续我们会做一个完整的实现。

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

可以看到,在 options
属性上会储存这些选项的信息。
然后你可以执行 juejin-vue-cli create projectName -d
命令,查看 const options = cleanArgs(cmd);
语句的处理结果:

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


哦,对了(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
的工具包,里面写了很多工具方法,很多是值得我们去研究和学习的,感兴趣的小伙伴可以细细导读看看。

创建核心文件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
文件夹内。


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

做完这些后,你可以执行一下 juejin-vue-cli create test
命令,没有报错就是对的。
那么本章就先写到这里了,下一篇文章我们再来具体完善 Creator.js
文件的逻辑。

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

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

橙某人
打杂工 | 广州郊区建筑工地