Cocos Creator 3.0 里如何玩转 npm 海量资源
共 11514字,需浏览 24分钟
·
2021-03-16 20:50
Cocos Creator 3.0 已全面支持 TypeScript 作为默认语言,不论是引擎提供的功能还是用户提供的脚本,所有代码都以模块的形式组织。
模块格式支持并推荐使用 ECMAScript(以下简称 ESM),也就是项目资源目录下以 .ts
作为后缀的文件。例如 assets/scripts/foo.ts。
但这不代表不支持 JavaScript 语言,毕竟 TypeScript 是 JavaScript 的超集并且 TypeScript 紧紧依赖 JavaScript。
因此,针对外部模块(例如:npm 模块)的使用,Cocos Creator 3.0 也在某种限度上支持了 CommonJS 模块格式(以下简称 CJS 模块格式)。
因此,清晰的了解 Cocos Creator 对模块格式的支持,可以更加方便地玩转 npm 里的海量资源。
01
模块
模块规范
目前主流的模块规范分别有:
UMD CommonJS ES6 module
在这里重点说一下与本文有关的 CommonJS 和 ES6 module。
CommonJS 模块规范
Node.js 环境所使用的模块系统就是基于 CommonJS 规范实现的,现在所说的 CommonJS 规范也大多是指 Node.js 的模块系统。
模块导出
使用的关键字 exports
和 module.exports
。
// foo.js
// 单独导出
module.exports.a = 10;
module.exports.b = function(){};
exports.c = 20;
// 整体导出
module.exports = { a: 10, b: funtion(){}, c: 20 };
// exports 与 module.exports都指向同一个地址,但是最终返回的是 module.exports
exports.a = 10;
module.exports = { b: function(){} };
// exports 不能单独导出,否则会失去和 module.exports 的关联性
exports = { a: 10, b: funtion(){}, c: 20 };
模块导入
使用的关键字 import
。
const foo = require('./foo.js')
接下来,了解一下模块导入(require)规则,假设文件目录为 src/project/index.js。
相对路径开头(假设此处要查找的模块是 moduleA)
在没有指定后缀名的情况下,先去寻找同级目录
src/project
是否有moduleA
文件。同级目录没有moduleA
文件,则会去找同级moduleA
目录src/project/moduleA
判断 src/project/moduleA
目录下是否有package.json
文件,如果有,返回main
字段定义的文件,如果没有main
字段,则尝试返回以下文件。src/project/moduleA/index.js
src/project/moduleA/index.json
src/project/moduleA/index.node
绝对路径跟 1 同理
react 没有路径开头
没有路径开头则视为导入一个包。优先判断
moduleA
是否是一个核心模块,如fs
、path
。否则,会从当前文件的同级目录node_modules
中寻找。寻找规则与 1 同理,src/project/node_modules
路径下以查找moduleA.js
为例,moduleA.js
->moduleA.json
->moduleA.node
->moduleA
目录 ->moduleA 下的 package.json main
->index.js
->...
。如果没找到,继续向父目录的node_modules
中找。
ES6 模块规范
模块导出
使用的关键字 export
。
foo.js
// 导出单个
export const a = 10;
export const b = function(){};
// 导出列表
export { a, b }
// 重命名导出
export { a as ma, b as mb, …, };
// 解构导出并重命名
export const { a, b: bar } = o;
// 导出模块合集
export * from 'export-name'; // 不能在当前模块(export-name)中使用。
export { a } from 'export-name' // 不能在当前模块(export-name)中使用。
// 默认导出
export default expression; // 一个模块只能有一个默认导出
模块导入
// 导入模块合集
import { a, b } from "module-name";
import * as moduleA from "module-name";
// 重命名导入
import { a as ma, b as mb } from "module-name";
// 默认导入
// foo.js
export const a = 1;
export const b = 2;
export default 10
// bar.js
import defaultExport from 'foo'; // defaultExport: 10
import defaultExport, { a, b as mb } from 'foo';
import defaultExport, * as foo from 'foo';
// 只运行模块
import 'module';
以上,module.exports
和 export default
是类似的,所以 ES6 module 可以很方便的兼容 CommonJS。接下来,开始了解一下 Cocos Creator 3.0 的模块格式。
Cocos Creator 3.0 模块格式
在 Cocos Creator 3.0 中,JavaScript 代码可能来源于:
项目中创建的代码; 引擎提供的功能; 非项目中创建也非引擎提供,但是被项目引用到的代码(npm 安装或者外部导入);
不同的来源可能导致 JavaScript 代码自身具有不同的模块格式,明确了解 Cocos Creator 3.0 的模块识别规则就能轻松解决大部分模块使用问题。
模块鉴别
Cocos Creator 选择与 Node.js 类似的规则来鉴别模块格式。整个模块识别的核心主要分为以下两部分:
ESM 模块格式鉴别标准:
以 .mjs
为后缀的文件;以 .js
为后缀的文件,并且与其最相近的package.json
文件中,顶级的 "type" 字段为 "module"。CJS 模块格式鉴别标准:
以 .cjs
为后缀的文件;以 .js
为后缀的文件,并且与其最相近的package.json
文件中,顶级的 "type" 字段为 "commonjs",或者无 "type" 字段。不在上述条件下的以 .js 为后缀的文件。
模块使用
在 ESM 模块中,通过标准的导入导出语句与目标模块进行交互,如上面提到的 ES6 模块规范。导入导出语句中关键字 from 后的字符串称为 模块说明符。模块说明符 也可作为参数出现在动态导入表达式 import() 中。
模块说明符 用于指定目标模块,方便通过该说明符正确解析出目标模块。
Cocos Creator 常见模块说明符:
相对说明符
相对说明符,如
'./foo'
、'../foo'
。它们指的是相对于导入文件位置的路径。不需要带后缀。裸说明符
裸说明符,可以用一个包名指代一个包的主入口点,或者是一个包中的特定功能模块,分别用包名作为前缀。例如:
Cocos Creator 采用 Node.js 模块解析算法。
foojs
解析为 npm 包的foojs
的入口模块;foojs/barjs
解析为 npm 包的foojs
中子路径./barjs
下的模块;
根据以上说明符规则,使用 import
关键字理解相对说明符:
如果目标文件后缀是
.mjs
或.js
时,模块说明符 必须指定 后缀。Node.js 式的目录导入是不支持的。import './foo.mjs'; // 正确
import './foo'; // 错误:无法找到指定模块
// Node.js 目录导入是不支持的
import './foo/index.mjs'; // 正确
import './foo'; // 错误:无法找到模块。如果目标文件后缀是
.ts
时,模块说明符 不允许指定 后缀。支持 Node.js 式的目录导入。import './foo'; // 正确:解析为同目录下的 `foo.ts` 模块
import './foo.ts'; // 错误:无法找到指定模块
// 支持 Node.js 目录导入
import './foo'; // 正确:解析为 `foo/index.ts` 模块
02
案例分析
1. ESM 与 CJS 交互
了解了模块内容后,现在的最大疑问点,就是如何应用到实际场景中,如何做到 ESM 和 CJS 的交互。这一点 Node.js 官方文档就有提到。在这里我简单的概括以下几点:
CommonJS 模块由一个
module.exports
对象组成,在导入 CommonJS 模块时,可以使用 ES 模块默认的导入方式或其对应的 sugar 语法进行可靠的导入。import { default as cjs } from 'cjs';
// 语法糖形式
import cjsSugar from 'cjs';
console.log(cjs); // <module.exports>
console.log(cjs === cjsSugar); // trueESM 模块的
default
导出指向 CJS 模块的exports
。非
default
部分的导出,Node.js 通过静态分析将其作为独立的 ES 模块提供。
接下来看一个例子:
// foo.js
module.exports = {
a: 1,
b: 2,
}
module.exports.c = 3;
// test.mjs
// default 指向 module.exports
import { default as foo } from './foo.js' 或 import foo from './foo.js'
console.log(JSON.stringify(foo)); // {"a":1,"b":2,"c":3}
// 导入 foo 模块的所有导出
import * as module_foo from './foo.js'
console.log(JSON.stringify(module_foo)); // {"c":3,"default":{"a":1,"b":2,"c":3}}
import { a } from './foo.js'
console.log(a); // Error: a is not defined
// 根据上方第三点,c 又有独立导出
import { c } from './foo.js'
console.log(c); // 3
2. 关于 protobufjs 包的使用
protobufjs 是一个 npm 包,因此需要通过 npm 进行安装。有关 npm 包下载问题,请参考使用 npm 镜像
首先,在项目目录下打开终端,执行 npm i protobufjs
,如果这个项目属于多人协作,甚至可以把 protobufjs
这个包作为依赖写入 package.json
,或者可以通过在上述命名行里加入 npm install --save protobufjs
即可利用命令行自动写入到 package.json
中。
执行完之后,就可以在项目录下的 node_module
文件夹里查找到 protobufjs
相关文件夹。(此处通过 @protobufjs 或 protobufjs 下的 package 判断出安装的包是 protobufjs,而 @protobufjs 是 protobufjs 包依赖相关)
有了 protobufjs
模块包之后。其次,判断模块格式。
查看 package.json
文件里的main
字段,判定入口文件index.js
;查看 package.json
文件里的type
字段,观察到没有type
字段;
根据之前模块鉴别里的内容,可以推断出,这是一个 CJS 模块。顺便一提在包里是能看到每一个 js 文件都对应一个 .d.ts 文件,说明 protobufjs
包里自带了 TypeScript
声明文件,方便导入 protobufjs
模块后可以通过代码提示获取内部方法。
接着,在 index.js
可以看到它导出写法。
"use strict";
module.exports = require("./src/index");
确定了模块格式和导出方式。接下来,就是脚本资源里如何使用 protobufjs
这个模块了。
首先,在 assets
下创建一个 test.ts
脚本。接着,在脚本的头部写入下列代码:
// 大部分 npm 模块都可以通过直接导入模块名的方式来使用。
import protobufjs from 'protobufjs';
console.log(protobufjs);
在 Chrome 运行后,控制台输出如下:
可能有部分同学,在上面这句书写的时候就遇到,import protobufjs
时就已经报红了,提示模块没有默认导出(has no default export
),这是因为 CJS 没有 default
导出,而 ESM 和 CJS 交互的时候是将 module.exports
视为 export default
。
因此,如果要保持原来的写法,可以在项目的 tsConfig.json
里加上下面这句即可:
// tsconfig.json
"compilerOptions": {
"allowSyntheticDefaultImports": true
}
接下来,就可以直接使用 protobufjs
提供的所有子模块了。当然,也可以通过直接导入所需的子模块来使用,子模块直接以模块名为路径,向下查找。
import minimal from 'protobufjs/minimal.js';
3. 将 proto 文件编译成 JavaScript 文件
本节主要讲述如何将 proto 编辑成 JavaScript 文件。其实在翻阅 protobufjs 文档的时候,可以发现它自身有提供命令行工具转换静态模块以及 ts 声明文件。本次讲解以新建一个 3.0 的空项目 example 为例。
首先,通过 npm 安装 protobufjs
并将它写入项目目录下的 package.json
的依赖项。
其次,在项目目录下新建 Proto 目录并定义几个 proto 文件。
// pkg1.proto
package pkg1;
syntax = "proto2";
message Bar {
required int32 bar = 1;
}
// pkg2.proto
package pkg2;
syntax = "proto2";
message Baz {
required int32 baz = 1;
}
// unpkg.proto 不属于任何的包
syntax = "proto2";
message Foo {
required int32 foo = 1;
}
接着,在 package.json 中定义。
"scripts": {
"build-proto:pbjs": "pbjs --dependency protobufjs/minimal.js --target static-module --wrap commonjs --out ./Proto.js/proto.js ./Proto/*.proto",
"build-proto:pbts": "pbts --main --out ./Proto.js/proto.d.ts ./Proto.js/*.js"
},
其中,第一段指令 build-proto:pbjs
的大致意思是将 proto 文件编译成 js。多加了 --dependency protobufjs/minimal.js
这一块其实是因为执行时 require
到了 protobufjs
,但是我们需要用的只是它的子模块 minimal.js
。
然后,将 js 生成到 Proto.js 文件夹中(注意:如果没有 Proto.js 文件夹,需手动创建)。第二段指令 build-proto:pbts
则是根据第一段的输出来生成类型声明文件。
根据以上步骤则成功完成了 proto 文件转换成 js 文件的过程。接着,可以在项目 assets
下脚本里引入 js 文件。
import proto from '../Proto.js/proto.js';
console.log(proto.pkg1.Bar);
此处还是要声明一下,如果有同学在导入的时候出现报红现象,提示 proto 没有默认导出,解决方案有两种。
通过向
tsconfig.json
增加允许对包含默认导出的模块使用默认导入字段来解决"compilerOptions": {
"allowSyntheticDefaultImports": true,
}增加默认导出
在项目目录下创建一个 Tools/wrap-pbts-result.js 文件,脚本代码如下:
const fs = require('fs');
const ps = require('path');
const file = ps.join(__dirname, '..', 'Proto.js', 'proto.d.ts');
const original = fs.readFileSync(file, { encoding: 'utf8' });
fs.writeFileSync(file, `
namespace proto {
${original}
}
export default proto;
`);将原来的
build-proto:pbts
命令改为:"build-proto:pbts": "pbts --main --out ./Proto.js/proto.d.ts ./Proto.js/*.js && node ./Tools/wrap-pbts-result.js"
最终,就可以直接运行了。完整项目内容请参考:npm-case
注意:打包出来的 js 文件即可以放在项目
assets
目录下,引入可以放在项目其它位置。assets
目录下的 js 文件不再需要勾选导入为插件,请各位悉知。
4. 关于 lodash-es 包的使用
同关于 protobufjs 包的使用方法类似,安装 lodash-es
包。得知入口文件是 lodash.js
,入口文件里也自动帮忙将其下所有子模块以 ESM 模块格式导出,再根据 type
也印证了当前是 ESM 模块。因此,可以直接导入任何模块。还是以 assets
下的 test.ts
脚本资源为例,引入 lodash
内的子模块。
import { array, add } from 'lodash-es';
此时,会发现代码层面会报错,但是实际却能够运行。这是因为,两者在语言类型上就有明显区分,JavaScript 是动态类型,TypeScript 是静态类型,因此,在使用 js 脚本的时候,是无法获知导出模块的具体类型的,此时最好的办法就是声明一份类型定义文件 .d.ts。
幸运的是,但我们将鼠标移到报错处的时候,有提示可以通过执行 npm i --save-dev @types/lodash-es
来安装 lodash
模块的类型声明文件。安装完之后,重启 VS Code 就会发现报错消失了,同时还有了代码提示。
5. MGOBE
这里以 MGOBE v1.3.6 为例。将下载后的 MGOBE 文件解压,获取到一份 js 文件以及它的类型声明文件 .d.ts。
可以将这两份文件放置到项目的任意位置,下面的内容会以将文件放置在 assets 同级目录新建的 lib 文件夹下为例讲解。
查看 js 文件,发现代码已经被压缩了,大致能看到 module.exports
和 exports
字样,并且没有 package.json
文件。因此,可以定义为是 CJS 模块,按 CJS 模块导入使用。
import MGOBE from '../lib/MGOBE_v1.3.6/MGOBE.js';
console.log(MGOBE);
写完代码后发现,报了如下错误:
认真阅读报错内容会发现是类型声明文件 MGOBE.d.ts
有问题,提示说它不是一个模块。查看声明文件内容会发现,有模块命名空间,但是没有模块导出,不能使用的罪魁祸首就在这里。此时,解决方案有如下两种:
既然没有导出,那么,可以在类型声明文件底部加上一句
export default MGOBE
,将模块命名空间直接作为导出来使用。直接导入 js 文件,此时的模块命名空间 MGOBE 就可以作为全局变量获取到
import '../lib/MGOBE_v1.3.6/MGOBE.js';
console.log(MGOBE);
更多案例
socket.io-client 使用案例:涉及引用 Node.js 内置模块而导致报错后的解决方案。
本专栏下期将为大家带来《Cocos Creator 3.0 的资源系统》by Santy Wang,敬请期待。
留言告诉我们其他你想看的内容,如果您计划使用 v3.0 立项开发原生重度游戏,欢迎向我们的产品负责人(jare@cocos.com)报名,您将有机会获得官方交流答疑、立项协助、引擎定制等服务喔,期待您的来信!
点击【阅读原文】了解 Node.js 详细信息。
参考链接
Cocos Creator 3.0 模块文档
https://docs.cocos.com/creator/3.0/manual/zh/scripting/modules/example-protobufjs.html#%E6%8B%93%E5%B1%95%EF%BC%9A%E4%BD%BF%E7%94%A8-npm-%E9%95%9C%E5%83%8F
socket.io-client 使用案例
https://discuss.cocos2d-x.org/t/v3-0-and-socket-io-client-npm-module/52910/4
往期精彩