从零到一:搭建一个Vue3开发框架

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 从零到一:搭建一个Vue3开发框架

网络异常,图片无法展示
|


说明:


之前因为做一个项目从零到一搭建了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-unityarn 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
复制代码


eslint


就不多说了,我们的目标,代码检查工具


eslint-plugin-vue


Vue.js 的官方 ESLint 插件,提供了,以及 .js 文件中的 Vue 代码的支持


@vue/eslint-config-typescript


为在 Vue 组件中编写 ts 代码提供支持


@typescript-eslint/parser


针对 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-stagedyorkie,使用


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
复制代码


eslint-plugin-prettier


为 prettier 在 eslint 中工作提供支持


@vue/eslint-config-prettier


为 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 RouterVue.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     
复制代码


安装 vite-plugin-mock


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




至此,本文就结束了,后续会在此基础上搭建后台管理系统,感兴趣的点个关注吧


关于本文有任何问题或建议,欢迎留言讨论!

相关文章
|
26天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
128 64
|
26天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
108 60
|
1天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
12 3
|
26天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
30 8
|
26天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
28 1
|
26天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
35 1
|
26天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
1月前
|
缓存 监控 JavaScript
Vue.js 框架下的性能优化策略与实践
Vue.js 框架下的性能优化策略与实践
|
1月前
|
JavaScript 索引
Vue 3.x 版本中双向数据绑定的底层实现有哪些变化
从Vue 2.x的`Object.defineProperty`到Vue 3.x的`Proxy`,实现了更高效的数据劫持与响应式处理。`Proxy`不仅能够代理整个对象,动态响应属性的增删,还优化了嵌套对象的处理和依赖追踪,减少了不必要的视图更新,提升了性能。同时,Vue 3.x对数组的响应式处理也更加灵活,简化了开发流程。
|
1月前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发