2. 三分钟带你开发一个web component跨框架组件

262 阅读3分钟

上一章讲解了什么是web component, 这一章带大家从0-1开发一个web component, 我们先观摩市面上已有的web component组件库

ionicframework

quark

UI5

下面跟着我的脚步从0-1开发一个web component

  1. 框架选用

    我们使用vue3来开发web component, 你没听错, 就是vue3, vue支持将组件编译成web component

  2. 新建项目模版

    npm i -g yarn
    yarn create vite web-component-ui --template vue-ts
    cd ./web-component-ui
    yarn
    
  3. 新建组件文件index.ce.vue

    web-component-ui/src/components/button/index.ce.vue

    <template>
      <button
        class="fl-button"
        :class="[
          `fl-button--${type}`,
          {
            'is-disabled': disabled,
            'is-loading': loading,
            'is-round': round,
          }
        ]"
        :disabled="disabled || loading"
        @click="handleClick"
      >
        <!-- loading 图标 -->
        <span v-if="loading" class="loading-icon">
          <svg viewBox="0 0 1024 1024" class="loading">
            <path d="M512 64q14.016 0 23.008 8.992T544 96v192q0 14.016-8.992 23.008T512 320t-23.008-8.992T480 288V96q0-14.016 8.992-23.008T512 64zm0 640q14.016 0 23.008 8.992T544 736v192q0 14.016-8.992 23.008T512 960t-23.008-8.992T480 928V736q0-14.016 8.992-23.008T512 704zm448-192q0 14.016-8.992 23.008T928 544H736q-14.016 0-23.008-8.992T704 512t8.992-23.008T736 480h192q14.016 0 23.008 8.992T960 512zm-640 0q0 14.016-8.992 23.008T288 544H96q-14.016 0-23.008-8.992T64 512t8.992-23.008T96 480h192q14.016 0 23.008 8.992T320 512z" />
          </svg>
        </span>
        
        <!-- 默认插槽 -->
        <slot></slot>
      </button>
    </template>
    
    <script setup lang="ts">
    import { defineProps, defineEmits } from 'vue'
    
    // 定义属性
    const props = defineProps({
      type: {
        type: String,
        default: 'default',
        validator: (val: string) => {
          return ['default', 'primary', 'success', 'warning', 'danger'].includes(val)
        }
      },
      disabled: {
        type: Boolean,
        default: false
      },
      loading: {
        type: Boolean,
        default: false
      },
      round: {
        type: Boolean,
        default: false
      }
    })
    
    // 定义事件
    const emit = defineEmits(['click'])
    
    // 点击处理
    const handleClick = (event: MouseEvent) => {
      if (props.disabled || props.loading) return
      emit('click', event)
    }
    </script>
    
    <style scoped>
    .fl-button {
      display: inline-flex;
      justify-content: center;
      align-items: center;
      line-height: 1;
      height: 32px;
      white-space: nowrap;
      cursor: pointer;
      color: #606266;
      text-align: center;
      box-sizing: border-box;
      outline: none;
      transition: .1s;
      font-weight: 500;
      padding: 8px 15px;
      font-size: 14px;
      border-radius: 4px;
      border: 1px solid #dcdfe6;
      background-color: #ffffff;
    }
    
    .fl-button:hover {
      color: #409eff;
      border-color: #c6e2ff;
      background-color: #ecf5ff;
    }
    
    .fl-button:active {
      color: #3a8ee6;
      border-color: #3a8ee6;
      outline: none;
    }
    
    /* 禁用状态 */
    .fl-button.is-disabled,
    .fl-button.is-disabled:hover {
      color: #c0c4cc;
      cursor: not-allowed;
      background-image: none;
      background-color: #fff;
      border-color: #ebeef5;
    }
    
    /* 加载状态 */
    .fl-button.is-loading {
      position: relative;
      pointer-events: none;
    }
    
    /* 圆角按钮 */
    .fl-button.is-round {
      border-radius: 20px;
    }
    
    /* 类型样式 */
    .fl-button--primary {
      color: #fff;
      background-color: #409eff;
      border-color: #409eff;
    }
    
    .fl-button--primary:hover {
      background: #66b1ff;
      border-color: #66b1ff;
      color: #fff;
    }
    
    .fl-button--success {
      color: #fff;
      background-color: #67c23a;
      border-color: #67c23a;
    }
    
    .fl-button--success:hover {
      background: #85ce61;
      border-color: #85ce61;
      color: #fff;
    }
    
    .fl-button--warning {
      color: #fff;
      background-color: #e6a23c;
      border-color: #e6a23c;
    }
    
    .fl-button--warning:hover {
      background: #ebb563;
      border-color: #ebb563;
      color: #fff;
    }
    
    .fl-button--danger {
      color: #fff;
      background-color: #f56c6c;
      border-color: #f56c6c;
    }
    
    .fl-button--danger:hover {
      background: #f78989;
      border-color: #f78989;
      color: #fff;
    }
    
    /* Loading 图标样式 */
    .loading-icon {
      margin-right: 5px;
    }
    
    .loading {
      width: 14px;
      height: 14px;
      animation: rotating 2s linear infinite;
    }
    
    @keyframes rotating {
      0% {
        transform: rotateZ(0deg);
      }
      100% {
        transform: rotateZ(360deg);
      }
    }
    </style>
    
  4. 新建编译入口文件index.ts

    web-component-ui/src/components/button/index.ts

    import Button from './index.ce.vue';
    import { defineCustomElement } from 'vue'
    
    /** 编译成webcomponent */
    const vueWebComponent = defineCustomElement(Button)
    
    /** 注册组件 */
    if (!customElements.get('fl-button'))
      customElements.define('fl-button', vueWebComponent);
    
    export { Button };
    
  5. 修改编译配置

    web-component-ui/vite.config.ts

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    
    // https://vite.dev/config/
    export default defineConfig({
      plugins: [
        vue({
          template: {
            compilerOptions: {
              isCustomElement: (tag) => tag.includes('-')
            }
          }
        }),
      ],
      build: {
        rollupOptions: {
          input: {
            "components/button": './src/components/button/index.ts',
          },
          output: {
            format: 'esm',
            dir: 'dist',
            entryFileNames: "js/[name].js",
            chunkFileNames: "js/deps/[name].[hash].js",
            assetFileNames: 'js/assets/[name]-[hash][extname]',
            /** 分包配置 */
            manualChunks: {
              'vue': ['vue'],
            },
          }
        }
      }
    })
    
  6. 打包

    yarn build
    

    打包成功后, 你就可以看到dist目录, 只要引入dist/js/components/button.js文件, 就可以使用对应的fl-button组件了, 就是那么简单

  7. 测试

    方案1. 新建一个.html文件, 引入button.js文件, 看看fl-button是否能正常使用;

    web-component-ui/dist/index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <script type="module" src="./js/components/button.js"></script>
    </head>
    <body>
      <fl-button type="primary">基本按钮</fl-button>
      <fl-button type="success">成功样式</fl-button>
      <fl-button type="warning">警告样式</fl-button>
      <fl-button type="danger">危险样式</fl-button>
      <fl-button type="primary" disabled>禁用状态</fl-button>
    </body>
    </html>
    

    方案2: 直接在当前的vue项目中测试, 修改App.vue(yarn vite启动项目即可预览)

    web-component-ui/src/App.vue

    <template>
      <div>
        <fl-button type="primary">基本按钮</fl-button>
        <fl-button type="success">成功样式</fl-button>
        <fl-button type="warning">警告样式</fl-button>
        <fl-button type="danger">危险样式</fl-button>
        <fl-button type="primary" disabled>禁用状态</fl-button>
      </div>
    </template>
    
    <script setup lang="ts">
    // 导入即注册
    import "../dist/js/components/button.js";
    </script>
    
    <style scoped>
    
    </style>
    

示例代码已经放到gitee上了: gitee.com/LAMMUpro/ar…

就这么简单? no,no,no, 这只是一个开始, 后续章节会讲解出现的问题及解决方案