Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 我想看一级黄色毛片,国产成人精品永久免费视频,不卡一区二区在线观看

          整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          HTML 2 PDF通用能力的設(shè)計(jì)與實(shí)現(xiàn)




          目前,教學(xué)、教研各種內(nèi)容線上沉淀、展示豐富多彩,但線上內(nèi)容“線下化”能力不足或過分依賴人力,比如,線上練習(xí)題組卷后以PDF形式分發(fā)給學(xué)生,家長希望將考試、練習(xí)題目打印后,學(xué)生帶到學(xué)校去做(高中生使用手機(jī)等電子設(shè)備的時(shí)間有限),線上各類分析報(bào)告以PDF形式分享給學(xué)生/家長等。


          從業(yè)務(wù)方面看,不同業(yè)務(wù)線的多個(gè)業(yè)務(wù)場景都有輸出PDF的訴求,如果各業(yè)務(wù)線自己設(shè)計(jì)、實(shí)現(xiàn)符合自身業(yè)務(wù)場景的具體方案,除調(diào)研、開發(fā)工作量較大之外,還會(huì)有重復(fù)調(diào)研,踩坑的情況。


          從技術(shù)角度看,線上內(nèi)容轉(zhuǎn)PDF的內(nèi)容源頭來自于H5富文本內(nèi)容,業(yè)界內(nèi)以此為基礎(chǔ)的PDF生成方案多種多樣,也各有優(yōu)劣,比如:


          方案對比-表格-1


          因此,我們綜合了各種PDF生成方案并總結(jié)了在探索講義生成PDF過程中的經(jīng)驗(yàn),抽象出了一套通用的,可復(fù)用的能力供各業(yè)務(wù)線快速利用,基本方案和優(yōu)劣如下:


          最終方案-表格-2


          目 標(biāo)




          旨在提供一套以H5為載體的PDF通用生成方案,這套方案有如下特點(diǎn):

          1. 通用性強(qiáng):能夠處理各類H5頁面,從分頁到生成,做到一套方案,多般兼容。
          2. 擴(kuò)展性、配置性強(qiáng):各場景可根據(jù)自己的需要自定義頁眉、頁腳、頁碼,水印,背景等配置,做到輸出形式豐富多彩。
          3. 方便易接入:各業(yè)務(wù)場景只需關(guān)注要展現(xiàn)的內(nèi)容,無需關(guān)注分頁,PDF生成等背后的處理 ,為需要產(chǎn)出PDF的業(yè)務(wù)場景提效賦能;整體來看,調(diào)研、設(shè)計(jì)、開發(fā)(+踩坑)一整套 H5 轉(zhuǎn) PDF的能力至少需要近 30人/日,我們希望這套通用能力的接入成本控制在 7人/日左右;在很多場景接入后,從實(shí)際反饋來看,平均只需要 2-3 人/日就接入了。
          4. 質(zhì)量高:保證輸出PDF中內(nèi)容的展示與H5中無異,各種復(fù)雜公式的展示也絲毫無差錯(cuò)。
          5. 性能好:保證 1 分鐘內(nèi)能處理 100+ 的 20頁左右的PDF生成任務(wù)
          6. 穩(wěn)定性高:保證有各種兜底策略妥善處理各類異常,同時(shí)能夠通過限流方案應(yīng)對突發(fā)流量,保證服務(wù)穩(wěn)定。


          這套方案可分為兩個(gè)核心部分,頁面展示側(cè) - Medusa,PDF生成側(cè) - Hydra


          頁面展示側(cè) - Medusa




          我們頁面展示側(cè)的通用能力——Medusa,是基于Paged.js的二次封裝,并以NPM包形式提供給業(yè)務(wù)方使用。Medusa可對任何HTML進(jìn)行分頁、并根據(jù)配置添加頁眉、頁腳等,最終將處理后的HTML渲染到頁面中。Medusa封裝并簡化了對PDF格式的配置,可覆蓋絕大多數(shù)業(yè)務(wù)場景,使得各業(yè)務(wù)場景將更多精力投入其自身業(yè)務(wù)邏輯的開發(fā)。


          之所以選擇Pagedjs為基礎(chǔ)開發(fā)我們自己的SDK,是因?yàn)樗悄壳拔覀兡苷业降奈ㄒ?/span>開源的、具有HTML內(nèi)容分頁,樣式處理的前端庫,同時(shí)我們也在講義中經(jīng)過了長期的摸索與沉淀。


          接下來將詳細(xì)介紹Paged.js原理、Medusa支持的功能與使用方法。


          一 Paged.js是如何工作的




          Paged.js包含了 3 個(gè)大模塊

          • Chunker(負(fù)責(zé)HTML內(nèi)容分頁)
          • Polisher (負(fù)責(zé)CSS樣式處理)
          • Previewer (負(fù)責(zé)預(yù)覽呈現(xiàn)Chunker和Polisher處理后的內(nèi)容)

          這里將主要介紹 Previewer 和 Chunker,因?yàn)槲覀兊亩伍_發(fā)和維護(hù)不涉及到Polisher。


          Previewer

          Previewer 的工作非常簡單,但我們會(huì)主要利用它封裝我們的Medusa,初始化一個(gè)Previewer對象,Previewer初始化了Chunker和Polisher對象:


          Medusa-代碼-1


          再調(diào)用Previewer的preview()方法,preview()方法做了兩件事:

          1. 通過Polisher處理樣式內(nèi)容
          2. 通過Chunker處理需要分頁的HTML內(nèi)容,如果沒有指定需要分頁的HTML,則會(huì)處理整個(gè)Body的內(nèi)容

          Medusa-代碼-2


          當(dāng)chunker.flow結(jié)束,即可在瀏覽器看到整個(gè)頁面處理完之后的樣子。


          Chunker


          首先,Chunker解析、預(yù)處理需要分頁的HTML,為其添加一些必要的屬性


          Medusa-代碼-3


          然后創(chuàng)建容納所有頁(pages)的容器,并掛載到renderTo容器下(默認(rèn)Body),以備組織后續(xù)的所有頁:



          Medusa-代碼-4


          接著,chunker創(chuàng)建了一個(gè)page模版,以便增加頁面使用:


          Medusa-代碼-5


          其中,TEMPLATE是Pagedjs內(nèi)部創(chuàng)建頁面時(shí)所使用的基礎(chǔ)模版。


          Medusa-代碼-6


          接下來,chunker進(jìn)入了渲染+分頁過程(這個(gè)過程我們不會(huì)在二次開發(fā)中做修改,但需要了解其基本思路以便在出問題時(shí)能有解決思路),這個(gè)過程在循環(huán)一個(gè)迭代器(*layout),迭代器一直在做3件事:


          1. 將內(nèi)容添加到模版內(nèi)容區(qū)域的容器中 -> 渲染。
          2. 探測overflow,找到overflow的offset并創(chuàng)建BreakToken (探測overflow過程中很多處都用到了迭代器,此處為了說明思路,簡化了相關(guān)代碼)。


          原則:

          尋找overflow時(shí)會(huì)將盡可能多的內(nèi)容節(jié)點(diǎn)插入內(nèi)容區(qū)域,這里,“盡可能多”分為幾種情況,比如:

          • 沒有剩余節(jié)點(diǎn)需要再添加了
          • 達(dá)到了一頁所能承載的最大字符數(shù);剛開始的時(shí)候,如果沒有指定每頁的最大字符數(shù),Pagedjs會(huì)給一個(gè)默認(rèn)值為 1500 的每頁最大字符用做判斷,在之后會(huì)記錄分隔好的每一頁中的字符數(shù),并取最近4頁 (少于4頁取全部)的平均值作為之后分頁的判斷條件,這里,Pagedjs相當(dāng)于對每一頁中能夠承載的內(nèi)容做了一個(gè)簡單的預(yù)測,這個(gè)算法對于比較規(guī)律的內(nèi)容做分頁時(shí)還是比較簡單高效的。

          步驟:

          Pagedjs遵循了如下步驟去尋找overflow:

          兩個(gè)前置條件:

          • 內(nèi)容區(qū)域盒子邊界已經(jīng)確定,下面以contentArea.right 和 contentArea.bottom 分別代指其右邊界和下邊界。
          • 處理過程中每個(gè)節(jié)點(diǎn)的邊界可以計(jì)算(對于文字節(jié)點(diǎn),Pagedjs中使用了Range對象為其創(chuàng)建邊界),下面以 node.left、 node.right、node.top 和 node.bottom 分別代指節(jié)點(diǎn)的左、右、上、下邊界。

          i. 從需要處理的內(nèi)容第一個(gè)節(jié)點(diǎn)開始,判斷是否 node.left >= contentArea.right || node.top >= contentArea.bottom


          Medusa-代碼-7


          ii.如果不滿足,則判斷 node.right <= contentArea.right && node.bottom <= contentArea.bottom


          Medusa-代碼-8


          iii.如果不滿足,那說明有子節(jié)點(diǎn)overflow了,則繼續(xù)深入其子節(jié)點(diǎn)查找即可。


          3.使用模版添加新的頁面,并從BreakToken處繼續(xù)上述動(dòng)作。


          二 Medusa支持的功能及使用方法




          基于Paged.js,Medusa支持了如下功能,并為業(yè)務(wù)方提供了更加簡潔、定制化的配置。


          1. 動(dòng)態(tài)頁面分頁能力
          2. 單頁模版配置 -> 生成能力
          3. 前、后置靜態(tài)頁面生成、與分頁后的動(dòng)態(tài)頁面拼接能力
          4. 頁面處理成功后,通知PDF生成服務(wù)(Hydra)執(zhí)行任務(wù)


          下方是調(diào)用Medusa的代碼示例:


          Medusa-代碼-9


          1.1 動(dòng)態(tài)頁面分頁能力

          Medusa核心功能,可將連續(xù)的HTML頁面轉(zhuǎn)化成一頁頁P(yáng)DF樣式的HTML。


          1.2 單頁模版配置 -> 生成能力


          通過Grid布局,Paged.js將一個(gè)單頁模版分為多個(gè)區(qū)域,整體分為2個(gè)大的部分:

          1. base 頁面基礎(chǔ)配置:每個(gè)PDF紙型、水印,內(nèi)容區(qū)域的寬高、margin與padding等等
          2. surround 頁面周圍區(qū)域:如頁眉、頁腳等配置


          業(yè)務(wù)方通過簡單的配置,即可還原UI設(shè)計(jì)稿中的PDF樣式,例子如下圖:



          1.2.1 base

          頁面基礎(chǔ)配置是對每頁的。支持紙型或頁面寬高、內(nèi)容區(qū)域margin、padding、背景及水印的設(shè)置。



          在封裝Medusa時(shí),Medusa將讀取傳入的頁面模版配置、靜態(tài)頁內(nèi)容配置,并將樣式上的配置解析并轉(zhuǎn)化為Previewer可理解的樣式內(nèi)容,比如頁面寬高的設(shè)置:


          Medusa-代碼-10


          將被轉(zhuǎn)化為:


          Medusa-代碼-11


          1.2.2 surround


          1. 可以看到圖中的16種不同位置的surround區(qū)域。通過設(shè)置position,可將業(yè)務(wù)方自定義的元素渲染到對應(yīng)的位置上。



          2. 目前支持3種類型的surround item:

          • text 文字
          • img 圖片
          • pageNum (動(dòng)態(tài)獲取)當(dāng)前頁碼


          example:


          Medusa-代碼-12


          1.3 前/后置靜態(tài)頁面


          業(yè)務(wù)方可通過如下方式配置靜態(tài)頁面的具體內(nèi)容:


          Medusa-代碼-13


          其中,傳入的React JSX Element將會(huì)被這樣處理:


          Medusa-代碼-14


          處理完成后,將HTML String拼接到頁面模版中,再插入分頁后內(nèi)容的前后。


          PDF生成側(cè) - Hydra:




          頁面展示側(cè)為PDF生成做好了頁面的準(zhǔn)備,對于PDF生成側(cè),需要做的工作就更純粹了,業(yè)務(wù)方除了請求生成PDF,定期檢查PDF生成的進(jìn)度,無需做任何額外工作。


          1.整體流程:

          PDF生成是CPU和內(nèi)存密集型的,由于頁面內(nèi)容的不確定性,也意味著頁面渲染時(shí)間與生成PDF的時(shí)間都是不確定的,因此整體PDF生成的鏈路被設(shè)計(jì)成是異步的,如下圖:



          整體流程上,業(yè)務(wù)方在請求生成PDF時(shí),會(huì)先在后端做一條記錄,后端再將任務(wù)發(fā)送給Node服務(wù),即Hydra;


          在生成PDF時(shí), 第 1 步是做頁面上的準(zhǔn)備,一個(gè)生成任務(wù)可能有多個(gè)URL頁面需要生成PDF,所以我們預(yù)先啟動(dòng)對應(yīng)URL數(shù)量的PPTR Page,頁面都啟動(dòng)完成后,進(jìn)入下一步;


          第 2 步:渲染頁面,這個(gè)過程中,如果請求是包含多個(gè)URL的,這些頁面會(huì)同步渲染,在所有頁面渲染完成后,進(jìn)入下一步。


          第 2.5 步,如果是需要生成連續(xù)頁碼的一整個(gè)PDF,還會(huì)做額外的一個(gè)動(dòng)作:頁碼矯正,通過頁碼矯正,可以將同步渲染的每個(gè)頁面,按照其之前頁面的頁碼數(shù)修正,以保證整體PDF的頁碼的連貫。


          第 3 步,通過PPTR Page的能力將頁面轉(zhuǎn)換為PDF buffer,如有必要,再將生成的PDF buffer拼接到一起生成一整個(gè)PDF,或者將每個(gè)PDF buffer都生成一個(gè)PDF,壓縮成zip文件。


          第 4 步,文件上傳OSS,最終返回OSS CDN鏈接。


          2.請求生成PDF:


          業(yè)務(wù)側(cè)請求將對應(yīng)頁面生成PDF的時(shí),只需傳入如下字段:


          Hydra-代碼-1


          3.PDF生成過程:


          正如在整體流程中所述,PDF生成側(cè),我們借助 PPTR 的能力打開頁面并生成PDF流。


          在頁面調(diào)用 Medusa 分頁、組裝能力時(shí),所有內(nèi)容分頁組裝完成后會(huì)向body中插入了一個(gè)額外的DOM以標(biāo)識該頁面處理完成:


          Hydra-代碼-2


          這是為了 Hydra 感知頁面渲染完成所做的準(zhǔn)備,當(dāng)生成服務(wù)的 PPTR 等到該DOM出現(xiàn)時(shí),則表示頁面成功渲染并處理完成了:


          Hydra-代碼-3


          此后,在上面已經(jīng)提到過,對于需要將多個(gè)頁面生成的PDF拼接成一個(gè)PDF的情況,在生成PDF之前需要做一個(gè)重要的動(dòng)作,即頁碼矯正,原因如下:


          1. 每個(gè)頁面無法感知其他頁面情況的,如:第二個(gè)頁面不知道第一個(gè)頁面會(huì)生成多少頁的PDF。
          2. 它們的頁碼需要是連續(xù)的。


          并且我們不希望頁面的處理是串行的,因?yàn)榇袆荼貙?dǎo)致速度較慢,生成時(shí)間長。


          這個(gè)問題的解決方案如下:

          1. 對于每個(gè)頁面都啟用一個(gè)page,并同時(shí)處理

          2. 每個(gè)頁面處理完成后(pdfLastDOM出現(xiàn)),通過Page.$eval()來統(tǒng)計(jì)頁數(shù)并記錄:

          Hydra-代碼-4


          3. 計(jì)算出頁面中分頁之后每一個(gè)頁面的起始頁碼,以及所有頁面的頁碼總和

          4. 再修改頁碼容器樣式的 counterReset 值即可,其后續(xù)頁碼可自遞增。


          Hydra-代碼-5


          5. 之后,再通過 Medusa 在頁面window對象中Polyfill的相關(guān)配置,比如需要生成的PDF的單頁寬、高以生成PDF流。


          Hydra-代碼-6


          6. 最后如有必要,通過pdf-lib拼接這些 pdfBuffer 即可。


          Hydra-代碼-7


          7. PDF生成完成后,上傳OSS并返回URL鏈接


          4.性能、穩(wěn)定性保證:


          在整體方案落地前,我們對服務(wù)進(jìn)行了多次性能測試:


          以下載題目為例,在4個(gè)容器,每個(gè)容器 3C 12G 的配置下的并行處理能力如下:


          對于 20 道題目,每個(gè)PDF生成任務(wù)在 15 頁左右,平均 1 分鐘內(nèi)能完成 280 個(gè)任務(wù)的處理。

          對于 40 道題目,每個(gè)PDF生成任務(wù)在 30 頁左右,平均 1 分鐘內(nèi)能完成 105 個(gè)任務(wù)的處理。

          對于 60 到題目,每個(gè)PDF生成任務(wù)在 40 頁左右,平均 1 分鐘內(nèi)能完成 54 個(gè)任務(wù)的處理。


          同時(shí),根據(jù) Hydra 服務(wù)的整體的處理能力,后端通過任務(wù)隊(duì)列的形式幫助我們保證服務(wù)不被瞬間的突刺流量擊垮。


          已接入/正在接入的相關(guān)業(yè)務(wù)線及場景:




          目前,公司有 5 大業(yè)務(wù)線,8 個(gè)場景已經(jīng)完全接入我們的能力用于 H5 轉(zhuǎn) PDF,如下是錯(cuò)題本、內(nèi)容資料庫接入后生成的PDF樣例:


          錯(cuò)題本:




          內(nèi)容資料庫試卷:




          未來展望




          目前整體的PDF生成方案已經(jīng)能夠滿足大多數(shù)場景和內(nèi)容,但依然有可改進(jìn)空間。


          HTML的流式布局要求我們必須手動(dòng)的對內(nèi)容分頁,才能添加頁眉,頁腳等(即Mdusa做的工作),正因?yàn)槿绱?,在處理?fù)雜的內(nèi)容時(shí),可能會(huì)出現(xiàn)一些問題:比如,遇到復(fù)雜表格時(shí),由于表格可能會(huì)有多種多樣的行、列合并,同時(shí)表格單元格內(nèi)的內(nèi)容也可以多種多樣,在分頁過程中,Medusa內(nèi)部的PagedJS并不能完美的處理對于長、且復(fù)雜的表格的分割,因此可能遇到分割后表格單元格缺失、錯(cuò)亂或?qū)捀咤e(cuò)誤的問題,這些問題在講義中體現(xiàn)較明顯。


          我們?nèi)栽诔掷m(xù)關(guān)注與研究復(fù)雜DOM內(nèi)容的分割問題,會(huì)嘗試加以優(yōu)化和改進(jìn)PagedJS的能力,同時(shí),我們也以另外一種思路設(shè)計(jì)了自己的DOM分頁器方案,但經(jīng)過評估,由于實(shí)現(xiàn)比較復(fù)雜,成本較高,暫時(shí)沒有投入開發(fā)資源。


          不過,我們相信,未來我們一定能以更完美的方式分割DOM以生成更高質(zhì)量的PDF。


          作者:高源、陳欣博

          來源:微信公眾號:高途技術(shù)

          出處:https://mp.weixin.qq.com/s/c_N7jdNklrNFKR_Cub2Tgg

          多朋友都一定聽說過競價(jià)單頁而且不管是什么行業(yè)醫(yī)療還是淘寶客產(chǎn)品基本上都會(huì)采用競價(jià)單頁來進(jìn)行營銷自己的產(chǎn)品在這一點(diǎn)上我們可以看到的是大家都比較親睞于競價(jià)單頁但是對于競價(jià)單頁還不是特別的了解清晰對于競價(jià)單頁該體現(xiàn)哪些內(nèi)容可能不太了解所以導(dǎo)致很多的單頁效果做的不是特別好所以小編來解答為大家介紹下什么是競價(jià)單頁?競價(jià)單頁該體現(xiàn)哪些內(nèi)容?


          什么是競價(jià)單頁?

          競價(jià)單頁一般是由個(gè)到幾個(gè)HTML組成有的頁面只是單獨(dú)的介紹頁有的做的是比較精美的頁面,這主要是和用戶體驗(yàn)有直接的關(guān)聯(lián)隨著競價(jià)單頁越來越有營銷意義很多公司都采用這樣的模式進(jìn)行營銷自己的產(chǎn)品衡量一個(gè)競價(jià)單頁是否好主要是考核其流量與轉(zhuǎn)化率那么對于競價(jià)單頁的內(nèi)容要求就高了許多

          競價(jià)單頁該體現(xiàn)哪些內(nèi)容?

          一.做競價(jià)單頁的目的性

          做競價(jià)頁面的目的性小編將這一點(diǎn)放在第一個(gè)是有原因的因?yàn)樾【幙催^很多的競價(jià)單頁做的不是很好,主要的原因是策劃不清楚的了解這個(gè)頁面是做了干什么這樣的目的怎么會(huì)策劃出好的競價(jià)單頁呢?所以說策劃在競價(jià)單頁的時(shí)候一定要清楚你最初的目的是什么?這樣做出的專題才不會(huì)跑題,尤其是小編建議做競價(jià)單頁的時(shí)候一定要很好的構(gòu)思不要一會(huì)想做這樣一會(huì)做成那樣會(huì)顯得整個(gè)頁面沒有中心點(diǎn)。


          二.相關(guān)媒體報(bào)道的營銷

          小編現(xiàn)在需要說的是競價(jià)單頁相關(guān)媒體報(bào)道的營銷這一點(diǎn)上是需要特別注意的因?yàn)槿绻阕龅暮檬欠浅?尚诺臅?huì)增加用花對你的喜愛但是如果你做的很假會(huì)有相反的效果對于競價(jià)單頁的營銷主要靠媒體推廣等來營銷的引用主要媒體的視頻文章等來達(dá)到推廣營銷的目的比如央視網(wǎng)搜狐新浪網(wǎng)易騰訊等主要媒體所以說小編建議大家在做競價(jià)單頁的時(shí)候可以采用這類營銷。

          三.營銷產(chǎn)品的效果與功效

          營銷產(chǎn)品的效果與功效是比較重要的因?yàn)槟闼鶢I銷的產(chǎn)品如果沒療效再好的競價(jià)單頁一樣沒有用而且你在競價(jià)單頁一定要凸顯出你營銷產(chǎn)品的效果與功效相信這點(diǎn)大家都會(huì)知道因?yàn)槲覀兙褪菫榱藸I銷產(chǎn)品而做的單頁所以說在做競價(jià)單頁的時(shí)候必須要將你的優(yōu)勢很好的展現(xiàn)給你的用戶看才行如果你的產(chǎn)品營銷不到位那么做了有什么意義呢?所以說一定要在單頁上把自己的營銷產(chǎn)品的效果與功效盡善盡美的體現(xiàn)。

          四.數(shù)量的展現(xiàn)(發(fā)貨量成交量咨詢量)

          在這一點(diǎn)上小編想說的是競價(jià)單頁的數(shù)量展現(xiàn)主要表現(xiàn)在發(fā)貨量成交量咨詢量方面如果你是做產(chǎn)品營銷那么你一定要做好這三點(diǎn)將這三個(gè)點(diǎn)做好因?yàn)榘l(fā)貨量成交量咨詢量是用戶非常關(guān)注的試想一下如果你的競價(jià)單頁顯示很多的發(fā)貨量成交量咨詢量那么對于用戶來講是不是要增加很高的可信度那么你的用戶還不會(huì)在你的頁面停留嗎?所以說想要營銷必須抓住用戶的心理。


          五.在線咨詢在線問答與自主訂單

          小編相信做競價(jià)單頁的朋友都知道一定要有在線咨詢因?yàn)楹芏嗟膯雾摮善娜窃诰€咨詢所以說小編估計(jì)不用說大家都知道但是小編想提醒的是過多的在線咨詢會(huì)降低用戶體驗(yàn)度在這一點(diǎn)上是無需質(zhì)疑的還有在線問答與自主訂單是雙方面的用戶體驗(yàn)對于用戶來講就是很好的提升但是很多朋友都沒有考慮到這一點(diǎn)沒有很好的尊重用戶體驗(yàn)這就對用戶造成很大的偏差。

          六.用戶體驗(yàn)的提升才是關(guān)鍵

          小編在前面說了很多相信大家對于競價(jià)單頁需要做的內(nèi)容都很清晰明了了而最后一點(diǎn)小編想特別說明下用戶體驗(yàn)因?yàn)檫@一點(diǎn)是朋友往往給忽略的就像是一些淘寶客站不尊重用戶體驗(yàn)成片的廣告這就對用戶造成很大的困擾銷量往往是很少所以說用戶體驗(yàn)的提升才是關(guān)鍵沒有好的用戶體驗(yàn)再漂亮的競價(jià)單頁一定沒有銷量這是應(yīng)該直接大家一起去研究的 。

          頁應(yīng)用程序 (SPA) 因其固有的豐富用戶體驗(yàn)而成為一種常用的 Web 應(yīng)用程序。 將客戶端 SPA 框架或庫(例如 Angular 或 React)與服務(wù)器端框架(例如 ASP.NET Core)集成在一起可能會(huì)很困難。 開發(fā) JavaScript Services 就是為了減少集成過程中的摩擦。 使用它可以在不同的客戶端和服務(wù)器技術(shù)堆棧之間無縫操作。

          警告

          本文所述的功能自 ASP.NET Core 3.0 起被棄用。 Microsoft.AspNetCore.SpaServices.Extensions NuGet 包提供了一種更簡單的 SPA 框架集成機(jī)制。 有關(guān)詳細(xì)信息,請參閱 [Announcement] Obsoleting Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices([公告] 棄用 Microsoft.AspNetCore.SpaServices 和 Microsoft.AspNetCore.NodeServices)。

          什么是 JavaScript Services

          JavaScript Services 是用于 ASP.NET Core 的客戶端技術(shù)集合。 其目標(biāo)是將 ASP.NET Core 定位為開發(fā)人員生成 SPA 時(shí)的首選服務(wù)器端平臺。

          JavaScript Services 由兩個(gè)不同的 NuGet 包組成:

          • Microsoft.AspNetCore.NodeServices (NodeServices)
          • Microsoft.AspNetCore.SpaServices (SpaServices)

          這些包在以下情況下很有用:

          • 在服務(wù)器上運(yùn)行 JavaScript
          • 使用 SPA 框架或庫
          • 通過 Webpack 生成客戶端資產(chǎn)

          本文重點(diǎn)介紹了如何使用 SpaServices 包。

          什么是 SpaServices

          創(chuàng)建 SpaServices 的目的是將 ASP.NET Core 定位為開發(fā)人員生成 SPA 時(shí)的首選服務(wù)器端平臺。 使用 ASP.NET Core 開發(fā) SPA 時(shí)不一定要使用 SpaServices,SpaServices 也不會(huì)將開發(fā)人員束縛在特定的客戶端框架中。

          SpaServices 可提供有用的基礎(chǔ)結(jié)構(gòu),例如:

          • 服務(wù)器端預(yù)呈現(xiàn)
          • Webpack 開發(fā)中間件
          • 熱模塊更換
          • 路由幫助程序

          將這些基礎(chǔ)結(jié)構(gòu)組件結(jié)合使用時(shí),可增強(qiáng)開發(fā)工作流和運(yùn)行時(shí)體驗(yàn)。 這些組件也可單獨(dú)使用。

          使用 SpaServices 的先決條件

          若要使用 SpaServices,請安裝以下各項(xiàng):

          • 帶有 npm 的 Node.js(版本 6 或更高版本)若要確保已安裝并且可找到這些組件,請從命令行運(yùn)行以下命令:控制臺復(fù)制node -v && npm -v 如果部署到 Azure 網(wǎng)站,則無需執(zhí)行任何操作 — 已經(jīng)在服務(wù)器環(huán)境中安裝 Node.js,并且 Node.js 可供使用。
          • .NET Core SDK 2.0 或更高版本在使用 Visual Studio 2017 的 Windows 上,通過選擇“.NET Core 跨平臺開發(fā)”工作負(fù)載來安裝該 SDK。
          • Microsoft.AspNetCore.SpaServices NuGet 包

          服務(wù)器端預(yù)呈現(xiàn)

          通用(也稱為同構(gòu))應(yīng)用程序是一種能夠在服務(wù)器和客戶端上運(yùn)行的 JavaScript 應(yīng)用程序。 Angular、React 和其他常用框架針對這種應(yīng)用程序開發(fā)風(fēng)格提供一個(gè)通用平臺。 其思路是先通過 Node.js 在服務(wù)器上呈現(xiàn)框架組件,然后將進(jìn)一步的執(zhí)行任務(wù)委托給客戶端。

          SpaServices 提供的 ASP.NET Core 標(biāo)記幫助程序通過調(diào)用服務(wù)器上的 JavaScript 函數(shù)來簡化服務(wù)器端預(yù)呈現(xiàn)的實(shí)現(xiàn)。

          服務(wù)器端預(yù)呈現(xiàn)的先決條件

          安裝 aspnet-prerendering npm 包:

          控制臺

          npm i -S aspnet-prerendering

          服務(wù)器端預(yù)呈現(xiàn)配置

          可以通過在項(xiàng)目的 _ViewImports.cshtml 文件中注冊命名空間來發(fā)現(xiàn)標(biāo)記幫助程序:

          CSHTML

          @using SpaServicesSampleApp
          @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
          @addTagHelper "*, Microsoft.AspNetCore.SpaServices"

          這些標(biāo)記幫助程序通過在 Razor 視圖中利用類似 HTML 的語法來抽象化與低級 API 直接通信的復(fù)雜性:

          CSHTML

          <app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>

          asp-prerender-module 標(biāo)記幫助程序

          上面的代碼示例中使用的 asp-prerender-module 標(biāo)記幫助程序通過 Node.js 在服務(wù)器上執(zhí)行 ClientApp/dist/main-server.js。 為清楚起見,main-server.js 文件是 Webpack 生成過程中 TypeScript 到 JavaScript 轉(zhuǎn)譯任務(wù)的產(chǎn)物。 Webpack 定義了入口點(diǎn)別名 main-server;此別名的依賴項(xiàng)關(guān)系圖遍歷始于 ClientApp/boot-server.ts 文件:

          JavaScript

          entry: { 'main-server': './ClientApp/boot-server.ts' },

          在以下 Angular 示例中,ClientApp/boot-server.ts 文件利用 createServerRenderer 函數(shù)和 aspnet-prerendering npm 包的 RenderResult 類型通過 Node.js 來配置服務(wù)器呈現(xiàn)。 用于服務(wù)器端呈現(xiàn)的 HTML 標(biāo)記傳遞到解析函數(shù)調(diào)用,該調(diào)用包裝在強(qiáng)類型的 JavaScript Promise 對象中。 Promise 對象的意義在于,它以異步方式將 HTML 標(biāo)記提供給頁面,以注入到 DOM 的占位符元素中。

          TypeScript

          import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
          
          export default createServerRenderer(params => {
              const providers = [
                  { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
                  { provide: 'ORIGIN_URL', useValue: params.origin }
              ];
          
              return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
                  const appRef = moduleRef.injector.get(ApplicationRef);
                  const state = moduleRef.injector.get(PlatformState);
                  const zone = moduleRef.injector.get(NgZone);
                  
                  return new Promise<RenderResult>((resolve, reject) => {
                      zone.onError.subscribe(errorInfo => reject(errorInfo));
                      appRef.isStable.first(isStable => isStable).subscribe(() => {
                          // Because 'onStable' fires before 'onError', we have to delay slightly before
                          // completing the request in case there's an error to report
                          setImmediate(() => {
                              resolve({
                                  html: state.renderToString()
                              });
                              moduleRef.destroy();
                          });
                      });
                  });
              });
          });

          asp-prerender-data 標(biāo)記幫助程序

          與 asp-prerender-module 標(biāo)記幫助程序結(jié)合使用時(shí),asp-prerender-data 標(biāo)記幫助程序可用于將上下文信息從 Razor 視圖傳遞到服務(wù)器端 JavaScript。 例如,以下標(biāo)記將用戶數(shù)據(jù)傳遞到 main-server 模塊:

          CSHTML

          <app asp-prerender-module="ClientApp/dist/main-server"
                  asp-prerender-data='new {
                      UserName = "John Doe"
                  }'>Loading...</app>

          收到的 UserName 參數(shù)使用內(nèi)置的 JSON 序列化程序進(jìn)行序列化,并存儲在 params.data 對象中。 在以下 Angular 示例中,該數(shù)據(jù)用于在 h1 元素內(nèi)構(gòu)造個(gè)性化問候語:

          TypeScript

          import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
          
          export default createServerRenderer(params => {
              const providers = [
                  { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
                  { provide: 'ORIGIN_URL', useValue: params.origin }
              ];
          
              return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
                  const appRef = moduleRef.injector.get(ApplicationRef);
                  const state = moduleRef.injector.get(PlatformState);
                  const zone = moduleRef.injector.get(NgZone);
                  
                  return new Promise<RenderResult>((resolve, reject) => {
                      const result = `<h1>Hello, ${params.data.userName}</h1>`;
          
                      zone.onError.subscribe(errorInfo => reject(errorInfo));
                      appRef.isStable.first(isStable => isStable).subscribe(() => {
                          // Because 'onStable' fires before 'onError', we have to delay slightly before
                          // completing the request in case there's an error to report
                          setImmediate(() => {
                              resolve({
                                  html: result
                              });
                              moduleRef.destroy();
                          });
                      });
                  });
              });
          });

          在標(biāo)記幫助程序中傳遞的屬性名稱用 PascalCase 表示法表示。 與之相反,JavaScript 用 camelCase 表示相同的屬性名稱。 默認(rèn)的 JSON 序列化配置是造成這種差異的原因所在。

          若要擴(kuò)展上面的代碼示例,可以通過解凍提供給 resolve 函數(shù)的 globals 屬性,將數(shù)據(jù)從服務(wù)器傳遞到視圖:

          TypeScript

          import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
          
          export default createServerRenderer(params => {
              const providers = [
                  { provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
                  { provide: 'ORIGIN_URL', useValue: params.origin }
              ];
          
              return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
                  const appRef = moduleRef.injector.get(ApplicationRef);
                  const state = moduleRef.injector.get(PlatformState);
                  const zone = moduleRef.injector.get(NgZone);
                  
                  return new Promise<RenderResult>((resolve, reject) => {
                      const result = `<h1>Hello, ${params.data.userName}</h1>`;
          
                      zone.onError.subscribe(errorInfo => reject(errorInfo));
                      appRef.isStable.first(isStable => isStable).subscribe(() => {
                          // Because 'onStable' fires before 'onError', we have to delay slightly before
                          // completing the request in case there's an error to report
                          setImmediate(() => {
                              resolve({
                                  html: result,
                                  globals: {
                                      postList: [
                                          'Introduction to ASP.NET Core',
                                          'Making apps with Angular and ASP.NET Core'
                                      ]
                                  }
                              });
                              moduleRef.destroy();
                          });
                      });
                  });
              });
          });

          globals 對象中定義的 postList 數(shù)組附加到瀏覽器的全局 window 對象。 將此變量提升到全局范圍可消除重復(fù)的工作,特別是在服務(wù)器上加載了一次數(shù)據(jù),之后又在客戶端上加載相同的數(shù)據(jù)時(shí)。

          Webpack 開發(fā)中間件

          Webpack 開發(fā)中間件引入了簡化的開發(fā)工作流,Webpack 可根據(jù)該工作流按需生成資源。 在瀏覽器中重新加載頁面時(shí),該中間件會(huì)自動(dòng)編譯并提供客戶端資源。 另一種方法是在第三方依賴項(xiàng)或自定義代碼發(fā)生更改時(shí),通過項(xiàng)目的 npm 生成腳本手動(dòng)調(diào)用 Webpack。 以下示例顯示了 package.json 文件中的 npm 生成腳本:

          JSON

          "build": "npm run build:vendor && npm run build:custom",

          Webpack 開發(fā)中間件的先決條件

          安裝 aspnet-webpack npm 包:

          控制臺復(fù)制

          npm i -D aspnet-webpack

          Webpack 開發(fā)中間件配置

          Webpack 開發(fā)中間件通過 Startup.cs 文件的 Configure 方法中的以下代碼注冊到 HTTP 請求管道中:

          C#

          if (env.IsDevelopment())
          {
              app.UseDeveloperExceptionPage();
              app.UseWebpackDevMiddleware();
          }
          else
          {
              app.UseExceptionHandler("/Home/Error");
          }
          
          // Call UseWebpackDevMiddleware before UseStaticFiles
          app.UseStaticFiles();

          通過 UseStaticFiles 擴(kuò)展方法注冊靜態(tài)文件托管之前,必須先調(diào)用 UseWebpackDevMiddleware 擴(kuò)展方法。 出于安全原因,僅在應(yīng)用以開發(fā)模式運(yùn)行時(shí)才注冊該中間件。

          webpack.config.js 文件的 output.publicPath 屬性指示中間件監(jiān)視 dist 文件夾中的更改:

          JavaScript

          module.exports = (env) => {
                  output: {
                      filename: '[name].js',
                      publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
                  },

          熱模塊更換

          可將 Webpack 的熱模塊更換 (HMR) 功能視作 Webpack 開發(fā)中間件的進(jìn)化版。 HMR 引入了所有相同的優(yōu)點(diǎn),但是通過在編譯更改后自動(dòng)更新頁面內(nèi)容,進(jìn)一步簡化了開發(fā)工作流。 不要將其與瀏覽器的刷新功能混淆,后者會(huì)干擾 SPA 的當(dāng)前內(nèi)存中狀態(tài)和調(diào)試會(huì)話。 Webpack 開發(fā)中間件服務(wù)與瀏覽器之間有一個(gè)實(shí)時(shí)鏈接,這意味著系統(tǒng)會(huì)將更改推送到瀏覽器。

          熱模塊更換的先決條件

          安裝 webpack-hot-middleware npm 包:

          控制臺

          npm i -D webpack-hot-middleware

          熱模塊更換配置

          必須在 Configure 方法中將 HMR 組件注冊到 MVC 的 HTTP 請求管道:

          C#

          app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
              HotModuleReplacement = true
          });

          與 Webpack 開發(fā)中間件一樣,調(diào)用 UseStaticFiles 擴(kuò)展方法之前,必須先調(diào)用 UseWebpackDevMiddleware 擴(kuò)展方法。 出于安全原因,僅在應(yīng)用以開發(fā)模式運(yùn)行時(shí)才注冊該中間件。

          webpack.config.js 文件必須定義一個(gè) plugins 數(shù)組,即便將其留空亦可:

          JavaScript

          module.exports = (env) => {
                  plugins: [new CheckerPlugin()]

          在瀏覽器中加載應(yīng)用后,開發(fā)人員工具的“控制臺”選項(xiàng)卡會(huì)提供 HMR 激活確認(rèn):

          路由幫助程序

          在大多數(shù)基于 ASP.NET Core 的 SPA 中,除服務(wù)器端路由外,通常還需要進(jìn)行客戶端路由。 SPA 和 MVC 路由系統(tǒng)可以獨(dú)立工作而互不干擾。 但是,有一種極端情況帶來了挑戰(zhàn):標(biāo)識 404 HTTP 響應(yīng)。

          以使用 /some/page 的無擴(kuò)展路由的情況為例。 假設(shè)請求的模式與服務(wù)器端路由不匹配,但與客戶端路由匹配。 現(xiàn)在以針對 /images/user-512.png 的傳入請求為例,該請求通常需要在服務(wù)器上查找映像文件。 如果請求的資源路徑與任何服務(wù)器端路由或靜態(tài)文件都不匹配,則客戶端應(yīng)用程序不太可能處理它 — 通常需要返回 404 HTTP 狀態(tài)代碼。

          路由幫助程序的先決條件

          安裝客戶端路由 npm 包。 以 Angular 為例:

          控制臺

          npm i -S @angular/router

          路由幫助程序配置

          在 Configure 方法中使用名為 MapSpaFallbackRoute 的擴(kuò)展方法:

          C#

          app.UseMvc(routes =>
          {
              routes.MapRoute(
                  name: "default",
                  template: "{controller=Home}/{action=Index}/{id?}");
          
              routes.MapSpaFallbackRoute(
                  name: "spa-fallback",
                  defaults: new { controller = "Home", action = "Index" });
          });

          系統(tǒng)按路由配置順序評估路由。 因此,上面的代碼示例中的 default 路由先用于模式匹配。

          創(chuàng)建新項(xiàng)目

          JavaScript Services 提供預(yù)配置的應(yīng)用程序模板。 在這些模板中,SpaServices 與各種框架和庫(例如 Angular、React 和 Redux)結(jié)合使用。

          可以通過使用 .NET Core CLI 運(yùn)行以下命令來安裝這些模板:

          .NET CLI

          dotnet new --install Microsoft.AspNetCore.SpaTemplates::*

          系統(tǒng)會(huì)顯示可用 SPA 模板的列表:



          若要使用其中一個(gè) SPA 模板創(chuàng)建新項(xiàng)目,請?jiān)?dotnet new 命令中包含該模板的 短名稱。 以下命令將使用為服務(wù)器端配置的 ASP.NET Core MVC 創(chuàng)建 Angular 應(yīng)用程序:

          .NET CLI

          dotnet new angular

          設(shè)置運(yùn)行時(shí)配置模式

          存在兩種主要運(yùn)行時(shí)配置模式:

          • 開發(fā):包含源映射以簡化調(diào)試。不優(yōu)化客戶端代碼的性能。
          • 生產(chǎn):不包含源映射。通過捆綁和縮小來優(yōu)化客戶端代碼。

          ASP.NET Core 使用名為 ASPNETCORE_ENVIRONMENT 的環(huán)境變量來存儲配置模式。 有關(guān)詳細(xì)信息,請參閱設(shè)置環(huán)境。

          使用 .NET Core CLI 運(yùn)行

          通過在項(xiàng)目根目錄下運(yùn)行以下命令來還原所需的 NuGet 和 npm 包:

          .NET CLI

          dotnet restore && npm i

          生成并運(yùn)行應(yīng)用程序:

          .NET CLI

          dotnet run

          應(yīng)用程序根據(jù)運(yùn)行時(shí)配置模式在 localhost 上啟動(dòng)。 在瀏覽器中導(dǎo)航到 http://localhost:5000 會(huì)顯示登陸頁面。

          使用 Visual Studio 2017 運(yùn)行

          打開由 dotnet new 命令生成的 .csproj 文件。 所需的 NuGet 和 npm 包在項(xiàng)目打開時(shí)會(huì)自動(dòng)還原。 此還原過程可能需要幾分鐘的時(shí)間,應(yīng)用程序在此過程完成后即可運(yùn)行。 單擊綠色的運(yùn)行按鈕或按 Ctrl + F5,瀏覽器將打開到應(yīng)用程序的登陸頁面。 應(yīng)用程序根據(jù)運(yùn)行時(shí)配置模式在 localhost 上運(yùn)行。

          測試應(yīng)用

          SpaServices 模板已預(yù)先配置為使用 Karma 和 Jasmine 運(yùn)行客戶端測試。 Jasmine 是適用于 JavaScript 的常用單元測試框架,而 Karma 是這些測試的測試運(yùn)行程序。 Karma 配置為使用 Webpack 開發(fā)中間件,使開發(fā)人員無需在每次進(jìn)行更改時(shí)都停止并運(yùn)行測試。 無論是針對測試用例運(yùn)行的代碼還是測試用例本身,測試都會(huì)自動(dòng)運(yùn)行。

          以 Angular 應(yīng)用程序?yàn)槔?,系統(tǒng)已經(jīng)為 counter.component.spec.ts 文件中的 CounterComponent 提供了兩個(gè) Jasmine 測試用例:

          TypeScript

          it('should display a title', async(() => {
              const titleText = fixture.nativeElement.querySelector('h1').textContent;
              expect(titleText).toEqual('Counter');
          }));
          
          it('should start with count 0, then increments by 1 when clicked', async(() => {
              const countElement = fixture.nativeElement.querySelector('strong');
              expect(countElement.textContent).toEqual('0');
          
              const incrementButton = fixture.nativeElement.querySelector('button');
              incrementButton.click();
              fixture.detectChanges();
              expect(countElement.textContent).toEqual('1');
          }));
          

          ClientApp 目錄中打開命令提示符。 運(yùn)行下面的命令:

          控制臺

          npm test

          該腳本將啟動(dòng) Karma 測試運(yùn)行程序,而后者將讀取 karma.conf.js 文件中定義的設(shè)置。 除其他設(shè)置外,karma.conf.js 還通過其 files 數(shù)組標(biāo)識要執(zhí)行的測試文件:

          JavaScript復(fù)制

          module.exports = function (config) {
              config.set({
                  files: [
                      '../../wwwroot/dist/vendor.js',
                      './boot-tests.ts'
                  ],
          

          發(fā)布應(yīng)用

          有關(guān)發(fā)布到 Azure 的詳細(xì)信息,請參閱此 GitHub 問題。

          將生成的客戶端資產(chǎn)和已發(fā)布的 ASP.NET Core 項(xiàng)目組合成一個(gè)可即時(shí)部署的包的過程可能會(huì)很繁瑣。 值得慶幸的是,SpaServices 可使用名為 RunWebpack 的自定義 MSBuild 目標(biāo)來協(xié)調(diào)整個(gè)發(fā)布過程:

          XML

          <Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
            <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
            <Exec Command="npm install" />
            <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
            <Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
          
            <!-- Include the newly-built files in the publish output -->
            <ItemGroup>
              <DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
              <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
                <RelativePath>%(DistFiles.Identity)</RelativePath>
                <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
              </ResolvedFileToPublish>
            </ItemGroup>
          </Target>
          

          該 MSBuild 目標(biāo)具有以下職責(zé):

          1. 還原 npm 包。
          2. 創(chuàng)建第三方客戶端資產(chǎn)的生產(chǎn)級生成。
          3. 創(chuàng)建自定義客戶端資產(chǎn)的生產(chǎn)級生成。
          4. 將 Webpack 生成的資產(chǎn)復(fù)制到發(fā)布文件夾。

          運(yùn)行以下命令時(shí)將調(diào)用該 MSBuild 目標(biāo):

          .NET CLI

          dotnet publish -c Release

          主站蜘蛛池模板: 亚洲国产综合精品中文第一区| tom影院亚洲国产一区二区| 亚洲视频在线一区| 精品一区二区三区在线观看l| 精品伦精品一区二区三区视频| 亚洲AV无码一区二区三区性色| 成人无码AV一区二区| 国产精品 视频一区 二区三区| 无码人妻精品一区二区三区久久久 | 日韩AV在线不卡一区二区三区| 亚洲综合一区二区| 91香蕉福利一区二区三区| 亚洲国产成人一区二区精品区| 亚洲爽爽一区二区三区| 老湿机一区午夜精品免费福利| 无码少妇A片一区二区三区| 亚洲av无码一区二区三区在线播放| 国产高清在线精品一区| 亚洲美女一区二区三区| 精品无码人妻一区二区三区 | 波多野结衣中文字幕一区二区三区 | 青娱乐国产官网极品一区| 日本精品啪啪一区二区三区| 欧洲精品码一区二区三区| 欧洲精品码一区二区三区| 精品视频在线观看一区二区| 色多多免费视频观看区一区| 男人免费视频一区二区在线观看| 国产在线精品观看一区| 日韩一区二区三区在线精品 | 亚洲Av无码国产一区二区| 亚洲a∨无码一区二区| 国产乱人伦精品一区二区在线观看| 精品欧美一区二区在线观看| 日韩伦理一区二区| 日韩综合无码一区二区| 无码精品一区二区三区在线| 亚洲av无码一区二区三区观看| 久久人妻无码一区二区| 亚洲A∨精品一区二区三区| 国产一区二区三区不卡AV|