玉英博客

家庭、工作、学习、旅行


  • 首页

  • 分类

  • 归档

  • 关于

网页进度loading

发表于 2017-11-03

loading随处可见,比如一个app经常会有下拉刷新,上拉加载的功能,在刷新和加载的过程中为了让用户感知到 load 的过程,我们会使用一些过渡动画来表达。最常见的比如“转圈圈”,“省略号”等等。

网页loading有很多用处,比如页面的加载进度,数据的加载过程等等,数据的加载loading很好做,只需要在加载数据之前(before ajax)显示loading效果,在数据返回之后(ajax completed)结束loading效果,就可以了。

首先准备一段loading的html:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>写一个网页进度loading</title>
</head>
<body>
<div class="loading" id="loading">
<div class="progress" id="progress">0%</div>
</div>
</body>
</html>

样式装扮一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.loading {
display: table;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #fff;
z-index: 5;
}
.loading .progress {
display: table-cell;
vertical-align: middle;
text-align: center;
}

假设这个loading只需要在页面加载完成之后隐藏,中间不需要显示进度。那么很简单,我们第一时间想到的就是window.onload:

(以下内容为了方便演示,默认使用jQuery,语法有es6的箭头函数)

var $loading = $(‘#loading’)
var $progress = $(‘#progress’)

window.onload = () => {
$loading.hide()
}

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
ok,这样基本的loading流程就有了,增加一个进度的效果,每隔100ms就自增1,一直到100%为止,而另一方面window loaded的时候,把loading给隐藏。
### 补充一下进度:
```bash
var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0 // 初始化进度
var timer = window.setInterval(() => { // 设置定时器
if (prg >= 100) { // 到达终点,关闭定时器
window.clearInterval(timer)
prg = 100
} else { // 未到终点,进度自增
prg++
}
$progress.html(prg + '%')
console.log(prg)
}, 100)
window.onload = () => {
$loading.hide()
}

效果不错,但是有个问题,万一window loaded太慢了,导致进度显示load到100%了,loading还没有隐藏,那就打脸了。所以,需要让loading在window loaded的时候才到达终点,在这之前,loading可以保持一个等待的状态,比如在80%的时候,先停一停,然后在loaded的时候快速将进度推至100%。这个做法是目前绝大部份进度条的做法。

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
var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = window.setInterval(() => {
if (prg >= 80) { // 到达第一阶段80%,关闭定时器,保持等待
window.clearInterval(timer)
prg = 100
} else {
prg++
}
$progress.html(prg + '%')
console.log(prg)
}, 100)
window.onload = () => {
window.clearInterval(timer)
window.setInterval(() => {
if (prg >= 100) { // 到达终点,关闭定时器
window.clearInterval(timer)
prg = 100
$loading.hide()
} else {
prg++
}
$progress.html(prg + '%')
console.log(prg)
}, 10) // 时间间隔缩短
}

ok,这差不多就是想要的功能了,重复的代码给封装一下:

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
var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
progress(80, 100)
window.onload = () => {
progress(100, 10, () => {
$loading.hide()
})
}
function progress (dist, delay, callback) {
window.clearInterval(timer)
timer = window.setInterval(() => {
if (prg >= dist) {
window.clearInterval(timer)
prg = dist
callback && callback()
} else {
prg++
}
$progress.html(prg + '%')
console.log(prg)
}, delay)
}

得到了一个progress函数,这个函数就是我们主要的功能模块,通过传入一个目标值、一个时间间隔,就可以模拟进度的演化过程。

目前来看,这个进度还是有些问题的:

进度太平均,相同的时间间隔,相同的增量,不符合网络环境的特点;
window.onload太快,还来不及看清100%,loading就已经不见了;
每次第一阶段都是在80%就暂停了,露馅儿了;

第一个点,要让时间间隔随机,增量也随机;第二个点很简单,延迟一下就好了;第三点也需要我们随机产生一个初始值。

增量随机很好办,如何让时间间隔随机?setInterval是无法动态设置delay的,那么我们就要把它改造一下,使用setTimeout来实现。(setInterval跟setTimeout的用法和区别就不细说了吧?)

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
var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
progress([80, 90], [1, 3], 100) // 使用数组来表示随机数的区间
window.onload = () => {
progress(100, [1, 5], 10, () => {
window.setTimeout(() => { // 延迟了一秒再隐藏loading
$loading.hide()
}, 1000)
})
}
function progress (dist, speed, delay, callback) {
var _dist = random(dist)
var _delay = random(delay)
var _speed = random(speed)
window.clearTimeout(timer)
timer = window.setTimeout(() => {
if (prg + _speed >= _dist) {
window.clearTimeout(timer)
prg = _dist
callback && callback()
} else {
prg += _speed
progress (_dist, speed, delay, callback)
}
$progress.html(parseInt(prg) + '%') // 留意,由于已经不是自增1,所以这里要取整
console.log(prg)
}, _delay)
}
function random (n) {
if (typeof n === 'object') {
var times = n[1] - n[0]
var offset = n[0]
return Math.random() * times + offset
} else {
return n
}
}

至此,差不多完成了需求。

but,还有一个比较隐蔽的问题,现在使用window.onload,发现从进入页面,到window.onload这中间相隔时间十分短,基本是感受不到第一阶段进度(80%)的,如果页面的加载资源数量很多,体积很大的时候,从进入页面,到window.onload就不是这么快速了,这中间可能会很漫长(5~20秒不等),但事实上,首屏资源 的加载争取时间就可以了,不需要等待所有资源就绪,而且更快地呈现页面也是提高用户体验的关键。

应该考虑页面loading停留过久的情况,需要为loading设置一个超时时间,超过这个时间,假设window.onload还没有完成,也要把进度推到100%,把loading结束掉。

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
51
52
53
var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
progress([80, 90], [1, 3], 100) // 使用数组来表示随机数的区间
window.onload = () => {
progress(100, [1, 5], 10, () => {
window.setTimeout(() => { // 延迟了一秒再隐藏loading
$loading.hide()
}, 1000)
})
}
window.setTimeout(() => { // 设置5秒的超时时间
progress(100, [1, 5], 10, () => {
window.setTimeout(() => { // 延迟了一秒再隐藏loading
$loading.hide()
}, 1000)
})
}, 5000)
function progress (dist, speed, delay, callback) {
var _dist = random(dist)
var _delay = random(delay)
var _speed = random(speed)
window.clearTimeout(timer)
timer = window.setTimeout(() => {
if (prg + _speed >= _dist) {
window.clearTimeout(timer)
prg = _dist
callback && callback()
} else {
prg += _speed
progress (_dist, speed, delay, callback)
}
$progress.html(parseInt(prg) + '%') // 留意,由于已经不是自增1,所以这里要取整
console.log(prg)
}, _delay)
}
function random (n) {
if (typeof n === 'object') {
var times = n[1] - n[0]
var offset = n[0]
return Math.random() * times + offset
} else {
return n
}
}

直接设置了一个定时器,5s的时间来作为超时时间。这样做是可以的。

but,还是有问题,这个定时器是在js加载完毕之后才开始生效的,也就是说,忽略了js加载完毕之前的时间,这误差可大可小,设置的5s,实际用户可能等待了8s,这是有问题的。我们做用户体验,需要从实际情况去考虑,所以这个开始时间还需要再提前一些,我们在head里来记录这个开始时间,然后在js当中去做对比,如果时间差大于超时时间,那我们就可以直接执行最后的完成步骤,如果小于超时时间,则等待 剩余的时间 过后,再完成进度。

先在head里埋点,记录用户进入页面的时间loadingStartTime:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<title>网页进度loading</title>
<script>
window.loadingStartTime = new Date()
</script>
<script src="index.js"></script>
</head>
<body>
<div class="loading" id="loading">
<div class="progress" id="progress">0%</div>
</div>
</body>
</html>

然后,对比 当前的时间 ,看是否超时:(为了方便复用代码,我把完成的部分封装成函数complete)

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
51
52
53
54
55
56
57
58
59
var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
var now = new Date() // 记录当前时间
var timeout = 5000 // 超时时间
progress([80, 90], [1, 3], 100)
window.onload = () => {
complete()
}
if (now - loadingStartTime > timeout) { // 超时
complete()
} else {
window.setTimeout(() => { // 未超时,则等待剩余时间
complete()
}, timeout - (now - loadingStartTime))
}
function complete () { // 封装完成进度功能
progress(100, [1, 5], 10, () => {
window.setTimeout(() => {
$loading.hide()
}, 1000)
})
}
function progress (dist, speed, delay, callback) {
var _dist = random(dist)
var _delay = random(delay)
var _speed = random(speed)
window.clearTimeout(timer)
timer = window.setTimeout(() => {
if (prg + _speed >= _dist) {
window.clearTimeout(timer)
prg = _dist
callback && callback()
} else {
prg += _speed
progress (_dist, speed, delay, callback)
}
$progress.html(parseInt(prg) + '%')
console.log(prg)
}, _delay)
}
function random (n) {
if (typeof n === 'object') {
var times = n[1] - n[0]
var offset = n[0]
return Math.random() * times + offset
} else {
return n
}
}

完整地实现了这一功能。

如果目的是为了写一个纯粹障眼法的伪loading,那跟其他loading的实现就没什么区别了,我们做事讲究脚踏实地,能实现的实现,不能实现的,为了团队和谐,我们不得已坑蒙拐骗。那么我们还能更贴近实际情况一点吗?其实是可以的。

来分析一个场景,假设让loading更加真实一些,那么可以选择性地对页面上几个比较大的资源的加载进行跟踪,然后拆分整个进度条,比如页面有三张大图a、b、c,那么将进度条拆成五段,每加载完一张图就推进一个进度:

随机初始化[10, 20] ->
图a推进20%的进度 ->
图b推进25%的进度 ->
图c推进30%的进度 ->
完成100%

这三张图要占20% + 25% + 30% = 75%的进度。

问题是,如果图片加载完成是按照顺序来的,那可以很简单地:10(假设初始进度是10%) -> 30 -> 55 -> 85 -> 100,但事实是,图片不会按照顺序来,谁早到谁晚到是说不准的,所以需要更合理的方式去管理这些进度增量,使它们不会互相覆盖。

需要一个能够累计增量的变量next;
由于progress都是传目的进度的,需要另外一个函数add,来传增量进度。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
var $loading = $('#loading')
var $progress = $('#progress')
var prg = 0
var timer = 0
var now = new Date()
var timeout = 5000
var next = prg
add([30, 50], [1, 3], 100) // 第一阶段
window.setTimeout(() => { // 模拟图a加载完
add(20, [1, 3], 200)
}, 1000)
window.setTimeout(() => { // 模拟图c加载完
add(30, [1, 3], 200)
}, 2000)
window.setTimeout(() => { // 模拟图b加载完
add(25, [1, 3], 200)
}, 2500)
window.onload = () => {
complete()
}
if (now - loadingStartTime > timeout) {
complete()
} else {
window.setTimeout(() => {
complete()
}, timeout - (now - loadingStartTime))
}
function complete () {
add(100, [1, 5], 10, () => {
window.setTimeout(() => {
$loading.hide()
}, 1000)
})
}
function add (dist, speed, delay, callback) {
var _dist = random(dist)
if (next + _dist > 100) { // 对超出部分裁剪对齐
next = 100
} else {
next += _dist
}
progress(next, speed, delay, callback)
}
function progress (dist, speed, delay, callback) {
var _delay = random(delay)
var _speed = random(speed)
window.clearTimeout(timer)
timer = window.setTimeout(() => {
if (prg + _speed >= dist) {
window.clearTimeout(timer)
prg = dist
callback && callback()
} else {
prg += _speed
progress (dist, speed, delay, callback)
}
$progress.html(parseInt(prg) + '%')
console.log(prg)
}, _delay)
}
function random (n) {
if (typeof n === 'object') {
var times = n[1] - n[0]
var offset = n[0]
return Math.random() * times + offset
} else {
return n
}
}

用setTimeout来模拟图片的加载,真实应用应该是使用image.onload。

以上,一步步实现一个进度loading的过程了,演示代码可以戳codePen 写一个网页进度loading。

看似很简单的一个功能,其实仔细推敲,还是有很多细节要考虑的。

到这里,其实真的已经完成了,

实现这个功能非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Progress = require('ez-progress')
var prg = new Progress()
var $loading = $('#loading')
var $progress = $('#progress')
prg.on('progress', function (res) {
var progress = parseInt(res.progress) // 注意进度取整,不然有可能会出现小数
$progress.html(progress + '%')
})
prg.go([60, 70], function (res) {
prg.complete(null, [0, 5], [0, 50]) // 飞一般地冲向终点
}, [0, 3], [0, 200])
window.onload = function () {
prg.complete(null, [0, 5], [0, 50]) // 飞一般地冲向终点
}

木油错,94这么简单!

前端项目操作vue指南

发表于 2017-10-12

下载Git:https://git-scm.com/downloads

下载Node:https://nodejs.org/en/download/

项目搭建

1
2
3
4
5
6
git clone http://gitlab.odc.com/FE-Projects/wuxi-zhigu.git
cd/FE-Projects
npm install
npm install cooking-cli -g
cooking watch -p // 启动项目
cooking build -p // 打包项目

项目开发说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
代码说明:
<script>
import .. from ..; //引入组件以及其他插件
export default {
component:{
// 注册引入的组件,插件无需注册
},
data(){
return {
obj:{},
number:0
// template中所引用的数据
}
},
methods:{ // 页面中需要用到的方法
handleDosomething(){
// 自定义方法
}
},
created(){
this.handleDosomething(); // 调用方法
}
}
</script>

参考资料汇总:

vue语法:https://cn.vuejs.org/v2/guide/
路由定义:https://router.vuejs.org/zh-cn/
UI框架使用指南:http://element.eleme.io/#/zh-CN/component/installation
构建工具使用说明:http://cookingjs.github.io/zh-cn/index.html
git使用指南:http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/

react性能优化

发表于 2017-09-09

React是一个专注于UI层的框架,它使用虚拟DOM技术,以保证它UI的高速渲染;使用单向数据流,因此它数据绑定更加简单;
那么它内部是如何保持简单高效的UI渲染呢?这种渲染机制有可能存在什么性能问题呢?

React组件渲染问题引出

React不直接操作DOM,它在内存中维护一个快速响应的DOM描述,render方法返回一个DOM的描述,
React能够计算出两个DOM描述的差异,然后更新浏览器中的DOM。这就是著名的DOM Diff。

就是说React在接收到属性(props)或者状态(state)更新时,就会更新UI。所以React整个UI渲染是比较快的。但是这里面可能出现的问题是:

假设我们定义一个父组件,其包含了5000个子组件。我们有一个输入框输入操作,每次输入一个数字,对应的那个子组件背景色变红。

1
2
3
4
5
6
7
<Components>
<Components-1 />
<Components-2 />
<Components-3 />
...
<Components-5000 />
</Components>

这样我们输入数字1,则子组件1背景色变化,但是在这个过程中,所有的子组件都进行了重新渲染,导致整体渲染变慢。
造成这种现象的原因是 React中父组件更新默认触发所有子组件更新。

同时,我们经常在遍历列表元素时候会遇到这样的提示:

1
Warning: Each child in an array or iterator should have a unique "key" prop.

这就是我们今天要讨论的两个性能优化点:

1
2
父组件更新默认触发所有子组件更新
列表类型的组件默认更新方式非常复杂

React性能检测工具

安装 react 性能检测工具 npm i react-addons-perf –save,然后在./app/index.jsx中

1
2
3
4
5
// 性能测试
import Perf from 'react-addons-perf'
if (__DEV__) {
window.Perf = Perf
}

检测方法,在浏览器控制台输入如下命令:

1
2
3
开始记录:Perf.start()
结束记录:Perf.stop()
打印结果:printInclusive()

运行程序。在操作之前先运行Perf.start()开始检测,
然后进行若干操作,运行Perf.stop停止检测,
然后再运行Perf.printWasted()即可打印出浪费性能的组件列表。
在项目开发过程中,要经常使用检测工具来看看性能是否正常。

如果性能的影响不是很大,例如每次操作多浪费几毫秒、十几毫秒,个人以为没必要深究,但是如果浪费过多影响了用户体验,就必须去搞定它。

PureRenderMixin 优化

React 最基本的优化方式是使用PureRenderMixin,
安装工具 npm i react-addons-pure-render-mixin –save,然后在组件中引用并使用

1
npm i react-addons-pure-render-mixin --save
1
2
3
4
5
6
7
8
9
import React from 'react'
import PureRenderMixin from 'react-addons-pure-render-mixin'
class List extends React.Component {
constructor(props, context) {
super(props, context);
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
}
//其原理就是重写了 shouldComponentUpdate 方法。
}

React 有一个生命周期 hook 叫做shouldComponentUpdate,
组件每次更新之前,都要过一遍这个函数,如果这个函数返回true则更新,如果返回false则不更新。
而默认情况下,这个函数会一直返回true,就是说,如果有一些无效的改动触发了这个函数,也会导致无效的更新
那么什么是无效的改动?之前说过,组件中的props和state一旦变化会导致组件重新更新并渲染,
但是如果props和state没有变化也莫名其妙的触发更新了呢(这种情况确实存在)———— 这不就导致了无效渲染吗?

1
2
3
4
这里使用
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
的意思是重写组件的shouldComponentUpdate函数,
在每次更新之前判断props和state,如果有变化则返回true,无变化则返回false。

因此,我们在开发过程中,在每个 React 组件中都尽量使用PureRenderMixin

PureComponent

React 15.3.0 新增了一个 PureComponent 类,
以 ES2015 class 的方式方便地定义纯组件 (pure component),用于取代之前的 PureRenderMixin。

这个类的用法很简单,如果你有些组件是纯组件,那么把继承类从 Component 换成 PureComponent 即可。
当组件更新时,如果组件的 props 和 state 都没发生改变,render 方法就不会触发,
省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。

1
2
3
4
5
6
import React, { PureComponent } from 'react'
class Example extends PureComponent {
render() {
// ...
}
}

虽然React提供了Virtual DOM DOM Diff 等优秀的能力来提高渲染性能,但是在实际使用过程中,我们经常会遇到父组件更新,
不需要更新所以子组件的场景(分页),此时必须考虑利用React本周的渲染机制来进行优化。

相关代码:https://github.com/younth/react-tutorial/tree/master/%E6%80%A7%E8%83%BD%E8%B0%83%E4%BC%98

避免更新Demo

React 使用虚拟 DOM,它是在浏览器中的 DOM 子树的渲染描述,这个平行的描述让 React 避免创建和操作 DOM 节点,
这些远比操作一个 JavaScript 对象慢。当一个组件的 props 或 state 改变,
React 会构造一个新的虚拟 DOM 和旧的进行对比来决定真实 DOM 更新的必要性,
只有在它们不相等的时候,React 才会使用尽量少的改动更新 DOM。
在此之上,React 提供了生命周期函数 shouldComponentUpdate,
在重新渲染机制回路(虚拟 DOM 对比和 DOM 更新)之前会被触发,赋予开发者跳过这个过程的能力。这个函数默认返回 true,让 React 执行更新。
shouldComponentUpdate (nextProps, nextState) {
return true;
}
返回true时就会调用componentDidUpdata() {

}
切住,React 会非常频繁的调用这个函数shouldComponentUpdate(){},所以要确保它的执行速度够快。

假如你有个带有多个对话的消息应用,如果只有一个对话发生改变,如果我们在 ChatThread 组件执行 shouldComponentUpdate,React 可以跳过其他对话的重新渲染步骤。
我们可以简单的实现 shouldComponentUpdate 如下:

1
2
3
shouldComponentUpdate (nextProps, nextState) {
return this.props.value !== nextProps.value;
}

wangEditor 轻量级web富文本框 http://wangEditor.github.io/

发表于 2017-09-06

wangEditor —— 轻量级 web 富文本编辑器,配置方便,使用简单。支持 IE10+ 浏览器

官网:www.wangEditor.com
文档:www.kancloud.cn/wangfupeng/wangeditor3/332599
源码:github.com/wangfupeng1988/wangEditor

直接下载:https://github.com/wangfupeng1988/wangEditor/releases
使用npm下载:npm install wangeditor (注意 wangeditor 全部是小写字母)
使用bower下载:bower install wangEditor (前提保证电脑已安装了bower)
使用CDN://unpkg.com/wangeditor/release/wangEditor.min.js

demo 运行

下载源码 git clone git@github.com:wangfupeng1988/wangEditor.git
安装或者升级最新版本 node(最低v6.x.x)
进入目录,安装依赖包 cd wangEditor && npm i
安装包完成之后,windows 用户运行npm run win-example,Mac 用户运行npm run example
打开浏览器访问localhost:3000/index.html
用于 React、vue 或者 angular 可查阅文档中其他章节中的相关介绍

项目的根目录下安装 npm install wangeditor || npm install –save wangeditor
node_modle 下查看 wangeditor 是否安装成功,

成功安装即可:

1
import E from 'wangeditor';

初始化项目:本项目用vue

1
2
3
4
this.editList = new E('#div3');
this.editList.customConfig.showLinkImg = true;
this.editList.customConfig.uploadImgServer = '/upload'; // 上传图片设置后端服务器
this.editList.create();

读取内容 可以html和text的方式读取编辑器的内容,详情见说明文档: https://www.kancloud.cn/wangfupeng/wangeditor3/335775

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="div1">
<p>欢迎使用 wangEditor 编辑器</p>
</div>
<button id="btn1">获取html</button>
<button id="btn2">获取text</button>
<script type="text/javascript" src="/wangEditor.min.js"></script>
<script type="text/javascript">
var E = window.wangEditor
var editor = new E('#div1')
editor.create()
// 读取 html
document.getElementById('btn1').addEventListener('click', function () {
alert(editor.txt.html())
}, false)
// 读取 text
document.getElementById('btn2').addEventListener('click', function () {
alert(editor.txt.text())
}, false)
</script>

webpack + React 开发环境

发表于 2017-09-05

什么是 webpack?

webpack是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如JS(含JSX)、coffee、样式(含less/sass)、图片等都作为模块来使用和处理。
我们可以直接使用 require(XXX) 的形式来引入各模块,即使它们可能需要经过编译(比如JSX和sass),但我们无须在上面花费太多心思,因为 webpack 有着各种健全的加载器(loader)在默默处理这些事情,因为以近期 Github 上各大主流的(React相关)项目来说,它们仓库上所展示的示例已经是基于 webpack 来开发的,比如 React-Bootstrap 和 Redux链接:https://github.com/gaearon/redux
webpack的官网是 http://webpack.github.io/ ,文档地址是 http://webpack.github.io/docs/ ,想对其进行更详细了解的可以点进去瞧一瞧。

webpack 的优势

其优势主要可以归类为如下几个:

  1. webpack 是以 commonJS 的形式来书写脚本,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。

  2. 能被模块化的不仅仅是 JS 了。

  3. 开发便捷,能替代部分 grunt/gulp 的工作,比如打包、压缩混淆、图片转base64等。

  4. 扩展性强,插件机制完善,特别是支持 React 热插拔(见 react-hot-loader )的功能让人眼前一亮。

我们谈谈第一点。以 AMD/CMD 模式来说,鉴于模块是异步加载的,所以我们常规需要使用 define 函数来帮我们搞回调:

1
2
3
4
5
6
7
8
9
10
define(['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});

另外为了可以兼容 commonJS 的写法,我们也可以将 define 这么写:

1
2
3
4
5
6
7
8
9
10
11
12
define(function (require, exports, module){
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});

然而对 webpack 来说,我们可以直接在上面书写 commonJS 形式的语法,无须任何 define (毕竟最终模块都打包在一起,webpack 也会最终自动加上自己的加载器):

1
2
3
4
5
6
7
8
9
10
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};

即使你保留了之前 define 的写法也是可以滴,毕竟 webpack 的兼容性相当出色,方便你旧项目的模块直接迁移过来。

安装和配置

一. 安装

我们常规直接使用 npm 的形式来安装:
初始化 npm 环境
首先保证有 node 和 npm 环境,运行node -v和npm -v查看
进入项目目录,运行npm init按照步骤填写最终生成package.json文件,所有使用 npm 做依赖管理的项目,根目录下都会有一个这个文件,该文件描述了项目的基本信息以及一些第三方依赖项(插件)。

1
2
$ npm install webpack webpack-dev-server --save-dev
$ npm i react react-dom --save

安装完成之后,查看package.json可看到多了 devDependencies 和 dependencies 两项,根目录也多了一个node_modules文件夹。

–save 和 –save-dev 的区别

npm i时使用–save和–save-dev,可分别将依赖(插件)记录到package.json中的dependencies和devDependencies下面。
dependencies下记录的是项目在运行时必须依赖的插件,
常见的例如react jquery等,即及时项目打包好了、上线了,这些也是需要用的,否则程序无法正常执行。

devDependencies下记录的是项目在开发过程中使用的插件,
例如这里我们开发过程中需要使用webpack打包,而我在工作中使用fis3打包,但是一旦项目打包发布、上线了之后,webpack和fis3就都没有用了,可卸磨杀驴。

延伸一下,我们的项目有package.json,其他我们用的项目如webpack也有package.json,
见./node_modules/webpack/package.json,其中也有devDependencies和dependencies。
当我们使用npm i webpack时,
./node_modules/webpack/package.json中的dependencies会被 npm 安装上,而devDependencies也没必要安装。

配置 webpack.config.js

为了提高学习效率,webpack最最基础的用法,webpack.config.js这个配置文件详细的讲解一下,

文件格式

webpack.config.js 就是一个普通的 js 文件,符合 commonJS 规范。最后输出一个对象,即module.exports = {…}

输入 & 输出

这个比较基础,不过需要新建./app/index.jsx作为入口文件,目前项目中只有这一个入口文件。不过 webpack 支持多个入口文件,可查阅文档。
输出就是一个bundle.js,js 和 css 都在里面,不过只有在开发环境下才用,发布代码的时候,肯定不能只有这么一个文件,接下来会讲到。

module

针对不同类型的文件,使用不同的loader,当然使用之前要安装,
例如npm i style-loader css-loader –save-dev。该项目代码中,我们用到的文件格式有:js/jsx 代码、css/less 代码、图片、字体文件。
less 是 css 的语法糖,可以更高效低冗余的写 css,不熟悉的朋友可去官网看看,非常简单。
在加载 css/less 时用到了postcss,主要使用autoprefixer功能,帮助自动加 css3 的浏览器前缀,非常好用。
编译 es6 和 jsx 语法时,用到家喻户晓的babel,另外还需增加一个.babelrc的配置文件。

plugins

使用 html 模板(需要npm i html-webpack-plugin –save-dev),这样可以将输出的文件名(如./bundle.js)自动注入到 html 中,不用我们自己手写。手写的话,一旦修改就需要改两个地方。
使用热加载和自动打开浏览器插件

devServer

对 webpack-dev-server 的配置

npm start

在 package.json 中,输入以下代码,将这一串命令封装为npm start,这样就可以运行项目代码了。

1
2
3
"scripts": {
"start": "NODE_ENV=dev webpack-dev-server --progress --colors"
}

代码中NODE_ENV=dev代表当前是开发环境下,这里的”dev”可被 js 代码中的process.env.NODE_ENV得到并做一些其他处理。
定义环境全局变量
以下定义,可使得代码通过DEV得到当前是不是开发模式。

1
2
3
new webpack.DefinePlugin({
__DEV__: JSON.stringify(JSON.parse((process.env.NODE_ENV == 'dev') || 'false'))
})

打开./app/util/localStore.js可以看到if (DEV) { console.error(‘localStorage.getItem报错, ‘, ex.message) },即只有开发环境下才提示error,发布之后就不会提示了。(因为发布的命令中用到NODE_ENV=production)

配置 webpack.production.config.js

开发环境下,可以不用考虑系统的性能,更多考虑的是如何增加开发效率。而发布系统时,就需要考虑发布之后的系统的性能,包括加载速度、缓存等。下面介绍发布用配置代码和开发用的不一样的地方。
发布到 ./build 文件夹中
path: __dirname + “/build”,

vendor

将第三方依赖单独打包。即将 node_modules 文件夹中的代码打包为 vendor.js 将我们自己写的业务代码打包为 app.js。这样有助于缓存,因为在项目维护过程中,第三方依赖不经常变化,而业务代码会经常变化。

md5后缀

为每个打包出来的文件都加md5后缀,例如”/js/[name].[chunkhash:8].js”,可使文件强缓存。
分目录
打包出来的不同类型的文件,放在不同目录下,例如图片文件将放在img/目录下

Copyright

自动为打包出来的代码增加 copyright 内容

分模块

new webpack.optimize.OccurenceOrderPlugin(),

压缩代码

使用 Uglify 压缩代码,其中warnings: false是去掉代码中的 warning
分离 css 和 js 文件
开发环境下,css 代码是放在整个打包出来的那个 bundle.js 文件中的,发布环境下当然不能混淆在一起,使用new ExtractTextPlugin(‘/css/[name].[chunkhash:8].css’),将 css 代码分离出来。
npm run build
打开package.json,查看以下代码。npm start和npm run build分别是运行代码和打包项目。另外,”start”、”test”可以不用run。

1
2
3
4
"scripts": {
"start": "NODE_ENV=dev webpack-dev-server --progress --colors",
"build": "rm -rf ./build && NODE_ENV=production webpack --config ./webpack.production.config.js --progress --colors"
},

这两个命令主要有以下区别:
前者中默认使用 webpack.config.js 作为配置文件,而后者中强制使用 webpack.production.config.js 作为配置文件
前者NODE_ENV=dev而后者NODE_ENV=production,标识不同的环境。而这个”dev” “production”可以在代码中通过process.env.NODE_ENV获取。
最小化压缩 React
以下配置可以告诉 React 当前是生产环境,请最小化压缩 js ,即把开发环境中的一些提示、警告、判断通通去掉,直流以下发布之后可用的代码。

1
2
3
4
5
new webpack.DefinePlugin({
'process.env':{
'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
}
}),

二. 配置

每个项目下都必须配置有一个 webpack.config.js ,它的作用如同常规的 gulpfile.js/Gruntfile.js ,就是一个配置项,告诉 webpack 它需要做什么。

我们看看下方的示例:

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
var webpack = require('webpack');
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');
module.exports = {
//插件项
plugins: [commonsPlugin],
//页面入口文件配置
entry: {
index : './src/js/page/index.js'
},
//入口文件输出配置
output: {
path: 'dist/js/page',
filename: '[name].js'
},
module: {
//加载器配置
loaders: [
{ test: /\.css$/, loader: 'style-loader!css-loader' },
{ test: /\.js$/, loader: 'jsx-loader?harmony' },
{ test: /\.scss$/, loader: 'style!css!sass?sourceMap'},
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}
]
},
//其它解决方案配置
resolve: {
//查找module的话从这里开始查找
root: 'E:/github/flux-example/src', //绝对路径
//自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
extensions: ['', '.js', '.json', '.scss'],
//模块别名定义,方便后续直接引用别名,无须多写长长的地址
alias: {
AppStore : 'js/stores/AppStores.js',//后续直接 require('AppStore') 即可
ActionType : 'js/actions/ActionType.js',
AppAction : 'js/actions/AppAction.js'
}
}
};

⑴ plugins 是插件项,这里我们使用了一个 CommonsChunkPlugin 的插件,它用于提取多个入口文件的公共脚本部分,然后生成一个 common.js 来方便多页面之间进行复用。

⑵ entry 是页面入口文件配置,output 是对应输出项配置(即入口文件最终要生成什么名字的文件、存放到哪里),其语法大致为:

1
2
3
4
5
6
7
8
9
10
11
{
entry: {
page1: "./page1",
//支持数组形式,将加载数组中的所有模块,但以最后一个模块作为输出
page2: ["./entry1", "./entry2"]
},
output: {
path: "dist/js/page",
filename: "[name].bundle.js"
}
}

该段代码最终会生成一个 page1.bundle.js 和 page2.bundle.js,并存放到 ./dist/js/page 文件夹下。
⑶ module.loaders 是最关键的一块配置。它告知 webpack 每一种文件都需要使用什么加载器来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
module: {
//加载器配置
loaders: [
//.css 文件使用 style-loader 和 css-loader 来处理
{ test: /\.css$/, loader: 'style-loader!css-loader' },
//.js 文件使用 jsx-loader 来编译处理
{ test: /\.js$/, loader: 'jsx-loader?harmony' },
//.scss 文件使用 style-loader、css-loader 和 sass-loader 来编译处理
{ test: /\.scss$/, loader: 'style!css!sass?sourceMap'},
//图片文件使用 url-loader 来处理,小于8kb的直接转为base64
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}
]
}

⑷ 最后是 resolve 配置,这块很好理解,直接写注释了:

1
2
3
4
5
6
7
8
9
10
11
12
resolve: {
//查找module的话从这里开始查找
root: 'E:/github/flux-example/src', //绝对路径
//自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
extensions: ['', '.js', '.json', '.scss'],
//模块别名定义,方便后续直接引用别名,无须多写长长的地址
alias: {
AppStore : 'js/stores/AppStores.js',//后续直接 require('AppStore') 即可
ActionType : 'js/actions/ActionType.js',
AppAction : 'js/actions/AppAction.js'
}
}

关于 webpack.config.js 更详尽的配置可以参考这里。http://webpack.github.io/docs/configuration.html

px、em、rem区别介绍

发表于 2017-08-04

px像素(Pixel)

相对长度单位。像素px是相对于显示器屏幕分辨率而言的。
PX特点

  1. IE无法调整那些使用px作为单位的字体大小;
  2. 国外的大部分网站能够调整的原因在于其使用了em或rem作为字体单位;
  3. Firefox能够调整px和em,rem,但是96%以上的中国网民使用IE浏览器(或内核)。

EM是相对长度单位

相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。
EM特点

  1. em的值并不是固定的;
  2. em会继承父级元素的字体大小。
    1
    2
    3
    4
    5
    6
    注意:任意浏览器的默认字体高都是16px。所有未经调整的浏览器都符合: 1em=16px。那么12px=0.75em,10px=0.625em。为了简化font-size的换算,需要在css中的body选择器中声明Font-size=62.5%,这就使em值变为 16px*62.5%=10px, 这样12px=1.2em, 10px=1em, 也就是说只需要将你的原来的px数值除以10,然后换上em作为单位就行了。
    所以我们在写CSS的时候,需要注意两点:
    1. body选择器中声明Font-size=62.5%;
    2. 将你的原来的px数值除以10,然后换上em作为单位;
    3. 重新计算那些被放大的字体的em数值。避免字体大小的重复声明。
    也就是避免1.2 * 1.2= 1.44的现象。比如说你在#content中声明了字体大小为1.2em,那么在声明p的字体大小时就只能是1em,而不是1.2em, 因为此em非彼em,它因继承#content的字体高而变为了1em=12px。

rem是CSS3新增的一个相对单位

这个单位引起了广泛关注。这个单位与em有什么区别呢?区别在于使用rem为元素设定字体大小时,仍然是相对大小,但相对的只是HTML根元素。这个单位可谓集相对大小和绝对大小的优点于一身,通过它既可以做到只修改根元素就成比例地调整所有字体大小,又可以避免字体大小逐层复合的连锁反应。目前,除了IE8及更早版本外,所有浏览器均已支持rem。对于不支持它的浏览器,应对方法也很简单,就是多写一个绝对单位的声明。这些浏览器会忽略用rem设定的字体大小。下面就是一个例子:

1
2
p {font-size:14px; font-size:.875rem;}
注意: 选择使用什么字体单位主要由你的项目来决定,如果你的用户群都使用最新版的浏览器,那推荐使用rem,如果要考虑兼容性,那就使用px,或者两者同时使用。

px 与 rem 的选择?
对于只需要适配少部分手机设备,且分辨率对页面影响不大的,使用px即可 。
对于需要适配各种移动设备,使用rem,例如只需要适配iPhone和iPad等分辨率差别比较挺大的设备。

react 踩过的坑

发表于 2017-08-04

1 在JSX的元素中写入内联样式,例如

1
2
<div style={"color:blue"}></div>
报错:warning:Style prop value must be an object react/style-prop-object

原因:在React框架的JSX编码格式要求,style必须是一个对象
解决方法:除了外部那个表示Javascript语句的花括号外,里面必须再写一个花括号{}包含的对象,例如

1
<div style={ { color:“blue” } }></div>,

外部的{ }表示这是Javascript句法,里面的{ }是一个对象

2 遍历数组元素:

1
2
3
4
5
var arr=[1,2,3]
arr.map(function(x){
return (<div></div>);
})
报错:Warning:Each child in an array or iterator should have a unique "key" prop. Check the render method of `NavBlock`

原因:在React中数组遍历返回元素或组件时需加上key属性作为唯一标识
解决方法:写成

1
2
3
4
var arr=[1,2,3]
arr.map(function(x,i){
return (<div key=i></div>);
})

3 在render()函数中返回时这样写:

1
2
3
4
5
render(){
return <div></div>
<div></div>
<div></div>
}

报错:Adjacent JSX elements must be wrapped in an enclosing tag (75:8)
原因:render()函数中返回的所有元素需要包裹在一个外部元素里面
解决方法:可改写为:

1
2
3
4
5
6
7
render(){
return <section>
<div></div>
<div></div>
<div></div>
</section>
}

最后一点—不能写成:(return语句和返回元素不在同一行会被解析器视为返回null导致错误)

1
2
3
4
5
6
7
8
render(){
return
<section>
<div></div>
<div></div>
<div></div>
</section>
}

React生命周期总结

发表于 2017-08-03

react生命周期流程

1.初始化,首次render

getDefaultProps 方法可以用来设置组件属性的默认值,在组件被建立时候就立即调用

getInitialState 方法用于定义初始状态

componentWillMount只在初始化时候被调用一次

render()

componentDidmount()在子组件也都加载完毕后执行,在RN中就是指组件初始化加载完毕,在react中DOM渲染完成

2.props发生改变时候更新

componentWillReceiveProps(nextProps)

componentWillReceiveProps方法可以比较this.props和nextProps来看是否发生了变化,然后可以进行setState等操作。
注意只要父组件此方法触发,那么子组件也会触发,也就是说父组件更新,子组件也会跟着更新。

shouldComponentUpdate(nextProps, nextState)

shouldComponentUpdate 方法在接收了新的props或state后触发,可以通过返回true或false来控制后面的生命周期流程是否触发。

componentWillUpdate(nextProps, nextState)

componentWillUpdate在props或state改变或shouldComponentUpdate返回true后触发。不可在其中使用setState。
render()

重新渲染。

componentDidupdate(prevProps, prevState)

会等子组件也都更新完后才触发。

3.state发生改变时候更新

shouldComponentUpdate(nextProps, nextState)

shouldComponentUpdate 方法在setState后state发生改变后触发,你可以通过返回true或false来控制后面的生命周期流程是否触发。

componentWillUpdate(nextProps, nextState)

componentWillUpdate在state改变或shouldComponentUpdate返回true后触发。不可在其中使用setState。

render()

重新渲染。

componentDidupdate(prevProps, prevState)

会等子组件也都更新完后才触发。

index作为key是一种反模式

1
2
3
{
this.state.data.map(el=><MyComponent key={Math.random()} />)
}

代码中MyComponent的key值是用Math.random随机生成的,虽然能够保持其唯一性,但是它的值是随机而不是稳定的,在数组动态改变时会导致数组元素中的每项都重新销毁然后重新创建,有一定的性能开销;另外可能导致一些意想不到的问题出现。所以:key的值要保持稳定且唯一,不能使用random来生成key的值。
所以,在不能使用random随机生成key时,我们可以像下面这样用一个全局的localCounter变量来添加稳定唯一的key值。

1
2
3
4
5
6
7
8
9
10
11
var localCounter = 1;
this.data.forEach(el=>{
el.id = localCounter++;
});
//向数组中动态添加元素时,
function createUser(user) {
return {
...user,
id: localCounter++
}
}

forEach和map和for方法的区别:

总结:
1、map速度比foreach快
2、map会返回一个新数组,不对原数组产生影响,foreach不会返回新数组,
3、map因为返回数组所以可以链式操作,foreach不能

为什么更推荐用.map(),而不是.forEach()?

第一:.map()要比.forEach()执行速度快。虽然我也说过执行速度不是我们需要考虑的主要因素,但是他们都比for()要好用,那肯定要选更优化的一个。
第二:.forEach()的返回值并不是array。如果你想用函数式编程写个链式表达式来装个逼,.map()将会是你不二的选择。

来看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var arr = [1, 2, 3];
console.log(
arr.map(function(i){
return i+i;
})
//链式风格
.sort()
);// [2,4,6]
console.log(
arr.forEach(function(i){
return i+i;
})
//接不起来,断了
.sort()
); // TypeError: Cannot read property 'sort' of undefined
最后,感谢大家耐心的阅读,排个序
.map() > .forEach() > for()

区别:map的回调函数中支持return返回值;return的是啥,就相当于返回一个新数组(并不影响原来的数组,只是相当于把原数组克隆一份,把克隆的这一份的数组中的对应项改变了);
不管是forEach还是map 都支持第二个参数值,第二个参数的意思是把匿名回调函数中的this进行修改。

es5的写法: bind可以改变this的指向

1
2
3
4
5
6
7
8
componentDidmount: function() {
alert('did');
window.setInterval(function(){
this.setState({
fontSize: '44px'
})
}, bind(this), 1000);
}

跨域

发表于 2017-07-28

document.domain 子域名跨域

这跨域个方法主要适用于 ajax跨子域,比如在 a.takozhang.cn 中使用ajax调用 takozhang.cn 中的接口时。

  1. 在被访问接口的服务器中,新建一个proxy.html文件,里面的代码是:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <script>
    document.domain = 'takozhang.cn'
    </script>
    <script src = 'jquery.js'></script>
    2. 在调用页面使用一个隐藏的iframe,地址指向proxy.html,代码如下:
    <iframe src="http://takozhang.cn/proxy.html" id="proxy"></iframe>
    3. 在a.takozhang.cn服务器中进行ajax发送请求的时候,代码如下:
    var iframe = document.getElementById('proxy').contentWindow;
    iframe.window.JQuery.ajax({
    url: 'takozhang.cn/php/index.php',
    method: 'post',
    success: function(data){},
    error: function(err){}
    });

jsonp

基本可以适用于任何场景

  1. 客户端动态创建script标签,并利用src属性去向服务端获取数据 src=”www.a.com/index.php?callback=fn”,切记一定要在后面加上一个callback回调函数
  2. 服务端会将请求的json数据放在这个你指定的回调函数里作为参数返回回来,如服务器会返回 fn(data),
  3. 客户端写好回调函数的实现,处理数据。
    4.但是jsonp是存在着一定的风险的,就是你用jsonp, 然后黑客给你发了一张图片。图片的src=”xxx/xxx/xx.jsonp?data=sxx&name=xxxx”, 你点击后。直接请求成功。 。这个就是CSRF 。跨域请求伪造
    还有一个比较典型的安全问题、XSS 。跨脚本注入js

window.name 也就是 iframe

只用于两个完全不同源的域,比如www.a.com www.b.com
window.name属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name

  1. 在www.a.com的服务器中创建一个iframe,并引入www.b.com,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <script>
    var inf=document.createElement("iframe"); //创建iframe
    inf.src="http://www.b.com/index.html"+"?h=5" //加载数据页www.b.com/index.html的同时携带参数h=5
    var body=document.getElementsByTagName("body")[0];
    body.appendChild(inf); //引入iframe
    inf.onload=function(){
    inf.src='http://www.a.com/index.html' //iframe加载完成,加载www.a.com域下边的空白页index.html
    console.log(inf.contentWindow.name) //输出window.name中的数据
    body.removeChild(inf) //清除iframe
    }
    </script>
  2. 在www.b.com中的index.html中进行ajax,并把返回值通过window.name储存,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var str=window.location.href.substr(-1,1); //获取url中携带的参数值
    $.ajax({
    type:"post",
    url:"http://www.b.com/index.php"+"?m="+str, //通过ajax将查询参数传给php页面
    success:function(res){
    window.name=res //将接收到的查询数据赋值给window.name
    },
    error:function(){
    window.name='error'
    }
    });

整体过程就是:a服务器的页面中中创建一个iframe,iframe引入b服务器中进行ajax的页面,该页面ajax之后将返回值赋值给window.name,
a服务器页面监听iframe事件的加载完成,再将iframe地址指向a服务器中的某个空白页面,然后读取iframe的window.name。

window.postMessage(message,targetOrigin)

这个方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,
无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。

在父页面中嵌入子页面,通过postMessage发送数据。parent.com/index.html中的代码:

1
2
3
4
5
6
7
8
9
10
<iframe id="ifr" src="child.com/index.html"></iframe>
<script type="text/javascript">
window.onload = function() {
var ifr = document.getElementById('ifr');
var targetOrigin = 'http://child.com';
// 若写成'http://child.com/c/proxy.html'效果一样
// 若写成'http://c.com'就不会执行postMessage了
ifr.contentWindow.postMessage('I was there!', targetOrigin);
};
</script>

在子页面中通过message事件监听父页面发送来的消息并显示。child.com/index.html中的代码:

1
2
3
4
5
6
7
8
9
10
11
<script type="text/javascript">
window.addEventListener('message', function(event){
// 通过origin属性判断消息来源地址
if (event.origin == 'http://parent.com') {
alert(event.data); // 弹出"I was there!"
alert(event.source);
// 对parent.com、index.html中window对象的引用
// 但由于同源策略,这里event.source不可以访问window对象
}
}, false);
</script>

主要是用来传递信息,父页面使用window.postMessage(),子页面使用

1
window.addEventListener('message',function(event){})

js中toFixed精度问题的解决办法

发表于 2017-07-10

最近在做项目的时候,遇到了有四舍五入保留两位的需求,当时不假思索的直接使用了js原生的toFixed方法,结果问题百出。有以下几点问题:

1.四舍五入并不是真正的四舍五入

这个问题是在测试阶段我们的测试人员提出来的。一开始我也很吃惊,结果待我在控制台试了一些数据之后,我懵逼了,我一直在用的toFixed方法竟然有问题,我竟然糊涂的用它做了很多事!以下是我在chrome上的结果:

1
2
3
4
5
6
1.35.toFixed(1) // 1.4 正确
1.335.toFixed(2) // 1.33 错误
1.3335.toFixed(3) // 1.333 错误
1.33335.toFixed(4) // 1.3334 正确
1.333335.toFixed(5) // 1.33333 错误
1.3333335.toFixed(6) // 1.333333 错误

果然有问题,只能网上找资料了,结果又发现同样是上面的一段代码,在IE下又小同大异,以下是IE上的结果:

1
2
3
4
5
6
1.35.toFixed(1) // 1.4 正确
1.335.toFixed(2) // 1.34 正确
1.3335.toFixed(3) // 1.334 正确
1.33335.toFixed(4) // 1.3334 正确
1.333335.toFixed(5) // 1.33334 正确
1.3333335.toFixed(6) // 1.333334 正确

果然IE才是爸爸。难道是浏览器兼容性问题?兼容性问题难道不应该是出在IE中吗?既然找到问题所在,就好下手。我的办法是把要四舍五入的后一位单独拎出来单独判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let result = number.toString();
const arr = result.split('.');
const integer = arr[0];
const decimal = arr[1];
result = integer + '.' + decimal.substr(0, n);
const last = decimal.substr(n, 1);
// 四舍五入,转换为整数再处理,避免浮点数精度的损失
if (parseInt(last, 10) >= 5) {
const x = Math.pow(10, n);
result = ((parseFloat(result) * x) + 1) / x;
result = result.toFixed(n);
}
return result;

自己测了几遍,貌似没什么问题,OK~

2.计算机二进制编码导致的精度问题

没过多久,测试又提出来页面报错了~~ 心塞啊,怎么可能报错呢?自己debugger,发现页面中的js进了死循环。很明显问题出在toFixed中回调了toFixed,结果没有走出来,继续debugger,又有了进人的发现。以下是控制台测试:

console.log(2.115 100) // 211.50000000000003
console.log(2.0115
1000) // 2011.4999999999998
能告诉我是在闹哪样?好吧,我猜到了,肯定是计算机的进制问题。既然你一直进入循环,我就手动把你拉出来。

result = (Math.round((parseFloat(result)) * x) + 1) / x;
强制四舍五入取整,不会进死循环了!

以下是全部代码:

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
// toFixed兼容方法
Number.prototype.toFixed = function (n) {
if (n > 20 || n < 0) {
throw new RangeError('toFixed() digits argument must be between 0 and 20');
}
const number = this;
if (isNaN(number) || number >= Math.pow(10, 21)) {
return number.toString();
}
if (typeof (n) == 'undefined' || n == 0) {
return (Math.round(number)).toString();
}
let result = number.toString();
const arr = result.split('.');
// 整数的情况
if (arr.length < 2) {
result += '.';
for (let i = 0; i < n; i += 1) {
result += '0';
}
return result;
}
const integer = arr[0];
const decimal = arr[1];
if (decimal.length == n) {
return result;
}
if (decimal.length < n) {
for (let i = 0; i < n - decimal.length; i += 1) {
result += '0';
}
return result;
}
result = integer + '.' + decimal.substr(0, n);
const last = decimal.substr(n, 1);
// 四舍五入,转换为整数再处理,避免浮点数精度的损失
if (parseInt(last, 10) >= 5) {
const x = Math.pow(10, n);
result = (Math.round((parseFloat(result) * x)) + 1) / x;
result = result.toFixed(n);
}
return result;
};

123
玉英(小丸子)

玉英(小丸子)

将来的你,一定会感谢现在拼命努力的自己

22 日志
Github
© 2017 玉英(小丸子)
由 Hexo 强力驱动
主题 - NexT.Pisces