redux
是一个目前比较流行的前端框架,它和 react
配合使用,作为 react
的 数据层。它继承了 flux
的思想,构建一个 store
保存前端所有的 state
,但是目前这样的模式也逐渐出现了一些争议,尤其是当一个项目变得比较庞大的时候,在一个 store
里面存储单个页面相关的数据并没有非常大的意义,这部分我以后再说。
redux
的几个关键概念 action
reducer
store
在 https://redux.js.org 都有详细的介绍,尤其是在官网推荐的教学视频介绍了 reduex
的一些实现细节,对理解 redux
是如何工作的有很大的帮助,强烈推荐观看
首先安装 redux
$ npm install --save redux
然后我们构建一个简单的目录结构
.
├── actions
├── dist
│ ├── bundle.js
│ ├── index.html
│ └── styles.css
├── entry.js
├── package.json
├── reducers
├── styles
│ ├── index.scss
│ └── theme.scss
└── webpack.config.js
两个新的文件夹 actions
和 reducers
分别用于存放 action
和 reducer
。然后我们实现一下 redux
官网没有视图的 counter
的例子,具体代码如下,其中 actions
用于定义应用所支持的动作,有点像是 request
,然后 reducer
定义依据动作的处理,有点像是 controller
中对应的一个个的方法。
actions/index.js
:
export const increment = () => {
return {
type: "INCREMENT"
}
}
export const decrement = () => {
return {
type: "DECREMENT"
}
}
reducers/counter.js
:
export default (state=0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
break;
case 'DECREMENT':
return state - 1;
break;
default:
return 0;
}
}
entry.js
:
require('./styles/index.scss');
import counter from './reducers/counter';
import { increment, decrement } from './actions/index';
import { createStore } from 'redux';
let store = createStore(counter);
console.log(store.getState());
let unsubscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(increment());
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(decrement());
unsubscribe();
前端越来越复杂,相应的测试也是必不可少的了。我们目前的应用比较简单,最复杂的就是 reducers
所以给 reducers
添加一些测试。我们这里使用 mocha
作为测试框架。redux
官网提供了如何写测试的文档
$ npm install --save-dev mocha expect
其中 expect
是一个支持比较 fancy 的 assert
语法的库。
为了和 babel
一起使用需要另外一个东西 babel-register
$ npm install --save-dev babel-register
添加一个 test
目录
$ mkdir test
添加 reducer
的测试 test/reducers/counter.spec.js
:
import expect from 'expect';
import counter from '../../reducers/counter';
describe('counter', () => {
it('should get init state 0', () => {
expect(counter(undefined, {})).toBe(0);
});
it('should increase state', () => {
expect(counter(1, { type: 'INCREMENT' })).toBe(2);
});
it('should decrease state', () => {
expect(counter(1, { type: 'DECREMENT' })).toBe(0);
});
it('should stay same with unknown action', () => {
expect(counter(1, { type: 'NO_ACTION' })).toBe(1);
});
});
是不是觉得全天下的 spec
都一样?
然后我们执行 mocha --compilers js:babel-register --recursive
跑测试。
是不是报错了?因为我们没有 .babelrc
文件。因为之前我觉得这是一个隐式声明,不如在 webpack.config.js
显式声明好。但是没办法,其他地方也要用,改回去好了。
.babelrc
:
{
"presets": ["es2015"]
}
webpack.config.js
:
var path = require("path");
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: [
'./entry'
],
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js"
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader"
},
{
test: /\.scss$/,
exclude: /node_modules/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader", "sass-loader")
}
]
},
plugins: [
new ExtractTextPlugin("styles.css")
]
};
和 npm start
类似,我们可以写一个 npm test
把那一堆命令移过去。
package.json
:
{
"name": "test-redux",
"version": "1.0.0",
"description": "",
"dependencies": {
"redux": "^3.5.2",
"webpack": "^1.13.0"
},
"devDependencies": {
"babel-core": "^6.8.0",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"css-loader": "^0.23.1",
"expect": "^1.20.1",
"extract-text-webpack-plugin": "^1.0.1",
"mocha": "^2.4.5",
"node-sass": "^3.7.0",
"sass-loader": "^3.2.0",
"style-loader": "^0.13.1",
"webpack": "^1.13.0"
},
"scripts": {
"start": "webpack-dev-server --inline --hot --content-base dist/",
"test": "mocha --compilers js:babel-register --recursive"
},
"author": "",
"license": "ISC"
}
现在再跑一下 npm test
就和刚才一样的结果。
Loaders allow you to preprocess files as you require() or “load” them. Loaders are kind of like “tasks” are in other build tools, and provide a powerful way to handle frontend build steps. Loaders can transform files from a different language like, CoffeeScript to JavaScript, or inline images as data URLs. Loaders even allow you to do things like require() css files right in your JavaScript!
webpack
本身并不能处理乱起八糟的语言,什么 css
scss
es6
jsx
都不可以。loader
就是一个额外的 preprocessor
用于将其他语言翻译成 js
然后再让 webpack
去打包处理。那么目前我们需要处理的其他语言主要就是 scss
es6
jsx
这几个。
babel
是目前比较主流的 es6
to js
的编译器,通过简单的包装就有了在 webpack
中将 es6
转换成 js
的 babel-loader
。babel
目前支持 es2015
(ECMAScript 2015 is the newest version of the ECMAScript standard),采用 webpack
+ babel
的模式,我们就可以直接写 es2015
的 js 脚本。
首先自然是安装 babel-loader
了。
npm install --save-dev babel-loader babel-core
需要说明的是 babel 6.x
将其可以翻译的语言做了拆分,目前还没有支持默认的翻译器,需要我们在 package.json
中显示的安装所需要的翻译器。
npm install babel-preset-es2015 --save-dev
这里就是显示的说明我需要 es2015
的翻译器,不过这个情况貌似在以后的版本会做调整。
然后需要在 webpack.config.js
提供一个 loader
的声明,说明什么样子的文件需要使用 babel-loader
这个 loader
做处理。
var path = require("path");
module.exports = {
entry: [
'./entry'
],
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js"
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
query: {
presets: ['es2015']
}
}
]
}
};
可以看到在 webpack.config.js
中多了一个 module.loaders
,通过 test
匹配末尾为 .js
的文件,并且忽略 node_modules
文件夹下的所有文件。当然,我们也可以加上 include
强调只处理某个文件夹下的文件。然后 query
这部分是 babel-loader
所需要的一个声明,指定需要什么具体的翻译器对这些文件做处理。在 babel
官方文档 https://babeljs.io/docs/setup/#installation 中有另外一种申明翻译器的方法:将 query
写在一个单独的 .babelrc
文件下,我个人觉得这样让配置过于分散了,还是采用了直接在 webpack.config.js
声明的办法。
然后,我们将上一部分的 module1
module2
用 es2015
的语法方式写出来。
module1.js:
export default () => {
console.log("module1");
}
module2.js:
export default () => {
console.log("module2");
}
entry.js:
import m1 from './module1'
import m2 from './module2'
m1();
m2();
前面介绍了 webpack
的 loader
也提及了它是用来将各种语言转换成 js 的翻译器。但是有一个特殊的情况,就是有一个 style-loader
和 css-loader
,他们并不是 js
但是最终可以以 text
的形式放到我们打包的那个文件 bundle.js
中去,并且这里是将两个 loader
一起使用,有点像是 filter & pipeline
的模式。虽然这里的 style-loader
并不知道为什么要单独分出来,听起来好像是 html
的 style 还可以有除了 css
之外的东西,不明觉厉。
css file | css-loader | style-loader > bundle.js
当然,我们现在都不怎么写纯粹的 css
了,都是采用 less
或者是 sass
写了之后再翻译成 css
,webpack
也支持 sass-loader
这样的东西,最终的流程是这样子的:
sass file | sass-loader | css-loader | style-loader > bundle.js
首先安装 sass-loader
以及其所依赖的 sass
to css
的翻译器 node-sass
$ npm install --save-dev sass-loader node-sass
然后安装 style-loader
以及 css-loader
$ npm install --save-dev style-loader css-loader
和配置 es2015
类似,在 webpack.config.js
中添加 loader
var path = require("path");
module.exports = {
entry: [
'./entry'
],
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js"
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
query: {
presets: ['es2015']
}
},
{
test: /\.scss$/,
exclude: /node_modules/,
loader: "style!css!sass"
}
]
}
};
这里的 loader
是一个 pipeline
的感觉,和 es2015
的有些不一样。多个 loader
以 !
分隔,并且顺序是倒序的。
然后我们添加一个 styles
的目录,并且添加两个 scss
文件
.
├── dist
│ ├── bundle.js
│ └── index.html
├── entry.js
├── module1.js
├── module2.js
├── package.json
├── styles
│ ├── index.scss
│ └── theme.scss
└── webpack.config.js
index.scss
:
@import './theme.scss';
theme.scss
:
body {
background-color: yellow;
}
这里只用了一个 @import
的 scss
语法,不过这样也应该足够验证 scss
了。
最后,在 entry.js
中添加对 index.scss
的引用。
import m1 from './module1'
import m2 from './module2'
require('./styles/index.scss')
m1();
m2();
对的,不要怀疑,就是在 js
里面引入了 scss
,npm start
一下,看看是不是 body
的背景色变了。
不过 css
和 js
放在一起总觉得怪怪的,可不可以拆分出来?当然可以了,这里需要一个额外的 webpack
插件。plugin
有点像是 webpack
的 postprocessor
是在 webpack
打包之后进行进一步处理的工具。这里我们用到了 extract-text-webpack-plugin 把 css
拆分出来放到一个单独的文件中。
首先安装
npm install --save-dev extract-text-webpack-plugin
然后修改 webpack.config.js
注册这个插件
var path = require("path");
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: [
'./entry'
],
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js"
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
query: {
presets: ['es2015']
}
},
{
test: /\.scss$/,
exclude: /node_modules/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader", "sass-loader")
}
]
},
plugins: [
new ExtractTextPlugin("styles.css")
]
};
注意,我们的 loader
这部分也会采用 ExtractTextPlugin
进行重写
loader: ExtractTextPlugin.extract("style-loader", "css-loader", "sass-loader")
然后 plugin
这部分说明我们最终要将 css
文件保存为 styles.css
,这里要说明的是 styles.css
文件是要遵循 webpack.config.js
文件中的 output
路径的,也就是说它会保存到 dist/styles.css
。我们修改一下 index.html
,引入这个文件
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="styles.css" type="text/css" media="screen" title="no title" charset="utf-8">
</head>
<body>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
执行 webpack
看看是不是在 dist
下多了一个 styles.css
。