Vue3混合组件库搭建 实战篇

2022.09.10
评论

亦有人云,直接开导

写最前面

此文章为 使用Vite和TypeScript带你从零打造一个属于自己的Vue3组件库 实践后的总结

实践本文章前,请注意时间;如果间隔太久,请关注关键依赖的版本情况,如 vite、gulp、vuepress 等;版本相差太大会导致意想不到的结果,参照物请访问 v3-component-library-frame

一.项目结构搭建

安装pnpm

我们要搭建一个 monorepo 项目,所以我们采用了对其支持性较好的 pnpm(注意,一旦确定了项目包管理工具,就不要改变;更不要和其他工具混用,如yarn等,这会导致库崩溃)

全局安装

npm install pnpm -g

项目初始化

创建文件夹,名称为自己定(想好了定,后期改名字会破坏依赖结构),方便演示,我这里就叫 sora-frame,然后在此文件夹下打开终端并执行:

pnpm init

根目录下,分别创建packagesexamplessite 文件夹(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 后缀文件的类型定义


二.组件库项目搭建

  1. 创建文件夹,名称为组件库名称;为方便演示,我这里就叫 sora(这个名字决定好后,先去npm (npmjs.com)搜索一下,搜索出结果就换名字,因为会导致后续的 组织 创建失败);然后在此文件夹下执行 pnpm init 初始化项目
  2. 执行 pnpm install vite @vitejs/plugin-vue -D -w,安装项目全局运行环境
  3. sora下分别创建vite.config.ts

接下来,我们先解决两个问题,ts 与 less 文件的打包

Less转义与打包

思路,先将组件打包;然后通过gulp的文件操作,获取打包后文件中需要处理的less文件,将其转为css,并覆盖源文件

首先,安装一大堆依赖:

pnpm install gulp gulp-autoprefixer gulp-less gulp-rimraf -D -w
pnpm install @types/gulp @types/gulp-autoprefixer @types/gulp-less -D -w

gulp 自动化工具;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发布

  1. 访问 npm官网,自行创建账号
  2. 跳转 npm-New Organization 页面,输入名字并创建组织(以前文为例,名字为 sora)
  3. 回到项目根目录,执行组件打本命令 sora:build;等待打包成功后,来到 sora文件夹下,打开终端并执行 pnpm publish --no-git-checks ,上传成功后就大功告成了(如果失败,建议细读控制台的输出,有可能是项目名字已经存在一类的);组件库依赖的上传方法相同;注意,上传时需要手动修改对应项目下package.jsonversion值,不能上传同版本内容。
  4. 自己的项目中使用。执行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