2 Star 11 Fork 1

ifer / vue3_71

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
README.md 21.97 KB
一键复制 编辑 原始数据 按行查看 历史
ifer 提交于 2022-04-11 16:17 . 会员中心完毕

0. 脑图链接

Vue3 day01: https://www.processon.com/view/link/6246d3ec0e3e74078d609f46

1. 如何介绍一个项目

项目背景、功能、技术栈、我负责的模块(心里面一定要准备碰到的问题以及解决方案)

2. 如何封装组件的

编辑/添加,公告头部的封装,每个页面都会用到的面包屑...考虑过哪些技术点:传值和校验、插槽和作用域插槽、自定义事件

3. 登录怎么做的

收集数据 => 前端进行校验 => 通过 Axios 提交到后端 => 后端校验成功后返回 Token 到前端 => 前端拿到 Token 后做了 2 件事件(存储到 Vuex 并持久化到本地)

有权限的接口会通过请求拦截器给请求头统一携带 Token。

界面访问控制:在路由全局前置导航守卫通过判断 Token 去实现的。

Token:过期...

4. APP

Web APP、可以安装的应用程序(Android、IOS、Android/IOS 提供 Webview + 套(网页))、uni-app 可以把网页打包成可以安装的应用程序

5. Git 工作流

Gitlab 公司内部私有部署的代码管理平台。

master => 打包上线

release => 测试分支

develop => 开发分支(普通开发者具有权限的分支)

功能分支 => 一般我在开发新功能的时候,会基于 develop 开一个自己的功能分支,在上面写代码,写完之后再合并到 develop

6. 这个项目接口请求是怎么做的/你对 axios 封装过哪些东西

首先我们会划分三个模块:utils/request.jsapi/*组件.vue

一般封装 axios 的时候会封装:首先创建一个 axios 实例、baseURL、timeout、请求拦截器(统一携带 Token、Token 过期时候的前端主动介入)、响应拦截器(成功的时候对数据进行脱壳、失败的时候统一错误提示、Token 过期处理)、transformResponse

Token 过期时候的前端主动介入:登录成功存一个时间戳、请求拦截器中,用当前时间戳 - 登录成功时候的那个时间戳,如果大于超时时间了,就直接拦截到登录页。

7. 每次发请求的时候,希望全局开启一个进度条,请求完毕关闭进度条

问题:在那做,做什么?

请求拦截器里面开启进度条,响应拦截器关闭进度条。

8. SEO

对外的,一般都要考虑 SEO,像商城网站(SSR、Nuxt.js)。

对内的,不需要,像后台管理系统。

9. CDN

内容分发网络:公司花钱 => 七牛 CDN => 买空间 => 把自己的资源上传到此空间 => 得到 CDN

10. 和 Vue 相关的性能优化的手段都考虑过哪些

  • v-for 循环的时候加 key

  • v-for 和 v-if 不要放一行(计算属性)

  • v-if 和 v-show 区分使用场景

  • 路由懒加载、组件动态加载

  • KeepAlive

  • computed 和 方法区分使用场景(优先 computed)

  • ...

11. Websocket

轮询或 Websocket

12. 你们公司接口文档是怎么交付的

SwaggerUI:一套接口生成的集成方案,是后端生成接口文档的框架,一般后端会部署一个地址,提供给我。

13. 开发项目的时候,有没有碰到过数据明明变了,但是视图没有变,这种情况?

  • 什么情况?

Vue2 有 2 种情况修改数据不是响应式的:给对象不存在的 key 进行赋值;通过索引修改数组的内容;

const obj = {}
obj.age = 18
const arr = ['a', 'b', 'c']
arr[0] = 'd'
  • 为什么?

给对象不存在的 key 进行赋值不是响应式的为什么?

由于 Vue2 是通过 Object.defineProperty 递归劫持的 data 里面的每一个【属性】,一上来就进行了递归劫持的操作,所以后续添加的属性当然就没有被劫持到!

通过索引修改数组的内容为什么不是响应式的?

性能!issue

  • 怎么解决的?
// const obj = {}
// obj.age = 18
// 解决
this.$set(this.obj, 'age', 19)
Vue.set(this.obj, 'age', 19)
// const arr = ['a', 'b', 'c']
// arr[0] = 'd'
// 解决
this.$set(this.arr, 0, 888)
// 使用数组的变更方法
this.arr.splice(0, 1, 888)
  • Vue3 呢?

Vue3 对数组通过索引修改就是响应式的,因为 Proxy 对数组的处理不存在性能问题!

Vue3 给对象不存在的 key 进行赋值也是响应式的啦,因为 Proxy 它劫持的直接就是整个对象(而不是一个个的属性),所以就无所谓你这个属性是不是后续添加的啦!

14. 项目开发的流程是怎样的

15. 小程序和网页开发的差异

  • 宿主环境不一样(网页=>浏览器,小程序=>微信软件)

  • 开发方式不一样(微信开发者工具)

  • 上线流程不一样

16. 上传图片怎么做的

  1. 准备一个 <input type="file" hidden> 并通过 hidden 属性隐藏

  2. 准备一个按钮,给按钮绑定点击事件,在事件的回调里面主动触发 input:file 的 click 事件

  3. 监听 input 的 onchange 事件,在回调里面通过 e.target.files 拿到文件信息

  4. 创建 formData 对象,把文件信息放进去通过 axios 传到后端

const form = new FormData()
form.append('avatar', e.target.files[0])
  1. 图片预览:URL.createObjectURL(Blob) 或 FileReader(base64)
<input type="file" hidden id="file" />
<img id="img" alt="" />
<button id="btn">click</button>
<script>
  btn.onclick = function () {
    file.click()
  }
  file.onchange = function (e) {
    const f = e.target.files[0]

    const formData = new FormData()
    formData.append('avatar', f)
    // axios({ url: '/xxx', data: formData })
    /* const s = URL.createObjectURL(f)
       img.src = s */
    const fileReader = new FileReader()
    // 图片 => base64
    fileReader.readAsDataURL(f)
    fileReader.onload = function () {
      img.src = this.result
    }
  }
</script>

17. 后端接口还没有开发好,作为前端你是怎么进行下去的

Mock: 模拟数据;拦截请求;

// mock/index.js
Mock.mock('/api/users', 'get', (req, res) => {
  // 通过 req 拿到前端的信息
  // 根据此信息返回对应的数据(Mock 的数据)
  res.send({ mock 的数据 })
})
// main.js
import './mock'

18. 组件传值

父传子

1、父传子的第一种方法

父组件通过自定义属性传递,子组件通过 props 接收?

接收到的数据可以改吗?简单数据类型不能改,复杂数据类型引用不能改内容可以改,但是即便如此,也不建议直接改,为什么?

单项数据流思想:如果任何一个地方都可以修改数据,应用复杂出现错误的时候,意味着这个错误可能是任何一个地方导致的,不方便追溯!

怎么处理比较好?数据在哪来的就在哪改!

2、父传子的第二种方式

父亲通过 ref 获取子组件实例,调用此实例的方法的同时并传递参数,儿子方法中接收到参数做对应的修改!

3、父传子的第三种方式

this.$children[0].changeAge(this.age)

4、父传子的第四种方式

// 非 props 属性
this.$attrs

子传父

1、儿子通过 $emit 触发父亲自定义事件的同时并传递数据,父亲监听自定义事件的同时在回调里面进行数据修改的操作

2、this.$parent 拿到父组件实例的同时,调用方法并传参

3、例如父传子,只不过传递的是一个方法,子组件调用这个方法的同时并传递参数

4、通过 this.$listeners 拿到父亲的自定义事件,调用并传参

兄弟

1、状态(数据)提升

A 修改 B,把 B 中的数据提升到公共的父组件里面,A 通过子传父修改父亲的数据,父亲通过父传子传递把数据传递到 B

2、EventBus(事件中心、发布订阅)

Vue3 中 EventBus 被废弃了,你觉得为什么?

太过于灵活,大型项目不太方便追溯问题;由于 EventBus 本身实现起来足够简单,Vue3 处于自身体积更小的考虑,所以被废弃了!

如果我还想再用怎么办?官方推荐了两个包,例如 mitttiny-emitter 或者自己实现一个

export default class EventBus {
  constructor() {
    this.subs = {}
  }
  // 订阅 => 订阅的是一个回调函数
  on(eventType, callback) {
    this.subs[eventType]
      ? this.subs[eventType].push(callback)
      : (this.subs[eventType] = [callback])
  }
  // 发布 => 发布事件让其(事件)对应的回到函数执行
  emit(eventType, ...args) {
    this.subs[eventType].forEach((callback) => callback(...args))
  }
}

全局的数据通信和共享(Vuex)

回答 Vuex 分两方面:配置项;触发流程;

mutation 里面可以放异步吗?

非严格模式确实是可以放异步,代码也可以正常执行!严格模式下不能这样操作(写异步),会有警告。

不建议放异步代码,目的是为了形成数据快照(拿到当时的那个数据状态),为了配合 DevTools 调试。

19. 说一下权限管理怎么做的

路由级别的权限

用户登录成功之后,后端返回当前用户的标识;['/user', '/news']

前端拿到标识之后筛选出【有权限的路由】;[{path: '/user', component: User}, {path: '/news', component: News}, ],#1

然后做了 2 件事情!

第 1 件事情:通过 addRoutes/addRoute 把【有权限的路由】添加到路由实例;router.addRoutes(#1),这样当前用户就具有了访问某个路由的权限啦;

第 2 件事情:把【有权限的路由】也添加到了 Vuex 一份,目的是为了给侧边栏或其他地方使用(通过 addRoutes 后续添加的路由,不能直接通过 router 获取到,官方推荐放到 Vuex 存储);

按钮级别的权限

封装一个全局的方法/指令,这个方法呢?只做一件事件,接收一个标识,内部进行判断,看一下这个标识这不在后端返回的功能列表里面,在就返回 true,不再就返回 false。

function isFn(flag) {
  return ['a', 'b', 'c', 'DELETE'].includes(flag)
}

在需要做按钮权限控制的时候,调用这个方法,根据返回的是 true 还是 false,对这个按钮做禁用/启用或显示/隐藏的操作。

<button v-if="isFn('DELETE')">删除</button>

20. 继承

// #1 Call 式继承 / 构造函数继承:继承的是属性
Person.call(this, name, age)
// #2 原型继承:继承的是方法
Star.prototype = new Person()
Star.prototype.constructor = Star
// 组合继承 = 构造函数继承 + 原型继承

不过,现在呢,我更喜欢使用 ES6 的 extends...

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  say() {
    console.log('!!!')
  }
}
class Star extends Person {}
const s = new Star('尼古拉斯', 40)
console.log(s.name, s.age)
s.say()

extends

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  say() {
    console.log('!!!')
  }
}
class Star extends Person {
  // 儿子有 constructor 一定要有 super,有 constructor 的目的是为了添加 Star 自己的属性
  constructor(name, age, money) {
    super(name, age)
    this.money = money
  }
}
const s = new Star('尼古拉斯', 40, 1000)
console.log(s.name, s.age, s.money)
s.say()

深拷贝

  1. JSON.stringify 的问题
// ..........JSON.stringify 的确点............

// 1. 会忽略值为 undefined、函数、Symbol
// 2. 当值为 bigint 或循环引用的时候会报错
// 3. 日期会被转成字符串、正则会被转成空对象
// 4. ...
  1. 三个点和 Object.assign 都是浅拷贝
它们都是用来进行浅拷贝的好的地方是能拷贝 undefined函数Symbol正则...
  1. 自己实现深拷贝:递归浅拷贝
// const copy = (target) => {
//   const type = Object.prototype.toString.call(target)
//   // 正则、日期
//   if (/(regexp|date)/i.test(type)) return new target.constructor(target)
//   // 错误对象
//   if (/error/i.test(type)) return new target.constructor(target.message)
//   // 函数
//   if (/function/i.test(type)) return new Function('return ' + target.toString())()
//   // null 和 简单数据类型
//   if (target === null || typeof target !== 'object') return target
//   // 数组和对象
//   /* const arr = []
//   const obj = {} */
//   const result = new target.constructor()
//   for (const attr in target) {
//     result[attr] = copy(target[attr])
//   }
//   return result
// }
  1. 如何循环引用
// const copy = (target, m = new Map()) => {
//   const type = Object.prototype.toString.call(target)
//   // 正则、日期
//   if (/(regexp|date)/i.test(type)) return new target.constructor(target)
//   // 错误对象
//   if (/error/i.test(type)) return new target.constructor(target.message)
//   // 函数
//   if (/function/i.test(type)) return new Function('return ' + target.toString())()
//   // null 和 简单数据类型
//   if (target === null || typeof target !== 'object') return target
//   // 数组和对象
//   /* const arr = []
//   const obj = {} */
//   // #2 m 里面存储了 target 就直接返回
//   // console.log(target, 233)

//   if (m.get(target)) return m.get(target)
//   const result = new target.constructor()
//   // #3
//   m.set(target, result)

//   for (const attr in target) {
//     // #4 传递 m
//     result[attr] = copy(target[attr], m)
//   }
//   return result
// };
  1. 实际我怎么做的
const o = _.cloneDeep(obj1)

防抖和节流

// 是什么?
// 防抖和节流都是性能优化的一种手段。
// 防抖:持续触发不执行,不触发的一段时间后才执行。
// 节流:持续触发也执行,只不过,执行的频率变低了。
// 实现一个?
// 匈牙利命名法的简版
// 类型 + 具体的含义
// const iNum = 8
// const bBar = false
// const aDiv = document.querySelctorAll('div')
// 把鼠标相对于盒子位置放到盒子里面
/*  oDiv.onmousemove = function (e) {
       let x = e.pageX - this.offsetLeft
       let t = e.pageY - this.offsetTop
       this.innerHTML = `x: ${x}, y: ${t}`
     }; */

// 防抖一下
/* let timer = null
    oDiv.onmousemove = function (e) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        let x = e.pageX - this.offsetLeft
        let t = e.pageY - this.offsetTop
        this.innerHTML = `x: ${x}, y: ${t}`
      }, 200)
    }; */
// 封装一个防抖/节流函数?
/* const debounce = (callback, time) => {
      let timer = null
      return function (e) {
        clearTimeout(timer)
        timer = setTimeout(() => {
          callback.call(this, e) // window.callback(e)
        }, time)
      }
    } */

/* oDiv.onmousemove = _.debounce(function (e) {
      let x = e.pageX - this.offsetLeft
      let t = e.pageY - this.offsetTop
      this.innerHTML = `x: ${x}, y: ${t}`
    }, 200) */
// 生活中的例子?
// 王者荣耀英雄回城是防抖还是节流
// 打散弹枪,节流

// 应用场景?
// 根据输入的内容请求接口?防抖
// 获取窗口缩放的大小,滚动位置?节流

// 实际开发怎么做?
oDiv.onmousemove = _.throttle(function (e) {
  let x = e.pageX - this.offsetLeft
  let t = e.pageY - this.offsetTop
  this.innerHTML = `x: ${x}, y: ${t}`
}, 1000)

原型链

定义:多个对象之间通过 __proto__ 链接起来的这种关系就是原型链。

Promise

.catch() 之后还能再触发 then 吗?

const p = new Promise((resolve, reject) => {
  reject(new Error('233'))
})
p.catch((r) => {
  console.log(r)
  // return Promise.resolve(undefined)
}).then((r) => {
  console.log(r)
})

如何实现并发请求,按顺序拿到结果?

Promise.all([p1, p2]).then((r) => {
  // r => [p1 的结果, p2 的结果]
})

// const p1 的结果 = await p1
// const p2 的结果 = await p2

Vue2 双向数据绑定原理

// 需求:双向数据绑定(数据变了,视图改变;视图改变,数据也变)

const data = {
  name: 'ifer'
}

/* const tempData = { ...data }
    Object.defineProperty(data, 'name', {
      get() {
        // 获取 name 的时候会走这儿
        return tempData.name
      },
      set(newValue) {
        // 设置 name 的时候会走这儿
        tempData.name = newValue
      }
    })

    // console.log(data.name) // 'ifer'
    data.name = 'elser'
    console.log(data.name) */

/* const tempData = { ...data }
    Object.defineProperty(data, 'name', {
      get() {
        // 获取 name 的时候会走这儿
        return tempData.name
      },
      set(newValue) {
        // model => view => Data bindings
        // #2 设置 name 的时候会走这儿
        tempData.name = newValue
        oP.innerHTML = newValue
        oInput.value = newValue
      }
    })

    // view => model => DOM Listeners
    oInput.oninput = function (e) {
      // #1
      data.name = e.target.value
    } */

/* oInput.oninput = function (e) {
      oP.innerHTML = e.target.value
    } */

Vue3

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <p id="oP"></p>
    <input id="oInput" type="text" />
    <script>
      // 需求:双向数据绑定(数据变了,视图改变;视图改变,数据也变)

      const data = {
        name: 'ifer',
        address: '北京'
      }

      /* const tempData = { ...data }
    Object.keys(data).forEach(key => {
      Object.defineProperty(data, key, {
        get() {
          return tempData[key]
        },
        set(newValue) {
          tempData[key] = newValue
          oP.innerHTML = newValue
          oInput.value = newValue
        }
      })
    })
    oInput.oninput = function (e) {
      data.address = e.target.value
    } */

      // 上面 Vue2 的问题:如果 data 里面有 100 个属性,那可不是要循环 100 次吗?

      // p 就是一个代理,我对 p 的任何操作都会影响到 data
      const p = new Proxy(data, {
        get(target, attr) {
          // 获取的时候触发
          // console.log(target, attr)
          // target => data
          // attr => 属性
          return target[attr]
        },
        set(target, attr, newValue) {
          target[attr] = newValue
          oP.innerHTML = newValue
          oInput.value = newValue
        }
      })
      // p.name = 'xxx'
      // console.log(data.name);

      /* p.name = 'xxx'
    console.log(p.name) */

      oInput.oninput = function (e) {
        // p.address = e.target.value
        p.test = e.target.value
      }
    </script>
  </body>
</html>

插件

Vue.use({
  install(Vue) {
    // 干了哪些事情
    Vue.mixin()
    Vue.component()
    Vue.directive()
    Vue.prototype.bus = new Vue()
  }
})
Vue.use(function (Vue) {})

难点

  1. 路由的缓存问题。

点击 用户 1 按钮 和 用户 2 按钮 共用了同一个路由组件 User,我在 User 里面根据 userId 发请求的时候,发现永远是第一次发起的那个请求,好奇怪,当时找不到原因。

template 里面获取数据是实时更新的,但是 created 拿不到最新的数据,后来发现原因是公用了同一个组件,而 created 这个钩子也只会在组件创建完毕执行一次,所以 ...

解决方式 1

<template>
  <div>User {{ $route.params.id }}</div>
</template>

<script>
  export default {
    name: 'User',
    created() {
      console.log(this.$route.params.id)
    },
    watch: {
      $route(newRoute) {
        console.log(newRoute.params.id)
      }
    }
  }
</script>

解决方式 2

<router-view :key="$route.fullPath"></router-view>
  1. 大数字问题。

根据文章 ID 请求文章详情的时候,有的时候正常,有的时候会出现 404,找不到详情,反复检查了传参和接口都没有问题,百思不得其解...

原因,后端给我返回的文章列表,当文章 ID 包含大数字的时候出事了。

{"name":"xxx", "age": 18}

本质上来说,后端返回的数据都是 JSON 格式的字符串,那么前端使用为什么可以直接当做对象去用呢?

就是因为 axios 帮我们进行了内部处理,它为了方便我们前端使用,内部进行了 JSON.parse 的操作转成了对象。

但是!!!当 JSON 格式的字符串里面包含了大数字的时候,JSON.parse 就搞不定了(转换出来的结果不对)

所以也就是当后端返回的 JSON 格式的字符串里面包含大数字的时候,axios 进行内部 JSON.parse 的时候,把那个大数字(文章 ID)转换成了另外一个结果,所以 404...

JSON.parse('{ "ID": 9007199254740999 }')

怎么解决?2 种方法。

找后端,那个 ID 不要用数字表示,用字符串表示文章 ID。

JSON.parse('{ "ID": "90071992547409998888888888888888" }')

自己搞定

import jsonBigInt from 'json-bigint'
// transformResponse
axios.create({
  transformResponse(data) {
    data => JSON 格式的字符串
    // 默认
    // return JSON.parse(data)
    // 内部用一种算法,如果是大数字转出来的是一个对象,这个对象里面的信息经过运算可以得到大数字
    return jsonBigInt.parse(data)
  }
})
1
https://gitee.com/ifercarly/v3_71.git
git@gitee.com:ifercarly/v3_71.git
ifercarly
v3_71
vue3_71
master

搜索帮助