想请教一下 Creator 3 对npm包的引入原理

注:

  • 我已阅读 通过 npm 使用 protobuf.js我能否在 Cocos Creator 中使用 npm 包…… 两篇文章,并正常引入了一些包;
  • 我并非在 Cocos 中遇到了无法使用 npm 包的问题,而是想进一步了解,为什么 Cocos 对于 npm 包的引入和常规的前端工程(例如 Webpack+React)有所不同。
  • 我的 CC 版本为 3.6.1 (当然我认为这并不重要)

想请教一下,为什么对于类似 axios 这样的包需要使用 import axios from 'axios/dist/axios.min.js' 而非 import axios from 'axios'

我知道后者会引发如下的报错:

  • 引入 axios 包会引入 axios/lib/axios.js 而非 axios/dist/axios.min.js ,前者会进一步引入一个 form-data 包,而这个包只会也只需要在 NodeJS 环境下需要,而 Cocos 在编译脚本时使用了 NodeJS 环境(这是我猜测的,理由是代码中使用require编译不会报错)而在加载编译目标文件(js文件)时因为找不到 form-data 包而报错:

但我觉得(只是我个人认为,可能不正确)常规的前端项目构建应当是相类似的构建方式(即使用 webpack 等工具对项目进行编译,生成的目标文件供前端使用),为什么没有出现像 Cocos 中这样无法直接引用包名的问题呢?

说得简单一点就是,为什么平时写前端可以直接 import 'axios',但是到了 Cocos 中就不可以?是故意这样设计,还是说这实际上是一个问题呢?

1赞

打开 axios/package.json at v1.x · axios/axios (github.com),可以看到 exports 字段是这样的:

"exports": {
    ".": {
      "types": {
        "require": "./index.d.cts",
        "default": "./index.d.ts"
      },
      "browser": {
        "require": "./dist/browser/axios.cjs",
        "default": "./index.js"
      },
      "default": {
        "require": "./dist/node/axios.cjs",
        "default": "./index.js"
      }
    },
    "./package.json": "./package.json"
  },

顶层有三个条件:typesbrowserdefault。因为 Creator 的默认导出条件里面有 browser,所以会选中 browser 条件。

接下来再看 browser 下面的子条件:

"browser": {
    "require": "./dist/browser/axios.cjs",
    "default": "./index.js"
},

require 是指如果 axis 这个包是被 require() 导入的,应该用什么。因为你是 import axios from 'axios';,所以只能 fallback 到 default

也就是说,当你 import axios from 'axios';axios 会被解析为 <node_modules>/axios/index.js


顺着 <node_modules>/axios/index.js,可以发现顺藤摸瓜到了 <node_modules>/mime-db/index.js,里面有一句 module.exports = require('./db.json')。这句导致了报错。

后续会修复 JSON 模块不能被 require 的问题。

1赞

非常感谢大佬的回复 :+1:!不过我仍有一些不解的地方:

  • 为什么db.json无法被引入会导致 axios 尝试引入一个前端不存在的包(form-data);
  • 为什么平时使用 webpack 等工具构建前端项目不会出现这样的问题;
  • 在 CCC 中可以正确执行脚本的 require 函数,是因为 CCC 是在 NodeJS 环境运行的吗;

我实际上最关心的问题

其实以上问题并不是我最关注的,我最关心的是想问一下,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 能够越来越好!

不会的,因为 Cocos 不包含 Node.js 环境,也无意这样做。为了彻底的跨平台和轻量,我们的运行环境在 Web 端是完全依赖 Web 环境,在原生端,只有一个 JS 运行时 v8,v8 提供的能力我们就支持,而 Node.js 则是完全不同的运行环境了,不在我们考虑范围内

我觉得我的问法不合理(但是好像没法编辑原文的样子),所以重新改一下问题:能否正常兼容那些通用的、和平台无关(或跨平台)的各种包,还拿 axios 举例,即不要出现需要改 import 路径这样的问题,而原生和Web的分发存在差异这件事情则是OK 的。

这个问题还是 @shrinktofit 来回答更专业一些

好的,谢谢您!

@shrinktofit 您好,想了解一下在解决了您说的json引入问题之后,axios是不是就可以正常引入了呀,以及进一步的,其他包是不是也可以正常引入了呀?

如我所说,这是一个会影响开发生态的问题,而且看到论坛里经常有人在问类似重复的问题。谢谢!

(现在cocos是用什么做ts编译的呀?webpack、rollup、esbuild或者是团队自己拿tsc手撸?)我也很愿意帮你们团队以及大家多做一些相关的改进尝试,一起来解决这个问题,毕竟这个对我来说是刚需。

您好,想了解一下在解决了您说的json引入问题之后,axios是不是就可以正常引入了呀

我们会再确认一下。

以及进一步的,其他包是不是也可以正常引入了呀

不是,其它有些包就是没法用。它们依赖了 node 内置模块比如 fs、http、os 等,Creator 是没有的。比如,你设想一下,你可以构建到微信、web、支付宝、快游戏,那么这几个平台哪里有 fs、http 这种模块呢?

现在cocos是用什么做ts编译的呀

在内部,Creator 使用 babel 进行编译。在非构建阶段,使用自研的工具进行“打包”。在构建阶段,使用 rollup 进行打包、tree shaking。


我们后面会继续进行 npm 包使用的优化,尽量做到“常见打包工具打包之后能用在浏览器上的库都能用在 Creator 上”

1赞

好的,我再补充一下我的问题哈,用在node环境的那些,比如fs,path这些无法引入这一点我是理解的。我是全栈,两种环境都会接触,所以理解你所说的,我也不会用到这样的包,非常感谢你的解释!

只需要支持那些纯前端包/有前端版本的包就可以啦。axios之所以特殊是因为它同时可以用在前端和后端,用此它本身会尝试导入nodejs环境的包,就会导致报错。大概只需要让它知道自己是在浏览器环境而非node环境就可以了,大概?

对于最后大佬所说的,未来会继续对npm包引入这一点,允我表示对cocos团队的感谢,因为这一点很重要!

听说最近大佬最近在忙动画图,所以npm这里如果有什么需要测试或者讨论的,可以直接找我! :+1:

OK,欢迎继续反馈,我们正需要你这样的深度用户

1赞

好的呢,另外想一下现在负责npm这一块的同学是哪位呀?

老哥,可否帮个忙解决这个这npm的问题,我迫切需要这种json包 请问npm包该如何引入cocos中使用? - Creator 3.x - Cocos中文社区
有没有临时的解决方案提供一下?

npm 的问题反馈给我好了

1赞

@_PP 您好,正好有另一个npm相关的问题想问一下:

我有一个自己构建的 npm 包,因为是放在私有源的,就不提供复现代码了,我尽量描述一下情况:

假设有一个叫api包,其中是这样的:

// index.ts
export const apiBase = ...

我使用 npm 在cocos 项目中安装了这个包,并且将tsconfig.json中的allowSyntheticDefaultImports设为true。接下来做如下测试:

import api from "api";
console.log({ api });

控制台正常,接下来将代码换成:

import * as api from "api";
console.log({ api });

将发现 default 重新出现了。进一步的,如果使用 deconstruct 语法进行引入:

import { apiBase } from "api";
console.log({ apiBase });

会导致循环引用并无法获得导出的对象:

进一步发现,这样引入同样是引入到了 default 对象中,即:

import { default as _default } from "api";
console.log({ _default });

同样的问题出现在另一篇帖子里:
请问npm包该如何引入cocos中使用? - Creator 3.x - Cocos中文社区


我也碰到这个问题。

大家可以一起 push 官方 :smirk: 如果有类似的问题或者解决办法或者思路可以跟帖~

3.5.2也出现,非常感谢老哥的鼎力支持,希望官方看下我们俩的2个帖子,找到修复方式

我猜测你这里 npm 包源码是 esm 格式的 ts 文件,是否还会通过 tsc 编译成 CommonJS 的 js 文件呢?如果是这样的话,应该会编译成

// index.js
module.exports.apiBase = ...

而编辑器的 build tool 只会处理 npm 包实际导出的 js 文件,即 index.js
这个 index.js 会被编辑器的 build tool 做多一次包装,包装成类似这样的模块

export const __cjsMetaURL = 'pack:///chunks/...';
export const default = {
    apiBase: ...,
};  // 注意:这个 default 对象的所有属性是运行时动态赋值的,因为 CommonJS 并不能被很好地支持静态解析
// 所以目前也没有办法解析生成  `export const apiBase = ...` 这样的导出语句。

所以后面的一系列输出应该就可以理解了,

import api from "api";  // 其实就是 `import { default as api } from 'api'`, api 就是 default 对象

import * as api from "api";   // 输出就是 index.js 导出的所有对象,包括 __cjsMetaURL  和 default

import { apiBase } from "api"; // 就有问题了,因为 index.js 没有导出 apiBase 对象, 就报错了
// 这里可能报错信息有一些误导性,应该不是循环引用导致的,这个后续我们优化一下

import { default as _default } from "api";  // 同上 `import api from "api";`
1赞

所以可以被解决吗?因为npm上其他包也出现了类似的问题,我知道这样说有点委屈你们,但是无论原理上是什么样子,无法正常引入就可以算是cocos的bug :joy: 因为一个npm包没办法用正常的方式使用了。

我的包是用比较规范的方式构建的,cjs、esm格式都有。用的是字节的modernjs框架直接生成的包。如果有任何问题,需要我的都可以问我,我会尽量提供细节。

1赞