Redux打造的同构Web应用,同构应用

React 同构应用 PWA 升级指南

2018/05/25 · JavaScript
· PWA,
React

原稿出处:
林东洲   

React/Redux创设的同构Web应用

2018/07/30 · CSS ·
Redux打造的同构Web应用,同构应用。React,
Redux

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

大家好,我是原一成(@herablog),近来在CyberAgent紧要担任前端开发。

Ameblo(注: Ameba博客,Ameba
Blog,简称Ameblo)于二零一六年8月,将前端部分由原来的Java架构的施用,重构成为以node.js、React为根基的Web应用。那篇小说介绍了此次重构的导火线、目标、系统规划以及尾声达到的结果。

新系列公布后,登时就有人注意到了那么些转变。

 图片 1

twitter_msg.png

React 同构

所谓同构,简单的讲就是客户端的代码可以在服务端运行,好处就是能大幅度的升官首屏时间,幸免白屏,别的同构也给SEO提供了许多有利。

React 同构得益于 React 的虚构 DOM。虚拟 DOM
以对象树的款式保留在内存中,并留存前后端三种表现格局。

  • 在客户端上,虚拟 DOM 通过 ReactDOM 的 render
    方法渲染到页面中,形成实事求是的 dom。
  • 在服务端上,React 提供了此外多个法子: ReactDOMServer.renderToString
    和 ReactDOMServer.renderToStatic马克up 将虚拟 DOM 渲染为 HTML
    字符串。

在服务端通过 ReactDOMServer.renderToString 方法将虚拟 DOM 渲染为 HTML
字符串,到客户端时,React 只须要做一些事变绑定等操作就能够了。

在这一整套流水线中,有限支撑 DOM 结构的一致性是主要的一些。 React 通过
data-react-checksum来检测一致性,即在服务端暴发 HTML
字符串的时候会卓越的盘算一个 data-react-checksum
值,客户端会对那几个值举行校验,若是与客户端统计的值一致,则 React
只会展开事件绑定,即便不等同,React 会舍弃服务端再次回到的 dom
结构重新渲染。

干什么要做同构

要应对那些题目,首先要问哪些是同构。所谓同构,顾名思义就是平等套代码,既可以运行在客户端(浏览器),又足以运作在服务器端(node)。

咱俩驾驭,在前端的开销进度中,大家一般都会有一个index.html,
在这一个文件中写入页面的骨干内容(静态内容),然后引入JavaScript脚本按照用户的操作更改页面的情节(数据)。在性质优化方面,寻常大家所说的各样优化措施也都是在这几个基础之上进行的。在那个形式下,前端有着的干活就好像都被限制在了这一亩三分地之上。

那么同构给了我们怎么样的不一样啊?前边说到,在同构格局下,客户端的代码也可以运行在服务器上。换句话说,大家在服务器端就足以将差其他多寡组装成页面重回给客户端(浏览器)。那给页面的性质,尤其是首屏品质带来了光辉的提拔可能。其余,在SEO等地方,同构也提供了高大的造福。除此以外,在全体开发进度中,同构会极大的下跌前后端的沟通费用,后端越发小心于工作模型,前端也可以小心于页面开发,中间的数据转换大能够交给node这一层来落实,省去了恒河沙数来回交换的工本。

前言

近来在给自家的博客网站 PWA 升级,顺便就记录下 React 同构应用在动用 PWA
时碰着的题材,那里不会从头开头介绍怎样是 PWA,要是你想上学 PWA
相关文化,可以看下下边我收藏的有的文章:

  • 您的首先个 Progressive Web
    App
  • 【ServiceWorker】生命周期这一个事儿
  • 【PWA学习与实施】(1)
    2018,开首你的PWA学习之旅
  • Progressive Web Apps (PWA)
    中文版

系统重构的缘起

二〇〇四年起,Ameblo成为了东瀛国内最大范围的博客服务。可是随着系统规模的进步,以及许多相关人士不断追加各样模块、页面辅导链接等,最后使得页面显示缓慢、对网页浏览量(PV)造成了要命严重的影响。并且页面显示速度方面,绝半数以上是前者的难题,并非是后端的难题。

据悉上述这个难题,大家决定以坚实页面呈现速度为重点对象,对系统进行到底重构。与此同时后端系统也在展开重构,将昔日的数额部分开展API化改造。此时正是一个将All-in-one的特大型Java应用举行恰当分割的绝佳良机。

服务端对 ES6/7 的支撑

React 新本子中早就在举荐应用 ES6/7 开发组件了,因而服务端对 ES6/7
的援救也不得不跟上大家开发组件的步子。不过现在 node 原生对 ES6/7
的接济还比较弱,那些时候我们就要求借助 babel 来形成 ES6/7 到 ES5
的转换。这一转移,大家由此
babel-register 来完成。

babel-register 通过绑定 require 函数的方法(require hook),在 require
jsx 以及利用 ES6/7 编写的 js 文件时,使用 babel
转换语法,因此,应该在此外 jsx 代码执行前,执行
require(‘babel-register’)(config),同时通过配备项config,配置babel语法等级、插件等。

此间大家给一个布署 demo,
具体安顿方式可参看官方文档。

{
  "presets": ["react", "es2015", "stage-0"],

  "plugins": [
    "transform-runtime",
    "add-module-exports",
    "transform-decorators-legacy",
    "transform-react-display-name"
  ],

  "env": {
    "development": {
      "plugins": [
        "typecheck",
        ["react-transform", {
            "transforms": [{
                "transform": "react-transform-catch-errors",
                "imports": ["react", "redbox-react"],
                "locals": ["module"]
              }
            ]
        }]
      ]
    }
  }
}

根据React的同构开发

说了如此多,如何是好同构开发呢?
那还得归功于 React提供的服务端渲染。

ReactDOMServer.renderToString  
ReactDOMServer.renderToStaticMarkup

不同于 ReactDom.render将DOM结构渲染到页面,
那五个函数将虚拟DOM在服务端渲染为一段字符串,代表了一段完整的HTML结构,最后以html的款式吐给客户端。

下边看一个粗略的例子:

// 定义组件 
import React, { Component, PropTypes } from 'react';

class News extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        var {data} = this.props;
        return <div className="item">
      <a href={data.url}>{ data.title }</a>
    </div>;
    }
}

export default News;

咱俩在客户端,经常通过如下格局渲染那么些组件:

// 中间省略了很多其他内容,例如redux等。
let data = {url: 'http://www.taobao.com', title: 'taobao'}
ReactDom.render(<News data={data} />, document.getElementById("container"));

在那些例子中我们写死了数额,寻常情况下,我们须求一个异步请求拉取数据,再将数据通过props传递给News组件。那时候的写法就象是于那般:

Ajax.request({params, success: function(data) {
    ReactDom.render(<News data={data} />, document.getElementById("container"));    
}});

此时,异步的光阴哪怕用户实际等待的时光。

那就是说,在同构情势下,我们怎么办吧?

// 假设我们的web服务器使用的是KOA,并且有这样的一个controller  
function* newsListController() {

  const data = yield this.getNews({params});

  const data = {
    'data': data
  };

  this.body = ReactDOMServer.renderToString(News(data));
};

那样的话,我么在服务端就生成了页面的装有静态内容,直接的机能就是压缩了因为首屏数据请求导致的用户的等候时间。除此以外,在禁用JavaScript的浏览器中,我们也得以提供丰富的多寡内容了。

PWA 特性

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

而就在如今岁月,IOS 11.3 刚刚接济 Service worker 和相近 manifest
添加到桌面的特性,所以这一次 PWA
改造重点仍旧贯彻那两有的功效,至于其他的风味,等 iphone 帮助了再进步吗。

目标

这次系统重构确立了以下多少个目的。

css、image 等公事服务端怎么样支撑

相似意况来说,不须求服务端处理非js文件,不过只要直白在服务端 require
一个非 js 文件的话会报错,因为 require 函数不认得非 js
文件,那时候我们须要做如下处理, 已样式文件为例:

var Module = require('module');
Module._extensions['.less'] = function(module, fn) {
  return '';
};
Module._extensions['.css'] = function(module, fn) {
  return '';
};

切切实实原理可以参照require
解读

或者直接在 babel-register 中配置忽略规则:

require("babel-register")({
  ignore: /(\.css|\.less)$/,
});

然则,若是项目中运用了 css_modules 的话,那服务端就非得要处理 less
等公事了。为了化解那几个标题,要求一个附加的工具
webpack-isomorphic-tools,帮忙识别
less 等公事。

简易地说,webpack-isomorphic-tools,完毕了两件事:

  • 以webpack插件的方式,预编译less(不囿于于less,还辅助图片文件、字体文件等),将其更换为一个
    assets.json 文件保留到项目目录下。
  • require hook,所有less文件的引入,代理到变化的 JSON
    文件中,匹配文件路径,重返一个优先编译好的 JSON 对象。

什么规律

实在,react同构开发并不曾地点的例子那么粗略。上边的例证只是为了验证服务端渲染与客户端渲染的主旨分化点。其实,及时已经在服务端渲染好了页面,大家照旧要在客户端重新行使ReactDom.render函数在render五遍的。因为所谓的服务端渲染,仅仅是渲染静态的页面内容而已,并不做别的的风浪绑定。所有的轩然大波绑定都是在客户端举办的。为了防止客户端重复渲染,React提供了一套checksum的体制。所谓checksum,就是React在服务端渲染的时候,会为组件生成对应的校验和(checksum),那样客户端React在拍卖同一个组件的时候,会复用服务端已成形的初叶DOM,增量更新,那就是data-react-checksum的功能。

所以,最后,大家的同构应该是以此样子的:

// server 端  
function* newsListController() {

  const data = yield this.getNews({params});

  const data = {
    'data': data
  };
  let news = ReactDOMServer.renderToString(News(data));
  this.body = '<!doctype html>\n\
                      <html>\
                        <head>\
                            <title>react server render</title>\
                        </head>\
                        <body><div id="container">' +
                            news +
                            '</div><script>var window.__INIT_DATA='+ JSON.stringify(data) +'</script><script src="app.js"></script>\
                        </body>\
                      </html>';
};

// 客户端,app.js中  
let data = JSON.parse(window.__INIT_DATA__);  
ReactDom.render(<News props={data} />, document.getElementById("container"));

Service Worker

service worker
在我看来,类似于一个跑在浏览器后台的线程,页面第五回加载的时候会加载这几个线程,在线程激活之后,通过对
fetch 事件,可以对各样得到的资源举行控制缓存等。

页面显示速度的创新(由此可见越快越好)

用来测定用户体验的目标有好多,大家觉得其中对用户最关键的目标就是页面彰显速度。页面突显速度越快,目的内容就能越快到达,让任务在长期内形成。本次重构的对象是尽可能的有限协助博客小说、以及在Ameblo内所显示的繁多的内容的原本格局,在不损坏现有价值、体验的底子上,提升突显和页面行为的速度。

构建

客户端的代码通过安顿 webpack 打包揭橥到 CDN 即可。

因此安排 webpack 和 webpack-isomorphic-tools 将非 js 文件打包成 assets
文件即可。

小结

近年直接在做同构相关的事物,本文首要琢磨react同构开发的基本原理和办法,作为一个引子,其中省去了广大细节难点。关于同构应用开发,其实有众多事情要做,比如node应用的发表、监控、日志管理,react组件是还是不是满意同构必要的自动化检测等。这一个工作都是一而再要一步一步去做的,到时候也会做一些收拾和积累。

妇孺皆知什么资源须要被缓存?

那么在上马利用 service worker 此前,首先须要知道什么资源要求被缓存?

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

陈年的Web应用是将数据以HTML的款式再次回到,这些时候并没有何问题。不过,随着情节的增多,体验的丰硕化,以及配备的各类化,使得前端所占的比重进一步大。从前要开发一个好的Web应用,若是要高质量,就必将不要将左右端分隔开。当年以那么些必要支付的体系,在经验了10年将来,已经远远不可能适应当下的生态系统。

「跟上近期生态系统」,以此来构建系统会推动不可估摸的利益。因为作为中央的生态系统,其支付相当活跃,每一日都会有数以百计新的idea。由此新式的技术和效益更便于被收取,同时落到实处高品质也越加不难。同时,那个「新」对于青春的技巧新人也进一步关键。仅知道旧规则旧技术的伯伯对于一个理想的协会来说是尚未前途的(自觉本人膝盖也中了一箭)。

缓存静态资源

第一是像 CSS、JS 这么些静态资源,因为自身的博客里引用的剧本样式都是经过 hash
做持久化缓存,类似于:main.ac62dexx.js 这样,然后打开强缓存,那样下次用户下次再拜访我的网站的时候就毫无再行请求资源。直接从浏览器缓存中读取。对于那部分资源,service
worker 没要求再去处理,直接放行让它去读取浏览器缓存即可。

自家以为即使你的站点加载静态资源的时候自己并未开启强缓存,并且你只想透过前端去完成缓存,而不须求后端在参加进行调整,那可以行使
service worker 来缓存静态资源,否则就有点画蛇添足了。

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

Ameblo的无绳电话机版在2010年经历了三回改版之后,就大约并未太大的变型。那之中很多用户都已经习惯了原生应用的统筹和经验。那么些类型也是为着不令人觉得很土很难用,达到顺应时代的二零一六年版界面设计和用户体验。

OK,接下去让自身实际详细聊聊。

缓存页面

缓存页面明显是不可或缺的,那是最基本的局地,当您在离线的情形下加载页面会之后出现:

图片 2

究其原因就是因为您在离线状态下不可以加载页面,现在有了 service
worker,即便你在没互联网的境况下,也可以加载此前缓存好的页面了。

页面加载速度的革新

缓存后端接口数据

缓存接口数据是索要的,但也不是必须经过 service worker
来贯彻,前端存放数据的地点有广大,比如通过 localstorage,indexeddb
来拓展仓储。那里我也是因此 service worker
来兑现缓存接口数据的,假如想通过其他方法来贯彻,只要求小心好 url
路径与数据对应的炫耀关系即可。

改善点

系统重构前,通过
SpeedCurve
进行分析,得出了上边结论:

  • 服务器响应速度很快
  • HTML文档较大(页面所有因素都饱含其中)
  • 卡住页面渲染的资源(JavaScript、Stylesheet)较多
  • 资源读取的次数过多,体积过大

按照那一个规定了下边这几项基本方针:

  • 为了不致于下跌服务器响应速度,对代码举行优化,缓存等
  • 尽可能减弱HTML文档大小
  • JavaScript异步地加载与履行
  • 前期显示页面时,仅仅加载所需的必不可少资源

缓存策略

不言而喻了哪些资源必要被缓存后,接下去就要商讨缓存策略了。

SSR还是SPA

多年来比较于添加到收藏夹中,用户更倾向于经过查找结果、脸书、推特(TWTR.US)等应酬媒体上的享受链接打开博客页面。谷歌和推文(Tweet)的AMP,
Facebook的Instant
Article标志第一页的变现速度大幅度影响到用户满足度。

除此以外,从谷歌Analytics等日志记录中打探到在小说列表页面和左右文章间开展跳转的用户也很多。这说不定是因为博客作为个体媒体,当某一用户观察一篇不错的篇章,万分感兴趣的时候,他也同时想看一看同一博客内的其他小说。也就是说,博客那种服务
率先页疾速加载与页面间快捷跳转同等重要

据此,为了让两者都能表明最佳品质,大家决定在首先页使用劳务器端渲染(Server-side
Rendering, SSR),从第二页起利用单页面应用(Single Page Application,
SPA)。那样一来,既能确保率先页的突显速度和机械可读性(Machine-Readability)(含SEO),又能获取SPA带来的很快突显速度。

BTW,对于近期的架构,由于服务器和客户端采取同样的代码,全体展开SSR或是全体拓展SPA也是可能的。近年来已经落到实处即便在无法运作JavaScript的环境中,也得以正常通过SSR来浏览。可以预言以后等到ServiceWorker普及之后,早先页面将越是高速化,而且能够兑现离线浏览。

图片 3

z-ssrspa.png

早先的系统完全采纳SSR,而明日的连串从第二页起变为SPA。

 图片 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. 终极促成的功力是:用户率先次打开页面,马上断掉网络,依旧能够离线访问我的站点

组合地点三点,我的艺术是:第一次加载的时候会缓存 /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);
    }
  }
}

那样就有限支撑了用户率先次加载页面,立时离线访问站点后,纵然不能像第五遍一样可以服务端渲染数据,可是随后能经过取得页面,异步加载数据的章程创设离线应用。

图片 5

用户率先次访问站点,即使在不刷新页面的情形切换路由到任何页面,则会异步获取到的数目,当下次做客对应的路由的时候,则失败到异步获取数据。

图片 6

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

图片 7

推迟加载

大家拔取SSR+SPA的措施来优化页面间跳转那种横向移动的快慢,并且选拔延缓加载来改革页面的纵向移动速度。一先导要显示的内容以及导航,还有博客作品等最早呈现,在那么些内容之下的附带内容随着页面的滚动逐步显现。这样一来,紧要的内容不会受页面上面内容的熏陶而更快的突显出来。对于那多少个想趁早读文章的用户来说,既不增加用户体验上的下压力,又能整体的提供页面下方的情节。

 图片 8

z-lazyload.png

事先的系统因为将页面内的全部内容都停放HTML文档里,所以使得HTML文档体积很大。而明天的系统,仅仅将主要内容放到HTML里重回,裁减了HTML的体积和数据请求的分寸。

相关文章