我用 React Native 开发了 POS 系统
这是我将要撰写的一系列文章的第一部分,这些文章将解释使用 React Native 为 Android 开发的定制 POS 应用程序的进展情况。
语境
我妻子刚开了一家猫咪咖啡馆,人们可以在那里一边喝咖啡一边看猫咪玩耍。咖啡馆里还有一家商店,顾客可以在那里购买猫咪相关的商品,比如吊坠、T恤等等。
在研究了市面上各种POS系统之后,我决定自己开发一个。这是一个业余项目,我利用周末和休息时间投入了近200个小时,目前仍在开发中,我们会不断添加新功能。
因此,主要要求是:
- 在安卓平板电脑(Galaxy Tab A10)上运行
- 使用蓝牙打印机进行打印。
- 跟踪时间客户与猫在一起(一小时、半小时等)。
- 跟踪每张工单上的客户数量。
- 通过每日开/关流程追踪抽屉里的现金流。
- 接受预订。
- 允许在订单中添加一次性商品。
- 管理长期产品(咖啡、茶、马克杯等)。
预习
当然,市面上那么多POS系统,谁会傻到自己写一套呢?一开始,我们尝试从不同的公司购买整套解决方案,但都超出了预算,而且我们担心很多需求都无法满足。所以我们去了亚马逊[1],花了将近100欧元买了一台蓝牙打印机和一个钱箱。因为我们家里已经有好几台平板电脑,所以光这一点就省了200欧元。那些带触摸屏的Windows POS系统价格可不便宜。
我仍然不想搞砸POS系统,于是就去了Google Play商店。那里有很多很棒的应用程序,比如Loyverse。但也有很多应用程序,嗯,只能说它们又丑又复杂,花的时间还不如我自己开发一个。最后,没有一个符合我们的要求,所以只好重新开始设计。
React Native
这并非我第一次开发 Android 或 React Native 应用,所以我很清楚自己会遇到什么问题,我最担心的是蓝牙打印机。幸运的是,我们拿到的这台打印机附带了 iOS、Android、Windows 和 Linux 的源代码。
所以,大部分蓝牙代码都已经完成了,剩下的就是清理代码,以免我的眼睛受罪,然后将 Android 端与 React Native 端连接起来。
接下来我担心的是 SQLite 数据库,我之前只在 Android 上用过,没在 React Native 里用过。经过一番研究,我找到了react-native-sqlite-storage,它能满足我的需求,不需要数百个依赖项,也不会给我带来任何麻烦。更多内容将在第二部分介绍。
另一个令人担忧的问题是货币和日期的处理,但是拜托,现在都 2017 年了,我们有 Moment 和Big,它们运行得非常完美。
以下是所有使用的依赖项:
- react-native 0.47.2
- Big.js 用于处理十进制数。
- 处理日期的时刻到了。
- react-navigation(我喜欢这个)
- react-native-vector-icons 和 Material Icons(没有它们,项目就无法启动)
- react-native-sqlite-storage
- react-native-fs
为什么不选择 Android 原生应用?
我想到这一点,但当你开发过一些 React Native 应用之后,你就会明白,用 JavaScript 实现 UI 和业务逻辑比用 Java 容易得多。此外,开发流程也快得多,因为rr在模拟器上点击一下就能刷新所有更改,无需等待构建过程。
特别是得益于 Flex,用户界面实现起来容易得多。当然,它也有一些局限性,比如某些方面,TextInput但这些对本项目来说并不是什么大问题。
蓝牙打印机
我的Android源代码文件夹最终变成了这样:
.
└── com
└── polinekopos
├── bt
│  ├── BTModule.java
│  ├── BTModulePackage.java
│  └── sdk
│  ├── BluetoothService.java
│  ├── Command.java
│  ├── PrinterCommand.java
│  └── PrintPicture.java
├── MainActivity.java
└── MainApplication.java
包装盒里sdk包含了打印机的所有配件,所以我只需要用它们创建 React Native 界面即可。文件夹btsdk.jar里还有一个文件,其中包含其他依赖项libs。
BluetoothService这里是所有神奇之处发生的地方。在仔细阅读代码并进行了一些重构(例如添加一个监听线程来通知断开连接事件)之后,我对蓝牙打印机的通信机制有了很好的理解。简而言之,它涉及大量的字节和缓冲区。
BTModule管理此服务与 React Native 应用之间的所有交互:
public class BTModule extends ReactContextBaseJavaModule {
public BTModule(ReactApplicationContext reactContext) {
...
}
@ReactMethod
public void connect(String address, Promise promise) { ... }
@ReactMethod
public void disconnect(Promise promise) { ... }
@ReactMethod
public void print(String message, Promise promise) { ... }
@ReactMethod
public void getState(Promise promise) { ... }
private void sendEvent(String eventName) { ... }
}
这些方法非常简单直接,React Native 可以使用。其他内容请参阅React Native 文档的相应章节。
此外,还有一些修改用于MainActivity暂停和恢复与打印机的连接:
@Override
public synchronized void onResume() {
super.onResume();
if (mBluetoothAdapter == null) {
return;
}
if (BluetoothService.get().getState() == BluetoothService.STATE_NONE) {
BluetoothService.get().start();
}
}
在 React Native 端,实现起来非常简单。顶部栏上有一个连接/断开连接的按钮,该按钮还会监听蓝牙事件并将字符串发送到打印机以显示连接状态。
export class Banner extends React.Component {
state = {
bluetooth: 'bt-unknown'
};
componentWillMount() {
events.forEach((e) => DeviceEventEmitter.addListener(e, () => this.setState({bluetooth: e})));
}
componentWillUnmount() {
events.forEach((e) => DeviceEventEmitter.removeAllListeners());
}
getBluetoothIcon() {
let icon = null;
switch (this.state.bluetooth) {
case 'bt-unknown': icon = IconFactory.Icons.BTUnknown; break;
case 'bt-connecting': icon = IconFactory.Icons.BTConnecting; break;
case 'bt-connected': icon = IconFactory.Icons.BTConnected; break;
case 'bt-disconnect': icon = IconFactory.Icons.BTDisconnected; break;
}
return IconFactory.build(icon, styles.drawerIcon);
}
async onBluetooth() {
const bluetooth = await BTModule.getState();
this.setState({bluetooth: bluetooth});
if(bluetooth !== 'bt-connected'){
const id = ConfigManager.of().getBluetoothID();
try {
const deviceName = await BTModule.connect(id);
AlertFactory.info(Messages.get('message.bluetooth.connected'));
} catch (e) {
AlertFactory.error(Messages.get('error.bluetooth.connection'));
}
}
}
render() {
return (
// ...
<View style={styles.bannerRight}>
<TouchableHighlight
underlayColor='transparent'
style={styles.drawerButton}
onPress={this.onBluetooth.bind(this)}>
{ this.getBluetoothIcon() }
</TouchableHighlight>
</View>
);
}
}
这里DeviceEventEmitter管理着从应用程序原生端通过sendEvent定义的方法发送的事件BTModule,并使用接收到的状态渲染视图。这样,蓝牙图标就能始终保持最新状态。
该onBluetooth函数绑定到按钮上,用于检查是否断开连接并尝试重新连接。无论发生什么情况,都会向用户显示提示信息。
该IconFactory对象负责管理和渲染react-native-vector-icons提供的图标。
打印操作相当简单,虽然需要用到很多工具padStart来padEnd对齐内容。总之,在创建包含所有行的数组之后,write只需调用该函数即可:
async print() {
// Some margin
await BTModule.print(' ');
await BTModule.print(' ');
// Now the ticket
this.lines.forEach((line) => BTModule.print(line));
// Cut margin
await BTModule.print(' ');
await BTModule.print(' ');
}
第一部分到此结束,希望您喜欢,敬请期待第二部分,我将在其中讨论 SQLite 方面的内容以及一些关于 react-navigation 的实用知识。
对了,你看到最后一张照片里的虫子了吗?看来还得再花点功夫!
脚注
- 在亚马逊上买这类东西时,一定要小心“一起购买”选项。我犯了个大错。我们买的打印机没有连接纸盒的接口(那种接口很小,像电话线一样),所以我们只能手动打开纸盒,而且热敏纸的型号也不对。


