【Blazor】在ASP.NET Core中使用Blazor組件 – 創建一個音樂播放器

前言

Blazor正式版的發布已經有一段時間了,.NET社區的各路高手也創建了一個又一個的Blazor組件庫,其中就包括了我和其他小夥伴一起參與的AntDesign組件庫,於上周終於發布了第一個版本0.1.0,共計完成了59個常用組件,那麼今天就來聊一聊如何在ASP.NET Core MVC項目中使用這些Blazor組件吧

 

環境搭建

.NET Core SDK 3.0.301

Vistual Studio 2019.16.6.3

 

調用Blazor組件

創建ASP.NET Core MVC項目,如果想要在已有的項目上使用AntDesign,需要確保Target Framework是netcoreapp3.1,然後在Nuget中搜索並安裝AntDesign 0.1.0版本。

修改Startup.cs

在ConfigureServices方法中添加

1 // add for balzor
2 services.AddServerSideBlazor();
3 // add for AntDesign
4 services.AddAntDesign();

在Configure方法中添加

1 app.UseEndpoints(endpoints =>
2 {
3     endpoints.MapControllerRoute(
4     name: "default",
5     pattern: "{controller=Home}/{action=Index}/{id?}");
6     // add for blazor
7     endpoints.MapBlazorHub();
8 });

修改./Views/Shared/_Layout.cshtml

在head區域添加

1 <!--add for AntDesign-->
2 <link href="/_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet">
3 <base href="/" />

在script區域添加

1 <!--add for blazor-->
2 <script src="~/_framework/blazor.server.js"></script>

 

這裡我們需要利用一個中間層,否則直接在View里添加組件會有很多限制,不太方便

創建一個razor文件./Components/HelloWorld.razor

 1 @using AntDesign
 2  
 3 <Button type="primary" OnClick="(e)=>OnClick(e)">@_content</Button>
 4  
 5 @code{
 6     private string _content = "Primay";
 7     private void OnClick(Microsoft.AspNetCore.Components.Web.MouseEventArgs args)
 8     {
 9         _content += "*";
10     }
11 }

最後在View中添加剛剛新建的中間組件

修改./Views/Home/Index.cshtml,添加程式碼

1 <component type="typeof(HelloWorld)" render-mode="ServerPrerendered" />

 

Build & Run

這時候主頁應該會出現一個ant-design風格的button,點擊後button的內容會變為Priamary*,每點擊一次就多一個*,效果如下

 

小結

一般來說,在MVC項目中,先將介面需要使用的組件組合在一起,然後整體包裝在一個中間組件(HelloWolrd.razor)中,最後在調用的View中展示中間組件。可以理解為組件庫為我們提供了各種各樣的零件,中間層將這些零件(以及原生HTML標籤)組合成一個產品,最後在View中展示產品。

 

創建一個播放器組件

首先我們創建好需要用到的JavaScript腳本

Nuget安裝Microsoft.TypeScript.MSBuild

創建文件main.ts

 1 interface Window {
 2     SoBrian: any;
 3 }
 4  
 5 function Play(element, flag) {
 6     var dom = document.querySelector(element);
 7     if (flag) {
 8         dom.play();
 9     }
10     else {
11         dom.pause();
12     }
13 }
14  
15 function GetMusicTime(element) {
16     var dom = document.querySelector(element);
17     let obj = {
18         currentTime: dom.currentTime,
19         duration: dom.duration
20     }
21     let json = JSON.stringify(obj);
22  
23     return json
24 }
25  
26 function SetMusicTime(element, time) {
27     var dom = document.querySelector(element);
28     dom.currentTime = time;
29 }
30  
31 window.Music = {
32     print: Print,
33     play: Play,
34     getMusicTime: GetMusicTime,
35     setMusicTime: SetMusicTime
36 }

創建文件tsconfig.json

{
  "compileOnSave": true,
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": false,
    "target": "es2015",
    "outDir": "wwwroot/js"
  },
  "files": [ "main.ts" ],
  "exclude": [
    "node_modules",
    "wwwroot"
  ]
}

創建文件夾./wwwroot/music/

放入幾首你喜歡的音樂,但要注意支援的文件格式

<audio> can be used to play sound files in the following formats:

.mp3: Supported by all modern browsers.
.wav: Not supported by Internet Explorer.
.ogg: Not supported by Internet Explorer and Safari.
 

創建./Components/MusicPlayer.razor

 1 @namespace SoBrian.MVC.Components
 2 @inherits AntDesign.AntDomComponentBase
 3  
 4 <audio id="audio" preload="auto" src="@_currentSrc"></audio>
 5 <div>
 6     <AntDesign.Row Justify="center" Align="middle">
 7         <AntDesign.Col Span="4">
 8             <p>@System.IO.Path.GetFileNameWithoutExtension(_currentSrc)</p>
 9         </AntDesign.Col>
10         <AntDesign.Col Span="4">
11             <AntDesign.Space>
12                 <AntDesign.SpaceItem>
13                     <AntDesign.Button Type="primary" Shape="circle" Icon="left" OnClick="OnLast" />
14                 </AntDesign.SpaceItem>
15                 <AntDesign.SpaceItem>
16                     <AntDesign.Button Type="primary" Shape="circle" Icon="@PlayPauseIcon" Size="large" OnClick="OnPlayPause" />
17                 </AntDesign.SpaceItem>
18                 <AntDesign.SpaceItem>
19                     <AntDesign.Button Type="primary" Shape="circle" Icon="right" OnClick="OnNext" />
20                 </AntDesign.SpaceItem>
21             </AntDesign.Space>
22         </AntDesign.Col>
23         <AntDesign.Col Span="9">
24             <AntDesign.Slider Value="@_currentTimeSlide" OnAfterChange="OnSliderChange" />
25         </AntDesign.Col>
26         <AntDesign.Col Span="3">
27             <p>@($"{_currentTime.ToString("mm\\:ss")} / {_duration.ToString("mm\\:ss")}")</p>
28         </AntDesign.Col>
29     </AntDesign.Row>
30 </div>

創建./Components/MusicPlayer.razor.cs

  1 public partial class MusicPlayer : AntDomComponentBase
  2 {
  3     private bool _isPlaying = false;
  4     private bool _canPlayFlag = false;
  5     private string _currentSrc;
  6     private List<string> _musicList = new List<string>
  7     {
  8         "music/周杰倫 - 蘭亭序.mp3",
  9         "music/周杰倫 - 告白氣球.mp3",
 10         "music/周杰倫 - 聽媽媽的話.mp3",
 11         "music/周杰倫 - 園遊會.mp3",
 12         "music/周杰倫 - 夜曲.mp3",
 13         "music/周杰倫 - 夜的第七章.mp3",
 14         "music/周杰倫 - 擱淺.mp3"
 15     };
 16     private Timer _timer;
 17     private double _currentTimeSlide = 0;
 18     private TimeSpan _currentTime = new TimeSpan(0);
 19     private TimeSpan _duration = new TimeSpan(0);
 20     private string PlayPauseIcon { get => _isPlaying ? "pause" : "caret-right"; }
 21     private Action _afterCanPlay;
 22     [Inject]
 23     private DomEventService DomEventService { get; set; }
 24  
 25     protected override void OnInitialized()
 26     {
 27         base.OnInitialized();
 28  
 29         _currentSrc = _musicList[0];
 30         _afterCanPlay = async () =>
 31         {
 32             // do not use _isPlaying, this delegate will be triggered when user clicked play button
 33             if (_canPlayFlag)
 34             {
 35                 try
 36                 {
 37                     await JsInvokeAsync("Music.play", "#audio", true);
 38                     _canPlayFlag = false;
 39                 }
 40                 catch (Exception ex)
 41                 {
 42                 }
 43             }
 44         };
 45     }
 46  
 47     protected override Task OnFirstAfterRenderAsync()
 48     {
 49         // cannot listen to dom events in OnInitialized while render-mode is ServerPrerendered
 50         DomEventService.AddEventListener<JsonElement>("#audio", "timeupdate", OnTimeUpdate);
 51         DomEventService.AddEventListener<JsonElement>("#audio", "canplay", OnCanPlay);
 52         DomEventService.AddEventListener<JsonElement>("#audio", "play", OnPlay);
 53         DomEventService.AddEventListener<JsonElement>("#audio", "pause", OnPause);
 54         DomEventService.AddEventListener<JsonElement>("#audio", "ended", OnEnd);
 55         return base.OnFirstAfterRenderAsync();
 56     }
 57  
 58         #region Audio EventHandlers
 59  
 60         private async void OnPlayPause(MouseEventArgs args)
 61         {
 62             try
 63             {
 64                 await JsInvokeAsync("Music.play", "#audio", !_isPlaying);
 65         }
 66             catch (Exception ex)
 67             {
 68             }
 69         }
 70  
 71     private async void OnCanPlay(JsonElement jsonElement)
 72     {
 73         try
 74         {
 75             string json = await JsInvokeAsync<string>("Music.getMusicTime", "#audio");
 76             jsonElement = JsonDocument.Parse(json).RootElement;
 77             _duration = TimeSpan.FromSeconds(jsonElement.GetProperty("duration").GetDouble());
 78  
 79             _afterCanPlay();
 80         }
 81         catch (Exception)
 82         {
 83         }
 84     }
 85  
 86     private void OnPlay(JsonElement jsonElement)
 87     {
 88         _isPlaying = true;
 89     }
 90  
 91     private async void OnLast(MouseEventArgs args)
 92     {
 93         _canPlayFlag = true;
 94         int index = _musicList.IndexOf(_currentSrc);
 95         index = index == 0 ? _musicList.Count - 1 : index - 1;
 96         _currentSrc = _musicList[index];
 97     }
 98  
 99     private async void OnNext(MouseEventArgs args)
100     {
101         _canPlayFlag = true;
102         int index = _musicList.IndexOf(_currentSrc);
103         index = index == _musicList.Count - 1 ? 0 : index + 1;
104         _currentSrc = _musicList[index];
105     }
106  
107     private void OnPause(JsonElement jsonElement)
108     {
109         _isPlaying = false;
110         StateHasChanged();
111     }
112  
113         private void OnEnd(JsonElement jsonElement)
114     {
115         _isPlaying = false;
116         StateHasChanged();
117  
118         OnNext(new MouseEventArgs());
119     }
120  
121     private async void OnTimeUpdate(JsonElement jsonElement)
122     {
123         // do not use the timestamp from timeupdate event, which is the total time the audio has been working
124         // use the currentTime property from audio element
125         string json = await JsInvokeAsync<string>("Music.getMusicTime", "#audio");
126         jsonElement = JsonDocument.Parse(json).RootElement;
127         _currentTime = TimeSpan.FromSeconds(jsonElement.GetProperty("currentTime").GetDouble());
128         _currentTimeSlide = _currentTime / _duration * 100;
129  
130         StateHasChanged();
131     }
132  
133     #endregion
134  
135     private async void OnSliderChange(OneOf<double, (double, double)> value)
136     {
137         _currentTime = value.AsT0 * _duration / 100;
138         _currentTimeSlide = _currentTime / _duration * 100;
139         await JsInvokeAsync("Music.setMusicTime", "#audio", _currentTime.TotalSeconds);
140     }
141 }

 

創建./Controllers/MusicController.cs

1 public class MusicController : Controller
2 {
3     public IActionResult Index(string name)
4     {
5         return View();
6     }
7 }

創建./Views/Music/Index.cshtml

1 <component type="typeof(MusicPlayer)" render-mode="Server" />

修改./Views/Shared/_Layout.cshtml,添加以下程式碼

1 <li class="nav-item">
2     <a class="nav-link text-dark" asp-area="" asp-controller="Music" asp-action="Index">Music</a>
3 </li>

Build & Run

點擊菜單欄的Music,效果如下

 

總結

 

WebAssembly並不是JavaScript的替代品,Blazor當然也不是,在開發Blazor組件的過程中,大部分情況下,仍然要通過TypeScript / JavaScript來與DOM進行交互,比如在這個播放器的案例中,還是需要JavaScript來調用audio的play,pause等方法。但是在View層面使用播放器這個組件時,我們幾乎可以不再關心JavaScript的開發。這讓前端的開發更類似於開發WPF的XAML介面。事實上,社區里也有這樣的項目,致力於提供一種類WPF介面開發的組件庫。

 

同時,也希望大家能多多關注中國小夥伴們共同參與開發的AntDesign,作為最熱門的Blazor組件庫之一,在今年的MS Build大會上也獲得了微軟官方的認可。雖然目前組件還有不少BUG和性能問題,但是在社區的努力下,相信它會越來越好,讓我們一起為.NET生態添磚加瓦!

 

參考:

//catswhocode.com/html-audio-tag

//www.w3schools.com/TAGS/tag_audio.asp

//github.com/ant-design-blazor/ant-design-blazor