您现在的位置是:首页 >技术交流 >vite + vue3 + storybook + ts 搭建组件库记录网站首页技术交流

vite + vue3 + storybook + ts 搭建组件库记录

JA+ 2024-06-24 06:01:02
简介vite + vue3 + storybook + ts 搭建组件库记录

目标

  • 只按需引入,不依赖babel-import-plugin 插件。
  • 第三方依赖都不打包。
  • 用原生fetch请求数据。
  • 仅支持esmodule。配置package.json type:"module"

搭建

根据storybook 官网文档,需要在已有的项目中运行 

npx storybook@latest init

 也就是事先需要通过vite创建一个项目。

npm create vite@latest

之后再运行storybook 的命令,storybook会自动分析使用的构建工具,安装好需要的依赖,并自动新增文件。

storybook 增加了/src/stories目录,用于存放组件。

默认情况下,/src/stories 中放着 .vue, .stories, .css 文件,由于我希望把组件源代码单独放在一个文件夹下

目录预设

因此在项目根目录 增加 /packages 目录,下面存放以组件名称命名的文件夹。以每个文件夹为单位表示为一个组件。

/src/stories 中.stories 文件引用/packages 目录下的.vue 用于生成storybook演示文档。

对于vite 的普通打包模式下,入口必须为index.html,因此需要使用vite 支持的lib模式打包。

lib模式下,对于图片等静态资源,处理为base64内联到代码中。且不支持配置

base64的问题在于增大js体积,且不利于相同图片的复用。这块待研究。

构建vue组件

直接使用vite build 命令,会使用项目根目录下的vite.config.js 对指定的入口进行构建。构建产物默认输出到dist目录下。

lib模式下,构建产物有

  • index.js
  • style.css

对于一个vue组件来说,这种格式正式我想要的。

而/package 目录下面会有好多组件,因此需要借助vite 的JavsScript API 进行循环

vite JS API 构建 /packages 目录

根据官网描述,使用下面js代码, 可以通过js启动vite 打包。

import { build } from 'vite';
build({
    //... vite config
})

这里的 build() 方法传入的config对象,会和项目下的 vite.config.js 合并

所以将一些公共的配置可以写在 vite.config.js下

vite.config.js

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import dts from 'vite-plugin-dts';

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['vue'], // 排除三方包
    },
  },
  plugins: [
    vue(),
    vueJsx(),
    dts({
      outputDir: './lib',
      entryRoot: './packages',
    }),
  ],
});

 build.js 主要的构建方法

/**
 * 构建一个组件
 * @param {string} compName 组件名称,对应packages目录下的文件夹名称
 */
async function buildAComponent(compName) {
  const entry = path.join(packagesDir, compName, 'index.ts');
  const out = path.join(outDir, compName);
  await build({
    build: {
      outDir: out,
      lib: {
        entry: [entry],
        formats: ['es'],
      },
    },
  });
}

 这个是构建一个组件的方法,因此,我只要通过node 的 fs 模块读取并分析/packages 路径下的文件夹,遍历出来构建即可。

build.js代码      

import { readdirSync, writeFileSync } from 'fs';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import { build } from 'vite';

const __dirname = dirname(fileURLToPath(import.meta.url));
const packagesDir = path.join(__dirname, './packages');
const outDir = path.join(__dirname, './lib');
async function main() {
  const packages = readdirSync(packagesDir);
  let indexFileContent = '';
  const exportNames = [];
  const promise = [];
  for (const folderName of packages) {
    // const stats = lstatSync(path.join(packagesDir, compName));
    // if (stats.isDirectory()) {
    //   // 如果是目录,则构建
    // }
    const firstLetter = folderName[0];
    const isFirstLetterUpperCase = /[A-Z]/.test(firstLetter);
    if (isFirstLetterUpperCase && folderName.indexOf('.ts') === -1) {
      promise.push(buildAComponent(folderName));
      indexFileContent += `import { ${folderName} } from './${folderName}/index.js';
`;
      exportNames.push(folderName);
    }
  }
  await Promise.all(promise);
  indexFileContent += `export { ${exportNames.join(', ')} };`;
  writeFileSync(path.join(outDir, 'index.js'), indexFileContent);
  console.log('created: lib/index.js');

}
main();

/**
 * 构建一个组件
 * @param {string} compName 组件名称,对应packages目录下的文件夹名称
 */
async function buildAComponent(compName) {
  const entry = path.join(packagesDir, compName, 'index.ts');
  const out = path.join(outDir, compName);
  await build({
    build: {
      outDir: out,
      lib: {
        entry: [entry],
        formats: ['es'],
      },
    },
  });
}

 main 函数中判断文件夹名称前面是大写字母开头的才当作组件来处理。

构建结果

  • /package/Button
    • index.ts
    • style.less
    • Button.vue

这一个目录入口为index.ts 构建完成后,输出到lib

  • /lib/Button
    • index.js
    • style.css

至此,主要构建完成了

库入口

由于一个npm库需要有一个默认的入口js,因此需要在/lib 目录下面增加一个index.js 用于导入lib 下面所有组件,并导出。这部分代码在上面 build.js 中已经体现。 

/lib/index.js

export { Button } from './Button/index.ts';

package.json 增加配置

{
  "main": "lib/index.js",
  "module": "lib/index.js",
  "files": [
    "lib",
    "packages"
  ],
  //...
}

d.ts生成

使用vite-plugin-dts 插件

import dts from 'vite-plugin-dts';

// ... 

plugins:[
    // vue(),
    // vueJsx(),
     dts({
      outputDir: './lib',
      entryRoot: './packages',
    }),
]

构建dts速度有点慢 。

这样配置,会在lib/Button/ 下面增加d.ts 文件了。

与库的默认入口文件一样,需要指定默认declare 文件,package.json types 指定为 /packages/main.ts。(这里暂时指定.ts 文件,可能在一些构建工具中不识别,因为那些工具只识别d.ts 文件)

实现自动引入

一个组件默认的构建结果为index.js, style.css

鉴于使用的情况下引入 import { Button } from 'xxx'; 之外还要 import 'xxx/lib/Button/style.css';

使用babel-import-plugin 可以解决按需引入样式问题。

由于我的目标是不使用babel-import-pluing 因此,完成这个效果,只需要在/lib/Button/index.js 文件的最上方引入/lib/Button/style.css即可。

方案

  • 使用node fs读写文件。
  • 借助vite plugin

使用vite plugin 实现

因为不想用node

由于vite基于rollup,根据rollup 官网文档,我们可以在插件的 generateBundle 钩子中在生成产物前操作文件内容。

代码

/**
 * 在产物js上导入css
 */
function () {
  return {
    name: 'auto-import-style',
    generateBundle(options, bundle) {
      bundle['index.js'].code = 'import "./style.css";
' + bundle['index.js'].code;
    },
  };
},

npm上也有类似的插件 vite-plugin-libcss

构建vue组件要点

不能将vue依赖打包进代码中,否则会导致组件无法使用。

打包组件的产物js中,最上方应该为 import {} from 'vue'; 这样的代码。

通过配置vite.config.js 下的rollupOptions.external即可:

build: {
    rollupOptions: {
      external: ['vue'], 
      // output: {
      //   globals: {
      //     vue: 'Vue', // umd需要
      //   },
      // },
    },
  },

 这里注释掉rollupOptions.output.globals 原因是默认构建lib会输入umd 和 es 模块的文件。我构建时会指定 build.lib.formats: ['es'] 就不需要了。

增量构建方案预设

由于现在我看使用vite-plugin-dts 生成d.ts 文件使用的时间过长。先制定一下增量构建的方案。

鉴于在正常开发过程中,始终用master分支作为正式分支。

因此在开发分支中,可以借助git 来对比当前开发分支与master分支的区别

使用node 的child_process 执行命令

git diff HEAD master --stat

可检查当前分支,对比master,哪些文件有变化。

得到控制台输出后,分析产物,确定package.json 中哪些组件有变动。以此仅构建变更过的组件。

vite可能会覆盖lib目录。构建前先保存一版lib目录?还是有官方配置。暂不研究。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。