react服务端渲染实践

文章目录
  1. 白屏对网站体验的影响
  2. 服务端渲染 vs 客户端渲染
  3. 为何放弃传统服务端渲染的方式
  4. 为何放弃SPA开发模式
  5. nodeJs引领的全栈开发模式
  6. react带来的同构开发模式
  7. 同构demo
  8. 总结
  9. 参考文献

前言
这篇主要总结为什么使用服务端渲染以及如何利用react的特性作服务端同构渲染,目前只是完成了一个demo网站,之后会将网站放在线上,在线上环境下,详细比较服务端渲染对于客户端渲染的提升。

白屏对网站体验的影响

在日常上网中,我们总是在追求更好的体验,其中有一项对于体验影响极大的参数,那就是网站白屏时间。通俗一点来说就是我们打开网站到网站开始出现文字图片之间的那段整个屏幕空白时间,如果这段时间很长,可想而知我们会感觉这个网站非常慢,所以这是一个必须要解决的问题。

服务端渲染 vs 客户端渲染

其实这个问题在以前的网站中并不是一个大的问题,因为以前的网站通常采用jsp或php编写页面,它们实际上就是走服务端渲染的路线,然而如今很多网站架构转变成了前后端分离式架构,渲染路线由服务端转变为了客户端渲染,于是就有了白屏过长的问题。这是为什么呢?很简单,因为渲染路线被拉长了。
客户端渲染路线:1. 请求一个html -> 2. 服务端返回一个html -> 3. 浏览器下载html里面的js/css文件 -> 4. 等待js文件下载完成 -> 5. 等待js加载并初始化完成 -> 6. js代码终于可以运行,由js代码向后端请求数据( ajax/fetch ) -> 7. 等待后端数据返回 -> 8. 客户端 从无到完整地,把数据渲染为响应页面

服务端渲染路线:2. 请求一个html -> 2. 服务端请求数据( 内网请求快 ) -> 3. 服务器初始渲染(服务端性能好,较快) -> 4. 服务端返回已经有正确内容的页面 -> 5. 客户端请求js/css文件 -> 6. 等待js文件下载完成 -> 7. 等待js加载并初始化完成 -> 8. 客户端 把剩下一部分渲染完成( 内容小,渲染快 )
说明:对同一个组件,服务端渲染“可视的”一部分( render/componentWillMount部分代码 ),为确保组件有完善的生命周期及事件处理,客户端需要再次渲染。即:服务端渲染,实际上也是需要客户端进行 再次地、但开销很小的二次渲染。

对比上面两条路线,客户端在第8步的时候才进行页面的渲染,而服务端则在4步就能将主要的页面呈现给我们,更不用说服务端渲染页面速度更快,获取初始数据更快,当然高并发下服务端渲染对服务器的压力会比较大,但是在实际情况下,可以利用集群,异步的方式来减轻服务器压力,而客户端则是我们无法控制的,所以服务端渲染是值得尝试的优化手段。

为何放弃传统服务端渲染的方式

既然jsp,php本身就是采用服务端渲染的,我们又在烦恼什么呢,直接采用jsp,php来渲染不好吗?是,如果你是个精通java,php又会js、css、html的人那么用它们来写页面可以,但是很多人并不是这么全能的人,往往写前端的人并不很懂php、java这些,那他们开发jsp、php相当于需要新学一门语言,更何况调试这些页面还得后端服务器的支持,极大的限制前端人员的开发。

为何放弃SPA开发模式

为了明确前后端的职责分工,我们提出前后端的分离的SPA(单页面应用)型开发模式。其主要思想为:将从数据处理和页面渲染交由客户端的js来进行,而动态数据则通过ajax请求服务端接口获取,形成了下图所示的结构:
Alt text
这种模式下,前后端的分工非常清晰,看起来是如此美妙,然而性能问题却非常突出,其中最严重的莫过于上面提及的极长的页面白屏时间,而且等于完全放弃了同步场景。

nodeJs引领的全栈开发模式

SPA式的前后端,是从物理的层面去区分前端和后端,认为客户端就是前端负责,服务端就是后端负责,而随着nodejs推广,js开始进入到了服务器端开发,前端可以也从事服务器开发了,于是我们就有了从职责上去划分前后端的想法并赋予前端更大的掌握领域,具体来说就是:

  • 前端:负责View和Controller层。
  • 后端:只负责Model层,业务处理/数据等。
    当前端掌握了Controller层,就可以自己实现服务端渲染,根据场景来选择做服务端渲染还是客户端渲染,可以自行设计渲染路由,且只需同后端协商好接口设计之后,便能够利用mock数据进行调试,不在受后端服务器的限制,可以说同时获得前两种模式的优点,当然对前端自己的要求也变高了,需要了解服务器端的相关知识。

    react带来的同构开发模式

    当我们实现服务渲染时,我们需要一套模板来将动态数据渲染为html,但不是所有的地方都需要服务端进行渲染,对于简单页面,或者动态切换的部分页面,无需从服务端进行渲染,我们自然在客户端也会保有一套渲染模板,那么我可不可以只写一套模板,然后服务端和客户端都可以使用这套模板来做。这是可行的,这种开发模式叫做同构开发。然而同构是种很难实现的开发模式,它面临如何统一两个不同的开发环境,如何统一状态,如何做到不重复渲染,目前能做到同构渲染的框架并不多,我了解的只有ng2、react、vue2可以解决,其中react与vue2 是通过由react研发的虚拟dom技术实现的,ng2则是通过在服务端引入独立渲染引擎直接渲染出dom结构来实现。目前,我只使用过react来实现同构渲染,所以无法评判三者实现效果的优劣,不过react无疑是最为主流的做同构开发的前端框架。
  • 简单介绍下react虚拟dom:其实也不难理解,就是用js对象模拟原生dom树并保存在内存中,然后根据js对象树生成一颗真正的dom树并append到html中。在发生变化重新渲染一个js对象树,然后与原来的对象树进行比较,记录两颗树的差异最后把它应用到真正的dom树上面。它会在进行差异比较时找寻一种尽量减少对真正dom的操作次数的更改方法,因为操作dom是耗时极高的一件事,这样就可以提升渲染的效率,同时还因为有了这颗虚拟的jsdom树,我们在服务器完全可以用同样的思路把它渲染出来只不过渲染成的是htmlString。

实际上react也提供了react-server-render的一套解决法案,其实核心方法就是renderToSting和reanderToStringMarkUp,其中后者渲染结果中不包含react-data-checksum等react相关属性,即渲染出的是纯html结构,为什么会有两种渲染方式,在这里是为了解决一个之前提到的重复渲染的问题,既然客户端使用了js渲染那即使不依赖服务端同样可以渲染出html结构,而我们肯定不想让它再次渲染我们在服务器已经渲染好的部分,所以当我们使用renderToSting进行渲染的时候,会生成相应的checksum(校验和) ,这样客户端在渲染时就会复用服务端已生成的初始dom并增量更新,所以renderToString就是为了渲染同时被两端使用的组件而reanderToStringMarkUp则可以更快的渲染那些只在服务端渲染的组件。事实上react不仅仅自己支持服务端渲染,它的主流插件也同样支持,包括react开发者熟知的redux、react-router。正因为这样,我们才能真正实现同构渲染,因为实际开发中我们大多离不开这些插件。

同构demo

我自己实现了react+redux+react-router+cssmoules+server-render+async+koa的demo,老实说这个体量已经不能叫做demo了,看起来会有点吃力,以后我会完善文档说明。目前基本解决了用react做中大型项目同构时遇到了各种困难。
项目的github地址:https://github.com/lsa2127291/ownsite-h5
目前还有不少场景没有实验,比如非常重要的用户登录场景,不过项目仍在持续更新中,最终会开发为一个h5博客平台。

总结

对于同构,我参考了网站上非常多的demo、文章,然而越深入的了解和实现同构,越让我觉得同构的难度确实非常大,很多实际场景是很复杂的,而很多文章仅仅只是在讲概念,将基本实现,这绝对是满足不了实际开发,当然如果要面面俱到那可以用一本书的长度来写如何同构了,我也只能用一个相对完整的demo来演示我设计的同构,通过查阅资料,很容易发现,同构的方式网上一篇文章是一个样子,根本提不出一个标准的实现方式,不过思路基本算是一致,利用react提供的服务端方法与客户端共用一套组件,并做router同步和状态同步,我的实现只是相对完整,但是只是众多方式中的一种,参考之后不用拘泥于我实现的方式,根据需求和自己的理解来设计同构即可,有新的思路和疑问也可以联系我一起探讨,共同学习,一起进步。

参考文献

前后端分离项目实践分析
前后端分离的思考与实践(一)
怎么更好的理解虚拟DOM?
React 同构实践与思考
我为什么选择 Angular 2?
Redux 中文文档
React Router 中文文档
react-production-starter

Redux中间件的使用心得

文章目录
  1. 前言
  2. 中间件是什么
  3. 为什么使用中件间
  4. 如何封装中间件
    1. 函数封装
    2. 实例替换
    3. 模块化
    4. 模块串联
  5. 使用Redux实现中间件
  6. 实际应用

前言

最近对React进行了一次比较完整的实践,基本把所有的技术栈跑了一遍,其中以Redux学起来最为困难,而中间件又是其中最难懂的一部分。相信有很多和我一样的入坑React的童鞋也会对这一块感到十分困惑,所以我把自己学习中间件的一点心得分享出来,希望能够帮助大家理解Redux中间件。

中间件是什么

在一般层面上来讲,中间件是一种介于上层与底层之间进行处理和转发的可插拔式组件,在Redux中也遵循这一理念,将功能以中间件的形式植入,这样十分利于代码解耦、复用以及易于添加和去除。

为什么使用中件间

学习中间件的时候,我们总有着这样一种疑问,究竟什么时候才会使用到中间件。这里我们通过一个很简单的例子来说明中间件的作用。比如我们想在简单的在Dispatch之前和之后,打印日志,检查状态改变情况,这时候最简单的方式是这样做:

1
2
3
console.log(store.getState());
store.dispatch(action);
console.log(store.getState());

但如果有多个action都需要打印日志,我们就必须在每次dispatch action之前加上这两句话,这样代码会显得非常冗余,而且写起来也很麻烦。
于是有没有一种方法可以将这两句话封装在dispatch之内,这样就解决了代码重复的问题,讲这你应该就发现了,中间件正是起到将各种功能封装到dispatch之中的作用,不仅如此,由于中间件的特性,使其不会污染原来的代码并且是可灵活插拔的。

如何封装中间件

封装中间件是最重要、最困难的一步,让我们先回到上一个问题,即如何把上面两句话封装进dispatch。

函数封装

首先我们尝试将操作抽取一个函数:

1
2
3
4
5
function dispatchAndLog (store, action) {
console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());
}

然后用它来代替store.dispatch():

1
dispatchAndLog(store, addTodo('Use Redux'));

这样的封装虽然简单,但使用起来总是要调用一个外部方法,有些麻烦。

实例替换

接着我们开始尝试直接替换掉store实例中的dispatch方法,使其具备日子打印功能:

1
2
3
4
5
6
7
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};

这样我们就在任何地方发起action,都会带有日志打印功能了,这离我们的目标很接近了,但是还一起存在不足,当我们想要附加的功能不只一个的时候,代码就混在一起。

模块化

这个问题可以采用模块解决,比如我们有添加了一个捕获错误的功能,那么我们可以使用多个不同模块添加不同的功能:

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
function patchStoreToAddLogging(store) {
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(actionconsole.log('next state', store.getState()););
console.log('next state', store.getState());
return result;
};
}

function patchStoreToAddCrashReporting(store) {
let next = store.dispatch;
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action);
} catch (err) {
console.error('捕获一个异常!', err);
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
});
throw err;
}
};
}

模块串联

上面的模块化已经可以方便的添加功能了,但是我们还可以设计一种方式能够将这些模块串联起来一并添加,那么这样一个自制的中间件模式就诞生了。

1
2
3
4
5
6
7
8
9
10
11
12
13
function logger(store) {
let next = store.dispatch;

// 我们之前的做法:
// store.dispatch = function dispatchAndLog(action) {

return function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
}

上面的函数不在直接替换掉原有的store.dispatch,而是返回一个新的dispatch。这就是一个中间件的形态,然后在用一个方法将中间件串联起来:

1
2
3
4
5
6
7
8
9
function applyMiddlewares(store, middlewares) {
middlewares = middlewares.slice();
middlewares.reverse();

// Transform dispatch function with each middleware.
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
);
}

其实它本质仍然还是替换store.dispatch,只不过封装在了函数内部。
至此我们自己完成了对功能进行中间件的封装,但略显粗糙,下面我们来研究redux是如何封装中间件的。

使用Redux实现中间件

下面是redux中中间件的形式,比起我们自己设计的中间件,它多了一层函数,不会直接将store传人,而是将dispatch单独传入作为参数,并采用了es6的arrow functions让其可读性变好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};

const crashReporter = store => next => action => {
try {
return next(action);
} catch (err) {
console.error('Caught an exception!', err);
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
});
throw err;
}
}

然后在使用下面的applyMiddleware实现中间件串联

1
2
3
4
5
6
7
8
9
10
11
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice();
middlewares.reverse();

let dispatch = store.dispatch;
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
);

return Object.assign({}, store, { dispatch });
}

当然,真正的applyMiddleware比这个有一些不同,比如作用在createStore上而不是store,不过大致上已经非常接近了。

实际应用

在Redux中可以直接引入applyMiddleware,于是我们需要做的就是按照规则编写中间件了,最后附上前面两个功能的中间件化及应用代码:

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
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};

const crashReporter = store => next => action => {
try {
return next(action);
} catch (err) {
console.error('Caught an exception!', err);
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
});
throw err;
}
}

import { createStore, combineReducers, applyMiddleware } from 'redux';

// applyMiddleware 接收 createStore()
// 并返回一个包含兼容 API 的函数。
let createStoreWithMiddleware = applyMiddleware(logger, crashReporter)(createStore);

// 像使用 createStore() 一样使用它。
let todoApp = combineReducers(reducers);
let store = createStoreWithMiddleware(todoApp);
store.dispatch(addTodo('Use Redux'));

介绍下自己设计的js插件开发框架

前言

最近在写mvvm框架和模块加载器时,发现开发比较大型的插件时如果直接在js文件写代码会遇到数不清的坑,大量的代码让排错变得非常困难,不规范的格式让可读性进一步下降,最为关键的是模块混合在一个文件里,让单元测试无法进行,只有在所有模块都编写完毕才能进行调试。正因为这些问题,让我决定设计一个模块化式的插件开发框架,并集成代码格式规范及单元测试框架。

如何实现简单的MVVM框架

前言

mvvm框架在目前的js开发中逐渐成为主流,现在非常流行的几大js框架(Ng, React, Vue),都是基于mvvm所以设计的。相信很多第一次使用这类框架的开发者都会觉得数据绑定,特别是双向绑定非常qi。我也是这样的开发者,当我在用Vue做项目的时候,我一直在想它是怎么做到这一点的,本着好奇和想深入学习js的源故,我自己实现了一个非常简单的mvvm框架,只包含了最基本的功能,但可以基本展示mvvm框架的设计思路。

Javascript如何进行自动化单元测试

前言

在不久之前我还一直用着console.log大法调试着代码,并且自认为这是一种很不错的方式,因为比起alert,它不会阻断UI线程,还可以把object的所有信息全部打印出来,而当我在编写模块加载器的时候,尽管console.log确实非常好用,但对于复杂且包含大量递归和回调的代码,调试起来还是太麻烦,特别是你很容易忘记这段console.log出的东西是那个地方,还有错误究竟出在哪里,console.log也无法得知。