指路2.0: 基于 Laravel 框架做 Vue.js 的 SSR 服务端渲染2.0
为什么要做 SSR?
主要的目的是有利于搜索引擎的 SEO 抓取
SPA场景下SEO的问题
- SPA 应用加载的基本流程:浏览器端先加载一个空页面和 JavaScript 脚本,然后异步请求接口获取数据,渲染页面数据内容后展示给用户
- 问题:搜索引擎抓取页面解析该页面HTML中关键字、内容时 JavaScript 尚未调用执行,仅仅是一个空页面(body为空),影响搜索引擎收录页面的内容排行。
- 解决方案:使用服务端端数据渲染,在页面请求时将页面内容渲染到页面上输出(即,后台直出)
服务端如何进行渲染?
在这里我们主要考虑的是 PHP 的环境下进行 SSR。我们首先需要:
- 一个可以执行 JavaScript 的引擎
- 一个可以在服务器上渲染应用的脚本
- 一个可以在客户端渲染和运行应用的脚本
在 PHP 中运行 JavaScript,我的选择是使用一个 PHP 的第三方扩展 V8Js(之前在其他的文章看到有人做过测试,它的效率比使用 Node.js 要高)。不过 V8Js 最大的缺点就是安装起来比较麻烦。
安装 V8Js
安装的大致思路及遇到的问题:
- v8js 需要依赖 libv8 等其他扩展,但是这些扩展也非常的不好装,我直接下载了所有需要的 .so 文件并将它们复制到 /opt 中直接使用
- pecl 直接 install 的 v8js 包有问题,我安装完后运行代码时提示找不到 class: v8js。于是我直接下载了 v8js-2.1.0.tgz 文件进行解压安装
在 laradock 中安装 V8Js
需要注意的是,不是 workspace 的 docker 中安装,而是在 php-fpm 中安装 V8Js,它才是真正运行的环境
- 首先,在
laradock/.env
中添加变量PHP_FPM_INSTALL_V8JS=true
- 在
laradock/docker-compose.yml
中 php-fpm 的 args:- INSTALL_V8JS=${PHP_FPM_INSTALL_V8JS}
- 在
laradock/php-fpm/DockerFile
中添加:########################################################################### # PHP V8JS: ########################################################################### USER root ARG INSTALL_V8JS=false ARG PHP_VERSION=${PHP_VERSION} WORKDIR / RUN if [ ${INSTALL_V8JS} = true ]; then \ apt-get update && \ apt-get install -y patchelf libv8-dev && \ apt-get -y install sudo wget && \ wget https://github.com/laradock/laradock/raw/6052393d05de8136d87801c27998f9aa07883a83/php-fpm/v8.tar.gz && \ tar xzf v8.tar.gz && \ for A in /opt/v8/lib/*.so; do \ patchelf --set-rpath '$ORIGIN' $A; \ done && \ wget http://pecl.php.net/get/v8js-2.1.0.tgz && \ tar xzf v8js-2.1.0.tgz && \ cd v8js-2.1.0 && \ phpize && \ ./configure --with-v8js=/opt/v8 LDFLAGS="-lstdc++" && \ make && \ make install && \ docker-php-ext-enable v8js && \ rm -rf v8.tar.gz && \ rm -rf v8js-2.1.0.tgz && \ rm -rf v8js-2.1.0 \ ;fi
- 在
laradock/php-fpm/php7.1.ini
中添加:[V8Js] extension=v8js.so
- 重新构建:
docker-compose build --no-cache php-fpm
- 重新启动 docker-compose
在 Vagrant 中安装 V8Js
- 安装 V8Js 所需依赖:
yum -y install re2c v8-devel php-devel wget https://github.com/laradock/laradock/raw/6052393d05de8136d87801c27998f9aa07883a83/php-fpm/v8.tar.gz tar xzf v8.tar.gz mv ./opt/v8 /opt/
- 安装 v8js
tar xzf v8js-2.1.0.tgz cd v8js-2.1.0 phpize #### 若 phpize 报错:perl: warning: Setting locale failed #### vim ~/.zshrc LC_CTYPE=en_US.UTF-8 LC_ALL=en_US.UTF-8 source ~/.zshrc ############################ ./configure --with-v8js=/opt/v8 LDFLAGS="-lstdc++" make make install (执行后会返回 v8js.so 的安装目录,如:Installing shared extensions: /usr/lib64/php/modules/) php -i | grep extension_dir (获取 php 安装扩展的路径,如:/usr/lib64/php/modules) #### 若两个路径不一致需要将 v8js.so 拷贝至 php extension_dir 中 #### cp v8js.so /usr/lib64/php/modules/ ############################ rm -rf v8.tar.gz rm -rf v8js-2.1.0.tgz rm -rf v8js-2.1.0
- 在 php.ini 中添加
extension=v8js.so
(php --ini
可以查看 php.ini 的文件位置) - 重启 nginx 和 php-fpm
- 验证:
若没有报错:php -a $v8 = new \V8Js();
Warning: Uncaught Error: Class 'V8Js' not found in php shell
,则安装成功
前端的两个脚本
安装完 V8Js 后我们已经具备了一个可以执行 JavaScript 的引擎,之前提到:我们还需要一个可以在服务器上渲染应用的脚本和一个可以在客户端渲染和运行应用的脚本,现在我们就在实现前端需要的几个文件吧!
在此之前我们需要 npm install vue-loader --save-dev
,npm install vue-server-renderer --save-dev
- App.vue
<template>
<router-view class="view"></router-view>
</template>
- Component.vue (自定义组件)
- route.js
Vue = require('vue');
Router = require('vue-router');
Component = require('./Componet.vue');
Vue.use(Router);
module.exports = new Router({
mode: 'history',
fallback: false,
scrollBehavior: function() {
return { y: 0 };
},
routes: [
{
path: '/',
component: Component
}
]
});
- app.js
Vue = require('vue');
App = require('./App.vue');
router = require('./router');
module.exports = new Vue({
router: router,
render: function(h) {
return h(App);
}
});
- entry_client.js
app = require('./app');
router = require('./router');
router.onReady(function() {
return app.$mount('#app');
});
- entry_server.js
app = require('./app');
router = require('./router');
renderVueComponentToString = require('vue-server-renderer/basic');
app.$router.push(url);
renderVueComponentToString(app, function(err, html) {
if (err) {
throw new Error(err);
}
return print(html);
});
写到这里,如果你认为我们可以直接使用上面的 entry_server.js
和 entry_client.js
那就大错特错了。因为这里我们使用 require 来引入其他的模块,在服务端是无法直接通过 require 来引入前端的依赖的。因此我们需要通过 webpack/gulp 或其他打包工具来构建出两个完整的脚本(具体过程不再赘述,我这里使用的是 gulp)。
在服务端完成渲染
假设我们之前打包得到的两个脚本为:build/entry_server.js
、build/entry_client.js
。
- www/app/Services/SsrService.php
class SsrService
{
public static function render($url, $source_path)
{
$app_source = File::get(base_path($source_path));
$v8 = new \V8Js();
ob_start();
$js =
<<<EOT
var process = { env: { VUE_ENV: "server", NODE_ENV: "production" } };
this.global = { process: process };
var url = "$url";
EOT;
$v8->executeString($js);
$v8->executeString($app_source);
return ob_get_clean();
}
}
- www/app/Http/Controllers/xxxController.php
use App\Services\SsrService;
use Illuminate\Http\Request as Req;
public function getView(Req $request)
{
$view = View::make('index');
$source_path = '../build/entry_server.js';
$ssr = SsrService::render($request->path(), $source_path);
$view->with('ssr', $ssr);
return $view;
}
- www/resources/views/index.blade.php
<html lang="zh-cmn-Hans">
<head></head>
<body>
<div id="app">{!! $ssr !!}</div>
</body>
<script type="text/javascript" src="build/entry_client" charset="utf-8"></script>
</html>
至此,基于 Laravel 框架做 Vue.js 的 SSR 服务端渲染就完成了!
注意事项
-
一开始我搜到的 Vue SSR 都是基于 webpack 的,然而我们的项目是 gulp 打包的,因此感到很慌张,甚至准备将 gulp 与 webpack 结合起来使用。随着 SSR 的进行,我发现打包构建这一项仅仅是用于引入模块,webpack 也并没有进行其他非常重要的作用,因此用什么工具进行打包实际上是一样的。
-
为了更方便的将后端的变量传入到前端使用,我们的项目是在 php 的前端模板中将后端变量注入到前端的全局变量 window 中,然后在 Vue 中直接通过 window.xxx 直接使用的。然而,使用 V8Js 进行渲染的一个弊端是,它在运行时不具备前端的 window global 变量。此处我使用的解决方法时,渲染前将前端要使用的变量先进行一次定义:
// SsrSerive.php $js_vars = [ ['name' => 'test', 'value' => 'test'], ]; foreach ($js_vars as $js_var) { $name = $js_var['name']; $value = json_encode($js_var['value']); $var = <<<EOT var jsVars_$name = JSON.parse(JSON.stringify($value)); EOT; $v8->executeString($var); }
然后在 Vue 组件中:
data: function() { return test = if typeof jsVars_test !== "undefined" ? jsVars_test : window.test; }
-
在 Vue 组件中直接定
<style></style>
的方式会导致报错,只能在index.blade.php
中引入样式文件 -
由于 Vue 本身实现的内部逻辑,使用 v-if、v-for 等指令时可能会调用 setTimeout,而该方法是属于 window 的,因此会产生报错。这里我暂时没有什么好的解决方法,只能将某些元素设置成 Vue 挂载完成后再渲染:
<template> <div v-if="isMounted"></div> </template> <script> mounted: function() { this.isMounted = true; } </script>