说明:
之前因为做一个项目从零到一搭建了Vue3的后台管理系统,写本文的时候本想采用现行版本,结果遇到了很多插件版本兼容问题。想起之前踩得那些坑,望而却步,时间、水平有限,故放弃了这个想法。
本文使用 node 14.16.1
版本,如果您的 node 版本与该版本相近,可以尝试能否兼容,也可以使用nvm安装该版本。
1. Vite创建项目
说到 Vue3 就不得不说一下 Vite,如果你受够了 webpack 启动服务40s+,热更新6s+的
话,一定要来看看 Vite,真的很香!
这里,我也是用 Vite 进行项目创建,文档提供了示例,可以使用
yarn create vite my-vue-app --template vue 复制代码
进行Vite+Vue项目创建,下面还提供了几种模板预设,因为要使用 ts,所以这里我使用的命令是
yarn create vite my-vue-app --template vue-ts 复制代码
然后进入目录,yarn 安装依赖,yarn dev 启动服务就好了
说明:因为 Vite 一直更新版本问题,所以如果您按上述命令创建项目,与该分支上代码并不一致。《项目地址》
2. 集成JEST单元测试
2-1. 安装jest
yarn add jest@26.6.3 -D 复制代码
2-2. 创建第一个测试内容
在根目录创建 tests 文件夹,再在内部创建 unit 文件夹存放单元测试文件,unit 文件夹内编写我们的第一个测试文件 index.spec.js
test('1+1=2',() => { expect(1+1).toBe(2) }) 复制代码
在 package.json
配置命令
"scripts": { "test-unit":"jest" } 复制代码
运行 yarn test-unit
发现第一个测试用例就通过了
2-3. 添加自动提示插件
上面在编写测试用例的时候,是没有代码提示的,这导致效率很低,这里安装
@types/jest实现代码提示
yarn add @types/jest@26.0.24 -D 复制代码
2-4. 支持import
接下来我们在unit文件夹下编写一个测试用文件 foo.js
export default function (){ return 'this is foo' } 复制代码
修改 index.spec.js
import foo from './foo.js' test('1+1=2',() => { expect(1+1).toBe(2); }) test('foo',() => { expect(foo()).toBe('this is foo') }) 复制代码
再次执行 yarn test-unit
发现报错了
会发现 jest 无法识别 import 语法,这是因为 jest 是基于 node 环境的,所以要将 import 这种语法转化为 nodejs 可以识别的语法
在根目录创建 jest.config.js
进行 jest 配置
module.exports = { // 转换器 transform: { // jest解析js的时候通过babel-jest解析 "^.+\\.jsx?$": "babel-jest" } }; 复制代码
安装 babel-jest
yarn add babel-jest@26.6.3 -D 复制代码
因为使用了 babel,所以还需要创建在根目录 babel.config.js
配置babel
module.exports = { presets: [ [ // 安装官方预设插件 "@babel/preset-env", // 指定解析的目标是本机node版本 { targets: { node: "current" } } ], ], }; 复制代码
安装 @babel/preset-env
yarn add @babel/preset-env@7.14.9 -D 复制代码
再次运行 yarn test-unit
发现测试通过了
2-5. 支持 .vue文件
在src/compontents下创建 Foo.vue
<template> <div> Foo </div> </template> 复制代码
修改 index.spec.js
import foo from './foo.js' import Foo from '../../src/components/Foo.vue 'test('1+1=2',() => { expect(1+1).toBe(2); }) test('foo',() => { expect(foo()).toBe('this is foo') }) test('Foo',() => { console.log('Foo',Foo) }) 复制代码
执行 yarn test-unit
发现报错无法解析vue的语法
这里需要在 jest.config.js
中新增解析 .vue 文件的规则
module.exports = { // 转换器 transform: { // jest解析js的时候通过babel-jest解析 "^.+\\.jsx?$": "babel-jest", // jest解析vue的时候通过vue-jest解析 "^.+\\.vue$": "vue-jest" } }; 复制代码
安装 vue-jest
yarn add vue-jest@next -D 复制代码
再次执行 yarn test-unit
发现测试失败,缺少 ts-jest 依赖
安装 ts-jest
yarn add ts-jest@26.5.6 -D 复制代码
再次执行 yarn test-unit
测试通过
2-6. 安装 Vue Test Utils
Vue Test Utils 是Vue官方推荐的Vue单元测试库,提供对vue文件测试的支持
yarn add @vue/test-utils@next -D 复制代码
修改 index.spec.js
import foo from './foo.js' import {mount} from '@vue/test-utils' import Foo from '../../src/components/Foo.vue 'test('1+1=2',() => { expect(1+1).toBe(2); }) test('foo',() => { expect(foo()).toBe('this is foo') }) test('Foo',() => { console.log('Foo',Foo) console.log('mount',mount(Foo)) }) 复制代码
执行 yarn test-unit
测试通过
2-7. 支持ts
将之前的 js 测试文件直接改成 .ts 文件再 yarn test-unit
肯定是不通过的,这里和之前一样,需要在jest.config.js
中新增转换器配置
module.exports = { // 转换器 transform: { // jest解析js的时候通过babel-jest解析 "^.+\\.jsx?$": "babel-jest", // jest解析vue的时候通过vue-jest解析 "^.+\\.vue$": "vue-jest", // jest解析ts的时候通过ts-jest解析 "^.+\\.tsx?$": "ts-jest" } }; 复制代码
ts-jest 之前已经安装过了,这里不需要重复安装
修改 babel.config.js
配置
module.exports = { presets: [ [ // 安装官方预设插件 "@babel/preset-env", // 指定解析的目标是本机node版本 { targets: { node: "current" } } ], "@babel/preset-typescript" ] }; 复制代码
安装 @babel/preset-typescript
yarn add @babel/preset-typescript@7.14.5 -D 复制代码
执行 yarn test-unit
测试通过 《项目地址》
3. 集成Cypress e2e测试
3-1. 安装Cypress
yarn add cypress@8.2.0 -D 复制代码
package.json
配置命令 "test-e2e":"cypress open“
并执行
第一次执行的时候会进行初始化,帮我们在根目录创建一个 cypress 文件夹,里边会有 cypress 相关的文件,初始化完成后会自动打开一个窗口
这里会有一个弹框供我们选择会在什么 CI 环境下使用 cypress,这里我们用不到,直接关闭即可
右上角可以切换运行测试环境的浏览器 相关文档
下面是所有的测试用例,点击会在浏览器中打开根据脚本进行测试
3-2. 调整目录
在我们的项目中,是希望所有测试相关的东西都放在 tests 文件夹下的,这里我们在 tests 下创建 e2e 文件夹,并将根目录 cypress 下的所有文件拷贝过来,这个时候如果我们再次执行
yarn test-e2e
,会发现 cypress 每次都会检测根目录是否有 cypress 文件夹,没有会再次创建,所以我们需要在 cypress.json
中修改配置
{ "pluginsFile":"tests/e2e/plugins/index.js" } 复制代码
然后修改 plugins/index.js
中的配置 相关文档
module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config return Object.assign({}, config, { // fixtures路径 fixturesFolder: "tests/e2e/fixtures", // 测试脚本文件夹 integrationFolder: "tests/e2e/specs", // 从 cy.screenshot() 命令或在 cypress 运行期间测试失败后保存屏幕截图的文件夹路径 screenshotsFolder: "tests/e2e/screenshots", // cypress 运行期间保存视频的文件夹路径 videosFolder: "tests/e2e/videos", // 在加载测试文件之前加载的文件路径。 这个文件被编译和捆绑。 (通过 false 禁用) supportFile: "tests/e2e/support/index.js" }); } 复制代码
需要注意的是这里将之前的 integration 文件夹重命名为 specs
执行 yarn test-e2e
出现如下弹窗
并且没有在根目录创建 cypress 文件夹
3-3. 支持 ts
首先把e2e文件夹下所有 .js 文件修改为 .ts
然后修改 e2e/plugins/index.ts
中的配置
module.exports = (on: any, config: Cypress.PluginConfig) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config return Object.assign({}, config, { // fixtures路径 fixturesFolder: "tests/e2e/fixtures", // 测试脚本文件夹 integrationFolder: "tests/e2e/specs", // 从 cy.screenshot() 命令或在 cypress 运行期间测试失败后保存屏幕截图的文件夹路径 screenshotsFolder: "tests/e2e/screenshots", // cypress 运行期间保存视频的文件夹路径 videosFolder: "tests/e2e/videos", // 在加载测试文件之前加载的文件路径。 这个文件被编译和捆绑。 (通过 false 禁用) supportFile: "tests/e2e/support/index.ts", }); }; 复制代码
修改 cypress.json
中的配置
{ "pluginsFile":"tests/e2e/plugins//index.ts" } 复制代码
清除 tests/e2e/specs 下的测试示例文件,创建 index.spec.ts
describe("index", () => { it("button click", () => { cy.visit("http://localhost:3000/"); cy.get("button").click(); }); }); 复制代码
执行 yarn test-e2e
,弹窗如下:
点击 index.spec.js
,cypress 会帮我们打开配置的浏览器并执行测试脚本
可以发现已经触发了按钮点击,因为上方红色数值由0变为了1,而且左侧提示
index.spec.ts
中的button click 测试用例通过
3-4. 解决 jest 测试覆盖 e2e 问题
此时执行 yarn test-unit
会发现报错了
此时我们发现执行 jest 单元测试把 cyress 的测试文件也执行了,修改
jest.config.js
配置,指定测试脚本匹配规则
module.exports = { // 转换器 transform: { // jest解析js的时候通过babel-jest解析 "^.+\\.jsx?$": "babel-jest", // jest解析vue的时候通过vue-jest解析 "^.+\\.vue$": "vue-jest", // jest解析ts的时候通过ts-jest解析 "^.+\\.tsx?$": "ts-jest" }, // 配置测试脚本文件匹配规则 testMatch: ["**/tests/unit/?(*.)+(spec).[jt]s?(x)"], }; 复制代码
再依次执行 yarn test-unit
yarn test-e2e
发现都可以通过了
3-5. 整合 jest & cypress 测试
package.json 配置命令"test":"npm run test-unit && npm run test-e2e"
并执行
可以在终端中看到jest测试通过,并打开了cypress窗口,那此时我们其实希望 cypress 的测试也在终端中进行。
cypress提供了另一种执行测试的命令 cypress run
,我们在 package.json
中添加命令
"test-e2e-ci":"cypress run"
并执行,发现此时 cypress 测试可以在终端中进行了,修改 test 命令如下2
"test":"npm run test-unit && npm run test-e2e-ci" 复制代码
执行 yarn test
,此时 jest 和 cypress 都可以在终端中完成了
3-6. 关闭 cypress 生成视频行为
当我们在终端中进行 cypress 测试的时候,会发现在 tests/e2e 文件夹下多了一个 video 文件夹,里边有一个 index.sepc.ts.mp4
文件,打开会发现该视频就是对应测试文件测试过程的录屏,这里我们其实是不需要的,每次生成.mp4文件会占用内存和增加测试时间,可以在 cypress.json
中进行配置关闭
{ "pluginsFile":"tests/e2e/plugins/index.ts", "video":false } 复制代码
至此,集成 cypress e2e 测试完成。《项目地址》
4. 集成eslint
4-1. 安装eslint及相关依赖
yarn add eslint@7.20.0 eslint-plugin-vue@7.6.0 @vue/eslint-config-typescript@7.0.0 @typescript-eslint/parser@4.15.2 @typescript-eslint/eslint-plugin@4.15.2 -D 复制代码
就不多说了,我们的目标,代码检查工具
Vue.js 的官方 ESLint 插件,提供了,以及 .js 文件中的 Vue 代码的支持
为在 Vue 组件中编写 ts 代码提供支持
针对 eslint 的一个 ts 解析器
@typescript-eslint/eslint-plugin
针对 ts 的 eslint plugin
接下来根目录创建 .eslintrc
文件进行 eslint 配置
{ // 指定当前目录为根目录 "root": true, // 环境配置项 "env": { // 是否浏览器环境 "browser": true, // 是否node环境 "node": true, // es2021 支持 "es2021": true }, // 引入配置项 "extends": [ // vue3 "plugin:vue/vue3-recommended", // eslint "eslint:recommended", // vue typescript "@vue/typescript/recommended" ], // 解析器配置 "parserOptions": { // 要使用的 ECMAScript 语法版本 "ecmaVersion": 2021 } } 复制代码
在 package.json
中 scripts 配置 eslint 命令
"lint":"eslint --ext .ts,vue src/**" 复制代码
检测 src 目录下的所有 .ts,.vue 文件
此时执行 yarn lint
会发现有一些警告和报错
这里我们再添加一个命令
"lint:fix":"eslint --ext .ts,vue src/** --fix" 复制代码
fix 会帮助我们自动修复一些警告
执行 yarn lint:fix
后发现,所有警告消失,只剩下一个报错
针对图片没有配置解析器问题,我们可以在根目录配置 .eslintignore
文件,使 eslint 忽略某些文件或目录
node_modules dist src/assets index.html 复制代码
再次执行 yarn lint:fix
发现可以通过了
4-2. 集成 lint-staged
这个时候我们想到每次 lint 检查都是检查 src 目录下所有的 .ts,.vue 文件是没有必要的,实际上每次我们只需要检查那些进行了修改的文件,也就是git暂存区的文件即可,这要怎么做呢?
这里需要用到 lint-staged 和 yorkie,使用
yarn add lint-staged@11.1.2 yorkie@2.0.0 -D 复制代码
进行安装,接下来需要在 package.json
进行一些配置
"gitHooks": { "pre-commit": "lint-staged"}, "lint-staged": { "*.{ts,vue}": "eslint --fix" } 复制代码
以上配置会在 commit 之前调用 lint-staged
,该命令会对所有的 .ts,.vue 文件进行 eslint --fix
这里我们将 App.vue
文件中的 HelloWorld 引入注释,然后提交代码,会发现在报错如下:
说明我们配置的校验生效了。《项目地址》
. 集成Prettier
Prettier 可以帮我们美化及统一代码格式,所以这里我们也集成进来。
安装prettier及相关依赖
yarn add prettier@2.2.1 eslint-plugin-prettier@3.3.1 @vue/eslint-config-prettier@6.0.0 -D 复制代码
为 prettier 在 eslint 中工作提供支持
为 eslint 代码校验规则与 prettier 代码校验规则部分冲突提供支持
.eslintrc
文件中新增配置
{ // 指定当前目录为根目录 "root": true, // 环境配置项 "env": { // 是否浏览器环境 "browser": true, // 是否node环境 "node": true, // es2021 支持 "es2021": true }, // 引入配置项 "extends": [ // vue3 "plugin:vue/vue3-recommended", // eslint "eslint:recommended", // vue typescript "@vue/typescript/recommended", "@vue/prettier", "@vue/prettier/@typescript-eslint" ], // 解析器配置 "parserOptions": { // 要使用的 ECMAScript 语法版本 "ecmaVersion": 2021 } } 复制代码
终端中执行 npx prettier -w -u .
-w: Edit files in-place. (Beware!) 就地编辑文件。 (谨防!)
-u: Ignore unknown files. 忽略未知文件。
. 指定路径为当前路径
会发现 prettier 帮我们格式化了一些文件,打开 App.vue 的变更
会发现 prettier 自动帮我们根据它的规则进行了一些修改,比如换行,空格,单双引号等
然后可以在根目录创建 .prettierrc
文件对 prettier 进行配置 相关文档
{ 是否结尾分号 "semi": true, 是否单引号 "singleQuote": false } 复制代码
这里只是进行示例测试,你可以根据团队规范或者自己需要根据文档进行配置,此时如果把某个文件的结尾分号删除,执行npx prettier -w -u .
,会发现prettier会帮我们把行尾分号加上
最后,为了方便使用,我们把 prettier 集成到 lint-staged 中,修改 package.json
"lint-staged": { "*.{ts,vue}": "eslint --fix", "*": "prettier -w -u" } 复制代码
至此,项目集成prettier完成。《项目地址》
. 集成commit message校验
对于团队来说,commit message 的规范也很重要,一个规范的 commit message 会让我们回顾之前的 git 历史时十分清晰明了,这里我们使用尤大在 vue 中的方法
该方法依赖 yorkie 库,我们在集成lint-staged 的时候已经进行了安装,这里不再重复安装。
配置 package.json
gitHooks
"gitHooks": { "commit-msg":"node scripts/verify-commit-msg.js", "pre-commit": "lint-staged" } 复制代码
这里的作用是在 commit-msg 钩子中,执行 scripts/verify-commit-msg.js
文件,
verify-commit-msg.js
文件我们直接在尤大的vue项目中拷贝过来即可。
该文件的大致逻辑为通过 fs 模块读取 git 文件中的 commit message,并通过正则进行校验,
校验不通过的话,报错提示并阻断 git commit
这里可能会有一个问题就是拷贝 verify-commit-msg.js
到文件后,eslint会报一个警告
Require statement not part of import statement. 复制代码
可以通过在 .eslintrc 中配置如下规则取消该校验
"rules":{ "@typescript-eslint/no-var-requires": 0 } 复制代码
verify-commit-msg.js文件中依赖 chalk 库,chalk 提供了终端中console设置颜色的功能
安装chalk yarn add chalk -D
然后我们通过 git commit -m "test"
测试发现可以拦截不符合要求的commit mesage了
我本地是win10系统,发现 chalk 并没有生效,这里需要在 verify-commit-msg.js
添加下图第二行代码
再次执行 git commit -m "test"
测试发现 chalk 生效了
至此,集成commit-msg校验完成。《项目地址》
7. gitHooks 添加 test 校验
上面我们添加了 jest 单元测试和 cypress e2e 测试,并进行了整合,但是通常我们不希望每次手动执行测试命令,而是希望在提交代码的时候自动执行测试保证推送到 git上的
代码的正确性即可。这一点,我们通过package.json
中添加 git hooks 钩子来实现
"gitHooks": { "commit-msg": "node scripts/verify-commit-msg.js", "pre-commit": "lint-staged", "pre-push": "npm run test" } 复制代码
这里设置在git push 之前执行 test 命令,上面我们在该命令下配置了 jest 和 cypress 测试,这样每次git push 之前,就会跑一遍测试脚本了
这一步比较简单,代码合并到了集成commit-msg这一步的分支中。《项目地址》
8. 配置 alias 路径别名
路径别名的作用就是通过制定符号简化到指定路径的操作。
8-1. 配置 vite.config.js
例如:如果在 views/Home.vue
中引入 HelloWorld.vue
的时候是这样的
import HelloWorld from "../components/HelloWorld.vue"; 复制代码
如果层级再深一些,例如在 views/Goods/List/index.vue
中,就需要这样
import HelloWorld from "../../../components/HelloWorld.vue"; 复制代码
可以看到很麻烦,而且如果 HelloWorld.vue
文件的位置发生了变化,所有
HelloWorld.vue
的引入代码修改起来也比较麻烦,为了方便我们解决这样的问题,vite 也提供了配置 alias 别名的方法,修改 vite.config.js
如下:
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import { resolve } from "path"; // https://vitejs.dev/config/ export default defineConfig({ resolve: { alias: { "@": resolve("./src") }, }, plugins: [ vue() ] }) 复制代码
这里我们设置了将 @ 符号指向到 ./src 目录下,以后无论在哪个文件,统一使用如下代码引入
HelloWorld.vue
即可,文件位置发生了变化,也可进行统一替换
import HelloWorld from "@/components/HelloWorld.vue"; 复制代码
文件位置发生了变化,这里也不需要进行调整,为我们提供了很大的便利。
但是到了这里还没有结束,在引入代码的时候可以发现当我们写 ./ 的时候,vscode 会有路径提示,但是当我们写 @/ 的时候,并没有路径提示,这里就需要对 ts 进行一些配置。
8-2. 配置 ts 支持 alias
{ "compilerOptions": { "target": "esnext", "useDefineForClassFields": true, "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "esModuleInterop": true, "lib": ["esnext", "dom"], "types": ["vite/client", "jest", "node"], "baseUrl":"./", "paths":{ "@/*":["src/*"] } }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] } 复制代码
baseUrl: 指定当前目录
paths 中是我们的别名配置,这里的含义和 vite.config.js
中的配置一样,再次回到 .vue 文件中,使用 @ 别名引入 HelloWorld.vue
就会有路径提示了,如果无效,可以尝试下重启 vscode
8-3. 配置 jest 支持 alias
接下来我们尝试在 jest 单元测试中使用 alias,修改 tests/unit/index.spec.ts
import foo from "./foo"; import { mount } from "@vue/test-utils"; import Foo from "@/components/Foo.vue"; test("1+1=2", () => { expect(1 + 1).toBe(2); }); test("foo", () => { expect(foo()).toBe("this is foo"); }); test("Foo", () => { console.log("Foo", Foo); console.log("mount", mount(Foo)); }); 复制代码
修改之后 vscode 就给我们报错提示了
然后我们尝试执行 yarn test-unit
发现对于 @ 路径别名,jest 是识别不了的,接下来需要修改 jest.config.js
module.exports = { // 转换器 transform: { // jest解析js的时候通过babel-jest解析 "^.+\\.jsx?$": "babel-jest", // jest解析vue的时候通过vue-jest解析 "^.+\\.vue$": "vue-jest", // jest解析ts的时候通过ts-jest解析 "^.+\\.tsx?$": "ts-jest" }, // 配置测试脚本文件匹配规则 testMatch: [ "**/tests/unit/?(*.)+(spec).[jt]s?(x)" ], // 配置路径别名 moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/$1" } }; 复制代码
接下来再次执行 yarn test-unit
,发现可以测试通过了。《项目地址》
9. 集成Vue Router
Vue Router 是 Vue.js 的官方路由,这里不必多说,直接安装
yarn add vue-router@next 复制代码
src 目录下创建 router 文件夹,编写 index.ts
import { createRouter, createWebHashHistory } from "vue-router"; export const routes = [ { path: "/", redirect: "/home" },{ path: "/home", name: "Home", component: () => import("@/views/Home.vue") },{ path: "/login", name: "Login", component: () => import("@/views/Login.vue") },{ path: "/404", name: "404", hidden: true, meta: { notNeedAuth: true }, component: () => import("@/views/404.vue") }, // 匹配所有路径 vue2使用* vue3使用/:pathMatch(.*)或/:catchAll(.*) { path: "/:catchAll(.*)", redirect: "/404" } ]; // 路由实例 const router = createRouter({ history: createWebHashHistory(), routes }); export default router; 复制代码
这里都是基本用法,不做赘述,看文档即可。
修改 App.vue
<template> <router-view> </router-view> </template> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style> 复制代码
创建 views/Home.vue
<template> <div> <p>This is Home</p> <img alt="Vue logo" src="@/assets/logo.png" /> <HelloWorld msg="Hello Vite + Vue 3 + TypeScript" /> </div> </template> <script lang="ts"> import { defineComponent } from "vue"; import HelloWorld from "@/components/HelloWorld.vue"; export default defineComponent({ name: "Home", components: { HelloWorld }, setup() { return {}; } }); </script> 复制代码
创建 views/Login.vue
<template> <div>This is Login</div> </template> <script lang="ts"> import { defineComponent } from "vue"; export default defineComponent({ name: "Login", setup() { return {}; } }); </script> 复制代码
main.ts
引入 vue router
import { createApp } from "vue"; import App from "./App.vue"; import router from "./router/index"; createApp(App).use(router).mount("#app"); 复制代码
至此,vue router 集成完成。 《项目地址》
10. 集成 Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化,直接安装。
yarn add vuex@next 复制代码
src 目录下创建 store 文件夹,这里我们一步到位,直接把模块划分开,创建一个 user
模块作为示例:
src/store/index.ts
import { createStore } from "vuex"; import getters from "./getters"; import user from "./modules/user"; const modules = { user,}; const store = createStore({ modules, getters,}); export default store; 复制代码
src/store/modules/user.ts
export default { namespaced: true, state: { userInfo: { userId:"001", name: "wzy" } }, mutations: { }, actions: { } }; 复制代码
src/store/getters.ts
type state = { user: { userInfo: { name: string; token: string; avatar: string; roles: string[]; }; } }; export default { userInfo: (state: state) => state.user.userInfo }; 复制代码
main.ts
引入vuex
import { createApp } from "vue"; import App from "./App.vue"; import router from "./router/index"; import store from "./store/index"; createApp(App) .use(store) .use(router) .mount("#app"); 复制代码
src/views/Home.vue
使用
<template> <div> <p>This is Home</p> <img alt="Vue logo" src="@/assets/logo.png" /> <HelloWorld msg="Hello Vite + Vue 3 + TypeScript" /> <p>Name: {{ userInfo.name }}</p> </div> </template> <script lang="ts"> import { defineComponent } from "vue"; import { useStore } from "vuex"; import HelloWorld from "@/components/HelloWorld.vue"; export default defineComponent({ name: "Home", components: { HelloWorld }, setup() { // 获取类型化的 store const store = useStore(); // 获取 userInfo const userInfo = store.getters.userInfo; return { userInfo }; } }) </script> 复制代码
至此,vuex 集成完成。 《项目地址》
11. 集成 Element3
Element3,一套为开发者、设计师和产品经理准备的基于 Vue 3.0 的桌面端组件库。安装
yarn add element3 复制代码
main.ts
中引入 element3
这里采用按需引入的方式引入部分组件
import { createApp } from "vue"; import App from "./App.vue"; import router from "./router/index"; import store from "./store/index"; // Element3样式文件 import "element3/lib/theme-chalk/index.css"; import { ElIcon, ElButton, ElForm, ElFormItem, ElInput } from "element3"; createApp(App) .use(store) .use(router) .use(ElIcon) .use(ElButton) .use(ElForm) .use(ElFormItem) .use(ElInput) .mount("#app"); 复制代码
修改 src/views/Login.vue
<template> <div class="login_main" @keyup.enter="login"> <!-- 中间盒子 --> <div class="content"> <h3 class="title">登录</h3> <el-form ref="form" :model="param" :rules="rules"> <el-form-item prop="name"> <el-input v-model="param.name" prefix-icon="el-icon-user" placeholder="账号" ></el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="param.password" prefix-icon="el-icon-lock" placeholder="密码" show-password autocomplete ></el-input> </el-form-item> <el-button class="w_100" type="primary" @click="login">登录</el-button> </el-form> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, reactive } from "vue"; export default defineComponent({ name: "Login", setup() { // 表单 const form = ref(null); // 请求参数 const param = reactive({ name: "", password: "", }); // 表单校验规则 const rules = reactive({ name: [ { required: true, message: "请输入账号", trigger: "blur" }, ], password: [ { required: true, message: "请输入密码", trigger: "blur" } ], }); // 密码表单类型 const passInputType = ref("password"); // 修改密码表单类型 const changeInputType = (val: string) => { passInputType.value = val; }; // 登录 const login = () => { form.value.validate((valid: boolean) => { if (valid) { console.log("login 校验通过!"); } else { return false; } }); }; return { form, param, rules, passInputType, changeInputType, login, }; } }); </script> <style scoped> .login_main { display: flex; align-items: center; justify-content: center; } .content { width: 500px; } </style> 复制代码
修改后的登录页如下:
至此,element3 集成完成。 《项目地址》
12. 集成 axios + mockjs + sass
12-1. 集成axios
项目中涉及到前后端交互就肯定需要一个HTTP库,这里我们集成最常用的 axios
yarn add axios 复制代码
src 目录下创建 utils 文件夹存放我们的工具方法
编写 request.ts
封装 axios
import axios from "axios"; import { Message } from "element3"; // 创建axios实例 const service = axios.create({ baseURL: import.meta.env.BASE_URL, timeout: 10000 }); // 请求拦截器 service.interceptors.request.use( (config) => { return config; }, (error) => { return Promise.reject(error); } ); // 响应拦截器 service.interceptors.response.use( (response:any) => { const res = response.data; if (res.header.code !== 0) { Message.error(res.header.msg || "Error"); return Promise.reject( new Error(res.header.msg || "Error")); } return res; }, (error) => { Message.error("request error"); return Promise.reject(error); } ); /** * 封装接口请求方法 * @param url 域名后需补齐的接口地址 * @param method 接口请求方式 * @param data d请求数据体 */ type Method = | "get" | "GET" | "delete" | "DELETE" | "head" | "HEAD" | "options" | "OPTIONS" | "post" | "POST" | "put" | "PUT" | "patch" | "PATCH" | "purge" | "PURGE" | "link" | "LINK" | "unlink" | "UNLINK"; const request = ( url: string, method: Method, data: Record<string, unknown> ) => { return service({ url, method, data, }); }; export default request; 复制代码
src 目录下创建 api 文件夹存放所有接口请求方法
src/api 下创建 user.ts
编写 user 相关接口请求
import request from "../utils/request"; type Method = | "get" | "GET" | "delete" | "DELETE" | "head" | "HEAD" | "options" | "OPTIONS" | "post" | "POST" | "put" | "PUT" | "patch" | "PATCH" | "purge" | "PURGE" | "link" | "LINK" | "unlink" | "UNLINK"; const curryRequest = ( url: string, method: Method, data?: Record<string, unknown> | any ) => { return request( `/module/user/${url}`, method, data ) }; // 登录 export function apiLogin(data: { name: string; password: string; }): PromiseLike<any> { return curryRequest("login", "post", data); } 复制代码
将登录相关操作放在 store/modules/user.ts
actions 中进行,并完善 mutations
import { apiLogin } from "@/api/user"; type userInfo = { userId: string; name: string; }; type state = { userInfo: userInfo; }; type context = { state: Record<string, unknown>; mutations: Record<string, unknown>; actions: Record<string, unknown>; dispatch: any; commit: any; }; type loginData = { name: string; password: string; }; export default { namespaced: true, state: { userInfo: { userId: "", name: "" }, }, mutations: { // 设置用户信息 SET_USERINFO(state:state,val:userInfo){ state.userInfo = val; } }, actions: { // 登录 login({ commit }: context, data: loginData) { return new Promise((resolve) => { apiLogin(data).then(async (res) => { // 更新用户信息 commit("SET_USERINFO", { userId: res.body.userId, name: res.body.name, }); resolve("success"); }) }) } } } 复制代码
更新 Login.vue
<template> <div class="login_main" @keyup.enter="login"> <!-- 中间盒子 --> <div class="content"> <h3 class="title">登录</h3> <el-form ref="form" :model="param" :rules="rules"> <el-form-item prop="name"> <el-input v-model="param.name" prefix-icon="el-icon-user" placeholder="账号" ></el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="param.password" prefix-icon="el-icon-lock" placeholder="密码" show-password autocomplete ></el-input> </el-form-item> <el-button class="w_100" type="primary" @click="login">登录</el-button> </el-form> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, reactive } from "vue"; import { useStore } from "vuex"; import { useRouter } from "vue-router"; export default defineComponent({ name: "Login", setup() { // sotre实例 const store = useStore(); // 路由实例 const router = useRouter(); // 表单 const form = ref(null); // 请求参数 const param = reactive({ name: "", password: "", }); // 表单校验规则 const rules = reactive({ name: [ { required: true, message: "请输入账号", trigger: "blur" }, ], password: [ { required: true, message: "请输入密码", trigger: "blur" } ], }); // 密码表单类型 const passInputType = ref("password"); // 修改密码表单类型 const changeInputType = (val: string) => { passInputType.value = val; }; // 登录 const login = () => { form.value.validate((valid: boolean) => { if (valid) { store.dispatch("user/login", param).then(() => { router.push({ name: "Home" }); }); } else { return false; } }); }; return { form, param, rules, passInputType, changeInputType, login, }; } }) </script> <style scoped> .login_main { display: flex; align-items: center; justify-content: center; } .content { width: 500px; } </style> 复制代码
我一般推荐使用 router.push({ name: "Home" })
进行路由跳转,因为每个路由定义的 name 一般不会发生变化,但是路径可能会有调整,这样就避免了路径变化后还需要修改对应路由操作的问题
至此,请求相关逻辑编写完成,但是当我们搭建框架的时候一般后台也处于初始阶段,接口还没有准备好,这时候就需要 mockjs 帮我们模拟接口请求
12-2. 集成 mockjs
安装 mockjs
yarn add mockjs@1.1.0 复制代码
yarn add vite-plugin-mock@2.9.4 -D 复制代码
修改 vite.config.js
引入 mockjs
import { UserConfigExport, ConfigEnv,loadEnv } from "vite"; import vue from "@vitejs/plugin-vue"; import { resolve } from "path"; import { viteMockServe } from "vite-plugin-mock"; export default ({ command, mode }: ConfigEnv): UserConfigExport => { const plugins = [vue()]; const env = loadEnv(mode, process.cwd()); // 如果当前为测试环境,添加mock插件 if (mode === "development") { plugins.push( viteMockServe({ mockPath: "mock", localEnabled: command === "serve" }) ); } return { resolve: { alias: { "@": resolve("./src") }, }, plugins } }; 复制代码
项目根目录创建 mock 文件夹,新建 user.ts
编写 user 相关 mock 接口
export default [ { url: `/module/user/login`, method: "post", response: (req) => { return { header: { code: 0, msg: "OK", }, body: { userId:"001", name: `${req.body.name}小明` } } } } ]; 复制代码
打开登录页面,输入账号1,密码1,点击登录
控制台可见成功发起了请求并收到了响应,页面跳转到了Home并获取到了userInfo
12-3. 集成 sass
相关文档 安装 sass
yarn add sass -D 复制代码
修改 src/views/Login.vue
style 测试
<style lang="scss" scoped> $bg: #364d81; .login_main { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: $bg; .content { width: 500px; .title { color: #fff; } }} </style> 复制代码
至此,集成 sass 完成。
12-4. 解决 jest 单元测试不支持 import.meta.env
本以为到此万事大吉,但是当我们针对 src/utils/request.ts
进行单元测试的时候
tests/unit/request.spec.ts
import request from "@/utils/request"; test("request", () => { expect(request).not.toBeNull(); expect(request).not.toBeUndefined(); }); 复制代码
发现 jest 报错了
这里我们之前tsconfig.js中是配置了 "module": "esnext"
的,其实该问题是当前的 ts-jest 库没有对 import.meta.env
做支持,这里采用了如下方法
修改 vite.config.js
import { UserConfigExport, ConfigEnv,loadEnv } from "vite"; import vue from "@vitejs/plugin-vue"; import { resolve } from "path"; import { viteMockServe } from "vite-plugin-mock"; export default ({ command, mode }: ConfigEnv): UserConfigExport => { const plugins = [vue()]; const env = loadEnv(mode, process.cwd()); // 如果当前是测试环境,使用添加mock插件 if (mode === "development") { plugins.push( viteMockServe({ mockPath: "mock", localEnabled: command === "serve" }) ) } // 处理使用import.meta.env jest 测试报错问题 const envWithProcessPrefix = Object.entries(env).reduce( (prev, [key, val]) => { return { ...prev, // 环境变量添加process.env ["process.env." + key]: `"${val}"` }; }, {} ); return { base: "./", resolve: { alias: { "@": resolve("./src"), }, }, plugins, define: envWithProcessPrefix, }}; 复制代码
将拿到的环境相关的变量保存到 process.env
中并向外暴露
修改 request.ts
import axios from "axios"; import { Message } from "element3"; // 创建axios实例 const service = axios.create({ baseURL: process.env.VITE_BASE_URL, timeout: 10000 }); 复制代码
baseURL 中 使用 process.env
下的变量
运行 yarn test-unit
测试通过。
至此,集成 axios+mockjs+sass 完成。《项目地址》
End
至此,本文就结束了,后续会在此基础上搭建后台管理系统,感兴趣的点个关注吧
!
关于本文有任何问题或建议,欢迎留言讨论!