Blazor Server,即运行在服务器上的 Blazor 应用程序,它的优点是应用程序在首次运行时,客户端不需要下载运行时。但它的代码是在服务器上执行的,然后通过 SignalR 通信来更新客户端的 UI,所以它要求必须建立 Web Socket 连接。
用于 Blazor 应用的 SignalR Hub 是 ComponentHub,默认的连接地址是 /_blazor。多数时候我们不需要修改它,但人是一种喜欢折腾的动物,既然 MapBlazorHub 方法的重载也允许我们修改地址,那咱们何不试试。
app.MapBlazorHub("/myapp");
app.MapFallbackToPage("/_Host");
我把 ComponentHub 的通信地址改为 /myapp。这时候,客户端上就不能使用 blazor.server.js 中的默认行为了,咱们必须手动启动 Blazor 应用了(因为自动启动用的是默认的 /_blazor 地址)。
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
Blazor.start({
configureSignalR: (connbuilder) => {
connbuilder.withUrl("myapp");
}
});
</script>
在引用 blazor.server.js 文件时,加上一个 autostart = "false",表示 blazor 应用手动启动。哦,这个 autostart 是怎么来的?来,咱们看看源代码。在 BootCommon.ts 文件中,定义有一个名为 shouldAutoStart 的函数,而且它已导出。看名字就知道,它用来判断是否自动启动 Blazor 应用。
export function shouldAutoStart(): boolean {
return !!(document &&
document.currentScript &&
document.currentScript.getAttribute('autostart') !== 'false');
}
现在,你明白这个 autostart 特性是怎么回事了吧。
在调用 Blazor.start 方法时咱们要设定一个配置项—— configureSignalR。它指定一个函数,函数的参数是 HubConnectionBuilder 对象。这是 signalR.js 中的类型。再调用 withUrl 方法更改连接地址,默认的代码是这样的。
const connectionBuilder = new HubConnectionBuilder()
.withUrl('_blazor')
.withHubProtocol(hubProtocol);
很遗憾的是,运行后发现并不成功。

其实咱们的代码并没有错,问题其实是出在 Blazor 自身的“八阿哥”上。别急,老周接下来一层层剥出这个问题,你会感叹,官方团队竟然会犯“高级错误”。
咱们先来解释这个奇葩的错误信息,什么JSON格式不对?什么无效的字符“<”?这个错误信息很容易误导你,咱们看看下面的图。

在请求到 blazor.server.js 脚本后,访问了一个 /initializers 地址。这个 fetch 是 Blazor 应用发出的,其目的是问一下服务器,在 Blazor 应用启动前后,有没用自定义的初始化脚本。
为啥会有这个请求?我们来看看服务器端的源代码。
public static ComponentEndpointConventionBuilder MapBlazorHub(
this IEndpointRouteBuilder endpoints,
string path,
Action<HttpConnectionDispatcherOptions> configureOptions)
{
……
var hubEndpoint = endpoints.MapHub<ComponentHub>(path, configureOptions);
var disconnectEndpoint = endpoints.Map(
(path.EndsWith('/') ? path : path + "/") + "disconnect/",
endpoints.CreateApplicationBuilder().UseMiddleware<CircuitDisconnectMiddleware>().Build())
.WithDisplayName("Blazor disconnect");
var jsInitializersEndpoint = endpoints.Map(
(path.EndsWith('/') ? path : path + "/") + "initializers/",
endpoints.CreateApplicationBuilder().UseMiddleware<CircuitJavaScriptInitializationMiddleware>().Build())
.WithDisplayName("Blazor initializers");
return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint, jsInitializersEndpoint);
}
如你所见,当你调用 MapBlazorHub 方法时,它同时注册了两个终结点:
a、/_blazor/disconnect/:断开 SignalR 连接时访问,由 CircuitDisconnectMiddleware 中间件负责处理。
b、/_blazor/initializers/:对,这个就是咱们在浏览器中看到的那个,请求初始化脚本的。由 CircuitJavaScriptInitializationMiddleware 中间件负责处理。
老周改了 Hub 地址是 myapp,所以这两路径应变为 /myapp/disconnect/ 和 /myapp/initializers/。
这个 initialicaers/ 地址返回的数据要求是 JSON 格式的,是一个字符串数组,表示初始化脚本的文件路径。
咱们继续跟踪,找到 CircuitJavaScriptInitializationMiddleware 中间件。
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsJsonAsync(_initializers);
}
就这?对,就一行,_initializers 是选项类 CircuitOptions 的 JavaScriptInitializers 属性。
internal IList<string> JavaScriptInitializers { get; } = new List<string>();
这厮还是 internal 的,也就是说你写的代码不能修改它。它是用 CircuitOptionsJavaScriptInitializersConfiguration 对象来设置的。
public void Configure(CircuitOptions options)
{
var file = _environment.WebRootFileProvider.GetFileInfo($"{_environment.ApplicationName}.modules.json");
if (file.Exists)
{
var initializers = JsonSerializer.Deserialize<string[]>(file.CreateReadStream());
for (var i = 0; i < initializers.Length; i++)
{
var initializer = initializers[i];
options.JavaScriptInitializers.Add(initializer);
}
}
}
老周解释一下:上面代码是说在 Web 目录下(默认就是静态文件专用的 wwwroot)下找到一个名为 {你的应用}.modules.json 的文件,然后读出来,再添加到 JavaScriptInitializers 属性中。
假如我们应用程序叫 BugApp,那么要找的这个JSON文件就是 BugApp.modules.json。这个JSON文件既可以自动生成,也可以你手动添加。
你在 wwwroot 目录下添加一个 js 文件,命名为 BugApp.lib.module.js。在生成项目时,会自动产生这个 JSON 文件。在生成后你是看不到 BugApp.modules.json 文件的,而是在 Debug|Release 目录下有个 BugApp.staticwebassets.runtime.json。
{
"ContentRoots": [
"C:\\XXXX\\BugApp\\wwwroot\\",
"C:\\XXXX\\BugApp\\obj\\Debug\\net7.0\\jsmodules\\"
],
"Root": {
……
"BugApp.modules.json": {
"Children": null,
"Asset": {
"ContentRootIndex": 1,
"SubPath": "jsmodules.build.manifest.json"
},
"Patterns": null
}
},
……
}
}
终于见到它了,它指向的是 obj 目录下的 jsmodules.build.manifest.json 文件,ContentRootIndex : 1 表示 ContentRoots 节点中的第二个元素,即 obj\Debug\net7.0\jsmodules\jsmodules.build.manifest.json。打开这个文件看看有啥。
[
"BugApp.lib.module.js"
]
如果你找不到这个文件,说明你没有生成项目,生成一下就有了。注意,这个文件只有你【发布】项目后才会出现在 wwwroot 目录下的。
看到没?就是一个 JSON 数组,然后列出我刚刚添加的 js 脚本。客户端访问 ./initializers 就是为了获得这个文件。现在你回想一下浏览器报的那个错误,是不是知道为什么会说无效的 JSON 文件了吧。
客户端所请求的地址仍是默认的 /_blazor/initializers/ ,而我已经改为 /myapp 了,它本应该请求 /myapp/initializers 的,可是,blazor 并没这么做。那,我们能在 js 代码中配置吗?唉!官方团队犯的“高级”错误,居然把 URL 写死在代码中。可以看看 JSInitializers.Server.ts 文件中是怎么写的。
export async function fetchAndInvokeInitializers(options: Partial<CircuitStartOptions>) : Promise<JSInitializer> {
const jsInitializersResponse = await fetch('_blazor/initializers', {
method: 'GET',
credentials: 'include',
cache: 'no-cache',
});
……
}
你看是不是这样?都 TM 的硬编码了,还怎么配置?哦,还没回答一个问题:既然找到问题所在了,那为什么会报无效 JSON 格式的错误?答:因为 /_blazor 被我改了,所以请求 /_blazor/initializers 是 404 的,但,我们为了让 Blazor 能启动,调用了 MapFallbackToPage 方法作为后备。
app.MapFallbackToPage("/_Host");
这样就导致在访问 /_blazor/initializers 得到404后转而返回 /_Host,也就是说,/initializers 获取一个 HTML 文档,HTML 文档的第一个字符不就是“<”吗,所以就是无效字符了,不是JSON。
所以,你说,这不是“八阿哥”是啥?如果你非要改掉默认地址,又想正常获取初始化脚本,咋整?
A方案:下载 TypeScript 源码,自己修改,然后编译。
B方案:我们在 HTTP 管道上加个中间件,把 /myapp 改回 /_blazor。
这里老周演示一下 B 方案。
// Blazor signalR Hub 的自定义地址
const string NewBlazorHubUrl = "/myapp";
app.UseStaticFiles();
// 要在路由中间件之前改地址
app.Use(async (context, next) =>
{
if(context.Request.Path.StartsWithSegments("/_blazor", StringComparison.OrdinalIgnoreCase))
{
var repl = context.Request.Path.ToString().Replace("/_blazor", NewBlazorHubUrl);
context.Request.Path = repl;
}
await next();
});
// 注意顺序
app.UseRouting();
app.MapBlazorHub(NewBlazorHubUrl);
app.MapFallbackToPage("/_Host");
因为新地址是 /myapp 开头,我们只要把以 /_blazor 开头的地址改为 /myapp 开头就行了。这个中间件一定要在路由中间件之前改地址。改地址后再做路由匹配才有意义。
当然,想简洁一点的,还可以用 URL Rewrite。
var rwtopt = new RewriteOptions()
.AddRewrite("^_blazor/(.+)", "myapp/$1", true);
app.UseRewriter(rwtopt);
app.UseRouting();
app.MapBlazorHub("/myapp");
app.MapFallbackToPage("/_Host");
app.Run();
替换时用的正则表达式,我们匹配 _blazor 后的内容,即 initializers,然后替换为 myapp + initializers。“$1”引用正则中匹配的分组,即“.+”,匹配一个以上任意字符。URL 重写时,不需要指定开头的“/”,所以处理的是 _blazor/... 而不是 /_blazor/...。
前面我们提到了 BugApp.lib.module.js 脚本。干脆咱们也写一个自定义脚本。在 wwwroot 目录下添加一个 BugApp.lib.module.js 文件。BugApp 是项目名称,你要根据实际来改。
export function beforeStart() {
console.log("Blazor应用即将启动");
}
export function afterStarted() {
console.log("Blazor应用已启动");
}
在这个脚本中,我们要导出两个函数:
beforeStart:在 Blazor 启动之前被调用。
afterStarted:在 Blazor 启动之后被调用。
现在,再次运行程序,用开发人员工具查看“控制台”消息,会看到这两条输出。

想玩直观一点的话,也可以修改 HTML 文档。
export function beforeStart() {
let ele = document.createElement("div");
// 设置样式
ele.style = 'color: green; margin-top: 16px';
// 文本
ele.textContent = "Blazor应用即将启动";
document.body.append(ele);
}
export function afterStarted() {
let ele = document.createElement("div");
ele.style = 'color: orange; margin-top: 15px';
ele.textContent = "Blazor应用已经启动";
document.body.append(ele);
}
运行之后,页面上会动态加了两个 <div> 元素。

XXX.lib.module.js 这个文件名是固定的,如果想自定义文件名,或想返回多个 js 文件,可以自己手动处理。
在 wwwroot 目录下添加一个名为 BugApp.modules.json 的文件。
[
"abc.js",
"def.js",
"opq.js"
]
以JSON数组的格式把你想用的初始化脚本写上。再次运行程序,就会下载这个文件,读取三个文件并将其下载。

你得注意,你指定的这些脚本必须是可访问,有效的,不然 Blazor 会启动失败。
好了,今天就说到这儿了,主要是发现了一个“八阿哥”。
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po
尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub
我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search
由于fast-stemmer的问题,我很难安装我想要的任何rubygem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=
当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub
我有一个存储主机名的Ruby数组server_names。如果我打印出来,它看起来像这样:["hostname.abc.com","hostname2.abc.com","hostname3.abc.com"]相当标准。我想要做的是获取这些服务器的IP(可能将它们存储在另一个变量中)。看起来IPSocket类可以做到这一点,但我不确定如何使用IPSocket类遍历它。如果它只是尝试像这样打印出IP:server_names.eachdo|name|IPSocket::getaddress(name)pnameend它提示我没有提供服务器名称。这是语法问题还是我没有正确使用类?输出:ge
我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
我在rspec中收到来自webkit驱动程序的以下消息:Capybara::Driver::Webkit::WebkitInvalidResponseError:UnabletoloadURL:http://127.0.0.1:44923/posts几天前它成功了。问题出在save_page方法上。有什么问题吗? 最佳答案 当我的页面出现错误时,我收到过类似的错误消息。您应该通过在测试模式下启动服务器(railss-etest)并自行访问页面来手动检查情况是否如此。 关于ruby-on-
首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有, 也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加