微前端解決方案-qiankun實戰(zhàn)及部署

先來張圖片壓壓驚
image.png

在線demo:wzs.bengdada.com/

單獨訪問在線子應用:

一.導讀

1.什么是微前端
  • 微前端是一種多個團隊通過獨立發(fā)布功能的方式來共同構建現(xiàn)代化 web 應用的技術手段及方法策略熊镣。
  • 微前端架構具備以下幾個核心價值:
    技術棧無關 : 主框架不限制接入應用的技術棧前域,微應用具備完全自主權
    獨立開發(fā)、獨立部署 : 微應用倉庫獨立,前后端可獨立開發(fā),部署完成后主框架自動完成同步更新
    增量升級:在面對各種復雜場景時,我們通常很難對一個已經存在的系統(tǒng)做全量的技術棧升級或重構灾挨,而微前端是一種非常好的實施漸進式重構的手段和策略
    獨立運行時 : 每個微應用之間狀態(tài)隔離,運行時狀態(tài)不共享
  • 微前端架構旨在解決單體應用在一個相對長的時間跨度下竹宋,由于參與的人員涨醋、團隊的增多、變遷逝撬,從一個普通應用演變成一個巨石應用(Frontend Monolith)后,隨之而來的應用不可維護的問題乓土。這類問題在企業(yè)級 Web 應用中尤其常見宪潮。
2.qiankun是什么
  • qiankun 是一個基于 single-spa微前端實現(xiàn)庫,旨在幫助大家能更簡單趣苏、無痛的構建一個生產可用微前端架構系統(tǒng)狡相。
    官網: https://qiankun.umijs.org/zh
  • qiankun特性
    基于 single-spa 封裝,提供了更加開箱即用的 API食磕。
    技術棧無關尽棕,任意技術棧的應用均可 使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等框架彬伦。
    HTML Entry 接入方式滔悉,讓你接入微應用像使用 iframe 一樣簡單。
    樣式隔離单绑,確保微應用之間樣式互相不干擾回官。
    JS 沙箱,確保微應用之間 全局變量/事件 不沖突搂橙。
    ?? 資源預加載歉提,在瀏覽器空閑時間預加載未打開的微應用資源,加速微應用打開速度区转。
    umi 插件苔巨,提供了 @umijs/plugin-qiankun 供 umi 應用一鍵切換成微前端架構系統(tǒng)。
  • 了解完理論基礎废离,讓我們動手實踐一下···

二.建立項目

image.png

如圖: 我建立了一個主應用和三個子應用
主應用 main vue3搭建 "vue": "^3.0.0",
子應用 micro-react react18搭建 "react": "^18.1.0",
子應用 micro-vue2 vue2搭建 "vue": "^2.6.11",
子應用 micro-vue3 vue3搭建 "vue": "^3.0.0",

注意 : vue3技術選型我使用的是vue3 + webpack 侄泽,vite目前對于qiankun還不是太友好 ,硬要搞vite代價會很大厅缺,后續(xù)等官網優(yōu)化后我們在去使用vite

由于搭建項目太簡單我就不說明了 ~ ovo

三.主應用

注意: qiankun 需要一個主應用 來注入所有的子應用
先安裝乾坤的依賴包

 yarn add qiankun # 或者 npm i qiankun -S

目前乾坤是2.0版本 安裝后package.json 是2.72版本

image.png

在安裝 element-plus 把項目的布局簡單做一下

npm install element-plus --save

注意: vue3 安裝element-plus蔬顾, vue2安裝element-ui

src下新建micro-app.js 用于存放所有子應用
const microApps = [
  {
    name: 'micro-react', //應用名 項目名最好也是這個
    entry: '//localhost:20000', //默認會加載這個html 解析里面的js 動態(tài)的執(zhí)行 (子應用必須支持跨域)內部用的fetch
    activeRule: '/react', // 激活的路徑
    container: '#micro-react', // 容器名
    props: {}, //父子應用通信
  },
  {
    name: 'micro-vue2',
    entry: '//localhost:30000',
    activeRule: '/vue2',
    container: '#micro-vue2',
    props: {},
  },
  {
    name: 'micro-vue3',
    entry: '//localhost:40000',
    activeRule: '/vue3',
    container: '#micro-vue3',
    props: {},
  },
];

export default microApps;
新建vue.config.js
module.exports = {
  devServer: {
    port: 8000,
    headers: {
      // 重點1: 允許跨域訪問子應用頁面
      'Access-Control-Allow-Origin': '*',
    },
  },
};
Main頁面
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// createApp(App).use(store).use(router).mount('#app')

//-----------------------上面是原先的,下面是新增的-----------------------------

import ElementPlus from 'element-plus'; //element-plus
import 'element-plus/dist/index.css'; //element-plus
import { registerMicroApps, start } from 'qiankun';
import microApps from './micro-app';

let app = createApp(App);
app.use(store);
app.use(router);
app.use(ElementPlus);
app.mount('#app');

registerMicroApps(microApps, {
  //還有一些生命周期 如果需要可以根據官網文檔按需加入
  beforeMount(app) {
    console.log('掛載前', app);
  },
  afterMount(app) {
    console.log('卸載后', app);
  },
});

start({
  prefetch: false, //取消預加載
  sandbox: { experimentalStyleIsolation: true }, //沙盒模式
}); 
進入App頁面簡單調下布局
<template>
  <el-menu :router="true" mode="horizontal">
    <!-- 子應用的跳轉路徑 -->
    <el-menu-item index="/">主應用 main</el-menu-item>
    <el-menu-item index="/react">子應用 react18</el-menu-item>
    <el-menu-item index="/vue2">子應用 vue2</el-menu-item>
    <el-menu-item index="/vue3">子應用 vue3</el-menu-item>
    <el-menu-item index="/about">{{ $store.state.GlobalData }}</el-menu-item>
  </el-menu>
  <router-view />
  <!-- 子應用的容器 -->

  <div id="micro-react"></div>
  <div id="micro-vue2"></div>
  <div id="micro-vue3"></div>
</template>

<script>
export default {
  name: 'App',
  setup() {
    return {};
  },
};
</script>

<style lang="less">
html,
body,
#app {
  width: 100%;
  height: 100%;
  margin: 0;
}
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

需要注意: app里的容器名和跳轉路徑都不是隨便起的 需要和micro-app.js 定義好的子應用一一對應

image.png

到此主應用搭建完畢~~~ovo

四.子應用

1.react

安裝npm install react-app-rewired 重寫默認的react配置文件
npm install react-app-rewired --save

修改package.json,原本的react-script 改為react-app-rewired

"scripts": {
   "start": "react-app-rewired start",
   "build": "react-app-rewired build",
   "test": "react-app-rewired test",
   "eject": "react-app-rewired eject"
 },
安裝npm i react-router-dom 我安裝的是最新版本 "react-router-dom": "^6.3.0"
npm i react-router-dom --save
根目錄下新建.env文件
PORT=20000
#  防止熱更新出錯
WDS_SOCKET_PORT=20000 
src下新建public-path.js (用于修改運行時的 publicPath)
//判斷是否是qiankun加載
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
src下新建 config-overrides.js
const { name } = require('./package');

module.exports = {
  webpack: config => {
    config.output.library = `${name}-[name]`; 
    config.output.libraryTarget = 'umd';
    config.output.globalObject = 'window';
    return config;
  },
  devServer: _ => {
    const config = _;

    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};

進入src下index.js
// import logo from './logo.svg';
// import './App.css';

// function App() {
//   return (
//     <div className="App">
//       <header className="App-header">
//         <img src={logo} className="App-logo" alt="logo" />
//         <p>
//           Edit <code>src/App.js</code> and save to reload.
//         </p>
//         <a
//           className="App-link"
//           
//           target="_blank"
//           rel="noopener noreferrer"
//         >
//           Learn React
//         </a>
//       </header>
//     </div>
//   );
// }

// export default App;

// ------------------------上面原先的宴偿,下面最新的------------------------------------

import logo from './logo.svg';
import './App.css';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <>
      {/* basename 判斷如果是qiankun加載 basename為react 相當于加個標識*/}
      <Router basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}>
        {/*  */}
        <Link to="/">首頁</Link>
        <Link to="/about">關于頁面</Link>
        <Routes>
          <Route path="/" element={<Home />}></Route>
          <Route path="/about" element={<About />}></Route>
        </Routes>
      </Router>
    </>
  );
}

function About() {
  return <div>about</div>;
}

function Home() {
  return (
    <div>
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a className="App-link"  target="_blank" rel="noopener noreferrer">
          Learn React
        </a>
      </header>
    </div>
  );
}
export default App;

2.vue2

src下新建public-path.js 用于修改運行時的 publicPath
// eslint-disable-next-line no-undef
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
在main頁面 引入public-path.js文件
// import Vue from 'vue';
// import App from './App.vue';
// import router from './router';

// Vue.config.productionTip = false

// new Vue({
//   router,
//   render: h => h(App)
// }).$mount('#app')

// ·················上面原先的 下面新增的·····················
import './public-path';
import Vue from 'vue';
import App from './App.vue';
import router from './router';

// Vue.config.productionTip = false

let instance = null;
function render(props = {}) {
  const { container } = props;
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

// 如何獨立運行微應用?
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap(props) {
  // 啟動
}
export async function mount(props) {
  // 掛載  onGlobalStateChange 可通過這個屬性來進行父子應用通信 發(fā)布訂閱機制
  render(props);
}
export async function unmount(props) {
  // 卸載
  instance.$destroy();
}

新增vue.config.js文件
const { name } = require('./package');

module.exports = {
  devServer: {
    port: 30000,
    headers: {
      'Access-Control-Allow-Origin': '*', //開發(fā)時增加跨域 表示所有人都可以訪問我的服務器
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把子應用打包成 umd 庫格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
router.js文件
const router = new VueRouter({
  mode: 'history',
  // base: process.env.BASE_URL,
  base: '/vue2',
  routes,
});

3.vue3

src下新建public-path.js 用于修改運行時的 publicPath
// eslint-disable-next-line no-undef
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
在main頁面 引入public-path.js文件
import './public-path'; // 注意需要引入public-path
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

let instance = null;

function render({ container } = {}) {
  instance = createApp(App);
  instance.use(router);
  instance.use(store);
  instance.mount(container ? container.querySelector('#app') : '#app');
}


// 如何獨立運行微應用诀豁?
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap(props) {
  // 啟動
}
export async function mount(props) {
  // 掛載
  render(props);
}
export async function unmount(props) {
  // 卸載
  instance.unmount();
  instance = null;
}
新增vue.config.js文件
const { name } = require('./package');

module.exports = {
  devServer: {
    port: 40000,
    headers: {
      'Access-Control-Allow-Origin': '*', //開發(fā)時增加跨域 表示所有人都可以訪問我的服務器
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把子應用打包成 umd 庫格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

到這里項目搭建完畢窄刘,基礎跳轉沒有問題 ,可以在主應用和子應用跳轉
bug :主應用和子應用使用不同版本的vue后路由切換報錯 ?
bug :主應用樣式與子應用樣式沖突 ?
需求 :父子組件傳參如何實現(xiàn) ?
需求 :如何部署 ?
別擔心 下面我一一解答

5.bug

[Bug]主應用和子應用使用不同版本的vue后路由切換報錯

問題的原因 : vue-router 3.x與vue-router 4.x設置的history.state的數據結構不同
低版本的 vue-router 在 pushState 的時候舷胜,會覆蓋丟失主路由的 history.state娩践,導致主路由跳轉異常
解決辦法 : 主應用監(jiān)聽router.beforEach 手動修改history.state數據結構

import _ from "lodash"

router.beforeEach((to, from, next) => {
  if (_.isEmpty(history.state.current)) {
    _.assign(history.state, { current: from.fullPath });
  }
  next();
});
[Bug]主應用樣式與子應用樣式沖突

可以通過給css樣式名加前綴來實現(xiàn)隔離
https://blog.csdn.net/zjscy666/article/details/107864891
https://blog.csdn.net/m0_54854484/article/details/123442168

6.需求

[需求] 父子組件傳參如何實現(xiàn)

qiankun通過initGlobalState, onGlobalStateChange, setGlobalState實現(xiàn)主應用的全局狀態(tài)管理,然后默認會通過props將通信方法傳遞給子應用烹骨。先看下官方的示例用法:

主應用

// main/src/main.js
import { initGlobalState } from 'qiankun';
// 初始化 state
const initialState = {
  user: {} // 用戶信息
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
  // state: 變更后的狀態(tài); prev 變更前的狀態(tài)
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

子應用

// 從生命周期 mount 中獲取通信方法翻伺,props默認會有onGlobalStateChange和setGlobalState兩個api
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 變更后的狀態(tài); prev 變更前的狀態(tài)
    console.log(state, prev);
  });
  props.setGlobalState(state);
}

這兩段代碼不難理解,父子應用通過onGlobalStateChange這個方法進行通信沮焕,這其實是一個發(fā)布-訂閱的設計模式吨岭。
ok,官方的示例用法很簡單也完全夠用峦树,純JavaScript的語法辣辫,不涉及任何的vue或react的東西,開發(fā)者可自由定制魁巩。

如果我們直接使用官方的這個示例急灭,那么數據會比較松散且調用復雜,所有子應用都得聲明onGlobalStateChange對狀態(tài)進行監(jiān)聽谷遂,再通過setGlobalState進行更新數據葬馋。

因此,我們很有必要對數據狀態(tài)做進一步的封裝設計

主應用src下新建actions.js
//src/actions.js
// 父子應用通信
import { initGlobalState } from 'qiankun';
import store from './store';

const state = {
  //這里寫初始化數據
  name: 'wang',
  age: 123,
  count: 0,
};

const actions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  console.log('主應用變更前:', state);
  console.log('主應用變更后:', prev);
  store.commit('setGlobalData', state);
});

store.commit('setGlobalData', state);

export default actions;

將初始化的數據存到vuex中 如果數據變更了 在將變更后的數據存到vuex

主應用main store文件夾下index.js中
//store/index.js 
import { createStore } from 'vuex';

export default createStore({
  state: {
    GlobalData: {},
  },
  mutations: {
    setGlobalData(state, value) {
      state.GlobalData = value;
    },
  },
  actions: {},
  modules: {},
});

最后在main.js 中導入

//main.js
import './actions.js'
子應用 (vue3)

核心:通過將主應用的onGlobalStateChange肾扰,setGlobalState方法掛載到全局就可以使用了

import './public-path'; // 注意需要引入public-path
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

let instance = null;

//核心
function render(props) {
  const { container, onGlobalStateChange, setGlobalState } = props;
  console.log(props);
  instance = createApp(App);
  instance.config.globalProperties.$onGlobalStateChange = onGlobalStateChange;
  instance.config.globalProperties.$setGlobalState = setGlobalState;
  instance.use(router);
  instance.use(store);
  instance.mount(container ? container.querySelector('#app') : '#app');
}

// 如何獨立運行微應用畴嘶?
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap(props) {
  // 啟動
}
export async function mount(props) {
  // 掛載
  render(props);
}
export async function unmount(props) {
  // 卸載
  instance.unmount();
  instance = null;
}
使用

主應用

<template>
  <div>
    <h1>主應用/vue3子應用 的全局數據</h1>
    <div>姓名 : {{ $store.state.GlobalData.name }}</div>
    <div>年齡 : {{ $store.state.GlobalData.age }}</div>
    <div>數量 : {{ $store.state.GlobalData.count }}</div>

    <el-button type="primary" @click="revampData">修改全局數據</el-button>
  </div>
</template>

<script setup>
import actions from '../actions';
const revampData = () => {
  actions.setGlobalState({ name: '主應用' });
};
</script>

子應用(vue3)

<template>
    <div>我是vue3項目</div>
    <button @click="revampData">修改全局數據</button>
</template>

<script>
import { getCurrentInstance } from 'vue';
export default {
  name: 'Home',
  components: {
    HelloWorld,
  },
  setup() {
    const { proxy } = getCurrentInstance();
    const revampData = () => {
      proxy.$setGlobalState({ name: 'vue3子應用應用' });
    };

    return {
      revampData,
    };
  },
};
</script>

[需求] 如何部署

qiankun部署的帖子網上根本找不到, 可能是感覺簡單就都不想說了吧,筆者這里也是部署了很多遍才跑通白对,這里說下我的思路掠廓。

考慮到主應用和子應用共用域名時可能會存在路由沖突的問題,子應用可能會源源不斷地添加進來甩恼,因此我們將子應用都放在xx.com/subapp/這個二級目錄下蟀瞧,根路徑/留給主應用。

步驟如下:
1.主應用main和所有子應用都打包出一份html,css,js,static条摸,分目錄上傳到服務器悦污,子應用統(tǒng)一放到subapp目錄下,最終如:

├── main
│   └── index.html
└── subapp
    ├── sub-react
    │   └── index.html
    └── sub-vue
        └── index.html

2.配置nginx钉蒲,預期是xx.com根路徑指向主應用切端,xx.com/subapp指向子應用,子應用的配置只需寫一份,以后新增子應用也不需要改nginx配置顷啼,以下應該是微應用部署的最簡潔的一份nginx配置了踏枣。

    server{
        listen 80;                                            #偵聽端口
        server_name http://wzs.bengdada.com/;                 #定義使用www.xx.com訪問
        charset utf-8;

        location / {
            root /data/wzs/main;  # 主應用所在的目錄
            try_files $uri $uri/ /index.html;
        }
        location /subapp {
            alias /data/wzs/subapp;  # 主應用所在的目錄
            try_files $uri $uri/ /index.html;
        }
    }
nginx -s reload后就可以了昌屉。

本文特地做了線上demo展示:
整站(主應用):wzs.bengdada.com/
單獨訪問子應用:

最后

本人從開始弄微前端反復查閱大量資料和視頻,踩過很多坑茵瀑,忍不住感嘆 : 真是學無止境.....
最后的最后间驮,喜歡本文的同學還請能順手給個贊鼓勵一下,非常感謝看到這里马昨。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末竞帽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子鸿捧,更是在濱河造成了極大的恐慌屹篓,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匙奴,死亡現(xiàn)場離奇詭異堆巧,居然都是意外死亡,警方通過查閱死者的電腦和手機泼菌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門恳邀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人灶轰,你說我怎么就攤上這事∷⒏郑” “怎么了笋颤?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長内地。 經常有香客問我伴澄,道長,這世上最難降的妖魔是什么阱缓? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任非凌,我火速辦了婚禮,結果婚禮上荆针,老公的妹妹穿的比我還像新娘敞嗡。我一直安慰自己,他們只是感情好航背,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布喉悴。 她就那樣靜靜地躺著,像睡著了一般玖媚。 火紅的嫁衣襯著肌膚如雪箕肃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天今魔,我揣著相機與錄音勺像,去河邊找鬼障贸。 笑死,一個胖子當著我的面吹牛吟宦,可吹牛的內容都是我干的篮洁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼督函,長吁一口氣:“原來是場噩夢啊……” “哼嘀粱!你這毒婦竟也來了?” 一聲冷哼從身側響起辰狡,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤锋叨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后宛篇,有當地人在樹林里發(fā)現(xiàn)了一具尸體娃磺,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年叫倍,在試婚紗的時候發(fā)現(xiàn)自己被綠了偷卧。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡吆倦,死狀恐怖听诸,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情蚕泽,我是刑警寧澤晌梨,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站须妻,受9級特大地震影響仔蝌,放射性物質發(fā)生泄漏。R本人自食惡果不足惜荒吏,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一敛惊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧绰更,春花似錦瞧挤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盒粮,卻和暖如春鸵鸥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工妒穴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宋税,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓讼油,卻偏偏與公主長得像杰赛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子矮台,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容