| 注册
请输入搜索内容

热门搜索

Java Linux MySQL PHP JavaScript Hibernate jQuery Nginx
keuo9813
8年前发布

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

   <h2><strong>背景</strong></h2>    <p>最近, 产品同学一如往常笑嘻嘻的递来需求文档, 纵使内心万般拒绝, 身体倒是很诚实。 接过需求,好在需求不复杂, 简单构思 后决定用Vue, 得心应手。 切好图, 挽起袖子准备撸代码的时候, SEO同学不知何时已经站到了背后。</p>    <p>"听说你要用Vue?"</p>    <p>"恩..."</p>    <p>"SEO考虑了吗?整个SPA出来,网页的SEO咋办?"</p>    <p>"奥..."</p>    <p>换以前, 估计只能无奈的换个实现方式, 但是Vue 2.0时代的到来, 给你多了一种可能。 你可以对SEO工程师说:用Vue没问题!</p>    <p>想必,很多前端同学都有类似这样的经历, 为了SEO,只能放弃得心应手的框架。 SEO(Search Engine Optimization)顾名思义就是一系列为了提高 网站收录排名,吸引精准用户的方案。 这么看来,SEO确实是有举足轻重的作用。 不过,好消息是,Vue2.0的发布为SEO提供了可能, 这就是SSR(serve side render)。</p>    <p>说起SSR,其实早在SPA (Single Page Application) 出现之前,网页就是在服务端渲染的。服务器接收到客户端请求后,将数据和模板拼接成完整的页面响应到客户端。 客户端直接渲染, 此时用户希望浏览新的页面,就必须重复这个过程, 刷新页面. 这种体验在Web技术发展的当下是几乎不能被接受的,于是越来越多的技术方案涌现,力求 实现无页面刷新或者局部刷新来达到优秀的交互体验。 比如Vue:</p>    <p>- 在客户端管理路由,用户切换路由,无需向服务器重新请求页面和静态资源,只需要使用 ajax 获取数据在客户端完成渲染,这样可以减少了很多不必要的网络传输,缩短了响应时间。</p>    <p>- 声明式渲染(告诉 vue 你要做什么,让它帮你做),把我们从烦人的DOM操作中解放出来,集中处理业务逻辑。</p>    <p>- 组件化视图,无论是功能组件还是UI组件都可以进行抽象,写一次到处用。</p>    <p>- 前后端并行开发,只需要与后端定好数据格式,前期用模拟数据,就可以与后端并行开发了。</p>    <p>- 对复杂项目的各个组件之间的数据传递 vue  - Vuex 状态管理模式</p>    <p>缺点大家自然猜到了, 对,主要的一点就是不利于SEO,或者说对SEO不友好。 来看下面两张图;</p>    <p>SPA页面的源代码</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/8a4b632d6d04ad21890c3f869c588a71.jpg"></p>    <p>下图SSR页面的源代码</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/2ae0f1f445ec3f55a97ee803686800a8.jpg"></p>    <p>上面两张图就是使用了传统单页应用和SSR的页面源代码, 第一张图中,很明显页面的数据都是通过Ajax异步获取,然而搜索引擎度娘家的爬虫看到这样空旷的源码并不会丝毫留恋. 相反,通过服务端渲染的页面,就有很多对于爬虫来讲有效的连接. 毕竟度娘一家独大,看来服务端渲染确实有探究的必要了。</p>    <p>vue 的服务端渲染是怎么回事?</p>    <p>先看一张Vue官网的服务端渲染示意图</p>    <p><img src="https://simg.open-open.com/show/588c42bee019f81be0beae263ce2d915.jpg"></p>    <p>从图上可以看出,ssr 有两个入口文件,client.js 和 server.js, 都包含了应用代码,webpack 通过两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle. 当服务器接收到了来自客户端的请求之后,会创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 之后,会和服务端生成的DOM 进行 Hydration(判断这个DOM 和自己即将生成的DOM 是否相同,如果相同就将客户端的vue实例挂载到这个DOM上, 否则会提示警告)。</p>    <h2>怎么实现?</h2>    <p>知道了Vue服务端渲染的大致流程,那怎么用代码来实现呢?</p>    <p>1. 创建一个 vue 实例</p>    <p>2. 配置路由,以及相应的视图组件</p>    <p>3. 使用 vuex 管理数据</p>    <p>4. 创建服务端入口文件</p>    <p>5. 创建客户端入口文件</p>    <p>6. 配置 webpack,分服务端打包配置和客户端打包配置</p>    <p>7. 创建服务器端的渲染器,将vue实例渲染成html</p>    <ul>     <li> <p>首先我们来创建一个 vue 实例</p> </li>    </ul>    <pre>  <code class="language-javascript">// 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      }</code></pre>    <p>和我们以前写的vue实例差别不大,但是我们不会在这里将app mount到DOM上,因为这个实例也会在服务端去运行,这里直接将 app 暴露出去。</p>    <ul>     <li> <p>配置 vue 路由</p> </li>    </ul>    <pre>  <code class="language-javascript">  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            }        ]    });</code></pre>    <p>注意这里的 base,在服务端传递 path 给 vue-router 的时候要注意去掉前面的 '/c/',否则会匹配不到。</p>    <ul>     <li> <p>创建视图组件,这里我们使用单文件组件,下面是 indexView.vue 文件的实例代码</p> </li>    </ul>    <pre>  <code class="language-javascript"><template>        <div class="content">            <course-cover :class-data="classData[0]"></course-cover>            <article-items :article-items="articleItems"></article-items>        </div>    </template>      <script>        import courseCover from '../components/courseCover.vue';        import articleItems from '../components/articleItems';          export default {            computed: {                classData() {                    return this.$store.state.courseListItems;                },                articleItems() {                    return this.$store.state.articleItems;                }            },            components: {                courseCover,                articleItems            },            // 服务端获取数据            fetchServerData ({ state, dispatch, commit }) {                let alias = state.route.params.alias;                  return Promise.all([                    dispatch('FETCH_ZT', { alias }),                    dispatch('FETCH_COURSE_ITEMS'),                    dispatch('FETCH_ARTICLE_ITEMS')                ])            },            // 客户端获取数据            beforeMount() {                return this.$store.dispatch('FETCH_COURSE_ITEMS');            }        }    </script></code></pre>    <p>这里我们暴露一个 fetchServerData 方法用来在服务端渲染时做数据的预加载,具体在哪调用,下面会讲到。 beforeMount 是vue的生命周期钩子函数,当应用在客户端切换到这个视图的时候会在特定的时候去执行,用于在客户端获取数据。</p>    <ul>     <li> <p>使用 vuex 管理数据,vue2.0 的服务端官方推荐使用 STORE 来管理数据,和1.0相比 api 有一些调整</p> </li>    </ul>    <pre>  <code class="language-javascript">  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;</code></pre>    <p>state 使我们应用层的数据,相当于一个仓库,整个应用层的数据都存在这里,与不使用vuex的vue应用有两点不同:</p>    <p>-  Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。</p>    <p>-  Vuex 不允许我们直接对 store 中的数据进行操作。改变 store 中的状态的唯一途径就是显式地提交(commit) mutations。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。</p>    <p>action 响应在view上的用户输入导致的状态变化,并不直接操作数据,异步的逻辑都封装在这里执行,它最终的目的是提交 mutation 来操作数据。 mutation vuex 中修改store 数据的唯一方法,使用 commit 来提交。</p>    <ul>     <li> <p>创建服务端的入口文件 server-entry.js</p> </li>    </ul>    <pre>  <code class="language-javascript">// 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;          })      }</code></pre>    <p>server.js 返回一个函数,该函数接受一个从服务端传递过来的 context 的参数,将 vue 实例通过 promise 返回。 context 一般包含 当前页面的url,首先我们调用 vue-router 的 router.push(url) 切换到到对应的路由, 然后调用 getMatchedComponents 方法返回对应要渲染的组件, 这里会检查组件是否有 fetchServerData 方法,如果有就会执行它。</p>    <p>下面这行代码将服务端获取到的数据挂载到 context 对象上,后面会把这些数据直接发送到浏览器端与客户端的vue 实例进行数据(状态)同步。</p>    <pre>  <code class="language-javascript">context.initialState = store.state`</code></pre>    <p>创建客户端入口文件 client-entry.js</p>    <pre>  <code class="language-javascript">// client-entry.js      import { app, store } from './app';      import './main.scss';      store.replaceState(window.__INITIAL_STATE__);      app.$mount('#app');</code></pre>    <p>客户端入口文件很简单,同步服务端发送过来的数据,然后把 vue 实例挂载到服务端渲染的 DOM 上。</p>    <ul>     <li> <p>配置 webpack</p> </li>    </ul>    <pre>  <code class="language-javascript">// 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"'              })          ]      })</code></pre>    <p>注意这里添加了 target: 'node' 和 libraryTarget: 'commonjs2',然后入口文件改成我们的 server-entry.js, 客户端的 webpack 和以前一样,这里就不贴了。</p>    <ul>     <li> <p>分别打包服务端代码和客户端代码</p> </li>    </ul>    <p>因为有两个 webpack 配置文件,执行 webpack 时候就需要指定 --config 参数来编译不同的 bundle。 我们可以配置两个 npm script</p>    <pre>  <code class="language-javascript">   "packclient": "webpack --config webpack.client.config.js",      "packserver": "webpack --config webpack.server.config.js"</code></pre>    <p>然后在命令行运行</p>    <pre>  <code class="language-javascript">   npm run packclient      npm run packserver</code></pre>    <p>就会生成两个文件 client-bundle.js 和 server-bundle.js</p>    <ul>     <li> <p>创建服务端渲染器</p> </li>    </ul>    <pre>  <code class="language-javascript">// 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);    }</code></pre>    <p>在 node 的 views 模板文件中只需要将上面的 html 输出就可以了</p>    <pre>  <code class="language-javascript">// ssr.html      {% extends 'layout.html' %}      {% block body %}          {{ html | safe }}      {% endblock %}        <script src="/public/client.js"></script></code></pre>    <p>这样,一个简单的服务端渲染就结束了,限于篇幅,详细的代码请 参考  Github代码库 。</p>    <p>https://github.com/pangz1/vue-ssr</p>    <h2>小结</h2>    <p>整个demo包含了:</p>    <p>- vue + vue-router + vuex 的使用</p>    <p>- 服务端数据获取</p>    <p>- 客户端数据同步以及DOM hydration。</p>    <p>没有涉及:</p>    <p>- 流式渲染</p>    <p>- 组件缓存</p>    <p>对Vue的服务端渲染有更深一步的认识,实际在生产环境中的应用可能还需要考虑很多因素。</p>    <p>选择Vue的服务端渲染方案,是情理之中的选择,不是对新技术的盲目追捧,而是一切为了需要。 Vue 2.0的SSR方案只是提供了一种可能,多了一种选择,框架本身在于服务开发者,根据不同的场景选择不同的方案,才会事半功倍。</p>    <p> </p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s/rClP45Eng4vlI887wY5fdw</p>    <p> </p>    
 本文由用户 keuo9813 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
 转载本站原创文章,请注明出处,并保留原始链接、图片水印。
 本站是一个以用户分享为主的开源技术平台,欢迎各类分享!
 本文地址:https://www.open-open.com/lib/view/open1487737525343.html
Vue.js Vue.js开发