小程序 从入门到实践开发

1,444 阅读25分钟

微信小程序

微信小程序介绍(可点击)
      小程序是一种新的开放能力,开发者可以快速地开发一个小程序。
      小程序可以在微信内被便捷地获取和传播,同时具有出色的使用体验。
      微信小程序的特点:免安装、操作更接近原生 APP、必须在微信中使用
      开放注册范围:个人、企业、政府、媒体、其他组织。
      微信小程序和微信的原生功能应用在本质上是一样的 —— 都是 Web App。 Web App 就是一种通过 H5 页面技术实现的,和 Native App的功能和界面几乎一样的手机 App 形态。
    
微信小程序的优势(可点击)
      1. 微信有海量用户,而且粘性很高,在微信里开发产品更容易触达用户;
      2. 推广 App 或 公众号的成本太高。
      3. 开发适配成本低。
      4. 容易小规模试错,然后快速迭代。
      5. 跨平台。
      
微信小程序对于创业者的优势(可点击)
      1. App 流量成本的急剧攀上
      2. 移动互联网格局基本已定,用户主要需求场景已被巨头把持
      3. 面向所有产品对用户时间的竞争
      
小程序宣传方式(可点击)
      - 小程序搜索入口 & 附近的小程序
      - 扫一扫、长按识别小程序码
      - 好友分享、群分享
      - 关联公众号
      - 第三方的小程序应用商店
      - 小程序之间相互跳转
      
小程序开发流程(可点击)
      1. 注册
      在微信公众平台注册小程序,完成注册后可以同步进行信息完善和开发。
	  2. 小程序信息完善
      填写小程序基本信息,包括名称、头像、介绍及服务范围等。
      3. 开发小程序
      完成小程序开发者绑定、开发信息配置后,开发者可下载开发者工具、参考开发文档进行小程序的开发和调试。
      4. 提交审核和发布
      完成小程序开发后,提交代码至微信团队审核,审核通过后即可发布(公测期间不能发布)。
      
微信小程序注册(可点击)
      - 通过公众号注册(避免了重复认证)(暂不支持个人类型公众号复用资质创建小程序)。
      - 进入[微信小程序官网](https://mp.weixin.qq.com/wxopen/waregister?action=step1)注册。(访问[注册页面](https://mp.weixin.qq.com/cgi-bin/registermidpage?action=index&lang=zh_CN&token=),耐心完成注册即可)
      注册注意事项
      · 个人主体无法完成支付,卡包,搜索附近小程序功能。
      · 使用邮箱进行注册时一个邮箱仅能申请一个小程序。
      · 邮箱不能使用注册过公众号、开发平台、企业号及绑定过个人号的邮箱。
      · 进行资料完善时保证信息准确性,主体信息一旦填写无法进行修改。
      · 上传企业基本资料时需要签名加盖公章,保证图片清晰度,否则导致审核不通过。
      · 已经申请微信公众号的企业可直接在首页中点击小程序进入下一步。
      
微信开发者工具使用(可点击)
      新建项目
      当符合以下条件时,可以在本地创建一个小程序项目
      1. 需要一个小程序的 AppID;如没有 AppID,可以选择申请使用测试号。
      2. 登录的微信号需要是该 AppID 的开发者;
      3. 需要选择一个空目录,或者选择的非空目录下存在 app.json 或者 project.config.json。当选择空目录时,可以选择是否在该目录下生成一个简单的项目。
      |
	  多开项目
      工具支持同时打开多个项目,每次打开项目时会从新窗口打开,入口有以下几种:
      1. 从项目选择页打开项目,处于项目窗口时可以从菜单栏的项目 -> 查看所有项目打开项目选择页
      2. 从菜单栏的最近打开项目列表中打开的项目会从新窗口打开
      3. 新建项目
      4. 命令行或 HTTP 调用工具打开项目
      |
      管理项目
      对本地项目进行删除和批量删除。
      |
      主界面
      开发者工具主界面,从上到下,从左到右,分别为:菜单栏、工具栏、模拟器、目录树、编辑区、调试器 六大部分。
      主界面
      |
      菜单栏
      - 切换帐号
      - 关于
      - 检查更新
      - [开发者论坛](https://developers.weixin.qq.com/community/develop/mixflow)
      - [开发者文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)
      - 调试
      - 更换开发模式:快速切换公众号网页调试和小程序调试
      - 推出
      |
      项目
      - 新建项目
      - 打开最近
      - 查看所有项目
      - 关闭当前项目
      |
      文件
      - 新建文件
      - 保存
      - 保存所有
      - 关闭文件
      |
      编辑:可以查看编辑相关的操作和快捷键
      |
      工具
      - 编译:编译当前小程序项目
      - 刷新:与编译的功能一致,由于历史原因保留对应的快捷键 ctrl(⌘) + R
      - 编译配置:可以选择普通编译或自定义编译条件
      - 前后台切换:模拟客户端小程序进入后台运行和返回前台的操作
      - 清除缓存:清除文件缓存、数据缓存、以及授权数据
      |
      界面:控制主界面窗口模块的显示与隐藏
      |
      设置:
      - 外观设置:控制编辑器的配色主题、字体、字号、行距
      - 编辑设置:控制文件保存的行为,编辑器的表现
      - 代理设置:选择直连网络、系统代理和手动设置代理
      - 通知设置:设置是否接受某种类型的通知
      |
      工具栏
      点击用户头像可以打开个人中心,在这里可以便捷的切换用户和查看开发者工具收到的消息。
      工具栏中间,可以选择普通编译,也可以新建并选择自定义条件进行编译和预览。
      通过切后台按钮,可以模拟小程序进入后台的情况
      工具栏上提供了清缓存的快速入口。可以便捷的清除工具上的文件缓存、数据缓存、还有后台的授权数据,方便开发者调试。
      工具栏右侧是开发辅助功能的区域,在这里可以上传代码、版本管理、查看项目详情
      |
      模拟器
      在模拟器底部的状态栏,可以直观地看到当前运行小程序的场景值,页面路径及页面参数
      |
      独立窗口
      点击 模拟器/调试器 右上角的按钮可以使用独立窗口显示 模拟器/调试器
      |
      项目页卡
      详情
      - 基本信息
      包括图标、AppID、第三方平台名(只有第三方平台的开发小程序才会显示)、目录信息、上次提交代码的时间以及代码包大小。
      - 本地设置
      开发者可以在此选择任意基础库版本,用于开发和调试旧版本兼容问题。
      - 项目配置
      |
      项目设置
       代码保护
        主要是对文件进行扁平化处理并替换 require 引用的文件名,以下情况不适合使用此功能
        1. 对于小程序只有简单页面的情况下,开启此功能效果不佳
        2. 有文件超过 500kb,且其中有使用 require 引用项目中的文件的情况,在运行时可能会报文件没有找到
        3. 动态引用的情况,如 var a = 'somefile.js'; require(a);
        4. 将 require 函数赋值给其他变量的情况,如 var a = require; a('somefile.js');
        5. 将 require 作为二元运算符的参数的情况,如 require + 1; 6.使用 ... 运算符且未开启 ES6 转 ES5 的情况
      不校验请求域名及 TLS 版本
      域名信息
      详情/项目配置
      将显示小程序的安全域名信息,合法域名可在 mp 管理后台进行设置。
      |
      设置
       外观设置
        - 主题
        - 调试器主题
        - 字体
        - 字号
        - 行距
        - 模拟器位置
       编辑设置
        - 文件保存
        - 编辑器
        - Tab 大小
       代理设置
        可以配置不使用代理,或使用系统代理,或使用自定义代理。
       安全设置
        可以开启和关闭 CLI/HTTP 调用功能
     |
     代码编辑
       文件格式
         因 iOS 下仅支持 UTF8 编码格式,最新版本的开发者工具会在上传代码时候对代码文件做一次编码格式校验。
       文件支持
         工具目前提供了 5 种文件的编辑:wxml、wxss、js、json、wxs 以及图片文件的预览。
       文件操作
         新建页面有两种方式
         1. 在目录树上右键,选择新建 Page,将自动生成页面所需要的 wxml、wxss、js、json
         2. 在 app.json 的 pages 字段,添加需要新建的页面的路径,将会自动生成该页面所需要的文件
    

微信小程序项目

项目目录结构

    demo
       |- /pages
          |- /index
             |- index.js  // 页面逻辑文件  
             |- index.json  // 页面配置文件 
             |- index.wxml  // 页面模板文件  
             |- index.wxss  // 页面样式文件
       |- /utils
          |- utils.js
       |- app.js  // 小程序的全局配置文件 
       |- app.json
       |- app.wxss  // 全局样式
       |- project.config.json  // 项目配置文件
       |- sitemap.json

小程序文件结构和传统 Web 对比

结构传统 web微信小程序
结构HTMLWXML
样式CSSWXSS
逻辑JavaScriptJavaScript
配置JSON

小程序宿主环境

微信客户端给小程序所提供的环境为宿主环境。

框架

整个小程序框架系统分为两部分:逻辑层(App Service)和 视图层(View)。 其中 WXML 模板和 WXSS 样式负责视图层,JS 脚本负责逻辑层。

小程序的渲染层和逻辑层分别由 2 个线程管理: 渲染层的界面使用了 WebView 进行渲染;逻辑层采用 JsCore 线程运行 JS 脚本。

一个小程序存在多个界面,所以渲染层存在多个 WebView 线程, 这两个线程的通信会经由微信客户端(下文中也会采用 Native 来代指微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。

有关渲染层和逻辑层的详细文档参考:小程序框架

程序与页面

微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。 紧接着通过 app.json 的 pages 字段就可以知道你当前小程序的所有页面路径: 于是微信客户端就把首页的代码装载进来,通过小程序底层的一些机制,就可以渲染出这个首页。 小程序启动之后,在 app.js 定义的 App 实例的 onLaunch 回调会被执行: 整个小程序只有一个 App 实例,是全部页面共享的,更多的事件回调参考文档 注册程序 App 页面的事件回调参考文档

配置

全局配置: app.json

常用配置项

  • pages 页面路径列表
  • window 全局的默认窗口表现
  • tabBar 底部
  • tab 栏的表现
  • networkTimeout
  • 网络超时时间
  • debug 是否开启 debug 模式,默认关闭
{
  "pages": ["pages/index/index", "pages/logs/index"],
  "window": {
    "navigationBarTitleText": "Demo"
  },
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页"
      },
      {
        "pagePath": "pages/logs/logs",
        "text": "日志"
      }
    ]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "debug": true,
}

页面配置: page.json

页面中配置项会覆盖 app.json 的 window 中相同的配置项。

常用配置项

  • navigationBarBackgroundColor 导航栏背景颜色
  • navigationBarTextStyle 导航栏标题颜色,仅支持 black / white
  • navigationBarTitleText 导航栏标题文字内容
  • backgroundColor 窗口的背景色
  • backgroundTextStyle 下拉 loading 的样式,仅支持 dark / light
{
  "navigationBarBackgroundColor": "#ffffff",
  "navigationBarTextStyle": "black",
  "navigationBarTitleText": "微信接口功能演示",
  "backgroundColor": "#eeeeee",
  "backgroundTextStyle": "light"
}

JSON注释事项

JSON 文件都是被包裹在一个大括号中 {},通过 key-value 的方式来表达数据。JSON 的 Key 必须包裹在一个双引号中。
JSON 文件中无法使用注释,试图添加注释将会引发报错

JSON 的值只能是以下几种数据格式,其他任何格式都会触发报错,例如 JavaScript 中的 undefined。
数字,包含浮点数和整数
字符串,需要包裹在双引号中
Bool 值,true 或者 false
数组,需要包裹在方括号中 []
对象,需要包裹在大括号中 {}
Null

WXML 模板语法

数据绑定

<view>{{message}}</view>
Page({
  data: {
    message: 'Hello MINA!'
  }
})

组件属性

<view id="item-{{id}}"> </view>
Page({
  data: {
    id: 0
  }
})

三元运算

三元运算

<view hidden="{{flag ? true : false}}"> Hidden </view>

算数运算

<view> {{a + b}} + {{c}} + d </view>
Page({
  data: {
    a: 1,
    b: 2,
    c: 3
  }
})

字符串运算

<view>{{"hello" + name}}</view>
Page({
  data:{
    name: 'MINA'
  }
})

逻辑判断

<view wx:if="{{length > 5}}"> </view>
<view wx:elif="{{length > 2}}"> 2 </view>
<view wx:else> 3 </view

<block wx:if="{{true}}">
  <view> view1 </view>
  <view> view2 </view>
</block>

<view hidden = "{{length > 5}}">
</view>

列表渲染

<view wx:for="{{array}}" wx:key="index">
  {{index}}: {{item.message}}
</view>
Page({
  data: {
    array: [{
      message: 'foo',
    }, {
      message: 'bar'
    }]
  }
})
<view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
  {{idx}}: {{itemName.message}}
</view>

block

<block wx:for="{{[1, 2, 3]}}">
  <view> {{index}}: </view>
  <view> {{item}} </view>
</block>

引用

WXML 提供两种文件引用方式importinclude

  <!-- item.wxml -->
  <template name="item">
    <text>{{text}}</text>
  </template>
  <import src="item.wxml"/>
  <template is="item" data="{{text: 'forbar'}}"/>
<!-- index.wxml -->
<include src="header.wxml"/>
<view> body </view>
<include src="footer.wxml"/>

<!-- header.wxml -->
<view> header </view>

<!-- footer.wxml -->
<view> footer </view>

WXSS 样式

尺寸单位 rpx

rpx 与 px 转换 在 iphone6 下 (375) 2rpx = 1px

样式导入

@import "common.wxss";
.middle-p {
  padding:15px;
}

样式其他补充

<view style="color:{{color}};" />
<view class="normal_view" />

引入字体图标

支持 在线字体图标 或将字体图标转为base64 的格式后引入

@font-face {
  font-family: "minifont";
  src: url("//at.alicdn.com/t/font_2593266_on928f0odc7.woff2?t=1622818124627")
      format("woff2"),
    url("//at.alicdn.com/t/font_2593266_on928f0odc7.woff?t=1622818124627")
      format("woff"),
    url("//at.alicdn.com/t/font_2593266_on928f0odc7.ttf?t=1622818124627")
      format("truetype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

使用 less/sass vscode 安装 Easy LESS 或 Easy sass 加入配置

 'less.comile': {
   'outEXt': '.wxss'
 }

Flex 布局

align-items: baseline | stretch; // 基线对齐 | 拉伸

示例

      .container {
        display: flex;
        flex-direction: row;
        align-items: stretch;
        width: 375px;
        height: 375px;
        background: rgb(68, 67, 67);
      }

      [class*="item-"] {
        width: 100px;
      }

      .item-red {
        background: red;
      }

      .item-green {
        background: green;
      }

      .item-blue {
        background: blue;
      }
    <div class="container">
      <div class="item-red">
        1
      </div>
      <div class="item-green">
        2
      </div>
      <div class="item-blue">
        3
      </div>
    </div>

不给子元素高度,给父元素加上 align-items: stretch; ,子元素会被拉伸至父元素高度。

      .container {
        display: flex;
        flex-direction: row;
        align-items: baseline;
        width: 375px;
        height: 375px;
        background: rgb(68, 67, 67);
      }

      [class*="item-"] {
        width: 100px;
      }

      .item-red {
        background: red;
        font-size: 20px;
      }

      .item-green {
        background: green;
        font-size: 25px;
      }

      .item-blue {
        background: blue;
        font-size: 30px;
      }

align-items: baseline; 基线对其。

关于基线对其有牵扯出 css 行高与字体大小这里就不做过多解释了,有兴趣和不明白的可以去网上搜索 css line-height 会有很多详细的介绍。

flex-wrap: wrap; 换行

      .container {
        display: flex;
        flex-direction: row;
        flex-wrap: wrap;
        justify-content: center;
        align-items: center;
        width: 375px;
        height: 300px;
        background: rgb(68, 67, 67);
      }

      [class*="item-"] {
        width: 150px;
        height: 150px;
      }

      .item-red {
        background: red;
      }

      .item-green {
        background: green;
      }

      .item-blue {
        background: blue;
      }

当父元素已经设置了 justify-content: center;align-items: center; 时,再加上 flex-wrap: wrap;,如果此时父元素高度大于行数×子元素高度,那么行与行之间会产生间隙,把父元素高度设置为 行数×子元素高度 不失为一种粗糙的解决方案。

组件

容器 view

常用属性:hover-class 按下的样式类。

 <view hover-class="van-action-sheet"></view>

文本 text

常用属性:

  • selectable 文本是否可选(已废弃)
  • user-select 文本是否可选
  • decode 是否解码

图片 image

默认:宽 320px 高 240px

常用属性:

  • mode 图片裁剪 常用值有:aspectFit、widthFix

滑块视图容器 swiper

<view class="page-section page-section-spacing swiper">
    <swiper indicator-dots="{{indicatorDots}}"
        autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}">
        <block wx:for="{{background}}" wx:key="*this"> // *this 表示是 循环项 例如:[1,2,3]、['1','2','3']
          <swiper-item>
            <view class="swiper-item {{item}}"></view>
          </swiper-item>
        </block>
    </swiper>
</view>

偶尔用到 previous-margin、next-margin 来实现不一样的展示

页面链接 navigator

<view class="btn-area">
  <navigator url="/page/navigate/navigate?title=navigate" hover-class="navigator-hover">跳转到新页面</navigator>
  <navigator url="../../redirect/redirect/redirect?title=redirect" open-type="redirect" hover-class="other-navigator-hover">在当前页打开</navigator>
  <navigator url="/page/index/index" open-type="switchTab" hover-class="other-navigator-hover">切换 Tab</navigator>
  <navigator target="miniProgram" open-type="navigate" app-id="" path="" extra-data="" version="release">打开绑定的小程序</navigator>
</view>

视频 video

<video 
 id="myVideo" 
 src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" 
 danmu-list="{{danmuList}}" 
 enable-danmu 
 danmu-btn 
 controls>
</video>

可滚动视图区域 scroll-view scroll-into-view、scroll-with-animation 实现滑动动画

输入框 input

搜索框

<input confirm-type='search' bindconfirm='shop_search_function'/>
自定义组件 components
<view class="my_header">
  {{cData}}
  <view>
    <slot name="before"></slot>
    <slot name="after"></slot>
  </view>
</view>
Component({
  options: {
    multipleSlots: true // 在组件定义时的选项中启用多slot支持
  },
  // 组件之间代码共享
  // 属性、方法会后面的覆盖前面的,本身覆盖引用的 生命周期是叠加的
  behaviors: [myBehavior],
  properties: {
    // 这里定义了innerText属性,属性值可以在组件使用时指定
    innerText: {
      type: String,
      value: 'default value',
    },
    text: Number
  },
  externalClasses: ['f-class']  //  外部样式类
  data: {
    // 这里是一些组件内部数据
    someData: {}
  },
  observers: {
    // 数据监听器
    // 在监听的值内不要修改监听的值会造成递归
    'numberA, numberB': function(numberA, numberB) {
      // 在 numberA 或者 numberB 被设置时,执行这个函数
    }
    'someData.subfield': function(subfield) {
      // 使用 setData 设置 this.someData.some.subfield 时触发
      // (除此以外,使用 setData 设置 this.someData.some 也会触发)
    },
    'someData.list[12]': function(arr12) {
      // 使用 setData 设置 this.someData.arr[12] 时触发
      // (除此以外,使用 setData 设置 this.someData.arr 也会触发)
    },
    'someData.field.**': function(field) {
      // 使用 setData 设置 this.someData.some.field 本身或其下任何子数据字段时触发
      // (除此以外,使用 setData 设置 this.data.some 也会触发)
    },
  }
  methods: {
    // 这里是一个自定义方法
    customMethod(event){
        // 修改父组件传值
        let innerText = this.properties.innerText
        let text = this.properties.text
        this.setData({
          innerText: '更改后的值',
          text: '更改后的值'
        })
    	this.triggerEvent('posttap',{pid:1}) // 只会触发posttap
    	// 详细看 组件之间通信与事件
    }
  }
})
// my-behavior.js
module.exports = Behavior({
  behaviors: [],
  properties: {
    myBehaviorProperty: {
      type: String
    }
  },
  data: {
    myBehaviorData: {}
  },
  attached: function(){},
  methods: {
    myBehaviorMethod: function(){}
  }
})

组件注册

{
  "usingComponents":{
    "PageHeader": "page/to/"
  }
}

使用组件

<PageHeader 
  innerText="正在上映" 
  f-class="movie-list" 
  bind:customMethod="posttap">
  
  <view slot="before">这里是插入到组件slot name="before"中的内容</view>
  
  <!-- 这部分内容将被放置在组件 <slot name="after"> 的位置上 -->
  <view slot="after">这里是插入到组件slot name="after"中的内容</view>
  
</PageHeader>
  posttap(event){
    // 获取自定义事件传参
    console.log(event.detail.pid)
  }

在使用自定义组件时候 hidden 不起作用,如果需要使用 hidden , 需要在组件内使用 hidden。

  <!-- hidden 不会生效 -->
  <product-info hidden="{{true}}"/> 

自定义 tabbar

将小程序官方的 自定义 tabBar 示例中的 custom-tab-bar 复制到项目的根目录下(与 pages 同级)

配置信息 在 app.json 中的 tabBar 项指定 custom 字段,同时其余 tabBar 相关配置也补充完整。 所有 tab 页的 json 里需声明 usingComponents 项,也可以在 app.json 全局开启。

在 tabbar 页面 onShow() 页面中添加如下代码

  onShow() {
    if (typeof this.getTabBar === "function" && this.getTabBar()) {
      this.getTabBar().setData({
        selected: 2,  //  修改选中的 tabbar 0是第一个 tab
      });
    }
  },

自定义 navigation

我通过 bilibili 的视频找到一个比较好的 github 组件。地址我放在相关资料中了。

生成二维码

weapp-qrcode 使用方法

  <canvas style="width: 200px; height: 200px;" canvas-id="myQrcode"></canvas>
  //  引入 weapp.qrcode.js 文件
  import drawQrcode from '../../utils/weapp.qrcode.min.js'
​
  data: {
  },
  draw () {
    drawQrcode({
      width: 160,
      height: 160,
      x: 20,
      y: 20,
      canvasId: 'myQrcode',
      // ctx: wx.createCanvasContext('myQrcode'),
      typeNumber: 10,
      text: '123',
      callback(e) {
        console.log('e: ', e)
      }
    })
  }
  onLoad: function(options) {
    this.draw()
  },

wxapp-qrcode 使用方法

  <canvas style="width: 686rpx;height: 686rpx;background:#f1f1f1;" canvas-id="mycanvas"/>
  import QR from '../../utils/qrcode.js'
​
  data: {
    imagePath: ''
  },
​
  onReady: function() {
    this.createQrCode('wxapp-qrcode', 'mycanvas', 300, 300)
  },
​
  createQrCode: function (content, canvasId, cavW, cavH) {
    //调用插件中的draw方法,绘制二维码图片
    //this.canvasToTempImage 为绘制完成的回调函数,可根据自己的业务添加
    QR.api.draw(content, canvasId, cavW, cavH, this, this.canvasToTempImage);
  },
​
  //获取临时缓存图片路径,存入data中
  canvasToTempImage: function (canvasId) {
    let that = this;
    wx.canvasToTempFilePath({
      canvasId,   // 这里canvasId即之前创建的canvas-id
      success: function (res) {
        let tempFilePath = res.tempFilePath;
        console.log(tempFilePath);
        that.setData({       // 如果采用mpvue,即 this.imagePath = tempFilePath
          imagePath:tempFilePath,
        });
      },
      fail: function (res) {
        console.log(res);
      }
    });
  }

**自定义 button **

<button bind:getUserInfo="onGetUserInfo" open-type="{{open-type}}" plain="{{true}}">
  <slot></slot>
</button>
组件库

组件库使用

例如: Linui

  • 在小程序跟目录初始化
    • npm init -y
    • npm i lin-ui@0.8.7
  • 找到工具点击构建 npm
  • 注册组件
// 全局 局部注册一致
{
  "usingComponents":{
    "l-avatar":"/miniprogram_npm/lin-ui/avatar/index"
  }
}

JS 逻辑交互

事件

与 bind 不同的是 catch 会阻止冒泡

<view id="outer" bind:tap="handleTap1">
  outer view
  <view id="middle" catchtap="handleTap2">
    middle view
    <view id="inner" bind:tap="handleTap3">
      inner view
    </view>
  </view>
</view>
setData
// 对于对象或数组字段,可以直接修改一个其下的子字段,这样做通常比修改整个对象或数组更好

               this.setData({
                            'array[0].text':'changed data',
                             'object.text' : 'changed data',
   ['formlist[' + index + '].' + key]: val : 'changed data'
                           })
模块
function sayHello(name) {
  console.log(`Hello ${name} !`)
}
function sayGoodbye(name) {
  console.log(`Goodbye ${name} !`)
}

module.exports.sayHello = sayHello
exports.sayGoodbye = sayGoodbye

// 引入
var common = require('common.js')
Page({
  helloMINA: function() {
    common.sayHello('MINA')
  },
  goodbyeMINA: function() {
    common.sayGoodbye('MINA')
  }
})

自定义属性

data- 开头。

<view 
  data-alpha-beta="1" 
  data-alphaBeta="2" 
  bindtap="bindViewTap"> 
  DataSet Test 
</view>
Page({
  bindViewTap:function(event){
    event.currentTarget.dataset.alphaBeta === 1 // - 会转为驼峰写法
    event.currentTarget.dataset.alphabeta === 2 // 大写会转为小写
  }
})

操作 dom

  <view id="the-id">
    达达
    达达
    达达
  </view>
  <canvas canvas-id="qrcode"></canvas>
  onLoad:function(){},
  onReady:function(){
    const query = wx.createSelectorQuery()
    query.select('#the-id').boundingClientRect()
    query.selectViewport().scrollOffset()
    query.exec(function (res) {
      res[0].top       // #the-id 节点的上边界坐标
      res[0].bottom    // #the-id 节点的下边界坐标
      res[1].scrollTop // 显示区域的竖直滚动位置
      console.log(res)
    })
    console.log(query);

    var context = wx.createCanvasContext('qrcode')
    context.setStrokeStyle("#00ff00")
    context.setLineWidth(5)
    context.rect(0, 0, 200, 200)
    context.stroke()
    context.setStrokeStyle("#ff0000")
    context.setLineWidth(2)
    context.moveTo(160, 100)
    context.arc(100, 100, 60, 0, 2 * Math.PI, true)
    context.moveTo(140, 100)
    context.arc(100, 100, 40, 0, Math.PI, false)
    context.moveTo(85, 80)
    context.arc(80, 80, 5, 0, 2 * Math.PI, true)
    context.moveTo(125, 80)
    context.arc(120, 80, 5, 0, 2 * Math.PI, true)
    context.stroke()
    context.draw()
  }

WXS

WXS 我把它当做 vue 中的过滤器使用

示例用法:

  // /filter/filter.wxs
  var foo = "'hello world' from tools.wxs";
  var bar = function (d) {
    return d;
  }
  module.exports = {
    FOO: foo,
    bar: bar,
  };
  module.exports.msg = "some msg";
  <!-- page/index/index.wxml -->
  <wxs src="./../filter.wxs" module="filter" />
  <view> {{tools.msg}} </view>
  <view> {{tools.bar(tools.FOO)}} </view>

千位符过滤器

  function Thousands(num) {
  //num = parseInt(num);
    var num = num + "";
    var d = "";
    if (num.slice(0, 1) == "-") {
      d = num.slice(0, 1);
      num = num.slice(1);
    }
    var len = num.length;
    var index = num.indexOf(".");
    if (index == -1) {
      num = num + ".00";
    } else if (index + 2 == len) {
      num = num + "0";
    }
    var index = num.indexOf("."); // 字符出现的位置
    var num2 = num.slice(-3);
    num = num.slice(0, index);
    var result = "";
    while (num.length > 3) {
      result = "," + num.slice(-3) + result;
      num = num.slice(0, num.length - 3);
    }
    if (num) {
      result = num + result;
    }
    return d + (result + num2);
  }
  //
  {{filter.Thousands(1000)}}

时间过滤器

  function formatTime(str) {
    var formatNumber = function (n) {
      n = n.toString();
      return n[1] ? n : "0" + n;
    };
    var date = getDate(str);
    var year = date.getFullYear();
    var month = date.getMonth() + 1;
    var day = date.getDate();

    var hour = date.getHours();
    var minute = date.getMinutes();
    var second = date.getSeconds();

    return (
      [year, month, day].map(formatNumber).join(".") +
      " " +
      [hour, minute, second].map(formatNumber).join(":")
    );
  }
  
  {{filter.formatTime('时间戳')}}

API

常用 API

路由

小程序中页面栈最多十层。

  • 减少不必要使用 wx.navigateTo。
  • 要想触发销毁页面onUnload生命周期执行方法,必须要使用不存在页面栈的路由。比如:wx.reLaunch、wx.redirectTo、wx.navigateBack。
  wx.redirectTo({
   url:'/pages/address/address'
  })
  // 页面卸载时触发。如wx.redirectTo或wx.navigateBack到其他页面时。
  onUnload:function(){
  let pages = getCurrentPages().length - 1;
    console.log('需要销毁的页面:'+pages);
    wx.navigateBack({
      delta: pages  //关闭当前页面,跳转到上一个页面。但是不允许跳转到 tabbar 页面。参数
    })
  }
wx.navigateTo({
  url: ''
})
wx.redirectTo({
  url: ''
})

显示操作菜单 wx.showActionSheet({})

打开另一个小程序

wx.navigateToMiniProgram({
  // 打开小程序的 appid
  appId: '',
  // 打开的页面路径以及参数 小程序的 App.onLaunch、App.onShow 和 Page.onLoad 的回调函数中可以获取到query 数据
  path: 'page/index/index?id=123',
  // 需要传递给目标小程序的数据,目标小程序可在 App.onLaunch,App.onShow 中获取到这份数据。
  extraData: {
    foo: 'bar'
  },
  // 体验版、开发版、正式版
  envVersion: 'develop',
  success(res) {
    // 打开成功
  }
})

返回到上一个小程序

wx.navigateBackMiniProgram({
  extraData: {
    foo: 'bar'
  },
  success(res) {
    // 返回成功
  }
})

要获取用户的地理位置时,只需要:

wx.getLocation({
  type: "wgs84",
  success: res => {
    var latitude = res.latitude; // 纬度
    var longitude = res.longitude; // 经度
  }
});

调用微信扫一扫能力,只需要:

wx.scanCode({
  success: res => {
    console.log(res);
  }
});

动态设置当前页面的标题

wx.setNavigationBarTitle({
  title: '当前页面'
})

隐藏 tabbar

  wx.showTabBar()
  wx.hideTabBar()

复制文本

wx.setClipboardData({
  data: 'data',
  success (res) {
    wx.getClipboardData({
      success (res) {
        console.log(res.data) // data
      }
    })
  }
})

获取收货地址

  handleChooseAddress(){
    wx.getSetting({
      success: (result) => {
        const scopeAddress = result.authSetting["scope.address"];
        if(scopeAddress === true || scopeAddress === undefined){
          wx.chooseAddress({
            success: (result1) => {
              console.log(result1);
            },
            fail:() => {},
            complete: () => {} 
          })
        }else{
          wx.openSetting({
            success: (result2) => {
              wx.chooseAddress({
                success: (result3) => {
                  console.log(result3);
                },
                fail:() => {},
                complete: () => {} 
              })
            },
            fail: () => {},
            complete: () => {}
          })
        }
      }
    })
  }
  // 优化
  async handleChooseAddress() {
    try {
      const res1 = await getSetting();
      const scopeAddress = res1.authSetting["scope.address"];
      if(scopeAddress === false){
        await openSetting()
      }
      const address = await chooseAddress();
      wx.setStorageSync("address",address)
    }catch(error){
      console.log(error)
    }
  }

分包

// app.json
{
  "pages":[
    "pages/index",
    "pages/logs"
  ],
  "subpackages": [ // 分包
    {
      "root": "packageA", // 分包目录文件
      "pages": [
        "pages/cat",
        "pages/dog"
      ]
    }, {
      "root": "packageB",
      "name": "pack2",
      "pages": [
        "pages/apple",
        "pages/banana"
      ],
      "independent": true  // 独立分包
    }
  ]
  "preloadRule": {
    "pages/index": {
      "network": "all",
      "packages": ["important"]
    },
    "sub1/index": {
      "packages": ["hello", "sub3"]
    },
    "sub3/index": {
      "packages": ["path/to"]
    },
    "indep/index": {
      "packages": ["__APP__"]
    }
  }
}

功能

编辑照片

  <canvas 
    canvas-id="myCanvas"
    :style="{width: cameraW + 'px', height: cameraH + 'px'}"
    binderror="canvasIdErrorCallback" 
    bindtouchstart="touchStart" 
    bindtouchmove="touchMove" 
    bindtouchend="touchEnd">
      <!-- 建议使用 cover- 实际测试时 view 和 image未能在 canvas上层-->
      <cover-view class="revoke">
        <cover-image src="../../../static/img/feature/icon_recall@2x.png" mode="aspectFit"  @click="revoke">
        </cover-image>
      </cover-view>
  </canvas>
  editPhoto(){
    // 进入编辑模式画布准备
    if(this.canvasCtx){
      this.setData({
        canvasCtx: null;
      }}
    }
    setTimeout(()=>{
      this.setData({
        record: this.data.records[this.data.imgIndex] || [] // 取出当前图片的绘画记录
        canvasCtx: wx.createCanvasContext('myCanvas');
      })
      this.repaint()
    },100)		
  },
  canvasIdErrorCallback (e) {
    console.error(e.detail.errMsg)
  },			
  touchStart(e){
    this.record.push([]); // 新增一条记录
    const index = this.record.length -1;
    const x = e.touches[0].x;
    const y = e.touches[0].y;
    this.canvasCtx.setStrokeStyle('red')
    this.canvasCtx.beginPath()
    this.canvasCtx.moveTo(x, y)
    this.record[index].push({x,y}) // 加入记录点
  },
				
  touchMove(e){
    const index = this.record.length -1;
    const x = e.touches[0].x;
    const y = e.touches[0].y;
    this.canvasCtx.lineTo(x, y)
    this.record[index].push({x,y}) // 加入记录点
    this.canvasCtx.stroke()
    this.records[this.imgIndex] = this.record.slice()
  },				
  touchEnd(e){
    // this.canvasCtx.closePath()
    // this.canvasCtx.stroke()
    this.canvasCtx.draw(true)
  },
  revoke(){
    this.canvasCtx.drawImage(this.sourceList[this.imgIndex], 0, 0,  this.cameraW, this.cameraH)
    this.canvasCtx.draw()
    this.record.pop(); // 删除上一画
    if(this.record.length>1){
      this.canvasCtx.setStrokeStyle('red')
      this.record.forEach((element)=>{
      this.canvasCtx.beginPath()
        element.forEach((coordinate,index)=>{
	if(index == 0){
	  this.canvasCtx.moveTo(coordinate.x, coordinate.y)
	}else{
	  this.canvasCtx.lineTo(coordinate.x, coordinate.y)
	}
      })
      this.canvasCtx.stroke()
      this.canvasCtx.draw(true)
    })
  }
},
				
  // 重绘
  repaint(){
    this.canvasCtx.drawImage(this.sourceList[this.imgIndex], 0, 0, this.cameraW, this.cameraH)
    this.canvasCtx.draw()
    if(this.record.length>0){
      this.canvasCtx.setStrokeStyle('red')
      this.record.forEach((element)=>{
      this.canvasCtx.beginPath()
      element.forEach((coordinate,index)=>{
        if(index == 0){
	  this.canvasCtx.moveTo(coordinate.x, coordinate.y)
	}else{
	  this.canvasCtx.lineTo(coordinate.x, coordinate.y)
	}
     })
     this.canvasCtx.stroke()
     this.canvasCtx.draw(true)
  })
 }
},

deleteUpload(index){
  this.imgList.splice(index,1)
},

// 保存绘画结果
submitPaintingResult() {
  // 有绘画记录再canvas绘制图片替换没有就切换状态
  const that = this
  if(that.record.length>0){
    wx.canvasToTempFilePath({
      destWidth: this.cameraW,
      destHeight: this.cameraH,
      canvasId: 'myCanvas',
      success: function (res) {
        // 保存图片
	that.imgList.splice(that.preImgIndex,1,res.tempFilePath); 
	// 记录保存
	that.records[that.preImgIndex] = that.record.map(element => element);
      },
      fail: function (error) {
        console.log(error);
      }
    })
  }else{
  that.imgList.splice(that.preImgIndex,1,this.sourceList[that.preImgIndex]); 
 }
},

动画效果

 <view 
   bindtouchstart = "handleTouchStart"
   bindtouchmove =  "handleTouchMove"
   bindtouchend = "handleTouchEnd"
   style="transform:"{{coverTransform}}; transition: {{coveTransition}}"
 >
  let startY = 0;  // 手指起始的坐标
  let moveY = 0;  // 手机移动的坐标
  let moveDistance = 0;  // 手指移动的距离
  
  data: {
    coverTransform: "translateY(0)",
    coveTransition: '',
  }
  
  handleTouchStart(e){
    this.setDate({
      coveTransition: ''
    })
    startY = e.touches[0].clientY;
  },
  handleTouchMove(e){
    moveY = e.touches[0].clientY;
    moveDistance = moveY - startY;
    if(moveDistance <= 0 ){
      return
    }
    if(moveDistance >= 80){
      moveDistance = 80
    }
    this.setData({
      coverTransform: `translateY($(moveDistance)rpx)`
    })
  },
  handleTouchEnd(){
    this.setData({
      coverTransform: `translateY(0)`
      coveTransition: 'transform 1s linear'
    })
    
  }

背景音乐

Page({
  data: {
      _mgr: wx.getBackgroundAudioManager()
  },
  onLoad(options){
    this.data._mgr.onPlay(this.onMusicStart)
    this.data._mgr.onStop(this.onMusicStop)
    this.data._mgr.onPause(this.onMusicStop)
  }
  onMusicStart(event){
      _mgr.src = music.url
      _mgr.title = music.title
      _mgr.coverImgUrl = music.coverImg
      // _mgr.play()
  } 
  onMusicStop(event){
      _mgr.stop()
  }
})

// app.json
{
    "requiredBackgroundModes":["audio","location"]
}

多视频播放与暂停

  <scroll-view 
    scroll-y 
    class="videoScroll" 
    refresher-enabled
    refresher-triggered= "{{isTriggered}}"
    bindrefresherrefresh="handleRefresher"
    bindscrolltolower= "handleTolower">
    <view 
      class="videoItem" 
      wx:for="{{videoList}}" 
      wx:key="id"
    >
      <video
      wx:if="{{videoId === item.data.vid}}"
      src="{{item.data.urlInfo.url}}" 
      id="{{item.data.vid}}"
      poster = "{{item.data.coverUrl}}"
      class = "common"
      object-fit = "cover"
      bindplay="handlePlay"
      bindtimeupdate= "handleTimeUpdate"
      bindended= "handleEnded"
      >
      </video>
    </view>
    <!-- 性能优化:使用 image 图片代替 video 标签 -->
    <image wx:else
    class="common" 
    src="{{item.data.coverUrl}}" 
    id="{{item.data.vid}}
    bindtap="handlePlay">
  </scroll-view>
  .videoScroll{
    margin-top: 10rpx;
    height: calc(100vh - 152rpx);
  }
  /**
    *  单例模式:
    *  1、需要创建多个对象的场景下,通过一个变量接收,始终保持只有一个对象,
    *  2、节省内存空间
    **/
  handlePlay(event){
    let vid = event.currentTarget.id
    // this.vid = vid && this.videoContext && this.videoContext.stop();
    // this.vid = vid;
    this.setData({
      videoId: vid
    })
    this.videoContext = wx.createVideoContext(vid);
    let {videoUpdateTime} = this.data;
    let videoItem = videoUpdateTime.find(item => item.vid === vid);
    if(videoItem){
      this.videoContext.seek(videoItem.currentTime);
    }
    this.videoContext.play();
  },
  
  handleTimeUpdate(event){
    let videoTimeObj = {vid: event.currentTarget.id, currentTime: event.detail.currentTime};
    let {videoUpdateTime} = this.data;
    // 判断记录播放时长的 videoUpdateTime 数组
    let videoItem = videoUpdateTime.find(item => item.vid === videoTimeObj.vid);
    if(videoItem){
      videoItem.currentTime = event.detail.currentTime;
    }else{
      videoUpdateTime.push(videoTimeObj);
    }
    this.setData({
      videoUpdateTime
    })
  },
  
  handleEnded(event){
    let {videoUpdateTime} = this.data;
    const index = videoUpdateTime.findIndex(item => item.vid === event.currentTarget.id);
    videoUpdateTime.splice(index,1)
    this.setData({
      videoUpdateTime
    })
  },
  
  handleRefresher(){
    this.setData({
      isTriggered: false
    })
  },
  
  handleTolower(){
  
  }

转发分享

  Page({
    onShareAppMessage: function (res) {
      if (res.from === 'button') {
        // 来自页面内转发按钮
        console.log(res.target)
      }
      return {
        title: '自定义转发标题',
        path: '/page/user?id=123'
      }
    }
  })
  <button open-type="share">分享</button>

扫码进入小程序或获取参数

小程序开发/开发设置中/扫普通链接二维码打开小程序,微信扫码的时候直接打开小程序,小程序内扫码的时候,获取参数。

扫普通二维码打开小程序

获取小程序码

微信支付

对比栏目 JSAPI JSSDK 小程序
统一下单 都需要先获取到Openid,调用相同的API
调起数据签名 五个字段参与签名(区分大小写):appId,nonceStr,package,signType,timeStamp
调起支付页面协议 HTTP或HTTPS HTTP或HTTPS HTTPS
支付目录
授权域名
回调函数 success回调 complete、fail、success回调函数
程序访问商户服务都是通过HTTPS,开发部署的时候需要安装HTTPS服务器
wx.requestPayment({
  timeStamp: '',
  nonceStr: '',
  package: '',
  signType: 'MD5',
  paySign: '',
  success (res) { },
  fail (res) { }
})

实时音视频截图

  • 截图:LivePlayerContext.snapshot(string quality)
  • 保存图片:wx.saveImageToPhotosAlbum(Object object)

用户登录

  <button bindtap="getUserProfile">获取用户头像</button>
getUserProfile(e) {
    wx.getSetting({
      success (res){
        if (res.authSetting['scope.userInfo']) {
          // 已经授权,可以直接调用 getUserInfo 获取头像昵称
          wx.getUserProfile({
            lang: 'zh_CN',
            desc: '用于完善会员资料', 
            success: (res) => {
              this.setData({
                userInfo: res.userInfo,
                hasUserInfo: true
              })
              wx.login({
                success (res) {
                  if (res.code) {
                    //发起网络请求
                    wx.request({
                      url: 'https://example.com/onLogin',
                      data: {
                        code: res.code
                      }
                    })
                  } else {
                    console.log('登录失败!' + res.errMsg)
                  }
                }
              })
            }
          })
        }
      }
    })
  },

技巧

不做数据绑定推荐用 _ 开头例如: _postsCollected

页面返回弹出对话框

 onLoad(options) {
  wx.enableAlertBeforeUnload({
    message: "是否退出该界面",
    success: function (res) {
      // console.log("方法注册成功:", res);
    },
    fail: function (errMsg) {
      // console.log("方法注册失败:", errMsg);
    },
  });
 },

onShow 获取参数

  onShow(){
    const pages = getCurrentPages();
    const currentPage = pages[pages.length-1];
    const {type} = currentPage.options;
    this.getOrders(type)
  }

页面之间传值

  1. onShow 直接请求接口
  2. globalData 存储数据 onShow 做刷新获取数据
  3. 获取页面实例,调用页面方法
  Page({
     onCartAdd(num) {
        //  获取页面栈  
        let pages = getCurrentPages()
        // 获取当前页的实例
        let currentPage = pages[pages.length - 1]
        // 获取当前页的路由
        const url = currentPage.route;
        // 使用当前页的方法
        currentPage.onCartAdd(num);
    }
  })
  1. eventBus(或者叫PubSub)方式
  2. gloabelData watcher方式
  3. 通过hack方法直接调用通信页面的方法
  // 使用 PubSub.js
  import PubSub from 'pubsub-js'
  
  onLoad(options){
    // 先订阅
    PubSub.subscribe('switchType',(msg,data) => {
      console.log(data); // 要接受的数据
    })
  }
  
  // 后发布
  handleSwitch(event){
    PubSub.publish('switchType',data) // 发布数据
  }

组件之间传值

获取组件实例

  // 父组件中
  onReady:function(){
    // 获取子组件的示例
    this.child = this.selectComponent('.the-id');
    //  子组件内的方法
    this.child.showToast()
  }

  <my-component id="the-id" />

async/await 的使用

小程序中支持 promise ,但是在实际开发中还是会有问题。 例如:

  this.getArticle.then(res=>{
    console.log(res);
    // 我们想等这个异步执行完成后再执行一个异步
  }).then(()=>{
    // 需要写成这种链式结构
    this.getArticle.then(res=>{
      console.log(res=>{
        console.log(res);
      })
    })
  })
  .catch(error=>{
    console.log(error);
  })

这种情况下还是避免不了嵌套,也许是我没有把 promise 理解透彻。 这时候我想到了async/await 但是 async/await 是 ES7 的语法。

  • 不勾选es6转es5,不勾选增强编译;该模式是纯es7的async/await,需要基础库高版本。
  • 勾选es6转es5,勾选增强编译;一般是因为调用了第三方的es5插件,通过增强编译支持async/await。
  • 勾选es6转es5,不勾选增强编译;手工引入runtime.js支持async/await。

我们需要引入 [regenerator-runtime]中 runtime.js 即可。在这之前你需要找个文件夹将它存到本地。 例如:lib\runtime\runtime.js

regenerator-runtime 编译的生成器和 async函数的独立运行时。

  // my 页面
  import regeneratorRuntime from '../../lib/runtime/runtime';
  async getGoodsList(){
    const res=await request({url:"/goods/search",data:this.QueryParams});
    // 获取 总条数
    const total=res.total;
    // 计算总页数
    this.totalPages=Math.ceil(total/this.QueryParams.pagesize);
    // console.log(this.totalPages);
    this.setData({
      // 拼接了数组
      goodsList:[...this.data.goodsList,...res.goods]
    })

    // 关闭下拉刷新的窗口 如果没有调用下拉刷新的窗口 直接关闭也不会报错  
    wx.stopPullDownRefresh();
  },

api Promise风格

  /**
   * promise 形式  getSetting
   */
  export const getSetting=()=>{
    return new Promise((resolve,reject)=>{
      wx.getSetting({
        success: (result) => {
          resolve(result);
        },
        fail: (err) => {
          reject(err);
        }
      });
    })
  }
  /**
   * promise 形式  chooseAddress
   */
  export const chooseAddress=()=>{
    return new Promise((resolve,reject)=>{
      wx.chooseAddress({
        success: (result) => {
          resolve(result);
        },
        fail: (err) => {
          reject(err);
        }
      });
    })
  }

  /**
   * promise 形式  openSetting
   */
  export const openSetting=()=>{
    return new Promise((resolve,reject)=>{
      wx.openSetting({
        success: (result) => {
          resolve(result);
        },
        fail: (err) => {
          reject(err);
        }
      });
    })
  }

  /**
   *  promise 形式  showModal
   * @param {object} param0 参数
   */
  export const showModal=({content})=>{
    return new Promise((resolve,reject)=>{
      wx.showModal({
        title: '提示',
        content: content,
        success :(res) =>{
          resolve(res);
        },
        fail:(err)=>{
          reject(err);
        }
      })
    })
  }


  /**
   *  promise 形式  showToast
   * @param {object} param0 参数
   */
  export const showToast=({title})=>{
    return new Promise((resolve,reject)=>{
      wx.showToast({
        title: title,
        icon: 'none',
        success :(res) =>{
          resolve(res);
        },
        fail:(err)=>{
          reject(err);
        }
      })
    })
  }

  /**
   * promise 形式  login
   */
  export const login=()=>{
    return new Promise((resolve,reject)=>{
      wx.login({
        timeout:10000,
        success: (result) => {
          resolve(result);
        },
        fail: (err) => {
          reject(err);
        }
      });
    })
  }

  /**
   * promise 形式的 小程序的微信支付
   * @param {object} pay 支付所必要的参数
   */
  export const requestPayment=(pay)=>{
    return new Promise((resolve,reject)=>{
    wx.requestPayment({
        ...pay,
      success: (result) => {
        resolve(result)
      },
      fail: (err) => {
        reject(err);
      }
    });
    })
  }
  // 使用
  import { getSetting, chooseAddress, openSetting, showModal, showToast, requestPayment } from "../../utils/asyncWx.js";
  requestPayment(pay);
  // 代码来源黑马

请求封装

  • 极简 promise 封装接口
  // 同时发送异步代码的次数
  let ajaxTimes=0;
  export const request=(params)=>{
    // 判断 url中是否带有 /my/ 请求的是私有的路径 带上header token
    let header={...params.header};
    if(params.url.includes("/my/")){
      // 拼接header 带上token
      header["Authorization"]=wx.getStorageSync("token");
    }
    ajaxTimes++;
    // 显示加载中 效果
    wx.showLoading({
      title: "加载中",
      mask: true
    });
    // 定义公共的url
    const baseUrl="https://api.zbztb.cn/api/public/v1";
    return new Promise((resolve,reject)=>{
      wx.request({
      ...params,
      header:header,
      url:baseUrl+params.url,
      success:(result)=>{
        resolve(result.data.message);
      },
      fail:(err)=>{
        reject(err);
      },
      complete:()=>{
        ajaxTimes--;
        if(ajaxTimes===0){
          //  关闭正在等待的图标
          wx.hideLoading();
        }
      }
      });
    })
  }
  // 使用
  import { request } from "../../request/index.js";
  request({url:"/goods/search",data:this.QueryParams}).then(res=>{
    console.log(res);
  })
  // 代码来源黑马

中文乱码

扫码、页面传值过程中,有遇到中文传输乱码的情况,需要 encodeURIComponent()编码、或者decodeURIComponent()解码。

骨架屏

返回键

  //生命周期函数--监听页面卸载
  onUnloadfunction () {
    wx.redirectTo({
      url"/pages/index/index",
    });
  },

下拉刷新

什么情况下使用 scroll-view 最好呢!我目前觉得自定义导航栏的时候,自定义导航会导致下拉整个页面跟着动。 但是用 scroll-view 也会有几个问题:

  • 上拉刷新 加载动画,用原生的整个页面都会动,需要设置、注意兼容。
  • 下拉加载更多 bindscrolltolower 事件触发 需要获取高度、然后动态设置高度。

canvas 画图 Painter

图表 wx-charts

new Date()

在 Android 和 Ios 下执行得到的结果不一致。(微信小程序、webApp上均遇到此类问题。)

setData 的替代品

wx-updata

一次获取多张图片信息

  const promixify = (api) => {
    return (options, ...params) => {
      return new Promise((resolve, reject) => {
        api(Object.assign({}, options, { success: resolve, fail: reject }), ...params)
      })
    }
  }
  const getImageInfo = promixify(wx.getImageInfo)
  const images = ['img1.jpg', 'img2.jpg', 'img3.jpg']
  Promise.all(
    images.map(img => getImageInfo({ src: img }))
  ).then((imageInfos) => {
    console.log(imageInfos)
  })

小程序日志监控工具 Fundebug

背景图片

小程序中无法使用本地的图片来作为背景图片

解决方案有:

  • 将图片转为 base64格式引入
  • 将图片放到 cnd 或者服务器上 引入
  • 用 image 标签作为背景层

请求缓存

对于一些时效性没那么强的请求接口做本地存储来减轻一部分的服务器压力。 我解决缓存可以跟请求封装再一起,通过参数来判断是否缓存,或者搞个缓存层,请求的时候先过缓存层,判断是否有缓存以及缓存是否过期再走请求层。

例子

  getList(){
    const list = wx.getStorageSync('list')
    if(!list){
      this.getApiList();
    }else{
      if(Date.now() - list.time > 1000 * 10){ // 查看时候过期
        this.getApiList();
      }else {
        this.list = list.data
      }
    }
  }
  // 代码来源黑马

ios margin-bottom 失效

在底部有固定定位时,用margin-bottom 在 ios 上会失效。

 // 可用用元素去顶一下
 .container :after{
   display: block;
   content: '';
   height: 150rpx; // 底部间距
 }
  
 // 设置多余的高度
 .container
   height: 1400rpx; 
 }
 
 // 改用 padding-bottom
 .container
   padding-bottom: 150rpx; // 底部间距
 }

云开发

云开发接触的少,后续学习整理完再分享,目前之前的博客有点乱,先整理到这吧。 原博客地址微信小程序 从入门到实践开发 - Fallen-down

扩展

数据劫持代理

 // Vue2 数据劫持代理
 
 // 模拟 vue 中 data 选项
 let data = {
   username: 'curry',
   age: 33
 }
 
 // 模拟组件的实例
 let _this = {}
 
 // 利用 Object.defineProperty()
 for(let item in data) { 
   Object.defineProperty(_this, item ,{
     // get : 用来获取扩展属性值,当获取该属性值的时候调用 get 方法
     get(){
       return data[item]
     },
     // set :监视扩展属性的,只要已修改就调用
     set(newValue){
       data[item] = newValue;
     }
   })
 }
 
 console.log(_this);
 // 通过 Object.defineProperty 的 get 方法添加的扩展属性不能直接对象属性修改
 _this.username = 'wade';
 console.log(_this.username);

node token

node-jsonwebtoken

  let person = {
    username: '北方汉字',
         age: 18,
      openId
  }
  //  生成 token
  let token = jwt.sign(person, 'xxxxxa');
  
  //  反编译 token
  let person2 = jwt.verify(token, 'xxxxxa')

fly.js:多端 http 请求库

构建自己的项目

项目目录结构

  • components 目录:主要用来存放一些自定义组件相关的内容
  • libs 目录:主要用来存放项目中依赖的第三方库。例如 jquery、seajs、qrcode、echarts 等。
  • models 目录:主要用来封装与后台进行交互的 model 操作类。
  • utils 目录:主要用来存放项目开发过程中要使用到的各种工具类,避免重复代码。

过程

1、自定义 tabBar

使用官方小程序示例中的 自定义 tabBar

2、引入字体图标

使用 在线 url

将字体 url 转成 base64 的格式后使用出现问题……

3、API Promise化

使用官方 扩展能力 API Promise化 miniprogram-api-promise 工具

4、封装接口

参考下面文档接合下

// 同时发送异步代码的次数
let ajaxTimes=0;
export const request=(params)=>{
  // 判断 url中是否带有 /my/ 请求的是私有的路径 带上header token
  let header={...params.header};
  if(params.url.includes("/my/")){
    // 拼接header 带上token
    header["Authorization"]=wx.getStorageSync("token");
  }
  ajaxTimes++;
  // 显示加载中 效果
  wx.showLoading({
    title: "加载中",
    mask: true
  });
  // 定义公共的url
  const baseUrl="https://api.zbztb.cn/api/public/v1";
  return new Promise((resolve,reject)=>{
    wx.request({
    ...params,
    header:header,
    url:baseUrl+params.url,
    success:(result)=>{
      resolve(result.data.message);
    },
    fail:(err)=>{
      reject(err);
    },
    complete:()=>{
      ajaxTimes--;
      if(ajaxTimes===0){
        //  关闭正在等待的图标
        wx.hideLoading();
      }
    }
    });
  })
}
// 使用
import { request } from "../../request/index.js";
request({url:"/goods/search",data:this.QueryParams}).then(res=>{
  console.log(res);
})
// 代码来源黑马

5、状态管理

用官方 扩展能力 架构扩展中的 mobx-miniprogram

6、开启开发者工具中 "开启上传代码时样式文件自动不全" 功能,这样小程序会自动不全其余一些样式兼容性写法。

7、封装组件库

参考 vant 封装 icon、官方指南中自定义组件

8、分包

9、引入事件管理工具 mitt 来作为时间管理中心,来处理组件、界面传值。

10、开启开发者工具中 “增强编译”,可以使用 async/await。

11、加入简单的防抖和节流

建议

利用场景值做数据统计

在模块里随便更改 exports 的指向会造成未知的错误。推荐开发者采用 module.exports 来暴露模块接口,除非你已经清晰地知道两者之间的关系。

  // 例如
  function sayHello (name) {
    console.log(`Hello ${name}!`)
  }
  module.exports.sayHello = sayHello

scroll-view 在滚动 scroll-view 时会阻止页面回弹,所以在 scroll-view 中 滚动是无法触发 onPullDownRefresh 的。 若要使用下拉刷新,请使用页面的滚动,而不是 scroll-view,这样也能通过点击顶部状态栏回到页面顶部。

在特殊机型(例如 iphoneX、iphone 11)中,因为取消了物理按键,会出现手机底部区域被手机底部的小黑条遮挡的情况,此时可以使用 padding-bottom: env(safe-area-inset-bottom)来告诉微信进行自动适配。

优化

  1. 启动慢主要从优化代码包上下手:

    • 对静态资源进行优化,将非必要的静态资源文件上传到CDN
    • 对小程序的组件进行依赖分析,过滤掉未使用的组件
    • 对独立性比较强的页面进行独立分包,减少主包下载耗时
  2. 请求慢主要从预加载和缓存下手:

    • 冷启动开启数据预拉取
    • 页面路由切换时提前拉取数据
    • 对数据进行缓存
  3. 交互慢需要从发起请求和页面渲染下手:

    • 保障与用户体验相关的业务请求正常发送
    • 页面分步渲染

相关资料



开源