疯狂吐槽 MAUI 以及 MAUI 入坑知识点

目录
  • 窗口
  • 窗口管理
  • 如何限制一次只能打开一个程序
  • MAUI 程序安装模式
  • 为 MAUI Blazor 设置语言
    • 坑 ①
    • 坑 ②
    • 坑 ③
  • 配置 MAUI 项目使用管理员权限启动
    • 问题背景
    • 定制编译过程
  • MAUI 实现前后端分离开发
    • 背景
    • 先搞前端
    • 创建 MAUI Blazor 项目
  • C# 自动化生成证书、本地安装证书、解决信任证书问题
    • 背景
    • 写代码
    • 在 ASP.NET Core 中使用

这里是笔者在开发 MAUI 应用时踩的坑,以及一些笔记的汇总。

不得不说 MAUI 挺垃圾的。

如果不是 Mono 金玉在前,估计社区不会有多少人关注败絮 MAUI。

目前 .NET 已经升级到 7.0,但是 MAUI 还是一如既往的拉跨,如果开发过 MAUI,做过定制,自定义标题栏之类的,便会发现 MAUI 有多难受。

MAUI 不知道跟 UWP 有啥关系,但是 MAUI 很多东西感觉都是在延续 UWP 的设计,而且 MAUI 也很可能是下一个 UWP。

如果是 Windows 或者 Linux 桌面开发,建议 WPF 或 Avalonia, MAUI 真上不来台面,在 WPF 下面可以找到 好多 API ,但是 MAUI 都没有。。。
MAUI Windows 基于 WINUI 开发的,但是你按照 WINUI 去找资料,找到的东西 MAUI 又用不了。。。

这里笔者推荐一个好用的前端打包客户端工具 tauri ,tauri 是用 rust 实现的,然后可以结合其它前端框架开发应用。

其开发模式比 MAUI Blazor 好很多,开发体验也非常好,生成的软件体积也是出奇的小,而且生成的 app 自带安装界面,生成的程序就是已经打包好的,省得自己手动重新打包。

文章介绍:http://www.whuanle.cn/archives/21062

与其他桌面开相比, MAUI 真的非常臃肿,而且 MAUI Blazor 开发得真的不爽,包括 Visual Studio 本身的开发体验,框架本身的使用,以及 Blazor UI 框架,三个方面的体验都不够好。

能打的 Blazor 框架少,可以轻易扩展容易改造的 UI 框架更加少,目前发现能够使用的 Blazor 框架,比较好的有 MASA Blazor。

为什么这么说呢,首先是 Blazor 编写过程中,编辑器对 Razor 的支持不好,会经常出现没有语法提示,代码有错误但是编辑器没有提示,编辑器提示有错误实际上代码没有错误,等等。。。

其次,关于 MAUI 下 Blazor 的使用和 Blazor 框架的选型。在 MAUI 下使用 Blazor,如果使用第三方 UI 框架,引入之后,会发现其天然有一种封闭性。
如果使用纯前端框架开发,你会发现依赖引用关系很清晰,需要引用什么包,编译器会提示,编译时会提示。

而 Blazor 框架,很难知道里面用了哪些 js,Blazor dll 里面嵌套了 js 等文件,其本身就是一种封闭性,而关于内部的情况更加难以了解,出现了 Bug 调试难。

而且 Blazor 框架封装的代码 是 C# + js 写的,由于 C# 代码编译后无法修改,因此引用的 Blazor 库出问题时,难以查看调试源代码。还有,笔者从目前的 Blazor 框架中,看到了很多框架本身的代码非常臃肿,里面的设计和逻辑也不清晰,很多地方的代码限制了组件的扩展,开发者难以替换里面的实现。

过度设计也是一种毛病,因为为了支持而支持,为了灵活而灵活,过多的 API 设计和过多的参数呈现,过多的逻辑封装,实际上会让这些组件更加难用。

大多数 Blazor 框架都是个人仓库维护。而市面上精品的前端框架,几乎都是有大公司做背书,ViewJS、Ant Design 等,其框架本身有专业团队维护和大佬对框架进行设计,共同维护一个精品。

但是目前的 Blazor,我觉得,除了 MASA 做的,其它很难提得上 “精品”。

要夸 MASA ,笔者也是有理由的。

MASA 是真的用心在做生态,吸引了很多开发者和粉丝活跃参与,其开源共享精神值得敬佩。

如果你对 Blazor 有问题,对 MAUI 开发有问题,即使你用的不是 MASA 框架,你也可以到 MASA 群众提问,不会出现付费解答问题,也不会有人笑你菜,也不会有人笑你这都不懂。当然笔者并不是说开源项目付费解答有问题,我只是称赞 MASA 的开源精神。

官网:http://www.masastack.com/blazor

期待 MASA 团队做出一个精品出来。

不过就目前来说, MAUI + Blazor 桌面开发,没啥优势。。。还会带来很多问题。。。

如果可以,不想再碰 MAUI。

下面来介绍一些 MAUI 的知识点。

窗口

首先,创建项目后, APP.cs 中,有个 Microsoft.Maui.Controls.Window

file

MauiProgram.cs 中,有个 Microsoft.UI.Xaml.Window ,然后在 Windows 下 Microsoft.UI.Xaml.Window Microsoft.Maui.MauiWinUIWindowMicrosoft.UI.Xaml.Window 多种平台统一的抽象。

file

然后 Microsoft.UI.Xaml.Window 可以获取一个 AppWindow。

AppWindow appWindow = nativeWindow.GetAppWindow()!;

MAUI 里面的 Window 类 API 很混乱,大多数是从 UWP 写法继承,然后有很多 API 是 UWP 有的,但是 MAUI 没有。

混乱。

如果自己写了一个页面,要弹出这个窗口页面,那么应该使用 Microsoft.Maui.Controls.Window ,但是自己写的页面是 ContentPage,并不是 Window。

因此并不能直接使用 Window,而是将 ContentPage 放到 Window 中,生成 Window 后再操作。

        private Microsoft.Maui.Controls.Window BuildUpdateWindow(ContentPage updatePage)
        {
            Window window = new Window(updatePage);
            window.Title = "更新通知";
            return window;
        }

然后弹出这个窗口。

Application.Current!.OpenWindow(updateWindow!);

如果要异步打开窗口,请使用 Application.Current!.Dispatcher.DispatchAsync

            await Application.Current!.Dispatcher.DispatchAsync(async () =>
            {
                try
                {
                        Application.Current!.OpenWindow(updateWindow!);
                }
                catch (Exception ex)
                {
                    Logger.LogError("无法启动更新窗口", ex);
                }
            });

如果想关闭所有窗口:

            await Application.Current!.Dispatcher.DispatchAsync(async () =>
            {
                var windows = Application.Current!.Windows.ToArray();
                foreach (var window in windows)
                {
                    try
                    {
                        Application.Current.CloseWindow(window);
                    }
                    catch (Exception ex)
                    {
                        Debug.Assert(ex != null);
                    }
                }
            });

虽然你获得了 Microsoft.Maui.Controls.Window ,但是不能直接管理这个 Window,而是应该通过 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow 管理。

也就是在依赖注入里面的窗口生命周期管理里面写。

或者除非你可以拿到 AppWindow 实例。

遗憾的是,Microsoft.Maui.Controls.Window 转不了 Microsoft.UI.Xaml.WindowMicrosoft.UI.Windowing.AppWindow

你应该这样写:

            builder.ConfigureLifecycleEvents(events =>
            {
                events.AddWindows(wndLifeCycleBuilder =>
                {
                    wndLifeCycleBuilder.OnWindowCreated(window =>
                    {
                        var nativeWindow = (window as Microsoft.Maui.MauiWinUIWindow)!;
                        ... ...
                    })
                    .OnActivated((window, args) =>
                    {
                    })
                    .OnClosed((window, args) =>
                    {
                    });
                });
            });
        private static void MainWindowCreated(MauiWinUIWindow nativeWindow)
        {
            const int width = 1440;
            const int height = 900;

            AppWindow appWindow = nativeWindow.GetAppWindow()!;

            // 扩展标题栏,要自定义标题栏颜色,必须 true

            nativeWindow.ExtendsContentIntoTitleBar = true;

            // 这里必须设置为 Overlapped,之后窗口 Presenter 就是 OverlappedPresenter,便于控制
            appWindow.SetPresenter(AppWindowPresenterKind.Overlapped);

            //if (appWindow.Presenter is OverlappedPresenter p)
            //{
            //   // p.SetBorderAndTitleBar(hasBorder: false, hasTitleBar: true);
            //}

            // 重新设置默认打开大小
            appWindow.MoveAndResize(new RectInt32(1920 / 2 - width / 2, 1080 / 2 - height / 2, width, height));

            // 窗口调整的各类事件
            appWindow.Changed += (w, e) =>
            {
                // 位置发生变化
                if (e.DidPositionChange) 
                {
                }
                if (e.DidPresenterChange) { }
                // 大小发生变化
                if (e.DidSizeChange) { }
                if (e.DidVisibilityChange) { }
                if (e.DidZOrderChange) { }
            };
            appWindow.Closing += async (w, e) =>
            {
                try
                {
                    Environment.Exit(0);
                }
                catch (Exception ex)
                {
                    var log = AppHelpers.LoggerFactory.CreateLogger<AppWindow>();
                    log.LogError(ex, "Can't close WebHost");
                    ProcessManager.ReleaseLock();
                }
                finally
                {
                    ProcessManager.ExitProcess(0);
                }
            };
            appWindow.MoveInZOrderAtTop();
        }

其次,你想直接获取当前的窗口实例,也是麻烦。

可以通过以下代码获取当前程序打开的所有窗口。

App.Current.Windows
Application.Current.Windows

如果你想获取当前正在使用或激活的窗口,笔者并不知道怎么通过里面的 API 获取。。。如果用 Win32 那么倒是可以。

问:有没有一种这样的 API 呢?

Current.GetWindos()

另外,MAUI 做不到自定义标题栏,天王老子来了都不行。

你想给标题栏改个背景色,估计都得累死。

如果要修改窗口标题,只能在窗口创建时修改,也就是 Microsoft.Maui.Controls.Windows,用 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow 都改不了。

            Microsoft.Maui.Controls.Window window = base.CreateWindow(activationState);
            window.Title = Constants.Name;

如果要获取原生的 Window 句柄,可以使用:

            var nativeWindow = mauiWindow.Handler.PlatformView;
            IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow);

窗口管理

前面提到,想管理窗口,API 要用 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow 的。

有些地方只能用原生的 Window 窗口句柄,然后用 Win32 操作。

自定义窗口生命周期时,一定要使用:

            // 这里必须设置为 Overlapped,之后窗口 Presenter 就是 OverlappedPresenter,便于控制
            appWindow.SetPresenter(AppWindowPresenterKind.Overlapped);

然后常用的窗口方法有:

    /*
     AppWindow 的 Presenter ,一定是 OverlappedPresenter
     */

    public class WindowService : IWindowService
    {
        private readonly AppWindow _appWindow;
        private readonly Window _window;

        private WindowService(AppWindow appWindow, Window window)
        {
            _appWindow = appWindow;
            _window = window;
        }
        
        // 检查当前窗口是否全屏
        public bool FullScreenState
        {
            get
            {
                switch (_appWindow.Presenter)
                {
                    case OverlappedPresenter p:return p.State == OverlappedPresenterState.Maximized;
                    case FullScreenPresenter p:return p.Kind == AppWindowPresenterKind.FullScreen;
                    case CompactOverlayPresenter p: return p.Kind == AppWindowPresenterKind.FullScreen;
                    case AppWindowPresenter p: return p.Kind == AppWindowPresenterKind.FullScreen;
                    default:return false;
                }
            }
        }

        // 让窗口全屏
        public void FullScreen()
        {
            switch (_appWindow.Presenter)
            {
                case OverlappedPresenter overlappedPresenter:
                    overlappedPresenter.SetBorderAndTitleBar(true, true);
                    overlappedPresenter.Maximize();
                    break;
            }
            // 全屏时去掉任务栏
            // _appWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
        }

        // 退出全屏
        public void ExitFullScreen()
        {
            switch (_appWindow.Presenter)
            {
                case OverlappedPresenter p: p.Restore();break;
                default: _appWindow.SetPresenter(AppWindowPresenterKind.Default); break;
            }
        }

        // 最小化到任务栏
        public void Minmize()
        {
#if WINDOWS
            var mauiWindow = App.Current.Windows.First();
            var nativeWindow = mauiWindow.Handler.PlatformView;
            IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow);

            PInvoke.User32.ShowWindow(windowHandle, PInvoke.User32.WindowShowStyle.SW_MINIMIZE);
#endif
        }

        /// <summary>
        /// 激活当前窗口
        /// </summary>
        public void Active()
        {
            _appWindow.Show(true);
        }
        
        // 关闭窗口
        public void Exit()
        {
            _window.Close();
        }

        public void SetSize(int _X, int _Y, int _Width, int _Height)
        {
            _appWindow.MoveAndResize(new RectInt32(_X, _Y, _Width, _Height));
        }

        public (int X, int Y) GetPosition()
        {
            var p = _appWindow.Position;
            return (p.X, p.Y);
        }

        public (int X, int Y) Move(int x, int y)
        {
            _appWindow.Move(new PointInt32(x, y));
            return GetPosition();
        }

        public (int Width, int Height, int ClientWidth, int ClientHeight) GetSize()
        {
            var size = _appWindow.Size;
            var clientSize = _appWindow.ClientSize;
            return (size.Width, size.Height, clientSize.Width, clientSize.Height);
        }

        public (PointInt32 Position, SizeInt32 Size, SizeInt32 ClientSize) GetAppSize()
        {
            return (_appWindow.Position, _appWindow.Size, _appWindow.ClientSize);
        }
    }

让窗口全屏有两种方法,一种是全屏时,窗口把任务栏吞了,真正意义上的的全屏,另一种是保留任务栏。

            // 保留任务栏
            switch (_appWindow.Presenter)
            {
                case OverlappedPresenter overlappedPresenter:
                    overlappedPresenter.SetBorderAndTitleBar(true, true);
                    overlappedPresenter.Maximize();
                    break;
            }
            // 全屏时去掉任务栏
            // _appWindow.SetPresenter(AppWindowPresenterKind.FullScreen);

最小化只能通过 Win32 API 处理,你要先获取 Microsoft.Maui.Controls.Windows,然后转换为 Window 句柄。

            var nativeWindow = mauiWindow.Handler.PlatformView;
            IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow);

            PInvoke.User32.ShowWindow(windowHandle, PInvoke.User32.WindowShowStyle.SW_MINIMIZE);

此时 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow 就用不上了。

前面提到,要使用 Microsoft.UI.Xaml.Window,或 Microsoft.UI.Windowing.AppWindow ,例如在 MauiProgram.cs 里面记录了窗口的事件,创建窗口时控制大小。

但是,窗口运行中,要设置窗口大小或限制大小,则是要通过 Microsoft.Maui.Controls.Windows

例如,控制主窗口大小不能太小,不能被无限缩小,要在 APP.cs 中这样写:

        protected override Window CreateWindow(IActivationState? activationState)
        {
            Window window = base.CreateWindow(activationState);
            window.Title ="窗口标题";

            var minSize = GetMinSize();
            window.MinimumWidth = minSize.MinWidth;
            window.MinimumHeight = minSize.MinHeight;

            // Give the Window time to resize
            window.SizeChanged += (sender, e) =>
            {
                var minSize = GetMinSize();
                window.MinimumWidth = minSize.MinWidth;
                window.MinimumHeight = minSize.MinHeight;
            };

            //window.Created += (s, e) =>
            //{
            //};

            //window.Stopped += (s, e) =>
            //{
            //};

            return window;
            
            (int MinWidth, int MinHeight) GetMinSize()
            {
                // 获取当前屏幕的长宽,用 X、Y 表示。
                var x = PInvoke.User32.GetSystemMetrics(PInvoke.User32.SystemMetric.SM_CXFULLSCREEN);
                var y = PInvoke.User32.GetSystemMetrics(PInvoke.User32.SystemMetric.SM_CYFULLSCREEN);
                // 设置窗口最小值,可以按照比例计算,也可以直接设置固定大小
                return (x / 3 * 2, y / 5 * 4);
            }
        }

PS,在 AppWindow 里面的事件做大小限制,是做不到的,这里主要是观察,想做窗口大小等限制是不行的。

            // 窗口调整的各类事件
            appWindow.Changed += (w, e) =>
            {
                // 位置发生变化
                if (e.DidPositionChange) 
                {

                }
                if (e.DidPresenterChange) { }
                // 大小发生变化
                if (e.DidSizeChange) { }
                if (e.DidVisibilityChange) { }
                if (e.DidZOrderChange) { }
            };

如何限制一次只能打开一个程序

场景,如果程序D 已被运行 进程 A,那么再次启动程序D 运行进程 B,B 会识别到已有相同的进程,此时 B 会将 A 窗口激活弹出来,然后 B 再退出。

这样不仅可以限制只能运行一个进程,而且可以让用户体验更加好。

锁可以使用 Mutex 来实现,在整个操作系统中,大家可以识别到同一个锁。

然后激活另一个窗口,可以使用 Win32。

    // 进程管理器
    internal static class ProcessManager
    {
        private static Mutex ProcessLock;
        private static bool HasLock;

        /// <summary>
        /// 获取进程锁
        /// </summary>
        public static void GetProcessLock()
        {
            // 全局锁
            ProcessLock = new Mutex(false, "Global\\" + "自定义锁名称", out HasLock);

            if (!HasLock)
            {
                ActiveWindow();
                Environment.Exit(0);
            }
        }

        /// <summary>
        /// 激活当前进程并将其窗口放到屏幕最前面
        /// </summary>
        public static void ActiveWindow()
        {
            string pName = Constants.Name;
            Process[] temp = Process.GetProcessesByName(pName);
            if (temp.Length > 0)
            {
                IntPtr handle = temp[0].MainWindowHandle;
                SwitchToThisWindow(handle, true);
            }
        }

        /// <summary>
        /// 释放当前进程的锁
        /// </summary>
        /// <remarks>小心使用</remarks>
        public static void ReleaseLock()
        {
            if (ProcessLock != null && HasLock)
            {
                ProcessLock.Dispose();
                HasLock = false;
            }
        }
        
        // 将另一个窗口激活放到前台。
        // Win32 API
        [DllImport("user32.dll")]
        public static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);
    }

然后在程序启动时使用。

file

MAUI 程序安装模式

如果直接使用原生的 MAUI 程序,安装时会特别麻烦,因为这种方式就是以前的 UWP。

因此,可以使用那种,不需要安装直接运行的方式。

但是这里我们要了解一下两种模式的区别。

如果使用原生 MAUI 模式,那么会被生成 Windows 应用市场应用,无论是发布、上架、安装,都是非常麻烦的。但是好在可以使用很多 Windows 应用的 API。

例如要获取应用程序安装目录:

ApplicationData appdata = Windows.Storage.ApplicationData.Current

获取本地存储目录和临时目录:

var localPath = Windows.Storage.ApplicationData.Current.LocalFolder.Path
var tempPath = Windows.Storage.ApplicationData.Current.TemporaryFolder.Path

目录一般安装位置:

"C:\\Users\\{用户名}\\AppData\\Local\\Packages\\{程序GUID}

还有其它一些语言处理等 API,使用商场应用模式是挺方便的。

接下来说一下自定义打包模式,就是直接编译生成一堆文件,然后直接启动 exe 就能运行的,不需要安装。如果想做成安装包,可以先发布,然后使用打包工具打包。

就是在项目文件中加上这两句即可:

		<WindowsPackageType>None</WindowsPackageType>
		<WindowsAppSDKSelfConatined>true</WindowsAppSDKSelfConatined>

之后会直接生成可执行文件,不再需要安装 MAUI 应用才行运行起来,也不需要证书才能运行。

为 MAUI Blazor 设置语言

MAUI Blazor 在 Windows 上使用的是 WebView2,MAUI Blazor 运行环境是跟程序没关系的,即使是系统设置了中文语言,程序集设置了中文,本地文化设置了中文,CultureInfo 设置了中文,统统都没有用。

你可以在程序启动后,按下 F12,然后执行 JavaScript 代码,检查浏览器的运行环境是何种语言:

navigator.language
'en-US'

file

或者使用 API:

// using Windows.Globalization
var langs = ApplicationLanguages.Languages.ToList<string>();

file

坑 ①

首先,设置 Windows.Globalization:

            ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";

然后重新启动程序,发现:

file

但是浏览器语言环境依然没有变化:
file

原因是 Preferences 文件需要重新生成才能起效,后面会提到。

坑 ②

程序启动后,会在 {Windows程序数据目录}/{你的程序ID}/LocalState\EBWebView\Default 下面生成一些 WebView2 的文件,其中 Preferences 文件,里面配置了 WebView2 的参数。

找到自己的程序数据目录:

var path = Windows.Storage.ApplicationData.Current.LocalFolder.Path;

file

因此,可以通过手动的方式修改文件,让 WebView2 使用中文环境。

file

file

var langs = ApplicationLanguages.Languages.ToList<string>();
var cultureInfo = CultureInfo.InstalledUICulture;
var index = langs.FindIndex((lang) => cultureInfo.Equals(CultureInfo.CreateSpecificCulture(lang)));
if (index > 0)
{
	ApplicationLanguages.PrimaryLanguageOverride = cultureInfo.Name;
	// "...this is immediately reflected in the ApplicationLanguages.Languages property."
	langs = ApplicationLanguages.Languages.ToList<string>();
}
var selectedLangs = string.Join(",", langs);
// Should check if this is the same as before but...
var preferences = Windows.Storage.ApplicationData.Current.LocalFolder.Path + "\\EBWebView\\Default\\Preferences";
if (File.Exists(preferences))
{
	var jsonString = File.ReadAllText(preferences);
	var jsonObject = JObject.Parse(jsonString);    // using Newtonsoft.JSON
	//var intl = jsonObject["intl"];
	jsonObject["intl"] = JObject.Parse($@"{{""selected_languages"": ""{selectedLangs}"",""accept_languages"": ""{selectedLangs}""}}");
	jsonString = JsonConvert.SerializeObject(jsonObject);
        File.WriteAllText(preferences, jsonString);
}

坑 ③

最后我发现, ① 的思路是对的,不起效的原因是 Preferences 文件需要删除等重新创建才行,只要在程序启动时(WebView 尚未启动),设置中文即可。
file

file

检查代码:

    public static class MauiProgram
    {
        private static void SetWebViewLanguage()
        {
            ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";

            var basePath = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
            var preferencesFile = Path.Combine(basePath, "EBWebView/Default/Preferences"); // Preferences
            if (!File.Exists(preferencesFile)) return;

            var jsonString = File.ReadAllText(preferencesFile);
            var jsonObject = JsonObject.Parse(jsonString).AsObject();
            var languages = jsonObject["intl"]["selected_languages"].Deserialize<string>() ?? "";
            // "zh-CN,en,en-GB,en-US"
            if (!languages.StartsWith("zh-CN"))
            {
                // File.Delete(preferencesFile);
                jsonObject.Remove("intl");
                jsonObject.Add("intl", JsonObject.Parse("{\"selected_languages\":\"zh-CN,en,en-GB,en-US\"}"));
                jsonString = JsonSerializer.Serialize(jsonObject);
                File.WriteAllText(preferencesFile, jsonString);
            }
        }

public static MauiApp CreateMauiApp() 中使用:
file

配置 MAUI 项目使用管理员权限启动

问题背景

在 Windows 中,开发的应用可以使用 app.manifest 资产文件配置程序启动时,使用何种角色权限启动。

效果如下:

file

正常情况下,在 app.manifest 加上以下配置即可:

如果项目中没有这个文件,可以在项目中新建项-清单文件。

  <trustInfo xmlns='urn:schemas-microsoft-com:asm.v2'>
    <security>
      <requestedPrivileges xmlns='urn:schemas-microsoft-com:asm.v3'>
        <requestedExecutionLevel level='requireAdministrator' uiAccess='false' />
      </requestedPrivileges>
    </security>
  </trustInfo>

file

但是在 MAUI 应用中,是加不上去的,如果加上去,就会出现报错。

Platforms\Windows\app.manifest : manifest authoring error c1010001: Values of attribute "level" not equal in different manifest snippets. 

因为 .NET 编译器中,已经默认为程序生成一个 app.manifest 文件,其文件内容中已经包含了 trustInfo 配置。

如果项目中开启了 <WindowsAppSDKSelfConatined>true</WindowsAppSDKSelfConatined>,那么应该查看 Microsoft.WindowsAppSDK.SelfContained.targets 文件:
file
file

所以如果要自定义 app.manifest ,要么就是把 Microsoft.WindowsAppSDK.SelfContained.targets 改了,但是这样不太好。

定制编译过程

如果观察编译过程,会发现 manifest 文件会生成到 obj 目录。
file

其中 mergeapp.manifest 便是项目中的 app.manifest ,.NET 编译的时候将开发者的文件改名字了。

程序编译时,首先从 Microsoft.WindowsAppSDK.SelfContained.targets 中生成默认的 app.manifest
接着将开发者项目中的 app.manifest 复制为 mergeapp.manifest 文件,然后将 mergeapp.manifest 合并到 app.manifest

如果 app.manifest 中已经存在配置,那么 mergeapp.manifest 中重复的记录就会导致编译报错。

既然了解到了编译过程,那么我们可以在编译过程中做手脚。
我们可以在编译生成 app.manifest 但是还没有编译主程序的时候,将 app.manifest 中的配置替换掉,替换命令是:

powershell -Command ";(gc app.manifest) -replace 'level=''asInvoker''', 'level=''requireAdministrator''' | Out-File -encoding ASCII app.manifest";

file

MSBuild 编译使用到的步骤可以参考官方文档:
http://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-targets?view=vs-2022

在编译过程中,有两个重要的环境变量:
_DeploymentManifestFiles :清单文件所在目录;
ApplicationManifestapp.manifest 文件路径。

可以在 .csproj 文件中加入以下脚本,这样会在程序编译时,自动修改清单文件。

	<Target Name="RequireAdministrator" BeforeTargets="GenerateManifests" Condition="'$(PublishDir)' != ''">
		<Exec WorkingDirectory="./" Command="echo $(ApplicationManifest)" />
		<Exec WorkingDirectory="./" Command="echo $(_DeploymentManifestFiles)" />
		<Exec WorkingDirectory="$(_DeploymentManifestFiles)" Command="dir" />
		<Exec WorkingDirectory="$(_DeploymentManifestFiles)" Command="powershell -Command "(gc app.manifest) -replace 'level=''asInvoker''', 'level=''requireAdministrator''' | Out-File -encoding ASCII app.manifest"" />
	</Target>

BeforeTargets="GenerateManifests" 表明在 GenerateManifests 之前,执行里面的自定义命令。

Condition="'$(PublishDir)' != ''" 表示触发条件,在 MAUI 中,只有发布的时候才会有这个变量,也可以改成 Condition="'$(Release)' != ''"

注意,有些情况下 _DeploymentManifestFiles 目录会不存在,因此可以多次测试一下。

当然,最保险的方法:

	<Target Name="RequireAdministrator" BeforeTargets="GenerateManifests" Condition="'$(PublishDir)' != ''">
		<Exec WorkingDirectory="./" Command=" powershell -Command "(gc $(ApplicationManifest)) -replace 'level=''asInvoker''', 'level=''requireAdministrator''' | Out-File -encoding UTF8 $(ApplicationManifest)"" />
	</Target>

编译过程主要在以下三步,其中只有 GenerateManifests 能够在 .csproj 中使用。

<Target Name="GenerateManifests"
        Condition="'$(GenerateClickOnceManifests)'=='true' or '@(NativeReference)'!='' or '@(ResolvedIsolatedComModules)'!='' or '$(GenerateAppxManifest)' == 'true'"
        DependsOnTargets="$(GenerateManifestsDependsOn)"/>

===================================================
GenerateApplicationManifest
Generates a ClickOnce or native application manifest.
An application manifest specifies declarative application identity, dependency and security information.
===================================================
<Target Name="GenerateApplicationManifest"
        DependsOnTargets="
                _DeploymentComputeNativeManifestInfo;
                _DeploymentComputeClickOnceManifestInfo;
                ResolveComReferences;
                ResolveNativeReferences;
                _GenerateResolvedDeploymentManifestEntryPoint"
        Inputs="
                $(MSBuildAllProjects);
                @(AppConfigWithTargetPath);
                $(_DeploymentBaseManifest);
                @(ResolvedIsolatedComModules);
                @(_DeploymentManifestDependencies);
                @(_DeploymentResolvedManifestEntryPoint);
                @(_DeploymentManifestFiles)"
        Outputs="@(ApplicationManifest)">

能够拿到参数:

                $(_DeploymentBaseManifest);
                @(ResolvedIsolatedComModules);
                @(_DeploymentManifestDependencies);
                @(_DeploymentResolvedManifestEntryPoint);
                @(_DeploymentManifestFiles)
				@(ApplicationManifest)

MAUI 实现前后端分离开发

背景

最先采用的是 Maui + Blazor 开发,使用社区热度比较高的 Blazor UI 框架。

可是开发进行过程中, Maui 巨多坑,Blazor UI 框架也是巨多坑,使用 Blazor UI 写的页面和样式,过不了设计师和产品经理的是法眼。

最终决定使用原生前端结合,生成静态内容放到 Maui 中,然后将两者结合起来打包发布。

先搞前端

对于前端来说,按照正常的开发模式就行,不对对前端的代码产生污染。

可以使用 VS 创建前端项目,将其放到解决方案中,也可以单独创建一个目录,将前端代码放到里面。

file

创建 MAUI Blazor 项目

创建 MAUI Blazor 项目,然后解决方案如下所示:

file

首先将 wwwroot/css/app.css 文件移出来,放到 wwwroot中,然后新建一个 app.js 也是放到 wwwroot 中。

app.css 文件中的内容删除与 Blazor 无关的内容,只保留以下内容:

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

.blazor-error-boundary::after {
    content: "An error has occurred."
}

.status-bar-safe-area {
    display: none;
}

@supports (-webkit-touch-callout: none) {
    .status-bar-safe-area {
        display: flex;
        position: sticky;
        top: 0;
        height: env(safe-area-inset-top);
        background-color: #f7f7f7;
        width: 100%;
        z-index: 1;
    }

	.flex-column, .navbar-brand {
		padding-left: env(safe-area-inset-left);
	}
}

接着,将以下代码放到 app.js 中,用于动态导入前端生成的 css 文件。

function InitializeCss(name) {
    document.getElementById("app-css").innerHTML = '<link rel="stylesheet" href="' + name + '">';
}

然后删除 jscss 目录。

剩下的文件如图所示:

file

然后修改 index.html,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
	<title>MauiApp3</title>
	<base href="/" />
	<link href="/app.css" rel="stylesheet" />
</head>

<body>
	<div id="app-css"></div>
	<div class="status-bar-safe-area"></div>
	<div id="app">Loading...</div>

	<div id="blazor-error-ui">
		An unhandled error has occurred.
		<a href="" class="reload">Reload</a>
		<a class="dismiss">?</a>
	</div>
	<script src="app.js"></script>
	<script src="_framework/blazor.webview.js" autostart="false"></script>

</body>

</html>

增加的 <div id="app-css"></div>,用于动态加载 css 文件。

其它内容基本不变。

打开 MainLayout.razor,这里负责动态加载前端文件,其内容如下:

@using MauiApp3.Data
@inherits LayoutComponentBase


@code {

    #region static fields

    private static readonly string[] css;
    private static readonly string[] js;

    #endregion

    #region instance fields

    [Inject]
    private IJSRuntime JS { get; set; }

    #endregion

    static MainLayout()
    {
        var path = Windows.Application­Model.Package.Current.Installed­Location.Path;

        if (Directory.Exists(Path.Combine(path, "wwwroot", "css")))
        {
            var cssList = Directory.GetFiles(Path.Combine(path, "wwwroot", "css"))
            .Where(x => x.EndsWith(".css"))
            .Select(x => Path.GetFileName(x)).ToArray();
            css = cssList;
        }
        else css = Array.Empty<string>();

        if (Directory.Exists(Path.Combine(path, "wwwroot", "js")))
        {
            var jsList = Directory.GetFiles(Path.Combine(path, "wwwroot", "js"))
                .Where(x => x.EndsWith(".js"))
                .Select(x => Path.GetFileName(x)).ToArray();
            js = jsList;
        }
        else js = Array.Empty<string>();
    }


    protected override async Task OnInitializedAsync()
    {
        await Task.CompletedTask;
    }


    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            foreach (var item in css)
            {
                await JS.InvokeVoidAsync("InitializeCss", $"css/{item}");
            }

            foreach (var item in js)
            {
                await JS.InvokeAsync<IJSObjectReference>("import", $"/js/{item}");
            }
        }
    }
}

然后为了能够将前端生成的内容自动复制到 Maui 中,可以设置脚本,在 Maui 的 .csproj 中,增加以下内容:

	<Target Name="ClientBuild"   BeforeTargets="BeforeBuild">
		<Exec WorkingDirectory="../vueproject1" Command="npm install" />
		<Exec WorkingDirectory="../vueproject1" Command="npm run build" />
		<Exec WorkingDirectory="../vueproject1" Command="DEL "dist\index.html"" />
		<Exec WorkingDirectory="./" Command="RMDIR /s/q "css"" />
		<Exec WorkingDirectory="./" Command="RMDIR /s/q "js"" />
		<Exec WorkingDirectory="../vueproject1" Command="Xcopy "dist" "../MauiApp3/wwwroot"  /E/C/I/Y" />
		<Exec WorkingDirectory="./" Command="RMDIR /s/q "$(LayoutDir)""/>
	</Target>

file

这样当启动 Maui 项目时,会编译前端项目,然后将生成的文件(不包括 index.html) 复制到 wwwroot 目录中。

启动后:

file

C# 自动化生成证书、本地安装证书、解决信任证书问题

背景

因为开发 Blazor 时 环境是 http://0.0.0.0/ ,也就是 MAUI Blazor 中只能发出 http 请求,既不能发出 http 请求,也不能发出不安全的 http 请求,但是内网的 http 是不安全的 http。
于是,只能再本地实现一个代理服务,然后让应用通过 http 访问,为了让 http 安全,需要安装自动化证书。
file

会导致 js 发不出请求。
为了让 http 安全,这里实现了本地 localhost 自动生成证书以及安装的过程。

写代码

生成证书使用的是 .NET 自带的库,不需要引入第三方包。

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

生成证书的方法参考 http://github.com/dotnetcore/FastGithub 项目。

第一步是编写一个证书生成器,其中,代码直接从这里复制: http://github.com/dotnetcore/FastGithub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGithub.HttpServer/Certs/CertGenerator.cs

然后,定义管理生成证书的服务,原版作者使用的是 .NET 7,而且当前稳定版本是 .NET 6,很多 API 不能使用,因此需要对其改造。原版地址:
http://github.com/dotnetcore/FastGithub/blob/9f9cbce624310c207b01699de2a5818a742e11ca/FastGithub.HttpServer/Certs/CertService.cs

定义证书位置和名称:

        private const string CACERT_PATH = "cacert";

        /// <summary>
        /// 获取证书文件路径
        /// </summary>
        public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/http.crt" : $"{CACERT_PATH}/http.cer";

        /// <summary>
        /// 获取私钥文件路径
        /// </summary>
        public string CaKeyFilePath { get; } = $"{CACERT_PATH}/http.key";

这里涉及到两个文件,客户端证书和私钥。
.key 是私钥,可以通过私钥来生成服务端证书和客户端证书,因此这里只需要保存 .key 私钥,不需要导出服务器证书。
.csr.cer 是客户端证书,在 Windows 下可以使用 .cer 格式。导出客户端证书的原因是要安装证书,而且安装一次即可,不需要动态生成。

证书管理服务的规则是,如果 ssl 目录下没有证书,那么就生成并安装;如果发现文件已经存在,则加载文件到内存,不会重新安装。

完整代码如下:

    /// <summary>
    /// 证书生成服务
    /// </summary>
    internal class CertService
    {

        private const string CACERT_PATH = "cacert";

        /// <summary>
        /// 获取证书文件路径
        /// </summary>
        public string CaCerFilePath { get; } = OperatingSystem.IsLinux() ? $"{CACERT_PATH}/http.crt" : $"{CACERT_PATH}/http.cer";

        /// <summary>
        /// 获取私钥文件路径
        /// </summary>
        public string CaKeyFilePath { get; } = $"{CACERT_PATH}/http.key";

        private X509Certificate2? caCert;


        /*
         本地会生成 cer 和 key 两个文件,cer 文件导入到 Window 证书管理器中。
         key 文件用于每次启动时生成 X509 证书,让 Web 服务使用。
         */

        /// <summary>
        /// 生成 CA 证书
        /// </summary> 
        public bool CreateCaCertIfNotExists()
        {
            if (!Directory.Exists(CACERT_PATH)) Directory.CreateDirectory(CACERT_PATH);
            if (File.Exists(this.CaCerFilePath) && File.Exists(this.CaKeyFilePath))
            {
                return false;
            }

            File.Delete(this.CaCerFilePath);
            File.Delete(this.CaKeyFilePath);

            var notBefore = DateTimeOffset.Now.AddDays(-1);
            var notAfter = DateTimeOffset.Now.AddYears(10);

            var subjectName = new X500DistinguishedName($"CN=运连网物流管理系统");
            this.caCert = CertGenerator.CreateCACertificate(subjectName, notBefore, notAfter);

            var privateKeyPem = ExportRSAPrivateKeyPem(this.caCert.GetRSAPrivateKey());
            File.WriteAllText(this.CaKeyFilePath, new string(privateKeyPem), Encoding.ASCII);

            var certPem = ExportCertificatePem(this.caCert);
            File.WriteAllText(this.CaCerFilePath, new string(certPem), Encoding.ASCII);

            return true;
        }

        /// <summary>
        /// 获取颁发给指定域名的证书
        /// </summary>
        /// <param name="domain"></param> 
        /// <returns></returns>
        public X509Certificate2 GetOrCreateServerCert(string? domain)
        {
            if (this.caCert == null)
            {
                using var rsa = RSA.Create();
                rsa.ImportFromPem(File.ReadAllText(this.CaKeyFilePath));
                this.caCert = new X509Certificate2(this.CaCerFilePath).CopyWithPrivateKey(rsa);
            }

            var key = $"{nameof(CertService)}:{domain}";
            var endCert = GetOrCreateCert();
            return endCert!;

            // 生成域名的1年证书
            X509Certificate2 GetOrCreateCert()
            {
                var notBefore = DateTimeOffset.Now.AddDays(-1);
                var notAfter = DateTimeOffset.Now.AddYears(1);

                var extraDomains = GetExtraDomains();

                var subjectName = new X500DistinguishedName($"CN={domain}");
                var endCert = CertGenerator.CreateEndCertificate(this.caCert, subjectName, extraDomains, notBefore, notAfter);

                // 重新初始化证书,以兼容win平台不能使用内存证书
                return new X509Certificate2(endCert.Export(X509ContentType.Pfx));
            }
        }
        private static IEnumerable<string> GetExtraDomains()
        {
            yield return Environment.MachineName;
            yield return IPAddress.Loopback.ToString();
            yield return IPAddress.IPv6Loopback.ToString();
        }

        internal const string RasPrivateKey = "RSA PRIVATE KEY";

        private static string ExportRSAPrivateKeyPem(RSA rsa)
        {
            var key = rsa.ExportRSAPrivateKey();
            var chars = PemEncoding.Write(RasPrivateKey, key);
            return new string(chars);
        }

        private static string ExportCertificatePem(X509Certificate2 x509)
        {
            var chars = PemEncoding.Write(PemLabels.X509Certificate, x509.Export(X509ContentType.Cert));
            return new string(chars);
        }

        /// <summary>
        /// 安装ca证书
        /// </summary>
        /// <exception cref="Exception">不能安装证书</exception>
        public void Install( )
        {
            var caCertFilePath = CaCerFilePath;
            try
            {
                using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
                store.Open(OpenFlags.ReadWrite);

                var caCert = new X509Certificate2(caCertFilePath);
                var subjectName = caCert.Subject[3..];
                foreach (var item in store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, false))
                {
                    if (item.Thumbprint != caCert.Thumbprint)
                    {
                        store.Remove(item);
                    }
                }
                if (store.Certificates.Find(X509FindType.FindByThumbprint, caCert.Thumbprint, true).Count == 0)
                {
                    store.Add(caCert);
                }
                store.Close();
            }
            catch (Exception ex)
            {
                throw new Exception($"请手动安装CA证书{caCertFilePath}到“将所有的证书都放入下列存储”\\“受信任的根证书颁发机构”" + ex);
            }
        }
    }
}

在 ASP.NET Core 中使用

在 ASP.NET Core 中加载服务端证书(每次启动时生成 X509 证书)。

            var sslService = new CertService();
            if(sslService.CreateCaCertIfNotExists())
            {
                try
                {
                    sslService.Install();
                }
                catch (Exception)
                {

                }
            }

           var webhost =  WebHost.CreateDefaultBuilder()
                .UseStartup<Startup>()
                .UseKestrel(serverOptions =>
                {
                    serverOptions.ListenAnyIP(39999,
                        listenOptions =>
                        {
                            var certificate = sslService.GetOrCreateServerCert("localhost");
                            listenOptions.UseHttps(certificate);
                        });
                })
                .Build();
            await webhost.RunAsync();  
一个逗逗的大学生
本文转载于网络 如有侵权请联系删除

相关文章

  • 一款跨时代的高性能 Java 框架!启动速度快到飞起

    你好呀,我是Guide!这里是JavaGuide的「优质开源项目推荐」第7期,每一期我都会精选5个高质量的Java开源项目推荐给大家。Quarkus:云原生时代高性能Java框架Quarkus(夸克斯)是RedHat在2018年开源的一款专为云原生开发设计的Java框架,与Kubernetes紧密结合,可以提高构建微服务、无服务和基于云的应用程序的开发效率。Quarkus的启动速度非常快(为GraalVM量身定制),可以很方便集成Docker和Kubernetes。目前,Quarkus项目的提交已经接近3w+,从提交看得出来维护的非常频繁。我个人还是非常看好这个项目的,绝对是云原生时代Java开发生态的一个利器。或许是因为参考资料太少,或许是国内目前还没有Quarkus比较好的生产实践案例,又或是因为很多云原生相关的应用开发用Go比较多,Quarkus这个项目目前在国内关注度一直不是很高。《Quarkus实战——专为Kubernetes而优化的Java解决方案》是国内唯一的一本Quarkus中文书籍,2021年3月份出版的。Quarkuscookbook实战Github地址:https

  • 两款 go 开发实用工具

    图片拍摄于2021年8月1日,杭州西溪。介绍推荐两款go开发中用的还行的工具。为什么推荐工具?是为了让评论区的大佬介绍其他更好用的工具,解放我的双手。顺便问问,有没有只说话就能自动打完代码的工具?JSON-To-Stuct这个工具可以把json格式的数据转换成go的struct。比如你在对接第三方的时候,就不需要根据对方的接口一个个定义struct字段。下面示例复制的微信小商店商品json数据到网站的左框即可,当然自己还是需要做一些局部的调整。其实这个功能21版的goland也支持了。在goland中你只需要这样,Table-To-Stuct被业务缠身的同学每天免不了CURD。CURD之前总得建表吧。建表之后总得在代码中定义模型吧。总不能又一个个字段定义,那么下面这个工具可能管用。假设你有一个库dream,库里有一个表category,结构如下,你只需引入包github.com/gohouse/converter,然后写这样的代码,就可以实现table-to-go功能。运行这段代码,最后会根据设置的SavePath里的地址(尚未存在的目录需要先自行创建),生成category.go文件

  • 【框架】[Spring]XML配置实现AOP拦截-切点:JdkRegexpMethodPointcut

    转载请注明出处:http://blog.csdn.net/qq_26525215 如果你把此种纯Java方式实现AOP拦截读懂了,理解本篇博客会更容易。 【框架】[Spring]纯Java的方式实现AOP切面(拦截)技术这篇讲解的是用xml配置文件来实现AOP拦截。 其实也挺简单的,无非是把一些对象通过xml文件配置new出来与初始化里面的一些值。需要的包什么的就不解释了,直接给个网址: http://repo.springsource.org/libs-release-local/org/springframework/spring/项目结构图直接上代码1、准备好原型对象:packagecn.hncu.xmlImpl; publicclassPerson{ publicvoidrun(){ System.out.println("我在run..."); } publicvoidrun(inti){ System.out.println("我在run"+i+"..."); } publicvoidsay(){ Sys

  • 产业安全专家谈丨区块链技术如何应用到政务数字化建设中?

    有人说,与“炒币”相比,“区块链+产业”宛若一股清流,作为产业助力的底层技术保障,区块链技术已经成为“时代的洪流”。距离区块链技术诞生已有10年历史,从比特币开启的数字金融区块链1.0,到以智能合约为代表的区块链2.0,如今,随着产业互联网深耕,技术作为基础设施一环,用技术+场景融合的方式,逐渐让区块链从单一场景过渡到多场景落地,进入到产业区块链3.0时代。区块链可以深度融入到传统产业中,通过融合产业升级过程中遇到的信任和自动化等问题,极大增强共享和重构等方式助力传统产业升级,重塑信任关系,提高产业效率。弥补金融和实体产业间的信息不对称,实现传统产业价值在数字世界的流转,帮助商流、信息流、资金流达到「三流合—」,进而推动传统产业数字化转型并构建产业区块链生态。腾讯安全联合云+社区打造的「产业安全专家谈」今天迎来第七期,邀请到的嘉宾是腾讯无线安全产品部副总经理、腾讯TUSI区块链创始人、物联网安全实验室负责人申子熹。拥有10年信息安全从业经验,曾参与国内外多个安全标准编写,拥有信息安全方向与区块链自主研发、实用新型专利74项,目前主要从事物联网与区块链方面的安全研究工作。Q:随着国家顶层

  • Node.js服务端开发教程 (六):依赖注入补漏篇

    最近在写前面两篇关于依赖注入的文章时,我总是在想用一句怎么的话来简单而朴素的描述依赖注入的概念,让从来没接触过的朋友能比较形象的去理解。想来想去,觉得可以站在依赖注入容器的角度说:你负责告诉我你需要什么(依赖),我负责给你送来什么(注入) 建议多读几遍上面这句话,再回头去阅读前面两篇文章,我觉得你会有更多的收获。其实在前两篇文章中,关于NestJS依赖注入功能相关的内容已经介绍的差不多了,如果你掌握了的话,已可以顺利的用于实际的开发工作。今天想给大家介绍的是一些关于依赖注入的零碎遗留内容,在日常开发中也会遇到,但不是那么高频。主要有以下几点:异步资源提供者循环依赖问题与解决方式注入范围异步资源提供者顾名思义,其实就是在资源创建的时候,存在异步的环节。比如在创建资源的时候,需要先访问一个后端API来获取一些配置信息,然后根据这些配置信息再做进一步的资源创建。这里的后端API访问就是一个异步的动作,这会导致整个资源创建流程也是异步的了。在NestJS中,大多数的资源提供者都是只支持同步,比如ValueProvider和ClassProvider,能支持异步的只有FactoryProvide

  • 你的编程能力从什么时候开始突飞猛进?

    1、学习一门新的编程语言的时候我的第一份工作是Android攻城师,做了有两年左右,发现自己平时的工作几乎都是在设计页面布局,这不是我理想的工作(我的理想工作是不上班/坏笑)。因为经常和Java后端人员接触,感觉他们工作挺炫酷的,敲几行代码,在浏览器输入一个地址,就能打开一个自己设计的网页,简直太牛P了。因此决定向Java后端发展。之后又有爬取数据的需求,有利用业余时间学习了Python。每当接触一门新的编程语言时,都感觉自己进步飞快。学习Java后端:刚接触时,真的是什么都不懂,首先是三大框架SSH(Struts、Spring、Hibernate),知道的同学请举个手,现在几乎都是SSM框架的天下了(Spring、SpringMVC,MyBatis)。一路走来搞懂了什么是JSP?什么是Servlet?什么是拦截器/过滤器?什么是Socket通信?什么是数据库操作?什么是事务?什么是分布式?什么是分布式事务?.......终于明白了,什么叫“一入编程深似海”学习Python:(人生苦短,我用Python)学习这门语言的最初动力就是做爬虫,直接在w3school和菜鸟教程简单学习了语法,

  • 微服务之初了解(一)

    一.什么是微服务微服务就是一些协同工作的小而自治的服务。1.服务要足够小在使用微服务的时候,内聚性是一个很重要的概念。RobertC.Martion对单一职责原则有个论述是:把相同原因而变化的东西聚合到一起,而把不同原因而变化的东西分离开。这个论述很好的强调了内聚性这个概念。那么服务要多小才算足够小呢?要考虑这些因素:服务越小,微服务架构的优点和缺点也就越明显。使用的服务越小,独立性带来的好处就越多。但是管理大量服务也会越复杂,之后会将这一复杂性。2.自治性一个微服务就是一个独立的实体。它可以独立的部署在PAAS(PlatformAsAService,平台即服务)上,也可以作为一个操作系统进程存在。我们要尽量避免把多个服务部署到一台机器上。服务之间通过网络调用进行通信,从而加强服务之间的隔离性,避免紧耦合。这些服务应该可以彼此间独立的进行修改,并且某个服务的部署不应该引起该服务消费方的变动。服务会暴露出API(ApplicationProgrammingInterface,应用编程接口),然后服务之间通过这些API进行通信。API的实现技术应该避免与消费方耦合,这就意味着应该选择与具体

  • Hadoop学习笔记—19.Flume框架学习

    START:Flume是Cloudera提供的一个高可用的、高可靠的开源分布式海量日志收集系统,日志数据可以经过Flume流向需要存储终端目的地。这里的日志是一个统称,泛指文件、操作记录等许多数据。一、Flume基础理论1.1常见的分布式日志收集系统  Scribe是facebook开源的日志收集系统,在facebook内部已经得到大量的应用。 Chukwa是一个开源的用于监控大型分布式系统的数据收集系统。这是构建在hadoop的hdfs和map/reduce框架之上的,继承了hadoop的可伸缩性和鲁棒性。而 Flume 是一个分布式、可靠、和高可用的海量日志采集、聚合和传输的系统。支持在日志系统中定制各类数据发送方,用于收集数据;同时,Flume提供对数据进行简单处理,并写到各种数据接受方(比如文本、HDFS、Hbase等)的能力。1.2Flume的数据流模型  Flume的核心是把数据从数据源收集过来,再送到目的地。为了保证输送一定成功,在送到目的地之前,会先缓存数据,待数据真正到达目的地后,删除自己缓存的数据。  Flume传输的数据的基本单位是Event,如果是文本文件,通常

  • Spark作业基本运行原理解析!

    1、基本原理Spark作业的运行基本原理如下图所示:我们使用spark-submit提交一个Spark作业之后,这个作业就会启动一个对应的Driver进程。提交作业的节点称为Master节点,Driver进程就是开始执行你Spark程序的那个Main函数(Driver进程不一定在Master节点上)。根据你使用的部署模式(deploy-mode)不同,Driver进程可能在本地启动,也可能在集群中某个工作节点上启动。Driver进程本身会根据我们设置的参数,占有一定数量的内存和CPUcore。而Driver进程要做的第一件事情,就是向集群管理器申请运行Spark作业需要使用的资源,这里的资源指的就是Executor进程。YARN集群管理器会根据我们为Spark作业设置的资源参数,在各个工作节点Worker上,启动一定数量的Executor进程,每个Executor进程都占有一定数量的内存和CPUcore。在申请到了作业执行所需的资源之后,Driver进程就会开始调度和执行我们编写的作业代码了。Driver进程会将我们编写的Spark作业代码分拆为多个stage,每个stage执行一部分

  • intel携手睿悦发布全球首款双系统VR一体机

    于2015年12月30日上午,由深圳赛格股份有限公司及镁客网协办的——“英特尔睿悦新品发布会”在南京珠江路隆重举行。一家来自南京本土的创业公司——睿悦信息与享誉全球的英特尔来了一次亲密接触。 今天,“英特尔睿悦新品发布会”的主角是采用了intelCHTT3最新处理器的虚拟现实一体机,近年来,英特尔在虚拟现实领域的动作频频,此次携手睿悦信息也是想在基于双系统的虚拟现实一体机做个试水,此次合作无疑也是睿悦信息在VR生态上的又一重要布局。 睿悦信息自2014年虚拟现实热潮席卷国内之际,基于其自研的Nibiru交互SDK而推出的NibiruVR系统在国内虚拟现实一体机市场占据了领先地位。而本次与英特尔合作产品的发布意味着睿悦信息在产品生态的完善上付出着更多的努力。 英特尔与睿悦信息在底层架构与图形处理技术上达成深度合作,并基于微软windows10操作系统进行了高度的定制与优化,相信这款产品将在应用生态上拥有更优秀的体验,因为这将是全球首款同时兼容windows10与android双系统的虚拟现实一体机。在“英特尔睿悦新品发布会”现场,intel战略发展部门负责人、中国软件谷领导以及深圳赛格股

  • 从星际2深度学习环境到神经机器翻译,上手机器学习这些开源项目必不可少

    本文主要编译于Github文章「Gettingstartedwithmachinelearning」(原文链接:https://github.com/collections/machine-learning)。同时,AI研习社在原文的基础上补充了部分开源项目,为AI开发者提供更加详细的AI项目和资源。机器学习是用数据来学习、概括、预测的研究。近几年,随着数据的开发、算法的改进以及硬件计算能力的提升,机器学习技术得以快速发展,不断延伸至新的领域。从模式识别到电子游戏,开发者们通过训练AI算法实现了各种各样好玩的应用:Richard-An/Wechat_AutoJumpGitHub地址:https://github.com/Richard-An/Wechat_AutoJumpAI玩微信跳一跳的正确姿势。SethBling/MarI/O源码地址:https://pastebin.com/ZZmSNaHX一段用神经网络和遗传算法写的程序,可以玩「超级马里奥世界」。lllyasviel/style2paintsGitHub地址:https://github.com/lllyasviel/styl

  • Golang语言社区--标准库分析之strconv包

    大家好,我是Golang语言社区主编彬哥,这篇是给大家转载的关于Go语言的strconv包相关的知识。strconv包中的函数和方法//atob.go------------------------------------------------------------//ParseBool将字符串转换为布尔值//它接受真值:1,t,T,TRUE,true,True//它接受假值:0,f,F,FALSE,false,False.//其它任何值都返回一个错误funcParseBool(strstring)(valuebool,errerror)packagemain import( "fmt" "strconv" ) funcmain(){ fmt.Println(strconv.ParseBool("1"))//true fmt.Println(strconv.ParseBool("t"))//true fmt.Println(strconv.ParseBool("T"))

  • CLR异常处理的流程

    因为微软的很多东西都没开源,比如Windows系统部分,虽然CLR开源了,但是作为微软的东西跟windows千丝万缕的联系。此处整理了下CLR异常处理部分的内容:通过RyuJit生成的IL_Throw函数调用RaiseException。后面的流程如下: RaiseException-》__imp_RaiseException-》__imp_RtlRaiseException-》RtlLookupFunctionEntry-》RtlpxLookupFunctionTableRaiseException-》__imp_RaiseException-》__imp_RtlRaiseException-》RtlVirtualUnwindRaiseException-》__imp_RaiseException-》__imp_RtlRaiseException-》RtlpGuardSynchronizeRestorePcRaiseException-》__imp_RaiseException-》__imp_RtlRaiseException-》NtRaiseException我们可以看到最后有个N

  • 比较推荐学习Linux系统应该看的书籍

    对于如何学习Linux,我想大家多多少少会有自己的一些想法--不管是学过Linux的还是没有学过Linux的。学习,对于我们来说,应该不是一件陌生的事:从小学开始,然后中学、大学、乃至于读硕读博,可以说,我们人生中最有活力、最美好的一面,大多是在学校中度过的。 不管是学习语文、英语,还是学习数学、物理,不管你采用何种方法,都离不开三个阶段:记忆、掌握、运用。也许我描述的并不十分准确,但大体上是这样子的。以此类推,对于如何学习Linux,也是可以参照上述三个阶段来学习的。 什么是记忆?我的理解是记住并且能够回忆起来。想想我们在学校里最初是如何学习语文的?在我的印象里,最开始的时候,是我们的班主任在讲台上一笔一划的写一个汉字,然后让我们在本子上照着描写,最痛苦的是听写的时候,有时候听到老师报出来的词语,明明感觉很熟悉,可就是想不起来如何去写...最后的结果可想而知,借用前几年网络上有名的一句话来说就是“一百遍啊一百遍”。对于数学、物理等课程而言,虽然不用抄,但是基本的公理、定理,你是一定要记住的,这是第一步,无法跳过,只有记住了,你才能接下去去解答题目,否则,一切都是空谈。  

  • 米哈游2023秋季招聘正式开始~提前批有机会免笔试!

    米哈游2023秋季招聘正式开始~ [灯泡]简历投递:7.4-10.31 提前批:7.22号之前投递部分岗位有机会免笔试 笔试:7月-10月 面试:7月中旬开始 [灯泡]成功密码 内推码拿好,冲! 校招唯一专属内推码EYTUC。 校招岗位链接:https://taou.cn/2piRI 选择好岗位直接使用内推码投递即可。 [灯泡]福利待遇 业界极具竞争力的薪酬(普遍16薪、六险2金) 年度旅游、体检(含家属) 每年至少10天年假、3天带薪病假 平等、扁平、有爱的工作氛围 不限零食饮料、节日礼物等 天下没有免费的晚餐?米哈游就有! 无加班文化,弹性不打卡(10-7-5) 有问题欢迎评论和私信~ 欢迎加我微信john_josh沟通内推事宜 微信公众号【程序员黄小斜】作者是前蚂蚁金服Java工程师,专注分享Java技术干货和求职成长心得,不限于BAT面试,算法、计算机基础、数据库、分布式、spring全家桶、微服务、高并发、JVM、Docker容器,ELK、大数据等。关注后回复【book】领取精选20本Java面试必备精品电子书。 ​

  • 第一周作业

    1.标识符命名规范 1.1概述 标识符的命名力求做到统一、达意和简洁。 1.1.1统一 统一是指,对于同一个概念,在程序中用同一种表示方法,比如对于供应商,既可以用supplier,也可以用provider,但是我们只能选定一个使用,至少在一个Java项目中保持统一。统一是作为重要的,如果对同一概念有不同的表示方法,会使代码混乱难以理解。即使不能取得好的名称,但是只要统一,阅读起来也不会太困难,因为阅读者只要理解一次。 1.1.2达意 达意是指,标识符能准确的表达出它所代表的意义,比如:newSupplier,OrderPaymentGatewayService等;而supplier1,service2,idtts等则不是好的命名方式。准确有两成含义,一是正确,而是丰富。如果给一个代表供应商的变量起名是order,显然没有正确表达。同样的,supplier1,远没有targetSupplier意义丰富。 1.1.3简洁 简洁是指,在统一和达意的前提下,用尽量少的标识符。如果不能达意,宁愿不要简洁。比如:theOrderNameOfTheTargetSupplierWhichIsTran

  • 大马猴队—— 团队项目

    码云:https://gitee.com/hzkkk/wx 队名 大马猴 队员 黄梓垲3116005138      风格:刺激战场钢枪王中王      擅长的技术:java,98K,m4,awm      编程兴趣:JavaWeb后台开发      希望的软工角色:开发      一句话宣言:多吃菜,少喝汤 黄钰城3116005137      风格:小谷围高学成      擅长的技术:phppythonADCarry      编程兴趣:python运维      希望的软工角色:开发     

  • Javascript单元测试框架Jasmine(结合karma使用)

      Jasmine是用来写javascript单元测试的框架 1.官网 下载最新版本,我当前使用jasmine-standalone-2.0.0 2.解压后,打开根目录下的 SpecRunner.html 其中sourcefiles就是你要测试的js,specfiles就是单元测试的JS,分别替换成对应你项目的JS   4.示例中我创建了一个被测试的对象app.js,包含一段简单的javascript   functiongiveMeFive(){ return5; }复制 再创建一个app.test.js用来对giveMeFive()进行单元测试, 其中describe方法就是由jasmine框架提供,这段单元测试期望giveMeFive()方法返回5 describe('giveMeFiveTest',function(){ it('shouldreturn5',function(){ expect(giveMeFive()).toEqual(5); }); });复制 示例中我把source和s

  • sql语句-第一章-DDL(Data Definition language)-

    (!!!!文中db1、db2等为数据库名,为了简化文本,,gbk是字符集,**************在sql语句中的关键字推荐大家大写,但是在sqlyog中会自动将关键字转换为大写,后面不想码大写字母了,所以一律写成了小写。)     第一节:DDL操作数据库 1.1.1 创建数据库几种方式 --创建数据库:CREATEDATABASEdb1; --判断数据库是否存在,不存在则创建:CREATEDATABASEIFNOTEXISTSdb1; --创建数据库指定字符集:CREATEDATABASEdb1CHARACTRSETgbk; 1.1.2 查看数据库 --查看所有数据库:SHOWDATABASE; --查看指定数据库:SHOWCREATEDATABASEdb1; 1.1.3修改数据库 --修改字符集:ALTERDATABASEdb1CHARACTERSETutf8; 1.1.4删除数据库 --删除:DROPDATABASEdb1; 1.1.5使用数据库 --查看正在使用的数据库:SELECTDATABASE(); --使用数据库/切换数据库

  • 句柄,文件描述符的理解

    有些是转载的。讲的应该很清楚了 什么是句柄 句柄就是一个标识符,只要获得对象的句柄,我们就可以对对象进行任意的操作。 句柄不是指针,操作系统用句柄可以找到一块内存,这个句柄可能是标识符,map的key,也可能是指针,看操作系统怎么处理的了。fd算是在某种程度上替代句柄吧;Linux有相应机制,但没有统一的句柄类型,各种类型的系统资源由各自的类型来标识,由各自的接口操作。 在操作系统层面上,文件操作也有类似于FILE的一个概念,在Linux里,这叫做文件描述符fd(FileDescriptor),而在Windows里,叫做句柄(Handle)(以下在没有歧义的时候统称为句柄)。用户通过某个函数打开文件以获得句柄,此后用户操纵文件皆通过该句柄进行。 粗暴的解释 windowns中是handle,liunx类似的是fd,最早的windows开发书籍,handle是被翻译成“把手”的。虽然不好听,但是个人认为相当传神。 虽然你握住的只是把手,却能拉动整扇门,而且你根本不用在意那门长什么样子 一扇门如果有多个把手,被不同的人(进程)握住,门往哪儿走就不好说了 设计这么一个句柄的原因在于句

  • vuejs入门小demo-搜索大全

    这个demo非常适合入门的同学,不再是简单的todolist。用到的知识点有组件通信,过渡效果,vue-rsource,还有一些基本的vue指令。   先放一张截图:   是不是感觉高端大气上档次呢,演示地址   如何运行: 将项目下载到本地   安装git 安装nodejs 配置node环境变量(百度一下) 命令行进入本案例目录 输入命令npminstall(最好使用cnpm淘宝镜像) 输入命令npmrundev   如果感觉本demo对你有用的话,就给棵星星吧github项目地址链接 爱写代码的孩子运气不会太差。 github:http://github.com/lavyun 新浪微博:http://weibo.com/u/5921186675 个人网站:http://lavyun.cn

相关推荐

推荐阅读