wxapp_basic

微信小程序

APPID :wxd79dedc99dd7ccbf

百度:

  • pages:小程序里面目前所有的页面

    • pages 里面一个文件夹表示一个页面,展开之后又分为 4 个部分
    • js:该页面对应的逻辑
    • json:该页面的一些配置信息
    • wxml:全称叫做 wei xin markup language,基本上语法就和 html 一样的,只不过不能使用 html 里面的那些标签,使用的是小程序为我们提供的组件,view、text
    • wxss:全称叫做 wei xin style sheets,负责样式的,基本上就和 css 是一样的
  • utils:工具目录

  • .eslintrc.js:eslint 配置文件

  • app.js:项目的入口 JS 文件

  • app.json:全局的配置文件,可以配置 tabBar、navigation 等

  • app.wxsss:全局的 CSS 样式

小程序的骨架—WXML

数据绑定

undefined值不会被输出到 wxml 中

在猫须语法中支持表达式

条件逻辑

  • wx:if

  • wx:elif

  • wx:else

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <view wx:if="{{age > 18}}">
    <text>可以进入网吧</text>
    </view>
    <view wx:elif="{{ age===18 }}">
    <text>刚好到能够进入网吧的年龄</text>
    </view>
    <view wx:else>
    <text>不能进入网吧</text>
    </view>

block 有点类似于 vue 里面的 tempalte,表示要渲染的一整块内容

1
2
3
4
5
6
7
8
9
<block wx:if="{{age>18}}">
<view>
<text>{{ a > b ? "Hello" : "world"}}</text>
<text>{{ a + b }}</text>
</view>
<view>
<text>当前时间:{{time}}</text>
</view>
</block>

列表渲染

基本上也是和 vue 是相同的,使用的是 wx:for

相比 v-for,在 wx:for 中已经将下标和迭代的元素变量默认确定好了,下标对应的是 index,迭代的每一项为 item

例如:

1
2
3
<view wx:for="{{fruits}}" wx:key="index">
<text>{{index}}</text> - <text>{{item}}</text>
</view>
1
2
3
4
5
6
Page({
data: {
...
fruits : ["苹果","香蕉","哈密瓜"]
},
})

注意,在进行列表渲染的时候,和 v-for 一样,也是需要添加 key,通过 wx:key

定义模板和引入模板

定义模板通过 template,使用 name 来设置模板的名称,模板中可以使用猫须语法接收动态的数据

1
2
3
4
5
6
<template name="msgItem">
<view>
<text>{{index}} : {{ msg }}</text>
<view>Time : {{ time }}</view>
</view>
</template>

引入模板的时候,可以使用 import 和 include

例如下面是使用 import 来引入模板

1
<import src="../../templates/msgItem"/>

在使用的时候通过 is 属性来指定模板的名称,并且通过 data 属性来传入模板所需要的数据

1
<template is="msgItem" data="{{index : 1, msg: '你好', time:'2023.1.10'}}"></template>

注意,在使用 import 引入模板的时候,有一个作用域相关的问题,详细请参阅文档。

还可以 include 来引入模板,这种方式一般适用于静态模板,做的实际上就是一个简单的替换操作。

1
2
3
<view>
<text>这是一个头部</text>
</view>
1
2
3
<view>
<text>这是一个页尾</text>
</view>
1
2
3
4
5
<include src="../../templates/header"/>

// ....

<include src="../../templates/footer"/>

小程序的外观—WXSS

app.wxsss 位于项目的根目录下面,是整个项目的公共样式,它会被注入到小程序的每个页面
尺寸单位

同一个元素,在不同宽度的屏幕下,如果使用px为尺寸单位,有可能造成页面留白过多。

开发 WebApp 的时候,通过 JS 获取到屏幕的尺寸信息,手动去计算应该如何进行缩放。(手机端如何适配)在微信小程序中,专门对尺寸进行了优化。为了适配不同分辨率的屏幕,小程序引入了新的单位:rpx

WXSS引用

基本上和 CSS 也是相同的,使用 @import 来进行引用。

但是和原生 CSS 有一个区别在于,WXSS 会把 @import 引用的 CSS 打包到一块儿,不会多一次请求

内联样式

关于内联样式,基本上和原生 CSS 一模一样。

在此基础上支持动态的样式。

1
<text style="color:{{color}};font-size: {{eleFontsize}};">当前时间:{{time}}</text>
1
2
3
4
5
6
7
Page({
data: {
// ...
color: 'blue',
eleFontsize: '48rpx'
},
})

组件库(下章)

用户交互核心—事件

在文档的开篇,提到了【渲染层】和【逻辑层】,关于这一部分知识我们会在后面第四章【小程序架构篇】进行介绍。

快速入门示例

通过 bind + 事件类型,例如下面的 bindtap 就是绑定了一个 tap 类型的事件

1
2
3
<view class="test" bindtap="tapHandle">
<text class="abc">this is a test</text>
</view>

接下来在 Page 构造函数中,书写对应的事件处理函数。

在事件处理函数中,会自动传入一个参数,该参数就是事件对象。

1
2
3
4
5
6
7
8
9
10
11
Page({
data: {
// ...
},
// 事件处理函数
// 会自动传入一个参数,该参数为此次事件对应的事件对象
tapHandle(e){
console.log('你触发了点击事件');
console.log(e);
}
})

事件类型

在上面的示例中,我们绑定的是一个 tap 事件,它是在点击的时候触发。

具体有的事情,如下表所示:

image-20230111100549023

longtap 和 longpress 的区别在于,如果同时还绑定了 tap 事件,那么longpress并不会再次触发tap事件,而longtap则会再次触发tap事件。

事件对象

我们在前面的例子中,看到了事件对象会自动传入到事件处理函数。

image-20230111100833539

我们先来看 detail,这个可以获取一些额外的信息:

image-20230111101214305

target 和 currentTarget:

  • currentTarget:为当前事件所绑定的组件
  • target:则是触发该事件的源头组件。

示例如下:

1
2
3
<view class="outter" bindtap="tapHandle2" data-id="outter">
<view class="innter" data-id="innter"></view>
</view>
1
2
3
4
tapHandle2(e){
console.log("target: ",e.target);
console.log("currentTarget: ",e.currentTarget);
}

效果如下:

image-20230111102258282

事件冒泡以及阻止冒泡

首先,在小程序中的事件,如果是采用的 bind 进行的绑定,会和 DOM 的事件流一样,有一个事件冒泡的行为:

1
2
3
<view class="outter" bindtap="outtertap" data-id="outter">
<view class="innter" data-id="innter" bindtap="innertap"></view>
</view>
1
2
3
4
5
6
outtertap(e){
console.log("触发了 outter 事件");
},
innertap(){
console.log("触发了 inner 事件");
}

在上面的代码中,如果我们针对 inner 进行点击,那么,事件会一直向上冒泡,outter 组件的 tap 事件也会触发。

可以通过 catch 来绑定事件,使用 catch 绑定的事件,不会向上冒泡。

示例如下:

1
2
3
4
5
<view class="outter" bindtap="outtertap" data-id="outter">
<view class="middle" catchtap="middletap" data-id="middle">
<view class="inner" data-id="inner" bindtap="innertap"></view>
</view>
</view>
1
2
3
4
5
6
7
8
9
outtertap(){
console.log("触发了 outter 事件");
},
middletap(){
console.log("触发了 middle 事件");
},
innertap(){
console.log("触发了 inner 事件");
}

在上面的示例中,因为 inner 是使用 bind 来绑定的,所以会向上冒泡,触发 middle 的 tap 事件,但是 middle 绑定 tap 事件的时候,使用的是 catch 来绑定,catch 会阻止冒泡。

事件捕获

从基础 1.5.0 开始,bind 和事件类型之间可以加一个冒号

例如以前是 bindtap=”事件处理函数”,就可以写作 bind:tap=”事件处理函数”

如果想要使用事件捕获,可以通过 capture-bind 来绑定事件,示例代码如下:

1
2
3
4
5
<view class="outter" capture-bind:tap="outtertap" data-id="outter">
<view class="middle" capture-bind:tap="middletap" data-id="middle">
<view class="inner" data-id="inner" capture-bind:tap="innertap"></view>
</view>
</view>

App与Page构造器

App 构造器

App构造器位于 app.js 里面,整个应用只有这一个

生命周期钩子函数

如果你有 vue 或者 react 的开发经验,那么生命周期钩子函数也是非常熟悉的。所谓生命周期钩子函数,就是在一些固定的时间点自动触发的函数。

在 App 构造器中,我们能够书写的生命周期钩子函数如下:

image-20230111133925087

什么叫做进入后台状态?

用户点击右上角的关闭按钮,或者按手机设备的Home键离开小程序,此时小程序并没有被销毁,这种情况称为“小程序进入后台状态”。

注意,onLaunch、onShow 这两个生命周期钩子函数是接收一个参数的。

因为打开小程序的方式多种多样,有些时候,我们需要根据不同的打开方式,做一些不同的业务处理。

示例如下:

image-20230111134912620

获取全局数据

在微信小程序中,我们有些时候需要不同的页面共享一些公共的数据。

在诸如 vue、react 这种框架中,有专门的状态处理库,在微信小程序中,通过的是 globalData 来共享数据。

globalData 位于 App 构造器中,如下图:

image-20230111135222913

其他页面如何获取公共的数据?

非常简单,在各个页面的 js 文件中,通过 getApp 函数首先获取到 App 的实例,之后访问该实例的 globalData 数据即可

1
2
const app = getApp()
console.log("globalData: ",app.globalData);

有一点一定要注意,虽然在小程序中有多个页面,但是多个页面的 JS 跑在一个线程中,这也就意味着假设你在当前页面设置了定时器,从一个页面跳到另外一个页面,之前所设置的计时器并不会被清除掉。所以需要我们在离开页面的时候手动的来清理掉这些计时器。

另外还有一点,虽然我们通过 getApp 能够获取到 App 的实例,但是一般仅仅是拿来获取 globalData,不要去主动调用生命周期钩子函数,生命周期钩子函数应当是在对应的时间点主动触发的。

Page 构造器

Page构造器位于每个页面的 JS 下面。

我们之前实际上已经接触过一个 Page 构造器的配置项,那就是 data。通过配置 data 里面的数据,可以指定在页面中渲染一些动态的数据。

生命周期钩子函数

Page 除了配置 data 配置项以外,还以配置相应的生命周期钩子函数。

能够配置的选项如下表:

image-20230111140518750
  • onLoad:页面销毁之前会调用一次,当前页面已经加载好了
  • onShow:每次当前页面被显示的时候会调用
  • onReady:页面销毁之前会调用一次,表示当前页面已经渲染完毕

什么算是页面销毁?或者说什么时候页面会被销毁?

当前页面使用wx.redirectTo或wx.navigateBack返回到其他页时,当前页面会被微信客户端销毁回收

和 App 构造器中的生命周期钩子函数相同,不要去主动调用,而是应该在对应的时间点自动触发。总之你记住,只要是生命周期钩子函数,都应该是自动的触发,而不应该去手动的调用。

关于参数的传递

在进行页面跳转的时候,往往存在一种情况,就是当前页面需要传递一个 id 给新的页面,新的页面就根据当前这个 id 显示详情信息。

在跳转的时候,可以通过如下的方式来进行跳转:

1
wx.navigateTo({ url: '/pages/detail/detail?id=1&other=abc' })

实际上就和我们 GET 请求传参是一样的。

接下来的问题就是新的页面如何拿到这个参数?

在 onLoad 生命周期钩子中,可以接收一个参数,通过该参数就能够拿到前一个页面传递过来的参数:

1
2
3
4
5
6
Page({
onLoad: function(option) {
console.log(option.id)
console.log(option.other)
}
})

data 中配置数据

最后就是关于设置 Page 构造器中 data 里面数据的问题,通过 this.setData 来进行设置。该方法接收两个参数,一个是新的数据,另一个是页面随着数据更新重新渲染后的回调函数。

1
2
3
4
5
6
7
editTestHandle(){
this.setData({
test : "aaaaaa"
}, function(){
console.log("修改完毕,页面已经更新了")
})
}

设置的时候,只需要设置更新的数据即可。

同时还有如下的注意点:

  1. 直接修改 Page实例的this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。

  2. 由于setData是需要两个线程的一些通信消耗,为了提高性能,每次设置的数据不应超过1024kB。

  3. 不要把data中的任意一项的value设为undefined,否则可能会有引起一些不可预料的bug。

    路由

    关于 app.json 中的配置

    app.json 主要是对整个小程序进行一个全局的配置。

    • pages:在这个配置项目中,就可以配置小程序里面的页面,小程序默认显示 pages 数组中的第一个页面
    • windows:主要配置和导航栏相关的

    当然,在 app.json 中,还可以进行更多的配置,可以参阅官方文档:https://developers.weixin.qq.com/miniprogram/dev/framework/config.html#%E5%85%A8%E5%B1%80%E9%85%8D%E7%BD%AE

    除了上面两项配置,有一个用的更多的就是 tabBar,在配置这个项目是,list 是必不可少的,list 对应的值为一个数组,数组里面为一个一个的对象,每个对象代表一个 tabBar 的配置,最少要有两个,最多只能有五个。

    关于 tabBar 的配置,更多请参阅官方文档:https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#tabBar

    路由的跳转

    首先,我们需要有页面栈的一个概念,这一点实际上和 vue-router 以及 react-router 这种前端路由库的栈的概念是一致。当你新推入一个页面的时候,这个页面会处于栈顶,当你点击返回的时候,当前页面会出栈。

    在小程序中,限制了一个路由栈的最大层级为 10 层,当已经达到 10 层之后,就无法推入新的页面。

    • wx.navigateTo:向当前的页面栈新推入一个页面
    • wx.navigateBack:当前页面出栈
    • wx.redirectTo:替换当前的页面,当页面栈到达10层没法再新增的时候,往往就是使用redirectTo这个API进行页面跳转。
    • wx.switchTab:负责 tabBar 的切换,注意,在进行 tabBar 的切换的时候,原来的页面栈会被清空。注意:wx.navigateTo和wx.redirectTo只能打开非TabBar页面,wx.switchTab只能打开Tabbar页面
    • wx. reLaunch:这个API本意是重启小程序,在重启的时候可以指定要打开的页面

    注意,路由之间的跳转,必然就会涉及到页面之间的跳转,页面之间的跳转,就会涉及到页面的显示和隐藏,那么也就必然的会涉及到页面的生命周期钩子函数的调用。

    至于哪些生命周期钩子函数会被调用,请参阅官网

    页面之间跳转的时候,如下表所示:

    image-20230111163416002

    tabBar切换的时候,如下表所示:

    image-20230111163507749

小程序中的网络请求

在小程序中,使用 wx.request( ) 这个方法来发送网路请求,整个请求的方式和 jQuery 里面的 $.ajax 方法是非常相似的。

在 wx.request( ) 这个方法中,接收一个配置对象,该配置对象中能够配置的项目如下表:

image-20230112100111671

关于服务器接口

有关服务器接口的配置,需要满足以下两点:

  • 要求必须要是 https 接口
  • https 接口对应的域名还必须要在小程序管理平台进行配置

【开发】-【开发管理】-【开发设置】下面有一个【服务器域名】,在这个位置进行配置

image-20230112100715734

我如果是开发环境怎么办?

在开发环境下,因为开发阶段的服务器接口还没部署到现网的域名下,所以我们可以选择不校验 HTTPS 证书,具体的方式如下图所示:

image-20230112101115812

向服务器传递参数

一般来讲,用得比较多的有 GET 和 POST 请求

  • GET
    • 可以放在 URL 后面(URL 长度有限制,并且还会做一次 URL 的 encode)
    • 也可以放在 data 配置项目里面
  • POST
    • 只能放在 data 里面

综上所述,建议就把数据放在 data 里面

收到回包

只要收到了服务器返回的信息,都会进入到 success 的回调函数,然后我们再在 success 回调函数中根据服务器返回的内容来做下一步操作。

接下来,我们来看一个具体例子

到时候大家会拿到一个名为 server 的服务器代码,大家拿到后,首先使用 npm i 安装依赖包,安装完成后,使用 npm start 启动这个服务器即可。该服务器默认监听 3000 端口,该服务器提供两个接口:

  • / :这是 GET 请求,服务器端会返回 {name : “zhangsan”, age : 18}
  • /abc:这是一个 POST 请求,服务器端会返回 {name : “lisi”, age : 20}

当你安装了依赖包,使用 npm start 启动服务器后,看到下面的画面说明服务器已经启动成功

image-20230112103354180

接下来在小程序端通过 wx.request 进行请求的发送,代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 向服务器发送 Get 请求
sendGet(){
wx.request({
url: 'http://localhost:3000',
data : {
loginId : this.data.loginId,
password : this.data.password
},
success(e){
console.log(e);
}
})
},
// 向服务器发送 Post 请求
sendPost(){
wx.request({
url: 'http://localhost:3000/abc',
method : "POST",
data : {
loginId : this.data.loginId,
password : this.data.password
},
success(e){
console.log(e);
}
})
},

使用技巧

一般来讲,在发送请求的时候,有三点可以优化:

  • 和服务器通信的过程中,需要显示一个 loading 框
    • wx.showLoading( ):显示 loading 框
    • wx.hideLoading( ):隐藏 loading 框
  • 设置超时时间
    • 在 app.json 中设置 networkTimeout
  • 如果处理失败,需要显示一个提示
    • wx.showToast( )

小程序中的本地存储

微信小程序中的本地存储基本上也和 localStorage 是类似的,分为读和写:

  • wx.getStorage(异步)
  • wx.getStorageSync(同步)
  • wx.setStorage(异步)
  • wx.setStorageSync(同步)

读取数据

异步的读取,接收一个配置对象,对象里面首先有 key,表示你要读取哪一个数据,因为是异步,所以读取到的数据会传给 success 回调函数,如果读取失败,那么会触发 fail 回调函数

1
2
3
4
5
6
7
8
9
wx.getStorage({
key : ...,
success(){
// 读取成功后的回调
},
fail(){
// 读取失败时的回调
}
})

同步的读取,直接将读取到的值取出来使用即可:

1
var value = wx.getStorageSync(key);

写入数据

写入实际上也很简单,首先我们来看一下异步写入,使用到的是 wx.setStorage

1
2
3
4
5
6
7
8
9
10
wx.setStorage({
key : ...,
data : ..., // 要写入的数据
success(){
// 写入成功后的回调
},
fail(){
// 写入失败时的回调
}
})

如果是同步写入,传入两个参数,如下:

1
wx.setStorageSync('key', 'value2')

从 2.21.3 版本开始,往本地存储写入数据时,可以进行一个加密的操作,只需要配置 encrypt 为 true 即可。

但是有一些注意事项:

  • 只有异步的存储支持加密(因为加密的时候,回调耗时会增加,所以只能采用异步的方式)
  • 如果进行了加密存储,在获取数据的时候,同样需要将 encrypt 设置为 true 进行解密
  • 因为加密后字符串的长度会膨胀,所以每个 key 最大存储的长度变为了 0.7MB,最大的存储上线由之前的 10MB变为了 7.1MB

缓存限制和隔离

  • 不同小程序的本地缓存空间是分开的,即便是同一个小程序,但是不同的用户之间,也是分开的
  • 每个小程序的缓存空间的上限为10M,如果超过了10M再往缓存里面写入,就会触发 fail 的回调

除了上面介绍到的获取和设置本地数据,常用还有:

  • wx.removeStorage:从本地缓存中移除指定 key。
  • wx.removeStorageSync:上面方法的同步版本
  • wx.clearStorage:清空整个本地存储
  • wx.clearStorageSync:上面方法的同步版本

本节课结束后,请通读官方文档对应的:https://developers.weixin.qq.com/ebook?action=get_post_info&docid=000a2c7c9f4a981b0086bd31e5b40a

特别是是官方文档举了两个使用本地存储的需求案例:

  • 利用本地缓存提前渲染界面

  • 缓存用户登录态SessionId

  • 基础组件串烧

    view

    相当于浏览器中的 div,这边主要说一下关于布局。

    在微信小程序中,布局推荐使用的就是 flex 布局。

    在 view 组件中,有一个 hover-class,可以设置手指按下去的时候的样式

    scroll-view

    这个是可以设置滚动的区域,可以设置横向滚动以及纵向滚动。

    主要通过 scroll-x 以及 scroll-y 这两个属性来设置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <scroll-view class="container2" scroll-x>
    <view class="scrollItem" style="background-color: lightsalmon;">1</view>
    <view class="scrollItem" style="background-color: lightseagreen;">2</view>
    <view class="scrollItem" style="background-color: lightblue;">3</view>
    <view class="scrollItem" style="background-color: pink;">4</view>
    </scroll-view>

    <view class="title">纵向滚动示例</view>
    <scroll-view class="container3" scroll-y>
    <view class="scrollItem" style="background-color: lightsalmon;">1</view>
    <view class="scrollItem" style="background-color: lightseagreen;">2</view>
    <view class="scrollItem" style="background-color: lightblue;">3</view>
    <view class="scrollItem" style="background-color: pink;">4</view>
    </scroll-view>

    text

    相当于是浏览器中的 span,可以横向的嵌套,设置某一段文字单独的样式

    image

    该组件用来设置图片。需要说明的是,因为小程序对程序的大小有要求。

    在我们的项目中,一般我们自己写的代码不会太大,一般比较大的就是静态资源。

    所以在小程序中,一般静态资源采用远程加载的方式。

    button

    按钮严格来讲,是属于表单组件,但是在平时开发中,哪怕没用到表单,按钮还是用得很多的。

    详细的属性请参阅官方文档

    navigator

    该组件是一个导航组件。我们前面在进行路由跳转的时候,使用的是 API 的方式进行的跳转。

    除了使用 API 的方式以外,还可以使用 navigator 组件进行跳转。

    icon

    微信小程序官方组件库提供了一些 icon,但是并不多,所以一般我们会用到第三方库或者 iconfont

    富文本

    在富文本输入框中,支持你填写 html 结构的字符串,回头在富文本输入框中会将这些 html 结构的字符串渲染为 wxml

    表单

    input

自定义组件

关于自定义组件:https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/

在使用自定义组件的时候,首先需要注意版本问题,基础库要大于等于 1.6.3

在使用自定义组件的时候,一般单独拿一个目录来存放自定义组件,一般是 components

页面中在使用自定义组件时,需要在在 json 文件中进行一个配置,例如:

1
2
3
4
5
{
"usingComponents": {
"item" : "/components/item/item"
}
}

不同于页面对应的 JS 文件中的 Page 构造器,在自定义组件中,对应的 JS 文件的构造器为 Component

  • properties:在使用自定义组件时,父组件传入的属性
  • data:表示该自定义组件自身的数据
  • methods:书写对应的事件处理函数
  • options: 关于自定义组件的选项配置,例如我们要使用多插槽的时候,就需要配置 multipleSlots 为 true
  • externalClasses:用于指定外部传入的样式类

从开发者工具 1.02.1810190 及以上版本开始,可以在 app.json 中使用 usingComponents 来注册组件,在 app.json 中所注册的组件被视为全局组件,各个页面,以及其他自定义组件中都可以使用。

在设计自定义组件的时候,是可以添加插槽,插槽的用法和 Vue 是非常类似的。

并且和 Vue 中的插槽一样,可以设置具名插槽

自定义组件

介绍和组件模板

关于自定义组件:https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/

在使用自定义组件的时候,首先需要注意版本问题,基础库要大于等于 1.6.3

在使用自定义组件的时候,一般单独拿一个目录来存放自定义组件,一般是 components

页面中在使用自定义组件时,需要在在 json 文件中进行一个配置,例如:

1
2
3
4
5
{
"usingComponents": {
"item" : "/components/item/item"
}
}

不同于页面对应的 JS 文件中的 Page 构造器,在自定义组件中,对应的 JS 文件的构造器为 Component

  • properties:在使用自定义组件时,父组件传入的属性
  • data:表示该自定义组件自身的数据
  • methods:书写对应的事件处理函数
  • options: 关于自定义组件的选项配置,例如我们要使用多插槽的时候,就需要配置 multipleSlots 为 true
  • externalClasses:用于指定外部传入的样式类

从开发者工具 1.02.1810190 及以上版本开始,可以在 app.json 中使用 usingComponents 来注册组件,在 app.json 中所注册的组件被视为全局组件,各个页面,以及其他自定义组件中都可以使用。

在设计自定义组件的时候,是可以添加插槽,插槽的用法和 Vue 是非常类似的。

并且和 Vue 中的插槽一样,可以设置具名插槽

定义插槽:

1
2
3
4
5
6
7
8
<view class="container">
<view bindtap="tapHandle">{{name}}</view>
<slot name="before"></slot>
<view>{{content}}</view>
<slot></slot>
<view>计数器:{{count}}</view>
<slot name="after"></slot>
</view>

使用自定义组件时,就可以往插槽插入动态的内容

1
2
3
4
5
6
7
8
9
<view class="container">
<view bindtap="tapHandle">index</view>
<item content="传入的内容" count="{{count}}" class="my-class"></item>
<item count="{{count}}">
<view slot="before">这部分内容会被放入到before</view>
<view slot="after">这部分内容会被放入到after</view>
<view>这部分内容会被放入到默认插槽</view>
</item>
</view>

Component 构造器

  • App:整个小程序的构造器
  • Page:页面对应的构造器
  • Component:自定义组件构造器
    • properties:在使用自定义组件时,父组件传入的属性
    • data:表示该自定义组件自身的数据
    • methods:书写对应的事件处理函数
    • options: 关于自定义组件的选项配置,例如我们要使用多插槽的时候,就需要配置 multipleSlots 为 true
    • externalClasses:用于指定外部传入的样式类
    • lifetimes:生命周期钩子函数,早期的时候,生命周期钩子函数和 Page、App 一样,直接写在配置对象里面,但是后面随着版本的更新,现在推荐写在 lifetimes 配置对象里面,并且写在 lifetimes 里面的优先级是最高的。
    • pageLifetimes:组件所在页面的生命周期

实际上,页面也可以被当作是一个自定义组件来使用。

组件之间涉及到数据的传递,和 Vue 是相似的,父传子通过 properties,子传父通过触发父组件的自定义事件来传递,注意在触发父组件的自定义事件时,使用的是 this.triggerEvent 来进行触发的。

可以通过 this.selectComponent(‘自定义组件的样式类’) 来获取自定义组件实例对象

生命周期

在自定义组件中,提供了

  • created:组件实例刚刚被创建好时,此时还不能调用 setData通常情况下,这个生命周期只应该用于给组件 this 添加一些自定义属性字段。有点类似于 Vue 里面的 created
  • attached:在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发,这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。有点类似于 Vue 里面的 mounted
  • detached:在组件离开页面节点树后触发,类似于 Vue 里面的 destory

behaviors

这个就类似于 Vue 里面的 mixin,用来提取组件公共的部分(data、method、生命周期钩子)

当我们要定义一个 behavior 的时候,需要用到 Behavior 构造器

1
2
3
Behavior({
// ...
})

组件间关系

在使用自定义组件的时候,可以使用 relations 字段来指定自定义组件之间的关系,指定了关系之后,就可以获取到对应组件的实例。

在使用 relations 的时候,必须两个关联的组件都要加入此字段。

组件间使用 relations 设定了相互关系后,最大的好处在于能够和关联的组件进行通信,如何通信的?

主要就是拿到关联组件的实例对象,实例对象一拿到,data 这些数据也就拿到了.

数据监听器

数据监听器可以用于监听和响应任何属性和数据字段的变化。从小程序基础库版本 2.6.1 开始支持。

这个实际上就和 Vue 里面的 watch 是类似的。

纯数据字段

在 data 里面所定义的数据,一般来讲在页面是会重新渲染的。如果有一些数据,既不会展示在界面上,也不会传递给其他组件,仅仅是拿来做数据计算的,那么这个时候如果定义在 data 中,就会参与页面重新渲染。但是我们不需要这些字段(纯数据字段)发生改变时页面重新渲染,因此在微信小程序中,提供了一种机制。

  1. 首先,在 Component 中的 options 中书写一个正则
  2. 在 data 中所定义的数据如果能够匹配上该正则,该数据字段就是一个纯数据字段
  3. 纯数据字段值发生变化时,不会引起页面的重新渲染

抽象节点

这个就类似于 react 的 renderProps,抽象节点的核心就是在使用自定义组件时,可以将另一个组件以 props 的形式传递到该自定义组件中。因此传递的是什么组件,最终渲染的就是什么组件。

首先第一步,我们在使用自定义组件的时候,可以将另外的自定义组件像 props 一样传入

1
2
<item4 generic:selectable="sel1"/>
<item4 generic:selectable="sel2"/>

在上面的代码中,我们使用了 item4 这个自定义组件,然后我们还分别将 sel1 和 sel2 作为 props 传入到了 item4 里面。

注意这里需要在对应的 json 文件中注册 sel1、sel2、item4 这几个组件。

接下来在 item4 这个自定义组件中,书写 selectable 来渲染传入的自定义组件

1
2
3
4
5
<view class="container">
<view>item4</view>
<view>该示例演示了抽象节点</view>
<selectable/>
</view>

另外,在接受渲染组件的自定义组件中(item4)的 json 文件中,需要开启 selectable

1
2
3
"componentGenerics": {
"selectable": true
}

这玩意儿和插槽非常类似?

这个抽象节点和 react 的 renderProps 非常相似,主要作用是用来横向抽离公共的逻辑,因为我们是传入的一个组件,组件里面是一套完整的功能。回头我们可以将一些公共的业务逻辑(视图、数据、行为)单独的以组件的形式抽离出来。

使用第三方组件库

微信小程序的组件库也是非常丰富的,比较有名的:

微信官方也提供了一套官方的组件,叫做 WeUI,但是风格基本上就和微信非常相似

vant 使用示例

首先第一步需要安装

1
npm i @vant/weapp -S --production

但是在安装之前,需要先使用 npm init -y 初始化一下整个项目

第二步去掉 app.json 里面的 “style”: “v2”,否则可能会出现样式混乱

新版的微信开发者工具不需要修改 project.config.json,所以我们直接进入到第四步,点击【工具】下面的【构建npm】,完成构建

image-20230202091459194

构建成功之后,根目录下面会生成 miniprogram_npm 目录

image-20230202091642411

使用组件时,需要现在页面的 json 中进行配置,例如:

1
2
3
4
5
{
"usingComponents": {
"van-button": "@vant/weapp/button/index",
}
}

云开发介绍

官方文档:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html

使用云开发,最大的好处就在于开发者无需搭建服务器

云开发模式和传统模式对比

云开发介绍

传统开发模式,需要考虑:

  • 成本:需要维护服务器的成本,在维护服务器时涉及到大量的服务器相关的知识内容,并且在并发量大了之后,还需要考虑服务器的扩展问题
  • 技术:对于单纯只会前端技术的人员来讲,还存在学习后端技术的成本

在微信小程序的云开发中,提供了3个核心的技术

  • 云数据库
  • 云存储
  • 云函数

云数据库

云端提供了一个数据库,开发人员在小程序端可以直接对云端数据库的数据做一个增删改查的操作

云端的数据库是一个类似于 MongoDB 的文档类型存储数据库,里面存储的是一条一条的文档(JSON文档),对于前端开发人员来讲非常好理解

云存储

在开发项目的时候,经常会涉及到文件的存储。有了云存储之后,我们可以上传文件到云端,当然也可以下载和删除。并且云存储自带CDN(内容分发系统)

云函数

提供了在云端服务器执行代码的能力。

假设我现在有一个函数,这个函数里面涉及到了大量的运算,比较耗时。现在有了云函数之后,你可以把这个耗时的函数放到云端的服务器执行,云端服务器执行完该函数后,将结果返回到小程序端。

因此在进行云函数的调用时,一定是异步的。

Serverless 就是指应用的开发不再需要考虑服务器这样的硬件基础设施,基于 Serverless 架构的应用主要依赖于像腾讯云这样的云服务商提供的后台服务。比如说无服务云函数、云数据库、对象存储服务等等。简单来说,相当于你现在要开个水果店卖水果,以前你还得要租店面,搞水电、装修门面。现在这些都不用了,你就在一个已经搭好各种各样设施的超市里,租一个已经帮你搞好门面的架子或者箱子,卖得好你就租大一点,卖不好就租小一点,随时随地随你的心意,非常灵活。

官方有一篇介绍云开发的文章:https://cloud.tencent.com/developer/article/1345700

点击【云开发】,就会进入到云开发控制台

image-20230202145015394

在云开发控制台中,有一个环境ID,这个ID很重要,后面我们在初始化小程序的云服务时,需要填写该ID

image-20230202145145958

在最早期的时候,微信小程序所提供的云开发是免费的,按量收费。但是现在整个小程序云开发更新了,更新为按月收费,并且根据你使用的量的不同,费用也不同。

云数据库

首先第一步,需要初始化云服务器

你需要拿到你的环境ID,接下来需要在 app.js 中做初始化工作

1
2
3
4
5
6
7
8
App({
onLaunch() {
// 初始化云服务
wx.cloud.init({
env: 'cloud1-5gsobkys7eb1b3ef'
});
},
})

初始化完毕后,我们就可以使用云服务了(云数据库、云存储、云函数)

下面是在云数据库中增加数据的示例:

1
2
// 获取云端的数据库实例
const db = wx.cloud.database();

首先获取云端数据库的实例,接下来通过数据库实例获取集合

1
2
// 再从数据库中获取到集合(表)
const students = db.collection('students');

通过集合调用相应的方法来进行增删改查,例如要增加一条记录,那就是调用 add 方法

1
2
3
4
5
students.add({
data : this.data
}).then(res=>{
console.log(res)
})

云存储

所谓云存储,就是指可以将文件存储到云端。

在小程序端可以分别调用 wx.cloud.uploadFilewx.cloud.downloadFile 完成上传和下载云文件操作。

在使用组件的时候,可能会出现“代码依赖分析,无法被其他模块引用”,这里可以参阅:https://developers.weixin.qq.com/community/develop/article/doc/00020631afc6c8c6f62e7b91855c13?idescene=6

主要就是在 project.config.json 中加入如下两项配置:

1
2
ignoreDevUnusedFiles: false , 
ignoreUploadUnusedFiles: false

在上传的时候,注意 cloudPath 有命名的限制,具体可以参阅:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage/naming.html

云函数

所谓云函数,就是一段代码在云端执行,执行完毕后将执行的结果返回给本地。

在使用云函数的时候,分为两个步骤:

  • 本地编写云函数,上传到云端
  • 本地调用云端的函数

上传云函数

首先第一步,我们在 project.config.json 里面配置云函数的目录:

1
"cloudfunctionRoot": "functions/"

在上面的代码中,我们指定云函数在根目录下的 functions 目录下面

image-20230203141842697

接下来在 functions 下面创建对应的云函数目录,例如:

image-20230203143202123

之后再在 calc 目录中创建对应的 js 文件,书写云函数对应的逻辑:

image-20230203143309653

注意:在编写和调用云函数的时候,一定要注意这是一个异步的过程。

在编写云函数的时候,需要使用到 wx-server-sdk 这个依赖,在终端中 cd 到云函数所在的目录(例如我们这里是 calc),然后首先进行 npm init -y 初始化操作,然后输入 npm i wx-server-sdk 安装该依赖。

云函数的传入参数有两个,一个是 event 对象,一个是 context 对象。event 指的是触发云函数的事件,当小程序端调用云函数时,event 就是小程序端调用云函数时传入的参数,外加后端自动注入的小程序用户的 openid 和小程序的 appid。context 对象包含了此处调用的调用信息和运行状态,可以用它来了解服务运行的情况。

现在我们的云函数就已经编写完毕了。

编写完毕后我们需要上传这个云函数到云端

image-20230203143504058

上传云函数成功之后,可以在控制面板的【云函数】面板中看到刚才所上传的云函数。

image-20230203143730905

本地调用云端的函数

在本地,通过 wx.cloud.callFunction 来进行云函数的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
wx.cloud.callFunction({
// 云函数名称
name: 'add',
// 传给云函数的参数
data: {
a: 1,
b: 2,
},
success: function(res) {
console.log(res.result.sum) // 3
},
fail: console.error
})

双线程架构

小程序的架构模型有别与传统 web 单线程架构,小程序为双线程架构。

微信小程序的渲染层与逻辑层分别由两个线程管理,渲染层的界面使用 webview 进行渲染;逻辑层采用 JSCore运行JavaScript代码。这里先看一下小程序的架构图。

image-20230214165008721

可以从图中看出,由于渲染层与逻辑层分开,一个小程序有多个界面,所以渲染层对应存在多个webview

webview 嵌入式浏览器

这两个线程之间由Native层进行统一处理。无论是线程之间的通讯、数据的传递、网络请求都由Native层做转发。

首先,我们来解释一下什么是webview

平常我们浏览网页都是在浏览器中,可以想象webview是一个嵌入式的浏览器,是嵌入在原生应用中的。webview 用来展示网页的 view 组件,该组件是你运行自己的浏览器或者在你的线程中展示线上内容的基础。使用 webkit 渲染引擎来展示,并且支持前进后退、浏览历史、放大缩小、等更多功能。

简单来说 webview 是手机中内置了一款高性能 webkit 内核浏览器,在 SDK 中封装的一个组件。不过没有提供地址栏和导航栏,只是单纯的展示一个网页界面。

因此,微信小程序本质上是一个 Hybrid 应用。

简单回忆一下当前移动端应用的三种模式:

  • 原生应用(react native)
  • WebApp(HTML、CSS、JS)
  • Hybrid 应用(uniapp、微信小程序)

那么,这里采用双线程的好处有哪些呢?在我看来,至少有如下几个点的好处:

  • 避免单线程阻塞问题
  • 多个webview更接近于原生应用的体验
  • 依赖Natvie层做转发,逻辑层与渲染层更加专注于自身的责任

避免单线程阻塞问题

我们知道,浏览器在渲染页面时,靠的是渲染线程进行渲染,所有的活儿都依赖于这个单线程,因此页面的渲染和 JS 的执行是互斥的。

1
2
3
<button id="btn">阻塞5秒</button>
<div class="one"></div>
<div class="two"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
div {
width: 100px;
height: 100px;
background-color: red;
border-radius: 50%;
}
.one{
animation: move1 5s infinite alternate;
}
.two{
background-color:blue;
position: absolute;
left: 10px;
top: 150px;
animation: move2 5s infinite alternate;
}
@keyframes move1 {
0% {
transform: translateX(0);
}
100% {
transform: translateX(500px);
}
}
@keyframes move2 {
0% {
left: 10px;
}
100% {
left: 500px;
}
}
1
2
3
4
5
6
7
function delay(duration) {
var start = Date.now();
while (Date.now() - start < duration) {}
}
btn.onclick = function () {
delay(5000);
};

在上面的示例中,一旦我们执行耗时的 JS 操作,那么小球移动的渲染工作就会被搁置。

但是在小程序中就不存在这个现象,因为它并非像 Web 那样单线程导致 JS 的执行会阻塞页面的渲染。在小程序中,即便执行耗时的 JS 操作,页面仍然能够正常的渲染,不被阻塞。

1
2
3
<button bindtap="handletap">阻塞</button>
<view class="one"></view>
<view class="two"></view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
view {
width: 100px;
height: 100px;
background-color: red;
border-radius: 50%;
}

.one {
animation: move1 5s infinite alternate;
}

.two {
background-color: blue;
position: absolute;
left: 0px;
top: 150px;
animation: move2 5s infinite alternate;
}

@keyframes move1 {
0% {
transform: translateX(0);
}

100% {
transform: translateX(250px);
}
}

@keyframes move2 {
0% {
left: 0px;
}

100% {
left: 250px;
}
}
1
2
3
4
5
6
7
8
9
10
11
Page({
delay(duration){
console.log("阻塞开始");
var start = Date.now();
while (Date.now() - start < duration) {}
console.log("阻塞结束");
},
handletap(){
this.delay(5000);
}
})

多个webview更接近于原生应用的体验

在浏览器的单页应用中,渲染页面是通过路由识别随后动态将页面(组件)挂载到root节点中去,如果单页面应用打开一个新的页面,需要先卸载掉当前页面结构,并且重新渲染。

但是原生APP并不是这个样子,比较明显的特征为从页面右侧向左划入一个新的页面,并且我们可以同时看到两个页面。

image-20230214203712804

多页面应用就很好达到这个效果,新页面直接滑动出来并且覆盖在旧页面上即可,这也是小程序现在所做的形式。多个webview能够加接近原生应用APP的用户体验。

依赖Natvie层做转发,逻辑层与渲染层更加专注于自身的责任

双线程的好处不仅仅是一分为二而已,还有强大的Native层做背后支撑。

Native层除了做一些资源的动态注入,还负责着很多的事情,请求的转发,离线存储,组件渲染等等。界面主要由成熟的 Web 技术渲染,辅之以大量的接口提供丰富的客户端原生能力。同时,每个小程序页面都是用不同的WebView去渲染,这样可以提供更好的交互体验,更贴近原生体验,也避免了单个WebView的任务过于繁重。

有了Native层这么一个靠山后,让逻辑层与渲染层更加专注于自身的责任。

课后阅读官方文档:


Exparser设计原理

本章主要包含以下内容:

  • WebComponent原理
  • Custom Element原理
  • ShadowDOM思想
  • Exparser原理

什么是WebComponent?

WebComponent 汉语直译过来第一感觉是web组件的意思,但是它只是一套规则、一套API。你可以通过这些API创建自定义的新的组件,并且组件是可以重复使用的,封装好的组件可以在网页和Web应用程序中进行使用。

当前的前端开发环境,Vue、React等都基于组件化开发的形式,但是他们的组件生态并不互通,如果你有过两个框架的开发经验的话,你应该知道最烦恼的就是两个框架的UI组件表现不一致的问题。

我们抽离组件为了提高复用率,提升开发效率。但是脱离了像Vue、React这样的框架后,你会发现,原生JS难道就不能开发自定义组件吗?WebComponent就是为了解决这个问题。

换一个角度来说,并不是所有的业务场景都需要Vue\React这样的框架进行开发、也并是都需要工程化。很多业务场景我们需要原生JS、HTML。

言归正传,WebComponent实现的组件可以和HTML原生标签一起使用,有了这个概念之后,我们看一下它的具体表现形式是怎样的。

1
2
3
<body>
<custom-component></custom-component>
</body>

上面我们看到<body>标签还是我们熟悉的标签,但是<custom-component>标签就是自定义组件的标签了,它不属于html语义化标签中的任何一个,是自定义的。

接下来我们从这个简单的DEMO入手,对WebComponent进行了解。首先就是三大规范:

  • Custom Elements规范
  • Template规范
  • Shadow DOM规范

MDN:https://developer.mozilla.org/zh-CN/docs/Web/Web_Components

Custom Element

所谓自定义元素,即当内置元素无法为问题提供解决方案时,自己动手来创建一个自定义元素来解决,上方的<custom-component>就是我们手动创建的自定义元素。

元素的状态是指定义该元素(或者叫做升级该元素)时元素状态的改变,升级过程是异步的。 元素内部的状态有:

  • undefined 未升级:即自定义元素还未被define。
  • failed 升级失败:即define过了也实例化过了,但失败了。会自动按HTMLUnknownElement类来实例化。
  • uncustomized 未定制化:没有define过但却被实例化了,会自动按HTMLUnknownElement类来实例化。
  • custom 升级成功:define过并且实例化成功了。

接下来我们来看一个示例:

1
<custom-component></custom-component>
1
2
3
4
5
6
7
.custom-style{
display: inline-block;
padding: 15px;
border: 1px solid red;
border-radius: 4px;
color: red;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义自定义组件
class CustomComponent extends HTMLElement {
constructor() {
super();

const box = document.createElement("div");
box.className = "custom-style";

const text = document.createElement("p");
text.innerText = "这是一个自定义组件";

box.appendChild(text);

this.appendChild(box);
}
}
window.customElements.define("custom-component", CustomComponent);

效果如下:

image-20230215094800286

首先可以看出,需要有个类的概念。自定义元素类必须继承自window内置的HTMLElement类。

然后在constructor中定义类一些标记模版,定义模板后,执行this.appendChild,其中this指向了当前类实例。

最后将自定义组件挂载到customElements上,通过window.customElements.define方法。这个时候注意了,需要给自定义组件起一个名字,可以看到上面例子中我起的名字为custom-component。起名字是有规则的,规则如下:

  • 自定义元素的名称,必须包含短横线(-)。它可以确保html解析器能够区分常规元素和自定义元素,还能确保html标记的兼容性。
  • 自定义元素只能一次定义一个,一旦定义无法撤回。
  • 自定义元素不能单标记封闭。比如<custom-component />,必须写一对开闭标记。比如 <custom-component></custom-component>

对于自定义组件挂载的相关API:

  • window.customElement.define('custom-component', CustomComponent, extendsInit) // 定义一个自定义元素
  • window.customElement.get('custom-component') // 返回已定义的自定义元素的构造函数
  • window.customElement.whenDefined('custom-component') // 接收一个promise对象,是当定义自定义元素时返回的,可监听元素状态变化但无法捕捉内部状态值。

其中window.customElement.whenDefined方法监听的元素状态为上述讲解的四种元素状态中的: failed升级失败和custom升级成功。

这里有个问题,我们demo里的dom结构比较简单,所以我们通过document.createElementappendChild方法进行构建还不算复杂,如果dom结构很复杂的组件怎么办呢?一顿使用createElement也不是个办法。这个时候我们就要引入<template>标记了。

Template

Web Components API 提供了<template>标签,可以在它里面使用HTML定义DOM结构。这样的话我们改版一下我们的自定义组件:

1
2
3
4
5
6
7
<custom-component></custom-component>

<template id="constomCompinentTemplate">
<div class="custom-style">
<p>这是一个自定义组件</p>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
// 定义自定义组件
class CustomComponent extends HTMLElement {
constructor() {
super();

const template = document.getElementById("constomCompinentTemplate");
const content = template.content.cloneNode(true);
this.appendChild(content);
}
}
window.customElements.define("custom-component", CustomComponent);

这里有两个需要考虑的地方:

  1. 这里因为是demo演示所以把<template>标签写在了一起,其实可以用脚本把<template>注入网页。这样的话,JavaScript 脚本跟<template>就能封装成一个 JS 文件,成为独立的组件文件。网页只要加载这个脚本,就能使用<custom-component>组件。
  2. <template>标签内的节点进行操作必须通过template.content返回的节点来操作。因为这里获取的template并不是一个正常的DOM结构,在控制台打印一下template.content得到的结果是#document-fragment。它其实是DocumentFragment节点,里面才是真正的结构。而且这个模板还要留给其他实例使用,所以不能直接移动它的子元素

在 Vue 和 React 中使用组件时,我们经常涉及到 props 的传递,例如:

1
2
<custom-component></custom-component>
<custom-component text="显示这个文本"></custom-component>

传入自定义的文本text,如果有text内容那么就展示text,如果没有,那么展示默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template id="constomCompinentTemplate">
<style>
.custom-style {
display: inline-block;
padding: 15px;
border: 1px solid red;
border-radius: 4px;
color: red;
}
</style>
<div class="custom-style">
<p class="component-text">这是一个自定义组件</p>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义自定义组件
class CustomComponent extends HTMLElement {
constructor() {
super();

const template = document.getElementById("constomCompinentTemplate");
const content = template.content.cloneNode(true);

// 从 this 上获取 text 属性,如果有值就赋值给 content
const textValue = this.getAttribute("text");
if(textValue){
content.querySelector(".component-text").innerHTML = textValue;
}

this.appendChild(content);
}
}
window.customElements.define("custom-component", CustomComponent);

你看,这样之后就可以传入参数了,但是我们平常使用组件的时候是可以嵌套的,我们不仅仅需要参数注入的形式,还需要嵌套的children形式。继续修改自定义组件。

slot

WebComponent有一个slot概念,插槽,提供了一个“缺口”让给需要嵌套的dom。用法和Vue是比较相似的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<custom-component>
<p slot="my-text">这是插入的内容!</p>
</custom-component>
<custom-component text="显示这个文本"></custom-component>
<p class="custom-style">这是一个测试</p>

<template id="constomCompinentTemplate">
<style>
.custom-style {
display: inline-block;
padding: 15px;
border: 1px solid red;
border-radius: 4px;
color: red;
}
</style>
<div class="custom-style">
<p class="component-text">这是一个自定义组件</p>
<slot name="my-text">插槽默认内容</slot>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义自定义组件
class CustomComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "closed" });

const template = document.getElementById("constomCompinentTemplate");
const content = template.content.cloneNode(true);

// 从 this 上获取 text 属性,如果有值就赋值给 content
const textValue = this.getAttribute("text");
if (textValue) {
content.querySelector(".component-text").innerHTML = textValue;
}

shadow.appendChild(content);
}
}
window.customElements.define("custom-component", CustomComponent);

在上面的代码中,我们使用到了 slot 插槽,代码本身比较容易理解,但是注意我们这边还引入了一个新的东西,就是 shadow,这也是 webcomponents 的三大特性之一,shadow DOM中的结构是与外界隔离的,外界是无法获取到内部dom的,它可以理解为一颗单独的dom树,隐藏的dom树。因此组件内部的样式也和外界完全隔离,即使下面的 p 也使用了 custom-style 的类名。

有关shadow DOM将会在后面具体进行介绍。

事件

有了参数之后不能离开事件Event,对吧,我们想添加一个文本的点击事件。继续来改造升级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义自定义组件
class CustomComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "closed" });

const template = document.getElementById("constomCompinentTemplate");
const content = template.content.cloneNode(true);

// 从 this 上获取 text 属性,如果有值就赋值给 content
const textValue = this.getAttribute("text");
const textDOM = content.querySelector(".component-text");
if (textValue) {
textDOM.innerHTML = textValue;
}

// 绑定事件
textDOM.addEventListener("click", (e) => {
e.stopPropagation();
alert("Hello Web Components");
});

shadow.appendChild(content);
}
}
window.customElements.define("custom-component", CustomComponent);

在上面的demo中,我们为里面的 p 元素绑定了一个点击事件,并且使用了e.stopPropagation()方法阻止了事件冒泡。

这里有个知识点,自定义事件 new Event()中,options有几个参数可以设置冒泡行为方式,其中就有关于Shadow DOM的。我们来看一下:

1
2
3
4
5
6
7
8
9
var options = {
detail : {
...
},
composed: false, // Boolean 类型,默认值为 false,指示事件是否会在 ShadowDOM 根节点之外触发侦听器
bubbles: true, // Boolean 类型,默认值为 false,表示该事件是否冒泡
canceable: false // Boolean 类型,默认值为 false,表示该事件是否能被取消
}
var myEvent = new CustomEvent(eventname, options);

Shadow DOM

Shadow DOM 允许将隐藏的 DOM 树附加到常规的 DOM 树中——它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。

image-20230215104608599

把本来DOM树中的一部分封装起来,并且隐藏起来,隐藏起来的树概念为Shadow Tree。把它理解成DOM上一棵特殊的子树,称之为shadow tree或影子树。也是树,但是很特殊,树里面也是DOM,就像我们上面用document.createElement创建的DOM一样。

影子树的根节点,我们称之为shadow root影子根

影子根的父节点,我称之为宿主shadow host

image-20230215105036312

在自定义元素中,里面的结构已经变成了Shadow DOM。顺带说下attachShadow中的mode参数有两种值“open”、“closed”;

  • open: 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,例如使用 Element.shadowRoot 属性:
1
let myShadowDom = myCustomElem.shadowRoot;
  • closed: 那么就不可以从外部获取Shadow DOM了myCustomElem.shadowRoot 将会返回 null

ShadowDOM的概念在HTML中非常常见,举一个例子,在 HTML 中有 Video 标签

1
2
3
4
5
<video 
src="http://maoyan.meituan.net/movie/videos/854x4804c109134879943f4b24387adc040504b.mp4"
controls
width="500"
></video>

当我们使用该标签渲染一个视频的时候,会发现页面中会呈现出来一个完整的播放器,里面有播放视频的进度条、播放按钮、音量调节等。明明只有一个标签,为什么内部有如此丰富的内容呢?

image-20230215110726053

打开控制台查看结构时,看到的也仅仅是一个 video 标签而已,我们可以打开控制台的【设置】,勾选上【显示用户代理Shadow DOM】

image-20230215111137642

之后就可以看到在 video 中的 shadowDOM了

image-20230215111304144

因此,像img、button、input、textarea、select、radio、checkbox,video等等这些标签是不可以作为宿主的,因为它们本身内部就已经有shadowDOM了。

Exparser框架原理

Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序提供各种各样的组件支撑。内置组件和自定义组件都有Exparser组织管理。

有关Exparser可参阅官网:https://developers.weixin.qq.com/ebook?action=get_post_info&docid=0000aac998c9b09b00863377251c0a

WXSS编译与适配

微信小程序的本质是一个 Hybrid 应用,在App组件中有一个 WebView 的组件可以用来显示网页。

而如果你把浏览器想象成两部分,那么一部分是 UI(地址栏,导航栏按钮等),其它部分是把标记跟代码转换成我们可见和可交互视图的浏览器引擎。

image-20220222115102001

WebView 就是浏览器引擎部分,你可以像插入 iframe 一样将 Webview 插入到你的原生应用中,并且编程化的告诉它将会加载什么网页内容。这样我们可以用它来作为我们原生 app 的视觉部分。当你使用原生应用时,WebView 可能只是被隐藏在普通的原生 UI 元素中,你甚至用不到注意到它。

image-20220222115121519

明确了这一点之后,那么我们可以知道,最终微信小程序中的 WXML 以及 WXSS 还是离不开原生的 HTML、CSS

有关 WXML 之前我们已经看过了,实际上就是使用的类似 WebComponents 来自定义的组件。

那么 WXSS 呢?

WXSS并不可以直接执行在webview层进行渲染,而是通过了一层编译。我们接下来就带大家编译一个WXSS看一下。

编译的工具名字叫WCSC,这个编译的过程是在微信开发者工具端执行的,那么这个编译工具在哪呢,我们来找一下。在微信开发者工具的控制台界面,输入help()命令可见如所示界面。

image-20230215141015364

如果help( )函数执行后无效果或者抛错,请检查控制台下方位置是否为top选项卡。

可以看到这里有一些命令。我们继续在控制台执行第八条openVendor()命令。

这时候弹出了一个名为WeappVendor的文件夹。在我截图的这个顺序里,可以看到最后一个文件名称正是我们要寻找的WCSC。文件种类是可执行文件。WXSS正是用这个工具来编译的。

image-20230215141122926

我们找到了WCSC编译工具后,把这个工具复制到项目的pages/index目录下,与index.wxss同目录。

image-20230215141202360

把终端目录打开到pages/index目录中。执行:

1
./wcsc -js index.wxss >> wxss.js

这时候可以看到目录中多了一个wxss.js文件。

wxss.js文件就是WXSS文件编译后的文件,index.wxss文件会先通过WCSC可执行程序文件编译成js文件。并不是直接编译成css文件。

那么我们直接看一下内部代码是怎样的呢。

这里我拆成了三部分来看,三部分加一起就是完整的代码。第一部分:设备信息

image-20230215141434913

这个部分用于获取一套基本设备信息,包含设备高度设备宽度物理像素与CSS像素比例设备方向

image-20230215141502522

这里就是rpx转化的方法了,rpx转化的具体算法就是中间那两句,并且做了一个精度收拢的优化。把那两句单独提取出来再看一下,平常在开发中自己写一个这样的方法也是一种不错的选择。

1
2
number = number / BASE_DEVICE_WIDTH * (newDeviceWidth || deviceWidth);
number = Math.floor(number + eps);
image-20230215141653173

最后这一段代码比较长,看到方法名称我们就可以猜到这个函数是干嘛用的了setCssToHead

首先看到最下方执行setCssToHead方法时候的传入参数。隐约可以看出来是我们在index.wxss之中写入的样式。但是仔细一看,格式不太一样了,变成了结构化数据,方便遍历处理,并且处理后便于makeup组装。还哪里不一样了呢,可以看到其中在index.wxss中写rpx单位的属性都变成了区间的样子[0, 128][0, 20]。其他单位并没有转换。这样的话就可以方便的识别哪里写了rpx单位,然后执行第二部分的transformRPX方法即可。

makeup组装之后,创建<style>标记,插入到<head>中。

这就是整个 WXSS 编译后得到的结果,编译后的 JS 代码是通过eval方法注入执行,这样的话完成了WXSS的一整套流程。


-EOF-


wxapp_basic
http://example.com/2024/05/02/微信小程序基础知识/
作者
John Doe
发布于
2024年5月2日
许可协议