likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 6

突然接到一个需求,改造某个项目的打包产物,原来会生成多个文件,现在要求修改为只要一个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文档里找下inlineDynamicImportsVite将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,我们这种情况未生效。

使用插件

大佬就是厉害,帮我们造好了轮子。

不过当我打开大佬留的仓库,居然变成只读了: 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'
        }
      }
    }
  }
})