使用Laravel構建小程式Api伺服器的準備工作

shebaoting發表於2022-05-11

開發了好幾個小程式,將我開發小程式的一些前期準備工作分享在這裡。大家可以參考一下。如果有好的提議,可以分享在下面。

第一步:準備基礎的api構建工作

具體見:wyz.xyz/d/262-laravel-api

第二步:安裝easywechat

建議安裝overtrue/laravel-wechat,和easywechat是一個作者,laravel-wechat依賴了easywechat。所以直接安裝即可:

composer require "overtrue/laravel-wechat"
  1. 建立配置檔案:
php artisan vendor:publish --provider="Overtrue\LaravelWeChat\ServiceProvider"
  1. 可選,新增別名
'aliases' => [
    // ...
    'EasyWeChat' => Overtrue\LaravelWeChat\EasyWeChat::class,
],
  1. 修改相關配置
    開啟 config/easywechat.php 配置好相應的資訊即可

使用者在小程式進行註冊的邏輯

  1. 建立註冊認證的控制器
php artisan make:controller Api/Weapp/AuthorizationsController

程式碼如下:

<?php

namespace App\Http\Controllers\Api\Weapp;

use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Auth\AuthenticationException;
use App\Http\Requests\Api\Weapp\AuthorizationRequest;
use Overtrue\LaravelWeChat\ServiceProvider;
use Overtrue\LaravelWeChat\EasyWeChat;

class AuthorizationsController extends Controller
{
    /**
     * @param AuthorizationRequest $request
     * @return array
     */
    public function store(AuthorizationRequest $request)
    {
        (new ServiceProvider(app()))->register();
        $code = $request->code;
        $miniProgram = EasyWeChat::miniApp();
        $utils = $miniProgram->getUtils();
        $data = $utils->codeToSession($code);

        // 找到 openid 對應的使用者
        $user = User::where('weapp_openid', $data['openid'])->first();

        $attributes['weixin_session_key'] = $data['session_key'];

        // 未找到對應使用者則註冊新使用者
        if (!$user) {
            $attributes['weapp_openid'] = $data['openid'];
            $user = User::create($attributes);
        } else {
            // 更新使用者資料
            $user->update($attributes);
        }


        // 為對應使用者建立 token
        $user->tokens()->delete();
        $token = $user->createToken($request->device_name);

        return ['token' => $token->plainTextToken];
    }

    /**
     * @param User $user
     * @param $name
     * @return array
     */
    public function responseToken(User $user, $name = '')
    {
        // 為對應使用者建立 token
        // $user->tokens()->delete();
        $token = $user->createToken($name);

        return [
            'token' => $token->plainTextToken,
            'expire_in' => config('sanctum.expiration')
        ];
    }
}

需要注意:這裡的控制器繼承的Controller是基於這個帖子中寫的自己建立的控制器基類。驗證類同樣也是。如果你之前的api準備工作沒有按照我之前的教程進行操作。那麼這裡自行修改下相應的繼承。類方法沒區別。

增加小程式註冊路由

// 小程式登入
Route::post('authorizations', 'AuthorizationsController@store')->name('authorizations.store');

將這個路由放在登入路由組裡即可。路由分組見 wyz.xyz/d/344-laravelapi

原生小程式的請求封裝

我的小程式全部是基於原生寫法。其中因為有大量的請求需要使用。所以將所有的請求封裝成了一個請求檔案。這裡將這個請求檔案分享出來。

在小程式建立以下檔案
utils/request.js

程式碼如下:

/************ API 定義 ************/

/**
 * api入口定義
 */
const host = 'http://supply.test',
    api_entry = host + '/api/weapp'

/**
 * 登入
 * @param {*} data
 * @returns
 */
async function login() {
    // 登入引數
    async function getLoginParams() {
        const wxLogin = () => new Promise((res, rej) => wx.login({ success: r => res(r), fail: e => rej(e) }))
        try {
            const { code } = await wxLogin(),
                { model: device_name } = wx.getSystemInfoSync()
            return { code, device_name }
        } catch (e) {
            wx.showModal({ title: '登入失敗,請稍後再試' })
            DEBUG && console.log('[request] getLoginParams, wxLogin fail:', e)
            return;
        }
    }
    const login_params = await getLoginParams()

    // 登入介面
    const { token, expire_in = 604800 } = await repository('/authorizations', post(login_params))
    // 登入成功並設定token快取
    setAccessToken(token, expire_in)

    return token
}

/**
 * 更新token
 * @param {*} data
 * @returns
 */
async function refreshToken() {
    const { token, expire_in = 604800 } = await repository('/authorizations/current', post('', withToken))
    // 登入成功並設定token快取
    setAccessToken(token, expire_in)
    return token
}



// 這裡是方法列表,你可以在下面增加你的方法,這裡只是做個示範
async function getCustom() {
    return await repository('/settings/custom', get(), { fresher: false, useCache: true })
}




// 這裡匯出上面的方法列表
export {
    host,
    getAccessToken,
    getCustom,
    login
}

/************ API 定義結束 ************/

/************ 可修改部分 ************/

const DEBUG = false

// 快取 key 定義
const KEY_ACCESS_TOKEN = 'access_token',
    KEY_TOKEN_EXPIRE = 'access_token_expired_at'
/**
 * 快取提供者
 * 可以使用其它快取介面,需要實現get、set方法
 */
const cacheProvider = {
    get: async (key) => {
        return await wx.getStorage({ key: 'repository/' + key }).then(res => res.data).catch(e => null)
    },
    set: async (key, data) => {
        await wx.setStorage({ key: 'repository/' + key, data })
    }
}

// repository 配置
const repositoryConfig = {
    // 快取提供者
    cacheProvider,
    // 需要更新資料
    fresher: true,
    // 使用系統快取
    useCache: false,
    // 驗證結果
    validate: true,
    // 只返回結果
    onlyFetchedData: true,
    // 重新整理快取
    refreshCache: false,
}

/**
 * 定義獲取器
 */
function fetcher(method, data, before) {
    method = method.toUpperCase()
    if (!['GET', 'POST', 'PUT', 'DELETE'].includes(method))
        throw new Error('[request] not allow method: ' + method)

    return async (url) => {
        let option = {
            url, data, method, header: {
                'X-Requested-With': 'XMLHttpRequest',
                'Accept': 'Application/json',
            }
        }
        if (!Array.isArray(before)) before = [before]
        for (const i in before) {
            if (typeof before[i] === 'function')
                option = await before[i](option)
        }

        return wxRequest(option).then(responseHandler)
    }
}
const get = (d, be) => fetcher('get', d || '', [urlCon, be])
const post = (d, be) => fetcher('post', d || '', [urlCon, be])
const put = (d, be) => fetcher('put', d || '', [urlCon, be])
const del = (d, be) => fetcher('delete', d || '', [urlCon, be])

const urlCon = (option) => ({ ...option, url: api_entry + option.url })
const withToken = async (option) => ({ ...option, header: { 'Authorization': 'Bearer ' + await getAccessToken() } })

/**
 * 響應錯誤處理
 * 返回物件 { error }
 * @param {Object} err
 */
function errorHandler(err) {
    if (err.response.statusCode == 422) {
        wx.showToast({ title: '提交內容錯誤', icon: 'error' })
        return { error: err }
    } else {
        wx.showToast({ title: err.message, icon: 'error' })

        return { error: err }
    }
}

/**
 * 
 * @param {data, error, response} params
 * @returns 
 */
function checkError(result) {
    let { error, response } = result
    return new Promise(resolve => error ? errorHandler({ ...error, response }) : resolve(result))
}

/**
 * 響應處理方法
 * 根據HTTP響應狀態碼處理響應內容,當錯誤時 error 不為空,即`!!error === true`
 * 返回物件 { data, error, response }
 * @param {*} response
 * @returns
 */
function responseHandler(response) {
    // DEBUG && console.log('[request] responseHandler; response:', response)
    if (200 <= response.statusCode && response.statusCode < 300) {
        return { data: response.data, error: null, response }
    }
    if (400 <= response.statusCode && response.statusCode < 500) {
        DEBUG && console.log('[request] responseHandler, request error; response:', response)

        // 當未提供正確token時,響應碼為401
        if (response.statusCode === 401) {
            DEBUG && console.log('[request] responsehandler, token 無效或過期')
        }

        return { data: null, error: response.data, response }
    }
    if (500 <= response.statusCode && response.statusCode < 600) {
        DEBUG && console.log('[request] responseHandler, server error; response:', response)
        return { data: null, error: response.data, response }
    }

}

/************ 可修改部分結束 ************/

/************ 約定部分 ************/

/**
 * 資料倉儲
 * 返回內容為fetcher的結果
 * @param {*} key
 * @param {*} fetcher
 * @param {*} _option
 * @returns
 */
async function repository(key, fetcher, _option) {
    _option = repositoryConfigure(_option || {})
    const fresher = _option.fresher === true,
        useCache = _option.useCache === true,
        validate = _option.validate === true,
        cacheProvider = useCache && _option.cacheProvider ? _option.cacheProvider : defaultRepositories,
        onlyFetchedData = _option.onlyFetchedData === true,
        refreshCache = _option.refreshCache === true

    // key = stringifyKey(key)
    if (typeof key === 'function') key = key()
    if (Array.isArray(key)) key = JSON.stringify(key)

    let result

    if (!fresher) {
        result = await cacheProvider.get(key)
    }

    // 獲取資料
    if (!result || refreshCache) {
        result = await fetcher(key).then(r => validate ? checkError(r) : r)
        DEBUG && console.log('[request] repository, fetched: ', key, result)
        await cacheProvider.set(key, result)
    } else {
        DEBUG && console.log('[request] repository, get from cache:', key, result)
    }

    function mutate(data) {
        DEBUG && console.log('[request] repository, mutate', data)
        return repository(key, async () => ({ data }), { ..._option, refreshCache: true, onlyFetchedData: true })
    }

    function refresh() {
        DEBUG && console.log('[request] repository, refresh')
        return repository(key, fetcher, { ..._option, refreshCache: true })
    }

    return onlyFetchedData ? result.data : { ...result, mutate, refresh };
}

const repositoryConfigure = (config) => ({ ...repositoryConfig, ...config })
const defaultRepositories = new Map()

/**
 * 微信請求promise封裝
 * @param {Object} options
 * @returns {Object}
 */
async function wxRequest({ url, data, method, header }) {
    return await new Promise((resolve, reject) =>
        wx.request({ url, data, method, header, success: res => resolve(res), fail: err => reject(err) })
    )
}

/**
 * 獲取token
 * @param {Boolean} fresher
 */
async function getAccessToken(retry = 1) {
    // 獲取快取 token
    let token = (await cacheProvider.get(KEY_ACCESS_TOKEN)),
        expire = await cacheProvider.get(KEY_TOKEN_EXPIRE)

    DEBUG && console.log('[request] getAccessToken, after cacheProvider; token, expire: ', token, expire)

    if (!token) {
        DEBUG && console.log('[request] getAccessToken, login')
        // 登入獲取token
        await login()
    }
    // 檢查過期時間
    else if (expire <= timestramp()) {
        DEBUG && console.log('[request] getAccessToken, refresh')
        // 重新整理token
        await refreshToken()
    }
    else {
        return token
    }

    return retry > 0 ? getAccessToken(0) : 'no_token'
}

/**
 * 儲存 token
 * @param {String} token
 * @param {Number} expire_in
 */
function setAccessToken(token, expire_in) {
    cacheProvider.set(KEY_ACCESS_TOKEN, token)
    cacheProvider.set(KEY_TOKEN_EXPIRE, timestramp() + expire_in)
}

/**
 * 獲取時間戳
 * 單位:秒
 * @returns
 */
function timestramp() {
    return parseInt((new Date().getTime() / 1000).toFixed(0))
}

/************ 約定部分結束 ************/

使用的時候只需引入在當前檔案export的的方法即可。

文件見:doc.wyz.xyz/pages/39ef55/

小程式開發交流QQ群:156516399

本作品採用《CC 協議》,轉載必須註明作者和本文連結
烏鴉嘴新手社群 wyz.xyz 為技術新手提供服務

相關文章