升级指南,Redux打造的同构Web应用。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)于二零一五年六月,将前端部分由原先的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)
    中文版

系统重构的起因

2002年起,Ameblo成为了日本境内最大局面包车型大巴博客服务。不过随着系统规模的滋长,以及许多有关人口不停追加各样模块、页面引导链接等,最后使得页面呈现缓慢、对网页浏览量(PV)造成了13分惨重的熏陶。并且页面突显速度方面,绝半数以上是前者的难点,并非是后端的题材。

传闻上述这一个题材,我们决定以增强页面展现速度为首要目的,对系统举行彻底重构。与此同时后端系统也在开展重构,将过去的数量部分开始展览API化改造。此时便是3个将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"));

在那些事例中大家写死了数据,平常状态下,大家须求3个异步请求拉取数据,再将数据经过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,还协助图片文件、字体文件等),将其转移为2个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的无绳话机版在二零零六年经历了贰遍改版之后,就大约并未太大的更动。那里面很多用户都早已习惯了原生应用的筹划和心得。这几个项目也是为了不令人认为很土很难用,达到顺应时代的二〇一六年版界面设计和用户体验。

OK,接下去让自家具体详尽聊聊。

缓存页面

缓存页面明显是不可或缺的,那是最宗旨的一对,当您在离线的情状下加载页面会之后出现:

图片 2

究其原因就是因为您在离线状态下不可能加载页面,以往有了 service
worker,固然你在没网络的图景下,也足以加载从前缓存好的页面了。

页面加载速度的千锤百炼

缓存后端接口数据

缓存接口数据是索要的,但也不是必须通过 service worker
来贯彻,前端存放数据的地点有那几个,比如通过 localstorage,indexeddb
来拓展仓库储存。那里小编也是透过 service worker
来完成缓存接口数据的,如若想经过任何措施来兑现,只需求注意好 url
路径与数据对应的映照关系即可。

改善点

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

  • 服务器响应速度相当慢
  • HTML文书档案较大(页面全部因素都蕴涵个中)
  • 卡住页面渲染的财富(JavaScript、Stylesheet)较多
  • 能源读取的次数过多,容量过大

据说这一个规定了下边这几项基本方针:

  • 为了不致于降低服务器响应速度,对代码举行优化,缓存等
  • 尽或许减少HTML文书档案大小
  • JavaScript异步地加载与实践
  • 初期展现页面时,仅仅加载所需的画龙点睛资源

缓存策略

大廷广众了怎么样财富需求被缓存后,接下去就要研究缓存策略了。

SSR还是SPA

近来比较于添加到收藏夹中,用户更赞成于通过搜寻结果、Instagram(推特)、Instagram等社交媒体上的享受链接打开博客页面。谷歌和Facebook的AMP,
Facebook的Instant
Article标志第叁页的表现速度大幅度震慑到用户满足度。

别的,从谷歌Analytics等日志记录中领会到在篇章列表页面和内外小说间展开跳转的用户也很多。那恐怕是因为博客作为个体媒体,当某一用户观望一篇不错的篇章,卓殊感兴趣的时候,他也还要想看一看同一博客内的别的作品。也正是说,博客那种服务
首先页飞快加载与页面间快捷跳转同等首要

所以,为了让两岸都能发挥最佳质量,我们决定在首先页使用劳务器端渲染(Server-side
Rendering, SS帕杰罗),从第3页起利用单页面应用(Single Page Application,
SPA)。这样一来,既能确认保证率先页的体现速度和机械可读性(Machine-Readability)(含SEO),又能获得SPA带来的火速显示速度。

BTW,对于近日的架构,由于服务器和客户端接纳同样的代码,全部拓展SS翼虎或是全体拓展SPA也是唯恐的。近年来已经落到实处就算在无法运营JavaScript的环境中,也得以平常通过SS酷路泽来浏览。能够预言以往等到ServiceWorker普及之后,伊始页面将越是高速化,而且可以兑现离线浏览。

图片 3

z-ssrspa.png

原先的类别完全选择SS福特Explorer,而现行反革命的系统从第②页起变为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

当用户第2遍加载页面的时候,因为 service worker
已经控制了站点,已经持有了缓存页面包车型大巴能力,之后在做客的页面都将会被缓存恐怕更新缓存,当用户离线访问的的时候,也能访问到服务端渲染的页面了。

图片 7

推迟加载

我们利用SSTucson+SPA的点子来优化页面间跳转那种横向移动的速度,并且动用延缓加载来革新页面包车型客车纵向移动速度。一伊始要显现的始末以及导航,还有博客小说等最早显示,在这一个剧情之下的扶助内容随着页面包车型地铁轮转渐渐彰显。那样一来,主要的始末不会受页面上面内容的影响而更快的显得出来。对于那一个想不久读小说的用户来说,既不扩张用户体验上的压力,又能完整的提供页面下方的内容。

 图片 8

z-lazyload.png

事先的系统因为将页面内的全体内容都放到HTML文书档案里,所以使得HTML文档体量极大。而明天的系统,仅仅将重点内容放到HTML里重临,减弱了HTML的容积和数量请求的轻重。

网站地图xml地图