前言 清明放假第二天,今日早读由@携程技术中心(公众号:ctriptech)投稿。内容长有点长,需要点耐心阅读。 正文从这开始~ 随着 Backbone 等老牌框架的逐渐衰退,前端 MVC 发展缓慢,有逐渐被 MVVM/Flux 所取代的趋势。 然而,纵观近几年的发展,可以发现一点,React/Vue 和 Redux/Vuex 是分别在 MVC 中的 View 层和 Model 层做了进一步发展。如果 MVC 中的 Controller 层也推进一步,将得到一种升级版的 MVC,我们称之为 IMVC(同构 MVC)。 IMVC 可以实现一份代码在服务端和浏览器端皆可运行,具备单页应用和多页应用的所有优势,并且可以这两种模式里通过配置项进行自由切换。配合 Node.js、Webpack、Babel 等基础设施,我们可以得到相比之前更加完善的一种前端架构。 1、同构的概念和意义 1.1、isomorphic 是什么? isomorphic,读作[as'm:fk],意思是:同形的,同构的。 维基百科对它的描述是:同构是在数学对象之间定义的一类映射,它能揭示出在这些对象的属性或者操作之间存在的关系。若两个数学结构之间存在同构映射,那么这两个结构叫做是同构的。一般来说,如果忽略掉同构的对象的属性或操作的具体定义,单从结构上讲,同构的对象是完全等价的。 同构,也被化用在物理、化学以及计算机等其他领域。 1.2、isomorphic javascript isomorphic javascript(同构 js),是指一份 js 代码,既然可以跑在浏览器端,也可以跑在服务端。 同构 js 的发展历史,比 progressive web app 还要早很多。2009 年, node.js 问世,给予我们前后端统一语言的想象;更进一步的,前后端公用一套代码,也不是不可能。 有一个网站 isomorphic.net,专门收集跟同构 js 相关的文章和项目。从里面的文章列表来看,早在 2011 年的时候,业界已经开始探讨同构 js,并认为这将是未来的趋势。 可惜的是,同构 js 其实并没有得到真正意义上的发展。因为,在 2011 年,node.js 和 ECMAScript 都不够成熟,我们并没有很好的基础设施,去满足同构的目标。 现在是 2017 年,情况已经有所不同。ECMAScript 2015 标准定案,提供了一个标准的模块规范,前后端通用。尽管目前 node.js 和浏览器都没有实现 ES2015 模块标准,但是我们有 Babel 和 Webpack 等工具,可以提前享用新的语言特性带来的便利。 2、同构的种类和层次 2.1、同构的种类 同构 js 有两个种类:「内容同构」和「形式同构」。 其中,「内容同构」指服务端和浏览器端执行的代码完全等价。比如:
不管在服务端还是浏览器端,add 函数都是一样的。 而「形式同构」则不同,从原教旨主义的角度上看,它不是同构。因为,在浏览器端有一部分代码永远不会执行,而在服务端另一部分代码永远不会执行。比如:
在 npm 里,有很多 package 标榜自己是同构的,用的方式就是「形式同构」。如果不作特殊处理,「形式同构」可能会增加浏览器端加载的 js 代码的体积。比如 React,它的 140 kb 的体积,是把只在服务端运行的代码也包含了进去。 2.2、同构的层次 同构不是一个布尔值,true 或者 false;同构是一个光谱形态,可以在很小范围里上实现同构,也可以在很大范围里实现同构。 function 层次:零碎的代码片断或者函数,支持同构。比如浏览器端和服务端都实现了 setTimeout 函数,比如 lodash/underscore 的工具函数都是同构的。 feature 层次:在这个层次里的同构代码,通常会承担一定的业务职能。比如 React 和 Vue 都借助 virtual-dom 实现了同构,它们是服务于 View 层的渲染;比如 Redux 和 Vuex 也是同构的,它们负责 Model 层的数据处理。 framework 层次:在框架层面实现同构,它可能包含了所有层次的同构,需要精心处理支持同构和不支持同构的两个部分,如何妥善地整合在一起。 我们今天所讨论的 isomorphic-mvc(简称 IMVC),是在 framework 层次上实现同构。 3、同构的价值和作用 3.1、同构的价值 同构 js,不仅仅有抽象上的美感,它还有很多实用价值。 SEO 友好:View 层在浏览器端和服务端都可以运行,意味着可以在服务端吐出 html,支持搜索引擎的抓取。 加快访问体验:服务端渲染可以加快浏览器端的首次访问的渲染速度,而浏览器端渲染,可以加快用户交互时的反馈速度。 代码的可维护性:同构可以减少语言切换的成本,减小代码的重复率,增加代码的可维护性。 不使用同构方案,也可以用别的办法实现前两个的目标,但是别的办法却难以同时满足三个目标。 3.2、同构如何加快访问体验 纯浏览器端渲染的问题在于,页面需要等待 js 加载完毕之后,才可见。 服务端渲染可以加速首次访问的体验,在 js 加载之前,页面就渲染了首屏。但是,用户只对首次加载有耐心,如果操作过程中,频繁刷新页面,也会带给用户缓慢的感觉。 SERVER-SIDE RENDERING 同构渲染则可以得到两种好处,在首次加载时用服务端渲染,在交互过程中则采取浏览器端渲染。 3.3、同构是未来的趋势 从历史发展的角度看,同构确实是未来的一大趋势。 在 Web 开发的早期,采用的开发模式是:fat-server, thin-client 前端只是薄薄的一层,负责一些表单验证,DOM 操作和 JS 动画。在这个阶段,没有「前端工程师」这个工种,服务端开发顺便就把前端代码给写了。 在 Ajax 被发掘出来之后,Web 进入 2.0 时代,我们普遍推崇的模式是:thin-server, fat-client 越来越多的业务逻辑,从服务端迁移到前端。开始有「前后端分离」的做法,前端希望服务端只提供 restful 接口和数据持久化。 但是在这个阶段,做得不够彻底。前端并没有完全掌控渲染层,起码 html 骨架需要服务端渲染,以及前端实现不了服务端渲染。 为了解决上述问题,我们正在进入下一个阶段,这个阶段所采取的模式是:shared, fat-server, fat-client 通过 node.js 运行时,前端完全掌控渲染层,并且实现渲染层的同构。既不牺牲服务端渲染的价值,也不放弃浏览器端渲染的便利。 这就是未来的趋势。 4、同构的实现策略 要实现同构,首先要正视一点,全盘同构是没有意义的。为什么? 服务端和浏览器端毕竟是两个不同的平台和环境,它们专注于解决不同的问题,有自身的特点,全盘同构就抹杀了它们固有的差异,也就无法发挥它们各自的优势。 因而,我们只会在 client 和 server 有交集的部分实现同构。就是在服务端渲染 html 和在浏览器端复用 html 的整个过程里,实现同构。 我们采取的主要做法有两个:1)能够同构的代码,直接复用;2)无法同构的代码,封装成形式同构。 举几个例子。 获取 User-Agent 字符串。 我们可以在服务端用 req.get('user-agent') 模拟出 navigator 全局对象,也可以提供一个 getUserAgent 的方法或函数。 获取 Cookies。 Cookies 处理在我们的场景里,存在快捷通道,因为我们只专注首次渲染的同构,其它的操作可以放在浏览器端二次渲染的时候再处理。 Cookies 的主要用途发生在 ajax 请求的时候,在浏览器端 ajax 请求可以设置为自动带上 Cookies,所以只需要在服务端默默地在每个 ajax 请求头里补上 Cookies 即可。 Redirects 重定向处理 重定向的场景比较复杂,起码有三种情况:
我们需要封装一个 redirect 函数,根据输入的 url 和环境信息,选择正确的重定向方式。 5、IMVC 架构 5.1、IMVC 的目标 IMVC 的目标是框架层面的同构,我们要求它必须实现以下功能
有些功能属于运行时的,有些功能则只服务于开发环境。JavaScript 虽然是一门解释型语言,但前端行业发展到现阶段,它的开发模式已经变得非常丰富,既可以用最朴素的方式,一个记事本加上一个浏览器,也可以用一个 IDE 加上一系列开发、测试和部署流程的支持。 5.2、IMVC 的技术选型 Router: create-app = history path-to-regexp View: React = renderToDOM || renderToString Model: relite = redux-like library Ajax: isomorphic-fetch 理论上,IMVC 是一种架构思路,它并不限定我们使用哪些技术栈。不过,要使 IMVC 落地,总得做出选择。上面就是我们当前选择的技术栈,将来它们可能升级或者替换为其它技术。 5.3、为什么不直接用 React 全家桶? 大家可能注意到,我们使用了许多 React 相关的技术,但却不是所谓的 React 全家桶,原因如下:
目前的全家桶,只是社区里的一些热门库的组合罢了。Facebook 真正用的全家桶是 react|flux|relay|graphql,甚至他们并不用 React 做服务端渲染,用的是 PHP。 我们认为 React-Router 的理念在同构上是错误的。它忽视了一个重大事实:服务端是 Router 路由驱动的,把 Router 和作为 View 的 React 捆绑起来,View 已经实例化了,Router 怎么再加载 Controller 或者异步请求数据呢? 从函数式编程的角度看,React 推崇纯组件,需要隔离副作用,而 Router 则是副作用来源,将两者混合在一起,是一种污染。另外,Router 并不是 UI,却被写成 JSX 组件的形式,这也是有待商榷的。 所以,即便是当前最新版的 React-Router-v4,实现同构渲染时,做法也复杂而臃肿,服务端和浏览器端各有一个路由表和发 ajax 请求的逻辑。点击这里查看代码 至于 Redux,其作者也已在公开场合表示:「你可能不需要 Redux」。在引入 redux 时,我们得先反思一下引入的必要性。 毫无疑问,Redux 的模式是优秀的,结构清晰,易维护。然而同时它也是繁琐的,实现一个功能,你可能得跨文件夹地操作数个文件,才能完成。这些代价所带来的显著好处,要在 app 复杂到一定程度时,才能真正体会。其它模式里,app 复杂到一定程度后,就难以维护了;而 Redux 的可维护性还依然坚挺,这就是其价值所在。(值得一提的是,基于 redux 再封装一层简化的 API,我认为这很可能是错误的做法。Redux 的源码很简洁,意图也很明确,要简化固然也是可以的,但它为什么自己不去做?它是不是刻意这样设计呢?你的封装是否损害了它的设计目的呢?) 在使用 Redux 之前要考虑的是,我们 web-app 属于大型应用的范畴吗? 前端领域日新月异,框架和库的频繁升级让开发者应接不暇。我们需要根据自身的需求,进行二次封装,得到一组更简洁的 API,将部分复杂度隐藏起来,以降低学习成本。 5.4、用 create-app 代替 react-router create-app 是我们为了同构而实现的一个 library,它由下面三部分组成:
create-app 复用 React-Router 的依赖 history.js,用以在浏览器端管理 history 状态;复用 expressjs 的 path-to-regexp,用以从 path pattern 中解析参数。 我们认为,React 和 Redux 分别对应 MVC 的 View 和 Model,它们都是同构的,我们需要的是实现 Controller 层的同构。 5.4.1、create-app 的同构理念 create-app 实现同构的方式是:
上述过程在服务端和浏览器端都保持一致。 5.4.2、create-app 的配置理念 服务端和浏览器端加载模块的方式不同,服务端是同步加载,而浏览器端则是异步加载;它们的 view-engine 也是不同的。如何处理这些不一致? 答案是配置。
服务端和浏览器端分别有自己的入口文件:client-entry.js 和 server.entry.js。我们只需提供不同的配置即可。 在服务端,加载 controller 模块的方式是 commonjsLoader;在浏览器端,加载 controller 模块的方式则为 webpackLoader。 在服务端和浏览器端,view-engine 也被配置为不同的 ReactDOM 和 ReactDOMServer。 每个 controller 实例,都有 context 参数,它也是来自配置。通过这种方式,我们可以在运行时注入不同的平台特性。这样既分割了代码,又实现了形式同构。 5.4.3、create-app 的服务端渲染 我们认为,简洁的,才是正确的。create-app 实现服务端渲染的代码如下:
|
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|