gruhn / vue-qrcode-reader

A set of Vue.js components for detecting and decoding QR codes.
https://gruhn.github.io/vue-qrcode-reader
MIT License
2.04k stars 330 forks source link

Doesn't work offline due to external dependency loading at runtime #197

Closed Ky6uk closed 3 years ago

Ky6uk commented 3 years ago

Describe the bug

There the breaking change was introduced in v2.3.10. The dependency of the webrtc-adapter package has been removed. Now it's impossible to use vue-qrcode-reader in local environments (without internet connection) and in applications with strict CSP rules (loading scripts from external resources is not allowed).

Screenshots

Screenshot 2020-08-25 at 22 08 13
gruhn commented 3 years ago

Thanks for reporting this. I released a fix:

npm i vue-qrcode-reader@2.3.12-alpha.2

This certainly won't resolve the issue completely since the QrcodeStream also spawns a webworker which in turn loads a script from a public CDN:

https://github.com/gruhn/vue-qrcode-reader/blob/bf32fa0b4b9c3473949a68d629b784ce04c96477/src/worker/jsqr.js#L15-L17

Can you please check if the webrtc-adapter related problem is fixed anyway?

Ky6uk commented 3 years ago

@gruhn Yep, webrtc problem has been resolved. Now I see only impossibility to load jsqr because of the same reason above. Strict CSP rules don't allow us to do that. Can you also add a bundled version to make it happen to work on local environments also?

gruhn commented 3 years ago

I tried to do that in the past but it's not trivial to get the configuration right when webworkers are involved. I fear it'll take a while before I find the time to do that properly. A workaround might be to:

  1. put jsQR.min.js together with your static assets
  2. copy src/worker/jsqr.js into your project
  3. modify importScripts part so jsQR.min.js is loaded from your static assets
  4. import Worker from "./jsqr-copy.js" and pass that via the worker prop, i.e.:
<qrcode-stream :worker="Worker"></qrcode-stream>

Please check if this works for you.

Ky6uk commented 3 years ago

What about to add jsqr dependency to this project and bundle worker module with this local jsqr package. Users of vue-qrcode-reader can decide which worker will they use, local or remote one.

gruhn commented 3 years ago

As I said this is not trivial.

You can't just import dependencies in web workers. You can importScripts as we are doing right now. Or bundle all dependencies together with the worker source code. For that matter you probably need a dedicated webpack loader like worker-loader and this should ideally be incorporated into the Vue CLI boilerplate config. You probably need to do that with webpack chaining.

If you manage to implement this or can find a better solution, please open a pull request.

mb-software commented 3 years ago

Hi, is there any solution for this? I am using latest V2.3.13 but I need it to work offline which it currently doesn't. Does anybody know an alternative which does work offline?

Thanks

gruhn commented 3 years ago

I tried to do that in the past but it's not trivial to get the configuration right when webworkers are involved. I fear it'll take a while before I find the time to do that properly. A workaround might be to:

  1. put jsQR.min.js together with your static assets
  2. copy src/worker/jsqr.js into your project
  3. modify importScripts part so jsQR.min.js is loaded from your static assets
  4. import Worker from "./jsqr-copy.js" and pass that via the worker prop, i.e.:
<qrcode-stream :worker="Worker"></qrcode-stream>

Please check if this works for you.

You could try this workaround.

mb-software commented 3 years ago

I tried to do that in the past but it's not trivial to get the configuration right when webworkers are involved. I fear it'll take a while before I find the time to do that properly. A workaround might be to:

  1. put jsQR.min.js together with your static assets
  2. copy src/worker/jsqr.js into your project
  3. modify importScripts part so jsQR.min.js is loaded from your static assets
  4. import Worker from "./jsqr-copy.js" and pass that via the worker prop, i.e.:
<qrcode-stream :worker="Worker"></qrcode-stream>

Please check if this works for you.

You could try this workaround.

I tried that but failed at step 3... importScripts from local (relative url) assets in inline worker doesn't seem as easy as it sounds like...

gruhn commented 3 years ago

Make sure the file is in a directory for static assets, i.e that is directly served by your web server/dev server. A directory where you usually put font files, images and so on that should not be processed by webpack. If you are using Vue CLI that would be the public folder. importScripts does NOT work like import. It works like <script src="..."></script>.

mb-software commented 3 years ago

Thanks for your answer. Yes, jsQR.min.js is in the static directory for static assets. However, importScripts does not accept relative urls (like './jsQR.min.js'). It throws an error "invalid url". It only accepts absolute urls like 'http://localhost/jsQR.min.js'. This way it works, but it works only on localhost of course...

gruhn commented 3 years ago

I see. Ok, that's unfortunate but can you set the origin dynamically with self.location.origin ? So something like this:

importScripts(self.location.origin + '/static/jsQR.min.js')
mb-software commented 3 years ago

Thanks a lot. That seems to work

ghost commented 3 years ago

I tried to do that in the past but it's not trivial to get the configuration right when webworkers are involved. I fear it'll take a while before I find the time to do that properly. A workaround might be to:

  1. put jsQR.min.js together with your static assets
  2. copy src/worker/jsqr.js into your project
  3. modify importScripts part so jsQR.min.js is loaded from your static assets
  4. import Worker from "./jsqr-copy.js" and pass that via the worker prop, i.e.:
<qrcode-stream :worker="Worker"></qrcode-stream>

Please check if this works for you.

@gruhn I'm getting stuck on step 4. I'm using Nuxt and I'm not sure where to put the import or how to pass it in via a prop. Also, it seems like "Worker" isn't defined in worker/jsqr.js

Here's my page file:

<template>
  <div>
    <h1>Test</h1>
    <qrcode-stream :worker="Worker" />
  </div>
</template>

<script>
import { QrcodeStream } from 'vue-qrcode-reader'
import { Worker } from '@/assets/js/jsqr.js'
export default {
  components: { QrcodeStream },
  head: {
    title: 'Activate posters',
  },
}
</script>

And assets/js/jsqr.js (the file copied from step 2):

const inlineWorker = (func) => {
  const functionBody = func
    .toString()
    .trim()
    .match(/^function\s*\w*\s*\([\w\s,]*\)\s*{([\w\W]*?)}$/)[1]

  return new Worker(
    URL.createObjectURL(new Blob([functionBody], { type: 'text/javascript' }))
  )
}

export default () => {
  return inlineWorker(function () {
    self.importScripts(self.location.origin + '/jsQR.min.js')

    self.addEventListener('message', function (event) {
      const imageData = event.data
      /* eslint-disable no-undef */
      const result = jsQR(imageData.data, imageData.width, imageData.height)
      /* eslint-enable */

      let content = null
      let location = null

      if (result !== null) {
        content = result.data
        location = result.location
      }

      const message = { content, location, imageData }
      self.postMessage(message, [imageData.data.buffer])
    })
  })
}

Thank you for your time.

ghost commented 3 years ago

Nevermind--I think I got it working. Sorry for pinging you, I didn't realize that there was documentation about the worker prop.

I think my problem was putting Worker inside of { } in the import statement. Here's what I used to get it working:

<template>
  <div>
    <h1>Test</h1>
    <qrcode-stream :worker="Worker" />
  </div>
</template>

<script>
import { QrcodeStream } from 'vue-qrcode-reader'
import Worker from '@/assets/js/jsqr.js'
export default {
  components: { QrcodeStream },
  data() {
    return {
      Worker,
    }
  },
  head: {
    title: 'Activate posters',
  },
}
</script>
gruhn commented 3 years ago

Nevermind--I think I got it working. Sorry for pinging you, I didn't realize that there was documentation about the worker prop

@ccsaposs Nevermind. This whole thing is arguably confusing.

ghost commented 3 years ago

Okay, it looks like I didn't get it working all the way. Right now, I'm running into an issue on line 8552 of dist/VueQrcodeReader.umd.js, version 2.3.14. Error from minfied file: TypeError: t is not a constructor (I believe this corresponds to Worker on line 8552 of the non-minified file.

I did some testing after import Worker from '@/assets/js/jsqr.js', and it looks like Worker is a function. Is that how it should be?

@gruhn Do you know how to fix this issue?

Thank you for your time.

gruhn commented 3 years ago

Yes, Worker is a function.

Let me repeat the full explanation in more detail here. Also because the naming is confusing. jsqr.js and jsQR.min.js are completely different files.

1)

Download jsQR.min.js and put into a static asset directory such that it's directly served by your web server / dev server. So you COULD (don't actually do it) embed it with a regular script tag like this:

<script src="/my-static-directory/jsQR.min.js"></script>

2)

Copy this file into your Vue project (not necessarily your static assets directory). Let's call this file worker.js from now on. Now change the path here

https://github.com/gruhn/vue-qrcode-reader/blob/bf32fa0b4b9c3473949a68d629b784ce04c96477/src/worker/jsqr.js#L15-L17

to

self.importScripts(
   self.location.origin + "/my-static-directory/jsQR.min.js"
)

AND CHANGE NOTHING ELSE. Note that the code of the function that is passed to inlineWorker is literally converted to a String and parsed with a regular expression. Merely using an arrow function there instead would break everything. If webpack is processing this file in a weird way that might also break the code. Maybe its safer to just store the code as a string directly (you loose transpilation, syntax highlighting and so on though).

const inlineWorker = functionBody => {
  return new Worker(
    URL.createObjectURL(new Blob([functionBody], { type: "text/javascript" }))
  );
};

export default () => {
  /* eslint-disable no-undef */
  return inlineWorker(`
    self.importScripts(
      self.location.origin + '/my-static-directory/jsQR.min.js'
    );

    self.addEventListener("message", function(event) {
      const imageData = event.data;
      const result = jsQR(imageData.data, imageData.width, imageData.height);

      let content = null;
      let location = null;

      if (result !== null) {
        content = result.data;
        location = result.location;
      }

      const message = { content, location, imageData };
      self.postMessage(message, [imageData.data.buffer]);
    });
  `);
  /* eslint-enable */
};

3)

Finally import the default export from worker.js and pass that as-is via the worker prop to the component.

<template>
    <qrcode-stream :worker="Worker" />
</template>

<script>
import { QrcodeStream } from 'vue-qrcode-reader'
import Worker from './path/to/worker.js'
export default {
  components: { QrcodeStream },
  data() {
    return {
      Worker,
    }
  }
}
</script>
ghost commented 3 years ago

@gruhn Thank you for your response!

I went through your steps and confirmed that matched what I did. I'm still running into the same issue: Screenshot 2020-11-18 at 8 53 33 AM

I also tried storing the code as a string and ran into the same issue.

gruhn commented 3 years ago

Ok, I think your initial point might be the key here. We construct an instance of the passed worker with the new keyword but this does not work for arrow functions. I think the reason we haven't noticed this yet, is that arrow functions get usually transpiled to regular functions. From the keyword modern in your error message, I infer that this is not true in your case.

ghost commented 3 years ago

Thank you for looking into this!

I think I have an idea of what you're saying, but I don't understand much. At this time, is there any action I should take or any information I can provide to help?

Thank you again!

gruhn commented 3 years ago

I already pushed a fix attempt but the pipeline is stalled for some reason: https://travis-ci.org/github/gruhn/vue-qrcode-reader/jobs/744525998

I'll give an update when it's through.

gruhn commented 3 years ago

Ok, please check if this works for you:

npm install vue-qrcode-reader@2.3.15-alpha.1
ghost commented 3 years ago

It works! Thank you!

gruhn commented 3 years ago

cool, non-alpha release is out: npm i vue-qrcode-reader@2.3.14

florianduchene commented 3 years ago

Hey there !

I'm sorry to reopen this subject, but it seems I'm having the same issue using the 2.3.16 version. Got a message "Failed to execute 'importScripts' on 'WorkerGlobalsScope'..." Didn't understand all the answers above, so I'm probably doing something wrong, any idea what it could be ?

gruhn commented 3 years ago

The central issue is still not resolved. The new version mentioned above only fixed an issue with the given workaround. You still have to follow these steps (have you tried that already?):

Yes, Worker is a function.

Let me repeat the full explanation in more detail here. Also because the naming is confusing. jsqr.js and jsQR.min.js are completely different files.

1)

Download jsQR.min.js and put into a static asset directory such that it's directly served by your web server / dev server. So you COULD (don't actually do it) embed it with a regular script tag like this:

<script src="/my-static-directory/jsQR.min.js"></script>

2)

Copy this file into your Vue project (not necessarily your static assets directory). Let's call this file worker.js from now on. Now change the path here

https://github.com/gruhn/vue-qrcode-reader/blob/bf32fa0b4b9c3473949a68d629b784ce04c96477/src/worker/jsqr.js#L15-L17

to

self.importScripts(
   self.location.origin + "/my-static-directory/jsQR.min.js"
)

AND CHANGE NOTHING ELSE. Note that the code of the function that is passed to inlineWorker is literally converted to a String and parsed with a regular expression. Merely using an arrow function there instead would break everything. If webpack is processing this file in a weird way that might also break the code. Maybe its safer to just store the code as a string directly (you loose transpilation, syntax highlighting and so on though).

const inlineWorker = functionBody => {
  return new Worker(
    URL.createObjectURL(new Blob([functionBody], { type: "text/javascript" }))
  );
};

export default () => {
  /* eslint-disable no-undef */
  return inlineWorker(`
    self.importScripts(
      self.location.origin + '/my-static-directory/jsQR.min.js'
    );

    self.addEventListener("message", function(event) {
      const imageData = event.data;
      const result = jsQR(imageData.data, imageData.width, imageData.height);

      let content = null;
      let location = null;

      if (result !== null) {
        content = result.data;
        location = result.location;
      }

      const message = { content, location, imageData };
      self.postMessage(message, [imageData.data.buffer]);
    });
  `);
  /* eslint-enable */
};

3)

Finally import the default export from worker.js and pass that as-is via the worker prop to the component.

<template>
    <qrcode-stream :worker="Worker" />
</template>

<script>
import { QrcodeStream } from 'vue-qrcode-reader'
import Worker from './path/to/worker.js'
export default {
  components: { QrcodeStream },
  data() {
    return {
      Worker,
    }
  }
}
</script>
Techn1c4l commented 3 years ago

I've spent a couple of hours trying to do this on a Cordova Android app. The workaround with the custom worker mentioned here does not work on Cordova. importScripts is unable to load the jsqr.js file. You need to use the jsQR package in your worker.

Instead, use this worker code:

import jsQR from "jsqr";

self.addEventListener("message", function(event) {
  const imageData = event.data;

  const result = jsQR(imageData.data, imageData.width, imageData.height, {
    inversionAttempts: "dontInvert"
  });

  let content = null;
  let location = null;

  if (result !== null) {
    content = result.data;
    location = result.location;
  }

  const message = { content, location, imageData };

  self.postMessage(message, [imageData.data.buffer]);
});
gruhn commented 3 years ago

@Techn1c4l could you make it work that way or are you proposing this solution?

The problem is to instantiate a WebWorker you need to pass a file path, Usually a JavaScript file

new Worker("./worker-source-code.js")

Because workers don't support import jsQR from "jsqr" directly we have two options:

  1. import dependencies with importScripts as we do now
  2. make the bundler inline the entire code of jsQR

Point two is appealing but let me quote myself on that:

As I said this is not trivial.

You can't just import dependencies in web workers. You can importScripts as we are doing right now. Or bundle all dependencies together with the worker source code. For that matter you probably need a dedicated webpack loader like worker-loader and this should ideally be incorporated into the Vue CLI boilerplate config. You probably need to do that with webpack chaining.

If you manage to implement this or can find a better solution, please open a pull request.

Techn1c4l commented 3 years ago

@gruhn The code I posted works both on web development server and on Android. I just installed the jsQR package and put the code above in my worker file. And it works alright.

gruhn commented 3 years ago

Have you tried non-Chrome based browsers?

https://caniuse.com/mdn-javascript_statements_import_worker_support

Techn1c4l commented 3 years ago

@gruhn I work on the Cordova Android project, so no, I didn't try. By the way, the solution I'm suggesting was taken from https://github.com/gruhn/vue-qrcode-reader/blob/91ee3fc8bf2f7fab96ac3f0a5d84d2d4c09b012f/src/worker/jsqr.js.

So if I understand correctly, this issue does not have a 100%-working solution for every case. You can't use importScripts on Cordova because it results in Uncaught DOMException: Failed to execute 'importScripts' error. And we can't use import in web worker environment.

Edit: Well, it seems like just copying and pasting the whole jsQR source code into the worker file is the only reliable solution for every case.

gruhn commented 3 years ago

:tada: This issue has been resolved in version 3.0.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket: