基於 PhantomJS + Node + Express + VueJS 1.x 的服務端渲染實踐

Jrain發表於2019-03-02

寫於 2016.08.23

基於 PhantomJS + Node + Express + VueJS 1.x 的服務端渲染實踐

專案地址:github.com/jrainlau/vu…

隨著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)
複製程式碼

執行,輸出結果如下:

基於 PhantomJS + Node + Express + VueJS 1.x 的服務端渲染實踐

可以看到,在<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>

基於 PhantomJS + Node + Express + VueJS 1.x 的服務端渲染實踐

結合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的內容,其結果如下:

基於 PhantomJS + Node + Express + VueJS 1.x 的服務端渲染實踐

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

然後我們爬取一下http://localhost:3000/vue,看看結果是什麼:

基於 PhantomJS + Node + Express + VueJS 1.x 的服務端渲染實踐

滿滿的內容。

我們也可以在瀏覽器開啟上面兩個地址,雖然結果都是如下圖,但是通過開發者工具的Network選項,可以看到所請求的html內容是不同的。

基於 PhantomJS + Node + Express + VueJS 1.x 的服務端渲染實踐

至此,基於PhantomJS + Node + Express + VueJS 1.x的服務端渲染實踐就告一段落了。

優化

由於PhantomJS開啟頁面並解析當中的js程式碼也需要一定時間,我們不應該在使用者每次請求的時候都重新執行一次服務端渲染,而是應該讓服務端把PhantomJS渲染的結果快取起來,這樣使用者的每次請求只需要返回快取的結果即可,大大減少伺服器壓力並節省時間。

後記

本文僅作拋磚引玉學習之用,並未進行深入的研究。同時此文章所研究的方法不僅僅適用於Vue的專案,理論上任何構建過後的單頁應用專案都可以使用。如果讀者發現文章有任何錯漏煩請指點一二,感激不盡。若有更好的服務端渲染的方法,也歡迎和我分享。

感謝你的閱讀。我是Jrain,歡迎關注我的專欄,將不定期分享自己的學習體驗,開發心得,搬運牆外的乾貨。下次見啦!

相關文章