Babel

Babel 是 JavaScript 的轉(zhuǎn)譯器。用于將 ES Next 的代碼轉(zhuǎn)換成瀏覽器或者其他環(huán)境支持的代碼人断。注意:不是轉(zhuǎn)化為 ES5 逾苫,因?yàn)椴煌愋鸵约安煌姹镜臑g覽器對(duì) ES Next 新特性的支持程度都不一樣愿伴,對(duì)于目標(biāo)環(huán)境已經(jīng)支持的部分,Babel 可以不轉(zhuǎn)化添瓷,所以 Babel 會(huì)依賴運(yùn)行環(huán)境的版本梅屉。

CLI

@babel/cli是的 babel 的 CLI 工具。讓我們可以使用babel命令編譯文件鳞贷,該命令存放在babel/cli/bin目錄下坯汤。

Babel 工作流程

babel 運(yùn)行分為三個(gè)階段:

  1. 解析 Parser :通過模塊 @babel/parser將源碼解析成 AST
  2. 轉(zhuǎn)換 Transfrom:通過一系列插件 轉(zhuǎn)換成新的 AST
  3. 生成 Generator:模塊 @babel/generator將轉(zhuǎn)換后的 AST 重新生成新的代碼
1.png

@babel/core

@babel/core 依賴于@babel/parse、 @babel/traverse搀愧、@babel/types惰聂、@babel/template 等 模塊, 通過 @babel/parse 解析器咱筛,基于ESTree 規(guī)范搓幌,將源碼轉(zhuǎn)換成 AST。再使用 @babel/traverse 模塊遍歷 AST迅箩,并在遍歷過程中調(diào)用 visitor 函數(shù)進(jìn)行 AST 的增刪改溉愁,該模塊提供了 path 的 api。然后轉(zhuǎn)換能力全部通過插件進(jìn)行饲趋。

@babel/core 模塊提供了以下幾個(gè)方法可進(jìn)行轉(zhuǎn)換:

  1. 字符串形式的 JavaScript 代碼可直接使用 babel.transform 來編譯:
babel.transform('code', options) // { code, map, ast }
  1. 如果是文件可使用異步 API:
babel.transformFile("file.js", options, (err, result:  { code, map, ast })  => {})
  1. 或使用同步 API:
babel.transformFileSync('file.js', options) // { code, map, ast }
  1. 直接從 Babel AST(抽象語法樹)進(jìn)行轉(zhuǎn)換
babel.transformFromAst(ast, code, options) // { code, map, ast }

源碼如下:

2.png
// source.js

const arr = [1, 2, 3, 4]
arr.forEach(v => v * 2)
// index.js
const babel = require('@babel/core')
const fs = require('fs')
const code = fs.readFileSync('./source.js').toString()

// @babel/core 內(nèi)部引用了 parser拐揭、generator等模塊
const res = babel.transform(code, {
  // 是否生成ast
  ast: true,
  // 是否生成解析的代碼
  code: true,
  // 是否生成 sourcemap
  sourceMaps: true
  // 用到的插件
  // plugins: ['@babel/plugin-transform-arrow-functions']
})

console.log(res)

可以看出轉(zhuǎn)換后的結(jié)果包含源碼的 ast、轉(zhuǎn)換后的 code 以及 map 等奕塑。

從 code 可以看出沒有進(jìn)行轉(zhuǎn)換堂污,是因?yàn)?core 模塊本身不提供轉(zhuǎn)換能力,需要使用插件進(jìn)行轉(zhuǎn)換龄砰。

3.png

對(duì)箭頭函數(shù)進(jìn)行轉(zhuǎn)換:

// index.js

// 設(shè)置轉(zhuǎn)換箭頭函數(shù)的插件

const res = babel.transform(code, {

  plugins: ['@babel/plugin-transform-arrow-functions']

})
4.png

以上看出盟猖,箭頭函數(shù)轉(zhuǎn)換成了普通函數(shù) ,但是 const 沒有轉(zhuǎn)換成 var寝贡,是因?yàn)橹皇褂昧宿D(zhuǎn)換箭頭函數(shù)的插件扒披。

traverse

@babel/traverse 是用來自動(dòng)遍歷AST的工具,它會(huì)訪問樹中的所有節(jié)點(diǎn)圃泡,在進(jìn)入每個(gè)節(jié)點(diǎn)時(shí)觸發(fā) enter 鉤子函數(shù)碟案,退出每個(gè)節(jié)點(diǎn)時(shí)觸發(fā) exit 鉤子函數(shù)。開發(fā)者可在鉤子函數(shù)中對(duì) AST 進(jìn)行修改颇蜡。

import traverse from '@babel/traverse'

traverse(ast, {
  enter(path) {
    // 進(jìn)入 path 后觸發(fā)
  },
  exit(path) {
    // 退出 path 前觸發(fā)
  }
})

types

@babel/types 是作用于 AST 的類 lodash 庫价说,其封裝了大量與 AST 有關(guān)的方法,大大降低了轉(zhuǎn)換 AST 的成本风秤。@babel/types 的功能主要有兩種:一方面可以用它驗(yàn)證 AST 節(jié)點(diǎn)的類型鳖目,例如使用 isClassMethod 或 assertClassMethod 方法可以判斷 AST 節(jié)點(diǎn)是否為 class 中的一個(gè) method;另一方面可以用它構(gòu)建 AST 節(jié)點(diǎn)缤弦,例如調(diào)用 classMethod 方法领迈,可生成一個(gè)新的 classMethod 類型 AST 節(jié)點(diǎn) 。

template

@babel/template 實(shí)現(xiàn)了計(jì)算機(jī)科學(xué)中一種被稱為準(zhǔn)引用(quasiquotes)的概念。說白了狸捅,它能直接將字符串代碼片段(可在字符串代碼中嵌入變量)轉(zhuǎn)換為 AST 節(jié)點(diǎn)衷蜓。例如下面的例子中,@babel/template 可以將一段引入 axios 的聲明直接轉(zhuǎn)變?yōu)?AST 節(jié)點(diǎn)尘喝。

import template from '@babel/template'

const ast = template.ast(`
  import vue from 'vue'
`)

Presets 和 Plugins

Presets

預(yù)設(shè)是一系列插件的集合磁浇。

比如:

preset 的順序

preset 是逆序排列的(從后往前)。主要是為了確保向后兼容朽褪。這里涉及到配置的繼承和重寫概念置吓。

繼承和重寫是面向?qū)ο缶幊陶Z言中的概念,是指一個(gè)類擴(kuò)展自父類缔赠,并且重新實(shí)現(xiàn)了一些屬性和方法衍锚。這種思想不止在編程語言中用到,在配置文件中也廣泛使用橡淑。比如 eslint 中 extends的配置构拳。

除了整體重寫以外,還支持文件級(jí)別和環(huán)境級(jí)別的重寫:

文件級(jí)別:
overrides: [
  {
    test: 'src/test.js',
    plugins: ['pluginA']
  }
]
環(huán)境級(jí)別:
{

    envName: 'development',
    env: {
        development: {
            plugins: ['pluginA']
        },
        production: {
            plugins: ['pluginB']
        }
    }
}

preset 的參數(shù)

preset 可以接受參數(shù)梁棠,參數(shù)由插件名和參數(shù)對(duì)象組成一個(gè)數(shù)組置森。

@babel/preset-env

最常用的是@babel/preset-env,是一個(gè)智能預(yù)設(shè)符糊。其轉(zhuǎn)譯是按需的凫海,對(duì)于環(huán)境支持的語法可以不做轉(zhuǎn)換的。開發(fā)者只需使用最新的 JavaScript男娄,而無需微觀管理目標(biāo)環(huán)境所需的語法轉(zhuǎn)換(以及可選的瀏覽器 polyfill)行贪。

通過配置 targets 屬性指定目標(biāo)瀏覽器,讓 Babel 只轉(zhuǎn)譯環(huán)境不支持的語法模闲。如果沒有配置會(huì)默認(rèn)轉(zhuǎn)譯所有 ES Next 的語法建瘫。

// 增加 presets配置
presets: [
   '@babel/preset-env'
]

默認(rèn)轉(zhuǎn)成 ES5:

5.png

然后給targets指定 chrome: '67'(chrome67 版本是支持 const 和箭頭函數(shù)的)

  presets: [
    [
      '@babel/preset-env'
        {
          targets: {
            chrome: '67'
          }
        }
    ]
  ]

const 和箭頭函數(shù)并沒有進(jìn)行轉(zhuǎn)換。

6.png

創(chuàng)建 preset

導(dǎo)出一個(gè)函數(shù)尸折,函數(shù)的返回值為配置對(duì)象啰脚。

preset 可以是插件組成的數(shù)組:

module.exports = (api, opts) => ({
  plugins: ['pluginA', 'pluginB', 'pluginC']
})

也可以包含其他的 preset 和帶參數(shù)的插件:

module.exports = (api, opts) => ({
  presets: [require('@babel/preset-env')],
  plugins: [[require('pluginA'), { loose: true }]]
})

比如 Vue-CLI 的 preset 就依賴于@babel/preset-env:

vue-cli 的 babel presets:

7.png

@vue/cli-plugin-babel/preset源碼:

8.png

Plugins

插件分為語法插件和轉(zhuǎn)換插件。

語法插件

作用于解析階段实夹。

大多數(shù)語法都可以被 babel 轉(zhuǎn)換橄浓,極少數(shù)情況下需要使用插件解析特定類型的語法。官方的語法插件以

babel-plugin-syntax開頭亮航。

語法插件雖名為插件荸实,但事實(shí)上其本身并不具有功能性。所對(duì)應(yīng)的語法功能其實(shí)都已在@babel/parser里實(shí)現(xiàn)缴淋,插件的作用只是將對(duì)應(yīng)語法的解析功能打開准给。

轉(zhuǎn)換插件

作用于轉(zhuǎn)換階段泄朴,用于轉(zhuǎn)換代碼。

官方的轉(zhuǎn)換插件以babel-plugin-transform(正式)或 babel-plugin-proposal(提案)開頭圆存。

轉(zhuǎn)換插件將啟用相應(yīng)的語法插件叼旋,因此你不必同時(shí)指定這兩種插件。

為什么在配置文件中有了預(yù)設(shè)還需要插件沦辙?

  1. 有些最新的語法可能還處于提案階段,沒有加入預(yù)設(shè)集合
  2. 根據(jù)項(xiàng)目需求讹剔,自己編寫的插件

執(zhí)行順序

plugin 和 preset 都是通過數(shù)組的形式在配置文件中配置油讯,生效順序是先 plugin 后 preset,plugin 從左到右延欠,preset 從右到左陌兑,這樣的生效順序使得配置里的插件可以覆蓋 preset 里面插件的配置的,也就是重寫由捎。

core-js

Babel 把 ES Next 標(biāo)準(zhǔn)分為 syntaxbuilt-in 兩種類型兔综。syntax 是語法,比如:const狞玛、 () => 等软驰。babel 默認(rèn)只轉(zhuǎn)換 syntax 類型。而 Promise心肪、Set锭亏、Map 等環(huán)境所內(nèi)置的 API 、靜態(tài)方法 Array.from硬鞍、Object.assign慧瘤、實(shí)例方法Array.prototype.includes等屬于 built-in。而 built-in 類型需要通過 polyfill 來完成轉(zhuǎn)譯固该。

Babel 在 7.4.0 版本中廢棄 @babel/polyfill 锅减,改用 core-js 替代。

配置 useBuiltIns

@babel/preset-env中通過 useBuiltIns 參數(shù)來控制 built-in 的注入伐坏≌唬可選值為 'entry'、'usage'著淆、false 劫狠。默認(rèn)值為 false,不注入 polyfill永部。

  1. false :默認(rèn)值独泞,不注入 polyfill。
  2. 'entry':需在整個(gè)項(xiàng)目的入口處手動(dòng)引入 core-js 苔埋,import 'core-js'懦砂。該方式編譯后 Babel 會(huì)把目標(biāo)環(huán)境不支持的所有 built-in 都注入進(jìn)來,不管這些 API 是否使用到,極大的增加打包后包的體積荞膘。
  3. 'usage':不需在項(xiàng)目的入口處手動(dòng)引入罚随,Babel 會(huì)在編譯源碼的過程中根據(jù) built-in 的使用情況來選擇注入相應(yīng)的實(shí)現(xiàn)。如下圖:
9.png

配置 corejs 版本

當(dāng) useBuiltIns 設(shè)置為 'usage' 或者 'entry' 時(shí)羽资,還需要設(shè)置 @babel/preset-env 的 corejs 參數(shù)淘菩,用來指定注入 built-in 的實(shí)現(xiàn)時(shí),使用 corejs 的版本屠升。否則 Babel 日志輸出會(huì)有一個(gè)警告潮改。

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          edge: '17',
          firefox: '60',
          chrome: '67',
          safari: '11.1'
        },
        useBuiltIns: 'usage',
        corejs: '3.6.5',
        // 開啟調(diào)試模式,編譯日志會(huì)打印在控制臺(tái)
        debug: true
      }
    ]
  ]
}

@babel/runtime

以 class 為例:

// source.js

class Person {
  name = 'jack'

  age = 18

  static skill = 'eat'
}

編譯結(jié)果:

// dist.js

'use strict'

require('core-js/modules/es.object.define-property.js')

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i]

    descriptor.enumerable = descriptor.enumerable || false

    descriptor.configurable = true

    if ('value' in descriptor) descriptor.writable = true

    Object.defineProperty(target, descriptor.key, descriptor)
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps)

  if (staticProps) _defineProperties(Constructor, staticProps)

  Object.defineProperty(Constructor, 'prototype', { writable: false })

  return Constructor
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,

      enumerable: true,

      configurable: true,

      writable: true
    })
  } else {
    obj[key] = value
  }

  return obj
}

var Person = /*#__PURE__*/ _createClass(function Person() {
  _classCallCheck(this, Person)

  _defineProperty(this, 'name', 'jack')

  _defineProperty(this, 'age', 18)
})

_defineProperty(Person, 'skill', 'eat')

可以看到腹暖,編譯后 built-in 類型在 文件中通過 require 引入了 polyfill汇在,而 syntax 類型的語法則在文件中定義了_defineProperties、_createClass脏答、_classCallCheck糕殉、_defineProperty四個(gè) helper 函數(shù),用于創(chuàng)建構(gòu)造函數(shù)殖告、設(shè)置屬性阿蝶、創(chuàng)建實(shí)例。這只是 class 語法的輔助函數(shù)丛肮,其他 syntax 類型的語法也會(huì)生成 helper 函數(shù)赡磅,比如:extends 關(guān)鍵字。然而真實(shí)的項(xiàng)目開發(fā)中宝与,文件動(dòng)輒幾十焚廊、上百甚至上千,這樣打包后的文件會(huì)非常大习劫。

@babel/runtime 模塊提供了 helper 函數(shù)的集合咆瘟。在編譯后的文件中不是再創(chuàng)建 helper 函數(shù),而是引用@babel/runtime 提供的 helper 函數(shù)诽里。使用該模塊還需要用到 @babel/plugin-transform-runtime插件袒餐。

yarn add @babel/plugin-transform-runtime -D

yarn add @babel/runtime

@babel/runtime 是一個(gè)運(yùn)行時(shí)依賴,所以不需要加-D 參數(shù)谤狡。

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'usage',
        corejs: '3.6.5',
        debug: true
      }
    ]
  ],
  plugins: ['@babel/plugin-transform-runtime']
}

再次編譯:

'use strict'

var _interopRequireDefault = require('@babel/runtime/helpers/interopRequireDefault')

var _createClass2 = _interopRequireDefault(
  require('@babel/runtime/helpers/createClass')
)

var _classCallCheck2 = _interopRequireDefault(
  require('@babel/runtime/helpers/classCallCheck')
)

var _defineProperty2 = _interopRequireDefault(
  require('@babel/runtime/helpers/defineProperty')
)

var Person = /*#__PURE__*/ (0, _createClass2['default'])(function Person() {
  ;(0, _classCallCheck2['default'])(this, Person)
  ;(0, _defineProperty2['default'])(this, 'name', 'jack')
  ;(0, _defineProperty2['default'])(this, 'age', 18)
})

;(0, _defineProperty2['default'])(Person, 'skill', 'eat')

可以看到 helper 函數(shù)都是引用@babel/runtime 模塊提供的 helper 函數(shù)了灸眼。

@babel/plugin-transform-runtime

該插件除了可以在文件中引用 helper 函數(shù)外,還為編譯后的代碼創(chuàng)建了一個(gè)沙箱環(huán)境墓懂,避免污染全局作用域焰宣。

比如通過引入 polyfill 的方式實(shí)現(xiàn)數(shù)組的 includes方法,編譯后的結(jié)果是:

'use strict'

require('core-js/modules/es.array.includes.js')

var arr = [1, 2, 3]

arr.includes(3)

再看 core-js 中實(shí)現(xiàn) includes 方法的源碼可以看出直接往 Array.prototype 上增加 includes 方法捕仔,如下:


1.png

直接修改原型的會(huì)造成全局污染匕积。假如開發(fā)的是工具庫盈罐,被其它項(xiàng)目引用,而恰好該項(xiàng)目自身實(shí)現(xiàn)了 Array.prototype.includes 方法闪唆,那這樣就造成了沖突盅粪。

去掉 presets 的corejsuseBuiltIns屬性,同時(shí)也就不再需要 @babel/runtime 和 core-js 模塊了悄蕾。

module.exports = {
  presets: [
    [
      '@babel/preset-env'
    ]
  ],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        corejs: 3
      }
    ]
  ]
}

給@babel/plugin-transform-runtime 插件設(shè)置 corejs 配置票顾,包含三個(gè)值:

  • false 默認(rèn)

  • 2:安裝 @babel/runtime-corejs2

  • 3:安裝 @babel/runtime-corejs3

根據(jù) corejs 得安裝對(duì)應(yīng)的模塊,編譯后的結(jié)果如下:

'use strict'

var _interopRequireDefault = require('@babel/runtime-corejs3/helpers/interopRequireDefault')

var _includes = _interopRequireDefault(
  require('@babel/runtime-corejs3/core-js-stable/instance/includes')
)

var arr = [1, 2, 3]

;(0, _includes['default'])(arr).call(arr, 3)

看出 _includes 僅僅是一個(gè)局部變量帆调,不會(huì)對(duì) Array.prototype 造成污染库物。

如何驗(yàn)證?

給 presets 配置 corejs 的方式贷帮,將編譯后的文件在不支持 Array.prototype.includes 的瀏覽器中運(yùn)行,在瀏覽器控制臺(tái)或者引用該文件之后就可以使用[].includes诱告,而給@babel/plugin-transform-runtime 插件配置 corejs 的方式撵枢,編譯后的文件同樣的瀏覽器運(yùn)行時(shí)會(huì)報(bào)錯(cuò),不存在此方法精居。

參考

Babel 文檔
Babel 用戶手冊(cè)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末锄禽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子靴姿,更是在濱河造成了極大的恐慌沃但,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佛吓,死亡現(xiàn)場(chǎng)離奇詭異宵晚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)维雇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門淤刃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吱型,你說我怎么就攤上這事逸贾。” “怎么了津滞?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵铝侵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我触徐,道長(zhǎng)咪鲜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任锌介,我火速辦了婚禮嗜诀,結(jié)果婚禮上猾警,老公的妹妹穿的比我還像新娘。我一直安慰自己隆敢,他們只是感情好发皿,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拂蝎,像睡著了一般穴墅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上温自,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天玄货,我揣著相機(jī)與錄音,去河邊找鬼悼泌。 笑死松捉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的馆里。 我是一名探鬼主播隘世,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼鸠踪!你這毒婦竟也來了丙者?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤营密,失蹤者是張志新(化名)和其女友劉穎械媒,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體评汰,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纷捞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了键俱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兰绣。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖编振,靈堂內(nèi)的尸體忽然破棺而出缀辩,到底是詐尸還是另有隱情,我是刑警寧澤踪央,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布臀玄,位于F島的核電站,受9級(jí)特大地震影響畅蹂,放射性物質(zhì)發(fā)生泄漏健无。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一液斜、第九天 我趴在偏房一處隱蔽的房頂上張望累贤。 院中可真熱鬧叠穆,春花似錦、人聲如沸臼膏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渗磅。三九已至嚷硫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間始鱼,已是汗流浹背仔掸。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留医清,地道東北人起暮。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像会烙,于是被迫代替她去往敵國(guó)和親鞋怀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

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