突然接到一个需求,改造某个项目的打包产品,本来会生成多个文件,现在要求修正为只需一个ESM文件。

定睛一看,是个Vite项目,Vite的版本是v5.0.10vite.config.ts是这样的:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'property',
      fileName: 'property'
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    }
  }
})

这个与Vite官方推荐的库模式差不多,应该是参阅装备的。

现状

打包后的dist目录如下:

dist
|-- abap-936FyI36.js
|-- ...
|-- ...
|-- property.js
|-- property.umd.cjs
|-- ...
|-- ...
|-- style.css
|-- ...
|-- ...
`-- yaml-GSgOAuiZ.js
0 directories, 81 files

咱们看到,property.umd.cjs中,是全量的代码。本来引证它是不错的,可是需求是要一份ESM代码,不是UMD的。

UMD

这里略微解释下什么是UMD,年轻的朋友可能不认识它。

UMD,全称为Universal Module Definition,即通用模块界说,是一种JavaScript模块界说的规范。它的方针是使一个模块的代码在各种模块加载器或者没有模块加载器的环境下都能正常运行。

UMD完成了这个方针,经过查看存在的JavaScript模块体系并适应性地供给一个模块界说。假如AMD(如RequireJS)存在,它将界说一个AMD模块,假如CommonJS(如Node.js)存在,它将界说一个CommonJS模块,假如都不存在,那么它将界说一个大局变量。

咱们以一个名为AI的包为例,以下便是UMD的中心代码:

(function (root, factory) {
  if (typeof exports === 'object' && typeof module === 'object') {
    module.exports = factory()
  } else if (typeof define === 'function' && define.amd) {
    define([], factory)
  } else if (typeof exports === 'object') {
    exports['AI'] = factory()
  } else {
    root['AI'] = factory()
  }
})(this, function () {
  // AI的业务代码
})

这种方式使得你的模块能够在各种环境中运用,包含浏览器和服务器。

UMDESMECMAScript Modules)出现前的产品,它当然不可能兼容ESM,而因为ESM的特别性,UMD也做不到兼容ESM

ESM

咱们再看property.js文件,它是标准的ESM,内容是这样的:

import { i as f } from "./index-o0DaZxYG.js";
export {
  f as default
};

dist目录下生成了这么多的文件,是因为默许状况下,将node_modules里的包都生成了一个JavaScript文件。

下来,咱们一步步处理这个需求。

计划

合并node_modules

先把node_modules中的文件合并成一个。这涉及到Vite的分包战略。

这时,为rollupOptions中装备manualChunks即可:

rollupOptions: {
  external: ['vue'],
  output: {
    globals: {
      vue: 'Vue'
    },
    manualChunks: (id: string) => {
       if (id.includes('node_modules')) {
         return 'vendor'
       }
    }
  }
}

不好,竟然报错了:

error during build:

RollupError: Invalid value for option “output.manualChunks” – this option is not supported for “output.inlineDynamicImports”.

咱们到Rollup文档里找下inlineDynamicImports

Vite将lib库房打包为一个JavaScript文件

这显着是说锅是UMD的,inlineDynamicImportsmanualChunks是冲突的,不能一起存在。Vite底层处理UMD时估量装备了这个选项。咱们到ViteGitHub源码里也找到这段,验证了咱们的想法:

Vite将lib库房打包为一个JavaScript文件

这是说UMDIIFEImmediately Invoked Function Expression,立即调用函数表达式)以及SSR的某种条件下,会开启这个选项。

正好咱们也不需要UMD,就把Vite的默许选项替换掉,只需要添加formats即可:

lib: {
  entry: path.resolve(__dirname, 'src/index.ts'),
  name: 'property',
  fileName: 'property',
  formats: ['es', 'cjs']
},

这样只会生成ESMCommonJS两种了:

dist
|-- property.cjs
|-- property.js
|-- style.css
|-- vendor-T520oB1z.js
`-- vendor-XYPt9q-o.cjs
0 directories, 5 files

这个称号咱们未必满意,能够进行一次修正:

 lib: {
    entry: path.resolve(__dirname, 'src/index.ts'),
    name: 'property',
    formats: ['es', 'cjs'],
-   fileName: 'property',
    fileName: (format) => `property.${format}.js` // 打包后的文件名
  },

这样,新的文件名便是这样了:

dist
|-- property.cjs.js
|-- property.es.js
|-- style.css
|-- vendor-T520oB1z.js
`-- vendor-XYPt9q-o.cjs
0 directories, 5 files

假如你喜欢.mjs.cjs的后缀,也都是能够的。

合并vendor

咱们的需求是只需一个主JS文件,那么还不能要vendor,怎么办呢?

很简单:

  manualChunks: (id: string) => {
-      if (id.includes('node_modules')) {
            return 'vendor' 
-	    }
  }

这样,就将所有文件都合并成一个JS了。

dist
|-- property.cjs.js
|-- property.es.js
`-- style.css
0 directories, 3 files

至于函数返回的字符串是什么,就无所谓了。

合并style.css

咱们的组件的款式都在style.css中,还需要用户单独引进,多少不便。

Vite官方并没有供给相关的插件或装备项。

Vite将lib库房打包为一个JavaScript文件

这个看着像,可是可能只针对外部的chunk,咱们这种状况未生效。

运用插件

我在上找到了这篇文章《完成一个打包时将CSS注入到JS的Vite插件》:

Vite将lib库房打包为一个JavaScript文件

大佬便是凶猛,帮咱们造好了轮子。

不过当我翻开大佬留的库房,竟然变成只读了:

Vite将lib库房打包为一个JavaScript文件

好吧,究竟这篇文章是22年的了。

好在大佬说明晰归档的原因,本来强中自有强中手,大佬安利了另一个插件:

Vite将lib库房打包为一个JavaScript文件

于是,咱们只需要引证这个插件就能够了:

  import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
export default defineConfig({
  plugins: [
     vue(),
     cssInjectedByJsPlugin()
  ],
  build: {
  }
})

这次只会生成2个文件:

dist
|-- property.cjs.js
`-- property.es.js
0 directories, 2 files

咱们来看下文件中榜首行注入的内容(以下是展开后的):

(function () {
  "use strict";
  try {
    if (typeof document < "u") {
      var A = document.createElement("style");
      A.appendChild(
        document.createTextNode(
          '@charset "UTF-8";:root{--el-color-white:#ffffff;}'
          // style.CSS的内容
        )
      ),
        document.head.appendChild(A);
    }
  } catch (e) {
    console.error("vite-plugin-css-injected-by-js", e);
  }
})();

其实便是往HTML的head中注入了这一段大局的CSS。

Vite将lib库房打包为一个JavaScript文件

咱们再仔细看这一句typeof document < "u"很有意思,typeof document有两种可能的值,当它存在时是object,不存在是undefinedobject < uundefined > u,所以typeof document < "u"相当于是typeof document === "object"或者typeof document !== "undefined"。这是JS代码紧缩的一种特别手法,咱们平常要是写出这样的代码必定要给人骂死。

功德圆满!

Vite将lib库房打包为一个JavaScript文件

TIPS

其实,假如不考虑必须内嵌CSS的话,有个更简洁的办法能够这样处理:

rollupOptions: {
  external: ['vue'],
  output: {
     intro: 'import "./style.css";',
     manualChunks: (id: string) => {
        return 'vendor'
     }
  }
}

这个introbanner是等价的装备项,表示往bundle后的代码的头部注入一段信息,与之对应的是outrofooter

// rollup.config.js
export default {
  ...,
  output: {
    ...,
    banner: '/* my-library version '   version   ' */',
    footer: '/* follow me on Twitter! @rich_harris */'
  }
};

这样,生成的JS文件中就有了以下内容:

var Ea = (s, e, t) => (Bg(s, typeof e != "symbol" ? e   "" : e, t), t);
import "./style.css";
import { getCurrentScope, onScopeDispose, unref, getCurrentInstance, onMounted, nextTick, watch, ref, defineComponent, openBlock, createElementBlock, createElementVNode, warn, computed, inject, isRef, shallowRef, onBeforeUnmount, onBeforeMount, provide, mergeProps, renderSlot, toRef, onUnmounted, useAttrs as useAttrs$1, useSlots, withDirectives, createCommentVNode, Fragment, normalizeClass, createBlock, withCtx, resolveDynamicComponent, withModifiers, createVNode, toDisplayString as toDisplayString$1, normalizeStyle, vShow, cloneVNode, Text as Text$1, Comment, Teleport, Transition, readonly, onDeactivated, vModelRadio, createTextVNode, reactive, toRefs, onUpdated, withKeys, vModelText, pushScopeId, popScopeId, renderList } from "vue";
...

当用户引进这个JS时,就会动态引证CSS了。

当然,前提是这个JS是要被ViteWebpack等打包工具处理的,假如在HTML里直接引进,尽管网络里下载到了这个CSS文件,但浏览器校验它不是JavaScript,仍是要报错的。

Vite将lib库房打包为一个JavaScript文件

修正package.json

你是不是以为万事大吉了?慢着,别着急走。

Vite将lib库房打包为一个JavaScript文件

因为咱们修正了打包后生成的文件名,别忘了修正package.json中对应的这几项装备:

{
  "main": "./dist/property.cjs.js",
  "module": "./dist/property.es.js",
  "exports": {
    ".": {
      "import": "./dist/property.es.js",
      "require": "./dist/property.cjs.js"
    }
  }
}

总结

本文介绍了怎么将Vite项目中的lib库房打包为一个JavaScript文件,供给了具体的过程和代码示例,包含怎么合并node_modules、修正Vite装备以及运用插件进行款式注入等,假如修正了文件名,记得修正package.json中对应的装备。

最终修正的代码如下:

import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
export default defineConfig({
  plugins: [
    ...,
    cssInjectedByJsPlugin(),
  ],
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.ts'),
      name: 'property',
      formats: ['es', 'cjs'],
      fileName: (format) => `property.${format}.js` // 打包后的文件名
    },
    rollupOptions: {
      output: {
        manualChunks: (id: string) => {
          return 'vendor'
        }
      }
    }
  }
})