上一篇文章中介紹瞭如何從零開始搭建一個簡單的 client-only webpack 配置。
接下來咱們在前面代碼的基礎上寫一個簡單的先後端同構的DEMO。javascript
當編寫純客戶端(client-only)代碼時,咱們習慣於每次在新的上下文中對代碼進行取值。可是,Node.js 服務器是一個長期運行的進程。當咱們的代碼進入該進程時,它將進行一次取值並留存在內存中。這意味着若是建立一個單例對象,它將在每一個傳入的請求之間共享。 css
爲了不狀態單例,改寫入口, Vue SSR 官方文檔介紹的比較詳細了,必定要去看一看。
建立對應的文件後,src 目錄是這樣的:html. ├── App.vue ├── app.js ├── assets │ └── logo.png ├── entry-client.js └── entry-server.js複製代碼
改寫 app.js 把裏面建立 Vue 實例的部分改寫一個工廠函數,用於建立返回 Vue 實例。前端
// app.js
import Vue from 'vue'
import App from './App.vue'
export function createApp () {
const app = new Vue({
render: h => h(App)
})
return app
}複製代碼
// entry-client.js
import { createApp } from './app.js'
const app = createApp()
app.$mount('#app')複製代碼
// entry-server.js
import { createApp } from './app.js'
export default context => {
const app = createApp()
return app
}複製代碼
由於服務器渲染的配置和客戶端的配置略有不一樣,但其中有不少共用的配置,官方建議咱們使用三個不一樣的配置文件:base、client、server, 經過 webpack-merge 插件來實現對 base 配置文件的覆蓋和擴展。vue
build 目錄下的文件目錄
.
├── webpack.base.conf.js
├── webpack.client.conf.js
└── webpack.server.conf.js複製代碼
再把以前 webpack.config.js 中的內容複製到 webpack.base.conf.js 中。在 webpack.server.conf.js 中加入 SSR 的 client 插件。java
const webpack = require('webpack')
const path = require('path')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
plugins: [
new VueSSRClientPlugin()
]
})複製代碼
客戶端的配置就完成了。server 端須要修改輸入和輸出的配置,還有 source-map 輸出的格式,module 中 引入的 css 文件不打包到 module 中,增長 SSR 的 server 端插件。node
const webpack = require('webpack')
const path = require('path')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
entry: './src/entry-server.js',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2' // 代碼中模塊的實現方式,Node.js 使用 commonjs2
},
target: 'node', // 指定代碼的運行環境是 node
devtool: '#source-map',
externals: nodeExternals({
whitelist: /\.css$/
}),
plugins: [
new VueSSRServerPlugin()
]
})複製代碼
而後在 package.json 中添加編譯的命令:webpack
"scripts": {
"test": "",
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot --config build/webpack.client.conf.js",
"server": "node server.js",
"build": "rimraf dist && npm run build:client && npm run build:server",
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.conf.js --progress --hide-modules",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules"
},複製代碼
運行 nom run build
在dist 目錄裏就會生成構建後的文件,而後把 index.html 修改成 indext.template.html 這個文件名隨便,不改也行。dist 目錄中有兩個不同的文件,vue-ssr-client-manifest.json 和 vue-ssr-server-bundle.json。具體的使用方法和實現方式,文檔寫的很清楚,先去 Bundle Renderer 指引 · GitBook 看看。git
而後在寫一個簡單 Node Server,我這裏使用 Koa,其餘的都是同樣。server.js 的內容以下:github
const Koa = require('koa')
const Vue = require('vue')
const { createBundleRenderer } = require('vue-server-renderer')
const path = require('path')
const fs = require('fs')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const app = new Koa()
const template = fs.readFileSync(path.resolve(__dirname, './index.template.html'), 'utf-8')
const renderer = createBundleRenderer(serverBundle, {
basedir: path.resolve(__dirname, './dist'),
runInNewContext: false,
template,
clientManifest
})
const renderToString = function (context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
if (err) reject(err)
resolve(html)
})
})
}
app.use(async ctx => {
console.log(ctx.req.url)
if (ctx.req.url === '/favicon.ico' || ctx.req.url === '/robots.txt') {
ctx.body = ''
return
}
// 簡單的靜態文件處理
if (ctx.req.url.indexOf('/dist/') > -1) {
const urlpath = ctx.req.url.split('?')[0].slice(1)
const filepath = path.resolve(__dirname, './', urlpath)
ctx.body = fs.readFileSync(filepath)
return
}
let html = ''
try {
html = await renderToString({})
} catch(err) {
ctx.throw(500, err)
}
ctx.body = html
})
app.listen(3000)
console.log('Server listening on http://localhost:3000.')複製代碼
運行 nom run server
就能夠看到服務器渲染出來的頁面了。
這只是一個簡單的靜態頁面,沒有 js 方法動態建立一些內容,咱們再添加一些前端方法,看看渲染出來的頁面中客戶端 js 的運行是否是能夠的。
修改 App.vue 文件:
<template>
<div class="demo" id="app">
<h1>Simple-webpack demo</h1>
<p>這是一個簡單的 Vue demo</p>
<img src="./assets/logo.png" alt="">
<p>測試一下 SSR</p>
<p v-for="(text, index) in textArr" :key="index">{{ text }}</p>
<button @click="clickHandler">新增一個行文字</button>
</div>
</template>
<script>
export default {
data () {
return {
textArr: []
}
},
methods: {
clickHandler () {
this.textArr.push(`${this.textArr.length + 1}. 這是新增的文字。`)
}
}
}
</script>複製代碼
而後再次構建整個工程,從新啓動服務器。
好比渲染一個新聞頁面,但願網頁的標題是頁面直接渲染出來的?應該怎麼作?Vue.js SSR 提供了方法,可以插入模板變量。只要在 index.template.html 中加入模板變量就能夠像其餘的後端模板同樣插入數據。首先修改一下 index.template.html 中,增長 title
變量,<title>SSR demo - {{ title }}</title>
。
而後在 server.js 中的 renderToString
方法中的第一個參數傳入 { title: '第一個 SSR Demo'}
。
最後再重啓一下後臺服務,以下圖,咱們的頁面標題變成了咱們定義的了。
若是還想更復雜的數據咱們只能用注入一個 window 全局變量了。這個時候咱們還沒辦法用組件的靜態方法,經過後臺服務去注入,由於咱們沒有用到router,不知道app中的組件是否是已經實例化,沒辦法去獲取組件裏的靜態方法。借鑑 SSR 官方中的 window.__INIT_STATE
的方式,先在 index.template.html 中 增長一個 script 標籤加入模板變量,而後在 server.js 中傳入數據,最後修改 App.vue 文件在 mounted
中判斷獲取這個變量,將變量賦值給組件的 data
屬性中,具體的代碼以下:
<!-- index.template.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>SSR demo - {{ title }}
</title>
<script></script>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>複製代碼
// server.js
html = await renderToString({
title: '第一個 SSR Demo',
injectData: 'window.__INIT_DATA__ = ' + JSON.stringify({
text: '這是服務器注入的數據。'
})
})複製代碼
<!-- App.vue -->
<template>
<div class="demo" id="app">
<h1>Simple-webpack demo</h1>
<p>這是一個簡單的 Vue demo</p>
<img src="./assets/logo.png" alt="">
<p>測試一下 SSR</p>
<p> {{ serverData.text }}</p>
<p v-for="(text, index) in textArr" :key="index">{{ text }}</p>
<button @click="clickHandler">新增一個行文字</button>
</div>
</template>
<script> export default { data () { return { textArr: [], serverData: '' } }, mounted () { this.serverData = window.__INIT_DATA__ }, methods: { clickHandler () { this.textArr.push(`${this.textArr.length + 1}. 這是新增的文字。`) } } } </script>複製代碼
從新編譯,重啓服務後,頁面上就會多一段文字了,以下圖所示:
Success!
全部的代碼都在這個上面 wheato/ssr-step-by-step