一、前言

使用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 项目名

vue一般项目的架构模式 vue大型项目架构_webpack


这里,选择Vue2版本。创建完后,一个脚手架工程就好了。这个项目现在就可以启动,输入npm run serve 命令,启动项目。

vue一般项目的架构模式 vue大型项目架构_App_02


此时,这个工程就相当于是一个helloworld。我们自己的工程,在此基础上改造就可以。2.2脚手架目录结构解析

vue一般项目的架构模式 vue大型项目架构_webpack_03


babel.config.js: ES6转ES5的配置文件

package.json: 类似于包的说明书

package-lock.json: 锁定依赖包版本的文件。

node_modules文件夹:脚手架为我们自动创建的依赖的第三方库,都放到了这个文件夹下。

src文件夹:

vue一般项目的架构模式 vue大型项目架构_webpack_04


assets文件夹:静态资源文件夹,放图片,视频等资源

components:所有程序员定义的组件,都放到这里

App.vue文件:所有组件的总管。

main.js文件:程序的入口,vue对象在这里创建。

public文件夹:放html文件。vue就是在渲染这里的html文件。

vue一般项目的架构模式 vue大型项目架构_webpack_05


2.3 main.js详解

首先,main.js是程序的入口,那这个入口文件,是在哪里配置定义的呢?

这是脚手架默认配置的,并且把配置文件隐藏起来了。输入命令vue inspect >output.js,就可以在项目中生成其默认配置文件。

vue一般项目的架构模式 vue大型项目架构_vue一般项目的架构模式_06


需要注意的是,这里生成配置文件只是让你看看,直接改这里的配置,不能生效。

想覆盖脚手架的默认配置,在vue.config.js文件中进行修改。修改项配置可在脚手架官网进行查看。

vue一般项目的架构模式 vue大型项目架构_vue.js_07


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函数,看其官网介绍:

vue一般项目的架构模式 vue大型项目架构_vue.js_08


使用了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的语法。这里简单说一下几种方式。

  1. export default方式:
    这种方式,只能导出一个对象。default只能使用一次。所以,使用export default时需要把导出的内容都放在一个对象中导出。
  2. export 单个对象
    就是每个需要导出的函数,对象,变量前面都加上export关键字,多写几个关键字的事儿。
  3. export {a,b,c}
    也是形成了一个对象,直接导出了这个对象,不用写default关键字了。这种方式导出的内容,在import时,需要import {a,b,c} from xxx来导入,也可以import * from xxx来全部导入。

ES6简写规则:

vue一般项目的架构模式 vue大型项目架构_App_09

需要注意的是,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对象,如下:

vue一般项目的架构模式 vue大型项目架构_前端_10


首先可以看到,插件对象,本质是一个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属性,是其组件的集合,如下图:

vue一般项目的架构模式 vue大型项目架构_vue一般项目的架构模式_11


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);
  }
}

输出结果如下:

vue一般项目的架构模式 vue大型项目架构_前端_12


可见$refs属性将我们定义的ref属性作为了key,vaule是相应的dom对象或组件对象。

vue一般项目的架构模式 vue大型项目架构_vue.js_13


组件标签上的ref获得的是VueComponent对象。

vue一般项目的架构模式 vue大型项目架构_vue一般项目的架构模式_14


普通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属性进行运算。如下图:

vue一般项目的架构模式 vue大型项目架构_vue.js_15


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一般项目的架构模式 vue大型项目架构_vue.js_16


实际用的不多,还是用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);
    }

输出内容如下:

vue一般项目的架构模式 vue大型项目架构_App_17


可见,先输出了没有调用$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如下:

vue一般项目的架构模式 vue大型项目架构_前端_18


可见,data是服务器返回的数据,还有headers请求头信息可以拿到。

注意:在开发中,通过配置proxyTable来实现跨域,在生产环境,使用nginx解决跨域问题。前端静态资源都放到nginx下面。

十、插槽

在上面讲到的props属性中,父组件可以向子组件传递数据,但是不能传递子组件的dom结构。想要在父组件中定义子组件的dom结构,就要用到插槽了。

默认插槽:

子组件中,定义<slot>,进行占位,这个标签的意思就是让父组件定义这块的dom元素。

父组件中,在子组件标签内部,定义dom元素,就替换到子组件slot处了。

vue一般项目的架构模式 vue大型项目架构_webpack_19


具名插槽:

子组件中定义多个slot标签,则需要用name做标识。父组件的子标签中,就要用slot属性指定代替的区域。

vue一般项目的架构模式 vue大型项目架构_App_20


作用域插槽

数据在子组件中,但是展示数据的dom,要在父组件中定义,则使用作用域插槽。

在子组件slot标签,绑定数据:

vue一般项目的架构模式 vue大型项目架构_webpack_21


父组件中,用scope属性定义作用域:

vue一般项目的架构模式 vue大型项目架构_前端_22