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

前几天看了社区翻译的一篇文章《使用 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 协议》,转载必须注明作者和本文链接
本帖由 Summer 于 6年前 加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 1

centos翻墙后按照好V8扩展 太辛苦了。填了几天的坑

5年前 评论

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