基于 PhantomJS + Node + Express + VueJS 1.x 的服务端渲染实践

图片描述

项目地址:https://github.com/jrainlau/v...

随着Vue 2.0的发布,服务端渲染一度成为了它的热卖点。在此之前,单页应用的首屏加载时长和SEO的问题,一直困扰着开发者们,也在一定程度上制约着前端框架的使用场景。React提出的服务端渲染方案,较好得解决了上述两个痛点,受到了开发者的青睐,但也因此多了一个抨击Vue的理由——Vue没有服务端渲染。为了解决这个问题,Vue的社区里也贡献了一个方案,名曰VueServer。然而这货并非单纯的服务端渲染方案,而是相当于另外一个一个服务端的Vue,看看它的readme就知道了:

VueServer.js is designed for static HTML rendering. It has no real reactivity.
Also, the module is not running original Vue.js on server. It has its own implementation.
It means VueServer.js is just trying to perfectly reproduce the same result as Vue.js does.

所以有没有一种通用的解决方法,既能够让我们使用原生的Vue 1.x,又能愉快地进行服务端渲染呢?下面请听我细细道来……

服务端渲染(SSR)

在文章开始之前,我们有必要先了解一下什么是服务端渲染,以及为什么需要服务端渲染(知道的同学可以跳过)。
服务端渲染(Sever Side Render,简称SSR),听起来高大上,其实原理就是我们最常见的“服务器直接吐出页面”。我们知道,传统的网站都是后端通过拼接、模版填充等方式,把数据与html结合,再一起发送到客户端的。这个把数据与html结合的过程就是服务端渲染。

服务端渲染的好处,首先是首屏加载时间。因为后端发送出来的html是完整的带有数据的html,所以浏览器直接拿来就可以用了。与之相反的,以Vue 1.x开发的单页应用为例,服务端发送过来的html只是一个空的模板,浏览器根据js异步地从后端请求数据,再渲染到html中。一个大型单页应用的js往往很大,异步请求的数量也很多,直接导致的结果就是首屏加载时间长,在网速不好的情况下,白屏或loading的漫长等待过程对于用户体验来说真的很不友好。

另外一点,一般的搜索引擎爬虫由于无法执行html里面的js代码(我大Google除外),所以对于单页应用,爬虫所获取到的仅仅是空的html,因此需要做SEO的网站极少采用单页应用的方案。我们可以看看例子——

首先我们来写一个通过js生成内容的html文件:

<!-- SPA.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SPA-DEMO</title>
</head>
<body>
    <script>
        var div = document.createElement('div')
        div.innerHTML = 'Hello World!'
        document.body.appendChild(div)
    </script>
</body>
</html>

浏览器打开,输出“Hello World!”,很好没有问题。

接下来我们来写一个小爬虫:

'use strict'
const superagent = require('superagent')
const cheerio = require('cheerio')

var theUrl = 'http://localhost:3000/spa.html'

const spider = (link) => {
  let promise =  new Promise( (resolve, reject) => {
    superagent.get(link)
      .end((err, res) => {
        if (err) return console.log(err)
        let $ = cheerio.load(res.text)
        console.log($('html').html())
        resolve($)
      })
  })
  return promise
}

spider(theUrl)

运行,输出结果如下:

图片描述

可以看到,在<body></body>标签之内并没有生成对应的div,爬虫无法解析页面当中的js代码。

PhantomJS

为了实现服务端渲染,我们的主角PhantomJS登场了。

PhantomJS is a headless WebKit scriptable with a JavaScript API. It has fast and native support for various web standards: DOM handling, CSS selector, JSON, Canvas, and SVG.

简单来说,PhantomJS封装了一个webkit内核,因此可以用它来解析js代码,除此以外它也有着其他非常实用的用法,具体使用方法可以到它的官网进行查看。由于PhantomJS是一个二进制文件,需要安装使用,比较麻烦,所以我找到了另外一个封装了PhantomJS的NodeJS模块——phantomjs-node

PhantomJS integration module for NodeJS

有了它,就可以结合node愉快地使用PhantomJS啦!

npm install phantom --save

新建一个phantom-demo.js文件,写入如下内容:

var phantom = require('phantom');

var sitepage = null;
var phInstance = null;
phantom.create()
    .then(instance => {
        phInstance = instance;
        return instance.createPage();
    })
    .then(page => {
        sitepage = page;
        return page.open('http://localhost:3000/spa.html');
    })
    .then(status => {
        console.log(status);
        return sitepage.property('content');
    })
    .then(content => {
        console.log(content);
        sitepage.close();
        phInstance.exit();
    })
    .catch(error => {
        console.log(error);
        phInstance.exit();
    });

你会在控制台看到完整的http://localhost:3000/spa.html的内容<div>Hello World!</div>

图片描述

结合Express对Vue 1.x项目进行服务端渲染。

接下来开始实战了。首先我们要建立一个Vue 1.x的项目,在这里使用vue-cli生成:

npm install vue-cli -g

vue init webpack vue-ssr

在生成的项目中执行下列代码:

npm install

npm run build

可以看到在根目录下生成了一个\dist目录,里面就是构建好的Vue 1.x的项目:

 |__ index.html
  |__ static
    |__ css
      |__ app.b5a0280c4465a06f7978ec4d12a0e364.css
      |__ app.b5a0280c4465a06f7978ec4d12a0e364.css.map
    |__ js
      |__ app.efe50318ee82ab81606b.js
      |__ app.efe50318ee82ab81606b.js.map
      |__ manifest.e2e455c7f6523a9f4859.js
      |__ manifest.e2e455c7f6523a9f4859.js.map
      |__ vendor.13a0cfff63c57c979bbc.js
      |__ vendor.13a0cfff63c57c979bbc.js.map

接下来我们随便找个地方建立Express项目:

express Node-SSR -e

cd Node-SSR && npm install

npm install phantom --save

然后,我们把之前\dist目录下的\static\css\static\js中的全部代码,分别复制粘贴到刚刚生成的Express项目的\public\stylesheets\public\javascripts文件夹当中(注意,一定要包括所有*.map文件),同时把\dist目录下的index.html改名为vue-index.ejs,放置到Express项目的\view文件夹当中,改写一下,把里面所有的引用路径改为以/stylesheets//javascripts/开头。

接下来,我们打开Express项目中的\routes\index.js文件,改写为如下内容:

const express = require('express')
const router = express.Router()

const phantom = require('phantom')


/* GET home page. */

router.get('/render-vue', (req, res, next) => {
    res.render('vue-index')
})

router.get('/vue', (req, res, next) => {
    let sitepage = null
    let phInstance = null
    let response = res
    phantom.create()
        .then(instance => {
            phInstance = instance
            return instance.createPage()
        })
        .then(page => {
            sitepage = page
            return page.open('http://localhost:3000/render-vue')
        })
        .then(status => {
            console.log('status is: ' + status)
            return sitepage.property('content')
        })
        .then(content => {
            // console.log(content)
            response.send(content)
            sitepage.close()
            phInstance.exit()
        })
        .catch(error => {
            console.log(error)
            phInstance.exit()
        })
})

module.exports = router

现在我们用之前的爬虫爬取http://localhost:3000/render-vue的内容,其结果如下:

图片描述

可以看到是一些未被执行的js。

然后我们爬取一下http://localhost:3000/vue,看看结果是什么:

图片描述

满满的内容。

我们也可以在浏览器打开上面两个地址,虽然结果都是如下图,但是通过开发者工具的Network选项,可以看到所请求的html内容是不同的。

图片描述

至此,基于PhantomJS + Node + Express + VueJS 1.x的服务端渲染实践就告一段落了。

优化

由于PhantomJS打开页面并解析当中的js代码也需要一定时间,我们不应该在用户每次请求的时候都重新执行一次服务端渲染,而是应该让服务端把PhantomJS渲染的结果缓存起来,这样用户的每次请求只需要返回缓存的结果即可,大大减少服务器压力并节省时间。

后记

本文仅作抛砖引玉学习之用,并未进行深入的研究。同时此文章所研究的方法不仅仅适用于Vue的项目,理论上任何构建过后的单页应用项目都可以使用。如果读者发现文章有任何错漏烦请指点一二,感激不尽。若有更好的服务端渲染的方法,也欢迎和我分享。

感谢你的阅读。我是Jrain,欢迎关注我的专栏,将不定期分享自己的学习体验,开发心得,搬运墙外的干货。下次见啦!


Jrain-前端玩具盆
记录一路以来的各种折腾。

Hiphop dancer,

12.9k 声望
11.6k 粉丝
0 条评论
推荐阅读
浅谈组件库和 SVG 图标库的解耦维护思路
任何的前端组件库,无论是业界内有名的 tdesign,ant-design 还是 element-ui 也好,它们都有着自己的一系列图标。经过观察发现,这些图标都是在组件库发布的时候就已经基本稳定了,鲜有调整,所以可以一直存放于...

jrainlau1阅读 1.9k

Vue微信公众号开发踩坑记录
JS-SDK需要向服务端获取签名,且获取签名中需要的参数包括所在页面的url,但由于单页应用的路由特殊,其中涉及到iOS和android微信客户端浏览器内核的差异性导致的兼容问题

imwty132阅读 67.7k评论 81

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木75阅读 7.1k评论 16

从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木45阅读 8.6k评论 6

从零搭建 Node.js 企业级 Web 服务器(二):校验
校验就是对输入条件的约束,避免无效的输入引起异常。Web 系统的用户输入主要为编辑与提交各类表单,一方面校验要做在编辑表单字段与提交的时候,另一方面接收表单的接口也要做足校验行为,通过前后端共同控制输...

乌柏木35阅读 6.7k评论 10

从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
回顾 从零搭建 Node.js 企业级 Web 服务器(一):接口与分层,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,控制层与服务层实现了业务处理过程,模型层定义了业务实体并以 对象-关系...

乌柏木34阅读 5.1k评论 9

2022大前端总结和2023就业分析
我在年前给掘金平台分享了《2022年热点技术盘点》的前端热点,算是系统性的梳理了一下我自己对前端一整年的总结。年后,在知乎上看到《前端的就业行情怎么样?》,下面都是各种唱衰前端的论调,什么裁员,外包化...

i5ting27阅读 2.3k评论 4

封面图

Hiphop dancer,

12.9k 声望
11.6k 粉丝
宣传栏