:::success

文章信息

🧢 作者:刘lyxAndy

🎒 写作日期:2025-01-18

🌎 联系方式:QQ 3449556207

:::

引言

CoCo编辑器给自定义控件提供了require函数,用于导入内置的模块。然而CoCo编辑器内置的模块数量极为有限,不能满足开发者的需求,就需要引入其他的npm包,本篇小技巧应运而生。

使用npm包有两种方法:

  1. CDN在线引入法
  2. 打包内置法

本篇小技巧会逐一进行讲解。

CDN在线引入法

一般来说,在需要的npm包体积较小的情况下,CDN在线引入法往往是最佳的选择。

针对IIFE格式的npm包

以下是一个简单的示例:

文件来源:IFTC

有部分修改

  1. var document = this.document;
  2. function loadScript (url) {
  3. var script = document.createElement("script");
  4. script.src = url;
  5. document.head.appendChild(script);
  6. }
  7. loadScript("https://cdn.jsdelivr.net/npm/eruda");
  8. const types = {
  9. isInvisibleWidget: true,
  10. type: "Eruda",
  11. icon: "https://cdn.cocotais.cn/project/waddle-2/logo/waddle2-logo.svg",
  12. title: "Eruda控制台",
  13. version: "1.0.0",
  14. isGlobalWidget: true,
  15. properties: [],
  16. methods: [],
  17. events: [],
  18. };
  19. class Widget extends InvisibleWidget {
  20. constructor(props) {
  21. super(props);
  22. }
  23. }
  24. types['methods'].push({
  25. key: 'init',
  26. label: '开启',
  27. params: [],
  28. })
  29. Widget.prototype.init = () => {
  30. this.window.eruda.init()
  31. }
  32. types['methods'].push({
  33. key: 'close',
  34. label: '关闭',
  35. params: [],
  36. })
  37. Widget.prototype.close = () => {
  38. this.window.eruda.close()
  39. }
  40. exports.types = types;
  41. exports.widget = Widget;

此控件提供了一个loadScript函数,接受一个字符串作为参数。当调用时将创建一个script元素并将src属性设置为提供的字符串,以达到加载任意JS文件的功能。

此文件向loadScript函数提供的参数为https://cdn.jsdelivr.net/npm/erudajsDelivr 是一个免费的在线npm包CDN,可以通过以下语法加载任意一个发布在npm上的包:

  1. https://cdn.jsdelivr.net/npm/包名@版本名/文件

下面是一些示例:

  1. // 加载jQuery 3.6.4
  2. https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js
  3. // 加载jQuery 3.6.x
  4. https://cdn.jsdelivr.net/npm/jquery@3.6/dist/jquery.min.js
  5. // 加载jQuery 3.x
  6. https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js
  7. // 加载jQuery最新版本
  8. https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js
  9. // 加载代码最小化后的jQuery 3.6.4
  10. https://cdn.jsdelivr.net/npm/jquery@3.6.4/src/core.min.js
  11. // 加载jQuery 3.6.x提供的默认输出文件
  12. https://cdn.jsdelivr.net/npm/jquery@3.6

更多语法,参见https://www.jsdelivr.com/documentation

当然,除了jsDelivr,还有如cdnjsunpkg等CDN可以使用,此处不再赘述。

加载指定的npm包过后,一般会暴露一个全局对象(本例中为eruda)到window(或globalThis)上。为了使用,需要以this.windoẇ的方式访问。如本例中就使用了this.window.eruda来访问暴露的全局对象。

针对ESM格式的npm包

  1. var document = this.document;
  2. let eruda;
  3. import("https://esm.run/eruda")
  4. .then((module)=>{
  5. eruda = module.default
  6. });
  7. const types = {
  8. isInvisibleWidget: true,
  9. type: "Eruda",
  10. icon: "https://cdn.cocotais.cn/project/waddle-2/logo/waddle2-logo.svg",
  11. title: "Eruda控制台",
  12. version: "1.0.0",
  13. isGlobalWidget: true,
  14. properties: [],
  15. methods: [],
  16. events: [],
  17. };
  18. class Widget extends InvisibleWidget {
  19. constructor(props) {
  20. super(props);
  21. }
  22. }
  23. types['methods'].push({
  24. key: 'init',
  25. label: '开启',
  26. params: [],
  27. })
  28. Widget.prototype.init = () => {
  29. eruda && eruda.init()
  30. }
  31. types['methods'].push({
  32. key: 'close',
  33. label: '关闭',
  34. params: [],
  35. })
  36. Widget.prototype.close = () => {
  37. eruda && eruda.close()
  38. }
  39. exports.types = types;
  40. exports.widget = Widget;

此控件使用了import方法,接受一个字符串作为参数。当调用时将加载字符串中指定的ESM格式的文件。

此文件向loadScript函数提供的参数为https://esm.run/erudaesm.run 是一个免费的在线npm包CDN,可以通过以下语法加载任意一个发布在npm上的包:

  1. https://esm.run/包名@版本名/文件

下面是一些示例:

  1. // 加载d3 v7.8.3
  2. https://esm.run/d3@7.8.3
  3. // 加载d3 v7.8.x
  4. https://esm.run/d3@7.8
  5. // 加载d3 v7.x
  6. https://esm.run/d3@7
  7. // 加载d3最新版本
  8. https://esm.run/d3

import语句会返回一个Promise,其结果为一个Module,其中包含模块导出的内容。要获取默认导出,则可以使用module.default。参见import() - JavaScript | MDN

打包内置法

倘若npm包体积较大,或是不想过分依赖于网络,则可以尝试打包内置法。

针对JavaScript文件的打包器多种多样,这里我们着重介绍esbuild的使用方法。

安装

要使用esbuild,请先在电脑上安装Node.js

安装后,请创建一个项目文件夹(以example为例),随后在此文件夹内运行以下命令:

  1. npm init -y # 你也可以选用自己的包管理器(pnpm, cnpm等)

此时,你的目录结构应为如下:

  1. example
  2. └──package.json

接下来,运行以下命令:

  1. npm i esbuild # 亦可以添加-D选项

此时,你的目录结构应为如下:

  1. example
  2. ├──node_modules
  3. └──...
  4. ├──package-lock.json
  5. └──package.json

运行以下命令:

  1. npx esbuild

你应可以看到如下的输出:

  1. Usage:
  2. esbuild [options] [entry points]
  3. Documentation:
  4. https://esbuild.github.io/
  5. Repository:
  6. https://github.com/evanw/esbuild
  7. ......

这代表你已经成功安装了esbuild。

打包

以Eruda控制台控件为例。先在项目目录中运行以下命令:

  1. npm install eruda --save

接着,创建index.js

  1. import eruda from "eruda";
  2. this.window.isErudaLoaded = true // 此行用于演示
  3. const types = {
  4. isInvisibleWidget: true,
  5. type: "Eruda",
  6. icon: "https://cdn.cocotais.cn/project/waddle-2/logo/waddle2-logo.svg",
  7. title: "Eruda控制台",
  8. version: "1.0.0",
  9. isGlobalWidget: true,
  10. properties: [],
  11. methods: [],
  12. events: [],
  13. };
  14. class Widget extends InvisibleWidget {
  15. constructor(props) {
  16. super(props);
  17. }
  18. }
  19. types['methods'].push({
  20. key: 'init',
  21. label: '开启',
  22. params: [],
  23. })
  24. Widget.prototype.init = () => {
  25. eruda.init()
  26. }
  27. types['methods'].push({
  28. key: 'close',
  29. label: '关闭',
  30. params: [],
  31. })
  32. Widget.prototype.close = () => {
  33. eruda.close()
  34. }
  35. exports.types = types;
  36. exports.widget = Widget;

要将其打包,可以使用以下命令:

  1. npx esbuild index.js --bundle --outfile=index.min.js --format=cjs --minify

--bundle指将npm包eruda打包内置进成品文件;--outfile指定了文件的输出为index.min.js--format指定了文件的格式为CommonJS,即保留exports对象,不将其转换为export关键字;--minify会对输出的文件进行最小化的代码压缩。

处理

打包完的文件并不能直接导入进CoCo编辑器,还需要进行一系列的处理工作。

第一,需要手动替换输出的文件里的一些非法关键字,如.fetchXMLHttpRequest等。示例:

  1. X=window.XMLHttpRequest.prototype
  2. X=window['XML'+'HttpRequest'].prototype
  3. window.fetch&&((z=(0,h.default)(window.fetch))
  4. window['fetch']&&((z=(0,h.default)(window['fetch']))

第二,需要手动在输出的文件最前端中添加获取权限的代码。示例:

  1. window=this.window,document=this.document,location=this.location;
  2. // 根据实际情况进行添加

第三,需要手动修复this的转换错误。示例:

  1. // 源文件:
  2. this.window.isErudaLoaded = true
  3. // 转换后:
  4. exports.window.isErudaLoaded = true
  5. // 替换后:
  6. this.window.isErudaLoaded = true

至此,生成的JavaScript文件就可以正常被作为自定义控件导入了。

Q&A

CDN在线引入法支持CJS模块吗?

不支持。浏览器环境没有提供对CJS模块的原生支持,需要转换为ESM格式的模块。

为什么使用esbuild?/为什么不使用Bun?

参见:https://bun.sh/docs/bundler/vs-esbuild

经过多此尝试,对Rollup、Bun、esbuild等多种打包器进行对比后,esbuild是我个人使用体验最好的一款打包器。本篇小技巧只是选择这一种打包器,并不代表其他打包器无法使用。它们的配置也是大同小异。

使用React遇到问题?

React不支持CDN在线引入法。在线引入会导致出现invalid hook call的错误,无法修复。

React支持打包内置法,但较为麻烦。

如果你使用esbuild,那么产生的错误一般会是invalid hook call。此时,在打包时请不要使用--minify选项,然后对产物进行以下修改:

  1. // React 路径部分省略
  2. // ../node_modules/.pnpm/*react@...*/node_modules/react/cjs/react.development.js
  3. var require_react_development = __commonJS({
  4. "../node_modules/.pnpm/*react@...*/node_modules/react/cjs/react.development.js"(exports2, module2) {
  5. // ...
  6. }
  7. });
  8. // ../node_modules/.pnpm/*react@...*/node_modules/react/index.js
  9. var require_react = __commonJS({
  10. "../node_modules/.pnpm/*react@...*/node_modules/react/index.js"(exports2, module2) {
  11. "use strict";
  12. if (false) {
  13. module2.exports = null;
  14. } else {
  15. module2.exports = require_react_development();
  16. }
  17. }
  18. });
  19. // ../node_modules/.pnpm/*react@...*/node_modules/react/cjs/react.development.js
  20. // ../node_modules/.pnpm/*react@...*/node_modules/react/index.js
  21. var require_react = __commonJS({
  22. "../node_modules/.pnpm/*react@...*/node_modules/react/index.js"(exports2, module2) {
  23. "use strict";
  24. if (false) {
  25. module2.exports = null;
  26. } else {
  27. module2.exports = React;
  28. }
  29. }
  30. });

修改之后,不使用--bundle选项,添加--minify选项,对修改后的文件进行二次打包即可。

此处附一个适用于React+esbuild的打包脚本:

  1. import esbuild from 'esbuild';
  2. import fs from 'fs';
  3. let code;
  4. await esbuild.build({
  5. entryPoints: [process.argv[2]],
  6. bundle: true,
  7. outfile: '_temp_' + process.argv[3],
  8. format: 'cjs'
  9. })
  10. code = fs.readFileSync('_temp_' + process.argv[3], 'utf8')
  11. code = code.replaceAll("require_react_development()", "React")
  12. fs.writeFileSync('_temp_' + process.argv[3], code)
  13. await esbuild.build({
  14. entryPoints: ['_temp_' + process.argv[3]],
  15. bundle: true,
  16. minify: true,
  17. outfile: process.argv[3],
  18. format: 'cjs'
  19. })
  20. fs.unlinkSync('_temp_' + process.argv[3])
  21. code = fs.readFileSync(process.argv[3], 'utf8')
  22. code = code.replaceAll(".fetchPriority", "['fetchPriority']")
  23. code = code.replaceAll("XMLHttpRequest", "window['XML'+'HttpRequest']")
  24. code = 'window=this.window;document=this.document;location=this.location;navigator=this.navigator;' + code
  25. fs.writeFileSync(process.argv[3], code)

如果你使用bun等将compilerOptions.jsx视为react-jsxreact-jsxdev的打包器,则除了上述对多个React副本的处理,还需要对转换的render函数进行处理。

  1. render() {
  2. return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Arco.Button, {
  3. type: this.btntype,
  4. shape: this.shape,
  5. status: this.status,
  6. disabled: this.disabled,
  7. loading: this.loading,
  8. style: {
  9. width: this.width + "px",
  10. height: this.height + "px"
  11. },
  12. onClick: () => this.emit("onClick"),
  13. children: this.content
  14. }, undefined, false, undefined, this);
  15. }
  16. render() {
  17. return <Arco.Button type={this.btntype} shape={this.shape}
  18. status={this.status} disabled={this.disabled} loading={this.loading}
  19. style={{
  20. width: this.width + 'px',
  21. height: this.height + 'px'
  22. }} onClick={() => this.emit('onClick')}>{this.content}</Arco.Button>;
  23. }