关于SSR
什么是SSR
可以将同一个组件渲染为服务器端的 HTML
字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序 简单点说:就是将页面在服务端渲染 完成后在客户端直接显示。无需等待所有的 JavaScript
都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。
为什么要用SSR
更好的 SEO
,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。 更快的内容到达时间 (time-to-content
),特别是对于缓慢的网络情况或运行缓慢的设备。
SSR原理
1)所有的文件都有一个公共的入口文件app.js
2)进入ServerEntry
(服务端入口)与clientEntry
(客户端入口) 3)经过webpack打包生成ServerBundle
(供服务端SSR
使用,一个json
文件)与ClientBundle
(给浏览器用,和纯Vue
前端项目Bundle
类似) 4)当请求页面的时候,node
中ServerBundle
会生成html
界面,通过ClientBundle
混合到html
页面中
通用代码约束:
实际的渲染过程需要确定性,所以我们也将在服务器上“预取 ”数据 ("pre-fetching" data
) - 这意味着在我们开始渲染时,我们的应用程序就已经解析完成其状态。 避免在 beforeCreate
和 created
生命周期时产生全局副作用的代码,例如在其中使用 setInterval
设置 timer
通用代码不可接受特定平台的 API
,因此如果你的代码中,直接使用了像 window
或 document
,这种仅浏览器可用的全局变量,则会在 Node.js
中执行时抛出错误,反之也是如此
构建SSR应用程序
创建vue项目
vue create vue- ssr- demo
根据提示启动项目
修改router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue. use ( VueRouter)
export function createRouter ( ) {
return new VueRouter ( {
mode: 'history' ,
routes: [
{
path: '/' ,
name: 'Home' ,
component: ( ) => import ( '../views/Home.vue' )
} ,
{
path: '/about' ,
name: 'About' ,
component: ( ) => import ( '../views/About.vue' )
}
]
} )
}
修改store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue. use ( Vuex)
export function createStore ( ) {
return new Vuex. Store ( {
state: {
} ,
mutations: {
} ,
actions: {
} ,
modules: {
}
} )
}
修改main.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
Vue. config. productionTip = false
export function createApp ( ) {
const store = createStore ( ) ;
const router = createRouter ( ) ;
const app = new Vue ( {
store,
router,
render: h => h ( App)
} )
return { app, store, router}
}
服务端数据预取
import { createApp } from './main'
export default context => {
return new Promise ( ( resolve, reject) => {
const { app, router, store } = createApp ( )
router. push ( context. url)
router. onReady ( ( ) => {
const matchedComponents = router. getMatchedComponents ( )
if ( ! matchedComponents. length) {
return reject ( { code: 404 } )
}
Promise. all ( matchedComponents. map ( Component => {
if ( Component. asyncData) {
return Component. asyncData ( {
store,
route: router. currentRoute
} )
}
} ) ) . then ( ( ) => {
context. state = store. state
resolve ( app)
} ) . catch ( reject)
} , reject)
} )
}
客户端数据预取
import { createApp } from './main' ;
const { app, store, router} = createApp ( ) ;
router. onReady ( ( ) => {
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 ( )
}
Promise. all ( activated. map ( c => {
if ( c. asyncData) {
return c. asyncData ( { store, route: to } )
}
} ) ) . then ( ( ) => {
next ( )
} ) . catch ( next)
} )
app. $mount ( '#app' )
} )
构建配置
const VueSSRServerPlugin = require ( 'vue-server-renderer/server-plugin' )
const VueSSRClientPlugin = require ( 'vue-server-renderer/client-plugin' )
const nodeExternals = require ( 'webpack-node-externals' )
const merge = require ( 'lodash.merge' )
const TARGET_NODE = process. env. WEBPACK_TARGET === 'node'
const target = TARGET_NODE ? 'server' : 'client'
module. exports = {
css: {
extract: false
} ,
configureWebpack: ( ) => ( {
entry: `./src/entry- ${ target} .js` ,
devtool: 'source-map' ,
target: TARGET_NODE ? 'node' : 'web' ,
node: TARGET_NODE ? undefined : false ,
output: {
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
} ,
externals: TARGET_NODE
? nodeExternals ( {
allowlist: [ /\.css$/ ]
} )
: undefined,
optimization: {
splitChunks: TARGET_NODE ? false : undefined
} ,
plugins: [ TARGET_NODE ? new VueSSRServerPlugin ( ) : new VueSSRClientPlugin ( ) ]
} ) ,
chainWebpack: config => {
config. module
. rule ( 'vue' )
. use ( 'vue-loader' )
. tap ( options => {
return merge ( options, {
optimizeSSR: false
} )
} )
}
}
webpack进行打包操作
"build:client" : "vue-cli-service build" ,
"build:server" : "cross-env WEBPACK_TARGET=node vue-cli-service build" ,
"build:win" : "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json"
执行build:win
, 生成的dist目录如下:
创建服务
const express = require ( 'express' ) ;
const fs = require ( 'fs' ) ;
const path = require ( 'path' ) ;
const { createBundleRenderer } = require ( 'vue-server-renderer' ) ;
const app = express ( ) ;
const serverBundle = require ( './dist/vue-ssr-server-bundle.json' ) ;
const clientManifest = require ( './dist/vue-ssr-client-manifest.json' ) ;
const template = fs. readFileSync ( path. resolve ( './src/index.template.html' ) , 'utf-8' ) ;
const render = createBundleRenderer ( serverBundle, {
runInNewContext: false ,
template,
clientManifest
} ) ;
app. use ( express. static ( './dist' , { index: false } ) )
app. get ( '*' , ( req, res) => {
const context = { url: req. url }
render. renderToString ( context, ( err, html) => {
console. log ( html)
res. end ( html)
} )
} )
const port = 3003 ;
app. listen ( port, function ( ) {
console. log ( `server started at localhost: ${ port} ` ) ;
} ) ;
<!DOCTYPE html>
< html lang = " en" >
< head>
< meta charset = " utf-8" >
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" >
< meta name = " viewport" content = " width=device-width,initial-scale=1.0" >
< title> </ title>
</ head>
< body>
</ body>
</ html>
运行node server.js