Esbuild 配置文档
简而言之就是:esbuild是go语言写的,编译速度快,支持的环境多。究竟有多快:它的编译速度是普通编译插件的 100多倍它的API可以通过三种方式访问:命令行、JavaScript和Go,而且文档还是独一份基础命令version构建快速转化APITransform API转换API调用只对单个字符串进行操作,而不访问文件系统。这使得它非常适合在没有文件系统(如浏览器)的环境中使用,或者作为另一个
Esbuild
为什么选择esbuild?
简而言之就是:esbuild是go语言写的,编译速度快,支持的环境多。
究竟有多快:它的编译速度是普通编译插件的 100多倍
它的API可以通过三种方式访问:命令行、JavaScript和Go,而且文档还是独一份
安装
npm install esbuild
基础命令
version
esbuild --version
构建
esbuild app.jsx --bundle --outfile=out.js
快速转化
echo 'let x: number = 1' | npx esbuild --loader=ts
# let x = 1;
API
Transform API
转换API调用只对单个字符串进行操作,而不访问文件系统。这使得它非常适合在没有文件系统(如浏览器)的环境中使用,或者作为另一个工具链的一部分。下面是一个简单的变换:
echo 'let x: number = 1' | npx esbuild --loader=ts
# let x = 1;
基础选项:
高级选项:
- Banner
- Charset
- Color
- Drop
- Footer
- Global name
- Ignore annotations
- JSX
- JSX factory
- JSX fragment
- Keep names
- Legal comments
- Log level
- Log limit
- Mangle props
- Pure
- Source root
- Sourcefile
- Sources content
- Tree shaking
- Tsconfig raw
Build API
构建API调用对文件系统中的一个或多个文件进行操作。这允许文件相互引用并捆绑在一起。下面是一个简单的构建:
echo 'let x: number = 1' > in.ts
esbuild in.ts --outfile=out.js
cat out.js
# let x = 1;
基础选项:
- Bundle
- Define
- Entry points
- External
- Format
- Inject
- Loader
- Minify
- Outdir
- Outfile
- Platform
- Serve
- Sourcemap
- Splitting
- Target
- Watch
- Write
高级选项:
- Allow overwrite
- Analyze
- Asset names
- Banner
- Charset
- Chunk names
- Color
- Conditions
- Drop
- Entry names
- Footer
- Global name
- Ignore annotations
- Incremental
- JSX
- JSX factory
- JSX fragment
- Keep names
- Legal comments
- Log level
- Log limit
- Main fields
- Mangle props
- Metafile
- Node paths
- Out extension
- Outbase
- Preserve symlinks
- Public path
- Pure
- Resolve extensions
- Source root
- Sourcefile
- Sources content
- Stdin
- Tree shaking
- Tsconfig
- Working directory
基础选项
Bundle
启用后将你的代会打包成一个包(文件)
举个例子
你有以下文件
/src/index.js
import { doSomething } from './utils'
doSomething()
/src/utils.js
export function doSomething() {}
执行命令
npx esbuild ./src/index.js --bundle --outfile=bundle.js
打包后是一个bundle.js
function doSomething() {}
doSomething()
如果不启用 bundle 那么打包出来bundle.js
长这样
import { doSomething } from "./utils";
doSomething();
Define
定义一个全局可以访问的变量
举个例子
index.js
console.log(**DEFINE**)
执行命令
npx esbuild ./src/index.js --bundle --outfile=./bundle.js "--define:__DEFINE__=\"define\"
打包结果
bundle.js
console.log("define")
Entry points
就是入口文件列表,esbuild 后边跟的参数
举个例子
index.js
console.log('index')
utils.js
console.log('utils')
执行脚本
npx esbuild index.js utils.js --outdir=out
# 或者
npx esbuild ./** --outdir=out
# 或者
npx esbuild out1=index.js out2=utils.js --outdir=out
External
将文件或包标记为外部文件, 构建时会自动排除
举个例子
index.js
require("fsevents")
执行脚本
npx esbuild index.js --bundle --external:fsevents --platform=node --outfile=bundle.js
说明
这个api一般是给 commenJs 的包用的
比如打包的 bundle.js 是给 node环境的代码使用,那么我们打包出来的内容不应该把 node_modules 包的内容打包到 bundle.js 里,而是应该在使用 bundle.js,就应该在本地的 node_modules 里包含它的依赖
Format
决定生成的JavaScript文件的输出格式
目前支持三个参数
- iife
自调用函数
!(function() { // ...do something })();
- cjs
commenJs
const xx = require("xx")
- esm
esModule
import xx from "xx"
Inject
允许你用另一个文件的导入替换全局变量
process-shim.js
export let process = {
cwd: () => ''
}
entry.js
console.log(process.cwd())
执行命令
npx esbuild entry.js --bundle --inject:./process-shim.js --outfile=bundle.js
bundle.js
let process = {cwd: () => ""};
console.log(process.cwd());
说明
你可以通过该 api 注入 jsx 语法的解析函数
例如,您可以自动导入 react 提供React.createElement之类的函数
详情请看 JSX DOC
Loader
此选项更改给定输入文件的解释方式。例如,js加载器将文件解释为JavaScript,而css加载器将文件解释为css
npx esbuild index.js --bundle --loader:.png=dataurl --loader:.svg=text
# 或者
echo 'import index = require("./index")' | npx esbuild --loader=ts --bundle
# 或者
echo 'let x: number = 1' | npx esbuild --loader=ts
# let x = 1;
注意
esbuild 仅仅是将 ts 语法转化为 js 语法,并不做类型校验
Minify
生成的代码将被最小化
echo 'fn = obj => { return obj.x }' | npx esbuild --minify
# fn=n=>n.x;
# 或者
echo 'fn = obj => { return obj.x }' | npx esbuild --minify-whitespace
# fn=obj=>{return obj.x};
# 或者
echo 'fn = obj => { return obj.x }' | npx esbuild --minify-identifiers
#fn = (n) => {
# return n.x;
#};
# 或者
echo 'fn = obj => { return obj.x }' | npx esbuild --minify-syntax
#fn = (obj) => obj.x;
注意
- 当启用缩小功能时,您可能还应该设置目标选项
默认情况下,esbuild利用了现代JavaScript的特性使你的代码更小
例如: a === undefined || a === = null ? 1 : a 可以被压缩为 a??1
如果你不想让esbuild在缩小时利用现代JavaScript特性,你应该使用一个比较老的语言,比如:--target=es6
- 在JavaScript模板文本中,字符转义序列 \n 将被替换为换行符
如果目标支持字符串字面量,并且这样做会导致更小的输出,那么字符串字面量也会被转换成模板字面量
这不是一个bug,缩小意味着你要求更小的输出,转义序列 \n 需要两个字节,而换行字符需要一个字节
- 默认情况下,esbuild不会缩小顶级声明的名称
这是因为esbuild不知道你将如何处理输出
您可能会将缩减后的代码注入到其他一些代码中,在这种情况下,缩减顶级声明名称将是不安全的
设置输出格式(或者启用绑定,如果你还没有设置,它会为你选择一个输出格式)告诉esbuild输出将在它自己的范围内运行,这意味着它可以安全地减少顶级声明名称。
- 对JavaScript代码来说,缩小不是100%安全的
对于esbuild以及其他流行的JavaScript迷你器(如terser)来说都是如此
特别是,esbuild并不是为了保留函数调用 .tostring() 的值而设计的
这样做的原因是,如果所有函数中的所有代码都必须逐字保存,那么最小化几乎什么都做不了,而且实际上是无用的
然而,这意味着依赖于.tostring() 返回值的JavaScript代码在最小化时可能会中断
例如,AngularJS框架中的一些模式在
- 默认情况下,esbuild不会在函数和类对象上保留 .name 的值
这是因为大多数代码不依赖于这个属性,使用更短的名称是一个重要的大小优化
但是,有些代码确实依赖于 .name 属性进行注册和绑定
如果你需要依赖这个选项,你应该启用keep names选项。
- 某些 JavaScript 特性可以禁用 esbuild 的优化,包括缩小
具体来说,使用直接 eval 或 with 语句可以防止esbuild将标识符重命名为更小的名称,因为这些特性会导致标识符绑定发生在运行时,而不是编译时
这可能是无关紧要的,因为大部分人不会这样做
Outdir
构建操作的输出目录
npx esbuild index.js --bundle --outdir=out
Outfile
构建操作的输出文件
npx esbuild index.js --bundle --outfile=bundle.js
Platform
默认情况下,esbuild的绑定器被配置为生成用于浏览器的代码。
如果你打包的代码打算在node中运行,你应该将 platform 设置为 node。
# 自调用函数 (默认)
npx esbuild index.js --bundle --platform=browser
# 或者 commenJs
npx esbuild index.js --bundle --platform=node
# 或者 Es module
npx esbuild index.js --bundle --platform=neutral
Serve
类似 webpack-server 的一个 http 服务器,意思就是让你改了代码不用一直执行 esbuild
执行命令
npx esbuild src/index.js --servedir=www --outdir=www/js --bundle
然后创建 www 目录,创建 index.html
<script script src="js/index.js"></script>
或者您只需要服务你的 js
npx esbuild src/index.js --outfile=out.js --bundle --serve=8000
index.html
<script src="http://localhost:8000/out.js"></script>
参数
interface ServeOptions {
port?: number;
host?: string;
servedir?: string;
onRequest?: (args: ServeOnRequestArgs) => void;
}
代理服务器示例
const esbuild = require('esbuild');
const http = require('http');
// 在一个随机的本地端口上启动esbuild服务器
esbuild.serve(
{
servedir: __dirname,
},
{
// ... 构建选项 ...
}
).then(result => {
// result 告诉我们 esbuild 的本地服务器在哪里
const {host, port} = result
// 然后在端口上启动代理服务器 3000
http.createServer((req, res) => {
const options = {
hostname: host,
port: port,
path: req.url,
method: req.method,
headers: req.headers,
}
// 将每个传入的请求转发给esbuild
const proxyReq = http.request(options, proxyRes => {
// 如果esbuild返回“未找到”,发送一个自定义404页面
if (proxyRes.statusCode === 404) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<h1>A custom 404 page</h1>');
return;
}
// 否则,将响应从esbuild转发到客户端
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
});
// 将请求体转发给esbuild
req.pipe(proxyReq, { end: true });
}).listen(3000);
});
Sourcemap
让打包后的代码有一个源代码得映射文件,就是说在浏览器调试的时候可以看源代码
# linked (默认)
# 生成为一个单独的 .js.map 源映射, 输出文件 bundle.js 包含一个特殊的 //# sourceMappingURL= 注释,该注释指向 .js.map。这样,当您打开调试器时,浏览器就知道在哪里找到给定文件的源映射
npx esbuild index.js --sourcemap --outfile=bundle.js
# external
# 生成为一个单独的 .js.map 源映射, 但与 linked 模式不同的是, 输出文件 bundle.js 不包含 //# sourceMappingURL=
npx esbuild index.js --sourcemap=external --outfile=bundle.js
# inline
# 不生成 .js.map 文件,注解 //# sourceMappingURL= 后边跟着一串映射的 base64 数据
npx esbuild index.js --sourcemap=inline --outfile=bundle.js
# both
# inline 和 external 的结合
npx esbuild index.js --sourcemap=both --outfile=bundle.js
注意
在浏览器中,需要打开设置里的Enable source maps的按钮
在nodejs中, v12.12.0 已经内置支持了原映射
node --enable-source-maps index.js
Splitting
代码分割,将相同的代码打包到一个公共的 js 中
注意:这个 api 还在开发中, 目前只适用于esm输出格式
npx esbuild index.js utils.js --bundle --splitting --outdir=out --format=esm
Target
打包的js需要支持的环境,默认是 esnext,也就是最新的 javascript 和 css 语法和特性,意味着默认打包出来的代码天然不支持 ie(that’s good)
npx esbuild index.js --target=es2020,chrome58,firefox57,safari11,edge16,node12
Watch
esbuild 监听文件系统上的更改,并在文件更改可能导致构建无效时重新构建
npx esbuild index.js --outfile=bundle.js --bundle --watch
当你在 js 中使用它的时候,watch 可以是一个对象,例如:
require('esbuild').build({
entryPoints: ['index.js'],
outfile: 'bundle.js',
bundle: true,
watch: {
onRebuild(error, result) {
if (error) console.error('监听失败:', error)
else console.log('监听成功:', result)
// result.stop() 可以停止监听
},
},
}).then(result => {
console.log('监听中...')
})
Write
默认情况下,build api 会将构建的内容自动写入系统文件,write: false 可以阻止这种行为
举个例子
let result = require('esbuild').buildSync({
entryPoints: ['app.js'],
sourcemap: 'external',
write: false,
outdir: 'out',
})
for (let out of result.outputFiles) {
console.log(out.path, out.contents)
}
高级选项
Allow overwrite
允许输出文件覆盖输入文件(我猜没人会这样干)
举个例子
npx esbuild index.js --outdir=. --allow-overwrite
# 这样你的 index.js 源代码会消失
Analyze
生成一个关于bundle内容的易于阅读的报告
npx esbuild --bundle index.js --outfile=bundle.js --minify --analyze
Asset names
loader选项为file时,给打包后的资源增加额外的信息
# [name], [hash], [dir], [ext] 这四个顾名思义,任意组合
npx esbuild index.js --asset-names=assets/[name]-[hash] --loader:.png=file --bundle --outdir=out
Banner
在生成的JavaScript和CSS文件的开头插入任意字符串,通常用于插入注释
在 git bash 上运行可能会有一些问题:https://github.com/evanw/esbuild/issues/2150
npx esbuild index.js --banner:js=//comment --banner:css=/*comment*/ --outfile=bundle.js
Charset
顾名思义,设置字符编码
echo 'let a = 你好' | npx esbuild
# let a = \u4F60\u597D;
echo 'let a = 你好' | npx esbuild --charset=utf8
# let a = 你好;
Chunk names
启用代码分割时自动生成的共享代码块的文件名
# [name], [hash], [ext]
esbuild index.js --chunk-names=chunks/[name]-[hash] --bundle --outdir=out --splitting --format=esm
Color
构建时候是否开启颜色
echo 'typeof x == "null"' | npx esbuild --color=true
Conditions
说实话不太懂这个api的含义和用处,所以这里解释比较简单
conditions 允许你将相同的导入路径重定向到不同的文件位置
npx esbuild index.js --bundle --conditions=custom1,custom2
举个例子
当我们 import “pkg/foo” 的时候,它指向 ./imported.mjs
当我们 require(“pkg/foo”) 的时候,它指向 ./required.cjs
{
"name": "pkg",
"exports": {
"./foo": {
"import": "./imported.mjs",
"require": "./required.cjs",
"default": "./fallback.js"
}
}
}
Drop
在构建之前需要删除的内容,比如页面里的 console
# 删除 debugger
npx esbuild index.js --drop:debugger
# 删除 console
npx esbuild index.js --drop:console
Entry names
入口文件名修改,一般情况下修改文件名,可以让我们的用户用到最新的功能
# [name], [hash], [dir], [ext]
npx esbuild index.js --entry-names=[dir]/[name]-[hash] --bundle --outdir=out
Footer
和 Banner API 有相同的问题
在生成的 JavaScript 和 CSS 文件的末尾插入任意字符串,通常用于插入注释
npx esbuild index.js --footer:js=//comment --footer:css=/*comment*/ --outfile=bundle.js
Global name
只在 format
为 iife
时起作用,设置全局变量的名称,也就是将 iife
自调用的函数赋值给该变量
echo 'module.exports = "test"' | npx esbuild --format=iife --global-name=vue
# 或者
echo 'module.exports = "test"' | npx esbuild --format=iife --global-name='vue.test["xx"]'
Ignore annotations
由于JavaScript是一种动态语言,对于编译器来说,识别未使用的代码有时非常困难,因此社区开发了一些注释,以帮助告诉编译器哪些代码应该被认为是没有副作用的,并且可以删除。目前esbuild支持两种形式的副作用注解
- 行内 /* @PURE */ 函数调用前的注释告诉 esbuild,如果结果值没有被使用,函数调用可以被删除。
- package.json 的 sideEffects 字段用来告诉esbuild,如果你的包中的所有导入文件最终都没有被使用,那么该包中的哪些文件可以被删除。这是Webpack 的约定,很多发布到 npm 的库都已经在包定义中包含了这个字段。你可以在 Webpack 的文档中了解更多关于这个字段的信息。
npx esbuild index.js --bundle --ignore-annotations
Incremental
用相同的选项反复调用 esbuild 的 build API,就打开这个选项
JSX
处理JSX语法,可以将 JSX 转换为 JS(默认),也可以在输出中保留 JSX 语法
echo '<div/>' | npx esbuild --jsx=preserve --loader=jsx
# <div />;
echo '<div/>' | npx esbuild --loader=jsx
# /* @__PURE__ */ React.createElement("div", null);
JSX factory
设置 JSX元素 调用的函数
echo '<div/>' | npx esbuild --jsx-factory=h --loader=jsx
# /* @__PURE__ */ h("div", null);
JSX fragment
设置 JSX片段调用的函数
echo '<>content</>' | npx esbuild --jsx-fragment=Fragment --loader=jsx
# /* @__PURE__ */ React.createElement(Fragment, null, "content");
Keep names
将函数的name属性设置为"fn",这个api也不懂它的含义在哪
npx esbuild index.js --minify --keep-names
Legal comments
- none
不要保留任何legal comment
- inline
保留所有legal comment
- eof
将legal comment
移到文件末尾. - linked
将legal comment
移动到 .LEGAL.txt 文件中,并用注释链接它. - external
将legal comment
移动到 .LEGAL.txt 文件中,但不链接它
npx esbuild index.js --legal-comments=eof
Log level
日志级别
- silent
不要显示任何日志输出 - error
只显示错误 - warning
只显示警告和错误 - info
显示警告、错误和输出文件摘要 - debug
记录所有的信息和一些额外的消息. - verbose
生成大量日志消息
echo 'typeof x == "null"' | npx esbuild --log-level=error
Log limit
控制打印日志的数量,否则可能控制台会很卡,默认是10条
npx esbuild index.js --log-limit=10
Main fields
当在node中导入包时,定义导入的是 package.json 的哪个字段
- main
- module
- browser
npx esbuild index.js --bundle --main-fields=module,main
Mangle props
传递一个正则表达式给esbuild,告诉esbuild自动重写所有匹配这个正则表达式的属性(破坏性很强的 api,少用为妙)
举个例子
index.js
let x = { xx_: 'x' };
let y = { xx_: "y" };
# 它会将所有的以 _ 结尾的属性全部改掉
npx esbuild index.js --mangle-props=_$
# let x = { a: "x" };
# let y = { b: "y" };
Metafile
以JSON格式生成一些关于构建的元数据
npx esbuild index.js --bundle --metafile=meta.json --outfile=out.js
Node paths
全局目录列表。除了查找所有父目录中的node_modules目录之外,还会搜索这些路径
NODE_PATH=someDir npx esbuild index.js --bundle --outfile=bundle.js
Out extension
自定义esbuild生成的文件的扩展名
npx esbuild index.js --bundle --outdir=dist --out-extension:.js=.mjs
Outbase
基础路径
npx esbuild src/pages/home/index.ts src/pages/about/index.ts --bundle --outdir=out --outbase=src
Preserve symlinks
This setting mirrors the --preserve-symlinks setting in node. If you use that setting (or the similar resolve.symlinks setting in Webpack), you will likely need to enable this setting in esbuild too. It can be enabled like this:
npx esbuild index.js --bundle --preserve-symlinks --outfile=bundle.js
Public path
为每个被这个 loader
加载的文件的导出字符串添加一个基本路径
npx esbuild index.js --bundle --loader:.png=file --public-path=https://www.example.com/v1 --outdir=out
Pure
各种JavaScript工具都有一个惯例,在new或call表达式之前有一个包含/* @PURE /或/ #PURE */的特殊注释,这意味着如果结果值未被使用,该表达式可以被删除。它看起来是这样的:
echo 'console.log("foo:", foo())' | npx esbuild --pure:console.log --minify
Resolve extensions
设置后引入文件可以省略文件扩展名
npx esbuild index.js --bundle --resolve-extensions=.ts,.js
Source root
设置映射源代码目录
npx esbuild index.js --sourcemap --source-root=https://raw.githubusercontent.com/some/repo/v1.2.3/
Sourcefile
设置映射源代码文件
cat index.js | npx esbuild --sourcefile=example.js --sourcemap
Sources content
设置映射源代码内容,不知道如何使用
npx esbuild --bundle app.js --sourcemap --sources-content=false
Stdin
通常用于没有入口文件的时候,比如对应于在命令行上将一个文件管道接到stdin
echo 'export * from "./another-file"' | npx esbuild --bundle --sourcefile=imaginary-file.js --loader=ts --format=cjs
Tree shaking
死代码删除
npx esbuild app.js --tree-shaking=true
Tsconfig
顾名思义,设置tsconfig
npx esbuild index.ts --bundle --tsconfig=custom-tsconfig.json
Tsconfig raw
echo 'class Foo { foo }' | npx esbuild --loader=ts --tsconfig-raw='{"compilerOptions":{"useDefineForClassFields":true}}'
Working directory
指定要用于构建的工作目录
require('esbuild').buildSync({
entryPoints: ['file.js'],
absWorkingDir: process.cwd(),
outfile: 'out.js',
})
Content Types
内容类型,每种内容类型都有一个关联的默认 loader
,你可以覆盖它
- JavaScript :
js
文件后缀:.js
,.cjs
,.mjs
你可以配置target
属性来控制编译出来的代码版本
Syntax transform | Transformed when --target is below | Example |
---|---|---|
Exponentiation operator | es2016 | a ** b |
Async functions | es2017 | async () => {} |
Spread properties | es2018 | let x = {…y} |
Rest properties | es2018 | let {…x} = y |
Optional catch binding | es2019 | try {} catch {} |
Optional chaining | es2020 | a?.b |
Nullish coalescing | es2020 | a ?? b |
import.meta | es2020 | import.meta |
Logical assignment operators | es2021 | a ??= b |
Class instance fields | esnext | class { x } |
Static class fields | esnext | class { static x } |
Private instance methods | esnext | class { #x() {} } |
Private instance fields | esnext | class { #x } |
Private static methods | esnext | class { static #x() {} } |
Private static fields | esnext | class { static #x } |
Ergonomic brand checks | esnext | #x in y |
Import assertions | esnext | import “x” assert {} |
Class static blocks | esnext | class { static {} } |
还有一些语法它不会转换
Syntax transform | Unsupported when --target is below | Example |
---|---|---|
Asynchronous iteration | es2018 | for await (let x of y) {} |
Async generators | es2018 | async function* foo() {} |
BigInt | es2020 | 123n |
Hashbang grammar | esnext | #!/usr/bin/env node |
Top-level await | esnext | await import(x) |
Arbitrary module namespace identifiers | esnext | export {foo as ‘f o o’} |
- TypeScript:
ts
或者tsx
文件后缀: .ts, .tsx, .mts, .cts
TypeScript类型声明会被解析并被忽略
Syntax feature | Example |
---|---|
Interface declarations | interface Foo {} |
Type declarations | type Foo = number |
Function declarations | function foo(): void; |
Ambient declarations | declare module ‘foo’ {} |
Type-only imports | import type {Type} from ‘foo’ |
Type-only exports | export type {Type} from ‘foo’ |
Type-only import specifiers | import {type Type} from ‘foo’ |
Type-only export specifiers | export {type Type} from ‘foo’ |
只支持typescript语法扩展,并且总是被转换成JavaScript
Syntax feature | Example | Notes |
---|---|---|
Namespaces | namespace Foo {} | |
Enums | enum Foo { A, B } | |
Const enums | const enum Foo { A, B } | |
Generic type parameters | (a: T): T => a | Not available with the tsx loader |
JSX with types | <Element/> | |
Type casts | a as B and a | |
Type imports | import {Type} from ‘foo’ | Handled by removing all unused imports |
Type exports | export {Type} from ‘foo’ | Handled by ignoring missing exports in TypeScript files |
Experimental decorators | @sealed class Foo {} | The emitDecoratorMetadata flag is not supported |
- JSX:
jsx
或tsx
- JSON:
json
- CSS:
css
- Text:
text
- Binary:
binary
- Base64:
base64
- Data URL:
dataurl
- External file:
file
Plugins
`Plugins`是新的,仍然处于实验阶段。在esbuild 1.0.0版本之前,它可能会随着新用例的出现而改变。您可以根据跟踪问题获得关于此特性的更新
Plugins 允许你将代码注入到构建过程的各个部分
寻找插件
https://github.com/esbuild/community-plugins
使用插件
一个esbuild插件是一个有名字和setup函数的对象。它们以数组的形式传递给构建API调用。每次构建API调用都运行一次setup函数
其他详情请查看Plugin
更多推荐
所有评论(0)