React提供了强大的组件复用系统,组件设计也非常灵活,几乎没有什么约束。但强大的灵活性,在实际开发中,也带来了问题。可能在同一项目中,不同开发者设计出来的组件就有不同的形态,导致组件不好复用,代码也不好维护。因此,我们需要有一套可行的原则来规范组件的设计。

在React技术社区,有一种广泛讨论的模式,那便是在设计时将组件分为展示组件和容器组件。但实际应用到项目中,对这一原则,不同人有不同理解。下面,结合个人在项目中的实践,谈谈对这一模式的理解。

展示组件

它是组件可复用的基本单元,与外部业务逻辑解耦,仅关心UI展现。一个展示组件应该符合以下特征:

  • 主要关注UI展示
  • 通过props参数控制业务逻辑,内部仅存在与UI相关的state
  • 组件拥有自己的CSS样式,便于复用和移植
  • 可以包含this.props.children元素
  • 可以包含其他展示组件

容器组件

它除了是展示组件和其他容器组件的容器,还承载了应用的业务逻辑,负责与服务器打交道以及各种数据的加工清洗等。一个容器组件应该符合以下特征:

  • 主要关注业务逻辑和数据加工
  • 至少包含一个展示组件或容器组件
  • 为展示组件或其他组件提供数据和方法
  • 无自己的CSS样式,基本不包含DOM标签
  • 通常通过state或回调函数控制应用业务逻辑

二者关系

image

实践中的一些额外约束

  • 展示组件不能包含任何容器组件
  • 展示组件可以包含展示组件,但组件设计尽量扁平化,避免展示组件的多层嵌套
  • 通过props传递给下级组件的数据(包括当前组件state数据,上一级的props等)尽量扁平化,尽量避免一个对象里包含结构复杂的数据

看个简单的例子

我们先来定义一个简单的组件,然后重构该组件,将其分成容器组件和展示组件。例子中的组件从服务端获取数据并显示到页面上。

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
// PageView.js
class PageView extends React.Component {
constructor() {
super();
this.state = {
pv: 0
};
this.getPV = this.getPV.bind(this);
}

componentDidMount() {
this.getPV();
}

getPV() {
ajax({
url: "/pv.json",
dataType: 'json'
}).then((pv) => {
this.setState({pv: pv});
});
}

render() {
return <div> 当前PV值是:{this.state.pv} </div>;
}
}

上面的组件:

  • 复用性 组件数据与视图没有分离,组件不能复用
  • 可维护性 组件既承担了应用的业务逻辑部分,又承担了视图显示部分,承担的职责太多,如果业务稍微复杂,组件将变得难以维护

下面,我们根据展示组件和容器组件分离原则来重新设计它:

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
33
34
35
36
37
38
39
40
41
// PageViewContainer.js - 容器组件
class PageViewContainer extends React.Component {
constructor() {
super();
this.state = {
pv: 0
};
this.getPV = this.getPV.bind(this);
}

componentDidMount() {
this.getPV();
}

getPV() {
ajax({
url: "/pv.json",
dataType: 'json'
}).then((pv) => {
this.setState({pv: pv});
});
}

render() {
return <PageView title="当前PV值是" count={this.state.pv} />;
}
}


// PageView.js - 展示组件
class PageView extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
count: PropTypes.number.isRequired
};

render() {
let {title, count} = this.props;
return <div> {title}:{count} </div>;
}
}

分离后的组件虽然代码量略增,但职责更加清晰,后续维护也更容易,而且PageView也可以复用了。

好处

总结起来,组件分离带来的好处,有以下几点:

  • 分离关注点使代码逻辑结构更清晰,更易维护。随着功能不断迭代,代码不断膨胀,清晰的逻辑划分,使问题更容易被定位,解耦模块间的依赖,修改BUG时也不易引入新的问题
  • 展示组件和容器组件的分离使组件更容易在不同项目间复用
  • 不改动任何业务逻辑代码的情况下,仅替换UI展示成为可能。也就是说通过组件业务逻辑与UI视图的分离,允许你可以任意组合你的容器组件和展示组件。

当然,这种分离并非必须遵循的原则,特别是在做业务开发的过程中,有时候很难界定一个组件是容器组件还是展示组件,怎么做,只能根据业务场景灵活应对了。

最近基于React开发的组件需要兼容IE8+。因此,总结了开发过程中的一些坑。

ie8,ie9首先考虑跨域问题

主要是采用服务端代理的方法,在项目代码里面放入java代码

(1)服务端支持在请求的接口加入前缀/v0.1/dispatcher,

(2)在请求的接口头部加入Dispatcher头

例如:

image

这样就可以通过访问同域的服务端转发接口,实现跨域请求

解决es3保留字的问题

保留字如:

1
default, class等

报错信息:

1
缺少标识符

解决方法:

在webpack1的配置文件中加入配置:

1
2
3
4
postLoaders: [{
test: /\.js$/,
loaders: ['es3ify-loader']
}]

解决压缩后UglifyJsPlugin的问题

报错信息:

1
缺少标识符

解决方法:

在配置里面加入screw_ie8,压缩的时候不要去掉ie8的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new webpack.optimize.UglifyJsPlugin({
compress: {
properties: false,
warnings: false,
screw_ie8: false
},
output: {
beautify: true,
comments: false,
quote_keys: true,
screw_ie8: false
},
mangle: {
screw_ie8: false
}
})

上面改完后,继续报错

报错信息:

image

解决方法:

原先的webpack版本装的是1.15.2版本的,后面降为1.13.2版本就ok了

解决发生异常未捕获的错误

报错信息:

1
从core-js的_object-dp.js发出了错误:发生异常,未捕获

解决方法:

原先是使用babel-polyfill用来解决es6的api的兼容es5的问题。

现在因为core-js抛出错误了,所以直接废弃babel-polyfill

分别引入 (注: babel-polyfill是包含core-js和regnerator-runtime/runtime的)

1
2
import 'core-js-ie8'
import 'regenerator-runtime/runtime'

虽然core-js-ie8能完美解决这个问题,但是意外发现又引发了另外一个问题,core-js-ie8这个包实现的Object.assign在360和ie8+下是有问题的,于是乎就自己发了个npm包

core-js的issue也有相关的core-js

最终解决问题的代码变成:

1
2
import 'core-js-for-ie8'
import 'regenerator-runtime/runtime'

解决es5的API兼容的问题

解决方法:

在index.html的模板文件里面引入shim和sham:

1
2
3
4
<!--[if lt IE 9]>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.7/es5-shim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.7/es5-sham.min.js"></script>
<![endif]-->

继续报错,苦逼了

报错信息:

1
无法修改属性type,不允许此命令

问题原因:

登录页面上有个“是否显示密码”;可以切换修改, 代码如下:

1
{ isShowPassword ? <input type="text" /> : <input type="password" /> }

因为对React来说,有虚拟Dom,对比下dom结构都没有改变,只是type改变,不会重新渲染新的input元素,只会做个diff,把type替换掉,于是报这个错了。

解决方法:

React组件只要带了key属性,都会重新render的,不会做diff比较。

于是最终更正后的代码:

1
{ isShowPassword ? <input type="text" key="text" /> : <input type="password" key="password" />

解决ie8下的事件不生效的问题:

问题:

1
2
3
4
5
6
7
8
9

<input type="text" onChange={this.onChange} onClick={this.onClick} onInput={this.onInput} onPropertyChange={this.onPropertyChange} />

<input type="checkbox" onChange={this.onChange} />

以上代码:
(1)<input type="checkbox"/>元素上的onChange事件是生效的

(2)<input type="text"/>元素上只有OnClick事件是生效的,其他都不生效

解决方法:

1
2
3
4
(1)保留onInut事件,其他事件不需要
<input type="text" onInput={this.onInput} />
(2)index.html的模板里面添加
<!--[if IE 8]><script src="//cdnjs.cloudflare.com/ajax/libs/ie8/0.3.2/ie8.js"></script><![endif]-->

解决Observable库的问题:

问题:

1
2
3
import { Observable } from 'rx-lite'

这个库是支持ie10+的,并不支持ie8,9

解决方法:

1
2
3
很开心的是,有个兼容库rx-lite-compat是支持ie8+的,心里默默开心,于是代码变成如下:

import { Observable } from 'rx-lite-compat'

上面解决完Observable问题的后遗症

问题根源:

1
开心的太早了,引入rx-lite-compat又报另外一个问题了,这库里面的事件报错了,估计不兼容了

报错信息:

image

1
无法获取未定义或null 引用的属性“keyCode”

解决方法:

1
2
3
引入ie8.js文件后,一切正常,好开心了。

<!--[if IE 8]><script src="//cdnjs.cloudflare.com/ajax/libs/ie8/0.3.2/ie8.js"></script><![endif]-->

样式兼容问题:

(1) transform:translate(-50%)这个是不支持的

(2) 透明度也是不支持的,改造后的代码:

1
2
3
4
5
6
if (browserLessIE8()) {
document.getElementById('shadow').style.backgroundColor = '#000'
document.getElementById('shadow').style.filter = 'progid:DXImageTransform.Microsoft.Alpha(Opacity = 80)'
} else {
document.getElementById('shadow').style.background = 'rgba(0,0,0,0.8)'
}

解决promise的兼容问题:

1
2
3
可以引入es6-promise库支持ie8,使用如下:

require('es6-promise').polyfill()

解决fetch的兼容问题:

问题根源:

1
2
3
 (1)发现请求成功了,也引入了es6-promise了,但是还是无法进入then方法,原来是fetch不支持ie8

(2)isomorphic-fetch是不支持ie8的

解决方法:

1
替换成universal-fetch支持ie8的

console的polyfill:

问题原因:

1
ie8没有打开控制台的时候,是没有console这个对象了,所以需要对console进行polyfil

解决方法:

1
import 'console-polyfill'

解决ie8下React项目在某些时候a标签无法跳转路由的情况

问题原因:

1
2
3
4
5
<a href="#/home"><button>我来试一试</button></a>

(1)以上代码在ie8下跳转失效

(2)因为a标签里面包了button标签,从而导致跳转失败了

解决方法:

1
2
3
4
5

<a href="#/home"><div>我来试一试</div></a>

(1)把button标签改成div就可以了
(2)所以注意不要a标签里面包含button标签哦

lodash版本:

lodash 从4.0.0开始支持的环境有: Chrome 46-47, Firefox 42-43, IE 9-11, Edge 13, Safari 8-9, Node.js 0.10.x, 0.12.x, 4.x, & 5.x, & PhantomJS 1.9.8。已不再支持IE6~IE8。

如果想兼容IE6~IE8,可以使用3.x版本。3.x版本支持的环境有:
Chrome 43-44, Firefox 38-39, IE 6-11, MS Edge, Safari 5-8, ChakraNode 0.12.2, io.js 2.5.0, Node.js 0.8.28, 0.10.40, & 0.12.7, PhantomJS 1.9.8, RingoJS 0.11, & Rhino1.7.6

lodash 3.x 版本没有直接提供可用的js, 需要手动构建。

1
2
3
4
5
安装
npm i -g lodash-cli@3.10.1
安装完成后执行
lodash compat
会输出兼容IE6~IE8的版本lodash.custom.js及lodash.custom.min.js

lodash 3.10.1版本文档地址: https://lodash.com/docs/3.10.1#template

es6对象中使用取值器和赋值器报错

报错信息:

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
SCRIPT1003: 缺少 ':'

问题定位到:

var Auth = {

__access_token: null,

get accessToken() {
return this.__access_token;
},

set accessToken(val) {
this.__access_token = val;
},

__mac_key: null,

get macKey() {
return this.__mac_key;
},

set macKey(val) {
this.__mac_key = val;
}
}
get set后面是空格,缺少':'

问题原因:

1
babel转化的时候没有加入插件babel-plugin-transform-es5-property-mutators

解决方法:

1
2
3
4
5
.babelrc里面引入这个插件:

{
"plugins": ["transform-es5-property-mutators"]
}

上面那个问题引发了另外一个问题:

上面的es6的get和set的代码,babel编译后的代码也会有get和set属性,编译后如下:

1
2
3
4
5
6
7
8
{
key: 'accessToken',
get: function get() {
return this.__access_token;
},
set: function set(val) {
this.__access_token = val;
}

1
2
3
export { default as request } from './request'
export { default as RBAC } from './rbac'
export { default as RBACAdapt } from './rbac/instance'

上面这个代码也是类似的,bable编译后的代码也会有get和set属性如下:

1
2
3
4
5
6
Object.defineProperty(exports, 'RBACAdapt', {
enumerable: true,
get: function get() {
return _interopRequireDefault(_instance)['default'];
}
});

问题原因:

1
2
1. ie8不支持设置访问器属性,即便是引了es5-shim;
2. Babel 会把export xxx from ‘xx’ 语法转码为访问器属性设置的exports对象。

解决方法:

  1. 代码里面不要使用get,set
  2. export { default as request } from ‘./request’类似这样的写法要分两句写:
1
2
3
import request from './request'

export request

babel插件和webpack的兼容ie8的loader可以等价:

1
2
3
4
5
6
7
8
9
10
es3ify-loader相当于babelrc里面的两个插件
transform-es3-property-literals
transform-es3-member-expression-literal

webpack里面要是有用es3ify-loader,在.babelrc里面就不用配置
transform-es3-property-literals
transform-es3-member-expression-literal
这两个插件了

反之亦然

本地调试

我们开发的组件,都是通过iframe的方式被业务系统所引用。由于在IE下,iframe模拟接入,存在同源限制(即iframe的src地址不能和浏览器里的url地址相同)。
因此,在IE下调试,需要解决同源问题。我们使用的方式是,通过修改etc\hosts文件,将同一本地IP地址映射到不同的域名下,保证本地地址在IE下可调试。

要支持ie8,注意的几个点:

1.webpack要用1.x,2.x不支持ie8了

2.源码中不要使用es6的get和set访问器

3.不要写成export x from ‘x’,要分两步走

4.react要用0.14.x版本

参考资料:

Webpack-IE低版本兼容指南

IE8下使用React开发总结

defineProperty

ie8.js

Exception thrown and not caught

Make your React app work in IE8

让Webpack+Babel支持IE8

alibaba的兼容ie8的项目

代码如下:

1
2
3
4
5
6
7
8
9
(function () {
window.iframeResizePostMessage = function (name) {
if (window.parent) {
var height = document.documentElement.scrollHeight || document.body.scrollHeight;
var width = document.documentElement.scrollWidth || document.body.scrollWidth;
window.parent.postMessage(JSON.stringify({ 'type': 'iframe-resize', name: name || '', height: height, width: width }), '*'); // name各业务组件自己定义
}
}
})();

原型链

使用new原型对象生成实例对象的缺点(原型链要解决的问题)

用原型对象生成实例对象,有一个缺点,那就是无法共享属性和方法。

例如,在Person对象的构造函数中,设置一个实例对象的共有属性species。

1
2
3
4
  function Person(name){
    this.name = name;
    this.species = '黄种人';
  }

然后,生成两个实例对象:

1
2
var personA = new Person('张三');
  var personB = new Person('Tom');

这两个对象的species属性是独立的,修改其中一个,不会影响到另一个。

1
2
3
  personA.species = '黑人';
alert(personA.species); // 显示"黑人"
  alert(personB.species); // 显示"黄种人",不受personA的影响

每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。

prototype属性(原型链)的引入

在了在来自同一原型对象的实例对象间共享数据(对象),于是在原型对象中引入了prototype属性。该属性包含一个对象(以下简称”prototype对象”),所有实例对象间需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。
实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。
还是以Person为例,现在用prototype属性进行改写:

1
2
3
4
5
6
7
8
9
10
  function Person(name){
    this.name = name;
  }
  Person.prototype = { species : '黄种人' };

  var personA = new Person('张三');
  var personB = new Person('Tom');

  alert(personA.species); // 黄种人
  alert(personB.species); // 黄种人

现在,species属性放在prototype对象里,是两个实例对象共享的。只要修改了prototype对象,就会同时影响到两个实例对象。

1
2
3
4
  Person.prototype.species = '黑人';

  alert(personA.species); // 黑人
  alert(personB.species); // 黑人

总结

由于源于同一原型对象的所有实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像”继承”了prototype对象一样。
这就是Javascript原型链的由来,也是Javascript继承机制的核心思想。

apply,call及bind的使用

apply,call,bind的作用:改变某个函数运行时的上下文(context)环境,换句话说,就是为了改变函数体内部 this 指向。

调用格式:
func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2]);
var newFunc = func.bind(this);

从上面可以看到:call和apply的作用相同,区别在于调用时传递的参数(apply第二个参数是数组格式)。bind的作用和call,apply相同,只是调用bind时不是立即执行func函数,而是返回新的函数对象,供后续代码调用。

看懂以下示例,几个方法的作用及区别就很明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
var func = function(arg1, arg2) {
console.log("name="+this.name+",arg1="+arg1+",arg2="+arg2);
};

//将func函数内的this指向第一个参数,即{name:'li'};后面2个参数为函数本身参数;
func.call({name:'li'},12,22) //输出:name=li,arg1=12,arg2=22

//将func函数内的this指向第一个参数,即{name:'wang'};后面2个参数为函数本身参数;
func.apply({name:'wang'},[33,44]) //输出:name=wang,arg1=33,arg2=44

//将func函数内的this指向第一个参数,即{name:'liu'};,然后返回新的方法,供后续调用。
var newFunc = func.bind({name:'liu'});
newFunc (55,66) //输出:name=liu,arg1=55,arg2=66

起因

最近,服务端在进行接口改版,原来旧接口返回的JSON数据使用的是驼峰式命名法,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
buildType : 2
chineseName : "部署测试"
createTime : 1472489455000
creator : 339691
demandAnalysisUrl : "应用描述33"
desc : "应用描述22"
designAnalysisUrl : "应用描述44"
members : [
{
yourName : 'li',
yourAge : 23
}
]

现在新改版的v2.0接口,由于基于新的框架,JSON数据使用的是连字符命名法,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
build_type : 2
chinese_name : "部署测试"
create_time : 1472489455000
creator : 339691
demand_analysisUrl : "应用描述33"
desc : "应用描述22"
design_analysisUrl : "应用描述44"
members : [
{
your_name : 'li',
your_nge : 23
}
]

因此,为了使改动量最小,需要实现一个适配函数,新接口返回结果经过该函数处理后,得到驼峰式命名的JSON数据,对该函数的要求如下:

  • 支持将一个JSON对象的连字符命名变量转换为驼峰式命名
  • 支持对对象进行深度递归遍历

函数实现

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
/**
* 将对象的变量名由连字符式转为驼峰式,支持对象的深度遍历转换
* @param obj JSON对象
* @return JSON 驼峰式的JSON对象
*/
function obj2CamelCased(obj){
if (!(obj instanceof Object)) {
return obj;
}

var newObj = {};
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) { //推荐在 for in 时,总是使用 hasOwnProperty 进行判断,没人可以保证运行的代码环境是否被污染过。
var camelProp = prop.replace(/_([a-z])/g, function (g) {
return g[1].toUpperCase();
});
if (obj[prop] instanceof Array) { //值为数组的处理
newObj[camelProp] = [];
var oldArray = obj[prop];
for (var k = 0, kLen = oldArray.length; k < kLen; k++) {
newObj[camelProp].push(arguments.callee(oldArray[k]));
}
} else if (obj[prop] instanceof Object) { //值为对象的处理
newObj[camelProp] = arguments.callee(obj[prop]);
} else { //值为字符串,或数字等的处理
newObj[camelProp] = obj[prop];
}
}
}
return newObj;
}

问题原因及解决方法

在使用vue-loader的热部署时,修改源码文件,界面没有自动更新。查了一堆资料,百度,google,花了几个小时,终于解决了。原来不是插件的问题,问题出在使用的编辑器上。 由于webstorm,它默认保存在临时文件,把settings=>appearance=>system=>synchornization=>最后一项勾去掉,热部署替换功能就能正常使用了。

image

简介

在传统的B/S架构中,当浏览器关心的某个数据发生变更时,服务器无法主动向浏览器端推送变更信息,只能由浏览器端通过AJAX定时轮询方式进行数据的查询和更新,这种方式下,数据的更新存在延迟,实时性不好。后来,出现的WebSocket彻底解决了该问题,但是使用起来较为复杂,而且需要服务器端的支持。
不过除了WebSocket,在HTML5标准中,还有一个Server-Sent Events(下文简称SSE)也可以实现服务器向浏览器主动推送消息。SSE的基本概念跟WebSocket有点类似,浏览器可以利用SSE来“订阅”服务器上的一个数据源,当这个数据源的数据有更新的时候,就会通过这条“订阅”的线路,
将数据主动推送给浏览器,从而实现数据的实时更新。

SSE VS WebSocket

  • SSE

    • 单向通信,只能服务器端往浏览器端推送数据,因此,比较适合股市行情,即时新闻等数据单向传输(服务端推浏览器端),数据量少的应用
    • 架构于传统的HTTP协议之上,向前兼容
    • 实现简单,服务器端只需添加几行应用级代码即可实现
    • 存在一些WebSocket没有的优点,例如:断线重连,事件ID与传送任意事件等
    • IE浏览器不支持(可以通过引入开源库解决)
  • WebSocket

    • 全双工双向通信,功能强大
    • 无法向前兼容,需服务器端特别支持
    • 适合线上游戏,聊天程序及各种需要及时双向传输数据的应用
    • 无论是服务器端还是浏览器端实现都相对复杂

SSE 浏览器兼容性

所有主流浏览器均支持SSE,除了IE。

1
提示:对于不支持Server-Sent Events功能的浏览器,建议使用开源库解决,例如:https://github.com/EventSource/eventsource

SSE 浏览器端实现

  • step1.浏览器支持检测
    1
    2
    3
    4
    5
    if (!!window.EventSource) {
    var source = new EventSource('stream.php');
    } else {
    // 瀏覽器不支援 SSE
    }

注意:当指定完整URL时,该URL必须与网页地址一致,不支持跨域,即不能为第三方服务地址。

  • step2.设置事件监听回调
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //数据接收回调
    source.addEventListener('message', function(e) {
    console.log(e.data); //当服务端数据推送过来时,可以通过e.data获取数据
    }, false);

    source.addEventListener('open', function(e) {
    // 通信链路已建立
    }, false);

    source.addEventListener('error', function(e) {
    if (e.readyState == EventSource.CLOSED) {
    // 通信链路已关闭
    }
    }, false);

SSE很好的一点是如果链路因为某些原因中断了,它会自动在大约3秒钟后重新连接,开发者也可以自己设置这个等待事件。

SSE 服务器端实现

以下为Node.js的实现版本:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var http = require('http');
var sys = require('sys');
var fs = require('fs');

http.createServer(function(req, res) {
//debugHeaders(req);

if (req.headers.accept && req.headers.accept == 'text/event-stream') {
if (req.url == '/events') {
sendSSE(req, res);
} else {
res.writeHead(404);
res.end();
}
} else {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write(fs.readFileSync(__dirname + '/sse-node.html'));
res.end();
}
}).listen(8000);

function sendSSE(req, res) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});

var id = (new Date()).toLocaleTimeString();

// Sends a SSE every 5 seconds on a single connection.
setInterval(function() {
constructSSE(res, id, (new Date()).toLocaleTimeString());
}, 5000);

constructSSE(res, id, (new Date()).toLocaleTimeString());
}

function constructSSE(res, id, data) {
res.write('id: ' + id + '\n');
res.write("data: " + data + '\n\n');
}

function debugHeaders(req) {
sys.puts('URL: ' + req.url);
for (var key in req.headers) {
sys.puts(key + ': ' + req.headers[key]);
}
sys.puts('\n\n');
}

数据格式

使用SSE传输数据,需要符合它约定的格式。
基本格式:以 data: 开头,加上数据内容,最后以2个换行符 \n\n 结束,例如:

1
data: My message\n\n

如果要传输的数据量比较大的话,也可以将数据进行分行传输,每行都以data:开头,然后以一个换行符\n结尾(最后一行需要2个换行符),例如:

1
2
data: first line\n
data: second line\n\n

像上面这样连续的data行会被认为是一个数据,这些数据被传送到浏览器端时,只会触发一次数据接收回调。并且数据会被自动合并。上面这个例子,浏览器收到的数据为:
e.data = “first line\nsecond line”

  • 传送JSON
    如果要传送 JSON 格式的资料,可以这样写:
1
2
3
4
data: {\n
data: "msg": "hello",\n
data: "id": 123\n
data: }\n\n

浏览器端收到这个 JSON 数据后,可以这样处理:

1
2
3
4
source.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
console.log(data.id, data.msg);
}, false);
  • 事件分类

发送的数据可以根据事件名称,对数据进行分类。事件名称通过event来指定,浏览器在接收到含有event的数据后,就可以根据不同的事件名称来进行处理。

下面发送的数据中,包含三个不同的事件,分别为:普通消息事件(message),用户登录事件(userlogon)和更新事件(update)。

1
2
3
4
5
data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

浏览器端针对上面事件的处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
source.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
console.log(data.msg);
}, false);

source.addEventListener('userlogon', function(e) {
var data = JSON.parse(e.data);
console.log('User login:' + data.username);
}, false);

source.addEventListener('update', function(e) {
var data = JSON.parse(e.data);
console.log(data.username + ' is now ' + data.emotion);
}, false);

安全性

为了避免恶意攻击,在使用SSE接收数据时,需要检查数据中的e.origin是否为合法来源。如下:

1
2
3
4
5
6
7
source.addEventListener('message', function(e) {
if (e.origin != 'http://www.data.com') {
alert('Origin was not http://www.data.com');
return;
}
// ...
}, false);

表单提交中的按钮禁用

在做富客户端的互联网Web应用时,通常都涉及到表单提交。如果希望做得稍微完美些的话,通常都要实现以下效果:表单项未填写或者输入数据非法时,提交按钮处于禁用状态(即鼠标放到提交按钮上,无任何反应)。当输入数据合法时,提交按钮变为可用状态。如下图:

image

image

1
问题:在如上场景中,如果我们只切换CSS样式,使按钮“看上去”像是不可用的。但是实际上,当鼠标放到按钮上或是点击鼠标时,事件仍然被执行(仍然响应提交事件)。

解决之道

想要不写一行javascript代码,就做到对鼠标事件(包括鼠标效果)的完全禁用,那只能是CSS3的pointer-events方能做到了。通过对按钮元素设置CSS3属性:

1
pointer-events: none;

即可禁用该元素下注册的所有事件。

浏览器兼容性

image

一个布局问题的优化

最近在做一个新的web运维系统,要求页面内容要能自适应屏幕视窗。如下图:( 图1)

image

有2个地方涉及自适应浏览器窗口:

  • 左侧菜单栏部分(区域2和区域3),区域2的菜单内容为动态生成,区域3要求始终位于页面底部。
  • 右侧主内容区域(区域4,区域5,区域6和区域7)。其中区域4高度固定,区域5的高度要随着浏览器视窗高度动态铺满。区域6,区域7要求也要动态铺满,且高度各位浏览器可视区域高度的1/2。

以上自适应部分,除非区域内部内容高度超出区域高度,否则不允许出现滚动条。

最初实现这个布局的是一位新同学,吧啦吧啦用了一堆js代码,通过监控窗口的resize事件总算完成了(大致实现方法应该大家都能想到,这里就不贴代码了)。由于框架通过路由来实现跳转,各个区域自然不在同一个js里面。导致代码看起来不那么“美观”,不好维护,而且在计算高度时,由于浏览器差异性,导致用户体验也不是很好。

于是问题出来了,有没有更好的方法实现呢?使这样的动态布局不需要依赖js来处理,从而与js解耦。答案就是CSS3的calc()方法了。于是又跟新人吧啦吧啦讲了一堆,最后仅使用calc(),没有写一行js,把这个布局问题完美解决了。

calc()简介

calc属于CSS3的一个函数,任何元素的宽高度都可以使用calc()函数进行计算;calc()函数支持 “+”, “-“, “*”, “/“ 运算符。
浏览器支持情况如下:( 图2)

calc()语法示例:

1
width: calc(100% - 10px);

calc()典型应用场景

当在一个纵向(或横向)排列的区域块中,其中某几个区域高度(或宽度)固定,另一些区域需要根据浏览器高度(或宽度)填满剩余空间时,建议使用calc()函数实现。如下图:( 图3,页面整体布局参考图1)

  • 区域1和区域3高度固定,区域1紧贴顶部,区域3紧贴底部,区域2根据配置动态生成,并要求区域2高度能根据浏览器高度自适应剩余区域,且当区域2内部内容高度过高而出现滚动条时,滚动条上下滚动不会导致区域2与区域3重叠。
  • 不满足要求(菜单栏滚动条出现后,区域2和区域3重叠)

  • 满足要求(菜单栏滚动条出现后,区域2和区域3不重叠)

  • 实现代码

CSS源码:

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
33
34
#portal-header {
position: fixed;
top: 0px;
width: 100%;
height: 70px;
padding: 0px;
background-color: #387AB7;
z-index: 8000;
min-width: 860px;
}

#portal-sidebar {
position: fixed;
top: 70px;
width: 230px;
background-color: #EAEDF1;
z-index: 8000;
overflow-y: auto;
bottom: 0;
}
/* 区域2高度使用了calc()函数结合min-height来满足布局要求 */
#sidebar-scroll {
min-height: -moz-calc(100% - 120px);
min-height: -webkit-calc(100% - 120px);
min-height: calc(100% - 120px);
}
.nav-feed-back {
overflow: hidden;
position: relative;
height: 120px;
padding: 16px;
margin: auto;
box-sizing: border-box;
}

HTML源码

1
2
3
4
5
6
7
8
9
10
11
<div id="portal-header">
<!-- 区域1 -->
</div>
<nav id="portal-sidebar">
<div id="sidebar-scroll">
<!-- 区域2 -->
</div>
<div class="nav-feed-back">
<!-- 区域3 -->
</div>
</nav>

从代码可知,为了使区域2的高度能自适应浏览器高度,使用了calc()函数。