-
下载微信开发者工具并安装
下载链接 -
建立一个todolist demo
该demo只为帮助熟悉微信小程序的开发,项目可以使用不使用云服务
html对应于小程序的wxml,常用html标签例如div,p等在wxml中可以使用view标签代替,微信小程序开发中除了使用js与ts,微信还提供了wxs作为可选的脚本语言。
一个JSX语法的标签
<div className="demo">{val}</div>
换成wxml标签如下
<view class="demo">{{val}}</view>
wxml中的监听事件
wxml中的标签
如果index.js文件中包含一个Page()构造器,说明该文件对应微信小程序中的一个页面,如果想通过小程序下方的tab点击跳转到对应页面,需要在app.json文件中注册,代码如下:
{
"pages": [
"pages/home/index",
"pages/todolist/index"
],
"tabBar": {
"list": [
{
"pagePath": "pages/home/index",
"text": "home"
},
{
"pagePath": "pages/todolist/index",
"text":"todo"
}
]
},
}
pages对应页面所在文件的路径,tabBar对应下方tab中显示标签的文本,以及对应的页面,tabBar样式的设置也在app.json中。 关于tabBar具体设置请参考。
react中可以通过react-router中的link标签实现路由跳转,微信小程序可以通过提供的navigator标签实现。
关于navigator标签及路由跳转详情请参考
react class component中在constructor中定义state初始数据可能需要的代码如下:
...
constructor(props){
super(props);
this.state={
year:2021,
month:12,
day:12.22
}
}
在微信小程序中定义一个页面的初始数据,应在对应页面文件夹下的index.js文件中定义,代码如下:
page({
data:{
year:2021,
month:12,
day:12.22
}
...
})
在该文件夹下的文件中访问data对象下的属性,只需填入所需属性的名称,大概如下:
<view>{{year}}</view>
与定义初始数据方式相同,可以在page()中定义页面加载时执行的函数,以及其他一些自定义的事件响应函数,例如handleClick等等。
具体请参考
react中 class component常常在componentDidMount中发送请求获取数据,在微信小程序中对应的生命周期为onLoad。当class component中state发生变化时会触发componenDidUpdate生命周期,在微信小程序中则没有对应的生命周期。具体生命周期请参考
react中实现受控组件
<input
onChange={(event)=>{this.setState({inputValue:event.target.value})}}
value={this.state.inputValue}/>
微信小程序中实现了类似vue中实现的双向绑定,语法较为接近,代码如下:
<input model:value="{{inputValue}}"
在vue中:
<input v-model="inputValue"/>
当输入框状态发生变化,index.ts中data的inputValue属性会自动更新,input的初始值即为data.inputValue的值.如不想实现受控组件,只想设置input的初始状态,只需要设置value:
<input value="{{inputValue}}">
具体请参考
class component对state进行更新是通过this.setState({year:2020})实现的,在微信小程序中写法类似,通过this.setData({year:2020})语句更新。
如果修改的时候通过 this.data.year=2020
这种方式修改,虽然值会发生改变,但如果<view>
标签中绑定了该属性值,当该属性发生变化时,UI层不会同步更新(同react一样),故不建议通过这种方式更新。(虽然在二者写法十分接近,但实现的效果有所不同,具体在todolist demo中有写)
function component中可以利用useEffect钩子函数,当一个或多个由useState定义的变量或props传递的变量发生变化时执行对应的副作用函数。在微信小程序中,可以通过自定义组件中提供的数据监听器实现同样的效果,假设有如下场景,变量c依赖于变量a,b当a,b发生变化时,c需要同步更新.
在react中可能需要的代码如下:
const [a,setA] = useState(5);
const [b,setB] = useState(10);
const [c,setC] = useState(15);
useEffect(()=>{
setC(a+b)
},[a,b])
在微信小程序中,通过数据监听器observers实现,在自定义组件中实现相同的效果的代码如下:
Component({
...,
data:{
a:5,
b:10,
c:15
},
observers:{
"a,b":function(a,b){
this.setData({c:a+b})
}
}
})
react中参数的传递可能有以下几种情况:
- 父子组件通过props传递
- 兄弟组件通过共享父组件状态,在props中加入修改state的回调函数,控制状态.
如果有一个变量需要被多个组件或页面获取,可以将该变量储存在app.js文件中,并通过以下方式获取,代码如下:
app({
globaldata:{
username:'tom',//username需要被多个组件和页面获取
...
},
onLanuch:()=>{},
onShow:()=>{},
})
页面想要获取该state的代码如下:
pages/subPage1/index.js
const app = getApp();
const username = app.globaldata.username
如果想在页面中对该state进行修改,直接修改即可:
pages/subPage1/index.js
const app = getApp();
app.globalData.username = 'lihua';
console.log(app.globalData.username) //lihua
如果有:
//pages/subPage1/index.js
const app = getApp();
const username = app.globalData.username;
Page({
data:{
username:username
},
setUserName(event){
app.globalData.username="lihua";
console.log(this.data.username) //tom
console.log(app.globalData.username) //lihua
}
})
//pages/subPage1/index.wxml
...
<view bindtap="setUserName">{{username}}</view>
当点击时app.globalData.username会发生变化,但是页面的data中的username不会发生改变,故view标签中的值也不会发生改变。
自定义组件的嵌套场景下,在父组件中可以通过this.selectComponent获取子组件的实例对象,从而获取子组件的一些状态.
//pages/subPage1/index.js
Page({
data: {},
getChildComponent: function () {
const child = this.selectComponent('.my-component');//请先确保父组件注册了子组件
console.log(child.data) //{...}
}
})
另外一种方式,可以通过自定义监听事件的方式实现父子组件之间的传值,具体可参考todolist demo中的使用.也可参考
react中进行一个列表渲染可能需要的代码如下:
<ul>
{this.state.dataArray.map((element,index)=><li>index is {index},and the value is{element}</li>)}
</ul>
微信小程序中实现相同效果的列表渲染代码如下:
<view wx:for="{{dataArray}}">the index is{{index}},and the value is{{item}}</view>
数组下标变量名默认为index,当前项变量名为item,若数组当前项为对象,可以通过item.xx读取对应的属性。
具体请参考
react中通过变量控制一个组件的渲染可能需要的代码如下
const isRender:boolean = this.state.isRender
return(
<div>
{isRender&&<p>some text</p>}
</div>
)
微信小程序中实现相同效果的条件渲染代码如下
<view wx:if="{{isRender}}">some text</view>
具体请参考
react中不存在模版的概念,如果想根据输入值生成可复用的jsx可能需要的代码如下
...
const fn=({year,month,day})=>
<div>
<p>year:{year}</p>
<p>month:{month}</p>
<p>day:{day}</p>
</div>
...
return(
<div>
{fn(data)}
</div>
)
微信小程序中可以通过模版实现相同效果:
在wxml文件中定义一个模版
<template name="date">
<view>
<view>year:{{year}}</view>
<view>month:{{month}}</view>
<view>day:{{day}}</view>
</view>
</template>
在wxml中插入对应name的模版:
<template is="date" data="{{item}}"/>
// pages/todolist/index.ts
Page({
data:{
todo:[],
finished:[]。
content:null, //储存todo的内容
date:null //储存todo的时间
}
})
// pages/todolist/index.wxml 一个简单的界面
<view>
<view>this is a todolist</view>
<input placeholder="请输入todo的内容" model:value="{{content}}" />
<input placeholder="请输入todo对应的时间" model:value="{{date}}" />
<button bindtap="addTodo">添加todo</button>
<view>
<view>已完成</view>
<view wx:for="{{todo}}">内容:{{item.content}},日期:{{item.date}}</view>
</view>
<view>
<view>未完成</view>
<view wx:for="{{finished}}">内容:{{item.content}},日期:{{item.date}}</view>
</view>
</view>
在app.json中注册对应的页面即路由,可以通过底部的tabBar点击跳转到todolist页面。
// app.json
{
...
"pages": [
"pages/home/index",//不是必要的页面
"pages/todolist/index"
],
"tabBar": {
"list": [
{
"pagePath": "pages/home/index",//不是必要的页面
"text": "home"
},
{
"pagePath": "pages/todolist/index",
"text":"todo"
}
]
},
}
上面代码input已经完成与content和date的双向绑定。新增todo通过点击button将此时输入框的内容保存到this.data.todo中并清除输入框的内容,如果输入框内容为空则创建一个toast提示。完成todo通过点击todo将对应部分移至finished中。代码如下:
// page/todolist/index.ts
Page({
...
++ addTodo() {
if (this.data.content === null || this.data.date === null) {
wx.showToast({
title: "请填入内容与时间",
icon:"none",//有图标时最多显示7个字符
duration: 2000
})
}
else {
const item = {
content: this.data.content,
date: this.data.date,
}
const newTodo: any = [...this.data.todo, item];
this.setData({
todo: newTodo,
content: null,
date: null
})
}
},
++ finishTodo(event: any) {
const targetIndex = event.target.dataset.index;
let newToDo: any = this.data.todo;
const item = this.data.todo[targetIndex];
newToDo.splice(targetIndex, 1);
const finished: any = [...this.data.finished, item];
this.setData({
todo: newToDo,
finished: finished
})
},
++ deleteTodo(event: any) {
const targetIndex = event.target.dataset.index;
const content = (this.data.finished[targetIndex] as any).content;
let finished = this.data.finished;
finished.splice(targetIndex, 1);
this.setData({
finished: finished
})
wx.showToast({
title: `已完成:${content}`,
icon:"none"
})
}
})
// page/todolist/index.wxml
...
<view>
<view>已完成</view>
-- <view wx:for="{{todo}}">内容:{{item.content}},日期:{{item.date}}</view>
++ <view wx:for="{{todo}}" data-index="{{index}}" bindtap="finishTodo">内容:{{item.content}},日期:{{item.date}}</view>
</view>
<view>
<view>未完成</view>
-- <view wx:for="{{finished}}">内容:{{item.content}},日期:{{item.date}}</view>
++ <view wx:for="{{finished}}" data-index="{{index}}" bindtap="deleteTodo">内容:{{item.content}},日期:{{item.date}}</view>
</view>
至此,在输入框输入todo的内容以及时间之后,点击“添加todo”即可新增todo,点击某一项todo之后会将该todo移至已完成,点击已完成的某一项todo会将其删除。
为了创造上述场景做出的改变如下:
创建一个与page同级的文件夹名为components,在components文件夹中创建一个unfinish文件夹,该文件夹中包含index.js,index.wxml,index.wxss,index.json 四个文件。
在index.js中有如下代码:
// components/unfinish/index.js
Component({
properties: {
todo: Array,
},
})
上面代码表示unfinish文件夹下包含一个自定义组件,该自定义组件接受父组件传递给其一个名为todo的参数,该参数是一个数组。
为了声明该文件夹下包含一个自定义组件,需要在index.json文件中声明:
//components/unfinish/index.json
{
"component":true
}
在其他页面中使用自定义组件,需要在该页面的json文件中声明,本例子中我们将在todolist页面中使用该自定义组件,故作出如下修改:
// pages/todolist/index.json
{
...
++ "usingComponents": {
"unfinish":"../../components/unfinish/index",
}
}
自定义组件输出的wxml模版写在该文件夹的index.wxml中如下:
// components/unfinsih/index.wxml
<view>
<view>未完成</view>
<checkbox-group>
<view wx:for="{{todo}}" wx:key="{{item.content}}">
<checkbox value="{{index}}">内容:{{item.content}},日期:{{item.date}}</checkbox>
</view>
</checkbox-group>
</view>
我希望这个自定义组件能完成这些事情:
- 根据父组件传递的todo数组列表渲染生成checkbox数组
- 当点击某一个checkbox标志该事件已完成时,父组件中将该事件从todo数组中移动到父组件的finished数组中
- 当长按某一个checkbox标志该事件需要被删除,将会弹出一个modal,若确认,父组件应将该事件从todo数组中删除,否则隐藏modal。
为此我需要做的是给checkbox添加监听事件,点击事件或长按事件触发时,父组件中todo数组和finished数组发生对应改变。
如果在react框架中实现上述逻辑可以通过父组件传递给子组件一个包含setTodo(...),setFinished(...)的函数,将对应改变后的数组填入即可。
在微信小程序中。实现上述逻辑的方式是,父组件中声明一个标志着checkbox发生更改的自定义事件,当子组件点击一个checkbox时通过this.triggerEvent触发该自定义事件,并传递给其一个数组下标,在父组件中编写该事件被触发时执行的回调函数,同样道理设置删除的逻辑。
为此新增的代码如下:
// components/unfinish/index.js
Component({
properties: {
todo: Array,
},
methods: {
finishTodo(event) {
const index = event.detail.value[0];
this.triggerEvent("finishtodo", index);
},
deleteTodo(event) {
const index = event.currentTarget.dataset.index
this.triggerEvent("deltodo", index)
}
}
})
// components/unfinsih/index.wxml
<view>
<view>未完成</view>
<checkbox-group bindchange="finishTodo">
<view wx:for="{{todo}}" wx:key="{{item.content}}">
<checkbox value="{{index}}" bindlongpress="deleteTodo" data-index="{{index}}">内容:{{item.content}},日期:{{item.date}}</checkbox>
</view>
</checkbox-group>
</view>
// pages/todolist/index.ts
page({
...
finishTodo(event: any) {
const index = event.detail
const item = this.data.todo[index];
let newTodo = this.data.todo;
newTodo.splice(index, 1);
let newFinished = [...this.data.finished, item];
this.setData({
todo: newTodo,
finished: newFinished,
})
},
delTodo(event: any) {
const index = event.detail;
wx.showModal({
title: "将永久删除该todo",
content: "此操作无法撤销",
success: () => {
let newTodo = this.data.todo;
newTodo.splice(index, 1);
this.setData({
todo: newTodo
})
}
})
},
// pages/todolist/index.wxml
...
-- <view>
-- <view>已完成</view>
-- <view wx:for="{{todo}}" data-index="{{index}}" bindtap="finishTodo">内容:{{item.content}},日期:{{item.date}}</view>
-- </view>
++ <unfinish bind:finishtodo="finishTodo" bind:deltodo="delTodo"todo="{{todo}}" id="unfinish" update="{{update}}" />
同样的,我将原先todolist/index.wxml中生成已完成列表的部份拆成单独的一个自定义组件,定义方式同上面相似.
该组件实现的功能是点击已完成的checkbox,对应的事件将会从父组件的finished数组中移动到unfinish数组中。当长按时将会弹出一个modal询问是否永久删除该事件,确认则永久删除否则隐藏modal。
定义该子组件过程省略,下面给出实现该逻辑的代码:
// components/finished/index.js
Component({
properties: {
finished: Array
},
methods: {
cancelFinish(event) {
const selected = event.detail.value;
let index = 0;
if (selected.length > 0) {
while (true) {
if (selected.indexOf(`${index}`) == -1) {
break;
} else index++;
}
}
this.triggerEvent("cancelfinish", index);
},
deleteFinished(event) {
const index = event.currentTarget.dataset.index;
this.triggerEvent("delfinished", index)
}
}
})
// components/finished/index.wxml
<view>
<view>已完成</view>
<checkbox-group bindchange="cancelFinish">
<view wx:for="{{finished}}" data-index="{{index}}">
<checkbox value="{{index}}" checked="{{true}}" bindlongpress="deleteFinished" data-index="{{index}}">内容:{{item.content}},日期:{{item.date}}</checkbox>
</view>
</checkbox-group>
</view>
//pages/todolist/index.wxml
...
-- <view>
-- <view>未完成</view>
-- <view wx:for="{{finished}}" data-index="{{index}}">内容:{{item.content}},日期:{{item.date}}</view>
</view>
++ <finished bind:cancelfinish="cancelFinish" bind:delfinished="deleteFinished" finished="{{finished}}" id="finished"/>
...
完成上述工作,理论上todolist已经完成了.这个时候会发现一个问题,当点击一个未完成的todo时,他会移动到已完成列表,但是此时,未完成列表的下一项会自动变为已完成状态。出现这个现象的原因是什么?
在react中,当组件接受的参数发生变化时,组件会重新渲染。在这个例子中,因为checkbox标签是根据父组件传递todo数组进行列表渲染生成,所以点击未完成todo会移动至已完成列表证明组件的状态的确已经更新了,下一个未完成todo自动变为已完成的原因是,checkbox.group标签中的部分状态没有发生更新。上述代码中todo的移动通过在checkbox.group标签上添加change事件完成,回调函数接受的event.detail.value数组中包含着已选中的项,当选中一个checkbox时对应下标会出现在数组中,但是当该checkbox被移动至已完成列表时,value数组的值并没有发生改变,所以下一个checkbox的下标自动减一就被选中了。这证明该子组件并没有发生react中整个组件的重渲染,而是只进行了与变化数据相关的标签的重渲染,所以checkbox.group的状态没有按照预期的变为原始状态。解决这个问题的方法是手动重置checkbox.group的状态,给checkbox标签添加checked属性,checked属性的值为子组件data中的一个boolean值,每一次移动todo都触发this.setData({checked:true}),这样相当于每次checkbox.group的状态都被重置为原始状态,就解决了checkbox会自动变为选中状态的原因。变动代码如下:
// components/unfinsih/index.js
...
++ data: {
++ checked: false
++ },
methods: {
finishTodo(event) {
const index = event.detail.value[0];
this.triggerEvent("finishtodo", index);
++ this.setData({
++ checked: false
++ })
},
deleteTodo(event) {
const index = event.currentTarget.dataset.index
this.triggerEvent("deltodo", index);
++ this.setData({
++ checked: false
++ })
}
// components/unfinish/index.wxml
...
-- <checkbox value="{{index}}" bindlongpress="deleteTodo" data-index="{{index}}">内容:{{item.content}},日期:{{item.date}}</checkbox>
++ <checkbox value="{{index}}" checked="{{checked}}" bindlongpress="deleteTodo" data-index="{{index}}">内容:{{item.content}},日期:{{item.date}}</checkbox>
//components/finished/index.js
...
++ data: {
++ checked: true
++ },
methods: {
cancelFinish(event) {
const selected = event.detail.value;
let index = 0;
if (selected.length > 0) {
while (true) {
if (selected.indexOf(`${index}`) == -1) {
break;
} else index++;
}
}
this.triggerEvent("cancelfinish", index);
++ this.setData({
++ checked: true
++ })
},
//components/finished/index.wxml
...
-- <checkbox value="{{index}}" checked="{{true}}" bindlongpress="deleteFinished" data-index="{{index}}">内容:{{item.content}},日期:{{item.date}}</checkbox>
++ <checkbox value="{{index}}" checked="{{checked}}" bindlongpress="deleteFinished" data-index="{{index}}">内容:{{item.content}},日期:{{item.date}}</checkbox>
至此,一个todolist已经完成。
上面代码中我们为了每次更新todo的时候checkbox的状态也随之更新,手动更新了checked。这种情况可以考虑添加一个对todo的observer,代码如下:
//components/unfinish/index.js
...
finishTodo(event) {
const index = event.detail.value[0];
this.triggerEvent("finishtodo", index);
-- this.setData({
-- checked: true
-- })
},
deleteTodo(event) {
const index = event.currentTarget.dataset.index
this.triggerEvent("deltodo", index);
-- this.setData({
-- checked: true
-- })
}
},
++ observers:{
++ "todo":function(){
++ this.setData({checked:false})
}
}
同样在unfinish组件中对todo添加监听,这样将setData部分单独写在observer中可以理解为todo或finished改变的“副作用”。
自定义组件具体请参考
checkbox相关请参考
组件间传参请参考
todolist代码仓库
- 微信小程序iOS 环境下的 Promise 是一个使用 setTimeout 模拟的 Polyfill。这意味着 Promise 触发的任务为普通任务,而非微任务,进而导致 在 iOS 下的 Promise 时序会和标准存在差异。详情请参考
- 父子组件之间可以通过this.triggerEvent触发自定义事件达到通信目的,但是多层嵌套情况通过单个this.triggerEvent无法与更高层组件进行通信。
举个例子,现在有三个组件a,b,c,a是b的父组件,b是c的父组件,a传给b 参数prop1,b将prop1传递给c。如果a在引用b时添加customEvent监听事件,则b可以通过this.triggerEvent("customeEvent")与a通信,但是c不可以通过this.triggerEvent("customeEvent")与a通信。
为了实现目标效果,需要在b中对c添加customEvent事件,在该事件的响应函数中添加this.triggerEvent("customeEvent"),则c可以通过该事件与a通信。