言
目前,教學、教研各種內容線上沉淀、展示豐富多彩,但線上內容“線下化”能力不足或過分依賴人力,比如,線上練習題組卷后以PDF形式分發給學生,家長希望將考試、練習題目打印后,學生帶到學校去做(高中生使用手機等電子設備的時間有限),線上各類分析報告以PDF形式分享給學生/家長等。
從業務方面看,不同業務線的多個業務場景都有輸出PDF的訴求,如果各業務線自己設計、實現符合自身業務場景的具體方案,除調研、開發工作量較大之外,還會有重復調研,踩坑的情況。
從技術角度看,線上內容轉PDF的內容源頭來自于H5富文本內容,業界內以此為基礎的PDF生成方案多種多樣,也各有優劣,比如:
方案對比-表格-1
因此,我們綜合了各種PDF生成方案并總結了在探索講義生成PDF過程中的經驗,抽象出了一套通用的,可復用的能力供各業務線快速利用,基本方案和優劣如下:
最終方案-表格-2
目 標
旨在提供一套以H5為載體的PDF通用生成方案,這套方案有如下特點:
這套方案可分為兩個核心部分,頁面展示側 - Medusa,PDF生成側 - Hydra
頁面展示側 - Medusa
我們頁面展示側的通用能力——Medusa,是基于Paged.js的二次封裝,并以NPM包形式提供給業務方使用。Medusa可對任何HTML進行分頁、并根據配置添加頁眉、頁腳等,最終將處理后的HTML渲染到頁面中。Medusa封裝并簡化了對PDF格式的配置,可覆蓋絕大多數業務場景,使得各業務場景將更多精力投入其自身業務邏輯的開發。
之所以選擇Pagedjs為基礎開發我們自己的SDK,是因為它是目前我們能找到的唯一開源的、具有HTML內容分頁,樣式處理的前端庫,同時我們也在講義中經過了長期的摸索與沉淀。
接下來將詳細介紹Paged.js原理、Medusa支持的功能與使用方法。
一 Paged.js是如何工作的
Paged.js包含了 3 個大模塊
這里將主要介紹 Previewer 和 Chunker,因為我們的二次開發和維護不涉及到Polisher。
Previewer
Previewer 的工作非常簡單,但我們會主要利用它封裝我們的Medusa,初始化一個Previewer對象,Previewer初始化了Chunker和Polisher對象:
Medusa-代碼-1
再調用Previewer的preview()方法,preview()方法做了兩件事:
Medusa-代碼-2
當chunker.flow結束,即可在瀏覽器看到整個頁面處理完之后的樣子。
Chunker
首先,Chunker解析、預處理需要分頁的HTML,為其添加一些必要的屬性
Medusa-代碼-3
然后創建容納所有頁(pages)的容器,并掛載到renderTo容器下(默認Body),以備組織后續的所有頁:
Medusa-代碼-4
接著,chunker創建了一個page模版,以便增加頁面使用:
Medusa-代碼-5
其中,TEMPLATE是Pagedjs內部創建頁面時所使用的基礎模版。
Medusa-代碼-6
接下來,chunker進入了渲染+分頁過程(這個過程我們不會在二次開發中做修改,但需要了解其基本思路以便在出問題時能有解決思路),這個過程在循環一個迭代器(*layout),迭代器一直在做3件事:
原則:
尋找overflow時會將盡可能多的內容節點插入內容區域,這里,“盡可能多”分為幾種情況,比如:
步驟:
Pagedjs遵循了如下步驟去尋找overflow:
兩個前置條件:
i. 從需要處理的內容第一個節點開始,判斷是否 node.left >= contentArea.right || node.top >= contentArea.bottom
Medusa-代碼-7
ii.如果不滿足,則判斷 node.right <= contentArea.right && node.bottom <= contentArea.bottom
Medusa-代碼-8
iii.如果不滿足,那說明有子節點overflow了,則繼續深入其子節點查找即可。
3.使用模版添加新的頁面,并從BreakToken處繼續上述動作。
二 Medusa支持的功能及使用方法
基于Paged.js,Medusa支持了如下功能,并為業務方提供了更加簡潔、定制化的配置。
下方是調用Medusa的代碼示例:
Medusa-代碼-9
1.1 動態頁面分頁能力
Medusa核心功能,可將連續的HTML頁面轉化成一頁頁PDF樣式的HTML。
1.2 單頁模版配置 -> 生成能力
通過Grid布局,Paged.js將一個單頁模版分為多個區域,整體分為2個大的部分:
業務方通過簡單的配置,即可還原UI設計稿中的PDF樣式,例子如下圖:
1.2.1 base
頁面基礎配置是對每頁的。支持紙型或頁面寬高、內容區域margin、padding、背景及水印的設置。
在封裝Medusa時,Medusa將讀取傳入的頁面模版配置、靜態頁內容配置,并將樣式上的配置解析并轉化為Previewer可理解的樣式內容,比如頁面寬高的設置:
Medusa-代碼-10
將被轉化為:
Medusa-代碼-11
1.2.2 surround
2. 目前支持3種類型的surround item:
example:
Medusa-代碼-12
1.3 前/后置靜態頁面
業務方可通過如下方式配置靜態頁面的具體內容:
Medusa-代碼-13
其中,傳入的React JSX Element將會被這樣處理:
Medusa-代碼-14
處理完成后,將HTML String拼接到頁面模版中,再插入分頁后內容的前后。
PDF生成側 - Hydra:
頁面展示側為PDF生成做好了頁面的準備,對于PDF生成側,需要做的工作就更純粹了,業務方除了請求生成PDF,定期檢查PDF生成的進度,無需做任何額外工作。
1.整體流程:
PDF生成是CPU和內存密集型的,由于頁面內容的不確定性,也意味著頁面渲染時間與生成PDF的時間都是不確定的,因此整體PDF生成的鏈路被設計成是異步的,如下圖:
整體流程上,業務方在請求生成PDF時,會先在后端做一條記錄,后端再將任務發送給Node服務,即Hydra;
在生成PDF時, 第 1 步是做頁面上的準備,一個生成任務可能有多個URL頁面需要生成PDF,所以我們預先啟動對應URL數量的PPTR Page,頁面都啟動完成后,進入下一步;
第 2 步:渲染頁面,這個過程中,如果請求是包含多個URL的,這些頁面會同步渲染,在所有頁面渲染完成后,進入下一步。
第 2.5 步,如果是需要生成連續頁碼的一整個PDF,還會做額外的一個動作:頁碼矯正,通過頁碼矯正,可以將同步渲染的每個頁面,按照其之前頁面的頁碼數修正,以保證整體PDF的頁碼的連貫。
第 3 步,通過PPTR Page的能力將頁面轉換為PDF buffer,如有必要,再將生成的PDF buffer拼接到一起生成一整個PDF,或者將每個PDF buffer都生成一個PDF,壓縮成zip文件。
第 4 步,文件上傳OSS,最終返回OSS CDN鏈接。
2.請求生成PDF:
業務側請求將對應頁面生成PDF的時,只需傳入如下字段:
Hydra-代碼-1
3.PDF生成過程:
正如在整體流程中所述,PDF生成側,我們借助 PPTR 的能力打開頁面并生成PDF流。
在頁面調用 Medusa 分頁、組裝能力時,所有內容分頁組裝完成后會向body中插入了一個額外的DOM以標識該頁面處理完成:
Hydra-代碼-2
這是為了 Hydra 感知頁面渲染完成所做的準備,當生成服務的 PPTR 等到該DOM出現時,則表示頁面成功渲染并處理完成了:
Hydra-代碼-3
此后,在上面已經提到過,對于需要將多個頁面生成的PDF拼接成一個PDF的情況,在生成PDF之前需要做一個重要的動作,即頁碼矯正,原因如下:
并且我們不希望頁面的處理是串行的,因為串行勢必導致速度較慢,生成時間長。
這個問題的解決方案如下:
1. 對于每個頁面都啟用一個page,并同時處理
2. 每個頁面處理完成后(pdfLastDOM出現),通過Page.$eval()來統計頁數并記錄:
Hydra-代碼-4
3. 計算出頁面中分頁之后每一個頁面的起始頁碼,以及所有頁面的頁碼總和
4. 再修改頁碼容器樣式的 counterReset 值即可,其后續頁碼可自遞增。
Hydra-代碼-5
5. 之后,再通過 Medusa 在頁面window對象中Polyfill的相關配置,比如需要生成的PDF的單頁寬、高以生成PDF流。
Hydra-代碼-6
6. 最后如有必要,通過pdf-lib拼接這些 pdfBuffer 即可。
Hydra-代碼-7
7. PDF生成完成后,上傳OSS并返回URL鏈接
4.性能、穩定性保證:
在整體方案落地前,我們對服務進行了多次性能測試:
以下載題目為例,在4個容器,每個容器 3C 12G 的配置下的并行處理能力如下:
對于 20 道題目,每個PDF生成任務在 15 頁左右,平均 1 分鐘內能完成 280 個任務的處理。
對于 40 道題目,每個PDF生成任務在 30 頁左右,平均 1 分鐘內能完成 105 個任務的處理。
對于 60 到題目,每個PDF生成任務在 40 頁左右,平均 1 分鐘內能完成 54 個任務的處理。
同時,根據 Hydra 服務的整體的處理能力,后端通過任務隊列的形式幫助我們保證服務不被瞬間的突刺流量擊垮。
已接入/正在接入的相關業務線及場景:
目前,公司有 5 大業務線,8 個場景已經完全接入我們的能力用于 H5 轉 PDF,如下是錯題本、內容資料庫接入后生成的PDF樣例:
錯題本:
內容資料庫試卷:
未來展望
目前整體的PDF生成方案已經能夠滿足大多數場景和內容,但依然有可改進空間。
HTML的流式布局要求我們必須手動的對內容分頁,才能添加頁眉,頁腳等(即Mdusa做的工作),正因為如此,在處理復雜的內容時,可能會出現一些問題:比如,遇到復雜表格時,由于表格可能會有多種多樣的行、列合并,同時表格單元格內的內容也可以多種多樣,在分頁過程中,Medusa內部的PagedJS并不能完美的處理對于長、且復雜的表格的分割,因此可能遇到分割后表格單元格缺失、錯亂或寬高錯誤的問題,這些問題在講義中體現較明顯。
我們仍在持續關注與研究復雜DOM內容的分割問題,會嘗試加以優化和改進PagedJS的能力,同時,我們也以另外一種思路設計了自己的DOM分頁器方案,但經過評估,由于實現比較復雜,成本較高,暫時沒有投入開發資源。
不過,我們相信,未來我們一定能以更完美的方式分割DOM以生成更高質量的PDF。
作者:高源、陳欣博
來源:微信公眾號:高途技術
出處:https://mp.weixin.qq.com/s/c_N7jdNklrNFKR_Cub2Tgg
多朋友都一定聽說過競價單頁而且不管是什么行業醫療還是淘寶客產品基本上都會采用競價單頁來進行營銷自己的產品在這一點上我們可以看到的是大家都比較親睞于競價單頁但是對于競價單頁還不是特別的了解清晰對于競價單頁該體現哪些內容可能不太了解所以導致很多的單頁效果做的不是特別好所以小編來解答為大家介紹下什么是競價單頁?競價單頁該體現哪些內容?
競價單頁一般是由個到幾個HTML組成有的頁面只是單獨的介紹頁有的做的是比較精美的頁面,這主要是和用戶體驗有直接的關聯隨著競價單頁越來越有營銷意義很多公司都采用這樣的模式進行營銷自己的產品衡量一個競價單頁是否好主要是考核其流量與轉化率那么對于競價單頁的內容要求就高了許多
一.做競價單頁的目的性
做競價頁面的目的性小編將這一點放在第一個是有原因的因為小編看過很多的競價單頁做的不是很好,主要的原因是策劃不清楚的了解這個頁面是做了干什么這樣的目的怎么會策劃出好的競價單頁呢?所以說策劃在競價單頁的時候一定要清楚你最初的目的是什么?這樣做出的專題才不會跑題,尤其是小編建議做競價單頁的時候一定要很好的構思不要一會想做這樣一會做成那樣會顯得整個頁面沒有中心點。
二.相關媒體報道的營銷
小編現在需要說的是競價單頁相關媒體報道的營銷這一點上是需要特別注意的因為如果你做的好是非常可信的會增加用花對你的喜愛但是如果你做的很假會有相反的效果對于競價單頁的營銷主要靠媒體推廣等來營銷的引用主要媒體的視頻文章等來達到推廣營銷的目的比如央視網搜狐新浪網易騰訊等主要媒體所以說小編建議大家在做競價單頁的時候可以采用這類營銷。
三.營銷產品的效果與功效
營銷產品的效果與功效是比較重要的因為你所營銷的產品如果沒療效再好的競價單頁一樣沒有用而且你在競價單頁一定要凸顯出你營銷產品的效果與功效相信這點大家都會知道因為我們就是為了營銷產品而做的單頁所以說在做競價單頁的時候必須要將你的優勢很好的展現給你的用戶看才行如果你的產品營銷不到位那么做了有什么意義呢?所以說一定要在單頁上把自己的營銷產品的效果與功效盡善盡美的體現。
四.數量的展現(發貨量成交量咨詢量)
在這一點上小編想說的是競價單頁的數量展現主要表現在發貨量成交量咨詢量方面如果你是做產品營銷那么你一定要做好這三點將這三個點做好因為發貨量成交量咨詢量是用戶非常關注的試想一下如果你的競價單頁顯示很多的發貨量成交量咨詢量那么對于用戶來講是不是要增加很高的可信度那么你的用戶還不會在你的頁面停留嗎?所以說想要營銷必須抓住用戶的心理。
五.在線咨詢在線問答與自主訂單
小編相信做競價單頁的朋友都知道一定要有在線咨詢因為很多的單頁成片的全是在線咨詢所以說小編估計不用說大家都知道但是小編想提醒的是過多的在線咨詢會降低用戶體驗度在這一點上是無需質疑的還有在線問答與自主訂單是雙方面的用戶體驗對于用戶來講就是很好的提升但是很多朋友都沒有考慮到這一點沒有很好的尊重用戶體驗這就對用戶造成很大的偏差。
六.用戶體驗的提升才是關鍵
小編在前面說了很多相信大家對于競價單頁需要做的內容都很清晰明了了而最后一點小編想特別說明下用戶體驗因為這一點是朋友往往給忽略的就像是一些淘寶客站不尊重用戶體驗成片的廣告這就對用戶造成很大的困擾銷量往往是很少所以說用戶體驗的提升才是關鍵沒有好的用戶體驗再漂亮的競價單頁一定沒有銷量這是應該直接大家一起去研究的 。
頁應用程序 (SPA) 因其固有的豐富用戶體驗而成為一種常用的 Web 應用程序。 將客戶端 SPA 框架或庫(例如 Angular 或 React)與服務器端框架(例如 ASP.NET Core)集成在一起可能會很困難。 開發 JavaScript Services 就是為了減少集成過程中的摩擦。 使用它可以在不同的客戶端和服務器技術堆棧之間無縫操作。
警告
本文所述的功能自 ASP.NET Core 3.0 起被棄用。 Microsoft.AspNetCore.SpaServices.Extensions NuGet 包提供了一種更簡單的 SPA 框架集成機制。 有關詳細信息,請參閱 [Announcement] Obsoleting Microsoft.AspNetCore.SpaServices and Microsoft.AspNetCore.NodeServices([公告] 棄用 Microsoft.AspNetCore.SpaServices 和 Microsoft.AspNetCore.NodeServices)。
JavaScript Services 是用于 ASP.NET Core 的客戶端技術集合。 其目標是將 ASP.NET Core 定位為開發人員生成 SPA 時的首選服務器端平臺。
JavaScript Services 由兩個不同的 NuGet 包組成:
這些包在以下情況下很有用:
本文重點介紹了如何使用 SpaServices 包。
創建 SpaServices 的目的是將 ASP.NET Core 定位為開發人員生成 SPA 時的首選服務器端平臺。 使用 ASP.NET Core 開發 SPA 時不一定要使用 SpaServices,SpaServices 也不會將開發人員束縛在特定的客戶端框架中。
SpaServices 可提供有用的基礎結構,例如:
將這些基礎結構組件結合使用時,可增強開發工作流和運行時體驗。 這些組件也可單獨使用。
若要使用 SpaServices,請安裝以下各項:
通用(也稱為同構)應用程序是一種能夠在服務器和客戶端上運行的 JavaScript 應用程序。 Angular、React 和其他常用框架針對這種應用程序開發風格提供一個通用平臺。 其思路是先通過 Node.js 在服務器上呈現框架組件,然后將進一步的執行任務委托給客戶端。
SpaServices 提供的 ASP.NET Core 標記幫助程序通過調用服務器上的 JavaScript 函數來簡化服務器端預呈現的實現。
安裝 aspnet-prerendering npm 包:
控制臺
npm i -S aspnet-prerendering
可以通過在項目的 _ViewImports.cshtml 文件中注冊命名空間來發現標記幫助程序:
CSHTML
@using SpaServicesSampleApp
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"
這些標記幫助程序通過在 Razor 視圖中利用類似 HTML 的語法來抽象化與低級 API 直接通信的復雜性:
CSHTML
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
上面的代碼示例中使用的 asp-prerender-module 標記幫助程序通過 Node.js 在服務器上執行 ClientApp/dist/main-server.js。 為清楚起見,main-server.js 文件是 Webpack 生成過程中 TypeScript 到 JavaScript 轉譯任務的產物。 Webpack 定義了入口點別名 main-server;此別名的依賴項關系圖遍歷始于 ClientApp/boot-server.ts 文件:
JavaScript
entry: { 'main-server': './ClientApp/boot-server.ts' },
在以下 Angular 示例中,ClientApp/boot-server.ts 文件利用 createServerRenderer 函數和 aspnet-prerendering npm 包的 RenderResult 類型通過 Node.js 來配置服務器呈現。 用于服務器端呈現的 HTML 標記傳遞到解析函數調用,該調用包裝在強類型的 JavaScript Promise 對象中。 Promise 對象的意義在于,它以異步方式將 HTML 標記提供給頁面,以注入到 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-module 標記幫助程序結合使用時,asp-prerender-data 標記幫助程序可用于將上下文信息從 Razor 視圖傳遞到服務器端 JavaScript。 例如,以下標記將用戶數據傳遞到 main-server 模塊:
CSHTML
<app asp-prerender-module="ClientApp/dist/main-server"
asp-prerender-data='new {
UserName = "John Doe"
}'>Loading...</app>
收到的 UserName 參數使用內置的 JSON 序列化程序進行序列化,并存儲在 params.data 對象中。 在以下 Angular 示例中,該數據用于在 h1 元素內構造個性化問候語:
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();
});
});
});
});
});
在標記幫助程序中傳遞的屬性名稱用 PascalCase 表示法表示。 與之相反,JavaScript 用 camelCase 表示相同的屬性名稱。 默認的 JSON 序列化配置是造成這種差異的原因所在。
若要擴展上面的代碼示例,可以通過解凍提供給 resolve 函數的 globals 屬性,將數據從服務器傳遞到視圖:
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 數組附加到瀏覽器的全局 window 對象。 將此變量提升到全局范圍可消除重復的工作,特別是在服務器上加載了一次數據,之后又在客戶端上加載相同的數據時。
Webpack 開發中間件引入了簡化的開發工作流,Webpack 可根據該工作流按需生成資源。 在瀏覽器中重新加載頁面時,該中間件會自動編譯并提供客戶端資源。 另一種方法是在第三方依賴項或自定義代碼發生更改時,通過項目的 npm 生成腳本手動調用 Webpack。 以下示例顯示了 package.json 文件中的 npm 生成腳本:
JSON
"build": "npm run build:vendor && npm run build:custom",
安裝 aspnet-webpack npm 包:
控制臺復制
npm i -D aspnet-webpack
Webpack 開發中間件通過 Startup.cs 文件的 Configure 方法中的以下代碼注冊到 HTTP 請求管道中:
C#
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
// Call UseWebpackDevMiddleware before UseStaticFiles
app.UseStaticFiles();
通過 UseStaticFiles 擴展方法注冊靜態文件托管之前,必須先調用 UseWebpackDevMiddleware 擴展方法。 出于安全原因,僅在應用以開發模式運行時才注冊該中間件。
webpack.config.js 文件的 output.publicPath 屬性指示中間件監視 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 開發中間件的進化版。 HMR 引入了所有相同的優點,但是通過在編譯更改后自動更新頁面內容,進一步簡化了開發工作流。 不要將其與瀏覽器的刷新功能混淆,后者會干擾 SPA 的當前內存中狀態和調試會話。 Webpack 開發中間件服務與瀏覽器之間有一個實時鏈接,這意味著系統會將更改推送到瀏覽器。
安裝 webpack-hot-middleware npm 包:
控制臺
npm i -D webpack-hot-middleware
必須在 Configure 方法中將 HMR 組件注冊到 MVC 的 HTTP 請求管道:
C#
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true
});
與 Webpack 開發中間件一樣,調用 UseStaticFiles 擴展方法之前,必須先調用 UseWebpackDevMiddleware 擴展方法。 出于安全原因,僅在應用以開發模式運行時才注冊該中間件。
webpack.config.js 文件必須定義一個 plugins 數組,即便將其留空亦可:
JavaScript
module.exports = (env) => {
plugins: [new CheckerPlugin()]
在瀏覽器中加載應用后,開發人員工具的“控制臺”選項卡會提供 HMR 激活確認:
在大多數基于 ASP.NET Core 的 SPA 中,除服務器端路由外,通常還需要進行客戶端路由。 SPA 和 MVC 路由系統可以獨立工作而互不干擾。 但是,有一種極端情況帶來了挑戰:標識 404 HTTP 響應。
以使用 /some/page 的無擴展路由的情況為例。 假設請求的模式與服務器端路由不匹配,但與客戶端路由匹配。 現在以針對 /images/user-512.png 的傳入請求為例,該請求通常需要在服務器上查找映像文件。 如果請求的資源路徑與任何服務器端路由或靜態文件都不匹配,則客戶端應用程序不太可能處理它 — 通常需要返回 404 HTTP 狀態代碼。
安裝客戶端路由 npm 包。 以 Angular 為例:
控制臺
npm i -S @angular/router
在 Configure 方法中使用名為 MapSpaFallbackRoute 的擴展方法:
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" });
});
系統按路由配置順序評估路由。 因此,上面的代碼示例中的 default 路由先用于模式匹配。
JavaScript Services 提供預配置的應用程序模板。 在這些模板中,SpaServices 與各種框架和庫(例如 Angular、React 和 Redux)結合使用。
可以通過使用 .NET Core CLI 運行以下命令來安裝這些模板:
.NET CLI
dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
系統會顯示可用 SPA 模板的列表:
若要使用其中一個 SPA 模板創建新項目,請在 dotnet new 命令中包含該模板的 短名稱。 以下命令將使用為服務器端配置的 ASP.NET Core MVC 創建 Angular 應用程序:
.NET CLI
dotnet new angular
存在兩種主要運行時配置模式:
ASP.NET Core 使用名為 ASPNETCORE_ENVIRONMENT 的環境變量來存儲配置模式。 有關詳細信息,請參閱設置環境。
通過在項目根目錄下運行以下命令來還原所需的 NuGet 和 npm 包:
.NET CLI
dotnet restore && npm i
生成并運行應用程序:
.NET CLI
dotnet run
應用程序根據運行時配置模式在 localhost 上啟動。 在瀏覽器中導航到 http://localhost:5000 會顯示登陸頁面。
打開由 dotnet new 命令生成的 .csproj 文件。 所需的 NuGet 和 npm 包在項目打開時會自動還原。 此還原過程可能需要幾分鐘的時間,應用程序在此過程完成后即可運行。 單擊綠色的運行按鈕或按 Ctrl + F5,瀏覽器將打開到應用程序的登陸頁面。 應用程序根據運行時配置模式在 localhost 上運行。
SpaServices 模板已預先配置為使用 Karma 和 Jasmine 運行客戶端測試。 Jasmine 是適用于 JavaScript 的常用單元測試框架,而 Karma 是這些測試的測試運行程序。 Karma 配置為使用 Webpack 開發中間件,使開發人員無需在每次進行更改時都停止并運行測試。 無論是針對測試用例運行的代碼還是測試用例本身,測試都會自動運行。
以 Angular 應用程序為例,系統已經為 counter.component.spec.ts 文件中的 CounterComponent 提供了兩個 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 目錄中打開命令提示符。 運行下面的命令:
控制臺
npm test
該腳本將啟動 Karma 測試運行程序,而后者將讀取 karma.conf.js 文件中定義的設置。 除其他設置外,karma.conf.js 還通過其 files 數組標識要執行的測試文件:
JavaScript復制
module.exports = function (config) {
config.set({
files: [
'../../wwwroot/dist/vendor.js',
'./boot-tests.ts'
],
有關發布到 Azure 的詳細信息,請參閱此 GitHub 問題。
將生成的客戶端資產和已發布的 ASP.NET Core 項目組合成一個可即時部署的包的過程可能會很繁瑣。 值得慶幸的是,SpaServices 可使用名為 RunWebpack 的自定義 MSBuild 目標來協調整個發布過程:
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 目標具有以下職責:
運行以下命令時將調用該 MSBuild 目標:
.NET CLI
dotnet publish -c Release
*請認真填寫需求信息,我們會在24小時內與您取得聯系。