英文:
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 <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.
答案1
得分: 1
你需要知道脚本何时准备好,以便使用提供的代码,并且理论上你可能需要在 onMounted
中执行此操作,因为只有在 DOM 存在时才能追加。
在将 tokenizerScript
追加到文档之前,需要考虑是否之前已经运行过,并且如果脚本已经加载,可以查看此帖子以防止重复加载(忽略它是关于 Chrome 扩展的事实):
你应该将 tokenizer 放在自己的方法中 (funciton doTokenize() { ... new Tokenizer() ... })
,如果脚本已加载,则可以调用该方法,或者在加载后通过回调进行调用。
为等待 tokenizer.js
加载,应该在执行 appendChild(tokenizerScript)
之前注册一个 onload
回调函数。
tokenizerScript.defer = true
tokenizerScript.onload = () => {
doTokenize()
}
document.head.appendChild(tokenizerScript);
竞争条件
我认为这是一个竞争条件,其中 @click="tokenizer.submit()"
将在脚本加载之前呈现,它一开始就是错误的,并且保持错误。
你需要防止在模板中呈现内容,以便它不尝试绑定尚不存在的方法。
你可以使用异步设置方法,它可以提供一个非常干净的设置,通过使用 Promise 加载脚本,这样你可以使用 Vue 3.3.4+ 中的 Suspense 功能等待其完成,或者你可以使用非常快速和简单的准备就绪状态:
<script setup>
import {ref, onMounted} from 'vue'
const ready = ref(false)
let tokenizer = null // 你的变量需要在 onMounted 之前声明
function doTokenize() {
tokenizer = new Tokenizer({...})
// 在 doTokenize 中将 ready 设置为 true。
// 如果你检查脚本是否已加载并且在 onload 之外调用此函数,
// 则希望立即将 ready 状态设置为 true。
ready.value = true
}
onMounted(() => {
...
}
</script>
<template>
<div class="mb-6 max-w-2xl">
<template v-if="ready">
<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()">
提交付款
</button>
</template>
<template v-else>
正在加载...
</template>
</div>
</template>
英文:
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):
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 = () => {
doTokenize()
}
document.head.appendChild(tokenizerScript);
Race condition
I think this is a race condition where @click="tokenizer.submit()"
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:
<script setup>
import {ref, onMounted} from 'vue'
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(() => {
...
}
</script>
<template>
<div class="mb-6 max-w-2xl">
<template v-if="ready">
<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>
</template>
<template v-else>
Loading...
</template>
</div>
</template>
答案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.
<script>
export default {
mounted() {
let tokenizerScript= document.createElement('script')
tokenizerScript .setAttribute('src', 'https://app.2apgateway.com/tokenizer/tokenizer.js')
document.head.appendChild(tokenizerScript)
}
}
</script>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论