公众号开发踩坑记录

1,492 阅读7分钟

真机调试公众号网页

微信公众号网页开发使用微信的 js-sdk 时,需要域名和公众号内配置的 js 接口安全域名一致,而我们开发调试过程中时不可能直接在线上进行的,因此需要一些操作,使我们在手机上访问我们在公众号内绑定的接口安全域名时,映射到我们开发的 PC 上 。

1. 修改本机 hosts 文件

打开 hosts 文件,Windows 上在 C:\Windows\System32\drivers\etc 文件夹下,将我们公众号内配置的域名指向 127.0.0.1 ,如下图:

image-20201103175122926

修改完毕之后保存,这时候跑起项目来,我们就可以在微信开发工具中通过访问 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,一般不需要更改。

image-20210112151054109

然后手机操作,确保手机和电脑在同一个 WIFI 下,配置代理,服务器填写我们电脑在局域网内的 ip,可以通过 ipconfig 命令查询,端口填写PC上Charles刚刚设置的端口,配置完毕后我们在手机端输入 test.com 就可以愉快的在手机 debug 我们的前端项目了。

image-20210112151552815 image-20210112151859033

微信 js-sdk 的配置问题

公众号开发大部分情况下都要用到微信提供的 js-sdk, 借助 js-sdk 可以使我们方便的调用手机的拍照、语音、位置、等手机系统功能以及一些微信特有的功能,这里我用到了 sdk 的拍照功能。在使用这个 sdk 前,需要先进行配置,执行 wx.config(options) 方法,配置成功后才可以进行调用。配置方法官网描述如下:

image-20210112161151569

其中参数 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} />
}