從0到1搭建兼容vue2和vue3的前端組件庫(文末附腳手架)

前言

隨著業(yè)務的不斷成熟瓣蛀、完善揖赴,大多數(shù)情況下我們會發(fā)現(xiàn)頁面中的功能大同小異鉴象,翻來覆去可能就是那幾套界面交互與邏輯,反復開發(fā)就很浪費成本古胆。

雖然目前市面上包括公司內已經有很多功能強大且完善的組件庫供我們使用肆良,但是對于我們目前復雜的產品的形態(tài)結構、各個頁面設計時也可能會有一個功能在不同迭代出了多套設計的情況逸绎,PM畫的原型也風格各異惹恃,同時也需要對多平臺、多業(yè)務的支持棺牧。導致很多場景下樣式也不能統(tǒng)一巫糙,溝通成本增大,效率低下颊乘,影響產品的可用性效果参淹,因此搭建符合自身B端產品平臺的組件庫的訴求迫在眉睫。

回顧組件庫搭建的整個項目過程乏悄,存在很多值得記錄浙值、反思的內容,文章接下來會分享和闡述項目中一些核心思路作為對整個項目流程的復盤檩小,同時也旨在幫助大家學習組件庫的構建思路开呐。

ps:但是開發(fā)組件庫還是需要投入時間和成本的,畢竟不是所有的業(yè)務都適合搞一套組件庫规求,一定要體現(xiàn)價值(提效筐付!提效!提效M窍)家妆。說了這么多鸵荠,接下來我們就來分析和實現(xiàn)一個團隊內部的組件庫吧冕茅。

通過本文檔可以得到什么

  • 從0到1搭建前端組件庫

  • 前端組件庫的系統(tǒng)設計

  • 同時兼容vue2 vue3的解決方案

  • 文檔站部署:如何接入kfx與流水線部署文檔站

  • 組件部署:組件發(fā)布到npm上

  • 一個基于vite的輕量版組件庫腳手架(附倉庫地址)

一、組件系統(tǒng)設計

1蛹找、層級設計

上圖是簡單描述了組件在頁面的作用方式及層級姨伤,我們可以看到一個頁面可以由基礎組件+業(yè)務組件+區(qū)塊構成。對于一個復雜的前端系統(tǒng)庸疾,想要組件能夠被頻繁使用乍楚,且能滿足大部分業(yè)務場景,就需要設計一個好的架構届慈,對頁面中的內容進行合理拆分徒溪。

2忿偷、組件拆分思路

成熟的大項目中有很多復雜的業(yè)務功能,但是不同模塊或者子系統(tǒng)之間很多業(yè)務往往是相通的或者相似的臊泌,一個功能往往需要寫很多次鲤桥,其實完全沒必要重復勞動,重復寫多次不僅浪費人力成本對于可維護性來說也是一種災難渠概,所以基于這種場景我們的 業(yè)務組件 就很有必要出場了茶凳。

我們可以把系統(tǒng)中常用的功能和需求進行梳理,把功能或者需求類似的有機體封裝成一個業(yè)務組件播揪,并對外暴露接口來實現(xiàn)靈活的可定制性贮喧,這樣的話我們就可以再不同頁面不同子系統(tǒng)中復用同樣的邏輯和功能了。

同理猪狈,不同頁面中往往有可能出現(xiàn)視覺或者交互完全相同或者類似的區(qū)塊(可以理解為大而全的業(yè)務組件)箱沦,為了提高可復用性和提高開發(fā)效率,我們往往會基于基礎組件和業(yè)務組件再進行一次封裝雇庙,讓其成為一個獨立的區(qū)塊以便直接復用饱普。

在需求收集上我們遵循兩個原則:

1、自上而下状共,首先需要根據對所有業(yè)務進行系統(tǒng)梳理套耕,提取出高優(yōu)組件;

2峡继、由內而外冯袍,這個過程可以卷起產品同學征集需求,避免閉門造車碾牌。

將組件劃分好后康愤,就需要有足夠細的粒度對組件進行拆分,這樣才能最大程度復用組件舶吗。

通過這樣一層層封裝征冷,我們就逐漸搭建了一套完整的組件化系統(tǒng)。但要注意一點就是高層次的組件會依賴低層次的組件誓琼,但是低層次的組件不可以包含高層次的組件检激。他們的關系就類似下圖:

image

二、組件庫搭建

先拋幾個問題:

  • 老項目都是vue2寫的腹侣,上了vue3叔收,之前vue2的代碼還能兼容嗎?

  • 老項目vue2太大了傲隶,暫時沒法全部升級饺律,怎么平滑過渡?

  • 新開工程用的vue3新技術研發(fā)出來的跺株,老項目也想引用這個模塊复濒,代碼是不是要降級脖卖?

1、怎么兼容vue2 vue3

常規(guī)思路:

開發(fā)一套vue2的組件庫巧颈,項目升級vue3時再同步升級一套vue3的組件庫胚嘲。

優(yōu)點:前期開發(fā)快,項目中要么是vue2要么是vue3洛二,所見即所得馋劈,沒有兼容的心智負擔

缺點:重構成本高,且需要維護兩套代碼晾嘶,維護成本高妓雾。(一個需求搞兩次,一個bug修兩遍垒迂,工作量加倍械姻,屬實頂不住)

懶癌思路:就想開發(fā)一套代碼机断,構建好可以同時支持vue2和vue3楷拳。

可行嗎?可行吏奸!使用vue-demi欢揖,打穿vue2-vue3的壁壘,上面的問題就不復存在了奋蔚。

根據創(chuàng)建者 Anthony Fu 的說法她混,Vue Demi 是一個開發(fā)實用程序,它允許用戶為 Vue 2 和 Vue 3 編寫通用的 Vue 庫泊碑,而無需擔心用戶安裝的版本坤按。

既然如此,我們先看下vue-demi的原理:主要是利用compositionAPI在寫法上和vue3的一致性進行兼容的過渡馒过。

核心:通過postinstall這個鉤子臭脓,對版本判斷從而去更改lib文件下的文件,動態(tài)改變用戶引用的版本腹忽。

image

v2 引入了compositionAPI支持vue3寫法

image

v3 什么都不用做来累,我們寫的就是vue3寫法,只不過沒有script setup留凭,具體原因后面會講佃扼。

image

總結一句偎巢,就是vue-demi會根據用戶使用vue的版本號來判斷蔼夜,vue2時加入@vue/composition-api。

那好了压昼,我們的寫法搞定了求冷,現(xiàn)在組件庫可以一套代碼兼容vue2和vue3了嗎瘤运?

還是不行,我們的核心功能是用sfc的vue文件打包的匠题,寫的是template拯坟,并不是render函數(shù),關于template的解析韭山,v2和v3解析出來的不能通用的郁季,因為v3之所以快,是因為對temlate的比對優(yōu)化了钱磅,具體咋優(yōu)化的大家可以查看vue3的源碼梦裂。這種場景肯定不能只打包一次就同時支持vue2和vue3調用。

那我們可以參考vue-demi盖淡,從postinstall著手年柠,也編譯兩個版本,在宿主系統(tǒng)中通過宿主系統(tǒng)的版本判斷要加載哪套組件代碼褪迟,不就ok了嗎冗恨?

沒錯,目前就是這么干的味赃。

image

此時就可以一套代碼掀抹,使用vue2和vue3編譯兩次,達到支持vue2和vue3的目的心俗。

我們來看下優(yōu)缺點:

優(yōu)點:一套代碼渴丸,易于維護,開發(fā)成本低另凌,同時支持vue2谱轨、vue3

缺點:寫代碼的時候,仍然有些寫法是vue2和vue3有差異的吠谢,并不能完全抹平土童,但是情況很少。需要編譯兩次工坊,

例如:組件上綁定v-model献汗,vue3子組件的屬性為 modelValue, vue2為 'value';

import { isVue2, isVue3 } from 'vue-demi' 
if (isVue2) { 
  // Vue 2 only 
} else { 
  // Vue 3 only 
}

問題:如果不用SFC王污,改用render的寫法罢吃,能只build一次嗎 ?

答:可以昭齐。

我們的組件render-demo.ts如下

import { defineComponent, h, ref } from 'vue-demi'

export default defineComponent({
  name: 'RenderDemo',
  props: {},
  setup() {
    const count = ref(0)
    return () => h('div', {
        on:{  // vue2 h函數(shù)底層為 createElement
            click(){
                console.log('update')
                count.value++
            }

        },
        onClick() {  // vue3  h函數(shù)
            console.log('update')
            count.value++
            }
        }, [
            h('div', `count: ${count.value}`),
            h('div', 'RenderDemo')
        ])
  }
})

通過編譯后產物是一樣的:即可認為不論在vue2還是vue3環(huán)境打包只build一次即可同時支持尿招。但是有vue2和vue3寫法上的區(qū)別(參照官方文檔:v3v2),需要手動處理就谜。PS:這樣享受不到vue3模板編譯靜態(tài)提升的優(yōu)化了怪蔑。

image

問題:vue3支持optionApi嗎?

答:支持丧荐。

問題:那能不能都用optionApi寫缆瓣,只build一次?

答:不能虹统,因為vue2和vue3編譯SFC的依賴插件不同弓坞,底層代碼有差異。

image
image

好吧...還是build兩次吧...

2车荔、怎么測試組件是否能在vue2 和vue3環(huán)境下正常使用

分別發(fā)布vue2 和vue3的包昼丑,然后在兩個環(huán)境引用進行測試驗證。

但是由于目前夸赫,潛在的問題就是組件庫依賴了其他的基礎組件庫菩帝,例如KwaiUi,這個在宿主系統(tǒng)中的版本是不確定的茬腿,所以這個地方可能有兼容性問題呼奢,暫時沒有想到好的解決辦法。

3切平、搭建流程

三握础、文檔站搭建

1、文檔站是拿什么寫的

基于vitepress構建

官網文檔:vitepress

2悴品、vue的組件為什么能寫到markdown里禀综?

VitePress 使用markdown-it作為 Markdown 渲染器。上面的很多擴展都是通過自定義插件實現(xiàn)的苔严。您可以使用以下選項進一步自定義markdown-it實例:markdown.vitepress/config.js

2定枷、為什么用vitepress

橫向對比

<colgroup><col width="0.2"><col width="0.2"><col width="0.2"><col width="0.2"><col width="0.2*"></colgroup>
|

框架

|

官網

|

優(yōu)點

|

缺點

|

匹配度

|
|

vuePress

|

https://www.vuepress.cn/

|

相比于vitePress插件更多一點,快速

|

需要對vite進行進一步支持

|

??????

|
|

vitePress

|

https://vitepress.vuejs.org/

|

原生支持vite以及vue3,提供了比較簡潔的文檔編輯能力,快速

|

因為是新出的错敢,所以插件不如vuePress豐富

|

?????????? 開箱即用

|
|

MDocs

|

https://main--mdocs-template.jinx.corp.kuaishou.com/

|

公司內部偶垮,可以嵌入可在線演示的代碼編輯器

|

react框架泼返,不匹配

|

??

|
|

通過markDown渲染器進行開發(fā)

|

市面上各種markDown解析器,webpack也有些loader插件。

例如:

import Markdown from 'vite-plugin-md'

export default defineConfig({
  // 默認的配置
  plugins: [
    vue({ include: [/\.vue$/, /\.md$/] }),
    Markdown(),
  ],
})

這樣就可以把md當vue用了

|

自由度高

|

成本高,基礎能力(例如路由荐虐,代碼高亮,導航等)需要自己開發(fā)各種插件

|

??

|

根據我們的需求情況丸凭,需要快速搭建福扬,核心內容也是將組件大范圍應用于業(yè)務腕铸,故而選擇vitepress。后續(xù)如果需要自由度更高的情況忧换,再換技術棧也是不遲的恬惯。

3向拆、vue的組件為什么能寫到markdown里亚茬?

image

底層大都是對markdown編譯為相應的html再套一層<template></template>進行預覽。具體如何做詞法解析的這里暫不做解釋浓恳。

VitePress 使用markdown-it作為 Markdown 渲染器刹缝。上面的很多擴展都是通過自定義插件實現(xiàn)的。vitepress也支持使用以下選項進一步自定義markdown-it實例:markdown.vitepress/config.js

const anchor = require('markdown-it-anchor')
module.exports = {
  markdown: {
    // options for markdown-it-anchor
    // https://github.com/valeriangalliat/markdown-it-anchor#permalinks
    anchor: {
      permalink: anchor.permalink.headerLink()
    },
    // options for markdown-it-toc-done-right
    toc: { level: [1, 2] },
    config: (md) => {
      // use more markdown-it plugins!
      md.use(require('markdown-it-xxx'))
    }
  }
}

4颈将、搭建流程

目錄架構如下(tips:使用mddir可以生成)

|-- docs
    |-- index.md // 首頁 yml語法描述填空即可
    |-- package.json
    |-- vite.config.ts 
    |-- yarn.lock
    |-- .vitepress  // vitepress 配置
    |   |-- config.js  // 主配置
    |   |-- routes // 文檔路徑配置
    |   |   |-- guide.js
    |   |-- theme  // 主題配置
    |       |-- index.js
    |-- guide    // 文檔在這個文件夾下
        |-- configuration.md
        |-- getting-started.md
        |-- what-is-commercial-ui.md
        |-- my-components  // 新建組件文檔目錄
        |   |-- my-components.md   // 組件文檔
        |   |-- demo  // 示例
        |       |-- demo-1.vue  // 按序號寫多個demo

安裝vitepress

npm i vitepress -D 
全局安裝 npm i vitepress -g

packages.json

{
  ...
  "scripts": {
    "docs:dev": "vitepress dev docs",
    "docs:build": "vitepress build docs",
    "docs:serve": "vitepress serve docs"
  },
  ...
}

docs目錄下執(zhí)行

$ yarn docs:dev

瀏覽器輸入:http://localhost:3000/getting-started

具體的配置可以參考官網文檔:vitepress

引入組件庫與依賴組件

cd docs/.vitepress/theme/index.js

import DefaultTheme from 'vitepress/theme';
import KwaiUI from '@ks/kwai-ui';

/** 庫導入 */
import AuditUI from '@ks-cqc/audit-ad-components';

/** 本地導入 */
// import AuditUI from '../../../src/index';

/** 依賴庫樣式導入 */
import '@ks/kwai-ui/lib/theme-new-era/index.css';
import '@ks-cqc/audit-ad-components/lib/v3/style.css';

export default {
    ...DefaultTheme,
    enhanceApp({ app }) {
        app.use(KwaiUI); // 依賴的基礎組件庫
        app.use(AuditUI); // 我們的業(yè)務組件庫
    },
};

現(xiàn)在我們就可以愉快地開發(fā)組件庫了梢夯,但是并不愉快,因為需要一直手動創(chuàng)建好多個文件晴圾,要是能一鍵創(chuàng)建就好了颂砸。

四、cli能力快速開發(fā)組件

到目前為止死姚,我們的整個“實時可交互式文檔”已經搭建完了人乓,是不是意味著可以交付給其他同學進行真正的組件開發(fā)了呢?假設你是另一個開發(fā)同學都毒,我跟你說:“你只要在這里色罚,這里和這里新建這些文件,然后在這里和這里修改一下配置就可以新建一個組件了账劲!”你會不會很想打人戳护?我們先看看需要哪些步驟...

1、快速開發(fā)一個基礎組件需要哪些文件瀑焦?

首先需要的是組件文件腌且,有了組件文件還需要文檔文件,至少需要四個榛瓮。

  • 創(chuàng)建四個文件如下:
image

image

創(chuàng)建了文件還不夠切蟋,還需要對一些文件進行修改。

  • 為文檔創(chuàng)建路由
image
  • 組件庫引入組件榆芦,注冊組件柄粹,暴露組件
image

好麻煩,組件還沒開發(fā)匆绣,竟然先要改這么多地方驻右,每次都這樣搞一遍人都麻了,這種枯燥乏味的事情還是交給程序干吧崎淳。堪夭。。

2、如何通過命令行快速創(chuàng)建模板文件森爽?

  • 首先就是需要解決文件創(chuàng)建

這個簡單恨豁,我們創(chuàng)建一套模板文件,對應上述四個必須的基礎文件

|-- component-template
  |-- comp   // 組件文件
  |   |-- component-template.vue // 組件代碼
  |   |-- index.ts  // 暴露組件
  |-- doc   // 文檔文件
    |-- component-template.md   // 組件文檔
    |-- demo // 文檔中的示例文件
    |-- demo-1.vue 

名稱怎么改呢爬迟?不能都叫demoComponent吧

文件名在copy時替換即可橘蜜,文件中的內容通過mastache語法進行模板替換即可。

代碼如下:

// 替換文件內容
function replaceInfo(filePath, componentName, componentDesc) {
    fs.readFile(filePath, (err, data) => {
        if (err) {
            return err;
        }
        let str = data.toString();
        str = str.replace(/{{component-name}}/g, componentName).replace(/{{component-desc}}/g, componentDesc);
        fs.writeFile(filePath, str, function (err) {
            if (err) {return err;}
        });
    });
}
// 替換文件名
async function modifyInfo({ filesPath, componentName, componentDesc}) {
    filesPath.forEach(async ({ target }) => {
        walk(target, function (path, fileName) {
            const oldPath = path + '/' + fileName;
            const newPath = path + '/' + fileName.replace('component-template', componentName);
            renameFile(oldPath, newPath);
            replaceInfo(newPath, componentName, componentDesc);
        });
    });
}
  • 自動修改上述的路由和組件引入以及暴露的文件

這里我們雖然也可以直接修改文件付呕,注入代碼计福,但是試想一下,如果日后組件越來越多徽职,這個時候路由和入口文件很可能這樣象颖,密密麻麻,還很亂姆钉,看著就頭疼说订。

image

image

所以我們不能用這么蠢的方式。潮瓶。陶冷。

需要簡單修改一下

  • 對于組件引入和暴露可以這樣

收攏到entry.ts中進行引入暴露

image

index.ts只需要對entry暴露出去的模塊對象遍歷注冊即可,單個導出也更簡潔了

// 引入公共樣式
import './styles/base.less';

// 引入組件
import * as components from './entry';
import { installArgument } from './utils/install';
// 全局注冊組件
const install = function (_vue: installArgument) {
    Object.values(components).forEach(comp => {
        if (!comp || !comp.name) {
            return;
        }
        comp.install(_vue);
    });
};

const plugin = {
    install,
};

// 單個導出
export * from './entry';

// 整體導出
export default plugin;
  • 文檔路由同理筋讨,也收攏到一個入口文件埃叭,進行遍歷
import components from '../../../components';

.....

compRouteConfig = [
        {
            text: '業(yè)務組件',
            collapsible: true,
            items: [
                ...Object.keys(components).map(comName => {
                    return { text: components[comName].zhName, link: `/guide/${components[comName].name}/${components[comName].name}` };
                }),
                // { text: 'demo', link: `/guide/demo/demo.md` }
            ],
        },
    ];

components.json中

{
    "demo-component": {
        "name": "demo-component",
        "zhName": "示例組件",
        "desc": "默認:這是一個新組件",
        "url": "./src/components/demo-component/index.ts"
    }
}

總結一下:

1、創(chuàng)建四個文件悉罕,替換其中關鍵信息

2赤屋、修改entry,components.json壁袄,添加新建的組件信息

需要的關鍵信息只有三個类早,組件名,組件描述嗜逻,組件中文名涩僻,所以我們創(chuàng)建時要輸入如下即可:

image

想實現(xiàn)命令行提問并收集輸入的答案需要用到一個庫 inquirer

代碼很簡單

inquirer.prompt([
        {
            type: 'input',
            name: 'componentName',
            message: 'Component name (kebab-case):',
            default: 'demo-component',
        },
        {
            type: 'input',
            name: 'componentZhName',
            message: '請輸入你要新建的組件名(中文):',
            default: '示例組件',
        },
        {
            type: 'input',
            message: '請輸入組件的功能描述:',
            name: 'componentDesc',
            default: '默認:這是一個新組件'
        }
    ]);

通過node運行后,會提出三個我們設計好的問題栈顷,保存在一個對象中我們導出使用即可逆日。然后拿著這些信息按照上面的流程替換模板內容,拷貝文件到對應目錄就可以快速開發(fā)了萄凤。

五室抽、部署

自己丟到服務器上即可,也可以托管到github靡努。

附:

組件庫基礎能力腳手架

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末坪圾,一起剝皮案震驚了整個濱河市晓折,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兽泄,老刑警劉巖漓概,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異病梢,居然都是意外死亡胃珍,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門飘千,熙熙樓的掌柜王于貴愁眉苦臉地迎上來堂鲜,“玉大人栈雳,你說我怎么就攤上這事护奈。” “怎么了哥纫?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵霉旗,是天一觀的道長。 經常有香客問我蛀骇,道長厌秒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任擅憔,我火速辦了婚禮鸵闪,結果婚禮上,老公的妹妹穿的比我還像新娘暑诸。我一直安慰自己蚌讼,他們只是感情好,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布个榕。 她就那樣靜靜地躺著篡石,像睡著了一般。 火紅的嫁衣襯著肌膚如雪西采。 梳的紋絲不亂的頭發(fā)上凰萨,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機與錄音械馆,去河邊找鬼胖眷。 笑死,一個胖子當著我的面吹牛霹崎,可吹牛的內容都是我干的珊搀。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼仿畸,長吁一口氣:“原來是場噩夢啊……” “哼食棕!你這毒婦竟也來了朗和?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤簿晓,失蹤者是張志新(化名)和其女友劉穎眶拉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體憔儿,經...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡忆植,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谒臼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朝刊。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蜈缤,靈堂內的尸體忽然破棺而出拾氓,到底是詐尸還是另有隱情,我是刑警寧澤底哥,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布咙鞍,位于F島的核電站,受9級特大地震影響趾徽,放射性物質發(fā)生泄漏续滋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一孵奶、第九天 我趴在偏房一處隱蔽的房頂上張望疲酌。 院中可真熱鬧,春花似錦了袁、人聲如沸朗恳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽僻肖。三九已至,卻和暖如春卢鹦,著一層夾襖步出監(jiān)牢的瞬間臀脏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工冀自, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留揉稚,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓熬粗,卻偏偏與公主長得像搀玖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子驻呐,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內容