esbuild 非常快速的 web 打包器,使用 go 语言编写。
📦 特点:
- 无需缓存也能很快速的编译打包。
- 内置 js、css、ts、jsx 类型文件编译。
- 支持 es6 和 commonjs 模块。
- 可以编译打包成 esm 模块和 common JS 模块
- tree shaking 摇树优化、优化资源大小、source-map 代码映射
- 启动本地服务,在监听模式下文件发生变化重新编译。
esbuild-vu3 代码仓库地址文章来源地址https://www.toymoban.com/news/detail-427501.html
安装使用
创建示例项目
$> mkdir esbuild-vue3
$> cd esbuild-vue3
安装 esbuild
$> npm init -y
$> npm install --save-exact esbuild vue@next
定义基础的构建脚本package.json
-
--bundle
打包编译文件,可以将任何依赖项都内联到文件中。 -
--outfile
定义输出文件名。多文件入口时则需要配置 outdir
{
"scripts": {
"build": "esbuild ./src/index.js --bundle --outfile=./dist/index.js"
},
"dependencies": {
"esbuild": "0.17.8",
"vue": "^3.2.47"
}
}
在index.js
中基本的输出 vue 版本。
import { createApp } from "vue";
const app = createApp();
console.log(app.version);
npm run build
编译后执行编译包node dist/index.js
可以看到打印出来的 vue 版本号。
编写 build 脚本文件
像这种简单的执行编译构建,可以直接书写 esbuild --**
,实际项目中需要更多的配置。
创建 scripts/build.js
/**
* 编译打包构建项目
*/
require("esbuild")
.build({
// 编译入口
entryPoints: ["src/index.js"],
//
bundle: true,
// 编译输出的文件名
// outfile: "out.js",
// 编译文件输出的文件夹
outdir: "dist",
})
.catch(() => process.exit(1));
在控制台测试node scripts/build.js
正常,更新 package.json 中的脚本文件。
两大常用 API build和transform
其他的一些 API 配置项有的只用于 build,有的只用于 transofrm,也有都可以用的。
-
build
打包编译代码,并写入文件系统。 -
transform
顾名思义,用于转换代码。比如.vue 文件转换、typescript 转 js 等等。
build(options)
一个最简单的示例。
const esbuild = require("esbuild");
esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
outdir: "dist",
});
入口为当前目录的 index.js。打包编译后输出到 dist 文件目录中。
在我们正常开发时,则需要监听文件的变化,重新编译。以及一个开发时的文件服务器。
-
watch mode
监视文件系统,在编辑和保存的时候重新编译。const esbuild = require("esbuild"); async () => { let context = await esbuild.context({ ...BaseConfig, sourcemap: "both", metafile: true, }); // 使用上下文,开启监听 await context.watch(); };
-
serve mode
开发的同时则需要静态资源服务器,以方便我们在浏览器中看到更改的变化const esbuild = require("esbuild"); async () => { let context = await esbuild.context({ ...BaseConfig, sourcemap: "both", metafile: true, }); // 使用上下文,开启监听 await context.watch(); // 开启一个服务 let { host, port } = await context.serve({ servedir: "dist", port: 8080, host: "127.0.0.1", }); console.log(`Serve is listening on http://${host}:${port}`); };
通过指定资源服务目录,就可以启动一个静态的资源服务器。搭配
watch mode
就可以支撑我们日常的开发模式了。 -
rebuild mode
手动重新编译,这个可以作为集成到其他构建工具一起时,可以手动进行编译。await context.rebuild();
transform(code,options)
转换代码,将 JS 语法糖,转换为浏览器可识别的 JS 原生代码。也包括 css 预编译 less、scss 等。
假设我们现在有一个用于转换 .vue
文件的库,可以读取到某个文件夹下的.vue 文件然后转换
const esbuild = require("esbuild");
const fs = require("fs");
async () => {
// 读取.vue文件
const contents = await fs.promises.readFile("src/App.vue", "utf8");
// 手动执行转换
const result = await esbuild.transform(contents, {
loader: "vue-loader",
});
};
这个 loader
配置稍后再将,假设暂时有这个一个解析 vue 文件的 loader。大概就是这个样子
async\sync
同步、异步 API
同步、异步 API 都可以在特定的场景下使用。
- 同步 API 和插件一起使用,插件是异步的。
- 同步 API 会阻塞线程,所以需要更好的性能表现,使用异步 API。
- 同步 APi 调用可以使你 的代码看起来更整洁。在
async...await...
可用时,我更喜欢用异步
import * as esbuild from "esbuild";
// 异步
let result1 = await esbuild.transform(code, options);
let result2 = await esbuild.build(options);
// 同步
let result1 = esbuild.transformSync(code, options);
let result2 = esbuild.buildSync(options);
API 配置项说明
标注说明哪些可以用 build,哪些可以用 transform。(我阅读过觉得重要的,还有一些未列出)
仅适用于build
-
entryPoints
编译入口,字符串是为单入口,多入口时配置为数组形式 -
bundle
打包文件,从入口文件开始,递归处理以来的文件,以内联的方式打包打包到一个文件中。 -
cancel
取消编译进程,context.cancel()
中断打包。 -
watch
监听文件系统,发生变化可重新构建。 -
serve
创建一个静态资源服务。 -
rebuild
手动调用,重新执行打包。 -
tsconfig
配置 ts 的配置文件,默认项目目录下的tsconfig.json
-
tsconfigRaw
可以在直接传递 ts 时配置选项,不用制定配置文件。 -
stdin
作为打包入口,可以手动书写内容。 -
splitting
代码分割,只适用于format:esm
. -
assetNames
资源配置输出路径、 -
chunkNames
分包配置输出块文件的文件路径 -
outdir
输出文件目录名 -
outfile
输出文件名,只适用于单入口 -
alias
为一些长路径配置别名 -
external
定义构建时不被处理的包。引入外部包,cdn 引入等 -
inject
定义全局变量的替换文件。 -
metafile
打包时生成一些元数据信息,可用于分析打包后的代码。
仅适用于transform
没有
同时适用build、transform
-
platform
代码生成面向的平台,默认浏览器browser
,可以指定为node、neutral
-
loader
指定文件该如何解析,根据文件后缀指定。 -
banner
可以自定义内容插入到文件顶部。 -
footer
可以自定义内容插入到文件尾部 -
charset
配置打包的字符集,默认是ASCII
-
format
配置输出文件的格式,包括 iife、cjs、esm -
jsx
jsx 语法解析的配置 -
jsxFactory
自定义 jsx 语法如何解析,定义函数名。vue 中是h
-
target
构建目标代码生成的环境,比如chrome\edge\node
,并可指定版本 -
define
自定义一些全局变量,以便在不同构建模式中,有不同的表现 -
drop
打包时,丢弃掉代码中指定的语句,比如 debugger、console -
minify
最小化代码 -
treeShaking
摇树优化 -
sourcemap
代码映射文件生成,代码浏览器调试。
配置 vue
创建 App.vue
,并修改 index.js. 在此编译时提示报错No loader is configured for ".vue" files: src/App.vue
安装vue-loader
$> npm i vue-loader -D
配置build.js
, 增加 loader 配置,针对文件后缀指定文件解析方式。
require("esbuild").build({
// ...
// 配置loader
loader: {
".vue": "vue-loader",
},
});
配置完成后,在此执行npm run build
,虽然不报错了,但是编译文件并没有生成,可以看到控制台当前命令执行失败的。但是看不到日志
配置打包日志输出,调整build.js
/**
* 编译打包构建项目
*/
const esbuild = require("esbuild");
// 开发、生产环境公用配置
const BaseConfig = require("./base.js");
(async () => {
let result = await esbuild.build({
...BaseConfig,
// 压缩代码
minify: true,
// 配合压缩移除空格
minifyWhitespace: true,
// 配合压缩重命名变量
minifyIdentifiers: true,
metafile: true,
});
let text = await esbuild.analyzeMetafile(result.metafile, {
verbose: true,
});
console.log(text);
})();
重新执行 npm run build
,这时候看到了打印的错误输出 Invalid loader value: "vue-loader"
看来是配置错误,不是这样配置的。😔
后来研究了好久,想利用 @vue/compiler-sfc
写一个 esbuild 插件,一直没有调试通,暂时放弃。
安装插件 esbuild-plugin-vue3
通过查找已经有人写好的插件供使用
$> npm i esbuild-plugin-vue3
调整基础脚本配置文件base.js
const vuePlugin = require("esbuild-plugin-vue3");
module.exports = {
// 插件
plugins: [vuePlugin()],
};
再次执行启动,运行成功。
这个插件支持生成 html 文件,并可以把生成 css 文件注入到视图中。
module.exports = {
// 插件
plugins: [
vuePlugin({
generateHTML: "public/index.html",
}),
],
};
遇到的一写问题:
-
alias 定义的'@'在插件中不能解析。提示文件不存在。是因为他没有转换
@
。配置
@
的时候,需要解析当前脚本所在的路径,/scripts/dev.js
. 配置为path.resolve(__dirname, "../src")
使用 jsx 语法
重新创建了App.jsx
文件,和 App.vue 内容一致。导入使用,报错React is not defined
import { defineComponent } from "vue";
export default defineComponent({
data() {
return {
name: "admin",
num: 0,
};
},
mounted() {
console.log("App init");
},
render() {
return (
<div class="app">
<h1>{this.name}</h1>
<p>{this.num}</p>
<button onClick={() => this.num++}>click++</button>
</div>
);
},
});
在 esbuild 中,默认 jsx 语法解析是使用的 react 库,所以没有安装 react 就会报错。修改配置,使用自定义 jsx 解析函数
module.exports = {
loader: {
".js": "jsx",
},
jsxFactory: "h",
jsxFragment: "Fragment",
};
顺便配置.js 文件是被 jsx 语法解析,这样文件后缀直接书写 App.js。
现在重新运行,还是会报错,报错h is not defined
. 虽然定义了,但是没有指明函数从哪里来。
修改App.js
文件,增加导入h
函数
import { h } from "vue";
再次运行,页面正常打开。
但有个问题,我们需要在每个页面都要导入import { h } from "vue";
就感觉比较麻烦。
可以通过属性inject
注入来定义 h 函数,从而达到自动注入的目的。
新建一个jsxFactory.js
文件,定义导出函数。
const { h, Fragment } = require("vue");
export { h as "React.createElement", Fragment as "React.Fragment" };
重新修改配置文件,这时使用了注入文件修改了全局函数React.createElement
,就不需要再配置 jsxFactory 了。
module.exports = {
// jsxFactory: "h",
// jsxFragment: "Fragment",
inject: ["libs/jsxFactory.js"],
};
现在可以开心的移除 App.js 中 h 函数的导入了。后续的文件也需要在配置。
使用 less
安装less
,即可正常使用
$> npm i less -D
但是单独引入.less 文件时,提示报错,没有解析该文件的 loader。
安装esbuild-plugin-less
,
const { lessLoader } = require("esbuild-plugin-less");
module.exports = {
// 插件
plugins: [lessLoader()],
};
周边组件库安装
axios\vue-router\vuex\element-plus
安装
$> npm i axios dayjs element-plus vue-router vuex
错误Cannot use import statement outside a module
解析问题
一些分包 chunk 还存在 import。可能是 es、cjs 混合导致无法被转义。
基础配置中,打包输出格式format:esm
, 支持分包配置splitting
,可根据 imort 动态导入的打包依赖项。
修改配置,移除分包配置。使用iife\cjs
模式编译输出项目访问正常。
module.exports = {
format: "iife",
// splitting: true,
};
使用
esm
进行分包编译时,存在一个包里没有 import 语句。其他分包都有,报错不能使用。
有 babel 插件转成 es5 应该就可以了
解决 format:'esm'
分包前端报错问题,也就是上面提到的问题
在使用了 esModule 采取模块分包后,所有的语法比如import、let、const
新语法都是支持的。我尝试通过配置构建目标而不使用这些特性语法。
module.exports = {
// 构建目标es新标准
target: ["es5"],
};
再次编译控制之态报错,全是语法不被支持。也就说明了 esbuild 只是一个编译打包器,想要转义这些语法,还得使用 babel。
自动 polyfill 注入不在 esbuild 的范围内
那我们还是使用最新的语法支持,构建目标。为了让浏览器支持 import 模块导入,需要在引入的所有 script 脚本中增加type='module'
之前使用插件esbuild-plugin-vue3
,生成了 index.html。查了配置没有地方配置给 script 增加 type。
module.exports = {
vuePlugin({ generateHTML: "public/index.html" }),
};
所以不使用生成的 index.html,去掉配置参数generateHTML
。先使用public/index.html
测试,待npm run start
后, 更改 index.html,手动导入主入口文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>esbuild+vue3</title>
<link rel="stylesheet" href="../dist/index.css" />
</head>
<body>
<div id="app"></div>
<script type="module" src="../dist/index.js"></script>
</body>
</html>
使用 vscode 的 live serve
插件功能起一个静态服务。访问正常,这样就给了一个思路,只需要复制public/index.html
,手动导入主入口文件即可。
不能处理vue-router
组件动态导入的问题,
vue-router
支持开箱即用的动态导入,这样可以将代码分隔成不同的代码块。现在的配置不能处理,可能需要配置 babel,额外处理了。
import MainPage from "../views/index.vue";
onst routes = [
{
path: "/",
redirect: "/main",
component: MainPage,
// component: () => import("../views/index.vue"),
}
]
安装了一个esbuild-plugin-babel
来配置使用 babel, 但因为不是 commonJS 规范的,导致不能导入使用。
import babel from "esbuild-plugin-babel";
// 需要修改package.json 中配置,
// type:'module'
// 这样就会导致在脚本中无法使用require,无法使用其他插件。冲突更多了。
解决package.json配置type:module时的问题
这个问题和上面的问题牵扯,单独提取是因为修改的比较多。
找一个自动生成index.html
的插件,并可以自动加载主入口文件。@chialab/esbuild-plugin-html
这个有点意思,当然还有其他的插件,之后尝试,
安装@chialab/esbuild-plugin-html
这个插件的package.json
配置属性 type 就是 module
。说明仅支持 esm,也就需要修改所有的脚本文件,不能再以 cjs 方式加载了。
修改了type:module
就表明所有的 js 文件都是 esModlue,也就不能使用require\module.exports
语句了。
这个插件将提供的index.html
作为入口文件,然后将编译过后的入口文件和 css 样式文件动态加载到 html 中。
所有的构建路径都变得无法捉摸。
修改配置,原来的 html 模板是放到 public 下的,配置并不能起作用,不能加载到 ./src/index.js
主入口文件。
看了示例,是放到 src 下的,也就是和入口文件同目录,我放到项目根目录下。
这让我想起了 vite 要求 index.html 在项目根目录下。
修改index.html
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>esbuild+vue3</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/index.js"></script>
</body>
</html>
修改编译配置文件scripts/base.js
import html from "@chialab/esbuild-plugin-html";
export default {
entryPoints: ["./index.html"],
// 资源目录文件路径
assetNames: "assets/[name]-[hash]",
// 分包资源路径
chunkNames: "[ext]/[name]-[hash]",
// 打包输出的格式
format: "esm",
// 代码分离,一是多入口共享文件;二是import动态导入的依赖项
splitting: true,
plugins: [
// ...
html(),
],
alias: {
"@": path.resolve("./src"),
},
};
虽然我们的脚本路径是scripts/base.js
,脚本中相对路径引用确实./
,而不是../
。esm 和 cjs 上下文不同导致的。
我们在项目根目录下执行的脚本npm run start
,在 esm 中,一直保持这种上下文状态。所以都是./src,./index.html
使用此插件是,必须配置chunkNames/assetNames
,指定资源编译目录。才可以正常加载。
脚本文件中导入需改为 esm,记录一下其他解决的问题
-
__dirname
是 node 环境下的特殊变量,现在改为 esm,是不能用了。只能依赖 node 库
// scripts/base.js
export default {
// alias: {
// "@": path.resolve(__dirname, "../src"),
// },
alias: {
"@": path.resolve("./src"),
},
};
-
esbuild-plugin-vue3
插件不能用了,只支持 require 加载, 安装esbuild-plugin-vue-next
-
解决 jsx 语法的语法不被支持了,这个很奇怪
Using a string as a module namespace identifier name is not supported in the configured target environment ("es2020")
// libs/jsxFacotry.js
const { h, Fragment } = require("vue");
// export { h as "React.createElement", Fragment as "React.Fragment" };
window.React = {
createElement: null,
Fragment: null,
};
window.React.createElement = h;
window.React.Fragment = Fragment;
突然发现只要定义全局变量命名覆盖就好了。
- esm 和 cjs 脚本相对路径上下文不同。
发现其他插件
-
json \ css \ text
文件都是默认支持导入,无需配置,当然也可以配置为其他 loader 组件。 -
图片资源
.png\jpg
等需要手动配置导入的 loader,可选多种方式,
-
binary
二进制文件,需要操作二进制文件时。打包时将编码嵌入到编译包。 -
base64
加载为 base64,将编码作为字符串嵌入到编译包。 -
dataurl
加载为二进制数据,作为 base64 编码嵌入到编译包。 -
file
将文件输出到输出目录中,使用文件名默认导出进行导入。 -
copy
复制文件到编译目录中,重写导入路径。引用该文件路径,module.exports = { // 配置loader loader: { ".png": "file", }, };
- 配置 babel,以便使用代码拆分功能,以及路由的动态导入。
可以关注仓库分支,有时间会完善 babel 的配置。文章来源:https://www.toymoban.com/news/detail-427501.html
esbuild-vu3 代码仓库地址
到了这里,关于初识esbuild、构建vue3脚手架的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!