Dva源碼解析

Dva 源碼解析

作者:楊光
dva官網(wǎng)源碼解析

隱藏在 package.json 里的秘密


隨便哪個 dva 的項目并淋,只要敲入 npm start 就可以運行啟動。之前敲了無數(shù)次我都沒有在意绳军,直到我準備研究源碼的時候才意識到:在敲下這行命令的時候锨推,到底發(fā)生了什么呢?

答案要去 package.json 里去尋找余佃。

有位技術(shù)大牛曾經(jīng)告訴過我:看源碼之前蔽氨,先去看 package.json 藐唠。看看項目的入口文件孵滞,翻翻它用了哪些依賴中捆,對項目便有了大致的概念。

package.json 里是這么寫的:

 "scripts": {
    "start": "roadhog server"
  },

翻翻依賴坊饶,"roadhog": "^0.5.2"

既然能在 devDependencies 找到蟋滴,那么肯定也能在 npm 上找到津函。原來是個和 webpack 相似的庫尔苦,而且作者看著有點眼熟...

如果說 dva 是親女兒,那 roadhog 就是親哥哥了允坚,起的是 webpack 自動打包和熱更替的作用魂那。

在 roadhog 的默認配置里有這么一條信息:

{
  "entry": "src/index.js",
}

后轉(zhuǎn)了一圈,啟動的入口回到了 src/index.js稠项。

src/index.js


src/index.js 里涯雅,dva 一共做了這么幾件事:

  1. 從 'dva' 依賴中引入 dva :import dva from 'dva';

  2. 通過函數(shù)生成一個 app 對象:const app = dva();

  3. 加載插件:app.use({});

  4. 注入 model:app.model(require('./models/example'));

  5. 添加路由:app.router(require('./routes/indexAnother'));

  6. 啟動:app.start('#root');

在這 6 步當中,dva 完成了 使用 React 解決 view 層展运、redux 管理 model活逆、saga 解決異步的主要功能。事實上在我查閱資料以及回憶用過的腳手架時拗胜,發(fā)現(xiàn)目前端框架之所以被稱為“框架”也就是解決了這些事情蔗候。前端工程師至今所做的事情都是在 分離動態(tài)的 data 和靜態(tài)的 view ,只不過側(cè)重點和實現(xiàn)方式也不同埂软。

至今為止出了這么多框架琴庵,但是前端 MVX 的思想一直都沒有改變。

dva

尋找 “dva”


既然 dva 是來自于 dva仰美,那么 dva 是什么這個問題自然要去 dva 的源碼中尋找了。

劇透:dva 是個函數(shù)诉字,返回一了個 app 的對象琅轧。

劇透2:目前 dva 的源碼核心部分包含兩部分,dvadva-core乍桂。前者用高階組件 React-redux 實現(xiàn)了 view 層睹酌,后者是用 redux-saga 解決了 model 層权谁。

老規(guī)矩,還是先翻 package.json 旺芽。

引用依賴很好的說明了 dva 的功能:統(tǒng)一 view 層洗出。

// dva 使用的依賴如下:

    "babel-runtime": "^6.26.0", // 一個編譯后文件引用的公共庫菠镇,可以有效減少編譯后的文件體積
    "dva-core": "^1.1.0", // dva 另一個核心隘梨,用于處理數(shù)據(jù)層
    "global": "^4.3.2", // 用于提供全局函數(shù)的引用
    "history": "^4.6.3", // browserHistory 或者 hashHistory
    "invariant": "^2.2.2", // 一個有趣的斷言庫
    "isomorphic-fetch": "^2.2.1", // 方便請求異步的函數(shù)捻脖,dva 中的 fetch 來源
    "react-async-component": "^1.0.0-beta.3", // 組件懶加載
    "react-redux": "^5.0.5", // 提供了一個高階組件矛渴,方便在各處調(diào)用 store
    "react-router-dom": "^4.1.2", // router4桂躏,終于可以像寫組件一樣寫 router 了
    "react-router-redux": "5.0.0-alpha.6",// redux 的中間件,在 provider 里可以嵌套 router
    "redux": "^3.7.2" // 提供了 store川陆、dispatch、reducer 

不過 script 沒有給太多有用的信息,因為 ruban build 中的 ruban 顯然是個私人庫(雖然在 tnpm 上可以查到但是也是私人庫)拂封。但根據(jù)慣例,應(yīng)該是 dva 包下的 index.js 文件提供了對外調(diào)用:

Object.defineProperty(exports, "__esModule", {
  value: true
});

exports.default = require('./lib');
exports.connect = require('react-redux').connect;

顯然這個 exports.default 就是我們要找的 dva,但是源碼中沒有 ./lib 文件夾缘回。當然直接看也應(yīng)該看不懂,因為一般都是使用 babel 的命令 babel src -d libs 進行編譯后生成的在孝,所以直接去看 src/index.js 文件魔招。

src/index.js


src/index.js在此

在這里,dva 做了三件比較重要的事情:

  1. 使用 call 給 dva-core 實例化的 app(這個時候還只有數(shù)據(jù)層) 的 start 方法增加了一些新功能(或者說髓迎,通過代理模式給 model 層增加了 view 層)。
  2. 使用 react-redux 完成了 react 到 redux 的連接。
  3. 添加了 redux 的中間件 react-redux-router床牧,強化了 history 對象的功能。

使用 call 方法實現(xiàn)代理模式

dva 中實現(xiàn)代理模式的方式如下:

1. 新建 function 暂吉,函數(shù)內(nèi)實例化一個 app 對象。 2. 新建變量指向該對象希望代理的方法绕辖, oldStart = app.start围小。 3. 新建同名方法 start刘绣,在其中使用 call绢馍,指定 oldStart 的調(diào)用者為 app。 4. 令 app.start = start源请,完成對 app 對象的 start 方法的代理决瞳。

上代碼:

export default function(opts = {}) {

  // ...初始化 route 胸囱,和添加 route 中間件的方法允蜈。

  /**
   * 1\. 新建 function 蛤克,函數(shù)內(nèi)實例化一個 app 對象彻犁。
   * 
   */
  const app = core.create(opts, createOpts);
  /**
   * 2\. 新建變量指向該對象希望代理的方法
   * 
   */
  const oldAppStart = app.start;
  app.router = router;
  /**
   * 4\. 令 app.start = start豺型,完成對 app 對象的 start 方法的代理仲智。
   * @type {[type]}
   */
  app.start = start;
  return app;

  // router 賦值

  /**
   * 3.1 新建同名方法 start,
   * 
   */
  function start(container) {
    // 合法性檢測代碼

    /**
     * 3.2 在其中使用 call姻氨,指定 oldStart 的調(diào)用者為 app钓辆。
     */
    oldAppStart.call(app);

    // 因為有 3.2 的執(zhí)行才有現(xiàn)在的 store
    const store = app._store;

    // 使用高階組件創(chuàng)建視圖
  }
}

為什么不直接在 start 方式中 oldAppStart ?

  • 因為 dva-core 的 start 方法里有用到 this肴焊,不用 call 指定調(diào)用者為 app 的話似嗤,oldAppStart() 會找錯對象。

實現(xiàn)代理模式一定要用到 call 嗎届宠?

  • 不一定烁落,看有沒有 使用 this 或者代理的函數(shù)是不是箭頭函數(shù)。從另一個角度來說豌注,如果使用了 function 關(guān)鍵字又在內(nèi)部使用了 this伤塌,那么一定要用 call/apply/bind 指定 this。

前端還有那里會用到 call 幌羞?

  • 就實際開發(fā)來講寸谜,因為已經(jīng)使用了 es6 標準竟稳,基本和 this 沒什么打交道的機會属桦。使用 class 類型的組件中偶爾還會用到 this.xxx.bind(this)熊痴,stateless 組件就洗洗睡吧(因為壓根沒有 this)。如果實現(xiàn)代理聂宾,可以使用繼承/反向繼承的方法 —— 比如高階組件果善。

使用 react-redux 的高階組件傳遞 store

經(jīng)過 call 代理后的 start 方法的主要作用,便是使用 react-redux 的 provider 組件將數(shù)據(jù)與視圖聯(lián)系了起來系谐,生成 React 元素呈現(xiàn)給使用者巾陕。

不多說,上代碼纪他。

// 使用 querySelector 獲得 dom
if (isString(container)) {
  container = document.querySelector(container);
  invariant(
    container,
    `[app.start] container ${container} not found`,
  );
}

// 其他代碼

// 實例化 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
// 如果有真實的 dom 對象就把 react 拍進去
if (container) {
  render(container, store, app, app._router);
  // 熱加載在這里
  app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
  // 否則就生成一個 react 鄙煤,供外界調(diào)用
  return getProvider(store, this, this._router);
}

 // 使用高階組件包裹組件
function getProvider(store, app, router) {
  return extraProps => (
    <Provider store={store}>
      { router({ app, history: app._history, ...extraProps }) }
    </Provider>
  );
}

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

React.createElement(getProvider(store, app, router)) 怎么理解?

  • getProvider 實際上返回的不單純是函數(shù)茶袒,而是一個無狀態(tài)的 React 組件梯刚。從這個角度理解的話,ReactElement.createElement(string/ReactClass type,[object props],[children ...]) 是可以這么寫的薪寓。

怎么理解 React 的 stateless 組件和 class 組件亡资?

  • 你猜猜?
JavaScript 并不存在 class 這個東西向叉,即便是 es6 引入了以后經(jīng)過 babel 編譯也會轉(zhuǎn)換成函數(shù)锥腻。因此直接使用無狀態(tài)組件,省去了將 class 實例化再調(diào)用 render 函數(shù)的過程母谎,有效的加快了渲染速度瘦黑。

即便是 class 組件,React.createElement 最終調(diào)用的也是 render 函數(shù)销睁。不過這個目前只是我的推論供璧,沒有代碼證據(jù)的證明。

react-redux 與 provider

provider 是個什么東西冻记?

本質(zhì)上是個高階組件睡毒,也是代理模式的一種實踐方式。接收 redux 生成的 store 做參數(shù)后冗栗,通過上下文 context 將 store 傳遞進被代理組件演顾。在保留原組件的功能不變的同時,增加了 store 的 dispatch 等方法隅居。

connect 是個什么東西钠至?

connect 也是一個代理模式實現(xiàn)的高階組件,為被代理的組件實現(xiàn)了從 context 中獲得 store 的方法胎源。

connect()(MyComponent) 時發(fā)生了什么棉钧?

只放關(guān)鍵部分代碼,因為我也只看懂了關(guān)鍵部分(捂臉跑):

import connectAdvanced from '../components/connectAdvanced' 
export function createConnect({
  connectHOC = connectAdvanced,
.... 其他初始值
} = {}) {

  return function connect( { // 0 號 connnect
    mapStateToProps,
    mapDispatchToProps,
    ... 其他初始值
    } = {}
  ) {
    ....其他邏輯
    return connectHOC(selectorFactory, {//  1號 connect
        .... 默認參數(shù)
        selectorFactory 也是個默認參數(shù)
      })
  }
}

export default createConnect() // 這是 connect 的本體涕蚤,導(dǎo)出時即生成 connect 0

// hoist-non-react-statics宪卿,會自動把所有綁定在對象上的非React方法都綁定到新的對象上
import hoistStatics from 'hoist-non-react-statics'
// 1號 connect 的本體
export default function connectAdvanced() {
    // 邏輯處理

    // 1 號 connect 調(diào)用時生成 2 號 connect
  return function wrapWithConnect(WrappedComponent) {
    // ... 邏輯處理

    // 在函數(shù)內(nèi)定義了一個可以拿到上下文對象中 store 的組件
    class Connect extends Component {

      getChildContext() {
        // 上下文對象中獲得 store
        const subscription = this.propsMode ? null : this.subscription
        return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
      }

        // 邏輯處理

      render() {

            //  最終生成了新的 react 元素的诵,并添加了新屬性
          return createElement(WrappedComponent, this.addExtraProps(selector.props))

      }
    }

    // 邏輯處理

    // 最后用定義的 class 和 被代理的組件生成新的 react 組件
    return hoistStatics(Connect, WrappedComponent)  // 2 號函數(shù)調(diào)用后生成的對象是組件
  }
}

結(jié)論:對于 connect()(MyComponent)

  1. connect 調(diào)用時生成 0 號 connect
  2. connect() 0 號 connect 調(diào)用,返回 1 號 connect 的調(diào)用 connectHOC() 佑钾,生成 2 號 connect(也是個函數(shù)) 西疤。
  3. connect()(MyComponent) 等價于 connect2(MyComponent),返回值是一個新的組件

redux 與 router

redux 是狀態(tài)管理的庫休溶,router 是(唯一)控制頁面跳轉(zhuǎn)的庫代赁。兩者都很美好,但是不美好的是兩者無法協(xié)同工作兽掰。換句話說芭碍,當路由變化以后,store 無法感知到孽尽。

于是便有了 react-router-redux豁跑。

react-router-redux 是 redux 的一個中間件(中間件:JavaScript 代理模式的另一種實踐 針對 dispatch 實現(xiàn)了方法的代理,在 dispatch action 的時候增加或者修改) 泻云,主要作用是:

加強了React Router庫中history這個實例艇拍,以允許將history中接受到的變化反應(yīng)到state中去。

github 在此

從代碼上講宠纯,主要是監(jiān)聽了 history 的變化:

history.listen(location => analyticsService.track(location.pathname))

dva 在此基礎(chǔ)上又進行了一層代理卸夕,把代理后的對象當作初始值傳遞給了 dva-core,方便其在 model 的 subscriptions 中監(jiān)聽 router 變化婆瓜。

看看 index.js 里 router 的實現(xiàn):

1.在 createOpts 中初始化了添加 react-router-redux 中間件的方法和其 reducer 快集,方便 dva-core 在創(chuàng)建 store 的時候直接調(diào)用。

  1. 使用 patchHistory 函數(shù)代理 history.linsten廉白,增加了一個回調(diào)函數(shù)的做參數(shù)(也就是訂閱)个初。

subscriptions 的東西可以放在 dva-core 里再說,

import createHashHistory from 'history/createHashHistory';
import {
  routerMiddleware,
  routerReducer as routing,
} from 'react-router-redux';
import * as core from 'dva-core';

export default function (opts = {}) {
  const history = opts.history || createHashHistory();
  const createOpts = {
    //  初始化 react-router-redux 的 router
    initialReducer: {
      routing,
    },
    // 初始化 react-router-redux 添加中間件的方法猴蹂,放在所有中間件最前面
    setupMiddlewares(middlewares) {
      return [
        routerMiddleware(history),
        ...middlewares,
      ];
    },
    // 使用代理模式為 history 對象增加新功能院溺,并賦給 app
    setupApp(app) {
      app._history = patchHistory(history);
    },
  };

  const app = core.create(opts, createOpts);
  const oldAppStart = app.start;
  app.router = router;
  app.start = start;
  return app;

  function router(router) {
    invariant(
      isFunction(router),
      `[app.router] router should be function, but got ${typeof router}`,
    );
    app._router = router;
  }

}

// 使用代理模式擴展 history 對象的 listen 方法,添加了一個回調(diào)函數(shù)做參數(shù)并在路由變化是主動調(diào)用
function patchHistory(history) {
  const oldListen = history.listen;
  history.listen = (callback) => {
    callback(history.location);
    return oldListen.call(history, callback);
  };
  return history;
}

劇透:redux 中創(chuàng)建 store 的方法為:

// combineReducers 接收的參數(shù)是對象
// 所以 initialReducer 的類型是對象
// 作用:將對象中所有的 reducer 組合成一個大的 reducer
const reducers = {}; 
// applyMiddleware 接收的參數(shù)是可變參數(shù)
// 所以 middleware 是數(shù)組
// 作用:將所有中間件組成一個數(shù)組磅轻,依次執(zhí)行
const middleware = []; 
const store = createStore(
  combineReducers(reducers),
  initial_state, // 設(shè)置 state 的初始值
  applyMiddleware(...middleware)
);

視圖與數(shù)據(jù)(上)


src/index.js 主要實現(xiàn)了 dva 的 view 層珍逸,同時傳遞了一些初始化數(shù)據(jù)到 dva-core 所實現(xiàn)的 model 層。當然聋溜,還提供了一些 dva 中常用的方法函數(shù):

  • dynamic 動態(tài)加載(2.0 以后官方提供 1.x 自己手動實現(xiàn)吧)
  • fetch 請求方法(其實 dva 只是做了一把搬運工)
  • saga(數(shù)據(jù)層處理異步的方法)谆膳。

這么看 dva 真的是很薄的一層封裝。

而 dva-core 主要解決了 model 的問題撮躁,包括 state 管理漱病、數(shù)據(jù)的異步加載、訂閱-發(fā)布模式的實現(xiàn),可以作為數(shù)據(jù)層在別處使用(看 2.0 更新也確實是作者的意圖)杨帽。使用的狀體啊管理庫還是 redux凝果,異步加載的解決方案是 saga。當然睦尽,一切也都寫在 index.js 和 package.json 里。

視圖與數(shù)據(jù)(下)


處理 React 的 model 層問題有很多種辦法型雳,比如狀態(tài)管理就不一定要用 Redux当凡,也可以使用 Mobx(寫法會更有 MVX 框架的感覺);異步數(shù)據(jù)流也未必使用 redux-saga纠俭,redux-thunk 或者 redux-promise 的解決方式也可以(不過目前看來 saga 是相對更優(yōu)雅的)沿量。

放兩篇個人感覺比較全面的技術(shù)文檔:

以及兩者的 github:

然后繼續(xù)深扒 dva-core朴则,還是先從 package.json 扒起。

package.json


dva-corepackage.json 中依賴包如下:

    "babel-runtime": "^6.26.0",  // 一個編譯后文件引用的公共庫钓简,可以有效減少編譯后的文件體積
    "flatten": "^1.0.2", // 一個將多個數(shù)組值合并成一個數(shù)組的庫
    "global": "^4.3.2",// 用于提供全局函數(shù)比如 document 的引用
    "invariant": "^2.2.1",// 一個有趣的斷言庫
    "is-plain-object": "^2.0.3", // 判斷是否是一個對象
    "redux": "^3.7.1", // redux 乌妒,管理 react 狀態(tài)的庫
    "redux-saga": "^0.15.4", // 處理異步數(shù)據(jù)流
    "warning": "^3.0.0" // 同樣是個斷言庫,不過輸出的是警告

當然因為打包還是用的 ruban外邓,script 里沒有什么太多有用的東西撤蚊。繼續(xù)依循慣例,去翻 src/index.js损话。

src/index.js


src/index 的源碼在這里

dvasrc/index.js 里侦啸,通過傳遞 2 個變量 optscreateOpts 并調(diào)用 core.createdva 創(chuàng)建了一個 app 對象丧枪。其中 opts 是使用者添加的控制選項光涂,createOpts 則是初始化了 reducer 與 redux 的中間件。

dva-coresrc/index.js 里便是這個 app 對象的具體創(chuàng)建過程以及包含的方法:

export function create(hooksAndOpts = {}, createOpts = {}) {
  const {
    initialReducer,
    setupApp = noop,
  } = createOpts;

  const plugin = new Plugin();
  plugin.use(filterHooks(hooksAndOpts));

  const app = {
    _models: [
      prefixNamespace({ ...dvaModel }),
    ],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin),
    model,
    start,
  };
  return app;
    // .... 方法的實現(xiàn)

    function model(){
        // model 方法
    }

    functoin start(){
        // Start 方法
    }
  }

我最開始很不習(xí)慣 JavaScript 就是因為 JavaScript 還是一個函數(shù)向的編程語言拧烦,也就是函數(shù)里可以定義函數(shù)忘闻,返回值也可以是函數(shù),class 最后也是被解釋成函數(shù)恋博。在 dva-core 里創(chuàng)建了 app 對象服赎,但是把 model 和 start 的定義放在了后面。一開始對這種簡寫沒看懂交播,后來熟悉了以后發(fā)現(xiàn)確實好理解重虑。一眼就可以看到 app 所包含的方法,如果需要研究具體方法的話才需要向后看秦士。

Plugin 是作者設(shè)置的一堆鉤子性監(jiān)聽函數(shù)——即是在符合某些條件的情況下下(dva 作者)進行手動調(diào)用缺厉。這樣使用者只要按照作者設(shè)定過的關(guān)鍵詞傳遞回調(diào)函數(shù),在這些條件下便會自動觸發(fā)。

有趣的是提针,我最初理解鉤子的概念是在 Angular 里命爬。為了能像 React 一樣優(yōu)雅的控制組件的生命周期,Angular 設(shè)置了一堆接口(因為使用的是 ts辐脖,所以 Angular 里有類和接口的區(qū)分)饲宛。只要組件實現(xiàn)(implements)對應(yīng)的接口————或者稱生命周期鉤子,在對應(yīng)的條件下就會運行接口的方法嗜价。

Plugin 與 plugin.use

Plugin 與 plugin.use 都有使用數(shù)組的 reduce 方法的行為:

const hooks = [
  'onError',
  'onStateChange',
  'onAction',
  'onHmr',
  'onReducer',
  'onEffect',
  'extraReducers',
  'extraEnhancers',
];

export function filterHooks(obj) {
  return Object.keys(obj).reduce((memo, key) => {
  // 如果對象的 key 在 hooks 數(shù)組中
  // 為 memo 對象添加新的 key艇抠,值為 obj 對應(yīng) key 的值
    if (hooks.indexOf(key) > -1) {
      memo[key] = obj[key];
    }
    return memo;
  }, {});
}

export default class Plugin {
  constructor() {
    this.hooks = hooks.reduce((memo, key) => {
      memo[key] = [];
      return memo;
    }, {});
    /*
        等同于

        this.hooks = {
            onError: [],
            onStateChange:[],
            ....
            extraEnhancers: []
        }
    */
  }

  use(plugin) {
    invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object');
    const hooks = this.hooks;
    for (const key in plugin) {
      if (Object.prototype.hasOwnProperty.call(plugin, key)) {
        invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
        if (key === 'extraEnhancers') {
          hooks[key] = plugin[key];
        } else {
          hooks[key].push(plugin[key]);
        }
      }
    }
  }

  // 其他方法
}

  • 構(gòu)造器中的 reduce 初始化了一個以 hooks 數(shù)組所有元素為 key,值為空數(shù)組的對象久锥,并賦給了 class 的私有變量 this.hooks家淤。

  • filterHooks 通過 reduce 過濾了 hooks 數(shù)組以外的鉤子。

  • use 中使用 hasOwnProperty 判斷 keyplugin 的自身屬性還是繼承屬性瑟由,使用原型鏈調(diào)用而不是 plugin.hasOwnProperty() 是防止使用者故意搗亂在 plugin 自己寫一個 hasOwnProperty = () => false // 這樣無論如何調(diào)用 plugin.hasOwnProperty() 返回值都是 false絮重。

  • use 中使用 reducethis.hooks 添加了 plugin[key]

model 方法


model 是 app 添加 model 的方法歹苦,在 dva 項目 的 index.js 是這么用的青伤。

app.model(require('./models/example'));

dva 中沒對 model 做任何處理,所以 dva-core 中的 model 就是 dva 項目 里調(diào)用的 model殴瘦。

  function model(m) {
    if (process.env.NODE_ENV !== 'production') {
      checkModel(m, app._models);
    }
    app._models.push(prefixNamespace(m));
  }

  • checkModel 主要是用 invariant 對傳入的 model 進行了合法性檢查潮模。

  • prefixNamespace 又使用 reduce 對每一個 model 做處理,為 model 的 reducers 和 effects 中的方法添加了 ${namespace}/ 的前綴痴施。

Ever wonder why we dispatch the action like this in dva ? dispatch({type: 'example/loadDashboard'

start 方法


start 方法是 dva-core 的核心擎厢,在 start 方法里,dva 完成了 store 初始化 以及 redux-saga 的調(diào)用辣吃。比起 dvastart动遭,它引入了更多的調(diào)用方式。

一步一步分析:

onError

    const onError = (err) => {
      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);
      }
    };

這是一個全局錯誤處理神得,返回了一個接收錯誤并處理的函數(shù)厘惦,并以 errapp._store.dispatch 為參數(shù)執(zhí)行調(diào)用。

看一下 plugin.apply 的實現(xiàn):

  apply(key, defaultHandler) {
    const hooks = this.hooks;
    /* 通過 validApplyHooks 進行過濾哩簿, apply 方法只能應(yīng)用在全局報錯或者熱更替上 */ 
    const  validApplyHooks = ['onError', 'onHmr'];
    invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`);
    /* 從鉤子中拿出掛載的回調(diào)函數(shù) 宵蕉,掛載動作見 use 部分*/
    const fns = hooks[key];

    return (...args) => {
        // 如果有回調(diào)執(zhí)行回調(diào)
      if (fns.length) {
        for (const fn of fns) {
          fn(...args);
        }
        // 沒有回調(diào)直接拋出錯誤
      } else if (defaultHandler) {
        defaultHandler(...args);

        /*
        這里 defaultHandler 為 (err) => {
          throw new Error(err.stack || err);
        }
        */
      }
    };
  }

sagaMiddleware

下一行代碼是:

const sagaMiddleware = createSagaMiddleware();

redux-sagas 的入門教程有點差異,因為正統(tǒng)的教程上添加 sagas 中間件的方法是: createSagaMiddleware(...sagas)

sagas 為含有 saga 方法的 generator 函數(shù)數(shù)組节榜。

但是 api 里確實還提到羡玛,還有一~~招從天而降的掌法~~種動態(tài)調(diào)用的方式:

const task = sagaMiddleware.run(dynamicSaga)

于是:

      const sagaMiddleware = createSagaMiddleware();
      // ...
      const sagas = [];
      const reducers = {...initialReducer
      };
      for (const m of app._models) {
        reducers[m.namespace] = getReducer(m.reducers, m.state);
        if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
      }
      // ....

      store.runSaga = sagaMiddleware.run;
      // Run sagas
      sagas.forEach(sagaMiddleware.run);

sagas

那么 sagas 是什么呢?

    const {
      middleware: promiseMiddleware,
      resolve,
      reject,
    } = createPromiseMiddleware(app);
    app._getSaga = getSaga.bind(null, resolve, reject);

    const sagas = [];

    for (const m of app._models) {
      if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
    }

顯然宗苍,sagas 是一個數(shù)組稼稿,里面的元素是用 app._getSaga 處理后的返回結(jié)果薄榛,而 app._getSaga 又和上面 createPromiseMiddleware 代理 app 后返回的對象有很大關(guān)系。

createPromiseMiddleware

createPromiseMiddleware 的代碼在此让歼。

如果看著覺得眼熟敞恋,那肯定不是因為看過 redux-promise 源碼的緣故,:-p谋右。

middleware

middleware 是一個 redux 的中間件硬猫,即在不影響 redux 本身功能的情況下為其添加了新特性的代碼。redux 的中間件通過攔截 action 來實現(xiàn)其作用的改执。

  const middleware = () => next => (action) => {
    const { type } = action;
    if (isEffect(type)) {
      return new Promise((resolve, reject) => {
        // .... resolve ,reject
      });
    } else {
      return next(action);
    }
  };

    function isEffect(type) {
        // dva 里 action 的 type 有固定格式: model.namespace/model.effects
        // const [namespace] = type.split(NAMESPACE_SEP); 是 es6 解構(gòu)的寫法
        // 等同于 const namespace = type.split(NAMESPACE_SEP)[0];
        // NAMESPACE_SEP 的值是 `/`
        const [namespace] = type.split(NAMESPACE_SEP);
        // 根據(jù) namespace 過濾出對應(yīng)的 model
        const model = app._models.filter(m => m.namespace === namespace)[0];
        // 如果 model 存在并且 model.effects[type] 也存在恍箭,那必然是 effects
        if (model) {
            if (model.effects && model.effects[type]) {
                return true;
            }
        }

        return false;
    }

const middleware = ({dispatch}) => next => (action) => {... return next(action)} 基本上是一個標準的中間件寫法唱较。在 return next(action) 之前可以對 action 做各種各樣的操作雳刺。因為此中間件沒用到 dispatch 方法仲锄,所以省略了霞丧。

本段代碼的意思是呢岗,如果 dispatch 的 action 指向的是 model 里的 effects,那么返回一個 Promise 對象蛹尝。此 Promise 的對象的解決( resolve )或者駁回方法 ( reject ) 放在 map 對象中后豫。如果是非 effects (那就是 action 了),放行突那。

換句話說挫酿,middleware 攔截了指向 effects 的 action。

神奇的 bind

bind 的作用是綁定新的對象愕难,生成新函數(shù)是大家都知道概念早龟。但是 bind 也可以提前設(shè)定好函數(shù)的某些參數(shù)生成新函數(shù),等到最后一個參數(shù)確定時直接調(diào)用猫缭。

JavaScript 的參數(shù)是怎么被調(diào)用的葱弟?JavaScript 專題之函數(shù)柯里化。作者:冴羽猜丹。文章來源:掘金

這段代碼恰好就是 bind 的一種實踐方式芝加。

  const map = {};

  const middleware = () => next => (action) => {
    const { type } = action;
    // ...
      return new Promise((resolve, reject) => {
        map[type] = {
          resolve: wrapped.bind(null, type, resolve),
          reject: wrapped.bind(null, type, reject),
        };
      });
    // ....
  };

  function wrapped(type, fn, args) {
    if (map[type]) delete map[type];
    fn(args);
  }

  function resolve(type, args) {
    if (map[type]) {
      map[type].resolve(args);
    }
  }

  function reject(type, args) {
    if (map[type]) {
      map[type].reject(args);
    }
  }

   return {
    middleware,
    resolve,
    reject,
  };

分析這段代碼,dva 是這樣做的:

  1. 通過 wrapped.bind(null, type, resolve) 產(chǎn)生了一個新函數(shù)射窒,并且賦值給匿名對象的 resolve 屬性(reject 同理)藏杖。

1.1 wrap 接收三個參數(shù),通過 bind 已經(jīng)設(shè)定好了兩個脉顿。wrapped.bind(null, type, resolve) 等同于 wrap(type, resolve, xxx)此處 resolve 是 Promise 對象中的)蝌麸。

1.2 通過 bind 賦給匿名對象的 resolve 屬性后,匿名對象.resolve(xxxx) 等同于 wrap(type, resolve, xxx)艾疟,即 reslove(xxx)祥楣。

  1. 使用 type 在 map 對象中保存此匿名對象开财,而 type 是 action 的 type,即 namespace/effects 的形式误褪,方便之后進行調(diào)用责鳍。

  2. return 出的 resolve 接收 type 和 args 兩個參數(shù)。type 用來在 map 中尋找 1 里的匿名函數(shù)兽间,args 用來像 1.2 里那樣執(zhí)行历葛。

這樣做的作用是:分離了 promise 與 promise 的執(zhí)行。在函數(shù)的作用域外依然可以訪問到函數(shù)的內(nèi)部變量嘀略,換言之:閉包恤溶。

getSaga

導(dǎo)出的 resolvereject 方法,通過 bind 先設(shè)置進了 getSaga (同時也賦給了 app._getSaga)帜羊,sagas 最終也將 getSaga 的返回值放入了數(shù)組咒程。

getSaga 源碼

export default function getSaga(resolve, reject, effects, model, onError, onEffect) {
  return function *() {
    for (const key in effects) {
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        const watcher = getWatcher(resolve, reject, key, effects[key], model, onError, onEffect);
        // 將 watcher 分離到另一個線程去執(zhí)行
        const task = yield sagaEffects.fork(watcher);
        // 同時 fork 了一個線程,用于在 model 卸載后取消正在進行中的 task
        // `${model.namespace}/@@CANCEL_EFFECTS` 的發(fā)出動作在 index.js 的 start 方法中讼育,unmodel 方法里帐姻。
        yield sagaEffects.fork(function *() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}

可以看到,getSaga 最終返回了一個 generator 函數(shù)奶段。

在該函數(shù)遍歷了 model 中 effects 屬性 的所有方法(注:同樣是 generator 函數(shù))饥瓷。結(jié)合 index.js 里的 for (const m of app._models),該遍歷針對所有的 model痹籍。

對于每一個 effect呢铆,getSaga 生成了一個 watcher ,并使用 saga 函數(shù)的 fork 將該函數(shù)切分到另一個單獨的線程中去(生成了一個 task 對象)蹲缠。同時為了方便對該線程進行控制棺克,在此 fork 了一個 generator 函數(shù)。在該函數(shù)中攔截了取消 effect 的 action(事實上线定,應(yīng)該是卸載effect 所在 model 的 action)逆航,一旦監(jiān)聽到則立刻取消分出去的 task 線程。

getWatcher
function getWatcher(resolve, reject, key, _effect, model, onError, onEffect) {
  let effect = _effect;
  let type = 'takeEvery';
  let ms;

  if (Array.isArray(_effect)) {
    // effect 是數(shù)組而不是函數(shù)的情況下暫不考慮
  }

  function *sagaWithCatch(...args) {
        // .... sagaWithCatch 的邏輯
  }

  const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);

  switch (type) {
    case 'watcher':
      return sagaWithCatch;
    case 'takeLatest':
      return function*() {
        yield takeLatest(key, sagaWithOnEffect);
      };
    case 'throttle':
      return function*() {
        yield throttle(ms, key, sagaWithOnEffect);
      };
    default:
      return function*() {
        yield takeEvery(key, sagaWithOnEffect);
      };
  }
}

function createEffects(model) {
    // createEffects(model) 的邏輯
}

function applyOnEffect(fns, effect, model, key) {
  for (const fn of fns) {
    effect = fn(effect, sagaEffects, model, key);
  }
  return effect;
}

先不考慮 effect 的屬性是數(shù)組而不是方法的情況渔肩。

getWatcher 接收六個參數(shù):

  • resolve/reject: 中間件 middleware 的 res 和 rej 方法因俐。
  • key:經(jīng)過 prefixNamespace 轉(zhuǎn)義后的 effect 方法名,namespace/effect(也是調(diào)用 action 時的 type)周偎。 -_effect:effects 中 key 屬性所指向的 generator 函數(shù)抹剩。
  • model: model
  • onError: 之前定義過的捕獲全局錯誤的方法
  • onEffect:plugin.use 中傳入的在觸發(fā) effect 時執(zhí)行的回調(diào)函數(shù)(鉤子函數(shù))

applyOnEffect 對 effect 進行了動態(tài)代理,在保證 effect (即 _effect)正常調(diào)用的情況下蓉坎,為期添加了 fns 的回調(diào)函數(shù)數(shù)組(即 onEffect)澳眷。使得在 effect 執(zhí)行時, onEffect 內(nèi)的每一個回調(diào)函數(shù)都可以被觸發(fā)蛉艾。

因為沒有經(jīng)過 effects 的屬性是數(shù)組的情況钳踊,所以 type 的值是 takeEvery衷敌,也就是監(jiān)聽每一個發(fā)出的 action ,即 getWatcher 的返回值最終走的是 switch 的 default 選項:

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

換句話說拓瞪,每次發(fā)出指向 effects 的函數(shù)都會調(diào)用 sagaWithOnEffect缴罗。

根據(jù) const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key); 的執(zhí)行情況,如果 onEffect 的插件為空的情況下祭埂,sagaWithOnEffect 的值為 sagaWithCatch面氓。

  function *sagaWithCatch(...args) {
    try {
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
      const ret = yield effect(...args.concat(createEffects(model)));
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
      resolve(key, ret);
    } catch (e) {
      onError(e);
      if (!e._dontReject) {
        reject(key, e);
      }
    }
  }

sagaWithOnEffect 函數(shù)中,sagas 使用傳入的參數(shù)(也就是 action)執(zhí)行了對應(yīng)的 model 中 對應(yīng)的 effect 方法蛆橡,同時將返回值使用之前保存在 map 里的 resolve 返回了其返回值舌界。同時在執(zhí)行 effect 方法的時候,將 saga 本身的所有方法(put泰演、call呻拌、fork 等等)作為第二個參數(shù),使用 concat 拼接在 action 的后面睦焕。在執(zhí)行 effect 方法前藐握,又發(fā)出了 start 和 end 兩個 action,方便 onEffect 的插件進行攔截和調(diào)用复亏。

因此趾娃,對于 if (m.effects) sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));缭嫡。

  1. dva 通過 app._getSaga(m.effects, m, onError, plugin.get('onEffect')) 返回了一個 genenrator 函數(shù)缔御。
  2. 在 genenrator 函數(shù)中手動 fork 出一個 watcher 函數(shù)的監(jiān)聽線程(當然也 fork 了取消線程的功能)。
  3. 該函數(shù)(在普通狀態(tài)下)是一個 takeEvery 的阻塞是線程妇蛀,接收 2 個參數(shù)耕突。第一個參數(shù)為監(jiān)聽的 action,第二個參數(shù)為監(jiān)聽到 action 后的回調(diào)函數(shù)评架。
  4. (普通狀態(tài)下)的回調(diào)函數(shù)眷茁,就是手動調(diào)用了 model 里 effects 中對應(yīng)屬性的函數(shù)。在此之前之后發(fā)出了 startend 的 action纵诞,同時用之前 promise 中間件保存在 map 中的 resolve 方法返回了值上祈。
  5. 最后使用 sagas.forEach(sagaMiddleware.run) 啟動了 watcher 的監(jiān)聽。

store

現(xiàn)在已經(jīng)有了針對異步數(shù)據(jù)流的解決辦法浙芙,那么該創(chuàng)建 store 了登刺。

正常情況的 redux 的 createStore 接收三個參數(shù) reducer, initState,applyMiddleware(middlewares)。

不過 dva 提供了自己的 createStore 方法嗡呼,用來組織一系列自己創(chuàng)建的參數(shù)纸俭。

    // Create store
    const store = app._store = createStore({ // eslint-disable-line
      reducers: createReducer(),
      initialState: hooksAndOpts.initialState || {},
      plugin,
      createOpts,
      sagaMiddleware,
      promiseMiddleware,
    });

createReducer

    function createReducer() {
      return reducerEnhancer(combineReducers({
        ...reducers,
        ...extraReducers,
        ...(app._store ? app._store.asyncReducers : {}),
      }));
    }

createReducer 實際上是用 plugin 里的 onReducer (如果有)擴展了 reducer 功能,對于 const reducerEnhancer = plugin.get('onReducer');南窗,plugin 里的相關(guān)代碼為:

function getOnReducer(hook) {
  return function (reducer) {
    for (const reducerEnhancer of hook) {
      reducer = reducerEnhancer(reducer);
    }
    return reducer;
  };
}

如果有 onReducer 的插件揍很,那么用 reducer 的插件擴展 reducer郎楼;否則直接返回 reducer。

combineReducers 中:

  • 第一個 ...reducers 是從 dva 里傳入的 historyReducer窒悔,以及通過 reducers[m.namespace] = getReducer(m.reducers, m.state); 剝離出的 model 中的 reducer
  • 第二個參數(shù)為手動在 plugin 里添加的 extraReducers呜袁;
  • 第三個參數(shù)為異步 reducer,主要是用于在 dva 運行以后動態(tài)加載 model 里的 reducer蛉迹。

createStore

現(xiàn)在我們有了一個 combine 過的 reducer傅寡,有了 core 中創(chuàng)建的 sagaMiddleware 和 promiseMiddleware,還有了從 dva 中傳入的 createOpts北救,現(xiàn)在可以正式創(chuàng)建 store 了荐操。

從 dva 中傳入的 createOpts 為

    setupMiddlewares(middlewares) {
      return [
        routerMiddleware(history),
        ...middlewares,
      ];
    },

用與把 redux-router 的中間件排在中間件的第一個。

雖然看起來很長珍策,但是對于大多數(shù)普通用戶來說托启,在未開啟 redux 的調(diào)試插件,未傳入額外的 onAction 以及 extraEnhancers 的情況下攘宙,上面的代碼等價于:

import { createStore, applyMiddleware, compose } from 'redux';
import flatten from 'flatten';
import invariant from 'invariant';
import window from 'global/window';
import { returnSelf, isArray } from './utils';

export default function ({
  reducers,
  initialState,
  plugin,
  sagaMiddleware,
  promiseMiddleware,
  createOpts: {
    setupMiddlewares = returnSelf,
  },
}) {

  const middlewares = setupMiddlewares([
    sagaMiddleware,
    promiseMiddleware
  ]);

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

  return createStore(reducers, initialState, compose(...enhancers));
  // 對于 redux 中 的 compose 函數(shù)屯耸,在數(shù)組長度為 1  的情況下返回第一個元素。
  // compose(...enhancers) 等同于 applyMiddleware(...middlewares)
}

訂閱

現(xiàn)在 dva 已經(jīng)創(chuàng)建了 store蹭劈,有了異步數(shù)據(jù)流加載方案疗绣,并且又做了一些其他的事情:

    // Extend store
    store.runSaga = sagaMiddleware.run;
    store.asyncReducers = {};

    // Execute listeners when state is changed
    const listeners = plugin.get('onStateChange');
    for (const listener of listeners) {
      store.subscribe(() => {
        listener(store.getState());
      });
    }

    // Run sagas
    sagas.forEach(sagaMiddleware.run);

  • 手動運行 getSaga 里返回的 watcer 函數(shù)。
  • 判斷如果有 onStateChange 的 plugin 也手動運行一下铺韧。

model 里的 state多矮、effect、reducer 已經(jīng)實現(xiàn)了哈打,就缺最后的訂閱 subscription 部分塔逃。

    // Setup app
    setupApp(app);

    // Run subscriptions
    const unlisteners = {};
    for (const model of this._models) {
      if (model.subscriptions) {
        unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
      }
    }

setupApp(app) 是從 dva 里傳過來的,主要是使用 patchHistory 函數(shù)代理 history.linsten料仗,即強化了 redux 和 router 的聯(lián)系湾盗,是的路徑變化可以引起 state 的變化,進而聽過監(jiān)聽 state 的變化來觸發(fā)回調(diào)立轧。

這也是 core 中唯一使用 this 的地方格粪,逼得 dva 中必須使用 oldStart.call(app) 來進行調(diào)用。

runSubscription

這是 runSubscription 的代碼

export 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 };
}

  • 第一個參數(shù)為 model 中的 subscription 對象氛改。
  • 第二個參數(shù)為對應(yīng)的 model
  • 第三個參數(shù)為 core 里創(chuàng)建的 app
  • 第四個參數(shù)為全局異常捕獲的 onError
  1. Object.prototype.hasOwnProperty.call(subs, key) 還是使用原型方法判斷 key 是不是 subs 的自有屬性帐萎。

  2. 如果是自由屬性,那么拿到屬性對應(yīng)的值(是一個 function)

  3. 調(diào)用該 function平窘,傳入 dispatch 和 history 屬性吓肋。history 就是經(jīng)過 redux-router 強化過的 history,而 dispatch瑰艘,也就是 prefixedDispatch(app._store.dispatch, model)

export default function prefixedDispatch(dispatch, model) {
  return (action) => {
    // 斷言檢測
    return dispatch({ ...action, type: prefixType(type, model) });
  };
}

實際上是用將 action 里的 type 添加了 ${model.namespance}/ 的前綴是鬼。

自此肤舞,model 中的四大組件全部完畢,完成了 dva 的數(shù)據(jù)層處理均蜜。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末李剖,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子囤耳,更是在濱河造成了極大的恐慌篙顺,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件充择,死亡現(xiàn)場離奇詭異德玫,居然都是意外死亡,警方通過查閱死者的電腦和手機椎麦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門宰僧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人观挎,你說我怎么就攤上這事琴儿。” “怎么了嘁捷?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵造成,是天一觀的道長。 經(jīng)常有香客問我雄嚣,道長晒屎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任现诀,我火速辦了婚禮夷磕,結(jié)果婚禮上履肃,老公的妹妹穿的比我還像新娘仔沿。我一直安慰自己,他們只是感情好尺棋,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布封锉。 她就那樣靜靜地躺著,像睡著了一般膘螟。 火紅的嫁衣襯著肌膚如雪成福。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天荆残,我揣著相機與錄音奴艾,去河邊找鬼。 笑死内斯,一個胖子當著我的面吹牛蕴潦,可吹牛的內(nèi)容都是我干的像啼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼潭苞,長吁一口氣:“原來是場噩夢啊……” “哼忽冻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起此疹,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤僧诚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蝗碎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湖笨,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年蹦骑,在試婚紗的時候發(fā)現(xiàn)自己被綠了赶么。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡脊串,死狀恐怖辫呻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情琼锋,我是刑警寧澤放闺,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站缕坎,受9級特大地震影響怖侦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谜叹,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一匾寝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧荷腊,春花似錦艳悔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疾忍,卻和暖如春乔外,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背一罩。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工杨幼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓差购,卻偏偏與公主長得像补疑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子歹撒,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345