发布于 2018-02-16 03:58:12 | 122 次阅读 | 评论: 0 | 来源: 网友投递

这里有新鲜出炉的Vue.js 教程,程序狗速度看过来!

Vue.js 轻量级 JavaScript 框架

Vue.js 是构建 Web 界面的 JavaScript 库,提供数据驱动的组件,还有简单灵活的 API,使得 MVVM 更简单。


本篇文章主要介绍了简单的 Vue SSR的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

前言

最近接手一个老项目,典型的 Vue 组件化前端渲染,后续业务优化可能会朝 SSR 方向走,因此,就先做些技术储备。如果对 Vue SSR 完全不了解,请先阅读官方文档。

思路

Vue 提供了一个官方 Demo,该 Demo 优点是功能大而全,缺点是对新手不友好,容易让人看蒙。因此,今天我们来写一个更加容易上手的 Demo。总共分三步走,循序渐进。

  1. 写一个简单的前端渲染 Demo(不包含 Ajax 数据);
  2. 将前端渲染改成后端渲染(仍然不包含 Ajax 数据);
  3. 在后端渲染的基础上,加上 Ajax 数据的处理;

第一步:前端渲染 Demo

这部分比较简单,就是一个页面中包含两个组件:Foo 和 Bar。


<!-- index.html -->
<body>
<div id="app">
 <app></app>
</div>
<script src="./dist/web.js"></script> <!--这是 app.js 打包出来的 JS 文件 -->
</body>
// app.js,也是 webpack 打包入口
import Vue from 'vue';
import App from './App.vue';
var app = new Vue({
 el: '#app',
 components: {
 App
 }
});

// App.vue
<template>
 <div>
 <foo></foo>
 <bar></bar>
 </div>
</template>
<script>
 import Foo from './components/Foo.vue';
 import Bar from './components/Bar.vue';
 export default {
 components:{
  Foo,
  Bar
 }
 }
</script>

// Foo.vue
<template>
 <div class='foo'>
 <h1>Foo</h1>
 <p>Component </p>
 </div>
</template>
<style>
 .foo{
 background: yellow;
 }
</style>

// Bar.vue
<template>
 <div class='bar'>
 <h1>Bar</h1>
 <p>Component </p>
 </div>
</template>
<style>
 .bar{
 background: blue;
 }
</style>

最终渲染结果如下图所示,源码请参考这里。

第二步:后端渲染(不包含 Ajax 数据)

第一步的 Demo 虽不包含任何 Ajax 数据,但即便如此,要把它改造成后端渲染,亦非易事。该从哪几个方面着手呢?

  1. 拆分 JS 入口;
  2. 拆分 Webpack 打包配置;
  3. 编写服务端渲染主体逻辑。

1. 拆分 JS 入口

在前端渲染的时候,只需要一个入口 app.js。现在要做后端渲染,就得有两个 JS 文件:entry-client.js 和 entry-server.js 分别作为浏览器和服务器的入口。

先看 entry-client.js,它跟第一步的 app.js 有什么区别吗? → 没有区别,只是换了个名字而已,内容都一样。

再看 entry-server.js,它只需返回 App.vue 的实例。


// entry-server.js
export default function createApp() {
 const app = new Vue({
 render: h => h(App)
 });
 return app; 
};

entry-server.js 与 entry-client.js 这两个入口主要区别如下:

  1. entry-client.js 在浏览器端执行,所以需要指定 el 并且显式调用 $mount 方法,以启动浏览器的渲染。
  2. entry-server.js 在服务端被调用,因此需要导出为一个函数。

2. 拆分 Webpack 打包配置

在第一步中,由于只有 app.js 一个入口,只需要一份 Webpack 配置文件。现在有两个入口了,自然就需要两份 Webpack 配置文件:webpack.server.conf.js 和 webpack.client.conf.js,它们的公共部分抽象成 webpack.base.conf.js。

关于 webpack.server.conf.js,有两个注意点:

  1. libraryTarget: 'commonjs2' → 因为服务器是 Node,所以必须按照 commonjs 规范打包才能被服务器调用
  2. target: 'node' → 指定 Node 环境,避免非 Node 环境特定 API 报错,如 document 等。

3. 编写服务端渲染主体逻辑

Vue SSR 依赖于包 vue-server-render,它的调用支持两种入口格式:createRenderer 和 createBundleRenderer,前者以 Vue 组件为入口,后者以打包后的 JS 文件为入口,本文采取后者。


// server.js 服务端渲染主体逻辑
// dist/server.js 就是以 entry-server.js 为入口打包出来的 JS 
const bundle = fs.readFileSync(path.resolve(__dirname, 'dist/server.js'), 'utf-8'); 
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
 template: fs.readFileSync(path.resolve(__dirname, 'dist/index.ssr.html'), 'utf-8')
});

server.get('/index', (req, res) => {
 renderer.renderToString((err, html) => {
 if (err) {
  console.error(err);
  res.status(500).end('服务器内部错误');
  return;
 }
 res.end(html);
 })
});

server.listen(8002, () => {
 console.log('后端渲染服务器启动,端口号为:8002');
});

这一步的最终渲染效果如下图所示,从图中我们可以看到,组件已经被后端成功渲染了。源码请参考这里

第三步:后端渲染(预获取 Ajax 数据)

这是关键的一步,也是最难的一步。

假如第二步的组件各自都需要请求 Ajax 数据的话,该怎么处理呢?官方文档给我们指出了思路,我简要概括如下:

  1. 在开始渲染之前,预先获取所有需要的 Ajax 数据(然后存在 Vuex 的 Store 中);
  2. 后端渲染的时候,通过 Vuex 将获取到的 Ajax 数据分别注入到各个组件中;
  3. 把全部 Ajax 数据埋在 window.INITIAL_STATE 中,通过 HTML 传递到浏览器端;
  4. 浏览器端通过 Vuex 将 window.INITIAL_STATE 里面的 Ajax 数据分别注入到各个组件中。

下面谈几个重点。

我们知道,在常规的 Vue 前端渲染中,组件请求 Ajax 一般是这么写的:“在 mounted 中调用 this.fetchData,然后在回调里面把返回数据写到实例的 data 中,这就 ok 了。”

在 SSR 中,这是不行的,因为服务器并不会执行 mounted 周期。那么我们是否可以把 this.fetchData

提前到 created 或者 beforeCreate 这两个生命周期中执行?同样不行。原因是:this.fetchData 是异步请求,请求发出去之后,没等数据返回呢,后端就已经渲染完了,无法把 Ajax 返回的数据也一并渲染出来。

所以,我们得提前知道都有哪些组件有 Ajax 请求,等把这些 Ajax 请求都返回了数据之后,才开始组件的渲染。


// store.js
function fetchBar() {
 return new Promise(function (resolve, reject) {
 resolve('bar ajax 返回数据');
 });
}

export default function createStore() {
 return new Vuex.Store({
 state: {
  bar: '',
 },
 actions: {
  fetchBar({commit}) {
  return fetchBar().then(msg => {
   commit('setBar', {msg})
  })
  }
 },
 mutations:{
  setBar(state, {msg}) {
  Vue.set(state, 'bar', msg);
  }
 }
 })
}


// Bar.uve
asyncData({store}) {
 return store.dispatch('fetchBar');
},
computed: {
 bar() {
 return this.$store.state.bar;
 }
}

组件的 asyncData 方法已经定义好了,但是怎么索引到这个 asyncData 方法呢?先看我的根组件 App.vue 是怎么写的。


// App.vue
<template>
 <div>
 <h1>App.vue</h1>
 <p>vue with vue </p>
 <hr>
 <foo1 ref="foo_ref"></foo1>
 <bar1 ref="bar_ref"></bar1>
 <bar2 ref="bar_ref2"></bar2>
 </div>
</template>
<script>
 import Foo from './components/Foo.vue';
 import Bar from './components/Bar.vue';

 export default {
 components: {
  foo1: Foo,
  bar1: Bar,
  bar2: Bar
 }
 }
</script>

从根组件 App.vue 我们可以看到,只需要解析其 components 字段,便能依次找到各个组件的 asyncData 方法了。


// entry-server.js 
export default function (context) {
 // context 是 vue-server-render 注入的参数
 const store = createStore();
 let app = new Vue({
 store,
 render: h => h(App)
 });

 // 找到所有 asyncData 方法
 let components = App.components;
 let prefetchFns = [];
 for (let key in components) {
 if (!components.hasOwnProperty(key)) continue;
 let component = components[key];
 if(component.asyncData) {
  prefetchFns.push(component.asyncData({
  store
  }))
 }
 }

 return Promise.all(prefetchFns).then((res) => {
 // 在所有组件的 Ajax 都返回之后,才最终返回 app 进行渲染
 context.state = store.state;
 // context.state 赋值成什么,window.__INITIAL_STATE__ 就是什么
 return app;
 });
};

还有几个问题比较有意思:

1、是否必须使用 vue-router?→ 不是。虽然官方给出的 Demo 里面用到了 vue-router,那只不过是因为官方 Demo 是包含多个页面的 SPA 罢了。一般情况下,是需要用 vue-router 的,因为不同路由对应不同的组件,并非每次都把所有组件的 asyncData 都执行的。但是有例外,比如我的这个老项目,就只有一个页面(一个页面中包含很多的组件),所以根本不需要用到 vue-router,也照样能做 SSR。主要的区别就是如何找到那些该被执行的 asyncData 方法:官方 Demo 通过 vue-router,而我通过直接解析 components 字段,仅此而已。

2、是否必须使用 Vuex? → 是,但也不是,请看尤大的回答。为什么必须要有类似 Vuex 的存在?我们来分析一下。

2.1. 当预先获取到的 Ajax 数据返回之后,Vue 组件还没开始渲染。所以,我们得把 Ajax 先存在某个地方。

2.2. 当 Vue 组件开始渲染的时候,还得把 Ajax 数据拿出来,正确地传递到各个组件中。

2.3. 在浏览器渲染的时候,需要正确解析 window.INITIAL_STATE ,并传递给各个组件。

因此,我们得有这么一个独立于视图以外的地方,用来存储、管理和传递数据,这就是 Vuex 存在的理由。

3、后端已经把 Ajax 数据转化为 HTML 了,为什么还需要把 Ajax 数据通过 window.INITIAL_STATE 传递到前端? → 因为前端渲染的时候仍然需要知道这些数据。举个例子,你写了一个组件,给它绑定了一个点击事件,点击的时候打印出 this.msg 字段值。现在后端是把组件 HTML 渲染出来了,但是事件的绑定肯定得由浏览器来完成啊,如果浏览器拿不到跟服务器端同样的数据的话,在触发组件的点击事件的时候,又上哪儿去找 msg 字段呢?

至此,我们已经完成了带 Ajax 数据的后端渲染了。这一步最为复杂,也最为关键,需要反复思考和尝试。具体渲染效果图如下所示,源码请参考这里

效果

大功告成了吗?还没。人们都说 SSR 能提升首屏渲染速度,下面我们对比一下看看到底是不是真的。(同样在 Fast 3G 网络条件下)。

官方思路的变形

行文至此,关于 Vue SSR Demo便已经结束了。后面是我结合自身项目特点的一些变形,不感兴趣的读者可以不看。

第三步官方思路有什么缺点吗?我认为是有的:对老项目来说,改造成本比较大。需要显式的引入 vuex,就得走 action、mutations 那一套,无论是代码改动量还是新人学习成本,都不低。

有什么办法能减少对旧有前端渲染项目的改动量的吗?我是这么做的。


// store.js
// action,mutations 那些都不需要了,只定义一个空 state
export default function createStore() {
 return new Vuex.Store({
 state: {}
 })
}
// Bar.vue
// tagName 是组件实例的名字,比如 bar1、bar2、foo1 等,由 entry-server.js 注入
export default {
 prefetchData: function (tagName) {
 return new Promise((resolve, reject) => {
  resolve({
  tagName,
  data: 'Bar ajax 数据'
  });
 })
 }
}

// entry-server.js
return Promise.all(prefetchFns).then((res) => {
 // 拿到 Ajax 数据之后,手动将数据写入 state,不通过 action,mutation 那一套
 // state 内部区分的 key 值就是 tagName,比如 bar1、bar2、foo1 等
 res.forEach((item, key) => {
 Vue.set(store.state, `${item.tagName}`, item.data);
 });
 context.state = store.state;
 return app;
});

// ssrmixin.js
// 将每个组件都需要的 computed 抽象成一个 mixin,然后注入
export default {
 computed: {
 prefetchData () {
  let componentTag = this.$options._componentTag; // bar1、bar2、foo1
  return this.$store.state[componentTag];
 }
 }
}

至此,我们就便得到了 Vue SSR 的一种变形。对于组件开发者而言,只需要把原来的 this.fetchData 方法抽象到 prefetchData 方法,然后就可以在 DOM 中使用 {{prefetchData}} 拿到到数据了。这部分的代码请参考这里

总结

Vue SSR 确实是个有趣的东西,关键在于灵活运用。此 Demo 还有一个遗留问题没有解决:当把 Ajax 抽象到 prefetchData,做成 SSR 之后,原先的前端渲染就失效了。能不能同一份代码同时支持前端渲染和后端渲染呢?这样当后端渲染出问题的时候,我就可以随时切回前端渲染,便有了兜底的方案。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持phperz。



最新网友评论  共有(0)条评论 发布评论 返回顶部

Copyright © 2007-2017 PHPERZ.COM All Rights Reserved   冀ICP备14009818号  版权声明  广告服务