'Unexpected token <' on every new build

2020-02-27 11:10发布

问题:

I know there are some similar questions like so unexpected token after angular 2 production build but it doesnt actually answer my question.

Basically I have an angular app and its a PWA, now everytime I do a new production build the first time I visit the site I get an error like so

and then as soon as I refresh the page, the site works as normal.

This happens on any browser/device

Now my suspicion is that every time the app is updated, the main bundled js file changes, and the PWA has cached the old one and the app is still trying to use the new one.

My Project structure is as follows

I have a root module that lazy loads the other modules now the first module that gets loaded is my account module so the user can log in

this is my root-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';

const routes: Routes = [
    { path: '', redirectTo: '/account/login', pathMatch: 'full' },
    {
        path: 'account',
        loadChildren: 'account/account.module#AccountModule',
        data: { preload: true }
    }
];

@NgModule({
    imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
    exports: [RouterModule],
    providers: []
})
export class RootRoutingModule {

}

so technically the first module the user gets to is the account module, but obviously it has to be redirected from the root module.

So what I did was in my root component module I check to see if the Service Worker needs updating like so...

root.component.ts

@import { SwUpdate } from '@angular/service-worker'
...

export class...

constructor(
    private _sw: SwUpdate
) {
    if (this._sw.isEnabled) {
        this._sw.available
            .subscribe(() => {
                this._sw.activateUpdate()
                    .then(() => {
                        window.location.reload(true);
                    });
            });
    }
}     

now this should check to see if there is an update and then just refresh the page.. but it doesn't work.

this is my ngsw-config.json

{
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "App",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js",
          "!/main*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}

as you can see I am excluding the main.js chunk file which should eliminate this issue... but it doesn't

Now when I check the network tab after a new build this is what I get

This image was before I excluded the main.js file

this is what my main.js calls look like after I have excluded it from the ngsw.config

I then thought to just try and catch this error using my sentry error handler like so..

export class RavenErrorHandler implements ErrorHandler {
  handleError(err: any): void {
    console.log('error', err);
    if (err.toLowerCase().includes('token <')) {
        window.location.reload(true);
    } else {
        Raven.captureException(err);
    }
  }
}

but this isn't working, I am getting the error in sentry but the app is not reloading??

Now I have done some testing, If you visit the site after a fresh build on private mode you will never get the error, it only seems to happen if you have visited the site before, which is what makes me think its a caching issue

My Application is hosted on Azure and my application is using Angular 7.27

Any help would be appreciated!

回答1:

The issue is caused by the installed ServiceWorker, which is the one that interferes and caches all responses, including the ones that you have just updated.

More information about service workers: MDN - Using Service workers

https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

That is why window.location.reload() does not work, because it does make a new request but this request is hijacked by the installed ServiceWorker and instead of letting the real request go through to the real server and fetch the real response (updated main.js and assets), it just returns back to your app a cached response, which is the old main.js and not the updated one and that is why it fails.

More specifically the ServiceWorker is caching the main.js and other assets, which are generated each time you change your code. Main.js when loaded in turn it lazy-loads (makes an http request to) other chunks of your application (e.g. the 5.xxxxx.js).

When you do make a change in your code, and even though all your chunks gets updated, including main.js and the 5.xxxxxx.js chunk, the browser is still running the older main.js. This older main.js has a reference to the older pair of 5.xxxxxx.js which no longer exists. At this point if you do a reload() programatically, the installed ServiceWorker responds with the older cached main.js which tries in turn to lazy-load the older version of 5.xxxxx.js from the server which does not exist and therefore gets a response of an 404 html error. Hence the '<' token exception (it's actually the first character of the tag of the 404 error page).

This is evident from the network panel screenshot. If you examine the screenshot you will see that the main.js and all other assets are delivered from (from service worker), which means that those are the cached older versions and are different from the actual updated files that now exist in your web server.

To fix this issue, the ServiceWorker must be configured to not cache the main.js and instead always let this request to go through to the real server in order to fetch any updated version of main.js that in turn will lazy-load the updated 5.xxxx.js chunk that now actually exist in the web server.

Here is how you can configure the ngsw-config.json of ServiceWorker to specifically not cache the main.js file (assuming it is of the pattern main.xxxxxxx.js) in order to fix this issue:

{
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "App",
      "installMode": "prefetch",
      "resources": {
        "files": [
          "/favicon.ico",
          "/index.html",
          "/*.css",
          "/*.js",
          "/!main*.js"
        ]
      }
    }, {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": {
        "files": [
          "/assets/**",
          "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
        ]
      }
    }
  ]
}

run ng build --prod to rebuild your app and test it.

UPDATE:

To make sure that this is not a browser caching issue, pass true as the first argument to the window.location.reload() method. This instructs the reload to be executed without cached assets by the browser:

root.component.ts

                    .then(() => {
                        window.location.reload(true);
                    });

UPDATE 2:

According to the documentation of Angular, the Service Worker that is installed already on browsers of clients is cached. If you update the Service Worker code (like you did recently), then the new Service Worker code must be updated in all the browsers of the clients that have it already installed. Angular documentation states that there is a periodic check, but it doesn't specify it, that will check if the Service Worker itself needs an update and will do it. https://angular.io/guide/service-worker-devops#service-worker-updates

Perhaps, the manefistation of the issue only in the production and not in development or with a Private Browser mode (clean browser slate with no SW installed), seems that this is the case.

To verify this, you should compare the existing browser's SW that is running on the page using the developer tools of Chrome, with the updated SW code that should be installed. If they are different then it means that it hasn't been updated yet and that's what's causing the issue.

You can do this using a chrome browser that exhibits this issue and by going to Chrome Dev tools -> Application Tab -> Service Workers (in the top of left panel) and there will be the SW that is registered in the browser. Click the Source link and it will open up the actual Code of the SW. Compare the code line by line (or use a diff utility) with the updated SW code that should be running.



回答2:

Double check your cache headers. Chrome doesn't always listen to no-cache. If you've visited the site recently chrome will load index.html from cache. Causing the error.

We've changed our index.html to no-store and that seemed to solve the issue. https://gertjans.home.xs4all.nl/javascript/cache-control.html



回答3:

This usually happens for me when the requested resource returns 404 and instead of requested JS file you receive your index.html which has tags hence '<' token. Could it somehow be the case? Say you have versioning on your scripts and rebuild invalidates old URLs or something like that.



回答4:

I'm no expert to angular problem. The best I can do is help you break down the problem, help you debug.

  1. If you just want the error go away (and you have control over the server that serves your index.html), I'm pretty sure it can solved by setting these http response headers (source):
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
  1. However, such setup more or less beats the point of PWA in the first place. PWA allow users to use your site even offline, they must be able to get to index.html first, so index.html has to be cached.

  2. Caching index.html is the right thing to do. It's likely your cache-control setting for your old .js and other assets has some problems. I'm not sure which part goes wrong, but the expected behavior is: these assets should be cached just like index.html, 404 should never happen in first place since browser should read from local cache.

  3. One thing you can do to quickly "cover up" the problem, is simply keep all the old assets of previous version on server too. This way, cached index.html ref to old assets, they remain accessible, no 404 thus no "unexpected token <".

  4. Now I would suggest you try point 4 first, to verify my theory at point 3 is correct. Also check if SwUpdate works properly. Ideally, you should see following:

    1. you publish newer version of code
    2. you visit the site, should see cached older version
    3. after older version of app is loaded, SwUpdate automatically refreshes the page for you and load the newer version.

To sum up, my theory is things go wrong at point 3. Older assets should be correctly cached, using proper http cache-control headers (or angular service worker has other way of doing same things? I'm not familiar with that part). If you cannot figure it out, then just keep the old assets accessible on server.



回答5:

I have experienced this error when lazy loading modules.

It was an Angular / Iconic PWA and was related to a missing import:

import { SharedModule } from '@shared/shared.module';

Also see:

  • Angular 2. Error: Loading chunk failed
  • How to overcome loading chunk failed with Angular lazy loaded modules


回答6:

I have one solution for this issue. For get https://angular.io/cli/build

get your app angular.json file. And change outputHashing value all to media

           "architect": {
                "build": {
                    ...
                    "configurations": {
                        "production": {
                            ...
                            "outputHashing": "media", // "all"
                        }
                    }
                }
            }