Edited at
PHPDay 16

PHPでVueをSSRする方法

これはPHP Advent Calendar 2018の16日目の記事です。


背景

PHPはテンプレートエンジンなんてよく言われますが、昨今ではTwig、Smarty、mustacheなど様々なテンプレートエンジンが存在しています。

しかし昨今のWeb界隈ではHTM、CSS、JavaScriptの三権分立からコンポーネントとして機能で責務を分離するパラダイムにシフトしつつあります。

それを支える技術の一旦にServer-Side-Renderingというサーバ上でJavaScriptを実行してレスポンスを構築する手段が確立され、

今年は特にNuxtJSの反響がおおきかったように感じます。

さて、SSRをしたくてもNuxtを導入するのは超えなければならないハードルがいくつかあり、少々難易度が高く感じます。

せめてVueをPHPでテンプレートエンジンのように使えればなぁ... なんて思ったりしませんか?

この記事は、それを目指してPHPでVueをSSRする方法を試した内容をお話します。


PHPでJavaScriptを実行する

PHPでJavaScriptを実行するには現在2通りの手段があります。

なおこれらについてはPHPのV8JsでSSRという記事で詳しく説明されています。

また、Reactを使ったSSRについてはBaracoaというライブラリを用いれば簡単にできそうなので興味ある人はぜひ読んでみてください。

今回はV8Jsを使ってVueをレンダリングしてみます。


VueでSSRする方法

Vue 2.xにおけるサーバサイドレンダリングについてはVue SSR ガイド - 非 Node.js 環境における使用にて解説されています。


render.php

<?php

$vue_source = file_get_contents('/path/to/vue.js');
$renderer_source = file_get_contents('/path/to/vue-server-renderer/basic.js');
$app_source = file_get_contents('/path/to/app.js');

$v8 = new V8Js();

$v8->executeString('var process = { env: { VUE_ENV: "server", NODE_ENV: "production" }}; this.global = { process: process };');
$v8->executeString($vue_source);
$v8->executeString($renderer_source);
$v8->executeString($app_source);



app.js

// app.js

var vm = new Vue({
template: `<div>{{ msg }}</div>`,
data: {
msg: 'hello'
}
})

// `vue-server-renderer/basic.js` によってエクスポーズ
renderVueComponentToString(vm, (err, res) => {
print(res)
})



解説



  1. vue.js vue-server-renderer/basic.js app.js をテキストとしてファイルから読み込み

  2. V8のインスタンスを作成


  3. processオブジェクトを作成し、グローバル空間にprocessオブジェクトを格納するJavaScriptコードをV8で実行。


  4. vue vue-server-renderer app の順でV8で実行

Reactに比べると少し複雑です。


vue-server-renderer

vue-server-rendererはVueをSSRするためのユーティリティです。サーバ環境上でVueを実行し、HTMLのテキストとして出力できます。

app.js上で使っているrenderVueComponentToStringはこのvue-server-rendererが生成した関数で、この処理をV8で実行するとコールバックに生成結果が入ってきます。V8のprintresの値を出力しているのでPHP側で生成済みのHTMLが書き出されるという寸法です。


実装してみる


V8の準備

僕的にはこれが一番こころ折れる作業だと思っています...

昨今は便利で、DockerでV8が使えるイメージが用意されているのでそちらを使いました。

V8Js docker image


ディレクトリ構造

今回はこんな感じで作ってみました。

app.phpでHTMLを出力して煩雑なレンダリング処理は別のファイルに追い出しました。

viewに関してはwebpackでバンドルしていますが、簡単に構造を示すためにVueCLIなどは用いていません。

+- src

| +- Renderer.php
+- views/
| +- dist/
| +- src/
| | +- App.vue
| | +- index.js
| +- package.json
| +- webpack.config.js
+- app.php
+- composer.json



  • app.php - HTMLを返すエントリポイント


  • src/Renderer.php - V8でJavaScriptを実行して結果を返すヤツ


  • views/src/index.js - renderVueComponentToStringするエントリポイント


  • views/src/App.vue - Vueファイル


PHP側の実装

Rendererを呼び出すエイリアスを作っとく。


composer.json

{

"autoload": {
"psr-4": {
"VueSSR\\": "src/"
}
}
}

Rendererはnode_modulesのパスをわたして初期化します。

renderメソッドを呼び出す最にエントリポイントとVueにわたすデータ構造を配列で指定できるようにしています。

renderで渡された$data__PRELOAD_STATE__というグローバルオブジェクトとしてJavaScriptに挿入されます。


src/Renderer.php

<?php

namespace VueSSR;
use V8Js;
class Renderer
{
private $nodePath;
private $vueSource;
private $rendererSource;
private $v8;
/**
* @param string $nodeModulesPath
* @return void
*/

public function __construct (string $nodeModulesPath)
{
$this->nodePath = $nodeModulesPath;
$this->v8 = new V8Js();
}
/**
* @param string $entrypoint
*/

public function render(string $entrypoint, array $data)
{
$state = json_encode($data);
$app = file_get_contents($entrypoint);
$this->setupVueRendderer();
$this->v8->executeString("var __PRELOAD_STATE__ = ${state}; this.global.__PRELOAD_STATE__ = __PRELOAD_STATE__;");
$this->v8->executeString($app);
}
private function setupVueRendderer()
{
$prepareCode = <<<'EOT'
var process = {
env: {
VUE_ENV: "server",
NODE_ENV: "production"
}
};
this.global = { process: process };
EOT;

$vueSource = file_get_contents($this->nodePath . 'vue/dist/vue.js');
$rendererSource = file_get_contents($this->nodePath . 'vue-server-renderer/basic.js');
$this->v8->executeString($prepareCode);
$this->v8->executeString($vueSource);
$this->v8->executeString($rendererSource);
}
}

出力するHTMLの用意。

以下のように実行するとPartial的に使えます。


app.php

<?php

require_once "vendor/autoload.php";
use VueSSR\Renderer;
$renderer = new Renderer('views/node_modules/');
?>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>Example SSR Vue on PHP</title>
</head>
<body>
<?php $renderer->render('./views/dist/main.bundle.js', [
'message' => 'hello vue!'
]); ?>
</body>
</html>


viewsの実装

依存の構築。

npm install --save vue vue-server-renderer

npm install -D webpack webpack-cli vue-loader vue-template-compiler

WebPackでビルドを設定します。今回はエントリポイントが一つですが、複数のエントリポイントを設定してページを分けることも可能です。

views/dist/main.bundle.jsに出力されるようになってます。


views/webpack.config.js

const path = require('path')

const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
entry: './src',
output: {
path: path.resolve('./dist'),
filename: '[name].bundle.js',
},
module: {
rules: [
{ test: /\.vue$/, use: 'vue-loader' },
]
},
plugins: [
new VueLoaderPlugin(),
]
}


App.vueを読み込んで、renderVueComponentToStringに渡しています。

このときに__PRELOAD_STATE__をサーバから受け取って、App.propsDataに渡しています。

これによって、App.vueからprops経由でサーバから渡らされた値を参照できるようになります。


views/src/index.js

import App from './App.vue'

App.propsData = __PRELOAD_STATE__
var vm = new Vue(App);

renderVueComponentToString(vm, (err, res) => {
if (err) throw new Error(err);
print(res)
})


はい、Vueファイルです。

見ての通り、propsでサーバから渡したmessageに値が取得できます。


views/src/App.vue

<template>

<p>{{message}}</p>
</template>

<script>
export default {
props: {
message: { type: String }
}
}
</script>



まとめ

PHPでVueJSをSSRする方法をご紹介しました。

現状だと簡単とまでは行かないですが、PHPを使っていてVueを試したいって方は見てみると新しい発見があるかもしれません。

今回ご紹介したコードはexample-php-ssr-vueというリポジトリで公開していますので興味ある方は是非読んでみてください。