初识esbuild、构建vue3脚手架
esbuild 非常快速的 web 打包器,使用 go 语言编写。
📦 特点:
- 无需缓存也能很快速的编译打包。
- 内置 js、css、ts、jsx 类型文件编译。
- 支持 es6 和 commonjs 模块。
- 可以编译打包成 esm 模块和 common JS 模块
- tree shaking 摇树优化、优化资源大小、source-map 代码映射
- 启动本地服务,在监听模式下文件发生变化重新编译。
安装使用
创建示例项目
$> 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、esmjsx
jsx 语法解析的配置jsxFactory
自定义 jsx 语法如何解析,定义函数名。vue 中是h
target
构建目标代码生成的环境,比如chrome\edge\node
,并可指定版本define
自定义一些全局变量,以便在不同构建模式中,有不同的表现drop
打包时,丢弃掉代码中指定的语句,比如 debugger、consoleminify
最小化代码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 的配置。