资讯动态

Spring Boot用jpackage打造桌面级独立安装包的实战与避坑指南

发布时间:2026/10/16 18:18:53 来源:尧图企业网站定制
上周帮某内部运维团队把一个小型数据看板服务从java -jar的绿色版方案改成了双击安装、自动带快捷方式的独立安装包。整个过程用的主要是 JDK 自带的 jpackage 工具Spring Boot 项目本身几乎没改代码只是调整了打包脚本和部分启动参数的传递方式。这件事本身有不少资料讲过但实际做的时候还是踩了几个文档不会直接告诉你的坑。比如启动器闪退、工作目录变化导致配置文件读不到、Linux 打包时缺少工具链、 Windows 打 MSI 时对安装器的依赖等等。如果你也想把 Spring Boot 应用交付给不懂命令行的同事或者希望产品在客户面前更像一个正经软件这篇文章应该能帮你少走不少弯路。1. 为什么要把 Spring Boot 做成独立安装包1.1 一个真实的分发场景很多 Spring Boot 项目的交付方式其实就是一条命令把项目打成可执行 Jar丢给部署的人让对方java -jar app.jar。这套流程在研发环境里完全没问题但一旦目标使用者是业务部门、运维值班、客户现场的 IT 人员麻烦就来了。我当时遇到的场景是某团队需要一个内部数据看板后端是 Spring Boot前端静态资源打包在一个 Jar 里。应用部署到几台 Windows 办公电脑上使用者只希望桌面有个图标双击之后浏览器自动打开看板页面。他们不会开命令行也不愿意去配置 Java 环境更不关心进程是怎么拉起来的。如果直接把一个 Jar 发给对方意味着我需要先确保每台机器都装了匹配的 JDK还要帮他们配环境变量甚至可能要写一个启动脚本。这确实能做但每台机器手动操作一次等着你的就是无尽的环境问题。安装包方案就不一样了。它至少能解决三件事把 JRE 或 JDK 运行时一起带给用户不再依赖目标机器预装 Java提供一个标准的安装、卸载入口自动创建桌面快捷方式、开始菜单目录和文件关联。这些能力对于内部工具来说未必算刚需但在客户现场、外出演示、标准化交付这些场景里价值非常明显。1.2 可执行 Jar 方案的四个痛点我并不是说绿色版 Jar 一无是处它在服务器部署和容器化场景里仍然是最合适的选择。但如果你想把它作为最终交付物发给非技术用户通常要面对这几个问题。第一JDK 版本和指令集不匹配。目标机器上有 Java但版本比编译时低或者 32 位、64 位匹配不上java -jar会直接失败。你在自己电脑上运行正常换台机器就歇菜这个现象在混用办公设备的环境里太常见了。第二命令行启动门槛高。双击 Jar 在很多 Windows 机器上默认不会执行只会打开压缩软件列表。即使有启动脚本用户也可能把脚本路径搞错或者误改脚本导致路径失效。未受过训练的用户对命令行天然有距离感任何需要手动输入的命令都是隐患。第三缺少卸载和版本管理。绿色版目录删不干净、旧版本覆盖新版本时可能残留升级只能靠手动覆盖文件。如果应用在运行Windows 还会锁定 Jar 文件直接覆盖会失败真正搞起来用户根本不知道该怎么办。第四使用体验不像一个“产品”。没有应用图标、没有安装路径定义、没有版本号显示、没有启动 Do 文件关联。对客户来说这看起来就是一个临时脚本而不是可以正式使用的工具。jpackage 不是唯一能解决这些问题的方案但它是最接近“开箱可用”的路线工具内置于 JDK不引入额外的授权成本也不需要第三方的商业安装包制作工具。2. jpackage 到底是怎么工作的2.1 它支持哪些安装包格式jpackage 是 JDK 官方提供的打包工具从 JDK 14 开始孵化JDK 16 正式成为标准工具。它能根据当前操作系统把 Java 应用打包成对应平台的原生安装格式。平台安装包格式说明WindowsMSI、EXEMSI 适合企业批量分发EXE 安装体验更友好都支持静默安装参数LinuxDEB、RPM分别对应 Debian 系和 RedHat 系的发行版macOSDMG、PKGDMG 是常见的拖拽安装镜像PKG 适合需要安装向导的场景除了这些安装包格式jpackage 还支持app-image也就是不生成安装器只生成一个解压后就能运行的应用目录。这个格式太重要了后面我会专门讲。有一点需要特别注意jpackage 不支持交叉打包。在 Windows 上运行 jpackage 只能产出 Windows 安装包在 Linux 上只能产出 Linux 安装包在 macOS 上只能产出 macOS 安装包。如果你需要同时交付三个平台必须准备三条构建链路或者使用各自平台的 CI 执行器并行跑。2.2 从 Jar 到安装包的内部流程jpackage 的整个工作流程大致可以分成三个阶段。第一阶段是解析启动参数。它会读取你提供的输入目录、主 Jar、主类、应用名称、图标、JVM 参数等信息构建出一个应用配置描述。这个阶段也是出错率最高的地方尤其是在 Spring Boot 场景下主类不是业务类而是启动器类如果这一阶段配置错了后面的所有操作都是在浪费时间。第二阶段是生成应用镜像也就是app-image。jpackage 会把目标应用的 Jar 放到镜像的 app 目录下再把 JDK 运行时复制到镜像的 runtime 目录中最后生成一个平台原生的启动器。这个启动器本质上是一个可执行程序内部会调用java启动逻辑把 JVM 跑起来并加载你的主类。app-image 在 Windows 下对应一个带.exe的目录在 Linux 下是一个包含可执行文件的目录在 macOS 下是一个.app包。第三阶段是封装成安装包。如果指定了--type msi或--type exejpackage 会调用系统上的安装包制作工具把 app-image 再包装成安装器。Windows 下生成 MSI 和 EXE 时通常依赖 WiX Toolset 这类开源工具Linux 下生成 DEB 依赖dpkg相关工具RPM 依赖rpmbuildmacOS 的 DMG 和 PKG 则有系统自带工具支持。这个过程可以手动拆开来看先做 app-image再手动验证运行最后再打成安装包。这样做的好处是一旦安装包有问题你能立刻知道到底是应用本身启动失败还是封装阶段出了问题。2.3 为什么 Spring Boot 的可执行 Jar 能直接配合Spring Boot 默认打出来的可执行 Jar 和普通 Jar 有一个很大的区别真正的业务代码不在根目录而是被放在BOOT-INF/classes目录下依赖库则被放在BOOT-INF/lib目录下。启动时Spring Boot 通过自定义类加载器读取这些内部结构而不是简单依靠 JVM 的 classpath 机制。这就意味着想让 Spring Boot 应用通过 jpackage 启动你不能把业务类的主类直接传给 jpackage而是要设置成org.springframework.boot.loader.JarLauncher。JarLauncher 是 Spring Boot 提供的启动器它负责创建自定义类加载器、定位嵌套 Jar 并加载真正的应用启动类。jpackage 干的事情其实很朴素启动一个 JVM设置好 classpath然后调用指定主类。至于主类内部怎么加载资源、怎么管理嵌套 Jar那是 Spring Boot 自己的逻辑。所以你只需要让 jpackage 去调用 JarLauncher剩下的事情 Spring Boot 会自动完成。很多人在这一步卡住是因为把 jpackage 的主类参数写成了自己的Application类结果启动时 JVM 只能看到外层的 JarLauncher 类路径找不到BOOT-INF/classes里的业务类直接报ClassNotFoundException。我建议在打包脚本里显式写清楚主类是 JarLauncher不要依赖 jpackage 自动推断省得换版本或换机器时突然失效。3. 打包前的工程准备与配置检查清单3.1 版本搭配是第一个关键决策jpackage 本身要求 JDK 16 以上如果你还在用 JDK 8 或 JDK 11那就没办法直接用。Spring Boot 2.7.x 支持 JDK 8 到 JDK 20Spring Boot 3.x 要求 JDK 17 起。我在这个项目里用的是 JDK 17 加 Spring Boot 2.7.14。选择 JDK 17 的主要原因是它是 LTS 版本jpackage 从 17 开始已经足够稳定Spring Boot 2.7 又正好在维护周期内兼容性上比较保守不会因为版本太新遇到一堆新特性问题。如果你准备用 Spring Boot 3.x那 JDK 至少得是 17这个组合没问题但要提醒一点Spring Boot 3 默认使用 Jakarta EE 命名空间部分旧依赖如果还在用javax包打包后会直接启动失败。这类兼容性问题不是 jpackage 的锅却会在安装包里集中暴雷因为安装包环境比开发环境更接近“干净机器”各种缺依赖的问题藏不住。3.2 检查可执行 Jar 的 Main-Class在开始打安装包之前我强烈建议你先确认一下项目打出来的 Jar 是标准 Spring Boot 可执行 Jar。检查方法很简单直接在命令行里查看 Jar 包内的META-INF/MANIFEST.MF。jar xf demo-data-board.jar META-INF/MANIFEST.MF cat META-INF/MANIFEST.MF正常情况下Main-Class应该是org.springframework.boot.loader.JarLauncher然后旁边还有一行Start-Class那才是你项目里的业务主类。这两个字段的区别很容易被忽略记住一句话jpackage 关心Main-ClassSpring Boot 关心Start-Class。如果你发现Main-Class指向的不是 JarLauncher而是直接改成业务主类那就说明项目的 spring-boot-maven-plugin 可能没有正确执行 repackage或者打包方式被覆盖了。这时候先去检查 pom.xml确认 spring-boot-maven-plugin 的repackage目标已经绑定到 package 阶段。plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId executions execution goals goalrepackage/goal /goals /execution /executions /plugin3.3 提前备好图标、版本号和供应商信息安装包看起来“正经不正经”图标和元信息的作用很大。jpackage 里和图标相关的参数有平台差异Windows 用.icoLinux 用.pngmacOS 用.icns。你需要准备对应格式的图标文件打包时用--icon指定。版本号、应用名称、供应商、版权这些信息通过--app-version、--name、--vendor、--copyright传进去会显示在安装向导、程序卸载列表、文件属性等位置。这个细节对内部工具来说可能无所谓但如果应用要拿给外部客户看这些字段直接决定了专业度。还建议顺手写一个构建脚本比如build-package.sh把这些参数固化下来。不要每次打包都手敲命令因为你迟早会忘记某个参数然后花一下午找原因。4. 一个数据看板应用从 Jar 到安装包的完整实操4.1 第一步先用 app-image 验证运行我不是一上来就打 MSI而是先打一个 app-image 目录。这个目录其实就是安装包解压后的结果相当于把运行环境完整地暴露在你面前。任何启动问题在 app-image 阶段暴露得最直接排查也最方便。jpackage \ --type app-image \ --name DataBoard \ --app-version 1.0.0 \ --vendor 某内部团队 \ --input target \ --main-jar demo-data-board.jar \ --main-class org.springframework.boot.loader.JarLauncher \ --dest dist这条命令会把target目录下的 Jar 取出来生成dist/DataBoard/。Windows 下里面会有一个DataBoard.exeLinux 下是一个可执行文件macOS 下是.app目录。全局搜索一下dist/DataBoard下有没有启动器文件然后直接双击运行。我在这里遇到过一件很有意思的事如果直接把dist/DataBoard/DataBoard.exe双击运行应用能正常启动但旁边的runtime目录却不会自动出现在系统 PATH 里。jpackage 生成的启动器并不依赖 PATH 环境变量它会通过相对路径找到自身所在的 runtime 目录所以这个不用担心。确认 app-image 能启动后再做下一步。如果在 app-image 阶段就启动失败请直接看第 5 节的排查思路不要在安装包封装阶段浪费时间。4.2 Windows 平台把 app-image 包装成 MSI确认 app-image 跑通之后Windows 下生成 MSI 的命令和前面非常相似只需要把--type改成msi再加上一些 Windows 特有的参数。jpackage \ --type msi \ --name DataBoard \ --app-version 1.0.0 \ --vendor 某内部团队 \ --input target \ --main-jar demo-data-board.jar \ --main-class org.springframework.boot.loader.JarLauncher \ --win-shortcut \ --win-menu \ --win-menu-group DataBoard Tools \ --license-file license.txt \ --dest dist--win-shortcut会在桌面创建快捷方式--win-menu会把应用目录放到开始菜单里。如果你需要生成 EXE 安装程序把--type改成exe即可。但要注意Windows 下生成 MSI 或 EXE 时jpackage 还需要额外的安装器工具支持最常见的是 WiX Toolset。如果缺少这个工具终端会报类似“The jpackage tool requires certain tools and libraries to be present”的错误。解决办法是去官网下载合适的版本安装并把安装目录加入 PATH然后重新打开终端再跑 jpackage。我当初第一次打 MSI 就卡在这个环境依赖问题上因为完全没意识到 jpackage 不是全自包含的。后来把 WiX 装好之后MSI 一分钟左右就能出来。4.3 Linux 平台DEB 与 RPM 的差异化处理Linux 平台的打包思路是一样的但命令参数有一定差异。Debian 系要用 DEBRedHat 系要用 RPMjpackage 分别通过--type deb和--type rpm来指定。jpackage \ --type deb \ --name databoard \ --app-version 1.0.0 \ --vendor 某内部团队 \ --input target \ --main-jar demo-data-board.jar \ --main-class org.springframework.boot.loader.JarLauncher \ --linux-shortcut \ --linux-menu-group Development \ --dest distLinux 的打包机需要确保有对应的打包工具链生成 DEB 需要dpkg-deb生成 RPM 需要rpmbuild及相关开发包。不同发行版的工具名略有差异可以先在打包机上执行which dpkg-deb或which rpmbuild检查。还有一个 Linux 特有的坑如果你在服务器上安装这个 DEB 包jpackage 默认会把应用装到/opt/databoard这样的目录下快捷方式的.desktop文件里写的 Exec 路径会指向这个目录。如果你的应用有自定义配置放在/etc/下会更符合 Linux 用户习惯但 jpackage 默认不会帮你处理配置文件的迁移这部分需要应用自己在启动时去读取外部目录。4.4 macOS 平台DMG 与应用签名macOS 平台的流程同理但多了一个不能跳过的环节应用签名。如果你不打任何签名构建出来的 DMG 或 PKG 在分发到其他 Mac 上时大概率会被 Gatekeeper 拦截用户需要手动去系统设置里允许运行。jpackage \ --type dmg \ --name DataBoard \ --app-version 1.0.0 \ --vendor 某内部团队 \ --input target \ --main-jar demo-data-board.jar \ --main-class org.springframework.boot.loader.JarLauncher \ --mac-sign \ --mac-signing-key-user-name Developer ID Application: ... \ --dest dist如果只是在团队内部使用没有开发者证书也可以不签名但分发路径会麻烦一点用户首次打开时可能看到“验证开发者”的警告或者需要右键打开并确认。我的建议是内部工具至少做一个本地签名或者准备一份简单的使用说明告诉用户遇到拦截时怎么放行。5. 安装后最容易踩的坑现象、原因、排查思路5.1 启动器闪退闪退背后的日志去哪了app-image 阶段最常见的现象是双击.exe后窗口一闪而过什么都没留下。这时候最难的问题是看不到日志因为 Spring Boot 的日志默认输出到控制台启动器一闪就没你根本来不及看。解决办法有两个方向。一是先不用 jpackage 启动直接手动执行 app-image 里对应的命令行把启动器拆开来看它到底调用了什么。Windows 下最简单的方式是打开终端切到 app-image 目录执行启动器同路径下的runtime/bin/java.exe手动带上一串参数运行主类。二是给 jpackage 传--java-options比如jpackage \ --type app-image \ --name DataBoard \ --input target \ --main-jar demo-data-board.jar \ --main-class org.springframework.boot.loader.JarLauncher \ --java-options -Dlogging.level.rootINFO \ --java-options -Dconsole.logtrue \ --dest dist如果你用的是 Logback 或 Log4j2可以提前配置好文件输出让 Spring Boot 启动时同时写文件日志。这样就算启动器闪退你也能去安装目录的 logs 文件夹看到详细信息。闪退最常见的原因是主类指定错误。很多教程里都强调过要写org.springframework.boot.loader.JarLauncher但实际执行时很容易手滑写成自己的业务类。如果Main-Class指向业务类JVM 会直接告诉你找不到BOOT-INF/classes里的类一瞬间就退出。第二种常见原因是启动时某个外部依赖无法加载。比如数据库驱动、消息队列客户端、本地化依赖等在开发环境有但在打包机的~/.m2里没有完整拉取。这种情况在开发环境看不出来在干净的安装包环境才会暴露。5.2 工作目录不是你以为的那个目录这是一个非常隐蔽的问题。Spring Boot 应用如果用了相对路径去读取外部配置文件在 IDEA 里运行没问题但通过 jpackage 生成的快捷方式启动时当前工作目录很可能不是你预期的地方。Windows 双击桌面快捷方式时工作目录取决于快捷方式里设置的“起始位置”。如果你没有显式设置可能是启动器所在目录也可能是C:\Windows\System32。Linux 从桌面启动时.desktop 文件里的Path字段也有同样的问题。我在开发这个看板应用时配置文件就放在 Jar 同级的config/目录下开发环境用相对路径读得好好的打成安装包以后同一个写法直接拿不到文件。最后排查发现应用的工作目录根本不是安装目录相对路径自然失效。更稳妥的做法是在应用代码里不要依赖相对路径。外部配置文件统一放到用户目录或者系统级配置目录比如 Windows 用AppDataLinux 用$HOME/.configmacOS 用~/Library/Application Support。然后通过System.getProperty(user.home)去拼路径。如果你确实需要支持安装目录下的config/那就要在启动参数里显式指定-Dspring.config.location并且把路径写死或者通过启动脚本把工作目录切到安装目录。jpackage 没有“安装目录”这种运行时变量给你直接用所以这并不是一个特别顺滑的方案。5.3 外置配置文件和升级时的残留问题安装包方案天然会让用户觉得“所有东西都在安装目录里”但对 Windows 来说安装目录通常在Program Files下普通用户没有写权限。如果你把application.yml放在安装目录下用户想修改数据库地址都无从下手。比较好的做法是把配置分成两层Jar 内置一份默认配置再通过外部配置目录读取可覆盖项。外部配置目录放在上面说的用户配置目录里。这样做既保证了应用开箱能跑又允许高级用户按需修改。升级时也有一个容易踩的坑如果旧版本正在运行新版本的安装包尝试覆盖安装目录里的 Jar 文件Windows 会因为文件被占用而失败或者要求用户先关闭应用。你需要在安装包层面处理这种情况比如在打包脚本里加入检测进程的逻辑或者在部署文档里明确告诉用户“升级前先退出旧版本”。这种问题在 CI 自动升级场景里尤其麻烦值得提前规划。6. 体积优化用 jlink 裁剪运行时到底值不值6.1 不裁剪时安装包体积有多大很多人第一次打完安装包会被体积吓一跳。一个只有几十 MB 的 Spring Boot Jar打出来的安装包很可能超过 150 MB。多出来的部分主要是捆绑的 JDK 运行时jpackage 默认会把当前 JDK 的大多数模块复制进去。这不是 bug而是 jpackage 的设计思路目标是让目标机器不依赖任何额外运行时那自然要带一个完整的 JRE。但你说一个只有基础 Web 功能的数据看板真的需要java.desktop、jdk.javadoc、java.compiler这些模块吗其实不需要。裁剪运行时的思路是用 jlink 生成一个精简的运行时镜像再把这个镜像交给 jpackage 使用。裁剪得好安装包体积能从 150 MB 降到 80 MB 甚至更低。但代价是你要对应用的依赖模块有清晰的认知否则容易出现启动时发现缺模块的尴尬。6.2 裁剪运行时的保守操作流程第一步用jdeps分析 Jar 的模块依赖。jdeps 是 JDK 自带工具可以扫描类文件里引用的包并映射到 Java 模块。jdeps --ignore-missing-deps --print-module-deps --recursive demo-data-board.jar把输出结果拿来作为 jlink 的--add-modules参数。但这里有个大前提jdeps 只能分析类之间的直接字节码引用如果应用用了反射、动态代理、SPI 机制jdeps 会漏报。Spring Boot 的自动配置大量使用META-INF/spring.factories和反射所以 jdeps 的结果只能作为参考不能直接当真。我建议采用一个保守的模块列表先保证核心运行再逐步裁剪。比如jlink \ --add-modules java.base,java.logging,java.naming,java.management,java.security.jgss,java.sql,jdk.crypto.ec,jdk.unsupported \ --strip-debug \ --no-header-files \ --no-man-pages \ --compresszip-6 \ --output runtime然后用 jpackage 的--runtime-image runtime参数让 jpackage 使用这个裁剪后的运行时而不是当前 JDK 的完整运行时。jpackage \ --type app-image \ --name DataBoard \ --input target \ --main-jar demo-data-board.jar \ --main-class org.springframework.boot.loader.JarLauncher \ --runtime-image runtime \ --dest dist这样做出来的 app-imageruntime目录会小很多。启动后如果能正常运行再打安装包。6.3 裁剪后哪些功能会悄悄失效我的经验是下面几类功能最容易在裁剪后出问题。使用反射加载的类。比如 Jackson、MyBatis、Hibernate 这些库很多地方通过反射获取构造函数或字段当对应模块被剪掉后运行时就会抛ClassNotFoundException或NoClassDefFoundError。解决方法通常是往--add-modules里补模块。Java 加密扩展。如果应用涉及 HTTPS、JWT、加密算法jdk.crypto.ec是必须的但java.base并不包含它。默认模块列表里如果漏了这个你会遇到各种“算法不可用”的异常。JNDI 和命名服务。如果应用用了数据源、JNDI 连接池java.naming必须保留。我有一次裁掉它之后Spring Boot 的数据源初始化阶段直接报错排查了很久才定位到。JDBC 和 SQL 模块。java.sql必须保留但如果你用的是某些数据库驱动它们内部还会依赖java.xml或者java.management这时候仅仅有java.sql还不够。所以我的建议是裁剪过程中频繁做冒烟测试。每次改一行--add-modules就重新启动一次 app-image把登录、查询、文件上传这些核心路径都过一遍。别一次性追求极致的体积先把模块列表调到稳定再考虑进一步瘦身。7. 让安装包方案进一步适配真实分发场景7.1 升级策略安装包也可以做增量更新安装包方案不等于“每次升级都要重新下载整个安装包”。如果你只是改了一个 Spring Boot 的业务 Jar完全可以考虑把业务 Jar 和安装包启动器分离安装包只负责放一个最小启动器真正的业务 Jar 放在外部目录升级时只替换这个 Jar 文件。Spring Boot 的PropertiesLauncher可以提供这种灵活性但实际操作起来要额外配置loader.path和MAIN环境变量复杂度会上去一点。另一个更简单的思路是继续用可执行 Jar但把它拷贝到用户可写的目录启动器只负责调用系统 Java 或随包自带的java.exe。这个方案本质上回到了绿色版但至少解决了运行环境问题。我的建议是如果应用还处于快速迭代期不要急着把所有东西都固化进安装包。把配置、业务 Jar、日志目录逻辑拆分好以后升级的压力会小很多。7.2 把打包过程固化到 CI手动执行 jpackage 一次两次没问题时间一长肯定出错。我之前把打包流程写进了一个脚本但每次换电脑、换 JDK 版本又出状况。后来改成 CI 流水线提交代码打好标签后流水线自动跑测试、打包、上传产物。如果你也用 CI记住一点jpackage 不能跨平台打包那就在流水线里同时配置三个平台的执行器各自跑各自的打包命令最终把产物归档。Linux 的打包步骤如果依赖rpmbuild还需要在 CI 机器上安装对应的工具包这些都应该提前固化成流水线环境。否则哪怕你本地跑通CI 上一样会报缺工具的错误。7.3 几个值得长期记住的经验最后分享几个我这次踩完坑以后总结出来的经验。执行顺序上先 app-image再安装包。任何启动问题都应该在 app-image 阶段解决因为这时候最容易定位也最容易重新跑测试。不要跳过这一步直接冲 MSI。命名和版本号尽量规范化。安装包的应用名称、版本、供应商这些信息在 Windows 卸载列表里会一直显示以后做升级、做问题反馈时都需要靠它定位。如果版本号一直写 1.0.0别人反馈问题时你根本不知道对方装的是哪一版。日志路径永远不要依赖安装目录。安装包场景下安装目录很可能没有写权限或者目录在工作组策略中受保护。把日志默认输出到用户目录或系统日志目录是一个非常值得提前做的事。否则应用一上线第一个被用户提到的就是“为什么找不到日志”。经历过这一轮之后我给内部工具定下了一个通用打包模板Windows 出 MSILinux 出 DEBmacOS 出 DMG每个平台都先跑通 app-image再封装安装包。jpackage 对我这种平时主要在服务端写代码的开发者来说已经足够友好只要你把它当成“一个生成启动脚本和各平台安装壳的工具”而不是一个魔法黑盒用起来就会顺畅很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑