2017 年 4 月

第 32 卷,第 4 期

本文章是由機器翻譯。

UWP app - 為 UWP 開發裝載的 Web 應用程式

Sagar Bhanudas Bhanudas

許多開發人員和公司建立 Web 介面,其產品和服務有助於發現及存取。另一個平台是應用程式平台。原生應用程式提供更豐富的 UX 和與 Web 應用程式的功能。現在,專案 Westminster 提供開發人員能夠將現今網站轉換成原生應用程式。

專案 Westminster 橋接器可讓 Web 開發人員運用現有的程式碼,使其回應靈敏的 Web 應用程式至通用 Windows 平台 (UWP) (請參閱 「 專案 Westminster 在 Nutshell 」 的 Windows 開發人員部落格文章, bit.ly/2jyhVQo)。這個 「 橋接器 」 的概念是為了重複使用現有的網站程式碼,並新增 UWP 專屬的程式碼,以形成的 Web 應用程式與基礎的 Windows 平台的整合點圖層。部落格文章將討論一些程式碼撰寫作法,以確保一致的經驗

Web 開發人員如何整合搭配運作,比方說,Cortana 現代的 Web 應用程式? 答案是透過公分 JavaScript。Windows 透過 JavaScript 和 Windows 命名空間公開的應用程式架構的功能 (Windows 執行階段 Api)。產生的應用程式稱為裝載的 Web 應用程式。

在本文中我將會分享我了解在使用獨立軟體廠商 (Isv) 和合作夥伴在他們自己專案 Westminster 工具透過 UWP 應用程式連接埠。此處的重點是共用如何最符合傳遞最佳 UX 平台應用程式執行過程中的 Web 應用程式的詳細資料。

入門

若要開始使用專案 Westminster 工具,您首先必須 UWP 轉換成您的網站 (請參閱 Channel 9 影片中,「 建立裝載 Web 應用程式與專案 Westminster,」,網址bit.ly/2jp4srs)。

做為初步測試 — 確定網站呈現如預期般和簡報看起來一致 — 您應該在 Microsoft Edge 瀏覽器上進行測試。這可協助識別並修正任何轉譯問題之前整合的 Windows 功能開始撰寫程式碼。

大部分的網站有幾個常見的功能,可讓使用者完成特定工作 (例如填寫表單) 或拿掉 (例如下載手動) 及其參考的資訊。在這種情況下,很重要,這些功能將保持不變,而且新產生裝載的 Web 應用程式的體驗一致的原始網站。其中幾種情況可能需要進行測試、 檢閱和重新分解透過程式碼,以配合使用者的期望。下一節中會視為幾個重要案例示範不同的類別或物件供開發人員使用專案 Westminster 工具與相關的程式碼工作流程。在應用程式移植到 UWP,最好考慮整合平台的幾個原生功能更豐富的 UX 和增強的應用程式經驗。以下是一些常實作的功能,由應用程式開發人員︰

  • 連絡人
  • Cortana 整合
  • 動態磚
  • 快顯通知
  • 攝影機與麥克風
  • 相片庫
  • 豐富的圖形和媒體 Windows 執行階段堆疊
  • 與其他應用程式共用內容

在本文中,重點在於整合 UWP 功能,例如 Cortana 和即時顯示,例如,啟動應用程式的語音為基礎的命令和傳遞資訊給使用者的協助。因此,以增強整體 UX UWP 應用程式基礎結構的支援。開發人員 Web 網頁的最新的 Windows 10 功能 (bit.ly/2iKufs6) 提供更多整合機會的快速概觀。[Web] 頁面,要轉換成應用程式,提供下列資訊︰

  1. Web 應用程式功能的功能
    1. 下載檔案
    2. 工作階段管理 」 或 「 單一登入
    3. 可以移至上一頁透過 [上一頁] 按鈕,以 stately 方式
  2. UWP 整合功能
    1. 動態磚
    2. Cortana

這些功能需要考量移轉和重構時提供可預測的應用程式體驗。

檔案下載狀況

現今大多數 Web 應用程式可讓各種不同的內容檔案下載。中所示**[圖 1**,預設的瀏覽器中的檔案下載的經驗是例如按一下按鈕或超連結,瀏覽器開始下載,也會將檔案儲存到 (大多數) rootdir:\users\username\Downloads。

預設的下載體驗

[圖 1 預設下載體驗

部分是因為瀏覽器會以完全信任權限執行的原生 Win32 應用程式,而將檔案直接寫入 [下載] 資料夾。

現在,假設相同的網站正在執行的應用程式 (具體 WWAHost.exe) 的內容中,按一下 [下載] 按鈕時。接下來呢 最有可能會發生任何事,而且看起來只是不使用的程式碼。它可能會顯示按鈕沒有回應或或許已開始下載檔案,但它所儲存嗎?

WWAHost.exe 是網站上應用程式容器的功能子集,相較於瀏覽器。這個子集的功能包括標準的指令碼執行與轉譯功能 (大部分是透過 HTML 簡報)。

什麼您正在使用現在是應用程式。在應用程式的世界中,開發人員,下載檔案應該明確的自動程式碼,否則遠端檔案就是只要 URL 和它的清楚/容器應用程式會執行具有遠端檔案的 URL (我可以討論什麼可能透過幕後發生特製化,偵錯工具,不過,我不會詳述,現在)。

若要處理這種情況下的,您要叫用適當的 UWP Api。這些 Api 可以同時存在以程式碼,讓這些功能使用瀏覽器的網站執行時的運作。[圖 2顯示相關的程式碼。

[圖 2 檔案下載 JavaScript 程式碼 (較大的檔案)

(function() {
  if (typeof Windows !== 'undefined' &&
    typeof Windows.UI !== 'undefined' &&
    typeof Windows.ApplicationModel !== 'undefined') {
  function WinAppSaveFileBGDownloader() {
    // This condition is only true when running inside an app.
    // The else condition is effective when running inside browsers.
    // This function uses the Background Downloader class to download a file.
    // This is useful when downloading a file more than 50MB. 
    // Downloads continue even after the app is suspended/closed.
    if (typeof Windows !== 'undefined' &&
      typeof Windows.UI !== 'undefined' &&
      typeof Windows.ApplicationModel !== 'undefined') {
        var fileSavePicker = new Windows.Storage.Pickers.FileSavePicker();
        // Example: You can replace the words EXTENSION and ext with the word PNG. 
        fileSavePicker.fileTypeChoices.insert(
          "EXTENSION file", [".ext"]);
            // Insert appropriate file format through code.
        fileSavePicker.defaultFileExtension = ".ext";
          // Extension of the file being saved.
        fileSavePicker.suggestedFileName = "file.ext";
          // Name of the file to be downloaded.
        fileSavePicker.settingsIdentifier = "fileSavePicker1";
        var fileUri =
          new Windows.Foundation.Uri("<URL of the file being downloaded>");
        fileSavePicker.pickSaveFileAsync().then(function (fileToSave) {
          var downloader =
            new Windows.Networking.BackgroundTransfer.BackgroundDownloader();
          var download = downloader.createDownload(
            fileUri,
            fileToSave);
          download.startAsync().then(function (download) {
            // Any post processing.
            console.log("Done");
          });
        });
      }
      else {
        // Use the normal download functionality already implemented for browsers,
        // something like <a href="<URL>" />.
      }
    }
}
})();

您可以撰寫程式碼**[圖 2**上按下按鈕或檔案下載功能必須的任一處。程式碼片段

使用 BackgroundDownloader 類別 (bit.ly/2jQeuBw),其中有很多優點,例如背景 post 應用程式暫止,並且能夠下載大型檔案中保存的下載項目。

中的程式碼**[圖 2**實際上會建立相同的經驗,因為瀏覽器,例如,提示使用者選取的檔案位置 (fileSavePicker 變數) 起始下載 (程式下載程式變數) 以及寫入選取的位置。

或者,您可以使用中的程式碼**[圖 3**較小的檔案下載,只要在應用程式執行將會執行︰

[圖 3 檔案下載 JavaScript 程式碼 (較小的檔案)

// Use the Windows.Web.Http.HttpClient to download smaller files and
// files are needed to download when the app is running in foreground.
(function() {
  if (typeof Windows !== 'undefined' &&
    typeof Windows.UI !== 'undefined' &&
    typeof Windows.ApplicationModel !== 'undefined') {
  function WinAppSaveFileWinHTTP() {
    var uri = new Windows.Foundation.Uri("<URL of the file being downloaded>");
    var fileSavePicker = new Windows.Storage.Pickers.FileSavePicker();
    fileSavePicker.fileTypeChoices.insert("EXT file",
      [".ext"]); //insert appropriate file format through code.
    fileSavePicker.defaultFileExtension = ".ext";
      // Extension of the file being saved.
    fileSavePicker.suggestedFileName = "file.ext";
      // Name of the file to be downloaded. Needs to be replaced programmatically.
    fileSavePicker.settingsIdentifier = "fileSavePicker1";
    fileSavePicker.pickSaveFileAsync().then(function (fileToSave) {
      console.log(fileToSave);
      var httpClient = new Windows.Web.Http.HttpClient();
      httpClient.getAsync(uri).then(function (remoteFile) {
        remoteFile.content.readAsInputStreamAsync().then(
           function (stream) {
          fileToSave.openAsync(
            Windows.Storage.FileAccessMode.readWrite).then(
            function (outputStream) {
            Windows.Storage.Streams.RandomAccessStream.copyAndCloseAsync(
              stream, outputStream).then(function (progress, progress2) {
          // Monitor file download progress.
            console.log(progress);
            var temp = progress2;
          });
        });
        });
      });
    });
  }
}
})();

中的程式碼**[圖 3用來處理檔案下載作業的 Windows.Web.Http.HttpClient (bit.ly/2k5iX2E)。如果您在比較中的程式碼片段[圖 2[圖 3**,您會注意到,後者需要進行一些進階撰寫程式碼,可讓您更充分掌控檔案下載。如此一來,您就也可以瀏覽儲存檔案,使用 UWP 應用程式資料流的方法。

此外,在應用程式也處理下載的檔案的情況下,最好是使用 FutureAccessList 屬性來快取及存取已儲存檔案的位置 (bit.ly/2k5fXn6)。這點很重要,因為使用者可以選擇要儲存已下載的檔案隨處系統 (例如 D:\files\ 或是在 C:\users\myuser\myfiles\) 應用程式的容器,在沙箱處理序可能無法直接存取檔案系統,而不需要使用者啟始的動作。這個類別可用來避免額外的步驟,讓使用者不必再次開啟相同的檔案。若要使用這個類別,它需要至少一次使用 FileOpenPicker 開啟檔案。

這應該可以協助簡化在裝載 Web 應用程式檔案下載功能,並提供直覺式的 ux。(提示︰ 請務必將檔案 URL 網域新增至在 ApplicationContentURI bit.ly/2jsN1WS來避免執行階段例外狀況。)

工作階段管理

很常見的最新應用程式目前使用者的多個工作階段間保存資料。例如,想像不必每次啟動應用程式時,登入常用的應用程式。如果在一天多次啟動應用程式,它必須耗費不少時間。沒有可用的 Web 應用程式,會轉換成 UWP 應用程式的 Windows 執行階段架構中的類別。

請考慮某些參數要保留相同的使用者工作階段之間的案例。例如,請考慮使用者和其他的其他資訊,例如最後一個工作階段和其他快取項目所識別的字串。

在此案例中使用的正確類別是 Windows.Storage.ApplicationData 類別。這個類別有許多屬性和方法,不過,請考慮針對此案例的 localSettings 屬性。

設定會儲存在系統中索引鍵 / 值組。看看下列的範例程式碼的實作︰

function getSessionData()
{
var applicationData = Windows.Storage.ApplicationData.current;
var localSettings = applicationData.localSettings;
// Create a simple setting.
localSettings.values["userID"] = "user998-i889-27";
// Read data from a simple setting.
var value = localSettings.values["userID"];
}

在理想情況下,將資料寫入設定的程式碼可以撰寫在檢查點在應用程式就必須擷取特定資訊,然後傳送至遠端 Web API 或服務。要讀取此設定通常可以寫入 App_activated 事件處理常式,就啟動應用程式和資訊保存從先前的應用程式工作階段中的程式碼可以存取。請注意,每個設定的名稱有 255 個字元限制,而每個設定的上限為 8 kb 的大小。

語音 (Cortana) 的整合案例

其中一個可以實作您的網站 (現在的應用程式) 的案例 — 張貼移植至 UWP — 就是將整合 Cortana,語音基礎互動式數位助理 Windows 裝置上。

請考慮的旅遊入口網站的網站,現在可以在其中整合 Cortana 的使用案例的其中一個移植到 Windows 10 應用程式,會顯示人員的路線的詳細資料。然後問題命令像是 「 顯示路線到倫敦 」,使用者可以 (或任何目的地),而且需要設定的應用程式開發中所示**[圖 4**。

[圖 4 Cortana 命令 XML

<?xml version="1.0" encoding="utf-8"?>
<VoiceCommands xmlns="https://schemas.microsoft.com/voicecommands/1.1">
  <CommandSet xml:lang="en-us" Name="AdventureWorksCommandSet_en-us">
    <CommandPrefix> Adventure Works, </CommandPrefix>
    <Example> Show trip to London </Example>
    <Command Name="showTripToDestination">
      <Example> Show trip to London </Example>
      <ListenFor RequireAppName=
        "BeforeOrAfterPhrase"> show trip to {destination} </ListenFor>
      <Feedback> Showing trip to {destination} </Feedback>
      <Navigate/>
    </Command>
    <PhraseList Label="destination">
      <Item> London </Item>
      <Item> Dallas </Item>
    </PhraseList>
  </CommandSet>
<!-- Other CommandSets for other languages -->
</VoiceCommands>

Cortana 可協助完成工作的第一方應用程式 (例如行事曆) 大量和自訂應用程式的介面公開 Api。基本上,這裡基本上是 Cortana 與應用程式的運作方式︰

  1. 在 voicecommands.xml 檔中註冊接聽命令。
  2. 啟動相關的應用程式,比對 Windows 搜尋列中的語音/輸入的命令。
  3. 傳遞至語音 / 文字的命令應用程式做為變數,讓應用程式程序輸入的使用者。

注意: 表示檔案名稱。它可以命名為依需要具有 XML 副檔名。Windows 開發人員文件 」 使用 Cortana 與客戶 (10 乘以 10) 的互動 」 在bit.ly/2iGymdE提供設定 XML 檔案和它的結構描述中的命令很好的概觀。

中的 XML 程式碼**[圖 4**會說明 Cortana 識別 Adventure Works 應用程式的需求以啟動時發出命令,如下所示︰

'Adventure Works, Show trip to London'
'Adventure Works, Show trip to Dallas'

現在,讓我們看看如何 Web 程式碼會處理啟動和巡覽至相關的頁面內輸入為基礎的應用程式 (或網站)。您建立個別的 JavaScript 檔案,這種情況下撰寫程式碼。

中的程式碼**[圖 5**轉介中<body>呼叫端/參考頁面的頁面之前 DOMContentLoaded 事件觸發程序的區段。</body>因此,最好是新增<script src=""></script>

[圖 5 語音命令處理常式程式碼

<!-- In HTML page :
<meta name="msapplication-cortanavcd" content="http://<URL>/vcd.xml" />
-->
(function () {
  if (typeof Windows !== 'undefined' &&
    typeof Windows.UI !== 'undefined' &&
    typeof Windows.ApplicationModel !== 'undefined') {
    Windows.UI.WebUI.WebUIApplication.addEventListener("activated", activatedEvent);
  }
  function activatedEvent (args) {
    var activation = Windows.ApplicationModel.Activation;
    // Check to see if the app was activated by a voice command.
    if (args.kind === activation.ActivationKind.voiceCommand) {
      // Get the speech recognition.
      var speechRecognitionResult = args.result;
      var textSpoken = speechRecognitionResult.text;
      // Determine the command type {search} defined in vcd.
      switch (textSpoken) {
         case "London":
           window.location.href =
             'https://<mywebsite.webapp.net>/Pages/Cities.html?value=London';
        break;
      case "Dallas":
        window.location.href =
          'https://<mywebsite.webapp.net>/Pages/Cities.html?value=Dallas';
        break;
                              ...                                   
        <other cases>
                              ...               
      }
    }
  }
})();

 (附註: 因為應用程式 「 啟動 」 所 Cortana 為 「 voiceCommand 」 使用 「 activationKind 」,務必註冊此啟用的事件處理常式。若要註冊的原生 WinJS 或 C# 其中一個應用程式不是應用程式週期事件,命名空間 Windows.UI.WebUI.WebUIApplication 提供協助訂閱及處理特定事件)。

程式碼中**[圖 5**,Cortana Api 會接收使用者的語音輸入,並填入的啟動類別 SpeechRecognition 屬性。這應該可以協助擷取程式碼中已轉換的文字,並協助應用程式執行相關的動作。在此片段中,您可以使用交換器 case 陳述式評估 textSpoken 變數,並將使用者路由至 Cities.html 頁面縣 (市) 附加為查詢字串的值。

很明顯地,這是其中一個案例並提供每個網站 (MVC、 REST 等等) 的路由設定,情況也會跟著變更。應用程式現在就來與 Cortana 溝通。

(遺失) 的上一頁按鈕

討論過之後的某些最進階的功能,讓我們看看其中一個基本 — 但重要 — 應用程式經驗層面︰ 瀏覽。容易瀏覽應用程式和直覺的瀏覽階層,可協助使用者 predictively 體驗的應用程式。

這種情況下是特殊的因為 UI 時您移植至 UWP 應用程式反應靈敏的網站,如預期般運作。不過,您需要對應用程式容器提供更多關於處理應用程式內瀏覽的指標。預設 UX 如下︰

  1. 使用者啟動應用程式。
  2. 使用者瀏覽應用程式的各個不同區段,按一下超連結或頁面上的功能表。
  3. 有時候,使用者會想要移至前一個頁面,然後按一下 Windows 作業系統 (尚無一步] 按鈕在應用程式) 所提供的硬體或軟體的上一頁按鈕。
  4. 應用程式結束。

點否 4 非預期的結果,必須修正。解決方法很簡單,其中包括指示經過一般 JavaScript Api 的應用程式容器視窗。[圖 6顯示此案例中的程式碼。

[圖 6 上一步] 按鈕的程式碼

(function() {
  if (typeof Windows !== 'undefined' &&
    typeof Windows.UI !== 'undefined' &&
    typeof Windows.ApplicationModel !== 'undefined') {
   Windows.UI.Core.SystemNavigationManager.getForCurrentView().
     appViewBackButtonVisibility =
     Windows.UI.Core.AppViewBackButtonVisibility.visible;
        Windows.UI.Core.SystemNavigationManager.getForCurrentView().
        addEventListener("backrequested", onBackRequested);
function onBackRequested(eventArgs) {
      window.history.back();
      eventArgs.handled = true;
    }
  }
  })();

初始幾行啟用架構提供上一頁按鈕,應用程式頂端會出現,然後註冊點選/click 事件的事件處理常式。您執行程式碼中的所有存取 「 視窗 」 DOM 物件,指示它返回上一頁。還有一件事来記住︰ 在巡覽堆疊底部應用程式時,有沒有進一步頁面可用歷程記錄中就會在此時結束應用程式。額外的程式碼需經過撰寫自訂的經驗是否需要會編譯成應用程式。

動態磚

動態磚是一種功能 UWP 應用程式顯示清晰的更新資訊的應用程式或任何項目中,而不需要啟動應用程式可能會感興趣使用者。按 Windows 裝置上的 [開始] 功能表,您可以檢視簡單的範例。很明顯的應用程式,例如新聞、 金錢和運動少數即時顯示。

以下是幾個使用案例的範例︰

  • 電子商務應用程式,磚可以顯示建議或您的訂單狀態。
  • 在特定業務應用程式磚可以顯示貴組織的報表的迷你映像。
  • 在遊戲應用程式,動態磚可以顯示優惠、 成積、 新的挑戰,依此類推。

[圖 7磚 Microsoft 第一方應用程式的兩個範例。

動態磚範例
[圖 7 的即時範例

其中一種整合裝載的 Web 應用程式的即時顯示最簡單的方式是建立 Web API,並設定應用程式程式碼 (示**[圖 8**) 輪詢它每隔幾分鐘。Web API 的工作是要傳送回 XML,將會用來建立 Windows 應用程式的並排顯示內容。(附註: 請務必使用者釘選應用程式,以體驗動態磚的 [開始] 功能表。)

[圖 8 即時顯示簡單的程式碼

function enableLiveTile()
  {
    if (typeof Windows !== 'undefined' &&
      typeof Windows.UI !== 'undefined' &&
      typeof Windows.ApplicationModel !== 'undefined') {
      {
        var notification = Windows.UI.Notifications;
        var tileUpdater =
          notification.TileUpdateManager.createTileUpdaterForApplication();
        var recurrence = notification.PeriodicUpdateRecurrence.halfHour;
        var url = new Windows.Foundation.Uri("<URL to receieve the XML for tile>");
        tileUpdater.startPeriodicUpdate(url, recurrence);
      }
      }       
  }

最重要的類別是 TileUpdateManager Windows.UI.Notifications 命名空間中。這個類別不只會建立在內部將傳送至磚範本,但也透過 startPeriodicUpdate 方法輪詢並排顯示 XML 內容中指定的 URL。可以使用 PeriodicUpdateRecurrence 列舉期間提取的 XML 內容方塊設定輪詢的持續時間。這種方法是多伺服器驅動型 Web API 用來將用戶端的 XML 程式碼和磚範本。這是可行的當開發人員可以控制應用程式和服務層。

現在請考慮應用程式中從第三方 Web Api,例如氣候或市場調查資料接收資訊的案例。在這種情況下,大部分 Web Api 傳送標準 HTTP 回應主體中,標頭方面,依此類推。這裡您可以剖析 Web API 回應,而然後表單 XML 並排顯示的 JavaScript 中的用戶端 UWP 應用程式程式碼中的內容。這會讓應用程式開發人員更充分掌控的範本,以顯示資料類型。您也可以提及 TileNotification 類別透過磚的到期時間。這個程式碼所示**[圖 9**。

[圖 9: 另一種建立動態磚

function createLiveTile() /* can contain parameters */
{
  var notifications = Windows.UI.Notifications,
  tile = notifications.TileTemplateType.tileSquare310x310ImageAndText01,
  tileContent = notifications.TileUpdateManager.getTemplateContent(tile),
  tileText = tileContent.getElementsByTagName('text'),
  tileImage = tileContent.getElementsByTagName('image');
  // Get the text for live tile here [possibly] from a remote service through xhr.
  tileText[0].appendChild(tileContent.createTextNode('Demo Message')); // Text here.
  tileImage[0].setAttribute('src','<URL of image>');
  var tileNotification = new notifications.TileNotification(tileContent);
  var currentTime = new Date();
  tileNotification.expirationTime = new Date(currentTime.getTime() + 600 * 1000);
    notifications.TileUpdateManager.createTileUpdaterForApplication().
    update(tileNotification);
}

請注意 TileTemplateType 類別提供建立方形磚範本 310 x 310px 大小的影像和文字中的功能。此外,磚的到期時間設為 10 分鐘,透過程式碼,這表示,過後,即時磚會回復成預設應用程式磚,提供在應用程式封裝中,除非新的通知會送達的應用程式中的推播通知形式。可用的並排顯示範本的相關資訊,請參閱bit.ly/2k5PDJj

總結

有幾個規劃從 Web 應用程式移轉到 UWP 應用程式時的考慮事項︰

  1. 測試您的應用程式的配置和轉譯的現代瀏覽器 (Microsoft Edge 是一個範例)。
  2. 如果您的 Web 應用程式相依的 ActiveX 控制項或外掛程式,請確定作用中功能的替代方式執行的現代瀏覽器,或為 UWP 應用程式。
  3. 使用位於 SiteScan 工具bit.ly/1PJBcpi介面與程式庫和功能相關的建議。
  4. 識別您的網站所參考的外部資源的 Url。這些都必須加入到 ApplicationContentUriRules Appx.manifest 檔案區段。

此外,還有許多更深入的整合功能,可透過 JavaScript Windows 物件和亮透過更豐富的體驗的應用程式功能的說明。連絡人、 相機、 麥克風、 快顯通知,以及其他許多功能開啟視窗的機會混用您的應用程式角色的網站。這篇文章中的程式碼已轉換成專案範本並會提供給開發人員透過 GitHub 的bit.ly/2k5FlJh


Sagar Bhanudas Joshi曾與開發人員和 Isv 通用 Windows 平台和 Microsoft Azure 上的六年以上。  他的職責包括初學者,幫助他們架構、 設計和提供內建的解決方案和 Azure、 Windows 和 Office 365 平台的應用程式使用。Joshi 存在及運作孟買,印度的。找到他的 Twitter: @sagarjms

感謝下列 Microsoft 技術專家檢閱這份文件︰ Sandeep Alur 和 Ashish Sahu