探索 PHP 与 Vue 通用直出模板方案

什么是“页面直出”

我们通常说的“页面直出”,其实就是服务端渲染(SSR, Server-Side Render)。

最初的 JS SPA 方案有个常见的问题,就是脚本没有加载执行完时,页面中没有内容。不仅影响访问体验,还不利于 SEO。

于是大家要么使用传统的 JSP、PHP、ASP.NET服务端页面模板,要么采用最新的 React/Vue 服务端渲染方案。

PHP 与 React/Vue SSR

目前动漫 H5 主站的前端架构没有使用 MVVM,存在大量累赘的 DOM 操作代码。

于是考虑切换到 React/Vue 方案,通过双向绑定和组件开发,简化累赘代码,提高可维护性和测试可能性。

但是为了优化 SEO 效果,H5主站需要做页面直出,而常用的 React/Vue 直出都是基于 node.js 服务端的,我们现有的服务端环境是 PHP,并不能直接使用。

方案1:php-v8js

首先找到的是 php-v8j,也就是实现一个 PHP 服务端的 JS V8 运行环境。

但根据 Vue 作者的回复,Vue 依赖于一些第三方模块,以及使用了node.js 的 stream 等功能,php-v8js提供的环境并不能实现 Vue 的服务端直出。

目前看来,至少使用 React 的方案是可行的。

可是即便如此,Github 的 react-php-v8js 仍然是“实验性”的项目,php-v8js的人气也不是很高,issue 中看来可能也会潜在不少问题。两者一起使用,风险较大。

并且 php-v8js 是在 PHP 内运行了个 v8 的沙盒运行环境,执行效率有待商榷。而 React/Vue 使用的虚拟 DOM 虽然在 v8 引擎内渲染速度不错,但相比传统字符串拼接的模板引擎仍然多了不少性能开销,React 很早实现了服务端渲染却没有铺开,便是出于对 node.js 服务端普及率和渲染性能的考虑。

方案2:mustache.php

通过搜索 php js template,发现 Mustache 其实已经实现了 Ruby, JavaScript, Python,Erlang, node.js, PHP, Perl, Objective-C, Java, C#/.NET, Android, C++,Go, Lua, ASP, Io, Dart, Haxe,Delphi, Swift, Bash 等各种平台和语言的支持。

其中 PHP 平台可以使用 mustache.php 作为模板引擎,进行服务端页面渲染mustache.php:

通过在服务器环境下加入 mustache.php,既可实现前端后台使用统一的 Mustach 语法模板渲染效果。

但是 Mustache 语法与Vue.js并不完全兼容(如循环、if 等写法),而 Mustache 本身只是单纯无逻辑的渲染模板,并不能满足我们 MVVM 改造的需求,所以是否使用 mustache.php 仍然有待考虑。

为什么需要直出?

回到开始的问题,为什么需要做页面直出呢?SEO 吗?

而为了 SEO 而需要直出的页面有哪些?

这些页面是否都是与用户个人状态无关,可以直接缓存的?

那这些页面使用 PHP 运行php-v8js 跑出一遍结果后,进行页面缓存,其它页面直接使用前端 React + ajax渲染数据。是否可行?

php-v8js 出现页面渲染意外的可能性多大?

提供异常时切换到普通方案是否可行?

思考解决方案

需要直出的页面一般与用户个人状态无关,可以在服务器端进行页面内容缓存,提高访问效率,利于 SEO。

为了服务端稳定,建议不建议使用重量级的服务端渲染库,尽可能减少现有系统的变动,避免运行中的系统出现异常。而出现异常时切换到普通方案的紧急方案也需要精力去实现和维护,成本略高。

换个思路,简化问题

以 Vue 为例,服务端渲染包括很多功能,涉及到 Vue 支持的各种v-*命令,需要对渲染后的页面中,各种数据状态、事件状态进行复杂的虚拟 DOM 关联处理,所以需要在Node.js 环境中借助完整的 SSR 模块来渲染。

但是我们日常真的需要实现这些效果吗?如果切换技术方案的代价这么大,能否折衷一下,找个简单的替代方案?

结合 mustache.php的思路,是否可以根据业务中直出的需求,使用一种简单的统一模板,让 Vue 和 php 都能支持渲染?

动漫业务中,需要直出的情况通常是输出漫画列表,将漫画信息展示出来,便于 SEO 和缓存。

对于这样的需求,我们可以在切图重构后,微调重构稿代码,将 Vue 挂载到页面内,展示出漫画列表:

 <ul class="comic-list" id="lst_comicList_1">
     <li v-cloak v-for="(id, item) in list"
         class="comic-item"
         @click="clickComicItem" 
         data-id="c_{{id}}">
         <span class="comic-title">{{item.title}}</span>
           <span class="comic-desc">{{item.short_desc != null ? item.short_desc : item.brief_intrd}}</span>
     </li>
 </ul> 
<script type="text/javascript">
 new Vue({
   el: '#lst_comicList_1',
   data: {
     list: {
       '531490': {title: '一人之下', short_desc: '身怀异术该何去何从', brief_intrd: '随着爷爷尸体被盗,神秘少女冯宝宝的造访,少年张楚岚的平静校园生活被彻底颠覆。急于解开爷爷和自身秘密的张楚岚和没有任何记忆“不死少女”冯宝宝开启了“异人”之旅……'},
       '537832': {title: '破晓世纪', brief_intrd: '一个是重度氪金的“非洲”少年林晓,一个是来历不明的神秘少女伊贝林。阴差阳错下两人签订契约,来到一个人类未曾探索过的宇宙。在这个荒诞而有趣的世界里,林晓是否能够摆脱他的“非洲人”属性,并且开辟属于自己的道路呢……'}
     }
   },
   method: {
     'clickComicItem': function (e) {
       var item = e.currentTarget,
           comicId = item.getAttribute('data-id');
       alert('Clicked comic: ' + comicId);
     }
   }
 });
</script>

复制

而目前使用 php 直出,页面模板代码需要稍作调整:

 <?php $list = array(
         "531490" => array("title" => "一人之下", "short_desc" => "身怀异术该何去何从", "brief_intrd" => "随着爷爷尸体被盗,神秘少女冯宝宝的造访,少年张楚岚的平静校园生活被彻底颠覆。急于解开爷爷和自身秘密的张楚岚和没有任何记忆“不死少女”冯宝宝开启了“异人”之旅……"),
         "537832" => array("title" => "破晓世纪", "brief_intrd" => "一个是重度氪金的“非洲”少年林晓,一个是来历不明的神秘少女伊贝林。阴差阳错下两人签订契约,来到一个人类未曾探索过的宇宙。
 在这个荒诞而有趣的世界里,林晓是否能够摆脱他的“非洲人”属性,并且开辟属于自己的道路呢……")
       ); ?>
 <ul class="comic-list" id="lst_comicList_1">
   <?php foreach ($list as $id => $item) { ?>
   <li class="comic-item"
       @click="clickComicItem"
       data-id="c_<?php echo $id; ?>">
     <span class="comic-title"><?php echo $item['title']; ?></span>
     <span class="comic-desc"><?php echo $item['short_desc'] != NULL ? $item['short_desc'] : $item['brief_intrd']; ?></span>
   </li>
   <?php }?>
 </ul>
<script type="text/javascript">
 new Vue({
   el: '#lst_comicList_1',
   method: {
     'clickComicItem': function (e) {
       var item = e.currentTarget,
           comicId = item.getAttribute('data-id');
       alert('Clicked comic: ' + comicId);
     }
   }
 });
</script>

复制

经过对比后,发现如果稍作限制,只使用 php 与 javascript 通用的语法的话,可以通过简单地替换就将上面的模板转化为下面的效果。

主要需要处理的地方在于 Vue 模板中的 v-for 和 Mustache 输出标记。

所以,我们做出以下约束:

  1. 只使用v-for 对列表数据进行渲染,并且必需指定(, )两个字段名(暂不支持v-ifv-elsev-else-if 的转换)。
  2. 只允许渲染简单的 DOM 结构(用于 SEO 或缓存),不渲染 Vue 组件。
  3. 只处理{{}} 标记的 Mustache 输出语法,将其简单替换为 php 的 echo 函数,各种 v-bindv-onv-model 等指令中参数不会被处理(数据状态不同步)。
  4. 因为是直接替换,php 中不支持的各种 js 运算仍然是不支持的(如:{{item[‘name’] || item[‘nick’]}}{{item[‘name’].join()}})。
  5. 需要拼接字符串时,请使用 {{id}}_{{item.text}}的形式,不要使用 {{id + item.text}} 运算(PHP中不能用+运算拼接字符串,会导致转换成整型后做加法)。
  6. 如果不需要对渲染出的 DOM 数据做绑定或更新,只需要做简单的事件控制。可以直接默认保留服务端渲染的 DOM,使用 vue 对象的事件监听器即可。
  7. 对于服务端渲染的 DOM,只能绑定监听器,无法在绑定属性内直接传参。如
  8. 需要改为
  9. 事件监听器内读取 e.currentTargetdata-id 属性,作为点击判断的依据(不过 Vue 不推荐在 HTML 属性内使用 Mustache,如果有更好的方案欢迎提供思路)。 .
  10. 如果要通过数据更新 DOM 或者做双向绑定时,需要给服务端渲染的元素增加 clear-before-render 属性。手动输出 json 数据到前端脚本,重新渲染 DOM 替代预渲染的占位 DOM(使用此属性的元素 v-if 会无效化)。 按照以上约束编写的前端模板,即可转换为 php 可用的模板。

于是根据这个思路,在团队日常使用的前端构建工具中,实现了这类脚本的转换构建任务。(日常使用的前端构建工具:Front Custos GUI)

在构建任务的帮助下,页面只需要编写如下的代码:


 <?php $list = array(
         "531490" => array("title" => "一人之下", "short_desc" => "身怀异术该何去何从", "brief_intrd" => "随着爷爷尸体被盗,神秘少女冯宝宝的造访,少年张楚岚的平静校园生活被彻底颠覆。急于解开爷爷和自身秘密的张楚岚和没有任何记忆“不死少女”冯宝宝开启了“异人”之旅……"),
         "537832" => array("title" => "破晓世纪", "brief_intrd" => "一个是重度氪金的“非洲”少年林晓,一个是来历不明的神秘少女伊贝林。阴差阳错下两人签订契约,来到一个人类未曾探索过的宇宙。
 在这个荒诞而有趣的世界里,林晓是否能够摆脱他的“非洲人”属性,并且开辟属于自己的道路呢……")
       ); ?>
 <ul class="comic-list" id="lst_comicList_1">
   <vue-php-ssr-template>
     <li v-cloak clear-before-render
         v-for="(id, item) in list"
         class="comic-item"
         @click="clickComicItem" 
         data-id="c_{{id}}">
         <span class="comic-title">{{item.title}}</span>
           <span class="comic-desc">{{item.short_desc != null ? item.short_desc : item.brief_intrd}}</span>
     </li>
   </vue-php-ssr-template>
 </ul> 
<script type="text/javascript">
 new Vue({
   el: '#lst_comicList_1',
   data: {
     list: <?php echo json_encode($list); ?>
   },
   method: {
     'clickComicItem': function (e) {
       var item = e.currentTarget,
           comicId = item.getAttribute('data-id');
       alert('Clicked comic: ' + comicId);
     }
   }
 });
</script>

复制

通过前端构建后,生成的代码如下:

 <?php $list = array(
         "531490" => array("title" => "一人之下", "short_desc" => "身怀异术该何去何从", "brief_intrd" => "随着爷爷尸体被盗,神秘少女冯宝宝的造访,少年张楚岚的平静校园生活被彻底颠覆。急于解开爷爷和自身秘密的张楚岚和没有任何记忆“不死少女”冯宝宝开启了“异人”之旅……"),
         "537832" => array("title" => "破晓世纪", "brief_intrd" => "一个是重度氪金的“非洲”少年林晓,一个是来历不明的神秘少女伊贝林。阴差阳错下两人签订契约,来到一个人类未曾探索过的宇宙。
 在这个荒诞而有趣的世界里,林晓是否能够摆脱他的“非洲人”属性,并且开辟属于自己的道路呢……")
       ); ?>
 <ul class="comic-list" id="lst_comicList_1">
   <!-- vue-php-ssr-template -->
     <?php foreach ($list as $id => $item) { ?>
       <li class="comic-item" 
           @click="clickComicItem" 
           data-id="c_<?php echo $id; ?>"
           v-if="false">
         <span class="comic-title"><?php echo $item['title']; ?></span>
           <span class="comic-desc"><?php echo $item['short_desc'] != NULL ? $item['short_desc'] : $item['brief_intrd']; ?></span>
       </li>
     <?php } ?>

     <li v-cloak clear-before-render
         v-for="(id, item) in list"
         class="comic-item"
         @click="clickComicItem" 
         data-id="c_{{id}}">
         <span class="comic-title">{{item.title}}</span>
         <span class="comic-desc">{{item.short_desc != null ? item.short_desc : item.brief_intrd}}</span>
     </li>
   <!-- vue-php-ssr-template -->
 </ul> 
<script type="text/javascript">
   new Vue({
     el: '#lst_comicList_1',
     data: {
       list: <?php echo json_encode($list); ?>
     },
     method: {
       'clickComicItem': function (e) {
         var item = e.currentTarget,
             comicId = item.getAttribute('data-id');
         alert('Clicked comic: ' + comicId);
       }
     }
   });
</script>

复制

由于两端差异,并不能真正实现前后端所有语法、状态的一致,只能说最后勉强达到了我们的目的:只需编写一次模板,php 可以根据转化后的模板在服务端渲染出对应 HTML;前端拿到数据后,可以根据原模板重新渲染或者追加数据。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
课程概述: 本课程是一个小型的vue周边技术+以php为基础的+微信接口开发的小型项目。本项目以微信扫码关注公众号实现网站自动登陆这一功能为载体,将会讲解如下主要核心知识点:前端你将学习到: 一、vue-cli4进行前端项目的创建 二、如何使用vuex进行登陆信息的管理与同步 三、如何使用axios进行接口请求的封装与拦截 四、在脚手架里使用Element-UI 五、学习组件化编程的思想 六、如何解决接口前后端分离引起的跨域问题以及在跨域下使用cookie凭证进行会话维护 后端你讲学习到: 一、如何申请微信公众号测试账号,如何进行相关参数的配置,如何进行微信相关接口的开发 二、如何使用微信接口开发,如获取临时二维码,获取用户基础信息,监听公众号关注以及扫描事件 三、如何使用redis对数据进行缓存 四、如何使用php原生代码进行接口的开发 五、如何使用laralvel 7.x 框架进行接口的开发 六、学习到laravel 中核心概念的使用方法,如什么是依赖注入,如何使用服务容器解决依赖注入、服务提供者、中间件的使用,如何处理请求数据,如何进行用户认证,以及如何使用Eloquent ORM进行数据库操作 七、开发过程中遇到的问题,如何进行排查 八、git远端仓库的建立与配置,如何在配置多仓库下的公钥,服务器端部署公钥的配置,如何进行代码部署,本地与服务器代码的开发实时同步 九、如何使用composer帮助我们进行第三方依赖包的安装 本课程的设计思路随笔: 从业务层面上来讲,扫码关注公众号,实现网站端自动登陆是一个非常实用的功能,可以为微信公众号引流。 技术层面上来说,使用前后端分离进行制作,可以将前端以及后端的知识都涵盖到。对于前端的路由,信息维护,脚手架的搭建,ui组件的使用,接口的请求与封装都能够讲解到。 对于后端,本课程对php原生代码以及工作中使用频率比较高的同时也很优雅的laravel框架都会进行讲解,分别予以代码的实现。让同学们能够看到原生开发与框架开发的区别,原生开发使得基础比较弱的同学能够慢慢上手,也知道此功能实现的核心要点,在进行原生代码开发后,再进行框架开发,会有个循序渐进的过程,同时在框架里面我们会降到主流框架都会用到的一些核心思想,比如依赖注入,服务容器,中间件等等,同时对于想学习laravel框架的同学,学习过这个案例后,再去看文档就知道该如何去看,如何去学了。 对于整个代码的管理与部署,我们也会引入git仓库进行项目代码管理,如何在服务器进行网站环境的搭建与代码部署等等实用技巧。 相信本课程会给大家带来十足的收获,大家加油。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栾还是恋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值