初识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.jsontsconfigRaw可以在直接传递 ts 时配置选项,不用制定配置文件。stdin作为打包入口,可以手动书写内容。splitting代码分割,只适用于format:esm.assetNames资源配置输出路径、chunkNames分包配置输出块文件的文件路径outdir输出文件目录名outfile输出文件名,只适用于单入口alias为一些长路径配置别名external定义构建时不被处理的包。引入外部包,cdn 引入等inject定义全局变量的替换文件。metafile打包时生成一些元数据信息,可用于分析打包后的代码。
仅适用于transform
没有
同时适用build、transform
platform代码生成面向的平台,默认浏览器browser,可以指定为node、neutralloader指定文件该如何解析,根据文件后缀指定。banner可以自定义内容插入到文件顶部。footer可以自定义内容插入到文件尾部charset配置打包的字符集,默认是ASCIIformat配置输出文件的格式,包括 iife、cjs、esmjsxjsx 语法解析的配置jsxFactory自定义 jsx 语法如何解析,定义函数名。vue 中是htarget构建目标代码生成的环境,比如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 的配置。
浙公网安备 33010602011771号