Vue 总结上

本文详细介绍了Vue.js的核心特性,如数据驱动、组件化和指令系统,对比了Vue与传统开发的区别,并探讨了单页应用(SPA)的概念及其与多页应用(MPA)的差异。此外,还讲解了如何实现SPA、SEO优化策略,以及Vue中的条件渲染指令v-if和v-show、列表渲染指令v-for的使用。同时,文章讨论了Vue实例的挂载过程、生命周期和组件通信,最后提到了混入(mixin)在代码复用中的作用。
摘要由CSDN通过智能技术生成

目录


一、Vue是什么

二、Vue核心特性

三、Vue跟传统开发的区别

四、SPA是什么

五、SPA和MPA的区别

六、实现一个SPA

七、如何给SPA做SEO

八、v-show与v-if

九、v-for与v-if

十、Vue实例挂载的过程

十一、Vue的生命周期

十二、如何解决SPA首屏加载速度慢

十三、为什么data属性是一个函数而不是一个对象

十四、动态给vue的data添加一个新的属性

十五、Vue中的组件和插件

十六、组件间通信

十七、NextTick

十八、vue中的mixin


一、Vue是什么

Vue.js(简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。(一个构建数据驱动的渐进性框架),通过API实现响应数据绑定和视图更新。

二、Vue核心特性

数据驱动(MVVM)

这里的详细解释参考之前的文章:Vue 前端面试题第一篇(MVVM、MVC、MVP设计模式)_低保和光头哪个先来的博客-CSDN博客

Model:模型层,负责处理业务逻辑以及和服务器端进行交互

View:视图层:负责将数据模型转化为UI展示出来,可以理解为HTML页面

ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁

双向绑定

此处双向绑定也是考点,参考:Vue 前端面试题第二篇(双向绑定与 v-model)_低保和光头哪个先来的博客-CSDN博客

组件化

把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件

组件化优势

1.降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现

2.调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单

3.提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

指令系统

指令是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

常用的指令:

条件渲染指令 v-if

列表渲染指令 v-for

属性绑定指令 v-bind---单向绑定

事件绑定指令 v-on---事件监听

双向数据绑定指令 v-model

三、Vue跟传统开发的区别

Vue所有的界面事件,都是只去操作数据的,Jquery操作DOM

Vue所有界面的变动,都是根据数据自动绑定出来的,Jquery操作DOM

四、SPA是什么

即单页应用。SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换,打断用户体验。在单页应用中,所有必要的代码(HTMLJavaScriptCSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面,页面在任何时间点都不会重新加载,也不会将控制转移到其他页面

五、SPA和MPA的区别

MPA(多页应用)中,每个页面都是一个独立的主页面。当访问另一个页面时,都需要重新加载htmlcssjs文件

单页面应用(SPA)多页面应用(MPA)
组成一个主页面和多个页面片段多个主页面
刷新方式局部刷新整页刷新
url模式哈希模式 (Hash)历史模式 (History)
SEO(搜索引擎优化)难实现,可使用SSR方式改善容易实现
数据传递容易通过url、cookie、localStorage等传递
页面切换速度快,用户体验良好切换加载资源,速度慢,用户体验差
维护成本相对容易相对复杂

解释SEO和SSR

SEO: 利用搜索引擎的规则,提高网站在有关搜索引擎内的自然排名

SSR:服务器渲染

单页应用优缺点

优点

  • 具有桌面应用的即时性、网站的可移植性和可访问性

  • 用户体验好、快,内容的改变不需要重新加载整个页面

  • 良好的前后端分离,分工更明确

缺点

  • 不利于搜索引擎的抓取

  • 首次渲染速度相对较慢

六、实现一个SPA

步骤

1.显示当前界面,点击界面按钮,或浏览器回退/前进按钮触发 hash 变化

2.检测 hash 或者 pushstate 变化

3.以当前 hash 为索引,加载对应资源

4.等待资源加载完毕,隐藏之前的界面,执行回调

hash 模式

通过监听url中的hash来进行路由跳转

history 模式

借用 HTML5 history apiapi 提供了丰富的 router 相关属性

history 相关的api

history.pushState: 浏览器历史记录添加记录

history.replaceState: 修改浏览器历史纪录中当前记录

history.popState: 当 history 发生变化时触发

七、如何给SPA做SEO

SEO: 利用搜索引擎的规则,提高网站在有关搜索引擎内的自然排名

1.SSR服务端渲染

将组件或页面通过服务器生成html,再返回给浏览器

2.静态化

目前主流的静态化主要有两种:

(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面实际存在于服务器的硬盘中

(2)另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。

3.使用Phantomjs针对爬虫处理

通过Nginx配置,判断访问来源是否为爬虫,如果是,则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。

八、v-show与v-if

共同点

能控制元素在页面是否显示

  • 当表达式为true时,都会占据页面的位置

  • 当表达式都为false时,都不会占据页面位置

区别

控制手段不同

v-show控制显示与隐藏:为该元素添加display:nonedom元素仍存在

v-if控制显示与隐藏:是将dom元素整个添加或删除

编译过程不同

v-show只是基于css切换

v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件

编译条件不同

v-showfalse变为true的时候不会触发组件的生命周期

v-if是真正的条件渲染,它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。渲染条件为假时不做操作,直到为真才渲染。由false变为true的时候,触发组件的beforeCreatecreatedbeforeMountmounted钩子。由true变为false的时候触发组件的beforeDestorydestoryed方法

性能消耗不同

v-show有更高的初始渲染消耗

v-if有更高的切换消耗

使用场景不同

v-ifv-show 都能控制dom元素在页面的显示

v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)

如果需要频繁地切换,使用 v-show 较好

如果在运行时条件很少改变,使用 v-if 较好

九、v-for与v-if

1.v-for优先级比v-if

2.v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值时被渲染

3.v-for 指令基于一个数组来渲染一个列表,使用 item in items 形式的特殊语法,其中 items 是源数据数组或对象, item 是被迭代的数组元素

在使用 v-for 的时候,建议设置唯一的key值,便于diff算法进行优化

<a v-if="isShow"/>
​
<p v-for="item in items" :key="id"/>

一般不会同时使用两者,如果确实需要同时使用,则不编写在同一元素身上,可以在外层嵌套template标签,在template 层进行 v-if 判断,在内部进行 v-for 循环

<template v-if="isShow">
    <p v-for="item in items" :key="id"/>
</template>

如果 v-if 判断 出现在 v-for 循环 内部,可通过计算属性computed提前过滤掉那些不需要显示的项

computed: {
    items: function() {
      return this.list.filter(function (item) {
        return item.isShow
      })
    }
}

十、Vue实例挂载的过程

new Vue会调用_init_方法

  • 定义 $set$get$delete$watch 等方法

  • 定义 $on$off$emit$off等事件

  • 定义 _update$forceUpdate$destroy生命周期

  • 调用$mount进行页面的挂载

  • 挂载的时候主要是通过mountComponent方法

  • 定义updateComponent更新函数

  • 执行render生成虚拟DOM

  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

十一、Vue的生命周期

生命周期是什么

Vue中,实例从创建到销毁的过程就是生命周期(创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等过程)

使用场景

生命周期描述
beforeCreate执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
created组件初始化完毕,各种数据可以使用,常用于异步数据获取
beforeMount未执行渲染、更新,dom未创建
mounted初始化结束,dom已创建,可用于获取访问数据和dom元素
beforeUpdate更新前,可用于获取更新前各种状态
updated更新后,所有状态已是最新
beforeDestroy销毁前,可用于一些定时器或订阅的取消
destroyed组件已销毁,作用同上组件实例完成销毁
activatedkeep-alive 缓存的组件激活时
deactivatedkeep-alive 缓存的组件停用时
errorCaptured捕获来自子孙组件的错误时

为什么一般在created进行异步数据请求

在钩子函数 created、beforeMount、mounted 中都可以进行异步数据请求,因为在这三个钩子函数中,data 已经创建,可以将服务端返回的数据进行赋值。

但一般在 created 中调用异步请求,有以下优点:

· 能更快获取到服务端数据,减少页面加载时间,用户体验更好

· SSR 不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性

十二、如何解决SPA首屏加载速度慢

首屏时间是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间。此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容。

加载慢的原因

1.网络延时问题

2.资源文件体积是否过大

3.资源是否重复发送请求去加载

4.加载脚本时,渲染内容堵塞

SPA首屏优化方式

1.减小入口文件体积

通过路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加

2.静态资源本地缓存

前端合理利用localStorage

后端返回资源问题:

  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头

  • 采用Service Worker离线缓存

3.UI框架按需加载

不要直接引用整个UI库

import ElementUI from 'element-ui'

而是按需引用,减小文件缓存体积

import { TableColumn, MessageBox } from 'element-ui'

4.组件重复打包

假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载

解决方案:在webpackconfig文件中,修改CommonsChunkPlugin的配置

minChunks: 3 // 表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件

5.图片资源的压缩

6.开启GZip压缩

拆完包之后,用gzip做一下压缩,安装compression-webpack-plugin

cnmp i compression-webpack-plugin -D

vue.congig.js中引入并修改webpack配置

const CompressionPlugin = require('compression-webpack-plugin')
​
configureWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') {
            // 为生产环境修改配置
            config.mode = 'production'
            return {
                plugins: [new CompressionPlugin({
                    test: /\.js$|\.html$|\.css/, //匹配文件名
                    threshold: 10240, //对超过10k的数据进行压缩
                    deleteOriginalAssets: false //是否删除原文件
                })]
            }
        }

在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip,就发送给它gzip格式的文件 我的服务器是用express框架搭建的 只要安装一下compression就能使用

const compression = require('compression')
app.use(compression())  // 在其他中间件使用之前调用

7.使用SSR

SSR(服务端渲染),组件或页面通过服务器生成html字符串,再发送到浏览器。使用Nuxt.js实现服务端渲染

十三、为什么data属性是一个函数而不是一个对象

根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况

组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象

十四、动态给vue的data添加一个新的属性

定义一个p标签用于显示data属性中的数据,点击按钮实现增加新数据的效果(同时改变vue中的data属性以及UI界面的显示)

<p v-for="(value,key) in item" :key="key">
    {{ value }}
</p>
<button @click="addProperty">动态添加新属性</button>

我们在vue实例中,定义data属性和methods方法,然后通过绑定点击事件输出data中的数据,点击按钮,数据更新了(console打印出了新属性),但页面没有更新,仍然只显示 ‘旧属性’

const app = new Vue({
    el:"#app",
   	data:()=>{
       	item:{
            oldProperty:"旧属性"
        }
    },
    methods:{
        addProperty(){
            this.items.newProperty = "新属性"  // 为items添加新属性
            console.log(this.items)  // 输出带有newProperty的items
        }
    }
})

在vue2中,因为初始设置的数据可以触发settergetter,从而变成响应式数据,而按钮点击新增的数据并不会通过Object.defineProperty 变成响应式数据,不会自动触发settergetter

解决方案

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()

  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象

vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式

十五、Vue中的组件和插件

Vue 核心特性中介绍了组件的定义,而插件是用来为 Vue 添加全局功能

插件的功能范围没有严格的限制——一般有下面几种:

  • 添加全局方法或者属性。如: vue-custom-element

  • 添加全局资源:指令/过滤器/过渡等。如 vue-touch

  • 通过全局混入来添加一些组件选项。如vue-router

  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。

  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如vue-router

区别:

编写形式不同

每一个.vue文件都可以看成是一个组件

`vue`文件标准格式
<template></template>

<script>
export default{ 
    ...
}
</script>

<style></style>

还可以通过template属性来编写组件,如果组件内容多,可以在外部定义template组件内容

<template id="testComponent">     // 组件显示的内容
    <div>component!</div>   
</template>

Vue.component('componentA',{ 
    template: '#testComponent'  
    template: `<div>component</div>`  // 组件内容少可以通过这种形式
})

vue插件的实现应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

注册形式不同

vue组件注册主要分为全局注册与局部注册

全局注册通过Vue.component方法,第一个参数为组件的名称,第二个参数为传入的配置项

Vue.component('my-component', { /* ... */ })

局部注册只需在用到的地方通过components属性注册一个组件

const component1 = {...} // 定义一个组件

export default {
	components:{
		component1   // 局部注册
	}
}

插件的注册通过Vue.use()的方式进行注册,第一个参数为插件的名字,第二个参数是可选择的配置项

Vue.use('my-plugin', { /* ... */} )

注册插件的时候,需要在调用 new Vue() 启动应用之前完成

Vue.use会自动阻止多次注册相同插件,只会注册一次

使用场景不同

组件 (Component) 是用来构成 App 的业务模块,它的目标是 App.vue

插件 (Plugin) 是对Vue的功能的增强或补充,它的目标是 Vue 本身,

十六、组件间通信

每个组件之间的都有独自的作用域,组件间通信是为了组件间数据共享

组件间通信的分类

父子组件之间的通信

兄弟组件之间的通信

祖孙与后代组件之间的通信

非关系组件间之间的通信

通信方式

父子关系的组件数据传递选择 props$emit进行传递,也可选择ref

props传递数据(父组件传递数据给子组件)

子组件设置props属性,定义接收父组件传递过来的参数

// Children.vue
props:{  
    // 字符串形式  
    name:String, // 接收的类型参数  
    // 对象形式  
    age:{    
        type:Number, // 接收的类型为数值  
        defaule:18,  // 默认值为18  
        require:true // age属性必须传递  
    }  
}

父组件在使用子组件标签中通过字面量来传递值

// Father.vue
<Children :name="jack" :age=18/>  

$emit 触发自定义事件(子组件传递数据给父组件)

子组件通过$emit触发自定义事件,$emit第二个参数为传递的数值

// Children.vue
this.$emit('add', good)

父组件绑定监听器获取到子组件传递过来的参数

// Father.vue
<Children @add="cartAdd($event)" />  

ref(子组件传递数据给父组件)

父组件在使用子组件的时候设置ref,父组件通过设置子组件ref来获取数据

// Father.vue
<Children ref="foo" />  
// Children.vue
this.$refs.foo  // 获取子组件实例,通过子组件实例能拿到对应的数据  

兄弟关系的组件数据传递可选择$bus,或者选择$parent进行传递

EventBus(兄弟组件传值)

创建一个全局事件总线EventBus

// 创建一个全局事件总线  
class Bus {  
  constructor() {  
    this.callbacks = {};   // 存放事件的名字  
  }  
  $on(name, fn) {  
    this.callbacks[name] = this.callbacks[name] || [];  
    this.callbacks[name].push(fn);  
  }  
  $emit(name, args) {  
    if (this.callbacks[name]) {  
      this.callbacks[name].forEach((cb) => cb(args));  
    }  
  }  
}  
  
// main.js  
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  
// 另一种方式  
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能

兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数值

// Brother1.vue
this.$bus.$emit('foo') 

另一个兄弟组件通过$on监听自定义事件

// Brother2.vue
this.$bus.$on('foo', this.handle) 

共同祖辈$parent或者$root(兄弟组件传值)

// Brother1.vue
this.$parent.on('add',this.add)
// Brother2.vue
this.$parent.emit('add')

祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject

$attrs 与$ listeners(祖先传递数据给子孙)

设置批量向下传属性$attrs$listeners

// child:并未在props中声明foo  
<p>{{$attrs.foo}}</p>  
  
// parent  
<HelloWorld foo="foo"/>  

包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外),通过 v-bind="$attrs" 传⼊内部组件

// 给Grandson隔代传值,communication/index.vue  
<Child2 msg="lalala" @some-event="onSomeEvent"></Child2>  
  
// Child2做展开  
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>  
  
// Grandson使⽤  
<div @click="$emit('some-event', 'msg from grandson')">  
	{{msg}}  
</div> 

provide 与 inject(祖先传递数据给子孙)

在祖先组件定义provide属性,返回传递的值

// GrandParent
provide(){  
    return {  
        foo:'foo'  
    }  
} 

在后代组件通过inject接收组件传递过来的值

// Grandson
inject:['foo'] // 获取到祖先组件传递过来的值

vuex存放共享的变量的容器(复杂关系的组件数据传递)

1.state:存放数据

2.mutations:存放操作数据的方法

3.actions:异步操作(也会有同步操作),actions不能直接修改state数据,而是通过提交mutations去更新state中数据

4.getters:存放基于state计算出来的一些值,类似与vue.js中的计算属性computed,当状态值发生改变是才会调用它的方法

5.modules:模块化管理,当需要管理的状态繁多时,可采用模块化管理,更易维护和阅读。

注意:如果采用模块化管理,默认mutations,actions,getters是注册到全局的,一般会开启命名空间:namespaced:true

大致流程:

客户端经过某种操作去更改了state中的状态值,然后把新的状态通知给组件,同时组件也可通过异步操作去发起状态更改,这个异步操作中可以是请求数据也可以是其他异步程序。

十七、NextTick

NextTick是什么

是一种优化策略。在下次 DOM 更新循环结束之后,执行延迟回调。即修改数据和更新数据是异步执行,在修改数据之后立即使用这个方法,获取更新后的 DOM

为什么要用NextTick

如果不适用 nextTick 更新机制,那么 num 每次更新值,都会触发视图更新

{{num}}
for(let i=0; i<100000; i++){
    num = i
}

有了nextTick机制,只需要等更新循环结束之后,更新一次

怎么用NextTick

在修改数据后,想立刻得到更新后的DOM结构,可以使用Vue.nextTick()

第一个参数为:回调函数(可以获取最近的DOM结构)

第二个参数为:执行函数上下文

// 修改数据
vm.message = '修改后的值'
// DOM 还没有更新
console.log(vm.$el.textContent) // 原始的值
Vue.nextTick(function () {
  // DOM 更新了
  console.log(vm.$el.textContent) // 修改后的值
})

组件内使用 vm.$nextTick() 实例方法只需要通过this.$nextTick(),并且回调函数中的 this 将自动绑定到当前的 Vue 实例上

this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
this.$nextTick(function () {
    console.log(this.$el.textContent) // => '修改后的值'
})

$nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情

this.message = '修改后的值'
console.log(this.$el.textContent) // => '原始的值'
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的值'

十八、vue中的mixin

mixin是什么

mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能,本质是一个js对象,可以包含组件中任意功能选项,如datacomponents等,将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时,所有mixins对象的选项都将被混入该组件本身的选项中来

minin的分类

局部混入

定义一个mixin对象

var Mixin = {
  methods: {
    hello: function () {
      console.log('hello mixin!')
    }
  }
}

组件通过mixins属性调用mixin对象

Vue.component('componentA',{
  	mixins: [Mixin]
})

该组件在使用的时候,混合了mixin里面的方法,执行hello方法

全局混入

通过Vue.mixin()进行全局的混入,常用于插件的编写

Vue.mixin({
  	created: function () {
      console.log("全局混入")
    }
})

使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)

什么时候用mixin

当组件A和组件B都要实现hello这个function时,为了减少代码重复,通过Vuemixin功能hello这个方法提出来,在两个组件中都调用mixin即可

const hello = {
  methods: {
    Hello() {
      console.log('hello!')
    }
  }
}
const A = {
  template: '#A',
  mixins: [hello]
};
 
const B = {
  template: '#B',
  mixins: [hello]
}

参考文献:面试官:SSR解决了什么问题?有做过SSR吗?你是怎么做的? | web前端面试 - 面试官系列 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

低保和光头哪个先来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值