视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
vue ssr 指南详读
2020-11-27 22:12:15 责编:小采
文档

本帖说明

该贴是对vue SSR Guide解读和补充,对于官网文档已有内容会以引用方式体现。由于官网demo在国内无法运行,该贴最后也提供了一个完整的可以运行的demo,帖子中提到的代码均是来自于该demo,供学习交流。

介绍

什么是服务器端渲染(SSR)?

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。

借助vue-server-renderer 将vue实例渲染为浏览器可以识别的html字符串。

为什么使用服务器端渲染(SSR)?

  1. 更好的 SEO
  2. 更快的内容到达时间 (白屏)

左侧为浏览器渲染,右侧为服务器渲染,从图中可以看出服务端渲染理论上明显少于浏览器渲染。

基本用法

安装

git clone https://github.com/s249359986/learnssr.git
cd learnssr 
npm install
npm run dev

代码结构

src
├── components
│ ├── Foo.vue
├── views
│ ├── Home.vue
├── App.vue
├── app.js 
├── client-entry.js
├── server-entry.js

代码详解

server.js 是服务端启动入口文件,接收客户端对页面的所有请求。

if (isProd) {
 /**
 生产环境,createRenderer将已经通过webpack打包好的server-bundle.js转化为一个可以操作的renderer对象。
 **/
 renderer = createRenderer(fs.readFileSync(resolve('./dist/server-bundle.js'), 'utf-8'))
 
 /**
 入口模板文件
 **/
 indexHTML = parseIndex(fs.readFileSync(resolve('./dist/index.html'), 'utf-8'))
} else {
 /**
 开发环境,createRenderer将已经通过webpack打包好的server-bundle.js转化为一个可以操作的renderer对象。
 **/
 require('./build/setup-dev-server')(app, {
 bundleUpdated: bundle => {
 renderer = createRenderer(bundle)
 },
 indexUpdated: index => {//index为入口文件及index.html
 indexHTML = parseIndex(index)
 }
 })
}

function createRenderer (bundle) {

 return require('vue-server-renderer').createBundleRenderer(bundle, {
 cache: require('lru-cache')({
 max: 1,//1000,
 maxAge: 2000//1000 * 60 * 15
 }),
 runInNewContext: false
 })
}
/*
读取入口文件
*/
function parseIndex (template) {
 const contentMarker = '<!-- APP -->'
 const i = template.indexOf(contentMarker)
 return {
 head: template.slice(0, i),
 tail: template.slice(i + contentMarker.length)
 }
}

const serve = (path, cache) => express.static(resolve(path), {
 maxAge: cache && isProd ? 60 * 60 * 24 * 30 : 0
})

app.use('/dist', serve('./dist'))

app.get('*', (req, res) => {
 if (!renderer) {
 return res.end('waiting for compilation... refresh in a moment.')
 }

 res.setHeader('Content-Type', 'text/html')
 res.setHeader('Server', serverInfo)

 var s = Date.now()
 const context = { url: req.url }
 /*
 渲染vue实例,context对象上下文
 */
 const renderStream = renderer.renderToStream(context)

 renderStream.once('data', () => {
 res.write(indexHTML.head)
 })

 renderStream.on('data', chunk => {
 res.write(chunk)
 })

 renderStream.on('end', () => {
 if (context.initialState) {
 res.write(
 `<script>window.__INITIAL_STATE__=${
 serialize(context.initialState, { isJSON: true })
 }</script>`
 )
 }
 res.end(indexHTML.tail)
 console.log(`whole request: ${Date.now() - s}ms`)
 })

 renderStream.on('error', err => {
 if (err && err.code === '404') {
 res.status(404).end('404 | Page Not Found')
 return
 }
 res.status(500).end('Internal Error 500')
 console.error(`error during render : ${req.url}`)
 console.error(err)
 })
})

const port = process.env.PORT || 8080
app.listen(port, () => {
 console.log(`server started at localhost:${port}`)
})

服务端vue实例入口文件,通过上下文对象获取请求的url,映射给对应的组件。

export default context => {
 const s = isDev && Date.now()
 router.push(context.url)
 const matchedComponents = router.getMatchedComponents()
 if (!matchedComponents.length) {
 return Promise.reject({ code: '404' })
 }
 return Promise.all(matchedComponents.map(component => {
/*
增加服务端数据预处理 start

*/
 if (component.asyncData) {
 return component.asyncData({
 store,
 route: router.currentRoute
 })
 }

 /*
 增加服务端数据预处理 end

 */
 })).then(() => {
 isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)
 context.initialState = store.state
 return app
 })
}

客户端vue实例入口文件.

/*

第一种方式
 */

Vue.mixin({
 beforeMount () {
 const { asyncData } = this.$options
 console.log('beforeMount',this.$store)
 if (asyncData) {

 // 将获取数据操作分配给 promise
 // 以便在组件中,我们可以在数据准备就绪后
 // 通过运行 `this.dataPromise.then(...)` 来执行其他任务
 this.dataPromise = asyncData({
 store: this.$store,
 route: this.$route
 })
 }
 },
 beforeRouteUpdate (to, from, next) {
 const { asyncData } = this.$options
 console.log('beforeRouteUpdate',this.$store)
 if (asyncData) {
 asyncData({
 store: this.$store,
 route: to
 }).then(next).catch(next)
 } else {
 next()
 }
}
})

/**
更新客户端store,与服务端store同步
**/
// store.replaceState(window.__INITIAL_STATE__)
if (window.__INITIAL_STATE__) {
 store.replaceState(window.__INITIAL_STATE__)
}
// actually mount to DOM


router.onReady(() => {
/**
挂载实例,客户端激活,所谓激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。注释掉app.$mount('#app') 可以清楚看到<div id="app" data-server-rendered="true"> 客户端通过data-server-rendered="true"知道该html是vue在服务端渲染的,并且不会在做多余的渲染。由于在服务端无法绑定事件,只有通过客户端vue处理。
**/
 app.$mount('#app')
})

下载本文
显示全文
专题