Eisen's Blog

© 2024. All rights reserved.

Redux get started

2016 May-10

what is redux

redux 是一个目前比较流行的前端框架,它和 react 配合使用,作为 react 的 数据层。它继承了 flux 的思想,构建一个 store 保存前端所有的 state,但是目前这样的模式也逐渐出现了一些争议,尤其是当一个项目变得比较庞大的时候,在一个 store 里面存储单个页面相关的数据并没有非常大的意义,这部分我以后再说。

redux 的几个关键概念 action reducer storehttps://redux.js.org 都有详细的介绍,尤其是在官网推荐的教学视频介绍了 reduex 的一些实现细节,对理解 redux 是如何工作的有很大的帮助,强烈推荐观看

a simple redux example

首先安装 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

两个新的文件夹 actionsreducers 分别用于存放 actionreducer。然后我们实现一下 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();

add test for reducer

前端越来越复杂,相应的测试也是必不可少的了。我们目前的应用比较简单,最复杂的就是 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 就和刚才一样的结果。

参考

  1. redux
  2. redux counter example
  3. redux writing tests
  4. babelrc

Webpack Loader

2016 May-09

loader 是做什么的

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 这几个。

在 webpack 的项目中使用 babel

babel 是目前比较主流的 es6 to js 的编译器,通过简单的包装就有了在 webpack 中将 es6 转换成 jsbabel-loaderbabel 目前支持 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 module2es2015 的语法方式写出来。

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();

参考

  1. Using babel
  2. Webpack loaders

Webpack Scss Loader

2016 May-09

Add css in webpack

前面介绍了 webpackloader 也提及了它是用来将各种语言转换成 js 的翻译器。但是有一个特殊的情况,就是有一个 style-loadercss-loader,他们并不是 js 但是最终可以以 text 的形式放到我们打包的那个文件 bundle.js 中去,并且这里是将两个 loader 一起使用,有点像是 filter & pipeline 的模式。虽然这里的 style-loader 并不知道为什么要单独分出来,听起来好像是 html 的 style 还可以有除了 css 之外的东西,不明觉厉。

css file  | css-loader | style-loader > bundle.js

当然,我们现在都不怎么写纯粹的 css 了,都是采用 less 或者是 sass 写了之后再翻译成 csswebpack 也支持 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;
}

这里只用了一个 @importscss 语法,不过这样也应该足够验证 scss 了。

最后,在 entry.js 中添加对 index.scss 的引用。

import m1 from './module1'
import m2 from './module2'

require('./styles/index.scss')

m1();
m2();

对的,不要怀疑,就是在 js 里面引入了 scssnpm start 一下,看看是不是 body 的背景色变了。

拆分 css 和 js

不过 cssjs 放在一起总觉得怪怪的,可不可以拆分出来?当然可以了,这里需要一个额外的 webpack 插件。plugin 有点像是 webpackpostprocessor 是在 webpack 打包之后进行进一步处理的工具。这里我们用到了 extract-text-webpack-plugincss 拆分出来放到一个单独的文件中。

首先安装

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

参考

  1. webpack get started
  2. webpack loader order
  3. webpack plugins
  4. extract-text-webpack-plugin