vue3复习-全家桶/实战/TS

153 阅读11分钟

1.vuex

数据共享问题

  1. 定义全局的window对象,然后使数据有响应式,用ref reactive等
  2. vuex全局维护所有组件状态,同时自带响应式

例子

store/index.js

import {createStore} from 'vuex'

const store = createStore({
    state(){
        return {
            cnt:111
        }
    },
    mutations: {
        addCnt(state){
            state.cnt++
        }
    }
})
export default store

test.vue

<template>
    <div>
        <button @click="test">{{store.state.cnt}}</button>
    </div>
</template>

<script setup lang="ts">
import {useStore} from 'vuex'
const store = useStore()
function test(){
    store.commit("addCnt")
}
</script>

<style scoped>

</style>

源码实现

vuexMini.js

import {inject,provide,reactive,computed} from 'vue'
const storeKey = '__store__key__'
function useStore(){ 
    return inject(storeKey)//这里返回注入的 store
}
function createStore(options){
    return new Store(options)
}
class Store{
    constructor(options){
        this._state = reactive({
            data:options.state() //这里需要主动调用一次state才能返回数据
        })
        this._mutations = options.mutations
        this._actions = options.actions
        this.getters = {}
        const kes = Object.keys(options.getters)
        for (let index = 0; index < kes.length; index++) {
            const key = kes[index];
            const fun = options.getters[key]
            Object.defineProperty(this.getters,key,{
                get: ()=>{
                        return computed(()=>{ //设置缓存
                            return fun(this)
                        } 
                    ) 
                }
            })
        }
    }
    install(app) {//注册时候的代码,把store挂载到全局 
        app.provide(storeKey,this)
    }
    get state(){
        return this._state.data // 这里只返回内部变量的data
    }
    commit(key,palyload){
        this._mutations[key](this,palyload)
    }
    dispatch(key,palyload){
        this._actions[key](this,palyload)
    }
} 
export {createStore, useStore}

store/index.js

import {createStore} from '../vuexMini.js'

const store = createStore({
    state(){
        return {
            cnt:111
        }
    },
    mutations: {
        addCnt(store){  
            store.state.cnt++
        }
    },
    getters: {
        doubleCnt(store){ 
            return store.state.cnt * 2
        }
    },
    actions: {
        addCntByReq( store){
            new Promise( (resolve,reject)=> {
                setTimeout(() => {
                    resolve()
                }, 1000);
            }).then((res) => {
                store.commit("addCnt",100)
            })
            
        }
    }
})
export default store

test.vue

<template>
    <div>
        {{store.state.cnt}}
        {{store.getters.doubleCnt}}
        <button @click="test">xxx</button>
    </div>
</template>

<script setup lang="ts">
import {useStore} from '../vuexMini.js'
const store = useStore() 
function test(){ 
    store.commit("addCnt",store.state.cnt++) 
    store.dispatch('addCntByReq')
}
</script>

<style scoped>

</style>

pinia

  • 解决vuex中对ts 类型支持差问题

2.vue-router

2种方式

  1. 传统jsp ,后端决定路由与页面数据,每次都是整页刷新
  2. 前端js控制本地路由与url变化,实现内容局部刷新交互: 早在jquery时代 pushState+ajax 就已经流行

前端路由方式

  • hash
    • 通过#锚点实现
    • hashChange监听变化
  • history
    • 通过history.pushState,replaceState
    • history.popState监听变化

源码实现

vueRouterMini.js


import {ref,inject} from 'vue' 
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'
const ROUTER_KEY = '__router__'
function createRouter(options){
    return new Router(options)
}
function useRouter(){
    return inject(ROUTER_KEY)
}
function createWebHashHistory(){//构造方法 返回 {bindEvents:xx, url:xxx} 
    function bindEvents(fn){
        window.addEventListener('hashchange',fn)
    }
    return {
        bindEvents,
        url:window.location.hash.slice(1) || '/'
    }
    // 等价于
    // this.current = ref("/")
    // window.addEventListener('hashchange', () => {
    //     this.current.value = window.location.hash.slice(1)|| '/'
    // })
}
class Router{
    constructor(options){
        this.history = options.history
        this.current = ref(this.history.url)
        this.history.bindEvents(()=>{
            this.current.value = window.location.hash.slice(1)
        })
        this.routes = options.routes
    }
    install(app){ 
        app.provide(ROUTER_KEY,this)
        app.component("router-link",RouterLink)
        app.component("router-view",RouterView)
    }
}
export {createRouter,createWebHashHistory,useRouter}

RouterLink.vue

<template>
    <a :href="'#'+props.to">
        <slot />
    </a>
</template>
<script setup>
import {defineProps} from 'vue'
let props = defineProps({
    to:{type:String,required:true}
})
</script>

RouterView.vue

<template>
    <component :is="comp"></component>
</template>
<script setup>
import {computed } from 'vue'
import { useRouter } from './routerMini.js'
let router = useRouter()
const comp = computed(()=>{
    const route = router.routes.find(
        (route) => route.path === router.current.value
    )
    return route?route.component : null
})
</script>

router/index.js


import {
    createRouter,
    createWebHashHistory,
  } from './routerMini'
  import Home from '../pages/home.vue'
  import About from '../pages/about.vue'
  
  
  const routes = [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
  const router = createRouter({
    history: createWebHashHistory(),
    routes
  })
  
  export default router

3.性能测试

  • lighthouse灯塔 选择performance分析,显示各个性能指标, 并根据建议优化

4.jsx

temlpate

  • 缺点
    • 功能单一
    • 不支持动态的需求
  • 优点
    • 在vue3里会做很多编译时候的性能优化
      • 把重复的数据做提升
      • 事件缓存
      • 静态节点标记,优化diff效率
    • 使用简单

vue组件的渲染流程

  • template -> h函数 -> vnode
  • 使用过于复杂

h函数

使用

<template>  
    <div>  
       <myhead :level="2">标题头1111</myhead>  
    </div>  
</template>  
  
<script setup lang="ts">  
import myhead from './myhead.jsx'  
</script>

myhead.jsx

import { defineComponent, h } from 'vue'  
export default defineComponent({  
  props: {  
    level: {  
      typeNumber,  
      requiredtrue  
    }  
  },  
  setup(props, info) {  
    return () => h(  
      'h' + props.level// 标签名  
      {}, // prop 或 attribute  
      info.slots // 子节点  
    )  
  }  
})

jsx

jsx是js的一种语法糖

参考

cn.vuejs.org/guide/extra…

const el = <h1 class="cls">test11</h1>  
//等价于  
const el = createVnode('h1',{class:"cls"}, 'test11')

myhead.jsx

import { defineComponent, h } from 'vue'  
export default defineComponent({  
  props: {  
    level: {  
      typeNumber,  
      requiredtrue  
    }  
  },  
  setup(props, info) {  
    const tag = 'h'+props.level  
    console.log(tag,info.slots.default)  
    return () =>  <tag>{info.slots.default()}</tag>;  
  }  
})
  • 特点
    • 支持更动态需求
    • 支持返回不同的组件
export const Button = (props,{slots})=><button {...props}>slots.default()</button>  
export const Input = (props)=><input {...props} />
  • 缺点
    • 失去vue3的性能优化
    • 维护麻烦

配置

安装

npm install @vitejs/plugin-vue-jsx -D

设置 vite.config.js

import vueJsx from '@vitejs/plugin-vue-jsx';  
export default defineConfig({  
  plugins: [vue(),vueJsx()],  
})

5.实战优化

1. Vue 项目的规范和基础库封装

配置

npm install element3 --save
npm i axios -D
npm install -D sass

axios

import axios from 'axios'
import { useMsgbox, Message } from 'element3'
import store from '@/store'
import { getToken } from '@/utils/auth'
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  timeout: 5000, // request timeout
})
service.interceptors.request.use(
  config => {
    if (store.getters.token) {
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    console.log(error) // for debug
    return Promise.reject(error)
  },
)
service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 20000) {
      console.log('接口信息报错',res.message)
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    console.log('接口信息报错' + error) 
    return Promise.reject(error)
  },
)
export default service

eslint

  npm i eslint -D
  npx eslint --init进行配置 

package.json

"scripts": {  "lint": "eslint src/** --fix" }

husky

npm install -D husky # 安装husky
npx husky install    # 初始化husky


npx husky add .husky/commit-msg "node scripts/verifyCommit.js"

scripts/verifyCommit.js

import { createRequire } from 'module'; 
const require = createRequire(import.meta.url);
const msg = require('fs')
.readFileSync('.git/COMMIT_EDITMSG', 'utf-8')
.trim()

const commitRE = /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?: .{1,50}/
const mergeRe = /^(Merge pull request|Merge branch)/
if (!commitRE.test(msg)) {
if(!mergeRe.test(msg)){
  console.log('git commit信息校验不通过')
  console.error(`git commit的信息格式不对, 需要使用 title(scope): desc的格式
    比如 fix: xxbug
    feat(test): add new 
    具体校验逻辑看 scripts/verifyCommit.js
  `)
  process.exit(1)
}
}else{
console.log('git commit信息校验通过')
}
执行流程
  1. git init 初始化git项目
  2. git commit -m 'xxx' 
  3. 触发.husky 钩子  .husky\commit-msg
  4. 执行对应的js文件

2. 权限

vite模拟mock

npm i mockjs -D 
npm i vite-plugin-mock -D

src/mock/index.ts

import { MockMethod } from 'vite-plugin-mock'

export default [
  {
    url: '/api/getUserInfo', // 注意,这里只能是string格式
    method: 'get',
    response: () => {
      return 'hello world and get mockData'
    }
  }
] as MockMethod[]

vue实现

  1. login页面判断是否有权限
  2. 登录成功后,token存localsotrage本地
  3. 修改axios头信息,加入token,请求接口
  4. 通过返回的状态码,配合路由的router.beforeEach全局拦截

登录

handleLogin() {
  formRef.value.validate(async valid => {
    if (valid) {
      loading.value = true
      const {code, message} = await useStore.login(loginForm)
      loading.value = false
      if(code===0){
        router.replace( toPath || '/')
      }else{
        message({
          message: '登录失败',
          type: 'error'
        })
      }
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

路由配置


import Login from '../components/Login.vue'
const routes = [
...
{
  path: '/login',
  component: Login,
  hidden: true,
}
]

发起网络请求


{
  url: '/geek-admin/user/login',
  type: 'post',
  response: config => {
    const { username } = config.body
    const token = tokens[username]
    // mock error
    if (user!=='dasheng') {
      return {
        code: 60204,
        message: 'Account and password are incorrect.'
      }
    }
    return {
      code: 20000,
      data: token
    }
  }
}

请求拦截

service.interceptors.request.use(
  config => {
    const token = getToken()
    // do something before request is sent
    if (token) {
      config.headers.gtoken = token
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

路由守卫

router.beforeEach(async (to, from,next) => {
  // canUserAccess() 返回 `true` 或 `false`
  let token = getToken()
  if(!token){
     next('/login')
  }
  return true
})

传统cookie方式

  • 原理:直接使用服务器 res.cookie('name', 'value', { maxAge: 900000, httpOnly: true });写入cookie到浏览器,每次浏览器会带上cookie发起请求,也叫做session
  • 缺点 - 当前后端分离时候,并且部署的不同的机器上,session不能做到唯一
  • 推荐使用token解决 - token过期
    • 后台校验,返回特殊码,前端根据特殊码提示过期 - 存储使用vuex+ localstorage

角色权限

  • RBAC(Role-Based Access Control,基于角色的访问控制) 用户 -> 角色 -> 页面
  • 登录的时候根据权限,返回有权限的路由
  • 把当前路由与 后台动态路由合并
addRoutes({ commit }, accessRoutes) {
    // 添加动态路由,同时保存移除函数,将来如果需要重置路由可以用到它们
    const removeRoutes = []
    accessRoutes.forEach(route => {
      const removeRoute = router.addRoute(route)
      removeRoutes.push(removeRoute)
    })
    commit('SET_REMOVE_ROUTES', removeRoutes)
  },

3. 集成第三方库

集成NProgress


import NProgress from 'nprogress' // progress bar
router.beforeEach(async (to, from, next) => {
  // start progress bar
  NProgress.start()
})
router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

注意事项

避免使用mixins,extends
  • 隐式添加的api无法溯源
  • 命名冲突
vue3全局使用
app.config.globalProperties.x 注册访问

自定义组件chart

通过 onmount 和unmonut 控制加载与卸载

<template>
  <div ref="chartRef" class="chart"></div>
</template>
<script setup>
import * as echarts from 'echarts'
import {ref,onMounted,onUnmounted} from 'vue'
// 通过ref获得DOM
let chartRef = ref()
let myChart 
onUnmounted(()=>{
  myChart.dispose()
  myChart = null
})
onMounted(()=>{
    myChart = echarts.init(chartRef.value)
     const option = {
        tooltip: {
            trigger: 'item'
        },
        color: ['#ffd666', '#ffa39e', '#409EFF'],
        // 饼图数据配置
        series: [
            {
                name: 'title',
                type: 'pie',
                radius: '60%',
                data: [
                    {value: 43340, name: '1111'},
                    {value: 7003, name: '2222'},
                    {value: 4314, name: '3333'}
                ]
            }
        ]
    }
    myChart.setOption(option)
})
</script>

自定义指令

const lazyPlugin = {
  install (app, options) {
    app.directive('lazy', {
      mounted: ...,
      updated: ...,
      unmounted: ...
    })
  }
}

4. 性能优化

从输入url 到显示页面浏览器都做了啥

1. dns解析

可以做预解析

建立连接,三次握手
  • 优化减少请求的数量
    • 雪碧图
    • http2
  • 优化请求的大小
    • 图片格式调整使用 webp png
    • 压缩图片
    • 开启gzip
  • 懒加载
    • 到可视区域才开始加载
  • 路由懒加载
  • vite可视化插件
npm i rollup-plugin-visualizer
npm run build

查看dist报告

下载 html js css
代码执行效率
  • 算法优化
    • 递归改递推遍历的方式
  • 尽量使用temlate语法,让vue3自动做优化

用户体验优化

  • 失去一点点性能,提高体验
  • 模糊的占位符到 原图
  • 大文件不一次性上传,分多几次上传,再合并
  • 先渲染鱼骨图,再显示内容

性能检测报告

  1. FCP
  • First Contentful Paint 首次显示第一个dom的时间
  1. TTI
  • Time to interactive 可以交互的时间
  1. 使用performance 对象
  • 自己通过start和end计算时间差

5. 自动化部署

git actions + docker

流程
  1. 代码提交
  2. 服务器检测变化
  3. 自动打包html到项目服务器
  4. 自动打包cdn的到cdn服务器
代码

name: 打包应用的actions
on:
  push: # 监听代码时间
    branches:
      - master  # master分支代码推送的时候激活当前action
jobs:
  build:
    # runs-on 操作系统
    runs-on: ubuntu-latest
    steps:
      - name: 迁出代码
        uses: actions/checkout@master
      # 安装Node
      - name: 安装Node
        uses: actions/setup-node@v1
        with:
          node-version: 14.7.6
      # 安装依赖
      - name: 安装依赖
        run: npm install
      # 打包
      - name: 打包
        run: npm run build

预发布

  • 都是正式环境数据
  • 只有开发和测试使用

支持版本回滚

  • 根据版本号回滚

AB测试

  • 上线的时候给某个区域的用户先试用
  • 稳定后再推广全国

部署结束后通知

  • 通过集成钉钉,或邮件通知关键用户
  • 内容:版本号、部署日期、发起人

6.什么是好的项目

有亮点的项目

对现有普通项目做更进一步的优化

组长建议

1. 提高研发效率

  1. 团队的脚手架
  2. 内部基础组件库 :测试覆盖率90%,稳定

2. 稳定性

  1. CI/CD项目 使用git actions
  2. 研发低代码平台

3. 性能

  • 性能监控系统
  • 提前通过日志预测用户行为
  • 实时监控客户端是否有报错

使用STAR法则来描述自己的项目

7.TS

TS定义

ts是js外再包裹一层,是js的超集

  • 好处
    • 更好的类型提示,开发阶段就可以报错提示
    • 阅读源码更容易: vue3中都定义好类型,vue2全是this 难以理解

常用关键字

interface

限制对象定义

interface Obj {
  name:string,
  age:number,
  score:number
}

let obj:Obj = {
  name : '张三',
  age:18,
  score : ''//报错必须是数字
}

enum 枚举

enum ColorType {
  RED = 1,
  GREEN = 2,
  YELLOW = 3
}
console.log(ColorType.RED == 1)

enum ColorType2 {
  RED,//不赋值默认从0开始
  GREEN,//1
  YELLOW//2
}

enum ColorType3 {
  RED = 'red',//
  GREEN = 'green',//
  YELLOW = 'yellow'//
}

联合 | 

基本类型联合

let b:boolean|string = false 
b = 'dd'
b = true

自定义类型联合

type mytype = 100 | 200 | 300
let f:mytype = 90 // 失败,只能是 100,200,300

函数

type 定义函数

type myfunType2 = ( para1:string,  para2:number) => string
const myfun2:myfunType2 = (para1:string,para2:number):string =>{
  return ''
}

interface定义函数

interface myfunType{
( para1:string,
  para2:number):string
}
const myfun1:myfunType = (para1:string,para2:number):string =>{
  return para1 + para2
}

返回参数使用 :xxxx定义

泛型

基本用法

//传入的类型是string , 则参数要string,返回也必须是string
function mytest<T>(args:T):T {
  return args
}
const bbb:string = mytest<string>('111')

使用

function myfun<某种类型>(args:某种类型):某种类型{
    return args  
}
//使用的使用必须要 泛型一致 
myfun<String>('xxxx')//正确
myfun<String>(100)//错误

属性校验

interface CourseType {
    name:string,
    price:number[], 
    img?:string|boolean, 
}
let myCourse: CourseType = {
    name:'title1111',
    price:[100,233,500], 
    img:'xxx',  //这里只能使用字符串或者布尔值
}

// T 类型 
// K 属性
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]
}

getProperty(myCourse,'name') // ok
getProperty(myCourse,'name1') // 提示没有定义这个属性

vue3的支持

常用ref reactive computed 支持<xx>调用

const a = ref<Number>(99)//传入必须是数字
const b = reactive<String>('xxx')//传入必须是字符串
const c = computed<String>( () => {
    return 'xxx' //返回必须是字符串
})
  1. Props和emits
const props = defineProps<{
  title: string
  value?: number
}>()
const emit = defineEmits<{ 
  (e: 'update', value: number): void
}>()

高阶

keyof

interface Obj2 {
  name:string,
  age:number,
  score?:number
}
type mykeys =  keyof Obj2
//等价于
type mykeys2 = 'name' | 'age' | 'score'
let a1:mykeys = 'name1'//提示错误,必须为name

extends 条件判断

type ExtendsType<T> = T extends number ? 'age': 'name'
type typeAge =  ExtendsType<number>
type typeName =  ExtendsType<string>

let a3:typeName = 'name' //必须是name
let a4:typeAge = 'age' //必须是age

in 实现循环

type mykeys = 'name' | 'age' | 'score'
type MyObj = {
  [key in mykeys]:string
}
//等价于
type MyObj2 = {
  name:string,
  age:string,
  score:string
}

infer更细的条件判断

必须要extends 配合使用

type Foo = () => string
// 如果T是一个函数,并且函数返回类型是P就返回P
type ReturnType1<T> = T extends ()=>infer P ?P:never 
type Foo1 = ReturnType1<Foo>

练习题

  1. 实现类型函数
interface Todo {
  title: string
  desc:string
  done: boolean
}
type partTodo = Partial1<Todo>
type Partial1<T> = {
  [K in keyof T]?:T[K]
}

let aa:Partial1 = {
  title: '1',
  desc:'1',
  done: false,
}
  1. 后端接口约定
import axios from 'axios'
interface Api{
    '/course/buy':{
        id:number
    },
    '/course/comment':{
        id:number,
        message:string
    }
}
function request<T extends keyof Api>(url:T,obj:Api[T]){
    return axios.post(url,obj)
}
request('/course/buy',{id:1})
request('/course/comment',{id:1,message:'xxxx'})
request('/course/comment',{id:1}) //如果message必传 
request('/course/404',{id:1}) //接口不存在 类型怎么需要报错

参考

玩转Vue 3