无需登录 数据私有 本地保存

Import Map 演示 - 浏览器端模块映射实验

14
0
0
0

Import Map 演示实验室

在浏览器中直接使用裸模块标识符(Bare Module Specifiers),无需打包工具。编辑 Import Map 映射和模块代码,实时预览执行结果。

Import Map JSON
模块代码 (JavaScript)
--:-- 等待执行...点击上方 "执行代码" 按钮开始实验。
当前生成的 HTML 文档结构

                
常见问题与知识点
什么是 Import Map?
Import Map 是一个 Web 标准,允许在浏览器中通过 <script type="importmap"> 标签定义模块标识符到 URL 的映射关系。它让开发者可以直接使用 import React from 'react' 这样的裸模块标识符,而无需通过打包工具解析。浏览器会根据 Import Map 将裸标识符转换为完整的 URL 进行加载。
哪些浏览器支持 Import Map?
Import Map 已被所有现代浏览器支持,包括 Chrome 89+、Edge 89+、Firefox 108+、Safari 16.4+。截至 2024 年,覆盖率达到 93%+ 的全球用户。对于不支持的旧浏览器,可以使用 es-module-shims 作为 polyfill。
Import Map 的 imports 和 scopes 有什么区别?
imports:全局映射,适用于所有模块。例如将 "react" 映射到 CDN 地址。
scopes:作用域映射,仅对特定路径下的模块生效。例如可以为 /admin/ 路径下的模块使用不同版本的依赖,实现精细化的依赖管理。
一个页面可以定义多个 Import Map 吗?
不可以。每个 HTML 文档只能有一个 <script type="importmap"> 标签。如果定义了多个,后面的会完全覆盖前面的。如果需要合并多个映射,必须在同一个 Import Map JSON 中完成所有配置。
Import Map 可以动态修改吗?
一旦页面中的模块开始加载,Import Map 就无法再被修改。它必须在任何 <script type="module"> 标签之前定义。如果需要动态切换映射,必须在模块加载之前通过 JavaScript 创建 importmap 脚本元素(但这种方法在不同浏览器中行为可能不一致,不推荐用于生产环境)。
使用 Import Map 还需要打包工具吗?
Import Map 可以减少对打包工具的依赖,但不能完全替代。对于小型项目和原型开发,Import Map + CDN 是非常高效的方案。但在生产环境中,打包工具仍然提供 Tree Shaking、代码压缩、CSS 处理、静态资源优化等 Import Map 不具备的能力。两者可以配合使用——打包工具处理应用代码,Import Map 管理外部依赖。
常见的模块 CDN 有哪些推荐?
esm.sh:专为 ES Modules 设计,自动处理 CJS 转 ESM,支持 TypeScript。
unpkg:从 npm 直接提供文件,适合加载已包含 ESM 构建的包。
jsDelivr:全球 CDN,速度优秀,支持 ESM 格式。
Skypack:已停止维护,但部分包仍可使用。推荐迁移至 esm.sh。
遇到 "Failed to resolve module specifier" 错误怎么办?
这个错误表示浏览器无法解析模块标识符。常见原因:① Import Map 中缺少对应的映射;② 映射的 URL 地址不正确或资源不可用;③ 模块标识符拼写错误(区分大小写);④ CDN 服务暂时不可达。可以先在本工具中测试映射是否有效。
`; $generatedHtmlPreview.text(html); } // 清除控制台输出 function clearOutput() { messageQueue = []; $outputConsole.html('
--:--控制台已清除,等待执行...
'); } // 向控制台添加日志 function appendLog(method, args, isSystem = false) { const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }); const cssClass = isSystem ? 'system' : (method + '-method'); const safeArgs = args.map(a => { if (typeof a === 'string') return a.replace(//g, '>'); return String(a).replace(//g, '>'); }); const line = $('
' + '' + timestamp + '' + safeArgs.join(' ') + '
'); $outputConsole.append(line); // 滚动到底部 $outputConsole.scrollTop($outputConsole[0].scrollHeight); } // 执行代码 function executeCode() { const importMapStr = $mapEditor.val().trim(); const userCode = $codeEditor.val().trim(); // 验证 Import Map JSON let importMapJson; try { importMapJson = JSON.parse(importMapStr || '{}'); } catch (e) { clearOutput(); appendLog('error', ['❌ Import Map JSON 解析错误: ' + e.message], true); $execStatus.html('JSON 错误'); $jsonStatus.removeClass('ok pending').addClass('err'); return; } $jsonStatus.removeClass('err pending').addClass('ok'); // 验证代码 if (!userCode) { clearOutput(); appendLog('warn', ['⚠️ 请先输入模块代码'], true); $execStatus.html('代码为空'); return; } // 清除输出 clearOutput(); $execStatus.html('执行中...'); $runBtn.prop('disabled', true).html('执行中'); // 构建 iframe 内容 const importMapEscaped = JSON.stringify(importMapJson); const iframeHtml = `
`; // 使用 Blob URL 加载 iframe(比 srcdoc 在某些浏览器中更可靠) const blob = new Blob([iframeHtml], { type: 'text/html;charset=utf-8' }); const blobUrl = URL.createObjectURL(blob); // 释放之前的 blob URL const oldUrl = $previewFrame.data('blobUrl'); if (oldUrl) { URL.revokeObjectURL(oldUrl); } $previewFrame.data('blobUrl', blobUrl); // 设置超时 const timeoutId = setTimeout(function() { if ($runBtn.prop('disabled')) { appendLog('system', ['⏱️ 执行超时或未收到输出。如果代码包含异步操作,输出可能在稍后出现。'], true); $execStatus.html('已完成(可能含异步操作)'); $runBtn.prop('disabled', false).html('执行代码'); } }, 4000); // 监听 iframe 加载 $previewFrame.off('load.importmaplab').on('load.importmaplab', function() { clearTimeout(timeoutId); // 再给一小段延迟以捕获可能在 load 之后到达的消息 setTimeout(function() { if ($runBtn.prop('disabled')) { $execStatus.html('执行完成'); $runBtn.prop('disabled', false).html('执行代码'); } }, 300); }); // 设置 src 触发加载 $previewFrame.attr('src', blobUrl); // 更新生成的 HTML 预览 updateGeneratedHtml(); } // 监听来自 iframe 的消息 $(window).off('message.importmaplab').on('message.importmaplab', function(e) { const data = e.originalEvent.data; if (!data || typeof data !== 'object') return; // 验证消息来源(简化处理:检查是否来自当前 iframe) const frameWindow = $previewFrame[0]?.contentWindow; if (e.originalEvent.source !== frameWindow && data.type !== 'test') return; if (data.type === 'console') { appendLog(data.method, data.args); } else if (data.type === 'error') { appendLog('error', ['❌ ' + data.message]); if (data.stack) { appendLog('error', [' ' + data.stack.split('\n').slice(0, 3).join('\n ')]); } $execStatus.html('执行出错'); } }); // 预设按钮点击 $('#preset-buttons').on('click', '.preset-btn', function() { const presetName = $(this).data('preset'); loadPreset(presetName); clearOutput(); $execStatus.text(''); // 清空 iframe $previewFrame.attr('src', 'about:blank'); }); // 执行按钮 $runBtn.on('click', executeCode); // 清除输出按钮 $('#btn-clear-output').on('click', function() { clearOutput(); $execStatus.text(''); }); // 格式化 JSON 按钮 $('#btn-format-json').on('click', function() { try { const parsed = JSON.parse($mapEditor.val()); $mapEditor.val(JSON.stringify(parsed, null, 2)); updateJsonStatus(); updateGeneratedHtml(); appendLog('info', ['✅ Import Map JSON 已格式化'], true); } catch (e) { appendLog('error', ['❌ 无法格式化: JSON 格式错误 - ' + e.message], true); } }); // 复制 Import Map $('#btn-copy-map').on('click', function() { const text = $mapEditor.val(); if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { appendLog('info', ['📋 Import Map 已复制到剪贴板'], true); }).catch(function() { appendLog('error', ['❌ 复制失败'], true); }); } else { // fallback $mapEditor.select(); document.execCommand('copy'); appendLog('info', ['📋 Import Map 已复制到剪贴板'], true); } }); // 复制代码 $('#btn-copy-code').on('click', function() { const text = $codeEditor.val(); if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { appendLog('info', ['📋 代码已复制到剪贴板'], true); }).catch(function() { appendLog('error', ['❌ 复制失败'], true); }); } else { $codeEditor.select(); document.execCommand('copy'); appendLog('info', ['📋 代码已复制到剪贴板'], true); } }); // 复制完整 HTML $('#btn-copy-html').on('click', function() { updateGeneratedHtml(); const html = $generatedHtmlPreview.text(); if (navigator.clipboard) { navigator.clipboard.writeText(html).then(function() { appendLog('info', ['📋 完整 HTML 文档已复制到剪贴板'], true); }).catch(function() { appendLog('error', ['❌ 复制失败'], true); }); } else { const tempTextarea = $('