kpack是一个追求更好构建产物的前端构建工具。它基于rollup,并兼容vite,如何使用基本可参考vite。是一个面向Web的rollup实践。
业界很多人对Webpack的速度不满意,但是我主要是对Webpack的构建产物不满意。首先Webpack的运行时太重了。还有就是Webpack对如何加载模块,没有开放太多的能力。我希望开发一次,打包时可以用不同的配置方案打多个包,然后在HTML中可以根据浏览器兼容性进行条件引入。Webpack对于polyfill的使用也不能让人满意。Webpack是将polyfill作为普通的依赖库一样,由webpack-runtime管理,这导致webpack-runtime自身所需的polyfills无法提前引入。在我的理念里,我把polyfills分成2部分。运行时必要的polyfills和运行时在一起在最前面引入;其余polyfills按普通依赖处理。在一番探索后我决定使用rollup这个工具。
在使用了一段时间rollup后,Vite出现了。Vite的设计很符合我的胃口,no bundle的设计大大提升了开发服务器的速度;打包时按照浏览器打不同的包也正是我想要的。但是Vite有着许多问题,让我只好继续使用我自己配置的打包方案。Vite大规模地使用了esbuild,让esbuild处理commonjs是没问题的,但是esbuild处理TS和CSS的能力很弱,很多特殊语法不支持,我这里使用了被广泛使用的Babel和PostCSS。Vite对通过路径使用CSS没有很好的方案。vite-legacy对兼容性打包不够理想,多条件兼容性的打包只处理了Babel和polyfills,像CSS、图片、别名,无法分别处理。多条件打包只分了2个版本,没能分得更细。经常出现一些polyfills没有被自动注入的情况。Vite把所有polyfills汇聚到一起,放到页面最前面运行。这会导致首屏内容增加。
在当前路径创建新项目
npm init kpack@latest
在指定路径创建新项目
npm create kpack@latest some-directory
手动用 NPM 安装 kpack 到工程中。
npm i -D kpack
在项目根目录中创建一个名为 kpack.config.js 的文件,并添加以下代码:
const { defineConfig } = require("kpack");
module.exports = function({ command, mode }) {
return defineConfig({
//这里是配置内容
});
};
其中配置内容根据后续需要进行配置
在安装了 kpack 的项目中,可以在 package.json
的 scripts
中使用 kpack 命令行运行它。下面是一个典型的 npm scripts 例子:
{
"scripts": {
"dev": "kpack serve", // 启动开发服务器
"build": "kpack build", // 构建产物
"preview": "kpack preview" // 本地预览构建产物
}
}
然后使用命令 npm run build
进行构建。
kpack可以导入npm上的模块。在原生 ES 导入不支持下面这样的裸模块导入:
import { useState } from 'react'
上面的代码会在浏览器中抛出一个错误。而在 kpack 中,可以导入npm中已安装的库。
kpack 内置了 ts
、tsx
的支持。可以通过创建tsconfig.json,进行ts配置。
内置ts使用babel实现,因此不会依赖tslib,与其他第三方库一样,经过babel转译后最终依赖 @babel/runtime
。转译过程只执行 ts 的转译工作,并不执行任何类型检查。内部对每个ts文件独立转化,因此需要配置 isolatedModules
为 true。
useDefineForClassFields
默认值为 true。
experimentalDecorators
默认值为 false。 即按照stage3的方式转译装饰器。当 experimentalDecorators
设为 true 时,按照ts中 experimentalDecorators
的方式进行转译(并非babel的legacy模式)。
您可以使用 @rollup/plugin-typescript
插件来达到与官方typescript一致的效果。
.jsx
也内置支持了。可以通过配置的jsx选项配置开启。.tsx
的支持见 TypeScript。
{
jsx?: {
runtime: 'classic' | 'automatic';
pragma?: string;
pragmaFrag?: string;
importSource?: string;
};
}
导入一个静态资源会返回解析后的 URL:
import imgUrl from "./img.png"
document.getElementById('hero-img').src = imgUrl;
也可以按以下方式编写
document.getElementById('hero-img').src = new URL('./img.png', import.meta.url).href;
导入 .css 文件,会在加载js时把css内容,用link标签动态引入。也能够以路径的形式引入 CSS。
import './example.css';
let link = document.createElement("LINK");
link.rel = "stylesheet";
link.type = "text/css";
// 作为路径使用
link.href = new URL('./example.css', import.meta.url).href;
css内部如果使用了@import url(xxxx.css)
,将会以内联的方式引入另一个CSS。如果,被引入的css含有相对路径,那么这个相对路径是相对于被引入的CSS的。
替换是基于语法解析的,因此在字符串或是注释中的变量不会被替换。
如果项目包含有效的 PostCSS 配置 (任何受 postcss-load-config 支持的格式,例如 postcss.config.js
),它将会自动应用于所有已导入的 CSS。
kpack内置了对 .scss
, .sass
的支持。没有必要为它们安装特定的插件,但必须安装相应的预处理器依赖
# .scss and .sass
npm add -D sass
任何以 .module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件。导入这样的文件会返回一个相应的模块对象:
/* example.module.css */
.red {
color: red;
}
import classes from './example.module.css'
document.getElementById('foo').className = classes.red
CSS modules 行为可以通过 css.modules 选项 进行配置。
你还可以和预处理器配合使用,例如 style.module.scss
JSON 可以被直接导入 —— 同样支持具名导入:
import json from './example.json'
动态导入可以通过模板字符串得到有限支持。
const module = await import(`./dir/${file}.js`)
注意变量仅代表一层深的文件名。如果 file 是 foo/bar,导入将会失败。对于更进阶的使用详情,你可以使用 glob 导入 功能。
kpack 支持使用特殊的 import.meta.glob
函数从文件系统导入多个模块:
const modules = import.meta.glob('./dir/*.js')
以上将会被转译为下面的样子:
const modules = {
'./dir/foo.js': () => import('./dir/foo.js'),
'./dir/bar.js': () => import('./dir/bar.js'),
}
你可以遍历 modules 对象的 key 值来访问相应的模块:
for (const path in modules) {
modules[path]().then((mod) => {
console.log(path, mod)
})
}
匹配到的文件默认是懒加载的,通过动态导入实现,并会在构建时分离为独立的 chunk。如果你倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),你可以使用 import.meta.globEager
代替:
const modules = import.meta.globEager('./dir/*.js')
以上会被转译为下面的样子:
import * as __glob__0_0 from './dir/foo.js'
import * as __glob__0_1 from './dir/bar.js'
const modules = {
'./dir/foo.js': __glob__0_0,
'./dir/bar.js': __glob__0_1,
}
kpack 在一个特殊的 import.meta.env
上暴露环境变量。这里提供了一些内建变量:
-
import.meta.env.MODE
: {string} 应用运行的模式。 -
import.meta.env.BASE_URL
: {string} 构建资源地址前缀。他由base配置项决定。 -
import.meta.env.DEV
: {boolean} 应用是否运行在开发环境 (判断 NODE_ENV == 'development')。 -
import.meta.env.PROD
: {boolean} 应用是否运行在生产环境 (永远与 import.meta.env.DEV相反)。
这些环境变量会被静态替换,因此,在引用它们时请使用完全静态的字符串。动态的 key 将无法生效。例如,动态 key 取值 import.meta.env[key] 是无效的。
替换是基于语法解析的,因此在字符串或是注释中的变量不会被替换。
kpack 使用 dotenv 从你的 环境目录 中的下列文件加载额外的环境变量:
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
加载的环境变量在构建代码中可以可以使用 process.env 获取;在客户端代码通过 import.meta.env 获取。
为了防止意外地将一些环境变量泄漏到客户端,通过envPrefix可以配置以哪些前缀可以暴露。
{
"envPrefix": ["VITE_", "VUE_APP_"]
}
- development 开发
- staging 测试
- production 生产
你可以通过传递 --mode 选项标志来使用模式。例如,如果你想为我们假设的 staging 模式构建应用:
kpack build --mode staging
而后,会从 .env.staging
文件中加载配置。
kpack 会自动地将一个异步 chunk 模块中使用到的 CSS 代码抽取出来并为其生成一个单独的文件。
在使用“dynamic import”加载脚本时,会同时预加载所依赖的js、css、img,从而避免界面“闪烁”的发生。
kpack 可以使用插件进行扩展。可以兼容rollup插件,和vite插件。
若要使用一个插件,需要将它添加到项目的 devDependencies 并在 kpack.config.js 配置文件中的 plugins 数组中引入它。例如,要想为esm提供require支持,可以按下面这样使用插件。
安装插件
npm add -D rollup-plugin-require
配置插件
const { defineConfig } = require("kpack");
const requireToImport = require("rollup-plugin-require");
module.exports = function({ command, mode }) {
return defineConfig({
plugins: [
requireToImport()
]
});
};
使用插件
document.getElementById("img").src = require("@/assets/image/foo.png");
不同插件可能需要按一定执行顺序运行才能正确运行。可以使用 enforce 修饰符来强制插件的位置。enforce 有 "pre"
、"post"
、undefined
三个可选值。运行顺序如下:
- "pre":核心插件之前调用,一般用来处理自定义格式或语法。
- 核心插件:环境变量、文件处理、ts\jsx\json\css\commonjs
- undefined:核心插件之后调用,到此的都是正规JS语法。JS处理插件都可以在此运行。
- 输出插件:到此已经处理完了功能,这里是非功能插件进行补充,如兼容性处理。
- "post":用来处理自定义补充。
const { defineConfig } = require("kpack");
const xml = require("rollup-plugin-xml");
module.exports = function({ command, mode }) {
return defineConfig({
plugins: [
{
...xml(),
enforce: 'pre'
}
]
});
};
默认情况下插件在开发 (serve) 和生产 (build) 模式中都会调用。如果插件在服务或构建期间按需使用,请使用 apply 属性指明它们仅在 'build' 或 'serve' 模式时调用:
- rollup-plugin-import 按需打包
- rollup-plugin-require require转import插件
选项 | 功能 |
---|---|
-v, --version | 输出软件版本 |
-i, --info | 输出软件信息 |
-h, --help | 输出命令帮助 |
kpack -h
kpack serve [root]
选项 | 功能 |
---|---|
--host [host] |
限制访问请求头中的HOST |
--port <port> |
监听端口 |
--open [path] |
运行后自动启动浏览器 |
--sourcemap |
是否输出源代码映射文件 |
-c, --config <file> |
配置文件路径 |
--base <path> |
资源路径引用前缀 |
--logLevel <level> |
日志输出等级info /warn /error /silent
|
-m, --mode <mode> |
模式,如development /production /staging
|
-h, --help |
输出帮助 |
kpack build [root]
选项 | 功能 |
---|---|
--outDir <dir> |
输出路径 (默认值: dist) |
--assetsDir <dir> |
输出路径中的资源路径 |
--sourcemap |
是否输出源代码映射文件 |
--minify |
是否压缩输出文件 |
-c, --config <file> |
配置文件路径 |
--base <path> |
资源路径引用前缀 |
--logLevel <level> |
日志输出等级info /warn /error /silent
|
-m, --mode <mode> |
模式,如development /production /staging
|
-h, --help |
输出帮助 |
运行一个服务器来预览已构建的包
kpack preview [root]
选项 | 功能 |
---|---|
--host [host] |
限制访问请求头中的HOST |
--port <port> |
监听端口 |
--open [path] |
运行后自动启动浏览器 |
--sourcemap |
是否输出源代码映射文件 |
-c, --config <file> |
配置文件路径 |
--base <path> |
资源路径引用前缀 |
--logLevel <level> |
日志输出等级info /warn /error /silent
|
-m, --mode <mode> |
模式,如development /production /staging
|
-h, --help |
输出帮助 |
运行 kpack 时, kpack 会自动解析 项目根目录 下名为 kpack.config.js 的文件。配置文件导出一个函数,由函数执行取得配置。
const { defineConfig } = require("kpack");
module.exports = function({ command, mode }) {
return defineConfig({
//这里是配置内容
});
};
这样设计的原因是这个函数会多次执行,会多次打包。
- 类型:
string
- 默认:
process.cwd()
root 项目根目录
- 类型:
string
- 默认:
"/"
在HTML中,引入的js、css等资源的路径前缀。合法的值包括以下几种:
- 绝对 URL 路径名,例如
"/foo/"
- 完整的 URL,例如
"https://bar.com/foo/"
- 相对路径,例如
"./"
- 类型:
Record<string, string>
定义全局常量替换方式。使用这种方式配置的值不会进行语法解析而是直接替换。因此常量值需要用JSON.stringify转译。也可以传入js表达式。
module.exports = function({ command, mode }) {
return {
define: {
__APP_VERSION__: JSON.stringify('v1.0.0'),
__API_BASE__: 'location.origin',
}
};
};
- 类型:
(Plugin | Plugin[])[]
兼容rollup插件和vite插件。Falsy 虚值的插件将被忽略,插件数组将被扁平化(flatten)。
- 类型:
string | false
- 默认:
"public"
作为静态资源服务的文件夹。该目录中的文件在开发期间在 /
处提供,并在构建期间复制到 outDir
的根目录,并且始终按原样提供或复制而无需进行转换。该值可以是文件系统的绝对路径,也可以是相对于项目根目录的相对路径。
将 publicDir
设定为 false
可以关闭此项功能。
- 类型:
Record<string, string> | Array<{ find: string | RegExp, replacement: string, customResolver?: ResolverFunction | ResolverObject }>
将会被传递到 @rollup/plugin-alias
作为 entries 的选项。也可以是一个对象,或一个 { find, replacement, customResolver }
的数组。
- 类型:
string[]
此选项强制将node包解析为项目根目录所依赖的node包。
- 类型:
string[]
- 默认:
['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx']
导入时想要省略的扩展名列表。注意,不 建议忽略自定义导入类型的扩展名(例如:.vue),因为它会影响 IDE 和类型支持。
配置 CSS modules 的行为。选项将被传递给 postcss-modules。
内联的 PostCSS 配置(格式同 postcss.config.js)。
对内联的 POSTCSS 配置,它期望接收与 postcss.config.js 一致的格式。但对于 plugins 属性有些特别,只接收使用 数组格式。
搜索是使用 postcss-load-config 完成的,只有被支持的文件名才会被加载。
指定传递给 CSS 预处理器的选项。文件扩展名用作选项的键。参考配置:
module.exports = function({ command, mode }) {
return {
css: {
preprocessorOptions: {
scss: {
additionalData: `$theme-primary: orange;`,
importer: require('node-sass-tilde-importer'),
},
sass: {
additionalData: `$theme-primary: orange;`,
importer: require('node-sass-tilde-importer'),
},
}
}
};
};
- 类型:
string | RegExp | (string | RegExp)[]
- 相关内容: 静态资源路径引入
- 类型:
'info' | 'warn' | 'error' | 'silent'
调整控制台输出的级别,默认为 'info'
。
- 类型:
string
- 默认: root
用于加载
.env
文件的目录。可以是一个绝对路径,也可以是相对于项目根的路径。
关于环境文件的更多信息,请参见 这里。
- 类型:
string | string[]
- 默认:
["APP_","VUE_APP_","REACT_APP_"]
以 envPrefix
开头的环境变量会通过 import.meta.env
暴露在你的客户端源码中。
- 类型:
string
- 默认:
undefined
指定服务器应该监听哪个 IP 地址。 如果将此设置为 undefined
将监听所有地址,包括局域网和公网地址。
也可以通过 CLI 使用 --host foo.bar.com。
- 类型:
number
- 默认值:
5173
指定开发服务器端口。注意:如果端口已经被使用,kpack 不会自动尝试下一个可用的端口,所以这需要保证端口没有被占用。
- 类型: boolean
- 开发服务器启动时,自动在浏览器中打开应用程序。
- 类型:
boolean
- 默认值:
"/"
或 base
服务器二级目录,必须以"/"开头以"/"结尾。如果base的配置以"/"开头以"/"结尾,那么默认值为base。
module.exports = function({ command, mode }) {
return {
base: "./",
server: {
contextPath: "/my-app/"
}
};
};
- 类型:
Record<string, string | ProxyOptions>
为开发服务器配置自定义代理规则。期望接收一个 { key: options }
对象。任何请求路径以 key 值开头的请求将被代理到对应的目标。如果 key 值以 ^
开头,将被识别为 RegExp
。configure
选项可用于访问 proxy 实例。
其他配置项请参考 http-proxy。
在某些情况下,你可能也想要配置底层的开发服务器。(例如添加自定义的中间件到内部的 connect 应用中)为了实现这一点,你需要编写你自己的插件并使用 configureServer
函数。
- 类型:
string
- 默认:
dist
指定输出路径(相对于项目根目录)。
- 类型:
string
- 默认:
assets
指定生成静态资源的存放路径(相对于 build.outDir)。
- 类型:
boolean
- 默认:
false
构建后是否生成 source map 文件。如果为 true,将会创建一个独立的 source map 文件。
- 类型:
boolean
- 默认:
false
是否开启代码混淆。
- 类型:
boolean
- 默认:
true
启用/禁用 gzip 压缩大小报告。压缩大型输出文件可能会很慢,因此禁用该功能可能会提高大型项目的构建性能。
- 类型:
number
- 默认:
500
启用/禁用 gzip 压缩大小报告。压缩大型输出文件可能会很慢,因此禁用该功能可能会提高大型项目的构建性能。
规定触发警告的 chunk 大小。(以 kB 为单位)。它将与未压缩的 chunk 大小进行比较,因为 JavaScript 大小本身与执行时间相关。
预览选项与开发服务器选项相同,请参考开发服务器选项配置。如果未配置,默认值取开发服务器选项中配置的值。
参考示例
module.exports = function({ command, mode }) {
return {
preview: {
host: "localhost",
port: 4173, // 默认值
contextPath: "/my-app/"
}
};
};
在vite中有许多特性是直接字符串替换而非语法解析的,使用kpack总是进行语法解析。
const foo = 'I like import.meta.env.MODE';
.foo::after{
content: "url(./bar.jpg)";
}
以上转化结果不一致。