从零到一:用 Vue 打造一个零依赖、插件化的 JS 库

2025-10-09 奥古斯宏

你是否曾想过,除了开发复杂的单页应用(SPA),我们还能用 Vue 做些什么?比如,开发一个像地图 SDK、在线客服或数据统计脚本那样的独立 JS 库?用户只需在页面上引入一个 <script> 标签,就能立即使用你提供的功能,而无需关心其内部实现。

今天,我们就来探讨如何使用 Vue 和 Vite,从零开始构建一个这样的 JS 库。

一、 需求分析:我们想做什么?

我们的目标非常明确,创建一个名为 phpreturn 的库,它需要满足以下条件:

  1. 最终产物单一:打包后只有一个 JS 文件,例如 phpreturn.js
  2. 使用简单:终端用户只需通过 <script src="phpreturn.js"></script> 引入,无需安装任何依赖或额外引入 Vue。
  3. 全局 API:引入后,在 window 对象上挂载一个全局变量(如 phpreturn),通过 phpreturn.chooseUser() 等方法调用功能。
  4. UI 驱动:调用方法后,会弹出由 Vue 组件构成的 UI 界面(如一个用户选择弹窗)。
  5. 环境无关:无论是在一个普通的静态 HTML 页面,还是在一个已经使用 Vue 2 或 React 的复杂项目中,这个库都必须能正常工作,且不产生任何冲突。

二、 核心要点与挑战:如何实现?

要满足上述需求,我们必须解决两个核心问题:打包隔离

  1. 打包 (Bundling) 我们希望交付的是一个“全家桶”文件,包含了 Vue 运行时和我们所有的业务组件。这意味着在打包时,不能像开发普通 Vue 插件那样将 Vue 设置为外部依赖(external)。我们需要告诉打包工具(Vite):“把 Vue 和我的代码一起打包进去!”
  2. 隔离 (Isolation) 这是最大的挑战。如果我们的库被引入一个已经存在 Vue 的页面,会发生什么? JS 冲突:可能会出现两个不同版本的 Vue 实例,导致全局状态污染或方法覆盖。 CSS 冲突:我们的组件样式可能会影响宿主页面,反之亦然。.btn 或 .container 这种通用类名简直是灾难的根源。 解决方案:Shadow DOM。 Shadow DOM 是 Web Components 技术的一部分,它允许我们将一块独立的、封装的 DOM 树附加到普通元素上。这块 DOM 树(包括其内部的 HTML 和 CSS)与主文档的 DOM 完全隔离,形成了一个完美的“沙箱”。我们的 Vue 应用将在这个沙箱中运行,从而彻底解决了 JS 和 CSS 的冲突问题。

三、 实现方式:一步步构建 phpreturn

让我们通过具体的代码来实现这个想法。

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

步骤 1:初始化 Vite 项目

首先,我们需要一个基础的 Vue + Vite 项目。打开你的终端,运行以下命令来创建项目:

npm create vite@latest phpreturn -- --template vue

进入项目目录并安装依赖:

cd phpreturn
npm install

这会创建一个名为 phpreturn 的新目录。为了保持项目整洁,我们可以清理一下 Vite 生成的默认文件:

  • 删除 src/assets 目录。
  • 删除 src/components/HelloWorld.vuesrc/App.vue 文件。
  • 清空 src/main.js 的内容,准备编写我们的库入口逻辑。

现在,我们的项目已经准备就绪。

步骤 2:配置 Vite 进行独立打包

接下来,修改项目根目录下的 vite.config.js,告诉 Vite 我们要构建一个库(library),而不是一个应用。

// File: vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  // 新增:在开发模式下,模拟 UMD 挂载到 window
  define: {
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
  },
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/main.js'),
      name: 'phpreturn',      // 挂在 window 上的全局变量名
      formats: ['umd'],   // 输出 UMD 格式,兼容 <script> 标签
      fileName: () => `phpreturn.js` // 固定输出文件名
    },
    // 注意:这里没有 rollupOptions.external,意味着所有依赖(包括Vue)都会被打包进去
  }
})

步骤 3:创建与 Shadow DOM 结合的入口文件

src/main.js 是我们库的灵魂。它负责创建 Shadow DOM 环境,定义全局 API,并将 Vue 组件挂载进去。

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

// File: src/main.js
import { createApp } from 'vue';
import ChooseUser from './components/ChooseUser.vue';

// 核心函数:在 Shadow DOM 中挂载组件
function mountInShadowDom(component, props) {
  const hostElement = document.createElement('div');
  document.body.appendChild(hostElement);

  const shadowRoot = hostElement.attachShadow({ mode: 'open' });
  const appContainer = document.createElement('div');
  shadowRoot.appendChild(appContainer);

  // 将组件的样式作为 <style> 标签注入到 Shadow DOM 中
  // 这是实现样式隔离的关键
  if (component.style) {
    const styleEl = document.createElement('style');
    styleEl.textContent = component.style;
    shadowRoot.appendChild(styleEl);
  }

  const app = createApp(component, props);
  app.mount(appContainer);

  // 返回一个清理函数,用于关闭弹窗后销毁实例和 DOM
  return () => {
    app.unmount();
    document.body.removeChild(hostElement);
  };
}

// 暴露给用户的全局 API
export function chooseUser() {
  return new Promise((resolve, reject) => {
    let unmount;
    const props = {
      onSelect: (user) => {
        resolve(user);
        if (unmount) unmount();
      },
      onClose: () => {
        reject(new Error('User cancelled.'));
        if (unmount) unmount();
      }
    };
    unmount = mountInShadowDom(ChooseUser, props);
  });
}

// 在开发模式下,将 API 挂载到 window 上,方便调试
if (process.env.NODE_ENV === 'development') {
  window.phpreturn = { chooseUser };
}

步骤 4:改造 Vue 组件以适应 Shadow DOM

由于样式需要被手动注入到 Shadow DOM,我们需要对 .vue 单文件组件做一点小改造。直接从 .vue 文件自身导入样式会导致循环引用问题,因此最佳实践是将样式分离到单独的 .css 文件中

首先,创建样式文件 src/components/ChooseUser.css

/* File: src/components/ChooseUser.css */
/* 样式不再需要 scoped,因为 Shadow DOM 本身就是作用域 */
.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 99999; /* 确保在最顶层 */
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
  min-width: 300px;
}

.modal-content ul {
  list-style: none;
  padding: 0;
  margin: 10px 0;
}

.modal-content li {
  padding: 8px;
  cursor: pointer;
  border-bottom: 1px solid #eee;
}

.modal-content li:hover {
  background: #f0f0f0;
}

然后,修改 src/components/ChooseUser.vue,从新的 CSS 文件导入样式,并移除 <style> 块。

<!-- File: src/components/ChooseUser.vue -->
<template>
  <div v-if="visible" class="modal-backdrop" @click.self="close">
    <div class="modal-content">
      <h2>Choose a User</h2>
      <ul>
        <li v-for="user in users" :key="user.id" @click="select(user)">
          {{ user.name }}
        </li>
      </ul>
      <button @click="close">Close</button>
    </div>
  </div>
</template>

<script>
import { ref } from 'vue';
// 关键:将 CSS 作为字符串导入
// `?inline` 是 Vite 的一个特性,它会将文件内容作为字符串导入
import styles from './ChooseUser.css?inline';

export default {
  name: 'ChooseUser',
  props: { onSelect: Function, onClose: Function },
  // 关键:导出样式,供 main.js 使用
  style: styles,
  setup(props) {
    const visible = ref(true);
    const users = ref([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
      { id: 3, name: 'Charlie' },
    ]);

    const select = (user) => {
      props.onSelect(user.name);
    };

    const close = () => {
      visible.value = false;
      props.onClose();
    };

    return { visible, users, select, close };
  }
}
</script>

四、 打包与使用

现在,只需运行 npm run build,你就会在 dist 目录下得到一个独立的 phpreturn.js 文件。

在任何 HTML 页面中这样使用它:

<!DOCTYPE html>
<html>
<head>
  <title>Test phpreturn</title>
</head>
<body>
  <h1>My Awesome Page</h1>
  <button id="my-button">Choose a User</button>

  <!-- 1. 引入你的库,就这么简单 -->
  <script src="./dist/phpreturn.js"></script>

  <script>
    // 2. 直接调用全局 API
    document.getElementById('my-button').onclick = async () => {
      try {
        const user = await phpreturn.chooseUser();
        alert(`You selected: ${user}`);
      } catch (error) {
        console.warn(error.message);
      }
    };
  </script>
</body>
</html>

附:如何实现开发时实时预览?

我们已经完成了库的构建,但如何在开发过程中实时看到效果,而不是每次修改都执行 npm run build 呢?Vite 强大的开发服务器(Dev Server)可以轻松解决这个问题。

版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

创建测试页面

在项目根目录下创建一个 `index.html` 文件。这个文件将作为我们开发时的宿主页面。

<!-- File: index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Test phpreturn Dev</title>
</head>
<body>
  <h1>My Awesome Page (Dev Mode)</h1>
  <button id="my-button">Choose a User</button>

  <!-- 关键:直接引入库的入口文件 -->
  <script type="module" src="/src/main.js"></script>

  <script>
    document.getElementById('my-button').onclick = async () => {
      try {
        // 直接调用挂在 window 上的 API
        const user = await window.phpreturn.chooseUser();
        alert(`You selected: ${user}`);
      } catch (error) {
        console.warn(error.message);
      }
    };
  </script>
</body>
</html>

启动开发服务器

现在,运行 npm run dev。Vite 会启动一个开发服务器,并自动打开 index.html。当你修改 src 目录下的任何文件(例如 ChooseUser.vue 的样式或逻辑)时,页面会热更新(HMR),让你能立即看到修改后的效果,极大地提升了开发效率。

结语

通过将 Vue 打包进库文件并利用 Shadow DOM 实现完美隔离,我们成功地创建了一个真正独立、即插即用的 JS
库。这种模式极大地扩展了 Vue 的应用场景,使其不再局限于构建大型应用,而是可以作为“功能组件”嵌入到任何 Web
环境中。希望这篇文章能为你打开一扇新的大门。


版权声明:本文由phpreturn.com(PHP武器库官网)原创和首发,所有权利归phpreturn(PHP武器库)所有,本站允许任何形式的转载/引用文章,但必须同时注明出处。

最近浏览
IP用户:66.249.*.*
1 小时前 Googlebot
IP用户:27.197.*.*
1 小时前 Firefox Windows 10
IP用户:223.77.*.*
2 小时前 Chrome Windows 10
IP用户:66.249.*.*
8 小时前 Googlebot
IP用户:142.44.*.*
9 小时前 aHrefs Bot
IP用户:43.130.*.*
10 小时前 Mobile Safari iOS 13.2
IP用户:34.195.*.*
12 小时前
IP用户:106.8.*.*
12 小时前 Generic Bot
IP用户:45.79.*.*
12 小时前 BazQux Reader
IP用户:8.134.*.*
14 小时前 Chrome Windows XP
IP用户:106.13.*.*
14 小时前 Go-http-client
IP用户:106.13.*.*
14 小时前 Chrome Mac 10.11
累计浏览次数:31
评论
点击登录
phpreturn,PHP武器库,专注PHP领域的项目和资讯,收录和介绍PHP相关项目。
最近浏览 点击登录
累计浏览次数:255123
一周浏览次数:1120
今日浏览次数:52

本站所有权利归 phpreturn.com 所有

举报/反馈/投稿邮箱:phpreturn@ulthon.com

鲁ICP备19027671号-2