VOOZH about

URL: https://read01.com/kE382xk.html

⇱ 使用 PHP 來做 Vue.js 的 SSR 服務端渲染 - 壹讀


Saturday, Apr 11, 2026

使用 PHP 來做 Vue.js 的 SSR 服務端渲染

2018/04/03 來源:知乎專欄

對於客戶端應用來說,服務端渲染是一個熱門話題。然而不幸的是,這並不是一件容易的事,尤其是對於不用 Node.js 環境開發的人來說。

我發布了兩個庫讓 PHP 從服務端渲染成為可能.spatie/server-side-renderingspatie/laravel-server-side-rendering適配 laravel 應用。

讓我們一起來仔細研究一些服務端渲染的概念,權衡優缺點,然後遵循第一法則用 PHP 建立一個服務端渲染。

什麼是服務端渲染

一個單頁應用(通常也叫做 SPA )是一個客戶端渲染的 App 。這是一個僅在瀏覽器端運行的應用。如果你正在使用框架,比如 React, Vue.js 或者 AngularJS ,客戶端將從頭開始渲染你的 App 。

瀏覽器的工作

在 SPA 被啟動並準備使用之前,瀏覽器需要經過幾個步驟。

  • 下載 JavaScript 腳本
  • 解析 JavaScript 腳本
  • 運行 JavaScript 腳本
  • 取回數據(可選,但普遍)
  • 在原本的空容器渲染應用 (首次有意義的渲染)
  • 準備完成! (可以交互啦)

用戶不會看到任何有意義的內容,直到瀏覽器完全渲染 App(需要花費一點時間)。這會造成一個明顯的延遲,直到 首次有意義的渲染 完成,從而影響了用戶體驗。

這就是為什麼服務端渲染(一般被稱作 SSR )登場的原因。SSR 在伺服器預渲染初始應用狀態。這裡是瀏覽器在使用服務端渲染後需要經過的步驟:

  • 渲染來自服務端的 HTML (首次有意義的渲染)
  • 下載 JavaScript 腳本
  • 解析 JavaScript 腳本
  • 運行 JavaScript 腳本
  • 取回數據
  • 使已存在的 HTML 頁面可交互
  • 準備完成! (可以交互啦)

由於伺服器提供了 HTML 的預渲染塊,因此用戶無需等到一切完成後才能看到有意義的內容。注意,雖然 交互時間 仍然處於最後,但可感知的表現得到了巨大的提升。

服務端渲染的優點

服務端渲染的主要優點是可以提升用戶體驗。並且,如果你的網站需要應對不能執行 JavaScript 的老舊爬蟲,SSR 將是必須的,這樣,爬蟲才能索引服務端渲染過後的頁面,而不是一個空蕩蕩的文檔。

服務端如何渲染?

記住服務端渲染並非微不足道,這一點很重要。當你的 Web 應用同時運行在瀏覽器和伺服器,而你的 Web 應用依賴 DOM 訪問,那麼你需要確保這些調用不會在服務端觸發,因為沒有 DOM API 可用。

基礎設施複雜性

假設你決定了服務端渲染你的應用端程序,你如果正在閱讀這篇文章,很大可能正在使用 PHP 構建應用的大部分(功能)。但是,服務端渲染的 SPA 需要運行在 Node.js 環境,所以將需要維護第二個程序。

你需要構建兩個應用程式之間的橋樑,以便它們進行通信和共享數據:需要一個 API。構建無狀態 API 相比於構建有狀態是比較 困難 的。你需要熟悉一些新概念,例如基於 JWT 或 OAUTH 的驗證,CORS,REST ,添加這些到現有應用中是很重要的。

有得必有所失,我們已經建立了 SSR 以增加 Web 應用的用戶體驗,但 SSR 是有成本的。

伺服器端渲染權衡取捨

伺服器上多了一個額外的操作。一個是伺服器增加了負載壓力,第二個是頁面響應時間也會稍微加長。 不過因為現在伺服器返回了有效內容,在用戶看來,第二個問題的影響不大。

大部分時候你會使用 Node.js 來渲染你的 SPA 代碼。如果你的後端代碼不是使用 Javascript 編寫的話,新加入 Node.js 堆棧將使你的程序架構變得複雜。

為了簡化基礎架構的複雜度, 我們需要找到一個方法,使已有的 PHP 環境作為服務端來渲染客戶端應用。

在 PHP 中渲染 JavaScript

在伺服器端渲染 SPA 需要集齊以下三樣東西:

  • 一個可以執行 JavaScript 的引擎
  • 一個可以在伺服器上渲染應用的腳本
  • 一個可以在客戶端渲染和運行應用的腳本

下面的例子使用了 Vue.js。你如果習慣使用其它的框架(例如 React),不必擔心,它們的核心思想都是類似的,一切看起來都是那麼相似。

簡單起見,我們使用經典的 「 Hello World 」 例子。

下面是程序的代碼(沒有 SSR):

// app.js import Vue from 'vue' new Vue({ template: ` <div>Hello, world!</div> `, el: '#app' })
這短代碼實例化了一個 Vue 組件,並且在一個容器(id 值為app的 空div)渲染。

如果在服務端運行這點腳本,會拋出錯誤,因為沒有 DOM 可訪問,而 Vue 卻嘗試在一個不存在的元素里渲染應用。

重構這段腳本,使其 可以 在服務端運行。

// app.js import Vue from 'vue' export default => new Vue({ template: ` <div>Hello, world!</div> ` }) // entry-client.js import createApp from './app' const app = createApp app.$mount('#app')
我們將之前的代碼分成兩部分。app.js作為創建應用實例的工廠,而第二部分,即entry-client.js,會運行在瀏覽器,它使用工廠創建了應用實例,並且掛載在 DOM。

現在我們可以創建一個沒有 DOM 依賴性的應用程式,可以為服務端編寫第二個腳本。

// entry-server.js import createApp from './app' import renderToString from 'vue-server-renderer/basic' const app = createApp renderToString(app, (err, html) => { if (err) { throw new Error(err) } // Dispatch the HTML string to the client... })

我們引入了相同的應用工廠,但我們使用服務端渲染的方式來渲染純 HTML 字符串,它將包含應用初始狀態的展示。

我們已經具備三個關鍵因素中的兩個:服務端腳本和客戶端腳本。現在,讓我們在 PHP 上運行它吧!

執行 JavaScript

在 PHP 運行 JavaScript,想到的第一個選擇是 V8Js。V8Js 是嵌入在 PHP 擴展的 V8 引擎,它允許我們執行 JavaScript。

使用 V8Js 執行腳本非常直接。我們可以用 PHP 中的輸出緩衝和 JavaScript 中的print來捕獲結果。
$v8 = new V8Js; ob_start; // $script 包含了我們想執行的腳本內容 $v8->executeString($script); echo ob_get_contents; print('<div>Hello, world!</div>')

這種方法的缺點是需要第三方 PHP 擴展,而擴展可能很難或者不能在你的系統上安裝,所以如果有其他(不需要安裝擴展的)方法,它會更好的選擇。

這個不一樣的方法就是使用 Node.js 運行 JavaScript。我們可以開啟一個 Node 進程,它負責運行腳本並且捕獲輸出。
Symfony 的Process組件就是我們想要的。
use Symfony\Component\Process\Process; // $nodePath 是可執行的 Node.js 的路徑 // $scriptPath 是想要執行的 JavaScript 腳本的路徑 new Process([$nodePath, $scriptPath]); echo $process->mustRun->getOutput; console.log('<div>Hello, world!</div>')
注意,(列印)在 Node 中是調用console.logspatie/server-side-rendering 包的其中一個關鍵理念是引擎接口。引擎就是上述 JavaScript 執行的一個抽象概念。
namespace Spatie\Ssr; /** * 創建引擎接口。 */ interface Engine { public function run(string $script): string; public function getDispatchHandler: string; }
run方法預期一個腳本的輸入 (腳本 內容,不是一條路徑),並且返回執行結果。getDispatchHandler允許引擎聲明它預期腳本如何展示發布。例如 V8 中的print方法,或是 Node 中的。

V8Js 引擎實現起來並不是很花俏。它更類似於我們上述理念的驗證,帶有一些附加的錯誤處理機制。

namespace Spatie\Ssr\Engines; use V8Js; use V8JsException; use Spatie\Ssr\Engine; use Spatie\Ssr\Exceptions\EngineError; /** * 創建一個 V8 類來實現引擎接口類 Engine 。 */ class V8 implements Engine。 { /** @var \V8Js */ protected $v8; public function __construct(V8Js $v8) { $this->v8 = $v8; } /** * 打開緩衝區。 * 返回緩衝區存儲v8的腳本處理結果。 */ public function run(string $script): string { try { ob_start; $this->v8->executeString($script); return ob_get_contents; } catch (V8JsException $exception) { throw EngineError::withException($exception); } finally { ob_end_clean; } } public function getDispatchHandler: string { return 'print'; } }
注意這裡我們將V8JsException重新拋出作為我們的EngineError。 這樣我們就可以在任何的引擎視線中捕捉相同的異常。

Node 引擎會更加複雜一點。不像 V8Js,Node 需要 文件 去執行,而不是腳本內容。在執行一個服務端腳本前,它需要被保存到一個臨時的路徑。

namespace Spatie\Ssr\Engines; use Spatie\Ssr\Engine; use Spatie\Ssr\Exceptions\EngineError; use Symfony\Component\Process\Process; use Symfony\Component\Process\Exception\ProcessFailedException; /** * 創建一個 Node 類來實現引擎接口類 Engine 。 */ class Node implements Engine { /** @var string */ protected $nodePath; /** @var string */ protected $tempPath; public function __construct(string $nodePath, string $tempPath) { $this->nodePath = $nodePath; $this->tempPath = $tempPath; } public function run(string $script): string { // 生成一個隨機的、獨一無二的臨時文件路徑。 $tempFilePath = $this->createTempFilePath; // 在臨時文件中寫進腳本內容。 file_put_contents($tempFilePath, $script); // 創建進程執行臨時文件。 $process = new Process([$this->nodePath, $tempFilePath]); try { return substr($process->mustRun->getOutput, 0, -1); } catch (ProcessFailedException $exception) { throw EngineError::withException($exception); } finally { unlink($tempFilePath); } } public function getDispatchHandler: string { return 'console.log'; } protected function createTempFilePath: string { return $this->tempPath.'/'.md5(time).'.js'; } }

除了臨時路徑步驟之外,實現方法看起來也是相當直截了當。

我們已經創建好了 Engine 接口,接下來需要編寫渲染的類。以下的渲染類來自於 spatie/server-side-rendering 擴展包,是一個最基本的渲染類的結構。

渲染類唯一的依賴是 Engine 接口的實現:

class Renderer { public function __construct(Engine $engine) { $this->engine = $engine; } }
渲染方法render里將會處理渲染部分的邏輯,想要執行一個 JavaScript 腳本文件,需要以下兩個元素:一個簡單的
class Renderer { public function render(string $entry): string { $serverScript = implode(';', [ "var dispatch = {$this->engine->getDispatchHandler}", file_get_contents($entry), ]); return $this->engine->run($serverScript); } }
此方法接受文件路徑作為參數。我們需要將解析前的 HTML 從腳本中分發到 PHP 環境中。dispatch方法返回Engine類里的方法,dispatch需要在伺服器腳本加載前運行。還記得我們的伺服器端入口腳本嗎?接下來我們在此腳本中調用我們的dispatch
// entry-server.js import app from './app' import renderToString from 'vue-server-renderer/basic' renderToString(app, (err, html) => { if (err) { throw new Error(err) } dispatch(html) })
file_get_contents方法讀取文件即可。我們已經成功創建了一個 PHP 的 SSR 。spatie/server-side-rendering 中的完整渲染器Renderer跟我們實現有點不一樣,他們擁有更高的容錯能力,和更加豐富的功能如有一套 PHP 和 JavaScript 共享數據的機制。如果你感興趣的話,建議你閱讀下源碼 server-side-rendering 代碼庫 。三思而後行

我們弄清楚了伺服器端渲染的利和弊,知道 SSR 會增加應用程式架構和基礎結構的複雜度。如果伺服器端渲染不能為你的業務提供任何價值,那麼你可能不應該首先考慮他。

如果你 確實 想開始使用伺服器端渲染,請先閱讀應用程式的架構。大多數 JavaScript 框架都有關於 SSR 的深入指南。Vue.js 甚至有一個專門的 SSR 文檔網站,解釋了諸如數據獲取和管理用於伺服器端渲染的應用程式方面的坑。

如果可能,請使用經過實戰檢驗的解決方案

有許多經過實戰檢驗的解決方案,能提供很好的 SSR 開發體驗。比如,如果你在構建 React 應用,可以使用 Next.js,或者你更青睞於 Vue 則可用 Nuxt.js,這些都是很引人注目的項目。

還不夠?嘗試 PHP 服務端渲染

你僅能以有限的資源來管理基礎架構上的複雜性。你想將服務端渲染作為大型 PHP 應用中的一部分。你不想構建和維護無狀態的 API。 如果這些原因和你的情況吻合,那麼使用 PHP 進行服務端渲染將會是個不錯方案。

我已經發布兩個庫來支持 PHP 的服務端 JavaScript 渲染: spatie/server-side-rendering 和專為 Laravel 應用打造的 spatie/laravel-server-side-rendering 。Laravel 定製版在 Laravel 應用中近乎 0 配置即可投入使用,通用版需要根據運行環境做一些設置調整。當然,詳細內容可以參考軟體包自述文件。

如果你僅是想體驗,從 spatie/laravel-server-side-rendering-examples 檢出項目並參考指南進行安裝。

如果你考慮服務端渲染,我希望這類軟體包可以幫到你,並期待通過 Github 做進一步問題交流和反饋!

您可能感興趣
免責聲明:本文內容來源于知乎專欄,文章觀點不代表壹讀立場,如若侵犯到您的權益,或涉不實謠言,敬請向我們提出檢舉
最新文章 / 服務條款 / 私隱保護 / DMCA / 聯絡我們

壹讀/READ01.COM