我司一直在维护开发一个ipad开单的系统,其中开单选择款式的时候,会实时查询可发库存。但是如果客户有多个仓库,有时,会希望能在开单界面里快速查询该款,在各个仓库里分色分码的可发和实际库存是多少,都分布在哪些仓库。这种需求是非常有实用价值的。
当拿到这个需求,分析后发现完全可以用ios的原生代码+接口来实现,但是如果这样来实现,呈现形式是非常固定的,并不灵活,而且要实现分色分码的原生代码显示也不太容易控制。同时,往往这类报表的需求可变因素很高,呈现形式又很复杂。用固定的原生代码来实现似乎并不明智。
同时,如果从产品经理的角度出发,app的某些功能可能是增值业务,如果能集中控制和部署似乎更方便。
结合以上需求和想法,我研究后提出一个实现方法:
可以在ios app里使用内嵌web控件,通过web控件里访问vue的前端页面,通过OC to JS的桥接方式,把调用参数(这里是选中的款号和颜色)传参给JS端,通过JS去调用app的后台接口获取报表数据(这里是单款的分布库存数据),然后利用vue的双向绑定的机制,自动刷新界面进行呈现。从而实现了在ios端查询报表的方法。
为了解决集中部署的问题,我架设了一台linux服务器,再上面部署了Nginx,并且配置了域名转发规则,将app的分布库存查询的需求,集中传入linux服务器的Nginx端口上,通过url匹配,让Nginx转发给app的应用服务器接口,调用到结果JSON后,套进vue的显示模板里进行显示,从而实现了在app里查看到分布库存了。这种思路也可以方便后期,把这个功能扩展给其他客户使用。
此方法的优点是:
(1)报表呈现灵活,实现方便 (2)部署集中,方便统一配置和管理 (3)修改报表时,基本不需要修改ios端
前端vue的核心代码如下:
<template>
<div>
<h4>货品编码:{{ParamInfo.GoodsCode}} ({{ParamInfo.ColorName}}) 的分布库存如下</h4>
<el-table
ref='singleTable'
:data='tableData3'
border
size='mini'
:summary-method='getSummaries'
show-summary
align='center'
style='width:100%'>
<el-table-column
type='index'
width='50'>
</el-table-column>
<el-table-column
prop='Department'
label='部门'
width='80'>
</el-table-column>
<el-table-column
prop='Color'
label='颜色'
width='80'>
</el-table-column>
<el-table-column
align='center'
label='可发库存'>
<el-table-column
v-for='sizeCol in ParamInfo.canUsedSizeGroup'
:prop='sizeCol.colName' :label='sizeCol.label'
width='40' >
</el-table-column>
</el-table-column>
<el-table-column
align='center'
label='实际库存'>
<el-table-column
v-for='sizeCol in ParamInfo.realSizeGroup'
:prop='sizeCol.colName' :label='sizeCol.label'
width='40' >
</el-table-column>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data() {
return {
//报表所用的查询参数
ParamInfo:{
HostName: 'xxxx', //主机名
BillType: '1', //触发的单据界面 0是销售订单 1是销售发货单
WareHoseId: '01C', //部门Id
GoodsCode: '118815S463', //要查询的货品编码
ColorName: '白色', //要查询的颜色名称
canUsedSizeGroup: [ //该货品对应的可发尺码组明细
{ colName: '可发库存_x_1',label: '均码'},
{ colName: '可发库存_x_2',label: 'S'},
{ colName: '可发库存_x_3',label: 'M'},
{ colName: '可发库存_x_4',label: 'L'}
],
realSizeGroup: [ //该货品对应的实际尺码组明细
{ colName: '实际库存_x_1',label: '均码'},
{ colName: '实际库存_x_2',label: 'S'},
{ colName: '实际库存_x_3',label: 'M'},
{ colName: '实际库存_x_4',label: 'L'}
]
},
tableData3: [{
//department: '总仓',
Code:'sss',
Department:'总仓',
Color: '白色',
可发库存_x_1: '10',
可发库存_x_2: '5',
可发库存_x_3: '60',
可发库存_x_4: '5',
实际库存_x_1: '10',
实际库存_x_2: '5',
实际库存_x_3: '60',
实际库存_x_4: '5'
}, {
Code:'sss',
Department:'店铺2',
Color: '白色',
可发库存_x_1: '10',
可发库存_x_2: '5',
可发库存_x_3: '60',
可发库存_x_4: '5',
实际库存_x_1: '10',
实际库存_x_2: '5',
实际库存_x_3: '60',
实际库存_x_4: '5'
},{
Code:'sss',
Department:'店铺1',
Color: '白色',
可发库存_x_1: '10',
可发库存_x_2: '5',
可发库存_x_3: '60',
可发库存_x_4: '5',
实际库存_x_1: '10',
实际库存_x_2: '5',
实际库存_x_3: '60',
实际库存_x_4: '5'
}]
}
},
methods: {
setCurrent(row) {
this.$refs.singleTable.setCurrentRow(row);
},
handleCurrentChange(val) {
this.currentRow = val;
},
getSummaries(param) { //合计行处理
const { columns, data } = param;
const sums = [];
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '合计';
return;
}
const values = data.map(item => Number(item[column.property]));
if (!values.every(value => isNaN(value))) {
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr);
if (!isNaN(value)) {
return prev + curr;
} else {
return prev;
}
}, 0);
sums[index] += '';
} else {
sums[index] = '--';
}
});
return sums;
},
QueryDisStockByParam(){ //异步查询远程服务器指定货品的分布库存
//GET获取库存分布
this.$ajax({
method: 'get',
//url:'../static/test/getInfo.json', //<---本地地址
url: '/api/SingleDistributeStockService/StockReportList?HostName=xxxdengfeitest&;'
+'billType=1&EndDate=2018-06-21&WareHose=00C&GoodsCode='+this.ParamInfo.GoodsCode+'&ColorName='
+this.ParamInfo.ColorName, //获取库存分部的Api
headers:{'Authorization':'1_987'}, //`headers`是自定义的要被发送的头信息 userCode_bindPassword 授权格式访问密码121.41.23.222/StockReportList?HostName=xxx&billType=yyy&EndDate=2018-06-01&WareHose=XXX&GoodsCode=A001&ColorName=black', //获取库存分部的Api
}).then(response=>{
let _data=response.data
console.log(_data) //StockReport
alert('hello,' + JSON.stringify(_data.StockReport))
this.tableData3=_data.StockReport
//赋值库存分部数组
//通知OC加载完毕
this.NotifyOCToFinished()
}).catch(function(err){
console.log(err)
alert('error,' + err)
})
},
NotifyOCToFinished(){ //JS2OC 通知ios端完成的消息
alert('开始执行调用OC函数!')
try {
this.JsBridge.callHandler(
'NotifyRemoteQueryFinished',//原生声明的函数名称,通知查询完成
{ data: `{'result':'1','msg':'查询完毕!'}` },//发送给原生的数据
(res) => {
res = JSON.parse(res)//原生处理完成后返回的数据
alert('收到OC的函数NotifyRemoteQueryFinished的返回值:'+res)
}
)
}
catch(err){
alert('调用OC函数出错,原因:'+err) // 可执行
}
},
RegisterOC2JSHandler() //注册OC2JS的调用函数
{
//注册OC调用的函数(触发分布库存查询)
this.JsBridge.registerHandler(
'QueryDistributStockByGoodCode',//注册的方法名,供原生调用
(data, responseCallback) => {
//data = JSON.stringify(data)//收到原生发来的数据
//alert('收到OC的函数传递过来的参数:'+data)
alert('收到OC的函数传递过来的参数:data.goodCode='+data.goodCode + ' data.color='+data.color)
//从OC传过来的参数参数
this.ParamInfo.GoodsCode=data.goodCode
this.ParamInfo.ColorName=data.color
//暂时直接用JSON参数进行模拟
/*
const jsonParam={
'HostName': 'xxxx', //主机名
'BillType': '1', //触发的单据界面 0是销售订单 1是销售发货单
'WareHoseId': '01C', //部门Id
'GoodsCode': '118815S463', //要查询的货品编码
'ColorName': '白色', //要查询的颜色名称
'canUsedSizeGroup': [{ //该货品对应的尺码组明细
'colName': 'X_1',
'label': '均码'
}, {
'colName': 'X_2',
'label': 'S'
}, {
'colName': 'X_3',
'label': 'M'
}, {
'colName': 'X_4',
'label': 'L'
}],
'realSizeGroup': [{ //该货品对应的尺码组明细
'colName': 'X_1',
'label': '均码'
}, {
'colName': 'X_2',
'label': 'S'
}, {
'colName': 'X_3',
'label': 'M'
}, {
'colName': 'X_4',
'label': 'L'
}]
}*/
//查询远程接口的数据
this.QueryDisStockByParam()
//先获取查询前需要指定的货品编码、尺码明细等信息
//ParamInfo=data;
responseCallback('js say: 查询参数初始化完毕!')//处理完成后返回给原生
})
}
},
created(){
},
mounted(){
this.RegisterOC2JSHandler()
//延迟1秒执行通知
/*
//var t;
//clearTimeout(t)
//t = setTimeout(function (){
//this.QueryDisStockByParam() //查询分布库存
//}, 1000)
*/
}
}
</script>
iOS端的调用代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
self.automaticallyAdjustsScrollViewInsets = NO;
//------WKWebviw的配置设定-------
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 设置偏好设置
config.preferences = [[WKPreferences alloc] init];
// 默认为0
config.preferences.minimumFontSize = 10;
// 默认认为YES
config.preferences.javaScriptEnabled = YES;
// 在iOS上默认为NO,表示不能自动通过窗口打开
config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
// web内容处理池
config.processPool = [[WKProcessPool alloc] init];
//-------通过JS与webview内容交互------
config.userContentController = [[WKUserContentController alloc] init];
// 注入JS对象名称AppModel,当JS通过AppModel来调用时,
// 我们可以在WKScriptMessageHandler代理中接收到
[config.userContentController addScriptMessageHandler:self name:@"AppModel"];
//------创建WKWebviw-------
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds
configuration:config];
[self loadExamplePage:self.webView]; //加载html
[self.view addSubview:self.webView];
// 导航代理
self.webView.navigationDelegate = self;
// 与webview UI交互代理
self.webView.UIDelegate = self;
//------添加KVO监听--------
[self.webView addObserver:self
forKeyPath:@"loading"
options:NSKeyValueObservingOptionNew
context:nil];
[self.webView addObserver:self
forKeyPath:@"title"
options:NSKeyValueObservingOptionNew
context:nil];
[self.webView addObserver:self
forKeyPath:@"estimatedProgress"
options:NSKeyValueObservingOptionNew
context:nil];
//-------添加进入条------
self.progressView = [[UIProgressView alloc] init];
self.progressView.frame = self.view.bounds;
[self.view addSubview:self.progressView];
self.progressView.backgroundColor = [UIColor redColor];
//self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"后退" style:UIBarButtonItemStyleDone target:self action:@selector(goback)];
//self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"前进" style:UIBarButtonItemStyleDone target:self action:@selector(gofarward)];
//-------添加按钮----------
self.view.backgroundColor = [UIColor whiteColor];
[self setTitle:@"货品分布库存"];
//右边添加多个按钮
UIButton *freshButton=[UIButton buttonWithType:(UIButtonTypeCustom)];
[freshButton setTitle:@"刷新" forState:(UIControlStateNormal)];
[freshButton setTitleColor:[UIColor whiteColor] forState:(UIControlStateNormal)];
freshButton.layer.masksToBounds=YES;
freshButton.layer.cornerRadius=3;
freshButton.titleLabel.font=[UIFont systemFontOfSize:15];
freshButton.backgroundColor=[UIColor orangeColor];
[freshButton addTarget:self action:@selector(freshToQueryData) forControlEvents:UIControlEventTouchUpInside];
UIButton *delCacheButton=[UIButton buttonWithType:(UIButtonTypeCustom)];
[delCacheButton setTitle:@"清除缓存" forState:(UIControlStateNormal)];
[delCacheButton setTitleColor:[UIColor whiteColor] forState:(UIControlStateNormal)];
delCacheButton.layer.masksToBounds=YES;
delCacheButton.layer.cornerRadius=3;
delCacheButton.titleLabel.font=[UIFont systemFontOfSize:15];
delCacheButton.backgroundColor=[UIColor orangeColor];
[delCacheButton addTarget:self action:@selector(delCacheForHtml) forControlEvents:UIControlEventTouchUpInside];
UIButton *closeButton=[UIButton buttonWithType:(UIButtonTypeCustom)];
[closeButton setTitle:@"关闭" forState:(UIControlStateNormal)];
[closeButton setTitleColor:[UIColor whiteColor] forState:(UIControlStateNormal)];
closeButton.layer.masksToBounds=YES;
closeButton.layer.cornerRadius=3;
closeButton.titleLabel.font=[UIFont systemFontOfSize:15];
closeButton.backgroundColor=[UIColor orangeColor];
[closeButton addTarget:self action:@selector(closeForm) forControlEvents:UIControlEventTouchUpInside];
freshButton.frame = CGRectMake(0, 0, 50, 30);
delCacheButton.frame=CGRectMake(0, 0, 100, 30);
closeButton.frame=CGRectMake(0, 0, 50, 30);
UIBarButtonItem *fresh = [[UIBarButtonItem alloc] initWithCustomView:freshButton];
UIBarButtonItem *delCache = [[UIBarButtonItem alloc] initWithCustomView:delCacheButton];
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithCustomView:closeButton];
[self.navigationItem setLeftBarButtonItem:close];
[self.navigationItem setRightBarButtonItems:[NSArray arrayWithObjects: fresh, delCache,nil]];
}
- (void)loadExamplePage:(WKWebView*)webView {
/*默认同一个目录加载
NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"Index" ofType:@"html"];
NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
[webView loadHTMLString:appHtml baseURL:baseURL];
*/
//带路径加载
//[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"ShowStock.html" relativeToURL:[[NSBundle mainBundle] bundleURL]]]];
//加载云端url
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://linux.chaorengo.com/vue/index.html"]]];
}
ngnix的核心配置:
nginx在测试调试中,一个重要的经验是:
#开启gzip压缩 gzip on;
这能大大压缩app打开vue文件需要等待的时间。
#运行用户
#user nobody;
#启动进程,通常设置成和cpu的数量相等
worker_processes 2;
#全局错误日志及PID文件
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
#工作模式及连接数上限
events {
#epoll是多路复用IO(I/O Multiplexing)中的一种方式,
#仅用于linux2.6以上内核,可以大大提高nginx的性能
use epoll;
#单个后台worker process进程的最大并发链接数
worker_connections 1024;
# 并发总数是 worker_processes 和 worker_connections 的乘积
# 即 max_clients = worker_processes * worker_connections
# 在设置了反向代理的情况下,max_clients = worker_processes * worker_connections / 4 为什么
# 为什么上面反向代理要除以4,应该说是一个经验值
# 根据以上条件,正常情况下的Nginx Server可以应付的最大连接数为:4 * 8000 = 32000
# worker_connections 值的设置跟物理内存大小有关
# 因为并发受IO约束,max_clients的值须小于系统可以打开的最大文件数
# 而系统可以打开的最大文件数和内存大小成正比,一般1GB内存的机器上可以打开的文件数大约是10万左右
# 我们来看看360M内存的VPS可以打开的文件句柄数是多少:
# $ cat /proc/sys/fs/file-max
# 输出 34336
# 32000 < 34336,即并发连接总数小于系统可以打开的文件句柄总数,这样就在操作系统可以承受的范围之内
# 所以,worker_connections 的值需根据 worker_processes 进程数目和系统可以打开的最大文件总数进行适当地进行设置
# 使得并发总数小于操作系统可以打开的最大文件数目
# 其实质也就是根据主机的物理CPU和内存进行配置
# 当然,理论上的并发总数可能会和实际有所偏差,因为主机还有其他的工作进程需要消耗系统资源。
# ulimit -SHn 65535
}
http {
#设定mime类型,类型由mime.type文件定义
include mime.types;
default_type application/octet-stream;
#设定日志格式
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
#sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,
#对于普通应用,必须设为 on,
#如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,
#以平衡磁盘与网络I/O处理速度,降低系统的uptime.
sendfile on;
#tcp_nopush on;
#连接超时时间
#keepalive_timeout 0;
keepalive_timeout 65;
tcp_nodelay on;
#开启gzip压缩
gzip on;
#gzip细节设置
gzip_min_length 1k;
gzip_buffers 4 16k;
#gzip_http_version 1.0;
#压缩级别,1-10,数字越大压缩的越好,时间也越长,看心情随便改吧
gzip_comp_level 6;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary off;
gzip_disable "MSIE [1-6].";
#设定请求缓冲
client_header_buffer_size 128k;
large_client_header_buffers 4 128k;
#设定虚拟主机配置
server {
#侦听80端口
listen 80;
#定义使用linux.chaorengo.com访问
server_name linux.chaorengo.com;
charset utf-8;
#定义服务器的默认网站根目录位置
#root html;
root /var/www;
#root /usr/share/nginx/html;
#设定本虚拟主机的访问日志
#access_log /usr/local/nginx/html/log/access.log main;
#error_log /usr/local/nginx/html/log/error.log error;
#默认请求
location / {
#定义首页索引文件的名称
index index.html index.htm;
#proxy_pass http://127.0.0.1:3080/index;
}
#api转发 added by dengfei 2018-06-26 demo测试
location ^~ /api_191.41.233.222_1111/ {
#rewrite ^.+api/?(.*)$ /api/$1 break;
proxy_pass http://191.41.223.222:1111/;
}
#api转发 added by dengfei 2018-07-06 时尚小熊接口
location ^~ /api_120.26.229.225_9999/ {
#rewrite ^.+api/?(.*)$ /api/$1 break;
proxy_pass http://120.26.229.225:9999/;
}
# 定义错误提示页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
#静态文件,nginx自己处理
location ~ ^/(images|javascript|js|css|flash|media|static)/ {
#过期30天,静态文件不怎么更新,过期可以设大一点,
#如果频繁更新,则可以设置得小一点。
expires 30d;
}
#PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置.
#location ~ .php$ {
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# include fastcgi_params;
#}
#禁止访问 .htxxx 文件
location ~ /.ht {
deny all;
}
}
}
示例如图:
点击当前库存,查询分布库存。 成功查询到选中款式的分布库存。总结:
以上解决app查询报表的思路,其实可以广泛用于一些特殊报表的实现场景,从而把app里查询复杂报表的需求,转化为了vue里如何实现复杂报表的问题;这样思路就被完全打开了,你甚至可以在vue里用大屏来呈现很复杂的数据。当然访问速度的优化还是需要做的。
希望这个研究能对读者有所帮助!从而实现更好的app体验和跨平台应用部署。
2019-08-21 菲哥 于杭州