Vue 2.0 vue迁移到服务端渲染染怎么玩

Vue 2.0 发布啦! - 囧克斯
Vue 2.0 发布啦!
今天我们非常激动的首发 Vue 2.0 preview 版本,这个版本带来了很多激动人心的改进和新特性。我们来看看这里面都有些什么!
更轻,更快
Vue.js 始终聚焦在轻量和快速上面,而 2.0 把它做得更好。现在的渲染层基于一个轻量级的 virtual-DOM 实现,在大多数场景下初试化渲染速度和内存消耗都提升了 2~4 倍 (详见这里的 )。从模板到 virtuel-DOM 的编译器和运行时是可以独立开来的,所以你可以将模板预编译并只通过 Vue 的运行时让你的应用工作起来,而这份运行时的代码 min+gzip 之后只有不到 12kb (提一下,React 15 在 min+gzip 之后的大小是 44kb)。编译器同样可以在浏览器中工作,也就是说你也可以写一段 script 标签然后开始你的工作,就像以前一样。而即便你把编译器加进去,build 出来的文件 min+gzip 之后也仅有 17kb,仍然小于目前的 1.0 版本。
不是普通的 Virtual-DOM
现在 virtual-DOM 有点让人听腻了,因为社区里有太多种实现,但是 Vue 2.0 的实现有与众不同的地方。和 Vue 的响应式系统结合在一起之后,它可以让你不必做任何事就获得完全优化的重渲染。由于每个组件都会在渲染时追踪其响应依赖,所以系统精确地知道应该何时重渲染、应该重渲染哪些组件。不需要 shouldComponentUpdate,也不需要 immutable 数据 - it just works.
除此之外,Vue 2.0 从模板到 virtuel-DOM 的编译阶段使用了一些高阶优化:
它会检测出静态的 class 名和 attributes 这样它们在初始化渲染之后就永远都不会再被比对。
它会检测出最大静态子树 (就是不需要动态性的子树) 并且从渲染函数中萃取出来。这样在每次重渲染的时候,它就会直接重用完全相同的 virtual nodes 同时跳过比对。
这些高阶优化通常只会在使用 JSX 时通过 Babel plugin 来做,但是 Vue 2.0 即使在使用浏览器内的编译器时也能做到。
新的渲染系统同时允许你通过简单的冻结数据来禁用响应式转换,配以手动的强制更新,这意味着你对于重渲染的流程实际上有着完全的控制权。
以上这些技术组合在一起,确保了 Vue 2.0 在每一个场景下都能够拥有高性能的表现,同时把开发者的负担和成本降到了最低。
Templates, JSX, or Hyperscript?
开发者对于用模板还是 JSX 有很多的争执。一方面,模板更接近 HTML - 它能更好地反映你的 app 的语义结构,并且易于思考视觉上的设计、布局和样式。另一方面,模板作为一个 DSL 也有它的局限性 - 相比之下 JSX/hyperscript 的程序本质使得它们具有图灵完备的表达能力。
作为一个兼顾设计和开发的人,我喜欢用模板来写大部分的界面,但在某些情况下我也希望能拥有 JSX/hyperscript 的灵活性。举例来说,当你想在一个组件中程序化的处理其子元素时,基于模板的 slot 机制会显得比较有局限性。
那么,为什么不能同时拥有它们呢?在 Vue 2.0 中,你可以继续使用熟悉的模板语法,但当你觉得受限制的时候,你也可以直接写底层的 virtual-DOM 代码,只需用一个 render 函数替换掉 template 选项。你甚至可以直接在你的模板里使用一个特殊的 &render& 标签来嵌入渲染函数!一个框架,两全其美。
流式服务端渲染
既然迁移到了 virtual-DOM,Vue 2.0 自然支持服务端渲染和客户端的 hydration(直接使用服务端渲染的 DOM 元素)。当前服务端渲染的实现有一个痛点,比如在 React 里,渲染是同步的,所以如果这个 app 比较复杂的话它会阻塞服务器的 event loop。同步的服务端渲染在优化不当的情况下甚至会对客户端获得内容的速度带来负面影响。Vue 2.0 提供了内建的流式服务端渲染 - 在渲染组件时返回一个可读的 stream,然后直接 pipe 到 HTTP response。流式渲染能够确保服务端的响应度,也能让用户更快地获得渲染内容。
解锁更多可能性
基于新的架构,我们还有更多的可能性有待开发 - 比如在手机端渲染到 native 界面。目前我们正在探索一个 Vue.js 2.0 的端,它会用 :一个由中国最大科技公司之一的阿里巴巴的工程师们维护的项目,作为一个 native 的渲染层。同时从技术角度 Vue 2.0 运行在 ReactNative 上也是可行的。让我们拭目以待!
兼容性以及接下来的计划
Vue.js 2.0 仍然处在 pre-alpha 阶段,但是你可以来 查看源代码。尽管 2.0 是一个完全重写的项目,但是除了一些有意废弃掉的功能,API 和 1.0 是大部分兼容的。看看
- 你会发现几乎没有什么变化!
对于部分功能的废弃,本质上是为了提供更简洁的 API 从而提高开发者的效率。你可以移步 查看 1.0 和 2.0 的特性比对。如果你在现有的项目中大量地使用着一些被废弃的特性,这意味着会有一定的迁移成本,不过我们在未来会提供更详实的升级指导。
现在我们还有很多工作没有完成。一旦我们达到了令人满意的测试覆盖率,我们将会推出 alpha 版本,同时我们希望能在五月底六月初推出 beta 版。除了更多的测试之外,我们也需要更新相关库(如 vue-router, Vuex, vue-loader, vueify...)的支持。目前只有 Vuex 在 2.0 下可以直接使用,但是我们会确保在 2.0 正式发布时所有东西都会顺畅地工作。
我们不会因此而忘记 1.x 哦!1.1 将会和 2.0 beta 独立发布,提供六个月 critical bug fixes 和九个月安全升级的长效服务 (LTS)。同时 1.1 还会包含可选的废弃特性警告,让你为升级到 2.0 做好充足的准备。尽请期待!
又是一个轮子。。。明明有react了,不明白意义何在
这种问题就算是你提到的 React 出来的时候也有人这么说,Vue 有很多存在的意义都已经写在原文里了,希望你可以感受到
添加新评论 &
: 关注技术的欢迎逛逛极客技术 http://log4geek.cc
欢迎码农加入QQ群 珠三角码农交流...
: 请问博主分析的是vue1.0的还是vue2.0的呢,博主写的好深奥,菜鸟表示看不懂,
: webpack能做图片压缩吗,雪碧图行么
: webpack能做图片压缩吗,雪碧图行么
: 就程序加载方式,你有很多选择,requireJS也是完全你自己可以用的,这个跟vue无关
: 可以把vue.js2.0的源码出个视频或文字解说版本吗?
: 刚接触vue2,发现学习起来感觉好有心劲!!!早日用到生产环境!
: 大黄请客吧,那个源码能共享一下么?
: 表示支持一下vue-easy-renderer - Nodejs服务端渲染 相关介绍、文档、教程 - OpenDigg
vue-easy-renderer
这个包提供了Nodejs服务端渲染,应用于vue 2.0,基于vue-server-render。
npm install vue-easy-renderer vue vuex vue-loader -S
在componet/hello_word/hello_word.vue中创建vue文件
&template&
&div&hello {{world}}&/div&
&/template&
&style scoped&
export default {
name: 'HelloWorld',
'use strict';
const path = require('path');
const Koa = require('koa');
const serve = require('koa-static');
const vueEasyRenderer = require('vue-easy-renderer').kaoR
const renderer = vueEasyRenderer(path.resolve(__dirname, './component'));
const app = new Koa();
app.use(serve('./dist'));
app.use(renderer);
// with ES7 async/await
// app.use(async ctx =& {
await ctx.vueRender('simple.vue', {hello: 'world!'});
app.use(ctx =& ctx.vueRender('hello_world/hello_world.vue', {world: 'world!'}));
app.listen(8080);
console.log('vue-easy-renderer koa example start in 127.0.0.1:8080');
module.exports =
Express.js
'use strict';
const path = require('path');
const express = require('express');
const vueEasyRenderer = require('vue-easy-renderer').connectR
const renderer = vueEasyRenderer(path.resolve(__dirname, './component'));
const app = express();
app.use(express.static('./dist'));
app.use(renderer);
app.get('/', (req, res) =& res.vueRender('hello_world/hello_world.vue', {world: 'world!'}));
app.listen(8081);
console.log('vue-easy-renderer express example start in 127.0.0.1:8081');
module.exports =最近, 产品同学一如往常笑嘻嘻的递来需求文档, 纵使内心万般拒绝, 身体倒是很诚实。 接过需求,好在需求不复杂, 简单构思 后决定用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页面的源代码
下图SSR页面的源代码
上面两张图就是使用了传统单页应用和SSR的页面源代码, 第一张图中,很明显页面的数据都是通过Ajax异步获取,然而搜索引擎度娘家的爬虫看到这样空旷的源码并不会丝毫留恋. 相反,通过服务端渲染的页面,就有很多对于爬虫来讲有效的连接. 毕竟度娘一家独大,看来服务端渲染确实有探究的必要了。
vue 的服务端渲染是怎么回事?
先看一张Vue官网的服务端渲染示意图
从图上可以看出,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 文件的实例代码
&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.courseListI & & & & & & &}, & & & & & & &articleItems() { & & & & & & & & &return this.$store.state.articleI & & & & & & &} & & & & &}, & & & & &components: { & & & & & & &courseCover, & & & & & & &articleItems & & & & &}, & & & & &// 服务端获取数据 & & & & &fetchServerData ({ state, dispatch, commit }) { & & & & & & &let alias = state.route.params. & & & & & & &return Promise.all([ & & & & & & & & &dispatch('FETCH_ZT', { alias }), & & & & & & & & &dispatch('FETCH_COURSE_ITEMS'), & & & & & & & & &dispatch('FETCH_ARTICLE_ITEMS') & & & & & & &]) & & & & &}, & & & & &// 客户端获取数据 & & & & &beforeMount() { & & & & & & &return this.$store.dispatch('FETCH_COURSE_ITEMS'); & & & & &} & & &} &&/script&
这里我们暴露一个 fetchServerData 方法用来在服务端渲染时做数据的预加载,具体在哪调用,下面会讲到。 beforeMount 是vue的生命周期钩子函数,当应用在客户端切换到这个视图的时候会在特定的时候去执行,用于在客户端获取数据。
使用 vuex 管理数据,vue2.0 的服务端官方推荐使用&&来管理数据,和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. & & & & & & & & &commit('SET_COURSE_ITEMS', data); & & & & & & &}); & & & & &}, & & & & &FETCH_ARTICLE_ITEMS: ({ commit, dispatch, state }) =& { & & & & & & &return axios.get(`${apiHost}/api/article_items`) & & & & & & & & & & & & &.then(response =& { & & & & & & & & & & & & & & &let data = response. & & & & & & & & & & & & & & &commit('SET_ARTICLE_ITEMS', data); & & & & & & & & & & & & &}) & & & & &} & & &}, & & &mutations: { & & & & &SET_COURSE_ITEMS: (state, data) =& { & & & & & & &state.courseListItems = & & & & &}, & & & & &SET_ALIAS: (state, { alias }) =& { & & & & & & &state.alias = & & & & &}, & & & & &SET_ZT_DATA: (state, { ztData }) =& { & & & & & & &state.ztData = ztD & & & & &}, & & & & &SET_ARTICLE_ITEMS: (state, items) =& { & & & & & & &state.articleItems = & & & & &} & & &} &}) &export default
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. & & & & & &return & & & &}) & &}
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代码库。
/pangz1/vue-ssr
整个demo包含了:
- vue + vue-router + vuex 的使用
- 服务端数据获取
- 客户端数据同步以及DOM hydration。
没有涉及:
- 流式渲染
- 组件缓存
对Vue的服务端渲染有更深一步的认识,实际在生产环境中的应用可能还需要考虑很多因素。
选择Vue的服务端渲染方案,是情理之中的选择,不是对新技术的盲目追捧,而是一切为了需要。 Vue 2.0的SSR方案只是提供了一种可能,多了一种选择,框架本身在于服务开发者,根据不同的场景选择不同的方案,才会事半功倍。
最近, 产品同学一如往常笑嘻嘻的递来需求文档, 纵使内心万般拒绝, 身体倒是很诚实。 接过需求,好在需求不复杂, 简单构思 后决定用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页面的源代码
下图SSR页面的源代码
上面两张图就是使用了传统单页应用和SSR的页面源代码, 第一张图中,很明显页面的数据都是通过Ajax异步获取,然而搜索引擎度娘家的爬虫看到这样空旷的源码并不会丝毫留恋. 相反,通过服务端渲染的页面,就有很多对于爬虫来讲有效的连接. 毕竟度娘一家独大,看来服务端渲染确实有探究的必要了。
vue 的服务端渲染是怎么回事?
先看一张Vue官网的服务端渲染示意图
从图上可以看出,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 文件的实例代码
&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.courseListI & & & & & & &}, & & & & & & &articleItems() { & & & & & & & & &return this.$store.state.articleI & & & & & & &} & & & & &}, & & & & &components: { & & & & & & &courseCover, & & & & & & &articleItems & & & & &}, & & & & &// 服务端获取数据 & & & & &fetchServerData ({ state, dispatch, commit }) { & & & & & & &let alias = state.route.params. & & & & & & &return Promise.all([ & & & & & & & & &dispatch('FETCH_ZT', { alias }), & & & & & & & & &dispatch('FETCH_COURSE_ITEMS'), & & & & & & & & &dispatch('FETCH_ARTICLE_ITEMS') & & & & & & &]) & & & & &}, & & & & &// 客户端获取数据 & & & & &beforeMount() { & & & & & & &return this.$store.dispatch('FETCH_COURSE_ITEMS'); & & & & &} & & &} &&/script&
这里我们暴露一个 fetchServerData 方法用来在服务端渲染时做数据的预加载,具体在哪调用,下面会讲到。 beforeMount 是vue的生命周期钩子函数,当应用在客户端切换到这个视图的时候会在特定的时候去执行,用于在客户端获取数据。
使用 vuex 管理数据,vue2.0 的服务端官方推荐使用&&来管理数据,和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. & & & & & & & & &commit('SET_COURSE_ITEMS', data); & & & & & & &}); & & & & &}, & & & & &FETCH_ARTICLE_ITEMS: ({ commit, dispatch, state }) =& { & & & & & & &return axios.get(`${apiHost}/api/article_items`) & & & & & & & & & & & & &.then(response =& { & & & & & & & & & & & & & & &let data = response. & & & & & & & & & & & & & & &commit('SET_ARTICLE_ITEMS', data); & & & & & & & & & & & & &}) & & & & &} & & &}, & & &mutations: { & & & & &SET_COURSE_ITEMS: (state, data) =& { & & & & & & &state.courseListItems = & & & & &}, & & & & &SET_ALIAS: (state, { alias }) =& { & & & & & & &state.alias = & & & & &}, & & & & &SET_ZT_DATA: (state, { ztData }) =& { & & & & & & &state.ztData = ztD & & & & &}, & & & & &SET_ARTICLE_ITEMS: (state, items) =& { & & & & & & &state.articleItems = & & & & &} & & &} &}) &export default
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. & & & & & &return & & & &}) & &}
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代码库。
/pangz1/vue-ssr
整个demo包含了:
- vue + vue-router + vuex 的使用
- 服务端数据获取
- 客户端数据同步以及DOM hydration。
没有涉及:
- 流式渲染
- 组件缓存
对Vue的服务端渲染有更深一步的认识,实际在生产环境中的应用可能还需要考虑很多因素。
选择Vue的服务端渲染方案,是情理之中的选择,不是对新技术的盲目追捧,而是一切为了需要。 Vue 2.0的SSR方案只是提供了一种可能,多了一种选择,框架本身在于服务开发者,根据不同的场景选择不同的方案,才会事半功倍。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:3017次
排名:千里之外
原创:39篇
转载:15篇
(9)(3)(18)(9)(4)(1)(2)(2)(3)(6)}

我要回帖

更多关于 vue2 服务端渲染 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信