tip:
用Flutter实现通用框架功能,然后把业务功能用Vue去实现,可以解决公司移动开发资源紧张,但是每个业务组都备有H5开发的场景。

而H5开发又不需要管理麻烦的app上架,原生硬件调用等移动端知识。

所以最近使用Flutter_inappwebview做了个app内置浏览器来实现这个功能。

下面说下实现方案:

引入Vue资源文件

用webview直接打开网页显然是体验比较卡的,所以需要把Vue做好的页面build成资源文件,打包到Flutter工程里。

pubspec.yaml配置文件引入资源文件。

assets:
  - assets/
  - assets/fonts/
  - assets/css/
  - assets/home/
  - assets/js/
  - assets/test/

如果这里我就引入了两个Vue的页面。一个test、一个home。

准备Flutter_inappwebview

Flutter_inappwebview官方案例里面用的例子使用的是最原始的方式,没有进度条,
随便页面状态发生变动都会重新rebuild webview。这显然不符合目前的Flutter开发理念。webview加载了网页后应该是固定而不需要重新build的。

当时用的Flutter_bloc框架来解决,(最近发现GetX框架更简便,这里推荐下)这里很简单的在页面头部加了个进度条,中间来了个load动画。

构建一个HPWebViewPage,核心代码

InAppWebView webviewInit(BuildContext context) {
  HPWebViewBloc vbloc = BlocProvider.of<HPWebViewBloc>(context);
  print("view init: ${this.viewInfo.url}");
  return InAppWebView(
      key: const Key("in_app_webview"),
      initialUrlRequest: this.viewInfo.url.startsWith(HPWebViewConst.filePath)
          ? null
          : URLRequest(url: Uri.parse(this.viewInfo.url)),
      onWebViewCreated: (controller) {
        if (jsHandler != null) {
          jsHandler!(controller, context);
        }
      },
      onLoadStart: (controller, uri) =>
          vbloc.add(HPWebViewLoadStartEvent(controller, uri)),
      onLoadError: (controller, uri, code, message) =>
          vbloc.add(HPWebViewLoadErrorEvent(controller, uri, code, message)),
      onLoadHttpError: (controller, uri, code, message) =>
          vbloc.add(HPWebViewLoadErrorEvent(controller, uri, code, message)),
      onLoadStop: (controller, uri) =>
          vbloc.add(WebViewLoadStopEvent(controller, uri)),
      onProgressChanged: (controller, progress) =>
          vbloc.add(WebViewProgressEvent(controller, progress)),
      initialUserScripts: this.injectJSList);
}

jsHandler、injectJSList 是由外部传入的。Flutter与H5交互的代码。这个是由不同的业务定义。

build 代码

@override
Widget build(BuildContext context) {
  return Stack(
    alignment: Alignment.center,
    fit: StackFit.expand,
    children: [
      webviewInit(context),
      Positioned(
          top: 0,
          child: BlocBuilder<HPWebViewBloc, HPWebViewState>(
            builder: (context, state) {
              print(state);
              if (state is HPWebViewLoadStartState) {
                return Container(
                    child: LinearProgressIndicator(value: 0), height: 2);
              }
              if (state is HPWebViewProgressState) {
                return Container(
                    child:
                        LinearProgressIndicator(value: state.progress / 100),
                    height: 2);
              }
              return Container(height: 0, width: 0);
            },
          ),
          left: 0,
          right: 0),
      Center(
        child: BlocBuilder<HPWebViewBloc, HPWebViewState>(
          builder: (context, state) {
            if (state is HPWebViewLoadStartState ||
                state is HPWebViewProgressState) {
              return CircularProgressIndicator();
            }
            return Container(height: 0, width: 0);
          },
        ),
      )
    ],
  );
}

打开Webview

打开webview需要注入交互的代码,不需要的话可以不加。

class WebViewUtil {
  static void openWebView(WebViewModel viewInfo, BuildContext context) async {
    String injectJS = await rootBundle.loadString("assets/files/inject.js");
    Navigator.of(context).push(MaterialPageRoute(builder: (context) {
      return HPWebViewPage(
          viewInfo: viewInfo,
          injectJSList: UnmodifiableListView<UserScript>([
            UserScript(
                source: injectJS,
                injectionTime: UserScriptInjectionTime.AT_DOCUMENT_END),
          ]),
          jsHandler: _addJSHandler);
    }));
  }

  static void _addJSHandler(
      InAppWebViewController controller, BuildContext context) {
    controller.addJavaScriptHandler(
        handlerName: JSHandlerConst.close,
        callback: (_) {
          Navigator.of(context).pop();
        });
    controller.addJavaScriptHandler(
        handlerName: JSHandlerConst.openUrl,
        callback: (args) {
          var url = args[0]['url'];
          var title = args[0]['title'];
          var filterUrl = args[0]['filterurl'];
          var filterTitle = args[0]['filtertitle'];
          Navigator.of(context).pushNamed(HPWebViewPage.routeName,
              arguments: WebViewModel(url,
                  title: title,
                  filterUrl: filterUrl,
                  filterTitle: filterTitle));
          // bloc.add(JSHandlerOpenUrlEvent(args));
        });

    controller.addJavaScriptHandler(
        handlerName: JSHandlerConst.back,
        callback: (args) {
          Navigator.of(context).pop();
        });
  }
}    

inject.js 只要是定义一套window.js.handler接口

//侧滑返回
handler.back = function() {
    window.flutter_inappwebview.callHandler('back');
}

目前的代码只能打开一个url,而不能打开本地的vue页面

void _openWebPage(WebViewModel viewInfo, BuildContext context) {
    WebViewUtil.openWebView(viewInfo, context);
  }

 _openWebPage(
        WebViewModel("https://github.com/wesin/hp_webview",
            title: "github"),
        context);

加载本地网页

加载本地网页需要在app里启动一个代理服务,当通过代理加载url时,拦截请求。返回H5资源文件。通过这种方式,我们可以加载从服务端下载过来的html文件,也可以加载已经打包在工程的html文件。

在main里启动本地代理服务,指定端口

final HPWebViewProxy localhostServer = new HPWebViewProxy(port: 8765);
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await localhostServer.start();
  print(localhostServer.isRunning());

如此就可以打开本地网页了

ElevatedButton(
      onPressed: () => _openWebPage(
          WebViewModel("http://localhost:8765/home/"), context),
      child: Text("打开本地网页")),

代理服务的代码和全部的代码都可以在我的github上看。
传送门

Flutter_bloc使用从代码生成说起

框架模板代码生成

VSCode 有个flutter的插件 Flutter Files,
安装后在项目栏,右击可以新建对应的模板代码。

选择【new small pack bloc】,输入page名称 小写下划线风格:wesin_test

对应的会生成一个wesin_test文件夹,
文件夹里有

  • wesin_test_page.dart
  • wesin_test_screen.dart
  • wesin_test_bloc.dart
  • wesin_test_event.dart
  • wesin_test_state.dart

大致说明下每个页面的用途。

  1. page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class RegisterPage extends StatefulWidget {
static const String routeName = '/register';
RegisterPage(this.phone, this.verifyCode);
final String verifyCode;
final String phone;

@override
_RegisterPageState createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
final _registerBloc = RegisterBloc(RegisterState());

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(LS.of(context, "register")),
),
body: BlocProvider<RegisterBloc>(
create: (_) => _registerBloc,
child: RegisterScreen(widget.phone),
),
);
}
}

作为一个主体的页面,定义了route名称。
可以使用StatelessWidget。因为正常也没什么状态管理

在state里面的builder里构建了BlocProvider

body: BlocProvider<RegisterBloc>(
    create: (_) => _registerBloc,
    child: RegisterScreen(widget.phone),
  )

这里是为后续的节点注册了一个RegisterBloc, 从这个节点下级的widget就可以通过Provider.of<RegisterBloc>来获取。
当然也可以有其他的方式比如context.read<RegisterBloc>来获取。

这里也可以把bloc传给screen,可以方便在screen里面获取bloc。而不是通过在配置树里查找bloc。能稍微减少些性能消耗。

  1. screen
    screen主要是构建配置页面内容用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class RegisterScreen extends StatelessWidget {
const RegisterScreen(
this.phone, {
Key? key,
}) : super(key: key);

final String phone;

@override
Widget build(BuildContext context) {
return BlocListener<RegisterBloc, RegisterState>(
listener: (ctx, state) {
if (state.status.isSubmissionInProgress) {
EasyLoading.show(
maskType: EasyLoadingMaskType.clear, dismissOnTap: false);
return;
}
if (state.status.isSubmissionFailure) {
EasyLoading.dismiss();
var message = LS.of(context, "register_error");
EasyLoading.showError('$message:${state.errMessage ?? ''}');
return;
}
if (state.status.isSubmissionSuccess) {
EasyLoading.dismiss();
showCupertinoDialog(
context: context,
builder: (context) {
return
AlertDialog(
content: Text(LS.of(context, "register_success")),
actions: [TextButton(onPressed: (){
Navigator.of(context).popUntil(ModalRoute.withName("/"));
}, child: Text(LS.of(context, "confirm")))],
);
});
}
},
child: Align(
alignment: Alignment(0, -1),
child: Padding(
padding: const EdgeInsets.only(top: 31, left: 36, right:36),
child: Column(children: [
_UsernameInput(),
_PasswordInput(),
_ConfirmPasswordInput(),
const SizedBox(
height: 20,
),
_RegisterButton()
]),
),
));
}
}

class _RegisterButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<RegisterBloc, RegisterState>(builder: (context, state) {
return CircleRectButton(
child: Text(LS.of(context, "register"), style: TextStyle(letterSpacing: 20)),
onPressed: state.status.isValidated
? () async {
context.read<RegisterBloc>().add(RegisterSubmitEvent());
}
: null);
});
}
}

BlocListener
通过BlocListener来监听状态消息,收到消息child不会rebuild。适合拿来弹出加载页,错误信息等。

BlocBuilder
通过BlocBuilder来包装需要修改的内容。
这里的在组装页面的时候尽量在涉及到更新的地方使用
BlocBuilder,也尽可能的考虑buildwhen 属性。保持当必须要更新时才重新build的思路。

  1. bloc
    bloc 在封装后没什么逻辑,他的思路就是接受页面的事件,转为状态变更返回给页面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class RegisterBloc extends Bloc<RegisterEvent, RegisterState> {

    RegisterBloc(RegisterState initialState) : super(initialState);

    @override
    Stream<RegisterState> mapEventToState(
    RegisterEvent event,
    ) async* {
    try {
    yield* event.applyAsync(currentState: state, bloc: this);
    } catch (_, stackTrace) {
    developer.log('$_', name: 'RegisterBloc', error: _, stackTrace: stackTrace);
    yield state;
    }
    }
    }

将对应的事件处理放在event里面去执行显然更符合设计模式。代码也会更清晰,而不用做很多判断

1
2
3
4
5
if(event is **event){
do(**)
} else if (event is **event) {
do(**)
}

  1. event
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@immutable
abstract class RegisterEvent {
const RegisterEvent();
Stream<RegisterState> applyAsync({required RegisterState currentState, required RegisterBloc bloc});
}

class UsernameChangeEvent extends RegisterEvent {
const UsernameChangeEvent(this.username);
final String username;

Stream<RegisterState> applyAsync({required RegisterState currentState, required RegisterBloc bloc}) async* {
var input = UsernameInput.dirty(username);
var status = Formz.validate([input, currentState.password, currentState.confirmPassword]);
yield currentState.copyWith(username: input, status: status);
}

}

抽象event,定义事件接口。
子类event来接收参数,实现事件接口并抛出新的状态给页面。

  1. state
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class RegisterState extends Equatable {
    final UsernameInput username;
    final PasswordTwoInput password;
    final PasswordTwoInput confirmPassword;
    final FormzStatus status;
    final String? errMessage;
    RegisterState({
    this.username = const UsernameInput.pure(),
    this.password = const PasswordTwoInput.pure(),
    this.confirmPassword = const PasswordTwoInput.pure(),
    this.status = FormzStatus.pure,
    this.errMessage
    });

    @override
    List<Object> get props => [username, password, confirmPassword, status];

    RegisterState copyWith({
    UsernameInput? username,
    PasswordTwoInput? password,
    PasswordTwoInput? confirmPassword,
    FormzStatus? status,
    }) {
    return RegisterState(
    username: username ?? this.username,
    password: password ?? this.password,
    confirmPassword: confirmPassword ?? this.confirmPassword,
    status: status ?? this.status,
    );
    }
    }

state可以考虑抽象state和单独一个state类。看具体业务吧。如果属性很简单,可以考虑抽象。属性比较多的情况下就没必要抽象了。

思路总结

  • Page作为整个页面的对外对象,给外部调用,同时定义和注册了本页面需要用的bloc.
  • Screen配置了整个页面展示的widget。并把事件抛给bloc.通过接收bloc的最新状态,并rebuild UI.
  • bloc里面处理事件并转为state返回给页面

题外话

Cubit

还有一套cubit的简单模板,其实就是把bloc换成了cubit。去掉了event的逻辑。

screen里面事件调用cubit的方法。在cubit里面emit(new state).如此页面就可以接收到最新的状态了。

GET

在使用体验上GET框架不依赖context。对于使用体验来说应该更好些。对于简单的状态管理,也是GET更方便。

ps: Maxcompute数据仓库建设的分享。

Maxcompute

大致介绍下Maxcompute, Maxcompute是阿里的一个大数据工具,基于Maxcompute阿里搭建了一个Datawork的数据平台。可以很“方便”的从各种数据源导入数据,做数据分析、机器学习等。

“方便”之所以加个引号,是因为某些方面的确很方便,
当然业务实在复杂了,很多时候也存在用的很难受的地方。

更多介绍去阿里官网了解吧。

回到正题,这里主要分享下批量删除分区的一个小技巧。

分区

介绍下分区的概念,Table是一个数据表,也是一个分区的数组。分区把Table的数据分成了一个个的区块。

Maxcompute是个不支持某条数据修改删除的数据仓库。而分区是可以删除和新增的。引入分区,就可以做到在小颗粒度上做到修改和删除的功能。

背景

首先描述下为何会有大量的分区需要删除的场景。

  • 数据从datahub归档,比如按频率最高的归档方式 15分钟一次,每次一个分区,可以想象几个月后将会有多少分区。
  • 数据从mysql分库增量读取,以每天一次增量,乘以分库的数量,也会产生大量的分区。

分区数量多了之后,文件将会变多。一个是Maxcompute对于表的分区是一个上限数量,另一个是分区数量多了之后计算将会很慢。

这些源数据将会经过清洗产生对应的中间表或者结果集,供BI或者数据分析使用。而源数据为了方便管理,可以通过sql聚合成一个大分区来存放。而原来的很多分区就可以删除,以便腾出空间。

方式

一般正常删除分区是通过sql来删除。

alter table 表A drop if EXISTS PARTITION(分区名='123')

这种方式只能一次删除一个分区,当分区有上万个的时候就不适用了。

这种情况可以通过pyodps来轻松的批量删除分区。

下面是一个删除datahub归档分区的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 清除历史分区,防止分区数达到上限
from datetime import date, timedelta
# 删除十天前分区
end_date = date.today() - timedelta(days=10)
end = end_date.strftime('%Y%m%d')

def drop_pars(tableName):
t = o.get_table(tableName)
s = list(filter(lambda x: x.name < "ds='{}'".format(end), t.partitions))
for par in s:
par.drop()

drop_pars("dhub_trade")
drop_pars("dhub_ft_trade")
drop_pars("dhub_dn_trade")
drop_pars("dhub_wms_trade")
drop_pars("dhub_trade_send")
drop_pars("dhub_wms_trade_send")

可以在dataworks里面新建个pyodps的节点来每天运行,一劳永逸。

如果是放在python本地运行的话建议使用ipython。具体还需要配置下odps的环境。可以参考下官方文档

后语

Dataworks使用中分库分表很多,怎么配置大量的同步任务。有经验的可以一起研究下。

ps: 春光如梭,一年又已过去,xcode10丝滑的过度了4.2版本。以为苹果终于给力了。然而xcode10.2的升级,Swift5的更新让我明白了一个道理,xcode10.2才是那个点,xcode10说不定只是老板催的急,临时发的一个版。

闲话不多说,描述下这次遇到的问题吧。

HashValue 彻底不能用了

其实这个swift4.2就已经有了新的方案,只是前面一直还能用。swift5直接编译报错了。

下面就是替换方案的使用方法。

// swift 3
extension Point: Equatable{
    func ==(lhs: Testhash, rhs: Testhash) -> Bool {
        return lhs.hashValue == rhs.hashValue
    }
}
extension Point: Equatable {
    var hashValue: Int {
        get {
            return self.x + self.y * self.x
        }
    }
}

// swift 4.2
extension Point: Equatable {
    static func ==(lhs: Point, rhs: Point) -> Bool {
        // Ignore distanceFromOrigin for determining equality
        return lhs.x == rhs.x && lhs.y == rhs.y
    }
}
extension Point: Hashable {
    func hash(into hasher: inout Hasher) {
        // Ignore distanceFromOrigin for hashing
        hasher.combine(x)
        hasher.combine(y)
    }
}

大坑,release版本发布后TableView某些delegate无法触发生效

具体描述下场景, 当TableView所创建的类没有实现某些可以不实现的代理,比如didselect, heightforrow。然后这个类被子类继承,实现了didselect、heightforheader、heightforfooter的方法。在debug模式下,代码运行很正常,点击事件,高度都能正常显示。但是打包发布后,从testflight下载过来,就有可能存在点击,tableview行焦点色会变化,但是事件没法触发的问题。或者header和footer高度不对的问题。

这个问题的原因估计是编译优化做了修改导致的。

最无脑的解决方案是在父类把这几个方法都写一个空的实现,子类做函数重写。亲测有效。

高端解决办法,可能需要去修改一些编译选项吧,然而没测试过,有大佬搞过的也可以指导下。

此问题是Xcode10.2的锅,与swift版本无关。

ps: 时隔一年差一个月,xcode9也出来了,swift4也升级了。犹记得上次swift3代码升级的惊悚场景,这次心中默默打了个底,一定不能手贱,一定要在xcode9.1出来我再升级。然而终究抵不住心中的好奇升级了,升级,升级了……幸好,幸好,相比xcode8,xcode9真是良心了。犹记得xcode8.0出来我是等了一个月xcode8.1出来我才能发版的,等的感觉要被公司辞退了T.T。

忆苦思甜过后,细数下代码升级中遇到的坑吧。

各种姿势的@objc @objcMembers

@objc

swift4里面所有的#selector(action) action方法都要加上@objc标记,资料说是为了兼容object-c代码。代码里面#selector的普及率还是相当高的,一个个改的手残。

@objcMembers

swift4里面NSObject的取值方法value(forkey)方法会造成运行时崩溃,有很多对象copy用了反射的免不得都会崩溃。然而编译器不会提示你代码错了。这里简单点处理可以给class对象添加@objcMembers标记, 也可以给属性添加@objc dynamic,realm就这是这么做的,当然所有realm数据库对象也得要改造下

@objcMembers
class StockDetailItem:NSObject {
    @objc dynamic var title = ""
    @objc dynamic var quantity = 0.0
    var actualNum = 0.0
    var fulllessNum = 0.0

    override init() {
        super.init()
    }

}

let item = StockDetailItem()
item.title = "haha"
item.value(forKey: "title")
item.value(forKey: "quantity")

这个代码修改也挺多,为了对象copy简便,自己写了个反射方法

swift3遗留的tableview delegate坑

在swift3里面大部分代理函数名称都简化了,比如

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

原来是

func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell

问题是不是所有的代理方法都改造,还落了几个 didselect 和canedit 这几个方法还是老样子,swift4里面全简化了。然而也没有警告或者错误提示。

扫描功能没反应了

切了swift4之后扫码全部失效了。一脸茫然,代码也没有任何错误提示。幸好stackoverflow上的有个解答,原来是代理方法名改了。

func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!)

改成了

func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection)

ipad上自定义UIBarButtonItem显示位置异常

一般这种显示异常的按钮都是自定义的customview。使用正常的UIBarButtonItem是没问题的.

如果确定要使用CustomView,需要专门设置view的intrinsicContentSize, 也就是设置大小即可。
参考代码如下

  • intrinsicContentSize是只读属性,需要继承后重写

    class CommonNavigationView: UIView {
    
        init(size: CGSize) {
            super.init(frame: CGRect(origin: CGPoint.zero, size: size))
            self.overrideSize = size
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        lazy var overrideSize = CGSize.zero
    
        override var intrinsicContentSize: CGSize {
            get {
                return overrideSize
            }
        }
    }
    
  • 放上个UIBarButton的扩展,这里使用Cartography做约束

    import Cartography
    
    extension UIBarButtonItem {
        enum Style {
            case left
            case right
        }
    
        convenience init(style: Style, customView: UIView, target:Any, action: Selector) {
            let backView = CommonNavigationView(size: CGSize(width: 44, height: 44))
            backView.addSubview(customView)
            let gesturer = UITapGestureRecognizer(target: target, action: action)
            backView.addGestureRecognizer(gesturer)
            constrain(customView) {
                imgv in
                if style == .left {
                    imgv.left == imgv.superview!.left
                } else {
                    imgv.right == imgv.superview!.right
                }
                imgv.centerY == imgv.superview!.centerY
                imgv.width == 25
                imgv.height == 25
            }
            self.init(customView: backView)
        }
    
        convenience init(style: Style, img: UIImage, target:Any, action: Selector) {
            let backView = CommonNavigationView(size: CGSize(width: 44, height: 44))
            let imgView = UIImageView(image: img)
            backView.addSubview(imgView)
            let gesturer = UITapGestureRecognizer(target: target, action: action)
            backView.addGestureRecognizer(gesturer)
            constrain(imgView) {
                imgv in
                if style == .left {
                    imgv.left == imgv.superview!.left
                } else {
                    imgv.right == imgv.superview!.right
                }
                imgv.centerY == imgv.superview!.centerY
                imgv.width == 25
                imgv.height == 25
            }
            self.init(customView: backView)
        }
    }
    

UITableView 顶部莫名留白

网上还是挺多解释的,然而基本都不适用,研究了好久,关闭行高估算, 设置头部高度。两者缺一不可

tableView.estimatedRowHeight = 0
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0

  func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 0.1
}

就先这样吧,祝各位升级愉快!

引言

混了两年开发,忽然又进入了PM的角色上,好久没有做过项目管理了,很多工作方法都忘了。这里回忆顺便做个记录。

背景

从上次跳槽进了一个创业公司,一个20来个人的小公司,万事从简,交流靠吼,需求靠说。人少了还好,和产品1v1的干也没什么大不了。不得不说,公司发展很快,短短两年,公司已经有150+的人马了。团队也变大了,然而产品的需求还是几句话的描述,再多加几个界面图,交互,场景纯靠描述。几个产品的对接更是一群产品在讲天书,路人完全不知道讲的什么鬼。心理默默想,图呢。

UML

UML:Unified Modeling Language(统一建模语言)
UML主要的就是画图,图有好几种。
可以按照需求画相应的图。

这边着重介绍几个图:

  • 用例
  • 活动图
  • 类图
  • 时序图
  • (流程图)

用例

用例图:从用户角度描述系统功能,并指各功能的操作者。

Image

活动图

活动图是UML用于对系统的动态行为建模的另一种常用工具,它描述活动的顺序,展现从一个活动到另一个活动的控制流。活动图在本质上是一种流程图。

它是UML中用于对系统动态活动建模的图形,反映系统中一个活动到另一个活动的流程,常常用于描述业务过程和并行处理过程。活动图中包括泳道、活动开始、活动结束、活动、对象、分支、消息等图形符号。

总的来说:活动图是描述多个角色间协同工作的流程图。

用门店采购对接多牛供应商流程画了个事例图。
Screen shot

流程图

流程图:流程图并不属于UML的基本图形类型,但是是程序员入门必须的一种图。可以拿来简单明朗的显示代码逻辑。

门店微信支付流程图。
Screen shot

类图

类图:一般用于程序员与程序员之间的对话。作用于描述复杂的设计模式、框架、架构、类关系等
Image

时序图

时序图:比较清晰的描述函数的运行流程。

Image

初始化篇

环境配置

mac本身自带了python版本,but是python2.x版本,如果有强迫症喜欢使用最新版本的可以安装python3。我这边安装在了/usr/local/bin,这个一般是在脚本文件第一行声明,表示使用python版本。

#!/usr/local/bin/python3

设置可执行的脚本权限

chmod +x xxxx.py

pip安装对应的python版本依赖库

sudo python3 -m pip install **

工具

推荐: Visual Studio Code

语法学习

随便挑几个语法python3语法网站学习
http://www.web3.xin/python3/107.html
http://www.runoob.com/python/python-pass-statement.html

进阶可以阅读下
Python进阶: https://github.com/eastlakeside/interpy-zh

方法篇

使用过sublime和vscode来学习和编程python,对于语法的智能提示和高亮都挺差的,so相对于java、c#、swift这种语法严谨的语言来说,编写体验极差,对新手非常不友好,经常写着写着要去翻词典。

所以新手阶段免不得多练多写了。

查看对象方法

>>> a 
[1, 2, 3, 4, 4, 7]
>>> dir(a)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

查看方法使用

help(a)

也可以help某个方法

help(a.count)

内置函数

例如 len(array)来获取列表长度这种,习惯了array.count无脑写法的,表示心累。

http://www.runoob.com/python/python-built-in-functions.html

技能

定位脚本性能瓶颈

python -m cProfile my_script.py

csv转json

python -c "import csv,json;print json.dumps(list(csv.reader(open('csv_file.csv'))))

yield 接收输入

def grep(pattern):
    print("Searching for", pattern)
    while True:
        line = (yield)
        if pattern in line:
            print(line)

search = grep('coroutine')
next(search)
#output: Searching for coroutine
search.send("I love you")
search.send("Don't you love me?")
search.send("I love coroutine instead!")
#output: I love coroutine instead!

读取文件数据

import io

with open('photo.jpg', 'rb') as inf:
    jpgdata = inf.read()
  • 如果你想读取文件,传入r
  • 如果你想读取并写入文件,传入r+
  • 如果你想覆盖写入文件,传入w
  • 如果你想在文件末尾附加内容,传入a

总结

配置好环境,浏览下语法,掌握了方法,应该就可以开搞了。

语法

  • 万物皆是对象
  • 属性就是对象的字典

引用类型和值类型

  • 踩坑版之从Swift过来操作数组

    var a = [1, 2, 4]
    var b = a
    b += [9]
    a
    b
    
  • 永远不要定义可变类型的参数

    >>> def add_to(num, target=[]):
    ...     target.append(num)
    ...     return target
    ... 
    >>> add_to(1)
    [1]
    >>> add_to(2)
    [1, 2]
    

内置函数之语法糖

  • 列表生成式

    [x for x in range(5) if x % 2 == 0]
    
  • map, filter, reduce

    list(map(lambda x:x*2, range(5)))
    list(filter(lambda x:x > 3, range(5)))
    
  • sorted

    >>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
    >>> def by_name(t):
    ...     return t[0].lower()
    ... 
    >>> l1 = sorted(L, key=by_name)
    >>> l1
    [('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]
    

踩坑pymysql

  • %d format: a number is required, not str

报错sql语句:

insert into sections(id, bookid, name, seq, url) values("%s", "%s", "%s",%d,"%s")'

正确sql语句:

insert into sections(id, bookid, name, seq, url) values("%s", "%s", "%s",%s,"%s")'

ps:打印,又是打印,销售不知道哪里找来个打印机要支持。好吧,不是所有的蓝牙打印机都是可以用corebluetooth去连接的。这次是Bixolon,spp型号的打印机。网上一搜,是个韩国的打印机。中国连官方代理也没有,只有淘宝有个买家,自称代理。这种打印机用的是苹果MFI认证的连接方式。免不得要适配下,废话不多说,献上教程。

项目配置

  • Build Phases 中添加ExternalAccessory.framework
  • Capabilities
    1.启用background modes 勾选External accessory communication、 Uses Bluetooth LE accessories、Acts as a Bluetooth LE accessory
    2.启用Wireless Accessory Configuration
  • info.plist添加Supported external accessory protocols
    Screen shot

代码

一般调用对应的sdk即可,用corebluetooth的寻找打印机是找不到的。

使用流程

  • 手机or pad蓝牙设置中先连接打印机。一般要输pin码。bixolon的pin码默认是0000
  • 连接好打印机后再去代码中调用sdk查找打印机,方能找到打印机。然后对应发指令即可。ps:网上有不需要sdk也可以连接打印机。没尝试过,代码略多

App审核

很关键,使用MFI认证的连接方式是必须要从厂家获取PPID的。这个流程一般是这样的。跟厂家沟通,厂家一般会发你一个表格可以填,需要的内容app名称、版本号、bundleid等。然后厂家拿这个去苹果申请把bundleid添加到他们的ppid里面。然后把ppid告诉我们。我们放到itunes里面的remark中备注好即可。一般如果已经审核被拒绝了,放上去之后最好在拒绝后面回复下已经放入备注中,不然人家也不会注意看,分分钟又拒绝了。在下已然有这样的经历。
这里大致贴下拒绝信息:
—– MFi - PPID —–

We have started your app review but are unable to complete the review because we cannot locate the MFi Product Plan ID (PPID) of the hardware accessory associated with your app.

If you do not know the PPID, please contact the accessory manufacturer for this information. Once you have the PPID, please enter this information in the Notes section for your app in iTunes Connect.

To edit the Notes section:

  • Log in to iTunes Connect
  • Click on “My Apps”
  • Select your app
  • Scroll down to “App Review Information”
  • Update “Notes”
  • Click Save
  • Click Submit for Review

Once you’ve updated the Notes section with the PPID, we can proceed with your app review.