How to use Laravel Mix and WorkBox? - laravel

I'm trying to build a PWA for my app; and have spent almost 48 hours trying to figure out how to make use of Workbox with Laravel Mix. What's ironical is that Google says Workbox is meant to make things easy!
Buh!
Okay, so far I've figured out that -
I will need to use the InjectManifest Plugin because I want to integrate Push notifications service in my Service Worker
I have no clue how to specifiy the paths for swSrc and swDest.
What code should go into my webpack.mix.js and whether I should have a temporary service-worker inside my resources/js folder to create a new service worker inside public/ folder.
Can someone help?
PS: I've read almost every blog and help article; but none talks about reliably using Workbox with Laravel mix. Would really appreciate some help here.

I have done a lot of research into this recently and whilst this may not be a full answer to your question, it should give you, or anyone else visiting this page, enough guidance to get started...
I will add to this answer as I learn and research more.
For the purposes of this answer, I will assume your service worker is called service-worker.js, however, you can obviously call it whatever you like.
Step 1 - Laravel Mix
Assuming you are using Dynamic Importing in your project (if you aren't, you should be), you will need to downgrade Laravel Mix to version 3. There is an acknowledged bug in Laravel Mix 4 that prevents CSS from bundling correctly and this will not be fixed until Webpack 5 is released.
In addition, the steps outlined in this answer are specifically configured for Laravel Mix 3.
Step 2 - Import or ImportScripts
The second issue to solve is whether to utilise the workbox-webpack-plugin for injecting the workbox global using importScripts or whether you should disable this (using importWorkboxFrom: 'disabled') and just individually import the specific modules you need...
The documentation states:
When using a JavaScript bundler, you don't need (and actually shouldn't use) the workbox global or the workbox-sw module, as you can import the individual package files directly.
This implies that we should be using import instead of injecting the importScripts.
However, there are various issues here:
We do not want service-worker.js to be included in the build manifest as this will be injected into the precache manifest
We do not want service-worker.js to be versioned in production i.e. the name should always be service-worker.js, not service-worker.123abc.js.
InjectManifest will fail to inject the manifest because the service-worker.js file will not exist at the time that it runs.
Therefore, in order to utilise import instead of importScripts, we must have two separate webpack (mix) configurations (see conclusion for guidance on how to do this). I am not 100% certain this is correct, but I will update my answer once I have received an answer to either of the following (please support them to increase chance of receiving an answer):
How to use Workbox with a Bundler (Webpack etc.)
https://github.com/GoogleChrome/workbox/issues/2207
Step 3 - File Structure
Assuming you are using InjectManifest and not GenerateSW, you will need to write your own service worker which will have the JS manifest injected into it by the webpack plugin on each build. This, quite simply, means you need to create a file in your source directory that will be used as the service worker.
Mine is located at src/js/service-worker.js (this will be different if you are building in a full Laravel project, I am simply using Laravel Mix in a standalone app)
Step 4 - Registering the Service Worker
There are various ways to do this; some like to inject inline JS into the HTML template, but others, myself included, simply register the service worker at the top of their app.js. Either way, the code should look something along the lines of:
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js');
});
}
Step 5 - Writing your Service Worker; workbox Global, or Module Importing
As mentioned in the previous quote from the documentation, it is encouraged to import the specifically required modules into your service worker, instead of utilising the workbox global or workbox-sw module.
For more information on how to use the individual modules, and how to actually write your service worker, see the following documentation:
https://developers.google.com/web/tools/workbox/guides/using-bundlers
Conclusion
Based on all of my research (which is still ongoing), I have taken the following approach outlined below.
Before reading, please bear in mind that this is configured for a standalone static PWA (i.e. not a full Laravel project).
/src/service-worker.js (the service worker)
When using a bundler such as webpack, it is advised to utlilise import to ensure you include only the necessary workbox modules. This is my service worker skeleton:
import config from '~/config'; // This is where I store project based configurations
import { setCacheNameDetails } from 'workbox-core';
import { precacheAndRoute } from 'workbox-precaching';
import { registerNavigationRoute } from 'workbox-routing';
// Set the cache details
setCacheNameDetails({
prefix: config.app.name.replace(/\s+/g, '-').toLowerCase(),
suffix: config.app.version,
precache: 'precache',
runtime: 'runtime',
googleAnalytics: 'ga'
});
// Load the assets to be precached
precacheAndRoute(self.__precacheManifest);
// Ensure all requests are routed to index.html (SPA)
registerNavigationRoute('/index.html');
/package.json
Splitting the Mix configuration
"scripts": {
"development": "npm run dev-service-worker && npm run dev-core",
"dev": "npm run development",
"dev-service-worker": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=service-worker.mix",
"dev-core": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=core.mix",
"watch": "npm run dev-core -- --watch",
"watch-poll": "npm run watch -- --watch-poll",
"production": "npm run prod-service-worker && npm run prod-core",
"prod": "npm run production",
"prod-service-worker": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=service-worker.mix",
"prod-core": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --env.mixfile=core.mix"
}
Command Explanation
All standard commands will work in the same way as usual (i.e. npm run dev etc.). See known issue about npm run watch
npm run <environment>-service-worker will build just the service worker in the specified environment
npm run <environment>-core will build just the core application in the specified environment
Known Issues
If you are using an html template that utilises the webpack manifest then you may have issues with npm run watch. I have been unable to get this to work correctly as of yet
Downgrading to Laravel Mix 3
"devDependencies": {
"laravel-mix": "^3.0.0"
}
This can also be achieved by running npm install laravel-mix#3.0.0
/static/index.ejs
This HTML template is used to generate the single page application index.html. This template is dependant on the webpack manifest being injected.
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" class="no-js">
<head>
<!-- General meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="<%= config.meta.description %>">
<meta name="rating" content="General">
<meta name="author" content="Sine Macula">
<meta name="robots" content="index, follow">
<meta name="format-detection" content="telephone=no">
<!-- Preconnect and prefetch urls -->
<link rel="preconnect" href="<%= config.api.url %>" crossorigin>
<link rel="dns-prefetch" href="<%= config.api.url %>">
<!-- Theme Colour -->
<meta name="theme-color" content="<%= config.meta.theme %>">
<!-- General link tags -->
<link rel="canonical" href="<%= config.app.url %>">
<!-- Manifest JSON -->
<link rel="manifest" href="<%= StaticAsset('/manifest.json') %>" crossorigin>
<!-- ----------------------------------------------------------------------
---- Icon Tags
---- ----------------------------------------------------------------------
----
---- The following will set up the favicons and the apple touch icons to be
---- used when adding the app to the homescreen of an iPhone, and to
---- display in the head of the browser.
----
---->
<!--[if IE]>
<link rel="shortcut icon" href="<%= StaticAsset('/favicon.ico') %>">
<![endif]-->
<link rel="apple-touch-icon" sizes="72x72" href="<%= StaticAsset('/apple-touch-icon-72x72.png') %>">
<link rel="apple-touch-icon" sizes="120x120" href="<%= StaticAsset('/apple-touch-icon-120x120.png') %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= StaticAsset('/apple-touch-icon-180x180.png') %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= StaticAsset('/favicon-16x16.png') %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= StaticAsset('/favicon-32x32.png') %>">
<link rel="icon" type="image/png" sizes="192x192" href="<%= StaticAsset('/android-chrome-192x192.png') %>">
<link rel="icon" type="image/png" sizes="194x194" href="<%= StaticAsset('/favicon-194x194.png') %>">
<link rel="mask-icon" href="<%= StaticAsset('/safari-pinned-tab.svg') %>" color="<%= config.meta.theme %>">
<meta name="msapplication-TileImage" content="<%= StaticAsset('/mstile-144x144.png') %>">
<meta name="msapplication-TileColor" content="<%= config.meta.theme %>">
<!-- ----------------------------------------------------------------------
---- Launch Images
---- ----------------------------------------------------------------------
----
---- Define the launch 'splash' screen images to be used on iOS.
----
---->
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-640x1136.png') %>" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-750x1294.png') %>" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1242x2148.png') %>" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1125x2436.png') %>" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1536x2048.png') %>" media="(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-1668x2224.png') %>" media="(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= StaticAsset('/assets/images/misc/splash-2048x2732.png') %>" media="(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
<!-- ----------------------------------------------------------------------
---- Application Tags
---- ----------------------------------------------------------------------
----
---- Define the application specific tags.
----
---->
<meta name="application-name" content="<%= config.app.name %>">
<meta name="apple-mobile-web-app-title" content="<%= config.app.name %>">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="<%= config.app.status_bar %>">
<meta name="mobile-web-app-capable" content="yes">
<meta name="full-screen" content="yes">
<meta name="browsermode" content="application">
<!-- ----------------------------------------------------------------------
---- Social Media and Open Graph Tags
---- ----------------------------------------------------------------------
----
---- The following will create objects for social media sites to read when
---- scraping the site.
----
---->
<!-- Open Graph -->
<meta property="og:site_name" content="<%= config.app.name %>">
<meta property="og:url" content="<%= config.app.url %>">
<meta property="og:type" content="website">
<meta property="og:title" content="<%= config.meta.title %>">
<meta property="og:description" content="<%= config.meta.description %>">
<meta property="og:image" content="<%= StaticAsset('/assets/images/brand/social-1200x630.jpg') %>">
<!-- Twitter -->
<meta name="twitter:card" content="app">
<meta name="twitter:site" content="<%= config.app.name %>">
<meta name="twitter:title" content="<%= config.meta.title %>">
<meta name="twitter:description" content="<%= config.meta.description %>">
<meta name="twitter:image" content="<%= StaticAsset('/assets/images/brand/social-440x220.jpg') %>">
<!-- ----------------------------------------------------------------------
---- JSON Linked Data
---- ----------------------------------------------------------------------
----
---- This will link the website to its associated social media page. This
---- adds to the credibility of the website as it allows search engines to
---- determine the following of the company via social media
----
---->
<script type="application/ld+json">
{
"#context": "http://schema.org",
"#type": "Organization",
"name": "<%= config.company.name %>",
"url": "<%= config.app.url %>",
"sameAs": [<%= '"' + Object.values(config.company.social).map(x => x.url).join('","') + '"' %>]
}
</script>
<!-- Define the page title -->
<title><%= config.meta.title %></title>
<!-- Generate the prefetch/preload links -->
<% webpack.chunks.slice().reverse().forEach(chunk => { %>
<% chunk.files.forEach(file => { %>
<% if (file.match(/\.(js|css)$/)) { %>
<link rel="<%= chunk.initial ? 'preload' : 'prefetch' %>" href="<%= StaticAsset(file) %>" as="<%= file.match(/\.css$/) ? 'style' : 'script' %>">
<% } %>
<% }) %>
<% }) %>
<!-- Include the core styles -->
<% webpack.chunks.forEach(chunk => { %>
<% chunk.files.forEach(file => { %>
<% if (file.match(/\.(css)$/) && chunk.initial) { %>
<link rel="stylesheet" href="<%= StaticAsset(file) %>">
<% } %>
<% }) %>
<% }) %>
</head>
<body ontouchstart="">
<!-- No javascript error -->
<noscript>JavaScript turned off...</noscript>
<!-- The Vue JS app element -->
<div id="app"></div>
<!-- Include the core scripts -->
<% webpack.chunks.slice().reverse().forEach(chunk => { %>
<% chunk.files.forEach(file => { %>
<% if (file.match(/\.(js)$/) && chunk.initial) { %>
<script type="text/javascript" src="<%= StaticAsset(file) %>"></script>
<% } %>
<% }) %>
<% }) %>
</body>
</html>
/service-worker.mix.js (building the service worker)
This mix configuration will build your Service Worker (service-worker.js), and place it into the root of /dist.
Note: I like to clean my dist folder each time I build my project, and as this functionality must be run at this stage of the build process, I have included it in the below configuration.
const mix = require('laravel-mix');
const path = require('path');
// Set the public path
mix.setPublicPath('dist/');
// Define all the javascript files to be compiled
mix.js('src/js/service-worker.js', 'dist');
// Load any plugins required to compile the files
const Dotenv = require('dotenv-webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// Define the required plugins for webpack
const plugins = [
// Grant access to the environment variables
new Dotenv,
// Ensure the dist folder is cleaned for each build
new CleanWebpackPlugin
];
// Extend the default Laravel Mix webpack configuration
mix.webpackConfig({
plugins,
resolve: {
alias: {
'~': path.resolve('')
}
}
});
// Disable mix-manifest.json (remove this for Laravel projects)
Mix.manifest.refresh = () => void 0;
/core.mix.js (building the application)
This mix configuration will build your main application and place it in /dist/js.
There are various key parts of this mix configuration, each of which has been clearly outlined in the comments within. These are the top-level areas:
Code splitting to app.js, manifest.js, and vendor.js (and dynamic importing)
Laravel Mix versioning does not work as needed for the HTML template so laravel-mix-versionhash is utilised instead
html-webpack-plugin is utilised to generate index.html based on the index.ejs template (see above)
webpack-pwa-manifest is utilised to generate a manifest based
copy-webpack-plugin is utilised to copy the static files to the /dist directory, and to copy any necessary icons to the site root
imagemin-webpack-plugin is used to compress any static images in production
workbox-webpack-plugin is used to inject the webpack manifest into the precaching array used in the service worker. InjectManifest is used, not GenerateSW
Any necessary manifest transformations are applied once the build process is complete
There may be additions to the above but pretty much everything is described by the comments in the following code:
const config = require('./config'); // This is where I store project based configurations
const mix = require('laravel-mix');
const path = require('path');
const fs = require('fs');
// Include any laravel mix plugins
// NOTE: not needed in Laravel projects
require('laravel-mix-versionhash');
// Set the public path
mix.setPublicPath('dist/');
// Define all the SASS files to be compiled
mix.sass('src/sass/app.scss', 'dist/css');
// Define all the javascript files to be compiled
mix.js('src/js/app.js', 'dist/js');
// Split the js into bundles
mix.extract([
// Define the libraries to extract to `vendor`
// e.g. 'vue'
]);
// Ensure the files are versioned when running in production
// NOTE: This is not needed in Laravel projects, you simply need
// run `mix.version`
if (mix.inProduction()) {
mix.versionHash({
length: 8
});
}
// Set any necessary mix options
mix.options({
// This doesn't do anything yet, but once the new version
// of Laravel Mix is released, this 'should' extract the
// styles from the Vue components and place them in a
// css file, as opposed to placing them inline
//extractVueStyles: true,
// Ensure the urls are not processed
processCssUrls: false,
// Apply any postcss plugins
postCss: [
require('css-declaration-sorter'),
require('autoprefixer')
]
});
// Disable mix-manifest.json
// NOTE: not needed in Laravel projects
Mix.manifest.refresh = () => void 0;
// Load any plugins required to compile the files
const Dotenv = require('dotenv-webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackPwaManifest = require('webpack-pwa-manifest');
const { InjectManifest } = require('workbox-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ImageminPlugin = require('imagemin-webpack-plugin').default;
// Define the required plugins for webpack
const plugins = [
// Grant access to the environment variables
new Dotenv,
// Process and build the html template
// NOTE: not needed if using Laravel and blade
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'static', 'index.ejs'),
inject: false,
minify: !mix.inProduction() ? false : {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
useShortDoctype: true
},
templateParameters: compilation => ({
webpack: compilation.getStats().toJson(),
config,
StaticAsset: (file) => {
// This will ensure there are no double slashes (bug in Laravel Mix)
return (config.app.static_url + '/' + file).replace(/([^:]\/)\/+/g, "$1");
}
})
}),
// Generate the manifest file
new WebpackPwaManifest({
publicPath: '',
filename: 'manifest.json',
name: config.app.name,
description: config.meta.description,
theme_color: config.meta.theme,
background_color: config.meta.theme,
orientation: config.app.orientation,
display: "fullscreen",
start_url: '/',
inject: false,
fingerprints: false,
related_applications: [
{
platform: 'play',
url: config.app.stores.google.url,
id: config.app.stores.google.id
},
{
platform: 'itunes',
url: config.app.stores.apple.url,
id: config.app.stores.apple.id
}
],
// TODO: Update this once the application is live
screenshots: [
{
src: config.app.static_url + '/assets/images/misc/screenshot-1-720x1280.png',
sizes: '1280x720',
type: 'image/png'
}
],
icons: [
{
src: path.resolve(__dirname, 'static/assets/images/icons/android-chrome-512x512.png'),
sizes: [72, 96, 128, 144, 152, 192, 384, 512],
destination: path.join('assets', 'images', 'icons')
}
]
}),
// Copy any necessary directories/files
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, 'dist'),
toType: 'dir',
ignore: ['*.ejs']
},
{
from: path.resolve(__dirname, 'static/assets/images/icons'),
to: path.resolve(__dirname, 'dist'),
toType: 'dir'
}
]),
// Ensure any images are optimised when copied
new ImageminPlugin({
disable: process.env.NODE_ENV !== 'production',
test: /\.(jpe?g|png|gif|svg)$/i
}),
new InjectManifest({
swSrc: path.resolve('dist/service-worker.js'),
importWorkboxFrom: 'disabled',
importsDirectory: 'js'
})
];
// Extend the default Laravel Mix webpack configuration
mix.webpackConfig({
plugins,
output: {
chunkFilename: 'js/[name].js',
}
}).then(() => {
// As the precached filename is hashed, we need to read the
// directory in order to find the filename. Assuming there
// are no other files called `precache-manifest`, we can assume
// it is the first value in the filtered array. There is no
// need to test if [0] has a value because if it doesn't
// this needs to throw an error
let filename = fs
.readdirSync(path.normalize(`${__dirname}/dist/js`))
.filter(filename => filename.startsWith('precache-manifest'))[0];
// In order to load the precache manifest file, we need to define
// self in the global as it is not available in node.
global['self'] = {};
require('./dist/js/' + filename);
let manifest = self.__precacheManifest;
// Loop through the precache manifest and apply any transformations
manifest.map(entry => {
// Remove any double slashes
entry.url = entry.url.replace(/(\/)\/+/g, "$1");
// If the filename is hashed then remove the revision
if (entry.url.match(/\.[0-9a-f]{8}\./)) {
delete entry.revision;
}
// Apply any other transformations or additions here...
});
// Filter out any entries that should not be in the manifest
manifest = manifest.filter(entry => {
return entry.url.match(/.*\.(css|js|html|json)$/)
|| entry.url.match(/^\/([^\/]+\.(png|ico|svg))$/)
|| entry.url.match(/\/images\/icons\/icon_([^\/]+\.(png))$/)
|| entry.url.match(/\/images\/misc\/splash-([^\/]+\.(png))$/);
});
// Concatenate the contents of the precache manifest and then save
// the file
const content = 'self.__precacheManifest = (self.__precacheManifest || []).concat(' + JSON.stringify(manifest) + ');';
fs.writeFileSync('./dist/js/' + filename, content, 'utf8', () => {});
});
/src/js/app.js (the main application)
This is where you register your service worker, and obviously define your application etc...
/**
* Register the service worker as soon as the page has finished loading.
*/
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
// TODO: Broadcast updates of the service worker here...
navigator.serviceWorker.register('/service-worker.js');
});
}
// Define the rest of your application here...
// e.g. window.Vue = require('vue');

Related

PWA - Why has my caching strategy such a negative impact on the performance?

So I am currently building my first pwa and I don't understand why the way I implemented caching, has such bad influence on the performance (LightHouse Performance Score drops from ~94 to 76 with a huge Increase (+4 seconds) in the LCP Score and I honestly don't get what I am doing wrong here.
The app is a blogging app and later on, I want to make whole articles available for offline use. For now, I did not want to do anything fancy, I just wanted to make the landingpage (which does not contain any mongo db queries, but many images and css) available for offline use.
So the Idea is the following:
Interrupt the fetch request
Check if the data is available in the cache
if yes: serve from the cache
if no: proceed with the fetch request
Implementation:
I have the following setup:
Node JS - blogging app (server-side rendered)
hooked up with a mongo db database
in combo with ejs templates
by entering: localhost:8000
the server responds with the ejs file that corresponds to landingpage with partials, that include the header and the footer.
app.get("/", resetVerifiedVariable, (req, res) => {
res.render("home.ejs");
});
This is the corresponding header , in which I make sure that the service worker is registered
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/icons/favicon.ico" />
<link rel="preload" href="/img/upchart-mobile.webp" as="image" />
<link rel="preload" href="css/main.css" as="style" />
<link type="text/css" rel="stylesheet" href="css/main.css" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#1045be" />
<!-- ios support for pwa -->
<link rel="apple-touch-icon" href="/icons/icon-96x96.png" />
<meta name="apple-mobile-web-status-bar" content="#1045be" />
<!-- REGISTER SERVICE WORKER -->
<script src="/scripts/initiateServiceWorker.js"></script>
<!-- REGISTER SERVICE WORKER -->
<!-- typeofOperator does not throw an error so we can check for the existence of an article-->
<% if (typeof article !== 'undefined') { %>
<title><%=article.title%>| title </title>
<!-- Meta Tags for Social Media sharing the articles -->
<meta
property="og:url"
content="https://example.com/blog/<%=article.slug%>"
/>
<meta property="og:type" content="article" />
<meta property="og:title" content="<%=article.title%>" />
<meta property="og:description" content="<%=article.metaDescription%>" />
<meta property="og:image" content="./img/logo-optimized.webp" />
<%} else { %>
<title>Title| Title </title>
<meta
name="description"
content="Description"
/>
<%}%>
</head>
Here is the corresponding Service Worker File that was hooked-up in the header:
// ###################################
// REGISTER SERVICE WORKER - PWA
// ###################################
// Check if the browser supports the serviceWorker
if ("serviceWorker" in navigator) {
//console.log(navigator.serviceWorker);
navigator.serviceWorker
.register("../serviceWorker.js")
.then((registrationObject) =>
console.log("ServiceWorker registered", registrationObject)
)
.catch((error) => console.log("ServiceWorker NOT registered", error));
}
With that out of the way, I have implemented the Cache
I want to cahce the following items:
-The html/ejs template for the landingpage
-The image from the Hero section
-All of the css
-and my main script
const staticCacheName = "staticServiceWorker-v1";
const staticAssets = ["/", "/img/upchart-mobile.webp", "/scripts/index.js", "css/main.css];
// ############################
// INSTALL
// ############################
self.addEventListener("install", (event) => {
const preCache = async () => {
const cache = await caches.open(staticCacheName);
// addAll returns a promise which must be passed on to waitUntil
return cache.addAll(staticAssets);
};
event.waitUntil(preCache());
});
//console.log("ServiceWorker installed");
//
// ############################
// ACTIVATE
// ############################
self.addEventListener("activate", (event) => {
//console.log("ServiceWorker activated");
});
// ############################
// FETCH
// ############################
self.addEventListener("fetch", (event) => {
console.log(event);
const checkCacheMatch = async () => {
const result = await caches.match(event.request);
return result || fetch(event.request);
};
event.respondWith(checkCacheMatch());
});
In the Fetch-Block I try to interrupt the fetch and serve the files from the cache instead
I assume, that this block right here is causing the performance decrease, but I dont know how to make this more performant.
So the main goal was to make the page much more performant because I make use of the cache and would there would be no need for a server request. What happens however is the exact opposite and I honestly can not figure out why that is the case
Any help would much appreciated
BR

Vite - Rollup node_modules not defined as modules

I have a Webpack project which I am migrating to Vite. I have the dev server working, and now I am trying to get the build to work. I have run into an issue where a number of my dependencies cannot be imported as modules because they must be global. This includes Backbone, jQuery, moment.js, and DevExtreme. (I will be slowly getting rid of these dependencies, but for now, there is a large code base which is based on them)
I first tried to have them loaded as modules and manually add them to the global context, but that failed because when importing you can't manage the order in which the files are loaded. Backbone and DevExtreme require jQuery to be loaded in first. If it isn't, the page fails to load.
So I have a list of node_modules referenced directly from my html entry points.
Here is a simplified example of what I currently have.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1"
/>
<title>rollup test</title>
<script src="/node_modules/jquery/dist/jquery.min.js"></script>
</head>
<body>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
oldMain.js
$(function() {
$("body").append("Here is some text");
});
main.ts
import './oldMain.js';
vite.config.ts
import path from "path";
import { defineConfig } from "vite";
export default defineConfig({
build: {
rollupOptions: {
input: {
index: path.resolve(__dirname, "index.html"),
},
}
}
});
package.json:
{
"name": "issue-example",
"version": "1.0.0",
"main": "index.html",
"scripts": {
"build": "vue-tsc --noEmit && vite build"
},
"dependencies": {
"jquery": "^3.6.0",
"vue": "^3.2.33",
},
"devDependencies": {
"typescript": "^4.6.3",
"vite": "^2.9.5",
"vue-tsc": "^0.34.10",
}
}
At the moment, the only way forward that I can see is to write a rollup plugin that extracts the list of non-module scripts from the html file, gets the target files from their paths, copies them to the dist/assets directory, and changes the src paths in the html to the new location.
Is there a better way? A plugin that already exists that knows how to do this? Is there a different way to include these dependencies so they will be globally available?
The solution I ended up going with is:
I use handlebars to create the index.html file the necessary scripts.
Have a .ts file which decides what scripts need to be in the index.html file. It checks whether the current build is dev or prod and puts the relevant path in for each script.
My index.html looks like:
<html>
<head>
[... meta, base, title, link, etc.]
{{{injectScripts externalScripts}}}
</head>
<body>
[... etc]
</body>
</html>
I have a simple handlebars plugin called injectScripts which simply does that - it puts the scripts in where I call the it in the html. Not my favorite solution to a problem, but it works well enough.

How to set up Alpine.js 3 with mix

I am having trouble getting started with Alpine.js v3 in a new project. This is my package.json and Alpine setup:
"devDependencies": {
"laravel-mix": "^6.0.19"
},
"dependencies": {
"alpinejs": "^3.0.6"
}
import Alpine from 'alpinejs'
window.Alpine = Alpine
window.Alpine.start()
Alpine.data('demo', () => ({
open: false,
toggle() {
this.open = !this.open
}
}))
driving this markup:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Demo</title>
<script src="assets/app.js"></script>
</head>
<body>
<div x-data="demo">
<button #click="toggle">Expand</button>
<div x-show="open">Content...</div>
</div>
</body>
</html>
and I build my javascript using laravel-mix:
// webpack.mix.js
let mix = require('laravel-mix');
mix
.setPublicPath('public')
.js('src/app.js', 'public/assets');
The javascript build fine and the page loads without errors, but when I click the toggle button, the content expands as expected, but I get the following error in the browser console and the content won't hide on subsequent clicks.
Uncaught (in promise) TypeError: i is not a function app.js:2282:28
If I defer my script or move it to the bottom, the page is broken on load and I get the following:
Uncaught TypeError: undefined is not a non-null object app.js:1995:12
Can anyone help me fix my setup?
Most likely you just forget to add defer attribute to script tag
<script defer src="assets/app.js"></script>
But also as documentation says
If you imported Alpine into a bundle, you have to make sure you are registering any extension code IN BETWEEN when you import the Alpine global object, and when you initialize Alpine by calling Alpine.start().
So change your script to
import Alpine from 'alpinejs';
Alpine.data('demo', () => ({
open: false,
toggle() {
this.open = !this.open
}
}));
window.Alpine = Alpine;
// should be last
Alpine.start();
Note: if there are some errors like assignment to undeclared variable data you may change your import to import Alpine from 'alpinejs/dist/module.cjs';

Failed to mount component on new laravel 5.5 project

me and a colleague have been having problems in new and old laravel projects when it comes to using vue js because we get the following error every time in the browser console
>`[Vue warn]: Failed to mount component: template or render function not defined.
found in
---> <Example>
<Root>
warn # app.js:32173
mountComponent # app.js:34241
Vue$3.$mount # app.js:39678
Vue$3.$mount # app.js:41868
init # app.js:35260
createComponent # app.js:36909
createElm # app.js:36852
createChildren # app.js:36980
createElm # app.js:36885
patch # app.js:37394
Vue._update # app.js:34147
updateComponent # app.js:34271
get # app.js:34614
Watcher # app.js:34603
mountComponent # app.js:34275
Vue$3.$mount # app.js:39678
Vue$3.$mount # app.js:41868
Vue._init # app.js:36000
Vue$3 # app.js:36085
(anonymous) # app.js:802
__webpack_require__ # app.js:20
(anonymous) # app.js:775
__webpack_require__ # app.js:20
(anonymous) # app.js:63
(anonymous) # app.js:66`
This happens even in new laravel projects using the default Example.vue
My current code is the following
Example.vue
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Example Component</div>
<div class="panel-body">
I'm an example component!
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted.')
}
}
</script>
assets/js/app.js
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
window.Vue = require('vue');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
Vue.component('example', require('./components/Example.vue'));
const app = new Vue({
el: '#app'
});
assets/js/bootstrap.js
window._ = require('lodash');
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.$ = window.jQuery = require('jquery');
require('bootstrap-sass');
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: 'your-pusher-key'
// });
welcome.blade.php
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" value="{{csrf_token()}}">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Raleway:100,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" type="text/css" href="/css/app.css">
<!-- Styles -->
</head>
<body>
<div id="app">
<example></example>
</div>
<script type="text/javascript" src="/js/app.js"></script>
</body>
</html>
webpack.mix.js
let mix = require('laravel-mix');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');
package.json
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"axios": "^0.16.2",
"bootstrap-sass": "^3.3.7",
"cross-env": "^5.0.1",
"jquery": "^3.1.1",
"laravel-mix": "^1.0",
"lodash": "^4.17.4",
"vue": "^2.1.10"
},
"dependencies": {
"cross-env": "^5.0.5",
"vee-validate": "^2.0.0-rc.17"
}
}
If you haven't changed anything and and have correctly bundled and installed JavaScript then the issue may lie with the default Laravel implementation.
The error message you are receiving means that you are likely importing the runtime only build (vue without the template compiler) in to an app that needs the template compiler.
To better understand this, Vue compiles everything into render functions (essentially a pure JavaScript representation of your webpage). When using single file components you end up with one base component that you mount to your Vue instance, which serves up your all your views, so, we would get something like this:
components/App.vue
<template>
<div>
I'm a base component
<!-- vue-router will mount components here -->
<router-view></router-view>
</div>
</template>
app.js
import App from './components/App.vue'
// "h" is just a standard taken from JSX
new Vue({
render: h => h(App)
}).$mount("#app");
app.blade.php
<html>
<head>
<!-- head stuff -->
</head>
<body>
<div id="app"></div>
<script src="app.js"></script>
</body>
</html>
The important thing here is that app.blade.php only acts as a mounting point for your entire app and App.vue serves as the base component, which is turns serves every other view (this would usually be done via vue-router). To get that to work we need to compile our assets into app.js via webpack, which creates all our render functions for us, so we don't need the compiler because everything has already been compiled. All that's left to do is create a route in routes/web.php to serve up the index blade file. That's essentially setting up an SPA.
What Laravel encourages you do, is add Vue components directly in your markup and register components globally, so you would do:
app.js
Vue.component('my-component', require('./components/My-component.vue'));
const app = new Vue({
el: '#app'
});
index.blade.php
<html>
<head>
<!-- head stuff -->
</head>
<body>
<div id="app">
<my-component></my-component>
</div>
<script src="app.js"></script>
</body>
</html>
Because we've added our component to the markup we need the template compiler to compile the bit between our app div tags into a render function at runtime. So, we need to import vue + compiler, which is what Laravel Mix should do for you by aliasing the runtime + compiler version of Vue (you can find details of how to do that in the docs).
To be honest, I'm not a fan of Laravel Mix because it abstracts away crucial implementation details that as a developer you need to know, such as, "is Laravel Mix correctly aliasing the Vue + compiler build"?, in your case it looks like it isn't.
In the end it's usually easier to just setup your own webpack config directly so you have total control over your config, you can use Vue's webpack simple config as a base.
Once you've correctly setup webpack, you just then need to add the alias to webpack config and you have your runtime + compiler build in:
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
I realise that I haven't really given a soultion to your specific problem. I find it hard to believe that Webpack Mix isn't correctly aliasing the vue + compiler build, but that is what that message implies. Hopefully though, this gives you enough information to find where the problem lies.

DHTMLXGantt and Laravel - Data doesn't display in extended templates

Background
Following this tutorial from Jan 2016, ( https://dhtmlx.com/blog/using-dhtmlxgantt-with-laravel/ ) I began experimenting with DHTMLXGantt (v4.1) and Laravel (v5.4) and ran into some troubles getting the sample data from a mysql/mariadb database to display in the gantt chart. The initial troubles had to do with the DHTMLX connector not staying current with some Laravel changes. For the sake of others who may read this and are struggling with the same issues, the two basic problems I already solved were:
(1) I was referred to this [updated] connector which was compatible with Laravel's recent versions ( https://github.com/mperednya/connector-php/tree/modern ). And,
(2) I specified a date range that matched the dates of the sample data (from 2013), such as...
gantt.config.start_date = new Date(2013, 04, 01);
gantt.config.end_date = new Date(2013, 04, 30);
At this point I was able to successfully use Laravel as a backend server for the DHTMLXGantt chart (including read-write with the sample data).
Problem
The problem I am having now is trying to move from the simplistic sample, to something slightly more complex. Specifically, when I employ Laravel's extended templating I get the gantt chart painted on screen, but no project/task data displays in the chart.
To explain this a little more specifically, a simple Laravel view with the whole page contained within it works as expected. For example, this blade file (gantt.blade.php) works.
<!DOCTYPE html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<script src="codebase/dhtmlxgantt.js"></script>
<link rel="stylesheet" href="codebase/dhtmlxgantt.css">
<link rel="stylesheet" href="codebase/skins/dhtmlxgantt_skyblue.css" type="text/css" media="screen" title="no title" charset="utf-8">
</head>
<body>
<div class="container">
<h1>Placeholder Header</h1>
<div id="gantt_here" style='width:100%; height:500px;'></div>
<script type="text/javascript">
gantt.config.xml_date = "%Y-%m-%d %H:%i:%s";
gantt.config.step = 1;
gantt.config.scale_unit= "week";
gantt.config.autosize = "xy";
gantt.config.fit_tasks = true;
gantt.config.columns = [
{name:"text", label:"Task name", width:"*", tree:true },
{name:"start_date", label:"Start time", align: "center" },
{name:"duration", label:"Duration", align: "center" },
{name:"add", label:"", width:44 }
];
gantt.init("gantt_here");
gantt.load("./gantt_data", "xml");
var dp = new gantt.dataProcessor("./gantt_data");
dp.init(gantt);
</script>
<h1>Placeholder Footer</h1>
</div>
</body>
But if I try using an extended app layout with the intention of building out a standard look & feel across all pages, the Gantt chart appears and is formated as I expect, but no data appears within the gantt chart. Here is top-level layout file (app.blade.php)
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
<link rel="stylesheet" href="codebase/dhtmlxgantt.css">
<link rel="stylesheet" href="codebase/skins/dhtmlxgantt_skyblue.css" type="text/css" media="screen" title="no title" charset="utf-8">
<!-- Scripts -->
<script>
window.Laravel = {!! json_encode([
'csrfToken' => csrf_token(),
]) !!};
</script>
<script src="codebase/dhtmlxgantt.js"></script>
</head>
<body>
<div id="app">
<!-- menus and other bootstrap styling removed for brevity -->
#yield('content')
</div>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
And here's the "content" view (index.blade.php)...
#extends('layouts.app')
#section('content')
<div id="gantt_here" style='width:100%; height:500px;'></div>
<script type="text/javascript">
gantt.config.xml_date = "%Y-%m-%d %H:%i:%s";
gantt.config.step = 1;
gantt.config.scale_unit= "week";
gantt.config.autosize = "xy";
gantt.config.fit_tasks = true;
gantt.config.columns = [
{name:"text", label:"Task name", width:"*", tree:true },
{name:"start_date", label:"Start time", align: "center" },
{name:"duration", label:"Duration", align: "center" },
{name:"add", label:"", width:44 }
];
gantt.init("gantt_here");
gantt.load("./gantt_data", "xml");
var dp = new gantt.dataProcessor("./gantt_data");
dp.init(gantt);
</script>
</div>
#endsection
Other things I've tried:
Using Chrome developer tools, I can see that the xml data was properly delivered to the browser in both examples. This made me think maybe it is a timing problem of some sort. So I put a couple links on the page just to test clearing the chart and reloading it. But still no change.
<a onclick="gantt.clearAll()" href="javascript:void(0);">Clear</a>
<a onclick="gantt.load('./gantt_data', 'xml')" href="javascript:void(0);">Refresh</a>
I also tried moving the div block "gantt_here" to various other places above and below Laravel's template directives. It fails in all cases, except when this div block is outside (either above or below) the "app" div tag in "app.blade.php" which, of course, defeats the purpose I am trying to achieve.
My goal is to use this chart within Laravel's extended templating capabilities. But I can't figure out what's wrong. Any ideas?
After further troubleshooting, I discovered the problem. It turns out that in app.blade.php, I was loading the app.js at the bottom of the body tag. When I moved this up to the HTML header area, the data began to display properly.

Resources