同构应用,pwa重构新加坡大巴线路图

我的前端之路:工具化与工程化

2017/01/07 · 基础技术 ·
工具化,
工程化

原文出处:
王下邀月熊_Chevalier   

图片 1

React 同构应用 PWA 升级指南

2018/05/25 · JavaScript
· PWA,
React

原文出处:
林东洲   

pwa重构上海地铁线路图

2018/03/28 · JavaScript
· PWA

原文出处:
Neal   

之前一直有在维护一个上海地铁线路图的 pwa,最主要的特性就是 “offline
first”。但是由于代码都是通过原生的 js
去实现,之前我都不是很喜欢去用框架,不想具有任何框架的偏好。但是到后期随着代码量的增加,代码的确变得混乱不堪,拓展新功能也变得尤为困难。因此,花了将近两个礼拜的时候对于应用进行了一次完整的重构。网站访问地址:

前言

前言

最近在给我的博客网站 PWA 升级,顺便就记录下 React 同构应用在使用 PWA
时遇到的问题,这里不会从头开始介绍什么是 PWA,如果你想学习 PWA
相关知识,可以看下下面我收藏的一些文章:

  • 您的第一个 Progressive Web
    App
  • 【Service
    Worker】生命周期那些事儿
  • 【PWA学习与实践】(1)
    2018,开始你的PWA学习之旅
  • Progressive Web Apps (PWA)
    中文版

准备

准备工作先做好,在 vue 和 react 之间,我还是选择了后者。基于
create-react-app
来搭建环境,crp 为你准备了一个开箱即用的开发环境,因此你无需自己亲手配置
webpack,因此你也不需要成为一名 webpack 配置工程师了。

另外一方面,我们还需要一些数据,包括站点信息,线路路径,文字说明等等。基于之前的应用,可以通过一小段的代码获取信息。就此如要我们获取我们以前的站点在
svg 图中的相关属性,普通的站点使用 circle 元素,为了获取其属性:

const circles = document.querySelectorAll(‘circle’); let result = [];
circles.forEach(circle => { let ele = { cx: circle.cx, cy: circle.cy,
sroke: circle.stroke, id: circle.id }; result.push(ele); }) const str =
JSON.stringify(result);

1
2
3
4
5
6
7
8
9
10
11
12
13
const circles = document.querySelectorAll(‘circle’);
let result = [];
circles.forEach(circle => {
  let ele = {
    cx: circle.cx,
    cy: circle.cy,
    sroke: circle.stroke,
    id: circle.id
  };
  result.push(ele);
})
const str = JSON.stringify(result);
 

通过这样的代码我们就可以获取 svg
普通站点信息,同理还可获取中转站信息,线路路径信息以及站点以及线路 label
信息。还有,我们还需要获取每个站点的时刻表信息,卫生间位置信息,无障碍电梯信息以及出入口信息。这里是写了一些爬虫去官网爬取并做了一些数据处理,再次就不一一赘述。

二十载光辉岁月

图片 2

近年来,随着浏览器性能的提升与移动互联网浪潮的汹涌而来,Web前端开发进入了高歌猛进,日新月异的时代。这是最好的时代,我们永远在前行,这也是最坏的时代,无数的前端开发框架、技术体系争妍斗艳,让开发者们陷入困惑,乃至于无所适从。Web前端开发可以追溯于1991年蒂姆·伯纳斯-李公开提及HTML描述,而后1999年W3C发布HTML4标准,这个阶段主要是BS架构,没有所谓的前端开发概念,网页只不过是后端工程师的顺手之作,服务端渲染是主要的数据传递方式。接下来的几年间随着互联网的发展与REST等架构标准的提出,前后端分离与富客户端的概念日渐为人认同,我们需要在语言与基础的API上进行扩充,这个阶段出现了以jQuery为代表的一系列前端辅助工具。2009年以来,智能手机开发普及,移动端大浪潮势不可挡,SPA单页应用的设计理念也大行其道,相关联的前端模块化、组件化、响应式开发、混合式开发等等技术需求甚为迫切。这个阶段催生了Angular
1、Ionic等一系列优秀的框架以及AMD、CMD、UMD与RequireJS、SeaJS等模块标准与加载工具,前端工程师也成为了专门的开发领域,拥有独立于后端的技术体系与架构模式。而近两年间随着Web应用复杂度的提升、团队人员的扩充、用户对于页面交互友好与性能优化的需求,我们需要更加优秀灵活的开发框架来协助我们更好的完成前端开发。这个阶段涌现出了很多关注点相对集中、设计理念更为优秀的框架,譬如React、VueJS、Angular
2等组件框架允许我们以声明式编程来替代以DOM操作为核心的命令式编程,加快了组件的开发速度,并且增强了组件的可复用性与可组合性。而遵循函数式编程的Redux与借鉴了响应式编程理念的MobX都是非常不错的状态管理辅助框架,辅助开发者将业务逻辑与视图渲染剥离,更为合理地划分项目结构,更好地贯彻单一职责原则与提升代码的可维护性。在项目构建工具上,以Grunt、Gulp为代表的任务运行管理与以Webpack、Rollup、JSPM为代表的项目打包工具各领风骚,帮助开发者更好的搭建前端构建流程,自动化地进行预处理、异步加载、Polyfill、压缩等操作。而以NPM/Yarn为代表的依赖管理工具一直以来保证了代码发布与共享的便捷,为前端社区的繁荣奠定了重要基石。

PWA 特性

PWA 不是单纯的某项技术,而是一堆技术的集合,比如:Service
Worker,manifest 添加到桌面,push、notification api 等。

而就在前不久时间,IOS 11.3 刚刚支持 Service worker 和类似 manifest
添加到桌面的特性,所以这次 PWA
改造主要还是实现这两部分功能,至于其它的特性,等 iphone 支持了再升级吧。

设计

数据准备好之后,就是应用的设计了。首先,对组件进行一次拆分:

纷扰之虹

笔者在前两天看到了Thomas
Fuchs的一则Twitter,也在Reddit等社区引发了热烈的讨论:我们用了15年的时间来分割HTML、JS与CSS,然而一夕之间事务仿佛回到了原点。
图片 3分久必合,合久必分啊,无论是前端开发中各个模块的分割还是所谓的前后端分离,都不能形式化的单纯按照语言或者模块来划分,还是需要兼顾功能,合理划分。笔者在2015-我的前端之路:数据流驱动的界面中对自己2015的前端感受总结中提到过,任何一个编程生态都会经历三个阶段,第一个是原始时期,由于需要在语言与基础的API上进行扩充,这个阶段会催生大量的Tools。第二个阶段,随着做的东西的复杂化,需要更多的组织,会引入大量的设计模式啊,架构模式的概念,这个阶段会催生大量的Frameworks。第三个阶段,随着需求的进一步复杂与团队的扩充,就进入了工程化的阶段,各类分层MVC,MVP,MVVM之类,可视化开发,自动化测试,团队协同系统。这个阶段会出现大量的小而美的Library。在2016的上半年中,笔者在以React的技术栈中挣扎,也试用过VueJS与Angular等其他优秀的前端框架。在这一场从直接操作DOM节点的命令式开发模式到以状态/数据流为中心的开发模式的工具化变革中,笔者甚感疲惫。在2016的下半年中,笔者不断反思是否有必要使用React/Redux/Webpack/VueJS/Angular,是否有必要去不断追逐各种刷新Benchmark
记录的新框架?本文定名为工具化与工程化,即是代表了本文的主旨,希望能够尽可能地脱离工具的束缚,回归到前端工程化的本身,回归到语言的本身,无论React、AngularJS、VueJS,它们更多的意义是辅助开发,为不同的项目选择合适的工具,而不是执念于工具本身。

总结而言,目前前端工具化已经进入到了非常繁荣的时代,随之而来很多前端开发者也甚为苦恼,疲于学习。工具的变革会非常迅速,很多优秀的工具可能都只是历史长河中的一朵浪花,而蕴藏其中的工程化思维则会恒久长存。无论你现在使用的是React还是Vue还是Angular
2或者其他优秀的框架,都不应该妨碍我们去了解尝试其他,笔者在学习Vue的过程中感觉反而加深了自己对于React的理解,加深了对现代Web框架设计思想的理解,也为自己在未来的工作中更自由灵活因地制宜的选择脚手架开阔了视野。

引言的最后,我还想提及一个词,算是今年我在前端领域看到的出镜率最高的一个单词:Tradeoff(妥协)。

Service Worker

service worker
在我看来,类似于一个跑在浏览器后台的线程,页面第一次加载的时候会加载这个线程,在线程激活之后,通过对
fetch 事件,可以对每个获取的资源进行控制缓存等。

组件结构

将整个地图理解成一个 Map 组件,再将其分为 4 个小组件:

图片 4

  • Label: 地图上的文本信息,包括地铁站名,线路名称
  • Station: 地铁站点,包括普通站点和中转站点
  • Line: 地铁线路
  • InfoCard:
    状态最复杂的一个组件,主要包含时刻表信息、卫生间位置信息、出入口信息、无障碍电梯信息

这是一个大致的组件划分,里面可能包含更多的其它元素,比如 InfoCard 就有
InfoCard => TimeSheet => TimesheetTable 这样的嵌套。

工具化

图片 5

月盈而亏,过犹不及。相信很多人都看过了2016年里做前端是怎样一种体验这篇文章,2016年的前端真是让人感觉从入门到放弃,我们学习的速度已经跟不上新框架新概念涌现的速度,用于学习上的成本远大于实际开发项目的成本。不过笔者对于工具化的浪潮还是非常欢迎的,我们不一定要去用最新最优秀的工具,但是我们有了更多的选择余地,相信这一点对于大部分非处女座人士而言都是喜讯。年末还有一篇曹刘阳:2016年前端技术观察也引发了大家的热议,老实说笔者个人对文中观点认同度一半对一半,不想吹也不想黑。不过笔者看到这篇文章的第一感觉当属作者肯定是大公司出来的。文中提及的很多因为技术负债引发的技术选型的考虑、能够拥有相对充分完备的人力去进行某个项目,这些特征往往是中小创公司所不会具备的。

明确哪些资源需要被缓存?

那么在开始使用 service worker 之前,首先需要清楚哪些资源需要被缓存?

组件通信和状态管理

本地开发的最大的难点应该就是这一块的内容了。本来由于组件的层级并不算特别复杂,所以我并不打算上
Redux
这种类型的全局状态管理库。主要组件之间的通信就是父子通信和兄弟组件通信。父子组件通信比较简单,父组件的
state 即为子组件的
props,可以通过这个实现父子组件通信。兄弟组件略为复杂,兄弟组件通过共享父组件的状态来进行通信。假如这样的情景,我点击站点,希望能够弹出信息提示窗,这就是
Station 组件和 InfoCard 组件之间的通信,通过 Map 组件来进行共享。点击
Station 组件触发事件,通过回调更新 Map 组件状态的更新,同时也实现了
InfoCard
组件的更新。同时为了实现,点击其它区域就可以关闭信息提示窗,我们对 Map
组件进行监听,监听事件的冒泡来实现高效的关闭,当然为了避免一些不必要的冒泡,还需要在一些事件处理中阻止事件冒泡。

图片 6

InfoCard 是最为复杂的一个组件,因为里面包含了好几个
icon,以及状态信息的切换,同时需要实现切换不同的站点的时候能够更新信息提示窗。需要注意信息提示窗信息初次点击信息的初始化,以及切换不同
icon
时分别显示不同的信息,比如卫生间信息或者出入口信息,以及对于时刻表,切换不同的线路的时候更新对应的时刻表。这些状态的转化,需要值得注意。另外值得一题的点就是,在切换不同站点的时候的状态,假如我正在看某个站点的卫生间信息的时候,我点击另外一个站点,这时候弹出的信息提示窗应该是时刻表信息还是卫生间信息呢?我的选择还是卫生间信息,我对于这一状态进行了保持,这样的用户体验从逻辑上来讲似乎更佳。具体实现的代码细节就不一一说明了,里面肯能包含更多的细节,欢迎使用体验。

工具化的意义

工具化是有意义的。笔者在这里非常赞同尤雨溪:Vue
2.0,渐进式前端解决方案的思想,工具的存在是为了帮助我们应对复杂度,在技术选型的时候我们面临的抽象问题就是应用的复杂度与所使用的工具复杂度的对比。工具的复杂度是可以理解为是我们为了处理问题内在复杂度所做的投资。为什么叫投资?那是因为如果投的太少,就起不到规模的效应,不会有合理的回报。这就像创业公司拿风投,投多少是很重要的问题。如果要解决的问题本身是非常复杂的,那么你用一个过于简陋的工具应付它,就会遇到工具太弱而使得生产力受影响的问题。反之,是如果所要解决的问题并不复杂,但你却用了很复杂的框架,那么就相当于杀鸡用牛刀,会遇到工具复杂度所带来的副作用,不仅会失去工具本身所带来优势,还会增加各种问题,例如培训成本、上手成本,以及实际开发效率等。

图片 7

笔者在GUI应用程序架构的十年变迁:MVC,MVP,MVVM,Unidirectional,Clean一文中谈到,所谓GUI应用程序架构,就是对于富客户端的代码组织/职责划分。纵览这十年内的架构模式变迁,大概可以分为MV*与Unidirectional两大类,而Clean
Architecture则是以严格的层次划分独辟蹊径。从笔者的认知来看,从MVC到MVP的变迁完成了对于View与Model的解耦合,改进了职责分配与可测试性。而从MVP到MVVM,添加了View与ViewModel之间的数据绑定,使得View完全的无状态化。最后,整个从MV*到Unidirectional的变迁即是采用了消息队列式的数据流驱动的架构,并且以Redux为代表的方案将原本MV*中碎片化的状态管理变为了统一的状态管理,保证了状态的有序性与可回溯性。
具体到前端的衍化中,在Angular
1兴起的时代实际上就已经开始了从直接操作Dom节点转向以状态/数据流为中心的变化,jQuery
代表着传统的以 DOM 为中心的开发模式,但现在复杂页面开发流行的是以 React
为代表的以数据/状态为中心的开发模式。应用复杂后,直接操作 DOM
意味着手动维护状态,当状态复杂后,变得不可控。React
以状态为中心,自动帮我们渲染出 DOM,同时通过高效的 DOM Diff
算法,也能保证性能。

缓存静态资源

首先是像 CSS、JS 这些静态资源,因为我的博客里引用的脚本样式都是通过 hash
做持久化缓存,类似于:main.ac62dexx.js 这样,然后开启强缓存,这样下次用户下次再访问我的网站的时候就不用重新请求资源。直接从浏览器缓存中读取。对于这部分资源,service
worker 没必要再去处理,直接放行让它去读取浏览器缓存即可。

我认为如果你的站点加载静态资源的时候本身没有开启强缓存,并且你只想通过前端去实现缓存,而不需要后端在介入进行调整,那可以使用
service worker 来缓存静态资源,否则就有点画蛇添足了。

性能优化

以上这些的开发得益于之前的维护,所以重构过程还是比较快的,稍微熟悉了下
react 的用法就完成了重构。但是,在上线之后使用 lighthouse
做分析,performan 的得分是 0 分。首屏渲染以及可交互得分都是 0
分,首先来分析一下。因为整个应用都是通过 js 来渲染,而最为核心的就是那个
svg。整个看下来,有几点值得注意:

  • 代码直接将 json 导入,导致 js 体积过大
  • 所有组件都在渲染的时候进行加载

找到问题点,就可以想到一些解决方案了。第一个比较简单,压缩 json
数据,去除一些不需要的信息。第二个,好的解决办法就是通过异步加载来实现组件加载,效果明显,尤其是对于
InfoCard 组件:

工具化的不足:抽象漏洞定理

抽象漏洞定理是Joel在2002年提出的,所有不证自明的抽象都是有漏洞的。抽象泄漏是指任何试图减少或隐藏复杂性的抽象,其实并不能完全屏蔽细节,试图被隐藏的复杂细节总是可能会泄漏出来。抽象漏洞法则说明:任何时候一个可以提高效率的抽象工具,虽然节约了我们工作的时间,但是,节约不了我们的学习时间。我们在上一章节讨论过工具化的引入实际上以承受工具复杂度为代价消弭内在复杂度,而工具化滥用的结局即是工具复杂度与内在复杂度的失衡

谈到这里我们就会明白,不同的项目具备不同的内在复杂度,一刀切的方式评论工具的好坏与适用简直耍流氓,而且我们不能忽略项目开发人员的素质、客户或者产品经理的素质对于项目内在复杂度的影响。对于典型的小型活动页,譬如某个微信H5宣传页,往往注重于交互动画与加载速度,逻辑复杂度相对较低,此时Vue这样渐进式的复杂度较低的库就大显身手。而对于复杂的Web应用,特别是需要考虑多端适配的Web应用,笔者会倾向于使用React这样相对规范严格的库。

缓存页面

缓存页面显然是必要的,这是最核心的部分,当你在离线的状态下加载页面会之后出现:

图片 8

究其原因就是因为你在离线状态下没办法加载页面,现在有了 service
worker,即使你在没网络的情况下,也可以加载之前缓存好的页面了。

同步

class InfoCard extends React.Component { constructor(props) {  
 super(props) { …    }  }  … }

1
2
3
4
5
6
7
8
9
class InfoCard extends React.Component {
  constructor(props) {
   super(props) {
    …
   }
 }
 …
}
 

React?Vue?Angular 2?

图片 9

笔者最近翻译过几篇盘点文,发现很有趣的一点,若文中不提或没夸Vue,则一溜的评论:垃圾文章,若文中不提或没夸Angular
2,则一溜的评论:垃圾文章。估计若是笔者连React也没提,估计也是一溜的评论:垃圾文章。好吧,虽然可能是笔者翻译的确实不好,玷污了原文,但是这种戾气笔者反而觉得是对于技术的不尊重。React,Vue,Angular
2都是非常优秀的库与框架,它们在不同的应用场景下各自具有其优势,本章节即是对笔者的观点稍加阐述。Vue最大的优势在于其渐进式的思想与更为友好的学习曲线,Angular
2最大的优势其兼容并包形成了完整的开箱即用的All-in-one框架,而这两点优势在某些情况下反而也是其劣势,也是部分人选用React的理由。笔者觉得很多对于技术选型的争论乃至于谩骂,不一定是工具的问题,而是工具的使用者并不能正确认识自己或者换位思考他人所处的应用场景,最终吵的驴唇不对马嘴。

缓存后端接口数据

缓存接口数据是需要的,但也不是必须通过 service worker
来实现,前端存放数据的地方有很多,比如通过 localstorage,indexeddb
来进行存储。这里我也是通过 service worker
来实现缓存接口数据的,如果想通过其它方式来实现,只需要注意好 url
路径与数据对应的映射关系即可。

异步

export default function asyncInfoCard (importComp) { class InfoCard
extends React.Component {    constructor(props) { super(props);
this.state = { component: null }; } asyncComponentDidMount() { const {
default: component } = await importComp(); this.setState({ component:
component })    }  } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function asyncInfoCard (importComp) {
  class InfoCard extends React.Component {
   constructor(props) {
      super(props);
      this.state = {
        component: null
      };
    }
    
    asyncComponentDidMount() {
      const { default: component } = await importComp();
      this.setState({
        component: component
      })
   }
 }
}
 

这样我们就实现了将同步组件改造成一个异步加载的组件,这样就无需一下子加载所有的组件。这样我们就可以在
Map 中使用异步的方式来进行组件的加载:

import asyncInfoCard from ‘./InfoCard’ const InfoCard = asyncInfoCard(()
=> import(‘./InfoCard’)

1
2
3
import asyncInfoCard from ‘./InfoCard’
const InfoCard = asyncInfoCard(() => import(‘./InfoCard’)
 

通过上线之后的性能分析,lighthouse 性能评分一下子就上升到了 80
多分,证明这样的改进还是比较有效的。另外一个值得提的点就是首屏,因为历史原因,整张图
svg 中元素的位置都是定死的,及横坐标和纵坐标都已经是定义好的,而 svg
被定为在中间。在移动端加载时,呈现的就是左边的空白区域,所以给用户一种程序未加载完毕的错觉。之前的版本的做法就是通过
scroll 来实现滚动条的滚动,将视图的焦点移动到中间位置。这次的想法是通过
transform 来实现:

.svg { transform: translate(-100px, -300px) }

1
2
3
.svg {
transform: translate(-100px, -300px)
}

这样实现了整个 svg 图位置的偏移,使用 lighthouse 进行分析,性能分降到了
70
多分。继续想想有没有其他的方法,后来我想在最左上上角定义一个箭头动画。

img src=”right_arrow.png” alt=”right arrow” title=”right arrow”
class=”right-arrow”/>

1
img src="right_arrow.png" alt="right arrow" title="right arrow" class="right-arrow"/>

.right-arrow { animation: moveright 3s linear infinite; } @keyframs
moveright { 0% { transform: translateX(2rem); } 50% { transform:
translateX(3rem); } 100% { transform: translateX(5rem); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.right-arrow {
  animation: moveright 3s linear infinite;
}
@keyframs moveright {
  0% {
    transform: translateX(2rem);
  }
  50% {
    transform: translateX(3rem);
  }
  100% {
    transform: translateX(5rem);
  }
}

图片 10

这样我们就可以创建一个循环向右移动的动画,提示用户向右滑动。部署之后发现性能分立马降到
0,索性也就放弃了这个做法。最后来时决定采用
transform: translateX(-200px) translateY(-300px); ,因为这样通过 css3
的属性可以在一些移动设备上还可以利用 GPU 加速,并且 translateX
不会引起页面的重绘或者重排,只会导致图层重组,最小避免对性能的影响。

小而美的视图层

React 与 VueJS 都是所谓小而美的视图层Library,而不是Angular
2这样兼容并包的Frameworks。任何一个编程生态都会经历三个阶段,第一个是原始时期,由于需要在语言与基础的API上进行扩充,这个阶段会催生大量的Tools。第二个阶段,随着做的东西的复杂化,需要更多的组织,会引入大量的设计模式啊,架构模式的概念,这个阶段会催生大量的Frameworks。第三个阶段,随着需求的进一步复杂与团队的扩充,就进入了工程化的阶段,各类分层MVC,MVP,MVVM之类,可视化开发,自动化测试,团队协同系统。这个阶段会出现大量的小而美的Library。
React
并没有提供很多复杂的概念与繁琐的API,而是以最少化为目标,专注于提供清晰简洁而抽象的视图层解决方案,同时对于复杂的应用场景提供了灵活的扩展方案,典型的譬如根据不同的应用需求引入MobX/Redux这样的状态管理工具。React在保证较好的扩展性、对于进阶研究学习所需要的基础知识完备度以及整个应用分层可测试性方面更胜一筹。不过很多人对React的意见在于其陡峭的学习曲线与较高的上手门槛,特别是JSX以及大量的ES6语法的引入使得很多的传统的习惯了jQuery语法的前端开发者感觉学习成本可能会大于开发成本。与之相比Vue则是典型的所谓渐进式库,即可以按需渐进地引入各种依赖,学习相关地语法知识。比较直观的感受是我们可以在项目初期直接从CDN中下载Vue库,使用熟悉的脚本方式插入到HTML中,然后直接在script标签中使用Vue来渲染数据。随着时间的推移与项目复杂度的增加,我们可以逐步引入路由、状态管理、HTTP请求抽象以及可以在最后引入整体打包工具。这种渐进式的特点允许我们可以根据项目的复杂度而自由搭配不同的解决方案,譬如在典型的活动页中,使用Vue能够兼具开发速度与高性能的优势。不过这种自由也是有利有弊,所谓磨刀不误砍材工,React相对较严格的规范对团队内部的代码样式风格的统一、代码质量保障等会有很好的加成。
一言蔽之,笔者个人觉得Vue会更容易被纯粹的前端开发者的接受,毕竟从直接以HTML布局与jQuery进行数据操作切换到指令式的支持双向数据绑定的Vue代价会更小一点,特别是对现有代码库的改造需求更少,重构代价更低。而React及其相对严格的规范可能会更容易被后端转来的开发者接受,可能在初学的时候会被一大堆概念弄混,但是熟练之后这种严谨的组件类与成员变量/方法的操作会更顺手一点。便如Dan
Abramov所述,Facebook推出React的初衷是为了能够在他们数以百计的跨平台子产品持续的迭代中保证组件的一致性与可复用性。

缓存策略

明确了哪些资源需要被缓存后,接下来就要谈谈缓存策略了。

部署

目前的部署方案是采取 create-react-app 的官方建议,通过 gh-pages 实现将
build 的打包文件上传到 gh-pages 分支上从而实现部署。

函数式思维:抽象与直观

近年来随着应用业务逻辑的日益复杂与并发编程的大规模应用,函数式编程在前后端都大放异彩。软件开发领域有一句名言:可变的状态是万恶之源,函数式编程即是避免使用共享状态而避免了面向对象编程中的一些常见痛处。不过老实说笔者并不想一味的推崇函数式编程,在下文关于Redux与MobX的讨论中,笔者也会提及函数式编程不可避免地会使得业务逻辑支离破碎,反而会降低整个代码的可维护性与开发效率。与React相比,Vue则是非常直观的代码架构,每个Vue组件都包含一个script标签,这里我们可以显式地声明依赖,声明操作数据的方法以及定义从其他组件继承而来的属性。而每个组件还包含了一个template标签,等价于React中的render函数,可以直接以属性方式绑定数据。最后,每个组件还包含了style标签而保证了可以直接隔离组件样式。我们可以先来看一个典型的Vue组件,非常直观易懂,而两相比较之下也有助于理解React的设计思想。

XHTML

<script> export default { components: {}, data() { return { notes:
[], }; }, created() { this.fetchNotes(); }, methods: { addNote(title,
body, createdAt, flagged) { return database(‘notes’).insert({ title,
body, created_at: createdAt, flagged }); }, }; </script>
<template> <div class=”app”> <header-menu
:addNote=’addNote’ > </div> </template> <style
scoped> .app { width: 100%; height: 100%; postion: relative; }
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script>
export default {
  components: {},
  data() {
    return {
      notes: [],
    };
  },
  created() {
    this.fetchNotes();
  },
  methods: {
    addNote(title, body, createdAt, flagged) {
     return database(‘notes’).insert({ title, body, created_at: createdAt, flagged });
  },
};
</script>
<template>
  <div class="app">
    <header-menu
      :addNote=’addNote’
      >
  </div>
</template>
<style scoped>
  .app {
    width: 100%;
    height: 100%;
    postion: relative;
  }
</style>

当我们将视角转回到React中,作为单向数据绑定的组件可以抽象为如下渲染函数:

JavaScript

View = f(Data)

1
View = f(Data)

这种对用户界面的抽象方式确实令笔者耳目一新,这样我们对于界面的组合搭配就可以抽象为对于函数的组合,某个复杂的界面可以解构为数个不同的函数调用的组合变换。0.14版本时,React放弃了MixIn功能,而推荐使用高阶函数模式进行组件组合。这里很大一个考虑便是Mixin属于面向对象编程,是多重继承的一种实现,而函数式编程里面的Composition(合成)可以起到同样的作用,并且能够保证组件的纯洁性而没有副作用。

很多人第一次学习React的时候都会觉得JSX语法看上去非常怪异,这种背离传统的HTML模板开发方式真的靠谱吗?(在2.0版本中Vue也引入了JSX语法支持)。我们并不能单纯地将JSX与传统的HTML模板相提并论,JSX本质上是对于React.createElement函数的抽象,而该函数主要的作用是将朴素的JavaScript中的对象映射为某个DOM表示。其大概思想图示如下:
图片 11

在现代浏览器中,对于JavaScript的计算速度远快于对DOM进行操作,特别是在涉及到重绘与重渲染的情况下。并且以JavaScript对象代替与平台强相关的DOM,也保证了多平台的支持,譬如在ReactNative的协助下我们很方便地可以将一套代码运行于iOS、Android等多平台。总结而言,JSX本质上还是JavaScript,因此我们在保留了JavaScript函数本身在组合、语法检查、调试方面优势的同时又能得到类似于HTML这样声明式用法的便利与较好的可读性。

页面缓存策略

因为是 React
单页同构应用,每次加载页面的时候数据都是动态的,所以我采取的是:

  1. 网络优先的方式,即优先获取网络上最新的资源。当网络请求失败的时候,再去获取
    service worker 里之前缓存的资源
  2. 当网络加载成功之后,就更新 cache
    中对应的缓存资源,保证下次每次加载页面,都是上次访问的最新资源
  3. 如果找不到 service worker 中 url 对应的资源的时候,则去获取 service
    worker 对应的 /index.html 默认首页

// sw.js self.addEventListener(‘fetch’, (e) => {
console.log(‘现在正在请求:’ + e.request.url); const currentUrl =
e.request.url; // 匹配上页面路径 if (matchHtml(currentUrl)) { const
requestToCache = e.request.clone(); e.respondWith( // 加载网络上的资源
fetch(requestToCache).then((response) => { // 加载失败 if (!response
|| response.status !== 200) { throw Error(‘response error’); } //
加载成功,更新缓存 const responseToCache = response.clone();
caches.open(cacheName).then((cache) => { cache.put(requestToCache,
responseToCache); }); console.log(response); return response;
}).catch(function() { //
获取对应缓存中的数据,获取不到则退化到获取默认首页 return
caches.match(e.request).then((response) => { return response ||
caches.match(‘/index.html’); }); }) ); } });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// sw.js
self.addEventListener(‘fetch’, (e) => {
  console.log(‘现在正在请求:’ + e.request.url);
  const currentUrl = e.request.url;
  // 匹配上页面路径
  if (matchHtml(currentUrl)) {
    const requestToCache = e.request.clone();
    e.respondWith(
      // 加载网络上的资源
      fetch(requestToCache).then((response) => {
        // 加载失败
        if (!response || response.status !== 200) {
          throw Error(‘response error’);
        }
        // 加载成功,更新缓存
        const responseToCache = response.clone();
        caches.open(cacheName).then((cache) => {
          cache.put(requestToCache, responseToCache);
        });
        console.log(response);
        return response;
      }).catch(function() {
        // 获取对应缓存中的数据,获取不到则退化到获取默认首页
        return caches.match(e.request).then((response) => {
           return response || caches.match(‘/index.html’);
        });
      })
    );
  }
});

为什么存在命中不了缓存页面的情况?

  1. 首先需要明确的是,用户在第一次加载你的站点的时候,加载页面后才会去启动
    sw,所以第一次加载不可能通过 fetch 事件去缓存页面
  2. 我的博客是单页应用,但是用户并不一定会通过首页进入,有可能会通过其它页面路径进入到我的网站,这就导致我在
    install 事件中根本没办法指定需要缓存那些页面
  3. 最终实现的效果是:用户第一次打开页面,马上断掉网络,依然可以离线访问我的站点

结合上面三点,我的方法是:第一次加载的时候会缓存 /index.html 这个资源,并且缓存页面上的数据,如果用户立刻离线加载的话,这时候并没有缓存对应的路径,比如 /archives 资源访问不到,这返回 /index.html 走异步加载页面的逻辑。

在 install 事件缓存 /index.html,保证了 service worker
第一次加载的时候缓存默认页面,留下退路。

import constants from ‘./constants’; const cacheName =
constants.cacheName; const apiCacheName = constants.apiCacheName; const
cacheFileList = [‘/index.html’]; self.addEventListener(‘install’, (e)
=> { console.log(‘Service Worker 状态: install’); const
cacheOpenPromise = caches.open(cacheName).then((cache) => { return
cache.addAll(cacheFileList); }); e.waitUntil(cacheOpenPromise); });

1
2
3
4
5
6
7
8
9
10
11
12
import constants from ‘./constants’;
const cacheName = constants.cacheName;
const apiCacheName = constants.apiCacheName;
const cacheFileList = [‘/index.html’];
 
self.addEventListener(‘install’, (e) => {
  console.log(‘Service Worker 状态: install’);
  const cacheOpenPromise = caches.open(cacheName).then((cache) => {
    return cache.addAll(cacheFileList);
  });
  e.waitUntil(cacheOpenPromise);
});

在页面加载完后,在 React 组件中立刻缓存数据:

// cache.js import constants from ‘../constants’; const apiCacheName =
constants.apiCacheName; export const saveAPIData = (url, data) => {
if (‘caches’ in window) { // 伪造 request/response 数据
caches.open(apiCacheName).then((cache) => { cache.put(url, new
Response(JSON.stringify(data), { status: 200 })); }); } }; // React 组件
import constants from ‘../constants’; export default class extends
PureComponent { componentDidMount() { const { state, data } =
this.props; // 异步加载数据 if (state === constants.INITIAL_STATE ||
state === constants.FAILURE_STATE) { this.props.fetchData(); } else {
// 服务端渲染成功,保存页面数据 saveAPIData(url, data); } } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// cache.js
import constants from ‘../constants’;
const apiCacheName = constants.apiCacheName;
 
export const saveAPIData = (url, data) => {
  if (‘caches’ in window) {
    // 伪造 request/response 数据
    caches.open(apiCacheName).then((cache) => {
      cache.put(url, new Response(JSON.stringify(data), { status: 200 }));
    });
  }
};
 
// React 组件
import constants from ‘../constants’;
export default class extends PureComponent {
  componentDidMount() {
    const { state, data } = this.props;
    // 异步加载数据
    if (state === constants.INITIAL_STATE || state === constants.FAILURE_STATE) {
      this.props.fetchData();
    } else {
        // 服务端渲染成功,保存页面数据
      saveAPIData(url, data);
    }
  }
}

这样就保证了用户第一次加载页面,立刻离线访问站点后,虽然无法像第一次一样能够服务端渲染数据,但是之后能通过获取页面,异步加载数据的方式构建离线应用。

图片 12

用户第一次访问站点,如果在不刷新页面的情况切换路由到其他页面,则会异步获取到的数据,当下次访问对应的路由的时候,则退化到异步获取数据。

图片 13

当用户第二次加载页面的时候,因为 service worker
已经控制了站点,已经具备了缓存页面的能力,之后在访问的页面都将会被缓存或者更新缓存,当用户离线访问的的时候,也能访问到服务端渲染的页面了。

图片 14

兼容性

目前该应用在 Chrome 浏览器的支持性是最好的,安卓浏览器建议安装 Chrome
浏览器使用,我一般也都比较喜欢在手机上使用谷歌浏览器。对于 Safari
浏览器,其它的浏览功能似乎没有什么大问题,目前应该还没支持添加到主屏幕。不过在之后的
ios 版本好像对于 pwa 有着更进一步的支持。

发表评论

电子邮件地址不会被公开。 必填项已用*标注