将外部JS文件加载到Vue 3组件的<script setup>中可用。

huangapple go评论68阅读模式
英文:

Load external JS to be available in <script setup> in Vue 3 component

问题

I am attempting to implement a tokenized payment form in my Vue3/InertiaJS/Laravel app, and am unable to get the external script loaded so that it is available in the <script setup> to define a value and attach it to a DOM element. Per the docs, I am doing this:

<script setup> 
    const tokenizerScript = document.createElement("script");
    tokenizerScript.setAttribute(
        "src",
        "https://app.2apgateway.com/tokenizer/tokenizer.js"
    );
    tokenizerScript.setAttribute(
        'language',
        'javascript'
    )
    document.head.appendChild(tokenizerScript);

    const tokenizer = new Tokenizer({
        url: apiUrl, 
        apikey: publicApiKey,
        container: '#container', 
        // Callback after submission request has been made
        submission: (resp) => { // Figure out what response you got back
            switch(resp.status) {
                case 'success':
                    // Successful submission
                    console.log(resp.token)
                    Inertia.post(`/users/${props.user.id}/dues/submit-online-payment`, {
                        token: resp.token
                    })
                    break;
                case 'error':
                    // Encountered an error while performing submission
                    console.log(resp.msg)
                    break;
                case 'validation':
                    // Encountered form validation specific errors
                    console.log(resp.invalid)
                    break;
            }
        }
    })
</script>

<template>
    <div id="container" />
</template>

No matter how I do it, whether this way, or using <link rel="prefetch">, or trying to load it in the onMounted or onBeforeMount hooks, I get an error that Tokenizer is not defined. Often it will work if I go back to the previous page using the browser back button and come back, but not always.

How can I get this script loaded so that when I call it in <script setup>, it is fully available?

UPDATE: Per Marc's suggestion below, I edited the code like this:

function doTokenize() {
    const tokenizer = new Tokenizer({
        url: apiUrl, // Optional - Only needed if domain is different than the one your on, example: localhost
        apikey: publicApiKey,
        container: '#container', // Make sure that this "container" is the same as the id of the div container you referenced above.
        // Callback after submission request has been made
        submission: (resp) => { // Figure out what response you got back
            switch (resp.status) {
                case 'success':
                    // Successful submission
                    console.log(resp.token)
                    Inertia.post(`/users/${props.user.id}/dues/submit-online-payment`, {
                        token: resp.token
                    })
                    break;
                case 'error':
                    // Encountered an error while performing submission
                    console.log(resp.msg)
                    break;
                case 'validation':
                    // Encountered form validation specific errors
                    console.log(resp.invalid)
                    break;
            }
        }
    })
}

onMounted(() => {
    // debugger;
    const tokenizerScript = document.createElement("script");
    tokenizerScript.setAttribute(
        "src",
        "https://app.2apgateway.com/tokenizer/tokenizer.js"
    );
    tokenizerScript.setAttribute(
        'language',
        'javascript'
    )

    tokenizerScript.defer = true

    tokenizerScript.onload = () => {
        doTokenize()
    }
    document.head.appendChild(tokenizerScript);
})

<template>
    <div class="mb-6 max-w-2xl">
        <h2 class="font-bold text-2xl">Pay Online Form</h2>

        <p>Amount to be charged: <span class="font-bold">${{ total }}</span></p>

        <div class="mt-4" id="container"></div>

        <button class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md ml-2"
                @click="tokenizer.submit()"
        >
            Submit Payment
        </button>

    </div>
</template>

and the form loads right away and without error, which is great. However, when clicking on my Submit Payment button, I get this error now:

Uncaught TypeError: can't access property "submit", _ctx.tokenizer is undefined

FIX: I finally realized that the issue was a simple matter of scope; I was defining tokenizer inside a function, but it wasn't making it available externally. Once I made this change:

let tokenizer = {};

function doTokenize() {
    tokenizer = new Tokenizer({
        url: apiUrl, // Optional - Only needed if domain is different than the one your on, example: localhost
        apikey: publicApiKey,
        container: '#container', // Make sure that this "container" is the same as the id of the div container you referenced above.
        // Callback after submission request has been made
        submission: (resp) => { // Figure out what response you got back
            switch (resp.status) {
                case 'success':
                    // Successful submission
                    console.log(resp.token)
                    Inertia.post(`/users/${props.user.id}/dues/submit-online-payment`, {
                        token: resp.token
                    })
                    break;
                case 'error':
                    // Encountered an error while performing submission
                    console.log(resp.msg)
                    break;
                case 'validation':
                    // Encountered form validation specific errors
                    console.log(resp.invalid)
                    break;
            }
        }
    })
}

it worked perfectly.

英文:

I am attempting to implement a tokenized payment form in my Vue3/InertiaJS/Laravel app, and am unable to get the external script loaded so that it is available in the &lt;script setup&gt; to define a value and attach it to a DOM element. Per the docs, I am doing this

&lt;script setup&gt; 
const tokenizerScript = document.createElement(&quot;script&quot;);
tokenizerScript.setAttribute(
&quot;src&quot;,
&quot;https://app.2apgateway.com/tokenizer/tokenizer.js&quot;
);
tokenizerScript.setAttribute(
&#39;language&#39;,
&#39;javascript&#39;
)
document.head.appendChild(tokenizerScript);
const tokenizer = new Tokenizer({
url: apiUrl, 
apikey: publicApiKey,
container: &#39;#container&#39;, 
// Callback after submission request has been made
submission: (resp) =&gt; { // Figure out what response you got back
switch(resp.status) {
case &#39;success&#39;:
// Successful submission
console.log(resp.token)
Inertia.post(`/users/${props.user.id}/dues/submit-online-payment`, {
token: resp.token
})
break;
case &#39;error&#39;:
// Encountered an error while performing submission
console.log(resp.msg)
break;
case &#39;validation&#39;:
// Encountered form validation specific errors
console.log(resp.invalid)
break;
}
}
})
&lt;/script&gt;
&lt;template&gt;
&lt;div id=&quot;container&quot; /&gt;
&lt;/template&gt;

No matter how I do it, whether this way, or using &lt;link rel=&quot;prefetch&quot;&gt;, or trying to load it in the onMounted or onBeforeMount hooks, I get an error that Tokenizer is not defined. Often it will work if I go back to the previous page using the browser back button and come back, but not always

How can I get this script loaded so that when I call it in &lt;script setup&gt;, it is fully available?

UPDATE: Per Marc's suggestion below, I edited the code like this

    function doTokenize() {
const tokenizer = new Tokenizer({
url: apiUrl, // Optional - Only needed if domain is different than the one your on, example: localhost
apikey: publicApiKey,
container: &#39;#container&#39;, // Make sure that this &quot;container&quot; is the same as the id of the div container you referenced above.
// Callback after submission request has been made
submission: (resp) =&gt; { // Figure out what response you got back
switch (resp.status) {
case &#39;success&#39;:
// Successful submission
console.log(resp.token)
Inertia.post(`/users/${props.user.id}/dues/submit-online-payment`, {
token: resp.token
})
break;
case &#39;error&#39;:
// Encountered an error while performing submission
console.log(resp.msg)
break;
case &#39;validation&#39;:
// Encountered form validation specific errors
console.log(resp.invalid)
break;
}
}
})
}
onMounted(() =&gt; {
// debugger;
const tokenizerScript = document.createElement(&quot;script&quot;);
tokenizerScript.setAttribute(
&quot;src&quot;,
&quot;https://app.2apgateway.com/tokenizer/tokenizer.js&quot;
);
tokenizerScript.setAttribute(
&#39;language&#39;,
&#39;javascript&#39;
)
tokenizerScript.defer = true
tokenizerScript.onload = () =&gt; {
doTokenize()
}
document.head.appendChild(tokenizerScript);
})
&lt;template&gt;
&lt;div class=&quot;mb-6 max-w-2xl&quot;&gt;
&lt;h2 class=&quot;font-bold text-2xl&quot;&gt;Pay Online Form&lt;/h2&gt;
&lt;p&gt;Amount to be charged: &lt;span class=&quot;font-bold&quot;&gt;${{ total }}&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;mt-4&quot; id=&quot;container&quot;&gt;&lt;/div&gt;
&lt;button class=&quot;mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md ml-2&quot;
@click=&quot;tokenizer.submit()&quot;
&gt;
Submit Payment
&lt;/button&gt;
&lt;/div&gt;
&lt;/template&gt;

and the form loads right away and without error, which is great. However, when clicking on my Submit Payment button, I get this error now:

Uncaught TypeError: can&#39;t access property &quot;submit&quot;, _ctx.tokenizer is undefined

FIX I finally realized that the issue was a simple matter of scope; I was defining tokenizer inside a function, but it wasn't making it available externally. Once I made this change:

let tokenizer = {};
function doTokenize() {
tokenizer = new Tokenizer({
url: apiUrl, // Optional - Only needed if domain is different than the one your on, example: localhost
apikey: publicApiKey,
container: &#39;#container&#39;, // Make sure that this &quot;container&quot; is the same as the id of the div container you referenced above.
// Callback after submission request has been made
submission: (resp) =&gt; { // Figure out what response you got back
switch (resp.status) {
case &#39;success&#39;:
// Successful submission
console.log(resp.token)
Inertia.post(`/users/${props.user.id}/dues/submit-online-payment`, {
token: resp.token
})
break;
case &#39;error&#39;:
// Encountered an error while performing submission
console.log(resp.msg)
break;
case &#39;validation&#39;:
// Encountered form validation specific errors
console.log(resp.invalid)
break;
}
}
})
}

it worked perfectly.

答案1

得分: 1

你需要知道脚本何时准备好,以便使用提供的代码,并且理论上你可能需要在 onMounted 中执行此操作,因为只有在 DOM 存在时才能追加。


在将 tokenizerScript 追加到文档之前,需要考虑是否之前已经运行过,并且如果脚本已经加载,可以查看此帖子以防止重复加载(忽略它是关于 Chrome 扩展的事实):

https://stackoverflow.com/questions/26368874/how-to-determine-if-a-script-has-already-been-injected-into-the-document-from-ch


你应该将 tokenizer 放在自己的方法中 (funciton doTokenize() { ... new Tokenizer() ... }),如果脚本已加载,则可以调用该方法,或者在加载后通过回调进行调用。

为等待 tokenizer.js 加载,应该在执行 appendChild(tokenizerScript) 之前注册一个 onload 回调函数。

tokenizerScript.defer = true

tokenizerScript.onload = () =&gt; {
  doTokenize()
}

document.head.appendChild(tokenizerScript);

竞争条件

我认为这是一个竞争条件,其中 @click=&quot;tokenizer.submit()&quot; 将在脚本加载之前呈现,它一开始就是错误的,并且保持错误。

你需要防止在模板中呈现内容,以便它不尝试绑定尚不存在的方法。

你可以使用异步设置方法,它可以提供一个非常干净的设置,通过使用 Promise 加载脚本,这样你可以使用 Vue 3.3.4+ 中的 Suspense 功能等待其完成,或者你可以使用非常快速和简单的准备就绪状态:

&lt;script setup&gt;
import {ref, onMounted} from &#39;vue&#39;

const ready = ref(false)

let tokenizer = null // 你的变量需要在 onMounted 之前声明

function doTokenize() {

  tokenizer = new Tokenizer({...})

  // 在 doTokenize 中将 ready 设置为 true。
  // 如果你检查脚本是否已加载并且在 onload 之外调用此函数,
  // 则希望立即将 ready 状态设置为 true。
  ready.value = true

}

onMounted(() =&gt; {

  ...

}
&lt;/script&gt;


&lt;template&gt;

  &lt;div class=&quot;mb-6 max-w-2xl&quot;&gt;

    &lt;template v-if=&quot;ready&quot;&gt;

      &lt;h2 class=&quot;font-bold text-2xl&quot;&gt;Pay Online Form&lt;/h2&gt;

      &lt;p&gt;Amount to be charged: &lt;span class=&quot;font-bold&quot;&gt;${{ total }}&lt;/span&gt;&lt;/p&gt;

      &lt;div class=&quot;mt-4&quot; id=&quot;container&quot;&gt;&lt;/div&gt;

      &lt;button class=&quot;mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md ml-2&quot;
        @click=&quot;tokenizer.submit()&quot;&gt;
            提交付款
      &lt;/button&gt;

    &lt;/template&gt;

    &lt;template v-else&gt;
      正在加载...
    &lt;/template&gt;

  &lt;/div&gt;

&lt;/template&gt;

英文:

You will need to know when the script is ready in order to use the code it provides and you may need to do this in onMounted in theory because you can only append when the DOM exists to append it to.


Before you append tokenizerScript to the document, you need to consider if this has been ran before and if the script is already loaded, check out this post on how to prevent duplication (ignore the fact its about a chrome extension):

https://stackoverflow.com/questions/26368874/how-to-determine-if-a-script-has-already-been-injected-into-the-document-from-ch


You should put tokenizer in its own method (funciton doTokenize() { ... new Tokenizer() ... }) that you can call if the script is already loaded or defer calling it after it has loaded via a callback.

To wait for tokenizer.js to be loaded you should register an onload callback function before doing appendChild(tokenizerScript).

tokenizerScript.defer = true

tokenizerScript.onload = () =&gt; {
  doTokenize()
}

document.head.appendChild(tokenizerScript);

Race condition

I think this is a race condition where @click=&quot;tokenizer.submit()&quot; will be rendered before the script is loaded, it starts broken and remains broken.

You need to prevent rendering of the content in the template so it does not try to bind a method that does not yet exist.

You could use an async setup approach that would offer a very clean setup you load the script using Promise so you can await its completion using the Suspense feature in Vue 3.3.4+ or you can use a very quick and easy ready state:

&lt;script setup&gt;
import {ref, onMounted} from &#39;vue&#39;

const ready = ref(false)

let tokenizer = null // You var needs to be declared before onMounted

function doTokenize() {

  tokenizer = new Tokenizer({...})

  // Set ready to true in doTokenize.
  // if you do check if the script is already loaded and 
  // call this function outside of onload, they you want
  // the ready state to be true immediately.
  ready.value = true

}

onMounted(() =&gt; {

  ...

}
&lt;/script&gt;


&lt;template&gt;

  &lt;div class=&quot;mb-6 max-w-2xl&quot;&gt;

    &lt;template v-if=&quot;ready&quot;&gt;

      &lt;h2 class=&quot;font-bold text-2xl&quot;&gt;Pay Online Form&lt;/h2&gt;

      &lt;p&gt;Amount to be charged: &lt;span class=&quot;font-bold&quot;&gt;${{ total }}&lt;/span&gt;&lt;/p&gt;

      &lt;div class=&quot;mt-4&quot; id=&quot;container&quot;&gt;&lt;/div&gt;

      &lt;button class=&quot;mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md ml-2&quot;
        @click=&quot;tokenizer.submit()&quot;&gt;
            Submit Payment
      &lt;/button&gt;

    &lt;/template&gt;

    &lt;template v-else&gt;
      Loading...
    &lt;/template&gt;

  &lt;/div&gt;

&lt;/template&gt;

答案2

得分: 0

你可以通过在你的代码中使用 mounted() 函数来解决这个问题。

<script>
  export default {
    
    mounted() {
      let tokenizerScript= document.createElement('script')
      tokenizerScript.setAttribute('src', 'https://app.2apgateway.com/tokenizer/tokenizer.js')
      document.head.appendChild(tokenizerScript)
    }

  }
</script>
英文:

You can solve this by using mounted() function in your code.

&lt;script&gt;
export default {
mounted() {
let tokenizerScript= document.createElement(&#39;script&#39;)
tokenizerScript .setAttribute(&#39;src&#39;, &#39;https://app.2apgateway.com/tokenizer/tokenizer.js&#39;)
document.head.appendChild(tokenizerScript)
}
}
&lt;/script&gt;

huangapple
  • 本文由 发表于 2023年5月14日 11:51:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76245735.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定