[开个新坑/完善中][Vue3+TS]硅谷甄选运营平台项目学习笔记

简单介绍

想着Vue2在今年年底也要停止维护了,是时候跟进学学Vue3更新下技术栈了。以笔记方式整理一下学习过程中的要点,顺便巩固一下基础知识。

一些我已经看过的内容链接:

余下的笔记内容主要整理自B站视频:尚硅谷Vue项目实战硅谷甄选,vue3项目+TypeScript前端项目一套通关B站大学好:joy:)

组件通信方式

  • props
  • 自定义事件:主要是子组件到父组件传递数据,父组件使用v-on/@绑定事件,子组件使用emit
  • 全局事件总线Bus:适用于任意组件间通信
  • 消息订阅与发布pubsub:需要引入pubsub-js依赖
  • vuex
  • ref:父组件取得子组件实例,从而使用子组件数据和方法
  • slot:插槽

几种通信模式的介绍和例子:Vue组件之间通信(props、自定义事件、全局事件总线、消息订阅与发布、Vuex)

props

在子组件使用defineProps定义props,使得父组件可以利用v-bind/:向子组件传递只读参数

自定义事件

在子组件使用defineEmits定义emit,使得父组件v-on/@绑定的为自定义事件,而非DOM原生事件

全局事件总线Bus

相比于Vue2,Vue3无法将自身实例挂载在Vue原型上、组合式函数setup中也无法使用this,因此无法直接使用$bus,可以利用mitt依赖实现。

可以构造一个js脚本,暴露mitt提供的bus实例,供其他组件引入使用

import mitt from 'mitt'
const $bus = mitt()
export default $bus

组件传递数据时,可以利用如下的方式实现

import $bus from '@/bus'
const handler => () => {
  $bus.emit('obj', {value: 'value'})
}

组件接收数据时,可以利用如下的方式实现

import $bus from '@/bus'
import {onMounted} from 'vue'
onMounted(() = > {
  $bus.on('obj', obj => {
    console.log(obj)
  })
})

v-model

同步单个属性

v-model="data"在Vue3中相当于:modelValue="data"@update:modelValue="handler"的组合。

  • update:modelValue为emit方法的自定义事件名称;
  • handler为父组件用于更新属性data值的方法。

稍微回顾一下Vue2中v-model的使用,v-model相当于:value@input的组合

同步多个属性

例如,组件需要接收pageNum和pageSize的参数,并对参数进行同步,则可以将v-model写成这样的形式:v-model:pageNum="pageNum" v-model:pageSize="pageSize",子组件的emit事件名称则对应为update:pageNumupdate:pageSize

useAttrs

使用useAttrs可以接收父组件传入的属性而无需列出具体名称,props的优先级更高,若属性已经在props内定义,则useAttrs取到的对象内无该属性。

这里以对ElementUI-Plus的按钮组件进行封装为例:

<template>
  <el-button :="$attrs" />
</template>

<script setup>
import {useAttrs} from 'vue'
const $attrs = useAttrs()
// 若要取得传入的title参数,可以使用$attrs.title取得
// $attrs内也可取得事件方法
</script>

$ref和$parent

ref

对于子组件,可以使用defineExpose暴露已定义的属性和方法,供其他组件调用

<script setup>
import {ref} from 'vue'
let money = ref(666)
const handler = () => {
  console.log('子组件方法')
}
// 对外暴露money属性和handler方法
defineExpose({
  money,
  handler
})
</script>

对于父组件,可以获取和使用子组件已经暴露出的属性和方法

<template>
  <div>
    <childComp ref="child"></childComp>
  </div>
</template>

<script setup>
  import ChildComp from './childComp'
  import {ref, onMounted} from 'vue'
  let child = ref(null); // 获取子组件实例,变量名要与ref属性值一致
  const onMounted = () => {
    console.log(child.value.money) // 输出子组件内的属性
    child.value.hadler()
  }
</script>

parent

对于子组件,在执行事件时传入$parent对象,即可通过$parent取得父组件实例

<template>
  <button @click="handler($parent)"></button>
</template>

<script setup>
const handler = ($parent) => {
  console.log($parent)
}
</script>

同样地,父组件需要使用defineExpose暴露已定义的属性和方法,供其他组件调用,就不再重复举例了。

provide和inject

Vue3提供provideinject方法,可以实现隔辈组件传递数据

在祖先组件中,可以使用如下的方式传递数据,provide方法的第一个参数是数据的Key,第二个参数为数据

import {ref, provide} from 'vue'
let token = ref('X-TOKEN')
provide("TOKEN", token)

在孙子组件中,可以使用inject引入数据,传入数据的Key即可

import {inject} from 'vue'
let token = inject('TOKEN')
console.log(token.value)

pinia状态管理

pinia与vuex类似,也是集中式状态管理库,是Vue3中使用的新的状态管理库,可以实现任意组件之间的通信。

中文文档可阅读:Pinia中文文档

复习一下,vuex的核心概念:state、mutations、actions、getters、modules

pinia的核心概念有:state、actions、getters,同样的他也可以模块化使用

创建store/index.ts

import {createPinia} from 'pinia'
const store = createPinia()
export default store

在main.ts中引入store

// ...
import store from './store'
// ...
app.use(store)
// ...

在store/modules/todo.ts中定义modules

import {defineStore} from 'pinia'
import {ref} from 'vue'
const useTodoStore = defineStore('todo', () => {
  const todos = ref({
    id: 1,
    title: '吃饭'
  })
  const addTodo = item => {
    todos.value.push(item)
  }
  return {
    todos,
    addTodo
  }
})

在组件内使用todoStore可以采用如下的方式

import useTodoStore from '@/store/modules/todo'
let todoStore = useTodoStore()
console.log(todoStore.todos.value)
todoStore.todos.addTodo({
  id: 2,
  title: '睡觉'
})

插槽

有以下三种类型的插槽:

  • 默认插槽
  • 具名插槽
  • 作用域插槽

Vue3插槽文档

默认插槽

在子组件中可以有如下的结构,

<template>
  <div class="childComp">
    <p>Last para</p>
    <slot></slot>
    <p>next para</p>
  </div>
</template>

父组件要将结构传入子组件内可以以这样的方式,

<template>
  <div>
    <ChildComp>
      <div>Content</div>
    </ChildComp>
  </div>
</template>

具名插槽

对于子组件,需要利用name属性定义插槽的名称,

<slot name="a"></slot>

在父组件中向子组件传递结构时,可以写成如下的形式,需要用到v-slot属性

<template v-slot:a>
  <div>填充</div>
</template>

或者可将v-slot简写成#

<template #a>
  <div>填充</div>
</template>

作用域插槽

子组件可以将数据回传给父组件,父组件决定回传的数据以何种结构或外观在子组件内部展示。

传递单个对象

<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

传递列表

<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>
<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

项目初始化

使用pnpm + Vite初始化项目

npm install -g pnpm
pnpm create vite

其中pnpm和vite对node有版本要求,项目初始化时需要注意

如果在Windows系统下,出现因为在此系统上禁止运行脚本的报错信息
可以使用win + x打开管理员终端,输入set-ExecutionPolicy RemoteSigned解除限制

运行项目

pnpm run dev

使用eslint

引入eslint用于保证代码质量

安装eslint

pnpm install eslint -D

生成eslint配置文件.eslint.cjs

npx eslint --init

Vue3环境代码校验插件

pnpm i -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser

安装插件后,修改eslint校验规则.eslint.cjs

// @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', // 对模板中的自定义组件强制执行属性命名样式
  }
}

创建.eslintignore忽略不需要eslint检测的目录

dist
node_modules

package.json新增运行脚本,分别用于检测和纠正src目录代码

"scripts": {
  "lint": "eslint src",
  "fix": "eslint src --fix"
}

配置prettier

在保证代码质量的同时,也要保证代码的美观,因此需要引入prettier配合eslint达到效果。

安装相关依赖

pnpm install -D eslint-plugin-prettier prettier eslint-config-prettier

添加.prettierrc.json规则

{
  "singleQuote": true,
  "semi": false,
  "bracketSpacing": true,
  "htmlWhitespaceSensitivity": "ignore",
  "endOfLine": "auto",
  "trailingComma": "all",
  "tabWidth": 2
}

添加.prettierignore忽略配置

/dist/*
/html/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*

配置styleLint

styleLint为css的lint工具。可格式化css代码,检查css语法错误与不合理的写法,指定css书写顺序等。

由于项目中使用scss以及为了使用styleLint,因此需要使用命令添加如下的依赖

pnpm add sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss -D

配置.stylelintrc.cjs文件

// @see https://stylelint.bootcss.com/
module.exports = {
  extends: [
    'stylelint-config-standard', // 配置stylelint拓展插件
    'stylelint-config-html/vue', // 配置 vue 中 template 样式格式化
    'stylelint-config-standard-scss', // 配置stylelint scss插件
    'stylelint-config-recommended-vue/scss', // 配置 vue 中 scss 样式格式化
    'stylelint-config-recess-order', // 配置stylelint css属性书写顺序插件,
    'stylelint-config-prettier', // 配置stylelint和prettier兼容
  ],
  overrides: [
    {
      files: ['**/*.(scss|css|vue|html)'],
      customSyntax: 'postcss-scss',
    },
    {
      files: ['**/*.(html|vue)'],
      customSyntax: 'postcss-html',
    },
  ],
  ignoreFiles: [
    '**/*.js',
    '**/*.jsx',
    '**/*.tsx',
    '**/*.ts',
    '**/*.json',
    '**/*.md',
    '**/*.yaml',
  ],
  /**
   * null  => 关闭该规则
   * always => 必须
   */
  rules: {
    'value-keyword-case': null, // 在 css 中使用 v-bind,不报错
    'no-descending-specificity': null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器
    'function-url-quotes': 'always', // 要求或禁止 URL 的引号 "always(必须加上引号)"|"never(没有引号)"
    'no-empty-source': null, // 关闭禁止空源码
    'selector-class-pattern': null, // 关闭强制选择器类名的格式
    'property-no-unknown': null, // 禁止未知的属性(true 为不允许)
    'block-opening-brace-space-before': 'always', //大括号之前必须有一个空格或不能有空白符
    'value-no-vendor-prefix': null, // 关闭 属性值前缀 --webkit-box
    'property-no-vendor-prefix': null, // 关闭 属性前缀 -webkit-mask
    'selector-pseudo-class-no-unknown': [
      // 不允许未知的选择器
      true,
      {
        ignorePseudoClasses: ['global', 'v-deep', 'deep'], // 忽略属性,修改element默认样式的时候能使用到
      },
    ],
  },
}

配置.stylelintignore忽略文件

/node_modules/*
/dist/*
/html/*
/public/*

运行脚本配置(在package.json中)

"format": "prettier --write \"./**/*.{html,vue,ts,js,json,md}\"",
"lint:eslint": "eslint src/**/*.{ts,vue} --cache --fix",
"lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"

配置husky(待完善)

husky是一个git hook,可以用于在代码提交到git前的检测和处理。

暂未使用,待完善。

配置commitLint(待完善)

commitLint用于规范项目团队开发时的commit信息,进行统一规范。

暂未使用,待完善。

统一包管理器工具(待完善)

利用preinstall脚本,在install前检测使用的包管理工具,避免团队因为使用不同包管理工具照成冲突或报错。

暂未使用,待完善。

集成Element-Plus

似乎有点过于基础了,简单做个笔记就好:cry:

安装Element-Plus及图标库依赖

pnpm install element-plus
pnpm install @element-plus/icons-vue

main.ts中引入Element-Plus,并引入中文语言包

// ...
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' // 中文语言包
// ...
const app = createApp(App)
app.use(ElementPlus, {
    locale: zhCn
})

app.mount('#app')

在组件中引入单个图标

import {Plus} from '@element-plus/icon-vue'

配置src目录别名

在项目开发过程中会经常用到/scr这个目录,通常会为其设置@作为别名,需要进行一定的配置。

配置vite.config.ts

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
        }
    }
})

配置TypeScript编译配置tsconfig.json

{
  "compilerOptions": {
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { //路径映射,相对于baseUrl
      "@/*": ["src/*"]
    }
  }
}

配置项目环境变量

可以为项目配置环境变量,以适应不同场景的运行情况。

主要有以下三种环境:

  • 开发环境.env.development
  • 测试环境.env.production
  • 生产环境.env.test

环境变量内容示例:

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/api'

package.json中配置不同环境的运行命令:

{
  "scripts": {
    "dev": "vite --open",
    "build:test": "vue-tsc && vite build --mode test",
    "build:pro": "vue-tsc && vite build --mode production",
    "preview": "vite preview"
  }
}

之后要取得环境变量时,可以使用import.meta.env读取

配置SvgIcon

SvgIcon的简单使用和封装

先安装svg依赖插件

主要是用于生成svg图标雪碧图,以减少网络请求

pnpm install vite-plugin-svg-icons -D

vite.config.ts中配置插件

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
  return {
    plugins: [
      createSvgIconsPlugin({
        // Specify the icon folder to be cached
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
        // Specify symbolId format
        symbolId: 'icon-[dir]-[name]',
      }),
    ],
  }
}

在入口文件main.ts中引入插件

import 'virtual:svg-icons-register'

将svg图标文件放置在src/assets/icons目录下,例如要使用apple.svg图标,可以采用如下的方式简单引入

<!-- svg:图标外层容器,内部需要use标签结合使用 -->
<svg>
  <!-- xlink:href执行用哪一个图标,属性值必须加上 #icon-图标名字 -->
  <use xlink:href="#icon-apple" fill="red"></use>
</svg>

为了方便后续项目中的多次使用svg图标,可以将其封装为一个SvgIcon组件

<template>
  <div>
    <svg :style="{ width: width, height: height }">
      <use :xlink:href="prefix + name" :fill="color"></use>
    </svg>
  </div>
</template>
<script setup lang="ts">
defineProps({
  //xlink:href属性值的前缀
  prefix: {
    type: String,
    default: '#icon-'
  },
  //svg矢量图的名字
  name: String,
  //svg图标的颜色
  color: {
    type: String,
    default: ""
  },
  //svg宽度
  width: {
    type: String,
    default: '16px'
  },
  //svg高度
  height: {
    type: String,
    default: '16px'
  }
})
</script>
<style scoped></style>

将SvgIcon注册为全局组件

创建src/index.ts文件,用于注册src/components目录下的所有全局组件

// 引入项目中全部的全局组件
import SvgIcon from '@/components/SvgIcon/index.vue';

// 全局组件的对象
const allGlobalComponents = { SvgIcon: SvgIcon };
// 对外暴露一个插件对象
export default {
    install(app) {
        Object.keys(allGlobalComponents).forEach((key) => {
            app.component(key, allGlobalComponents[key]);
        })
    }
}

在入口文件main.ts中引入刚刚创建的index.js,并使用use方法全局引入组件

// 引入自定义插件对象:注册全局组件
import globalComponent from '@/components/index.ts'
// 安装自定义插件
app.use(globalComponent)

配置scss

配置mock

axios封装

API接口统一管理

发布者

《[开个新坑/完善中][Vue3+TS]硅谷甄选运营平台项目学习笔记》上有1条评论

  1. 在组件中引入单个图标
    import {Plus} from ‘@element-plus/icon-vue’

    这里写错了,应该是:
    import { Plus} from ‘@element-plus/icons-vue’

    就为这少一个s,我查了半天的问题

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注