如何判断变量 是否为数组?
Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"
判断数据类型的方法
typeof instanceof constructor Object.prototype.toString.call()
- typeof:只能判断基本数据类型,不能判端引用数据类型
- instanceof :只能正确判断引用数据类型,而不能判断基本数据类型。
instanceof运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。 - constructor:有两个作用,一是判断数据的类型,二是对象实例通过
constrcutor对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了: - Object.prototype.toString.call() :使用 Object 对象的原型方法 toString 来判断数据类型:
Js数据类型有那些
基本类型(值类型):String、Number、boolean、null、undefined。
引用类型:object。里面包含的 function、Array、Date。
引用类型和值类型的区别
- 值类型存在于栈中, 存取速度快 引用类型存在于堆,存取速度慢
- 值类型复制的是值本身 引用类型复制的是指向对象的指针
- 值类型结构简单只包含基本数据, 引用类型结构复杂,可以实现多层嵌套 栈中存储:基本类型:Number(数值),String(字符串),Boolean (布尔),Null,Undefined 堆中存储:引用类型:Object(对象),Array(数组),Function(函数)
数据类型转换
换为String
- a = a.toString() ; -该方法不会影响到原变量,它会将转换的结果返回 -但是注意:null 和 undefined这两个值没有toString 如果调用他们的方法,会报错
- a = String(a) ; 但对于null 和 undefined 它会将null 直接转换为“null”,将undefined 转换为“undefined”
换为Number 转换方式一 使用Number() 函数 -字符串 -> 数字 1.纯数字的字符串,则直接将其转换为数字 2.字符串中有非数字的内容,则转换为 NaN 3.如果字符串是一个空串或者是一个全是空格的字符串,则转换为 0 -布尔 -> 数字 true 转成 1,false 转成 0 -null -> 数字 0 -undefined -> 数字 NaN 转换方式二 parseInt() 取有效整数(从左到右) parseFloat() 取有效浮点数 (小数) 非String 如果对非String使用parseInt()或parseFloat()。他会先将其转换为String. 然后再操作-NaN
转为Boolean a = Boolean(a) ‘’,0,NaN,null,undefined --> false,其余转为 true
vue生命周期
生命周期就是一个 vue 实例从创建到销毁的过程。
beforeCreate created beforeMount mounted
beforeupdate updated beforedestroy destroyed
常用的生命周期钩子:
- mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。
- beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】(交代后事)。
宏任务 和 微任务
同步任务 > 微任务 > UI渲染 > 宏任务
微任务(先):一次性执行全部 清空
Promise.then/catch/finally,MutationObserver, process.nextTick (Node.js), queueMicrotask
宏任务(后):一次执行一个
setTimeout, setInterval, setImmediate (Node.js), I/O 操作, UI 渲染, postMessage整个 Script
同步和异步分别进入到不同的执行场所,同步进入到主线程,异步的进入到event table 并注册函数,异步任务又可以分为宏任务和微任务。
一个宏任务执行完后会立即执行该宏任务中的微任务队列,直至微任务队列清空继续执行任务队列中的下一个宏任务。 宏任务执行完会去清空微任务队列,清空微任务后如果有UI渲染逻辑会线执行UI渲染,然后执行下一个宏任务。
mixin(混入)
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如data、components、methods、created、computed等等
我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来。
功能:可以把多个组件共用的配置提取成一个混入对象 数据/方法:自己>组件混入>全局混入 生命周期:先全局混入→再组件混入→最后自己
重绘 回流(重排)
- 重绘: 当页面中元素样式的改变并不影响它在文档流中的位置时,也就是说布局没有发生改变时 (比如只是改变元素的颜色)。
- 回流:当渲染树(Render Tree)中的部分(或全部)元素的尺寸、结构、显示隐藏等发生改变时,浏览器重新渲染的过程称为回流。 简而言之,任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流。 回流是影响浏览器性能的关键 因素。 比如: (1)添加或者删除可见的 DOM 元素(不可见元素不会触发回流); (2)元素尺寸或位置发生改变 (3)元素内容变化,比如文字数量或图片大小 (4)浏览器窗口大小发生改变 (5)CSS伪类的激活(例如::hover,从而改变了元素的布局的)
注意
- 回流必定会发生重绘,重绘不一定会引发回流。
- 回流比重绘的代价要更高。有时即使仅仅回流一个单一的元素,它的父元素以及任何跟它相关的元素也会产生回流,牵一发动全身。
避免(减少)回流
css
- 避免设置多层内联样式。
- 如果需要设置动画效果,最好将元素脱离正常的文档流。
- 避免使用CSS表达式(例如:calc())。
v-show, v-if
对于需要频繁切换的视图来说,使用v-show比v-if更加节约性能。因为v-show可以避免dom节点的销毁和重建
- v-show操作的是元素的display属性;
- v-if操作的是元素的创建和插入。
如果这个dom不经常改变的话,建议使用v-if,因为这样也不会造成频繁的回流、重绘; 如果dom经常改变的话,建议使用v-show。
nextTick 的作用是什么?
作用: 在下一次DOM更新结束后执行指定的回调
当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行
简单的理解是:当数据更新了,在dom中渲染后,自动执行该函数,
watch 与 computed 的区别是什么?
computed适用于需要根据依赖属性计算出一个新值的场景;支持缓存,多对一。
而watch一对多(监听某一个值变化,执行对应操作)适用于那些需要在属性变化时执行异步或开销较大的操作场景。
- computed能完成的功能,watch都可以完成。
- watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作。
事件冒泡 事件捕获
冒泡:从最具体的元素到不具体的元素(由内到外)阻止冒泡:@click.stop
捕获:从最不具体的元素到最具体的元素(由外向内)开启捕获:@click.capture
只点自己触发:@click.self
阻止冒泡 原生js e.stopPropagation()
数组API
-
在Vue修改数组中的某个元素一定要用如下方法:(因为无法直接更改)
- 1.使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse() -----------------尾加 、 尾删、 头删 、 头加 、增删改、 排序、 颠倒
- 2.Vue.set() 或 vm.$set()
- 3.替换法(新数组): 过滤filter(), 合并数组concat(), 截取slice(star/t,end)
- this.$set(x,x,x)
-
特别注意: Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!
-
indexOf:返回下标数字,找不到返回
-1includes:直接返回布尔值 true/false,代码更直观
null 和 undefined 的区别
- undefined 是未定义,系统自动给
- null 是空值,手动赋值
==判断相等,===判断不相等typeof null是object(历史 bug)
Js函数中的this有哪些指向?怎样改变函数中this的指向。
- this 谁调用指向谁
- 箭头函数没有 this,继承外层
- call /apply/bind 强行改 this
this 指向
- 普通函数 → window
- 对象方法 → 该对象
- 构造函数 → 实例对象
- 事件绑定 → 事件源
- 箭头函数 → 外层 this
改变 this 指向
- call → 立即执行,逗号传参
- apply → 立即执行,数组传参
- bind → 不执行,返回新函数
盒子塌陷
- 父盒子加
overflow: hidden - 父盒子伪元素 ::after + clear: both(清除浮动)
- 父盒子设置固定高度,缺点:不灵活,不推荐。
var、let、const 区别
var:旧,无块级作用域,可重复,存在变量提升
let:有块级,可修改,不重复
const:有块级,必须赋值,基本类型不能改
原型 & 原型链
- 每个对象都有
__proto__ - 每个函数都有
prototype - 原型链就是:对象通过 proto 往上找属性 / 方法,找不到就一直往上找,直到 null,这就是原型链
-
原型:函数都有prototype属性,称之为原型,也称为原型对象
- 原型可以放一些属性和方法,共享给实例对象使用
- 原型可以做继承
-
原型链:对象都有
__proto__属性,这个属性指向它的原型对象,原型对象也是对象,也有__proto__属性,指向原型对象的原型对象,这样一层一层形成的链式结构称为原型链,最顶层找不到则返回null
在js语言中,每个实例对象都有一个__proto__属性,改属性指向他的原型对象,且这个实例对象的构造函数都有一个原型属性prototype,与实例对象的proto属性指向同一个对象,当这个对象在查找一个属性的值时,自身没有就会根据proto向他的原型上寻找,如果不存在,则会到生成这个实例对象的构造函数的原型对象上寻找,如果还是不存在,就继续道Object的原型对象上找,在往上找就为null了,这个链式寻找的过程,就被称为原型链。
闭包
函数套函数,内部函数访问外部函数变量 → 形成闭包
作用
- 延长变量生命周期
- 私有化变量,不污染全局
缺点
- 变量不销毁,可能内存泄漏
什么是回调地狱?
回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调执行的条件
异步、Promise、async/await
JS 是单线程,异步就是不阻塞代码往下走。不用等一件事做完,就能做下一件事。 定时器、ajax、请求、读取文件 都是异步
Promise 链式调用 解决回调地狱。 promise 对象用来封装一个异步操作并可以获取其成功/失败的结果值
三种状态:pending / resolved / rejected
- Promise.resolve 如果传入的参数为 非Promise类型的对象,则返回的结果为成功promise对象 如果传入的参数为Promise对象,则参数的结果决定了resolve的结果
- Promise.reject 传什么都是失败
- Promise.all 数组全部成功返回成功,有一个失败就失败
- Promise.race 赛跑,第一个完成状态和结果
async/await(Promise 语法糖)
- async 声明异步函数,await 等待 Promise
- 让异步代码写法同步化,更易读
- 错误处理用 try/catch
axios
一、axios 是一个基于 Promise 的 HTTP 客户端,专门用来发送 ajax 请求(前后端交互)。
二、axios 有什么特点?(面试常问)
- 基于 Promise → 支持
then / catch或async/await - 浏览器、node 端都能用
- 自动转换 JSON 数据
- 支持请求 / 响应拦截器
- 支持取消请求
- 防止 XSRF 攻击
三、最常用 4 种请求(背)
// 1. 获取数据
axios.get(url, { params: {} })
// 2. 提交数据
axios.post(url, data)
// 3. 修改数据
axios.put(url, data)
// 4. 删除数据
axios.delete(url)
四、最标准写法(async/await 最优雅)
async function getData() {
try {
let res = await axios.get('/api/list')
console.log(res.data)
} catch (err) {
console.log('请求出错', err)
}
}
五、axios 核心:拦截器(必背)
- 请求拦截器(发请求前做什么)
axios.interceptors.request.use(config => {
// 统一加 token
config.headers.token = 'xxxx'
return config
})
- 响应拦截器(拿到数据后做什么)
axios.interceptors.response.use(res => {
// 直接返回 data,不用每次 .data
return res.data
})
六、axios 与 原生 ajax /fetch 区别(面试)
- axios 是封装好的库,更稳定、更好用
- 自动处理 JSON
- 支持拦截器
- 支持 Promise
- 浏览器兼容性更好
七、超级总结(背这一段)
axios = 基于 Promise 的 HTTP 请求库 用来发请求、拿数据 支持 get/post/put/delete 有请求 / 响应拦截器 配合 async/await 最优雅!
同源策略
同源:协议、域名、端口号必须完全相同。
违背同源策略就是跨域。
深拷贝
浅拷贝 就是通过赋值的方式进行拷贝,那为什么说这是浅拷贝呢?就是因为赋值的方式只会把对象的表层赋值给一个新的对象,如果里面有属性值为数组或者对象的属性,那么就只会拷贝到该属性在栈空间的指针地址,新对象的这些属性数据就会跟旧对象公用一份,也就是说两个地址指向同一份数据,一个改变就会都改变。
深拷贝 则不会出现上述问题,引用数据类型,地址跟数据都会拷贝出来。
巧劲(意料之外的)
let arr = [1,2,[1,2]]
// let ar = [ ...arr]
let ar = JSON.parse(JSON.stringify(arr))
ar.push('123')
console.log(arr,ar);//[1, 2, Array(2)] , [1, 2, Array(2), '123']
- 浅拷贝:对象数组 只拷贝第一层,深层还是同一个地址
{ ...obj }展开运算符 - 深拷贝:完全拷贝一个全新对象,所有层级都独立,互不影响
JSON.parse(JSON.stringify(obj))
JSON.stringify() 用于将JavaScript对象转化为JSON字符串 JSON.parse() 用于将JSON字符串转化为一个对象
2.让localStorage/sessionStorage存储对象 因为localStorage/sessionStorage默认只能存储字符串,所以我们利用JSON.stringify()将对象转为字符串放在缓存里面,之后把缓存区出来的时候再用JSON.parse()转回对象就可以了。
限制:这种方式成立的根本就是保证数据在“中转”时的完整性,而JSON.stringify()将值转换为相应的JSON格式时也有缺陷:
undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。 函数、undefined 被单独转换时,会返回 undefined
拷贝函数、undefined等stringify转换有问题的数据时,就会出错
递归的条件,一个是终止条件,一个是调用自己。
浅拷贝
{ ...obj } 利用展开操作符 生成新对象 一个对象的属性是引用类型,那么改变该属性值里面的内容,另外的拷贝对象也会改变,因此这种拷贝是浅拷贝
单行溢出显示成 ...
white-space: nowrap; // 文本不换行
overflow: hidden; // 超出部分隐藏
text-overflow: ellipsis; // 超出部分显示成...
弹性布局 flex
-
display: flex;
- 在弹性布局中,块元素可以左右排列
-
flex-wrap: wrap;
- 设置项目换行
-
justify-content: space-between;
- flex-start(默认值):左对齐
- center: 居中
- flex-end:右对齐
- space-between 两端对齐 中间间距平分
- space-around 元素左右两边距离都相等
-
align-content:space-between;
- 交叉轴两端对齐
-
align-items: center;
- align-items 设置元素在交叉轴(垂直方向)的对齐方式
- flex-start:交叉轴的起点对齐。
- flex-end:交叉轴的终点对齐。
- center 垂直居中
-
:hover
- block 会覆盖上面的flex,导致弹性布局失败
- 上面样式用 display: none; :hover用 display: flex;
如何将一个div盒子水平垂直都居中?
方法一:利用定位(常用方法,推荐)
//子绝父相,子盒子top、left都50%,
.parent{
position:relative;
}
.child{
position:absolute;
top:50%;
left:50%;
transform:translate(-50%,50%)
}
方法一的原理就是定位中心点是盒子的左上顶点,所以定位之后我们需要回退盒子一半的距离。
方法二:利用margin:auto;
//给绝对定位,left、right、top、bottom都给0,margin给auto;
.parent{
position:relative;
}
.child{
position:absolute;
margin:auto;
top:0;
left:0;
right:0;
bottom:0;
}
方法s三:利用display:flex;设置垂直水平都居中;
//flex布局
.parent{
display:flex;
justify-content:center;
align-items:center;
}
rem为什么可以实现自适应布局
em:基于父级元素的字体大小; rem:是基于html的字体大小来进行布局
由于浏览器的字体大小一般默认是16px;1rem=16px;
new实现
- 创建一个新对象;var obj = {};
- 将构造函数的作用域赋给新对象(用新对象的隐式原型指向构造函数的显式原型obj.proto = ClassA.prototype;,用call改变this指向ClassA.call(obj););
- 执行构造函数中的代码(为这个新函数添加属性);
- 返回新对象;
为什么会出现跨域问题?
- 因为浏览器受到同源策略的限制,当前域名的js只能读取同域下的窗口
- 同源 指域名、协议、端口号 相同才允许共享资源的,保障浏览器安全。
解决跨域
- cors (后端配置响应头)返回数据携带一些特殊的响应头,支持get 和 post 请求。
- jsonp 借助scrpt标签里src属性在引入外部资源的时候,不受同源策略限制的特点。 (前后端均需配置)只能解决get请求
- 代理服务器 写后端接口,在后端调用页面拿到返回值返回给html文件。相当于绕过了浏览器,就不会存在跨域问题。
- PHP端修改header
行内元素、块元素有哪些?它们有什么不同?
行内元素:b、span、a、u、em、i、img、input、select、label、textarea、button
块级元素:div、h、ol、ul、dl、li、table、td、th、tr、dd、dt、p、caption
1、行内元素会在一条直线上分列,横向排列; 块级元素,垂直排列
2、行内元素不可以包括块级元素,只能包容文本或许其余行内元素。| 块级元素能够包括行内元素和块级元素,还能够包容内联元素和其余元素;。
3、块级元素 可以设置宽高;行内元素 设置宽高无效(能够设置line-height),margin、padding设置上下有效。
js中本地存储有哪些?有什么不同?
localStorage、sessionStorage、cookie;
- localStorage:永久、5MB、token、用户信息、主题、手动清除才会消失
- sessionStorage:临时、5MB、浏览器刷新还在,窗口一关就没
- cookie:可设时间、4KB、自动传后端 、登录态、身份凭证用得最多
| 特性 | localStorage | sessionStorage | cookie |
|---|---|---|---|
| 生命周期 | 永久保存,手动清除 | 窗口关闭就消失 | 可设置过期时间 |
| 存储大小 | 5MB | 5MB | 4KB |
| 随请求发送给后端 | 不发送 | 不发送 | 每次请求自动带过去 |
| 作用域 | 同一域名共享 | 同一窗口 + 域名 | 同一域名 |
| 易用性 | 简单 | 简单 | 稍麻烦 |
Vue中如何做样式穿透
加了scoped是局部样式的,
使用父元素 >>> 子元素,在有less和sass语言/deep/ , ::v-deep ,Stylus语言时均可用
深度监听怎么做
watch 里面一个属性 deep,为true时,可进行深度监听,任何属性发生变化,都可以监听到
immediate:true (立即执行)这个配置也要慎用
组件通信
超级记忆表(面试直接说)
- 父 → 子:props
- **子 → 父:emit 发射)
- 父拿子:children
- 子拿父:$parent
- 双向:v-model(子收 value / 子发 input 事件)
- 跨组件:Bus总线(发 on)
- 跨多层:provide/inject(祖先提供 / 后代注入)
- 全局:Vuex/Pinia
vue双向绑定的原理(vue 的响应式原理)
- 通过
Object.defineProperty()劫持 data 中所有属性的getter和setter,实现数据劫持。 - 使用发布订阅模式,当数据发生变化时,自动通知视图更新。
- 从而实现数据驱动视图的双向绑定效果。
Vue2 响应式的缺点?
- 不能监听对象新增属性 / 删除属性
- 不能监听数组下标直接赋值
- 必须用
Vue.set / this.$set
Vue3 用什么代替?
- Proxy 代理整个对象,功能更强
vue中的data为什么用return返回
因为组件会被多次复用,用 return 返回一个新对象,能保证每个组件实例有独立的数据,互不干扰。
Vue 组件可能会被多次创建、复用, data 写成函数并 return 一个对象, 能保证每个组件实例拥有独立的数据作用域, 避免多个实例共享同一个数据对象,造成数据污染。
防抖和节流
防抖:高频触发时,延迟执行,每次触发重新计时,只执行最后一次。 搜索框输入联想、窗口 resize、按钮频繁点击
function debounce(fn, delay) {
let timer = null
return function(...args) {
// 每次触发都清空定时器
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
节流:高频触发时,规定时间内只执行一次,避免频繁执行。 页面滚动加载、鼠标移动、高频点击
function throttle(fn, delay) {
let flag = true // 开关锁
return function(...args) {
if(!flag) return
flag = false
setTimeout(() => {
fn.apply(this, args)
flag = true
}, delay)
}
}
vuex
Vuex 是 Vue 的全局状态管理工具,用来在多个组件之间共享数据。
state → 数据(data)
getters → 计算属性(computed)
mutations → 唯一修改 state 的地方(同步)
actions → 异步操作(发请求)
modules → 模块化拆分
记忆口诀
数(state)算(getters)改(mutations)异(actions)模(modules)
Vuex 全局存数据, state 存、getters 算, mutations 同步改, actions 异步发, 组件用 dispatch 和 commit 调用。
最重要规则(面试必问)
- 修改 state 必须走 mutations
- 异步操作必须放 actions
- 组件不能直接改 state
- getters 相当于 store 的 computed
vuex更改state
更改数据需要在仓库中使用mutation来更改数据,在里面定义很多的处理程序handlers,来更改state中的数据;利用vuex的mapState方法来获取vuex的state对象中属性;
es6 新增特性
let,const,class,Symbol,{...}展开运算符,解构赋值,模板字符串,箭头函数,Set,Map,模块化(import / export),Proxy构造函数
let const 块级域
箭头函数无 this
解构取值超方便
模板字符串用 ``
展开运算符 ...
Promise 异步香
async await 最优雅
class 面向对象强
import export 模块化
Set 去重 Map 键值强
webpack 打包
- 打包
npm run build生成dist文件夹 (把所写vue生成最纯粹的html css js + 图标) - 打包后部署才可打开 ===> 上传服务器
vue2和vue3的区别
(一)vue2使用的是options的API 代码逻辑比较分散 可读性差 可维护性差,vue3使用的是 compositionAPI逻辑分明 可维护性高,更友好的支持TS,在template模板中支持多个根节点,支持jsx 语法,在Vue2中,每次更新diff,都是全量对比,Vue3则只对比带有标记的,这样大大减少了非动态内容的对 比消耗 vue3重写双向绑定
(二)vue2基于Object.defineProperty()实现 , vue3 基于Proxy实现
Proxy对比Object.defineProperty()的优势
1.可以监听数组变化 2.可以监听删除的属性 3.可以监听数组的索引和 length 属性等
git
一般使用 GitHub官方可视化工具(Desktop)
Git 常用的是以下 6 个命令:
git clone、拷贝一份远程仓库
git add . 添加文件到暂存区
git commit、提交暂存区到本地仓库。
git checkout、切换分支
git push、上传远程代码并合并
git pull、下载远程代码并合并,
git冲突
基本上一个人一个分支 只在自己分支上写,公共组件提前沟通交流,避免冲突
解决冲突的步骤:
- 把两个分支的代码都拉到你本地
- 手动去把代码整合一下
- 然后提交你的本地代码
路由器的两种工作模式
- hash 模式:带 #,兼容性好,不用配置后端
- history 模式:不带 #,更美观,部署需后端配置,否则刷新会 404
路由传参分为 params 传参与 query 传参
- params 传参
- 地址栏不显示参数
- 相当于POST
- 刷新页面 参数会丢失,必须用 name 跳转
- query 传参
- 地址栏显示参数
?key=value - 相当于GET
- 刷新页面 参数不会丢失,可以用 path 或 name 跳转
缓存路由组件 keep-alive
keep-alive 是 Vue 内置组件,用来缓存路由组件,避免组件反复创建、销毁,提升性能。
两个专属生命周期钩子
组件被缓存后,会多出 2 个生命周期:
- activated → 组件激活(页面显示时触发)
- deactivated → 组件失活(页面离开时触发)
原来的 created、mounted 只会执行一次!
销毁 keep-alive
需要this.$destory() 或者关闭浏览器,才能销毁缓存的组件
Vue 路由守卫(导航守卫)
路由跳转前后做拦截、判断、权限控制。
① 全局前置守卫 router.beforeEach 进入路由之前触发
② 全局后置钩子 router.afterEach进入路由之后触发,**没有 next
路由独享守卫beforeEnter 只在当前路由生效(登录拦截)
① beforeRouteEnter进入组件前
② beforeRouteUpdate路由更新、组件复用时(如详情页 id 变化)
③ beforeRouteLeave离开当前页面之前(最常用:清除表单、阻止跳转、销毁 keep-alive)
三个参数含义(必背)
-
to:要跳转到的路由
-
from:从哪个路由离开
-
next:放行函数,必须调用
next()放行next(false)阻止跳转next('/login')跳转到登录