项目地址: https://github.com/jrainlau/vue1.x-ssr-demo
随着 Vue 2.0 的发布, 服务端渲染一度成为了它的热卖点. 在此之前, 单页应用的首屏加载时长和 SEO 的问题, 一直困扰着开发者们, 也在一定程度上制约着前端框架的使用场景. React 提出的服务端渲染方案, 较好得解决了上述两个痛点, 受到了开发者的青睐, 但也因此多了一个抨击 Vue 的理由 --Vue 没有服务端渲染. 为了解决这个问题, Vue 的社区里也贡献了一个方案, 名曰 VueServer https://github.com/ngsru/vue-server . 然而这货并非单纯的服务端渲染方案, 而是相当于另外一个一个服务端的 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 http://phantomjs.org/ 登场了.
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 的 Node.JS 模块 -- https://github.com/amir20/phantomjs-node
PhantomJS integration module for Node.JS
有了它, 就可以结合 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, 欢迎关注我的专栏, 将不定期分享自己的学习体验, 开发心得, 搬运墙外的干货. 下次见啦!
来源: https://juejin.im/post/5c238035e51d4570f14544ed