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

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

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

ant-design-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个打包产物:

有没有发现不对,应该打包出4个产物才对,因为YnDialog、YnMessageBox组件基本没有增加css,但打包后却多出了2个css文件,这就是将共享组件打包进代码的问题之一

再来看下打包后的产物:
YnButton.js

YnButton.css

YnDialog.js

YnDialog.css

通过查看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个产物了:

再来看下产物代码:
YnButton.js

YnButton.css

YnButton组件打包产物没有变动,没问题

YnDialog.js

YnDialog组件打包产物没有再蕴含YnButton组件代码,而是通过import模式引入,这就是咱们想要的后果。
功败垂成!