@supermapgis/webpack-plugin-loadsdk
v1.0.0
Published
Webpack 插件,用于在 Webpack 项目中集成 @supermapgis/clientx 或 @supermapgis/iclient3d。通过 target 参数切换目标包。提供以下功能:1) 自动设置 window.SUPERMAP3D_BASE_URL(用户无需手动设置);2) 自动注入 Widget CSS link 标签;3) dev server 静态资源代理(wasm/worker/图片等);4) 构建后自动复制运行时静态资源到产物目录(支持 glob 过滤);5) 自动配置 re
Readme
@supermapgis/webpack-plugin-loadsdk
用于在 Webpack 项目中一键集成 @supermapgis/clientx 或 @supermapgis/iclient3d 三维 GIS SDK 的官方 Webpack 插件。
为什么需要这个插件
@supermapgis/clientx / @supermapgis/iclient3d 是复杂的 WebGL 三维 GIS SDK,其 NPM 包包含两部分:
- ESM 模块代码(
SourceRelease/):供打包器按需引入和 tree-shaking - 运行时静态资源(
Build/<assetsDirName>/):WASM 文件、Worker 脚本、纹理图片、Widget CSS 等
直接在 Webpack 项目中使用会面临以下问题,本插件自动完成全部处理:
- SDK 运行时通过
window.SUPERMAP3D_BASE_URL定位静态资源,需手动注入 - Widget CSS 需手动引入
- dev server 需配置静态资源代理,将
/<assetsDirName>/映射到node_modules npm run build后dist/需要包含静态资源,否则部署后页面 404draco3d等库引用了 Node.jsfs/path模块,Webpack 5 不再自动 polyfill,需配置resolve.fallback- 扁平化后部分动态
import()仍引用原始文件名,Webpack 预解析时报 "Module not found" - 包内部分 bare import 的依赖未安装,Webpack 默认中断构建(esbuild 预打包则会跳过)
- 混淆器导出名称不一致导致 "export 'X' was not found" 构建错误
- 包内小写路径导入(
core/Accessor.js)与实际目录(Core/)大小写不一致,产生 "multiple modules" 警告 - dev 模式下 SDK 源码可通过 DevTools 直接调试,缺乏保护
安装
npm install @supermapgis/webpack-plugin-loadsdk @supermapgis/clientx注意: 需同时在项目根目录
package.json中配置overrides强制依赖版本:"overrides": { "@zip.js/zip.js": "2.7.57", "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz" }
快速开始
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const SuperMapLoadSDKWebpackPlugin = require('@supermapgis/webpack-plugin-loadsdk');
module.exports = (env, argv) => ({
entry: './src/index.js',
output: {
filename: 'bundle/bundle.js',
chunkFilename: 'bundle/[name].[contenthash:8].js',
assetModuleFilename: 'assets/[contenthash][ext][query]',
publicPath: argv.mode === 'development' ? '/' : './',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.(wasm|hdr|ktx2|terrain|glb|gltf|png|jpg|svg|xml|json)$/, type: 'asset/resource' },
{
// SourceRelease 是 ESM 模块,但 import 路径省略了 .js 扩展名,
// Webpack 5 对 ESM 要求 fully specified,这里关闭它以兼容
test: /\.m?js$/,
resolve: { fullySpecified: false },
},
],
// 禁用 AMD 解析器,避免 sprintfjs 的 "define cannot be used indirect" 错误
parser: { javascript: { amd: false } },
},
plugins: [
new HtmlWebpackPlugin({ template: './index.html' }),
new SuperMapLoadSDKWebpackPlugin({ target: 'clientx' }),
],
devServer: {
// devServer 必须存在,插件在其 static 配置上追加 SDK 资源目录的映射
static: { directory: path.resolve(__dirname, 'dist') },
hot: true,
port: 8085,
open: true,
},
resolve: {
mainFields: ['module', 'browser', 'main'],
symlinks: false,
},
experiments: {
asyncWebAssembly: true,
},
});// src/index.js
import * as ClientX from '@supermapgis/clientx';
const viewer = new ClientX.Viewer('Container', {});target 参数
插件通过 target 参数切换目标 SDK 包,内部自动推导 packagePath、assetsDirName 等常量:
| target | packagePath | assetsDirName | 入口文件 |
|--------|-------------|---------------|----------|
| 'clientx'(默认) | @supermapgis/clientx | ClientX | clientx.js |
| 'iclient3d' | @supermapgis/iclient3d | SuperMap3D | supermap3d.js |
// 集成 @supermapgis/clientx(默认,可省略 target)
new SuperMapLoadSDKWebpackPlugin({ target: 'clientx' })
// 集成 @supermapgis/iclient3d
new SuperMapLoadSDKWebpackPlugin({ target: 'iclient3d' })配置项
| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| target | 'clientx' \| 'iclient3d' | 'clientx' | 目标 SDK 包,决定 packagePath / assetsDirName 等内部常量 |
| packagePath | string | 根据 target 自动推导 | npm 包路径,可显式覆盖 |
| assetsDirName | string | 根据 target 自动推导 | 运行时静态资源目录名,可显式覆盖 |
| copyFolders | string[] | 见下方 | build 后复制的目录(全量模式生效) |
| baseUrl | string | 自动计算 | 覆盖 SUPERMAP3D_BASE_URL,dev 用 /<assetsDirName>/,build 用 ./<assetsDirName>/ |
| isAddWidgetCSS | boolean | true | 是否自动注入 Widget CSS link 标签 |
| preventDebug | boolean | true | dev 模式下是否禁止调试 SDK 模块,设为 false 可关闭此功能以便排查问题 |
| staticAssets | object | { filter: false, keepFiles: [] } | 静态资源复制控制 |
copyFolders 默认值:
['Assets', 'Workers', 'ThirdParty', 'Widgets', 'language', 'ReactiveWidgets', 'SkyAtmosphereSystem']staticAssets 子选项:
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| filter | boolean | false | false = 全量拷贝 copyFolders 中的所有目录;true = 仅拷贝 keepFiles 匹配的文件 |
| keepFiles | string[] | [] | glob 规则列表,路径相对于 Build/<assetsDirName>/,仅在 filter: true 时生效 |
注意:
staticAssets.filter为true时依赖glob,该依赖已包含在本插件的dependencies中,安装插件时会自动安装,无需手动添加。
插件内部结构
Webpack 插件是一个 Class,通过 apply(compiler) 注册多个钩子,自动完成以下工作:
| 功能 | 钩子 | 作用 |
|------|------|------|
| 防调试保护 | compiler 初始化 | dev 模式下禁用 SDK 包的 source map,防止用户在 DevTools 中调试 SDK 代码 |
| 缺失文件空模块回退 | resolver.hooks.file | 扁平化后悬空 import 的文件不存在时,回退到空模块 _empty_module.js |
| bare import 空模块回退 | resolver.hooks.resolve | 目标包内发起的 bare import 无法解析时,回退为空模块并输出 warning |
| named export 修复 | module.rules | 混淆器导出名称不一致时降级为警告,不阻断构建 |
| 路径大小写规范化 | resolver.hooks.file | 将 core/Accessor.js 规范化为 Core/Accessor.js,消除 multiple modules 警告 |
| Node.js polyfill | resolve.fallback | 自动配置 fs: false、path: false,禁止 Webpack 尝试 polyfill Node.js 核心模块 |
| 自动注入 BASE_URL | processAssets | 在 bundle.js 最前面注入 window.SUPERMAP3D_BASE_URL |
| dev server 资源代理 | devServer.static | 将 /<assetsDirName>/ 路径映射到 node_modules 中的资源目录 |
| build 资源复制 | afterEmit | 将运行时静态资源复制到 dist/<assetsDirName>/(全量或过滤模式) |
| Widget CSS 注入 | alterAssetTags | 在 HTML <head> 中插入 <link> 引入 widgets.css |
防调试保护(preventDebug)
解决的问题: Webpack dev 模式默认使用 eval-cheap-module-source-map,模块代码会作为独立文件出现在 DevTools Sources 面板中,用户可以直接查看源码并设置断点。
工作方式(仅 dev 模式,preventDebug: true 时启用):
- 设置
compiler.options.devtool = false,禁用 eval 包装,模块代码不再作为独立文件出现 - 注册
SourceMapDevToolPlugin,通过exclude排除目标包(使用包根路径正则匹配),不为其生成 source map
效果: 用户无法在 DevTools 中查看 SDK 的可读源码或设置有效断点。用户自身的业务代码仍可正常调试。
缺失文件空模块回退
解决的问题: 目录扁平化后,部分动态 import 上下文中仍引用原始文件名(如 import("./ModelMesh.js")),但文件已被重命名为哈希名(如 _9a65fcb3.js)。Webpack 在构建阶段会预先解析动态 import 上下文中的所有文件,导致 "Module not found" 错误。
工作方式:
- 插件初始化时在
SourceRelease/目录下创建_empty_module.js(内容为export default {};) - 在 resolver 的
filehook(stage: -100,先于 FileExistsPlugin 执行)中检查文件是否存在,不存在则重定向到空模块 - 范围限定:仅处理目标包内的
.js文件,排除入口文件(如clientx.js/supermap3d.js)、_shaderdecoder.js、空模块自身,以及包目录本身被当作.js文件解析的情况
bare import 空模块回退
解决的问题: esbuild 预打包时对解析失败的 bare import 会跳过/externalize,不阻断构建;而 Webpack 默认严格报 "Module not found" 错误并中断构建。
工作方式:
- 在 resolver 的
resolvehook 中拦截 bare import(如import xxx from 'some-package') - 对每个 bare import,先用
require.resolve探测是否可解析(结果缓存,避免重复探测) - 如果无法解析且包目录也不存在,则回退为空模块,构建可继续完成;如果包目录存在但
require.resolve失败(如纯 ESM 包无 require 导出条件),则交还 Webpack 自行解析,不回退,避免误吞真实存在的依赖 - 范围限定:仅处理 issuer(导入发起文件)位于
@supermapgis/clientx或@supermapgis/iclient3d包内的情况(两个包均覆盖,无论当前 target 是哪个),用户项目自身源码中的导入错误不受影响,仍按 Webpack 默认行为报错 - 构建结束时在
finishModules阶段汇总输出 warning(仅提示,不阻断构建),提示形式:bare import "xxx" 无法解析(被 N 个文件引用),已回退为空模块
named export 修复
解决的问题: javascript-obfuscator 混淆时可能对不同文件中的同名导出重命名不一致,导致 "export 'X' was not found" 构建错误。
工作方式:
- 通过
module.rules对SourceRelease目录内的.js文件设置exportsPresence/importExportsPresence/reexportExportsPresence为'warn'(仅警告不报错;不用false是因为 false 会完全跳过导出检查,可能导致有效导出无法解析) - 通过
ignoreWarnings过滤来自目标包的导出警告和缺失文件警告(正则匹配assetsDirName或SourceRelease,已转义正则特殊字符) - 不影响用户项目中其他 JS 文件的导出检查行为
路径大小写规范化
解决的问题: 包内部分文件用小写路径导入(如 ../core/Accessor.js),而实际目录是大写 Core/。Windows 不区分大小写所以能运行,但 Webpack 会把 core/Accessor.js 和 Core/Accessor.js 当作两个不同模块,产生 "multiple modules with names that only differ in casing" 警告。
工作方式: 在 resolver 的 file hook(stage: -100)中,读取文件系统上的实际目录条目,将路径规范化为实际大小写(带缓存,避免重复读取文件系统)。仅处理路径中包含 SourceRelease 的请求。
自动注入 BASE_URL
解决的问题: SDK 运行时需要通过 window.SUPERMAP3D_BASE_URL 定位静态资源(WASM、Worker、纹理等)的 URL 路径。
工作方式: 在 processAssets(OPTIMIZE_INLINE 阶段)中,向所有 initial chunk 的 .js 文件最前面注入:
window.SUPERMAP3D_BASE_URL="/ClientX/"; // dev 模式
window.SUPERMAP3D_BASE_URL="./ClientX/"; // build 模式路径自动适配:
| 模式 | 注入的 BASE_URL |
|------|----------------|
| dev | /<assetsDirName>/ |
| build | ./<assetsDirName>/ |
| 自定义 | options.baseUrl |
注意:
window.SUPERMAP3D_BASE_URL在 clientx 和 iclient3d 两个 target 下保持一致,不随 target 变化。
dev server 资源代理
工作方式: 向 compiler.options.devServer.static 追加 { directory: <assetsDir>, publicPath: '/<assetsDirName>/' },webpack-dev-server 自动提供 /<assetsDirName>/ 路径下的静态资源。
重要: 插件仅在
devServer配置对象已存在时才追加映射。如果webpack.config.js中完全不配置devServer,插件无法挂载静态资源代理,项目启动后将找不到ClientX/下的 WASM、Worker、纹理图片等静态资源,页面报 404。因此devServer是必须配置的(至少包含static字段),其余选项(hot、port、open等)按需调整。
build 资源复制
工作方式: 在 afterEmit 钩子中(非 dev 模式时执行),将 Build/<assetsDirName>/ 下的资源目录复制到 dist/<assetsDirName>/:
- 全量模式(默认): 复制
copyFolders中配置的所有目录 - 过滤模式: 仅复制
keepFiles中 glob 规则匹配的文件,减少产物体积
Widget CSS 注入
工作方式: 通过 html-webpack-plugin 的 alterAssetTags 钩子,在 HTML <head> 中插入 <link rel="stylesheet" href="<baseUrl>Widgets/widgets.css">(isAddWidgetCSS 为 true 时才注入)。
插件不处理、需要手动配置的项
以下配置插件不会自动注入,需在 webpack.config.js 中手动配置(参见快速开始中的完整示例):
| 配置项 | 推荐值 | 原因 |
|--------|--------|------|
| resolve.mainFields | ['module', 'browser', 'main'] | 优先使用 module 字段(指向 SourceRelease),否则 Webpack 可能解析到 main 字段 |
| resolve.symlinks | false | 避免 file: 协议安装的本地 tgz 产生软链解析问题 |
| module.rules 的 fullySpecified: false | 对 .m?js 启用 | SourceRelease 的 import 路径省略了 .js 扩展名,Webpack 5 对 ESM 要求 fully specified |
| parser.javascript.amd | false | 避免 sprintfjs 的 "define cannot be used indirect" 错误 |
| devServer | 至少包含 static | 插件在其上追加 SDK 资源目录映射,见上文说明 |
| experiments.asyncWebAssembly | true | SDK 的 WASM 模块需要异步 WebAssembly 支持 |
| CSS 相关 loader | style-loader + css-loader | 处理项目自身的 CSS 引入 |
配置示例
默认配置(推荐)
自动计算 baseUrl,自动注入 Widget CSS,开启防调试,全量拷贝静态资源:
const SuperMapLoadSDKWebpackPlugin = require('@supermapgis/webpack-plugin-loadsdk');
plugins: [
new HtmlWebpackPlugin({ template: './index.html' }),
new SuperMapLoadSDKWebpackPlugin(),
]切换 target
// 集成 @supermapgis/clientx
new SuperMapLoadSDKWebpackPlugin({ target: 'clientx' })
// 集成 @supermapgis/iclient3d
new SuperMapLoadSDKWebpackPlugin({ target: 'iclient3d' })关闭防调试(排查问题时使用)
dev 模式下需要调试 SDK 模块代码时,可关闭防调试:
new SuperMapLoadSDKWebpackPlugin({ target: 'clientx', preventDebug: false })自定义 baseUrl
当静态资源不在默认的 ./ClientX/ 路径下时(如部署到 CDN 或自定义目录):
new SuperMapLoadSDKWebpackPlugin({ target: 'clientx', baseUrl: './ClientX/' })关闭 Widget CSS 注入
如果项目中已手动引入 widgets.css,或不需要 Widget 样式:
new SuperMapLoadSDKWebpackPlugin({ target: 'clientx', isAddWidgetCSS: false })静态资源过滤(减小产物体积)
默认全量拷贝会将所有静态资源目录复制到 dist/ClientX/,如果项目只需要部分资源,可开启过滤模式,仅拷贝匹配的文件:
new SuperMapLoadSDKWebpackPlugin({
target: 'clientx',
staticAssets: {
filter: true, // 开启过滤模式
keepFiles: [ // 仅拷贝匹配以下 glob 规则的文件
'Assets/**/*', // 纹理、模型等资源
'SkyAtmosphereSystem/**/*', // 天空大气资源
'ThirdParty/**/*', // 第三方库(WASM 等)
'Widgets/**/*', // Widget CSS
'Workers/**/*', // Worker 脚本
]
}
})常用 glob 模式:
| 模式 | 说明 |
|------|------|
| Assets/**/* | Assets 目录下的所有文件(含子目录) |
| Workers/*.js | Workers 根目录下的所有 .js 文件 |
| Workers/**/*.js | Workers 及其子目录下的所有 .js 文件 |
| ThirdParty/**/*.wasm | ThirdParty 目录下所有 WASM 文件 |
| Widgets/**/*.css | 所有 Widget CSS 文件 |
用户代码写法
插件配置好后,用户代码非常简洁,无需任何手动设置:
全量导入(默认):
import * as ClientX from '@supermapgis/clientx';
const viewer = new ClientX.Viewer("Container", {});
window.viewer = viewer;按需导入(利用 tree-shaking 减小产物体积):
import { Viewer } from '@supermapgis/clientx';
const viewer = new Viewer("Container", {});import { Cartesian3, Cartographic, Math } from '@supermapgis/clientx';
const c3 = Cartesian3.fromDegrees(116.45, 39.91, 5);缓存清理
当依赖更新后页面未生效,可清除构建缓存:
rmdir /s /q node_modules\.cache
npm run dev