后端程序员两个周末入门微信小程序开发

再过几个小时,2018就结束了,赶着最后几个小时,完成了一个微信小程序,感谢时间,也感谢自己。

平时工作比较忙,整个开发花了两个周末的休息时间,输出包括后端服务器以及微信小程序,当然还有一些购买和部署服务器、CDN、https 证书等打杂的事情。

很遗憾,作为一个自诩为程序猿老司机的自己并没有在9小时搞定微信小程序开发。作为一个平时只写后端代码的老司机,这个完成速度勉强算及格吧。下面介绍一下自己这两个周末入门小程序开发用到的资料以及遇到的

微信小程序最好的资料

小程序这块技术栈对于自己来说是一个从0到1的过程,因此最开始还是想找找有没有一些现成的教程。看了一些免费教程,发现不太系统,并且很多js的写法在我这个业务选手看来都业余。于是极客时间找到了一个付费专栏:《9小时搞定微信小程序开发》

就在我准备付费的时候,发现其课程大纲不过是官方教程的一个搬运和翻译。

课程目录:

官方教程(部分):

(没有贬低专栏作者的意思,事实上,小程序这块技术栈让我来写,估计目录也差不多。或者说,对于有一定学习能力的?‍?‍,根本就不应该存在所谓的学习课程?)

作为一个聆听过《左耳听风》耗子叔的极客时间专栏的老司机(好像秒打脸了?),自然是记得他老人家对于学习知识的谆谆告诫:

我问过几个在别的领域知识付费的专栏作者:你们写的这些东西,不就是卡内基的那些东西吗?不就是某某书里的那些东西吗?不就是某某英文资料里的那些内容吗?

他们告诉我,是的,我们就是搬运知识,有些国外的东西,国人理解不了,需要用他们喜欢的语言讲出来,而他们又不读书,英文水平也一般,但他们想速成,所以,才有了我们的市场。这有点像餐馆,他们不想自己做饭吃,那就我们来做,有的人还要别人喂到嘴边,甚至好些人都需要先帮他们嚼一遍,他们才能吃得下去。所以,我们这些学习能力强的人挣点他们的钱也是应该的。

所以,兜兜转转一圈,最后还是选择了微信团队的官方开发教程。而这个选择从后面的开发过程看,是无比正确的:

  1. 官方教程不长,非常干练,整体系统感强。举个例子,很多奖微信小程序开发的二手教程都会给你说这个API是干嘛的以及如何使用这种废话。但是,微信的框架-API文档中,则直接给你言简意赅的讲解了其所有API涉及的规范和原则,你看了这个规约后,你看到一个API你其实就已经知道它该如何使用了。对于我这种入门菜鸟来说,这种知识点如果在入门时候就知其然,后面会在这个点上重复采坑而不自知,并且后续要知其所以然要花费的时间只会多不会少。
  2. 这是小程序开发第一手资料的来源,具有权威性和最高的时效性。从两年前关注小程序,到现在开发自己的第一个小程序前,一直以为小程序技术这块从发布至今没有什么迭代和更新,但是从其文档的众多的API兼容以及事件兼容描述看,小程序团队过去两年真的是挺努力的。作为一个开发者,迭代造成的不兼容,你只能去适配,因此,第一手的资料往往能给你节省数小时的时间。
  3. 丰富的样式库和源代码示例。这块重点推荐两个资源:

第一个是微信小程序设计指南。如果你需要自己设计产品原型,你一定要研读一下这个设计指南,这样你就不用去重新发明很多UI轮子,也不用纠结这个交互是方案A好还是方案B好。这个指南从一定程度上反应了微信整个的产品观和设计理念,你一定也能找出这些指南中不够完美的地方,但是当前阶段,这的确是一个能够让你快速设计出一个80分微信小程序产品的 Bible. 指南中给出了WeUI_sketch组件库 WeUI_PS组件库以及对应的预览地址,可以帮助你快速设计产品原型。

第二个是WeUI for 小程序. 如果第一个资源主要是辅助你进行产品设计,那么这个资源则是辅助你进行快速开发的必备参考库(老鸟可以无视)。用了这个代码库以后,你开发页面大部分时候就是从这个库里面找你需要的组件和样式,然后复制-粘贴搞定:

那些微信小程序开发中的坑

修改 page data 不生效

坦率讲,这并不是小程序的坑,而是自己看文档不仔细。修改页面的 data 只能通过 setData:

  1. 直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。
  2. 仅支持设置可 JSON 化的数据。
  3. 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。
  4. 请不要把 data 中任何一项的 value 设为 undefined ,否则这一项将不被设置并可能遗留一些潜在问题。

setData 可以直接修改一个其下的子字段:

但是有一种情况需要注意:如果你的 key 是一个变量,这样修改子字段是不会生效的:

你应该这样做:

navigateTo 不生效

这个依然是看文档不仔细:

wx.navigateTo(Object object)
保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。使用 wx.navigateBack 可以返回到原页面。

如果你要跳转的页面是 tabbar 页面,请使用 wx.switchTab.

列表条目多时出现卡顿

这个问题容易出现在动态加载数据,数据又绑定了前端列表view的场景。根据官方文档,使用 wx:key 解决即可。其原理是:

当数据改变触发渲染层重新渲染的时候,会校正带有 key 的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。

wx.requestDELETE 传参问题

根据w3c标准,http DELETE 请求中的参数应该跟 GET 方法一样,放在 url query 中,然而微信封装的 wx.request 防范在指定为 DELETE 方法后,data 依然被放在了 body 中。如果你在服务器端获取不到参数,请尝试从 body 中获取。

文件上传阿里云OSS 403 权限问题

如果你直接使用阿里云 OSS 域名上传,因为微信封禁了 OSS 域名,因此你需要参考微信小程序文件上传二三事 绑定自定义域名。同时,千万注意,绑定的域名不要开启CDN,否则,会造成 403 权限错误:

如果同时希望使用阿里云的CDN,请绑定另外一个域名,与上传域名分开。

总结

也许是孕妇效应吧,最近发现很多人都开始做微信小程序。从整体开发体验看,这的确是一个分发自己创意的轻量级产品方案。过去的两年,微信小程序已经远超百万,但是真正让你能记住的可能比你能记住的公众号还少。因此,载体本身的影响能力圈是有限的,还是要回归到你的产品解决了什么问题,这也是你做任何产品的价值所在。此外,因为苹果税的问题,当前 (2018.12.31)iOS 平台的微信小程序无法进行虚拟物品支付(会员、道具、课程等)的,这显然会极大的影响开发者的积极性。虽然你张小龙的理念是小程序用完即走,但是你不能让开发者只有一个广告变现的路径,更不能只是自私的维护好自己的地盘儿不管开发者死活。

基于 Docker 搭建 Mac 本地 HBase 环境

说起玩大数据,相信很多人都会因为 Apache 全家桶软件配置而菊花一紧。Docker 的出现,把很多玩大数据就是配机器、配环境的开发者从泥潭中拯救了出来,虽然还不能完全替代线上环境,但是在开发环境,无疑为开发者节约了大量搭建本地环境的时间。比较遗憾的是,我们团队之前也是没有独立的数据测试环境?,于是把在本地搭建 HBase 环境整理和记录如下。

系统环境:

  • MacBook Pro (Retina, 15-inch, Mid 2015)
  • 2.2 GHz Intel Core i7
  • 16 GB 1600 MHz DDR3
  • macOS 10.13.6

安装 Docker CE for Mac

Docker Community Edition for Mac下载安装。

Mac 上的 Docker 环境经过 docker-machine/virtualbox 几次变化,如今的 Docker CE 已经支持原生 Mac 环境,因此当前阶段 Docker CE for Mac 就是唯一推荐的 Mac Docker 环境,再也不用通过安装 virtualbox 这种借蛋生鸡的方式了,实在是很赞。此外,现在的 Docker CE 集成了 Kubernetes, 因此本地玩 k8s 也不需要额外进行安装配置。如果你计划以后就是玩 k8s, 那么你以前安装的 Kitematic 也可以卸载掉了。Kitematic 除了一个图形化的 container 管理界面,实在没有什么值得留恋的,因此官方停止其开发无疑是个正确的决定。

获取和启动 HBase Docker 镜像

  • 获取容器镜像

更多大数据全家桶 Docker 镜像可以参见 HariSekhon/Dockerfiles

  • 启动容器

参数解释:

  • -d: 后台启动。
  • -h: 容器主机名,必须设置该项并配置 hosts,否则无法连通容器。
  • -p: 网络端口映射,这里只把要使用的端口(zookeeper端口、HBase Master端口、HBase RegionServer端口等)映射了出来,你可以根据自己需要进行端口映射。常用 HBase端口可以参见下表:

但是,harisekhon/hbase 修改了默认端口:

因此,你看到启动参数中的端口参数是那样的。

  • --name: 容器别名。

设置hosts (推荐使用 Gas Mask):

成功启动后,就可以在 http://localhost:16010/master-status 查看 HBase 状态了:

需要注意的一点是:容器销毁后,数据也也会被同时销毁。因此你可以通过 -v YOUR_DIR:/hbase-data 的方式将数据目录映射到宿主机目录,防止数据丢失。

编写测试代码

  • 创建 table

  • 读写 table

Maven 添加依赖:

HelloHBase.java:

扩展阅读

Go 中一个非典型不加锁读写变量案例分析

前段时间在 v2 看到一个关于并发读写变量的问题:go 一个线程写, 另外一个线程读, 为什么不能保证最终一致性。帖子中给出的例子非常简单(稍作修改)main.go

package main

import (
    "fmt"
    "runtime"
    "time"
)

var i = 0

func main() {
    runtime.GOMAXPROCS(2)
    go func() {
        for {
            fmt.Println("i am here", i)
            time.Sleep(time.Second)
        }
    }()
    for {
        i += 1
    }
}

既然是问题贴,直接运行的结果应该是出乎大多数人预料的:

╰─➤  go run main.go                                                                                                                                     1 ↵
i am here 0
i am here 0
i am here 0
i am here 0
i am here 0
i am here 0
...

帖子的回复比较多,涉及的信息量相对杂乱,爬完楼反而感觉没有看懂。这里就不卖关子,直接给出脱水后的结论:出现上面结果的原因是 go 的编译器把代码 i 自加 1 的 for 循环优化掉了。要验证这一点也很简单,我们使用 go tool objdump -s 'main\.main' main 查看编译出的二进制可执行文件的汇编代码:

╰─➤  go tool objdump -s 'main\.main' main
TEXT main.main(SB) /Users/liudanking/code/golang/gopath/src/test/main.go
  main.go:11        0x108de60       65488b0c25a0080000  MOVQ GS:0x8a0, CX
  main.go:11        0x108de69       483b6110        CMPQ 0x10(CX), SP
  main.go:11        0x108de6d       7635            JBE 0x108dea4
  main.go:11        0x108de6f       4883ec18        SUBQ $0x18, SP
  main.go:11        0x108de73       48896c2410      MOVQ BP, 0x10(SP)
  main.go:11        0x108de78       488d6c2410      LEAQ 0x10(SP), BP
  main.go:12        0x108de7d       48c7042402000000    MOVQ $0x2, 0(SP)
  main.go:12        0x108de85       e8366bf7ff      CALL runtime.GOMAXPROCS(SB)
  main.go:13        0x108de8a       c7042400000000      MOVL $0x0, 0(SP)
  main.go:13        0x108de91       488d05187f0300      LEAQ go.func.*+115(SB), AX
  main.go:13        0x108de98       4889442408      MOVQ AX, 0x8(SP)
  main.go:13        0x108de9d       e8fe13faff      CALL runtime.newproc(SB)
  main.go:20        0x108dea2       ebfe            JMP 0x108dea2
  main.go:11        0x108dea4       e8c7dffbff      CALL runtime.morestack_noctxt(SB)
  main.go:11        0x108dea9       ebb5            JMP main.main(SB)
  :-1           0x108deab       cc          INT $0x3
  :-1           0x108deac       cc          INT $0x3
  :-1           0x108dead       cc          INT $0x3
  :-1           0x108deae       cc          INT $0x3
  :-1           0x108deaf       cc          INT $0x3

TEXT main.main.func1(SB) /Users/liudanking/code/golang/gopath/src/test/main.go
  main.go:13        0x108deb0       65488b0c25a0080000  MOVQ GS:0x8a0, CX
  main.go:13        0x108deb9       483b6110        CMPQ 0x10(CX), SP
  main.go:13        0x108debd       0f8695000000        JBE 0x108df58
  main.go:13        0x108dec3       4883ec58        SUBQ $0x58, SP
  main.go:13        0x108dec7       48896c2450      MOVQ BP, 0x50(SP)
  main.go:13        0x108decc       488d6c2450      LEAQ 0x50(SP), BP
  main.go:15        0x108ded1       0f57c0          XORPS X0, X0
  main.go:15        0x108ded4       0f11442430      MOVUPS X0, 0x30(SP)
  main.go:15        0x108ded9       0f11442440      MOVUPS X0, 0x40(SP)
  main.go:15        0x108dede       488d059b020100      LEAQ runtime.types+65664(SB), AX
  main.go:15        0x108dee5       4889442430      MOVQ AX, 0x30(SP)
  main.go:15        0x108deea       488d0d0f2d0400      LEAQ main.statictmp_0(SB), CX
  main.go:15        0x108def1       48894c2438      MOVQ CX, 0x38(SP)
  main.go:15        0x108def6       488d1583fb0000      LEAQ runtime.types+63872(SB), DX
  main.go:15        0x108defd       48891424        MOVQ DX, 0(SP)
  main.go:15        0x108df01       488d1d107c0c00      LEAQ main.i(SB), BX
  main.go:15        0x108df08       48895c2408      MOVQ BX, 0x8(SP)
  main.go:15        0x108df0d       e84eddf7ff      CALL runtime.convT2E64(SB)
  main.go:15        0x108df12       488b442410      MOVQ 0x10(SP), AX
  main.go:15        0x108df17       488b4c2418      MOVQ 0x18(SP), CX
  main.go:15        0x108df1c       4889442440      MOVQ AX, 0x40(SP)
  main.go:15        0x108df21       48894c2448      MOVQ CX, 0x48(SP)
  main.go:15        0x108df26       488d442430      LEAQ 0x30(SP), AX
  main.go:15        0x108df2b       48890424        MOVQ AX, 0(SP)
  main.go:15        0x108df2f       48c744240802000000  MOVQ $0x2, 0x8(SP)
  main.go:15        0x108df38       48c744241002000000  MOVQ $0x2, 0x10(SP)
  main.go:15        0x108df41       e85a9dffff      CALL fmt.Println(SB)
  main.go:16        0x108df46       48c7042400ca9a3b    MOVQ $0x3b9aca00, 0(SP)
  main.go:16        0x108df4e       e87d27fbff      CALL time.Sleep(SB)
  main.go:15        0x108df53       e979ffffff      JMP 0x108ded1
  main.go:13        0x108df58       e813dffbff      CALL runtime.morestack_noctxt(SB)
  main.go:13        0x108df5d       e94effffff      JMP main.main.func1(SB)
  :-1           0x108df62       cc          INT $0x3
  :-1           0x108df63       cc          INT $0x3
  :-1           0x108df64       cc          INT $0x3
  :-1           0x108df65       cc          INT $0x3
  :-1           0x108df66       cc          INT $0x3
  :-1           0x108df67       cc          INT $0x3
  :-1           0x108df68       cc          INT $0x3
  :-1           0x108df69       cc          INT $0x3
  :-1           0x108df6a       cc          INT $0x3
  :-1           0x108df6b       cc          INT $0x3
  :-1           0x108df6c       cc          INT $0x3
  :-1           0x108df6d       cc          INT $0x3
  :-1           0x108df6e       cc          INT $0x3
  :-1           0x108df6f       cc          INT $0x3

显然,

    for {
        i += 1
    }

直接被优化没了。我们可以在语句 i += 1 添加一个其他语句来避免被优化掉:

    for {
        i += 1
        time.Sleep(time.Nanosecond)
    }

重新运行程序,运行结果“看似正确”了:

╰─➤  go run main.go                                                                                                                                     1 ↵
i am here 30
i am here 1806937
i am here 3853635
i am here 5485251
...

显然,如此修改之后,这段代码并非真正正确。因为变量 i 存在并发读写,即 data race 的问题。而 data race 场景下,go 的行为是未知的。程序员最讨厌的几件事中,不确定性必居其一。因此,一步小心写出 data race 的bug,调试起来是不太开心的。这里的例子因为只有几行代码,我们可以目测定位问题。如果代码规模比较大,我们可以借助 golang 工具链中的 -race 参数来排查该类问题:

╰─➤  go run -race main.go                                                                                                                               2 ↵
==================
WARNING: DATA RACE
Read at 0x0000011d4318 by goroutine 6:
  runtime.convT2E64()
      /usr/local/go/src/runtime/iface.go:335 +0x0
  main.main.func1()
      /Users/liudanking/code/golang/gopath/src/test/main.go:15 +0x7d

Previous write at 0x0000011d4318 by main goroutine:
  main.main()
      /Users/liudanking/code/golang/gopath/src/test/main.go:20 +0x7f

Goroutine 6 (running) created at:
  main.main()
      /Users/liudanking/code/golang/gopath/src/test/main.go:13 +0x53
==================
i am here 1
i am here 558324
i am here 1075838

除了在 go run 上可以使用 -trace, 其他几个常用的golang工具链指令也支持这个参数:

$ go test -race mypkg    // to test the package
$ go run -race mysrc.go  // to run the source file
$ go build -race mycmd   // to build the command
$ go install -race mypkg // to install the package

需要说明的是, -trace 并不保证能够检查出程序中所有的 data race, 而检查出 data race 则必然存在。说起来比较绕,大家记住它跟布隆过滤器 (Bloom Filter) 的真值表是一样的就对了。

而要把最开始提到的代码改对,方法有很多,我们可以使用 The Go Memory Model 推荐的 sync 包中的读写锁即可:

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

var i = 0

func main() {
    runtime.GOMAXPROCS(2)
    mtx := sync.RWMutex{}
    go func() {
        for {
            mtx.RLock()
            fmt.Println("i am here", i)
            mtx.RUnlock()
            time.Sleep(time.Second)
        }
    }()
    for {
        mtx.Lock()
        i += 1
        mtx.Unlock()
        time.Sleep(time.Nanosecond)
    }

扩展阅读