• 【愚公系列】2022年09月 微信小程序-微信小程序实现网页一键登录功能



    前言

    如果微信小程序要获取微信登录的用户信息,需要拿到code去后台换取用户信息,具体步骤又如下:

    • 使用微信开放功能button按钮绑定点击事件为获取用户授权
    • 授权成功调用微信登录接口获取code
    • 用获取到的code去调用后台接口获取到用户的openid
    • code+openid去调用后台写的小程序自动登录接口获取到access_token
    • access_token拿到就可以去查询用户信息了

    一、微信小程序实现网页一键登录功能

    首先服务端先安装两个包

    npm i koa-weixin-auth --save
    npm i koa-body --save
    
    • 1
    • 2

    1.旧版登录方法

    小程序端

    <button bindgetuserinfo="login" open-type="getUserInfo" type="primary">登陆</button>
    
    • 1
    login(e) {
      console.log(e);
      let {userInfo,
        encryptedData,
        iv} = e.detail
      wx.login({
        success(res0) {
          if (res0.code) {
            //发起网络请求
            wx.request({
              url: 'http://localhost:3000/user/wexin-login',
              method: 'POST',
              header: {
                'content-type': 'application/json'
              },
              data: {
                code: res0.code,
                userInfo,
                encryptedData,
                iv
              },
              success(res) {
                console.log('请求成功', res.data)
                getApp().globalData.token = res.data.data.authorizationToken
                console.log('authorization', getApp().globalData.token)
              },
              fail(err) {
                console.log('请求异常', err)
              }
            })
          } else {
            console.log('登录失败!' + res.errMsg)
          }
        }
      })
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    服务端

    const WeixinAuth = require("../lib/koa2-weixin-auth")
    const WXBizDataCrypt = require('../lib/WXBizDataCrypt')
    
    // 小程序的机要信息
    const miniProgramAppId = '自己的appId'
    const miniProgramAppSecret = '自己的appSecret'
    const weixinAuth = new WeixinAuth(config.miniProgram.appId, config.miniProgram.appSecret);
    
    // 这是第一次小程序登陆法
    router.post("/wexin-login", async (ctx) => {
      let { code } = ctx.request.body
    
      const token = await weixinAuth.getAccessToken(code);
      // const accessToken = token.data.access_token;
      const openid = token.data.openid;
    
      // const userinfo = await weixinAuth.getUser(openid)
      // 这个地方有一个错误,invalid credential, access_token is invalid or not latest
      // 拉取不到userInfo
    
      ctx.status = 200
      ctx.body = {
        code: 200,
        msg: 'ok',
        data: openid
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2.新版登录方法

    小程序端

    <button bindgetuserinfo="login" open-type="getUserInfo" type="primary">登陆</button>
    
    • 1
    login(e) {
      console.log(e);
      let {
        userInfo,
        encryptedData,
        iv
      } = e.detail
      console.log('userInfo', userInfo);
      const requestLoginApi = (code)=>{
        //发起网络请求
        wx.request({
          url: 'http://localhost:3000/user/wexin-login2',
          method: 'POST',
          header: {
            'content-type': 'application/json'
          },
          data: {
            code: code,
            userInfo,
            encryptedData,
            iv
          },
          success(res) {
            console.log('请求成功', res.data)
            let token = res.data.data.authorizationToken
            wx.setStorageSync('token', token)
            onUserLogin(token)
            console.log('authorization', token)
          },
          fail(err) {
            console.log('请求异常', err)
          }
        })
      }
      const onUserLogin = (token)=>{
        getApp().globalData.token = token
        wx.showToast({
          title: '登陆成功了',
        })
      }
      wx.checkSession({
        success () {
          //session_key 未过期,并且在本生命周期一直有效
          console.log('在登陆中');
          let token = wx.getStorageSync('token')
          if (token) onUserLogin(token)
        },
        fail () {
          // session_key 已经失效,需要重新执行登录流程
          wx.login({
            success(res0) {
              if (res0.code) {
                requestLoginApi(res0.code)
              } else {
                console.log('登录失败!' + res.errMsg)
              }
            }
          })
        }
      })
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    const WeixinAuth = require("../lib/koa2-weixin-auth")
    const WXBizDataCrypt = require('../lib/WXBizDataCrypt')
    
    // 小程序的机要信息
    const miniProgramAppId = '自己的appId'
    const miniProgramAppSecret = '自己的appSecret'
    router.post("/wexin-login2", async (ctx) => {
      console.log('request.body', ctx.request.body);
      let { code,
        userInfo,
        encryptedData,
        iv,
        sessionKeyIsValid } = ctx.request.body
    
      console.log("sessionKeyIsValid", sessionKeyIsValid);
    
      let sessionKey
      // 如果客户端有token,则传来,解析
      if (sessionKeyIsValid) {
        let token = ctx.request.header.authorization;
        token = token.split(' ')[1]
        // token有可能是空的
        if (token) {
          let payload = await util.promisify(jsonwebtoken.verify)(token, config.jwtSecret).catch(err => {
            console.log('err', err);
          })
          console.log('payload', payload);
          if (payload) sessionKey = payload.sessionKey
        }
      }
      // 除了尝试从token中获取sessionKey,还可以从数据库中或服务器redis缓存中获取
      // 如果在db或redis中存储,可以与cookie结合起来使用,
      // 目前没有这样做,sessionKey仍然存在丢失的时候,又缺少一个wx.clearSession方法
      // 
      console.log("ctx.session.sessionKeyRecordId", ctx.session.sessionKeyRecordId);
      if (sessionKeyIsValid && !sessionKey && ctx.session.sessionKeyRecordId) {
        let sessionKeyRecordId = ctx.session.sessionKeyRecordId
        console.log("sessionKeyRecordId", sessionKeyRecordId);
        // 如果还不有找到历史上有效的sessionKey,从db中取一下
        let sesskonKeyRecordOld = await SessionKey.findOne({
          where: {
            id: ctx.session.sessionKeyRecordId
          }
        })
        if (sesskonKeyRecordOld) sessionKey = sesskonKeyRecordOld.sessionKey
        console.log("从db中查找sessionKey3", sessionKey);
      }
      // 如果从token中没有取到,则从服务器上取一次
      if (!sessionKey) {
        const token = await weixinAuth.getAccessToken(code)
        // 目前微信的 session_key, 有效期3天
        sessionKey = token.data.session_key;
        console.log('sessionKey2', sessionKey);
      }
    
      let decryptedUserInfo
      var pc = new WXBizDataCrypt(config.miniProgram.appId, sessionKey)
      // 有可能因为sessionKey不与code匹配,而出错
      // 通过错误,通知前端再重新拉取code
      decryptedUserInfo = pc.decryptData(encryptedData, iv)
      console.log('解密后 decryptedUserInfo.openId: ', decryptedUserInfo.openId)
    
      let user = await User.findOne({ where: { openId: decryptedUserInfo.openId } })
      if (!user) {//如果用户没有查到,则创建
        let createRes = await User.create(decryptedUserInfo)
        console.log("createRes", createRes);
        if (createRes) user = createRes.dataValues
      }
      let sessionKeyRecord = await SessionKey.findOne({ where: { uid: user.id } })
      if (sessionKeyRecord) {
        await sessionKeyRecord.update({
          sessionKey: sessionKey
        })
      } else {
        let sessionKeyRecordCreateRes = await SessionKey.create({
          uid: user.id,
          sessionKey: sessionKey
        })
        sessionKeyRecord = sessionKeyRecordCreateRes.dataValues
        console.log("created record", sessionKeyRecord);
      }
      // ctx.cookies.set("sessionKeyRecordId", sessionKeyRecord.id)
      ctx.session.sessionKeyRecordId = sessionKeyRecord.id
      console.log("sessionKeyRecordId", sessionKeyRecord.id);
    
      // 添加上openId与sessionKey
      let authorizationToken = jsonwebtoken.sign({
        uid: user.id,
        nickName: decryptedUserInfo.nickName,
        avatarUrl: decryptedUserInfo.avatarUrl,
        openId: decryptedUserInfo.openId,
        sessionKey: sessionKey
      },
        config.jwtSecret,
        { expiresIn: '3d' }//修改为3天,这是sessionKey的有效时间
      )
      Object.assign(decryptedUserInfo, { authorizationToken })
    
      ctx.status = 200
      ctx.body = {
        code: 200,
        msg: 'ok',
        data: decryptedUserInfo
      }
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106

    二、相关第三方包源码

    koa2-weixin-auth.js

    const querystring = require("querystring");
    const request = require("request");
    
    const AccessToken = function(data){
    	if(!(this instanceof AccessToken)){
    		return new AccessToken(data);
    	}
    	this.data = data;
    }
    
    /*!
     * 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比
     *
     * Examples:
     * ```
     * token.isValid();
     * ```
     */
    AccessToken.prototype.isValid = function() {
    	return !!this.data.session_key && (new Date().getTime()) < (this.data.create_at + this.data.expires_in * 1000);
    }
    
    /**
     * 根据appid和appsecret创建OAuth接口的构造函数
     * 如需跨进程跨机器进行操作,access token需要进行全局维护
     * 使用使用token的优先级是:
     *
     * 1. 使用当前缓存的token对象
     * 2. 调用开发传入的获取token的异步方法,获得token之后使用(并缓存它)。
    
     * Examples:
     * ```
     * var OAuth = require('oauth');
     * var api = new OAuth('appid', 'secret');
     * ```
     * @param {String} appid 在公众平台上申请得到的appid
     * @param {String} appsecret 在公众平台上申请得到的app secret
     */
    const Auth =  function (appid, appsecret) {
    	this.appid = appid;
      	this.appsecret = appsecret;
      	this.store = {};
      	
      	this.getToken = function (openid) {
        	return this.store[openid];
      	};
    
      	this.saveToken = function (openid, token) {
    	    this.store[openid] = token;
    	};
    }
    
    /**
     * 获取授权页面的URL地址
     * @param {String} redirect 授权后要跳转的地址
     * @param {String} state 开发者可提供的数据
     * @param {String} scope 作用范围,值为snsapi_userinfo和snsapi_base,前者用于弹出,后者用于跳转
     */
    Auth.prototype.getAuthorizeURL = function(redirect_uri, scope, state) {
    	return new Promise((resolve, reject) => {
    		const url = "https://open.weixin.qq.com/connect/oauth2/authorize";
    		let info = {
    			appid: this.appid,
    			redirect_uri: redirect_uri,
    			scope: scope || 'snsapi_base',
    			state: state || '',
    			response_type: 'code'
    		}
    		resolve(url + '?' + querystring.stringify(info) + '#wechat_redirect')
    	})
    }
    
    /*!
     * 处理token,更新过期时间
     */
    Auth.prototype.processToken = function(data){
    	data.create_at = new Date().getTime();
    	// 存储token
      	this.saveToken(data.openid, data);
      	return AccessToken(data);
    }
    
    /**
     * 根据授权获取到的code,换取access token和openid
     * 获取openid之后,可以调用`wechat.API`来获取更多信息
     * Examples:
     * ```
     * api.getAccessToken(code);
     * ```
     * Exception:
     *
     * - `err`, 获取access token出现异常时的异常对象
     *
     * 返回值:
     * ```
     * {
     *  data: {
     *    "access_token": "ACCESS_TOKEN",
     *    "expires_in": 7200,
     *    "refresh_token": "REFRESH_TOKEN",
     *    "openid": "OPENID",
     *    "scope": "SCOPE"
     *  }
     * }
     * ```
     * @param {String} code 授权获取到的code
     */
    Auth.prototype.getAccessToken = function(code){
    	return new Promise((resolve, reject) => {
    		const url = "https://api.weixin.qq.com/sns/jscode2session";
    		// const url = "https://api.weixin.qq.com/sns/oauth2/access_token";
    		const info = {
    			appid: this.appid,
    			secret: this.appsecret,
    			js_code: code,
    			grant_type: 'authorization_code'
    		}
    		request.post(url,{form:info},(err, res, body) => {
    			if(err){
    				reject(err)
    			}else{
    				const data = JSON.parse(body);
    				resolve(this.processToken(data))
    			}
    		})
    	})
    }
    
    /**
     * 根据refresh token,刷新access token,调用getAccessToken后才有效
     * Examples:
     * ```
     * api.refreshAccessToken(refreshToken);
     * ```
     * Exception:
     *
     * - `err`, 刷新access token出现异常时的异常对象
     *
     * Return:
     * ```
     * {
     *  data: {
     *    "access_token": "ACCESS_TOKEN",
     *    "expires_in": 7200,
     *    "refresh_token": "REFRESH_TOKEN",
     *    "openid": "OPENID",
     *    "scope": "SCOPE"
     *  }
     * }
     * ```
     * @param {String} refreshToken refreshToken
     */
    Auth.prototype.refreshAccessToken = function(refreshToken){
    	return new Promise((resolve, reject) => {
    		const url = 'https://api.weixin.qq.com/sns/oauth2/refresh_token';
    		var info = {
    		    appid: this.appid,
    		    grant_type: 'refresh_token',
    		    refresh_token: refreshToken
    		};
    		request.post(url,{form:info},(err, res, body) => {
    			if(err){
    				reject(err)
    			}else{
    				const data = JSON.parse(body);
    				resolve(this.processToken(data))
    			}
    		})
    	})
    }
    
    /**
     * 根据openid,获取用户信息。
     * 当access token无效时,自动通过refresh token获取新的access token。然后再获取用户信息
     * Examples:
     * ```
     * api.getUser(options);
     * ```
     *
     * Options:
     * ```
     * openId
     * // 或
     * {
     *  "openId": "the open Id", // 必须
     *  "lang": "the lang code" // zh_CN 简体,zh_TW 繁体,en 英语
     * }
     * ```
     * Callback:
     *
     * - `err`, 获取用户信息出现异常时的异常对象
     *
     * Result:
     * ```
     * {
     *  "openid": "OPENID",
     *  "nickname": "NICKNAME",
     *  "sex": "1",
     *  "province": "PROVINCE"
     *  "city": "CITY",
     *  "country": "COUNTRY",
     *  "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
     *  "privilege": [
     *    "PRIVILEGE1"
     *    "PRIVILEGE2"
     *  ]
     * }
     * ```
     * @param {Object|String} options 传入openid或者参见Options
     */
    Auth.prototype.getUser = async function(openid){
    	const data = this.getToken(openid);
    	console.log("getUser",data);
    	if(!data){
    		var error = new Error('No token for ' + options.openid + ', please authorize first.');
    		error.name = 'NoOAuthTokenError';
    		throw error;
    	}
    	const token = AccessToken(data);
    	var accessToken;
    	if(token.isValid()){
    		accessToken = token.data.session_key;
    	}else{
    		var newToken = await this.refreshAccessToken(token.data.refresh_token);
    		accessToken = newToken.data.session_key
    	}
    	console.log('accessToken',accessToken)
    	return await this._getUser(openid,accessToken);
    }
    
    Auth.prototype._getUser = function(openid, accessToken,lang){
    	return new Promise((resolve, reject) => {
    		const url = "https://api.weixin.qq.com/sns/userinfo";
    		const info = {
    			access_token:accessToken,
    			openid:openid,
    			lang:lang||'zh_CN'
    		}
    		request.post(url,{form:info},(err, res, body) => {
    			if(err){
    				reject(err)
    			}else{
    				resolve(JSON.parse(body));
    			}
    		})
    	})
    }
    
    /**
     * 根据code,获取用户信息。
     * Examples:
     * ```
     * var user = yield api.getUserByCode(code);
     * ```
     * Exception:
     *
     * - `err`, 获取用户信息出现异常时的异常对象
     *
     * Result:
     * ```
     * {
     *  "openid": "OPENID",
     *  "nickname": "NICKNAME",
     *  "sex": "1",
     *  "province": "PROVINCE"
     *  "city": "CITY",
     *  "country": "COUNTRY",
     *  "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
     *  "privilege": [
     *    "PRIVILEGE1"
     *    "PRIVILEGE2"
     *  ]
     * }
     * ```
     * @param {String} code 授权获取到的code
     */
    Auth.prototype.getUserByCode = async function(code){
    	const token = await this.getAccessToken(code);
    	return await this.getUser(token.data.openid);
    }
    
    module.exports = Auth;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282

    WXBizDataCrypt .js

    var crypto = require('crypto')
    
    function WXBizDataCrypt(appId, sessionKey) {
      this.appId = appId
      this.sessionKey = sessionKey
    }
    
    WXBizDataCrypt.prototype.decryptData = function (encryptedData, iv) {
      // base64 decode
      var sessionKey = new Buffer.from(this.sessionKey, 'base64')
      encryptedData = new Buffer.from(encryptedData, 'base64')
      iv = new Buffer.from(iv, 'base64')
    
      try {
         // 解密
        var decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv)
        // 设置自动 padding 为 true,删除填充补位
        decipher.setAutoPadding(true)
        // 问题是cipher.update(data, 'binary')输出一个缓冲区,该缓冲区自动字符串化为十六进制编码的字符串
        var decoded = decipher.update(encryptedData,'binary',"utf8")
        // 这里有一个错误发生:
        // error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
        // 本质是由于sessionKey与code不匹配造成的
        decoded += decipher.final('utf8')
        decoded = JSON.parse(decoded)
    
      } catch (err) {
        console.log('err',err);
        throw new Error('Illegal Buffer')
      }
    
      if (decoded.watermark.appid !== this.appId) {
        throw new Error('Illegal Buffer')
      }
    
      return decoded
    }
    
    module.exports = WXBizDataCrypt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
  • 相关阅读:
    JVM内存模型:类加载过程
    皕杰报表使用字体和部署后添加字体
    LabVIEW调试技巧
    看完 2022 雷军年度演讲,我总结了我的故事
    MapReduce(三)
    【Rust】快速教程——冻结&表达式
    Hadoop生态之Yarn
    vite + react + ts 配置路径别名alias
    2023年了,为何 Selenium 依然这么火?
    node的http模块
  • 原文地址:https://blog.csdn.net/aa2528877987/article/details/126790667