前言

8月底的时候接到了一个微信公众号网页开发的任务, 在此之前我从没开发过微信公众号网页的项目, 心想这回又能学到新东西了, 然后又是移动端项目, 可以放心地使用框架和es6+的语法提升开发体验了, 因为平时的项目很多都是jQuery, 并且需要兼容低版本ie浏览器.

接下来就是技术选型, 结合我擅长的react考虑, 移动端UI组件首先我就想到了ant design mobile[1], 然后看看微信网页开发#网页授权[2]的文档, 就可以上手开发了, 紧接着就是兵来将挡水来土掩了

但想到我对react已经相对比较熟悉, 而且公司前端开发团队里的其他人用的都是vue, 如果我也掌握了vue, 那么后续如果有一些紧急的项目, 比如之前有其他小伙伴做过的项目, 那么我拿到代码之后就能快速了解对方的代码逻辑, 从而能够借鉴已有的代码, 达到敏捷开发的目的, 既能学习一项新的框架, 同时还能有助于后续的开发, 而这仅仅只是多花点时间就能获得的, 何乐而不为呢

vue目前最新的版本是vue3, 之前用过一下vue3, 但并没有用vue3中的<script setup></script> 语法糖, 而是用的setup()方法, 然后整个文件的写法感觉就有些不伦不类, 看着像vue2对象的写法, 又像vue3组合式API的写法, 当然了, 也可能是我的姿势不对, 而这次我打算直接使用vue3<script setup></script> 语法糖以及组合式API, vue3组合式API看起来和reacthooks的写法非常相似, 上手应该没有太大的难度

而说起vue的移动端UI解决方案, 那就不得不提大名鼎鼎的vant[3]了, 我之前也没用过vant, 但见招拆招, 兵来将挡水来土掩, 至此, 技术选型就完成了, 剩下的就是踩(开)坑(发)了

说踩(干)就踩(干)

环境信息

  • os: win10

  • node: v16.17.0

  • yarn: 1.22.18

  • 编辑器: VS Code

package.json

先贴一下我的package.json:

{
  "name": "vue-project",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build --base=/path1/",
    "build:test": "vite build --mode=test --base=/path1/path2/",
    "preview": "vite preview --port 4173",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
  },
  "dependencies": {
    "axios": "^0.27.2",
    "vant": "^3.5.4",
    "vconsole": "^3.14.6",
    "vue": "^3.2.37",
    "vue-router": "^4.1.3",
    "weixin-js-sdk": "^1.6.0"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.1.4",
    "@vitejs/plugin-vue": "^3.0.1",
    "@vue/eslint-config-prettier": "^7.0.0",
    "@xianzhengquan/postcss-px-2-vw": "^0.0.1",
    "eslint": "^8.21.0",
    "eslint-plugin-vue": "^9.3.0",
    "object-assign": "^4.1.1",
    "prettier": "^2.7.1",
    "sass": "^1.54.4",
    "unplugin-vue-components": "^0.22.4",
    "vite": "^3.0.4"
  },
  "peerDependencies": {
    "postcss": "^8.4.16"
  }
}
复制代码

项目是按照Vue3官方文档[4]上的步骤来创建的, 这个文档上写的很详细, 这里就不再熬述了

VS Code扩展

这里我使用的是官方推荐的Vue Language Features \(Volar\)[5], 它提供了vue3的语法高亮功能, 以及它和著名的Vetur[6]有些冲突, 于是我就把Vetur卸载了, 但官方推荐的Volar并没有提供代码块, 而就在我苦恼之际, 我阅读Volar的文档后发现里面提到了一个Vue VSCode Snippets[7]的扩展, 于是我就安装了, 一开始不是太习惯, 后来就适应了, 我使用的是vbase-3-setup, 但一般为了方便, 我就只敲v3, 然后鼠标选择vbase-3-setup, 这样就会有一个vue3<script setup></script> 语法糖的基础代码块了

css预处理器

这里的css预处理器我选择的是scss, 文档是sass[8], 这里之所以用的是scss而不是我所熟悉的less, 主要是因为想借此机会学一学scss, 还有就是上面提到的Vue VSCode Snippets所提供的模板中的css预处理器用的也是scss, 于是我就直接使用scss了, 它的语法和less有些类似, 嵌套的写法类似, 继承有些不同, 有差异, 但就我目前使用的程度上来说还好, 其他的文档上解释得也比较详细, 这里就不展开了

使用scss而不是sass做文件扩展名是因为scsscss超集, css能用的语法在scss中也能用, 同时由于它和css的相似性, 使得它更容易上手

vite.config.js

vite的配置如下:

import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';

// https://cn.vitejs.dev/
export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [VantResolver()]
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
});
复制代码

主要是做了vant 按需引入的配置, 这同样也是vant文档中提到的一个方法: vant#按需引入组件样式[9]

lint

lint的配置每个人都有每个人的习惯, 在这之前我一直用的是eslint, eslint-plugin-react以及react/recommended, 同时也配置了保存时候对代码进行lint格式化的操作:

{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}
复制代码

但这里还是想和大家聊一聊vue3默认的lint配置, 不是因为它用的是prettier, 而是因为它默认的lint规则背后作者的一些想法, 由于和我以前的习惯相悖, 一开始我还是有些排斥的, 但最后慢慢理解了, 就接受了, 同时觉得挺了不起的, 不愧行业中顶尖的人, 首先贴一下我的配置:

.eslintrc.cjs:

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-prettier'
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  },
  rules: {
    'prettier/prettier': [
      'error',
      {
        endOfLine: 'auto', //行末换行符: auto表示保持当前的换行符
        singleQuote: true, //是否使用单引号替代双引号: 使用
        bracketSpacing: true, //是否在对象字面量中括号之间使用空格: 使用
        trailingComma: 'none', //在多行逗号分隔的句法结构中尽可能打印尾部逗号: 从不
        vueIndentScriptAndStyle: true //是否在vue script和style标签中缩进: 使用
      }
    ]
  }
};
复制代码

Delete CR错误

关于这个问题, 掘金上有大佬做了详细的解释, 详情可以看这篇文章: Delete `␍`eslint\(prettier/prettier\) 错误的解决方案[10], 我司开发人员用的电脑都是windows系统, 于是我就直接将其配置成了auto

vue script和style标签的缩进问题

vueIndentScriptAndStyle是否在vue scriptstyle标签中缩进, 尤大的习惯是不缩进, 而我个人的习惯是缩进, 这个因人而异, 没有好坏对错之分, prettier官网 Vue files script and style tags indentation[11]中有尤大的一个解释: Indent script and style tags content in \*.vue files#a comment from the creator of vue[12], prettire增加了一个选项来让用户使用, 我个人很喜欢这个选项

arrowParens(箭头函数圆括号)

接下来是箭头函数圆括号的问题, 一直以来我都是尽可能的省略圆括号, 比如只有一个参数的时候我会省略它的圆括号:

const foo = a => {};
复制代码

多个参数的时候才写:

const foo = (a, b) => {};
复制代码

我在做这个项目之前, 一直都是这个写法, 而这个项目默认的配置却是: 无论有多少参数都不省略圆括号. 我觉得这样比较地影响观感, 一个参数写什么括号嘛, 看着多难看, 又不报错, 多个参数才需要写括号, 不写就会报错了, 当然在用ts的时候, 一个参数也要写括号的, 这样才能声明该参数的类型, 比如:

const foo = (a: string): void => {};
复制代码

可我还是不理解为何它要采用无论有多少参数都不省略圆括号的策略, 于是我就去prettier的官网上看了看, 文档Arrow Function Parentheses[13]中关于箭头函数圆括号的解释给了我答案:

At first glance, avoiding parentheses may look like a better choice because of less visual noise. However, when Prettier removes parentheses, it becomes harder to add type annotations, extra arguments or default values as well as making other changes. Consistent use of parentheses provides a better developer experience when editing real codebases, which justifies the default value for the option.

乍一看,避免使用小括号可能是一个更好的选择,因为可以减少视觉噪音。然而,当Prettier删除小括号时,就很难添加类型注释、额外的参数或默认值以及进行其他修改。在编辑真正的代码库时,小括号的一致使用为开发者提供了更好的体验,这证明了该选项的默认值是合理的。

看完这段话, 瞬间醍醐灌顶, 是啊, 有了小括号, 才使得ts的类型注释成为了可能, 以及也方便设置默认值, 所有参数都使用括号使得我们的代码具有了高度的一致性, 易读也易用, 于是我就将这个用法保留了下来

vant相关问题

postcss.config.js

vite.config.js中对vant进行了配置之后, 就不需要再次在main.js中进行注册了, 直接在我们的vue文件中使用即可

除了这个按需引入之外, 还有移动端适配, 这里我使用的是vw的方案, rem方案考虑到要监听浏览器缩放事件, 频繁修改根字号, 而且配置要比这个繁琐, 再加上vw移动端的兼容性已经很好了, 而且我也想在实际项目中用一用, 之前只用过rem, vw还没有特别完整的项目使用, 于是就采用了vw的方案

vant官方文档#浏览器适配[14]里面推荐了一个px2vw的解决方案: postcss-px-to-viewport[15], 这个方案倒不是说不好, 只是照文档上来使用的时候postcss给我报了个warning:

postcss-px-to-viewport: postcss.plugin was deprecated. Migration guide:
https://evilmartians.com/chronicles/postcss-8-plugin-migration
复制代码

对于这个警告, 我最终在插件的issue中找到了解决方案: 支持 postcss-8[16], 改用评论中提到的这个插件: \@xianzhengquan/postcss-px-2-vw[17]就没有这个迁移的警告了, 同时还要安一下object-assign[18], 不然会报错

我参照警告的链接文章里所说的对postcss做了优化, package.json最终改为了上面贴出来那样, 具体的配置和由来参考postcss.config.js文件:

//https://github.com/evrone/postcss-px-to-viewport
//https://github.com/evrone/postcss-px-to-viewport/issues/83

module.exports = {
  plugins: {
    '@xianzhengquan/postcss-px-2-vw': {
      viewportWidth: 750
    }
  }
};
复制代码

以及在一开始的时候我打算看看postcss是如何配置rem的, 因为之前使用rem方案都是加载的js文件, 比如页面中直接加载rem.js这样的, 并没有使用postcss, 而这个尝试花了我3个小时, 最后就一句话: Tips: 在配置 postcss-pxtorem 时,同样应避免 ignore node_modules 目录,否则会导致 Vant 样式无法被编译。, 这也是我对postcss不熟导致的, 为了避免坑到其他人, 我也给vant提了一个pr: docs: add tips in advanced-usage.zh-CN.md[19], 同时这里也附上一些配置的解释, postcss-px-to-viewport同样适用:

配置释义

//详细配置请查看: https://github.com/cuth/postcss-pxtorem#readme
/**
 * rootValue:
 * 最终元素值 = 设计稿上的值 / rootValue;
 * 比如设计稿上某元素某属性是40px, rootValue为100, 那么最终那个元素的那个属性会被转换为0.4rem;
 * 可是这个时候会发现, 字号还是小了, 因为默认根字号是16px, 而不是我们设置的100px, 因此需要修改真正的根字号,
 * 也就是html标签的font-size值
 *
 * replace (Boolean) Replaces rules containing rems instead of adding fallbacks.
 * 替换包含rem的规则, 而不是增加回退规则
 * 默认为true, 表示直接将px转换为rem
 * 若为false, 除了转换px为rem外, 还会保留原来的px单位
 * 例如:
 * div{
 *  width: xx px;
 * }
 * replace: true, 转换结果为:
 * div{
 *  width: xx / rootValue rem;
 * }
 * replace: false, 转换结果为:
 * div{
 *  width: xx px;
 *  width: xx / rootValue rem;
 * }
 */

module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 100, //根元素字号(单位px)
      unitPrecision: 5, //转换成rem后, 小数点后保留几位
      propList: ['*'], //需要转成rem单位的属性
      selectorBlackList: [], //选择器黑名单(哪些不用转成rem)
      replace: true, //是否替换包含rem的规则, 而不是增加回退规则
      mediaQuery: false, //是否转换媒体查询中的px为rem
      minPixelValue: 0, //需要转换的最小px值
      exclude: '' //排除项, 即不需要转换的目录或文件
    }
  }
};
复制代码

样式定制/覆盖

这个需求比较频繁, 毕竟有设计稿, 不太可能完完全全按照UI组件库的默认样式来开发, 于是我就对它的样式进行了一些配置, 详细内容请查阅ConfigProvider 全局配置[20], 我这里使用的是修改基础变量的方式: 基础变量[21], 同时文档示例工程[22]中提到的示例仓库: Vant Demo[23]中提供了另一种配置方式, 具体可以查看代码: theme config[24], 只是这个方式我并没有尝试过, 有用过的小伙伴欢迎在评论区留言讨论

List组件bug

在使用vant的过程中我发现List会额外触发一次load事件, 这个问题可查看\[Bug Report\] vant-list 监听的scroll事件 和 vue-router的push后维持scroll冲突[25], 该issue中是路由切换导致额外触发一次load, 本项目中数据多了, 导致触发了scroll事件, 因此List会多触发一次load事件, 解决方法参考了上面提到的那个issue, 同时这里贴一下关键代码:

<template>
  <div v-if="state.isRender">
    <van-list></van-list>
  </div>
</template>

<script setup>
  import { reactive } from 'vue';
  import api from '@/api';

  const state = reactive({
    data: [],
    isRender: true
  });

  const fetchData = async () => {
    state.isRender = false;
    const res = await api.getData();
    if(res.code === 0) {
      state.data = res.data;
      state.isRender = true;
    }
  };
</script>
复制代码

每次都初始化List, 这样就不会多触发一次load事件了, 具体可以查阅上面提到的那个issue

调试工具

这里我用的调试工具是vConsole[26], 使用起来非常方便, 同时功能强大, 它在手机端页面给我们提供了类似chrome中的控制台, 是手机端页面调试的利器

组合式函数/组合式API

这里主要是我个人使用vue3的一些记录, 不敢说写得多么好, 就只是我个人的一些方法, 由于业务逻辑并不是特别的复杂, 具体的vue的使用就不展开了, 这里主要用了一些组合式函数, 个人觉得它和react中的自定义hooks有些像, 同时文档中推荐以use开头, 比如我这里用到的useHtmlBgc.js:

import { onMounted } from 'vue';
/**
 * @param {string} type 操作类型 set | remove
 * @param {string} color 颜色值
 */
export const useHtmlBgc = (type, color = '#f2f2f7') => {
  const htmlNode = document.documentElement;

  if (type === 'set') {
    onMounted(() => {
      htmlNode.style = `background-color: ${color};`;
    });
  } else {
    htmlNode.removeAttribute('style');
  }
};

复制代码

设置html的背景色. 有的页面背景色和其他页面不一样, 这个时候需要单独处理一下, 然后离开那个页面的时候移除一下, 因为下一个页面的背景色和它不一样, 还是贴一下关键代码:

import { onBeforeRouteLeave } from 'vue-router';
import { useHtmlBgc } from '@/utils/useHtmlBgc';

useHtmlBgc('set');

onBeforeRouteLeave(() => {
  useHtmlBgc('remove');
});
复制代码

vue3官网对于组合式函数有更详细的介绍, 详情可移步官方文档: 组合式函数[27]

微信公众号网页开发

接下来就是微信公众号网页开发的主要内容了, 都是我个人遇到的坑, 在此和大家分享一下, 如果能帮到大家就再好不过了

太长不看版

  1. 路由推荐使用history形式的, hash形式的路由最主要的影响是url参数, 因为#及其后面内容无法以查询字符串参数的形式传递(可使用encodeURIComponent处理一下, 但history形式的路由就没有这个烦恼了, 详情可查阅这个贴子: A sharp in URL parameter[28]), 如果实在是要用hash形式的, 注意获取到的code会在#前面, 同时如果有其他参数的时候也要留意

  2. 签名urlwindow.location.href.split('#')[0], 这也是官方文档推荐的写法, 并且留意前端传递的url, 实际发起请求的url, 后端用于签名的url三者要一致

  3. 苹果手机权限验证配置的时候会以第一次进入页面的url为准, 同时jsApiList也会以第一次的为准, 要么提前配置所有可能用到的接口, 要么只在每次用户产生交互的时候配置

  4. 获取本地图片wx.getLocalImgData返回的base64数据需要处理一下才可用于img标签显示

网页授权流程

官方文档: 微信网页开发#网页授权[29], 我这个项目的后端之前做过一个微信公众号网页开发的项目, 因此他保留了之前的代码, 而之前的项目中, 网页授权获取code是后端来做, 因此我这里也就沿用了之前的做法, 即后端来做, 大致流程如下:

访问https://open.weixin.qq.com/connect/oauth2/authorize获取code可以前端来做(前端拼地址, 官网文档上的做法), 也可以后端来做, 后端的处理方式是接收一个url, 也就是文档中提到的redirect_uri, 然后302https://open.weixin.qq.com/connect/oauth2/authorize(后端帮忙拼), 这里需要注意redirect_uri要用encodeURIComponent处理一下(当使用hash路由的时候尤其需要, 不然#以及后面的字符将无法传递过去), 要让浏览器不把它当一个地址, 而是一个回调地址参数, 微信做跳转的时候会做解码的操作; 后端拼的话注意后端的接口不能以ajax的形式调用, 而应该以window.location.href={后端接口}的形式使用, 只有地址栏中的链接返回302, 浏览器才会去找响应头中的Location做重定向, ajax请求则不会

然后是获取openid, 以及用户信息, 这部分操作也可以后端来做, 比如我这个项目就是后端来做的, 我获取到code之后再次调后端的接口, 然后后端给我返回用户信息以及openid, 具体操作文档上也比较详细, 就不再熬述了, 接下来和大家分享一下我遇到的坑

坑一: 登录和重定向(回调页面redirect_uri)

涉及到微信的网页授权, 那自然是需要登录了, 我们在登录页做登录的操作, 登录成功了再走后续的逻辑

由于之前有运维小伙伴nginxhistory路由配置了很久很久最后耽误上线的先例, 于是我把脚手架初始化的项目时候默认的history路由改成了hash形式的路由, 以及我从入行开始接触的前端路由的形式就一直都是history形式的, 对hash形式的不熟, 只知道#后面的部分服务端不认, 只是浏览器认, 以为这个问题不影响, 然而这其实就是问题的答案了: #及其后面的内容无法发送到服务端.

#在浏览器端主要是起一个分隔定位的作用, 比如我们最常见的页面锚点, 但请求后端的时候#及其后面的部分是无法发送到后端的. 详情可以看看阮一峰老师的这篇文章: URL的井号[30]. 简而言之就是我传递的#及其之后的参数丢失了, 目前解决方案是后端来帮我拼#/login(后端302 响应头中的Location字段中是可以使用#的), 不然我传过去的就成/了, 就不对了, 就进不到登录页而会去到首页了. 其实最好的方式是我这边将#做一个转义即可, 就是用上面提到的encodeURIComponent, 但当时我并没能想到这个方法, 和项目经理(后端)聊了聊我遇到的问题之后他说他协助我来解决, 于是最终就由他那边来处理了

那具体情况是怎样的呢? 我们一起来看一下吧:

  1. 我们在登录页: http://xxx/#/login进行登录的操作, 也就是获取code再获取openid去进行登录的操作

  2. 登录页给后端提供的获取code的接口传当前页面的url, 然后去网页授权获取code openid登录, 获取code的过程会进行多次跳转, 获取到code之后会带着code跳转回登录页: http://xxx/#/login?code={code}

然后问题就出现了: 获取到code之后不是带着code跳转到登录页, 而是带着code跳到了首页. 这就很奇怪了啊, 回调页面参数我明明是在登录页传的当前页面的url, 也就是登录页的url, 怎么会是带着code跳到首页呢? 多方调试之后我才发现, 是因为#/login没有传过去导致的, 也就是说, 我传递的是http://xxx/#/login, 但后端收到的却是http://xxx/, 因此获取到code之后的跳转地址从http://xxx/#/login变成了http://xxx/, 也就是获取到code之后带着code跳转到了首页而非登录页

坑二: 权限验证配置(wx.config)

这个问题是我这个项目中遇到的最蛋疼的的问题, 我想也有不少人遇到, 因为网上各种问这个的, 也有各种解答这个的, 然后各种方式都有各种方式的道理, 但是似乎都不对, 而这正是这个问题的"奇妙"之处...

invalid signature

这是我遇到的最多次的问题, 网上搜了之后发现很多人也遇到过这个问题, 遇到配置错误可参照官方文档上进行排查: 附录5-常见错误及解决方法[31], 这个问题我调了好久, 总结一下大概需要注意那么几点:

  1. 不要使用hash形式的路由, #和它后面部分会传不过去, 以及网页授权获取code的时候, 获取成功回调回来的时候code参数会在#之前, 这可能会对签名造成影响, 需要考虑进去

  2. 调后端签名接口的时候传的url用这个: window.location.href.split('#')[0](这也是官方文档中推荐的做法)

  3. 参照附录5-常见错误及解决方法[32]invalid signature的排错步骤逐一排查

  4. 确保传给后端的url和实际发起请求的url, 以及后端拿去签名的url三者一致

  5. 苹果手机权限验证配置的时候会以第一次进入页面的url为准(后面会提供可用的js代码)

传给后端的url和实际发起请求的url一致这个问题都比较好调试, 关键是后端拿到我们传的url去签名的时候是不是就是我们传过去的url, 他们有没有再加其他的参数, 我这里遇到过一次报错是因为后端之前做微信公众号网页项目的时候苹果手机需要多加一个state=这样的参数才行, 然后我这个项目的时候也加了这个参数, 从而导致的报错

以及苹果手机需要特别处理一下, 下面代码封装部分会详细阐述

the permission value is offline verifying

这是我遇到另一个问题, 只是这个错是在苹果手机上遇到的, 安卓手机上并没遇到, 造成这个问题的原因是: 苹果手机权限验证配置的时候不但会以第一次进入页面的url为准, jsApiList也会以第一次的为准. 解决方式是: 要么提前配置所有可能用到的接口, 要么只在每次用户产生交互的时候配置

我的处理方式是前者: 提前配置所有可能用到的接口

我这里需要禁用分享hideMenuItems, 以及选择图片chooseImage, 苹果手机提前配置二者, 然后在点击的时候就不再单独配置选择图片了, 不然就会报错: chooseImage: the permission value is offline verifying, 具体的代码下面会提供

坑三: 获取本地图片(wx.getLocalImgData)无法显示问题

选择图片之后将图片转为base64, 但遇到一个问题: 得到的base64无法显示, 这个问题我推测是差个类型, 为什么是推测呢, 因为这个问题最终是后端解决的, 本来是我这边来处理的, 但我在调微信权限验证配置的问题, 后端刚好有空, 就他处理了. base64无法转成图片, 一般就是差个类型: data:image/jpeg;base64,, 这个问题需要留意

处理微信相关逻辑的参考代码

这里也贴一下我自己的代码, 欢迎小伙伴们在评论区跟我留言讨论

获取程序的运行平台的方法

getPlatform.js:

export const getPlatform = () => {
  let platform = 'android';

  if (navigator.userAgent.includes('iPhone')) {
    platform = 'ios';
  }

  return platform;
};
复制代码

签名的时候需要传平台参数, 因此需要判断一下当前运行平台是安卓还是iOS

根据运行平台处理签名url的方法

handleSignatureUrlByPlatform.js:

/**
 * @param {string} type get | set
 */
import { getPlatform } from '@/utils/getPlatform';

export const handleSignatureUrlByPlatform = (type) => {
  let url = window.location.href.split('#')[0];

  if (getPlatform() === 'ios') {
    if (type === 'set') {
      sessionStorage.setItem('ios-1st-entry-url', url);
      return;
    } else {
      url = sessionStorage.getItem('ios-1st-entry-url');
    }
  }

  return url;
};
复制代码

安卓手机每次路由变更都重新获取url, 苹果手机使用第一次进入的url, 也就是说苹果手机需要记录第一次进入程序时候的url, 后续使用的时候要从sessionStorage当中取, sessionStorage的键名可以根据实际情况来修改, 比如在App.vue中调用一下这个方法, 然后就会根据当前的环境和操作类型做处理了:

App.vue:

import { handleSignatureUrlByPlatform } from '@/utils/handleSignatureUrlByPlatform';

handleSignatureUrlByPlatform('set');
复制代码

后续在需要获取url的代码中直接调handleSignatureUrlByPlatform()或者handleSignatureUrlByPlatform('get')使用即可

权限验证配置方法(wx.config)

handleWxConfig.js:

import wx from 'weixin-js-sdk';
import api from '@/api';
import { getPlatform } from '@/utils/getPlatform';
import { handleSignatureUrlByPlatform } from '@/utils/handleSignatureUrlByPlatform';

export const handleWxConfig = (jsApiList) => {
  return new Promise((resolve, reject) => {
    api
      .retrieveWxJsSdkConfig({
        platform: getPlatform(),
        url: handleSignatureUrlByPlatform()
      })
      .then((res) => {
        if (res.code === 0) {
          wx.config({
            // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
            debug: false,
            appId: res.data.appId, // 必填,公众号的唯一标识
            timestamp: res.data.timestamp, // 必填,生成签名的时间戳
            nonceStr: res.data.nonceStr, // 必填,生成签名的随机串
            signature: res.data.signature, // 必填,签名
            jsApiList // 必填,需要使用的 JS 接口列表
          });
          wx.ready(function () {
            // config信息验证后会执行 ready 方法,所有接口调用都必须在 config 接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在 ready 函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在 ready 函数中。
            resolve(true);
          });
          wx.error(function (res) {
            // config信息验证失败会执行 error 函数,如签名过期导致验证失败,具体错误信息可以打开 config 的debug模式查看,也可以在返回的 res 参数中查看,对于 SPA 可以在这里更新签名。
            reject(res);
          });
        } else {
          reject(res.msg);
        }
      });
  });
};
复制代码

方法只接收一个参数jsApiList, 哪里需要配置哪里就调用这个方法, 同时传需要的jsApiList即可. 方法内部获取签名数据的api名称可以修改为自己的, 我这里是api.retrieveWxJsSdkConfig(), 参数如果不一样也可以修改, 但一般都是平台参数和url参数, 返回的结构一般也一样. 整个方法封装成了Promise, 外部使用的时候方便以同步的形式来处理异步逻辑

选择图片(chooseImage)和获取本地图片(getLocalImgData)的方法

这里主要是将它们封装成了Promise, 便于使用

handleWxChooseImg.js:

import wx from 'weixin-js-sdk';

export const handleWxChooseImg = () => {
  return new Promise((resolve, reject) => {
    wx.chooseImage({
      count: 1, // 默认9
      sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
      sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
      success(res) {
        resolve(res);
        // 返回选定照片的本地 ID 列表,localId可以作为 img 标签的 src 属性显示图片
        // var localIds = res.localIds;
      },
      fail(error) {
        reject(error);
      }
    });
  });
};
复制代码

handleWxGetLocalImgData.js:

import wx from 'weixin-js-sdk';

export const handleWxGetLocalImgData = (localId) => {
  return new Promise((resolve, reject) => {
    //获取本地图片
    wx.getLocalImgData({
      localId, // 图片的localID
      success(res) {
        // res.localData是图片的base64数据,可以用 img 标签显示
        resolve(res);
      },
      error(error) {
        reject(error);
      }
    });
  });
};
复制代码

人脸认证方法(选择图片之后将图片转为base64)

这里由于业务方的缘故, 因此并没有真正的做人脸认证, 只是简单得选一张图片或者拍个照传给后端

handleFaceAuth.js:

import { handleWxChooseImg } from '@/utils/handleWxChooseImg';
import { handleWxGetLocalImgData } from '@/utils/handleWxGetLocalImgData';
//选择图片(拍照/相册), 接着转为本地图片数据, 然后将本地图片数据返回出去
export const handleFaceAuth = async () => {
  try {
    const res1 = await handleWxChooseImg();
    const res2 = await handleWxGetLocalImgData(res1.localIds[0]);
    return res2;
  } catch (error) {
    return Promise.reject(error);
  }
};
复制代码

如果前端来处理base64无法显示的问题, 那么这里的res2.localData就需要处理一下了, 这里写一下伪代码, 仅供参考:

try {
  const res1 = await handleWxChooseImg();
  const res2 = await handleWxGetLocalImgData(res1.localIds[0]);
  if (res2.localData.indexOf('data:image') === -1) {
    res2.localData = `data:image/jpeg;base64,${res2.localData}`;
  }
  return res2;
} catch (error) {
  return Promise.reject(error);
}
复制代码

同时这里需要注意的是try...catch...语句中的catch捕获到的errortry代码块中抛出的第一个错误, 而这里具体指的是Promise reject的结果, 也就是Promise构造函数中reject方法中接收的参数, 但如果async函数中直接return这个error的话得到的将是resolve的结果, 而不是reject结果, 也就是说这么做会导致async函数返回的Promise的状态是fulfilled, 要想得到rejected的结果, 必须使用Promise.reject方法处理一下: return Promise.reject(error)

而具体使用自然是应该先进行配置wx.config, 然后再使用, 例如:

const handleClick = async () => {
  await handleWxConfig(['chooseImage']);
  const res = await handleFaceAuth();
};
复制代码

async方法里面的await会依次执行, 当且仅当第一个await之后的Promise resolve了, 确切的说是状态变成了fulfilled之后, 第一个await之后的代码才会执行, 也就是说当这个Promise的状态是pending或者rejected的时候, 它后面的代码一定不会执行. pending变为fulfilled则后续代码正常执行, pending变为rejected则后续代码不执行, 且会抛出错误, 这个错误可以由try...catch...语句中的catch捕获, 因此上面的写法和下面这个写法是等价的:

const handleClick = async () => {
  const isSuccess = await handleWxConfig(['chooseImage']);
  if(isSuccess) {
    const res = await handleFaceAuth();
  }
};
复制代码

全局导航守卫(afterEach)处理微信权限验证配置(wx.config)设置微信分享

由于所有页面都需禁止分享hideMenuItems, 因此可以考虑在全局导航守卫中处理, 然后选择图片chooseImage的话用户每次点击的时候再进行配置, 当然了, 需要区分一下安卓和苹果手机, 具体代码如下:

路由配置文件中, 关键代码:

import { getPlatform } from '@/utils/getPlatform';
import { handleWxConfig } from '@/utils/handleWxConfig';

router.afterEach(async () => {
  //所有页面都要隐藏分享相关按钮
  const finalJsApiList =
    getPlatform() === 'ios'
      ? ['hideMenuItems', 'chooseImage']
      : ['hideMenuItems'];

  await handleWxConfig(finalJsApiList);
  wx.hideMenuItems({
    menuList: [
      'menuItem:share:appMessage', //发送给朋友
      'menuItem:share:timeline', //分享到朋友圈
      'menuItem:share:qq', //分享到QQ
      'menuItem:share:weiboApp', //分享到Weibo
      'menuItem:share:facebook', //分享到FB
      'menuItem:share:QZone' //分享到 QQ 空间
    ]
  });
});
复制代码

苹果手机提前配置; 安卓手机只配置禁止分享hideMenuItems, 后续用户点击的时候再单独配置选择图片chooseImage即可

这里需要留意一下, 微信分享需要每个页面都配置, 比如只在A页面配置, 那么在A页面分享是正常的, 但换到B页面, 然后在B页面分享的话分享就不正常了, 就没法获取描述和logo图片了, 但是每个页面都配置代码就比较冗余, 此时也可以在全局导航守卫中处理

这里也贴一下关键代码:

handleWxShare.js:

import wx from 'weixin-js-sdk';

//需要在微信签名配置成功之后调用, 这个分享包括分享给朋友和分享到朋友圈, 以及签名的时候记得配置分享相关的jsApiList
export const handleWxShare = () => {
  const title = '分享的标题';
  const desc = '分享的描述';
  const link = import.meta.env.VITE_WXSHARE_LINK;
  const imgUrl = import.meta.env.VITE_WXSHARE_IMG_URL;
  wx.updateAppMessageShareData({
    title, // 分享标题
    desc, // 分享描述
    link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号 JS 安全域名一致
    imgUrl, // 分享图标
    success() {
      // 设置成功
      console.log('“分享给朋友”及“分享到QQ”按钮的分享内容设置成功');
    }
  });
  wx.updateTimelineShareData({
    title, // 分享标题
    link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号 JS 安全域名一致
    imgUrl, // 分享图标
    success() {
      // 设置成功
      console.log('“分享到朋友圈”及“分享到 QQ 空间”按钮的分享内容设置成功');
    }
  });
};
复制代码

titledesc一般是固定的, linkimgUrl可能会因为发布的环境不同而不同, 比如生产环境和测试环境, 因此我把这两个参数写成了环境变量, 具体的使用也是要在配置之后进行:

router.afterEach(async () => {
  await handleWxConfig([
    'updateAppMessageShareData',
    'updateTimelineShareData'
  ]);
  handleWxShare();
});
复制代码

好的, 这就是这篇文章的全部内容了, 欢迎大家在评论区和我一起交流探讨, 最后, 如果你觉得这篇文章写得还不错, 别忘了给我点个赞, 如果你觉得对你有帮助, 可以点个收藏, 以备不时之需

关于本文

作者:_音魂不散_

https://juejin.cn/post/7152024109755400223

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

Logo

前往低代码交流专区

更多推荐