菜单

React 同构应用 PWA 升级指南

2019年4月13日 - Ajax

React 同构应用 PWA 升级指南

2018/05/25 · JavaScript
· PWA,
React

初稿出处:
林东洲   

React/Redux创设的同构Web应用

2018/07/30 · CSS ·
React,
Redux

原稿出处: 原 一成(Hara
Kazunari)
   译文出处:侯斌   

世家好,小编是原一成(@herablog),近日在CyberAgent首要担任前端开发。

Ameblo(注: Ameba博客,Ameba
Blog,简称Ameblo)于2016年3月,将前端部分由原本的Java架构的运用,重构成为以node.js、React为根基的Web应用。那篇小说介绍了此次重构的缘起、目的、系统规划以及最终实现的结果。

新体系揭橥后,立即就有人注意到了这么些变化。

 manbetx2.0手机版 1

twitter_msg.png

前言

近来在给自己的博客网址 PWA 升级,顺便就记下下 React 同构应用在利用 PWA
时境遇的题材,那里不会从头发轫介绍怎样是 PWA,假若您想学学 PWA
相关知识,能够看下上面小编收藏的一部分小说:

系统重构的导火线

200四年起,Ameblo成为了东瀛国内最大范围的博客服务。可是随着系统规模的增长,以及众多相关职员不停增添种种模块、页面指点链接等,最后使得页面呈现缓慢、对网页浏览量(PV)造成了尤其严重的熏陶。并且页面突显速度方面,绝大部分是前者的难题,并非是后端的难点。

基于以上这几个题材,大家决定以进步页面呈现速度为关键对象,对系统举办彻底重构。与此同时后端系统也在拓展重构,将昔日的多寡部分进行API化改造。此时就是二个将All-in-one的特大型Java应用进行适当分割的绝佳良机。

PWA 特性

PWA 不是唯有的某项技术,而是一群技术的聚合,比如:ServiceWorker,manifest 添加到桌面,push、notification api 等。

而就在新近时间,IOS 1一.3 刚刚辅助 Service worker 和好像 manifest
添加到桌面包车型客车风味,所以此次 PWA
改造主要依旧促成那两部分机能,至于其余的性状,等 iphone 协助了再升级吗。

目标

此番系统重构确立了以下多少个对象。

Service Worker

service worker
在作者眼里,类似于二个跑在浏览器后台的线程,页面第1次加载的时候会加载这些线程,在线程激活之后,通过对
fetch 事件,能够对各个收获的能源进行控制缓存等。

页面突显速度的立异(同理可得越快越好)

用以测定用户体验的指标有好多,我们认为在这之中对用户最要害的指标正是页面展现速度。页面呈现速度越快,指标内容就能越快到达,让职责在长时间内做到。本次重构的对象是尽或然的保持博客小说、以及在Ameblo内所彰显的五花八门的内容的原来方式,在不损坏现有价值、体验的根底上,进步展现和页面行为的进程。

旗帜明显哪些财富需求被缓存?

那么在起来采取 service worker 此前,首先必要通晓怎么财富需求被缓存?

系统的现代化(搭乘生态系统)

此前的Web应用是将数据以HTML的款式重返,今年并未怎么难题。不过,随着剧情的加码,体验的充裕化,以及配备的二种化,使得前端所占的比重更加大。此前要付出3个好的Web应用,若是要高质量,就决然毫无将左右端分隔离。当年以那几个供给开发的种类,在经验了十年今后,已经远远不能够适应当下的生态系统。

「跟上脚下生态系统」,以此来塑造系统会推动巨大的补益。因为作为大旨的生态系统,其开发格外活跃,每一天都会有许许多多新的idea。由此新型的技术和效力更易于被采纳,同时落到实处高品质也特别便于。同时,这几个「新」对于年轻的技巧新人也愈来愈重大。仅知道旧规则旧技术的老伯对于贰个可观的团伙来说是未有前途的(自觉本身膝盖也中了一箭)。

缓存静态能源

第三是像 CSS、JS 这个静态能源,因为本人的博客里引用的本子样式都以通过 hash
做持久化缓存,类似于:main.ac62dexx.js 那样,然后打开强缓存,那样下次用户下次再拜访笔者的网址的时候就不要再行请求财富。直接从浏览器缓存中读取。对于那一部分能源,service
worker 没供给再去处理,直接放行让它去读取浏览器缓存即可。

自我以为借使您的站点加载静态财富的时候本人未有拉开强缓存,并且你只想透过前端去落到实处缓存,而不须求后端在参加举行调整,那能够应用
service worker 来缓存静态能源,不然就有点画蛇添足了。

晋升界面设计、用户体验(二零一六年版Ameblo)

Ameblo的无绳电话机版在20十年经历了三遍改版之后,就大致并未有太大的变化。那之中很多用户都已经习惯了原生应用的陈设性和体会。那么些类型也是为了不令人觉得很土很难用,达到顺应时期的201陆年版界面设计和用户体验。

OK,接下去让自个儿实际详尽聊聊。

缓存页面

缓存页面鲜明是必备的,那是最基本的部分,当你在离线的动静下加载页面会之后现身:

manbetx2.0手机版 2

究其原因就是因为你在离线状态下不能加载页面,今后有了 service
worker,就算你在没网络的情形下,也可以加载从前缓存好的页面了。

页面加载速度的革新

缓存后端接口数据

缓存接口数据是急需的,但也不是必须经过 service worker
来促成,前端存放数据的地点有众多,比如通过 localstorage,indexeddb
来进展仓库储存。那里本身也是由此 service worker
来贯彻缓存接口数据的,要是想透过别的格局来落到实处,只必要留意好 url
路径与数量对应的映照关系即可。

改善点

系统重构前,通过
SpeedCurve
实行解析,得出了下边结论:

听说那么些规定了上边这几项基本方针:

缓存策略

人人皆知了怎么着能源须求被缓存后,接下去就要研究缓存策略了。

SSR还是SPA

近些年比较于添加到收藏夹中,用户更赞成于通过查找结果、推特(TWTR.US)(Instagram)、Twitter等社交媒体上的享受链接打开博客页面。谷歌(Google)和照片墙的AMP,
Facebook的Instant
Article
标志第二页的显现速度大幅度影响到用户知足度。

其余,从谷歌(Google)Analytics等日志记录中打探到在小说列表页面和左右作品间开始展览跳转的用户也很多。那说不定是因为博客作为个人体媒介体,当某一用户观望壹篇不错的篇章,很是感兴趣的时候,他也同时想看一看同一博客内的其他文章。也正是说,博客这种服务
第3页神速加载与页面间飞速跳转同等首要

于是,为了让互相都能发布最好质量,大家决定在第2页使用服务器端渲染(Server-side
Rendering, SS大切诺基),从第2页起选用单页面应用(Single Page Application,
SPA)。那样1来,既能确定保障率先页的彰显速度和机械和工具可读性(Machine-Readability)(含SEO),又能获得SPA带来的高效展现速度。

BTW,对于近年来的架构,由于服务器和客户端接纳同样的代码,全部进展SSSportage或是全体进展SPA也是唯恐的。近来早已落到实处即使在不可能运作JavaScript的环境中,也得以平常通过SS途胜来浏览。能够预感以往等到ServiceWorker普及之后,早先页面将更高速化,而且能够兑现离线浏览。

manbetx2.0手机版 3

z-ssrspa.png

在此在此之前的系统完全选拔SS酷威,而现在的连串从第三页起变为SPA。

 manbetx2.0手机版 4

z-spa-speed.gif

SPA的魅力在于展现速度之快。因为唯有通过API获取所需的必备数据,所以速度尤其快!

页面缓存策略

因为是 React
单页同构应用,每趟加载页面包车型地铁时候数据都以动态的,所以笔者动用的是:

  1. 网络优先的办法,即优先得到互联网上流行的能源。当网络请求失败的时候,再去取得
    service worker 里此前缓存的能源
  2. 当互连网加载成功以往,就创新 cache
    中对应的缓存能源,保证下次每便加载页面,都是上次做客的新星资源
  3. 假定找不到 service worker 中 url 对应的能源的时候,则去赢得 service
    worker 对应的 /index.html 暗许首页

// sw.js self.add伊芙ntListener(‘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. 末尾促成的效应是:用户率先次打开页面,马上断掉网络,如故得以离线访问作者的站点

结缘方面3点,作者的艺术是:第三回加载的时候会缓存 /index.html 这些能源,并且缓存页面上的数码,如若用户及时离线加载的话,那时候并未缓存对应的路子,比如 /archives 财富访问不到,那重回 /index.html 走异步加载页面包车型大巴逻辑。

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

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);
    }
  }
}

如此就确定保证了用户率先次加载页面,立刻离线访问站点后,固然不能像第三次壹样能够服务端渲染数据,可是随后能因此获取页面,异步加载数据的艺术营造离线应用。

manbetx2.0手机版 5

用户率先次访问站点,假如在不刷新页面包车型客车事态切换路由到其余页面,则会异步获取到的数据,当下次访问对应的路由的时候,则退步到异步获取数据。

manbetx2.0手机版 6

当用户第叁次加载页面包车型客车时候,因为 service worker
已经决定了站点,已经有所了缓存页面包车型地铁力量,之后在做客的页面都将会被缓存可能更新缓存,当用户离线访问的的时候,也能访问到服务端渲染的页面了。

manbetx2.0手机版 7

延期加载

咱俩选择SS奇骏+SPA的方式来优化页面间跳转那种横向移动的快慢,并且应用延缓加载来更始页面包车型地铁纵向移动速度。一初始要表现的内容以及导航,还有博客作品等最早展现,在那些内容之下的协助内容随着页面包车型大巴轮转逐步显现。那样一来,首要的内容不会受页面上面内容的影响而越来越快的展示出来。对于那么些想赶紧读小说的用户来说,既不增添用户体验上的压力,又能完全的提供页面下方的情节。

 manbetx2.0手机版 8

z-lazyload.png

事先的系统因为将页面内的全体内容都停放HTML文书档案里,所以使得HTML文书档案体积相当大。而前几日的系统,仅仅将首要内容放到HTML里重临,收缩了HTML的容量和数量请求的分寸。

接口缓存策略

谈完页面缓存,再来讲讲接口缓存,接口缓存就跟页面缓存很接近了,唯壹的例外在于:页面第1回加载的时候不肯定有缓存,不过会有接口缓存的存在(因为伪造了
cache 中的数据),所以缓存策略跟页面缓存类似:

  1. 网络优先的办法,即优先得到互联网上接口数据。当网络请求退步的时候,再去得到service worker 里在此以前缓存的接口数据
  2. 当网络加载成功之后,就更新 cache
    中对应的缓存接口数据,保险下次历次加载页面,都以上次拜会的时尚接口数据

因此代码就像是这么(代码类似,不再赘述):

self.add伊芙ntListener(‘fetch’, (e) => { console.log(‘以后正在呼吁:’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
self.addEventListener(‘fetch’, (e) => {
  console.log(‘现在正在请求:’ + e.request.url);
  const currentUrl = e.request.url;
  if (matchHtml(currentUrl)) {
    // …
  } else if (matchApi(currentUrl)) {
    const requestToCache = e.request.clone();
    e.respondWith(
      fetch(requestToCache).then((response) => {
        if (!response || response.status !== 200) {
          return response;
        }
        const responseToCache = response.clone();
        caches.open(apiCacheName).then((cache) => {
          cache.put(requestToCache, responseToCache);
        });
        return response;
      }).catch(function() {
        return caches.match(e.request);
      })
    );
  }
});

那里实在能够再进行优化的,比如在获取数据接口的时候,能够先读取缓存中的接口数据开始展览渲染,当真正的网络接口数据重返之后再展开替换,那样也能一蹴而就削减用户的首屏渲染时间。当然这可能会发生页面闪烁的功用,能够加上一些卡通来进展交接。

HTML缓存

博客作品是静态文档,对于特定U奥迪Q5L的呼吁会再次回到固定的始末,因而相当适合实行缓存。缓存使得服务器处理内容收缩,在增高页面响应速度的还要减轻了服务器的承负。大家将不变的内容(小说等)生成的HTML实行缓存重临,对于由于变化的始末能过JavaScript、CSS等展开操作(比如突显、隐藏等)。

 manbetx2.0手机版 9

z-newrelic-entrylist.png

那张图体现了201六年10月最后4日New
relic上的总结数据。小说列表页面包车型大巴HTML的响应时间基本在50ms以下。

manbetx2.0手机版, manbetx2.0手机版 10

z-newrelic-entry.png

这张图是作品详细页面包车型地铁总计数据。能够见见,这几个页面包车型客车响应时间也基本上是在50ms以下。由于存在小说过长的时候会招致页面体量变大,以及小说页面不可能完全缓存等气象,所以对待列表页面会设有更加多较慢的响应。

对于因请求的客户端而发出变化部分的处理,我们在HTML的body标签中通过加入相应的class,然后在客户端通过JavaScript和CSS等展开操作。比如,一些内容不想在好几操作系统上海展览中心示,大家就用CSS对那几个剧情开展隐蔽。由于CSS样式表会先载入,页面布局鲜明下来今后再实行页面渲染,所以那个也可以解决后边要涉及的「咯噔」难点。

<!– html –> <body class=”OsAndroid”>

1
2
3
<!– html –>
 
<body class="OsAndroid">

CSS

/* main.css */ body.OsAndroid .BannerForIos { dsplay: none; }

1
2
3
4
5
/* main.css */
 
body.OsAndroid .BannerForIos {
  dsplay: none;
}

其余难点

到明日了却,已经差不离能够完毕 service worker
离线缓存应用的成效了,可是还有依然存在有的标题:

系统的现代化(搭乘生态系统)

急速激活 service worker

私下认可景况下,页面的呼吁(fetch)不会通过 sw,除非它本人是经过 sw
获取的,也正是说,在设置 sw 之后,须要刷新页面才能有效应。sw
在装置成功并激活以前,不会响应 fetch或push等事件。

因为站点是单页面应用,那就招致了您在切换路由(未有刷新页面)的时候未有缓存接口数据,因为此时
service worker 还从未起初工作,所以在加载 service worker
的时候必要快捷地激活它。代码如下:

self.addEventListener(‘activate’, (e) => { console.log(‘Service
Worker 状态: activate’); const cachePromise = caches.keys().then((keys)
=> { return Promise.all(keys.map((key) => { if (key !== cacheName
&& key !== apiCacheName) { return caches.delete(key); } return null;
})); }); e.waitUntil(cachePromise); // 急忙激活 sw,使其能够响应 fetch
事件 return self.clients.claim(); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.addEventListener(‘activate’, (e) => {
  console.log(‘Service Worker 状态: activate’);
  const cachePromise = caches.keys().then((keys) => {
    return Promise.all(keys.map((key) => {
      if (key !== cacheName && key !== apiCacheName) {
        return caches.delete(key);
      }
      return null;
    }));
  });
  e.waitUntil(cachePromise);
  // 快速激活 sw,使其能够响应 fetch 事件
  return self.clients.claim();
});

有的文章说还亟需在 install
事件中添加 self.skipWaiting(); 来跳过等待时间,不过自身在实践中发现就是不添加也得以健康激活
service worker,原因未知,有读者精通的话能够交流下。

今昔当您首先次加载页面,跳转路由,马上离线访问的页面,也能够万事大吉地加载页面了。

技术选型

此番项指标技能采用时,遵循了尽量使用当下当前市集上早已存在的科学普及应用的技术那1标准化。暗号就是:「活脱脱像范例应用相同Start」。那样一来,无论是哪个人都得以轻松的拿走到对应的文档等消息,同时其他的集体和公司尽管要插手到品种中来也能一点也不慢的左手。但是在真正实行开发的时候,1些细节达成上因为各类各个的原因存在有的例外的状态,不过在巨大程度上保证了逐条模块的独立性。最终系统的大约构成如下图所示:

 manbetx2.0手机版 11

z-bigpicture.png

(有个别地点做了简约)

并非强缓存 sw.js

用户每一遍访问页面包车型大巴时候都会去重新赢得
sw.js,依据文件内容跟在此之前的版本是还是不是同样来判定 service worker
是或不是有创新。所以即使你对 sw.js
开启强缓存的话,就将深陷死循环,因为老是页面获得到的 sw.js
都是壹律,那样就不大概提高你的 service worker。

其余对 sw.js 开启强缓存也是从未有过须求的:

  1. 本身 sw.js
    文件本身就不大,浪费不了多少带宽,觉得浪费能够应用协议缓存,但附加增支负担
  2. sw.js 是在页面空闲的时候才去加载的,并不会影响用户首屏渲染速度

React with Redux

使用React和React实行支付的的时候,很多地点能够用 纯函数
的款式展开重组。纯函数是指特定的参数总是回到特定的结果,不会对函数以外的限创立成污染。使用纯函数实行开发能够保险各样处理模块最小化,不用操心会无意改变引用对象的值。那样一来,12分拉动大规模开发以及在同等客户端中维系四个情景。

界面更新的流水生产线是:
Action(Event) -> Reducer (返回新的state(状态)) -> React (基于更新后的store内的state更新显示内容)

那是八个Redux Action的事例,演示了React Action (Action Creator)
基于参数重回三个Plain Object。处理异步请求的时候,大家参考
合法文书档案
,分别定义了成功请求和曲折请求。获取数据时接纳了
redux-dataloader

JavaScript

// actions/blogAction.js export const FETCH_BLOG_REQUEST =
‘blog/FETCH_BLOG/REQUEST’; export function fetchBlogRequest(blogId) {
return load({ type: FETCH_BLOG_REQUEST, payload: { blogId, }, }); }

1
2
3
4
5
6
7
8
9
10
11
12
// actions/blogAction.js
 
export const FETCH_BLOG_REQUEST = ‘blog/FETCH_BLOG/REQUEST’;
 
export function fetchBlogRequest(blogId) {
  return load({
    type: FETCH_BLOG_REQUEST,
    payload: {
      blogId,
    },
  });
}

Redux
Reducer是一完全基于Action中指导的数目,对已有state举行理并答复制并立异的函数。

JavaScript

// reducers/blogReducer.js import as blogAction from
‘../actions/blogAction’; const initialState = {}; function
createReducer(initialState, handlers) { return (state = initialState,
action) => { const handler = (action && action.type) ?
handlers[action.type] : undefined; if (!handler) { return state; }
return handler(state, action); }; } export default
createReducer(initialState, { [blogAction.FETCH_BLOG_SUCCESS]:
(state, action) => { const { blogId, data } = action.payload; return
{ …state, [blogId]: 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
// reducers/blogReducer.js
 
import  as blogAction from ‘../actions/blogAction’;
 
const initialState = {};
 
function createReducer(initialState, handlers) {
  return (state = initialState, action) => {
    const handler = (action && action.type) ? handlers[action.type] : undefined;
    if (!handler) {
      return state;
    }
    return handler(state, action);
  };
}
 
export default createReducer(initialState, {
  [blogAction.FETCH_BLOG_SUCCESS]: (state, action) => {
    const { blogId, data } = action.payload;
    return {
      …state,
      [blogId]: data,
    };
  },
});

React/Redux基于更新后的store中的数据,对UI进行创新。种种零部件根据传递过来的props值,总是以同等的结果回到HTML。React将View组件也作为函数来对待。

JavaScript

// main.js <SpBlogTitle blogTitle=”渋谷のブログ” /> //
SpBlogTitle.js import React from ‘react’; export class SpBlogTitle
extends React.Component { static propTypes = { blogTitle:
React.PropTypes.string, }; shouldComponentUpdate(nextProps) { return
this.props.blogTitle !== nextProps.blogTitle; } render() { return (
<h1>{this.props.blogTitle}</h1> ); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.js
<SpBlogTitle blogTitle="渋谷のブログ" />
 
// SpBlogTitle.js
import React from ‘react’;
 
export class SpBlogTitle extends React.Component {
  static propTypes = {
    blogTitle: React.PropTypes.string,
  };
 
  shouldComponentUpdate(nextProps) {
    return this.props.blogTitle !== nextProps.blogTitle;
  }
 
  render() {
    return (
      <h1>{this.props.blogTitle}</h1>
    );
  }
}

有关Redux的音讯在
合法文书档案
中表明得极度详细,推荐随时参考一下以此文书档案。

防止改变 sw 的 U智跑L

在 sw 中如此做是“最差实践”,要在原地点上修修改改 sw。

举个例子来表明为什么:

  1. index.html 注册了 sw-v1.js 作为 sw
  2. sw-v一.js 对 index.html 做了缓存,也正是缓存优先(offline-first)
  3. 你更新了 index.html 重新注册了在新鸿基土地资金财产方的 sw sw-v二.js

借使你像上边那么做,用户永远也拿不到 sw-v2.js,因为 index.html 在
sw-v一.js 缓存中,那样的话,若是你想翻新为 sw-v贰.js,还亟需转移原来的
sw-v一.js。

同构Web应用(Isomorphic web app)

Ameblo
201陆年版基本上完全是用JavaScript重写的。无论是Node服务器上依旧客户端上都使用了1致的代码和流程,也正是所谓的同构Web应用。项目标目录结构大体上上如下所示,服务器端的入口文件是
server.js ,浏览器的输入文件是 client.js

 manbetx2.0手机版 12

z-isomorphic.png

写好的JavaScript同时运行在劳动器端依然客户端上的运行行为、以及从数额读取直到在页面上显得结束的上上下下浏程,都是平等的样式进行。

manbetx2.0手机版 13

z-code-stats.png

应用Github的言语总计能够见到
,JavaScript占了整个项目标九四.0%,大致全体都以由JavaScript写成的。

测试

随后,大家早已成功了接纳 service worker
对页面举行离线缓存的功力,假使想体验效果的话,访问我的博客:https://lindongzhou.com

随便浏览任意的页面,然后关掉网络,再度做客,从前您浏览过的页面都能够在离线的状态下开始展览访问了。

IOS 要求 1壹.3 的本子才支撑,使用 Safari 实行访问,Android 请选拔帮忙service worker 的浏览器

原子设计(Atomic Design)

对于组件的筹划,我们应用了
原子设计
理念。其实项目并从未1上马就选取原子设计,而是依照 Presentational and
Container
Components

,对 containercomponent
举行了两层划分。不过Ameblo中的组件实在是太多,很简单造成任务不醒指标景观,因而最后使用了原子设计理念。项指标实在应用中,选取了以下的条条框框。

 manbetx2.0手机版 14

z-atomic-design.png

manifest 桌面应用

前方讲完了什么样采用 service worker 来离线缓存你的同构应用,不过 PWA
不仅限于此,你还足以选用安装 manifest
文件来将你的站点添加到运动端的桌面上,从而完结趋近于原生应用的体会。

Atoms

组件的细小单位,比如Icon、Button等。原则上不享有状态,从父组件中得到传递过来的props,并回到HTML。

使用 webpack-pwa-manifest 插件

自笔者的博客站点是因此 webpack 来营造前端代码的,所以本人在社区里找到
webpack-pwa-manifest 插件用来生成 manifest.json。

先是安装好 webpack-pwa-manifest 插件,然后在你的 webpack
配置文件中丰硕:

// webpack.config.prod.js const WebpackPwaManifest =
require(‘webpack-pwa-manifest’); module.exports =
webpackMerge(baseConfig, { plugins: [ new WebpackPwaManifest({ name:
‘Lindz\’s Blog’, short_name: ‘Blog’, description: ‘An isomorphic
progressive web blog built by React & Node’, background_color: ‘#333’,
theme_color: ‘#333’, filename: ‘manifest.[hash:8].json’, publicPath:
‘/’, icons: [ { src: path.resolve(constants.publicPath, ‘icon.png’),
sizes: [96, 128, 192, 256, 384, 512], // multiple sizes destination:
path.join(‘icons’) } ], ios: { ‘apple-mobile-web-app-title’: ‘Lindz\’s
Blog’, ‘apple-mobile-web-app-status-bar-style’: ‘#000’,
‘apple-mobile-web-app-capable’: ‘yes’, ‘apple-touch-icon’:
‘//xxx.com/icon.png’, }, }) ] })

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
// webpack.config.prod.js
const WebpackPwaManifest = require(‘webpack-pwa-manifest’);
module.exports = webpackMerge(baseConfig, {
  plugins: [
    new WebpackPwaManifest({
      name: ‘Lindz\’s Blog’,
      short_name: ‘Blog’,
      description: ‘An isomorphic progressive web blog built by React & Node’,
      background_color: ‘#333’,
      theme_color: ‘#333’,
      filename: ‘manifest.[hash:8].json’,
      publicPath: ‘/’,
      icons: [
        {
          src: path.resolve(constants.publicPath, ‘icon.png’),
          sizes: [96, 128, 192, 256, 384, 512], // multiple sizes
          destination: path.join(‘icons’)
        }
      ],
      ios: {
        ‘apple-mobile-web-app-title’: ‘Lindz\’s Blog’,
        ‘apple-mobile-web-app-status-bar-style’: ‘#000’,
        ‘apple-mobile-web-app-capable’: ‘yes’,
        ‘apple-touch-icon’: ‘//xxx.com/icon.png’,
      },
    })
  ]
})

大概地阐释下安排音讯:

  1. name: 应用名称,便是图标上面包车型地铁来得名称
  2. short_name: 应用名称,但 name 不可能出示完全时候则显示这一个
  3. background_color、theme_color:顾名思义,相应的颜色
  4. publicPath: 设置 cdn 路径,跟 webpack 里的 publicPath 一样
  5. icons: 设置图标,插件会活动帮您转移区别 size
    的图纸,可是图片大小必须超越最大 sizes
  6. ios: 设置在 safari 中怎么样去添加桌面应用

安装完未来,webpack 会在构建进度中变化对应的 manifest 文件,并在 html
文件中援引,下面就是生成 manifest 文件:

{ “icons”: [ { “src”:
“/icons/icon_512x512.79ddc5874efb8b481d9a3d06133b6213.png”, “sizes”:
“512×512”, “type”: “image/png” }, { “src”:
“/icons/icon_384x384.09826bd1a5d143e05062571f0e0e86e7.png”, “sizes”:
“384×384”, “type”: “image/png” }, { “src”:
“/icons/icon_256x256.d641a3644ce20c06855db39cfb2f7b40.png”, “sizes”:
“256×256”, “type”: “image/png” }, { “src”:
“/icons/icon_192x192.8f11e077242cccd9c42c0cbbecd5149c.png”, “sizes”:
“192×192”, “type”: “image/png” }, { “src”:
“/icons/icon_128x128.cc0714ab18fa6ee6de42ef3d5ca8fd09.png”, “sizes”:
“128×128”, “type”: “image/png” }, { “src”:
“/icons/icon_96x96.dbfccb1a5cef8093a77c079f761b2d63.png”, “sizes”:
“96×96”, “type”: “image/png” } ], “name”: “Lindz’s Blog”,
“short_name”: “Blog”, “orientation”: “portrait”, “display”:
“standalone”, “start_url”: “.”, “description”: “An isomorphic
progressive web blog built by React & Node”, “background_color”:
“#333”, “theme_color”: “#333” }

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
32
33
34
35
36
37
38
39
40
41
42
{
  "icons": [
    {
      "src": "/icons/icon_512x512.79ddc5874efb8b481d9a3d06133b6213.png",
      "sizes": "512×512",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_384x384.09826bd1a5d143e05062571f0e0e86e7.png",
      "sizes": "384×384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_256x256.d641a3644ce20c06855db39cfb2f7b40.png",
      "sizes": "256×256",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_192x192.8f11e077242cccd9c42c0cbbecd5149c.png",
      "sizes": "192×192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_128x128.cc0714ab18fa6ee6de42ef3d5ca8fd09.png",
      "sizes": "128×128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon_96x96.dbfccb1a5cef8093a77c079f761b2d63.png",
      "sizes": "96×96",
      "type": "image/png"
    }
  ],
  "name": "Lindz’s Blog",
  "short_name": "Blog",
  "orientation": "portrait",
  "display": "standalone",
  "start_url": ".",
  "description": "An isomorphic progressive web blog built by React & Node",
  "background_color": "#333",
  "theme_color": "#333"
}

html 中会引用这一个文件,并且拉长对 ios 添加桌面应用的扶助,就好像那样。

<!DOCTYPE html> <html lang=en> <head> <meta
name=apple-mobile-web-app-title content=”Lindz’s Blog”> <meta
name=apple-mobile-web-app-capable content=yes> <meta
name=apple-mobile-web-app-status-bar-style content=#838a88> <link
rel=apple-touch-icon href=xxxxx> <link rel=manifest
href=/manifest.21d63735.json> </head> </html>

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang=en>
<head>
  <meta name=apple-mobile-web-app-title content="Lindz’s Blog">
  <meta name=apple-mobile-web-app-capable content=yes>
  <meta name=apple-mobile-web-app-status-bar-style content=#838a88>
  <link rel=apple-touch-icon href=xxxxx>
  <link rel=manifest href=/manifest.21d63735.json>
</head>
</html>

就那样简单,你就足以运用 webpack 来添加你的桌面应用了。

Molecules

以复用为前提的机件,比如List、Modal、User
thunmbnail等。原则上不具有状态,从父组件中收获传递过来的props,并赶回HTML。

测试

添加完之后您能够经过 chrome 开发者工具 Application – Manifest 来查阅你的
mainfest 文件是不是见效:

manbetx2.0手机版 15

那般表达您的布署生效了,安卓机会自动识别你的布局文件,并问询用户是或不是丰盛。

Organisms

页面上较大的1块组件,比如Header,Entry,Navi等。对于那1层的机件,能够在内部进展多少获得处理,以及利用Redux
State 和
connect
,维护组件的气象。那里收获的零件状态以props的样式,传递给 Molecules
Atom

JavaScript

// components/organisms/SpProfile.js import React from ‘react’; import {
connect } from ‘react-redux’; import { routerHooks } from
‘react-router-hook’; import { fetchBloggerRequest } from
‘../../../actions/bloggerAction’; // 数据获得处理
(使用react-router-hook) const defer = async ({ dispatch }) => { await
dispatch(fetchBloggerRequest()); }; // Redu store的state作为props const
mapStateToProps = (state, owndProps) => { const amebaId =
owndProps.params.amebaId; const bloggerMap = state.bloggerMap; const
blogger = bloggerMap[amebaId]; const nickName = blogger.nickName;
return { nickName, }; }; @connect(mapStateToProps) @routerHooks({ done
}) export class SpProfileInfo extends React.Component { static propTypes
= { nickName: React.PropTypes.string.isRequired, }; render() { return (
<div>{this.props.nickName}</div> ); } }

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
32
33
34
35
36
37
38
// components/organisms/SpProfile.js
 
import React from ‘react’;
import { connect } from ‘react-redux’;
import { routerHooks } from ‘react-router-hook’;
 
import { fetchBloggerRequest } from ‘../../../actions/bloggerAction’;
 
// 数据获取处理 (使用react-router-hook)
const defer = async ({ dispatch }) => {
  await dispatch(fetchBloggerRequest());
};
 
// Redu store的state作为props
const mapStateToProps = (state, owndProps) => {
  const amebaId = owndProps.params.amebaId;
  const bloggerMap = state.bloggerMap;
  const blogger = bloggerMap[amebaId];
  const nickName = blogger.nickName;
 
  return {
    nickName,
  };
};
 
@connect(mapStateToProps)
@routerHooks({ done })
export class SpProfileInfo extends React.Component {
  static propTypes = {
    nickName: React.PropTypes.string.isRequired,
  };
 
  render() {
    return (
      <div>{this.props.nickName}</div>
    );
  }
}

结尾

讲到那基本上就完了,等随后 IOS 协助 PWA
的任何功效的时候,到时候作者也会相应地去履行其它 PWA 的特色的。未来 IOS
1一.三 也但是扶助 PWA 中的 service worker 和 app manifest
的效用,但是相信在不久的现在,其余的效应也会相应得到帮忙,到时候相信 PWA
将会在活动端绽放异彩的。

1 赞 收藏
评论

manbetx2.0手机版 16

Template

各样请求路径(UCRUISERL)所对应的组件。其任务是将所需的预制构件从Organisms中import过来,以自然的逐一和格式整合在共同。

Pages

用作页面包车型地铁页面组件。基本上是把传递过来的 this.props.children
原原本本的来得出来。由于Ameblo是单页面应用,因此只有1个页面组件。

CSS Modules

CSS样式表使用 CSS
Modules

将CSS样式规则的功用范围严刻限制到了逐条零部件内。各种样式规则的职能范围开始展览界定使得样式的转移和删除特别便于。因为Ameblo是由许四人1齐开发到位,不自然每种人都精晓CSS,而且不免要平日对部分不知是什么人何时写的代码进行改动,在这一年将作用范围限定到零部件的CSS
Modules就公布其功效了。

CSS

/ components/organisms/SpNavigationBar.css / .Nav { background: #fff;
border-bottom: 1px solid #e3e5e4; display: flex; height: 40px; width:
100%; } .Logo { text-align: center; }

1
2
3
4
5
6
7
8
9
10
11
12
13
/ components/organisms/SpNavigationBar.css /
 
.Nav {
  background: #fff;
  border-bottom: 1px solid #e3e5e4;
  display: flex;
  height: 40px;
  width: 100%;
}
 
.Logo {
  text-align: center;
}

JavaScript

// components/organisms/SpNavigationBar.js import React from ‘react’;
import style from ‘./SpNavigationBar.css’ export class SpBlogInfo
extends React.Component { render() { return ( <nav
className={style.Nav}> <div className={style.Logo}> <img
alt=”Ameba” height=”24″ src=”logo.svg” width=”71″ /> </div>
<div …> </nav> ); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// components/organisms/SpNavigationBar.js
 
import React from ‘react’;
import style from ‘./SpNavigationBar.css’
 
export class SpBlogInfo extends React.Component {
  render() {
    return (
      <nav className={style.Nav}>
        <div className={style.Logo}>
          <img
            alt="Ameba"
            height="24"
            src="logo.svg"
            width="71"
           />
        </div>
        <div …>
      </nav>
    );
  }
}

逐一class的名号经过webpack编写翻译之后,变成像
SpNavigationBar__Nav___3g5MH 那样含hash值的大局唯一名称。

ESLint, stylelint

此番的品种将ESLint和stylelint放到了必须的地方,固然3个字母出错,整个项目也非常的小概测试通过。指标就在于统一代码风格,节约代码审查时的麻烦。具体规则分别继承自
eslint-config-airbnb

stylelint-config-standard
,对于一些必备的底细做了个别定制。因为规则较严,初叶的时候大概有些不方便。新成员出席项目组时,代码通过Lint测试便成了要通过的第二关。

 manbetx2.0手机版 17

z-code-review.png

提防了代码审查时对于这个微小写法挑错。被机器告知错误时,心理上会感觉稍好有的。

manbetx2.0手机版 18

z-ci-error.png

进入项目组之后,最初的那段时日里发出Lint错误是有史以来的事。

CI, Build, Tesing

代码的
构建
测试

部署
统壹使用CI(公司里面使用
CircleCI
)来达成。各样分支向GHE(Github
Enterprise)PUSH之后,依照种种分支产生不相同的动作。这么些流程的功利便是创设相关的处理不必要特地职员来成功,而是统1写在
circle.ymlpackage.json (node环境下)里。

Docker

本次系统重构,也对node.js应用进行docker化创设。这一次重构的是前者系统,大家愿意能够在微小查对之后随即展开安顿。docker化之后,一旦将镜像营造达成,能够不受node模块版本的左右开始展览布局,回滚也很简单。

除此以外,node.js本人发表非凡频仍,如若放置不管,不知不觉之间系统就成古董了。docker化之后,能够不受各主机环境的震慑自由的拓展升高。

更关键的是,设置docker容器数是比较不难的,那对于系统横向扩大容积以及对服务器配置作优化时也尤其便于。

提升界面设计、用户体验(2016年版Ameblo)

不再「咯噔」

系统重构从前的Ameblo由于存在1些冲天未有固定的模块,出现了「咯噔」现象。这种「咯噔」会招致误点击以及页面包车型地铁重绘,13分令人发指痛恨。而此模块中度稳定也做为此次系统重构的UI设计的前提。特别是页面间导航作为12分至关心珍视要的要素,大家经过努力使得在页面跳转时老是都足以触击到平等的岗位。

manbetx2.0手机版 19

z-gatan.gif

「咯噔」的三个事例。点击[次のページ](下一页)的时候,额外的因素由于加载缓慢,造成误点击。

 manbetx2.0手机版 20

z-paging-fixed.gif

系统重构之后,成分的职位被固定下来,减轻了页面跳转时给用户心理上带来的担当。

智能手提式无线电话机时期的用户界面

2016年在移动环境下利用的用户大致都在行使智能机。在智能手提式有线电话机上,由于各样平台的提供者制定了各自分化的用户界面规范,用户已经司空眼惯并适应了用户界面。相比较之下,虽说浏览器上的正式11分少,但是只要和当今风行的界面差别太大的话,就会变得很难用。

Ameblo的无绳电话机版在20十年开始展览改版之后,自然对1部分细节进行了考订,不过出于未有太大的变动,所未来后总的来说众多地点已经给人一种很旧的纪念。用户在浏览的时候,对于界面并不区分是原生应用仍旧浏览器,因此制作出适应当下一代那一个平台的用户界面突显尤其关键。那里介绍一下此番重构中,对于界面包车型大巴有个别升格。

 manbetx2.0手机版 21

z-update-design.png

内容占据界面上横向整个空间。2010年的时候,壹般选拔推特(Twitter)倡导的「将逐一模块圈起来的设计」。

manbetx2.0手机版 22

z-searchbar.gif

日增了导航栏,把导航相关操作集中停放在那边。

可访问性

本次系统重构正值可访问性成为热点话题的时候。仔细的为HTML扩张一定标签属生就能够使全部系统丰裕可访问。首先在HTML标签属性添加上时要用心研讨。对于标题、
img 等充裕适当的 alt 属性,对于可点击的因素一定要运用 a button
等可点击的竹签。假如能自动对可访问性实行视察就再好但是了,ESlint的
jsx-a11y
插件能够援助成功那或多或少。

在品种开展的时候,正好公司内进行了1回可访问性的求学活动( Designing
Web
Accessibility

的撰稿人太田先生和伊最初的小说人也参与了此番活动),在这一次活动上也尝尝了Ameblo到如今截至未有留神过的语音朗读器。当时用语音朗读器在Ameblo上实行朗读时,有几处至极的地点,使用
WAI-ARIA
对这几处加以纠正(与 data-* 相同,JSX也支持 aria-* 属性)。

这里
的PPT中有详细的牵线,欢迎观察(日文)。

结果

OK,下面介绍了本次重构带来的众多浮动,那么结果什么呢?

首先是性质相关指标(测试的UTiggoL都以Ameblo中单壹页面请求能源最多,展现速度最慢的页面)。

闭塞渲染的能源(Critical Blocking Resources)

 manbetx2.0手机版 23

z-speed-blocking.png

卡住渲染的财富数 减少了75%
!JavaScript全体变成了异步读取与实施。CSS样式因为运行的缘由,维持了重构前的景况。

剧情请求(Content Requests)

 manbetx2.0手机版 24

z-speed-requests.png

财富请求数 减少了58.04%
!由于采取了推迟加载,首屏突显只加载供给的财富,与此同时对文本举行适量的整理,并删除了部分不须要的模块,最后完成了这些情状。

渲染(Rendering)

 manbetx2.0手机版 25

z-speed-rendering.png

渲染速度做为前端的首要品质目标,本次 提升了44.68%

页面加载时间(Page Load Time)

 manbetx2.0手机版 26

z-speed-pageload.png

页面加载时间 缩短了40.5 !此外,后端的回来时间也保障在了0.二ms ~
0.3ms之间。

接下去介绍一下有关的业务目的。

网页浏览量(Pageviews)

 manbetx2.0手机版 27

z-ga-pv.png

因为2016年1月有一个人资深的博客主成为了热点话题,所以那几个目标内含有1二分意况。网页浏览量提高了5七.1伍%。假若将热点话题所拉动的数值除去后,实际上只是由系统重构所拉动的升官在百分之十到1/5里头。

每回对话浏览页数 (Pages / Session)

 manbetx2.0手机版 28

z-ga-pps.png

Pages / Session是指在单个会话内页面包车型客车浏览数,那一个指标 提升了35.54
。SPA改正了页面间跳转的速度,获取了斐然的功用。

跳出率(Bounce Rate)

 manbetx2.0手机版 29

z-ga-bounce.png

跳出率指在一个对话内,仅看了一个页面包车型客车比值,这一个目标 改善了44.44%
。大家觉得那是出于首屏和页面跳转速度的纠正,用户界面升级(更便于掌握的分页),「咯噔」革新所拉动的结果。

而是还设有很多更上一层楼的退路,任何二个目的都可以再次提高。大家想以此标志
网址质量的升迁会推动业务目的的升官

上述数量是在以下条件下获得的:

写在最终

本次系统重构的视角是对技术的挑衅,结果取得了地道的用户举报,并对作业作出了孝敬,大家自家也倍感分外有价值,得到了高大的引以自豪。采取新型迎合时流的技艺自然进步服务的品质,也使得那种知识在商店在生根。在此,对不久导入Isomorphic
JavaScript,并向东瀛国内推广的同事
@ahomu
表示多谢! 

笔者介绍:

笔者:原 一成(Hara Kazunari),二零零六年出席扶桑CyberAgent公司。担任Ameblo
201六平移前端改版项目首脑导。著有《GitHubの教科書》,《CSS三逆引きデザインレシピ》,《フロントエンドエンジニア育成読本》。

翻译:侯 斌(Hou
Bin),201肆年入职东瀛CyberAgent公司。现任Ameblo前端开发。在这一次Ameblo
201陆平移前端改版项目中担纲主要开发,负责基础架构和技术选型以及主要模块开发等。

1 赞 收藏
评论

manbetx2.0手机版 30

相关文章

发表评论

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

网站地图xml地图