寫於 2016.08.23javascript
項目地址:github.com/jrainlau/vu…css
隨着Vue 2.0的發佈,服務端渲染一度成爲了它的熱賣點。在此以前,單頁應用的首屏加載時長和SEO的問題,一直困擾着開發者們,也在必定程度上制約着前端框架的使用場景。React提出的服務端渲染方案,較好得解決了上述兩個痛點,受到了開發者的青睞,但也所以多了一個抨擊Vue的理由——Vue沒有服務端渲染。爲了解決這個問題,Vue的社區裏也貢獻了一個方案,名曰VueServer。然而這貨並不是單純的服務端渲染方案,而是至關於另一個一個服務端的Vue,看看它的readme就知道了:html
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,又能愉快地進行服務端渲染呢?下面請聽我細細道來……vue
在文章開始以前,咱們有必要先了解一下什麼是服務端渲染,以及爲何須要服務端渲染(知道的同窗能夠跳過)。 服務端渲染(Sever Side Render,簡稱SSR),聽起來高大上,其實原理就是咱們最多見的「服務器直接吐出頁面」。咱們知道,傳統的網站都是後端經過拼接、模版填充等方式,把數據與html結合,再一塊兒發送到客戶端的。這個把數據與html結合的過程就是服務端渲染。java
服務端渲染的好處,首先是首屏加載時間。由於後端發送出來的html是完整的帶有數據的html,因此瀏覽器直接拿來就能夠用了。與之相反的,以Vue 1.x開發的單頁應用爲例,服務端發送過來的html只是一個空的模板,瀏覽器根據js異步地從後端請求數據,再渲染到html中。一個大型單頁應用的js每每很大,異步請求的數量也不少,直接致使的結果就是首屏加載時間長,在網速很差的狀況下,白屏或loading的漫長等待過程對於用戶體驗來講真的很不友好。node
另一點,通常的搜索引擎爬蟲因爲沒法執行html裏面的js代碼(我大Google除外),因此對於單頁應用,爬蟲所獲取到的僅僅是空的html,所以須要作SEO的網站極少採用單頁應用的方案。咱們能夠看看例子——react
首先咱們來寫一個經過js生成內容的html文件:webpack
<!-- 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!」,很好沒有問題。git
接下來咱們來寫一個小爬蟲:
'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 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>
接下來開始實戰了。首先咱們要創建一個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,歡迎關注個人專欄,將不按期分享本身的學習體驗,開發心得,搬運牆外的乾貨。下次見啦!