0


Vue2.x - 组件化编程

初识组件化

什么是组件、组件化

组件定义:实现应用(网页)中局部功能的代码(html,css,js)和资源(图片、视频、音频、文件、压缩包)的集合。

如上网页被划分为了四个组件:header、content、footer、nav。

每个组件中都包含了实现网页局部效果的html、css、js代码以及图片资源。

而前端编程中,将网页需求分解为组件颗粒来开发的模式就叫组件化。

为什么需要组件化

传统前端编程提倡的是:样式css和行为js的高复用。

所以我们开发网页时,一般都是尽可能地引用外部css文件和js文件,或者将自定义的css代码或js代码解耦出去独立为一个文件,然后通过link和script标签再引入回html中。

这种开发模式很好地解决了css、js的复用问题,但是忽略了局部html结构的复用性!

比如,我们平时开发网页时,往往不会在一个html文件中只开发某个网页的局部结构,而是开发该网页的整体结构。

当然,我们也可以在一个html文件中开发网页局部结构,然后在整体html文件中通过iframe标签引入局部结构的html文件,这样也能实现局部html结构的高复用。但是这种通过iframe标签引入局部结构html文件的方式会带来诸多问题,比如iframe引入的网页其实是作为子网页,而不是真实的局部结构,这样便造成了父子网页间样式和行为的共享不通。

那么这种忽视网页局部结构的复用性,独立性的开发模式会导致什么问题呢?

我们不妨想一想在传统前端开发中,遇到的如下问题:

  • 网页中某html标签使用了外部css样式,但是开发中,我们很难找到该外部css样式所在的文件
  • 网页中某内联script标签使用了外部js行为,但是开发中,我们很难找到该外部js行为所在的文件

导致这些问题的原因就是,当一个网页引入外部css、js文件时,对应css、js的服务对象是整体网页,换句话说,对应css、js不单独服务于网页某个局部结构,即使它真的是专门服务于该局部结构的。

这无疑会造成 "局部html结构" 和 "专门服务于它的外部css、js" 的关系上的割裂,影响开发效率和后期代码维护的成本。

而组件,作为实现网页局部功能的代码(html,css,js)的结合,则可以在开发过程就建立起局部html结构和 "专门服务于它的外部css、js"的联系。

另外,如今的前端开发节奏越来越快,企业已经不满足于开发人员手写html,从0搭建网页的砌砖开发模式了,而更多的是要求开发人员会使用组件库,变成拼图达人。

简单了解Vue组件和vm的关系

Vue组件是实现网页结构的代码集合,也就是说Vue组件是控制网页某个局部视图的搭建的。

而vm是控制网页整体视图的搭建的。

所以Vue组件实例和vm实例在

  • 功能上有相似性,都是负责视图搭建(管理View和Model),可以将Vue组件实例看出一个微型的vm实例
  • 关系上有从属性,即vm应该负责排版Vue组件,Vue组件关注具体HTML结构。

Vue组件分类

Vue的组件可以分为两类:非单文件组件、单文件组件。

非单文件组件:即一个文件可能存在一个或多个组件;

单文件组件:即一个文件只存在一个组件;

其中非单文件组件依旧书写在*.html文件中,而单文件组件需要书写在*.vue文件中。

单文件组件的复用性更强,而非单文件组件其实并不适合于企业开发,但是非单文件组件可以更好地帮助我们理解Vue组件。

非单文件组件

非单文件组件组件基本使用流程为:定义组件、注册组件、使用(创建)组件

定义组件

Vue.extend 方法

  • 接收一个options配置对象
  • 返回一个特定的Vue组件构造函数

特别注意:该步骤只是产生了一个Vue组件构造函数,而不是产生了Vue组件实例。

由于组件实例其实就是一个微型的vm实例,所以Vue.extend入参配置对象 和 Vue构造函数入参配置对象具有极大的相似性,但是也具有较为明显的区别。

前面学习Vue构造函数时,我们知道其入参配置对象,具有如下属性:

  • el
  • template
  • data
  • computed
  • methods
  • watch
  • directives
  • 生命周期钩子(beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed)

而Vue.extend入参配置对象只有el属性和data属性的用法和Vue构造函数入参配置对象对应属性不同。Vue.extend入参配置对象:

  • 不能使用el,只能使用template
  • data不能使用对象式写法,只能使用函数式写法

我们来解释下为啥Vue组件会有以上两点不同:

不能使用el,只能使用template

vm实例与视图模板el是一一对应的,即一个vm实例只能对应一个视图模板el,一个视图模板el只能对应一个vm实例。

而Vue.extend在底层其实是复用了Vue构造函数创建vm实例的逻辑,即Vue.extend入参配置对应的el属性也是指定的vm实例控制的视图模板,所以为了避免vm实例被指定多个el,所以Vue.extend创建的组件控制的视图需要使用另一个属性template。

如上报错意思是:el只能用于new Vue时

关于template属性的说明

Vue构造函数入参配置对象的template属性用于指定被vm实例控制的视图模板的html结构,即template属性值为一个html结构字符串。

而Vue.extend入参配置对象的template属性的值也是一个html结构字符串,也是用于指定Vue.extend定义的组件控制的视图模板。

需要注意的是,template属性值指定的html结构只能有一个根节点。

data不能使用对象式写法,只能使用函数式写法

我们知道组件的提出,就是为了实现组件高复用,而组件实例的Model数据源就是Vue.extend入参配置对象的data属性值。如果data属性值为一个对象

Vue.extend({
    template: xxx,
    data: {
      xxx: xxx
    }
})

则当组件被复用时,多个复用组件对应的数据源其实是同一个对象,即入参options.data指定的对象。这样就破坏了组件的独立性。

而data的函数式写法:

Vue.extend({
    template: xxx,
    data(){
        return {
            xxx: xxx
        }
    }
})

相当于每次创建组件都会调用data函数返回一个新创建的对象,这样即使同一个组件被复用多次,每个组件对应的数据源都是一个新对象,而不是同一个对象。

注册组件

局部注册

前面我们了解了组件和vm的关系,组件需要被vm管理,所以我们必须建立组件和vm之间的关系。

在创建vm实例时,Vue构造函数入参配置对象还有一个内置属性:components。

components属性值是一个对象,用于注册组件到vm实例上:

  • 属性名是自定义组件名
  • 属性值是Vue.extend返回的组件构造函数

如上图中,haha就是自定义组件名。

另外上述方式是组件的局部注册,即被注册的组件只能用于该vm实例的容器(视图模板)中。

全局注册

还有一个种全局注册组件的方式,被注册的组件不仅可以用于vm实例,还可以用于vm实例直接或间接管理的任意组件。

全局注册使用如下方法:

Vue.components(组件名,组件构造函数)

定义组件的简写形式

注册组件时,options.components.xxx的属性值可以是:

  • Vue.extend返回的Vue组件构造函数
  • Vue.extend入参的配置对象

当options.components.xxx值为Vue.extend入参配置对象时,Vue底层会自动将它传入Vue.extend,即自动根据配置对象生成组件构造函数。

这样,我们定义组件时,就不需要调用Vue.extend了,而是只需要定义配置对象。

使用(创建)组件

组件标签

注册组件时通过options.components指定的组件名,可以在对应的管理者容器中直接当成标签使用,我们一般将此类标签称为“组件标签”。

使用组件标签时,支持自闭和形式

创建组件实例的时机

前面说过,定义组件时,Vue.extend方法返回的只是一个组件构造函数,而不是组件实例。

注册组件时,依旧是将组件构造函数注册给管理者(vm或其他组件),并没有创建组件实例。

使用组件时,我们将组件以组件标签的形式写入管理者视图模板中,而当Vue在解析和编译试图模板时,就会创建组件实例。

组件名的定义

组件名定义的两种情况

  1. 在注册组件时,通过options.components来定义组件的名字
  2. 在定义组件时,通过options.name来定义组件的名字

注册组件时定义的组件名字,直接用作使用组件时的组件标签名字

定义组件时定义的组件名,不能作为使用组件时的组件标签的名字,而是作为Vue DevTools显示组件结构时的优先选择的组件名。

为什么组件会有两种命名方式,是否有必要存在两种命名方式?

我们在开发过程中,会遇到两种组件,一种是开发自定义组件,一种是第三方库组件。而项目中开发往往更多地使用第三方库组件。

但是开发人员是不参与第三方库组件的内部逻辑编写的,而只是单纯的使用。所以可能会遇到一个问题:第三方库组件和自定义组件重名。

此时一个快捷的解决办法就是自定义组件重命名,但是这种方法对于项目来说不够友好。我们不期望第三方库组件影响到业务组件,业务组件也不应该损害自身来为第三方库组件让路。

所以需要在开发过程中,即使用组件标签时,支持对第三方库组件进行重命名,来保证业务组件命名不受影响。

此时options.components命名组件被提出。

但是,在调试过程中,我们应该期望不参照源码,就可以分辨出业务组件和第三方库组件,如果继续使用options.components命名作为调试中显示的组件名,则第三方库组件名字会被变更,导致无法在调试时被直观的分辨出来,所以为了保证调试流畅,我们期望第三方库组件有一个从定义时,到调试时,都不变的名字,此时options.name被提出。

总结以下:组件的options.components命名完全用于组件标签,options.name命名完全用于Vue DevTools调试。

组件名的命名规则以及Vue DevTools显示规则

当组件名只有一个单词时:

  • 注册组件名:只能是全小写,或者首字母大写,不能是其他形式
  • 定义组件名:无限制

如上图所示注册组件名不能是全大写。

如上图所示,定义组件名没有形式要求。

Vue DevTools优先会显示定义组件名,若无定义组件名,则再显示注册组件名。

显示规则为:首字母必转为大写,其余不变。

由于注册组件名只能是首字母大写或者全小写,所以如果Vue DevTools显示注册组件名时,必然是首字母大写形式。

而定义组件名形式没有要求,所以Vue DevTools显示定义组件名时,只会强转其首字母为大写,其余不变。

当组件名由多个单词组成时,则

  • 注册组件名:只能是cabe-case写法,不能是小驼峰或大驼峰
  • 定义组件名:无限制

当Vue DevTools显示注册组件名时,会将cabe-case写法的组件名转为大驼峰。

对于定义组件名,没有命名限制,但是在Vue DevTools上显示时会将其首字母转为大写。

组件标签对于组件命名的限制

由于组件会以组件标签的形式使用,所以组件的名字不能是HTML内置标签的名字,如h1、div等,否则标签不会被识别为组件标签,而是当成HTML内置标签被解析。

组件嵌套

实现组件嵌套

Vue构造函数入参配置对象options有一个属性components,支持注册组件。

定义组件的Vue.extend入参配置对象options同样有components属性,也支持注册组件。

当我们将一个组件注册给另一个组件时,就可以在组件的template中使用另一个组件标签,形式组件嵌套。

app管理员组件

当前我们都是让vm实例来直接管理组件的排版,但是vm实例的有很多其他重要任务,比如Vue.config设置、Vue.components设置、Vue.driective设置以及后面要提到的Vue插件注册,这些都是全局性工作。所以如果让vm实例继续管理组件的排版的话,就会造成vm实例代码复杂化,不利于后期维护。

所以企业级开发中,会创建一个APP组件来代替vm实例管理其他组件,然后vm实例只需要注册APP组件即可。

深入理解组件

VueComponent

VueComponent构造函数的来源及特点

Vue.extend返回值是一个VueComponent构造函数,且每次Vue.extend调用返回的VueComponent都是不同的。

为什么不把VueComponent设置为全局的构造函数?

其实这个问题可以理解为:为啥不开放创建组件实例的权力,而是将创建组件实例的权力交给Vue底层。

这是因为 组件实例 不同于 vm实例,组件实例并不仅仅只是一个ViewModel的角色,它还可以当成组件标签来使用,后面我们会学习

  • 组件标签通过标签属性来传数据给子组件
  • 组件标签通过标签内容来传插槽结构给子组件

这些标签相关的东西都是需要在Vue编译模板时才能被解析出来交给组件实例管理的。

所以创建组件实例的时机应该在Vue编译模板时,而该时机是一个比较底层的时机,开发人员是不易捕捉到的。

为什么Vue.extend每次调用产生的VueComponent都是不一样的?

我们可以查看下Vue.extend源码逻辑

    Vue.extend = function (extendOptions) {
      // ...

      var Sub = function VueComponent (options) {
        this._init(options);
      };

      // ...

      return Sub
    }

可以发现,Vue.extend返回一个Sub变量,Sub指向一个VueComponent构造函数,由于该构造函数是借助function关键字实时定义,所以每次Vue.extend调用都会产生一个新的VueComponent。

VueComponent与Vue的关系

VueComponent本质上是Vue的子类,从源码中来看

    Vue.extend = function (extendOptions) {
      // ...
      var Super = this; // this指向Vue构造函数
      // ...

      var Sub = function VueComponent (options) {
        this._init(options);
      };
      Sub.prototype = Object.create(Super.prototype); // VueComponent与Vue的继承关系
      Sub.prototype.constructor = Sub;

      // ...

      return Sub
    }

Sub指向VueComponent,Super指向Vue。即VueComponent与Vue存在如下关系:

VueComponent.prototype = Object.create(Vue.prototype);

对应的原型链示意图如下:

所以VueComponent本质上是Vue的子类,或者说是拓展类。

VueComponent实例也可以沿着原型链调用Vue.prototype上的属性和方法。

new VueComponent的时机

由于VueComponent并没有被设置为全局的,所以程序员无法直接new Component创建组件。

而在Vue规定的组件基本使用过程中:

  1. 定义组件时,Vue.extend创建了一个新的VueComponent构造函数;
  2. 注册组件时,只是将VueComponent与管理员组件或vm绑定,此时并没有进行new VueComponent动作。
  3. 使用组件时,将组件以组件标签的形式写在管理员组件或vm的视图模板中,此时也没有进行new VueComponent动作。

只有当组件标签所在的视图模板被Vue解析编译时,Vue底层才会对组件标签对应的VueComponent进行new操作。

VueComponent实例相关配置对象的方法中this指向说明

前面学习Vue构造函数时,Vue入参配置对象中:

  • methods
  • computed.get || set
  • watch.handler

这些方法的this都指向vm实例。

而Vue.extend入参配置对象,也同样具备上述属性对象,这些对象的方法的this指向Vue.extend所返回的VueComponent构造函数对应的VueComponent实例(简称为vc实例)。

单文件组件

为什么需要单文件组件

非单文件组件就是在一个文件中写多个组件,这样的坏处是破坏了组件的复用性。

所以为了组件能够被更好地复用,一个文件中只包含一个组件,成为一种设计需求。

为此,Vue设计出一种新的文件类型:*.vue。

这种.vue后缀就是Vue专属的单组件文件的类型。

单文件组件的定义

我们知道组件是 实现网页某局部功能地代码的集合,这里的代码包含html结构,css样式,js行为。

所以单文件组件的结构也分为三部分:

  • template
  • script
  • style

这三个部分都以标签的形式书写在*.vue文件中。

  • template标签:内容为组件的html结构
  • script标签:内容为定义组件的js代码
  • style标签:内容为组件的css样式

如下图,将“非单文件组件” 转换为 “单文件组件” 主要动作如下:

  • Vue.extend入参配置对象的template属性值(html结构代码) => 单文件组件的的template标签
  • 非单文件组件html文件的style标签 => 单文件组件的style标签
  • Vue.extend组件定义 => 单文件组件的的script标签

需要注意的是,单文件组件定义的组件需要被暴露出去,因为*.vue文件会被import复用,而单文件组件的暴露,可以使用ES6的三种暴露语法:分别暴露export,统一暴露export {},默认暴露export default,由于script标签中只会定义一个组件,所以使用默认暴露更加方便。

关于ES6暴露语法可以参考:随笔-深入理解ES6模块化(一)_伏城之外的博客-CSDN博客_es6 模块化 阮一峰https://blog.csdn.net/qfc_128220/article/details/121922494?spm=1001.2014.3001.5501

单文件组件的注册和使用

*.vue无法被浏览器直接解析

按照上述流程,理论上可以完成单文件组件的注册和使用,但是有一个问题,*.vue文件并不能被浏览器直接识别,所以上述流程在第2步引入组件构造时就会出问题。

Vue CLI 脚手架

使用脚手架创建Vue项目

由于浏览器无法直接解析*.vue文件,所以需要借助打包工具来解析*.vue为浏览器可以直接识别的html文件和js文件。Vue2.x选择了webpack打包工具,并且将打包逻辑全部内置好了,推出了一个npm包:@vue/cli,简称为Vue脚手架

由于@vue/cli是一个命令行工具,所以建议全局安装。

当我们安装好Vue脚手架后,需要使用其内置命令来创建一个Vue项目:

vue create 项目名

上图,创建了一个名为vue_component的项目,且设置其运行版本为Vue2.x。

当项目创建完成后,就会在命令执行目录上创建一个vue_component文件夹,该文件夹中包含了将*.vue打包为浏览器可识别的html,js文件的webpack代码。比如vue_component/node_modules中就有webpack相关的包。

当我们创建好项目后,Vue会为我们准备一个默认的Vue项目代码,我们需要先进入Vue项目目录,然后使用下面命令运行项目:

npm run serve

当项目启动成功后,Vue会将其部署在本地8080端口上,我们在浏览器输入上述网址即可访问

认识Vue项目架构

首先脚手架创建的Vue项目本质是一个node项目,比如:

  • node_modules:当node项目依赖的第三方软件包,比如webpack
  • package.json:设置node项目,比如项目版本号,项目入口文件,npm简写命令,开发依赖和项目依赖
  • package-lock.json:记录node项目依赖的第三方软件包的版本号和下载地址

其次Vue项目的版本管理工具使用的是git,比如:.gitignore文件,就是用于设置不用提交到git仓库的文件,如:node_modules目录

babel.config.js是为了转化项目中使用到的ES6语法为浏览器普遍适配的ES5语法。

vue.config.js就是Vue内置的webpack打包逻辑的配置文件,我们可以参考官方的指南配置参考 | Vue CLI (vuejs.org)

进行个性化打包逻辑定制

jsconfig.json主要用来配置一个默认根路径,以后可以通过该根路径快速访问到子路径,比如我们在*.vue中经常需要import外部文件,比如js,css,vue等,如果这些文件所在目录与当前文件目录相差层级很远,比如我要在a.js中引入test.js

|- src
|-- test.js
|-- f1
    |- f2
        |- f3
            |- a.js

就会出现 import test from '../../../test.js' 这种逆向跳转多级目录的情况,体验感非常差,

此时我们可以在jsconfig.json配置src根目录为@,这样a.js就可以正向引入test.js

import test from '@/test.js'

底层webpack会根据jsconfig.json帮助我们解析@为src所在目录。

Vue项目的src目录是我们进行组件化开发的源码目录,其中包含了:

  • main.js
  • App.vue
  • components文件夹
  • assets文件夹

main.js的作用是创建vm实例,该vm实例的配置是固定的,即挂载视图容器为”#app“所在的DOM,

且只负责App管理员组件的渲染。

App.vue是管理员组件,其他组件都被App组件直接或间接管理。

assets文件夹放一些静态资源,如css,js,图片,字体,音频视频等。

Vue项目的public目录下包含:

  • favicon.ico
  • index.html

其中favicon.ico是浏览器网页页签小图标,index.html是Vue项目部署后的网站首页。

上图中,index.html中**

**就是被main.js中产生的vm实例控制的视图容器。

另外npm run serve只是在本地部署Vue项目,当我们项目开发完成,需要导出最终打包文件,此时需要借助命令:

npm run build

此时项目根目录下就会多出一个dist目录,dist目录中包含的就是最终可以直接在浏览器运行的项目代码。

部署单文件组件到Vue项目

一般而言,我们只需要操作src/components目录下的组件和App.vue组件。

需要注意的是,Vue项目默认使用了eslint对js代码进行严格的语法检查,如果存在不合理的代码,如变量定义未使用,则会直接报错。

另外eslint对于组件文件命名也有要求

eslint强制要求组件文件名为多个单词,且建议使用cabe-case写法。import导入接口名不支持cabe-case写法,最好写成大驼峰。

或者干脆,组件文件名、组件注册名、组件定义名都采用大驼峰写法。

前面学习过,对于多单词组成的组件名,非单文件组件的注册名不支持大驼峰,只支持cabe-case,但是这里基于脚手架的单文件组件的注册名是支持大驼峰的。

了解Vue项目启动过程

1、在项目根目录下,cmd执行命令: npm run serve

npm run是node项目定制命令,当执行npm run xxx后,node会去命令执行目录下找到package.json中的scripts对象,找到其中的xxx属性,比如npm run serve就是找scripts.serve属性,并最终将npm run serve 转化为 vue-cli-service serve命令执行

而 vue-cli-service serve 就是@vue/cli脚手架的项目启动命令。

2、当Vue项目启动过程中,首先会将执行src/main.js

首先会引入Vue构造函数,然后引入App.vue组件构造函数

3、引入App引发了App.vue的解析

App.vue的解析从执行script标签中的代码开始,发现又要引入src/components/my-hello.vue,所以又要解析my-hello.vue

4、解析my-hello.vue

解析过程中,会将template和style标签中结构与样式 转化为 script标签中导出配置对象的属性template,然后直接将新配置对象对外暴露。

5、App.vue得到my-hello.vue暴露的配置对象后,

将其注册给App组件,然后为my-hello组件自动生成组件构造函数。最终对外导致App组件的配置对。

6、 main.js引入了App.vue的组件配置对象后,继续执行后续代码:

Vue.config.productionTip = false

即关闭Vue开发提示。

然后执行new Vue创建vm实例,在创建vm实例后,设置其控制的视图模板为#app对应的DOM。

这里的#app对应的DOM在public/index.html中

但是这里vm实例并没有注册App组件,并在index.html中使用App组件标签?而是使用了render配置项来完成App组件的注册与使用。

options.render配置项

如果不使用options.render配置,而是采用以前的思路:注册App组件,使用App组件标签

为了避免操作index.html,我们可以借助options.template属性,即在options.template书写视图模板

结果发现:项目虽然可以正常启动,但是用浏览器访问时报错。

浏览器报错提示:你引入的Vue没有模板编译器,所以项目无法正常运行。

我们检查下 import Vue from 'vue' 到底引入的是啥?

node不仅支持CommonJS模块化语法,也支持ES6模块化语法,所以这里引入的'vue'在路径分析时,被认定为第三方包,

  1. 首先在项目根目录下的node_modules中查找是否有vue.js
  2. 若没有则继续查找是否有vue.json
  3. 若没有则继续查找是否有vue.node
  4. 若没有则继续查找是否有vue文件夹

发现有vue文件夹,则继续查找vue文件夹中的package.json中的项目入口文件:

  • 对于CommonJS模块化而言,项目入口文件就是main配置
  • 对于ES6模块化而言,项目入口就是module配置

所以最终import Vue from 'vue'导入的Vue就是vue.runtime.esm.js对外暴露的Vue构造函数。

根据Vue官方介绍,vue.runtime.esm.js是一个Runtime-only的,而Runtime-only的是不带模板编译器的。

而不带模板编译器的Vue是无法编译如下情况指定的视图模板的:

  • 通过options.template指定的视图模板
  • 在options.el指定的容器中书写的视图模板

只有Runtime + Compiler的Vue才能够解析上述两种情况指定的视图模板。

因此我们再换一个Runtime + Compiler的Vue试试,即选一个含有模板编译器的vue试试,比如vue.esm.js

此时项目启动成功,运行正常。

那么为什么Vue默认提供的是,不带模板编译器的Vue呢?

根据官方文档解释:

  • 模板编译器占了完整版vue.js的1/3体积,如果去掉模板编译器,vue.js体重将大大减轻。
  • 最根本的原因是:vue模板编译器是开发依赖的,而不是项目依赖的。比如开发过程中,我们需要Vue实时的编译模板进行不断地调试修改,但是开发完成后,我们使用Vue定制的webpack打包工具将项目打包,而打包好的文件都是原生的HTML,JS,而不会再存在Vue的模板语法,所以此时就不再需要vue模板编译器了。

所以为了使Runtime-only的Vue在没有Compiler的情况下,也可以解析Vue视图模板,此时就需要借助options.render了。

options.render的值为一个渲染函数,用于渲染动态的视图模板。相比较而言,options.template是设定一个静态的视图模板。

所谓静态的视图模板,是指options.template指定的字符串HTML只能写死,而无法动态地创建,比如下面options.template只能写死为某个HTML结构

new Vue({
    template: '<h1>Hello Vue!</h1>'
})

而options.render值为一个函数,options.render函数会在Vue底层被调用,并传入createElement参数,我们可以在options.render函数体中使用createElement动态创建标签

new Vue({
    render: function(createElement) {
        return createElement('h1', {}, 'Hello VUe!')
    }
})

options.render函数的入参createElement也是一个函数,可以接收三个参数,依次是:

  1. HTML标签名或Vue组件标签名
  2. 标签属性配置对象
  3. 标签的子节点

createElement的返回值为一个虚拟DOM节点。

而options.render函数会将createElement的返回值继续返回给Vue底层,Vue底层负责将虚拟DOM节点解析为真实DOM节点,并渲染到浏览器中。

上面options.render可以进行简写:

/*
new Vue({
    render: function(createElement) {
        return createElement(App)
    }
})

new Vue({
    render: (createElement) => {
        return createElement(App)
    }
})

new Vue({
    render: createElement => createElement(App)
})
*/

new Vue({
    render: h=> h(App)
})

而runtime-only的Vue必须使用options.render才能完成模板的解析和编译。

另外*.vue文件中的template标签指定的视图模板,是通过vue-template-compiler进行解析的, vue-template-compiler是专门用于解析Vue视图模板的。

下面使用该模板解析器来解析Vue视图模板

可以发现解析出来的东西包含一个render,该render本质也是一个函数,函数的返回一个_c函数返回值,而_c函数本质上就是createElement。

所以*.vue文件中template标签使用vue-template-compiler解析,其实就是生成一个render函数

组件间通信

ref属性:父组件中获取子组件实例

ref属性是Vue中内置的一个标签属性,这里的标签既可以是HTML标签,也可以是组件标签。

一旦某标签定义了ref属性,该标签对应的DOM实例或者组件实例就会挂载到其所在父组件实例的$refs属性对象的属性。

通过例子可以发现,如果ref属性

  • 打在HTML标签上,则父组件实例的$refs上挂载的是HTML标签对应的DOM对象
  • 打在组件标签上,则父组件实例的$refs上挂载的是该组件实例

父组件传递数据给子组件

组件标签属性与options.props配置

组件间父子关系的形成:

  • 子组件注册给父组件
  • 父组件的template中使用子组件标签

父组件template在使用子组件标签时,可以在子组件标签上设置自定义属性,这些自定义属性和属性值就会作为数据传给子组件的options.props配置项

上例中,需求是MyHello组件中显示的内容,由App组件指定。

实现:App组件template中使用MyHello组件标签时,设置了自定义属性msg,属性值为“Hello Vue!”,MyHello组件预先定义了options.props来接收一个名为msg的属性,并在视图模板中使用该msg属性。

options.props的基本使用

组件的options.props用于接收:使用该组件标签时设置在其身上的自定义属性的值。

类似于HTML标签变为DOM对象,而HTML标签的属性就变为了DOM对象的属性。

而组件标签其实就是对应组件实例,而组件标签的属性就对应组件实例vc.props上的属性

options.props的形式有两种:

  • 数组式
  • 对象式

options.props的值可以为一个数组,数组元素就是其组件标签上自定义标签属性名。

数组式的options.props只能单纯的获取 “组件标签自定义属性” 的值,而无法对值进行预校验。

所以options.props还支持对象式写法,

此时options.props对象的属性就是 “组件标签自定义属性”,options.props对象的属性的值可以是:

  • JS类型
  • 配置对象

当options.props对象的属性的值为JS类型时,作用是告诉Vue需要校验对应属性的值是否指定的JS类型,若不是则报错。

比如上例中,a和b标签属性接收的值为字符串类型,但是options.props的a和b却要求传入数字类型,所以类型不匹配,产生错误。

那么为什么标签属性a和b的值是字符串,而不是数字呢?

其实很好理解,HTML标签属性的值的类型默认就是字符串类型,而组件标签属性的值默认也是字符串类型。

而Vue核心中的v-bind指令可以绑定标签属性,此时标签属性值就是一个表达式,而不是字符串,此时标签属性值的类型由表达式结果决定。

比如上例中组件标签的a,b属性被v-bind绑定后,其属性值就是一个表达式,即1,2会被当成字面量表达式(数字),而不是字符串。

如果我们不仅想要校验组件标签属性值的类型,还有其他的,比如可选必选,指定默认值,则此时options.props.xxx也可以是一个配置对象,该配置对象具有如下校验属性:

  • type:校验组件标签自定义属性值的类型,值为一个JS类型
  • required:校验组件标签自定义属性的可选必选,值为true/false
  • default:组件标签自定义属性未定义时的默认值,和required:true互斥

另外需要注意的是,如果options.props.xxx.default的值为一个对象或数组,则必须由一个函数返回,而不能直接设置。原因和options.data使用函数式写法一致,都是为了避免组件复用时,组件间数据的互相污染。

如果组件标签属性的值除了需要校验类型,可选必选,以及指定默认值外,还有其他校验规则,比如大小范围等,此时则需要使用自定义校验函数validator,validator的返回值若为true表示校验通过,若为false标签校验不通过。

关于validator的执行时机说明,validator是在组件实例尚未创建时就执行的,所以此时validator函数无法访问当组件实例,及组件实例上的属性。

深入理解options.props

options.props可以是数组或者对象,而最终options.props数组(或对象)的元素(或属性)会挂载为所在组件实例的属性。这也是为什么我们可以在视图模板中使用options.props数组(或对象)的元素(或属性)的原因。

而挂载到组件实例上的options.props的属性是只读的,即不能重写的。

如果强行重写,则会报错。

这样设计的原因是,防止原始数据被篡改。

解决方案是:我们可以使用计算属性来拷贝options.props的属性值,然后操作计算属性来实现相同的效果,这样就能保护原始的options.props数据不会被篡改。

另外需要注意的是options.props的重写检测是浅度的,不是深度的。

所以当options.props的属性是一个对象时,该对象的属性的修改,不会引起options.props的只读报错。

还有一个比较重要的问题,options.props的属性会挂载为组件实例的属性,而options.data的属性也会挂载为组件实例的属性,那么二者如果同名的话,以哪个为准呢?

可以发现报错提示,msg是先被声明为了options.props属性,所以options.data就无法再声明msg属性了。

在Vue源码中,对于会被挂载到vm或组件实例上的options.xxx的属性的优先级进行了排序:

props > methods > data > computed > watch。

子组件传数据给父组件

函数参数实现反向传参

父组件可以很自然地,在使用子组件标签时,通过给子组件标签设置自定义属性实现传参,子组件只需要通过options.props接收参数即可。

但是这个过程无法反过来,只能是父传子的单向数据传递。

那么如何实现子组件传数据给父组件呢?

其实最简单的方式就是利用函数参数实现反向传参。

上述流程顺序是:

  1. 父组件定义了一个方法send,该方法可以接收一个参数
  2. 父组件将send方法作为子组件标签的属性haha的属性值(即父组件传参send给子组件)
  3. 子组件通过options.props接收父组件传递的haha参数(send函数)
  4. 子组件调用haha(send函数)时,传入数据“MyHello data”作为函数参数
  5. 父组件定义的send函数被触发,收到子组件的“MyHello data”数据

其实这种利用函数参数实现反向传参的例子很多,比如axios的CancelToken实现

组件自定义事件

组件标签支持绑定事件,且当事件触发后,绑定给组件的事件回调函数就会被触发。

组件自定义事件的实现主要就两步:

  • 事件绑定(包括事件定义)
  • 事件触发

组件实例的原型对象上有两个方法:

  • $on:为组件实例绑定事件
  • $emit:触发组件实例绑定的事件

上例种,利用组件自定义事件的回调函数实现了:子组件传数据给父组件。

组件绑定事件的方式有两种:

  • 组件实例适用$on绑定
  • 组件标签上使用v-on绑定

上例是基于组件标签v-on的方式为组件绑定事件。

两种方式的区别在于:

  • $on的方式更加灵活,v-on的方式更加简单
  • $on设置的回调函数如果不是options.methods的方法,则需要注意回调函数this执行,如果回调函数是箭头函数,则this就是外部作用域的this,如果回调函数是普通函数,则this就是绑定事件的组件实例。而v-on设置的回调函数一般都是options.methods的方法。

之前学习生命周期钩子时,有两个特别的钩子,分别是mounted和beforeDestroy

我们一般在mounted中为组件绑定事件,在beforeDestroy中为组件解绑事件。

为组件解绑事件使用 组件实例原型上的方法$off,该方法入参设计:

  • 入一个字符串参数,则该参数为需要被解绑的事件名
  • 入一个数组参数,则每个数组元素就是需要被解绑的事件名
  • 无参数,则解绑组件上所有的事件

一般来说,当组件被销毁后,绑定在组件上的事件会自动被解绑。皮之不存,毛将焉附。

另外,组件也可以绑定浏览器原生事件,比如click,只是此时需要加事件修饰符native,表示绑定的事件是原生事件,而不是自定义事件。

且此时原生事件是绑定给组件的template结构的根元素。

还有需要注意的是,此时点击事件的是由浏览器监听的,当点击时,浏览器触发了绑定给组件的click事件,并且调用了事件回调。

由于事件不是我们$emit触发的,所以也无法传参。

组件绑定自定义事件和原生事件的区别在于:

  • 形式上,原生事件需要加.native修饰符,否则会被识别为自定义事件
  • 生命周期上,当组件被销毁后,绑定给事件的自定义事件会被解绑,而原生事件不会

任意组件之间通信

基于父子通信思路实现任意组件通信

前面学习的都是父子组件间的通信:

  • 父传子:组件标签属性+options.props
  • 子传父:子调用父函数传参;为子绑定自定义事件,事件回调由父指定

那么爷孙组件之间如何通信呢?

按照前面学习的方法,我们可以 爷组件 传参给 父组件, 父组件 再透传给 子(孙)组件

如下面例子中,App是爷组件,A是父组件,B是孙组件

可以发现爷孙之间的通信,必须要经过一个中间人组件,中间人组件要进行繁重的透传工作。

还有兄弟组件如何实现通信呢?

根据父子通信的思路来实现的话,则逻辑很复杂,且非常混乱,如上例。此时App组件变成中间组件,A和B是兄弟组件,A与B之间的交流都要通过App。

全局事件总线

基于父子组件通信的逐层消息传递,虽然可以实现任意非父子组件间的通信,但是实现非常不友好。不友好的原因是:进行通信的上下游组件之间传递的消息,必须经过一些列中间人组件的透传,这些消息透传逻辑对于中间人组件而言是业务不相干的,会使中间人组件的可读性变差,增加代码冗余度。

为了优雅地实现任意组件间通信,Vue提出了全局事件总线的概念,即:

首先找到一个可以被任意组件访问到的全局组件,

然后通信上下游组件将通信事件绑定到全局组件上,

最后通信上下游组件通过触发对方绑定在全局组件上的通信事件,进行消息传递。

那么什么组件可以被任意组件访问到呢?

我们知道组件实例是由VueComponent构造的,而每一个组件实例对应的VueComponent都是不同的,但是所有VueComponent的原型的原型是相同的,都是Vue.prototype,所以如果将组件定义在Vue.prototype上,则该组件可以被所有组件实例沿着原型链访问到。

所以全局组件的定义位置确定了。

接下来,我们需要研究如何创建一个组件?

我们知道,组件实例需要new Component得到,但是Component不是全局的,所以我们无法访问Component,也就无法执行new操作。

再回头想想我们为何要创建一个组件?因为我们想在该组件上绑定事件,以及触发事件,也就是说

该组件只需要能够调用$on、$emit即可。

并且我们期望全局组件不参与到用户页面排版中。所以,vm实例是符合该要求的:

  • vm实例不参与用户页面排版,排版工作都有App组件管理
  • vm实例由Vue构造,自然可以访问到Vue.proptotype,而$on、$emit就是Vue.prototype上的方法

全局组件就是vm,全局事件总线就是将vm定义到Vue.prototype上,而最佳时机就是创建vm实例时的beforeCreate钩子执行时,此时vm实例已被创建,但是各种组件自定义事件尚未挂载。

这里全局事件总线名字是$bus。

我们需要注意的是,全局事件总线会被很多通信组件绑定事件,所以全局事件总线的压力很大。而当通信组件被销毁,全局事件总线上绑定的对应事件是不会被自动清理掉的,需要我们手动清理。因此,我们需要在通信组件销毁时,在其beforeDestroy钩子中为全局事件总线解绑对应事件。

这一点和组件自定义事件的解绑不同,因为绑定在组件自身的事件,会随着组件的销毁自动解绑。

另外,为了减轻全局事件总线的压力,我们对于父子组件间通信依然提倡使用老方式。

消息订阅与发布

全局事件总线本质上是依据“发布订阅模式”开发。

所谓“发布订阅模式”,一个发布者,对应多个订阅者,即一对多的依赖关系(利用消息队列)。

全局事件总线就是发布者,通信组件就是订阅者。全局事件总线只有一个,通信组件有多个。

当订阅消息发生变化时,发布者就会通知(emit)所有的订阅者。

比如全局事件总线触发了事件,就会调用事件回调,而事件回调的调用

准确的发布订阅模式定义:

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

下面我们可以实现一个简单的消息订阅与发布

export default class PubSub {
    constructor(){
        this.cache = {} // 调度中心的消息队列
    }
    
    $on(event, callback){ // 订阅
        if(!this.cache[event]){
            this.cache[event] = []
        }
        this.cache[event].push(callback)
    }
    
    $emit(event, ...data){ // 发布
        if(!this.cache[event]) return
        this.cache[event].forEach(fn => {
            fn(...data)
        })
    }
    
    $off(event, callback) { // 解除订阅
        if(!this.cache[event]) return
        if(!callback) {
            delete this.cache[event]
        } else {
            this.cache[event] = this.cache[event].filter(fn => fn !== callback)
        }
    }
}

我们在Vue项目中,使用自定义的PubSub,替代全局事件总线vm实例,其他代码不变

发现项目可以正常运行。(这里没有做$off的适配,即无法处理$off入参数组的情况)。

在真实项目开发中,我们一般都是引入第三方的发布订阅模块,而不是自己开发。下面以pubsub-js为例。

需要注意的是:

我们先在A.vue中 import PubSub from 'pubsub-js' 得到了一个PubSub,

然后在B.vue中 import PubSub from 'pubsub-js' 也得到了一个PubSub,

这两个PubSub是同一个对象。原因是:

ES6模块化语法中,同一模块多次import,则只会在首次import时执行一次模块代码,后面再次import模块将不执行模块代码,而是沿用首次执行结果。

而正是由于多次引入'pubsub-js',只得到一个PubSub对象,即只存在一个调度中心(消息队列),所以才保证了不同文件中PubSub的发布订阅操作是联通的。

关于pubsub-js的用法:

  • 订阅消息:订阅ID = PubSub.subscribe(消息名,消息处理程序)
  • 发布消息:PubSub.publish(消息名,消息内容)
  • 取消订阅:PubSub.unsubscribe(订阅ID)

需要注意的是:

  • 订阅消息subscribe方法的第二个参数是一个函数,该函数在订阅的消息被发布时调用,并且调用时传参:第一个参数默认是消息名,第二个参数才是发布消息publish的消息内容。
  • 取消订阅unsubscribe不是根据“消息名”取消的,因为考虑到消息名可能重复,所以在订阅消息时subscribe方法每次都会返回一个“订阅ID",而取消订阅也是根据"订阅ID"取消的。

slot插槽

什么是插槽

我们可以看如下一段代码

在Category.vue中展示的HTML结构是不确定的(动态的),具体展示什么结构,由App.vue决定(type参数)。

而Category.vue使用的是v-if判断,并将所有可能的结构都写了出来。

这种方式虽然可以解决需求,但是当判断条件多的时候,Category.vue的template将变得十分臃肿,且不易维护。

按照需求来看,Category.vue中展示的结构本质是由App.vue决定的,那么如果Category.vue中动态结构也可以使用模板语法占位,然后App.vue将需要展示的结构当作参数传给Category组件,这样就可以解决上面的问题了。而可以为组件中动态的HTML结构占位的语法就是插槽。我们可以这样类比理解,Mustache语法为模板中动态数据占位,插槽语法为模板中动态结构占位。

默认插槽

上面介绍了插槽的作用是:为组件的模板中动态结构占位。

那么如何定义插槽,如何使用插槽呢?

定义插槽,即将组件模板中动态结构用<slot></slot>标签占位。

使用插槽,即将 父组件决定的结构 传入 子组件slot标签占位处。

在实现上,表现为:将 父组件决定的结构 作为 子组件标签的内容(子节点)。这样在Vue编译时,就会将子组件标签的子节点标签,替换掉子组件中的slot标签。

如上例,使用插槽后,Category组件就不必维护动态结构的展示逻辑了,只需要使用slot标签接收父组件指定的结构即可。

我们还需要注意到:

  • 动态结构使用的数据
  • 动态结构使用的样式

如果数据(如front数组)定义在了App.vue中,则使用插槽后,动态结构和数据就都定义在了App.vue中,此时将不必再将数据传入Category.vue中了。

(ps:如果数据一开始就定义在了Category.vue中,那么使用插槽后,动态结构和数据就不在一起了,那该怎么办呢?)

没有使用插槽前,动态结构和其样式都定义在了Category中,那么使用插槽后,动态结构和其样式就分离了,这是否会产生影响呢?

答案是不会,因为App.vue会先将插槽中HTML结构先编译一次,然后插入到Category.vue的slot位置后,Category.vue会再编译一次。每次编译时,都会将组件的样式和结构绑定,所以对于插槽中HTML结构而言,其样式既可以定义在App.vue中,也可以定义在Category.vue中。

另外,如果我们定义了插槽,但是未给插槽传入结构,那么Category.vue插槽位置会展示什么呢?即是否可以设定默认结构?

答案是可以,当我们未给插槽传入结构,slot标签的内容就会被当成默认结构展示。

如果我们在Category.vue中定义多次slot标签,则父组件指定的结构将被复制多次

原因是:子组件中slot标签 都将被替换为 父组件在子组件标签内容中指定的结构。

具名插槽

所谓具名插槽就是具有名字的插槽,表现为<slot name="xxx"></slot>这种具有name属性的slot标签。

此时想要给具名插槽传入指定结构,则指定结构也需要指定传入的具名插槽的名字,表现为在指定结构的根元素上添加slot属性,属性值为具名插槽的名字。

我们将有名字的插槽叫做具名插槽,没有命名的插槽称为默认插槽。

具名插槽和默认插槽的区别:

  • 定义时,具名插槽slot标签有name属性,默认插槽没有
  • 使用时,传给具名插槽的结构的根元素上必须有slot属性,且属性值为具名插槽的名字。而传给默认插槽的结构不需要定义slot属性。

如果设置多个动态结构传给一个具名插槽,则表现为追加,而不是覆盖,如上例。

但是上例中,传入多个结构给一个具名插槽,每个结构都设置了slot属性,显得非常冗余,我们可以将指定给同一个具名插槽的多个结构包含到一个根元素下,然后给统一给根元素设置slot属性

但是这种写法破坏了原有结构,多出一个div。

我可以利用使用template标签来代替div,因为template标签在Vue编译后会被去除,这样就不会破坏原有结构了。

作用域插槽

在前面学习默认插槽时,我们遗留了一个问题,如果因为使用了插槽,导致动态结构和数据定义在了不同的vue文件中;

如下面例子中,使用插槽后,动态结构(ol/li)定义了在App.vue中,数据(front数组)定义在了Category.vue中,这导致动态结构无法访问到数据,进而导致动态结构编译失败。

此时该怎么办?

子组件中slot标签 最终会被 父组件传给子组件标签内容中的结构 取代。

所以我们可以理解 slot标签 就等价于 指定结构。所以我们传给slot标签的数据,就有能力传给 指定结构。

我们只需要在 指定结构 定义好数据入口,就可以完成对接,之后就可以在指定结构中使用来自slot标签传递的数据。

以上就是作用域插槽的工作原理。

下面是作用域插槽的具体使用:

  1. 将存在于子组件中的数据front,绑定为子组件中slot标签的自定义属性的属性值。
  2. 为父组件指定给子组件插槽的结构,包裹一层template标签(必须的行为),并给template定义一个scope属性,该scope属性就是接收插槽数据的入口。
  3. 子组件中slot标签的所有自定义属性会被组合进一个对象,组合对象会作为最终数据传入 父组件指定给插槽的结构scope入口。也就是说父组件指定结构的根元素的template标签的scope属性可以收到一个组合对象。
  4. 在父组件指定给子组件插槽的结构中使用组合对象的属性数据。

这里大家可能有个疑问,为啥scope属性收到的数据是一个组合对象?而不是一个数据呢?

原因是,slot标签可以设置多个自定义属性,即可以传多个数据,此时将多个数据组合到一个对象中传递更加方便。

如果slot标签只设置了一个自定义属性,此时我们通过scope属性获得组合对象,基于组合对象来操作就显得冗余,所以scope属性支持进行对组合对象进行解构,以简化代码。

作用域插槽并不是一种新类型的插槽,它既可以是默认插槽,也可以是具名插槽。

作用域插槽的重点在于“作用域”,这里的“作用域”指的是template标签的scope接收到的对象只能用于template标签下的动态结构,而无法被外部结构使用。

Vue组件样式

style标签

style标签的lang属性

默认情况下组件文件中 style标签没有定义lang属性,此时lang属性默认为“css”,表示组件编译时会将style标签中的代码当成css样式解析。

另外,组件文件中style标签的lang属性也可以设为“less”或者“sass”,表示组件编译时会将style标签中的代码以"less"或“sass”语法解析。

比如上例中style标签中的样式语法会以“less”解析,但是项目运行报错了,报错提示:缺少“less-loader”。意思是,Vue CLI(webpack)没有引入less-loader,所以无法解析less样式。

给Vue项目安装less-loader(开发依赖)后,项目正常运行。

PS:我的Vue项目依赖的webpack是5.72.1版本的,下载的less-loader是11.0.0版本的,二者是适配的。

有的人的Vue项目依赖的webpack是4.46.0版本,此版本webpack不支持8以及以上版本的less-loader,需要npm i less-loader@7。

style标签的scoped属性

在style标签上还有另一个属性scoped,该属性的作用是让style标签中的样式只能作用于当前组件的template,防止组件间样式交叉污染。

scoped的工作原理是:

  1. 首先为所在组件生成一个唯一标识,形式如:data-v-dc87507c,然后将该唯一标识设置为组件的template中所有标签的属性
  2. 为style标签中所有的样式附加属性选择器条件,比如原来样式为 .test 类,则现在样式变更为 .test[data-v-dc87507c]

比如上例中,由于Test.vue中的style标签设置了scoped属性,所以首先为该Test组件生成一个唯一标识:data-v-dc87507c,然后将该唯一标识设置为Test组件的template中每个标签的属性,则template标签内容应该变为如下:

<template>
  <div data-v-dc87507c id="test">
    <div data-v-dc87507c class="test">Hello Vue!</div>
    <Demo data-v-dc87507c></Demo>
  </div>
</template>

需要注意的是,如果data-v-dc87507c属性设置给了组件标签,如上例中Demo组件标签,则本质上是设置到了组件标签对应的template的根元素上

之后再给style标签中定义的所有样式追加 [data-v-dc87507c] 属性选择器,即style标签内容应该变为如下:

<style scoped>
  .test[data-v-dc87507c] {
    width: 200px;
    font-size: 18px;
    color: red;
    border: 1px solid black;
    padding: 5px;
  }
</style>

所以上述样式其实只能作用于具有 data-v-dc87507c 属性的标签,而data-v-dc87507c又是Test.vue专属的,这就是scoped控制样式私有的原理。

需要注意的是:scoped控制样式私有存在漏洞,即如果具有scoped样式的组件中使用了子组件,则子组件的根元素也可以使用父组件中的scoped样式,如上例中Test组件中使用了Demo组件,则虽然Test组件的style标签设置scoped私有,而Demo组件的根元素依旧可以使用Test的私有样式。原因是在给Test组件template中所有标签打唯一标识属性时,也会到Demo组件标签上,而Demo组件标签上的唯一标识属性会继承给Demo组件template的根元素。

样式穿透

前面学习了style的scoped属性,我们了解到,一般只有App.vue的style标签不设置scoped属性,因为App.vue中的style样式通常当作公共样式,可以被所有组件使用。而App.vue下管理的各级组件的style标签都应该默认设置scoped属性,来确保组件间样式隔离,不会发生交叉污染。

但是有一种情况,scoped的样式隔离给我们开发带来了不便。

在企业级开发中,我们都是引入第三方组件库的组件来开发的,而第三方组件库组件又是自带样式的,那么如果我们觉得第三方组件的样式不符合需求,需要修改,此时会如何改呢?

比如上面例子中,Demo是一个第三方组件,但是Demo组件展示出来的标题效果为红色,我们想改为绿色。由于我们不能直接变动第三方组件内部代码,所以我们只能找到红色样式,然后对其进行样式覆盖。

通过元素审查,我们找到了Demo组件中 .demo样式是导致标题为红色的原因。按照一般样式覆盖思路,我们只需要在第三方组件的.demo样式引入后,再定义一个新的.demo样式即可进行覆盖。

但是最终Demo组件的内容还是红色的,我们在Test.vue中定义的覆盖样式并未生效,检查打包文件的网页发现,Demo组件的红色样式为 .demo[data-v-xxx],比我们定义的 .demo 的权重值更高。那么我们是否可以将提升 .demo的权重值呢?

CSS-样式以及样式权重_伏城之外的博客-CSDN博客

发现法子是可行的。

那么是否就万事大吉了呢?答案是否定的,我们之前就说过,除了App.vue中style可以不加scoped外,默认其他所有组件的style都要加scoped,那么我们再把Test.vue中style标签的scoped属性加上试试:

完了,芭比Q了。由于Test.vue的style标签加上了scoped属性,所以Test.vue会在编译时生成一个唯一标签data-v-yyy,并给Test.vue中所有样式都追加一个data-v-yyy的属性选择器,也就是说我么们定义的样式 .demo 变为了 .demo[data-v-yyy] ,这样的话, .demo[data-v-yyy] 就无法覆盖第三方组件Demo中的.demo[data-v-xxx]样式了。

为了解决这一问题,Vue官方提供了样式穿透的方法,对于css语法的样式,可以使用如下语法:

第三方组件样式 {

    /*

            覆盖样式

    */

}

可以发现使用样式穿透后,在Test.vue中定义的覆盖样式.demo就生效了。

检查打包后文件,可以发现,使用了样式穿透的覆盖样式最终会被修改为:

[data-v-yyy] .demo {/覆盖样式/}

而不是未使用样式穿透前的:

.demo[data-v-yyy] {/覆盖样式/}

二者的意思完全不一样。

[data-v-yyy] .demo 样式含义是:具有data-v-yyy属性的标签的子孙标签中具有class:demo的标签使用该样式。

.demo[data-v-yyy] 样式含义是:具有class:demo属性,且具有data-v-yyy属性的标签使用该样式。

根据实际生成的HTML来看,样式穿透的样式是可以作用到Demo.vue中使用了class:demo的标签的。

另外,还需要注意的是,对于css语法的样式穿透可以使用 >>> ,但是对于less语法

这种样式穿透写法就无法被识别了,此时需要换成

/deep/ 第三方组件样式 { /覆盖样式/ }

过渡与动画

CSS3的过渡与动画的简介

CSS3提供了两个重要的样式属性

  • transition:过渡
  • animation:动画

在没有过渡与动画之前,网页中的一些动态效果都要依赖于JS代码实现的,有了过渡和动画后,通过简单的CSS样式就可以实现炫酷的动态效果。

CSS3过渡

transition属性介绍:

transition: 样式名 持续时间 速度曲线 延迟时间

transition属性的属性值由四部分组成:

  • 样式名:即被监听的样式的名字,一旦该样式值发生变化,则让其变化不是一眨眼完成的,而是具有缓慢的过渡效果。【必选】
  • 持续时间:即过渡效果的持续时间,单位为s(秒),并且单位必须带上。【必选】
  • 速度曲线:即过渡效果是匀速的linear,还是逐渐慢下来ease,还是加速ease-in,还是减速ease-out,还是先加速再减速ease-in-out。【可选,默认为ease】
  • 延迟时间:即等待几秒后,再执行过渡效果,单位为s(秒)。【可选,默认为0s】

transition属性的使用:

  • 谁要过渡效果,就加到谁的样式中
  • 过渡的样式一定要发生变化

上面例子中,我们想让h1的背景色的变化有个过渡效果,所以transition属性就加到h1标签选择器上。另外要想产生过渡效果,则h1的背景色必须要发生变化,这里我们使用:hover伪类来使h1的背景色发生变化,则最终效果如下:

h1被鼠标经过时,背景色由绿色渐变为了黄色。

CSS3的动画

CSS3的动画用法有两步:

  1. 定义动画(关键帧)
  2. 使用动画

定义动画,即定义关键帧,其语法如下

<style>

    @keyframes 动画名称 {

        0% {
            /* 具体样式 */
        }

        /*

        xx% {}
        
        xx是 (0,100)开区间中任意整数值,并且可以定义多个        

        */

        100% {
            /* 具体样式 */
        }
    }

</style>

如果定义的关键帧只有0%和100%,则可以简写为:使用动画,即在需要动画的标签的样式中添加animation属性,animation属性值由以下几部分组成:
animation-name动画名称必选animation-duration 动画持续时间必选animation-timing-function动画速度曲线可选,默认”ease“animation-delay 动画延迟时间可选,默认为0animation-iteration-count动画循环执行次数,如果想要无限循环,则填infinite可选,默认为1animation-direction动画本轮播放结束后,下次是从头开始(normal)播放,还是从本次结束位置开始逆向播放(alternate)可选,默认为normalanimation-play-state动画运行(running)或暂停(pause)控制可选,默认为runninganimation-fill-mode动画结束后,保持在结束位置(backwards),还是回到起始位置(forwards)可选,默认为forwards

最终效果如下 :

动画会将动画持续时间均匀分配到每个关键帧上,比如动画持续时间10s,则每个关键帧都对应一个时刻:

  • 0%:第0秒
  • 25%:第2.5秒
  • 50%:第5秒
  • 75%:第7.5秒
  • 100%:第10秒

当动画播放到对应关键帧时刻,比如2.5s时,则此时动画所在DOM的样式,必然是25%关键帧定义的样式。

而在关键帧与关键帧之间的时间中,则是样式的过渡效果。

动画和过渡的联系与区别:

动画的关键帧定义了样式的变化,且某个关键帧样式 变化到 另一个关键帧样式的过程就是过渡。

而动画与过渡的区别除了用法上的,还有:

动画的关键帧会自动从0%依次切换到100%,形成样式变化,而这也是动画可以自动播放的原因。而过渡无法自动播放,因为过渡的样式无法自动变化,必须由外部改变过渡样式的值,才能播放。

Vue封装的过渡和动画

进入/离开 & 列表过渡 — Vue.js (vuejs.org)

上面链接是Vue官方对于Vue封装过渡和动画的介绍。

下面我来介绍下Vue封装过渡和动画的使用:

transition组件

transition组件的作用是:

当插入或删除包含在

transition

组件中的元素时,Vue底层会自动嗅探目标DOM是否应用了 CSS 过渡或动画,如果是,则Vue底层会在恰当的时机添加/删除 CSS 类名到目标DOM上。这里的CSS类名包括如下六种:

  • v-enter
  • v-enter-active
  • v-enter-to
  • v-leave
  • v-leave-active
  • v-leave-to

那么上面六个类名被从DOM元素添加或删除的恰当时机是什么时候呢?

我们已经知道了上述样式类名会在指定的时机被Vue底层自动添加到插入元素上。但是这些类名的作用是啥呢?

  • v-enter的作用是:定义被插入元素的初始样式
  • v-enetr-to的作用是:定义被插入元素的最终样式
  • v-enter-active的作用是:定义被插入元素的CSS过渡

由于v-enter类并不涉及过渡过程,在过渡开始前,v-enter类就被移除了,所以CSS过渡不能定义在v-enter类上。而v-enter-to和v-enter-active是贯穿过渡始终的。也就是说,CSS过渡既可以定义在v-enter-active上,也可以定义v-enter-to上。

通过抓拍,我们可以看到,即使我们未定义v-enter-active类,Vue底层也会默认将v-enter-active添加到被插入元素上。而v-enter-active、v-eneter-to的本质作用是是将其它们中的样式作用到被插入元素上,而这两个类的生效时机存在高度吻合,所以CSS过渡可以添加到它们任意一个上面。

但是,我们让v-enter-to更加关注于被插入元素的最终样式的定义,Vue提倡将CSS过渡定义到v-enter-active中,而不是v-enter-to中。

以上是v-leave,v-leave-active,v-leave-to加到被删除元素上的时机。

v-leave的作用是:定义被删除元素的初始样式

v-leave-to的作用是:定义被删除元素的最终样式

v-leave-active的作用是:定义被删除元素的CSS过渡

这里我们需要思考的是,要想删除一个元素,则该元素必然要存在,那么该元素就必然有初始样式,所以v-leave完全可以不定义。

另外v-leave-active和v-enter-active的情况一样,v-leave-active的工作时间和v-leave-to存在高度重叠,所以CSS过渡既可以定义在v-leave-active上,也可以定义在v-leave-to上。

当然,我们应该按照Vue的设计思路来开发,即应该老老实实定义v-leave,v-leave-active,v-leave-to。

我们再来回头看下transition组件的作用:

插入或删除包含在

transition

组件中的元素时,Vue底层会自动嗅探目标DOM是否应用了 CSS 过渡或动画,如果是,则Vue底层会在恰当的时机添加/删除 CSS 类名到目标DOM上。

其实这里似乎有点矛盾,因为我们将CSS过渡定义在了v-xxx-active或v-xxx-to上了,而这两个类必须是被插入或删除的DOM上能嗅探到CSS过渡时,才会被添加给DOM上的,但是DOM的CSS过渡又定义在这两个类上,不添加的话,理论上Vue无法嗅探到DOM的CSS过渡的。

所以这两个类必然在被添加到DOM上前,就已经和DOM建立了联系。我理解应该是在虚拟DOM阶段,这两个类就被和虚拟DOM绑定,Vue嗅探的也是虚拟DOM是否存在CSS过渡属性,存在的话,则给真实DOM追加这两个属性。

当插入或删除包含在

transition

组件中的元素时,Vue底层会自动嗅探目标DOM是否应用了 CSS 过渡或动画,如果是,则Vue底层会在恰当的时机添加/删除 CSS 类名到目标DOM上。

关于transition组件还有一个非常重要的点需要想清楚,那就是:transition组件的工作时机是在其包含的元素发生插入或删除时。

但是在Vue中,我们是不提倡直接操作DOM的,那么transition组件要怎么触发其工作呢?

Vue本质就是将DOM操作封装到底层,对开发者暴露Vue指令语法,让开发者可以利用简单的声明式命令,来代替复杂的DOM操作,而最常见的通过Vue进行的DOM操作有:

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

这些操作对应着DOM的插入和删除。

Vue动画

本质上和Vue过渡没有区别,只是设置在v-xxx-active中的CSS过渡变成了CSS动画

需要注意的是,由于动画的关键帧已经定义了被插入/删除的元素的初始样式和最终样式,所以此时我们无需再定义v-xxx,v-xxx-to类了,因为v-xxx-active中animation动画属性引入的动画中的关键帧已经定义好了元素的初始样式和最终样式。

transition-group组件

关于transition组件,还有一点需要注意的是,transition组件来对只有单个根元素或组件进行封装

如果我们在transition中封装多个并列元素或者组件,则会报错

那么是否存在transition组件中需要包裹多个并列元素的场景吗?

答案是存在的,比如针对v-for循环产生的列表项,我们应该为其设置统一的过渡样式,此时就有必要将所有的列表项包裹在一个transition下。

但是Vue规定了transition下只能包裹一个根元素,为了防止出现下面这种冗余的情况

Vue提供了transition-group标签,它的作用和transition相同,但是可以包裹多个并列元素。

但是需要注意的是transition-group组件包裹的多个并列元素上都需要定义key属性,否则报错。一般而言,多个并列元素多由v-for循环生成,此时列表项都是默认带key属性的。而对于非v-for生成的并列元素,我们可以手动设置key,此时key值只要保证唯一即可。

transition-group和transition还有一个较为明显的区别是:

  • transition组件在编译后,不产生任何实际DOM元素
  • transition-group组件在编译后,默认产生一个span元素

另外我们可以通过transition-group组件标签上的tag属性来指导transition-group组件编译后产生的DOM元素。

transition组件标签的name属性

很多时候,我们需要在一个组件中设计多个不同的过渡样式,此时会出现一个问题,transition关联的类名只有:

v-enter、v-enter-active、v-enter-to、v-leave、v-leave-active、v-leave-to。

所以我们在一个组件中只能定义一种Vue过渡。

为了避免这种情况,transition组件标签上支持设置name属性,而此时具有name=xxx的transition组件关联的类名就变了:

xxx-enter、xxx-enter-active、xxx-enter-to、xxx-leave、xxx-leave-active、xxx-leave-to。

这样一个组件种就可以定义多个Vue过渡了。

该属性同样适用于transition-group组件

transition组件标签的appear属性

有时候,我们想要transition包裹的元素在首次渲染时(插入),就执行Vue过渡,此时,我们需要给transition组件设置一个appear属性,该属性是一个标记类属性,不需要设置属性值。

该属性同样适用于transition-group组件

集成第三方动画库:如何自定义过渡的类名

Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:

  • 在 CSS 过渡和动画中自动应用 class
  • 可以配合使用第三方 CSS 动画库,如 Animate.css
  • 在过渡钩子函数中使用 JavaScript 直接操作 DOM
  • 可以配合使用第三方 JavaScript 动画库,如 Velocity.js

上述是Vue官方给定的应用过渡效果的方式,我们使用transition组件对应第一种:在 CSS 过渡和动画中自动应用 class。

而由于好的动画很难开发出来,所以在实际企业开发中,我们多是引用第三方CSS动画库,这里Vue推荐的是Animate.css。

Animate.css | A cross-browser library of CSS animations.https://animate.style/

上面链接是Animate.css的官网地址。

首先我们需要在项目中下载到anitmate.css样式库

npm install animate.css --save

然后应用到我们的组件内(或全局)

import 'animate.css';

此时我们就可以使用animate.css中各种定义好的样式类了。

<h1 class="animate__animated animate__bounce">An animated element</h1>

如animate__animated和animate__bounce这两个类。

其中 animate__animated 是基础样式类,animate__bounce是特效样式类。

每一个特效样式类必须和animate__animated搭配才能生效。

但是此时我们需要的是,将animate库中定义好的动画和transition组件关联,而当前我们只知道transition组件会默认和六个类关联,v-enter、v-enter-active、v-enter-to、v-leave、v-leave-active、v-leave-to,我们定义在这六个类中的动画过渡样式,最终会转移到tansition包裹的DOM元素上。

难道我们要将animate库中的动画过渡样式,复制粘贴到 上面六个类中吗?有没有一种方式可以直接建立transition组件和自定义样式类的关联呢?

按照Vue官网的教程:

在transition组件标签上具有上述六个内置属性,作用和v-enter、v-enter-active、v-enter-to、v-leave、v-leave-active、v-leave-to一致,只是xxx-class是引入自定义过渡的类名,而v-xxx是内置过渡的类名,而xxx-class引入的自定义过渡的类名优先级是较高的。

$nextTick

有如下需求:

有一段文本,当我们双击该文本时,文本会变为输入框,并且输入框可以自动获取焦点,之前的文本内容会作为输入框的初始内容,我们可以在输入框中修改初始内容,修改完成后,让输入框失去焦点,即可结束文本内容的修改。效果如下图:

我们书写如下代码实现需求:

发现了一个bug,就是双击文本后,新出来的输入框无法自动获得焦点,这会造成当用户双击完文本后想放弃修改的话,则必须先点击输入框,手动让其获得焦点,然后再点击输入框外的地方,让输入框失去焦点,才能完成放弃修改动作,这是非常不友好的。

问题原因分析:

我们在文本上双击后,触发了绑定的dblclick事件,执行了事件回调changeEditStatus,该函数中,先将vc实例上的isEdit属性值改为了true,表示已经进入编辑状态,

此时我们错误地理解:

当changeEditStatus事件回调中将this.isEdit被改为true后,就立马触发了单向数据绑定,网页重新渲染,然后span被隐藏,input被显示。

之后再执行changeEditStatus事件回调中 this.$refs.ipt获取到input对应的DOM对象,然后通过focus方法为其获取焦点。

实际上,changeEditStatus事件回调中代码执行都是同步的,this.isEdit = true执行后,就立马执行了this.$refs.ipt.focus()。

当事件回调执行完后,才会因为this.isEdit的值改变,触发单向双向数据,网页被重新渲染,然后span被隐藏,input被显示。

所以this.$refs.ipt.focus()执行时,this.$refs.ipt虽然可以拿到DOM元素,但是该DOM元素尚未在网页上显示,而focus方法只对显示在网页上的DOM元素有效。所以input没有在双击后自动拿到焦点。

而Vue为什么不在this.isEdit被修改后,就立马触发单向数据绑定,重新渲染视图呢?

其实这是一个性能的考量,如果在changEditStatus事件回调中,我们频繁的修改this.isEdit值,或者有很多data数据被修改,那就会意味着将进行频繁的的数据绑定和视图重新渲染,并且大部分视图渲染的结果,会被最后一次渲染的结果覆盖,导致最后一次渲染前的渲染工作都是无意义的。

另外,单向数据绑定和视图重新渲染是需要时间的,这将导致changEditStatus事件回调中代码执行出现异步任务(重新渲染)阻塞同步任务(事件回调后续代码)的情况。

那么我们该如何处理此问题呢?

其这个问题的本质是:我们在本轮视图中,操作了下一轮视图中的DOM。如果我们的操作可以延迟到下一轮再执行,就避免了上面问题的发生。

在Vue构造函数上有一个nextTick方法

Vue.nextTick接收一个函数作为参数,该参数函数会被延迟到下次 DOM 更新循环结束之后执行。

另外Vue.prototype.$nextTick复用Vue.nextTick的代码逻辑,二者的区别在于Vue.prototype.$nextTick的参数函数的this被指定为调用$nextTick方法的vm实例或组件实例。

我们使用$nextTick来修改上面需求实现中的bug

这里$nextTick的参数函数是箭头函数,则箭头函数的his为外层作用域的this,而外层作用域的this刚好就是组件实例,所以这里$nextTick参数无论传箭头函数,还是普通函数,它们的this都一样。

Vue组件中的AJAX

Vue中AJAX库的选择

什么是AJAX

AJAX:即在浏览器中通过javascript,实现网页无刷新地,发送异步的HTTP的请求到服务器。

AJAX已经是浏览器网页与后端服务器交互的必备手段。

前端网络基础-通过XMLHttpRequest实现AJAX(一)_伏城之外的博客-CSDN博客_前端如何请求xml接口https://blog.csdn.net/qfc_128220/article/details/122152642?spm=1001.2014.3001.5502前端网络基础-通过XMLHttpRequest实现AJAX (二)_伏城之外的博客-CSDN博客https://blog.csdn.net/qfc_128220/article/details/122188427?spm=1001.2014.3001.5502

浏览器内置类XMLHttpReuqest

但是通过浏览器内置类XMLHttpRequest实现AJAX过于繁琐,所以出现了很多基于XMLHttpReuqest封装的AJAX库,比较常用有jQuery和axios。

jQuery库

但是jQuery库的绝大部分代码适用于封装DOM操作的,只有小部分用于实现AJAX封装,所以如果我们只是为了使用jQuery的AJAX,而引入jQuery库,显得得不偿失。

axios库

而axios是专门用于封装AJAX的,所以非常轻量,另外axios不仅基于浏览器内置类XMLHttpRequest封装了适用于浏览器端的AJAX,还基于Node服务器端http模块封装了适用于服务器间通信的AJAX。所以axios对于大前端开发而言非常友好。

前端网络基础 - axios使用_伏城之外的博客-CSDN博客_前端使用axioshttps://blog.csdn.net/qfc_128220/article/details/122927315?spm=1001.2014.3001.5502前端网络基础 - axios源码分析_伏城之外的博客-CSDN博客_xhradapterhttps://blog.csdn.net/qfc_128220/article/details/123034811?spm=1001.2014.3001.5502

浏览器内置函数fetch

由于浏览器内置类XMLHttpRequest实现AJAX的步骤非常繁琐,所以出现了jQuery,axios这些基于XMLHttpRequest实现的AJAX库。虽然这些AJAX库做的封装非常出色,但是想使用它们则必须要在网页中引入它们,这虽然不是什么费事的操作,但是也挺闹心的,可能开发都会想一件事,如果浏览器把axios收购了,内置为浏览器的AJAX库不是更好吗,这样就省去每次引入的操作了。

为了响应前端的快速发展,浏览器近些年也推出了一个新的AJAX实现:fetch函数。首先需要注意的是fetch并不是基于XMLHttpRequest实现的,fetch和XMLHttpRequest都是浏览器内置对象,二者是兄弟关系。

可能大家觉得fetch出来,axios就要凉了,但是实际上,fetch还有一些缺点:

  • fetch是高版本的浏览器才支持的,一些低版本的浏览器不支持fetch,而XMLHttpRequest是几乎所有浏览器都支持的,所以fetch的兼容性没有axios好
  • fetch的axios的语法非常相似,但是fetch在细节上的处理并不如axios,归根到底,fetch是浏览器内置对象,算是一个底层实现,它考虑的事情并不是满足用户使用体验,而是功能设计的全面性,性能的考量,而这也是fetch用起来没有axios舒服的原因。

前端网络基础 - fetch_伏城之外的博客-CSDN博客_前端fetchhttps://blog.csdn.net/qfc_128220/article/details/123049684?spm=1001.2014.3001.5502

Vue官方的选择

Vue官方提倡使用axios

Vue组件中使用axios

首先在项目中安装axios

npm i axios

然后在组件中引入axios

import axios from 'axios'

使用axios

使用时需要注意axios的响应是一个Promise对象,我们最好结合async await语法使用

Promise - (七)初识async&await_伏城之外的博客-CSDN博客https://blog.csdn.net/qfc_128220/article/details/120455351?spm=1001.2014.3001.5502
Promise - (九)重识async&await_伏城之外的博客-CSDN博客https://blog.csdn.net/qfc_128220/article/details/121744775?spm=1001.2014.3001.5502

Promise - (十)await暂停async函数执行的实现_伏城之外的博客-CSDN博客https://blog.csdn.net/qfc_128220/article/details/121763924?spm=1001.2014.3001.5502

Vue处理AJAX跨域问题

什么是跨域问题

浏览器本身有一个同源策略,所谓同源策略即指:

如果网页所在网址和此网页发出的AJAX请求的服务器地址的

  • 协议
  • 主机
  • 端口

如果上面三个信息,全部都一样的话,则该AJAX请求就会被浏览器认定为“同源请求”,此时浏览器不会对AJAX响应结果做拦截。

如果上面三个信息,任意一个信息不一致的话,则该AJAX请求就会被浏览器认定为“非同源请求”,此时浏览器会对AJAX响应结果做拦截,并代替返回一个报错:cors error。

而非同源请求也被叫做跨域请求,而跨域问题就是指:浏览器对于跨域请求的响应结果的拦截。

浏览器的同源策略是一种安全策略,它是为了保护服务器接口不会被恶意盗用。

但是我们需要注意浏览器同源策略的拦截动作的位置

浏览器的同源策略,并不是拦截跨域的ajax请求,跨域的ajax请求依旧可以发送到服务器。

浏览器是在得到服务器的ajax响应后,对其跨域的ajax响应进行的拦截。

那么为什么浏览器不直接在ajax请求时,就将跨域请求拦截住呢,而要将跨域请求放行到服务器呢?

因为有些服务器需要支持跨域ajax请求,比如 https://www.baidu.com:80上的网页 发送请求给 https://www.baidu.com:8080上的接口 ,而如果浏览器在请求阶段就直接把跨域ajax请求拦截了,则就彻底堵死了跨域请求。所以为了兼容服务器支持跨域请求,所以浏览器和服务器约定:

如果服务器某接口支持跨域ajax请求,则服务器接口需要在跨域ajax响应头中设置:

Access-Control-Allow-Origin

该响应头的值就是:服务器接口允许发起跨域AJAX的网页所在的网址,如果值为*,表示服务器接口允许所有网址的网页请求。

浏览器在收到服务器接口的ajax响应后,首先检查该ajax响应是否为跨域的,如果为跨域的,则检查跨域ajax响应头中是否有Access-Control-Allow-Origin,若有则检查该响应头的值中是否包含发起ajax请求的网页所在网址,若有,则浏览器会放过跨域ajax响应给网页,否则报错cors。

如何解决跨域问题

解决AJAX跨域有三种方案:

  • JSONP
  • CORS
  • 代理服务器

前端网络基础 - 跨域xhr/fetch_伏城之外的博客-CSDN博客_xhr 跨域

Vue代理服务器配置

配置单个代理服务器

Vue解决AJAX跨域的方案是通过代理服务器中转跨域ajax请求,因为同源策略是浏览器的,它只对浏览器与服务器间的交互有用,而服务器本身是没有同源策略的,所以服务器与服务器之间不存在跨域问题。

上面例子中,服务器getMsg接口不允许跨域,下面同设置代理服务器来实现跨域

配置参考 | Vue CLI (vuejs.org)https://cli.vuejs.org/zh/config/#devserver-proxy

Vue配置代理服务器的方式很简单,只需要在vue.config.js中新增一个配置项devServer.proxy

devServer.proxy配置项的值为:跨域服务器地址

当在vue.config.js中新增devServer.proxy完成后,需要重启Vue项目让该配置生效。

此时,我们需要让Vue项目中原本请求跨域服务器的AJAX的请求URL变为 代理服务器的URL。

而代理服务器的URL就是Vue项目的URL。

那么代理服务器的工作机制是啥呢?

代理服务器和vue-cli为Vue项目启动的服务器其实是同一个服务器,它们的协议、主机、端口都相同。

而Vue项目网页发送AJAX请求给代理服务器,代理服务器会先检查自身是否具有被请求的资源,若自身没有,则转发给dbServer.proxy配置的服务器。

在Vue项目的/public/text4.txt的内容为"Hello Vue"

而Node服务器也有一个接口/text4.txt,返回一个文本文件,其内容为“Hello Node”

那么在Vue项目中请求 http://localhost:8080/test4.txt时,最终得到的是哪个?

按照代理服务器的工作机制,会先去Vue项目中查找对应资源,若有,则直接返回,而不再转发请求到devServer.proxy配置的跨域服务器。

配置多个代理服务器

devServer.proxy不仅可以配置为一个字符串,还可以配置为一个对象,如下例子

devServer.proxy对象的属性是代理服务器代理AJAX请求资源路径的前缀标识。

即只有AJAX请求资源路径前缀为“/api”,代理服务器才会代理转发对应AJAX请求。

devServer.proxy[“/api”] 对象的属性含义:

target
代理服务器转发AJAX请求到哪个服务器的地址pathRewrite重写代理转发AJAX请求资源路径ws是否支持websocket协议,默认truechangeOrigin是否修改请求来源信息,默认true
target属性没啥好说的,相当于devServe.proxy为字符串的值的含义。

pathRewrite既可以是一个对象,也可以是一个函数,我倾向于函数写法,pathRewrite函数的作用是重写代理转发的AJAX请求的资源路径,那么为什么要重写呢?

因为我们为了让AJAX请求被代理,篡改了它的请求资源路径,即为其添加了/api前缀,比如原始请求路径为 /getMsg,修改后变为了 /api/getMsg,此时虽然走进了代理服务器,但是代理服务器转发该请求时,却不会擦除/api前缀,所以我们要手动对前缀进行擦除,而pathRewrite函数会被传入一个请求资源路径(如"/api/getMsg")作为入参,我们在函数中对其进行修改,然后将修改结果作为pathRewrite返回值,这样代理服务器就会使用pathRewrite返回值作为转发AJAX请求的资源路径。

ws属性默认为true,表示代理服务器支持转发websocket协议的请求,我们可以不写该配置。

changeOrigin属性默认为false,当值为true时,表示代理服务器会对转发给真实服务器的HTTP请求的来源进行篡改, changes the origin of the host header to the target URL,即修改请求头中的host值为真实服务器的地址。

例如:我们Vue项目中AJAX请求是先请求自身的代理服务器,

此时请求头host信息为locahost:8080 ,即代理服务器的地址。

之后,代理服务器会转发此请求给真实服务器

此时转发的HTTP请求的来源不再是localhost:8080,而是localhost:80,HTTP协议的80端口可以省略,所以上面host头为localhost,即为真实服务器的地址。

为啥要改host信息呢?

因为服务器为了防止跨域请求,一般会在收到请求时,对其host信息进行校验,若为同源请求,则放行,若为跨域请求则阻止。

所以代理服务器为了防止真实服务器也对请求做跨域检查,所以对请求头中的host信息进行篡改,来迷惑真实服务器。

但是这里的修改并不彻底,真实服务器还是可以通过请求头中的referer,x-forward-host等进行校验,一样可以做跨域阻止。

对比来看,这种devServe.proxy对象式写法似乎没有字符串写法好用,因为非常繁琐。但是evServe.proxy对象式写法的威力在于配置多个代理,如下

而此时,我们才知道为啥devServer.proxy对象式写法的代理要在请求资源路径前加前缀了,原因是为了区别出哪些AJAX请求走哪个代理。

Vue的复用性增强

mixin混入

局部混入 options.mixins

我们在进行Vue组件化开发时,难免会遇到多个组件中使用了相同功能,比如相同的options.data的某些数据,相同的options.methods的某些方法,或者相同的生命周期钩子,此时不好的做法是,在每一个组件中都重复定义一遍上述功能,好的做法是将相同功能解耦出去,然后由多个组件按需引入。

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项options.xxx。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项options。

例如上面两个组件的options.data.msg 和 options.methods.tip,以及options.mounted钩子都是一样的逻辑,所以好的做法是将他提取到mixin中。

下面是mixin的用法

  • 首先在src根目录下创建一个mixin.js来定义一个或多个混入对象,并用分别暴露语法暴露出去
  • 然后在组件中引入mixin.js暴露的混入对象,可以引入一个或多个,按需引入即可
  • 之后在组件实例的options.mixins数组中添加混入对象即可,options.mixins是一个数组,目的是支持多个混入对象

一旦组件实例的options.mixins有了混入对象,则混入对象的配置就会作为组件实例的options的配置。

options.mixins配置引入的混入对象只在当前组件中生效,所以叫局部混入。

混入对象合并规则

一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

混入对象的属性其实就是组件实例的options配置,比如data、methods、各种生命周期钩子,那么如果 混入对象 和 组件实例本身的options 的配置的属性发生冲突,那么以谁的为准呢?

  • data函数返回的对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
  • 值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
  • 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

需要特别注意下混入对象和组件options配置的,生命周期钩子的合并,并非是覆盖,而是合并为一个数组,且混入对象的钩子先执行。

全局混入 Vue.mixin

还有一些功能是全局所有组件通用的,此时要借助Vue.mixin函数实现全局混入。

一旦使用全局混入,它将影响每一个之后创建的 Vue 实例(包括第三方组件),请务必谨慎使用。

插件

什么是插件

插件通常用来为 Vue 添加全局功能。

我们知道Vue构造函数上有多个静态方法,如

  • Vue.mixin:全局混入
  • Vue.directive:全局指令
  • Vue.component:全局组件

这些方法都是为Vue添加全局功能的,我们一般是在main.js中定义它们,但是这样会造成main.js的臃肿,此时Vue提出了插件,插件支持对这些全局功能进行整理。

如何用插件

定义插件对象

插件本质是一个对象,该对象有一个install方法,在install方法会得到入参:

  • 第一个入参:Vue构造函数
  • 之后的入参:自定义参数

在install方法中我们可以获得Vue构造函数并在其上定义全局功能。

为了更好地解耦,避免main.js臃肿,我们会在项目根目录下定义一个plugins.js来定义或暴露插件对象,一个plugins.js最好只定义一个插件对象。

注册插件对象

我们需要在main.js中引入插件对象,并使用 Vue.use注册应用它。

Vue.use使用注意:

  • Vue.use必须在new Vue之前完成,否则插件不生效
  • Vue.use是一个函数,第一个参数接收插件对象,第二个及之后的入参会作为入参插件对象install方法的的第二个及之后的入参。Vue建议Vue.use和插件对象install方法的第二个参数都使用配置对象,因为这样参数含义更加明确。
  • Vue.use会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件

标签: Vue 组件化 组件

本文转载自: https://blog.csdn.net/qfc_128220/article/details/124926439
版权归原作者 伏城之外 所有, 如有侵权,请联系我们删除。

“Vue2.x - 组件化编程”的评论:

还没有评论