视图层
框架的视图层由WXML(WeiXin Markup language)与WXSS(WeiXin Style Sheet)编写,由组件来进行展示。对于微信小程序而言,视图层就是所有.wxml文件与.wxss文件的集合。
微信小程序在逻辑层将数据进行处理后发送给视图层展现出来,同时接受视图层的事件反馈。
■.wxml文件用于描述页面的结构。
■.wxss文件用于描述页面的样式。
视图层以给定的样式展现数据并将事件反馈给逻辑层,而数据展现是以组件来进行的。组件(Component)是视图的基本组成单元,是构建.wxml文件必不可少的。
对于小程序的WXML编码开发,基本上可认为就是使用组件、结合事件系统,构建页面结构的过程。.wxml文件中所绑定的数据,均来自于对应页面的.js文件中Page方法的data对象。
WXML
WXML是框架设计的一套类似HTML的标签语言,结合基础组件、事件系统,可以构建出页面的结构,即.wxml文件。
.wxml文件第一行建议写。比如logs页面的wxml文件(logs. wxml)如下:
<!--logs.wxml-->
<view class="container log-list">
<block wx:for="{{logs}}" wx:for-item="log">
<text class="log-item">{{index+1}}.{{log}}</text>
</block>
</view>
上述代码,我们通过组件控制页面内容展现,通过组件与组件实现页面数据的绑定;
WXML具有数据绑定、列表渲染、条件渲染、模板及事件绑定的能力:
1)数据绑定:
//wxml
<view>{{message}}</view>
//page.js
Page({
data:{
message:'hello MINA'
}
})
上例中,在.wxml文件中绑定message变量,在.js文件的data对象中给message赋值“hello MINA ”
2)列表渲染:
//wxml
<view wx:for="{{array}}">{{item}}</view>
//page.js
Page({
data:{
array:[1,2,3,4,5]
}
})
3)条件渲染:
//wxml
<view wx:if="{{view=='WEBVIEW'}}">WEBVIEW</view>
<view wx:elif="{{view=='APP'}}">APP</view>
<view wx:else="{{view=='MINA'}}">MINA</view>
//page.js
Page({
data:{
view:'MINA'
}
})
4)模板:
//wxml
<template name="staffName">
<view>FirstName:{{firstName}},LastName:{{lastName}}</view>
</template>
<template is="staffName" data="{{...staffA}}"></template>
<template is="staffName" data="{{...staffB}}"></template>
<template is="staffName" data="{{...staffC}}"></template>
//Pages.js
Page({
data:{
staffA:{firstName:'hh',lastName:'hhh'},
staffB:{firstName:'bb',lastName:'bbb'},
staffC:{firstName:'cc',lastName:'ccc'}
}
})
注:字例代码中,“...”为扩展运算符,用它来展开一个对象,如staffA对象。
5)事件绑定:
//wxml
<view bindtap="add">{{count}}</view>
//page.js
Page({
data:{
count:1
},
add:function(e){
this.setData({
count:this.data.count+1
})
}
})
1.数据绑定
.wxml文件中的动态数据均来自对应页面的.js文件中Page的data对象。
(1)简单绑定
数据绑定使用Mustache语法(即“双大括号”语法)将变量包起来,可以作用于:
1).内容:
<!--wxml-->
<view>{{content}}</view>
//page.js
Page({
data:{
content:'hello world'
}
})
2).组件属性(需要在双引号之内):
<!--wxml-->
<view id="item-{{id}}">{{content}}</view>
//page.js
Page({
data:{
content:'hello world',
id:0
}
})
3).控制属性(需要在双引号之内):
<!--wxml-->
<view wx:if="{{condition}}">{{content}}</view>
//page.js
Page({
data:{
content:'hello world',
condition:true
}
})
(2)运算
可以在{{}}内进行简单的运算,支持如下几种方式;
1).三元运算:
<!--wxml-->
<view hidden="{{flag?true:false}}">Hidden</view>
2).算数运算:
<!--wxml-->
<view>{{a+b}}+{{c}}+d</view>
//page.js
Page({
data:{
a:1,
b:2,
c:3
}
})
view中的内容为3+3+d
3).逻辑判断:
<view wx:if="{{length>5}}"></view>
4).字符串运算:
<!--wxml-->
<view>{{"hello"+name}}</view>
//page.js
Page({
data:{
name:'MINA'
}
})
5).数据路径运算:
<view>{{object.key}}{{array[0]}}</view>
Page({
data:{
object:{
key:'hello'
},
array:['world']
}
})
(3)组合
也可以在Mustache内直接进行组合,构成新的对象或者数组。
1).数组
<!--wxml-->
<view wx:for="{{[zero,1,2,3,4]}}">{{item}}</view>
//page.js
Page({
data:{
zero:0
}
})
最终组合成数组[0, 1, 2, 3, 4]。
2).对象
<!--wxml-->
<template is="objectCombine" data="{{for:a, bar:b}}"></template>
//page.js
Page({
data:{
a:1,
b:2
}
})
最终组合成的对象是{for: 1, bar: 2}
3).也可以用扩展运算符. ..来将一个对象展开:
<!--wxml-->
<template is="objectCombine" data="{{...obj1,...obj2,e:5}}"></template>
//page.js
Page({
data:{
obj1:{
a:1,
b:2
},
obj2:{
c:3,
d:4
}
}
})
最终组合成的对象是{a: 1, b: 2, c: 3, d: 4, e: 5}。
4).如果对象的key和value相同,也可以间接地表达:
<!--wxml-->
<template is="objectCombine" data="{{foo,bar}}"></template>
//page.js
Page({
data:{
foo:'my-foo',
bar:'my-bar'
}
})
最终组合成的对象是{foo: 'my-foo', bar:'my-bar'}。
5).注意:
上述方式可以随意组合,但是如果变量名相同,后边的对象会覆盖前面的对象。
<!--wxml-->
<template is="objectCombine" data="{{...obj1,...obj2,a,c:6}}"></template>
//page.js
Page({
data:{
obj1:{
a:1,
b:2
},
obj2:{
b:3,
c:4
},
a:5
}
})
最终组合成的对象是{a: 5, b: 3, c: 6}。
2.条件语句
条件语句可用于.wxml中进行条件渲染,不同的条件进行不同的渲染。
(1)wx:if
我们用wx:if="{{condition}}"来判断是否需要渲染该代码块。比如:
<view wx:if="{{condition}}">True</view>
也可以用wx:elif和wx:else来添加一个else块:
<view wx:if="{{length>5}}">1</view>
<view wx:elif="{{length>2}}">2</view>
<view wx:else>3</view>
(2)block wx:if
因为wx:if是一个控制属性,需要将它添加到一个组件标签上。如果想一次性判断多个组件标签,其实可以使用一个标签将多个组件包装起来,并在其上使用wx:if控制属性:
<block wx:if="{{true}}">
<view>view1<view>
<view>view2</view>
<block>
注意
并不是一个组件,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性。
(3)wx:if vs hidden
因为wx:if之中的模板也可能包含数据绑定,所以当wx:if的条件值切换时,框架有一个局部渲染的过程,从而确保条件块在切换时销毁或重新渲染。
同时wx:if也是惰性的,如果在初始渲染条件为false,框架什么也不做,在条件第一次变成真的时候才开始局部渲染。
相比之下,hidden就简单得多,组件始终会被渲染,只需简单地控制显示与隐藏。
一般来说,wx:if有更高的切换消耗,而hidden有更高的初始渲染消耗。因此,如果需要频繁切换的情景下,用hidden更好;如果运行时条件不大可能改变,则wx:if较好。
3.列表语句
列表语句可用于.wxml中进行列表渲染,将列表中的各项数据进行重复渲染。
(1)wx:for
在组件上使用wx:for控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。
默认数组当前项的下标变量名默认为index,数组当前项的变量名默认为item。
示例如下:
<view wx:for="{{items}}">{{index}}:{{item.message}}</view>
//page.js
Page({
data:{
items:[{message:'foo'},{message:'go'}]
}
})
使用wx:for-item可以指定数组当前元素的变量名。
而使用wx:for-index则可以指定数组当前下标的变量名:
<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">{{idx}}:{{itemName.message}}</view>
wx:for也可以嵌套,例如下边是一个九九乘法表:
<view wx:for="{{[1,2,3,4,5,6,7,8,9]}}" wx:for-item="I">
<view wx:for="{{[1,2,3,4,5,6,7,8,9]}}" wx:for-item="j">
<view wx:if="{{i<j}}">{{I}}*{{j}}={{i*j}}</view>
</view>
</view>
(2)block wx:for
类似于block wx:if,也可以将wx:for用在标签上,渲染一个包含多节点的结构块。
例如:
<block wx:for="{{[1,2,3]}}">
<view>{{index}}:</view>
<view>{{item}}</view>
</block>
(3)wx:key
如果列表中项目的位置会动态改变,或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如input标签中的输入内容,switch中的选中状态),需要使用wx:key来指定列表中项目的唯一标识符。
wx:key的值以两种形式提供:
■字符串,代表在for循环的array中item的某个property,该property的值需要是列表中唯一的字符串或数字,且不能动态改变。
■保留关键字*this代表在for循环中的item本身,这种表示需要item本身是一个唯一的字符串或者数字,例如:当数据改变触发渲染层重新渲染的时候,会校正带有key的组件,框架会确保它们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。
注意:
如不提供wx:key,会报错,如果明确知道该列表是静态,或者不必关注其顺序,可以选择忽略。
示例代码如下:
//wx-key-demo.wxml
<switch wx:for ="{{objectArray}}" wx:key="unique" style="display:block;">{{item:id}}</switch>
<button bindtap="switch">Switch</button>
<button bindtap="addToFront">Add to the front</button>
<switch wx:for="{{numberArray}}" wx:key="*this" style="display:block;">{{item}}</switch>
<button bindtap="addNumberToFront">Add to the front</button>
//wx-key-demo.js
Page({
data:{
objectArray:[{id:5,unique:'unique_5'},{id:4,unique:'unique_4'},
{id:3,unique:'unique_3'},{id:2,unique:'unique_2'},],
numberArray:[1,2,3,4]
},
switch:function(e){
const length = this.data.objectArray.length
for(let I=0;i<length;++i){
const x=Math.floor(Math.random()*length)
const y=Math.floor(Math.random()*length)
const temp = this.data.objectArray[x]
this.data.objectArray[x]=this.data.objectArray[y]
this.data.objectArray[y] = temp
}
this.setData({
objectArray:this.data.objectArray
})
},
addToFront: function(e) {
const length = this.data.objectArray.length
this.data.objectArray = [{id: length, unique: 'unique_' + length}].concat (this.data.objectArray)
this.setData({
objectArray: this.data.objectArray
})
},
addNumberToFront: function(e){
this.data.numberArray = [ this.data.numberArray.length + 1 ].concat(this. data.numberArray)
this.setData({
numberArray: this.data.numberArray
})
}
})
4.模板
WXML支持模板(template),可以在模板中定义代码片段,然后在不同的地方调用。
(1)定义模板
使用name属性,作为模板的名字。然后在<template/>内定义代码片段:
<template name="msgItem">
<view>
<text>{{index}}:{{msg}}</text>
<text>time:{{time}}</text>
</view>
</template>
(2)使用模板
使用is属性,声明需要使用的模板,然后将模板所需要的data传入,例如:
<!--wxml-->
<template is="msgItem" data="{{...item}}"/>
//page.js
Page({
data:{
item:{
index:0,
msg:'this is a template',
time:'2018-12-12'
}
}
})
is属性可以使用Mustache语法,来动态决定具体需要渲染哪个模板:
<template name="odd">
<view> odd </view>
</template>
<template name="even">
<view> even </view>
</template>
<block wx:for="{{[1, 2, 3, 4, 5]}}">
<template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
</block>
(3)模板的作用域
模板拥有自己的作用域,只能使用data传入的数据。
5.引用
WXML提供两种文件引用方式:import和include。
(1)import
import可以在该文件中使用目标文件定义的template,例如:在item.wxml中定义了一个叫item的template:
<! -- item.wxml -->
<template name="item">
<text>{{text}}</text>
</template>
在index.wxml中引用了item.wxml,就可以使用item模板:
<import src="item.wxml"/>
<template is="item" data="{{text: 'forbar'}}"/>
(2)import的作用域
import有作用域的概念,即只会引用目标文件中定义的template,而不会引用目标文件嵌套import的template。
例如:C import B, B import A,在C中可以使用B定义的template,在B中可以使用A定义的template,但是C不能使用A定义的template。如下所示:
<! -- A.wxml -->
<template name="A">
<text> A template </text>
</template>
<! -- B.wxml -->
<import src="a.wxml"/>
<template name="B">
<text> B template </text>
</template>
<! -- C.wxml -->
<import src="b.wxml"/>
<template is="A"/>
<! -- Error! Can not use tempalte when not import A. --> <template is="B"/>
(3)include
include可将目标文件除模板代码(<template/>)块的所有代码引入,相当于拷贝到include位置,例如:
<! -- index.wxml -->
<include src="header.wxml"/>
<view> body </view>
<include src="footer.wxml"/>
<! -- header.wxml -->
<view> header </view>
<! -- footer.wxml -->
<view> footer </view>
6.事件绑定
事件的定义如下:
■事件是视图层到逻辑层的通信方式。
■事件可以将用户的行为反馈到逻辑层进行处理。
■事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。
■事件对象可以携带额外信息,如id、dataset、touches。
(1)事件的使用
小程序与用户的交互多数情况下是通过事件来进行的。
首先,在组件中绑定一个事件处理函数。如下面代码中,我们使用bindtap,当用户点击该组件view的时候会在该页面对应的Page中找到相应的事件处理函数tapName:
// 指定view组件的唯一标识tapTest;自定义属性hi,其值为MINA;绑定事件tapName
<view id="tapTest" data-hi="MINA" bindtap="tapName"> Click me! </view>
注意
应将bindtap理解为:bind+tap,即绑定冒泡事件tap(手指触摸后离开)。
其次,要在相应的Page定义中写上相应的事件处理函数,参数是event。如下列示例代码中,定义了tapName函数,将事件信息输出到控制台上:
Page({
tapName: function(event) {
console.log(event)
}
})
(2)事件详解
微信小程序里的事件分为冒泡事件和非冒泡事件:
■冒泡事件:当一个组件上的事件被触发后,该事件会向父节点传递。
■非冒泡事件:当一个组件上的事件被触发后,该事件不会向父节点传递。WXML中的冒泡事件仅有6个,列表如下:
冒泡事件 | 含义或触发条件 |
---|---|
touchstart | 手指触摸 |
touchmove | 手指触摸后移动 |
touchcancel | 手指触摸动作被打断,如来电提醒、弹窗 |
touchend | 手指触摸动作结束 |
tap | 手指触摸后离开 |
longtap | 手指触摸后超过350ms再离开 |
注意
除上表之外的其他组件自定义事件都是非冒泡事件,如<form/>的submit事件、<input/>的input事件、<scroll-view/>的scroll事件。
事件绑定的写法同组件的属性,以key、value的形式,如下所示:
■key以bind或catch开头,然后跟上事件的类型,如bindtap、catchtouchstart。
■value是一个字符串,需要在对应的Page中定义同名的函数。不然当触发事件的时候会报错。
注意
bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡。
例子,点击id为inner的组件view会先后触发handleTap3和handle-Tap2(因为tap事件会冒泡到id为middle的组件view,而middle view阻止了tap事件冒泡,不再向父节点传递),点击middle view会触发handleTap2,点击id为outter的组件view会触发handleTap1:
<view id="outter" bindtap="handleTap1"> outer view
<view id="middle" catchtap="handleTap2"> middle view
<view id="inner" bindtap="handleTap3"> inner view </view> </view> </view>
如无特殊说明,当组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象。事件对象具有的属性参见表
事件对象的属性 | 类型 | 说明 |
---|---|---|
type | String | 事件类型 |
timeStamp | Integer | 事件生成的时间戳 |
target | Object | 触发事件组件的一些属性值集合 |
currentTarget | Object | 当前组件的一些属性值集合 |
touches | Array | 触摸事件,当前停留在屏幕中触摸点信息的数组 |
changedTouches | Array | 触摸事件,当前变化的触摸点信息的数组 |
detail | Object | 额外的信息 |
其中:
■type指通用事件类型。
■timeStamp是该页面打开到触发事件所经过的毫秒数。
■target触发事件的源组件,是一个对象,具有以下3个属性
源组件对象属性 | 说明 |
---|---|
id | 事件组件的id |
tagName | 事件组件的类型 |
dataset | 事件组件上由data-开头的自定义属性组成的集合 |
■currentTarget事件绑定的当前组件。与target类似,是一个对象,同样具有上表三个属性。(组件<canvas />中的触摸事件为特殊事件,不可冒泡,所以无currentTarget。)
说明:
1)target和currentTarget可以参考上例中,点击inner view时,handleTap3收到的事件对象target和currentTarget都是inner,而handleTap2收到的事件对象target就是inner, currentTarget就是middle。
2)dataset在组件中可以定义数据,这些数据将会通过事件传递给App Service。dataset书写方式以data-开头,多个单词由连字符“-”连接,不能有大写(大写会自动转成小写),如data-element-type,最终在event.target.dataset中会将连字符转成驼峰形式:elementType。
示例代码如下:
// bindviewtap.wxml
<view data-alpha-beta="1" data-alphaBeta="2" bindtap="bindViewTap"> DataSet Test </view>
// bindviewtap.js
Page({
bindViewTap:function(event){
event.target.dataset.alphaBeta == 1 //- 会转为驼峰写法
event.target.dataset.alphabeta == 2 //大写会转为小写
}
})
■touches是一个触摸点的数组。每个元素为一个Touch(触摸点)对象,具有以下属性:
■changedTouches数据格式同touches。表示有变化的触摸点,如从无变有(touch-start)、位置变化(touchmove)、从有变无(touchend、touchcancel)。
WXSS
WXSS是一套样式语言,用于描述WXML的组件样式。它将决定WXML的组件应该怎么显示。
官方文档表明,WXSS的选择器目前支持(“.class”、“#id”、“element”、“element, element”、“::after”、“::before”),而且本地资源无法通过WXSS获取,所以WXSS中的样式都是用的网络图片,或者base64。这样,对于某些前端开发者而言,会有所局限。
与CSS相比,WXSS扩展的特性有:
■尺寸单位。
■样式导入。
1.尺寸单位
WXSS新增了针对移动端屏幕的两种尺寸单位:rpx与rem。
rpx(responsive pixel):可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在iPhone6上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
设备 | rpx换算px(屏幕宽度/750) | px换算rpx(750/屏幕宽度) |
---|---|---|
iPhone5 | 1rpx=0.42px | 1px=2.34rpx |
iPhone6s | 1rpx=0.5px | 1px=2rpx |
iPhone6sPlus | 1rpx=0.552px | 1px=1.81rpx |
rem(root em):规定屏幕宽度为20rem;1rem =(750/20)rpx。
因此建议,开发微信小程序时设计师可以用iPhone6作为视觉稿的标准。
2.导入样式
可以使用@import语句来导入外联样式表。@import后跟需要导入的外联样式表的相对路径,并用;表示语句结束。
示例代码如下:
/** common.wxss **/
.small-p { padding:5px; }
/** app.wxss **/
@import "common.wxss";
.middle-p { padding:15px; }
3.内联样式
内联样式指的是框架组件上支持使用style、class属性来控制组件的样式:
■style:静态的样式统一写到class中。style接收动态的样式,在运行时会进行解析,请尽量避免将静态的样式写进style中,以免影响渲染速度:
<view style="color:{{color}}; " />
■class:用于指定样式规则,其属性值是样式规则中类选择器名(样式类名)的集合,样式类名不需要带上“. ”,比如,“.normal-view”样式类的使用:
<view class="normal_view" />
4.选择器
WXSS目前支持的选择器有:
选择器 | 样例 | 样例描述 |
---|---|---|
.class | .intro | 选择所有拥有class="intro"组件 |
#id | #firstname | 选择所有拥有id="firstname"组件 |
element | view | 选择所有view组件 |
element,element | view,checkbox | 选择所有文档的view组件和所有的checkbox组件 |
::after | view::after | 在view组件后面插入内容 |
::before | view::before | 在view组件前边插入内容 |
5.全局样式和局部样式
定义在app.wxss中的样式为全局样式,作用于每一个页面。在page的.wxss文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖app.wxss中相同的选择器。
6.WXSS与CSS开发的差异
(1)选择器的差异
WXSS提供的选择器目前官网提供得很少,下面也是通过图表来对比下WXSS与CSS选择器的差异
(2)适配
WXSS刚开始时并不能适配各种设备,虽然支持rem,但是并不能改变HTML的属性,这使得HTML 5中的rem适配方案失效。最终微信团队推出了rpx(responsive pixel)这个新的计量单位,它规定屏幕宽度为750rpx,从而可以依据屏幕宽度进行自适应。rpx的实现原理跟rem很相似,而且最终也是换算成rem。
rpx计量最大的优势在于750设计稿不需要进行任何转换即可适配。750设计稿量是多少就是多少,如在iPhone6上,屏幕宽度为375px,共有750个物理像素,则750rpx =375px = 750物理像素,1rpx = 0.5px = 1物理像素。但是目前的方案还存在一定的问题,那就是非750的设计稿则需要进行一次换算,如640的设计稿就需要进行一次换算在640设计稿中的1rpx = 640/750rpx,而在WXSS中并不支持算术运算符,所以小程序的视觉设计稿尽量使用750来给出。
(3)样式级联
如"element element",微信团队回复说“级联会破坏掉组件的结构,级联最终会取消”,因此推荐使用BEM,即Block(块)、Element(元素)、Modifier(修饰符),是由Yandex团队提出的一种CSSClass命名方法。后续会提供另外的一种层级关系来解决依赖层级的情况。虽然现在还能使用级联的写法,但是最终可能会废弃,所以建议大家尽量不要使用级联。
框架组件
组件是视图层的基本组成单元,除自带某些功能外,也具有微信风格的样式。一个组件通常包括“开始标签”和“结束标签”,组件由属性来定义与修饰,放在“开始标签”中。组件的内容则包含在两个标签之内;
注意:
所有的组件与属性都需使用小写字符。
所有组件都有的共同属性:
属性名 | 类型 | 描述 | 注解 |
---|---|---|---|
id | String | 组件的唯一标示 | 保持整个页面唯一 |
class | String | 组件的样式类 | 在对应的wxss中定义的样式类 |
style | String | 组件的内联样式 | 可以动态设置的内联样式 |
hidden | Boolean | 组件是否显示 | 所有组件默认显示 |
data-* | Any | 自定义属性 | 组件上触发事件时,会发送给事件处理函数 |
bind/catch | EventHandler | 组件的事件 | 事件绑定 |
同时每一个组件也可以有自定义的属性(称为“特殊独有属性”),用于对该组件的功能或样式进行修饰。但属性只支持下面这七种数据类型:
类型 | 描述 | 注解 |
---|---|---|
Boolean | 布尔值 | 组件写上该属性,不管该属性等于什么,其值为true,该组件上没写该属性,属性值才为false |
Number | 数字 | 1,2,5 |
String | 字符串 | "string" |
Array | 数组 | [1,"string"] |
Object | 对象 | [key:value] |
EventHandler | 事件处理函数名 | "handlerName"是page中定义的事件处理函数 |
Any | 任意属性 | ... |
微信小程序为开发者提供了九大类组件:
组件类型 | 组件用途 | 包含组件 |
---|---|---|
视图容器组件 | 控制视图样式 | view\scroll-view\swiper |
基础内容组件 | 图标,文本与进度条 | icon\text\progress |
表单组件 | 构建表单 | button\form\input\checkbox\radio\picker\picker-view\slider\switch\label\textarea |
互动操作组件 | 操作反馈 | action-sheet\modal\toast\loading |
页面导航组件 | 页面链接 | navigator |
媒体组件 | 多媒体控制 | audio\image\video |
地图组件 | 地图 | map |
画布组件 | 画图 | canvas |
客服会话组件 | 客服会话服务 | contact-button |