同步用户微信头像的 NodeJs 实现

共 6909字,需浏览 14分钟

 ·

2021-07-03 22:54

对于使用微信登录的系统,在用户授权后,将其微信头像直接同步到服务器,可以省去用户上传的操作。本文最终给出一个 NodeJs 中间层的实现,并展示实现的过程和在实施过程中几个需要注意的地方。


BFF 架构


微服务架构已然成为了企业信息化架构中的主流,这种架构风格给前端带来了挑战。为了灵活应对业务需求的变化和适配不同的前端用户体验,BFF 层应运而生。

由于天然的限制或者使用场景的区别,不同的前端用户体验并不一致。拿阿迪达斯的微信小程序和其原生 APP 举例,你会看到用户体验完全不同,有些是因为微信小程序的限制(比如分享体验),有些是不同的产品运营需要。


小程序

APP


BFF 是 Backend for Frontend 的简称,它用来对众多后端微服务进行聚合和裁剪,以适配前端。如今,端用户体验层 -> 网关层 -> BFF 层 -> 微服务层这种分层模式已经成为了典型的现代微服务架构分层方式。


NodeJs


NodeJs 的出现使得 JavaScript 可以运行在服务器上,并且天然适合网络 IO 密集型的场景,以及不适合计算密集型场景。这使得它作为 BFF 层非常适合,因为 BFF 层通常只是联结前端与后端,做一些透传,没有密集的计算,但是重网络传输。


分析微信头像的存储方案


直接存储微信头像的 url


比如,我目前的微信头像 url 是 https://thirdwx.qlogo.cn/mmopen/vi_32/rgPgbf5XE2ancz9ibobSibZEMPOibp4LdsQEXiaQeRZ78WJgVe7xgMamYXd6eibo9rg0Wje1rnh9aLMc87DVS4vrItA/132。显然后端可以很简单的接收一个字符串,将其存储起来,这样前端下次拿到这个 url,就可以展示出来。


但是这样做有个问题,以上链接是微信的 CDN 地址。一旦用户在微信端更新了头像,那么上面的地址将不再被使用。如果某一天它被清除了,那么系统的前端展示用户头像时将是一个死链接的图片。所以方案得改成:


将微信头像 url 下载下来以图片文件格式存储


这样就需要后端实现一个文件上传的接口,然后由 BFF 层把前端传过来的 url 转成表单数据传输给后端。所以最终后端不是存储一个字符串,而是存储图片文件。

这样就没有用户更改微信头像后,系统中的头像失效的问题。至于微信头像更新后,系统中还是老的图片的不同步问题,第一种方案也不能解决。实际上这种情况下只需要再次同步即可,至于如何自动同步,不在本文讨论范围内。


结论


只需要在 BFF 层使用 NodeJs 将微信头像的 url 下载下来,再调用后端的文件上传接口即可。


代码实现


需求分析明确后,只差写代码了。经常有人问,高手写代码是不是不用百度,直接啪啪啪就能写出来?实际上,不需要搜索就能写代码的,那说明是熟练工,同样的事情干过很多回了。对于高手,也可能接到不熟悉的任务,这时他可能不用百度,而是用 Google 和 StackOverflow。


Axios


既然要使用 NodeJs 上传文件到后端,那么就需要给后端发起一个 Http 请求。通过简单搜索就能知道在 NodeJs 的世界里,Axios 是一个不错的 Http 客户端,因此再进一步搜索如何使用 Axios 发起一个文件上传的 Http 请求。



搜索工具是程序员经常要使用的,虽然说如今搜索方便,但是要甄别结果的可靠性并没那么容易。被一些答案带到坑里是常有的事情。比如搜索使用 NodeJs 上传文件,多数答案如下:



var formData = new FormData();

formData.append("image", yourFile);

axios.post('upload_file', formData, {

    headers: {

      'Content-Type': 'multipart/form-data'

    }

})


注意上面的代码显式指定了 Content-Type 这个请求头,然后实际试过后你就知道这并不工作!


Postman


Postman 是一个强大的 Http 请求监控工具,可以按需定制请求体。BFF 层要同步微信头像,无非就是要调用后端接口,发送一个 Http 请求,将用户头像存储起来。因此真正的高手对这个需求是真的不会去搜索的,而是直接使用 Postman 构造一个 Http 请求,手动上传文件,拿到后端的响应结果。



然后,点击代码,就能选择将刚才手动构造的 Http 请求,转换成可以构造同样请求的代码。我们选择 NodeJs Axios:



抄作业


从 Postman 生成的代码可以看出,第一 NodeJs 的世界里,没有原生的表单数据结构,需要引入 form-data 包;第二在请求头里不能直接写死 Content-Type = 'multipart/form-data',而是要用 form-data 生成的请求头。


题外话


如果是前端直接文件上传,那么在 Browser 的 JavaScript 世界里,是自带 FormData 数据结构的,这时候要显式不指定 Content-Type,以实现自动生成 Content-Type 请求头。对于文件上传不能显示指定 Content-Type 的原因是,构造 Http 请求时,payload 中要使用 Content-Type 请求头中的 boundary 来分割文件和其他非文件字段,而这个 boundary 需要动态生成。如果不显示指定 Content-Type,就能享受浏览器端 FormData 或者 NodeJs 端的 form-data 自动生成的 Content-Type 以及 boundary。


TDD


在写实现代码前,建议先将自动化测试代码写上,以便构建重构屏障。详细步骤参考 TDD 相关的文章。


jest/nock/TypeScript


在实际的 NodeJs 工程项目中,还是建议引入 TypeScript,以享受类型系统带来的好处。这里使用 jest 测试框架。为了控制后端的 Http 响应,可以使用 nock 将之前的 Postman 抓到的后端服务器响应作为 mock。


后端服务器的 API 可能做了 token 验证,只信任指定的客户端(BFF 层)发来的请求,因此还需要做好相关 Token 端点的 nock,最终测试代码如下(假定要将实现写在一个叫 MemberService 的类中):


import { MemberService } from './member.service'

import * as nock from 'nock'


describe('MemberService', () => {

  beforeEach(async () => {

    const mockConfig = {

      backend: {

        url: 'https://your.back.end',

        auth: {

          url: 'https://your.back.end/auth/token',

          clientId: 'fakeId',

          clientSecret: 'fakeSecret',

          clientKey: 'fakeKey'

        }

      }

    }

   

    describe('update user\'s head image', () => {

      it('pipe weixin head img to back end', async () => {

        const mockRes = {

          code: 200,

          message: '操作成功',

          success: true,

          data: 'https://upload.image.url',

          time: '2021-06-29 11:20:30'

        }

       

        nock(mockConfig.backend.url).post('/auth/token').reply(200, {status: 'SUCCESS', data: {access_token: 'xxx', expires_in: 3600, refresh_token: 'yyy'}})

        nock(mockConfig.backend.url).put('/upload/image/head/abcdefg').reply(200, mockRes)

       

        const sut = new MemberService(nockConfig)

       

        const res = await sut.updateAvatar('abcdefg', 'https://thirdwx.qlogo.cn/mmopen/vi_32/rgPgbf5XE2ancz9ibobSibZEMPOibp4LdsQEXiaQeRZ78WJgVe7xgMamYXd6eibo9rg0Wje1rnh9aLMc87DVS4vrItA/132')

        expect(res).toStrictEqual(mockRes)

      })

    })

  })

})


流到流


前面分析了,实现代码只需要将微信的 url 对应的图片下载下来,再上传到后端服务器即可,但是为了提高效率,可以不用等待先全部下载完毕再进行上传,而是将下载流直接对接到上传流上。这只需要对 Postman 生成的代码稍加改造。仔细观察 Postman 生成的代码,由于我们是从本地文件系统选择的文件构造出的请求,因此生成的代码创建了一个本地文件读取流,我们需要把这个本地文件读取流改造成远程文件下载流。


下载文件其实也就是向微信服务器(CDN)端构造一个 Http GET 请求,仍然采用 Axios,那么只需要设置 responseType 为 stream,就能得到文件下载流:


import axios from 'axios'

import * as FormData from 'form-data'


export class MemberService {

  constructor(private readonly config: Config) {}

 

  async updateAvatar(userId: string, avatar: string | undefined) {

    if (!avatar) {

      return undefined

    }

   

    // 大致逻辑,实际上从统一的令牌管理类中拿可用的 token

   

    const {data: {access_token}} = await axios.post(this.config.backend.auth.url, {clientId, clientSecret, ...})

   

    const formData = new FormData()

    formData.append('headImg', (await axios.get(avatar, { responseType: 'stream' })).data, 'headImage.jpg')

   

    return axios.put(`${this.config.backend.url}/upload/image/head/${userId}`, {

      data: formData,

      headers: {

        'Authorization': `Bearer ${access_token}`,

        ...formData.getHeaders(),

      }

    })

  }

}


总结


在实际的 BFF 开发中,可以使用 Postman 手动调用后端服务,然后生成实际的代码,这节省了搜索的工作,而且保证代码可靠。


对于微信头像的同步,一定不能只保存微信的 CDN url,而要下载后保存图片。通过使用 NodeJs Axios,下载到上传是可以很方便地流到流接上的。


浏览 74
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报