无标题文章

##Flux与面向组件化开发首先要明确的是,Flux并不是一个前端框架,而是前端的一个设计模式,其把前端的一个交互流程简单的模拟成了一个单向数据流。![](http://facebook.github.io/flux/img/flux-simple-f8-diagram-1300w.png)在上图中,我们可以看到Flux的四个核心构成:####Action一个交互动作,来源于用户在页面组件上的某个行为,如点击,失焦,双击等等。其往往具有两个组成:*交互类型 ,例如创建、删除、更新等*交互体,或者说交互的携带信息, 例如创建的文本####Dispatcher分发器,从上图的数据流中,我们可以看到,用户产生的一个交互行为将被送入Dispatcher,分发器对Action进行简单的包裹之后分发该行为到所有__向其注册了的Store__中。>!注意,Dispatcher的这种广播行为有别于__Pub/Sub__模型,在__Pub/Sub__模型中,需要声明订阅的消息类型,然后发布者会像订阅者广播特定类型的消息。而在Dispatcher中,Store向其注册的任意回调接口都不要声明订阅的Action类型,当Dispatcher派发Action时,所有注册到Dispatcher的callback都会得到相应。回调可以通过简单工厂模式(通常是一个switch块)来针对不对类型的Action做出不同的行为。####Store数据存储仓,其保存了我们某个前端App的数据,并封装了对于数据的操作。Store会向其对应的Dispatcher注册一个回调函数,其参数为一个交互。当Action被派发到Store时,该回调函数被调用,借由Action中描述的__交互类型__,Store进行不同处理(一个简单工厂模式),这些处理都将被持久化到Store维护的数据对象上。Store完成数据的变更后,由于Flux并不是双向数据绑定的,所以此时,页面组件的数据并未得到更新,组件也不会重新渲染。所以,为了告知组件去更新数据,Store会emit一个变更事件,并且监听该事件。当监听到变更事件产生时,注册到这个事件上的回调(往往是我们App的状态维护器的状态更新函数)会被调用,从而更新各个组件的状态。####View显而易见,这就是用户所能看到的视图,有别于传统的MVC,在Flux中,View并不会和数据模型(Model)产生交互,其只会产生各种交互行为(Actions),这些行为将会被送到Dispatcher中,如下图所示:![Action被送入Dispatcher](http://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png)###TODO栗子下面我们分析一个用React+Flux实现的一个Flux栗子,其源码托管在[github](https://github.com/facebook/flux/tree/master/examples/flux-todomvc)上。在项目实践中,面向组件化开发的最佳场景我认为是__交互驱动型的开发__,可能描述不够准确,准确点说就是一旦一个完善的交互设计稿产生时,我们就可以去__分割__和__分析__组件了,我们现在来分析Todo的交互原型:![Todo交互](http://7pulhb.com2.z0.glb.clouddn.com/Flux-Todo%E4%BA%A4%E4%BA%92%E5%8E%9F%E5%9E%8B.png)>这是交互设计师的给我们的原稿,并且,原稿可能远不止这样一幅简单的图像,可能还包括更多的交互效果我们将会把这个应用拆分为如下组件:####TodoApp![TodoApp](http://7pulhb.com2.z0.glb.clouddn.com/Flux-TodoApp.png)通常,在前端面向组件化的开发过程中,我们往往需要一个顶部容器包裹住我们的组件,一个页面可以存在若干个这样的顶部容器,这个容器类似一个集装箱或者盒子,封装了某个页面应用的所有组件和状态。例如,在某视频网站中,视频播放窗口可以作为一个顶部容器,其包裹了播放窗口,进度条,播放选项等各个组件,同时,评论部分也可以作为一个顶部容器,其包裹了评论列表,评论框等组件。在Todo例子中,TodoApp作为一个顶部容器,包裹了所有Todo应用需要的组件,这样,我们在应用入口只需要渲染TodoApp就完成了整个TodoApp的渲染。但更为重要的是,TodoApp将会封装其下各个组件需要用到的状态,通过数据流,各个组件将会收到状态,并且在状态改变时,重新渲染自己,最终更新页面内容。####Header![TodoHeader](http://7pulhb.com2.z0.glb.clouddn.com/Flux-TodoHeader.png)这是一个头部组件,根据交互设计,他除了将保有静态的“todos”文字标题以外,还将会具有如下行为:*右侧输入框失焦或者相应回车键:创建新的任务####Footer![TodoFooter](http://7pulhb.com2.z0.glb.clouddn.com/Flux-TodoFooter.png)这是一个底部组件,它将显示未完成任务数,并能删除所有已完成任务,故而,首先他需要获得如下__状态__:*所有任务:*通过遍历任务的完成情况,能获得未完成任务数*通过遍历任务的完成情况,统计已完成任务的信息*如果当前无任务,不现实Footer并且,他具有如下行为:*单击右侧按钮(Clear completed): 清除所有已完成任务------------####MainSection![MainSection](http://7pulhb.com2.z0.glb.clouddn.com/TodoMainSection.png)该组件将会负责渲染所有的以创建任务,因而他需要维护的状态为:*所有任务其具有的行为:*点击顶部左侧图标按钮:完成/取消完成所有任务,具体根据__所有任务__是否都完成了决定----------------####TodoItem![TodoItem](http://7pulhb.com2.z0.glb.clouddn.com/TodoItem.png)这是Todo项,其Todo对象来源于MainSection的迭代,并且该组件具有如下行为:*单击左侧按钮:完成/取消完成该任务*单击右侧按钮:删除该Todo*双击Todo文本:进入如下的编辑模式![编辑模式](http://7pulhb.com2.z0.glb.clouddn.com/%E5%8F%8C%E5%87%BB%E8%BF%9B%E5%85%A5%E7%BC%96%E8%BE%91.png)我们不难发现,“是否处于编辑模式”实际上可作为该组件的一个状态,该状态的切花直接影响了该组件的展示和行为,所以,组件应当维护一个状态:*是否编辑模式在编辑模式中,具有如下行为:*输入框失焦或者相应回车键:更新任务可以看到,在__Header__组件及__TodoItem__组件的输入框组件具有一致的交互行为,所以,我们可以将其提出来作为单独的组件,这也体现了,一份晚上的交互设计原型将预测到实现过程中的复用和抽象,避免了一些代码重构的时间。----------------####TodoTextInput现在,我们抽象出一个可复用的输入组件TodoTextInput,他具有如下行为:*输入框失焦或者相应回车键:调用存储过程(创建,更新等等)综上,我们以一个简单的示意图表示如上的划分:![组件结构](http://7pulhb.com2.z0.glb.clouddn.com/%E7%BB%84%E4%BB%B6%E7%BB%93%E6%9E%84.png)![状态维护](http://7pulhb.com2.z0.glb.clouddn.com/Flux-%E7%8A%B6%E6%80%81.png)上图__蓝色__椭圆封装的属性,__黄色__椭圆封装的是状态。 在每个TodoItem中,还需要单独维护一个”是否可编辑状态”,该状态决定了TodoItem的行为和展示。注意到,因为__所有任务__这个状态会被多个组件共享(MainSection,Footer),所以,该状态被提到了顶部容器TodoApp中进行维护,这样,通过TodoApp的__SetState()__方法,所有绑定到TodoApp的组件都获得了状态更新,避免了组件间的相互引用,实现了组件解耦(唯一的耦合存在于组件与顶层容器),如下图所示:![状态共享](http://7pulhb.com2.z0.glb.clouddn.com/%E7%8A%B6%E6%80%81%E6%9B%B4%E6%96%B0-%E6%9D%BE%E8%80%A6%E5%90%88.png)倘若我们在MainSection及Footer中分别维护这个状态,由于MainSection与Footer属于平级的组件,所以,当MainSection中的__所有任务__这一状态发生改变时,为使Footer中的状态也发生改变,为此,MainSection及Footer组件都要保存对方引用,二者将会是强耦合的,如下图所示:![状态不共享](http://7pulhb.com2.z0.glb.clouddn.com/%E7%8A%B6%E6%80%81%E6%9B%B4%E6%96%B0-%E5%BC%BA%E8%80%A6%E5%90%88.png)设想,如果以后还有更多的组件需要__所有任务__这一状态,这一设计模式将会是十分糟糕的,任何一个组件的脱离将可能导致整个引用网络的崩溃,如下图所示:![状态更新-崩溃](http://7pulhb.com2.z0.glb.clouddn.com/%E7%8A%B6%E6%80%81%E6%9B%B4%E6%96%B0-%E5%B4%A9%E6%BA%83.png)###封装![目录结构](http://7pulhb.com2.z0.glb.clouddn.com/Flux-%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84.png)其中app.js为应用的入口文件,通常,单页面应用(SPA)都需要提供一个最初的文件,然后递归渲染DOM树。下面,开始实现我们的逻辑,顺着Flux的单向数据流,逐个分析Todo例子中的实现。####Dispatcher__js/AppDispatcher.js__```javascriptvar Dispatcher = require('flux').Dispatcher;module.exports = new Dispatcher();```可以看到,Dispatcher的实现主要依赖于官方的[flux](https://www.npmjs.com/package/flux)提供支持。我们可以看下flux中的Dispatcher源码,所有解说都放在代码注释中:首先看到__Dispatcher__的构造函数:```javascriptfunction Dispatcher() {_classCallCheck(this, Dispatcher);this._callbacks = {}; // 保存向Dispatcher注册回调函数this._isDispatching = false; // 是否正在分派Actionthis._isHandled = {}; // 已经完成执行的回调列表this._isPending = {}; // 正在执行中的回调列表this._lastID = 1; // 回调Id的起始标志}```再看注册方法__register(callback)__,每个向Dispatcher的注册的回调(callback)都拥有唯一Id进行标识:```javascript/*** 向Dispatcher注册回调函数,每个回调函数都有唯一id进行标识* @param callback* @returns {string} 注册回调的id*/Dispatcher.prototype.register = function register(callback) {var id = _prefix + this._lastID++;this._callbacks[id] = callback;return id;};/*** 根据id删除回调*/Dispatcher.prototype.unregister = function unregister(id) {!this._callbacks[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.unregister(...): `%s` does not map to a registered callback.', id) : invariant(false) : undefined;delete this._callbacks[id];};```执行一个注册了的回调函数将经历如下过程:1.标识当前正在执行的回调为进行中(Pending)状态2.将用户行为(payload)送回调执行3.执行完成,标识该回调已经完成(Handled)```javascript/*** 执行回调函数,该过程为:* 1. 标识当前正在执行的回调为Pending状态* 2. 将payload送入回调执行* 3. 执行完成,标识该回调已经完成* @internal*/Dispatcher.prototype._invokeCallback = function _invokeCallback(id) {this._isPending[id] = true;this._callbacks[id](this._pendingPayload);this._isHandled[id] = true;};```派发__dispatch(payload)__指定的用户行为payload到所有的callback将经历如下过程:首先,需要明确的是能够进行派发的前提是当前Dispatcher为空闲状态,接下来1.派发前的预处理___startDispatching()__1.初始化所有回调的状态2.设置当前正在分发的payload3.标识当前的Dispatcher状态为"正在进行派发"2.根据注册顺序依次执行回调___invokeCallback(id)__3.派发结束后的收尾工作___stopDispatching()__1.清除派发对象2.标识当前的Dispatcher状态为"结束派发"```javascript/*** 派发一个payload到所以已注册的callback中*/Dispatcher.prototype.dispatch = function dispatch(payload) {!!this._isDispatching ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.') : invariant(false) : undefined;this._startDispatching(payload);try {for (var id in this._callbacks) {if (this._isPending[id]) {continue;}this._invokeCallback(id);}} finally {this._stopDispatching();}};/*** 分发payload前的初始化:* 1. 初始化所有回调的状态* 2. 设置当前正在分发的payload* 3. 标识当前"正在进行派发"* @internal*/Dispatcher.prototype._startDispatching = function _startDispatching(payload) {for (var id in this._callbacks) {this._isPending[id] = false;this._isHandled[id] = false;}this._pendingPayload = payload;this._isDispatching = true;};/*** 结束派发时的收尾工作* 1. 清除派发对象* 2. 标识当前"结束派发"* @internal*/Dispatcher.prototype._stopDispatching = function _stopDispatching() {delete this._pendingPayload;this._isDispatching = false;};```#####waitFor再看Dispatcher中一个很重要的方法:__waitFor(ids)__, 顾名思义,该方法的作用是等待指定的回调的函数调用完成。因而,该方法主要保证了回调函数的执行的顺序性。例如,在一个航班订票系统中,我们首先要选择完国家(Country),才能选择城市(City),所以,当一个类型为“更新选择国家”的交互被送到CityStore所注册的回调时,为了保证能正确的选择更新后国家的城市```javascriptCityStore.dispatchToken = flightDispatcher.register(function(payload) {if (payload.actionType === 'country-update') {/** 如果不执行waitFor(),那么可同CityStore的回调先于ContryStore的回调执行* 此时的国家尚未更新,得到的默认城市是错误的,而并不是最新的* */flightDispatcher.waitFor([CountryStore.dispatchToken]);// waitFor()保证了ContryStore先响应了'country-update',即保证了国家更新先于城市更新// 此时我们能正确的选择该国家的城市CityStore.city = getDefaultCityForCountry(CountryStore.country);}});```下面我们看__waitFor()__的源码实现:```javascript/*** 等待指定的回调完成*/Dispatcher.prototype.waitFor = function waitFor(ids) {!this._isDispatching ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): Must be invoked while dispatching.') : invariant(false) : undefined;for (var ii = 0; ii < ids.length; ii++) {var id = ids[ii];if (this._isPending[id]) {!this._isHandled[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): Circular dependency detected while ' + 'waiting for `%s`.', id) : invariant(false) : undefined;continue;}!this._callbacks[id] ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Dispatcher.waitFor(...): `%s` does not map to a registered callback.', id) : invariant(false) : undefined;this._invokeCallback(id);}};```####Store实现在__js/stores/TodoStore.js__中:首先,我们维护我们的数据对象,并提供若干对于该数据的操作:```javascript// 保存TODO列表var _todos = {};/*** 创建一个 Todo* @param text {string} Todo内容*/function create(text) {// ...}/*** 更新一个 TODO item* @param id {string}* @param updates {object} 待更新对象的属性*/function update(id, updates) {// ...}/*** 根据一个更新属性值对象更新所有 Todo* @param updates {object}*/function updateAll(updates) {// ...}/*** 删除 Todo* @param id {string}*/function destroy(id) {// ...}/*** 删除所有的已完成的 TODO items*/function destroyCompleted() {// ...}```然后导出一个全局单例,该单例提供了常用的外部访问接口,并且通过node提供的EventEmitter来实现事件的派发和监听:```javascriptvar TodoStore = assign({}, EventEmitter.prototype, {/*** 是否所有TODO 都已完成* @return {boolean}*/areAllComplete: function () {// ...},/*** 获得所有的TODO* @returns {object}*/getAll: function () {// ...},/*** 发送变更事件*/emitChange: function () {// ...},/*** 添加变更事件监听* @param callback*/addChangeListener: function (callback) {// 一旦受到变更事件, 触发回调/**  例如, 当我们创建一条todo时,*  TodoStore将会发出一条变更事件,*  上游的状态维护器将会调用callback进行状态更新*/this.on(CHANGE_EVENT, callback);},/*** 删除变更事件监听* @param callback*/removeChangeListener: function (callback) {this.removeListener(CHANGE_EVENT, callback);}});```最后,我们需要向__AppDispatcher__注册回调函数,以便在payload被分发到TodoStore时,TodoStore能做出相应:```javascriptAppDispatcher.register(function callback(action) {var text;// 根据不同的action类型(即不同的交互逻辑), 执行不同过程switch (action.actionType) {case TodoConstants.TODO_CREATE:text = action.text.trim();if( text!=='') {create(text);// 一旦变更,发出变更事件,TodoStore.emitChange();}break;case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:// ...break;case TodoConstants.TODO_UNDO_COMPLETE:// ...break;case TodoConstants.TODO_COMPLETE:// ...break;case TodoConstants.TODO_UPDATE_TEXT:// ...break;case TodoConstants.TODO_DESTROY:// ...break;case TodoConstants.TODO_DESTROY_COMPLETED:// ...break;default:// no op}});```>!注意, 在回调执行过程中,如果发生状态的变动,需要发出变更事件,以便上游注册的回调函数能够获得相应并更新状态到下游。####Actions我们将TodoApp中常见的Action都封装到了__js/TodoActions.js__中, 通过其中的__AppDispatcher__单例,我们可以将Action派发出去:```javascriptvar TodoActions = {/*** 创建行为* @param text {string}*/create: function (text) {// 将创建行为送到Dispatcher, Dispatcher派发这个行为(action对象)到各个StoreAppDispatcher.dispatch({actionType: TodoConstants.TODO_CREATE,text: text});},/*** 更新行为* @param id {string}* @param text {string}*/updateText: function (id, text) {// ...},/*** 全部设置为完成* @param todo*/toggleComplete: function (todo) {// ...},/*** 标记所有的Todo为已完成*/toggleCompleteAll: function () {// ...},/**** @param id*/destroy: function (id) {// ...},/*** 删除所有已完成的Todo*/destroyCompleted: function() {// ...}};```####Components下面开始实现各个组件, 个人偏向的流程是先在组件目录下创建好各个组件文件,并以如下内容先导出,亦即,我们先创建空白组件,之后再依序进行装填```javascriptvar React = require('react');var Header = React.createClass({render: function () {// TODO::render},});module.exports = Header;```装填顺序我会选择先装填顶部容器(此例中即为__TodoApp__),之后按照DOM树自底向上的进行装填:__TodoApp.react.js__:```javascriptvar Footer = require('./Footer.react');var Header = require('./Header.react');var MainSection = require('./MainSection.react');var React = require('react');var TodoStore = require('../stores/TodoStore');// 在根DOM下维护状态,// 这样的状态往往是共享状态(会向下传递的状态)function getTodoState() {return {allTodos: TodoStore.getAll(),areAllComplete: TodoStore.areAllComplete()};}var TodoApp = React.createClass({getInitialState: function () {return getTodoState();},/*** 绑定生命期--挂载*/componentDidMount: function () {// 挂载时再为TodoStore添加监听器TodoStore.addChangeListener(this._onChange);},componentWillUnmount: function () {TodoStore.removeChangeListener(this._onChange);},render: function () {return (

);},/*** Event handler for 'change' events coming from the TodoStore*/_onChange: function() {this.setState(getTodoState());}});module.exports = TodoApp;```>为了方便,TodoApp不仅维护__allTodos(所有任务)__这个状态,还维护__areAllComplete(是否所有任务都已完成)__,该状态主要服务于__MainSection__中的---”完成所有/取消完成所有任务“这一用例,避免重复遍历__allTodos__的开销。我们可以看到,TodoApp提供了一个___onChange()__方法作为TodoStore的__change__事件的回调,当TodoStore发出change事件时,TodoApp将刷新状态,借此通知其下组件如MainSection等重新渲染。通过这样一个顶层组件,我们不用把对Store的事件监听和俘获进行集中化处理,避免在更多的组件的中监听Store的事件。更多组件的实现不再赘述。下面着重介绍flux的工作流程####工作流程我们以__创建新的Todo__这一工作流程为例展示Flux的工作过程。在Flux中,该流程如下图所示:![创建Todo工作流程](http://7pulhb.com2.z0.glb.clouddn.com/Flux-%E5%88%9B%E5%BB%BATodo.png)1.我们在创建Todo的输入框中敲入数据,在输入框上,我们监听了__失焦(onBlur)__和__按下键盘按键(onKeyDown)__的事件```javascript// js/components/TodoTextInput.react.js/*** @return {object}*/render: function() /*object*/ {return ();},```当事件发生时,调用___save()__方法进行处理:```_save: function() {this.props.onSave(this.state.value);this.setState({value: ''});},```2.注意,我们通过给__TodoTextInput__设定__onSave__属性来指定事件发生后的回调,在__Header__组件中,我们通过属性指定了这个回调,使得我们在失焦或回车按下后,能够像Dispatch请求派发(dispatch)一个__“创建行为”__```javascript// js/components/Header.react.js/*** @return {object}*/render: function() {return (

todos

);},/*** Event handler called within TodoTextInput.* Defining this here allows TodoTextInput to be used in multiple places* in different ways.* @param {string} text*/_onSave: function(text) {if (text.trim()){TodoActions.create(text);}}```>我们之所以不再TodoTextInput中创建Action主要是考虑到灵活性,其save后的回调通过绑定__onSave__而不是写死在__save()__中,可以派发种类更多的Action3.在__TodoActions.create()__中,我们会请求Dispatcher派发一个Todo创建行为到__TodoStore__:```javascript// js/actions/TodoActions.js/*** @param  {string} text*/create: function(text) {AppDispatcher.dispatch({actionType: TodoConstants.TODO_CREATE,text: text});},```4.TodoStore在接收到Dispatcher派发来的Action之后,其注册的回调被调用, 并且在持久化这个TODO之后,引起了全局维护的___todos__的改变,所以TodoStore会发射出一个change事件:```javascript// js/stores/TodoStore.jsAppDispatcher.register(function(action) {var text;switch(action.actionType) {case TodoConstants.TODO_CREATE:text = action.text.trim();if (text !== '') {create(text);TodoStore.emitChange();}break;// ...default:// no op}});```5.由于TodoApp向TodoStore注册了一个回调监听change事件```javascript// js/components/TodoApp.react.jscomponentDidMount: function() {TodoStore.addChangeListener(this._onChange);},```此时,change事件发生, 回调___onChange()__被触发, TodoApp维护的状态得到更新:```javascript/*** Event handler for 'change' events coming from the TodoStore*/_onChange: function() {this.setState(getTodoState());}```6.由于MainSection及Footer等组件中的属性绑定了TodoApp维护的状态,所以在TodoApp刷新状态后,二者将会重新渲染。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Flux与面向组件化开发 首先要明确的是,Flux并不是一个前端框架,而是前端的一个设计模式,其把前端的一个交互流...
    吴小蛆阅读 212评论 0 0
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • React创建组件的三种方式及其区别 React推出后,出于不同的原因先后出现三种定义react组件的方式,殊途同...
    程序猿吴彦祖阅读 251评论 0 0
  • WebView·开车指南 2016-08-31BugDev 北京市东城区首席Bug布道师开山之作,一整月交通事故血...
    53c021c38a1d阅读 798评论 0 1
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,152评论 0 4