<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>

          如何利用AOP+IOC思想解構(gòu)前端項目開發(fā)

          共 12379字,需瀏覽 25分鐘

           ·

          2020-11-12 19:43

          作者:evio

          來源:SegmentFault 思否社區(qū)




          本文將通過 TypeClient 架構(gòu)來闡述如何利用AOP+IOC思想來解構(gòu)前端項目的開發(fā)。


          首先聲明,AOP+IOC思想的理解需要有一定的編程架構(gòu)基礎(chǔ)。目前,這兩大思想使用的場景,基本都在nodejs端,在前端的實踐非常少。我本著提供一種新的項目解構(gòu)思路的想法,而非推翻社區(qū)龐大的全家桶。大家看看就好,如果能給你提供更好的靈感,那么再好不過了,非常歡迎交流。


          以下我們將以 TypeClient 的 React 渲染引擎為例。




          AOP


          一種面向切面編程的思想。它在前端的表現(xiàn)是前端的裝飾器,我們可以通過裝飾器來攔截函數(shù)執(zhí)行前與執(zhí)行后的自定義行為。


          • AOP的主要作用是把一些跟核心業(yè)務(wù)邏輯模塊無關(guān)的功能抽離出來,這些跟業(yè)務(wù)邏輯無關(guān)的功能通常包括日志統(tǒng)計、安全控制、異常處理等。把這些功能抽離出來之后, 再通過“動態(tài)織入”的方式摻入業(yè)務(wù)邏輯模塊中。AOP的好處首先是可以保持業(yè)務(wù)邏輯模塊的純凈和高內(nèi)聚性,其次是可以很方便地復(fù)用日志統(tǒng)計等功能模塊。


          以上是網(wǎng)絡(luò)上對AOP的簡單解釋。那么實際代碼也許是這樣的


          @Controller()
          class?Demo?{
          ??@Route()?Page()?{}
          }
          復(fù)制代碼


          但很多時候,我們僅僅是將某個class下的函數(shù)當(dāng)作一個儲存數(shù)據(jù)的對象而已,而在確定運行這個函數(shù)時候拿出數(shù)據(jù)做自定義處理??梢酝ㄟ^ reflect-metadata 來了解更多裝飾器的作用。




          IOC


          Angular難以被國內(nèi)接受很大一部分原因是它的理念太龐大,而其中的DI(dependency inject)在使用時候則更加讓人迷糊。其實除了DI還有一種依賴注入的思想叫 IOC。它的代表庫為 inversify。它在github上擁有6.7K的star數(shù),在依賴注入的社區(qū)里,口碑非常好。我們可以先通過這個庫來了解下它對項目解構(gòu)的好處。


          例子如下:


          @injectable()
          class?Demo?{
          ??@inject(Service)?private?readonly?service:?Service;
          ??getCount()?{
          ????return?1?+?this.service.sum(2,?3);
          ??}
          }
          復(fù)制代碼


          當(dāng)然,Service已經(jīng)優(yōu)先被注入到inversify的container內(nèi)了,才可以通過 TypeClient 這樣調(diào)用。




          重新梳理前端項目運行時


          一般地,前端項目會經(jīng)過這樣的運行過程。


          1. 通過監(jiān)聽hashchange或者popstate事件攔截瀏覽器行為。
          2. 設(shè)定當(dāng)前獲得的window.location 數(shù)據(jù)如何對應(yīng)到一個組件。
          3. 組件如何渲染到頁面。
          4. 當(dāng)瀏覽器URL再次變化的時候,我們?nèi)绾螌?yīng)到一個組件并且渲染。

          這是社區(qū)的通用解決方案。當(dāng)然,我們不會再講解如何設(shè)計這個模式。我們將采用全新的設(shè)計模式來解構(gòu)這個過程。

          重新審視服務(wù)端路由體系


          我們聊的是前端的架構(gòu),為什么會聊到服務(wù)端的架構(gòu)體系?

          那是因為,其實設(shè)計模式并不局限在后端或者前端,它應(yīng)該是一種比較通用的方式來解決特定的問題。

          那么也許有人會問,服務(wù)端的路由體系與前端并不一致,有何意義?

          我們以nodejs的http模塊為例,其實它與前端有點類似的。http模塊運行在一個進程中,通過http.createServer的參數(shù)回調(diào)函數(shù)來響應(yīng)數(shù)據(jù)。我們可以認為,前端的頁面相當(dāng)于一個進程,我們通過監(jiān)聽相應(yīng)模式下的事件來響應(yīng)得到組件渲染到頁面。

          服務(wù)端多個Client發(fā)送請求到一個server端端口處理,為什么不能類比到前端用戶操作瀏覽器地址欄通過事件來得到響應(yīng)入口呢?

          答案是可以的。我們稱這種方式為 virtual server 即基于頁面級的虛擬服務(wù)。
          既然可以抽象稱一種服務(wù)架構(gòu),那當(dāng)然,我們可以完全像nodejs的服務(wù)化方案靠攏,我們可以將前端的路由處理的如nodejs端常見的方式,更加符合我們的意圖和抽象。



          改造路由設(shè)計


          如果是以上的書寫方式,那么也可以解決基本的問題,但是不符合我們AOP+IOC的設(shè)計,書寫的時候還是比較繁瑣的,同時也沒有解構(gòu)掉響應(yīng)的邏輯。

          我們需要解決以下問題:

          1. 如何解析路由字符串規(guī)則?
          2. 如何利用這個規(guī)則快速匹配到對應(yīng)的回調(diào)函數(shù)?

          在服務(wù)端有很多解析路由規(guī)則的庫,比較代表的是 path-to-regexp,它被使用在KOA等著名架構(gòu)中。它的原理也就是將字符串正則化,使用當(dāng)前傳入的path來匹配相應(yīng)的規(guī)則從而得到對應(yīng)的回調(diào)函數(shù)來處理。但是這種做法有一些瑕疵,那就是正則匹配速度較慢,當(dāng)處理隊列最后一個規(guī)則被匹配的時候,所有規(guī)則都將被執(zhí)行過,當(dāng)路由過多時候性能較差,這一點可以參看我之前寫的 koa-rapid-router超越koa-router性能的100多倍。還有一點瑕疵是,它的匹配方式是按照你編寫順序匹配的,所以它具有一定的順序性,開發(fā)者要非常注意。比如:

          http.get('/:id(d+)',?()?=>?console.log(1));
          http.get('/1234',?()?=>?console.log(2));
          復(fù)制代碼

          如果我們訪問/1234,那么它將打印出1,而非2。

          為了解決性能以及優(yōu)化匹配過程的智能性,我們可以參考 find-my-way 的路由設(shè)計體系。具體請看官自己看了,我不解析。總之,它是一種字符串索引式算法,能夠快速而智能地匹配到我們需要的路由。著名的 fastify 就是采用這個架構(gòu)來達到高性能的。



          TypeClient 的路由設(shè)計


          我們可以通過一些簡單的裝飾器就能快速定義我們的路由,本質(zhì)還是采用find-my-way的路由設(shè)計原則。

          import?React?from?'react';
          import?{?Controller,?Route,?Context?}?from?'@typeclient/core';
          import?{?useReactiveState?}?from?'@typeclient/react';
          @Controller('/api')
          export?class?DemoController?{
          ??@Route('/test')
          ??TestPage(props:?Reat.PropsWithoutRef)?{
          ????const?status?=?useReactiveState(()?=>?props.status.value);
          ????return?
          Hello?world!?{status}
          ;
          ??}
          }
          //?--------------------------
          //?在index.ts中只要
          app.setController(DemoController);
          //?它就自動綁定了路由,同時頁面進入路由?`/api/test`?的時候
          //?就會顯示文本?`Hello world! 200`。
          復(fù)制代碼

          可見,TypeClient 通過 AOP 理念定義路由非常簡單。



          路由生命周期


          當(dāng)從一個頁面跳轉(zhuǎn)到另一個頁面的時候,前一個頁面的生命周期也隨即結(jié)束,所以,路由是具有生命周期的。再此,我們將整個頁面周期拆解如下:

          1. beforeCreate 頁面開始加載
          2. created 頁面加載完成
          3. beforeDestroy 頁面即將銷毀
          4. destroyed 頁面已經(jīng)銷毀

          為了表示這4個生命周期,我們根據(jù)React的hooks特制了一個函數(shù)useContextEffect來處理路由生命周期的副作用。比如:

          import?React?from?'react';
          import?{?Controller,?Route,?Context?}?from?'@typeclient/core';
          import?{?useReactiveState?}?from?'@typeclient/react';
          @Controller('/api')
          export?class?DemoController?{
          ??@Route('/test')
          ??TestPage(props:?Reat.PropsWithoutRef)?{
          ????const?status?=?useReactiveState(()?=>?props.status.value);
          ????useContextEffect(()?=>?{
          ??????console.log('路由加載完成了');
          ??????return?()?=>?console.log('路由被銷毀了');
          ????})
          ????return?
          Hello?world!?{status}
          ;
          ??}
          }
          復(fù)制代碼

          其實它與useEffect或者useLayoutEffect有些類似。只不過我們關(guān)注的是路由的生命周期,而react則關(guān)注組件的生命周期。

          其實通過上面的props.status.value我們可以猜測出,路由是有狀態(tài)記錄的,分別是100和200還有500等等。我們可以通過這樣的數(shù)據(jù)來判斷當(dāng)前路由處于什么生命周期內(nèi),也可以通過骨架屏來渲染不同的效果。



          中間件設(shè)計


          為了控制路由生命周期的運行,我們設(shè)計了中間件模式,用來處理路由前置的行為,比如請求數(shù)據(jù)等等。中間件原則上采用與KOA一致的模式,這樣可以大大兼容社區(qū)生態(tài)。

          const?middleware?=?async?(ctx,?next)?=>?{
          ??//?ctx.....
          ??await?next();
          }
          復(fù)制代碼

          通過AOP 我們可以輕松引用這個中間件,達到頁面加載完畢狀態(tài)前的數(shù)據(jù)處理。

          import?React?from?'react';
          import?{?Controller,?Route,?Context,?useMiddleware?}?from?'@typeclient/core';
          import?{?useReactiveState?}?from?'@typeclient/react';
          @Controller('/api')
          export?class?DemoController?{
          ??@Route('/test')
          ??@useMiddleware(middleware)
          ??TestPage(props:?Reat.PropsWithoutRef)?{
          ????const?status?=?useReactiveState(()?=>?props.status.value);
          ????useContextEffect(()?=>?{
          ??????console.log('路由加載完成了');
          ??????return?()?=>?console.log('路由被銷毀了');
          ????})
          ????return?
          Hello?world!?{status}
          ;
          ??}
          }
          復(fù)制代碼

          設(shè)計周期狀態(tài)管理 - ContextStore


          不得不說這個是一個亮點。為什么要設(shè)計這樣一個模式呢?主要是為了解決在中間件過程中對數(shù)據(jù)的操作能夠及時響應(yīng)到頁面。因為中間件執(zhí)行與react頁面渲染是同步的,所以我們設(shè)計這樣的模式有利于數(shù)據(jù)的周期化。

          我們采用了非常黑科技的方案解決這個問題:@vue/reactity

          對,就是它。

          我們在react中嵌入了VUE3最新的響應(yīng)式系統(tǒng),讓我們開發(fā)快速更新數(shù)據(jù),而放棄掉dispatch過程。當(dāng)然,這對中間件更新數(shù)據(jù)是及其有力的。

          這里 我非常感謝 sl1673495 給到的黑科技思路讓我們的設(shè)計能夠完美兼容react。

          我們通過@State(callback)來定義ContextStore的初始化數(shù)據(jù),通過useContextState或者useReactiveState跟蹤數(shù)據(jù)變化并且響應(yīng)到React頁面中。

          來看一個例子:

          import?React?from?'react';
          import?{?Controller,?Route,?Context,?useMiddleware,?State?}?from?'@typeclient/core';
          import?{?useReactiveState?}?from?'@typeclient/react';
          @Controller('/api')
          export?class?DemoController?{
          ??@Route('/test')
          ??@useMiddleware(middleware)
          ??@State(createState)
          ??TestPage(props:?Reat.PropsWithoutRef)?{
          ????const?status?=?useReactiveState(()?=>?props.status.value);
          ????const?count?=?useReactiveState(()?=>?props.state.count);
          ????const?click?=?useCallback(()?=>?ctx.state.count++,?[ctx.state.count]);
          ????useContextEffect(()?=>?{
          ??????console.log('路由加載完成了');
          ??????return?()?=>?console.log('路由被銷毀了');
          ????})
          ????return?Hello?world!?{status}?-?{count}
          ;
          ??}
          }

          function?createState()?{
          ??return?{
          ????count:?0,
          ??}
          }
          復(fù)制代碼

          你可以看到不斷點擊,數(shù)據(jù)不斷變化。這種操作方式極大簡化了我們數(shù)據(jù)的變化寫法,同時也可以與vue3響應(yīng)式能力看齊,彌補react數(shù)據(jù)操作復(fù)雜度的短板。

          除了在周期中使用這個黑科技,其實它也是可以獨立使用的,比如在任意位置定義:

          //?test.ts
          import?{?reactive?}?from?'@vue/reactity';

          export?const?data?=?reactive({
          ??count:?0,
          })
          復(fù)制代碼

          我們可以在任意組件中使用

          import?React,?{?useCallback?}?from?'react';
          import?{?useReactiveState?}?from?'@typeclient/react-effect';
          import?{?data?}?from?'./test';

          function?TestComponent()?{
          ??const?count?=?useReactiveState(()?=>?data.count);
          ??const?onClick?=?useCallback(()?=>?data.count++,?[data.count]);
          ??return?{count}

          }
          復(fù)制代碼

          利用IOC思想解構(gòu)項目


          以上的講解都沒有設(shè)計IOC方面,那么下面將講解IOC的使用。

          Controller 服務(wù)解構(gòu)


          我們先編寫一個Service文件

          import?{?Service?}?from?'@typeclient/core';

          @Service()
          export?class?MathService?{
          ??sum(a:?number,?b:?number)?{
          ????return?a?+?b;
          ??}
          }
          復(fù)制代碼

          然后我們可以在之前的Controller中直接調(diào)用:

          import?React?from?'react';
          import?{?Controller,?Route,?Context,?useMiddleware,?State?}?from?'@typeclient/core';
          import?{?useReactiveState?}?from?'@typeclient/react';
          import?{?MathService?}?from?'./service.ts';
          @Controller('/api')
          export?class?DemoController?{
          ??@inject(MathService)?private?readonly?MathService:?MathService;

          ??@Route('/test')
          ??@useMiddleware(middleware)
          ??@State(createState)
          ??TestPage(props:?Reat.PropsWithoutRef)?{
          ????const?status?=?useReactiveState(()?=>?props.status.value);
          ????const?count?=?useReactiveState(()?=>?props.state.count);
          ????const?click?=?useCallback(()?=>?ctx.state.count++,?[ctx.state.count]);
          ????const?value?=?this.MathService.sum(count,?status);
          ????useContextEffect(()?=>?{
          ??????console.log('路由加載完成了');
          ??????return?()?=>?console.log('路由被銷毀了');
          ????})
          ????return?Hello?world!?{status}?+?{count}?=?{value}
          ;
          ??}
          }

          function?createState()?{
          ??return?{
          ????count:?0,
          ??}
          }
          復(fù)制代碼

          你可以看到數(shù)據(jù)的不斷變化。

          Component 解構(gòu)


          我們?yōu)閞eact的組件創(chuàng)造了一種新的組件模式,稱IOCComponent。它是一種具備IOC能力的組件,我們通過useComponent的hooks來調(diào)用。

          import?React?from?'react';
          import?{?Component,?ComponentTransform?}?from?'@typeclient/react';
          import?{?MathService?}?from?'./service.ts';

          @Component()
          export?class?DemoComponent?implements?ComponentTransform?{
          ??@inject(MathService)?private?readonly?MathService:?MathService;

          ??render(props:?React.PropsWithoutRef<{?a:?number,?b:?number?}>)?{
          ????const?value?=?this.MathService.sum(props.a,?props.b);
          ????return?
          {value}

          ??}
          }
          復(fù)制代碼

          然后在任意組件中調(diào)用

          import?React?from?'react';
          import?{?Controller,?Route,?Context,?useMiddleware,?State?}?from?'@typeclient/core';
          import?{?useReactiveState?}?from?'@typeclient/react';
          import?{?MathService?}?from?'./service.ts';
          import?{?DemoComponent?}?from?'./component';
          @Controller('/api')
          export?class?DemoController?{
          ??@inject(MathService)?private?readonly?MathService:?MathService;
          ??@inject(DemoComponent)?private?readonly?DemoComponent:?DemoComponent;

          ??@Route('/test')
          ??@useMiddleware(middleware)
          ??@State(createState)
          ??TestPage(props:?Reat.PropsWithoutRef)?{
          ????const?status?=?useReactiveState(()?=>?props.status.value);
          ????const?count?=?useReactiveState(()?=>?props.state.count);
          ????const?click?=?useCallback(()?=>?ctx.state.count++,?[ctx.state.count]);
          ????const?value?=?this.MathService.sum(count,?status);
          ????const?Demo?=?useComponent(this.DemoComponent);
          ????useContextEffect(()?=>?{
          ??????console.log('路由加載完成了');
          ??????return?()?=>?console.log('路由被銷毀了');
          ????})
          ????return?
          ??????Hello?world!?{status}?+?{count}?=?{value}?
          ??????
          ????
          ;
          ??}
          }

          function?createState()?{
          ??return?{
          ????count:?0,
          ??}
          }
          復(fù)制代碼

          Middleware 解構(gòu)


          我們完全可以拋棄掉傳統(tǒng)的中間件寫法,而采用能加解構(gòu)化的中間件寫法:

          import?{?Context?}?from?'@typeclient/core';
          import?{?Middleware,?MiddlewareTransform?}?from?'@typeclient/react';
          import?{?MathService?}?from?'./service';

          @Middleware()
          export?class?DemoMiddleware?implements?MiddlewareTransform?{
          ??@inject(MathService)?private?readonly?MathService:?MathService;

          ??async?use(ctx:?Context,?next:?Function)?{
          ????ctx.a?=?this.MathService.sum(1,?2);
          ????await?next();
          ??}
          }
          復(fù)制代碼

          為react新增Slot插槽概念


          它支持Slot插槽模式,我們可以通過useSlot獲得Provider與Consumer。它是一種通過消息傳送節(jié)點片段的模式。

          const?{?Provider,?Consumer?}?=?useSlot(ctx.app);
          "foo">provider?data
          "foo">placeholder
          復(fù)制代碼

          然后編寫一個IOCComponent或者傳統(tǒng)組件。

          //?template.tsx
          import?{?useSlot?}?from?'@typeclient/react';
          @Component()
          class?uxx?implements?ComponentTransform?{
          ??render(props:?any)?{
          ????const?{?Consumer?}?=?useSlot(props.ctx);
          ????return?

          ??????

          title


          ??????"foo"?/>
          ??????{props.children}
          ????

          ??}
          }
          復(fù)制代碼

          最后在Controller上調(diào)用

          import?{?inject?}?from?'inversify';
          import?{?Route,?Controller?}?from?'@typeclient/core';
          import?{?useSlot?}?from?'@typeclient/react';
          import?{?uxx?}?from?'./template.tsx';
          @Controller()
          @Template(uxx)
          class?router?{
          ??@inject(ttt)?private?readonly?ttt:?ttt;
          ??@Route('/test')
          ??test()?{
          ????const?{?Provider?}?=?useSlot(props.ctx);
          ????return?

          ??????child?...
          ??????"foo">
          ????????this?is?foo?slot
          ??????
          ????

          ??}
          }
          復(fù)制代碼

          你能看到的結(jié)構(gòu)如下:


          ??

          title


          ??this?is?foo?slot
          ??
          child?...


          復(fù)制代碼

          解構(gòu)項目的原則


          我們可以通過對IOC服務(wù)與Middleware還有組件進行不同緯度的解構(gòu),封裝成統(tǒng)一的npm包上傳到私有倉庫中供公司內(nèi)部開發(fā)使用。

          類型

          1. IOCComponent + IOCService
          2. IOCMiddleware + IOCService
          3. IOCMiddlewware
          4. IOCService

          原則

          1. 通用化
          2. 內(nèi)聚合
          3. 易擴展

          遵循這種原則的化可以使公司的業(yè)務(wù)代碼或者組件具有高度的復(fù)用性,而且通過AOP能夠很清楚直觀的表現(xiàn)代碼即文檔的魅力。

          通用化


          即保證所封裝的邏輯、代碼或者組件具體高度的通用化特性,對于不太通用的沒必要封裝。比如說,公司內(nèi)部統(tǒng)一的導(dǎo)航頭,導(dǎo)航頭有可能被用到任意項目中做統(tǒng)一化,那么就非常適合封裝為組件型模塊。

          內(nèi)聚性


          通用的組件需要得到統(tǒng)一的數(shù)據(jù),那么可以通過IOCComponent + IOCService + IOCMiddleware的形式將其包裝,在使用的適合只需要關(guān)注導(dǎo)入這個組件即可。還是舉例通用導(dǎo)航頭。比如導(dǎo)航頭需要下拉一個團隊列表,那么,我們可以這樣定義這個組件:

          一個service文件:

          //?service.ts
          import?{?Service?}?from?'@typeclient/core';
          @Service()
          export?class?NavService?{
          ??getTeams()?{
          ????//?...?這里可以是ajax請求的結(jié)果
          ????return?[
          ??????{
          ????????name:?'Team?1',
          ????????id:?1,
          ??????},
          ??????{
          ????????name:?'Team?2',
          ????????id:?1,
          ??????}
          ????]
          ??}

          ??goTeam(id:?number)?{
          ????//?...
          ????console.log(id);
          ??}
          }
          復(fù)制代碼

          組件:

          //?component.ts
          import?React,?{?useEffect,?setState?}?from?'react';
          import?{?Component,?ComponentTransform?}?from?'@typeclient/react';
          import?{?NavService?}?from?'./service';

          @Component()
          export?class?NavBar?implements?ComponentTransform?{
          ??@inject(NavService)?private?readonly?NavService:?NavService;
          ??render()?{
          ????const?[teams,?setTeams]?=?setState'getTeams']>>([]);
          ????useEffect(()?=>?this.NavService.getTeams().then(data?=>?setTeams(data)),?[]);
          ????return?

            ??????{
            ????????teams.map(team?=>??this.NavService.goTeam(team.id)}>{team.name})
            ??????}
            ????

          ??}
          }
          復(fù)制代碼

          我們將這個模塊定義為@fe/navbar,同時導(dǎo)出這個個對象:

          //?@fe/navbar/index.ts
          export?*?from?'./component';
          復(fù)制代碼

          在任意的IOC組件中就可以這樣調(diào)用

          import?React?from?'react';
          import?{?Component,?ComponentTransform,?useComponent?}?from?'@typeclient/react';
          import?{?NavBar?}?from?'@fe/navbar';

          @Component()
          export?class?DEMO?implements?ComponentTransform?{
          ??@inject(NavBar)?private?readonly?NavBar:?NavBar;
          ??render()?{
          ????const?NavBar?=?useComponent(this.NavBar);
          ????return?
          ??}
          }
          復(fù)制代碼

          你可以發(fā)現(xiàn)只要加載這個組件,相當(dāng)于請求數(shù)據(jù)都自動被載入了,這就非常有區(qū)別與普通的組件模式,它可以是一種業(yè)務(wù)型的組件解構(gòu)方案。非常實用。

          易擴展


          主要是讓我們對于設(shè)計這個通用型的代碼或者組件時候保持搞擴展性,比如說,巧用SLOT插槽原理,我們可以預(yù)留一些空間給插槽,方便這個組件被使用不同位置的代碼所傳送并且替換掉原位置內(nèi)容,這個的好處需要開發(fā)者自行體會。

          演示


          我們提供了一個demo來表現(xiàn)它的能力,而且可以從代碼中看到如何解構(gòu)整個項目。我們的每個Controller都可以獨立存在,使得項目內(nèi)容遷移變得非常容易。

          • 框架:?github.com/flowxjs/Typ…
          • 項目模板:?github.com/flowxjs/Typ…
          • 簡單的最佳實踐:?github.com/flowxjs/Typ…

          大家可以通過以上的兩個例子來了解開發(fā)模式。



          總結(jié)


          新的開發(fā)理念并不是讓你摒棄掉傳統(tǒng)的開發(fā)方式和社區(qū),而且提供更好的思路。當(dāng)然,這種思路的好與壞,各有各的理解。但是我還是想聲明下,我今天僅僅是提供一種新的思路,大家看看就好,喜歡的給個star。非常感謝!



          點擊左下角閱讀原文,到?SegmentFault 思否社區(qū)?和文章作者展開更多互動和交流。

          -?END -

          瀏覽 44
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  视频国产精品 | 日韩精品av在线 日韩精品一级毛斤 | 欧美性爱综合网站 | 日韩AV一二三 | 青青草23视频 |