一、前端框架的由来
1、服务端渲染
sequenceDiagram
浏览器->>+服务器: https://www.bilibili.com/
Note right of 服务器: 组装页面(服务端渲染)
服务器->>-浏览器: 完整页面
2、前后端分离
sequenceDiagram
浏览器->>服务器: https://www.bilibili.com/
服务器->>浏览器: 无内容的html
activate 浏览器
浏览器-->>服务器: ajax
服务器-->>浏览器: 各种业务数据
Note left of 浏览器: 运行js,创建元素,渲染页面
deactivate 浏览器
3、单页应用
sequenceDiagram
浏览器->>服务器: https://www.bilibili.com/
服务器->>浏览器: 无内容的html
activate 浏览器
Note left of 浏览器: 运行js,创建元素,渲染页面
浏览器-->>服务器: ajax
服务器-->>浏览器: 各种业务数据
Note left of 浏览器: 跳转页面
浏览器-->>服务器: ajax
服务器-->>浏览器: 各种业务数据
Note left of 浏览器: JS重新构建页面元素
deactivate 浏览器
4、vue
sequenceDiagram
浏览器->>服务器: https://www.bilibili.com/
服务器->>浏览器: 无内容的html
activate 浏览器
Note left of 浏览器: 运行包含vue的js,使用框架渲染页面
浏览器-->>服务器: ajax
服务器-->>浏览器: 各种业务数据
Note left of 浏览器: 使用vue-router跳转页面
deactivate 浏览器
二、vue的一些底层原理
1、注入
vue会将以下配置注入到vue实例:
- data:和界面相关的数据
- computed:通过已有数据计算得来的数据
- methods:方法
模板中可以使用vue实例中的成员
2、虚拟DOM树
直接操作真实的DOM会引发严重的效率问题,vue使用虚拟DOM(vnode)的方式来描述要渲染的内容
vnode是一个普通的js对象,用于描述界面上应该有什么,比如:
var vnode = {tag: "h1",children: [{tag: undefined,text: "Hello"}]}
上面的对象描述了:
有一个标签名为h1的节点,它有一个子节点,该子节点是一个文本,内容为【Hello】
vue模板并不是真实的DOM,它会编译为虚拟DOM
<div id="app"><h1>第一个vue应用:{{title}}</h1><p>作者:{{author}}</p>
</div>
上面的模板会被编译为类似下面结构的虚拟DOM
var vnode = {tag: "div",children: [{tag: "h1",children: [{text: "第一个vue应用:Hello World"}]}, {tag: "p",children: [{text: "作者:siuser"}]}]}
虚拟DOM树会最终成为真实的DOM树
当数据变化后,将引发重新渲染,vue会比较新旧两棵vnode tree,找出差异,然后仅把差异部分应用到真实dom tree中
可见,在vue中,要得到最终的界面,必须要生成一个vnode tree
vue通过以下逻辑生成vnode tree:
注意:虚拟节点树必须是单根的
3、挂载
将生成的真实DOM树,放置到某个元素位置,称之为挂载
挂载的方式:
- 通过el:"css选择器"进行配置
- 通过vue实例.$mount(“css选择器”)进行配置
4、完整流程
三、组件开发
1、创建组件
组件是根据一个普通的配置对象创建的,所以要开发一个组件,只需要写一个配置对象即可
该配置对象和vue实例的配置是几乎一样的
//组件配置对象
var myComp = {data(){return {// ...}},template: `....`
}
值得注意的是,组件配置对象和vue实例有以下几点差异:
- 无el
- data必须是一个函数,该函数返回的对象作为数据
- 由于没有el配置,组件的虚拟DOM树必须定义在template或render中
2、注册组件
注册组件分为两种方式,一种是全局注册,一种是局部注册
2.1、全局注册
一旦全局注册了一个组件,整个应用中任何地方都可以使用该组件
全局注册的方式是:
// 参数1:组件名称,将来在模板中使用组件时,会使用该名称
// 参数2:组件配置对象
// 该代码运行后,即可在模板中使用组件
Vue.component('my-comp', myComp)
在模板中,可以使用组件了
<my-comp />
<!-- 或 -->
<my-comp></my-comp>
但在一些工程化的大型项目中,很多组件都不需要全局使用
比如一个登录组件,只有在登录的相关页面中使用,如果全局注册,将导致构建工具无法优化打包
因此,除非组件特别通用,否则不建议使用全局注册
2.2、局部注册
局部注册就是哪里要用到组件,就在哪里注册
局部注册的方式是,在要使用组件的组件或实例中加入一个配置:
// 这是另一个要使用my-comp的组件
var otherComp = {components:{// 属性名为组件名称,模板中将使用该名称// 属性值为组件配置对象"my-comp": myComp},template: `<div><!-- 该组件的其他内容 --><my-comp></my-comp></div>`;
}
3、应用组件
在模板中使用组件特别简单,把组件名当作HTML元素名使用即可
但要注意以下几点:
- 组件必须有结束
组件可以自结束,也可以用结束标记结束,但必须要有结束
下面的组件使用是错误的:
<my-comp>
- 组件的命名
无论你使用哪种方式注册组件,组件的命名需要遵循规范。
组件可以使用kebab-case 短横线命名法,也可以使用PascalCase 大驼峰命名法
下面两种命名均是可以的
var otherComp = {components:{"my-comp": myComp, // 方式1MyComp: myComp //方式2}
}
实际上,使用
小驼峰命名法 camelCase
也是可以识别的,只不过不符合官方要求的规范
使用PascalCase
方式命名还有一个额外的好处,即可以在模板中使用两种组件名
var otherComp = {components:{MyComp: myComp}
}
模板中:
<!-- 可用 -->
<my-comp />
<MyComp />
因此,在使用组件时,为了方便,往往使用以下代码:
var MyComp = {//组件配置
}var OtherComp = {components:{MyComp // ES6速写属性}
}
四、组件树
一个组件创建好后,往往会在各种地方使用它。它可能多次出现在vue实例中,也可能出现在其他组件中
于是就形成了一个组件树
五、向组件传递数据(基础)
大部分组件要完成自身的功能,都需要一些额外的信息
比如一个头像组件,需要告诉它头像的地址,这就需要在组件时向组件传递数据
传递数据的方式有很多种,很常见的一种是使用组件属性component props
首先在组件中申明可以接收哪些属性:
var MyComp = {props:["p1", "p2", "p3"],// 和vue实例一样,使用组件时也会创建组件的实例// 而组件的属性会被提取到组件实例中,因此可以在模板中使用template: `<div>{{p1}}, {{p2}}, {{p3}}</div>`
}
在使用组件时,向其传递属性:
var OtherComp = {components: {MyComp},data(){return {a:1}},template: `<my-comp :p1="a" :p2="2" p3="3"/>`
}
注意:在组件中,属性是只读的,绝不可以更改,这叫做单向数据流
六、v-if和v-show
面试题:v-if和v-show有什么区别?
v-if能够控制是否生成vnode,也就间接控制了是否生成对应的dom。
当v-if为true时,会生成对应的vnode,并生成对应的dom元素;当其为false时,不会生成对应的vnode,自然不会生成任何的dom元素
v-show始终会生成vnode,也就间接导致了始终生成dom。
它只是控制dom的display属性,当v-show为true时,不做任何处理;当其为false时,生成的dom的display属性为none
使用v-if可以有效的减少树的节点和渲染量,但也会导致树的不稳定;
而使用v-show可以保持树的稳定,但不能减少树的节点和渲染量
因此,在实际开发中,显示状态变化频繁的情况下应该使用v-show,以保持树的稳定;
显示状态变化较少时应该使用v-if,以减少树的结点和渲染量
七、组件事件
- 抛出事件:子组件在某个时候发生了一件事,但自身无法处理,于是通过事件的方式通知父组件处理
- 事件参数:子组件抛出事件时,传递给父组件的数据
- 注册事件:父组件申明,当子组件发生某件事的时候,自身将做出一些处理
八、插槽
在某些组件模板中,有一部分区域需要父组件来指定
<!-- message组件:一个弹窗消息 -->
<div class="message-container"><div class="content"><!-- 这里是消息内容,可以是一个文本,也可能是一段html,具体是什么不知道,需要父组件指定 --></div><button>确定</button><button>关闭</button>
</div>
1、插槽的简单用法
此时,就需要使用插槽来定制组件的功能
<!-- message组件:一个弹窗消息 -->
<div class="message-container"><div class="content"><!-- slot是vue的内置组件 --><slot></slot></div><button>确定</button><button>关闭</button>
</div><!-- 父组件App -->
<Message><div class="app-message"><p>App Message</p><a href="">detail</a></div>
</Message><!-- 最终的结果 -->
<div class="message-container"><div class="content"><div class="app-message"><p>App Message</p><a href="">detail</a></div></div><button>确定</button><button>关闭</button>
</div>
2、具名插槽
如果某个组件中需要父元素传递多个区域的内容,也就意味着需要提供多个插槽
为了避免冲突,就需要给不同的插槽赋予不同的名字
<!-- Layout 组件 -->
<div class="layout-container"><header><!-- 我们希望把页头放这里,提供插槽,名为header --><slot name="header"></slot></header><main><!-- 我们希望把主要内容放这里,提供插槽,名为default --><slot></slot></main><footer><!-- 我们希望把页脚放这里,提供插槽,名为footer --><slot name="footer"></slot></footer>
</div><!-- 父组件App -->
<BaseLayout><template v-slot:header><h1>Here might be a page title</h1></template><template v-slot:default><p>A paragraph for the main content.</p><p>And another one.</p><template v-slot:default><template v-slot:footer><p>Here's some contact info</p></template>
</BaseLayout>
九、路由
vue-router官网:https://router.vuejs.org/zh/
1、路由插件
npm i vue-router
路由插件的使用
import Vue from 'vue'
import VueRouter from 'vue-router'Vue.use(VueRouter); // Vue.use(插件) 在Vue中安装插件const router = new VueRouter({// 路由配置
})
new Vue({...,router
})
2、基本使用
// 路由配置
const router = new VueRouter({routes: [ // 路由规则// 当匹配到路径 /foo 时,渲染 Foo 组件{ path: '/foo', component: Foo },// 当匹配到路径 /bar 时,渲染 Bar 组件{ path: '/bar', component: Bar }]
})
<!-- App.vue -->
<div class="container"><div><!-- 公共区域 --></div><div><!-- 页面区域 --><!-- vue-router 匹配到的组件会渲染到这里 --><RouterView /></div>
</div>
3、路由模式
- 路由从哪里获取访问路径
- 路由如何改变访问路径
vue-router提供了三种路由模式:
- hash:默认值。路由从浏览器地址栏中的hash部分获取路径,改变路径也是改变的hash部分。该模式兼容性最好
http://localhost:8081/#/blog --> /blog
http://localhost:8081/about#/blog --> /blog
- history:路由从浏览器地址栏的location.pathname中获取路径,改变路径使用的H5的history api。该模式可以让地址栏最友好,但是需要浏览器支持history api
http://localhost:8081/#/blog --> /
http://localhost:8081/about#/blog --> /about
http://localhost:8081/blog --> /blog
- abstract:路由从内存中获取路径,改变路径也只是改动内存中的值。这种模式通常应用到非浏览器环境中
内存: / --> /
内存: /about --> /about
内存: /blog --> /blog
4、导航
vue-router提供了全局的组件RouterLink,它的渲染结果是一个a元素
<RouterLink to="/blog">文章</RouterLink>
<!-- mode:hash 生成 -->
<a href="#/blog">文章</a>
<!-- mode:history 生成 -->
<!-- 为了避免刷新页面,vue-router实际上为它添加了点击事件,并阻止了默认行为,在事件内部使用hitory api更改路径 -->
<a href="/blog">文章</a>
激活状态
默认情况下,vue-router会用当前路径匹配导航路径
- 如果当前路径是以导航路径开头,则算作匹配,会为导航的a元素添加类名router-link-active
- 如果当前路径完全等于导航路径,则算作精确匹配,会为导航的a元素添加类名router-link-exact-active
例如,当前访问的路径是/blog
,则:
导航路径 | 类名 |
---|---|
/ | router-link-active |
/blog | router-link-active router-link-exact-active |
/about | 无 |
/message | 无 |
可以为组件RouterLink
添加bool属性exact
,将匹配规则改为:必须要精确匹配才能添加匹配类名router-link-active
例如,当前访问的路径是/blog
,则:
导航路径 | exact | 类名 |
---|---|---|
/ | true | 无 |
/blog | false | router-link-active router-link-exact-active |
/about | true | 无 |
/message | true | 无 |
例如,当前访问的路径是/blog/detail/123
,则:
导航路径 | exact | 类名 |
---|---|---|
/ | true | 无 |
/blog | false | router-link-active |
/about | true | 无 |
/message | true | 无 |
另外,可以通过active-class
属性更改匹配的类名,通过exact-active-class
更改精确匹配的类名
5、命名路由
使用命名路由可以解除系统与路径之间的耦合
// 路由配置
const router = new VueRouter({routes: [ // 路由规则// 当匹配到路径 /foo 时,渲染 Foo 组件{ name:"foo", path: '/foo', component: Foo },// 当匹配到路径 /bar 时,渲染 Bar 组件{ name:"bar", path: '/bar', component: Bar }]
})
<!-- 向to属性传递路由信息对象 RouterLink会根据你传递的信息以及路由配置生成对应的路径 -->
<RouterLink :to="{ name:'foo' }">go to foo</RouterLink>
十、vue的一些边角知识
1、css module
当在main.js使用纯dom操作,需要引用css module
需要将样式文件命名为xxx.module.ooo
xxx
为文件名
ooo
为样式文件后缀名,可以是css、less
// 测试一下纯DOM操作
import styles from "./styles/message.module.less";
console.log(styles);
const div = document.createElement("div");
div.className = styles.message;
div.innerText = "asdfasdf";
document.body.appendChild(div);
2、得到组件渲染的Dom
/**获取某个组件渲染的Dom根元素*/
function getComponentRootDom(comp, props){const vm = new Vue({render: h => h(comp, {props})})vm.$mount();return vm.$el;
}
3、扩展vue实例
prototype的知识点延申
4、ref
<template><div><p ref="para">some paragraph</p><ChildComp ref="comp" /><button @click="handleClick">查看所有引用</button></div>
</template><script>import ChildComp from "./ChildComp"export default {components:{ChildComp},methods:{handleClick(){// 获取持有的所有引用console.log(this.$refs);/*{para: p元素(原生DOM),comp: ChildComp的组件实例}*/}}}
</script>
通过ref可以直接操作dom元素,甚至可能直接改动子组件,这些都不符合vue的设计理念。
除非迫不得已,否则不要使用ref
十一、远程获取数据的意义
1、开发环境有跨域问题
sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>后端测试服务器: ajax 跨域:http://test-data:3000/api/news
后端测试服务器->>浏览器: JSON数据
rect rgb(224,74,74)
Note right of 浏览器: 浏览器阻止数据移交
end
2、生产环境没有跨域问题
sequenceDiagram
浏览器->>服务器: http://www.my-site.com/
服务器->>浏览器: 页面
浏览器->>服务器: ajax:http://www.my-site.com/api/news
服务器->>浏览器: JSON数据
sequenceDiagram
浏览器->>静态资源服务器: http://www.my-site.com/
静态资源服务器->>浏览器: 页面
浏览器->>数据服务器: ajax 跨域:http://api.my-site.com/api/news
数据服务器->>浏览器: [允许www.my-site.com]JSON数据
3、解决开发环境的跨域问题
sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>前端开发服务器: ajax:http://localhost:8080/api/news
前端开发服务器->>后端测试服务器: 代理请求:http://test-data:3000/api/news
后端测试服务器->>前端开发服务器: JSON数据
前端开发服务器->>浏览器: JSON数据
4、为什么要用mock数据
sequenceDiagram
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>前端开发服务器: ajax:http://localhost:8080/api/news
前端开发服务器->>后端测试服务器: 代理请求:http://test-data:3000/api/news
后端测试服务器->>前端开发服务器: 404 (后端正在开发中)
前端开发服务器->>浏览器: 404
sequenceDiagram
participant 浏览器
participant MockJS
participant 前端开发服务器
activate MockJS
Note left of MockJS: 定义ajax拦截规则
deactivate MockJS
浏览器->>前端开发服务器: http://localhost:8080/
前端开发服务器->>浏览器: 页面
浏览器->>MockJS: ajax:http://localhost:8080/api/news
MockJS->>浏览器: 模拟的JSON数据
十二、组件生命周期
1、常见应用
1.1、加载远程数据
export default {data(){return {news: []}},async created(){this.news = await getNews();}
}
1.2、直接操作DOM
export default {data(){return {containerWidth:0,containerHeight:0}},mounted(){this.containerWidth = this.$refs.container.clientWidth;this.containerHeight = this.$refs.container.containerHeight;}
}
1.3、启动和清除计时器
export default {data(){return {timer: null}},created(){this.timer = setInterval(()=>{... }, 1000)},destroyed(){clearInterval(this.timer); }
}
十三、自定义指令
1、全局定义
// 指令名称为:mydirec1
Vue.directive('mydirec1', {// 指令配置
})// 指令名称为:mydirec2
Vue.directive('mydirec2', {// 指令配置
})
之后,所有的组件均可以使用mydirec1
和mydirec2
指令
<template><!-- 某个组件代码 --><div><MyComp v-mydirec1="js表达式" /><div v-mydirec2="js表达式">...</div><img v-mydirec1="js表达式" /></div>
</template>
2、局部定义
局部定义是指在某个组件中定义指令,和局部注册组件类似。
定义的指令仅在该组件中有效。
<template><!-- 某个组件代码 --><div><MyComp v-mydirec1="js表达式" /><div v-mydirec2="js表达式">...</div><img v-mydirec1="js表达式" /></div>
</template><script>
export default {// 定义指令directives: {// 指令名称:mydirec1mydirec1: {// 指令配置},// 指令名称:mydirec2mydirec2: {// 指令配置}}
}
</script>
和局部注册组件一样,为了让指令更加通用,通常我们会把指令的配置提取到其他模块。
<template><!-- 某个组件代码 --><div><MyComp v-mydirec1="js表达式" /><div v-mydirec2="js表达式">...</div><img v-mydirec1="js表达式" /></div>
</template><script>// 导入当前组件需要用到的指令配置对象import mydirec1 from "@/directives/mydirec1";import mydirec2 from "@/directives/mydirec2";export default {// 定义指令directives: {mydirec1,mydirec2}}
</script>
指令配置对象
没有配置的指令,就像没有配置的组件一样,毫无意义
vue
支持在指令中配置一些钩子函数,在适当的时机,vue
会调用这些钩子函数并传入适当的参数,以便开发者完成自己想做的事情。
常用的钩子函数:
// 指令配置对象
{bind(){// 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。},inserted(){// 被绑定元素插入父节点时调用。},update(){// 所在组件的 VNode 更新时调用}
}
查看更多的钩子函数
每个钩子函数在调用时,vue
都会向其传递一些参数,其中最重要的是前两个参数
// 指令配置对象
{bind(el, binding){// el 是被绑定元素对应的真实DOM// binding 是一个对象,描述了指令中提供的信息}
}
bingding 对象
查看更多bingding对象的属性
配置简化
比较多的时候,在配置自定义指令时,我们都会配置两个钩子函数
{bind(el, bingding){},update(el, bingding){}
}
这样,在元素绑定和更新时,都能运行到钩子函数
如果这两个钩子函数实现的功能相同,可以直接把指令配置简化为一个单独的函数:
function(el, bingding){// 该函数会被同时设置到bind和update中
}
利用上述知识,可满足大部分自定义指令的需求
更多的自定义指令用法见官网