在 maui 微軟的官方方案是使用 blazor 開發,但是當前市場大多數的 web 項目使用 vue,react 等技術構建,如果我們沒法繞過已經積累的技術,用 blazor 重寫整個項目並不現實。
vue 是當前流行的 web 框架, 簡單來說是一套模板引擎,利用“模板”和“綁定”兩大特性實現 web 頁面 mvvm 模式開發。利用.net maui 框架可以將 vue 應用嵌入到 web 容器中。可以實現跨平台的混合開發。
例如我在某醫療行業項目中,已經用這個混合開發的方式生成應用,vue 代碼不需要做什麼改動,就能跨平台運行:

如果你有一套 vue 開發的網站,可以根據這篇文章,嘗試移值進你的 iphone,android 以及平板電腦等行動裝置。
混合開發的核心工作是構建 web 與.net 的互操作,我們將利用 blazor 引擎的如下功能:
- 資源的統一管理
- js 代碼的注入
- js 調用 c#代碼
- c#調用 js 代碼
如果你还不了解混合开发的概念,请回看上一章节[MAUI] 混合开发概念_jevonsflash 的专栏-CSDN 博客
整個工作分為 maui 部分,vue 部分和混合改造。
maui 部分
創建 maui app 項目:

你也可以創建 maui blazor app 項目,命名為 matoproject,但是這個模板主要圍繞 blazor 開發,有的功能我們並不需要,得刪很多文件。
創建完成後編輯 matoproject.csproj,在 sdk 最末尾加上.razor,vs 會自動安裝 microsoft.aspnetcore.components.webview.maui 依賴包(注意不要手動 nuget 添加這個包,否則程式無法運行)


安裝完成後在項目目錄中創建一個 wwwroot 文件夾

這個文件夾將是混合開發 web 部分的根目錄,這個名稱不能隨便定義,我們看看為什麼:
打開 microsoft.aspnetcore.components.webview.maui.targets 這個文件:

我们可以看到构建项目时,这个库会将 wwwroot 文件夹里的内容作为 Maui 资源(MauiAsset)类型设置标签,编译器则会根据 MauiAsset 标签将这些内容打包进各个平台的资源文件夹,具体的 Maui 资源类型可以参考这个文章.NET MAUI – Manage App Resources – Developer Thoughts (egvijayanand.in) ,
打開 mauiprogram.cs 在 builder 中註冊 blazormauiwebview 組件,在服務中使用擴展方法 addblazorwebview()來添加相關 blazor 的服務
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Controls.Compatibility;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.AspNetCore.Components.WebView.Maui;
using Microsoft.Extensions.DependencyInjection;
namespace MatoProject
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.RegisterBlazorMauiWebView()
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddBlazorWebView();
return builder.Build();
}
}
}
打開 mainpage.xaml,編輯原生應用的主頁面:
建立 blazorwebview 控制項鋪滿屏幕,並設置 hostpage 為 web 部分的主頁 index.html
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MatoProject.MainPage"
xmlns:b="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui"
BackgroundColor="{DynamicResource SecondaryColor}">
<Grid>
<b:BlazorWebView HostPage="wwwroot/index.html">
<b:BlazorWebView.RootComponents>
<b:RootComponent Selector="#blazorapp" x:Name="MainWebView" ComponentType="{x:Type local:Index}/>
</b:BlazorWebView.RootComponents>
</b:BlazorWebView>
</Grid>
</ContentPage>
建立_import.razor
@using System.Net.Http @using Microsoft.AspNetCore.Components.Forms @using
Microsoft.AspNetCore.Components.Routing @using
Microsoft.AspNetCore.Components.Web @using
Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop
@using MatoProject
vue 部分
至此我們建立好了原生開發的 web 容器,接下來需要處理 vue 項目了:
cd 到項目目錄,使用 vue-cli 創建一個空白 vue 項目:

這裡可以按照 vue 的編程喜好建立,比如我選擇了 2.0 項目,支持 typescript,es6 的 class 命名方式等,最終都要通過 webpack 打包成靜態資源,所以無所謂。
建立 src/api/fooservice.ts,創建如下的函數:
window['dotnet']對象將是 maui blazor 中注入的交互操作對象
export async function GetAll(data) {
var result = null;
await window["DotNet"]
.invokeMethodAsync("MatoProject", "GetFoo")
.then((data) => {
console.log("DotNet method return the value:" + data);
result = data;
});
return result;
}
export async function Add(data) {
var result = null;
await window["DotNet"]
.invokeMethodAsync("MatoProject", "Add", data)
.then((data) => {
console.log("DotNet method return the value:" + data);
result = data;
});
return result;
}
打開 home.vue 編輯:
這是 web 的主頁面,我們需要三個按鈕以及相關函數,測試 js 與 c#的交互操作。
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<div>
<h3>foo:</h3>
<button @click="getFoo">click to get foo</button>
<br />
<span>{{ foo }}</span>
</div>
<div>
<h3>bar:</h3>
<span>{{ bar }}</span>
</div>
<div>
<button @click="add">click here to add</button>
<span>click count:{{ cnt }}</span>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
import { GetAll, Add } from "@/api/fooService";
@Component({
components: {
HelloWorld,
},
})
export default class Home extends Vue {
foo: string = "";
bar: string = "";
cnt: number = 0;
async created() {
window["postBar"] = this.postBar;
}
async add() {
this.cnt = await Add({ a: this.cnt, b: 1 });
}
async getFoo() {
var foo = await GetAll(null);
this.foo = foo;
}
async postBar(data) {
this.bar = data;
console.log("DotNet invocked the function with param:" + data);
return this.bar;
}
}
</script>
到此已經完成了一個簡單的 vue 項目
運行打包命令:
PS D:\Project\maui-vue-hybirddev\hybird-host> yarn build
將 dist 目錄中的所有內容複製到 wwwroot 文件夾下。
混合改造
這是混合開發的重點,改造 maui 項目,以適配 vue
打開 wwwroot/index.js 重寫為:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="favicon.ico" />
<title>hybird-host</title>
<link href="js/about.dc8b0f2b.js" rel="prefetch" />
<link href="css/app.03043124.css" rel="preload" as="style" />
<link
href="js/app.b6b5425b.js"
rel="preload"
as="script"
crossorigin="anonymous"
/>
<link
href="js/chunk-vendors.cf6d8f84.js"
rel="preload"
as="script"
crossorigin="anonymous"
/>
<link href="css/app.03043124.css" rel="stylesheet" />
</head>
<body>
<div id="blazorapp">Loading...</div>
<script src="_framework/blazor.webview.js" autostart="false"></script>
</body>
</html>
注意,僅全部重寫 body 部分,不要更改 head 的 link 標籤內容,僅在 js 後面加上 crossorigin="anonymous" 以解決跨域問題。
建立 index.razor 文件:
@using Microsoft.Maui.Controls @inject IJSRuntime JSRuntime @implements
IDisposable
<noscript
><strong
>We're sorry but CareAtHome doesn't work properly without JavaScript
enabled. Please enable it to continue.</strong
></noscript
>
<div id="app"></div>
@code { [JSInvokable] public static Task<string>
GetFoo() { return Task.FromResult("this is foo call C# method from js"); }
[JSInvokable] public static Task<int>
Add(AddInput addInput) { return Task.FromResult(addInput.a + addInput.b); }
public async void Post(object o, EventArgs a) { await
JSRuntime.InvokeAsync<string
>("postBar", "this is bar call js method from C#"); } protected override
async Task OnAfterRenderAsync(bool firstRender) { ((App.Current as
App).MainPage as MainPage).OnPostBar += this.Post; try { if (firstRender)
{ await JSRuntime.InvokeAsync<IJSObjectReference
>("import", "./js/chunk-vendors.cf6d8f84.js", new { crossorigin =
"anonymous" }); await JSRuntime.InvokeAsync<IJSObjectReference
>("import", "./js/app.b6b5425b.js", new { crossorigin = "anonymous"
}); } } catch (Exception ex) { Console.WriteLine(ex); } } public void
Dispose() { (Application.Current.MainPage as MainPage).OnPostBar -=
this.Post; } }</IJSObjectReference
></IJSObjectReference
></string
></int
></string
>
注意以下這兩個語句需要對應打包生成的實際文件名,並且加上跨域標籤
await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/chunk-vendors.cf6d8f84.js", new { crossorigin = "anonymous" });
await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/app.b6b5425b.js", new { crossorigin = "anonymous" });
mainpage.xaml 建立一個按鈕並且設置觸發事件方法:
<button
Text="Post Bar To WebView"
HorizontalOptions="Center"
VerticalOptions="End"
HeightRequest="40"
Clicked="PostBar_Clicked"
></button>
CodeBehind:
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Essentials;
namespace MatoProject
{
public partial class MainPage : ContentPage
{
public event EventHandler<EventArgs> OnPostBar;
int count = 0;
public MainPage()
{
InitializeComponent();
}
private async void PostBar_Clicked(object sender, EventArgs args)
{
OnPostBar?.Invoke(this, args);
}
}
}
至此,所有的代碼工作已經完成,在 pc 上可以選擇 windows 或者 android 模擬器來運行程式

運行效果:

若在 windows 平台上運行,原生控制項使用 edge webview2 呈現器加載頁面, 按 f12 會調用原生的調試工具,在這裡看到列印

現在,可能有人會問為什麼要使用這樣的技術架構?明明可能有更好用的混合開發技術 ionic,react native,uni-app。首先不可否認這些技術都有他們的特點與優勢,但當你擁有一個成熟的 xamarin 框架,你可以輕鬆遷移到 maui,利用 efcore 實現數據持久化或者集成 abp 框架來配置依賴注入,全局事件,本地化等移動開發常用的功能(另一篇文章將會教大家如何將 abp 移值進 maui)。xamarin 是一個設備抽象層,提供的 webview 也有較好的 h5 兼容性。
當然主要原因還是在快速開發上,你的代碼積累才是寶貴的,更少的修改代碼量才是王道,如果你在用 react 技術棧編寫 web 代碼,也許 react native 才是你最佳選擇 。沒有最優的技術,只有最適合你的技術。
代碼倉庫: