Vue.js 服务端渲染业务入门实践

笔者:威威(沪江前端开发工程师)
本文原创,转载请注明作者与出处。

背景

日前, 产品同学一样如既往笑哈哈的递来需求文档, 纵使内心万相似拒绝,
身体倒是可怜平实。 接了要求,好以要求不复杂, 简单构思 后控制就此Vue,
得心应手。 切好图, 挽起袖子准备撸代码的时光,
SEO同学不知何时已立到了幕后。

"听说你要用Vue?"
"恩..."
"SEO考虑了吗?整个SPA出来,网页的SEO咋办?"
"奥..."

易以前, 估计只能无可奈何的变个实现方式, 但是Vue 2.0时代之来到,
给你多了同等种可能。 你得对SEO工程师说:用Vue没问题!

也许,很多前端同学还出像样这样的涉, 为了SEO,只能放弃得心应手的框架。
SEO(Search Engine Optimization)顾名思义就是是平雨后春笋以增强
网站收录排名,吸引精准用户之方案。 这么看来,SEO确实是生重点的打算。
不过,好信息是,Vue2.0之通告为SEO提供了或, 这就是SSR(serve side
render)。

说由SSR,其实早以SPA (Single Page Application)
出现前,网页就是在服务端渲染的。服务器收到及客户端请求后,将数据及模板拼接成完整的页面响应到客户端。
客户端直接渲染, 此时用户期望浏览新的页面,就亟须再次是过程,
刷新页面.
这种体验在Web技术提高的即是几未可知叫受的,于是更加多的技巧方案涌现,力求
实现无页面刷新或者有刷新来上优良之互动体验。 比如Vue:

- 在客户端管理路由,用户切换路由,无需向服务器重新请求页面和静态资源,只需要使用 ajax 获取数据在客户端完成渲染,这样可以减少了很多不必要的网络传输,缩短了响应时间。
- 声明式渲染(告诉 vue 你要做什么,让它帮你做),把我们从烦人的DOM操作中解放出来,集中处理业务逻辑。
- 组件化视图,无论是功能组件还是UI组件都可以进行抽象,写一次到处用。
- 前后端并行开发,只需要与后端定好数据格式,前期用模拟数据,就可以与后端并行开发了。
- 对复杂项目的各个组件之间的数据传递 vue  - Vuex 状态管理模式

缺陷大家自然猜到了, 对,主要的一些不怕是匪便民SEO,或者说对SEO不友好。
来拘禁下两布置图;

SPA页面的源代码

Ajax 1

生图SSR页面的源代码

Ajax 2

地方两摆设图就是是采取了传统单页应用以及SSR的页面源代码,
第一张图中,很明朗页面的数都是通过Ajax异步获取,然而搜索引擎度娘家的爬虫看到这般空旷的源码并无见面毫发留下恋.
相反,通过劳动端渲染的页面,就发过多对此爬虫来提实惠之连接.
毕竟度娘一家独大,看来服务端渲染确实发生探讨的必需了。

Vue.js 的劳务端渲染是怎么回事?

先行押同样布置Vue官网的服务端渲染示意图

Ajax 3

自图上得看来,ssr 有个别个输入文件,client.js 和 server.js,
都蕴涵了应用代码,webpack 通过简单独输入文件分别于包改成受劳务端用的 server
bundle 和受客户端用的 client bundle.
当服务器收到至了来自客户端的乞求后,会创造一个渲染器
bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle
文件,并且实施其的代码, 然后发送一个别好之 html
到浏览器,等及客户端加载了 client bundle 之后,会以及劳务端生成的DOM 进行
Hydration(判断这个DOM 和融洽快要转移的DOM
是否同样,如果同样便拿客户端的vue实例挂载到是DOM上, 否则会唤醒警示)。

怎么落实?

清楚了Vue服务端渲染之盖流程,那怎么用代码来落实吗?

1. 创建一个 vue 实例
2. 配置路由,以及相应的视图组件
3. 使用 vuex 管理数据
4. 创建服务端入口文件
5. 创建客户端入口文件
6. 配置 webpack,分服务端打包配置和客户端打包配置
7. 创建服务器端的渲染器,将vue实例渲染成html
  • 率先我们来创造一个 vue 实例

    // app.js

    import Vue from 'vue';
    import router from './router';
    import store from './store';
    import App from './components/app';   
    
    let app = new Vue({
        template: '<app></app>',
        base: '/c/',
        components: {
            App
        },
        router,
        store
    });
    
    export {
        app,
        router,
        store
    }
    

以及我们以前写的vue实例差别不充分,但是咱不见面当此地拿app
mount到DOM上,因为是实例也会见在服务端去运转,这里一直用 app 暴露出来。

  • 配置 vue 路由

    import Vue from ‘vue’;
    import VueRouter from ‘vue-router’;

    import IndexView from ‘../views/indexView’;
    import ArticleItems from ‘../views/articleItems’;

    Vue.use(VueRouter);

    const router = new VueRouter({

      mode: 'history',
      base: '/c/',
      routes: [
          {
              path: '/:alias',
              component: IndexView
          }, {
              path: '/:alias/list',
              component: ArticleItems
          }
      ]
    

    });

顾这里的 base,在劳务端传递 path 给 vue-router 的时候要留心去丢前面的
‘/c/’,否则会配合不顶。

  • 始建视图组件,这里我们采用单文件组件,下面是 indexView.vue
    文件之实例代码

此处我们表露一个 fetchServerData
方法用来以劳动端渲染时举行多少的预加载,具体于哪调用,下面会说到。
beforeMount
是vue的生命周期钩子函数,当使用在客户端切换到之视图的当儿会以一定的时光去履行,用于在客户端获取数据。

  • 运用 vuex 管理数据,vue2.0 的劳动端官方推荐使用 STORE
    来治本数据,和1.0对比 api 有部分调动

    import Vue from ‘vue’;
    import Vuex from ‘vuex’;
    import axios from ‘axios’;

    Vue.use(Vuex);

    let apiHost = ‘http://localhost:3000’;

    const store = new Vuex.Store({

      state: {
          alias: '',
          ztData: {},
          courseListItems: [],
          articleItems: []
      },
      actions: {
          FETCH_ZT: ({ commit, dispatch, state }, { alias }) = {
              commit('SET_ALIAS', { alias });
              return axios.get(`${apiHost}/api/zt`)
                          .then(response => {
                              let data = response.data || {};
                              commit('SET_ZT_DATA', data);
                          })
          },
          FETCH_COURSE_ITEMS: ({ commit, dispatch, state }) => {
              return axios.get(`${apiHost}/api/course_items`).then(response => {
                  let data = response.data;
                  commit('SET_COURSE_ITEMS', data);
              });
          },
          FETCH_ARTICLE_ITEMS: ({ commit, dispatch, state }) => {
              return axios.get(`${apiHost}/api/article_items`)
                          .then(response => {
                              let data = response.data;
                              commit('SET_ARTICLE_ITEMS', data);
                          })
          }
      },
      mutations: {
          SET_COURSE_ITEMS: (state, data) => {
              state.courseListItems = data;
          },
          SET_ALIAS: (state, { alias }) => {
              state.alias = alias;
          },
          SET_ZT_DATA: (state, { ztData }) => {
              state.ztData = ztData;
          },
          SET_ARTICLE_ITEMS: (state, items) => {
              state.articleItems = items;
          }
      }
    

    })

    export default store;

state
使我们应用层的数额,相当给一个仓库,整个应用层的多少还留存这里,与非使vuex的vue应用来三三两两点不同:

-  Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
-  Vuex 不允许我们直接对 store 中的数据进行操作。改变 store 中的状态的唯一途径就是显式地提交(commit) mutations。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
action 响应在view上的用户输入导致的状态变化,并不直接操作数据,异步的逻辑都封装在这里执行,它最终的目的是提交 mutation 来操作数据。 mutation vuex 中修改store 数据的唯一方法,使用 commit 来提交。
  • 创立服务端的进口文件 server-entry.js

    // server-entry.js

    import {app, router, store} from './app';
    
    export default context => {
    
        const s = Date.now();
        router.push(context.url);
        const matchedComponents = router.getMatchedComponents();
        if(!matchedComponents) {
            return Promise.reject({ code: '404' });
        }
    
        return Promise.all(
            matchedComponents.map(component => {
                if(component.fetchServerData) {
                    return component.fetchServerData(store);
                }
            })
        ).then(() => {
            context.initialState = store.state;
            return app;
        })
    }
    

server.js 返回一个函数,该函数接受一个从劳动端传递过来的 context
的参数,将 vue 实例通过 promise 返回。 context 一般包含
当前页面的url,首先我们调用 vue-router 的 router.push(url)
切换到到相应的路由, 然后调整用 getMatchedComponents
方法返回对应要渲染的机件, 这里会见检查组件是否生 fetchServerData
方法,如果出就是会实施其。

脚这行代码用服务端获取到之数目挂载到 context
对象上,后面会拿这些多少直接发送到浏览器端与客户端的vue
实例进行数量(状态)同步。

context.initialState = store.state

创客户端入口文件 client-entry.js

// client-entry.js
    import { app, store } from './app';
    import './main.scss';
    store.replaceState(window.__INITIAL_STATE__);
    app.$mount('#app');

客户端入口文件特别简单,同步服务端发送过来的数,然后拿 vue
实例挂载到服务端渲染之 DOM 上。

  • 配置 webpack

    // webpack.server.config.js

    const base = require('./webpack.base.config'); // webpack 的通用配置
    module.exports = Object.assign({}, base, {
        target: 'node',
        entry: './src/server-entry.js',
        output: {
            filename: 'server-bundle.js',
            libraryTarget: 'commonjs2'
        },
        externals: Object.keys(require('../package.json').dependencies),
        plugins: [
            new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
            'process.env.VUE_ENV': '"server"'
            })
        ]
    })
    

在意这里添加了 target: ‘node’ 和 libraryTarget:
‘commonjs2’,然后输入文件改化我们的 server-entry.js, 客户端的 webpack
和原先一样,这里就是非贴了。

  • 各自打包服务端代码和客户端代码

因生少数只 webpack 配置文件,执行 webpack 时候就需指定 –config
参数来编译不同之 bundle。 我们好配备有限单 npm script

    "packclient": "webpack --config webpack.client.config.js",
    "packserver": "webpack --config webpack.server.config.js"

然后以指令执行运行

    npm run packclient
    npm run packserver

即使见面变动两个文本 client-bundle.js 和 server-bundle.js

  • 创造服务端渲染器

    // controller.js

    const serialize = require(‘serialize-javascript’);
    // 因为我们当vue-router 的配备内部用了 base: '/c',这里需要去丢请求path中之 ‘/c’
    let url = this.url.replace(/\/c/, ”);
    let context = { url: this.url };
    // 创建渲染器
    let bundleRenderer = createRenderer(fs.readFileSync(resolve(‘./dist/server-bundle.js’), ‘utf-8’))
    let html = yield new Promise((resolve, reject) => {

      // 将vue实例编译成一个字符串
      bundleRenderer.renderToString(
          context,   // 传递context 给 server-bundle.js 使用
          (err, html) => {
              if(err) {
                  console.error('server render error', err);
                  resolve('');
              }
              /**
               * 还记得在 server-entry.js 里面 `context.initialState = store.state` 这行代码么?
               * 这里就直接把数据发送到浏览器端啦
              **/
              html += `<script>
                          // 将服务器获取到的数据作为首屏数据发送到浏览器
                          window.__INITIAL_STATE__ = ${serialize(context.initialState, { isJSON: true })}
                      </script>`;
              resolve(html);
          }
      )
    

    })

    yield this.render(‘ssr’, html);

    // 创建渲染器函数
    function createRenderer(code) {

      return require('vue-server-renderer').createBundleRenderer(code);
    

    }

每当 node 的 views 模板文件中单待用地方的 html 输出就足以了

// ssr.html
    {% extends 'layout.html' %}
    {% block body %}
        {{ html | safe }}
    {% endblock %}

    <script src="/public/client.js"></script>

诸如此类,一个简短的劳务端渲染就得了了。

杀篇幅,详细的代码请参考
Github代码库:https://github.com/ikcamp/vue-ssr

小结

整个demo包含了:

  • vue + vue-router + vuex 的使用
  • 服务端数据获得
  • 客户端数据并同DOM hydration。

尚未关联:

  • 流式渲染
  • 零件缓存

对Vue的服务端渲染有双重怪一步之认识,实际于生产条件受到的施用或Ajax还欲考虑森素。

摘Vue的劳动端渲染方案,是情理之中的挑选,不是对新技巧之盲目追拍,而是全为用。
Vue
2.0之SSR方案只是供了扳平栽可能,多了相同种植选择,框架本身在服务开发者,根据不同的面貌选择不同的方案,才会事半功倍。

文章就表示个人观点,有无稳当地方烦请大家指出,共同进步!

Ajax 4

Ajax 5

iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。

>>
沪江Web前端上海团队招聘【Web前端架构师】,有意者简历及:zhouyao@hujiang.com
<<

相关文章