Redux 笔记

介绍

动机

JavaScript 需要更多的 stateRedux 试图让 state 的变化变得可预测。

三大原则

单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store

State 是只读的

惟一改变 state 的方法就是触发 actionaction(表达想要修改的意图) 是一个用于描述已发生事件的普通对象。

使用纯函数来执行修改

为了描述 action 如何改变 state tree ,你需要编写 reducers

Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。

基础

Action

Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。

例如:

1
2
3
4
{
type: ADD_TODO,
text: 'Build my first Redux app'
}

action 对象的结构完全由自己决定,但是为了多人合作方便,最好遵循一定的标准 Flux 标准 Action

store 里能直接通过 store.dispatch() 调用 dispatch()

但是多数情况下你会使用 react-redux 提供的 connect() 帮助器来调用。bindActionCreators() 可以自动把多个 action 创建函数 绑定到 dispatch() 方法上。

Reducer

处理 action

reducer 就是一个纯函数,接收旧的 stateaction,返回新的 state。持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:

  • 修改传入参数
  • 执行有副作用的操作,如 API 请求和路由跳转
  • 调用非纯函数,如 Date.now() 或 Math.random()

建议你尽可能地把 state 范式化,不存在嵌套。把应用的 state 想像成数据库。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

注意:

  • 不要修改 state,使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用 { ...state, ...newState } 达到相同的目的。
  • default 情况下返回旧的 state

拆分 Reducer

Redux 提供了 combineReducers() 工具类来合并拆分的 Reducer

Store

Redux 应用只有一个单一的 store

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

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

createStore() 的第二个参数是可选的, 用于设置 state 初始状态。

数据流

严格的单向数据流是 Redux 架构的设计核心。

Redux 应用中数据的生命周期遵循下面 4 个步骤:

  1. 调用 store.dispatch(action)
  2. Redux store 调用传入的 reducer 函数。

    Store 会把两个参数传入 reducer: 当前的 state 树和 action。

  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

搭配 React

安装 React Redux

Redux 默认并不包含 React 绑定库,需要单独安装。

1
npm install --save react-redux

connect()

我们想要通过 react-redux 提供的 connect() 方法将包装好的组件连接到 Redux。尽量只做一个顶层的组件,或者 route 处理。从技术上来说你可以将应用中的任何一个组件 connect()Redux store 中,但尽量避免这么做,因为这个数据流很难追踪。

任何一个从 connect() 包装好的组件都可以得到一个 dispatch 方法作为组件的 props,以及得到全局 state 中所需的任何内容。connect() 的唯一参数是 selector。此方法可以从 Redux store 接收到全局的 state,然后返回组件中需要的 props。最简单的情况下,可以返回一个初始的 state (例如,返回认证方法),但最好先将其进行转化。

selector 的作用就是为 React Components 构造适合自己需要的状态视图。是一个自定义函数,selector 的实现完全是自定义函数

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
import { connect } from 'react-redux';
...

class App extends Component {
render() {
// 通过调用 connect() 注入:
const { dispatch, visibleTodos, visibilityFilter } = this.props
return (
...
)
}
}

App.propTypes = {
...
}

// 基于全局 state ,哪些是我们想注入的 props ?
// 注意:使用 https://github.com/reactjs/reselect 效果更佳。
function select(state) {
return {
visibilityFilter: state.visibilityFilter
};
}

// 包装 component ,注入 dispatch 和 state 到其默认的 connect(select)(App) 中;
export default connect(select)(App);

reselect 这个项目提供了带 cache 功能的 selector。如果 Store/State 和构造 view 的参数没有变化,那么每次 Component 获取的数据都将来自于上次调用/计算的结果。得益于 Store/State Immutable 的本质,状态变化的检测是非常高效的。

高级

异步Action

当调用异步 API 时,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻 (也可能是超时)。

这两个时刻都可能会更改应用的 state;为此,你需要 dispatch 普通的同步 action。一般情况下,每个 API 请求都需要 dispatch 至少三种 action:

  1. 一种通知 reducer 请求开始的 action。

    对于这种 action,reducer 可能会切换一下 state 中的 isFetching 标记。以此来告诉 UI 来显示进度条。

  2. 一种通知 reducer 请求成功结束的 action。

    对于这种 action,reducer 可能会把接收到的新数据合并到 state 中,并重置 isFetching。UI 则会隐藏进度条,并显示接收到的数据。

  3. 一种通知 reducer 请求失败的 action。

    对于这种 action,reducer 可能会重置 isFetching。或者,有些 reducer 会保存这些失败信息,并在 UI 里显示出来。

为了区分这三种 action,可能在 action 里添加一个专门的 status 字段作为标记位:

1
2
3
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

又或者为它们定义不同的 type:

1
2
3
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

异步 Action Creator

如何把之前定义的同步 action creator 和 网络请求结合起来呢?标准的做法是使用 Redux Thunk middleware。要引入 redux-thunk 这个专门的库才能使用。

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
import fetch from 'isomorphic-fetch'

export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}

// 来看一下我们写的第一个 thunk action creator!
// 虽然内部操作不同,你可以像其它 action creator 一样使用它:
// store.dispatch(fetchPosts('reactjs'))

export function fetchPosts(subreddit) {

// Thunk middleware 知道如何处理函数。
// 这里把 dispatch 方法通过参数的形式传给函数,
// 以此来让它自己也能 dispatch action。

return function (dispatch) {

// 首次 dispatch:更新应用的 state 来通知
// API 请求发起了。

dispatch(requestPosts(subreddit))

// thunk middleware 调用的函数可以有返回值,
// 它会被当作 dispatch 方法的返回值传递。

// 这个案例中,我们返回一个等待处理的 promise。
// 这并不是 redux middleware 所必须的,但这对于我们而言很方便。

return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json =>

// 可以多次 dispatch!
// 这里,使用 API 请求结果来更新应用的 state。

dispatch(receivePosts(subreddit, json))
)

// 在实际应用中,还需要
// 捕获网络请求的异常。
}
}

本示例使用了 fetch API。它是替代 XMLHttpRequest 用来发送网络请求的非常新的 API。由于目前大多数浏览器原生还不支持它,建议你使用 isomorphic-fetch 库:

在底层,它在浏览器端使用 whatwg-fetch polyfill,在服务器端使用 node-fetch,所以如果当你把应用改成同构时,并不需要改变 API 请求。

评论和共享

Mocha 基本用法

安装

1
$ npm install --global mocha

例子

1
2
3
4
5
6
7
8
var add = require('./add.js');
var expect = require('chai').expect;

describe('加法函数的测试', function() {
it('1 加 1 应该等于 2', function() {
expect(add(1, 1)).to.be.equal(2);
});
});

describe 块称为”测试套件”,表示一组相关的测试。可以嵌套多层。

it 块称为”测试用例”,表示一个单独的测试,是测试的最小单位

通常,测试脚本与所要测试的源码脚本同名,但是后缀名为 .test.js (表示测试)或者 .spec.js (表示规格)

断言库

Mocha 本身不带断言库,所以必须先引入断言库。上面代码引入的断言库是 chai,并且指定使用它的 expect 断言风格。

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
// 相等或不相等
expect(4 + 5).to.be.equal(9);
expect(4 + 5).to.be.not.equal(10);
expect(foo).to.be.deep.equal({ bar: 'baz' });

// 布尔值为true
expect('everthing').to.be.ok;
expect(false).to.not.be.ok;

// typeof
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
expect(foo).to.be.an.instanceof(Foo);

// include
expect([1,2,3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');

// empty
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;

// match
expect('foobar').to.match(/^foo/);

参数

--recursive

Mocha 默认只执行 test 子目录下面第一层的测试用例,不会执行更下层的用例。

为了改变这种行为,就必须加上 --recursive 参数

1
mocha --recursive

--watch,-w

视指定的测试脚本。只要测试脚本有变化,就会自动运行Mocha

配置文件 mocha.opts

Mocha 允许在test目录下面,放置配置文件 mocha.opts,把命令行参数写在里面。

然后,把这参数写入test目录下的mocha.opts文件。

1
2
3
--reporter tap
--recursive
--growl

然后,执行 mocha 就能取得一样的效果。

ES6 测试

如果测试脚本是用ES6写的,那么运行测试之前,需要先用Babel转码。

1
2
3
4
5
6
7
8
9
10
import add from '../src/add.js';
import chai from 'chai';

let expect = chai.expect;

describe('加法函数的测试', function() {
it('1 加 1 应该等于 2', function() {
expect(add(1, 1)).to.be.equal(2);
});
});

ES6转码,需要安装Babel。

1
$ npm install babel-core babel-preset-es2015 --save-dev

然后,在项目目录下面,新建一个 .babelrc 配置文件。

1
2
3
4

{
"presets": [ "es2015" ]
}

最后,使用 --compilers 参数指定测试脚本的转码器。

1
mocha --compilers js:babel-core/register

Babel默认不会对Iterator、Generator、Promise、Map、Set等全局对象,以及一些全局对象的方法(比如Object.assign)转码。如果你想要对这些对象转码,就要安装babel-polyfill。

1
$ npm install babel-polyfill --save

然后,在你的脚本头部加上一行。

1
import 'babel-polyfill'

异步测试

Mocha 默认每个测试用例最多执行 2000 毫秒,如果到时没有得到结果,就报错。对于涉及异步操作的测试用例,这个时间往往是不够的,需要用 -t--timeout 参数指定超时门槛。

1
2
3
4
5
6
7
8
9
it('测试应该5000毫秒后结束', function(done) {
var x = true;
var f = function() {
x = false;
expect(x).to.be.not.ok;
done(); // 通知Mocha测试结束
};
setTimeout(f, 4000);
});
1
$ mocha -t 5000 timeout.test.js

测试用例的钩子

Mochadescribe 块之中,提供测试用例的四个钩子: before()after()beforeEach()afterEach()。它们会在指定时间执行。

  • before() 在本区块的所有测试用例之前执行
  • after() 在本区块的所有测试用例之后执行
  • beforeEach() 在本区块的每个测试用例之前执行
  • afterEach() 在本区块的每个测试用例之后执行

评论和共享

如果不阻止默认行为,使用 {passive: true} 能够有效的增加流畅度

passive 的事件监听器

很久以前,addEventListener() 的参数约定是这样的:

1
addEventListener(type, listener, useCapture)

后来,最后一个参数,也就是控制监听器是在捕获阶段执行还是在冒泡阶段执行的 useCapture 参数,变成了可选参数(传 true 的情况太少了):

1
addEventListener(type, listener[, useCapture ])

2015 年底,DOM 规范做了修订:addEventListener() 的第三个参数可以是个对象值了,也就是说第三个参数现在可以是两种类型的值了:

1
2
addEventListener(type, listener[, useCapture ])
addEventListener(type, listener[, options ])

这个修订是为了扩展新的选项,从而自定义更多的行为,目前规范中 options 对象可用的属性有三个:

1
2
3
4
5
addEventListener(type, listener, {
capture: false,
passive: false,
once: false
})

三个属性都是布尔类型,默认值都为 false

  1. capture 属性等价于以前的 useCapture 参数
  2. once 属性就是表明该监听器是一次性的,执行一次后就被自动 removeEventListener 掉,还没有浏览器实现它;
  3. passive 属性 Firefox 和 Chrome 已经实现

浏览器无法预先知道一个监听器会不会调用 preventDefault(),它能做的只有等监听器执行完后再去执行默认行为,而监听器执行是要耗时的,有些甚至耗时很明显,这样就会导致页面卡顿。视频里也说了,即便监听器是个空函数,也会产生一定的卡顿,毕竟空函数的执行也会耗时。

有 80% 的滚动事件监听器是不会阻止默认行为的,也就是说大部分情况下,浏览器是白等了。所以,passive 监听器诞生了,passive 的意思是“顺从的”,表示它不会对事件的默认行为说 no,浏览器知道了一个监听器是 passive 的,它就可以在两个线程里同时执行监听器中的 JavaScript 代码和浏览器的默认行为了。

如果不阻止默认行为,使用 {passive: true} 能够有效的增加流畅度

假如不小心在 passive 的监听器里调用了 preventDefault(),也无妨,因为 preventDefault() 不会产生任何效果,但开发者工具会发出警告。

Chrome 发现耗时超过 100 毫秒的非 passive 的监听器,警告你加上 {passive: true}

何时调用 removeEventListener

第三个参数是布尔值的时候,addEventListener("foo", listener, true) 添加的监听器,必须用 removeEventListener("foo", listener, true) 才能删除掉。因为这个监听器也有可能还注册在了冒泡阶段,那样的话,同一个监听器实际上对应着两个监听器对象

  • 如果要移除事件句柄,addEventListener() 的执行函数必须使用外部函数
  • 类似 document.removeEventListener("event", function(){ myScript }); 该事件是无法移除的。
1
2
3
4
5
6
7
8
9
var div1 = document.getElementById("div1");

function listener(e){
console.log("div listener")
}

div1.addEventListener('mousedown', listener, true);

div1.removeEventListener("mousedown", listener, true);

那现在 addEventListener("foo", listener, {passive: true}) 添加的监听器只要 removeEventListener("foo", listener) 就可以了

所以说在 removeEventListener 的时候永远不需写 passive 和 once,但 capture 可能要

评论和共享

React Touch 事件

兼容性

经测试,兼容 ie9+ 主流浏览器

手机单击触发事件

Chrome

onTouchStart -> onTouchEnd -> onMouseDown -> onMouseUp

Firefox

onTouchStart -> onTouchEnd -> onMouseDown -> onMouseMove -> onMouseUp

手机触摸移动触发事件

Chrome

onTouchStart -> onTouchMove -> onTouchEnd

Firefox

onTouchStart -> onTouchMove -> onTouchEnd -> onMouseDown -> onMouseMove -> onMouseUp

获取坐标方式

事件 方法
onMouseDown const {pageX, pageY} = e;
onMouseMove const {pageX, pageY} = e;
onMouseUp const {pageX, pageY} = e;
onTouchStart const {pageX,pageY} = e.touches[0];
onTouchMove const {pageX, pageY} = e.touches[0];
onTouchEnd const {clientX, clientY} = e.changedTouches[0];

示例

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
import React,{ Component } from 'react';
import styles from './App.css';

class App extends Component {
constructor(props) {
super(props);
this.checked = false;

this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);

this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
}

handleTouchStart(e){
const {pageX,pageY} = e.touches[0];
console.log("handleTouchStart", pageX, pageY);
}

handleTouchMove(e){
const {pageX, pageY} = e.touches[0];
console.log("handleTouchMove", pageX, pageY);
}

handleTouchEnd(e){
const {clientX, clientY} = e.changedTouches[0];
console.log("handleTouchEnd", clientX, clientY);
}

handleMouseDown(e){
this.checked = true;
console.log("handleMouseDown", e.pageX, e.pageY);
}

handleMouseMove(e){
if(this.checked)
console.log("handleMouseMove", e.pageX, e.pageY);
}

handleMouseUp(e){
this.checked = false;
console.log("handleMouseUp", e.pageX, e.pageY);
}

render(){
return (
<div className={styles.app}>
<h2>Hello, Nice</h2>
<div className={styles.touchBox}
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onMouseUp={this.handleMouseUp}

onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
/>
</div>
)
}
}

export default App;

评论和共享

Git 常用命令

安装

源码安装:

先安装依赖库

1
2
3
4
$ sudo yum install curl-devel expat-devel gettext-devel \
openssl-devel zlib-devel
$ sudo apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \
libz-dev libssl-dev

为了能够添加更多格式的文档(如 doc, html, info),安装依赖包:

1
2
$ sudo yum install asciidoc xmlto docbook2x
$ sudo apt-get install asciidoc xmlto docbook2x

编译安装:

1
2
3
4
5
6
$ tar -zxf git-2.0.0.tar.gz
$ cd git-2.0.0
$ make configure
$ ./configure --prefix=/usr
$ make all doc info
$ sudo make install install-doc install-html install-info

更新 git

1
$ git clone git://git.kernel.org/pub/scm/git/git.git

常用命令

命令 描述
git init 创建版本库
git add <file> 把文件添加到仓库
git add -A 全部添加到仓库
git commit -m "message" 提交到仓库,并添加描述
git status 查看当前仓库状态
git diff 工作区(work dict)和暂存区(stage)的比较
git diff --cached 暂存区(stage)和分支(master)的比较
git diff HEAD -- <file> 查看工作区和版本库里面最新版本的区别
git log 查看提交历史
git reflog 记录操作命令
git reset --hard HEAD~n 回退n个版本
git reset --hard $id 回到 $id 版本
git checkout -- <file> 丢弃工作区的修改
git reset HEAD <file> 把暂存区的修改撤销掉

远程命令

命令 描述
git remote add origin <git@address> 关联远程库
git push -u origin master 把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来
git push origin master 推送到远程
git clone <git@address> 克隆远程库到本地
git fetch 将远程分支信息获取到本地

分支管理

命令 描述
git branch 查看分支信息,带*为当前
git checkout -b <branch-name> 创建并切换到新分支
git checkout -b dev origin/dev 创建远程 origindev 分支到本地
git branch --set-upstream dev origin/dev 设置dev和origin/dev的链接
git branch <branch-name> 创建分支
git checkout <branch-name> 切换分支
git merge <branch-name> 合并某分支到当前分支
git merge --no-ff -m "<message>" <branch-name> 禁用Fast forward合并,可以看到合并历史
git branch -d <branch-name> 删除分支
git branch -D <branch-name> 丢弃一个没有合并的分支
git log --graph 查看分支合并图

分支策略

分支 描述
master 稳定,仅用来发布新版本,不能在上面干活
dev 不稳定,到达新版本合并到master

分支

每人都有自己的分支,时不时地往dev分支上合并就可以了。

master 分支是主分支,因此要时刻与远程同步;

dev 分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;

bug 分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;

feature 分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。

fork的项目与原作者同步

1
2
3
4
5
6
7
git remote add ecmadao git@github.com:ecmadao/react-times.git

git fetch ecmadao

git merge ecmadao/master

git push origin master

bug分支

commit 时关闭 issues

1
git commit -m "fix #<issues-id>"

命令 描述
git stash 保存当前工作现场
git stash list 查看所有工作现场
git stash apply 恢复工作现场,但不删除
git stash drop 删除工作现场
git stash pop 恢复工作现场并删除
  1. dev 下正常开发中,说有1个bug要解决,首先我需要把 dev 分支封存 stash
  2. master 下新建一个issue-101分支,解决bug,成功后
  3. master 下合并 issue-101,并 fix #1
  4. dev 下合并 master,这样才同步了里面的bug解决方案
  5. 恢复dev封存 stash pop,系统自动合并 & 提示有冲突,因为封存前 dev 写了东西,此时去文件里手动改冲突
  6. 继续开发dev,最后 addcommit
  7. master 下合并最后完成的dev

Feature分支

开发一个新feature,最好新建一个分支;

多人协作

从远程库 clone 时,默认情况下,只能看到本地的 master 分支

命令 描述
git remote 查看远程库信息
git remote -v 查看远程库详细信息,如果没有推送权限,看不到 push 地址

多人协作的工作模式通常是这样:

  1. 首先,可以试图用 git push origin branch-name推送自己的修改
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用 git pull 试图合并
  3. 如果合并有冲突,则解决冲突,并在本地提交
  4. 没有冲突或者解决掉冲突后,再用 git push origin branch-name 推送就能成功!
  5. 如果 git pull 提示 “no tracking information”,则说明本地分支和远程分支的链接关系没有创建,用命令 git branch --set-upstream branch-name origin/branch-name

tag

命令 描述
git tag v1.0 打一个标签
git tag -a v0.1 -m "version 0.1 released" 3628164 带有说明的标签,用-a指定标签名,-m指定说明文字
git tag 查看所有标签
git log --pretty=oneline --abbrev-commit 查看历史提交
git tag v0.9 6224937 给历史 commit 添加 tag
git show <tagname> 查看标签信息
git tag -d v0.1 删除标签
git push origin <tagname> 推送某个标签到远程
git push origin --tags 推送全部尚未推送到远程的本地标签

gitignore

查找 gitignore 哪条规则 ignore 了该文件

1
git check-ignore -v <file>

评论和共享

ESLint 配置

配置方式

  1. 注释配置-使用 JavaScript 注释直接嵌入配置信息到文件。
  2. 配置文件-可以使用 JavaScriptJSONYAML文件来指定,.eslintrc.*

指定解释器选项

默认情况为 ECMAScript 5 语法。可以选择其他 ECMAScript 版本以及 JSX 语法。

但是支持 JSX 并不能支持 React,如果想使用 React 应该使用 eslint-plugin-react

分析器选项 .eslintrc.* 文件中使用parserOptions属性。可用的选项有:

  • ecmaVersion ECMAScript语法的版本,默认为3,5
  • sourceType 设置”script”(默认值)或者”module”如果你的代码是在ECMAScript中的模块。
  • ecmaFeatures 指示哪些额外的语言特性的对象
    • globalReturn 允许在全局范围 return
    • impliedStrict 使用全局严格模式
    • jsx 使用JSX
    • experimentalObjectRestSpread 启用对实验性支持对象静止/扩展性
1
2
3
4
5
6
7
8
9
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
}

指定解释器

默认情况下,ESLint 使用 Espree作为解释器

也可以使用以下解释器:

  • Esprima
  • Babel-ESLin
1
2
3
4
5
6
{
"parser": "esprima",
"rules": {
"semi": "error"
}
}

指定环境

可用的有:

  • browser - 浏览器全局变量。
  • node - Node.js加载全局变量和Node.js的作用域。
  • commonjs - CommonJS的全局变量和CommonJS的作用域(用这个使用Browserify /只的WebPack浏览器的代码)。
  • shared-node-browser - 全局常见的两种节点和浏览器。
  • es6-使除了模块的所有的 ECMAScript 6 的特性(这自动地将设置ecmaVersion解析器选项〜6)。
  • worker - 网络工作者全局变量。
  • amd-定义 require()define() 全局变量为每AMD规范。
  • mocha - 将所有的 mocha 全局变量。
  • jasmine - 将所有的jasmine的1.3和2.0版本测试全局变量。
  • jest - jest 全局变量。
  • phantomjs - PhantomJS 全局变量。
  • protractor - protractor 全局变量。
  • qunit - QUnit 全局变量。
  • jquery - jQuery 的全局变量。
  • prototypejs - Prototype.js 全局变量。
  • shelljs - ShellJS全局变量。
  • meteor - meteor全局变量。
  • mongo - MongoDB 的全局变量。
  • applescript -AppleScript 的全局变量。
  • nashorn - Java 8 Nashorn 全局变量。
  • serviceworker - serviceworker 的全局变量。
  • atomtest - atomtest 全局变量。
  • embertest - embertest 全局变量。
  • webextensions - webextensions 全局变量
  • greasemonkey - GreaseMonkey 全局变量。

这些环境不是相互排斥的,所以你可以定义一个超过一次。

环境可以文件的内部被指定,在配置文件或使用–env 命令行标志,例如:

1
/* eslint-env node, mocha */

要在配置文件中指定的环境中,使用env键,并指定要由每设置要启用的环境true

1
2
3
4
5
6
{
"env": {
"browser": true,
"node": true
}
}

或在一个package.json文件

1
2
3
4
5
6
7
8
9
10
{
"name": "mypackage",
"version": "0.0.1",
"eslintConfig": {
"env": {
"browser": true,
"node": true
}
}
}

全局说明

使用没有声明的变量会被警告,但如果使用的是文件里的全局变量使 ESLint 不会发出警告。

JavaScript 注释方式:
1
/* global var1, var2 */

如果你想选择指定这些全局变量不应该被写入(只读),那么你可以设置每一个false标志:

1
/* global var1:false, var2:false */
配置文件方式:
1
2
3
4
5
6
{
"globals": {
"var1": true,
"var2": false
}
}

配置插件

ESLint 支持使用第三方插件。使用插件之前,你必须使用 npm 来安装它。
要配置一个配置文件内的插件,使用的 plugins 键,其中包含插件名称的列表。该 eslint-plugin- 前缀可以从插件名称被省略。

例如:

1
2
3
4
5
6
{
"plugins": [
"plugin1",
"eslint-plugin-plugin2"
]
}

配置规则

注释方式:
1
/* eslint eqeqeq: "off", curly: "error" */

在该示例中,eqeqeq 关闭,curly 为错误。也可以使用数字相当于该规则的严重性:

1
/* eslint eqeqeq: 0, curly: 2 */

如果规则有更多的选项,你可以使用数组文本语法,如指定它们:

1
/* eslint quotes: ["error", "double"], curly: 2 */

配置文件方式:
1
2
3
4
5
6
7
{
"rules": {
"eqeqeq": "off",
"curly": "error",
"quotes": ["error", "double"]
}
}

注意:当从插件指定的规则,一定要忽略 eslint-plugin-ESLint 只使用前缀的名称在内部找到规则。

文件中暂时禁用 ESLint

例如:

1
2
3
/* eslint-disable */
alert('foo');
/* eslint-enable */

还可以禁用或启用特定规则的警告:

1
2
3
4
/* eslint-disable no-alert, no-console */
alert('foo');
console.log('bar');
/* eslint-enable no-alert, no-console */

要禁用特定行上的规则:

1
2
3
4
alert('foo'); // eslint-disable-line

// eslint-disable-next-line
alert('foo');

使用配置文件

1
eslint -c myconfig.json myfiletotest.js

规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"no-alert": 0,//禁止使用alert confirm prompt
"no-array-constructor": 2,//禁止使用数组构造器
"no-bitwise": 0,//禁止使用按位运算符
"no-caller": 1,//禁止使用arguments.caller或arguments.callee
"no-catch-shadow": 2,//禁止catch子句参数与外部作用域变量同名
"no-class-assign": 2,//禁止给类赋值
"no-cond-assign": 2,//禁止在条件表达式中使用赋值语句
"no-console": 2,//禁止使用console
"no-const-assign": 2,//禁止修改const声明的变量
"no-constant-condition": 2,//禁止在条件中使用常量表达式 if(true) if(1)
"no-continue": 0,//禁止使用continue
"no-control-regex": 2,//禁止在正则表达式中使用控制字符
"no-debugger": 2,//禁止使用debugger
"no-delete-var": 2,//不能对var声明的变量使用delete操作符
"no-div-regex": 1,//不能使用看起来像除法的正则表达式/=foo/
"no-dupe-keys": 2,//在创建对象字面量时不允许键重复 {a:1,a:1}
"no-dupe-args": 2,//函数参数不能重复
"no-duplicate-case": 2,//switch中的case标签不能重复
"no-else-return": 2,//如果if语句里面有return,后面不能跟else语句
"no-empty": 2,//块语句中的内容不能为空
"no-empty-character-class": 2,//正则表达式中的[]内容不能为空
"no-empty-label": 2,//禁止使用空label
"no-eq-null": 2,//禁止对null使用==或!=运算符
"no-eval": 1,//禁止使用eval
"no-ex-assign": 2,//禁止给catch语句中的异常参数赋值
"no-extend-native": 2,//禁止扩展native对象
"no-extra-bind": 2,//禁止不必要的函数绑定
"no-extra-boolean-cast": 2,//禁止不必要的bool转换
"no-extra-parens": 2,//禁止非必要的括号
"no-extra-semi": 2,//禁止多余的冒号
"no-fallthrough": 1,//禁止switch穿透
"no-floating-decimal": 2,//禁止省略浮点数中的0 .5 3.
"no-func-assign": 2,//禁止重复的函数声明
"no-implicit-coercion": 1,//禁止隐式转换
"no-implied-eval": 2,//禁止使用隐式eval
"no-inline-comments": 0,//禁止行内备注
"no-inner-declarations": [2, "functions"],//禁止在块语句中使用声明(变量或函数)
"no-invalid-regexp": 2,//禁止无效的正则表达式
"no-invalid-this": 2,//禁止无效的this,只能用在构造器,类,对象字面量
"no-irregular-whitespace": 2,//不能有不规则的空格
"no-iterator": 2,//禁止使用__iterator__ 属性
"no-label-var": 2,//label名不能与var声明的变量名相同
"no-labels": 2,//禁止标签声明
"no-lone-blocks": 2,//禁止不必要的嵌套块
"no-lonely-if": 2,//禁止else语句内只有if语句
"no-loop-func": 1,//禁止在循环中使用函数(如果没有引用外部变量不形成闭包就可以)
"no-mixed-requires": [0, false],//声明时不能混用声明类型
"no-mixed-spaces-and-tabs": [2, false],//禁止混用tab和空格
"linebreak-style": [0, "windows"],//换行风格
"no-multi-spaces": 1,//不能用多余的空格
"no-multi-str": 2,//字符串不能用\换行
"no-multiple-empty-lines": [1, {"max": 2}],//空行最多不能超过2行
"no-native-reassign": 2,//不能重写native对象
"no-negated-in-lhs": 2,//in 操作符的左边不能有!
"no-nested-ternary": 0,//禁止使用嵌套的三目运算
"no-new": 1,//禁止在使用new构造一个实例后不赋值
"no-new-func": 1,//禁止使用new Function
"no-new-object": 2,//禁止使用new Object()
"no-new-require": 2,//禁止使用new require
"no-new-wrappers": 2,//禁止使用new创建包装实例,new String new Boolean new Number
"no-obj-calls": 2,//不能调用内置的全局对象,比如Math() JSON()
"no-octal": 2,//禁止使用八进制数字
"no-octal-escape": 2,//禁止使用八进制转义序列
"no-param-reassign": 2,//禁止给参数重新赋值
"no-path-concat": 0,//node中不能使用__dirname或__filename做路径拼接
"no-plusplus": 0,//禁止使用++,--
"no-process-env": 0,//禁止使用process.env
"no-process-exit": 0,//禁止使用process.exit()
"no-proto": 2,//禁止使用__proto__属性
"no-redeclare": 2,//禁止重复声明变量
"no-regex-spaces": 2,//禁止在正则表达式字面量中使用多个空格 /foo bar/
"no-restricted-modules": 0,//如果禁用了指定模块,使用就会报错
"no-return-assign": 1,//return 语句中不能有赋值表达式
"no-script-url": 0,//禁止使用javascript:void(0)
"no-self-compare": 2,//不能比较自身
"no-sequences": 0,//禁止使用逗号运算符
"no-shadow": 2,//外部作用域中的变量不能与它所包含的作用域中的变量或参数同名
"no-shadow-restricted-names": 2,//严格模式中规定的限制标识符不能作为声明时的变量名使用
"no-spaced-func": 2,//函数调用时 函数名与()之间不能有空格
"no-sparse-arrays": 2,//禁止稀疏数组, [1,,2]
"no-sync": 0,//nodejs 禁止同步方法
"no-ternary": 0,//禁止使用三目运算符
"no-trailing-spaces": 1,//一行结束后面不要有空格
"no-this-before-super": 0,//在调用super()之前不能使用this或super
"no-throw-literal": 2,//禁止抛出字面量错误 throw "error";
"no-undef": 1,//不能有未定义的变量
"no-undef-init": 2,//变量初始化时不能直接给它赋值为undefined
"no-undefined": 2,//不能使用undefined
"no-unexpected-multiline": 2,//避免多行表达式
"no-underscore-dangle": 1,//标识符不能以_开头或结尾
"no-unneeded-ternary": 2,//禁止不必要的嵌套 var isYes = answer === 1 ? true : false;
"no-unreachable": 2,//不能有无法执行的代码
"no-unused-expressions": 2,//禁止无用的表达式
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}],//不能有声明后未被使用的变量或参数
"no-use-before-define": 2,//未定义前不能使用
"no-useless-call": 2,//禁止不必要的call和apply
"no-void": 2,//禁用void操作符
"no-var": 0,//禁用var,用let和const代替
"no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }],//不能有警告备注
"no-with": 2,//禁用with

"array-bracket-spacing": [2, "never"],//是否允许非空数组里面有多余的空格
"arrow-parens": 0,//箭头函数用小括号括起来
"arrow-spacing": 0,//=>的前/后括号
"accessor-pairs": 0,//在对象中使用getter/setter
"block-scoped-var": 0,//块语句中使用var
"brace-style": [1, "1tbs"],//大括号风格
"callback-return": 1,//避免多次调用回调什么的
"camelcase": 2,//强制驼峰法命名
"comma-dangle": [2, "never"],//对象字面量项尾不能有逗号
"comma-spacing": 0,//逗号前后的空格
"comma-style": [2, "last"],//逗号风格,换行时在行首还是行尾
"complexity": [0, 11],//循环复杂度
"computed-property-spacing": [0, "never"],//是否允许计算后的键名什么的
"consistent-return": 0,//return 后面是否允许省略
"consistent-this": [2, "that"],//this别名
"constructor-super": 0,//非派生类不能调用super,派生类必须调用super
"curly": [2, "all"],//必须使用 if(){} 中的{}
"default-case": 2,//switch语句最后必须有default
"dot-location": 0,//对象访问符的位置,换行的时候在行首还是行尾
"dot-notation": [0, { "allowKeywords": true }],//避免不必要的方括号
"eol-last": 0,//文件以单一的换行符结束
"eqeqeq": 2,//必须使用全等
"func-names": 0,//函数表达式必须有名字
"func-style": [0, "declaration"],//函数风格,规定只能使用函数声明/函数表达式
"generator-star-spacing": 0,//生成器函数*的前后空格
"guard-for-in": 0,//for in循环要用if语句过滤
"handle-callback-err": 0,//nodejs 处理错误
"id-length": 0,//变量名长度
"indent": [2, 4],//缩进风格
"init-declarations": 0,//声明时必须赋初值
"key-spacing": [0, { "beforeColon": false, "afterColon": true }],//对象字面量中冒号的前后空格
"lines-around-comment": 0,//行前/行后备注
"max-depth": [0, 4],//嵌套块深度
"max-len": [0, 80, 4],//字符串最大长度
"max-nested-callbacks": [0, 2],//回调嵌套深度
"max-params": [0, 3],//函数最多只能有3个参数
"max-statements": [0, 10],//函数内最多有几个声明
"new-cap": 2,//函数名首行大写必须使用new方式调用,首行小写必须用不带new方式调用
"new-parens": 2,//new时必须加小括号
"newline-after-var": 2,//变量声明后是否需要空一行
"object-curly-spacing": [0, "never"],//大括号内是否允许不必要的空格
"object-shorthand": 0,//强制对象字面量缩写语法
"one-var": 1,//连续声明
"operator-assignment": [0, "always"],//赋值运算符 += -=什么的
"operator-linebreak": [2, "after"],//换行时运算符在行尾还是行首
"padded-blocks": 0,//块语句内行首行尾是否要空行
"prefer-const": 0,//首选const
"prefer-spread": 0,//首选展开运算
"prefer-reflect": 0,//首选Reflect的方法
"quotes": [1, "single"],//引号类型 `` "" ''
"quote-props":[2, "always"],//对象字面量中的属性名是否强制双引号
"radix": 2,//parseInt必须指定第二个参数
"id-match": 0,//命名检测
"require-yield": 0,//生成器函数必须有yield
"semi": [2, "always"],//语句强制分号结尾
"semi-spacing": [0, {"before": false, "after": true}],//分号前后空格
"sort-vars": 0,//变量声明时排序
"space-after-keywords": [0, "always"],//关键字后面是否要空一格
"space-before-blocks": [0, "always"],//不以新行开始的块{前面要不要有空格
"space-before-function-paren": [0, "always"],//函数定义时括号前面要不要有空格
"space-in-parens": [0, "never"],//小括号里面要不要有空格
"space-infix-ops": 0,//中缀操作符周围要不要有空格
"space-return-throw-case": 2,//return throw case后面要不要加空格
"space-unary-ops": [0, { "words": true, "nonwords": false }],//一元运算符的前/后要不要加空格
"spaced-comment": 0,//注释风格要不要有空格什么的
"strict": 2,//使用严格模式
"use-isnan": 2,//禁止比较时使用NaN,只能用isNaN()
"valid-jsdoc": 0,//jsdoc规则
"valid-typeof": 2,//必须使用合法的typeof的值
"vars-on-top": 2,//var必须放在作用域顶部
"wrap-iife": [2, "inside"],//立即执行函数表达式的小括号风格
"wrap-regex": 0,//正则表达式字面量用小括号包起来
"yoda": [2, "never"]//禁止尤达条件

详细规则参考官方文档

评论和共享

本地开发服务器

安装 webpack-dev-server

1
npm install --save-dev webpack-dev-server

修改配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
...
devServer: {
contentBase: "./", //告诉服务器在哪里,从提供内容
port: 9000, //端口
inline: true, //
compress: false, //是否启用gzip压缩
historyApiFallback: false, //将404定向到固定位置
}
...
};

修改 package.json

1
2
3
4
5
6
7
{
...
"scripts": {
"dev": "webpack-dev-server",
},
...
}

webpack HotModuleReplacement

1
npm install --save react-hot-loader@next

Create a .babelrc

1
2
3
4
5
6
7
8
9
{
"presets": [
["es2015", {"modules": false}],
"react"
],
"plugins": [
"react-hot-loader/babel"
]
}

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
43
44
45
46
47
const { resolve } = require('path');
const webpack = require('webpack');

module.exports = {
context: resolve(__dirname, 'src'),

entry: [
'react-hot-loader/patch',
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/only-dev-server',
'./index.js'
// the entry point of our app
],
output: {
filename: 'bundle.js',
path: resolve(__dirname, 'dist'),
publicPath: '/'
},

devtool: 'inline-source-map',

devServer: {
hot: true,
// enable HMR on the server
},

module: {
rules: [
{
test: /\.jsx?$/,
use: [ 'babel-loader', ],
exclude: /node_modules/
//注意,这里不能使用 options: {} 应该在项目根目录创建 .babelrc
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader?modules', ],
},
],
},

plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
// prints more readable module names in the browser console on HMR updates
],
};

src/index.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
import React from 'react';
import ReactDOM from 'react-dom';

import { AppContainer } from 'react-hot-loader';
// AppContainer is a necessary wrapper component for HMR

import App from './components/App';

const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Component/>
</AppContainer>,
document.getElementById('root')
);
};

render(App);

// Hot Module Replacement API
if (module.hot) {
module.hot.accept('./components/App', () => {
render(App)
});
}

webpack 仪表盘

安装 webpack-dashboard

1
npm install webpack-dashboard --save-dev

修改配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
// Import the plugin:
var DashboardPlugin = require('webpack-dashboard/plugin');

// If you aren't using express, add it to your webpack configs plugins section:
plugins: [
new DashboardPlugin({ port: 3001 })
]

// If you are using an express based dev server, add it with compiler.apply
compiler.apply(new DashboardPlugin());

HTML代码热加载

webpack-dev-server 只能监控入口文件(JS/LESS/CSS/IMG)的变化,因此 HTML 文件的变化必须依赖插件来进行监控。
安装 html-webpack-plugin

1
npm install html-webpack-plugin --save-dev

修改配置文件 webpack.config.js, 把 index.html 加入监控

1
2
3
4
5
6
7
8
9
10
11
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
...
plugins: [
new HtmlWebpackPlugin({ // html代码热加载
template: './index.html'
}),
],
...
};

自动打开浏览器

安装 open-browser-webpack-plugin

1
npm install open-browser-webpack-plugin --save-dev

修改配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
var OpenBrowserPlugin = require('open-browser-webpack-plugin');

module.exports = {
...
plugins: [
new OpenBrowserPlugin({ //自动打开浏览器
url: 'http://localhost:9000'
})
],
...
};

配置 json 加载器

安装 json-loader

1
npm install json-loader --save-dev

修改配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
...
module: {
rules: [{
test: /\.json$/,
use: [
{ loader: "json-loader" },
]
}]
}
};

创建 config.json 文件,内容如下

1
2
3
4
{
"name": "demo",
"type": "HTML5"
}

使用

1
2
3
var config = require('../config.json')

console.log(config.name);

配置 LESS 编译

安装 less style-loader css-loader less-loader

1
npm install less style-loader css-loader less-loader --save-dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
...
module: {
rules: [
{
test: /\.less$/,
use: [
{ loader: "less-loader" },
{ loader: "style-loader" },
{
loader: "css-loader",
options: {
modules: true,
}
}
]
},
}
};

配置 Babel 编译

安装 babel-core babel-loader babel-preset-es2015

1
npm install babel-core babel-loader babel-preset-es2015 --save-dev

修改配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
...
module: {
rules: [{
test: /\.js$/, //babel解析器
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015']
}
}]
}
};

配置 React

安装 babel-core babel-loader babel-preset-es2015 babel-preset-react

1
npm install babel-core babel-loader babel-preset-es2015 babel-preset-react --save-dev

修改配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
...
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: [
path.resolve(__dirname, "/node_modules/")
],
query: {
presets: ['es2015','react']
}
}
]
}
};

配置 jQuery 解析器

安装 jquery

1
npm install jquery --save-dev

修改配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
module.exports = {
...
plugins: [
new webpack.ProvidePlugin({ //jquery解析器
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery"
})
]
};

配置 js 代码压缩

修改配置文件 webpack.config.js, 在 plugin 中添加 webpack.optimize.UglifyJsPlugin 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
var webpack = require('webpack');
var uglifyJsPlugin = webpack.optimize.UglifyJsPlugin;

module.exports = {
...
plugins: [
new uglifyJsPlugin({ //js代码压缩
compress: {
warnings: false
}
})
]
};

配置 eslint 语法解析

安装 esline

1
npm install eslint eslint-loader eslint-friendly-formatter eslint-plugin-html babel-eslint eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard --save-dev

在项目根目录下添加 eslint 配置文件 .eslintrc.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
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
"indent": [2, 4],//缩进风格
'no-undef': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
}
}

修改配置文件 webpack.config.js
安装 url-loader

1
npm install url-loader --save-dev

修改配置文件 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
...
module: {
loaders: [{
test: /\.(png|jpg)$/,
use: [
{
loader: "url-loader"
}
]
}]
}
};

配置图片

1
2
3
4
5
6
7
module.exports = {
...
module: {
loaders: [
]
}
};

配置 normalize.css

安装 normalize.css

1
npm install normalize.css --save

使用

1
import 'normalize.css';

配置 iconfont

  • http://www.iconfont.cn/ 选图标,添加到购物车,下载代码。
  • 有三种方式,推荐使用 unicode 方式,将字体文件和 iconfont.css 放到项目中
  • iconfont.css 修改字体路径例如 url('../font/iconfont.woff?t=1494653894295') 形式
  • 修改 webpack.config.js 配置 url-loader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    module.exports = {
    ...
    module: {
    loaders: [{
    test: /\.(woff|svg|eot|ttf)\??.*$/,
    use: [
    {
    loader: "url-loader"
    }
    ]
    }]
    }
    };
  • 使用 iconfont

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import React, { Component } from 'react';
    import Font from '../../style/iconfont.css';

    class Banner extends Component{
    render(){
    return (
    <div className={Style.historyButtonBack+" "+Font.iconfont+" "+Font["icon-houtui"]}></div>
    )
    }
    }

评论和共享

CSS-Modules

CSS Modules 用法

局部作用域

CSS 的规则是全局的,产生局部作用域的唯一方法,就是使用一个独一无二的 class 的名字,不会与其他选择器重名。这就是 CSS Modules 的做法。
下面是一个 React 组件 App.js

1
2
3
4
5
6
7
8
9
10
import React from 'react';
import style from './App.css';

export default () => {
return (
<h1 className={style.title}>
Hello World
</h1>
);
};

将样式文件 App.css 输入到 style 对象,然后引用 style.title 代表一个 class

1
2
3
.title {
color: red;
}

构建工具会将 style.title 编译成一个哈希字符串。

1
2
3
<h1 class="_3zyde4l1yATCOkgn-DBWEL">
Hello World
</h1>

app.css 也会同时被编译。

1
2
3
._3zyde4l1yATCOkgn-DBWEL {
color: red;
}

这样一来,这个类名就变成独一无二了,只对 App 组件有效。

CSS Modules 提供各种插件,支持不同的构建工具。本文使用的是 Webpackcss-loader 插件,因为它对 CSS Modules 的支持最好,而且很容易使用。

示例配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = {
entry: __dirname + '/index.js',
output: {
publicPath: '/',
filename: './bundle.js'
},
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2015', 'stage-0', 'react']
}
},
{
test: /\.css$/,
loader: "style-loader!css-loader?modules"
},
]
}
};

但个人认为使用下面这种方法更加好些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: [
path.resolve(__dirname, "/node_modules/")
],
query: {
presets: ['es2015','react']
}
},
{
test: /\.css$/,
use: [
{ loader: "style-loader" },
{loader: "css-loader",options: {modules: true}}
]
}
]
},

上面代码中,关键的一行是 style-loader!css-loader?modules ,它在 css-loader 后面加了一个查询参数 modules ,表示打开 CSS Modules 功能。

全局作用域

CSS Modules 允许使用 :global(.className) 的语法,声明一个全局规则。凡是这样声明的class,都不会被编译成哈希字符串。

App.css加入一个全局class

1
2
3
4
5
6
7
.title {
color: red;
}

:global(.title) {
color: green;
}

CSS Modules 还提供一种显式的局部作用域语法 :local(.className),等同于.className,所以上面的App.css也可以写成下面这样。

1
2
3
4
5
6
7
:local(.title) {
color: red;
}

:global(.title) {
color: green;
}

定制哈希类名

css-loader默认的哈希算法是[hash:base64],这会将.title编译成._3zyde4l1yATCOkgn-DBWEL这样的字符串。
webpack.config.js里面可以定制哈希字符串的格式。

1
2
3
4
5
6
7
{
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}

Class 组合

在 CSS Modules 中,一个选择器可以继承另一个选择器的规则,这称为”组合”(”composition”)。

App.css 中,让 .title 继承 .className

1
2
3
4
5
6
7
8
.className {
background-color: blue;
}

.title {
composes: className;
color: red;
}

输入其他模块

选择器也可以继承其他 CSS 文件里面的规则。

App.css 可以继承 another.css 里面的规则。

1
2
3
4
.title {
composes: className from './another.css';
color: red;
}

输入变量

CSS Modules 支持使用变量,不过需要安装 postcss-loaderpostcss-modules-values

1
npm install postcss-loader postcss-modules-values --save-dev

1
2
3
4
{
test: /\.css$/,
loader: "style-loader!css-loader?modules!postcss-loader"
}

接着,在colors.css里面定义变量

1
2
3
@value blue: #0c77f8;
@value red: #ff0000;
@value green: #aaf200;

App.css 可以引用这些变量。

1
2
3
4
5
6
7
@value colors: "./colors.css";
@value blue, red, green from colors;

.title {
color: red;
background-color: blue;
}

评论和共享

CSS-Modules

根据浏览器CSS渲染原理写出高性能的CSS代码

注意:CSS 引擎查找样式表,对每条规则都按从右到左的顺序去匹配。

例子

示例1

1
#nav li {}

CSS选择符是从右到左进行匹配的,浏览器必须遍历页面上每个li元素并确定其父元素的id是否为nav。

示例2

1
*{}

这种效率是差到极点的做法,因为 * 通配符将匹配所有元素,所以浏览器必须去遍历每一个元素,这样的计算次数可能是上万次

示例3

1
ul#nav{} ul.nav{}

在页面中一个指定的ID只能对应一个元素,所以没有必要添加额外的限定符,而且这会使它更低效。同时也不要用具体的标签限定类选择符,而是要根据实际的情况对类名进行扩展。例如把 ul.nav 改成 .main_nav 更好。

优化方案

  1. 避免使用通配规则。如 *{} 计算次数惊人!只对需要用到的元素进行选择
  2. 尽量少的去对标签进行选择,而是用 class。如: #nav li{} ,可以为 li 加上 nav_item 的类名,如下选择 .nav_item{}

  3. 不要去用标签限定ID或者类选择符。如:ul#nav ,应该简化为 #nav

  4. 尽量少的去使用后代选择器,降低选择器的权重值。后代选择器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三层,更多的使用类来关联每一个标签元素。

  5. 考虑继承。了解哪些属性是可以通过继承而来的,然后避免对这些属性重复指定规则

评论和共享

CSS-Modules

css-loader & style-loader 的区别

css-loader以一个字符串的形式读入一个css文件。并返回带导入的CSS,并通过webpack的require功能解决url(…):

style-loader会使用这些样式,并在包含这些样式的页面的元素中创建一个

评论和共享

作者的图片

Archie Shi

Nothing to say


Front-End Development Engineer