vue 2.x 版本 基础知识点

  • Post author:
  • Post category:vue



Vue

是基于

MVVM

设计模式

是单页面应用



引入Vue代码

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>



将标签绑定为Vue



el

指定一个选择器, 代表该元素中使用vue来渲染

let app = new Vue({
    el:"#app"
})//app为标签id

el支持class(以点)、标签名方式



在标签里显示数据用双大括号


Mustache

语法糖

<div class="#app">{{content}}</div>
<script> new Vue({
        el:"#app",
        data:{
            content:"内容"
        }
    })</script>

双大括号里面可以写表达式

{{true?name:age}}



Vue里的数据



data

一般数据来源后端(通过ajax请求)

var app=new Vue({
    el:"#app",
    data:{
        content:"内容"
    }
})

data里的数据可以与标签进行绑定

<div id="app">{{content}}</div>

建议写函数形式

用脚手架需要

let app = new Vue({
    el:"#app",
    data(){
        return{
            //数据
        }
    }
})



methods事件


里面存放的都是函数事件

var app=new Vue({
    el:"#app",
    data:{
        content:""
    },
    methods:{
        foo:function(){//}
    }
})


methods事件

里面通过

this

可以访问到data里面的数据也可以访问到methods

let app=new Vue({
    el:"#app",
    data:{
        content:"内容"
    },
    methods:{
        foo:function(){
            this.content = "更改后的内容"
        }
    }
})



filters过滤

差值符号里面加上

竖线

就是进行数据的判断

二次渲染

<p v-for="item in arr" :key:"item.name">
    {{item.name | goudan}}
</p>
<script>
	let app=new Vue({
    el:"#app",
    data:{
        arr:"数据"
    },
    filters:{
        goudan(v){//这个v就是差值符号里面的item
            return  "加的东西"+v //过滤后的数据
        }
    }
})
</script>



computed 简化差值符号

不能先定义在data里

将数据进行一次处理

<div id="app">
    <p>{{funl}}</p>
</div>
<script>
	let app=new Vue({
    el:"#app",
    data:{
        arr:"数据"
        ,o:"bb"
    },
    computed:{
        funl(){
            return this.arr+"."+this.o
        }
    }
})
</script>



可以写set和get

computed:{
    msg:{
        get(){
        return 2
        }
        ,set(){
            this.lie ++
        }
    }
    
}



监听数据的变化watch

要先在data里面定义

监听的数据发生变化之后这个函数会触发

data:{
    a:10
},
watch:{
    a(newVal,oldVale){//两个参数一个新值 一个旧值
        console.log(1)
    }
}



Vue的标签方式



v-text

<div id="app" v-text="content"></div>
let app=new Vue({
    el:"#app",
    data:{
        content:"内容"
    }
})

不能解析标签名,可以用字符串拼接

无论内容是什么只会解析为文本

可以用模板字符串

<div id="app" v-text=`${name}`></div>



v-cloak

当浏览器加载的数据非常庞大

加载的一瞬间会出现双大括号的差值表达式

解决这种 闪现 的现象

在需要隐藏的地方添加

v-cloak


然后再

css

里面先隐藏这个标签

<div id="app" v-cloak></div>
<!-- 添加 v-cloak属性 -->
[v-cloak]{
/* css里面不进行显示 */
	display:none;
}

原理就是在页面加载的时候先不进行渲染



Vue

挂在完成之后内部进行渲染



v-html

<div id="app" v-html="content"></div>
let app=new Vue({
    el:"#app",
    data:{
        content:"<a helf="">内容</a>"
    }
})

设置元素的innerHTML

内容有html结构会被解析为标签



v-on事件/@

v-on:事件名=“方法”

<input type="button" value="事件绑定" v-on:click="foo">
<input type="button" value="事件绑定" v-on:mouseenter="foo">
<input type="button" value="事件绑定" v-on:dblclick="foo">
<input type="button" value="事件绑定" v-on:click="foo">
let app=new Vue({
    el:"#app",
    data:{
        content:"1234"
    },
    methods:{
        foo:function(){}
    }
})

methods里面写事件

v-on可以简写为@



自定义参数和事件修饰符

在方法里面

传入参数


v-on:click=“foo(x)”

let app=new Vue({
    el:"#app",
    methods:{
        foo:function(x){}//这里接受参数
    }
})



键盘按下事件

@keydown.enter=“方法”

enter回车键

<div @keyup.k="方法($event)"></div><!--按K键触发事件-->

可以串联修饰

<div @keyup.k.enter.space="方法"></div>



@click.once只触发一次



只有左键触发@click.left



只有右键触发@click.right



只有中键触发@click.middle



阻止冒泡

<div @click="方法">
    <button @click.stop="方法"></button>
</div>



阻止默认事件



@click.right.prevent
<div @click.right.prevent="方法"></div>
<a href="地址"  @click.right.prevent="方法"></a><!--阻止a标签跳转-->

传入两个事件需要传对象

<div v-on="{click:方法,mouseenter:方法}"></div>



接触事件

变相取消

<div @click="ifClick && clickAdd()"></div>
<!--判断ifClick的值决定执不执行clickAdd-->



v-model

设置和获取

表单元素

的值(双向数据绑定)

在表单元素里面添加v-model指令在指令里面添加data里面的数据

<div id="app">
	<input type="text" v-model="content">
</div>
<script>
	let app=new Vue({
        el:"#app",
        data:{
            content:"绑定的数据"
        }
    })
</script>

无论是修改表单元素里的值还是数据里的值都会互相更改



v-show

根据表达式的真假来显示或者隐藏

true表示显示

false表示隐藏

<img v-show="true"><!--显示-->
<img v-show="12<11"><!--隐藏-->

原理是修改元素的

display

实现显示隐藏

指令后面的内容最终都会解析为布尔值

数据改变之后,对应元素的显示状态会同步更新



v-if

根据表达式的真假来显示或者隐藏操作的是dom元素

直接删除dom元素

性能消耗比较大

如果频繁切换用v-show

为false时从dom树中移出



v-else


配合v-if

使用

如果if是false ,else就是显示

必须要

紧跟

v-if使用中间不能有东西



v-else-if

根据上一个v-if进行再次判断



v-bind设置元素的属性

v-bind:属性名=表达式

可以用冒号简写:

<img v-bind:src="imgSrc">
<img :src="imgSrc">
<img :class="imgClass?类名:''"><!--判断imgClass的值如果为假类名为空-->
<img :class="{类名:imgClass}">
<script>
	var app=new Vue({
        el:"#app",
        data:{
            imgSrc:"地址",
            imgClass:false
        }
    })
</script>



更改class操作

<div :class="{red:ifRed}"></div>判断ifRed的值来确定class添不添加red值
ifRed是真class就有red值
<div :class="[
             {'red':ifRed},
             'green'
             ]"></div>数组里面必须有引号不加引号就会去data里面找



style操作

<div :style="{font-size:12px}"></div>



v-for生成列表结构

根据数据生成列表结构

<ul>
    <li v-for="(item,index) in arr"> {{index}} {{item}}</li>
</ul>
<h3 v-for="item in obj"> {{item.name}} </h3>
<div v-for="(value,key,index) in o">value{{value}}key{{key}}index{{index}}</div>value是键key是值index是序号
<script>
	var app=new Vue({
        el:"#app",
        data:{
            arr:[1,2,3,4,5],
            obj:[
                {name:"张三"},
                {name:"李四"},
                {name:"王五"}
            ],
            o:{user:"名字",age:18,sex:"男"}
        }
    })
</script>

li标签里item是数组的每一项 arr是数组本身

数组的长度的更新会同步到页面上去,是响应式的



v-key顺序绑定

根v-for一块使用

<div v-for="item in arr" :key="item"></div>生成的每个div与item绑定
数组在后期进行打乱那么div也会随着数据进行移动

如果数据是对象数组就选取对象里面独一无二的属性



v-once渲染单次



v-pre不渲染



全局API



自定义指令directive

Vue.directive("goudan",(node,data)=>{//节点,数据
    //里面进行的操作
})
//或者
Vue.directive('dachui',{
    bind(){
        //指令第一次绑定时调用的函数
    },
    inserted(){
        //当被绑定的元素插入DOM中时
    },
    update(){
        //数据进行更改时
    },
    componentUpdate(){
        //
    },
    upbind(){
        //解绑
    }
})

标签里面添加v-goudan

指令里面传一个名字和函数

函数里面有两个参数

绑定的节点 , 带来的数据




extend

构造器创建子类

const goudan = Vue.extend({
    template:'<div>{{name}}</div>',
    data(){
        return{
            name:'dachui'
        }
    }
})
//挂载到#cuihua里
new goudan().$mount('#cuihua')



Vue.set

在Vue里面操作数据

数组没办法改

其中

一个

或几个值(除了更改全部或重新赋值)

只要是数组就不行对象可以

改变数组里的其中一项

methods:{
    fn(){
        Vue.set(this.arr,0,"goudan")//把arr的第0项改成goudan
    }
}



this.$set改变数组里的其中一项

methods:{
    fn(){
        this.$set(this.arr,0,"goudan")//把arr的第0项改成goudan
    }
}



this.$delete删除数组特定的某一项

this.$delete(this.arr,1)//删除下标是1的值

或者用JS splice

this.arr.splice(1,1)



DOM更新之后进行操作

nextTick

DOM进行更新之后进行操作

但是你写在那个方法里面那个才触发

this.$nextTick(function(){
    console.log("DOM更新了")
})



查看版本 Vue.version

console.log(Vue.version)



组件

放在new Vue之前定义组件

组件中间不要有任何的东西



component全局组件

全局组件可以被任意组件所调用的

Vue.component(
	//组件名 , 在html里面就是用这个标签来渲染 标签里面不写东西
    "goudan",//还必须一些选项参数
    {
        //对应要生成的DOM结构
        template:`<p>内容</p>`
        ,data(){//data必须是函数
            return {
                msg:""
                ,num ++
            }
        }
        ,methods:{
            add(){
                this.num ++
            }
        }
    }
)



template 生成DOM结构

生成对应的结构

但是结构只能有一个跟标签

起的名字不能与原有的标签名重复

驼峰命名的时候在标签里面用横杠显示-


但是在组件里面不是

单词第一个大写

Vue.component("goudan",{
    template:`<goudan>123456</goudan>`
})
<div>
    <goudan></goudan>
</div>



子组件

在实例里面定义



components

属性就是

组件名


根组件

可以

调用全局组件

全局组件

不能

调用根组件



template

new Vue({
    el:"#app",
    components(){
        aaa:{//首先是用什么标签包起来
            template:`<div><p>组件</p></div>`
        }
    }
})


在标签里面引入子组件


is

component标签是vue自带的

<component is="aaa"></component>
<component :is="x"></component>
<script>
	new Vue({
        el:"#app",
        data:{
            x:"aaa"
        },
        components:{
            aaa:{
                template:`<div>这是组件</div>`
            }
        }
    })
</script>


template vue标签


不能写在

绑定

Vue实例的标签

里面

<template id="f"></template>
<script>
new Vue({
    el:"#app",
    components:{
        aaa:{
            template:"#f"//和标签进行绑定
        }
    }
})
</script>


组件之间的传值


父组件给子组件传值

父组件传过去的值改变 子组件也改变

<div id="app">
   <aaaa :goudan="arr"></aaaa>
</div>

通过绑定标签属性传值

子组件接受



props 子组件接受
componente:{
    aaaa:{
        template:"#aaa"
        ,data(){
            return {}
        },
        props:["goudan"]//这里数组方式接受数据
    }
}


接受可以用对象接受
componente:{
    aaa:{
        temolate:"#aaa"
        ,data(){
            return {}
        }
        ,prop:{//对象非常像mongodb里面的表
            goudan:Array
            ,m:{
                type:String
                ,default:"这是默认值"
            }
            ,o:{
                type:String
                ,required:true//这个值是必须的
            }
        }
    }
}


子组件给父组件传值

$emit

事件里的自定义事件传值

设置的

自定义事件

写在组件的标签上面

不建议使用驼峰命名,建议

短横线

命名

-

父组件里的子组件标签
<aaa @goudan="fun"></aaa>fun在父组件里写函数 ,父组件函数接受值
<script>
methods:{
    fun(data){
       //data就是传来的参数
    }
}
</script>
/*-------------下面是子组件里进行事件发射*/
<script>
methods:{
    dachui(){
       this.$emit("goudan",this.msg)//自定义事件名 ,传过来的数据
    }
}
</script>



绑定组件ref

<h1 ref="goudan"></h1>



Vue

实例里面使用

this.$refs

获取此节点

如果在组件里使用

ref

那么

this.$refs

返回的是这个组件的实例方法



依赖注入

相当于组件的子组件传值个父组件

Vue.component("goudan",{
    template:"#goudan",
    data(){
        return{}
    },
    provide(){
        //发送数据
        return{
            goudan:'11'
        }
    }
})
Vue.component("dachui",{//另一个组件
    template:"#dachui",
    data(){
        return{}
    },
    inject:['goudan']//接受
})

耦合

改的方法更麻烦一点

数据传过来的是非响应式的



Axios

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

引入axios的地址

必须

先导入再使用


使用get和set方法

axios.get("地址")//如果要查询具体的数值后面加上问号在添加属性名,等号属性值
    .then((response)=>{
    //请求数据成功的函数
    console.log(response)
    //response就是后端返回的数据
	})
    .catch(err=>{
    //请求数据失败的函数
	})

不同的地址获取的数据不同查询的方法不同

如果是表单元素(post)要在地址后面加逗号,在用对象的方式输入数值

    axios.post("地址",{username:"名称"})
    .then((response)=>{
        //请求数据成功的函数
    }).catch((err)=>{
        //请求数据失败的函数
    })



组件里面调用

下载

axios




npm i axios -S



Axios

单独放一个

js

文件里

import axios from "axios";
//配置默认的参数
axios.defaults.baseURL = 'http://localhost:3000';//默认访问地址
axios.defaults.withCredentials = true;//跨域允许携带cookie
axios.defaults.headers.post["Content-Type"] = "application/x-www-from-urlencoded";//设置post请求格式

使用

axios


一种是全部引用

将所有的接口都引用

export default axios

使用

import request from '../api/index'
request.post("/articleInfo").then(data=>{console.log(data);}//articleInfo是地址
//post是请求方法

另一种方法是按需引用

导出的时候导出一个对象

export default {
  goudan(){//标签
    return axios.post("/articleInfo")//articleInfo是地址post是请求方法
  }
};

使用

不支持解构

import axios from '../../api';
let goudan = axios.goudan;
created() {
      goudan()
          .then(res=>{
            res.data.data;//data.data就是返回的数据
          })
    }

支持解构

export function goudan(){
    return axios.get("地址")
}

使用

import {goudan} from '../../api';



new vue

new vue返回一个实例化后的东西

let vm = new Vue({
    el:"#app",
    data:{
        content:"123"
    }
})
//

created渲染DOM之前执行函数



使用JQuery

npm i jquery -S//安装jquery

再导入

import $ from 'jquery'
//原型里添加$方法
Vue.prototype.$ = $



引入

css

文件

@import url("地址")



引入

js

文件

import "地址"//不用命名



脚手架


npm i webpack -g



npm i webpack-cli -g



建议用cmd启动脚手架

npm i @vue/cli -g

检测版本

vue -V//斜杠后面是大写的v



创建

vue create 项目名 //创建项目文件

多选 选择是空格

或者是

vue ui

一个ui界面

在js里面导入

import "名字" from "地址"

导出

export default "导出的东西"



启动项目

npm run sever



打包项目

npm run build



打包注意事项

如果打包后放到路由的子路径

需要注意

在根目录创建

vue.config.js

文件

写上如下代码

module.exports = {
    publicPath:'./'
}

如果安装了前端路由请参照

https://www.cnblogs.com/zsg88/articles/12557862.html 进行更改



插槽

在你的组件写上slot标签

他不是一个正常的html标签所以插槽本身不会渲染

<slot></slot>

使用插槽之后 组件中间可以写东西了

顺序按照设置的属性

在插槽之后用差值符号数据就是App的数据/根组件的数据

如果同个插槽显示不同内容 给插槽起名字

<slot name="defalut"></slot>默认是defalut
<slot name="goudan"></slot>
<slot name="dachui"></slot>

显示指定插槽内容用

template

标签



v-slot/#

简写是#号

<template v-slot:goudan>
	<p>我是第一段内容</p>
</template>
<template #dachui>
	<p>我是第二段内容</p>
</template>

可以用于传值

<slot :goudan="msg" :dachu="gg" :name="goudan"></slot>
/使用
<template v-slot:goudan="{msg,gg}">
	<p>我是第一段内容</p>
</template>
///或者是
<template #goudan="{msg,gg}">
	<p>我是第一段内容</p>
</template>



生命周期函数

一个组件的生命周期里做的一些事情



beforeCreate() 实例化之后 观测数据之前



created()开始检测数据



beforeMount()在页面即将渲染之前

在渲染和实例挂载之间 去调用子组件 完成子组件的生命周期



mounted()实例被挂载之后



beforeUpdate()数据更新之前



update()数据更新之后



activated()路由激活

<keyy-alive>
	将组建写在这里 保存到缓存里面
    节省性能
</keyy-alive>



deactivated()注销

上面两个需要

keyy-alive

标签才会触发



beforeDestroy()消失之前

组件消失



destroyed()消失之后

组件重新显示



UI组件

下载

引入

//全局
impot {Button} from "Vant"
Vue.use(Button)
//局部 局部需要下载插件
impot {Button} from "Vant"
export default {
    components:{
        [Button.name]:Button//用它组件里面的name
    }
}
Vue add element-ui



前端路由

需要Router

有两种

一个是默认的hash路由

一个是history

默认的路由地址上多一个#号

目的是判断是否是前端路由

history路由

路径就没有#号

但是他的路由容易跟后端冲突

比如 前端有一个/adou路由 后端有一个/adou路由 一旦刷新页面就出错

后端需要设置get路由返回同一个

HTML

文件

切换模式就是更改

router

下面的

index

文件

const router = new VueRouter({
    mode:"history",//默认是hash路由 ,就是带#号的
  	routes
});

export default router



配置路由

先引用

在router文件里面的index引用你写的页面

router = [
    path:"/",
    name:"Home",
    component:import("页面文件")
]

404页面就是星号 找不到路由

path:"*",
name:"404"

然后在页面里面引入标签

to就是你跳转的链接

<router-link to="/goudan">跳转</router-link>跳转的标签
<router-view/>跳转时渲染的地方



编程式路由 $router



push go replace

this.$router.push("/")//添加一个当前的新历史 浏览记录
this.$router.go(-1)//当前页面向前访问 正数是向后
this.$router.replace("/")//替换当前浏览的记录




push

的操作

//可以传对象
this.$router.push({path:'/foo1',query:{name:'goudan'}})//query是参数



子路由children

在配置路由的地方需要的地方写上属性

children

是一个数组

[
    {
        path:"/",
        name:"Home",
        component:""
    },{
        path:"/about",
        name:"about",
        component:"",
        children:[//拥有子路由
            {
                path:"all",
                name:"All",
                component:"文件路径"
            }
        ]
    }
]

渲染就跟原来的一样


但是

在写router-link标签的时候注意

里面的to属性浏览器里的地址怎么写你就怎么写

<router-link to="/about/all"></router-link>

如果想默认就出现一个子路由

他的路由里面的path就为空

path:""

在router-link标签里面的to可以进行筛选路由

<router-link :to={name:"All"}></router-link>
找到路由里面name是All的路由

但是路由的命名时会冲突的



别名

路由里面path有一个

别名


他既可以有这个名字又可以有另一个名字

{
    path:"",//在子路由里path不写意思指默认显示这个页面
    alias:"all",
    name:"All",
    component:()=>import("路径")
}



路由传参$ruote

参数在

query

里面

mounted(){
    this.$route.query
}



动态路由params

在路由里面设置

{
    path:"about/:id"
}

请求链接的时候可以带上

about/2222

获取用params

mounted(){
    this.$route.params
}



路由是守卫

next括号里面可以写false



全局守卫



main.js里

面写



全局前置守卫


切换完成之前

监听前端所有

路由的变化


触发内部的

函数


只要路由切换就

触发

函数

router.beforeEach((to,from,next)=>{
    //to从哪那个路由来
    //from到那个路由去
    //next放行(相当于保安放行)
    router.path("/")
    next()
})



全局后置守卫


切换路由后


没有next因为你

己经进入了

那个页面

router.afterEach((to,from)=>{
    //to从哪那个路由来
    //from到那个路由去
})



局部守卫


在路由配置里面添加

在你想要

添加

的页面

在某个

单独

的页面

设置

守卫

beforeEnter

{
    path:"/about",
    beforeEnter(to,from,next){
        //to从哪那个路由来
        //from到那个路由去
    }
}



组件内的守卫

能够监听到

路由的变化



监听路由的进入

beforeRouteEnter

因为在

渲染之前

执行所以

监听不到this


需要使用到this的话在

next里写函数


但是注意每次进入到路由里会

重新渲染数据

data(){
    return{
        msg:0
    }
}
,beforeRouterEnter(to,from,next){
    next(vm=>{
        vm.msg++
    })
}



监听路由的变化

beforeRouteUpdate

路由传入的

参数

变化触发函数

data(){
    return{
        msg:0
    }
}
,beforeRouterUpdate(to,from,next){
    next(vm=>{
        vm.msg++
    })
}



监听路由离开

beforeRouteLeave

离开此页面触发

data(){
    return{
        msg:0
    }
}
,beforeRouterLeave(to,from,next){
    
}



路由切换动画

transition标签

<transition name="goudan">
	<router-view class='dachui'/>
</transition>

设定一个name在style里面写样式

<style>
    .dachui{
        transsition: 2s;
    }
    进入的状态
    .goudan-enter{
        进入的初始状态
        opacity:0;
    }
    .goudan-enter-active{
        进入动画过程中
    }
    .goudan-enter-to{
        进入的结束状态
        opacity:1;
    }
    离开的状态
    .goudan-leave{
        离开的初始状态
        opacity:1;
    }
    .goudan-leave-to{
        离开的结束状态
        opacity:0;
    }
</style>



Vuex

多了一个store文件

相当于一个

仓库,

存储变量

方便

组件之间

进行

通讯



state: 状态

组件都能使用的一些数据

使用方法:

<h2>
    {{$store.state.num}}
</h2>
<script>
	export default {
        name:"Home",
        methods:{
            fn(){//不要这么写 仓库只能使用不能更改 
                //需要更改就写 方法
                this.$store.state.num ++
            }
        }
    }
</script>

但是一般这个使用方法名字太长了

可以使用

computed

方法

computed:{
    num(){
        return this.$store.state.num
    }
}

这样可以直接使用

{

{num}}

来书写

如果还是麻烦可以用

vuex里的mapState



mapState

他是一个

函数

需要执行

函数执行需要

传入参数

,传入你

需要的变量



数组

的形式传入

import {mapState} from 'Vuex';
export default {
    name:"Home",
    computed:mapState(['num','goudan'])
}

mpaState执行之后

返回一个对象

,对象

里面是你需要的数据

但是

每个数据

是一个函数


所以要写到

computed

里面

computed里面的

变量都是一个函数

形式

但是如果你要在computed里写自己的方法就不好写了

可以

使用

ES6的…

解构

computed:{
    dachui(){
        return 2+"";
    },
    ...mapState(['num','goudan'])
}



getters

相当于computed

getters:{
    goudan(state){
        return state.num +''//state是上面的vuex里的state
    }
}

getters和state的关系相当于computed和data的关系



mutations 定义方法

定义操作state的方法 (里面写同步方法)

mutations:{
    addN(state){
        state.num ++
    }
}



使用方法

$store.commit

<button @click='$store.commit("addN")'>按钮</button>

如果觉得名字太长可以调用

mapMutations


在mathods里面挂载

mathods:{
    ...mapMutations(['addN'])
}

使用:

<button @click='addN(2)'></button>
mutations:{
    addN(state,n){
        state.num += n
    }
}



actions 异步的方法

存放异步操作

actions:{
    addAsync(context){
        new Promise(req=>{
            setTimeout(req,1000);
        }).then(()=>{
            context.commit('addN')
        })
    }
}


$store.dispath

方法小范围使用


...mapActions([])

进行使用



modules 模块

将一些状态模块化

将一些定义的变量和相关的额方法写到一个对象里面

const num = {
    namespaced:true,
    state:{
        num:1
    },
    mutations:{},
    actions:{}
}

namespaced创建一个

命名空间


可以用一个变量名去接受他

然后添加到

modules

里面

modules:{
    num:num
}

在导入数据的时候加上名字


mapState('num',['num'])



APP里路由的变化监听

watch:{
    $route(){//watch监听
        //监听route的变化
    }
}



版权声明:本文为m0_62544473原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。