iOS开发经验谈-(1)如何利用vue页面做为内嵌web,按照查询参数生成报表(单款分布库存为例)

395 阅读10分钟

我司一直在维护开发一个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 菲哥 于杭州