Skip to content

Commit

Permalink
feat: new function - loadScript
Browse files Browse the repository at this point in the history
  • Loading branch information
GreatAuk committed Mar 7, 2023
1 parent b39cc3a commit d7a18ea
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 2 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pnpm add @utopia-utils/dom
* isIOS: 判断是否是 IOS 系统。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/dom/src/isIOS.ts)
* isWeixin: 判断是否是微信浏览器。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/dom/src/isWeixin.ts)
* isMobile: 判断是否是移动端浏览器。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/share/src/isMobile.ts)
* loadScript: 动态加载脚本。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/share/src/loadScript.ts)
### 杂项
* [defineDictionary](#defineDictionary): 定义业务字典。 **typesafe** [source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/defineDictionary.ts)
* ~~[createEnumFromOptions](#createEnumFromOptions): 通过 `options` 自动生成对应的 `enum`, 后期只需要维护 `options`**typesafe**[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/createEnumFromOptions.ts)~~
Expand All @@ -84,7 +85,7 @@ pnpm add @utopia-utils/dom
* callLimit: 限制函数调用次数。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/callLimit.ts)
* once: 限制函数只能调用一次。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/once.ts)
* encryptPhone: 加密手机号, 中间 4 位显示成 *[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/encryptPhone.ts)
* getByPath: 通过路径获取对象的值。**typesafe**[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/getByPath.ts)
* getByPath: 通过路径获取对象的值。**typesafe** [source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/getByPath.ts)
* arrayToCSV: 数组转换为 CSV 字符串。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/csv.ts)
* memoize: 创建一个会缓存返回结果的函数。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/memoize.ts)
* getFileName: 获取文件名。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/getFileName.ts)
Expand All @@ -95,7 +96,7 @@ pnpm add @utopia-utils/dom
* uniqueWith: 数组去重,使用自定义的比较函数。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/uniqueWith.ts)
* intersection: 两个数组交集。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/intersection.ts)
* deepClone: 深拷贝。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/deepClone.ts)
* deepEqual: 深拷贝[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/deepEqual.ts)
* deepEqual: 深比较[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/deepEqual.ts)
* compose: 函数组合, 从右到左执行。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/compose.ts)
* pipe: 函数组合, 从左到右执行。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/pipe.ts)
* onlyResolvesLast: 解决竞态问题,只保留最后一次调用的结果。[source](https://github.com/GreatAuk/utopia-utils/blob/main/packages/core/src/onlyResolvesLast.ts)
Expand Down
20 changes: 20 additions & 0 deletions example/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import { loadScript } from '@utopia-utils/core'
let unload_
const foo = () => {
console.log('[9]-App.vue', unload_)
unload_()
}
const loadScript_ = () => {
const { unload } = loadScript('https://unpkg.com/browse/axios@1.3.224/index.js', {
appendPosition: 'body',
onStatusChange: (status) => {
console.log(status)
},
})
unload_ = unload
}
</script>

<template>
<header>
<button @click="loadScript_">load</button>
<button @click="foo">unload</button>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />

<div class="wrapper">
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"keywords": [],
"scripts": {
"build": "pnpm --filter=./packages/** build",
"example": "pnpm --filter=example dev",
"lint": "eslint .",
"release": "bumpp package.json packages/**/package.json",
"test": "vitest",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/deepClone.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @vitest-environment happy-dom
*/
import { deepClone } from './deepClone'

describe('deepClone', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './isAndroid'
export * from './isIOS'
export * from './isMobile'
export * from './isWeixin'
export * from './loadScript'
export * from './waitForSelector'
76 changes: 76 additions & 0 deletions packages/dom/src/loadScript.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { loadScript } from './loadScript'

// TODO 每个用户单独跑没问题,但一直跑就会报错
describe.todo('loadScript', () => {
afterEach(() => {
vi.resetAllMocks()
document.body.innerHTML = ''
document.head.innerHTML = ''
})
const src = 'https://unpkg.com/@plugin-web-update-notification/core@1.3.1/dist/webUpdateNoticeInjectScript.js'
const getScriptTag = (): HTMLScriptElement | null =>
document.head.querySelector(`script[src="${src}"]`)

it('call unload, should remove script tag', async () => {
const removeListener = vi.spyOn(Element.prototype, 'remove')
const { unload } = loadScript(src)
expect(getScriptTag()).not.toBeNull()
unload()
expect(getScriptTag()).toBeNull()
expect(removeListener).toHaveBeenCalled()
})

it('should add script tag', async () => {
const appendChildListener = vitest.spyOn(document.head, 'appendChild')
const { scriptTag, unload } = loadScript(src)
expect(scriptTag).instanceof(HTMLScriptElement)
expect(scriptTag?.src).toBe(src)
expect(getScriptTag()).toBe(scriptTag)
expect(appendChildListener).toHaveBeenCalled()
unload()
})

it('should re-use the same src for multiple loads', async () => {
const addChildListener = vitest.spyOn(document.head, 'appendChild')

expect(addChildListener).not.toBeCalled()
expect(getScriptTag()).toBeNull()

const { scriptTag: scriptTag1 } = loadScript(src)
const { scriptTag: scriptTag2, unload } = loadScript(src)

expect(scriptTag1).not.toBeNull()
expect(scriptTag2).not.toBeNull()
expect(document.querySelectorAll(`script[src="${src}"]`)).toHaveLength(1)

expect(addChildListener).toBeCalledTimes(1)
expect(getScriptTag()).toBeInstanceOf(HTMLScriptElement)
unload()
})

it('should support attributes', async () => {
const { unload } = loadScript(src, {
attrs: { 'id': 'id-value', 'data-test': 'data-test-value' },
defer: true,
crossOrigin: 'anonymous',
})

const element = getScriptTag()
// expect(element).toBeInstanceOf(HTMLScriptElement)
expect(element?.getAttribute('id')).toBe('id-value')
expect(element?.getAttribute('data-test')).toBe('data-test-value')
expect(element?.defer).toBe(true)
expect(element?.crossOrigin).toBe('anonymous')
unload()
})
it('status change', async () => {
let status = ''
const onStatusChange = vi.fn((status_) => {
status = status_
})
const { unload } = loadScript(src, { onStatusChange })
expect(status).toBe('loading')
expect(onStatusChange).toBeCalledTimes(1)
unload()
})
})
98 changes: 98 additions & 0 deletions packages/dom/src/loadScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
interface LoadScriptOptions {
/**
* Add `async` attribute to the script tag
* @default true
*/
async?: boolean
/**
* Add `defer` attribute to the script tag
*/
defer?: boolean
/**
* Script type
*
* @default 'text/javascript'
* @see https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script#attr-type
*/
type?: string
crossOrigin?: 'anonymous' | 'use-credentials'
/**
* @see https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script#attr-referrerpolicy
*/
referrerPolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'
noModule?: boolean
/**
* Add custom attribute to the script tag
*
*/
attrs?: Record<string, string>
/**
* Append the script tag to the document.head or document.body
* @default 'head'
*/
appendPosition?: 'head' | 'body'
onStatusChange?: (status: 'loading' | 'loaded' | 'error') => void
}

export function loadScript(src: string, options?: LoadScriptOptions) {
const {
async = true,
defer,
type = 'text/javascript',
crossOrigin,
referrerPolicy,
attrs = {},
noModule,
appendPosition = 'head',
onStatusChange,
} = options || {}
let scriptTag = document.querySelector<HTMLScriptElement>(`script[src="${src}"]`)

// if the script is exist, it has high probability loaded
if (scriptTag)
onStatusChange?.('loaded')

if (!scriptTag) {
scriptTag = document.createElement('script')
scriptTag.src = src
scriptTag.type = type
scriptTag.async = async

if (defer)
scriptTag.defer = defer
if (crossOrigin)
scriptTag.crossOrigin = crossOrigin
if (referrerPolicy)
scriptTag.referrerPolicy = referrerPolicy
if (noModule)
scriptTag.noModule = noModule
Object.entries(attrs).forEach(([name, value]) => scriptTag?.setAttribute(name, value))

const appendTarget = appendPosition === 'head' ? document.head : document.body
appendTarget.appendChild(scriptTag)
onStatusChange?.('loading')
}

scriptTag?.addEventListener('load', () => {
onStatusChange?.('loaded')
})
scriptTag?.addEventListener('error', () => {
onStatusChange?.('error')
})
scriptTag?.addEventListener('abort', () => {
onStatusChange?.('error')
})

return {
/** remove the script tag */
unload: () => unload(src),
scriptTag,
}
}

/** remove the script tag */
function unload(src: string) {
const script = document.querySelector(`script[src="${src}"]`)
if (script)
script.remove()
}

0 comments on commit d7a18ea

Please sign in to comment.