三个点怎样改变了 JavaScript

当访问函数调用参数值的时候,我总是对 arguments 对象感觉不舒服。它硬编码的名字使得在一个内部函数中访问外部函数的 arguments 变得困难。
更糟糕的是 JS 中它作为类数组对象,但是不能使用数组的方法像 .map().forEach()

在封闭函数中访问 arguments ,你不得不把它存储在一个临时变量中。遍历类数组对象,你不得不用鸭子类型间接调用。来看下面的例子:

1
2
3
4
5
6
7
8
9
10
function outerFunction() {  
// store arguments into a separated variable
var argsOuter = arguments;
function innerFunction() {
// args is an array-like object
var even = Array.prototype.map.call(argsOuter, function(item) {
// do something with argsOuter
});
}
}

另外一种情形是函数调用接收动态数量的参数。用一个数组作为参数是非常令人讨厌的。

.push(item1, ..., itemN) 将元素一个个的插入到一个数组为例:
你必须枚举每个作为参数的元素。这样做有时候是不方便:经常整个数组的元素需要被添加。
在 ES5 中的解决方法是 .apply() :一个不友好而且繁琐的方法。让我们看看:

1
2
3
4
var fruits = ['banana'];  
var moreFruits = ['apple', 'orange'];
Array.prototype.push.apply(fruits, moreFruits);
console.log(fruits); // ['banana', 'apple', 'orange']

幸运的是 JS 世界正在变化。三点操作符 ... 能解决很多这种情形的问题。ES6 引入了这个操作符,对 JS 是一个显著的改善。

这篇文章将通过 ... 操作符的使用场景展示怎么解决类似问题。

Three dots

可变操作符被用来获得通过函数调用的变量列表。

1
2
3
4
5
function countArguments(...args) {  
return args.length;
}
// get the number of arguments
countArguments('welcome', 'to', 'Earth'); // => 3

展开操作符被用于数组的解构,以及用数组填充函数调用参数。

1
2
3
4
5
6
7
8
9
10
11
let cold = ['autumn', 'winter'];  
let warm = ['spring', 'summer'];
// construct an array
[...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer']
// destruct an array
let otherSeasons, autumn;
[autumn, ...otherSeasons] = cold;
otherSeasons // => ['winter']
// function arguments from an array
cold.push(...warm);
cold // => ['autumn', 'winter', 'spring', 'summer']

改良的参数存取

可变参数

在简介中提到的,在复杂场景中函数体内处理 arguments 对象变得麻烦。

例如 JS 内部函数 filterNumbers() 想访问外部函数 sumOnlyNumbers()arguments

1
2
3
4
5
6
7
8
9
10
11
function sumOnlyNumbers() {  
var args = arguments;
var numbers = filterNumbers();
return numbers.reduce((sum, element) => sum + element);
function filterNumbers() {
return Array.prototype.filter.call(args,
element => typeof element === 'number'
);
}
}
sumOnlyNumbers(1, 'Hello', 5, false); // => 6

为了让 filterNumbers() 访问 sumOnlyNumbers()arguments,你必须创建一个临时变量 args 。这是因为 filterNumbers() 定义自己的 arguments 对象会重写外部的 arguments 对象。
这种方法可行,但是太繁琐了。

可变操作符优雅的解决了这个问题。它允许在函数声明中定义一个可变参数 ...args

1
2
3
4
5
6
7
8
function sumOnlyNumbers(...args) {  
var numbers = filterNumbers();
return numbers.reduce((sum, element) => sum + element);
function filterNumbers() {
return args.filter(element => typeof element === 'number');
}
}
sumOnlyNumbers(1, 'Hello', 5, false); // => 6

函数声明 function sumOnlyNumbers(...args) 表示 args 接收调用参数放在一个数组中。名字冲突解决了, args 就能够被内部的 filterNumbers() 使用了。
并且忘记类数组对象:args是一个数组,非常棒的结果。 filterNumbers() 也能摆脱 Array.prototype.filter.call() 了,筛选方法直接调用 args.filter()

注意可变参数应该是函数的最后一个参数。

有选择的可变参数

如果不是所有的参数都包含在可变参数内,你可以一开始用逗号分隔的参数定义他们。明确的定义参数不包含在可变参数内。
让我们看个例子:

1
2
3
4
5
function filter(type, ...items) {  
return items.filter(item => typeof item === type);
}
filter('boolean', true, 0, false); // => [true, false]
filter('number', false, 4, 'Welcome', 7); // => [4, 7]

arguments 对象没有可选的特性并且总是包含所有的值。

箭头函数

箭头函数不能在函数体内定义 arguments 对象,但是能访问封闭作用域的 arguments 对象。如果你想要获取所有的参数,可以用可变参数。让我们在这个例子中尝试:

1
2
3
4
5
6
7
8
(function() {
let outerArguments = arguments;
const concat = (...items) => {
console.log(arguments === outerArguments); // => true
return items.reduce((result, item) => result + item, '');
};
concat(1, 5, 'nine'); // => '15nine'
})();

items 可变参数在一个数组中包含了函数调用的所有参数。并且 arguments 对象是从封闭作用域中取得与 outerArguments 变量相等,所以它没有实际意义。

改良的函数调用

在文章简介中,第二个问题提出了有没有更好的方式用一个数组填充调用参数。
ES5 在函数对象上提供了 .apply() 方法解决这个问题。不幸的是,这个方法有三个问题:

  • 必须手动指定函数调用的上下文
  • 不能用做构造函数调用
  • 最好有一个更短的解决方案

让我们来看 .apply() 的用例:

1
2
3
let countries = ['Moldova', 'Ukraine'];  
countries.push.apply(countries, ['USA', 'Japan']);
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']

上面例子中, .apply() 中表示的第二次上下文 countries 看起来是不相干的。在对象上 countries.push 决定方法调用已经足够了。
整个调用看起来很繁琐。

展开运算符用数组中的值填充函数调用的参数。让我们用展开运算符改良上面的例子:

1
2
3
let countries = ['Moldova', 'Ukraine'];  
countries.push(...['USA', 'Japan']);
console.log(countries); // => ['Moldova', 'Ukraine', 'USA', 'Japan']

正如所见,展开运算符是更清晰简单的解决方案。只附加了三点(...)字符。

展开运算符能让一个数组填充构造函数的调用参数,用 .apply() 的时候这事不可能的。让我们看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
class King {  
constructor(name, country) {
this.name = name;
this.country = country;
}
getDescription() {
return `${this.name} leads ${this.country}`;
}
}
var details = ['Alexander the Great', 'Greece'];
var Alexander = new King(...details);
Alexander.getDescription(); // => 'Alexander the Great leads Greece'

此外你也可以在同一个调用中联合多个展开操作符和普通参数。下面的例子移除数组中已经存在的元素,然后添加另一个数组和一个元素:

1
2
3
4
5
var numbers = [1, 2];  
var evenNumbers = [4, 8];
const zero = 0;
numbers.splice(0, 2, ...evenNumbers, zero);
console.log(numbers); // => [4, 8, 0]

改良的数组操作

数组结构

数组字面量 [item1, item2, ...] 除了枚举不能提供的功能。

展开运算符通过允许插入其他数组(或任何可迭代的类型)到初始化实例中改良数组字面量。
这个改良使下面描述的任务更容易实现。

创建一个初始化元素来自其他数组的数组:

1
2
3
4
5
var initial = [0, 1];  
var numbers1 = [...initial, 5, 7];
console.log(numbers1); // [0, 1, 5, 7]
let numbers2 = [4, 8, ...initial];
console.log(numbers2); // => [4, 8, 0, 1]

number1number2 数组用数组字面量创建,同时 initial 中的项被用来初始化。

连结两个或更多的数组:

1
2
3
4
var odds = [1, 5, 7];  
var evens = [4, 6, 8];
var all = [...odds, ...evens];
console.log(all); // [1, 5, 7, 4, 6, 8]

all 数组通过连结 oddsevens 数组创建。

克隆数组实例:

1
2
3
4
var words = ['Hi', 'Hello', 'Good day'];  
var otherWords = [...words];
console.log(otherWords); // => ['Hi', 'Hello', 'Good day']
console.log(otherWords === words); // => false

otherWordswords 数组的一个克隆版本。注意克隆只发生在数组本身,不是内部元素(即不是深度克隆)。

数组解构

在 ES6 中可以使用的解构赋值是很强大的从数组对象中提取数据的表达式。

作为解构的一部分,展开操作符提取数组的一部分。提取结果总是一个数组。

按照语法,展开操作符应该是解构赋值的最后一个:[extractItem1, ...extractedArray] = destructedArray

让我们看一些应用:

1
2
3
4
5
var seasons = ['winter', 'spring', 'summer', 'autumn'];  
var coldSeason, otherSeasons;
[coldSeason, ...otherSeasons] = seasons;
console.log(coldSeason); // => 'winter'
console.log(otherSeasons); // => ['spring', 'summer', 'autumn']

[coldSeason, ...otherSeasons] 提取了第一项 wintercoldSeason 变量,余下的元素放在 otherSeason 数组。

展开操作符和迭代协议

展开操作符用迭代协议在元素上操作并收集结果。这使得展开操作符非常有用,因为任意的对象能定义操作符怎么提取数据。

一个遵守迭代协议的对象是可迭代的。
迭代协议要求对象包含一个特殊属性。这个属性名叫 Symbol.iterator ,它的值是一个返回迭代对象的函数。这个迭代对象应该遵守迭代协议。它需要提供一个 next 属性,属性的值是一个函数,函数的返回值是包含 done (表示迭代结束的布尔值)和 value (迭代结果)属性的对象。
看起来通过文字描述理解了迭代协议,然后后面的代码相当简单。

操作符从对象或原生类型取出数据必须被有序迭代。

很多原生类型和对象能被迭代:字符串、数组、类型数组、sets 和 maps。所以他们默认能使用展开操作符。
我们来看遵守迭代协议的字符串怎么被展开的:

1
2
3
4
5
6
7
var str = 'hi';  
var iterator = str[Symbol.iterator]();
iterator.toString(); // => '[object String Iterator]'
iterator.next(); // => { value: 'h', done: false }
iterator.next(); // => { value: 'i', done: false }
iterator.next(); // => { value: undefined, done: true }
[...str]; // => ['h', 'i']

我喜欢用对象自定义迭代执行展开操作符。你能控制怎样展开你的对象——一个实用的代码技巧。

下面的例子制造一个符合迭代协议的类数组对象,然后用展开操作符转换成数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function iterator() {  
var index = 0;
return {
next: () => ({ // Conform to Iterator protocol
done : index >= this.length,
value: this[index++]
})
};
}
var arrayLike = {
0: 'Cat',
1: 'Bird',
length: 2
};
arrayLike[Symbol.iterator] = iterator; //Conform to Iterable Protocol
var array = [...arrayLike];
console.log(array); // => ['Cat', 'Bird']

arrayLike[Symbol.iterator] 在对象上创建一个包含迭代函数 iterator() 的属性使对象符合迭代协议。 iterator() 返回一个包含返回一个控制对象 {done: <boolean>, value: <item>}next 函数的对象(符合迭代协议)。
由于现在 arrayLike 是可迭代的,展开操作符就能取出它的元素放入一个数组: [...arrayLike]

最后

三点操作符为 JS 添加了一堆非常好的特性。

可变操书似的收集参数更容易了。它理应替代硬编码的类数组对象 arguments。如果情形允许在可变参数和 arguments 之间选择,那就选择第一个。

.apply() 的冗长语法是不方便的。当调用参数取自一个数组的时候,展开操作符是一个很好的选择。

展开操作符改良了数组字面量。你能更简单的初始化、连结和克隆数组。

你能用解构赋值取出数组的一部分。结合迭代协议在更多可配置的方式中使用展开操作符变得可能。

我希望从现在开始展开操作符经常出现在你的代码中!

如何自己实现虚拟DOM

原文地址:medium.com @deathmood

自己实现虚拟DOM需要知道两件事情。你不需要深入研究 React 源码或者其它的实现源码,他们非常庞大和复杂,但是真正实现虚拟DOM部分的代码不到50行。50行代码!!!

这里有两个概念:

  • 虚拟DOM是任何一种真实DOM的表现形式
  • 改变虚拟DOM树中的东西,我们将获得一个新的虚拟DOM树。通过算法来比较这两个树(新的和旧的虚拟树),找出不同点。然后对应虚拟树在真实DOM树中做最小的必要改变。

就是这样!让我们一起来深入理解每个概念。

表示 DOM 树

首先需要以某种方式将DOM树存储在内存中。我们可以用原生 Object 来做这件事。假设我们有一个这样的树:

1
2
3
4
<ul class="list">
<li>item 1</li>
<li>item 2</li>
</ul>

看起来很简单,不是吗?我们怎样只用JS对象来表示它呢?

1
2
3
4
{ type: 'ul', props: { 'class': 'list' }, children: [
{ type: 'li', props: {}, children: ['item 1'] },
{ type: 'li', props: {}, children: ['item 2'] }
] }

这里我们需要注意两点:

  • 我们用对象表示DOM元素

    1
    { type: '…', props: { … }, children: [ … ] }
  • 我们用原生JS字符串表示DOM文本节点

但是用这样的方法写一个大树是很困难的。所以我们写一个辅助函数,她能让我们更容易理解结构:

1
2
3
function h(type, props, …children) {
return { type, props, children };
}

现在,我们可以像这样来写DOM树:

1
2
3
4
h('ul', { 'class': 'list' },
h('li', {}, 'item 1'),
h('li', {}, 'item 2'),
);

它看起来简洁多了,不是吗?但是我们可以更进一步。你听说过 JSX 吗?是的,在这里我想用它。它是怎么工作的?

如果你读过这里的官方 Babel JSX 文档,你将会知道,Babel 编译这段代码:

1
2
3
4
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>

像这样:

1
2
3
4
React.createElement('ul', { className: 'list' },
React.createElement('li', {}, 'item 1'),
React.createElement('li', {}, 'item 2'),
);

注意到有一些相似?是的是的。如果我们能将 React.createElement(…) 替换成我们的 h(…)。事实上我们可以这样做——通过用叫做 jsx pragma 的东西。我们只需要在文件顶部写一行类注释的内容:

1
2
3
4
5
/** @jsx h */
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>

它告诉 Babel “嘿,编译 JSX 但是要用 h 代替 React.createElement。” 在这你可以用任意一个函数代替 h 进行编译。

所以,像我前面说的那样,我们将用这种方法写DOM:

1
2
3
4
5
6
7
/** @jsx h */
const a = (
<ul className="list">
<li>item 1</li>
<li>item 2</li>
</ul>

);

它将会被 Babel 编译成这样:

1
2
3
4
5
6
const a = (
h('ul', { className: 'list' },
h('li', {}, 'item 1'),
h('li', {}, 'item 2'),
);
);

当函数 h 执行的时候,它会返回一个原生JS对象——我们虚拟DOM表示:

1
2
3
4
5
6
const a = (
{ type: 'ul', props: { className: 'list' }, children: [
{ type: 'li', props: {}, children: ['item 1'] },
{ type: 'li', props: {}, children: ['item 2'] }
] }
);

开始吧!在 JSFiddle 中尝试(不要忘记设置语言为 Babel ):

应用我们的 DOM 表示法

好了,现在我们已经用自己的结构的原生对象表示了我们的 DOM 树。这非常酷,但是我们需要用它以一种方式创建真实的DOM树,我们不能只将我们的表示法附加到 DOM 中。

首先让我们做一些假设和定义一些术语:

  • 所有的真实 DOM 节点(元素和文本节点)变量将会用 $ 开头——因此 $parent 将会是一个真实的 DOM 元素
  • 虚拟节点表示法将指定变量名为 node
  • 像在 React 中,你只能有一个根节点——所有的其他节点都在里面

好了,像这样说的,让我们写一个能将虚拟 DOM 节点转化成真实 DOM 节点的函数 createElement(…) 。现在先忽略 propschildren ——我们以后将创建它:

1
2
3
4
5
6
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
return document.createElement(node.type);
}

因为我们有文本节点(原生字符串)和 Elements(含有 type 的JS对象)像这样:

1
{ type: '…', props: { … }, children: [ … ] }

因此,我们能传递虚拟的文本节点和虚拟的 element 节点,它都能工作。
现在,让我们一起来思考子节点——他们每一个不是文本节点就是 Element 节点。所以它们也能被 createElement(…) 函数创建。耶,你感觉到了吗?它感觉像是递归。所以我们能为每个元素子节点调用 createElement(…) ,然后像这样 appendChild() 它们到我们的元素中:

1
2
3
4
5
6
7
8
9
10
function createElement(node) {
if (typeof node === ‘string’) {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}

哇哦,看起来很不错。现在我们先把 props 放在一边,以后再讨论它。我们不需要它来理解虚拟DOM的基础概念,但是它却会增加复杂度。

现在开始让我们在 JSFiddle 中尝试它:

变化处理

我们已经能把虚拟DOM转换成真实DOM,现在思考虚拟DOM树之间的差别。我们需要写一个来对比两个虚拟DOM树(新和旧)并在真实DOM树中只做必要改变的算法。

怎么对比树?我们需要下面几种情况:

  • 对应位置没有旧的节点——需要 appendChild(…) 添加节点
    对应位置没有旧的节点
  • 对应位置没有新的节点——需要 removeChild(…) 删除节点
    对应位置没有新的节点
  • 对应位置有不同的节点——需要 replaceChild(…) 替换节点
    对应位置没有新的节点
  • 节点相同——需要进一步对比子节点
    节点相同

好了,我们写一个 updateElement(…) 函数,传递三个参数($parent, newNode and oldNode)给它,$parent 是虚拟节点对应的真实节点的父节点。现在我们来看怎么处理上面描述的几种情况。

没有旧节点

这是非常简单的:

1
2
3
4
5
6
7
function updateElement($parent, newNode, oldNode) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
}
}

没有新节点

这里有个问题——如果新虚拟树的当前位置没有节点,我们应该在真实DOM中移除它,但是我们应该怎么做呢?我们知道父元素(已被传递到函数中),因此我们假设调用 $parent.removeChild(…) 并传递真实DOM的引用给它。但是我们没有真实DOM的引用。如果我们知道节点在父元素中的位置,我们能够通过 $parent.childNodes[index] 得到它的引用, index 就是节点在父元素中的索引。

我们假设 index 会传递给函数(后面你会看到它真的会被传递)。因此我们的代码会变成这样:

1
2
3
4
5
6
7
8
9
10
11
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
}
}

节点变化

首先我们需要一个对比两个节点的函数来告诉我们节点是否改变。我们应该考虑 elements 和文本节点:

1
2
3
4
5
function changed(node1, node2) {
return typeof node1 !== typeof node2 ||
typeof node1 === ‘string’ && node1 !== node2 ||
node1.type !== node2.type
}

现在我们已经有了当前节点在父元素的索引 (index) ,我们能很容易用新创建的节点替换它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
}
}

比较子节点

最后,但是并非不重要的。我们应该遍历每个节点的子节点并对比它们,通过调用 updateElement(…) 。是的,又是递归。
但是有些写代码之前需要考虑的事情:

  • 我们应该比较只有节点是 Element 的子节点(文本节点没有子节点)
  • 传递当前节点的引用作为父节点
  • 必须一个一个的比较子节点,即使有些地方我们只有 “undefined” 这都是没问题的,我们的函数可以处理
  • 最后,index——它只是子节点在子节点数组中的索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function updateElement($parent, newNode, oldNode, index = 0) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
} else if (!newNode) {
$parent.removeChild(
$parent.childNodes[index]
);
} else if (changed(newNode, oldNode)) {
$parent.replaceChild(
createElement(newNode),
$parent.childNodes[index]
);
} else if (newNode.type) {
const newLength = newNode.children.length;
const oldLength = oldNode.children.length;
for (let i = 0; i < newLength || i < oldLength; i++) {
updateElement(
$parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
);
}
}
}

所有的代码放在一起

我们把所有的代码放在 JSFiddle, 就像我承诺的一样它的执行部分只有50行代码。我们来看看:

打开开发者工具,观察点击 “Reload” 按钮时变化的应用。

结语

恭喜!我们已经做到了。我们写一个虚拟DOM的执行程序,它工作的很好。我希望通过阅读这篇文章,你能够理解虚拟DOM和React底层的运行机制。

然而有些事情我们并没有在这里介绍。(在后面的文章中我会尝试着覆盖到它们)

  • 设置/对比/更新 Element 属性(props)
  • 处理事件——在 Elements 上添加事件监听
  • 像React一样在组件中使用虚拟DOM
  • 真正的 DOM 节点引用
  • 在直接改变真实 DOM 的库中使用虚拟DOM——像 jQuery 和它的插件一样
  • 以及更多的内容…

React 学习笔记 - 4

前言

前边几节中学习了组件和路由控制,已经能够做一个静态的SPA了。但是对于一个富应用来说,我们只学习了View,接下来我们将要学习数据处理模块,在正式开始学习之前,先来介绍下这位新朋友。

Redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。Redux 由 Flux 演变而来,但受 Elm 的启发,避开了 Flux 的复杂性。
Redux 规定,将模型的更新逻辑全部集中于 reducer 层,不允许程序直接修改数据,而是用一个叫作 action 的普通对象来对更改进行描述。

三个基本原则

  1. 单一数据源
    整个应用只存在一个 Object tree,并且 Object tree 只存在于唯一的 store 中。
  2. State 是只读的
    唯一改变 state 的方法就是触发 action
  3. 使用纯函数来执行修改
    为了描述 action 如何改变 state tree ,你需要编写 reducers。

Action

action是把数据从应用传到 Store 的载体,是 Store 数据的唯一来源。

  • 示例Action:
    1
    2
    3
    4
    5
    6
    7
    const ADD_MESSAGE = 'ADD_MESSAGE';

    {
    type: ADD_MESSAGE,
    data
    // 其它数据
    }

这里约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。建议在单独的模块或文件中使用常量定义,以方便维护。

Action 创建函数

Action 创建函数,是一个返回action的简单函数,这样做方便移植和测试。

  • 示例
    1
    2
    3
    4
    5
    6
    function addMessage(data) {
    return {
    type: ADD_MESSAGE,
    data
    };
    }

Redux 中只需把 action 创建函数的结果传给 dispatch() 方法即可发起一次 dispatch 过程。

Reducer

Action 描述了事情已经发生,Reducer来执行具体如何更新state。

  1. 先设计state的结构,以一个消息列表为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    list: [
    {
    text: 'Consider using Redux',
    create_time: '2016-05-21 12:22:56'
    },
    {
    text: 'Keep all state in a single tree',
    create_time: '2016-05-21 12:22:56'
    }
    ]
    }
  2. Action 处理
    reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import {ADD_MESSAGE} from '../actions/constants';

    const initState = {
    list: []
    };

    export function Message(state = initState, action) {
    switch(action.type){
    case ADD_MESSAGE:
    // return Object.assign({}, state, {
    // list: [...state.list, action.data]
    // });

    let list = [...state.list, action.data];
    return { ...state, list };
    default:
    return state;
    }
    }

    action参数的值是 Action对象,在这里不要在state上直接进行修改,可以使用Object.assign({}, state) 或 其他库的 _.assign() 对state新建一个副本,第一个参数必须设置为空对象。还可以使用了ES7提案中的对象展开运算符,需要使用转换编译器,如Babel。Babel中使用 babel-plugin-transform-object-rest-spread 插件。

    注意:永远不要在reducer中做以下操作:

    • 修改传入参数;
    • 执行有副作用的操作,如 API 请求和路由跳转;
    • 调用非纯函数,如 Date.now() 或 Math.random()。
  3. 多个Action
    要处理多个Action的时候,在switch中为每一个Action添加一个case。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    export function message(state = initState, action) {
    switch(action.type){
    case ADD_MESSAGE:
    let list = [...state.list, action.data];
    return { ...state, list };
    case DELETE_MESSAGE:
    let index = action.index;
    if(index >= 0 && index < state.list.length){
    let list = [...state.list.slice(0, index), ...state.list.slice(index+1)];
    return {...state, list};
    }

    return state;
    default:
    return state;
    }
    }
  4. 多个reducer
    多个Action时,需要合理的设计state结构,例如如下结构state,可以拆分filter和message为两个reducer。建议为每个reducer对应一个文件,应用庞大时可使用文件夹分类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    filter: {
    key: 'Redux'
    },
    message: [
    {
    text: 'Consider using Redux',
    create_time: '2016-05-21 12:22:56'
    },
    {
    text: 'Keep all state in a single tree',
    create_time: '2016-05-21 12:22:56'
    }
    ]
    }

    现在我们在reducers文件夹下建两个reducer文件,分别命名为filter.reducer.js和message.reducer.js。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // filter.reducer.js
    import { INPUT_FILTER_KEY } from '../actions/constants';

    const initState = {
    key: ''
    };

    export default function filter(state = initState, action) {
    switch(action.type) {
    case INPUT_FILTER_KEY:
    let key = action.key || '';
    return {...state, key};
    default:
    return state;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // message.reducer.js
    import {ADD_MESSAGE, DELETE_MESSAGE} from '../actions/constants';

    const initState = []

    export default function message(state = initState, action) {
    switch(action.type){
    case ADD_MESSAGE:
    return [...state, action.data];
    case DELETE_MESSAGE:
    let index = action.index;
    if(index >= 0 && index < state.length){
    return [...state.slice(0, index), ...state.slice(index+1)];
    }

    return state;
    default:
    return state;
    }
    }

    由于应用中只能存在单一的state树,所以我们需要将上面两个reducer合并成一个,Redux 提供了 combineReducers() 工具类来合并多个reducer。
    现在我们在reducers文件夹下,新建一个 index.js 文件使用 combineReducers() 合并两个reducer:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { combineReducers } from 'redux';

    import message from './message.reducer';
    import filter from './filter.reducer';

    const messageApp = combineReducers( {
    message,
    filter
    } );

    export default messageApp;

    注意上面的写法和下面完全等价:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import message from './message.reducer';
    import filter from './filter.reducer';

    export default function messageApp(state = {}, action){
    return {
    message: message(state.message, action),
    filter: filter(state.filter, action)
    }
    };

Store

Store 就是把Action 和 reducer 联系到一起的对象。Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。
根据已有的reducer创建store非常容易。现在我们导入前面合并后的reducer,并传递给 ctreateStore()

  • 创建一个store文件
    1
    2
    3
    4
    5
    6
    7
    import { createStore } from 'redux';

    import messageApp from '../reducers';

    let store = createStore(messageApp);

    export default store;

发起Actions

现在我们已经建好了store,在没有页面的情况下就已经可以开始测试数据了。

  • 测试store
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import store from './store';
    import {addMessage, deleteMessage, inputFilterKey} from './actions';

    console.log(store.getState());

    // 每次 state 更新时,打印日志
    // 注意 subscribe() 返回一个函数用来注销监听器
    let unsubscribe = store.subscribe(() =>
    console.log(store.getState())
    );

    // 发起一系列 action
    store.dispatch(addMessage({text: 'Learn about actions', create_time: '2016-05-24 11:09:24'}));
    store.dispatch(addMessage({text: 'Learn about reducers', create_time: '2016-05-24 11:09:24'}));
    store.dispatch(addMessage({text: 'Learn about store', create_time: '2016-05-24 11:09:24'}));
    store.dispatch(deleteMessage(2));
    store.dispatch(deleteMessage(0));
    store.dispatch(inputFilterKey('Learn'));

    // 停止监听 state 更新
    unsubscribe();

连接Redux

在我们已经写好页面的情况下,如何将页面组件与Store连接?
我们用 react-redux 提供的 connect() 方法,将Message Component 转化成容器组件。

  • containers/message.container.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
    import { connect } from 'react-redux';

    import { Message } from '../components/';
    import { addMessage, deleteMessage, inpuFilterKey } from '../actions';


    // 哪些 action 创建函数是我们想要通过 props 获取的?
    function mapDispatchToProps(dispatch) {
    return {
    onAddMessage: (data) => dispatch(addMessage(data)),
    onDeleteMessage: (index) => dispatch(deleteMessage(index)),
    onChangeFilterKey: (key) => dispatch(inputFilterKey(key))
    };
    }

    function filterMessage(messages, filter_key) {
    if(filter_key){
    var messageList = [];
    messages.map( (message, index) => {

    if( ~message.text.indexOf(filter_key) ){
    messageList.push({
    id: index,
    text: message.text,
    create_time: message.create_time
    });
    };
    });

    return messageList;
    } else {
    return messages;
    }
    }
    // 哪些 Redux 全局的 state 是我们组件想要通过 props 获取的?
    // 这里我们还使用了 filterMessage 来根据filter_key 筛选出相匹配的 message
    function mapStateToProps(state){
    return {
    messages: filterMessage(state.message, state.filter.key),
    filter_key: state.filter.key
    }
    }

    export default connect(
    mapStateToProps,
    mapDispatchToProps
    )(Message);

到此,我们已经可以使用Redux来管理React数据了,但是当我们需要和服务器进行交互或者是需要进行非纯函数操作时,应该怎么做呢?

异步Action

在使用异步Action时,一般情况下每个请求都要dispatch三个不同的Action,为了区分不同的Action,我们建议为他们定义不同的type。
上面 addMessage 的create_time值都是静态数据,现在我们想动态的获取create time。

  • 1
    2
    3
    { type: 'GET_TIME_REQUEST' }
    { type: 'GET_TIME_FAILURE', error: 'error info' }
    { type: 'GET_TIME_SUCCESS', data: { ... } }
  1. 创建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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    export function addMessage(data) {
    // return {
    // type: ADD_MESSAGE,
    // data
    // };

    return dispatch => {
    dispatch( getTimeRequest() );
    getTime().then(time => {
    dispatch( getTimeSuccess() );

    data.create_time = time;

    dispatch( addMessageToStore(data) );

    }).catch(error => dispatch( getTimeFailure(error && error.message || error) ));
    }

    }

    function addMessageToStore(data) {
    return {
    type: ADD_MESSAGE,
    data
    };
    }

    function getTimeRequest() {
    return {
    type: GET_TIME_REQUEST
    }
    }

    function getTimeFailure(error) {
    return {
    type: GET_TIME_FAILURE,
    error
    }
    }

    function getTimeSuccess(data) {
    return {
    type: GET_TIME_SUCCESS,
    data
    }
    }

    function getTime(){
    return new Promise(function(resolve, reject){
    let time = moment().format('YYYY-MM-DD HH:mm:ss');

    resolve(time);
    });
    }
  2. 创建reducer

    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
    import { GET_TIME_REQUEST, GET_TIME_FAILURE, GET_TIME_SUCCESS, GET_TIME_ERROR_CLEAR } from '../actions/constants';

    const initState = {
    loading: false,
    errorInfo: ''
    };

    export default function getTime(state = initState, action) {
    let loading = false;
    let errorInfo = '';
    switch(action.type) {
    case GET_TIME_REQUEST:
    loading = true;
    return {...state, loading};
    case GET_TIME_FAILURE:
    case GET_TIME_SUCCESS:
    loading = false;

    if(action.error){
    errorInfo = JSON.stringify(action.error);
    } else {
    errorInfo = '';
    }
    return {...state, loading, errorInfo};
    case GET_TIME_ERROR_CLEAR:
    errorInfo = '';
    return {...state, errorInfo};
    default:
    return state;
    }
    }
  3. 添加Middleware
    要在 Action 中使用 dispatch() 函数,需要中间件支持,我们选择使用 redux-thunk 。

    1
    2
    import thunk from 'redux-thunk';
    let store = createStore(reducers, applyMiddleware(thunk));

redux-thunk 中间件不是唯一的方法。也可以使用 redux-promise 或者 redux-promise-middleware 来 dispatch Promise 替代函数。当然也可以写一个自定义的middleware。

现在已经可以使用redux通过同步和异步Action管理数据了。

参考链接

更多API参考 Redux Document 中文

示例代码地址 Github

推荐阅读

從 source code 來看 Redux 更新 state 的運行機制
從 source code 來看 React-Redux 怎麼讓 Redux 跟 React 共舞
需要搬梯子

React 学习笔记 - 3

简介

本节主要学习React Component相关内容

目录

  • create
  • props
  • state
  • Event system
  • lifecycle
  • context

create

有三种方式创建Component,实际开发中可根据实际情况选择

  1. class App extends React.Component {}
    这是ES6中class 继承的写法
  2. const App = React.createClass( {} )
    这是通过createClass函数来创建的写法
  3. 无状态函数
    1
    2
    const HelloMessage = (props) => <div>Hello {props.name}</div>;
    ReactDOM.render(<HelloMessage name="Sebastian" />, mountNode);

(1)(2)两种方法类似,具体区别参考官方文档中的 ES6 classes

(3)只适用于简单组件

props

props的属性和方法来自于调用该 Component 时传递的内容。

children

JSX有两种方式render Component: <App /> & <App>Hello world</App>,第一种 没有children属性,第二种children可以是 字符串DOM树,为DOM树时children是一个和树对应的数组。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 需要先 import Parent Component
    <div>
    <Parent />
    <code>&lt;Parent /&gt;</code>
    </div>
    <div>
    <Parent>Hello world!</Parent>
    <code>&lt;Parent&gt;字符串&lt;/Parent&gt;</code>
    </div>

    <div>
    <Parent>
    <h1>这是H1</h1>
    <h2>这是H2</h2>
    </Parent>
    <code>&lt;Parent&gt;DOM Tree&lt;/Parent&gt;</code>
    </div>
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // Parent Component 
    import React from 'react';

    class Parent extends React.Component {
    render() {
    return (
    <div>
    <h3>Parent</h3>
    <p>This is children:</p>
    {this.props.children}
    </div>

    );
    }
    }

    export default Parent;

手动传递

React中 this.porps的属性除children外都需要手动传递,手动传递有两种方式:

  1. “不定参数“形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import React from 'react';

    import Receive from './receive.component';

    class Transfer extends React.Component {
    render() {
    let props = {
    a: 1,
    b: 'show Text',
    c: ['tv', 'ico', 'co']
    }
    return (
    <div>
    <h2>Transfer</h2>
    <Receive {...props} />
    </div>
    )
    }
    }

    export default Transfer;

  2. 属性方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import React from 'react';

    import Receive from './receive.component';

    class Transfer extends React.Component {
    render() {
    return (
    <div>
    <h2>Transfer</h2>
    <Receive x="2002" y={['fb', 'tw', 'gl']} z={{text: 'hello', name: 'Deo'}} />
    </div>
    )
    }
    }

    export default Transfer;

Tips: v15已不再建议通过setProps replaceProps修改props。

state

state用于维护当前组件的状态

初始化state, 需要用到getInitialState方法,但ES6 classes需要写在constructor中,示例如下:

  1. createClass

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import React from 'react';

    const StateEs5 = React.createClass({
    getInitialState() {
    return {count: 0};
    },

    tick() {
    this.setState({count: this.state.count + 1});
    },

    render() {
    return (
    <div onClick={this.tick}>
    Clicks: {this.state.count}
    </div>

    );
    }
    });

    export default StateEs5;
  2. ES6 classes

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import React from 'react';

    class State extends React.Component {
    constructor(props) {
    super(props);
    this.state = {count: 0};
    // this.tick = this.tick.bind(this);
    }

    tick() {
    this.setState({count: this.state.count + 1});
    }

    render() {
    return (
    <div onClick={() => this.tick()}>
    Clicks: {this.state.count}
    </div>

    );
    }

    }

    export default State;

除了初始化state的区别外,ES6 classes 不支持自动绑定,需要在绑定时使用箭头函数 ()=>this.tick() 或者在constructor中添加 this.tick = this.tick.bind(this),两种方法任选其一。

事件绑定

  • 绑定示例
    1
    2
    3
    4
    <div onClick={this.handleClick}>
    Clicks: {this.state.count}
    </div>
    <input type="text" onChange={this.handleChange} />

支持的事件列表参看 官方文档

生命周期

方法分为三类:

  1. 初始化调用且只调用一次
    • componentWillMount
    • componentDidMount
  2. 更新props或state时调用(初始化时不调用)
    • componentWillReceiveProps
    • shouldComponentUpdate (执行forceUpdate时该方法不执行)
    • componentWillUpdate
    • componentDidUpdate
  3. 卸载组件时调用
    • componentWillUnmount

附:官方文档

Context

Context是一个高级实验功能,未来可能会有变化,请谨慎使用。

  • 示例代码
    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
    import React from 'react';

    const Button = React.createClass({
    contextTypes: {
    btnClass: React.PropTypes.string
    },

    render() {
    return (
    <button className={'btn ' + this.context.btnClass}>
    {this.props.children}
    </button>

    );
    }
    });

    const Message = React.createClass({
    render() {
    return (
    <div>
    {this.props.text} <Button>Delete</Button>
    </div>

    );
    }
    });

    const MessageList = React.createClass({
    childContextTypes: {
    btnClass: React.PropTypes.string
    },
    getChildContext() {
    return {btnClass: "btn-default"};
    },
    render() {
    let children = ['Context 示例', '在祖先组件定义并设置值', '在子组件取得相应值'].map(function(message, index) {
    return <Message text={message} key={index} />;
    });
    return <div>{children}</div>;
    }
    });

    export default MessageList;

结语

props state context 为Component提供了需要的数据

  • state: 维护Component内部的状态
  • props: 属性或方法由父组件传递到Component
  • context: 方法或属性可继承祖先组件要传给后代的数据

参考链接

更多API参考 React Document

示例代码地址 Github

React 学习笔记 - 2

简介

本节中包含React-router控制React SPA路由。

关于React-router的详细使用,请查阅官方文档。

示例代码

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
import React from 'react';
import { render } from 'react-dom';

import {Router, Route, IndexRoute, Link} from 'react-router';

class App extends React.Component {
render() {
return (
<div>
<h1>Welcome to React!</h1>
<ul>
<li><Link to="/">Index</Link></li>
<li><Link to="/about">About</Link></li>
</ul>

{this.props.children}
</div>

);
}
}

class Index extends React.Component {
render() {
return (
<div>
<h2>Index</h2>
</div>

);
}
}

class About extends React.Component {
render() {
return (
<div>
<h2>About</h2>
</div>

);
}
}

render( (
<Router>
<Route path="/" component={App}>
<IndexRoute component={Index} />
<Route path="about" component={About} />
</Route>
</Router>
), document.getElementById('content') );

运行以上代码能正常运行,URL地址为http://hostname/#/pageRoute?_k=xxxx,并在Console中看到以下警告

1
Warning: [react-router] `Router` no longer defaults the history prop to hash history. Please use the `hashHistory` singleton instead. http://tiny.cc/router-defaulthistory

根据以上提示信息,给Router Component添加history属性 <Router history={hashHistory}>使用hashHistory前需要先在react-router模块中引入

URL格式

  1. 想要使用http://hostname/pageRoute格式的URL地址,只需用 browserHistory 替换掉hashHistory即可。
  2. 想要使用http://hostname/projectName/pageRoute格式的URL地址,需要引入 useRouterHistoryhistory模块的 createHistory,然后添加如下代码
    1
    2
    3
    const history = useRouterHistory(createHistory)({
    basename: '/projectName'
    })

多级路由

本节需到的API:

  • IndexRoute from react-router

多级路由需要将次级对应的<Route />节点作为父级的字节点

1
2
3
4
5
6
7
8
<Router>
<Route path="/" component={App}>
<IndexRoute component={Index} />
<Route path="about" component={About}>
<Route path="author" component={Author} />
</Route>
</Route>
</Router>

并在父级component中输出{this.props.children}

1
2
3
4
5
6
7
8
9
10
class About extends React.Component {
render() {
return (
<div>
<h2>About</h2>
{this.props.children}
</div>

);
}
}

About Component的文字出现在了/about/author中,怎么才能让/about/author不显示About Component的内容呢?
这时候就要用到react-router的另外一个Component IndexRoute,使用方法和 Route 相同,修改后的代码如下

1
2
3
4
<Route path="about" component={About}>
<IndexRoute component={AboutIndex} />
<Route path="author" component={Author} />
</Route>

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 About extends React.Component {
render() {
return (
<div>
{this.props.children}
</div>

);
}
}

class AboutIndex extends React.Component {
render() {
return (
<div>
<h2>About</h2>
</div>

);
}
}

class Author extends React.Component {
render() {
return (
<div>
<h2>Author: Gavin</h2>
</div>

);
}
}

可变路由

当需要路由/user/gavin /user/david等这类有规律且对应的Component是同一个的时候,可设置路由规则为/user/:username
示例如下

1
2
3
4
<Route path="user" component={User}>
<IndexRoute component={UserIndex} />
<Route path=":username" component={UserCenter} />
</Route>

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 User extends React.Component {
render() {
return (
<div>
{this.props.children}
</div>

)
}
}

class UserIndex extends React.Component {
render() {
return (
<div>
<h2>User Index</h2>
</div>

)
}
}

class UserCenter extends React.Component {
render() {
return (
<div>
<h2>User Name: {this.props.params.username}</h2>
</div>

)
}
}

可以看出:username对应的值在Component中通过this.props.params.username获取。

应用内部链接需要使用<Link />代替<a href="">Link</a>,具体使用方法参考官方文档。

生命周期

在1.x中提供了Mixin Lifecycle的方法,可以添加方法routerWillLeave,当离开当前Component触发,2.x版本已不再建议。
在2.x中相同需求的实现方法如下

1
2
3
4
5
6
7
8
9
10
const RouteComponent = React.createClass({
contextTypes: {
router: React.PropTypes.object.isRequired
},
componentDidMount() {
const { route } = this.props
const { router } = this.context
router.setRouteLeaveHook(route, this.routerWillLeave)
}
});

Tips: 这里用 React.createClass, 而不是继承React.Component将在下一节中学习。

其他

React Router 还提供了一些有用的Component/Function:

  • <Redirect/> 路由跳转以便兼容旧路由
  • RouterContext 通过context.router访问router的方法(具体的实现原理将在下一节中学习)
  • withRouter 通过this.props.router访问router的方法

Tips: router 类似于1.x的history,2.x不再推荐history和Mixins,后续版本可能会移除

参考链接

相关API参考 React Router Document

示例代码地址 Github

React 学习笔记 - 1

文档

  1. React Document
  2. React-router Document
  3. Redux Document
  4. Webpack Document

简介

React SPA 开发,需要引入React-router控制路由,这里选择了Redux管理数据层,构建工具选择Webpack

本节中包含React SPA 开发的相关配置和一个React的简单示例。

Tips :文章中所有示例代码使用ES6

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├──app
| ├──js
| | ├──action
| | ├──components
| | ├──containers
| | ├──reducer
| | ├──store
| | └──app.js
| └──index.html
├──package.json
├──server.js
└──webpack.config.js

Layout

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>react example</title>
</head>
<body>
<div id="content"></div>
<script src="/build/app.js"></script>
</body>
</html>

Tips:示例中的样式使用bootstrap,在js中import,然后webpack构建

Webpack配置

  1. webpack 配置写入 webpack.config.js 文件
  2. 本地开发使用webpack-server启动web服务,监控文件变化

webpack.config.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
var path = require('path');
var webpack = require('webpack');

module.exports = {

devtool: 'inline-source-map', // map 实际开发中,可使用NODE_ENV配置

entry: path.join(__dirname, 'app', 'js', 'app.js'),

output: {
path: __dirname + '/build/',
filename: 'app.js',
chunkFilename: '[id].chunk.js',
publicPath: '/build/'
},

module: {
loaders: [
{ test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel', query: {
presets: ['es2015','react'],
plugins: ['transform-object-rest-spread']
}
}, //使用babel把 jsx和es6 转换为 es5
{ test: /\.css$/, loader: 'style!css' },
{ test: /\.(woff2?|svg)$/, loader: 'url?limit=10000' },
{ test: /\.(ttf|eot)$/, loader: 'file' },
]
},

resolve: {
extensions: ['', '.js', '.jsx'] //处理文件扩展名
},

plugins: [
// new webpack.optimize.CommonsChunkPlugin('shared.js'),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
__DEV__: JSON.stringify(JSON.parse(process.env.DEBUG || 'false'))
})
]

}

webpack-server

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
var path = require('path')
var express = require('express')
var rewrite = require('express-urlrewrite')
var webpack = require('webpack')
var webpackDevMiddleware = require('webpack-dev-middleware')
var WebpackConfig = require('./webpack.config')

var app = express()

app.use(webpackDevMiddleware(webpack(WebpackConfig), {
publicPath: '/build/',
stats: {
colors: true
}
}))

app.use(express.static(__dirname))

app.get('/*', function (request, response){
response.sendFile(path.resolve(__dirname, 'app', 'index.html'))
})

app.listen(8080, function () {
console.log('Server listening on http://localhost:8080, Ctrl+C to stop')
})

依赖模块

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
{
"devDependencies": {
"babel-cli": "^6.7.5",
"babel-core": "^6.7.6",
"babel-eslint": "^5.0.4",
"babel-loader": "^6.2.4",
"babel-plugin-add-module-exports": "^0.1.2",
"babel-plugin-dev-expression": "^0.2.1",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-polyfill": "^6.7.4",
"babel-preset-es2015": "^6.6.0",
"babel-preset-es2015-loose": "^7.0.0",
"babel-preset-es2015-loose-native-modules": "^1.0.0",
"babel-preset-react": "^6.5.0",
"babel-register": "^6.7.2",
"bootstrap": "^3.3.6",
"bundle-loader": "^0.5.4",
"codecov.io": "^0.1.6",
"coveralls": "^2.11.9",
"css-loader": "^0.23.1",
"eslint": "^1.10.3",
"eslint-config-rackt": "^1.1.1",
"eslint-plugin-react": "^3.16.1",
"expect": "^1.18.0",
"express": "^4.13.4",
"file-loader": "^0.8.5",
"gzip-size": "^3.0.0",
"history": "^2.1.0",
"isomorphic-fetch": "^2.2.1",
"isparta-loader": "^2.0.0",
"pretty-bytes": "^3.0.1",
"qs": "^6.1.0",
"react": "^15.0.0",
"react-addons-css-transition-group": "^15.0.0",
"react-addons-test-utils": "^15.0.0",
"react-bootstrap": "^0.29.3",
"react-dom": "^15.0.0",
"react-redux": "^4.4.5",
"react-router": "^2.4.0",
"react-router-bootstrap": "^0.23.0",
"redux": "^3.5.2",
"redux-logger": "^2.6.1",
"redux-thunk": "^2.1.0",
"rimraf": "^2.5.2",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.12.14",
"webpack-dev-middleware": "^1.6.1"
}
}

到这里配置部分已经结束,接下来写一个小demo 来试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import { render } from 'react-dom';

class App extends React.Component {
render() {
return (
<div>
<h1>Welcome to React!</h1>
</div>

);
}
}

render( ( <App /> ), document.getElementById('content') );

示例代码地址 Github