前端优化探讨:Cache

增强单页应用操作体验的一个重要思路就是增加本地(浏览器)缓存的处理机制。

良好的缓存机制有以下好处

  • 增加页面的响应速度
    • 如果之前访问过某资源,应直接返回缓存的该资源; 同时重新请求,并以静默的方式更新此资源
  • 对于后端API错误相应更加强健
    • 对于轮询的页面,在已有数据的情况下,应该返回缓存的内容
  • 减少API请求

由于增加缓存处理会给前端开发带来一些新的开发难度,现提供一些处理思路供参考:

  • 针对单个GET请求进行全局性缓存
    • 有的API,比如静态资源如 user profile / feature flags,可以做成用户单次加载控制台后全局缓存。这些数据由于很多组件或页面经常调用,而且有些组件严重依赖它们,缓存这些数据可以带来比较显著的加载速度提升。
  • 针对数据驱动的列表组件。稍后会重点讲一下分页列表组件的缓存处理思路
  • 针对路由跳转
    • 从不同路由跳转时,上下文之间很可能可以把Object通过ui-router v1.0的参数传递
    • 例如
      • 点击资源列表某一项,跳转到资源的详情页

Case Study: 列表

  • 列表一般需要有三种状态:
    • Loading
    • Load Error
      • 只有在没有缓存数据,并且API出错时,才应该提示错误,并提供刷新按钮
    • 正常数据显示
  • 切换分页页码,更新页面数据

根据分析,我把列表抽象成如下的显示逻辑:

pageno 为键缓存资源列表数据

用页码切换pageNoChange来做页面内容刷新当前页面的数据

pageNoChange分成三步走:

  • 载入当前缓存数据
  • 刷新当前页数据,并更新缓存
    • 当API出错时,标记loadError = true
  • 重新载入缓存数据

模版显示当前资源列表vm.list的数据

  • vm.listundefined
  • loadError = false, 认为数据正在加载,显示loading mask
  • loadError = true,认为API出错,应提供刷新按钮,绑定在pageNoChange
  • vm.list是数组时
    • 如果是空数组,显示空数据提示信息
    • 如果不为空,显示正常数据

Reactive Application with Firebase

Component interaction and using RxJs/observable

此篇文章是在下为公司内部交流所写。整理后放入私人博客供参考。

Component based APP

  • each major component is made with smaller components. In angular 1.4, we simulate components with directives and later in Angular 1.5 and 2.0 we will have standard component API
  • each component should only control its own view and data
  • each component (or a directive) should has its own set of bound input and output, like a well-defined public API. The parent and child talk to each other with the input and output functions or data.

This makes each component as a blackbox following the golden rule separation of concerns.

Here is a good article in Angular 1.5 official docs on Component-based application architecture.

Component interaction

The rules of using API for component interaction looks promising at first look, but we will soon have problems with it. Below is a common component tree of a complex view. Think that each box is a component (and each of them may also be consisted with smaller components).

A complex component based app

There are various ways of doing component interactions in a complex app with angular 1.x. One problem is that we don’t have a best practice guideline on how to do data flow and event handling. To name a few possible ways:

  1. emit/broadcast event on $scope chain. This may be the most common way of event handling in our app right now. The issues of using it:
    • scope object will be removed from Angular 2, which indicates that using this way is no longer encouraged
    • we need to be careful for the event flow. For example, by default an event will be broadcast from main to bottom and each controllers on the way will probably handle the event, but may be we just want to handle it once
    • it’s not possible to communicate between sibling components using this way. for example, we cannot pass an event from top to bottom, or between left and right. In the legacy app, we have a ChannelService to make event reflects in the scope hierarchy. In my opinion this solution is a bit hacky and hard to maintain
  2. introducing an angular factory to store a global state object and assign its reference to the directive’s controller, thus we could use the directive’s scope to bind watchers to the global state. However:
    • the watcher functions should be idempotent, otherwise we cannot guarantee termination of angular digest cycle
    • the watcher will always run when initializing, however the context may not be ready yet and there will be some dependency issues
    • the global state can be modified by any subject easily and the maintainer may not be aware that if there are side-effects modifying them.
  3. we have no context why the value changes when a watcher is triggered. require the parent controller, thus the child controller can have access to the parent’s controller object in the link hook.
    • since we add new dependencies, the reusability will be limited
  4. params passing via isolate scope. In angular 1.5, we can have one way binding to ensure one way binding. This looks like a standard way of defining input/output API.
    • it is easier to communicate between adjacent parent-child with this approach, but:
    • will be a lot harder if we want to pass data from Main to a deeper component such as bottom, since Main cannot directly have access to the Bottom‘s API. If we enforce this rule, we may need to define the same APIs for each component in the way.
  5. A basic Observer pattern implementation as a global Angular service. Since we often have prior knowledge on the possible events in the system, we can inject the event emitter to a global factory, so that each component can attach new listeners to the global event.
  6. plain old DOM events
    • Similar issues to the scope solution. We may not consider this solution since this is highly bound to DOM, which is not a good practice to do in Angular.

Summary

Here are some of my suggestions after listing all kinds of these component interaction solutions:

  • We probably should avoid using $scope for event handling from now on
  • Use the component API interaction styles whenever possible
  • Even though in a standard component based app, we may not need a global service for “long range” communication, however we always need some way to do it, especially between different major modules. For example, the activity log may need to listen to a global ‘open activity log’ event which may be emitted anywhere in the app.

In my opinion, what we need for now is a standalone messaging/component interaction data service to let the major components interact with each other. observer pattern could be a solution, but here I want to propose the use of RxJs into our app.

Using Reactive.js

What is Reactive and Observable

I don’t want to explain reactive programming deeply since this email could be a paper. You may find the following articles worth reading:

Angular 2 also has built-in RxJs usages in its http-client.

TL;DR

The key component of a reactive programming library is the Observable object. An Observable is essentially a data stream of values in time. In our case, the most important usage is to construct new global event streams, so that we could subscribe to them, and react to the new values.

Compared to plain observer pattern solution

The idea is quite similar to the observer pattern. However, the observer pattern is just an callback factory to be triggered sequentially when a new event emits. The Observable object also provides some other flexible functionalities:

  • event streams can be combined and transform them with functional operators like map, filters to produce new Observable streams. Also this gives us the ability to separate the listeners in different places without inference each other
  • unlike observer pattern, the event subscribe functions will be called asynchronously
  • and more …

Using in Angular 1.x

We could use RxJs/Observable in Angular 1.x system as an global Event manager service. Different components could create their own event streams (observables) and register in the manager with identifiers, which means we store all global event streams which may be used in different component in a key-value pairs map. So the subscribers could fetch the observables with keys, transform them into new Observables and then attach callbacks to the events.

将 Node 的异步函数调用转化为 Promise

借助于 ES7 的 async 语法,我们可以方便的使用 Promise 来书写优美的异步调用代码。 不过现有的 Node 版本,大部分的异步函数都还是回调函数方式接口的,比如 fs.read(fd, buffer, offset, length, position, callback)

笔者认为,使用 Promise 而不是回调函数的一大优势是: 将函数调用的责任转移到了使用异步函数者身上,而不是转嫁到异步函数去负责调用函数。 对于异步函数,我们其实最关心的是它的返回结果,然后根据结果做一些有用的事情。这样的调用才是更自然,符合直觉的编程方式。

好消息是,通过 Node 新版本原生支持的 Promise 类,我们可以很方便的将一个回调函数接口的函数转化成 Promise 版本的接口。 以最简单的 setTimeout 为例,我们使用它的一大作用是在当前的异步调用环境中加一个等待时间,然后接下来做一些别的事情。

改写 setTimeout


function foo() {
  setTimeout(() => {
    doSomething1();
    setTimeout(() => {
      doSomething2();
    }, 100);
  }, 100);
}

上面的形式就是回调函数经常会出现的问题。借助 Promise, 我们可以用下面的方式来改写:

// Returns a promise which will resolve in $milliseconds
function delay(milliseconds) {
  return new Promise(res => {
    setTimeout(res, milliseconds);
  });
}

async function awesomeFoo() {
  await delay(100);
  doSomething1();
  await delay(100);
  doSomething2();
}

是不是看起来清爽很多了呢?

改写 fs.readFile

对于更复杂一点的 fs.readFile(file, callback) 函数,我们可以将其以类似的方式改写成 Promise 版本。

传统方式调用如下

function foo() {
  fs.readFile('/etc/passwd', (err, data) => {
    if (err) throw err;
    console.log(data);
  });
}

改写后:


// Promise based fs.readFile
function awesomeReadFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if (err) reject(err);
      resolve(data);
    });
  });
}

async function awesomeFoo() {
  try {
    const data = await awesomeReadFile();
    console.log(data);
  } catch (err) {
    throw err;
  }
}

最后,希望新版本的 Node 可以提供原生的 Promise API。别做梦了