创建新的 Blazor WebAssembly App
将其命名为BlazerWithTSInterop在您选择的目录中。
仅使用 .NET 6.0 客户端,无安全性且无 PWA。
CTRL+F5 在热重载模式下生成和运行。
总结已经创建了一个准备演示 JavaScript 互操作演练的项目。忽略模板附带的“计数器”和“获取数据”页。此演示将仅使用主页。
在我们开始Typescript之前,让我们看看JavaScript是如何互操作的。
在 Pages/Index.razor 的所有内容替换为以下代码片段。
@page "/" @inject IJSRuntime JS <h1>Hello, Interop!</h1> <hr />@Message<hr /> <h4>JS Interop</h4> <button class="btn btn-primary" @onclick="@Prompt">Prompt</button> <hr> @code{ string Message { get; set; } = string.Empty; async void Prompt(){ string answer = await JS.InvokeAsync<string>("propmpt","say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing":answer)}"; } }
解释一下:
JS.InvokeAsync:
第一个参数是:执行JavaScript 函数的名称
第二个参数是:执行JavaScript 函数的参数
返回一个字符串
保存以在热重载模式下运行并进行测试。
创建新的 JavaScript 文件。为JavaScript和Typescript文件创建新的'js'文件夹。
创建新的“wwwroot/js/script.js”文件。
添加下面的代码到script.js
function ScriptPrompt(message) { return prompt(message); } function ScriptAlert(message) { alert(message); }
ScriptPrompt 和 ScriptAlert 将静态加载和全局。
其他 JavaScript 模块(包括隔离模块)可访问。
请注意,脚本方法调用浏览器 API 提示并恭敬地发出警报。
在“wwwroot/index.html”中的“webassemly.js”之后添加“script.js”作为静态资源
<body> ... <script src="_framework/blazor.webassembly.js"></script> <script src="src/script.js"></script> ... </body>
请将所有“Pags/Index.razor”内容替换为以下代码片段。
使用操作方法添加脚本提示和脚本警报按钮。
@page "/" @inject IJSRuntime JS <h1>Hello, Interop!</h1><br /> <h4 style="background-color:aliceblue; padding:20px">JavaScript Interop</h4> @Message<hr /> <button class="btn btn-primary" @onclick="@Prompt">Prompt</button> <button class="btn btn-primary" @onclick="@ScriptPrompt">Script Prompt</button> <button class="btn btn-primary" @onclick="@ScriptAlert">Script Alert</button> <hr> @code{ string Message { get; set; } = string.Empty; async void Prompt() { string answer = await JS.InvokeAsync<string>("prompt","say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing":answer)}"; StateHasChanged(); } async void SrciptPrompt() { // string answer = await JS.InvokeAsync<string>("ScriptPrompt", "say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing" : answer)}"; StateHasChanged(); } async void ScriptAlert() { await JS.InvokeAsync("ScriptAlert", "Script Alert"); } }
运行并测试。
创建新的 'wwwroot/js/script.module.js' JavaScript 文件。
将下面的代码添加到'wwwroot/js/script.module.js'
export function ModulePrompt(message) { return ScriptPrompt(message); } export function ModulAlert(message) { return ScriptAlert(message); }
模块方法演示如何调用全局脚本方法。
请注意“export”方法前缀。这是将代码标记为可导入的 ES 模块语法。全局嵌入式 script.js 不使用“export”。
将所有“Index.razor”内容替换为以下代码片段,以添加模块按钮和方法。
@page "/" @inject IJSRuntime JS @implements IAsyncDisposable <h1>Hello, Interop!</h1><br /> <h4 style="background-color:aliceblue; padding:20px">JavaScript Interop</h4> @Message<hr /> <button class="btn btn-primary" @onclick="@Prompt">Prompt</button> <button class="btn btn-primary" @onclick="@ScriptPrompt">Script Prompt</button> <button class="btn btn-primary" @onclick="@ScriptAlert">Script Alert</button> <hr> <button class="btn btn-primary" @onclick="@ModulelPrompt">Module Prompt</button> <button class="btn btn-primary" @onclick="@ModulelAlert">Module Alert</button> <hr> @code{ private IJSObjectReference module; string Message { get; set; } = string.Empty; string Version{ get => $"?v={DateTime.Now.Ticks.ToString()}"; } async ValueTask IAsyncDisposable.DisposeAsync() { if (module is not null) await module.DisposeAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if(firstRender){ module = await JS.InvokeAsync<IJSObjectReference>("import", $"./src/script.module.js{Version}"); } } async void ModulelAlert() { await module.InvokeVoidAsync("ModulAlert", "Modulel Alert"); } async void ModulelPrompt() { string answer = await module.InvokeAsync<string>("ModulePrompt", "Module Prompt say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing" : answer)}"; StateHasChanged(); } async void Prompt() { string answer = await JS.InvokeAsync<string>("prompt","say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing":answer)}"; StateHasChanged(); } async void ScriptPrompt() { string answer = await JS.InvokeAsync<string>("ScriptPrompt", "say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing" : answer)}"; StateHasChanged(); } async void ScriptAlert() { await JS.InvokeVoidAsync("ScriptAlert", "Script Alert"); } }
隔离的模型支持 IAsyncDisposable 与 DisposeAsync 在不再需要时清理模块资源。
模块在首次呈现后由 OnAfterRenderAsync 方法加载。
ModulePrompt 演示如何调用静态方法 ScriptPrompt。
ModuleAlert 演示如何从同一模块调用另一个导出的方法。
通知模块在加载时附加一个唯一参数 Version 标记:
... string Version { get { return "?v=" + DateTime.Now.Ticks.ToString(); } } ... module = await JS.InvokeAsync<IJSObjectReference> ("import", "./js/script.module.js" + Version); ...
script.module.js 避免了由唯一参数标记缓存。
这是一个绕过浏览器缓存的黑客,该缓存可能会在开发过程中粘附。
要重新获得缓存性能,版本值可以替换为应用程序发行版号。
然后,这将在新版本的第一次客户端运行时强制缓存刷新一次。
构建并运行。
**总结 ** 第 2 部分介绍了如何调用独立和嵌入式 JavaScript。从 Blazor 调用 TypeScript 互操作的前身。
现在是回顾从Visual Studio调试JavaScript的好时机
Visual Studio可能会犹豫是否要附加到Chrome调试器。这个问题不是Blazor独有的。
随着JavaScript代码和符号的增长,这一点更加明显。以下是一些可能有帮助的情况和解决方法。
在中设置断点script.js如图所示
在调试模式 F5 下运行应用程序。
如果断点红色圆圈为空心,则不会附加调试器。
您可以在“脚本文档”文件夹中看到缓存的文件。
单击文件以查看缓存的内容是否来自以前的版本。
请尝试删除断点并重新应用。
调试器可能会重新附加。
应用运行时,在浏览器中按 CTR+Shift+I 可查看开发人员工具。
在“source”面板中选择 js/script.js,并在显示处设置断点。
这将触发Visual Studio调试器重新附加到Chrome。如果这不起作用,则在Chrome中调试就足够了。
**总结 ** 第 3 部分介绍了调试和调试器附件解决方法。建议执行调试代码演练以查看操作中的互操作
创建新的 'scripts/hello.ts' TypeScript 文件。
将代码添加到“hello.ts”。
注意 类方法从嵌入式“script.js”访问脚本警报
declare function ScriptAlert(message: string); export class Hello{ hello(): void { ScriptAlert("hello"); } static goodbye(): void { ScriptAlert("goodbye"); } } export var HelloInstance = new Hello();
安装 Microsoft.TypeScript.MSBuild
右键单击项目节点,然后选择“添加”>“新项”。 选择“TypeScript JSON 配置文件”,然后单击“添加”。
Visual Studio 会将 tsconfig.json 文件添加到项目根目录中。 可以使用此文件为 TypeScript 编译器配置选项。
打开 tsconfig.json 并更新,以设置所需的编译器选项。
设置版本:ECMAScript, TSX:无, 模块:ES2015 在项目/属性/类型脚本构建
{ "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "target": "es2015", "outDir": "wwwroot/js" }, "include": [ "scripts/**/*" ] }
将所有“Index.razor”内容替换为以下代码片段,以添加模块按钮和方法。
@page "/" @inject IJSRuntime JS @implements IAsyncDisposable <h1>Hello, Interop!</h1><br /> <h4 style="background-color:aliceblue; padding:20px">JavaScript Interop</h4> @Message<hr /> <button class="btn btn-primary" @onclick="@Prompt">Prompt</button> <button class="btn btn-primary" @onclick="@ScriptPrompt">Script Prompt</button> <button class="btn btn-primary" @onclick="@ScriptAlert">Script Alert</button> <hr> <button class="btn btn-primary" @onclick="@ModulelPrompt">Module Prompt</button> <button class="btn btn-primary" @onclick="@ModulelAlert">Module Alert</button> <hr> <h4 style="background-color:aliceblue; padding:20px">TypeScript Interop</h4><hr> <button class="btn btn-primary" @onclick="@HelloAlert">Hello Alert</button> @code{ private IJSObjectReference module; private IJSObjectReference hello; string Message { get; set; } = string.Empty; string Version{ get => $"?v={DateTime.Now.Ticks.ToString()}"; } async ValueTask IAsyncDisposable.DisposeAsync() { if (module is not null) await module.DisposeAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if(firstRender){ module = await JS.InvokeAsync<IJSObjectReference>("import", $"./js/script.module.js{Version}"); hello = await JS.InvokeAsync<IJSObjectReference>("import", $"./js/hello.js{Version}"); } } async void ModulelAlert() { await module.InvokeVoidAsync("ModulAlert", "Modulel Alert"); } async void ModulelPrompt() { string answer = await module.InvokeAsync<string>("ModulePrompt", "Module Prompt say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing" : answer)}"; StateHasChanged(); } async void Prompt() { string answer = await JS.InvokeAsync<string>("prompt","say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing":answer)}"; StateHasChanged(); } async void ScriptPrompt() { string answer = await JS.InvokeAsync<string>("ScriptPrompt", "say what?"); Message = $"Prompt:{(string.IsNullOrEmpty(answer) ? "nothing" : answer)}"; StateHasChanged(); } async void ScriptAlert() { await JS.InvokeVoidAsync("ScriptAlert", "Script Alert"); } async void HelloAlert() { await hello.InvokeVoidAsync("HelloInstance.hello"); await hello.InvokeVoidAsync("Hello.goodbye"); } }
添加了另一个模块“hello”来加载由“hello.ts”生成的JavaScript文件“hello.js”。HelloAlert 方法演示调用 TypeScript 类方法“goodbye”和对象实例方法“hello”。这些方法使用嵌入式脚本“script.js”中的脚本警报函数
构建、运行和测试 Hello 警报。
右键单击“BlazerWithTSInterop”文件夹,然后选择弹出菜单项“在终端中打开”。
这将在编辑器中打开 PowerShell 终端窗口。
执行以下命令以创建 package.json
npm init -y
执行以下命令以安装 webpack 和 typescript 工具
npm i ts-loader typescript webpack webpack-cli
在 'package.json' 中添加脚本条目 "build": "webpack" 或者将 'package.json' 替换为下面的 json 内容。
{ "name": "blazerwithtsinterop", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "ts-loader": "^9.3.1", "typescript": "^4.8.2", "webpack": "^5.74.0", "webpack-cli": "^4.10.0" } }
修改tsconfig.json
{ "display": "Node 14", "compilerOptions": { "allowJs": true, "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "lib": [ "es2020", "DOM" ], "target": "es6", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "moduleResolution": "node" }, "include": [ "scripts/**/*" ], "exclude": [ "node_modules", "wwwroot" ] }
这将禁用不再适用的项目 TypeScript 生成属性。Visual Studio现在将使用tsconfig.json进行TypeScript配置。
在“BlazerWithTSInterop”文件夹中创建webpack.config.json,其中包含以下内容。
const path = require("path"); module.exports = { mode: 'development', devtool: 'eval-source-map', module: { rules: [ { test: /\.(ts)$/, exclude: /node_modules/, include: [path.resolve(__dirname, 'scripts')], use: 'ts-loader', } ] }, resolve: { extensions: ['.ts', '.js'], }, entry: { index: ['./scripts/index'] }, output: { path: path.resolve(__dirname, './wwwroot/public'), filename: '[name]-bundle.js', library: "[name]" } };
此脚本告诉 webpack 使用 ts-loader 将 .ts 文件转译为.js。
对于每个条目 [name] 创建一个 JavaScript 库 [name]。
文件 [name]-bundle 在“wwwroot/public”文件夹中进行类型化。
此脚本有一个名为“index”的条目。
将输入文件 './scripts/index.ts' 转译为输出文件 './scripts/index.js'。
第二个传递将 “./scripts/index.js” 与依赖项代码捆绑在一起,并输出到文件 '../wwwroot/public/index-bundle.js'
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> ... <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Exec Command="npm install" WorkingDirectory="wwwroot" /> <Exec Command="npm run build" WorkingDirectory="wwwroot" /> </Target> ... </Project>
不再需要 Microsoft.TypeScript.MSBuild 过程,因为它绕过了 webpack typescript pre-build。
没有做任何伤害,让它留在这个演示中。或者,您可以选择并删除以将其删除。
将代码添加到“index.ts”。索引类是 Hello 类包装器。索引模块还导出 Hello 类和 HelloInstance 对象。
import { Hello, HelloInstance } from "./hello"; export { Hello, HelloInstance } from "./hello"; export class Index { hello(): void { HelloInstance.hello(); } static goodbye(): void{ Hello.goodbye(); } } export var IndexInstance = new Index()
生成 Ctrl+Shift+B 会在“公共”目录中创建“index.js”和“index_bundle.js”。
在新的“index-bundle.js”中搜索“ScriptAlert”。 验证 'index.js' 包包含 'hello.js' 依赖代码。 捆绑包包括相关代码。 然而; 一个包将只有一个模块条目。 Interop 只能访问从模块中导出的项目。
在 'script.js' 之后的 'Index.html' 中添加 'index-bundle.js' 作为静态资源。
<body> ... <script src="_framework/blazor.webassembly.js"></script> <script src="js/script.js"></script> <script src="public/index-bundle.js"></script> ... </body>
在 'Index.razor' html 部分中,将此行添加为最后一个按钮。
<button class="btn btn-primary" @onclick="@BundleIndexHello">Bundle Index Hello</button>
在“Index.razor”代码部分添加这个作为最后一个方法。
async void BundleIndexHello() { await JS.InvokeVoidAsync("index.IndexInstance.hello"); await JS.InvokeVoidAsync("index.Index.goodbye"); }
构建并运行。
Bundle Index Hello 按钮演示调用从“index”库导出的索引类方法
在 'Index.razor' html 部分中,将此行添加为最后一个按钮。
<button class="btn btn-primary" @onclick="@ReExportHello">ReExport Hello</button>
在“Index.razor”代码部分添加这个作为最后一个方法。
async void ReExportHello() { await JS.InvokeVoidAsync("index.HelloInstance.hello"); await JS.InvokeVoidAsync("index.Hello.goodbye"); }
构建并运行。
ReExport Hello 按钮演示调用从“index”库导出的 Hello 类方法
总结 本节介绍了从 Webpack 包调用 TypeScript。 包可以通过从包入口模块导出来公开依赖代码。 捆绑包是 Blazor 互操作可通过捆绑包库前缀访问的嵌入式资源。
右键单击项目控制台执行下面的命令,将threejs添加到 package.json
npm i three
创建新的“scripts/cube.ts”文件,将代码复制到“cube.js”。
import * as THREE from 'three'; export class Cube { camera: THREE.PerspectiveCamera; scene: THREE.Scene; renderer: THREE.WebGLRenderer; cube: any; constructor() { this.camera = new THREE.PerspectiveCamera(75, 2, .1, 5); this.camera.position.z = 2; let canvas = document.querySelector('#cube') as HTMLCanvasElement; this.renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true }); this.scene = new THREE.Scene(); this.scene.background = null; const light = new THREE.DirectionalLight(0xFFFFFF, 1); light.position.set(-1, 2, 4); this.scene.add(light); const geometry = new THREE.BoxGeometry(1, 1, 1); const loadManager = new THREE.LoadingManager(); const loader = new THREE.TextureLoader(loadManager); const texBlazor = loader.load('images/blazor.png'); const texInterop = loader.load('images/interop.png'); const texCircle = loader.load('images/tscircle.png'); const matBlazor = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texBlazor, transparent: false, opacity: 1 }); const matInterop = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texInterop, transparent: false, opacity: 1 }); const matCircle = new THREE.MeshPhongMaterial({ color: 0xffffff, map: texCircle, transparent: false, opacity: 1 }); const materials = [matBlazor, matInterop, matCircle, matBlazor, matInterop, matCircle]; loadManager.onLoad = () => { this.cube = new THREE.Mesh(geometry, materials); this.scene.add(this.cube); this.animate(); }; } animate(time = 0) { time = performance.now() * 0.0005; this.cube.rotation.x = time; this.cube.rotation.y = time; this.renderer.render(this.scene, this.camera); requestAnimationFrame(this.animate.bind(this)); } static Create(): void { new Cube(); } }
将多维数据集条目添加到 webpack.config.js用下面的代码段替换 entry: 部分。
entry: { index: ['./scripts/index'], cube:['./scripts.cube.js'] }
在“Index.html”中添加“cube-bundle.js”作为静态资产。
<script src="public/cube-bundle.js"></script>
在“Index.razor”html 部分的末尾添加立方体画布。
<canvas id="cube"/>
将“Index.razor”中的 OnAfterRenderAsync 替换为具有多维数据集互操作调用的以下方法。
protected override async Task OnAfterRenderAsync(bool firstRender) { if(firstRender){ module = await JS.InvokeAsync<IJSObjectReference>("import", $"./js/script.module.js{Version}"); hello = await JS.InvokeAsync<IJSObjectReference>("import", $"./js/hello.js{Version}"); await JS.InvokeVoidAsync("cube.Cube.Create"); } }
构建并运行。
利用 TypeScript 有利于互操作软件设计。 典型的结构设计模式。
TypeScript 转换为浏览器 JavaScript 准备互操作。
Blazor C# 编译为准备执行的 Wasm 浏览器。
Blazor C# 界面设计与 TypeScript 对应物保持一致。
Blazor Wasm 通过可互操作的 JavaScript 与浏览器对话。
原文连接:Trog.NET Blazor Typescript Interop