项目初始化

  • npm create vite TIndex创建vite项目

  • 进入TIndex目录,npm install 下载配置文件

配置处理

找不到模块vue

image-20230620151159924

在ts的配置文件tsconfig.json中将moduleResolution: "bundler"修改为moduleResolution: "node"

image-20230620153331040

原因:当使用此选项时,TypeScript 将按照 Node.js 的模块解析规则来解析模块。它会根据 node_modules 文件夹和 package.json 文件中的 module 字段来找到模块。

找不到vue文件

image-20230620152547953

解决办法:在自动生成的vite-env.d.ts配置文件中添加如下配置。

1
2
3
4
5
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

​ 原因:TS无法解析vue文件,需要添加配置进行解析

tsconfig.json错误

image-20230620153155337

最新的TS已经废弃了allowImportingTsExtensions配置项,需改为allowSyntheticDefaultImports: true

代码校验、美观配置

Eslint

  1. 安装eslint npm i eslint -D (安装在开发环境)
  2. 生成配置文件 npx eslint --init(后面会有提示,比如仅检验代码规范还是代码问题等按照提示操作即可,最后会生成**.eslintrc.cjs文件**)
  3. vue3环境代码校验插件
1
2
3
4
5
6
7
8
9
10
# 让所有与prettier规则存在冲突的Eslint rules失效,并使用prettier进行代码检查
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
# 运行更漂亮的Eslint,使prettier规则优先级更高,Eslint优先级低
"eslint-plugin-prettier": "^4.2.1",
# vue.js的Eslint插件(查找vue语法错误,发现错误指令,查找违规风格指南
"eslint-plugin-vue": "^9.9.0",
# 该解析器允许使用Eslint校验所有babel code
"@babel/eslint-parser": "^7.19.1",

安装指令

1
npm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser
  1. 在**.eslintrc.cjs**文件中加入如下配置(出自尚硅谷——硅谷甄选项目)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// @see https://eslint.bootcss.com/docs/rules/

module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true,
},
/* 指定如何解析语法 */
parser: 'vue-eslint-parser',
/** 优先级低于 parse 的语法解析配置 */
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: '@typescript-eslint/parser',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true,
},
},
/* 继承已有的规则 */
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
plugins: ['vue', '@typescript-eslint'],
/*
* "off" 或 0 ==> 关闭规则
* "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行)
* "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错)
*/
rules: {
// eslint(https://eslint.bootcss.com/docs/rules/)
'no-var': 'error', // 要求使用 let 或 const 而不是 var
'no-multiple-empty-lines': ['warn', { max: 1 }], // 不允许多个空行
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-unexpected-multiline': 'error', // 禁止空余的多行
'no-useless-escape': 'off', // 禁止不必要的转义字符

// typeScript (https://typescript-eslint.io/rules)
'@typescript-eslint/no-unused-vars': 'error', // 禁止定义未使用的变量
'@typescript-eslint/prefer-ts-expect-error': 'error', // 禁止使用 @ts-ignore
'@typescript-eslint/no-explicit-any': 'off', // 禁止使用 any 类型
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-namespace': 'off', // 禁止使用自定义 TypeScript 模块和命名空间。
'@typescript-eslint/semi': 'off',

// eslint-plugin-vue (https://eslint.vuejs.org/rules/)
'vue/multi-word-component-names': 'off', // 要求组件名称始终为 “-” 链接的单词
'vue/script-setup-uses-vars': 'error', // 防止<script setup>使用的变量<template>被标记为未使用
'vue/no-mutating-props': 'off', // 不允许组件 prop的改变
'vue/attribute-hyphenation': 'off', // 对模板中的自定义组件强制执行属性命名样式
},
}

也可以使用以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ["plugin:vue/vue3-recommended", "plugin:prettier/recommended"],
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint", "prettier"],
rules: {
"prettier/prettier": "error",
},
};

  1. 生成忽略文件

文件名.eslintignore忽略dist和node_modules文件夹

Prettier

有了eslint,为什么还要有prettier?eslint针对的是javascript,他是一个检测工具,包含js语法以及少部分格式问题,在eslint看来,语法对了就能保证代码正常运行,格式问题属于其次;

而prettier属于格式化工具,它看不惯格式不统一,所以它就把eslint没干好的事接着干,另外,prettier支持包含js在内的多种语言。

安裝依赖包

npm install -D eslint-plugin-prettier prettier eslint-config-prettier

引入Ant Design组件库

vite按需加载

antd官网vite按需加载推荐在 vite.config.js 文件中引用 ‘’vite-plugin-components‘’,该方法是错误的,官网将vite-plugin-components 已经更名 unplugin-vue-components

  • 安装unplugin-vue-components

npm i unplugin-vue-components -D

  • 在vite.config.ts中引用
1
2
3
4
5
6
7
8
9
10
11
12
13
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
plugins: [
//...
Components({
resolvers: [AntDesignVueResolver()]
})
]
})

随后即可在项目中直接使用所需的组件,不需要引用,再用app.use进行注册。

设置路径别名

如果使用**@代替src**,而不使用相对路径,如下图所示。

image-20230708101030651

则需要配置两个配置文件vite.config.tstsconfig.json

  • vite.config.ts
  1. 引入path模块

import path from 'path'

  1. 在defineConfig中进行如下配置
1
2
3
4
5
6
7
8
export default defineConfig({
//...
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"), //为src配置别名
},
},
}
  • tsconfig.json

​ 在compilerOptions中添加如下配置

1
2
3
4
5
6
7
8
{
"compilerOptions": {
//...
"baseUrl": ".",
"paths": {
"@/*":["src/*"]
}
}

Pinia状态管理

使用步骤

  1. npm安装

npm i piniayarn add pinia

  1. 在main.ts中创建pinia实例进行使用
1
2
3
4
5
6
7
8
9
10
11
// main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')
  1. 通过defineStoreAPI创建store(一般新建一个store文件夹专门存放状态文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { defineStore } from "pinia";

export const useTerminalConfigStore = defineStore("terminalConfig", {
state: () => {
return {
// 状态
};
},
getters: {
// 计算属性
},
actions: {
// 方法,用于进行异步操作和操作状态
},
});

命令组成

TIndex是借鉴Linux的命令格式的,所以命令由三部分组成,分别是命令本身命令参数命令选项。命令本身是必须的,命令参数和命令选项是可选的。

命令选项的作用是说明对命令的要求

命令参数是描述命令的作用对象img

接下来看看TIndex的命令组成,以baidu命令为例(只看与Linux对应的部分)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const baiduCommand: CommandType = {
func: "baidu", // 对应command
params: [ // 对应arguments
{
key: "word",
desc: "搜索内容",
required: true,
},
],
options: [ // 对应options
{
key: "self",
desc: "是否在当前页打开",
alias: ["s"],
type: "boolean", // 用于设置s或者self的类型
defaultValue: false, // 默认值
},
{
key: "picture",
desc: "是否搜索图片",
alias: ["p"],
type: "boolean", // 用于设置s或者self的类型
defaultValue: false, // 默认值
},
],
action(options, terminal) {
...
},
};

命令执行过程

  1. 终端Terminal 输出命令通过doSubmitCommand函数给父组件传递命令,参数为命令字符串
  2. 通过父组件IndexPage调用props.onSubmitCommand提交命令,参数为命令字符串
  3. 组件IndexPage中通过doCommandExecute(函数位于core文件夹中)进行命令解析(解析文本,解析参数),参数为命令字符串,终端组件本身(通过ref获取组件)
  4. 每个命令(core文件夹下)最终都会有一个action函数进行执行命令,最后执行调用action即可

命令解析后的parsedOptions

_ 存放 params,剩下options以[key:value]的形式保存。

getOpts库会将以-或–开头的参数置为当作options,以key:value的形式保存。

当输入user –help时,help后面没有跟参数,那么getOpts库就会将help当作key,value默认置为true,即help: true(注:当输入 user -help只有一个-时,getOpts会把help拆分成字符数组,即h:true,e:true,l:true,p:true)

image-20230722110029147

image-20230722110050416

而当输入user –help you 时,help后面跟了参数,则help的value值为you,即help: you

image-20230722110151715

image-20230722110200186

当输入user help时,getOpts就会将help当作params,置于_数组中。

image-20230722110534132

image-20230722110545151

依据这些获取的参数就可以进行一系列操作,就以user register -u huajiao1 -p 12345 -e 123@qq.com 为例,此时getOpts的解析结果应该是这样的

1
2
3
4
5
6
{
_:['register'],
u:'huajiao1',
p:'12345',
e:'123@qq.com',
}

image-20230722111215469

此时因为_中有子命令,所以会在doCommandExecute中进行判断,并进行递归执行。

1
2
3
4
5
6
7
8
9
10
if (
_.length > 0 &&
command.subCommands &&
Object.keys(command.subCommands).length > 0
) {
// 把子命令当做新命令解析,user login xxx => login xxx
const subText = text.substring(text.indexOf(" ") + 1);
await doCommandExecute(subText, terminal, command);
return; // return是因为只执行子命令即可
}

所以register又会作为新命令再执行一次,即register -u huajiao1 -p 12345 -e 123@qq.com

image-20230722112757031

具体命令

快捷键命令

监听键盘的onkeydown事件,获取按下的键值。将所需要的快捷键都注册到快捷键列表中,快捷键列表中每一个快捷键的参数类型定义如下。

1
2
3
4
5
6
7
8
9
10
// 快捷键类型定义
interface shortcut {
code: string; // 按下的键对应的code
desc?: string; // 案件描述
keyDesc?: string; // 功能描述
ctrlKey?: boolean; // 是否按下ctrl键
shiftKey?: boolean; // 是否按下shift键
metaKey?: boolean; // 是否按下meta键
action: (e: KeyboardEvent, terminal: TerminalType) => void;
}

其中ctrlKeyshiftKeymetaKey参数的作用为用于判定Ctrl+L这样的快捷键,判断时需要跟event的ctrlKey、shiftKey、metaKey键进行比较,因为KeyboardEvent中可以获取到这些键是否按下的布尔值。

image-20230715110831932

随后只需进行比较即可,快捷键实现的完全代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
export const shortcutRegister = (terminal: TerminalType) => {
document.onkeydown = (e: KeyboardEvent) => {
let key = e.key; // 获取快捷键
if (key >= "a" && key <= "z" && !e.metaKey && !e.shiftKey && !e.ctrlKey) {
terminal.inputFocus();
return;
}
let code = e.code;
for (const shortcut of shortcutList) {
if (
code === shortcut.code &&
e.ctrlKey === !!shortcut.ctrlKey && // 取两次反是因为快捷键中可能没有定义ctrlKey、shift或metaKey,所以其值为undefined,需要使用!!将其转换为布尔值
e.shiftKey === !!shortcut.shiftKey &&
e.metaKey === !!shortcut.metaKey
) {
shortcut.action(e, terminal);
}
}
};

// 快捷键类型定义
interface shortcut {
code: string; // 按下的键对应的code
desc?: string; // 案件描述
keyDesc?: string; // 功能描述
ctrlKey?: boolean; // 是否按下ctrl键
shiftKey?: boolean; // 是否按下shift键
metaKey?: boolean; // 是否按下meta键
action: (e: KeyboardEvent, terminal: TerminalType) => void;
}

// 快捷键列表,以后想要新增快捷键只需在这里进行添加即可,注意填写正确其对应的参数
const shortcutList: shortcut[] = [
{
desc: "清屏",
code: "KeyL",
keyDesc: "Ctrl + L",
ctrlKey: true,
action(e, terminal) {
e.preventDefault();
terminal.clear();
},
},
{
desc: "回车执行命令后聚焦文本框",
code: "Enter",
action(e, terminal) {
e.preventDefault();
terminal.inputFocus();
},
},
{
desc: "查看上一条命令",
code: "ArrowUp",
keyDesc: "↑",
action(e, terminal) {
e.preventDefault();
terminal.showPrevCommand();
},
},
{
desc: "查看下一条命令",
code: "ArrowDown",
keyDesc: "↓",
action(e, terminal) {
e.preventDefault();
terminal.showNextCommand();
},
},
];
};

Tab补全快捷键

判断hint是否有值,有值就将其赋给当前输入框,即InputCommand中的text(因为InputCommand的text属性和输入框双向绑定)。

1
2
3
4
5
6
7
const setTabPatching = () => {
if (hint.value) {
InputCommand.value.text = `${hint.value.split(" ")[0]}${
hint.value.split(" ").length > 1 ? " " : ""
}`;
}
};

随即在快捷键列表注册即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const shortcutList: shortcut[] = [
{
code: "Tab",
desc: "快捷键补齐",
action(e, terminal) {
e.preventDefault();
if (terminal.isInputFocus()) {
terminal.setTabPatching();
} else {
return;
}
},
}
]

历史命令(快捷键↑和↓实现)

使用快捷键可查看上一条或者下一条命令,具体快捷键注册在上面已经讲过了。而历史命令的快捷键则是通过终端调用showPrevCommandshowPrevCommand方法去实现。接下来讲这两个方法的具体实现过程。

这两个方法都封装在useHistory这个hook中,定义在与YuTerminal.vue同级目录下。

这个hook接受两个参数,分别是commandList(命令列表,即输入的不为空的有效命令)和inputCommand(当前输入框对应的值),返回四个方法或值,commandHistoryPos(可以看作指针,用于指向当前查看的命令位置)、showPrevCommand、showPrevCommand、listCommandHistory(用于history命令)。

这里以showPrevCommand为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @description: 查看下一条历史命令
* @return {*}
*/
const showNextCommand = () => {
if (commandHistoryPos.value < commandList.length - 1) {
/* 触发条件:指针指向命令列表最后一项之前的项时,此时指针长度小于命令列表长度,将其加1
最后一次触发时:指针指向命令列表最后一项,此时指针长度等于命令列表长度减1
因为指针先加1,再获取命令列表对应的值的
*/
commandHistoryPos.value++;
// 将当前文本框的赋值为命令列表中指针对应的值
inputCommand.value.text = commandList[commandHistoryPos.value].text;
} else if (commandHistoryPos.value === commandList.length - 1) {
// 触发条件:指针指向命令列表最后一项时再点一次↓箭头,此时指针长度超过命令列表长度,指向当前输入的文本框,输入命令文本置为空
commandHistoryPos.value++;
inputCommand.value.text = "";
}
};

help命令

help命令文件夹中有四个文件:CommandHelpBox.vue、HeloBox.vue、helpCommand.ts、heloUtils.ts。

HelpBox.vue

用于呈现所有的命令。

helpCommand.ts

help命令。

  • defineAsyncComponent

    用于异步加载一个组件,接收一个返回Promise的加载函数

    1
    2
    3
    4
    5
    6
    7
    const AsyncComp = defineAsyncComponent(()=>{ 

    return new Promise((resolve, reject) => {
    resolve();
    })

    })

    因为import动态导入也返回一个Promise,所以大多数情况下和import一起使用。

    1
    const AsyncComp = defineAsyncComponent(()=> import('./xxxx.vue'))

当使用defineAsyncComponent异步加载组件时,可能会出现以下警告。

image-20230722144104460

这是因为此时Vue接收了一个响应式的组件(看意思是这个,具体我也不清楚),所以需要使用markRaw(让其变为非响应式)或shallowRef(仅让其浅层为响应式)。如下所示。

image-20230722144616013