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-loader 和 chenos/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 协议》,转载必须注明作者和本文链接
centos翻墙后按照好V8扩展 太辛苦了。填了几天的坑