普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月8日首页

大厂面试官都在问的 WEUI Uploader,源码里藏了多少干货?🤔

作者 呆呆的心
2025年7月8日 13:18

作为前端开发,想进大厂光会写业务可不够 ——啃透优秀开源组件的源码,才是拉开差距的关键!今天就带大家扒一扒微信 WEUI Uploader 组件的源码,从 HTML 结构到 CSS 细节,手把手拆解那些让面试官眼前一亮的「神仙操作」✨

一、先看整体:这个上传组件到底长啥样?

WEUI Uploader 是一个支持图片 / 文件 / 视频上传的通用组件。整体设计遵循「移动端友好」原则,从布局到交互都透着大厂级的细节把控。

先放张简化的结构示意图(直观感受下):

.page(页面容器)
├─ .page__hd(头部)
└─ .page__bd(主体)
   └─ .weui-cells(表单容器)
      └─ .weui-cell(表单单元)
         └─ .weui-uploader(上传核心组件)
            ├─ .weui-uploader__hd(上传头部:标题+计数)
            └─ .weui-uploader__bd(上传区域:文件列表)

是不是一目了然?这种「嵌套式结构」正是大厂组件设计的精髓 ——职责分明,层级清晰🧩

二、HTML 结构:语义化 + BEM 命名,代码会「说话」📚

先看HTML的核心代码,里面藏着 2 个前端基本功的「教科书级示范」:

1. 语义化标签:不止是好看,更是专业

<header class="page__hd">...</header>
<main class="page__bd">...</main>

这里没用一堆<div>堆结构,而是用了<header>(头部)、<main>(主体)等语义化标签。好处是:

  • 机器能看懂:搜索引擎更易抓取内容,SEO 友好;
  • 人能看懂:同事接手代码时,不用猜「这个 div 是干嘛的」。

2. BEM 命名规范:类名即注释,维护不头秃

WEUI 的类名堪称 BEM 典范,比如:

  • weui-uploader:Block(块)—— 整个上传组件

  • weui-uploader__hd:Element(元素)—— 上传组件的头部(__连接块和元素)

  • weui-cell_uploader:Modifier(修饰符)—— 带上传功能的表单单元(_连接块和修饰符)

这种命名方式就像给代码贴标签,看到weui-uploader__info就知道是「上传组件的计数信息」,再也不用对着class="left-box"猜半天了😎

三、CSS 细节:这些「骚操作」让组件活起来🎨

CSS里的代码看似简单,实则全是移动端适配的「小心机」,挑 3 个最值得学的点:

1. 伪元素玩得溜:用::before 画分隔线,不污染 HTML

.weui-cells::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background-color: rgba(0,0,0,0.1);
}

用伪元素::before给表单容器加顶部边框,好处是:

  • HTML 里不用写额外的<div class="line">,结构更干净;
  • 边框样式改起来只动 CSS,符合「结构与样式分离」原则。

2. 移动端滚动优化:-webkit-overflow-scrolling 让滑动丝滑

.page {
  overflow: scroll;
  -webkit-overflow-scrolling: touch; /* 关键! */
}

这个属性专门为移动端设计:开启后滚动会带「惯性」,手指离开屏幕后还能滑一会儿,比普通滚动手感好 10 倍!而且只在 webkit 内核(安卓 / 苹果浏览器)生效,完美适配移动端🌍

3. float 布局的「复古用法」:多图排列还得靠它

.weui-uploader__file {
  float: left; /* 左浮动 */
  margin-right: 8px;
  margin-bottom: 8px;
  width: 96px;
  height: 96px;
}

虽然现在 flex/grid 是主流,但 float 在「多图瀑布流」场景下依然能打:

  • 图片左浮动,一行排满自动换行;

  • 用负 margin(.weui-uploader__bdmargin-right: -8px)抵消外层间距,避免最后一行右侧多出空白。

这种「旧技术新用法」的思路,面试官超爱考!💡

移动端效果图

image.png

四、核心技术点总结:5 个让你加分的知识点📝

  1. 语义化标签<header><main>提升可读性和 SEO;
  2. BEM 命名Block__Element--Modifier让类名有意义;
  3. 伪元素技巧:用::before/::after做装饰,不污染结构;
  4. 移动端滚动优化-webkit-overflow-scrolling: touch增强滑动体验;
  5. float 布局场景:多列排列 + 自动换行,复古但实用。

最后:源码学习的正确姿势是什么?

WEUI 这类大厂组件的源码,值得学的不只是「代码怎么写」,更是「为什么这么写」:

  • 为什么用 BEM?因为团队协作需要统一规范;

  • 为什么加 - webkit 前缀?因为要兼容移动端主流浏览器;

  • 为什么用 float 排图片?因为在低版本浏览器里更稳定。

吃透这些「设计思路」,下次自己写组件时,才能从「实现功能」升级到「做好体验」🚀

免费开源!微信小程序商城源码,快速搭建你的线上商城系统!

作者 一念杂记
2025年7月8日 11:54

由于近来有不少朋友提了一些关于微信小程序的需求,所以决定新增一些关于微信小程序的分享内容,结合实际的一些需求情况来看,决定优先分享关于商城小程序的内容。

今天介绍的是开发者lin-xin在几年前开源的一款微信小程序商城系统项目——wxapp-mall。虽然是几年前的项目了,但是个人看来不论是用来做二开,还是学习都是非常好的,所以在此分享给大家。

优点:

  • 具备商城常规的所有功能,首页、商品分类、详情、购物车、下单、个人中心、订单列表等等;
  • UI完整度较好;
  • 提供了uni-app版本,支持编译为app、小程序或者H5页面,覆盖全平台;
  • 开源易于定制开发;

项目地址:

github.com/lin-xin/wxa…

项目预览:

image.png

项目使用:

1、代码下载

使用以下命令将代码下载到本地:

git clone https://github.com/lin-xin/wxapp-mall.git

注:由于github少部分人不一定能顺利下载,也可以在文末直接下载项目源码。

2、导入项目

将下载到的项目代码,使用微信小程序开发者工具导入打开即可预览看到上述项目截图预览效果。

3、其他注意事项

  • API地址

由于项目确实有一定时间了,作者提供的API已经无法访问,需要我们自己根据自己业务更换地址使用,不影响页面交互和页面的逻辑计算。

  • uni分支

uni-app分支的代码下载路径为github.com/lin-xin/wxa…

代码下载

如果github无法下载代码,可以在以下链接中下载: mp.weixin.qq.com/s/3J_Gx7Uca…

写在最后

最近我会陆续增加分享一下微信小程序相关的用法和实用项目、技巧啥的,感兴趣的朋友欢迎关注,私信、点赞评论交流~

如果你也有更感兴趣的内容,也可以私信我,我也学习了解一下,充实充实个人不太聪明的脑子~

作为一个新手,如果让你去用【微信小程序通过BLE实现与设备通讯】,你会怎么做,

2025年7月8日 11:47

背景

作为一个程序员,基本上公司的需求就是你的技能。最近公司让我做一个与设备进行蓝牙通信的微信小程序。起初我一脸懵,但经过摸索,还是成功打通了 BLE 通信的完整流程。

适合第一次接触 BLE 的小程序开发者,本文将完整讲解连接、读写、监听的基础用法。

一、目标拆解

我们的目标是:
🔧 通过微信小程序连接蓝牙设备,并实现读写数据通信。

实现步骤大致如下:

  1. 初始化蓝牙模块
  2. 监听蓝牙模块状态
  3. 获取本机蓝牙状态
  4. 开启扫描设备
  5. 监听发现设备
  6. 停止扫描
  7. 连接设备
  8. 获取服务列表
  9. 获取特征值并监听通知
  10. 读写数据
  11. 断开连接&关闭蓝牙

二、准备工作

环境要求

  • 微信开发者工具(建议新版本)
  • 安卓或支持 BLE 的 iOS 手机(真机调试必须)
  • 一台支持蓝牙 BLE 的设备(或模拟器)

权限配置(app.json

"permission": {
  "scope.userLocation": {
    "desc": "用于蓝牙设备搜索"
  }
},
"requiredBackgroundModes": ["bluetooth"]

很重要,如果你没有配置,你会发现上线后,你会搜索不到设备或者直接使用不了蓝牙。

蓝牙权限

在真机中需要开启蓝牙和定位,安卓手机必须开启定位权限才能搜索到设备。

1、初始化蓝牙模块

目标:激活微信小程序蓝牙模块。

注意:安卓首次需授权蓝牙权限,IOS如果为开启蓝牙,将直接报错。

wx.openBluetoothAdapter({
  success() {
    console.log("蓝牙初始化成功");
    // 可继续 startBluetoothDevicesDiscovery
  },
  fail(err) {
    console.error("蓝牙初始化失败", err);
  }
});
2、监听蓝牙模块状态

目的:检查蓝牙是否被用户关闭,或临时断开

wx.onBluetoothAdapterStateChange((res) => {
  console.log("蓝牙状态变化:", res);
  if (!res.available) {
    wx.showToast({ title: '蓝牙不可用', icon: 'none' });
  }
});
3、获取本机蓝牙状态

目的:初始化时判断蓝牙是否可用。避免用户未开启蓝牙就触发后续流程。

wx.getBluetoothAdapterState({
  success(res) {
    if (!res.available) {
      wx.showToast({ title: '请开启蓝牙', icon: 'none' });
    }
  }
});
4、开始扫描设备

目的:搜索附近蓝牙设备,通常需设置allowDuplicatesKey: false,避免重复。

wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: falsesuccess() {
    console.log("开始搜索设备");
  }
});
5、监听发现设备

目的:拿到设备列表,根据设备名或广播字段筛选出目标设备。

wx.onBluetoothDeviceFound(function (res) {
  const devices = res.devices;
  devices.forEach(device => {
    console.log('发现设备:', device);
    if (device.name.includes('MyDevice')) {
      // 记录下来供点击连接
    }
  });
});
6、停止扫描

目的:找到设备后停止扫描,可以节省资源,防止冲突。

wx.stopBluetoothDevicesDiscovery();

7、连接蓝牙设备

目的:建立BLE连接,注意处理连接失败重试。

wx.createBLEConnection({
  deviceId: deviceId,
  success() {
    console.log("连接成功");
  },
  fail(err) {
    console.log("连接失败", err);
  }
});
8、获取服务列表

目的:获取设备的服务(service),筛选出主服务(一般不是isPrimary:false的跳过)

wx.getBLEDeviceServices({
  deviceId,
  success(res) {
    const primaryService = res.services.find(s => s.isPrimary);
    console.log('主服务', primaryService);
  }
});

9、获取特征值并监听通知

目的:获取通知特征值,用于接收设备传回的数据。

wx.getBLEDeviceCharacteristics({
  deviceId,
  serviceId,
  success(res) {
    const notifyChar = res.characteristics.find(item => item.properties.notify);
    wx.notifyBLECharacteristicValueChange({
      deviceId,
      serviceId,
      characteristicId: notifyChar.uuid,
      state: true
    });
  }
});

10、读写数据

目的:收发数据,完成通信。

// 写入数据
wx.writeBLECharacteristicValue({
  deviceId,
  serviceId,
  characteristicId,
  value: new Uint8Array([0x01, 0xA0]).buffer
});

// 监听数据返回
wx.onBLECharacteristicValueChange(function (res) {
  const buffer = res.value;
  const data = new Uint8Array(buffer);
  console.log('收到数据:', data);
});

11、断开连接&关闭蓝牙
wx.closeBLEConnection({
  deviceId,
  success() {
    console.log("🔌 连接断开");
  }
});

wx.closeBluetoothAdapter();

三、踩坑小结(新手必看)

问题 解决方式
安卓搜索不到设备 一定要打开位置权限
ios设备搜索不到服务 ios设备服务较少,需特定服务
发现设备列表 设备一定要去重,不然会出现几百条数据
设备MTU IOS一般都是512,但安卓一般的都是23,并且不支持修改,所以你发送的数据包字节数大于20的时候,是需要分包发送的,ios则是509
数据格式 每个设备的接受的数据格式是不一样的,这个你需要和嵌入式那边去定好协议
监听不到设备数据 必须先 notifyBLECharacteristicValueChange 成功后设备才上报数据
写入数据错误 数据类型必须是ArrayBuffer

四、总结

BLE开发并不复杂,难的是:不知从何下手,以及细节调试一堆。 这篇文章希望给初学者一个完整的思路,如果你也在学习BLE项目,欢迎留言交流。

下一篇:实现蓝牙列表点击连接,扫码连接,以及自动回连

五 代码实例(可直接用)

// 文件结构:
// ├── pages
// │   └── ble-demo
// │       └── index.js / index.json / index.wxml / index.wxss
// └── utils
//     └── ble.js

// ========== utils/ble.js ==========
const ble = {
    deviceId: null,
    serviceId: null,
    writeCharId: null,
    notifyCharId: null,
    /**
     * 打开蓝牙适配器
     */
    openAdapter() {
        return new Promise((resolve, reject) => {
            wx.openBluetoothAdapter({
                success: resolve,
                fail: reject
            });
        });
    },
    /**
     * 开始搜索蓝牙设备
     */
    startDiscovery() {
        return new Promise((resolve, reject) => {
            wx.startBluetoothDevicesDiscovery({
                allowDuplicatesKey: false,
                success: resolve,
                fail: reject
            });
        });
    },
    /**
     * 监听设备发现事件,并通过回调返回设备信息
     * @param {*} callback 
     */
    onDeviceFound(callback) {
        wx.onBluetoothDeviceFound(callback);
    },
    /**
     * 停止搜索设备
     */
    stopDiscovery() {
        return wx.stopBluetoothDevicesDiscovery();
    },
    /**
     * 创建蓝牙连接
     * @param {*} deviceId 
     */
    createConnection(deviceId) {
        this.deviceId = deviceId;
        return new Promise((resolve, reject) => {
            wx.createBLEConnection({
                deviceId,
                success: resolve,
                fail: reject
            });
        });
    },
    /**
     * 获取设备主服务
     */
    getPrimaryService() {
        return new Promise((resolve, reject) => {
            wx.getBLEDeviceServices({
                deviceId: this.deviceId,
                success(res) {
                    const primary = res.services.find(s => s.isPrimary);
                    ble.serviceId = primary.uuid;
                    resolve(primary);
                },
                fail: reject
            });
        });
    },
    /**
     * 获取设备特征值
     */
    getCharacteristics() {
        const that=this;
        return new Promise((resolve, reject) => {
            wx.getBLEDeviceCharacteristics({
                deviceId: this.deviceId,
                serviceId: this.serviceId,
                success(res) {
                    for (let char of res.characteristics) {
                        if (char.properties.write) ble.writeCharId = char.uuid;
                        if (char.properties.notify) ble.notifyCharId = char.uuid;
                        if (char.properties.notify || char.properties.indicate) {
                            that.enableNotify(char.uuid); // 启用特征值变化监听
                        }
                    }
                    resolve(res.characteristics);
                },
                fail: reject
            });
        });
    },
    /**
     * 开启特征值通知(监听蓝牙数据)
     * @param {*} characteristicId 
     */
    enableNotify(callback) {
        wx.notifyBLECharacteristicValueChange({
            deviceId: this.deviceId,
            serviceId: this.serviceId,
            characteristicId: this.notifyCharId,
            state: true,
            success: () => {
            },
            fail:()=>{

            }
        });
        wx.onBLECharacteristicValueChange(callback)
        
        // wx.onBLECharacteristicValueChange((characteristic) => {
        //     console.log(characteristic)
        //     const value = characteristic.value;
        //     const data = new Uint8Array(value);
        //     console.log(this.ab2hex(data))
        // })
    },
    // ArrayBuffer转16进度字符串示例
    ab2hex(buffer) {
        var hexArr = Array.prototype.map.call(
            new Uint8Array(buffer),
            function (bit) {
                return ('00' + bit.toString(16)).slice(-2)
            }
        )
        return hexArr.join('');
    },
    /**
     * 向设备写入数据(valueArray 是一个字节数组)
     * @param {*} valueArray 
     */
    write(valueArray) {
        let buffer = new Uint8Array(valueArray).buffer;
        return new Promise((resolve, reject) => {
            wx.writeBLECharacteristicValue({
                deviceId: this.deviceId,
                serviceId: this.serviceId,
                characteristicId: this.writeCharId,
                value: buffer,
                success: resolve,
                fail: reject
            });
        });
    },
    /**
     * 获取设备MTU大小
     * @param {*} mtu 
     */
    requestMTU() {
        return new Promise((resolve, reject) => {
            wx.getBLEMTU({
                deviceId: this.deviceId,
                success: (res) => {
                    let mtu = res.mtu;
                    let maxWriteSize = Math.min(mtu - 3, 509); // **最大单次可写入的数据**
                    console.log(`✅ 设备 MTU: ${mtu}, 单次最大可写: ${maxWriteSize}`);
                    resolve({
                        mtu,
                        maxWriteSize
                    })
                },
                fail: (err) => {
                    reject(err)
                }
            });
        });
    },
    /**
     * 断开蓝牙连接
     */
    closeConnection() {
        return new Promise((resolve, reject) => {
            wx.closeBLEConnection({
                deviceId: this.deviceId,
                success: resolve,
                fail: reject
            });
        });
    },
    /**
     * 工具方法:将十六进制字符串转换为字节数组
     * @param {*} hexStr 
     */
    hexStringToBytes(hexStr) {
        if (!hexStr || typeof hexStr !== 'string') return [];
        return hexStr.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
    },
};

module.exports = ble;
// ========== pages/ble-demo/index/index.js ==========
const ble = require('../../../utils/ble');
Page({

    /**
     * 页面的初始数据
     */
    data: {
        log: '',
        foundDevice: null
    },
    log(msg) {
        this.setData({
            log: this.data.log + `\n` + msg
        });
    },
    /**
     * 初始化 BLE 并扫描设备
     */
    async startBLE() {
        try {
            this.log('开始初始化蓝牙');
            await ble.openAdapter();
            this.log('蓝牙模块已打开');
            await ble.startDiscovery();
            this.log('正在扫描设备...');
            this.discoveredIds = new Set();
            ble.onDeviceFound(res => {
                 /**
                 * 增加筛选条件,不然会出现很多设备
                 * 可以是名字,也可以是UUID
                 * 还可以是值搜索有广播的数据
                 * 添加去重
                 */
                const devices = res.devices || [res.device];
                devices.forEach(device => {
                    if (!device.name || !device.name.includes('TPS-22C1')) return;
                    if (this.discoveredIds.has(device.deviceId)) return; // 跳过重复设备

                    this.discoveredIds.add(device.deviceId); // 添加到已发现集合
                    this.setData({ foundDevice: device });
                    ble.stopDiscovery();
                    this.log(`发现设备: ${device.name} 设备id:(${device.deviceId})`);
                });
               
            });

        } catch (e) {
            this.log('初始化失败: ' + e.errMsg);
        }
    },
    /**
     * 与设备建立连接
     */
    async connectBLE() {
        try {
            const {
                foundDevice
            } = this.data;
            if (!foundDevice) return;

            this.log('正在连接设备...');
            await ble.createConnection(foundDevice.deviceId);
            await ble.getPrimaryService();
            await ble.getCharacteristics();

            // 协商 MTU 提高数据传输效率
            const mtuInfo = await ble.requestMTU();
            this.log(`MTU: ${mtuInfo.mtu}, 单次最大可写: ${mtuInfo.maxWriteSize}`);
            ble.enableNotify(res => {
              
                console.log(res)
                const value = res.value;
                const data = new Uint8Array(value);
                this.log('监听设备返回数据: ' + ble.ab2hex(data));
                console.log(ble.ab2hex(data))
            });
            this.log('设备连接成功');

        } catch (e) {
            this.log('连接失败: ' + e.errMsg);
        }
    },
    /**
     * 发送测试数据(0x01, 0xA0)
     */
    sendTestData() {
        const hex = '5AA5060000';
        const data = ble.hexStringToBytes(hex);
        ble.write(data).then(() => {
            this.log('指令已发送: 5AA5060000');
        }).catch(err => {
            this.log('指令发送失败: ' + err.errMsg);
        });
    },
    /**
     * 断开设备连接
     */
    disconnectBLE() {
        ble.closeConnection().then(() => {
            this.log('已断开连接');
        }).catch(err => {
            this.log('断开失败: ' + err.errMsg);
        });
    },
    
})

// ========== pages/ble-demo/index/index.wxml ==========
<view class="container">
    <button bindtap="startBLE">初始化并扫描</button>
    <button bindtap="connectBLE">连接设备</button>
    <button bindtap="sendTestData">发送测试数据</button>
    <button bindtap="disconnectBLE">断开连接</button>
    <view class="log-area">{{log}}</view>
</view>

// ========== pages/ble-demo/index/index.wxss ==========
.container {
    padding: 20rpx;
    width: 96%;
}

.log-area {
    margin-top: 30rpx;
    white-space: pre-wrap;
    font-size: 28rpx;
    background: #333333;
    padding: 20rpx;
    border-radius: 12rpx;
    max-height: 600rpx;
    overflow-y: scroll;
    color: #ffffff;
}
❌
❌