:::success
文章信息
🧢 作者:刘lyxAndy
🎒 写作日期:2025-01-18
🌎 联系方式:QQ 3449556207
:::
引言
CoCo编辑器给自定义控件提供了require
函数,用于导入内置的模块。然而CoCo编辑器内置的模块数量极为有限,不能满足开发者的需求,就需要引入其他的npm包,本篇小技巧应运而生。
使用npm包有两种方法:
- CDN在线引入法
- 打包内置法
本篇小技巧会逐一进行讲解。
CDN在线引入法
一般来说,在需要的npm包体积较小的情况下,CDN在线引入法往往是最佳的选择。
针对IIFE格式的npm包
以下是一个简单的示例:
文件来源:IFTC
有部分修改
var document = this.document;
function loadScript (url) {
var script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
}
loadScript("https://cdn.jsdelivr.net/npm/eruda");
const types = {
isInvisibleWidget: true,
type: "Eruda",
icon: "https://cdn.cocotais.cn/project/waddle-2/logo/waddle2-logo.svg",
title: "Eruda控制台",
version: "1.0.0",
isGlobalWidget: true,
properties: [],
methods: [],
events: [],
};
class Widget extends InvisibleWidget {
constructor(props) {
super(props);
}
}
types['methods'].push({
key: 'init',
label: '开启',
params: [],
})
Widget.prototype.init = () => {
this.window.eruda.init()
}
types['methods'].push({
key: 'close',
label: '关闭',
params: [],
})
Widget.prototype.close = () => {
this.window.eruda.close()
}
exports.types = types;
exports.widget = Widget;
此控件提供了一个loadScript
函数,接受一个字符串作为参数。当调用时将创建一个script
元素并将src
属性设置为提供的字符串,以达到加载任意JS文件的功能。
此文件向loadScript
函数提供的参数为https://cdn.jsdelivr.net/npm/eruda
。jsDelivr 是一个免费的在线npm包CDN,可以通过以下语法加载任意一个发布在npm上的包:
https://cdn.jsdelivr.net/npm/包名@版本名/文件
下面是一些示例:
// 加载jQuery 3.6.4
https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js
// 加载jQuery 3.6.x
https://cdn.jsdelivr.net/npm/jquery@3.6/dist/jquery.min.js
// 加载jQuery 3.x
https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js
// 加载jQuery最新版本
https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js
// 加载代码最小化后的jQuery 3.6.4
https://cdn.jsdelivr.net/npm/jquery@3.6.4/src/core.min.js
// 加载jQuery 3.6.x提供的默认输出文件
https://cdn.jsdelivr.net/npm/jquery@3.6
更多语法,参见https://www.jsdelivr.com/documentation。
当然,除了jsDelivr,还有如cdnjs、unpkg等CDN可以使用,此处不再赘述。
加载指定的npm包过后,一般会暴露一个全局对象(本例中为eruda
)到window
(或globalThis
)上。为了使用,需要以this.windoẇ
的方式访问。如本例中就使用了this.window.eruda
来访问暴露的全局对象。
针对ESM格式的npm包
var document = this.document;
let eruda;
import("https://esm.run/eruda")
.then((module)=>{
eruda = module.default
});
const types = {
isInvisibleWidget: true,
type: "Eruda",
icon: "https://cdn.cocotais.cn/project/waddle-2/logo/waddle2-logo.svg",
title: "Eruda控制台",
version: "1.0.0",
isGlobalWidget: true,
properties: [],
methods: [],
events: [],
};
class Widget extends InvisibleWidget {
constructor(props) {
super(props);
}
}
types['methods'].push({
key: 'init',
label: '开启',
params: [],
})
Widget.prototype.init = () => {
eruda && eruda.init()
}
types['methods'].push({
key: 'close',
label: '关闭',
params: [],
})
Widget.prototype.close = () => {
eruda && eruda.close()
}
exports.types = types;
exports.widget = Widget;
此控件使用了import
方法,接受一个字符串作为参数。当调用时将加载字符串中指定的ESM格式的文件。
此文件向loadScript
函数提供的参数为https://esm.run/eruda
。esm.run 是一个免费的在线npm包CDN,可以通过以下语法加载任意一个发布在npm上的包:
https://esm.run/包名@版本名/文件
下面是一些示例:
// 加载d3 v7.8.3
https://esm.run/d3@7.8.3
// 加载d3 v7.8.x
https://esm.run/d3@7.8
// 加载d3 v7.x
https://esm.run/d3@7
// 加载d3最新版本
https://esm.run/d3
import
语句会返回一个Promise
,其结果为一个Module
,其中包含模块导出的内容。要获取默认导出,则可以使用module.default
。参见import() - JavaScript | MDN。
打包内置法
倘若npm包体积较大,或是不想过分依赖于网络,则可以尝试打包内置法。
针对JavaScript文件的打包器多种多样,这里我们着重介绍esbuild的使用方法。
安装
要使用esbuild,请先在电脑上安装Node.js。
安装后,请创建一个项目文件夹(以example
为例),随后在此文件夹内运行以下命令:
npm init -y # 你也可以选用自己的包管理器(pnpm, cnpm等)
此时,你的目录结构应为如下:
example
└──package.json
接下来,运行以下命令:
npm i esbuild # 亦可以添加-D选项
此时,你的目录结构应为如下:
example
├──node_modules
│ └──...
├──package-lock.json
└──package.json
运行以下命令:
npx esbuild
你应可以看到如下的输出:
Usage:
esbuild [options] [entry points]
Documentation:
https://esbuild.github.io/
Repository:
https://github.com/evanw/esbuild
......
这代表你已经成功安装了esbuild。
打包
以Eruda控制台控件为例。先在项目目录中运行以下命令:
npm install eruda --save
接着,创建index.js
:
import eruda from "eruda";
this.window.isErudaLoaded = true // 此行用于演示
const types = {
isInvisibleWidget: true,
type: "Eruda",
icon: "https://cdn.cocotais.cn/project/waddle-2/logo/waddle2-logo.svg",
title: "Eruda控制台",
version: "1.0.0",
isGlobalWidget: true,
properties: [],
methods: [],
events: [],
};
class Widget extends InvisibleWidget {
constructor(props) {
super(props);
}
}
types['methods'].push({
key: 'init',
label: '开启',
params: [],
})
Widget.prototype.init = () => {
eruda.init()
}
types['methods'].push({
key: 'close',
label: '关闭',
params: [],
})
Widget.prototype.close = () => {
eruda.close()
}
exports.types = types;
exports.widget = Widget;
要将其打包,可以使用以下命令:
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编辑器,还需要进行一系列的处理工作。
第一,需要手动替换输出的文件里的一些非法关键字,如.fetch
、XMLHttpRequest
等。示例:
X=window.XMLHttpRequest.prototype
X=window['XML'+'HttpRequest'].prototype
window.fetch&&((z=(0,h.default)(window.fetch))
window['fetch']&&((z=(0,h.default)(window['fetch']))
第二,需要手动在输出的文件最前端中添加获取权限的代码。示例:
window=this.window,document=this.document,location=this.location;
// 根据实际情况进行添加
第三,需要手动修复this
的转换错误。示例:
// 源文件:
this.window.isErudaLoaded = true
// 转换后:
exports.window.isErudaLoaded = true
// 替换后:
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
选项,然后对产物进行以下修改:
// React 路径部分省略
// ../node_modules/.pnpm/*react@...*/node_modules/react/cjs/react.development.js
var require_react_development = __commonJS({
"../node_modules/.pnpm/*react@...*/node_modules/react/cjs/react.development.js"(exports2, module2) {
// ...
}
});
// ../node_modules/.pnpm/*react@...*/node_modules/react/index.js
var require_react = __commonJS({
"../node_modules/.pnpm/*react@...*/node_modules/react/index.js"(exports2, module2) {
"use strict";
if (false) {
module2.exports = null;
} else {
module2.exports = require_react_development();
}
}
});
// ../node_modules/.pnpm/*react@...*/node_modules/react/cjs/react.development.js
// ../node_modules/.pnpm/*react@...*/node_modules/react/index.js
var require_react = __commonJS({
"../node_modules/.pnpm/*react@...*/node_modules/react/index.js"(exports2, module2) {
"use strict";
if (false) {
module2.exports = null;
} else {
module2.exports = React;
}
}
});
修改之后,不使用--bundle
选项,添加--minify
选项,对修改后的文件进行二次打包即可。
此处附一个适用于React+esbuild的打包脚本:
import esbuild from 'esbuild';
import fs from 'fs';
let code;
await esbuild.build({
entryPoints: [process.argv[2]],
bundle: true,
outfile: '_temp_' + process.argv[3],
format: 'cjs'
})
code = fs.readFileSync('_temp_' + process.argv[3], 'utf8')
code = code.replaceAll("require_react_development()", "React")
fs.writeFileSync('_temp_' + process.argv[3], code)
await esbuild.build({
entryPoints: ['_temp_' + process.argv[3]],
bundle: true,
minify: true,
outfile: process.argv[3],
format: 'cjs'
})
fs.unlinkSync('_temp_' + process.argv[3])
code = fs.readFileSync(process.argv[3], 'utf8')
code = code.replaceAll(".fetchPriority", "['fetchPriority']")
code = code.replaceAll("XMLHttpRequest", "window['XML'+'HttpRequest']")
code = 'window=this.window;document=this.document;location=this.location;navigator=this.navigator;' + code
fs.writeFileSync(process.argv[3], code)
如果你使用bun等将compilerOptions.jsx
视为react-jsx
或react-jsxdev
的打包器,则除了上述对多个React副本的处理,还需要对转换的render
函数进行处理。
render() {
return /* @__PURE__ */ jsx_dev_runtime.jsxDEV(Arco.Button, {
type: this.btntype,
shape: this.shape,
status: this.status,
disabled: this.disabled,
loading: this.loading,
style: {
width: this.width + "px",
height: this.height + "px"
},
onClick: () => this.emit("onClick"),
children: this.content
}, undefined, false, undefined, this);
}
render() {
return <Arco.Button type={this.btntype} shape={this.shape}
status={this.status} disabled={this.disabled} loading={this.loading}
style={{
width: this.width + 'px',
height: this.height + 'px'
}} onClick={() => this.emit('onClick')}>{this.content}</Arco.Button>;
}