CSR是Client Side Render簡稱;頁面上的內容是咱們加載的js文件渲染出來的,js文件運行在瀏覽器上面,服務端只返回一個html模板。javascript
SSR是Server Side Render簡稱;頁面上的內容是經過服務端渲染生成的,瀏覽器直接顯示服務端返回的html就能夠了。css
本文以Vue.js 作爲演示框架來區分SSR和CSR。默認狀況下,Vue.js能夠在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操做 DOM。然而也能夠將同一個組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將這些靜態標記"激活"爲客戶端上徹底可交互的應用程序。html
服務器渲染的 Vue.js 應用程序也能夠被認爲是"同構"或"通用",由於應用程序的大部分代碼均可以在服務器和客戶端上運行。vue
附:vue-ssr官方文檔java
基本用法 | Vue SSR 指南從輸入頁面URL到頁面渲染完成大體流程爲:node
<canvas>
元素。根據上圖devtool時間軸的結果,雖然CSR配合預渲染方式(loading、骨架圖)能夠提早FP、FCP從而減小白屏問題,但沒法提早FMP;SSR將FMP提早至js加載前觸發,提早顯示網頁中的"主角元素"。SSR不只能夠減小白屏時間還能夠大幅減小首屏加載時間。webpack
第一步 利用express框架寫一個簡單node服務nginx
Express是基於Node.js平臺,快速、開放、極簡的 Web 開發框架git
/* 第一步 利用express框架寫一個簡單node服務 */
var express = require('express');
var app = express();
app.get('*', function(req, res){
res.send('hello world');
});
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
複製代碼
附:express文檔github
Express - 基於 Node.js 平臺的 web 應用開發框架第二步 利用vue-server-renderer提供的createRenderer將vue與node結合
renderer.renderToString(vm, context?, callback?): ?Promise<string>
將 Vue 實例渲染爲字符串。上下文對象 (context object) 可選。回調函數是典型的 Node.js 風格回調,其中第一個參數是可能拋出的錯誤,第二個參數是渲染完畢的字符串。
/* 第一步 利用express框架寫一個簡單node服務 第二步 利用vue-server-renderer提供的createRenderer將vue與node結合 */
var express = require('express');
var app = express();
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
app.get('*', function(req, res){
render(req,res)
});
function render(req, res) {
const app = new Vue({
data: {
url: req.url
},
template: `<div>req.url:{{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
} else {
res.end(`${html}`)
}
})
}
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
複製代碼
第三步 讀入index.template.html文件
建立 renderer 時提供一個頁面模板。多數時候,咱們會將頁面模板放在特有的文件中,例如index.template.html
<!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body> <!--vue-ssr-outlet--> </body> </html>複製代碼
<!--vue-ssr-outlet-->
註釋 -- 這裏將是應用程序 HTML 標記注入的地方。
/* 第一步 利用express框架寫一個簡單node服務 第二步 利用vue-server-renderer提供的createRenderer將vue與node結合 第三步 讀入index.template.html文件 */
var express = require('express');
var app = express();
const Vue = require('vue')
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync( resolve('./src/index.template.html'), 'utf-8')
})
app.get('*', function(req, res){
render(req,res)
});
function render(req ,res){
const app = new Vue({
data: {
url: req.url
},
template: `<div>req.url:{{ url }}</div>`
})
const context = {
title: 'ssr測試',
}
renderer.renderToString(app,context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}else{
res.end(`${html}`)
}
})
}
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
複製代碼
第四步 引入已經打包好的vue-ssr-server-bundle.json
vue-server-renderer
提供一個名爲 createBundleRenderer
的 API,用於處理此問題,經過使用 webpack 的自定義插件,server bundle 將生成爲可傳遞到 bundle renderer 的特殊 JSON 文件。所建立的 bundle renderer,用法和普通 renderer 相同,可是 bundle renderer 提供如下優勢:
devtool: 'source-map'
)*.vue
文件時):自動內聯在渲染過程當中用到的組件所需的CSS。更多細節請查看 CSS 章節。/* 第一步 利用express框架寫一個簡單node服務 第二步 利用vue-server-renderer提供的createRenderer將vue與node結合 第三步 讀入index.template.html文件 第四步 引入已經打包好的vue-ssr-server-bundle.json */
var express = require('express');
var app = express();
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const templatePath = resolve('./src/index.template.html')
const bundle = require('./dist/vue-ssr-server-bundle.json')
const { createBundleRenderer } = require('vue-server-renderer')
let renderer = createBundleRenderer(bundle, {
template: require('fs').readFileSync(templatePath, 'utf-8'),
})
app.get('*', function (req, res) {
render(req, res)
});
function render(req, res) {
const context = {
title: 'ssr測試',
url: req.url // 傳遞path,這個參數很重要
}
renderer.renderToString(context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
} else {
res.end(`${html}`)
}
})
}
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
複製代碼
第五步 將bundle換成webpack實時輸入的內存的bundle
webpack 默認使用普通文件系統來讀取文件並將文件寫入磁盤。可是,還可使用不一樣類型的文件系統(內存(memory), webDAV 等)來更改輸入或輸出行爲。爲了實現這一點,能夠改變inputFileSystem或outputFileSystem
調用watch方法會觸發 webpack 執行器,但以後會監聽變動(很像 CLI 命令:webpack --watch),一旦 webpack 檢測到文件變動,就會從新執行編譯。該方法返回一個Watching實例。
/* 第一步 利用express框架寫一個簡單node服務 第二步 利用vue-server-renderer提供的createRenderer將vue與node結合 第三步 讀入index.template.html文件 第四步 引入已經打包好的vue-ssr-server-bundle.json 第五步 將bundle換成webpack實時輸入的內存的bundle */
var express = require('express');
var app = express();
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const templatePath = resolve('./src/index.template.html')
//const bundle = require('./dist/vue-ssr-server-bundle.json')
const webpack = require('webpack')
const serverConfig = require('./build/webpack.server.config')
const MFS = require('memory-fs')
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(serverConfig.output.path, file), 'utf-8')
} catch (e) { }
}
const { createBundleRenderer } = require('vue-server-renderer')
let renderer;
app.get('*', function (req, res) {
render(req, res)
});
function render(req, res) {
const context = {
title: 'ssr測試',
url: req.url // 傳遞path,這個參數很重要
}
renderer.renderToString(context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
} else {
res.end(`${html}`)
}
})
}
const serverCompiler = webpack(serverConfig)
const mfs = new MFS()
serverCompiler.outputFileSystem = mfs //打包至內存中
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
let bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
renderer = createBundleRenderer(bundle, {
template: require('fs').readFileSync(templatePath, 'utf-8'),
})
})
const port = process.env.PORT || 8080
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
複製代碼
附:webpack在Node.js 中的API
Node.js API | webpack 中文網
通用配置(Base Config)
服務器配置 (Server Config)
服務器配置,是用於生成傳遞給 createBundleRenderer
的 server bundle。它應該是這樣的:
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
// 將 entry 指向應用程序的 server entry 文件
entry: '/path/to/entry-server.js',
// 這容許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動態導入(dynamic import),
// 而且還會在編譯 Vue 組件時,
// 告知 `vue-loader` 輸送面向服務器代碼(server-oriented code)。
target: 'node',
// 對 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 此處告知 server bundle 使用 Node 風格導出模塊(Node-style exports)
output: {
libraryTarget: 'commonjs2'
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化應用程序依賴模塊。可使服務器構建速度更快,
// 並生成較小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 須要處理的依賴模塊。
// 你能夠在這裏添加更多的文件類型。例如,未處理 *.vue 原始文件,
// 你還應該將修改 `global`(例如 polyfill)的依賴模塊列入白名單
whitelist: /\.css$/
}),
// 這是將服務器的整個輸出
// 構建爲單個 JSON 文件的插件。
// 默認文件名爲 `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin()
]
})
複製代碼
在生成 vue-ssr-server-bundle.json
以後,只需將文件路徑傳遞給 createBundleRenderer
:
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
// ……renderer 的其餘選項
})
複製代碼
客戶端配置 (Client Config)
除了 server bundle 以外,咱們還能夠生成客戶端構建清單 (client build manifest)。使用客戶端清單 (client manifest) 和服務器 bundle(server bundle),renderer 如今具備了服務器和客戶端的構建信息,所以它能夠自動推斷和注入資源預加載 / 數據預取指令(preload / prefetch directive),以及 css 連接 / script 標籤到所渲染的 HTML。
好處是雙重的:
html-webpack-plugin
來注入正確的資源 URL。<script>
標籤,以免客戶端的瀑布式請求 (waterfall request),以及改善可交互時間 (TTI - time-to-interactive)。要使用客戶端清單 (client manifest),客戶端配置 (client config) 將以下所示:
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
entry: '/path/to/entry-client.js',
plugins: [
// 重要信息:這將 webpack 運行時分離到一個引導 chunk 中,
// 以即可以在以後正確注入異步 chunk。
// 這也爲你的 應用程序/vendor 代碼提供了更好的緩存。
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
}),
// 此插件在輸出目錄中
// 生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
複製代碼
beforeCreate
和created
會在服務器端渲染 (SSR) 過程當中被調用。這就是說任何其餘生命週期鉤子函數中的代碼(例如beforeMount
或mounted
),只會在客戶端執行 2.通用代碼不可接受特定平臺的 API,所以若是你的代碼中,直接使用了像window
或document
,這種僅瀏覽器可用的全局變量,則會在 Node.js 中執行時拋出錯誤,反之也是如此(global)
解決方案:
通用 entry(app.js
)
app.js
是咱們應用程序的「通用 entry」。在純客戶端應用程序中,咱們將在此文件中建立根 Vue 實例,並直接掛載到 DOM。可是,對於服務器端渲染(SSR),責任轉移到純客戶端 entry 文件。app.js
簡單地使用 export 導出一個 createApp
函數
服務端數據預取 (Server entry)
在entry-server.js
中,咱們能夠經過路由得到與router.getMatchedComponents()
相匹配的組件,若是組件暴露出asyncData
,咱們就調用這個方法。而後咱們須要將解析完成的狀態,附加到渲染上下文(render context)中。
// entry-server.js
import { createApp } from './app'
export default context => {
// 由於有可能會是異步路由鉤子函數或組件,因此咱們將返回一個 Promise,
// 以便服務器可以等待全部的內容在渲染前,
// 就已經準備就緒。
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
// 設置服務器端 router 的位置
router.push(context.url)
// 等到 router 將可能的異步組件和鉤子函數解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()//當前路由匹配到組件
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 等到 router 將可能的異步組件和鉤子函數解析完
// 對全部匹配的路由組件調用 `asyncData()`
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 在全部預取鉤子(preFetch hook) resolve 後,
// 咱們的 store 如今已經填充入渲染應用程序所需的狀態。
// 當咱們將狀態附加到上下文,
// 而且 `template` 選項用於 renderer 時,
// 狀態將自動序列化爲 `window.__INITIAL_STATE__`,並注入 HTML。
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
複製代碼
客戶端數據預取 (Client entry)
router.onReady該方法把一個回調排隊,在路由完成初始導航時調用,這意味着它能夠解析全部的異步進入鉤子和路由初始化相關聯的異步組件。router.beforeResolve在導航被確認以前,同時在全部組件內守衛和異步路由組件被解析以後,解析守衛就被調用。
router.onReady(() => {
// 添加路由鉤子函數,用於處理 asyncData.
// 在初始路由 resolve 後執行,
// 以便咱們不會二次預取(double-fetch)已有的數據。
// 使用 `router.beforeResolve()`,以便確保全部異步組件都 resolve。
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to) //當前路由匹配的組件數組
const prevMatched = router.getMatchedComponents(from)
// 咱們只關心非預渲染的組件
// 因此咱們對比它們,找出兩個匹配列表的差別組件
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
// 這裏若是有加載指示器 (loading indicator),就觸發
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 中止加載指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
複製代碼
同一個組件不一樣參數切換路由時會觸發重用組件內部beforeRouteUpdate,經過全局mixin路由鉤子來監聽調用asyncData方法拉取數據進行客戶端渲染
Vue.mixin({
beforeRouteUpdate (to, from, next) {
const { asyncData } = this.$options
if (asyncData) {
asyncData({
store: this.$store,
route: to
}).then(next).catch(next)
} else {
next()
}
}
})
複製代碼
附:完整的導航解析流程
beforeEach
守衛。beforeRouteUpdate
守衛 (2.2+)。beforeEnter
。beforeRouteEnter
。beforeResolve
守衛 (2.5+)。afterEach
鉤子。beforeRouteEnter
守衛中傳給 next
的回調函數。進程管理pm2
以cluster模式(多實例多進程模式)啓動服務--watch參數,意味着當你的express應用代碼發生變化時,pm2會幫你重啓服務。
pm2 start server.js -i 4 --watch
或者pm2 -i 4 start npm -- run start --watch(同npm run start)
查詢全部服務 pm2 list
附:pm2的cluster模式官方介紹
PM2 - Cluster Modenginx反向代理
修改nginx.config文件,增長對應虛擬主機反向代理到node對應的服務端口
server {
listen 80;
server_name csyry.com;
location / {
proxy_pass http://127.0.0.1:8080;
index index.html index.htm;
}
}複製代碼
重啓nginx服務器: sudo nginx -s reload
附:nginx中文配置文檔
Nginx中文文檔修改DNS
正式環境經過域名服務商修改映射解析,本機測試修改/etc/hosts文件