国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

微前端框架實(shí)現(xiàn)原理

共 32672字,需瀏覽 66分鐘

 ·

2021-09-12 08:24

本文適合對(duì)微前端感興趣、以及想深入微前端原理學(xué)習(xí)的小伙伴閱讀。

歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~

作者:廣東靚仔

一、前言

本文轉(zhuǎn)載于掘金:

https://juejin.cn/post/7004661323124441102

業(yè)界微前端的實(shí)現(xiàn)方案有挺多:
1、qiankun,icestark 自己實(shí)現(xiàn) JS 及樣式隔離
2、emp,Webpack 5 Module Federation(聯(lián)邦模塊)方案
3、iframe 、WebComponent 等方案,瀏覽器原生隔離,但存在一些問(wèn)題

這么多實(shí)現(xiàn)方案解決的場(chǎng)景問(wèn)題還是分為兩類(lèi):

  • 單實(shí)例:當(dāng)前頁(yè)面只存在一個(gè)子應(yīng)用,一般使用 qiankun 就行

  • 多實(shí)例:當(dāng)前頁(yè)面存在多個(gè)子應(yīng)用,可以使用瀏覽器原生隔離方案,比如 iframe 或者 WebComponent 這些

瀏覽器原生隔離方案不足:
    iframe 最大的特性就是提供了瀏覽器原生的硬隔離方案,不論是樣式隔離、js 隔離這類(lèi)問(wèn)題統(tǒng)統(tǒng)都能被完美解決。但他的最大問(wèn)題也在于他的隔離性無(wú)法被突破,導(dǎo)致應(yīng)用間上下文無(wú)法被共享,隨之帶來(lái)的開(kāi)發(fā)體驗(yàn)、產(chǎn)品體驗(yàn)的問(wèn)題。
Iframe不足-推薦文章:https://www.yuque.com/kuitos/gky7yw/gesexv
本文的實(shí)現(xiàn)方案和 qiankun 一致,但是其中涉及到的功能及原理方面的東西都是通用的,你換個(gè)實(shí)現(xiàn)方案也需要這些。

二、前置工作

    在正式開(kāi)始之前,我們需要搭建一下開(kāi)發(fā)環(huán)境,這邊大家可以任意選擇主 / 子應(yīng)用的技術(shù)棧,比如說(shuō)主應(yīng)用用 React,子應(yīng)用用 Vue,自行選擇即可。每個(gè)應(yīng)用用對(duì)應(yīng)的腳手架工具初始化項(xiàng)目就行,這邊就不帶著大家初始化項(xiàng)目了。記得如果是 React 項(xiàng)目的話(huà),需要另外再執(zhí)行一次 yarn eject
     例子中主應(yīng)用為 React,子應(yīng)用為 Vue,最終我們生成的目錄結(jié)構(gòu)大致如下:


三、正文

  主應(yīng)用:負(fù)責(zé)整體布局以及子應(yīng)用的配置及注冊(cè)這類(lèi)內(nèi)容。


應(yīng)用注冊(cè)

在有了主應(yīng)用之后,我們需要先在主應(yīng)用中注冊(cè)子應(yīng)用的信息,內(nèi)容包含以下幾塊:

  • name:子應(yīng)用名詞

  • entry:子應(yīng)用的資源入口

  • container:主應(yīng)用渲染子應(yīng)用的節(jié)點(diǎn)

  • activeRule:在哪些路由下渲染該子應(yīng)用

其實(shí)這些信息和我們?cè)陧?xiàng)目中注冊(cè)路由很像,entry 可以看做需要渲染的組件,container 可以看做路由渲染的節(jié)點(diǎn),activeRule 可以看做如何匹配路由的規(guī)則。

接下來(lái)我們先來(lái)實(shí)現(xiàn)這個(gè)注冊(cè)子應(yīng)用的函數(shù):

// src/types.ts
export interface IAppInfo {
  name: string;
  entry: string;
  container: string;
  activeRule: string;
}

// src/start.ts
export const registerMicroApps = (appList: IAppInfo[]) => {
  setAppList(appList);
};

// src/appList/index.ts
let appList: IAppInfo[] = [];

export const setAppList = (list: IAppInfo[]) => {
  appList = list;
};

export const getAppList = () => {
  return appList;
};

只需要將用戶(hù)傳入的 appList 保存起來(lái)即可。


路由劫持

在有了子應(yīng)用列表以后,我們需要啟動(dòng)微前端以便渲染相應(yīng)的子應(yīng)用,也就是需要判斷路由來(lái)渲染相應(yīng)的應(yīng)用。但是在進(jìn)行下一步前,我們需要先考慮一個(gè)問(wèn)題:如何監(jiān)聽(tīng)路由的變化來(lái)判斷渲染哪個(gè)子應(yīng)用?


對(duì)于非 SPA(單頁(yè)應(yīng)用) 架構(gòu)的項(xiàng)目來(lái)說(shuō),這個(gè)完全不是什么問(wèn)題,因?yàn)槲覀冎恍枰趩?dòng)微前端的時(shí)候判斷下當(dāng)前 URL 并渲染應(yīng)用即可;但是在 SPA 架構(gòu)下,路由變化是不會(huì)引發(fā)頁(yè)面刷新的,因此我們需要一個(gè)方式知曉路由的變化,從而判斷是否需要切換子應(yīng)用或者什么事都不干。

路由原理
目前單頁(yè)應(yīng)用使用路由的方式分為兩種:
  1. hash 模式,也就是 URL 中攜帶 #
  2. histroy 模式,也就是常見(jiàn)的 URL 格式了
這兩種模式分別會(huì)涉及到哪些事件及 API:

從上述圖中我們可以發(fā)現(xiàn),路由變化會(huì)涉及到兩個(gè)事件:

  • popstate

  • hashchange

因此這兩個(gè)事件我們肯定是需要去監(jiān)聽(tīng)的。除此之外,調(diào)用 pushState 以及 replaceState 也會(huì)造成路由變化,但不會(huì)觸發(fā)事件,因此我們還需要去重寫(xiě)這兩個(gè)函數(shù)。

知道了該監(jiān)聽(tīng)什么事件以及重寫(xiě)什么函數(shù)之后,接下來(lái)我們就來(lái)實(shí)現(xiàn)代碼:

// src/route/index.ts

// 保存原有方法
const originalPush = window.history.pushState;
const originalReplace = window.history.replaceState;

export const hijackRoute = () => {
  // 重寫(xiě)方法
  window.history.pushState = (...args) => {
    // 調(diào)用原有方法
    originalPush.apply(window.history, args);
    // URL 改變邏輯,實(shí)際就是如何處理子應(yīng)用
    // ...
  };
  window.history.replaceState = (...args) => {
    originalReplace.apply(window.history, args);
    // URL 改變邏輯
    // ...
  };

  // 監(jiān)聽(tīng)事件,觸發(fā) URL 改變邏輯
  window.addEventListener("hashchange"() => {});
  window.addEventListener("popstate"() => {});

  // 重寫(xiě)
  window.addEventListener = hijackEventListener(window.addEventListener);
  window.removeEventListener = hijackEventListener(window.removeEventListener);
};

const capturedListeners: Record<EventType, Function[]> = {
  hashchange: [],
  popstate: [],
};
const hasListeners = (name: EventType, fn: Function) => {
  return capturedListeners[name].filter((listener) => listener === fn).length;
};
const hijackEventListener = (func: Function): any => {
  return function (name: string, fn: Function{
    // 如果是以下事件,保存回調(diào)函數(shù)
    if (name === "hashchange" || name === "popstate") {
      if (!hasListeners(name, fn)) {
        capturedListeners[name].push(fn);
        return;
      } else {
        capturedListeners[name] = capturedListeners[name].filter(
          (listener) => listener !== fn
        );
      }
    }
    return func.apply(windowarguments);
  };
};
// 后續(xù)渲染子應(yīng)用后使用,用于執(zhí)行之前保存的回調(diào)函數(shù)
export function callCapturedListeners() {
  if (historyEvent) {
    Object.keys(capturedListeners).forEach((eventName) => {
      const listeners = capturedListeners[eventName as EventType]
      if (listeners.length) {
        listeners.forEach((listener) => {
          // @ts-ignore
          listener.call(this, historyEvent)
        })
      }
    })
    historyEvent = null
  }
}

以上代碼看著很多行,實(shí)際做的事情很簡(jiǎn)單,總體分為以下幾步:

  1. 重寫(xiě) pushState 以及 replaceState 方法,在方法中調(diào)用原有方法后執(zhí)行如何處理子應(yīng)用的邏輯

  2. 監(jiān)聽(tīng) hashchangepopstate 事件,事件觸發(fā)后執(zhí)行如何處理子應(yīng)用的邏輯

  3. 重寫(xiě)監(jiān)聽(tīng) / 移除事件函數(shù),如果應(yīng)用監(jiān)聽(tīng)了 hashchangepopstate 事件就將回調(diào)函數(shù)保存起來(lái)以備后用


應(yīng)用生命周期

在實(shí)現(xiàn)路由劫持后,我們現(xiàn)在需要來(lái)考慮如果實(shí)現(xiàn)處理子應(yīng)用的邏輯了,也就是如何處理子應(yīng)用加載資源以及掛載和卸載子應(yīng)用??吹竭@里,大家是不是覺(jué)得這和組件很類(lèi)似。組件也同樣需要處理這些事情,并且會(huì)暴露相應(yīng)的生命周期給用戶(hù)去干想干的事。
因此對(duì)于一個(gè)子應(yīng)用來(lái)說(shuō),我們也需要去實(shí)現(xiàn)一套生命周期,既然子應(yīng)用有生命周期,主應(yīng)用肯定也有,而且也必然是相對(duì)應(yīng)子應(yīng)用生命周期的。
那么到這里我們大致可以整理出來(lái)主 / 子應(yīng)用的生命周期。
對(duì)于主應(yīng)用來(lái)說(shuō),分為以下三個(gè)生命周期:
  1. beforeLoad:掛載子應(yīng)用前
  2. mounted:掛載子應(yīng)用后
  3. unmounted:卸載子應(yīng)用
當(dāng)然如果你想增加生命周期也是完全沒(méi)問(wèn)題的,筆者這里為了簡(jiǎn)便就只實(shí)現(xiàn)了三種。
對(duì)于子應(yīng)用來(lái)說(shuō),通用也分為以下三個(gè)生命周期:
  1. bootstrap:首次應(yīng)用加載觸發(fā),常用于配置子應(yīng)用全局信息
  2. mount:應(yīng)用掛載時(shí)觸發(fā),常用于渲染子應(yīng)用
  3. unmount:應(yīng)用卸載時(shí)觸發(fā),常用于銷(xiāo)毀子應(yīng)用
接下來(lái)我們就來(lái)實(shí)現(xiàn)注冊(cè)主應(yīng)用生命周期函數(shù):
// src/types.ts
export interface ILifeCycle {
  beforeLoad?: LifeCycle | LifeCycle[];
  mounted?: LifeCycle | LifeCycle[];
  unmounted?: LifeCycle | LifeCycle[];
}

// src/start.ts
// 改寫(xiě)下之前的

export const registerMicroApps = (
  appList: IAppInfo[],
  lifeCycle?: ILifeCycle
) => {
  setAppList(appList);
  lifeCycle && setLifeCycle(lifeCycle);
};

// src/lifeCycle/index.ts
let lifeCycle: ILifeCycle = {};

export const setLifeCycle = (list: ILifeCycle) => {
  lifeCycle = list;
};

因?yàn)槭侵鲬?yīng)用的生命周期,所以我們?cè)谧?cè)子應(yīng)用的時(shí)候就順帶注冊(cè)上了。

然后子應(yīng)用的生命周期:

// src/enums.ts
// 設(shè)置子應(yīng)用狀態(tài)

export enum AppStatus {
  NOT_LOADED = "NOT_LOADED",
  LOADING = "LOADING",
  LOADED = "LOADED",
  BOOTSTRAPPING = "BOOTSTRAPPING",
  NOT_MOUNTED = "NOT_MOUNTED",
  MOUNTING = "MOUNTING",
  MOUNTED = "MOUNTED",
  UNMOUNTING = "UNMOUNTING",
}
// src/lifeCycle/index.ts
export const runBeforeLoad = async (app: IInternalAppInfo) => {
  app.status = AppStatus.LOADING;
  await runLifeCycle("beforeLoad", app);

  app = await 加載子應(yīng)用資源;
  app.status = AppStatus.LOADED;
};

export const runBoostrap = async (app: IInternalAppInfo) => {
  if (app.status !== AppStatus.LOADED) {
    return app;
  }
  app.status = AppStatus.BOOTSTRAPPING;
  await app.bootstrap?.(app);
  app.status = AppStatus.NOT_MOUNTED;
};

export const runMounted = async (app: IInternalAppInfo) => {
  app.status = AppStatus.MOUNTING;
  await app.mount?.(app);
  app.status = AppStatus.MOUNTED;
  await runLifeCycle("mounted", app);
};

export const runUnmounted = async (app: IInternalAppInfo) => {
  app.status = AppStatus.UNMOUNTING;
  await app.unmount?.(app);
  app.status = AppStatus.NOT_MOUNTED;
  await runLifeCycle("unmounted", app);
};

const runLifeCycle = async (name: keyof ILifeCycle, app: IAppInfo) => {
  const fn = lifeCycle[name];
  if (fn instanceof Array) {
    await Promise.all(fn.map((item) => item(app)));
  } else {
    await fn?.(app);
  }
};


以上代碼看著很多,實(shí)際實(shí)現(xiàn)也很簡(jiǎn)單,總結(jié)一下就是:

  • 設(shè)置子應(yīng)用狀態(tài),用于邏輯判斷以及優(yōu)化。比如說(shuō)當(dāng)一個(gè)應(yīng)用狀態(tài)為非 NOT_LOADED 時(shí)(每個(gè)應(yīng)用初始都為 NOT_LOADED 狀態(tài)),下次渲染該應(yīng)用時(shí)就無(wú)需重復(fù)加載資源了

  • 如需要處理邏輯,比如說(shuō) beforeLoad 我們需要加載子應(yīng)用資源

  • 執(zhí)行主 / 子應(yīng)用生命周期,這里需要注意下執(zhí)行順序,可以參考父子組件的生命周期執(zhí)行順序


完善路由劫持

實(shí)現(xiàn)應(yīng)用生命周期以后,我們現(xiàn)在就能來(lái)完善先前路由劫持中沒(méi)有做的如何處理子應(yīng)用的這塊邏輯。
這塊邏輯在我們做完生命周期之后其實(shí)很簡(jiǎn)單,可以分為以下幾步:
  1. 判斷當(dāng)前 URL 與之前的 URL 是否一致,如果一致則繼續(xù)
  2. 利用當(dāng)然 URL 去匹配相應(yīng)的子應(yīng)用,此時(shí)分為幾種情況:
    • 初次啟動(dòng)微前端,此時(shí)只需渲染匹配成功的子應(yīng)用
    • 未切換子應(yīng)用,此時(shí)無(wú)需處理子應(yīng)用
    • 切換子應(yīng)用,此時(shí)需要找出之前渲染過(guò)的子應(yīng)用做卸載處理,然后渲染匹配成功的子應(yīng)用
  3. 保存當(dāng)前 URL,用于下一次第一步判斷
理清楚步驟之后,我們就來(lái)實(shí)現(xiàn)它:
let lastUrl: string | null = null
export const reroute = (url: string) => {
  if (url !== lastUrl) {
    const { actives, unmounts } = 匹配路由,尋找符合條件的子應(yīng)用
    // 執(zhí)行生命周期
    Promise.all(
      unmounts
        .map(async (app) => {
          await runUnmounted(app)
        })
        .concat(
          actives.map(async (app) => {
            await runBeforeLoad(app)
            await runBoostrap(app)
            await runMounted(app)
          })
        )
    ).then(() => {
      // 執(zhí)行路由劫持小節(jié)未使用的函數(shù)
      callCapturedListeners()
    })
  }
  lastUrl = url || location.href
}


以上代碼主體就是在按順序執(zhí)行生命周期函數(shù),但是其中匹配路由的函數(shù)并未實(shí)現(xiàn),因?yàn)槲覀冃枰葋?lái)考慮一些問(wèn)題。
大家平時(shí)項(xiàng)目開(kāi)發(fā)中肯定是用過(guò)路由的,那應(yīng)該知道路由匹配的原則主要由兩塊組成:
  • 嵌套關(guān)系
  • 路徑語(yǔ)法
嵌套關(guān)系指的是:假如我當(dāng)前的路由設(shè)置的是 /vue,那么類(lèi)似 /vue 或者 /vue/xxx 都能匹配上這個(gè)路由,除非我們?cè)O(shè)置 excart 也就是精確匹配。
路徑語(yǔ)法這里就拿個(gè)文檔里的例子呈現(xiàn)了:
<Route path="/hello/:name">         // 匹配 /hello/michael 和 /hello/ryan
<Route path="/hello(/:name)">       // 匹配 /hello, /hello/michael 和 /hello/ryan
<Route path="/files/*.*">           // 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg

這樣看來(lái)路由匹配實(shí)現(xiàn)起來(lái)還是挺麻煩的,那么我們是否有簡(jiǎn)便的辦法來(lái)實(shí)現(xiàn)該功能呢?答案肯定是有的,我們只要閱讀 Route 庫(kù)源碼就能發(fā)現(xiàn)它們內(nèi)部都使用了path-to-regexp這個(gè)庫(kù),有興趣的可以閱讀下這個(gè)庫(kù)的文檔,這里我們只看其中一個(gè) API 的使用就行。


有了解決方案以后,我們就快速實(shí)現(xiàn)下路由匹配的函數(shù):

export const getAppListStatus = () => {
  // 需要渲染的應(yīng)用列表
  const actives: IInternalAppInfo[] = []
  // 需要卸載的應(yīng)用列表
  const unmounts: IInternalAppInfo[] = []
  // 獲取注冊(cè)的子應(yīng)用列表
  const list = getAppList() as IInternalAppInfo[]
  list.forEach((app) => {
    // 匹配路由
    const isActive = match(app.activeRule, { end: false })(location.pathname)
    // 判斷應(yīng)用狀態(tài)
    switch (app.status) {
      case AppStatus.NOT_LOADED:
      case AppStatus.LOADING:
      case AppStatus.LOADED:
      case AppStatus.BOOTSTRAPPING:
      case AppStatus.NOT_MOUNTED:
        isActive && actives.push(app)
        break
      case AppStatus.MOUNTED:
        !isActive && unmounts.push(app)
        break
    }
  })

  return { actives, unmounts }
}
完成以上函數(shù)之后,大家別忘了在 reroute 函數(shù)中調(diào)用一下,至此路由劫持功能徹底完成了。


完善生命周期

之前在實(shí)現(xiàn)生命周期過(guò)程中,我們還有很重要的一步加載子應(yīng)用資源未完成,這一小節(jié)我們就把這塊內(nèi)容搞定。
既然要加載資源,那么我們肯定就先需要一個(gè)資源入口,就和我們使用的 npm 包一樣,每個(gè)包一定會(huì)有一個(gè)入口文件。回到 registerMicroApps 函數(shù),我們最開(kāi)始就給這個(gè)函數(shù)傳入了 entry 參數(shù),這就是子應(yīng)用的資源入口。
資源入口其實(shí)分為兩種方案:
  1. JS Entry
  2. HTML Entry
這兩個(gè)方案都是字面意思,前者是通過(guò) JS 加載所有靜態(tài)資源,后者則通過(guò) HTML 加載所有靜態(tài)資源。
JS Entry 是 single-spa 中使用的一個(gè)方式。但是它限制有點(diǎn)多,需要用戶(hù)將所有文件打包在一起,除非你的項(xiàng)目對(duì)性能無(wú)感,否則基本可以 pass 這個(gè)方案。
HTML Entry 則要好得多,畢竟所有網(wǎng)站都是以 HTML 作為入口文件的。在這種方案里,我們基本無(wú)需改動(dòng)打包方式,對(duì)用戶(hù)開(kāi)發(fā)幾乎沒(méi)侵入性,只需要尋找出 HTML 中的靜態(tài)資源加載并運(yùn)行即可渲染子應(yīng)用了,因此我們選擇了這個(gè)方案。
接下來(lái)我們開(kāi)始來(lái)實(shí)現(xiàn)這部分的內(nèi)容。

加載資源

首先我們需要獲取 HTML 的內(nèi)容,這里我們只需調(diào)用原生 fetch 就能拿到東西了。

// src/utils
export const fetchResource = async (url: string) => {
  return await fetch(url).then(async (res) => await res.text())
}
// src/loader/index.ts
export const loadHTML = async (app: IInternalAppInfo) => {
  const { container, entry } = app

  const htmlFile = await fetchResource(entry)

  return app
}

切換路由至 /vue 之后,我們可以打印出加載到的 HTML 文件內(nèi)容。


<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="/favicon.ico">
    <title>sub</title>
  <link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"></head>
  <body>
    <noscript>
      <strong>We're sorry but sub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  <script type="text/javascript" src="/js/chunk-vendors.js"></script>
  <script type="text/javascript" src="/js/app.js"></script></body>
</html>

我們可以在該文件中看到好些相對(duì)路徑的靜態(tài)資源 URL,接下來(lái)我們就需要去加載這些資源了。但是我們需要注意一點(diǎn)的是,這些資源只有在自己的 BaseURL 下才能被正確加載到,如果是在主應(yīng)用的 BaseURL 下肯定報(bào) 404 錯(cuò)誤了。
然后我們還需要注意一點(diǎn):因?yàn)槲覀兪窃谥鲬?yīng)用的 URL 下加載子應(yīng)用的資源,這很有可能會(huì)觸發(fā)跨域的限制。因此在開(kāi)發(fā)及生產(chǎn)環(huán)境大家務(wù)必注意跨域的處理。
舉個(gè)開(kāi)發(fā)環(huán)境下子應(yīng)用是 Vue 的話(huà),處理跨域的方式:
// vue.config.js
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin''*',
    },
  },
}

接下來(lái)我們需要先行處理這些資源的路徑,將相對(duì)路徑拼接成正確的絕對(duì)路徑,然后再去 fetch

// vue.config.js
// src/utils
export function getCompletionURL(src: string | null, baseURI: string{
  if (!src) return src
  // 如果 URL 已經(jīng)是協(xié)議開(kāi)頭就直接返回
  if (/^(https|http)/.test(src)) return src
 // 通過(guò)原生方法拼接 URL
  return new URL(src, getCompletionBaseURL(baseURI)).toString()
}
// 獲取完整的 BaseURL
// 因?yàn)橛脩?hù)在注冊(cè)應(yīng)用的 entry 里面可能填入 //xxx 或者 https://xxx 這種格式的 URL

export function getCompletionBaseURL(url: string{
  return url.startsWith('//') ? `${location.protocol}${url}` : url
}

以上代碼的功能就不再贅述了,注釋已經(jīng)很詳細(xì)了,接下來(lái)我們需要找到 HTML 文件中的資源然后去 fetch。

既然是找出資源,那么我們就得解析 HTML 內(nèi)容了:

// src/loader/parse.ts
export const parseHTML = (parent: HTMLElement, app: IInternalAppInfo) => {
  const children = Array.from(parent.children) as HTMLElement[]
  children.length && children.forEach((item) => parseHTML(item, app))

  for (const dom of children) {
    if (/^(link)$/i.test(dom.tagName)) {
      // 處理 link
    } else if (/^(script)$/i.test(dom.tagName)) {
      // 處理 script
    } else if (/^(img)$/i.test(dom.tagName) && dom.hasAttribute('src')) {
      // 處理圖片,畢竟圖片資源用相對(duì)路徑肯定也 404 了
      dom.setAttribute(
        'src',
        getCompletionURL(dom.getAttribute('src')!, app.entry)!
      )
    }
  }

  return {  }
}


解析內(nèi)容這塊還是簡(jiǎn)單的,我們遞歸尋找元素,將 linkscript、img 元素找出來(lái)并做對(duì)應(yīng)的處理即可。

首先來(lái)看我們?nèi)绾翁幚?nbsp;link

// src/loader/parse.ts
// 補(bǔ)全 parseHTML 邏輯

if (/^(link)$/i.test(dom.tagName)) {
  const data = parseLink(dom, parent, app)
  data && links.push(data)
}
const parseLink = (
  link: HTMLElement,
  parent: HTMLElement,
  app: IInternalAppInfo
) => {
  const rel = link.getAttribute('rel')
  const href = link.getAttribute('href')
  let comment: Comment | null
  // 判斷是不是獲取 CSS 資源
  if (rel === 'stylesheet' && href) {
    comment = document.createComment(`link replaced by micro`)
    // @ts-ignore
    comment && parent.replaceChild(comment, script)
    return getCompletionURL(href, app.entry)
  } else if (href) {
    link.setAttribute('href', getCompletionURL(href, app.entry)!)
  }
}

處理 link 標(biāo)簽時(shí),我們只需要處理 CSS 資源,其它 preload / prefetch 的這些資源直接替換 href 就行。

// src/loader/parse.ts
// 補(bǔ)全 parseHTML 邏輯

if (/^(link)$/i.test(dom.tagName)) {
  const data = parseScript(dom, parent, app)
  data.text && inlineScript.push(data.text)
  data.url && scripts.push(data.url)
}
const parseScript = (
  script: HTMLElement,
  parent: HTMLElement,
  app: IInternalAppInfo
) => {
  let comment: Comment | null
  const src = script.getAttribute('src')
  // 有 src 說(shuō)明是 JS 文件,沒(méi) src 說(shuō)明是 inline script,也就是 JS 代碼直接寫(xiě)標(biāo)簽里了
  if (src) {
    comment = document.createComment('script replaced by micro')
  } else if (script.innerHTML) {
    comment = document.createComment('inline script replaced by micro')
  }
  // @ts-ignore
  comment && parent.replaceChild(comment, script)
  return { url: getCompletionURL(src, app.entry), text: script.innerHTML }
}
處理 script 標(biāo)簽時(shí),我們需要區(qū)別是 JS 文件還是行內(nèi)代碼,前者還需要 fecth 一次獲取內(nèi)容。
然后我們會(huì)在 parseHTML 中返回所有解析出來(lái)的 scripts, links, inlineScript。
接下來(lái)我們按照順序先加載 CSS 再加載 JS 文件:


// src/loader/index.ts
export const loadHTML = async (app: IInternalAppInfo) => {
  const { container, entry } = app

  const fakeContainer = document.createElement('div')
  fakeContainer.innerHTML = htmlFile
  const { scripts, links, inlineScript } = parseHTML(fakeContainer, app)

  await Promise.all(links.map((link) => fetchResource(link)))

  const jsCode = (
    await Promise.all(scripts.map((script) => fetchResource(script)))
  ).concat(inlineScript)

  return app
}


以上我們就實(shí)現(xiàn)了從加載 HTML 文件到解析文件找出所有靜態(tài)資源到最后的加載 CSS 及 JS 文件。但是實(shí)際上我們這個(gè)實(shí)現(xiàn)還是有些粗糙的,雖然把核心內(nèi)容實(shí)現(xiàn)了,但是還是有一些細(xì)節(jié)沒(méi)有考慮到的。
因此我們也可以考慮直接使用三方庫(kù)來(lái)實(shí)現(xiàn)加載及解析文件的過(guò)程,這里我們選用了 import-html-entry 這個(gè)庫(kù),內(nèi)部做的事情和我們核心是一致的,只是多處理了很多細(xì)節(jié)。
如果你想直接使用這個(gè)庫(kù)的話(huà),可以把 loadHTML 改造成這樣:
export const loadHTML = async (app: IInternalAppInfo) => {
  const { container, entry } = app

  // template:處理好的 HTML 內(nèi)容
  // getExternalStyleSheets:fetch CSS 文件
  // getExternalScripts:fetch JS 文件

  const { template, getExternalScripts, getExternalStyleSheets } =
    await importEntry(entry)
  const dom = document.querySelector(container)

  if (!dom) {
    throw new Error('容器不存在 ')
  }
  // 掛載 HTML 到微前端容器上
  dom.innerHTML = template
  // 加載文件
  await getExternalStyleSheets()
  const jsCode = await getExternalScripts()

  return app
}

運(yùn)行 JS

當(dāng)我們拿到所有 JS 內(nèi)容以后就該運(yùn)行 JS 了,這步完成以后我們就能在頁(yè)面上看到子應(yīng)用被渲染出來(lái)了。

這一小節(jié)的內(nèi)容說(shuō)簡(jiǎn)單的話(huà)可以沒(méi)幾行代碼就寫(xiě)完,說(shuō)復(fù)雜的話(huà)實(shí)現(xiàn)起來(lái)會(huì)需要考慮很多細(xì)節(jié),我們先來(lái)實(shí)現(xiàn)簡(jiǎn)單的部分,也就是如何運(yùn)行 JS。

對(duì)于一段 JS 字符串來(lái)說(shuō),我們想執(zhí)行的話(huà)大致上有兩種方式:

  1. eval(js string)

  2. new Function(js string)()

這邊我們選用第二種方式來(lái)實(shí)現(xiàn):


const runJS = (value: string, app: IInternalAppInfo) => {
  const code = `
    ${value}
    return window['${app.name}']
  `

  return new Function(code).call(windowwindow)
}
不知道大家是否還記得我們?cè)谧?cè)子應(yīng)用的時(shí)候給每個(gè)子應(yīng)用都設(shè)置了一個(gè) name 屬性,這個(gè)屬性其實(shí)很重要,我們?cè)谥蟮膱?chǎng)景中也會(huì)用到。另外大家給子應(yīng)用設(shè)置 name 的時(shí)候別忘了還需要略微改動(dòng)下打包的配置,將其中一個(gè)選項(xiàng)也設(shè)置為同樣內(nèi)容。
舉個(gè)例子,我們假如給其中一個(gè)技術(shù)棧為 Vue 的子應(yīng)用設(shè)置了 name: vue,那么我們還需要在打包配置中進(jìn)行如下設(shè)置:


// vue.config.js
module.exports = {
  configureWebpack: {
    output: {
      // 和 name 一樣
      library: `vue`
    },
  },
}

這樣配置后,我們就能通過(guò) window.vue 訪問(wèn)到應(yīng)用的 JS 入口文件 export 出來(lái)的內(nèi)容了:

大家可以在上圖中看到導(dǎo)出的這些函數(shù)都是子應(yīng)用的生命周期,我們需要拿到這些函數(shù)去調(diào)用。

最后我們?cè)?nbsp;loadHTML 中調(diào)用一下 runJS 就完事了:

export const loadHTML = async (app: IInternalAppInfo) => {
  const { container, entry } = app

  const { template, getExternalScripts, getExternalStyleSheets } =
    await importEntry(entry)
  const dom = document.querySelector(container)

  if (!dom) {
    throw new Error('容器不存在 ')
  }

  dom.innerHTML = template

  await getExternalStyleSheets()
  const jsCode = await getExternalScripts()

  jsCode.forEach((script) => {
    const lifeCycle = runJS(script, app)
    if (lifeCycle) {
      app.bootstrap = lifeCycle.bootstrap
      app.mount = lifeCycle.mount
      app.unmount = lifeCycle.unmount
    }
  })

  return app
}

完成以上步驟后,我們就能看到子應(yīng)用被正常渲染出來(lái)了!


但是到這一步其實(shí)還不算完,我們考慮這樣一個(gè)問(wèn)題:子應(yīng)用改變?nèi)肿兞吭趺崔k? 我們目前所有應(yīng)用都可以獲取及改變 window 上的內(nèi)容,那么一旦應(yīng)用之間出現(xiàn)全局變量沖突就會(huì)引發(fā)問(wèn)題,因此我們接下來(lái)需要來(lái)解決這個(gè)事兒。


JS 沙箱

我們即要防止子應(yīng)用直接修改 window 上的屬性又要能訪問(wèn) window 上的內(nèi)容,那么就只能做個(gè)假的 window 給子應(yīng)用了,也就是實(shí)現(xiàn)一個(gè) JS 沙箱。
實(shí)現(xiàn)沙箱的方案也有很多種,比如說(shuō):
  1. 快照
  2. Proxy
先來(lái)說(shuō)說(shuō)快照的方案,其實(shí)這個(gè)方案實(shí)現(xiàn)起來(lái)特別簡(jiǎn)單,說(shuō)白了就是在掛載子應(yīng)用前記錄下當(dāng)前 window 上的所有內(nèi)容,然后接下來(lái)就隨便讓子應(yīng)用去玩了,直到卸載子應(yīng)用時(shí)恢復(fù)掛載前的 window 即可。這種方案實(shí)現(xiàn)容易,唯一缺點(diǎn)就是性能慢點(diǎn),有興趣的讀者可以直接看看 qiankun 的實(shí)現(xiàn),這里就不再貼代碼了。
再來(lái)說(shuō)說(shuō) Proxy,也是我們選用的方案,這個(gè)應(yīng)該挺多讀者都已經(jīng)了解過(guò)它的使用方式了,畢竟 Vue3 響應(yīng)式原理都被說(shuō)爛了。如果你還不了解它的話(huà),可以先自行閱讀 MDN 文檔。


export class ProxySandbox {
  proxy: any
  running = false
  constructor() {
    // 創(chuàng)建個(gè)假的 window
    const fakeWindow = Object.create(null)
    const proxy = new Proxy(fakeWindow, {
      set(target: any, p: string, value: any) => {
        // 如果當(dāng)前沙箱在運(yùn)行,就直接把值設(shè)置到 fakeWindow 上
        if (this.running) {
          target[p] = value
        }
        return true
      },
      get(target: any, p: string): any {
        // 防止用戶(hù)逃課
        switch (p) {
          case 'window':
          case 'self':
          case 'globalThis':
            return proxy
        }
        // 假如屬性不存在 fakeWindow 上,但是存在于 window 上
        // 從 window 上取值

        if (
          !window.hasOwnProperty.call(target, p) &&
          window.hasOwnProperty(p)
        ) {
          // @ts-ignore
          const value = window[p]
          if (typeof value === 'function'return value.bind(window)
          return value
        }
        return target[p]
      },
      has() {
        return true
      },
    })
    this.proxy = proxy
  }
  // 激活沙箱
  active() {
    this.running = true
  }
  // 失活沙箱
  inactive() {
    this.running = false
  }
}
以上代碼只是一個(gè)初版的沙箱,核心思路就是創(chuàng)建一個(gè)假的 window 出來(lái),如果用戶(hù)設(shè)置值的話(huà)就設(shè)置在 fakeWindow 上,這樣就不會(huì)影響全局變量了。如果用戶(hù)取值的話(huà),就判斷屬性是存在于 fakeWindow 上還是 window 上。
當(dāng)然實(shí)際使用的時(shí)候我們還是需要完善一下這個(gè)沙箱的,還需要處理一些細(xì)節(jié),這里推薦大家直接閱讀 qiankun 的源碼,代碼量不多,無(wú)非多處理了不少邊界情況。
另外需要注意的是:一般快照和 Proxy 沙箱都是需要的,無(wú)非前者是后者的降級(jí)方案,畢竟不是所有瀏覽器都支持 Proxy 的。
最后我們需要改造下 runJS 里的代碼以便使用沙箱:
const runJS = (value: string, app: IInternalAppInfo) => {
  if (!app.proxy) {
    app.proxy = new ProxySandbox()
    // 將沙箱掛在全局屬性上
    // @ts-ignore

    window.__CURRENT_PROXY__ = app.proxy.proxy
  }
  // 激活沙箱
  app.proxy.active()
  // 用沙箱替代全局環(huán)境調(diào)用 JS 
  const code = `
    return (window => {
      ${value}
      return window['${app.name}']
    })(window.__CURRENT_PROXY__)
  `

  return new Function(code)()
}

至此,我們其實(shí)已經(jīng)完成了整個(gè)微前端的核心功能。

接下來(lái)我們會(huì)來(lái)做一些改善型功能。

改善型功能

prefetch

我們目前的做法是匹配一個(gè)子應(yīng)用成功后才去加載子應(yīng)用,這種方式其實(shí)不夠高效。我們更希望用戶(hù)在瀏覽當(dāng)前子應(yīng)用的時(shí)候就能把別的子應(yīng)用資源也加載完畢,這樣用戶(hù)切換應(yīng)用的時(shí)候就無(wú)需等待了。
實(shí)現(xiàn)起來(lái)代碼不多,利用我們之前的 import-html-entry 就能馬上做完了:
// src/start.ts
export const start = () => {
  const list = getAppList()
  if (!list.length) {
    throw new Error('請(qǐng)先注冊(cè)應(yīng)用')
  }

  hijackRoute()
  reroute(window.location.href)

  // 判斷狀態(tài)為 NOT_LOADED 的子應(yīng)用才需要 prefetch
  list.forEach((app) => {
    if ((app as IInternalAppInfo).status === AppStatus.NOT_LOADED) {
      prefetch(app as IInternalAppInfo)
    }
  })
}
// src/utils.ts
export const prefetch = async (app: IInternalAppInfo) => {
  requestIdleCallback(async () => {
    const { getExternalScripts, getExternalStyleSheets } = await importEntry(
      app.entry
    )
    requestIdleCallback(getExternalStyleSheets)
    requestIdleCallback(getExternalScripts)
  })
}


接下來(lái)主要來(lái)聊下 requestIdleCallback 這個(gè)函數(shù)。

window.requestIdleCallback() 方法將在瀏覽器的空閑時(shí)段內(nèi)調(diào)用的函數(shù)排隊(duì)。這使開(kāi)發(fā)者能夠在主事件循環(huán)上執(zhí)行后臺(tái)和低優(yōu)先級(jí)工作,而不會(huì)影響延遲關(guān)鍵事件,如動(dòng)畫(huà)和輸入響應(yīng)。

我們利用這個(gè)函數(shù)實(shí)現(xiàn)在瀏覽器空閑時(shí)間再去進(jìn)行 prefetch,其實(shí)這個(gè)函數(shù)在 React 中也有用到,無(wú)非內(nèi)部實(shí)現(xiàn)了一個(gè) polyfill 版本。因?yàn)檫@個(gè) API 有一些問(wèn)題(最快 50ms 響應(yīng)一次)尚未解決,但是在我們的場(chǎng)景下不會(huì)有問(wèn)題,所以可以直接使用。

資源緩存機(jī)制

當(dāng)我們加載過(guò)一次資源后,用戶(hù)肯定不希望下次再進(jìn)入該應(yīng)用的時(shí)候還需要再加載一次資源,因此我們需要實(shí)現(xiàn)資源的緩存機(jī)制。

上一小節(jié)我們因?yàn)槭褂玫搅?import-html-entry,內(nèi)部自帶了緩存機(jī)制。如果你想自己實(shí)現(xiàn)的話(huà),可以參考內(nèi)部的實(shí)現(xiàn)方式。

簡(jiǎn)單來(lái)說(shuō)就是搞一個(gè)對(duì)象緩存下每次請(qǐng)求下來(lái)的文件內(nèi)容,下次請(qǐng)求的時(shí)候先判斷對(duì)象中存不存在值,存在的話(huà)直接拿出來(lái)用就行。


全局通信及狀態(tài)

這部分內(nèi)容在筆者的代碼中并未實(shí)現(xiàn),如果你有興趣自己做的話(huà),筆者可以提供一些思路。

全局通信及狀態(tài)實(shí)際上完全都可以看做是發(fā)布訂閱模式的一種實(shí)現(xiàn),只要你自己手寫(xiě)過(guò) Event 的話(huà),實(shí)現(xiàn)這個(gè)應(yīng)該不是什么難題。

另外你也可以閱讀下 qiankun 的全局狀態(tài)實(shí)現(xiàn),總共也就 100 行代碼。

五、總結(jié)

    在我們閱讀有關(guān)微前端相關(guān)解決方案的官方文檔后后,我們一定會(huì)進(jìn)行更深層次的學(xué)習(xí),比如看下框架底層是如何運(yùn)行的,以及源碼的閱讀。
    這里廣東靚仔給下一些小建議:
  • 在看源碼前,我們先去官方文檔復(fù)習(xí)下框架設(shè)計(jì)理念、源碼分層設(shè)計(jì)
  • 閱讀下框架官方開(kāi)發(fā)人員寫(xiě)的相關(guān)文章
  • 借助框架的調(diào)用棧來(lái)進(jìn)行源碼的閱讀,通過(guò)這個(gè)執(zhí)行流程,我們就完整的對(duì)源碼進(jìn)行了一個(gè)初步的了解
  • 接下來(lái)再對(duì)源碼執(zhí)行過(guò)程中涉及的所有函數(shù)邏輯梳理一遍

關(guān)注我,一起攜手進(jìn)階

歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~

瀏覽 151
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 不卡av在线| 黄色爱爱| 黄色成人网站在线观看| 日本无码一区二区三区| 国产91精品看黄网站在线观看 | 北条麻妃久久视频在线播放| 黄色操逼网站| 日韩一级黄色| 国产又爽又黄免费网站在线看| 一级a免一级a做免费线看内裤的注意事项| 久草国产在线视频| 青青草网站在线观看| 日日夜夜超碰| 国产免费乱伦| 久久成人123| 伊人大香蕉在线观看| 国产精品一卡二卡| 免费看国产黄色视频| 狠狠干婷婷| 国产一区二区视频在线| 欧美成人综合一区| 婷婷精品秘进入| 色综合五月| 超碰199| 欧美一区二区在线| 久草免费在线视频| 四川BBB搡BBB搡多人乱| 躁BBB躁BBB躁BBBBB乃| 国产精品欧美一区二区| 最近日本中文字幕中文翻译歌词| 北条麻妃久久久| 香蕉视频a| 99亚洲无码| 亭亭色| 日AV在线无| 日韩av在线免费观看| 免费高清无码视频| 午夜香蕉视频| 嫩小槡BBBB槡BBBB槡漫画 | 亚欧美日韩| 五月天丁香成人| 久久久久久三级电影| 亚洲免费观看| 国产777| 亚洲一区视频| 日韩av免费| 波多野结衣AV在线播放| 欧美AAAAAA| 黄色视频一级| 草少妇| 日韩大香蕉在线| 黄色无码电影| 手机在线看A片| 夜夜爽妓女77777毛片A片| 色播一区| 国外成人性视频免费| 九九黄色| 成人在线综合| 四虎成人无码A片观看| 日韩,变态,另类,中文,人妻| 亚洲AV无码秘翔田| 国产精品天天狠天天看| 无码视频久久| 在线视频99| 777三级| 日韩在线一区二区三区四区| 69国产成人综合久久精品欧美| 免费一区视频| 成人69AV| 免费视频在线观看一区| 国产成人网站免费观看| 天天天天日天天干| 亚洲人成电影网| 国产一区二区视频在线| 亚洲vs无码秘蜜桃| 一区二区三区久久久久| 丁香五月激情在线| 日本中文字幕亚洲| 免费的av网站| 一级黄色电影免费观看| 久久久久久久久国产精品| 日本免费不卡| 日本A片在线播放| 在线观看成年人视频| 欧美一区二区在线视频| 99久久99九九九99九他书对| 欧美成人性爱网站| 麻豆成人无码精品视频| 国产精品96久久久| 三级在线视频| 亚洲成人视频网站| 逼特逼在线观看| 五月天婷婷激情| 秋霞午夜成人无码精品| 成人免费观看的毛视频| 俺来也官网欧美久久精品| 极品小仙女69| 西西www444无码大胆| 欧美一级AAA大片免费观看| zzjicom| 黄色网址在线观看视频| 亚洲免费视频网站| 日B免费视频| 日韩色情网| 99国产精品久久久久久久成人| 99久热在线精品视频| 人人射人人爱| 欧美伊人大香蕉| 农村一级婬片A片AAA毛片古装| 怡红院一区| 俺也干| 777久久| 久久精品中文字幕| 黄色成人网站在线观看| 欧美性爱一级视频| 亚洲最新无码视频| 中文字幕无码一区二区三区一本久 | 国产女人操逼视频| 久久精品视频在线免费观看| 国产a片视频| 中文免费高清在线| 欧美性猛交XXXXⅩXX| 摸BBB搡BBB搡BBBB| 一区二区三区无码高清| www.色悠悠| 少妇嫩搡BBBB搡BBBB| 99视频内射三四| 99中文字幕| 日韩精品一区二区三区四区蜜桃视频 | 午夜亚洲| 五月天亚洲激情| 嫩BBB搡BBBB搡BBBB| 久久青青草在线视频| 高清无码视频免费| AV天天干| 久久与婷婷| 免费黄色视频大全| 3D动漫精品啪啪一区二区免费| 婷婷涩嫩草鲁丝久久午夜精品| 肉片无遮挡一区二区三区免费观看视频| 免费国产h| 日本一区二区精品| 日韩精品无码一区二区三区| 免费Av在线| 人人操人人摸人人看| 青娱乐欧美| 久久久久久精| 大香蕉久| 亚洲三区视频| 四虎影院最新地址| www444www| 你操综合| 播五月婷婷| 美女天天操| 国产Av婬乱麻豆| 欧美喷水视频| 99热日韩| 中文资源在线a中文| 亚洲欧洲成人在线| 精品中文字幕在线观看| 亚洲任你操超碰在线| 欧美激情婷婷| 十八禁网站在线播放| 黄片中文字幕| 秋霞中文字幕| 午夜激情在线观看| 五月少妇| 欧美在线A| 午夜天堂精品久久久久9| 特级西西444WWW视频| 97在线免费视频| 国产乱伦网站| 91久久久久久久91| 免费在线观看亚洲| 91黄色视频网站| 免费高潮视频| 69视频在线观看| 美女裸体视频网站| 国产精品无码无套在线照片| www.日批| 农村乱子伦毛片国产乱| 久久婷婷五月综合| 色老师综合| 亚洲欧美v在线视频| 男女做爱网站| 色色看片| 国产激情在线观看| 亚洲中文字幕免费观看视频| 91欧美亚洲| 丁香五月综合网| 亚洲视频在线看| 成人免费Av| 欧美在线A片| 国产手机精品视频| 一级欧美日韩| 午夜毛片| 人妻久操| 日韩久久人妻| 黄网在线免费观看| 蜜桃一区二区中午字幕| 亚洲欧美视频| 日韩成人高清| 狼友在线观看| 国产成人+综合亚洲+天堂| 日本久久电影| 中文字幕av高清片,中文在线观看| 国产高清Av| 久久久久亚洲AV成人网人人软件| 国产一区二区不卡| 黄片精品| 亚洲永久免费精品| 欧美亚洲在线| 俺去操| 日韩18在线| 亚洲一区二区三区在线视频| 蜜芽无码| 熟女人妻一区二区三区| 成人精品一区日本无码网站suv/ | 日韩三级片网址| 伊人99re| 东京热一区二区三区| 人人摸人人摸| jzzijzzij亚洲成熟少妇在线观看 九色蝌蚪9l视频蝌蚪9l视频成人熟妇 | 成人免费Av| 亚洲AV女人18毛片水真多| 视频一区18| 国产午夜无码福利视频| 无码人妻丰满熟妇区毛片蜜桃麻豆| 欧美成人免费在线| 亚洲精品娱乐| 人人爱人人操| 丰满人妻一区二区三区46| 精品一区二区三区在线观看| 黄色成人网站在线免费观看| 日韩成人无码片| 亚洲最新无码| 婷婷五月在线观看| 久久人妻无码中文字幕系列 | 成人无遮挡| 国产AV天堂| www.777熟女人妻| 熟妇槡BBBB槡BBBB图| 亚洲第一页在线| 亚洲精品成人片在线观看精品字幕| 欧美日韩第一区| 色婷婷免费视频| 日韩欧美在线观看| 欧美大鸡巴在线观看| 日韩中文字码无砖| 欧美色色网| 国产AV二区| 国产日韩欧美综合在线| 无码人妻AⅤ一区二区三区A片一| 中文免费高清在线| 欧美成人午夜福利| 日日射人妻| 中文字幕在线永久| 三级网站大全| 国产av探花| 97精品人妻一区二区| 成人午夜无码视频| 久久亚洲Aⅴ成人无码国产丝袜 | 中文字幕免费无码| 水蜜桃网| 久草资源视频| 久久足交| 东京热高清无码| 欧美a∨| 久久久久久毛片| 人妻日韩精品中文字幕| 97午夜福利视频| 青青草视频| 蜜臀AV一区二区三区免费看| 日韩,变态,另类,中文,人妻| 国产激情精品| 91国产视频网站| 亚洲中文久久| 日韩毛片在线观看| 一本一道久久综合| 天堂综合| 天堂在线视频免费| 欧美在线无码| 性爱无码| 欧美狠狠插| 国产精品av在线| 五月婷婷色欲| 中文字幕精品在线视频| 91嫩草欧美久久久九九九| 北岛玲丝袜办公室高跟| 色丁香五月婷婷| 中国1级毛片| 大香蕉综合视频| 日日干日日操| 人人爱人人操人人干| 日韩成人无码电影| 欧美精品xxx| 伊人精品A片一区二区三区| 麻豆91麻豆国产传媒| 日韩无码人妻一区| 刘玥91精一区二区三区| 国产高清在线免费观看AV片| 国产无遮挡又黄又爽| 在线免费观看黄片| 午夜视频在线看| 国产成人AV在线| 国产一区二区不卡视频| 91视频色| 一级a片在线| 国产2页| 欧美性猛交一区二区三区| 色色五月天网站| 久久精品熟妇丰满人妻99| 三级AV在线观看| 91精品国产综合久久蜜臀使用方法 | 欧美激情综合| 久久精品女同亚洲女同13| 国产www在线观看| 人妻无码精品蜜桃| 国产精品视频福利| 欧美A级黄片| 2025av天堂网| 久久精品v| 亚洲精品中文字幕在线观看| 操逼逼一区二区三区| 久久这里只有精品9| 美女毛片视频| 久久成人福利| 色婷婷成人做爰A片免费看网站| 天堂8在线视频| 久久久久成人片免费观看蜜芽| 色色色五月婷婷| 精品无码在线| 丁香五月伊人| 91精品国产偷窥一区二区| 日本www视频| 日本伊人大香蕉| 免费aa片| 国精产品久拍自产在线网站| 超碰护士| 一区二区三区四区视频在线 | 日韩黄色小视频| 山东乱子伦视频国产| 国产在线观看黄色| www.av在线播放| 亚洲中文字幕视频在线观看| 精品国产一级A片黄毛网站 | 91在线无精精品秘白丝| 日逼导航| 国产毛片毛片毛片| 狠狠干老司机| 在线免费观看AV片| 欧美色图色就是色| 亚洲h| 国产成人秘在线观看免费网站| 热99| 亚洲群交视频| AV黄片| 97精品一区二区三区A片| 丁香五月激情综合| 色片无码| 色婷婷一区二区三区四区五区精品视| 国产69精品久久久久久| 北条麻妃在线播放一区| 91网在线| 先锋影音亚洲无码av| 中文三级片| 日韩免费看片| 天天av天天av天天爽| 亚洲男同tv| 亚洲天天操| 影音先锋在线视频观看| 色婷婷AV| 亚洲v在线观看| 亚洲国际中文字幕在线| 伊人国产女| 成人五月天黄色电影| 激情小说激情视频| 久久久极品| 国产乱国产乱老熟300部视频 | 欧美日韩无码| 国产农村乱婬片A片AAA图片| 男女草逼| 影音先锋男人资源网| 国产情趣网站| 国产办公室丝袜人妖| 黄色一级在线| 在线无码av| 色欲AV秘无码一区二区三区| 伊人大香蕉综合| 久久天堂AV综合合色蜜桃网| 中文字幕在线观看福利视频| 欧美18禁网站| 天天干狠狠| 亚洲一区中文字幕| 一本色道综合久久欧美日韩精品| 91在线观看| 日韩av中文字幕在线| 久操手机在线| 五月天操逼网站| 91人人妻| 国产亚洲日韩在线| 牛牛在线精品视频| 中文字幕AV在线免费观看| 人人妻人人爱人人操| 2018天天操天天干| 日韩欧美中文在线| 欧美色色综合| 91啦丨露脸丨熟女| 四川BBB搡BBB爽爽爽电影| 高清无码免费视频| 九九操比| 天天干天天日天天射| 欧美日韩A片欧美日| 91国语对白| 五香丁香天堂网| 精品成人电影| AV天堂资源| 国产口爆| 亚洲视频高清无码| 亚洲综合网在线| 久久久久久久免费视频| 一区二区三区无码专区| 免费播放片色情A片| 五月婷婷国产| 在线看操逼| www.男人天堂| 开心黄色网| 一区二区三区免费观看| 中文在线字幕免费观| 91麻豆天美传媒在线| 欧美激情亚洲| 亚洲欧洲高清无码| 国产精品96久久久久久| 久草国产视频| 欧美一级成人片| 午夜香蕉| 久久夜色精品国产噜噜亚洲AV| 操你啦日韩| 91人妻人人澡人人爽人人精品乱| 18精品爽视频| 色婷婷久久综合久色| 麻豆一区在线观看| 亚洲人成777| 亚洲中文字幕一区二区| 国产免费一级特黄A片| 91夫妻交友视频| 国产精品a久久久久| 日韩无码黄色视频| 中文字幕AV在线播放| 老婆被黑人杂交呻吟视频| 宅男看片| 黄色www| 人妻熟女在线| 黄色一级在线| 日韩精品123| 欧美高潮视频| 国产精品无码一区二区三| 五月天福利视频| 玖玖激情| 国产精品探花熟女AV| 精品国产乱码久久久久久郑州公司 | 91视频免费网站| 黄色无码av| 中文原创麻豆传媒md0052 | 国产十欧洲十美国+亚洲一二三区在线午夜 | 激情五月婷婷五月| 亚洲va欧美va| 大香蕉国产在线| 五月天在线观看| av老鸭窝| 黄色片免费看| 人人上人人干| 亚洲精品在线视频观看| 久九视频| 成人午夜无码视频| 婷婷国产精品视频| 麻豆传媒av| 国产一区免费| 韩国精品在线观看| 1204手机看片| 最近日韩中文字幕中文翻译歌词| 无码AV中文字幕| 国产精品无码无套在线照片| 中文字幕精品在线免费视频观看视频 | 大鷄巴成人A片视频| 99久久综合国产精品二区| 国产性爱在线| 青草国产视频| 久久青草免费视频| 久久毛久久久j| 91夫妻视频| 国产高清无码在线观看视频| 免费看黄片视频| 精品视频在线播放| 成人无码区免费A片久久| 亚洲性爱小说网址| 在线观看高清无码中文字幕| 在线免费观看黄| 激情乱伦网站| 亚洲精品三级片| 欧美人妻日韩精品| 亚洲成av| 女人特级毛片18| 边摸边操| 无码中文字幕在线视频| 欧美一级免费视频| www.a日逼| 五月激情综合| A级视频免费观看| 亚洲高清视频免费| 玖玖爱av| 在线观看91| 欧美一级黄色大片| 欧美视频区| 亚洲中文自拍| 波多野结衣精品无码| 国产精品秘久久久久久网站| 毛片在线观看网站| 国产乱子伦一区二区三区在线观看 | 九色PORNY国产成人蝌蚪| 中文字幕人成人乱码亚洲电影| 亚洲高清视频免费| 亚洲网站免费在线观看| 中文字幕无码在线观看| 国产午夜在线观看| 国产伦子伦一级A片在线| 亚洲福利在线免费观看| 国产一级a毛一级a毛片视频黑人| 精品人妻一区二区| 国产AV久久| 黄色录像毛片| 91美女操逼视频| 日韩99| 国产黄色视频在线免费观看| 青草超碰| 色色综合热| 西西www444无码免费视频| 蜜桃av秘无码一区三区四| 三级片在线网站| 熟妇槡BBBB槡BBBB图| 美女网站永久免费观看| 国产色婷婷一区二区| 午夜激情av| 免费看成人片| 亚洲综合中文字幕在线| 亚洲美女网站免费观看网址| 国产在线一二三| 水蜜桃视频在线观看| 欧美视频在线一区| 国产成人免费视频在线| 日本免费版网站nba| 国模一区二区三区| 国产成人电影| 久操视频免费观看| 中日韩免费视频| 婷婷五月色综合| 亚洲婷婷视频| 俺也去俺去啦| 欧美三级在线播放| 人人干人人干| 大黑人荫蒂BBBBBBBBB| 黄色片a片| 久久人视频| 久久久久人| 躁BBB躁BBB躁BBBBBB日| 九九成人网站| 成人做爰100部免费网站| 51妺嘿嘿午夜福利在线| 美女被操网站免费| 一区二区三区四区日韩| av福利电影在线| 自拍做爱视频| 一本一道波多野结衣潮喷视频 | 国色天香一区二区| 国产无码免费视频| 一级a片在线观看| 麻豆视频一区| 自拍偷拍15p| 亚洲五月丁香婷婷| 国产精品毛片VA一区二区三区| 北条麻妃在线播放一区| 久久久三级片| 激情五月天成人| 福利网站在线观看| 国产V视频| 国产亚洲无码| 招土一级黄色片| 泄火熟妇2-ThePorn| 亚洲五月婷| 麻豆性爱视频| 男人的天堂在线播放| 丝袜三级片| 中文字幕无码Av在线| 国产精品系列视频| 国精品无码人妻一区二区三区免费| 在线无码免费| 99er热精品视频| 日本黄在线观看| 久久在线精品| 日韩黄色在线视频| www.99免费视频| 五月婷婷中文字幕| 国产福利在线播放| 国内成人精品| 成人免费在线| 久热福利| 亚l洲视频在线观看| 国产网站免费| 激情无码在线观看| 日韩一区二区视频在线观看| 一本免费视频| 一级二级无码| 国产精品免费av在线| 69av在线观看| 亚洲人成在线观看| 国产精品成人AV片| 久操中文| 欧美日韩精品一区二区| 欧美日韩国内| 九九久久99| 神马午夜福利影院| 大鸡巴视频在线| 韩国三级HD中文字幕2019年| 人人摸人人搞| 精品视频在线播放| 3D动漫精品啪啪一区二区免费| 操人人| 国产一区二区三区免费视频| 免费无码一区| 久久婷婷亚洲| 91av免费| 午夜福利aaa| 黑人巨粗进入疼哭A片| 性爱AV在线| 高清无码在线视频观看| 国产精品国产精品国产专区不52| 2016超碰| 最新中文| 91探花秘入囗| 又大又粗AV| 另类综合激情| 黄色成人网站大全| 自拍偷拍福利视频网站| 国产三级片视频在线观看| 最新久欠一区二区免费看| 制服丝袜无码| 俺来也官网欧美久久精品| 91在线免费视频| 日韩中文字幕在线播放| 影音先锋成人电影| 99这里只有精品| 91久久国产综合久久91| 香蕉福利视频| 日本欧美在线| 激情六月天| 无码精品视频| 69超碰| 久久xxx| 性无码专区| 91国产爽黄在线相亲| 无码精品视频| 中文字幕亚洲有码| 黄色免费网站在线观看| 国产高清在线视频| 欧美三级片视频| 五月天操逼网| 日本Sm/调教/捆绑/紧缚| 日日撸| 国产精品久久久久久久久| 一区二区三区久久久| 欧美性爱视频网站| 欧美操操操| 伊人狼人香蕉| 无码东京热国产| A片观看视频| 久久久久久三级电影| 国内精产品一二区秘| 日韩三级中文| 亚洲欧洲在线播放| 91视频熟女| 亚洲成a人无码| 日韩AV资源网| 国产一级特黄大片| 97干在线| 黄色成人免费视频| 欧美888| 狠狠狠狠狠操| 日韩天堂网| 91人妻最真实刺激绿帽| 国产aⅴ激情无码久久久无码| 日本二区三区| A毛片| 欧美一区视频| 一级a一级a爰片免费| 中文字幕第9页| 亚洲激情图| 精品一区无码| 亚洲AV成人无码精品直播在线 | 亚洲丁香五月天| 人人妻人人操人人干| 北条麻妃AV观看| jizz久久| 无码中文AV| 91成人看片| 777无码| 五月丁香婷婷啪啪| 欧美成人无码一区二区三区 | 亚洲黄色Av| 九九九九九九精品| 欧美激情视频一区二区| 无码一区二区在线观看| 成人色综合| 黄页网站免费在线观看| 一级片日韩| 男女日皮视频| 中文字幕免费久久| 日韩中文字幕无码人妻| 大香蕉久久久久久久| 中国12一13毛片| jizz免费在线观看| 日韩大鸡巴| 亚洲美女免费视频| 在线播放JUY-925被丈夫上司侵犯的第7天| 18禁亚洲| 日本欧美国产| 午夜无码鲁丝片午夜精品| 午夜黄色影视| 日日夜夜天天综合| 狼友视频免费在线观看| 少妇AV| 免费在线观看黄色片| 国产精品久久久久久亚洲影视| 国产毛片在线看| 夜夜操夜夜操| 天堂AV色| 亚韩在线| av天堂资源在线| 狠狠狠狠狠狠狠狠| 日皮视频在线| 天天日天天射天天操| 久久18| 亚洲777| 国产成人高清在线| 国产免费AV片在线无码| 翔田千里在线观看| 久久精品黄色| 免费黄片视频在线观看| 国内免费av| 一区二区无码视频| 中文字幕在线观看a| 精品视频导航| 97超碰碰碰| 国产又粗又长视频| www.99精品| 強姦婬片A片AAA毛片Mⅴ| 国产成人电影| 国产精品无码一区二区三| 欧美在线A| 蜜臀av在线播放| JLZZJLZZ亚洲女人| 日韩黄片视频| 无码国产精品一区二区三| 五月婷婷免费视频| 亚洲综合一区二区三区| 大香蕉天天操| 日本内射在线观看| 日朝无码| 无码秘蜜桃一区二区| 亚洲综合伊人无码| 欧美三级欧美三级三级| 在线免费观看视频黄| 91久久婷婷国产| 亚洲综合日韩在线| 中文字幕人妻精品一区| 亚洲AV久久无码| 99爱免费视频| 日本特黄AA片免费视频| 久久无码一区二区三区| 欧美色图在线观看| 丁香五月亚洲综合| 亚洲毛片亚洲毛片亚洲毛片| ww无码| 99热这里只有精品1| 性v天堂| 五月婷婷色欲| 天堂性爱AV| 亚洲国产激情视频| 少妇人妻无码| 夜夜骚AV一二三区无码| 久久婷婷婷| 亚洲秘无码一区二区三区电影| 中文字幕在线免费播放| 91麻豆精品传媒国产| 亚洲小电影| 亚洲天堂在线观看视频网站| 亚洲中文字幕日韩| 91在线无码精品秘入口国战| 狠狠干在线视频| 91最新在线播放| 黄色片一级片| 北条麻妃精品在线| 丁香九月婷婷| 亚洲成人av| 淫香欲色| 精品成人在线视频| 日本成人一区| 九九九九精品| 强开小嫩苞毛片一二三区| 亚洲在线成人| 久久免费视屏| 亚洲天堂在线免费观看| 亚洲国产精品欧美久久| 在线免费中文字幕| 十八禁无码网站在线观看| 久久精品福利视频| 人妻少妇一区二区| 亚洲在线一区二区| 久久精品99久久久久久| 黄色国产在线观看| 69午夜| 美国黄色A片| 日韩家庭乱伦| 51乱伦| 欧美A片在线免费观看| 日韩专区在线观看| 99精品免费| 亚洲无码高清在线| 国产精品伦子伦免费视频| 2019人人操| 青草青草| 亚洲欧美在线综合| 亚洲成免费| 久久国内| 成人网在线视频| 国产在线色视频| 京东热av| 尤物AV| 俺来也俺去也www色| 亚洲热视频在线观看| 囯产精品久久久久久久| 天天干天天爽| 色片网| 色综合欧美| 青青草97国产精品麻豆| 亚洲日韩免费在线观看| 成人毛片av| 在线播放JUY-925被丈夫上司侵犯的第7天 | 精品国产污污免费网站入口| 日本无码一区二区| www.操B| 黄片无码免费| 操碰97| 91麻豆一区二区| 免费黄色视频在线| 人人插人人操| 无码人妻精品一区二区50| 日韩成人免费观看| 男女日比视频| 亚洲香蕉av| 日韩欧美视频在线播放| 国产成人无码区免费AV片在线 | 精品人妻一区二区三区鲁大师| 日韩国产成人在线| 天天操人人| 西西人体444大胆高清张悠雨| 欧美性爱内射| 色色一区二区| 午夜无码电影| 日韩无码一卡二卡| 午夜福利成人网站| 九九九色| 日韩av电影免费在线观看| 久草性爱| 中文亚洲字幕| 免费观看在线无码视频| 婷婷五月综合中文字幕| 国产夫妻自拍AV| 欧美精品99久久久| 在线观看无码AV| 一级A色情大片| 亚洲久操| 国产成人精品亚洲男人的天堂| 天天天天天天天操| 国内成人精品| 美女裸体视频网站| 天天日比| 久久久国产精品视频| 手机在线看片av| 欧美久久性爱| 插逼综合网| 在线看的av| 超碰碰碰碰碰| 躁BBB躁BBB躁BBBBBB日| 深爱激情五月婷婷| 大香蕉尹在线| 亚洲无码操逼视频| 国产美女AV| 神马午夜福利视频| 蜜桃免费网站| 无码人妻精品一区二区三区蜜臀百度 |