暂时想了一个解决 npm 包引入问题的临时方案

阅读须知

  1. 本方案不可能包治百病,也没有经过全面的验证,只是把我工作中遇到的一些问题的解决办法分享出来;
  2. 对于一些对 NodeJS 有依赖的包,本方法依旧不适用,而且也没有办法适用,这些包往往会使用到诸如 fs、http 等非浏览器环境的工具;
  3. 请先尝试参考如 示例:通过 npm 使用 protobuf.js · Cocos CreatorCan I use … npm module… (shrinktofit.github.io) 等文档解决问题;
  4. 官方在我所提到的另一个相关帖子中 (想请教一下 Creator 3 对npm包的引入原理) 已将相关问题排期,未来可以被解决。
  5. 我不仅仅需要引入成功,还需要零成本地保留原始的类型推断,因此直接导入 js 这种方案无效,也不应该有额外的declare modeule 'xxx'
  6. 我在七楼又跟了一些可能对你有帮助的内容,走过路过不要错过!

大家可以相互分享一下 npm 包引入的问题,我们一起看看有没有解决方案。


相关帖子:想请教一下 Creator 3 对npm包的引入原理

场景 1:一些包导入后变成 undefined

import { Client } from 'colyseus.js'; // Client is undefined

可以尝试:

import Colyseus from 'colyseus.js';
const { Client } = Colyseus;

但是这种情况在包有默认导出export default时也会失效,因此最稳妥的方式如下:

import * as _XXX from 'xxx';

const ensureImport = <T>(raw: T): T =>
  typeof (raw as any).default === 'object' ? (raw as any).default : raw;

// 引入默认导出
const XXX = ensureImport(_XXX).default;
// 引入其中的某些内容
const { a, b, c } = ensureImport(_XXX);

以上方式不会丢失原本的类型推断,修改成本较低,核心有两点:

  1. 使用 * as _XXX;
  2. 自定义了一个能够保留类型推断的ensureImport函数;

场景 2:不得不使用 js 目标文件

常见于以下情况:

  1. Cocos 无法正常识别包的环境,一般这种包会针对不同的环境有不同的代码和依赖,而 Cocos 错误的将 node 环境对应的代码(而非浏览器环境对应的代码)引入进来,却找不到 node 环境需要的包导致报错,例如 axios、sha256 等,如果感兴趣,我可以跟帖讲一讲:
// sha256
[Programming] 无法加载模块 node:crypto :Error: 拒绝对模块 node:crypto 的访问。
  1. Cocos 很迷惑地对 esm 格式的目标代码使用 cjs 模式进行引入,例如 uuid:
[Programming] 无法加载模块 file:///xxx/node_modules/uuid/dist/esm-browser/index.js :Error: Unexpected export statement in CJS module.
  1. Cocos 找不到一个包依赖的其他包(明明是按照正规的npm intsall流程安装的),例如protobuf-js(注:以下情况可能是由于我使用了 pnpm 导致,但这并不应当归咎于我自己的问题,毕竟 pnpm 也是下一代主流包管理方案,总是要支持一下的):
[Scene] Error: Error: 以 file:///xxx/node_modules/protobufjs/src/util.js 为起点找不到模块 "@protobufjs/codegen"

这里比较搞笑的是我遇到的一种情况,原本想要把我自己写的一些 Cocos 的轮子打包分享到 npm 上造福大家,谁知道包是发出去了,但是在引入到 Cocos 中遇到了以下报错:

[Scene] Error: Error: 以 file:///xxx/node_modules/cocos-helper/src/index.ts 为起点找不到模块 "cc"

找不见 CocosCreator 自己可就蚌埠住了 :sob:

吐槽结束,诸如上述这些情况,我们只能使用替代方案:要么将对应的包中的 src 文件直接扔到 assets 中来,要么就选择使用诸如 xxx.min.js 的前端最小目标文件,前一种比较简单就不多说了,我们来聊聊后一种。对于后一种,其实论坛上方案已经很多了,例如: Can I use … npm module… (shrinktofit.github.io),但可惜该文指出的方案已经失效了,因为在新的 axios 包描述中禁用掉了对该路径的直接引用:

[Scene] Error: Error: 子路径 "./dist/axios.min.js" 未在 file:///xxx/node_modules/axios/package.json 中导出

没办法,只能曲线救国:

import axios from "../../node_modules/axios/dist/esm/axios.min.js";

但是用 deconstruct 方案引入的依旧会变成和场景 1 一样的 undefined:

import { get } from "../../node_modules/axios/dist/esm/axios.min.js";

对于以上两种情况,究极方案是什么呢?在你的 assets 下,或者 assets/scripts 下建立一个专门用于处理第三方包的文件(例如 packages.ts ):

// assets/packages.ts
import * as _axios from "../../node_modules/axios/dist/esm/axios.min.js";
import { AxiosStatic } from 'axios';

const ensureImport = <T>(raw: T): T =>
  typeof (raw as any).default === 'object' ? (raw as any).default : raw;

// 将其解包、并添加相关的类型推断
export const axios = ensureImport(_axios).default as AxiosStatic;

而你的其他脚本,都可以经过这个文件对第三方库进行引用,尽可能还原 npm 生态:

import { axios } from 'db://assets/packages';

我看论坛上,这几年来有过 npm 零零星星的讨论,大家往往是为了做一些轻量级的小游戏,因此添加的都是一些比较简单的 npm 包,往往也可以通过使用 min.js 的方式来解决。

但是正如 Cocos 官方所构想的那样, Cocos 不能只讲自己限制在页游,还需要向元宇宙、XR 进军,Cocos 也肯定期望看到开发者使用自己做出更多更大型的游戏,而这一定少不了对 npm 包,以及 npm 工程化的支持。但是现在这方面做得并不好,以我自己的开发体验来看,10 个 npm 包有 9 个用不了,也没有什么替代方案,已经想要骂人了。就像我之前在另一个帖子中讲的那样:

我们现在只能解决“你 引用了 包A”的情况,那如果“包A”用同样不被兼容的方式引入了“包B”,是不是就不行了。这种情况还挺常见的。

就比如,你能通过之前的方式引入 typedjson ,但是有一天你必须要用的另一个包也引入了 typedjson 并且报了同样的错,该怎么办?进一步的,如果是包引用包引用包这样的递归情况中,任一环节出了问题,是不是这个包就没法用了。这是一个复合概率,会随着包的复杂程度而增加,最后变成 100%。

所以正确的方式是,让 cocos 正确支持各种 import 的方式,保证不出错,而不是去动包。要解决问题不能去解决出问题的人啊

我实际上最关心的问题

其实以上问题并不是我最关注的,我最关心的是想问一下,Cocos是否会有一天,能完整兼容 npm 生态?能否有一天不再需要任何 from 'xxx/dist/xxx'

感觉当时 Cocos 选择 TS,一个是看重了 TS 简单易学,另一个就是希望接入 JS 的生态,而 npm 则是生态的主要部分。但是现在使用 npm 生态还存在一些问题,例如:

  1. axios 作为一个比较常用的包,会经常被大家用到,如果每次都使用 axios/dist/xxx 的方式引入目标文件,会带来一些不便(且 TS 类型推断丢了,还要自己补);
  2. 如果说第一点是痒点,第二点就是痛点:对于比较大的项目来讲,会引入各种包,而这些包又会引入其他包,假如项目使用的某个包递归引入了像 axios、web3 这样的包,同样会引发报错,这种情况总不能让开发者手动去 node_modules里面把每个包的源码都改掉吧;

因为以上两点问题,目前也只敢拿 Cocos 开发一些比较小的项目,因为 npm 生态接不进来(也不是全接不进来,而是要看命),axios 这样的基础库可能还能靠直接使用 min.js 的方法对付一下,多级依赖的包是真的不敢拿来用了。这样一来,npm 生态的兼容度就变得很低,很多 npm 包都没法拿来用,开发成本就上来了。

上述问题会影响到 Cocos 的开发生态和竞争力,已经不仅是开发者感到不便的问题了。我们团队使用 Creator 这一年来,是非常看好 Cocos 潜力的,所以希望看到 Cocos 能够越来越好!

21赞

我也是装5个包4个用不了:joy:
开发组说优先级低,我的终极解决方案是换成2.4.10了

2赞

搞不懂他们的优先级标准…我认为这个比新增视觉效果要重要。视觉效果不行可以先凑活着发一版难看的,等有了再更新游戏版本,但是 npm 包加不进来,连游戏都没法开发了。

3赞

感谢分享,很不错的方案

多谢你的方案,你比官方更加 “官方”

1赞

谢谢,很高的评价!

发现帖子第一次被置顶了,开心hhh

那就顺便把最近其他的一些经验也写在这里吧

引入入口的优先级

首先是对于某个包,假如有多个引入入口(main, module, exports 等)如何看 Cocos 引入了其哪个入口文件:

首先 cocos 会去寻找类似这样的东西(某个包的package.json中):

{
  "exports": {
    "browser": {
      "import": "xxx",
      "require": "xxx",
      "default": "xxx"
    }
  }
}

如果你是 import 引入的,那么对应入口就是 exports.browser.import,如果是 require 引入的(实际上不支持这种引入方式,不要使用),对应入口就是 exports.browser.require ,如果没有就会使用 exports.browser.default。

如果 exports.browser 是一个字符串,相等于是 exports.browser.default。

如果 exports.browser 也不存在,就会寻找 exports.default,如果 exports 是字符串也就相当于exports.default。exports 还支持不同路径不同入口等嵌套定义,但大同小异,具体的看文档:

Package exports | webpack

如果没有 exports ,则 cocos 会去找 “main”。

CJS 包的引入,即 npm 大多数包的引入方式

不完全严谨的理解什么是 cjs 格式的包:用require引入、用 exports.xxx = … 导出的就是 cjs

对应的 esm 格式的包:用 import 引入、用 export const xxx = … 导出

那么打开大多数的包看一看入口文件,基本都是 cjs 的。而 cocos 对 cjs 的包引入存在缺陷。例如,我们假设有某个包 abc,其 package.json 包含如下内容:

"main": "./dist/1.js"

而 1.js 中:

console.log('Import from main');
console.log({ exports });

若干 Object.defineProperty(exports, ...);
若干 exports.xxx = ...;

console.log({ exports });

尝试在某个 Cocos 脚本中引入:

import { a, default as default_ } from 'abc';
console.log({ a });
console.log({ default: default_ });

则会在控制台打出:

Import from main
{ exports: {} }
{ exports: { 若干导出对象 } }
{ a: undefined }
{ default: { 若干导出对象 } }

继续之前帖子(想请教一下 Creator 3 对npm包的引入原理)的内容,可知在引入 node.js 这个文件时 exports 是正常的,但是真正拿出来之后,这个 exports 中的内容到了 exports.default 中,导致没办法正确导入所有的 CJS 格式的 npm 包。

所以我猜测引擎在进行 systemjs 打包的时候可能做了 exports = { default: exports } 这样的事情;只要把这个多套一层 default 的动作拿掉就可以了。不是很理解当时为什么要这样做。

另外,一般 webpack 等打包工具会将 export default xxx 打包成 exports.default,即 exports.default 是为默认导出预留的。但这并不代表 import abc from 'abc';abc.a 就完全没问题了,实现这一点需要 export default { a: ... },和 export const a = ... 是不同的。二者可以指向不同的东西。

综上,我于是采用了一楼提出的方案。

Cocos 目前(3.6.2) 仍不能和 pnpm 合用

另外还发现一个问题,cocos 无法和 pnpm 一起工作。我大概介绍一下 pnpm 的工作原理。其会在 node_modules 下创建一个 .pnpm 文件夹,里面大概是这样一些文件夹(实际上是硬链接,链接到 pnpm 的全局缓存):

registry.npmmirror.com+@rollup+plugin-json@4.1.0_rollup@2.79.1
registry.npmmirror.com+async@3.2.4
registry.npmmirror.com+axios@0.21.4
registry.npmmirror.com+axios@0.24.0

而在 node_modules 中的包,假如是 axios,就会软链接到 .pnpm/registry.npmmirror.com+axios@0.24.0

但是如果在 cocos 中使用 pnpm 做了包管理,就会出错。例如 axios 引入了 http-form 这个包,cocos 就没有办法在 node_modules 下找到这个包(报错无法在 xxxx.js 路径下找到模块'http-form'),因为现在没有显式安装 http-form ,http-form 只被 axios 依赖,所以并不在 node_modules 中,而是在 .pnpm 中。

我不太清楚其他打包工具是怎么解决这个问题的,Cocos 后面可以参考一下其他打包工具的寻包方式,来兼容 pnpm,而现在姑且推荐开发者使用 npm 进行包管理。

2赞

点个赞给你,真的很不错

赞赞楼主,希望coco提升易用性。不要总提升画面效果了。小游戏用不上,重度项目更看底层工具。

好文,这个方法也不行了,import { get } from “…/…/node_modules/axios/dist/esm/axios.min.js”;也变成未导出了

这些本来是应该引擎做的…

想想我以前的回复(帖子找不到了),说想要无缝、正常使用npm包,但不包含某些原生无效的包

引擎开发者就回复说不支持fs等特属于某些平台的包,

大家都知道不能用,我想要的只是nodejs能正常导入使用的包(不包含原生不支持代码),
在creator能用同样的代码导入使用就行,可是官方却来了一套自己的导入方法,最开始其他人导入失败还要一个一个问这个包怎么正常导入,实在是忍不住吐槽

甚至现在还把自己的导入方法放在文档里…而不是想着解决问题,就突出一个特立独行

在我看来就是自己抛弃npm优势,只剩下一个性能底下的js

1赞

同样被导包折磨过,这一点很难受,论坛里面也经常看到问这一类的问题。其中 can i use npm in cocos 里面只有寥寥几个包,整个npm生态那么大,像玩具一样?能不能让开发者知道一个包的正确导入方式,不能用的话告知原因。

比如搞个编辑器插件,可以自动扫项目目录下的npm包,告知这个信息。(如果包的导入规则,即使不一样,但就像cocos文档说的那样明确,就能静态分析出来)

1赞

Creator 就是用的 node ESM 的那一套,如果:

  • 你用的包不包含 node 内置模块;

  • 你能直接能用 node xxxx.[m]js 去执行。

但在 Creator 里面引用不了,那就是 Creator 的问题。

Creator 现在相比 Node 少了一个功能就是 Modules: ECMAScript modules | Node.js v20.2.0 Documentation (nodejs.org) 里面的:

For better compatibility with existing usage in the JS ecosystem…

这个导致有些包得这么用 import buffer from 'buffer'; new buffer.Buffer()

这个有计划做。


大家都知道不能用,我想要的只是nodejs能正常导入使用的包(不包含原生不支持代码),
在creator能用同样的代码导入使用就行

开发者在 Creator 里遇到的有些问题,在 Node 里面执行也会报错,比如:

这个帖子 里面反馈 helper/TimerHelper.ts 为起点找不到模块 “dayjs/plugin/utc”

那么也可以在 Node 里试一下:node js (forked) - CodeSandbox

Node 也有这个报错:

image

cocos用npm太折磨人

我觉得这引擎再这样搞下去,要完蛋的,一堆问题,还在埋头加新的乱七八糟的效果,功能.
基本的都没夯实.

直接用 protobufjs 实验,tsconfig 一致,package.json 的 module 都为空,但是 creator 导入失败

tsconfig 配置

{
  /* Add your custom configuration here. */
  "compilerOptions": {
		"target": "es6",
		"lib": ["es2015", "es2017", "ES2020", "ES2022", "dom"],
		"module": "commonjs",
		"sourceMap": true,
		"noEmit": true,
		"strict": true,
		"noImplicitAny": false,
		"downlevelIteration": true,
		"esModuleInterop": true,
		"experimentalDecorators": true,
		"allowSyntheticDefaultImports": true,
		"isolatedModules": true,
  }
}

nodejs

creator 3.3.2

creator 3.6.3

1赞

Cocos官方要不要搞一个自己的包管理器,能支持脚本组件、Prefab等资源,这生态岂不是起飞么

https://shrinktofit.github.io/can-i-use-npm-in-cocos-creator/

compressing包能否在cocoscreator里面使用呢???

扩展管理器之后就会是这样的