2016年8月

项目采坑笔记

有段时间没写博客了,前段时间在看 Underscore 源码所以写的多了点,这段时间还是在忙自己的其他项目去了,还是有不少收获的。

Angular 视图过渡动画

之前使用 angular-promise-button 这个模块实现了按钮的自动变化,以前自己是用很多标志位来判断特别二。不仅如此,页面切换动画也是用标志位判断,这样就特别不好维护特别不优雅,上次重构的时候就把这些全部去掉了。但是问题来了,页面数据未到达时候页面就渲染肯定会造成视觉上的问题,怎么解决呢。
我们都想写一些应用很广的代码,比如指令,比如上面这个 angular-promise-button 模块等等。其实要解决上面的问题,也是几行代码就可以解决的事情了。
我所使用的是 Angular 的 ui-router。ngRoute 应该也差不多。
在 ui-router 中可以使用 resolve 达到在控制器初始化以及视图加载前确保数据到达。比如:

$stateProvider
    .state('me',{
        url: '/me',
        controller: 'MeCtrl',
        templateUrl: 'me/me_tpl.html',
        controllerAs: 'vm',
        nav: true,
        resolve: {
            me: function(userservice) {
                return userservice.getUserInfo()
                    .then(response => response);
            }
        }
    })

只有 resolve 中的全部方法执行完后,才会开始初始化控制和加载视图。这个数据如果在控制器或者视图中要使用,可以在控制器中进行依赖注入。例如上面这个我的控制器是这样写的:

(function(){
    'use strict';

    angular
        .module('index')
        .controller('MeCtrl', MeCtrl);

    MeCtrl.$inject = ['me'];

    function MeCtrl(me) {
        let vm = this;
        vm.user = me;
    }
}());

resolve中的方法是阻塞页面进行的,这样就会带来问题了,如果数据请求比较久将导致网站停滞,我们这时候就希望可以有过渡动画出来。要达到全局过渡效果的作用,可以直接监听 $rootScope 中的三个状态即 $stateChangeStart$stateChangeSuccess 以及 $stateChangeError 事件。例如上面这个例子中,当我们触发 me 这个 state 时,也就触发了 $rootScope 上的 $stateChangeStart 事件,当处理结束后将出发 $stateChangeSuccess 并加载视图, 处理失败就会触发 $stateChangeError 事件。代码如下:

    angular
        .module('index', [
            'ui.router',
            'ui.bootstrap',
            'ngAnimate',
            'ngSanitize',
            'ngTouch',
            'infinite-scroll',
            'angularPromiseButtons'
        ])
        .config(config)
        .run(($state,$rootScope) => {
            $rootScope.$state = $state;
            $rootScope.$on("$stateChangeStart", (event, toState, toStateParams, fromState, fromStateParams) => {
                var isLoading = toState.resolve;
                if(!isLoading) {
                    for (var prop in toState.views) {
                        if (toState.views.hasOwnProperty(prop)) {
                            if(toState.views[prop].resolve) {
                                isLoading = true;
                                break;
                            }
                        }
                    }
                }
                if (isLoading) {
                    $rootScope.loading = true;
                }
            });
            $rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
                $rootScope.loading = false;
            });
            $rootScope.$on("$stateChangeError", (event, toState, toParams, fromState, fromParams, error) => {
                $rootScope.loading = false;
            });
        });

入口页面,省去了其他代码,这里第一行就是视图,第二行是加载动画,通过ng-show来控制显示。第三行是引入导航栏,这个在后面会说下。

<div ui-view class="uiview" ng-show="!$root.loading"></div>
<div class="cssload-thecube" ng-show="$root.loading"> loading... </div>
<div ng-show="$state.current.nav" ng-include="'navbar/navbar_tpl.html'"></div>

可以看到上面的代码中是监听了 $stateChangeStart 事件,然后获取目标 state 上的 resolve 方法,当 state 上的 resolve 方法全部结束后,$rootScope.loading 设置为 false,否则保持为 true。
当监听到 $stateChangeSuccess 或者 $stateChangeError 事件时,置 $rootScope.loading 为 false,退出过渡动画。在视图中可以使用 $root 得到 $rootScope
可以看到这里有很多参数,可见其功能是很强大的。

再看下上面这个第三行

<div ng-show="$state.current.nav" ng-include="'navbar/navbar_tpl.html'"></div>

结合上面的 JS 代码来看,我已经把 $state 注入到了 $rootScope 中了,之后我就可以使用 $state.current 来获取当前的视图状态和信息。我需要实现导航栏仅仅出现在我指定的页面中,下方按钮可以根据当前视图来激活。第一点可以通过给路由 state 补充变量比如我这里的 nav 来实现,需要导航栏的地方就设置 nav 为 true, 否则就不设置。第二点则可以利用 ui-sref-active 来实现。如下:

<!-- navbar/navbar_tpl.html -->
<nav class="bookist-nav navbar navbar-default" role="navigation">
    <ul class="nav navbar-nav">
        <li ui-sref-active="active"><a ui-sref="index"><i class="fa fa-home fa-lg"></i><p>首页</p></a></li>
        <li ui-sref-active="active"><a ui-sref="booklists"><i class="fa fa-th-large fa-lg"></i><p>书单</p></a></li>
        <li ui-sref-active="active"><a ui-sref="cart"><i class="fa fa-shopping-cart fa-lg"></i><p>购物车</p></a></li>
        <li ui-sref-active="active"><a ui-sref="me"><i class="fa fa-user fa-lg"></i><p>我的</p></a></li>
    </ul>
</nav>

最后还有一个地方就是,下面这个代码

<div ui-view class="uiview" ng-show="!$root.loading"></div>

ui-view 的用法其实还不少,如果你有去注意的话,会看到这个视图在变化的时候其类名会变化,依次我们可以结合 angular-animate 来实现切换动画。注意这个切换动画是在视图加载后才开始的,和上面的不一样,如果同时使用,则会在上方过渡效果结束后触发。例如,我们可以实现淡入淡出:

.uiview {
  &.ng-enter {
    transition: .5s;
    opacity: 0;
  }
  &.ng-enter-active {
    opacity: 1;
  }
}

所以上面其实说了好几点:

  • 使用路由上的 resolve 来实现在控制器初始化前获取到需要的数据
  • 监听 $rootScope 上的事件来实现 resolve 等待动画
  • 在 ui-view 上通过使用 angular-animate 来实现视图的切换动画
  • 通过 ui-sref-active 在当 ui-sref 和当前 state 一致时激活 active 类名
  • 把 $state 注入到 $rootScope 达到在视图中获取 $state 用途
  • 使用 $root 得到 $rootScope,利用 $root 获取 $rootScope 上的对象

静态资源自动发布七牛云

这个很简单啦,我使用了 gulp-qiniu 这个模块来实现,很简单,结合前面说的 gulp-usemin 就更完美了。

<!-- build:js //cdn.bookist.org/bookist.min.js -->
<script src="src/js/bookist.js"></script>
<script src="src/js/templates.js"></script>
<!-- endbuild -->

例如对上面 index.html 中的这一片段,我们要在开发环境中使用本地资源,而在线上环境则使用 CDN 资源。我们可以这样配置 gulpfile。

gulp.task('cdn', () => {
    gulp.src('index.html')
        .pipe(usemin())
        .pipe(gulp.dest('backend/app/templates'));
    gulp.src(['./src/js/bookist.js', './src/js/templates.js'])
        .pipe(plumber())
        .pipe(uglify())
        .pipe(babel())
        .pipe(concat('bookist.min.js'))
        .pipe(qiniu({
            accessKey: "xxx",
            secretKey: "xxx",
            bucket: "bookist",
            private: false
        }));
}

运行上面的任务后就会在 backend/app/templates 生成一个修改过的 index.html。对于上面的片段,处理之后是这样的:

<script src="//cdn.bookist.org/bookist.min.js"></script>

前面的注释是给 usemin 使用的,所以要按照规范书写。
同时上面的任务也会把资源发布到七牛云上面,如果你还想指定文件头或者版本号之类的信息,可以参考 gulp-qiniu 的文档进行配置。
上面的方法有一个小问题就是还会在 backend/app/templates 生成一个 cdn.bookist.org 的文件夹,不过无所谓咯,放到 .gitignore 就好了。
至于图片和 css 都差不多吧,就不多说了。

微信坑逼

有段时间我的网站在微信显示部分地方是有问题的,但在手机浏览器和电脑都没任何问题,一开始以为是样式的问题。等到自己要去解决这个问题了,才知道是 js 的问题。
由于微信开发工具不支持 linux,然后我虚拟机的 win10 有不知道什么原因一直连接不到手机。所以就手动调试了,这边改一点,发布上去,然后等微信缓存没了看效果(微信恶心的缓存...我都叫用苹果的人帮我看,因为苹果可以刷新..)。就这样一步步看,最后定位了问题代码。中间过程就不描述了,直接看:

for(let book of books) {
    if(book.rate)   book.star = Math.ceil(book.rate/2);
    else if(book.star)   book.star = Math.ceil(book.star/2);
}

省略了无关代码,这是一个方法里面的部分代码,如果一个对象传入这个方法中,在微信会得不到返回值,在手机浏览器和电脑上都正常。
虽然这里使用了 ES6 的语法,但是我其实已经大量使用 let...of... 语句,项目并没有出现太大问题,就只是上面描述的一些地方异常而已。再者我使用了 babel 转码了。越想越觉得没道理啊。。。
最后虚拟机换了 win7 然后手机连接上了,打开了微信开发工具,调试微信 webview,报错 Symbol is not defined
果断控制台敲 Symbol,结果 Symbol is not defined
好吧,问题出来了,微信这个辣鸡不支持 Symbol...
但问题是我没有用 Symbol 这个东西啊,我瞄了 babel 一眼。
看下上面那段代码转码的结果:

var _iteratorNormalCompletion5 = true;
var _didIteratorError5 = false;
var _iteratorError5 = undefined;

try {
    for (var _iterator5 = books[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
        var book = _step5.value;

        if (book.rate) book.star = Math.ceil(book.rate / 2);else if (book.star) book.star = Math.ceil(book.star / 2);
    }
} catch (err) {
    _didIteratorError5 = true;
    _iteratorError5 = err;
} finally {
    try {
        if (!_iteratorNormalCompletion5 && _iterator5.return) {
            _iterator5.return();
        }
    } finally {
        if (_didIteratorError5) {
            throw _iteratorError5;
        }
    }

什么鬼...算了,不看了,但是确定了是 babel 转码导致的。然后查了下资料:

Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator, Generator, Set, Maps, Proxy, Reflect, Symbol, Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

其实早有耳闻 polyfill 这个东西,但我想他一般是用在 IE 这种辣鸡浏览器上的,没想到微信 webview 这么不争气。
多打包一个 babel-polyfill 解决了这个问题。
坑比微信...

故事还没结束...
昨天发现网站并没有使用 http2,但是已经进行了设置了,最后查了很多资料原因好像是 openssl 的问题,但由于使用的是 docker,貌似不太好搞,但其实更换 nginx 版本就好了,原先是1.9版本,换成1.11-alpine 版就解决了,用 docker 更换 nginx 版本非常方便。额,具体原因不深究了,可能跟 alpine 这个字眼有关?不清楚...
http2 具备多路复用的特点,在 http1.1 中,并行传输文件是有限制的,因为用户端和服务端的最大连接数是有限制的,而连接的建立和销毁又会带来开销,所以在 http1.1 中对文件进行压缩合并是很有必要的。不过在 http2 就不需要这样做了,http2 可以在一条通道上传输多个文件,如果合并剩几个,就没法发挥并行传输的优势,而且文件太大,还会降低运输层的效率,即丢包或者乱序到达的影响。
我把网站改到了 http2 后,就不再进行文件合并了,转而可以大量使用 bootcss 的 CDN,bootcss 的 CDN 支持 http2,传输很快。至于上面没有的和自己写的,就发布到七牛云上面。恩,电脑上加载是变快了很多。
但是感觉不到微信加载变快...最后发现是微信不支持 http2...
好伤心...
最后我的方案是产生两个 css 文件和两个 js 文件,之所以是两个,因为一个是自己写的,经常变,另一个是用别人的,几乎不会变。

最后又发现微信好像支持 spdy... 心好累,算了,降了 nginx 版本开启 spdy 不理了。


总结:

  • Angular 是一个大而全的框架,我觉得很强大很牛逼,越来越喜欢 Angular 了
  • 使用 CDN 可以大大的加速,尽量使用 CDN
  • 微信这个坑比我就不说了,不支持 Symbol 不支持 http2
  • 能用 http2 就尽量用 http2
  • 考虑浏览器兼容性,根据需要引入 babel-polyfill

浅谈 JavaScript 模块定义规范

JS 模块定义常见的有三种方式,即 AMD, CMD 和 CommonJS。其实还有一个 UMD,他是 CommonJS 和 AMD 揉和在一起而已。不过这些都 out 了,拥抱 ES6 吧。话虽这么说,你让那些不用 ES6 不用 babel 的怎么活,所以还是要了解下滴。

CommonJS

CommonJS 是服务端即 Node.js 采用的模块化方案,我们应该都很熟悉了。例如:

const fs = require('fs');
fs.readFileSync();

这个过程是同步的,只有成功加载 fs 后才能执行后面的步骤。但在服务器文件都在本地,所以这个问题不大。但这个在浏览器就不合适了,如果文件加载耗时很长,将导致一直等待。

AMD

AMD 全称 Asynchronous Module Definition,意思就是异步模块定义。
用法如下:

require(['math'], function(math) {
    math.add(1, 2);
});

math 模块的加载和 math.add() 方法的执行不是同步的,这样浏览器就不会假死。
RequireJs 和 CurlJs 实现了 AMD 规范,将他们嵌入网页,就可以在浏览器端进行模块化编程了。
关于 AMD 的详细模块定义可以参考wiki。这里给出 Underscore 的 AMD 定义方法:

if (typeof define == 'function' && define.amd) {
    define('underscore', [], function() {
        return _;
    });
}

CMD

CMD 全称 Common Module Definition,意思就是通用模块定义。。
对于依赖的模块,AMD 是提前执行,而 CMD 是延迟执行。AMD 推崇依赖前置,而 CMD 则推崇依赖就近。例如:

define(function(require, exports, module) {
    const math = require('./math');
    math.add(1, 2);
});

CMD 的主要实现是 SeaJS。

UMD

UMD 全称 Universal Module Definition。
UMD 是 AMD 和 CommonJS 的揉和,他优先使用 CommonJS 的加载方式,其次才使用 AMD 的加载方式。
例如:

(function (window, factory) {
    if (typeof exports === 'object') {

        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {

        define(factory);
    } else {

        window.eventUtil = factory();
    }
})(this, function () {
    //module ...
});

其实就是一个服务端和浏览端通用的模块解决方案。

ES6 Module

ES6 在语言规格的层面上实现了模块功能,并且实现非常简单,完全可以替代现有的模块加载方案,成为浏览器和服务端都通用的模块解决方案。

- 阅读剩余部分 -

Underscore 源码学习(七) - flatten

Underscore 中间 flatten 相关的方法之前一直不是很理解,现在完全搞懂了,稍微说一下。

var flatten = function(input, shallow, strict, output) {
    output = output || [];
    var idx = output.length;
    for (var i = 0, length = getLength(input); i < length; i++) {
        var value = input[i];
        // 若value为数组,把里面东西去出来赋值给output
        // 否则直接赋值给output
        // isArrayLike的判断可以去掉,保留的原因是因为他用来判断value是否为数组很快,可以迅速筛选掉非数组
        if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
        // Flatten current level of array or arguments object.
        if (shallow) {
            // 如果给了shallow参数,只只遍历一层
            var j = 0, len = value.length;
            while (j < len) output[idx++] = value[j++];
        } else {
            // 一直遍历下去,如果是元素则按下面赋值,如果是数组则继续遍历
            flatten(value, shallow, strict, output);
            idx = output.length;
        }
        } else if (!strict) {
        output[idx++] = value;
        }
    }
    return output;
};

这个方法不难看懂,作用是将input平铺展开,如果 shallowtrue,则只展开一层。

_.flatten([1, 2, [3], [[4, [5]]]])              // [1, 2, 3, 4, 5]
_.flatten([1, 2, [3], [[4, [5]]]], true)        // [1, 2, 3, [4, [5]]]

这里的 strict 参数就是之前一直卡住的原因,就是下面这个地方:

_.without = restArgs(function(array, otherArrays) {
    return _.difference(array, otherArrays);
});

_.difference = restArgs(function(array, rest) {
    rest = flatten(rest, true, true);
    // 遍历array,如果array中一个元素包含在rest中,则去掉该元素
    return _.filter(array, function(value){
        return !_.contains(rest, value);
    });
});

这是两个方法,那时候想 without 方法调用的时候, otherArrays是一个数组了,到 difference 方法的时候,这个数组去调用 flatten 方法的时候不是会出问题吗?

_.flatten([1, 2, 3], true, true)        // []

脑子里面就这样想...卡了好久,等我基本看了全部源码才会过来看才理解了。
difference 方法的 restArgs 很重要,他们两个是各自独立的方法,但是 without 可以共用 difference 的逻辑。
上面那样子理解是有问题的,因为在 without 方法中 otherArrays 如果是[1, 2, 3],到了 flatten 调用的时候因为 restArgs 的关系他变成了 [[1, 2, 3]],调用最后返回结果[1, 2, 3]。然后我就纳闷了,加了一层又解除这是何解...
不过抛开 without 方法去看 difference 方法就能理解了。

- 阅读剩余部分 -

Underscore 源码学习(六) - mixin

关于 Underscore 的 mixin 方法。
首先先看个例子:

function Panel() {
  consoe.log(this, this instanceof Panel);
  if(this instanceof Panel) {
    return this;
  } else {
    return new Panel();
  }
}
a = Panel();        // Window   false
b = new Panel();    // Panel{}  true

当函数作为构造器使用时,函数内的 this 执行被新建的对象。当函数被调用时,函数内的 this 则为被调用的对象,在这里是 Window

  var _ = function(obj) {
    if (obj instanceof _) return obj;
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
  };

同样的,如果我们使用下面方法调用:

var under = _();

第二个条件成立,所以新建一个 _ 对象后返回,注意这里是再次调用这个函数。
如果我们这样调用:

var under = new _();

就好像上面第二次调用一样,这时候就构造了 under 这个对象,如果传入了参数 obj,则把 obj 存入 under 这个对象的 _wrapped 属性中。
Underscore 提供了一个 OO 的调用方法,即:

var chainResult = function(instance, obj) {
    return instance._chain ? _(obj).chain() : obj;
};

_.mixin = function(obj) {
    // 遍历obj中的函数
    _.each(_.functions(obj), function(name) {
        // 避免原型链查找,提升性能
        var func = _[name] = obj[name];
        _.prototype[name] = function() {
            // 把wrapped作为数组第一个参数(context),其余传参push到这个数组中
            var args = [this._wrapped];
            push.apply(args, arguments);
            // 如果this是一个_实例,则使用func调用的结果来新建_实例后返回以供继续链式调用
            // 如果this不是一个_实例,则直接返回func调用的结果
            return chainResult(this, func.apply(_, args));
        };
    });
    return _;
};

// 把Underscore对象mixin化,这样就可以直接在_上调用方法
_.mixin(_);

当然我们还可以把自己写的方法通过 mixin 加入到 Underscore 对象中。

- 阅读剩余部分 -

Underscore 源码学习(五) - Throttle 和 Debounce

Underscore 的函数大部分还是挺好理解的,感觉过一遍就行了,不过今天看到两个函数感觉还是挺有意思的,并且也挺常用。这两个函数就是 throttle 和 debounce。

throttle

_.throttle = function(func, wait, options) {
  var timeout, context, args, result;
  var previous = 0;
  if (!options) options = {};

  var later = function() {
    // 更改previous即上一次执行时间为当前时间
    previous = options.leading === false ? 0 : _.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    var now = _.now();
    // 如果leading为false时禁用第一次首先执行,previous等于now(效果同已经执行过一次,所以第一次被禁用)
    // 这个if语句只在第一次执行该函数的时候有效
    if (!previous && options.leading === false) previous = now;
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    // 超时处理和未到时的处理
    if (remaining <= 0 || remaining > wait) {
      // timeout不为null时清除掉并设置为null
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      // 立即调用
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {    // 如果没有禁用最后一次执行
      timeout = setTimeout(later, remaining);               // remaining毫秒后执行later
    }
    // 返回调用的结果
    return result;
  };

  throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  return throttled;
};

其实一步步去理解也不难,就不细说这个方法了。这个方法有可选项设置,分别为 {leading: false} 和 {trailing: false}。

所以一般有四类情况:

  • 默认情况
    第一次调用时立即响应,之后每个周期内最多执行一次,周期内触发会产生定时执行使在上一次执行时间 preview 周期时间后再次执行。
  • 设置 leadingfalse
    同默认情况区别在于第一次调用不会立即执行而是等待周期时间后再次执行,如果在周期时间内触发,一样等待上一次执行时间 preview 周期时间后再执行。
  • 设置 trailingfalse
    最后周期内最多执行一次,但在周期时间内调用不会触发 timeout,只能在上一次 timeout 失效后调用才能生效并且此时调用将立即执行。
  • 设置 leadingtrailingfalse
    如果同时还设置 leadingfalse 的话,那么第一次调用不会立即执行而是等待周期时间后才执行,在这段时间内调用都不会有效果。

- 阅读剩余部分 -

谈谈 React 和 Redux

刚回到家的时候学习了 React 和 Redux,现在才想来总结一下,不知道会不会忘的差不多了...本来是想写一个问卷发布系统的,使用 React 和 Redux 已经完成了基础的几个功能,但是那个代码量...用 Angular 写简直轻轻松松的好吧...然后就去重构项目还有现在学的 Underscore 去了。是时候重新回顾下了。
此处主要讲的是 Redux 。

关于 Redux

redux 是facebook 提出的 flux 架构的一种优秀实现;而且不局限于为 react 提供数据状态处理。它是零依赖的,可以配合其他任何框架或者类库一起使用。要想配合 react,还得引入 react-redux。

关于 Flux

那什么是 Flux 呢?见下图
flux-overview.png
Flux 可以分为四个部分:

  • View: 视图层
  • Action: 视图层触发的动作
  • Dispatcher: 派发器,用来接受 Actions, 执行回调函数
  • Store:数据层,用来存放应用的状态,其变更会触发 View 层更新

Flux 的最大特点就是单向流动,他的过程大概如下:

  1. 用户访问 View ,触发了动作 Action
  2. Dispatcher 收到 Action ,根据 Action 类别进行相应的处理,处理结束后要求 Store 更新
  3. Store 进行更新,通知 View 层刷新
  4. View 层收到通知更新页面

额,其实我没有用 Flux,不敢讲太多了,简单的说就是一种单项数据流动的解决方案吧。我是直接学 Redux,对 Flux 也就大概了解这么多了。

Redux 和 Flux

Redux 是 Flux 的一种实现,但他们又有所不同,在 Flux 中,Store 可以有多个,但 Redux 有且只能有一个 Store,Flux 中存在 Dispatcher,在 Redux 则没有这个,而是用 reducer 代替了。 Flux 了=.=

理解Redux

Redux 由四部分组成:

  • Action
  • Reducer
  • Store
  • Views

我们结合具体的应用场景来看

Action

先从 Action 说起,一个 Action 是一个普通的对象。

export const SET_PAPER_TITLE = 'SET_PAPER_TITLE';
export const ADD_QUESTION = 'ADD_QUESTION';
export const REMOVE_QUESTION = 'REMOVE_QUESTION';
export function setPaperTitle(newTitle) {
    return {
        type: SET_PAPER_TITLE,
        value: newTitle
    }
}
export function addQuestion(type) {
    return {
        type: ADD_QUESTION,
        questionType: type
    }
}
export function removeQuestion(questionId) {
    return {
        type: REMOVE_QUESTION,
        questionId: questionId
    }
}

type 属性是必须的,表示动作类别,其他的参数可以自定。
我们先不用管 Action 有什么用,后面会提到。

- 阅读剩余部分 -