Browserify + watchify

參考
http://browserify.org/
前端模塊及依賴管理的新選擇:Browserify

NodeJS 把 JavaScript 的使用從瀏覽器端擴展到了服務(wù)器端蜜宪,使得前端開發(fā)人員可以用熟悉的語言編寫服務(wù)器端代碼寄疏。這一變化使得 NodeJS 很快就流行起來同蜻。在 NodeJS 社區(qū)中有非常多的高質(zhì)量模塊可以直接使用。根據(jù)最新的統(tǒng)計結(jié)果懂讯,NodeJS 的 npm 中的模塊數(shù)量已經(jīng)超過了 Java 的 Maven Central 和 Ruby 的 RubyGems澈缺,成為模塊數(shù)量最多的社區(qū)。不過這些 NodeJS 模塊并不能直接在瀏覽器端應(yīng)用中使用,原因在于引用這些模塊時需要使用 NodeJS 中的 require 方法兜蠕,而該方法在瀏覽器端并不存在扰肌。Browserify 作為 NodeJS 模塊與瀏覽器端應(yīng)用之間的橋梁,讓應(yīng)用可以直接使用 NodeJS 中的模塊熊杨,并可以把應(yīng)用所依賴的模塊打包成單個 JavaScript 文件曙旭。通過 Browserify 還可以在應(yīng)用開發(fā)中使用與 NodeJS 相同的方式來進行模塊化和管理模塊依賴盗舰。如果應(yīng)用的后臺是基于 NodeJS 的,那么 Browserify 使得應(yīng)用的前后端可以使用一致的模塊管理方式桂躏。即便應(yīng)用的后端不使用 NodeJS钻趋,Browserify 也可以幫助進行前端代碼的復(fù)用和組織。

一剂习、示例

1.npm install -g browserify

//name.js:
module.exports = "aya";

//main.js:
var name = require("./name");

console.log("Hello! " + name);

使用browserify編譯:

browserify main.js -o bundle.js

現(xiàn)在可以在瀏覽器里直接使用bundle.js了蛮位,與在命令行里使用node main.js結(jié)果一致。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>browserify</title>
    <script src="bundle.js"></script>
</head>
<body>
</body>
</html>
二鳞绕、結(jié)構(gòu)

上面的例子很神奇失仁,看一下bundle.js里到底是什么

(function e(t, n, r) {
    // ...
})({
    1: [function (require, module, exports) {
        var name = require("./name");

        console.log("Hello! " + name);
    }, {"./name": 2}],
    2: [function (require, module, exports) {
        module.exports = "aya";
    }, {}]
}, {}, [1])

請先忽略掉省略號里的部分。然后们何,它的結(jié)構(gòu)就清晰多了萄焦。可以看到冤竹,整體是一個立即執(zhí)行的函數(shù)([IIFE][])拂封,該函數(shù)接收了3個參數(shù)。其中第1個參數(shù)比較復(fù)雜鹦蠕,第2冒签、3個參數(shù)在這里分別是{}和[1]。

1.模塊map
第1個參數(shù)是一個Object片部,它的每一個key都是數(shù)字镣衡,作為模塊的id,每一個數(shù)字key對應(yīng)的值是長度為2的數(shù)組档悠±扰福可以看出,前面的main.js中的代碼辖所,被function(require, module, exports){}這樣的結(jié)構(gòu)包裝了起來惰说,然后作為了key1數(shù)組里的第一個元素。類似的缘回,name.js中的代碼吆视,也被包裝,對應(yīng)到key2酥宴。

數(shù)組的第2個元素啦吧,是另一個map對應(yīng),它表示的是模塊的依賴拙寡。main.js在key1授滓,它依賴name.js,所以它的數(shù)組的第二個元素是{"./name": 2}。而在key2的name.js般堆,它沒有依賴在孝,因此其數(shù)組第二個元素是空Object{}。

因此淮摔,這第1個復(fù)雜的參數(shù)私沮,攜帶了所有模塊的源碼及其依賴關(guān)系,所以叫做模塊map和橙。

2.包裝
前面提到仔燕,原有的文件中的代碼,被包裝了起來胃碾。為什么要這樣包裝呢涨享?

因為,瀏覽器原生環(huán)境中仆百,并沒有require()厕隧。所以,需要用代碼去實現(xiàn)它(RequireJS和Sea.js也做了這件事)俄周。這個包裝函數(shù)提供的3個參數(shù)吁讨,require、module峦朗、exports建丧,正是由Browserify實現(xiàn)了特定功能的3個關(guān)鍵字。

3.緩存
第2個參數(shù)幾乎總是空的{}波势。它如果有的話翎朱,也是一個模塊map,表示本次編譯之前被加載進來的來自于其他地方的內(nèi)容〕呦常現(xiàn)階段拴曲,讓我們忽略它吧。

4.入口模塊
第3個參數(shù)是一個數(shù)組凛忿,指定的是作為入口的模塊id澈灼。前面的例子中,main.js是入口模塊店溢,它的id是1叁熔,所以這里的數(shù)組就是[1]。數(shù)組說明其實還可以有多個入口床牧,比如運行多個測試用例的場景荣回,但相對來說,多入口的情況還是比較少的戈咳。

5.實現(xiàn)功能

(function() {
    function r(e, n, t) {
        function o(i, f) {
            if (!n[i]) {
                if (!e[i]) {
                    var c = "function" == typeof require && require;
                    if (!f && c)
                        return c(i, !0);
                    if (u)
                        return u(i, !0);
                    var a = new Error("Cannot find module '" + i + "'");
                    throw a.code = "MODULE_NOT_FOUND",
                    a
                }
                var p = n[i] = {
                    exports: {}
                };
                e[i][0].call(p.exports, function(r) {
                    var n = e[i][1][r];
                    return o(n || r)
                }, p, p.exports, r, e, n, t)
            }
            return n[i].exports
        }
        for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)
            o(t[i]);
        return o
    }
    return r
}
)()

還記得前面忽略掉的省略號里的代碼嗎心软?這部分代碼將解析前面所說的3個參數(shù)革砸,然后讓一切運行起來。這段代碼是一個函數(shù)糯累,來自于browser-pack項目prelude.js。令人意外的是册踩,它并不復(fù)雜泳姐,而且寫有豐富的注釋,很推薦你自行閱讀暂吉。

// modules are defined as an array
// [ module function, map of requireuires ]
//
// map of requireuires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the requireuire for previous bundles

(function() {

function outer(modules, cache, entry) {
    // Save the require from previous bundle to this closure if any
    var previousRequire = typeof require == "function" && require;

    function newRequire(name, jumped){
        if(!cache[name]) {
            if(!modules[name]) {
                // if we cannot find the module within our internal map or
                // cache jump to the current global require ie. the last bundle
                // that was added to the page.
                var currentRequire = typeof require == "function" && require;
                if (!jumped && currentRequire) return currentRequire(name, true);

                // If there are other bundles on this page the require from the
                // previous one is saved to 'previousRequire'. Repeat this as
                // many times as there are bundles until the module is found or
                // we exhaust the require chain.
                if (previousRequire) return previousRequire(name, true);
                var err = new Error('Cannot find module \'' + name + '\'');
                err.code = 'MODULE_NOT_FOUND';
                throw err;
            }
            var m = cache[name] = {exports:{}};
            modules[name][0].call(m.exports, function(x){
                var id = modules[name][1][x];
                return newRequire(id ? id : x);
            },m,m.exports,outer,modules,cache,entry);
        }
        return cache[name].exports;
    }
    for(var i=0;i<entry.length;i++) newRequire(entry[i]);

    // Override the current require with this new one
    return newRequire;
}

return outer;

})()

6.在瀏覽器加載 CommonJS 模塊的原理與實現(xiàn)中胖秒,介紹了browser-unpack

browserify main.js > compiled.js

browser-unpack < compiled.js

[
  {
    "id":1,
    "source":"module.exports = function(x) {\n  console.log(x);\n};",
    "deps":{}
  },
  {
    "id":2,
    "source":"var foo = require(\"./foo\");\nfoo(\"Hi\");",
    "deps":{"./foo":1},
    "entry":true
  }
]

7.與require.js沖突的問題
參考模塊(一) CommonJs,AMD, CMD, UMD主模塊會這樣寫:

  // main.js
  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });

這樣就會與browserify里面的require沖突,可以參見Using Browserify and RequireJS on the same page?
的解決辦法慕的,就是用browserify-derequire改改名字

This Browserify plugin applies derequire in order to rename all require() calls to dereq() calls in the bundle output.

參見flv.js的gulpfile.js

function doWatchify() {
    let customOpts = {
        entries: 'src/index.js',
        standalone: 'flvjs',
        debug: true,
        transform: ['babelify', 'browserify-versionify'],
        plugin: ['browserify-derequire']
    };

    let opts = Object.assign({}, watchify.args, customOpts);
    let b = watchify(browserify(opts));

    b.on('update', function () {
        return doBundle(b).on('end', browserSync.reload.bind(browserSync));
    });
    b.on('log', console.log.bind(console));

    return b;
}

8.browserify-versionify
Browserify transform to replace placeholder with package version.By default, it replaces VERSION with the version from package.json in your source code.

看一下flv.js有一部分代碼:

Object.defineProperty(flvjs, 'version', {
    enumerable: true,
    get: function () {
        // replaced by browserify-versionify transform
        return '__VERSION__';
    }
});

當我們使用gulp.js打包后阎肝,這段代碼就變成了

Object.defineProperty(flvjs, 'version', {
    enumerable: true,
    get: function get() {
        // replaced by browserify-versionify transform
        return '1.4.3';
    }
});

而這個1.4.3正是從package.json中讀取的

{
  "name": "flv.js",
  "version": "1.4.3",
  "description": "HTML5 FLV Player",
  "main": "./dist/flv.js",
...
三、結(jié)合gulp使用
var gulp = require("gulp");
var browserify = require("browserify");
var sourcemaps = require("gulp-sourcemaps");
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
 
gulp.task("browserify", function () {
var b = browserify({
  entries: "./main.js",
  debug: true
});
 
return b.bundle()
  .pipe(source("bundle.js"))
  .pipe(buffer())
  .pipe(sourcemaps.init({loadMaps: true}))
  .pipe(sourcemaps.write("."))
  .pipe(gulp.dest("./"));
});

安裝完上述腳本里的Gulp插件肮街,就可以使用gulp browserify任務(wù)來生成bundle.js了风题。

在上面的代碼中,entries指定打包入口文件嫉父,debug: true是告知Browserify在運行同時生成內(nèi)聯(lián)sourcemap用于調(diào)試沛硅。引入gulp-sourcemaps并設(shè)置loadMaps: true是為了讀取上一步得到的內(nèi)聯(lián)sourcemap,并將其轉(zhuǎn)寫為一個單獨的sourcemap文件绕辖。vinyl-source-stream用于將Browserify的bundle()的輸出轉(zhuǎn)換為Gulp可用的[vinyl][](一種虛擬文件格式)流摇肌。vinyl-buffer用于將vinyl流轉(zhuǎn)化為buffered vinyl文件(gulp-sourcemaps及大部分Gulp插件都需要這種格式)。

關(guān)于gulp-sourcemaps仪际,可以參考Gulp學(xué)習(xí)筆記

// 引入gulp
var gulp = require('gulp');
// 引入gulp-concat插件
var concat = require('gulp-concat');
// 引入gulp-uglify插件
var uglify = require('gulp-uglify');
// 引入gulp-sourcemaps插件
var sourceMap = require('gulp-sourcemaps');

gulp.task('sourcemap',function() {
    gulp.src('./src/*.js')  
    .pipe( sourceMap.init() )
    .pipe( concat('all.js') )  
    .pipe( uglify() )  
    .pipe( sourceMap.write('../maps/',{addComment: false}) )
    .pipe( gulp.dest('./dist/') ) 
})
sourcemaps.init({
      loadMaps: true,  //是否加載以前的 .map 
      largeFile: true,   //是否以流的方式處理大文件
})

sourceMap.write( path )围小,將會在指定的 path,生成獨立的sourcemaps信息文件树碱。如果指定的是相對路徑肯适,是相對于 all.js 的路徑。無法指定路徑為 src 目錄赴恨,否則疹娶,sourcemaps文件會生成在 dist 目錄下。

addComment : true / false ; 是控制處理后的文件(本例是 all.js )伦连,尾部是否顯示關(guān)于sourcemaps信息的注釋雨饺。不加這個屬性,默認是true惑淳。設(shè)置為false的話额港,就是不顯示。

四歧焦、watchify

如果你的代碼比較多移斩,可能像上面這樣一次編譯需要1s以上肚医,這是比較慢的。這種時候向瓷,推薦使用[watchify][]肠套。它可以在你修改文件后,只重新編譯需要的部分(而不是Browserify原本的全部編譯)猖任,這樣你稚,只有第一次編譯會花些時間,此后的即時變更刷新則十分迅速朱躺。

1.原理
參考如何在Gulp中提高Browserify的打包速度
在gulp中我們可以把一個完整的任務(wù)拆分成很多個局部任務(wù)刁赖,然后使用gulp.watch對這些局部任務(wù)進行監(jiān)聽,例如:

gulp.task('build-js1', ...);
gulp.task('build-js2', ...);
gulp.task('build-all-js', ['build-js1', 'build-js2']);

gulp.task('watch-js1', function () {
  gulp.watch('./src/models/**/*.js', ['build-js1']);
});

gulp.task('watch-js2', function () {
  gulp.watch('./src/views/**/*.js', ['build-js2']);
});

//gulp.task('watch-js', function () {
//  gulp.watch('./src/**/*.js', ['build-all-js']);
//});

如上例所示长搀,在監(jiān)測不同局部位置的js文件發(fā)生改動后宇弛,則只會自動執(zhí)行相應(yīng)的build-js1或build-js2等局部任務(wù);而如果直接監(jiān)測所有的js文件源请,就必須每次執(zhí)行build-all-js任務(wù)了枪芒。

watchify的提速原理和這個思路有點類似,它可以監(jiān)測個別文件的改動巢钓,從而觸發(fā)只將需要更新的文件打包病苗。它須要先執(zhí)行一次完整的打包,首次打包的速度和正常速度是一樣的症汹;然后每次用戶改變某個和browserify關(guān)聯(lián)的js文件時硫朦,會自動執(zhí)行打包,而這次打包的速度卻非潮痴颍快咬展。

參考watchify和gulp.watch之間的區(qū)別

watchify understands commonjs modules (require(./foo.js) stuff) and will watch for changes for all dependencies. It can then recompile the bundle with the changes needed and only reload the changed files from disk. If you use gulp.watch and manually call browserify, it has to build up the dependency tree every time a change happens. This means a lot more disk i/o and hence it will be much slower.

2.改造我們上面的腳本
參考Gulp中文網(wǎng) 使用 watchify 加速 browserify 編譯

var gulp = require("gulp");
var browserify = require("browserify");
var sourcemaps = require("gulp-sourcemaps");
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');

var watchify = require('watchify');

// gulp.task("browserify", function () {
// var b = browserify({
  // entries: "./main.js",
  // debug: true
// });

// return b.bundle()
  // .pipe(source("bundle.js"))
  // .pipe(buffer())
  // .pipe(sourcemaps.init({loadMaps: true}))
  // .pipe(sourcemaps.write("."))
  // .pipe(gulp.dest("./"));
// });

// 在這里添加自定義 browserify 選項
var customOpts = {
  entries: './main.js',
  debug: true
};
var opts = Object.assign({}, watchify.args, customOpts);
var b = watchify(browserify(opts));

// 在這里加入變換操作
// 比如: b.transform(coffeeify);

// 這樣你就可以運行 `gulp build-all-js` 來編譯文件了
gulp.task('build-all-js', bundle);

function bundle() {
  return b.bundle()
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest('./'));
}

//啟動watchify監(jiān)測文件改動
gulp.task('watch-js', function() {
  b.on('update', function(ids) {  //監(jiān)測文件改動
    ids.forEach(function(v) {
      console.log('bundle changed file:' + v);  //記錄改動的文件名
    });

    gulp.start('build-all-js');  //觸發(fā)打包js任務(wù)
  });

  return bundle();  //須要先執(zhí)行一次bundle
});

這里為了測試效果,又添加了一個age.js文件:

//name.js
module.exports = "cuixu4";

//age.js
module.exports = "31";

//main.js
var name = require("./name");
var age = require("./age");

console.log("Hello! " + name + ",age:" + age);

使用gulp watch-js啟動監(jiān)視任務(wù)后瞒斩,無論改age.js還是name.js還是main.js都會觸發(fā)更新

PS E:\node\browserifyDemo> gulp watch-js
[17:17:20] Using gulpfile E:\node\browserifyDemo\gulpfile.js
[17:17:20] Starting 'watch-js'...
[17:17:20] Finished 'watch-js' after 61 ms
bundle changed file:E:\node\browserifyDemo\name.js
[17:17:33] Starting 'build-all-js'...
[17:17:33] Finished 'build-all-js' after 49 ms
bundle changed file:E:\node\browserifyDemo\main.js
[17:20:21] Starting 'build-all-js'...
[17:20:21] Finished 'build-all-js' after 76 ms
bundle changed file:E:\node\browserifyDemo\age.js
[17:20:45] Starting 'build-all-js'...
[17:20:45] Finished 'build-all-js' after 48
五破婆、其它細節(jié)

在本文的第三部分中,關(guān)于browserify的參數(shù)胸囱,只寫了兩個:

var b = browserify({
  entries: "./main.js",
  debug: true
});

其中祷舀,entries指定打包入口文件,debug: true是告知Browserify在運行同時生成內(nèi)聯(lián)sourcemap用于調(diào)試烹笔。還有其它一些屬性也需要使用裳扯,這里以flv.js的gulpfile.js為例

    let customOpts = {
        entries: 'src/index.js',
        standalone: 'flvjs',
        debug: true,
        transform: ['babelify', 'browserify-versionify'],
        plugin: ['browserify-derequire']
    };

1.standalone
在運行 browserify 命令時使用”--standalone”參數(shù)來指定模塊的名稱。所產(chǎn)生的模塊可以在 NodeJS 和瀏覽器中使用谤职。對于瀏覽器來說饰豺,如果應(yīng)用支持 AMD,則使用 AMD 來定義模塊允蜈;否則把模塊暴露為全局對象冤吨。如“browserify log.js --standalone log > log-bundle.js”把模塊 log.js 打包成名為 log 的獨立模塊蒿柳。

也就是把打包后的函數(shù)掛在window下指定的名字下,在本例中就是flvjs漩蟆,看一下DEMO

<script>
    if (flvjs.isSupported()) {
        var videoElement = document.getElementById('videoElement');
        var flvPlayer = flvjs.createPlayer({
            type: 'flv',
            //"isLive": true,
            url: 'http://192.168.198.102/jay.flv'
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load();
        flvPlayer.play();
    }
</script>

2.transform
Browserify使用了transform以及配合transform的相應(yīng)插件實現(xiàn)了引入模板垒探、樣式等等文本文件的功能。在解析require調(diào)用之前來轉(zhuǎn)換引入的源代碼怠李,通過這一層類似于中間件的功能叛复,使得browserify在拓展性上大有可為。

Babel 入門教程中扔仓,使用babelify模塊:

  "browserify": {
    "transform": [["babelify", { "presets": ["es2015"] }]]
  }
}

放置在配置文件.babelrc中

  {
    "presets": [
      "es2015",
      "react",
      "stage-2"
    ],
    "plugins": []
  }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市咖耘,隨后出現(xiàn)的幾起案子翘簇,更是在濱河造成了極大的恐慌,老刑警劉巖儿倒,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件版保,死亡現(xiàn)場離奇詭異,居然都是意外死亡夫否,警方通過查閱死者的電腦和手機彻犁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凰慈,“玉大人汞幢,你說我怎么就攤上這事∥⑽剑” “怎么了森篷?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長豺型。 經(jīng)常有香客問我仲智,道長,這世上最難降的妖魔是什么姻氨? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任钓辆,我火速辦了婚禮,結(jié)果婚禮上肴焊,老公的妹妹穿的比我還像新娘前联。我一直安慰自己,他們只是感情好抖韩,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布蛀恩。 她就那樣靜靜地躺著,像睡著了一般茂浮。 火紅的嫁衣襯著肌膚如雪双谆。 梳的紋絲不亂的頭發(fā)上壳咕,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機與錄音顽馋,去河邊找鬼谓厘。 笑死,一個胖子當著我的面吹牛寸谜,可吹牛的內(nèi)容都是我干的竟稳。 我是一名探鬼主播,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼熊痴,長吁一口氣:“原來是場噩夢啊……” “哼他爸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起果善,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤诊笤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后巾陕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讨跟,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年鄙煤,在試婚紗的時候發(fā)現(xiàn)自己被綠了晾匠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡梯刚,死狀恐怖凉馆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情亡资,我是刑警寧澤句喜,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站沟于,受9級特大地震影響咳胃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜旷太,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一展懈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧供璧,春花似錦存崖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至演顾,卻和暖如春供搀,著一層夾襖步出監(jiān)牢的瞬間隅居,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工葛虐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留胎源,地道東北人。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓屿脐,卻偏偏與公主長得像涕蚤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子的诵,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361

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