微信小程序
对于微信小程序开发入门, 还是比较简单的, 只需要具备基本的 CSS+js 知识就可以了, 成本比较低. 写了小程序和 RN 之后, 有一种原生很笨重的感觉, 就是小程序或者是 RN 等这些新的开发方式在效率上面真的有比较大的优势, 唯一不足就是运行速度了 (使用 Canvas 就会有这样子的感觉). 感觉目前所接触的种类前端开发 (包括移动端), 都是基本一个套路: UI, 网络, 数据保存, 富文本, 图片 / 视频. 本文也是从这几个方向去总结自己的小程序开发经验.
小程序的入门
其实小程序的开发过程一直都是查看文档, 按照文档去操作就可以了. 一般流程是先看简易教程 https://developers.weixin.qq.com/miniprogram/dev/ . 看完之后, 再去看组件 https://developers.weixin.qq.com/miniprogram/dev/component/ . 之后可以开始尝试写需求, 这个过程中, 开始不断的去查 API https://developers.weixin.qq.com/miniprogram/dev/api/ 和框架 https://developers.weixin.qq.com/miniprogram/dev/framework/MINA.html 即可.
多列列表
在开发中, 有一个需求是需要实现类似 Android 的 GridView 网格列表的. 但是微信中并没有提供这样子的组件, 但是小程序是跟 html/css 前端很类似的, 他可以通过指定 display:flex, 然后去设置 flex-wrap:wrap 就可以. 例如, 有一个数组
data:["A","B","C","D","E","F","G","H","I","J","K","L","M","N"]
需要显示为一个三列的列表, 可以如下处理:
- //GridPage.wxml
- <view class='grid-container'>
- <view wx:for="{{data}}" wx:key="{{item}}" class="grid-list">
- <view class='grid-item'>
- <text class='grid-item-text'>{{item}}</text>
- </view>
- </view>
- </view>
- //GridPage.wxss
- Page {
- min-height: 100%;
- background-color: #fff;
- }
- .grid-container {
- margin-left: 4rpx;
- margin-right: 4rpx;
- display: flex;
- flex-wrap: wrap;
- flex-direction: row;
- }
- .grid-list {
- width: 33.33%;
- }
- .grid-item {
- margin: 2rpx;
- background: #999;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .grid-item-text {
- color: black;
- }
这里的重点就是 grid-container 中的 flex-wrap 为 wrap, 方向是 row 了. 然后他的每一个 item 宽度都是 33.33%. 需要注意的是一定是去设置外部的 contanier 而不是内部的 list.
层级布局
在 CSS 中, 需要使用层级布局, 就是类似 Android 的 FrameLayout 效果, 可以使用 z-index, 也可以使用一个绝对定位. 比如, 我们有一个需求是: 下面是一个图片, 上面是文字.
- //PositionPage.wxml
- <view class='root'>
- <image src='https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3756982450,995202616&fm=27&gp=0.jpg' class='image'></image>
- <text class='text'> 我是权律二啊 </text>
- </view>
- //PositionPage.wcss
- .root{
- align-items: center;
- display: flex;
- flex-direction: column;
- position: relative;
- }
- .image{
- width: 300rpx;
- height: 300rpx;
- }
- .text{
- background-color: #999;
- position: absolute;
- }
主要是两点: 父布局的 position 必须是 relative, 它本身 position 必须是 absolute.
网络请求
小程序的网络请求是使用 wx.request() 方法, 但是该方法太臃肿, 并没有使用 Promise 那样子简洁. 幸运的是小程序支持 Promise, 所以我们可以把 http 封装一下, 变成有条理. 说到这里, 大家做的时候需要注意去微信后台配置各种 request 域名, upload 域名, downloadFIle 域名. 下面封装的例子的数据返回格式都是 json 格式 post 请求方式发出的:
- // 真正发起请求
- function _request(url, param) {
- if (isDebug) {
- Log.i("http==> params->" + JSON.stringify(param));
- Log.i("http==> url->" + url);
- }
- return new Promise((resolve, reject) => {
- wx.request({
- url: url,
- data: param,
- header: {
- 'content-type': 'application/json',
- "Accept": "application/json"
- },
- method: "POST",
- success: function (response) {
- if (isDebug) {
- const jsonResponse = JSON.stringify(response);
- Log.i("http==> response->" + jsonResponse);
- }
- const {data, statusCode, ok = false} = response;
- // 只有 ok 为 true 的是时候才返回成功, data 不一定是包含数据的
- if (statusCode === 200 && data && data.ok) {
- resolve(data, ok);
- } else {
- if (statusCode != 404 && statusCode <500 && statusCode> 300) {
- ToastUtil.showError();
- }
- reject(data);
- }
- },
- fail: reject
- });
- });
- }
使用:
- function getInfo(fid) {
- const params = {};
- params.token = user.token;
- params.uid = user.uid;
- return _request("INFO_URL", params);
- }
然后需要发起请求就调用该方法即可, 处理 Promise.
上传图片到阿里云
需要注意微信 upload 接口配置目前好像不可以直接配置阿里云的 URL, 需要阿里云先 跟我们的域名绑定, 之后再去把设置到微信后台的 upload 接口中. 可以参考博客: 小程序图片上传阿里 OSS 使用方法 https://www.jianshu.com/p/4372ad69c861 , 获取签名阿里云 Demo 地址: JavaScript 客户端签名直传 https://help.aliyun.com/document_detail/31925.html?spm=5176.doc31923.6.628.JYYHox , 通过打 log 获得 policy 和 signature(签名时间可以稍微设置久一点) 之后, 就开始封装 upload 方法了. 如下:
- /**
- * 真实上传代码
- */
- function _upload(file, success, fail) {
- const suffix = file.substring(file.lastIndexOf("."));
- // 做一下 md5 处理
- const fileName = hex_md5(file);
- Log.i("fileName=" + (fileName + suffix));
- wx.uploadFile({
- url: ALIYUNPHOTOADDRESS,
- formData: {
- "OSSAccessKeyId": "你的阿里云 accessKey",
- "key": DIR+ (fileName + suffix),
- "policy": "你的 policy",
- "success_action_status": '200',
- "signature": "你的 signature"
- },
- filePath: file,
- name: 'file',
- success: function (res) {
- const {statusCode} = res;
- if (statusCode === 200) {
- console.log(JSON.stringify(res));
- success("" + fileName + suffix);
- } else {
- console.log("上传失败");
- fail(res);
- }
- },
- fail: function (e) {
- fail(e);
- console.log("上传失败");
- console.log("e=" + JSON.stringify(e));
- }, complete: function () {
- console.log("上传过程结束");
- }
- })
- }
- }
其中 url 是上传 OOS 的地址, key 是需要上传的文件夹 + 上传之后的文件名. 这里的 fileName 我们通过一个 md5 去计算得来, 保证唯一性又没有什么特殊字符. md5 的算法来自 JS-MD5 加密 https://www.cnblogs.com/xinyueBlog/p/6179172.html .
我们可以顺带封装一个上传多张图片的方法, 而且使用 Promise 返回:
- /**
- * files 需要上传的文件, 是一个数组, 里面是文件的绝对路径
- */
- function uploadFiles(files) {
- if (!files || files.length <= 0) {
- wx.showModal({
- title: '图片错误',
- content: '请重试',
- showCancel: false,
- });
- return Promise.reject();
- }
- Log.i("开始上传" + files);
- return new Promise((resolve, reject) => {
- // 上传成功的文件名称
- let uploadPaths = [];
- for (let i = 0; i <files.length; i++) {
- _upload(files[i], (path) => {
- // 成功的文件名
- uploadPaths[uploadPaths.length] = path;
- if (uploadPaths.length>= files.length) {
- // 把 url+name 返回
- resolve([ALIYUNPHOTOADDRESS + "/" +DIR, uploadPaths]);
- }
- }, () => {
- //error
- reject(res);
- });
- }
- });
Canvas 使用
由于需要使用 Canvas 画一棵树, 所以还是在这里走了比较多的坑的. 我的需求是 Canvas 全屏, 除了画一个树之外还需要画别的一些独立 Button. 首先, 设置 Canvas 全屏和不可滑动, 可以通过以下方式:
<canvas disable-scroll='true' style="width: {{width}}px; height: {{height}}px;background-color:#efeff4;flex:1;" canvas-id="canvas" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd"></canvas>
其中, 在设置了 disable-scroll 设置为 true, 同时需要绑定三个 touch 事件, 才能响应画布的触摸 event. 其中这里的 width+height 是通过 wx.getSystemInfo() 获得.
其次, canvas 没有类似 View 的 catchtap 事件, 只有一些 touch 事件, 详情可以看 Canvas https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html#canvas
然后在微信小程序中 Canvas 是层级最高的, 无法通过设置 z-index 去调, 所以假如你的 Canvas 全屏, 还需要一些其他的 Button, 那么只能通过最后 canvas 去 draw 了.
在 canvas 中, 假如通过 moveTo+lineTo 去画线, 一般需要先调用 canvas.beginPath() 画完成之后, 先调用 canvas.stoke(), 然后在调用 canvas.closePath();
- c.beginPath();
- c.setLineWidth(this.arrowPaint.width);
- c.setStrokeStyle(this.arrowPaint.color);
- c.setLineCap("square");
- c.moveTo(this.arrowStartPointF.x, this.arrowStartPointF.y);
- c.lineTo(this.arrowCenterPointF.x, this.arrowCenterPointF.y);
- c.moveTo(this.arrowCenterPointF.x, this.arrowCenterPointF.y);
- c.lineTo(this.arrowEndPointF.x, this.arrowEndPointF.y);
- c.stroke();
- c.closePath();
在连续画多种图片 / 线条的时候, 不要连续多次调用 draw(true) 方法, 消耗性能, 一般最后调用 fill()/stoke() 方法即可. 比如
- // 绘制点
- c.beginPath();
- let y = node.noteView.pointFrameCenter.y + Constant.FRAME_HEIGHT / 2 + Constant.GAP_BETWEEN_DOT + Constant.RADIUS_DOT;
- c.setFillStyle(Constant.LINE_COLOR_RED);
- c.setLineWidth(Constant.LINE_WIDTH);
- c.arc(node.noteView.pointFrameCenter.x, y, Constant.RADIUS_DOT, 0, 2 * Math.PI);
- //
- y += (Constant.GAP_BETWEEN_DOT + Constant.RADIUS_DOT);
- c.arc(node.noteView.pointFrameCenter.x, y, Constant.RADIUS_DOT, 0, 2 * Math.PI);
- //
- //
- y += (Constant.GAP_BETWEEN_DOT + Constant.RADIUS_DOT);
- c.arc(node.noteView.pointFrameCenter.x, y, Constant.RADIUS_DOT, 0, 2 * Math.PI);
- c.fill();
- c.closePath();
对于 draw 方法, 建议只是调用 draw() 就好, 不要调用 draw(true) 方法, draw(true) 是在原画布之上再去画, 不会清空旧画布, draw() 会清空. 一般, 我们会在所有的 image,rectangle,line,circle 去 fill/stoke 完之后, 再调用 draw() 方法, 这样子就可以避免 draw(true) 多次, 性能耗损. 而且再次去 reDraw 的时候也不用先去清空画布.
Canvas 的跟随手势拖动
- //index.wxml
- <canvas disable-scroll='true' style="width: {{width}}px; height: {{height}}px;background-color:#efeff4;" canvas-id="canvas" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd"></canvas>
- //index.wcss
- Page {
- overflow: hidden;
- display: flex;
- }
- //index.js
- const app = getApp()
- Page({
- data: {
- width: 0,
- height: 0,
- },
- onLoad: function (e) {
- this.time = (new Date()).valueOf();
- this.x = 0;
- this.y = 0;
- this.moveX = 0;
- this.moveY = 0;
- const that = this;
- wx.getSystemInfo({
- success: function (res) {
- that.setData({ width: res.screenWidth, height: res.screenHeight })
- },
- })
- const ctx = wx.createCanvasContext("canvas", this)
- this.canvas = ctx;
- },
- onReady: function () {
- this.draw();
- },
- draw: function () {
- this.canvas.fillRect(10, 10, 150, 100)
- this.canvas.fill();
- this.canvas.draw()
- },
- touchMove: function (e) {
- console.log("touchMove")
- let xOffset = e.touches[0].x - this.x;
- let yOffset = e.touches[0].y - this.y;
- this.x = e.touches[0].x;
- this.y = e.touches[0].y;
- this.moveX = this.moveX + xOffset;
- this.moveY = this.moveY + yOffset;
- this.canvas.translate(this.moveX, this.moveY);
- this.draw();
- },
- touchStart: function (e) {
- this.x = e.touches[0].x;
- this.y = e.touches[0].y;
- },
- touchEnd: function (e) {
- console.log("touchEnd")
- }
- })
其他的 scale 等方法类似.
来源: https://juejin.im/post/5afaa9f05188253064653fba