qiankun源碼深挖

使用qiankun也有一段時(shí)間了球匕,但是在使用的過(guò)程中纹磺,總會(huì)遇到各種問(wèn)題,面對(duì)這些問(wèn)題最終雖然解決了亮曹,但是總感覺(jué)心里面不踏實(shí)橄杨,想要去看看qiankun的源碼,加深一些理解乾忱,學(xué)習(xí)一下qiankun內(nèi)部的實(shí)現(xiàn)原理讥珍。斷斷續(xù)續(xù)讀了好幾周,主要的目的是為了弄清楚qiankun窄瘟、主應(yīng)用衷佃、子應(yīng)用、single-spa這四個(gè)部分的生命周期的執(zhí)行順序以及觸發(fā)的條件蹄葱。

生命周期

這里給出qiankun氏义、主應(yīng)用锄列、子應(yīng)用、single-spa的生命周期的總圖惯悠。

qiankun總的生命周期.jpg

主應(yīng)用和子應(yīng)用的生命周期

在主應(yīng)用中注冊(cè)子應(yīng)用的時(shí)候邻邮,可以傳入五個(gè)生命周期函數(shù),如下

  • beforeLoad - Lifecycle | Array<Lifecycle> - 可選
  • beforeMount - Lifecycle | Array<Lifecycle> - 可選
  • afterMount - Lifecycle | Array<Lifecycle> - 可選
  • beforeUnmount - Lifecycle | Array<Lifecycle> - 可選
  • afterUnmount - Lifecycle | Array<Lifecycle> - 可選
registerMicroApps(apps, {
    beforeLoad: app => console.log('before load', app.name),
    beforeMount: [
      app => console.log('before mount', app.name),
    ],
    afterMount: app => console.log('after mount', app.name),
    beforeUnmount: app => console.log('before unmount', app.name),
    afterUnmount: app => console.log('after unmount', app.name)
});

依照子應(yīng)用的接入規(guī)范克婶,子應(yīng)用必須暴露三個(gè)生命周期筒严,如下

export async function bootstrap() {
  console.log('app bootstraped');
}
export async function mount(props) {
  console.log('app mount', props);
  render(props);
}
export async function unmount() {
  console.log('app unmount');
  instance.$destroy();
  instance = null;
  router = null;
}

qiankun的生命周期

qiankun主要是利用beforeLoadbeforeMount情萤、beforeUnmount這三個(gè)生命周期添加和刪除全局變量鸭蛙。

//qiankun-master\src\addons\engineFlag.ts
export default function getAddOn(global: Window): FrameworkLifeCycles<any> {
  return {
    async beforeLoad() {
      // eslint-disable-next-line no-param-reassign
      global.__POWERED_BY_QIANKUN__ = true;
    },

    async beforeMount() {
      // eslint-disable-next-line no-param-reassign
      global.__POWERED_BY_QIANKUN__ = true;
    },

    async beforeUnmount() {
      // eslint-disable-next-line no-param-reassign
      delete global.__POWERED_BY_QIANKUN__;
    },
  };
}
//qiankun-master\src\addons\runtimePublicPath.ts
export default function getAddOn(global: Window, publicPath = '/'): FrameworkLifeCycles<any> {
  let hasMountedOnce = false;

  return {
    async beforeLoad() {
      // eslint-disable-next-line no-param-reassign
      global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = publicPath;
    },

    async beforeMount() {
      if (hasMountedOnce) {
        // eslint-disable-next-line no-param-reassign
        global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = publicPath;
      }
    },

    async beforeUnmount() {
      if (rawPublicPath === undefined) {
        // eslint-disable-next-line no-param-reassign
        delete global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
      } else {
        // eslint-disable-next-line no-param-reassign
        global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = rawPublicPath;
      }

      hasMountedOnce = true;
    },
  };
}

以上是qiankun、主應(yīng)用筋岛、子應(yīng)用分別可以使用的生命周期函數(shù)娶视,但是這里還無(wú)法知道這些生命周期函數(shù)是何時(shí)何地執(zhí)行,有誰(shuí)進(jìn)行調(diào)用睁宰,這是下面章節(jié)的主要內(nèi)容肪获,核心就是single-spa

生命周期的執(zhí)行機(jī)制

首先需要明確的是qiankun實(shí)際上是對(duì)single-spa的進(jìn)一步封裝柒傻,使其更加容易開(kāi)箱即用孝赫。

qiankun

我們關(guān)注qiankun暴露的registerMicroApps方法,

//qiankun-master\src\apis.ts
export function registerMicroApps<T extends object = {}>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles?: FrameworkLifeCycles<T>,
) {
  //過(guò)濾出還沒(méi)有注冊(cè)的應(yīng)用
  const unregisteredApps = apps.filter(app => !microApps.some(registeredApp =>                                           registeredApp.name === app.name));
  microApps = [...microApps, ...unregisteredApps];
  //對(duì)于每一個(gè)沒(méi)有注冊(cè)的應(yīng)用調(diào)用qiankun的API進(jìn)行注冊(cè)
  unregisteredApps.forEach(app => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app;
    registerApplication({
      name,
      app: async () => {
        loader(true);
        //這就是為了控制整個(gè)流程诅愚,可以看到frameworkStartedDefer.promise在start方法的最后才會(huì)              resolve寒锚,所以在start調(diào)用后,這邊才會(huì)繼續(xù)執(zhí)行
        await frameworkStartedDefer.promise;
        //這里需要關(guān)注的是loadApp這個(gè)方法违孝,該方法主要就是完成qiankun與主應(yīng)用和子應(yīng)用的生命周期的整合刹前,           最終返回整理后的生命周期數(shù)組,并且是按照?qǐng)?zhí)行順序排列的
        const { mount, ...otherMicroAppConfigs } = await loadApp(
          { name, props, ...appConfig },
          frameworkConfiguration, //這個(gè)參數(shù)在start函數(shù)中被賦值
          lifeCycles,
        );
        return {
          mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

由于loadApp這個(gè)方法代碼過(guò)長(zhǎng)雌桑,這邊就不完全貼出來(lái)喇喉,主要的執(zhí)行過(guò)程用一個(gè)流程圖給出來(lái),

localapp執(zhí)行過(guò)程.jpg

詳細(xì)的代碼大家可以自已前去閱讀校坑,這里簡(jiǎn)單給出函數(shù)的返回值拣技,其中有部分代碼省略,只關(guān)注主要的執(zhí)行代碼耍目,

//qiankun-master\src\loader.ts
const parcelConfig: ParcelConfigObject = {
    name: appInstanceId,
    //子應(yīng)用暴露的生命周期
    bootstrap,
    //這里是一個(gè)數(shù)組膏斤,里面有qiankun執(zhí)行的一些邏輯
    mount: [
      //該函數(shù)是判斷是否為單例模式,如果是單例模式就必須等待前面的應(yīng)用先完成卸載操作邪驮。由于整個(gè)mount數(shù)組后面再執(zhí)行的時(shí)候都是阻塞式的莫辨,所以這里如果返回的是一個(gè)promise,那么后續(xù)的函數(shù)只有等待該函數(shù)resolve之后才能繼續(xù)執(zhí)行。
      async () => {
        if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
          return prevAppUnmountedDeferred.promise;
        }
        return undefined;
      },
      // 該函數(shù)確保每次應(yīng)用加載前容器 dom 結(jié)構(gòu)已經(jīng)設(shè)置完畢
      async () => {
        element = element || createElement(appContent, strictStyleIsolation);
        render({ element, loading: true }, 'mounting');
      },
      //該函數(shù)是執(zhí)行beforeMount生命周期數(shù)組
      async () => execHooksChain(toArray(beforeMount), app, global),
      //該函數(shù)是掛載沙盒
      mountSandbox,
      //該函數(shù)是執(zhí)行子應(yīng)用暴露的mount生命周期函數(shù)
      async props => mount({ ...props, container: containerGetter(), setGlobalState,                                   onGlobalStateChange }),
      //該函數(shù)是執(zhí)行afterMount生命周期數(shù)組
      async () => execHooksChain(toArray(afterMount), app, global),
      //該函數(shù)是檢測(cè)若是單例模式沮榜,那么就需要給prevAppUnmountedDeferred賦予一個(gè)promise盘榨,該promise會(huì)在unmount的時(shí)候resolve,并且在一些需要判斷單例模式下的一些執(zhí)行時(shí)需要使用該變量
      async () => {
        if (await validateSingularMode(singular, app)) {
          prevAppUnmountedDeferred = new Deferred<void>();
        }
      }
    ],
    unmount: [
      //該函數(shù)是執(zhí)行beforeUnmount生命周期數(shù)組
      async () => execHooksChain(toArray(beforeUnmount), app, global),
      //該函數(shù)是執(zhí)行子應(yīng)用暴露的unmount生命周期函數(shù)
      async props => unmount生命周期函數(shù)({ ...props, container: containerGetter() }),
      //該函數(shù)是卸載沙盒
      unmountSandbox,
      //該函數(shù)是執(zhí)行afterUnmount生命周期數(shù)組
      async () => execHooksChain(toArray(afterUnmount), app, global),
      //該函數(shù)是卸載子應(yīng)用的dom并且去除全局狀態(tài)監(jiān)聽(tīng)
      async () => {
        render({ element: null, loading: false }, 'unmounted');
        offGlobalStateChange(appInstanceId);
        // for gc蟆融,為了垃圾回收草巡,引用計(jì)數(shù)為零可以回收
        element = null;
      },
      //該函數(shù)是將prevAppUnmountedDeferred進(jìn)行resolve,防止單例模式下影響其他流程的正常運(yùn)轉(zhuǎn)
      async () => {
        if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
          prevAppUnmountedDeferred.resolve();
        }
      },
    ],
  };

single-spa

qiankun在調(diào)用single-sparegisterApplication方法后型酥,就是將自己以及主應(yīng)用山憨、子應(yīng)用的相關(guān)生命周期托管給了single-spa來(lái)進(jìn)行觸發(fā)。

我們看single-sparegisterApplication方法冕末,

single-spa的registerApplication.jpg
//single-spa-master\src\applications\apps.js
export function registerApplication(
  appNameOrConfig,
  appOrLoadApp,
  activeWhen,
  customProps
) {
  //這里做一些參數(shù)化校驗(yàn)以及規(guī)整
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );
  //檢查該app是否已經(jīng)注冊(cè)過(guò)了
  if (getAppNames().indexOf(registration.name) !== -1)
    throw Error(
      formatErrorMessage(
        21,
        __DEV__ &&
          `There is already an app registered with name ${registration.name}`,
        registration.name
      )
    );
  //這里的apps是single-spa維護(hù)的變量萍歉,用來(lái)存儲(chǔ)所有的應(yīng)用
  apps.push(
    assign(
      {
        loadErrorTime: null,
        status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );
  if (isInBrowser) {
    ensureJQuerySupport();
    //這個(gè)方法是single-spa的核心方法,類(lèi)似一個(gè)狀態(tài)機(jī)档桃,完成應(yīng)用不同狀態(tài)的流轉(zhuǎn)
    reroute();
  }
}

到這里,基本上所有應(yīng)用都已全部托管給single-spa憔晒,而single-spa后續(xù)對(duì)于每一個(gè)應(yīng)用的處理藻肄,狀態(tài)的變化,事件的觸發(fā)都基本通過(guò)reroute這個(gè)方法完成拒担,所以這個(gè)方法也是我們關(guān)注的一個(gè)重點(diǎn)嘹屯。

single-spa的核心方法reroute()

首先需要從整體上看,reroute()這個(gè)方法基本是有三個(gè)地方會(huì)調(diào)用从撼,

reroute的調(diào)用點(diǎn).jpg

這三個(gè)調(diào)用點(diǎn)可以大致分為顯示調(diào)用(微應(yīng)用注冊(cè)州弟、start),隱式調(diào)用(路有變化)低零。有路由的變化是隨時(shí)可能發(fā)生婆翔,更多的隨機(jī)性,更多的情況需要考慮掏婶,所以先梳理當(dāng)路由變化時(shí)啃奴,reroute方法是如何被觸發(fā)的。

路由變化觸發(fā)reroute

這里的代碼邏輯比較細(xì)碎雄妥,并且比較長(zhǎng)最蕾,有興趣的可以去下載源碼找到代碼位置為,

single-spa-master\src\navigation\navigation-events.js自行閱讀一下老厌。

我這邊閱讀之后畫(huà)了一個(gè)基礎(chǔ)的流程圖瘟则,大概的說(shuō)明single-spa對(duì)于路由變化的處理方式,

路由變化時(shí)觸發(fā)reroute.jpg

這里面主要需要注意的就是single-spa是通過(guò)監(jiān)聽(tīng)hashchange 和popstate 兩個(gè)事件來(lái)判斷路由發(fā)生變化枝秤,但是由于window.history.pushState 和window.history.replaceState 并沒(méi)有觸發(fā)popstate 事件醋拧,所以碎玉這兩個(gè)函數(shù)需要加強(qiáng),

//single-spa-master\src\navigation\navigation-events.js
window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
);
window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
);

在patchedUpdateState 方法中完成兩個(gè)任務(wù)

(1)正確完成URl的變化

(2)調(diào)用reroute方法,并且new一個(gè)popstate 事件傳遞過(guò)去趁仙,這么做的主要目的是能夠正確的執(zhí)行用戶的事件監(jiān)聽(tīng)洪添。

這里single-spa之所以能夠拿到用戶的監(jiān)聽(tīng)回調(diào),是因?yàn)槠渲貙?xiě)了window.addEventListener 雀费,并在其中判斷是否是監(jiān)聽(tīng)的hashchange 和popstate 這兩個(gè)事件干奢,如果是,則會(huì)緩沖到一個(gè)回調(diào)函數(shù)的數(shù)組之中盏袄。

  //single-spa-master\src\navigation\navigation-events.js
  const originalAddEventListener = window.addEventListener;
  const originalRemoveEventListener = window.removeEventListener;
  window.addEventListener = function (eventName, fn) {
    // 如果用戶監(jiān)聽(tīng)的是 hashchange 和 popstate 事件忿峻,并且這個(gè)監(jiān)聽(tīng)器此前未加入事件監(jiān)聽(tīng)列表
    // 那這個(gè)事件是有可能引發(fā)應(yīng)用變更的,需要加入 capturedEventListeners 中
    // 直接 return 掉辕羽,說(shuō)明 hashchange 和 popstate 事件并沒(méi)有馬上執(zhí)行
    // 而是在執(zhí)行完 reroute 邏輯之后在執(zhí)行
    if (typeof fn === "function") {
      if (
        routingEventsListeningTo.indexOf(eventName) >= 0 &&
        !find(capturedEventListeners[eventName], (listener) => listener === fn)
      ) {
        capturedEventListeners[eventName].push(fn);
        return;
      }
    }
    return originalAddEventListener.apply(this, arguments);
  };

弄清楚路由變化的時(shí)候逛尚,single-spa的主要操作之后,下面就是要摸清楚reroute函數(shù)具體是做了哪些操作刁愿。

執(zhí)行reroute(pendingPromises = [], eventArguments)

在這個(gè)方法中最主干的邏輯是绰寞,

//single-spa-master\src\navigation\reroute.js
export function reroute(pendingPromises = [], eventArguments) {
  //如果正在執(zhí)行上一個(gè)路由變化的操作,則將該事件緩存到peopleWaitingOnAppChange數(shù)組中铣口,該事件大概率是用     戶的監(jiān)聽(tīng)事件滤钱,被single-spa劫持了。
  if (appChangeUnderway) {
    return new Promise((resolve, reject) => {
      peopleWaitingOnAppChange.push({
        resolve,
        reject,
        eventArguments,
      });
    });
  }
  //依據(jù)當(dāng)前變化后的URl判斷目前所有應(yīng)用的狀態(tài)即將發(fā)生的變化
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
    
  let appsThatChanged;
  //判斷是否已經(jīng)執(zhí)行start函數(shù)脑题,如果是件缸,則進(jìn)一步執(zhí)行每一個(gè)app的生命周期,如果沒(méi)有叔遂,則只load應(yīng)用他炊。
  if (isStarted()) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }
}

依據(jù)上面的主要執(zhí)行過(guò)程,畫(huà)了一個(gè)基本的流程圖已艰,大概說(shuō)明了reroute方法對(duì)于應(yīng)用狀態(tài)的變化以及single-spa提供的各個(gè)公共事件的派發(fā)機(jī)制痊末。

reroute的執(zhí)行過(guò)程.jpg

single-spa在執(zhí)行過(guò)程中派發(fā)的幾個(gè)全局事件,應(yīng)用可以進(jìn)行監(jiān)聽(tīng)旗芬,這其實(shí)也算是single-spa的一個(gè)生命周期舌胶,主要的執(zhí)行過(guò)程為,

single-spa派發(fā)的事件.jpg

每一個(gè)應(yīng)用自身的狀態(tài)都會(huì)隨著URl的不斷改變而變化疮丛,基本由下面的幾個(gè)階段組成幔嫂,

應(yīng)用狀態(tài)的幾個(gè)階段.jpg

小結(jié)

源碼還有很多的細(xì)節(jié)可以去看,我這邊只是大致梳理了一下qiankun生命周期的執(zhí)行過(guò)程誊薄,基本就是將生命周期的邏輯封裝好履恩,托管給single-spa去執(zhí)行。

reroute 流程作為 single-spa 的核心流程呢蔫,充當(dāng)了一個(gè)應(yīng)用狀態(tài)機(jī)的角色切心,控制了應(yīng)用的生命周期的流轉(zhuǎn)和事件分發(fā)飒筑。qiankun 就是利用了這一特性,將應(yīng)用交給 single-spa 管理绽昏,自己實(shí)現(xiàn)應(yīng)用的加載方法(loadApp)和生命周期

參考

single-spa 的生命周期和事件管理

萬(wàn)字長(zhǎng)文+圖文并茂+全面解析微前端框架 qiankun 源碼 - qiankun 篇

single-spa官網(wǎng)

qiankun官網(wǎng)

還有一些就不全羅列出來(lái)了协屡。。眼睛酸全谤,多休息肤晓。歡迎大家多多吐槽。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末认然,一起剝皮案震驚了整個(gè)濱河市补憾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卷员,老刑警劉巖盈匾,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異毕骡,居然都是意外死亡削饵,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)未巫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)葵孤,“玉大人,你說(shuō)我怎么就攤上這事橱赠。” “怎么了箫津?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵狭姨,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我苏遥,道長(zhǎng)饼拍,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任田炭,我火速辦了婚禮师抄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘教硫。我一直安慰自己叨吮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布瞬矩。 她就那樣靜靜地躺著茶鉴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪景用。 梳的紋絲不亂的頭發(fā)上涵叮,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼割粮。 笑死盾碗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的舀瓢。 我是一名探鬼主播廷雅,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼氢伟!你這毒婦竟也來(lái)了榜轿?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤朵锣,失蹤者是張志新(化名)和其女友劉穎谬盐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體诚些,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡飞傀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了诬烹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片砸烦。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖绞吁,靈堂內(nèi)的尸體忽然破棺而出幢痘,到底是詐尸還是另有隱情,我是刑警寧澤家破,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布颜说,位于F島的核電站,受9級(jí)特大地震影響汰聋,放射性物質(zhì)發(fā)生泄漏门粪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一烹困、第九天 我趴在偏房一處隱蔽的房頂上張望玄妈。 院中可真熱鬧,春花似錦髓梅、人聲如沸拟蜻。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瞭郑。三九已至,卻和暖如春鸭你,著一層夾襖步出監(jiān)牢的瞬間屈张,已是汗流浹背擒权。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阁谆,地道東北人碳抄。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像场绿,于是被迫代替她去往敵國(guó)和親剖效。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355