项目2|vue3

342 阅读15分钟

一、功能

用户登录 对用户增删改查 中英文切换 全屏

二、核心思想

基于element-plus和vue-cli实现

  • vuex存储token数据
  • 通过axios发送请求,返回响应,对请求和响应进行拦截
  1. 通过axios创建一个服务请求
  2. interceptors.request.use 请求拦截器
  3. interceptors.response.use 响应拦截器 对响应回来的数据进行进一步取之后再传回去
  • localStorage存储token数据
  1. setItem存
  2. getItem拿 拿出来之后存到了vuex中的state中去
  • element-plus完成的事情
  1. 实现页面的基本buju
  2. 完成对一些输入数据的校验,比如说必须输入用户名/密码 等效于input中的required:true,一些正则化操作
  3. 使用一些icon图标

三、项目初始创建

3.1 项目创建

cmd vue-ui
scss
eslint代码规范
axios依赖

3.2 代码规范

1.在Vscode插件中安装prettier

2.在根目录下导入配置文件

3.在vscode的设置里 搜索save 就能看到并勾上format on save

4.右键 使用...格式化文档 配置默认格式化文档程序 选择prettier

5.在配置下.eslintrc.js里的rules 新增 为了解决eslintprettier冲突

'indent': 0,
'space-before-function-paren': 0

6.使用husky强制代码格式化 创建配置文件

npx husky add .husky/pre-commit

7.往第六步生成的文件中写入

npx lint-staged

8.把package.json文件的lint-staged修改为

"lint-staged": {
    "src/**/*.{js,vue}": [      //src目录下所有的js和vue文件
         "eslint --fix",        // 自动修复
         "git add"              // 自动提交时修复
       ]
 }

3.3 git commit规范

1.安装commitizencz-customizable

npm install -g commitizen@4.2.4
npm i cz-customizable@6.3.0 --save-dev

2.在package.json中进行新增

"config": {
  "commitizen": {
    "path": "node_modules/cz-customizable"
  }
}

3.在根目录下新建.cz-config.js文件并写入配置 之后就可以用git cz来代替 git commit 4.使用husky进行强制git代码提交规范

npm install --save-dev @commitlint/config-conventional@12.1.4 @commitlint/cli@12.1.4
npm install husky@7.0.1 --save-dev // 强制性规范
npx husky install

5.在package.json中新增指令

"prepare": "husky install"

6.并执行

npm run prepare

7.新增husky配置文件 并往里面写入

npx husky add .husky/commit-msg
npx --no-install commitlint --edit

3.4 element-ui使用

1 yarn add element-plus
2 按需导入
yarn add -D unplugin-vue-components unplugin-auto-import
3 在vue.config.js中导入
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
module.exports = {
  configureWebpack: config => {
    config.plugins.push(AutoImport({
      resolvers: [ElementPlusResolver()],
    }))
    config.plugins.push(Components({
      resolvers: [ElementPlusResolver()],
    }))
  },
}

3.5 vue3.2新特性

  • 在template中不需要再用根标签包裹
  • 在css中可以直接访问js变量
<template>
    <div class = 'box1'></div>
</template>
<script setup>
    const boxwidth = '100px'
</script>
<style lang = 'scss'>
.box1{
    width:v-bind(boxwidth)
}
</style>

3.6 初始化项目

  • App.vue
<script setup></script>
<template>
  <router-view />
</template>
<style lang="scss">
.el-message-box__status {
  position: absolute !important;
}
</style>
  • main.js 存放一些方法

  • 新建文件夹style 存放一些scss文件 例如一些公用的样式信息,存成变量的形式

  • - scss的特点

  • 新建文件夹views 存放各个功能的页面

image.png

./login/App.vue 模板内容

<template>
  <div>login</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>
  • 在路由router文件夹中进行配置 ./router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/login')
  },
]
const router = createRouter({
  history: createWebHashHistory(),
  routes
})
export default router

总结

在views中创建页面之后,要在router中进行路由配置

四、功能实现

4.1 登录功能

4.1.1 双向数据绑定

  • ref+v-model
import { ref } from 'vue'
const form = ref({
  username: '',
  password: ''
})
// 在template便可以使用js中的变量
<el-input v-model="form.username" />

请注意我们在取ref传递的数据时,必须要通过value来获得 即form.value.xxx

4.1.2 element-plus icon

// 直接导入图标名 
import { User, Lock } from '@element-plus/icons-vue'
// 在template中可以直接使用
<el-icon :size="20" class="svg-container">
  <User />
</el-icon>

4.1.3 element-plus+element-ui表单校验

  • 在填写过程中进行校验
// required表示是否是必填项 message表示提示语 trigger表示如何触发
const rules = ref({
  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  password: [
    {
      required: true,
      message: '请输入密码',
      trigger: 'blur'
    }
  ]
})

// 在需要进行校验的组件中添加规则,并为对应的组件设置props绑定对应的规则
<el-form-item prop="username">
:rules="rules"
  • 点击登录按钮时进行统一校验,需要为button按钮绑定点击事件
<el-button round class="login-button" @click="handleLogin">{{
 登录
}}</el-button>
const formRef = ref(null) // 
const handleLogin = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      ...
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

4.1.4 发起请求操作

  • 设置基础路径
  1. 开发环境在项目根目录下创建.env.development
ENV = 'development'
VUE_APP_BASE_API = '/api'
  1. 生产环境
ENV = 'production'
VUE_APP_BASE_API = '/prod-api'
  1. 解决跨域问题 在vue.config.js设置代理
  devServer: {
    https: false,
    // hotOnly: false, // 是否热更新
    // hot: 'only',
    proxy: {
      '/api': { // 基础路径设置为api
        target: 'http://43.143.0.76:8889/api/private/v1/', // 接口地址
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  },
  • 在src中创建api文件夹,在文件夹中创建request.js 这是一个总的设置
import axios from 'axios'
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // 基础路径
  timeout: 5000
})
export default service // 导出供外界使用
  • 设置单独的login请求操作

这部分代码就是在说:从客户端输入信息后,通过为登录按钮设置点击事件向后台接口发送请求,在api/login.js中通过一个箭头函数返回请求的一些信息。例如:方法、地址、数据。请求的方法是post,请求的url地址是http://api/login 请求传递来的参数是向表单中输入的数据 请求的接口在vue.config.js中设置:'http://43.143.0.76:8889/api/private/v1/'

  1. 在api文件夹下创建./login.js
import request from './request'
export const login = (data) => { // 导出login方法
  return request({
    url: 'login', // 发请求的地址
    method: 'POST',
    data // 接收从登录页面传递过来的参数:用户信息(form)
  })
}
  1. 在.views/login/index.vue中导入login请求
import { login } from '@/api/login'

这样便可以在登录校验时向login传入用户写入的信息,带着他们发送请求

// 登录按钮触发的事件
const handleLogin = () => {
  formRef.value.validate(async (valid) => {
    if (valid) {
      const res = await login(form.value) // 将表单中传递的信息传给了api/login中 从中向服务端发送请求 得到响应数据
      console.log(res) // 传递回来的数据
    } else {
      console.log('error submit!!')
      return false
    }
  })
}
  • 使用await的原因?????

4.1.5 响应拦截器 更快的得到想要的信息

利用axios的响应拦截器,从服务端拿到我们想要的信息 并利用element-plus中的ElMessage向外抛出错误信息

在./api/request.js中

import {ElMessage} from 'element-plus'
sevice.interceptors.response.use((response) => {
    // console.log(response) 
    const {data, meta} = response.data // 解构
    if(meta.status === 200 || meta.status === 201){
        return data // 返回从服务器响应来的的信息
    }else{
        // 利用element中的ElMessage向外界传递
        ElMessage.error(meta.msg)
        return Promise.reject(new Error(meta.msg))
    }
}, error => {
    error.response && ElMessage.error(error.response.data)
    return Promise.reject(new Error(error.response.data))
})

4.1.6 将从服务器传来的token保存在vuex和localstorage中

token是存储在后端服务器中,这个项目中的token在用户发送请求后,服务器返回响应,将token返回。

  • vuex中的store的主要模板
export default {
  namespaced: true,
  state: () => ({}),
  mutations: {
  },
  actions: {
  }
}
  1. 在store中新建module/app.js
  • 我们通过token来使得他去进行登录,因为我们把登录的请求放到这里 对.views/login/index.vue中的代码进行改进
import { login as loginApi } from '@/api/login'
state: () => ({
    token: localStorage.getItem('token') || '', // 从localstorage中取出来token
  }),
mutations: {
    setToken(state, token) { 
      state.token = token  // 外界传入的token
      localStorage.setItem('token', token) // 接收到token并存进去
    },
},
actions: {
    login({ commit }, userInfo) {
      return new Promise((resolve, reject) => { // 发请求
        loginApi(userInfo) // 调用api中的login
          .then((res) => {
            console.log(res)
            commit('setToken', res.token) 
            // 调用settoken,拿到token,执行mutation中的操作
            setTokenTime() // 
            router.replace('/') // 成功之后跳到首页
            resolve() 
          })
          .catch((error) => { // 请求失败
            reject(error)
          })
      })
    },
    logout({ commit }) {
      commit('setToken', '')
      localStorage.clear()
      router.replace('/login')
    }
}
  1. 对.views/login/index.vue中的代码进行改进
import { useStore } from 'vuex'
const store = useStore()
const formRef = ref(null)
const handleLogin = () => {
  formRef.value.validate(async (valid) => {
    if (valid) {
      store.dispatch('app/login', form.value) 
      // 通过dispatch触发store/module/app中actions中的login
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

总结

利用vuex和localstorage完成login请求,并在请求成功时,拿到token并进行保存。我们在views/login/index.vue中的登录的单击响应函数中通过dispatch方法向调用vuex中的login方法,这个login放在写在store/module/app.js的actions中,actions中完成的操作为:发送login请求,并且在请求成功时,拿到响应中的token,触发mutation中的操作,将token保存在vuex(即state中)和localStorage中

  • vuex总结
  1. 利用请求拦截器对每一个接口添加上token信息
service.interceptors.request.use(
  (config) => {
    config.headers.Authorization = localStorage.getItem('token')
    return config
  },
  (error) => {
    return Promise.reject(new Error(error))
  }
)

4.2 对密码可见和不可见以及图标进行切换

对密码的输入框绑定动态的type、图标绑定单击响应函数

<el-input v-model="form.password" :type="passwordType" show-password />
<el-icon @click="changeType" :size="20" class="svg-container"> // 这里采用的是up主提到的矢量图
const passwordType = ref('password')
// 单击响应函数
const changeType = () => {
    if(passwordType.value === 'password'){
        passwordType.value = 'text'
    }else{
        passwordType.value = 'password'
    } 
}

4.3 路由守卫 只允许登录的用户访问主页面

  1. router下创建permission.js
  2. ./router/permisson.js
import router from './index' // 需要使用路由
import store from '@/store' // 判断是否登录 所以需要有token
const whiteList = ['/login']
router.beforeEach((to, from, next) => {
  if (store.getters.token) { // 如果有token表示登录成功
    if (to.path === '/login') { // 判断路径是否是login
      next('/')
    } else {
      next() // 不去login 随便去其他哪里
    }
  } else { // 
    if (whiteList.includes(to.path)) { // 设置白名单 在没有登录的时候也可以访问
      next()
    } else { // 如果都不在白名单内就需要登录
      next('/login')
    }
  }
})
  1. 为了方便拿到token,在store中创建一个getters.js
export default {
  token: (state) => state.app.token,
}
  1. 使用路由守卫 在main.js导入
import '@/router/permission'

总结

使用vue-router中的beforeEach全局前置守卫,需要接收三个参数。

image.png

4.4 主页面布局

4.4.1 layout父组件

  • element-plus设置布局
  • 在router/index.js中配置路由
path: '/',
name: '/',
component: () => import('../layout'),
  • 将我们事先设置的变量值全局导入,通过webpack配置来完成这个操作
 在vue.config.js中写入
 css: {
    loaderOptions: {
      sass: {
        additionalData: //或 prependData:   // sass-loader版本为8用prependData: 
        `
          @import "@/styles/variables.scss";  // scss文件地址
          @import "@/styles/mixin.scss";     // scss文件地址
        `
      }
    }
  }

每次更改完webpack都要重新启动服务器

4.4.2 menu子组件

  • 新建menu子组件文件夹
  • 利用element-plus创建菜单 image.png
  • 在父组件中引入子组件 import Menu from './Menu'便可以在template中作为标签使用
  • 向后端发起请求拿到接口中的数据./api/menu.js
import request from './request'
export const menuList = () => {
  return request({
    url: '/menus' //接口中的地址
  })
}
  • 在menu/index.vue中使用
import { menuList } from '@/api/menu'
const initMenusList = async () => { // 定义方法发请求
  menusList.value = await menuList()
  //   console.log(res)
}
  • 进行数据渲染
const menusList = ref([])
将上述从接口中拿来的数据存放到menuList中 数据为一级和二级数据
  • 在template中将数据显示
    <el-sub-menu :index="item.id" v-for="(item, index) in menusList" :key="item.id">
      <template #title>
        <span>{{ item.authName }}</span>
      </template>
      <el-menu-item index="1-3" v-for="child in item.children" :key="child.id">
        <template #title>
          <span>{{ child.authName }}</span>
        </template>
      </el-menu-item>
    </el-sub-menu>
  • 实现点击子项时,页面跳转,先把页面中的路径配置好 image.png 将子项的index进行修改:index="'/' + child.path"
  • 对跳转的页面配置路由(router/index.js)和页面(views)

在4.4.1 layout父组件设置的路由下,router/index.js中设置下一级路由 image.png

4.4.3 优化

  1. 优化点击菜单时,点击一项,每一项都展开了
  • 在template配置中设置唯一展开 image.png
  1. 默认一个展开项,和我们在router中设置的重定向匹配。在./layout/menu/index.vue
const defaultActive = ref('/users')
:default-active="defaultActive"
  1. 点击每一项显示的内容要和菜单相匹配。因此在点击每一项时,都要修改defaultActive的值,对子项绑定一个click事件。
  • <el-menu-item index="1-3" v-for="child in item.children" :key="child.id" @click="savePath(child.path)>
  • 把点击每一块子项的路径存在sessionStorage
const savePath = (path) => {
  sessionStorage.setItem('path', `/${path}`) 
}
  • 对默认打开的路径进行重新赋值 从sessionStorage中取路径信息
const defaultActive = ref(sessionStorage.getItem('path') || '/users') 
  • 统一导入icon
  1. image.png
  2. 作为组件在template中被使用。<component :is="icon"></component>

总结

  1. 设置变量供全局使用时,采用webpack进行配置
  2. 父子组件的设计:父组件是框架,子组件是每部分的细节
  3. 数据的拿取:在api/xxx.js配置接口的url信息,然后在xxx/index.vue中通过async和await结合得到取出来数据存放到事先定义好的响应式数据类型的变量中。注意:一定要存放在.value中。
  4. 数据的渲染:在xxx.vue中的template中渲染数据,利用v-for or 其他代码
  5. 对于每个页面的跳转,要在router/index.js中配置相应的路由

4.5 被动退出(设置token有效期)

  1. 新建./utils/auth.js
// 定义常量值 ./utils/costant.js
export const TOKEN_TIME = ' tokenTime'
export const TOKEN_TIME_VALUE = 2 * 60 * 60 * 1000 // 2小时
import { TOKEN_TIME, TOKEN_TIME_VALUE } from './constant'
// 登录时设置时间
export const setTokenTime = () => {
  localStorage.setItem(TOKEN_TIME, Date.now())
}
// 获取登录时的时间
export const getTokenTime = () => {
  return localStorage.getItem(TOKEN_TIME)
}
// 是否已经过期
export const diffTokenTime = () => {
  const currentTime = Date.now()
  const tokenTime = getTokenTime()
  return currentTime - tokenTime > TOKEN_TIME_VALUE
}
  1. 在请求拦截器(api/request.js)中判断token是否过期
import { diffTokenTime } from '@/utils/auth'
if (localStorage.getItem('token')) { // 是否有token
  if (diffTokenTime()) {
    // 通过vuex实现退出操作
    store.dispatch('app/logout')
    return Promise.reject(new Error('token 失效了'))
  }
}
  1. 在.router/module/app.js的actions中完成登出操作

logout({ commit }) {
  commit('setToken', '') // 将存的token变成一个空字符串
  localStorage.clear()
  router.replace('/login')
}
  1. 在登录时也要设置上登录时间
import { setTokenTime } from '@/utils/auth'
  .then((res) => {
    console.log(res)
    commit('setToken', res.token)
    setTokenTime()
    router.replace('/')
    resolve()
  })

总结

  1. 首先新建一个js,定义一个token和token的有效期
  2. 定义箭头函数保存登录时的时间和判断是否过期
  3. 将2中返回的值在请求拦截器和router中使用:在请求拦截器中每次发起请求先去判断是否有token,有的话是否过期,若从2中返回为true则利用dispatch触发在store中的actions中的logout方法,用commit提交新的token值存到state中,并跳转到登陆页面。

4.6 头像退出(用户主动退出)

  1. 对头像设置一个点击事件:@click:"logout"
  2. 写事件
const logout = () => {
  store.dispatch('app/logout')
}

eg:logout是共同使用的,则可以将logout这个方法保存在vuex中的actions中,提高代码的复用。在需要使用的地方利用dispatch方法抓取即可。

4.7 中英文切换i18n

  1. yarn add vue-i18n@next
  2. 创建./i18n文件夹
  3. ./i18n/index.js
import { createI18n } from 'vue-i18n'
// 创建数组源
const messages = {
  en: {
    msg:{
      title:'user'
    }
  },
  zh: {
    msg:{
      title:'用户'
    }
  }
}
  1. 设置语言
const getCurrentLanguage = () => {
  const UAlang = navigator.language // 通过浏览器设置上语言
  const langCode = UAlang.indexOf('zh') !== -1 ? 'zh' : 'en' // 判断是否为中文
  localStorage.setItem('lang', langCode) // 并把当前语言存在本地
  return langCode // 返回给外界
}

const i18n = createI18n({ // 创建i18n
  legacy: false, 
  globalInjection: true, // 全局t函数
  locale: getCurrentLanguage() || 'zh', // 语言
  messages: messages // 数据源
})

export default i18n // 供外界使用
  1. 在main.js中导入 导入i18n 并利用app.use(i18n)
  2. 在显示语言的部分利用全局t()函数进行修改
  • ./views/login/index.vue <h3 class="title">{{ $t('msg.title') }}</h3>
  1. 导入中英文数据源
import EN from './en'
import ZH from './zh'
const messages = {
  en: {
    ...EN
  },
  zh: {
    ...ZH
  }
}
  1. 修改各部分
  • {{ $t('login.title') }}
  • {{ $t('login.btnTitle') }}
  • {{ $t(menus.${item.path}) }}
  • {{ $t(menus.${child.path}) }}
  1. 实现切换
  • 使用一个下拉菜单完成切换 image.png
  • @command="handleCommand" 绑定指令事件
const handleCommand = (val) => {
  i18n.locale.value = val // val是标签中定义的值 修改i18n中的值
  // 将值存储在vuex和localStorage中
  store.commit('app/changeLang', val) // 提交给store中的mutations处理
  localStorage.setItem('lang', val)
}

优化

当处于中文时,中文不能选中。

  • 在内部设置computed函数,用于返回语言的值
  • 取当前的语言,利用i18n中的locale
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
const i18n = useI18n()
const currentLanguage = computed(() => {
  return i18n.locale.value
})
  • 在template中利用disabled属性,将其动态化:disabled="currentLanguage === 'zh'"

总结

中英文的切换通过值的改变来更换,这里将值存在store中是利用mutations中,是因为只是传递给值过去,无需再进行什么请求或者其他操作。个人理解:mutations中的操作是简单的一些逻辑操作,actions中的操作可能会向外界发送请求等。

4.8 全屏功能

  1. yarn add screenfull@5.1.0
  2. 对图标绑定点击事件handleFullScreen,利用点击事件传来的值和v-if完成图标的切换
  <div @click="handleFullScreen" id="screenFull">
    <el-icon v-if="icon === false"><FullScreen /></el-icon>
    <el-icon v-if="icon === true"><CloseBold /></el-icon>
  </div>
  1. 设置全屏的点击事件,利用toggle()完成
import screenfull from 'screenfull'
const handleFullScreen = () => {
  if (screenfull.isEnabled) {
    screenfull.toggle()
  }
}
  1. 监听screenfull的改变,普通的watch无法完成监听,需要利用onMounted和onBeforeUnmount
import { ref, onMounted, onBeforeUnmount } from 'vue'
const icon = ref(screenfull.isFullscreen)
const changeIcon = () => {
  icon.value = screenfull.isFullscreen
}
onMounted(() => {
  screenfull.on('change', changeIcon)
})
onBeforeUnmount(() => { // 取消监听的钩子函数
  screenfull.off('change', changeIcon)
})

总结

screenfull图标的的切换需要对状态进行监听,利用vue的生命周期的钩子函数。

仍需学习内容:

选项式API

<script>
export default{
    data(){},
    method:{updateAge(){}},
    computed:{xxx:function(){}},
    components: {}
}
</script>

1. vue响应式原理 data() 指定响应式属性

  • 借用代理。
// 创建一个对象
const obj = {
    name: "孙悟空",
    age: 18
}

对这个对象的属性修改不是响应式的,无法使页面重新渲染,展示改变之后的数据,需要借助代理。

// 为对象创建一个代理
const handle = { // handle用来处理代理的行为
    // get用来指定读取数据时的行为 他的返回值就是最终读取到的数据
    // 指定get后 在通过代理读取对象属性时 就会调用get方法
    get(target, prop, receiver){
        // 返回值之前做一些操作。。。 track()追踪谁用了这个属性
        /* 
            会收到3个参数
                target, 被代理的对象
                prop,  读取的属性
                receiver 代理对象
        */
        return target[prop]
    },
    // set会在通过代理修改对象时调用
    set(target, prop, value, receiver){
        // 在值修改之后做一些其他的操作
        // console.log(target, prop, value, receiver); 
        // 被代理的对象 属性名 修改后的属性值 代理对象
        target[prop] = value
        // 值修改之后做一些其他的事情  trigger() 相当于是一个触发器 触发所有使用该值的位置进行更新
    }
} 
// 创建代理
// Proxy 参数1 要被代理的对象 参数2 handle 行为 proxy是一个原生的类,专门用来创建代理。
const proxy = new Proxy(obj, handle) 
// 修改属性
proxy.age =28 // 实际上就是修改了obj的属性 调用了handle中的set方法
console.log(proxy.age); // 28 // 调用了handle中的get方法
  • 在vue中,data()返回的对象会被vue代理,vue代理后,
  • 当我们通过代理读取属性时,返回值之前会先做一个跟踪的操作。
  • 当通过代理去修改属性时,修改后会通知之前所有用到该值的位置进行更新。

总结

  • vue3的响应式原理是通过proxy代理来完成,代理中传递了两个参数,第一个参数为被代理的对象,第二个参数为handle行为,通过代理来读取某对象的属性时,会调用handle中的get方法对该属性进行追踪,改变某对象的属性时,会调用handle中的set方法,值修改完成后,再通过trigger函数触发,完成更新。

  • 只有通过代理去改变属性值才是响应式

  • vue在构建响应式对象时,会同时将对象中的属性也做成响应式属性(深层响应式对象)

  • (浅层响应式对象)import { shallowReactive } from 'vue' return shallowReactive({})一般不需要

  • 在data中可以通过this.$data.xxx = 'xxx' 动态的添加响应数据(不建议这样做),建议将暂时不使用的属性也在data中return,值先设置为null

  • 所有组件实例上的属性都可以在模板中直接访问{{ }}

  • 补充:响应式原理

  • vue3实现原理:
    通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
    通过Reflect(反射): 对源对象的属性进行操作。\
new Proxy(data, {
// 拦截读取属性值
  get (target, prop) {
        return Reflect.get(target, prop)
  },
  // 拦截设置属性值或添加新属性
  set (target, prop, value) {
        return Reflect.set(target, prop, value)
  },
  // 拦截删除属性
  deleteProperty (target, prop) {
        return Reflect.deleteProperty(target, prop)
  }
})
proxy.name = 'tom'  
  • vue2.x的响应式
    实现原理:
    对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。
    数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
    存在问题:
    新增属性、删除属性, 界面不会更新。
    直接通过下标修改数组, 界面不会自动更新。
Object.defineProperty(data, 'count', {
    get () {}, 
    set () {}
})

2. methods 指定实例对象中的方法

  • methods是一个对象,可以在里边定义多个方法,这些方法最终将挂载到组件实例上,可以通过组件实例调用这些方法。
  • 在模板中调用时需要加上括号{{ xxx() }}
  • 在可以通过@click=''来调用
  • methods的this就是组件实例,proxy代理对象

3. computed用来指定计算属性

  • 可以把{{}}中的复杂的表达式写在其中
  • 和data一样,返回值在模板中可以直接被读取,无需加括号()便可以在模板中{{}}去调用,但是它【可以通过一个函数】返回结果 可以写一个逻辑
  • 和methods不同的点在于
    1. 计算属性只在其依赖的数据发生变化时才会重新执行 会对数据进行缓存,而methos每次都会重新渲染
    1. 并且无需加括号去调用
  • 在computed中尽量只做读取相关的逻辑
  • 可以为计算属性设置set 使得计算属性可写(修改数据) 但是不建议这样写

在模板中的{{}}必须要写具有表达式,即有返回值的代码

组合式API

<script>
    setup(){return xxx, xxx}
</script>
<script setup></script>

1. setup()钩子函数

  • setup()是一个钩子函数 可以通过这个函数向外部暴露组件的配置
  • 可以通过返回值来指定哪些内容要暴露给外部
  • 在组合式api中直接声明的变量 就是一个普通的变量 不是一个响应式变量(通过ref和reactive)

2. 响应式ref和reactive

  • reactive响应式变量
    • 返回一个对象的响应式代理
const stu = reactive({
    name:"swk",
    age:18,
    gender:"男"
})
    • 返回一个深层的响应式对象
    • 也可以使用shallowReactive()创建一个浅层响应式对象
    • 缺点: 只能返回对象的响应式代理 不能处理原始值
  • ref()
    • 可以接收任意值并返回他的响应式代理 const count = ref(0)
    • ref在生成响应式代理时 将值包装成一个对象0 -> {value:0}
    • 在js中访问ref对象时需要通过.value访问值
    • 在模板中访问ref对象时 模板自身会将其自动解包 无需通过value读值{{ count }}

在js中无法实现对一个变量的代理

双向数据绑定(如何处理表单)

1. 事件相关的东西

v-on:click简写为@click 绑定事件 等同于dom中的btn01.onclick = () => {}

方法事件处理器的回调函数 vue会将事件对象作为参数传递
这个事件对象就是DOM中原生的事件对象 他里边包含了事件触发时的相关信息
通过该对象 可以获取:触发事件的对象 触发事件时一些情况
同时通过该对象 也可以对事件进行一些配置 取消事件的传播、取消事件的行为

.stop 停止事件的冒泡?似乎只能停止冒泡
.capture 在捕获阶段触发事件
.prevent 取消默认行为
.self 只有事件由自身触发时 才会有效
.once 绑定一个一次性的事件
.passive 只要用于提升滚动事件的性能

2. 表单的单向数据绑定

监听input的变化

let text = ""
function submitHandle() {
  console.log(text.value);
  // 将text提交给服务器 再根据服务器返回的数据做后续的操作
}
</script>
<template>
  // 不希望表单去提交 利用.prevent取消默认行为
  <form @submit.prevent="submitHandle">
    <div>
      <input type="text" @input="(event) => (text = event.target.value)"/>
    </div>
    <div><button>提交</button></div>
  </form>
</template>
  • 这里我们将表单项的value属性和变量text做了绑定
  • 当value发生变化时 text变量也随之改变(这种情况称为单向绑定)
  • 当value或text任意一个发生变化,另一个也随之变化(这种情况称为双向绑定)

3. 表单的双向数据绑定

将表单项的value属性和text进行绑定

import {ref} from "vue"
let text = ref("")
<input type="text" @input="(event) => (text = event.target.value)" :value="text"/>

4. vue双向绑定的优雅形式:v-model

将value的值传递给v-model中

import {ref} from "vue"
let text = ref("")
<input type="text" v-model = 'text'/>
  • 选择框的v-model传递一个布尔值
是否:<input type="checkbox" v-model="bool" true-value="是" false-value="否"/>
  • 多选框
const hobbies = ref([])
<div>
  爱好:
  <input v-model="hobbies" type="checkbox" name="hobby" value="足球">足球
  <input v-model="hobbies" type="checkbox" name="hobby" value="篮球">蓝球
  <input v-model="hobbies" type="checkbox" name="hobby" value="羽毛球">羽毛球
  <input v-model="hobbies" type="checkbox" name="hobby" value="乒乓球">乒乓球
</div>
  • 下拉框
const friend = ref("")
<div>
  朋友
  <select v-model="friend">
    <option disabled value="">请选择你的好朋友....</option>
    <option>孙悟空</option>
    <option>猪八戒</option>
    <option>唐僧</option>
  </select>
</div>

5. v-model的修饰符 v-model.xxx

  • .lazy 使用change来处理数据 不会频繁处理事件
  • .trim 去除前后的空格
  • .number 将数据转换为数值

v-show和v-if

  • v-show 切换且资源是静态的推荐v-show
  1. 可以根据值来决定元素是否显示(通过display来切换元素的显示状态)
  2. v-show通过css来切换组建的显示与否 切换时不会涉及到组建的重新渲染
  3. 切换的性能比较高 但是初始化时需要对所有组件初始化 初始化性能比较低
  • v-if 资源是动态的,可以根据表达式的值来决定是否显示元素 (直接将元素删除)是懒加载(使用的时候才加载)
  1. v-if通过删除添加元素的方式来切换元素的显示 切换时会反复渲染组件
  2. 切换的性能比较低 只对用的元素初始化 初始化性能比较好
  3. v-if 可以配合其他的组件使用,eg v-else-if v-else结合使用v-if
  4. 可以配合template使用 template不显示在代码中

动态组件 components

最终以什么标签呈现 由is属性值决定

const isShow = ref(false)
<button @click="isShow = !isShow">点我一下</button>
<component :is="isShow ? A : B">
  我是一个component
</component>

组件通信

子组件的数据一般不会再子组件中直接定义,这样会导致数据和视图发生耦合,一般会在创建组件实例中(父组件)传递数据。

1. props|单向流动

  • 父组件可以通过props向子组件传递数据(单向流动的数据)
  • 父组件传给子组件的props只能读 不能改 锁住的是变量地址值,但是如果改变对象中的属性值就可以修改
  • 即使可以修改 也尽量不要去修改。!!!尽量不要在子组件中修改父组件的值。如果非要修改,后边会讲(自定义事件)
  • 尽可能保证 所有的数据修改从同一个入口进入
  • eg 只能从根组件App.vue修改,子组件只去读数据
  • 定义属性名时 属性名要遵从驼峰命名法 maxLength

使用props
先在子组件中定义props
const props = defineProps(["item"]) 表示属性名
然后在父组件中对props定义的属性赋值 再通过父组件传递给子组件
import {ref} from 'vue'
const player = ref({...})
<TabItem :item="palyer"></TabItem>

2. props配置

const props = defineProps({
    count: Number, // 限定属性的数据类型
    obj: Object,
    isCheck: Boolean, // 默认为true
    maxLength:{
        type: String,
        require: true, // 必须有这个属性
        default: "HAHA", // 默认值
        validator(value){ // 用于在开发过程中进行验证 返回flase则不弹出警告 返回true就会有警告
        // 用于判断是否传入了一些不想要的东西
            // console.log(value);
            return value !== "嘻嘻"
        }
    },
    
})

3. 插槽

解决问题:

  • 传递层级太多 相比props的特点:
  • 不用传递属性之类的,插槽入口可以是标签、值等任意
<!-- 
  希望在父组件中指定在子组件中的内容
    可以通过插槽实现该需求<slot>
    - 非命名插槽的入口:
      <MyButton>插槽的入口</MyButton>
      <button>
         <slot></slot> 插槽的出口
      </button>
    - 具名插槽的入口:
        方式1:<template v-slot:插槽的名字></template>
        方式2:<template #插槽的名字></template>
        出口:<slot name="插槽的名字"></slot>//
 -->

4. 自定义事件(可以由子组件向父组件传递)

(App中无法知道这个参数存在)在模板中可以通过$emit()来触发自定义事件 但是空参

如何传参:$emit(要触发的事件名,传递的参数)

但是通常不这样使用,更好的方式:

// // 使用自定义事件时 最好通过defineEmits来完成
const emits = defineEmits(['delStu'])
<StudentItem :stus="props.stus" @del-stu="emits('delStu', $event)"></StudentItem>

5. 依赖注入,要求发生在层级关系之间,兄弟不可以

  • 解决从根组件到子子子子子..组件传值的问题
  • 通过依赖注入 可以跨越多层组件 向其他组件传递数据
  • 两步使用依赖注入
  1. 设置依赖(provide) provide("name","value")
  2. 输入数据(inject) const value = inject(name, default)
  • 注意: 依赖注入存在于有层次关系的组件中 并且取值符合就近原则

6. vuex

7. 全局事件总线 eventBus

Vue3全局组件通信之EventBus - 简书 (jianshu.com)

vuex的工作原理

Snipaste_2023-04-24_22-03-47.png

  • state
  1. vuex 管理的状态对象
  2. 它应该是唯一的
  • actions
  1. 值为一个对象,包含多个响应用户动作的回调函数
  2. 通过 commit( )来触发 mutation 中函数的调用, 间接更新 state
  3. 如何触发 actions 中的回调? 在组件中使用: $store.dispatch('对应的 action 回调名') 触发
  4. 可以包含异步代码(定时器, ajax 等等)
  • mutations
  1. 值是一个对象,包含多个直接更新 state 的方法
  2. 谁能调用 mutations 中的方法?如何调用? 在 action 中使用:commit('对应的 mutations 方法名') 触发
  3. mutations 中方法的特点:不能写异步代码、只能单纯的操作 state
  • getters
  1. 值为一个对象,包含多个用于返回数据的函数
  2. 如何使用?—— $store.getters.xxx
  • modules
  1. 包含多个 module
  2. 一个 module 是一个 store 的配置对象
  3. 与一个组件(包含有共享数据)对应

vue的生命周期,比如一些钩子函数

  • vue2 生命周期.png
  • created之前是创建数据代理和数据监测
  • mounted这个过程结束才会将页面渲染成功,在这里再做一些初始化的事情,eg:发送网络请求,开启定时器等;
  • 完成一些收尾工作

常用的生命周期钩子:
1.mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。
2.beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。
关于销毁Vue实例
1.销毁后借助Vue开发者工具看不到任何信息。
2.销毁后自定义事件会失效,但原生DOM事件依然有效。
3.一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了。

  • vue3 将vue2中的最后一组钩子更名为:unmounted和mounted,在初始化之前需要万事俱备,如下所示,才可以下一步操作
//创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”)
const app = createApp(App)
//挂载
app.mount('#app')

通过组合式api ef3fa271daecdc0e3f6085e1dfd9cc3.jpg

vue的路由 hash和history

服务器路由和客户端路由

服务端路由指的是服务器根据用户访问的 URL 路径返回不同的响应结果。当我们在一个传统的服务端渲染的 web 应用中点击一个链接时,浏览器会从服务端获得全新的 HTML,然后重新加载整个页面。 然而,在单页面应用中,客户端的 JavaScript 可以拦截页面的跳转请求,动态获取新的数据,然后在无需重新加载的情况下更新当前页面。这样通常可以带来更顺滑的用户体验,尤其是在更偏向“应用”的场景下,因为这类场景下用户通常会在很长的一段时间中做出多次交互。 在这类单页应用中,“路由”是在客户端执行的。一个客户端路由器的职责就是利用诸如 History API 或是 hashchange 事件这样的浏览器 API 来管理应用当前应该渲染的视图。

不同的客户端路由模式

image.png hash 模式是一种把前端路由的路径用井号 # 拼接在真实 URL 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 hashchange 事件。 image.png