背景

在之前《温故知新,.Net Core遇见Blazor(FluentUI),属于未来的SPA框架》中我们已经初步了解了Blazor的相关概念,并且根据官方的指引完成了《创建我的第一个Blazor应用》《生成Blazor待办事项列表应用》《结合ASP.NET Core SignalR和Blazor实现聊天室应用》三个基础应用的实践探索,接下来我们继续探索如果通过Blazor的相关技术来完成一个独立的SPA应用。

什么是大写人民币翻译机(ChineseYuanParser)

大写人民币翻译机(ChineseYuanParser),是一款结合BlazorWebAssembly技术联合打造并且运行在.Net 5.0运行时的数字金额转大写人民币金额的应用,适用于差旅报销时填写报销单需要将阿拉伯数字报销金额翻译成大写人民币金额的场景。

https://github.com/CraigTaylor/ChineseYuanParser

借鉴和引用

该项目为一个演示项目,旨在实践和练习Blazor技术,其原版来自阿迪的RMBCapitalization-Blazor和技术文章《Blazor WASM 实现人民币大写转换器》

运行效果

基于Azure静态网站应用服务(Azure Static Web Apps) 免费预览实现的一个临时发布:https://rmbcc.ledesign.org, 随时可能因为订阅原因失效。

如果想了解Azure静态网站应用服务(Azure Static Web Apps),可以查看另外一个文章:尝鲜一试,Azure静态网站应用服务(Azure Static Web Apps) 免费预览,协同Github自动发布静态SPA

开始创作

接下来,我将一步步拆解实现该应用的细节,这个过程中,我们也可以收获很多关于BlazorWebAssembly.Net 6.0JavascriptBootstrap的知识点。

前置清单

创建名为"ChineseYuanParser"的Blazor WebAssembly应用

  1. dotnet new blazorwasm -o ChineseYuanParser --no-https

或者

  1. dotnet new blazorwasm -o ChineseYuanParser --no-https --pwa

选择好你要存档项目的根目录,然后在这个目录中右键打开Windows Terminal进入,通过DotNet-Cli命令new来创建一个模板类型为blazorwasm、输出名为ChineseYuanParser的应用,中文译为大写人民币翻译机,并且我们标记不需要强制https,还是添加--pwa来创建符合PWA规则的模板。

创建成功后,在终端中,可直接走命令打开Visual Studio Code加载当前项目

  1. cd ChineseYuanParser
  1. code .

接下来,我们找到wwwroot\index.html,修改主页面的Title,为中文的大写人民币翻译机

我们走通过DotNet-Cli命令run看下初始的效果,这里加一个watch参数,可以做到修改后自动热重载。

  1. dotnet watch run

适当剪裁,搭建应用的基本面板

根据模板直接创建的项目会自带一些页面,这对我们要做的实际应用来说,其实是多余的,我们先来一波减法。

1. 删掉Pages目录中除Index以外的页面,我们只需要保留一个Index.razor即可。

2. 删掉Index.razor中除路由路径以外的信息,其他的都需要我们重新来过。

3. 在Index.razor的同级目录新建一个空白的Index.razor.css样式文件,根据命名规则,它会作用于Index页面,这为后续给这个页面增加定制化的样式提供一个基础。

4. 删掉Shared目录中除MainLayout以外的组件,我们只需要保留一个MainLayout.razor即可,这里多说一句,默认模板带的左侧导航就是存在于NavMenu.razor中,而对MainLayout.razor的引用其实是在App.razor中。

5. 删除MainLayout.razor中自带的html内容,添加我们应用需要的,这里因为内置Bootstrap的需要,基于Gird网格原理,需要将网格内容放在一个.container class内,以便获得对齐和内边距支持,所以这里我们在@Body的父级放一个带container classDiv元素。

Bootstrap提供了一套响应式、移动设备优先的流式网格系统,随着屏幕或视口(viewport)尺寸的增加,系统会自动分为最多12列。Bootstrap包含了一个响应式的、移动设备优先的、不固定的网格系统,可以随着设备或视口大小的增加而适当地扩展到12列。它包含了用于简单的布局选项的预定义类,也包含了用于生成更多语义布局的功能强大的混合类。Bootstrap网格系统(GridSystem)的工作原理:行必须放置在.container class内,以便获得适当的对齐(alignment)和内边距(padding)。

  1. @inherits LayoutComponentBase
  2. <div class="container">
  3. @Body
  4. </div>

6. 删除wwwroot下的sample-data演示数据,这是模板创建自带的,已经完全没用了。

7. 移除wwwroot\css\app.css中的多余自带样式,只保留blazor-error-ui相关的部分,其余的都可以删掉了,同时添加我们需要的样式效果,这里添加的样式主要是为了打造一个灰色背景,应用区域为白色悬浮的效果。

  1. :root {
  2. --transparent-dark-1: rgba(0,0,0,.108);
  3. --transparent-dark-2: rgba(0,0,0,.125);
  4. --transparent-dark-3: rgba(0,0,0,.132);
  5. --transparent-dark-4: rgba(0,0,0,.175);
  6. --gray-1: #f2f2f2;
  7. --gray-2: #eee;
  8. }
  9. body {
  10. background: #F2F2F2;
  11. }
  12. .box {
  13. background-color: white;
  14. padding: 1.8rem;
  15. box-shadow: 0 1.6px 3.6px 0 var(--transparent-dark-3),0 .3px .9px 0 var(--transparent-dark-1);
  16. border-radius: 3px;
  17. }

8. 在Pages\Index.razor中,添加我们承载应用内容的主体骨架,它有一个点睛之笔,也就是标题,把我们的应用名字突出来,接着我们把我们基于的技术栈表达出来,这里用到了一个@Environment.Version用来读取当前.Net Core的版本号信息,在尾部,我们打上自己的作者链接和名称。

对于.NETCore 2.x.NET5+Environment.Version属性返回.net运行时版本号。

  1. @page "/"
  2. <div class="main box mt-4">
  3. <h1 class="text-center">
  4. 大写人民币翻译机
  5. </h1>
  6. <div class="text-center">
  7. <small>Blazor WASM By .Net @Environment.Version</small>
  8. </div>
  9. <hr />
  10. <div>
  11. </div>
  12. </div>
  13. <div class="mt-3 text-center author">
  14. <a href="https://www.cnblogs.com/taylorshi" target="_blank">Taylor Shi</a>
  15. </div>

9. 查看基础效果,一个应用的基本底子就出来,这就像女孩子化妆一样,我们先要打个好底子。

按需组合,搭建应用的功能面板

1. 构建翻译机左侧顶部翻译结果和按键功能区面板。

在前面的div块上添加class="row",然后基于我们的视觉规划效果,构建左侧顶部翻译结果和按键功能区。

  1. <!-- 左侧功能区 -->
  2. <div class="col-md-8">
  3. <!-- 展示及动作 -->
  4. <section>
  5. <!-- 转换结果展示 -->
  6. <div class="cap-result border bg-light mb-2 p-3">
  7. <h3>
  8. @ParseResult
  9. </h3>
  10. </div>
  11. <!-- 数字金额输入框 -->
  12. <div class="row">
  13. <div class="col-md-8" style="margin-bottom: 10px;">
  14. <input type="text" class="form-control" placeholder="请输入数字金额" @bind-value="DigitalAmount" @bind-value:event="oninput" />
  15. </div>
  16. <div class="col-md-4" style="margin-bottom: 10px;">
  17. <div class="row">
  18. <button class="col-md-3 btn btn-success myButton" @onclick="CopyResult" >复制</button>
  19. <button class="col-md-3 btn btn-primary myButton" @onclick="ReadResult" >朗读</button>
  20. <button class="col-md-3 btn btn-danger myButton" @onclick="ClearResult" >清除</button>
  21. </div>
  22. </div>
  23. </div>
  24. </section>
  25. </div>
  1. .myButton {
  2. margin-left: 10px;
  3. max-height: 38px;
  4. min-width: 79px;
  5. max-width: 147px;
  6. }

在转换结果展示区域,我们直接展示ParseResult结果,还拥有一个输入框,输入框绑定了变量DigitalAmount,这里有个技巧是,因为输入框输入内容我们需要及时的做出翻译结果,所以我们需要一个监听输入的方式,这里采用了@bind-value:event="oninput"来做,同时我们设计了三个按钮在输入框旁边,分别是:复制按钮,绑定事件CopyResult、朗读按钮,绑定事件ReadResult、清除按钮,绑定事件ClearResult

2. 响应左侧顶部实时翻译和翻译结果展示的实现。

先看最简单的解析结果。

  1. /// <summary>
  2. /// 解析结果
  3. /// </summary>
  4. /// <value></value>
  5. public string ParseResult { get; set; }

接下来,看DigitalAmount的实现,这是整个应用功能的关键,因为我们希望在它值变更的时候,马上得出翻译结果,那么这里主要是采用它的set方法来实现。

  1. /// <summary>
  2. /// 数字金额
  3. /// </summary>
  4. private string _digitalAmount;
  5. public string DigitalAmount
  6. {
  7. get => _digitalAmount;
  8. set
  9. {
  10. _digitalAmount = value;
  11. // 输入值为空不做处理
  12. if(!string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value))
  13. {
  14. // 最大值:99999999999.99
  15. if(!Equals(value, ".") && double.Parse(value) > 99999999999.99)
  16. {
  17. return;
  18. }
  19. // 小数点后最多两位
  20. if(value.Contains("."))
  21. {
  22. // 如果只有一个点,特殊处理成0.
  23. if(!Equals(value, "."))
  24. {
  25. // 按.拆分,且只允许存在一个.
  26. var spiltValues = value.Split('.');
  27. if(spiltValues.Length != 2)
  28. {
  29. return;
  30. }
  31. // 小数部分长度不能超过2位
  32. var decimalValue = spiltValues.LastOrDefault();
  33. if(!string.IsNullOrEmpty(decimalValue) && decimalValue.Length > 2)
  34. {
  35. return;
  36. }
  37. }
  38. else
  39. {
  40. value = "0.";
  41. _digitalAmount = value;
  42. }
  43. }
  44. // 将01234 格式化成1234
  45. if(value.StartsWith("0") && !value.Contains("."))
  46. {
  47. var intValue = int.Parse(value);
  48. value = intValue.ToString();
  49. _digitalAmount = value;
  50. }
  51. // 转换
  52. ParseResult = RMBConverter.RMBToCap(_digitalAmount);
  53. }
  54. else
  55. {
  56. ParseResult = string.Empty;
  57. }
  58. }
  59. }

DigitalAmount的set实现里面,我们先规避了空值和最大值,然后我们处理了小数位最多支持2位小数,接下来我们将首位是0的情况做了处理。

最终,才迎来关键的一个动作,也就是ParseResult = RMBConverter.RMBToCap(_digitalAmount)代码,说白了,就是我们把拿到的金额丢进这个方法中,最终翻译得到我们要的大写人民币值,这也就是整个翻译机核心功能。

关于RMBConverter.RMBToCap的实现,这个不用多说,都是借鉴了网上已经很成熟的代码实现,所以就直接贴代码了。

  1. public class RMBConverter
  2. {
  3. public static string RMBToCap(string input)
  4. {
  5. // Constants:
  6. var MAXIMUM_NUMBER = 99999999999.99;
  7. // Predefine the radix characters and currency symbols for output:
  8. var CN_ZERO = "零";
  9. var CN_ONE = "壹";
  10. var CN_TWO = "贰";
  11. var CN_THREE = "叁";
  12. var CN_FOUR = "肆";
  13. var CN_FIVE = "伍";
  14. var CN_SIX = "陆";
  15. var CN_SEVEN = "柒";
  16. var CN_EIGHT = "捌";
  17. var CN_NINE = "玖";
  18. var CN_TEN = "拾";
  19. var CN_HUNDRED = "佰";
  20. var CN_THOUSAND = "仟";
  21. var CN_TEN_THOUSAND = "万";
  22. var CN_HUNDRED_MILLION = "亿";
  23. var CN_SYMBOL = "人民币";
  24. var CN_DOLLAR = "元";
  25. var CN_TEN_CENT = "角";
  26. var CN_CENT = "分";
  27. var CN_INTEGER = "整";
  28. if (double.Parse(input) > MAXIMUM_NUMBER)
  29. {
  30. throw new ArgumentOutOfRangeException(nameof(input), "金额必须小于一百亿元");
  31. }
  32. string integral;
  33. string decimalPart;
  34. var parts = input.Split('.');
  35. if (parts.Length > 1)
  36. {
  37. integral = parts[0];
  38. decimalPart = parts[1];
  39. if (decimalPart == string.Empty)
  40. {
  41. decimalPart = "00";
  42. }
  43. if (decimalPart.Length == 1)
  44. {
  45. decimalPart += "0";
  46. }
  47. // Cut down redundant decimal digits that are after the second.
  48. decimalPart = decimalPart.Substring(0, 2);
  49. }
  50. else
  51. {
  52. integral = parts[0];
  53. decimalPart = string.Empty;
  54. }
  55. // Prepare the characters corresponding to the digits:
  56. var digits = new[] { CN_ZERO, CN_ONE, CN_TWO, CN_THREE, CN_FOUR, CN_FIVE, CN_SIX, CN_SEVEN, CN_EIGHT, CN_NINE };
  57. var radices = new[] { "", CN_TEN, CN_HUNDRED, CN_THOUSAND };
  58. var bigRadices = new[] { "", CN_TEN_THOUSAND, CN_HUNDRED_MILLION };
  59. var decimals = new[] { CN_TEN_CENT, CN_CENT };
  60. string outputCharacters = string.Empty;
  61. if (long.Parse(integral) > 0)
  62. {
  63. var zeroCount = 0;
  64. for (int i = 0; i < integral.Length; i++)
  65. {
  66. var p = integral.Length - i - 1;
  67. var d = integral.Substring(i, 1);
  68. var quotient = p / 4;
  69. var modulus = p % 4;
  70. if (d == "0")
  71. {
  72. zeroCount++;
  73. }
  74. else
  75. {
  76. if (zeroCount > 0)
  77. {
  78. outputCharacters += digits[0];
  79. }
  80. zeroCount = 0;
  81. outputCharacters += digits[int.Parse(d)] + radices[modulus];
  82. }
  83. if (modulus == 0 && zeroCount < 4)
  84. {
  85. outputCharacters += bigRadices[quotient];
  86. zeroCount = 0;
  87. }
  88. }
  89. outputCharacters += CN_DOLLAR;
  90. }
  91. // Process decimal part if there is:
  92. if (decimalPart != string.Empty)
  93. {
  94. for (int i = 0; i < decimalPart.Length; i++)
  95. {
  96. var d = decimalPart.Substring(i, 1);
  97. if (d != "0")
  98. {
  99. outputCharacters += digits[int.Parse(d)] + decimals[i];
  100. }
  101. }
  102. }
  103. // Confirm and return the final output string:
  104. if (outputCharacters == string.Empty)
  105. {
  106. outputCharacters = CN_ZERO + CN_DOLLAR;
  107. }
  108. if (decimalPart == string.Empty)
  109. {
  110. outputCharacters += CN_INTEGER;
  111. }
  112. return outputCharacters;
  113. }
  114. }

3. 响应左侧顶部三个功能按键:复制、朗读、清除。

先说最简单的清除吧,这个很简单,清除就是直接清空DigitalAmount的值就行了,因为我们在DigitalAmount的set里面也设计了,如果set为空,那么我们也会把ParseResult设置为空,所以这样就达到了双清的目的。

  1. /// <summary>
  2. /// 清除结果
  3. /// </summary>
  4. private void ClearResult()
  5. {
  6. DigitalAmount = string.Empty;
  7. }

接下来,复制和朗读功能就比较特殊了,要知道这两个工作,毕竟我们现在是在wasm的里面,也就是应用实际上是跑在浏览器了,所以我们需要借助JS原生的支持来完成。

我们需要在Index.razor的顶部添加@inject IJSRuntime JavaScriptRuntime来引入从Blazor对原生Js的调用。

然后我们将朗读和复制的JS写在首页的Index.html中,这里借助Clipboard.writeText方法来实现对复制功能的实现,借助speechSynthesis.speak方法来实现对指定文本的朗读功能。

  1. <script>
  2. window.clipboardCopy = {
  3. copyText: function (text) {
  4. navigator.clipboard.writeText(text).then(function () {
  5. console.log(text);
  6. })
  7. .catch(function (error) {
  8. alert(error);
  9. });
  10. }
  11. };
  12. window.readAloud = {
  13. readText: function (text) {
  14. let utterance = new SpeechSynthesisUtterance(text);
  15. utterance.lang = 'zh-CN';
  16. speechSynthesis.speak(utterance);
  17. }
  18. }
  19. </script>

完成Index.html中的JS对应函数支持后,我们回到Index.razor来通过调用JS来实现对复制和朗读功能的支持,这里用到JavaScriptRuntime.InvokeVoidAsync的方式来调用JS方法。

  1. /// <summary>
  2. /// 复制结果
  3. /// </summary>
  4. private async Task CopyResult()
  5. {
  6. if (!string.IsNullOrEmpty(ParseResult))
  7. {
  8. await JavaScriptRuntime.InvokeVoidAsync("clipboardCopy.copyText", ParseResult);
  9. }
  10. }
  11. /// <summary>
  12. /// 朗读结果
  13. /// </summary>
  14. private async Task ReadResult()
  15. {
  16. if (!string.IsNullOrEmpty(ParseResult))
  17. {
  18. await JavaScriptRuntime.InvokeVoidAsync("readAloud.readText", ParseResult);
  19. }
  20. }

4. 构建翻译机左侧快捷数字输入面板

先贴代码再解读,这里主要是界面处理的逻辑。

  1. <!-- 快捷键 -->
  2. <section>
  3. <!-- 快捷键1-9 -->
  4. <div class="row">
  5. @for (var i = 1; i <= 9; i++)
  6. {
  7. var num = i;
  8. <div class="col-4">
  9. <button class="btn btn-light border key" @onclick="() => ShortCutInvoked(num.ToString())">@num</button>
  10. </div>
  11. }
  12. </div>
  13. <!-- 快捷键0和. -->
  14. <div class="row">
  15. <div class="col-8">
  16. <button class="btn btn-light border key" @onclick='() => ShortCutInvoked("0")'>0</button>
  17. </div>
  18. <div class="col-4">
  19. <button class="btn btn-light border key" @onclick='() => ShortCutInvoked(".")'>.</button>
  20. </div>
  21. </div>
  22. </section>

针对数字1-9,我们很好处理,构建一个Gird网格结构就行,每行支持3个按钮,这里我们需要用到for的写法,需要注意的就是var num = i;推荐这个写法。

我们在所有数字按键中统一绑定@onclick="() => ShortCutInvoked(num.ToString())",来支撑按键动作,其实逻辑也很简单,就是把新输入的数值,追加到原来的字符串后面就行了。

  1. /// <summary>
  2. /// 快捷键触发事件
  3. /// </summary>
  4. /// <param name="num"></param>
  5. private void ShortCutInvoked(string num)
  6. {
  7. DigitalAmount += num;
  8. }
  1. .key {
  2. font-family: "Consolas";
  3. font-size: 250%;
  4. width: 100%;
  5. min-height: 90px;
  6. margin-bottom: 10px;
  7. }

针对数字0.按键需要特殊处理,因为这里只有两个元素了,所以采用2:1的比例来分配Gird的空间。

5. 构建翻译机右侧翻译参考表面板

在翻译机中,为了更加直观的表达每一个数字会被翻译成什么样的大写人民币,这里我们在整个界面的右侧放一个参考表。

这个看起来不难,首先我们需要准备一个字典ReferList

  1. /// <summary>
  2. /// 参照列表
  3. /// </summary>
  4. /// <value></value>
  5. public Dictionary<string, string> ReferList { get; set; } = new Dictionary<string, string>
  6. {
  7. {
  8. "零","0"
  9. },
  10. {
  11. "壹","1"
  12. },
  13. {
  14. "贰","2"
  15. },
  16. {
  17. "叁","3"
  18. },
  19. {
  20. "肆","4"
  21. },
  22. {
  23. "伍","5"
  24. },
  25. {
  26. "陆","6"
  27. },
  28. {
  29. "柒","7"
  30. },
  31. {
  32. "捌","8"
  33. },
  34. {
  35. "玖","9"
  36. },
  37. {
  38. "拾","10"
  39. },
  40. {
  41. "佰","百"
  42. },
  43. {
  44. "仟","千"
  45. },
  46. {
  47. "万","万"
  48. },
  49. {
  50. "亿","亿"
  51. },
  52. };

然后我们遍历这个字典,来构建我们的参考表,这里我们可以用foreach来遍历ReferList,以便可以取到它的keyvalue

  1. <!-- 右侧参照表 -->
  2. <div class="col-md-4">
  3. <div class="card">
  4. <div class="card-header">
  5. 参照表
  6. </div>
  7. <div class="card-body">
  8. <!-- 快捷键1-9 -->
  9. <div class="row">
  10. @foreach (var item in ReferList)
  11. {
  12. <div class="col-4">
  13. <button class="btn btn-light border refer">
  14. <div class="text-bottom">
  15. @item.Key<small>(@item.Value)</small>
  16. </div>
  17. </button>
  18. </div>
  19. }
  20. </div>
  21. </div>
  22. </div>
  23. </div>
  1. .refer {
  2. font-family: "Consolas";
  3. font-size: 150%;
  4. width: 100%;
  5. min-height: 80px;
  6. min-width: 60px;
  7. margin-bottom: 10px;
  8. }

其实到这里呢,我们的主要是任务已经完成了,完成了整个左侧和右侧功能区的实现,啦啦啦。

看下效果吧。

定制启动页

Blazor的启动页默认是个很简单的Loading字样,这要是应用写大了或者用户的网络慢一点,就真的挺难看的,好在我们其实可以也很容易在wwwroot\index.html找到它,并且对它完成自己的定制,当然效果这东西取决于你的审美和前端技术功底了。

  1. <body>
  2. <div id="app">Loading...</div>
  3. <div id="blazor-error-ui">
  4. An unhandled error has occurred.
  5. <a href="" class="reload">Reload</a>
  6. <a class="dismiss"></a>
  7. </div>
  8. <script src="_framework/blazor.webassembly.js"></script>
  9. </body>

如果你需要定制,你只需要替换掉divLoading...这个部分就行了,我从github搜了下,找到了一个凑着用的效果。

https://github.com/BlazorPlus/BlazorDemoWasmLoading

替换后的效果是:

替换后的代码如下:

  1. <body>
  2. <div id="app">
  3. <div
  4. style="position:fixed;left:0;top:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;">
  5. <svg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='80px' height='80px'
  6. viewBox='0 0 40 40' enable-background='new 0 0 40 40' xml:space='preserve'>
  7. <path opacity='0.2' fill='#000'
  8. d='M20.201,5.169c-8.254,0-14.946,6.692-14.946,14.946c0,8.255,6.692,14.946,14.946,14.946 s14.946-6.691,14.946-14.946C35.146,11.861,28.455,5.169,20.201,5.169z M20.201,31.749c-6.425,0-11.634-5.208-11.634-11.634 c0-6.425,5.209-11.634,11.634-11.634c6.425,0,11.633,5.209,11.633,11.634C31.834,26.541,26.626,31.749,20.201,31.749z'>
  9. </path>
  10. <path fill='#000'
  11. d='M26.013,10.047l1.654-2.866c-2.198-1.272-4.743-2.012-7.466-2.012h0v3.312h0 C22.32,8.481,24.301,9.057,26.013,10.047z'
  12. transform='rotate(228 20 20)'>
  13. <animateTransform attributeType='xml' attributeName='transform' type='rotate' from='0 20 20'
  14. to='360 20 20' dur='0.5s' repeatCount='indefinite'></animateTransform>
  15. </path>
  16. </svg>
  17. <div style="height:30px">
  18. Loading..
  19. </div>
  20. <div id="progressbar"
  21. style="display: inline-block; width: 260px; height: 12px; border: solid 1px gray; border-radius:6px; position: relative;">
  22. </div>
  23. </div>
  24. </div>
  25. <script type="text/javascript">
  26. new function () {
  27. var preLoadTime = 0;
  28. var preLoadCount = 0;
  29. var preLoadError = 0;
  30. var preLoadFinish = 0;
  31. var preLoadPercent = 0;
  32. var preLoadStart = 0;
  33. var preLoadTotal = 0;
  34. var preLoadLoaded = 0;
  35. var preLoadCLength = 0;
  36. var preLoadSampleLoaded = 0;
  37. var preLoadSampleCLength = 0;
  38. function preLoadUpdateUI() {
  39. var progressbar = document.getElementById("progressbar");
  40. if (progressbar) {
  41. var p = preLoadFinish / preLoadCount;
  42. if (preLoadTotal) {
  43. p = preLoadLoaded / preLoadTotal;
  44. }
  45. else if (preLoadSampleLoaded) {
  46. var ratio = preLoadSampleCLength / preLoadSampleLoaded;
  47. var p2 = Math.min(1, (preLoadLoaded * ratio / preLoadCLength) * (preLoadStart / preLoadCount));
  48. p = (p + p2) / 2;
  49. }
  50. preLoadPercent = Math.max(preLoadPercent, p);
  51. progressbar.innerHTML = "<span style='position:absolute;left:0;background-color:darkgreen;height:10px;border-radius:5px;width:" + (progressbar.offsetWidth * preLoadPercent) + "px'></span>";
  52. }
  53. }
  54. function preLoadResource(dllname) {
  55. preLoadCount++;
  56. var xh = new XMLHttpRequest();
  57. xh.open("GET", dllname, true);
  58. var loaded = 0;
  59. var total = 0;
  60. var clength = 0;
  61. xh.onprogress = function (e) {
  62. if (!e.loaded) return;
  63. if (loaded == 0) {
  64. preLoadStart++;
  65. clength = parseInt(xh.getResponseHeader("Content-Length"));
  66. total = e.total;
  67. preLoadCLength += clength;
  68. preLoadTotal += total;
  69. }
  70. preLoadLoaded += e.loaded - loaded;
  71. loaded = e.loaded;
  72. preLoadUpdateUI();
  73. }
  74. xh.onload = function () {
  75. if (loaded && clength) {
  76. preLoadSampleLoaded += loaded;
  77. preLoadSampleCLength += clength;
  78. }
  79. preLoadFinish++;
  80. if (xh.status != 200) preLoadError++;
  81. //console.log(preLoadFinish + "/" + preLoadCount, clength / loaded, dllname);
  82. if (preLoadFinish == preLoadCount) {
  83. var span = new Date().getTime() - preLoadTime;
  84. console.log("All Done In " + span + " ms , " + preLoadError + " errors");
  85. }
  86. }
  87. xh.send("");
  88. }
  89. function preLoadAll() {
  90. preLoadTime = new Date().getTime();
  91. var xh = new XMLHttpRequest();
  92. xh.open("GET", "_framework/blazor.boot.json", true);
  93. xh.onload = function () {
  94. var res = JSON.parse(xh.responseText);
  95. console.log(res);
  96. var arr = [];
  97. function moveFront(part) {
  98. for (var i = 0; i < arr.length; i++) {
  99. if (arr[i].indexOf(part) != -1) {
  100. arr.unshift(arr.splice(i, 1)[0]);
  101. break;
  102. }
  103. }
  104. }
  105. arr.push("_framework/blazor.webassembly.js");
  106. for (var p in res.resources.runtime)
  107. arr.push("_framework/wasm/" + p);
  108. for (var p in res.resources.assembly)
  109. arr.push("_framework/_bin/" + p);
  110. moveFront("System.Core.dll");
  111. moveFront("System.Data.dll");
  112. moveFront("System.dll");
  113. moveFront("System.Xml.dll");
  114. moveFront("mscorlib");
  115. moveFront("dotnet.wasm");
  116. arr.forEach(preLoadResource);
  117. //console.log(arr);
  118. }
  119. xh.send("");
  120. }
  121. preLoadAll();
  122. }
  123. </script>
  124. <div id="blazor-error-ui">
  125. An unhandled error has occurred.
  126. <a href="" class="reload">Reload</a>
  127. <a class="dismiss"></a>
  128. </div>
  129. <script src="_framework/blazor.webassembly.js"></script>
  130. </body>

添加项目对PWA的支持

渐进式Web应用(PWA)通常是一种单页应用程序(SPA),它使用新式浏览器API和功能以表现得如桌面应用。Blazor Web Assembly是基于标准的客户端Web应用平台,因此它可以使用任何浏览器API,包括以下功能所需的PWA API:

  • 脱机工作并即时加载(不受网络速度影响)。
  • 在自己的应用窗口中运行,而不仅仅是在浏览器窗口中运行。
  • 从主机操作系统的开始菜单、扩展坞或主屏幕启动。
  • 从后端服务器接收推送通知,即使用户没有在使用该应用。
  • 在后台自动更新。

使用“渐进式”一词来描述此类应用的原因如下:

  • 用户可能先是在其网络浏览器中发现应用并使用它,就像任何其他单页应用程序一样。
  • 过了一段时间后,用户进而将其安装到操作系统中并启用推送通知。

其实我们也可以通过命令行添加--pwa参数来创建PWA模板的应用。

  1. dotnet new blazorwasm -o MyBlazorPwa --pwa

但是我们现在要做的是把当前项目转成PWA项目。

1. 在ChineseYuanParser.csproj文件中,做如下修改。

将以下ServiceWorkerAssetsManifest属性添加到PropertyGroup中。

  1. <PropertyGroup>
  2. <TargetFramework>net6.0</TargetFramework>
  3. <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
  4. </PropertyGroup>

将以下ServiceWorker项添加到ItemGroup中。

  1. <ItemGroup>
  2. <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
  3. </ItemGroup>

这里提到两个js文件,我们可以从Blazor WebAssembly 项目模板wwwroot文件夹(dotnet/aspnetcore GitHub 存储库 main 分支)来获取。

service-worker.published.js

  1. // Caution! Be sure you understand the caveats before publishing an application with
  2. // offline support. See https://aka.ms/blazor-offline-considerations
  3. self.importScripts('./service-worker-assets.js');
  4. self.addEventListener('install', event => event.waitUntil(onInstall(event)));
  5. self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
  6. self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
  7. const cacheNamePrefix = 'offline-cache-';
  8. const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
  9. const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
  10. const offlineAssetsExclude = [ /^service-worker\.js$/ ];
  11. async function onInstall(event) {
  12. console.info('Service worker: Install');
  13. // Fetch and cache all matching items from the assets manifest
  14. const assetsRequests = self.assetsManifest.assets
  15. .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
  16. .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
  17. .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
  18. //#if(IndividualLocalAuth && Hosted)
  19. // Also cache authentication configuration
  20. assetsRequests.push(new Request('_configuration/ComponentsWebAssembly-CSharp.Client'));
  21. //#endif
  22. await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
  23. }
  24. async function onActivate(event) {
  25. console.info('Service worker: Activate');
  26. // Delete unused caches
  27. const cacheKeys = await caches.keys();
  28. await Promise.all(cacheKeys
  29. .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
  30. .map(key => caches.delete(key)));
  31. }
  32. async function onFetch(event) {
  33. let cachedResponse = null;
  34. if (event.request.method === 'GET') {
  35. // For all navigation requests, try to serve index.html from cache
  36. // If you need some URLs to be server-rendered, edit the following check to exclude those URLs
  37. //#if(IndividualLocalAuth && Hosted)
  38. const shouldServeIndexHtml = event.request.mode === 'navigate'
  39. && !event.request.url.includes('/connect/')
  40. && !event.request.url.includes('/Identity/');
  41. //#else
  42. const shouldServeIndexHtml = event.request.mode === 'navigate';
  43. //#endif
  44. const request = shouldServeIndexHtml ? 'index.html' : event.request;
  45. const cache = await caches.open(cacheName);
  46. cachedResponse = await cache.match(request);
  47. }
  48. return cachedResponse || fetch(event.request);
  49. }

service-worker.js

  1. // In development, always fetch from the network and do not enable offline support.
  2. // This is because caching would make development more difficult (changes would not
  3. // be reflected on the first load after each change).
  4. self.addEventListener('fetch', () => { });

manifest.json

  1. {
  2. "name": "大写人民币翻译机",
  3. "short_name": "大写人民币翻译机",
  4. "start_url": "./",
  5. "display": "standalone",
  6. "background_color": "#ffffff",
  7. "theme_color": "#03173d",
  8. "prefer_related_applications": false,
  9. "icons": [
  10. {
  11. "src": "icon-512.png",
  12. "type": "image/png",
  13. "sizes": "512x512"
  14. },
  15. {
  16. "src": "icon-192.png",
  17. "type": "image/png",
  18. "sizes": "192x192"
  19. }
  20. ]
  21. }

我们还需要准备两个图标文件,分别是icon-512.pngicon-192.png

在应用的wwwroot/index.html文件中,为清单和应用图标添加<link>元素。

  1. <head>
  2. <link href="manifest.json" rel="manifest" />
  3. <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
  4. </head>

在应用的wwwroot/index.html文件中,将以下<script>标记添加到紧跟在blazor.webassembly.js脚本标记后面的</body>结束标记中。

  1. <script src="_framework/blazor.webassembly.js"></script>
  2. <script>navigator.serviceWorker.register('service-worker.js');</script>

2. 安装和体验PWA应用

添加了PWA的支持,我们重新运行一次,记得Ctrl+F5刷新一次比较好。

在Edge中,我们可以在地址栏的尾部看到安装PWA应用的按钮。

安装后,我们可以在Windows10开始菜单中找到这个应用了。

并且可以右键添加到开始菜单磁贴区域。

在iOS上,访问者可以通过Safari的“共享”按钮和“添加到主屏幕”选项安装PWA 。在适用于Android的Chrome上,用户应该选择右上角的“菜单”按钮,然后选择“添加到主屏幕”。

若要自定义窗口的标题、配色方案、图标或其他详细信息,可以修改wwwroot目录中的manifest.json文件。

https://developer.mozilla.org/zh-CN/docs/Web/Manifest

参考

温故知新,Blazor遇见大写人民币翻译机(ChineseYuanParser),践行WebAssembly SPA的实践之路的更多相关文章

  1. C#小写数字金额转换成大写人民币金额的算法

    C#小写数字金额转换成大写人民币金额的算法 第一种方法: using System.Text.RegularExpressions;//首先引入命名空间 private string DaXie(st ...

  2. JS实现将数字金额转换为大写人民币汉字

    function convertCurrency(money) { //汉字的数字 var cnNums = new Array('零', '壹', '贰', '叁', '肆', '伍', '陆', ...

  3. 数字转换大写人民币的delphi实现

    function TForm1.changeRmb(const strRmb:string):string; var txt,strhighlevel:string; i,n,m,ilen,ipos: ...

  4. irport报表,把数字金额转换成大写人民币金额

    1.编写oracle函数 CREATE OR REPLACE Function MoneyToChinese(Money In Number) Return Varchar2 Is strYuan ) ...

  5. 吴裕雄--天生自然KITTEN编程:翻译机

  6. 上位机开发之西门子PLC-S7通信实践

    写在前面: 就目前而言,在中国的工控市场上,西门子仍然占了很大的份额,因此对于上位机开发而言,经常会存在需要与西门子PLC进行通信的情况.然后对于西门子PLC来说,通信方式有很多,下面简单列举一下: ...

  7. 【翻译】ScyllaDB数据建模的最佳实践

    文章翻译自Scylla官方文档:https://www.scylladb.com/2019/08/20/best-practices-for-data-modeling/ 转载请注明出处:https: ...

  8. C#中小写人民币转大写

    /// <summary> /// 转换成大写人民币 /// </summary> /// <param name="myMoney">< ...

  9. Google的通用翻译机能成为未来的巴别鱼吗?

    “巴别鱼,”<银河系漫游指南>轻轻朗读着,“体型很小,黄色,外形像水蛭,很可能是宇宙中最奇异的事物.它靠接收脑电波的能量为生,并且不是从其携带者身上接收,而是从周围的人身上.……如果你把一 ...

随机推荐

  1. Java集合详解(二):ArrayList原理解析

    概述 本文是基于jdk8_271版本进行分析的. ArrayList是Java集合中出场率最多的一个类.底层是基于数组实现,根据元素的增加而动态扩容,可以理解为它是加强版的数组.ArrayList允许 ...

  2. Word·去掉复制粘贴自动添加的空格

    阅文时长 | 0.05分钟 字数统计 | 145.6字符 主要内容 | 1.引言&背景 2.声明与参考资料 『Word·去掉复制粘贴自动添加的空格』 编写人 | SCscHero 编写时间 | ...

  3. Nginx导航

    简介 最近都在弄微服务的东西,现在来记录下收获.我从一知半解到现在能从0搭建使用最大的感触有两点 1.微服务各大组件的版本很多,网上很多博客内容不一定适合你的版本,很多时候苦苦琢磨都是无用功 2.网上 ...

  4. C语言风格的 for 循环(SHELL的循环写法 已验证20200517)

    C语言风格的 for 循环 C语言风格的 for 循环的用法如下: for((exp1; exp2; exp3))do    statementsdone 几点说明: exp1.exp2.exp3 是 ...

  5. Ubuntu 20.04 版本安装

    Ubuntu 20.04 版本安装 安装步骤 首先创建好Ubuntu 20.04虚拟机 等待系统检查完整性 选择语言 选择不更新,回车确定 键盘语言默认即可 网卡IP配置 设置代理服务器 设置源 自定 ...

  6. 027. Python面向对象的__init__方法

    __init__魔术方法(构造方法) 触发时机:实例化对象,初始化的时候触发 功能:为对象添加成员 参数:参数不固定,至少一个self参数 返回值:无 基本用法,至少含有一个参数 class MyCl ...

  7. STM32标准外设库中USE_STDPERIPH_DRIVER, STM32F10X_MD的含义

    在项目中使用stm32标准外设库(STM32F10x Standard Peripherals Library)的时候,我们会在项目的选项中预定义两个宏定义:USE_STDPERIPH_DRIVER, ...

  8. 详解 WebRTC 高音质低延时的背后 — AGC(自动增益控制)

    前面我们介绍了 WebRTC 音频 3A 中的声学回声消除(AEC:Acoustic Echo Cancellation)的基本原理与优化方向,这一章我们接着聊另外一个 "A" - ...

  9. Java反射机制详情(2)

    | |目录 运行环境 Java语言的反射机制 Class中的常用方法(获得类的构造方法) Class中的常用方法(获得类的属性) Class中的常用方法(获得类的方法) 反射动态调用类的成员 1.运行 ...

  10. 彻底解决Could not transfer artifact org.apache.maven.plugins问题

    今天在学习maven框架的时候出现Could not transfer artifact org.apache.maven.plugins问题,后面根据很多博客综合总结,终于解决了,现在分享一下我的方 ...