In my previous post, I discussed the JavaScript interop feature in Blazor WebAssembly (WASM) and how to leverage it for the Chrome extension development. In this post, I'm going to expand the same extension to support cross-browser compatibility, like Mozilla Firefox, and what to consider for compatibility.
You can download the sample Chrome extension from this GitHub repository:
This provides sample codes for a Chrome Extension app built on Blazor WASM
Blazor WASM Browser Extension Sample
This provides sample codes for a cross-browser extension app built on Blazor WASM. This sample app originally started for building a Chrome extension with Blazor WASM, but it now does the cross-browser support including Chromium-based browsers and Mozilla FireFox.
The Browser Extension Working Group at W3.org proposes the web standards based on the Chrome extension manifest, which supports all web browsers. Based on that proposal, Mozilla has released the Browser Extension Polyfill library that supports the modern promise pattern instead of callback. Therefore, if you import this polyfill library, theoretically, your Chrome extension quickly turns into the browser extension that runs on multiple browser engines.
Therefore, add the polyfill library through CDN to index.html like below. The link takes the latest release of the polyfill.
<!DOCTYPE html><htmllang="en">
...
<body><divid="app">Loading...</div>
...
<!-- ⬇️⬇️⬇️ Add this line ⬇️⬇️⬇️ --><script src="https://unpkg.com/browse/webextension-polyfill/dist/browser-polyfill.min.js"></script><!-- ⬆️⬆️⬆️ Add this line ⬆️⬆️⬆️ --><script src="_framework/blazor.webassembly.js"></script></body></html>
However, you can't use the direct link to the polyfill because of the "Content Security Policy" violation error.
In order to sort out this issue, you should download the JavaScript file and give its local reference. Visit the CDN page and download each files and save them into the wwwroot/js/dist directory. Then, update the index.html file like below:
<!DOCTYPE html><htmllang="en">
...
<body><divid="app">Loading...</div>
...
<!-- ⬇️⬇️⬇️ Add this line ⬇️⬇️⬇️ --><script src="js/dist/browser-polyfill.min.js"></script><!-- ⬆️⬆️⬆️ Add this line ⬆️⬆️⬆️ --><script src="_framework/blazor.webassembly.js"></script></body></html>
Now, it's OK to use the polyfill. Next, let's move on to manifest.json for cross-browser support.
manifest.json Accommodation
First of all, you need to get rid of Chrome's exclusive features. Especially, as the Declarative Content part only works for the Chrome extensions, you need to replace it with other approaches. In this example, if you want the extension to only work with the domain like developer.chrome.com, developer.mozilla.org or docs.microsoft.com, add domain URLs to the permissions collection like below and remove the declarativeContent value from it.
Updating manifest.json is over. Now, let's move on to the JavaScript files.
background.js Update
For background.js, you must change all the chrome. instances to the browser. ones because the chrome instances are specific to the Chrome extension, while the browser instances are for the browser extensions in general. In other words, if your background.js looks like the following:
chrome.runtime.onInstalled.addListener(function(){chrome.storage.sync.set({color:'#3aa757'},function(){console.log("The color is green.");});chrome.declarativeContent.onPageChanged.removeRules(undefined,function(){chrome.declarativeContent.onPageChanged.addRules([{conditions:[newchrome.declarativeContent.PageStateMatcher({pageUrl:{hostEquals:'developer.chrome.com'},}),newchrome.declarativeContent.PageStateMatcher({pageUrl:{hostEquals:'docs.microsoft.com'},})],actions:[newchrome.declarativeContent.ShowPageAction()]}]);});});
All chrome. declared above must be replaced with browser. like below:
// Use 'browser.' instead of 'chrome.'browser.runtime.onInstalled.addListener(function(){// Use 'browser.' instead of 'chrome.'browser.storage.sync.set({color:'#3aa757'},function(){console.log("The color is green.");});// Use 'browser.' instead of 'chrome.'browser.declarativeContent.onPageChanged.removeRules(undefined,function(){// Use 'browser.' instead of 'chrome.'browser.declarativeContent.onPageChanged.addRules([{conditions:[// Use 'browser.' instead of 'chrome.'newbrowser.declarativeContent.PageStateMatcher({pageUrl:{hostEquals:'developer.browser.com'},}),// Use 'browser.' instead of 'chrome.'newbrowser.declarativeContent.PageStateMatcher({pageUrl:{hostEquals:'docs.microsoft.com'},})],// Use 'browser.' instead of 'chrome.'actions:[newbrowser.declarativeContent.ShowPageAction()]}]);});});
Once you replace all the chrome instances with browser ones, change all the callback patterns to promise patterns. For example, this example requires the browser's local storage access. How can it be changed?
// Beforebrowser.storage.sync.set({color:'#3aa757'},function(){console.log("The color is green.");});// Afterbrowser.storage.sync.set({color:'#3aa757'}).then(()=>{console.log("The color is green.");});
It's written as an anonymous function and added as the event handler to the onInstalled event. Therefore, it's better to create an independent function like the below:
functionhandleRuntimeOnInstalled(details){browser.storage.sync.set({color:'#3aa757'}).then(()=>{console.log("The color is green.");});}
As mentioned above, we can't use the Declarative Content feature any longer. Hence, instead of the events that belong to the declarativeContent property, you need other events and event handlers. This sample extension only works in specific domains like developer.google.com, developer.mozilla.org or docs.microsoft.com, and the declarativeContent property performs this sort of detection. Therefore, replace it with the handleTabs() function like below, using the pageAction property.
Change all callback patterns to the promise patterns.
letchangeColor=document.getElementById('changeColor');// Use 'browser.' instead of 'chrome.'// Use the promise patternbrowser.storage.sync.get('color').then((data)=>{changeColor.style.backgroundColor=data.color;changeColor.setAttribute('value',data.color);});changeColor.onclick=function(element){letcolor=element.target.value;// Use 'browser.' instead of 'chrome.'// Use the promise patternbrowser.tabs.query({active:true,currentWindow:true}).then((tabs)=>{letmatched=tabs[0].url.includes('developer.chrome.com')||tabs[0].url.includes('developer.mozilla.org')||tabs[0].url.includes('docs.microsoft.com');if (matched){// Use 'browser.' instead of 'chrome.'browser.tabs.executeScript(tabs[0].id,{code:'document.body.style.backgroundColor = "'+color+'";'});}else{console.log('URL not matched');}});};
The popup.js is done.
options.js Update
This time, it's the turn for options.js, which initially looks like:
letpage=document.getElementById('buttonDiv');constkButtonColors=['#3aa757','#e8453c','#f9bb2d','#4688f1'];functionconstructOptions(kButtonColors){for (letitemofkButtonColors){letbutton=document.createElement('button');button.className='color-button';button.style.backgroundColor=item;button.style.padding='10px';button.addEventListener('click',function(){chrome.storage.sync.set({color:item},function(){console.log('color is '+item);})});page.appendChild(button);}}constructOptions(kButtonColors);
Replace all chrome. instances with browser. ones.
Change all callback patterns to the promise patterns.
letpage=document.getElementById('buttonDiv');constkButtonColors=['#3aa757','#e8453c','#f9bb2d','#4688f1'];functionconstructOptions(kButtonColors){for (letitemofkButtonColors){letbutton=document.createElement('button');button.className='color-button';button.style.backgroundColor=item;button.style.padding='10px';button.addEventListener('click',function(){// Use 'browser.' instead of 'chrome.'// Use the promise patternbrowser.storage.sync.set({color:item}).then(()=>{console.log('color is '+item);})});page.appendChild(button);}}constructOptions(kButtonColors);
The options.js file is updated.
Blazor Component Abstraction
Suppose you want to get rid of importing the browser polyfill from index.html and import it directly from the Blazor components. In that case, it's a good idea to create a common page component that each Popup.razor and Options.razor can inherit. First of all, declare a LoadAdditionalJsAsync() method that is called within the OnAfterRenderAsync(...) method.
Then, invoke the method at the end of OnAfterRenderAsync(...). By doing so, the OnAfterRenderAsync(...) method first imports the js/main.js followed by the browser polyfill script, then import the page-specific JavaScript files.
publicclassPageComponentBase:ComponentBase{[Inject]privateIJSRuntimeJS{get;set;}protectedIJSObjectReferenceModule{get;privateset;}protectedoverrideasyncTaskOnAfterRenderAsync(boolfirstRender){this.Module=awaitthis.JS.InvokeAsync<IJSObjectReference>("import","./js/main.js");varsrc="js/dist/browser-polyfill.min.js";awaitthis.Module.InvokeVoidAsync("loadJs",src);// Invoke the page-specific JavaScript loaderawaitthis.LoadAdditionalJsAsync();}protectedabstractTaskLoadAdditionalJsAsync();}
With the Popup.razor page, inherit the PageComponentBase class and implement the LoadAdditionalJsAsync() method like below, which imports the js/popup.js file. You don't need the IJSRuntime instance as a dependency any longer, so remove it.
Similarly, the Options.razor page also inherits the PageComponentBase class and implement the LoadAdditionalJsAsync() method to import js/options.js. The IJSRuntime dependency is no longer necessary, either.
Unlike the Chrome extensions, the Mozilla-based web extension needs a .zip file for installation. Therefore, the PowerShell script, Run-PostBuild.ps1, requires an additional step to generate a .zip file as an artifact.
Once you complete all the steps above, build the project and run the PowerShell script, and it will generate the wwwroot.zip file under the published directory. Next, install the zip file to Firefox, and you will see the screen below:
You can see the options page as a pop-up modal due to the manifest.json update. In the options modal, change the background colour to yellow.
Then, you are able to change the background colour to yellow.
So far, we've walked through how our Blazor WASM-based chrome extension can support cross-browser compatibility and showed how it works on the Firefox browser.