uni-app黑魔法:小程序自定義組件運行到H5平臺

引言

移動互聯(lián)網(wǎng)的初期,囿于設(shè)備硬件性能限制粘茄,流量以原生App為主,iOS秕脓、Android是當時兩大平臺柒瓣。

隨著硬件及OS的更新?lián)Q代,H5可承載的體驗逐步完善吠架,為提高開發(fā)效率芙贫、節(jié)約資源(復(fù)用代碼)以及熱更新等目的,Hybrid模式成為主流傍药;以及輕應(yīng)用磺平、服務(wù)號等平臺的助推,H5網(wǎng)頁流量暴漲拐辽,成為第三大平臺拣挪。

2017年1月9日,微信發(fā)布小程序俱诸,歷經(jīng)3年發(fā)展菠劝,在今年主題為”未完成 Always Beta“的微信公開課 PRO上,微信團隊披露睁搭,2019年小程序日活躍用戶超過 3 億赶诊,全年累計成交額達8000億,同比增長超160%园骆。

看到小程序如此驚人的增長力舔痪,我們有理由相信,有中國特色的小程序互聯(lián)網(wǎng)時代已經(jīng)到來锌唾,微信小程序也已成為繼iOS锄码、Android、H5之后的第四大流量平臺晌涕。

平臺分裂滋捶,為不同平臺編寫相同的業(yè)務(wù)代碼,是件無趣的事情渐排。

有追求的程序員炬太,一直在探索代碼復(fù)用的方案,Hybrid App即是代表驯耻。

而在如今的小程序時代亲族,對于同樣基于WEB技術(shù)的H5和小程序炒考,如何實現(xiàn)代碼復(fù)用,是很多前端工程師探索的方向霎迫。業(yè)內(nèi)也已有不少成熟方案斋枢,從場景上來說,大致分為三類:

  1. 基于跨端框架知给,從頭開發(fā)瓤帚,一套代碼,發(fā)行多個平臺涩赢,比如DCloud出品的uni-app戈次、京東凹凸實驗室的taro
  2. 復(fù)用H5代碼,轉(zhuǎn)換H5代碼在小程序環(huán)境中執(zhí)行筒扒;適用于有H5平臺沉淀怯邪,未開發(fā)小程序或小程序完善度較低的開發(fā)者;
  • 美團的mpvue框架是早期探索解決這個問題的代表花墩,但因小程序不支持dom操作悬秉,故mpvue適用于vue的無dom操作的H5代碼轉(zhuǎn)換;

  • 最近微信官方推出的kbone冰蘑,也是為了解決“把 Web 端的代碼挪到小程序環(huán)境內(nèi)執(zhí)行”和泌;不過,kbone 相比 mpvue 前進了一步(當然也有了新的性能缺陷)祠肥,因為:

    kbone實現(xiàn)了一個適配器武氓,在適配層里模擬出了瀏覽器環(huán)境,讓 Web 端的代碼可以不做什么改動便可運行在小程序里搪柑。

  1. 復(fù)用小程序代碼聋丝,轉(zhuǎn)換小程序代碼在web環(huán)境中運行;適用于有小程序代碼沉淀工碾,未開發(fā)H5或H5平臺完善度較低的開發(fā)者;這個方向業(yè)內(nèi)成熟的方案還比較少百姓。

uni-app近期支持了小程序自定義組件運行到H5平臺渊额,是對如上第三種場景的一種探索。

需求場景

鑒于小程序的低成本獲客特征垒拢,很多廠商選擇先開發(fā)小程序旬迹,驗證業(yè)務(wù)模式后,再擴展至H5求类、App等其它平臺奔垦。

開發(fā)者雖可借助轉(zhuǎn)換器將小程序代碼轉(zhuǎn)換為uni-app項目(或其它跨端框架項目),快速實現(xiàn)多平臺發(fā)行尸疆;但不少開發(fā)者是不敢輕易決策將跨端版本替換之前線上的小程序版本的椿猎,畢竟線上版本已穩(wěn)定運行了一段時間惶岭。

常選的方案是:讓原生小程序版本和uni-app跨端版本并行一段時間,微信平臺繼續(xù)使用原生版本犯眠,其它平臺使用uni-app跨端版本按灶;經(jīng)過一段時間驗證uni-app版本穩(wěn)定后,再使用uni-app版替換掉原生小程序版本筐咧。

在這段并行的時間內(nèi)鸯旁,開發(fā)者需要同時維護微信原生、uni-app兩個版本量蕊,新增業(yè)務(wù)需編寫兩份邏輯相同的代碼铺罢,重復(fù)勞動,成本疊加残炮,如何改善韭赘?

借助uni-app 支持將微信小程序組件運行到H5平臺的特性,我們給出一種思路:

  • 開發(fā)者在原生小程序項目中吉殃,將新增業(yè)務(wù)以自定義組件的方式開發(fā)辞居,優(yōu)先上線小程序;
  • 拷貝小程序組件的wxml/wxss/js/json文件到uni-app 項目下蛋勺,通過uni-app的編譯器及運行時瓦灶,保證小程序自定義組件在H5平臺的正確運行。

這個方案的好處是:

  • 優(yōu)先小程序開發(fā)抱完,畢竟小程序早已上線贼陶,有存量用戶

  • 復(fù)用小程序組件,新增業(yè)務(wù)僅需開發(fā)一套代碼即可巧娱,降低開發(fā)成本

不止自己開發(fā)的小程序組件碉怔,業(yè)內(nèi)開源的三方小程序組件,均可復(fù)制到uni-app項目項目中禁添,運行到H5平臺仿滔。

另外,部分公司的產(chǎn)品經(jīng)理肖爵,會要求不同平臺有不同的交互舔哪,但核心業(yè)務(wù)邏輯是相同的,開發(fā)者常會通過維護不同項目的方式來滿足產(chǎn)品經(jīng)理需求铺峭。此時墓怀,采取如上方案,同樣可滿足多個項目復(fù)用相同業(yè)務(wù)邏輯的訴求卫键。

實際上傀履,uni-app之前已支持將小程序自定義組件運行到App平臺,對于有小程序組件沉淀或優(yōu)先小程序的開發(fā)者來說莉炉,這是個好消息钓账,一套業(yè)務(wù)組件碴犬,快速運行到iOS、Android官扣、H5翅敌、微信小程序這四大流量平臺(實際上也可運行到QQ小程序平臺)。

image

uni-app 引用小程序組件演示

uni-app項目中使用自定義組件的方法很簡單惕蹄,分為三步:

1蚯涮、拷貝小程序自定義組件到uni-app項目根目錄下的wxcomponents文件夾下

2、在 pages.json 對應(yīng)頁面的 style -> usingComponents引入組件卖陵,如:

{
    "pages": [
        {
            "path": "index/index",
            "style": {
                "usingComponents": {
                     "custom": "/wxcomponents/custom/index"
                }
            }
        }
    ]
}

3遭顶、在頁面中使用自定義組件,如:

<!-- 頁面模板 (index.vue) -->
<view>
    <!-- 在頁面中對自定義組件進行引用 -->
    <custom name="uni-app"></custom>
</view>

方案實現(xiàn)思路

簡單介紹下uni-app的多端發(fā)行原理泪蔫。

uni-app基于Vue.js runtime棒旗,頁面文件遵循Vue.js 單文件組件 (SFC) 規(guī)范,天然對H5的支持比較好撩荣,發(fā)行到H5平臺時铣揉,先通過vue-loader解析.vue文件,導(dǎo)出Vue.js 組件選項對象餐曹,然后在運行時補充規(guī)范實現(xiàn):

  • 組件:框架提供內(nèi)置組件(view/swiper/picker等)的實現(xiàn)逛拱,保證平臺UI及交互的一致性
  • 接口:在H5平臺封裝框架接口,比如路由跳轉(zhuǎn)台猴,showToast等界面交互
  • 生命周期:Vue.js的理念是一切皆為組件朽合,沒有應(yīng)用和頁面的概念;框架需創(chuàng)造出應(yīng)用及頁面的概念饱狂,模擬onLaunch曹步、onShow等鉤子

uni-app發(fā)行到小程序平臺時,邏輯又有不同休讳,主要工作有2塊:

  • 編譯器:將.vue文件拆分成wxml/wxss/js/json4個原生頁面文件
  • 運行時:Vue.js和小程序都是邏輯視圖層框架讲婚,都有數(shù)據(jù)綁定功能;運行時會實現(xiàn)Vue.js到小程序的數(shù)據(jù)同步俊柔,及小程序到Vue.js的事件代理

小程序自定義組件類似小程序原生的頁面開發(fā)磺樱,一個自定義組件同樣由wxml/wxss/js/json 4個文件組成,另有單獨的組件規(guī)范(如Component 構(gòu)造器婆咸、Behaviors特性等)。

所以芜辕,小程序自定義組件運行到H5平臺尚骄,可借助uni-app已有平臺功能快速實現(xiàn):

  • 編譯階段:將wxml/wxss/js/json4個文件合并為.vue文件(類似 uni-app 發(fā)行到小程序的逆過程),然后調(diào)用uni-app發(fā)行H5平臺的編譯過程侵续,通過vue-loader解析.vue文件倔丈,導(dǎo)出 Vue.js 組件選項對象
  • 運行階段:實現(xiàn) Component 構(gòu)造器憨闰、Behaviors特性,模擬自定義組件特有的生命周期

編譯:轉(zhuǎn)換文件(mp2vue)

小程序自定義組件發(fā)行到H5平臺需五,在編譯環(huán)節(jié)主要有2項工作:

  1. 將自定義組件的wxml/wxss/js/json 4個文件組成鹉动,編譯轉(zhuǎn)換成.vue文件,即小程序轉(zhuǎn)vue宏邮,可簡寫為mp2vue

  2. 通過vue-loader解析.vue文件泽示,導(dǎo)出 Vue.js 組件選項對象

其中,步驟2是Vue.js項目的標準編譯過程蜜氨,略過不提械筛;我們重點闡述步驟1。

mp2vue將4個獨立wxml/wxss/js/json 的文件合并成一個.vue文件飒炎,并組裝成template埋哟、scriptstyle 這種三段式的結(jié)構(gòu)郎汪,流程包括:

  1. wxml文件生成template節(jié)點赤赊,同時完成指令、事件等模板語法轉(zhuǎn)換
  2. js/json文件生成script節(jié)點煞赢,同時完成組件注冊過
  3. wxss文件生成style節(jié)點抛计,自動轉(zhuǎn)換部分css兼容語法
  4. 合并為.vue文件

具體實現(xiàn)上,uni-app編譯前先掃描wxcomponents目錄耕驰,若存在則認為有小程序自定義組件爷辱,啟動文件轉(zhuǎn)換工作(uni-migration插件來完成):

//加載轉(zhuǎn)換器
const migrate = require('@dcloudio/uni-migration') 
//掃描wxcomponents目錄
const wxcomponents = path.resolve(process.env.UNI_INPUT_DIR, 'wxcomponents') 
if (fs.existsSync(wxcomponents)) { 
  migrate(wxcomponents, false, {
    silent: true 
  }) // 轉(zhuǎn)換 mp-weixin 小程序組件
}

接著開始對wxml/wxss/js/json文件逐個解析,并合并為一個.vue文件:

module.exports = function transformFile(input, options) {
    //首先轉(zhuǎn)換json文件朦肘,判斷是否為組件
  const [jsCode, isComponent] = transformJsonFile(filepath + '.json', deps)
  options.isComponent = isComponent
    //轉(zhuǎn)換 wxml 文件
  const [templateCode, wxsCode = '', wxsFiles = []] = transformTemplateFile(filepath + templateExtname, options)
    //轉(zhuǎn)換wxss文件
  const styleCode = transformStyleFile(filepath + styleExtname, options, deps) || ''
  //轉(zhuǎn)換js文件
  const scriptCode = transformScriptFile(filepath + '.js', jsCode, options, deps)
    // 生成合并后的.vue文件源碼
  return [
    `${commentsCode}<template>
${templateCode}
</template>
${wxsCode}
<script>
${scriptCode}
</script>
<style platform="mp-weixin">
${styleCode}
</style>`,
    deps,
    wxsFiles
  ]
}

進一步細節(jié)說明饭弓,wxml文件轉(zhuǎn)為template節(jié)點時,需完成各項指令媒抠、事件等模板語法的轉(zhuǎn)換弟断,例如:

小程序自定義組件 Vue組件 描述
wx:if v-if 條件渲染
wx:for v-for 列表渲染
bindtap @click 元素點擊事件

將一個最簡自定義組件,按照如上流程轉(zhuǎn)換趴生,結(jié)果示意如下:

image

運行時:模擬小程序組件環(huán)境

uni-app的編譯器并不轉(zhuǎn)換小程序組件的 JS 代碼阀趴,依然保留Component構(gòu)造器的寫法,甚至其中的API依然是wx.開頭的方式苍匆,這些都依賴uni-app在H5平臺的運行時來解決刘急,主要有如下幾部分內(nèi)容:

  • Component構(gòu)造器:解析小程序組件的各種選項配置,轉(zhuǎn)換為Vue組件定義浸踩,包括變通實現(xiàn)其中的差異部分叔汁,如小程序組件特有的”組件所在頁面的生命周期“
  • Behaviors特性:轉(zhuǎn)換為Vue的混入(mixin)
  • 數(shù)據(jù)響應(yīng):在H5平臺實現(xiàn)setData接口及this.data.xx = yy的數(shù)據(jù)通訊機制
  • API前綴:可在運行時通過代理機制,自動將wx.xx替換為uni.xx,這個比較簡單据块,不詳述

Component構(gòu)造器

uni-app在H5平臺定義了一個Component函數(shù)码邻,執(zhí)行到小程序的Component構(gòu)造器函數(shù)后,開始循環(huán)解析其屬性另假,并轉(zhuǎn)換成Vue組件屬性像屋,流程示意代碼如下:

export function Component (options) {
  const componentOptions = parseComponent(options)
  componentOptions.mixins.unshift(polyfill)
  componentOptions.mpOptions.path = global['__wxRoute']
  initRelationsHandler(componentOptions)
  global['__wxComponents'][global['__wxRoute']] = componentOptions
}

export function parseComponent (mpComponentOptions) {
  const {
    data,
    options,
    methods,
    behaviors,
    lifetimes,
    observers,
    relations,
    properties,
    pageLifetimes,
    externalClasses
  } = mpComponentOptions

  const vueComponentOptions = {
    mixins: [],
    props: {},
    watch: {},
    mpOptions: {
      mpObservers: []
    }
  }

    // 開始逐個解析所有屬性
  parseData(data, vueComponentOptions)
  parseOptions(options, vueComponentOptions)
  parseMethods(methods, vueComponentOptions)
  parseBehaviors(behaviors, vueComponentOptions)
  parseLifetimes(lifetimes, vueComponentOptions)
  parseObservers(observers, vueComponentOptions)
  parseRelations(relations, vueComponentOptions)
  parseProperties(properties, vueComponentOptions)
  parsePageLifetimes(pageLifetimes, vueComponentOptions)
  parseExternalClasses(externalClasses, vueComponentOptions)
  parseLifecycle(mpComponentOptions, vueComponentOptions)
  parseDefinitionFilter(mpComponentOptions, vueComponentOptions)
    // 返回 Vue 組件
  return vueComponentOptions
}

在這個過程中,需處理小程序自定義組件和 Vue組件的屬性對應(yīng)關(guān)系及細節(jié)差異边篮,如小程序組件的lifetimes

小程序自定義組件 Vue/uni-app 描述
created onServiceCreated 小程序的created觸發(fā)時己莺,可以訪問子組件信息,而Vuecreated訪問不到苟耻,故需uni-app框架映射到其它時機(onServiceCreated)執(zhí)行
attached onServiceAttached 同上
ready mounted Vue 生命周期
moved - Vue中不存在該鉤子篇恒,暫不支持轉(zhuǎn)換
detached destroyed Vue 生命周期

小程序的pageLifetimes(組件所在頁面的生命周期)在Vue中是沒有的,需要映射為uni-app封裝的頁面生命周期:

小程序自定義組件 uni-app 描述
ready onPageShow 頁面被展示時執(zhí)行
hide onPageHide 頁面被隱藏時執(zhí)行
resize onPageResize 頁面尺寸變化時執(zhí)行

Behaviors特性的實現(xiàn)過程凶杖,類似Component構(gòu)造器胁艰,不再贅述。

數(shù)據(jù)響應(yīng)

Vue和小程序都有一套數(shù)據(jù)綁定系統(tǒng)智蝠,但機制不同腾么,比如在Vue體系下,數(shù)據(jù)賦值是這樣的:

this.a = 1

但在小程序中杈湾,數(shù)據(jù)賦值方式則是這樣的:

this.setData({
    a:1
}) //響應(yīng)式
this.data.a = 2 //非響應(yīng)式

另外解虱,小程序和Vue在數(shù)據(jù)的propertiesobserver等方面都存在不少差異漆撞,經(jīng)過我們評估殴泰,若將小程序的數(shù)據(jù)響應(yīng)用法直接映射到Vue體系下,復(fù)雜度較高且有性能壓力浮驳,故uni-app在H5平臺按照微信的語法規(guī)范悍汛,單獨實現(xiàn)了一套數(shù)據(jù)響應(yīng)系統(tǒng)。

// 小程序的setData在H5平臺的實現(xiàn)
function setData (data, callback) {
  if (!isPlainObject(data)) {
    return
  }
  Object.keys(data).forEach(key => {
    if (setDataByExprPath(key, data[key], this.data)) {
      !hasOwn(this, key) && proxy(this, SOURCE_KEY, key);
    }
  });
  this.$forceUpdate();//數(shù)據(jù)變化至会,強制視圖更新(響應(yīng)式)
  isFn(callback) && this.$nextTick(callback);
}

setData掛載到 vm 對象上离咐,可通過this.setData這種小程序的方式調(diào)用;同時將數(shù)據(jù)綁定到data屬性上奉件,支持this.data.xx的訪問方式宵蛀。

export function initState (vm) {
  const instanceData = JSON.parse(JSON.stringify(vm.$options.mpOptions.data || {}))
  vm[SOURCE_KEY] = instanceData
  
  //vm對象上掛載 setData 方法,實現(xiàn)小程序方法
    vm.setData = setData 
    
  const propertyDefinition = {
    get () {
      return vm[SOURCE_KEY]
    },
    set (value) {
      vm[SOURCE_KEY] = value
    }
  }
    //小程序用法县貌,可通過this.data.xx訪問
  Object.defineProperties(vm, {
    data: propertyDefinition,
    properties: propertyDefinition
  })

  Object.keys(instanceData).forEach(key => {
    proxy(vm, SOURCE_KEY, key)
  })
}

雖然數(shù)據(jù)響應(yīng)是uni-app自己實現(xiàn)的术陶,但渲染依然使用了Vue框架的render函數(shù),此時需小程序規(guī)范中的this.data.xx和Vue規(guī)范中的this.xx保持一致煤痕,通過代理的方式實現(xiàn):

// mp/polyfill/state/proxy.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true
};

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

這里僅列出了主要的幾步瞳别,中間涉及細節(jié)很多征候;部分無法通過Vue擴展機制實現(xiàn)的功能,只好修改Vue.js的內(nèi)核源碼祟敛,比如updateProperties支持、小程序wxs兆解、externalClasses等功能在H5平臺的支持馆铁,都需要定制部分 Vue.js runtime 源碼。

結(jié)語

本文分享了uni-app將微信小程序自定義組件發(fā)行到H5平臺的實現(xiàn)思路锅睛,希望對大家有所啟發(fā)埠巨。

但這種方案,歸根到底是為了解決多套項目并存時的業(yè)務(wù)重復(fù)開發(fā)的問題现拒。

如果你是從頭開發(fā)辣垒,我們建議直接選擇業(yè)內(nèi)成熟的跨端框架,既可以保持一套代碼印蔬,更省力的維護勋桶,還可以借助框架的成熟生態(tài)(如跨端UI庫插件市場),基于成熟輪子侥猬,快速完成業(yè)務(wù)的上線開發(fā)例驹;

uni-app框架代碼,包括小程序組件發(fā)行到H5平臺的代碼退唠,全部開源在github鹃锈,如果大家對本文邏輯有疑問,歡迎提交issue交流瞧预。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末屎债,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子垢油,更是在濱河造成了極大的恐慌盆驹,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秸苗,死亡現(xiàn)場離奇詭異召娜,居然都是意外死亡,警方通過查閱死者的電腦和手機惊楼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門玖瘸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人檀咙,你說我怎么就攤上這事雅倒。” “怎么了弧可?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵蔑匣,是天一觀的道長。 經(jīng)常有香客問我,道長裁良,這世上最難降的妖魔是什么凿将? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮价脾,結(jié)果婚禮上牧抵,老公的妹妹穿的比我還像新娘。我一直安慰自己侨把,他們只是感情好犀变,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秋柄,像睡著了一般获枝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上骇笔,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天省店,我揣著相機與錄音,去河邊找鬼蜘拉。 笑死萨西,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的旭旭。 我是一名探鬼主播谎脯,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼持寄!你這毒婦竟也來了源梭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤稍味,失蹤者是張志新(化名)和其女友劉穎废麻,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體模庐,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡烛愧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了掂碱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片怜姿。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖疼燥,靈堂內(nèi)的尸體忽然破棺而出沧卢,到底是詐尸還是另有隱情,我是刑警寧澤醉者,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布但狭,位于F島的核電站披诗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏立磁。R本人自食惡果不足惜呈队,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望息罗。 院中可真熱鬧掂咒,春花似錦、人聲如沸迈喉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挨摸。三九已至,卻和暖如春岁歉,著一層夾襖步出監(jiān)牢的瞬間得运,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工锅移, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熔掺,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓非剃,卻偏偏與公主長得像置逻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子备绽,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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