?? ?????? ??? ??? ??? ?????? ? ????? ?? ???? ???? ?? ??? ??? ???????. ??? ?? ????!
?? ???? ???? ?? ??
https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html
Request?? ??
Method | Description |
---|---|
RequestTask.abort() | ?? ??? ?????. |
RequestTask.onHeadersReceived(?? ??) | HTTP ?? ?? ???? ?????. ?? ?? ????? ?? ?????. |
RequestTask.offHeadersReceived(?? ??) | HTTP ?? ?? ??? ??? ?????. |
RequestTask.onChunkReceived(?? ??) | Transfer-Encoding Chunk ?? ???? ????. ??? ??? ???? ?????. |
RequestTask.offChunkReceived(?? ??) | Transfer-Encoding Chunk received ??? ??? ?????. |
wx.request(Object object) attribute
???? ? ????? ???? ??? ???? ????. ?? ??? ???? ??? ?????.
Property | Type | ??? | ?? | Description |
---|---|---|---|---|
url | string | ? | ??? ?? ????? ?? | |
data | string /object/ArrayBuffer | No | ??? ???? | |
header | Object | No | ??? ??? ??? ? ????. content-type 默認為 application/json
|
|
timeout | number | No | Timeout(???) | |
method | string | GET | No | HTTP ?? ?? |
?? | ?? |
|
No | ????? ?? ??? ?? ?? ?? |
fail | function | No | ????? ?? ?? ?? ?? | |
complete | function | ??? | ??? ?????? ????? ??? ??? ?? ??(?? ??/?? ??? ???? ???)! |
????? ?? ?? ???? ??????? ????? ? ?? ??? ????.
????? ?? ?????. ???? ?? ???? ????.
?? ??? ?? ?? ??? ?????. ?? ??, ??, ???? ? ?? ?? ??? ????.
????? ??? ??? ???? ???? errMsg ??? ?????.
?? ?? | errMsg ?? |
---|---|
success | {errMsg:"request:ok"...} |
fail | {er rM sg:"??:??". . .} ?? ?????? ?? ?? ??? ???? ??? ??? ????? ???? ???? ?? ?? ????. indexOf ??? ???? -1?? ?? ??? ?? ????. |
abort | {errMsg:"??:?? ??"...} |
?? ??
let reqTask = wx.request({ url: getApp().globalData.api, success(res) { if (res.errMsg === "request:ok") console.log("res", res); }, fail(err) { // if(err.errMsg.indexOf('request:fail')>-1) console.log('err', err); if (/^request:fail/i.test(err.errMsg)) console.log("err", err); }, complete(res) { console.log("resOrErr", res); }, }); const reqTaskOnHeadersReceived = (headers) => { reqTask.offHeadersReceived(reqTaskOnHeadersReceived); console.log("headers", headers); // 由于請求還未完全結束,所以我們沒辦法獲得請求的狀態(tài)碼,但是我們可以通過返回的requestBody的長度來進行判斷。 // 兩點說明:1. 兩個~~可以把字符串數(shù)字快速轉(zhuǎn)化為數(shù)字。 // 2. 為什么取小于19,是由于后臺返回沒有權限的requestBody的時候Content-length為“18”,正常情況下是大于19的。所以具體多少得看一下具體情況。 if (~~headers.header["Content-length"] < 19) reqTask.abort(); }; reqTask.onHeadersReceived(reqTaskOnHeadersReceived);
?? ???? ??? ?????
??? ??? ?????. ????? ?? ???? ?????(?:
button
上bindtap
的回調(diào)中)后才可調(diào)用,每次請求都會彈出授權窗口,用戶同意后返回userInfo
。該接口用于替換wx.getUserInfo
, ??? ??? ??? ?? ????? ?? ??? ?????.wx.checkSession(Object object)
??? ??? ?????? ?????. ??? ??? ?? wx.login ?????? ?? ?? ??? ?? ?? ?? ?? ????? ???? ???? ???? ??? ??? ???? ???? ????. ?? ??? ??? WeChat? ?? ???? ????? ???? ?????. ???? ?? ??? ??? ??? ???? ???? ?? wx.checkSession ?????? ???? ???.
??? ??? ??? ? ???? wx? ??? ? ????. .login? ???? ? ??? ??? ??? ????. ??? ???? ?? session_key? ?????? ????, ??? ???? Mini ???? ???? ?????. wx.login(Object object)
- ??? ??? ?? ??(??)? ?? ?? ?????? ?????. ?? ?? ????? ?? ???(openid), WeChat ?? ??? ??? ?? ???(unionid, ??) ?? ?? ????? ?? ?? ??? ??? ????? ???, ? ???? ?? ?(session_key) ?? ????. ??? ???
???? NodeJS, ? ????? KOA ?? ^2.13, ??? ????? @koa/??? ?? ^10.1.1, ????? ??, ?? ^2.88? ?????. 2, jsonwebtoken? ?? ??? ????? ???? ? ?????. ?? ^8.5.1// app.js
const Koa = require("koa");
const Router = require("@koa/router");
const WeixinAuth = require("./lib/koa2-weixin-auth");
const jsonwebtoken = require("jsonwebtoken");
const app = new Koa();
// 小程序機票信息
const miniProgramAppId = "*********";
const miniProgramAppSecret = "***********";
const weixinAuth = new WeixinAuth(miniProgramAppId, miniProgramAppSecret);
const JWT_SECRET = "JWTSECRET";
// 路由中間件需要安裝@koa/router
// 開啟一個帶群組的路由
const router = new Router({
prefix: "/user",
});
// 這是正規(guī)的登陸方法
// 添加一個參數(shù),sessionKeyIsValid,代表sessionKey是否還有效
router.post("/weixin-login", async (ctx) => {
let { code, userInfo, encryptedData, iv, sessionKeyIsValid } =
ctx.request.body;
// 解析openid
const token = await weixinAuth.getAccessToken(code);
userInfo.openid = token.data.openid;
// 這里可以自己進行處理,比方說記錄到數(shù)據(jù)庫,處理token等
let authorizationToken = jsonwebtoken.sign(
{ name: userInfo.nickName },
JWT_SECRET,
{ expiresIn: "1d" }
);
Object.assign(userInfo, { authorizationToken });
ctx.status = 200;
ctx.body = {
code: 200,
msg: "ok",
data: userInfo,
};
});
// lib/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是否有效,檢查規(guī)則為當前時間和過期時間進行對比
*
* Examples:
* ```
* token.isValid();
* ```
*/
AccessToken.prototype.isValid = function () {
return (
!!this.data.session_key &&
new Date().getTime() < this.data.create_at + this.data.expires_in * 1000
);
};
/**
* 根據(jù)appid和appsecret創(chuàng)建OAuth接口的構造函數(shù)
* 如需跨進程跨機器進行操作,access token需要進行全局維護
* 使用使用token的優(yōu)先級是:
*
* 1. 使用當前緩存的token對象
* 2. 調(diào)用開發(fā)傳入的獲取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 授權后要跳轉(zhuǎn)的地址
* @param {String} state 開發(fā)者可提供的數(shù)據(jù)
* @param {String} scope 作用范圍,值為snsapi_userinfo和snsapi_base,前者用于彈出,后者用于跳轉(zhuǎn)
*/
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);
};
/**
* 根據(jù)授權獲取到的code,換取access token和openid
* 獲取openid之后,可以調(diào)用`wechat.API`來獲取更多信息
* @param {String} code 授權獲取到的code
*/
Auth.prototype.getAccessToken = function (code) {
return new Promise((resolve, reject) => {
const url = "https://api.weixin.qq.com/sns/jscode2session";
//由于此框架版本很久沒有更新了,此處地址發(fā)生了變化,需要修改為以上地址,不然會出現(xiàn)
//41008錯誤。這也是沒有直接使用框架,引用本地使用的原因。
// 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));
}
});
});
};
/**
* 根據(jù)refresh token,刷新access token,調(diào)用getAccessToken后才有效
* @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));
}
});
});
};
/**
* 根據(jù)openid,獲取用戶信息。
* 當access token無效時,自動通過refresh token獲取新的access token。然后再獲取用戶信息
* @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));
}
});
});
};
/**
* 根據(jù)code,獲取用戶信息。
* @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;
?? ??? ??? ?? ??
<!--pages/index.wxml--> <view class="page-section"> <text class="page-section__title">微信登錄</text> <view class="btn-area"> <button bindtap="getUserProfile" type="primary">登錄</button> </view> </view>
// pages/index.js Page({ /** * 頁面的初始數(shù)據(jù) */ data: {}, // 正確的登錄方式 getUserProfile() { // 推薦使用wx.getUserProfile獲取用戶信息,開發(fā)者每次通過該接口獲取用戶個人信息均需用戶確認 // 開發(fā)者妥善保管用戶快速填寫的頭像昵稱,避免重復彈窗 wx.getUserProfile({ desc: "用于完善會員資料", // 聲明獲取用戶個人信息后的用途,后續(xù)會展示在彈窗中,請謹慎填寫 success: (res) => { let { userInfo, encryptedData, iv } = res; const requestLoginApi = (code) => { // 發(fā)起網(wǎng)絡請求 wx.request({ url: "http://localhost:3000/user/weixin-login", method: "POST", header: { "content-type": "application/json", }, data: { 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: "登錄成功了", }); }; //必須進行session是否過期檢查,不然會出現(xiàn)第一次點擊登錄,服務器報Illegal Buffer //的錯誤,但是第二次點擊登錄正常。 wx.checkSession({ success: (res) => { // session_key 未過期,并且在本生命周期一直有效 console.log("在登陸中"); let token = wx.getStorageSync("token"); if (token) onUserLogin(token); }, fail: (res) => { // session_key已經(jīng)失效,需要重新執(zhí)行登錄流程 wx.login({ success(res0) { if (res0.code) { requestLoginApi(res0.code); } else { console.log("登錄失敗!" + res0.errMsg); } }, }); }, }); }, }); }, });
??? ??? ?? ?? ???? ??? ? ???? ?????, ?? ???? ?? ???? ??? ???? ???(???? ?? ? ??? ?? ???). ):
Maintainability?? "?? ??"? ??? ??? ??? ????. ?? ??, ?? ?? ??, ? ?? ?? ? ?? "??? ?? ???? ??"? ?? ???? ?? ?? ???? ????? ??? ??? ???? ?? ??? ????? ??? ? ??? ?????. ??? ?? ??? ?? ????."?? ?? ??? ????? ???? ??? ??? ??? ??? ??? ????, ???? ? ??? ?? ???? ?????.
Readability
- ????? ?? ???? ?? ???(Martin Fowler)? ??? ?? ?????. "?? ???? ?? ? ? ????. ?? ?????? ??? ??? ? ?? ??? ?????." ???? ???? "?? ???? ???? ??? ? ?? ??? ??? ? ????. ?? ?????? ??? ??? ? ?? ??? ??? ? ????. "???? ?????? ??? ??? ????. ? ??? ??? ?????? ?? ?? ?? ?? ??? ?? ??? ??? ? ?? ??? ????. ??? ???? ??? ???? ? ? ????. ?? ??? ???? ???. ???? ??? ???? ??? ?? ??? ?????, ?? ??? ??? ???, ??? ????, ?? ??? ????, ??? ???? ???? ???. ?? ???? ?? ??? ?? ????? ??? ?????.
-
Extensibility(???)
Extensibility? ?? ??? ???? ? ?? ??? ?????. ???? ?? ?? ?? ??? ?? ??? ? ????. ?? ?? ?? ??? ?? ??? ?????. ??? ???? ?? ??? ?? ????? ????
.代碼的可復用性可以簡單地理解為,盡量減少重復代碼的編寫,復用已有的代碼。
那么接下來就來優(yōu)化一下代碼吧:
模塊化
可以把登錄的代碼模塊化,代碼如下:
// lib/login.js function loginWithCallback(cb) { // 推薦使用wx.getUserProfile獲取用戶信息,開發(fā)者每次通過該接口獲取用戶個人信息均需用戶確認 // 開發(fā)者妥善保管用戶快速填寫的頭像昵稱,避免重復彈窗 wx.getUserProfile({ desc: "用于完善會員資料", // 聲明獲取用戶個人信息后的用途,后續(xù)會展示在彈窗中,請謹慎填寫 success: (res) => { let { userInfo, encryptedData, iv } = res; const requestLoginApi = (code) => { // 發(fā)起網(wǎng)絡請求 wx.request({ url: "http://localhost:3000/user/weixin-login", method: "POST", header: { "content-type": "application/json", }, data: { 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: "登錄成功了", }); if (cb && typeof cb == "function") cb(token); }; wx.checkSession({ success: (res) => { // session_key 未過期,并且在本生命周期一直有效 console.log("在登陸中"); let token = wx.getStorageSync("token"); if (token) onUserLogin(token); }, fail: (res) => { // session_key已經(jīng)失效,需要重新執(zhí)行登錄流程 wx.login({ success(res0) { if (res0.code) { requestLoginApi(res0.code); } else { console.log("登錄失敗!" + res0.errMsg); } }, }); }, }); }, }); } export default loginWithCallback;
Promise化
回調(diào)地獄問題,不利于代碼的閱讀,所以接下來我們基于Promise進行代碼優(yōu)化。有了 Promise 對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調(diào)函數(shù)。此外,Promise 對象提供統(tǒng)一的接口,使得控制異步操作更加容易。
Promise的幾個方法簡介
??? ?? | Description |
---|---|
Promise.prototype.then | ???? ??? Promise ??? ????? ?? ???? ??? ? ????. ? ??? ?? ??? ??? ??? ?? ??? "?? ??"?? "?? ??"? ?? ?? ??? ? ????. |
Promise.prototype.catch | ? Promise.prototype.then(null, Rejection)? ????, ?? ?? ? ?? ??? ???? ? ?????. Promise ??? ??? "??" ??? ??? ??? ??? ?? ?????. ?, ??? ?? ?? catch ??? ?????. |
Promise.prototype.finally | ???? Promise Promise 。在promise結束時,無論結果是fulfilled或者是rejected,都會執(zhí)行指定的回調(diào)函數(shù)。這為在Promise 是否成功完成后都需要執(zhí)行的代碼提供了一種方式。 |
Promise.all | 這避免了同樣的語句需要在then() 和catch() 中各寫一次的情況。Promise.all 方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例。Promise.all 方法接受一個數(shù)組作為參數(shù),var p = Promise.all([p1,p2,p3]); p1、p2、p3 都是 Promise 對象的實例。(Promise.all 方法的參數(shù)不一定是數(shù)組,但是必須具有 iterator 接口,且返回的每個成員都是 Promise 實例。)p 的狀態(tài)由 p1、p2、p3 決定,分成兩種情況。 (1)只有p1、p2、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled,此時p1、p2、p3的返回值組成一個數(shù)組,傳遞給p的回調(diào)函數(shù)。 (2)只要p1、p2、p3之中有一個被rejected,p的狀態(tài)就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調(diào)函數(shù)。 |
Promise.race | Promise.race 方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。var p = Promise.race([p1,p2,p3]); 上面代碼中,只要p1、p2、p3之中有一個實例率先改變狀態(tài),p的狀態(tài)就跟著改變。那個率先改變的Promise實例的返回值,就傳遞給p的返回值。 |
Promise.any | 接收一個Promise可迭代對象,只要其中的一個 promise 成功,就返回那個已經(jīng)成功的 promise 。所有子實例都處于rejected狀態(tài),總的promise才處于rejected狀態(tài)。 |
Promise.allSettled | 返回一個在所有給定的promise都已經(jīng)fulfilled 或rejected 后的promise,并帶有一個對象數(shù)組,每個對象表示對應的promise結果。相比之下,Promise.all() 更適合彼此相互依賴或者在其中任何一個reject ? ?????. Promise? ??? ??? ????? ?????? ???? ??? ?? ??? ?????. ?? Promise ? ????? ?????? ??? ???? ????? ?? ??? ?? ??? ?????. |
then()
?? ? ??catch()
???? ??? ?? ? ? ???? ?? ??? ?? ? ????. Promise.all ???? ?? Promise ????? ? Promise ????? ???? ? ?????. Promise.all ???? ??? ????? ?????. var p = Promise.all([p1,p2,p3]);
p1, p2, p3? ?? Promise ??? ???????. (Promise.all ???? ?? ??? ??? ??? ??? ??? ??? ?????? ??? ?? ??? ? ??? Promise ???????.) p? ??? p1, p2, p3? ?? ???? ??? ?? ????. ? ?? ??. (1) p1, p2, p3? ??? ?? ????? p? ??? ?????. ?? p1, p2, p3? ?? ?? ??? ???? ?????. p? ?? ??. (2) p1, p2, p3 ? ??? ???? p? ??? ?????. ?? ? ?? ??? ????? ?? ?? p? ?? ??? ?????. ???????? Promise.race??????Promise.race ???? ?? Promise ????? ??? Promise ????? ?????. var p = Promise.race([p1,p2,p3]);
? ????? p1, p2, p3 ? ??? ????? ?? ??? ???? ? p? ??? ?????. ?? ?? ??????. ?? ??? Promise ????? ?? ?? p? ?? ??? ?????. ???????? Promise.any??????? Promise ?? ?? ??? ????. Promise ? ??? ???? ???? Promise? ?????. ?? ?? ????? ??? ????, ?? ????? ??? ?????. ???????? Promise.allSettled?????? ??? ?? ??? ??
?? ??
? ? ?? ??? ???? ??? ?????. ? ??? ?? ?? ??? ?????. ??? Promise.all()
? ?? ????? ? ? ??? ??
? ? ?? ???? ??? ? ?????. ????????小程序API接口Promise化并且把需要登錄的調(diào)用接口模塊化
1、安裝插件。請先查看npm支持文檔。
npm install --save miniprogram-api-promise
2、在微信開發(fā)者工具右方詳情中勾選使用npm模塊,并在菜單欄工具中點擊構建npm。
3、初始化代碼。
// app.js import {promisifyAll} from 'miniprogram-api-promise' import login from "../lib/login"; const wxp ={} promisifyAll(wx,wxp) // 需要token的請求統(tǒng)一處理登錄和設置header,并且處理錯誤信息 wxp.requestNeedLogin = async function (args) { let token = wx.getStorageSync("token"); if (!token) { token = await loginWithPromise(); } if (!args.header) args.header = {}; args.header["Authorization"] = `Bearer ${token}`; return wxp.request(args).catch(console.error); }; // app.js App({ wxp:wxp, });
4、改寫login.js代碼
// lib/login.js function login() { return new Promise((resolve, reject) => { // 推薦使用wx.getUserProfile獲取用戶信息,開發(fā)者每次通過該接口獲取用戶個人信息均需用戶確認 // 開發(fā)者妥善保管用戶快速填寫的頭像昵稱,避免重復彈窗 wx.getUserProfile({ desc: "用于完善會員資料", // 聲明獲取用戶個人信息后的用途,后續(xù)會展示在彈窗中,請謹慎填寫 success:async (res0) => { let { userInfo, encryptedData, iv } = res0; const app = getApp(); try { app.wxp.checkSession(); } catch (err) { reject(err); } let token = wx.getStorageSync("token"); if (!token) { let res1 = await app.wxp.login().catch(err => reject(err)); let code = res1.code; let res = await app.wxp.request({ url: "http://localhost:3000/user/weixin-login", method: "POST", header: { "content-type": "application/json", }, data: { code, userInfo, encryptedData, iv, } }).catch(err => reject(err)); token = res.data.data.authorizationToken; wx.setStorageSync("token", token); app.globalData.token = token; wx.showToast({ title: "登錄成功了", }); resolve(token); } }, }); }) } export default login;
5、調(diào)用代碼
<view class="container page-head"> <text class="page-section__title">需要登錄的請求調(diào)用</text> <view class="btn-area"> <button bindtap="request1" type="primary">請求1</button> <button bindtap="request2" type="primary">請求2</button> </view> </view>
// pages/index.js Page({ /** * 頁面的初始數(shù)據(jù) */ data: {}, request1() { getApp().wxp.requestNeedLogin({ url: "http://localhost:3000/user/home?name=andying", }).then(console.log) }, request2() { getApp().wxp.requestNeedLogin({ url: "http://localhost:3000/user/home?name=eva", }).then(console.log) }, });
【相關學習推薦:小程序開發(fā)教程】
? ??? ?? ?????? ??? ??? ???? ??? ?? ??? ??? ?? ?????. ??? ??? PHP ??? ????? ?? ?? ??? ?????!

? AI ??

Undress AI Tool
??? ???? ??

Undresser.AI Undress
???? ?? ??? ??? ?? AI ?? ?

AI Clothes Remover
???? ?? ???? ??? AI ?????.

Clothoff.io
AI ? ???

Video Face Swap
??? ??? AI ?? ?? ??? ???? ?? ???? ??? ?? ????!

?? ??

??? ??

???++7.3.1
???? ?? ?? ?? ???

SublimeText3 ??? ??
??? ??, ???? ?? ????.

???? 13.0.1 ???
??? PHP ?? ?? ??

???? CS6
??? ? ?? ??

SublimeText3 Mac ??
? ??? ?? ?? ?????(SublimeText3)

?? ???? ??? ???? Xiaohongshu? ?? ????? ??? ?? ???? ???? ??? ? ?? ?? ???? ?????. ?? ?? ??? ???? ?? ??? ????? ? ???? ?? ? ????. ? ???? Xiaohongshu? ?? ??? ????? ??? ???? ??? ??? ?? ? ?? ??? ?? ???? ???? ??? ?? ??? ?????. 1. Xiaohongshu? ?? ??? ??? ??????? 1. ????? ?? ??????. ??? Xiaohongshu? ????? ??? ???? ?? ??? ???? ? ????. ?? ??? ????? ????? ???? ??? ?? ???? ???. ?? ??? ??? ????. (1) Xiaohongshu ??? ?? ????? ?? "???" ??? ?????. (2) "???? ??"? ?????. (3) ???? ? ??? ??? ??? ?????.

??? ????? ?? ??? Steam ??? ????? ? ?? ??? ??? ??? ???? ?????? ?? ??, Steam? ??? ???? ?? ??? ? ?? ??? ??? ??? ????? ???? ???????. ?? ???? ???? ???. ?? ??? ???? ? wallpaperengine? ?? ??? ????? ?????? ?? ?? ?? 1. ??? Steam ??? ????? ???? ???? ???? ?? Steam Cloud ???? ???. 2. ??? ????? ?? ??? Steam ???? ????? Wallpaper Creative Workshop? ?? ?? ???? ?? ?? ?? ??? ?????. (??? ????? ?? ? ?? ?? ?? ??? ? ??? ??? ? ????.) 3. ??? ???? ?? ?????.

?? ?? ???? Kuaishou ??? ??? ????? ??? ?????. Kuaishou ??? ??? ??? ??? ??? ????. ??? ??? ???? ?? ??? ??? ? ????. 1??: ?? ??? ?????? Baidu? Kuaishou ?? ????? ?????. 2??: ?? ?? ???? ? ?? ??? ?????. 3??: Kuaishou ?? ???? ?? ???? ??? ? ??? ??? ?????. 4??: ??? ??? ?? ??? ???? ?????. 5??: ?? ??? ???? QR ??? ???? ??????. 6??: ?? ?? ????? Kuaishou? ?? ?? ??? ?? ???? ?????. 7??: QR ?? ??? ?????. 8??: ? QR ?? ????? ??? ??? ?? ?? ???? ??? ? ????? QR ??? ?????. 9??: ????? Kuaishou? ??? ??? ??????.

?? ??? ?? ?? ??? ??? ???, ?? ?? ??? ????. ??? ???? ?? ???? ?????? ?? ?? ?? ?? ?? ??? ??? ????. ???. [???? ?? ???·???? ?? ??? ?? ??] ???? ???? ????, ?? ?? ???? ??? ????, ?? ?? ??? ??? ???? ???? ?? ??? ??? ?????. ??. ???? ????? ???? ?? ??? ???? ?? ??, ? ?? ?? ??? ??? ???? ??? ?? ????. ??? ??? ??? ??? ??? ??? ????, ??? ??? ?? ?????. ???? ??? ??? ???? ?? ????? ????. [?? ???? ??? ???·?? ????] [?? ????·?? ??? ??] [?? ??] [?? ??] ???? ?? ??? ??·?? ????? 12? 28? ?? ? ?? ????? ???? ? ????.

Discuz ????? ??? ??? ?? ???? ???????. ???? ??? ???? ?? ? ??? ??? ?? ????? ??? ????? ???? ?? ? ??? ?? ????? Discuz? ???? ????. ?? ????. ??? ??? ???? ?? Discuz? ??? ? ????? ??? ??? ?? ? ?? ??? ??? ? ????. ??? Discuz ????? ??? ??? ?? ???? ???? ???? ?? ??? ???? ??? ??? ???? ??? ??? ??????.

Xiaohongshu? ?? ?? ???? ????? ?????, ??? ???? ??? ?? ???? ?? ????? ?? ??? ????. ?? ?? ????? ???? ?? ???, ??? ???? ???? ? ?? ?? ?? ???? ????. 1. Xiaohongshu? ??? ???? ?? ??? ??????? ????? ???? ?? ???? ????? ?? Xiaohongshu? ???? ? ????. ???? ??? ??? ????. 1. Xiaohongshu ? ?? Xiaohongshu ? ??? ???. 2. "???" ??? ???? "?? ? ???? ???"? ?????. 3. "????? ??????" 5. ????? ????? ?? ??? ????, ?? ??? ??? ? '??'? ?????. ?3? ??(?:

Baidu Netdisk? ??? ????? ???? ??? ? ?? ?? ??? ?? ?? ??? ??? ?? ????. ???? ????? ?????? ?? ?? ? ???? ??? ? ????. ???? Baidu Netdisk ? ??? ????? ??? ?????? ??? ??? ???????. Baidu Netdisk ? ?? ??? ??: https://pan.baidu.com (???? ?? ??) ????? ?? 1. ?? ?? ?? ??? ???? ???? ??? ???? ??? ???? ??? ? ????. 2. ????: ???? ?? ???? ????. ???? ??? ????? ???? ??? ??? ????? ?????. 3. ?? ??: ???? ?? ?? ??? ???? ??? ???? ???? ??? ?? ?? ??? ? ? ??? ?????.?

WeChat ?? ?????? ?? ??? ?? ?? WeChat ?? ?????? ?? ??? ??? ???? ?? ??? ??? ????? ?? ??? ??? ???? ? ?? ???? ????? ?????. ??? WeChat ????? ?? ??? ??? ???? ??? ??? ???? ?? ?? ??? ?????. ??, ?? ????? ??? ???? ??? ? ?? ?? ??? ???? ???. ??? ?? ??? ???? ?? ??? ?? ??? ?? ??? ???? ?? ????. <--index.wxml- ->&l
