Clean old workbox cache in a SPA served by Pyramid - caching

I'm new to PWAs and service workers. I started to test workbox with a Pyramid application which serves a preact app bundled with webpack 4. This is the home.jinja2 template:
<!DOCTYPE html>
<html lang="{{request.locale_name}}">
<head>
...
<link href="{{request.static_url('app:static/app.css')}}" rel="stylesheet">
<script type="text/javascript" src="{{request.static_url('app:static/runtime.js')}}"></script>
<script type="text/javascript" src="{{request.static_url('app:static/vendor.js')}}"></script>
</head>
<body>
<div id="app"/>
{% block scripts %}
{% endblock %}
<script type="text/javascript">
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js');
});
}
</script>
</body>
</html>
The SPA makes calls to /api/v1/. I want to provide a network first experience. This is part of the webpack config:
optimization: {
runtimeChunk,
},
output: {
path: path.resolve(path.join(__dirname, '..', 'app', 'static')),
},
plugins: [
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: { discardComments: { removeAll: true } },
canPrint: false,
}),
new ManifestPlugin({
publicPath: '',
}),
new WebpackCleanupPlugin({
exclude: [
'robots.txt',
],
}),
new GenerateSW({
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /\//,
handler: 'networkFirst',
},
{
urlPattern: /https:\/\/fonts.(?:googleapis|gstatic).com\/(.*)/,
handler: 'staleWhileRevalidate',
},
],
navigateFallback: '/app-shell',
navigateFallbackBlacklist: [/^\/api/],
templatedUrls: {
'/app-shell': 'index.html',
},
}),
],
The generated index.html basically equals to the home.jinja2 layout (both contain the hashed assets urls). The problem I face is whenever I make changes to the frontend and deploy them, once the update process of the service worker is completed, the browser requests both the new and old assets (43f6a4e6 is the hash of the new assets).
Refreshing has no effect at all, even after 24 hours. I delete old assets on every deployment, therefore all requests to old assets error and a blank page is displayed. I temporarily had to redirect old assets requests to the new ones.
I implemented this recipe https://developers.google.com/web/tools/workbox/guides/advanced-recipes#warm_the_runtime_cache and the site is completely broken. Firefox displays a data corrupted error. Any ideas would be much appreciated.

Related

Laravel Breeze (vue) and Homestead - npm run dev and HMR not working

I followed the instructions from the documentation:
Homestead: https://laravel.com/docs/9.x/homestead#installation-and-setup
Breeze with Vue and inertia: https://laravel.com/docs/9.x/starter-kits#breeze-and-inertia
When I run npm run build everything works fine. I can visit my new app over http://homestead.test/. When I try to use the dev server with hot reload npm run dev, the debug console in my browser (host) tells:
GET http://127.0.0.1:5173/#vite/client net::ERR_CONNECTION_REFUSED
GET http://127.0.0.1:5173/resources/js/app.js net::ERR_CONNECTION_REFUSED
I already tried to change my package.json file to from "dev": "vite", to "dev": "vite --host homestead.test", but this only results in the errors
GET http://homestead.test:5173/#vite/client net::ERR_CONNECTION_REFUSED
GET http://homestead.test:5173/resources/js/app.js net::ERR_CONNECTION_REFUSED
In app.blade.php the scripts are imported with #
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title inertia>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.bunny.net/css2?family=Nunito:wght#400;600;700&display=swap">
<!-- Scripts -->
#routes
#vite('resources/js/app.js')
#inertiaHead
</head>
<body class="font-sans antialiased">
#inertia
</body>
</html>
#routes seems to be a part of the Laravel Ziggy package. No error from this side.
But the #vite('resources/js/app.js') and #inertiaHead are throwing errors. These directives link to a wrong destination.
How to solve this?
I've found the solution. Add the server part in your vite.config.js. And add the app.css to the inputs
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '#vitejs/plugin-vue';
export default defineConfig({
server: {
hmr: {
host: "192.168.56.56",
},
host: "192.168.56.56",
watch: {
usePolling: true,
},
},
plugins: [
laravel({
input: ['resources/js/app.js', 'resources/css/app.css'],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
});
My Solution is to turn on the HTTPS with Vite SSL cert generation plugin
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '#vitejs/plugin-react';
import basicSsl from '#vitejs/plugin-basic-ssl'
export default defineConfig({
server: {
https: true,
},
plugins: [
basicSsl(),
laravel({
input: 'resources/js/app.jsx',
refresh: true,
}),
react(),
],
});
I will share a little of my experience when I get a problem like this. This is because the existing IP is not detected on the device.
http://127.0.0.1:5173 that's the route that made this error occur. So I tried building with
npm run build
after that I refreshed my page and the page was successfully opened.
looks like it was not accessible during development stage. that's all I understand. sorry if there is an error that I convey.

Laravel Vue production instance

I have a Laravel Vue project that works fine in development. I have been trying to get it to our production server for some hours now. However, when I visit the URL, I get a blank page. Upon inspection in the console, I notice all the js chunk files return status of 200, but their contents are "We're sorry, but Project Name doesn't work properly without Javascript."
The following is a snippet of my webpack.mix.js.
mix
.js('resources/js/app.js', 'public/js')
.webpackConfig({
resolve: {
alias: {
'#': path.resolve(__dirname, 'frontend/src/'),
'#themeConfig': path.resolve(__dirname, 'frontend/themeConfig.js'),
'#core': path.resolve(__dirname, 'frontend/src/#core'),
'#validations': path.resolve(__dirname, 'frontend/src/#core/utils/validations/validations.js'),
'#axios': path.resolve(__dirname, 'frontend/src/libs/axios')
}
},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
{
loader: 'sass-loader',
options: {
sassOptions: {
includePaths: ['frontend/node_modules', 'frontend/src/assets']
}
}
}
]
},
{
test: /(\.(png|jpe?g|gif|webp)$|^((?!font).)*\.svg$)/,
loaders: {
loader: 'file-loader',
options: {
name: 'images/[path][name].[ext]',
context: '../vuexy-vuejs-bootstrap-vue-template/src/assets/images'
// context: 'frontend/src/assets/images'
}
}
}
]
}
})
.sass('resources/sass/app.scss', 'public/css')
.options({
postCss: [require('autoprefixer'), require('postcss-rtl')]
})
// ------------------------------------------------
// If you are deploying on subdomain/subfolder. Uncomment the below code before running the 'yarn prod' or 'npm run production' command.
// Please Change below 'publicPath' and 'setResourceRoot' options as per your sub-directory path.
// ------------------------------------------------
if (mix.inProduction()) {
mix.version()
mix.webpackConfig({
output: {
publicPath: '/app/',
chunkFilename: 'js/chunks/[name].[chunkhash].js'
}
})
mix.setResourceRoot('/app/')
}else{
mix.webpackConfig({
output: {
chunkFilename: 'js/chunks/[name].js'
}
})
}
I set up production using the ff on my local machine:
npm run prod
Then I run the following via ssh on the server:
composer install --optimize-autoloader --no-dev
php artisan key:generate
In the console, the js chunk files show this as their response from the server:
<!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">
<meta name="csrf-token" content="j9kLqaAInZ4043rRqT9Zn3kNMHaSfCdF7h5ejnnZ">
<link rel="icon" href="/favicon.ico">
<title>project Title</title>
<link rel="stylesheet" href="https://app.myprojects.com/css/app.css?id=bff15ac59733e85aedce">
<link rel="shortcut icon" href="https://app.myprojects.com/images/logo/favicon.png">
</head>
<body>
<noscript>
<strong>We're sorry but this platform doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
</div>
<script src="https://app.myprojects.com/js/app.js?id=a74eb61fbd09ea2e1e92"></script>
</body>
</html>
Any help would be appreciated!
Upon lots of further investigations, i noticed that both app.css and app.js loads properly and only the chunk files had the issue. Apparently they were loading from
https://app.myproject.com/app/js/chunks/1.edcb1b13fece690b424f.js
instead of from
https://app.myproject.com/js/chunks/1.edcb1b13fece690b424f.js
I was able to get the page to load properly by setting publicPath and resourceRoot to /

Get Google analytics data every hour (by crone)

I used the following API code to get data from Google analytics.
but now I need to split it because I need to get user authorization and then create a crone job every hour (without having to ask another permission) from a different server to get GA data.
What changes should be made?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="google-signin-client_id" content="<REPLACE_WITH_CLIENT_ID>">
<meta name="google-signin-scope" content="https://www.googleapis.com/auth/analytics.readonly">
</head>
<body>
<!-- The Sign-in button. This will run `queryReports()` on success. -->
<p class="g-signin2" data-onsuccess="queryReports"></p>
<script>
// Replace with your view ID.
var VIEW_ID = 'ga:104831427';
// Query the API and print the results to the page.
function queryReports() {
gapi.client.request({
path: '/v4/reports:batchGet',
root: 'https://analyticsreporting.googleapis.com/',
method: 'POST',
body: {
reportRequests: [
{
viewId: VIEW_ID,
dateRanges: [
{
startDate: '7daysAgo',
endDate: 'today'
}
],
"metrics": [
{
"expression": "ga:sessions"
}
],
"dimensions": [
{ "name": "ga:date" }
]
}
]
}
}).then(displayResults, console.error.bind(console));
}
function displayResults(response) {
var formattedJson = JSON.stringify(response.result, null, 2);
window.parent.postMessage({formattedJson:(formattedJson)}, "my-domain.com");
}
</script>
<!-- Load the JavaScript API client and Sign-in library. -->
<script src="https://apis.google.com/js/client:platform.js"></script>
</body>
</html>
You can use the Google SuperProxy to fetch the results for you https://developers.google.com/analytics/solutions/google-analytics-super-proxy
Furthermore, you can use the extend the token using the below:
https://accounts.google.com/o/oauth2/token?client_id=CLIENT_ID
&client_secret=CLIENT_SECRET
&refresh_token=REFRESH_TOKEN
&grant_type=refresh_token
More details on how to do it is available here: http://thisistony.com/blog/googleanalytics/google-analytics-api-oauth-ever-wondered-how-to-get-the-access_token/

How to deal when llogged user refresh page of vuejs/vuex app?

in my laravel 5.8 / vuejs 2.5 / "vuex 3.1 user login into the system and some data are stored in user's store, like in auth/Login.vue:
<script>
export default {
...
mounted() {
this.setAppTitle("", 'Login', bus);
}, // mounted() {
computed: {
authError() {
return this.$store.getters.authError;
}
}, // computed: {
methods: {
authenticate() {
this.$store.dispatch('login'); // calling action
login(this.$data.form)
.then((res) => {
this.$store.commit("loginSuccess", res); // calling mutation
this.$store.dispatch('retrieveUserLists', res.user.id );
this.$router.push({path: '/websites-blogs'}); // For debugging!
})
.catch((error) => {
this.$store.commit("loginFailed", {error}); // calling mutation
});
}
}, // methods: {
and store where user's account and his data are kept resources/js/store.js :
export default {
state : {
currentLoggedUser: user,
// personal data only for logged user
userLists: [],
},
getters : {
...
userLists(state) {
return state.userLists;
},
It works ok, until logged user refresh page (F5 or CTRL+R) and user is still logged in my page, but some data, say (userLists)
are empty and some listing is empty.
I have MainApp.vue :
<template>
<body class="account-body">
<v-dialog/>
<MainHeader></MainHeader>
<div class="content p-0 m-0" style="width: 100% !important; margin: auto !important;">
<notifications group="wiznext_notification"/>
<router-view></router-view>
</div>
</body>
</template>
<script>
...
export default {
name: 'main-app',
components: {MainHeader},
mixins: [appMixin],
created() {
},
mounted() {
...
},
methods: {
...
}, // methods: {
}
</script>
and resources/views/index.blade.php :
<?php $current_dashboard_template = 'Horizontal' ?>
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title id="app_title">{{ config('app.name', 'Laravel') }}</title>
<link href="{{ asset(('css/Horizontal/bootstrap.min.css')) }}" rel="stylesheet" type="text/css">
<link href="{{ asset(('css/Horizontal/icons.css')) }}" rel="stylesheet" type="text/css">
<link href="{{ asset(('css/Horizontal/style.css')) }}" rel="stylesheet" type="text/css">
...
{{--<link href="css/Horizontal/ion.rangeSlider.css" rel="stylesheet" type="text/css"/>--}}
{{--<link href="css/Horizontal/ion.rangeSlider.skinModern.css" rel="stylesheet" type="text/css"/>--}}
{{--<link href="css/Horizontal/powerange.css" rel="stylesheet" type="text/css"/>--}}
<link href="{{ asset(('css/'.$current_dashboard_template.'/app.css')) }}" rel="stylesheet" type="text/css">
...
</head>
<body>
<div class="wrapper" id="app">
<main>
<div id="main_content">
<mainapp></mainapp>
</div>
</main>
</div>
</body>
#include('footer')
<script src="{{ asset('js/jquery.min.js') }}"></script>
<script src="{{ asset('js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ asset('js/metisMenu.min.js') }}"></script>
<script src="{{ asset('js/waves.min.js') }}"></script>
<script src="{{ asset('js/jquery.slimscroll.min.js') }}"></script>
<script src="{{ asset('js/app.js') }}{{ "?dt=".time() }}"></script>
{{--<script type="text/javascript" src="{{ asset('js/vuecsv.min.js') }}"></script>--}}
</html>
Could you please to advice a proper way of such situations ? What could I do?
MODIFIED :
I tried to remake my storage and seems it works ok :
const user = getLocalUser();
export default {
state : {
currentLoggedUser: user,
isLoggedIn: !!user,
loading: false,
auth_error: null,
api_url: '/api',
// personal data only for logged user
userLists: [], // I need to save data on page refresh
},
getters : {
...
userLists(state) {
// that works ok after user logged into the system and userLists is read on all page without refresh
if ( state.userLists.length > 0 ) {
return state.userLists;
}
// if userLists is empty check data in localStorage which were saved in refreshUserLists mutation
let localStorageUserLists = JSON.parse(localStorage.getItem("userLists"));
if ( localStorageUserLists.length > 0 ) {
console.log("localStorageUserLists RETURNED::")
return localStorageUserLists;
}
return [];
},
},
mutations : {
// that works ok after user logged into the system and his personal data(in my case 5 rows) are read from db into the vuex store
refreshUserLists(state, payload) {
state.userLists = payload;
localStorage.setItem("userLists", JSON.stringify(payload) );
},
},
What else have I to pay attention at ?
So the thing is, Vuex control your data status across your entire application and components, but once your user press F5 or any other refresh option, the Vuex is reset, there is nothing you can do about it, it's just like it works.
One work around solution i can think right now is to save the data on Vuex and maybe on localstorage, then, on vuex create lifecycle you can just populate the data with the localstorage data if it's empty.
I disagree with the proposed solutions. A browser refresh usually indicates that the user wants to fetch the most fresh data from the server, and reset the app state with that fresh data. Therefore you shouldn't store Vuex state in localStorage or cookies to get around this. If you do that, you now have your app state in two places, and also the user thinks they have the most fresh data when they don't.
The best way to handle a browser refresh event in a SPA is to bootstrap your state from scratch. When your app loads after a refresh, it should notice that the user is not logged in (since that is the default state), and should log them in by negotiating with the server behind the scenes, such as by sending the session cookie and receiving confirmation (or redirecting to login page if the session has expired). It should also re-fetch all relevant data from the server and store that in Vuex again.
you can use both localStorage and cookies.due to problems that I had with LocalStorage i recommand cookies. Vue has a rich plugin called Vue-cookies.
you can set and get data by these commands:
var user = { id:1, name:'Journal',session:'25j_7Sl6xDq2Kc3ym0fmrSSk2xV2XkUkX' };
this.$cookies.set('user',user);
// print user name
console.log(this.$cookies.get('user').name)
more details:
Github

Laravel app.js Conflicts with Datatables

I am using datatables on a laravel project. But seems like laravels' app.js is conflicting with datatables.min.js I get this error in console.
Uncaught TypeError: $(...).DataTable is not a function
If I remove app.js from head everything works relation to datatables but then bootstraps menu dropdowns and some other js related stuff stops working obviously because I remove app.js How can I resolve this by including both in head section?
UPDATE: Here is head section of my laravel app. Laravel version is latest 5.6
<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">
<meta name="csrf-token" content="Yhn7OFsFoV2qKhwsF7URC9GzjwNIb8muUT2u5kkD">
<title>Application</title>
<script src="http://127.0.0.1:8000/js/app.js" defer></script>
<script src="http://127.0.0.1:8000/js/datatables.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function(){
$('#users').DataTable({
processing: true,
serverSide: true,
searching: true,
filter: true,
ajax: 'http://127.0.0.1:8000/api/users/data',
columnDefs: {
targets: [-1],
visible: false,
searchable: true,
},
columns: [
{data: 'id', name: 'id'},
{data: 'name', name: 'name', sortable: false},
{data: 'email', name: 'email', sortable: false},
{data: 'role', name: 'role'},
{data: 'created_at', name: 'created_at'},
],
initComplete: function() {
this.api().columns([2]).every(function () {
var column = this;
var input = document.createElement("input");
input.classList.add('form-control');
input.setAttribute('placeholder', 'search by email..');
input.setAttribute('name', 'search-email');
$(input).appendTo($(column.header()).empty())
.on('change', function () {
column.search($(this).val(), false, false, true).draw();
});
});
$('.dataTables_filter input[type="search"]').addClass('form-control');
}
});
});
</script>
It is because jquery is available to you through app.js, and you probably linked the jquery for datatable again.
Try removing the link for jquery and it should work.
First, you didn't specify Laravel version so I assume it's the latest stable release.
Second you didn't say how you installed datatables, I assume you just included minified source manually.
According to the Documentation you should be able to install datatables through package.json file (with npm or yarn, depending on your preferences).
To be clear, the datatables package is available in NPM repo:
https://datatables.net/download/npm
you can replace the laravel default JS and CSS with Bootstrap. The bootstrap javascript will work the same as App.js.
Another advantage of replacing the laravel default is Bootstrap will all you to create admin-templates like pages.That worked for me
add defer attribute to your script tag
<script src="http://127.0.0.1:8000/js/datatables.min.js" type="text/javascript" ***defer***></script>
Include defer attribute to your <script src="{{ asset('js/datatables.min.js') }}" defer></script>
Problem is that scripts are not loaded in right order. Defer runs script after document is loaded

Resources