吐槽一下vue loader

前几天在如何创建一个webpack loader中提到我要吐槽一下vue-loader,于是今天我就来吐槽了

先来看一段webpack官网的定义:

do only a single task
Loaders can be chained. Create loaders for every step, instead of a loader that does everything at once.

This also means they should not convert to JavaScript if not necessary.

Example: Render HTML from a template file by applying the query parameters

I could write a loader that compiles the template from source, execute it and return a module that exports a string containing the HTML code. This is bad.

啥意思?就是官方推荐一个loader应该只做一件事情,如果对于一个文件有多次处理,可以把这些处理放在不同的loader里面进行链式调用。比如我们如果要写less,那么我们的webpack配置文件中应该会出现style!css!less这代表我们对于一个less文件,首先要将less处理成css,然后再有css-loader进行一些处理成js可用的css,最后通过style-loader统一抛出去。

分工明确吧?这样的好处就是style-loader和css-loader可以复用,sass,stylus都可以这么用。

那么vue-loader做了什么呢?代码我就不贴了,直接说原理吧

首先vue-loader要做的是loader一个.vue文件,这个文件中会包含html,js,css三个部分,最终的处理结果应该是css处理通过style-loader抛出去的方式,html处理成字符串,js处理成一个vue-component并require之前的html当做自己的模板,所以最终一个.vue文件最终会变成三个module

越是尤大神就在vue-loader里面做了这么一件事,vue-loader的最终产出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require("!!vue-style-loader!css-loader?sourceMap!./../node_modules/vue-loader/lib/style-rewriter.js!sass!./../node_modules/vue-loader/lib/selector.js?type=style&index=0!./button.vue")
__vue_script__ = require("!!babel-loader?presets[]=es2015&plugins[]=transform-runtime&comments=false!./../node_modules/vue-loader/lib/selector.js?type=script&index=0!./button.vue")
if (__vue_script__ &&
__vue_script__.__esModule &&
Object.keys(__vue_script__).length > 1) {
console.warn("[vue-loader] example/button.vue: named exports in *.vue files are ignored.")}
__vue_template__ = require("!!vue-html-loader!./../node_modules/vue-loader/lib/selector.js?type=template&index=0!./button.vue")
module.exports = __vue_script__ || {}
if (module.exports.__esModule) module.exports = module.exports.default
if (__vue_template__) {
(typeof module.exports === "function" ? (module.exports.options || (module.exports.options = {})) : module.exports).template = __vue_template__
}
if (module.hot) {(function () { module.hot.accept()
var hotAPI = require("vue-hot-reload-api")
hotAPI.install(require("vue"), true)
if (!hotAPI.compatible) return
var id = "/Users/Jokcy/workspace/office/x-vue/example/button.vue"

其中有三个require,这几个require里面的内容前面各不相同,但是最后却有一些类似:

1
2
3
4
5
!./../node_modules/vue-loader/lib/selector.js?type=style&index=0!./button.vue

!./../node_modules/vue-loader/lib/selector.js?type=script&index=0!./button.vue

!./../node_modules/vue-loader/lib/selector.js?type=script&index=0!./button.vue

是的,都是通过vue-loader里面的一个selector.js去loader同一个.vue文件,也就是vue-loader正在loader的文件

所以:vue-loader根本没有处理.vue文件里面的内容!!!根本没有!他只是告诉你应该尤其他的方式来loader来处理这个文件,而且一次来还是三个(你考虑过.vue文件的感受么!!!)

可能到这里你们还没觉得这有什么不对。那我就来扯一扯

首先这个之前webpack官方的建议就不一致,vue-loader不能进行链式调用,因为他不接受在vue-loader之前处理过的内容(因为最终selector.js还是会重新去读一遍源文件),同时你也不能再vue-loader之后去修改一些内容(因为他暴露出来的内容跟原内容没半毛钱关系)。所以vue-loader是一个独立的个体,我们无法对其进行扩展,这导致我们失去了很多具有想象力的做法(比如我要做的就是对特定的.vue文件进行一些处理,自动生成文档),这样的做法让vue-loader显得有点hack,同时我们也要考虑这样的做法对未来是否真的做好了准备。

最近这半年进场看到尤大推广他的vue,并经常跟react比较,甚至从某些方面给人感觉vue相较react还有挺大的优越性。其实没必要这样做,现在的vue跟react根本没有可比性,vue目前的生态和react的生态相比简直就跟清朝人民见了美帝的军舰一样,这不是你一个人在四处游说vue的好处能抵消的。我并不是说vue不好,我现在在用vue做项目,目前一个vue的组件库也正在建立中,可能马上回开源,但目前来说,vue真的没有react好。

但不管怎样,希望尤大继续努力,可能多发展一下社区的力量,壮大一下vue的生态圈,生态圈壮大了,才能有vue更好的发展。

如何创建自己的webpack loader

在目前的开源市场,前端架构中最火热的项目非webpack莫属了。在使用webpack的过程中,我们会用到各式各样的loader,毫无疑问,因为loader机制的存在让webpack拥有了无限的可能性,让webpack几乎可以容纳一切前端需要的资源。同时合理得利用loader也有助于我们在架构项目的时候省去很多重复工作,今天我们就来讲讲如何创建一个webpack的loader

在最开始想到要写loader的时候,其实我是拒绝的,因为webpack主要的功能是处理依赖以及编译,一提到编译我就头疼,各种字符串处理能让我上天。然而进一步了解之后我发现我想多了,大部分的时候编译的工作并不需要你来做,不多讲,看代码。

首先
你需要知道如何调试你本地的loader,幸运的是,不管是在webpack.config.js中写相对路径还是直接require('./loader-name!<file path>')webpack都是可以访问到我们的本地loader的,所以这点无需担心

其次
一个loader就是一个方法,这个方法接受一个source参数包括指定文件的内容,this包含了很多webpack的方法和属性供调用,该方法需要将你处理之后的内容返回,如果有sourcemap,也可以一并将sourcemap返回,这个时候需要调用this.callback(null, source, map),第一个null代表没有错误,如果有错误的话就是一个Error对象

所以
一个loader大致长成这样

1
2
3
4
5
6
7
8
9
module.exports = function (source) {
if (cacheable) this.cacheable()

// do something about the source

return dealedSource // 返回处理过的source
// this.callback(null, dealedSource, map) // 如果有sourcemap

}

记住cacheable那一步必须要执行,一方面他可以提高webpack除第一次之外的编译熟读,再次如果有cacheable官方推荐是必须cacheable的,实践情况也是不执行的话会有奇葩错误,这点上因为webpack了解不深,同时也没有相关文档,所以不是很了解清楚(你知道webpack的源码多大么!!!!!)

然后
其实该讲的就已经讲完了。。。因为loader里面的处理逻辑是根据你的实际情况来的,这没什么好说的,比如less-loader里面就是调用了less把source处理一下然后return出去,所以想到什么的朋友应该已经可以动手写自己的loader了

好吧,再说点什么
在我遇到情况中,我需要在vue-loader之前做一些特定操作(通过demo生成文档),所以我先去研究了一下vue-loader的源码。vue-loader的操作逻辑我会重新起一片文章讲,到时候我再贴过来,我只想抱怨一下vue-loader真是一个大坑,因为vue-loader实际上调用两次文件的source,所以你在vue-loader之前对source做的任何操作都是没什么卵用,我了一整晚/(ㄒoㄒ)/~~

吐槽来了,点这里

但在这种情况下,还是有办法处理,只是感觉有点hacker,并不是那么好,所以webpack得loader还是可以发挥你的很多想象力的。就酱,回家吃饭~~~

————— 4-15日补充 —————-
发现一个重点,那就是如果你的loader处理的文件有依赖于别的文件,你必须在loader里面生命Dependency,不然的话很容易出现内容不更新等情况

后端接口服务化的意义

每个公司都有一个核心业务,当我们在开发核心业务的时候,我们就跟着业务走,有新的功能新增一个接口,
前端写完页面,前后端联调,然后搞定。在这个阶段,这么做无可厚非,因为核心业务基本是不会重叠的。
但是公司发展壮大之后,产品线就会进行扩展,不管对内还是对外。产品线扩展之后,数据类型并不是同步增长的,
数据更多是以组合得方式进行扩展。这个时候我们再跟着需求来写接口就会发现,我们做了很多重复工作。

加入:我们有一个用户体系,我们有两个内部系统和一个app具有用户操作权限(指对所有用户都有可操作权限),
所以我们在这三个系统上都需要调用用户列表的接口。而app和web端由于交互形式不同,显示的数据也不同,同时app
端还因为希望省流量所以希望返回的字段尽量少。那么我们发现以传统的RPC形式开发的接口明显是不能满足需求的,
我们需要对每个系统都提供一个获取用户列表的接口,这就是资源浪费以及重复劳动。

这就是后端接口服务化的意义,服务化代表着这接口是面向大众的,没有业务耦合性,所有地方都可以调用。

restful api design

rest是最经典的接口服务化设计方案,其基于http,充分利用http协议的特性,无状态的特点非常适合业务的解耦。
我们来看一个简单的例子:

我们有一个user数据库,我们需要对用户进行增删查改的操作,于是我们有如下的设计:

1
2
3
4
5
6
GET     http://host:port/user/:id
POST http://host:port/user/:id
PUT http://host:port/user
DELETE http://host:post/user/:id

PATCH http://host:post/user/:id // 局部修改

非常简洁明了得表示了获取用户修改用户新增用户删除用户的操作

当然光这些操作肯定是不能涵盖所有的业务场景的,于是我们就来列举一下一些常见的业务场景。

列表

列表请求是最常见的请求之一,所以基本上我们也是最方便来进行抽象的接口:

1
GET    http://host:port/query/user?filter={...}

基本上就是GET请求不带id,带上删选条件

批量操作

常见的批量操作例如:批量删除,批量新增,批量修改等

1
2
3
4
5
6
7
8
9
POST|PUT|DELETE|PATCH    http://host:port/

{
data: [
{
// ... single action data
}
]
}

其他类型接口

auth

其他好处

restful api的设计对于代码复用也是有促进作用的,因为所有变化的内容都是作为参数出现的,那么很容易就能把不变的东西封装起来,
比如CRUD请求做到极致我么可以把资源名(如/user)也看做参数(/:className),可以我们接收到一个CRUD请求的时候,
我们完全可以动态得去获取数据。

使用Flux的几个问题

最近使用react进行项目开发的时候遇到了几个比较纠结的问题,记录下来大家可以看看,
自己也可以慢慢研究,或者有哪位大神有什么好的解决方案,可以留个言或者发个邮件给我

异步操作结果处理

最简单的例子就是我们进行登录,我们需要输入用户名密码,并发送一个请求到服务器,等待服务器的返回结果,如果正确,我们跳转到登录成功页面,失败则跳转到登录失败页面。

换到flux里面,可能就是以下几个步骤:

  1. 在登录组件内输入用户名密码
  2. 点击登录按钮后dispatch一个action声明一下我要进行登录了
  3. action里面跟服务器进行沟通,获取是否登录成功的结果
  4. 如果成功,触发一个登录成功的action,如果失败,触发一下登录失败的action
  5. 然后store根据不同的action结果,渲染不同的结果

这看起来貌似非常正常的一个流程,从组建操作,到动作触发,再到数据确认,最后重新渲染。

但是,如果我们追究到细节,就会提出一个问题:

登录组件如何知道登录是否成功?

我们会有这个问题是因为这是一个单页面应用,所以在进行异步请求的时候我们肯定就要维护不同的状态,试想我们不适用react进行开发,
而是使用jQuery我们会怎么做。我们就会绑定一个submit事件到form上,form在提交的时候我们获取事件,读取form表单的数据,通过ajax发送请求,
然后我们再发送ajax之前我们先把登录按钮disable掉,并且还可以改文字为正在登录...,然后ajax请求结束之后,
根据不同的结果我们可以修改取消登录按钮的disable状态,然后把文字改回来,或者登录成功直接跳转到新的view上面。

而在flux下,我们在点击按钮之后操作就交给action,在当前组件的上下文当中我们没办法知道action进行到哪一步了,唯一知道的办法是通过action改变store里面的状态。
我们可以想象在我们的user store里面加上几个状态{syncing: true, success: true, error: false}

这似乎解决我们的问题,但仔细一想,似乎又有些不对(ˇˍˇ) 。如果我们把success, error这样的状态放到一个store里面,如果有其他的组件也要使用这个store,那么这个状态也就一起过去了。
但是我们仔细想一想,这个状态应该是这次action的状态,这并不应该一个store的状态,我们放在store里面的数据应该是非常慎重的,是在整个app下面都通用的状态,
对于一个store我们可以有非常多的action,如果每个action都需要不同的状态,那么当app非常大之后,我们就没犯法很好得维护这些状态。

当然如果我们要解决也是很好解决的,我们可以给action加上两个callback

但是这是违背flux的单向数据量的原则。

思考

我提出上面的例子,似乎很大程度上是因为我还没有从老式的MVC模式中走出来。借此机会我再来好好理解一下flux

fb当初提出flux的时候有这么一个设想,他们联系以前老式的网站开发的模式,前端提交一个请求到后端,后端根据请求处理数据,然后根据结果渲染好对应的HTML,返回给浏览器,然后给浏览器展示。
这整个过程实际上跟fluxcomponent --> action --> store -(props)-> component的流程很相似。那么在这里action其实就是一个http请求处理程序,store是一个数据集(数据库)存储数据

那么action应该做的事情就是根据请求告诉store应该如何进行数据更新,在老式的服务器渲染的开发模式中浏览器在发送请求之后就直接等待服务器发送的HTML了,
所以在这里组件发送action之后也是不应该有状态回馈的(除了syncing状态,类比浏览器请求中的情况)。

那么最终如何决定请求结束后页面该以什么形态展示呢?

是URL

我们可以看到传统的网站url是经常在变动的,登录,表单提交,查看列表,每个请求其实就对应一个URL,换句话说,URL表明了我们以什么样的方式展现我们的网站

redux深入学习之中间件机制

上一篇文章讲解了redux如何使用,本篇文章将进一步深入,从redux的源码入手,深入学习redux的中间件机制。
在这里我们会以一个redux-thunk中间件为例,逐步分解redux的中间机制如何操作,如何执行。
闲话不多说,上代码。

如何加载中间件

1
2
3
4
5
6
7
8
9
10
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';

// create a store that has redux-thunk middleware enabled
const createStoreWithMiddleware = applyMiddleware(
thunk
)(createStore);

const store = createStoreWithMiddleware(rootReducer);

这里需要用到redux中提供的一个工具方法,叫做applyMiddleware,向该方法传入你想要使用的中间件,完了之后再传入createStore方法,
最终形成新的创建store的方法。

这显然是一个装饰器模式,通过不同的中间件对createStore方法进行修饰,最后形成新的createStore方法,那么创建的store就具有这些中间件的特性,
非常出色的设计,惊喜不仅在这,看了之后的代码你就更不得不佩服作者的代码设计能力。

瞬间觉得别人都是码神,而我就是码农有木有/(ㄒoㄒ)/~~

中间件加载机制的实现

先来看applyMiddleware方法的实现

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
import compose from './compose';

/**
* Creates a store enhancer that applies middleware to the dispatch method
* of the Redux store. This is handy for a variety of tasks, such as expressing
* asynchronous actions in a concise manner, or logging every action payload.
*
* See `redux-thunk` package as an example of the Redux middleware.
*
* Because middleware is potentially asynchronous, this should be the first
* store enhancer in the composition chain.
*
* Note that each middleware will be given the `dispatch` and `getState` functions
* as named arguments.
*
* @param {...Function} middlewares The middleware chain to be applied.
* @returns {Function} A store enhancer applying the middleware.
*/
export default function applyMiddleware(...middlewares) {
return (next) => (reducer, initialState) => {
var store = next(reducer, initialState);
var dispatch = store.dispatch;
var chain = [];

var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);

return {
...store,
dispatch
};
};
}

这就是redux里面这个方法的源码,其中还一半是注释有木有。。。本来以为肯定有百来行代码的
当然这里不得不说es6的特性提供了非常多的帮助,所以为了省力吧es6玩透还是灰常有必要的(更别说为了装X了(^__^) )

从这里开始代码就有点绕了,我们逐行分析

1
return (next) => (reducer, initialState) => {...}

整个applyMiddleware方法就是返回了一个方法,根据applyMiddleware方法的使用,我们可以知道next就是createStore方法,
因为最终我们要返回的是一个装饰过的createStore方法,那么接收的参数肯定是不会变,所以最终我们调用createStoreWithMiddleware方法其实就是调用

1
2
3
4
function (reducer, initialState) {
var store = next(reducer, initialState); // next即为最初的createStore方法
// ...以下省略
}

1
2
3
var store = next(reducer, initialState);
var dispatch = store.dispatch;
var chain = [];

这里没什么好讲的,首先创建了一个store,这个store就是最原始的通过createStore创建的store,后两行只是变量赋值

1
2
3
4
5
6
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);

这里是关键,必须详细进行讲解。

首先,这边声明了一个middlewareAPI对象,这个对象包含两个方法:

  1. getState:store中的getState方法的引用
  2. dispatch:对本身的dispatch方法进行一次封装

然后

1
chain = middlewares.map(middleware => middleware(middlewareAPI));

我们来仔细看看这行代码,首先我们对所有的中间件进行一个map,map结果就是调用中间件方法,将middlewareAPI作为参数传入,
这里我们拿redux-thunk中间件举例,来看看一个中间件是长什么样子的,传入的参数又是用来干嘛的。

1
2
3
4
5
6
export default function thunkMiddleware({ dispatch, getState }) {
return next => action =>
typeof action === 'function' ?
action(dispatch, getState) :
next(action);
}

redux-thunk的功能是让action支持异步,让我们可以在action中跟服务器进行交互等操作,而他的实现。。。(⊙﹏⊙)b是的,又是这么几行代码。

我们回顾之前的代码,在map所有中间件的时候我们调用了thunkMiddleware方法,传入两个方法dispatchgetState,然后返回了一个方法,
我们大致抽象一下,应该如下:

1
2
3
4
5
6
7
8
9
function (next) {

return function (action) {
typeof action === 'function' ?
action(dispatch, getState) :
next(action)
}

}

于是我们接下去分析applyMiddleware里面的代码,

1
chain = middlewares.map(middleware => middleware(middlewareAPI));

现在我们知道chain是一个数组,每一项是调用每个中间件之后的返回函数

1
dispatch = compose(...chain)(store.dispatch);

compose是redux里面的一个帮助函数,代码如下:

1
2
3
export default function compose(...funcs) {
return arg => funcs.reduceRight((composed, f) => f(composed), arg);
}

~~(>_<)~~我已经不想再吐槽什么了,

我们看到这边先调用了compose函数,传入了结构后的chain数组,然后compose函数返回的也是一个函数:

1
2
3
4
function (arg) {
return funcs.reduceRight((composed, f) => f(composed), arg);
// funcs就是中间件数组
}

然后我们把store.dispatch函数作为arg传入这个结果,这里reduceRight可以参考这里
。那么这边得到的结果是什么呢?

1
2
// 假设中间件数组是[A, B, C]
// 那么结果就是A(B(C(store.dispatch)))

再次结合redux-thunk来看,我们假设只有一个中间件,那么最终的dispatch方法就是

1
2
3
4
5
6
7
function (action) {
typeof action === 'function' ?
action(dispatch, getState) :
next(action)
}
// 这里的next方法,就是真正的store.dispatch方法
// 这里的dispatch是(action) => store.dispatch(action)

我们再结合redux-thunk的使用方法来分析一下,

1
2
3
4
5
6
7
8
function incrementAsync() {
return dispatch => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
};
}

这是使用redux-thunk时可以定义的异步action,我们触发action的时候调用的是

1
dispatch(incrementAsync())

incrementAsync返回的是

1
2
3
4
5
6
function (dispatch) {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
}

这个时候我们回想经过中间件加工的dispatch方法:

1
2
3
4
5
6
7
function (action) {
typeof action === 'function' ?
action(dispatch, getState) :
next(action)
}
// 这里的next方法,就是真正的store.dispatch方法
// 这里的dispatch是(action) => store.dispatch(action)

action是一个函数,所以action === 'function' ?成立,那么就执行action, 并把中间件接收到的dispatch方法((action) => store.dispatch(action))方法作为参数传入,
在异步方法执行完之后再次触发真正的action。如果action不是异步的,那么久直接返回一个对象,这个时候action === 'function' ?不成立,就直接调用next
也就是原始的store.dispatch方法。

我们再接着想,如果我们有许多个中间件,那么没一个中间件的next就是下一个中间件直到最后一个中间件调用store.dispatch为止。

以上的代码非常绕,建议去专研一下源码。这么精简的代码包含了非常多的函数式编程的思想,也用到了装饰器模式的原理,不得不说:

太烧脑啦/(ㄒoㄒ)/~~

学习使用redux

在react火热的年代,flux作为fb提出的最适合react的数据模型,时下有非常多的实现。
而redux作为在众多的flux地实现中脱颖而出,及其精简的代码,却能带来实用的功能,正好自己的项目中要用,所以让我们来分析redux

为什么要写这个文档呢,因为我看官方文档各种看不懂啊,琢磨了半天都不理解,最后是去看了源码才看明白
因为他的一些概念没搞清楚的话,就不知道他的文档在说什么。为了不让更多的人掉坑里面,这里稍微解释一些概念。

学习redux需要知道redux的三个部分:

  1. action
  2. reducer
  3. store

action

redux中得action就是你自己定义的一个动作,什么是动作?你可以理解为用户的动作你做出的反应,最简单地例子就是当你进行分页的时候,
跳到特定的页数这个动作。我们可以通过类似如下的代码定义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
/*
* action types
*/

export const ADD_TODO = 'ADD_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';

/*
* other constants
*/

export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
};

/*
* action creators
*/

export function addTodo(text) {
return { type: ADD_TODO, text };
}

export function completeTodo(index) {
return { type: COMPLETE_TODO, index };
}

export function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter };
}

在这里定义action之后用来出发的,通过dispatch方法来触发动作,在这里action只是一些常亮的定义。
dispatch方法接收的参数是一个object,而且object必须包含一个type属性,告诉我们需要执行的操作。
而对象里面的包含的其他属性则可以在执行动作的时候用作其他用途。

dispatch方法是会在store连接组件的时候随着组件的props传递到各个组件的,所以组件内都是可以用的。

reducer

这是在redux里面提出来的概念,具体啥含义请参考官网,因为我也解释不清楚╮(╯▽╰)╭

reducer在这里是核心,因为redux是只有一个store的,所以整个app的状态和数据都存储在一个store里面,
如果所有状态变化都在store里面进行逻辑操作,那么这个store肯定是无法维护的,所以在这里我们把状态的变化放到了reducer里面。
我们先来看一下如何定义一个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
32
33
34
35
36
37
38
39
import { combineReducers } from 'redux';
import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions';
const { SHOW_ALL } = VisibilityFilters;

function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
}

function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, {
text: action.text,
completed: false
}];
case COMPLETE_TODO:
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
default:
return state;
}
}

const todoApp = combineReducers({
visibilityFilter,
todos
});

export default todoApp;

如你所见,reducer只是一个方法,在reducer里面根据传入的action里面的type进行不同的state地操作。
在这里必须理解一点,在你调用dispatch方法的时候传入的action动作就是reducer里面接受的action

在这里我们唯一用到redux的功能只有combineReducers方法,这个方法的作用是把不同的reducer合并到一起,
因为在创建store的时候我们只能传入一个reducer,但是我们不可能把所有逻辑操作写到一个reducer里面,所以这边提供了这个方法。

store

store的作用即是整合所有的reducer,然后提供一些帮助方法,例如dispatch等方法让我们使用,
代码如下:

1
let store = createStore(reducer);

是的,就是这么简单。

如何跟react一起使用

请参考文档
这边并不进行详细讲解,以为这不是这篇文章的重点,以后会单独在其他文章中进行讲解。

理解

如何理解redux的重点就在于,redux如何处理整个数据流的走向。
基本的思路如下:

1
component --dispatch(action)--> reducer --update(state)--> store --update(props)--> component

这就是整个数据的走向

看到这里,你们肯定跟我有相同的想法:reducer到底是个什么东西!

那么我们就来理解一下

我们看一下reducer的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, {
text: action.text,
completed: false
}];
case COMPLETE_TODO:
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
default:
return state;
}
}

首先他接受两个参数,一个是state,一个是action。

action我们知道是在dispatch的时候传入的告诉我们进行什么操作的,那么state是什么?
state就是store里面存着的状态,即数据。我们可以看到每个reducer都会返回state,而这些state最终都会保存在store里面。

每次触发一个action的时候,store调用reducer,同时传入本身保存着的state,reducer根据传入的state和action返回新的state,
store更新state,返回以props的方式传入组件,这就形成了整个数据流循环

以上是redux的最基础使用,这也是redux的核心,然后后面还有一堆redux的扩展以及中间件进行学习,这仅仅是一个开始,以后还有更长的路要走^_^

讨论一下es7的装饰器

近期一直在做react的项目,看了很多开源项目的代码,其中有一个redux-react-router-async-example的项目给我印象深刻,因为。。。代码看不懂啊!!!
首先,用react开发项目用webpack打包已经是业界惯例(当然也有用bowserify的),然后用了webpack基本你就会用babel-loader来编译你的jsx代码,
然后用了babel之后这年头的大神们怎么可能忍得住不用es6甚至是es7
的新特性?所以这些项目中的代码语法基本就是怎么新怎么来,苦了我这个逗逼码农,算了,还是研究研究es6和es7的语法吧~

关于es6,es7新增了哪些语法,大家可以参考babel的LearnES2015
类似template stringLet + Const这些仅仅是加强了一下js原先一些不合理设计的没什么好说的,需要深入理解的可能就是:

  • Promise
  • Generators
  • Class
  • Map + Set + WeakMap + WeakSet
  • decorators
  • ……

一边写一边看,发现es7的新语法还是很多的,这要全列出来可能得很长一条,先把一些较为常用的列出来看看,反正我们今天讲的也就是decorators ( ̄_, ̄ )

废话不多说,进入正题。

decorators:因为整好看到google developers上有一篇很好的文章描述,我就不通篇写我自己的理解了,前文就直接翻译过来了,当然语言是会做一些自己的表达方式的。
先附上原文链接

探索ES2016装饰器

js的装饰器不得不说是来源于python的,在python里面,装饰器是一个包含其他方法的方法,这个方法在不修改他包含的方法前提下扩展他,以此来达到装饰的作用。

在python中一个装饰器长这样

1
2
3
@mydecorator
def myfunc():
pass

(@mydecorator)就是一个装饰器,这看上去跟es7的装饰器并没有太大的区别。

@标识我们这边写的代码是需要编译成一个使用mydecorator作为装饰的装饰器方法,我们的装饰器把需要装饰的方法作为一个参数传入,并把他加工之后再返回。
装饰器有很多的应用场景,比如:记录,实施权限控制和认证,进行监控,记录日志等等。

在es5和es6中的装饰器

在es5中因为并没有原生的类的支持所以使用装饰器并没有那么重要。但是到了es6,因为有了类的支持,我们就需要一些在多个类之间分配一些工具方法的方式。

Yehuda(我并不知道他是谁)的装饰器希望能够通过注解改变Javascript的类,属性以及字面量。

es7中的装饰器

es7中的装饰器就是返回一个函数的表达式,他接受三个参数:目标对象,属性名,以及一个属性描述符。
你可以通过把他放在你想要修饰的函数上面,并在最前面加上@符号来使用他。
装饰器可以设计用来修饰类或者属性。

装饰一个属性

我们先声明一个类

1
2
3
class Cat {
meow() {return `${this.name} says Meow!`;}
}

假如我们想要将meow方法放入到Cat.prototype上,大致上就像下面这样:

1
2
3
4
5
6
Object.defineProperty(Cat.prototype, 'meow', {
value: specifiedFunction,
enumerable: false,
configurable: true,
writable:true
});

想象一下如果我们想要让一个属性或者方法名称不能修改,我们需要声明一个装饰器放在这个属性或者方法声明前。所以我们声明了@readonly装饰器,如下:

1
2
3
4
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}

然后我们把它放在我们的meow方法上面

1
2
3
4
class Cat {
@readonly
meow() {return `${this.name} says Meow!`;}
}

一个装饰器只是一个返回一个方法的表达式,所以@readonly@something(params)都可以工作。
(这里解释一下,作者的意思是readonly表达式表示一个方法,而something也表示一个方法,
@something(params)其实这个装饰器是调用了something这个方法,真正的装饰器是调用something之后返回的函数)

把这个装饰器挂到meow方法上之后,实际是执行了以下的代码:

1
2
3
4
5
6
7
8
9
let descriptor = {
value: specifiedFunction,
enumerable: false,
configurable: true,
writable:true
};

descriptor = readonly(Cat.prototype, 'meow', descriptor) || descriptor;
Object.defineProperty(Cat.prototypr, 'meow', descriptor);

现在meow方法现在是只读的了。我们可以通过以下代码来验证:

1
2
3
4
5
6
var garfield = new Cat();
garfield.meow = function() {
console.log('I want lasagne');
}

// Exception:attempted to assign to readonly property

小菜一碟?那么我们一起来看一下类的装饰器(中间略过了一段介绍一个装饰器库的内容,有兴趣的可以看原文)

装饰一个类

下一步我们一起来看一下如何装饰一个类。根据es7的建议标准,类的装饰器把类的构造方法作为target
举个例子,我们定义一个MySuperHero类,我们在定义一个简单的装饰器@superhero

1
2
3
4
5
6
7
8
9
function subperhero(target) {
target.isSuperhero = true;
target.power = 'flight';
}

@superhero
class MySuperHero() {}

console.log(MysuperHero.isSuperhero); // true

我们还可以继续扩展,我们可以通过定义我们的装饰器是一个工厂来让我们的装饰器可以接受参数

1
2
3
4
5
6
7
8
9
10
11
12
13
function subperhero(isSuperhero) {
return function(target) {
target.isSuperhero = isSuperhero;
};
}

@superhero(true)
class MySuperheroClass() {}
console.log(MysuperHero.isSuperhero); // true

@superhero(false)
class MySuperheroClass() {}
console.log(MysuperHero.isSuperhero); // false

es7的装饰器可以作用在属性描述符和类之上。
他们自动获得属性名和目标对象,同时进行覆盖。调用描述符允许装饰器完成一些类似让一个属性变成一个getter方法,
启用一些用其他方式实现会显得很笨重的方法,比如第一次调用一个属性时自动绑定方法到当前对象。

es7装饰器和mixins

我(作者)非常喜欢阅读Reg Braithwaite’s的那篇
ES2016 Decorators as mixins,

Functional Mixins
Reg建议提出了一个把行为加入到目标(类属性或者独立对象)的帮助方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function mixin(behaviour, sharedBehaviour = {}) {
const instanceKeys = Reflect.ownKeys(behaviour);
const sharedkeys = Reflect.ownKeys(sharedBehaviour);
const typeTag = Symbol('isa') // 需要native实现

function _mixin(clazz) {
for (let property of instanceKeys) {
Object.defineProperty(clazz.property, property, {value: behaviour[property]});
}
Object.defineProperty(clazz.prototype, typeTag, {value: true});
return clazz;
}
for (let property of sharedKeys) {
Object.defineProperty(_mixin, property, {
value: sharedBehaviour[property],
enumerable:sharedBehaviour.propertyIsEnumerable(property)
});
}
Obecj.defineProperty(_mixin, Symbol.hasInstance, {
value:(i) => !!i[typeTag]
});
return _mixin;
}

非常好,现在我们可以定义一些混合方法并尝试去修饰一个类。想象一下我们有一个简单的ComicBookCharacter

1
2
3
4
5
6
7
8
9
class ComicBookCharacter {
constructor(first, last) {
this.firstName = first;
this.lastName = last;
}
realName() {
return this.firstName + ' ' + this.lastName;
}
}

这可能是这世界上最无聊的字符了,但是现在我们定义一些混合方法来为这个类提供SuperPowersUtilityBelt行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const SuperPowers = mixin({
addPower(name) {
this.powers().push(name);
return this;
},
powers() {
return this._powers_processed || (this._powers_pocessed = []);
}
});

const UtilityBelt = mixin({
addToBelt(name) {
this.utilities().push(name);
return this;
},
utilities() {
return this._utility_items || (this._utility_items = []);
}
});

现在我们可以用@来把这些装饰器用在我们的ComicBookCharacter类上面,
注意我们现在在一个类上用了两个装饰器

1
2
3
4
5
@SuperPowers
@UtilityBelt
class ComicBookCharacter {
...
}

现在我们用我们刚才定义的来打造一个蝙蝠侠角色

1
2
3
4
5
6
7
8
9
10
11
12
13
const batman = new ComicBookCharacter('Bruce', 'Wayne');
console.log(batman.realName());
// Bruce Wayne

batman.addToBelt('batarang').addToBelt('cape');

console.log(batman.utilites());
// ['batarang', 'cape']

batman.addPower('detective').addPower('voice sounds like Gollum has asthma');

console.log(batman.powers());
// ['detective', 'voice sounds like Gollum has asthma']

在Babel中使用装饰器

可以参考原文,这边说一下在webpack中babel使用装饰器的配置

1
2
3
{
loader: 'babel-loader?stage=1'
}

后续

文章的后续介绍了一个有趣的实验以及为什么要去尝试使用装饰器,因为跟装饰器的理解没有太大的关系,所以就不翻译了。

我的理解

看到这里其实我对类的装饰器和方法的装饰器存在一些疑虑的。因为方法的装饰器返回的是一个描述符,
然后通过definePorperty把描述符挂载到方法上面。而类的装饰器貌似都是直接操作这个类了。
所以我又去看了一下babel对于装饰器的实现链接在这里
看一下对于类和方法装饰器不同的实现:

类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@F("color")
@G
class Foo {
}

// 实现
var Foo = (function () {
class Foo {
}

Foo = F("color")(Foo = G(Foo) || Foo) || Foo;
return Foo;
})();

方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Foo {
@F("color")
@G
bar() { }
}

// 实现
var Foo = (function () {
class Foo {
bar() { }
}

var _temp;
_temp = F("color")(Foo.prototype, "bar",
_temp = G(Foo.prototype, "bar",
_temp = Object.getOwnPropertyDescriptor(Foo.prototype, "bar")) || _temp) || _temp;
if (_temp) Object.defineProperty(Foo.prototype, "bar", _temp);
return Foo;
})();

好吧。。。大概的意思就是类的装饰器就真的只是把方法传入然后加工了一下,而方法的装饰器是用属性描述符来装饰。

至于为什么是这样。。。大概就是这么定义的吧O(∩_∩)O~,需要深入的时候再研究吧。

让我们一起构建Web的未来

欢迎来到WebFuture的博客,在这里我会经常更新一些前端相关的技术博客,前端在可见的未来在客户端编程中的地位会越来越高。

js是唯一一个有可能统一移动端和桌面的客户端构建语言,我们现在有足够的理由去期待Web的将来。

越来越多的声音在呼喊云时代的到来,在PC上浏览器已经替代了大部分的桌面应用,而这个结果很可能也即将发生在移动端,
而现在我们应该做的,就是掌握好技能,为自己的将来,为Web的将来做好充足的准备。

让我们一起努力!!!