小程序软装: 组件库开发

本节概述

经过前面小节的学习,我们已经搭建起了小程序的编译构建环境,能够将我们开发的小程序项目编译成为对应的逻辑代码文件 logic.js,页面渲染文件 view.js,样式文件 style.css 和配置文件 config.json

在编译小程序的过程中,我们是将小程序的 WXML 文件中的组件节点编译为了 ui-xxx 格式的自定义组件,并将一些属性和事件处理进行了转化,这节开始我们就来针对性的完善我们的组件库.

搭建环境

这里我们的小程序页面渲染使用vue2来进行,首先我们使用vite来搭建一个开发环境,使用 @vitejs/plugin-vue2 来编译组件内容,并将输出为 iife 格式的文件: iife 格式的文件会将逻辑放在一个自执行的函数中进行,能够有效的避免全局的变量的冲突等问题;

为了加载方便,我们还可以把组件的样式也一起注入的输出的JS文件中,这样就可以少加载一个文件了。

ts 复制代码
import { defineConfig } from 'vite';
import path from 'path';
import vue from '@vitejs/plugin-vue2';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'

export default defineConfig({
  plugins: [
    vue(),
    cssInjectedByJsPlugin(), // 将css样式注入到JS中
  ],
  build: {
    lib: {
      entry: path.resolve(__dirname, './src/index.ts'),
      formats: ['iife'],
      name: 'components',
    },
    outDir: path.resolve(__dirname, 'dist'),
    rollupOptions: {
      external: ['vue']
    },
  },
  resolve: {
    extensions: ['.js', '.ts'],
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
});

统一处理组件事件绑定

在上一节我们的编译过程中,我们是将小程序的事件绑定如 bindtap="tapHandler(1, $event, true)" 编译为了下面格式:

ts 复制代码
attrs {
  bindtap: { methodName: "tapHandler", params: [1, "$event", true] }
}

那么最终就得有组件库来对这种 bind 开头的属性进行特殊处理,转化为vue的事件处理逻辑,并调用bridge进行事件传递;

当然在处理过程中,我们还需要对事件对象进行包装,小程序的事件对象里面主要有两块内容:

  • detail 节点的数据, 如input输入框的value等
  • currentTarget.dataset 当前节点上携带的 data-* 数据

这部分逻辑是在每个组件都需要用到的,这里我们将其封装为一个vue mixin

ts 复制代码
// 用于将属性名称转化为驼峰命名格式: 如 `test-handler` => testHandler
function toCamelCase(attr: string) {
  return attr.toLowerCase().replace(/-(.)/g, function (_, group) {
    return group.toUpperCase();
  });
}

// 获取节点属性上的 `data-*` 数据
function makeAttrParams(attrs: Record<string, any>) {
  const result = {};
  
  for (const attr in attrs) {
    if (!/^data-/.test(attr)) {
      continue;
    }
    
    const theAfter = attr.replace(/^data-/, '');
    const transAttr = toCamelCase(theAfter);
    result[transAttr] = attrs[attr];
  }

  return result;
}

export const miniAppMixin = {
  created() {
    for (let attr in (this as any).$attrs) {
      if (!/^bind/.test(attr)) {
        continue;
      }
      
      if (!(this as any).$attrs[attr]) {
        continue;
      }
      
      // 获取事件名称,如 "tap" 
      const eventName = attr.replace(/^bind/, '');
      const { methodName, params } = (this as any).$attrs[attr];
      // 这个会由 ui 线程创建Vue渲染实例的时候进行注入,用户获取到对应 bridge id,用于进行事件传递
      const { id } = (this as any).$vnode.context._bridgeInfo;

      (this as any).$on(eventName, (sysParams) => {
        // 构造小程序事件参数对象
        const _event = {
          detail: {
            ...sysParams,
          },
          currentTarget: {
            dataset: makeAttrParams((this as any).$attrs)
          }
        };
        // 组装事件参数
        const paramsList = params.map(param => {
          if (param === '$event') {
            return _event;
          }
          return param;
        });
        
        if (!paramsList.length) {
          paramsList.push(_event);
        }
        window.JSBridge.onReceiveUIMessage({
          type: 'triggerEvent',
          body: {
            methodName,
            id,
            paramsList
          }
        });
      });
    }
  }
}

编写组件

这里我们以 view 组件为例,其他组件类似我们就不全部实现,大家可前往文末点击本节代码前往查看

vue 复制代码
<template>
  <div class="ui-view" @click="clicked">
    <slot></slot>
  </div>
</template>

<script>
import { miniAppMixin } from '@/mixins';

export default {
  name: 'ui-view',
  mixins: [miniAppMixin],
  methods: {
    clicked() {
			this.$emit('tap');
		}
  }
}
</script>

<style lang="less" scoped>
.ui-view {
  display: block;
}
</style>

编写完组件后我们需要在入口文件进行统一注册:

ts 复制代码
import View from './components/view/index.vue';

const components = {
  'ui-view': View,
};
Object.keys(components).forEach(name => {
  // vue会通过全局进行引入
  window.Vue.component(name, components[name]);
});

小程序页面交互API

在小程序中,除了页面上的组件以外,还有一部分API也是能够控制页面内容显示的,如 showToast 能够控制页面弹出一个提示框,这部分作用于页面渲染的api我们也实现在组件库中。

ts 复制代码
interface ToastInfo {
  dom: HTMLElement | null;
  timer: number | null;
}
const toastInfo: ToastInfo = {
  dom: null,
  timer: null,
}

export function showToast(opts) {
  const title = opts.title;
  const duration = opts.duration || 1500;
  const icon = opts.icon || 'success';
  
  if (!title) return;
  
  // 移除旧的toast
  if (toastInfo.dom) {
    document.body.removeChild(toastInfo.dom);
    toastInfo.dom = null;
    clearTimeout(toastInfo.timer!);
  }
  
  // 创建toast组件的容器,并按照icon添加不同的状态类名
  toastInfo.dom = document.createElement('div');
  toastInfo.dom.classList.add('ui-toast', `ui-toast--${icon}`);
  toastInfo.dom.innerHTML = `<p>${title}</p>`;
  document.body.appendChild(toastInfo.dom);
  document.body.appendChild(toastInfo.dom);
  toastInfo.timer = setTimeout(() => {
    document.body.removeChild(toastInfo.dom!);
  }, duration) as unknown as number;
}

将API挂载到全局对象上:

ts 复制代码
import { showToast } from './showToast';

window.wxComponentsApi = {
  showToast,
}

这样我们的组件包就开发好啦,文章中组件只以 view 组件为例进行了实现,其他组件可前往本节代码仓库。

本节代码已上传至 Github: mini-wx-app

相关推荐
求知摆渡15 分钟前
共享代码不是共享风险——公共库解耦的三种进化路径
java·后端·架构
绅士玖16 分钟前
📝 深入浅出 JavaScript 拷贝:从浅拷贝到深拷贝 🚀
前端
MeteorSeed18 分钟前
别让理论成为“紧箍咒”!打破开发教条主义做正确的软件
架构
中微子25 分钟前
闭包面试宝典:高频考点与实战解析
前端·javascript
brzhang26 分钟前
前端死在了 Python 朋友的嘴里?他用 Python 写了个交互式数据看板,着实秀了我一把,没碰一行 JavaScript
前端·后端·架构
G等你下课1 小时前
告别刷新就丢数据!localStorage 全面指南
前端·javascript
该用户已不存在1 小时前
不知道这些工具,难怪的你的Python开发那么慢丨Python 开发必备的6大工具
前端·后端·python
爱编程的喵1 小时前
JavaScript闭包实战:从类封装到防抖函数的深度解析
前端·javascript
LovelyAqaurius1 小时前
Unity URP管线着色器库攻略part1
前端
Xy9101 小时前
开发者视角:App Trace 一键拉起(Deep Linking)技术详解
java·前端·后端