摸魚過程中偶然發現了騰訊雲出了 SCF
函式服務(可能早就有了,但我不知道),經過一些研究探索,成功執行了 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
函式
- 登入騰訊雲控制檯,搜尋並開啟
函式服務
- 點選新建,選擇從頭開始,函式型別選擇 事件函式 ,執行環境選擇
php 8.0
- 使用本地上傳 zip 包方式,將原生程式碼上傳,執行方法填寫
handler.handler
,即上文建立的handler.php
中的handler
方法
下方高階配置按需要調整即可,(注意:函式執行日誌會記錄到騰訊雲 CLS
,該服務為收費服務,有免費額度,具體可參考配置頁面下方說明),填寫完成後點選完成生成函式
3.建立 api 閘道器服務
該服務月呼叫量小於等於 100 萬次不收費,超過 100 萬次後,每萬次 0.06 元
- 開啟騰訊雲控制檯, 搜尋並進入
Api 閘道器
控制檯,點選新建, 共享性, 直接提交即可 - 在剛建立的
Api 閘道器
上點選配置管理, 點選管理 Api ,點選新建 - API名稱隨意填寫, 路徑填寫
/
, 請求方法選擇 any , 點選下一步 - 在後端配置中選擇 雲函式SCF ,選擇剛建立的函式,並勾選響應整合
- 點選下一步,直接提交儲存即可,根據提示點選立即釋出
- 點選基礎配置,複製訪問地址
- 瀏覽器開啟測試,返回如圖
- 根據個人需要在 自定義域名 中繫結個人域名即可
建立 TDSQL-C資料庫
TDSQL-C
100% 相容 mysql
, 該服務可選擇按量付費, 不使用不計費模式
- 開啟騰訊雲控制檯,搜尋並進入 TDSQL-C MySQL 版
- 點選新建,計費方式選擇 Serverless, 之後根據個人需要調整各項配置即可
提示
- 該服務無法使用檔案方式儲存 session,可選擇使用 redis 或 mysql 儲存 session
- 由於 api 閘道器限制,上傳檔案最大支援 2M,如有上傳需求,需在建立 api 閘道器勾選 base64 編碼,建議使用第三方儲存
- 如不需函式執行日誌,可在
CLS
控制檯直接刪除系統建立的日誌集
另外本人準備搭建一個個人部落格網站,用於記錄日常,有沒有小夥伴的部落格可以借(抄)鑑(襲)一下
本作品採用《CC 協議》,轉載必須註明作者和本文連結