Vue-SSR系列(一)vue2.0+node+express+webpack實(shí)現(xiàn)vue-ssr服務(wù)端渲染的單頁(yè)應(yīng)用小demo

一麸拄、什么是服務(wù)器端渲染(SSR)派昧?

大致就是在服務(wù)端拼接好用戶請(qǐng)求的靜態(tài)頁(yè)面,直接返回給客戶端拢切,客戶端激活這些靜態(tài)頁(yè)面蒂萎,讓他們變成動(dòng)態(tài)的,并且能夠響應(yīng)后續(xù)的數(shù)據(jù)變化淮椰。

二五慈、為什么使用服務(wù)器端渲染(SSR)?

1主穗、更好的 SEO泻拦,由于搜索引擎爬蟲(chóng)抓取工具可以直接查看完全渲染的頁(yè)面。

2忽媒、產(chǎn)生更好的用戶體驗(yàn)争拐,更快的內(nèi)容到達(dá)時(shí)間(time-to-content),特別是對(duì)于緩慢的網(wǎng)絡(luò)情況或運(yùn)行緩慢的設(shè)備猾浦。無(wú)需等待所有的 JavaScript 都完成下載并執(zhí)行陆错,才顯示服務(wù)器渲染的標(biāo)記,所以你的用戶將會(huì)更快速地看到完整渲染的頁(yè)面金赦。

三音瓷、基本用法

首先vue-ssr 需要一個(gè)及其重要的插件,所以我們需要安裝一下
npm install vue vue-server-renderer --save

3.1 渲染一個(gè)簡(jiǎn)單的實(shí)例(官網(wǎng))

// 第 1 步:創(chuàng)建一個(gè) Vue 實(shí)例
const Vue = require('vue')
const app = new Vue({
  template: `<div>Hello World</div>`
})

// 第 2 步:創(chuàng)建一個(gè) renderer
const renderer = require('vue-server-renderer').createRenderer()

// 第 3 步:將 Vue 實(shí)例渲染為 HTML
renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html)
  // => <div data-server-rendered="true">Hello World</div>
})

// 在 2.5.0+夹抗,如果沒(méi)有傳入回調(diào)函數(shù)绳慎,則會(huì)返回 Promise:
renderer.renderToString(app).then(html => {
  console.log(html)
}).catch(err => {
  console.error(err)
})

3.2 與服務(wù)器集成的案例(官網(wǎng))
先安裝npm install express --save

const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>訪問(wèn)的 URL 是: {{ url }}</div>`
  })

  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})

server.listen(8080)

3.3 使用一個(gè)頁(yè)面模板

// index.html
<!DOCTYPE html>
<html lang="en">
  <head><title>Hello</title></head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

注意 注釋 -- 這里將是應(yīng)用程序 HTML 標(biāo)記注入的地方。

const renderer = createRenderer({
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})

renderer.renderToString(app, (err, html) => {
  console.log(html) // html 將是注入應(yīng)用程序內(nèi)容的完整頁(yè)面
})

當(dāng)然,以上內(nèi)容只是基礎(chǔ)中的基礎(chǔ)杏愤,最終我們使用靡砌,都是需要集成vue-cli,并結(jié)合node珊楼,express通殃,webpack等的配置,來(lái)生成一個(gè)更加靈活的vue-ssr應(yīng)用厕宗,花了將近4天時(shí)間画舌,一遍看api,一遍查資料已慢,終于搭建出了一個(gè)靜態(tài)的單頁(yè)應(yīng)用vue-ssr曲聂。趕緊趁熱乎分享一下,畢竟年紀(jì)大了佑惠,記性不好朋腋。


四、vue2.0+node+express+webpack實(shí)現(xiàn)vue-ssr單頁(yè)應(yīng)用

首先膜楷,我們需要安裝vue-cli旭咽,具體安裝,可以參考筆者的vue系列 第一篇文章把将,如果之前已經(jīng)全局安裝過(guò)轻专,那么只需要init一下即可。

屏幕快照 2018-08-28 下午9.55.30.png

剛開(kāi)始看不懂官網(wǎng)上的圖察蹲,后來(lái)搭建出來(lái)以后,發(fā)現(xiàn)官方的就是官方的催训,就跟北京地鐵圖一樣清晰洽议,借鑒一下別人的話就是,ssr 有兩個(gè)入口文件漫拭,client.js 和 server.js亚兄, 都包含了應(yīng)用代碼,webpack 通過(guò)兩個(gè)入口文件分別打包成給服務(wù)端用的 server bundle 和給客戶端用的 client bundle. 當(dāng)服務(wù)器接收到了來(lái)自客戶端的請(qǐng)求之后采驻,會(huì)創(chuàng)建一個(gè)渲染器 bundleRenderer审胚,這個(gè) bundleRenderer 會(huì)讀取上面生成的 server bundle 文件,并且執(zhí)行它的代碼礼旅, 然后發(fā)送一個(gè)生成好的 html 到瀏覽器膳叨,等到客戶端加載了 client bundle 之后,會(huì)和服務(wù)端生成的DOM 進(jìn)行 Hydration(判斷這個(gè)DOM 和自己即將生成的DOM 是否相同痘系,如果相同就將客戶端的vue實(shí)例掛載到這個(gè)DOM上菲嘴, 否則會(huì)提示警告)。

根據(jù)以上內(nèi)容,我們先來(lái)搭建一個(gè)簡(jiǎn)單的暫時(shí)沒(méi)有數(shù)據(jù)請(qǐng)求的vue-ssr龄坪。

如何搭建昭雌?

1、創(chuàng)建一個(gè)vue實(shí)例
2健田、配置路由烛卧,以及相應(yīng)的視圖組件
3、創(chuàng)建客戶端入口文件
4妓局、創(chuàng)建服務(wù)端入口文件
5总放、配置 webpack,分服務(wù)端打包配置和客戶端打包配置
6跟磨、創(chuàng)建服務(wù)器端的渲染器间聊,將vue實(shí)例渲染成html

創(chuàng)建vue實(shí)例:為每個(gè)請(qǐng)求創(chuàng)建一個(gè)新的根 Vue 實(shí)例。這與每個(gè)用戶在自己的瀏覽器中使用新應(yīng)用程序的實(shí)例類似抵拘。如果我們?cè)诙鄠€(gè)請(qǐng)求之間使用一個(gè)共享的實(shí)例哎榴,很容易導(dǎo)致交叉請(qǐng)求狀態(tài)污染,因此僵蛛,我們應(yīng)該暴露一個(gè)可以重復(fù)執(zhí)行的工廠函數(shù)尚蝌,為每個(gè)請(qǐng)求創(chuàng)建新的應(yīng)用程序?qū)嵗?/p>

//  app.js
import Vue from 'vue'
import App from './App.vue'
import createRouter  from './router'

Vue.config.productionTip = false

export function createApp () {
  const router = createRouter()
  const app = new Vue({
    // el: '#app',
    router,
    render: h => h(App)
  })
  return { app, router }
}

配置路由,以及相應(yīng)的視圖組件: 注意充尉,類似于 createApp飘言,我們也需要給每個(gè)請(qǐng)求一個(gè)新的 router 實(shí)例,所以文件導(dǎo)出一個(gè) createRouter 函數(shù).

// router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import home from '@/components/home'
import about from '@/components/about'

Vue.use(Router)

export default function createRouter() {
  return new Router({
    mode:'history',
    routes: [
      {
        path: '/',
        name: 'home',
        component: home
      },
      {
        path: '/about',
        name: 'about',
        component: about
      }
    ]
  })
}

創(chuàng)建客戶端入口文件:entry-client.js 客戶端 entry 只需創(chuàng)建應(yīng)用程序驼侠,并且將其掛載到 DOM 中

import { createApp } from './app'

// 客戶端特定引導(dǎo)邏輯……

const { app, router } = createApp()

// 這里假定 App.vue 模板中根元素具有 `id="app"`
router.onReady(() => {
  app.$mount('#app')
})

創(chuàng)建服務(wù)端入口文件:entry-server.js 中實(shí)現(xiàn)服務(wù)器端路由邏輯

import { createApp } from './app'

export default (context) => {
  // 因?yàn)橛锌赡軙?huì)是異步路由鉤子函數(shù)或組件姿鸿,所以我們將返回一個(gè) Promise,
  // 以便服務(wù)器能夠等待所有的內(nèi)容在渲染前倒源,
  // 就已經(jīng)準(zhǔn)備就緒苛预。
  return new Promise((resolve, reject) => {
    const { app, router } = createApp(context)

    const { url } = context
    const { fullPath } = router.resolve(url).route
    if (fullPath !== url) {
      return reject({ url: fullPath })
    }
    // 設(shè)置服務(wù)器端 router 的位置
    router.push(context.url)
    // 等到 router 將可能的異步組件和鉤子函數(shù)解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // Promise 應(yīng)該 resolve 應(yīng)用程序?qū)嵗员闼梢凿秩?      resolve(app)

    }, reject)
  })
}

創(chuàng)建好以后笋熬,我們的項(xiàng)目還不能啟動(dòng)热某,我們需要配置webpack,來(lái)生成服務(wù)端用的 server bundle 和給客戶端用的 client bundle胳螟。

客戶端的client bundle比較好創(chuàng)建昔馋,我們?cè)趘ue-cli下的build文件夾下的webpack.dev.conf.js中,引入const vueSSRClient = require('vue-server-renderer/client-plugin')糖耸,然后秘遏,在下方配置plugins的地方new vueSSRClient() 創(chuàng)建一個(gè) vueSSRClient()就好了,此時(shí)我們客戶端的bundle就已經(jīng)悄然生成了蔬捷。

服務(wù)端的server bundle垄提,就需要我們?cè)赽uild文件夾下新建一個(gè)webpack.server.conf.js榔袋,配置如下

const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const nodeExternals = require('webpack-node-externals')

module.exports = merge(baseWebpackConfig,{
    entry: './src/entry-server.js',
    devtool:'source-map',
    target:'node',
    output:{
        filename:'server-bundle.js',
        libraryTarget:'commonjs2'
    },
    externals: [nodeExternals({
        // do not externalize CSS files in case we need to import it from a dep
        whitelist: /\.css$/
    })],
    plugins:[
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
            'process.env.VUE_ENV': '"server"'
          }),

        new VueSSRServerPlugin()
    ]
})

現(xiàn)在我們已經(jīng)能通過(guò)webpack,生成了服務(wù)端用的 server bundle 和給客戶端用的 client bundle铡俐,但是我們需要?jiǎng)?chuàng)建一個(gè)渲染器 bundleRenderer凰兑,這個(gè) bundleRenderer 會(huì)讀取上面生成的 server bundle 文件,并且執(zhí)行它的代碼审丘, 然后發(fā)送一個(gè)生成好的 html 到瀏覽器吏够,所以我們?cè)赽uild文件夾下 ,新建一個(gè)dev-server.js 文件滩报,內(nèi)容如下(注意看注釋)

// dev-server.js

const webpack = require('webpack')
const baseWebpackConfig = require('./webpack.server.conf')
const clientConfig = require('./webpack.base.conf')
const fs = require('fs')
const path = require('path')
// 讀取內(nèi)存的文件
const Mfs = require('memory-fs')
const axios = require('axios')

module.exports = (cb) => {
    // 用來(lái)讀取內(nèi)存文件
    var mfs = new Mfs()
    const webpackComplier = webpack(baseWebpackConfig)
    webpackComplier.outputFileSystem = mfs
    const readFile = (fs, file) => {
        try {
          return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
        } catch (e) {}
    }


    webpackComplier.watch({},async (err,stats) => {
        if(err) {
            return console.log(err)
        }
        stats = stats.toJson();
        stats.errors.forEach(err => {console.log(err)});
        stats.warnings.forEach(err => {console.log(err)});
        // 獲取vue-server-renderer/server-plugin生成的服務(wù)端bundle的json文件 默認(rèn)名字為vue-ssr-server-bundle.json
        let serverBundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
        // 獲取vue-server-renderer/client-plugin生成的客戶端bundle的json文件锅知,默認(rèn)名字vue-ssr-client-manifest.json
        let clientBundle =  await axios.get('http://localhost:8080/vue-ssr-client-manifest.json')
        // 獲取模板文件 注意模板文件里面加上<!--vue-ssr-outlet-->
        let template = fs.readFileSync(path.join(__dirname,'..','index.html'), 'utf-8')
        cb(serverBundle, clientBundle, template)
    })
}

現(xiàn)在就差臨門(mén)一腳了,就是調(diào)用我們的dev-server脓钾,用bundleRenderer生成我們的html售睹,并返回到頁(yè)面上、那我們?cè)诟夸浵滦陆ㄒ粋€(gè)server.js ,整合所有資源可训,使其成為我們客戶端的入口

// server.js
const devServer = require('./build/dev-server.js')
const server = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')
// 在服務(wù)器處理函數(shù)中……
server.get('*', (req, res) => {
  const context = { url: req.url }
    res.status(200)
    devServer((serverBundle, clientBundle, template) => {
        let renderer = createBundleRenderer(serverBundle, {
            runInNewContext: false, // 推薦
            template, // (可選)頁(yè)面模板
            clientManifest:clientBundle.data // (可選)客戶端構(gòu)建 manifest
        })

        renderer.renderToString(context,(err,html) => {
            res.send(html)
        })

    })

})

const port = process.env.PORT || 5001;
server.listen(port, () => {
    console.log(`server started at localhost:${port}`)
    console.log('啟動(dòng)成功')
})

最終我們的項(xiàng)目目錄如下:


項(xiàng)目目錄

為了能在服務(wù)端啟動(dòng)我們的項(xiàng)目昌妹,在package.json的scripts中,加一行代碼"server": "node server.js"
現(xiàn)在握截,我們運(yùn)行npm run dev啟動(dòng)的就是客戶端渲染飞崖,運(yùn)行npm run server啟動(dòng)的就是服務(wù)端渲染,前端頁(yè)面呈現(xiàn)是一樣的谨胞,但是翻看頁(yè)面源碼的時(shí)候就會(huì)發(fā)現(xiàn)區(qū)別
npm run dev 默認(rèn)啟動(dòng)8080端口

8080.gif

查看客戶端源碼

屏幕快照 2018-08-28 下午10.39.02.png

npm run server 默認(rèn)啟動(dòng)5001端口

5001.gif

查看服務(wù)端源碼固歪,會(huì)有一個(gè)明顯的 data-server-rendered="true"

屏幕快照 2018-08-28 下午10.39.16.png

現(xiàn)在,我們的簡(jiǎn)單的案例就完成了胯努,那么最終我們還要集成vuex來(lái)動(dòng)態(tài)顯示數(shù)據(jù)牢裳,就放下集吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末叶沛,一起剝皮案震驚了整個(gè)濱河市贰健,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恬汁,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辜伟,死亡現(xiàn)場(chǎng)離奇詭異氓侧,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)导狡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)约巷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人旱捧,你說(shuō)我怎么就攤上這事独郎〔嚷螅” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵氓癌,是天一觀的道長(zhǎng)谓谦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)贪婉,這世上最難降的妖魔是什么反粥? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮疲迂,結(jié)果婚禮上才顿,老公的妹妹穿的比我還像新娘。我一直安慰自己尤蒿,他們只是感情好郑气,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著腰池,像睡著了一般尾组。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上巩螃,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天演怎,我揣著相機(jī)與錄音,去河邊找鬼避乏。 笑死爷耀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拍皮。 我是一名探鬼主播歹叮,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼铆帽!你這毒婦竟也來(lái)了咆耿?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤爹橱,失蹤者是張志新(化名)和其女友劉穎萨螺,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體愧驱,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡慰技,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了组砚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吻商。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖糟红,靈堂內(nèi)的尸體忽然破棺而出艾帐,到底是詐尸還是另有隱情乌叶,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布柒爸,位于F島的核電站准浴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏揍鸟。R本人自食惡果不足惜兄裂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阳藻。 院中可真熱鬧晰奖,春花似錦、人聲如沸腥泥。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蛔外。三九已至蛆楞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間夹厌,已是汗流浹背嬉挡。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工挽拔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肥卡,地道東北人霜医。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像或南,于是被迫代替她去往敵國(guó)和親孩等。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容