​Cocos Creator 3.0 里如何玩转 npm 海量资源

COCOS

共 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 的模块系统。

模块导出

使用的关键字 exportsmodule.exports

// foo.js

// 单独导出
module.exports.a = 10;
module.exports.b = function(){};
exports.c = 20;

// 整体导出
module.exports = { a10b: funtion(){}, c20 };

// exports 与 module.exports都指向同一个地址,但是最终返回的是 module.exports
exports.a = 10;
module.exports = { bfunction(){} };

// exports 不能单独导出,否则会失去和 module.exports 的关联性
exports = { a10b: funtion(){}, c20 };

模块导入

使用的关键字 import

const foo = require('./foo.js')

接下来,了解一下模块导入(require)规则,假设文件目录为 src/project/index.js。

  1. 相对路径开头(假设此处要查找的模块是 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
  2. 绝对路径跟 1 同理

  3. react 没有路径开头

    没有路径开头则视为导入一个包。优先判断 moduleA 是否是一个核心模块,如 fspath。否则,会从当前文件的同级目录 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.exportsexport 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); // true
  • ESM 模块的 default 导出指向 CJS 模块的 exports

  • default 部分的导出,Node.js 通过静态分析将其作为独立的 ES 模块提供。

接下来看一个例子:

// foo.js
module.exports = {
    a1,
    b2,
}

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 运行后,控制台输出如下:

protobufjs-print-default

可能有部分同学,在上面这句书写的时候就遇到,import protobufjs 时就已经报红了,提示模块没有默认导出(has no default export),这是因为 CJS 没有 default 导出,而 ESMCJS 交互的时候是将 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 文件夹下为例讲解。

MGOBE

查看 js 文件,发现代码已经被压缩了,大致能看到 module.exports 和  exports 字样,并且没有 package.json 文件。因此,可以定义为是 CJS 模块,按 CJS 模块导入使用。

import MGOBE from '../lib/MGOBE_v1.3.6/MGOBE.js';
console.log(MGOBE);

写完代码后发现,报了如下错误:

MGOBE-no-module

认真阅读报错内容会发现是类型声明文件 MGOBE.d.ts 有问题,提示说它不是一个模块。查看声明文件内容会发现,有模块命名空间,但是没有模块导出,不能使用的罪魁祸首就在这里。此时,解决方案有如下两种:

  1. 既然没有导出,那么,可以在类型声明文件底部加上一句 export default MGOBE,将模块命名空间直接作为导出来使用。

  2. 直接导入 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



往期精彩


浏览 169
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报