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

React 同构应用 PWA 升级指南

2018/05/25 · JavaScript
· PWA,
React

初稿出处:
林东洲   

Redux打造的同构Web应用,同构应用。React/Redux营造的同构Web应用

2018/07/30 · CSS ·
React,
Redux

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

我们好,笔者是原百分之十(@herablog),近期在CyberAgent首要担任前端开发。

Ameblo(注: Ameba博客,Ameba
Blog,简称Ameblo)于2014年三月,将前端部分由原来的Java架构的利用,重构成为以node.js、React为底蕴的Web应用。那篇文章介绍了此次重构的起因、指标、系统规划以及最后达成的结果。

新系统公布后,立时就有人注意到了那几个变化。

 亚洲城ca88手机版官网 1

twitter_亚洲城ca88手机版官网,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)造成了12分惨重的熏陶。并且页面突显速度方面,绝大多数是前者的难点,并非是后端的题材。

听说上述那么些题材,大家决定以提升页面突显速度为第壹目的,对系统进行彻底重构。与此同时后端系统也在举办重构,将过去的数据部分进行API化改造。此时正是2个将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语法等级、插件等。

此地大家给2个安排 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"));

在那几个例子中大家写死了数据,常常情状下,我们须要1个异步请求拉取数据,再将数据通过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
等文件了。为了缓解那一个难题,必要3个十一分的工具
webpack-isomorphic-tools,援助识别
less 等文件。

简单地说,webpack-isomorphic-tools,完成了两件事:

  • 以webpack插件的样式,预编写翻译less(不囿于于less,还帮助图片文件、字体文件等),将其转移为三个assets.json 文件保留到品种目录下。
  • require hook,全体less文件的引入,代理到变化的 JSON
    文件中,匹配文件路径,再次回到二个优先编写翻译好的 JSON 对象。

何以规律

实质上,react同构开发并没有上面包车型大巴例子那么简单。上面的例证只是为了求证服务端渲染与客户端渲染的中央分化点。其实,及时已经在服务端渲染好了页面,我们依然要在客户端重新选用ReactDom.render函数在render3次的。因为所谓的服务端渲染,仅仅是渲染静态的页面内容而已,并不做任何的轩然大波绑定。全数的事件绑定都以在客户端实行的。为了制止客户端重复渲染,React提供了一套checksum的体制。所谓checksum,正是React在服务端渲染的时候,会为组件生成对应的校验和(checksum),这样客户端React在处理同1个零部件的时候,会复用服务端已变更的早先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的样式再次回到,那个时候并不曾什么难点。不过,随着情节的扩充,体验的丰盛化,以及设备的多种化,使得前端所占的比例进一步大。以前要支付1个好的Web应用,假如要高质量,就必定不要将左右端分隔绝。当年以这几个供给支付的种类,在经验了10年过后,已经远远不恐怕适应当前的生态系统。

「跟上近年来生态系统」,以此来营造系统会带来巨大的裨益。因为作为大旨的生态系统,其支付相当活跃,每一日都会有大批判新的idea。由此新型的技能和遵循更易于被选拔,同时完成高品质也更是便于。同时,那些「新」对于身强力壮的技术新人也越加首要。仅知道旧规则旧技术的四叔对于二个绝妙的团伙来说是绝非前途的(自觉自己膝盖也中了一箭)。

缓存静态财富

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

自家以为只要您的站点加载静态财富的时候本人没有打开强缓存,并且你只想经过前端去贯彻缓存,而不要求后端在加入实行调整,那能够行使
service worker 来缓存静态能源,不然就有点画蛇添足了。

晋级界面设计、用户体验(贰零壹肆年版Ameblo)

Ameblo的手提式有线电话机版在2009年经验了二遍改版之后,就大多没有太大的浮动。那其中很多用户都曾经熟视无睹了原生应用的规划和体会。那几个体系也是为了不令人觉着很土很难用,达到顺应时期的二〇一四年版界面设计和用户体验。

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

缓存页面

缓存页面显明是必不可少的,那是最宗旨的片段,当你在离线的情事下加载页面会之后出现:

亚洲城ca88手机版官网 2

究其原因正是因为您在离线状态下不能够加载页面,以后有了 service
worker,固然你在没互联网的场地下,也能够加载在此以前缓存好的页面了。

页面加载速度的立异

缓存后端接口数据

缓存接口数据是内需的,但也不是必须透过 service worker
来兑现,前端存放数据的地方有好多,比如通过 localstorage,indexeddb
来开始展览仓库储存。那里自个儿也是因而 service worker
来促成缓存接口数据的,借使想经过其余措施来实现,只须要留意好 url
路径与数量对应的照射关系即可。

改善点

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

  • 服务器响应速度相当慢
  • HTML文书档案较大(页面全体因素都饱含在那之中)
  • 闭塞页面渲染的能源(JavaScript、Stylesheet)较多
  • 资源读取的次数过多,体积过大

基于这么些规定了上边这几项基本方针:

  • 为了不致于下降服务器响应速度,对代码举办优化,缓存等
  • 尽可能收缩HTML文书档案大小
  • JavaScript异步地加载与实践
  • 早期彰显页面时,仅仅加载所需的不可或缺财富

缓存策略

一目理解了何等能源须要被缓存后,接下去就要讨论缓存策略了。

SSR还是SPA

近期比较于添加到收藏夹中,用户更倾向于经过搜寻结果、推文(Tweet)、Instagram等应酬媒体上的享受链接打开博客页面。谷歌和Instagram的AMP,
Facebook的Instant
Article标明第2页的显现速度大幅影响到用户满足度。

别的,从GoogleAnalytics等日志记录中询问到在小说列表页面和前后小说间实行跳转的用户也很多。那大概是因为博客作为个体媒体,当某一用户阅览一篇不错的文章,相当感兴趣的时候,他也还要想看一看同一博客内的其余小说。相当于说,博客那种服务
率先页连忙加载与页面间快捷跳转同等主要

之所以,为了让两者都能公布最佳质量,我们决定在第叁页使用劳务器端渲染(Server-side
Rendering, SSKoleos),从第贰页起利用单页面应用(Single Page Application,
SPA)。那样一来,既能确定保证率先页的体现速度和机器可读性(Machine-Readability)(含SEO),又能获取SPA带来的快捷展现速度。

BTW,对于日前的架构,由于服务器和客户端应用同样的代码,全部进展SSKoleos或是全体进展SPA也是恐怕的。近年来早已落到实处固然在不可能运作JavaScript的条件中,也足以平日通过SSR来浏览。可以预知今后等到ServiceWorker普及之后,初阶页面将更为高速化,而且可以兑现离线浏览。

亚洲城ca88手机版官网 3

z-ssrspa.png

先前的系列完全使用SSKuga,如今日的系统从第贰页起变为SPA。

 亚洲城ca88手机版官网 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);
    }
  }
}

诸如此类就保证了用户率先次加载页面,立时离线访问站点后,纵然不只怕像第三次一样能够服务端渲染数据,不过随后能因而获得页面,异步加载数据的法子塑造离线应用。

亚洲城ca88手机版官网 5

用户率先次访问站点,如若在不刷新页面包车型客车动静切换路由到其它页面,则会异步获取到的多少,当下次走访对应的路由的时候,则失利到异步获取数据。

亚洲城ca88手机版官网 6

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

亚洲城ca88手机版官网 7

延期加载

咱俩选拔SSKuga+SPA的章程来优化页面间跳转那种横向移动的速度,并且应用延缓加载来改进页面包车型客车纵向移动速度。一初阶要彰显的内容以及导航,还有博客小说等最早呈现,在那些剧情之下的附带内容随着页面包车型地铁轮转逐步展现。那样一来,首要的内容不会受页面上边内容的熏陶而更快的显示出来。对于那多少个想尽快读小说的用户来说,既不扩张用户体验上的压力,又能全体的提供页面下方的内容。

 亚洲城ca88手机版官网 8

z-lazyload.png

事先的系统因为将页面内的全体内容都放到HTML文书档案里,所以使得HTML文书档案容量非常大。而后天的种类,仅仅将重点内容放到HTML里重回,收缩了HTML的体量和数量请求的轻重。

发表评论

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

网站地图xml地图