diff --git a/README.md b/README.md index 93e8242..d73a153 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,32 @@ This strategy will always try the network first for all resources and then fall This strategy is completely safe to use and is primarily useful for offline-only scenarios since it isn't giving any performance benefits. +### CustomStrategy +This strategy will allow the user to specify their own implementation as a Javascript(.js) file. By default the app will search for a file named `customserviceworker.js` in the wwwroot folder. +A filename may be explicitly set by providing it as an option when registering the service in the `Startup.cs` or `appsettings.json` file. + +```C# +public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + services.AddProgressiveWebApp(new PwaOptions { RegisterServiceWorker = true, Strategy = ServiceWorkerStrategy.CustomStrategy, CustomServiceWorkerStrategyFileName = "myCustomServiceworkerStrategy.js"}); + } +``` + +When creating the `customserviceworker.js` by providing {version}, {routes}, {ignoreRoutes} and {offlineRoute} values within the javascript file string, interpolation will be used to replace these values with option values as set in the `Startup.cs` or `appsettings.json` file. + +```javascript +(function () { + //Insert Your Service Worker In place of this one! + + // Update 'version' if you need to refresh the cache + var version = '{version}'; + var offlineUrl = "{offlineRoute}"; + var routes = "{routes}"; + var routesToIgnore = "{ignoreRoutes}"; +}); +``` + ## .Net Core Application hosted as Virtual Directory You can now specify a specific BaseURL if you plan to host your application as a Virtual Directory in IIS: @@ -322,4 +348,4 @@ Make sure to update your `wwwroot/manifest.json` file: ## License -[Apache 2.0](LICENSE) \ No newline at end of file +[Apache 2.0](LICENSE) diff --git a/sample/Startup.cs b/sample/Startup.cs index aeadd1a..f47903f 100644 --- a/sample/Startup.cs +++ b/sample/Startup.cs @@ -30,7 +30,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseBrowserLink(); } - app.UseDeveloperExceptionPage(); + + app.UseDeveloperExceptionPage(); app.UseStaticFiles(); app.UseMvcWithDefaultRoute(); diff --git a/sample/wwwroot/customserviceworker.js b/sample/wwwroot/customserviceworker.js new file mode 100644 index 0000000..db862f5 --- /dev/null +++ b/sample/wwwroot/customserviceworker.js @@ -0,0 +1,9 @@ +(function () { + //Insert Your Service Worker In place of this one! + + // Update 'version' if you need to refresh the cache + var version = '{version}'; + var offlineUrl = "{offlineRoute}"; + var routes = "{routes}"; + var routesToIgnore = "{ignoreRoutes}"; +}); \ No newline at end of file diff --git a/src/Constants.cs b/src/Constants.cs index 3ed9fcf..caf8863 100644 --- a/src/Constants.cs +++ b/src/Constants.cs @@ -3,6 +3,7 @@ internal class Constants { public const string ServiceworkerRoute = "/serviceworker"; + public const string CustomServiceworkerFileName = "customserviceworker.js"; public const string Offlineroute = "/offline.html"; public const string DefaultCacheId = "v1.0"; public const string WebManifestRoute = "/manifest.webmanifest"; diff --git a/src/PwaController.cs b/src/PwaController.cs index bb3851f..52866a7 100644 --- a/src/PwaController.cs +++ b/src/PwaController.cs @@ -2,8 +2,10 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.Semantics; using Microsoft.Net.Http.Headers; namespace WebEssentials.AspNetCore.Pwa @@ -14,13 +16,15 @@ namespace WebEssentials.AspNetCore.Pwa public class PwaController : Controller { private readonly PwaOptions _options; + private readonly RetrieveCustomServiceworker _customServiceworker; /// /// Creates an instance of the controller. /// - public PwaController(PwaOptions options) + public PwaController(PwaOptions options, RetrieveCustomServiceworker customServiceworker) { _options = options; + _customServiceworker = customServiceworker; } /// @@ -33,22 +37,35 @@ public async Task ServiceWorkerAsync() Response.ContentType = "application/javascript; charset=utf-8"; Response.Headers[HeaderNames.CacheControl] = $"max-age={_options.ServiceWorkerCacheControlMaxAge}"; - string fileName = _options.Strategy + ".js"; - Assembly assembly = typeof(PwaController).Assembly; - Stream resourceStream = assembly.GetManifestResourceStream($"WebEssentials.AspNetCore.Pwa.ServiceWorker.Files.{fileName}"); + if (_options.Strategy == ServiceWorkerStrategy.CustomStrategy) + { + string js = _customServiceworker.GetCustomServiceworker(_options.CustomServiceWorkerStrategyFileName); + return Content(InsertStrategyOptions(js)); + } - using (var reader = new StreamReader(resourceStream)) + else { - string js = await reader.ReadToEndAsync(); - string modified = js - .Replace("{version}", _options.CacheId + "::" + _options.Strategy) - .Replace("{routes}", string.Join(",", _options.RoutesToPreCache.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(r => "'" + r.Trim() + "'"))) - .Replace("{offlineRoute}", _options.BaseRoute + _options.OfflineRoute); + string fileName = _options.Strategy + ".js"; + Assembly assembly = typeof(PwaController).Assembly; + Stream resourceStream = assembly.GetManifestResourceStream($"WebEssentials.AspNetCore.Pwa.ServiceWorker.Files.{fileName}"); - return Content(modified); + using (var reader = new StreamReader(resourceStream)) + { + string js = await reader.ReadToEndAsync(); + return Content(InsertStrategyOptions(js)); + } } } + private string InsertStrategyOptions(string javascriptString) + { + return javascriptString + .Replace("{version}", _options.CacheId + "::" + _options.Strategy) + .Replace("{routes}", string.Join(",", _options.RoutesToPreCache.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(r => "'" + r.Trim() + "'"))) + .Replace("{offlineRoute}", _options.BaseRoute + _options.OfflineRoute) + .Replace("{ignoreRoutes}", string.Join(",", _options.RoutesToIgnore.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(r => "'" + r.Trim() + "'"))); + } + /// /// Serves the offline.html file /// diff --git a/src/PwaOptions.cs b/src/PwaOptions.cs index cc953a4..87dbd9e 100644 --- a/src/PwaOptions.cs +++ b/src/PwaOptions.cs @@ -23,6 +23,8 @@ public PwaOptions() EnableCspNonce = false; ServiceWorkerCacheControlMaxAge = 60 * 60 * 24 * 30; // 30 days WebManifestCacheControlMaxAge = 60 * 60 * 24 * 30; // 30 days + CustomServiceWorkerStrategyFileName = Constants.CustomServiceworkerFileName; + RoutesToIgnore = ""; } internal PwaOptions(IConfiguration config) @@ -32,6 +34,9 @@ internal PwaOptions(IConfiguration config) RoutesToPreCache = config["pwa:routesToPreCache"] ?? RoutesToPreCache; BaseRoute = config["pwa:baseRoute"] ?? BaseRoute; OfflineRoute = config["pwa:offlineRoute"] ?? OfflineRoute; + RoutesToIgnore = config["pwa:routesToIgnore"] ?? RoutesToIgnore; + CustomServiceWorkerStrategyFileName = + config["pwa:customServiceWorkerFileName"] ?? CustomServiceWorkerStrategyFileName; if (bool.TryParse(config["pwa:registerServiceWorker"] ?? "true", out bool register)) { @@ -118,5 +123,15 @@ internal PwaOptions(IConfiguration config) /// Generate code even on HTTP connection. Necessary for SSL offloading. /// public bool AllowHttp { get; set; } + + /// + /// The file name of the Custom ServiceWorker Strategy + /// + public string CustomServiceWorkerStrategyFileName { get; set; } + + /// + /// A comma separated list of routes to ignore when implementing a CustomServiceworker. + /// + public string RoutesToIgnore { get; set; } } } diff --git a/src/RetrieveCustomServiceworker.cs b/src/RetrieveCustomServiceworker.cs new file mode 100644 index 0000000..405774e --- /dev/null +++ b/src/RetrieveCustomServiceworker.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.FileProviders; + +namespace WebEssentials.AspNetCore.Pwa +{ + /// + /// A utility that can retrieve the contents of a CustomServiceworker strategy file + /// + public class RetrieveCustomServiceworker + { + private readonly IHostingEnvironment _env; + + public RetrieveCustomServiceworker(IHostingEnvironment env) + { + _env = env; + } + + /// + /// Returns a containing the contents of a Custom Serviceworker javascript file + /// + /// + public string GetCustomServiceworker(string fileName = "customserviceworker.js") + { + IFileInfo file = _env.WebRootFileProvider.GetFileInfo(fileName); + return File.ReadAllText(file.PhysicalPath); + } + } +} diff --git a/src/ServiceCollectionExtensions.cs b/src/ServiceCollectionExtensions.cs index bc61537..b18c7e7 100644 --- a/src/ServiceCollectionExtensions.cs +++ b/src/ServiceCollectionExtensions.cs @@ -19,6 +19,7 @@ public static IServiceCollection AddServiceWorker(this IServiceCollection servic { services.TryAddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddTransient(svc => new PwaOptions(svc.GetRequiredService())); return services; @@ -31,6 +32,7 @@ public static IServiceCollection AddServiceWorker(this IServiceCollection servic { services.TryAddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddTransient(factory => options); return services; @@ -39,10 +41,11 @@ public static IServiceCollection AddServiceWorker(this IServiceCollection servic /// /// Adds ServiceWorker services to the specified . /// - public static IServiceCollection AddServiceWorker(this IServiceCollection services, string baseRoute = "", string offlineRoute = Constants.Offlineroute, ServiceWorkerStrategy strategy = ServiceWorkerStrategy.CacheFirstSafe, bool registerServiceWorker = true, bool registerWebManifest = true, string cacheId = Constants.DefaultCacheId, string routesToPreCache = "") + public static IServiceCollection AddServiceWorker(this IServiceCollection services, string baseRoute = "", string offlineRoute = Constants.Offlineroute, ServiceWorkerStrategy strategy = ServiceWorkerStrategy.CacheFirstSafe, bool registerServiceWorker = true, bool registerWebManifest = true, string cacheId = Constants.DefaultCacheId, string routesToPreCache = "", string routesToIgnore ="", string customServiceWorkerFileName = Constants.CustomServiceworkerFileName) { services.TryAddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddTransient(factory => new PwaOptions { BaseRoute = baseRoute, @@ -51,7 +54,9 @@ public static IServiceCollection AddServiceWorker(this IServiceCollection servic RegisterServiceWorker = registerServiceWorker, RegisterWebmanifest = registerWebManifest, CacheId = cacheId, - RoutesToPreCache = routesToPreCache + RoutesToPreCache = routesToPreCache, + CustomServiceWorkerStrategyFileName = customServiceWorkerFileName, + RoutesToIgnore = routesToIgnore }); return services; @@ -61,7 +66,7 @@ public static IServiceCollection AddServiceWorker(this IServiceCollection servic /// Adds Web App Manifest services to the specified . /// /// The service collection. - /// The path to the Web App Manifest file relative to the wwwroot rolder. + /// The path to the Web App Manifest file relative to the wwwroot folder. public static IServiceCollection AddWebManifest(this IServiceCollection services, string manifestFileName = Constants.WebManifestFileName) { services.AddTransient(); @@ -81,7 +86,7 @@ public static IServiceCollection AddWebManifest(this IServiceCollection services /// Adds Web App Manifest and Service Worker to the specified . /// /// The service collection. - /// The path to the Web App Manifest file relative to the wwwroot rolder. + /// The path to the Web App Manifest file relative to the wwwroot folder. public static IServiceCollection AddProgressiveWebApp(this IServiceCollection services, string manifestFileName = Constants.WebManifestFileName) { return services.AddWebManifest(manifestFileName) @@ -92,7 +97,7 @@ public static IServiceCollection AddProgressiveWebApp(this IServiceCollection se /// Adds Web App Manifest and Service Worker to the specified . /// /// The service collection. - /// The path to the Web App Manifest file relative to the wwwroot rolder. + /// The path to the Web App Manifest file relative to the wwwroot folder. /// Options for the service worker and Web App Manifest public static IServiceCollection AddProgressiveWebApp(this IServiceCollection services, PwaOptions options, string manifestFileName = Constants.WebManifestFileName) { diff --git a/src/ServiceWorker/ServiceWorkerStrategy.cs b/src/ServiceWorker/ServiceWorkerStrategy.cs index 4faa1ee..e5c7c85 100644 --- a/src/ServiceWorker/ServiceWorkerStrategy.cs +++ b/src/ServiceWorker/ServiceWorkerStrategy.cs @@ -29,6 +29,11 @@ public enum ServiceWorkerStrategy /// /// Always tries the network first and falls back to cache when offline. /// - NetworkFirst + NetworkFirst, + + /// + /// Allows a user defined custom strategy to be provided. + /// + CustomStrategy } }