Vue/React 元件 PHP 服務端渲染(SSR)可行性分析

chenos發表於2018-03-27

前幾天看了社群翻譯的一篇文章《使用 PHP 來做 Vue.js 的 SSR 服務端渲染》,文章講述的是現在很火的 JavaScript 服務端渲染(SSR),恰巧我對這方面略有研究,所以就趁著熱鬧寫上一篇。

概念

得益於 Google V8 引擎,服務端執行 JavaScript,除了正統的 Node.js 以外,還有其他語言各自實現的 V8 擴充套件庫,這是解決 JavaScript 服務端執行最有效的辦法。不過 V8 擴充套件和 Node 還是有許多不同,除了缺少 Node API,還包括一些最佳化(如事件驅動、非同步 I/O)。

你可能看不懂上面這段描述,不妨,我們接著科普幾個概念。

V8 是什麼?

V8 是 Google 開源的 JavaScript 引擎(C++ 編寫),用於 Chrome 系列瀏覽器。JavaScript 引擎是用於執行 JavaScript 指令碼的虛擬機器(解析器),實現了核心的 ECMAScript 語法解析功能,但不包括 WEB API(DOM/CSSOM 等)和 Node API 等。

V8 和 Node.js 的關係

Node.js 是基於 V8 引擎實現的可用於服務端 JavaScript 的執行環境,為了方便服務端應用,提供了一系列 API。除了 V8 之外,Node.js 還引進了 libuv 來處理事件驅動、非同步 I/O。

V8 和 ext-v8js 擴充套件庫的關係

ext-v8js 是基於 V8 實現的 PHP 擴充套件庫,可用於 PHP 服務端執行 JavaScript 指令碼。JavaScript 執行於安全沙盒中,可以透過注入和匯出實現上下文資料通訊。ext-v8js 只實現了核心的 ECMAScript 語法解析功能,不包含 Node API 和事件驅動、非同步 I/O 處理,如果需要這方面特性,可以結合 ext-ev(libev)/ext-event(libevent)擴充套件庫實現,如 reactphp、amphp 等。PHP 提供的事件驅動相關的擴充套件庫有很多(libev、libuv、libevent ),推薦使用基於 libevent 實現的 ext-event。

概念性的就說到這裡,接下來簡單介紹如何安裝 v8js 擴充套件庫。v8js 是我見過最難安裝的擴充套件庫,安裝擴充套件前需要安裝 v8 引擎,但是因為各種原因(此處省略 800 字),建議想嚐鮮的朋友使用 Docker 環境。分享個早先我編譯好的 docker-v8,可以用於 Debian Jessie。另外,Ubuntu 還可以用 pinepain 的 libv8 ,Mac OS 也可以用 brew 安裝。

業務需要,折騰 v8js 也有段時間了,但是 v8js 太原始,所以我寫了 chenos/v8js-module-loaderchenos/execjs,感興趣的可以 star 一下。

PHP 服務端執行 JavaScript 程式碼

好了,我們進入正題。先來看看 Node.js 是怎麼做的,以 vue.js 為例。

// hello.js
const Vue = require('vue')
const renderer = require('vue-server-renderer')
const renderVueComponentToString = require('vue-server-renderer/basic')

const app = new Vue({
  template: `<div>Hello Vue!</div>`
})

renderVueComponentToString(app, (err, html) => {
    console.log(html)
})

執行結果如下:

$ node hello.js
# <div data-server-rendered="true">Hello Vue!</div>

那在 PHP 裡應該怎麼做呢?以 chenos/execjs 為例:

<?php
// hello.php

// 初始化 JS 安全沙盒執行環境
$context = new Chenos\ExecJs\Context();

// 指定 module loader 的路徑
$context->getLoader()
    ->setEntryDir(__DIR__)
    ->addVendorDir(__DIR__.'/node_modules')
    ->addOverride('vue', 'vue/dist/vue.runtime.common.js')
;

// 宣告全域性變數 & 函式
$context->eval("
    global.process = {env: {VUE_ENV: 'server'}}
    global.console = {log: print}
");

// 必須加上 ./ 表示相對路徑
$context->load('./hello.js'); 
echo PHP_EOL;

執行結果如下:

$ php hello.php
# <div data-server-rendered="true">Hello Vue!</div>

這個例子太簡單了,增加一下難度吧。我們宣告一個 Vue 類用來處理相關操作。

class Vue
{
    public function __construct()
    {
        $context = new Chenos\ExecJs\Context();

        // 指定 module loader 的路徑
        $context->getLoader()
            ->setEntryDir(__DIR__)
            ->addVendorDir(__DIR__.'/node_modules')
            ->addOverride('vue', 'vue/dist/vue.runtime.common.js')
        ;
        // 宣告全域性變數 & 函式
        $context->eval("
            global.process = {env: {VUE_ENV: 'server'}}
            global.console = {log: print}
        ");

        $this->context = $context;
    }

    public function render($component, $propsData = [])
    {
        $this->context->component = $component;
        $this->context->propsData = $propsData;
        $this->context->eval("
            var renderVueComponentToString = require('vue-server-renderer/basic')
            var Component = require(PHP.component)
            var component = new Component({ propsData: PHP.propsData })
            renderVueComponentToString(component, (err, html) => {
                console.log(html)
            })
        ");
        echo PHP_EOL;
    }
}

新增一個簡單的 hello.js 元件,需要注意的是如果是 vue 或者 jsx 模板需要透過 webpack/rollup 打包再執行編譯過的 JS 檔案。

const Vue = require('vue')

module.exports = Vue.extend({
  template: `<div class="hello">
    <h1 class="hello__title">Hello {{ msg }}!</h1>
  </div>`,
  props: ['msg'],
})

呼叫:

// vue.php
$vue = new Vue();
$vue->render('./hello.js', ['msg' => 'Vuejs']);

執行結果:

$ php vue.php
# <div data-server-rendered="true" class="hello"><h1 class="hello__title">Hello Vuejs!</h1></div>

引申到框架層面,只要實現各自框架的 View 介面,我們就可以在任意框架裡實現 Vue/React 元件的服務端渲染了,而並非只能用 twig/php/html 之類的模板引擎了。

PHP 部分的核心思想也就這些了,更多的可能與 Node 有關,如果有相關的 SSR 經歷,會很容易理解上述程式碼。不知道我講清楚了沒有,大家看懂程式碼了沒有,更多例子 點選這裡檢視

效能

接下來說個大家比較關注的效能問題,業務需要,我們的一些專案前端用了 markdown-it/js-yaml 這些非常好用的 JS 庫,最關鍵的是它們可以滿足我們自定義方面的需求,但是後端苦苦找不到合適的第三方庫,直到我們嘗試了 v8js 方案,解析效率比 PHP 版本好太多,最關鍵的是前後程式碼同構,我們只需要維護前端庫就可以了。

效能方面,我用了幾個常用的 npm package 做了簡單的 time 測試,直接放結論:

ext-v8js > node.js >> php

對細節感興趣的可以 點選這裡檢視

最後

在 PHP 端執行 JavaScript 程式碼,不是個常規需求,但是合理使用能夠解決一些痛點需求,尤其在如今越來越多的業務邏輯前移到前端之後,當你眼饞 NPM 上大量 JS 庫不能跨平臺使用的時候,我們並不需要另立一個 Node 伺服器,使用 ext-v8js 擴充套件也能夠解決這類問題。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章