0%

我们以一个Button组件为例。尝试从设计到实现一个React组件。

按钮的设计

轮廓线(按钮的边框)

主要的作用就是按钮与背景色相近的时候让按钮更加的明显。
成像效果不好的显示器,显示效果会有偏差。加上轮廓线确保显示。

类型检测

静态类型检测是写组件必不可少的。一方面是在开发时避免类型问题,它还可以通过IDE的智能提醒方便使用组件的开发者。将使用Typescript对组件进行类型检测声明。

Props声明

首先是Props声明,Props声明我们用组件名后面跟Props的形式来定义接口名称,并将它导出。

1
2
3
4
5
export interface BaseButtonProps {
type?: ButtonType;
children?: React.ReactNode;
...
}

在定义props的时候,我们要保持接口小,props数量要少。

组件声明

在组件使用中,应该无状态组件>有状态组件>Class组件。尤其是在hook出现后,函数组件被React大肆推广。

函数组件

我们用FC来声明函数组件,FC就是FunctionComponent的缩写,我们可以通过源码看到,它的props默认定义了children。

1
2
3
const ButtonBase:FC<BaseButtonProps> = props => {
return <div>{props.children}</div>
}

然后将这个组件导出

1
export default Button;

但是在之前FC类型来声明函数组件时并不能完美支持propsDefault(现在已经解决)。
一般用解构赋值的默认值来代替propsDefault

1
2
3
4
const BaseButton:FC<BaseButtonProps> = props => {
const { type = "default" } = props;
return <div>{props.children}</div>
}

也有直接使用普通函数来进行组件声明

1
2
3
4
const BaseButton = (props: BaseButtonProps): JSX.Element => {
const { type = "default" } = props;
return <div>{type}</div>
}

用普通函数来进行组件声明还有以下好处:

  1. React.FC隐式的提供了一个children props,如果你的组件不需要子组件,用普通函数组件也许是一个很好的选择
  2. 支持泛型。例如

    1
    2
    3
    4
    5
    6
    7
    type OptionValue = string | number;

    type Option<T extends OptionValue> = {
    value: T;
    };

    const App = <T extends OptionValue>(props: Option<T>): JSX.Element => <div>{props.value}</div>
  3. 在使用defaultProps不会出错。

【注意】在使用普通函数来进行组件声明的时候,defaultProps的类型和组件本身的props没有关联性,会使得defaultProps无法得到类型约束。比如

1
2
3
4
5
6
7
interface ButtonProps {
name: string;
}

const Button = (props: ButtonProps) => <div>{props.name}</div>

Button.defaultProps = { name: 123 } //这样是不会报错的

我们需要改写成这样

1
2
3
4
5
type Partial<T> = {
[P in keyof T]?: T[P];
};

Button.defaultProps = { name: 'Liam' } as Partial<ButtonProps>

概念

树的结构就很像生活中的树,一个枝干上分出很多的树杈。 在数据结构中,我们将树中的每个元素称为节点。没有父节点的叫做根节点。没有子节点的节点叫做叶子节点。如果多个节点的父节点是同一个节点,那么我们就将它们称为兄弟节点。
树还有 高度、深度、层。这三个概念

  • 节点的高度: 节点到叶子节点的最长路径,也就是边数
  • 节点的深度: 根节点到这个节点所经历的边的个数
  • 节点的层数: 节点的深度+1
  • 树的的高度: 根节点的高度

二叉树

每个节点上最多有两个叉。

满二叉树

除了叶子节点之外,每个节点都有左右两个子节点。

完全二叉树

最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大。

存储方式。

  1. 链式存储法。 每个节点有三个字段,一个存储数据,另外两个是指向左右节点的指针。
  2. 数组存储法。根节点在i=1的位置,左节点存储在2 i的位置,右节点存储在2 i + 1的位置。反过来 i/2存储的就是父节点的位置。 如果是完全二叉树,数组存储方式是最不占空间的。堆就是一种完全二叉树。

二叉树的遍历

  • 前序遍历是指,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
  • 中序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
  • 后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

在之前的文章中,介绍过JavaScript的typeof运算符。在JavaScript中typeof主要是用于类型检测。
而在TypeScript中typeof不光能类型检测,还能用来获取类型。
具体是什么,得结合环境来看。
举一个简单地例子

1
2
3
let bar = { a: 0 }
let b = typeof bar // "object"
type c = typeof bar // "{a: number}"

我们可以看出来,如果typeof赋值给一个变量的话,那将用JavaScript运算符。 如果赋给一个type的话,就是获取它的类型。TypeScript将其对象中的a属性自动推断为number类型。

也可以获取类型通过一个字符数组

1
2
3
const data = ['text 1', 'text 2'] as const;
type Data = typeof data[number];
// type Data = "text 1" | "text 2"

或者是一个tuple

1
2
3
4
5
6
export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

const animals = tuple('cat', 'dog', 'rabbit', 'snake');
type Animal = (typeof animals)[number];
//'cat' | 'dog' | 'rabbit' | 'snake'

还可以与keyof结合,将对象的key转为联合类型。

1
2
3
4
5
6
7
const datas = {
key1: 'value1',
key2: 'value2',
key3: 'value3',
}
type data = keyof typeof datas;
// type data = "key1" | "key2" | "key3"

旧的生命周期

constructor构造函数

构造函数的主要作用就是1、通过this.state来初始化内部的state。2、为事件函数绑定this。在组件挂载之前,会调用它的构造函数。

我们知道如果在为 React.Component 子类实现构造函数时,应在其他语句之前前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。我们可以来想想为什么每次都必须要写super,我们可以不写它么?

首先,super指的是父类的构造函数。在调用super之前,我们是不能使用this的。为什么这样做呢,我们试想一下,如果在调用super之前不禁止this,我们来看看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Father {
constructor(name) {
this.name = name
}
}

class Son extends Father {
constructor(name) {
this.greet();
super(name)
}
greet() {
console.log('hello my name is' + this.name);
}
}

我们可以看出在这个例子中,this.greet()在super()给this.name赋值之前就已经执行了。这时候this.name尚未定义。为了避免这种情况,强制在使用this之前先行调用super。
下面我们看看为什么要传入props么?

我们可以看看React内部

1
2
3
4
5
6
class Component {
constructor(props) {
this.props = props;
// ...
}
}

在React内部,的构造方法里我们一目了然,它直接将props赋值给this.props。
即便你调用super()的时候没有传入props,你依然能够在render函数或其他地方访问到this.props。这是因为,React 在调用构造函数后也立即将 props 赋值到了实例上

1
2
3
 // React 内部
const instance = new YourComponent(props);
instance.props = props;

尽管这样,我们也要避免使用super()而使用super(props),因为如果使用super(),会使得 this.props 在 super 调用一直到构造函数结束期间值为 undefined。

【注意】避免将props的值直接赋值给state。因为这样做毫无意义,并且props改变的时候,state并不改变。

componentWillMount

这是一个即将被废弃的方法。而且16.3已经将这个更名为UNSAFE_componentWillMount。它在挂载组件前调用,在componentWillMount中执行setState是毫无意义的。因为组件只挂在一次,componentWillMount也只执行一次,应该把这里的setState放到constructor中。
之前讨论的最多的就是异步请求api的时候,应该在componentWillMount里还是componentDidMount中,网上有一种说法是,放在componentWillMount中异步的请求数据,先会render一次空数据,等数据回来之后再走一遍render,所以应该放到componentDidMount中,但我觉得这种说法是不可靠的。因为在DidMount之前,同样也会render一遍空数据。早发起请求确实能早点获得结果,但是省的这几微妙根本微不足道。再加之WillMount即将被废弃。所以应该避免使用它。还有一点要注意,它是服务器渲染上调用的唯一方法。

render

render()是在componentWillMount()和componentWillReceive()之后调用。主要的作用是渲染组件,它是在class组件中唯一必须实现的方法。render应该为纯函数,意味着在state不变的情况下。每次调用返回的结果都应该相同。

componentDidMount

该方法会在render方法后立即执行,它跟componentWillMount一样也是永远只执行一次。在这里可以对DOM进行操作,因为这时组件已经加载完毕。之前我们也说了,网络请求数据的操作也应该放在这里。需要注意的是render函数结束之后,不会立即调用componentDidMount。是因为render函数本身并不往DOM树上渲染或者装载内容,它只是返回一个JSX表示的对象,然后由React库来根据返回对象决定如何渲染。而React库需要把所有组件返回的结果综合起来。才知道该如何产生对应的DOM修改。只有React库调用了全部render后,再依次调用各个组件的componentDidMount函数作为装载过程的收尾。

1
2
3
4
5
6
7
8
9
componentWillMount First
render First
componentWillMount Second
render Second
componentWillMount Third
render Third
componentDidMount First
componentDidMount Second
componentDidMount Third

componentWillReceiveProps

这个的用法主要是,需要根据新的prop改变状态,可以比较this.props和nextProps,并使用setState()执行state转换。
一般对这个方法有一个误区,就是它会通常被认为当props改变了它才会被调用,但事实是,只要是父组件的render函数被调用,在render函数里面被渲染的子组件就会经历更新过程,不管父组件传给子组件的props有没有改变,都会触发子组件的componentWillReceiveProps。我们唯一能保证的就是当props改变的时候,一定会触发componentWillReceiveProps,反之不然。注意在setState触发的更新过程是不会调用这个方法的。我们在使用的时候需要避免,在这个方法中调用父组件的回调函数修改父组件的state,这样父组件rerender会导致死循环。
同样这个方法在React 17中将被废弃。
想要使用应该写成UNSAFE_componentWillReceiveProps

shouldComponentUpdate

根据shouldComponentUpdate的返回值,判断React组件的输出是否受当前state或props更改的影响。意思就是判断组件是否重新渲染。默认行为state每次发生改变的时候组件都会重新渲染。我们要注意在首次渲染或使用forceUpdate()时不会调用该方法。在官方介绍中,说应该考虑使用PureComponent组件,而不是手动编写shouldComponentUpdate()。PureComponent会对props和state进行浅层比较。【注意】 返回 false 并不会阻止子组件在 state 更改时重新渲染。

componentWillUpdate

这个方法很简单,就是当组件收到新的props或state时,会在渲染之前调用这个方法。初始渲染不会调用它。
同样这个方法在React 17中将被废弃。
想要使用应该写成UNSAFE_componentWillUpdate

componentDidUpdate

componentDidUpdate会在更新后会被立即调用。首次渲染不会执行此方法。也可以在这个函数中直接调用setState(),但它必须包裹在一个条件语句中。要不然就会导致死循环。也不要将props镜像给state,应该直接使用props。

componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。
componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。

新的生命周期

新增了两个生命周期getDerivedStateFromPropsgetSnapshotBeforeUpdate
我们先来看看图
React新生命周期

getDerivedStateFromProps

组件每次被rerender的时候,都会触发这个生命周期函数,无论是props更新,还是setState,还是调用forceUpdate(),并且它在组件挂载和后续更新时都会被调用。它应该返回一个对象来更新state,如果返回null则不更新任何内容。注意,getDerivedStateFromProps是一个静态函数,所以函数体内不能访问this,输出完全由输入决定。

getSnapshotBeforeUpdate

它的触发时间实在update发生的时候,render之后,组件dom渲染之前。这个函数的返回值会作为componentDidUpdate的第三个参数。它可以来替代componentWillUpdate。

还有两个生命周期函数

getDerivedStateFromError

这个生命周期函数会在子组件抛出一个错误之后被调用。它会接收到这个throw出来的参数,然后去return一个值去更新state来处理这个错误。设置错误边界可以让代码在出错的情况下,也能将错误显示到页面中,而不是出现空白页面。
一般使用static getDerivedStateFromError() 来渲染一个提示错误的UI,使用componentDidCatch() 来记录一些error的详细信息,错误调用栈等等

componentDidCatch

这个也是在后代组件抛出错误后被调用。 它应该用于记录错误之类的情况。如果要是降级渲染UI,还是应该用getDerivedStateFromError来处理。

父子组件生命周期执行顺序总结

当子组件自身状态改变时,不会对父组件产生副作用的情况下,父组件不会更新,不会触发父组件的生命周期。
当父组件中状态发生变化(包括子组件的挂载以及)时,会触发自身对应的生命周期以及子组件的更新。
还是洋葱圈模型,render 以及 render 之前的生命周期,则 父组件 先执行。render 以及 render 之后的声明周期,则子组件先执行,并且是与父组件交替执行。

Classnames 简单介绍

Classnames是一个开发过程中经常能用到的类库。它的作用就是将多个className拼接到一块,还可以加入条件判断。

仓库地址:https://github.com/JedWatson/classnames

安装方式

1
2
3
4
5
6
7
8
# via npm
npm install classnames

# via Bower
bower install classnames

# or Yarn (note that it will automatically save the package to your `dependencies` in `package.json`)
yarn add classnames

我们可以先来简单地看一下它的用法。

  1. 最基础的两个className拼接,接收多个className返回拼接好的字符串

    1
    classNames('foo', 'bar'); // => 'foo bar'
  2. 也可以接收对象,对象的key作为要拼接的className,key所对应的value为一个布尔值,来表示是否拼接。true拼接,false不拼接。一般用于控制一个元素的显隐。

    1
    2
    classNames('foo', { bar: true }); // => 'foo bar'
    classNames('foo'. { bar: false }); // => 'foo'
  3. 也可以传一个数组,就像这样:

    1
    2
    var arr = ['b', { c: true, d: false }];
    classNames('a', arr); // => 'a b c'
  4. 与ES6的字符串模板配合,使用动态class name:

    1
    2
    let buttonType = 'primary';
    classNames({ [`btn-${buttonType}`]: true });

源码初探

源码很简单,大概30多行。我们可以先来简单的看一下整个源码。

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
var hasOwn = {}.hasOwnProperty;

function classNames() {
var classes = [];

for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) continue;

var argType = typeof arg;

if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
if(arg.length) {
var inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
} else if (argType === 'object') {
if (arg.toString !== Object.prototype.toString) {
classes.push(arg.toString());
} else {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
}

return classes.join(' ');
}

接下来我们来详细分析一下。
var hasOwn = {}.hasOwnProperty;
这一行我们先不看。我们直接进入方法。

  1. 看第一个for循环:
    作用就是遍历这个方法传入的参数,然后判断参数,如果为falsy(如:null,undefined,false,’’)就直接返回。接下来就判断参数类型,根据类型分别进不同的判断条件。这里有两个知识点【arguments对象】和【JS的类型判断】。
  2. 第一个if条件很简单,如果传入参数为string类型或者number类型的话,就直接push到classes中,最后直接join(‘ ‘)即可。
  3. 第二个if,就是判断传入的参数是数组的时候。这里有一点,把判断为数组放在判断是否为对象之前因为type Array 返回的也是”object”,数组也是对象这很正常。这块主要就是递归调用这个方法。也是存在两个知识点【JS的数组判断】和【JS的递归实现
  4. 最后一个if就是当传入的参数为一个对象的时候。
    arg.toString !== Object.prototype.toString就是判断,是否是Object对象。如果不是的话就直接push toString()的结果。
    如果是Object对象的话。直接用for…in遍历它。这里有一个判断是否是对象它自身的属性。如果既是自身属性,属性值又为真的时候。就将这个属性名(也就是key)push到classes数组中。至此整个源码就分析完毕了。在这里if里涉及到的知识点有。【Object.prototype.toString() 与Object.toString()的区别】、【for…in陷阱】、【为什么不直接用hasOwnProperty判断】。

知识点讲解

arguments 对象

常用方法

arguments是一个数组对象,它的prototype并不是指向Array而是指向Object,代表传入function的参数列表。
一般通过数组下标的方式来访问每一个参数,如arguments[0]、arguments[1]]。
通过arguments.length 来获取传入了几个参数。

arguments转数组

在ES6之前,通常用Array.prototype.slice.call(arguments);或者更简单地方法[].slice.call(arguments)来将arguments对象转换为数组。

简单地讲一下,一般slice()方法,一般用于返回一个新的数组对象,这一对象是一个由传入的两个参数begin和end决定的原数组的浅拷贝(包括begin,不包括end)。原数组不会改变。
我们知道JS中,Array也不是真正的Array,也是一个类数组。于是只要我们构建一个类数组,将this绑定到该类数组上,不传入begin和end就可以实现该类数组转数组。 这块比较抽象。
ES6出来之后,我们就可以愉快地用Array.from()方法,来对类数组对象转为数组对象。

另外,有一个需要注意的地方就是,不能将函数的 arguments 泄露或者传递出去。如果将arguments对象泄漏出去了,最终的结果就是V8引擎将会跳过优化,导致相当大的性能损失。

修改arguments的值

这要分严格模式,和非严格模式。
在严格模式下,参数与arguments对象没有联系,修改一个值不会改变另一个值。而在非严格模式下两个会互相影响。
如果参数没有传入,失去绑定关系

用arguments模拟重载

1
2
3
4
5
6
7
8
9
10
11
function add(num1, num2, num3) {
if (arguments.length === 2) {
console.log("Result is " + (num1 + num2));
}
else if (arguments.length === 3) {
console.log("Result is " + (num1 + num2 + num3));
}
}

add(1, 2);
add(1, 2, 3)

caller 与 callee

讲到arguments,就顺带讲一下caller和callee

caller

返回调用指定函数的函数,如果是在全局作用内调用,则返回null。 如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数。
但是arguments.caller已经被废弃,被arguments.callee.caller所代替。

callee

callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。返回的就是fun函数本身。但在严格模式中不允许使用

我们还可以用它来验证传入的参数是否与形参相等

1
2
3
4
5
6
7
function fun(fun1, fun2){
if(arguments.length===arguments.callee.length){//function对象的length属性返回的是参数个数
console.log("参数正确");
} else{
console.log("参数不正确");
}
}

ES6 中的 arguments

  1. 扩展操作符

直接上栗子:

1
2
3
4
5
function func() {
console.log(...arguments);
}

func(1, 2, 3);

执行结果是:

1
1 2 3

简洁地讲,扩展操作符可以将 arguments 展开成独立的参数。

  1. Rest 参数

还是上栗子:

1
2
3
4
5
6
function func(firstArg, ...restArgs) {
console.log(Array.isArray(restArgs));
console.log(firstArg, restArgs);
}

func(1, 2, 3);

执行结果是:

1
2
true   
1 [2, 3]

从上面的结果可以看出,Rest 参数表示除了明确指定剩下的参数集合,类型是 Array。

  1. 默认参数

栗子:

1
2
3
4
5
6
function func(firstArg = 0, secondArg = 1) {
console.log(arguments[0], arguments[1]);
console.log(firstArg, secondArg);
}

func(99);

执行结果是:

1
2
99 undefined   
99 1

可见,默认参数对 arguments 没有影响,arguments 还是仅仅表示调用函数时所传入的所有参数。

JS类型判断

typeof

用typeof可以检测基本类型函数类型

6大原始类型Null、Undefined、String、Number、Boolean和Symbol。
【注意】 typeof null 返回的是 object。

我们来谈谈为什么typeof null会返回object,这是一个历史遗留问题。
js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息

  1. 1:整型(int)
  2. 000:引用类型(object)
  3. 010:双精度浮点型(double)
  4. 100:字符串(string)
  5. 110:布尔型(boolean
    另外还用两个特殊值:
  6. undefined,用整数−2^30(负2的30次方,不在整型的范围内)
  7. null,机器码空指针(C/C++ 宏定义),低三位也是000

所以,typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为0,因此直接被当做了对象来看待。

instanceof

基于原型链进行判断,一般用于判断对象类型。
instanceof左操作数,为一个对象 如果不是对象会直接返回false。右操作数为一个函数构造器。

instanceof可以来判断一个实例是否属于某种类型,也可以判断一个实例是否是其父类型或者祖先类型的实例。

instanceof 主要的实现原理就是只要右边变量的prototype在左边变量的原型链上即可。因此instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

一般用于自定义对象实例判断。

1
2
3
4
5
6
7
8
let Person = function () {
}
let Programmer = function () {
}
Programmer.prototype = new Person()
let liam = new Programmer()
liam instanceof Person // true
liam instanceof Programmer // true

Object.prototype.toString

这是一个不错的判断类型的方法。但是要注意,在IE 6、7、8的时候, null和undefined会返回”[object Object]”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Object.prototype.toString.call(1) // "[object Number]"

Object.prototype.toString.call('hi') // "[object String]"

Object.prototype.toString.call({a:'hi'}) // "[object Object]"

Object.prototype.toString.call([1,'a']) // "[object Array]"

Object.prototype.toString.call(true) // "[object Boolean]"

Object.prototype.toString.call(() => {}) // "[object Function]"

Object.prototype.toString.call(null) // "[object Null]"

Object.prototype.toString.call(undefined) // "[object Undefined]"

Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"

constructor

constructor含义就是指向该对象的构造函数,每个对象都有构造函数,有可能是本身拥有也有可能是继承而来。所有函数和对象最终都是由Function构造函数得来。

1
2
const arr = []
console.log(arr.constructor); // Array

定义一个数组arr,然后我们取arr的constructor
它本身没有constructor属性。
就从arr.proto中查找
arr.proto又指向Array.prototype
有因为Array.prototype.constructor === Array, 所以arr.constructor为Array

但是由于我们可以对原型的指向进行修改。所以检测结果有时并不准确。

duck type

通过特性嗅探来进行类型验证,比如Array类型,就通过判断是否有.sort或者.slice等等数组特有的方法来判断是否为数组。

JS对数组的判断

这个一般会单独拿出来讲,数组中内置了一个数据类型判断函数isArray,但会有兼容性问题。所以一般的写法如下:

1
2
3
isArray = Array.isArray || function(array){
return Object.prototype.toString.call(array) === '[object Array]';
}

JS递归

我们知道JS中实现一个递归跟别的语言一样还是很容易的。

1
2
3
function fn() {
return fn();
}

有一些问题,函数是在定以后绑定到fn这个变量上,如果解释器先解析函数体,再把函数绑到变量上,在解析函数体的时候fn这个变量还处于未定义。

进阶:用匿名函数来实现递归,这时就需要借助callee

1
2
3
function (n) {
return arguments.callee(n - 1);
}

要注意的是,这个在严格模式下不能使用。

Object.toString和Object.prototype.toString之间的区别

我们要注意Object.proto === Function.prototype;因为proto指向的是构造该对象的构造函数的原型。
而构造函数除了是方法它也是对象,而函数的构造函数就是Function,所以Ojbect.proto指向的就是Function.prototype。 而Function.prototype又是对象,它的构造函数就是Object,因此Function.prototype.proto指向的就是Object.prototype。

我们知道,Object本身是没有toString方法的,它会去它的原型链上去找,也就是proto上,于是。就找到了Function.prototype.toString 而不是 Object.prototype.toString。

一般对象都会在原型上实现自己的toString方法。所以源码中利用toString方法来判断是否是一个真正的Object对象。

for in陷阱

我们先来看看for in的定义,以任意顺序遍历一个对象的除Symbol以外的可枚举属性。
首先我们可以知道,一、for in是以任意顺序遍历;二、它遍历可枚举的属性,要注意的是它也会遍历原型链上可枚举的属性。
我们可以使用对象的hasOwnProperty()方法来避免这个问题。而在使用hasOwnProperty()的时候就引出了下面的问题。

为什么不直接用hasOwnProperty判断?

这很简单,因为 JS 不保护属性名hasOwnProperty,你可以轻松地重写它,就像这样。

1
2
3
4
5
var foo = {
hasOwnProperty: function() {
return false;
},
};

所以我们在使用hasOwnProperty的时候这样来使用它。
({}).hasOwnProperty.call 或者 Object.prototype.hasOwnProperty.call

React简介

React是一个UI组件库,核心部分解决的就是声明式渲染和组件问题。

什么是声明式?

声明式编程是一种编程范式,它关注的是你要做什么,而不是如何做。它表达逻辑而不显式地定义步骤也就是说它没有描述具体的步骤。声明式编程的例子有HTML、SQL等。声明式就好像你去饭店点菜,你只需要告诉服务员你吃什么,并不需要关心后厨是怎么做的。

什么是命令式?

命令式是与声明式相对的,声明式描述了应该做什么,而命令式是描述了如何做。它会具体的描述每一个步骤。命令式就像自己照着食谱做饭,先洗菜,切菜,倒油,炒它,加调料,装盘。精确地定义好每一步,然后去实施。

什么是函数式编程?

函数式是声明式的一部分,既然是声明式的一部分,函数式也更加强调程序执行的结果而非执行的过程。JavaScript中的函数是第一类公民,就意味着函数跟其它的数据类型一样处于平等的地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值(这就是高阶函数)。函数式编程的目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用并减少对状态的改变。
我们可以看一下函数式的几个重要的概念。

不可变数据

在函数式编程中,不能更改数据。如果要改变或者更改数据,则必须复制数据副本来更改。

纯函数,拒绝副作用

纯函数实际上就是,没有副作用的函数,相同的输入有相同的输出。

引用透明

函数的返回值只依赖于其输入值。

React走的就是函数式宗教,在React声明式渲染中,有一个公式就是UI = render(data)开发者只需要维护可变的数据,React会帮助我们处理具体的DOM操作。

JSX

JSX是学习React必须知道的一个概念,JSX是一个语法糖,它既不是字符串也不是HTML,它是JavaScript的语法扩展。它就像一个拥有JavaScript所有功能的模板引擎。
React UI与逻辑 高耦合。与传统的html、css、js三种语言分在三种不同的文件里面不同。React根据同一件事,把实现这个功能的所有代码集中在一个文件里。

JSX中的onClick事件与HTML的onclick的处理方式有很大的不同。

JSX最终会通过Babel转译成为一个名为React.createElement()的函数调用。这就是为什么只要使用JSX就必须import React。

JSX防止注入攻击
React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

元素的渲染。

React 元素是创建开销极小的普通对象。React DOM会负责更新DOM来与React元素保持一致。React只更新它需要更新的部分,ReactDOM会将元素和它的子元素的状态进行比较,只会进行必要的更新。

render

想要把一个React元素渲染到根DOM节点中,只需要把它们一起传入ReactDOM.render();
ReactDOM.render(element, container[, callback])
在container里渲染一个React元素,callback是可选的。将在组件渲染或者更新之后被执行。

render会控制你传入容器节点里的内容。当首次调用时,容器节点里的所有DOM元素都会被替换点。但是不会修改容器节点。
会返回对根组件ReactComponent实例的引用。但是要避免使用返回的引用。因为之后React版本中,组件渲染在某些情况下可能是异步的。

unmountComponentAtNode()

ReactDOM.unmountComponentAtNode(container)
从DOM中卸载组件,会将其event handlers和state一并清除。如果组件被移除返回true,如果没有组件可被移除将会返回false。

createPortal

Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。
ReactDOM.createPortal(child, container)
第一个参数是任何可渲染的React子元素,第二个参数则是一个DOM元素。

函数组件与class组件。

组件是React中重要的概念。这样就允许将你的UI拆分为独立可复用的代码片段。之前我们介绍过React的核心就是UI=render(data), 它接收任意的props,返回用于展示内容的React元素。而React中定义组件的两种方式

  1. 函数组件

    1
    2
    3
    function Welcome(props) {
    return <h1>Hello, {props.name}</h1>
    }
  2. 用ES6的class来定义组件

    1
    2
    3
    4
    5
    class Welcome extends React.Component {
    render() {
    return <h1>Hello, {this.props.name}</h1>
    }
    }

Props

讲到组件就离不开props。props的定义是,当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。
通过定义我们可以知道,通过props可以获得两个东西,一是组件的属性,二是子组件。

<Component name="name"> // props.name 来取得name属性上的值
    <SubComponent> // props.children 来获取子组件
</Component>

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

State

State是私有的,并且完全受控于当前组件。state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。他与props类似,它的更改会触发reRender。
在使用state的时候要注意以下几点。

  • 不要直接修改State
    直接修改State,代码不会重新渲染组件。
    应该使用setState();
  • State的更新可能是异步的
    这点很重要,因为this.props和this.state可能会异步更新,所以不要依赖他们的值来更新下一个状态。
    要是解决这个问题,可以让setState接收一个函数,而不是对象。这个函数第一个参数是上一个state,此次更新被应用时的props做为第二个参数。
  • State的更新会被合并
    简单地说就是,state中包含多个独立的变量,可以在setState中单独个更新它们,setState中只用写其中的一个变量。它会完整保留其他变量,并完全替换了你要单独改变的变量。

Babel是一个广泛使用的转码器,我觉得Babel这个名字起得非常的好,有个神话故事,据说之前人们想要建造一个通天塔,神为了阻止他们,讲他们分散到各个地方,让彼此语言不通,这就导致了这个塔无法继续建造。这个塔就巴别塔。

Jest 钩子函数

我们如果有一些工作是在每次跑测试用例之前或者结束的时候要做的。我们就需要使用Jest提供的beforeEachafterEach
如果我们只想跑一次的话,那么我们就要用Jest提供的beforeAllafterAll这两个函数会在所有测试用例开始之前,和全部结束之后调用。

我们可以通过具体代码看一下。

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
beforeEach(() => {
console.log('beforeEach');
})

afterEach(() => {
console.log('afterEach');
})

beforeAll(() => {
console.log('beforeAll');
})

afterAll(() => {
console.log('afterAll');
})

test('test', () => {
console.log('test');
})

test('test1', () => {
console.log('test1');
})

test('test2', () => {
console.log('test2');
})

输出结果

1
2
3
4
5
6
7
8
9
10
11
beforeAll
beforeEach
test
afterEach
beforeEach
test1
afterEach
beforeEach
test2
afterEach
afterAll

通过打印的内容我们就能很直观的看到,beforeAll和afterAll在整个测试文件的开头和结尾调用一次。beforeEach和afterEach会在每个测试用例的开始和结束都会调用。

Jest 作用域

我们可以用describe来将测试分组。当afterbeforedescribe内部的时候,只适用于该describe内部的测试,但是顶级的beforeEachafterEach也会作用在该describe内部的测试用例。但是顶部的beforeEach会比内部的先执行,顶部的afterEach会比内部的晚执行。具体可以看官网给的例子。

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
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll'));
afterAll(() => console.log('2 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('2 - afterEach'));
test('', () => console.log('2 - test'));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll

describe 和 test 的执行顺序

Jest会在具体test代码块之前执行所有describe处理器部分,所以我们要将准备工作都放在before、after中。

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
describe('outer', () => {
console.log('describe outer-a');

describe('describe inner 1', () => {
console.log('describe inner 1');
test('test 1', () => {
console.log('test for describe inner 1');
expect(true).toEqual(true);
});
});

console.log('describe outer-b');

test('test 1', () => {
console.log('test for describe outer');
expect(true).toEqual(true);
});

describe('describe inner 2', () => {
console.log('describe inner 2');
test('test for describe inner 2', () => {
console.log('test for describe inner 2');
expect(false).toEqual(false);
});
});

console.log('describe outer-c');
});

// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test for describe inner 1
// test for describe outer
// test for describe inner 2

Jest Mock函数

在平时,我们需要测试一些回调函数是否被调用,而我们不关心调用内部的执行过程和结果,这时我们就需要jest.fn来mock一个函数。

1
2
3
4
5
6
7
const mockCallback = jest.fn();
forEach([0, 1], mockCallback);

test('该模拟函数被调用了两次', () => {
// 此模拟函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);
})

我们可以看看.mock的属性都有哪些。

  1. calls: 每次调用传入的参数
  2. instances: 每次调用时this的值,
  3. invocationCallOrder: 每次调用的执行顺序
  4. results: 每次调用的返回值

如果我们想要取调用的参数可以这样写
expect(mockCallback.mock.calls[0][0])这是第一次调用的参数。

还可以通过mockReturnValueOnce()mockReturnValue()来控制返回值。

对于API的调用,我们不必等待API的返回结果,因为这非常耗时,我们可以直接用jest.mock()来模拟axios模块,可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。 实际上,我们想让 axios.get(‘/users.json’) 有个假的 response。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);

// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))

return Users.all().then(data => expect(data).toEqual(users));
});

Jest Snapshot 快照

快照是我个人觉得非常有用的功能,一般用于测试配置文件,UI有没有改动。
我们先通过测试配置文件来讲解测试快照。之后会具体讲使用snapshot来测试UI组件。

snapshot的原理简单的来说,就是第一次测试的时候生成一个快照文件,第二次跑测试用例的时候,会与这个快照进行对比,如果有变化则测试不通过,当然你也可以更新快照,这时快照的内容就是你最新的改变。

Jest VSCode插件

推荐使用 Facebook官方出的 vscode-jest 插件。 安装完插件后,不用每次都手动输入jest命令。每次回自动执行测试用例。并在通过的测试用例前用绿色的小点表示,失败的为红色小点。

Jest

Jest是Facebook开源的测试框架,几乎是0配置直接进行单元测试,相对其他测试框架,其一大特点就是内置了常用的测试工具,比如自带断言、测试覆盖率等工具。Jest还有很多好处这里就不一一介绍了,详情可以到Jest官网查看https://jestjs.io/

Jest 安装

这里默认安装了node。 如果没有安装的,可自行到Node官网进行下载安装https://nodejs.org/。安装好Node后是默认自带NPM包管理工具的。我们可以通过 node -vnpm -v 来检测node、和npm是否安装成功,如果都成功的显示了版本号,就说明node和npm都安装成功了。下面我们就可以对Jest来进行安装。

Jest的安装十分简单,就一行命令

npm install --save-dev jest

因为只有在开发的时候我们才去运行测试用例,所以我们在安装Jest的时候加上--save-dev。安装完成后我们就可以进行Jest的学习了。

Jest初体验

我们可以根据Jest官网给的简单的例子,来体验一下Jest。

我们写一个需要被测试的函数,这个函数就是做一个简单的两个数相加的运算然后返回结果。首先我们先创建一个名为 sum.js 的js文件:

1
2
3
4
function sum(a, b) {
return a + b;
}
module.exports = sum;

我们用module.exports将这个函数导出
然后我们创建一个名为 sum.test.js的js文件,这个文件里写我们真正的测试代码:

1
2
3
4
5
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

然后我们修改 package.json 文件,使得我们输入 npm test 的时候就可以运行我们的测试用例:

1
2
3
4
5
{
"scripts": {
"test": "jest"
}
}

最后我们输入npm run test来运行我们的测试用例,并可以看到Jest打印的信息:

1
2
PASS  ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

至此我们就成功的用Jest写了第一个测试用例。

我们可以看到这段测试代码的核心就两条语句

  1. 调用一个test方法,第一个参数是对这个测试用例的描述,第二参数是一个回调函数,里面是具体的测试方法。
  2. expect(resultValue).toBe(actualValue) 这行代码,我们可以看做,当expect方法中的传入的这个值和toBe这个方法中的值完全相等。这条测试用例就算通过(例子中,resultValue是调用sum函数的返回值,actualValue是我们认为函数运行正确应该返回的值)

我们可以简单地试着实现一下这两行代码,这样能更好的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function expect(resultValue) { //函数的返回值
return {
//返回一个对象,其中有toBe方法,toBe方法接收真实值。
toBe: function(actualValue) {
//判断函数的返回值,与真实值是否相等,如果不相等,抛出错误。
if (resultValue !== actualValue) {
throw new Error('Error');
}
}
}
}

function test(description: string, fn: Function) {
try {
fn(); //执行函数fn,如果没有抛出错误,输出PASS
console.log(`PASS`);
} catch(e) {
//如果fn抛出错误,将在这里捕获,并输出错误信息。
console.log(`${description} : ${e}`)
}
}

我们可以简单的理解为,expect(resultValue).toBe(actualValue) 就是对上述代码的简化。它的实质其实就是比较。expect函数中resultValue和toBe函数中actualValue是否完全相等。之后会介绍Jest中更多的方法。

配置你的Jest

首先 我们输入下面命令,来生成Jest的配置文件。
npx jest --init

注意,我们这里使用的是npx,不是npm,npx的意思是运行的时候,会到 node_modules/.bin 路径和环境变量 $PATH 里面,检查命令是否存在。 而不是去全局环境查找命令。

输入完命令后会有几个问题让你回答,来创建基本的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
Would you like to use Jest when running "test" script in "package.json"? › (Y/n)
这个就是帮你配置package.json当,npm test的时候执行Jest。

Choose the test environment that will be used for testing › - Use arrow-keys. Return to submit.
❯ node
jsdom (browser-like)
选择node环境还是浏览器环境

Do you want Jest to add coverage reports? › (y/N)
是否增加测试覆盖率报告

Automatically clear mock calls and instances between every test? › (y/N)
每次运行测试时自动清除所有mock

回答完问题之后,会生成一个 jest.config.js 的配置文件。我们可以在里面进行Jest更多的配置。我们将会在之后专门来讲Jest的配置,以及通过babel支持TypeScript和ES Modules。

Jest –watch 监视模式运行

  • --watch 监听变化的测试用例,如果想一个文件改变而运行全部测试用例的话需要使用--watchAll

Jest Matchers 常用匹配器

匹配器(Matchers)是Jest中非常重要的一个概念,它可以提供很多种方式来让你去验证你所测试的返回值

Truthiness

  • toBe 匹配器:相当于 Object.is 或者 ===
  • toEqual 匹配器: 只匹配内容,不匹配引用。
  • toBeNull 匹配器: 内容是否等于Null
  • toBeUndefined 匹配器: 内容是否等于undefined
  • toBeDefined 匹配器: 希望内容是定义过的。
  • toBeTruthy 匹配器: 内容是否为true。
  • toBeFalsy 匹配器: 内容是否为false。
  • not 匹配器: 在其他匹配器之前,相当于取反操作

与数字相关

  • toBeGreaterThan 匹配器: 输入的数字是否大于
  • toBeGreaterThanOrEqual 匹配器: 输入的数字是否大于等于
  • toBeLessThan 匹配器: 输入的数字是否小于
  • toBeLessThanOrEqual 匹配器: 输入的数字是否小于等于
  • 对于浮点数判断相等,为了解决浮点数的bug。要用toBeCloseTo匹配器。

与String相关

  • toMatch 匹配器: 结果中是否包含内容,可以是String也可以是正则

与Array相关

  • toContain 匹配器: 判断元素是否存在数组中。

与异常相关

  • toThrow匹配器来判断在调用一个函数出现异常时,这个函数是否抛出了异常。

Jest测试异步函数

callback

我们先看下面的测试用例

1
2
3
4
5
6
7
test('testing asynchronous code', () => {
function callback(data) {
expect(data).toBe('success');
}

fetchData(callback);
});

在上面这个函数中,fetchData是一个异步的方法,去请求数据,当数据返回时调用,callback函数。我们期望这个异步函数的返回值是”success“。但是Jest并不知道这个异步函数什么时候返回。Jest仅仅只是从头执行到尾。这样这个测试用例是无效的。

为了解决这个问题。Jest提供了一个 done 参数,这个参数通过test函数的回调方法传进去,done 是一个不接受任何参数的方法。具体怎么用我们来看代码

1
2
3
4
5
6
7
8
test('testing asynchronous code with done', done => {
function callback(data) {
expect(data).toBe('success');
done();
}

fetchData(callback);
})

我们对比两个代码可以看出,下面这块代买仅仅是在回调函数中多执行了一句done()它的意思就是告诉Jest,只有运行到了done()这个命令的时候,这个test才算完事。如果一直不调用done()的话会报出超时错误。
但是这里还有点问题。就是当测试不通过的时候,会到的done不被调用。这时我们还需要改写一下代码,用try/catch 来捕获expect错误从而实现我们这个需求。

1
2
3
4
5
6
7
8
9
10
11
12
test('testing asynchronous code with done', done => {
function callback(data) {
try {
expect(data).toBe('success');
done();
} catch (e) {
done(e);
}
}

fetchData(callback);
})

Promises

如果使用Promise,会简单很多。我们只需要把 promise返回, Jest会等待promise resolve。 如果 rejected 的话,这条测试用例会自动不通过。代码如下。

1
2
3
4
5
test('testing asynchronous code', () => {
return fetchData().then(data => {
expect(data).toBe('success');
});
});

如果你用catch捕获了rejected。 那就一定要添加assertions匹配器,assertions匹配器接收一个参数,这个参数表示expect的次数,如果没有出现指定的次数,就会报错。我们改写一下上面的方法。

1
2
3
4
test('the fetch fails with an error', () => {
expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});

resolves / rejects 匹配器

我们也可以用 resolves和rejects匹配器。我们直接看代码

1
2
3
4
5
6
7
test('testing asynchronous code', () => {
return expect(fetchData()).resolves.toBe('success');
});

test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});

Async/Await

最后我们可以用,Await来等待异步函数执行完成,我们只需要把test中的回调函数改写成async/await形式即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});

至此我们简单的介绍了Jest的基本用法。更多的用法我们会在下节讲到。

为什么需要构建工具?
主要是为了实现转换ES6语法、转换JSX、CSS前缀补全/预处理器、压缩混淆、图片压缩等功能。

webpack 安装

npm install webpack webpack-cli --save-dev
安装完成后,我们可以通过npx webpack -v来检测webpack是否安装成功。

webpack 核心概念

webpack配置文件

webpack 默认配置文件: webpack.conf.js
可以通过 webpack –config 指定配置文件。

Entry 打包的入口文件

指定打包的入口,告诉webpack应该使用哪个模块,来作为入口文件,webpack会找出哪些模块库是入口的直接或间接依赖。

默认入口是./src/index/js,我们也可以配置来指定一个或多个不同的入口。

简单例子:

1
2
3
module.exports = {
entry: './path/to/my/entry/file.js'
};

这是作为单文件入口时,其实是下面的简写。

1
2
3
4
5
module.exports = {
entry: {
main: './path/to/my/entry/file.js'
}
}

entry的值可以是一个字符串,也可以是一个数组,为数组时,表示有多个主入口,想要多个依赖文件一起注入时,可以用数组。
如果是多页应用的话,也就是多入口的话entry就得写成一个对象。

1
2
3
4
5
6
module.exports = {
entry: {
app: './path/to/my/entry/app.js',
adminApp: './path/to/my/entry/adminApp.js'
}
}

Output 打包的输出

告诉webpack在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。我们也可以来配置。下面是官网给出的例子。

1
2
3
4
5
6
7
8
9
const path = require('path');

module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};

这其中涉及到一些Node的知识,__dirname我们可以把它当做node的一个全局变量,它的值为当前目录。我们在最上面引入了path模块,调用了path中的resolve方法。这个方法主要是从左到右拼接出一个绝对路径。注:若字符以 / 开头,不会拼接到前面的路径(因为拼接到此已经是一个绝对路径)
output中的path属性就是输出文件的地址,filename则是输出文件名。
如果entry是多入口配置的话,output就需要通过占位符确保文件名称的唯一。如下

1
2
3
output: {
filename: '[name].js'
}

Loader

webpack 只能理解 JavaScript 和 JSON 文件。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。
我们来配置一下,让webpack支持TypeScript,老规矩先上代码。

1
2
3
4
5
6
7
8
9
10
11
12
const path = require('path');

module.exports = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader' }
]
}
};

在module对象中有一个rules属性,里面定义一个数组,数组中的对象的test属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。这里用正则表示所有ts或者tsx结尾的文件。use属性,表示进行转换时,应该使用哪个 loader,我们这里用的是ts-loader。现在我们就可以让webpack帮我们将TypeScript转为JavaScript。

Plugins 插件配置

loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务,增强webpack。包括:打包优化,资源管理,注入环境变量。
我们如果使用一个插件,就需要require()它,然后把它添加到plugins数组中,如果我们想要多次用一个插件,这时我们就可以用new来创建一个它的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins

module.exports = {
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};

上面插件的作用就是将生成的bundle注入到指定的HTML文件中。

Mode 环境

webpack4新概念,通过选择 development, production 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production。

1
2
3
module.exports = {
mode: 'production'
};