手摸手教你寫個(gè)ESLint 插件以及了解ESLint的運(yùn)行原理

image

這篇文章目的是介紹如何創(chuàng)建一個(gè)ESLint插件和創(chuàng)建一個(gè)ESLint rule智袭,用以幫助我們更深入的理解ESLint的運(yùn)行原理汉嗽,并且在有必要時(shí)可以根據(jù)需求創(chuàng)建出一個(gè)完美滿足自己需求的Lint規(guī)則塔沃。

插件目標(biāo)

禁止項(xiàng)目中setTimeout的第二個(gè)參數(shù)是數(shù)字絮供。

PS: 如果是數(shù)字的話罗丰,很容易就成為魔鬼數(shù)字硝岗,沒有人知道為什么是這個(gè)數(shù)字, 這個(gè)數(shù)字有什么含義织鲸。

使用模板初始化項(xiàng)目:

1. 安裝NPM包

ESLint官方為了方便開發(fā)者開發(fā)插件媚狰,提供了使用Yeoman模板(generator-eslint)。

對(duì)于Yeoman我們只需知道它是一個(gè)腳手架工具诱篷,用于生成包含指定框架結(jié)構(gòu)的工程化目錄結(jié)構(gòu)壶唤。

npm install -g yo generator-eslint

2. 創(chuàng)建一個(gè)文件夾:

mkdir eslint-plugin-demo
cd eslint-plugin-demo

3. 命令行初始化ESLint插件的項(xiàng)目結(jié)構(gòu):

yo eslint:plugin

下面進(jìn)入命令行交互流程,流程結(jié)束后生成ESLint插件項(xiàng)目框架和文件棕所。

? What is your name? OBKoro1
? What is the plugin ID? korolint   // 這個(gè)插件的ID是什么
? Type a short description of this plugin: XX公司的定制ESLint rule // 輸入這個(gè)插件的描述
? Does this plugin contain custom ESLint rules? Yes // 這個(gè)插件包含自定義ESLint規(guī)則嗎?
? Does this plugin contain one or more processors? No // 這個(gè)插件包含一個(gè)或多個(gè)處理器嗎
// 處理器用于處理js以外的文件 比如.vue文件
   create package.json
   create lib/index.js
   create README.md

現(xiàn)在可以看到在文件夾內(nèi)生成了一些文件夾和文件闸盔,但我們還需要?jiǎng)?chuàng)建規(guī)則具體細(xì)節(jié)的文件。

4. 創(chuàng)建規(guī)則

上一個(gè)命令行生成的是ESLint插件的項(xiàng)目模板琳省,這個(gè)命令行是生成ESLint插件具體規(guī)則的文件蕾殴。

yo eslint:rule // 生成 eslint rule的模板文件

創(chuàng)建規(guī)則命令行交互:

? What is your name? OBKoro1
? Where will this rule be published? (Use arrow keys) // 這個(gè)規(guī)則將在哪里發(fā)布?
? ESLint Core  // 官方核心規(guī)則 (目前有200多個(gè)規(guī)則)
  ESLint Plugin  // 選擇ESLint插件
? What is the rule ID? settimeout-no-number  // 規(guī)則的ID
? Type a short description of this rule: setTimeout 第二個(gè)參數(shù)禁止是數(shù)字  // 輸入該規(guī)則的描述
? Type a short example of the code that will fail:  占位  // 輸入一個(gè)失敗例子的代碼
   create docs/rules/settimeout-no-number.md
   create lib/rules/settimeout-no-number.js
   create tests/lib/rules/settimeout-no-number.js

加了具體規(guī)則文件的項(xiàng)目結(jié)構(gòu)

.
├── README.md
├── docs // 使用文檔
│   └── rules // 所有規(guī)則的文檔
│       └── settimeout-no-number.md // 具體規(guī)則文檔
├── lib // eslint 規(guī)則開發(fā)
│   ├── index.js 引入+導(dǎo)出rules文件夾的規(guī)則
│   └── rules // 此目錄下可以構(gòu)建多個(gè)規(guī)則
│       └── settimeout-no-number.js // 規(guī)則細(xì)節(jié)
├── package.json
└── tests // 單元測試
    └── lib
        └── rules
            └── settimeout-no-number.js // 測試該規(guī)則的文件

4. 安裝項(xiàng)目依賴

npm install

以上是開發(fā)ESLint插件具體規(guī)則的準(zhǔn)備工作岛啸,下面先來看看AST和ESLint原理的相關(guān)知識(shí),為我們開發(fā)ESLint rule 打一下基礎(chǔ)茴肥。

AST——抽象語法樹

AST是: Abstract Syntax Tree的簡稱坚踩,中文叫做:抽象語法樹。

AST的作用

將代碼抽象成樹狀數(shù)據(jù)結(jié)構(gòu)瓤狐,方便后續(xù)分析檢測代碼瞬铸。

代碼被解析成AST的樣子

astexplorer.net是一個(gè)工具網(wǎng)站:它能查看代碼被解析成AST的樣子。

如下圖:在右側(cè)選中一個(gè)值時(shí)础锐,左側(cè)對(duì)應(yīng)區(qū)域也變成高亮區(qū)域嗓节,這樣可以在AST中很方便的選中對(duì)應(yīng)的代碼

AST 選擇器:

下圖中被圈起來的部分皆警,稱為AST selectors(選擇器)拦宣。

AST 選擇器的作用:使用代碼通過選擇器來選中特定的代碼片段,然后再對(duì)代碼進(jìn)行靜態(tài)分析。

AST 選擇器很多鸵隧,ESLint官方專門有一個(gè)倉庫列出了所有類型的選擇器: estree

下文中開發(fā)ESLint rule就需要用到選擇器绸罗,等下用到了就懂了,現(xiàn)在知道一下就好了豆瘫。

將代碼解析成AST

ESLint的運(yùn)行原理

在開發(fā)規(guī)則之前珊蟀,我們需要ESLint是怎么運(yùn)行的,了解插件為什么需要這么寫外驱。

1. 將代碼解析成AST

ESLint使用JavaScript解析器Espree把JS代碼解析成AST育灸。

PS:解析器:是將代碼解析成AST的工具,ES6昵宇、react磅崭、vue都開發(fā)了對(duì)應(yīng)的解析器所以ESLint能檢測它們的,ESLint也是因此一統(tǒng)前端Lint工具的趟薄。

2. 深度遍歷AST绽诚,監(jiān)聽匹配過程。

在拿到AST之后杭煎,ESLint會(huì)以"從上至下"再"從下至上"的順序遍歷每個(gè)選擇器兩次恩够。

3. 觸發(fā)監(jiān)聽選擇器的rule回調(diào)

在深度遍歷的過程中,生效的每條規(guī)則都會(huì)對(duì)其中的某一個(gè)或多個(gè)選擇器進(jìn)行監(jiān)聽羡铲,每當(dāng)匹配到選擇器蜂桶,監(jiān)聽該選擇器的rule,都會(huì)觸發(fā)對(duì)應(yīng)的回調(diào)也切。

4. 具體的檢測規(guī)則等細(xì)節(jié)內(nèi)容扑媚。


開發(fā)規(guī)則

規(guī)則默認(rèn)模板

打開rule生成的模板文件lib/rules/settimeout-no-number.js, 清理一下文件,刪掉不必要的選項(xiàng):

module.exports = {
    meta: {
        docs: {
            description: "setTimeout 第二個(gè)參數(shù)禁止是數(shù)字",
        },
        fixable: null,  // 修復(fù)函數(shù)
    },
   // rule 核心
    create: function(context) {
       // 公共變量和函數(shù)應(yīng)該在此定義
        return {
            // 返回事件鉤子
        };
    }
};

刪掉的配置項(xiàng)雷恃,有些是ESLint官方核心規(guī)則才是用到的配置項(xiàng)疆股,有些是暫時(shí)不必了解的配置,需要用到的時(shí)候倒槐,可以自行查閱ESLint 文檔

create方法-監(jiān)聽選擇器

上文ESLint原理第三部中提到的:在深度遍歷的過程中旬痹,生效的每條規(guī)則都會(huì)對(duì)其中的某一個(gè)或多個(gè)選擇器進(jìn)行監(jiān)聽,每當(dāng)匹配到選擇器讨越,監(jiān)聽該選擇器的rule两残,都會(huì)觸發(fā)對(duì)應(yīng)的回調(diào)。

create返回一個(gè)對(duì)象把跨,對(duì)象的屬性設(shè)為選擇器人弓,ESLint會(huì)收集這些選擇器,在AST遍歷過程中會(huì)執(zhí)行所有監(jiān)聽該選擇器的回調(diào)着逐。

// rule 核心
create: function(context) {
    // 公共變量和函數(shù)應(yīng)該在此定義
    return {
        // 返回事件鉤子
        Identifier: (node) => {
            // node是選中的內(nèi)容崔赌,是我們監(jiān)聽的部分, 它的值參考AST
        }
    };
}

觀察AST:

創(chuàng)建一個(gè)ESLint rule需要觀察代碼解析成AST意蛀,選中你要檢測的代碼,然后進(jìn)行一些判斷峰鄙。

以下代碼都是通過astexplorer.net在線解析的浸间。

setTimeout(()=>{
    console.log('settimeout')
}, 1000)
setTimeout第二個(gè)參數(shù)為數(shù)字時(shí)的AST

rule完整文件

lib/rules/settimeout-no-number.js:

module.exports = {
    meta: {
        docs: {
            description: "setTimeout 第二個(gè)參數(shù)禁止是數(shù)字",
        },
        fixable: null,  // 修復(fù)函數(shù)
    },
    // rule 核心
    create: function (context) {
        // 公共變量和函數(shù)應(yīng)該在此定義
        return {
            // 返回事件鉤子
            'CallExpression': (node) => {
                if (node.callee.name !== 'setTimeout') return // 不是定時(shí)器即過濾
                const timeNode = node.arguments && node.arguments[1] // 獲取第二個(gè)參數(shù)
                if (!timeNode) return // 沒有第二個(gè)參數(shù)
                // 檢測報(bào)錯(cuò)第二個(gè)參數(shù)是數(shù)字 報(bào)錯(cuò)
                if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') {
                    context.report({
                        node,
                        message: 'setTimeout第二個(gè)參數(shù)禁止是數(shù)字'
                    })
                }
            }
        };
    }
};

context.report():這個(gè)方法是用來通知ESLint這段代碼是警告或錯(cuò)誤的,用法如上吟榴。在這里查看contextcontext.report()的文檔魁蒜。

規(guī)則寫完了,原理就是依據(jù)AST解析的結(jié)果吩翻,做針對(duì)性的檢測兜看,過濾出我們要選中的代碼,然后對(duì)代碼的值進(jìn)行邏輯判斷狭瞎。

可能現(xiàn)在會(huì)有點(diǎn)懵逼细移,但是不要緊,我們來寫一下測試用例熊锭,然后用debugger來看一下代碼是怎么運(yùn)行的弧轧。

測試用例:

測試文件tests/lib/rules/settimeout-no-number.js:

/**
 * @fileoverview setTimeout 第二個(gè)參數(shù)禁止是數(shù)字
 * @author OBKoro1
 */
"use strict";
var rule = require("../../../lib/rules/settimeout-no-number"), // 引入rule
    RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester({
    parserOptions: {
        ecmaVersion: 7, // 默認(rèn)支持語法為es5 
    },
});
// 運(yùn)行測試用例
ruleTester.run("settimeout-no-number", rule, {
    // 正確的測試用例
    valid: [
        {
            code: 'let someNumber = 1000; setTimeout(()=>{ console.log(11) },someNumber)'
        },
        {
            code: 'setTimeout(()=>{ console.log(11) },someNumber)'
        }
    ],
    // 錯(cuò)誤的測試用例
    invalid: [
        {
            code: 'setTimeout(()=>{ console.log(11) },1000)',
            errors: [{
                message: "setTimeout第二個(gè)參數(shù)禁止是數(shù)字", // 與rule拋出的錯(cuò)誤保持一致
                type: "CallExpression" // rule監(jiān)聽的對(duì)應(yīng)鉤子
            }]
        }
    ]
});

下面來學(xué)習(xí)一下怎么在VSCode中調(diào)試node文件,用于觀察rule是怎么運(yùn)行的碗殷。

實(shí)際上打console的形式精绎,也是可以的,但是在調(diào)試的時(shí)候打console實(shí)在是有點(diǎn)慢锌妻,對(duì)于node這種節(jié)點(diǎn)來說代乃,信息也不全,所以我還是比較推薦通過debugger的方式來調(diào)試rule仿粹。

在VSCode中調(diào)試node文件

  1. 點(diǎn)擊下圖中的設(shè)置按鈕, 將會(huì)打開一個(gè)文件launch.json
  2. 在文件中填入如下內(nèi)容搁吓,用于調(diào)試node文件。
  3. rule文件中打debugger或者在代碼行數(shù)那里點(diǎn)一下小紅點(diǎn)吭历。
  4. 點(diǎn)擊圖中的開始按鈕堕仔,進(jìn)入debugger
vscode 設(shè)置
{
    // 使用 IntelliSense 了解相關(guān)屬性。 
    // 懸停以查看現(xiàn)有屬性的描述晌区。
    // 欲了解更多信息摩骨,請(qǐng)?jiān)L問: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "啟動(dòng)程序", // 調(diào)試界面的名稱
            // 運(yùn)行項(xiàng)目下的這個(gè)文件:
            "program": "${workspaceFolder}/tests/lib/rules/settimeout-no-number.js",
            "args": [] // node 文件的參數(shù)
        },
        // 下面是用于調(diào)試package.json的命令 之前可以用,貌似vscode出了點(diǎn)bug導(dǎo)致現(xiàn)在用不了了
        {
            "name": "Launch via NPM",
            "type": "node",
            "request": "launch",
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "run-script", "dev"    //這里的dev就對(duì)應(yīng)package.json中的scripts中的dev
            ],
            "port": 9229    //這個(gè)端口是調(diào)試的端口契讲,不是項(xiàng)目啟動(dòng)的端口
        },
    ]
}

運(yùn)行測試用例進(jìn)入斷點(diǎn)

  1. lib/rules/settimeout-no-number.js中打一些debugger
  2. 點(diǎn)擊開始按鈕,以調(diào)試的形式運(yùn)行測試文件tests/lib/rules/settimeout-no-number.js
  3. 開始調(diào)試rule滑频。

發(fā)布插件

eslint插件都是以npm包的形式來引用的捡偏,所以需要把插件發(fā)布一下:

  1. 注冊(cè):如果你還未注冊(cè)npm賬號(hào)的話,需要去注冊(cè)一下峡迷。

  2. 登錄npm: npm login

  3. 發(fā)布npm包: npm publish即可银伟,ESLint已經(jīng)把package.json弄好了你虹。

集成到項(xiàng)目:

安裝npm包:npm i eslint-plugin-korolint -D

  1. 常規(guī)的方法: 引入插件一條條寫入規(guī)則
// .eslintrc.js
module.exports = {
  plugins: [ 'korolint' ],
  rules: { 
    "korolint/settimeout-no-number": "error"
 }
}
  1. extends繼承插件配置:

當(dāng)規(guī)則比較多的時(shí)候,用戶一條條去寫彤避,未免也太麻煩了傅物,所以ESLint可以繼承插件的配置

修改一下lib/rules/index.js文件:

'use strict';
var requireIndex = require('requireindex');
const output = {
  rules: requireIndex(__dirname + '/rules'), // 導(dǎo)出所有規(guī)則
  configs: {
    // 導(dǎo)出自定義規(guī)則 在項(xiàng)目中直接引用
    koroRule: {
      plugins: ['korolint'], // 引入插件
      rules: {
        // 開啟規(guī)則
        'korolint/settimeout-no-number': 'error'
      }
    }
  }
};
module.exports = output;

使用方法:

使用extends來繼承插件的配置,extends不止這種繼承方式琉预,即使你傳入一個(gè)npm包董饰,一個(gè)文件的相對(duì)路徑地址,eslint也能繼承其中的配置圆米。

// .eslintrc.js
module.exports = {
  extends: [ 'plugin:korolint/koroRule' ] // 繼承插件導(dǎo)出的配置
}

PS : 這種使用方式, npm的包名不能為eslint-plugin-xx-xx,只能為eslint-plugin-xx否則會(huì)有報(bào)錯(cuò)卒暂,被這個(gè)問題搞得頭疼o(╥﹏╥)o

擴(kuò)展:

以上內(nèi)容足夠開發(fā)一個(gè)插件,這里是一些擴(kuò)展知識(shí)點(diǎn)娄帖。

遍歷方向:

上文中說過: 在拿到AST之后也祠,ESLint會(huì)以"從上至下"再"從下至上"的順序遍歷每個(gè)選擇器兩次。

我們所監(jiān)聽的選擇器默認(rèn)會(huì)在"從上至下"的過程中觸發(fā)近速,如果需要在"從下至上"的過程中執(zhí)行則需要添加:exit诈嘿,在上文中CallExpression就變?yōu)?code>CallExpression:exit。

注意:一段代碼解析后可能包含多次同一個(gè)選擇器削葱,選擇器的鉤子也會(huì)多次觸發(fā)奖亚。

fix函數(shù):自動(dòng)修復(fù)rule錯(cuò)誤

修復(fù)效果

// 修復(fù)前
setTimeout(() => {

}, 1000)
// 修復(fù)后 變量名故意寫錯(cuò) 為了讓用戶去修改它
const countNumber1 = 1000
setTimeout(() => {

}, countNumber2)
  1. 在rule的meta對(duì)象上打開修復(fù)功能:
// rule文件
module.exports = {
  meta: {
    docs: {
      description: 'setTimeout 第二個(gè)參數(shù)禁止是數(shù)字'
    },
    fixable: 'code' // 打開修復(fù)功能
  }
}
  1. context.report()上提供一個(gè)fix函數(shù):

把上文的context.report修改一下,增加一個(gè)fix方法即可佩耳,更詳細(xì)的介紹可以看一下文檔遂蛀。

context.report({
    node,
    message: 'setTimeout第二個(gè)參數(shù)禁止是數(shù)字',
    fix(fixer) {
        const numberValue = timeNode.value;
        const statementString = `const countNumber1 = ${numberValue}\n`
        return [
        // 修改數(shù)字為變量 變量名故意寫錯(cuò) 為了讓用戶去修改它
        fixer.replaceTextRange(node.arguments[1].range, 'countNumber2'),
        // 在setTimeout之前增加一行聲明變量的代碼 用戶自行修改變量名
        fixer.insertTextBeforeRange(node.range, statementString)
        ];
    }
});

項(xiàng)目地址:

eslint-plugin-korolint


呼~ 這篇博客斷斷續(xù)續(xù),寫了好幾周干厚,終于完成了李滴!

大家有看到這篇博客的話,建議跟著博客的一起動(dòng)手寫一下蛮瞄,動(dòng)手實(shí)操一下比你mark一百篇文章都來的有用所坯,花不了很長時(shí)間的,希望各位看完本文挂捅,都能夠更深入的了解到ESLint的運(yùn)行原理芹助。

覺得我的博客對(duì)你有幫助的話,就關(guān)注一下/點(diǎn)個(gè)贊吧闲先!

前端進(jìn)階積累状土、公眾號(hào)GitHub伺糠、wx:OBkoro1蒙谓、郵箱:obkoro1@foxmail.com

基友帶我飛

ESLint插件是向基友yeyan1996學(xué)習(xí)的,在遇到問題的時(shí)候训桶,也是他指點(diǎn)我的累驮,特此感謝酣倾。

參考資料:

創(chuàng)建規(guī)則
ESLint 工作原理探討

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市谤专,隨后出現(xiàn)的幾起案子躁锡,更是在濱河造成了極大的恐慌,老刑警劉巖置侍,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件映之,死亡現(xiàn)場離奇詭異,居然都是意外死亡墅垮,警方通過查閱死者的電腦和手機(jī)惕医,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來算色,“玉大人抬伺,你說我怎么就攤上這事≡置危” “怎么了峡钓?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長若河。 經(jīng)常有香客問我能岩,道長,這世上最難降的妖魔是什么萧福? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任拉鹃,我火速辦了婚禮,結(jié)果婚禮上鲫忍,老公的妹妹穿的比我還像新娘膏燕。我一直安慰自己,他們只是感情好悟民,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布坝辫。 她就那樣靜靜地躺著,像睡著了一般射亏。 火紅的嫁衣襯著肌膚如雪近忙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天智润,我揣著相機(jī)與錄音及舍,去河邊找鬼。 笑死窟绷,一個(gè)胖子當(dāng)著我的面吹牛锯玛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播钾麸,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼更振,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了饭尝?” 一聲冷哼從身側(cè)響起肯腕,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钥平,沒想到半個(gè)月后实撒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涉瘾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年知态,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片立叛。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡负敏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秘蛇,到底是詐尸還是另有隱情其做,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布赁还,位于F島的核電站妖泄,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏艘策。R本人自食惡果不足惜蹈胡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望朋蔫。 院中可真熱鬧罚渐,春花似錦、人聲如沸斑举。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽富玷。三九已至璧坟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赎懦,已是汗流浹背雀鹃。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留励两,地道東北人黎茎。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像当悔,于是被迫代替她去往敵國和親傅瞻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子踢代,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345