一、Vue是什么
二、Vue核心特性
三、Vue跟传统开发的区别
四、SPA是什么
五、SPA和MPA的区别
六、实现一个SPA
七、如何给SPA做SEO
八、v-show与v-if
九、v-for与v-if
十、Vue实例挂载的过程
十一、Vue的生命周期
十二、如何解决SPA首屏加载速度慢
十三、为什么data属性是一个函数而不是一个对象
十四、动态给vue的data添加一个新的属性
十五、Vue中的组件和插件
十六、组件间通信
十七、NextTick
十八、vue中的mixin
Vue.js(简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。
这里的详细解释参考之前的文章:http://t.csdn.cn/Mab2F
Model:模型层,负责处理业务逻辑以及和服务器端进行交互
View:视图层:负责将数据模型转化为UI展示出来,可以理解为HTML页面
ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁
此处双向绑定也是考点,参考:http://t.csdn.cn/z97Te
把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue
中每一个.vue
文件都可以视为一个组件
1.降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
2.调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
3.提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级
指令是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM
常用的指令:
条件渲染指令 v-if
列表渲染指令 v-for
属性绑定指令 v-bind
---单向绑定
事件绑定指令 v-on
---事件监听
双向数据绑定指令 v-model
Vue所有的界面事件,都是只去操作数据的,Jquery操作DOM
Vue所有界面的变动,都是根据数据自动绑定出来的,Jquery操作DOM
即单页应用。SPA
是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换,打断用户体验。在单页应用中,所有必要的代码(HTML
、JavaScript
和CSS
)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面
在MPA
(多页应用)中,每个页面都是一个独立的主页面。当访问另一个页面时,都需要重新加载html
、css
、js
文件
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和多个页面片段 | 多个主页面 |
刷新方式 | 局部刷新 | 整页刷新 |
url模式 | 哈希模式 (Hash) | 历史模式 (History) |
SEO(搜索引擎优化) | 难实现,可使用SSR方式改善 | 容易实现 |
数据传递 | 容易 | 通过url、cookie、localStorage等传递 |
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
SEO: 利用搜索引擎的规则,提高网站在有关搜索引擎内的自然排名
SSR:服务器渲染
优点
具有桌面应用的即时性、网站的可移植性和可访问性
用户体验好、快,内容的改变不需要重新加载整个页面
良好的前后端分离,分工更明确
缺点
不利于搜索引擎的抓取
首次渲染速度相对较慢
1.显示当前界面,点击界面按钮,或浏览器回退/前进按钮触发 hash 变化
2.检测 hash 或者 pushstate 变化
3.以当前 hash 为索引,加载对应资源
4.等待资源加载完毕,隐藏之前的界面,执行回调
hash 模式
通过监听url
中的hash
来进行路由跳转
history 模式
借用 HTML5 history api
,api
提供了丰富的 router
相关属性
history 相关的api
history.pushState: 浏览器历史记录添加记录
history.replaceState: 修改浏览器历史纪录中当前记录
history.popState: 当 history 发生变化时触发
三种方式
1.SSR服务端渲染
将组件或页面通过服务器生成html,再返回给浏览器
2.静态化
目前主流的静态化主要有两种:
(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
(2)另外一种是通过WEB服务器的 URL Rewrite
的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。
3.使用Phantomjs针对爬虫处理
通过Nginx
配置,判断访问来源是否为爬虫,如果是,则搜索引擎的爬虫请求会转发到一个node server
,再通过PhantomJS
来解析完整的HTML
,返回给爬虫。
能控制元素在页面是否显示
当表达式为true
时,都会占据页面的位置
当表达式都为false
时,都不会占据页面位置
v-show
控制显示与隐藏:为该元素添加display:none
,dom
元素仍存在
v-if
控制显示与隐藏:是将dom
元素整个添加或删除
v-show
只是基于css切换
v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件
v-show
由false
变为true
的时候不会触发组件的生命周期
v-if
是真正的条件渲染,它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。渲染条件为假时不做操作,直到为真才渲染。由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子。由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法
v-show
有更高的初始渲染消耗
v-if
有更高的切换消耗
v-show与v-if的使用场景
v-if
与 v-show
都能控制dom
元素在页面的显示
v-if
相比 v-show
开销更大的(直接操作dom
节点增加与删除)
如果需要频繁地切换,使用 v-show 较好
如果在运行时条件很少改变,使用 v-if 较好
1.v-for
优先级比v-if
高
2.v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true
值时被渲染
3.v-for
指令基于一个数组来渲染一个列表,使用 item in items
形式的特殊语法,其中 items
是源数据数组或对象, item
是被迭代的数组元素
在使用 v-for
的时候,建议设置唯一的key
值,便于diff
算法进行优化(一般不建议使用index值)
一般不会同时使用两者,如果确实需要同时使用,则不编写在同一元素身上,可以在外层嵌套template
标签,在template 层进行 v-if 判断,在内部进行 v-for 循环
如果 v-if 判断 出现在 v-for 循环 内部,可通过计算属性computed
提前过滤掉那些不需要显示的项
computed: {
items: function() {
return this.list.filter(function (item) {
return item.isShow
})
}
}
new Vue
会调用_init
方法
定义 $set
、$get
、$delete
、$watch
等方法
定义 $on
、$off
、$emit
、$off
等事件
定义 _update
、$forceUpdate
、$destroy
生命周期
调用$mount
进行页面的挂载
挂载的时候主要是通过mountComponent
方法
定义updateComponent
更新函数
执行render
生成虚拟DOM
_update
将虚拟DOM
生成真实DOM
结构,并且渲染到页面中
在Vue
中,实例从创建到销毁的过程就是生命周期(创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等过程)
生命周期 | 描述 |
---|---|
beforeCreate | 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务 |
created | 组件初始化完毕,各种数据可以使用,常用于异步数据获取 |
beforeMount | 未执行渲染、更新,dom未创建 |
mounted | 初始化结束,dom已创建,可用于获取访问数据和dom元素 |
beforeUpdate | 更新前,可用于获取更新前各种状态 |
updated | 更新后,所有状态已是最新 |
beforeDestroy | 销毁前,可用于一些定时器或订阅的取消 |
destroyed | 组件已销毁,作用同上组件实例完成销毁 |
activated | keep-alive 缓存的组件激活时 |
deactivated | keep-alive 缓存的组件停用时 |
errorCaptured | 捕获来自子孙组件的错误时 |
在钩子函数 created、beforeMount、mounted 中都可以尽心异步数据请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
· 能更快获取到服务端数据,减少页面加载时间,用户体验更好
· SSR 不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性
首屏时间是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间。此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容。
1.网络延时问题
2.资源文件体积是否过大
3.资源是否重复发送请求去加载
4.加载脚本的时候,渲染内容堵塞
1.减小入口文件体积
通过路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加
2.静态资源本地缓存
前端合理利用localStorage
后端返回资源问题:
采用HTTP
缓存,设置Cache-Control
,Last-Modified
,Etag
等响应头
采用Service Worker
离线缓存
3.UI框架按需加载
不要直接引用整个UI库
import ElementUI from 'element-ui'
而是按需引用,减小文件缓存体积
import { TableColumn, MessageBox } from 'element-ui'
4.组件重复打包
假设A.js
文件是一个常用的库,现在有多个路由使用了A.js
文件,这就造成了重复下载
解决方案:在webpack
的config
文件中,修改CommonsChunkPlugin
的配置
minChunks: 3 // 表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
5.图片资源的压缩
6.开启GZip压缩
拆完包之后,用gzip
做一下压缩,安装compression-webpack-plugin
cnmp i compression-webpack-plugin -D
在vue.congig.js
中引入并修改webpack
配置
const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}
在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip
,就发送给它gzip
格式的文件 我的服务器是用express
框架搭建的 只要安装一下compression
就能使用
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用
7.使用SSR
SSR(服务端渲染),组件或页面通过服务器生成html字符串,再发送到浏览器。使用Nuxt.js
实现服务端渲染
根实例对象data
可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
组件实例对象data
必须为函数,目的是为了防止多个组件实例对象之间共用一个data
,产生数据污染。采用函数的形式,initData
时会将其作为工厂函数都会返回全新data
对象
定义一个p标签用于显示data属性中的数据,点击按钮实现增加新数据的效果(同时改变vue中的data属性以及UI界面的显示)
{{ value }}
我们在vue
实例中,定义data
属性和methods
方法,然后通过绑定点击事件输出data中的数据,点击按钮,数据更新了(console
打印出了新属性),但页面没有更新,仍然只显示 ‘旧属性’
const app = new Vue({
el:"#app",
data:()=>{
item:{
oldProperty:"旧属性"
}
},
methods:{
addProperty(){
this.items.newProperty = "新属性" // 为items添加新属性
console.log(this.items) // 输出带有newProperty的items
}
}
})
在vue2中,因为初始设置的数据可以触发setter
与getter
,从而变成响应式数据,而按钮点击新增的数据并不会通过Object.defineProperty 变成响应式数据,不会自动触发setter
与getter
解决方案
如果为对象添加少量的新属性,可以直接采用Vue.set()
如果需要为新对象添加大量的新属性,则通过Object.assign()
创建新对象
vue3
是用过proxy
实现数据响应式的,直接动态添加新属性仍可以实现数据响应式
Vue 核心特性中介绍了组件的定义,而插件是用来为 Vue
添加全局功能
插件的功能范围没有严格的限制——一般有下面几种:
添加全局方法或者属性。如: vue-custom-element
添加全局资源:指令/过滤器/过渡等。如 vue-touch
通过全局混入来添加一些组件选项。如vue-router
添加 Vue
实例方法,通过把它们添加到 Vue.prototype
上实现。
一个库,提供自己的 API
,同时提供上面提到的一个或多个功能。如vue-router
每一个.vue
文件都可以看成是一个组件
`vue`文件标准格式
还可以通过template
属性来编写组件,如果组件内容多,可以在外部定义template
组件内容
// 组件显示的内容
component!
Vue.component('componentA',{
template: '#testComponent'
template: `component` // 组件内容少可以通过这种形式
})
vue
插件的实现应该暴露一个 install
方法。这个方法的第一个参数是 Vue
构造器,第二个参数是一个可选的选项对象
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
vue
组件注册主要分为全局注册与局部注册
全局注册通过Vue.component
方法,第一个参数为组件的名称,第二个参数为传入的配置项
Vue.component('my-component', { /* ... */ })
局部注册只需在用到的地方通过components
属性注册一个组件
const component1 = {...} // 定义一个组件
export default {
components:{
component1 // 局部注册
}
}
插件的注册通过Vue.use()
的方式进行注册,第一个参数为插件的名字,第二个参数是可选择的配置项
Vue.use('my-plugin', { /* ... */} )
注册插件的时候,需要在调用 new Vue()
启动应用之前完成
Vue.use
会自动阻止多次注册相同插件,只会注册一次
组件 (Component)
是用来构成 App
的业务模块,它的目标是 App.vue
插件 (Plugin)
是对Vue
的功能的增强或补充,它的目标是 Vue
本身,
每个组件之间的都有独自的作用域,组件间通信是为了组件间数据共享
父子组件之间的通信
兄弟组件之间的通信
祖孙与后代组件之间的通信
非关系组件间之间的通信
父子关系的组件数据传递选择 props
与 $emit
进行传递,也可选择ref
子组件设置props
属性,定义接收父组件传递过来的参数
// Children.vue
props:{
// 字符串形式
name:String, // 接收的类型参数
// 对象形式
age:{
type:Number, // 接收的类型为数值
defaule:18, // 默认值为18
require:true // age属性必须传递
}
}
父组件在使用子组件标签中通过字面量来传递值
// Father.vue
子组件通过$emit触发
自定义事件,$emit
第二个参数为传递的数值
// Children.vue
this.$emit('add', good)
父组件绑定监听器获取到子组件传递过来的参数
// Father.vue
父组件在使用子组件的时候设置ref
,父组件通过设置子组件ref
来获取数据
// Father.vue
this.$refs.foo // 获取子组件实例,通过子组件实例能拿到对应的数据
兄弟关系的组件数据传递可选择$bus
,或者选择$parent
进行传递
创建一个中央事件总线EventBus
// 创建一个全局事件总线
class Bus {
constructor() {
this.callbacks = {}; // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
兄弟组件通过$emit
触发自定义事件,$emit
第二个参数为传递的数值
// Brother1.vue
this.$bus.$emit('foo')
另一个兄弟组件通过$on
监听自定义事件
// Brother2.vue
this.$bus.$on('foo', this.handle)
$parent
或者$root
(兄弟组件传值)// Brother1.vue
this.$parent.on('add',this.add)
// Brother2.vue
this.$parent.emit('add')
祖先与后代组件数据传递可选择attrs
与listeners
或者 Provide
与 Inject
设置批量向下传属性$attrs
和 $listeners
// child:并未在props中声明foo
{{$attrs.foo}}
// parent
包含了父级作用域中不作为 prop
被识别 (且获取) 的特性绑定 ( class 和 style 除外),通过 v-bind="$attrs"
传⼊内部组件
// 给Grandson隔代传值,communication/index.vue
// Child2做展开
// Grandson使⽤
{{msg}}
在祖先组件定义provide
属性,返回传递的值
// GrandParent
provide(){
return {
foo:'foo'
}
}
在后代组件通过inject
接收组件传递过来的值
// Grandson
inject:['foo'] // 获取到祖先组件传递过来的值
vuex
存放共享的变量的容器(复杂关系的组件数据传递)state
用来存放共享变量
getter
可以增加一个getter
派生状态,(相当于store
中的计算属性),用来获得共享变量的值
mutations
用来存放修改state
的方法
actions
是在mutations
的基础上进行,也用来存放修改state
的方法。常用来进行异步操作
是一种优化策略。在下次 DOM 更新循环结束之后,执行延迟回调。即修改数据和更新数据是异步执行,在修改数据之后立即使用这个方法,获取更新后的 DOM
如果不适用 nextTick
更新机制,那么 num
每次更新值,都会触发视图更新
{{num}}
for(let i=0; i<100000; i++){
num = i
}
有了nextTick
机制,只需要等更新循环结束之后,更新一次
在修改数据后,想立刻得到更新后的DOM
结构,可以使用Vue.nextTick()
第一个参数为:回调函数(可以获取最近的DOM
结构)
第二个参数为:执行函数上下文
// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
Vue.nextTick(function () {
// DOM 更新了
console.log(vm.$el.textContent) // 修改后的值
})
组件内使用 vm.$nextTick()
实例方法只需要通过this.$nextTick()
,并且回调函数中的 this
将自动绑定到当前的 Vue
实例上
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '修改后的值'
})
$nextTick()
会返回一个 Promise
对象,可以是用async/await
完成相同作用的事情
this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'
mixin
(混入),提供了一种非常灵活的方式,来分发 Vue
组件中的可复用功能,本质是一个js
对象,可以包含组件中任意功能选项,如data
、components
等,将共用的功能以对象的方式传入 mixins
选项中,当组件使用 mixins
对象时,所有mixins
对象的选项都将被混入该组件本身的选项中来
定义一个mixin
对象
var Mixin = {
methods: {
hello: function () {
console.log('hello mixin!')
}
}
}
组件通过mixins
属性调用mixin
对象
Vue.component('componentA',{
mixins: [Mixin]
})
该组件在使用的时候,混合了mixin
里面的方法,执行hello
方法
通过Vue.mixin()
进行全局的混入,常用于插件的编写
Vue.mixin({
created: function () {
console.log("全局混入")
}
})
使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)
当组件A和组件B都要实现hello这个function时,为了减少代码重复,通过Vue
的mixin
功能hello这个方法提出来,在两个组件中都调用mixin即可
const hello = {
methods: {
Hello() {
console.log('hello!')
}
}
}
const A = {
template: '#A',
mixins: [hello]
};
const B = {
template: '#B',
mixins: [hello]
}
参考文献:面试官:SSR解决了什么问题?有做过SSR吗?你是怎么做的? | web前端面试 - 面试官系列