一、前言
使用vue,大部分是使用前后端分离技术,前端vue单独打包成项目运行。首先大概介绍一下vue脚手架,nodejs和webpack之间的关系。
vue脚手架:
vue官方提供的vue项目目录结构。无需程序员手动搭建框架,使用脚手架的架构,继续开发项目即可。
webpack:
在vue脚手架项目中,会使用到很多ES6语法和特殊结构。而这些语法和结构,是我们在开发中应用的,最终部署成项目,结构会发生变化,ES6语法浏览器也不能很好的支持。这时webpack就发挥了作用。它会把工程中的语法转换成浏览器认识的语法,并会对项目结构进行整理,打包,最终形成打包后的项目。
nodejs:
js的运行环境。webpack打包工具依赖nodejs环境才能工作。
由上面可知,想使用vue搭建前端项目,需要搭建nodejs环境,webpack工具并安装vue脚手架。
二、脚手架介绍
2.1 脚手架的安装
1.需要首先搭建node.js环境,使用内置的npm命令来搭建脚手架环境。
2. npm install -g @vue/cli --安装vue脚手架环境
3. 在合适目录下创建vue项目:vue create 项目名
这里,选择Vue2版本。创建完后,一个脚手架工程就好了。这个项目现在就可以启动,输入npm run serve 命令,启动项目。
此时,这个工程就相当于是一个helloworld。我们自己的工程,在此基础上改造就可以。2.2脚手架目录结构解析
babel.config.js: ES6转ES5的配置文件
package.json: 类似于包的说明书
package-lock.json: 锁定依赖包版本的文件。
node_modules文件夹:脚手架为我们自动创建的依赖的第三方库,都放到了这个文件夹下。
src文件夹:
assets文件夹:静态资源文件夹,放图片,视频等资源
components:所有程序员定义的组件,都放到这里
App.vue文件:所有组件的总管。
main.js文件:程序的入口,vue对象在这里创建。
public文件夹:放html文件。vue就是在渲染这里的html文件。
2.3 main.js详解
首先,main.js是程序的入口,那这个入口文件,是在哪里配置定义的呢?
这是脚手架默认配置的,并且把配置文件隐藏起来了。输入命令vue inspect >output.js,就可以在项目中生成其默认配置文件。
需要注意的是,这里生成配置文件只是让你看看,直接改这里的配置,不能生效。
想覆盖脚手架的默认配置,在vue.config.js文件中进行修改。修改项配置可在脚手架官网进行查看。
2.4 main.js源码详解
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
首先,import进来vue。这里的vue,是node_modules里的vue类库。在这里引用之后,这里集成的组件,就无需再引入vue类库了。
import App from './App.vue'
这句是引入App组件。App组件是所有子组件的总管。App组件汇总其他子组件后,交给main.js中的Vue对象。
下面看创建Vue对象的代码:
new Vue({
render: h => h(App),
}).$mount('#app')
这里使用了render函数,看其官网介绍:
使用了render函数,就不能再使用el来绑定dom元素了,所以使用了$mount方法来绑定。
其中,render函数有一个参数,是一个函数,函数名叫createElement。用于创建元素用的。这样也能绑定组件。render函数必须写return,否则页面无法加载元素。
render函数的完整写法如下:
render(h){
return h(APP名字)
}
因为方法里没有用到关键字this,所以可以使用箭头函数来简写,因为有参数,又有返回值,所以简写后为:
render: h => h(App),
三、组件化编程
3.1 什么是组件
上述脚手架中,带有.vue后缀的文件,都是组件。实现应用中局部功能的代码和资源的集合,就是一个组件。局部功能可以按照功能点儿划分,如搜索组件、列表组件等。也可以根据布局划分,如头部组件,尾部组件等。一个组件里包含了html,css,js,img等等东西。
3.2组件的定义
语法如下:
var Profile = Vue.extend({
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
});
需要注意的是,data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数,不能写成简写。
还需要注意的是,组件最终都引用在了App.vue组件中,而App.vue组件,也被main.js所引用。所以,定义的所有组件,都要使用export来修饰,才能使其正常在其他地方import进去。
甜点:export的几种写法:
export是ES6的语法。这里简单说一下几种方式。
- export default方式:
这种方式,只能导出一个对象。default只能使用一次。所以,使用export default时需要把导出的内容都放在一个对象中导出。 - export 单个对象
就是每个需要导出的函数,对象,变量前面都加上export关键字,多写几个关键字的事儿。 - export {a,b,c}
也是形成了一个对象,直接导出了这个对象,不用写default关键字了。这种方式导出的内容,在import时,需要import {a,b,c} from xxx来导入,也可以import * from xxx来全部导入。
ES6简写规则:
需要注意的是,Vue.extend写法,需要在每个组件文件中,都要import进vue才行,否则就会报错。所以,一般使用简写方式来定义组件。简写方式就是直接写{},就是组件,然后定义其属性即可。最后导出组件。最终写法就是:
export default {
//组件属性
... ...
}
3.3 组件对象的本质
组件,可以看作是一个小的vue对象。组件对象的内部,和vue对象的内部结构是一样的。所以,在第一节中讲到的vue对象的所有属性,在组件对象中,都可以定义。
下面,定义一个多属性的组件,如下:
import Vue from 'vue'
var a= Vue.extend({
name: 'HelloWorld',
data: function (){
return {
"firstName":'zhang',
"lastName":'san',
}
},
methods:{
method1(){},
method2(){},
}
});
console.log(new a);
export default a;
通过console.log输出一下a对象,如下:
首先可以看到,插件对象,本质是一个VueComponent对象。
而其内部结果,和第一节中看到的vue对象基本一模一样。有_data属性,且也把data中的属性提出来了,形成了get,set方法。等等。
需要注意的是,每一个组件,都是新new VueComponent。每个组件都是一个新对象。3.4 VueComponent对象和vm对象的区别与联系
1.vc定义时,不能用el配置项,且data函数不能简写。不能简写主要是为了防止组件复用时一处修改值,而其他复用组件的地方值也修改问题发生。
2.this指向问题:
在vm的data函数、methods函数,watch函数,computed函数中,this指的是vm实例对象。
在vc的data函数,methods函数,watch函数,computed函数中,this指的是vc实例对象。
3.vm实例中的$children属性,是其组件的集合,如下图:
4.vc中如何获取vm上的属性和数据:
js实例对象的_proto_是隐式原型链,在实例对象本身没有的属性或方法,如果在_proto_中有,也 可以直接js对象实例进行调用。
一个重要的内置关系:VueComponent.prototype.proto ===Vue.prototype。
由上面vc的内置关系可知,vc中,直接.属性,可以获取到vm中的属性。其最终走的就是原型链,找到的vm中的属性。
四、组件的用法
4.1组件的结构
组件中,<template>
标签中写html元素,需要一个大的<div>
包裹其中,也就是需要有一个跟元素,而不能是多个根元素。<script>
中定义组件对象,<style>
中写css样式,为了与其他组件的样式区分,加scoped属性,即<style scoped>
。
在<template>
中,可以使用插值表达式等等方式,来写页面,并为页面赋值。
4.2组件的用法
定义好组件后,在引用组件的<script>
中,import进来该组件,就可以使用这个组件了。使用方式就是在<template>
中,写该组件的名字标签,即可。
比如在App.vue中使用HelloWorld组件,那么在App.vue的<script>
中,
import HelloWorld from './components/HelloWorld.vue'
在components属性中,加上HelloWorld组件:
export default {
name: 'App',
components: {
HelloWorld
}
}
然后在<template>
中,写<HelloWorld />
。那么HelloWorld组件中的内容就出现在了App.vue中。
4.3 ref属性
在使用vue后,尽量避免直接操作dom元素,否则就失去了使用vue框架的意义。那么在一个方法中,如何通过id获取到一个元素呢?这就用到了ref属性。
ref用在普通html元素中,那获取到的就是dom元素,ref写在子组件标签中,那获取到的就是VueComponent对象。
通过VueComponent对象的$refs属性,可以获取到定义的ref集合,然后再通过 .属性,来具体拿到某个元素。示例如下:
<template>
<div id="app">
<HelloWorld ref="ceshi" msg="Welcome to Your Vue.js App"/>
<div ref="ceshi1">666</div>
</div>
</template>
Helloword组件标签定义了ref属性,普通dom元素div定义了ref属性,通过钩子方法mounted函数来输出,如下:
export default {
name: 'App',
components: {
HelloWorld
},
mounted() {
console.log('$refs',this.$refs);
console.log('mounted-ceshi',this.$refs.ceshi);
console.log('mounted-ceshi1',this.$refs.ceshi1);
}
}
输出结果如下:
可见$refs属性将我们定义的ref属性作为了key,vaule是相应的dom对象或组件对象。
组件标签上的ref获得的是VueComponent对象。
普通dom元素直接获取的dom元素内容。
4.4props属性
组件是可以多处复用的。而复用时,父组件可能会有重新定义子组件中data中属性值的需求,此时,就需要用props属性了。
props属性用于子组件中,定义父组件中可以赋值的属性。定义在props中的属性就无需再定义在data中了。
简写方式:
props:[‘key1’,‘key2’,‘key3’]
这种写法,key1,ke2,key3就是可以在父组件中赋值的属性。
全写方式:
props:{
‘key1’:{
type:String, //类型
required:false,//是否必传
default:123 //默认值
}
}
在props中定义的属性,也是组件中有的属性,其使用方式等同于在data中定义的属性的使用方式。
props中定义的属性,不支持运算,这样vue会报错。如果想对props属性进行运算,就定义一个中间变量,去对props属性进行运算,而不是直接对props属性进行运算。如下图:
props中,不仅可以定义属性,还可以定义函数。定义函数的意思就是通过父组件来传递函数,定义子组件中这个函数的具体实现。
子组件定义了props属性后,父组件怎么给其传值呢?
如果是固定值,则直接把props属性当作子组件标签的一个属性传值即可。如果想给子组件的props属性传vue中的数据,那么,需要写成:props属性=(父组件数据)。多加个冒号,也就是v-bind的缩写,这样,才会去vue中找对应的数据。
示例如下:
首先,定义子组件,并定义props属性。
<template>
<div class="hello">
<!-- 使用props中定义的属性 -->
<h1>{{ msg }}</h1>
<h2>{{ceshi}}</h2>
<h3>{{hanshu()}}</h3>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String,
ceshi: String,
hanshu:Function //定义函数
}
}
</script>
然后,在父组件中,对这些属性进行传值,如下:
<template>
<div id="app">
<HelloWorld ref="ceshi"
<!-- 直接赋值,则直接写属性,然后赋值即可 -->
msg="Welcome to Your Vue.js App"
<!-- a是父组件定义的属性,需要在props属性前加冒号 -->
:ceshi="a"
<!-- method是父组件定义的函数,加冒号引用此函数 -->
:hanshu="method"
/>
<div ref="ceshi1">666</div>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
data:function (){
return {
a:'绑定props属性'
}
},
methods:{
method(){
return '函数props绑定';
}
}
}
</script>
4.5minix混入技术
一些共用的东西,可以提取出来,形成js,然后供所有组件使用。具体用法如下:
定义共用js:
minix.js:
export default {
data () {
return {
name: 'minix',
minixName: 'minixObj',
flag: false,
obj: {
class: 'classtest',
id: 'idtest'
}
}
},
mounted() {
console.log('minixMounted');
},
methods: {
speak() {
console.log('this is minix');
},
getData() {
return '100';
}
}
}
在组件中,使用这个js的方法:
import myMinix from './minix'; //引入公用js
export default {
data () {
return {
name: 'todo',
lists: [1, 2, 3, 4],
obj: {
todoclass: 'todoclasstest',
id: 'idtodotest'
}
}
},
mounted() {
console.log('todoMounted');
},
minixs: [myMinix], // todo.vue 中声明minix 进行混合。这样引入js中的东西这个组件都有了
methods: {
speak () {
console.log('this is todo');
},
submit() {
console.log('submit');
},
}
}
五、自定义事件
在组件标签上,可以自定义事件,来进行子组件向父组件数据的传递。注意:上面说的props属性是父组件向子组件传数据。这里讨论子组件向父组件传数据。<Demo @自定义事件="函数" />
这个自定义事件如何触发呢?需要在Demo组件中去定义。如点击Demo组件中的某个按钮触发selfevent事件,就在Demo组件中写@click事件,然后在回调函数中,调用this.$emit
(‘selfevent’,)函数,来写selfevent的触发时机。而selfevent的触发函数,就在绑定事件的组件中定义就行。当eventself触发后,自动执行其回调函数。
需要注意的是,在组件标签上绑定事件,即使内置事件,如@click,那么vue也会当成自定义事件去解析。如果想用内置事件,则需要用@click,navite='xxx’来绑定。
自定义事件的作用就是子组件给父组件传递信息通信用的。
因为子组件定义自定义事件的触发时机,在this.$emit中,就可以传递子组件的数据,父组件写子组件的标签时,绑定自定义事件回调函数,那么在回调函数中就可以接收到子组件传递的数据。
示例如下:
首先,子组件定义自定义事件:
<template>
<div class="hello">
<!-- 使用props中定义的属性 -->
<h1>{{ msg }}</h1>
<h2>{{ceshi}}</h2>
<h3>{{hanshu()}}</h3>
<div @click="demo">点我触发自定义事件</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String,
ceshi: String,
hanshu:Function //定义函数
},
methods:{
demo(){
//触发自定义事件,并向父组件传递信息
this.$emit('selfevent',this.msg,this.ceshi);
}
}
}
</script>
然后在父组件中,写自定义事件的回调函数,在回调函数里,就可以接收到子组件传来的数据。
<template>
<div id="app">
<HelloWorld ref="ceshi"
msg="Welcome to Your Vue.js App"
:ceshi="a"
:hanshu="method"
@selfevent="method1"
/>
<div ref="ceshi1">666</div>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
data:function (){
return {
a:'绑定props属性'
}
},
methods:{
method(){
return '函数props绑定';
},
method1(a,b,c){
console.log('触发自定义事件');
console.log('接收到参数a',a);
console.log('接收到参数b',b);
}
}
}
</script>
由此可以看到,自定义事件就是在子组件中定义自定义事件的触发时机,并给父组件传递数据,然后在父组件定义回调函数,然后接收子组件传递的数据。
六、全局事件总线
组件间通信方式,适用于任意组件间通信。
6.1 安装全局事件总线
在main.js中,new Vue的beforeCreate函数中,安装全局事件总线
new Vue({
el:'#app',
//将app组件放入#app容器中
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus=this;//安装全局事件总线,$bus就是当前应用的vm。vm中定义的属性,在各个组件中,都可以获得。上面的_proto_隐式链中获得。
}
})
6.2 使用事件总线
提供数据:
<template>
<div class="hello">
<!-- 使用props中定义的属性 -->
<h1>{{ msg }}</h1>
<h2>{{ceshi}}</h2>
<h3>{{hanshu()}}</h3>
<div @click="demo">点我触发自定义事件</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String,
ceshi: String,
hanshu:Function //定义函数
},
methods:{
demo(){
//触发自定义事件,并向父组件传递信息
// this.$emit('selfevent',this.msg,this.ceshi);
//全局事件总线,通过this.$bus隐式链获得vm对象上的$bus属性。
this.$bus.$emit('selfevent',this.msg,this.ceshi)
}
}
}
</script>
接收数据方:
methods:{
method(){
return '函数props绑定';
},
method1(a,b,c){
console.log('触发自定义事件');
console.log('接收到参数a',a);
console.log('接收到参数b',b);
},
demo(a,b){
console.log('接收到全局事件总线参数a',a);
console.log('接收到全局事件总线参数b',b);
}
},
mounted() {
console.log('vc',this.$bus);//$bus就是vm实例
//vm上绑定自定义事件,接收数据
this.$bus.$on('selfevent',this.demo);
}
在绑定事件的组件上,beforeDestory方法中,解绑事件:
beforeDestroy() {
//对事件进行解绑
this.$bus.$off('selfevent');
}
由此可见,全局事件总线,在子组件中,还需要通过$emit
方法绑定自定义事件。哪里需要接收数据,在哪里定义$bus.$on
绑定自定义事件即可。
七、消息订阅与发布
实际用的不多,还是用Vue原生的消息事件总线多。这里主要有一个意识,可以依赖一些第三方类库,实现某些功能。
八、$nextTick函数
我们修改vue中的数据时,修改的是vue虚拟内存中的数据,而虚拟内存的数据更新到真实dom上,是有延迟的。如果想在真实dom更新后进行一些操作,那么就要用这个函数来执行回调函数了。看如下示例:
data中定义属性:
data:function (){
return {
change:'改变元素'
}
}
html元素中使用这个属性,
<div ref="change">{{change}}</div>
<div @click="demo1">点我改变元素内容</div>
单击事件函数,改变change属性的值:
demo1(){
console.log('change',this.$refs.change.innerHTML);
this.change='change-change';
this.$nextTick(function (){
console.log('$nextTick',this.$refs.change.innerHTML);
})
console.log('no-$nextTick',this.$refs.change.innerHTML);
}
输出内容如下:
可见,先输出了没有调用$nextTick
函数的旧值,后输出了$nextTick
回调函数中改变后的值。
九、Vue发送ajax请求
用axios封装的方法进行发送。之前用的jquery的$.get等方法底层操作的是XMLHttpRequest对象,进行请求的发送。
axios底层也是这个对象,相当于重新对其进行了封装。不同框架进行了不同的封装而已。
axios的使用,可参考https://www.bbsmax.com/A/xl56nop95r/。
跨域的理解:
跨域是浏览器的同源策略做的保护机制。就是发送请求的浏览器的ip,端口号与发送的请求中的ip,端口号不一致时做的限制。需要注意的是,跨域请求是能发送出去的,服务器也能收到请求,并返回数据。但是浏览器收到数据后,会根据同源策略,报错。所以跨域是收到服务器的数据后报的跨域问题。
当后台给请求头加上一些信息后,浏览器就不会报错了,就会返回收到的数据。
vue-cli脚手架提供了一个与前端服务同域名,同端口号的后台服务,可以通过这个服务,在后台转发跨域请求,这样,也就避免了前端跨域的问题。
vue-cli之proxy的配置如下:
在vue.config.js配置文件中,配置:
module.exports = {
devServer:{
// 本地域名
host:'localhost',
// 本地端口
port:'8080',
open:true,
proxy:{ //配置跨域
// 当访问到 api 开头的接口时走下面的内容
'/api':{
// 最终想要访问的地址
target:'http://localhost:8088/resource',
changeOrigin:true,//允许跨域
pathRewrite:{//在使用axios发送请求时,用api代表target的路径。但是真实的里面没有api,则在这里配置成'',就自动去掉api了
'^/api':''
}
}
}
}
}
在发送请求的组件,引入axios,
import axios from "axios";
在发送请求的函数里定义axios请求:
methods:{
method(){
axios.get('api/login').then(
response=>{
console.log('返回数据',response);
},
error=>{
console.log('错误消息',error)
}
)
}
}
输出的response如下:
可见,data是服务器返回的数据,还有headers请求头信息可以拿到。
注意:在开发中,通过配置proxyTable来实现跨域,在生产环境,使用nginx解决跨域问题。前端静态资源都放到nginx下面。
十、插槽
在上面讲到的props属性中,父组件可以向子组件传递数据,但是不能传递子组件的dom结构。想要在父组件中定义子组件的dom结构,就要用到插槽了。
默认插槽:
子组件中,定义<slot>
,进行占位,这个标签的意思就是让父组件定义这块的dom元素。
父组件中,在子组件标签内部,定义dom元素,就替换到子组件slot
处了。
具名插槽:
子组件中定义多个slot
标签,则需要用name做标识。父组件的子标签中,就要用slot
属性指定代替的区域。
作用域插槽
数据在子组件中,但是展示数据的dom,要在父组件中定义,则使用作用域插槽。
在子组件slot
标签,绑定数据:
父组件中,用scope属性定义作用域: