使用Laravel构建小程序Api服务器的准备工作

开发了好几个小程序,将我开发小程序的一些前期准备工作分享在这里。大家可以参考一下。如果有好的提议,可以分享在下面。

第一步:准备基础的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 为技术新手提供服务
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!