nginx部署

网页请求,静态资源请求,可以看成请求html资源文件,CSS资源文件,JS资源文件。

如果在nginx上设置了二级目录,比如localhost:80/app。将/app的隐射到某个目录文件后,那么/app会请求到这个资源文件目录,假设请求了index.html,此时index.html被正常响应,同时加载对应的js,css文件,此时根据src属性中的路径去请求js,css文件,如果js,css请求路径不是以/app开头的,意味着映射到了其他目录(假如有进行映射),但是正常的一个应用肯定放在一个文件夹里,此时js,css去其他目录请求,要么请求不到,要么不是想要的资源,造成错误。所以一但给web应用设置了二级路径(非根路径),那么该应用绑定的其他资源src,想要映射到该应用目录,必须以这个二级路径开头进行请求。

这也是为什么vue应用中,如果部署的应用设置了二级路径,那么publicPath必须进行同步设置的原因,build时资源会自动加入publicPath设置的路径。

//vue.config.js
module.exports = {
 //根据环境变量设置路径,生产环境下是ztjt,开发环境下是根目录
  publicPath: process.env.NODE_ENV === 'production' ? '/app/' : '/',
}

如果不是通过打包生成的资源,比如ico文件,需要手动配置下,可以使用环境变量BASE_URL,编译打包时会自动进行替换

<link rel="icon" href="<%= BASE_URL %>favicon.ico">

当然可以使用相对路径,不管是几级路径都没有问题,但是vue中前端路由使用history模式时会失败,所以建议不要使用。

nginx配置

location /app/ {
    alias /apps/dist/; # 应用目录
    index index.html index.htm; # 查找顺序
 }

在nginx中有2种映射目录的方式,alias和root

alias会使用配置的应用目录和location配置的虚拟目录进行替换,比如请求/app/js/app.js,实际转到/apps/dist/js/app.js。

而root是将location配置的路径拼接到root配置的目录上

location / {
	root   /apps/dist/;
	index  index.html index.htm;
}

比如请求/js/app.js,会转到/apps/dist/js/app.js路径下寻找。

区别很明显了,alias配置的location允许有虚拟路径,而root对应的location,由于要拼接到真实目录后面,所以root对应的location不能是虚拟路径

vue-router相关配置

vue如果构建的是单页面应用,采用前端路由,比如访问localhost/app/user/1,此时实际获取的还是index.html这个页面资源,所以nginx需要做相应的配置,也就是不管/app后面是什么路径都返回index.html

location /app/ {
    alias /apps/app/; 
    index index.html index.htm; 
    try_files $uri $uri/ /app/; # 配置try_files,如果访问/app/user首先找/app/user这个资源,然后找/app/user/这个目录下的index.html和index.htm,最后找/app/index.html和/app/index.htm,也就是资源找不到最后都会找到应用目录下的index文件
 }

同时如果应用不是部署在根域名上,vue-router也需要做相关配置

// createRouter.js
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes'
export default createRouter({
  routes,
  history: createWebHistory('/app/')  //传入二级路径
})

nginx转发

前后端分离,由于跨域的原因,前端请求数据,需要前端服务器做一层转发,转发到API服务器上。

location ^~/api/ {
        	proxy_set_header Host $host;
        	#proxy_set_header Origin $host;
         	proxy_set_header X-Real-IP $remote_addr;
         	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         	rewrite ^/(api)/(.*)$ /hello/$1/$2 break; 
        	proxy_pass http://localhost:8989;
        }

module

目前主要有2种模块化标准,commonjs和es6 modules

commonjs

commonjs采用module.exports导出,使用require导入,可以导出多个值,也可以一个值,还可以导出匿名对象,require引入的时候通过对象接收

// 导出一个值
module.exports = var1
let var1 = require('path')
 
// 导出多个值
module.exports = {var1,var2,var3} 
const var = require('path') // 获得整个对象 var.var1,var.var2
const {var1,va2,var3} = require('path') //解构赋值获得导出内容

如果导出多个值,还可以使用exports

exports.var1 = var1
exports.var2 = var2
exports.var2 = var3
let var = require('path')
const {var1,va2,var3} = require('path')

模块解析策略:

这里只讨论webpack环境下,不同环境下的实现不同,而且webpack也有大量配置来修改解析规则,这里只针对一般配置。比如vuecli4构建的应用有很多默认配置,位于node_modules\@vue\cli-service\lib\config\base.js路径下:

.modules
    .add('node_modules')
    .add(api.resolve('node_modules'))
    .add(resolveLocal('node_modules'))
    .end()
    .alias
    .set('@', api.resolve('src'))

NodeJS的模块解析策略如下:

当遇到 require(X) 时,按下面的顺序处理:

(1)如果 X 是内置模块(比如 require(‘http’))   a. 返回该模块。   b. 不再继续执行。

(2)如果 X 以 ”./” 或者 ”/” 或者 ”../” 开头   a. 根据 X 所在的父模块,确定 X 的绝对路径。   b. 将 X 当成文件,依次查找下面文件(webpack中可配置),只要其中有一个存在,就返回该文件,不再继续执行。

X
X.js
X.json
X.node

  c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。

X/package.json(main字段)
X/index.js
X/index.json
X/index.node

(3)如果 X 不带路径

​ 比如:/home/ry/projects/foo.js 执行了 require(‘bar’)

  a. 根据 X 所在的父模块,确定 X 可能的安装目录。

# 每一层父级目录寻找node_modules
/home/ry/projects/node_modules
/home/ry/node_modules
/home/node_modules
/node_modules

  b. 依次在每个目录中,将 X 当成文件名或目录名加载。

依次在上面的node_modules中按文件、目录的顺序查找

# 首先在node_modules目录中寻找下面类型的文件
bar
bar.js
bar.json
bar.node
# 如果上面的没找到,则把bar当成目录,在这个目录中按下面的顺序查找
bar/package.json(main字段)
bar/index.js
bar/index.json
bar/index.node

(4) 抛出 “not found”

webpack下的commonjs基本采用node解析策略,然后配合一些设置完成。


es6 module

es6采用export导出,import引入

export var1
export var2
export var3
 
import * as var from 'path' // 包装成var对象,通过var.var1,var.var2
import {var1,var2,var3} from 'path' // 解构赋值

只要使用export导出,就必须要按照上述方法之一引入。同样可以导出匿名对象

export default function fn(){}
import xxx from 'path' // import 一个对象,那么导出的文件必须要有export default匹配

如果即包含普通的export,又包含default,那么导入时,default就代表export default的那个属性。

export var1
export default function fn(){}
import * as var from 'path' // var.var1,var.default
import {var1,defautl} from 'path' 

如果不需要导入模块的任何内容,仅仅是希望执行一下导出的文件

import 'path' // 执行导入的文件

模块解析策略:

浏览器环境下的import会按照路径进行解析,就是普通的HTML规范,比如

import 'vue' //实际就等于 import './vue',会去指定路径下找vue这个文件,注意是vue,不会自动变成vue.js

webpack下的解析,webpack下es6 module实际就是commonjs的语法糖,最后都会转成commonjs,所以解析策略同commonjs。

webpack的运行环境为NodeJS,所以使用webpack时实际上都会按照node解析策略去寻找模块。

npm包管理

npm install #下载模块放入node_modules,但是信息不会写入package.json,也就是发布后别人通过npm install不会下载这个依赖
npm install --save #写入package.json,dependencies
npm install --save-dev #写入package.json,devDependencies

配置全局资源

比如有个全局主题样式等,不可能每个地方都手动引入,可以使用style-resources-loader

// vue.config.js
const path = require('path')
module.exports = {
  chainWebpack: config => {
    const types = ['vue-modules', 'vue', 'normal-modules', 'normal']
    types.forEach(type => addStyleResource(config.module.rule('less').oneOf(type)))
  },
  publicPath: process.env.NODE_ENV === 'production' ? '/ztjt/' : '/',
}
 
function addStyleResource(rule) {
  rule
    .use('style-resource')
    .loader('style-resources-loader')
    .options({
      patterns: [path.resolve(__dirname, './src/assets/styles/theme.less')],
    })
}
 

配置路径别名

chainWebpack: config => {
    config.resolve.alias
      .set('components', path.join(__dirname, 'src/components'))
      .set('assets', path.join(__dirname, 'src/assets'))
  }

prettier配置

ESlint用于规范语法,Prettier用于规范格式。

// .prettierrc
{
  "printWidth": 120,
  "tabWidth": 2,
  "singleQuote": true,
  "semi": false,
  "arrowParens": "avoid"
}
 

视口的概念

  • 布局视口,layout viewport

    设计的页面会在屏幕下的虚拟画布上先渲染,然后显示在屏幕上,如果布局视口大于屏幕分辨率,那么显示不全。

    在移动端上布局视口总是包裹html的,但是PC端则不是。

    移动端为了展示PC端网页,所以浏览器的布局视口一般默认为980px或1024px。

  • 视觉视口,visual viewport

    用户看到的网页区域,布局视口大小是固定的,视觉视口是可变的,如果放大视觉视口,那么看到更多的布局视口内容(缩小网页),缩小视觉视口,看到更少的内容(放大网页)。

    PC端布局视口和视觉视口是一样的(滚动条差异),但是html可能大于视口。

  • 理想视口,ideal viewport

    在移动端出现后,需要匹配的分辨率太多,出现了理想视口的概念,即布局视口 = 设备宽度 = 视觉视口(禁止缩放)

逻辑像素

更具体的说明见《windows和浏览器缩放问题.md》

组合式API

基础

Vue2中只能通过选项式API,不管是不是相关联的逻辑全部分散在各种API属性中,data,computed,method...,造成一个业务逻辑到处都有,维护起来非常不方便。vue3中开始使用组合式API,可以将相关逻辑全部封装到一起,组合式API需要在setup(props)函数中使用,参数有2个,props,context,这个方法是在beforeCreated之前调用,意味着此时没有组件实例,所以无法持有this,也就是除了props之外其他都 不能访问。从setup(props)返回的任何内容,都可以暴露给组件的其他部分,比如模板、方法、计算属性、钩子函数等。

setup(props) {
    console.log(props.msg)
    let arr = ref([])
    function fn() {
      arr.value.push(1)
    }
    return { arr, fn }
  },

通过vue导出的ref方法,包裹属性,以达到响应式,这和传值、传引用无关,数组不加ref同样无法响应到模板(修改数组的值,因为引用类型的原因,组件持有的这个数组值确实改变了,但是无法响应式的渲染到模板),因为响应式原理是通过proxy代理了对象,所以要想某个对象是响应式的必须经过proxy包装。(vue3用proxy重写了响应式,proxy是ES6的新特性)

ref方法会返回一个对象,并将包裹的值赋值给该对象的value属性,同时进行响应式包装

生命周期钩子函数

setup中可以使用生命周期钩子函数,通过vue导出,命名方式为onMouted,onCreated....

这些方法接收回调函数,但生命周期执行时,触发钩子函数,执行回调。

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

Provide/Inject

用于解决跨组件传递数据,vue2中常规下只能父传子,要实现跨多个组件传递,比如祖传孙,只能使用hack手段,比如总线bus。或者使用state。

vue3中可以通过一组Provide/Inject实现,接受方不需要知道是谁提供的,提供方也不需要知道谁使用了

选项式API使用:

//提供数据的组件
data() {
      return {
        info2: { key: '123', obj: { count: 0 } },
        str: 'abc',
        arr: [1, 2, 3],
      }
    },
    provide() {
      return {
        info: this.info2,
      }
    },
//接受数据的组件
inject: ['info'],

注意:只有提供data属性中的对象给inject时,响应式才有效,因为data中的对象也是proxy,传递给inject时,inject获得对象的引用,也就是获得了这个proxy,使用时会正常响应。

但是提供props中的属性或者props/data中的基本类型时无法转换响应式。必须借助相关的Composition API。

组合式API中使用:

provide

setup() {
      const num = { num: 0 }
      const reactiveNum = reactive(num)
      const readonlyNum = readonly(reactiveNum)
 
      function updateNum() {
        reactiveNum.num += 1
      }
 
      provide('num', num) // 非响应式
      provide('num', reactiveNum) // 响应式 inject可修改
      provide('num', readonlyNum) // 响应式 inject不可修改,此时可将修改方法,一起提供,数据流更清晰
      provide('updateNum', updateNum)
 
      return { readonlyNum }
    }

inject

setup() {
      const num = inject('num',  () => ({ num: 0 }) ) //和props一样默认值如果是对象、数组,必须使用工厂函数返回
      const updateNum = inject('updateNum')
      return { num, updateNum }
    }

最好提供一个包裹响应式对象的只读代理,并在需要时提供修改方法,修改被包裹的响应式对象,只读代理对应也会同步触发响应式。

watch

setup中同样可以使用watch

setup(props) {
    // 使用toRefs,创建对props中的count属性的响应式引用
    let { count } = toRefs(props)
    let arr = ref([])
    function fn() {
        arr.value.push(1)
    }
    onMounted(fn)
    watch(count, (newValue, oldValue) => {
        console.log('oldValue is : ' + oldValue + ' ,newValue is : ' + newValue)
    })
    return { arr, fn }
},

为了能监听到count的变化,需要toRefs转化props。

props本身是响应式的,但是解构赋值后,解构到的对象会消除响应(没有了proxy代理),所以上面需要toRefs

这里需要注意,props都是从其他组件传过来进行赋值的,本组件无法赋值,否则数据的流向就会混乱,所以props在传到setup时应该做了特殊处理,本身是响应式的,接收传值时会实时渲染,但是传到setup后里面的内容单独都不是响应式的,因为没有赋值需求,只有读取需求。

注册为watch监听后,被监听对象,实际是通过getter方法将watch收集为这个对象的依赖,所以watch对象必须属于某个响应式对象,否则无法注册,如果上面直接监听props.count(可以通过函数返回值来实现),实际上这个watch就没订阅上去,所以props.count发生改变了,不会调用后面的回调函数,如果转换为响应式对象后,订阅成功,只要触发监听对象的set方法就可以触发回调,而基于一个target封装的响应式对象其实是一个引用,所以toRefs后的count和在组件中的count是一个响应式对象,传值时触发了组件中的count的set方法,所以会触发watch。

深入:

watch函数第一个参数source可以有3种类型,reactive响应式对象,函数,目标数组(侦听多个目标),如果直接是reactive响应式对象,deep选项默认为true,深度侦听,如果是函数,需要手动开启deep;

首先监听的对象必须是响应式的,如果使用函数式参数,那么函数的返回值必须能够**触发getter**,才能被收集为依赖。

赋值newVal.oldVal的过程就是把监听对象在setter之前和之后的值分别赋予他们。

监听source不是方法时,直接获取source在setter前后对应的值进行赋值

监听source是方法时,获取setter前后方法的返回值分别赋值

所以source实际有2个作用,收集依赖,赋值newVal,oldVal

监听source为方法时,有以下几种情况:

  • 返回被监听的reactive对象

    const state = reactive({ 
      id: 1, 
      attributes: { 
        name: "",
      },
    });
     
    watch(
      () => state,
      (state, prevState) => {
        console.log(
          "not deep ",
          state.attributes.name,
          prevState.attributes.name
        );
      }
    );
     

    注意监听source是一个对象时,会出现newVal 和 oldVal一样,因为赋值newVal和oldVal就是简单的直接赋值,如果对象是引用类型,赋值实际传的地址,2者指向同一个地址,所以最后newVal === oldVal

    watch(
        () => obj,
        (newVal, oldVal) => {
            console.log(newVal === oldVal) //true
        }
    )
  • 被监听对象的副本

    副本读取的过程会触发getter

    如果要deep侦听,并且oldVal正常返回,需要深拷贝,原因见上。

    const numbers = reactive([1, 2, 3, 4])
     
    watch(
      () => [...numbers],
      (numbers, prevNumbers) => {
        console.log(numbers, prevNumbers);
      })
     
    numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]
  • 被监听对象的属性

    正常无法监听state.value,因为不是响应式对象,但是通过函数返回后,state.value触发了getter

    const state = ref(0)
    watch(
        () => state.value,
        (count, prevCount) => {
            console.log(count, prevCount)
        }
    )
     

watch可以通过数组,侦听多个数据源。

const firstName = ref('');
const lastName = ref('');
 
watch([firstName, lastName], (newValues, prevValues) => {
  console.log(newValues, prevValues);
})
let obj1 = reactive({
        name: 'bob',
        age: 18,
      })
let obj2 = ref({
    name: 'bob',
    age: 18,
})
watch(
    [obj1, obj2],
    (newVal, oldVal) => {
        console.log(newVal, oldVal)
    },
    {
        deep: true,
    }
)

watchEffect

副作用函数

let num = ref(0)
const numRef = computed(() => num.value * 2)
function computedNum() {
    num.value++
}
watch(numRef, (v1, v2) => {
    console.log(v1, v2)
})
watchEffect(() => {
    console.log(numRef.value)
})

watchEffect会立即执行传入的方法,并响应式收集依赖,获取属性时,触发Proxy的getter方法,getter方法会收集依赖,setter值时通知依赖,重新执行该方法

可以显示的停止,或者等待组件卸载unMounted时自动停止

const stop = watchEffect(() => {
    console.log(numRef.value)
})
stop() //停止

watchEffect一般用在异步的时候较多,这就可能造成一个问题,当进行异步任务时,异步还未完成,状态却改变了,比如根据userId获取用户信息,在请求过程中userId发生改变了,又会触发,那么会造成多次的请求数据,返回数据的顺序也不稳定,所以最好是在userId改变时,先清除上一次的操作。

 let timer = null
 const stop = watchEffect(async onInvalidate => {
     console.log('numRef:' + numRef.value)
     await new Promise(resolve => {
         timer = setTimeout(() => {
             console.log('async operate ' + numRef.value)
             resolve()
         }, 3000)
     })
     onInvalidate(() => {
         clearTimeout(timer)
         console.log('onInvalidate run...' + numRef.value)
     })
 })

通过传入一个onInvalidate参数,该参数接收一个回调函数,onInvalidate执行的时机是,watchEffect即将被重新执行时,或者组件卸载时,这和await无关,从代码逻辑看,他应该在await之后调用,但是实际上重新执行时就调用了。

watchEffect默认是在组件update之前执行

如果需要在组件更新后运行,可以传递一个flush参数,默认时'pre'

// 在组件更新后触发,这样你就可以访问更新的 DOM。
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'post'
  }
)

watch也可以停止侦听、回调函数中传入onInvalidate参数,以及options,控制刷新时机以及调试,并增加了deep,imediate属性

computed

const reCount = computed(() => props.count * 2)

computed, 函数返回值必须是能触发getter的,才能生效,而且返回的值也是响应式的,相当于用ref包装了。

setup能做什么

setup本身就是一个函数,和组件解耦的函数,意味着可以将功能抽到其他js文件中,然后这里引入。

为什么vue2中不能抽取功能到单独的js中呢,因为vue2没有单独提供各种组合API,比如watch、computed、钩子函数,无法完成一个逻辑的完整抽取。

所以setup本身只是一个入口,Composition API才是核心。

这样我们可以实现独立于组件的业务逻辑复用,就像我们复用组件一样的方便。

而抽取出去的js文件,一般需要暴露返回的属性,以及这些属性的操作方法,实际就是使用了闭包,将属性的作用域延申到了外部。

为什么不在操作方法中直接返回这个属性呢,那样每次调用方法得到的必然是最新的值,这就和响应式没什么关系了,无法使用响应式的一些功能,造成性能下降,同时会带来新的问题,如果使用{{fn()}},只要重新渲染了这里也会触发。

响应式怎么触发

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <button @click="updateuser">updateuser</button>
    <button @click="updateusers">updateusers</button>
    <div v-for="(obj, key) in users" :key="key">
      <p v-for="(value, k) in obj" :key="k">{{ k }}-{{ value }}</p>
    </div>
    <p>---------------------------</p>
    <p v-for="(value, key) in user" :key="key">{{ key }}-{{ value }}</p>
  </div>
</template>
 
<script>
  import { reactive } from 'vue'
  export default {
    name: 'HelloWorld',
    props: {
      msg: String,
    },
    setup() {
      const user = { name: 'bob', age: 18 }
      const users = reactive({
        user,
        other: {
          name: 'alice',
          age: 16,
        },
      })
      function updateuser() {
        user.age = 28
      }
      function updateusers() {
        // users.user.age = 30
        user.age = 28
        users.other.age = 36
      }
      return { user, users, updateuser, updateusers }
    },
  }
</script>
 
<style scoped lang="less">
  h1 {
    color: @primary-color;
  }
</style>
 

user是一个普通js对象,users经过reactive包装后变成响应式,此时实际是一个Proxy实例,对这个实例进行赋值会触发响应式,组件重新渲染。由于users包含了对user的引用,user的值确实改变了(引用类型原因),但是没有响应式,组件上user显示更新后的数据,是因为users的操作触发了组件渲染,重新渲染获取了新的user值。如果注释users.other.age = 36就能发现,界面什么都没变,只是内存里的值变了,除非有其他操作能触发组件重新渲染,否则内存的值到不了界面上。

响应式的本质就是收集依赖,在调用setter时驱动依赖更新

ref vs reactive

reactive和ref包裹的对象里所有嵌套对象都会变成响应式的。并且后续给对象添加的内容也是响应式的

const users = reactive({
        user,
        other: {
          name: 'alice',
          age: 16,
        },
    	num:0
      })

此时other也是响应式的proxy实例

ref如果包裹对象

const obj = { name: 'bob', age: 18 }
const user = ref(obj)
user.value === reactive(obj) // true

而且ref包裹会有一个好处,可以重新分配value 的值,分配的值也是响应式的。而reactive返回的对象,如果重新分配就是另外一个对象了,脱离原有的proxy实例(除非使用一个property对应整个对象,就像上面users中other例子,然后给这个property赋值)。

ref还一个作用是转化基本类型ref(0),ref('str')为响应式。可以理解为给基本类型放在了一个有value属性的对象中,并进行了reactive。

toRef是对对象某个属性进行ref转换,toRefs对对象所有属性进行ref转换

ref对象被模板渲染时会自动拆包,但是如果时嵌套的ref对象,必须用value指定属性。


实际上:

通过ref,reactive转换的对象,会自动遍历,将引用的所有对象转换为响应式,但是基本类型的值,在这个对象的环境下也就是this下是响应式的,这是这个对象的响应,不是这个基本类型的值是响应的,如果把这个基本类型的值赋值出去,或者解构出去,传某个变量,这个变量只是单纯的拥有这个值,不是响应式的。

比如上面的例子,users的num在users的环境下修改,是响应式的,因为users是响应式的,此时针对的是users的属性修改,如果把num解构、赋值出去,只是把0传了出去,接收的变量已经和users没有关系了,不是响应式的。

但是把other解构、赋值出去,接收的对象依然是响应式的,因为只是把地址传给了其他变量,本身依然还是个Proxy实例。

有个特殊的就是props,props本身是响应式的,但是props里任何类型的值解构、赋值出去都会失去响应式。

根据测试:props本身被转换成了proxy,并对所有的属性,包括嵌套的属性进行了getter,setter监听,一但修改,触发渲染。但是props持有的对象并没有转化成proxy,也就是失去了props的环境就失去了响应式。

// ref,reactive
const proxy = {
    a: proxy,
    b: 0,
    c: {
        d: proxy,
        e: 0,
    },
}
// props
const proxy = {
    a: {},
    b: 0,
    c: {
        d: {},
        e: 0,
    },
}

data

data可以简单理解为和reactive效果相同。

注意data被加载时自动扫描成reactive对象,但是如果data中的属性引用了其他地方的值或对象,此时修改不会触发响应式,除非引用的对象本身就是响应式的

其他常用API

  • toRaw 返回reactive,readonly代理的原始对象
  • readonly 返回普通对象、reactive、ref的只读代理(普通对象、reactive针对原始target封装了只读代理,ref由于包装了层value,所以并不能算严格意义上的原始对象,但是也是只读的)
  • isProxy,isReactive,isReadonly 判断是否为代理,是否为响应式代理,是否为readonly创建的代理
  • shallowReactive 只跟踪自身属性的响应式,既不管嵌套
  • shallowReadonly 自身的属性变为只读,嵌套对象不管
  import { reactive, readonly, ref, toRaw } from 'vue'
  export default {
    setup() {
      // reactive对象toRaw
      const obj = { key: 1 }
      const reactiveObj = reactive(obj)
      const reverseObj = toRaw(reactiveObj)
      console.log(obj === reverseObj) // true
 
      // ref对象toRaw,实际还是通过value获取proxy,然后转化为原始target
      const refObj = ref(obj)
      const reverseRefObj = toRaw(refObj.value)
      console.log(obj === reverseRefObj) // true
 
      // 基本类型的ref toRaw,没有意义,直接.value获得即可
      let num = 0
      const refNum = ref(num)
      const reverseRefNum = toRaw(refNum.value)
      console.log(reverseRefNum) // 0
 
      // readonly 返回普通对象,reactive对象,ref的原始对象的只读代理
      console.log(readonly(obj))
      console.log(readonly(reactiveObj))
      // 数组同样无法操作
      const arr = [1, 2, 3, 4]
      const arrReadonly = readonly(arr)
      arrReadonly.push(5) // failed
      // readonly 是深层次的,所以value无法重新赋值,obj也无法重新赋值
      const refObjReadonly = readonly(refObj)
      refObjReadonly.value = { name: 'a' } // failed
      refObjReadonly.value.key = 2 // failed
      // 基本类型的ref
      const refNumReadonly = readonly(refNum)
      refNumReadonly.value = 1 // failed
      return {}
      // shallowReactive 第一层响应式
      // shallowReadonly 第一层只读
      // isProxy isReactive isReadonly 判断是否为代理,是否为响应式代理,是否为readonly创建的代理
    },
  }

模板引用

通过在template可以使用ref获取当前的dom或组件实例。

如果template中的ref对应setup中的ref,那么会将ref绑定的dom或组件实例分配给setup中的ref value。

<test-item ref="item"></test-item>
.....
const item = ref(null)
onMounted(() => {
console.log(item.value.$el // #text
})

v-for中使用ref:

	<div
      v-for="n in 5"
      :key="n"
      :ref="
        (el) => {
          if (el) divs[n] = el
        }
      "
    ></div>

可以使用自定义函数接受一个dom或者组件实例。这个用法和vue2中没有什么区别。

mixin

有了composition API mixin实际没有什么用处了。

内置组件teleport

应用采用单组件树的形式构建,可能存在某个模板处于很深的dom节点中,如果要做一些样式渲染,比如position:absolute,这对父节点有要求,否则可能错乱。常规做法就是一层层的检查,调整。但是teleport组件可以快速将一些模板代码发送到指定的dom节点下渲染,避免了一层层检查的痛苦。

<teleport to="body">
    <div v-if="modalOpen" class="modal">
        <div>
            I'm a teleported modal! 
            (My parent is "body")
            <button @click="modalOpen = false">
                Close
            </button>
        </div>
    </div>
</teleport>

to属性的值需要是css选择器。

注意:teleport并不改变组件逻辑,发送出去的模板内容在逻辑上依然没有变,不管teleport to到哪个dom节点。这点可以从devtools上查看,如果teleport下还有其他组件,devtools上这个组件依然属于teleport所在的组件。

多个teleport使用,比如多个teleport的to都是body,并不会覆盖,就是简单的追加在目标节点下。

插件

自定义插件如果是一个对象,则安装时执行install属性对应的方法,如果是一个方法,则直接执行这个方法,该方法接受app实例为第一个参数

export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = key => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
  }
}

安装,使用app.use安装,第一个参数为插件,第二个参数可选,根据插件需要

const app = createApp(App)
app.use(myPlugin, {
  greetings: {
    hi: 'Hallo!'
  }
})

使用,插件的使用首先需要组件能够访问,vue2中使用protoType原型对象来完成。vue3使用app.config.globalProperties注册全局属性。使用时可以通过this直接访问,查找顺序为先在本组件中寻找,如果没有就去app.config.globalProperties中寻找。

通过this可以直接获取这个属性,但是setup中没有this,可以通过getCurrentInstance()返回当前实例,但是无法通过这个实例直接读取globalProperties

getCurrentInstance().appContext.config.globalProperties  // 获取app上注册的属性

注意:getCurrentInstance()只能在setup和生命周期钩子函数中使用。this是个代理对象,getCurrentInstance()返回的是原始实例。

emits选项

emits可以是数组或者对象,包含的信息是本组件自定义的事件

emits:['check','click'] //实际就是this.$emit('check',args) 在这里提前声明

如果是对象,key为事件名,值可以是验证方法

// 对象语法
app.component('reply-form', {
  emits: {
    // 没有验证函数
    click: null,
 
    // 带有验证函数
    submit: payload => {
      if (payload.email && payload.password) {
        return true
      } else {
        console.warn(`Invalid submit event payload!`)
        return false
      }
    }
  }
})

父组件中绑定的属性和方法,不在emits和props中的,如果inheritAttrs: true则在根节点上绑定,如果inheritAttrs: false,则可以通过$attr访问。

组件实例方法

$watch,使用方法类似选项watch

$emit,触发事件,常用于自定义事件的发射

$forceUpdate,强制组件重新渲染,影响范围为实例本身以及放入实例插槽内的内容

$nextTick,确保组件完成更新后,调用回调

路由

单页面应用,需要使用前端路由,这里使用官方路由vue-router。

应用由根组件生成,所以根路由(/开头的path)必然是渲染到根组件上。

安装

cnpm i --save vue-router@4  # 安装依赖
// 配置路由 routes.js
import HelloWord from '@components/HelloWorld'
const routes = [
  {
    path: '/hello',
    component: HelloWord
  }
]
 
export default routes
// 导出全局路由实例 createRouter.js
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes'
 
export default createRouter({ history: createWebHistory(), routes })
// 挂载实例,让app应用支持路由 main.js
import router from '@router/createRouter'
 
const app = createApp(App)
app.use(router)

访问路由

可以在所有组件中使用

this.$router // 访问全局路由实例
this.$route // 获得当前路由
getCurrentInstance().appContext.config.globalProperties.$route //setup 中使用
// setup中可以使用相关API获得
useRouter() // 或者使用Composition API,从vue-router中导入
useRoute()

动态路由

URL参数匹配,和Spring中Controller获取pathVariable一样

const routes = [
  {
    path: '/hello/:id', // :id表示URL参数
    component: HelloWord
  }
]
// 组件中可以获得参数,比如:/hello/12345
<p>{{ $route.params }}</p>  // {id:12345}

可以使用多个参数:/hello/:Id/arg/:arg

动态路由区分

# 都只有一个参数,系统无法区分/hello/1,和/hello/bob之间的区别,会匹配到一个path上面去
/hello/:id
/hello/:name
  • 解决方案1:

    前面加静态路径 /hello/id/:id,/hello/name/:name。简单实用,但是有时候确认不太想加

  • 解决方案2:

    可以在参数后面使用(regExp),括号里面跟正则表达式来限定参数

    /hello/:id(\\d+) # 匹配数字
    /hello/:name([a-zA-Z]+) # 匹配字母

可重复的参数

/hello/:id(\\d+) # 可以匹配/hello/1,但是无法匹配/hello/1/2
/hello/:id(\\d+)* # 可以匹配/hello,/hello/1,/hello/1/2... 

* 表示0个或多个

+表示1个或多个

?表示0个或1个,意味可选参数

可重复参数,传入params,必须以数组的形式

this.$router.push({name:'hello',params:{id:[1,2,3]}}) // path:/hello/1/2/3

Get参数

/hello/1234?name=www

可以通过this.$route.query获得

<p>{{ $route.params.id }}</p>
<p>{{ $route.query }}</p> //{name:'www'}

嵌套路由

可以嵌套多层路由,定义路由时,在需要嵌套的路由下使用children属性,值是路由数组,和根路由一样。

{
    path: '/user/:id(\\d+)',
        name: 'user',
            component: User,
                children: [
                    {
                        path: ':name([a-zA-Z]+)+',
                        name: 'profile',
                        component: Profile
                    }
                ]
}

嵌套路由出现时,父路由必须持有router-view组件,否则无法渲染。

如果url为/user/1,那么子路由对应的组件将无法渲染,因为匹配不到,如果想子组件也匹配上来,可以使用一个空白path:

children: [
    {
        path: '',
        name: 'profile',
        component: Profile
    }
]

或者使用可选参数的形式:

children: [
    {
        path: ':name([a-zA-Z]+)*', // * 可重复,?不可重复
        name: 'profile',
        component: Profile
    }
]

编程式导航

push

除了可以使用router-link组件,配合to属性实现路由跳转,还可以使用编程式导航,实际router-link调用的就是this.$router.push方法

注意:使用path时,动态参数必须拼接在path后面

 setup() {
      const router = useRouter()
      let id = 1
      let name = ['bob', 'alice', 'foo']
      const userRouteWithName = () => {
        // 使用name,可以单独设置params和query
        router.push({ name: 'user', params: { id }, query: { age: 18 } })
      }
      const userRouteWithPath = () => {
        // 使用path,param必须拼接在path上,可以单独设置query
        router.push({ path: '/user/' + id, query: { age: 30 } })
      }
      const profileRouteWithName = () => {
        router.push({ name: 'profile', params: { name } })
      }
      const profileRouteWithPath = () => {
        // 对于只有参数的子路由,必须要用name,path为空无法生效
        router.push({ path: '', params: { name } })
      }
      return { userRouteWithName, userRouteWithPath, profileRouteWithName, profileRouteWithPath }
    }

router-linkto属性和push接收的参数完全一致。

push返回的是一个promise

replace

使用方法同push,唯一不同的是不会留下histroy记录,相当于替换了当前的路由

也可以在push中直接使用

router.push({ name: 'user', params: { id }, query: { age: 18 },repalce : true })
// 效果同下
router.replace({ name: 'user', params: { id }, query: { age: 18 } })

< router-link to="path" replace >中也可以使用

go

类似前进、后退的功能,go中可以传递整数,跨多个历史记录导航

router.go(-1) === router.back()
router.go(1) === router.forward()

命名视图

如果一个组件有多个router-view就需要命名,没有命名的拥有default名称

使用component则直接对应默认视图

使用components则可以自行配对

export default [
  {
    path: '/hello',
    name: 'hello',
    // 放入default
    component: HelloWorld
  },
  {
    path: '/user/:id(\\d+)',
    name: 'user',
    // 放入default
    components: {
      default: User
    },
    children: [
      {
        path: ':name([a-zA-Z]+)+',
        name: 'profile',
        component: Profile
      }
    ]
  }
]

重定向

将路由重定向到另一个路由

  {
    path: '/',
    // 由于直接重定向了,当前实际没有组件对应,所以不需要component选项
    redirect: { name: 'hello' }
  }

redirect可以是一个函数

{
    path: '/search',
    name: 'searchQ',
    components: {
      content: Search
    }
  },
  {
    path: '/search/:searchText',
    name: 'searchP',
    components: {
      content: Search
    },
    // redirect可以是一个方法,接受当前路由为参数
    redirect: to => {
      // 可以将url参数转换为query参数
      return { name: 'searchQ', query: { searchText: to.params.searchText } }
    }
  }

路由别名

{
    path: '/',
    redirect: { name: 'hello' },
    // 访问/index就是访问/
    alias: '/index'
  },
{
    path: '/user/:id(\\d+)',
    name: 'user',
    // 放入default
    components: {
      default: User
    },
    children: [
      {
        path: ':name([a-zA-Z]+)+',
        name: 'profile',
        component: Profile,
        // 访问/1/a 就直接到这个子路由了,可以用数组表示多个别名
        alias: ['/:id/:name']
      }
    ]
  },

注意,如果路由有路径参数,必须在别名中表明

路由props

可以给渲染在router-view中的组件传参 如果是通过component渲染到默认router-view

  • props:true ,将路径参数传递给组件对应的props
  • props:{attr:attrValue},将静态属性attr传递给组件对应的props
  • props:route { return {}},将返回的对象解构赋值传递给组件对应的props

如果是通过componets指定view name

  • props:{defautl:true},给对应的view设置布尔,然后决定是否将params传给渲染到这个router-view的组件
  • props:{default:{attr:attrValue}},传静态值
  • props:{default:route{return {}}},传返回值

props的键和components的键是一样 的。

{
    path: '/user/:id(\\d+)',
    name: 'user',
    components: {
      content: User
    },
    // props: { content: true },
    props: { content: { sex: 'man' } },
    children: [
      {
        path: ':name([a-zA-Z]+)+',
        name: 'profile',
        component: Profile,
        alias: ['/:id/:name']
      }
    ]
  },
  {
    path: '/search',
    name: 'searchQ',
    component: Search,
    // props: true
    props: route => {
      return { query: route.query.searchText }
    }
  }

注意:使用方法传递props,方法只接受route,所以不要和其他有状态的值耦合,因为只有route改变才能重新触发

路由导航守卫

vue-router4中,导航守卫大部分可以接收2-3个参数

2个参数的情况,to,from,to目的路由,from离开的路由。通过return返回boolean或push参数类型的对象,完成守卫的执行

3个参数的情况,to,from,next,next是一个传递方法,类似java servelet中的filter.chain,执行守卫链中的下一个。next必须被执行最多一次,可以接收一个push参数类型的对象作为参数。

如果使用了redirect,实际导航会以redirect那个导航生效,所以守卫也只会在redirect那个导航生效

全局导航

  • router.beforeEach全局前置守卫,当导航被触发时执行

    // 全局前置守卫,导航触发时执行。可以使用return false取消导航,如果没有返回值undefined或者return true就正常执行。还可以 return {} ,对象内容就是push参数,用于改变导航
    // 也可以使用next(),完成导航相当于return。注意next在一次守卫执行中只能被调用一次。next同样可以传push参数
    // 以return 或 next 的执行作为守卫执行的完成标志,意味着可以执行下一个守卫了,不管中间还有没有异步任务
    router.beforeEach(async (to, from, next) => {
      await new Promise(resolve => {
        console.log('first beforeEach guard')
        resolve()
        /* setTimeout(() => {
          if (to.name === 'error') resolve()
          else throw new Error('some error message')
        }, 100) */
      })
      next()
    })
     
    router.beforeEach((to, from, next) => {
      console.log('second beforeEach guard')
      next()
      /* if (to.name === 'error') next()
      else throw new Error('some error message') */
    })
  • router.beforeResolve全局解析守卫,解析守卫是所有导航以及异步组件都解析完后才会被调用,此时导航基本已被确认,等待resolve守卫执行完导航就被真正确认。

    // 全局解析守卫,会在所有前置守卫、组件内守卫、异步路由组件都解析完后执行
    router.beforeResolve(to => {
      console.log(to.path + ' before resolve')
    })
  • router.afterEach全局后置钩子,导航完后执行

    // 全局后置钩子,导航完成后执行,没有next参数,可以接收failure作为第三个参数
    router.afterEach(to => {
      console.log(to.path + ' after each')
    })
  • router.onError注册一个全局导航错误handler,导航中出现未被捕获的错误时会调用

    // 守卫中出现错误,会进行捕获
    router.onError(error => {
      router.push({ name: 'error', params: { error } })
    })
     

路由独享守卫

beforeEnter进入路由时触发,注意改变参数并没有改变路由,比如/user/1和/user/2之间的导航是不会触发beforeEnter。

可以传入一个方法数组,便于复用

  {
    path: '/user/:id(\\d+)',
    name: 'user',
    components: {
      content: User
    },
    beforeEnter: [routeOperate1, routeOperate2],
  }

组件守卫

  • beforeRouteEnter渲染该组件的路由在被确认前调用,此时组件还没有创建无法访问this,不过可以通过next传入一个回调函数,函数会接收一个this作为参数
  • beforeRouteUpdate路由改变,且组件被复用时,这个导航守卫就是为了处理上述/user/1和/user/2的问题
  • beforeRouteLeave路由离开时调用
	beforeRouteEnter(to, from, next) {
      console.log('beforeRouteEnter User')
      next((vm) => {
        console.log(vm)
      })
    },
    beforeRouteUpdate() {
      console.log('beforeRouteUpdate User')
    },
    beforeRouteLeave() {
      console.log('beforeRouteLeave User')
    },
composition api 守卫

目前只有2个

  • onBeforeRouteLeave
  • onBeforeRouteUpdate

除了不能访问this用法和组合式API相同,且在同样的组合式API后面执行

导航守卫执行顺序

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

路由元信息

有时候可以给路由加meta,添加属性标记

怎么获取meta,2种方式

  • route.matched可以获取所有匹配的路由,父子嵌套路由,一个path可能匹配多个路由,然后遍历matched获得meta
  • route.meta直接获取某个路由上的meta属性

如果使用TS,想要添加更多的属性需要扩展 meta类型。

过度效果

可以通过router-view暴露出来的属性Component,route设置动画效果。父组件要提前使用子组件的属性需要用v-slot,作用域插槽。

<router-view v-slot="{ Component }">
  <transition name="fade">
    <component :is="Component" />
  </transition>
</router-view>

可以通过route meta进行单独的定制

<router-view v-slot="{ Component, route }">
  <!-- 使用任何自定义过渡和回退到 `fade` -->
  <transition :name="route.meta.transition || 'fade'">
    <component :is="Component" />
  </transition>
</router-view>

甚至可以动态修改meta属性,达到动态动画的效果

router.afterEach((to, from) => {
  const toDepth = to.path.split('/').length
  const fromDepth = from.path.split('/').length
  to.meta.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
})
<router-view v-slot="{ Component, route }">
  <transition :name="route.meta.transition">
    <component :is="Component" />
  </transition>
</router-view>

滚动行为

通过路由导航到某个页面/组件时,可以配置期望的滚动位置,比如顶部、某个DOM等,也可以使用之前的位置

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    // 就像浏览器的原生表现,可以记录历史记录的滚动位置
    if (savedPosition) {
      return savedPosition
    } else {
      // 滚动到顶部
      return { top: 0 }
      // 始终在元素 #main 上方滚动 10px
      return {
      // 也可以这么写
      // el: document.getElementById('main'),
      el: '#main',
      behavior: 'smooth', //如果浏览器支持的化,可以让滚动更流畅
      top: -10,
    }
    }
  },
})

如果要进行延迟滚动,可以返回一个promise,resolve的结果就是上面return的类型即可。

路由懒加载

就是动态导入组件,需要的时候才会加载。使用动态导入结合webpack会自动分割代码,实现按需加载,不至于一次把一个巨大的js文件都加载了

// routes.js
const User = () => import('@components/User')

建议所有的路由组件都使用懒加载。

导航故障

push方法会返回一个promise,可以通过then,await获得resolve的值,然后进行处理。错误已在push方法内部进行了捕获,所有只会resolve出来。

如果没有出现错误,什么都不会返回,结果为undefined

如果有值,那么肯定是Error实例,这个实例封装了很多内容,足够进行判断使用。

可以结合NavigationFailureTypeisNavigationFailure判断错误类型,然后针对性的给出提示

NavigationFailureType是枚举类:

  • aborted:在导航守卫中返回 false 中断了本次导航。
  • cancelled: 在当前导航还没有完成之前又有了一个新的导航。比如,在等待导航守卫的过程中又调用了 router.push
  • duplicated:导航被阻止,因为我们已经在目标位置了。
 const userRouteWithPath = async () => {
     	// failure会暴露to,from
        let failure = await router.push({ path: '/user/' + id, query: { age: 30 } })
        // 判断是否重复导航了,如果没有第二个参数只能判断是否失败
        if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
          alert('重复导航')
        }
      }

如果在导航守卫中return/next了一个新的导航,上面的类型无法检测到,需要通过路由实例获得

await router.push('/my-profile')
if (router.currentRoute.value.redirectedFrom) {
  // redirectedFrom 是解析出的路由地址,就像导航守卫中的 to和 from
}

动态路由

  • router.addRoute({})添加路由,直接添加会是根路由,addRoute(name,{})可以指定添加到某个路由下,变成子路由
  • router.removeRoute(name)删除路由。addRoute会返回一个方法,调用该方法也可以删除路由
  • router.hasRoute(name):检查路由是否存在。
  • router.getRoutes():获取一个包含所有路由记录的数组。

其他API

名词解释:

  • RouteRecordRaw ,也是routes中配置的对象类型 routes: RouteRecordRaw[]
  • RouteRecordNormalized,标准化的路由记录,和上面的类似,最大的区别是有components
  • RouteLocationNormalized 标准化的路由地址,to,from的类型,也是this.$route的类型
  • RouteLocationRaw 用户级路由地址,传入push的参数类型
  • RouteLocation 包含重定向解析的RouteLocationRaw

API:

  • router.currentRoute 当前路由地址,只读,实际就是to,from类型的响应式只读对象Readonly<RouteLocationNormalized >

  • router.options 创建路由原始配置对象,createRouter时传入的对象

  • router.resolve 将RouteLocationRaw 转化成 RouteLocationNormalized ,也就是路由记录转路由地址

    const locationRoute = router.resolve({ name: 'user', params: { id }, query: { age: 18 } })
    //输出 {fullPath: "/user/1?age=18", hash: "", query: {…}, name: "user", path: "/user/1", …}

状态管理

如果多个组件使用同一个状态,在深度嵌套时可以使用provide/inject获得,但是兄弟组件之间呢,无法处理,只能通过其他hack方法处理。如果状态太多,在各组件之间传递状态,同时多个组件都要修改状态,其他所有组件都要实时响应,在不引入状态管理的情况下简直是灾难。状态管理就是在组件树之外抽取公共的状态库,用于维护整个应用的状态,而组件不管在哪一层都能访问。

当然是否要引入第三方状态管理工具,视应用的大小而定,如果就是个简单的应用,完成可以使用一个对象封装状态以及状态的方法(类似class),然后在需要的组件处引入即可。

//store.js
import { reactive } from 'vue'
export default {
  state: reactive({ a: 1, b: 2 }),
  setState() {
    this.state.a = 'a'
  }
}

安装

npm install vuex@next --save # 当前最新版本为非稳定版,需要用next下载
//state.js 导出配置
export default {
  state() {
    return {
      count: 0,
    }
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
}
 
//createStore.js 导出store
import { createStore } from 'vuex'
import storeOptions from './state'
export default createStore(storeOptions)
//main.js 安装
import store from '@store/createStore'
app.use(store)

会给每个组件都注入$store实例

在组件之外访问实例需要 import store from '@store/createStore'

vuex特点:

  • state都是响应式的
  • 不能直接修改state必须经过commit(‘mutations’)提交修改,目的是为了追踪
  • 一个vue应用只有一个store实例,是为单一状态树

核心概念

store可以由多个模块构成,每一个模块可以有state,getter,mutation,action以及子模块几个属性。

{
    state:() => ({}),
    mutations:{}
    ....
    modules:{
        moduleA,
        moduleB
    }
}

我们把构建store的options可以理解为**root模块,root模块下面可以嵌套其他模块。如果使用模块,==强烈建议开启namspaced = true==**,这对于后面的操作代码分割等有很多好处。

对于模块化,不管是局部state,getters,还是rootState,rootGetters(不管嵌套多少层,root特指root模块),都能直接获取模块对应的state,getter,还能间接的获取本下模块的state,getters

  • 在vuex中可以间接获取本下的所有命名空间state,getters,名称前需要用命名空间,比如state.module.property;getters需要用[‘some/nested/module/getterName’]获得

  • 在vuex中可以间接获取本下所有命名空间mutation,action,名称前需要用命名空间,比如commit(‘some/nested/module/mutation’)

  • 在组件中通过this.store.state.module.property。getters需要用[‘some/nested/module/getterName’]获得。

  • 在组件中commit,dispatch非root模块的mutation和action,mutation,action名称前需要带命名空间,比如commit(‘some/nested/module/mutation’)。

state

类似组件的data属性,尽量是简单的对象类型。为避免模块复用,所以state都用工厂函数返回,和data一样。

在vuex中

state定义为简单对象类型,供其他属性调用,或者被组件直接调用

在组件中

通常放在计算属性中。

  • this.$store.state.<property name> 获得,注意这种方式还可以间接获得其他命名空间模块的state

  • mapState方式

    // mapState可以简化state的获取
    ...mapState({
        count: 'count', // = count: state => state.count
        // 计算新的值,可以使用本组件的data props computed属性
        countDouble: state => state.count * 2,
    }),
    // 甚至可以传数组
    ...mapState(['name', 'age']),

getters

vuex的计算属性。

在vuex中
  getters: {
    // 类似计算属性,只不过是store的计算属性,可以解决多个组件想使用这一个计算属性的复用问题
    user: state => `${state.name} is ${state.age} years old .`,
    // 可以接收 state,其他getters作为参数
    userExtend: (state, getters) => `${getters.user} he has ${state.count} children`,
    // 返回一个方法,很多时候很有用,特别是动态计算时
    nameSwitch: state => split => [...state.name].join(split)
  },

getters可以接收的参数[state],[getters],[rootState],[rootGetters]

在组件中

通常放在计算属性中。

  • this.$store.getters.<getters name>,这种方式同样可以间接获取其他命名空间模块getters

  • mapGetters方式

    // getters同样支持map方式快速读取
    ...mapGetters(['user', 'userExtend']),
    // 返回函数,计算属性作为一个函数,可以在模板上直接调用,虽然计算属性不能传参,但是计算属性返回的方法可以传参
    ...mapGetters({
    nameSwitchAlias: 'nameSwitch',
    }),

mutations

用于操作state,不要直接对state进行修改。

在vuex中
  // mutation不能直接调用,这里更像是注册了一个事件,通过commit触发该类型事件来执行回调
  // mutation必须是同步的,如果有2个异步mutation改变一个state,那么这个state的值将是不可预估的
  mutations: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    },
    // mutation 支持传入payload作为额外的参数,多数情况下payload应该是一个对象,可以承载更多的参数
    incrementByPayload: (state, payload) => {
      state.count += payload.amount
    }
  }

注意:mutation只能接收2个参数[state],[payload],无法使用rootState,rootGetters。

在组件中

mutation通常放在methods下。

  • 可以自定义方法,然后在方法中使用

    increment() {
             this.$store.commit('increment',payload)
     }
    // 或者采用对象风格提交
    incrementByPayload() {
        // 可以采用对象风格的方式触发事件,这种风格会将整个对象都当作payload
        this.$store.commit({ type: 'incrementByPayload', amount: this.payload })
    }

    建议用第一种,不然很容易混淆type和payload,特别是payload要经过多层action传递时。

    指定模块需要使用 commit('some/nested/module/mutationName',payload)

  • mapMutations方式

    // mutations同样支持mapMutations格式
    ...mapMutations(['increment']),
    ...mapMutations({incrementAlias:'increment'}),

actions

很显然我们不可能没有异步操作,所以引入action的概念,action并不直接改变state,他只用来commit mutation。而且可以异步。

实际应用中action更像是mutation的组合体,一个action将一组逻辑组合起来,每个逻辑至少有一个action或mutation。

在vuex中
	actions: {
    // action接收一个和store实例具有相同属性和方法的对象context,由于属性太多,所以调用的时候一般解构赋值只获取需要的
    incrementByPayload({ commit, state }, payload) {
      return new Promise(resolve => {
        setTimeout(() => {
          commit({ type: 'incrementByPayload', ...payload })
          resolve(state.count)
        }, 500)
      })
    },
    async decrementAction({ commit, dispatch }, payload) {
      await dispatch('incrementByPayload', payload)
      await new Promise(resolve => {
        setTimeout(() => {
          commit('decrement')
          resolve()
        }, 500)
      })
      console.log('decrementAction finish.')
    }
  }

接收2个参数context,[payload]

context拥有和store实例相同的属性和方法,一般用的较多的就是{state,getters,commit,dispatch,rootState,rootGetters}

所有的action都会返回一个promise,如果返回值不是promise会自动进行包装。

在组件中

通常放在methods中。

  • 自定义方法,然后在方法中dispatch

    // 由于返回值必然是promise,所以可以then()操作
    this.$store.dispatch('decrementAction', { amount: this.payload }).then(value => {
              console.log('dispatch value :' + value)
    })

    指定模块需要使用 dispatch('some/nested/module/actionName',payload)

  • mapActions方式

    // mutations同样支持mapMutations格式
    ...mapActions(['decrementAction']),
    ...mapActions({decrementActionAlias:'decrementAction'}),

map映射方式,都只是将相关的属性、方法映射过来,可以使用this操作罢了。

模块和命名空间

模块化,指的是在构建store时进行模块划分。但是虽然进行了模块划分,但是还是注册在一个全局的命名空间下,比如commit,实际会去全局命名空间下找对应的mutation操作,如果有多个重名的,那都会触发。虽然属于不同的模块,但是权限实际还是没有隔离。==所以有module尽量开启namspaced = true。==

import moduleAA from './moduleAA'
export default {
  namespaced: true,
  state() {
    return { mod: 'moduleA' }
  },
  getters: {
    mAGetters: (state, getters, rootState, rootGetters) => {
      console.log('----------------mAGetters--------------')
      console.log(state)
      console.log(getters)
      console.log(rootState)
      console.log(rootGetters)
      return 'mAGetters' + state.mod
    }
  },
  mutations: {
    splitMod: state => {
      console.log('----------------mAMutations--------------')
      console.log(state)
      state.mod = [...state.mod].join('-')
    }
  },
  actions: {
    splitModAction: ({ commit }) => {
      commit('splitMod')
    },
    doActionLocal: ({ dispatch }) => {
      dispatch('splitModAction')
    },
    doActionAll: ({ dispatch }) => {
      dispatch('splitModAction', null, { root: true })
    }
  },
  modules: {
    moduleAA
  }
}
 

在组件中使用命名空间

没有开启namespaced = true的情况下,你想要通过mapGetters找到getters,会将所有全局命名空间下的getters都找到,这并不符合预期,所以尽量启用命名空间,让代码更严格。

  • 使用createNamespacedHelpers,他会构造对应命名空间的辅助映射函数

    // 默认导入的可以看作是操作root模块的,为了方便区分对import和解构赋值的对象都做了重命名
    import { createNamespacedHelpers, mapGetters as mapGettersRoot, mapState as mapStateRoot } from 'vuex'
      const {
        mapGetters: mapGettersModuleA,
        mapState: mapStateModuleA,
        mapMutations: mapMutationsModuleA,
        mapActions: mapActionsModuleA,
      } = createNamespacedHelpers('moduleA')
      const {
        mapGetters: mapGettersModuleAA,
        mapState: mapStateModuleAA,
        mapMutations: mapMutationsModuleAA,
      } = createNamespacedHelpers('moduleA/moduleAA')
     export default{
         ...
     }
  • 给辅助函数传参

    [some/nested/module],{[]|String|{}}

    ...mapActions('some/nested/module',['action1','action2'])

    第一个参数传入命名空间,如果是嵌套的需要使用fatherModule/sonModule的形式

命名空间模块中注册和访问全局内容

有些特殊需求,需要在命名空间模块下访问全局(大部分情况下就是root)内容(在组件中不存在这个问题,都可以访问,这里指得是vuex下),rootState和rootGetter可以访问对应的root内容,但是mutation和action呢?

// 此时调用的action是全局命名空间下的,commit同理
dispatch('splitModAction', null, { root: true })

甚至可以在命名空间模块下注册一个全局的action

// 此时splitModAction注册在全局命名空间下,相对于在root下注册了
splitModAction: {
    root: true,
    handler: ({ commit }) => {
        commit('splitMod')
    }
},

注意:命名空间模块下注册的action只是在全局可以直接访问到,但是逻辑上仍然属于对应的模块,context也是对应模块的。

模块动态注册

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})
  • store.unregisterModule(moduleName)卸载模块,module如果是嵌套的,需要使用数组['nested','moduleName']。不能卸载静态模块,也就是创建store时传入的。
  • store.hasModule(moduleName)查看是否有该模块,同样的如果时嵌套模块,需要传入数组。

项目结构

项目尽可能的根据功能模块,进行分解,便于维护

在一个模块中,state,getter,mutation,action,module尽可能分开写,然后进行导出

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块

组合式API

export default {
  setup () {
    const store = useStore()
    // 计算属性访问,保证响应式
    const checkoutStatus = computed(() => store.state.cart.checkoutStatus)
    const products = computed(() => store.getters['cart/cartProducts'])
    const total = computed(() => store.getters['cart/cartTotalPrice'])
    const checkout = (products) => store.dispatch('cart/checkout', products)
    return {
      currency,
      checkoutStatus,
      products,
      total,
      checkout
    }
  }
}

可以通过useStore获得store实例,然后通过这个实例正常获取state,getter触发mutation,action。

插件

插件是一个接收store参数的函数

可以通过subscribe订阅相关mutation,继而做出一些操作

export default store => {
  store.subscribe((mutation, state) => {
    console.log('mutation type:' + mutation.type)
    if (mutation.type === 'incrementByPayload') console.log('incrementByPayload')
    console.log(state)
  })
}
 
// 导入插件
const store = createStore({
  // ...
  plugins: [myPlugin]
})

其他API

  • subscribe,订阅store的mutation,当mutation完成后,触发handler,并将执行的这个mutation和相关state作为参数传入,可以给store添加多个订阅,后订阅的放在后面,可以通过选项{prepend:true}插队,放在订阅链的最前面。调用此方法的返回值即可停止订阅。

    subscribe(handler: Function, options?: Object): Function
    store.subscribe((mutation, state) => {
      console.log(mutation.type)
      console.log(mutation.payload)
    })
  • subScribeAction,订阅store的action,当dispatch时触发handler,接收对应的action和state作为参数。同样可以使用{prepend:true}插队。同样执行返回值停止订阅

    subscribeAction(handler: Function, options?: Object): Function
    store.subscribeAction((action, state) => {
      console.log(action.type)
      console.log(action.payload)
    },{prepend:true})

    默认是在dispather之前执行handler,也可以设置完成action之后执行,同时可以设置一个error方法,用于捕获action抛出的错误

    store.subscribeAction({
      before: (action, state) => {
        console.log(`before action ${action.type}`)
      },
      after: (action, state) => {
        console.log(`after action ${action.type}`)
      },
      error: (action, state, error) => {
        console.log(`error action ${action.type}`)
        console.error(error)
      }
    })

其他说明

严格模式下,不是通过mutation修改状态都会报错,正常下不会有什么影响,但是input绑定v-model问题就来了,v-model双向绑定会直接赋值,破坏了严格模式,不过可以将v-model绑定的值,定义成计算属性,并设置计算属性的set方法。

computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

axios

axios是一个基于promise的HTTP库,可以用于浏览器和nodejs中。

安装

npm install --save axios
import axios from 'axios' // 引入

axios API

  • axios.request(config)

  • axios.get(url[, config])

  • axios.delete(url[, config])

  • axios.head(url[, config])

  • axios.options(url[, config])

  • axios.post(url[, data[, config]])

  • axios.put(url[, data[, config]])

  • axios.patch(url[, data[, config]])

常用的方法都做了别名处理

实例API

可以通过axios创建一个实例,通过实例发送请求

export default axios.create({
  timeout: 3600
})
  • axios#request(config)

  • axios#get(url[, config])

  • axios#delete(url[, config])

  • axios#head(url[, config])

  • axios#options(url[, config])

  • axios#post(url[, data[, config]])

  • axios#put(url[, data[, config]])

  • axios#patch(url[, data[, config]])

  • axios#getUri([config])

实例方法和上面基本相同。可以设置不同的初始配置生成不同的实例,比如要向多个系统发送数据请求,可以一个子系统一个实例。

通常使用request方法,方便统一传参数。

config配置

有4种设置配置的方法

  • axios本身自带的

  • 通过axios全局设置的

    axios.defaults.baseURL = 'https://api.example.com';
    axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
  • 通过实例设置的

    instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
  • 通过请求设置的

    instance.get('/longRequest', {
      timeout: 5000
    });

最后会取并集,并且后面的会覆盖前面的

config常用配置

{
   // `url` 是用于请求的服务器 URL
  url: '/user',
 
  // `method` 是创建请求时使用的方法
  method: 'get', // default
 
  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',
 
  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
  transformRequest: [function (data, headers) {
    // 对 data 进行任意转换处理
    return data;
  }],
 
  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对 data 进行任意转换处理
    return data;
  }],
 
  // `headers` 是即将被发送的自定义请求头
  headers: {'X-Requested-With': 'XMLHttpRequest'},
 
  // `params` 是即将与请求一起发送的 URL 参数
  // 必须是一个无格式对象(plain object)或 URLSearchParams 对象
  params: {
    ID: 12345
  },
 
   // `paramsSerializer` 是一个负责 `params` 序列化的函数
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function(params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },
 
  // `data` 是作为请求主体被发送的数据
  // 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
  // 在没有设置 `transformRequest` 时,必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属:FormData, File, Blob
  // - Node 专属: Stream
  data: {
    firstName: 'Fred'
  },
 
  // `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
  // 如果请求话费了超过 `timeout` 的时间,请求将被中断
  timeout: 1000,
 
   // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // default
 
  // `adapter` 允许自定义处理请求,以使测试更轻松
  // 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)).
  adapter: function (config) {
    /* ... */
  },
 
 // `auth` 表示应该使用 HTTP 基础验证,并提供凭据
  // 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },
 
   // `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
  responseType: 'json', // default
 
  // `responseEncoding` indicates encoding to use for decoding responses
  // Note: Ignored for `responseType` of 'stream' or client-side requests
  responseEncoding: 'utf8', // default
 
   // `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
  xsrfCookieName: 'XSRF-TOKEN', // default
 
  // `xsrfHeaderName` is the name of the http header that carries the xsrf token value
  xsrfHeaderName: 'X-XSRF-TOKEN', // default
 
   // `onUploadProgress` 允许为上传处理进度事件
  onUploadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
  },
 
  // `onDownloadProgress` 允许为下载处理进度事件
  onDownloadProgress: function (progressEvent) {
    // 对原生进度事件的处理
  },
 
   // `maxContentLength` 定义允许的响应内容的最大尺寸
  maxContentLength: 2000,
 
  // `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject  promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
  validateStatus: function (status) {
    return status >= 200 && status < 300; // default
  },
 
  // `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
  // 如果设置为0,将不会 follow 任何重定向
  maxRedirects: 5, // default
 
  // `socketPath` defines a UNIX Socket to be used in node.js.
  // e.g. '/var/run/docker.sock' to send requests to the docker daemon.
  // Only either `socketPath` or `proxy` can be specified.
  // If both are specified, `socketPath` is used.
  socketPath: null, // default
 
  // `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
  // `keepAlive` 默认没有启用
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),
 
  // 'proxy' 定义代理服务器的主机名称和端口
  // `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
  // 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
  proxy: {
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },
 
  // `cancelToken` 指定用于取消请求的 cancel token
  // (查看后面的 Cancellation 这节了解更多)
  cancelToken: new CancelToken(function (cancel) {
  })
}

数据响应格式

{
  // `data` 由服务器提供的响应
  data: {},
 
  // `status` 来自服务器响应的 HTTP 状态码
  status: 200,
 
  // `statusText` 来自服务器响应的 HTTP 状态信息
  statusText: 'OK',
 
  // `headers` 服务器响应的头
  headers: {},
 
   // `config` 是为请求提供的配置信息
  config: {},
 // 'request'
  // `request` is the request that generated this response
  // It is the last ClientRequest instance in node.js (in redirects)
  // and an XMLHttpRequest instance the browser
  request: {}
}

拦截器

可以在数据请求前,或者响应到达then、catch前进行拦截。

// Add a request interceptor
axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });
 
// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
  }, function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    return Promise.reject(error);
  });

移除拦截

const myInterceptor = instance.interceptors.request.use(function () {/*...*/});
instance.interceptors.request.eject(myInterceptor);

如果拦截器代码是同步代码可以添加属性

axios.interceptors.request.use(function (config) {
  config.headers.test = 'I am only a header!';
  return config;
}, null, { synchronous: true });

还可以给拦截器添加条件,返回true时才进行拦截

function onGetCall(config) {
  return config.method === 'get';
}
axios.interceptors.request.use(function (config) {
  config.headers.test = 'special get headers';
  return config;
}, null, { runWhen: onGetCall });

错误处理

可以根据catch的错误类型进行分别处理

axios.get('/user/12345')
  .catch(function (error) {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // The request was made but no response was received
      // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
      // http.ClientRequest in node.js
      console.log(error.request);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.log('Error', error.message);
    }
    console.log(error.config);
  });

可以将error转换成Json格式,来获取更多信息

axios.get('/user/12345')
  .catch(function (error) {
    console.log(error.toJSON());
  });