概述

报名参加了我大鹅厂的小程序开发大赛。我这个爆栈工程师(CSS、JavaScript、Typscript、Swift、Flutter、Golang、Rust,容器、DevOps、安全攻防啥都会一点)必然是要去晃一圈的,名次啥的不重要,重要的是一定要拿到那件文化衫!!!

产品设计

技术选型

后端

按比赛要求,后端上是要用云开发 Cloudbase 的。讲真的,作为一个专家级的客服+运维+开发+修锅容器工程师,我绝对不会想要自己去买个服务器,建个 k8s 然后还要再买点什么 MySQL 啊 Redis 啊各种杂七杂八的基础服务的——是加班没加够,还是出去浪不够香哪?说句良心话,云开发不但对菜鸟开发者来说挺好用的,就我这种老油条级别的也觉得真的香啊!就用云开发了,不管基础设施啥的真的太香了!

前端

为了快速实现碳迹小程序,我选用了 @tarojs 多端统一开发解决方案,同时选用基于 Taro 框架开发的多端 UI 组件库 @taro-ui 。开发语言上 Typescript 是不二之选——懂得都懂。啥?不懂,同学你还需要磨练磨练啊。

产品设想

自定义底部选项卡遇到的问题

组件没有原生香

在开始的时候,使用 taro-ui 的 AtTabBar 组件来实现,单一组件、自带成套的图标。初看起来很是美好,但是很快就发现了问题:

  1. 不太方便抽象成 React Component,需要每个Page 都写 AtTabBar
  2. 整个小程序是其实只用一个页面,通过 AtTabBar 切换页面的时候闪屏;

在翻阅微信开发者社区、Taro Issues 后发现闪屏问题基本上无解,这必然会严重的影响到用户的交互体验——遂而放弃使用 AtTabBar ,改用小程序原生的 tabBar 和多页面模式。

图标图片成了大问题

但是,新的问题随之而来, tabBar 的 icon是需要图片的。icon 用图片而不是字体貌似不是什么问题,单个 icon 图片找起来没有什么问题,在阿里巴巴矢量图标库 有海量的素材供人挑选。我们面临最大的问题就是——没有专门美术同学,要把素材变成风格一致的资源对业余选手费时费力,而我们缺乏的恰恰就是时间。

没有刀子自己造——font2png

那就用 taro-ui 里面的 icon 吧。燃鹅——这个决定硬是催生了一个 rust 编写的工具 font2png 。本身时间紧,事务多,吃饱了撑着才会没事造轮子。事实是,穷尽中英文、海内外互联网,我就没找到一个好用的把图标字体转化成图标图片的工具:

  • github 上的 Python 代码运行不了,各种依赖、报错信息折腾的没完没了的;
  • 某大侠在2015 年用 QT 写的 Mac/Windows 图标转换工具只能选内置丑丑的图标——哎呦喂,我只想用我选定的 taro-ui 字体里面的;
  • 某英文在线 convert 工具硕大的大字卸载 woff to png,没地方让我输入字符不说,下载下来就是个大黑条——感觉智商受到了侮辱。

这么看下去,再奢求在网上找一个可以用的字体转图片工具简直就是痴心妄想——不怕,没有粮食我们自己中,没有刀子我们自己造!在 crates.io 挑挑拣拣了一些可用的依赖库后,我只用了一个小时就用 rust 写了一个自己相当满意的转换工具—— font2png,它简单好用:

  • 自定义图片大小
  • 自定义图标大小
  • 自定义图标演示
  • 默认透明背景
  • 内置 taro-ui 的图标字体

安装起来也是相当简单,在 Mac 电脑已经安装了 Homebrew 的情况下,你只需要引入我维护的 brew tap 就能轻松安装了:

1
2
3
4
# 引入 kofj 这个 tap
brew tap kofj/kofj
# 安装 font2png
brew install font2png

如果你按安装了 rust 的开发工具 Cargo 而且想要从源码编译的话,也是可以的。我已经把它发布到了 creates.io https://crates.io/crates/font2png 了:

1
cargo install font2png

运行起来看看使用信息吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
font2png --help
font2img v1.0.0
Fanjiankong <kfanjian@gmail.com>
A tool for converting TTF icon font to images.

example:
	font2png --charter $(printf '\ue957') -s 80 -f a -o src/assets/on/user.png -c "#d43c33"

USAGE:
    font2png [FLAGS] [OPTIONS] --charter <charter> --color <color> --font <fontpath> --output <output>

FLAGS:
    -h, --help           Prints help information
    -t, --transparent    transparent background
    -V, --version        Prints version information

OPTIONS:
        --charter <charter>    icon charter
    -c, --color <color>        icon css style color
    -f, --font <fontpath>      font file path
    -s, --size <iconsize>      icon's height and width(pixel) [default: 54]
    -o, --output <output>      output filename
    -s, --size <size>          output image's height and width(pixel) [default: 78]

不知道有没有同学注意到——我们 font2png 中除了 size 还有一个 iconsize ,为啥?因为当图标和图片是一样的尺寸的时候, tabBar 看着实在是好丑啊。要是能够好看点谁不愿意哪?!我在微信开放社区的一片帖子里面找到了一个 tabBar 的尺寸建议:

总的大小78.图标大小54 https://developers.weixin.qq.com/community/develop/doc/00020e62594268ce0b19e83ed52800?_at=1567814400111

按照这个建议产生的图片确实是好看了不少(这俩已经作为默认参数值了)——不信对比下:

各位看官,走过路过,不要忘记给我的小工具在 Github 点个 star。当然,开 issues 提需求、贡献代码也是万分欢迎的,地址奉上:

小程序开发问题随笔

页面背景色问题

Taro 的配置文件 app.config.ts 在编译成小程序代码后会转化成为 app.json 。这个文件的 window 窗口设置 当中的 backgroundColor 相当有迷惑性——这是在页面下拉时的颜色,而非事页面本身的背景色。真正的背景色要在页面的样式文件中声明。

1
2
3
4
5
6
window: {
    backgroundTextStyle: 'light',
    navigationBarBackgroundColor: '#f7f7f7',
    navigationBarTextStyle: 'black',
    backgroundColor: '#f7f7f7'
  }

为了全局生效,我把页面的背景色写在了 app.scss 当中:

1
2
3
page {
  background-color: #f7f7f7;
}

关联OpenId 与用户信息

在小程序端调用 wx.getUserProfile 后,云开发控制台用户列表依然匿名。

这时候,再调用一次 wx.getUserInfo 即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
getUserProfile = () => {
    this.setState({ deny: "获取授权中" })
    wx.getUserProfile({
      desc: "授权登录"
    }).then(resp => {
      console.log("getUserProfile", resp, this.props)
      console.log("getUserProfile", resp, this.props)
      wx.getUserInfo({})
      let info = resp.userInfo
      if (info) {
        this.props.update_userinfo({
          isAuthorized: true,
          cloudId: resp["cloudID"],
          userInfo: info
        })
      }
    }).catch(err => {
      console.log("获取授权失败", err);
      this.setState({ deny: "授权用户信息并登录后才能使用" });
    })
  }

嵌套组件报错 TS2559

抽象出来一个 Auth 组件以便于根据用户的登录状态来决定显示登录按钮还是该页面的内容。页面内容作为 children 位于标签中间——形如 <Auth></Auth>。最早版本的实现虽然能够工作,但是当便签中嵌入其它组件标签的时候,控制台报错: "类型“{ children: Element; }”与类型“IntrinsicAttributes & Omit<ClassAttributes<Authorized> & AuthorizedProps, \"user\" | \"update_userinfo\">”不具有相同的属性。"。原始代码如下:

 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
71
72
73
74
75
import React, { Component } from 'react'
import { View, Button, Text } from '@tarojs/components'
import { connect } from 'react-redux'
import { UserState } from '../../constants/types'
import { update_userinfo } from '../../actions/user'

type AuthorizedProps = {
  user: UserState,
  update_userinfo(user: UserState): void,
}

type AuthorizedState = {
  tips: string
}

class Authorized extends Component<AuthorizedProps, AuthorizedState> {
  constructor(props) {
    super(props);
    this.state = {
      tips: ""
    }
  }
  getUserProfile = () => {
    this.setState({ tips: "获取授权中" })
    wx.getUserProfile({
      desc: "授权登录"
    }).then(resp => {
      console.log("getUserProfile", resp, this.props)
      console.log("getUserProfile", resp, this.props)
      wx.getUserInfo({})
      let info = resp.userInfo
      if (info) {
        this.props.update_userinfo({
          isAuthorized: true,
          cloudId: resp["cloudID"],
          userInfo: info
        })
      }
    }).catch(err => {
      console.log("获取授权失败", err);
      this.setState({ tips: "授权用户信息并登录后才能使用" });
    })
  }

  render() {
    if (this.props.user.isAuthorized) {
      return (
        React.createElement(
          View, {}, this.props?.children
        )
      )
    } else {
      return (
        React.createElement(
          View, { className: 'login' },
          React.createElement(Button, { onClick: this.getUserProfile }, "授权登录"),
          React.createElement(Text, {}, this.state.tips),
        )
      )
    }
  }
}

export default connect(
  (state: {
    user: UserState,
  }) => ({
    user: state.user,
  }),
  (dispatch) => ({
    update_userinfo(userState: UserState) {
      dispatch(update_userinfo(userState))
    },
  })
)(Authorized)

通过为 Auth 组件的 AuthorizedProps 声明可空成员 children 搞定:

1
2
3
4
5
type AuthorizedProps = {
  user: UserState,
	children?: JSX.Element | any,
  update_userinfo(user: UserState): void,
}

Number to int32

1
2
3
static NumberToUint32(x:number):number {
      return x >>> 0;
}

碳排放数据

一吨碳在氧气中燃烧后能产生大约3.67吨二氧化碳。 其计算是这样的:碳的分子量为12,二氧化碳的分子量为44,44/12=3.67

骑行减排

每公里骑行减排50g二氧化碳 美团单车助力2020年上海公共机构节能宣传周 https://zhuanlan.zhihu.com/p/151802473

燃油

按照一般的计算方法,汽车的碳排放量(公斤)=油耗消耗数(升)×2.7公斤/升,也就是说,如果一辆汽车百公里油耗在7L,其百公里碳排放量就为18.9公斤,如果每天来回跑40公里,每月跑1200公里,其每月的碳排放量就为226.8公斤,一年为2721.6公斤。

先看看各类汽油的平均密度:90号为0.72g/ml, 93号为0.725g/ml, 97号为0.737g/ml。 那么,1升油的重量分别为:90号0.72千克,93号0.725千克,97号0.737千克。 整体算下来1升油差不多为0.7千克,不到1千克。

|按照平均百公里耗油8升计算

以为例,官网公布85kWh的车型续航里程是500公里,平均百公里电耗17kWh。而的秦电动车,电池容量13kWh