dva源码随记

前言

在学习antd的UI框架时,了解到了dva这个轻量级的应用框架,集成了react,redux,redux-saga,react-router 。视图,数据流,路由都有。开发起来还是比较简洁的。学习难度不大,主要还是一些约定开发,因为核心还是上面的4个库。

源码主要分成两大块,dva是建立路由,视图与数据层的关系,dva-core是数据层。

开始

版本 : 2.5.0-beta.2

dva入口

src/index.js

只有100多行代码。实际是把配置传入dva-core中生成app实例,然后绑定视图,而视图就是路由组件,并且使用Provider 组件(参考官网高级用法context,有点像Vue的provide和inject)包裹,共享了store。
有些小细节。

  1. app.model是dva-core实例的方法,后面再看。
  2. app.router方法是为了检测参数是否为function。因为后期要传入对象。只接受function类型。
  3. patchHistory是为了装饰一下history.listen,使得能在监听设置时就能获得当前的location值。对于框架来说,是为了获得应用启动时的location。
  4. 除router方法外,初始化dva应用时使用的方法都是dva-core也就是数据层的方法。不过start就作了装饰代理,使数据与视图绑定,并传入history等参数给用户去绑定路由。
import React from 'react';
import invariant from 'invariant';
import createHashHistory from 'history/createHashHistory';
import {
  routerMiddleware,
  routerReducer as routing,
} from 'react-router-redux';
import document from 'global/document';
import { Provider } from 'react-redux';
import * as core from 'dva-core';
import { isFunction } from 'dva-core/lib/utils';

export default function (opts = {}) {
  const history = opts.history || createHashHistory();

  //  传给core初始化数据层。
  const createOpts = {
    initialReducer: {
      routing,
    },
    setupMiddlewares(middlewares) {
      return [
        routerMiddleware(history),// 路由的中间件
        ...middlewares,
      ];
    },
    setupApp(app) {
      app._history = patchHistory(history);// 为了能通过listen获得初始的location数据
    },
  };

  const app = core.create(opts, createOpts); // 创建数据层实例
  const oldAppStart = app.start;
  app.router = router; // router组件
  app.start = start; // 主入口
  return app;

  // 为了断言router须为function
  function router(router) {
    invariant(
      isFunction(router),
      `[app.router] router should be function, but got ${typeof router}`,
    );
    app._router = router;
  }

  //  
  function start(container) {
    // 允许 container 是字符串,然后用 querySelector 找元素
    if (isString(container)) {
      container = document.querySelector(container);
      invariant(
        container,
        `[app.start] container ${container} not found`,
      );
    }

    // 并且是 HTMLElement
    invariant(
      !container || isHTMLElement(container),
      `[app.start] container should be HTMLElement`,
    );

    // 路由必须提前注册
    invariant(
      app._router,
      `[app.start] router must be registered before app.start()`,
    );

    if (!app._store) {
      oldAppStart.call(app);
    }
    const store = app._store;

    // export _getProvider for HMR
    // ref: https://github.com/dvajs/dva/issues/469
    app._getProvider = getProvider.bind(null, store, app);

    // If has container, render; else, return react component
    if (container) {
      render(container, store, app, app._router);
      app._plugin.apply('onHmr')(render.bind(null, container, store, app));
    } else {
      return getProvider(store, this, this._router);
    }
  }
}

function isHTMLElement(node) {
  return typeof node === 'object' && node !== null && node.nodeType && node.nodeName;
}

function isString(str) {
  return typeof str === 'string';
}

function getProvider(store, app, router) {
  const DvaRoot = extraProps => (
    <Provider store={store}>
      { router({ app, history: app._history, ...extraProps }) }
    </Provider>
  );
  return DvaRoot;
}

function render(container, store, app, router) {
  const ReactDOM = require('react-dom');  // eslint-disable-line
  ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}

//  装饰一下history.listen,使得能在监听设置时能获得当前的location值
function patchHistory(history) {
  const oldListen = history.listen;
  history.listen = (callback) => {
    callback(history.location); // 初始值
    return oldListen.call(history, callback);
  };
  return history;
}

接下来就要进入dva-core.create方法里。

dva-core/src/index.js

代码的编写好清晰,主要的都在前面,主到次的写法。

  1. 第一个参数是可传入钩子函数
  2. new Plugin() 是保存及应用钩子的对象
  3. dvaModel的是在model更新时触发reducer。
  4. prefixNamespace方法是为reducers和effects的对象添加namespace前缀,并提醒开发者代码中不需要加前缀
  5. 能发现app.use 能设置钩子函数。
//  第一个参数是可传入钩子函数
export function create(hooksAndOpts = {}, createOpts = {}) {
const { initialReducer, setupApp = noop } = createOpts;

  const plugin = new Plugin();// 保存及应用钩子的对象
  plugin.use(filterHooks(hooksAndOpts)); // 筛选及保存钩子函数

  // dvaModel好似是unmodel时值加一。
  // prefixNamespace是为reducers和effects的对象添加namespace前缀,并提醒开发者代码中不需要加前缀
  // 能发现app.use 能设置钩子函数。
  const app = {
    _models: [prefixNamespace({ ...dvaModel })],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin),
    model,
    start,
  };
  return app;

接着看app.model

  1. checkModel函数:开发环境检查model对象的格式是否正确,并且namespace不能有重复
  2. 在checkModel这里再次发现reduces是可以传入数组,格式是[object,function],初步了解得知object是平时的写法,而function是store enhancer,在这function里可对store对象进行扩展
  3. 所有的model保存在了app._models中
  4. 函数返回的就是格式化后的model
// 所有的model保存在了app._models中
   // 函数返回的就是格式化后的model
  function model(m) {
    if (process.env.NODE_ENV !== 'production') {
      checkModel(m, app._models);/* 开发环境检查model对象的格式是否正确,并且namespace不能有重复
      在这里再次发现reduces是可以传入数组,格式是[object,function],
      初步了解得知object是平时的写法,而function是store enhancer,在这function里可对store对象进行扩展
      */
    }
    const prefixedModel = prefixNamespace({ ...m });
    app._models.push(prefixedModel);
    return prefixedModel;
  }

然后到最重点处原始的start方法。

  1. 原来的start函数是没有参数传入的

首先定义可触发onError钩子的函数

在全局错误处理函数中,能发现之前plugin.apply的写法的意义,调用时可设置默认的函数,并且apply后返回的是一个函数,可传入任何参数去触发,很灵活。

const onError = (err, extension) => {
      if (err) {
        if (typeof err === 'string') err = new Error(err);
        err.preventDefault = () => {
          err._dontReject = true;
        };
        plugin.apply('onError', err => {
          throw new Error(err.stack || err);
        })(err, app._store.dispatch, extension);
      }
    };

然后是获得初始化store时传入的中间件

  1. createPromiseMiddleware: 为了dispatch时找到effect的话,返回Promise,后面的处理model.effects能看到
const sagaMiddleware = createSagaMiddleware(); // saga的提供store的中间件。
    const promiseMiddleware = createPromiseMiddleware(app); 
// 为了dispatch时找到effect的话,返回Promise
export default function createPromiseMiddleware(app) {
  return () => next => action => {
    const { type } = action;
    if (isEffect(type)) {
      return new Promise((resolve, reject) => {
        next({
          __dva_resolve: resolve,
          __dva_reject: reject,
          ...action,
        });
      });
    } else {
      return next(action);
    }
  };
}

然后开始循环app._model去收集reduces与saga

  1. getReducer 合并所有的reducers成一个函数。
    可看出高阶的用法是把reduce的结果返回给数组第二个位置的function。
    有默认的defaultHandleActions,它会对所有的reducers的key分别生成key与action.type进行对比,
    相同则调用value函数,否则直接返回state的函数,最后再reduce前面所有的函数
    。有一个想法,可app.use{_handleActions:fn(用some先找出来,
    这样执行一次函数就好)}
for (const m of app._models) {
      /* getReducer 合并所有的reducers成一个函数。
      可看出高阶的用法是把reduce的结果返回给数组第二个位置的function。
      有默认的defaultHandleActions,它会对所有的reducers的key分别生成key与action.type进行对比,
      相同则调用value函数,否则直接返回state的函数,最后再reduce前面所有的函数
      。有一个想法,可app.use{_handleActions:fn(用some先找出来,
        这样执行一次函数就好)}*/
      reducers[m.namespace] = getReducer(
        m.reducers,
        m.state,
        plugin._handleActions
      );
      if (m.effects)
        // 初始化saga传入effects对象,model,全局错误处理函数,onEffect的钩子数组
        sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
    }

我们重点看看getSaga函数。

  1. 这是初始化saga,传入effects对象,model,全局错误处理函数,onEffect的钩子数组
  2. 对每一个watcher开出一个并行任务
  3. 同时对外设置了cancel上面的任务的方法。dispatch({type:${model.namespace}/@@CANCEL_EFFECTS})就cancel该model的所有effects。(主要用于给app.unmodel与app.replaceModel)
 function getSaga(effects, model, onError, onEffect) {
  return function*() {
    for (const key in effects) {
      // 保证是原始的hasOwnProperty条用
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        // 生成一个观察者。
        const watcher = getWatcher(key, effects[key], model, onError, onEffect);
        // 对每一个watcher开出一个并行任务
        const task = yield sagaEffects.fork(watcher);
        // 同时对外设置了cancel上面的任务的方法。put(`${model.namespace}/@@CANCEL_EFFECTS`)就cancel
        // 该model的所有effects。
        yield sagaEffects.fork(function*() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}

明显重点在getWatcher。

  1. 默认type类型是takeEvery
  2. effect写成数组形式可以在arr[0]中传入opt,可以设置观察的type,
    只能是'watcher', 'takeEvery', 'takeLatest', 'throttle'的之一

直接看默认的takeEvery

最后执行的是

return function*() {
        yield takeEvery(key, sagaWithOnEffect);
      }

为什么要是sagaWithOnEffect呢,因为有onEffect钩子,这是提供修改effect的钩子。

  1. onEffect钩子可获取的参数是effect,saga的操作集合,该model对象,effect的key
// 触发onEffect的钩子,这个可以修改初始化的saga
  const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);

....
....

function applyOnEffect(fns, effect, model, key) {
  for (const fn of fns) {
    // 传入的参数为包装好的effect,saga的操作集合,该model对象,effect的key
    effect = fn(effect, sagaEffects, model, key);
  }
  return effect;
}

所以重点的转到了sagaWithCatch。

  1. 还记得createPromiseMiddleware中间件吗,是effect的action会next({ __dva_resolve: resolve, __dva_reject: reject, ...action, }),所以这里能取得中间件返回promise的resolve和reject。由此,对于effect的action,我们可以用dispatch({ type: 'any/any', payload: xxx, }).then(() => ...);去在effect结束或者报错时作一些操作
  2. 可在reducers中设置开始${key}${NAMESPACE_SEP}@@start与结束的钩子${key}${NAMESPACE_SEP}@@end。如果没报错的话,确实end可以当做resolve去使用。
  3. 报错的话肯定会触发包装过的全局公用onError,但如果设置钩子时执行了err.preventDefault(),则不再抛出错误,也就是dispatch().catch()无效
function* sagaWithCatch(...args) {
    const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
      args.length > 0 ? args[0] : {};
    try {
      // effect开始钩子
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
      // createEffects: 把加工过的操作符集合加在参数的最后一个。
      // 并对put,put.resolve,take做了包装,使得不需要传type时不需要加namespace
      const ret = yield effect(...args.concat(createEffects(model)));
      // effect结束钩子
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
      resolve(ret);
    } catch (e) {
      onError(e, {
        key,
        effectArgs: args,
      });
      // 如果在onError钩子中执行了err.preventDefault(),则不再抛出错误
      if (!e._dontReject) {
        reject(e);
      }
    }
  }

saga与reducer收集好后,就可以创建store了

  1. createReducer是把对combineReducer(还传入了非model里的reducersplugin.get('extraReducers'))后reducer传入onReducer钩子组合成的reducerEnhancer函数(plugin.get('onReducer'))
  2. plugin.get('extraEnhancers')获得用户设置的store增强工具
  3. plugin.get('onAction')获得用户设置的store中间件。
  4. 然后收集所有框架内中间键,dva-core中有promiseMiddleware与sagaMiddleware,dva中有routerMiddleware(history),从createOpts传入setupMiddlewares 函数setupMiddlewares(middlewares) { return [ routerMiddleware(history), ...middlewares, ]; },
  5. 对于 redux 中 的 compose 函数,在数组长度为 1 的情况下返回第一个元素。compose(...enhancers) 等同于 applyMiddleware(...middlewares)
const store = (app._store = createStore({
      // eslint-disable-line
      reducers: createReducer(),
      initialState: hooksAndOpts.initialState || {},
      plugin,
      createOpts,
      sagaMiddleware,
      promiseMiddleware,
    }));

function({
  reducers,
  initialState,
  plugin,
  sagaMiddleware,
  promiseMiddleware,
  createOpts: { setupMiddlewares = returnSelf },
}) {
  // extra enhancers
  const extraEnhancers = plugin.get('extraEnhancers');
  invariant(
    isArray(extraEnhancers),
    `[app.start] extraEnhancers should be array, but got ${typeof extraEnhancers}`
  );

  // 由这个初始化可以知道,onAction钩子必须在app.start前设置
  const extraMiddlewares = plugin.get('onAction');
  const middlewares = setupMiddlewares([
    promiseMiddleware,
    sagaMiddleware,
    ...flatten(extraMiddlewares),
  ]);

  const composeEnhancers =
  process.env.NODE_ENV !== "production" &&
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    : compose;

  const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];

  return createStore(reducers, initialState, composeEnhancers(...enhancers));
}
  1. 把saga中间件的监听Saga的函数赋值给store.runSaga
  2. 把记录运行中传入的reducers的store.asyncReducers设为空对象
store.runSaga = sagaMiddleware.run; // 开始监听Saga的函数
    store.asyncReducers = {};
  1. 登记onStateChange钩子,其实就是调用store.subscribe去触发
// 其实就是调用store.subscribe去触发onStateChange钩子
    const listeners = plugin.get('onStateChange');
    for (const listener of listeners) {
      store.subscribe(() => {
        listener(store.getState());
      });
    }

监听所有的Saga

// 循环监听Saga
    sagas.forEach(sagaMiddleware.run);

执行了dva传入的setupApp(app) {app._history = patchHistory(history)} 给app赋值了_history

setupApp(app) {
      app._history = patchHistory(history);
    },  给app赋值了_history*/ 
    setupApp(app);

接着处理subscriptions,遍历models去订阅

  1. 订阅所有model的subscriptions,并记录返回取消订阅的方法
const unlisteners = {};
    for (const model of this._models) {
      if (model.subscriptions) {
        // 订阅所有model的subscriptions,并返回取消订阅的方法
        unlisteners[model.namespace] = runSubscription(
          model.subscriptions,
          model,
          app,
          onError
        );
      }
    }

我们暂停往下,关注runSubscription

  1. subscriptions中参数了dispatch只能触发当前model的action,因为会自动加prefix
  2. 有history对象,就是说可以切换路由
  3. 可触发onError钩子。
  4. 有可能移除或覆盖model的话,用户必须返回取消订阅的方法。不返回的话,下面unlisten方法(遍历运行而已)会发出警告
function run(subs, model, app, onError) {
  const funcs = [];
  const nonFuncs = [];
  for (const key in subs) {
    if (Object.prototype.hasOwnProperty.call(subs, key)) {
      const sub = subs[key];
      const unlistener = sub({
        dispatch: prefixedDispatch(app._store.dispatch, model),
        history: app._history,
      }, onError);
      if (isFunction(unlistener)) {
        funcs.push(unlistener);
      } else {
        nonFuncs.push(key);
      }
    }
  }
  return { funcs, nonFuncs };
}

回到主线。基本到最后了。添加工具函数app.model,app.unmodel,app.replaceModel。

先看app.model

  1. 此函数已bind了前3个参数:初始化合并reducers的函数,处理错误函数,记录取消订阅的对象。
app.model = injectModel.bind(app, createReducer, onError, unlisteners);
1. model(m)之前讲过,是检查及保存model。
2. 把此模块的reducers合成后赋值给store.asyncReducers[m.namespace] 
3. 调用原始的store. replaceReducer,会与store.asyncReducers作比较合并,createReducer就是过得当前所有reducers的combine。
4. 监听effect与订阅subscriptions与之前一致
function injectModel(createReducer, onError, unlisteners, m) {
    m = model(m);

    const store = app._store;
    store.asyncReducers[m.namespace] = getReducer(
      m.reducers,
      m.state,
      plugin._handleActions
    );
    // 调用原始的replaceReducer,会与store.asyncReducers作比较合并
    store.replaceReducer(createReducer());
    if (m.effects) {
      store.runSaga(
        app._getSaga(m.effects, m, onError, plugin.get('onEffect'))
      );
    }
    if (m.subscriptions) {
      unlisteners[m.namespace] = runSubscription(
        m.subscriptions,
        m,
        app,
        onError
      );
    }
  }

我们再看看 app.unmodel,移除model

  1. 删除reducers是直接把store.asyncReducers与reducers里的key删除,简单粗暴。然后再次执行store.replaceReducer(createReducer())
  2. cancel effects前面一分析了,整个model的effect tasks移除
  3. unlisteners在之前订阅时已经收集过了,所以直接根据namespace取消就好
  4. 最后记得移除app._models里对应的model
function unmodel(createReducer, reducers, unlisteners, namespace) {
    const store = app._store;

    // Delete reducers
    delete store.asyncReducers[namespace];
    delete reducers[namespace];

    store.replaceReducer(createReducer());
    store.dispatch({ type: '@@dva/UPDATE' });

    // Cancel effects
    store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });

    // Unlisten subscrioptions
    unlistenSubscription(unlisteners, namespace);

    // Delete model from app._models
    app._models = app._models.filter(model => model.namespace !== namespace);
  }

最后app.replaceModel逻辑其实就是unmodel后model。

还有就是,内部@@dva的model,是会在replaceModel与unmodel中进行update的action:state自增1。

这样整个dva框架的流程就走完了。挺轻量巧妙的。

编外:

dva还提供有一些工具函数:fetch,dynamic

fetch只是export了isomorphic-fetch

dynamic动态加载model与视图

用法:
app: dva 实例,加载 models 时需要
models: 返回 Promise 数组的函数,Promise 返回 dva model
component:返回 Promise 的函数,Promise 返回 React Component

const UserPageComponent = dynamic({
  app,
  models: () => [
    import('./models/users'),
  ],
  component: () => import('./routes/UserPage'),
});
  1. 传入resolve返回一个async组件。获取所有的models和component,model可为空。
  2. 组件中有一个AsyncComponent的state,render函数是根据AsyncComponent是否为空去渲染的,所以只要resolve后更新state就好了,这里也了解到一点,组件挂载前不需要使用setState去更新state。
  3. 可使用dynamic.setDefaultLoadingComponent去设置加载时的过度组件。
function dynamic(config) {
  const { app, models: resolveModels, component: resolveComponent } = config;
  return asyncComponent({
    resolve: config.resolve || function () {
      const models = typeof resolveModels === 'function' ? resolveModels() : [];
      const component = resolveComponent();
      return new Promise((resolve) => {
        Promise.all([...models, component]).then((ret) => {
          if (!models || !models.length) {
            return resolve(ret[0]);
          } else {
            const len = models.length;
            ret.slice(0, len).forEach((m) => {
              m = m.default || m;
              if (!Array.isArray(m)) {
                m = [m];
              }
              m.map(_ => registerModel(app, _));
            });
            resolve(ret[len]);
          }
        });
      });
    },
    ...config,
  });
}

function asyncComponent(config) {
  const { resolve } = config;

  return class DynamicComponent extends Component {
    constructor(...args) {
      super(...args);
      this.LoadingComponent =
        config.LoadingComponent || defaultLoadingComponent;
      this.state = {
        AsyncComponent: null,
      };
      this.load();
    }

    componentDidMount() {
      this.mounted = true;
    }

    componentWillUnmount() {
      this.mounted = false;
    }

    load() {
      resolve().then((m) => {
        const AsyncComponent = m.default || m;
        if (this.mounted) {
          this.setState({ AsyncComponent });
        } else {
          this.state.AsyncComponent = AsyncComponent; // eslint-disable-line
        }
      });
    }

    render() {
      const { AsyncComponent } = this.state;
      const { LoadingComponent } = this;
      if (AsyncComponent) return <AsyncComponent {...this.props} />;

      return <LoadingComponent {...this.props} />;
    }
  };
}

到此,对dva源码分析完成。接着下篇学习及分析umi

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

推荐阅读更多精彩内容