React 同构应用 PWA 升级指南
2018/05/25 · JavaScript
· PWA,
React
原稿出处:
林东洲
React/Redux创设的同构Web应用
2018/07/30 · CSS ·
React,
Redux
原稿出处: 原 一成(Hara
Kazunari) 译文出处:侯斌
世家好,小编是原10%(@herablog),如今在CyberAgent首要担任前端开发。
Ameblo(注: Ameba博客,Ameba
Blog,简称Ameblo)于二零一五年12月,将前端部分由原本的Java架构的利用,重构成为以node.js、React为底蕴的Web应用。那篇文章介绍了此次重构的起因、指标、系统规划以及最后完毕的结果。
新系统一发布表后,立刻就有人注意到了这么些转变。
twitter_msg.png
前言
近些年在给自身的博客网站 PWA 升级,顺便就记录下 React 同构应用在选拔 PWA
时蒙受的难题,那里不会从头开头介绍怎么样是 PWA,假设你想学习 PWA
相关文化,能够看下下边作者收藏的有的稿子:
- 你的第二个 Progressive Web
App - 【ServiceWorker】生命周期那几个事儿
- 【PWA学习与执行】(1)
2018,起初你的PWA学习之旅 - Progressive Web Apps (PWA)
中文版
系统重构的起因
2003年起,Ameblo成为了东瀛境内最大范围的博客服务。然则随着系统规模的加强,以及众多生死相依人口不停加码各类模块、页面携带链接等,最终使得页面彰显缓慢、对网页浏览量(PV)造成了老大惨重的熏陶。并且页面展现速度方面,绝当先四分之二是前者的难题,并非是后端的难点。
依照上述这个难题,我们决定以增加页面彰显速度为关键对象,对系统进行到底重构。与此同时后端系统也在拓展重构,将昔日的数量部分进行API化改造。此时就是二个将All-in-one的巨型Java应用举行适宜分割的绝佳良机。
PWA 特性
PWA 不是单纯的某项技术,而是一堆技术的集合,比如:ServiceWorker,manifest 添加到桌面,push、notification api 等。
而就在新近日子,IOS 11.3 刚刚扶助 Service worker 和接近 manifest
添加到桌面包车型客车性状,所以此次 PWA
改造首要依旧促成这两片段效用,至于此外的特点,等 iphone 协理了再升格吗。
目标
此次系统重构确立了以下多少个指标。
Service Worker
service worker
在笔者眼里,类似于多个跑在浏览器后台的线程,页面第③回加载的时候会加载这么些线程,在线程激活之后,通过对
fetch 事件,能够对每一个收获的能源开始展览支配缓存等。
页面呈现速度的革新(总之越快越好)
用以测定用户体验的目的有众多,大家认为在那之中对用户最重庆大学的目标正是页面展现速度。页面呈现速度越快,指标内容就能越快到达,让职分在短期内到位。这一次重构的目标是拼命三郎的涵养博客文章、以及在Ameblo内所彰显的五花八门的剧情的原本格局,在不损坏现有价值、体验的功底上,提升展现和页面行为的速度。
鲜明性哪些能源必要被缓存?
那正是说在初始采纳 service worker 在此以前,首先须求领会怎么财富须求被缓存?
系统的现代化(搭乘生态系统)
陈年的Web应用是将数据以HTML的格局重临,那几个时候并不曾什么难题。不过,随着剧情的充实,体验的丰盛化,以及配备的种种化,使得前端所占的比重越来越大。此前要支付贰个好的Web应用,假若要高质量,就自然毫无将左右端分隔断。当年以那几个要求开发的种类,在经历了10年之后,已经远远不能适应现阶段的生态系统。
「跟上目前生态系统」,以此来创设系统会带来不可估摸的便宜。因为作为主导的生态系统,其支付十一分活跃,天天都会有巨大新的idea。由此摩登的技术和效应更便于被吸收接纳,同时落实高品质也尤为简单。同时,这些「新」对于青春的技巧新人也愈加关键。仅知道旧规则旧技术的岳丈对于3个精美的团伙来说是未曾前途的(自觉本身膝盖也中了一箭)。
缓存静态能源
先是是像 CSS、JS 那么些静态财富,因为笔者的博客里引用的剧本样式都是经过 hash
做持久化缓存,类似于:main.ac62dexx.js
那样,然后打开强缓存,那样下次用户下次再拜访作者的网站的时候就绝不再行请求财富。间接从浏览器缓存中读取。对于这部分能源,service
worker 没须要再去处理,直接放行让它去读取浏览器缓存即可。
本人觉得一旦您的站点加载静态能源的时候笔者并未打开强缓存,并且你只想通过前端去贯彻缓存,而不需求后端在参加进行调整,那能够利用
service worker 来缓存静态能源,不然就有点画蛇添足了。
进步界面设计、用户体验(贰零壹伍年版Ameblo)
Ameblo的无绳电话机版在二零零六年经验了一次改版之后,就大多没有太大的更动。这几个中很多用户都曾经见惯司空了原生应用的陈设性和体验。那个项目也是为着不令人觉着很土很难用,达到顺应时期的二零一五年版界面设计和用户体验。
OK,接下去让作者切实详尽聊聊。
缓存页面
缓存页面显明是须求的,那是最宗旨的有个别,当您在离线的气象下加载页面会之后出现:
究其原因正是因为您在离线状态下不能够加载页面,以后有了 service
worker,即便你在没网络的情事下,也得以加载以前缓存好的页面了。
页面加载速度的改进
缓存后端接口数据
缓存接口数据是亟需的,但也不是必须透过 service worker
来落到实处,前端存放数据的地点有很多,比如通过 localstorage,indexeddb
来拓展仓库储存。那里笔者也是因而 service worker
来达成缓存接口数据的,假使想透过其余格局来贯彻,只须求留意好 url
路径与数量对应的照耀关系即可。
改善点
系统重构前,通过
SpeedCurve
实行解析,得出了上边结论:
- 服务器响应速度非常的慢
- HTML文书档案较大(页面全体因素都包罗在那之中)
- 堵塞页面渲染的财富(JavaScript、Stylesheet)较多
- 财富读取的次数过多,体量过大
听新闻说那些规定了上面这几项基本方针:
- 为了不致于下跌服务器响应速度,对代码进行优化,缓存等
- 尽也许减少HTML文书档案大小
- JavaScript异步地加载与实践
- 中期展现页面时,仅仅加载所需的必不可少财富
缓存策略
门到户说了怎么能源要求被缓存后,接下去就要钻探缓存策略了。
SSR还是SPA
多年来比较于添加到收藏夹中,用户更倾向于经过查找结果、Twitter、推特(TWTR.US)等应酬媒体上的享受链接打开博客页面。谷歌(Google)和Facebook的AMP,
Facebook的Instant
Article标志第③页的展现速度大幅度影响到用户满足度。
其余,从谷歌(Google)Analytics等日志记录中询问到在文章列表页面和前后作品间展开跳转的用户也很多。那说不定是因为博客作为个人媒体,当某一用户看到一篇不错的篇章,万分感兴趣的时候,他也同时想看一看同一博客内的别样小说。也正是说,博客那种服务
第3页火速加载与页面间快捷跳转同等首要 。
故而,为了让双方都能公布最好质量,大家决定在第3页使用服务器端渲染(Server-side
Rendering, SS路虎极光),从第1页起选拔单页面应用(Single Page Application,
SPA)。那样一来,既能确定保障率先页的展现速度和机械可读性(Machine-Readability)(含SEO),又能获得SPA带来的神速呈现速度。
BTW,对于近来的框架结构,由于服务器和客户端选取同样的代码,全体拓展SS大切诺基或是全体拓展SPA也是大概的。近年来已经落到实处尽管在不可能运作JavaScript的环境中,也得以寻常通过SS途锐来浏览。可以预知现在等到ServiceWorker普及之后,初步页面将越是高速化,而且能够实现离线浏览。
z-ssrspa.png
初始的系统完全选取SSENVISION,而前日的种类从第三页起变为SPA。
z-spa-speed.gif
SPA的魅力在于展现速度之快。因为唯有通过API获取所需的必备数据,所以速度尤其快!
页面缓存策略
因为是 React
单页同构应用,每趟加载页面包车型客车时候数据都以动态的,所以小编动用的是:
- 网络优先的不二法门,即优先获得网络上流行的财富。当网络请求退步的时候,再去取得
service worker 里以前缓存的财富 - 当网络加载成功今后,就更新 cache
中对应的缓存能源,保险下次历次加载页面,都以上次做客的风行财富 - 假定找不到 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’);
});
})
);
}
});
|
缘何存在命中穿梭缓存页面包车型大巴事态?
- 率先须要理解的是,用户在率先次加载你的站点的时候,加载页面后才会去运维sw,所以率先次加载不容许通过 fetch 事件去缓存页面
- 自个儿的博客是单页应用,但是用户并不一定会经过首页进入,有恐怕会经过此外页面路径进入到自个儿的网站,那就招致笔者在
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);
}
}
}
|
如此就保证了用户率先次加载页面,立刻离线访问站点后,即便不或者像第3次一样能够服务端渲染数据,不过随后能因此取得页面,异步加载数据的法门营造离线应用。
用户率先次访问站点,假诺在不刷新页面包车型客车状态切换路由到别的页面,则会异步获取到的数量,当下次拜会对应的路由的时候,则退步到异步获取数据。
当用户第一次加载页面包车型地铁时候,因为 service worker
已经控制了站点,已经拥有了缓存页面包车型客车能力,之后在做客的页面都将会被缓存可能更新缓存,当用户离线访问的的时候,也能访问到服务端渲染的页面了。
延迟加载
我们应用SS凯雷德+SPA的艺术来优化页面间跳转这种横向移动的进度,并且选取延缓加载来改革页面包车型大巴纵向移动速度。一开主要展现的剧情以及导航,还有博客文章等最早突显,在这么些情节之下的次要内容随着页面的滚动渐渐显现。那样一来,重要的剧情不会受页面下边内容的震慑而更快的突显出来。对于那么些想尽快读小说的用户来说,既不扩大用户体验上的下压力,又能全体的提供页面下方的始末。
z-lazyload.png
以前的种类因为将页面内的全部内容都置于HTML文书档案里,所以使得HTML文书档案体量不小。而明天的体系,仅仅将第3内容放到HTML里再次来到,减少了HTML的体积和数据请求的尺寸。
接口缓存策略
谈完页面缓存,再来讲讲接口缓存,接口缓存就跟页面缓存很类似了,唯一的不等在于:页面第三遍加载的时候不自然有缓存,但是会有接口缓存的存在(因为伪造了
cache 中的数据),所以缓存策略跟页面缓存类似:
- 网络优先的方法,即优先得到网络上接口数据。当互连网请求退步的时候,再去赢得
service worker 里从前缓存的接口数据 - 当网络加载成功未来,就创新 cache
中对应的缓存接口数据,保证下次每一回加载页面,皆以上次造访的新型接口数据
就此代码仿佛那样(代码类似,不再赘言):
self.add伊夫ntListener(‘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); }) ); } });
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缓存
博客作品是静态文书档案,对于特定ULacrosseL的乞求会回来固定的剧情,由此格外适合实行缓存。缓存使得服务器处理内容收缩,在增加页面响应速度的还要减轻了服务器的担当。我们将不变的内容(小说等)生成的HTML进行缓存再次来到,对于由于变化的始末能过JavaScript、CSS等展开操作(比如展现、隐藏等)。
z-newrelic-entrylist.png
那张图展现了二〇一五年六月最后30日New
relic上的总计数据。小说列表页面包车型地铁HTML的响应时间基本在50ms以下。
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,原因未知,有读者知道的话可以调换下。
今昔当您首先次加载页面,跳转路由,立即离线访问的页面,也能够万事大吉地加载页面了。
技术选型
本次项目标技巧选拔时,遵从了尽量使用当下当前市镇上曾经存在的普遍利用的技术这一标准化。暗号正是:「活脱脱像范例应用相同Start」。那样一来,无论是何人都足以轻松的拿走到对应的文书档案等音讯,同时别的的集体和卖家倘诺要参预到品种中来也能非常的慢的左手。不过在真正实行开发的时候,一些细节完毕上因为各样各个的由来存在有的例外的状态,不过在大幅程度上保证了逐条模块的独立性。最终系统的大体构成如下图所示:
z-bigpicture.png
(有些地点做了简约)
并非强缓存 sw.js
用户每便访问页面包车型客车时候都会去重新获得sw.js,依据文件内容跟在此之前的本子是不是相同来判断 service worker
是或不是有更新。所以假使您对 sw.js
开启强缓存的话,就将沦为死循环,因为每趟页面获得到的 sw.js
都是一样,那样就不只怕晋级你的 service worker。
其余对 sw.js 开启强缓存也是从未须求的:
- 本人 sw.js
文件自己就不大,浪费不了多少带宽,觉得浪费能够使用协议缓存,但附加增支负担 - sw.js 是在页面空闲的时候才去加载的,并不会影响用户首屏渲染速度
React with Redux
选取React和React进行付出的的时候,很多地方可以用 纯函数
的款型开展组合。纯函数是指特定的参数总是回到特定的结果,不会对函数以外的限定造成污染。使用纯函数举行支付能够保险种种处理模块最小化,不用操心会无意改变引用对象的值。那样一来,10分拉动大规模开发以及在相同客户端中保持多个状态。
界面更新的流水生产线是:
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。
举个例子来验证为啥:
- index.html 注册了 sw-v1.js 作为 sw
- sw-v1.js 对 index.html 做了缓存,也正是缓存优先(offline-first)
- 您更新了 index.html 重新注册了在新鸿营地产点的 sw sw-v2.js
假定你像上边那么做,用户永远也拿不到 sw-v2.js,因为 index.html 在
sw-v1.js 缓存中,那样的话,假设你想翻新为 sw-v2.js,还索要变更原来的
sw-v1.js。
同构Web应用(Isomorphic web app)
Ameblo
2014年版基本上完全是用JavaScript重写的。无论是Node服务器上或然客户端上都接纳了扳平的代码和流程,也正是所谓的同构Web应用。项指标目录结构大体上上如下所示,服务器端的进口文件是
server.js
,浏览器的进口文件是 client.js
。
actions/
Redux Action (服务器,客户端共用)api/
封装的API接口components/
React组件 (服务器,客户端共用)reducer/
<span class=”underline”>Redux
Reducers</span> (服务器,客户端共用)services/
服务层模型,使用
Fetchr
对数据请求举办适宜粒度的分开。同时这个也使得node.js作为代理,间接请求API(服务器专用)。server.js
服务器入口(服务器专用)app.js
node服务器的布置、运行,由server.js调用(服务器专用)client.js
客户端入口(客户端专用)
z-isomorphic.png
写好的JavaScript同时运营在劳动器端依旧客户端上的运维行为、以及从数额读取直到在页面上显得截止的一切浏程,都是同一的花样展开。
z-code-stats.png
利用Github的语言计算能够见见
,JavaScript占了整套项指标94.0%,大概全体都以由JavaScript写成的。
测试
其后,我们早就做到了运用 service worker
对页面进行离线缓存的功效,若是想体验效果的话,访问小编的博客:https://lindongzhou.com
自由浏览任意的页面,然后关掉互连网,再度做客,从前您浏览过的页面都能够在离线的事态下开始展览走访了。
IOS 供给 11.3 的版本才支撑,使用 Safari 进行访问,Android 请选用扶助service worker 的浏览器
原子设计(Atomic Design)
对此组件的安顿性,大家运用了
原子设计
理念。其实项目并从未一始发就利用原子设计,而是基于 Presentational and
Container
Components
,对 container
和 component
实行了两层划分。但是Ameblo中的组件实在是太多,很不难造成任务不明了的动静,因而最后选择了原子设计理念。项目标实际上选用中,选择了以下的平整。
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’,
},
})
]
})
|
不难易行地演说下安顿信息:
- name: 应用名称,正是图标上面的来得名称
- short_name: 应用名称,但 name 不能呈现完全时候则显得那一个
- background_color、theme_color:顾名思义,相应的颜色
- publicPath: 设置 cdn 路径,跟 webpack 里的 publicPath 一样
- icons: 设置图标,插件会活动帮您转移分歧 size
的图纸,不过图片大小必须超过最大 sizes - 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 文件是还是不是见效:
如此表达你的安排生效了,安卓机遇自动识别你的配置文件,并问询用户是或不是丰盛。
Organisms
页面上较大的一块组件,比如Header,Entry,Navi等。对于这一层的零件,能够在里面进行数量得到处理,以及采纳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
11.3 也可是帮忙 PWA 中的 service worker 和 app manifest
的效能,可是相信在不久的明日,其余的成效也会相应得到协助,到时候相信 PWA
将会在移动端绽放异彩的。
1 赞 收藏
评论
Template
梯次请求路径(U福特ExplorerL)所对应的组件。其职责是将所需的构件从Organisms中import过来,以一定的逐一和格式整合在一道。
Pages
用作页面包车型地铁页面组件。基本上是把传递过来的 this.props.children
原原本本的来得出来。由于Ameblo是单页面应用,因此唯有贰个页面组件。
CSS Modules
CSS样式表使用 CSS
Modules
将CSS样式规则的效用范围严俊界定到了逐条零部件内。各样样式规则的效益范围举行限制使得样式的转移和删除越发便于。因为Ameblo是由许多个人一道开发到位,不肯定每种人都理解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放到了必须的岗位,即便三个字母出错,整个项目也无能为力测试通过。指标就在于统一代码风格,节约代码审查时的分神。具体规则分别继承自
eslint-config-airbnb
和
stylelint-config-standard
,对于有些必需的细节做了有限定制。因为规则较严,初阶的时候大概有个别不方便。新成员出席项目组时,代码通过Lint测试便成了要经过的第叁关。
z-code-review.png
提防了代码审查时对于那么些微小写法挑错。被机器告知错误时,心境上会感觉稍好有的。
z-ci-error.png
参与项目组之后,最初的那段日子里发出Lint错误是一贯的事。
CI, Build, Tesing
代码的
构建
、测试
和
部署
统一选用CI(集团内部选择
CircleCI
)来完毕。各种分支向GHE(Github
Enterprise)PUSH之后,依照各种分支发生分歧的动作。这一个流程的便宜正是构建相关的拍卖不供给特地人士来成功,而是统一写在
circle.yml
和 package.json
(node环境下)里。
develop
开发(下次发表)用分支。创设、测试之后自动安顿到staging环境中。release/vX.X.X
发表分支。由develop分支派生,创设、测试之后,自动安顿到semi(准生育)环境中。hotfix/vX.X.X
hotfix分支。由master分支派生,创设、测试之后,自动安插到semi(准生育)环境中。deploy/${SERVER_NAME}
布置到支行所钦点的呼应服务器上。首纵然在支付条件中选用。master
那几个分支创设之后生成能够用于安插到production(生产)环境的docker镜像。其它
开发用分支。仅举办营造和测试。
Docker
此次系统重构,也对node.js应用实行docker化营造。这一次重构的是前者系统,大家盼望得以在微小考订之后立时展开计划。docker化之后,一旦将镜像营造形成,能够不受node模块版本的左右拓展配置,回滚也很简单。
除此以外,node.js本身公布尤其频仍,借使放置不管,不知不觉之间系统就成古董了。docker化之后,能够不受各主机环境的震慑自由的开展升级换代。
更要紧的是,设置docker容器数是比较简单的,那对于系统横向扩大体量以及对服务器配置作优化时也极度便于。
进步界面设计、用户体验(二〇一五年版Ameblo)
不再「咯噔」
系统重构从前的Ameblo由于存在一些惊人没有一定的模块,现身了「咯噔」现象。那种「咯噔」会招致误点击以及页面包车型客车重绘,十二分让人咳嗽。而此模块中度稳定也做为这次系统重构的UI设计的前提。尤其是页面间导航作为尤其珍视的要素,我们经过努力使得在页面跳转时老是都足以触击到平等的岗位。
z-gatan.gif
「咯噔」的贰个事例。点击[次のページ](下一页)的时候,额外的因素由于加载缓慢,造成误点击。
z-paging-fixed.gif
系统重构之后,成分的地点被一定下来,减轻了页面跳转时给用户思维上带来的承受。
智能手提式有线电话机时期的用户界面
2016年在移动环境下利用的用户大约都在动用智能手提式有线电话机。在智能手提式有线电电话机上,由于种种平台的提供者制定了独家区别的用户界面规范,用户已经习惯并适应了用户界面。相比较之下,虽说浏览器上的标准分外少,可是借使和今天风靡的界面差别太大的话,就会变得很难用。
Ameblo的手提式有线电电话机版在2009年开始展览改版之后,自然对有个别细节举办了立异,不过由于尚未太大的转移,所以现在看来众多地点早就给人一种很旧的印象。用户在浏览的时候,对于界面并不区分是原生应用照旧浏览器,由此制作出适应当下时代那一个平台的用户界面显得特别关键。那里介绍一下此次重构中,对于界面包车型大巴局地荣升。
z-update-design.png
内容占据界面上横向整个空间。二〇〇八年的时候,一般选取Facebook倡导的「将相继模块圈起来的设计」。
z-searchbar.gif
充实了导航栏,把导航相关操作集中停放在那里。
可访问性
此次系统重构正值可访问性成为热点话题的时候。仔细的为HTML扩充一定标签属生就能够使全体系列丰裕可访问。首先在HTML标签属性添加上时要用心探讨。对于标题、
img
等充裕适当的 alt
属性,对于可点击的要素一定要利用 a
button
等可点击的价签。假如能活动对可访问性实行验证就再好可是了,ESlint的
jsx-a11y
插件能够扶持成功那或多或少。
在类型进行的时候,正好公司内展开了三遍可访问性的就学活动( Designing
Web
Accessibility
的小编太田先生和伊原作化人也到庭了此次活动),在本次活动上也尝尝了Ameblo到方今截止没有留神过的语音朗读器。当时用语音朗读器在Ameblo上进行朗读时,有几处不日常的地点,使用
WAI-ARIA
对这几处加以订正(与 data-*
相同,JSX也支持 aria-*
属性)。
这里
的PPT中有详尽的牵线,欢迎旁观(日文)。
结果
OK,上边介绍了本次重构带来的众多转移,那么结果什么呢?
先是是性质相关目标(测试的ULANDL都以Ameblo中单一页面请求财富最多,体现速度最慢的页面)。
堵塞渲染的财富(Critical Blocking Resources)
z-speed-blocking.png
闭塞渲染的能源数 减少了75%
!JavaScript全部成为了异步读取与履行。CSS样式因为运行的原因,维持了重构前的情形。
剧情请求(Content Requests)
z-speed-requests.png
财富请求数 减少了58.04%
!由于使用了延期加载,首屏呈现只加载须要的能源,与此同时对文件进行适量的盘整,并删除了部分不供给的模块,最后落得了这些意况。
渲染(Rendering)
z-speed-rendering.png
渲染速度做为前端的主要质量指标,本次 提升了44.68% 。
页面加载时间(Page Load Time)
z-speed-pageload.png
页面加载时间 缩短了40.5 !其余,后端的归来时间也保持在了0.2ms ~
0.3ms之间。
接下去介绍一下相关的业务目的。
网页浏览量(Pageviews)
z-ga-pv.png
因为二零一四年四月有一个人盛名的博客主成为了热点话题,所以这几个指标内涵盖独特处境。网页浏览量升高了57.15%。假如将热点话题所推动的数值除去后,实际上不过由系统重构所牵动的升官在1/10到二成以内。
历次对话浏览页数 (Pages / Session)
z-ga-pps.png
Pages / Session是指在单个会话内页面包车型地铁浏览数,这么些指标 提升了35.54
。SPA改良了页面间跳转的快慢,获取了斐然的功能。
跳出率(Bounce Rate)
z-ga-bounce.png
跳出率指在三个对话内,仅看了一个页面包车型大巴比值,那些指标 改善了44.44%
。大家认为那是出于首屏和页面跳转速度的创新,用户界面升级(更便于领悟的分页),「咯噔」革新所推动的结果。
不过还存在重重改进的后路,任何2个指标都得以重复升高。大家想以此标志
网站质量的晋升会带来业务指标的升官 。
上述数量是在以下标准下获得的:
- 页面质量
- 使用
SpeedCurve - 测试的URL是
http://s.ameblo.jp/ebizo-ichikawa/entry-12152370365.html - 浏览器钦定为 Chrome, 53.0.2785.143移动端模拟形式
- 互连网内定为4G仿照格局(14.6 Mbps,Upload 7.8Mbps,Latency 53ms)
- 使用
- 业务目的
- 使用 Google
Analytics - 获取自 s.ameblo.jp 内的方方面面数额
- 对2015年十二月和二〇一六年7月的数值实行相比
- 使用 Google
写在终极
这一次系统重构的角度是对技术的挑衅,结果获得了优秀的用户反馈,并对业务作出了进献,我们本人也深感尤其有价值,得到了小幅的引以自豪。选拔新型迎合时流的技能自然提高服务的身分,也使得那种知识在店堂在生根。在此,对不久导入Isomorphic
JavaScript,并向北瀛国内推广的同事
@ahomu
表示感激!
作者介绍:
作者:原 一成(Hara Kazunari),2009年投入东瀛CyberAgent集团。担任Ameblo
二零一四移动前端改版项目总COO。著有《GitHubの教科書》,《CSS3逆引きデザインレシピ》,《フロントエンドエンジニア育成読本》。
翻译:侯 斌(Hou
Bin),2016年入职东瀛CyberAgent公司。现任Ameblo前端开发。在本次Ameblo
二零一五移动前端改版项目中担任机要费用,负责基础架构和技能选型以及重庆大学模块开发等。
1 赞 收藏
评论