手把手教你vue组件库共享组件不直接打包进代码

日报写手
• 阅读 2384

1、“共享组件不直接打包进代码”什么意思?

比如你写了一个组件库,里面有ButtonDialogMessageBox3个组件,其中DialogMessageBox组件都引用了Button组件,那么Button组件就是共享组件。当对这3个组件挨个打包完成后去查看DialogMessageBox组件打包完成后的产物代码,会发现DialogMessageBox组件里面都包含了Button组件产物代码,这样一来产物体积就变大了。

那有没有什么办法使得打包完成后的DialogMessageBox组件产物里不包含Button组件产物代码呢?
答案肯定是有的,比如ant-design-vueelement-ui它们打包后的产物代码中就不包含共享组件代码,它们是通过importrequire去加载共享组件。

ant-design-vue打包产物代码:
手把手教你vue组件库共享组件不直接打包进代码
手把手教你vue组件库共享组件不直接打包进代码

2 、怎么实现呢?

这个问题也困扰了我很久,直到我用esbuild去打包我的【vue3 bootstrap图标组件库】时我才解决这个问题。
原理很简单:把共享组件当成外部扩展(Externals)

在打包时通常会把vue设置为外部扩展,那共享组件为甚么不能设置为外部扩展呢?将共享组件设置为外部扩展后webpack或其他打包工具就不会将其打包进产物中,而是以importrequire的形式去加载。

看到这里你应该豁然开朗了吧!接下来的代码你就会写了。

3、代码实现(vue3)

我这里以esbuild打包为例,webpack或vite代码差不多。

首先需要安装2个关键依赖:npm i esbuild esbuild-plugin-vue -D
接下来安装esbuild打包进去依赖:npm i esbuild-plugin-progress -D
安装css处理依赖:npm i postcss esbuild-sass-plugin autoprefixer postcss-preset-env postcss-import -D

目录结构:

-my-project
  +node_modules
  -src
    -components
       yn-button.scss
       YnButton.vue
       YnDialog.vue
       YnMessageBox.vue
    App.vue
    main.js
  build-lib.js
  package.json

yn-button.scss

.yn-button{
  transition: all .3s;
}

YnButton.vue

<template>
<button
  class="yn-button"
  :type="nativeType"
  :disabled="disabled">
  <slot></slot>
</button>
</template>

<script>
export default {
  name: 'YnButton',
  props: {
    nativeType: {
      type: String,
      default: 'button'
    },
    disabled: {
      type: Boolean,
      default: false
    }
  }
};
</script>

<style lang="scss">
@import "yn-button";
</style>

YnDialog.vue

<template>
<dialog
  v-show="visible"
  class="yn-dialog"
  :class="{
    'dialog-opened': visible
  }"
  :data-opened-count="openCount">
  <h1>
    <slot name="title">{{ title }}</slot>
  </h1>
  <div class="dialog-content">
    <slot></slot>
  </div>
  <div class="dialog-footer">
    <yn-button class="dialog-cancel-btn" @click="close">取消</yn-button>
    <yn-button class="dialog-ok-btn" @click="close">确定</yn-button>
  </div>
</dialog>
</template>

<script>
import {
  ref,
  watch
} from 'vue';
// 由于esbuild打包时是以脚本执行目录为根目录,因此这里不以项目路径去引入button组件
import YnButton from 'src/components/YnButton.vue';
let count = 0;
export default {
  name: 'YnDialog',
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    title: {
      type: String,
      default: ''
    }
  },
  components: {
    YnButton
  },
  emits: ['update:visible', 'close'],
  setup (props, ctx) {
    let openCount = ref(count);
    watch(() => props.visible, function (isVisible) {
      count++;
      openCount.value = count;
    });
    return {
      openCount,

      close () {
        ctx.emit('update:visible', false);
        ctx.emit('close');
      }
    };
  }
};
</script>

YnMessageBox.vue

<template>
<div class="yn-message-box">
  <slot></slot>
  <div class="yn-message-box-operate">
    <yn-button>确定</yn-button>
  </div>
</div>
</template>

<script>
import YnButton from 'src/components/YnButton.vue';
export default {
  name: 'YnMessageBox',
  components: {
    YnButton
  }
};
</script>

3.1、常规打包(共享组件打包进产物)

build-lib.js

const path = require('path');

const vue = require('esbuild-plugin-vue').default; // 处理vue组件
const esBuild = require('esbuild');
const progress = require('esbuild-plugin-progress'); // esbuild打包进度条

// scss、css处理
const { sassPlugin } = require('esbuild-sass-plugin');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const postcssPresetEnv = require('postcss-preset-env');
const postcssImport = require('postcss-import');

function build (entryPoint) {
  esBuild.build({
    bundle: true,
    entryPoints: [entryPoint],
    outdir: 'libs',
    external: ['vue'],
    loader: {
      '.ts': 'ts'
    },
    format: 'esm',
    drop: ["console", "debugger"], // 移除console、debugger信息
    treeShaking: true,
    plugins: [
      sassPlugin({
        async transform (source) {
          const { css } = await postcss([
            autoprefixer,
            postcssPresetEnv(),
            postcssImport()
          ]).process(source, { from: undefined });
          return css;
        }
      }),
      vue(),
      progress()
    ]
  });
}

build(path.resolve(__dirname, './src/components/YnButton.vue'));
build(path.resolve(__dirname, './src/components/YnDialog.vue'));
build(path.resolve(__dirname, './src/components/YnMessageBox.vue'));

在控制台执行node ./build-lib.js,在libs目录将会有6个打包产物:
手把手教你vue组件库共享组件不直接打包进代码
有没有发现不对,应该打包出4个产物才对,因为YnDialog、YnMessageBox组件根本没有添加css,但打包后却多出了2个css文件,这就是将共享组件打包进代码的问题之一

再来看下打包后的产物:
YnButton.js
手把手教你vue组件库共享组件不直接打包进代码
YnButton.css
手把手教你vue组件库共享组件不直接打包进代码

YnDialog.js
手把手教你vue组件库共享组件不直接打包进代码
YnDialog.css
手把手教你vue组件库共享组件不直接打包进代码

通过查看YnDialog.js产物的内容,发现里面包含了YnButton组件的代码,显然这不是我们想要的

3.2、分离共享组件代码打包

esbuild有一个external参数可以配置指定的依赖为外部扩展,但它是静态的,只适用于配置一些全局的外部扩展,对于动态的、需要根据条件去判断的外部扩展它是做不到的。
那有什么办法可以做到呢?答案就是:plugin(插件)
我们可以写一个插件来完成

const path = require('path');

const vue = require('esbuild-plugin-vue').default; // 处理vue组件
const esBuild = require('esbuild');
const progress = require('esbuild-plugin-progress'); // esbuild打包进度条

// scss、css处理
const { sassPlugin } = require('esbuild-sass-plugin');
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const postcssPresetEnv = require('postcss-preset-env');
const postcssImport = require('postcss-import');

function build (entryPoint) {
  esBuild.build({
    bundle: true,
    entryPoints: [entryPoint],
    outdir: 'libs',
    external: ['vue'],
    loader: {
      '.ts': 'ts'
    },
    format: 'esm',
    drop: ["console", "debugger"], // 移除console、debugger信息
    treeShaking: true,
    plugins: [
      sassPlugin({
        async transform (source) {
          const { css } = await postcss([
            autoprefixer,
            postcssPresetEnv(),
            postcssImport()
          ]).process(source, { from: undefined });
          return css;
        }
      }),
      { // 自定义设置外部扩展 esbuild插件
        name: 'my-esbuild-plugin',
        setup (build) {
          let filter = /./;
          /*
           onResolve钩子是一个路径解析的钩子,是esbuild去解析路径的时候触发的,
           也就是你执行import ... from ...的时候会调用onResolve钩子
           */
          build.onResolve({ filter: filter }, function (args) {
            // console.log('args', args);
            let modulePath = args.importer;
            let kind = args.kind;
            let path = args.path;
            let fileInComponents = modulePath.includes('/components/');
            // 只有通过导入的,并且在components目录下的、以.vue结尾的文件才能视为外部扩展
            let external = fileInComponents && kind === 'import-statement' && path.endsWith('.vue');

            // 不需要指定为外部扩展的依赖直接return null
            if (!external) {
              return null;
            }
            /*
             将import路径中的'src/components/'部分替换成 './',因为打包后它们都是在同一个文件夹。
             打包后都产物中共享组件将以 import XX from './YY.vue' 形式引入
             */
            // tip: 这部分逻辑需要根据自己项目目录结构来判断是否需要这样做
            if (path.startsWith('src/components/')) {
              path = path.replace('src/components/', './');
              path = path.replace('.vue', '.js');
            }
            return {
              path,
              // 是否需要 external (如果该模块为external,则esbuild构建时不会去加载该模块,而是保留原有的import xxx from 'yyy'代码)
              external
            };
          });
        }
      },
      vue(),
      progress()
    ]
  });
}

build(path.resolve(__dirname, './src/components/YnButton.vue'));
build(path.resolve(__dirname, './src/components/YnDialog.vue'));
build(path.resolve(__dirname, './src/components/YnMessageBox.vue'));

再到控制台运行node ./build-lib.js命令,此时在libs目录下只有4个产物了:
手把手教你vue组件库共享组件不直接打包进代码

再来看下产物代码:
YnButton.js
手把手教你vue组件库共享组件不直接打包进代码
YnButton.css
手把手教你vue组件库共享组件不直接打包进代码
YnButton组件打包产物没有变化,没问题

YnDialog.js
手把手教你vue组件库共享组件不直接打包进代码
YnDialog组件打包产物没有再包含YnButton组件代码,而是通过import形式引入,这就是我们想要的结果。
大功告成!

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Stella981 Stella981
3年前
List的Select 和Select().tolist()
List<PersondelpnewList<Person{newPerson{Id1,Name"小明1",Age11,Sign0},newPerson{Id2,Name"小明2",Age12,
Wesley13 Wesley13
3年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
JavaScript常用函数
1\.字符串长度截取functioncutstr(str,len){vartemp,icount0,patrn/^\x00\xff/,strre"";for(vari
Wesley13 Wesley13
3年前
MBR笔记
<bochs:100000000000e\WGUI\Simclientsize(0,0)!stretchedsize(640,480)!<bochs:2b0x7c00<bochs:3c00000003740i\BIOS\$Revision:1.166$$Date:2006/08/1117