本节概述
经过前面小节的学习,我们已经搭建起了小程序的编译构建环境,能够将我们开发的小程序项目编译成为对应的逻辑代码文件 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