成人激情在线一区二区,天天操天天爽天天舔天天操一操,极品人妻少妇诱惑日本,美女视频黄A视频全免费,国产成人综合在线观看,国产成:a人亚洲y品,亚洲国产久久久一区二区三区,成人日韩一区二区在线观看,天天搞天天做天天操天天要

小程序模板網(wǎng)

近兩萬字小程序攻略發(fā)布了

發(fā)布時間:2018-09-07 08:51 所屬欄目:小程序開發(fā)教程

該內(nèi)容由銀科控股融匯研發(fā)部曹俊及其團(tuán)隊(duì)授權(quán)提供。該團(tuán)隊(duì)擁有 10 多名小程序開發(fā),深耕小程序領(lǐng)域,總結(jié)出了本篇優(yōu)質(zhì)長文。同時本篇內(nèi)容也已經(jīng)合并入我的 開源項(xiàng)目 中,目前項(xiàng)目內(nèi)容包含了 JS、網(wǎng)絡(luò)、瀏覽器相關(guān)、性能優(yōu)化、安全、框架、Git、數(shù)據(jù)結(jié)構(gòu)、算法等內(nèi)容,無論是基礎(chǔ)還是進(jìn)階,亦或是源碼解讀,你都能在本圖譜中得到滿意的答案,希望這個面試圖譜能夠幫助到大家更好的準(zhǔn)備面試。

小程序-登錄

unionid和openid

了解小程序登陸之前,我們寫了解下小程序/公眾號登錄涉及到兩個最關(guān)鍵的用戶標(biāo)識:

OpenId
UnionId

關(guān)鍵Api

  • wx.login 官方提供的登錄能力

  • wx.checkSession 校驗(yàn)用戶當(dāng)前的session_key是否有效

  • wx.authorize 提前向用戶發(fā)起授權(quán)請求

  • wx.getUserInfo 獲取用戶基本信息

登錄流程設(shè)計(jì)

以下從筆者接觸過的幾種登錄流程來做闡述:

利用現(xiàn)有登錄體系

直接復(fù)用現(xiàn)有系統(tǒng)的登錄體系,只需要在小程序端設(shè)計(jì)用戶名,密碼/驗(yàn)證碼輸入頁面,便可以簡便的實(shí)現(xiàn)登錄,只需要保持良好的用戶體驗(yàn)即可。

利用OpenId 創(chuàng)建用戶體系

:point_up_2:提過, OpenId 是一個小程序?qū)τ谝粋€用戶的標(biāo)識,利用這一點(diǎn)我們可以輕松的實(shí)現(xiàn)一套基于小程序的用戶體系,值得一提的是這種用戶體系對用戶的打擾最低,可以實(shí)現(xiàn)靜默登錄。具體步驟如下:

  1. 小程序客戶端通過 wx.login 獲取 code

  2. 傳遞 code 向服務(wù)端,服務(wù)端拿到 code 調(diào)用微信登錄憑證校驗(yàn)接口,微信服務(wù)器返回 openid和會話密鑰 session_key ,此時開發(fā)者服務(wù)端便可以利用 openid 生成用戶入庫,再向小程序客戶端返回自定義登錄態(tài)

  3. 小程序客戶端緩存 (通過 storage )自定義登錄態(tài)(token),后續(xù)調(diào)用接口時攜帶該登錄態(tài)作為用戶身份標(biāo)識即可

利用 Unionid 創(chuàng)建用戶體系

如果想實(shí)現(xiàn)多個小程序,公眾號,已有登錄系統(tǒng)的數(shù)據(jù)互通,可以通過獲取到用戶 unionid 的方式建立用戶體系。因?yàn)?unionid 在同一開放平臺下的所所有應(yīng)用都是相同的,通過 unionid 建立的用戶體系即可實(shí)現(xiàn)全平臺數(shù)據(jù)的互通,更方便的接入原有的功能,那如何獲取 unionid 呢,有以下兩種方式:

  1. 如果戶關(guān)注了某個相同主體公眾號,或曾經(jīng)在某個相同主體App、公眾號上進(jìn)行過微信登錄授權(quán),通過 wx.login 可以直接獲取 到 unionid

  2. 結(jié)合 wx.getUserInfo 和 <button open-type="getUserInfo"><button/> 這兩種方式引導(dǎo)用戶主動授權(quán),主動授權(quán)后通過返回的信息和服務(wù)端交互 (這里有一步需要服務(wù)端解密數(shù)據(jù)的過程,很簡單,微信提供了示例代碼) 即可拿到 unionid 建立用戶體系, 然后由服務(wù)端返回登錄態(tài),本地記錄即可實(shí)現(xiàn)登錄,附上微信提供的最佳實(shí)踐:

    • 調(diào)用 wx.login 獲取 code,然后從微信后端換取到 session_key,用于解密 getUserInfo返回的敏感數(shù)據(jù)。

    • 使用 wx.getSetting 獲取用戶的授權(quán)情況

      • 如果用戶已經(jīng)授權(quán),直接調(diào)用 API wx.getUserInfo 獲取用戶最新的信息;
      • 用戶未授權(quán),在界面中顯示一個按鈕提示用戶登入,當(dāng)用戶點(diǎn)擊并授權(quán)后就獲取到用戶的最新信息。
    • 獲取到用戶數(shù)據(jù)后可以進(jìn)行展示或者發(fā)送給自己的后端。

注意事項(xiàng)

  1. 需要獲取 unionid 形式的登錄體系,在以前(18年4月之前)是通過以下這種方式來實(shí)現(xiàn),但后續(xù)微信做了調(diào)整(因?yàn)橐贿M(jìn)入小程序,主動彈起各種授權(quán)彈窗的這種形式,比較容易導(dǎo)致用戶流失),調(diào)整為必須使用按鈕引導(dǎo)用戶主動授權(quán)的方式,這次調(diào)整對開發(fā)者影響較大,開發(fā)者需要注意遵守微信的規(guī)則,并及時和業(yè)務(wù)方溝通業(yè)務(wù)形式,不要存在僥幸心理,以防造成小程序不過審等情況。
wx.login(獲取code) ===> wx.getUserInfo(用戶授權(quán)) ===> 獲取 unionid
復(fù)制代碼
  1. 因?yàn)樾〕绦虿淮嬖?nbsp;cookie 的概念, 登錄態(tài)必須緩存在本地,因此強(qiáng)烈建議為登錄態(tài)設(shè)置過期時間

  2. 值得一提的是如果需要支持風(fēng)控安全校驗(yàn),多平臺登錄等功能,可能需要加入一些公共參數(shù),例如platform,channel,deviceParam等參數(shù)。在和服務(wù)端確定方案時,作為前端同學(xué)應(yīng)該及時提出這些合理的建議,設(shè)計(jì)合理的系統(tǒng)。

  3. openid , unionid 不要在接口中明文傳輸,這是一種危險(xiǎn)的行為,同時也很不專業(yè)。

小程序-圖片導(dǎo)出

經(jīng)常開發(fā)和使用小程序的同學(xué)對這個功能一定不陌生,這是一種常見的引流方式,一般同時會在圖片中附加一個小程序二維碼。

基本原理

  1. 借助 canvas 元素,將需要導(dǎo)出的樣式首先在 canvas 畫布上繪制出來 (api基本和h5保持一致,但有輕微差異,使用時注意即可)

  2. 借助微信提供的 canvasToTempFilePath 導(dǎo)出圖片,最后再使用 saveImageToPhotosAlbum(需要授權(quán))保存圖片到本地

如何優(yōu)雅實(shí)現(xiàn)

根據(jù)上述的原理來看,實(shí)現(xiàn)是很簡單的,只不過就是設(shè)計(jì)稿的提取,繪制即可,但是作為一個常用功能,每次都這樣寫一坨代碼豈不是非常的難受。那小程序如何設(shè)計(jì)一個通用的方法來幫助我們導(dǎo)出圖片呢?思路如下:

  1. 繪制出需要的樣式這一步是省略不掉的。但是我們可以封裝一個繪制庫,包含常見圖形的繪制,例如矩形,圓角矩形,圓, 扇形, 三角形, 文字,圖片減少繪制代碼,只需要提煉出樣式信息,便可以輕松的繪制,最后導(dǎo)出圖片存入相冊。筆者覺得以下這種方式繪制更為優(yōu)雅清晰一些,其實(shí)也可以使用加入一個type參數(shù)來指定繪制類型,傳入的一個是樣式數(shù)組,實(shí)現(xiàn)繪制。

  2. 結(jié)合上一步的實(shí)現(xiàn),如果對于同一類型的卡片有多次導(dǎo)出需求的場景,也可以使用自定義組件的方式,封裝同一類型的卡片為一個通用組件,在需要導(dǎo)出圖片功能的地方,引入該組件即可。

class CanvasKit {
    constructor() {
    }
    drawImg(option = {}) {
      ...
      return this
    }
    drawRect(option = {}) {
      return this
    }
    drawText(option = {}) {
      ...
      return this
    }
    static exportImg(option = {}) {
      ...
    }
  }

  let drawer = new CanvasKit('canvasId').drawImg(styleObj1).drawText(styleObj2)
  drawer.exportImg()

注意事項(xiàng)

  1. 小程序中無法繪制網(wǎng)絡(luò)圖片到canvas上,需要通過downLoadFile 先下載圖片到本地臨時文件才可以繪制
  2. 通常需要繪制二維碼到導(dǎo)出的圖片上,有一種方式導(dǎo)出二維碼時,需要攜帶的參數(shù)必須做編碼,而且有具體的長度(32可見字符)限制,可以借助服務(wù)端生成 短鏈接 的方式來解決

小程序-數(shù)據(jù)統(tǒng)計(jì)

數(shù)據(jù)統(tǒng)計(jì)作為目前一種常用的分析用戶行為的方式,小程序端也是必不可少的。小程序采取的曝光,點(diǎn)擊數(shù)據(jù)埋點(diǎn)其實(shí)和h5原理是一樣的。但是埋點(diǎn)作為一個和業(yè)務(wù)邏輯不相關(guān)的需求,我們?nèi)绻诿恳粋€點(diǎn)擊事件,每一個生命周期加入各種埋點(diǎn)代碼,則會干擾正常的業(yè)務(wù)邏輯,和使代碼變的臃腫,筆者提供以下幾種思路來解決數(shù)據(jù)埋點(diǎn):

設(shè)計(jì)一個埋點(diǎn)sdk

小程序的代碼結(jié)構(gòu)是,每一個 Page 中都有一個 Page 方法,接受一個包含生命周期函數(shù),數(shù)據(jù)的 業(yè)務(wù)邏輯對象 包裝這層數(shù)據(jù),借助小程序的底層邏輯實(shí)現(xiàn)頁面的業(yè)務(wù)邏輯。通過這個我們可以想到思路,對Page進(jìn)行一次包裝,篡改它的生命周期和點(diǎn)擊事件,混入埋點(diǎn)代碼,不干擾業(yè)務(wù)邏輯,只要做一些簡單的配置即可埋點(diǎn),簡單的代碼實(shí)現(xiàn)如下:

page = function(params) {
    let keys = params.keys()
    keys.forEach(v => {
        if (v === 'onLoad') {
          params[v] = function(options) {
            stat()   //曝光埋點(diǎn)代碼
            params[v].call(this, options)
          }
        }
        else if (v.includes('click')) {
          params[v] = funciton(event) { 
            let data = event.dataset.config
            stat(data)  // 點(diǎn)擊埋點(diǎn)
            param[v].call(this)
          }
        }
    })
  }

這種思路不光適用于埋點(diǎn),也可以用來作全局異常處理,請求的統(tǒng)一處理等場景。

分析接口

對于特殊的一些業(yè)務(wù),我們可以采取 接口埋點(diǎn) ,什么叫接口埋點(diǎn)呢?很多情況下,我們有的api并不是多處調(diào)用的,只會在某一個特定的頁面調(diào)用,通過這個思路我們可以分析出,該接口被請求,則這個行為被觸發(fā)了,則完全可以通過服務(wù)端日志得出埋點(diǎn)數(shù)據(jù),但是這種方式局限性較大,而且屬于分析結(jié)果得出過程,可能存在誤差,但可以作為一種思路了解一下。

微信本身提供的數(shù)據(jù)分析能力,微信本身提供了常規(guī)分析和自定義分析兩種數(shù)據(jù)分析方式,在小程序后臺配置即可。借助 小程序數(shù)據(jù)助手 這款小程序可以很方便的查看。

小程序-工程化

工程化做什么

目前的前端開發(fā)過程,工程化是必不可少的一環(huán),那小程序工程化都需要做些什么呢,先看下目前小程序開發(fā)當(dāng)中存在哪些問題需要解決:

  1. 不支持 css預(yù)編譯器,作為一種主流的 css解決方案,不論是 less,sass,stylus 都可以提升css效率
  2. 不支持引入npm包 (這一條,從微信公開課中聽聞,微信準(zhǔn)備支持)
  3. 不支持ES7等后續(xù)的js特性,好用的async await等特性都無法使用
  4. 不支持引入外部字體文件,只支持base64
  5. 沒有 eslint 等代碼檢查工具

方案選型

對于目前常用的工程化方案,webpack,rollup,parcel等來看,都常用與單頁應(yīng)用的打包和處理,而小程序天生是 “多頁應(yīng)用” 并且存在一些特定的配置。根據(jù)要解決的問題來看,無非是文件的編譯,修改,拷貝這些處理,對于這些需求,我們想到基于流的 gulp 非常的適合處理,并且相對于webpack配置多頁應(yīng)用更加簡單。所以小程序工程化方案推薦使用 gulp

具體開發(fā)思路

通過 gulp 的 task 實(shí)現(xiàn):

  1. 實(shí)時編譯 less 文件至相應(yīng)目錄
  2. 引入支持async,await的運(yùn)行時文件
  3. 編譯字體文件為base64 并生成相應(yīng)css文件,方便使用
  4. 依賴分析哪些地方引用了npm包,將npm包打成一個文件,拷貝至相應(yīng)目錄
  5. 檢查代碼規(guī)范

上述實(shí)現(xiàn)起來其實(shí)并不是很難,但是這樣的話就是一份純粹的 gulp 構(gòu)建腳本和 約定好的目錄而已,每次都有一個新的小程序都來拷貝這份腳本來處理嗎?顯然不合適,那如何真正的實(shí)現(xiàn) 小程序工程化 呢? 我們可能需要一個簡單的腳手架,腳手架需要支持的功能:

  1. 支持新建項(xiàng)目,創(chuàng)建Page,創(chuàng)建Component
  2. 支持內(nèi)置構(gòu)建腳本
  3. 支持發(fā)布小程序,也可以想辦法接入Jenkins等工具做持續(xù)集成 (小程序持續(xù)集成后面會提) ...

小程序架構(gòu)

微信小程序的框架包含兩部分 View 視圖層、App Service邏輯層。View 層用來渲染頁面結(jié)構(gòu),AppService 層用來邏輯處理、數(shù)據(jù)請求、接口調(diào)用。

它們在 兩個線程里 運(yùn)行。

它們在 兩個線程里 運(yùn)行。

它們在 兩個線程里 運(yùn)行。

視圖層和邏輯層通過系統(tǒng)層的 JSBridage 進(jìn)行通信,邏輯層把數(shù)據(jù)變化通知到視圖層,觸發(fā)視圖層頁面更新,視圖層把觸發(fā)的事件通知到邏輯層進(jìn)行業(yè)務(wù)處理。

補(bǔ)充

視圖層使用 WebView 渲染,iOS 中使用自帶 WKWebView,在 Android 使用騰訊的 x5 內(nèi)核(基于 Blink)運(yùn)行。

邏輯層使用在 iOS 中使用自帶的 JSCore 運(yùn)行,在 Android 中使用騰訊的 x5 內(nèi)核(基于 Blink)運(yùn)行。

開發(fā)工具使用 nw.js 同時提供了視圖層和邏輯層的運(yùn)行環(huán)境。

在 Mac下 使用 js-beautify 對微信開發(fā)工具 @v1.02.1808080代碼批量格式化:

cd /Applications/wechatwebdevtools.app/Contents/Resources/package.nw
find . -type f -name '*.js' -not -path "./node_modules/*" -not -path -exec js-beautify -r -s 2 -p -f '{}' \;
復(fù)制代碼

在 js/extensions/appservice/index.js 中找到:

267: function(a, b, c) {
    const d = c(8),
      e = c(227),
      f = c(226),
      g = c(228),
      h = c(229),
      i = c(230);
    var j = window.__global.navigator.userAgent,
      k = -1 !== j.indexOf('game');
    k || i(), window.__global.getNewWeixinJSBridge = (a) => {
      const {
        invoke: b
      } = f(a), {
        publish: c
      } = g(a), {
        subscribe: d,
        triggerSubscribeEvent: i
      } = h(a), {
        on: j,
        triggerOnEvent: k
      } = e(a);
      return {
        invoke: b,
        publish: c,
        subscribe: d,
        on: j,
        get __triggerOnEvent() {
          return k
        },
        get __triggerSubscribeEvent() {
          return i
        }
      }
    }, window.WeixinJSBridge = window.__global.WeixinJSBridge = window.__global.getNewWeixinJSBridge('global'), window.__global.WeixinJSBridgeMap = {
      __globalBridge: window.WeixinJSBridge
    }, __devtoolsConfig.online && __devtoolsConfig.autoTest && setInterval(() => {
      console.clear()
    }, 1e4);
    try {
      var l = new window.__global.XMLHttpRequest;
      l.responseType = 'text', l.open('GET', `http://${window.location.host}/calibration/${Date.now()}`, !0), l.send()
    } catch (a) {}
  }
復(fù)制代碼

在 js/extensions/gamenaitveview/index.js 中找到:

299: function(a, b, c) {
    'use strict';
    Object.defineProperty(b, '__esModule', {
      value: !0
    });
    var d = c(242),
      e = c(241),
      f = c(243),
      g = c(244);
    window.WeixinJSBridge = {
      on: d.a,
      invoke: e.a,
      publish: f.a,
      subscribe: g.a
    }
  },
復(fù)制代碼

在 js/extensions/pageframe/index.js 中找到:

317: function(a, b, c) {
    'use strict';

    function d() {
      window.WeixinJSBridge = {
        on: e.a,
        invoke: f.a,
        publish: g.a,
        subscribe: h.a
      }, k.a.init();
      let a = document.createEvent('UIEvent');
      a.initEvent('WeixinJSBridgeReady', !1, !1), document.dispatchEvent(a), i.a.init()
    }
    Object.defineProperty(b, '__esModule', {
      value: !0
    });
    var e = c(254),
      f = c(253),
      g = c(255),
      h = c(256),
      i = c(86),
      j = c(257),
      k = c.n(j);
    'complete' === document.readyState ? d() : window.addEventListener('load', function() {
      d()
    })
  },
復(fù)制代碼

我們都看到了 WeixinJSBridge 的定義。分別都有 on 、 invoke 、 publish 、 subscribe 這個幾個關(guān)鍵方法。

拿 invoke 舉例,在 js/extensions/appservice/index.js 中發(fā)現(xiàn)這段代碼:

f (!r) p[b] = s, f.send({
    command: 'APPSERVICE_INVOKE',
    data: {
        api: c,
        args: e,
        callbackID: b
    }
});
復(fù)制代碼

在 js/extensions/pageframe/index.js 中發(fā)現(xiàn)這段代碼:

g[d] = c, e.a.send({
    command: 'WEBVIEW_INVOKE',
    data: {
        api: a,
        args: b,
        callbackID: d
    }
})

復(fù)制代碼

簡單的分析得知:字段 command 用來區(qū)分行為, invoke 用來調(diào)用 Native 的 Api。在不同的來源要使用不同的前綴。 data 里面包含 Api 名,參數(shù)。另外 callbackID 指定接受回調(diào)的方法句柄。Appservice 和 Webview 使用的通信協(xié)議是一致的。

我們不能在代碼里使用 BOM 和 DOM 是因?yàn)楦緵]有,另一方面也不希望 JS 代碼直接操作視圖。

在開發(fā)工具中 remote-helper.js 中找到了這樣的代碼:

const vm = require("vm");

const vmGlobal = {
    require: undefined,
    eval: undefined,
    process: undefined,
    setTimeout(...args) {
        //...省略代碼
        return timerCount;
    },
    clearTimeout(id) {
        const timer = timers[id];
        if (timer) {
            clearTimeout(timer);
            delete timers[id];
        }
    },
    setInterval(...args) {
        //...省略代碼
        return timerCount;
    },
    clearInterval(id) {
        const timer = timers[id];
        if (timer) {
            clearInterval(timer);
            delete timers[id];
        }
    },
    console: (() => {
        //...省略代碼
        return consoleClone;
    })()
};
const jsVm = vm.createContext(vmGlobal);
// 省略大量代碼...
function loadCode(filePath, sourceURL, content) {
    let ret;
    try {
        const script = typeof content === 'string' ? content : fs.readFileSync(filePath, 'utf-8').toString();
        ret = vm.runInContext(script, jsVm, {
            filename: sourceURL,
        });
    }
    catch (e) {
        // something went wrong in user code
        console.error(e);
    }
    return ret;
}
復(fù)制代碼

這樣的分層設(shè)計(jì)顯然是有意為之的,它的中間層完全控制了程序?qū)τ诮缑孢M(jìn)行的操作, 同時對于傳遞的數(shù)據(jù)和響應(yīng)時間也能做到監(jiān)控。一方面程序的行為受到了極大限制, 另一方面微信可以確保他們對于小程序內(nèi)容和體驗(yàn)有絕對的控制。

這樣的結(jié)構(gòu)也說明了小程序的動畫和繪圖 API 被設(shè)計(jì)成生成一個最終對象而不是一步一步執(zhí)行的樣子, 原因就是 Json 格式的數(shù)據(jù)傳遞和解析相比與原生 API 都是損耗不菲的,如果頻繁調(diào)用很可能損耗過多性能,進(jìn)而影響用戶體驗(yàn)。

下載小程序完整包

App Service - Life Cylce

面試題

1.動畫需要綁定在 data 上,而繪圖卻不用。你覺得是為什么呢?

var context = wx.createCanvasContext('firstCanvas')
    
context.setStrokeStyle("#00ff00")
context.setLineWidth(5)
context.rect(0, 0, 200, 200)
context.stroke()
context.setStrokeStyle("#ff0000")
context.setLineWidth(2)
context.moveTo(160, 100)
context.arc(100, 100, 60, 0, 2 * Math.PI, true)
context.moveTo(140, 100)
context.arc(100, 100, 40, 0, Math.PI, false)
context.moveTo(85, 80)
context.arc(80, 80, 5, 0, 2 * Math.PI, true)
context.moveTo(125, 80)
context.arc(120, 80, 5, 0, 2 * Math.PI, true)
context.stroke()
context.draw()
復(fù)制代碼
Page({
  data: {
    animationData: {}
  },
  onShow: function(){
    var animation = wx.createAnimation({
      duration: 1000,
  	  timingFunction: 'ease',
    })

    this.animation = animation
    
    animation.scale(2,2).rotate(45).step()
    
    this.setData({
      animationData:animation.export()
    })
  }
})
復(fù)制代碼

2.小程序的 Http Rquest 請求是不是用的瀏覽器 Fetch API?

知識點(diǎn)考察

  • 知道 Request 是由 Native 實(shí)現(xiàn)的
  • JSCore 是不帶 Http Request、Websocket、Storage等功能的,那是 Webkit 帶的
  • 小程序的 wx.request 是不是遵循 fetch API 規(guī)范實(shí)現(xiàn)的呢?答案,顯然不是。因?yàn)闆]有 Promise

View - WXML

WXML(WeiXin Markup Language)

  • 支持?jǐn)?shù)據(jù)綁定
  • 支持邏輯算術(shù)、運(yùn)算
  • 支持模板、引用
  • 支持添加事件(bindtap)

Wxml編譯器:Wcc 把 Wxml文件 轉(zhuǎn)為 JS

執(zhí)行方式:Wcc index.wxml

使用 Virtual DOM,進(jìn)行局部更新

View - WXSS

WXSS(WeiXin Style Sheets)

wxss編譯器:wcsc 把wxss文件轉(zhuǎn)化為 js

執(zhí)行方式: wcsc index.wxss

支持大部分CSS特性

親測包含但不限于如下內(nèi)容:

  • Transition
  • Animation
    • Keyframes
  • border-radius
  • calc()
  • 選擇器,除了官方文檔列出的,其實(shí)還支持
    • element>element
    • element+element
    • element element
    • element:first-letter
    • element:first-line
    • element:first-child
    • element:last-child
    • element~element
    • element:first-of-type
    • element:last-of-type
    • element:only-of-type
    • element:only-child
    • element:nth-child(n)
    • element:nth-last-child(n)
    • element:nth-of-type(n)
    • element:nth-last-of-type(n)
    • :root
    • element:empty
    • :not(element)
  • iconfont

建議 Css3 的特性都可以做一下嘗試。

尺寸單位 rpx

rpx(responsive pixel): 可以根據(jù)屏幕寬度進(jìn)行自適應(yīng)。規(guī)定屏幕寬為 750rpx。公式:

const dsWidth = 750

export const screenHeightOfRpx = function () {
  return 750 / env.screenWidth * env.screenHeight
}

export const rpxToPx = function (rpx) {
  return env.screenWidth / 750 * rpx
}

export const pxToRpx = function (px) {
  return 750 / env.screenWidth * px
}
復(fù)制代碼

可以了解一下 pr2rpx-loader 這個庫。

樣式導(dǎo)入

使用 @import 語句可以導(dǎo)入外聯(lián)樣式表, @import 后跟需要導(dǎo)入的外聯(lián)樣式表的相對路徑,用 ; 表示語句結(jié)束。

內(nèi)聯(lián)樣式

靜態(tài)的樣式統(tǒng)一寫到 class 中。style 接收動態(tài)的樣式,在運(yùn)行時會進(jìn)行解析, 請盡量避免將靜態(tài)的樣式寫進(jìn) style 中,以免影響渲染速度 。

全局樣式與局部樣式

定義在 app.wxss 中的樣式為全局樣式,作用于每一個頁面。在 page 的 wxss 文件中定義的樣式為局部樣式,只作用在對應(yīng)的頁面,并會覆蓋 app.wxss 中相同的選擇器。

iconfont

截止20180810

小程序未來有計(jì)劃支持字體。參考微信公開課。

小程序開發(fā)與平時 Web開發(fā)類似,也可以使用字體圖標(biāo),但是 src:url() 無論本地還是遠(yuǎn)程地址都不行,base64 值則都是可以顯示的。

將 ttf 文件轉(zhuǎn)換成 base64。打開這個平臺 transfonter.org/。點(diǎn)擊 Add fonts 按鈕,加載ttf格式的那個文件。將下邊的 base64 encode 改為 on。點(diǎn)擊 Convert 按鈕進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換后點(diǎn)擊 download 下載。

復(fù)制下載的壓縮文件中的 stylesheet.css 的內(nèi)容到 font.wxss ,并且將 icomoon 中的 style.css 除了 @font-face 所有的代碼也復(fù)制到 font.wxss 并將i選擇器換成 .iconfont,最后:

<text class="iconfont icon-home" style="font-size:50px;color:red"></text>
復(fù)制代碼

View - Component

小程序提供了一系列組件用于開發(fā)業(yè)務(wù)功能,按照功能與HTML5的標(biāo)簽進(jìn)行對比如下:

小程序的組件基于Web Component標(biāo)準(zhǔn)

使用Polymer框架實(shí)現(xiàn)Web Component

View - Native Component

目前Native實(shí)現(xiàn)的組件有

  • cavnas

  • video

  • map

  • textarea

Native組件層在 WebView 層之上。這目前帶來了一些問題:

  • Native 實(shí)現(xiàn)的組件會遮擋其他組件
  • WebView 渲染出來的視圖在滾動時,Native 實(shí)現(xiàn)的組件需要更新位置,這會帶來性能問題,在安卓機(jī)器上比較明顯
  • 小程序原生組件 cover-view 可以覆蓋 cavnas video 等,但是也有一下弊端,比如在 cavnas 上覆蓋 cover-view ,就會發(fā)現(xiàn)坐標(biāo)系不統(tǒng)一處理麻煩

目前小程序的問題或限制

截止20180810

包含但不限于:

  • 小程序仍然使用 WebView 渲染,并非原生渲染。(部分原生)

  • 服務(wù)端接口返回的頭無法執(zhí)行,比如:Set-Cookie。

  • 依賴瀏覽器環(huán)境的 JS 庫不能使用。

  • 不能使用 npm,但是可以自搭構(gòu)建工具或者使用 mpvue。(未來官方有計(jì)劃支持)

  • 不能使用 ES7,可以自己用babel+webpack自搭或者使用 mpvue。

  • 不支持使用自己的字體(未來官方計(jì)劃支持)。

  • 可以用 base64 的方式來使用 iconfont。

  • 小程序不能發(fā)朋友圈(可以通過保存圖片到本地,發(fā)圖片到朋友前。二維碼可以使用B接口)。

  • 獲取二維碼/小程序接口的限制。

    • B 接口 scene 最大32個可見字符。
    • AC 接口總共生成的碼數(shù)量限制為 100,000,請謹(jǐn)慎調(diào)用。
    • 真機(jī)掃描二維碼只能跳轉(zhuǎn)到線上版本,所以測試環(huán)境下只可通過開發(fā)者工具的通過二維碼編譯進(jìn)行調(diào)試。
    • 沒有發(fā)布到線上版本的小程序頁面路徑會導(dǎo)致生成二維碼失敗,需要先將添加了頁面的小程序發(fā)布到線上版本。
  • 小程序推送只能使用“服務(wù)通知” 而且需要用戶主動觸發(fā)提交 formId,formId 只有7天有效期。(現(xiàn)在的做法是在每個頁面都放入form并且隱藏以此獲取更多的 formId。后端使用原則為:優(yōu)先使用有效期最短的)

  • 小程序大小限制 2M,分包總計(jì)不超過 8M

  • 轉(zhuǎn)發(fā)(分享)小程序不能拿到成功結(jié)果,原來可以。鏈接(小游戲造的孽)

  • 拿到相同的 unionId 必須綁在同一個開放平臺下。開放平臺綁定限制:

    • 50個移動應(yīng)用
    • 10個網(wǎng)站
    • 50個同主體公眾號
    • 5個不同主體公眾號
    • 50個同主體小程序
    • 5個不同主體小程序
  • 公眾號關(guān)聯(lián)小程序,鏈接

    • 所有公眾號都可以關(guān)聯(lián)小程序。
    • 一個公眾號可關(guān)聯(lián)10個同主體的小程序,3個不同主體的小程序。
    • 一個小程序可關(guān)聯(lián)500個公眾號。
    • 公眾號一個月可新增關(guān)聯(lián)小程序13次,小程序一個月可新增關(guān)聯(lián)500次。
  • 一個公眾號關(guān)聯(lián)的10個同主體小程序和3個非同主體小程序可以互相跳轉(zhuǎn)

  • 品牌搜索不支持金融、醫(yī)療

  • 小程序授權(quán)需要用戶主動點(diǎn)擊

  • 小程序不提供測試 access_token

  • 安卓系統(tǒng)下,小程序授權(quán)獲取用戶信息之后,刪除小程序再重新獲取,并重新授權(quán),得到舊簽名,導(dǎo)致第一次授權(quán)失敗

  • 開發(fā)者工具上,授權(quán)獲取用戶信息之后,如果清緩存選擇全部清除,則即使使用了wx.checkSession,并且在session_key有效期內(nèi),授權(quán)獲取用戶信息也會得到新的session_key

小程序HTTP2支持情況

HTTP2支持情況:模擬器與真機(jī)均不支持

為了驗(yàn)證小程序?qū)TTP的支持適配情況,我找了兩個服務(wù)器做測試,一個是網(wǎng)上搜索到支持HTTP2的服務(wù)器,一個是我本地起的一個HTTP2服務(wù)器。測試中所有請求方法均使用 wx.request。

  1. 網(wǎng)上支持HTTP2的服務(wù)器: HTTPs://www.snel.com:443

  2. 在Chrome上查看該服務(wù)器為 HTTP2

  3. 在模擬器上請求該接口, 請求頭 的HTTP版本為HTTP1.1,模擬器不支持HTTP2

  4. 由于小程序線上環(huán)境需要在項(xiàng)目管理里配置請求域名,而這個域名不是我們需要的請求域名,沒必要浪費(fèi)一個域名位置,所以打開不驗(yàn)證域名,TSL 等選項(xiàng)請求該接口,通過抓包工具表現(xiàn)與模擬器相同

HTTP2服務(wù)器需要對小程序做兼容性適配

由上可以看出,在真機(jī)與模擬器都不支持 HTTP2,但是都是成功請求的,并且 響應(yīng)頭 里的 HTTP 版本都變成了HTTP1.1 版本,說明服務(wù)器對 HTTP1.1 做了兼容性適配。

  1. 本地新啟一個 node 服務(wù)器,返回 JSON 為請求的 HTTP 版本

  2. 如果服務(wù)器只支持 HTTP2,在模擬器請求時發(fā)生了一個 ALPN 協(xié)議的錯誤。并且提醒使用適配 HTTP1

  3. 當(dāng)把服務(wù)器的 allowHTTP1 ,設(shè)置為 true ,并在請求時處理相關(guān)相關(guān)請求參數(shù)后,模擬器能正常訪問接口,并打印出對應(yīng)的 HTTP 請求版本

授權(quán)獲取用戶信息流程

  • session_key 有有效期,有效期并沒有被告知開發(fā)者,只知道用戶越頻繁使用小程序,session_key 有效期越長
  • 在調(diào)用 wx.login 時會直接更新 session_key,導(dǎo)致舊 session_key 失效
  • 小程序內(nèi)先調(diào)用 wx.checkSession 檢查登錄態(tài),并保證沒有過期的 session_key 不會被更新,再調(diào)用 wx.login 獲取 code。接著用戶授權(quán)小程序獲取用戶信息,小程序拿到加密后的用戶數(shù)據(jù),把加密數(shù)據(jù)和 code 傳給后端服務(wù)。后端通過 code 拿到 session_key 并解密數(shù)據(jù),將解密后的用戶信息返回給小程序

面試題:先授權(quán)獲取用戶信息再 login 會發(fā)生什么?

  • 用戶授權(quán)時,開放平臺使用舊的 session_key 對用戶信息進(jìn)行加密。調(diào)用 wx.login 重新登錄,會刷新 session_key,這時后端服務(wù)從開放平臺獲取到新 session_key,但是無法對老 session_key 加密過的數(shù)據(jù)解密,用戶信息獲取失敗
  • 在用戶信息授權(quán)之前先調(diào)用 wx.checkSession 呢?wx.checkSession 檢查登錄態(tài),并且保證 wx.login 不會刷新 session_key,從而讓后端服務(wù)正確解密數(shù)據(jù)。但是這里存在一個問題,如果小程序較長時間不用導(dǎo)致 session_key 過期,則 wx.login 必定會重新生成 session_key,從而再一次導(dǎo)致用戶信息解密失敗。

性能優(yōu)化

我們知道view部分是運(yùn)行在webview上的,所以前端領(lǐng)域的大多數(shù)優(yōu)化方式都有用。

我們知道view部分是運(yùn)行在webview上的,所以前端領(lǐng)域的大多數(shù)優(yōu)化方式都有用。

我們知道view部分是運(yùn)行在webview上的,所以前端領(lǐng)域的大多數(shù)優(yōu)化方式都有用。

加載優(yōu)化

代碼包的大小是最直接影響小程序加載啟動速度的因素。代碼包越大不僅下載速度時間長,業(yè)務(wù)代碼注入時間也會變長。所以最好的優(yōu)化方式就是減少代碼包的大小。

小程序加載的三個階段的表示。

優(yōu)化方式

  • 代碼壓縮。
  • 及時清理無用代碼和資源文件。
  • 減少代碼包中的圖片等資源文件的大小和數(shù)量。
  • 分包加載。

首屏加載的體驗(yàn)優(yōu)化建議

  • 提前請求: 異步數(shù)據(jù)請求不需要等待頁面渲染完成。
  • 利用緩存: 利用 storage API 對異步請求數(shù)據(jù)進(jìn)行緩存,二次啟動時先利用緩存數(shù)據(jù)渲染頁面,在進(jìn)行后臺更新。
  • 避免白屏:先展示頁面骨架頁和基礎(chǔ)內(nèi)容。
  • 及時反饋:即時地對需要用戶等待的交互操作給出反饋,避免用戶以為小程序無響應(yīng)。

使用分包加載優(yōu)化

在構(gòu)建小程序分包項(xiàng)目時,構(gòu)建會輸出一個或多個功能的分包,其中每個分包小程序必定含有一個主包,所謂的主包,即放置默認(rèn)啟動頁面/TabBar 頁面,以及一些所有分包都需用到公共資源/JS 腳本,而分包則是根據(jù)開發(fā)者的配置進(jìn)行劃分。

在小程序啟動時,默認(rèn)會下載主包并啟動主包內(nèi)頁面,如果用戶需要打開分包內(nèi)某個頁面,客戶端會把對應(yīng)分包下載下來,下載完成后再進(jìn)行展示。

優(yōu)點(diǎn):

  • 對開發(fā)者而言,能使小程序有更大的代碼體積,承載更多的功能與服務(wù)
  • 對用戶而言,可以更快地打開小程序,同時在不影響啟動速度前提下使用更多功能

限制:

  • 整個小程序所有分包大小不超過 8M
  • 單個分包/主包大小不能超過 2M

原生分包加載的配置假設(shè)支持分包的小程序目錄結(jié)構(gòu)如下:

├── app.js
├── app.json
├── app.wxss
├── packageA
│   └── pages
│       ├── cat
│       └── dog
├── packageB
│   └── pages
│       ├── apple
│       └── banana
├── pages
│   ├── index
│   └── logs
└── utils

復(fù)制代碼

開發(fā)者通過在 app.json subPackages 字段聲明項(xiàng)目分包結(jié)構(gòu):


{
  "pages":[
    "pages/index",
    "pages/logs"
  ],
  "subPackages": [
    {
      "root": "packageA",
      "pages": [
        "pages/cat",
        "pages/dog"
      ]
    }, {
      "root": "packageB",
      "pages": [
        "pages/apple",
        "pages/banana"
      ]
    }
  ]
}

分包原則

  • 聲明 subPackages 后,將按 subPackages 配置路徑進(jìn)行打包,subPackages 配置路徑外的目錄將被打包到 app(主包) 中
  • app(主包)也可以有自己的 pages(即最外層的 pages 字段
  • subPackage 的根目錄不能是另外一個 subPackage 內(nèi)的子目錄
  • 首頁的 TAB 頁面必須在 app(主包)內(nèi)

引用原則

  • packageA 無法 require packageB JS 文件,但可以 require app、自己 package 內(nèi)的 JS 文件
  • packageA 無法 import packageB 的 template,但可以 require app、自己 package 內(nèi)的 template
  • packageA 無法使用 packageB 的資源,但可以使用 app、自己 package 內(nèi)的資源

官方即將推出分包預(yù)加載

獨(dú)立分包

渲染性能優(yōu)化

  • 每次 setData 的調(diào)用都是一次進(jìn)程間通信過程,通信開銷與 setData 的數(shù)據(jù)量正相關(guān)。

  • setData 會引發(fā)視圖層頁面內(nèi)容的更新,這一耗時操作一定時間中會阻塞用戶交互。

  • setData 是小程序開發(fā)使用最頻繁,也是最容易引發(fā)性能問題的。

避免不當(dāng)使用 setData

  • 使用 data 在方法間共享數(shù)據(jù), 可能增加 setData 傳輸?shù)臄?shù)據(jù)量。 。data 應(yīng)僅包括與頁面渲染相關(guān)的數(shù)據(jù)。
  • 使用 setData 傳輸大量數(shù)據(jù),**通訊耗時與數(shù)據(jù)正相關(guān),頁面更新延遲可能造成頁面更新開銷增加。**僅傳輸頁面中發(fā)生變化的數(shù)據(jù),使用 setData 的特殊 key 實(shí)現(xiàn)局部更新。
  • 短時間內(nèi)頻繁調(diào)用 setData,**操作卡頓,交互延遲,阻塞通信,頁面渲染延遲。**避免不必要的 setData,對連續(xù)的setData調(diào)用進(jìn)行合并。
  • 在后臺頁面進(jìn)行 setData,**搶占前臺頁面的渲染資源。**頁面切入后臺后的 setData 調(diào)用,延遲到頁面重新展示時執(zhí)行。

避免不當(dāng)使用onPageScroll

  • 只在有必要的時候監(jiān)聽 pageScroll 事件。不監(jiān)聽,則不會派發(fā)。
  • 避免在 onPageScroll 中執(zhí)行復(fù)雜邏輯
  • 避免在 onPageScroll 中頻繁調(diào)用 setData
  • 避免滑動時頻繁查詢節(jié)點(diǎn)信息(SelectQuery)用以判斷是否顯示,部分場景建議使用節(jié)點(diǎn)布局橡膠狀態(tài)監(jiān)聽(inersectionObserver)替代

使用自定義組件

在需要頻繁更新的場景下,自定義組件的更新只在組件內(nèi)部進(jìn)行,不受頁面其他部分內(nèi)容復(fù)雜性影響。

官方小程序技術(shù)能力規(guī)劃

自定義組件2.0

小程序的幾個頁面間,存在一些相同或是類似的區(qū)域,這時候可以把這些區(qū)域邏輯封裝成一個自定義組件,代碼就可以重用,或者對于比較獨(dú)立邏輯,也可以把它封裝成一個自定義組件,也就是微信去年發(fā)布的自定義組件,它讓代碼得到復(fù)用、減少代碼量,更方便模塊化,優(yōu)化代碼架構(gòu)組織,也使得模塊清晰,后期更好地維護(hù),從而保證更好的性能。

但微信打算在原來的基礎(chǔ)上推出的自定義組件 2.0,它將擁有更高級的性能:

  • usingComponents 計(jì)劃支持全局定義和通配符定義:這意味著不用在每個頁面反復(fù)定義,可以批量導(dǎo)入目錄下的所有自定義組件
  • 計(jì)劃支持類似 Computed 和 watch 的功能,它能使代碼邏輯更清晰
  • 計(jì)劃支持 Component 構(gòu)造器插件,在實(shí)例化一個自定義組件的時候,允許你在構(gòu)造器的這個階段,加入一些邏輯,方便進(jìn)行一些擴(kuò)展,甚至是可以擴(kuò)展成 Vue 的語法

npm支持

目前小程序開發(fā)的痛點(diǎn)是:開源組件要手動復(fù)制到項(xiàng)目,后續(xù)更新組件也需要手動操作。不久的將來,小程序?qū)⒅С謓pm包管理,有了這個之后,想要引入一些開源的項(xiàng)目就變得很簡單了,只要在項(xiàng)目里面聲明,然后用簡單的命令安裝,就可以使用了。

官方自定義組件

微信小程序團(tuán)隊(duì)表示,他們在考慮推出一些官方自定義組件,為什么不內(nèi)置到基礎(chǔ)庫里呢?因?yàn)閮?nèi)置組件要提供給開發(fā)者,這個組件一定是開發(fā)者很難實(shí)現(xiàn)或者是無法實(shí)現(xiàn)的一個能力。所以他們更傾向于封裝成自定義組件,想基于這些內(nèi)置組件里,封裝一些比較常見的、交互邏輯比較復(fù)雜的組件給大家使用,讓大家更容易開發(fā)。類似彈幕組件,開發(fā)者就不用關(guān)注彈幕怎么飄,可以節(jié)省開發(fā)者的開發(fā)成本。

同時,他們也想給開發(fā)者提供一些規(guī)范和一些模板,讓開發(fā)者設(shè)計(jì)出好用的自定義組件,更好地被大家去使用。

添加體驗(yàn)評分

當(dāng)小程序加載太慢時,可能會導(dǎo)致用戶的流失,而小程序的開發(fā)者可能會面臨著不知道如何定位問題或不知如何解決問題的困境。

為此,小程序即將推出一個體驗(yàn)評分的功能,這是為了幫助開發(fā)者可以檢查出小程序有一些什么體驗(yàn)不好的地方,也會同時給出一份優(yōu)化的指引建議。

原生組件同層渲染

小程序在最初的技術(shù)選型時,引入了原生組件的概念,因?yàn)樵M件可以使小程序的能力更加豐富,比如地圖、音視頻的能力,但是原生組件是由客戶端原生渲染的,導(dǎo)致了原生組件的層級是最高的,開發(fā)者很容易遇到打開調(diào)試的問題,發(fā)現(xiàn)視頻組件擋在了 vConsole 上。

為了解決這個問題,當(dāng)時微信做了一個過渡的方案:cover-view。cover-view可以覆蓋在原生組件之上,這一套方案解決了大部分的需求場景。比如說視頻組件上很多的按鈕、標(biāo)題甚至還有動畫的彈幕,這些都是用 cover-view 去實(shí)現(xiàn)的,但它還是沒有完全解決原生組件的開發(fā)體驗(yàn)問題,因?yàn)?cover-view 有一些限制:

  • 無法與其他組件混在一起渲染
  • 沒有完整的觸摸事件
  • cover-view 對樣式的表現(xiàn)有差異
  • cover-view 對樣式的支持度不夠高

因此微信決定將用同層渲染取代 cover-view,它能像普通組件一樣使用,原生組件的層級不再是最高,而是和其他的非原生組件在同一層級渲染,可完全由 z-index 控制,可完全支持觸摸事件。

微信表示,同層渲染在 iOS 平臺小程序上已經(jīng)開始內(nèi)測,會很快開放給開發(fā)者,Android 平臺已經(jīng)取得突破性進(jìn)展,目前正在做一輪封裝的工作,開放指日可待。

wepy vs mpvue

數(shù)據(jù)流管理

相比傳統(tǒng)的小程序框架,這個一直是我們作為資深開發(fā)者比較期望去解決的,在 Web 開發(fā)中,隨著 Flux、Redux、Vuex 等多個數(shù)據(jù)流工具出現(xiàn),我們也期望在業(yè)務(wù)復(fù)雜的小程序中使用。

  • WePY 默認(rèn)支持 Redux,在腳手架生成項(xiàng)目的時候可以內(nèi)置

  • Mpvue 作為 Vue 的移植版本,當(dāng)然支持 Vuex,同樣在腳手架生成項(xiàng)目的時候可以內(nèi)置

組件化

如果你和我們一樣,經(jīng)歷了從無到有的小程序業(yè)務(wù)開發(fā),建議閱讀【小程序的組件化開發(fā)】章節(jié),進(jìn)行官方語法的組件庫開發(fā)(從基礎(chǔ)庫 1.6.3 開始,官方提供了組件化解決方案)。

  • WePY 類似 Vue 實(shí)現(xiàn)了單文件組件,最大的差別是文件后綴 .wpy,只是寫法上會有差異,具體可以查看【主流框架使用案例 1:WePY】章節(jié),學(xué)習(xí)起來有一定成本,不過也會很快適應(yīng):
export default class Index extends wepy.page {}
復(fù)制代碼
  • Mpvue 作為 Vue 的移植版本,支持單文件組件,template、script 和 style 都在一個 .vue 文件中,和 vue 的寫法類似,所以對 Vue 開發(fā)熟悉的同學(xué)會比較適應(yīng)。

工程化

所有的小程序開發(fā)依賴官方提供的開發(fā)者工具。開發(fā)者工具簡單直觀,對調(diào)試小程序很有幫助,現(xiàn)在也支持騰訊云(目前我們還沒有使用,但是對新的一些開發(fā)者還是有幫助的),可以申請測試報(bào)告查看小程序在真實(shí)的移動設(shè)備上運(yùn)行性能和運(yùn)行效果,但是它本身沒有類似前端工程化中的概念和工具。

  • wepy 內(nèi)置了構(gòu)建,通過 wepy init 命令初始化項(xiàng)目,大致流程如下:
  • wepy-cli 會判斷模版是在遠(yuǎn)程倉庫還是在本地,如果在本地則會立即跳到第 3 步,反之繼續(xù)進(jìn)行。
  • 會從遠(yuǎn)程倉庫下載模版,并保存到本地。
  • 詢問開發(fā)者 Project name 等問題,依據(jù)開發(fā)者的回答,創(chuàng)建項(xiàng)目。
  • mpvue 沿用了 vue 中推崇的 webpack 作為構(gòu)建工具,但同時提供了一些自己的插件以及配置文件的一些修改,比如:
  • 不再需要 html-webpack-plugin
  • 基于 webpack-dev-middleware 修改成 webpack-dev-middleware-hard-disk
  • 最大的變化是基于 webpack-loader 修改成 mpvue-loader
  • 但是配置方式還是類似,分環(huán)境配置文件,最終都會編譯成小程序支持的目錄結(jié)構(gòu)和文件后綴。

綜合比較

選型的個人看法

先說結(jié)論:選擇 mpvue。

wepy vs mpvue。

理由:

工程化原生開發(fā)因?yàn)椴粠Чこ袒?,諸如NPM包(未來會引入)、ES7、圖片壓縮、PostCss、pug、ESLint等等不能用。如果自己要搭工程化,不如直接使用wepy或mpvue。mpvue和wepy都可以和小程序原生開發(fā)混寫。, 參考wepy 。 而問題在于wepy沒有引入webpack(wepy@2.0.x依然沒有引入),以上說的這些東西都要造輪子(作者造或自己造)。沒有引入 Webpack 是一個重大的硬傷。社區(qū)維護(hù)的成熟 Webpack 顯然更穩(wěn)定,輪子更多。

維護(hù)wepy 也是社區(qū)維護(hù)的,是官方的?其實(shí) wepy 的主要開發(fā)者只有作者一人,附上一個 contrubutors 鏈接。另外被官方招安了也是后來的事,再說騰訊要有精力幫著一起維護(hù)好 wepy,為什么不花精力在小程序原生開發(fā)上呢?再來看看 mpvue,是美團(tuán)一個前端小組維護(hù)的。

學(xué)習(xí)成本Vue 的學(xué)習(xí)曲線比較平緩。mpvue 是 Vue的子集。所以 mpvue 的學(xué)習(xí)成本會低于 wepy。尤其是之前技術(shù)棧有學(xué)過用過 Vue 的。

未來規(guī)劃mpvue 已經(jīng)支持 web 和小程序。因?yàn)?mpvue 基于AST,所以未來可以支持支付寶小程序和快應(yīng)用。他們也是有這樣的規(guī)劃。

請?jiān)谛枨蟪叵旅孀约赫?/p>

坑兩者都有各自的坑。但是我覺得有一些wepy的坑是沒法容忍的。比如 repeat組建里面用computed得到的列表全是同一套數(shù)據(jù) 而且1.x是沒法解決的。 wepy和mpvue我都開發(fā)過完整小程序的體驗(yàn)下,我覺得wepy的坑更多,而且wepy有些坑礙于架構(gòu)設(shè)計(jì)沒辦法解決。

mpvue

Vue.js 小程序版, fork 自 vuejs/vue@2.4.1,保留了 vue runtime 能力,添加了小程序平臺的支持。 mpvue 是一個使用 Vue.js 開發(fā)小程序的前端框架。框架基于 Vue.js 核心, mpvue 修改了 Vue.js 的 runtime 和 compiler 實(shí)現(xiàn),使其可以運(yùn)行在小程序環(huán)境中,從而為小程序開發(fā)引入了整套 Vue.js 開發(fā)體驗(yàn)。

框架原理

兩個大方向

mpvue
mpvue-loader

七個具體問題

要了解 mpvue 原理必然要了解 Vue 原理,這是大前提。但是要講清楚 Vue 原理需要花費(fèi)大量的篇幅,不如參考 learnVue 。

現(xiàn)在假設(shè)您對 Vue 原理有個大概的了解。

由于 Vue 使用了 Virtual DOM,所以 Virtual DOM 可以在任何支持 JavaScript 語言的平臺上操作,譬如說目前 Vue 支持瀏覽器平臺或 weex,也可以是 mp(小程序)。那么最后 Virtual DOM 如何映射到真實(shí)的 DOM 節(jié)點(diǎn)上呢?vue為平臺做了一層適配層,瀏覽器平臺見 runtime/node-ops.js 、weex平臺見 runtime/node-ops.js ,小程序見 runtime/node-ops.js 。不同平臺之間通過適配層對外提供相同的接口,Virtual DOM進(jìn)行操作Real DOM節(jié)點(diǎn)的時候,只需要調(diào)用這些適配層的接口即可,而內(nèi)部實(shí)現(xiàn)則不需要關(guān)心,它會根據(jù)平臺的改變而改變。

所以思路肯定是往增加一個 mp 平臺的 runtime 方向走。但問題是小程序不能操作 DOM,所以 mp 下的 node-ops.js 里面的實(shí)現(xiàn)都是直接 return obj 。

新 Virtual DOM 和舊 Virtual DOM 之間需要做一個 patch,找出 diff。patch完了之后的 diff 怎么更新視圖,也就是如何給這些 DOM 加入 attr、class、style 等 DOM 屬性呢? Vue 中有 nextTick 的概念用以更新視圖,mpvue這塊對于小程序的 setData 應(yīng)該怎么處理呢?

另外個問題在于小程序的 Virtual DOM 怎么生成?也就是怎么將 template 編譯成 render function 。這當(dāng)中還涉及到 運(yùn)行時-編譯器-vs-只包含運(yùn)行時 ,顯然如果要提高性能、減少包大小、輸出 wxml、mpvue 也要提供預(yù)編譯的能力。因?yàn)橐A(yù)輸出 wxml 且沒法動態(tài)改變 DOM,所以動態(tài)組件,自定義 render,和 <script type="text/x-template"> 字符串模版等都不支持(參考)。

另外還有一些其他問題,最后總結(jié)一下

render function

platform/mp的目錄結(jié)構(gòu)

.
├── compiler //解決問題1,mpvue-template-compiler源碼部分
├── runtime //解決問題3 4 5 6 7
├── util //工具方法
├── entry-compiler.js //mpvue-template-compiler的入口。package.json相關(guān)命令會自動生成mpvue-template-compiler這個package。
├── entry-runtime.js //對外提供Vue對象,當(dāng)然是mpvue
└── join-code-in-build.js //編譯出SDK時的修復(fù)
復(fù)制代碼

后面的內(nèi)容逐步解答這幾個問題,也就弄明白了原理

mpvue-loader

mpvue-loader 是 vue-loader 的一個擴(kuò)展延伸版,類似于超集的關(guān)系,除了 vue-loader 本身所具備的能力之外,它還會利用 mpvue-template-compiler 生成 render function 。

  • entry

它會從 webpack 的配置中的 entry 開始,分析依賴模塊,并分別打包。在entry 中 app 屬性及其內(nèi)容會被打包為微信小程序所需要的 app.js/app.json/app.wxss,其余的會生成對應(yīng)的頁面page.js/page.json/page.wxml/page.wxss,如示例的 entry 將會生成如下這些文件,文件內(nèi)容下文慢慢講來:

// webpack.config.js
{
    // ...
    entry: {
        app: resolve('./src/main.js'),               // app 字段被識別為 app 類型
        index: resolve('./src/pages/index/main.js'),   // 其余字段被識別為 page 類型
        'news/home': resolve('./src/pages/news/home/index.js')
    }
}

// 產(chǎn)出文件的結(jié)構(gòu)
.
├── app.js
├── app.json
├──· app.wxss
├── components
│   ├── card$74bfae61.wxml
│   ├── index$023eef02.wxml
│   └── news$0699930b.wxml
├── news
│   ├── home.js
│   ├── home.wxml
│   └── home.wxss
├── pages
│   └── index
│       ├── index.js
│       ├── index.wxml
│       └── index.wxss
└── static
    ├── css
    │   ├── app.wxss
    │   ├── index.wxss
    │   └── news
    │       └── home.wxss
    └── js
        ├── app.js
        ├── index.js
        ├── manifest.js
        ├── news
        │   └── home.js
        └── vendor.js
復(fù)制代碼
  • wxml 每一個 .vue 的組件都會被生成為一個 wxml 規(guī)范的 template,然后通過 wxml 規(guī)范的 import 語法來達(dá)到一個復(fù)用,同時組件如果涉及到 props 的 data 數(shù)據(jù),我們也會做相應(yīng)的處理,舉個實(shí)際的例子:
<template>
    <div class="my-component" @click="test">
        <h1>{{msg}}</h1>
        <other-component :msg="msg"></other-component>
    </div>
</template>
<script>
import otherComponent from './otherComponent.vue'

export default {
  components: { otherComponent },
  data () {
    return { msg: 'Hello Vue.js!' }
  },
  methods: {
    test() {}
  }
}
</script>
復(fù)制代碼

這樣一個 Vue 的組件的模版部分會生成相應(yīng)的 wxml

<import src="components/other-component$hash.wxml" />
<template name="component$hash">
    <view class="my-component" bindtap="handleProxy">
        <view class="_h1">{{msg}}</view>
        <template is="other-component$hash" wx:if="{{ $c[0] }}" data="{{ ...$c[0] }}"></template>
    </view>
</template>
復(fù)制代碼

可能已經(jīng)注意到了 other-component(:msg="msg") 被轉(zhuǎn)化成了 。mpvue 在運(yùn)行時會從根組件開始把所有的組件實(shí)例數(shù)據(jù)合并成一個樹形的數(shù)據(jù),然后通過 setData 到 appData, $c 是 $children 的縮寫。至于那個 0 則是我們的 compiler 處理過后的一個標(biāo)記,會為每一個子組件打一個特定的不重復(fù)的標(biāo)記。 樹形數(shù)據(jù)結(jié)構(gòu)如下:

// 這兒數(shù)據(jù)結(jié)構(gòu)是一個數(shù)組,index 是動態(tài)的
{
  $child: {
    '0'{
      // ... root data
      $child: {
        '0': {
          // ... data
          msg: 'Hello Vue.js!',
          $child: {
            // ...data
          }
        }
      }
    }
  }
}
復(fù)制代碼
  • wxss

這個部分的處理同 web 的處理差異不大,唯一不同在于通過配置生成 .css 為 .wxss ,其中的對于 css 的若干處理,在 postcss-mpvue-wxss 和 px2rpx-loader 這兩部分的文檔中又詳細(xì)的介紹。

app.json/page.json 1.1.1 以上

推薦和小程序一樣,將 app.json/page.json 放到頁面入口處,使用 copy-webpack-plugin copy 到對應(yīng)的生成位置。

1.1.1 以下

這部分內(nèi)容來源于 app 和 page 的 entry 文件,通常習(xí)慣是 main.js,你需要在你的入口文件中 export default { config: {} },這才能被我們的 loader 識別為這是一個配置,需要寫成 json 文件。

import Vue from 'vue';
import App from './app';

const vueApp = new Vue(App);
vueApp.$mount();

// 這個是我們約定的額外的配置
export default {
    // 這個字段下的數(shù)據(jù)會被填充到 app.json / page.json
    config: {
        pages: ['static/calendar/calendar', '^pages/list/list'], // Will be filled in webpack
        window: {
            backgroundTextStyle: 'light',
            navigationBarBackgroundColor: '#455A73',
            navigationBarTitleText: '美團(tuán)汽車票',
            navigationBarTextStyle: '#fff'
        }
    }
};
復(fù)制代碼

同時,這個時候,我們會根據(jù) entry 的頁面數(shù)據(jù),自動填充到 app.json 中的 pages 字段。 pages 字段也是可以自定義的,約定帶有 ^ 符號開頭的頁面,會放到數(shù)組的最前面。

style scoped 在 vue-loader 中對 style scoped 的處理方式是給每個樣式加一個 attr 來標(biāo)記 module-id,然后在 css 中也給每條 rule 后添加 [module-id],最終可以形成一個 css 的“作用域空間”。

在微信小程序中目前是不支持 attr 選擇器的,所以我們做了一點(diǎn)改動,把 attr 上的 [module-id] 直接寫到了 class 里,如下:

<!-- .vue -->
<template>
    <div class="container">
        // ...
    </div>
</template>
<style scoped>
    .container {
        color: red;
    }
</style>

<!-- vue-loader -->
<template>
    <div class="container" data-v-23e58823>
        // ...
    </div>
</template>
<style scoped>
    .container[data-v-23e58823] {
        color: red;
    }
</style>

<!-- mpvue-loader -->
<template>
    <div class="container data-v-23e58823">
        // ...
    </div>
</template>
<style scoped>
    .container.data-v-23e58823 {
        color: red;
    }
</style>
復(fù)制代碼
  • compiler

生產(chǎn)出的內(nèi)容是:

(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
// mpvue-template-compiler會利用AST預(yù)編譯生成一個render function用以生成Virtual DOM。
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
  // _c創(chuàng)建虛擬節(jié)點(diǎn),參考https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3606
  // 以及https://github.com/Meituan-Dianping/mpvue/blob/master/packages/mpvue/index.js#L3680
  return _c('div', {
    staticClass: "my-component"
  }, [_c('h1', [_vm._v(_vm._s(_vm.msg))]), _vm._v(" "), _c('other-component', {
    attrs: {
      "msg": _vm.msg,
      "mpcomid": '0'
    }
  })], 1)
}

// staticRenderFns的作用是靜態(tài)渲染,在更新時不會進(jìn)行patch,優(yōu)化性能。而staticRenderFns是個空數(shù)組。
var staticRenderFns = []
render._withStripped = true
var esExports = { render: render, staticRenderFns: staticRenderFns }
/* harmony default export */ __webpack_exports__["a"] = (esExports);
if (false) {
  module.hot.accept()
  if (module.hot.data) {
     require("vue-hot-reload-api").rerender("data-v-54ad9125", esExports)
  }
}

/***/ })
復(fù)制代碼

compiler

compiler相關(guān),也就是template預(yù)編譯這塊,可以參考《 聊聊Vue的template編譯 》來搞明白。原理是一樣的。

mpvue自己實(shí)現(xiàn)了 export { compile, compileToFunctions, compileToWxml } ( 鏈接 )其中 compileToWxml 是用來生成wxml,具體代碼 在這 。

另外mpvue是不需要提供運(yùn)行時-編譯器的,雖然理論上是能夠做到的。因?yàn)樾〕绦虿荒懿僮鱀OM,即便提供了運(yùn)行時-編譯器也產(chǎn)生不了界面。

詳細(xì)講解compile過程:

1.將vue文件解析成模板對象

// mpvue-loader/lib/loader.js
var parts = parse(content, fileName, this.sourceMap)
復(fù)制代碼

假如vue文件源碼如下:

<template>
  <view class="container-bg">
    <view class="home-container">
      <home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />
    </view>
  </view>
</template>

<script lang="js">
import homeQuotationView from '@/components/homeQuotationView'
import topListApi from '@/api/topListApi'

export default {
  data () {
    return {
      lists: []
    }
  },
  components: {
    homeQuotationView
  },
  methods: {
    async loadRankList () {
      let {data} = await topListApi.rankList()
      if (data) {
        this.dateTime = data.dt
        this.lists = data.rankList.filter((item) => {
          return !!item
        })
      }
    },
    itemViewClicked (quotationItem) {
      wx.navigateTo({
        url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`
      })
    }
  },
  onShow () {
    this.loadRankList()
  }
}
</script>

<style lang="stylus" scoped>
  .container-bg
    width 100%
    height 100%
    background-color #F2F4FA

  .home-container
    width 100%
    height 100%
    overflow-x hidden

</style>
復(fù)制代碼

調(diào)用 parse(content, fileName, this.sourceMap) 函數(shù)得到的結(jié)果大致如下:

{
  template: {
    type: 'template',
    content: '\n<view class="container-bg">\n  <view class="home-container">\n    <home-quotation-view v-for="(item, index) in lists" :key="index" :reason="item.reason" :stockList="item.list" @itemViewClicked="itemViewClicked" />\n  </view>\n</view>\n',
    start: 10,
    attrs: {},
    end: 251
  },
  script: {
    type: 'script',
    content: '\n\n\n\n\n\n\n\n\nimport homeQuotationView from \'@/components/homeQuotationView\'\nimport topListApi from \'@/api/topListApi\'\n\nexport default {\n  data () {\n    return {\n      lists: []\n    }\n  },\n  components: {\n    homeQuotationView\n  },\n  methods: {\n    async loadRankList () {\n      let {data} = await topListApi.rankList()\n      if (data) {\n        this.dateTime = data.dt\n        this.lists = data.rankList.filter((item) => {\n          return !!item\n        })\n      }\n    },\n    itemViewClicked (quotationItem) {\n      wx.navigateTo({\n        url: `/pages/topListDetail/main?item=${JSON.stringify(quotationItem)}`\n      })\n    }\n  },\n  onShow () {\n    this.loadRankList()\n  }\n}\n',
    start: 282,
    attrs: {
      lang: 'js'
    },
    lang: 'js',
    end: 946,
    ...
  },
  styles: [{
    type: 'style',
    content: '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.container-bg\n  width 100%\n  height 100%\n  background-color #F2F4FA\n\n.home-container\n  width 100%\n  height 100%\n  overflow-x hidden\n\n',
    start: 985,
    attrs: [Object],
    lang: 'stylus',
    scoped: true,
    end: 1135,
    ...
  }],
  customBlocks: []
}
復(fù)制代碼

2.調(diào)用mpvue-loader/lib/template-compiler/index.js導(dǎo)出的接口并傳入上面得到的html模板:

var templateCompilerPath = normalize.lib('template-compiler/index')
...
var defaultLoaders = {
  html: templateCompilerPath + templateCompilerOptions,
  css: options.extractCSS
    ? getCSSExtractLoader()
    : styleLoaderPath + '!' + 'css-loader' + cssLoaderOptions,
  js: hasBuble ? ('buble-loader' + bubleOptions) : hasBabel ? babelLoaderOptions : ''
}

// check if there are custom loaders specified via
// webpack config, otherwise use defaults
var loaders = Object.assign({}, defaultLoaders, options.loaders)
復(fù)制代碼
調(diào)用mpvue/packages/mpvue-template-compiler/build.js的compile接口:
// mpvue-loader/lib/template-compiler/index.js
var compiled = compile(html, compilerOptions)
復(fù)制代碼
compile方法生產(chǎn)下面的ast(Abstract Syntax Tree)模板,render函數(shù)和staticRenderFns

{
  ast: {
    type: 1,
    tag: 'view',
    attrsList: [],
    attrsMap: {
      class: 'container-bg'
    },
    parent: undefined,
    children: [{
      type: 1,
      tag: 'view',
      attrsList: [],
      attrsMap: {
        class: 'home-container'
      },
      parent: {
        type: 1,
        tag: 'view',
        attrsList: [],
        attrsMap: {
          class: 'container-bg'
        },
        parent: undefined,
        children: [
          [Circular]
        ],
        plain: false,
        staticClass: '"container-bg"',
        static: false,
        staticRoot: false
      },
      children: [{
        type: 1,
        tag: 'home-quotation-view',
        attrsList: [{
          name: ':reason',
          value: 'item.reason'
        }, {
          name: ':stockList',
          value: 'item.list'
        }, {
          name: '@itemViewClicked',
          value: 'itemViewClicked'
        }],
        attrsMap: {
          'v-for': '(item, index) in lists',
          ':key': 'index',
          ':reason': 'item.reason',
          ':stockList': 'item.list',
          '@itemViewClicked': 'itemViewClicked',
          'data-eventid': '{{\'0-\'+index}}',
          'data-comkey': '{{$k}}'
        },
        parent: [Circular],
        children: [],
        for: 'lists',
        alias: 'item',
        iterator1: 'index',
        key: 'index',
        plain: false,
        hasBindings: true,
        attrs: [{
          name: 'reason',
          value: 'item.reason'
        }, {
          name: 'stockList',
          value: 'item.list'
        }, {
          name: 'eventid',
          value: '\'0-\'+index'
        }, {
          name: 'mpcomid',
          value: '\'0-\'+index'
        }],
        events: {
          itemViewClicked: {
            value: 'itemViewClicked',
            modifiers: undefined
          }
        },
        eventid: '\'0-\'+index',
        mpcomid: '\'0-\'+index',
        static: false,
        staticRoot: false,
        forProcessed: true
      }],
      plain: false,
      staticClass: '"home-container"',
      static: false,
      staticRoot: false
    }],
    plain: false,
    staticClass: '"container-bg"',
    static: false,
    staticRoot: false
  },
  render: 'with(this){return _c(\'view\',{staticClass:"container-bg"},[_c(\'view\',{staticClass:"home-container"},_l((lists),function(item,index){return _c(\'home-quotation-view\',{key:index,attrs:{"reason":item.reason,"stockList":item.list,"eventid":\'0-\'+index,"mpcomid":\'0-\'+index},on:{"itemViewClicked":itemViewClicked}})}))])}',
  staticRenderFns: [],
  errors: [],
  tips: []
}
復(fù)制代碼

其中的render函數(shù)運(yùn)行的結(jié)果是返回 VNode 對象,其實(shí) render 函數(shù)應(yīng)該長下面這樣:

(function() {
  with(this){
    return _c('div',{   //創(chuàng)建一個 div 元素
      attrs:{"id":"app"}  //div 添加屬性 id
      },[
        _m(0),  //靜態(tài)節(jié)點(diǎn) header,此處對應(yīng) staticRenderFns 數(shù)組索引為 0 的 render 函數(shù)
        _v(" "), //空的文本節(jié)點(diǎn)
        (message) //三元表達(dá)式,判斷 message 是否存在
         //如果存在,創(chuàng)建 p 元素,元素里面有文本,值為 toString(message)
        ?_c('p',[_v("\n    "+_s(message)+"\n  ")])
        //如果不存在,創(chuàng)建 p 元素,元素里面有文本,值為 No message. 
        :_c('p',[_v("\n    No message.\n  ")])
      ]
    )
  }
})
復(fù)制代碼

其中的 _c 就是vue對象的 createElement 方法 (創(chuàng)建元素), _m 是 renderStatic (渲染靜態(tài)節(jié)點(diǎn)), _v 是 createTextVNode (創(chuàng)建文本dom), _s 是 toString (轉(zhuǎn)換為字符串)

// src/core/instance/render.js
export function initRender (vm: Component) {
  ...
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  ...
}

...
Vue.prototype._s = toString
...
Vue.prototype._m = renderStatic
...
Vue.prototype._v = createTextVNode
...
復(fù)制代碼
  1. 調(diào)用compileWxml方法生產(chǎn)wxml模板,這個方法最終會調(diào)用 mpvue/packages/mpvue-template-compiler/build.js的compileToWxml方法將第一步compile出來的模板轉(zhuǎn)成小程序的wxml模板
// mpvue-loader/lib/template-compiler/index.js
compileToWxml.call(this, compiled, html)
復(fù)制代碼

以上解答了問題1、2

runtime

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

.
├── events.js //解答問題5
├── index.js //入口提供Vue對象,以及$mount,和各種初始化
├── liefcycle //解答問題6、7
├── node-ops.js //操作真實(shí)DOM的相關(guān)實(shí)現(xiàn),因?yàn)樾〕绦虿荒懿僮鱀OM,所以這里都是直接返回
├── patch.js //解答問題3
└── render.js //解答問題4
復(fù)制代碼

patch.js

和vue使用的 createPatchFunction 保持一致,任然是舊樹和新樹進(jìn)行patch產(chǎn)出diff,但是多了一行this.$updateDataToMP()用以更新。

render.js

兩個核心的方法 initDataToMP 、 updateDataToMP 。

initDataToMP 收集vm上的data,然后調(diào)用小程序Page示例的 setData 渲染。

updateDataToMP 在每次patch,也就是依賴收集發(fā)現(xiàn)數(shù)據(jù)改變時更新(參考patch.js代碼),這部分一樣會使用 nextTick 和隊(duì)列。最終使用了節(jié)流閥 throttleSetData 。50毫秒用來控制頻率以解決頻繁修改Data,會造成大量傳輸Data數(shù)據(jù)而導(dǎo)致的性能問題。

其中 collectVmData 最終也是用到了 formatVmData 。尤其要注意的是一句注釋:

getVmData 這兒獲取當(dāng)前組件內(nèi)的所有數(shù)據(jù),包含 props、computed 的數(shù)據(jù)

我們又知道,service到view是兩個線程間通信,如果Data含有大量數(shù)據(jù),增加了傳輸數(shù)據(jù)量,加大了傳輸成本,將會造成性能下降。

events.js

正如官網(wǎng)所說的,這里使用 eventTypeMap 做了各事件的隱射

import { getComKey, eventTypeMap } from '../util/index'
復(fù)制代碼
// 用于小程序的 event type 到 web 的 event
export const eventTypeMap = {
  tap: ['tap', 'click'],
  touchstart: ['touchstart'],
  touchmove: ['touchmove'],
  touchcancel: ['touchcancel'],
  touchend: ['touchend'],
  longtap: ['longtap'],
  input: ['input'],
  blur: ['change', 'blur'],
  submit: ['submit'],
  focus: ['focus'],
  scrolltoupper: ['scrolltoupper'],
  scrolltolower: ['scrolltolower'],
  scroll: ['scroll']
}
復(fù)制代碼

使用了 handleProxyWithVue 方法來代理小程序事件到vue事件。

另外看下作者自己對這部分的思路

事件代理機(jī)制:用戶交互觸發(fā)的數(shù)據(jù)更新通過事件代理機(jī)制完成。在 Vue.js 代碼中,事件響應(yīng)函數(shù)對應(yīng)到組件的 method, Vue.js 自動維護(hù)了上下文環(huán)境。然而在小程序中并沒有類似的機(jī)制,又因?yàn)?Vue.js 執(zhí)行環(huán)境中維護(hù)著一份實(shí)時的虛擬 DOM,這與小程序的視圖層完全對應(yīng),我們思考,在小程序組件節(jié)點(diǎn)上觸發(fā)事件后,只要找到虛擬 DOM 上對應(yīng)的節(jié)點(diǎn),觸發(fā)對應(yīng)的事件不就完成了么;另一方面,Vue.js 事件響應(yīng)如果觸發(fā)了數(shù)據(jù)更新,其生命周期函數(shù)更新將自動觸發(fā),在此函數(shù)上同步更新小程序數(shù)據(jù),數(shù)據(jù)同步也就實(shí)現(xiàn)了。

getHandle 這個方法應(yīng)該就是作者思路當(dāng)中所說的:找到對應(yīng)節(jié)點(diǎn),然后找到handle。

lifecycle.js

在 initMP 方法中,自己創(chuàng)建小程序的App、Page。實(shí)現(xiàn)生命周期相關(guān)方法,使用 callHook代理兼容小程序App、Page的生命周期。

官方文檔生命周期中說到了:

同 vue,不同的是我們會在小程序 onReady 后,再去觸發(fā) vue mounted 生命周期

這部分查看, onReady 之后才會執(zhí)行 next ,這個 next 回調(diào)最終是vue的 mountComponent??梢栽?nbsp;index.js 中看到。這部分代碼也就是解決了"小程序生命周期中觸發(fā)vue生命周期"。

export function initMP (mpType, next) {
  // ...
    global.Page({
      // 生命周期函數(shù)--監(jiān)聽頁面初次渲染完成
      onReady () {
        mp.status = 'ready'

        callHook(rootVueVM, 'onReady')
        next()
      },
    })
  // ...
}
復(fù)制代碼

在小程序onShow時,使用$nextTick去第一次渲染數(shù)據(jù),參考上面提到的render.js。

export function initMP (mpType, next) {
  // ...
  global.Page({
    // 生命周期函數(shù)--監(jiān)聽頁面顯示
    onShow () {
      mp.page = this
      mp.status = 'show'
      callHook(rootVueVM, 'onShow')

      // 只有頁面需要 setData
      rootVueVM.$nextTick(() => {
        rootVueVM._initDataToMP()
      })
    },
  })
  // ...
}
復(fù)制代碼

在mpvue-loader生成template時,比如點(diǎn)擊事件 @click 會變成 bindtap="handleProxy" ,事件綁定全都會使用 handleProxy 這個方法。

可以查看上面回顧一下。

最終handleProxy調(diào)用的是event.js中的 handleProxyWithVue 。

export function initMP (mpType, next) {
  // ...
    global.Page({
      handleProxy (e) {
        return rootVueVM.$handleProxyWithVue(e)
      },
    })
  // ...
}
復(fù)制代碼

index.js

最后index.js就負(fù)責(zé)各種初始化和mount。

Class和Style為什么暫不支持組件

原因:目前的組件是使用小程序的 template 標(biāo)簽實(shí)現(xiàn)的,給組件指定的class和style是掛載在template標(biāo)簽上,而template 標(biāo)簽不支持 class 及 style 屬性。

解決方案: 在自定義組件上綁定class或style到一個props屬性上。

// 組件ComponentA.vue
 <template>
  <div class="container" :class="pClass">
    ...
  </div>
</template>
復(fù)制代碼
<script>
    export default {
    props: {
      pClass: {
        type: String,
        default: ''
      }
    }
  }
</script>
復(fù)制代碼
<!--PageB.vue-->
<template>
    <component-a :pClass="cusComponentAClass"  />
</template>
復(fù)制代碼
<script>
data () {
    return {
      cusComponentAClass: 'a-class b-class'
    }
  }
</script>
復(fù)制代碼
<style lang="stylus" scoped>
  .a-class
    border red solid 2rpx
  .b-class
    margin-right 20rpx
</style>
復(fù)制代碼

但是這樣會有問題就是style加上scoped之后,編譯模板生成的代碼是下面這樣的:

.a-class.data-v-8f1d914e {
   border: #f00 solid 2rpx;
 }
 .b-class.data-v-8f1d914e {
   margin-right 20rpx
 }
復(fù)制代碼

所以想要這些組件的class生效就不能使用scoped的style,改成下面這樣,最好自己給a-class和b-class加前綴以防其他的文件引用這些樣式:

<style lang="stylus">
  .a-class
    border red solid 2rpx
  .b-class
    margin-right 20rpx
</style>

<style lang="stylus" scoped>
  .other-class
    border red solid 2rpx
    
   ...
</style>
復(fù)制代碼
  • 在定義組件上綁定style屬性到一個props屬性上:
<!--P組件ComponentA.vue-->
 <template>
  <div class="container" :style="pStyle">
    ...
  </div>
</template>
復(fù)制代碼
<script>
  export default {
    props: {
      pStyle: {
        type: String,
        default: ''
      }
    }
  }
</script>
復(fù)制代碼
<!--PageB.vue-->
<template>
    <component-a :pStyle="cusComponentAStyle"  />
</template>
復(fù)制代碼
<script>
const cusComponentAStyle = 'border:red solid 2rpx; margin-right:20rpx;'
data () {
    return {
      cusComponentAStyle
    }
  }
</script>
復(fù)制代碼
<style lang="stylus" scoped>
  ...
</style>
復(fù)制代碼

也可以通過定義styleObject,然后通過工具函數(shù)轉(zhuǎn)化為styleString,如下所示:

const bstyle = {
  border: 'red solid 2rpx',
  'margin-right': '20rpx'
}
let arr = []
for (let [key, value] of Object.entries(bstyle)) {
  arr.push(`${key}: ${value}`)
}

const cusComponentAStyle = arr.join('; ')
復(fù)制代碼
  • 當(dāng)然自定義組件確定只會改變某個css樣式,通過pros傳入單個樣式的值,然后通過:style綁定肯定沒問題:
<!--組件ComponentA.vue-->
 <template>
  <div class="container" :style="{'background-color': backgroundColor}">
    ...
  </div>
</template>
復(fù)制代碼
<script>
    export default {
    props: {
      backgroundColor: {
        type: String,
        default: 'yellow'
      }
    }
  }
</script>
復(fù)制代碼
<!-- PageB.vue -->
<template>
    <component-a backgroundColor="red"  />
</template>
復(fù)制代碼

分包加載

package.json修改

  • 升級: "mpvue-loader": "^1.1.2-rc.4" "webpack-mpvue-asset-plugin": "^0.1.1"
  • 新增: "relative": "^3.0.2"

注意事項(xiàng)

  • 1.1.2-rc.5 修復(fù) slot 文件路徑生成錯誤的問題
  • 1.1.x 版本還不是很穩(wěn)定,對穩(wěn)定性要求較高的項(xiàng)目建議暫時使用 1.0.x 版本

移動src/main.js中config相關(guān)內(nèi)容到同級目錄下main.json(新建)中

export default {
  // config: {...} 需要移動
}

復(fù)制代碼

to

{
 "pages": [
   "pages/index/main",
   "pages/logs/main"
  ],
  "subPackages": [
    {
      "root": "pages/packageA",
     "pages": [
       "counter/main"
     ]
   }
 ],
 "window": {...}
}
復(fù)制代碼

webpack 配置配合升級指南

  • 本次升級意在調(diào)整生成文件目錄結(jié)構(gòu),對依賴的文件由原來的寫死絕對路徑該改為相對路徑
  • mpvue-loader@1.1.2-rc.4 依賴 webpack-mpvue-asset-plugin@0.1.0 做依賴資源引用
  • 之前寫在 main.js 中的 config 信息,需要在 main.js 同級目錄下新建 main.json 文件,使用 webapck-copy-plugin copy 到 build 目錄下
  • app.json 中引用的圖片不會自動 copy 到 dist 目錄下 json 配置文件是由 webapck-copy-plugin copy 過去的,不會處理依賴,可以將圖片放到根目錄下 static 目錄下,使用 webapck-copy-plugin copy 過去

build/webpack.base.conf.js

+var CopyWebpackPlugin = require('copy-webpack-plugin')
+var relative = require('relative')

 function resolve (dir) {
   return path.join(__dirname, '..', dir)
 }

-function getEntry (rootSrc, pattern) {
-  var files = glob.sync(path.resolve(rootSrc, pattern))
-  return files.reduce((res, file) => {
-    var info = path.parse(file)
-    var key = info.dir.slice(rootSrc.length + 1) + '/' + info.name
-    res[key] = path.resolve(file)
-    return res
-  }, {})
+function getEntry (rootSrc) {
+  var map = {};
+  glob.sync(rootSrc + '/pages/**/main.js')
+  .forEach(file => {
+    var key = relative(rootSrc, file).replace('.js', '');
+    map[key] = file;
+  })
+   return map;
 }

   plugins: [
-    new MpvuePlugin()
+    new MpvuePlugin(),
+    new CopyWebpackPlugin([{
+      from: '**/*.json',
+      to: 'app.json'
+    }], {
+      context: 'src/'
+    }),
+    new CopyWebpackPlugin([ // 處理 main.json 里面引用的圖片,不要放代碼中引用的圖片
+      {
+        from: path.resolve(__dirname, '../static'),
+        to: path.resolve(__dirname, '../dist/static'),
+        ignore: ['.*']
+      }
+    ])
   ]
 }
復(fù)制代碼

build/webpack.dev.conf.js

module.exports = merge(baseWebpackConfig, {
   devtool: '#source-map',
   output: {
     path: config.build.assetsRoot,
-    filename: utils.assetsPath('js/[name].js'),
-    chunkFilename: utils.assetsPath('js/[id].js')
+    filename: utils.assetsPath('[name].js'),
+    chunkFilename: utils.assetsPath('[id].js')
   },
   plugins: [
     new webpack.DefinePlugin({
    module.exports = merge(baseWebpackConfig, {
     // copy from ./webpack.prod.conf.js
     // extract css into its own file
     new ExtractTextPlugin({
-      filename: utils.assetsPath('css/[name].wxss')
+      filename: utils.assetsPath('[name].wxss')
     }),
    module.exports = merge(baseWebpackConfig, {
       }
     }),
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'vendor',
+      name: 'common/vendor',
       minChunks: function (module, count) {
         // any required modules inside node_modules are extracted to vendor
         return (
        module.exports = merge(baseWebpackConfig, {
       }
     }),
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'manifest',
-      chunks: ['vendor']
+      name: 'common/manifest',
+      chunks: ['common/vendor']
     }),
-    // copy custom static assets
-    new CopyWebpackPlugin([
-      {
-        from: path.resolve(__dirname, '../static'),
-        to: config.build.assetsSubDirectory,
-        ignore: ['.*']
-      }
-    ]),

復(fù)制代碼

build/webpack.prod.conf.js

var webpackConfig = merge(baseWebpackConfig, {
   devtool: config.build.productionSourceMap ? '#source-map' : false,
   output: {
     path: config.build.assetsRoot,
-    filename: utils.assetsPath('js/[name].js'),
-    chunkFilename: utils.assetsPath('js/[id].js')
+    filename: utils.assetsPath('[name].js'),
+    chunkFilename: utils.assetsPath('[id].js')
   },
   plugins: [
    var webpackConfig = merge(baseWebpackConfig, {
     }),
     // extract css into its own file
     new ExtractTextPlugin({
-      // filename: utils.assetsPath('css/[name].[contenthash].css')
-      filename: utils.assetsPath('css/[name].wxss')
+      // filename: utils.assetsPath('[name].[contenthash].css')
+      filename: utils.assetsPath('[name].wxss')
     }),
     // Compress extracted CSS. We are using this plugin so that possible
     // duplicated CSS from different components can be deduped.
    var webpackConfig = merge(baseWebpackConfig, {
     new webpack.HashedModuleIdsPlugin(),
     // split vendor js into its own file
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'vendor',
+      name: 'common/vendor',
       minChunks: function (module, count) {
         // any required modules inside node_modules are extracted to vendor
         return (
     var webpackConfig = merge(baseWebpackConfig, {
     // extract webpack runtime and module manifest to its own file in order to
     // prevent vendor hash from being updated whenever app bundle is updated
     new webpack.optimize.CommonsChunkPlugin({
-      name: 'manifest',
-      chunks: ['vendor']
-    }),
+      name: 'common/manifest',
+      chunks: ['common/vendor']
+    })
-    // copy custom static assets
-    new CopyWebpackPlugin([
-      {
-        from: path.resolve(__dirname, '../static'),
-        to: config.build.assetsSubDirectory,
-        ignore: ['.*']
-      }
-    ])
   ]
 })
復(fù)制代碼

config/index.js

module.exports = {
     env: require('./prod.env'),
     index: path.resolve(__dirname, '../dist/index.html'),
     assetsRoot: path.resolve(__dirname, '../dist'),
-    assetsSubDirectory: 'static', // 不將資源聚合放在 static 目錄下
+    assetsSubDirectory: '',
     assetsPublicPath: '/',
     productionSourceMap: false,
     // Gzip off by default as many popular static hosts such as
@@ -26,7 +26,7 @@ module.exports = {
     port: 8080,
     // 在小程序開發(fā)者工具中不需要自動打開瀏覽器
     autoOpenBrowser: false,
-    assetsSubDirectory: 'static', // 不將資源聚合放在 static 目錄下
+    assetsSubDirectory: '',
     assetsPublicPath: '/',
     proxyTable: {},
     // CSS Sourcemaps off by default because relative paths are "buggy"

復(fù)制代碼


易優(yōu)小程序(企業(yè)版)+靈活api+前后代碼開源 碼云倉庫:starfork
本文地址:http://m.chqfk.com/wxmini/doc/course/24780.html 復(fù)制鏈接 如需定制請聯(lián)系易優(yōu)客服咨詢: 點(diǎn)擊咨詢
在線客服
易小優(yōu)
轉(zhuǎn)人工 ×
亚洲国产精品成人久久动漫| 美女主播大奶子黄瓜插逼| 另类图片亚洲图区第一页| 特黄特猛aaaaaaaaa片| 日韩中文字幕乱码一区| 亚洲国产精品av久久久| 91老熟女连续高潮对白| 高清日本wwwcom| 偷窥中国丰满多毛老熟女| 极品国模sm镣铐调教| 五月婷婷综合大香蕉五月天| 久久精品亚洲国产| 骚白虎插入在线观看| 欧美激情自拍2020| 日本美国亚洲一区二区| 国产大神一区二区三区| 最新天堂一区二区三区| 国产白丝美女在线网站| 国产精品亚洲综合av| 中字av在线一区二区中字| 欧美,日本中文高清视频| 天天大香蕉一区二区三区| 亚洲视频专区在线播放| 草草影院黄色在线观看| 伦人伦xxxx国语对白| 日本黄页网络站免费| 一区二区三区四区不卡在线观看| 中文字幕第一区久久| 999久久久精品精品| 9l九色自拍蝌蚪9l视频| 日本v片 中文字幕| 亚洲天堂网久久av| 国自产拍偷拍福利精品免费观看 | 欧美一级性生活片人与动物| 成人大片精品在线观看| 成人a毛片久久免费播放| 三级黄色亚洲成人av| 天天碰天天摸人人看| 日韩破处精品在线观看| 免费阿v视频在线播放| 97超碰在线视频观看| 爆操嫩逼黑丝袜美女小骚逼| 99精品视频中文字幕| 涩涩漫画网站在线观看| 麻豆一区二区大豆行情| 熟女人妻人妻のhd| 日韩欧美在线一区二区在线| 少妇人妻不满足中文字幕| 亚洲av一区视频在线观看| 亚洲性趣老熟妇高清| av男人天堂精品久久| 国产熟妇一区二区三区av| 国产原创成人av剧情在线播放| 国产精品亚洲综合视频| 人妻天天操avxx| 4个黑人操素人视频网站精品91| 综合另类小说欧美另类图片| 亚洲天堂 校园春色| 粉嫩av入口一区二区三区| 日本大鸡巴乱伦肏屄网| 登录国产黄色一区二区三区| 中国熟妇丰满大乳大屁股| 亚洲天堂av插插插| 人妻不卡一区二区三区| 99精品视频在线观看专区| 亚洲av男人的天堂久久久| 亚洲精品成人a8198| 久久视频十八岁亚洲精品久久视频 | 国产又粗又长又黄视频| 欧美亚洲三级色图网站| 国产精品久久久久白浆| 国产又粗又长又硬又猛又黄的视频| 欧洲av性色在线看| 美女吃鸡巴黑料破处自慰| 国产极品尤物粉嫩泬在线观看| 久久精品视频全部视频在线| 欧美激情性4一级完整版| 色吊最新在线视频免费观看| 亚洲av永久久久久久久蜜桃| 欧美日韩精品久久一区二区三区| 日韩美女精品一在线观看| 亚洲综合欧美熟一区| 成人av影视一区在线观看| 国产精品精品3d动漫| 特黄特色大片观看免费| 亚洲熟妇色在线观看| 亚洲成年人三级电影| 婷婷视频在线观看一区的| 日韩av中文字幕网址| 亚洲精久久久久久久久久久久久| 98资源站精品视频在线观看| 三级经典三级日本三级欧美| 日韩资源在线中文字幕| 99精品视频中文字幕| 伊人情人综合成人久久网小说| 阿v视频在线观看免费播放| 一区二区三区在线 日韩| 韩日一级片中文字幕| 日日夜夜夜操天天干| 中国熟妇丰满大乳大屁股| 亚洲性趣老熟妇高清| 国产1区2区在线视频| 又黑又硬又粗又黄又猛| 欧洲av性色在线看| 中国福利在线黄色片| 日日夜夜狠狠干干亚洲| 蜜桃视频18在线观看| 欧美在线一区日韩国产| 亚洲国产精品美女久久久av| mm在线视频观看免费观看| 亚洲天堂欧美中文字幕| 日韩成人在线电影,| 少妇的激情夜夜爽爽爽爽爽| 99精品私筹模特大尺度视频| 五月婷婷丁香花激情网| 起碰97视频在线播放| 日韩无码成人电影一区二区| 国产成人午夜精品视频| 欧美激情在线观看新地址| 大黑鸡巴 狂插 欧亚小嫩逼| 亚洲激情视频免费在线| 男人爽爽女人的视频一区二区三区| 教资是不是人人都可以考| 亚洲一区 成人在线| 色视频免费在线观看视频| 超碰人人澡人人碰人人| 蜜臀 av一区二区| 9l九色自拍蝌蚪9l视频| 三级三级三级日本99| 亚洲女同性同志熟女女同| 天天操天天射天天综合网| 国产原创成人av剧情在线播放 | 中国熟妇丰满大乳大屁股| 欧美在线观看视频一区五区| 可以直接在线观看的一区| 亚洲精久久久久久久久久久久久 | 91精选视频在线播放| 日韩中文字幕乱码一区| 无套内谢少妇高潮毛片免费看| 国产一级二级三级亚洲| 亚洲综合精品推荐69堂| 久草免费资源视频在线观看| 亚洲国产中文字幕在线视频| 好吊视频一区二 区三区视频| 在线观看高清日韩av| 老师让我插进去69AV| 搡四十路e五十路熟女av| 999久久久精品精品| 99热6在线播放免费| 伊人精品在线大香蕉| 日韩欧美亚洲在线第一页| 一级白丝美女久久久久| 亚洲人成青青操免费观看| av大片在线观看免费| 在线91精品亚洲网站精品成人| 91人妻人澡人人爽人人精品| av国产在线观看网站| 欧美成人a v在线| 亚洲精品免费一二三区| 国产粉嫩粉嫩的在线18观看| 日本老熟妇色狠狠一区| 精品国产污污免费网站aⅴ17| 一区二区中文字幕18| 欧美一级操逼啊啊啊哦哦哦| 欧美丝袜熟女日韩亚洲| 97超级碰最新在线视频| 亚洲国产中文字幕在线视频| 欧美午夜不卡一区二区三区| 人妻久久免费视频中文字幕| 17c在线观看视频国产aa| 人妻中文字幕在线视频免费观看| 4个黑人操素人视频网站精品91| 91久久精一区二区三区大全| 男人天堂在线免费观看| 亚洲综合欧美熟一区| 91表用白丝脚帮我脚交| 色淫骚色色色色色色美女| 欧美中文字幕综合在线| 微拍一区二区在线观看| 日韩国产亚洲在线视频| 亚洲人成亚洲人成在线观看com| 在线免费观看欧美激情视频| 国内精品久久久久久久| 销魂少妇一区二区视频| 久久精品国产亚洲av视瓶| 在线观看懂色精品大神视频| av 在线 麻豆| 亚洲美女乱1区2区3区| 隔壁放荡人妻bd完整版| 熟女免费在线观看视频| 中文字幕黄色av网址| 蜜桃精品在线观看一区| 99久久精品久久久久久清纯| 超碰人妻中文字幕在线| 亚洲情品中文字幕人妻久久久边| 99精品私筹模特大尺度视频| 77777蜜臀精品久久综合| 欧美成人a v在线| 亚洲免费国产在线日韩| 午夜性色福利视频自拍偷拍| 国产精品啊啊啊不要在线观看| 婷婷视频在线观看一区的| 日日躁夜夜躁狠狠久久av| 播放灌醉水嫩大学生国内精品| 2020精品国产自在现线官网| 都市激情校园春色av| 2020中文字幕在线播放| 午夜在线小视频在线观看| 亚洲自偷自拍另类性受不了| 亚洲精品综合视频自拍| 99热6在线播放免费| 亚洲综合一区二区人妻| 久久久999精品在线| 亚洲欧美另类激情综合区动漫| 日韩高清视频在线播放| 嗯嗯嗯啊啊啊不要好爽视频| 国产av人人夜夜澡人人爽下载| 中文字幕一区三区二区国产黄色| 免费在线观看国产成人大片| 天天日天天添天天爽| 黑人熟女一区二区三区| 啪啪男女日韩网站蜜桃| 亚洲人成青青操免费观看| 视频你懂得在线观看| 好的一级毛片免费毛片直播| 国产美女遭高潮免费视频| 久久99精品国产99久久6尤| 天天插天天爱天天日| 成人教育 在线学习| 要看tv在线观看欧美日韩| 老鸭窝在线观看免费观看高清版| 亚洲一区二区视频在线免费观看| 99久久精品国产欧美一区二区| 色片免费在线观看喷水| 亚洲免费国产在线日韩| 妍强被迫伦姧惨叫123| 亚洲欧美 在线视频| 亚洲熟妇av一区二区蜜桃第1集| 婷婷综合尤物精品国产| www国产精品久久久| 久久91精品国产91久久分享| 人人妻人人澡人人爽电台app| 三上悠亚和黑人665番号| 在线视频 国产 日韩 欧美| 东京热av在线播放| 青青草成人免费电影| 日本女护士久久精品| 男人的坤插进女人的屁股里的视频 | 欧美一级黄片视频免费| 操白嫩人妻少妇真爽视频| 午夜精品久久久久久99| 午夜福利久久久国产视频| 97视频人人人人人性| 国产亚洲精品电影aa在线观看| 麻豆精品午夜福利在线| 古典武侠校园春色亚洲| 视频你懂得在线观看| 偷拍亚洲另类图片视频| 爆操嫩逼黑丝袜美女小骚逼| 久久这里精品视频3| 欧美成年性精品三级网站| 天天操天天射天天综合网| 国产1区2区在线视频| 欧美精品第5页在线观看视频| 风间由美亚洲一区二区三区| 漂亮的人妻不敢呻吟被中出| 搡四十路e五十路熟女av| 免费二区三区四区在线观看| 人妻熟一区二区三区四区不卡| 大香蕉人妻少妇av| 91麻豆精品国产乱码久久久久久| www国产精品久久久久久| 成人黄色大全在线观看| 免费在线观看网址你懂的| 亚洲欧美另类图片88| 久久草大香蕉在线视频| 亚洲人成亚洲人成在线观看com | 日韩高清av一区二区三区| 人妻久久免费视频中文字幕| 麻豆一区二区大豆行情| 在线中文字幕综合一区| www国产精品久久久久久| 自拍偷拍亚洲黄色照片| 国产又粗又长又黄视频| 青青青青青青青青草青青| 久精彩视频免费观看| 国产高清在线免费视频| 天天日天天色天天摸| 9色自拍视频在线观看| 国内精品久久久久久久999| 日韩破处精品在线观看| 国内精品久久久久精品爽爽| 欧美视频一区二区三区在线观看 | 91 chinese 在线播放| 欧美激情在线观看新地址| 美女极品美女福利视频在线| 国产三级av在线免费观看| 国产国语露脸在线视频播放| 亚洲国产欧美日韩国产| 日韩国产精品高清中文在线| 女人扒开逼逼让男人操| 青青视频青青成人免费| 国产传媒中文字幕在线| 亚洲欧美国产其他二区| 国产美女主播丝袜高潮白浆| 青娱乐精品视频在线免费观看| 163黄页网在线观看,| 在线看片1024你懂得| 亚洲图片一区偷拍自拍| 啊啊啊操死我轮操视频免费| 国产国产午夜全部视频| 青青草精品视频在线免费观看| 亚洲主要位于五带中的什么带?| 日韩毛片基地免费看| 久久一区二区三区杨幂| 亚洲精品超熟女av| 国产mm视频在线观看| 国产原创成人av剧情在线播放| 在线观看高清日韩av| 91久久综合九色综合欧美98| 欧美视频在线观看18| 熟女人妻人妻のhd| 日本黄色操碌缬巴| 日韩资源在线中文字幕| 亚洲五区四区欧美视频| 性感丝袜美女诱惑大鸡吧| 久久久久18精品国产乱码78m| 人妻熟一区二区三区四区不卡| 国产成人一区二区三区久| 欧美视频一区二区三区在线观看| 天天色天天干天天操| 丝袜美图一区二区三区| 99久久精品国产欧美一区二区 | 国产美女遭高潮免费视频| 国产综合av免费观看| 4455vw在线观看| 天天碰天天摸人人看| 国产av综合av国产精品| 港台经典一级成人免费av| 亚洲 网友 在线 观看| 999在线视频一区二区三区| 玩弄放荡人妻少妇精品| 另类图片亚洲图区第一页| 欧美va久久久噜噜噜久久| 999热这里只有精品视频| 日本免费啪啪啪啪啪啪啪啪啪啪| 91精品在线播放hd| 欧美亚洲国产成人免费在线| 国产在线视频国产资源| 日韩精品高清免费视频| av乱亚洲一区二区三区| 成人一级黄色片免费看| 国产精品亚洲综合视频| 亚洲一区二区成人综合| 91麻豆精品国产乱码久久久久久 | 1717精品视频在线观看| 天天日天天添天天爽| 五月天免费在线观看| 亚洲精品专区一区二区| 亚洲天堂一二三四在线播放| 欧美一区二区三区乱轮| 欧美色一区二区三区在线观看| 久久这里精品视频3| 午夜老司机福利一二三区 | 欧美一区二区三区乱轮| 亚洲精品在线观看高清资源| 成人大片精品在线观看| 欧美丰满熟妇高潮xxxx| 天天色天天情天天透| 奇米一区二区三区视频在线观看| 91人妻人澡人人爽人人精品| 日本av毛片在线播放| 日本一区二区不卡电影| 黄色污污污网站免费观看| 部长侵犯人妻一区二区三区| 亚洲高清中文字幕在线的| 国产精品午夜在线观看| 三级三级三级日本99| 亚洲另类熟女国产精品老| 午夜剧场欧美一区二区| 骚白虎插入在线观看| 四川操bb操bb操b| 日韩最新视频在线播放| 人妻熟妇丰满不伦一区二区三区| 狠狠操操操操操操操操操操| 日本老熟妇色狠狠一区| 亚洲精品国偷自产久色| ai给视频自动加字幕| 91精品国产综合久久婷婷香蕉| 天天天天拍天天天天天天| 天堂av在线中文在线新版| 91人妻人人做人人爽九色全集| 日韩加勒比东京热二区| 亚洲情品中文字幕人妻久久久边| 97国产婷婷在线观看| 国产综合av免费观看| 男人插女人逼app| 国产91精品久久久久高潮| 骚白虎插入在线观看| 色片免费在线观看喷水| 99,九九,久久精品| 老男人久久青草av高清| 日韩欧美在线不卡一区二区三区| 免费无码专区毛片高潮喷水| 亚洲一区 成人在线| 丝袜美图一区二区三区| 日本av都是真做吗| 午夜偷拍福利小视频| 东方av在线免费进入| 98资源站精品视频在线观看| 亚洲图色熟女五月天| 女人张开双腿让男人捅视频 | 婷婷综合尤物精品国产| 蜜桃av在线网址观看| 国产又粗又长又黄视频 | 亚洲三级伦理在线播放| 高清日本欧美亚洲视频| 欧美亚洲国产成人免费在线| 四川熟女a一区二区三区| 亚洲欧美在线x视频| 99久久精品美女高潮喷水| 免费在线国产观看av| 98视频在线免费观看| 亚洲,自拍,中文,另类| 爆操性感美女性色av| 97人妻人人做人碰人人爽一| 黄色中文字幕在线观看| 成人午夜伦理在线观看| 日韩av中文字幕乱码| 变态另类97人妻av| 日韩毛片基地免费看| 99,九九,久久精品| 亚洲天堂欧美中文字幕| 欧美大长腿美女抽插网站| 好男人资源在线视频观看社区| 播放灌醉水嫩大学生国内精品| 色片免费在线观看喷水| 亚洲精品乱码久久观看网| 天天日天天色天天摸| av男人天堂精品久久| 啪啪男女日韩网站蜜桃| 欧美色一区二区三区在线观看| 欧美色一区二区三区在线观看| 日韩精品一区二区三区中文精| 国产美女视频在线播放| heyzo久久综合色88| 大奶子美女免费操逼视频| 免费二区三区四区在线观看| 日本免费播放一区二区三区| 午夜精品久久99蜜桃| 骚白虎插入在线观看| 亚洲中文有码一区二区| 顶级销魂极品少妇在线观看| 日韩中文字幕免费大片| 97超级碰最新在线视频| 青青青青青青青青草青青| 欧美图色 亚洲图色| 五月激情丁香久久亚洲| 白峰美羽在线观看av| 国产午夜福利精品久久不卡 | 亚洲av性色在线观看黄色| 亚洲自偷自拍另类性受不了| 4个黑人操素人视频网站精品91| 亚洲日本一区二区嫩草| 亚洲自偷自拍另类18p| 狠狠躁日日躁夜夜躁2| 欧美精品黑人粗大破除| 国产麻豆在线av| 玩弄放荡人妻少妇精品| 制服丝袜诱惑综合网| 蜜桃一区二区17c| 99精品热视频在线观看| 亚洲天堂av插插插| 成人麻豆免费视频精品区| 成人大片免费看45分钟| 国自产拍偷拍福利精品免费观看| 成人3d动漫一区二区三区91| 亚洲视频 中文字幕 人妻| 香蕉久久av一区二区三区四区| 国产精品啊啊啊不要在线观看| 91 chinese 在线播放| 一区二区三区国产精选在线播放| 教资是不是人人都可以考| 午夜在线小视频在线观看| 97人妻人人做人碰人人爽一| 日韩资源在线中文字幕| 天天碰天天摸人人看| 爆操嫩逼黑丝袜美女小骚逼| 久久精品国产二区AV无码 | 色欲天天天久久久综合| 人人妻人人妻人人妻人人妻人人人| 日韩欧美亚洲一区第一| 免费观看黄色韩日av| 日韩毛片基地免费看| 亚洲成人一区二区三区av| 午夜精品久久久久久99| 国产熟妇一区二区三区av| 新婚人妻聚会被中出| 熟妇人妻va精品中文字幕九色| 性感的人妻在线观看| 掀开奶罩边吃边摸下娇喘视频| 综合另类小说欧美另类图片| 北条麻妃制服丝袜在线播放| 免费在线观看视频色播| 国产中文字幕在线免费播放| 97国产在线精品观看| 国产黑色丝袜视频在线观看下| 公一区二区三区高清99| av中文字幕在线播放| r人人妻人人澡人人爽| 91九色porny国产探花| 教资是不是人人都可以考| mm在线视频观看免费观看| Av资源站中文字幕| 亚洲视频专区在线播放| 99久久免费国产特黄| 精精品久久久久久琪琪| 91久久精品视频91| 91jk麻豆美女丝袜诱惑| 日本中文字幕人妻一区二区| 富二代av一区二区| 午夜福利免费福利视频| 日韩av一区二区三区久久| 天天日天天添天天爽| 五月天丁香婷久久爱| 二根鸡巴日一个穴视频| 色综合久久加勒比高清剧情| 丰满人妻被猛烈进入中文字幕四川| 亚洲三级伦理在线播放| 亚洲精品免费一二三区| mm在线视频观看免费观看| 不卡黄色免费在线观看| 免费阿v视频在线播放| 一级白丝美女久久久久| 亚洲在线一区二区三区免费| 熟女一区二区三区四区五区视频| 2018免费天天干夜夜操| 五月天中文字幕剧情在线| 99久久精品免费看蜜桃的推荐词| 在线视频 国产 日韩 欧美| 伊人青青青在线观看| 视频在线观看免费99| 91精品综合国产熟女| 欧美xxxx视频在线| 久草视频免费在线视频观看| av网址大全在线播放| 四川熟女a一区二区三区| 99精品私筹模特大尺度视频| 鸡巴操进我的小穴欧美国产| 99久久精品美女高潮喷水| 日本老太婆老熟妇av| 国产美女主播丝袜高潮白浆| 日本很黄很黄的动态视频| 99,九九,久久精品| 天天日天天透天天操| 国产一区精品在线观看免费| 97超碰在线come| 亚洲av性色在线观看黄色| 青青操在线视频精品| 少妇的激情夜夜爽爽爽爽爽| 91精品国产国语自拈产在| av手机在线免费播放| 精品一区二区三区在线免费播放| 欧美日韩国产综合不卡| 亚洲人成青青操免费观看| 91精品1080部在线播放| 在线精品亚洲区一区二区| 天天操天天操天天干天天| 久久久国产视频91| 天堂av在线中文在线新版| 夜夜操夜夜操夜夜爽| 亚洲精品专区一区二区| 午夜精品久久久久久99| 欧美黑人性色黄在线视频| 天天色天天干天天操| 亚洲偷偷自拍视频网| 熟妇人妻va精品中文字幕九色| 日韩无码成人电影一区二区| 人妻中文字幕不卡av观看| 青青草精品视频在线免费观看| 91精彩刺激对白露脸偷拍| 草逼美女逼话多的视频出水| 丰满雪白人妻人爽16av精品| 88888欧美精品久久久| 亚洲永久av午夜福利| 国产精品一品二区三区日韩| 国产av综合av国产精品| 97超碰在线cao| 要看tv在线观看欧美日韩 | 麻豆一区二区大豆行情| 伊人大香线蕉亚洲五月天| 在线小视频,你懂的| 国产午夜在线免费视频| 欧美 亚洲 激情 自拍| 超碰人妻中文字幕在线| 久久91精品国产91久久分享| av精选一区二区久久| 国产精品爽黄69天堂ai蜜乳| 最新日韩成人毛片在线| 男人把女人捅爽动漫| 五月天亚洲精品综合网| 最大最全av中文字幕网| 亚洲情品中文字幕人妻久久久边| 亚洲无码成人福利视频| 男女啪啪啪网站入口| 久久久久国产一毛片高清| 天天插天天爱天天日| 欧美激情在线观看新地址| 国产视频精品在线免费观看| 久久精品国产亚洲a| 欧美丝袜熟女日韩亚洲| 国产c片免费观看| 精品熟女后入一区二区三区| 久久一区二区三区杨幂| 欧美,日本中文高清视频| 伊人情人综合成人久久网小说| 日韩成人在线电影,| 日本高清插阴视频免费| 色片免费在线观看喷水| 亚洲 网友 在线 观看| 午夜剧场欧美一区二区| 未满18禁止入内免费视频| 亚洲人成小说网站色在线| av男人天堂精品久久| av中文字幕高清在线| 午夜精选视频在线观看| 免费二区三区四区在线观看| 亚洲精品成人原创视频| 九月丁香婷婷中文字幕| 国产乱人伦av麻豆网| 国产剧情swag在线观看| 美女吃鸡巴黑料破处自慰| 久久精品国产亚洲av视瓶 | 亚洲日本一区二区嫩草| 亚洲综合一区二区人妻| 99久久香蕉国产线看| 欧美大黑硬鸡巴操骚肥湿逼| 免费观看日本黄页网站| 熟妇人妻va精品中文字幕九色| 国产午夜在线免费视频| 天天日天天色天天搞| 日本老太婆老熟妇av| 男人的坤插进女人的屁股里的视频| 日韩高清av一区二区三区| 日本老熟妇色狠狠一区| 亚洲综合精品推荐69堂| julia 人妻中文字幕| 青青操在线视频精品| 精品成人午夜免费看| 亚洲av欧美av在线播放| 99国国视频在线播放| 适合黄黑皮的显白发色| 99热这里只有的精品666| 天天操夜夜爽夜夜操| av色哟哟国产精品| 久久精品视频全部视频在线| 三级经典三级日本三级欧美| 国产剧情星空无限传媒| 成人性生交大免费三人| 亚洲久久久久久久久久久久久久 | 亚洲中文字幕av在线播放| 亚洲欧美另类专区第一页| 久久91精品国产91久久分享| 97久久精品熟女超碰| 26uuu天堂在线青青在线视频| 亚洲 欧洲 国产 麻豆| 国产a v一区二区三区香蕉| 免费成视频人免费91| 在线中文字幕综合一区| 啪啪小视频免费网站| 91人妻人人做人人爽九色全集| 亚洲av好看xx站| 日日天天干夜夜夜操狠狠干| 久草视频免费在线视频观看| 三级经典三级日本三级欧美| 操女人下阴黄色一级视频| 性感美女一区二区美女| 98资源站精品视频在线观看| 9797人人妻人人澡| 国产视频在线精品视频| 97超碰在线come| 久久视频十八岁亚洲精品久久视频| 国产成人午夜精品视频| 天天日天天色天天摸| 欧美亚洲国产成人免费在线| 天天色天天干天天操| 日本福利片免费在线播放| 亚洲无码成人福利视频| 人人妻人人妻人人妻人人妻人人人| 新婚人妻聚会被中出| 免费二区三区四区在线观看| 亚洲少妇av在线播放| 午夜福利久久久国产视频| 亚洲主要位于五带中的什么带? | 思思91精品国产综合在线| 4455vw在线观看| 天堂av在线中文在线新版| 亚洲国产图片小说一区二区| 一区二区三区在线中国| 大秀视频一区二区三区| 亚洲天堂欧美中文字幕| 亚洲主要位于五带中的什么带?| 163黄页网在线观看,| 伊人大香线蕉亚洲五月天| 欧美一级操逼啊啊啊哦哦哦| 国产精品亚洲综合视频| 青青草原x全国在线观看| 日本中文字幕人妻一区二区| 国产精品爽黄69天堂ai蜜乳| 在线中文字幕综合一区| 粉嫩av入口一区二区三区| 免费一级黄色片麻豆系列| 欧美 日韩 中文 字幕| 特黄特猛aaaaaaaaa片| 夭天曰天天躁东京热天天摸| 免费观看视频一区,二区,三区 | 青青视频青青成人免费| 亚洲三级伦理在线播放| 国产成人一区二区三区久| 国产精品啪啪啪免费网站| 成人一级黄色片免费看| 91国产精品久久久久麻豆| 一区二区三区欧美高清| 国产免费一级高清淫日本片| 青青操视频在线免费| 精品少妇人妻av免费久久胖妇| 91九色porny国产探花| 香蕉久久av一区二区三区四区| 免费观看视频一区,二区,三区| 熟女人妻av中文字幕| 美女的逼让男人桶的视频| r人人妻人人澡人人爽| 亚洲国产精品av久久久| 黑人系列哪个最猛番号| av网址在线观看日韩| yy111111少妇蜜桃| 国产喷水在线免费观看| av网站资源在线观看| 麻豆精品午夜福利在线| 国产精品自拍亚洲春色| 日韩精品在线播放视频成年人| 亚洲欧美日韩不卡人妻中文字幕| 老鸭窝在线观看免费观看高清版| 国产传媒网址在线观看| 成年大片40分钟免费视频播放| 天天操天天日天天啪| 国产精品一品二区三区日韩| 亚洲欧美日韩偷窥自拍| heyzo久久综合色88| 91福利精品一区二区| 综合久久天天搞天天]| 超碰超碰超碰超碰超碰情侣| 亚洲真人性在线观看| 未满18禁止入内免费视频| 天天干夜夜操日日操| 亚洲视频专区在线播放| 亚洲天堂都市激情av| 三上悠亚和黑人665番号| 天天色天天爱天天舔| 日韩一区二区视频在线看| 97人妻人人做人碰人人爽一| 高清日本欧美亚洲视频| 亚洲天堂网久久av| 无套内谢少妇高潮毛片免费看| 97超碰在线cao| 久久亚洲伊人99精品影院| 最大最全av中文字幕网| 亚洲欧美国产其他二区| 草逼美女逼话多的视频出水| 日本系列变态另类一区二区三区| 大香蕉大香蕉在线播放| 日本老熟妇色狠狠一区| 亚洲久久久久久久久久久久久久| 亚洲欧美精品tv久久久久久久久| 午夜性色福利视频自拍偷拍| 黄色av日韩免费在线观看| 蜜桃aⅴ噜噜一区二区三区网址 | 东游记中文字幕版哪里可以看到| 精品一区二区三区大全| 国产av人人夜夜澡人人爽下载| 国产半推半就精品强推视频| 久久99精品国产99久久6尤| 天天睡天天摸天天添天天日天天射| 国产亚洲精品天堂在线观看| 国产av在线一区二区| 草逼美女逼话多的视频出水| 日本日本熟妇中文在线视频| 国产c片免费观看| 国产欧美精品va在线观看| 狂野黑人性猛交xxxxxx| 欧美福利专区一区二区三区| 国产一级免费黄色录像片| 91人妻人人做人人爽九色全集 | 手机视频在线观看99精品视频| 国产美女遭高潮免费视频| ai给视频自动加字幕| Av资源站中文字幕| av网址大全在线播放| 麻豆一区二区大豆行情| 亚洲av欧美av在线播放| 99国国视频在线播放| 亚洲天堂av插插插| 中国福利在线黄色片| 部长侵犯人妻一区二区三区| 久草免费资源视频在线观看| 少妇的激情夜夜爽爽爽爽爽| 欧美精品日韩第一页| 日本av毛片在线播放| 天天色天天操综合网| 熟女人妻av中文字幕| 亚洲日产av一区二区在线| 亚洲中文字幕在线成人| 国产又粗又猛又爽又色视频| 天天色天天爱天天日| 免费熟女精品一区二区三区| 国内精品久久久久久久| 91国内精品视频在线| 日本少妇人妻久久中文| 天天日天天色天天摸| 色片免费在线观看喷水| 自拍偷拍 中文字幕 日韩| 免费观看黄色韩日av| 成人午夜伦理在线观看| 国产精品爽黄69天堂ai蜜乳| 性色av一区二区三区观看| 亚洲精品综合视频自拍| 18人妻人碰人人做人人爽| 天天色天天爱天天日| 欧美丰满熟妇高潮xxxx| 一区二区三区四区免费福利视频| 国产午夜在线免费视频| 亚洲精品 国产成人| yy111111少妇蜜桃| 亚洲无码精品中的精品| 亚洲av男人的天堂久久久| 青青草青青草成人免费公开| 欧美亚洲综合偷拍另类| 97超碰在线视频观看| 午夜偷拍福利小视频| 欧美综合一二区在线| 欧美 激情 另类 自拍| 国产乱人伦av麻豆网| 亚洲精品**不卡在线播he| 成人18禁视频网站在线看| 99国国视频在线播放| 国模视频写真一区二区| 6080日韩伦理片| 91麻豆精品一二三区在线成人| 97人妻碰碰碰久久久| 国产av综合av国产精品| 狠狠操 在线视频 轻轻草 | 亚洲视频综合在线播放| 国内精品久久久久久久999| 国产av一区二区三区天堂| 91精品久久久久久婷婷高清| 人人玩精品人妻少妇性性色| 亚洲中文字幕在线成人| 神乃麻美三级在线观看视频| 亚洲精品1234区在线看| 亚洲三级这里只有精品| 91表用白丝脚帮我脚交| 少妇人妻中文字幕专区视频| 亚洲av男人的天堂久久久| yy111111少妇蜜桃| 色淫骚色色色色色色美女| 久操av在线免费观看| 93乱子伦国产乱子伦| 97精品在线观看视频| 日韩国产精品高清中文在线| 港台经典一级成人免费av| 欧美日韩无卡一二三区| 在线免费看亚洲精品少妇69式| 亚洲国产aⅴ成人精品无吗| 亚洲天堂一二三四在线播放| 精品成人午夜免费看| 欧美精品乱码久久久久久| 在线视频 国产 日韩 欧美| 青青操成人免费在线视频| 中文字幕 日韩在线播放| 人妻熟女一区二区aⅴ佐佐木明希| 亚洲人体艺术二区三区视频| 欧美福利专区一区二区三区| 男人天堂地址在线播放| 国产女人露脸高潮对白视频| 成人大片精品在线观看| 国产精品黄色自拍视频| 蜜桃视频18在线观看| 亚洲无码精品中的精品| 一区二区三区在线 日韩| 激情综合网激情俺她去| 99国产精品免费视频观看a| 在线高清视频你懂得| 韩国美女主播福利视频| 国产喷水在线免费观看| 久久精品国产亚洲a| 国产专区视频在线观看免费| 中文字幕一区三区二区国产黄色| 色就色欧美亚洲αv| 性感美女一区二区美女| 亚洲熟妇av一区二区蜜桃第1集| 免费在线观看网址你懂的| 亚洲伊人av 综合福利| 亚洲精品超熟女av| 黄色污污污网站免费观看| 美女吃鸡巴黑料破处自慰| 四川熟女a一区二区三区| 寂寞少妇一区二区三区| 欧美色一区二区三区在线观看| av乱亚洲一区二区三区 | 欧美熟妇亚洲中文不卡少妇 | 男人天堂在线免费观看| 97免费人妻超碰97在线| 奇米一区二区三区视频在线观看| 在线视频你懂的视频| 老男人久久青草av高清| 91人妻人澡人人爽人人精品 | 欧美久久久久亚洲综合| 黄色成人激情福利在线影院| 亚洲小说区图片另类春色| 久久一区二区三区杨幂| 日日天天干夜夜夜操狠狠干| 夜夜操夜夜操夜夜爽| 登录国产黄色一区二区三区| 操人妻一区二区三区| 亚洲国产aⅴ成人精品无吗| 日本 久久久 香蕉| 欧美熟妇亚洲中文不卡少妇| 超碰超碰超碰超碰超碰情侣| 2020精品国产自在现线官网 | 亚洲三级这里只有精品| 在线91精品亚洲网站精品成人| 精品视频在线观看久久| 天天操天天色天天天| 欧美熟妇亚洲中文不卡少妇 | 欧美精品日韩第一页| 亚洲主要位于五带中的什么带?| 99国国视频在线播放| 欧美激情自拍2020| 可以直接在线观看的一区| 日日爽夜夜爽夜夜爽精品视频| 狠狠干狠狠操五月天| 三级经典三级日本三级欧美| 国产一区精品在线观看免费| 99国国视频在线播放| 日韩欧美在线一区二区在线| 91老熟女连续高潮对白| 1717精品视频在线观看| 亚洲av日韩一区二区三区四区| 男人把女人捅爽动漫| 五月激情四射丁香婷婷激情四射| 91麻豆精品一二三区在线成人| av影视在线免费观看| 九月丁香婷婷中文字幕| 国产高清在线免费视频| 免费av区在线观看| 青青草精品视频在线免费观看| 老鸭窝最新网址在线| 无人码一区二区三区视频| yy111111少妇蜜桃| 天天操天天射天天综合网| 国产精品久久久久精品蜜月| 掀开奶罩边吃边摸下娇喘视频| 亚洲无码精品中的精品| 成人a毛片久久免费播放| 成人一级黄色片免费看| 免费av区在线观看| 日韩中文字幕乱码一区| 伊人精品在线大香蕉| 亚洲天堂精品区三区二区一区| 亚洲国产精品日日夜夜| 亚洲国产日韩欧美高清片vr| 一级白丝美女久久久久| 要看tv在线观看欧美日韩 | 人妻熟一区二区三区四区不卡| av乱亚洲一区二区三区| 一级黄色片美女吃春满| 最大最全av中文字幕网| 欧美 日韩 中文 字幕| 人妻少妇偷人视频一| 成人教育 在线学习| 国自产拍偷拍福利精品免费观看| 91精品人妻中文字幕| av色哟哟国产精品| 国产情侣激情在线对白| 国产九九视频在线观看| 青草视频在在线成人av| 播放灌醉水嫩大学生国内精品| 中文字幕一区三区二区国产黄色| 91高清免费观看在线| 最新天堂一区二区三区| 天天插天天色天天透| 久久人人爽人人爽人人亚洲| 午夜网在线观看视频| 高清日本欧美亚洲视频| 操日本裸体美女骚逼| 伊人大香线蕉亚洲五月天| 天天扣天天日天天摸| 97偷偷碰在线视频| 日韩高清av一区二区三区| 国产九九视频在线观看| 国产精品福利免费视频不卡| av影视在线免费观看| 女人样男人用大鸡巴操她的逼逼| 99精品私筹模特大尺度视频| 亚洲一区二区成人综合| 中文字幕一区三区二区国产黄色| 可以免费看的黄页视频| 99国产精品免费视频观看a| 成人教育 在线学习| 风间由美亚洲一区二区三区| 亚洲主要位于五带中的什么带? | 国产精品啪啪啪免费网站| 色婷婷久久久久av| 极品人妻vide0sss人妻| 中文字幕永久在线一区二区| 亚洲小说区图片另类春色| 亚洲中文有码一区二区| 亚洲国产精品美女久久久av| 三级三级三级日本99| 香蕉影视在线观看av最新| 天天天天拍天天天天天天| 伊人情人综合成人久久网小说| 国产盗摄女子私密保健视频| 亚洲精品 国产成人| 成人黄色大全在线观看| 99久久精品久久久久久清纯| 国产精品自拍亚洲春色| 搡四十路e五十路熟女av| 人人妻人人妻人人妻人人妻人人人| 色吊最新在线视频免费观看| 四川操bb操bb操b| 国产欧美日韩经典一区| 韩国一区二区三区在线观看| 欧美综合一二区在线| 日韩一区二区视频在线看| 最新天堂一区二区三区| 国产一级二级三级亚洲| 成人大片精品在线观看| 91 chinese 在线播放| 综合另类小说欧美另类图片| 欧美图色 亚洲图色| 日本女护士久久精品| 亚洲一区二区综合网| 精品午夜国产福利观看| 操女人下阴黄色一级视频| 美女的逼让男人桶的视频| 欧美精品第5页在线观看视频| 黑人系列哪个最猛番号| 在线免费观看欧美激情视频| 大香蕉97精品一区二区三区| 最新日韩成人毛片在线| 免费在线国产观看av| 综合另类小说欧美另类图片| 97国产婷婷在线观看| 日本成人午夜电影视频| 日韩三级中文字幕熟女| 亚洲欧洲美洲无码在线| 欧美1234不卡视频| 男人天堂新在线电影| 国产成人午夜精品视频| 91久色porny视频在线| 操女人下阴黄色一级视频| 91色porny 在线播放| 欧美激情自拍2020| 九九热在线这里只有精品| 亚洲成年人三级电影| 午夜福利久久久国产视频| 亚洲国产成人在线观看网址| 日韩成人综艺在线播放| 国产伦精品三区精品国偷自产在线 | 色噜噜在线综合亚洲欧美| 天天色天天操综合网| 微拍一区二区在线观看| 大肉大捧一进一出免费视频网址 | 成人一级黄色片免费看| 免费在线观看国产成人大片| 可以免费看啪啪啪的网站| 国产熟妇一区二区三区av | 在线手机免费观看视频| 狠狠躁日日躁夜夜躁视频| 美女草草影院在线观看视频| 亚洲精品超熟女av| 人妻 日韩精品免费| 国产av大全网站天堂| 神乃麻美三级在线观看视频| 成人性生交大免费三人| 掀开奶罩边吃边摸下娇喘视频| 久久久国产视频91| av中文字幕高清在线| 国产精品福利免费视频不卡| 销魂少妇一区二区视频| 欧美在线观看视频一区五区| 2022AV天堂免费在线观看| 日本av毛片在线播放| 日韩av在线区二区| 中文字幕不卡av在线播放| 国产精品午夜在线观看| 天堂网作爱视频在线播放| av大片在线观看免费| 欧美日韩精品久久一区二区三区| 国产成人午夜精品视频| 天天插天天爱天天日| 久草视频免费在线视频观看| 午夜精选视频在线观看| 婷婷爱在线视频精品| 国产绿帽人妻精品系列| 熟女吧国产精品一区二区三区| 亚洲性趣老熟妇高清| 欧美av色香蕉一区二区小说| 国产成人一区二区三区久| 999在线视频一区二区三区| 亚洲天堂都市激情av| 2020中文字幕在线播放| 国产女人露脸高潮对白视频| 涩涩漫画网站在线观看| 天天日天天日天天日天天干| 97超级碰最新在线视频| 自拍分享国产亚洲欧美| 十分钟在线观看视频| 亚洲成av中文字幕| 999在线精品视频观看| 性感的人妻在线观看| 国产视频在线精品视频| 91表用白丝脚帮我脚交| yy111111少妇蜜桃| 久草免费资源视频在线观看 | 欧美激情性4一级完整版| 一区二区三区四区免费福利视频| 成人麻豆免费视频精品区| 日韩高清av一区二区三区| 一级黄色片美女吃春满| 熟女人妻人妻のhd| 色淫骚色色色色色色美女| 人人妻在线视频97| 在线视频你懂的视频| 亚洲精品 国产成人| 丰满的女教师bd视频| 四川熟女a一区二区三区| 一区二区三区欧美日韩电影| 欧美最猛黑人xxxx黑人猛交文| 精品人妻欧美一区二区| 亚洲另类欧美在线观看| 少妇高潮尖叫久久久久| 隔壁老王国产在线观看| 免费观看视频一区二区三区| 国产黄色大片在线免费观看| 污污污视频在线观看91| av色哟哟国产精品| 涩涩网站在线观看视频| 欧美福利专区一区二区三区| 亚洲国产成人在线观看网址| 亚洲图片,自拍偷拍网| 亚洲一区二区成人综合| 婷婷av一区二区三区7| 免费人妻av一区二区| 干风骚美女av在线| 日本性少妇xxxx| 精品欧美一亚洲精品午夜| 亚洲永久av午夜福利| 视频在线播放一区二区| 成人性生交大免费三人| 丝袜制服 亚洲 国产 91| 日本黄页网络站免费| 91在线免费观看成人| 伦人伦xxxx国语对白| 日本性感黑丝美女一区二区| av在线手机免费观看| 日韩无码成人电影一区二区| 亚洲va欧美va人人爽午夜| 日本a爱视频二区三区| 天天射天天日天天干天天舔| 欧美一区二区蜜桃视频| 日本日本熟妇中文在线视频| 在线观看小视频国产| 亚洲国产久久久久久| 欧美午夜不卡一区二区三区| 亚洲伊人av 综合福利| 亚洲精品免费一二三区| 午夜福利精品视频在线观看| 青青草精品视频在线免费观看| 日本成人午夜电影视频| 91久色porny视频在线| 欧美一级黄片视频免费| 大鸡巴操的好爽好舒服啊视频| 日本少妇人妻久久中文| 国产精品久久久久精品蜜月| 青青热久免费精品视频21| 瑟瑟的网站在线观看| 部长侵犯人妻一区二区三区| 一区二区三区极品人妻| 午夜在线看1000集|