<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從理解路由到實(shí)現(xiàn)一套R(shí)outer(路由)

          共 29343字,需瀏覽 59分鐘

           ·

          2024-06-25 09:20

          點(diǎn)擊上方 前端Q,關(guān)注公眾號(hào)

          回復(fù)加群,加入前端Q技術(shù)交流群

          作者:betterwlf

          https://juejin.cn/post/7150794643985137695


          平時(shí)在Vue項(xiàng)目中經(jīng)常用到路由,但是也僅僅處于會(huì)用的層面,很多基礎(chǔ)知識(shí)并不是真正的理解。于是就趁著十一”小長(zhǎng)假“查閱了很多資料,總結(jié)下路由相關(guān)的知識(shí),查缺不漏,加深自己對(duì)路由的理解。

          路由

          在 Web 開(kāi)發(fā)過(guò)程中,經(jīng)常遇到路由的概念。那么到底什么是路由呢?簡(jiǎn)單來(lái)說(shuō),路由就是 URL 到函數(shù)的映射。

          路由這個(gè)概念本來(lái)是由后端提出來(lái)的,在以前用模板引擎開(kāi)發(fā)頁(yè)面的時(shí)候,是使用路由返回不同的頁(yè)面,大致流程是這樣的:

          1. 瀏覽器發(fā)出請(qǐng)求;
          2. 服務(wù)器監(jiān)聽(tīng)到 80 或者 443 端口有請(qǐng)求過(guò)來(lái),并解析 UR L路徑;
          3. 服務(wù)端根據(jù)路由設(shè)置,查詢相應(yīng)的資源,可能是 html 文件,也可能是圖片資源......,然后將這些資源處理并返回給瀏覽器;
          4. 瀏覽器接收到數(shù)據(jù),通過(guò)content-type決定如何解析數(shù)據(jù)

          簡(jiǎn)單來(lái)說(shuō),路由就是用來(lái)跟后端服務(wù)器交互的一種方式,通過(guò)不同的路徑來(lái)請(qǐng)求不同的資源,請(qǐng)求HTML頁(yè)面只是路由的其中一項(xiàng)功能。

          服務(wù)端路由

          當(dāng)服務(wù)端接收到客戶端發(fā)來(lái)的 HTTP 請(qǐng)求時(shí),會(huì)根據(jù)請(qǐng)求的 URL,找到相應(yīng)的映射函數(shù),然后執(zhí)行該函數(shù),并將函數(shù)的返回值發(fā)送給客戶端。

          對(duì)于最簡(jiǎn)單的靜態(tài)資源服務(wù)器,可以認(rèn)為,所有 URL 的映射函數(shù)就是一個(gè)文件讀取操作。對(duì)于動(dòng)態(tài)資源,映射函數(shù)可能是一個(gè)數(shù)據(jù)庫(kù)讀取操作,也可能進(jìn)行一些數(shù)據(jù)處理,等等。

          客戶端路由

          服務(wù)端路由會(huì)造成服務(wù)器壓力比較大,而且用戶訪問(wèn)速度也比較慢。在這種情況下,出現(xiàn)了單頁(yè)應(yīng)用。

          單頁(yè)應(yīng)用,就是只有一個(gè)頁(yè)面,用戶訪問(wèn)網(wǎng)址,服務(wù)器返回的頁(yè)面始終只有一個(gè),不管用戶改變了瀏覽器地址欄的內(nèi)容或者在頁(yè)面發(fā)生了跳轉(zhuǎn),服務(wù)器不會(huì)重新返回新的頁(yè)面,而是通過(guò)相應(yīng)的js操作來(lái)實(shí)現(xiàn)頁(yè)面的更改。

          前端路由其實(shí)就是:通過(guò)地址欄內(nèi)容的改變,顯示不同的頁(yè)面。

          前端路由的優(yōu)點(diǎn):

          • 前端路由可以讓前端自己維護(hù)路由與頁(yè)面展示的邏輯,每次頁(yè)面改動(dòng)不需要通知服務(wù)端。
          • 更好的交互體驗(yàn):不用每次從服務(wù)端拉取資源。

          前端路由的缺點(diǎn): 使用瀏覽器的前進(jìn)、后退鍵時(shí)會(huì)重新發(fā)送請(qǐng)求,來(lái)獲取數(shù)據(jù),沒(méi)有合理利用緩存。

          前端路由實(shí)現(xiàn)原理: 本質(zhì)就是監(jiān)測(cè) URL 的變化,通過(guò)攔截 URL 然后解析匹配路由規(guī)則。

          前端路由的實(shí)現(xiàn)方式

          1. hash模式(location.hash + hashchange 事件)

          hash 模式的實(shí)現(xiàn)方式就是通過(guò)監(jiān)聽(tīng) URL 中的 hash 部分的變化,觸發(fā)haschange事件,頁(yè)面做出不同的響應(yīng)。但是 hash 模式下,URL 中會(huì)帶有 #,不太美觀。

          1. history模式

          history 路由模式的實(shí)現(xiàn),基于 HTML5 提供的 History 全局對(duì)象,它的方法有:

          • history.go():在會(huì)話歷史中向前或者向后移動(dòng)指定頁(yè)數(shù)
          • history.forward():在會(huì)話歷史中向前移動(dòng)一頁(yè),跟瀏覽器的前進(jìn)按鈕功能相同
          • history.back():在會(huì)話歷史記錄中向后移動(dòng)一頁(yè),跟瀏覽器的后腿按鈕功能相同
          • history.pushState():向當(dāng)前瀏覽器會(huì)話的歷史堆棧中添加一個(gè)狀態(tài),會(huì)改變當(dāng)前頁(yè)面url,但是不會(huì)伴隨這刷新
          • history.replaceState():將當(dāng)前的會(huì)話頁(yè)面的url替換成指定的數(shù)據(jù),replaceState 會(huì)改變當(dāng)前頁(yè)面的url,但也不會(huì)刷新頁(yè)面
          • window.onpopstate:當(dāng)前活動(dòng)歷史記錄條目更改時(shí),將觸發(fā)popstate事件

          history路由的實(shí)現(xiàn),主要是依靠pushState、replaceStatewindow.onpopstate實(shí)現(xiàn)的。但是有幾點(diǎn)要注意:

          • 當(dāng)活動(dòng)歷史記錄條目更改時(shí),將觸發(fā) popstate 事件;
          • 調(diào)用history.pushState()history.replaceState()不會(huì)觸發(fā) popstate 事件
          • popstate 事件只會(huì)在瀏覽器某些行為下觸發(fā),比如:點(diǎn)擊后退、前進(jìn)按鈕(或者在 JavaScript 中調(diào)用history.back()、history.forward()history.go()方法)
          • a 標(biāo)簽的錨點(diǎn)也會(huì)觸發(fā)該事件

          對(duì) pushState 和 replaceState 行為的監(jiān)聽(tīng)

          如果想監(jiān)聽(tīng) pushState 和 replaceState 行為,可以通過(guò)在方法里面主動(dòng)去觸發(fā) popstate 事件,另一種是重寫history.pushState,通過(guò)創(chuàng)建自己的eventedPushState自定義事件,并手動(dòng)派發(fā),實(shí)際使用過(guò)程中就可以監(jiān)聽(tīng)了。具體做法如下:

          function eventedPushState(state, title, url{
              var pushChangeEvent = new CustomEvent("onpushstate", {
                  detail: {
                      state,
                      title,
                      url
                  }
              });
              document.dispatchEvent(pushChangeEvent);
              return history.pushState(state, title, url);
          }

          document.addEventListener(
              "onpushstate",
              function(event{
                  console.log(event.detail);
              },
              false
          );

          eventedPushState({}, """new-slug"); 

          router 和 route 的區(qū)別

          route 就是一條路由,它將一個(gè) URL 路徑和一個(gè)函數(shù)進(jìn)行映射。而 router 可以理解為一個(gè)容器,或者說(shuō)一種機(jī)制,它管理了一組 route。

          概括為:route 只是進(jìn)行了 URL 和函數(shù)的映射,在當(dāng)接收到一個(gè) URL 后,需要去路由映射表中查找相應(yīng)的函數(shù),這個(gè)過(guò)程是由 router 來(lái)處理的。

          動(dòng)態(tài)路由和靜態(tài)路由

          • 靜態(tài)路由

          靜態(tài)路由只支持基于地址的全匹配。

          • 動(dòng)態(tài)路由

          動(dòng)態(tài)路由除了可以兼容全匹配外還支持多種”高級(jí)匹配模式“,它的路徑地址中含有路徑參數(shù),使得它可以按照給定的匹配模式將符合條件的一個(gè)或多個(gè)地址映射到同一個(gè)組件上。

          動(dòng)態(tài)路由一般結(jié)合角色權(quán)限控制使用。

          動(dòng)態(tài)路由的存儲(chǔ)有兩種方式:

          1. 將路由存儲(chǔ)到前端
          2. 將路由存儲(chǔ)到數(shù)據(jù)庫(kù)

          動(dòng)態(tài)路由的好處:

          • 靈活,無(wú)需手動(dòng)維護(hù)
          • 存儲(chǔ)到數(shù)據(jù)庫(kù),增加安全性

          實(shí)現(xiàn)一個(gè)路由

          一個(gè)簡(jiǎn)單的Router應(yīng)該具備哪些功能

          • 以 Vue為例,需要有 <router-link>鏈接、<router-view>容器、component組件和path路由路徑:
          <div id="app">
              <h1>Hello World</h1>
              <p>
                  <!-- 使用 router-link 組件進(jìn)行導(dǎo)航 -->
                  <!-- 通過(guò)傳遞 to 來(lái)指定鏈接 -->
                  <!-- <router-link> 將呈現(xiàn)一個(gè)帶有正確 href屬性的<a>標(biāo)簽 -->
                  <router-link to="/">Go to Home</router-link>
                  <router-link to="/about">Go to About</router-link>
              </p>
              <!-- 路由出口 -->
              <!-- 路由匹配到的組件將渲染在這里 -->
              <router-view></router-view>
          </div>
          const routes = [{
              path: '/',
              component: Home
          },
          {
              path: '/about',
              component: About
          }]
          • 以React為例,需要有<BrowserRouter>容器、<Route>路由、組件和鏈接:
          <BrowserRouter>
              <Routes>
                  <Route path="/" element={<App />}>
                      <Route index element={<Home />} />
                      <Route path="teams" element={<Teams />}>
                          <Route path=":teamId" element={<Team />} />
                          <Route path="new" element={<NewTeamForm />} />
                          <Route index element={<LeagueStandings />} />
                      </Route>
                  </Route>
              </Routes>

          </BrowserRouter>


          <div>
              <h1>Home</h1>
              <nav>
                  <Link to="/">Home</Link> | {""}
                  <Link to="about">About</Link>
              </nav>
          </div>
          • 綜上,一個(gè)簡(jiǎn)單的 Router 應(yīng)該具備以下功能:
            • 容器(組件)
            • 路由
            • 業(yè)務(wù)組件 & 鏈接組件

          不借助第三方工具庫(kù),如何實(shí)現(xiàn)路由

          不借助第三方工具庫(kù)實(shí)現(xiàn)路由,我們需要思考以下幾個(gè)問(wèn)題:

          • 如何實(shí)現(xiàn)自定義標(biāo)簽,如vue的<router-view>,React的<Router>
          • 如何實(shí)現(xiàn)業(yè)務(wù)組件
          • 如何動(dòng)態(tài)切換路由

          準(zhǔn)備工作

          1、根據(jù)對(duì)前端路由 history 模式的理解,將大致過(guò)程用如下流程圖表示:


          2、如果不借助第三方庫(kù),我們選擇使用 Web components 。Web Components由三項(xiàng)主要技術(shù)組成,它們可以一起使用來(lái)創(chuàng)建封裝功能的定制元素。
            • Custom elements(自定義元素) :一組JavaScript API,允許我們定義 custom elements及其行為,然后可以在界面按照需要使用它們。
            • Shadow DOM(影子DOM) :一組JavaScript API,用于將封裝的“影子”DOM樹(shù)附加到元素(與主文檔分開(kāi)呈現(xiàn))并控制關(guān)聯(lián)的功能。通過(guò)這種方式,可以保持元素的功能私有。
            • HTML template(HTML模版)<template><slot>可以編寫不在頁(yè)面顯示的標(biāo)記模板,然后它們可以作為自定義元素結(jié)構(gòu)的基礎(chǔ)被多次重用。

          另外還需要注意 Web Components 的生命周期:

          connectedCallback:當(dāng) custom element 首次被插入文檔DOM時(shí),被調(diào)用

          disconnectedCallback:當(dāng) custom element 從文檔DOM中刪除時(shí),被調(diào)用

          adoptedCallback:當(dāng)custom element 被移動(dòng)到新的文檔時(shí),被調(diào)用

          attributeChangedCallback:當(dāng) custom element 增加、刪除、修改自身屬性時(shí),被調(diào)用

          3、Shadow DOM

            • open:shadow root 元素可以從 js 外部訪問(wèn)根節(jié)點(diǎn)
            • close :拒絕從 js 外部訪問(wèn)關(guān)閉的 shadow root 節(jié)點(diǎn)
            • 語(yǔ)法:const shadow = this.attachShadow({mode:closed});
            • Shadow host:一個(gè)常規(guī)DOM節(jié)點(diǎn),Shadow DOM 會(huì)被附加到這個(gè)節(jié)點(diǎn)上
            • Shadow tree:Shadow DOM 內(nèi)部的 DOM 樹(shù)
            • Shadow boundary:Shadow DOM 結(jié)束的地方,也是常規(guī)DOM開(kāi)始的地方
            • Shadow root:Shadow tree 的根節(jié)點(diǎn)
            • Shadow DOM 特有的術(shù)語(yǔ):
            • Shadow DOM的重要參數(shù)mode:
          1. 通過(guò)自定義標(biāo)簽創(chuàng)建容器組件、路由、業(yè)務(wù)組件和鏈接組件標(biāo)簽,使用

            CustomElementRegistry.define()注冊(cè)自定義元素。其中,Custom elements 的簡(jiǎn)單寫法舉例:

            <my-text></my-text>

            <script>
                class MyText extends HTMLElement{
                    constructor(){
                        super();
                        this.append(“我的文本”);
                    }
                }
                window.customElements.define("my-text",MyText);
            </
            script>
            1. 組件的實(shí)現(xiàn)可以使用 Web Components,但是這樣有缺點(diǎn),我們沒(méi)有打包引擎處理 Web Components組件,將其全部加載過(guò)來(lái)。

            為了解決以上問(wèn)題,我們選擇動(dòng)態(tài)加載,遠(yuǎn)程去加載一個(gè) html 文件。html文件里面的結(jié)構(gòu)如下:支持模版(template),腳本(template),腳本(script),樣式(style),非常地像vue。組件開(kāi)發(fā)模版如下:

            <template>
                <div>商品詳情</div>
                <div id="detail">
                    商品ID:<span id="product-id" class="product-id"></span>
                </div>
            </template>

            <script>
                this.querySelector("#product-id").textContent = history.state.id;
            </script>

            <style>
                .product-id{
                    color:red;
                }
            </style>
            1. 監(jiān)聽(tīng)路由的變化:

            popstate可以監(jiān)聽(tīng)大部分路由變化的場(chǎng)景,除了pushStatereplaceState。

            pushStatereplaceState可以改變路由,改變歷史記錄,但是不能觸發(fā)popstate事件,需要自定義事件并手動(dòng)觸發(fā)自定義事件,做出響應(yīng)。

            1. 整體架構(gòu)圖如下:

            8.  組件功能拆解分析如下:

            • 鏈接組件 — CustomLink(c-link)

            當(dāng)用戶點(diǎn)擊<c-link>標(biāo)簽后,通過(guò)event.preventDefault();阻止頁(yè)面默認(rèn)跳轉(zhuǎn)。根據(jù)當(dāng)前標(biāo)簽的to屬性獲取路由,通過(guò)history.pushState("","",to)進(jìn)行路由切換。

            //  <c-link to="/" class="c-link">首頁(yè)</c-link>
            class CustomLink extends HTMLElement {
                connectedCallback() {
                    this.addEventListener("click", ev => {
                        ev.preventDefault();
                        const to = this.getAttribute("to");
                        // 更新瀏覽器歷史記錄
                        history.pushState("""", to)
                    })

                }
            }
            window.customElements.define("c-link", CustomLink);
            • 容器組件 — CustomRouter(c-router)

            主要是收集路由信息,監(jiān)聽(tīng)路由信息的變化,然后加載對(duì)應(yīng)的組件

            • 路由 — CustomRoute(c-route)

            主要是提供配置信息,對(duì)外提供getData 的方法

            // 優(yōu)先于c-router注冊(cè)
            //  <c-route path="/" component="home" default></c-route>
            class CustomRoute extends HTMLElement {
                #data = null;
                getData() {
                    return {
                        defaultthis.hasAttribute("default"),
                        paththis.getAttribute("path"),
                        componentthis.getAttribute("component")
                    }
                }
            }
            window.customElements.define("c-route", CustomRoute);
            • 業(yè)務(wù)組件 — CustomComponent(c-component)

            實(shí)現(xiàn)組件,動(dòng)態(tài)加載遠(yuǎn)程的html,并解析

            完整代碼實(shí)現(xiàn)

            index.html:

            <div class="product-item">測(cè)試的產(chǎn)品</div>
            <div class="flex">
                <ul class="menu-x">
                    <c-link to="/" class="c-link">首頁(yè)</c-link>
                    <c-link to="/about" class="c-link">關(guān)于</c-link>
                </ul>
            </div>
            <div>
                <c-router>
                    <c-route path="/" component="home" default></c-route>
                    <c-route path="/detail/:id" component="detail"></c-route>
                    <c-route path="/about" component="about"></c-route>
                </c-router>
            </div>

            <script src="./router.js"></script>

            home.html:

            <template>
                <div>商品清單</div>
                <div id="product-list">
                    <div>
                        <a data-id="10" class="product-item c-link">香蕉</a>
                    </div>
                    <div>
                        <a data-id="11" class="product-item c-link">蘋果</a>
                    </div>
                    <div>
                        <a data-id="12" class="product-item c-link">葡萄</a>
                    </div>
                </div>
            </template>

            <script>
                let container = this.querySelector("#product-list");
                // 觸發(fā)歷史更新
                // 事件代理
                container.addEventListener("click"function (ev{
                    console.log("item clicked");
                    if (ev.target.classList.contains("product-item")) {
                        const id = +ev.target.dataset.id;
                        history.pushState({
                                id
                        }, ""`/detail/${id}`)
                    }
                })
            </script>

            <style>
                .product-item {
                    cursor: pointer;
                    color: blue;
                }
            </style>

            detail.html:

            <template>
                <div>商品詳情</div>
                <div id="detail">
                    商品ID:<span id="product-id" class="product-id"></span>
                </div>
            </template>

            <script>
                this.querySelector("#product-id").textContent=history.state.id;
            </script>

            <style>
                .product-id{
                    color:red;
                }
            </style>

            about.html:

            <template>
                About Me!
            </template>

            route.js:

            const oriPushState = history.pushState;

            // 重寫pushState
            history.pushState = function (state, title, url{
                // 觸發(fā)原事件
                oriPushState.apply(history, [state, title, url]);
                // 自定義事件
                var event = new CustomEvent("c-popstate", {
                    detail: {
                        state,
                        title,
                        url
                    }
                });
                window.dispatchEvent(event);
            }

            // <c-link to="/" class="c-link">首頁(yè)</c-link>
            class CustomLink extends HTMLElement {
                connectedCallback() {
                    this.addEventListener("click", ev => {
                        ev.preventDefault();
                        const to = this.getAttribute("to");
                        // 更新瀏覽歷史記錄
                        history.pushState("""", to);
                    })
                }
            }
            window.customElements.define("c-link", CustomLink);

            // 優(yōu)先于c-router注冊(cè)
            // <c-toute path="/" component="home" default></c-toute>
            class CustomRoute extends HTMLElement {
                #data = null;
                getData() {
                    return {
                        defaultthis.hasAttribute("default"),
                        paththis.getAttribute("path"),
                        componentthis.getAttribute("component")
                    }
                }
            }
            window.customElements.define("c-route", CustomRoute);

            // 容器組件
            class CustomComponent extends HTMLElement {
                async connectedCallback() {
                    console.log("c-component connected");
                    // 獲取組件的path,即html的路徑
                    const strPath = this.getAttribute("path");
                    // 加載html
                    const cInfos = await loadComponent(strPath);
                    const shadow = this.attachShadow({ mode"closed" });
                    // 添加html對(duì)應(yīng)的內(nèi)容
                    this.#addElement(shadow, cInfos);
                }
                #addElement(shadow, info) {
                    // 添加模板內(nèi)容
                    if (info.template) {
                        shadow.appendChild(info.template.content.cloneNode(true));
                    }
                    // 添加腳本
                    if (info.script) {
                        // 防止全局污染,并獲得根節(jié)點(diǎn)
                        var fun = new Function(`${info.script.textContent}`);
                        // 綁定腳本的this為當(dāng)前的影子根節(jié)點(diǎn)
                        fun.bind(shadow)();
                    }
                    // 添加樣式
                    if (info.style) {
                        shadow.appendChild(info.style);
                    }
                }
            }
            window.customElements.define("c-component", CustomComponent);

            // <c-router></c-router>
            class CustomRouter extends HTMLElement {
                #routes
                connectedCallback() {
                    const routeNodes = this.querySelectorAll("c-route");
                    console.log("routes:", routeNodes);

                    // 獲取子節(jié)點(diǎn)的路由信息
                    this.#routes = Array.from(routeNodes).map(node => node.getData());
                    // 查找默認(rèn)的路由
                    const defaultRoute = this.#routes.find(r => r.default) || this.#routes[0];
                    // 渲染對(duì)應(yīng)的路由
                    this.#onRenderRoute(defaultRoute);
                    // 監(jiān)聽(tīng)路由變化
                    this.#listenerHistory();
                }

                // 渲染路由對(duì)應(yīng)的內(nèi)容
                #onRenderRoute(route) {
                    var el = document.createElement("c-component");
                    el.setAttribute("path"`/${route.component}.html`);
                    el.id = "_route_";
                    this.append(el);
                }

                // 卸載路由清理工作
                #onUploadRoute(route) {
                    this.removeChild(this.querySelector("#_route_"));
                }

                // 監(jiān)聽(tīng)路由變化
                #listenerHistory() {
                    // 導(dǎo)航的路由切換
                    window.addEventListener("popstate", ev => {
                        console.log("onpopstate:", ev);
                        const url = location.pathname.endsWith(".html") ? "/" : location.pathname;
                        const route = this.#getRoute(this.#routes, url);
                        this.#onUploadRoute();
                        this.#onRenderRoute(route);
                    });
                    // pushStat或replaceSate
                    window.addEventListener("c-popstate", ev => {
                        console.log("c-popstate:", ev);
                        const detail = ev.detail;
                        const route = this.#getRoute(this.#routes, detail.url);
                        this.#onUploadRoute();
                        this.#onRenderRoute(route);
                    })
                }

                // 路由查找
                #getRoute(routes, url) {
                    return routes.find(function (r{
                        const path = r.path;
                        const strPaths = path.split('/');
                        const strUrlPaths = url.split("/");

                        let match = true;
                        for (let i = 0; i < strPaths.length; i++) {
                            if (strPaths[i].startsWith(":")) {
                                continue;
                            }
                            match = strPaths[i] === strUrlPaths[i];
                            if (!match) {
                                break;
                            }
                        }
                        return match;
                    })
                }
            }
            window.customElements.define("c-router", CustomRouter);

            // 動(dòng)態(tài)加載組件并解析
            async function loadComponent(path, name{
                this.caches = this.caches || {};
                // 緩存存在,直接返回
                if (!!this.caches[path]) {
                    return this.caches[path];
                }
                const res = await fetch(path).then(res => res.text());
                // 利用DOMParser校驗(yàn)
                const parser = new DOMParser();
                const doc = parser.parseFromString(res, "text/html");
                // 解析模板,腳本,樣式
                const template = doc.querySelector("template");
                const script = doc.querySelector("script");
                const style = doc.querySelector("style");
                // 緩存內(nèi)容
                this.caches[path] = {
                    template,
                    script,
                    style
                }
                return this.caches[path];
            }

            往期推薦


            別想調(diào)試我的前端代碼!
            來(lái)自38歲大廠程序員的忠告!
            一種更好的前端組件結(jié)構(gòu):組件樹(shù)

            最后


            • 歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

            • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

            點(diǎn)個(gè)在看支持我吧

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

            手機(jī)掃一掃分享

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

            手機(jī)掃一掃分享

            分享
            舉報(bào)
            <kbd id="afajh"><form id="afajh"></form></kbd>
            <strong id="afajh"><dl id="afajh"></dl></strong>
              <del id="afajh"><form id="afajh"></form></del>
                  1. <th id="afajh"><progress id="afajh"></progress></th>
                    <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    国产免费黄色小视频 | 日逼 | 不卡在线视频 | 无码野外| 国产白丝在线 |