前端学习之路


  • 首页

  • 归档

  • 分类

  • 标签

  • 搜索
close
小朱

小朱

前端学习之路

168 日志
37 分类
37 标签
RSS
GitHub
友情链接
  • 极客学院
  • caniuse
  • codepen
  • JS Bin
  • Babel在线编译
  • Iconfinder图标
  • 在线JSON格式化
  • 智能图像压缩

JavaScript-事件循环

发表于 2017-01-08   |   分类于 JavaScript

参考
JavaScript运行机制之事件循环(Event Loop)详解
从setTimeout说事件循环模型

单线程

JavaScript语言是单线程的,这事因为作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。这是这门语言的核心特征。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,并没有改变JavaScript单线程的本质。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。JavaScript就有两种执行方式:一种是CPU按顺序执行,前一个任务结束,再执行下一个任务,这叫做同步执行;另一种是CPU跳过等待时间长的任务,先处理后面的任务,这叫做异步执行。
异步执行的运行机制如下(同步执行也是如此,因为它可以被视为没有异步任务的异步执行):

  • 所有任务(同步和异步的)都在主线程上执行,形成一个”执行栈”(execution context stack)
  • 主程之外,还存在一个”任务队列”(task queue),系统把异步任务放到”任务队列”之中,然后继续执行后续的任务
  • 一旦”执行栈”中的所有任务执行完毕,系统就会读取”任务队列”,如果这个时候,异步任务已经结束了等待状态,就会从”任务队列”进入执行栈,恢复执行
  • 主线程不断重复上面的第三步,只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制

事件循环

“任务队列”实质上是一个事件的队列(也可以理解成消息的队列),主线程读取”任务队列”,就是读取里面有哪些事件。如IO设备完成一项任务,就在”任务队列”中添加一个事件,表示相关的异步任务可以进入”执行栈”了;鼠标点击、页面滚动等只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。

所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当异步任务从”任务队列”回到执行栈,回调函数就会执行。

Javascript执行引擎的主线程运行的时候,产生堆和栈。程序中代码依次进入栈中等待执行。栈中的代码调用各种外部API,在”任务队列”中加入各种事件,只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。执行栈中的代码,总是在读取”任务队列”之前执行。不同的操作添加到任务队列的时机也不同。

onclick 由浏览器内核的DOM Binding模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
setTimeout 会由浏览器内核的timer模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
ajax 则会由浏览器内核的network模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。

主线程从”任务队列”中读取事件,只要执行栈一清空,”任务队列”上第一位的事件就自动返回主线程,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环(Event Loop)。

示例一输出结果都是4,表明程序是先运行完4次循环后,再进入setTimeout的。
实例二输出结果是1001,表明程序是向下执行完while循环后,再进入setTimeout的。

1
2
3
4
5
6
// 示例一
for(var i = 0; i<=3; i++){
setTimeout(function() {
console.log(i); // 输出4
}, 0);
}
1
2
3
4
5
6
7
// 实例二
var startDate = new Date();
setTimeout(function() {
var endDate = new Date();
console.log(endDate - startDate); // 输出1001
}, 500);
while(new Date() - startDate < 1000){};

定时器

“任务队列”除了放置异步任务,还可以放置定时事件,即指定某些代码在多少时间之后执行,即到达设置的延时时间时被添加至任务队列里。定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

需要注意的是,setTimeout()只是将事件插入了”任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

Node.js除了setTimeout和setInterval这两个方法,还提供了另外两个与”任务队列”有关的方法:process.nextTick和setImmediate。

  • process.nextTick方法可以在当前”执行栈”的尾部,也就是主线程下一次读取”任务队列”之前,触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。
  • setImmediate方法则是在当前”任务队列”的尾部触发回调函数,也就是说,它指定的任务总是在主线程下一次读取”任务队列”时执行,这与setTimeout很像,但一次”事件循环”只能触发一个由setImmediate指定的回调函数。

React-Native-基础2

发表于 2016-12-29   |   分类于 React Native

View组件

View组件还有三个没有在官方文档中公布的回调函数:onTouchStart、onTouchMove、onTouchEnd。这三个回调函数都有一个特殊的性质,就是在React Native开发中,通常是在最上层的组建中(render函数靠后渲染的组件)处理触摸事件的;但这三个回调函数总能收到事件,而不管用户当前触摸区域是空白的,还是有其它组件已经处理了触摸事件。

pointEvents属性

在React Native中,触摸事件总是被传送给最上层的组件。当很多组件被布局在手机屏幕上时,有的组件可能会遮盖住它下方的其它组件。对于某些应用逻辑,被遮盖住的组件需要处理触摸事件,这时就可以使用pointerEvents属性。

  • 取值none,表示发生在本组件与本组件的子组件上的触摸事件都会交给本组件的父组件处理;
  • 取值box-none,表示发生在本组件显示范围内(但非本组件的子组件显示范围内)的事件将交由本组件的父组件处理,发生在本组件的子组件显示范围内的触摸事件由子组件处理;
  • 取值box-only,表示发生在本组件显示范围内的触摸事件将全部有本组件处理(即使触摸事件发生在本组件的子组件显示范围内);
  • 取值auto会视组件的不同而不同。

设备放置状态、onLayout回调函数

使项目在两个平台行都能自动调整应用的显示方式来适应设备当前的放置状态,需要:

  • 在应用启动时能检测到设备是横置的还是竖置的;
  • 当设备从横置变为竖置或从竖置变为横置时,应用要能监测到这个事件。

检测设备当前是竖置还是横置的一个方法是取当前设备屏幕的宽与高,正常的设备在竖置时宽小于高,在横置时宽大于高。Demensions API 可以获取屏幕的宽和高。

通常,React Native 开发的应用有一个或多个根View,根View的特点是它没有父组件。onLayout回调函数在组件被加载或者布局被改变时会被调用,通过指定这个根View组件的onLayout回调函数可以很方便地得到初始设备的放置状态,检测设备放置状态的改变并得到改变后新的屏幕高度与宽度。为了实现这些功能,不能指定根View的宽和高,并需要设定根View组件的样式flex值为1。注意,模拟器显示方式被改变时不会触发Layout事件。

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
'user strick';
var React = require('react-native');
var { AppRegister, StyleSheet, Text, View, Dimensions } = React;
var Project19 = React.createClass({
_onLayout: function (event) {
console.log('屏幕的宽度:', Dimensions.get('window').width);
console.log('屏幕的高度:', Dimensions.get('window').height);
let { x, y, width, height } = event.nativeEvent.layout;
console.log('View的宽度:', width);
console.log('View的高度:', height);
console.log('View的左上顶点横坐标:', x);
console.log('View的左上顶点纵坐标:', y);
},
_onLayoutText: function(event) {
let { x, y, width, height } = event.nativeEvent.layout;
console.log('Text的宽度:', width);
console.log('Text的高度:', height);
console.log('Text的左上顶点横坐标:', x);
console.log('Text的左上顶点纵坐标:', y);
},
render: function() {
return (
<View style={styles.container} onLayout={this._onLayout}>
<Text style={styles.welcom} onLayout={this._onLayoutText}>
Welcom to React Native!
</Text>
</View>
);
}
});
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItem: 'center',
backgroundColor: '#F5FCFF',
},
welcom: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
});
AppRegister.registerComponent('Project19', () => Project19);

setNativeProps成员函数

每个React Native组件都有一个公开的成员函数setNativeProps,使用它可以增加或修改组件的属性,但不建议使用该函数,它是一个“简单、粗暴”的方法,可以直接操作任何层面组件的属性,而不是使用React Native组件的状态机变量,这样会使代码逻辑混乱。

在不得不频繁刷新而又遇到性能瓶颈时,比如创建连续的动画,同时要避免渲染组件结构和同步太多的视图变化所带来的大量开销时,才考虑使用setNativeProps函数。

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
let Project19 = React.createClass({
getInitialState: function() {
return {
textInputValue: ''
};
},
buttonPressed: function() {
let textInputValue = 'new value';
this.setState({ textInputValue });
this.refs.textInputRefer.setNativeProps({
editable: false
});
this.refs.text2.setNativeProps({
style: {
color: 'blue',
fontSize: 30
}
});
},
render: function() {
return (
<View style={styles.container}>
<Text style={styles.buttonStyle} onPress={this.buttonPressed}>
Press me genterly
</Text>
<Text style={styles.textPromptStyle} ref="text2"
文字提示
</Text>
<View>
<TextInput
style={styles.textInputStyle}
ref="textInputRefer"
value={this.state.textInputValue}
onChangeText={(textInputValue)=>this.setState({textInputValue})}
/>
</View>
</View>
);
},
});

measure成员函数

每一个React Native组件都有一个measure成员函数,调用它可以得到组件当前的宽、高与位置信息。使用View组件的onLayout回调函数是获取组件的宽、高与位置信息的好办法,但对某些代码生成的组件,使用组件的measure成员函数是唯一的方法。Ant Design Mobile组件无法使用measure方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
// 在componentDidMount执行完后才可以获取位置信息
componentDidMount: function() {
var aref = this.tempfunc;
window.setTimeout(aref, 1);
},
tempfunc: function() {
this.refs.aTextInputRef.measure( this.getTextInputPosition);
},
getTextInputPosition: function(fx, fy, width, height, px, py) {
console.log('getTextInputPosition方法');
console.log('Component width: ' + width);
console.log('Component height: ' + height);
console.log('X offset to frame: ' + fx); // 这个值无用
console.log('Y offset to frame: ' + fy);
console.log('X offset to page: ' + px);
console.log('Y offset to page: ' + py);
},
...

AsyncStorage

React Native框架不支持调用JavaScript的fs包进行文件读写操作,但提供了AsyncStorage API将“字符串键值对”存储在手机存储空间中。AsyncStorage不提供索引、排序等数据库中经常使用的功能,每个AsyncStorage API提供的方法都会返回一个JavaScript的Promise对象。

使用AsyncStorage的静态函数时,如果参数类型提供错误,try…catch机制将无法捕捉到这个错误。使用Promise机制的rejection状态处理函数中或者利用Promise机制发生错误时的回调函数可以捕捉到错误。但这三种机制中的任何一种都不能阻止手机屏幕的红屏错误。

Promise-Async-基础

发表于 2016-12-26   |   分类于 ES6

Promise对象

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。

Promise的特点:

  • 对象的状态不受外界影响,Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败),任何其他操作都无法改变这个状态。这也是Promise这个名字的由来。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果,再对Promise对象添加回调函数,也会立即得到这个结果。

Promise的缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于Pending状态时,无法得知目前进展到哪一个阶段。

如果某些事件不断地反复发生,一般来说,使用stream模式是比部署Promise更好的选择。

Promise对象是一个构造函数,用来生成Promise实例,它接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。resolve函数的作用是,将Promise对象的状态从Pending变为Resolved,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;reject函数的作用是,将Promise对象的状态从Pending变为Rejected,在异步操作失败时调用,并将异步操作报出的错误作为参数传递出去。Promise实例生成后,可以用then方法分别指定Resolved状态和Rejectded状态的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});

then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行。
resolve函数的参数除了正常的值以外,还可能是另一个Promise实例,形式如下。这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是Pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调函数将会立刻执行。由于p2返回的是另一个Promise,所以后面的then语句都变成针对p1了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var p1 = new Promise(function (resolve, reject) {
// ...
});
var p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
});
p2.then(function(value) {
// success
}, function(error) {
// failure
});

如果异步操作抛出错误,状态就会变为Rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

1
2
3
4
5
6
promise.then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});

跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。
需要注意的是,catch方法返回的还是一个Promise对象,因此后面还可以接着调用then方法。如果没有报错,则会跳过catch方法。

Node 有一个unhandledRejection事件,专门监听未捕获的reject错误。

1
2
3
4
// 第一个参数是错误对象,第二个参数是报错的Promise实例
process.on('unhandledRejection', function (err, p) {
console.error(err.stack)
});

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。p1、p2、p3都是Promise对象的实例,如果不是,就会先调用Promise.resolve方法将参数转为Promise实例。

1
var p = Promise.all([p1, p2, p3]);

p的状态由p1、p2、p3决定:

  • 只有p1、p2、p3的状态都变成resolved,p的状态才会变成resolved,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
  • 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。该方法与Promise.all类似,区别是只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变,那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

1
var p = Promise.race([p1, p2, p3]);

Promise.resolve方法将现有对象转为Promise对象:

  • 如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
  • 如果参数是一个thenable对象(thenable对象指的是具有then方法的对象),那么Promise.resolve将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。
  • 如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为Resolved。
  • 如果不带任何参数,直接返回一个Resolved状态的Promise对象。

如给fetch添加客户端超时,如果5秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

1
2
3
4
5
6
7
8
const p = Promise.race([
fetch(url),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));

注意,立即resolve的Promise对象,是在本轮“事件循环”的结束时执行,setTimeout(fn, 0)在下一轮“事件循环”开始时执行。

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。它的参数用法与Promise.resolve方法完全一致。

Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到。因此,可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

1
2
3
4
5
6
7
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 抛出一个全局错误
setTimeout(() => { throw reason }, 0);
});
};

finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

1
2
3
4
5
6
7
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};

如果不知道或者不想区分函数f是同步函数还是异步操作,但是想用 Promise 来处理它,像下面这样,会有一个缺点

1
Promise.resolve().then(f)

上面的写法有一个缺点,就是如果f是同步函数,那么它会在本轮事件循环的末尾执行。

立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了

让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API,有两种方式:

  • 用async函数,但是async () => f()会吃掉f()抛出的错误,需要使用promise.catch方法。
  • 使用new Promise()。

但是Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下。

异步操作和Async函数

异步操作

异步编程的四种方式:回调函数、事件监听、发布/订阅、Promise。

为什么Node约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是null)?原因是执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了,在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二段。

回调函数在多个回调函数嵌套时,代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理,称为”回调函数地狱”。Promise对象就是为了解决这个问题而提出的,将回调函数的嵌套,改成链式调用。

Promise的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。Promise的最大问题是代码冗余,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。

Generator函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

使用Generator函数,执行一个异步任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
/*
* 执行Generator函数,获取遍历器对象,然后用next方法执行异步任务的第一阶段。
* 由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next方法。
*/
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});

Thunk函数现在可以用于Generator函数的自动流程管理。下面就是一个基于Thunk函数的Generator执行器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function run(fn) {
var gen = fn();
// next函数是Thunk的回调函数
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);

Async函数

从语法上看,async函数就是将Generator函数的星号(*)替换成async,将yield替换成await。async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

async函数对 Generator 函数的改进,体现在以下四点:

  • 内置执行器。Generator函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。
  • 更好的语义。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性。co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值。
  • 返回值是 Promise。async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。

async函数的语法规则总体上比较简单,难点是错误处理机制:

  • async函数返回一个Promise对象。async函数内部return语句返回的值,会成为then方法回调函数的参数。async函数内部抛出错误,会导致返回的Promise对象变为reject状态,抛出的错误对象会被catch方法回调函数接收到。
  • async函数返回的Promise对象,必须等到内部所有await命令的Promise对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
  • 正常情况下,await命令后面是一个Promise对象。如果不是,会被转成一个立即resolve的Promise对象。await命令后面的Promise对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。
  • 只要一个await语句后面的Promise变为reject,那么整个async函数都会中断执行。为了避免这个问题,可以将await放在try…catch结构里面,这样后面的await就会执行;另一种方法是await后面的Promise对象再跟一个catch方法,处理前面可能出现的错误。

async函数的实现:

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
async function fn(args){
// ...
}
// 等同于
function fn(args){
return spawn(function*() {
// ...
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}

async 函数的用法:

1
2
3
4
5
6
7
8
9
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});

Ant Design Mobile 组件学习经验

发表于 2016-12-17   |   分类于 Ant Design

使用过的组件

TabBar、Toast、ActivityIndicator、Button、SegmentedControl、Tabs、WingBlank、Icon

自定义组件样式

参见有情怀的程序猿

TabBar组件

使用TabBar组件需注意:

  • TabBar.Item必须多余1个否则报错
  • TabBar.Item中包裹的组件要有背景色,否则会重合显示

InputItem组件

使用该组件时在Android上无法输入,所以直接使用的React Native的TextInput组件。

SegmentedControl组件

其实是一个标签的效果,通过在SegmentedControl标签内,根据selectedIndex不同包裹不同的子组件,可以实现内容切换的效果。但是这种方式每次点击显示的内容都是重新加载的,不像Tabs有缓存的功能。

Icon组件

详见Icon 如何使用-(RN-版)),在android上这样配置没问题,在ios上还要参考issues-1027。

RN兼容性及Android兼容性

个人感觉 Ant Design Mobile 当前不太稳定,RN兼容性及Android兼容性都有些问题,如果没有特殊要求且RN中存在满足需求的组件,建议还是使用RN的。

关于 antd-mobile 对应的 RN 版本使用说明
组件 RN 版本兼容性测试记录
SwipeAction在安卓上无效

React-Native-基础1

发表于 2016-11-30   |   分类于 React Native

使用过的组件及API

KeyboardAvoidingView、View、ListView、Navigator、RefreshControl、ScrollView、Text、TextInput、TouchableHighlight、WebView、Alert、AppRegistry、AsyncStorage、BackAndroid、Dimensions、Keyboard、StyleSheet、Platform、Picker、Geolocation、InteractionManager、Vibration

初始化工程

react-native init ProjectName 初始化的工程。

react-native init ProjectName –version 0.42.0 初始化指定版本react-native工程。

工程重命名:工程代码中有工程名称,所有不好直接给工程重命名,可能需要重新初始化一个项目,然后将代码复制过去。

如果要修改App显示名称,将/android/app/src/main/res/values/strings.xml 中修改app_name改为想要的名字即可。

项目名称修改

目前只找到了针对Android的项目名称修改的方法,如将项目名称修改为HelloRN,步骤如下:

  • /package.json 中修改name属性值为”HelloRN”
  • /index.android.js 中修改根组件名称为”HelloRN”及AppRegistry.registerComponent(‘HelloRN’, () => HelloRN);
  • 包结构修改为/android/app/src/main/java/com/hellorn
  • /android/app/build.gradle 中修改applicationId为”com.hellorn”
  • /android/app/src/AndroidManifest.xml 中修改package为”com.hellorn”
  • /android/app/src/main/java/com/hellorn/MainActivity.java 中getMainComponentName方法的返回值修改为”HelloRN”
  • /android/app/src/main/res/values/strings.xml 中修改app_name为”HelloRN”

针对IOS项目名称修改的方法还未找到?

android启动图标

  • /android/app/src/main/res/mipmap-hdpi目录下,替换ic_launcher.png图片,72×72
  • /android/app/src/main/res/mipmap-mdpi目录下,替换ic_launcher.png图片,48×48
  • /android/app/src/main/res/mipmap-xhdpi目录下,替换ic_launcher.png图片,96×96
  • /android/app/src/main/res/mipmap-xxhdpi目录下,替换ic_launcher.png图片,144×144

android首屏背景

  • /android/app/src/main/res/drawable-hdpi目录下480×800,添加png图片
  • /android/app/src/main/res/drawable-mdpi目录下320×480,添加png图片
  • /android/app/src/main/res/drawable-xhdpi目录下720×1280,添加png图片
  • /android/app/src/main/res/drawable-xxhdpi目录下1080×1920,添加png图片

/android/app/src/main/res/values/styles.xml

1
2
3
4
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- 这里将刚刚那张图片设置为背景图片, splash对应图片名称 -->
<item name="android:windowBackground">@drawable/splash</item>
</style>

android项目版本号修改

/android/app/目录下修改build.gradle文件:

1
2
3
4
5
6
7
8
9
...
android {
...
defaultConfig {
...
varsionName "1.1" // 打包后apk的版本
}
}
...

启动项目

react-native run-android 在android启动项目,如果运行在真机,需要摇晃手机调出开发者菜单->Dev Settings->Debug server host for device,进行IP和端口的设置。

react-native run-ios 在ios启动项目。

无论是运行上面哪个命令,都会打开一个启动package服务的命令行窗口,如果没有打开这个窗口,请用react-native start手动启动package服务,react-native start –port XXXX 开启服务的时候直接指定端口号。

如何使用 Ant Design Mobile

  • npm install antd-mobile --save
  • npm install babel-plugin-import --save-dev
  • .babelrc中配置如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "presets": [
    "react-native"
    ],
    "plugins": [
    [
    "import",
    {
    "libraryName": "antd-mobile"
    }
    ]
    ]
    }

如何使用图标

Android上直接使用Ant Design Mobile的Icon组件没有效果,个人感觉Icon只支持移动Web。如果想使用图标,目前用过两种方式:

  • 将Iconfinder中图标的Base64编码值定义为变量,在使用地方的uri值指定为该变量。该网站上比较容易找到配套图标,就是点击和未点击配套的图标,但是不能改变图标颜色。
  • 使用FontAwesome,配置方式详见react-native-vector-icons。使用该图标可通过style指定图标样式,包括颜色,但要找配套的图标比较困难。
    注意FontAwesome中描述<i class="fa fa-user" aria-hidden="true"></i>使用图标,这应该是移动Web的使用方式,在React Native中使用如下。
1
2
3
import Icon from 'react-native-vector-icons/FontAwesome';
...
<Icon name="user" size={20} style={styles.icon} />

如何设置样式

StyleSheet提供了一种类似CSS样式表的抽象。const styles = StyleSheet.create()传入样式定义的对象,该对象键为样式名称,值为定义样式的对象。使用时通过styles的键名即可引用到对应的样式。也可以使用内联样式,但比较低效,内联样式对象会在每一个渲染周期都被重新创建。

Stylesheet.Create方法是可选的,但有一些重要的优势。它保证了值是不可变的,并且通过将它们转换成指向内部表的纯数字,保持了代码的不透明性。将它们放在文件的末尾可保证它们在应用中只会被创建一次,而不是每一次渲染周期都被重新创建。

我在RN工程中定义了styles目录,将每个组件引用的样式单独定义成组件同名的文件放在该目录,想要做到样式独立管理,但有些样式可能是根据组件中的一些属性定义的,还是不可避免的要放到组件定义的文件中。

React Native支持部分样式属性,不同的组件有些样式属性也是不能使用的,会报红屏错误。下面是在源码中添加console.log打印出的可用的样式属性。

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
84
85
86
87
88
89
90
Valid style props: [
"alignItems",
"alignSelf",
"backfaceVisibility",
"backgroundColor",
"borderBottomColor",
"borderBottomLeftRadius",
"borderBottomRightRadius",
"borderBottomWidth",
"borderColor",
"borderLeftColor",
"borderLeftWidth",
"borderRadius",
"borderRightColor",
"borderRightWidth",
"borderStyle",
"borderTopColor",
"borderTopLeftRadius",
"borderTopRightRadius",
"borderTopWidth",
"borderWidth",
"bottom",
"color",
"decomposedMatrix",
"elevation",
"flex",
"flexBasis",
"flexDirection",
"flexGrow",
"flexShrink",
"flexWrap",
"fontFamily",
"fontSize",
"fontStyle",
"fontVariant",
"fontWeight",
"height",
"justifyContent",
"left",
"letterSpacing",
"lineHeight",
"margin",
"marginBottom",
"marginHorizontal",
"marginLeft",
"marginRight",
"marginTop",
"marginVertical",
"maxHeight",
"maxWidth",
"minHeight",
"minWidth",
"opacity",
"overflow",
"overlayColor",
"padding",
"paddingBottom",
"paddingHorizontal",
"paddingLeft",
"paddingRight",
"paddingTop",
"paddingVertical",
"position",
"resizeMode",
"right",
"rotation",
"scaleX",
"scaleY",
"shadowColor",
"shadowOffset",
"shadowOpacity",
"shadowRadius",
"textAlign",
"textAlignVertical",
"textDecorationColor",
"textDecorationLine",
"textDecorationStyle",
"textShadowColor",
"textShadowOffset",
"textShadowRadius",
"tintColor",
"top",
"transform",
"transformMatrix",
"translateX",
"translateY",
"width",
"writingDirection",
"zIndex"
]

如果当前元素的父元素没有使用弹性盒模型,也就是没有flex属性,则只给当前元素flex属性是不起作用的。

文字可以用’\n’换行,如<Text>这是内容...{'\n'}这是内容...</Text>。

样式适配问题

React Native中指定大小通常只能使用数字,不能使用百分比,这样就存在在一个设备上设定好的样式,换一台设备可能就变样了。

  • 使用flex布局能解决一部分问题
  • 使用Dimensions获取屏幕宽度、高度,通过计算指定样式
  • onLayout回调函数可得到组件被加载时的放置状态以及监听设备放置状态
  • 使用measure成员函数以得到组件当前的宽、高与位置信息

个人感觉关于样式适配还是存在问题,我们自己指定的样式,可以知道样式值的大小,但如果使用了Ant Design Mobile等其它组件,有些样式值是不知道,也无法通过代码动态获取到,只能查看源码找到相应值,再进行计算,这样可能不把握。

Navigator组件的使用

Navigator是React Native提供的路由管理的组件,通常一个工程中只有一个Navigator组件,个人感觉放在index.android.js文件中比较好,还有对物理返回键BackAndroid的管理最好也放在这里,常用的就是push和pop方法,push时可传入参数。具体使用方法参见React Native中文网-Navigator即可。

  • 使用时好像必须作为render的根组件返回,即使外面包层View,都不会有显示内容
  • 如果在TabBar.Item中返回的Navigator组件,路由切换时页面底部的TabBar一直存在
  • navigator需要作为属性一直向下传递,否则子组件无法引用

ES6语法问题

ES6语法不需要在babelrc文件中配置东西,但有的es6语法用不了,目前发现的是startsWith和new Date()传入日期字符串的构造函数,不报任何错误信息,但是打开浏览器调试时是好使的。网上查原因是打开调试是通过浏览器处理js的,浏览器认识这些语法,但到手机上就不认识了。
这个问题很奇怪,一开始感觉是babel配置不对,但是大多数ES6甚至ES7的语法是没问题的。在网上没有查到react-native具体怎么配置babel,查看过react-native源码babel的配置,改成一样的配置也没解决这个问题。

TextInput触发两次onSubmitEditing问题

在Android手机上,当软键盘的确定/提交按钮被按下的时候调用onSubmitEditing函数。但在做demo时发现该函数会被调用两次,stackoverflow给出一个解决方案如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<TextInput
ref={component => this._textInput = component}
onSubmitEditing={this._onSubmitEditing.bind(this)}
onEndEditing={this._onPressSearch.bind(this)}
returnKeyType='search'
style={styles.inputText}
/>
_onSubmitEditing(){
this._textInput.blur();
}
_onPressSearch(event){
console.log(event.nativeEvent.text);
}

该解决方案以点击软键盘确认执行搜索为例,意思是点击确认时触发失去焦点,当文本输入结束后调用搜索方法(或其它方法),但会存在输入时,如果不点击确认,直接点击页面其它区域,会直接触发搜索方法。

ListView组件的使用

ListView是React Native核心组件,使用时注意以下几点。

  • ListView最好添加enableEmptySections属性,处理ListView没有显示数据时报的黄色警告。
  • 如果发现列表没滑倒底部就调用onEndReached回调方法了,可以试试设置onEndReachedThreshold值。
  • 如果在onEndReached回调方法中进行的是获取更多数据的操作,注意判断条件是否正确,如当前页数已为最大页数或数据已达到总条数,如果还继续执行获取数据操作,有可能导致一直执行获取数据的操作,就像死循环一样。

TouchableHighlight的使用

使用时建议里面包裹React Native提供的组件,而不要使用自定义的组件,否则会报
Touchable child must either be native or forward setNativeProps to a native component.的错误信息。如果非要使用自定义组件,需要给自定义组件添加setNativeProps方法,参见React Native中文网-直接操作。

网络请求

可直接使用React Native已经封装好的fetch。查看了react-native的package.json文件,在依赖中与fetch相关的只有node-fetch和whatwg-fetch,在node-fetch和whatwg-fetch中都添加了打印语句,发现打印的都是来自whatwg-fetch的。但在《React Native跨平台移动应用开发》书中第12章提到,“React Native框架在初始化项目时,安装了node-fetch包,开发者可以使用node-fetch包通过HTTP协议来获取网络册的数据”。

消息提醒

可使用react-native-push-notification组件进行消息提醒。

组件间交互

React Native用起来感觉跟React差不多,目前发现复杂的地方还是组件间的交互问题。如对于如下结构的组件

1
2
3
4
5
6
7
8
A
|
--------+---------
| |
| |
B-------+--------C
|
D

场景描述:A拥有B、C组件,B、C组件都会进入D组件,在D组件中需要调用B、C中的方法。
解决方案:我使用的方法是给A的state添加两个属性methodB、methodC,初始可为null,添加一个可以更新state的方法update,将methodB、methodC、update一起传入B、C中,在B、C组件调用update方法将methodB和methodC赋值给A的同名state属性,这样在D中就可调用到B和C的方法了。

同步异步问题

React Native在使用fetch进行网络请求是异步的,有时我们想要的是同步的效果,或是执行状态的更新setState后的操作,这时可以使用async函数。

场景一
如下代码执行后,输出的state并不是methodA方法中更新后的state。

1
2
3
4
5
6
7
8
9
function methodA() {
...
this.setState({});
...
this.methodB();
}
function methodB() {
consolt.log("更新后的state:", this.state);
}

使用async await后即可输入更新后的state。

1
2
3
4
5
6
7
8
9
async function methodA() {
...
await this.setState({});
...
this.methodB();
}
function methodB() {
consolt.log("更新后的state:", this.state);
}

场景二
AsyncStorage的get操作是一个异步操作,如果将所有AsyncStorage操作放在一个文件中管理,在组件中要获取缓存中的信息以决定下一步操作时,可以使用如下方式。

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
// storageUtils.js中
export let storage = {
save(name,value){
...
},
remove(name){
...
},
async get(name){
let result = null;
await AsyncStorage.getItem(name)
.then((value) => {
if (value) {
result = JSON.parse(value);
}
}).catch((error) => {
console.log('获取失败:', error);
});
return result;
}
}
// MobileUi组件中
class MobileUi extends Component {
constructor(props){
super(props);
this.state = {
token: null,
};
this.getUserInfo = this.getUserInfo.bind(this);
}
async getUserInfo() {
let token = await storage.get('token');
this.setState({
token,
});
}
render() {
let initialRoute = {name: '主页', component: App};
if (this.state.token === null) {
initialRoute = {name: '登录', component: Login};
}
return (
<Navigator
ref="navigator"
initialRoute={initialRoute}
shadowHidden={true}
translucent={true}
configureScene={this.configureScene}
renderScene={this.renderScene}
/>
);
}
}

场景三
根据fetch请求的返回结果决定下一步的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async ifRunning() {
let result = '';
await fetch(`${config.applicationIpAndPort}${config.applicationContext}/OA_UTILS_FLOW_VERIFY.ifRunning.do`, {
method: 'POST',
headers: commonHeaders,
body: JSON.stringify({
PI_ID: this.state.cardData.nodeInstBean.PI_ID,
NI_ID: this.state.cardData.nodeInstBean.NI_ID,
})
}).then(responseData => {
result = responseData.IF_RUNNING;
}).catch((errors) => {
showError(errors, this.props.navigator, this.ifRunning, 'ifRunning请求失败')
});
return result;
}
async agreeHandler(data) {
const ifRunningResult = await this.ifRunning();
if (ifRunningResult === '1') {
console.log("进行审批操作");
} else {
console.log("单据已过期");
}
}

调试问题

在Android上连续两个R键可进行刷新,ctrl+M可打开调试菜单,可以选择Debug JS Remotely启动浏览器的调试工具进行调试,主要是使用console.log打印日志或是debugger进行断点查看。

当你加载了开启Chrome调试工具的React Native应用之后,Google的Chrome浏览器便会通过React Native包管理器使用一个标准的<script>标签来执行相同的JavaScript代码,因此你可以拥有一个基于浏览器的调试器。随后,包管理器使用WebSocket进行设备与浏览器之间的通信。

如果Packager的控制台编译未完成或模拟器报Unexpected token,很有可能是存在语法错误。

如果模拟器报如下错误,不容易定位到哪里发生了错误,打开浏览器调试工具->Network,点击显示红色的请求,Preview里面错误信息。

如果模拟器报如下错误,哪里都不要改,重新执行react-native run-android重新启动,直到启起来。

如果用 ios 模拟器调试时所有操作都变得很慢,检查模拟器 → Debug → Slow Animations 是否为选中了,取消选中即可。

React 调试

直接使用Debug JS Remotely启动浏览器的调试工具是没有React组件调试的。按如下方式可以进行React组件 调试,但React调试和浏览器的调试 http://localhost:8081/debugger-ui 不能同时使用。

1
2
3
4
5
6
7
8
9
10
11
// 安装依赖
npm install --save-dev react-devtools
// 修改package.json中
"scripts": {
"devtools": "react-devtools",
// ...
}
// 启动调试
npm run devtools
// 启动项目
react-native run-android

Redux支持

跟React使用redux是一样的,在用浏览器的调试工具进行redux调试时,需要特殊处理。目前Windows上是安装remote-redux-devtools的0.5.0版本,在react-native为0.34.0时是好使的,但是react-native为0.42.0时就不好使了,感觉应该是目前还不支持0.42.0。

安装依赖

1
2
npm install --save-dev remote-redux-devtools@0.5.0
npm install --save-dev remote-redux-devtools-on-debugger

package.json里添加

1
2
3
"scripts": {
"postinstall": "remotedev-debugger --hostname localhost --port 5678 --injectserver"
}

configureStore.js修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Platform } from 'react-native';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import devTools from 'remote-redux-devtools';
import reducer from '../reducers';
export default function configureStore(initialState) {
const enhancer = compose(
applyMiddleware(thunk),
devTools({
name: Platform.OS,
hostname: 'localhost',
port: 5678
})
);
return createStore(reducer, initialState, enhancer);
}

启动

1
2
npm run postinstall
react-native run-android

Webpack支持

Webpack有一段介绍如下:

Webpack是Web前端打包的工具,不是用在移动端的。如果要在浏览器上运行React Native项目,就需要Webpack了。

运行在Web浏览器上面

参考三步将 React Native 项目运行在 Web 浏览器上面或者Ant Design Mobile的MobileDemo怎么在Web端运行的。

注意:

  • react、react-dom 版本为15.3.x,否则可能会报错
  • webpack.config.js中要有publicPath,如下说是,否则可能会报错
1
2
3
4
5
output:{
publicPath:'',
path: path.join(__dirname, 'output'),
filename: 'bundle.js'
},

已经按照教程测试过,简单的组件可以跑在浏览器上,可以使用浏览器的React调试工具看组件的结构。使用React Native的StyleSheet定义的样式在Web端也生效,可以使用div等html的标签。

但是我现在还不明白React Native 项目运行在Web浏览器上面是为了方便测试,还是为了一套代码可以运行在三端。像我测试中用的React Native的Button组件在浏览器上显示是占满屏幕整个宽度的,还有进行网络请求用的fetch报错了。不是简单的一套代码就能运行在三端,但是个人感觉样式和redux的部分是可以公用的,可能显示组件需要调整。

组件不显示

如果组件没显示出来,还没有报错,可鞥是组件外层没有高度,换成ScrollView或者显示指定height试试。

初始化的项目启动报错

在一个目录下执行react-native init project_name 初始化了一个项目,能跑起来。但换个目录再执行 react-native init project_name 初始化同名项目,即使什么都没修改,也可能跑不起来,会报如下错误。但换个目录可能就能跑起来了,很奇怪。

在网上查了很多都没有找到原因,github上也有人发现类似问题。

这时请确认是否使用了babel-plugin-transform-runtime依赖及使用是否正确,很有可能是这个依赖导致的,如果没用可以去掉。我在去掉之后再没有发现这个问题。

项目启动报错

如果项目启动时报Could not find com.atlassian.mobile.video:okhttp-ws-compat:3.7.0-atlassian1.错误,在android/build.gradle文件中,allprojects中添加如下代码即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
allprojects {
repositories {...}
configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'com.facebook.react' && details.requested.name == 'react-native') {
details.useVersion "0.42.0" // Your real React Native version here
}
}
}
}
}
...

平台扩展名及平台检测

React- Native 特定平台扩展名, 及平台检测

React - Native InteractionManager 动画交互管理器

链接原生

1
2
3
react-native link -- 链接所有原生依赖
react-native link xxx -- 链接某个原生依赖
react-native unlink xxx -- 取消对某个原生依赖的链接

Redux-学习经验

发表于 2016-11-25   |   分类于 React

进行网络请求

发起网络请求的操作通常放在组件的componentDidMount方法中,使用redux进行网络请求就是发起一个action,但这个action比较特殊,它的内部还会发起其它action,形式如下:

  • 需要使用redux需要使用redux-thunk中间件

    1
    2
    3
    4
    5
    6
    import { createStore, applyMiddleware, compose } from 'redux';
    import thunk from 'redux-thunk';
    const store = createStore(reducer, initialState, compose(
    applyMiddleware(..., thunk),
    window.devToolsExtension ? window.devToolsExtension() : f => f // 可使用浏览器的Redux DevTools插件进行redux调试
    ));
  • 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
    28
    29
    30
    function xxxRequest() {
    return {
    type: REQUEST,
    }
    }
    function xxxSuccess(data) {
    return {
    type: SUCCESS,
    data: data,
    }
    }
    function xxxFailure(error) {
    return {
    type: FAILURE,
    error: error,
    }
    }
    export function xxx(url, objParam) {
    return (dispatch, getState) => {
    dispatch(xxxRequest());
    return fetch(url, {
    method: 'post',
    headers: {
    ...
    },
    body: JSON.stringify(objParam)
    }).then(data => dispatch(xxxSuccess(data)))
    .catch(errors => dispatch(xxxFailure(error)));
    };
    }

保证请求按序进行

使用redux-sequence-action中间件

这里说的按序请求,不是指上面的发起一个网络请求时发起的多个action的情况,而是指按序发起多个网络请求、每个网络请求是在上个网络请求完成再执行的情况,比如点击保存先调用save网络请求,再调用refresh网络请求。至于为什么不能直接用dispatch(save),再dispatch(refresh),是因为进行网络请求是异步的,可能在save请求还没完成,就进行了refresh请求并返回了结果,这时的结果是未更新的结果。

  • 安装依赖

    1
    npm install --save redux-sequence-action
  • createStore.js中

    1
    2
    3
    4
    5
    6
    7
    import sequenceAction from 'redux-sequence-action';
    ...
    const store = createStore(reducer, initialState, compose(
    applyMiddleware(invariant(), thunk, sequenceAction),
    window.devToolsExtension ? window.devToolsExtension() : f => f
    ));
    ...
  • containers/下引用的组件中

    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
    ...
    class MyComponent extends Component {
    ...
    componentDidMount() {
    // 发起actionA
    this.props.allActions.actionA('a','b');
    // 发起actionC
    this.props.allActions.actionC('c');
    }
    ...
    }
    function mapStateToProps(state) {
    return state;
    }
    function mapDispatchToProps(dispatch) {
    return {
    allActions: {
    ...bindActionCreators({
    actionA,
    actionB,
    }, dispatch),
    // actionC中先发起actionB,actionB结束后再发起actionC
    actionC: (arg1, arg2, arg3) => dispatch([actionA(arg1, arg2), actionB(arg3)]),
    },
    };
    }
    export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

Redux-基础

发表于 2016-11-19   |   分类于 React

参考
Redux 入门教程(一):基本用法
Redux 入门教程(二):中间件与异步操作
Redux 入门教程(三):React-Redux 的用法

Redux官方文档
前30集
后30集
action的规范

Redux简介

先简单说一下Flux。Flux是一种架构思想,专门解决软件的结构问题,它最大特点是数据的”单向流动”。React本身只涉及UI层,如果搭建大型应用,必须搭配一个前端框架,Facebook官方使用的是 Flux框架。Redux将Flux与函数式编程结合一起,很短时间内就成为了最热门的前端架构。

“如果你不知道是否需要 Redux,那就是不需要它。”
“只有遇到 React 实在解决不了的问题,你才需要 Redux 。”

适用场景

  • 用户的使用方式复杂
  • 不同身份的用户有不同的使用方式(比如普通用户和管理员)
  • 多个用户之间可以协作
  • 与服务器大量交互,或者使用了WebSocket
  • View要从多个来源获取数据

从组件角度看,如果有以下场景,可以考虑使用 Redux

  • 某个组件的状态,需要共享
  • 某个状态需要在任何地方都可以拿到
  • 一个组件需要改变全局状态
  • 一个组件需要改变另一个组件的状态

设计思想

(1)Web 应用是一个状态机,视图与状态是一一对应的。
(2)所有的状态,保存在一个对象里面。

工作流程

  • 用户发出 Action,store.dispatch(action);
  • Store 自动调用 Reducer,let nextState = todoApp(previousState, action);
  • State 一旦有变化,Store 就会调用监听函数,监听函数可以通过store.getState()得到当前状态,可以触发重新渲染View
1
2
3
4
5
6
7
// 设置监听函数
store.subscribe(listener);
...
function listerner() {
let newState = store.getState();
component.setState(newState);
}

基本概念

Store

Store 就是保存数据的地方,可以把它看成一个容器,整个应用只能有一个Store,Redux提供createStore函数用来生成Store。

State

Store对象包含所有数据。如果想得到某个时点的数据,就要对Store生成快照。这种时点的数据集合,就叫做State。当前时刻的State,可以通过store.getState()拿到。Redux 规定, 一个 State 对应一个 View,只要 State 相同,View 就相同,反之亦然。

Action

State的变化必须是View导致的,Action就是View发出的通知,描述当前发生的事情,表示State应该要发生变化了。Action是一个对象,其中的type属性是必须的,表示Action的名称,其他属性可以自由设置。

Action Creator

每个Action都手写会很麻烦,用来生成Action的函数就叫Action Creator。

store.dispatch()

store.dispatch()是View发出Action的唯一方法,通常会结合Action Creator使用。

Reducer

Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer,Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。Reducer函数最重要的特征是,它是一个纯函数,只要是同样的输入,必定得到同样的输出。因此,Reducer 函数里面不能改变State,必须返回一个全新的对象。

实际应用中,Reducer函数不用像上面这样手动调用,store.dispatch方法会触发Reducer 的自动执行。为此,Store需要知道Reducer函数,做法就是在生成Store的时候,将Reducer传入createStore方法,以后每当store.dispatch发送过来一个新的Action,就会自动调用Reducer,得到新的State。createStore方法还可以接受第二个参数,表示State的最初状态,这通常是服务器给出的,如果提供了这个参数,它会覆盖Reducer函数的默认初始值。

为什么这个函数叫做Reducer呢?因为它可以作为数组的reduce方法的参数,Array.prototype.reduce(reducer, ?initialValue),一系列Action对象按照顺序作为一个数组,对每个Action调用reducer。请看下面的例子。

1
2
3
4
5
6
const actions = [
{ type: 'ADD', payload: 0 },
{ type: 'ADD', payload: 1 },
{ type: 'ADD', payload: 2 }
];
const total = actions.reduce(reducer, 0); // 3

Reducer拆分

Redux提供了一个combineReducers方法,用于Reducer的拆分,只要定义各个子Reducer函数,然后将它们合成一个大的Reducer。这种拆分与React应用的结构相吻合:一个React根组件由很多子组件构成,子组件与子Reducer完全可以对应。

1
2
3
4
5
6
7
8
9
10
11
12
// State的属性名必须与子Reducer同名
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
});
// 如果不同名,就要采用下面的写法
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
});

store.subscribe()

Store 允许使用store.subscribe方法设置监听函数,一旦State发生变化,就自动执行这个函数。因此,只要把View的更新函数(对于React项目,就是组件的render方法或setState方法)放入listen,就会实现View的自动渲染。store.subscribe方法返回一个函数,调用这个函数就可以解除监听。

中间件

中间件就是一个函数,对store.dispatch方法进行了改造,在发出Action和执行Reducer这两步之间,添加了其他功能。

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

使用中间件

1
2
3
4
5
6
7
8
9
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
initial_state, // 整个应用的初始状态,没有可省略该参数
applyMiddleware(logger) // 中间件的次序有讲究,logger一定要放在最后
);

异步操作的思路

  • 操作开始时,送出一个Action,触发State更新为”正在操作”状态,View重新渲染
  • 操作结束后,再送出一个Action,触发State更新为”操作结束”状态,View 再一次重新渲染

方式一:redux-thunk

异步操作至少要送出两个Action,如何才能在操作结束时,系统自动送出第二个 Action 呢?奥妙就在 Action Creator 之中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 处理异步的 Action Creator
const fetchPosts = postTitle => (dispatch, getState) => {
// 先发出一个Action表示操作开始
dispatch(requestPosts(postTitle));
// 返回了一个函数
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
// 再发出一个 Action表示操作结束
.then(json => dispatch(receivePosts(postTitle, json)));
};
};
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);

这样的处理,就解决了自动发送第二个Action的问题。但是,又带来了一个新的问题,Action是由store.dispatch方法发送的,而store.dispatch方法正常情况下,参数只能是对象,不能是函数。这时,就要使用中间件redux-thunk,改造store.dispatch,使得后者可以接受函数作为参数。

redux-thunk 的实现非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

方式二:redux-promise

另一种异步操作的解决方案,就是让Action Creator返回一个Promise对象。这就需要使用redux-promise中间件,这个中间件使得store.dispatch方法可以接受Promise对象作为参数。

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
// 写法一,返回值是一个 Promise 对象
const fetchPosts =
(dispatch, postTitle) => new Promise(function (resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
// 写法二,Action 对象的payload属性是一个 Promise 对象
import { createAction } from 'redux-actions';
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
// 发出同步 Action
dispatch(requestPosts(selectedPost));
// 发出异步 Action
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
}
}

React-Redux的用法

React-Redux将所有组件分成两大类:UI组件(presentational component)和容器组件(container component)。

UI 组件有以下几个特征:

只负责 UI 的呈现,不带有任何业务逻辑
没有状态(即不使用this.state这个变量)
所有数据都由参数(this.props)提供
不使用任何 Redux 的 API

容器组件有以下几个特征:

负责管理数据和业务逻辑,不负责 UI 的呈现
带有内部状态
使用 Redux 的 API

UI组件负责UI的呈现,容器组件负责管理数据和逻辑。如果一个组件既有UI又有业务逻辑,将它拆分成外面是一个容器组件,里面包了一个UI组件,前者负责与外部的通信,将数据传给后者,由后者渲染出视图。React-Redux规定,所有的UI组件都由用户提供,容器组件则是由 React-Redux 自动生成。

connect

connect方法,用于从UI组件生成容器组件,connect的意思,就是将这两种组件连起来。

1
2
import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);

TodoList是UI组件,VisibleTodoList就是由React-Redux通过connect方法自动生成的容器组件。但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是UI组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息:

(1)输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数。
(2)输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。

mapStateToProps

mapStateToProps是一个函数,它建立一个从外部的state对象到UI组件的props对象的映射关系。mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发UI组件重新渲染。

mapDispatchToProps

mapDispatchToProps用来建立UI组件的参数到store.dispatch方法的映射,它定义了哪些用户的操作应该当作Action传给Store。它可以是一个函数,也可以是一个对象。

如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的props对象)两个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// mapDispatchToProps是函数,返回一个对象,该对象的每个键值对都是一个映射,定义了UI组件的参数怎样发出Action,如果有多个Action,可以使用Redux的bindActionCreators
const mapDispatchToProps = ( dispatch, ownProps ) => {
return {
onClick: () => dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
}),
onChange: bindActionCreators({// 里面是Action creator
addTodos,
deleteTodos
}, dispatch)
};
}
// mapDispatchToProps是对象,它的每个键名也是对应UI组件的同名参数,键值应该是一个函数,会被当作Action creator,返回的Action会由Redux自动发出
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}

Provider

Provider组件,可以让容器组件拿到state。Provider在根组件App外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。它的原理是React组件的context属性,store放在了上下文对象context上面,子组件就可以从context拿到store了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class VisibleTodoList extends Component {
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();
// ...
}
}
VisibleTodoList.contextTypes = {
store: React.PropTypes.object
}

API的简单实现

createStore的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);// 什么意思
}
};
dispatch({}); // 干嘛呢
return { getState, dispatch, subscribe };
};

combineReducer的简单实现

1
2
3
4
5
6
7
8
9
10
11
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
},
{}
);
};
};

applyMiddleware的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import compose from './compose';
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {...store, dispatch}
}
}

redux-promise的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action)
? action.then(dispatch)
: next(action);
}
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
};
}

Provider的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
store: React.PropTypes.object
}

jQuery-基础

发表于 2016-11-14   |   分类于 JavaScript

parent([expr]) – 获得当前匹配元素集合中每个元素的父元素,使用选择器进行筛选是可选的,沿 DOM 树向上遍历单一层级
parents([expr]) – 获得当前匹配元素集合中每个元素的祖先元素,一直向上查找直到根元素,再使用选择器进行筛选可选的
closest([expr]) – 首先检查当前元素是否匹配,如果匹配则直接返回元素本身,如果不匹配则向上查找父元素,一层一层往上,直到找到匹配选择器的元素就停止了,如果什么都没找到则返回一个空的jQuery对象

Webpack-基础

发表于 2016-11-06   |   分类于 Webpack

前端模块管理器简介

浏览器本身并不提供模块管理的机制,为了调用各个模块,有时不得不在网页中,加入一大堆script标签。这样就使得网页体积臃肿,难以维护,还产生大量的HTTP请求,拖慢显示速度,影响用户体验。
为了解决这个问题,前端的模块管理器(package management)应运而生。它可以轻松管理各种JavaScript脚本的依赖关系,自动加载各个模块,使得网页结构清晰合理。不夸张地说,将来所有的前端JavaScript项目,应该都会采用这种方式开发。

Webpack介绍

官网
Webpack 中文指南
Webpack 介绍
webpack学习笔记
快速上手
一小时包教会
webpack-howto
Diving into Webpack Webpack and React is awesome
React Webpack cookbook
webpack-demos

Webpack是近期最火的一款模块加载器兼模块打包工具,主要是用来打包在浏览器端使用的javascript,同时也能转换、捆绑、打包其它的静态资源,例如JS(含JSX)、coffee、样式(含less/sass)、图片等都作为模块来使用和处理,输出的静态文件只剩下js与png,而css、less、jade其他的文件都合并到了js中。
Webpack主要解决如何在一个大规模的代码库中,维护各种模块资源的分割和存放,维护它们之间的依赖关系,并且无缝的将它们整合到一起生成适合浏览器端请求加载的静态资源。
Webpack中涉及路径配置最好使用绝对路径,建议通过path.resolve(__dirname, "app/folder")或path.join(__dirname, "app", "folder")的方式来配置,以兼容 Windows 环境。

特点:

  • 代码拆分、Loader、智能解析、插件系统、快速运行。

优点:

  • webpack 是以 commonJS 的形式来书写脚本的,但对 AMD/CMD 的支持也很全面
  • 直接使用 require(XXX) 的形式来引入各模块,即使它们可能需要经过编译
  • 能被模块化的不仅仅是 JS 了
  • 开发便捷,比如打包、压缩混淆、图片转base64等
  • 扩展性强,插件机制完善,特别是支持 React 热插拔(见 react-hot-loader )
  • 可以使用别名(alias),简化require中的内容
  • 支持多入口

Webpack安装

全局安装:npm install webpack -g
当然如果常规项目还是把依赖写入 package.json 包去更人性化:
npm init
npm install webpack --save-dev

配置文件

示例代码如下

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
var webpack = require('webpack');
// 用于提取多个入口文件的公共脚本部分,然后生成一个 common.js 来方便多页面之间进行复用。
// 注意html中引入时先引入common.js,再引入index.s
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');
// 定义全局变量
var data = 'hello world';
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?modules' },// 样式开启局部作用域,如果要定义全局作用域 :global(选择器) {样式}
{ test: /\.js$/, loader: 'jsx-loader?harmony' },
{ test: /\.scss$/, loader: 'style!css!sass?sourceMap'},
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'},
{ test: /\.js[x]?$/, exclude: /node_modules/, loader: 'babel-loader?presets[]=es2015&presets[]=react'},
{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel', query: { presets: ['es2015', 'react'] } }
]
},
//其它解决方案配置
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'
}
},
externals: {
// 定义全局变量,index.html中需引入data.js,即可直接使用require('data')
'data': 'data'
}
};

  • context:处理entry选项时的基础路径(绝对路径),默认值为process.cmd(),即webpack.config.js文件所在路径。
  • entry:配置入口文件,值可以是字符串、数组、对象;字符串指定的模块在项目程序启动的时候加载;字符串数组指定的所有模块被当做一个模块集,在项目程序启动的时候都会加载,数组最后一个元素作为模块集输出;对象的每个属性名作为模块集的名称,属性值可以是字符串和字符串数组。这个配置可用于实现非单页的程序,程序会有多个启动入口。
  • output.filename:打包后的文件名 ‘[name].js’,表示name为对应entry的键。
  • output.path:打包文件存放的绝对路径。
  • output.publicPath:网站运行时的访问路径,比如publicPath为”/test”,访问localhost:8080/test/bundle.js可访问编译后的文件。
  • module.loaders:值为一个数组,数组的每一个元素是一个对象,对象里可有text、exclude、loader以“!”分隔多个loader的string、loaders是一个字符串数组;加载器配置,以处理非javascript类型的模块,都需要先安装。
  • module.noParse:值为一个数组,声明这个模块不需要parse查找依赖,忽略对已知文件的解析。告诉当webpack尝试去解析压缩文件时,这种行为是不允许的。
  • plugins:值为一个数组,new webpack.optimize.CommonsChunkPlugin('common.js')表示提取多个入口文件的公共脚本部分生成一个 common.js。注意引入时先引入common.js,再引入index.s。如压缩打包的文件(UglifyJsPlugin)、允许错误不打断程序等。
  • resolve.root:绝对路径,查找module的话从这里开始查找。添加默认搜索路径。
  • resolve.extensions:自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名。
  • resolve.alias:模块别名定义,方便后续直接引用别名,无须多写长长的地址;告诉webpack,当引入react时,试图去匹配压缩过的react。
  • externals:当我们想在项目中require一些其它的类库或者API,而又不想让这些类库的源码被构建到运行时文件中,这在实际开发中很有必要。externals: {"jquery": "jQuery"},这样我们就可以放心的在项目中使用这些API了:var jQuery = require("jquery")。如果你想将react分离,不打包到一起,可以使用externals。然后用<script>单独将react引入。
  • devtool:就是生成sourcemap的不同方式,eval不支持IE8,推荐用source-map,开发环境常用eval-source-map。
  • debug:加载器(loader)转换到调试模式。
  • catch:缓存生成的模块,watch 模式下默认就是启动的。

常用Loader
常用plugin

常用命令:

1
2
3
4
5
6
webpack // 执行webpack.config.js文件
webpack --display-error-details //方便出错时能查阅更详尽的信息
webpack --config XXX.js //使用另一份配置文件(比如webpack.config2.js)来打包
webpack --watch //监听变动并自动打包
webpack -p //压缩混淆脚本,这个非常非常重要!
webpack -d //生成map映射文件,告知哪些模块被最终打包到哪里了

webpack-dev-server

命令:webpack-dev-server --devtool eval --progress --colors --hot --content-base dist
webpack-dev-server //启动一个小的express Web服务,8080端口
–devtool eval //为你的代码创建源地址。当有任何报错的时候可以让你更加精确地定位到文件和行号
–progress // 显示合并代码进度
–colors - Yay //命令行中显示颜色!
–content-base build // 指向设置的输出目录,这样不能访问其它路径的文件了
–history-api-fallback // 这样在浏览器中直接输入url才不会报Cannot GET
–inline // 自动刷新页面
–hot // 启动热替换

当运行 webpack-dev-server 的时候,它会监听你的文件修改。当项目重新合并之后,会通知浏览器刷新。为了能够触发这样的行为,你需要把你的 index.html 放到 build/ 文件夹下。可访问http://localhost:8080/webpack-dev-server/bundle或
http://localhost:8080/webpack-dev-server/index.html。注意,访问带webpack-dev-server的url会自动刷新,直接访问localhost:8080的可能不刷新。

配置webpack-dev-server代理

自动编译并刷新页面

index.html 放到 build/ 文件夹下
Index.html中添加 <script src="http://localhost:8080/webpack-dev-server.js"></script>

1
2
3
4
5
entry: [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
path.resolve(__dirname, 'app/main.js')
],

命令:webpack-dev-server –hot
当Webpack-dev-server在浏览器自动刷新下运行的时候,CSS也会自动更新,不过有点不同的是,当你改变了一个 CSS 文件,属于那个文件的标签会更新新的内容但不会刷新。
注意,访问带webpack-dev-server的url会自动刷新,直接访问localhost:8080的可能不刷新。

独立打包样式文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ExtractTextPlugin = require("extract-text-webpack-plugin");
// webpack.config.js 中
...
plugins: [
...
new ExtractTextPlugin("[name].css"), // 如果只有一个 style.css 就行
],
loaders:[
...
{
test: /\.less$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!less-loader',{ publicPath: './'})
},
],

CSS压缩、去重

1
2
3
4
5
6
7
8
9
10
11
12
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
// webpack.config.js 中
...
plugins: [
...
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorOptions: { discardComments: {removeAll: true } },
canPrint: true
}),
],

分离应用和第三方

当你的应用依赖其他库尤其是像 ReactJS 这种大型库的时候,你需要考虑把这些依赖分离出去,这样就能够让用户在你更新应用之后不需要再次下载第三方文件。记住要把这些文件都加入到你的 HTML 代码中,不然你会得到一个错误。

1
2
3
4
5
6
7
8
9
10
entry: {
app: path.resolve(__dirname, 'app/main.js'),
// 当 React 作为一个 node 模块安装的时候,
// 我们可以直接指向它,就比如 require('react')
vendors: ['react', 'react-dom', 'jquery']
},
plugins: [
...
new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.js')
]

代码压缩

webpack打包后的文件可能会很大,生成环境应该去掉注释及依赖的copyright信息。

1
2
3
4
5
6
7
8
9
10
11
plugins: [
...
new webpack.optimize.UglifyJsPlugin({
output: {
comments: false,
},
compress: {
warnings: false,
}
}),
]

多重入口

你的应用可能有多个路径, 就是应用中有两个或者多个 URL 相应不同的页面,这里就是提供这样的解决方案。可能你有一个普通用户页和一个管理员页,他们共享了很多代码,但是不想在普通用户页中加载所有管理员页的代码,所以好方案是使用多重入口。

生成html、打开浏览器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var HtmlwebpackPlugin = require('html-webpack-plugin');
var OpenBrowserPlugin = require('open-browser-webpack-plugin');
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new HtmlwebpackPlugin({
title: 'Webpack-demos',
filename: 'index.html'
}),
new OpenBrowserPlugin({
url: 'http://localhost:8080'
})
]
};

在开发环境使用的代码

main.js

1
2
3
4
5
document.write('<h1>Hello World</h1>');
if (__DEV__) {
document.write(new Date());
}

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
var webpack = require('webpack');
var devFlagPlugin = new webpack.DefinePlugin({
__DEV__: JSON.stringify(JSON.parse(process.env.DEBUG || 'false'))
});
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
},
plugins: [devFlagPlugin]
};
1
2
3
4
5
6
# Linux & Mac
$ env DEBUG=true webpack-dev-server
# Windows
$ set DEBUG=true
$ webpack-dev-server

代码分割

方式一

使用require.ensure定义一个分割点,require.ensure会告诉webpack被引入的文件要编译成单独的文件,与编译输出文件bundle.js分开,index.html和weback.config.js都不需要修改。从表面上也看不出有什么不同,实际上webpack把main.js和a.js编译成不同的块bundle.js和1.bundle.js,并在bundle.js需要1.bundle.js时才加载。

1
2
// a.js
module.exports = 'Hello World';
1
2
3
4
5
6
7
// main.js
require.ensure(['./a'], function(require) {
var content = require('./a');
document.open();
document.write('<h1>' + content + '</h1>');
document.close();
});
1
2
3
4
5
6
// index.html
<html>
<body>
<script src="bundle.js"></script>
</body>
</html>
1
2
3
4
5
6
7
// webpack.config.js
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
}
};

如果要配合react-router使用,示例如下:

1
2
3
4
5
6
7
8
const app = (location, callback) => {
require.ensure([], require => {
callback(null, require('./containers/App').default)
}, 'app')
};
...
<Route getComponent={app} />
...

方式二

使用bundle-loader

1
2
3
4
5
6
7
8
9
// main.js
var load = require('bundle-loader!./a.js');
// You need to async wait until a.js is available (and get the exports)
load(function(file) {
document.open();
document.write('<h1>' + file + '</h1>');
document.close();
});

提取供应商模块

可以使用CommonsChunkPlugin提取供应商库到单独的文件中。

1
2
3
// main.js
var $ = require('jquery');
$('h1').text('Hello World');
1
2
3
4
5
6
7
8
// index.html
<html>
<body>
<h1></h1>
<script src="vendor.js"></script>
<script src="bundle.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js
var webpack = require('webpack');
module.exports = {
entry: {
app: './main.js',
vendor: ['jquery'],
},
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin(/* chunkName= */'vendor', /* filename= */'vendor.js')
]
};

如果想在其它文件使用一个模块又不想使用require引入,如使用$和jQuery但是不用require('jquery'),就需要使用ProvidePlugin。

1
2
// main.js
$('h1').text('Hello World');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// webpack.config.js
var webpack = require('webpack');
module.exports = {
entry: {
app: './main.js'
},
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery"
})
]
};

热模块替换

方式一

启动命令添加参数
webpack-dev-server --hot --inline

方式二

修改webpack.config.js文件,添加HotModuleReplacementPlugin插件,修改entry

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
var webpack = require('webpack');
var path = require('path');
module.exports = {
entry: [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
'./index.js'
],
output: {
filename: 'bundle.js',
publicPath: '/static/'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
module: {
loaders: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
},
include: path.join(__dirname, '.')
}]
}
};

Git-基础

发表于 2016-11-01   |   分类于 Git

常用命令

参考 Git远程操作详解 和 Git教程

which -a git # 查看所有版本的 git
git –version # 查看使用 git 的版本
git xxx –help # 查看 xxx 命令相关的帮助文档
git config –global alias.别名 命令 # 给命令起别名
git log –oneline –decorate –graph –all # 查看提交记录

拉取和提交操作

git clone url [name] – 从远程主机克隆一个版本库
git clone -b url – 拉取指定分支
git pull – 拉取远程仓库,默认为远程origin master分支
git pull origin [remoteBranchName] [localBranchName] – 拉取远程仓库remoteName分支到本地localBranchName分支
git fetch origin remoteBranchName – 从远程分支更新最新的版本到origin/remoteBranchName分支上,没有更新到工作区,默认情况下取回所有分支的更新
git merge origin/localBranchName – 合并内容到本地localBranchName分支,更新到工作区
git diff master origin/master – 比较本地的master分支和origin/master分支的差别
git status – 列出当前目录所有还没有被git管理的文件和被git管理且被修改但还未提交( commit)的文件
git add fillname – 把文件添加到仓库,新添加的文件必须add后提交才生效
git rm fillname – 把文件从仓库删除,被删除的文件必须rm后提交才生效
git commit -m “description” – 把文件提交到本地仓库
git push origin [name] – 把所有本地提交推送到远程库name分支,默认为master分支

临时备份

git stash – 备份当前的工作区的内容,从最近的一次提交中读取相关内容,让工作区保证和上次提交的内容一致,同时将当前的工作区内容保存到Git栈中
git stash pop stash@{id} – 从Git栈中读取最近一次保存的内容,恢复工作区的相关内容,由于可能存在多个Stash的内容,所以用栈来管理,pop会从最近的一个stash中读取内容并恢复,将对应的stash id 从stash list里删除
git stash apply stash@{id} – 与stash pop的区别是会继续保存stash id
git stash list – 显示Git栈内的所有备份,可以利用这个列表来决定从那个地方恢复
git stash clear – 清空Git栈
git stash show stash@{0} – 查看压入Git栈的版本为0的文件列表
git stash drop – 如果不加stash编号,默认的就是删除最新的,也就是编号为0的那个,加编号就是删除指定编号的stash

忽略文件不提交

在仓库根目录下创建名称为“.gitignore”的文件,写入不需要的文件夹名或文件,每个元素占一行即可,如:

1
2
*.log
node_modules/

恢复操作

git reset – 将当前的分支重设(reset)到指定的commit或者HEAD(默认,即最新的一次提交),可有–hard 或 –soft 参数
git revert filename – 恢复指定文件
git rebase localBranchName – 用于把localBranchName分支的修改合并到当前分支,该命令会把当前分支里的每个提交(commit)取消掉,然后把当前分支更新为最新的localBranchName分支

分支操作

git branch – 查看本地分支
git branch -r – 查看远程分支
git branch [name] – 创建本地分支
git checkout [name] – 切换分支
git checkout -b [name] [commit 引用] – 基于指定的 commit 新建并切换到该分支
git branch -d [name] – 删除分支,-d选项只能删除已经参与了合并的分支,对于未有合并的分支是无法删除的,如果想强制删除一个分支,可以使用-D选项
git merge [name] – 将指定分支合并到当前分支
git push origin [name] – 本地分支push到远程同名分支,远程没有该分支则创建
git push origin :heads/[name] – 删除远程分支
git push origin :master – 删除远程分支
git push origin –delete master – 删除远程分支
git push origin [remoteBranchName] [localBranchName] – 提交本地local分支作为远程的remote分支,如果没有则创建
git push –force origin – 如果远程主机的版本比本地版本更新,可以使用–force选项,结果导致远程主机上更新的版本被覆盖
git branch –set-upstream dev origin/dev – 指定本地dev分支与远程origin/dev分支的链接

版本操作

git tag – 查看版本
git tag [localTagName] – 创建版本
git push origin –tags – 把本地tag推送到远程
git fetch origin tag – 获取远程tag
git tag -d [localTagName] – 删除本地tag
git push origin :refs/tags/[remoteTagName] – 删除远程tag
git checkout tagName – 切换到指定版本

fork的项目与原项目同步

fork一个项目并clone到本地,如果原项目已经更新了,要保持github上fork的项目和本地的项目与原项目同步,只需执行以下操作。

1
2
3
4
5
6
7
8
# 远程拉取更新项目,先将原包作为 remote 加到本地项目中,A_REPOSITORY_URL需替换
git remote add upstream A_REPOSITORY_URL
# 从上游仓库获取到分支
git fetch upstream
# 合并到本地当前分支
git merge upstream/master
# 提交到自己的 fork
git push origin master

初始化项目

1
2
3
4
5
git init
git add .
git commit -m "first commit"
git remote add origin https://github.com/zhulichao/react-native-hello.git
git push -u origin master
1…14151617
© 2021 小朱
由 Hexo 强力驱动
主题 - NexT.Pisces