IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Vue学习笔记(三)Vue2三种slot插槽的概念与运用 | ES6 对象的解构赋值 | 基于Vue2使用axios发送请求实现GitHub案例 | 浏览器跨域问题与解决 -> 正文阅读

[JavaScript知识库]Vue学习笔记(三)Vue2三种slot插槽的概念与运用 | ES6 对象的解构赋值 | 基于Vue2使用axios发送请求实现GitHub案例 | 浏览器跨域问题与解决

一、参考资料


二、运行环境


  • Windows11
  • Visual Studio Code v2022
  • Node.js v16.5.01
  • Vue/cli v5.0.6
  • Bootstrap 5.1.3

三、Vue2插槽


Vue2 实现了一套内容分发的 API,,将 <slot>元素作为承载分发内容的出口。

作用: 父组件通过定义插槽向子组件中传递定制化的内容(数据或函数)。

3.1 默认插槽

接下来举一个案例来体现插槽的作用。

比如在App组件中:

<template>
  <div>
    <Header></Header>
  </div>
</template>

我定义了一个模板,里面有一个div,div里面引入了自定义的Vue组件Header

如果我想在App组件里引入Header组件,并向里面添加内容,比如:

<Header>
	<h1> 搜索引擎 </h1>
</Header>

在默认情况下,<h1>是不会渲染出来的,当然其他普通的HTML元素也不会渲染,此时就可以使用Vue提供的 slot 插槽,我们放一个插槽在这个Header组件里面,类似这样:

(如果引入Header组件时,里面没有内容,那么就会出现Header里<slot>标签的内容:“插槽为空”

<template>
    <div>
        <slot>插槽为空</slot>
    </div>
</template>

这仿佛就是在提示Vue:<slot> 这里是一个待填的坑,将由父组件来填。

由于是父组件来填的,所以 slot 插槽可用于父组件向子组件传递数据。

注意,通过 slot 插槽渲染的元素,其样式可以通过父组件来设置。

3.2 具名插槽

具名插槽

据Vue官方的文档介绍,所谓的具名插槽其实就是有name属性值的插槽,(从vue2.6.0开始,默认的插槽的name值为default),比如我们可以在同一个组件中设置多个插槽,这样这个组件的父组件就可以向不同的插槽里插入元素。

同样以App组件 和 Header组件为例。

Header 子组件:

<template>
    <div>
        <slot name="a">插槽a为空</slot>
        <slot name="c">插槽c为空</slot>
    </div>
</template>

App父组件:

<template>
  <div>
    <Header>
    	<h1 slot="a">插槽a</h1>
    	<h1 slot="c">插槽c 内容1</h1>
    	<h1 slot="c">插槽c 内容2</h1>
    </Header>
  </div>
</template>

上述App组件中的h1标签可以是其他的HTML标签。

这里指定的slot是可以复用的,不过插入相同插槽的元素可以用template标签来指定子组件的插槽,上述App组件代码的简写形式如下:

<template>
  <div>
    <Header>
    	<h1 slot="a">插槽a</h1>
    	<template slot="c">
    		<h1>插槽c 内容1</h1>
    		<h1>插槽c 内容2</h1>
    	</template>
    </Header>
  </div>
</template>

注意:上述在template直接使用slot的写法已经在Vue2.6.0 起被废弃 查看文档

Vue2.6.0 版本以后的 具名插槽 用法如下 查看文档

声明插槽的方式不变(不过添加了default默认的插槽机制):

<!-- 插槽名为 slot1 -->
<slot name="slot1"></slot>
<!-- 插槽名默认为default -->
<slot></slot>

父组件在引入指定插槽时候,template指定插槽时必须使用 v-slot ,类似于 v-on 的@和 v-bind的:v-slot 其简写形式为 #,比如:

<template>
  <div>
    <Header>
    	<h1 slot="a">插槽a</h1>
    	<template #c>
    		<h1>插槽c 内容1</h1>
    		<h1>插槽c 内容2</h1>
    	</template>
    </Header>
  </div>
</template>

注意: v-slot 属性只支持 <template>标签

同时, 由于插槽默认名称为 "default ",所以父组件可以指定默认插槽的内容:

<Header>
    <template v-slot:default>
    	<h1>插槽c 内容1</h1>
    	<h1>插槽c 内容2</h1>
   </template>
</Header>

<!-- 简写形式如下 -->

<Header>
    <template #defualt>
    	<h1>插槽c 内容1</h1>
    	<h1>插槽c 内容2</h1>
   </template>
</Header>

3.3 作用域插槽

使用场景:父组件渲染元素时候会用到子组件的数据,此时可通过作用域插槽,将设置插槽的子组件里的数据传给父组件,方便父组件往子组件里设置元素。

在Vue2中引入子组件时,父组件可以通过:Key=Value来传值,比如在App组件中:

<Header :name="uni"/>

这里在引入Header组件的同时,传入了名为name,值为"uni"的变量。

然后在Header组件中就可以通过 {{ name }} 直接获取这个值。

<slot> 标签也可以通过这种方式传入数据,比如在Header子组件中:

<template>
    <div>
        <slot :name="uni">插槽a为空</slot>
    </div>
</template>

这个 name变量就会传入到引入这个Header组件的父组件 App,然后父组件App在写插槽元素时就可以获取到数据了:

<template>
  <div>
    <Header>
    	<template slot-scope="data">
    		<h1> {{ data.name }}</h1>
    	</template>
    </Header>
  </div>
</template>

这里的 slot-scope 声明了被接收的 prop 对象会作为 slotProps 变量存在于 作用域中。我们可以像命名 JavaScript 函数参数一样随意命名 data。

说起来可能会比较绕,简单理解就是作用域插槽,可以让子组件传递数据给父组件,不过父组件只能在引入该组件标签的内部获取到数据。

绑定在 <slot> 元素上的 attribute 被称为 插槽 prop

关于作用域插槽,官方提到的一句很重要的话:

  • 父级模板里的所有内容都是在父级作用域中编译的;

  • 子模板里的所有内容都是在子作用域中编译的。

注意:以上通过 slot-scope来指定作用域的方法自Vue2.6.0开始已被废弃 官方文档

接下来是Vue2最新版本的作用域插槽用法 官方文档

<template v-slot:default="data">
  {{ data.name }}
</template>

<!-- 简写形式如下: -->

<template #default="data">
  {{ data.name }}
</template>

其实这里和之前差不多,新版本的改动就是 template必须使用v-slot来指定插槽,或者指定插槽引入的数据。

和默认插槽一样,如果在引入数据时没有指定插槽名称,那么数据将从默认插槽中获取

<template v-slot="data">
  {{ data.name }}
</template>

这里需要注意 v-slot="data"v-slot:default 二者的区别,前者是从默认插槽里获取数据,定义为data,后者是指定为默认插槽。

ES6解构赋值概念 & 作用域插槽的解构赋值

ES6的解构赋值

“ES6是门脚本语言,从名字(ECMAScript6)中我们可以看出,他是JS的组成部分,直白点说,它规定了我们怎样写JS。”

ES6解构赋值,是指我们在写JavaScript代码时可以实现的一些特殊机制。

在解构中,有下面两部分参与:

  • 解构的源,解构赋值表达式的右边部分。
  • 解构的目标,解构赋值表达式的左边部分。

在Vue2作用域插槽中的解构赋值,是将参数值转为的Object对象,所以我们先主要了解ES6关于对象的解构赋值的一些机制:
(这里注意,等式的左边相当于子组件在定义slot标签时传入的参数,等式的右边则相当于父组件在插入元素到指定slot时,在v-slot:xxx=“Object” 中的Object)

  • 基本的解构
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
// foo = 'aaa'
// bar = 'bbb'
 
let { baz : foo } = { baz : 'ddd' };
// foo = 'ddd' , 可理解为替换了Key=baz的Value
  • 可嵌套可忽略
let obj = {p: ['hello', {y: 'world'}] };
let {p: [x, { y }] } = obj;
// obj.x = 'hello'
// obj.y = 'world'
// PS: 之前我们使用的作用域插槽就类似于这种,父组件只接收数据赋值为 obj , 然后我们通过obj来调用子组件在<slot>里面指定的属性
let obj = {p: ['hello', {y: 'world'}] };
let {p: [x, {  }] } = obj;
// x = 'hello'
  • 不完全解构
let obj = {p: [{y: 'world'}] };
let {p: [{ y }, x ] } = obj;
// x = undefined
// y = 'world'
// 尽管obj里的p对象中只有一个元素,但是它同样可以赋值给具有两个元素的对象,多出来的对象就为undefined类型
  • 剩余运算符
let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40};
// a = 10
// b = 20
// rest = {c: 30, d: 40} 剩余的元素将以对象的形式赋值给rest
  • 解构默认值(只会根据相应的Key进行替换,而不会替换整个Object对象)
let {a = 10, b = 5} = {a: 3};
// a = 3; b = 5;
let {a: aa = 10, b: bb = 5} = {a: 3};
// aa = 3; bb = 5;

Array数组模型的解构和对象类似,不过多描述了,在Vue2中主要用到的是对象解构可以参考 点击查看

作用域插槽的内部工作原理是将插槽的内容包裹在一个拥有单个参数的函数里。

解构插槽 Prop ,这是官方在介绍作用域插槽时的拓展内容,我觉得有必要Mark一下,将来使用组件的时候可能会用到。

根据官方解释,我们举个实际的例子来理解一下这句话:

  • 子组件Header组件定义一个插槽
<template>
  <div>
    <h1>头部</h1>
    <slot name="slot1" :username="username" :age="age"></slot>
  </div>
</template>

<script>
export default {
    name: 'Header',
    data(){
        return {
            username: 'uni',
            age: '22',
        }
    }
}
</script>
  • 父组件App使用插槽(#是v-slot的缩写形式)
<template>
  <div id="app">
    <Header>
      <!-- 填补插槽 -->
      <!-- 
      <template #slot1="data">
          <p>不同的方式输出slot的数据: {{data}}</p>
          <p>直接获取对象的属性: {{data.username}}</p>
          <p>直接获取对象的属性: {{data.age}}</p>
      </template> 
      -->
      <template #slot1="{username, age, abc}">
          <p>不同的方式输出slot的数据: {{username}}, {{age}}</p>
          <p>当获取不存在的参数时:{{typeof(abc)}}</p>
      </template>
    </Header>
  </div>
</template>

<script>
import Header from './components/Header.vue'

export default {
  name: 'App',
  components: {Header}
}
</script>

这里其实是将参数 username 和 age 包裹在一个函数中,类似下面的JS函数:

function (data){
	// 插槽的内容
}
这说明 v-slot 的值实际上是任何能作为函数定义中的参数的JavaScript表达式

那么在子组件声明插槽的这句话中:

 <slot name="slot1" :username="username" :age="age"></slot>

Vue2底层做的工作:从当前的Vue组件里的data变量username和age,如果data中没有就从methods里查找,查找数据后,在填补该插槽的父组件调用时将获取到这个数据,比如在这里就应该是:

{
  username: 'uni',
  age: '22',
}

这是一个JS中的Object对象,而且里面的value已经是比较简单的字符串了,复杂的情况下可能value
会是一个函数,或者仍然是一个对象,层层嵌套。

接下来就详细分析在上述案例中,在父组件填补插槽两种不同的情况下的发生的解构赋值:

情况一:指定为整个对象(slot传参少的话可以使用)

<template #slot1="data">
    <p>不同的方式输出slot的数据: {{data}}</p>
    <p>直接获取对象的属性: {{data.username}}</p>
    <p>直接获取对象的属性: {{data.age}}</p>
</template> 

运行效果:
在这里插入图片描述

这里发生的解构是:基础解构

// 相当于
let data = {
  username: 'uni',
  age: '22',
}

情况二:指定为对象(推荐使用)

<template #slot1="{username, age, abc}">
    <p>不同的方式输出slot的数据: {{username}}, {{age}}</p>
    <p>当获取不存在的参数时:{{typeof(abc)}}</p>
</template>

运行效果:
在这里插入图片描述
这里发生的解构是:基础解构(和之前相同)

// 相当于
let {username, age, abc} = {
  username: 'uni',
  age: '22',
}

通过上面案例可以看到,如果{ } 里的参数值在子组件的slot标签里未指定,那么在父组件进行调用时候就默认为undefined类型。

我们可以通过下面的默认值解构来解决这个问题,就是当数据不存在时赋予一个默认值。

解构的进阶:默认值解构

假设将之前的HTML代码替换成如下:

<template #slot1="{username, age, abc='Vue666'}">
    <p>不同的方式输出slot的数据: {{username}}, {{age}}</p>
    <p>当获取不存在的参数时,但设置了默认值时:{{abc}}</p>
</template>

运行效果:

在这里插入图片描述
这里发生的解构是:默认值解构

// 相当于
let {username, age, abc='Vue666'} = {
  username: 'uni',
  age: '22',
}

综上,Vue2作用域插槽支持子组件向父组件传递参数,同时支持ES6的解构赋值。

3.4 动态插槽名

动态插槽名 是指 动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:

使用语法:

<template v-slot:[dynamicSlotName]>
  ...
</template>

案例:

  • Header子组件:
<template>
  <div>
    <h1>头部</h1>
    <slot name="slot1" :title="'插槽1'"></slot>
    <slot name="slot2" :title="'插槽2'"></slot>
  </div>
</template>

<script>
export default {
    name: 'Header',
}
</script>
  • App父组件:
<template>
  <div id="app">
    <button @click="slotType = 'slot1'">选择插槽1</button>
    <button @click="slotType = 'slot2'">选择插槽2</button>
    <Header>
      <template #[slotType]="title">
          当前选择的插槽为: {{title}}
      </template>
    </Header>
  </div>
</template>

<script>
import Header from './components/Header.vue'

export default {
  name: 'App',
  data(){
    return {
      slotType: 'slot1'
    }
  },
  components: {Header}
}
</script>

实现效果:

在这里插入图片描述
如上图,动态插槽支持父组件在不同的场景下引入子组件的插槽。

四、GitHub用户搜索案例


做一个简单的搜索页面,用户输入用户名后,发送GET请求到GitHub提供的接口,获取对应用户名的列表信息(部分),最终列表信息将展示到页面上。

实现效果如下:

在这里插入图片描述

4.1 准备静态的HTML文件

index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!-- 引入 Bootstrap 样式-->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong> 浏览器不支持JavaScript. </strong>
    </noscript>
    <h1 class="text-center">基于Vue2的GitHub用户搜索案例</h1>
    <!-- Container Begin-->
    <div class="container shadow">
        <!--Head 搜索栏 Begin-->
        <div class="mb-3 row">
          <label for="input-search" class="col-sm-2 col-form-label text-end">搜索GitHub用户</label>
          <div class="col-sm-8">
            <input class="form-control" id="input-search">
          </div>
          <div class="col-sm-2">
            <button class="btn btn-primary">搜索</button>
          </div>
        </div>
        <!--Head 搜索栏 End -->

        <!--Body GitHub用户列表 Begin -->
        <div class="container">
          <ul class="list-unstyled row row-cols-md-4 ">
            <li class="mb-5">
              <div class="card text-center mx-auto" style="width: 120px;">
                <div>
                  <img src="logo.png" class="card-img-top mx-auto shadow" style="width: 100px;height: 100px;border-radius: 50%;">
                </div>
                <div class="card-body">
                  <p class="card-text text-success"> 用户名 </p>
                </div>
              </div>
            </li>
            <li class="mb-5">
              <div class="card text-center mx-auto" style="width: 120px;">
                <div>
                  <img src="logo.png" class="card-img-top mx-auto shadow" style="width: 100px;height: 100px;border-radius: 50%;">
                </div>
                <div class="card-body">
                  <p class="card-text text-success"> 用户名 </p>
                </div>
              </div>
            </li>
            <li class="mb-5">
              <div class="card text-center mx-auto" style="width: 120px;">
                <div>
                  <img src="logo.png" class="card-img-top mx-auto shadow" style="width: 100px;height: 100px;border-radius: 50%;">
                </div>
                <div class="card-body">
                  <p class="card-text text-success"> 用户名 </p>
                </div>
              </div>
            </li>
            <li class="mb-5">
              <div class="card text-center mx-auto" style="width: 120px;">
                <div>
                  <img src="logo.png" class="card-img-top mx-auto shadow" style="width: 100px;height: 100px;border-radius: 50%;">
                </div>
                <div class="card-body">
                  <p class="card-text text-success"> 用户名 </p>
                </div>
              </div>
            </li>
          </ul>
        </div>
      <!--Body GitHub用户列表 End-->
    </div>
    <!-- Container End -->

    <!-- Vue2 App 组件入口 -->
    <div id="app"></div>
  </body>
</html>

运行效果:

在这里插入图片描述

4.2 根据静态文件划分组件

除了管理主页面的App组件以外,该页面内部分为Header搜索和Body用户列表显示
在这里插入图片描述

4.3 代码实现

index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!-- 引入 Bootstrap 样式-->
    <!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong> 浏览器不支持JavaScript. </strong>
    </noscript>
    <h1 class="text-center">基于Vue2的GitHub用户搜索案例</h1>
    <!-- Vue2 App 组件入口 -->
    <div id="app"></div>
  </body>
</html>

Main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false
import vueResource from 'vue-resource'
//npm i vue-resource
//使用插件vue-resource 发送GET请求
Vue.use(vueResource)
new Vue({
  el: '#app',
  render: h => h(App),
  beforeCreate(){
    Vue.prototype.$bus = this
  }
})

App.vue

组件通信采用和上回一样的全局事件总线:

Vue学习笔记(二)基于Vue2的TodoList待办事项案例 | localStorage本地存储 | Vue2的发布与订阅 | Vue2支持的动画类 |JavaScript原型对象

这里主要是自定义的Header和自定义的Body两个组件的数据通信,Header负责搜索GitHub的用户信息,发送出去,而Body则负责接收(订阅)用户的列表数据。

<template>
  <!-- Container Begin-->
  <div class="container shadow">
    <!--Head 搜索栏 Begin-->
    <Header></Header>
    <!--Head 搜索栏 End -->

    <!--Body GitHub用户列表 Begin -->
    <Body></Body>
     <!--Body GitHub用户列表 End-->
  </div>
  <!-- Container End -->
</template>

<script>

// npm i bootstrap
import 'bootstrap/dist/js/bootstrap'
import 'bootstrap/dist/css/bootstrap.min.css'
import Header from './components/GitHubHeader.vue'
import Body from './components/GitHubBody.vue'
export default {
  name: 'App',
  components: {Header, Body},
}
</script>

GitHubHeader.vue 基于axios发送AJAX请求

<template>
    <div class="mb-3 row">
        <label for="input-search" class="col-sm-2 col-form-label text-end">搜索GitHub用户</label>
        <div class="col-sm-8">
        <input class="form-control" id="input-search" v-model="searchName">
        </div>
        <div class="col-sm-2">
        <button class="btn btn-primary" @click="search()">搜索</button>
        </div>
    </div>
</template>

<script>
import axios from 'axios'

export default {
    name: 'Header',
    data(){
        return {
            searchName: ''
        }
    },
    methods: {
        search(){
            // 防止用户输入空的名称
            if(this.searchName === '') return this.$bus.$emit(
                'selectUserByName', {isFirst: false, errorMsg: '无法查询空的名称'}
            )
            // 发布加载的消息
            this.$bus.$emit('selectUserByName', {errorMsg: '', isFirst: false, isLoading: true})

            // 方式一: 使用 axios
            /*
            axios.get(`https://api.github.com/search/users?q=${this.searchName}`).then(
                response => {
                    console.log('请求成功')
                    // 发布请求成功的消息
                    this.$bus.$emit('selectUserByName', {
                        errorMsg: '',
                        isLoading: false,
                        userList: response.data.items,
                    })
                },
                error => {
                    console.log('请求失败', error.message)
                    // 发布请求失败的消息
                    this.$bus.$emit('selectUserByName', {
                        isLoading: false,
                        errorMsg: '获取数据失败'
                    })
                }
            )
            */
            // 方式二: 使用 vue-resource
            this.$http.get(`https://api.github.com/search/users?q=${this.searchName}`).then(
                response => {
                    console.log('请求成功')
                    // 发布请求成功的消息
                    this.$bus.$emit('selectUserByName', {
                        errorMsg: '',
                        isLoading: false,
                        userList: response.data.items,
                    })
                },
                error => {
                    console.log('请求失败', error.message)
                    // 发布请求失败的消息
                    this.$bus.$emit('selectUserByName', {
                        isLoading: false,
                        errorMsg: '获取数据失败'
                    })
                }
            )
        }
    },
}
</script>

GitHubBody.vue

<template>   
    <div class="container">
        <!-- 欢迎提示 -->
        <h1 v-show="info.isFirst" class="text-center">? 欢迎访问 ? </h1>
        <!-- 错误提示 -->
        <h1 v-show="info.errorMsg" class="text-center text-danger">错误: {{ info.errorMsg }}</h1>
        <!-- 加载页面 -->
        <div v-show="info.isLoading" class="d-flex justify-content-center">
            <div v-show="info.isLoading" class="spinner-border text-info" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
        </div>
        <!-- 用户列表 -->
        <h5 v-show="info.userList.length" >共 {{info.userList.length}} 条结果</h5>
        <ul v-show="info.userList.length" class="list-unstyled row row-cols-md-4 ">
            <li class="mb-5" v-for="(user) in info.userList" :key="user.name">
                <div class="card text-center mx-auto" style="width: 120px;">
                <div>
                    <a :href="user.html_url" target="_blank">
                        <img :src="user.avatar_url" class="card-img-top mx-auto shadow" style="width: 100px;height: 100px;border-radius: 50%;">
                    </a>
                </div>
                <div class="card-body">
                    <p class="card-text text-success"> {{ user.login }} </p>
                </div>
                </div>
            </li>
        </ul>
    </div>
     
</template>

<script>


export default {
    name: 'Body',
    data(){
        return {
            info:{
                isFirst: true,      // 初次访问时, 默认为true
                isLoading: false,  // 加载标记
                errorMsg: '',      // 错误信息
                userList:[]        // 用户数据
            }
        }
    },
    methods: {

    },
    mounted(){
        // 订阅(合并参数)
        this.$bus.$on('selectUserByName', (newInfo)=>{
            this.info = {...this.info, ...newInfo}
        })
    },
    beforeDestroy(){
        this.$bus.$off('selectUserByName')
    }
}
</script>

五、浏览器跨域

参考文献

[1] 周晓黎.Ajax跨域访问Web Services[J].电脑编程技巧与维护,2014(08):93-96.DOI:10.16184/j.cnki.comprg.2014.08.058.
[2] 详解浏览器跨域的几种方法

5.1 什么是浏览器跨域

在WEB前端开发中,最常见的跨域问题就是浏览器在A域名页面发送B域名的请求时会被限制。

这种跨域往往来自对URL的GET请求或者是JavaScript通过AJAX发送的GET、POST等请求。

AJAX是 Asynchronus JavaScript and XML(异步JavaScript和XML)的缩写,AJAX主要运用了XMLHttpRequest,即异步数据读取技术,从而可以自主的发起Web请求,与远程服务器进行数据交互。[1]

在Vue中,通常使用axios发送异步请求,而不是AJAX。

axios技术是基于JS的 Promise 实现的,Promise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复杂的异步任务,所以 axios与ajax的最大区别就是,axios支持Promise中的API,所以在Vue技术中,axios是发送请求的主流技术。

而axios和ajax本质上都是向某个URL发送请求,接下来将以AJAX的角度来描述跨域问题。

传统Web应用程序和Ajax方式的Web应用程序交互方式的比较如下图所示:
在这里插入图片描述

从图中可以看出,使用AJAX技术可以减少用户在前端页面等待的时间,提升用户的体验。
使用AJAX技术构建的Web页面,无需终端交互流程即可重新加载和动态更新。[1]

当使用AJAX向服务器请求资源时,此时就会涉及安全问题,在常见的JavaWeb开发中,通常是通过Tomcat部署Web项目,如果是在本地部署的话,就不会出现这个问题:

比如在 login.html 登录页面中,点击登录后需要向服务器发送登录请求,比如是向 /user/login 发送请求,为什么这时我们可以直接访问到服务器呢?其实不难理解,毕竟是在同一台机器上,那么肯定不会出现权限问题。

其实在Web浏览器中,有这么一个规则限制了JavaScript脚本代码的访问权限,并不是说想对哪个服务器发送请求,服务器就会接受,通常JS的限制有两种。第一种是对客户端的操作权限进行限制,第二个是对远程操作进行限制。

JS操作客户端的限制

1)不允许写本地文件和目录
2)不允许删除本地文件和目录

说白了就是操作本地文件和目录的话只能进行读取,最常见的就是设置头像,比如我们可以在CSDN网站的个人中心里修改自己的头像,修改头像操作本质上就是通过JavaScript代码来读取我们电脑的本地文件,但是我们不能在网站里面修改自己电脑上的文件,这是不允许的,否则当我们进了一个恶意篡改的网站,万一允许的话,自己电脑的文件就会被修改或者被删除,那么就会出现很严重的问题。

JS远程操作的限制

JavaScript 脚本代码对远程服务器的操作限制称为 同源策略(Same-Origin Policy),即 JS脚本只能访问与包含它的文档在同一域(domain)下的文档。

什么是域呢?

首先一个远程网页的URL由四个部分组成: 协议://主机:端口号/路径 ,比如:http://localhost:8080(此时路径为 /

同一域就是指两个URL的 协议://主机:端口 这3个部分相同的情况。

HTTP协议的默认端口号为80,HTTPS协议的默认端口号为443,因为是国际统一的标准,我们在访问网页时,这两个端口号不会显示出来,而我们自己在部署Web项目时,则可以指定为其他的端口号,比如8080、8081、80等,接下来以最常见的 http://localhsot:8080 这个URL为例,与它同源或需跨域的各种URL样式如下(这里默认本地主机名的DNS域名配置的主机名与IP不同):

JS访问的URL访问方式原因
http://localhost:8080/user/login同源
http://localhost:8080/index同源
http://uni:8080跨域主机不同
http://www.localhost:8080跨域主机不同
http://localhost:80跨域端口不同
https://localhost:8080跨域协议不同

根据 [2] 中提出的例子:

A 网站是一家银行网站,A网站在用户的机器上设置了一个Cookie,包含一些隐私信息(比如存款总额),当用户离开A网站后,又去访问B网站,如果没有同源限制,那么B网站就可以读取A网站的Cookie,那么隐私信息就会泄露。更可怕的是,有的网站会使用Cookie来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。

在Web前端非同源情况下,共有三个行为受到限制:

  • Cookie、LocalStorage和IndexDB 无法读取
  • DOM无法获得
  • AJAX请求不能发送

正因为有这个限制,当我们访问不同的网站时,用浏览器调试工具去查看Cookie、localStorage、IndexDB这些信息,显示出来的结果也是不一样的,虽然都是保存在我们本地机器上,但是不同的网站只能从我们的机器上读取各自的信息,而不能读取其他网站保存到我们机器上的信息。

通过一个案例来查看跨域问题:

后端SpringBoot 提供一个接口,/hello,该接口返回简单的JSON数据,格式如下:

{
	code: '200',
	msg: '你好呀!'
}

Controller层代码:

package com.uni.crossdomain.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class UserController {
    @GetMapping("/hello")
    public Map<String, Object> hello(){
        return new HashMap<String, Object>(){{
            put("code", 200);
            put("msg", "你好呀!");
        }};
    }
}

运行效果:

在这里插入图片描述
接下来,使用vscode编辑HTML页面,使用 Live Server 插件部署一个端口号为5000的前端项目

HTML代码如下(使用JQuery实现AJAX请求):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <html>
    <title>跨域测试</title>
</head>
<body>
    <h1>待发送请求</h1>
    <button onclick="get(this)">点击向 http://localhost:8080/hello 发送AJAX请求</button>
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    <script type="text/javascript">
        function get(btn){
            $.ajax({
                url: 'http://localhost:8080/hello',
                type: 'GET',
                error: (errMsg) =>{
                    alert('请求失败')
                    $(btn).prev().text(JSON.stringify(errMsg))
                },
                success: (data) =>{
                    alert('请求成功')
                    $(btn).prev().text(JSON.stringify(data))
                }
            })
        }
    </script>
</body>
</html>

在这里插入图片描述
此时后端没有收到任何信息,AJAX返回的错误信息为:

{
	"readyState":0
	"status":0,
	"statusText":"error"
}

浏览器报错的信息为:

Access to XMLHttpRequest at ‘http://localhost:8080/hello’ from origin ‘http://127.0.0.1:5500’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

翻译后:

访问位于’http://localhost:8080/hello’从原点’http://127.0.0.1:5500“”已被CORS策略阻止:请求的资源上不存在“Access Control Allow Origin”标头。

可以看到,由于我用5500的端口号去请求8080的端口号的资源,这里就被CORS策略拦截了,不过这里的localhost和127.0.0.1,即请求的主机是一样的,因为在本机的DNS域名配置文件(默认在C:\Windows\System32\drivers\etc\hosts)中有这么一段注释,即localhost本质上就是127.0.0.1的地址。

# localhost name resolution is handled within DNS itself.
#	127.0.0.1       localhost
#	::1             localhost

5.2 浏览器跨域的解决方案

CORS标准

CORS 是一个 W3C 标准,全称是跨域资源共享(CORSs-origin resource sharing),它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求。
其实,准确的来说,跨域机制是阻止了数据的跨域获取,不是阻止请求发送。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。[2]

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。[2]

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨域通信。更多相关内容可以参考文章 点击查看,这里主要是做个了解。

JSONP跨域

在HTML文档中,通常会引入JS的脚本例如:

    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
    <script type="text/javascript">
        ... 自定义JS脚本
    </script>

浏览器在加载HTML文档时,会顺序加载script标签中src的地址,这种加载是可以跨域加载的,浏览器不会阻止跨域的js加载。

比如在上个案例中

从本地的 http://127.0.0.1:5000 向 https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js 请求获取到了JS脚本资源,这样一来,自定义的JS脚本里就可以使用JQuery的函数了,这就是一种跨域访问资源的真实案例。

此外,还有img等标签,可以跨域加载,比如我们可以在自己的网站中引入网络上的图片。

而 JSONP正是利用了script/img等标签能够跨域加载,来实现跨域请求的功能,例如:

function showData(data){
	console.log(data);
}
$('body').append(`<script src='http://localhost:8080/hello?callback=showData'><\/script>`);

测试结果:

跨源读取阻止(CORB)功能阻止了 MIME 类型为 application/json 的跨源响应http://localhost:8080/hello?callback=showData&_=1655954732590。有关详细信息,请参阅 https://www.chromestatus.com/feature/5629709824032768。

仍然失败,这是因为服务端没有支持CORB跨域,这个BUG将在下面使用Vue2支持的代理服务器解决。

代理服务器跨域 【Vue采取的策略】

参考文章:通俗易懂的Nginx工作原理

提到 proxy反向代理不得不说一说 Nginx这个目前非常流行的框架。

Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。

Ngnix的优点有很多:模块化、异步、多进程单线程等,除此之外,它解决了Ajax跨域的问题。

在这里插入图片描述

大概了解这么一个解决跨域方式后,我们来熟悉一下Vue2中配置跨域的流程。

核心的配置:devServer.proxy

Type: string | Object

如果你的前端应用和后端 API 服务器没有运行在同一个主机上,你需要在开发环境下将 API 请求代理到 API 服务器。这个问题可以通过 vue.config.js 中的 devServer.proxy 选项来配置。

案例:同样是之前的 5.1 部分提到的跨域案例。

SpringBoot后端,端口号为8080,Controller层为:

package com.uni.crossdomain.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class UserController {
    @GetMapping("/hello")
    public Map<String, Object> hello(){
        return new HashMap<String, Object>(){{
            put("code", 200);
            put("msg", "你好呀!");
        }};
    }
}

前端Vue2脚手架:
vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({

  // 关闭eslint语法检查
  lintOnSave: false,
  devServer: {
    // 配置本地服务器的端口号
    port: 5000,
    // 开启代理服务器
    /* 方式一: 不写前缀, 当发起本地5000端口请求时,如果本地没有找到资源,就请求到8080端口
     缺点:
      1) 只能配置单个请求转发
      2) 不支持路径与静态资源重复, 如请求 http://localhost:5000/hello 时, 会先在本地查找hello的静态资源文件,如果没有的话才会转发到 http://localhost:8080/hello
    */ 
    // proxy: 'http://localhost:8080'

    /*
      方式二: 带前缀配置(可设置多个代理)
    */
   proxy: {
    '/springboot': {
      target: 'http://localhost:8080',
      ws: true, // 用于支持 websocket
      pathRewrite: {'^/springboot':''},
      changeOrigin: false // 设置代理服务器是否发送真实的本地URL
    }
   }
  }
})

App.vue

<template>
  <div>
      <h1> {{info}} </h1>
      <button @click="get()">向 {{reqUrl}} 发送请求</button>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'App',
  data(){
    return {
      reqUrl: 'http://localhost:5000/springboot/hello',
      info: '请求内容'
    }
  },
  methods: {
    get(){
      axios.get(this.reqUrl).then(
        response => {
          alert('请求成功!')
          this.info = response.data
        },
        error => {
          alert('请求失败.')
          this.info = error.message
        }
      )
    }
  }
}
</script>

运行结果:

在这里插入图片描述
注:这里后端SpringBoot没有进行跨域相关的配置,这里是由Vue2内部提供的代理服务器实现的跨域。

5.3 Vue2脚手架实现跨域总结

方式一:在 vue.config.js中添加配置

devServer: {
	proxy: "http://localhost:5000"
}
  • 优点:配置简单,请求资源时候直接发给前端8080,它会将其转发给5000

  • 缺点:无法配置多个代理,不能灵活控制请求是否要进行代理

  • 运行机制:按照上面的配置,当请求了前端不存在的资源时,那么该请求才会转发给配置的服务器(即优先匹配本地前端的资源)

方式二:在 vue.config.js中添加具体的代理规则

module.exports = defineConfig({
  // 关闭eslint语法检查
  lintOnSave: false,
  devServer: {
   // 配置本地服务器的端口号
   port: 5000,
   proxy: {
    '/springboot': { // 匹配所有以 '/springboot' 开头的请求路径
      target: 'http://localhost:8080',	// 代理目标的基础路径
      ws: true, // 用于支持 websocket
      pathRewrite: {'^/springboot':''}, // 发送请求时正则替换路径
      changeOrigin: true // 设置代理服务器是否发送真实的本地URL(推荐为true)
    },
    
    '/uni': { // 匹配所有以/uni开头的请求路径
	    target: 'http://localhost:8081', // 代理目标的基础路径
		pathRewrite: {'^/uni':''}	    // 发送请求时正则替换路径
	    changeOrigin: true
     }
  }
})
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-25 18:01:20  更:2022-06-25 18:01:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年9日历 -2024/9/17 3:41:45-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码