最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 理解Vue SSR原理,搭建项目框架

    正文概述 掘金(Alaso)   2021-04-16   596

    一、为什么使用SSR ?

    在传统vue单页面应用中,页面的渲染都是由js完成,如下图所示,在服务端返回的html文件中,body中只有一个div标签和一个script标签,页面其余的dom结构都将由bundle.js生成,然后挂载到<div id="app"></div>上。这让搜索引擎爬虫抓取工具无法爬取页面的内容,如果 SEO 对你的站点很重要,则你可能需要服务器端渲染(SSR)解决此问题。

    理解Vue SSR原理,搭建项目框架 除了SEO,使用SSR还能加快首屏的呈现速度,因为服务端直接返回渲染好的页面html,不需要js就能看到完整渲染的页面。比起单页应用通常比较大的js文件,这部分代码量很小,所以首屏的到达时间会更快,白屏的时间更短。

    当然,SSR的使用也有一些局限性,首先,开发条件受限,在服务端渲染中,created和beforeCreate之外的生命周期钩子不可用。其次,更多的服务器端负载,在服务端中渲染完整的应用程序,显然会比仅仅提供静态文件的服务器更加占用 CPU 资源。此外,SSR在部署方面有更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序(SPA)不同,服务器渲染应用程序,需要处于Node.js的运行环境。所以涉及到SSR技术选型的时候,要综合考虑它的优缺点,看看是否有必要使用。

    二、基础功能实现

    SSR的本质就服务端返回渲染好的html文档。我们先在项目根目录启动一个服务器,然后返回一个html文档。这里我们使用koa作为服务端框架。

    //server.js
    const Koa = require('koa')
    const router = require('koa-router')()
    
    const koa = new Koa()
    koa.use(router.routes())
    
    router.get('/',(ctx)=>{
      ctx.body = `<!DOCTYPE html>      //要返回给客户端的html
      <html lang="en">
        <head><title>Vue SSR</title></head>
        <body>
          <div>This is a server render page</div>
        </body>
      </html>`
    })
    
    koa.listen(9000, () => {
      console.log('server is listening in 9000');
    })
    

    在命令行启动服务器: node server.js, 然后在浏览器访问http://localhost:9000/,服务端回返回的内容如下,浏览会根据这段html,渲染出页面。

    理解Vue SSR原理,搭建项目框架

    vue-server-renderer

    当然,要返回的html字符串可以是由vue模板生成的,这就需要用到vue-server-renderer,它会基于Vue实例生成html字符串,是Vue SSR的核心。在上面的server.js中稍作修改:

    const Koa = require('koa')
    const router = require('koa-router')()
    
    const koa = new Koa()
    koa.use(router.routes())
    
    const Vue = require('Vue')     //导入Vue,用于创建Vue实例
    const renderer = require('vue-server-renderer').createRenderer()  //创建一个 renderer 实例
    const app = new Vue({          //创建Vue实例
      template: `<div>{{msg}}</div>`,
      data(){
        return {
          msg: 'This is renderred by vue-server-renderer'
        }
      }
    })
    
    router.get('/',(ctx)=>{
      //调用renderer实例的renderToString方法,将Vue实例渲染成字符串
      //该方法接受两个参数,第一个是Vue实例,第二个是一个回调函数,在渲染完成后执行
      renderer.renderToString(app, (err, html) => {   //渲染得到的字符串作为回调函数的第二个参数传入
        ctx.body = `<!DOCTYPE html>
        <html lang="en">
          <head><title>Vue SSR</title></head>
          <body>
            ${html}    //将渲染得到的字符串拼接到要返回的结果中
          </body>
        </html>`
      })
    })
    
    koa.listen(9000, () => {
      console.log('server is listening in 9000');
    })
    

    重启服务器,再访问:

    理解Vue SSR原理,搭建项目框架

    这样,我们就完成了一个极其基础的Vue SSR。但是不太具备实操性,我们在实际项目开发时,是不可能这样写的,我们会模块化地搭建项目,然后通过打包工具打包成一个或多个js文件。

    正式一点的使用

    搭建一个模块化的vue项目

    我们模块化地搭建一个简单地vue项目,用vue-router管理路由。

    // 打包入口文件 src/main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    Vue.config.productionTip = false
    new Vue({
      el: '#app',
      router,
      render: h => h(App)
    })
    
    //  src/App.vue
    <template>
      <div id="app">
        <div id="nav">
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </div>
        <router-view/>
      </div>
    </template>
    <style lang="less">
    #app{
      margin: 0 auto;
      width: 700px;
      #nav{
        margin-bottom: 20px;
        text-align: center;
      }
    }
    </style>
    
    //  src/router/index.js
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'
    import About from '../views/About.vue'
    
    Vue.use(VueRouter)
    
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: Home
      },
      {
        path: '/about',
        name: 'About',
        component: About
      }
    ]
    
    export default new VueRouter({
      mode: 'history',
      routes
    })
    
    // src/views/Home.vue
    <template>
      <div class="home">
        <h1>This is home page</h1>
      </div>
    </template>
    
    // src/views/About.vue
    <template>
      <div class="about">
        <h1>This is an about page</h1>
      </div>
    </template>
    

    src/main.js作为打包入口文件,按照客户端单页面的方式打包,然后在浏览器打开,渲染结果如下:

    理解Vue SSR原理,搭建项目框架

    将项目改造成服务端渲染

    我们接下来就把上面这个demo改造成服务端渲染。

    主要的改造点:服务端渲染需要Vue实例,每一次客户端请求页面,服务端渲染都是用一个新的Vue实例,不同的用户不能访问同一个Vue实例。所以服务端需要一个生成Vue实例的工厂函数,每次渲染由这个工厂函数生成Vue实例。

    新建一个专门用于服务端渲染的入口文件entry.server.js:

    import { createApp } from './main'
    
    export default context => {  //生成Vue实例的工厂函数,
      return new Promise((resolve, reject) => {
        const app = createApp()
        const router = app.$router
    
        const { url } = context    //context包含服务端需要传递给Vue实例的一些数据,比如这里的路由
        const { fullPath } = router.resolve(url).route
    
        if(fullPath !== url){  //判断当前路由在Vue实例中是否存在
          return reject({
            url: fullPath
          })
        }
    
        router.push(url)      //设置Vue实例的当前路由
    
        router.onReady(() => {
          const matchedComponents = router.getMatchedComponents()  //判断当前路由是否有对应组件
          if(!matchedComponents.length){
            return reject({
              code: 404
            })
          }
          resolve(app)    //返回Vue实例
        }, reject)
      })
    }
    

    src/main.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({
        router,
        render: h => h(App)
      })
      return app
    }
    

    基于entry.server.js打包的webpack配置,也要作一些修改:

    target: 'node',
    entry: './src/entry.server.js',
    output: {
      path: path.join(__dirname, '../dist'),
      filename: 'bundle.server.js',
      libraryTarget: 'commonjs2'
    },
    

    然后,在服务端,我们就可以通过打包后的bundle.server.js进行服务端渲染了。

    //server.js作如下改变:
    const renderer = require('vue-server-renderer').createRenderer({   //基于模板创建一个 renderer 实例
      template: require('fs').readFileSync('./index.template.html', 'utf-8')
    })
    const app = require('./dist/bundle.server.js').default    //导入Vue实例工厂函数
    router.get('/(.*)', async (ctx, next) => {
      const context = {                   //获取路由,用于传递给Vue实例
        url: ctx.url
      }
      let htmlStr
      await app(context).then( res => {    //生成Vue实例,并传递给renderer实例生成字符串
        renderer.renderToString(res, context, (err,html)=>{
          if(!err){
            htmlStr = html
          }
        })
      })
      ctx.body = htmlStr
    });
    

    理解Vue SSR原理,搭建项目框架 可以看到,这里我们已经完成了服务端的渲染,页面dom结构出现在了由服务器返回的html文档中。

    客户端激活

    我们已经窥见了一点在实际项目使用SSR的曙光,但是这只是第一步。现在每次点击Home/About都会从服务端请求html资源,单页面的前端路由优势并没有发挥。接下来我们将加上一步客户端激活,让网页应用同时具备单页面的优势。这也是Vue SSR的官方流程。 理解Vue SSR原理,搭建项目框架

    所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。 激活原理参考官网。

    操作起来很简单,就是在返回的html页面中,加上client bundle,用于在客户端管理当前html。下面我们来打包生成它。

    新建一个entry.client.js

    import { createApp } from './main'
    
    const app = createApp()
    const router = app.$router
    
    router.onReady(() => {
      app.$mount('#app')      //服务端渲染默认会生成一个id为app的div
    })
    

    打包的webpack配置:

    entry: './src/entry.client.js',
    output: {
      path: path.join(__dirname, '../dist'),
      filename: 'bundle.client.js'
    },
    

    打包完成后,就是把bundle.client.js加到html中,之前我们是基于模板渲染:

    const renderer = require('vue-server-renderer').createRenderer({   //基于模板创建一个 renderer 实例
      template: require('fs').readFileSync('./index.template.html', 'utf-8')
    })
    

    所以只需要把bundle.client.js加到index.template.html就可以了。

    //index.template.html
    <!DOCTYPE html>
    <html lang="en">
      <head><title>Vue SSR</title></head>
      <body>
        <!--vue-ssr-outlet-->
      </body>
      <script src="bundle.client.js"></script>
    </html>
    

    重启服务,再访问,就可以看到,点击Home/About切换路由时,不会再从服务器请求html文档了。 理解Vue SSR原理,搭建项目框架

    三、请求数据

    在实际项目中,页面往往是由从接口请求的数据填充渲染出来的,下面我们将用请求的数据来渲染页面。为了方便(省事),就不另外写数据接口了,我们去请求豆瓣的电影排行前20的数据。

    具体思路,就是假如一个组件需要请求数据,当它是服务端渲染时,我们在服务端请求数据,当客户端以SPA的路由切换方式使用该组件时,也能在客户端发送ajax请求数据。

    我们将借助vuex来完成。 因为它将数据挂载在vue实例上,传递访问数据真的很方便。

    在服务器端请求数据

    回顾我们客户端渲染常规的请求数据的场景,在created或mounted钩子函数中发送ajax请求,请求成功后把返回数据写到实例的data中。SSR的请求数据不能这样,因为ajax请求是异步请求,请求发出去之后,数据还没返回,后端就已经渲染完了,ajax请求的数据无法填充到页面中。

    所以我们直接由服务端发送请求获取数据,也就是一个服务器向另一个服务器发送http请求,和客户端向服务器发送请求不同,这里我们使用axios,这两种它都支持。

    对于每个需要请求数据的组件,我们将在组件上暴露出一个自定义静态方法asyncData,由于此函数会在组件实例化之前调用,所以它无法访问 this。需要将 store 和路由信息作为参数传递进去。

    //Home.vue
    <template>
      <div class="movie-list">
        <div v-for="(item, index) in list" class="movie">
          <img class="cover" :src="item.cover">
          <p>
            <span class="title">{{item.title}}</span>
            <span class="rate">{{item.rate}}</span>
          </p>
        </div>
      </div>
    </template>
    
    <script>
      export default {
        name: 'MovieList',
        asyncData ({ store,route }) {    //自定义静态方法asyncData
          return store.dispatch('getTopList')     
        },
        
        /*****
        在这里,执行asyncData,就会调用getTopList方法去请求数据
        并将数据更新到vue实例的$store.state中
        actions: {            
          getTopList (store) {      
            return top20().then((res) => {
              store.commit('setTopList', res.data.subjects)
            })
          }
        }
        *****/
        
        computed: {
          list () {
            return this.$store.state.topList
          }
        },
        created () {
          if(!this.$store.state.topList){
            this.$store.dispatch('getTopList')
          }
        }
      }
    </script>
    

    entry.server.js 中,我们通过路由获得了与 router.getMatchedComponents() 相匹配的组件,如果组件暴露出 asyncData,就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文(render context)中。

    import { createApp } from './main'
    
    export default context => {
      return new Promise((resolve, reject) => {
        const app = createApp()
        const router = app.$router
        const store = app.$store
        ...
        router.onReady(() => {
          const matchedComponents = router.getMatchedComponents()
          if(!matchedComponents.length){
            return reject({
              code: 404
            })
          }
          Promise.all(matchedComponents.map(Component => {
            if (Component.asyncData) {
              //如果组件暴露出 asyncData,就调用这个方法
              //在本例中,就会去请求豆瓣数据,并把数据更新到app.$store.state
              return Component.asyncData({  
                store,
                route: router.currentRoute
              })
            }
          })).then(() => {
            context.state = store.state  //将app.$store.state赋值给渲染上下文context.state,后面同步数据到客户端的时候会用到。
            resolve(app)
          }).catch(reject)
        }, reject)
      })
      })
    }
    

    当在数据更新到app.$store.state后,服务端渲染的html中就有数据了。可是页面是空白的,并且发送了ajax请求。原因是当客户端激活其实经过了二次渲染,也就是当bundle.client.js加载并执行后,页面由bundle.client.js再次渲染,通常来说,渲染结果会跟之前一样,所以察觉不到。

    理解Vue SSR原理,搭建项目框架

    避免客户端重复请求数据

    这里是跨域,所以ajax请求没有成功。如果不是跨域,页面也能出现内容的,是由客户端发送ajax获得的数据渲染而得。但在服务端已经请求的数据,在客户端应该避免重复请求,怎样同步数据到客户端?

    当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中。而在客户端激活时,在挂载到应用程序之前,客户端的vm.$store 就应该获取到window.__INITIAL_STATE__ 状态。

    1.在server.js中,为renderer.renderToString方法添加第二个参数context,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中。

    router.get('/(.*)', async (ctx, next) => {
      const context = {
        url: ctx.url
      }
      let htmlStr
      await app(context).then( res => {
        renderer.renderToString(res, context, (err,html)=>{  //添加第二个参数context
          if(!err){
            htmlStr = html
          }
        })
      })
      ctx.body = htmlStr
    });
    

    理解Vue SSR原理,搭建项目框架

    1. 修改entry.client.js
    import { createApp } from './main'
    
    const app = createApp()
    const router = app.$router
    const store = app.$store
    
    if (window.__INITIAL_STATE__) {   //如果window.__INITIAL_STATE__有内容,就存到app.$store中
      store.replaceState(window.__INITIAL_STATE__)
    }
    
    router.onReady(() => {
      app.$mount('#app')
    })
    

    这样就避免了客户端重复请求数据,最终效果如下,可以看到客户端没有发送ajax请求了。

    理解Vue SSR原理,搭建项目框架

    这个项目搭建很简易,主要是整理一下Vue SSR的原理、流程,实际开发可以选择nuxt.js这种比较成熟的框架。

    项目地址: github.com/alasolala/v…

    参考资料

    解密Vue SSR

    手把手教你搭建SSR(vue/vue-cli + express)

    Vue SSR Guide


    起源地下载网 » 理解Vue SSR原理,搭建项目框架

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元