I am trying to super-optimize a PWA by combining and minifying as much as I can. My application is based mostly on a google tutorial on service workers - and as such I have code such as this in my service worker:
var filesToCache = [
'/',
'/index.html',
'/scripts/app.js',
'/styles/inline.css'
];
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
I have a gulpfile.js which, among other things uses gulp-smoosher to inline my css during build:
<!-- smoosh -->
<link rel="stylesheet" type="text/css" href="styles/inline.css">
<!-- endsmoosh -->
Which works great - it inlines my css directly into the HTML - however clearly my filesToCache in my serviceworker now has an entry which wont exist in the build
var filesToCache = [
'/',
'/index.html',
'/scripts/app.js',
'/styles/inline.css' // <!--- this shouldn't be here in the build
];
Is there any options, using a gulp task or otherwise (perhaps some sort of config for this which can be updated on build) to resolve this issue?
I ended up solving this by making the following changes.
Move the filesToCache variable to its own json file - filesToCache.json
Update my service worker to load that file in during install
Use gulp-json-editor to manipulate the file on build.
Code in gulpfile
const jsonEditor = require('gulp-json-editor');
// snip
gulp.task("filesToCache", function(){
var out = folder.build;
return gulp.src(folder.src + "filesToCache.json")
.pipe(jsonEditor(function(json){
json.splice(json.indexOf("/styles/inline.css"),1);
return json;
}))
.pipe(gulp.dest(out));
});
Code in service worker
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
return fetch("/filesToCache.json").then(function(response){
if(response && response.ok){
return response.json()
}
throw new Error("Failed to load files to cache for app shell")
})
.then(function(filesToCache){
console.log('[ServiceWorker] Caching app shell', filesToCache);
return cache.addAll(filesToCache);
})
.catch(function(error){
console.error(error)
})
})
);
});
Hope this helps someone in future!
Related
I am trying to transfer an old node-express project over to be able to use es6. I have seen many posts about using gulp with es6. Most of them discuss using a syntax like this:
const gulp = require("gulp");
const babel = require("gulp-babel");
gulp.src('./index.js')
.pipe(
babel({
presets: [
["#babel/env", { modules: false }],
],
})
)
However my existing project's gulpfile does't use gulp.src at all. Instead, it uses gulp-develop-server. The gulpfile looks like this:
const gulp = require("gulp");
const devServer = require("gulp-develop-server");
const spawn = require("child_process").spawn;
const fs = require("fs");
const basedir = ".";
function serverRestart(done) {
// perform some cleanup code here
devServer.restart();
done();
}
function serverStart() {
devServer.listen({
path: basedir + "/index.js",
});
}
function serverWatch() {
serverStart();
gulp.watch(
[
basedir + "/paths/**/*",
// more directories to watch
],
serverRestart
);
}
function reload(done) {
serverWatch();
done();
}
function defaultTask() {
let p;
gulp.watch(["gulpfile.js"], killProcess);
spawnChild();
function killProcess(e) {
if (p && !p.killed) {
devServer.kill();
p.kill("SIGINT");
spawnChild();
}
}
function spawnChild() {
p = spawn("gulp", ["reload"], { stdio: "inherit" });
}
}
process.stdin.resume();
process.on("exit", handleExit.bind(null, { cleanup: true }));
process.on("SIGINT", handleExit.bind(null, { exit: true }));
process.on("uncaughtException", handleExit.bind(null, { exit: true }));
function handleExit(options, err) {
// perform some cleanup code here
if (options.cleanup) {
devServer.kill();
}
if (err) {
console.log(err.stack);
}
if (options.exit) {
process.exit();
}
}
gulp.task("serverRestart", serverRestart);
gulp.task("serverStart", serverStart);
gulp.task("serverWatch", serverWatch);
gulp.task("reload", reload);
gulp.task("default", defaultTask);
The existing flow is important because it executes needed code for setup and cleanup every time I hit save, which runs serverRestart. I've been trying a few different methods based on the other questions which recommended using gulp.src().pipe(), but I havne't had much luck integrating it with the existing pattern which uses gulp-develop-server. I am trying to not have to rewrite the whole gulpfile. Is there a simple way to integrate babel with my existing gulpfile such that I can use es6 in my source code?
There's an example with CoffeeScript in the gulp-develop-server documentation.
Using that as a model, try this:
function serverStart() {
devServer.listen({
path: "./dist/index.js",
});
}
function serverWatch() {
serverStart();
gulp.watch(
[
basedir + "/paths/**/*",
],
serverRestart
);
}
function serverRestart() {
gulp.src('./index.js')
.pipe(
babel({
presets: [
["#babel/env", { modules: false }],
],
})
)
.pipe( gulp.dest( './dist' ) )
.pipe( devServer() );
}
Other suggestions
That being said, your existing Gulp file doesn't actually really use Gulp. That is, everything is defined as a function and it doesn't leverage any of Gulp's useful features, like managing task dependencies. This is because (pre-es6), this was a very simple project. The Gulp tasks in that file are an over-elaborate way to watch files and run a server. The same could be done (with less code) using nodemon.
With the introduction of React and more complicated build processes, Gulp seems to have fallen out of favor with the community (and in my personal experience, Gulp was a time sinkhole anyhow).
If the main change you want to make is to use import, you can simply use a more recent Node version. You'll surely run into the error SyntaxError: Cannot use import statement outside a module. Simply rename the file to .mjs and it will work. This provides a way to incrementally migrate files to import syntax. Other features should automatically work (and are all backwards-compatible, anyhow). Once your project is mostly, or all, compliant, you can add "type": "module" to your package.json file, then rename all of your require-style js files to .cjs, and rename all of your .mjs files to .js, or leave them as .mjs. Read more about the rules of mixing CommonJS and Module imports in the Node.js blog post (note that some things may have changed since that article was written).
I have two different projects (or repo) on the server. one of them is wroten with vue.js (SPA) and another is created by static site generator (mkdoc).
then I add PWA to my SPA project (from here) and a strange thing happened🙁. in my project, I navigate to example.com/help but it shown a 404 page while /help route is for my mddoc project!
in other words:
SPA Project domain: example.com
mkdoc Project domain: example.com/help
so I searched on the web and thought about this problem and I realized why this problem happens!
I think that's because of the service worker in PWA! the service worker cached all the files and assets of the project.
now I think to solve this problem I should prevent cache the /help route! but I couldn't understand how to config PWA in vue.config.js vue cli.
really I don't know that can we exclude one route and prevent to cache with the service worker or not!?
how can I fix this problem!?
vue.config.js
module.exports = {
publicPath: '/',
css: {
loaderOptions: {
sass: {
prependData: `
#import "#/assets/styles/_boot.scss";
`
}
}
},
pwa: {
name: "app",
themeColor: "#004581",
msTileColor: "white",
appleMobileWebAppCapable: "yes",
appleMobileWebAppStatusBarStyle: "white",
iconPaths: {
favicon32: 'img/icons/favicon-32x32.png',
favicon16: 'img/icons/favicon-16x16.png',
appleTouchIcon: 'img/icons/apple-touch-icon-180x180.png',
maskIcon: 'img/icons/safari-pinned-tab.svg',
msTileImage: 'img/icons/mstile-144x144.png',
},
// configure the workbox plugin
workboxPluginMode: "GenerateSW",
workboxOptions: {
importWorkboxFrom: "local",
navigateFallback: "/",
ignoreUrlParametersMatching: [/help*/],
// Do not precache images
exclude: [/\.(?:png|jpg|jpeg|svg)$/],
// Define runtime caching rules
runtimeCaching: [
{
// Match any request that ends with .png, .jpg, .jpeg or .svg.
urlPattern: /\.(?:png|jpg|jpeg|svg)$/,
// Apply a cache-first strategy.
handler: "CacheFirst",
options: {
// Use a custom cache name.
cacheName: "images",
// Only cache 10 images.
expiration: {
maxEntries: 10
}
}
}
]
}
}
};
registerServiceWorker.js
/* eslint-disable no-console */
import { register } from 'register-service-worker';
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa'
);
},
registered() {
console.log('Service worker has been registered.');
},
cached() {
console.log('Content has been cached for offline use.');
},
updatefound() {
console.log('New content is downloading.');
},
updated() {
console.log('New content is available; please refresh.');
},
offline() {
console.log('No internet connection found. App is running in offline mode.');
},
error(error) {
console.error('Error during service worker registration:', error);
}
});
}
If you want to exclude specific route or routes from service worker you should add navigateFallbackDenylist to workboxOptions.
navigateFallbackDenylist: [/help*/],
I am trying to pre-cache some of my static app shell files using service worker. I can't use 'sw-appcache' as I am not using any build system. However, I tried using 'sw-toolbox' but I am not being able to use it for pre-caching.
Here is what I am doing in my service worker JS:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('gdvs-static').then(function(cache) {
var precache_urls = [
'/',
'/?webapp=true',
'/css/gv.min.css',
'/js/gv.min.js'
];
return cache.addAll(precache_urls);
});
);
});
I've also tried this:
importScripts('/path/to/sw-toolbox.js');
self.addEventListener('install', function(event) {
var precache_urls = [
'/',
'/?webapp=true',
'/css/gv.min.css',
'/js/gv.min.js'
];
toolbox.precache(precache_urls);
});
Here is the URL of my app: https://guidedverses-webapp-staging.herokuapp.com/
Here is the URL of my service worker file: https://guidedverses-webapp-staging.herokuapp.com/gdvs-sw.js
What am I missing?
Silly me. I forgot to add the fetch handler into my service worker. I thought it works like appchache and automatically returns the cached data when matches with the cache URL. I underestimated the power of Service Worker. Following code did the trick for me.
this.addEventListener('fetch', function(event) {
console.log(event.request.url);
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
I'm trying to set the current Git SHA in my project's Grunt configuration, but when I try to access it from another task it isn't available, What am I missing?
grunt.registerTask('sha', function () {
var done = this.async();
grunt.util.spawn({
cmd: 'git',
args: ['rev-parse', '--short', 'HEAD']
}, function (err, res) {
if (err) {
grunt.fail.fatal(err);
} else {
grunt.config.set('git', {sha: res.stdout});
if (grunt.option('debug') || grunt.option('verbose')) {
console.log("[sha]:", res.stdout);
}
}
done();
});
});
After running the task, I expect the config to be available in another task configuration:
requirejs: {
dist: {
...
out: '<%= app.dist %>/scripts/module_name.<%= git.sha %>.js'
...
}
}
So... What's the problem?
The problem is that Require JS is writing to the file public/scripts/module_name..js, the SHA is not available in the configuration (when the filename should be public/scripts/module_name.d34dc0d3.js).
UPDATE:
The problem is that I'm running requirejs tasks with grunt-concurrent, so the Grunt configuration is not available for requirejs.
grunt.registerTask('build', [
...
'getsha',
'concurrent:dist',
...
]);
And the concurrent task, looks like:
concurrent: {
dist: [
...
'requirejs',
...
]
}
Since grunt-concurrent will spawn tasks in child processes, they do not have access to the context of the parent process. Which is why doing grunt.config.set() within the parent context is not available in the config of the child context.
Some of the solutions to make the change available in the child context are:
Write the data to the file system
Write the data to a temporary file with grunt.file.write('./tmp/gitsha', res.stdout) and then have the task being ran in a child process read the temporary file:
out: (function() {
var out = grunt.config('app.dist') + '/scripts/module_name.';
if (grunt.file.exists('./tmp/gitsha')) {
out += grunt.file.read('./tmp/gitsha');
} else {
out += 'unknown';
}
return out + '.js';
}())
Use a socket
This is a very convoluted solution but a solution nonetheless. See the net node docs: http://nodejs.org/api/net.html#net_net_createserver_options_connectionlistener on creating a server on the parent process then have the child process connect to the socket for the data.
Or check out https://github.com/shama/blackbox for a library that makes this method a bit simpler.
Fork the parent process instead of spawn/exec
Another method is to use fork: http://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options instead of grunt-concurrent. Fork lets you send messages to child processes with child.send('gitsha') and receive them in the child with process.on('message', function(gitsha) {})
This method also can get very convoluted.
Use a proxy task
Have your sha task set the config as you're currently doing:
grunt.registerTask('sha', function() {
grunt.config.set('git', { sha: '1234' });
});
Change your concurrent config to call a proxy task with the sha:
grunt.initConfig({
concurrent: {
dist: [
'proxy:requirejs:<%= git.sha %>'
]
}
});
Then create a proxy task that runs a task with setting the passed value first:
grunt.registerTask('proxy', function(task, gitsha) {
grunt.config.set('git', { sha: gitsha });
grunt.task.run(task);
});
The above can be simplified to set values specifically on requirejs but just shown here as a generic example that can be applied with any task.
I have a web application that uses requirejs to load its modules. The web applications works without problems in any desktop browser, it also works on iOS and Android when packaged with Cordova. Does however NOT work when building a Windows Phone 8 Cordova application.
I get the following error:
"View Not Found: Searched for "views/shell" via path "text!views/shell.html"
(I'm using Durandal)
I have the following application structure:
lib/require/require.js
www/app/viewmodels/shell.js
www/app/views/shell.html
www/app/main.js
www/index.html (contains line: )
:
www/app/main.js contains the following code:
requirejs.config({
//baseUrl: 'x-wmapp0:www/app',
baseUrl: 'app',
enforceDefine: true,
paths: {
'text': '../lib/require/text',
'durandal':'../lib/durandal/js',
'plugins' : '../lib/durandal/js/plugins',
'transitions' : '../lib/durandal/js/transitions',
'knockout': '../lib/knockout/knockout-2.3.0',
'bootstrap': '../lib/bootstrap3/js/bootstrap',
'jquery': '../lib/jquery/jquery-1.9.1',
'modules' : 'modules'
},
shim: {
'bootstrap': {
deps: ['jquery'],
exports: 'jQuery'
}
}
});
define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'bootstrap'], function (system, app, viewLocator, bootstrap) {
//>>excludeStart("build", true);
system.debug(true);
//>>excludeEnd("build");
app.title = 'MyApp';
app.configurePlugins({
router: true,
dialog: true,
widget: true
});
app.start().then(function() {
//Replace 'viewmodels' in the moduleId with 'views' to locate the view.
//Look for partial views in a 'views' folder in the root.
viewLocator.useConvention('viewmodels', 'views', 'views');
//Show the app by setting the root view model for our application with a transition.
app.setRoot('viewmodels/shell', 'entrance', 'applicationHost');
});
});
This code works perfectly on all devices except WP8. I tried the line baseUrl: 'x-wmapp0:www/app' to set the actual path used internally by WP8 Cordova, but that does not work. Is that because it is not a path starting with '/' or http?
Any ideas on how to configure requirejs to be able to load modules and views using requirejs?
See if this is any help to you http://mikaelkoskinen.net/durandal-phonegap-windows-phone-8-tutorial/
Also, as of the 18th October 2013, Phonegap Build has added beta support for WP8. Add "winphone: as the gap:platform and ensure you're using phonegap 3.0. We're currently having no luck but maybe you can use that.
I just solve this problem for myself as well, what I ended up doing was waiting for the deviceloaded event before I loaded my main.js.
Cordova applies a patch to XMLHttpRequest which has been applied when deviceloaded has been triggered.
In my app I it ended up looking similar to this:
<script src="js/require.js"></script>
<script>
document.addEventListener('deviceloaded', function() {
var script = document.createElement('script');
script.src = 'js/main.js';
document.body.appendChild(script);
}, false);
</script>
I hope that works out for you as well!
One other thing, I noticed in my app that if I try to load weinre I couldn't get my app started at all with require.js. I haven't been able to research this further yet though.
I recommend to set a breakpoint to cordovalib/XHRHelper.cs HandleCommand method and see what is going on. Also the following version of HandleCommand could work better for you
public bool HandleCommand(string commandStr)
{
if (commandStr.IndexOf("XHRLOCAL") == 0)
{
string url = commandStr.Replace("XHRLOCAL/", "");
Uri uri = new Uri(url, UriKind.RelativeOrAbsolute);
try
{
using (IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStoreForApplication())
{
if (isoFile.FileExists(uri.AbsolutePath))
{
using (
TextReader reader =
new StreamReader(isoFile.OpenFile(uri.AbsolutePath, FileMode.Open, FileAccess.Read))
)
{
string text = reader.ReadToEnd();
Browser.InvokeScript("__onXHRLocalCallback", new string[] { "200", text });
return true;
}
}
}
url = uri.AbsolutePath;
}
catch { }
var resource = Application.GetResourceStream(new Uri(url, UriKind.Relative)) ??
// for relative paths and app resources we need to add www folder manually
// there are many related issues on stackoverflow + some prev worked sample don't because of this
Application.GetResourceStream(new Uri("www/" + url, UriKind.Relative));
if (resource == null)
{
// 404 ?
Browser.InvokeScript("__onXHRLocalCallback", new string[] { "404" });
return true;
}
else
{
using (StreamReader streamReader = new StreamReader(resource.Stream))
{
string text = streamReader.ReadToEnd();
Browser.InvokeScript("__onXHRLocalCallback", new string[] { "200", text });
return true;
}
}
}
return false;
}