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

2,184 阅读6分钟
原文链接: segmentfault.com

项目地址: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-DEMO


    

浏览器打开,输出“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)

运行,输出结果如下:

可以看到,在标签之内并没有生成对应的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的内容

Hello World!

结合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,欢迎关注我的专栏,将不定期分享自己的学习体验,开发心得,搬运墙外的干货。下次见啦!