これは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 環境における使用にて解説されています。
<?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
var vm = new Vue({
template: `<div>{{ msg }}</div>`,
data: {
msg: 'hello'
}
})
// `vue-server-renderer/basic.js` によってエクスポーズ
renderVueComponentToString(vm, (err, res) => {
print(res)
})
解説
-
vue.js
vue-server-renderer/basic.js
app.js
をテキストとしてファイルから読み込み - V8のインスタンスを作成
-
process
オブジェクトを作成し、グローバル空間にprocess
オブジェクトを格納するJavaScriptコードをV8で実行。 -
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のprint
でres
の値を出力しているのでPHP側で生成済みのHTMLが書き出されるという寸法です。
実装してみる
V8の準備
僕的にはこれが一番こころ折れる作業だと思っています...
昨今は便利で、DockerでV8が使えるイメージが用意されているのでそちらを使いました。
ディレクトリ構造
今回はこんな感じで作ってみました。
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を呼び出すエイリアスを作っとく。
{
"autoload": {
"psr-4": {
"VueSSR\\": "src/"
}
}
}
Rendererはnode_modules
のパスをわたして初期化します。
render
メソッドを呼び出す最にエントリポイントとVueにわたすデータ構造を配列で指定できるようにしています。
render
で渡された$data
は__PRELOAD_STATE__
というグローバルオブジェクトとしてJavaScriptに挿入されます。
<?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的に使えます。
<?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
に出力されるようになってます。
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
経由でサーバから渡らされた値を参照できるようになります。
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
に値が取得できます。
<template>
<p>{{message}}</p>
</template>
<script>
export default {
props: {
message: { type: String }
}
}
</script>
まとめ
PHPでVueJSをSSRする方法をご紹介しました。
現状だと簡単とまでは行かないですが、PHPを使っていてVueを試したいって方は見てみると新しい発見があるかもしれません。
今回ご紹介したコードはexample-php-ssr-vueというリポジトリで公開していますので興味ある方は是非読んでみてください。