项目初始化
配置处理
找不到模块vue
在ts的配置文件tsconfig.json
中将moduleResolution: "bundler"
修改为moduleResolution: "node"
。
原因:当使用此选项时,TypeScript 将按照 Node.js 的模块解析规则来解析模块。它会根据 node_modules
文件夹和 package.json
文件中的 module
字段来找到模块。
找不到vue文件
解决办法:在自动生成的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错误
最新的TS已经废弃了allowImportingTsExtensions
配置项,需改为allowSyntheticDefaultImports: true
。
代码校验、美观配置
Eslint
- 安装eslint
npm i eslint -D
(安装在开发环境)
- 生成配置文件
npx eslint --init
(后面会有提示,比如仅检验代码规范还是代码问题等按照提示操作即可,最后会生成**.eslintrc.cjs文件**)
- 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
|
- 在**.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
|
module.exports = { env: { browser: true, es2021: true, node: true, jest: true, }, parser: 'vue-eslint-parser', 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'],
rules: { 'no-var': 'error', '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-eslint/no-unused-vars': 'error', '@typescript-eslint/prefer-ts-expect-error': 'error', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-namespace': 'off', '@typescript-eslint/semi': 'off',
'vue/multi-word-component-names': 'off', 'vue/script-setup-uses-vars': 'error', 'vue/no-mutating-props': 'off', '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", }, };
|
- 生成忽略文件
文件名.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
1 2 3 4 5 6 7 8 9 10 11 12 13
| import Components from 'unplugin-vue-components/vite' import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({ plugins: [ Components({ resolvers: [AntDesignVueResolver()] }) ] })
|
随后即可在项目中直接使用所需的组件,不需要引用,再用app.use进行注册。
设置路径别名
如果使用**@代替src**,而不使用相对路径,如下图所示。
则需要配置两个配置文件vite.config.ts和tsconfig.json
- 引入path模块
import path from 'path'
- 在defineConfig中进行如下配置
1 2 3 4 5 6 7 8
| export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, }
|
在compilerOptions中添加如下配置
1 2 3 4 5 6 7 8
| { "compilerOptions": { "baseUrl": ".", "paths": { "@/*":["src/*"] } }
|
Pinia状态管理
使用步骤
- npm安装
npm i pinia
或yarn add pinia
- 在main.ts中创建pinia实例进行使用
1 2 3 4 5 6 7 8 9 10 11
|
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')
|
- 通过
defineStore
API创建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的命令格式的,所以命令由三部分组成,分别是命令本身、命令参数、命令选项。命令本身是必须的,命令参数和命令选项是可选的。
命令选项的作用是说明对命令的要求
命令参数是描述命令的作用对象
接下来看看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", params: [ { key: "word", desc: "搜索内容", required: true, }, ], options: [ { key: "self", desc: "是否在当前页打开", alias: ["s"], type: "boolean", defaultValue: false, }, { key: "picture", desc: "是否搜索图片", alias: ["p"], type: "boolean", defaultValue: false, }, ], action(options, terminal) { ... }, };
|
命令执行过程
- 终端Terminal 输出命令通过doSubmitCommand函数给父组件传递命令,参数为命令字符串
- 通过父组件IndexPage调用props.onSubmitCommand提交命令,参数为命令字符串
- 组件IndexPage中通过doCommandExecute(函数位于core文件夹中)进行命令解析(解析文本,解析参数),参数为命令字符串,终端组件本身(通过ref获取组件)
- 每个命令(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
)
而当输入user –help you 时,help后面跟了参数,则help的value值为you,即help: you
当输入user help时,getOpts就会将help当作params,置于_数组中。
依据这些获取的参数就可以进行一系列操作,就以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', }
|
此时因为_中有子命令,所以会在doCommandExecute
中进行判断,并进行递归执行。
1 2 3 4 5 6 7 8 9 10
| if ( _.length > 0 && command.subCommands && Object.keys(command.subCommands).length > 0 ) { const subText = text.substring(text.indexOf(" ") + 1); await doCommandExecute(subText, terminal, command); return; }
|
所以register又会作为新命令再执行一次,即register -u huajiao1 -p 12345 -e 123@qq.com
具体命令
快捷键命令
监听键盘的onkeydown事件,获取按下的键值。将所需要的快捷键都注册到快捷键列表中,快捷键列表中每一个快捷键的参数类型定义如下。
1 2 3 4 5 6 7 8 9 10
| interface shortcut { code: string; desc?: string; keyDesc?: string; ctrlKey?: boolean; shiftKey?: boolean; metaKey?: boolean; action: (e: KeyboardEvent, terminal: TerminalType) => void; }
|
其中ctrlKey、shiftKey、metaKey参数的作用为用于判定Ctrl+L这样的快捷键,判断时需要跟event的ctrlKey、shiftKey、metaKey键进行比较,因为KeyboardEvent中可以获取到这些键是否按下的布尔值。
随后只需进行比较即可,快捷键实现的完全代码如下所示。
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 && e.shiftKey === !!shortcut.shiftKey && e.metaKey === !!shortcut.metaKey ) { shortcut.action(e, terminal); } } };
interface shortcut { code: string; desc?: string; keyDesc?: string; ctrlKey?: boolean; shiftKey?: boolean; metaKey?: boolean; 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; } }, } ]
|
历史命令(快捷键↑和↓实现)
使用快捷键可查看上一条或者下一条命令,具体快捷键注册在上面已经讲过了。而历史命令的快捷键则是通过终端调用showPrevCommand和showPrevCommand方法去实现。接下来讲这两个方法的具体实现过程。
这两个方法都封装在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
|
const showNextCommand = () => { if (commandHistoryPos.value < commandList.length - 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异步加载组件时,可能会出现以下警告。
这是因为此时Vue接收了一个响应式的组件(看意思是这个,具体我也不清楚),所以需要使用markRaw(让其变为非响应式)或shallowRef(仅让其浅层为响应式)。如下所示。