封裝一個(gè)Nodejs框架實(shí)踐

前言

在大多數(shù)nodejs項(xiàng)目里都使用了ExpressJS框架進(jìn)行開發(fā)循诉,ExpressJS 是一個(gè)簡(jiǎn)潔而靈活的 Node.js Web應(yīng)用框架, 提供一系列強(qiáng)大特性幫助你創(chuàng)建各種 Web 應(yīng)用畔况,express對(duì)nodejs自帶的HTTP模塊和路由做了適度的封裝,并加入了中間件功能脯倒,足以應(yīng)付大多數(shù)的項(xiàng)目開發(fā)星虹,筆者也是用ExpressJS做基礎(chǔ)框架,在做過(guò)幾個(gè)項(xiàng)目之后除师,結(jié)合項(xiàng)目組成員及項(xiàng)目的一些特點(diǎn)地回,我想對(duì)傳統(tǒng)的項(xiàng)目結(jié)構(gòu)做了一下調(diào)整扁远,我希望按不同的業(yè)務(wù)模塊進(jìn)行目錄劃分,每個(gè)模塊目錄可擁有獨(dú)立的controller刻像、service畅买、model、static等细睡,主要目的也是讓開發(fā)人員更關(guān)注具體的業(yè)務(wù)谷羞,一些雜活就要框架去處理好了。

本文采用目前比較主流的框架及模塊進(jìn)行實(shí)踐,底層框架使用Express湃缎,ORM使用了SequelizeJS犀填,日志模塊log4js,模板引擎nunjucks嗓违。文章中不會(huì)介紹上述模塊的使用以及框架里具體代碼實(shí)現(xiàn)九巡,只是介紹如果將這些框架模塊結(jié)合起來(lái)開發(fā)一套自己的框架。

為什么要封裝

單從實(shí)現(xiàn)業(yè)務(wù)需求上來(lái)說(shuō)蹂季,不封裝也是完全可以的冕广,封裝的主要目的有以下幾點(diǎn):

  1. 希望即使從未接觸過(guò)Nodejs和其它框架模塊的人也可以快速開發(fā),實(shí)現(xiàn)基本業(yè)務(wù)需求偿洁,熟悉javascript的同學(xué)撒汉,即使沒有后臺(tái)經(jīng)驗(yàn),也能完成前后端開發(fā)任務(wù)涕滋。
  2. 針對(duì)團(tuán)隊(duì)項(xiàng)目特點(diǎn)睬辐,做更多的抽象及復(fù)用,建立內(nèi)部開發(fā)規(guī)范宾肺,為團(tuán)隊(duì)開發(fā)帶來(lái)便利提高開發(fā)效率溉委。
  3. 方便擴(kuò)展和升級(jí),如果底層的某個(gè)功能模塊需要升級(jí)爱榕,不會(huì)影響到已編寫的業(yè)務(wù)代碼,哪怕是更換了底層模塊坡慌,只要保證調(diào)用的方法一致黔酥,也是可以正常運(yùn)行的。
  4. 通過(guò)封裝實(shí)踐洪橘,能更多的了解底層框架及用途跪者,純粹學(xué)習(xí)和提高自己的設(shè)計(jì)能力:)

如何設(shè)計(jì)

在封裝之前,先考慮一下最終如何給人使用熄求,即開發(fā)人員的項(xiàng)目結(jié)構(gòu)應(yīng)該是什么樣渣玲,需要做哪些事情等,這決定了框架開發(fā)規(guī)范及約束的設(shè)計(jì)弟晚,如果要實(shí)現(xiàn)各模塊獨(dú)立開發(fā)忘衍,每個(gè)模塊擁有自己的控制層、服務(wù)層之類的卿城,應(yīng)該需要一個(gè)moduels目錄用于存放各個(gè)目錄枚钓,通常做一個(gè)WEB項(xiàng)目最常見的功能有路由定義、視圖渲染瑟押、數(shù)據(jù)操作及業(yè)務(wù)邏輯處理幾大項(xiàng)搀捷,在這我采用了傳統(tǒng)的MVC分層方式爬迟,為了根目錄更加簡(jiǎn)潔一點(diǎn)门怪,我考慮用app做為根目錄,將模塊等都放在app目錄下,另外還需要有一個(gè)配置目錄说订,用于區(qū)分不同環(huán)境下的配置,最后設(shè)計(jì)的目錄如下:

目錄結(jié)構(gòu)

├─app
│  |- modules    //模塊目錄
│  │  |─ module_A   //業(yè)務(wù)模塊A
│  │  │  |─ ctrls //控制器目錄
│  │  │  |   └─ controller1.js
│  │  │  |─ views //視圖模板目錄
│  │  │  |   └─ index.html
│  │  │  |─ [static] 靜態(tài)文件等
│  │  │  |─ [models] 數(shù)據(jù)處理文件
│  │  │  |─ [servs]  服務(wù)層 業(yè)務(wù)處理
│  │  │  └─ [router.js] 模塊的路由配置文件
│  │  |── module_B 模塊B
│  |─ [models]  //ORM 模型定義(頂級(jí)先巴,表示可通用)
│  |─ [routes]  //通用路由器配置
│  |─ [views]   //通用視圖模板
|  |- [bridge]  //橋接文件目錄
│  └─ [ctrls]   //通用控制器文件
├─ config -> 環(huán)境配置目錄
│   |─ default.js 默認(rèn)配置文件
│   |─ [development.js]  //開發(fā)環(huán)境配置文件
│   |─ [production.js]   //生產(chǎn)環(huán)境配置文
│   └─ [testing.js]      //測(cè)試環(huán)境配置
└─ run.js         //啟動(dòng)文件

這樣看來(lái)致扯,整個(gè)項(xiàng)目的根目錄就只有app、config兩個(gè)像街,再加一個(gè)run.js用于啟動(dòng)項(xiàng)目黎棠,modules目錄用于存放各個(gè)模塊,每個(gè)模塊可以編寫自己的業(yè)務(wù)代碼镰绎,當(dāng)然脓斩,不能強(qiáng)制開發(fā)人員什么情況都需要使用模塊,所以即使沒有modules模塊也是可以的畴栖,我們可以直接將一些通用或是不需要?jiǎng)澐帜K的代碼寫到頂級(jí)随静,即app目錄下。
現(xiàn)在從項(xiàng)目結(jié)構(gòu)上看吗讶,我們已經(jīng)定出了一個(gè)開發(fā)規(guī)范燎猛,框架根據(jù)上面的目錄結(jié)構(gòu)進(jìn)行路由及模塊的動(dòng)態(tài)加載,上面加方括號(hào)的表示可選項(xiàng)照皆。

有了原型結(jié)構(gòu)重绷,就可以開始封裝了,我們先從路由開始膜毁,先來(lái)看一下Express中路由的定義:

//來(lái)自官方文檔
var express = require('express');
var app = express();

app.get('/', function(req, res) {
  res.send('hello world');
});

上面定義了一個(gè)首頁(yè)路由昭卓,輸入域名會(huì)返回hello world,這是個(gè)非常簡(jiǎn)單的示例瘟滨,首先我們需要引入express候醒,然后定義一個(gè)HTTP請(qǐng)求方法GET、POST等杂瘸,當(dāng)然一般情況下我們會(huì)將路由寫到獨(dú)立的文件里倒淫,然后再啟動(dòng)項(xiàng)目時(shí)導(dǎo)入,但是不管寫多少個(gè)路由文件败玉,都是需要引入require('express')敌土,如果將路由的句柄(就叫控制器吧)也單獨(dú)寫到文件中,則需要在路由文件中引入控制器文件绒怨,來(lái)看一下例子:

home.js (回調(diào)函數(shù)句柄-控制器)

module.exports = {
    home: function(req, res){
        res.send('home');
    },
    
    about: function(req, res){
        res.send('about');
    }
}

main_router.js (路由文件)

const express = require('express');
 //需使用 express.Router 創(chuàng)建模塊化纯赎、可掛載的路由
const router = express.Router();
//引入控制器
const homeController = require('controller/home')

// 匹配根路徑的請(qǐng)求
app.get('/', homeController.home);
// 匹配 /about 路徑的請(qǐng)求
app.get('/about', homeController.about );

app.js

//在應(yīng)用中加載路由模塊:
var express = require('express');
var app = express();
var router = require('./main_router');

app.use('/', router);
...

路由文件的作用就是如何定義應(yīng)用的端點(diǎn)(URIs)以及如何響應(yīng)客戶端的請(qǐng)求,我們想一下南蹂,其實(shí)關(guān)鍵的幾個(gè)點(diǎn)就是定義了請(qǐng)求方法和指定回調(diào)函數(shù)犬金,所以能不能將路由做一個(gè)配置文件就可以了,對(duì)于引入框架和控制器這些都交由框架實(shí)現(xiàn)就好了,比如把上面的路由文件寫成這樣:

//router.js
exports.routers = [
    { prefix: ["/", "/index.html"], ctrl: "home", action: "home" },
    { prefix: "/about", ctrl: "home", action: "about"}
]
  • prefix 指定路由的路徑
  • ctrl 指定路由命中后要執(zhí)行的控制器
  • action 指定默認(rèn)執(zhí)行的方法晚顷,跟 ctrl 控制器中指定的方法名對(duì)應(yīng)
  • [method] 指定請(qǐng)求的方法峰伙, 比如get, post, put等,默認(rèn)值為get

當(dāng)框架啟動(dòng)時(shí)该默,路由模塊router.js應(yīng)該根據(jù)ctrl參數(shù)自動(dòng)加載控制器文件瞳氓,再將action函數(shù)注入到Express路由里,這樣配置過(guò)后栓袖,不需要在應(yīng)用中再進(jìn)引入路由文件匣摘,開發(fā)人員只需要編寫控制器文件,實(shí)現(xiàn)業(yè)務(wù)邏輯就好裹刮。

模塊路由
既然分模塊開發(fā)音榜,一般情況路由的地址也會(huì)按模塊進(jìn)行區(qū)分,所以如果是模塊下的路由捧弃,框架考慮自動(dòng)將模塊目錄名做為前綴添加到URI前面赠叼,比如模塊名為user,其下的路由都需要加上 /user/.. 進(jìn)行訪問(wèn)违霞。

當(dāng)然嘴办,我們還要考慮讓用戶自定義前綴名稱,所以再加一個(gè)參數(shù)可以額外指定前綴:

//router.js
//指定模塊別名 第一參數(shù)為別名买鸽,第二個(gè)為模塊名
exports.url_prefix = ["a", "user"]
...

加上別名后涧郊,模塊下的路由都會(huì)通過(guò)別名進(jìn)行訪問(wèn),比如要訪問(wèn)上面routers眼五,需要添加別名:

  • http://.../a/index.html
  • http://.../a/about

過(guò)濾器
當(dāng)然底燎,像這樣簡(jiǎn)單的配置還是不夠的,比如要實(shí)現(xiàn)一個(gè)過(guò)濾器弹砚,這是一個(gè)很常用的功能,那就考慮給配置項(xiàng)添加一個(gè)新的節(jié)點(diǎn)filter枢希,如下:

//router.js
exports.routers = [
    { prefix: "/about", ctrl: "home", action: "about", filter: "login_required"}
]
//定義過(guò)濾器
exports.filters = {
    //定義一個(gè)過(guò)濾器名稱
    "login_required": {
        //prefix指定是一個(gè)掛載桌吃,如果是*號(hào)就是模塊下所有路由都應(yīng)用
        prefix: "mount",
        //handler 要執(zhí)行的句柄
        handler:  function(req, res, next){
            if(req.session.loginUser == undefined ){
                res.redirect('/user/login.html')
            }else next();
        }
    }
}

當(dāng)訪問(wèn)/about時(shí),框架要先執(zhí)行過(guò)濾器 login_required 方法苞轿,當(dāng)然茅诱,過(guò)濾器的定義可以寫成一個(gè)單獨(dú)的文件。
除了過(guò)濾器搬卒,框架還實(shí)現(xiàn)了多控制器處理等功能瑟俭,實(shí)現(xiàn)的代碼在這里不詳解了,可以參考源碼或者示例源碼

數(shù)據(jù)層ORM封裝

數(shù)據(jù)層主要是使用Sequelize做為基礎(chǔ)框架契邀,要使用ORM需要先定義好數(shù)據(jù)模型摆寄,先來(lái)看一下官方的例子:

//引入sequelize
const Sequelize = require('sequelize');
//定義一個(gè)模型
const User = sequelize.define('user', {
  firstName: {
    type: Sequelize.STRING
  },
  lastName: {
    type: Sequelize.STRING
  }
});
//導(dǎo)出方法
exports.addUser = function(userName, email) {
    //向 user 表中插入數(shù)據(jù)
    return User.create(...);
}
//通過(guò)用戶名查找用戶
exports.findByName = function(userName) {
    return User.findOne(...);
}

我希望讓開發(fā)人員以類的形式去聲明數(shù)據(jù)模型,所以我打算按下面的樣子封裝一下,讓框架能按類的形式去定義模型:

class Users {
    constructor(){
        super()

        this.tableName = "user"
        this.fields = {
            firstName: {type: "string(11)"},
            lastName: {type: "string(11)"}
        }
    }
    
    //添加一個(gè)獲取用戶列表的方法
    addUser (userName, email) {
        return this.create(...);
    }
    //通過(guò)用戶名查找用戶
    findByName (){
        return this.findOne(...);
    }
}

這種方法微饥,隱去了直接掉用 Sequelize 的方法逗扒,這樣的目的是為了讓非nodejs開發(fā)人員去太關(guān)注Sequelize的用法,只需要關(guān)注如果實(shí)現(xiàn)業(yè)務(wù)就好欠橘,另外一點(diǎn)是如果要更換數(shù)據(jù)庫(kù)框架或是升級(jí)矩肩,可以不用去修改這些模型,不影響業(yè)務(wù)肃续。
既然按類的形式去封裝了黍檩,那是不是可以給每個(gè)模型都定義一個(gè)基類,這樣可以給模型附加一些基礎(chǔ)的方法始锚,比如這樣寫:

class Users extends APP.DB.DBModel{
    ...
}

APP.DB.DBModel 是框架提供的一個(gè)基類刽酱,提供一些基礎(chǔ)或是擴(kuò)展的方法,比如一些對(duì)數(shù)據(jù)格式化轉(zhuǎn)換之類的疼蛾。當(dāng)然肛跌,如果按類的方法封裝后,要想使用一些原生的方法怎么辦察郁?我們可以考慮將類做一個(gè)代理衍慎,通過(guò)反射將原生方法暴露出去:

//通過(guò)代理調(diào)用Sequelize原生方法
var cProxy = new Proxy(class, {
    get: function(target, key, receiver) {
        if (target[key] == undefined) {
            //target.ORM 是sequelize對(duì)象
            return target.ORM[key];
        }
        return target[key];
    }
});

定義好模型后,在需要用到數(shù)據(jù)模型的代碼里引入模型就可以使用皮钠,示例代碼:

//導(dǎo)入模型
const models = require("./models")
var m_user = models.Users;
let u = await m_user.getone({where: {"firstName": "xx"}})
console.log(u.firstName)

橋接文件

橋接文件主要是給開發(fā)人員編寫額外通用業(yè)務(wù)代碼所用的稳捆,也是用框架自己加載執(zhí)行,比如我們需要給模板引擎添加方法變量麦轰、添加一些通用中間件等乔夯。看一個(gè)給模板引擎的例子:

//templateExt.js 專門用于定制模板文件
module.exports.tpl  = {
    //init初始化方法款侵,將會(huì)自動(dòng)執(zhí)行
    init (app){
        //獲取模板對(duì)象
        let tpl = app.get('tpl');
        //添加一個(gè)日期過(guò)濾器
        tpl.addFilter('formatTimestamp', function(t, f="yyyy-MM-dd HH:mm:ss"){
            return new Date(t*1000).pattern(f)
        })
        ...
    }
}

//comm.js 通用文件
module.exports.comm = {
    methodA (){ ... }
}

在代碼任何地方末荐,我們可以通過(guò)框架提供的全局變量APP訪問(wèn)到橋接文件里的成員,比如要訪問(wèn)comm.js里的methodA方法新锈,可以通過(guò)APP.comm.methodA形式訪問(wèn)甲脏。

配置文件

配置文件主要用于讓開發(fā)人員指定不同環(huán)境下所需要的配置參數(shù),比如指定數(shù)據(jù)庫(kù)妹笆、日志等相關(guān)信息块请,通常我們會(huì)使用一個(gè)JSON文件定義配置,在JSON里配置不同環(huán)境下的節(jié)點(diǎn)參數(shù)拳缠,比如:

//config.js
{
    'development': ...
    'testing': ...
    'production': ...
}

這里墩新,我考慮使用類的形式來(lái)定義,感覺看起來(lái)比較清晰獨(dú)立一點(diǎn)窟坐,最后部署的時(shí)候海渊,只需要對(duì)應(yīng)的配置文件就可以了:

//config.js
class Config{
    constructor(){
        //是否開啟調(diào)試模式
        this.debug = false;
        //數(shù)據(jù)庫(kù)連接配置
        this.database = {...};
        //自定義變量
        this.BaseUrl = "/"
        //配置日志信息
        this.log4js = { ... }
    }
}
module.exports = Config;

//development.js
var Config = require('./config');
class Development extends Config{
    constructor(){
        super();
        this.debug = true;
    }
}
module.exports = Development;

啟動(dòng)文件run.js

啟動(dòng)文件就比較簡(jiǎn)單了绵疲,很多都是框架做了,所以啟動(dòng)文件只需要引入框架切省,啟動(dòng)服務(wù)就可以:

//run.js 啟動(dòng)文件
var em = require('express-moduledev');
var config = {
    //指定端口最岗,默認(rèn)端口 8000
    "port":801,
    //使用環(huán)境 'default','development','production','testing'
    "use_env": "development",
}
//啟動(dòng)服務(wù)
em.Run(config)

至此,給用戶呈現(xiàn)一個(gè)怎么樣的框架已經(jīng)都明確了朝捆,只要框架去實(shí)現(xiàn)這些功能就好了般渡。

框架結(jié)構(gòu)

根據(jù)上面的這些需求定義,我們知道框架要具備哪些的核心功能芙盘,大概可以畫出一個(gè)腦圖驯用,如下圖:


framework.png
  • 主文件index.js:
    這個(gè)文件包括web服務(wù)的啟動(dòng),配置文件的載入及核心模塊的初始化等儒老,跟平常我們開發(fā)Express應(yīng)用時(shí)的主入口相似
  • 路由模塊 router.js:
    用于加載和處理路由蝴乔,自動(dòng)加載與相關(guān)的控制器文件,實(shí)現(xiàn)過(guò)濾器和中間件方面的處理
  • 基礎(chǔ)文件base.js:
    這個(gè)文件提供控制器的加載管理驮樊,并導(dǎo)出一個(gè)全局對(duì)象APP薇正,方便在項(xiàng)目中使用框架提供的各項(xiàng)功能接口
    加載橋接文件
  • 數(shù)據(jù)庫(kù) db.js
    處理數(shù)據(jù)庫(kù)相關(guān)的事情,對(duì)用戶定義的模型進(jìn)行解析囚衔,對(duì)底層ORM進(jìn)行封裝挖腰,并提供了最基礎(chǔ)的方法
  • 日志模塊 log.js
    為開發(fā)人員提供一個(gè)日志API,處理日志相關(guān)的事情
  • 通用單元 utils.js
    這個(gè)文件主要是給框架內(nèi)部使用练湿,包含一些通用判斷函數(shù)等

框架源碼

附上框架源碼猴仑,歡迎瀏覽改進(jìn):)
https://github.com/rob668/express-moduledev

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肥哎,隨后出現(xiàn)的幾起案子辽俗,更是在濱河造成了極大的恐慌,老刑警劉巖篡诽,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崖飘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡杈女,警方通過(guò)查閱死者的電腦和手機(jī)坐漏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)碧信,“玉大人,你說(shuō)我怎么就攤上這事街夭∨椴辏” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵板丽,是天一觀的道長(zhǎng)呈枉。 經(jīng)常有香客問(wèn)我趁尼,道長(zhǎng),這世上最難降的妖魔是什么猖辫? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任酥泞,我火速辦了婚禮,結(jié)果婚禮上啃憎,老公的妹妹穿的比我還像新娘芝囤。我一直安慰自己,他們只是感情好辛萍,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布悯姊。 她就那樣靜靜地躺著,像睡著了一般贩毕。 火紅的嫁衣襯著肌膚如雪悯许。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天辉阶,我揣著相機(jī)與錄音先壕,去河邊找鬼。 笑死谆甜,一個(gè)胖子當(dāng)著我的面吹牛垃僚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播店印,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼冈在,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了按摘?” 一聲冷哼從身側(cè)響起包券,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炫贤,沒想到半個(gè)月后溅固,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兰珍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年侍郭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掠河。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡亮元,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唠摹,到底是詐尸還是另有隱情爆捞,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布勾拉,位于F島的核電站煮甥,受9級(jí)特大地震影響盗温,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜成肘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一卖局、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧双霍,春花似錦砚偶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至顷蟀,卻和暖如春酒请,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鸣个。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工羞反, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人囤萤。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓昼窗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親涛舍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子澄惊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容