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;
}-
^~/api/,以/api开头的请求
-
rewrite,重写地址,很有用,格式为 : rewrite regx replacement [flag]
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.nodec. 将 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_modulesb. 依次在每个目录中,将 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.jswebpack下的解析,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....
这些方法接收回调函数,但生命周期执行时,触发钩子函数,执行回调。
| 选项式 API | Hook inside setup |
|---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
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/3Get参数
/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个
onBeforeRouteLeaveonBeforeRouteUpdate
除了不能访问this用法和组合式API相同,且在同样的组合式API后面执行。
导航守卫执行顺序
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫(2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
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实例,这个实例封装了很多内容,足够进行判断使用。
可以结合NavigationFailureType和isNavigationFailure判断错误类型,然后针对性的给出提示
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): Functionstore.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): Functionstore.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 axiosimport 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());
});