温故知新,Blazor遇见大写人民币翻译机(ChineseYuanParser),践行WebAssembly SPA的实践之路
背景
在之前《温故知新,.Net Core遇见Blazor(FluentUI),属于未来的SPA框架》中我们已经初步了解了Blazor的相关概念,并且根据官方的指引完成了《创建我的第一个Blazor应用》、《生成Blazor待办事项列表应用》、《结合ASP.NET Core SignalR和Blazor实现聊天室应用》三个基础应用的实践探索,接下来我们继续探索如果通过Blazor的相关技术来完成一个独立的SPA应用。
什么是大写人民币翻译机(ChineseYuanParser)
大写人民币翻译机(ChineseYuanParser
),是一款结合Blazor
和WebAssembly
技术联合打造并且运行在.Net 5.0
运行时的数字金额转大写人民币金额的应用,适用于差旅报销时填写报销单需要将阿拉伯数字报销金额翻译成大写人民币金额的场景。
借鉴和引用
该项目为一个演示项目,旨在实践和练习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
开始创作
接下来,我将一步步拆解实现该应用的细节,这个过程中,我们也可以收获很多关于Blazor
、WebAssembly
、.Net 6.0
、Javascript
、Bootstrap
的知识点。
前置清单
创建名为"ChineseYuanParser"的Blazor WebAssembly应用
dotnet new blazorwasm -o ChineseYuanParser --no-https
或者
dotnet new blazorwasm -o ChineseYuanParser --no-https --pwa
选择好你要存档项目的根目录,然后在这个目录中右键打开Windows Terminal
进入,通过DotNet-Cli
命令new
来创建一个模板类型为blazorwasm
、输出名为ChineseYuanParser
的应用,中文译为大写人民币翻译机
,并且我们标记不需要强制https
,还是添加--pwa
来创建符合PWA规则的模板。
创建成功后,在终端中,可直接走命令打开Visual Studio Code加载当前项目
cd ChineseYuanParser
code .
接下来,我们找到wwwroot\index.html
,修改主页面的Title
,为中文的大写人民币翻译机
。
我们走通过DotNet-Cli
命令run
看下初始的效果,这里加一个watch
参数,可以做到修改后自动热重载。
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 class
的Div
元素。
Bootstrap提供了一套响应式、移动设备优先的流式网格系统,随着屏幕或视口(viewport)尺寸的增加,系统会自动分为最多12列。Bootstrap包含了一个响应式的、移动设备优先的、不固定的网格系统,可以随着设备或视口大小的增加而适当地扩展到12列。它包含了用于简单的布局选项的预定义类,也包含了用于生成更多语义布局的功能强大的混合类。Bootstrap网格系统(GridSystem)的工作原理:行必须放置在.container class内,以便获得适当的对齐(alignment)和内边距(padding)。
@inherits LayoutComponentBase
<div class="container">
@Body
</div>
6. 删除wwwroot
下的sample-data
演示数据,这是模板创建自带的,已经完全没用了。
7. 移除wwwroot\css\app.css
中的多余自带样式,只保留blazor-error-ui
相关的部分,其余的都可以删掉了,同时添加我们需要的样式效果,这里添加的样式主要是为了打造一个灰色背景,应用区域为白色悬浮的效果。
:root {
--transparent-dark-1: rgba(0,0,0,.108);
--transparent-dark-2: rgba(0,0,0,.125);
--transparent-dark-3: rgba(0,0,0,.132);
--transparent-dark-4: rgba(0,0,0,.175);
--gray-1: #f2f2f2;
--gray-2: #eee;
}
body {
background: #F2F2F2;
}
.box {
background-color: white;
padding: 1.8rem;
box-shadow: 0 1.6px 3.6px 0 var(--transparent-dark-3),0 .3px .9px 0 var(--transparent-dark-1);
border-radius: 3px;
}
8. 在Pages\Index.razor
中,添加我们承载应用内容的主体骨架,它有一个点睛之笔,也就是标题,把我们的应用名字突出来,接着我们把我们基于的技术栈表达出来,这里用到了一个@Environment.Version
用来读取当前.Net Core
的版本号信息,在尾部,我们打上自己的作者链接和名称。
对于
.NETCore 2.x
和.NET5+
,Environment.Version
属性返回.net
运行时版本号。
@page "/"
<div class="main box mt-4">
<h1 class="text-center">
大写人民币翻译机
</h1>
<div class="text-center">
<small>Blazor WASM By .Net @Environment.Version</small>
</div>
<hr />
<div>
</div>
</div>
<div class="mt-3 text-center author">
<a href="https://www.cnblogs.com/taylorshi" target="_blank">Taylor Shi</a>
</div>
9. 查看基础效果,一个应用的基本底子就出来,这就像女孩子化妆一样,我们先要打个好底子。
按需组合,搭建应用的功能面板
1. 构建翻译机左侧顶部翻译结果和按键功能区面板。
在前面的div
块上添加class="row"
,然后基于我们的视觉规划效果,构建左侧顶部翻译结果和按键功能区。
<!-- 左侧功能区 -->
<div class="col-md-8">
<!-- 展示及动作 -->
<section>
<!-- 转换结果展示 -->
<div class="cap-result border bg-light mb-2 p-3">
<h3>
@ParseResult
</h3>
</div>
<!-- 数字金额输入框 -->
<div class="row">
<div class="col-md-8" style="margin-bottom: 10px;">
<input type="text" class="form-control" placeholder="请输入数字金额" @bind-value="DigitalAmount" @bind-value:event="oninput" />
</div>
<div class="col-md-4" style="margin-bottom: 10px;">
<div class="row">
<button class="col-md-3 btn btn-success myButton" @onclick="CopyResult" >复制</button>
<button class="col-md-3 btn btn-primary myButton" @onclick="ReadResult" >朗读</button>
<button class="col-md-3 btn btn-danger myButton" @onclick="ClearResult" >清除</button>
</div>
</div>
</div>
</section>
</div>
.myButton {
margin-left: 10px;
max-height: 38px;
min-width: 79px;
max-width: 147px;
}
在转换结果展示区域,我们直接展示ParseResult
结果,还拥有一个输入框,输入框绑定了变量DigitalAmount
,这里有个技巧是,因为输入框输入内容我们需要及时的做出翻译结果,所以我们需要一个监听输入的方式,这里采用了@bind-value:event="oninput"
来做,同时我们设计了三个按钮在输入框旁边,分别是:复制按钮,绑定事件CopyResult
、朗读按钮,绑定事件ReadResult
、清除按钮,绑定事件ClearResult
。
2. 响应左侧顶部实时翻译和翻译结果展示的实现。
先看最简单的解析结果。
/// <summary>
/// 解析结果
/// </summary>
/// <value></value>
public string ParseResult { get; set; }
接下来,看DigitalAmount
的实现,这是整个应用功能的关键,因为我们希望在它值变更的时候,马上得出翻译结果,那么这里主要是采用它的set方法来实现。
/// <summary>
/// 数字金额
/// </summary>
private string _digitalAmount;
public string DigitalAmount
{
get => _digitalAmount;
set
{
_digitalAmount = value;
// 输入值为空不做处理
if(!string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value))
{
// 最大值:99999999999.99
if(!Equals(value, ".") && double.Parse(value) > 99999999999.99)
{
return;
}
// 小数点后最多两位
if(value.Contains("."))
{
// 如果只有一个点,特殊处理成0.
if(!Equals(value, "."))
{
// 按.拆分,且只允许存在一个.
var spiltValues = value.Split('.');
if(spiltValues.Length != 2)
{
return;
}
// 小数部分长度不能超过2位
var decimalValue = spiltValues.LastOrDefault();
if(!string.IsNullOrEmpty(decimalValue) && decimalValue.Length > 2)
{
return;
}
}
else
{
value = "0.";
_digitalAmount = value;
}
}
// 将01234 格式化成1234
if(value.StartsWith("0") && !value.Contains("."))
{
var intValue = int.Parse(value);
value = intValue.ToString();
_digitalAmount = value;
}
// 转换
ParseResult = RMBConverter.RMBToCap(_digitalAmount);
}
else
{
ParseResult = string.Empty;
}
}
}
在DigitalAmount
的set实现里面,我们先规避了空值和最大值,然后我们处理了小数位最多支持2
位小数,接下来我们将首位是0
的情况做了处理。
最终,才迎来关键的一个动作,也就是ParseResult = RMBConverter.RMBToCap(_digitalAmount)
代码,说白了,就是我们把拿到的金额丢进这个方法中,最终翻译得到我们要的大写人民币值,这也就是整个翻译机核心功能。
关于RMBConverter.RMBToCap
的实现,这个不用多说,都是借鉴了网上已经很成熟的代码实现,所以就直接贴代码了。
public class RMBConverter
{
public static string RMBToCap(string input)
{
// Constants:
var MAXIMUM_NUMBER = 99999999999.99;
// Predefine the radix characters and currency symbols for output:
var CN_ZERO = "零";
var CN_ONE = "壹";
var CN_TWO = "贰";
var CN_THREE = "叁";
var CN_FOUR = "肆";
var CN_FIVE = "伍";
var CN_SIX = "陆";
var CN_SEVEN = "柒";
var CN_EIGHT = "捌";
var CN_NINE = "玖";
var CN_TEN = "拾";
var CN_HUNDRED = "佰";
var CN_THOUSAND = "仟";
var CN_TEN_THOUSAND = "万";
var CN_HUNDRED_MILLION = "亿";
var CN_SYMBOL = "人民币";
var CN_DOLLAR = "元";
var CN_TEN_CENT = "角";
var CN_CENT = "分";
var CN_INTEGER = "整";
if (double.Parse(input) > MAXIMUM_NUMBER)
{
throw new ArgumentOutOfRangeException(nameof(input), "金额必须小于一百亿元");
}
string integral;
string decimalPart;
var parts = input.Split('.');
if (parts.Length > 1)
{
integral = parts[0];
decimalPart = parts[1];
if (decimalPart == string.Empty)
{
decimalPart = "00";
}
if (decimalPart.Length == 1)
{
decimalPart += "0";
}
// Cut down redundant decimal digits that are after the second.
decimalPart = decimalPart.Substring(0, 2);
}
else
{
integral = parts[0];
decimalPart = string.Empty;
}
// Prepare the characters corresponding to the digits:
var digits = new[] { CN_ZERO, CN_ONE, CN_TWO, CN_THREE, CN_FOUR, CN_FIVE, CN_SIX, CN_SEVEN, CN_EIGHT, CN_NINE };
var radices = new[] { "", CN_TEN, CN_HUNDRED, CN_THOUSAND };
var bigRadices = new[] { "", CN_TEN_THOUSAND, CN_HUNDRED_MILLION };
var decimals = new[] { CN_TEN_CENT, CN_CENT };
string outputCharacters = string.Empty;
if (long.Parse(integral) > 0)
{
var zeroCount = 0;
for (int i = 0; i < integral.Length; i++)
{
var p = integral.Length - i - 1;
var d = integral.Substring(i, 1);
var quotient = p / 4;
var modulus = p % 4;
if (d == "0")
{
zeroCount++;
}
else
{
if (zeroCount > 0)
{
outputCharacters += digits[0];
}
zeroCount = 0;
outputCharacters += digits[int.Parse(d)] + radices[modulus];
}
if (modulus == 0 && zeroCount < 4)
{
outputCharacters += bigRadices[quotient];
zeroCount = 0;
}
}
outputCharacters += CN_DOLLAR;
}
// Process decimal part if there is:
if (decimalPart != string.Empty)
{
for (int i = 0; i < decimalPart.Length; i++)
{
var d = decimalPart.Substring(i, 1);
if (d != "0")
{
outputCharacters += digits[int.Parse(d)] + decimals[i];
}
}
}
// Confirm and return the final output string:
if (outputCharacters == string.Empty)
{
outputCharacters = CN_ZERO + CN_DOLLAR;
}
if (decimalPart == string.Empty)
{
outputCharacters += CN_INTEGER;
}
return outputCharacters;
}
}
3. 响应左侧顶部三个功能按键:复制、朗读、清除。
先说最简单的清除吧,这个很简单,清除就是直接清空DigitalAmount
的值就行了,因为我们在DigitalAmount
的set里面也设计了,如果set为空,那么我们也会把ParseResult
设置为空,所以这样就达到了双清的目的。
/// <summary>
/// 清除结果
/// </summary>
private void ClearResult()
{
DigitalAmount = string.Empty;
}
接下来,复制和朗读功能就比较特殊了,要知道这两个工作,毕竟我们现在是在wasm的里面,也就是应用实际上是跑在浏览器了,所以我们需要借助JS原生的支持来完成。
我们需要在Index.razor
的顶部添加@inject IJSRuntime JavaScriptRuntime
来引入从Blazor对原生Js的调用。
然后我们将朗读和复制的JS写在首页的Index.html
中,这里借助Clipboard.writeText
方法来实现对复制功能的实现,借助speechSynthesis.speak
方法来实现对指定文本的朗读功能。
<script>
window.clipboardCopy = {
copyText: function (text) {
navigator.clipboard.writeText(text).then(function () {
console.log(text);
})
.catch(function (error) {
alert(error);
});
}
};
window.readAloud = {
readText: function (text) {
let utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'zh-CN';
speechSynthesis.speak(utterance);
}
}
</script>
完成Index.html
中的JS对应函数支持后,我们回到Index.razor
来通过调用JS来实现对复制和朗读功能的支持,这里用到JavaScriptRuntime.InvokeVoidAsync
的方式来调用JS方法。
/// <summary>
/// 复制结果
/// </summary>
private async Task CopyResult()
{
if (!string.IsNullOrEmpty(ParseResult))
{
await JavaScriptRuntime.InvokeVoidAsync("clipboardCopy.copyText", ParseResult);
}
}
/// <summary>
/// 朗读结果
/// </summary>
private async Task ReadResult()
{
if (!string.IsNullOrEmpty(ParseResult))
{
await JavaScriptRuntime.InvokeVoidAsync("readAloud.readText", ParseResult);
}
}
4. 构建翻译机左侧快捷数字输入面板
先贴代码再解读,这里主要是界面处理的逻辑。
<!-- 快捷键 -->
<section>
<!-- 快捷键1-9 -->
<div class="row">
@for (var i = 1; i <= 9; i++)
{
var num = i;
<div class="col-4">
<button class="btn btn-light border key" @onclick="() => ShortCutInvoked(num.ToString())">@num</button>
</div>
}
</div>
<!-- 快捷键0和. -->
<div class="row">
<div class="col-8">
<button class="btn btn-light border key" @onclick='() => ShortCutInvoked("0")'>0</button>
</div>
<div class="col-4">
<button class="btn btn-light border key" @onclick='() => ShortCutInvoked(".")'>.</button>
</div>
</div>
</section>
针对数字1-9,我们很好处理,构建一个Gird网格结构就行,每行支持3个按钮,这里我们需要用到for
的写法,需要注意的就是var num = i;
推荐这个写法。
我们在所有数字按键中统一绑定@onclick="() => ShortCutInvoked(num.ToString())"
,来支撑按键动作,其实逻辑也很简单,就是把新输入的数值,追加到原来的字符串后面就行了。
/// <summary>
/// 快捷键触发事件
/// </summary>
/// <param name="num"></param>
private void ShortCutInvoked(string num)
{
DigitalAmount += num;
}
.key {
font-family: "Consolas";
font-size: 250%;
width: 100%;
min-height: 90px;
margin-bottom: 10px;
}
针对数字0
和.
按键需要特殊处理,因为这里只有两个元素了,所以采用2:1
的比例来分配Gird的空间。
5. 构建翻译机右侧翻译参考表面板
在翻译机中,为了更加直观的表达每一个数字会被翻译成什么样的大写人民币,这里我们在整个界面的右侧放一个参考表。
这个看起来不难,首先我们需要准备一个字典ReferList
。
/// <summary>
/// 参照列表
/// </summary>
/// <value></value>
public Dictionary<string, string> ReferList { get; set; } = new Dictionary<string, string>
{
{
"零","0"
},
{
"壹","1"
},
{
"贰","2"
},
{
"叁","3"
},
{
"肆","4"
},
{
"伍","5"
},
{
"陆","6"
},
{
"柒","7"
},
{
"捌","8"
},
{
"玖","9"
},
{
"拾","10"
},
{
"佰","百"
},
{
"仟","千"
},
{
"万","万"
},
{
"亿","亿"
},
};
然后我们遍历这个字典,来构建我们的参考表,这里我们可以用foreach
来遍历ReferList
,以便可以取到它的key
和value
。
<!-- 右侧参照表 -->
<div class="col-md-4">
<div class="card">
<div class="card-header">
参照表
</div>
<div class="card-body">
<!-- 快捷键1-9 -->
<div class="row">
@foreach (var item in ReferList)
{
<div class="col-4">
<button class="btn btn-light border refer">
<div class="text-bottom">
@item.Key<small>(@item.Value)</small>
</div>
</button>
</div>
}
</div>
</div>
</div>
</div>
.refer {
font-family: "Consolas";
font-size: 150%;
width: 100%;
min-height: 80px;
min-width: 60px;
margin-bottom: 10px;
}
其实到这里呢,我们的主要是任务已经完成了,完成了整个左侧和右侧功能区的实现,啦啦啦。
看下效果吧。
定制启动页
Blazor
的启动页默认是个很简单的Loading
字样,这要是应用写大了或者用户的网络慢一点,就真的挺难看的,好在我们其实可以也很容易在wwwroot\index.html
找到它,并且对它完成自己的定制,当然效果这东西取决于你的审美和前端技术功底了。
<body>
<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="_framework/blazor.webassembly.js"></script>
</body>
如果你需要定制,你只需要替换掉div
中Loading...
这个部分就行了,我从github
搜了下,找到了一个凑着用的效果。
替换后的效果是:
替换后的代码如下:
<body>
<div id="app">
<div
style="position:fixed;left:0;top:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;">
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' width='80px' height='80px'
viewBox='0 0 40 40' enable-background='new 0 0 40 40' xml:space='preserve'>
<path opacity='0.2' fill='#000'
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'>
</path>
<path fill='#000'
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'
transform='rotate(228 20 20)'>
<animateTransform attributeType='xml' attributeName='transform' type='rotate' from='0 20 20'
to='360 20 20' dur='0.5s' repeatCount='indefinite'></animateTransform>
</path>
</svg>
<div style="height:30px">
Loading..
</div>
<div id="progressbar"
style="display: inline-block; width: 260px; height: 12px; border: solid 1px gray; border-radius:6px; position: relative;">
</div>
</div>
</div>
<script type="text/javascript">
new function () {
var preLoadTime = 0;
var preLoadCount = 0;
var preLoadError = 0;
var preLoadFinish = 0;
var preLoadPercent = 0;
var preLoadStart = 0;
var preLoadTotal = 0;
var preLoadLoaded = 0;
var preLoadCLength = 0;
var preLoadSampleLoaded = 0;
var preLoadSampleCLength = 0;
function preLoadUpdateUI() {
var progressbar = document.getElementById("progressbar");
if (progressbar) {
var p = preLoadFinish / preLoadCount;
if (preLoadTotal) {
p = preLoadLoaded / preLoadTotal;
}
else if (preLoadSampleLoaded) {
var ratio = preLoadSampleCLength / preLoadSampleLoaded;
var p2 = Math.min(1, (preLoadLoaded * ratio / preLoadCLength) * (preLoadStart / preLoadCount));
p = (p + p2) / 2;
}
preLoadPercent = Math.max(preLoadPercent, p);
progressbar.innerHTML = "<span style='position:absolute;left:0;background-color:darkgreen;height:10px;border-radius:5px;width:" + (progressbar.offsetWidth * preLoadPercent) + "px'></span>";
}
}
function preLoadResource(dllname) {
preLoadCount++;
var xh = new XMLHttpRequest();
xh.open("GET", dllname, true);
var loaded = 0;
var total = 0;
var clength = 0;
xh.onprogress = function (e) {
if (!e.loaded) return;
if (loaded == 0) {
preLoadStart++;
clength = parseInt(xh.getResponseHeader("Content-Length"));
total = e.total;
preLoadCLength += clength;
preLoadTotal += total;
}
preLoadLoaded += e.loaded - loaded;
loaded = e.loaded;
preLoadUpdateUI();
}
xh.onload = function () {
if (loaded && clength) {
preLoadSampleLoaded += loaded;
preLoadSampleCLength += clength;
}
preLoadFinish++;
if (xh.status != 200) preLoadError++;
//console.log(preLoadFinish + "/" + preLoadCount, clength / loaded, dllname);
if (preLoadFinish == preLoadCount) {
var span = new Date().getTime() - preLoadTime;
console.log("All Done In " + span + " ms , " + preLoadError + " errors");
}
}
xh.send("");
}
function preLoadAll() {
preLoadTime = new Date().getTime();
var xh = new XMLHttpRequest();
xh.open("GET", "_framework/blazor.boot.json", true);
xh.onload = function () {
var res = JSON.parse(xh.responseText);
console.log(res);
var arr = [];
function moveFront(part) {
for (var i = 0; i < arr.length; i++) {
if (arr[i].indexOf(part) != -1) {
arr.unshift(arr.splice(i, 1)[0]);
break;
}
}
}
arr.push("_framework/blazor.webassembly.js");
for (var p in res.resources.runtime)
arr.push("_framework/wasm/" + p);
for (var p in res.resources.assembly)
arr.push("_framework/_bin/" + p);
moveFront("System.Core.dll");
moveFront("System.Data.dll");
moveFront("System.dll");
moveFront("System.Xml.dll");
moveFront("mscorlib");
moveFront("dotnet.wasm");
arr.forEach(preLoadResource);
//console.log(arr);
}
xh.send("");
}
preLoadAll();
}
</script>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss"></a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
添加项目对PWA的支持
渐进式Web应用(PWA)
通常是一种单页应用程序(SPA),它使用新式浏览器API和功能以表现得如桌面应用。Blazor Web Assembly
是基于标准的客户端Web应用平台,因此它可以使用任何浏览器API,包括以下功能所需的PWA API:
- 脱机工作并即时加载(不受网络速度影响)。
- 在自己的应用窗口中运行,而不仅仅是在浏览器窗口中运行。
- 从主机操作系统的开始菜单、扩展坞或主屏幕启动。
- 从后端服务器接收推送通知,即使用户没有在使用该应用。
- 在后台自动更新。
使用“渐进式”一词来描述此类应用的原因如下:
- 用户可能先是在其网络浏览器中发现应用并使用它,就像任何其他单页应用程序一样。
- 过了一段时间后,用户进而将其安装到操作系统中并启用推送通知。
其实我们也可以通过命令行添加--pwa
参数来创建PWA模板的应用。
dotnet new blazorwasm -o MyBlazorPwa --pwa
但是我们现在要做的是把当前项目转成PWA项目。
1. 在ChineseYuanParser.csproj
文件中,做如下修改。
将以下ServiceWorkerAssetsManifest
属性添加到PropertyGroup
中。
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
将以下ServiceWorker
项添加到ItemGroup
中。
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
这里提到两个js文件,我们可以从Blazor WebAssembly 项目模板wwwroot文件夹(dotnet/aspnetcore GitHub 存储库 main 分支)来获取。
service-worker.published.js
// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
//#if(IndividualLocalAuth && Hosted)
// Also cache authentication configuration
assetsRequests.push(new Request('_configuration/ComponentsWebAssembly-CSharp.Client'));
//#endif
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
async function onActivate(event) {
console.info('Service worker: Activate');
// Delete unused caches
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
}
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
//#if(IndividualLocalAuth && Hosted)
const shouldServeIndexHtml = event.request.mode === 'navigate'
&& !event.request.url.includes('/connect/')
&& !event.request.url.includes('/Identity/');
//#else
const shouldServeIndexHtml = event.request.mode === 'navigate';
//#endif
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}
service-worker.js
// In development, always fetch from the network and do not enable offline support.
// This is because caching would make development more difficult (changes would not
// be reflected on the first load after each change).
self.addEventListener('fetch', () => { });
manifest.json
{
"name": "大写人民币翻译机",
"short_name": "大写人民币翻译机",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"prefer_related_applications": false,
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "icon-192.png",
"type": "image/png",
"sizes": "192x192"
}
]
}
我们还需要准备两个图标文件,分别是icon-512.png
、icon-192.png
。
在应用的wwwroot/index.html
文件中,为清单和应用图标添加<link>
元素。
<head>
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
</head>
在应用的wwwroot/index.html
文件中,将以下<script>
标记添加到紧跟在blazor.webassembly.js
脚本标记后面的</body>
结束标记中。
<script src="_framework/blazor.webassembly.js"></script>
<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://github.com/CraigTaylor/ChineseYuanParser
- https://github.com/EdiWang/RMBCapitalization-Blazor
- Blazor WASM 实现人民币大写转换器
- 尝鲜一试,Azure静态网站应用服务(Azure Static Web Apps) 免费预览,协同Github自动发布静态SPA
- https://github.com/BlazorPlus/BlazorDemoWasmLoading
- Bootstrap 网格系统
- Environment.Version 属性
- Javascript 实现复制(Copy)动作方法大全
- js文字转语音(speechSynthesis)
- 利用 ASP.NET Core Blazor WebAssembly 生成渐进式 Web 应用程序
- 托管和部署 ASP.NET Core Blazor
温故知新,Blazor遇见大写人民币翻译机(ChineseYuanParser),践行WebAssembly SPA的实践之路的更多相关文章
- C#小写数字金额转换成大写人民币金额的算法
C#小写数字金额转换成大写人民币金额的算法 第一种方法: using System.Text.RegularExpressions;//首先引入命名空间 private string DaXie(st ...
- JS实现将数字金额转换为大写人民币汉字
function convertCurrency(money) { //汉字的数字 var cnNums = new Array('零', '壹', '贰', '叁', '肆', '伍', '陆', ...
- 数字转换大写人民币的delphi实现
function TForm1.changeRmb(const strRmb:string):string; var txt,strhighlevel:string; i,n,m,ilen,ipos: ...
- irport报表,把数字金额转换成大写人民币金额
1.编写oracle函数 CREATE OR REPLACE Function MoneyToChinese(Money In Number) Return Varchar2 Is strYuan ) ...
- 吴裕雄--天生自然KITTEN编程:翻译机
- 上位机开发之西门子PLC-S7通信实践
写在前面: 就目前而言,在中国的工控市场上,西门子仍然占了很大的份额,因此对于上位机开发而言,经常会存在需要与西门子PLC进行通信的情况.然后对于西门子PLC来说,通信方式有很多,下面简单列举一下: ...
- 【翻译】ScyllaDB数据建模的最佳实践
文章翻译自Scylla官方文档:https://www.scylladb.com/2019/08/20/best-practices-for-data-modeling/ 转载请注明出处:https: ...
- C#中小写人民币转大写
/// <summary> /// 转换成大写人民币 /// </summary> /// <param name="myMoney">< ...
- Google的通用翻译机能成为未来的巴别鱼吗?
“巴别鱼,”<银河系漫游指南>轻轻朗读着,“体型很小,黄色,外形像水蛭,很可能是宇宙中最奇异的事物.它靠接收脑电波的能量为生,并且不是从其携带者身上接收,而是从周围的人身上.……如果你把一 ...
随机推荐
- 查看linux系统是多少位,使用 getconf LONG_BIT
查看linux系统是多少位,使用 getconf LONG_BIT echo $HOSTTYPE
- 更新索引库: $locate string 寻找包含有string的路径: $updatedb
更新索引库: $locate string 寻找包含有string的路径: $updatedb 与find不同,locate并不是实时查找.你需要更新数据库,以获得最新的文件索引信息.
- Linux讲解之定时任务
https://www.php.cn/linux-369884.html Linux讲解之定时任务 原创2018-05-14 10:11:3101319 本文目录: 12.1 配置定时任务 1 ...
- ipmi配置方法-20200328
ipmi配置错误-20200328[root@localhost home]# ipmitool lan set 1 ipsrc staticCould not open device at /dev ...
- nginx版本无缝升级与回滚
chookie和session 你们公司的会话保持怎么做的? 1.开发做的:记录用户登陆的状态,将用户登陆状态保存到,redis服务器中,nfs,mysql. 记录用户的登陆状态. 通过登陆用 ...
- 拉勾、Boss直聘、内推、100offer
BOSS直聘 拉勾.Boss直聘.内推.100offer
- 30-- A 代码记录分析
张的代码 30-- -A if(BT_INFO.RX.CACHE == BT_RX_CACHE[0]) { BT_INFO.RX.CACHE = BT_RX_CACHE[1]; } else { B ...
- JQuery 使用教程
引言 JQuery 是一个 JavaScript 库,它极大地简化了 JavaScript 编程.JQuery 拥有丰富的选择器,可以非常方便的获取和操作 DOM 元素,而在 JQuery 中所有选择 ...
- Redis SWAPDB 命令背后做了什么
Redis SWAPDB 命令背后做了什么 目录 Redis SWAPDB 命令背后做了什么 0x00 摘要 0x01 SWAPDB 基础 1.1 命令说明 1.2 演示 0x02 预先校验 0x03 ...
- Spring5.0源码学习系列之事务管理概述
Spring5.0源码学习系列之事务管理概述(十一),在学习事务管理的源码之前,需要对事务的基本理论比较熟悉,所以本章节会对事务管理的基本理论进行描述 1.什么是事务? 事务就是一组原子性的SQL操作 ...