真机调试公众号网页
微信公众号网页开发使用微信的
js-sdk时,需要域名和公众号内配置的 js 接口安全域名一致,而我们开发调试过程中时不可能直接在线上进行的,因此需要一些操作,使我们在手机上访问我们在公众号内绑定的接口安全域名时,映射到我们开发的 PC 上 。
1. 修改本机 hosts 文件
打开 hosts 文件,Windows 上在 C:\Windows\System32\drivers\etc 文件夹下,将我们公众号内配置的域名指向 127.0.0.1 ,如下图:
修改完毕之后保存,这时候跑起项目来,我们就可以在微信开发工具中通过访问 test.com + port 来访问我们的项目了。接下来通过 nodejs 写几行代码,使我们可以直接在80端口上访问项目
2. 端口代理
建立一个 js 文件,在我们启动项目的时候运行一下这个 js 文件,项目跑起来后,我们访问 test.com 就能直接访问到我们开发的项目了
const http = require('http')
const httpProxy = require('http-proxy')
const proxy = httpProxy.createProxyServer()
http.createServer(function(req, res) {
// 8080 换成自己项目启动的端口
proxy.web(req, res, { target: 'http://127.0.0.1:8080' })
}).listen(80)
3. 手机代理设置
首先在电脑上安装Charles, Charles 是一个抓包工具,我们需要利用它来代理我们的手机网络请求,将我们手机的 test.com 的网络请求转发到本地。安装完毕后选择 代理 > 代理设置 设置代理端口,默认为8888,一般不需要更改。
然后手机操作,确保手机和电脑在同一个 WIFI 下,配置代理,服务器填写我们电脑在局域网内的 ip,可以通过 ipconfig 命令查询,端口填写PC上Charles刚刚设置的端口,配置完毕后我们在手机端输入 test.com 就可以愉快的在手机 debug 我们的前端项目了。
微信 js-sdk 的配置问题
公众号开发大部分情况下都要用到微信提供的 js-sdk, 借助 js-sdk 可以使我们方便的调用手机的拍照、语音、位置、等手机系统功能以及一些微信特有的功能,这里我用到了 sdk 的拍照功能。在使用这个 sdk 前,需要先进行配置,执行 wx.config(options) 方法,配置成功后才可以进行调用。配置方法官网描述如下:
其中参数 signature 的生成步骤需要公众号的 AppSecret ,因此这一步要交给后端来完成,前端需要把URL传递给后端,后端获取到签名后返回给前端。在这里有几个坑需要说明一下:
- 坑1:官网描述:
同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用。 由于在微信中url可能在某些条件下微信会加一些参数形如#STATE,因此路由尽量使用history模式,因此通过URL获取签名的时候URL获取方式为:location.href.split('#')[0]。测试之后发现在安卓机和开发者工具上都正常,在iOS上提示invalid signature,但是刷新一下就提示:config: ok了,肯定不能让用户去刷新的,一番搜索后发现在iOS上不管页面跳转几次都只认首次进入的URL,看到网上有些解决方案是缓存首次进入页面的URL,签名时使用缓存的URL,尝试了一下,都OK了,在iOS上尝试刷新一下结果又不行了,这次连invalid signature都没跳出来,wx.config执行了后完全没反应,但是再刷新一次又可以了。折腾了半天后也没弄好,后来想了一下,既然iOS只认首次进入的URL,那么直接在首次进入页面的时候 config 一下不就好了,尝试了一下,做一个判断,如果是iOS就只在最顶层组件执行 config,尝试之后完美解决,首页配置后,其他页面调用sdk都没问题,不管刷新多少次都能弹出config: ok。 - 坑2:拿到URL后需要使用
encodeURI编码一下,否则某些情况还是会出现invalid signature的错误,比如URL中含有拼接的json字符串参数。 - 坑3:在PC上的微信内置浏览器中配置成功的,提示:
config: ok但是在调用 sdk 具体功能时提示:permission denied。这个暂时没找到解决方案,不过我这里只用到了选择图片之类的一些基本功能,在PC上都很好实现,就做了一个判断,如果是PC微信浏览器就不使用微信的 sdk,直接前端实现。
js-sdk 选择多张图片问题
微信的 sdk 调用都是回调函数的方式,为了方便使用时对其进行了 promise 化封装,调用了 chooseImage 方法和 getLocalImgData 方法,直接返回图片的 base64 数据,最开始 chooseImage 封装方法如下:
// 有问题的版本
const chooseImage = params => {
return new Promise((resolve, reject) => {
wx.chooseImage({
...params,
success: res => {
const { localIds } = res
const results = localIds.map(localId => new Promise((innerResolve, innerReject) => {
wx.getLocalImgData({
localId,
success: response => innerResolve(response.localData),
fail: err => innerReject(err)
})
}))
return resolve(Promise.all(results))
}
})
})
}
上面的方法在选择单张图片的时候没出现问题,但是在选择多张图片的时候发现只返回了一张图片,于是面向搜索引擎编程的我在各种搜索后发现 getLocalImgData 这个方法不能直接遍历,如果遍历执行他就只执行一次后面就不执行了,于是改为递归调用的方式,改进如下:
// 可以正常选择多张的版本
const chooseImg = params => {
return new Promise((resolve, reject) => {
wx.chooseImage({
...params,
success: res => {
const { localIds } = res
const imgList = [] // 用来存储图片base64
const getResults = ids => {
const localId = localIds.shift()
wx.getLocalImgData({
localId,
success: response => {
const { localData } = response
imgList.push(localData)
if (ids.length > 0) {
getResults(ids)
} else {
// 递归结束
return resolve(imgList)
}
}
})
}
getResults(localIds)
},
fail: error => {
return reject(error)
},
})
})
}
移动端输入框 maxLength 遇到 emoji 的问题
在移动端使用 input 输入框或者 textarea 文本域的时候,当限制了 maxLength 的时候,在安卓上和 iOS 上表现不同,不同的 emoji 表情的 length 有 2、3、4 等不同的值,但是在 iOS 上 emoji 的长度却被算做了1,比如限制了 maxLength 为5,在安卓上输入两个表情后就无法输入了,而在 iOS 上却可以输入5个 emoji,这个时候在输入框下面添加字数展示的时候,value.length 显示的可能是10或者更大的值,已经超出了我们限制的 maxLength, 因此这里要做一下统一处理,使用 js 来限制字数而不能用 maxLength 来限制,这里搜索了一下,网上大部分用到的方法都是监听输入,处理emoji,然后使用 value.slice(0, maxLength) 来处理的,这里有一个弊端,就是当用户输入已经达到最大限制的时候,把光标移到输入的文本中间,继续输入,会在光标处插入新输入的内容,尾部原来的内容就被截没了,而限制了 maxLength 的输入框在这种情况下是不能输入任何内容的,也不会修改原来的内容,这显然与预期不符合,因此不能简单的使用 slice 截取。
这里先说一下含有 emoji 的字符串截取问题:上面说到 emoji 的length并不是1,因此当使用字符串的 slice 方法是就有可能遇到一个 emoji 表情被截一半的情况,这种情况下就会出现 � 这种方块问号的乱码,比如:
const emojiStr = '123😀'
console.log(emojiStr.length) // 5
emojiStr.slice(3, 4) // '�'
使用 string.split('') 方法将含有 emoji 的字符串转为数组的时候,emoji 表情都会被截成一个个的�,不过使用 Array.from(string) 方法可以将字符中的 emoji 保留而不被截断,如下:
const emojiStr = '123😀'
emojiStr.split('') // ["1", "2", "3", "�", "�"]
Array.from(emojiStr) // ["1", "2", "3", "😀"]
因此可以借助该方法处理含有 emoji 的字符串截取,方法如下:
const sliceEmoji = (str, start, end) => {
if (typeof str !== 'string' || start < 0 || end < 0 || start > end) {
throw '参数非法'
}
const strArr = Array.from(s).slice(start, end)
const slicedStr = s.slice(start, end)
// 取两个字符串前面相同的一部分
let result = ''
if (strArr.join('') === slicedStr) {
result = slicedStr
return result
}
for (let i = 0; i < strArr.length; i++) {
if (strArr[i] !== slicedStr[i]) {
result = strArr.slice(0, i).join('')
break
}
}
return result
}
有了上面的认识,我们就可以按照以下方式来处理限制 maxLength 的输入框:
const Textarea = () => {
const [selection, setSelection] = useState({ start: 0, end: 0 }) // 用来记录光标位置
const [value, setValue] = useState('') // 输入框值
const textareaRef = useRef(null)
const MAX_LENGTH = 20, // 这是根据需要设置的最大可输入值
const onSelect = e => {
const { selectionStart: start, selectionEnd: end } = e
const el = textareaRef.current
setSelection({ start, end })
}
const onChange = e => {
const { start, end } = selection
const val = e.target.value
const length = value.length
if (length <= MAX_LENGTH) {
// 输入内容未达到限制长度
setValue(val)
} else {
// 输入内容超出限制
const delta = length - val.length // 输入内容与现有内容的差值
const before = val.slice(0, start) // 光标之前的内容
const after = val.slice(end + delta) // 光标之后的内容
const diff = val.slice(start, delta) // 新输入的内容
const validVal = sliceEmoji(diff, 0, MAX_LENGTH - value.length) // 可输入的内容
const finallyVal = before + validVal + after // 输入框内最终的内容
setValue(finallyVal)
}
}
return <textarea ref={textareaRef} value={value} onSelect={onSelect} onChange={onChange} />
}