<center> javascript-适配器模式</center>

本文探讨了适配器模式的概念及应用,通过实例说明如何解决接口不兼容问题,对比装饰者、代理和外观模式,强调适配器模式的独特价值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

将一个类(对象)的接口(方法或属性)转化成另一个接口,以满足用户需求,使类(对象)之间接口的不兼容性问题通过适配器方法得以解决

适配器主要有3个角色组成:
(1)客户端:调用接口的类
(2)适配器:用来连接客户端接口和提供服务的接口的类
(3)适配者:提供服务,但是却与客户端接口需求不兼容服务类。

下面是一个实例,向googleMap和baiduMap都发出“显示”请求时,googleMap和baiduMap分别以各自的方式在页面中展现了地图:

var googleMap = {
    show: function(){
        console.log( '开始渲染谷歌地图' );
    }
};
var baiduMap = {
    show: function(){
        console.log( '开始渲染百度地图' );
    }
};
var renderMap = function( map ){
    if ( map.show instanceof Function ){
        map.show();
    }
};

renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMap ); // 输出:开始渲染百度地图

这段程序得以顺利运行的关键是googleMap和baiduMap提供了一致的show方法,但第三方的接口方法并不在控制范围之内,假如baiduMap提供的显示地图的方法不叫show而叫display呢?

baiduMap这个对象来源于第三方,正常情况下都不应该去改动它。此时可以通过增加baiduMapAdapter来解决问题:

var googleMap = {
    show: function(){
        console.log( '开始渲染谷歌地图' );
    }
};
var baiduMap = {
    display: function(){
        console.log( '开始渲染百度地图' );
    }
};
var baiduMapAdapter = {
    show: function(){
        return baiduMap.display();

    }
};

renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMapAdapter ); // 输出:开始渲染百度地图

再看看另一个例子。假设正在编写一个渲染北京市地图的页面。目前从第三方资源里获得了北京市的所有地区以及它们所对应的ID,并且成功地渲染到页面中:

var getBeijingCity = function(){
    var beijingCity = [
    {
        name: 'chaoyang',
        id: 11,
    }, {
        name: 'haidian',
        id: 12,
    }
    ];
    return beijingCity;
};
var render = function( fn ){
    console.log( '开始渲染北京市地图' );
    document.write( JSON.stringify( fn() ) );
};
render( getBeijingCity );

利用这些数据,编写完成了整个页面,并且在线上稳定地运行了一段时间。但后来发现这些数据不太可靠,里面还缺少很多地区。于是又在网上找到了另外一些数据资源,这次的数据更加全面,但遗憾的是,数据结构和正运行在项目中的并不一致。新的数据结构如下:

var BeijingCity = {
    chaoyang: 11,
    haidian: 12,
    pinggu: 13
};

除了大动干戈地改写渲染页面的前端代码之外,另外一种更轻便的解决方式就是新增一个数据格式转换的适配器:

var getBeijingCity = function(){
    var beijingCity = [
    {
        name: 'chaoyang',
        id: 11,
    }, {
        name: 'haidian',
        id: 12,
    }

    ];
    return beijingCity;
};
var render = function( fn ){
    console.log( '开始渲染北京市地图' );
    document.write( JSON.stringify( fn() ) );
};
var addressAdapter = function( oldAddressfn ){
    var address = {},
    oldAddress = oldAddressfn();
    for ( var i = 0, c; c = oldAddress[ i++ ]; ){
        address[ c.name ] = c.id;
    }
    return function(){
        return address;
    }
};
render( addressAdapter( getBeijingCity ) );

那么接下来需要做的,就是把代码中调用getBeijingCity的地方,用经过addressAdapter适配器转换之后的新函数来代替

总结

适配器模式是一对相对简单的模式。有一些模式跟适配器模式的结构非常相似,比如装饰者模式、代理模式和外观模式。这几种模式都属于“包装模式”,都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够使它们协同作用。装饰者模式和代理模式也不会改变原有对象的接口,但装饰者模式的作用是为了给对象增加功能。装饰者模式常常形成一条长的装饰链,而适配器模式通常只包装一次。代理模式是为了控制对对象的访问,通常也只包装一次。外观模式的作用倒是和适配器比较相似,有人把外观模式看成一组对象的适配器,但外观模式最显著的特点是定义了一个新的接口

能不能在代码中帮我增加动态申请蓝牙权限的相关代码,在打开蓝牙适配器的时候,增加蓝牙权限的动态申请功能;这是源代码<template> <view> <view class="uni-list"> <!-- 隐藏画布,用于标签绘制 --> <canvas :id="canvasId" :canvas-id="canvasId" type="2d" :style="{ width: labelWidth + 'px', height: labelHeight + 'px' }" style="position: fixed; left: -999999rpx; top: -999999rpx" /> <!-- 打印机列表 --> <view class="uni-list-cell"> <view class="uni-list-cell-left">打印机:</view> <view class="uni-list-cell-db"> <picker :value="deviceIndex" :range="deviceList" @change="onDeviceChanged" range-key="name"> <view class="uni-input">{{ deviceList[deviceIndex].name }}</view> </picker> </view> </view> </view> <view class="uni-padding-wrap uni-common-mt"> <!-- 设备搜索 --> <button type="primary" @click="startDiscovery">开始搜索打印机</button> <button type="primary" @click="stopDiscovery">停止搜索打印机</button> <!-- 链接打印机 --> <button type="primary" @click="openPrinter">打开打印机</button> <button type="primary" @click="closePrinter">关闭打印机</button> <!-- 标签编辑及打印 --> <button type="primary" plain="true" @click="onPrintTest">打印测试</button> </view> <view style="text-align: center; padding: 10px"> <view style="margin: 10px; border: solid lightgray 1px"> <image v-for="item in previewList" :src="item.value" :key="item.key" class="image" mode="widthFix" style="margin: 10rpx; border: dashed lightgray 1px" /> </view> </view> </view> </template> <script> import { LPAPIFactory, LPA_Result, LPAUtils } from "@/uni_modules/dothan-lpapi-ble/js_sdk/index.js"; // export default { data() { return { canvasId: "lpapi-ble-uni1", labelWidth: 960, labelHeight: 960, deviceList: [{ name: "未检测到打印机" }], deviceIndex: 0, isAppPlus: false, isWeiXin: false, isLark: false, isAlipay: false, isDingTalk: false, /** * 图片预览列表 * @type {{value: string; key: string;}[]} ***/ previewList: [], threshold: 128, }; }, onLoad() { consol
04-02
Error: Please import the top-level fullcalendar lib before attempting to import a plugin.下面是我的代码,并且使用的版本一致<template> <!-- 办公-我的日程页面 --> <div class="divBox"> <el-card body-style="padding:0 0 0 0; " class="normal-page"> <div class="header"> <span class="title">我的日程</span> <el-button type="primary" size="small" :icon="Plus" @click="addSchedule"> {{ $t('calendar.newschedule') }} </el-button> </div> <el-row :gutter="14"> <el-col :span="7"> <calendar-bar ref="calendarBar" @handle-date="handleDate" /> </el-col> <el-col :span="17" class="line" v-loading="loading" element-loading-text="数据正在加载中"> <div class="plan-tabs-content"></div> <!-- 我的日历 --> <div class="needToBeDealt"> <div class="add-btn"> <div class="content-right"></div> <div class="content-button"> <el-radio-group v-model="time" size="small" @change="selectChange"> <el-radio-button v-for="(itemn, indexn) in fromList" :key="indexn" :label="itemn.val" > {{ itemn.text }} </el-radio-button> </el-radio-group> </div> </div> <FullCalendar ref="calendar" id="calendar" style="height: calc(100vh - 150px)" :options="calendarOptions"> <template v-slot:slotLabelContent="arg"> <div class="calendar-timeline"> <!-- <span v-if="moment(arg.date).format('HH:mm') !== '00:00'">--> <!-- {{ moment(arg.date).format('HH:mm') }}--> <!-- </span>--> </div> </template> <template v-slot:dayHeaderContent="arg"> <div class="header-box"> <div class="day-header" :class="period == 1 ? 'ml30' : ''"> <!-- <span class="week">{{ getWeek(arg.date) }}</span>--> <!-- <span class="date" v-if="period !== 3">{{ moment(arg.date).format('D') }}</span>--> </div> </div> </template> <template v-slot:eventContent="arg"> <div class="item" :style="{ background: period == 3 ? getColorFn(arg.event.textColor, '0.1') : arg.event.color }" > <div class="flex over-title" @click="handleEventClick(arg)"> <img v-if="period !== 2" :src="arg.event.extendedProps.avatar" class="img" alt="" /> <div class="over-title" :style="{ color: period == 3 ? arg.event.textColor : arg.event.textColor }" > {{ arg.event.title }} </div> </div> <template v-if="arg.event.extendedProps.show > -1"> <span v-if="arg.event.extendedProps.finish == 2" class="el-icon-error" :style="{ color: arg.event.textColor }" ></span> <span v-else-if="arg.event.extendedProps.finish == 3" @click="putStatus('取消完成', 1, arg.event)" class="el-icon-success" :style="{ color: arg.event.textColor }" ></span> <span v-else class="yuan" :style="{ 'border-color': arg.event.textColor }" @click="putStatus('完成', 3, arg.event, arg)" ></span> </template> </div> </template> </FullCalendar> </div> </el-col> </el-row> </el-card> <!-- 通用弹窗表单 --> <add-todo ref="addTodo" @get-list="getList" :left-time="leftTime"></add-todo> <calendar-details ref="calendarDetails" @delete-fn="getList" @edit-fn="editFn" :date-info="dateInfo" ></calendar-details> <contract-dialog ref="contractDialog" :config="configContract" @is-ok="getList"></contract-dialog> <!-- 跟进弹窗 --> <el-dialog title="添加跟进记录" class="record" v-model="dialogVisible" width="40%"> <record-upload :form-info="formInfo" @change="recordChange"></record-upload> </el-dialog> <edit-examine ref="editExamine" :parameter-data="parameterData" :ids="formInfo.data.id" @schedule-record="scheduleRecord" ></edit-examine> </div> </template> <script> import FullCalendar from '@fullcalendar/vue3' // 核心库必须第一个导入 import dayGridPlugin from '@fullcalendar/daygrid' import timeGridPlugin from '@fullcalendar/timegrid' import interactionPlugin from '@fullcalendar/interaction' import listPlugin from '@fullcalendar/list' import { defineComponent, defineAsyncComponent, ref, reactive, onMounted, computed, toRefs, nextTick } from 'vue'; import { Plus } from '@element-plus/icons-vue'; //import { ElMessage } from 'element-plus'; import moment from 'moment'; // 正确顺序:先核心库再插件 // 样式导入(必须在插件之后) // import '@fullcalendar/common/main.css' // import '@fullcalendar/daygrid/main.css' // import '@fullcalendar/timegrid/main.css' // import '@fullcalendar/list/main.css' console.log(window.FullCalendar); // 检查全局对象 console.log(window.FullCalendarCore); // 如果你手动挂载了核心库 // 根据实际需要取消注释这些API导入 // import { // clientRemindDetailApi, // scheduleListApi, // scheduleStatusApi // } from '@/api/enterprise'; // import { configRuleApproveApi } from '@/api/config'; // import { toGetWeek, getColor } from '@/utils/format'; export default defineComponent({ name: 'WorkDealt', components: { CalendarBar: defineAsyncComponent(() => import('./components/calendarBar.vue')), AddTodo: defineAsyncComponent(() => import('./components/addTodo')), FullCalendar, CalendarDetails: defineAsyncComponent(() => import('./components/calendarDetails')) // ContractDialog: defineAsyncComponent(() => import('@/views/customer/contract/components/contractDialog')), // RecordUpload: defineAsyncComponent(() => import('@/views/customer/list/components/recordUpload')), // EditExamine: defineAsyncComponent(() => import('@/views/user/examine/components/editExamine')) }, setup() { const state = reactive({ loading: false, calendarApi: null, title: '', time: '1', cid: null, dialogVisible: false, configContract: {}, formInfo: { avatar: '', type: 'add', show: 1, data: {}, follow_id: 0 }, fromList: computed(() => [ {text: '日', val: '1'}, {text: '周', val: '2'}, {text: '月', val: '3'} ]), start_time: '', end_time: '', dateInfo: {}, type: [], period: 1, calendarOptions: { height: 'calc(100vh - 232px)', eventColor: '', initialDate: moment().format('YYYY-MM-DD HH:mm:ss'), plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin], handleWindowResize: true, displayEventTime: false, slotDuration: '00:60:00', scrollTime: '05:30:00', slotMinTime: '00:00:00', slotMaxTime: '24:00:00', headerToolbar: { left: '', center: 'prev title next', right: '' }, buttonText: { month: '月', week: '周', day: '天' }, allDaySlot: true, dayMaxEventRows: 6, allDayText: '', moreLinkContent: state => `还有${state.num}个日程`, weekends: true, nowIndicator: true, weekNumbers: false, slotLabelFormat: { hour: '2-digit', minute: '2-digit', meridiem: false, hour12: false }, selectable: true, displayEventEnd: false, initialView: 'timeGridDay', dateClick: (e) => handleDateClick(e), customButtons: { next: { text: 'PREV', click: () => next() }, prev: { text: 'PREV', click: () => prev() } }, events: [], slotEventOverlap: false, eventOverlap: false, locale: 'zh-cn', weekNumberCalculation: 'ISO' }, detailedData: {}, parameterData: { contract_id: '', customer_id: '', invoice_id: '', bill_id: '' }, leftTime: '', buildData: [], item: {}, status: 0 }) const calendarBar = ref(null); const addTodo = ref(null); const calendarDetails = ref(null); const contractDialog = ref(null); const editExamine = ref(null); const userId = ref(null); // 日期更新函数 const updateCalendarDates = () => { if (!state.calendarApi) return; state.title = moment(state.calendarApi.view.currentStart).format('YYYY-MM-DD'); state.start_time = state.title + ' 00:00:00'; state.end_time = state.title + ' 23:59:59'; }; onMounted(() => { const userInfo = JSON.parse(localStorage.getItem('userInfo')); if (userInfo) { userId.value = userInfo.userId; state.formInfo.avatar = userInfo.avatar; } // 使用 nextTick 确保在 DOM 更新后执行 nextTick(() => { if (calendarBar.value && calendarBar.value.getApi) { state.calendarApi = calendarBar.value.getApi(); dayRender(); updateCalendarDates(); } }); }); // 原始方法开始(保持所有方法逻辑不变) // const handleEventClick = e => { calendarDetails.value.open(e.event.extendedProps); } const recordChange = () => { state.dialogVisible = false; getList(); } const getConfigApprove = async () => { // 实际项目中取消注释 // const result = await configRuleApproveApi(0); // state.buildData = result.data; } const putStatus = async (text, status, item) => { const {extendedProps} = item; state.item = extendedProps; const bill_id = extendedProps.bill_id; switch (extendedProps.cid_value) { case 3: case 4: const id = bill_id.bill_id !== 0 ? bill_id.bill_id : bill_id.remind_id; if (status == 3) { // 实际项目中取消注释 // clientRemindDetailApi(id).then((res) => { // res.data.id = bill_id.remind_id; // state.parameterData.customer_id = res.data.eid; // state.parameterData.contract_id = res.data.cid; // // const switchType = res.data.types === 0 ? 'contract_refund_switch' : 'contract_renew_switch'; // const data = {id: state.buildData[switchType]}; // editExamine.value.open(data, res.data.cid, switchType, item, status); // }) } break; case 2: if (extendedProps.finish === 3) return false; state.formInfo.follow_id = bill_id ? bill_id.follow_id : 0; state.formInfo.data.eid = extendedProps.link_id; state.dialogVisible = true; break; default: scheduleRecord(extendedProps, status); } } const scheduleRecord = async (data = state.item, status = state.status) => { const {start_time: start, end_time: end, itemId: id} = data; const info = {status, start, end}; // 实际项目中取消注释 // await scheduleStatusApi(id, info); await getList(); } const handleDateClick = e => { state.detailedData = { startDate: moment(e.date).format('YYYY-MM-DD'), startTime: moment(e.date).format('HH:mm:ss'), endDate: moment(e.date).format('YYYY-MM-DD'), time: moment(e.date).format('YYYY-MM-DD HH:mm:ss') } addTodo.value.open(state.detailedData); } const dayRender = () => { document.documentElement.style.setProperty(`--fc-today-bg-color`, '#fff'); let dayTime = document.querySelectorAll('.fc-timegrid-slot-label-cushion'); dayTime[0]?.classList.add('fcTime'); document.querySelectorAll('.fc-button-primary').forEach(li => li.setAttribute('title', '')); } const day = () => { state.period = 1; state.calendarApi.changeView('timeGridDay'); getList(); removeTitle(); dayRender(); } const month = () => { state.period = 3; state.calendarApi.changeView('dayGridMonth'); getList(); removeTitle(); } const week = () => { state.period = 2; state.calendarApi.changeView('timeGridWeek'); getList(); removeTitle(); dayRender(); } const prev = () => { state.calendarApi.prev(); updateCalendarDates(); getList(); if (state.period === 2) { const date = calendarBar.value.value; calendarBar.value.value = moment(date).subtract(7, 'd').format('YYYY-MM-DD'); } else { calendarBar.value.value = moment(state.calendarApi.view.currentStart).format('YYYY-MM-DD'); } } const next = () => { state.calendarApi.next(); updateCalendarDates(); getList(); if (state.period == 2) { let date = calendarBar.value.value; calendarBar.value.value = moment(date).add(7, 'd').format('YYYY-MM-DD'); } else { calendarBar.value.value = moment(state.calendarApi.view.currentStart).format('YYYY-MM-DD'); } } const getList = () => { // 实际项目中取消注释 // scheduleListApi().then(res => { // let newArr = [] // res.data.forEach(item => { // // ...处理逻辑... // }) // state.calendarOptions.events = newArr // }) calendarBar.value?.getList(); } const findItem = (arr, key, val) => { for (var i = 0; i < arr.length; i++) { if (arr[i].id === val || arr[i].id == userId.value) { return 2; } } return -1; } // const getWeek = date => toGetWeek(date); const editFn = (id, type, date) => { let data = { id, type, edit: true, date } addTodo.value.openBox(data); } const handleDate = (data, val) => { state.cid = data.type; state.leftTime = data.time; if (val) { state.calendarApi.gotoDate(data.time); } setTimeout(() => { state.calendarApi = calendarBar.value.getApi(); state.time = 1; day(); getList(); }, 300); } const selectChange = e => { state.time = e; if (state.time == 1) { day(); } else if (state.time == 2) { week(); } else { month(); } } const addSchedule = () => addTodo.value.open(); // 临时 getColorFn 实现 - 实际项目中应该使用真实实现 const getColorFn = (thisColor, thisOpacity) => { // 如果是透明背景 if (state.period === 3) { // RGBA 格式 const r = parseInt(thisColor.slice(1, 3), 16); const g = parseInt(thisColor.slice(3, 5), 16); const b = parseInt(thisColor.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${thisOpacity})`; } return thisColor; } const removeTitle = () => { setTimeout(() => { document.querySelectorAll('.fc-daygrid-day-bottom a').forEach(li => li.title = ''); }, 300); } return { ...toRefs(state), Plus, calendarBar, addTodo, calendarDetails, contractDialog, editExamine, handleEventClick, recordChange, putStatus, scheduleRecord, handleDateClick, dayRender, day, month, week, prev, next, getList, findItem, // getWeek, editFn, handleDate, selectChange, addSchedule, getColorFn, removeTitle } } }) </script> <style lang="scss" scoped> .divBox { :deep(.fc-header-toolbar) { position: absolute; top: 0; left: 0; } .header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dcdfe6; .title { font-weight: 500; font-size: 18px; color: #303133; } } .day-header { display: flex; flex-direction: column; justify-content: center; height: 52px; font-family: PingFang SC-Regular, PingFang SC; line-height: 20px; .week { font-size: 13px; color: #909399; } .date { font-size: 18px; color: #606266; font-weight: 800; } } .ml30 { margin-left: -30px; } :deep(.fc-timegrid-slot-label-cushion) { color: #909399; font-size: 12px; } .line { border-left: 1px solid #f0f2f5; } .item { width: 100%; display: flex; font-size: 14px; font-family: PingFang SC-Regular, PingFang SC; justify-content: space-between; align-items: center; padding-right: 4px; border-radius: 13px; .img { flex-shrink: 0; width: 18px; height: 18px; border-radius: 50%; margin-right: 5px; object-fit: cover; } .yuan { width: 12px; height: 12px; border-radius: 50px; border: 1px solid #fff; } } .over-title { width: 98% !important; overflow: hidden !important; white-space: nowrap; text-overflow: ellipsis !important; } } </style>
最新发布
06-07
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值