使用腾讯云函数服务运行 laravel 9 (黄了各位😅, 不免费了)

2022年4月23日18:42:56更新
刚才收到腾讯云 SCF 的短信提醒, 从 2022 年 5 月 23 日开始 SCF 不再提供免费服务了, 最低的套餐为 9.9元 每月, 这个费用已经超过了购买 VPS 的费用, 没想到文章才发三天就不免费了, 下面的文章大家可以忽略了, 附通告截图
通告截图
计费截图


摸鱼过程中偶然发现了腾讯云出了 SCF 函数服务(可能早就有了,但我不知道:relaxed:),经过一些研究探索,成功运行了 laravel 框架,搭配 Api网关服务TDSQL-C数据库,几乎可实现免费搭建小型网站

计费详情

1.创建项目代码

使用 Composer 安装

composer create-project laravel/laravel

因云函数服务不支持在项目路径写入文件,故将各处写入文件定位至 /tmp,编辑 .env 文件,在底部新增

# 设置模板缓存路径
VIEW_COMPILED_PATH=/tmp
# 设置应用缓存路径
APP_STORAGE=/tmp
# 设置日志输出至 stderr
LOG_CHANNEL=stderr
# 设置 session 以内存形式储存 或自行修改使用 mysql 储存
SESSION_DRIVER=array

在项目根目录下创建处理文件 handler.php,内容如下

<?php

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\HeaderBag;

define('LARAVEL_START', microtime(true));
define('TEXT_REG', '#\.html.*|\.js.*|\.css.*|\.html.*#');
define('BINARY_REG', '#\.ttf.*|\.woff.*|\.woff2.*|\.gif.*|\.jpg.*|\.png.*|\.jepg.*|\.swf.*|\.bmp.*|\.ico.*#');

/**
 * 静态文件处理
 */
function handlerStatic($path, $isBase64Encoded)
{
    $filename = __DIR__ . "/public" . $path;
    if (!file_exists($filename)) {
        return [
            "isBase64Encoded" => false,
            "statusCode" => 404,
            "headers" => [
                'Content-Type' => '',
            ],
            "body" => "404 Not Found",
        ];
    }
    $handle = fopen($filename, "r");
    $contents = fread($handle, filesize($filename));
    fclose($handle);

    $base64Encode = false;
    $headers = [
        'Content-Type' => '',
        'Cache-Control' => "max-age=8640000",
        'Accept-Ranges' => 'bytes',
    ];
    $body = $contents;
    if ($isBase64Encoded || preg_match(BINARY_REG, $path)) {
        $base64Encode = true;
        $headers = [
            'Content-Type' => '',
            'Cache-Control' => "max-age=86400",
        ];
        $body = base64_encode($contents);
    }
    return [
        "isBase64Encoded" => $base64Encode,
        "statusCode" => 200,
        "headers" => $headers,
        "body" => $body,
    ];
}

function initEnvironment($isBase64Encoded)
{
    $envName = '';
    if (file_exists(__DIR__ . "/.env")) {
        $envName = '.env';
    } elseif (file_exists(__DIR__ . "/.env.production")) {
        $envName = '.env.production';
    } elseif (file_exists(__DIR__ . "/.env.local")) {
        $envName = ".env.local";
    }
    if (!$envName) {
        return [
            'isBase64Encoded' => $isBase64Encoded,
            'statusCode' => 500,
            'headers' => [
                'Content-Type' => 'application/json'
            ],
            'body' => $isBase64Encoded ? base64_encode([
                'error' => "Dotenv config file not exist"
            ]) : [
                'error' => "Dotenv config file not exist"
            ]
        ];
    }

    $dotenv = Dotenv\Dotenv::createImmutable(__DIR__, $envName);
    $dotenv->load();
}

function decodeFormData($rawData)
{
    $files = array();
    $data = array();
    $boundary = substr($rawData, 0, strpos($rawData, "\r\n"));

    $parts = array_slice(explode($boundary, $rawData), 1);
    foreach ($parts as $part) {
        if ($part == "--\r\n") {
            break;
        }

        $part = ltrim($part, "\r\n");
        list($rawHeaders, $content) = explode("\r\n\r\n", $part, 2);
        $content = substr($content, 0, strlen($content) - 2);
        // 获取请求头信息
        $rawHeaders = explode("\r\n", $rawHeaders);
        $headers = array();
        foreach ($rawHeaders as $header) {
            list($name, $value) = explode(':', $header);
            $headers[strtolower($name)] = ltrim($value, ' ');
        }

        if (isset($headers['content-disposition'])) {
            $filename = null;
            preg_match('/^form-data; *name="([^"]+)"(; *filename="([^"]+)")?/', $headers['content-disposition'], $matches);
            $fieldName = $matches[1];
            $fileName = (isset($matches[3]) ? $matches[3] : null);

            // If we have a file, save it. Otherwise, save the data.
            if ($fileName !== null) {
                $localFileName = tempnam('/tmp', 'sls');
                file_put_contents($localFileName, $content);

                $arr = array(
                    'name' => $fileName,
                    'type' => $headers['content-type'],
                    'tmp_name' => $localFileName,
                    'error' => 0,
                    'size' => filesize($localFileName)
                );

                if (substr($fieldName, -2, 2) == '[]') {
                    $fieldName = substr($fieldName, 0, strlen($fieldName) - 2);
                }

                if (array_key_exists($fieldName, $files)) {
                    array_push($files[$fieldName], $arr);
                } else {
                    $files[$fieldName] = $arr;
                }

                // register a shutdown function to cleanup the temporary file
                register_shutdown_function(function () use ($localFileName) {
                    unlink($localFileName);
                });
            } else {
                parse_str($fieldName . '=__INPUT__', $parsedInput);
                $dottedInput = arrayDot($parsedInput);
                $targetInput = arrayAdd([], array_keys($dottedInput)[0], $content);

                $data = array_merge_recursive($data, $targetInput);
            }
        }
    }
    return (object)([
        'data' => $data,
        'files' => $files
    ]);
}

function arrayGet($array, $key, $default = null)
{
    if (is_null($key)) {
        return $array;
    }

    if (array_key_exists($key, $array)) {
        return $array[$key];
    }

    if (strpos($key, '.') === false) {
        return $array[$key] ?? value($default);
    }

    foreach (explode('.', $key) as $segment) {
        $array = $array[$segment];
    }

    return $array;
}

function arrayAdd($array, $key, $value)
{
    if (is_null(arrayGet($array, $key))) {
        arraySet($array, $key, $value);
    }

    return $array;
}

function arraySet(&$array, $key, $value)
{
    if (is_null($key)) {
        return $array = $value;
    }

    $keys = explode('.', $key);

    foreach ($keys as $i => $key) {
        if (count($keys) === 1) {
            break;
        }

        unset($keys[$i]);

        if (!isset($array[$key]) || !is_array($array[$key])) {
            $array[$key] = [];
        }

        $array = &$array[$key];
    }

    $array[array_shift($keys)] = $value;

    return $array;
}

function arrayDot($array, $prepend = '')
{
    $results = [];

    foreach ($array as $key => $value) {
        if (is_array($value) && !empty($value)) {
            $results = array_merge($results, dot($value, $prepend . $key . '.'));
        } else {
            $results[$prepend . $key] = $value;
        }
    }

    return $results;
}

function getHeadersContentType($headers)
{
    if (isset($headers['Content-Type'])) {
        return $headers['Content-Type'];
    } else if (isset($headers['content-type'])) {
        return $headers['content-type'];
    }
    return '';
}

function handler($event, $context)
{
    require __DIR__ . '/vendor/autoload.php';

    $isBase64Encoded = $event->isBase64Encoded;


    initEnvironment($isBase64Encoded);

    $app = require __DIR__ . '/bootstrap/app.php';

    // change storage path to APP_STORAGE in dotenv
    $app->useStoragePath(env('APP_STORAGE', base_path() . '/storage'));


    // 获取请求路径
    $path = str_replace("//", "/", $event->path);

    if (preg_match(TEXT_REG, $path) || preg_match(BINARY_REG, $path)) {
        return handlerStatic($path, $isBase64Encoded);
    }

    // 处理请求头
    $headers = $event->headers ?? [];
    $headers = json_decode(json_encode($headers), true);

    // 处理请求数据
    $data = [];
    $rawBody = $event->body ?? null;
    if ($event->httpMethod === 'GET') {
        $data = !empty($event->queryString) ? $event->queryString : [];
    } else {
        if ($isBase64Encoded) {
            $rawBody = base64_decode($rawBody);
        }
        $contentType = getHeadersContentType($headers);
        if (preg_match('/multipart\/form-data/', $contentType)) {
            $requestData = !empty($rawBody) ? decodeFormData($rawBody) : [];
            $data = $requestData->data;
            $files = $requestData->files;
        } else if (preg_match('/application\/x-www-form-urlencoded/', $contentType)) {
            if (!empty($rawBody)) {
                mb_parse_str($rawBody, $data);
            }
        } else {
            $data = !empty($rawBody) ? json_decode($rawBody, true) : [];
        }
    }

    // 将请求交给 laravel 处理
    $kernel = $app->make(Kernel::class);

    var_dump($path, $event->httpMethod);

    $request = Request::create($path, $event->httpMethod, (array)$data, [], [], $headers, $rawBody);
    $request->headers = new HeaderBag($headers);
    if (!empty($files)) {
        $request->files->add($files);
    }


    $response = $kernel->handle($request);

    // 处理返回内容
    $body = $response->getContent();
    $headers = $response->headers->all();
    $response_headers = [];
    foreach ($headers as $k => $header) {
        if (is_string($header)) {
            $response_headers[$k] = $header;
        } elseif (is_array($header)) {
            $response_headers[$k] = implode(';', $header);
        }
    }

    return [
        'isBase64Encoded' => $isBase64Encoded,
        'statusCode' => $response->getStatusCode() ?? 200,
        'headers' => $response_headers,
        'body' => $isBase64Encoded ? base64_encode($body) : $body
    ];
}

2.创建 SCF 函数

  1. 登录腾讯云控制台,搜索并打开 函数服务

函数服务截图

  1. 点击新建,选择从头开始,函数类型选择 事件函数 ,运行环境选择 php 8.0
  2. 使用本地上传 zip 包方式,将本地代码上传,执行方法填写 handler.handler,即上文创建的 handler.php 中的 handler 方法

新建函数截图

下方高级配置按需要调整即可,(注意:函数运行日志会记录到腾讯云 CLS,该服务为收费服务,有免费额度,具体可参考配置页面下方说明),填写完成后点击完成生成函数

3.创建 api 网关服务

该服务月调用量小于等于 100 万次不收费,超过 100 万次后,每万次 0.06 元

  1. 打开腾讯云控制台, 搜索并进入 Api 网关 控制台,点击新建, 共享性, 直接提交即可
  2. 在刚创建的 Api 网关 上点击配置管理, 点击管理 Api ,点击新建
  3. API名称随意填写, 路径填写 /, 请求方法选择 any , 点击下一步
  4. 在后端配置中选择 云函数SCF ,选择刚创建的函数,并勾选响应集成

后端配置截图

  1. 点击下一步,直接提交保存即可,根据提示点击立即发布
  2. 点击基础配置,复制访问地址

访问地址

  1. 浏览器打开测试,返回如图

返回截图

  1. 根据个人需要在 自定义域名 中绑定个人域名即可

创建 TDSQL-C数据库

TDSQL-C 100% 兼容 mysql, 该服务可选择按量付费, 不使用不计费模式

  1. 打开腾讯云控制台,搜索并进入 TDSQL-C MySQL 版
  2. 点击新建,计费方式选择 Serverless, 之后根据个人需要调整各项配置即可

提示

  1. 该服务无法使用文件方式储存 session,可选择使用 redis 或 mysql 储存 session
  2. 由于 api 网关限制,上传文件最大支持 2M,如有上传需求,需在创建 api 网关勾选 base64 编码,建议使用第三方储存
  3. 如不需函数运行日志,可在 CLS 控制台直接删除系统创建的日志集

另外本人准备搭建一个个人博客网站,用于记录日常,有没有小伙伴的博客可以借(抄)鉴(袭)一下 :sunglasses:

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 14

队列和后台守护怎么跑?如果有了后台守护和队列,你的数据库会被一直连着,按需付费就没个卵用,比包年包月还贵,这些你考虑了吗?

2个月前 评论
荒街! (楼主) 2个月前

有些类似 AWS 的 Lambda 嘛?

2个月前 评论
荒街! (楼主) 2个月前
Yoooooo 2个月前

靠,突然发现我们三个头像,哈哈哈哈哈 :joy: :joy: :joy:

2个月前 评论

感觉上是在白嫖的样子啊.去试试

2个月前 评论

这个可以重新解析域名吗??

2个月前 评论
荒街! (楼主) 2个月前

不想折腾腾讯云服务器也很便宜 4核\4G\5M 三年也才199...

2个月前 评论
九霄道长 2个月前
win27149 2个月前
mengdodo 2个月前

赞,之前看到一直没有去折腾,下午去试试!感谢铺路!

2个月前 评论

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