Javascript interop in Blazor web assembly - part 2 - Javascript Isolation

...

In the previous article we ended up making a very basic javascript call from our .NET code using javascript interop via the injected IJSRuntime service. This approach already opens us up to extending our Blazor apps capabilities and allows us to take advantage of the vast ecosystem of existing javascript libraries and the functionality they provide. However, it does come with some issues. To my mind the biggest of these is that the javascript functions must be on the global window object. This exposes us to the possibility of some other code using the same name for a function. In this case our function may get overwritten by someone elses, or we may overwrite someone elses function. Additionally, our function becomes available for everyone. We may or may not want this.

This kind of issue is one of the reasons I have disliked Javascript. However, over the last few years, particularly in the process of the current project I'm working on, I've realised that there have been some great improvements. One of these improvements is the introduction of Modules.

What are Javascript Modules?

Back in 2015 Javascript underwent a pretty radical change. A new specification was released with lots of additional functionality. Known originally as ES6 or subsequently ES2015, this release included the concept of modules. A module is a Javascript file that exported one or more of it's functions. All the other functions remained internal to that file. The module could then be imported by other files but could only use the functions that had explicitly been exported. If you're reading this article about Blazor, I'm guessing that like me, you are a C# programmer; in which case a module is kind of like a class with public and private methods on it. Those exported are the public ones and the others are private. 

Modules are a massive step forward and the great news is that Blazor now supports them through a process called Javascript isolation. What's fantastic about this, is that it allows us to write C# that calls out to a javascript file without us having to add that file to our index.html.

So if we continue from where we left off in the previous  article we had an index.js file that contained

function showJavascriptAlert(message) {
    alert(message);
}

now in order to turn this into a module all we have to do is export that function. But just to prove a point we'll do something slightly different. We'll export a new function that calls the existing one.

function showJavascriptAlert(message) {
    alert(message);
}
 
export function showJavascriptAlertExported(message) {
    showJavascriptAlert(message);
}

Now if you run the application and press the button, instead of getting the pop up we get an error. Checking the console window you'll find the same error message as we had in the last article before we referenced the javascript file. It can no longer find the javascript function its trying to call. By exporting a function in the file, it's turned the file into a module and all non exported functions become private.

So what if we update the function we are trying to call through interop to be the exported and therefore public one?

so in index.razor change the code to

@page "/"
 
 
<h1>Hello, world!</h1>
 
 
 
<button type="button" class="btn btn-info" @onclick="CallSomeJavascript">Call Some Javascript</button>
 
@code
{
 
    [Inject]
    public IJSRuntime JSRuntime { get; set; }
    public async Task CallSomeJavascript()
    {
        
        await JSRuntime.InvokeVoidAsync("showJavascriptAlertExported", "Congratulations! You called some Javascript from .NET");
    }
 
}

This still has a similar error and can't find the function. 

Well remember we compared a module to being kind of like a C# class? Well at the moment we are trying to call a public method on the class before instantiating an instance of the class. (This isn't exactly the case, but as an analogy it works for me)

So  before we can call it's methods we have to import the module. 

@code
{
 
    [Inject]
    public IJSRuntime JSRuntime { get; set; }
    public async Task CallSomeJavascript()
    {
 
            var    module = await JSRuntime.InvokeAsync<IJSObjectReference>(
                        "import", "/js/index.js");     
              
        await module.InvokeVoidAsync("showJavascriptAlertExported", "Congratulations! You called some Javascript from .NET");
 
    }
 
}

You'll notice we have two calls. The first is to import the file. The second is to call the public function on that file. Note we are no longer using the JSRuntime for the second call. In the first call we import the file and store a reference to it - of type IJSObjectReference.  As previously the first parameter is the Javascript function (import) and the subsequent parameters are those to be passed to that function, In this case we are passing the file location. If you run the app now, the button will work correctly again.

However, we still have one more thing to do. We are still referencing the javascript file in index.html.

Remove the following line from index.html

<script src="js/index.js"></script>

When we run the app now it still runs. The only reference to the javascript file is in our code at the point we import it. There is no risk of polluting the global namespace from our javascript file. Next up we'll extract this into a reusable component. We can reuse this on several pages and no-one need ever know we are using javascript.