:::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.4https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js// 加载jQuery 3.6.xhttps://cdn.jsdelivr.net/npm/jquery@3.6/dist/jquery.min.js// 加载jQuery 3.xhttps://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js// 加载jQuery最新版本https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js// 加载代码最小化后的jQuery 3.6.4https://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.3https://esm.run/d3@7.8.3// 加载d3 v7.8.xhttps://esm.run/d3@7.8// 加载d3 v7.xhttps://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.prototypeX=window['XML'+'HttpRequest'].prototypewindow.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.jsvar 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.jsvar 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.jsvar 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;' + codefs.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>;}
