手把手系列:打造企业级vue-components

993 阅读6分钟

前言

在《手把手系列:打造企业级vue-hooks》已经介绍了开发hooks库的方法。本文将基于上文的vue-hooks架构做延伸,补充组件库建设知识,如果还没阅读上篇文章,建议先阅读。

本文会把焦点放在构建脚本设计上,因为这是组件库最复杂部分,如果你有看过element-plus或者vant-ui的构建脚本,会发现它需要引入大量插件和工具实现构建,但也许你是不需要这么复杂的构建流程的。

因此,为了弄清楚哪些构建流程是必要的,我们先从最简单的构建脚本开始,一步步增加功能。

设计构建脚本

下面由简入深分析构建脚本的设计,建议结合源码阅读

源码地址:github.com/zhengguoron…

项目准备

我们先把上节课的vue-hooks项目拷贝一份重命名为vue-components并修改package.jsonname属性改为@xboss/components

从最简单的开始

以往vue组件一般基于SFC单文件的形式开发,也就是把templatescriptcss放在同一个.vue文件,但是这种方式需要构建工具支持。

现在我们希望不依赖构建工具,用纯js实现组件库开发,那可以利用vue提供了一个叫渲染函数的方法编写组件,因为渲染函数本身就是以js的方式直接创建虚拟DOM,所以不需要多余的构建过程。

下面是用render函数编写的组件代码

// src/toggle-button/index.ts
import { defineComponent, h, ref } from 'vue';

const toggleButton = defineComponent({
  setup() {
    const state = ref(false);
    const toggle = () => {
      state.value = !state.value;
    };
    return {
      state,
      toggle
    };
  },
  // 定义渲染函数
  render() {
    // h函数用来创建VNode
    return h('button', { onClick: this.toggle }, this.state);
  }
});

export default toggleButton;

在项目入口以插件的形式导出install方法,那使用者只要执行app.use(组件库名)即可全局注册组件

// src/index.ts
import { App } from 'vue';
import toggleButton from './toggle-button';

const install = (app: App) => {
  // 注册全局组件
  app.component('ToggleButton', toggleButton);
};

// 作为插件导出,暴露install方法
export default {
  install
};

增加JSX/TSX语法支持

如果基于render函数创建复杂布局是非常麻烦的,Vue建议使用更接近模板语法的JSX语法,但是要支持该语法需要Babel插件支持。

另外,由于vscode无法识别jsx语法,会出现下图的波浪线

image.png

VSCode支持JSX语法

# 安装插件
npm i eslint-plugin-vue vue-eslint-parser -D

.eslintrc增加vue3语法检测配置

module.exports = {
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: {
      js: 'espree',
      ts: '@typescript-eslint/parser',
      '<template>': 'espree'
    }
  },
  plugins: ['@typescript-eslint'],
  // extends有顺序关系,规则按从左到右应用,假设把prettier放最左边,他的规则会被后面规则覆盖
  extends: [
    'airbnb-base',
    'plugin:@typescript-eslint/recommended',
    'plugin:vue/vue3-recommended',
    'prettier'
  ],
  rules: {
    'import/prefer-default-export': 'off',
    'import/extensions': 'off',
    'import/no-unresolved': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off'
  }
};

修改.tsconfig.json新增"jsx": "preserve",

{
  "compilerOptions": {
    "jsx": "preserve",
    "declaration": true,
    "declarationDir": "dist/types"
  }
}

还要注意把.ts文件修改为.tsx

Rollup支持JSX

#安装插件
npm i rollup-plugin-esbuild rollup-plugin-vue-jsx-compat -D

构建脚本修改为下面,其中导出文件修改为esm和umd格式,cjs在组件库不适用

// rollup.config.js
import esBuild from 'rollup-plugin-esbuild';
import vueJsx from 'rollup-plugin-vue-jsx-compat';

export default {
  // 入口文件
  input: 'src/index.ts',
  // 分别导出esm和umd
  output: [
    {
      dir: 'dist/esm',
      format: 'esm'
    },
    // umd格式放cdn提供给浏览器直接使用
    {
      dir: 'dist/umd',
      format: 'umd',
      name: 'xboss',
      globals: {
        vue: 'Vue'
      }
    }
  ],
  plugins: [
    vueJsx(),
    esBuild({
      jsxFactory: 'vueJsxCompat',
      tsconfig: 'tsconfig.json'
    })
  ],
  external: ['vue']
};

增加模板语法和sass语法

#安装依赖
npm i rollup-plugin-vue rollup-plugin-postcss node-sass -D

修改rollup.config.js

image.png

调试组件

由于开发过程需要频繁修改代码调试,如果每次都把组件构建发布到npm会过于麻烦。npm提供了本地发布的方法,可以执行npm link命令将组件发布到本地全局仓库,然后在项目执行npm link @xboss/components方法和组件建立关联,当组件更新项目依赖会自动更新。

但是我在调试过程中发现该方法会出现Vue多次引用问题,而且解决起来会比较麻烦,所以我改为vitepress,它除了可以调试组件,还可以构建组件文档。

# 安装vitepress
npm i vitepress -D

接着创建docs目录并创建导航配置,这里要注意的是.vitepress/theme/index.js文件,vitepress在该文件提供vue实例,可利用该实例注册组件。官方文档

// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme';
import xboss from '../../../dist/esm/index';

export default {
  ...DefaultTheme,
  enhanceApp({ app }) {
    app.use(xboss);
  }
};

使用组件方法和在template写法一样 docs/components/toggle-button.md

## 介绍

toggle-button 组件用于切换按钮状态

## 快速入门

<toggle-button></toggle-button>

package.json增加构建命令,执行npm run docs:dev查看效果

"docs:dev": "vitepress dev docs"

按需加载

假如现在项目有三个组件,还是按以前方法注册组件,当应用使用组件库时,无论组件是否被调用,都会被打包到最终应用资源。

import { App } from 'vue';
import tag from './tag';
import toggleButton  from './toggle-button';
import icon from './icon';

const install = (app: App) => {
  // 注册全局组件
  app.component('ToggleButton', toggleButton);
  app.component('Icon', icon);
  app.component('Tag', tag);
};

// 作为插件导出,暴露install方法
export default {
  install
};

下面是我截图vant组件库注册单个组件方法,分别支持use和component方法注册

image.png

为了实现这两种方式注册,我们把toggle-button组件改成下面的导出写法

// src/toggle-button/index.ts
import { App } from 'vue';
import toggleButton from './ToggleButton.vue';

// 支持app.use()方法注册
export const ToggleButton = {
  install: (app: App) => {
    app.component(toggleButton.name, toggleButton);
  }
};

// 支持app.component()注册
export default toggleButton;

另外,由于打包出去的是一个js文件,我们还要在入口处理所有组件的导出

// src/index.ts
import { App } from 'vue';
import tag, { Tag } from './tag';
import toggleButton, { ToggleButton } from './toggle-button';
import icon, { Icon } from './icon';

const components = [tag, toggleButton, icon];
// 全局注册
const install = (app: App) => {
  components.forEach((component) => {
    // 因为导入的是组件,可以用app.component方法注册
    app.component(component.name, component);
  });
};

// 分别导出组件,实现按需加载
export { Tag, ToggleButton, Icon };

export default {
  install
};

现在当在应用导入单个组件,tree shaking会把未使用代码清除。

总结

大家看到最终的构建脚本几十行代码就能满足我们基本需求,但是我在研究组件库构建过程中最麻烦的是技术选型,因为实现同一功能有很多开源方案,而有些方案由于缺乏维护是无法使用的,你要在众多方案中挑选合适的方案,这是颇有挑战。

往期文章

《不要再用vue2的思维写vue3了》

《为什么vue3 watch( )无法监听对象属性》

《手把手系列:打造企业级vue-hooks》

《手把手系列:打造企业级vue-components》