WeChat Development

微信小程序开发

pikadetective
pikadetective

快速上手

目录结构

以 app 开头的文件是应用程序级别的文件,更改一处全局生效。页面 pages 的配置优先级高于全局配置。小程序所有页面放在 pages 目录,以单独文件夹形式存在。

// 基本项目目录
./
├── app.js => 项目的全局入口
├── app.json => 项目的全局配置文件
├── app.wxss => 项目的全局样式文件
├── components
├── miniprogram_npm
├── node_modules
├── package-lock.json
├── package.json
├── pages => 页面文件夹
│     ├── index => 首页文件夹 => 逻辑、标记、样式、配置文件
│     └── logs => 日志文件夹
├── project.config.json => 项目的配置文件 => appid
├── sitemap.json => 配置小程序与页面是否允许被微信索引
├── static
└── utils => 第三方工具

配置文件

app.json 是当前⼩程序的全局配置,包括了⼩程序的所有页⾯路径、界⾯表现、⽹络超时时间、底部 tab 等。

{
  "pages":[ // 定义页面路径
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window":{ // 页面通用配置
    "backgroundTextStyle":"light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "WeChat",
    "navigationBarTextStyle":"black"
  },
  "tabBar": { // 底部 tab 栏的表现
    "list": [{
      "pagePath": "pages/index/index",
      "text": "首页"
    }, {
      "pagePath": "pages/logs/index",
      "text": "日志"
    }]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "debug": true,
  "style": "v2", // 样式版本 => 需要旧版本直接删除
  "sitemapLocation":"sitemap.json" // 指明 sitemap.json 存放位置
}

project.config.json 是项目配置文件,用来记录对小程序开发工具所做的个性化配置。

{
  "description": "Project configuration file, more information: https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
  "packOptions": {
    "ignore": [],
    "include": []
  },
  "setting": { // 编译相关的配置
    "urlCheck": false,
    "es6": true,
    "enhance": true,
    "postcss": true,
    "preloadBackgroundData": false,
    ...
    "useApiHook": true,
    "useApiHostProcess": true,
    "babelSetting": {
      "ignore": [],
      "disablePlugins": [],
      "outputPath": ""
    },
    "enableEngineNative": false,
    "bundle": false,
    "useIsolateContext": true,
  },
  ...
  "appid": "<YOURID>", // 小程序的账号ID
  "projectname": "<随意不影响小程序名>", // 项目名称
  "condition": {},
  "editorSetting": {
    "tabIndent": "insertSpaces",
    "tabSize": 2
  }
}

sitemap.json

微信现已开放小程序内搜索,效果类似于 PC 网页的 SEO。sitemap.json 文件用来配置小程序页面是否允许微信索引。当开发者允许微信索引时,微信会通过爬虫的形式,为小程序的页面内容建立索引。当用户的搜索关键字和页面的索引匹配成功的时候,小程序的页面将可能展示在搜索结果中。

sitemap 的索引提示是默认开启的,如需要关闭 sitemap 的索引提示,可在小程序项目配置文件 project.configjson 的setting 中配置字段 checkSiteMap 为 false。

// 所有页面都会被微信索引 => 默认情况
{
  "rules":[{
    "action": "allow",
    "page": "*"
  }]
}
// 配置 path/to/page 页面不被索引,其余页面允许被索引
{
  "rules":[{
    "action": "disallow",
    "page": "path/to/page"
  }]
}
// path/to/page 页面被索引,其余页面不被索引
{
  "rules":[{
    "action": "allow",
    "page": "path/to/page"
  }, {
    "action": "disallow",
    "page": "*"
  }]
}
// 包含 a 和 b 参数的 path/to/page 页面会被微信优先索引,其他页面都会被索引
{
  "rules":[{
    "action": "allow",
    "page": "path/to/page",
    "params": ["a", "b"],
    "matching": "inclusive"
  }, {
    "action": "allow",
    "page": "*"
  }]
}
/*
path/to/page?a=1&b=2 => 优先被索引
path/to/page?a=1&b=2&c=3 => 优先被索引
path/to/page => 不被索引
path/to/page?a=1 => 不被索引
其他页面由于命中第二条规则 => 不会被索引
由于优先级的问题 => 第三条规则是没有意义的
*/
{
  "rules":[{
    "action": "allow",
    "page": "path/to/page",
    "params": ["a", "b"],
    "matching": "inclusive"
  }, {
    "action": "disallow",
    "page": "*"
  }, {
    "action": "allow",
    "page": "*"
  }]
}

小程序启动的过程是先解析 app.json 全局配置文件,再执行 app.js 小程序入口文件,调用 App 函数创建小程序实例。

页面渲染的过程是先加载解析页面的 .json 配置文件,再加载页面的 .wxml 模板和 .wxss 样式,然后执行页面的 js 文件,调用 Page 函数创建页面实例。

app.js => 整个小程序项目的入口文件
页面的 .js 文件 => 页面的入口文件,通过调用 Page() 函数来创建并运行页面

逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。

注册原理

每个小程序都需要在 app.js 中调用 App 方法注册小程序实例,绑定生命周期回调函数、错误监听和页面不存在监听函数等。整个小程序只有一个 App 实例,是全部页面共享的。开发者可以通过 getApp 方法获取到全局唯一的 App 实例,获取App上的数据或调用开发者注册在 App 上的函数。

// app.js
App({
  onLaunch (options) {
    // Do something initial when launch.
  },
  onShow (options) {
    // Do something when show.
  },
  onHide () {
    // Do something when hide.
  },
  onError (msg) {
    console.log(msg)
  },
  globalData: 'I am global data'
})
// xxx.js
const appInstance = getApp()
console.log(appInstance.globalData) // I am global data

视图层

视图结构

视图层由 wxml 与 wxss 编写,常以组件来进行展示。将逻辑层的数据反映成视图,同时将视图层的事件发送给逻辑层。wxml wx markup language 用于描述页面的结构。wxs wx script 是小程序的脚本语言。wxss wx style sheet 用于描述页面的样式。组件 Component 是视图的基本组成单元。

/* WXML 和 HTML 区别 */
// 标签名称不同
HTML (div, span, img, a)
WXML (view, text, image, navigator)
// 属性节点不同
<a href="#">超链接</a>
navigator url="/pages/home/home"›</navigator>
// 类似于 vue 中的模板语法
数据绑定\列表渲染\条件渲染
/* WXSS 和 CSS 的区别 */
// 新增 rpx 尺寸单位
CSS 中需要手动进行像素单位换算 => rem
WXSS 在底层支持新的尺寸单位rpx => 在不同大小的屏幕上小程序会自动进行换算
// 有区别的全局的样式和局部样式
// WXSS 仅支持部分 CSS 选择器
.class、#id、element、并集选择器、后代选择器、::after、::before 等伪类选择器
// 支持@import引入
@import '需导入的外联样式相对路径'

wxml 模板语法

小程序使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层实例的数据。所有模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。

常用事件

小程序中的事件传参比较特殊,不能在绑定事件的同时为事件处理函数传递参数。因为小程序会把 bind\<EventType> 的属性值,统一当作事件名称来处理,相当于要调用一个名称为 Handler(\<args>) 的事件处理函数。可以为组件提供 data-* 自定义属性传参,其中 * 代表的是参数名,最终 * 内容会被解析为参数的名字,对应数值会被解析为参数的值。

<!-- 错误 -->
<button type="primary" bindtap='btnHandler(<args>)'>事件传参</button>
<!-- 正确 -->
<!-- e.target.dataset.xxx 获取 -->
<button bindtap="btnHandler" data-info="{{2}}">事件传参</button>

rpx

rpx responsive pixel 是微信小程序独有的,用来解决屏适配的尺寸单位。其实现原理是鉴于不同设备屏幕的大小不同,把所有设备的屏幕,在宽度上等分为750份,即当前屏幕的总宽度为750rpx。在较小的设备上,1rpx 所代表的宽度较小;在较大的设备上,1rpx 所代表的宽度较大。小程序在不同设备上运行的时候,会自动把 rpx 的样式单位换算成对应的像素单位来渲染,从而实现屏幕适配。

建议用屏幕宽度为 375px 的 iPhone6 作为视觉稿的标准。在 rpx 与 px 之间的单位换算上,屏幕宽度为 375px,共有 750 个物理像素,等分 750rpx。

750rpx = 375px = 750 物理像素
1rpx = 0.5px = 1 物理像素

数据请求

跨域问题只存在于基于浏览器的 web 开发中。由于小程序的宿主环境不是浏览器,而是微信客户端,所以小程序中不存在跨域的问题。Ajax 技术的核心是依赖于浏览器中的 XMLHttpRequest 对象,所以小程序中不能叫做发起 Ajax 请求,而是叫做发起网络数据请求,常放入 onLoad 生命周期。

假设在微信小程序中需请求某域名(非 IP 地址或 localhost)下的接口。配置步骤是登录微信小程序管理后台 => 开发 => 开发设置 => 服务器域名 => 修改 request 合法域名。

页面导航

页面导航有两种方式,其一为在页面上声明一个 <navigator> 的导航组件,通过点击此组件实现页面跳转的声明式导航;其二为调用小程序的导航 API,实现页面跳转的编程式导航

  • 声明式导航

使用 <navigator> 组件跳转到配置或未被配置为 tabBar 的页面时,需指定 url 和 open-type 属性,前者表示要跳转的页面的地址,必须以 / 开头,后者表示跳转的方式,依情况选择 switchTab 或者 navigate。

url 属性在用来指定待跳转页面路径的同时,其后还可以携带参数。参数与路径之间使用 ?分隔,参数键与参数值用 = 相连,不同参数用 & 分隔。

// 配置为 tabBar
<navigator url="/pages/message/message" open-type="switchTab">导航到消息页面</navigator>
// 未被配置为 tabBar
<navigator url="/pages/info/info" open-type="navigate">导航到info页面</navigator>
// 可省略
<navigator url="/pages/info/info">导航到info页面</navigator>
// 路由带参
<navigator url="/pages/info/info?name=zs&age=20">跳转到info页面</navigator>

后退页面导航需要指定 open-type 和 delta 属性,前者的值是 navigateBack,表示要进行后退导航,后者的值必须是数字,表示要后退的层级。如果只是后退到上一页面,则可以省略 delta 属性,因其默认值是 1。

<navigator open-type='navigateBack' delta='1'>返回上一页</navigator>

wx.switchTab|navigateTo(Object object) 方法表示跳转到配置|未被配置为 tabBar 的页面。wx.navigateBack(Object object) 方法可以返回上一页面或多级页面。其中应注意参数对象的属性。

<button bindtap="gotoMessage">跳转到消息页面</button>
<button bindtap="gotoInfo">跳转到info页面</button>
<button bindtap="goBack">后退</button>
// 编程式导航跳转页面 => 配置tabBar
gotoMessage() {
  wx.switchTab({
    // 路由带参
    url:'/pages/message/message'
  })
}
// 编程式导航跳转页面 => 未配置tabBar且带参
gotoInfo() {
  wx.navigateTo({
    url: "/pages/info/info?name=zs&age=20"
  })
}
// 编程式导航后退页面
goBack() {
  wx.navigateBack({
    delta:1
  })
}

通过声明式导航传参或编程式导航传参所携带的参数,可以直接在 onLoad 事件中直接获取到。

// 导航目标页js
data:{
  // 导航传递过来的参数对象
  query: {}
},
...
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options){
  console.log(options)
  this. setData({
    query: options
  })
}

逻辑层

wxs

wxml 无法调用在页面 js 中定义的函数,但是 wxml 可以调用 wxs 中定义的函数。因此小程序中 wxs 的典型应用场景就是过滤器

/* 虽然 wxs 的语法类似于 js,但是 wxs 和 js 是完全不同的两种语言. */
// wxs 有自己的数据类型
number 数值类型、string 字符串类型、boolean 布尔类型、object 对象类型、function 函数类型、array 数组类型、date 日期类型、regexp正则
// wxs 不支持类似于 ES6 及以上的语法形式
不支持:let const 解构赋值、展开运算符、箭头函数、对象属性简写
支持:var定义变量、 普通 function 两数等类似于 ES5 的语法
// wxs 遵循 CommonJS 规范
module对象、require() 函数、module.exports 对象
  • 内嵌 wxs 脚本

wxs 代码编写在 wxml 文件中的 <wxs>标签内,就像 js 代码可以编写在 html 文件中的 <script> 标签内一样。wxml 文件中的每个 <wxs></wxs> 标签,必须提供 module 属性,用来指定当前 wxs 的模块名称,方便在 wxml 中访问模块中的成员。

<view>{{m1.toUpper(username)}}</view>
<wxs module="m1">
  //将文本转为大写形式
  module.exports.toUpper = function(str) {
    return str.toUpperCase()
  }
</wxs>
  • 外联 wxs 脚本

wxs 代码可以编写在以 .wxs 为后缀名的文件内,就像 js 代码可以编写在以 .js 为后缀名的文件中一样。在 wxml 中引入外联的 wxs 脚本时,必须为 <wxs> 标签添加 module 和 src 属性,其中 module 用来指定模块的名称,src 用来指定要引入的脚本的路径,且必须是相对路径。

// 定义外联 wxs
// tools.wxs
function toLower (str) {
  return str.toLowerCase()
}
module.exports = {
  // 不支持对象属性简写
  toLower: toLower
}
// 调用 m2 模块中的方法
<view>{{m2.toLower(country)}}</view>
// 引用外联的 tools.wxs 脚本并命名为 m2
<wxs src="../../utils/tools.wxs" module="m2"></wxs>

自定义组件

为保证目录结构清晰,在项目根目录创建 components 文件夹后,应在其内创建具体组件文件夹。右键具体组件文件夹选择 "新建 Component",输入组件名称,会自动生成组件对应的文件。

组件和页面都是 js、json、wxml 和 wxss 组成。但是组件和页面的 js 与 json 文件有明显的不同。组件的 json 文件中需要声明 "component":true 属性;组件的 js 文件调用的是 Component() 函数,页面调用 Page() 函数;组件的事件处理函数需要定义在 methods 节点中,而页面只需定义于与 data 平级的位置。

  • 局部引用

在页面的.json 配置文件中引用组件的方式称为局部引用。若某个组件只在特定页面被用到,推荐局部引用。

// 在页面的 .json 文件中引入组件
{
  "usingComponents": {
    // 直接写test => 编译后四个文件组成一个test组件
    "my-test1": "/components/test1/test1"
  }
}
// 在页面的 .wxml 文件中使用组件
<my-test1></my-test1>
  • 全局引用

在 app.json 全局配置文件中引用组件的方式称为全局引用。若某个组件在多个页面频繁使用,推荐此引用。

// 在app.json 文件中引入组件
{
  "pages":[/* 省略 */],
  "window":{/* 省路 */},
  "usingComponents":{
    "my-test2":"/components/test2/test2"
  }
}
// 在页面的 .wxml 文件中使用组件
<my-test2></my-test2>

组件和引用组件的页面建议使用 class 选择器,不要使用 id、属性、标签选择器,否则可能存在样式污染的问题。默认情况下,自定义组件的样式隔离特性能够防止组件内外样式的互相干扰,可以通过 styleIsolation 修改组件的样式隔离选项。

// 在组件的 js 文件中新增如下配置
Component({
  options:{
    styleIsolation:"isolated"
  }
})
// 或在组件的 json 文件中新增如下配置
{
  "styleIsolation": "isolated"
}

在自定义组件的 wxml 结构中,可以提供一个 <slot> 节点,用于承载组件使用者提供的具体内容结构。简而言之,在封装组件后其中存在部分节点内容是需要通过组件使用者进行提供,故放置一个占位符,即插槽。

① 属性绑定 => 用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容的数据,不能传递方法。
② 事件绑定 => 用于子组件向父组件传递数据,可以传递任意数据。
③ 获取组件实例 => 父组件可以通过 this.selectComponent 获取子组件实例对象,直接访问子组件的任意数据和方法。

/* 属性绑定 */
// 子组件js => 接收
Component({
  properties:{
    count:Number
  }
})
// 子组件wxml => 使用
<view>子组件count值{{count}}</view>
// 父组件js => 定义数据
data:{
  count:6
}
// 父组件wxml => 属性绑定
<my-test count="{{count}}"></my-test>
/* 事件绑定 */
// 1.父组件js定义函数,以自定义事件的形式传递给子组件
syncCount(){
  console.log('syncCount')
  // 获取传递回数据的操作...
}
// 2.父组件wxml通过自定义事件将定义函数的引用传递子组件
// 方式一
<my-test count="{{count}}" bind:sync="syncCount"></my-test>
// 方式二
<my-test count="{{count}}" bindsync="syncCount"></my-test>
// 3.子组件js通过 this.triggerEvent('自定义事件名',{/*参数对象*/}) 将数据发送
// 子组件wxml
<text>子组件中,count值为 => {{count}}</text>
<button type="primary" bindtap="addCount">+1</button>
// 子组件js
methods:{
  addCount(){
  this.setData({
    count: this.properties.count + 1
  })
  this.triggerEvent("sync", {value: this.properties.count})
  }
}
// 4.父组件js通过e.detail获取子组件传递的数据
syncCount(e){
  this.setData({
    count: e.detail.value
  })
}
/* 父组件调用this.selectComponent("id|class选择器")获取子组件实例对象 */
// 父组件wxml
<my-test count="{{count}}" bind:sync="syncCount" class="customA" id="cA"></my-test>
<button bindtap="getChild">获取子组件实例</button>
---
getChild(){ //按钮的 tap 事件处理函数
  // 切记下面参数不能传递标签选择器'my-test',不然返回的是 null
  const child = this.selectCompoment(".customA") // 也可以传递 id 选择器 #cA
  // 测试调用子组件的方法 => 注意是child不是this
  child.setData({ count: child.properties.count + 1 }) // 调用子组件的 setData
  child.addCount() // 调用子组件的 addCount 方法
}

behaviors 实现组件间代码共享的特性,类似于 Vue 的 mixins。在小程序中组件可能存在部分代码一致的情况,此时没有必要把这些代码在每个组件都进行声明,而是封装出来,需要时进行引入。

目前小程序中已支持使用 npm 安装第三方包,但是使用上存在部分限制。不支持依赖于 Node.js 内置库的包;不支持依赖于浏览器内置对象的包以及不支持依赖于 C++ 插件的包。

Vant Weapp 有赞前端团队开源的小程序UI组件库可以帮助开发者快速搭建应用。常搭配 CSS 自定义属性实现定制主题。值得注意的是小程序页面的根节点为 page。

# !当前工程目录
# 初始化包管理配置文件
npm init
npm i @vant/weapp@1.3.3 -S --production
# 构建npm包见官方文档

API Promise 化是通过额外的配置,将官方提供的基于回调函数的异步 API 升级改造为基于 promise 的异步 API,从而提高代码的可读性、维护性,避免回调地狱的问题。

# 主要依赖于 miniprogram-api-promise 第三方包
npm install --save miniprogram-api-promise@1.0.4

不能直接使用 node_modules 内的包,而是需构建生成 miniprogram_npm 后才可使用。此时若已经存在有内容的后者文件夹,再想导入新包选择直接构建的方式容易出现异常,建议在删除此文件夹后,通过工具选项再次进行构建较为稳健。

// 只需入口文件 app.js 调用一次 promisifyAll() 方法就可实现异步 API Promise 化
import { promisifyAll } from 'miniprogram-api-promise'
const wxp = wx.p = {}
// promisify all wx's api
promisifyAll(wx, wxp)
// wxml
<van-button type="danger" bindtap="getInfo"> vant button </van-button>
// js文件事件处理函数
async getInfo(){
  const {data:res} = await wx.p.request({
    method:'GET',
    url:'https://www.xxx.cn/api/get',
    data:{
      name:'zs',
      age:20
    }
  })
  console.log(res) // Promise => Object
}

全局数据共享

全局数据共享又称状态管理,常用于解决组件之间数据共享的问题。小程序通过 mobx-miniprogram 创建 Store 实例对象;mobx-miniprogram-bindings 把 Store 中共享的数据和方法绑定到组件或者页面使用。

# 稳定版本安装后建议删除miniprogram_npm再一次重构npm
npm install --save mobx-miniprogram@4.13.2 mobx-miniprogram-bindings@1.2.1

分包

分包指的是把一个完整的小程序项目,按照需求划分为不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。对小程序进行分包处理可以优化小程序首次启动的下载时间以及在多团队共同开发时能有更好的解耦协作。

不分包的小程序项目中所有的页面和资源都会被打包在一起,导致整个项目体积过大,影响首次启动的下载时间。分包后的小程序项目由一个主包和多个分包组成。主包一般只包含项目的启动或 TabBar 页面,以及分包都需要用到的公共资源;分包只包含和当前分包有关的页面及私有资源。

uni-app

配置见官方文档。

使用 Git 管理项目

在项目根目录中新建 .gitignore 忽略文件,并配置如下。

# 忽略 node_modules 目录
/node_modules
/unpackage/dist

由于忽略了 unpackage 目录中仅有的 dist 目录,因此默认情况下,unpackage 目录不会被 Git 追踪。此时为了让 Git 能够正常追踪 unpackage 目录,可以在 unpackage 目录下创建一个叫做 .gitkeep 的文件进行占位。完成后可在根目录进行 Git 初始化操作。

wepy

WePY@2.x.x 变化较大,本文版本 WePY@1.7.3。v1.x.x 与 v2.x.x 相比 node—modules 文件夹一致,而前者的 dist 文件夹在后者中更新为 weapp,存放实时编译后代码。

异常处理

  • 初始化报错
$ wepy init standard project_name
# 网络状态异常
downloading template   wepy-cli · Failed to download repo standard: read ECONNRESET

1.更换网络,注意kxsw。
2.使用本地下载的初始化 template 进入 WePYGithub,选择下载需要的分支至本地,最后初始化项目由 wepy init standard project_name 修改为 wepy init 下载文件中 standard 存放路径 project_name。

  • ESLint 语法报错 => 在对应报错目录中删除多余空行
.../src/app.wpy
15:1  error  More than 1 blank line not allowed  no-multiple-empty-lines
✖ 1 problem (1 error, 0 warnings)
.../src/pages/index.wpy
91:1  error  More than 1 blank line not allowed  no-multiple-empty-lines
✖ 1 problem (1 error, 0 warnings)
  • 未找到入口 app.json 文件、或文件读取失败
[ app.json 文件内容错误] app.json: app.json 未找到

补全项目根目录下的 project.config.json 文件缺少的代码,注意路径补全的书写正确。

"miniprogramRoot": "/dist"
  • VSC 配置 Vetur 后 .wpy 文件报错 => Settings 选项内关闭部分勾选项
Validation Script、Validation Style、Validation Template
  • MiniProgramError module "wxs/test.wxs.js" is not defined

把报错的 test.wxs 后缀加上 .js 可以消除报错。产生的主要原因是缓存问题,待不报错后修改回即可。

  • 运行时未定义
ReferenceError: regeneratorRuntime is not defined

1.将新运行时文件引入报错的文件中,这里注意在模块内 regeneratorRuntime 是以 runtime 运作,且此方法存在重新编译失效的问题。
2.增强编译且更换版本可以较为简便的解决此问题,但是也存在切换版本后错误的复现。

Bugs 解决

  • 微信支付操作

微信商户平台下载证书 apiclient_cert 和 apiclient_key,存入对应框架的位置。后续应在微信支付前产生相关订单;控制平台余额不要过大;在商户平台配置安全设置以增加安全性。

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!