refactor(loader): 简化全局加载测试(by AI)
This commit is contained in:
parent
5c6a4a883a
commit
2b0128cef7
@ -11,7 +11,7 @@
|
|||||||
### A. 极简带版本模式 (防缓存)
|
### A. 极简带版本模式 (防缓存)
|
||||||
```html
|
```html
|
||||||
<!-- 使用 ?v= 指定全局版本,所有依赖包都会自动带上该版本后缀 -->
|
<!-- 使用 ?v= 指定全局版本,所有依赖包都会自动带上该版本后缀 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/loader@1.0.6/dist/loader.min.js?v=1.0.12#state,base,datatable"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/loader@1.0.7/dist/loader.min.js?v=1.0.23#state,base,datatable"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### B. 简写 Hash 模式
|
### B. 简写 Hash 模式
|
||||||
@ -41,11 +41,11 @@ Loader 内部维护了完整的依赖链。例如:
|
|||||||
|
|
||||||
### `__apigo_load(...pkgs)`
|
### `__apigo_load(...pkgs)`
|
||||||
- **`__apigo_load('base')`**: 加载默认稳定版。
|
- **`__apigo_load('base')`**: 加载默认稳定版。
|
||||||
- **`__apigo_load('state:1.0.11')`**: 指定特定包的版本。
|
- **`__apigo_load('state:1.0.23')`**: 指定特定包的版本。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 开发者提示 (AI 必读)
|
## 开发者提示 (AI 必读)
|
||||||
1. **参数优先级**: URL 查询参数 `?load=` 优先级高于 Hash 参数 `#`。
|
1. **参数优先级**: URL 查询参数 `?load=` 优先级高于 Hash 参数 `#`。
|
||||||
2. **同步执行**: Loader 利用浏览器对动态插入脚本的顺序解析机制,保障了依赖项在业务代码运行前就绪。
|
2. **同步执行**: Loader 利用浏览器对动态插入脚本的顺序解析机制,保障了依赖项在业务代码运行前就绪。
|
||||||
3. **自清理**: `__apigo_load` 仅在 `<script type="module">` 运行前可用。
|
3. **自清理**: `__apigo_load` 仅在页面初始化期间可用。
|
||||||
|
|||||||
10
dist/loader.js
vendored
10
dist/loader.js
vendored
@ -3,15 +3,15 @@
|
|||||||
})(function() {
|
})(function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
const DEFAULT_VERSIONS = {
|
const DEFAULT_VERSIONS = {
|
||||||
"state": "1.0.21",
|
"state": "1.0.23",
|
||||||
"bootstrap": "1.0.8",
|
"bootstrap": "1.0.9",
|
||||||
"base": "1.0.20",
|
"base": "1.0.23",
|
||||||
"datatable": "1.0.15",
|
"datatable": "1.0.16",
|
||||||
"kanban": "1.0.2",
|
"kanban": "1.0.2",
|
||||||
"mindmap": "1.0.2",
|
"mindmap": "1.0.2",
|
||||||
"chart": "1.0.2",
|
"chart": "1.0.2",
|
||||||
"editor": "1.0.2",
|
"editor": "1.0.2",
|
||||||
"loader": "1.0.5"
|
"loader": "1.0.7"
|
||||||
};
|
};
|
||||||
const DEPENDENCY_GRAPH = {
|
const DEPENDENCY_GRAPH = {
|
||||||
"bootstrap": ["state"],
|
"bootstrap": ["state"],
|
||||||
|
|||||||
2
dist/loader.min.js
vendored
2
dist/loader.min.js
vendored
@ -1 +1 @@
|
|||||||
!function(t){"function"==typeof define&&define.amd?define(t):t()}(function(){"use strict";const t={state:"1.0.21",bootstrap:"1.0.8",base:"1.0.20",datatable:"1.0.15",kanban:"1.0.2",mindmap:"1.0.2",chart:"1.0.2",editor:"1.0.2",loader:"1.0.5"},e={bootstrap:["state"],base:["state","bootstrap"],datatable:["state","bootstrap","base"],editor:["state","bootstrap","base"],mindmap:["state","bootstrap","base"]},n={_loaded:new Set,_v:null,load:(...s)=>{if("undefined"==typeof document)return;const c=document.currentScript,o=c&&c.src?c.src:"";let a="";const r=o.includes(".min.js")?".min.js":".js";if(o&&(o.includes("jsdelivr.net")||o.includes("unpkg.com")||o.includes("npm.elemecdn.com"))){let t="https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}";o.includes("unpkg.com")?t="https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}":o.includes("npm.elemecdn.com")&&(t="https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}"),a=t+r}else if(o){const t=o.substring(0,o.lastIndexOf("/")+1);a=t+"{project}"+r}else a="https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}"+r;const i=s.map(t=>{const e=String(t).trim(),n=e.indexOf(":");return{name:-1===n?e:e.slice(0,n),version:-1===n?null:e.slice(n+1)}}).filter(t=>t.name),d=[],l={},p=t=>{const n=t.name.toLowerCase();t.version&&(l[n]=t.version);(e[n]||[]).forEach(t=>p({name:t})),d.includes(n)||d.push(n)};i.forEach(t=>p(t));const m="loading"===document.readyState;d.forEach(e=>{if(n._loaded.has(e))return;const s=l[e]||t[e]||"latest";let c=a.replace(/{project}/g,e).replace(/{tag}/g,s);if(n._v){const t=c.includes("?")?"&":"?";c+=`${t}v=${n._v}`}if(m)document.write(`<script src="${c}"><\/script>`);else{const t=document.createElement("script");t.src=c,document.head.appendChild(t)}n._loaded.add(e)})}};if("undefined"!=typeof document&&document.currentScript){const t=new URL(document.currentScript.src,location.href);n._v=t.searchParams.get("v");let e=t.searchParams.get("load");const s=t.hash.substring(1);!e&&s&&(e=s.startsWith("load=")?s.substring(5):s),e&&n.load(...e.split(","))}globalThis.__apigo_load=n.load});
|
!function(t){"function"==typeof define&&define.amd?define(t):t()}(function(){"use strict";const t={state:"1.0.23",bootstrap:"1.0.9",base:"1.0.23",datatable:"1.0.16",kanban:"1.0.2",mindmap:"1.0.2",chart:"1.0.2",editor:"1.0.2",loader:"1.0.7"},e={bootstrap:["state"],base:["state","bootstrap"],datatable:["state","bootstrap","base"],editor:["state","bootstrap","base"],mindmap:["state","bootstrap","base"]},n={_loaded:new Set,_v:null,load:(...s)=>{if("undefined"==typeof document)return;const c=document.currentScript,o=c&&c.src?c.src:"";let a="";const r=o.includes(".min.js")?".min.js":".js";if(o&&(o.includes("jsdelivr.net")||o.includes("unpkg.com")||o.includes("npm.elemecdn.com"))){let t="https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}";o.includes("unpkg.com")?t="https://unpkg.com/@apigo.cc/{project}@{tag}/dist/{project}":o.includes("npm.elemecdn.com")&&(t="https://npm.elemecdn.com/@apigo.cc/{project}@{tag}/dist/{project}"),a=t+r}else if(o){const t=o.substring(0,o.lastIndexOf("/")+1);a=t+"{project}"+r}else a="https://cdn.jsdelivr.net/npm/@apigo.cc/{project}@{tag}/dist/{project}"+r;const i=s.map(t=>{const e=String(t).trim(),n=e.indexOf(":");return{name:-1===n?e:e.slice(0,n),version:-1===n?null:e.slice(n+1)}}).filter(t=>t.name),d=[],l={},p=t=>{const n=t.name.toLowerCase();t.version&&(l[n]=t.version);(e[n]||[]).forEach(t=>p({name:t})),d.includes(n)||d.push(n)};i.forEach(t=>p(t));const m="loading"===document.readyState;d.forEach(e=>{if(n._loaded.has(e))return;const s=l[e]||t[e]||"latest";let c=a.replace(/{project}/g,e).replace(/{tag}/g,s);if(n._v){const t=c.includes("?")?"&":"?";c+=`${t}v=${n._v}`}if(m)document.write(`<script src="${c}"><\/script>`);else{const t=document.createElement("script");t.src=c,document.head.appendChild(t)}n._loaded.add(e)})}};if("undefined"!=typeof document&&document.currentScript){const t=new URL(document.currentScript.src,location.href);n._v=t.searchParams.get("v");let e=t.searchParams.get("load");const s=t.hash.substring(1);!e&&s&&(e=s.startsWith("load=")?s.substring(5):s),e&&n.load(...e.split(","))}globalThis.__apigo_load=n.load});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@apigo.cc/loader",
|
"name": "@apigo.cc/loader",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/loader.js",
|
"main": "dist/loader.js",
|
||||||
"module": "dist/loader.js",
|
"module": "dist/loader.js",
|
||||||
|
|||||||
10
src/index.js
10
src/index.js
@ -4,15 +4,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const DEFAULT_VERSIONS = {
|
const DEFAULT_VERSIONS = {
|
||||||
'state': '1.0.21',
|
'state': '1.0.23',
|
||||||
'bootstrap': '1.0.8',
|
'bootstrap': '1.0.9',
|
||||||
'base': '1.0.20',
|
'base': '1.0.23',
|
||||||
'datatable': '1.0.15',
|
'datatable': '1.0.16',
|
||||||
'kanban': '1.0.2',
|
'kanban': '1.0.2',
|
||||||
'mindmap': '1.0.2',
|
'mindmap': '1.0.2',
|
||||||
'chart': '1.0.2',
|
'chart': '1.0.2',
|
||||||
'editor': '1.0.2',
|
'editor': '1.0.2',
|
||||||
'loader': '1.0.6'
|
'loader': '1.0.7'
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEPENDENCY_GRAPH = {
|
const DEPENDENCY_GRAPH = {
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Loader Integration Test</title>
|
|
||||||
<!-- 引入本地构建的 loader.js -->
|
|
||||||
<script src="../dist/loader.js"></script>
|
|
||||||
<script>
|
|
||||||
// 声明加载:
|
|
||||||
// state 采用默认版本号(验证常规版本拼接)
|
|
||||||
// bootstrap 采用相对路径(验证本地重定向)
|
|
||||||
// base 采用绝对 HTTP 路径(验证自定义域名重定向)
|
|
||||||
Loader.load(
|
|
||||||
'state',
|
|
||||||
'bootstrap:./mock-bootstrap.js',
|
|
||||||
'base:http://127.0.0.1:8084/test/mock-base.js'
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="status">loading</div>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
import { Bootstrap } from '@apigo.cc/bootstrap';
|
|
||||||
import { HTTP } from '@apigo.cc/base';
|
|
||||||
|
|
||||||
// 验证导入的重定向模块是否正确加载
|
|
||||||
if (Bootstrap && Bootstrap.isMock && HTTP && HTTP.isMock) {
|
|
||||||
document.getElementById('status').textContent = 'success';
|
|
||||||
} else {
|
|
||||||
document.getElementById('status').textContent = 'failed';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,33 +1,43 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
test('loader dynamic importmap generation and custom path redirect', async ({ page }) => {
|
test('loader injects dependency script urls', async ({ page }) => {
|
||||||
page.on('console', msg => console.log('BROWSER LOG:', msg.text()));
|
page.on('console', msg => console.log('BROWSER LOG:', msg.text()));
|
||||||
|
|
||||||
// 打开测试页面
|
|
||||||
await page.goto('http://127.0.0.1:8084/test/index.html');
|
|
||||||
|
|
||||||
// 1. 验证生成的 importmap 标签结构
|
await page.setContent(`
|
||||||
const importMapJson = await page.evaluate(() => {
|
<!DOCTYPE html>
|
||||||
const script = document.querySelector('script[type="importmap"]');
|
<html>
|
||||||
return script ? JSON.parse(script.textContent) : null;
|
<head>
|
||||||
|
<script src="http://127.0.0.1:8084/dist/loader.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="status">loading</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, { waitUntil: 'commit' });
|
||||||
|
|
||||||
|
await page.waitForFunction(() => typeof window.__apigo_load === 'function');
|
||||||
|
await page.waitForFunction(() => document.readyState !== 'loading');
|
||||||
|
|
||||||
|
const urls = await page.evaluate(() => {
|
||||||
|
const captured = [];
|
||||||
|
const originalAppend = document.head.appendChild.bind(document.head);
|
||||||
|
|
||||||
|
document.head.appendChild = node => {
|
||||||
|
if (node && node.tagName === 'SCRIPT' && node.src) {
|
||||||
|
captured.push(node.src);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
__apigo_load('state', 'bootstrap', 'base');
|
||||||
|
|
||||||
|
document.head.appendChild = originalAppend;
|
||||||
|
return captured;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(importMapJson).not.toBeNull();
|
expect(urls).toEqual([
|
||||||
expect(importMapJson.imports).toBeDefined();
|
'https://cdn.jsdelivr.net/npm/@apigo.cc/state@1.0.23/dist/state.js',
|
||||||
|
'https://cdn.jsdelivr.net/npm/@apigo.cc/bootstrap@1.0.9/dist/bootstrap.js',
|
||||||
// 2. 检查 state (常规 CDN 映射) 是否生成正确
|
'https://cdn.jsdelivr.net/npm/@apigo.cc/base@1.0.23/dist/base.js'
|
||||||
const stateUrl = importMapJson.imports['@apigo.cc/state'];
|
]);
|
||||||
expect(stateUrl).toContain('cdn.jsdelivr.net/npm/@apigo.cc/state');
|
|
||||||
|
|
||||||
// 3. 检查 bootstrap (本地相对路径重定向) 是否保留
|
|
||||||
const bootstrapUrl = importMapJson.imports['@apigo.cc/bootstrap'];
|
|
||||||
expect(bootstrapUrl).toBe('./mock-bootstrap.js');
|
|
||||||
|
|
||||||
// 4. 检查 base (本地绝对 HTTP 重定向) 是否保留
|
|
||||||
const baseUrl = importMapJson.imports['@apigo.cc/base'];
|
|
||||||
expect(baseUrl).toBe('http://127.0.0.1:8084/test/mock-base.js');
|
|
||||||
|
|
||||||
// 5. 验证重定向的 mock 模块确实可以被 ESM 成功导入并运行 (通过页面 status 的变化)
|
|
||||||
const status = page.locator('#status');
|
|
||||||
await expect(status).toHaveText('success');
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export const HTTP = { isMock: true, name: 'base-mock' };
|
|
||||||
export const UI = { toast: () => {} };
|
|
||||||
export default { HTTP, UI };
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export const Bootstrap = { isMock: true, name: 'bootstrap-mock' };
|
|
||||||
export default Bootstrap;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user