0%

前言

最近完成了一个内容分发类的小程序,具体核心功能是提供稿件的编辑,上传,评论等等(公众号plus);应用到的框架为mpvue和vant-weapp,框架的选型是我最熟悉的框架,主要原因为项目比较赶,虽然之前研究过uniapp,毕竟没有用它写过,踩坑也需要时间,所以最终敲定了这个技术框架作为这个小程序的基础。由于这次做的小程序类型之前并没有接触过,但也因此碰到了一些微信不常用组件的问题。

踩坑记录

  1. editor组件

稿件的编辑是核心功能之一,由于稿件需要同时展示图片及文本,和后端协商后选定了一个最简单的方案,即存储为富文本;小程序端能用rich-text组件展示,后台管理端也没有问题,数据库使用text存储。后台管理端使用tinymce进行富文本编辑,而小程序,我只找到了editor组件。

说问题:

  • editor组件无法上滑,只能使用输入框的光标移动
  • editor组件在输入到底部时,输入回车,光标会移动到底部不可见,必须输入内容时才会显示
  • editor组件提供了很多api,但是很多内容需要自己实现,可以参考这位老哥(富文本编辑器封装
  • editor组件虽然输入功能完善,可以上传图片,修改大小等,功能很全。但是实机体验很差,需要用户的学习成本;组件的focus会和自定义按键冲突,键盘会不断唤起。

在这些问题的影响下,最终砍掉了editor组件的大部分功能,只保留了最基础的文字编辑功能,更多对样式的编辑留在了web后台。这里我有几个改进的想法,具体再看项目后期的需求。

  • 使用textarea + 图片上传 替换掉editor组件,最终合成富文本提交后台。用户可以选择添加段落或图片,操作成本会降低,缺点是无法自定义样式,但同样统一的样式有助于小程序端展示风格的统一。我觉得在没有能力做到和公众号编辑相同的技术力之前,这是最经济的做法。
  • 在编辑时跳转一个只有editor组件的页面,去掉其他所有组件,专注于解决键盘的问题。这个方案需要研究,有一定的风险。
  • 可以自己写一个虚拟键盘,将自定义按键做在这个键盘里。这个或许是终极解决方案。(微信官方可以照这个改
  • 坐等官方改bug(无限延期
  1. ios与安卓实际展现不一样

这个问题由来已久,实际上,微信小程序的创举之一就是双端可以同时运行,在不同机型上有相同的体验(虽然有些特别的机型有特别的bug,说的就是iponeX)。但有一说一,是坑就等认,是bug就得填。

ios在js中对new Date()方法格式要求与安卓不同
这个问题是之前的小程序中发现的,这次统一提一下。
一般来说,后台存储的日期格式为yyyy-MM-dd,js中的new Date()方法可以正确解析这个日期,但是ios与众不同,它的js只支持解析日期格式yyyy/MM/dd,由于普通安卓手机也支持解析这个格式,所以需要对后台传来的日期字符串做统一处理:

1
2
const dateString = "2020-12-14 00:00:00"
const date = new Date(dateString.replace(/-/g,'/'))

感觉麻烦的同学可以要求(威胁)后台做统一的数据处理。。。
一般后台(这里指Java),可以通过fastjson的注解(一个一个加);或者通过配置统一拦截器对date数据格式进行处理(这些配置一般可以使用在long转string上,因为js的number类型对java的long类型有精度缺失)。

ios的input焦点丢失有问题(疑似bug)
在一般表单设计中,通常涉及input和picker。在ios实机体验中,先点击input输入框,再点击picker(原生picker或vant组件picker都一样),键盘无法自动收起。
这里的解决方法是调用一个微信的apiwx.hideKeyboard(),缺点是开发者工具无法正常显示。

1
2
3
4
5
6
7
8
9
10
popup(showPicker) {
// console.log(this[showPicker])
// this[showPicker] = true
// 解决ios键盘不收起的问题,开发者工具时可以注掉
wx.hideKeyboard({
complete: res => {
this[showPicker] = true
}
})
}

ios中包含editor组件的页面中position: fixed;表现与安卓不同
具体信息可以看这个 bug反馈
暂时只是体验不好,没有修改。二楼老哥的做法

iOS的只能获取键盘高度之后手动设置bottom距离

可以一试。

思考反思

这次这个项目的规模不算大,共有25个页面,其中只有两个页面相似重复(一个tabbar页面,一个普通页面,tabbar页面不好传参)。许多稿件列表页面重用,效果很好。由于项目较赶,没有开发业务组件,一些相似的元素(如列表中的稿件item)没有重用,修改ui需要同步修改,所幸页面不多,能够顾及。
回顾整个开发过程,有几点仍需改进:

  • 需要设计几个常用页面模板(list,detail,info),整理几个常用方法(分页,输入,跳转页面),这样能大大加快开发速率。
  • 使用ui组件或许需要自己fork一个版本,需要对其进行定制化开发
  • 前后端交互需要文档,不然后期维护非常麻烦
  • 可以根据命名区分模块,方便以后遇到相似业务可以直接迁移模块
  • 学习样式类的模块化开发,方便统一修改
  • 通过ScreenToGif软件记录动态效果,方便(水一篇博客)思考反思,学习成长

前言

虽然说都快1202年了,还在使用mpvue可能已经跟不上时代。但是mpvue仍然是我认为写小程序前端最靠近vue写法的框架,而且从使用情况来看,即使已经两年没有更新,使用mpvue框架编写小程序前端依然没有什么致命的问题,我觉得只要小程序的主要框架不发生大的改变,mpvue的使用就没有太大的问题。
从移动端前端的发展来看,小程序的出现是革命性的。最主要的是小程序不仅提供了较为简单的开发环境,降低了开发成本;同时提供了国民级软件(如微信)的用户信息。这点尤为重要:app的构建太过沉重,用户量的累积也过于漫长,这点限制了规模较小商户的会员体系构建,而小程序解决了这个问题,并同时为双方创造收益,商户获得了用户流量入口,微信也可以借此扩大自身的影响。

问题

由于mpvue的生命周期,它并不完全等同与小程序的生命周期,在页面切换的时候很容易发现一个问题:之前页面表单填写的内容还在,之前页面的弹窗还没有因为页面销毁而隐藏。。。然后打log一看,原来是值没有重置,于是就在onLoad里更新了一下值。一个还好,一堆呢?
一个一个赋值太麻烦了,好在这个问题一行代码就能搞定
Object.assign(this.$data, this.$options.data())
在onLoad或在onUnload方法加上这行代码就能让data恢复初始值

深入

这个问题的出现源于mpvue在相同页面时共用一个实例,并且在小程序onUnload时并未销毁。具体问题在github/mpvue/issue_140中解释的很清楚,解决方案也有很多。

Object.assign()对象的拷贝

基本用法
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
同时对于第一级,这个方法是深拷贝,而对于更深的层次则是浅拷贝

Object.assign()这个方法让我想起的es6中的扩展运算符(…),这两者的实现近乎相同,果然:

object spread spec 明确指出{… obj}等同于Object.assign({},obj)

这两者还是有一点区别的,如果我将Object.assign(this.$data, this.$options.data())改为this.$data = {...this.$options.data()},会报错

TypeError: Cannot set property “prop” of #Object which has only a getter (Chrome)

具体参考
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only

可以理解为Java实体没有setter,属性只读。

吐槽一下报错信息,为什么要叫只有一个getter,而不叫没有setter,这样不是更清楚吗

其中关键的区别是:Object.assign()函数是直接修改其第一个传入对象obj,而扩展运算符(…)的使用更像是一个赋值操作,因此会触发setter。

最后

为了代码的简洁和易读性,使用扩展运算符(…)更好,但是也有无法使用的情况,这时不要忘记还有一个Object.assign()函数可以代替。

好家伙,报错那行有导致md解析出问题的字符,更骚的是我不能在这行打出来…

题目

传送门:灯泡开关

初始时有 n 个灯泡关闭。
第 1 轮,你打开所有的灯泡。 第 2 轮,每两个灯泡你关闭一次。 第 3 轮,每三个灯泡切换一次开关(如果关闭则开启,如果开启则关闭)。
第 i 轮,每 i 个灯泡切换一次开关。 对于第 n 轮,你只切换最后一个灯泡的开关。
找出 n 轮后有多少个亮着的灯泡。

思路

开始思维很简单:如果我要知道n个灯泡的开关情况,只需要知道n-1个灯泡的情况+第n个灯泡的情况;而第n个灯泡的情况很显然跟它的因子个数相关。比如12有6个因子,经过偶数次开关,灯泡为暗。由此得到以下代码:

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int bulbSwitch(int n) {
if(n==0) return 0;
if(n==1) return 1;
int r = 1;
for(int i=1;i<=n/2;i++){
if(n%i==0) r++;
}
return bulbSwitch(n-1) + r%2;
}
}

那么很显然,这个代码的时间复杂度为O(n²)。
在n=99999时就已经超时了。


接下来,我对计算因子个数的方法进行了优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public int bulbSwitch(int n) {
if(n==0) return 0;
if(n==1) return 1;
int r = dcpCount(n);
return bulbSwitch(n-1) + r%2;
}
public int dcpCount(int x){//所有因子的个数(包括1)
int ans = 1;
for(int i = 2; i * i <= x; i++){
if(x % i == 0){
int temp = 0;
while(x % i == 0){
x /= i;
temp++;
}
ans *= (temp+1);//运用上面的公式,计算所有因子的个数
}
}
if(x > 1) ans *= 2;
return ans;
}
}

这比循环n/2次快了许多,但仍然在最后9999999时超时了。
看来按照一开始的思路注定是走不通了。


先上代码

1
2
3
4
5
class Solution {
public int bulbSwitch(int n) {
return (int)Math.sqrt(n);//别问,问就是强转
}
}

很神奇,但这是为什么呢?
在之前的思路中,我们都是通过计算一个数的因子个数来判断灯泡的亮暗情况。事实上,我们无需计算个数,只需知晓个数奇偶就行。通过观察发现,大部分因子都是成双成对的,只有一种可能,那就是平方数,只有平方数拥有奇数个因子,它们的灯泡才会亮。问题迎刃而解。

总结

这是一道很有意思的题,设置的非常巧妙。
妙啊

免费CDN

找到了一个好用的cdn,mark一下

github资源在国内加载速度较慢,因此需要使用CDN提高速度。JSDelivr + Github就可以提供非常好用的CDN服务。

对于个人来说,可以将图片存在github上;通过cdn访问链接,实际上达到了oss的效果。

具体使用

准备一个GitHub仓库,将图片上传至这个仓库。

cdn的路径为
https://cdn.jsdelivr.net/gh/${你的用户名}/${你的仓库名}@${分支或版本}/文件路径

例子

https://cdn.jsdelivr.net/gh/init-qy/init-qy.github.io@master/images/avatar.gif

https://cdn.jsdelivr.net/gh/init-qy/init-qy.github.io/images/avatar.gif

如果分支为master,那么可以只需仓库名即可

背景

由于钉钉没有现成的类似 vant-weapp 这样的picker选择组件,于是打算自己封装一个。使用到的依赖有mini-ali-ui的popup和button组件。使用了原生的picker-view

1
2
3
4
5
6
7
{
"component": true,
"usingComponents": {
"popup": "mini-ali-ui-rpx/es/popup/index",
"button": "mini-ali-ui-rpx/es/button/index"
}
}

思路

由于是自己使用的一个组件,考虑到使用情况大多数为多列,可能需要联动的情况,参考了vant-weapp中picker联动的做法,将onChange事件暴露出去,由页面自行实现联动切换效果,将列表进行动态赋值。
这里我实现了一个setColumnValues方法,作用是对列表赋值;但实际上只需重新为props赋值就行,这样更加方便。

  • 单列选择也可以使用此组件,支付宝也有多列选择,但是钉钉没有
  • position为bottomtop,左右暂不考虑实现
  • 没有考虑自定义样式,作为一个form表单的选择组件,样式统一比较好
  • onChange是实现多列联动的核心
  • onConfirmdetail.value 为选项index数组, detail.detail 为选项值数组

实现

axml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<popup show="{{show}}" animation="{{animation}}" position="{{position}}" onClose="onCancel" zIndex="{{zIndex}}">
<view class="box">
<slot name="toolbar" a:if="{{position === 'bottom'}}"><view class="toolbar">
<button type="text" onTap="onCancel">{{cancelButtonText}}</button>
<text a:if="{{title}}" class="title">{{title}}</text>
<button type="text" onTap="onConfirm">{{confirmButtonText}}</button>
</view></slot>
<picker-view value="{{value}}" onChange="onChange" class="my-picker">
<picker-view-column a:for="{{columns}}">
<view a:for="{{item.values}}" a:for-index="_index" a:for-item="_item">{{_item}}</view>
</picker-view-column>
</picker-view>
<slot name="toolbar" a:if="{{position === 'top'}}"><view class="toolbar">
<button type="text" onTap="onCancel">{{cancelButtonText}}</button>
<text a:if="{{title}}" class="title">{{title}}</text>
<button type="text" onTap="onConfirm">{{confirmButtonText}}</button>
</view></slot>
</view>
</popup>
acss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.box{
background: #fff;
height:500rpx;
}
.toolbar{
padding: 0 24rpx;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
}
.am-button{
font-size: 28rpx;
}
.title{
font-weight: 600;
}

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import fmtEvent from "mini-ali-ui/es/_util/fmtEvent";
const noop = function noop() {};
Component({
mixins: [],
data: {
value:[]
},
props: {
show:false,
animation:true,
position:'bottom',
zIndex:100,
title:'',
columns:[],
cancelButtonText:'取消',
confirmButtonText:'确认',
onCancel:noop,
onChange:noop,
onConfirm:noop,
},
didMount() {
let t = []
t.length = this.props.columns.length
t.fill(0)
this.setData({value: t})
},
didUpdate() {},
didUnmount() {
},
methods: {
onChange(e) {
const value = e.detail.value
let index = -1
value.forEach((e,idx)=> {
if(e!==this.data.value[idx]) index = idx
})
this.setData({value:value})
e.detail.picker = this
e.detail.index = index
const event = fmtEvent(this.props, e);
this.props.onChange(event);
},
onCancel(e){
const event = fmtEvent(this.props, e);
this.props.onCancel(event);
},
onConfirm(e){
const value = this.data.value
let detail = []
e.detail.value = value
value.forEach((e,idx)=> {
detail.push(this.props.columns[idx].values[Number(e)])
})
e.detail.detail = detail
const event = fmtEvent(this.props, e);
this.props.onConfirm(event);
},
// setColumnValues(index,values){
// this.setData({
// [`columns[${index}]`]: values ,
// [`value[${index}]`]: 0
// })
// }
},
});

思考

  • 可以考虑更多样式
  • 其实最多三列,更多不适合这样展示,可以做限制
  • 使用popup和picker的组合,可以考虑替换原生的日期选择