为何要作 SSR?
主要的目的是有利于搜索引擎的 SEO 抓取php
SPA场景下SEO的问题
SPA 应用加载的基本流程:浏览器端先加载一个空页面和 JavaScript 脚本,而后异步请求接口获取数据,渲染页面数据内容后展现给用户
问题:搜索引擎抓取页面解析该页面HTML中关键字、内容时 JavaScript 还没有调用执行,仅仅是一个空页面(body为空),影响搜索引擎收录页面的内容排行。
解决方案:使用服务端端数据渲染,在页面请求时将页面内容渲染到页面上输出(即,后台直出)
服务端如何进行渲染?
在这里咱们主要考虑的是 PHP 的环境下进行 SSR。咱们首先须要:html
一个能够执行 JavaScript 的引擎
一个能够在服务器上渲染应用的脚本
一个能够在客户端渲染和运行应用的脚本
在 PHP 中运行 JavaScript,个人选择是使用一个 PHP 的第三方扩展 V8Js(以前在其余的文章看到有人作过测试,它的效率比使用 Node.js 要高)。不过 V8Js 最大的缺点就是安装起来比较麻烦。前端
安装 V8Js
安装的大体思路及遇到的问题:vue
v8js 须要依赖 libv8 等其余扩展,可是这些扩展也很是的很差装,我直接下载了全部须要的 .so 文件并将它们复制到 /opt 中直接使用
pecl 直接 install 的 v8js 包有问题,我安装完后运行代码时提示找不到 class: v8js。因而我直接下载了 v8js-2.1.0.tgz 文件进行解压安装
在 laradock 中安装 V8Js
须要注意的是,不是 workspace 的 docker 中安装,而是在 php-fpm 中安装 V8Js,它才是真正运行的环境java
首先,在 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/
安装 v8jstar 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 的引擎,以前提到:咱们还须要一个能够在服务器上渲染应用的脚本和一个能够在客户端渲染和运行应用的脚本,如今咱们就在实现前端须要的几个文件吧!webpack
在此以前咱们须要 npm install vue-loader --save-dev,npm install vue-server-renderer --save-devnginx
App.vue
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)。c++
在服务端完成渲染
假设咱们以前打包获得的两个脚本为:build/entry_server.js、build/entry_client.js。git
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 =
<<
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
至此,基于 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 =
<<
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 组件中直接定 的方式会致使报错,只能在 index.blade.php 中引入样式文件
因为 Vue 自己实现的内部逻辑,使用 v-if、v-for 等指令时可能会调用 setTimeout,而该方法是属于 window 的,所以会产生报错。这里我暂时没有什么好的解决方法,只能将某些元素设置成 Vue 挂载完成后再渲染:
this.isMounted = true;
}