亦有人云,直接开导
写最前面
此文章为 使用Vite和TypeScript带你从零打造一个属于自己的Vue3组件库 实践后的总结
实践本文章前,请注意时间;如果间隔太久,请关注关键依赖的版本情况,如 vite、gulp、vuepress 等;版本相差太大会导致意想不到的结果,参照物请访问 v3-component-library-frame
一.项目结构搭建
安装pnpm
我们要搭建一个 monorepo 项目,所以我们采用了对其支持性较好的 pnpm(注意,一旦确定了项目包管理工具,就不要改变;更不要和其他工具混用,如yarn等,这会导致库崩溃)
全局安装
npm install pnpm -g项目初始化
创建文件夹,名称为自己定(想好了定,后期改名字会破坏依赖结构),方便演示,我这里就叫 sora-frame,然后在此文件夹下打开终端并执行:
pnpm init根目录下,分别创建packages、examples、site 文件夹(packages为需要打包发布的项目文件夹,examples是用于本地测试的文件夹,site 则为文档库文件夹)
根目录下,创建pnpm运行时文件 .npmrc,并写入:
shamefully-hoist = true
此行为是为了创建一个扁平 node_modules 目录结构,方便我们子项目能使用主项目依赖;具体解释查看 pnpm-shamefully-hoist
根目录下,创建 pnpm-workspace.yaml,并写入:
packages:
- "packages/**"
- "examples"
- "site"
此行为实现了 monorepo ,让此项目 packages、examples、site 文件夹下的项目进行关联
依赖安装
vue3 无可厚非,ts 懂得都懂;less 嘛,我自己常用的,当然你也可以选择 scss ,但后面的部分 gulp 自动化会有一定的影响,需要自己去了解配置
pnpm i vue typescript less sucrase -D -w
注意,-D为运行时依赖,-w为项目主依赖;sucrase 是新一代的babel类工具
然后就是配置 ts 运行环境;根目录下创建 tsconfig.json文件,具体内容看个人习惯,举例如下:
{
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve",
"strict": true,
"target": "ES2015",
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"moduleResolution": "Node",
"lib": ["esnext", "dom"]
}
}根目录下,创建vue-shim.d.ts文件,并写入:
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
}这是对 vue 后缀文件的类型定义
二.组件库项目搭建
- 创建文件夹,名称为组件库名称;为方便演示,我这里就叫
sora(这个名字决定好后,先去npm (npmjs.com)搜索一下,搜索出结果就换名字,因为会导致后续的 组织 创建失败);然后在此文件夹下执行pnpm init初始化项目 - 执行
pnpm install vite @vitejs/plugin-vue -D -w,安装项目全局运行环境 - 在
sora下分别创建vite.config.ts
接下来,我们先解决两个问题,ts 与 less 文件的打包
Less转义与打包
思路,先将组件打包;然后通过gulp的文件操作,获取打包后文件中需要处理的less文件,将其转为css,并覆盖源文件
首先,安装一大堆依赖:
pnpm install gulp gulp-autoprefixer gulp-less gulp-rimraf -D -wpnpm install @types/gulp @types/gulp-autoprefixer @types/gulp-less -D -wgulp 自动化工具;gulp-less less转css;gulp-autoprefixer 兼容性级自动前缀,如css名称;gulp-rimraf 文件删除工具;@types/** 为一些类型依赖
然后,就是实现,在sora下创建script文件夹,并在其下创建gulpfile.ts,此时的文件结构如下:
-- sora-frame
-- packages
-- sora
-- script
-- gulpfile.ts
gulpfile.ts内容如下(为了方便演示,并没有进行代码抽离,可看个人需求拆分)
import { spawn } from "child_process";
import { dest, parallel, series, src } from "gulp";
import autoprefixer from "gulp-autoprefixer";
import less from "gulp-less";
import { resolve } from "path";
const rimraf = require("rimraf"); // rimraf没有对应的ts包,所以用了require导入
const componentPath = resolve(__dirname, "../");
/**
* 终端指令
*/
const run = (command: string, path: string) => {
//cmd表示命令,args代表参数,如 rm -rf rm就是命令,-rf就为参数
const [cmd, ...args] = command.split(" ");
return new Promise((resolve, _) => {
const app = spawn(cmd, args, {
cwd: path, //执行命令的路径
stdio: "inherit", //输出共享给父进程
shell: true, //mac不需要开启,windows下git base需要开启支持
});
//执行完毕关闭并resolve
app.on("close", resolve);
});
};
/**
* less 编译
*/
const lessTranspile = () => {
return src(`${componentPath}/src/**/style/**.less`) // 匹配项目指定目录结构下less文件
.pipe(less()) // less转css
.pipe(autoprefixer()) // css兼容前缀补充
.pipe(dest(`${componentPath}/dist/lib/src`)) // 放入lib包
.pipe(dest(`${componentPath}/dist/es/src`)); // 放入es包
};
/**
* 在sora下的终端执行 pnpm run build
*/
export const componentTranspile = async () => {
return run('pnpm run build', componentPath)
}
// series创建一个同步执行队列,parallel创建一个并发队列
export default series(
// 同步执行删除
async (e) =>
new Promise((onFull) => rimraf(`${componentPath}/dist`, e, onFull)),
/**
* 并发执行打包
* lessTranspile与componentTranspile构建出来的结果目录是一致的,此时两个结果会合并
*/
parallel(
async () => lessTranspile(),
async () => componentTranspile()
)
);Ts的转义与打包
思路,用了vite的一个插件。后面在打包时再说怎么用;具体文档查看 vite-plugin-dts
pnpm i vite-plugin-dts -D -w组件创建
接下来的操作根目录以sora为准
根目录下,创建src文件夹,用于存放具体的组件文件,具体结构如下:
-- sora
-- src
-- 组件名称
-- index.ts 组件导出入口
-- 组件名.vue/组件名.ts/组件名.tsx
-- type.ts 类型文件
-- README.md 组件使用文档
-- style 样式文件夹
-- 样式名.less/样式名.css
注意,由我们上面实现的 Less转义打包程序 中,生成了以下文件定义规则:存放样式的文件夹名称应该为 style,组件中应该在script中通过相对路径导入样式,切勿使用style标签
注意,对应ts类型来说,为了统一性(setup、ts、vue等导出类型的操作差异),我们需要将其抽离到type.ts
为了方便演示,我们这里假设以上述结构创建了一个 Button组件,然后说一说导出
在src文件夹下创建index.ts文件,作为所有组件的导出文件,具体如下:
// 举个栗子,具体看个人需要,只要把需要导出的 组件或类型 导出去就行了
export { default as SrButton } from "./button";
export type { SrButtonType } from "./button/type";在sora文件夹下,创建index.ts文件,作为组件库项目的出口,具体如下:
// 同样的,按自己需求导出,但不要在此处写过多代码,这只是一个出口
export * from './src'组件库打包
定位到sora/vite.config.ts,并写入:
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
export default defineConfig({
build: {
target: 'modules',
outDir: 'es',
rollupOptions: {
// 排除不需要打包的内容
external: ['vue', /\.less/],
// 打包输入的入口文件
input: ['index.ts'],
// 打包输出为cjs(CommonJS)和esm(ESModule)两种形式
output: [
{
format: 'es',
entryFileNames: '[name].js',
dir: resolve(__dirname, './dist/es'),
preserveModules: true
},
{
format: 'cjs',
entryFileNames: '[name].js',
dir: resolve(__dirname, './dist/lib'),
preserveModules: true
}
]
},
lib: {
entry: './index.ts',
name: 'sora'
}
},
plugins: [
vue(),
// ts类型处理
dts({
entryRoot: 'src',
outputDir: [
resolve(__dirname, './dist/es/src'),
resolve(__dirname, './dist/lib/src')
],
// 这里需要引入我们项目根目录的ts配置,当然你也可以自己再下一个配置
tsConfigFilePath: '../../tsconfig.json'
}),
/**
* 自定义插件
* 通过读取已打包文件,将组件中样式的导入.less字符串替换为.css;配合前面写的gulp工具
*/
{
name: 'lessSuffixReplace',
generateBundle(_, bundle) {
const keys = Object.keys(bundle)
for (const key of keys) {
const bundler: any = bundle[key]
this.emitFile({
type: 'asset',
fileName: key,
source: bundler.code.replace(/\.less/g, '.css')
})
}
}
}
]
})定位到sora/package.json,并修改如下:
{
"name": "sora",
"version": "1.0.0",
"main": "./dist/lib/index.js",
"module": "./dist/es/index.js",
"files": [
"dist"
],
"description": "",
"scripts": {
"build": "vite build"
},
"author": "",
"license": "MIT",
"typings": "./dist/es/index.d.ts"
}- main和module 分别指此库在使用时的CommonJS和ESModule入口文件,不建议修改
- files 指npm发布时上传的文件夹
- scripts 终端命令
- typings 打包后的类型路径
定位到sora-frame/package.json,并新增一条script:
"scripts": {
"sora:build": "gulp -f packages/sora/script/gulpfile.ts"
}执行流程:入口触发sora下的gulp命令,gulp命令中执行sora的vite打包命令
然后在sora-frame下打开终端,执行 pnpm run sora:build,执行成功后,会在sora文件夹下生成dist文件夹,内容结构如下:
-- dist
-- lib
-- _virtual
-- src
-- index.d.ts
-- index.js
-- es
-- _virtual
-- src
-- index.d.ts
-- index.js
如果文件结构不同,请检查以往配置是否出错,或检查版本是否相差过大
三.组件依赖项目创建
此操作为可选;合理的抽离能更好的优化项目,同样也带来了一定的心智负担;为了实战,这里推荐还是过一遍
举个例子
packages创建依赖项目;举个例子,其下创建utils文件夹,并执行pnpm init初始化项目
在utils文件夹下创建src内容文件夹、index.ts依赖项目出口、vite.config.ts;并在src文件夹下创建index.ts文件,作为src下内容的出口,和组件库导出流程类似。项目结构如下:
-- utils
-- src
-- index.ts
-- index.ts
-- package.json
-- vite.config.ts
修改package.json
{
"name": "@sora/utils", // 注意,请使用npm组织名称
"version": "1.0.0",
"main": "./dist/lib/index.js",
"module": "./dist/es/index.js",
"files": [
"dist"
],
"license": "MIT",
"scripts": {
"build": "vite build"
}
}修改vite.config.ts
import { resolve } from 'path'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
export default defineConfig({
build: {
target: 'modules',
outDir: 'es',
minify: true,
rollupOptions: {
input: ['src/index.ts'],
output: [
{
format: 'es',
entryFileNames: '[name].js',
preserveModules: true,
dir: resolve(__dirname, './dist/es')
},
{
format: 'cjs',
entryFileNames: '[name].js',
preserveModules: true,
dir: resolve(__dirname, './dist/lib')
}
]
},
lib: {
entry: './index.ts',
name: 'sora-utils'
}
},
plugins: [
dts({
outputDir: [
resolve(__dirname, './dist/es'),
resolve(__dirname, './dist/lib')
],
tsConfigFilePath: '../../tsconfig.json'
})
]
})然后在utils下打开终端,执行 pnpm run build,执行成功后,会在utils文件夹下生成dist文件夹,内容结构如下:
-- dist
-- lib
-- src
-- index.d.ts
-- index.js
-- es
-- src
-- index.d.ts
-- index.js
如果文件结构不同,请检查以往配置是否出错,或检查版本是否相差过大
组件库中使用依赖库utils
定位到组件库sora文件夹下,打开终端并执行pnpm i @sora/utils (注意,需要@sora/utils包至少执行过一次build操作;因为我们在使用依赖时,肯定是使用的其依赖package.json中指向的入口,而不是他的源文件),成功后观察sora下的package.json,会放下依赖新增了一条内容,如下:
"dependencies": {
"@sora/utils": "workspace:^1.0.0"
}然后将版本号给为任意版本(这是为了避免其他包更新时的频繁更改 与 包管理工具的依赖版本自动生成)
"dependencies": {
"@sora/utils": "workspace:*"
}依赖版本号前面为workspace,则代表引用的为本地依赖;完成了这一步结构连接,在每一次utils项目执行build成功后,都会自动更新引用了utils的包,比如这里的sora组件库;
然后是使用,比较简单,导入就行了:
// sora/src/button/button.vue - script
import { test } from "@sora/utils";实践此操作时,建议执行一次sora的打包命令,方便判断是否存在问题
四.本地测试项目搭建
回到sora-frame项目根目录,进入examples文件夹,并执行pnpm init初始化项目
创建app.vue,里面随便写个vue模板就行了
创建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>Document</title>
</head>
<body>
<div id="app"></div>
<script src="main.ts" type="module"></script>
</body>
</html>创建main.ts,并写入:
import { createApp } from 'vue'
import App from './app.vue'
const app = createApp(App)
app.mount('#app')创建vite.config.ts,并写入:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 2333
}
})然后就像sora安装utils中的操作相同
五.NPM发布
- 访问 npm官网,自行创建账号
- 跳转 npm-New Organization 页面,输入名字并创建组织(以前文为例,名字为 sora)
- 回到项目根目录,执行组件打本命令
sora:build;等待打包成功后,来到sora文件夹下,打开终端并执行pnpm publish --no-git-checks,上传成功后就大功告成了(如果失败,建议细读控制台的输出,有可能是项目名字已经存在一类的);组件库依赖的上传方法相同;注意,上传时需要手动修改对应项目下package.json的version值,不能上传同版本内容。 - 自己的项目中使用。执行
pnpm i sora(其他包管理工具同理),然后就是开箱即用,就像examples项目中一样
六.文档库搭建
目前仅实现了基础功能,并不优雅
接下来的操作根目录以site为准
根目录下,打开终端并执行pnpm init,并执行下列依赖安装命令:
pnpm i @vuepress/plugin-git @vuepress/plugin-register-components @vuepress/plugin-search vuepress@next -D
都是vuepress的依赖,plugin-search是一个搜索框工具,plugin-register-components是组件自动导入工具;
定位到site/package.json,并修改script:
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}执行流程:dev文档运行,build文档打包
说几个vuepress2常见的问题
如何在md文件中使用vue组件
site/docs/.vuepress/components下的组件会被全局注册,当然你得在config.ts中进行配置,详情见 register-components
在md文件中使用的vue组件,怎么使用组件插槽
vuepress2已经移除了Markdown 插槽,取而代之的是vue组件的原生写法,如下:
`基础用法`
<CodeShow>
<template #source>
<list-enter-transition-1 />
</template>
<template #meta>
@[code vue:no-line-numbers](../../.vuepress/components/list-enter-transition-1.vue)
</template>
</CodeShow>注意,上面代码块是一个md文件中的内容;一般来说插槽内部会被解析(如source插槽内部,会被解析为组件),如果想以md原样渲染,就需要在插槽内部前后各空出一行(如meta插槽内部,被导入的组件被解析为md代码块)
为什么选择vuepress2,而不是vuepress1或vitepress
因为vuepress1不支持vue3,而vitepress又处于beta阶段,他自己都写明了目前,不推荐将其用于任何正式的场景。
你就讲这些,我这文档库就搭建完了?
当然没有,vuepress2大部分都是配置上的知识,而且其本身的文档就比较优秀;与其让我搬运,不如自己去啃一遍文档 vuepress2
