fengxianqi
type: channel
link: https://www.fengxianqi.com/
description: 前端实战笔录；静心沉淀，思考进阶。
items: 微信jsapi签名报错config:invalid url domain和config:invalid signature问题
items: SQL联表去重查询和优化
items: 如何给项目添加eslint校验
items: 前端配合nginx代理微信头像
items: 为什么说rollup比webpack更适合打包库
items: 微前端qiankun从搭建到部署的实践
items: 前端部署和提效：从静态到node再到负载均衡
items: Centos8使用docker迁移typecho博客
items: axios如何利用promise无痛刷新token（二）
items: axios如何利用promise无痛刷新token

微信jsapi签名报错config:invalid url domain和config:invalid signature问题
type: item
date: 2023-07-26T12:19:12+00:00
description: 背景记录下最近遇到了jsapi签名的报错：config:invalid url domainconfig:invalid signature排查步骤1. 先根据官方文档的步骤先排查一遍这是常规的解决办法，大部分的报错都是这些原因导致的，请确认没问题再往下排查参考：企业微信：https://developer.work.weixin.qq.com/document/path/90509公众号附录5：https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html2. 疑难杂症的处理2.1 在微信中失败，在企业微信正常这个主要针对的是企业微信的自建应用（猜测是企业微信的老应用没有同步问题），可以尝试一下这个操作：可信域名删掉，保存；再重新填写，保存停用当前应用，然后再启用应用参考：https://developer.work.weixin.qq.com/community/question/detail?content_id=164827725388820618882.2 在iOS系统失败，在安卓正常问题原因：获取签名时，安卓应为实时域名，iOS应为首次进入域名解决办法：解法1：链接通过location.href这类方法打开，而不是使用vue、react的路由。解法2：判断是iOS且非企业微信时，把页面首次地址记录到window下的变量中，获取签名时取该地址作为签名地址，可参考https://blog.csdn.net/Sweet__Cat/article/details/88591688解法2,vue2处理方案代码：// router.js在页面首次进入时把url存放到`window._ios_entry_url_`变量中 // 判断是否是ios系统 export function isIos () { return /(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent) } // 判断是否是企业微信 export function isWxWork () { return /wxwork/i.test(navigator.userAgent) } router.beforeEach((to, from, next) => { // 企业微信没有该问题，因此只针对微信内做处理 if (isIos() && !isWxWork() && !window._ios_entry_url_) { // 此处不可使用location.replace window._ios_entry_url_ = window.location.href } next() }) // 在需要获取签名的地方如page.vue，传参url时区分iOS和企业微信 let webUrl = window.location.href if (isIos() && !isWxWork() && window._ios_entry_url_) { // 在ios系统，vue history模式的url不会变，会一直是首次访问的链接：https://www.fengerzh.com/jssdk-invalid-signature/ // 否则会出现签名失败的问题 webUrl = window._ios_entry_url_ } // 发起请求获取签名 getWechatConfig({ webUrl }) 参考：https://developers.weixin.qq.com/community/develop/doc/000ae2cb950808f90d8bc415551800

SQL联表去重查询和优化
type: item
date: 2021-07-14T03:44:00+00:00
description: 项目背景一个工单管理系统，工单处理流程（简化后）是： 创建工单 -> 分派工单 -> 处理工单 -> 完成工单。表结构数据库用的是SQL Server（MySQL其实也类似），有两张表： 工单表Order 和 工单记录表OrderRecord。 工单记录表里面记录者每张工单的操作记录，是一对多的关系。 工单表Order： idtitlestatus1工单122工单213工单33id是自增idtitle是工单的标题status是工单当前状态， 1(待处理), 2(处理中), 3(已完成) 工单记录表OrderRecord ：idrecordTypeoperatororderIdcreatedAtremark11张三12021-07-01 22李四12021-07-02 33张三12021-07-03 44张三12021-07-04 52李四12021-07-05需要返工，重新分派工单61王五22021-07-05 73王五12021-07-06 84王五12021-07-07 id是自增idrecordType记录类型的枚举，有：1(创建工单), 2(分派工单), 3(处理工单), 4(完成工单)。operator操作者名字。orderId该记录所属的工单id，对应Order表的id字段。createdAt 记录创建时间。remark 备注信息。查询一张工单的操作记录，如查询工单id为1的操作记录： select * from OrderRecord where orderId=1，配合程序得到的结果如下：记录ID操作类型操作人操作时间备注1创建工单张三2021-07-01 2分派工单李四2021-07-02 3处理工单张三2021-07-03 4完成工单张三2021-07-04 5分派工单李四2021-07-05需要返工，重新分派工单7处理工单王五2021-07-06 7完成工单李四2021-07-07 通过这个操作记录可以看到，工单处理完成后，可能由于一些特殊原因处理地不好，这张工单中途被李四重新分派出去了，然后由王五进行了完工。需求为了统计员工的工作量来做绩效管理，所以老板想要统计每个月员工的工单完工情况，需要导出一个数据报表。因为特殊原因，工单的分派者的工作量很大，所以老板希望在统计报表里面看到工单分派员是谁（工单有多次分派情况时以最后一次分派为准），报表内容大概如下：工单ID工单名称完工时间分派员1工单12021-07-07李四3工单32021-07-08张三需求应该很容易理解，就是查询某个月的完工单的情况，而且需要从工单的记录表里面找到最新的一个分派员，这个SQL语句应该怎么写呢？需求实现工单的完工时间和分派员需要从 OrderRecord 表里面取值，而且一张工单会有多次分派、多次完工的情况，比较复杂。我们一步一步来实现这个过程。1. 查询所有的完工单通过表的定义，我们知道，当status=3时表示工单已完工了，所以我们首先实现查询所有的完工单：select * from Order status=32. 查询某个月份的完工单每个月的完工单，我们暂时不考虑给Order表加字段的情况，而是通过 OrderRecord 进行联表查询。 假设每个工单只能完工一次（即recordType=4的情况只会出现一次），那么我们可以很轻松地联表查询去做到，SQL这样写就可以了：select * from Order o left join OrderRecord or on or.orderId=o.id where o.status=3 and or.recordType=4 and or.createdAt between '2021-07-01' and '2021-08-01'但是，咱们这里的工单是允许完工多次的，上面这条语句会导致出现多条工单id一样的记录，而且没法用distinct去重，所以上面这条SQL语句没法满足需求。上面知道工单1有两次完工的记录，我们查询该工单的完工记录的SQL语句如下：select * from OrderRecord where ordeId=1 and recordType=4得到的结果如下：idrecordTypeoperatororderIdcreatedAtremark44张三12021-07-04 84王五12021-07-07 我们的需求是：实际的完工时间是取最后一次完工时间，所以要取记录ID为8的这一条作为该工单的完工时间。即按时间倒序排取第一条： select top 1 * from OrderRecord where ordeId=1 and recordType=4 order by createdAt desc。但如果和工单表联表查询的话没法使用top 1这种写法呀，…

如何给项目添加eslint校验
type: item
date: 2021-06-24T06:26:00+00:00
description: 前提给一个纯原生的项目添加eslint，如：remember-scroll。目标做到项目运行时自动提示语法错误。做到 git commit 时校验语法错误。步骤1. 安装和配置eslint安装eslintnpm install eslint --save-dev初始化.eslintrc.jsnpx eslint --init可能还需配置一些global变量和exclude node_modules，博主这里最终的.eslint.js如下：module.exports = { "env": { "browser": true, "es2021": true }, "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 12, "sourceType": "module" }, "rules": { }, globals: { "process": true, "__dirname": true, "module": true, } }; 在根目录添加.eslintignore:node_modules dist2.git commit添加校验使用lint-staged 添加git commit钩子npx mrm@2 lint-staged这个命令会根据package.json里面的依赖工具，自动安装和配置好 husky和 lint-staged，跑完后就能在git commit。该命令还会更改我们的package.json文件，注意其中有一行是： "lint-staged": { "*.js": "eslint --cache --fix" }这就是在git commit时执行的命令： eslint --cache --fix。--cache表示每次只校验有修改的文件，该参数会在根目录生成一个.eslintcache文件，我们需要在.gitignore中忽略这个文件。如果想每次都全量校验所有文件，可以把该参数去掉，去掉后就不会生成.eslintcache文件了。--fix 表示自动修复。3.项目运行时自动提示语法错误由于我项目使用的是rollup来打包，因此需要在rollup.config.js里面去新增一个eslint相关的插件来做到，如果是webpack项目请自行搜索对应的插件或配置方法。安装@rollup/plugin-eslintnpm install @rollup/plugin-eslint --save-devrollup.config.js配置import eslint from '@rollup/plugin-eslint' export default { // ... plugins: [ eslint({ throwOnError: true, throwOnWarning: true, include: ['src/**'], exclude: ['node_modules/**'] }), // ... ] }至此，已经实现了我们的前面提到的两个目标啦，可以测试一下。

前端配合nginx代理微信头像
type: item
date: 2021-06-05T13:32:00+00:00
description: 有个企业微信的应用，最近想要在页面里面展示当前用户的微信头像。用户的头像是在授权登录后，可以拿到userId，然后服务端再通过获取用户个人信息的接口拿到用户的个人信息，包括头像，存到数据库里。为了不增加额外的存储成本，所以不会将用户的头像再上传到我们自己的存储服务，使用时是直接使用微信头像对应的url从微信服务器中取，得到的微信头像url如下：http://wework.qpic.cn/bizmail/ia7iaXMXfXKuOiapqsvvaibY332FrQpdWPGrbbfGhIeObj2dmXZPJfFLOA/0问题如果我们的前端页面想要直接通过 插入到页面中，会出现跨域问题:Access to image at 'http://wework.qpic.cn/bizmail/ia7iaXMXfXKuOiapqsvvaibY332FrQpdWPGrbbfGhIeObj2dmXZPJfFLOA/0' from origin 'http://${your-domain}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.解决思路上面说了，为了省成本，我们不会在自己的服务器上再存一份头像的图片，所以拷贝到自己服务器的方案就不考虑了。主要两个方案：后端写个接口帮忙获取图片，获取到的图片转为base64返回给前端。前端拼接特殊url前缀，通过nginx代理转发到微信图片服务器。两个方案其实都差不多，我们来看下第二种方案的具体思路。前端发起/wechat_image前缀的请求，url上携带原微信头像的路径，如:/wechat_image?url=http://wework.qpic.cn/bizmail/ia7iaXMXfXKuOiapqsvvaibY332FrQpdWPGrbbfGhIeObj2dmXZPJfFLOA/0&host=wework.qpic.cnnginx配置拦截 location /wechat_image的代理。实现步骤1. 处理url从接口拿到avatar的url：http://wework.qpic.cn/bizmail/ia7iaXMXfXKuOiapqsvvaibY332FrQpdWPGrbbfGhIeObj2dmXZPJfFLOA/0我们可以使用new URL来解析一个url，这里以vue代码为例： 2. nginx拦截配置考虑到微信头像的域名可能有多个，所以我们的代理需要根据url动态改变。但nginx有个安全限制，就是如果proxy_pass后面是一个参数变量，它会报一个no resolver defined to resolve的错误。因此需要在http节点配置一下：resolver 8.8.8.8 ipv6=off;http { # 解决no resolver defined to resolve错误 resolver 8.8.8.8 ipv6=off; server { listen 80; location /wechat_image { proxy_set_header Host "$arg_host"; proxy_pass $arg_url; } # 其他的配置... } }PS: 在nginx中，可以通过$arg_name来获取url后的query参数。可以看到使用了两个参数$arg_url和$arg_host，它们就是上一步在url上拼接的参数，在这个例子里面，他们分别是http://wework.qpic.cn/bizmail/ia7iaXMXfXKuOiapqsvvaibY332FrQpdWPGrbbfGhIeObj2dmXZPJfFLOA/100 和 wework.qpic.cn。本来host参数也是可以在url上截取的，但在nginx中不好处理，所以索性传多一个host参数了。实践证明， proxy_set_header Host "$arg_host";是必不可少的，不然的话微信图片服务器不会返回真正的图片。

为什么说rollup比webpack更适合打包库
type: item
date: 2020-12-28T23:29:00+00:00
description: 前言大概一年前写了个小小的js插件 remember-scroll，并且分享了一篇文章：用Class写一个记住用户离开位置的js插件,是一个纯js库，功能是在用户再次进入页面时能自动定位到上一次浏览的位置，使用webpack+babel打包，里面的webpack和babel的配置至今看来也算是很典型的。前端打包工具有很多——webpack,gulp,rollup等等，网上有很多文章分析它们分别更适合哪些场景，webpack更适合打包组件库、应用程序之类的应用，而rollup更适合打包纯js的类库。因此笔者一直有想法尝试将 remember-scroll 的打包工具由webpack更换为rollup，从实际应用的角度来对比一下两者的区别。从零配置rollup安装rollup和一些插件npm i rollup rollup-plugin-uglify rollup-plugin-filesize @rollup/plugin-node-resolve @rollup/plugin-commonjs -Drollup-plugin-uglify 用于压缩混淆打包后的js。rollup-plugin-filesize打包后在控制台显示文件大小。@rollup/plugin-node-resolve让 rollup 能够识别node_modules的第三方模块。@rollup/plugin-commonjs将 CommonJS 的模块转换为 ES2015 供 rollup 处理。添加babelnpm i @rollup/plugin-babel @babel/core @babel/plugin-transform-runtime @babel/preset-env core-js@2 -D@rollup/plugin-babel rollup 的babel插件。@babel/core babel核心。@babel/plugin-transform-runtime 用于避免污染全局函数（不是必须要用到，但作为类库最好要加上）。@babel/preset-env 自动根据目标浏览器注入相关的polyfill。core-js polyfill的类库，这里使用的是2.x版本（使用3会增加包的大小）。根目录下的babel.config.js如下：const presets = [ [ '@babel/env', { useBuiltIns: 'usage', corejs: { version: 2 } }, ], ] const plugins = [ '@babel/plugin-transform-runtime' ] module.exports = { presets, plugins }根目录下新建rollup.config.js,全部配置如下：import filesize from 'rollup-plugin-filesize' import babel from '@rollup/plugin-babel' import resolve from '@rollup/plugin-node-resolve' import { uglify } from 'rollup-plugin-uglify' import commonjs from '@rollup/plugin-commonjs' const isProd = process.env.NODE_ENV === 'production' export default { input: 'src/index.js', output: { file: isProd ? 'dist/remember-scroll.min.js' : 'dist/remember-scroll.js', format: 'umd', exports: 'default', name: 'RememberScroll', }, plugins: [ resolve(), commonjs(), filesize(), babel({ babelHelpers: 'runtime', exclude: ['node_modules/**'] }), (isProd && uglify()) ] }package.json打包命令如下： "build": "rollup -c --environment NODE_ENV:production && rollup -c", "dev": "rollup -c --watch",总之，一切配置都是与之前webpack版本的一样，都使用了babel。npm run build就可以将资源打包到dist了，接下来我们对比一下webpack和rollup两个…

微前端qiankun从搭建到部署的实践
type: item
date: 2020-09-22T16:46:00+00:00
description: 最近负责的新项目用到了qiankun，写篇文章分享下实战中遇到的一些问题和思考。示例代码： https://github.com/fengxianqi/qiankun-example。在线demo：http://qiankun.fengxianqi.com/单独访问在线子应用：subapp/sub-vuesubapp/sub-react为什么要用qiankun项目有个功能需求是需要内嵌公司内部的一个现有工具，该工具是独立部署的且是用React写的，而我们的项目主要技术选型是vue，因此需要考虑嵌入页面的方案。主要有两条路：iframe方案qiankun微前端方案两种方案都能满足我们的需求且是可行的。不得不说，iframe方案虽然普通但很实用且成本也低，iframe方案能覆盖大部分的微前端业务需求，而qiankun对技术要求更高一些。技术同学对自身的成长也是有强烈需求的，因此在两者都能满足业务需求时，我们更希望能应用一些较新的技术，折腾一些未知的东西，因此我们决定选用qiankun。项目架构后台系统一般都是上下或左右的布局。下图粉红色是基座，只负责头部导航，绿色是挂载的整个子应用，点击头部导航可切换子应用。参考官方的examples代码，项目根目录下有基座main和其他子应用sub-vue，sub-react，搭建后的初始目录结构如下： ├── common //公共模块 ├── main // 基座 ├── sub-react // react子应用 └── sub-vue // vue子应用基座是用vue搭建，子应用有react和vue。基座配置基座main采用是的Vue-Cli3搭建的，它只负责导航的渲染和登录态的下发，为子应用提供一个挂载的容器div，基座应该保持简洁（qiankun官方demo甚至直接使用原生html搭建），不应该做涉及业务的操作。qiankun这个库只需要在基座引入，在main.js中注册子应用,为了方便管理，我们将子应用的配置都放在：main/src/micro-app.js下。const microApps = [ { name: 'sub-vue', entry: '//localhost:7777/', activeRule: '/sub-vue', container: '#subapp-viewport', // 子应用挂载的div props: { routerBase: '/sub-vue' // 下发路由给子应用，子应用根据该值去定义qiankun环境下的路由 } }, { name: 'sub-react', entry: '//localhost:7788/', activeRule: '/sub-react', container: '#subapp-viewport', // 子应用挂载的div props: { routerBase: '/sub-react' } } ] export default microApps 然后在src/main.js中引入import Vue from 'vue'; import App from './App.vue'; import { registerMicroApps, start } from 'qiankun'; import microApps from './micro-app'; Vue.config.productionTip = false; new Vue({ render: h => h(App), }).$mount('#app'); registerMicroApps(microApps, { beforeLoad: app => { console.log('before load app.name====>>>>>', app.name) }, beforeMount: [ app => { console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name); }, ], afterMount: [ app => { console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name); } ], afterUnmount: [ app => { console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name); }, ], }); start();在App.vue中，需要声明micro-app.js配置的子应用挂载div（注意id一定要一致），以及基座布局相关的，大概这样： 头部导…

前端部署和提效：从静态到node再到负载均衡
type: item
date: 2020-01-20T07:58:00+00:00
description: 前言相信很多前端同学对 vue 或 react 的开发很熟悉了，也知道如何去打包生成一个生产环境的包，但对于生产环境的部署可能有些同学了解比较少。小公司可能都是后端帮忙部署了，大公司会有专门的运维同学部署，对于生产环境的部署工作有些同学接触的不多，所以这次来分享和总结下前端项目部署相关的实战经验：从静态站点的部署，到 node 项目的部署，再到负载均衡的部署，顺便也会分享一下提高部署效率的脚本和方法。准备工作一台或多台服务器或虚拟机。一份 vue 或 react 项目的打包后的文件。一份 node 项目的源码。静态站点的部署静态站点的部署指的是前端的 html/css/js 资源的部署，如 vue 或 react 打包后生成的 html，css 和 js 文件，我们将这些文件上传到服务器后，通过 Nginx 将这些资源暴露到公网上。上传文件到服务器就是将文件人工将打包后的文件拷贝到服务器上，这个很简单，但如果每次都是人工拷贝，部署效率未免会低了一些，所以建议使用一些脚本或工具。在大公司，一般对服务器权限控制得很严格，可能需要各种跳板机或动态密码等，而大公司一般都有专门的运维人员或者 CI/CD 工具。小公司可能相对自由一些，可以允许个人直接 ssh 连接服务器，此时可以配合使用rsync或scp命令（Linux 或 Mac 系统）来一键上传文件到服务器上，给部署提效。在这里分享一下以前使用过的部署脚本,在前端项目根目录新建一个名为deploy.sh的文件：#!/bin/bash function deploy() { # 测试服务器 test_host="root@test_server_ip" # 生产服务器 prod_host="root@prod_server_ip" project_path="/srv/YourProject" if [ "$1" == "prod" ]; then target="$prod_host:$project_path" else target="$test_host:$project_path" fi rsync -azcuP ./dist/ --exclude node_modules --exclude coverage --exclude .env --exclude .nyc_output --exclude .git "$target" echo "deploy to $target" } deploy $@以上脚本的意思是将/dist目录下的所有文件，上传到对应服务器的/srv/YourProject目录下。测试环境的部署是直接在根目录运行./deploy.sh，该命令会将/dist目录直接上传到root@test_server_ip服务器上；生产环境的部署是在后面加一个参数./deploy.sh prod，这样可以实现多环境部署。更进一步的做法是将运行脚本的命令直接写进package.json中，如： "scripts": { "build": "vue-cli-service build --mode staging", "deploy": "npm run build && ./deploy.sh", "deploy:prod": "npm run build && ./deploy.sh prod" },这样，通过npm run deploy命令就可以实现直接打包并部署到测试环境了。如果你的公司目前还在用人工拷贝或 FTP 工具这种低效的部署方式，不妨试一下用上面的脚本来提效哦。PS：由于 rsync 命令只在 Linux 或 Mac 才有，所以只有开发环境是 Linux 或 Mac 的用户才可以运行哦，Windows 用户是没法跑这个命令的。编写网站的 conf上传文件到服务器后，就可以着手配置 nginx 了。一般 nginx 的配置都会放在/etc/nginx/conf.d目录下，我们在该目录新建一个test.conf作为该项目的配置：server { listen 80; server_name your-domain.com; # 域名 location / { root /srv/YourProject; # 网站源码根目录 index index.html; } location /api { proxy_pass http://localhost:8080; # 反向代理后端接口地址 } }一般来说，静态站点只需配置以上几个就可以了，server_name表示域名，需要先解析到服务器的公网 ip；root表示服务器中代码所在的位置，index指明了默认的处理文件是index.html；location /api是反向代理后端服务(这里假设了后端服务部署…

Centos8使用docker迁移typecho博客
type: item
date: 2019-12-25T16:08:00+00:00
description: 最近在学docker，先拿自己的博客来开下刀[手动狗头]。安装docker我是根据这个教程来安装的：Centos安装Docker。步骤如下：卸载旧版本sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-selinux \ docker-engine-selinux \ docker-engine安装依赖包sudo yum install -y yum-utils \ device-mapper-persistent-data \ lvm2添加yum源(建议使用国内镜像)sudo yum-config-manager \ --add-repo \ https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo更新软件源缓存并安装docker-cesudo yum makecache fast sudo yum install docker-cesudo yum install docker-ce这一步可能会报错：containerd.io (>= 1.2.2-3)，此时需要先安装新版本的containerd.ioyum install -y https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.6-3.3.el7.x86_64.rpm最新的containerd.io版本请在https://download.docker.com/linux/centos/7/x86_64/stable/Packages/找（国内直接安装containerd.io可能会非常慢导致安装失败，建议可以想办法将rpm包下载到本机再安装）。安装完containerd.io后再sudo yum install docker-ce即可。启动Docker CEsudo systemctl enable docker sudo systemctl start docker测试Docker是否安装正确docker run hello-world配置国内镜像加速vi /etc/docker/daemon.json（该文件不存在时请新建）{ "registry-mirrors": [ "https://dockerhub.azk8s.cn", "https://reg-mirror.qiniu.com" ] }保存后重启服务sudo systemctl daemon-reload sudo systemctl restart docker安装mariadbmkdir -p /data/mariadb docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=test --mount type=bind,source=/data/mariadb,target=/var/lib/mysql --restart=always --name mariadb mariadb--mount命令也可以用-v /data/mariadb:/var/lib/mysql --name mariadb mariadb代替，官方更推荐使用--mount,详情请看官网。参数说明：-d 代表 daemon，即后台运行-p是映射容器的3306端口到宿主机的3306端口，规则是：-p IP:host_port:container_port-e是设置mariadb的密码--mount是让容器的/var/lib/mysql映射到宿主机的/data/mariadb目录中--restart=always是为了在docker重启时，容器能够自动启动PS: 后面笔者会将所有容器依赖的一些数据都放在/data目录下，包括数据库、网站源码、nginx的conf等，目的是为了以后迁移方便，直接将/data拷贝到新服务器就可以。安装nginxmkdir -p /data/nginx/conf.d docker run -p 80:80 -p 443:443 --mount type=bind,source=/data/nginx/conf.d,target=/etc/nginx/conf.d --mount type=bind,source=/data/solution,target=/data/solution --restart=always -d --na…

axios如何利用promise无痛刷新token（二）
type: item
date: 2019-11-13T03:07:00+00:00
description: 前言前段时间写了篇文章《axios如何利用promise无痛刷新token》，陆陆续续收到一些反馈。发现不少同学会想要从在请求前拦截的思路入手，甚至收到了几个邮件来询问博主遇到的问题，所以索性再写一篇文章来说说另一个思路的实现和注意的地方。过程会稍微啰嗦，不想看实现过程的同学可以直接拉到最后面看最终代码。PS：在本文就略过一些前提条件了，请新同学阅读本文前先看一下前一篇文章《axios如何利用promise无痛刷新token》。前提条件前端登录后，后端返回token和token有效时间段tokenExprieIn，当token过期时间到了，前端需要主动用旧token去获取一个新的token，做到用户无感知地去刷新token。PS: tokenExprieIn是一个单位为秒的时间段，不建议使用绝对时间，绝对时间可能会由于本地和服务器时区不一样导致出现问题。实现思路方法一在请求发起前拦截每个请求，判断token的有效时间是否已经过期，若已过期，则将请求挂起，先刷新token后再继续请求。方法二不在请求前拦截，而是拦截返回后的数据。先发起请求，接口返回过期后，先刷新token，再进行一次重试。前文已经实现了方法二，本文会从头实现一下方法一。实现基本骨架在请求前进行拦截，我们主要会使用axios.interceptors.request.use()这个方法。照例先封装个request.js的基本骨架：import axios from 'axios' // 从localStorage中获取token，token存的是object信息，有tokenExpireTime和token两个字段 function getToken () { let tokenObj = {} try { tokenObj = storage.get('token') tokenObj = tokenObj ? JSON.parse(tokenObj) : {} } catch { console.error('get token from localStorage error') } return tokenObj } // 给实例添加一个setToken方法，用于登录后方便将最新token动态添加到header，同时将token保存在localStorage中 instance.setToken = (obj) => { instance.defaults.headers['X-Token'] = obj.token window.localStorage.setItem('token', JSON.stringify(obj)) // 注意这里需要变成字符串后才能放到localStorage中 } // 创建一个axios实例 const instance = axios.create({ baseURL: '/api', timeout: 300000, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }) // 请求发起前拦截 instance.interceptors.request.use((config) => { const tokenObj = getToken() // 为每个请求添加token请求头 config.headers['X-Token'] = tokenObj.token // **接下来主要拦截的实现就在这里** return config }, (error) => { // Do something with request error return Promise.reject(error) }) // 请求返回后拦截 instance.interceptors.response.use(response => { const { code } = response.data if (code === 1234) { // token过期了，直接跳转到登录页 window.location.href = '/' } return response }, error => { console.log('catch', error) return Promise.reject(error) }) export default instance与前文略微不同的是，由于方法二不需要用到过期时间，所以前文localStorage中只存了token一个字符串，而方法一这里需要用到过期时间了，所以得存多一个数据，因此localStorage中存的是Object类型的数据，从localStorage中取值出来需要…

axios如何利用promise无痛刷新token
type: item
date: 2019-08-28T03:56:00+00:00
description: 需求最近遇到个需求：前端登录后，后端返回token和token有效时间，当token过期时要求用旧token去获取新的token，前端需要做到无痛刷新token，即请求刷新token时要做到用户无感知。需求解析当用户发起一个请求时，判断token是否已过期，若已过期则先调refreshToken接口，拿到新的token后再继续执行之前的请求。这个问题的难点在于：当同时发起多个请求，而刷新token的接口还没返回，此时其他请求该如何处理？接下来会循序渐进地分享一下整个过程。实现思路由于后端返回了token的有效时间，可以有两种方法：方法一：在请求发起前拦截每个请求，判断token的有效时间是否已经过期，若已过期，则将请求挂起，先刷新token后再继续请求。方法二：不在请求前拦截，而是拦截返回后的数据。先发起请求，接口返回过期后，先刷新token，再进行一次重试。两种方法对比方法一优点： 在请求前拦截，能节省请求，省流量。缺点： 需要后端额外提供一个token过期时间的字段；使用了本地时间判断，若本地时间被篡改，特别是本地时间比服务器时间慢时，拦截会失败。PS：token有效时间建议是时间段，类似缓存的MaxAge，而不要是绝对时间。当服务器和本地时间不一致时，绝对时间会有问题。方法二优点：不需额外的token过期字段，不需判断时间。缺点： 会消耗多一次请求，耗流量。综上，方法一和二优缺点是互补的，方法一有校验失败的风险（本地时间被篡改时，当然一般没有用户闲的蛋疼去改本地时间的啦），方法二更简单粗暴，等知道服务器已经过期了再重试一次，只是会耗多一个请求。在这里博主选择了 方法二。实现这里会使用axios来实现，方法一是请求前拦截，所以会使用axios.interceptors.request.use()这个方法；而方法二是请求后拦截，所以会使用axios.interceptors.response.use()方法。封装axios基本骨架首先说明一下，项目中的token是存在localStorage中的。request.js基本骨架:import axios from 'axios' // 从localStorage中获取token function getLocalToken () { const token = window.localStorage.getItem('token') return token } // 给实例添加一个setToken方法，用于登录后将最新token动态添加到header，同时将token保存在localStorage中 instance.setToken = (token) => { instance.defaults.headers['X-Token'] = token window.localStorage.setItem('token', token) } // 创建一个axios实例 const instance = axios.create({ baseURL: '/api', timeout: 300000, headers: { 'Content-Type': 'application/json', 'X-Token': getLocalToken() // headers塞token } }) // 拦截返回的数据 instance.interceptors.response.use(response => { // 接下来会在这里进行token过期的逻辑处理 return response }, error => { return Promise.reject(error) }) export default instance这个是项目中一般的axios实例的封装，创建实例时，将本地已有的token放进header，然后export出去供调用。接下来就是如何拦截返回的数据啦。instance.interceptors.response.use拦截实现后端接口一般会有一个约定好的数据结构，如：{code: 1234, message: 'token过期', data: {}}如我这里，后端约定当code === 1234时表示token过期了，此时就要求刷新token。instance.interceptors.response.use(response => { const { code } = response.data if (code === 1234) { // 说明token过期了,刷新token return refreshToken().then(res => { // 刷新token成功，将最新的token更新到header中，同时保存在localStorage…