如果你也喜欢这种组件库打包方式希望这篇文章可以帮到你
如果你觉得麻烦也可以直接下载我已经配置完的组件库模板项目直接开始组件库开发
搭建基础过程参考大佬文章使用 Vite 和 TypeScript 带你从零打造一个属于自己的 Vue3 组件库
按大佬文章一直搭建到打包前都没有问题,但打包的步骤遇到了很多的问题
当走到vite打包步骤添加了声明文件后发现打包后的es和lib文件夹的的文件结构非常的奇怪,
为什么说奇怪呢,拿来和右侧两图的Vant的打包结构比较就可以看出:
1.es和lib目录下只有 index.js 文件而index.d.ts文件被打包到了src下
2.button组件的js和ts分别打包到了button目录和src/button目录下
这样对比这看就非常的奇怪了。
经过多次尝试发现了解决办法
1.首先我们让index.js和index.d.ts打包在同一个文件夹下,这个其实很简单只要把vite.config.ts配置文件里的 preserveModulesRoot:'src' 修改成 preserveModulesRoot:'' 就是删掉这里的src
改完后的vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"
import dts from 'vite-plugin-dts'
export default defineConfig(
{
build: {
target: 'modules',
//打包文件目录
outDir: "es",
//压缩
minify: false,
//css分离
//cssCodeSplit: true,
rollupOptions: {
//忽略打包vue文件
external: ['vue'],
input: ['src/index.ts'],
output: [
{
format: 'es',
//不用打包成.es.js,这里我们想把它打包成.js
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'es',
preserveModulesRoot: ''
},
{
format: 'cjs',
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'lib',
preserveModulesRoot: ''
}
]
},
lib: {
entry: './src/index.ts',
formats: ['es', 'cjs']
}
},
plugins: [
vue(),
dts({
//指定使用的tsconfig.json为我们整个项目根目录下掉,如果不配置,你也可以在components下新建tsconfig.json
}),
//因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个
dts({
outDir: 'lib',
})
]
}
)
改完后的打包目录结构
这样我们的js文件和.d.ts文件就打包到一起了
接下来我们有会发现,原来打包到es目录下的的index.js被打包到了src目录下,这可就不太行了,这个我也不知道应该怎么弄,但是src目录看起来其实也没有太大的意义,因为这是组件库各个组件都是通过index入口文件到出的,而且Vant的文件目录里也没有src目录,索性我就去掉了src目录,vite.config.ts文件也需要改动去掉input和entry中的src
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"
import dts from 'vite-plugin-dts'
export default defineConfig(
{
build: {
target: 'modules',
//打包文件目录
outDir: "es",
//压缩
minify: false,
//css分离
//cssCodeSplit: true,
rollupOptions: {
//忽略打包vue文件
external: ['vue'],
input: ['index.ts'],
output: [
{
format: 'es',
//不用打包成.es.js,这里我们想把它打包成.js
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'es',
preserveModulesRoot: ''
},
{
format: 'cjs',
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'lib',
preserveModulesRoot: ''
}
]
},
lib: {
entry: './index.ts',
formats: ['es', 'cjs']
}
},
plugins: [
vue(),
dts({
//指定使用的tsconfig.json为我们整个项目根目录下掉,如果不配置,你也可以在components下新建tsconfig.json
}),
//因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个
dts({
outDir: 'lib',
})
]
}
)
去掉后的打包目录就和Vant一模一样了
大佬文章中对WithInstall方法的公共化也没有做相关介绍,于是我从Vant里把这个方法弄了出来
formate.ts
const camelizeRE = /-(\w)/g;
export const camelize = (str: string): string =>
str.replace(camelizeRE, (_, c) => c.toUpperCase());
wthInstall.ts
import type { App, Component } from 'vue';
import { camelize } from './formate';
type EventShim = {
new(...args: any[]): {
$props: {
onClick?: (...args: any[]) => void;
};
};
};
export type WithInstall<T> = T & {
install(app: App): void;
} & EventShim;
export function withInstall<T extends Component>(options: T) {
(options as Record<string, unknown>).install = (app: App) => {
const { name } = options;
if (name) {
app.component(name, options);
app.component(camelize(`-${name}`), options);
}
};
return options as WithInstall<T>;
}
index.ts
export * from './formate'
export * from './wthInstall'
组件index.ts中使用
import { withInstall } from '../utils'
import _Button from './button.vue'
export const Button = withInstall(_Button)
export default Button
因为我们打包在了组件项目文件夹中,我们要上传的话要过滤掉原始文件,刚开始还好说,但是如果我们后面每一个组件都要加过滤就太蠢了,这个时候我们就可以把vite.config.ts配置文件里的出口配置调整一下加上一层 dist ,注意dts的出口文件需要同时更改保持一致
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"
import dts from 'vite-plugin-dts'
export default defineConfig(
{
build: {
target: 'modules',
//打包文件目录
outDir: "es",
//压缩
minify: false,
//css分离
//cssCodeSplit: true,
rollupOptions: {
//忽略打包vue文件
external: ['vue'],
input: ['index.ts'],
output: [
{
format: 'es',
//不用打包成.es.js,这里我们想把它打包成.js
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'dist/es',
preserveModulesRoot: ''
},
{
format: 'cjs',
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'dist/lib',
preserveModulesRoot: ''
}
]
},
lib: {
entry: './index.ts',
formats: ['es', 'cjs']
}
},
plugins: [
vue(),
dts({
//指定使用的tsconfig.json为我们整个项目根目录下掉,如果不配置,你也可以在components下新建tsconfig.json
outDir: 'dist/es',
}),
//因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个
dts({
outDir: 'dist/lib',
})
]
}
)
然后再项目文件夹中新建dist文件夹并复制一份package.json文件进去,在package,json文件不变的情况下每次发布只需要修改dist文件夹下的package,json文件的版本号就好了
关于组件发布到npm的过程可参考大佬文章npm 上传发布自定义组件以及使用详细流程(Vue包含)
--2023/11/17增加
在后续过程中我并未采用原文作者的css打包方式,因为我开发方式选择了SFC的方式所以css在.vue文件里,打包时会被分离出来集中到dist/style,css文件中,打开vite配置的css分离后(cssCodeSplit: true),组件css文件被打包在了dist目录下,而且文件名称非常的长(button.vue_vue_type_style_index_0_lang.css),这样的css文件的位置不但不在组件目录下,很长的文件名使用起来也很麻烦,经过反复查找各种资料,发现可以利用rollupOptions.output.assetFileNames的函数方式达到将组件css和组件打包在一起的目的。
assetFileNames: (assetInfo) => {
//判断是静态资源且是css
if(assetInfo.type === 'asset' && /\.(css)$/i.test(assetInfo.name || "")){
//截取出css打包名称中的组件名 button.vue_vue_type_style_index_0_lang.css 截出button
let str = (assetInfo.name || 'style.').split('.')[0]
//将组件css输出到组件目录下 组件/组件.css
return `${str}/${str}.css`
}else{
//其他静态资源则返回默认的 后续也可以对其他资源文件细分
return 'assets/[name].[ext]'
}
}
我们将css打包出来后接下来要做的工作就是让导入组件的时候能自动导入样式,不管使用的时候是全部导入还是单独导入都能自动导入样式,按照我们平常使用组件的经验,导入样式时直接import "./button.css"; 就可以了,既然这样那我们就只需要在组件的入口文件也就是导出后的index.js文件的顶部加上对应的代码就可以了。在多番查找后并没有发现官方文档中有追加代码方法,搜索出来的插件也不太适合我们现有的模式。不过办法总比困难多,在看了多篇插件作者的文章后发现他们插件中使用的renderChunk钩子函数可以往生成的js文件中追加自己想要的代码,那咱就直接举一反三,直接在vite.config.ts文件的plugins直接写一个自己的插件代码,观察大佬的插件代码发现vue(),dts(),这种形式的插件调用都是返回一个对象,那我们就直接在末尾追加一个对象代码如下
{
name: 'vite:import-css',
apply: 'build',
enforce: 'post',
renderChunk(code, chunk) {
// 判断是不是组件入口js
if(!chunk.isEntry && chunk.type==='chunk' && /\index.(js)$/i.test(chunk.fileName)){
// 截取出组件名称
let str = chunk.fileName.split('/')[0]
return `import './${str}.css';\n${code}`
}
},
}
加上后运行 pnpm run build 就会发现es和lib里的组件文件夹下的indx.ts的头部就加上了import "./组件名.css";这行代码,在examples测试也能正常显示样式了(package重新打包后examples也要重新运行才能显示最新的)
需要注意的是,CSS 文件的导入是具有副作用的,我们还需要在库的 package.json 文件中声明 sideEffects 字段,防止用户侧构建时 CSS 文件被意外移除,为了防止万一我们也要加上
"sideEffects": [
"**/*.css"
]
到这里我们的整个组件库就算搭建完成了,完整的vite.config.ts文件如下
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import dts from 'vite-plugin-dts';
export default defineConfig(
{
build: {
target: 'modules',
//打包文件目录
outDir: "es",
//静态文件目录
assetsDir: 'css',
//压缩
minify: false,
//css分离
cssCodeSplit: true,
rollupOptions: {
//忽略打包vue文件
external: ['vue'],
input: ['index.ts'],
output: [
{
format: 'es',
//不用打包成.es.js,这里我们想把它打包成.js
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'dist/es',
assetFileNames: (assetInfo) => {
//判断是静态资源且是css
if (assetInfo.type === 'asset' && /\.(css)$/i.test(assetInfo.name || "")) {
//截取出css打包名称中的组件名 button.vue_vue_type_style_index_0_lang.css 截出button
let str = (assetInfo.name || 'style.').split('.')[0]
//将组件css输出到组件目录下 组件/组件.css
return `${str}/${str}.css`
} else {
//其他静态资源则返回默认的 后续也可以对其他资源文件细分
return 'assets/[name].[ext]'
}
},
preserveModulesRoot: ''
},
{
format: 'cjs',
entryFileNames: '[name].js',
//让打包目录和我们目录对应
preserveModules: true,
//配置打包根目录
dir: 'dist/lib',
assetFileNames: (assetInfo) => {
//判断是静态资源且是css
if (assetInfo.type === 'asset' && /\.(css)$/i.test(assetInfo.name || "")) {
//截取出css打包名称中的组件名 button.vue_vue_type_style_index_0_lang.css 截出button
let str = (assetInfo.name || 'style.').split('.')[0]
//将组件css输出到组件目录下 组件/组件.css
return `${str}/${str}.css`
} else {
//其他静态资源则返回默认的 后续也可以对其他资源文件细分
return 'assets/[name].[ext]'
}
},
preserveModulesRoot: ''
},
]
},
lib: {
entry: './index.ts',
formats: ['es', 'cjs']
}
},
plugins: [
vue(),
dts({
//指定使用的tsconfig.json为我们整个项目根目录下掉,如果不配置,你也可以在components下新建tsconfig.json
outDir: 'dist/es',
}),
//因为这个插件默认打包到es下,我们想让lib目录下也生成声明文件需要再配置一个
dts({
outDir: 'dist/lib',
}),
{
name: 'vite:import-css',
apply: 'build',
enforce: 'post',
renderChunk(code, chunk) {
// 判断是不是组件入口js
if(!chunk.isEntry && chunk.type==='chunk' && /\index.(js)$/i.test(chunk.fileName)){
// 截取出组件名称
let str = chunk.fileName.split('/')[0]
return `import './${str}.css';\n${code}`
}
},
}
]
}
)