小程序开发


前言

写在开篇

熟悉的也好,陌生的也好,方向要自己去寻找。

学做小程序,同时在学着用JS做算法,真的很想吐槽其堪称腌臜的输入方式,但是在做了一下青训的笔试题之后感到自己就像是膨胀的肥皂泡,来硬的一戳就破——还有太多路要走,还得加油!

每当我读文档、博客或者视频等各种学习资料,看到技术的发展和更迭,以及编写者、讲述人的故事和过往,我就仿佛在走近一段历史岁月,日新月异变迁中不仅是技术的进步,更有着人物的沧桑——大概真是如此,世上半数人都可谓传奇!

那么如果有一天,有人读到我这一篇博文,那么是否也会产生和我相仿的情感呢?

不论如何,计算机真是一个充满理想浪漫主义色彩的领域啊!就像是我曾看到一名物理学子高呼愿去宇宙的最深处追寻纯真的世界那般,我也希望通过二进制让世界成为我的画布——

现在,继续前行吧!

更新日志

2022/01/13
读了些文档,看了点视频,写了点代码,先把提纲理好

2022/01/14
我敲,今天下午一点才醒,我是废物啊

2022/01/15
我…下午两点半醒的….不说的,今天的题晚上再做,先把技术学了尽快上线一个小程序

2022/02/01
更新自定义组件部分

2022/02/02
更新走近小程序、进阶开发技巧和分包

走近小程序

小程序发展史

2017-2018

2018-2019

2019-2020

当然,2020-2022也有很多发展,不过这里就不一一例举了

各种小程序

各种小程序

语法

众所周知,小程序开发语言的语法是参照了前端框架的,具体参照对照如下表

Vue 美团(mpvue)、网易(megalo)、京东(taro)、Hbuilder(uni-app)、腾讯(?)
React 蚂蚁金服(remax)、京东(taro)

WXML

常用基础标签

标签太多了,这里只举出基础的、常见的标签的部分知识点,更多细致内容参考官方文档

view

块级,可以看做div标签
其衍生的标签还有scroll-view,可以看做加上了滚动条的div标签

block

行内元素(就离谱,明明叫block)

相当于span标签

image

不用我说也知道是img

用法基本一致

swiper

轮播图,结合其衍生的swiper-item标签配套使用

具体使用方式和大部分组件库中的轮播图一致

text

行内元素

特点是能够长按选中复制

衍生的rich-text标签能够当做iframe使用

button

和原来一样,但是注意点击事件在这里变成了按下-抬起事件(bindtap)

全局配置

在app.json中:

pages选项里依葫芦画瓢地添加新的内容后,就添加了新页面的存放路径,并且将会自动创建其相关文件

window中可以全局设置小程序窗口的外观,包括navigationBar(指的是手机最上面显示电量的那一行),background(下拉时可见)和页面主体部分

tabBar就是设置tabBar,tabBar就是平时写的navigator那种东西,这里可以设置位置、路由等等

style中可以设置是否启用新版组件,其中v2是新版,注释这一段代码即可回到旧版

具体配置细则可参见官方文档

常用小程序API

事件监听API

同步API

异步API

协同开发权限管理

全局、局部数据和方法

数据

点进index.js一看,哎呀好家伙,这不是咱mustache吗!

虽然知道mustache语法支持运算,但是一直没有意识到可以进行三目运算——如果可以进行三目运算的话,那么就可以配合逗号运算符做一些奇妙的操作了

事件

还记得事件的概念吗?不记得就倒立喝水同时大声朗读下述文字

事件是渲染层到逻辑层的通讯方式,通过事件可以将用户在渲染层产生的行为,反馈到逻辑层进行业务的处理

可以说,事件是渲染层到逻辑层的通讯方式

独特的参数传递方式

小程序的事件传参比较特殊,比如下述代码

是错误的
是错误的
是错误的

<button type="primary" bindtap="Count(123)">+1</button>

上述内容在微信小程序中是被理解为一个叫做”Count(123)”的方法,传递参数的正确形式应该是下面这样

其中data-xxx=”{{}}“是传递参数的格式,xxx为参数的名称,而花括号内的则为参数的值

(如果不使用mustache传参,那么传递的就是文本字符串)

<button type="primary" bindtap="Count" data-abc="{{123}}">+1</button>

其中的bindtap是绑定触屏事件,也可以写做bind:tap
类似的还有bindinput绑定输入事件,bindchange绑定状态改变事件等等(更多的事件自己查官方文档)

data-xxx=“yyy”,表示传递一个参数xxx,值为yyy,其中data-是固定写法

函数中则通过调用事件对象即可拿到参数:

e.currentTarget.dataSet

条件渲染和列表渲染

其实说的就是wx:if=””和wx:for=””

同样地,要使用mustache语法,而且对于列表渲染来说,最好指定一个wx:key=””(没错就是因为diff算法)

WXSS

几乎完全移植了CSS的选择器和常用的样式属性(意思是也有相当一部分属性没有被移植),然后也进行了一些扩展加入了很多特有的内容(比如超强的自适应单位rpx,还有@import导入CSS)

rpx

总是会把宽度750等分,然后根据实际尺寸做相应的适配

与px的换算就很简单了,直接750rpx == (实际尺寸大小)px就可以了

@import

在WXSS中使用,其后加上路径即可将其他文件中的WXSS导入进来

当然,如果有需要的话,可以考虑在app.wxss中写,这个就是直接对全局生效的

网络数据请求

安全性

和AJAX的区别

微信小程序开发是基于客户端的,而AJAX的核心技术是依赖浏览器的XMLHttpRequest对象的,所以小程序中这种并不是AJAX请求,而是发起网络数据请求,而且不存在跨域问题

请求权限

需要先登录微信开发后台,然后配置一下域名信息,只有设置了目标URL等相关信息才能进行相关URL下的网络数据请求

相关使用

通过配置wx.request({})发起网络数据请求,

其中的wx对象和window对象类似,可以看做是BOM的一种

WXS

页面导航

页面导航是指的页面之间的相互跳转

在浏览器中我们一般采用两种形式实现上述功能,一种是a标签,另一种是location.href

而在小程序中,类似地,我们可以通过navigator标签,或者小程序的导航API来实现

这两个例子中,前者都可以认为是声明式导航,后者均可以认为是编程式导航

声明式导航


<navigator url="/pages/index/index" open-type="switchTab">导航1</navigator>
<navigator url="/pages/" open-type="navigate">导航2</navigator>
<a href="https://serio.gitee.io">跳转3</a>

第一种形式是导航到tabBar页面,何为tabBar页面?即app.json中配置了路径的页面

第二种则是用于导航到非tabBar页面

第三种似乎在微信小程序中失效了?(但是标签有自动补全,具体情况有待考证)

另外,我们可以设置open-type为navigateBack使点击这个按钮的效果变为回退,并且可以设置delta为n从而实现指定回退n级页面

编程式导航

wx.switchTab({
   url: 'url',
 })
wx.navigateTo({
   url: 'url',
 })

同样,

第一种是导航到tabBar页面,

第二种是导航到非tabBar页面

并且也能设置后退,此处不再赘述,具体信息查看官方文档

导航传参

就是url?yyy=xxx&yy=xx这样的

要知道左下角可以查看页面参数

而且,参数可以直接在生命周期函数onload的默认参数中找到

下拉刷新 和 上拉触底

注意上拉和下拉,上拉是手指由下往上(下拉反之),说的是手指不是页面

下拉刷新

分为全局和局部,但是都是将enablePullDownRefresh设置为true

另外,下拉刷新的loading效果不会主动消失,需要手动处理,这时候只需要调用wx.stopPullDownRefresh()就可以了

除了这种loading以外,还可以通过wx.showLoading来主动展示并且通过wx.hideLoading来隐藏

上拉触底

手指向上滑动,从而加载更多数据

大部分时候用于实现分页数据请求

实现是在js文件中,通过onReachBottom()监听

所谓触底,也并非和底部距离为0,默认是50rpx,可以通过在配置文件中修改onReachBottomDistance属性来更改

另外,记得对上拉触底做一下节流处理,其中的一个技巧是,方法内存在生命周期函数complete,利用好这一点可以更方便地处理

生命周期

Life Cycle,和Vue差不多,具体有什么生命周期函数直接看文档,这里挑重点记一下概念

这里的生命周期存在一个分类:

应用生命周期

启动->运行->销毁

页面生命周期

加载->渲染->销毁

其中,页面的生命周期范围较小,应用程序的生命周期范围较大,总的来看,整体的周期是:

启动->页面1的生命周期->页面2的生命周期->etc…->销毁

WXS脚本

WeiXin Script,小程序独有的一套结构

和JS类似但是不一样,本质上还是两种语言,WXS有以下几个比较重要的特点:

1.有自己的数据类型

2.不支持类似ES6及以上的语法形式

3.遵循CommonJS

看到这些我就大胆猜测一手,一定就是你——NodeJS!

4.有隔离性,一是wxs和JS不能相互使用,而是wxs不能调用小程序的API

5.性能更好,IOS设备上WXS会比JS快2-20倍,但是在Android上无差异

我缓缓打出一个?

wxml中无法调用页面级js文件中定义的函数,但是wxml可以调用wxs中的函数。所以小程序中的wxs应用的典型场景就是 过滤器(没错就是filter)

使用

内嵌式

在wxml文件中写一个wxs标签,并且每一个wxs标签都必须具有一个module属性用于指明当前wxs的模块名称,此后在wxml中可以将wxs当做一个对象来使用,可以通过module属性指明的名称来访问其中定义的方法和变量——没错嘛这不就是还没exports的模块的使用方法吗

那么,这和过滤器有什么关系呢?

没错,我们这里可以处理数据,但是又不会修改js文件中的数据,这就是过滤的效果

外联式

目录下创建一个文件,然后单独写,最后exports即可

但是导入不是用require,而是利用wxs中的src属性

自定义组件

简单使用

1.创建组件
在项目的根目录中,鼠标右键,创建components -> test文件夹
在新建的components -> test文件夹上,鼠标右键,点击“新建Component”
键入组件的名称之后回车,会自动生成组件对应的4个文件,后缀名分别为.js,.json,.wxml和.wxss

组件和页面的区别:

1.组件的配置文件中的component字段值为true

2.组件的.js中调用的是Component()函数(可能是构造函数)

3.组件的事件处理函数需要定义到methods节点中

文件结构

不同于Vue,组件的wxml文件中并不要求唯一一个标签包裹其余所有标签

2.局部引入组件

在配置文件中写:

{
    "usingComponents": {
        "a-good-component": "/components/test/test1"
    }
}

其中test1是组件文件的名字

然后要使用这个组件的话,只需要在wxml中写:

<a-good-component></a-good-component>

3.全局引入组件

在全局配置中新增字段

"usingComponents":{
    "a-good-component": "/components/test/test1"
}

其余一样

4.区别于页面

  1. 组件的.json文件中需要声明”component” : true属性
  2. 组件的.js 文件中调用的是Component()函数组件的事件
  3. 处理函数需要定义到 methods节点中,而页面则是直接放到page内
  4. 页面的自定义函数传参可以通过event.target.dataset获取,组件则有专门的properties负责接收数据

样式隔离

组件之间的样式不会相互影响,也不会受到引用它的页面的样式的影响,包括全局样式也对组件无效

一个中肯但是不知道为什么如此的建议是:在组件样式中不要使用除了class以外的选择器

如果希望破坏这种隔离性的话,可以修改其配置文件中的styleIsolation字段的值为isolated

或者在js文件中写如下代码也能达到同样的效果:

Component({
    options: {
        styleIsolation: 'isolated'
    }
})

父向子传参

是熟悉的属性绑定语法

Component({
    // 属性定义的完整方式
    properties: { 
        type: Number, // 指定类型
        value: 10	  // 默认值
    }, 
    // 属性定义的简化版
    name: String      
})

传参直接如下图所示,不需要有额外的关键字,直接写就行了

<a-good-component name="沈俞佑"></a-good-component>

properties中可以写一些键值对,和data类似

1.properties和data都是可读可写的

2.data更倾向于存储私有组件的私有属性

3.properties更倾向于存储外界传递到组件中的值

4.this.data === this.properties 的结果是true,所以使用this.setData也是可以对this.properties起作用的

数据监听器

类似于Vue的watch,是监听数据而不是事件

在组件的methods方法列表内除了写方法之外还能写监听器等

添加以下字段对n1,n2进行监听

observers: {
    'n1, n2': function (newN1, newN2)
}

即使是在this.setData方法里面,后面的值也要用this.data.xxx访问

当然,也可以监听某个对象的属性——是个值都能监听

纯数据字段

只是一个概念性的东西,知道就行

指不用于界面渲染的data字段,既不会展示在页面上,也不会传递给其他组件,仅在其所属组件的内部使用

组件生命周期

不再赘述

插槽

单个slot没什么好说的,就当看一遍复习了

<view class="wrapper">
  <view>小广告</view>
  <slot></slot>  
</view>  
<ad>
  	<image 				src="https://p.qlogo.cn/hy_personal/3e28f14aa0516842d7aa5377124ce70be528a816990658073ed67ea9851b805d/0.png">	</image>
</ad>

当然,如果有多个插槽,那么操作就有点不一样了

首先要在启用多slot的组件的js文件中 与 data等字段平级的位置添加一个options字段,并设置multipleSlots: true

然后又是一套基本操作了

<view class="wrapper">
  <slot name="before"></slot>
  <view>小广告</view>
  <slot name="after"></slot>  
</view>  
<ad>
  	<view slot="after">slot写上插槽名字就好了</view>	
</ad>

子向父传参

是学过但是依旧不是很熟练的自定义事件

(以前每次都是去现查一遍再用…并且能不用就不用,就算用了也会因为搞不清到底有几个事件而把所有事件都命名相同)

使用步骤如下:

  1. 在父组件的js 中,定义一个函数
  2. 在父组件的wxml中,通过自定义事件的形式,将步骤1中定义的函数引用,传递给子组件
  3. 在子组件的js中,通过调用this.triggerEvent('自定义事件名称',{ /*参数对象*/ }),将数据发送到父组件
  4. 在父组件的js中,通过e.detail获取到子组件传递过来的数据

简明扼要说一下就是:

父级给个函数,子级用this.triggerEvent自定义一个事件,在父级用e.detail就能接收到数据

绑定事件的时候需要用到关键字bind:xxx,注意这里似乎没有缩写为:xxx这样的语法糖,但是可以直接写作bindxxx(离谱吧,没错我也觉得离谱)

比如bindtap实际上也可以写作bind:tap

知道真相的我大为震撼!

获取组件实例

也可以用这种方式来进行父子间通信

在父组件中调用**this.selectComponent( id选择器/class选择器 )**来获取子组件的实例对象,这样就能够使用子组件内部的所有数据了

<ad id="ad-one">
  	<image src="https://p.qlogo.cn/hy_personal/3e28f14aa0516842d7aa5377124ce70be528a816990658073ed67ea9851b805d/0.png">		 	</image>
</ad>
<button bindtap="getChild" data-name="{{'#ad-one'}}">获取#ad-one的信息</button>
getChild(e) {
  console.log(this.selectComponent(e.currentTarget.dataset.name).data.msg)
}

behaviors

behaviors是小程序中,用于实现组件间代码共享的一种特性,类似Vue中的mixins(mixins是啥啊)

每个behavior都可以包含一组属性、数据、生命周期函数和方法,
组件引用它时,属性、数据和方法等z会合并到组件中,
behavior也可以引用behavior

总结一下
wxml有模板可以用,那么这里就大胆地把JS也搞一个模板,这个模板就是behavior

创建一个behavior

导入一个behavior

注意behavior字段的值是以数组的形式进行存储的

使用behavior的数据

那么什么时候使用behavior呢?

还记得曾经苦恼过的——开发vue项目的过程中,总是感慨应该把复用多次的xxx函数先封装到一个JS文件里面的——这时候我们就该想到behavior了!

但是这时候又出现了新的问题,如果behavior中属性和调用组件已有的属性冲突,那么这些属性将会怎么被处理呢?这就是我们需要思考的新问题了

同名字段的覆盖和组合规则

组件和它引用的behavior中可以包含同名的字段,此时可以参考如下3种同名时的处理规则

1.同名数据字段(data)

  1. 如果都是对象类型,那么会发生对象合并(那么问题来了,这时候属性名有重复了咋办)
  2. 其余情况则和↓↓↓第2点↓↓↓中提到的一致

2.同名属性(properties和methods)

  1. 组件自身的优先
  2. behavior内部出现重复属性则靠后的优先
  3. behavior调用behavior时,父级优先

3.同名生命周期函数

  1. 不同生命周期则会依次执行
  2. 相同生命周期则和↑↑↑第2点↑↑↑中的一致

进阶开发技巧

使用npm

可以使用npm但是在小程序中多了如下三点限制:

  1. 不支持依赖于Node.js内置库的包
  2. 不支持依赖于浏览器内置对象的包
  3. 不支持依赖于C++插件的包

当时我就知道能用的没多少了

但是我们可以安装 Vant Weapp、uni-app等UI库帮助我们开发

当然,小程序里面使用npm安装也没那么方便就是了,还得手动配置一些东西…

npm下载之后东西会放到node_module文件夹下,

但是小程序不争气啊没法直接用,还得先构建才能用

构建

根目录下有个文件夹:miniprogram_npm

这之中的包可以被小程序直接使用,而要构建这个文件夹,首先需要正常npm操作一波,然后点击这里:

之后如果再添加新的包,那么再构建npm可能会不生效,这时候我们把miniprogram_npm文件夹删了重新构建就好了

当然,不同的包在安装的时候可能有不同的要求

具体安装和使用方式参考官方文档,此处不再赘述

API的Promise化

Promise是为了解决回调地狱而出现的

在小程序中要使用Promise的话需要先用npm安装一下

npm install --save miniprogram-api-promise

此后构建一下miniprogram_npm,之后在app.js中书写如下代码进行全局promise化

import {promisifyAll} from 'miniprogram-api-promise'

const wxp = wx.p = {}

promisifyAll(wx, wxp)

在这之后,wx对象中的所有API都会有一个pormise化的版本,而这些新版本会被放到wxp和wx.p对象上

页面的js文件中,定义对应

全局数据共享

概念

没错,可以类比Vuex、Redux、Mobx等等

也称为状态管理

小程序的实现方式是通过mobx-miniprogram配合mobx-miniprogram-bindings实现全局数据共享

其中,
mobx-miniprogram用来创建Store实例对象
mobx-miniprogram-bindings用来把Store中的共享数据或方法绑定到组件或页面中使用

安装

npm install --save mobx-miniprogram mobx-miniprogram-bindings

然后又是基本的构建操作

创建store实例

在根目录下创建Store文件夹,里面再新建一个store.js,我们将用这个js文件专门来创建store实例对象

import {observable} from 'mobx-miniprogram'

export const store = observable({
    // 数据字段
    num1: 1,
    num2: 2
    // get修饰符: 计算属性,类似于watch
    get sum() {
    	return this.num1 + this.num2
	},
	// actions 方法,用来修改 store 中的数据,
  	// 外界调用updateNum即可触发内部函数
     updateNum1: action(function(step) {
         this.num1 += step
     }),
     updateNum2: action(function(step) {
         this.num2 += step
     })
})

页面绑定store

import { createStoreBindings } from 'mobx-miniprogram-bindings'
import { store } from '../../store/store'

Page({
    // 生命周期上挂一下
    onLoad: function () {
        this.storeBindings = createStoreBindings(this, {
            store,	// 数据源
            fields: ['num1', 'num2', 'sum'], // 当前页面需要的数据字段
            actions: ['updateNum1']	// 当前页面需要的方法
        })
    },
    onUnLoad: function () {
        this.storeBindings.destoryStoreBindings()
    }
})

页面使用store

// 依旧是this访问就行了
xxxFun(e) {
	this.updateNum1(e.target.dataset.step)   
}

组件绑定store

要额外给一个storeBindingsBehavior来过渡,

暂时不清楚为什么要这么设计

import { storeBindingsBehavior } from 'mobx-miniprogram-bindings'
import { store } from '../../store/store'

Component({
    behaviors:[storeBindingsBehavior], // 通过storeBindingsBehavior自动绑定
    storeBindings:{
        store,
        fields: {
            num1: () => store.num1,   // 绑定字段的第1种方式
            num2: (store) => store.num2,  // 绑定字段的第2中方式
            sum: 'sum'	// 绑定字段的第3种方式,前两者也可以这么写
        }
    }
})

使用还是那么使用

如果需要在wxml中使用,那么遵循mustache语法使用就好了

分包

概念

把一个完整的小程序分为不同的子包,用户使用时可以按需进行加载

好处就是优化了小程序首次启动的下载时间,在多团队合作的之后有更好的解耦协作

分包前后对比

分包前

分包之后,小程序变为由1个主包 + n个分包

主包
一般只能包含项目的启动页面或TabBar页面、以及所有分包需要用到的一些公共资源

分包
只包含和当前分包有关的页面和私有资源

分包后

分包目录结构

可以在app.json文件中的subpackages字段里面以类似路由配置的形式进行管理

app.json内的subpackages字段

如果编辑这一部分,那么保存后会自动创建响应的文件

subpackages之内是写的分包,之外的内容则是主包

注意分包之内不能相互嵌套

注意事项

加载规则

  1. 小程序启动时,默认会下载主包并启动主包内页面

(所以tabBar放主包里面)

  1. 用户访问某个分包页面的时候,客户端会把对应包下载下来

我寻思,这不就是个懒加载吗,说的这么高级….

大小限制

一个包不能超过2M,所有包加起来不能超过20M

(当然这个具体数值是随着技术发展不断增加的,但是一定要知道每个包大小是需要控制的)

我寻思着公共资源文件夹里面塞个图片那不是直接爆炸吗…

独立分包

一种特殊的分包,可以不依赖主包独立运行

独立分包与其他分包之间相互隔绝,不能相互引用资源

这种设计应该应用于什么样的场景呢?

有的功能不依赖主包即可运行,但是进入小程序时却需要默认下载主包——这时候就该独立分包了

而设置独立分包的操作也非常容易实现,只需要在app.json对每个包的设置中加上independent字段并设置为true

预下载

在进入某个页面时,由框架自动预先下载好之后可能需要的分包,从而优化进入后续分包页面的速度

在app.json中使用preloadRule字段配置预下载:

一个分包的预下载大小限额:2M,超出的部分会下载失败

发布

(这部分后面再更)


文章作者: Serio
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Serio !
  目录