Vue 2.0vue 服务端渲染染怎么玩

如何使用Vue2做服务端渲染 - 知乎专栏
{"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"title":"如何使用Vue2做服务端渲染","author":"wang-shi-yang-15","content":"花费了一个月时间,终于在新养车之家项目中成功部署了vue2服务端渲染(SSR),并且使用上了Vuex 负责状态管理,首屏加载时间从之前4G网络下的1000ms,提升到了现在500-700ms之间,SSR的优势有很多,现在让我来跟你细细道来。技术栈服务端:Nodejs(v6.3)前端框架 Vue2.1.10前端构建工具:webpack2.2 && gulp代码检查:eslint源码:es6前端路由:vue-router2.1.0状态管理:vuex2.1.0服务端通信:axios日志管理:log4js项目自动化部署工具:jenkinsVue2与服务端渲染(SSR)Vue2.0在服务端创建了虚拟DOM,因此可以在服务端可以提前渲染出来,解决了单页面一直存在的问题:SEO和初次加载耗时较多的问题。同时在真正意义上做到了前后端共用一套代码。SSR的实现原理客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回 Promise (官方是preFetch方法)来将需要的数据拿到。最后再通过&script&window.__initial_state=data&/script&\n将其写入网页,最后将服务端渲染好的网页返回回去。接下来客户端会将vuex将写入的 __initial_state__ 替换为当前的全局状态树,再用这个状态树去检查服务端渲染好的数据有没有问题。遇到没被服务端渲染的组件,再去发异步请求拿数据。说白了就是一个类似React的 shouldComponentUpdate 的Diff操作。Vue2使用的是单向数据流,用了它,就可以通过 SSR 返回唯一一个全局状态, 并确认某个组件是否已经SSR过了。开启服务端渲染(SSR)Web框架目前我们使用的是express,之前使用过一次时间的koa来做SSR,结果发现坑很多,相关的案例太少,有些坑不太好解决,所以为了线上项目的稳定,从而选择了express。SSR流程图安装SSR相关npm install --save express vue-server-renderer lru-cache es6-promise serialize-javascript vue vue-router axios\nvue更新到2.0之后,作者就宣告不再对vue-resource更新,并且vue-resource不支持SSR,所以我推荐使用axios, 在服务端和客户端可以同时使用。vue2使用了虚拟DOM, 因此对浏览器环境和服务端环境要分开渲染, 要创建两个对应的入口文件。浏览器入口文件 client-entry.js使用 $mount 直接挂载服务端入口文件 server-entry使用vue的SSR功能直接将虚拟DOM渲染成网页client-entry.js 文件import 'es6-promise/auto';\n\nimport { app, store } from './app';\n\nstore.replaceState(window.__INITIAL_STATE__);\n\napp.$mount('#app');\n在
client-entry.js 文件中引入了app.js, 判断如果在服务端渲染时已经写入状态,则将vuex的状态进行替换,使得服务端渲染的html和vuex管理的数据是同步的。然后将vue实例挂载到html指定的节点中。server-entry 文件import { app, router, store } from './app';\n\nconst isDev = process.env.NODE_ENV !== 'production';\n\t\nexport default context =& {\n
const s = isDev && Date.now();\n\n
router.push(context.url);\n
const matchedComponents = router.getMatchedComponents();\n\n
if (!matchedComponents.length) {\n
return Promise.reject({ code: '404' });\n
return Promise.all(matchedComponents.map(component =& {\n
if (component.preFetch) {\n
return component.preFetch(store);\n
})).then(() =& {\n
return app;\n
});\n};\n在
server-entry 文件中服务端会传递一个context对象,里面包含当前用户请求的url,vue-router 会跳转到当前请求的url中,通过 router.getMatchedComponents( ) 来获得当前匹配组件,则去调用当前匹配到的组件里的 preFetch 钩子,并传递store(Vuex下的状态),会返回一个 Promise 对象,并在then方法中将现有的vuex state 赋值给context,给服务端渲染使用,最后返回vue实例,将虚拟DOM渲染成网页。服务端会将vuex初始状态也生成到页面中。 如果 vue-router 没有匹配到请求的url,直接返回 Promise中的reject方法,传入404,这时候会走到下方renderStream的error事件,让页面显示错误信息。// 处理所有的get请求\napp.get('*', (req, res) =& {\n
// 等待编译\n
if (!renderer) {\n
return res.end('waiting for compilation... refresh in a moment.');\n
var s = Date.now();\n
const context = { url: req.url };\n
// 渲染我们的Vue实例作为流\n
const renderStream = renderer.renderToStream(context);\n\t\n
// 当块第一次被渲染时\n
renderStream.once('data', () =& {\n
\t // 将预先的HTML写入响应\n
res.write(indexHTML.head);\n
// 每当新的块被渲染\n
renderStream.on('data', chunk =& {\n
\t // 将块写入响应\n
res.write(chunk);\n
// 当所有的块被渲染完成\n
renderStream.on('end', () =& {\n
// 当vuex初始状态存在\n
if (context.initialState) {\n
\t// 将vuex初始状态以script的方式写入到页面中\n
res.write(\n
`&script&window.__INITIAL_STATE__=${\n
serialize(context.initialState, { isJSON: true })\n
}&/script&`\n
// 将结尾的HTML写入响应\n
res.end(indexHTML.tail);\n
// 当渲染时发生错误\n
renderStream.on('error', err =& {\n
if (err && err.code === '404') {\n
res.status(404).end('404 | Page Not Found');\n
res.status(500).end('Internal Error 500');\n
});\n})\n上面是vue2.0的服务端渲染方式,用流式渲染的方式,将HTML一边生成一边写入相应流,而不是在最后一次全部写入。这样的效果就是页面渲染速度将会很快。还可以引入 lru-cache 这个模块对数据进行缓存,并设置缓存时间,我一般设置15分钟的缓存时间。可以参考vue ssr 官方演示项目的服务端实现 &axios在客户端和服务端的使用创建2个文件用于客户端和服务端的的通信create-api-client.js 文件(用于客户端)const axios = require('axios');\\n\naxios.defaults.timeout = 10000;\n\naxios.interceptors.response.use((res) =& {\n
if (res.status &= 200 && res.status & 300) {\\n
return Promise.reject(res);\n}, (error) =& {\n
// 网络异常\n
return Promise.reject({message: '网络异常,请刷新重试', err: error});\n});\n\nif (process.__API__) {\n
api = process.__API__;\n} else {\n
get: function(target, params = {}) {\n
const suffix = Object.keys(params).map(name =& {\n
return `${name}=${JSON.stringify(params[name])}`;\n
}).join('&');\n
const urls = `${target}?${suffix}`;\n
return new Promise((resolve, reject) =& {\n
axios.get(urls, params).then(res =& {\n
resolve(res.data);\n
}).catch((error) =& {\n
reject(error);\n
post: function(target, options = {}) {\n
return new Promise((resolve, reject) =& {\n
axios.post(target, options).then(res =& {\n
resolve(res.data);\n
}).catch((error) =& {\n
reject(error);\n
};\n}\n\nmodule.exports =\ncreate-api-server.js 文件(用于服务端)const isProd = process.env.NODE_ENV === 'production';\n\nconst axios = require('axios');\nlet host = isProd ? 'http://yczj..cn' : 'http://t.yczj..cn';\nlet cook = process.__COOKIE__ || '';\nlet api;\n\naxios.defaults.baseURL = host;\naxios.defaults.timeout = 10000;\n\naxios.interceptors.response.use((res) =& {\n
if (res.status &= 200 && res.status & 300) {\n
return res;\n
return Promise.reject(res);\n}, (error) =& {\n
// 网络异常\n
return Promise.reject({message: '网络异常,请刷新重试', err: error, type: 1});\n});\n\nif (process.__API__) {\n
api = process.__API__;\n} else {\n
get: function(target, options = {}) {\n
return new Promise((resolve, reject) =& {\n
axios.request({\n
url: target,\n
method: 'get',\n
headers: {\n
'Cookie': cook\n
params: options\n
}).then(res =& {\n
resolve(res.data);\n
}).catch((error) =& {\n
reject(error);\n
post: function(target, options = {}) {\n
return new Promise((resolve, reject) =& {\n
axios.request({\n
url: target,\n
method: 'post',\n
headers: {\n
'Cookie': cook\n
params: options\n
}).then(res =& {\n
resolve(res.data);\n
}).catch((error) =& {\n
reject(error);\n
};\n}\n\nmodule.exports = api;\n由于在服务端,接口不会主动携带 cookie,所以需要在headers里写入cookie。由于接口数据经常发生变化,所以没有做缓存。如果您想了解更多最新前端技术,请关注 AutoHome车服务前端团队 微信公众号","updated":"T05:16:12.000Z","canComment":false,"commentPermission":"anyone","commentCount":18,"collapsedCount":0,"likeCount":61,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","titleImage":"/v2-bfc9c0f0b8668f3ecf84ac5da71d9aea_r.png","links":{"comments":"/api/posts//comments"},"reviewers":[],"topics":[{"url":"/topic/","id":"","name":"Vue.js"},{"url":"/topic/","id":"","name":"vue-router"},{"url":"/topic/","id":"","name":"Node.js"}],"adminClosedComment":false,"titleImageSize":{"width":912,"height":364},"href":"/api/posts/","excerptTitle":"","tipjarState":"closed","annotationAction":[],"sourceUrl":"","pageCommentsCount":18,"hasPublishingDraft":false,"snapshotUrl":"","publishedTime":"T13:16:12+08:00","url":"/p/","lastestLikers":[{"bio":"知乎吃枣药丸","isFollowing":false,"hash":"5dddc1c939ce7df8513430","uid":895700,"isOrg":false,"slug":"zhong-yi-wei-69","isFollowed":false,"description":"","name":"钟艺伟","profileUrl":"/people/zhong-yi-wei-69","avatar":{"id":"v2-4d81aae531facb66b58ed77155d88bee","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":null,"isFollowing":false,"hash":"46a35b5ccd5d444e4a72f","uid":72,"isOrg":false,"slug":"wang-kun-wei-48","isFollowed":false,"description":"","name":"密斯特拉","profileUrl":"/people/wang-kun-wei-48","avatar":{"id":"bbe5e4a9c","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"前端","isFollowing":false,"hash":"944c0dd5ab352abd6d857ac","uid":697000,"isOrg":false,"slug":"fan-sen-ze","isFollowed":false,"description":"- 想要未来过的好其实很简单,找到一个自己还不算讨厌的方向,剩余的,无非就是坚定的去学习,努力的去拓展,让它变成你的一技之长.","name":"bilibilili","profileUrl":"/people/fan-sen-ze","avatar":{"id":"34b6ef58eae1da17eec928","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"滚五/毁灭法师/三流后端/不入流前端","isFollowing":false,"hash":"fabeadd0ea5","uid":12,"isOrg":false,"slug":"lin-shen-shi-jian-lu","isFollowed":false,"description":"短发萧骚襟袖冷","name":"张黑子","profileUrl":"/people/lin-shen-shi-jian-lu","avatar":{"id":"29ba2b7f9f8c3a1e8b72dd","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"yoooooooo","isFollowing":false,"hash":"a13ffaa7c22f35fb3c1bdef6e81d1b5a","uid":24,"isOrg":false,"slug":"deng-hong-cai","isFollowed":false,"description":"随心的生活","name":"邓宏才","profileUrl":"/people/deng-hong-cai","avatar":{"id":"5fac3f89ea","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false}],"summary":"花费了一个月时间,终于在新养车之家项目中成功部署了vue2服务端渲染(SSR),并且使用上了Vuex 负责状态管理,首屏加载时间从之前4G网络下的1000ms,提升到了现在500-700ms之间,SSR的优势有很多,现在让我来跟你细细道来。技术栈服务端:Nodejs(v6.3)前…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"annotationDetail":null,"commentsCount":18,"likesCount":61,"FULLINFO":true}},"User":{"wang-shi-yang-15":{"isFollowed":false,"name":"王诗扬","headline":"负责汽车之家车服务前端部门","avatarUrl":"/07c41be876bd6c19e3ec1fe_s.jpg","isFollowing":false,"type":"people","slug":"wang-shi-yang-15","bio":"超越自己,生活将充满希望","hash":"b40c7f6694669cfe8babef","uid":92,"isOrg":false,"description":"负责汽车之家车服务前端部门","profileUrl":"/people/wang-shi-yang-15","avatar":{"id":"07c41be876bd6c19e3ec1fe","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false,"badge":{"identity":null,"bestAnswerer":null}}},"Comment":{},"favlists":{}},"me":{},"global":{},"columns":{"next":{}},"columnPosts":{},"columnSettings":{"colomnAuthor":[],"uploadAvatarDetails":"","contributeRequests":[],"contributeRequestsTotalCount":0,"inviteAuthor":""},"postComments":{},"postReviewComments":{"comments":[],"newComments":[],"hasMore":true},"favlistsByUser":{},"favlistRelations":{},"promotions":{},"switches":{"couldAddVideo":false},"draft":{"titleImage":"","titleImageSize":{},"isTitleImageFullScreen":false,"canTitleImageFullScreen":false,"title":"","titleImageUploading":false,"error":"","content":"","draftLoading":false,"globalLoading":false,"pendingVideo":{"resource":null,"error":null}},"drafts":{"draftsList":[],"next":{}},"config":{"userNotBindPhoneTipString":{}},"recommendPosts":{"articleRecommendations":[],"columnRecommendations":[]},"env":{"isAppView":false,"appViewConfig":{"content_padding_top":128,"content_padding_bottom":56,"content_padding_left":16,"content_padding_right":16,"title_font_size":22,"body_font_size":16,"is_dark_theme":false,"can_auto_load_image":true,"app_info":"OS=iOS"},"isApp":false},"sys":{}}Vue 2 服务端渲染初探
写这篇文章, Vue 2 还在 Beta 呢...
官方文档写得很清楚
似乎 Vue 1 有看到过通过 jsdom 做后端渲染的例子, 性能不佳.Vue 2 开始将 Virtual DOM 作为底层实现, 于是模块分离开始支持 SSR.
4 步走战略~
安装 hackernews 的例子, 完整的 app 渲染的例子包括:
用 Webpack 的 node 模式把整个应用单独打一个包
Node 环境通过 API 将这个包加载到 vm 环境当中
应用在 vm 内部启动 HTTP 请求抓取当前路由依赖的数据
生成网页模板, 将 HTML 和初始数据嵌在中间
如果网页依赖的数据少或者不依赖, 可以简化一点,比如中间抓取 HTTP 的步骤去掉, 可以简化不少,也许还可以去掉 vm 那步, 直接通过引用文件来生成 HTML.
两套 API 哦... 好像只用带 bundle 那套...
createRenderer([rendererOptions])
renderer.renderToString(vm, cb)
renderer.renderToStream(vm)
createBundleRenderer(code, [rendererOptions])
bundleRenderer.renderToString([context], cb)
bundleRenderer.renderToStream([context])
后面三个 API 都带上了 bundle, 此外看上去和前面的一样,bundle 是通过 Node.js 的 vm 模块运行的, 每次的都重新启动一遍代码,作者解释这样能清空整个 app 的状态,我推测这是因为用了 Vuex 之后, 数据会被缓存在内部无法清理,如果是单纯通过 props 传递数据, 应该是可以用前一套 API.
服务端渲染原理
有了 Virtual DOM 就好办了
VNode 定义
HTML 渲染的代码, 通过 write 同时支持到了 Stream 输出:
如果用 bundle 模式, 注意每次都会运行 vm.runInNewContext 新建环境.
最后返回用户的 HTML 其实是拼接出来的,注意首屏的动态数据, 也通过 window.__INITIAL_STATE__ 发送到浏览器,
速度快是因为缓存呢吧...
大致就是如果组件可以根据一个 key 来确定, 就可以进行缓存,静态的组件当然是有固定的 key, 动态的组件根据 id 等数据生成 key,
serverCacheKey: props => props.item.id + '::' + props.item.last_updated
如果组件可以找到缓存, 就直接返回缓存内容:
这也就意味着顶层的组件总之就是不能缓存的, 性能开销免不了.hackernews 的例子本地用 ab 压了一下, Mac Pro 到 130+qps 了,
Concurrency Level:
Time taken for tests:
3.013 seconds
Complete requests:
Failed requests:
Total transferred:
HTML transferred:
Requests per second:
132.77 [#/sec] (mean)
Time per request:
753.205 [ms] (mean)
Time per request:
7.532 [ms] (mean, across all concurrent requests)
Transfer rate:
3742.21 [Kbytes/sec] received
但是这个 Demo 是用了缓存的, 破坏掉缓存性能落差很大,我自己做的 Demo, 实际上加上缓存性能还不到这个一半...看来跟应用的类型是有关的, 特别是节点偏多的应用影响更大.
想象一下后端有个浏览器...
对于依赖数据, 目前的方案是在组件定义上提供 preFetch 函数,服务端渲染时会主动查找挂载的部分, 调用进行数据抓取:
官方的例子当中 App 是带了 Vuex 跟 vue-router 的,所以 preFetch 方案整个集成在这些库当中.从实验看, 内部嵌套的 preFetch 是不会被调用的, 只能从路由开始,同时中间要用到 Promise.all 合并请求, 脑补一下.
好吧我觉得这是一个相当简单粗暴的获取数据的办法,但其实也很难解耦, 不然就要从路由直接推算数据才行,主要觉得还是不够清晰, 限制挺多, 实际操作能犯错的地方不少.
反正比不上模板引擎
编译后大致还能看到 Virtual DOM 的影子, 会有一些性能开销,不过话说回来 Virtual DOM 本来就很慢, 能优化一点已经不容易了...
module.exports={render:function(){with(this) {
return _h('li', {
staticClass: "news-item"
}, [_h('span', {
staticClass: "score"
}, [_s(item.score)]), " ", _h('span', {
staticClass: "title"
}, [(item.url) ? [_h('a', {
"href": item.url,
"target": "_blank"
}, [_s(item.title)]), " ", _h('span', {
staticClass: "host"
}, ["(" + _s(_f("host")(item.url)) + ")"])] : [_h('router-link', {
"to": '/item/' + item.id
另外 vm.runInNewContext 有潜在的性能问题,不清楚用在生产环境是怎样, 我个人对此没有多少经验..
越来越像 React...
Vue 2 算是把这么多内容整合在一起相当不容易,不过服务端渲染 React 那么久了, 还是没普及开, 性能是大问题,相比较而言, Vue 2 增加了 cache 机制, 这可以提高性能,但是依赖数据时会带来启动 vm 开销, 要是代码量不小在么办?具体效果还是要等正式发布后, 等有权威的评测...
此外服务端抓取数据的策略需要挖一挖, 找找更漂亮的策略,我个人希望能更好地解耦, 梳理出更加清晰的依赖,那样也可以适应更多的场景, 灵活地使用, 而不是限定死了这样用.当然也是因为服务端渲染, 这个本来存在的问题显得更明确了.
<div class="like-count align-center" data-v-深入vue2.0底层思想--模板渲染 - WEB前端 - 伯乐在线
& 深入vue2.0底层思想&#8211;模板渲染
在使用vue2.0的过程,有时看API很难理解vue作者的思想,这促使我想要去深入了解vue底层的思想,了解完底层的一些思想,才能更好的用活框架,虽然网上已经有很多源码解析的文档,但我觉得只有自己动手了,才能更加深印象。
vue2.0和1.0模板渲染的区别
Vue 2.0 中模板渲染与 Vue 1.0 完全不同,1.0 中采用的 DocumentFragment (),而 2.0 中借鉴 React 的 Virtual DOM。基于 Virtual DOM,2.0 还可以支持服务端渲染(SSR),也支持 JSX 语法。
在开始阅读源码之前,先了解一些相关的知识:AST 数据结构,VNode 数据结构,createElement 的问题,render函数。
AST 数据结构
AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。而vue就是将模板代码映射为AST数据结构,进行语法解析。
我们看一下 Vue 2.0 源码中
declare type ASTNode = ASTElement | ASTText | ASTExpression
declare type ASTElement = { // 有关元素的一些定义
attrsList: Array{ name: value: string }&;
attrsMap: { [key: string]: string | null };
parent: ASTElement |
children: ArrayASTNode&;
declare type ASTExpression = {
expression:
declare type ASTText = {
<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2<div class="crayon-num crayon-striped-num" data-line="crayon-598e2<div class="crayon-num" data-line="crayon-598e2
declare type ASTNode = ASTElement | ASTText | ASTExpressiondeclare type ASTElement = { // 有关元素的一些定义&&type: 1;&&tag: string;&&attrsList: Array{ name: string; value: string }&;&&attrsMap: { [key: string]: string | null };&&parent: ASTElement | void;&&children: ArrayASTNode&;&&//......}declare type ASTExpression = {&&type: 2;&&expression: string;&&text: string;&&static?: boolean;}declare type ASTText = {&&type: 3;&&text: string;&&static?: boolean;}
我们看到 ASTNode 有三种形式:ASTElement,ASTText,ASTExpression。用属性 type 区分。
VNode数据结构
下面是 Vue 2.0 源码中
的定义 (带注释的跟下面介绍的内容有关):
constructor {
this.tag = tag
//元素标签
this.data = data
this.children = children
//子元素列表
this.text = text
this.elm = elm
//对应的真实 DOM 元素
this.ns = undefined
this.context = context
this.functionalContext = undefined
this.key = data && data.key
ponentOptions = componentOptions
ponentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false //是否被标记为静态节点
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
1234567891011121314151617181920
constructor {&&this.tag = tag&& //元素标签&&this.data = data&&//属性&&this.children = children&&//子元素列表&&this.text = text&&this.elm = elm&&//对应的真实 DOM 元素&&this.ns = undefined&&this.context = context &&this.functionalContext = undefined&&this.key = data && data.key&&this.componentOptions = componentOptions&&this.componentInstance = undefined&&this.parent = undefined&&this.raw = false&&this.isStatic = false //是否被标记为静态节点&&this.isRootInsert = true&&this.isComment = false&&this.isCloned = false&&this.isOnce = false}
真实DOM存在什么问题,为什么要用虚拟DOM
我们为什么不直接使用原生 DOM 元素,而是使用真实 DOM 元素的简化版 VNode,最大的原因就是 document.createElement 这个方法创建的真实 DOM 元素会带来性能上的损失。我们来看一个 document.createElement 方法的例子
let div = document.createElement('div');
for(let k in div) {
console.log(k);
let div = document.createElement('div');for(let k in div) {&&console.log(k);}
打开 console 运行一下上面的代码,会发现打印出来的属性多达 228 个,而这些属性有 90% 多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,关联着真实的dom,比如属性elm,只包括我们需要的属性,并新增了一些在 diff 过程中需要使用的属性,例如 isStatic。
render函数
这个函数是通过编译模板文件得到的,其运行结果是 VNode。render 函数 与 JSX 类似,Vue 2.0 中除了 Template 也支持 JSX 的写法。大家可以使用 方法编译下面这段模板。
div id="app"&
h1&I am a template!/h1&
p v-if="message"&
{{ message }}
No message.
1234567891011
div id="app"&&&header&&&&&h1&I am a template!/h1&&&/header&&&p v-if="message"&&&&&{{ message }}&&/p&&&p v-else&&&&&No message.&&/p&/div&
方法会返回一个对象,对象中有 render 和 staticRenderFns 两个值。看一下生成的 render函数
(function() {
with(this){
return _c('div',{
//创建一个 div 元素
attrs:{"id":"app"}
//div 添加属性 id
//静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数
_v(" "), //空的文本节点
(message) //三元表达式,判断 message 是否存在
//如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
?_c('p',[_v("\n
"+_s(message)+"\n
//如果不存在,创建 p 元素,元素里面有文本,值为 No message.
:_c('p',[_v("\n
No message.\n
12345678910111213141516
(function() {&&with(this){&&&&return _c('div',{&& //创建一个 div 元素&&&&&&attrs:{"id":"app"}&&//div 添加属性 id&&&&&&},[&&&&&&&&_m(0),&&//静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数&&&&&&&&_v(" "), //空的文本节点&&&&&&&&(message) //三元表达式,判断 message 是否存在&&&&&&&& //如果存在,创建 p 元素,元素里面有文本,值为 toString(message)&&&&&&&&?_c('p',[_v("\n&&&&"+_s(message)+"\n&&")])&&&&&&&&//如果不存在,创建 p 元素,元素里面有文本,值为 No message. &&&&&&&&:_c('p',[_v("\n&&&&No message.\n&&")])&&&&&&]&&&&)&&}})
要看懂上面的 render函数,只需要了解 _c,_m,_v,_s 这几个函数的定义,其中 _c 是 createElement(创建元素),_m 是 renderStatic(渲染静态节点),_v 是 createTextVNode(创建文本dom),_s 是 toString (转换为字符串)
除了 render 函数,还有一个 staticRenderFns 数组,这个数组中的函数与 VDOM 中的 diff 算法优化相关,我们会在编译阶段给后面不会发生变化的 VNode 节点打上 static 为 true 的标签,那些被标记为静态节点的 VNode 就会单独生成 staticRenderFns 函数
(function() { //上面 render 函数 中的 _m(0) 会调用这个方法
with(this){
return _c('header',[_c('h1',[_v("I'm a template!")])])
(function() { //上面 render 函数 中的 _m(0) 会调用这个方法&&with(this){&&&&return _c('header',[_c('h1',[_v("I'm a template!")])])&&}})
模板渲染过程(重要的函数介绍)
了解完一些基础知识后,接下来我们讲解下模板的渲染过程
函数,主要是获取 template,然后进入 compileToFunctions 函数。
函数,主要将 template 编译成 render 函数。首先读缓存,没有缓存就调用 compile 方法拿到 render 函数 的字符串形式,再通过 new Function 的方式生成 render 函数。
// 有缓存的话就直接在缓存里面拿
const key = options && options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
const res = {}
const compiled = compile(template, options) // compile 后面会详细讲
res.render = makeFunction(compiled.render) //通过 new Function 的方式生成 render 函数并缓存
const l = compiled.staticRenderFns.length
res.staticRenderFns = new Array(l)
for (let i = 0; i++) {
res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])
return (cache[key] = res) // 记录至缓存中
123456789101112131415161718
// 有缓存的话就直接在缓存里面拿const key = options && options.delimiters&&&&&&&&&&&&? String(options.delimiters) + template&&&&&&&&&&&&: templateif (cache[key]) {&&&&return cache[key]}const res = {}const compiled = compile(template, options) // compile 后面会详细讲res.render = makeFunction(compiled.render) //通过 new Function 的方式生成 render 函数并缓存const l = compiled.staticRenderFns.lengthres.staticRenderFns = new Array(l)for (let i = 0; i&&l; i++) {&&&&res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])}......}return (cache[key] = res) // 记录至缓存中
函数就是将 template 编译成 render 函数的字符串形式,后面一小节我们会详细讲到。
完成render方法的生成后,会进入
中进行DOM更新。该方法的核心逻辑如下:
// 触发 beforeMount 生命周期钩子
callHook(vm, 'beforeMount')
// 重点:新建一个 Watcher 并赋值给 vm._watcher
vm._watcher = new Watcher(vm, function updateComponent () {
vm._update(vm._render(), hydrating)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
1234567891011121314
// 触发 beforeMount 生命周期钩子callHook(vm, 'beforeMount')// 重点:新建一个 Watcher 并赋值给 vm._watchervm._watcher = new Watcher(vm, function updateComponent () {&&vm._update(vm._render(), hydrating)}, noop)hydrating = false// manually mounted instance, call mounted on self// mounted is called for render-created child components in its inserted hookif (vm.$vnode == null) {&&vm._isMounted = true&&callHook(vm, 'mounted')}return vm
首先会new一个watcher对象(主要是将模板与数据建立联系),在watcher对象创建后,会运行传入的方法 vm._update(vm._render(), hydrating) 。其中的vm._render()主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。vm.update() 则会对比新的 vdom 和当前 vdom,并把差异的部分渲染到真正的 DOM 树上。
推荐个图,响应式工程流程
(想深入了解watcher的背后实现原理的,可以观看这篇文章 )
上文中提到 compile 函数就是将 template 编译成 render 函数 的字符串形式。
export function compile (
template: string,
options: CompilerOptions
): CompiledResult {
const AST = parse(template.trim(), options) //1. parse
optimize(AST, options)
//2.optimize
const code = generate(AST, options) //3.generate
render: code.render,
staticRenderFns: code.staticRenderFns
12345678910111213
export function compile (&&template: string,&&options: CompilerOptions): CompiledResult {&&const AST = parse(template.trim(), options) //1. parse&&optimize(AST, options)&&//2.optimize&&const code = generate(AST, options) //3.generate&&return {&&&&AST,&&&&render: code.render,&&&&staticRenderFns: code.staticRenderFns&&}}
这个函数主要有三个步骤组成:parse,optimize 和 generate,分别输出一个包含 AST,staticRenderFns 的对象和 render函数 的字符串。
函数,主要功能是将 template字符串解析成 AST。前面定义了ASTElement的数据结构,parse 函数就是将template里的结构(指令,属性,标签等)转换为AST形式存进ASTElement中,最后解析生成AST。
函数(src/compiler/optimizer.js)主要功能就是标记静态节点,为后面 patch 过程中对比新旧 VNode 树形结构做优化。被标记为 static 的节点在后面的 diff 算法中会被直接忽略,不做详细的比较。
函数(src/compiler/codegen/index.js)主要功能就是根据 AST 结构拼接生成 render 函数的字符串。
const code = AST ? genElement(AST) : '_c("div")'
staticRenderFns = prevStaticRenderFns
onceCount = prevOnceCount
render: `with(this){return ${code}}`, //最外层包一个 with(this) 之后返回
staticRenderFns: currentStaticRenderFns
const code = AST ? genElement(AST) : '_c("div")' staticRenderFns = prevStaticRenderFnsonceCount = prevOnceCountreturn {&&&&render: `with(this){return ${code}}`, //最外层包一个 with(this) 之后返回&&&&staticRenderFns: currentStaticRenderFns}
函数(src/compiler/codegen/index.js)是会根据 AST 的属性调用不同的方法生成字符串返回。
function genElement (el: ASTElement): string {
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el)
} else if (el.once && !el.onceProcessed) {
return genOnce(el)
} else if (el.for && !el.forProcessed) {
return genFor(el)
} else if (el.if && !el.ifProcessed) {
return genIf(el)
} else if (el.tag === 'template' && !el.slotTarget) {
return genChildren(el) || 'void 0'
} else if (el.tag === 'slot') {
return code
12345678910111213141516
function genElement (el: ASTElement): string {&&if (el.staticRoot && !el.staticProcessed) {&&&&return genStatic(el)&&} else if (el.once && !el.onceProcessed) {&&&&return genOnce(el)&&} else if (el.for && !el.forProcessed) {&&&&return genFor(el)&&} else if (el.if && !el.ifProcessed) {&&&&return genIf(el)&&} else if (el.tag === 'template' && !el.slotTarget) {&&&&return genChildren(el) || 'void 0'&&} else if (el.tag === 'slot') {&&}&&&&return code&&}}
以上就是 compile 函数中三个核心步骤的介绍,compile 之后我们得到了 render 函数 的字符串形式,后面通过 new Function 得到真正的渲染函数。数据发现变化后,会执行 Watcher 中的
函数(src/core/instance/lifecycle.js),_update 函数会执行这个渲染函数,输出一个新的 VNode 树形结构的数据。然后在调用 patch 函数,拿这个新的 VNode 与旧的 VNode 进行对比,只有发生了变化的节点才会被更新到真实 DOM 树上。
patch.js 就是新旧 VNode 对比的 diff 函数,主要是为了优化dom,通过算法使操作dom的行为降到最低,diff 算法来源于 snabbdom,是 VDOM 思想的核心。snabbdom 的算法为了 DOM 操作跨层级增删节点较少的这一目标进行优化,它只会在同层级进行, 不会跨层级比较。
想更加深入VNode diff算法原理的,可以观看()
compile 函数主要是将 template 转换为 AST,优化 AST,再将 AST 转换为 render函数;
render函数 与数据通过 Watcher 产生关联;
在数据发生变化时调用 patch 函数,执行此 render 函数,生成新 VNode,与旧 VNode 进行 diff,最终更新 DOM 树。
可能感兴趣的话题
关于伯乐前端
伯乐前端分享Web前端开发,包括JavaScript,CSS和HTML5开发技术,前端相关的行业动态。
新浪微博:
推荐微信号
(加好友请注明来意)
&#8211; 好的话题、有启发的回复、值得信赖的圈子
&#8211; 分享和发现有价值的内容与观点
&#8211; 为IT单身男女服务的征婚传播平台
&#8211; 优秀的工具资源导航
&#8211; 翻译传播优秀的外文文章
&#8211; 国内外的精选文章
&#8211; UI,网页,交互和用户体验
&#8211; 专注iOS技术分享
&#8211; 专注Android技术分享
&#8211; JavaScript, HTML5, CSS
&#8211; 专注Java技术分享
&#8211; 专注Python技术分享
& 2017 伯乐在线

我要回帖

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

 

随机推荐