_vue-3

news2024/11/14 21:38:11

Vue3有了解过吗?能说说跟vue2的区别吗?

1. 哪些变化

从上图中,我们可以概览Vue3的新特性,如下:

  • 速度更快
  • 体积减少
  • 更易维护
  • 更接近原生
  • 更易使用

1.1 速度更快

vue3相比vue2

  • 重写了虚拟Dom实现
  • 编译模板的优化
  • 更高效的组件初始化
  • undate性能提高1.3~2倍
  • SSR速度提高了2~3倍

1.2 体积更小

通过webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的

能够tree-shaking,有两大好处:

  • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大
  • 对使用者,打包出来的包体积变小了

vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多

1.3 更易维护

compositon Api

  • 可与现有的Options API一起使用
  • 灵活的逻辑组合与复用
  • Vue3模块可以和其他框架搭配使用

更好的Typescript支持

VUE3是基于typescipt编写的,可以享受到自动的类型定义提示

1.4 编译器重写

1.5 更接近原生

可以自定义渲染 API

1.6 更易使用

响应式 Api 暴露出来

轻松识别组件重新渲染原因

2. Vue3新增特性

Vue 3 中需要关注的一些新功能包括:

  • framents
  • Teleport
  • composition Api
  • createRenderer

2.1 framents

Vue3.x 中,组件现在支持有多个根节点

<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

2.2 Teleport

Teleport 是一种能够将我们的模板移动到 DOMVue app 之外的其他位置的技术,就有点像哆啦A梦的“任意门”

vue2中,像 modals,toast 等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难

通过Teleport,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它

<button @click="showToast" class="btn">打开 toast</button>
<!-- to 属性就是目标位置 -->
<teleport to="#teleport-target">
    <div v-if="visible" class="toast-wrap">
        <div class="toast-msg">我是一个 Toast 文案</div>
    </div>
</teleport>

2.3 createRenderer

通过createRenderer,我们能够构建自定义渲染器,我们能够将 vue 的开发模型扩展到其他平台

我们可以将其生成在canvas画布上

关于createRenderer,我们了解下基本使用,就不展开讲述了

import { createRenderer } from '@vue/runtime-core'

const { render, createApp } = createRenderer({
  patchProp,
  insert,
  remove,
  createElement,
  // ...
})

export { render, createApp }

export * from '@vue/runtime-core'

2.4 composition Api

composition Api,也就是组合式api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理

关于compositon api的使用,这里以下图展开

简单使用:

export default {
    setup() {
        const count = ref(0)
        const double = computed(() => count.value * 2)
        function increment() {
            count.value++
        }
        onMounted(() => console.log('component mounted!'))
        return {
            count,
            double,
            increment
        }
    }
}

3. 非兼容变更

3.1 Global API

  • 全局 Vue API 已更改为使用应用程序实例
  • 全局和内部 API 已经被重构为可 tree-shakable

3.2 模板指令

  • 组件上 v-model 用法已更改
  • <template v-for>和 非 v-for节点上key用法已更改
  • 在同一元素上使用的 v-ifv-for 优先级已更改
  • v-bind="object" 现在排序敏感
  • v-for 中的 ref 不再注册 ref 数组

3.3 组件

  • 只能使用普通函数创建功能组件
  • functional 属性在单文件组件 (SFC)
  • 异步组件现在需要 defineAsyncComponent 方法来创建

3.4 渲染函数

  • 渲染函数API改变
  • $scopedSlots property 已删除,所有插槽都通过 $slots 作为函数暴露
  • 自定义指令 API 已更改为与组件生命周期一致
  • 一些转换class被重命名了:
    • v-enter -> v-enter-from
    • v-leave -> v-leave-from
  • 组件 watch 选项和实例方法 $watch不再支持点分隔字符串路径,请改用计算函数作为参数
  • Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器的 innerHTML

3.5 其他小改变

  • destroyed 生命周期选项被重命名为 unmounted
  • beforeDestroy 生命周期选项被重命名为 beforeUnmount
  • [prop default工厂函数不再有权访问 this 是上下文
  • 自定义指令 API 已更改为与组件生命周期一致
  • data 应始终声明为函数
  • 来自 mixindata 选项现在可简单地合并
  • attribute 强制策略已更改
  • 一些过渡 class 被重命名
  • 组建 watch 选项和实例方法 $watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。
  • <template> 没有特殊指令的标记 (v-if/else-if/elsev-forv-slot) 现在被视为普通元素,并将生成原生的 <template> 元素,而不是渲染其内部内容。
  • Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML,这意味着容器本身不再被视为模板的一部分。

3.6 移除 API

  • keyCode 支持作为 v-on 的修饰符
  • $on$off$once 实例方法
  • 过滤filter
  • 内联模板 attribute
  • $destroy 实例方法。用户不应再手动管理单个Vue 组件的生命周期。

----------@----------

你知道哪些Vue3新特性?

官网列举的最值得注意的新特性:v3-migration.vuejs.org(opens new window)

  • Composition API
  • SFC Composition API语法糖
  • Teleport传送门
  • Fragments片段
  • Emits选项
  • 自定义渲染器
  • SFC CSS变量
  • Suspense

以上这些是api相关,另外还有很多框架特性也不能落掉

回答范例

  1. api层面Vue3新特性主要包括:Composition APISFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense
  2. 另外,Vue3.0在框架层面也有很多亮眼的改进:
  • 更快
    • 虚拟DOM重写,diff算法优化
    • 编译器优化:静态提升、patchFlags(静态标记)、事件监听缓存
    • 基于Proxy的响应式系统
    • SSR优化
  • 更小 :更好的摇树优化 tree shakingVue3移除一些不常用的 API
  • 更友好vue3在兼顾vue2options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力
  • 更容易维护TypeScript + 模块化
  • 更容易扩展
    • 独立的响应化模块
    • 自定义渲染器

----------@----------

Vue3速度快的原因

Vue3.0 性能提升体现在哪些方面

  • 代码层面性能优化主要体现在全新响应式API,基于Proxy实现,初始化时间和内存占用均大幅改进;
  • 编译层面做了更多编译优化处理,比如静态标记pachFlagdiff算法增加了一个静态标记,只对比有标记的dom元素)、事件增加缓存静态提升(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff过程;
  • 打包时更好的支持tree-shaking,因此整体体积更小,加载更快
  • ssr渲染以字符串方式渲染

一、编译阶段

试想一下,一个组件结构如下图

<template>
    <div id="content">
        <p class="text">静态文本</p>
        <p class="text">静态文本</p>
        <p class="text">{ message }</p>
        <p class="text">静态文本</p>
        ...
        <p class="text">静态文本</p>
    </div>
</template>

可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费

因此,Vue3在编译阶段,做了进一步优化。主要有如下:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

1. diff 算法优化

  • Vue 2x 中的虚拟 dom 是进行全量的对比。
  • Vue 3x 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容化

Vue2.x的diff算法

vue2.xdiff算法叫做全量比较,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom对比,即使有些内容是永恒固定不变的

Vue3.0的diff算法

vue3.0diff算法有个叫静态标记(PatchFlag)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了

已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高

export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("p", null, "'HelloWorld'"),
  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
                        //上面这个1就是静态标记
 ]))
}

关于静态类型枚举如下

TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
// 指示在diff算法中退出优化模式
BALL = -2

2. hoistStatic 静态提升

  • Vue 2x : 无论元素是否参与更新,每次都会重新创建。
  • Vue 3x : 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用
<p>HelloWorld</p>
<p>HelloWorld</p>

<p>{ message }</p>

开启静态提升前

export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("p", null, "'HelloWorld'"),
  _createVNode("p", null, "'HelloWorld'"),
  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
 ]))
}

开启静态提升后编译结果

const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _hoisted_1,
  _hoisted_2,
  _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
 ]))
}

可以看到开启了静态提升后,直接将那两个内容为helloworldp标签声明在外面了,直接就拿来用了。同时 _hoisted_1_hoisted_2 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff

3. cacheHandlers 事件监听缓存

  • 默认情况下 绑定事件会被视为动态绑定 ,所以每次都会去追踪它的变化
  • 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
<div>
 <button @click = 'onClick'>点我</button>
</div>

开启事件侦听器缓存之前:

export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
                       // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
 ]))
})

这里有一个8,表示着这个节点有了静态标记,有静态标记就会进行diff算法对比差异,所以会浪费时间

开启事件侦听器缓存之后:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
 return (_openBlock(), _createBlock("div", null, [
  _createVNode("button", {
   onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
  }, "点我")
 ]))
}

上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用

4. SSR优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

<div>
    <div>
        <span>你好</span>
    </div>
    ...  // 很多个静态属性
    <div>
        <span>{{ message }}</span>
    </div>
</div>

编译后

import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  }><div><span>你好</span>...<div><span>你好</span><div><span>${
    _ssrInterpolate(_ctx.message)
  }</span></div></div>`)
}

二、源码体积

相比Vue2Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking

任何一个函数,如refreactivecomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
  setup(props, context) {
    const age = ref(18)

    let state = reactive({
      name: 'test'
    })

    const readOnlyAge = computed(() => age.value++) // 19

    return {
        age,
        state,
        readOnlyAge
    }
  }
});

三、响应式系统

vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式

vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历

  • 可以监听动态属性的添加
  • 可以监听到数组的索引和数组length属性
  • 可以监听删除属性

----------@----------

Composition API 与 Options API 有什么不同

分析

Vue3最重要更新之一就是Composition API,它具有一些列优点,其中不少是针对Options API暴露的一些问题量身打造。是Vue3推荐的写法,因此掌握好Composition API应用对掌握好Vue3至关重要

What is Composition API?(opens new window)

  • Composition API出现就是为了解决Options API导致相同功能代码分散的现象

体验

Composition API能更好的组织代码,下面用composition api可以提取为useCount(),用于组合、复用

compositon api提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的hooks

回答范例

  1. Composition API是一组API,包括:Reactivity API生命周期钩子依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件
  2. Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options APImixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对ts支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixinsprovide/inject
  3. Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益

可能的追问

  1. Composition API能否和Options API一起使用?

可以在同一个组件中使用两个script标签,一个使用vue3,一个使用vue2写法,一起使用没有问题

<!-- vue3 -->
<script setup>
  // vue3写法
</script>

<!-- 降级vue2 -->
<script>
  export default {
    data() {},
    methods: {}
  }
</script>

----------@----------

ref和reactive异同

这是Vue3数据响应式中非常重要的两个概念,跟我们写代码关系也很大

const count = ref(0)
console.log(count.value) // 0
​
count.value++
console.log(count.value) // 1

const obj = reactive({ count: 0 })
obj.count++
  • ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象
  • 从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式
  • 两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题
  • ref返回的响应式数据在JS中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.valueref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。
  • reactive内部使用Proxy代理传入对象并拦截该对象各种操作,从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式

----------@----------

Vue3.2 setup 语法糖汇总

提示:vue3.2 版本开始才能使用语法糖!

Vue3.0 中变量必须 return 出来, template 中才能使用;而在 Vue3.2 中只需要在 script 标签上加上 setup 属性,无需 returntemplate 便可直接使用,非常的香啊!

1. 如何使用setup语法糖

只需在 script 标签上写上 setup

<template>
</template>
<script setup>
</script>
<style scoped lang="less">
</style>

2. data数据的使用

由于 setup 不需写 return ,所以直接声明数据即可

<script setup>
import {
  ref,
  reactive,
  toRefs,
} from 'vue'

const data = reactive({
  patternVisible: false,
  debugVisible: false,
  aboutExeVisible: false,
})

const content = ref('content')
//使用toRefs解构
const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)
</script>

3. method方法的使用

<template >
  <button @click="onClickHelp">帮助</button>
</template>
<script setup>
import {reactive} from 'vue'

const data = reactive({
  aboutExeVisible: false,
})
// 点击帮助
const onClickHelp = () => {
  console.log(`帮助`)
  data.aboutExeVisible = true
}
</script>

4. watchEffect的使用

<script setup>
import {
  ref,
  watchEffect,
} from 'vue'

let sum = ref(0)

watchEffect(()=>{
  const x1 = sum.value
  console.log('watchEffect所指定的回调执行了')
})
</script>

5. watch的使用

<script setup>
import {
  reactive,
  watch,
} from 'vue'
//数据
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
  name:'张三',
  age:18,
  job:{
    j1:{
      salary:20
    }
  }
})
// 两种监听格式
watch([sum,msg],(newValue,oldValue)=>{
    console.log('sum或msg变了',newValue,oldValue)
  },
  {immediate:true}
)

watch(()=>person.job,(newValue,oldValue)=>{
  console.log('person的job变化了',newValue,oldValue)
},{deep:true}) 

</script>

6. computed计算属性的使用

computed 计算属性有两种写法(简写和考虑读写的完整写法)

<script setup>
import {
  reactive,
  computed,
} from 'vue'

// 数据
let person = reactive({
  firstName:'poetry',
  lastName:'x'
})

// 计算属性简写
person.fullName = computed(()=>{
  return person.firstName + '-' + person.lastName
})

// 完整写法
person.fullName = computed({
  get(){
    return person.firstName + '-' + person.lastName
  },
  set(value){
    const nameArr = value.split('-')
    person.firstName = nameArr[0]
    person.lastName = nameArr[1]
  }
})
</script>

7. props父子传值的使用

父组件代码如下(示例):

<template>
  <child :name='name'/>  
</template>

<script setup>
  import {ref} from 'vue'
  // 引入子组件
  import child from './child.vue'
  let name= ref('poetry')
</script>

子组件代码如下(示例):

<template>
  <span>{{props.name}}</span>
</template>

<script setup>
import { defineProps } from 'vue'
// 声明props
const props = defineProps({
  name: {
    type: String,
    default: 'poetries'
  }
})  
// 或者
//const props = defineProps(['name'])
</script>

8. emit子父传值的使用

父组件代码如下(示例):

<template>
  <AdoutExe @aboutExeVisible="aboutExeHandleCancel" />
</template>
<script setup>
import { reactive } from 'vue'
// 导入子组件
import AdoutExe from '../components/AdoutExeCom'

const data = reactive({
  aboutExeVisible: false, 
})
// content组件ref

// 关于系统隐藏
const aboutExeHandleCancel = () => {
  data.aboutExeVisible = false
}
</script>

子组件代码如下(示例):

<template>
  <a-button @click="isOk">
    确定
  </a-button>
</template>
<script setup>
import { defineEmits } from 'vue';

// emit
const emit = defineEmits(['aboutExeVisible'])
/**
 * 方法
 */
// 点击确定按钮
const isOk = () => {
  emit('aboutExeVisible');
}
</script>

9. 获取子组件ref变量和defineExpose暴露

vue2中的获取子组件的ref,直接在父组件中控制子组件方法和变量的方法

父组件代码如下(示例):

<template>
  <button @click="onClickSetUp">点击</button>
  <Content ref="content" />
</template>

<script setup>
import {ref} from 'vue'

// content组件ref
const content = ref('content')
// 点击设置
const onClickSetUp = ({ key }) => {
  content.value.modelVisible = true
}
</script>
<style scoped lang="less">
</style>

子组件代码如下(示例):

<template>
   <p>{{data }}</p>
</template>

<script setup>
import {
  reactive,
  toRefs
} from 'vue'

/**
 * 数据部分
* */
const data = reactive({
  modelVisible: false,
  historyVisible: false, 
  reportVisible: false, 
})

defineExpose({
  ...toRefs(data),
})
</script>

10. 路由useRoute和useRouter的使用

<script setup>
  import { useRoute, useRouter } from 'vue-router'

  // 声明
  const route = useRoute()
  const router = useRouter()

  // 获取query
  console.log(route.query)
  // 获取params
  console.log(route.params)

  // 路由跳转
  router.push({
    path: `/index`
  })
</script>

11. store仓库的使用

<script setup>
  import { useStore } from 'vuex'
  import { num } from '../store/index'

  const store = useStore(num)

  // 获取Vuex的state
  console.log(store.state.number)
  // 获取Vuex的getters
  console.log(store.state.getNumber)

  // 提交mutations
  store.commit('fnName')

  // 分发actions的方法
  store.dispatch('fnName')
</script>

12. await的支持

setup语法糖中可直接使用await,不需要写asyncsetup会自动变成async setup

<script setup>
  import api from '../api/Api'
  const data = await Api.getData()
  console.log(data)
</script>

13. provide 和 inject 祖孙传值

父组件代码如下(示例):

<template>
  <AdoutExe />
</template>

<script setup>
  import { ref,provide } from 'vue'
  import AdoutExe from '@/components/AdoutExeCom'

  let name = ref('py')
  // 使用provide
  provide('provideState', {
    name,
    changeName: () => {
      name.value = 'poetries'
    }
  })
</script>

子组件代码如下(示例):

<script setup>
  import { inject } from 'vue'
  const provideState = inject('provideState')

  provideState.changeName()
</script>

----------@----------

说说Vue 3.0中Treeshaking特性?举例说明一下?

一、是什么

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去

treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕

也就是说 ,tree shaking 其实是找出使用的代码

Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

import Vue from 'vue'

Vue.nextTick(() => {})

Vue3源码引入tree shaking特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中

import { nextTick, observable } from 'vue'

nextTick(() => {})

二、如何做

Tree shaking是基于ES6模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量

Tree shaking无非就是做了两件事:

  • 编译阶段利用ES6 Module判断哪些模块已经加载
  • 判断那些模块和变量未被使用或者引用,进而删除对应代码

下面就来举个例子:

通过脚手架vue-cli安装Vue2Vue3项目

vue create vue-demo

Vue2 项目

组件中使用data属性

<script>
    export default {
        data: () => ({
            count: 1,
        }),
    };
</script>

对项目进行打包,体积如下图

为组件设置其他属性(comptedwatch

export default {
    data: () => ({
        question:"", 
        count: 1,
    }),
    computed: {
        double: function () {
            return this.count * 2;
        },
    },
    watch: {
        question: function (newQuestion, oldQuestion) {
            this.answer = 'xxxx'
        }
};

再一次打包,发现打包出来的体积并没有变化

Vue3 项目

组件中简单使用

import { reactive, defineComponent } from "vue";
export default defineComponent({
  setup() {
    const state = reactive({
      count: 1,
    });
    return {
      state,
    };
  },
});

将项目进行打包

在组件中引入computedwatch

import { reactive, defineComponent, computed, watch } from "vue";
export default defineComponent({
  setup() {
    const state = reactive({
      count: 1,
    });
    const double = computed(() => {
      return state.count * 2;
    });

    watch(
      () => state.count,
      (count, preCount) => {
        console.log(count);
        console.log(preCount);
      }
    );
    return {
      state,
      double,
    };
  },
});

再次对项目进行打包,可以看到在引入computerwatch之后,项目整体体积变大了

三、作用

通过Tree shakingVue3给我们带来的好处是:

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

----------@----------

用Vue3.0 写过组件吗?如果想实现一个 Modal你会怎么设计?

一、组件设计

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式

现在有一个场景,点击新增与编辑都弹框出来进行填写,功能上大同小异,可能只是标题内容或者是显示的主体内容稍微不同

这时候就没必要写两个组件,只需要根据传入的参数不同,组件显示不同内容即可

这样,下次开发相同界面程序时就可以写更少的代码,意义着更高的开发效率,更少的 Bug和更少的程序体积

二、需求分析

实现一个Modal组件,首先确定需要完成的内容:

  • 遮罩层
  • 标题内容
  • 主体内容
  • 确定和取消按钮

主体内容需要灵活,所以可以是字符串,也可以是一段 html 代码

特点是它们在当前vue实例之外独立存在,通常挂载于body之上

除了通过引入import的形式,我们还可通过API的形式进行组件的调用

还可以包括配置全局样式、国际化、与typeScript结合

三、实现流程

首先看看大致流程:

  • 目录结构
  • 组件内容
  • 实现 API 形式
  • 事件处理
  • 其他完善

目录结构

Modal组件相关的目录结构

├── plugins
│   └── modal
│       ├── Content.tsx // 维护 Modal 的内容,用于 h 函数和 jsx 语法
│       ├── Modal.vue // 基础组件
│       ├── config.ts // 全局默认配置
│       ├── index.ts // 入口
│       ├── locale // 国际化相关
│       │   ├── index.ts
│       │   └── lang
│       │       ├── en-US.ts
│       │       ├── zh-CN.ts
│       │       └── zh-TW.ts
│       └── modal.type.ts // ts类型声明相关

因为 Modal 会被 app.use(Modal) 调用作为一个插件,所以都放在plugins目录下

组件内容

首先实现modal.vue的主体显示内容大致如下

<Teleport to="body" :disabled="!isTeleport">
    <div v-if="modelValue" class="modal">
        <div
             class="mask"
             :style="style"
             @click="maskClose && !loading && handleCancel()"
             ></div>
        <div class="modal__main">
            <div class="modal__title line line--b">
                <span>{{ title || t("r.title") }}</span>
                <span
                      v-if="close"
                      :title="t('r.close')"
                      class="close"
                      @click="!loading && handleCancel()"
                      ></span
                    >
            </div>
            <div class="modal__content">
                <Content v-if="typeof content === 'function'" :render="content" />
                <slot v-else>
                    {{ content }}
                </slot>
            </div>
            <div class="modal__btns line line--t">
                <button :disabled="loading" @click="handleConfirm">
                    <span class="loading" v-if="loading"></span>{{ t("r.confirm") }}
                </button>
                <button @click="!loading && handleCancel()">
                    {{ t("r.cancel") }}
                </button>
            </div>
        </div>
    </div>
</Teleport>

最外层上通过Vue3 Teleport 内置组件进行包裹,其相当于传送门,将里面的内容传送至body之上

并且从DOM结构上来看,把modal该有的内容(遮罩层、标题、内容、底部按钮)都实现了

关于主体内容

<div class="modal__content">
    <Content v-if="typeof content==='function'"
             :render="content" />
    <slot v-else>
        {{content}}
    </slot>
</div>

可以看到根据传入content的类型不同,对应显示不同得到内容

最常见的则是通过调用字符串和默认插槽的形式

// 默认插槽
<Modal v-model="show"
       title="演示 slot">
    <div>hello world~</div>
</Modal>

// 字符串
<Modal v-model="show"
       title="演示 content"
       content="hello world~" />

通过 API 形式调用Modal组件的时候,content可以使用下面两种

  • h 函数
$modal.show({
  title: '演示 h 函数',
  content(h) {
    return h(
      'div',
      {
        style: 'color:red;',
        onClick: ($event: Event) => console.log('clicked', $event.target)
      },
      'hello world ~'
    );
  }
});
  • JSX
$modal.show({
  title: '演示 jsx 语法',
  content() {
    return (
      <div
        onClick={($event: Event) => console.log('clicked', $event.target)}
      >
        hello world ~
      </div>
    );
  }
});

实现 API 形式

那么组件如何实现API形式调用Modal组件呢?

Vue2中,我们可以借助Vue实例以及Vue.extend的方式获得组件实例,然后挂载到body

import Modal from './Modal.vue';
const ComponentClass = Vue.extend(Modal);
const instance = new ComponentClass({ el: document.createElement("div") });
document.body.appendChild(instance.$el);

虽然Vue3移除了Vue.extend方法,但可以通过createVNode实现

import Modal from './Modal.vue';
const container = document.createElement('div');
const vnode = createVNode(Modal);
render(vnode, container);
const instance = vnode.component;
document.body.appendChild(container);

Vue2中,可以通过this的形式调用全局 API

export default {
    install(vue) {
       vue.prototype.$create = create
    }
}

而在 Vue3 的 setup 中已经没有 this概念了,需要调用app.config.globalProperties挂载到全局

export default {
    install(app) {
        app.config.globalProperties.$create = create
    }
}

事件处理

下面再看看看Modal组件内部是如何处理「确定」「取消」事件的,既然是Vue3,当然采用Compositon API 形式

// Modal.vue
setup(props, ctx) {
  let instance = getCurrentInstance(); // 获得当前组件实例
  onBeforeMount(() => {
    instance._hub = {
      'on-cancel': () => {},
      'on-confirm': () => {}
    };
  });

  const handleConfirm = () => {
    ctx.emit('on-confirm');
    instance._hub['on-confirm']();
  };
  const handleCancel = () => {
    ctx.emit('on-cancel');
    ctx.emit('update:modelValue', false);
    instance._hub['on-cancel']();
  };

  return {
    handleConfirm,
    handleCancel
  };
}

在上面代码中,可以看得到除了使用传统emit的形式使父组件监听,还可通过_hub属性中添加 on-cancelon-confirm方法实现在API中进行监听

app.config.globalProperties.$modal = {
   show({}) {
     /* 监听 确定、取消 事件 */
   }
}

下面再来目睹下_hub是如何实现

// index.ts
app.config.globalProperties.$modal = {
    show({
        /* 其他选项 */
        onConfirm,
        onCancel
    }) {
        /* ... */

        const { props, _hub } = instance;

        const _closeModal = () => {
            props.modelValue = false;
            container.parentNode!.removeChild(container);
        };
        // 往 _hub 新增事件的具体实现
        Object.assign(_hub, {
            async 'on-confirm'() {
            if (onConfirm) {
                const fn = onConfirm();
                // 当方法返回为 Promise
                if (fn && fn.then) {
                    try {
                        props.loading = true;
                        await fn;
                        props.loading = false;
                        _closeModal();
                    } catch (err) {
                        // 发生错误时,不关闭弹框
                        console.error(err);
                        props.loading = false;
                    }
                } else {
                    _closeModal();
                }
            } else {
                _closeModal();
            }
        },
            'on-cancel'() {
                onCancel && onCancel();
                _closeModal();
            }
    });
}
};

其他完善

关于组件实现国际化、与typsScript结合,大家可以根据自身情况在此基础上进行更改

----------@----------

Vue中v-html会导致哪些问题

  • 可能会导致 xss 攻击
  • v-html 会替换掉标签内部的子元素
let template = require('vue-template-compiler'); 
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) 

// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})} 
console.log(r.render);

// _c 定义在core/instance/render.js 
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') { 
    if (vnode.children) vnode.children.length = 0 
    if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property 
    if (elm.childNodes.length === 1) { 
        elm.removeChild(elm.childNodes[0]) 
    } 
}

----------@----------

说下 a t t r s 和 attrs和 attrslisteners的使用场景

API考察,但$attrs$listeners是比较少用的边界知识,而且vue3有变化,$listeners已经移除,还是有细节可说的

体验

一个包含组件透传属性的对象

<template>
    <child-component v-bind="$attrs">
        将非属性特性透传给内部的子组件
    </child-component>
</template>

回答范例

  • 我们可能会有一些属性和事件没有在props中定义,这类称为非属性特性,结合v-bind指令可以直接透传给内部的子组件。
  • 这类“属性透传”常常用于包装高阶组件时往内部传递属性,常用于爷孙组件之间传参。比如我在扩展A组件时创建了组件B组件,然后在C组件中使用B,此时传递给C的属性中只有props里面声明的属性是给B使用的,其他的都是A需要的,此时就可以利用v-bind="$attrs"透传下去。
  • 最常见用法是结合v-bind做展开;$attrs本身不是响应式的,除非访问的属性本身是响应式对象。
  • vue2中使用listeners获取事件,vue3中已移除,均合并到attrs中,使用起来更简单了

原理

查看透传属性foo和普通属性bar,发现vnode结构完全相同,这说明vue3中将分辨两者工作由框架完成而非用户指定:

<template>
  <h1>{{ msg }}</h1>
  <comp foo="foo" bar="bar" />
</template>
<template>
  <div>
    {{$attrs.foo}} {{bar}}
  </div>
</template>

<script setup>
defineProps({
  bar: String
})
</script>
_createVNode(Comp, {
    foo: "foo",
    bar: "bar"
})

----------@----------

在Vue中使用插件的步骤

  • 采用ES6import ... from ...语法或CommonJSrequire()方法引入插件
  • 使用全局方法Vue.use( plugin )使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })

----------@----------

构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么

  • vue.jsvue-cli工程的核心,主要特点是 双向数据绑定 和 组件系统。
  • vue-routervue官方推荐使用的路由框架。
  • vuex:专为 Vue.js 应用项目开发的状态管理器,主要用于维护vue组件间共用的一些 变量 和 方法。
  • axios( 或者 fetchajax ):用于发起 GET 、或 POSThttp请求,基于 Promise 设计。
  • vuex等:一个专为vue设计的移动端UI组件库。
  • 创建一个emit.js文件,用于vue事件机制的管理。
  • webpack:模块加载和vue-cli工程打包器。

----------@----------

vue-cli 工程常用的 npm 命令有哪些

  • 下载 node_modules 资源包的命令:
npm install
  • 启动 vue-cli 开发环境的 npm命令:
npm run dev
  • vue-cli 生成 生产环境部署资源 的 npm命令:
npm run build
  • 用于查看 vue-cli 生产环境部署资源文件大小的 npm命令:
npm run build --report

在浏览器上自动弹出一个 展示 vue-cli 工程打包后 app.jsmanifest.jsvendor.js 文件里面所包含代码的页面。可以具此优化 vue-cli 生产环境部署的静态资源,提升 页面 的加载速度

----------@----------

请说出vue cli项目中src目录每个文件夹和文件的用法

  • assets文件夹是放静态资源;
  • components是放组件;
  • router是定义路由相关的配置;
  • view视图;
  • app.vue是一个应用主组件;
  • main.js是入口文件

----------@----------

delete和Vue.delete删除数组的区别?

  • delete只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
  • Vue.delete直接删除了数组 改变了数组的键值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a)  //[empty,2,3,4]
this.$delete(b,0)
console.log(b)  //[2,3,4]

----------@----------

v-on可以监听多个方法吗?

可以监听多个方法

<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />

v-on 常用修饰符

  • .stop 该修饰符将阻止事件向上冒泡。同理于调用 event.stopPropagation() 方法
  • .prevent 该修饰符会阻止当前事件的默认行为。同理于调用 event.preventDefault() 方法
  • .self 该指令只当事件是从事件绑定的元素本身触发时才触发回调
  • .once 该修饰符表示绑定的事件只会被触发一次

----------@----------

v-once的使用场景有哪些

分析

v-onceVue中内置指令,很有用的API,在优化方面经常会用到

体验

仅渲染元素和组件一次,并且跳过未来更新

<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

回答范例

  • v-oncevue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新
  • 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段
  • 我们只需要作用的组件或元素上加上v-once即可
  • vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了
  • 编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算

原理

下面例子使用了v-once

<script setup>
import { ref } from 'vue'const msg = ref('Hello World!')
</script><template>
  <h1 v-once>{{ msg }}</h1>
  <input v-model="msg">
</template>

我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:

// ...
return (_ctx, _cache) => {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    // 从缓存获取vnode
    _cache[0] || (
      _setBlockTracking(-1),
      _cache[0] = _createElementVNode("h1", null, [
        _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
      ]),
      _setBlockTracking(1),
      _cache[0]
    ),
// ...

----------@----------

Vue Ref的作用

  • 获取dom元素this.$refs.box
  • 获取子组件中的datathis.$refs.box.msg
  • 调用子组件中的方法this.$refs.box.open()

----------@----------

scoped样式穿透

scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性

  1. 使用/deep/
<!-- Parent -->
<template>
<div class="wrap">
    <Child />
</div>
</template>

<style lang="scss" scoped>
.wrap /deep/ .box{
    background: red;
}
</style>

<!-- Child -->
<template>
    <div class="box"></div>
</template>
  1. 使用两个style标签
<!-- Parent -->
<template>
<div class="wrap">
    <Child />
</div>
</template>

<style lang="scss" scoped>
/* 其他样式 */
</style>
<style lang="scss">
.wrap .box{
  background: red;
}
</style>

<!-- Child -->
<template>
  <div class="box"></div>
</template>

----------@----------

Class 与 Style 如何动态绑定

Class 可以通过对象语法和数组语法进行动态绑定

对象语法:

<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>

data: {
  isActive: true,
  hasError: false
}

数组语法:

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

Style 也可以通过对象语法和数组语法进行动态绑定

对象语法:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}

数组语法:

<div v-bind:style="[styleColor, styleSize]"></div>

data: {
  styleColor: {
     color: 'red'
   },
  styleSize:{
     fontSize:'23px'
  }
}

----------@----------

Vue为什么没有类似于React中shouldComponentUpdate的生命周期

  • 考点: Vue的变化侦测原理
  • 前置知识: 依赖收集、虚拟DOM、响应式系统

根本原因是VueReact的变化侦测方式有所不同

  • 当React知道发生变化后,会使用Virtual Dom Diff进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要 shouldComponentUpdate 进行手动操作来减少diff,从而提高程序整体的性能
  • Vue在一开始就知道那个组件发生了变化,不需要手动控制diff,而组件内部采用的diff方式实际上是可以引入类似于shouldComponentUpdate相关生命周期的,但是通常合理大小的组件不会有过量的diff,手动优化的价值有限,因此目前Vue并没有考虑引入shouldComponentUpdate这种手动优化的生命周期

----------@----------

SPA、SSR的区别是什么

我们现在编写的VueReactAngular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别

  1. SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSRSSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA
  2. SPA应用只会首次请求html文件,后续只需要请求JSON数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO不友好。为了解决以上缺点,就有了SSR方案,由于HTML内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时SSR方案也会有性能,开发受限等问题
  3. 在选择上,如果我们的应用存在首屏加载优化需求,SEO需求时,就可以考虑SSR
  4. 但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR反而浪费资源,我们可以考虑预渲染(prerender)方案。另外nuxt.js/next.js中给我们提供了SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果,且能节约服务器资源

内容生成上的区别:

SSR

SPA

部署上的区别

----------@----------

vue-loader是什么?它有什么作用?

回答范例

  1. vue-loader是用于处理单文件组件(SFCSingle-File Component)的webpack loader
  2. 因为有了vue-loader,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为<template><script><style>,代码会异常清晰。结合其他loader我们还可以用Pug编写<template>,用SASS编写<style>,用TS编写<script>。我们的<style>还可以单独作用当前组件
  3. webpack打包时,会以loader的方式调用vue-loader
  4. vue-loader被执行时,它会对SFC中的每个语言块用单独的loader链处理。最后将这些单独的块装配成最终的组件模块

原理

vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面

// source.vue被vue-loader处理之后返回的代码// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
​
script.render = render
export default script

我们想要script块中的内容被作为js处理(当然如果是<script lang="ts">被作为ts理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC

import script from 'source.vue?vue&type=script

将被展开为:

import script from 'babel-loader!vue-loader!source.vue?vue&type=script'

类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,对下面的代码

<style scoped lang="scss">

vue-loader将会返回给我们下面结果:

import 'source.vue?vue&type=style&index=1&scoped&lang=scss'

然后webpack会展开如下:

import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
  • 当处理展开请求时,vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader
  • 对于<script>块,处理到这就可以了,但是<template><style>还有一些额外任务要做,比如
    • 需要用 Vue 模板编译器编译template,从而得到render函数
    • 需要对 <style scoped>中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前

实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:

// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'

----------@----------

说说你对slot的理解?slot使用场景有哪些

一、slot是什么

在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符

该占位符可以在后期使用自己的标记语言填充

举个栗子

<template id="element-details-template">
  <slot name="element-name">Slot template</slot>
</template>
<element-details>
  <span slot="element-name">1</span>
</element-details>
<element-details>
  <span slot="element-name">2</span>
</element-details>

template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,

customElements.define('element-details',
  class extends HTMLElement {
    constructor() {
      super();
      const template = document
        .getElementById('element-details-template')
        .content;
      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(template.cloneNode(true));
  }
})

Vue中的概念也是如此

Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口

二、使用场景

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理

如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情

通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

比如布局组件、表格列、下拉选、弹框显示内容等

----------@----------

三、分类

slot可以分来以下三种:

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

1. 默认插槽

子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面

父组件在使用的时候,直接在子组件的标签内写入内容即可

子组件Child.vue

<template>
    <slot>
      <p>插槽后备的内容</p>
    </slot>
</template>

父组件

<Child>
  <div>默认插槽</div>  
</Child>

2. 具名插槽

子组件用name属性来表示插槽的名字,不传为默认插槽

父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值

子组件Child.vue

<template>
    <slot>插槽后备的内容</slot>
  <slot name="content">插槽后备的内容</slot>
</template>

父组件

<child>
    <template v-slot:default>具名插槽</template>
    <!-- 具名插槽⽤插槽名做参数 -->
    <template v-slot:content>内容...</template>
</child>

3. 作用域插槽

子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上

父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用

子组件Child.vue

<template> 
  <slot name="footer" testProps="子组件的值">
          <h3>没传footer插槽</h3>
    </slot>
</template>

父组件

<child> 
    <!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
    <template v-slot:default="slotProps">
      来⾃⼦组件数据:{{slotProps.testProps}}
    </template>
    <template #default="slotProps">
      来⾃⼦组件数据:{{slotProps.testProps}}
    </template>
</child>

小结:

  • v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用
  • 默认插槽名为default,可以省略default直接写v-slot
  • 缩写为#时不能不写参数,写成#default
  • 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

四、原理分析

slot本质上是返回VNode的函数,一般情况下,Vue中的组件要渲染到页面上需要经过template -> render function -> VNode -> DOM 过程,这里看看slot如何实现:

编写一个buttonCounter组件,使用匿名插槽

Vue.component('button-counter', {
  template: '<div> <slot>我是默认内容</slot></div>'
})

使用该组件

new Vue({
    el: '#app',
    template: '<button-counter><span>我是slot传入内容</span></button-counter>',
    components:{buttonCounter}
})

获取buttonCounter组件渲染函数

(function anonymous(
) {
with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})

_v表示穿件普通文本节点,_t表示渲染插槽的函数

渲染插槽函数renderSlot(做了简化)

function renderSlot (
  name,
  fallback,
  props,
  bindObject
) {
  // 得到渲染插槽内容的函数    
  var scopedSlotFn = this.$scopedSlots[name];
  var nodes;
  // 如果存在插槽渲染函数,则执行插槽渲染函数,生成nodes节点返回
  // 否则使用默认值
  nodes = scopedSlotFn(props) || fallback;
  return nodes;
}

name属性表示定义插槽的名字,默认值为defaultfallback表示子组件中的slot节点的默认值

关于this.$scopredSlots是什么,我们可以先看看vm.slot

function initRender (vm) {
  ...
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  ...
}

resolveSlots函数会对children节点做归类和过滤处理,返回slots

function resolveSlots (
    children,
    context
  ) {
    if (!children || !children.length) {
      return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {
      var child = children[i];
      var data = child.data;
      // remove slot attribute if the node is resolved as a Vue slot node
      if (data && data.attrs && data.attrs.slot) {
        delete data.attrs.slot;
      }
      // named slots should only be respected if the vnode was rendered in the
      // same context.
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {
        // 如果slot存在(slot="header") 则拿对应的值作为key
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        // 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面
        if (child.tag === 'template') {
          slot.push.apply(slot, child.children || []);
        } else {
          slot.push(child);
        }
      } else {
        // 如果没有就默认是default
        (slots.default || (slots.default = [])).push(child);
      }
    }
    // ignore slots that contains only whitespace
    for (var name$1 in slots) {
      if (slots[name$1].every(isWhitespace)) {
        delete slots[name$1];
      }
    }
    return slots
}

_render渲染函数通过normalizeScopedSlots得到vm.$scopedSlots

vm.$scopedSlots = normalizeScopedSlots(
  _parentVnode.data.scopedSlots,
  vm.$slots,
  vm.$scopedSlots
);

作用域插槽中父组件能够得到子组件的值是因为在renderSlot的时候执行会传入props,也就是上述_t第三个参数,父组件则能够得到子组件传递过来的值

----------@----------

Vue.observable你有了解过吗?说说看

一、Observable 是什么

Observable 翻译过来我们可以理解成可观察的

我们先来看一下其在Vue中的定义

Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象

返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器

Vue.observable({ count : 1})

其作用等同于

new vue({ count : 1})

Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象

Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

二、使用场景

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择

创建一个js文件

// 引入vue
import Vue from 'vue
// 创建state对象,使用observable让state对象可响应
export let state = Vue.observable({
  name: '张三',
  'age': 38
})
// 创建对应的方法
export let mutations = {
  changeName(name) {
    state.name = name
  },
  setAge(age) {
    state.age = age
  }
}

.vue文件中直接使用即可

<template>
  <div>
    姓名:{{ name }}
    年龄:{{ age }}
    <button @click="changeName('李四')">改变姓名</button>
    <button @click="setAge(18)">改变年龄</button>
  </div>
</template>
import { state, mutations } from '@/store
export default {
  // 在计算属性中拿到值
  computed: {
    name() {
      return state.name
    },
    age() {
      return state.age
    }
  },
  // 调用mutations里面的方法,更新数据
  methods: {
    changeName: mutations.changeName,
    setAge: mutations.setAge
  }
}

三、原理分析

源码位置:src\core\observer\index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 判断是否存在__ob__响应式属性
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 实例化Observer响应式对象
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that have this object as root $data

    constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this)
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods)
            } else {
                copyAugment(value, arrayMethods, arrayKeys)
            }
            this.observeArray(value)
        } else {
            // 实例化对象是一个对象,进入walk方法
            this.walk(value)
        }
}

walk函数

walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍历key,通过defineReactive创建响应式对象
    for (let i = 0; i < keys.length; i++) {
        defineReactive(obj, keys[i])
    }
}

defineReactive方法

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  // 接下来调用Object.defineProperty()给对象定义响应式属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 对观察者watchers进行通知,state就成了全局响应式对象
      dep.notify()
    }
  })
}

----------@----------

Vue中的过滤器了解吗?过滤器的应用场景有哪些?

过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数

Vue 允许你自定义过滤器,可被用于一些常见的文本格式化

ps: Vue3中已废弃filter

如何用

vue中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript表达式的尾部,由“管道”符号指示:

<!-- 在双花括号中 -->
{ message | capitalize }

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

定义filter

在组件的选项中定义本地的过滤器

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

定义全局过滤器:

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})

注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数

过滤器可以串联:

{ message | filterA | filterB }

在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。

过滤器是 JavaScript函数,因此可以接收参数:

{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数。

其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数

举个例子:

<div id="app">
  <p>{{ msg | msgFormat('疯狂','--')}}</p>
</div>

<script>
    // 定义一个 Vue 全局的过滤器,名字叫做  msgFormat
    Vue.filter('msgFormat', function(msg, arg, arg2) {
        // 字符串的  replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则
        return msg.replace(/单纯/g, arg+arg2)
    })
</script>

小结:

  • 部过滤器优先于全局过滤器被调用
  • 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右

应用场景

平时开发中,需要用到过滤器的地方有很多,比如单位转换数字打点文本格式化时间格式化之类的等

比如我们要实现将30000 => 30,000,这时候我们就需要使用过滤器

Vue.filter('toThousandFilter', function (value) {
  if (!value) return ''
  value = value.toString()
  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})

原理分析

使用过滤器

{{ message | capitalize }}

在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters,我们放到最后讲

_s(_f('filterFormat')(message))

首先分析一下_f

_f 函数全名是:resolveFilter,这个函数的作用是从this.$options.filters中找出注册的过滤器并返回

// 变为
this.$options.filters['filterFormat'](message) // message为参数

关于resolveFilter

import { indentity,resolveAsset } from 'core/util/index' 

export function resolveFilter(id){
  return resolveAsset(this.$options,'filters',id,true) || identity
}

内部直接调用resolveAsset,将option对象,类型,过滤器id,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;

resolveAsset的代码如下:

export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西
  if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回
      return 
  }
  const assets = options[type]  // 将我们注册的所有过滤器保存在变量中
  // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配
  if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器
  // 没有找到,代码继续执行
  const camelizedId  = camelize(id) // 万一你是驼峰的呢
  if(hasOwn(assets,camelizedId)) return assets[camelizedId]
  // 没找到,继续执行
  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
  if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
  // 如果还是没找到,则检查原型链(即访问属性)
  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  // 如果依然没找到,则在非生产环境的控制台打印警告
  if(process.env.NODE_ENV !== 'production' && warnMissing && !result){
    warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)
  }
  // 无论是否找到,都返回查找结果
  return result
}

下面再来分析一下_s

_s 函数的全称是 toString,过滤器处理后的结果会当作参数传递给 toString函数,最终 toString函数执行后的结果会保存到Vnode中的text属性中,渲染到视图中

function toString(value){
  return value == null
  ? ''
  : typeof value === 'object'
    ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距
    : String(value)
}

最后,在分析下parseFilters,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式

function parseFilters (filter) {
    let filters = filter.split('|')
    let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
    let i
    if (filters) {
        for(i = 0;i < filters.length;i++){
            experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数
        }
    }
    return expression
}
// warpFilter函数实现
function warpFilter(exp,filter){
    // 首先判断过滤器是否有其他参数
    const i = filter.indexof('(')
    if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接
        return `_f("${filter}")(${exp})`
    }else{
        const name = filter.slice(0,i) // 过滤器名称
        const args = filter.slice(i+1) // 参数,但还多了 ‘)’
        return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'
    }
}

小结:

  • 在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)
  • 编译后通过调用resolveFilter函数找到对应过滤器并返回结果
  • 执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnodetext属性中,渲染到视图

----------@----------

Vue项目中有封装过axios吗?主要是封装哪方面的?

一、axios是什么

axios 是一个轻量的 HTTP客户端

基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选

特性

  • 从浏览器中创建 XMLHttpRequests
  • node.js 创建 http请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换JSON 数据
  • 客户端支持防御XSRF

基本使用

安装

// 项目中安装
npm install axios --S
// cdn 引入
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

导入

import axios from 'axios'

发送请求

axios({        
  url:'xxx',    // 设置请求的地址
  method:"GET", // 设置请求方法
  params:{      // get请求使用params进行参数凭借,如果是post请求用data
    type: '',
    page: 1
  }
}).then(res => {  
  // res为后端返回的数据
  console.log(res);   
})

并发请求axios.all([])

function getUserAccount() {
    return axios.get('/user/12345');
}

function getUserPermissions() {
    return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
    .then(axios.spread(function (res1, res2) { 
    // res1第一个请求的返回的内容,res2第二个请求返回的内容
    // 两个请求都执行完成才会执行
}));

二、为什么要封装

axios 的 API 很友好,你完全可以很轻松地在项目中直接使用。

不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍

这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用

举个例子:

axios('http://localhost:3000/data', {
  // 配置代码
  method: 'GET',
  timeout: 1000,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'xxx',
  },
  transformRequest: [function (data, headers) {
    return data;
  }],
  // 其他请求配置...
})
.then((data) => {
  // todo: 真正业务逻辑代码
  console.log(data);
}, (err) => {
  // 错误处理代码  
  if (err.response.status === 401) {
  // handle authorization error
  }
  if (err.response.status === 403) {
  // handle server forbidden error
  }
  // 其他错误处理.....
  console.log(err);
});

如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了

这时候我们就需要对axios进行二次封装,让使用更为便利

三、如何封装

  • 封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时间…
  • 设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分
  • 请求头 : 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务)
  • 状态码: 根据接口返回的不同status , 来执行不同的业务,这块需要和后端约定好
  • 请求方法:根据getpost等方法进行一个再次封装,使用起来更为方便
  • 请求拦截器: 根据请求的请求头设定,来决定哪些请求可以访问
  • 响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务

设置接口请求前缀

利用node环境变量来作判断,用来区分开发、测试、生产环境

if (process.env.NODE_ENV === 'development') {
  axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
  axios.defaults.baseURL = 'http://prod.xxx.com'
}

在本地调试的时候,还需要在vue.config.js文件中配置devServer实现代理转发,从而实现跨域

devServer: {
    proxy: {
      '/proxyApi': {
        target: 'http://dev.xxx.com',
        changeOrigin: true,
        pathRewrite: {
          '/proxyApi': ''
        }
      }
    }
  }

设置请求头与超时时间

大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置

const service = axios.create({
    ...
    timeout: 30000,  // 请求 30s 超时
      headers: {
        get: {
          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
          // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
        },
        post: {
          'Content-Type': 'application/json;charset=utf-8'
          // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
        }
  },
})

封装请求方法

先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出去

// get 请求
export function httpGet({
  url,
  params = {}
}) {
  return new Promise((resolve, reject) => {
    axios.get(url, {
      params
    }).then((res) => {
      resolve(res.data)
    }).catch(err => {
      reject(err)
    })
  })
}

// post
// post请求
export function httpPost({
  url,
  data = {},
  params = {}
}) {
  return new Promise((resolve, reject) => {
    axios({
      url,
      method: 'post',
      transformRequest: [function (data) {
        let ret = ''
        for (let it in data) {
          ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
        }
        return ret
      }],
      // 发送的数据
      data,
      // url参数
      params

    }).then(res => {
      resolve(res.data)
    })
  })
}

把封装的方法放在一个api.js文件中

import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })

页面中就能直接调用

// .vue
import { getorglist } from '@/assets/js/api'

getorglist({ id: 200 }).then(res => {
  console.log(res)
})

这样可以把api统一管理起来,以后维护修改只需要在api.js文件操作即可

请求拦截器

请求拦截器可以在每个请求里加上token,做了统一处理后维护起来也方便

// 请求拦截器
axios.interceptors.request.use(
  config => {
    // 每次发送请求之前判断是否存在token
    // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
    token && (config.headers.Authorization = token)
    return config
  },
  error => {
    return Promise.error(error)
  })

响应拦截器

响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授权

// 响应拦截器
axios.interceptors.response.use(response => {
  // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
  // 否则的话抛出错误
  if (response.status === 200) {
    if (response.data.code === 511) {
      // 未授权调取授权接口
    } else if (response.data.code === 510) {
      // 未登录跳转登录页
    } else {
      return Promise.resolve(response)
    }
  } else {
    return Promise.reject(response)
  }
}, error => {
  // 我们可以在这里对异常状态作统一处理
  if (error.response.status) {
    // 处理请求失败的情况
    // 对不同返回码对相应处理
    return Promise.reject(error.response)
  }
})

小结

  • 封装是编程中很有意义的手段,简单的axios封装,就可以让我们可以领略到它的魅力
  • 封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案

----------@----------

说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢

一、为什么要划分

使用vue构建项目,项目结构清晰会提高开发效率,熟悉项目的各种配置同样会让开发效率更高

在划分项目结构的时候,需要遵循一些基本的原则:

  • 文件夹和文件夹内部文件的语义一致性
  • 单一入口/出口
  • 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
  • 公共的文件应该以绝对路径的方式从根目录引用
  • /src 外的文件不应该被引入

文件夹和文件夹内部文件的语义一致性

我们的目录结构都会有一个文件夹是按照路由模块来划分的,如pages文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅应该包含路由模块,而不应该有别的其他的非路由模块的文件夹

这样做的好处在于一眼就从 pages文件夹看出这个项目的路由有哪些

单一入口/出口

举个例子,在pages文件夹里面存在一个seller文件夹,这时候seller 文件夹应该作为一个独立的模块由外部引入,并且 seller/index.js 应该作为外部引入 seller 模块的唯一入口

// 错误用法
import sellerReducer from 'src/pages/seller/reducer'

// 正确用法
import { reducer as sellerReducer } from 'src/pages/seller'

这样做的好处在于,无论你的模块文件夹内部有多乱,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点

就近原则,紧耦合的文件应该放到一起,且应以相对路径引用

使用相对路径可以保证模块内部的独立性

// 正确用法
import styles from './index.module.scss'
// 错误用法
import styles from 'src/pages/seller/index.module.scss'

举个例子

假设我们现在的 seller 目录是在 src/pages/seller,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller

如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller 文件夹内部不需要做任何变更。

但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import 的路径做修改

公共的文件应该以绝对路径的方式从根目录引用

公共指的是多个路由模块共用,如一些公共的组件,我们可以放在src/components

在使用到的页面中,采用绝对路径的形式引用

// 错误用法
import Input from '../../components/input'
// 正确用法
import Input from 'src/components/input'

同样的,如果我们需要对文件夹结构进行调整。将 /src/components/input 变成 /src/components/new/input,如果使用绝对路径,只需要全局搜索替换

再加上绝对路径有全局的语义,相对路径有独立模块的语义

src 外的文件不应该被引入

vue-cli脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src文件夹,里面放着所有的项目需要的资源,js,css, png, svg 等等。src 外会放一些项目配置,依赖,环境等文件

这样的好处是方便划分项目代码文件和配置文件

二、目录结构

单页面目录结构

project
│  .browserslistrc
│  .env.production
│  .eslintrc.js
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│  yarn-error.log
│  yarn.lock
│
├─public
│      favicon.ico
│      index.html
│
|-- src
    |-- components
        |-- input
            |-- index.js
            |-- index.module.scss
    |-- pages
        |-- seller
            |-- components
                |-- input
                    |-- index.js
                    |-- index.module.scss
            |-- reducer.js
            |-- saga.js
            |-- index.js
            |-- index.module.scss
        |-- buyer
            |-- index.js
        |-- index.js

多页面目录结构

my-vue-test:..browserslistrc
│  .env.production
│  .eslintrc.js
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│  yarn-error.log
│  yarn.lock
│
├─public
│      favicon.ico
│      index.html
│
└─src
    ├─apis //接口文件根据页面或实例模块化
    │      index.js
    │      login.js
    │
    ├─components //全局公共组件
    │  └─header
    │          index.less
    │          index.vue
    │
    ├─config //配置(环境变量配置不同passid等)
    │      env.js
    │      index.js
    │
    ├─contant //常量
    │      index.js
    │
    ├─images //图片
    │      logo.png
    │
    ├─pages //多页面vue项目,不同的实例
    │  ├─index //主实例
    │  │  │  index.js
    │  │  │  index.vue
    │  │  │  main.js
    │  │  │  router.js
    │  │  │  store.js
    │  │  │
    │  │  ├─components //业务组件
    │  │  └─pages //此实例中的各个路由
    │  │      ├─amenu
    │  │      │      index.vue
    │  │      │
    │  │      └─bmenu
    │  │              index.vue
    │  │
    │  └─login //另一个实例
    │          index.js
    │          index.vue
    │          main.js
    │
    ├─scripts //包含各种常用配置,工具函数
    │  │  map.js
    │  │
    │  └─utils
    │          helper.js
    │
    ├─store //vuex仓库
    │  │  index.js
    │  │
    │  ├─index
    │  │      actions.js
    │  │      getters.js
    │  │      index.js
    │  │      mutation-types.js
    │  │      mutations.js
    │  │      state.js
    │  │
    │  └─user
    │          actions.js
    │          getters.js
    │          index.js
    │          mutation-types.js
    │          mutations.js
    │          state.js
    │
    └─styles //样式统一配置
        │  components.less
        │
        ├─animation
        │      index.less
        │      slide.less
        │
        ├─base
        │      index.less
        │      style.less
        │      var.less
        │      widget.less
        │
        └─common
                index.less
                reset.less
                style.less
                transition.less

小结

项目的目录结构很重要,因为目录结构能体现很多东西,怎么规划目录结构可能每个人有自己的理解,但是按照一定的规范去进行目录的设计,能让项目整个架构看起来更为简洁,更加易用

----------@----------

从0到1自己构架一个vue项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织

综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可

思路

  • 构建项目,创建项目基本结构
  • 引入必要的插件:
  • 代码规范:prettiereslint
  • 提交规范:husky,lint-staged`
  • 其他常用:svg-loadervueusenprogress
  • 常见目录结构

回答范例

  1. 0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
  2. 目前vue3项目我会用vite或者create-vue创建项目
  3. 接下来引入必要插件:路由插件vue-router、状态管理vuex/piniaui库我比较喜欢element-plus和antd-vuehttp工具我会选axios
  4. 其他比较常用的库有vueusenprogress,图标可以使用vite-svg-loader
  5. 下面是代码规范:结合prettiereslint即可
  6. 最后是提交规范,可以使用huskylint-stagedcommitlint
  7. 目录结构我有如下习惯: .vscode:用来放项目中的 vscode 配置
  • plugins:用来放 vite 插件的 plugin 配置
  • public:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下
  • src:用来放项目代码文件
  • api:用来放http的一些接口配置
  • assets:用来放一些 CSS 之类的静态资源
  • components:用来放项目通用组件
  • layout:用来放项目的布局
  • router:用来放项目的路由配置
  • store:用来放状态管理Pinia的配置
  • utils:用来放项目中的工具方法类
  • views:用来放项目的页面文件

----------@----------

vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做

一、是什么

权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源

而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发

  • 页面加载触发
  • 页面上的按钮点击触发

总的来说,所有的请求发起都触发自前端路由或视图

所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:

  • 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页
  • 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件
  • 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截

二、如何做

前端权限控制可以分为四个方面:

  • 接口权限
  • 按钮权限
  • 菜单权限
  • 路由权限

接口权限

接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录

登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token

axios.interceptors.request.use(config => {
    config.headers['token'] = cookie.get('token')
    return config
})
axios.interceptors.response.use(res=>{},{response}=>{
    if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误
        router.push('/login')
    }
})

路由权限控制

方案一

初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验

const routerMap = [
  {
    path: '/permission',
    component: Layout,
    redirect: '/permission/index',
    alwaysShow: true, // will always show the root menu
    meta: {
      title: 'permission',
      icon: 'lock',
      roles: ['admin', 'editor'] // you can set roles in root nav
    },
    children: [{
      path: 'page',
      component: () => import('@/views/permission/page'),
      name: 'pagePermission',
      meta: {
        title: 'pagePermission',
        roles: ['admin'] // or you can only set roles in sub nav
      }
    }, {
      path: 'directive',
      component: () => import('@/views/permission/directive'),
      name: 'directivePermission',
      meta: {
        title: 'directivePermission'
        // if do not set roles, means: this page does not require permission
      }
    }]
  }]

这种方式存在以下四种缺点:

  • 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。
  • 全局路由守卫里,每次路由跳转都要做权限判断。
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

方案二

初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制

登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie

NProgress.configure({ showSpinner: false })// NProgress Configuration

// permission judge function
function hasPermission(roles, permissionRoles) {
  if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
  if (!permissionRoles) return true
  return roles.some(role => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/authredirect']// no redirect whitelist

router.beforeEach((to, from, next) => {
  NProgress.start() // start progress bar
  if (getToken()) { // determine if there has token
    /* has token*/
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done() // if current page is dashboard will not trigger    afterEach hook, so manually handle it
    } else {
      if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetUserInfo').then(res => { // 拉取user_info
          const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
          store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
            router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
          })
        }).catch((err) => {
          store.dispatch('FedLogOut').then(() => {
            Message.error(err || 'Verification failed, please login again')
            next({ path: '/' })
          })
        })
      } else {
        // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
        if (hasPermission(store.getters.roles, to.meta.roles)) {
          next()//
        } else {
          next({ path: '/401', replace: true, query: { noGoBack: true }})
        }
        // 可删 ↑
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
      next()
    } else {
      next('/login') // 否则全部重定向到登录页
      NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
  }
})

router.afterEach(() => {
  NProgress.done() // finish progress bar
})

按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限

这种方式也存在了以下的缺点:

  • 全局路由守卫里,每次路由跳转都要做判断
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

菜单权限

菜单权限可以理解成将页面与理由进行解耦

方案一

菜单与路由分离,菜单由后端返回

前端定义路由信息

{
    name: "login",
    path: "/login",
    component: () => import("@/pages/Login.vue")
}

name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验

全局路由守卫里做判断

function hasPermission(router, accessMenu) {
  if (whiteList.indexOf(router.path) !== -1) {
    return true;
  }
  let menu = Util.getMenuByName(router.name, accessMenu);
  if (menu.name) {
    return true;
  }
  return false;

}

Router.beforeEach(async (to, from, next) => {
  if (getToken()) {
    let userInfo = store.state.user.userInfo;
    if (!userInfo.name) {
      try {
        await store.dispatch("GetUserInfo")
        await store.dispatch('updateAccessMenu')
        if (to.path === '/login') {
          next({ name: 'home_index' })
        } else {
          //Util.toDefaultPage([...routers], to.name, router, next);
          next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路由
        }
      }  
      catch (e) {
        if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
          next()
        } else {
          next('/login')
        }
      }
    } else {
      if (to.path === '/login') {
        next({ name: 'home_index' })
      } else {
        if (hasPermission(to, store.getters.accessMenu)) {
          Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
        } else {
          next({ path: '/403',replace:true })
        }
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
      next()
    } else {
      next('/login')
    }
  }
  let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
  Util.title(menu.title);
});

Router.afterEach((to) => {
  window.scrollTo(0, 0);
});

每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的

如果根据路由name找不到对应的菜单,就表示用户有没权限访问

如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载

这种方式的缺点:

  • 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
  • 全局路由守卫里,每次路由跳转都要做判断

方案二

菜单和路由都由后端返回

前端统一定义路由组件

const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {
    home: Home,
    userInfo: UserInfo
};

后端路由组件返回以下格式

[
    {
        name: "home",
        path: "/",
        component: "home"
    },
    {
        name: "home",
        path: "/userinfo",
        component: "userInfo"
    }
]

在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件

如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理

这种方法也会存在缺点:

  • 全局路由守卫里,每次路由跳转都要做判断
  • 前后端的配合要求更高

按钮权限

方案一

按钮权限也可以用v-if判断

但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判断

这种方式就不展开举例了

方案二

通过自定义指令进行按钮权限的判断

首先配置路由

{
    path: '/permission',
    component: Layout,
    name: '权限测试',
    meta: {
        btnPermissions: ['admin', 'supper', 'normal']
    },
    //页面需要的权限
    children: [{
        path: 'supper',
        component: _import('system/supper'),
        name: '权限测试页',
        meta: {
            btnPermissions: ['admin', 'supper']
        } //页面需要的权限
    },
    {
        path: 'normal',
        component: _import('system/normal'),
        name: '权限测试页',
        meta: {
            btnPermissions: ['admin']
        } //页面需要的权限
    }]
}

自定义权限鉴定指令

import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
    bind: function (el, binding, vnode) {
        // 获取页面按钮权限
        let btnPermissionsArr = [];
        if(binding.value){
            // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
            btnPermissionsArr = Array.of(binding.value);
        }else{
            // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
            btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
        }
        if (!Vue.prototype.$_has(btnPermissionsArr)) {
            el.parentNode.removeChild(el);
        }
    }
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
    let isExist = false;
    // 获取用户按钮权限
    let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
    if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
        return false;
    }
    if (value.indexOf(btnPermissionsStr) > -1) {
        isExist = true;
    }
    return isExist;
};
export {has}

在使用的按钮中只需要引用v-has指令

<el-button @click='editClick' type="primary" v-has>编辑</el-button>

小结

关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离

权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断

----------@----------

Vue项目中你是如何解决跨域的呢

一、跨域是什么

跨域本质是浏览器基于同源策略的一种安全手段

同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能

所谓同源(即指在同一个域)具有以下三个相同点

  • 协议相同(protocol)
  • 主机相同(host)
  • 端口相同(port)

反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。

----------@----------

二、如何解决

解决跨域的方法有很多,下面列举了三种:

  • JSONP
  • CORS
  • Proxy

而在vue项目中,我们主要针对CORSProxy这两种方案进行展开

CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应

CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

只要后端实现了 CORS,就实现了跨域

!

koa框架举例

添加中间件,直接设置Access-Control-Allow-Origin响应头

app.use(async (ctx, next)=> {
  ctx.set('Access-Control-Allow-Origin', '*');
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200; 
  } else {
    await next();
  }
})

ps: Access-Control-Allow-Origin 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host

Proxy

代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击

方案一

如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象

通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域

vue.config.js文件,新增以下代码

amodule.exports = {
    devServer: {
        host: '127.0.0.1',
        port: 8084,
        open: true,// vue项目启动时自动打开浏览器
        proxy: {
            '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
                target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
                changeOrigin: true, //是否跨域
                pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
                    '^/api': "" 
                }
            }
        }
    }
}

通过axios发送请求中,配置请求的根路径

axios.defaults.baseURL = '/api'

方案二

此外,还可通过服务端实现代理请求转发

express框架为例

var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false
                      }));
module.exports = app

方案三

通过配置nginx实现代理

server {
    listen    80;
       location / {
        root  /var/www/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
    location /api {
        proxy_pass  http://127.0.0.1:3000;
        proxy_redirect   off;
        proxy_set_header  Host       $host;
        proxy_set_header  X-Real-IP     $remote_addr;
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}

----------@----------

Vue项目本地开发完成后部署到服务器后报404是什么原因呢

如何部署

前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可

我们知道vue项目在构建后,是生成一系列的静态文件

常规布署我们只需要将这个目录上传至目标服务器即可


web容器跑起来,以nginx为例

server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}

配置完成记得重启nginx

// 检查配置是否正确
nginx -t 

// 平滑重启
nginx -s reload

操作完后就可以在浏览器输入域名进行访问了

当然上面只是提到最简单也是最直接的一种布署方式

什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开

404问题

这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?

我们先还原一下场景:

  • vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了404错误

先定位一下,HTTP 404 错误意味着链接指向的资源不存在

问题在于为什么不存在?且为什么只有history模式下会出现这个问题?

为什么history模式下有问题

Vue是属于单页应用(single-page application)

SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html

现在,我们回头来看一下我们的nginx配置

server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
  }
}

可以根据 nginx 配置得出,当我们在地址栏输入 www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们在跳转路由进入到 www.xxx.com/login

关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出现 404 的情况

为什么hash模式下没有问题

router hash 模式我们都知道是用符号#表示的,如 website.com/#/login, hash 的值为 #/login

它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面

hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误

解决方案

看到这里我相信大部分同学都能想到怎么解决问题了,

产生问题的本质是因为我们的路由是通过JS来执行视图切换的,

当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404

所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理

nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;

server {
  listen  80;
  server_name  www.xxx.com;

  location / {
    index  /data/dist/index.html;
    try_files $uri $uri/ /index.html;
  }
}

修改完配置文件后记得配置的更新

nginx -s reload

这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件

为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '*', component: NotFoundComponent }
  ]
})

----------@----------

实际工作中,你总结的vue最佳实践有哪些

从编码风格、性能、安全等方面说几条:

编码风格方面:

  • 命名组件时使用“多词”风格避免和HTML元素冲突
  • 使用“细节化”方式定义属性而不是只有一个属性名
  • 属性名声明时使用“驼峰命名”,模板或jsx中使用“肉串命名”
  • 使用v-for时务必加上key,且不要跟v-if写在一起

性能方面:

  • 路由懒加载减少应用尺寸
  • 利用SSR减少首屏加载时间
  • 利用v-once渲染那些不需要更新的内容
  • 一些长列表可以利用虚拟滚动技术避免内存过度占用
  • 对于深层嵌套对象的大数组可以使用shallowRefshallowReactive降低开销
  • 避免不必要的组件抽象

安全:

  • 不使用不可信模板,例如使用用户输入拼接模板:template: <div> + userProvidedString + </div>
  • 避免使用v-html:url:style等,避免htmlurl、样式等注入

----------@----------

vue 中使用了哪些设计模式

  • 工厂模式 传入参数即可创建实例:虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
  • 单例模式 整个程序有且仅有一个实例:vuexvue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
  • 发布-订阅模式 (vue 事件机制)
  • 观察者模式 (响应式数据原理)
  • 装饰模式: (@装饰器的用法)
  • 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

----------@----------

如果让你从零开始写一个vuex,说说你的思路

思路分析

这个题目很有难度,首先思考vuex解决的问题:存储用户全局状态并提供管理状态API。

  • vuex需求分析
  • 如何实现这些需求

回答范例

  1. 官方说vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
  • 要实现一个Store存储全局状态
  • 要提供修改状态所需API:commit(type, payload), dispatch(type, payload)
  1. 实现Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commitdispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件
  2. commit(type, payload)方法中可以获取用户传入mutations并执行它,这样可以按用户提供的方法修改状态。 dispatch(type, payload)类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果

实践

Store的实现:

class Store {
    constructor(options) {
        this.state = reactive(options.state)
        this.options = options
    }
    commit(type, payload) {
        this.options.mutations[type].call(this, this.state, payload)
    }
}

vuex简易版

/**
 * 1 实现插件,挂载$store
 * 2 实现store
 */

let Vue;

class Store {
  constructor(options) {
    // state响应式处理
    // 外部访问: this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更
    this._vm = new Vue({
      data: {
        $$state: options.state
      }
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state () {
    return this._vm._data.$$state
  }

  set state (val) {
    return new Error('Please use replaceState to reset state')
  }

  handleGetters (getters) {
    Object.keys(getters).map(key => {
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](this.state)
      })
    })
  }

  commit (type, payload) {
    let entry = this._mutations[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch (type, payload) {
    let entry = this._actions[type]
    if (!entry) {
      return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({
    beforeCreate () {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    },
  })
}


export default { Store, install }

验证方式

import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
    // state从哪里来的
    add (state) {
      state.counter++
    }
  },
  getters: {
    doubleCounter (state) {
      return state.counter * 2
    }
  },
  actions: {
    add ({ commit }) {
      setTimeout(() => {
        commit('add')
      }, 1000)
    }
  },
  modules: {
  }
})

----------@----------

使用vue渲染大量数据时应该怎么优化?说下你的思路!

分析

企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。

回答

  1. 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树

  2. 处理时要根据情况做不同处理:

  • 可以采取分页的方式获取,避免渲染大量数据

  • vue-virtual-scroller (opens new window)等虚拟滚动方案,只渲染视口范围内的数据

  • 如果不需要更新,可以使用v-once方式只渲染一次

  • 通过v-memo (opens new window)可以缓存结果,结合v-for使用,避免数据变化时不必要的VNode创建

  • 可以采用懒加载方式,在用户需要的时候再加载数据,比如tree组件子树的懒加载

  1. 还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案

----------@----------

动态给vue的data添加一个新的属性时会发生什么?怎样解决?

Vue 不允许在已经创建的实例上动态添加新的响应式属性

若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()

Vue.set()

Vue.set( target, propertyName/index, value )

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的值

通过Vue.set向响应式对象中添加一个property,并确保这个新 property同样是响应式的,且触发视图更新

关于Vue.set源码(省略了很多与本节不相关的代码)

源码位置:src\core\observer\index.js

function set (target: Array<any> | Object, key: any, val: any): any {
  ...
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这里无非再次调用defineReactive方法,实现新增属性的响应式

关于defineReactive方法,内部还是通过Object.defineProperty实现属性拦截

大致代码如下:

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                console.log(`set ${key}:${newVal}`);
                val = newVal
            }
        }
    })
}

Object.assign()

直接使用Object.assign()添加到对象的新属性不会触发更新

应创建一个新的对象,合并原对象和混入对象的属性

this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})

$forceUpdate

如果你发现你自己需要在 Vue中做一次强制更新,99.9% 的情况,是你在某个地方做错了事

$forceUpdate迫使Vue 实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

小结

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()
  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象
  • 如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)

PS:vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

----------@----------

你是怎么处理vue项目中的错误的?

分析

  • 这是一个综合应用题目,在项目中我们常常需要将App的异常上报,此时错误处理就很重要了。
  • 这里要区分错误的类型,针对性做收集。
  • 然后是将收集的的错误信息上报服务器。

思路

  • 首先区分错误类型
  • 根据错误不同类型做相应收集
  • 收集的错误是如何上报服务器的

回答范例

  1. 应用中的错误类型分为"接口异常"和“代码逻辑异常
  2. 我们需要根据不同错误类型做相应处理:接口异常是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但是返回的是错误状态。以Axios为例,这类异常我们可以通过封装Axios,在拦截器中统一处理整个应用中请求的错误。代码逻辑异常是我们编写的前端代码中存在逻辑上的错误造成的异常,vue应用中最常见的方式是使用全局错误处理函数app.config.errorHandler收集错误
  3. 收集到错误之后,需要统一处理这些异常:分析错误,获取需要错误信息和数据。这里应该有效区分错误类型,如果是请求错误,需要上报接口信息,参数,状态码等;对于前端逻辑异常,获取错误名称和详情即可。另外还可以收集应用名称、环境、版本、用户信息,所在页面等。这些信息可以通过vuex存储的全局状态和路由信息获取

实践

axios拦截器中处理捕获异常:

// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (error) => {
    // 存在response说明服务器有响应
    if (error.response) {
      let response = error.response;
      if (response.status >= 400) {
        handleError(response);
      }
    } else {
      handleError(null);
    }
    return Promise.reject(error);
  },
);

vue中全局捕获异常:

import { createApp } from 'vue'const app = createApp(...)
​
app.config.errorHandler = (err, instance, info) => {
  // report error to tracking services
}

处理接口请求错误:

function handleError(error, type) {
  if(type == 1) {
    // 接口错误,从config字段中获取请求信息
    let { url, method, params, data } = error.config
    let err_data = {
       url, method,
       params: { query: params, body: data },
       error: error.data?.message || JSON.stringify(error.data),
    })
  }
}

处理前端逻辑错误:

function handleError(error, type) {
  if(type == 2) {
    let errData = null
    // 逻辑错误
    if(error instanceof Error) {
      let { name, message } = error
      errData = {
        type: name,
        error: message
      }
    } else {
      errData = {
        type: 'other',
        error: JSON.strigify(error)
      }
    }
  }
}

----------@----------

SPA首屏加载速度慢的怎么解决

一、什么是首屏加载

首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容

首屏加载可以说是用户体验中最重要的环节

关于计算首屏时间

利用performance.timing提供的数据:

通过DOMContentLoad或者performance来计算出首屏时间

// 方案一:
document.addEventListener('DOMContentLoaded', (event) => {
    console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming的实例,结构如下:
{
  name: "first-contentful-paint",
  entryType: "paint",
  startTime: 507.80000002123415,
  duration: 0,
};

二、加载慢的原因

在页面渲染的过程,导致加载速度慢的因素可能如下:

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否重复发送请求去加载了
  • 加载脚本的时候,渲染内容堵塞了

三、解决方案

常见的几种SPA首屏优化方式

  • 减小入口文件积
  • 静态资源本地缓存
  • UI框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启GZip压缩
  • 使用SSR

1. 减小入口文件体积

常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加

vue-router配置路由的时候,采用动态加载路由的形式

routes:[ 
    path: 'Blogs',
    name: 'ShowBlogs',
    component: () => import('./components/ShowBlogs.vue')
]

以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件

2. 静态资源本地缓存

后端返回资源问题:

  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头
  • 采用Service Worker离线缓存

前端合理利用localStorage

3. UI框架按需加载

在日常使用UI框架,例如element-UI、或者antd,我们经常性直接引用整个UI

import ElementUI from 'element-ui'
Vue.use(ElementUI)

但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用

import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)

4. 组件重复打包

假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载

解决方案:在webpackconfig文件中,修改CommonsChunkPlugin的配置

minChunks: 3

minChunks为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件

5. 图片资源的压缩

图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素

对于所有的图片资源,我们可以进行适当的压缩

对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。

6. 开启GZip压缩

拆完包之后,我们再用gzip做一下压缩 安装compression-webpack-plugin

cnmp i compression-webpack-plugin -D

vue.congig.js中引入并修改webpack配置

const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') {
            // 为生产环境修改配置...
            config.mode = 'production'
            return {
                plugins: [new CompressionPlugin({
                    test: /\.js$|\.html$|\.css/, //匹配文件名
                    threshold: 10240, //对超过10k的数据进行压缩
                    deleteOriginalAssets: false //是否删除原文件
                })]
            }
        }

在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip,就发送给它gzip格式的文件 我的服务器是用express框架搭建的 只要安装一下compression就能使用

const compression = require('compression')
app.use(compression())  // 在其他中间件使用之前调用

7. 使用SSR

SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器

从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染

四、小结

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化页面渲染优化

下图是更为全面的首屏优化的方案

大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化

----------@----------

Vue中常见性能优化

编码优化

  1. 使用v-show复用DOM:避免重复创建组件
<template>
  <div class="cell">
    <!-- 这种情况用v-show复用DOM,比v-if效果好 -->
    <div v-show="value" class="on">
      <Heavy :n="10000"/>
    </div>
    <section v-show="!value" class="off">
      <Heavy :n="10000"/>
    </section>
  </div>
</template>
  1. 合理使用路由懒加载、异步组件,有效拆分App尺寸,访问时才异步加载
const router = createRouter({
  routes: [
    // 借助webpack的import()实现异步组件
    { path: '/foo', component: () => import('./Foo.vue') }
  ]
})
  1. keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件状态
<router-view v-slot="{ Component }">
    <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  1. v-oncev-memo:不再变化的数据使用v-once
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

按条件跳过更新时使用v-momo:下面这个列表只会更新选中状态变化项

<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
  <p>...more child nodes</p>
</div>
  1. 长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
<recycle-scroller
  class="items"
  :items="items"
  :item-size="24"
>
  <template v-slot="{ item }">
    <FetchItemView
      :item="item"
      @vote="voteItem(item)"
    />
  </template>
</recycle-scroller>
  1. 防止内部泄漏,组件销毁后把全局变量和事件销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
export default {
  created() {
    this.timer = setInterval(this.refresh, 2000)
  },
  beforeUnmount() {
    clearInterval(this.timer)
  }
}
  1. 图片懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载

<!-- 参考 https://github.com/hilongjw/vue-lazyload -->
<img v-lazy="/static/img/1.png">
  1. 滚动到可视区域动态加载

https://tangbc.github.io/vue-virtual-scroll-list(opens new window)

  1. 第三方插件按需引入:(babel-plugin-component

element-plus这样的第三方组件库可以按需引入避免体积太大

import { createApp } from 'vue';
import { Button, Select } from 'element-plus';const app = createApp()
app.use(Button)
app.use(Select)
  1. 服务端渲染:SSR

如果SPA应用有首屏渲染慢的问题,可以考虑SSR

以及下面的其他方法

  • 不要将所有的数据都放在data中,data中的数据都会增加gettersetter,会收集对应的watcher
  • v-for 遍历为 item 添加 key
  • v-for 遍历避免同时使用 v-if
  • 区分 computedwatch 的使用
  • 拆分组件(提高复用性、增加代码的可维护性,减少不必要的渲染 )
  • 防抖、节流

用户体验

  • app-skeleton 骨架屏
  • pwa serviceworker

SEO优化

  • 预渲染插件 prerender-spa-plugin
  • 服务端渲染 ssr

打包优化

  • Webpack 对图片进行压缩
  • 使用 cdn 的方式加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • 优化 SourceMap
  • 构建结果输出分析,利用 webpack-bundle-analyzer 可视化分析工具

基础的 Web 技术的优化

  • 服务端 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

----------@----------

Vue项目性能优化-详细

Vue 框架通过数据双向绑定和虚拟 DOM 技术,帮我们处理了前端开发中最脏最累的 DOM 操作部分, 我们不再需要去考虑如何操作 DOM 以及如何最高效地操作 DOM;但 Vue 项目中仍然存在项目首屏优化、Webpack 编译配置优化等问题,所以我们仍然需要去关注 Vue 项目性能方面的优化,使项目具有更高效的性能、更好的用户体验

代码层面的优化

1. v-if 和 v-show 区分使用场景

  • v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块
  • v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS displaynone/block属性进行切换。
  • 所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景

2. computed 和 watch 区分使用场景

  • computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
  • watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的

3. v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  • v-for 遍历必须为 item 添加 key
    • 在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff
  • v-for 遍历避免同时使用 v-if
    • vue2.xv-forv-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性

推荐:

<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
     return user.isActive
    })
  }
}

不推荐:

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>

4. 长列表性能优化

Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了

export default {
  data: () => ({
    users: {}
  }),
  async created() {
    const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};

5. 事件的销毁

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListener 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:

created() {
  addEventListener('click', this.click, false)
},
beforeDestroy() {
  removeEventListener('click', this.click, false)
}

6. 图片资源懒加载

对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vuevue-lazyload 插件

npm install vue-lazyload --save-dev

在入口文件 man.js 中引入并使用

import VueLazyload from 'vue-lazyload'

Vue.use(VueLazyload)

// 或者添加自定义选项
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: 'dist/error.png',
  loading: 'dist/loading.gif',
  attempt: 1
})

vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示

<img v-lazy="/static/img/1.png">

以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址(opens new window)

7. 路由懒加载

Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来

路由懒加载:

const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
})

8. 第三方插件的按需引入

我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例

npm install babel-plugin-component -D

.babelrc 修改为:

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

main.js 中引入部分组件:

import Vue from 'vue';
import { Button, Select } from 'element-ui';

Vue.use(Button)
Vue.use(Select)

9. 优化无限列表性能

如果你的应用存在非常长或者无限滚动的列表,那么需要采用虚拟列表的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list (opens new window) 和 vue-virtual-scroller (opens new window)来优化这种无限列表的场景的

10. 服务端渲染 SSR or 预渲染

服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。

  • 如果你的项目的 SEO首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和 SEO
  • 如果你的 Vue 项目只需改善少数营销页面(例如 //about/contact 等)的 SEO,那么你可能需要预渲染,在构建时简单地生成针对特定路由的静态 HTML 文件。 优点是设置预渲染更简单 ,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin (opens new window) 就可以轻松地添加预渲染

Webpack 层面的优化

1. Webpack 对图片进行压缩

对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片

npm install image-webpack-loader --save-dev
{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000,
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    {
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true,
      }
    }
  ]
}

2. 减少 ES6 转为 ES5 的冗余代码

Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码

class HelloWebpack extends Component{...}

这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:

babel-runtime/helpers/createClass  // 用于实现 class 语法
babel-runtime/helpers/inherits  // 用于实现 extends 语法    

在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小

npm install babel-plugin-transform-runtime --save-dev

修改 .babelrc 配置文件为:

"plugins": [
    "transform-runtime"
]

3. 提取公共代码

如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:

  • 相同的资源被重复加载,浪费用户的流量和服务器的成本。
  • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:

// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: function(module, count) {
    return (
      module.resource &&
      /\.js$/.test(module.resource) &&
      module.resource.indexOf(
        path.join(__dirname, '../node_modules')
      ) === 0
    );
  }
}),
// 抽取出代码模块的映射关系
new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest',
  chunks: ['vendor']
})

4. 模板预编译

  • 当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
  • 预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
  • 如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader (opens new window),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数

5. 提取组件的 CSS

当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存

6. 优化 SourceMap

我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的

SourceMap 的可选值如下(+ 号越多,代表速度越快,- 号越多,代表速度越慢, o 代表中等速度)

  • 开发环境推荐: cheap-module-eval-source-map
  • 生产环境推荐: cheap-module-source-map

原因如下:

  • cheap: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息;
  • module :不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个 Vue 文件报错了,我们希望能定位到具体的 Vue 文件,因此我们也需要 module配置;
  • soure-mapsource-map 会为每一个打包后的模块生成独立的 soucemap 文件 ,因此我们需要增加source-map 属性;
  • eval-source-mapeval 打包代码的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。

7. 构建结果输出分析

Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer

if (config.build.bundleAnalyzerReport) {
  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  webpackConfig.plugins.push(new BundleAnalyzerPlugin());
}

执行 $ npm run build --report 后生成分析报告如下

基础的 Web 技术优化

1. 开启 gzip 压缩

gzipGNUzip 的缩写,最早用于 UNIX 系统的文件压缩。HTTP 协议上的 gzip 编码是一种用来改进 web 应用程序性能的技术,web 服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,zip 压缩效率非常高,通常可以达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右

以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:

npm install compression --save
var compression = require('compression');
var app = express();
app.use(compression())

重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功

Nginx开启gzip压缩

#是否启动gzip压缩,on代表启动,off代表开启
gzip  on;

#需要压缩的常见静态资源
gzip_types text/plain application/javascript   application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;

#由于nginx的压缩发生在浏览器端而微软的ie6很坑爹,会导致压缩后图片看不见所以该选
项是禁止ie6发生压缩
gzip_disable "MSIE [1-6]\.";

#如果文件大于1k就启动压缩
gzip_min_length 1k;

#以16k为单位,按照原始数据的大小以4倍的方式申请内存空间,一般此项不要修改
gzip_buffers 4 16k;

#压缩的等级,数字选择范围是1-9,数字越小压缩的速度越快,消耗cpu就越大
gzip_comp_level 2;

要想配置生效,记得重启nginx服务

nginx -t
nginx -s reload

2. 浏览器缓存

为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)

3. CDN 的使用

浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率

4. 使用 Chrome Performance 查找性能瓶颈

ChromePerformance 面板可以录制一段时间内的 js 执行细节及时间。使用 Chrome 开发者工具分析页面性能的步骤如下。

  • 打开 Chrome 开发者工具,切换到 Performance 面板
  • 点击 Record 开始录制
  • 刷新页面或展开某个节点
  • 点击 Stop 停止录制

----------@----------

Vue与Angular以及React的区别?

Vue与AngularJS的区别

  • Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
  • AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
  • AngularJS社区完善, Vue的学习成本较小

Vue与React的区别

相同点:

  1. Virtual DOM。其中最大的一个相似之处就是都使用了Virtual DOM。(当然Vue是在Vue2.x才引用的)也就是能让我们通过操作数据的方式来改变真实的DOM状态。因为其实Virtual DOM的本质就是一个JS对象,它保存了对真实DOM的所有描述,是真实DOM的一个映射,所以当我们在进行频繁更新元素的时候,改变这个JS对象的开销远比直接改变真实DOM要小得多。
  2. 组件化的开发思想。第二点来说就是它们都提倡这种组件化的开发思想,也就是建议将应用分拆成一个个功能明确的模块,再将这些模块整合在一起以满足我们的业务需求。
  3. PropsVueReact中都有props的概念,允许父组件向子组件传递数据。
  4. 构建工具、Chrome插件、配套框架。还有就是它们的构建工具以及Chrome插件、配套框架都很完善。比如构建工具,React中可以使用CRAVue中可以使用对应的脚手架vue-cli。对于配套框架Vue中有vuex、vue-routerReact中有react-router、redux

不同点

  1. 模版的编写。最大的不同就是模版的编写,Vue鼓励你去写近似常规HTML的模板,React推荐你使用JSX去书写。
  2. 状态管理与对象属性。在React中,应用的状态是比较关键的概念,也就是state对象,它允许你使用setState去更新状态。但是在Vue中,state对象并不是必须的,数据是由data属性在Vue对象中进行管理。
  3. 虚拟DOM的处理方式不同。Vue中的虚拟DOM控制了颗粒度,组件层面走watcher通知,而组件内部走vdomdiff,这样,既不会有太多watcher,也不会让vdom的规模过大。而React走了类似于CPU调度的逻辑,把vdom这棵树,微观上变成了链表,然后利用浏览器的空闲时间来做diff

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/372551.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据挖掘概述

目录1、数据挖掘概述2、数据挖掘常用库3、模型介绍3.1 分类3.2 聚类3.3 回归3.4 关联3.5 模型集成4、模型评估ROC 曲线5、模型应用1、数据挖掘概述 数据挖掘&#xff1a;寻找数据中隐含的知识并用于产生商业价值 数据挖掘产生原因&#xff1a;海量数据、维度众多、问题复杂 数…

直接拿项目运行npm start 会出现’react-scripts’ 不是内部或外部命令,也不是可运行的程序或批处理文件错误

目录 解决方案 原因 解决方案 npm install react-scripts或npm install安装完成后再次运行 npm start 即可 原因 create-react-app有丢包的缺陷&#xff0c;手动安装包后&#xff0c;需要重新npm install一下&#xff0c;这样node_modules/.bin/目录下才会重新出现react-s…

【论文阅读】基于LevelDB的分布式数据库研究

基于LevelDB的分布式数据库研究 基于LevelDB的分布式数据库的研究与实现 - 中国知网 (cnki.net) 实现了什么&#xff1f; 基于键值型NoSQL数据库LevelDB&#xff0c;并与数据一致性算法Raft、 数据分片和负载均衡相结合&#xff0c;设计并实现基于LevelDB的分布式数据库。 主要…

Wireshark “偷窥”浏览器与服务器三次握手

本文使用的是Wireshark 4.0.3, Java 11 编写简易服务器&#xff0c;客户端使用Chrome浏览器移动端开发或是前、后端开发又或是高大上的云计算都脱离不了网络&#xff0c;离开了网络的计算机就是一个孤岛&#xff0c;快速上手开发、背面试八股文固然有些急功近利&#xff0c;但确…

jstatd的启动方式与关闭方式

启动方式与注意事项&#xff1a; 启动方式&#xff1a; 前台启动不打印日志&#xff1a; jstatd -J-Djava.security.policyjstatd.all.policy -J-Djava.rmi.server.hostname服务器IP 前台启动并打印日志&#xff1a; ./jstatd -J-Djava.security.policyjstatd.all.policy -…

傻瓜式minio使用指南

傻瓜式minio使用指南1. docker部署minio1.1 docker拉取minio镜像1.2 创建docker容器1.3 查看docker容器是否启动正常2.登陆minio2.1 账户、密码为原先设置minioadmin2.2 创建桶2.3 设置桶属性3.Java客户端使用3.1引入依赖3.2 使用3.3 结果1. docker部署minio 1.1 docker拉取mi…

你应该知道的ChatGPT提示语

ChatGPT 自上线以来&#xff0c;凭借其优异的自然语言理解和输出能力&#xff0c;仅花 5天就成为了活跃用户过百万的现象级产品。而上一个现象级产品 instagram 花了 2 个半月。到目前为止 ChatGPT 在全球累计用户数量已经过亿&#xff0c;相信现在也有很多人在跟 ChatGPT 聊过…

Acwing 蓝桥杯 第二章 二分与前缀和

今天来补一下之前没写的总结&#xff0c;题是写完了&#xff0c;但是总结没写感觉没什么好总结的啊&#xff0c;就当打卡了789. 数的范围 - AcWing题库思路&#xff1a;一眼二分&#xff0c;典中典先排个序&#xff0c;再用lower_bound和upper_bound维护相同的数的左界和右界就…

Google Guice 4:Bindings(2)

4 Scopes (实例的作用域&#xff09; 4.1 默认规则&#xff1a;unreuse instance 到目前为止&#xff0c;通过bind().to()和Provides定义的binding&#xff0c;每次需要注入实例对象时&#xff0c;Guice都会创建一个新的实例 // 修改DatabaseTransactionLog&#xff0c;使其打…

【python学习笔记】:SQL常用脚本(二)

11、四舍五入ROUND函数 ROUND ( numeric_expression , length [ ,function ] ) function 必须为 tinyint、smallint 或 int。 如果省略 function 或其值为 0&#xff08;默认值&#xff09;&#xff0c;则将舍入 numeric_expression。 如果指定了0以外的值&#xff0c;则将截…

TypeScript笔记-进行中

学习来源&#xff1a; 本笔记由尚硅谷教学视频整理而来 文章目录学习来源&#xff1a;一.TS简介TypeScript是什么TypeScript增加了什么二环境搭建安装nvm环境搭建二.TypeScript中的基本类型类型声明类型类型示例代码三.编译配置自动编译文件自动编译整个项目四.使用webpack打包…

一文掌握如何轻松稿定项目风险管理【静说】

风险管理对于每个项目经理和PMO都非常重要&#xff0c;如果管理不当会出现很多问题&#xff0c;咱们以前分享过很多风险管理的内容&#xff1a; 风险无处不在&#xff0c;一旦发生&#xff0c;会对一个或多个项目目标产生积极或消极影响的确定事件或条件。那么接下来介绍下五大…

在成都想转行IT,选择什么专业比较好?

很多创新型的互联网服务公司的核心其实都是软件&#xff0c;创新的基础、运行的支撑都是软件。例如&#xff0c;软件应用到了出租车行业&#xff0c;就形成了巅覆行业的滴滴;软件应用到了金融领域&#xff0c;就形成互联网金融;软件运用到餐饮行业&#xff0c;就形成美团;软件运…

学渣适用版——Transformer理论和代码以及注意力机制attention的学习

参考一篇玩具级别不错的代码和案例 自注意力机制 注意力机制是为了transform打基础。 参考这个自注意力机制的讲解流程很详细&#xff0c; 但是学渣一般不知道 key&#xff0c;query&#xff0c;value是啥。 结合B站和GPT理解 注意力机制是一种常见的神经网络结构&#xff0…

[计算机网络(第八版)]第二章 物理层(复习笔记)

2.1 物理层的概念 物理层是屏蔽掉传输媒体和通信手段的差异&#xff0c;为数据链路层提供一个统一的数据传输服务&#xff0c;将比特流按照传输媒体的需要进行编码&#xff0c;然后将信号通过传输媒体传输到下一个节点的物理层&#xff0c;并不是指具体的传输媒体。用于物理层…

一文带你看懂:如何进行一次高质量CR?

程序员对代码评审&#xff08;Code Review&#xff09;不可谓不熟悉&#xff0c;而代码评审也已经是许多组织的标准化实践。结合笔者的五年多的开发经验&#xff0c;既有经历过零CR的小组织&#xff0c;也有接触过完善CR规范的大厂团队。对于“如何进行一次--高质量的组内代码C…

力扣-销售员

大家好&#xff0c;我是空空star&#xff0c;本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目&#xff1a;607. 销售员二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.正确示范④提交SQL运行结果5.其他总结前言 …

【测试】自动化测试03(JUnit)

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录JUnit一&#xff09; 注解1. Test2. BeforeEach3. BeforeAll4. AfterEach5. AfterAll二&#xff09; 断言&#xff08;Assertions类&#xff09;三&#xff09;用例的执行顺序四&#xff09;参数化五&#xff09;测试…

Qt windeployqt.exe 打包qml

Qt系列文章目录 文章目录Qt系列文章目录前言一、遇到的坑二、参考前言 我们在QtCreator下面开发程序&#xff0c;一般都会遇到工程发布给客户使用的情况。我们通常会使用Qt自带的打包工具&#xff1a;windeployqt.exe。 windeployqt.exe是Qt自带的工具&#xff0c;用于创建应用…

使用windwow windbg 吃透64位分页内存管理

前言 分页基础概念是操作系统基础知识&#xff0c;网上已经有太多太多了。所以本文记录使用windwow内核调试工具验证理论知识。 具体可以参阅intel volume3的 4.1.1 Four Paging Modes章节。 简而言之&#xff1a;CR0.PG 0表示不开启分页.并且根据CR4各种标志开启不同类别的…