@darkchest/wck

@darkchest/wck是一个通过将 Vue 单文件组件 (SFC) 转换为通用的web-component组件的解决方案。
2,266
Install
npm install @darkchest/wck
Latest Version:0.0.17
License:MIT
Last Updated:Feb 3, 2026
Links: npm
Maintainer: destiny.michael

@darkchest/wck

🌉 Vue 单文件组件 (SFC) 转web-component组件

@darkchest/wck (全称@darkchest/WebComponentKit)是一个通过在vite中作为插件编译Vue单文件组件 (SFC) 为通用的web-component组件的解决方案。

前言

总会有一些场景, 我们无法使用现代化的框架系统.

在习惯了现代化数据驱动的开发模式下再次回到操作dom来更新UI的模式(没有了事件, scss, 模板, 数据监听等), 的确让人感到非常不便. 并且因为全局作用域的关系, 我们很容易碰上js变量, css样式的冲突导致的预期之外的bug. 更不幸的是由于没有固定的模板结构, 导致我们的静态网页代码有很大概率变得臃肿且越来越难以维护.

所以, 如果能够以vue2的options格式去编写web-component组件, 在开发阶段借助vue的ide插件帮助我们进行代码提示及错误警告. 在应用阶段又以web-component组件的方式去使用, 将vue组件内部的逻辑和样式封装在web-component组件内, web-component组件内部负责抹平与vue组件的差异并且对外只暴露出vue组件中定义的props, methods, events, slots等, 这就足够应对大部分的需求了. 这不就是应对困境的最好方案吗?

这也是我开发@darkchest/wck的初衷!

实际上在这之前我开发过几个版本的解决方案:

  1. 通过vue3官方提供的方案构建web-component组件, 最终因为一并将vue3的标准版代码打包到web-component组件中导致整个组件体积巨形膨胀(一个小组件体积上涨到90kb+), 考虑到既然都直接打包了vue3代码, 那么构建web-component组件好像没什么意义(还不如直接使用vue3进行开发), 此方案废弃.

  2. 通过lit来封装web-component组件, 整体来说是很顺利的. 但是在开发过程中, 由于要使用装饰器(不太熟悉ts), 且要自行实现render更新html, 整个开发过程不太舒适并且没有代码提示, 最终打包后体积大约为30kb+, 此方案搁置.

  3. 通过官方提供的@vue/compiler-sfc解析vue单文件组件 (SFC), 并将其script部分进行转换成标准的web-component组件. 逻辑部分已完成, 但在解析template部分时出现重大问题, 因为template部分解析出来属于vue特有的抽象语法树, 手上又没有详尽的资料又不想去啃源码, 导致template部分无法正确转换, 此方案被废弃.

  4. 最终, 我找到了petite-vue, 根据网络描述, 它是由 Vue.js 团队推出的重量仅约6KB的小型Vue版本,专为网页上的渐进式增强设计。它保留了Vue的核心模板语法与响应式机制,但特别优化用于在已有的HTML页面上增添少量交互效果. 经过测试它无需经过编译即可直接支持template语法(v-for, v-if, v-model, v-html, @click等), 允许直接在dom上应用template语法且无编译直接生效. 于是, 将方案3中的script转换与petite-vue的模板语法合并, 再抹平petite-vue语法与标准web-component的差异后的最终方案终于完成(与标准版vue仍有差距, 但是核心功能都已实现, 用于开发web-component组件应该是足够了), 最小体积降至16kb(未进行gzip压缩).

祝好!~

✨ 核心特性

  • ✅ 支持props属性(注意vue中允许使用大驼峰和小驼峰定义属性, 但是web-component组件属性只允许小写, 所以vue中定义的属性在编译成web-component组件时时会转成小写和中划线连字符形式的属性并进行映射)
  • ✅ 支持$el, $parent, $children, $root属性
  • ✅ 支持组件内querySelector(selector)查询组件内dom元素(因为web组件内的dom都隔离在shadowDom中)
  • ✅ 支持$emit事件
  • ✅ 支持methods方法
  • ✅ 支持compputed属性
  • ✅ 支持watch属性
  • ✅ 支持slot匿名和具名插槽
  • ✅ 支持mounted/onMounted, destroyed/unmounted/onUnmounted生命周期
  • ✅ 支持scss样式
  • [] 支持script setup语法(规划中...)

关于props大小写问题

由于vue中定义props是允许大小写字母的.而web-component组件的attributes只允许小写字母(包含中划线), 所以当我们在vue组件中定义一个带有大写字母的属性时(例如appTitle), 在@darkchest/wck插件将它转成web-component组件后, 插件会自动在web-component组件中声明apptitle, app-title两种形式(纯小写形式和小写中划线形式)的属性来映射appTitle这个vue组件属性, 所以当我们使用web-component组件时, 可以直接在html上使用全小写的属性名来设置组件的默认属性值:

  <todo-list id="demo1" apptitle="*我的清单*"></todo-list>
  <todo-list id="demo2" app-title="*我的清单*"></todo-list>
  <!-- 以上两种写法都可以设置vue组件的appTitle属性默认值 -->

  <script type="module">
    var todo1 = document.querySelector('#demo1');
    todo1.apptitle="我的清单1"; // 设置有效
    // todo1['app-title'] = "我的清单1"; // 设置有效

    var todo2 = document.querySelector('#demo1');
    todo2.apptitle="我的清单2"; // 设置有效
    // todo2['app-title'] = "我的清单2"; // 设置有效
  </script>

关于属性, 方法, 事件, 生命周期钩子

  • 在vue组件中props和methods定义的属性和方法, 都会直接通过web-component组件的属性和方法暴露给页面.
  • 在vue组件中支持(onMounted/mounted)和(onUnmounted/destroyed)生命周期钩子函数来执行初始化与销毁操作.
  • 所有编译后的web-component组件都会默认触发mounted/unmounted两个生命周期事件方便页面监听并进行某些初始化操作.
  • 所有编译后的web-component组件都默认增加$el, $parent, $children, $root属性.
  • 所有编译后的web-component组件都默认增加$emit(name, data), on(name, handler), once(name, handler), off(name, handler)方法.

🛠️ 安装与配置

1. 使用示例(重要: 请参考TodoList.vue文件示例, 该文件中已包含所有核心功能并且开箱即用)

// 创建一个项目文件夹, 例如helloworld, 然后在文件夹中执行npm init -y初始化一下(命令执行完会自动生成package.json文件).
// 然后我们在文件夹中手动创建:
//  - vite.config.js
//  - index.html
//  - src/index.js
//  - src/components/TodoList.vue
// 以上文件的内容直接复制下面对应的内容即可.
// (注意package.json不能复制, 而是手动修改scripts命令的部分, 具体请看下面的package.json部分)

2. 安装依赖(在刚刚创建的helloworld文件夹中执行以下命令安装依赖)

npm install @darkchest/wck vite -D

3. 在package.json中手动配置命令

// package.json
// 主要是增加dev/build俩个命令来启动vite开发和打包.
{
  ...,
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  ...,
}

4. 复制下面的文件内容到项目目录对应的文件中

// vite.config.js
import { defineConfig } from 'vite';
import path from 'path';
import wck from '@darkchest/wck';

export default defineConfig({
  plugins: [
    wck(), // 设置转换插件
  ],
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/index.js'),
      name: 'MyLib', // 在iife和umd模式下必填
      formats: ['es', 'iife', 'umd'],
      fileName: (format, name) => `${name}.${format}.min.js`
    },
  }
})
// src/index.js
import TodoList from './components/TodoList.vue';

customElements.define('todo-list', TodoList);
// 注意: web-component组件名必须为小写+中划线命名(有且至少一个中划线), 否则无法成功注册.
// src/components/TodoList.vue

<template>
  <div id="app">
    <div class="todo-container">
      <!-- Header Slot -->
      <header>
        <slot name="header">
          <h1 class="app-title">{{ appTitle }}</h1>
          <p class="app-description">使用 Vue2 实现的 Todo List 应用</p>
        </slot>
      </header>

      <!-- Add Todo Form -->
      <div class="add-todo">
        <input
          type="text"
          v-model="newTodo"
          placeholder="请输入待办事项..."
          @keyup.enter="addTodo"
          class="todo-input"
        />
        <button @click="addTodo" class="add-btn">添加</button>
        <button @click="clearCompleted" class="clear-btn">清除已完成</button>
      </div>

      <!-- Filter Controls -->
      <div class="filter-controls">
        <button 
          @click="filter = 'all'"
          :class="{ active: filter === 'all' }"
          class="filter-btn"
        >
          全部 ({{ totalTodos }})
        </button>
        <button 
          @click="filter = 'active'"
          :class="{ active: filter === 'active' }"
          class="filter-btn"
        >
          未完成 ({{ activeTodosCount }})
        </button>
        <button 
          @click="filter = 'completed'"
          :class="{ active: filter === 'completed' }"
          class="filter-btn"
        >
          已完成 ({{ completedTodosCount }})
        </button>
      </div>

      <!-- Todo List -->
      <div class="todo-list">
        <div v-if="filteredTodos.length === 0" class="empty-state">
          <p v-if="todos.length === 0">暂无待办事项,请添加一个吧!</p>
          <p v-else>没有{{ filter === 'active' ? '未完成' : '已完成' }}的事项</p>
        </div>
        
        <div 
          v-for="todo in filteredTodos" 
          :key="todo.id"
          class="todo-item"
          :class="{ completed: todo.completed }"
        >
          <div class="todo-content">
            <input 
              type="checkbox" 
              v-model="todo.completed"
              class="todo-checkbox"
              :id="'todo-' + todo.id"
            />
            <label :for="'todo-' + todo.id" class="todo-text">
              <span>{{ todo.text }}</span>
              <span class="todo-date">{{ formatDate(todo.createdAt) }}</span>
            </label>
          </div>
          <div class="todo-actions">
            <button @click="editTodo(todo)" class="edit-btn">编辑</button>
            <button @click="deleteTodo(todo.id)" class="delete-btn">删除</button>
          </div>
        </div>
      </div>

      <!-- Stats -->
      <div class="stats">
        <p v-if="todos.length > 0">
          已完成 {{ completedTodosCount }} / 总共 {{ totalTodos }} 个待办事项
          <span v-if="hasCompletedTodos" class="completion-rate">
            (完成率: {{ completionRate }}%)
          </span>
        </p>
        
        <!-- Progress Bar Slot -->
        <slot name="progress">
          <div class="progress-container">
            <div class="progress-bar" :style="{ width: completionRate + '%' }"></div>
          </div>
        </slot>
      </div>

      <!-- Footer Slot -->
      <footer>
        <slot name="footer">
          <p class="footer-text">双击事项可标记为完成/未完成</p>
          <p class="footer-text">使用 Vue2 实现 - 包含 Props, Data, Methods, Slots, Watch, Computed, 生命周期</p>
        </slot>
      </footer>
    </div>

    <!-- Edit Modal -->
    <div v-if="editingTodo" class="modal-overlay" @click="cancelEdit">
      <div class="modal" @click.stop>
        <h3>编辑待办事项</h3>
        <input 
          type="text" 
          v-model="editingTodo.text" 
          @keyup.enter="saveEdit"
          class="edit-input"
        />
        <div class="modal-actions">
          <button @click="saveEdit" class="save-btn">保存</button>
          <button @click="cancelEdit" class="cancel-btn">取消</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'TodoApp',
  
  // Props 示例
  props: {
    initialTodos: {
      type: [Array, Object],
      default: () => []
    },
    appTitle: {
      type: String,
      default: '我的待办清单'
    },
    enableLocalStorage: {
      type: Boolean,
      default: true
    }
  },
  
  // Data 示例
  data() {
    return {
      newTodo: '',
      todos: [],
      filter: 'all',
      editingTodo: null,
      lastTodoId: 0
    };
  },
  
  // Computed 示例
  computed: {
    // 计算过滤后的待办事项
    filteredTodos() {
      switch (this.filter) {
        case 'active':
          return this.todos.filter(todo => !todo.completed);
        case 'completed':
          return this.todos.filter(todo => todo.completed);
        default:
          return this.todos;
      }
    },
    
    // 计算未完成事项数量
    activeTodosCount() {
      return this.todos.filter(todo => !todo.completed).length;
    },
    
    // 计算已完成事项数量
    completedTodosCount() {
      return this.todos.filter(todo => todo.completed).length;
    },
    
    // 计算总事项数量
    totalTodos() {
      return this.todos.length;
    },
    
    // 计算完成率
    completionRate() {
      if (this.totalTodos === 0) return 0;
      return Math.round((this.completedTodosCount / this.totalTodos) * 100);
    },
    
    // 检查是否有已完成事项
    hasCompletedTodos() {
      return this.completedTodosCount > 0;
    }
  },
  
  // Watch 示例
  watch: {
    // 监听 todos 变化并保存到 localStorage
    todos: {
      handler(newTodos, oldTodos) {
        if (this.enableLocalStorage) {
          localStorage.setItem('vue-todos', JSON.stringify(newTodos));
        }
        
        // 发送事件给父组件(如果需要)
        this.$emit('todos-updated', newTodos);
      },
      deep: true
    },
    
    // 监听 filter 变化
    filter(newFilter, oldFilter) {
      console.log('过滤器已更改为: '+newFilter);
    }
  },
  
  // Methods 示例
  methods: {
    // 添加新的待办事项
    addTodo() {
      if (this.newTodo.trim() === '') return;
      
      this.todos.push({
        id: ++this.lastTodoId,
        text: this.newTodo.trim(),
        completed: false,
        createdAt: new Date()
      });
      
      this.newTodo = '';
      console.log('已添加新的待办事项');
    },
    
    // 删除待办事项
    deleteTodo(id) {
      const index = this.todos.findIndex(todo => todo.id === id);
      if (index !== -1) {
        this.todos.splice(index, 1);
        console.log('已删除待办事项 ID: '+id);
      }
    },
    
    // 编辑待办事项
    editTodo(todo) {
      this.editingTodo = { ...todo };
    },
    
    // 保存编辑
    saveEdit() {
      if (this.editingTodo && this.editingTodo.text.trim() !== '') {
        const index = this.todos.findIndex(todo => todo.id === this.editingTodo.id);
        if (index !== -1) {
          this.$set(this.todos, index, { ...this.editingTodo });
          console.log('已更新待办事项 ID: '+this.editingTodo.id);
        }
      }
      this.editingTodo = null;
      // 已知这里会报错, 这是petite-vue内部的bug, 因为并不影响使用, 所以我不打算修复(没有petite-vue源码).
      // 感觉这是因为v-if的优先级没有v-model高导致的, 所以我们可以通过额外设置一个开关变量来绕开这个报错, 只要不设置对象为null就行.
    },
    
    // 取消编辑
    cancelEdit() {
      this.editingTodo = null;
      // 已知这里会报错, 这是petite-vue内部的bug, 因为并不影响使用, 所以我不打算修复(没有petite-vue源码).
      // 感觉这是因为v-if的优先级没有v-model高导致的, 所以我们可以通过额外设置一个开关变量来绕开这个报错, 只要不设置对象为null就行.
    },
    
    // 清除已完成事项
    clearCompleted() {
      const originalLength = this.todos.length;
      this.todos = this.todos.filter(todo => !todo.completed);
      const clearedCount = originalLength - this.todos.length;
      console.log('已清除 '+clearedCount+' 个已完成事项');
    },
    
    // 格式化日期
    formatDate(date) {
      return new Date(date).toLocaleDateString('zh-CN', {
        month: 'short',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit'
      });
    }
  },
  
  // 生命周期钩子示例
  created() {
    console.log('TodoApp 组件已创建');
    
    // 从 props 或 localStorage 初始化 todos
    if (this.initialTodos && this.initialTodos.length > 0) {
      this.todos = [...this.initialTodos];
      this.lastTodoId = Math.max(...this.initialTodos.map(todo => todo.id), 0);
    } else if (this.enableLocalStorage) {
      const savedTodos = localStorage.getItem('vue-todos');
      if (savedTodos) {
        try {
          this.todos = JSON.parse(savedTodos);
          this.lastTodoId = Math.max(...this.todos.map(todo => todo.id), 0);
          console.log('从 localStorage 加载待办事项');
        } catch (e) {
          console.error('解析保存的待办事项失败:', e);
        }
      }
    }
  },
  
  mounted() {
    console.log('TodoApp 组件已挂载到 DOM');
    
    // 为每个待办事项添加双击切换完成状态的功能
    this.$nextTick(() => {
      const todoItems = document.querySelectorAll('.todo-item');
      todoItems.forEach(item => {
        item.addEventListener('dblclick', () => {
          const todoId = parseInt(item.dataset.id);
          const todoIndex = this.todos.findIndex(todo => todo.id === todoId);
          if (todoIndex !== -1) {
            this.todos[todoIndex].completed = !this.todos[todoIndex].completed;
            console.log('双击切换待办事项状态 ID: '+todoId);
          }
        });
      });
    });
  },
  
  beforeUpdate() {
    console.log('TodoApp 组件即将更新');
  },
  
  updated() {
    console.log('TodoApp 组件已更新');
  },
  
  beforeDestroy() {
    console.log('TodoApp 组件即将销毁');
  },
  
  destroyed() {
    console.log('TodoApp 组件已销毁');
  }
};
</script>

<style lang="scss" scoped>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.todo-container {
  background: white;
  border-radius: 10px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
  padding: 30px;
}

header {
  text-align: center;
  margin-bottom: 30px;
  border-bottom: 1px solid #eee;
  padding-bottom: 20px;
}

.app-title {
  color: #42b983;
  font-size: 2.5rem;
  margin-bottom: 10px;
}

.app-description {
  color: #7f8c8d;
  font-size: 1rem;
}

.add-todo {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.todo-input {
  flex: 1;
  padding: 12px 15px;
  border: 2px solid #e0e0e0;
  border-radius: 6px;
  font-size: 16px;
  transition: border-color 0.3s;
}

.todo-input:focus {
  border-color: #42b983;
  outline: none;
}

.add-btn, .clear-btn {
  padding: 12px 20px;
  border: none;
  border-radius: 6px;
  font-weight: bold;
  cursor: pointer;
  transition: all 0.3s;
}

.add-btn {
  background-color: #42b983;
  color: white;
}

.add-btn:hover {
  background-color: #3aa876;
}

.clear-btn {
  background-color: #f0f0f0;
  color: #333;
}

.clear-btn:hover {
  background-color: #e0e0e0;
}

.filter-controls {
  display: flex;
  gap: 10px;
  margin-bottom: 25px;
}

.filter-btn {
  flex: 1;
  padding: 10px;
  background-color: #f8f9fa;
  border: 1px solid #dee2e6;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.3s;
}

.filter-btn.active {
  background-color: #42b983;
  color: white;
  border-color: #42b983;
}

.filter-btn:hover:not(.active) {
  background-color: #e9ecef;
}

.todo-list {
  margin-bottom: 25px;
}

.empty-state {
  text-align: center;
  padding: 40px 20px;
  color: #95a5a6;
  font-style: italic;
}

.todo-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  border-bottom: 1px solid #f0f0f0;
  transition: all 0.3s;
  border-radius: 6px;
  margin-bottom: 8px;
  background-color: #f9f9f9;
}

.todo-item:hover {
  background-color: #f0f0f0;
  transform: translateY(-2px);
}

.todo-item.completed {
  opacity: 0.8;
  background-color: #f0f9f0;
}

.todo-content {
  display: flex;
  align-items: center;
  gap: 15px;
  flex: 1;
}

.todo-checkbox {
  width: 20px;
  height: 20px;
  cursor: pointer;
}

.todo-text {
  flex: 1;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.todo-item.completed .todo-text span:first-child {
  text-decoration: line-through;
  color: #95a5a6;
}

.todo-date {
  font-size: 0.8rem;
  color: #95a5a6;
}

.todo-actions {
  display: flex;
  gap: 8px;
}

.edit-btn, .delete-btn {
  padding: 6px 12px;
  border: none;
  border-radius: 4px;
  font-size: 0.9rem;
  cursor: pointer;
  transition: all 0.2s;
}

.edit-btn {
  background-color: #f0f0f0;
  color: #333;
}

.edit-btn:hover {
  background-color: #e0e0e0;
}

.delete-btn {
  background-color: #ff6b6b;
  color: white;
}

.delete-btn:hover {
  background-color: #ff5252;
}

.stats {
  margin-top: 25px;
  padding-top: 20px;
  border-top: 1px solid #eee;
  color: #7f8c8d;
}

.completion-rate {
  color: #42b983;
  font-weight: bold;
}

.progress-container {
  height: 10px;
  background-color: #f0f0f0;
  border-radius: 5px;
  margin-top: 10px;
  overflow: hidden;
}

.progress-bar {
  height: 100%;
  background-color: #42b983;
  border-radius: 5px;
  transition: width 0.5s ease;
}

footer {
  margin-top: 30px;
  padding-top: 20px;
  border-top: 1px solid #eee;
  text-align: center;
  color: #95a5a6;
  font-size: 0.9rem;
}

.footer-text {
  margin: 5px 0;
}

/* 编辑模态框样式 */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal {
  background-color: white;
  padding: 30px;
  border-radius: 10px;
  width: 90%;
  max-width: 500px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}

.modal h3 {
  margin-top: 0;
  margin-bottom: 20px;
  color: #2c3e50;
}

.edit-input {
  width: 100%;
  padding: 12px 15px;
  border: 2px solid #42b983;
  border-radius: 6px;
  font-size: 16px;
  margin-bottom: 20px;
}

.modal-actions {
  display: flex;
  gap: 10px;
  justify-content: flex-end;
}

.save-btn, .cancel-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  font-weight: bold;
  cursor: pointer;
}

.save-btn {
  background-color: #42b983;
  color: white;
}

.save-btn:hover {
  background-color: #3aa876;
}

.cancel-btn {
  background-color: #f0f0f0;
  color: #333;
}

.cancel-btn:hover {
  background-color: #e0e0e0;
}
</style>

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>my web components</title>
  </head>
  <body>
    <todo-list app-title="*待办清单*"></todo-list>
    <script type="module">
      import './src/index.js';
      // 执行npm run dev时vite会启动服务并打开index.html, 我们在这个页面中导入src/index.js就能正常执行它里面的代码并注册web-component组件了
    </script>

    <script src="./dist/index.iife.min.js"></script>
    <!-- 也可以执行npm run build直接编译src/index.js到dist目录, 然后在script的src属性中引入打包后的文件, 方案二选一即可 -->
  </body>
</html>

5. 运行项目

  • 如果执行npm run dev, 浏览器打开index.html并运行在localhost下, 此时请参考上面的说明使用import './src/index.js'的方式引入src/index.js.
  • 如果运行npm run build, 则会使用vite打包文件到dist目录下的index.iife.min.js, 然后我们手动在index.html中通过script的src属性引入这个打包后的文件, 最后双击index.html打开网页即可.

最后的最后

要是还看不明白, 直接将源码下载下来后, 或者安装npm包后, 在node_modules/@drakchest/wck中有个example文件夹, 双击打开index.html即可看到效果.

您也可以将example文件夹复制到其它地方, 然后执行npm install安装依赖, 最后npm run dev启动开发服务, 并按上面提到的修改index.html里的script部分, 改成直接import 'src/index.js'即可体验效果. 一但正常启动并以localhost打开网页后, 您还可以尝试着修改TodoList.vue代码观察效果.

同理, 在开发模式下, 您还可以在src/components中创建更多的组件并在src/index.js中导入并注册为web-component组件, 在index.html中可以直接使用新注册的web-component组件.

最后, 如果需要异步注册, 编译每个组件为独立js文件等功能, 您可能需要自行在vite.config.js中配置多入口编译, 这不属于本教程的讨论范围. 请自行阅读vite的官方文档.

祝您使用愉快.

Related Packages

@dullar/mercury

Advanced and highly reusable Mobile UI Components built on Vue2 & You can define...

8,553 6
@halobear/vue-upload

vue上传组件

460 1
@vue/web-component-wrapper

wrap a vue component as a web component.

26,969,355
vite-plugin-vue2

Vite plugin for Vue2

1,474,659 618
vue-jstree

a tree plugin for vue2

137,015 630

Version History

Version Released License
0.0.17 MIT
0.0.16 MIT
0.0.15 MIT
0.0.14 MIT
0.0.13 MIT