Giter Club home page Giter Club logo

me's Introduction

关于仓库

记录和分享技术的博客,折腾来去最后还是发现会简单的才是最好的。

联系作者

邮箱: [email protected]

文章索引

软件开发

通识部分

lang

Node

React

硬件开发

人工智能

数学

深度学习涉及到的三个数学部分:

  1. 微积分
  2. 线性代数
  3. 概率

通用认知

其他

博客推荐

文章推荐

me's People

Contributors

nonocast avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

me's Issues

数学基础认识

维基百科英语定义

Mathematics (from Greek μάθημα máthēma, "knowledge, study, learning") includes the study of such topics as quantity (number theory), structure (algebra), space (geometry), and change (mathematical analysis). It has no generally accepted definition.

这个定义要比维基百科中文清楚不少, 提到4大主题:

  • Quantity (number theory): 数量, 对应的是"数论"

    • 自然数 (Natural numbers) $\mathbb{N}$: $1, 2, 3$
    • 整数 (Integers) $\mathbb{Z}$: $-2, -1, 0, 1, 2$
    • 有理数 (Rational numbers) $\mathbb{Q}$: $-2, \frac{2}{3}, 1.21$
    • 实数 (Real numbers) $\mathbb{R}$: $-e$, $\sqrt{2}$, 3, $\pi$
    • 复数 (Complex numbers) $\mathbb{C}$: $2$, $i$, $-2+3i$
    • 超限数 (Infinite cardinals)
  • Structure (algebra): 结构, 对应的是代数

    个人理解代数就是用符号(变量)代替具体数字, 不再研究具体数字, 而是研究各种抽象化的结构, 比如$f(x)=x+1$, 我们并不关心$x$到底是几, 而是关心$f$这个模型的结构。

    其中包括了:

    • 组合数学 (Combinatorics)
    • 数论 (Number theory)
    • 群论 (Group theory)
    • 图论 (Graph theory)
    • 序理论 (Order theory)
    • 代数 (Algebra)
  • Space (geometry): 空间, 对应的是几何

    空间的研究源于几何学--尤其是欧几里得几。三角学则结合了空间及数, 且包含了著名的勾股定理。现今对空间的研究更推进到了更高维的几何、非欧几里得几何及拓扑学。数和空间在解析几何、微分几何和代数几何中有着很重要的角色。

    • 几何 (Geometry)
    • 三角学 (Trigonometry)
    • 微分几何 (Differential geometry)
    • 拓扑学 (Topology)
    • 分形 (Fractal geometry)
    • 测度论 (Measure theory)
  • Change (mathematical analysis): 变化, 对应的是数学分析

    了解及描述变化在自然科学里是一普遍的议题,而微积分学更为研究变化的有利工具。函数诞生于此,做为描述一变化的量的核心概念。对于实数及实变函数的严格研究为实分析,而复分析则为复数的等价领域。

    • 微积分 (Calculus)
    • 矢量分析 (Vector calculus)
    • 微分方程 (Differential equations)
    • 动力系统 (Dynamical systems)
    • 混沌理论 (Chaos theory)
    • 复分析 (Complex analysis)

大体可以分为以下3类:

  • Pure mathematics (纯粹数学, 纯数学): 包括数量, 结构, 空间, 变化。
  • Applied mathematics (应用数学): 应用数学思考将抽象的数学工具运用在解答科学、工商业及其他领域上之现实问题。主要包括: 统计学, 统计学, 计量金融, 生物数学, 物理数学, 数值分析等。
  • Discrete mathematics (离散数学): 是数学的几个分支的总称, 研究基于离散空间而不是连续的数学结构。主要包括: 数理逻辑, 集合论, 信息论, 数论, 组合数学, 图论, 抽象代数, 理论计算机科学, 拓扑学, 运筹学, 博弈论等。

structure

现代数学有数不清的分支,但是,它们都有一个共同的基础——集合论——因为它,数学这个庞大的家族有个共同的语言。集合论中有一些最基本的概念:集合(set),关系(relation),函数(function),等价 (equivalence),是在其它数学分支的语言中几乎必然存在的。对于这些简单概念的理解,是进一步学些别的数学的基础。我相信,理工科大学生对于这些都不会陌生。

在集合论的基础上,现代数学有两大家族: 分析(Analysis)和代数(Algebra)。

  • 代数(Algebra)包括了初等代数、抽象代数(abstract algebra)和线性代数(linear algebra)。
  • 分析(Analysis)则是建立在极限(limits)的基础上, 最主要的就是微积分的研究。

事实上, 对于CS来说, 也恰恰就是线性代数和微积分这两个概念。

参考:

How We Test Software at Microsoft 读书笔记

Published 10/12/2008
1st Edition

顶层三大部门

  • PSD (Platform Products and Services Division, 平台产品及服务)
  • MBD (Microsoft Business Division, 业务)
  • E&D (Entertainment & Devices Division, 娱乐及智能设备)

两种管理模式

  • 独立产品 (PUM: Product Unit Manager): 每组都由开发经理,测试经理和项目经理组成
  • 共享团队 (SS: Shared Service)

工程类职位

  • SDET (Software Development Engineer in Test, 软件测试工程师)
  • SDE (Software Development Engineer)
  • PM
  • Ops: 维护在线服务,以及企业内部IT基础设施
  • Usability and Design 可用性工程师: 交互体验
  • 内容
  • 策划
  • 研究
  • IPE (International Project Engineer, 本地化)
  • 管理 (PUM, GM, Group Manager etc.)

最主要的就是SDE, PM, SDET。

十大胜任能力是微软所有工程师必备的核心能力

  • 分析问题和解决问题的能力
  • 面向客户的创新:是否以客户为本,是否能够充分理解软件如何帮助客户解决问题,并对此充满兴趣和热情
  • 精湛的技术
  • 项目管理
  • 对质量的执着追求
  • 战略远见
  • 自信
  • 冲击力和影响力
  • 跨界合作
  • 人际意识

工程师有两条路线

  • IC (Individual Contributor, 独立贡献者)
  • Manager (主管)

管理结构

职称 团队大小 组织深度
主管 2-10 1
三级经理 15-50 2
二级经理 30-100 3~4
一级经理 200+ 4~5

第三章 工程生命周期

大多数敏捷开发专家认为不超过10人的团队是最合适的。微软采用功能小组的方式来组织。功能小组是一个小型的、跨职能的小组,由310个来自不同职能部门(通常包括开发、测试和项目管理)组成。这个小组从头到尾负责整个系统中一个功能块的实现。典型的Group由一个PM, 35个SDET和3~5个SDE组成。他们协同工作,用较短的周期设计、实现、测试和整合该功能到整个产品中。

这个团队的关键因素

  • 足够独立,甚至可以有团队自己的方法
  • 可以从定义、开发、测试和整合等方面来全方位推动这个产品功能,知道能直接向用户展示其价值。

Office和Windows所有部门都采用这种方式来激发更多的责任感和更好的自主性,同时又能有效地管理整个产品的进度。Office 2007的项目下有3000多个功能小组。

质量级别

  • 测试
  • 功能缺陷已关闭
  • 性能
  • 测试计划
  • 代码审查
  • 功能说明
  • 文档计划
  • 安全
  • 代码覆盖
  • 本地化

产品宏观视野

  • 产品: 战略备忘录 (多版本产品线战略)
  • 工程: 定义 (愿景文档, 工程系统, 发布目标, 里程碑进度, 功能优先级),开发 (M0, M1, M2, Beta, 发布,服务
  • 功能: 功能规格说明书, 设计说明书, 测试文档, 产品代码, 测试代码

Crash Course: Computer Science 读书笔记

【计算机科学速成课】[40集全/精校] - Crash Course Computer Science_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

ep1. Early Computing

  • 算盘
  • 加法器 (机械,通过齿轮实现加减法)
  • 差分机 (Difference Engine by Charles Babbage, 多个参数计算)
  • 分析机 (Analytical Engine, Charles Babbage -> 英国数学家Ada Lovelace)
  • 打孔机 (用于人口普查)

整个计算机体系就是通过不断的抽象提供不同的层次解决不同层面的问题。
就是Carrie Anne一直说的: A NEW LEVEL OF ABSTRACTION!

ep2. Electronic Computing

  • Relay (继电器)
  • bug (在机器中的一只虫子引起了计算错误,所以称之为bug)
  • 电流只能单向流动的电子部件叫"二极管" (diode), 三极管(tridoe)则可以和Relay一样控制电流
  • ENIAC 是第一台GENERAL PURPOSE, PROGRAMMABLE, ELECTRONIC
  • 贝尔高出了晶体管(transistor), 它和relay, tridoe一样是一个开关(switch),可以控制线路来控制开或关, 晶体管的核心就是"半导体" (semiconductor), 半导体就是有时候导电,有时候不导电,
    通过给晶体管的控制线电流就能激活半导体的激活属性,使之使电流通过或者不通过。
  • 今天的晶体管的尺寸已经到50 nanometers,每秒可以切换上百万次, 生产半导体最常见的材料是硅(silicon)

整个计算机从继电器, 到三极管,最后到晶体管的过程, 也是一个从机电到电子的过程。

ep3. Boolean Logic & Logic Gate

  • binary
    • true: 电路闭合,电流流过 ("on" state, when electricity is flowing)
    • false: 电路断开,没有电流 ("off" state, when no electricity flowing)
  • 布尔代数 (George Bool, 通过逻辑方程证明整理(truth), 三个基本操作: NOT, AND, OR)

开始将三极管了, 三极管三个pin分别是:

  • B (Base)
  • C (Controller)
  • E (Emitter)

Base是source,然后把controller看成input, emitter就对应output。
c为true时emitter也为true, 反之c为false时emitter也为false。
但如果把emitter接地,把base看成output,则形成了一个NOT的逻辑,这个要看视频,动画讲解。

ep4. Representing Numbers and Letters with binary

  • bit/byte
  • address
  • floating: 小数点可以在数字之间移动,32-bit表示floating时,第一个bit表示sign(正负),其后8个bits表示exponent(10的几次方), 后面的23个bits表示有效位数
  • letter: ascii (/æski]/ 注意发音, 是阿斯ki, 不是阿斯ka)
  • 不同系统采用通用交换信息的能力叫做interoperability (互用性)

ep5. How Computers Calculate - The ALU

  • ALU (Arithmetic & Logic Unit) 算数逻辑单元
  • ALU有2个单元, 1个算术单元(arithmetic unit)和1个逻辑单元(logic unit)

Arithmetic Unit

看bit相加:

  • 0+0=0
  • 1+0=1
  • 0+1=1
  • 1+1=0 (进1)
    之前的XOR gate和上面的逻辑是一样的,然后通过CARRY表示是否进位(A,B进一个AND即表示CARRY), 用一个XOR和AND就形成了一个HALF ADDER(半加器), 两个HALF ADDER加上一个OR就可以处理A+B+C的进位情况,这个就是FULL ADDER。

注: 半加器能产生进位但是不能处理进位,而全加器可以。
通过FULL ADDER就解决了8位加法的问题
Screen Shot 2019-10-02 at 7 28 29 PM
如果最后一个FULL ADDER也CARRAY就表示OVERFLOW (溢出)

ALU的常规算术操作:

  • ADD
  • ADD with CARRY
  • SUBTRACT
  • SUBTRACT with BORROW
  • NEGATE (反)
  • INCREMENT (add 1 to A)
  • DECREMENT (subtract 1 from A)
  • PASS THROUGH

ep6. Registers and RAM

  • AND-OR LATCH (AND-OR 锁存器)
  • GATED LATCH (门锁)
    • [in] DATA IN
    • [in] WRITE ENABLE
    • [out] DATA OUT
    • 当WRITE ENABLE置为1时,DATA IN的值就会写到DATA OUT上
  • register (寄存器)
    • width 表示register存放数字的位数
    • matrix
    • multiplexer (多路复用器)

ep7. CPU (The Central Processing Unit)

CPU的组成:

  • ALU
  • RAM
  • Register
    • Register A
    • Register B
    • Register C
    • Register D
    • INSTRUCTION ADDRESS REGISTER (指令地址寄存器)
    • INSTRUCTION REGISTER (指令寄存器)

CPU的运行过程:

  1. fetch phase (取指令阶段) 从指令地址寄存器找到对应的RAM地址,默认为0000 0000, 然后将RAM 0的bits复制到指令寄存器
  2. decode phase (解码阶段) 0010 1100,视频中定义0010为op即LOAD A, 表示将RAM location加载到Register A
  3. execute phase: 将Register A设置为RAM location对应的value

clock

  • clock speed: CPU完成一次'fetch-decode-execute'的速度
  • 1 Hertz表示一秒1个周期, 最早的CPU 72万Hertz, 现在的i7要到4GHZ, 也就是每秒40亿次
  • 对于移动设备,降频可以省电,这个很重要,现代处理器可以按需做到加快或减慢时钟速度,称为dynamic frequency scaling (动态调整频率)

提升抽象后,将ALU+Register看成CPU,RAM独立于CPU,当然MCU是把ROM, RAM, IO全部整合了,这个后面说。

ep8. Instructions & Programs

ep7中的instruction只有8bits,前4bits表示op, 后4bits表示操作数, 肯定不够啊,现代CPU有2种策略:

  • 提升INSTRUCTION LENGTH (指令长度), 提升到32bits或64bits
  • VARIABLE LENGTH INSTRUCTIONS (可变指令长度)

ep9. Advanced CPU Design

CPU和RAM之间通过BUS连接,因为CPU速度已经到了4GHz,但是RAM跟不上,所以在CPU内增加了CACHE,CPU和CACHE之间只需要1个clock cycle,CACHE和RAM可以一次取一批数据,以此提高效率。

INSTRUCTIONS PIPELINES
流水线操作,考虑到依赖性,减少同步等待时间,就需要做乱序执行。
CPU在有条件的JUMP时候会提前预测,现代CPU的正确率超过90%,厉害

MULTIPLE CORE (多核)
共享CACHE

ep10. Early Programming

  • Von Neumann Architecture 冯诺伊曼结构: 程序和数据都存在一个地方 (Unifying the program and data into a single shared memory), VNA的标志是,一个CPU(ALU)+数据寄存器(data registers)+指令寄存器(instruction register)+指令地址寄存器(instruction address register)+内存(memory, 负责存数据和指令, both data and instructions)

ep11. The First Programming Language

ep12. Programming Basic

ep13. Intro to Algorithms

O (算法复杂度) 描述了输入规模(input size)和运行步骤(number of steps)间的关系

ep14. Data Structures

ep15. Alan Turing

最早的图灵机就是状态机FSM
图灵测试

ep16. Software Engineering

/

ep17. Integrated Circuits

IC: Integrated Circuits, 我理解是用硅做的器件,三极管,芯片等等
PCB: Printed Circuit Boards, 印刷电路板
Photolithography: 光刻

INTEL 4004一个IC就有2300个晶体管,可以把整个CPU单元放到一个chip中,而仅仅20年前,用分立元件会占满整个屋子。2010年,一个CPU里面已经有10亿个晶体管,华为麒麟980中有69亿晶体管。

对光刻的要求非常高,分辨率需要达到14纳米。

光刻从道理上来说和PCB是一样的,只是通过光掩膜把图案印到晶圆上。

ep18. Operating Systems

OS也是一个程序,但它有操作硬件的特殊权限,可以运行和管理其他程序。
操作系统一般是开机第一个启动的程序。
为什么会有OS?
起先代码都是给固定已知的硬件运行,但是随着计算机越来越普及,比如相同CPU不同打印机,程序就能使用了,讲到底就是为了隔离硬件的区别才会有OS。
所以最基本的来说,OS提供API来抽象硬件,叫"设备驱动程序" (Device Drivers), 这样程序员可以通过标准方法操作I/O。

外设IO速度远远慢于CPU clock,就想到用多套IO共享CPU空闲时间,但如果需要同时运行多个程序,每个程序都会占用一些内存,当切换到另一个程序时,我们不能丢失数据,所以OS要管理程序切换。
因为物理内存分配会不连续,所以就引进了虚拟内存,对于上层应用来说,虚拟内存地址是一致的,具体分配到哪里的物理内存你就别管了。OS需要维系PHYSICAL MEMORY和VIRTUAL MEMORY的映射关系就行。

Dennis和Ken Thompson联手打造了Unix,他们设计把系统分为两个部分:

  • Kernel: 操作系统的核心功能,包括内存管理, 多任务和IO处理
  • Utility: 一堆有用的工具,包括程序和库文件

ep19. Memory & Storage

cpu clock和大容量磁盘读写速度的差异形成了层次:

  • processor register: very fast, very expensive
  • processor cache: very fast, very expensive
  • RAM: fast, affordable
  • Flash / USB Memory: slower, cheap
  • Hard driver: slow, very cheap
  • Tape backup: very slow, affordable

之前需要持久化二进制是非常不容易的,起先用水银压力波来实现,获取一个bit需要顺序获取,而后才有random, 称之为RAM。
现在硬盘的寻道时间差不多1/100秒
SSD通过IC持久化数据,没有机械结构,但是寻道时间在1/1000秒,比RAM慢很多倍。
所以现代计算机,荏苒采用层次化存储 (memory hierarchies)。

ep20. Files & Files Systems

  • file format: txt, jpeg, mp3
  • directory file用来描述文件名称,和meta info,如果文件名乱码问题就出在directory file上。

ep21. Compression

  • RUN-LENGTH ENCODING
  • DICTIONARY CODERS (霍夫曼编码,视频介绍的很精彩)

ep22. Keyboards & Command Line Interfaces

teletype (电传打印机): 就是一台打印机打印回车后会发送到远端另一台打印机
最早的交互是通过打印机, 你在打印机输入ls,电脑会控制打印机输出目录下的文件,体会一下。

Screen Shot 2019-10-03 at 2 30 22 AM

ep23 Screens & 2D Graphic

  • CRT: 分为矢量和光栅扫描(raster scanning)
  • LCD

早年内存不够所以用80x25字符来显示,内存中的字符要图形化则需要借助character generator(字符生成器),基本算是第一代graphics cards(显卡),显卡内部有一块ROM,存着每个字符的图形 (dot matrix pattern, 点阵图案), 所以内存中会有一块特殊区域专门为图形保留,叫做screen buffer (屏幕缓冲区),程序想要显示文字时,直接修改这块区域的内容就行。

搞定文字以后,接下来就是图形,上手还是从矢量入手,因为内存是硬伤,LOGO语言了解一下。

1960年代末才出现了真正像素的计算机和显示器,RAM中的映射区域称为FRAME BUFFER,后来也称为VRAM

ep24. 冷战和消费主义

ep25. 个人计算机革命

  • Apple II
  • Altas8000 + Basic
  • IBM发力IBM COMPATIBLE,形成开放架构,直接干翻Apple, 成就了DOS

商业策略:
open architecture - IBM
closed architecture - APPLE, 通过控制FULL STACK,提升用户体验和可靠性,背后的目的还是为了赚钱,这个不矛盾

ep26. Graphics User Interfaces

Metaphor 隐喻

ep27. 3D Graphics

projection

  • orthographic (正交: 立方体的边在投影过程中互相平行,没有灭点)
  • perspective (透视,这个有消失点)

Polygon - Mesh - Triangles
Triangles是定义一个面最小的单位
fill: 扫描线渲染, fillrate

远近处理
painter: 先画背景再画前景
z-buffering: 标记每个像素属于那个对象

灯光 (lighting)
shading: surface normal (法线)

GPU有独立的运算和RAM处理渲染问题,比如GTX有3584个core,提供大规模并行处理能力,美妙处理上亿个多边形。

ep28. Computer Networks

LAN: Local area network, 比如一个房间放一台主机,但是有N个tty
CSMA (Carrier Sense Multiple Access, 载波侦听多路访问), 多台电脑共享一个传输媒介, 在LAN上A发给B的数据会同时发给C,D,E, 通过包头MAC区分。
exponential backoff (指数退避) 解决冲突

SWITCH

因为是CSMA会产生collision,在组网的时候就要通过交换机(switch)划分collision domain, switch位于两个网络之间,只有在必要时才在两个网络之间传输数据,减少无效传输。

ROUTING

message switch, message沿着路由hop (跳转)的次数叫hop count (跳数)。

总体来说,将data拆分成多个small packets (数据包), 然后通过灵活的路由传递,这个就叫Packet Switching (分组交换), packet是乱序到达的,所以需要tcp/ip进行排序。

ep29. The Internet

LAN: local area network (比如家里的wifi)
WAN: wide area network (一般指ISP)

traceroute github.com

可以追踪路由。

IP (Internet Protocol)
PACKET = IP HEADER + DATA PAYLOAD

TCP/IP= IP HEADER + (TCP HEADER + DATA)
UDP/IP = IP HEADER + (UDP HEADER + DATA)

ep30. The World Wide Web

HTTP
network neutrality (网络中立性)

ep31. Cybersecurity

安全主要归结为2个问题:

  1. Who are you?
  2. What should you have access to?
    你是谁,你能访问什么内容?
    分为2个阶段:
  3. Authentication (身份认证)
  4. Access Control (权限管理)

ep32. Hackers and cyber attack

  • Fishing (钓鱼)
  • Pretexting (假托)
  • NAND Mirroring (直接复制内存,然后3次失败再重新还原内存)
  • Buffer Overflow (缓冲区溢出)
  • Code Injection (代码注入)
    在login/username中写下 whatever; drop table users; 体会一下。

当软件制造商不知道软件有新漏洞被发现时,那么这个漏洞就叫"Zero day vulnerability"(零日漏洞 Day0)

如何使用配置?

Node

这个和传统Java, C#应用程序基本一致,

- config
  - default.yaml
  - development.yaml
  - production.yaml
  - test.yaml
- src
- test
- .env
- .env.production
- .env.test

index.js

require('dotenv').config(); // 这句后续应交由dotenv-cli来实现
const config = require('config');

config是用来做常规设置,而dotenv区别config主要有2个因素:

  1. dotenv写到process的环境变量,比如说NODE_ENV, DEBUG的这些和process.env强绑定的设置
  2. 用户名密码之类不对外公开的信息,如果放在config则一并上传到了git repo,如果是公开的repo则公开了用户名密码,所以一般.env是不用来上传的

注:

  • 整个加载过程是层叠覆盖式的,比如当NODE_ENV=production时,会先加载.env,然后再加载.env.production覆盖 (dotenv-cli -e则不会显式层叠加载)

对应的webpack.config.js

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const _externals = require('externals-dependencies');

module.exports = {
  mode: 'production',
  target: 'node',
  entry: './src/index.js',
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: [
          /test/,
          /node_modules/
        ],
        loader: 'babel-loader'
      }
    ]
  },
  node: {
    __filename: false,
    __dirname: false
  },
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'bundle.js'
  },
  optimization: {
    minimizer: [new TerserPlugin()]
    // minimizer: []
  },
  plugins: [
        new CopyWebpackPlugin(['config', '.env*', 'package.json'])
  ],
  externals: _externals()
};

在运行build的时候还是需要先制定NODE_ENV

NODE_ENV=production node bundle.js

再来看dotenv-cli, 这样可以去掉require('dotenv').config(),然后在运行时选择NODE_ENV,

.env

NODE_ENV=development
DEBUG=app*

.env.production

NODE_ENV=production

ok, 然后配置运行指令,

dotenv -e .env node app
dotenv -e .env.prodcution node app

这样就比较有一致性了。

最后补充一下pm2的情况,pm2的环境变量建议还是直接在ecosystem.config.js中设置,不要混dotenv。

最终可以整理如下:

"scripts": {
  "start": "node src/index",
  "start:prod": "cross-env NODE_ENV=production node src/index",
  "build": "webpack --mode production --progress --display-modules --colors --display-reasons"
},

参考:

React

React的配置比Node会受限一些,主要是因为它运行在浏览器中,刚刚的config都是运行时,但React的配置则基本是编译时,因为.env的内容直接hard code到最终编译的js, 所以和Node是不同的,遵循create-react-app的要求,可以采用.env和.env.production等支持多环境,在加载.env.production时会先加载.env。

如果需要运行时,就比如build以后需要做配置,则需要在HTML中引入js,

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>React App</title>

    <script type="text/javascript">
      window.RUNTIME = {
        SERVICE: "http://xyz.api"
      };
    </script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

然后在页面获取,

function App() {
  return (
    <div className="App">
      {window.RUNTIM && window.RUNTIME.SERVICE &&
        <h1>{window.RUNTIME.SERVICE}</h1>
      }
      <h1>{process.env.REACT_APP_MESSAGE}</h1>
    </div>
  );
}

此外,yarn start时NODE_ENV=development, yarn build后则NODE_ENV=production

现在需求来了,

  • 在本地运行时,默认连接本地的服务,但可以手动设置采用其他服务地址
  • 编译后, 则默认使用云端地址, 可手动修改服务地址

config.js

import Debug from 'debug';

const debug = Debug('app:config');

class Config {
  constructor() {
    debug(`NODE_ENV: ${process.env.NODE_ENV}`);

    if (window.RUNTIME && window.RUNTIME.SERVICE) {
      this.service = window.RUNTIME.SERVICE;
    } else {
      if (process.env.REACT_APP_SERVICE) {
        this.service = process.env.REACT_APP_SERVICE;
      } else {
        throw new Error("Missing service configration");
      }
    }
  }

}

export default new Config();

在index.js中直接可以import './config', 然后在app.js就可以消费了,

function App() {
  return (
    <div className="App">
      <h1>{config.service}</h1>
    </div>
  );
}

最后补充一下env,

.env

REACT_APP_SERVICE=http://localhost:9007

.env

REACT_APP_SERVICE=https://api.space365.live

如果在HTML设置了service,则无视.env

<script type="text/javascript">
  window.RUNTIME = { SERVICE: "http://xyz.api" };
</script>

上述代码可以在这里获取。

C语言基础

C语言基础

gcc从app.c到a.out分为4个步骤:

  • Prepressing (预处理)
  • Compilation (编译)
  • Assembly (汇编)
  • Linking (链接)

Prepressing (预编译)

$gcc -E app.c -o app.i

gcc实际是一个工具链,针对不同的语言有不同的套路,如果是c语言则采用cpp来做真正的预编译,所以效果等同于调用,

$cpp hello.c > hello.i

假定app.c

int main(void) {
  return 0;
}

得到的结果是app.i

# 1 "app.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 362 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "app.c" 2
int main(void) {
  return 0;
}

加上stdio.h的app.c

#include <stdio.h>

int main(void) {
  printf("hello world\n");
  return 0;
}

这样生成app.i就有544行,

# 1 "app.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 362 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "app.c" 2
# 1 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 1 3 4
# 64 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 3 4
# 1 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h" 1 3 4
# 68 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h" 3 4
# 1 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 1 3 4
# 626 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 3 4
...
# 1 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/i386/_types.h" 1 3 4
# 37 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/i386/_types.h" 3 4
typedef signed char __int8_t;

typedef unsigned char __uint8_t;
typedef short __int16_t;
typedef unsigned short __uint16_t;
typedef int __int32_t;
typedef unsigned int __uint32_t;
typedef long long __int64_t;
typedef unsigned long long __uint64_t;

...

extern FILE *__stdinp;
extern FILE *__stdoutp;
extern FILE *__stderrp;
# 142 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 3 4
void clearerr(FILE *);
int fclose(FILE *);
int feof(FILE *);
int ferror(FILE *);
int fflush(FILE *);
int fgetc(FILE *);
int fgetpos(FILE * restrict, fpos_t *);
char *fgets(char * restrict, int, FILE *);

FILE *fopen(const char * restrict __filename, const char * restrict __mode) __asm("_" "fopen" );

int fprintf(FILE * restrict, const char * restrict, ...) __attribute__((__format__ (__printf__, 2, 3)));
int fputc(int, FILE *);
int fputs(const char * restrict, FILE * restrict) __asm("_" "fputs" );
size_t fread(void * restrict __ptr, size_t __size, size_t __nitems, FILE * restrict __stream);
FILE *freopen(const char * restrict, const char * restrict,
                 FILE * restrict) __asm("_" "freopen" );
int fscanf(FILE * restrict, const char * restrict, ...) __attribute__((__format__ (__scanf__, 2, 3)));
int fseek(FILE *, long, int);
int fsetpos(FILE *, const fpos_t *);
long ftell(FILE *);
size_t fwrite(const void * restrict __ptr, size_t __size, size_t __nitems, FILE * restrict __stream) __asm("_" "fwrite" );
int getc(FILE *);
int getchar(void);

...

extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
       const char * restrict, va_list);
# 408 "/Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 2 3 4
# 2 "app.c" 2

int main(void) {
  printf("hello world: %d\n");
  return 0;
}

大概就是这么个意义,把所有依赖的.h内容全部merge到一个文件。

stdio.h太复杂,我们改为自己的header试试,

app.c

#include "calc.h"

int main(void) {
  int result = add(1, 2);
  return 0;
}

calc.h

#ifndef CALC_H
#define CALC_H

int add(int a, int b);

#endif

生成的app.i很干净

# 1 "app.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 362 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "app.c" 2
# 1 "./calc.h" 1



int add(int a, int b);
# 2 "app.c" 2

int main(void) {
  int result = add(1, 2);
  return 0;
}

这个时候还不需要calc.c

我们来做个实验,include是不是只能.h文件,能不能多次include

hello.txt

hello world

app.c

#include "hello.txt"
#include "hello.txt"
#include "hello.txt"

int main(void) {
  return 0;
}

生成app.i如下

# 1 "app.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 361 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "app.c" 2
# 1 "./hello.txt" 1
hello world
# 2 "app.c" 2
# 1 "./hello.txt" 1
hello world
# 3 "app.c" 2
# 1 "./hello.txt" 1
hello world
# 4 "app.c" 2

int main(void) {
  int result = add(1, 2);
  return 0;
}

出乎意料吧,显然重复不太好,
修改hello.txt

#ifndef HELLO
#define HELLO

hello world

#endif

这样就把重复include的问题解决了,加一个变量判断而已,

# 1 "app.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 361 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "app.c" 2
# 1 "./hello.txt" 1


hello world

# 2 "app.c" 2


int main(void) {
  int result = add(1, 2);
  return 0;
}

小结一下,

  • 预处理过程主要是针对一个文件,解析其中以‘#’开始的预编译指令,比如#include, #define等
  • 递归处理所有include, 复制文件内容
  • 处理所有#define和条件指令 #if, #ifdef #ifndef等
  • 删除所有注释
  • 添加行号和文件名标识,比如# 2 "app.c" 2,辅助后续的错误和警告信息生成
  • 保留所有#pragma编译器指令,后续编译阶段需要用到,目前不能删
  • 展开所有宏 macro

所以当我们无法判断宏定义的时候,可以直接查看cpp生成的.i文件。

根据以上的

#include <stdio.h>
#include "calc.c"

int main(void) {
  printf("1+1=%d", add(1, 1));
  return 0;
}

其实这样gcc app.c也是ok,可以正常运行的,你了解这个原理就行。
你完全可以理解为,

#include <stdio.h>

int add(int a, int b) {
  return a + b;
}

int main(void) {
  printf("1+1=%d", add(1, 1));
  return 0;
}

而引用.h则可以理解为

#include <stdio.h>

int add(int, int);

int main(void) {
  printf("1+1=%d", add(1, 1));
  return 0;
}

int add(int a, int b) {
  return a + b;
}

其实你想明白就知道原来多文件在这个时候就是针对一个文件,因为其他文件都被include进来了,当然后面我们来解决申明和实现的'对应'。

Compilation (编译)

编译器就是将高级语言翻译成CPU相关的汇编语言的过程。

$gcc -S app.i -o app.s

等同于

$cc app.i

app.s

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 15	sdk_version 10, 15
	.globl	_main                   ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16G
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	xorl	%eax, %eax
	movl	$0, -4(%rbp)
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

.subsections_via_symbols

你可以通过gcc -S app.c直接完成propressing和compilation得到.s文件。

Assembly (汇编)

gcc -c app.s -o app.o
等同于
as app.s -o app.o

或者通过-c直接跳步
gcc -c app.c -o app.o

这里的.o文件称为Object File(目标文件),Object File是不可以执行的,
这里先不展开编译和汇编的内容,我们接着往下开Linking部分

Linking (链接)

首先linking用到的是ld,称之为linker,linker作用就是link各种object files, libraries后生成可执行文件(output file),ld可以生成一个最终的linked image, 可执行文件或者库文件。

最简单的app.c

int main(void) {
  return 0;
}

在mac上linker只需要

gcc -c app.c -o app.o
ld app.o -lSystem

这里的-lSystem中-l表示增加library,而System表示/usr/lib/libSystem.dylib,如果-lssl,则表示/usr/lib/libssl.dylib
ld(1) [osx man page]

多平台情况

我们知道,windows的exe可以从一个机器复制到另外一台机器,但是linux每次都要make configure, make然后install,这是为什么呢?
个人理解:

  1. windows可执行程序采用PE格式,而linux采用ELF,所以可执行文件肯定不能跨系统运行
  2. windows只支持x86体系,所以汇编包括链接是一致的,所以可以跨设备运行
  3. linux不仅支持x86,更是可以支持mips, arm等众多cpu, 所以跨cpu的会得到不同的汇编结果,所以需要重新汇编
  4. 在当前平台编译别的平台运行的程序称为交叉编译,比如在windows或mac上编译android系统的程序就是一种典型的交叉编译,又比如编译单片机程序
  5. 同样一个hello world的c文件在不同的系统上有完全不同的编译链路,windows, mac, linux又或者嵌入式系统

Mac上交叉编译Android的hello world

启动android模拟器,
emulator -list-avds
emulator -avd xxx

这里就需要用到ndk作为交叉编译的工具链,
补充一下交叉工具命名一般规则: arch-[vendor]-kernel-system-toolname

  • arch,指的是CPU架构, 一般包括如下几种架构: arm, mips, powerpc, x86, x86_64。
  • verdor, 一般指的是生产厂商, 如果没有生产厂商可以为空。
  • kernel, 指的目标环境使用的 kernel,以android为例,它使用的是 linux内核,所以在这部分会填写为linux。
  • system, 指的是那个系统, 如androideabi, android
  • toolname: 指的是 gcc, ld, ar等。

android对应的就是arm-linux-andirod-gcc

常用的环境变量:

  • PREFIX: 指明交叉编译后输出的目录。
  • ARCH: 指明交叉编译后输出的CPU架构。
  • CROSS-PREFIX:指明交叉编译前辍 arch-vender-kernel-system
  • SYSROOT: 指明交叉编译目标机器的头文件和库文件目录
  • TOOLCHAIN: 指明交叉编译工具链的位置。
  • PLATFROM: 指明交叉编译时使用的是哪个版本的的头文件和库文件。它是 SYSROOT的一部分。
  • ANDROID_NDK: 指明 Android NDK 所在目录。

android ndk官方定义宿主系统如下:

  • macOS: darwin-x86_64
  • Linux: linux-x86_64
  • 32-bit Windows: windows
  • 64-bit Windows: windows-x86_64

目标系统tag如下:

  • armeabi-v7a: armv7a-linux-androideabi
  • arm64-v8a: aarch64-linux-android
  • x86: i686-linux-android
  • x86-64: x86_64-linux-android

使用 NDK 编译代码主要有三种方法:

  • 基于 Make 的 ndk-build。
  • CMake。
  • 独立工具链,用于与其他编译系统集成,或与基于 configure 的项目搭配使用。

通过下面的片段我们可以看到整个工具链的构成

$ export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/$HOST_TAG
$ export AR=$TOOLCHAIN/bin/aarch64-linux-android-ar
$ export AS=$TOOLCHAIN/bin/aarch64-linux-android-as
$ export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang
$ export CXX=$TOOLCHAIN/bin/aarch64-linux-android21-clang++
$ export LD=$TOOLCHAIN/bin/aarch64-linux-android-ld
$ export RANLIB=$TOOLCHAIN/bin/aarch64-linux-android-ranlib
$ export STRIP=$TOOLCHAIN/bin/aarch64-linux-android-strip

google采用了clang取代gcc/g++,其实大同小异。

什么是ABI?
不同的 Android 手机使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口,即 ABI。ABI 可以非常精确地定义应用的机器代码在运行时如何与系统交互。您必须为应用要使用的每个 CPU 架构指定 ABI。
armeabi-v7a, arm64-v8a, x86, x86-64就是响应的abi

下载NDK

  1. 直接从官方NDK Downloads下载
  2. 通过Android Studio/SDK Manager,下载后在/Users/nonocast/Library/Android/sdk/ndk/20.0.5594570

首先确定目标设备的abi,因为我是在mac上的emulator,所以肯定是x86的,

$adb shell getprop ro.product.cpu.abilist
x86

Mipad

$ adb shell getprop ro.product.cpu.abi    
armeabi-v7a

生成对应arch的toolchain,

python make_standalone_toolchain.py --arch arm --install-dir ~/toolchain/arm
python make_standalone_toolchain.py --arch arm64 --install-dir ~/toolchain/arm64
python make_standalone_toolchain.py --arch x86 --install-dir ~/toolchain/x86
// mipad
/Users/nonocast/toolchain/arm/bin/clang app.c -o app
✗ file app
app: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, not stripped

// emulator
/Users/nonocast/toolchain/x86/bin/clang app.c -o app
➜  src git:(master) ✗ file app
app: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, not stripped

Cross Compiling C/C++ for Android - Nick Desaulniers
搞机:AS自带模拟器AVD Root 和 Xposed安装 - 掘金

$ adb push app /data/local/tmp
$ adb shell /data/local/tmp/app
hello world

继续

app.c

int add(int, int);

int main(void) {
  add(1, 2);
  return 0;
}

我们接着来看add这个方法,

  • prepressing: gcc -E app.c -o app.i
# 1 "app.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 362 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "app.c" 2
int add(int, int);

int main(void) {
  add(1, 2);
  return 0;
}
  • compilation: $gcc -S app.i -o app.s
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 15	sdk_version 10, 15
	.globl	_main                   ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movl	$0, -4(%rbp)
	movl	$1, %edi
	movl	$2, %esi
	callq	_add
	xorl	%esi, %esi
	movl	%eax, -8(%rbp)          ## 4-byte Spill
	movl	%esi, %eax
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

.subsections_via_symbols

对比有add方法定义的app.c生成的asm
app.c

int add(int a, int b) {
  return a + b;
}

int main(void) {
  add(1, 2);
  return 0;
}

app.s

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 15	sdk_version 10, 15
	.globl	_add                    ## -- Begin function add
	.p2align	4, 0x90
_add:                                   ## @add
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	movl	-4(%rbp), %esi
	addl	-8(%rbp), %esi
	movl	%esi, %eax
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	_main                   ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movl	$0, -4(%rbp)
	movl	$1, %edi
	movl	$2, %esi
	callq	_add
	xorl	%esi, %esi
	movl	%eax, -8(%rbp)          ## 4-byte Spill
	movl	%esi, %eax
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

.subsections_via_symbols

对比发现缺少_add的定义,没法完成callq _add

最终导致link error

gcc -c app.s -o app.o
➜  src git:(master) ✗ ld app.o -lSystem
Undefined symbols for architecture x86_64:
  "_add", referenced from:
      _main in app.o
ld: symbol(s) not found for architecture x86_64

换句话说,_add是在link阶段完成申明和调用的对接。

缺什么补什么,gcc -S calc.c -o calc.s
calc.s

	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 10, 15	sdk_version 10, 15
	.globl	_add                    ## -- Begin function add
	.p2align	4, 0x90
_add:                                   ## @add
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	movl	-4(%rbp), %esi
	addl	-8(%rbp), %esi
	movl	%esi, %eax
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

.subsections_via_symbols

然后生成app.o
gcc -c calc.s -o calc.o

最后link的时候合并,
ld app.o calc.o -lSystem -o app

搞定。
回过头来,没有.h一样可以搞定link
所以,.h只不过是提取公因式,没有什么奥秘,无形中形成一个申明定义而已。

app.c

#include <stdio.h>
#include "calc.h"

int main(void) {
  printf("1+2: %d\n", add(1, 2));
  return 0;
}

calc.h

#ifndef CALC_H
#define CALC_H

int add(int a, int b);

#endif

calc.c

#include "calc.h"

int add(int a, int b) {
  return a + b;
}

gcc app.c calc.c -o app

分解步骤

  • 针对每个.c编译汇编生成object file(.o)
  • 将object file link生成可执行文件

lib是什么?

lib就是.o的container
通过ar来完成打包

$ ar -crv libcalc.a calc.o
$ file libcalc.a
libcalc.a: current ar archive random library

linux下lib一般的命名规则:libxxx.a

可以通过ar -t查看,

$ar -t libcalc.a
__.SYMDEF SORTED
calc.o

通过nm查看object file中的symbol

$ nm libcalc.a

libcalc.a(calc.o):
0000000000000000 T _add

client如何调用呢?

ld app.o -lSystem -L. -lcalc -o app

回忆一下之前的link

ld app.o calc.o -lSystem -o app

-L告诉ld link directory,然后-l表示lib name

-lx
This option tells the linker to search for libx.dylib or libx.a in the library search path.  If string x is of the form y.o, then that file is searched for in the same places, but without prepending `lib' or appending `.a' or `.dylib' to the filename.

-Ldir
Add dir to the list of directories in which to search for libraries.  Directories specified with -L are searched in the order they appear on the command line and before the default search path. In Xcode4 and later, there can be a space between the -L and directory.

这样就通过lib完成了模块分离。
静态的好处是容易理解,但问题是lib升级就需要重新编译app,不能自行完成升级,不能被多个app共享。

补充一句gcc link方式,
gcc app.c -L. -lcalc -o app

All about Static Libraries in C - megha mohan - Medium

下面来看动态库,linux下是so, windows下是dll, mac下则是dylib
gcc -shared calc.o -o libcalc.so
gcc app.c -L. -lcalc -o app

当同时存在libcalc.a和libcalc.so时,gcc会优先采用so方式,
同样的语句link app这时候会动态link到libcalc.so, 验证一下,

$ file app
app: Mach-O 64-bit executable x86_64
$ du -h app
16K    app
$ otool -L app
app:
        libcalc.so (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1279.0.0)

对比一下之前libcalc.a的链接,

$ otool -L app
app:
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1279.0.0)

mac的otool对应linux下的ldd.

此时如果移除so则运行会报错,

./app
dyld: Library not loaded: libcalc.so
  Referenced from: /Users/nonocast/Developer/projects/learn-c/03-multi-files/src/./app
  Reason: image not found
[1]    28999 abort      ./app

将libcalc.so ln到/usr/local/lib

ln -s ~/Developer/projects/learn-c/03-multi-files/src/libcalcx.so /usr/local/lib/libcalc.so
./app
1+2=3

那么搜索so的默认路径是什么呢?
默认值: $(HOME)/lib:/usr/local/lib:/lib:/usr/lib

确实如此,放到~/lib下,也能work。

参考:

如何开始做软件测试?

什么是软件测试?

软件测试是一个评估过程,评估软件是否达到了预期,达到预期才能发布,才能交付,从商业角度来说,软件测试是一个很好的“赚钱”方式,如果软件质量差,就会产生更多的后期修改和维护工作,无形中失去了信任和口碑,所以毫无疑问,软件测试是一个必选项 (MUST)。

  • 测试等于质量吗?

如果问题出在需求、设计这些源头上,换句话说"预期"出现了偏差,那么再bug free也毫无意义。所以测试只是决定了产品的"下限", 让你的"下限"不那么吓人。

整个测试的核心逻辑就是"1+1=2", 这里面有3个概念:

  • 实际
  • 预期
  • 判定

在这个3个概念中,预期是最重要的,因为实际是看得到的,而判定是有逻辑的,唯独预期这个东西是虚拟的。不能抓住、夯实预期,那么就很容易出现1+1=3,所以"预期"一定要在不同的人之间强同步。

测试流程有标准格式吗?

答案肯定是没有的,不同的公司规模、不同的产品类型、不同的预算都会导致很大的差异性,团队负责人要在通用测试方式上裁剪、变异出合适的测试方式。汽车刹车软件和游戏APP的测试等级,花费的资源天差地别,所以不要指望一个培训能告诉你团队应该怎么做测试。

传统的流程差不多是开发完成以后由全职测试人员来做测试,对于小团队来说,是否要专门建立一个测试团队,我个人更倾向于不建,不仅仅是为了节约成本,我主张应该由产品、开发、实施人员来共同承担测试工作,更多考虑如下:

  • 每个人都要对质量负责,很多程序员为了方便,炫耀而不考虑可测试性,缺少质量的意识,而对于产品则经常提出一些"天马行空"的想法,别bb,你自己来测。
  • 另外一方面从技能分布来说,程序员可以更好的来做白盒单元测试,开发测试脚本,做接口测试; 产品和实施人员的测试更容易站在客户的角度,因为他们更接近用户。

所以小团队应该只保留测试的最少人员就OK,负责建立测试规则和运转。

流程

  • 同步预期: 输出 CTM (Requirement Traceability Matrix)
  • 制定测试计划:输出 Test Planing
  • 开发测试场景和用例: 输出 Test Scripts
  • 准备测试环境: 输出设备清单(excel)和系统图(cad)
  • 不断的测试: 根据版本完成测试报告(在Test Cases填上测试结果)和缺陷报告
  • 发布!

其他补充:

  • 采用看板类工具同步工作 (如trello, worktile, coding等)
  • 能自动化的全部自动化

测试肯定没有开发难,不要一下子有太高的要求,持续做下去就对了。

参考阅读:

Electron 学习

安装

按guide来说yarn add electron -D就能搞定, 在国内就比较麻烦,

通过@electron/get将平台依赖下载到cache, 然后npm install electron到全局

~  node download.js
Downloading electron-v8.2.1-darwin-x64.zip: [============] 100% ETA: 0.0 seconds
~ npm install electron -g
/usr/local/bin/electron -> /usr/local/lib/node_modules/electron/cli.js

> [email protected] postinstall /usr/local/lib/node_modules/electron/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
> https://opencollective.com/core-js 
> https://www.patreon.com/zloirock 

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> [email protected] postinstall /usr/local/lib/node_modules/electron
> node install.js

+ [email protected]
added 87 packages from 98 contributors in 18.909s

download.js

const { download } = require('@electron/get');

let run = async () => {
	const zipFilePath = await download('8.2.1');
};

run().then(() => {
	console.log('#eof');
}).catch(error => {
	console.log(error);
});

Hello World

Writing Your First Electron App | Electron

app.js

const { app, BrowserWindow } = require('electron')

function createWindow() {
  // Create the browser window.
  let window = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

  // and load the index.html of the app.
  window.loadFile('index.html');
}

app.whenReady().then(createWindow);

然后你就能得到一个GUI

Screen Shot 2020-04-08 at 2 37 54 AM

什么是计算机?

Wikipedia的定义如下:

A computer is a machine that can be instructed to carry out sequences of arithmetic or logical operations automatically via computer programming. Modern computers have the ability to follow generalized sets of operations, called programs. These programs enable computers to perform an extremely wide range of tasks. A "complete" computer including the hardware, the operating system (main software), and peripheral equipment required and used for "full" operation can be referred to as a computer system.

个人理解Computer是一个可以运行程序的电子设备, 通过CPU和相关硬件完成运算和输入输出。如果更加精确一点的话, 可以分为通用系统和专用系统, 一个系统通过硬件和软件组成, 通用系统英文叫做General Purpose System, 也就是我们一般说的Computer, 而专用系统也被翻译为嵌入式系统, 即Embedded System, 嵌入式系统需要在限定成本、
尺寸又或者特定的接口、性能下设计的硬件和软件系统。

嵌入式处理器架构大体可以分为:

  • Micro Control Unit (MCU, 嵌入式微控制器, 俗称单片机)
  • Digital Signal Processor (DSP, 嵌入式DSP处理器)
  • Micro Processor Unit (MPU, 嵌入式微处理器)
  • System on Chip (SoC, 嵌入式片上系统)
  • System on a Programmable Chip (SoPC, 可编程片上系统)

其中, 我比较感兴趣的是:

  • 基于 8bit/32bit microcontroller (微控制器, 比如8051, STM32) 的单片机系统
  • 基于 32bit high-end embedded processor (高端嵌入式处理器, 比如rockchip RK3288) 系统, 采用Linux或Android操作系统

这里强推一下 Carrie Anne 的 Crash Course Computer Science, 中文字幕, 差不多前20集可以帮助你迅速建立起对计算机的概念。

对于计算机来说, 最重要的3个组成部分是:

  • CPU
  • Memory
  • I/O

简单来说, CPU的核心ALU(Arithmetic & Logic Unit)通过bus(总线)从ROM中读取instruction(指令), 也就是常说的fetch phase(取指令阶段),进而decode, 最后execute, 然后将执行结果写入register或RAM, 周而复始。

CPU很单纯, 但是Memory就有些麻烦, 我们用树状结构来表达:

  • System
    • CPU
    • Memory (内存, 内部存储器, 記憶體)
      • CACHE
      • Primary Memory (主存, 主存储器)
        • RAM
        • ROM
    • I/O (外部设备)
      • External Storage (外部设备)
        • Hard Drive (硬盘)
        • Flash Drive (U盘)
      • Input Device (输入设备)
        • Keyboard
        • Mouse
      • Output Device (输出设备)
        • Display (显示器)
        • Speaker (音响)

几点说明:

  • 很多人会认为计算机存储分为RAM和ROM, 如果把RAM理解为内存条, 那么硬盘就变成了ROM, 其实这句话应该这样说:计算机内存包括RAM和ROM, 硬盘是外部设备, 不是memory。
  • 对于单片机来说RAM和ROM都集成在了单片机中, 当程序足够放在ROM时则不需要外设, 而程序通过ISP直接写到ROM中, MCU上电后直接运行ROM中0000H的instruction(指令)
  • 对于Computer, 就是我们平时用的电脑来说, RAM是内存条, 而ROM则是由CMOS作为介质的设备, 其中写入的程序称为BIOS, 是由生产商提供的硬件引导程序, 通过BIOS引导到硬盘上, 进而把操作系统引导起来
  • 我们平时双击运行程序, 可以理解为操作系统将软件image加载进内存条(实则是主存的RAM), 所以说内存条这个说法也挺害人的, 为什么要加载呢? 因为CPU运行硬盘上的指令太慢, 和RAM差太多了, 但是单片机运行程序则不需要从ROM中加载到RAM中, 第一可以直接寻址, 第二ROM足够快
  • 单片机中的ROM和U盘都采用EEPROM作为存储媒介, 但单片机中的EEPROM就上升为ROM, 而U盘就是外部设备, 所以这个就和CPU寻址有直接关系

Transistor (晶体管)

整个计算机中最重要的就是晶体管, 事实上晶体管可以理解为一个没有机械结构的开关(switch), 通过一个弱小的电流控制开关的打开和闭合。

推荐这个视频 Transistors, How do they work ? - YouTube, 墙内就这个链接(bilibili)

  • Semiconductor (半导体)
  • 硅(Si)是四键, 通过和其他元素混合形成特定效果
  • N-type DOPING (N型参杂, 参杂五键的麟(Boracium, B))
  • P-type DOPING (P型参杂, 参杂三键的硼(Phosphorum, P))
  • BJT (双极性晶体管) 和 FET (场效应晶体管)
  • 整个计算逻辑都是在晶体管之间完成

0和1是如何转化为电的?

其实从来就是电, 整个计算机都是电的世界, CPU的输入输出以及存储都是采用电的形式, 0和1的世界是虚构出来, 最终的物理载体就是电信号。

换句话说, 我们是用一种意识在操纵整个电路。

见知乎讨论
代码是如何控制硬件的? - 知乎

参考阅读

Github API 学习

GitHub Developer | GitHub Developer Guide

REST API v3

overview

  • 全程采用https, https://api.github.com
  • All timestamps return in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
  • POST表示新建, PATCH表示更新
  • 错误处理
    • 当传入错误时,返回400
    • 认证失败返回401
    • 权限scope不够返回404, 这个有点妖
    HTTP/1.1 400 Bad Request
    Content-Length: 35
    
    {"message":"Problems parsing JSON"}
    
    • 当内容参数有问题时,返回422, code包括missing, missing_field, invalid, already_exists
    HTTP/1.1 422 Unprocessable Entity
    Content-Length: 149
    
    {
      "message": "Validation Failed",
      "errors": [
        {
          "resource": "Issue",
          "field": "title",
          "code": "missing_field"
        }
      ]
    }
    
  • HTTP verbs: HEAD, GET, POST, PATCH, PUT, DELETE
  • 分页: https://api.github.com/user/repos?page=2&per_page=100 (Note that page numbering is 1-based and that omitting the ?page parameter will return the first page.)
  • 支持rate limiting, 回告知X-RateLimit-Limit, Remaining, Reset

OAuth 认证

原有3.0的oauth被新的web application flow所取代.

标准的2步:

获取token后嵌入header发送, curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com/user

scope: Understanding scopes for OAuth Apps

  • (no scope): Grants read-only access to public information (includes public user profile info, public repository info, and gists)
  • repo: Grants full access to private and public repositories. That includes read/write access to code, commit statuses, repository and organization projects, invitations, collaborators, adding team memberships, deployment statuses, and repository webhooks for public and private repositories and organizations. Also grants ability to manage user projects.
  • user: Grants read/write access to profile info only. Note that this scope includes user:email and user:follow.

简单尝试一下,

List users repositories (这个不需要权限, 公开信息)

~ curl https://api.github.com/users/nonocast
{
  "login": "nonocast",
  "id": 1457904,
  "node_id": "MDQ6VXNlcjE0NTc5MDQ=",
  "avatar_url": "https://avatars0.githubusercontent.com/u/1457904?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/nonocast",
  "html_url": "https://github.com/nonocast",
  "followers_url": "https://api.github.com/users/nonocast/followers",
  "following_url": "https://api.github.com/users/nonocast/following{/other_user}",
  "gists_url": "https://api.github.com/users/nonocast/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/nonocast/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/nonocast/subscriptions",
  "organizations_url": "https://api.github.com/users/nonocast/orgs",
  "repos_url": "https://api.github.com/users/nonocast/repos",
  "events_url": "https://api.github.com/users/nonocast/events{/privacy}",
  "received_events_url": "https://api.github.com/users/nonocast/received_events",
  "type": "User",
  "site_admin": false,
  "name": "Hui",
  "company": null,
  "blog": "http://nonocast.cn",
  "location": "Shanghai",
  "email": null,
  "hireable": true,
  "bio": null,
  "public_repos": 44,
  "public_gists": 1,
  "followers": 29,
  "following": 18,
  "created_at": "2012-02-21T16:01:32Z",
  "updated_at": "2020-02-22T17:45:15Z"
}

同样获取repos, curl https://api.github.com/users/nonocast/repos

Get the authenticated user, 这个时候就需要token,同时包含user scope

~ curl https://api.github.com/user -i
HTTP/1.1 401 Unauthorized

{
  "message": "Requires authentication",
  "documentation_url": "https://developer.github.com/v3/users/#get-the-authenticated-user"
}

这里可以通过postman获取oauth2.0 token, 参数如下:

注意这里需要将OAuth App中的Callback回postman, 即设置为https://app.getpostman.com/oauth2/callback

拿到token后就可以访问/user

{
    "login": "nonocast",
    "id": 14500004,
    "node_id": "MDQ6V***Tc5MDQ=",
    "avatar_url": "https://avatars0.githubusercontent.com/u/145***4?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/nonocast",
    "html_url": "https://github.com/nonocast",
    "followers_url": "https://api.github.com/users/nonocast/followers",
    "following_url": "https://api.github.com/users/nonocast/following{/other_user}",
    "gists_url": "https://api.github.com/users/nonocast/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/nonocast/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/nonocast/subscriptions",
    "organizations_url": "https://api.github.com/users/nonocast/orgs",
    "repos_url": "https://api.github.com/users/nonocast/repos",
    "events_url": "https://api.github.com/users/nonocast/events{/privacy}",
    "received_events_url": "https://api.github.com/users/nonocast/received_events",
    "type": "User",
    "site_admin": false,
    "name": "Hui",
    "company": null,
    "blog": "http://nonocast.cn",
    "location": "Shanghai",
    "email": "no***[email protected]",
    "hireable": true,
    "bio": null,
    "public_repos": 44,
    "public_gists": 1,
    "followers": 1,
    "following": 1,
    "created_at": "2012-02-21T16:01:32Z",
    "updated_at": "2020-02-29T22:19:58Z",
    "private_gists": 1,
    "total_private_repos": 0,
    "owned_private_repos": 0,
    "disk_usage": 38365,
    "collaborators": 0,
    "two_factor_authentication": false,
    "plan": {
        "name": "free",
        "space": 976562499,
        "collaborators": 0,
        "private_repos": 10000
    }
}

这里关注一下restful的文档结构, github会对Parameters作出一个明确的说明,不管是GET还是POST,

比如Update the authenticated user, `PATCH /user'

Name Type Description
name string The new name of the user.
email string The publicly visible email address of the user.
blog string Thre new blog URL of the user.
company string The new company of the user.
location string The new location of the user.
hireable boolean The new hiring availability of the user.
bio string The new short biography of the user.

然后给出一个Example,

{
  "name": "monalisa octocat",
  "email": "[email protected]",
  "blog": "https://github.com/blog",
  "company": "GitHub",
  "location": "San Francisco",
  "hireable": true,
  "bio": "There once..."
}

提交后的Response是一个全量的json

{
  "login": "nonocast",
  "id": 1,
  ...
}

查询也是一样会有Parameters,比如List issues, 给出了3个途径,

  • GET /issues
  • GET /user/issues
  • GET /orgs/:org/issues

Parameters

Name Type Description
filter string Indicates which sorts of issues to return. Can be one of:* assigned: Issues assigned to you* created: Issues created by you* mentioned: Issues mentioning you* subscribed: Issues you're subscribed to updates for* all: All issues the authenticated user can see, regardless of participation or creationDefault: assigned
state string Indicates the state of the issues to return. Can be either open, closed, or all. Default: open
labels string A list of comma separated label names. Example: bug,ui,@high
sort string What to sort results by. Can be either created, updated, comments. Default: created
direction string The direction of the sort. Can be either asc or desc. Default: desc
since string Only issues updated at or after this time are returned. This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.

然后来看看github对api的naming

  • Repositories
    • List your repositories
    • List user repositories
    • List organization repositories
    • List all public repositories
    • Create
    • Create repository using a repository template
    • Get
    • Edit
    • List all topics for a repository
    • Replace all topics for a repository
    • Check if vulnerability alerts are enabled for a repository
    • Enable vulnerability alerts
    • Disable vulnerability alerts
    • Enable automated security fixes
    • Disable automated security fixes
    • List contributors
    • List languages
    • List teams
    • List tags
    • Delete a repository
    • Transfer a repository
    • Create a repository dispatch event
  • Branches
    • List branches
    • Get branch
    • Get branch protection
    • Update branch protection
    • Remove branch protection
    • Get required status checks of protected branch
    • Update required status checks of protected branch
    • Remove required status checks of protected branch
    • List required status checks contexts of protected branch
    • Replace required status checks contexts of protected branch
    • Add required status checks contexts of protected branch
    • Remove required status checks contexts of protected branch
    • Get pull request review enforcement of protected branch
    • Update pull request review enforcement of protected branch
    • Remove pull request review enforcement of protected branch
    • Get required signatures of protected branch
    • Add required signatures of protected branch
    • Remove required signatures of protected branch
    • Get admin enforcement of protected branch
    • Add admin enforcement of protected branch
    • Remove admin enforcement of protected branch
    • Get restrictions of protected branch
    • Remove restrictions of protected branch
    • List teams with access to protected branch
    • Replace team restrictions of protected branch
    • Add team restrictions of protected branch
    • Remove team restrictions of protected branch
    • List users with access to protected branch
    • Replace user restrictions of protected branch
    • Add user restrictions of protected branch
    • Remove user restrictions of protected branch
    • List apps with access to protected branch
    • Replace app restrictions of protected branch
    • Add app restrictions of protected branch
    • Remove app restrictions of protected branch
  • Project
    • List repository projects
    • List organization projects
    • List user projects
    • Get a project
    • Create a repository project
    • Create an organization project
    • Create a user project
    • Update a project
    • Delete a project
  • Organizations
    • List your organizations
    • List all organizations
    • List user organizations
    • List installations for an organization
    • Get an organization
    • Edit an organization
    • List credential authorizations for an organization
    • Remove a credential authorization for an organization

这里列了这么多,想表达一个很重要的观点,我们常规在设计API的时候常常会List, Get, Post, Delete something,但是github做的很细,把行为做的非常强类型。

这里提一下REST API v3右侧除了Reference还有一个Guides,里面有很多实践内容,帮助理解API。

最后再看一下scope,New personal access token这个页面列举了所有scope,可以依次参考一下,

所以github在设计认证和授权的时候采用了标准的OAuth2.0方案,主要的条线是: user - token - scope

GraphQL API v4

https://api.github.com/graphql

可以通过explorer来实践。

GraphQL提供了一个更好的框架,尤其是解决了原本多个rest call合并,同时按需获取数据提供了更大的优势。

Authentication

和REST同样,首先还是需要通过OAuth token获取right scopes。所以这个地方和前面是一致的。

curl -H "Authorization: bearer token" -X POST -d " \
 { \
   \"query\": \"query { viewer { login }}\" \
 } \
" https://api.github.com/graphql

GraphQL文档方式就可以通过Explorer提供的强类型文档直接看,更方便。

如何记录日志? -- node/winston

日志是程序很重要的组成部分,程序在线上只能通过日志才能观察到运行情况,所以好的日志能够帮助你了解情况,分析问题。

一般来说,日志记录应以rotate方式记录成文件,常规日志文件会采用plain text,

[INFO] 11:06:12 login ok.

但现在更多的倾向于持久化为结构化JSON,

{"requestId":"0f111049-09f9-4f8e-9306-900652c833bf","filename":"server/middleware/koa-winston.js","line":14,"level":"info","message":"--> GET /oauth/me?token=WZbxb6O5 200","timestamp":1581954414}

在生产大量日志后,经由filebeat推送到ELK进行分析,这个单开issues说明。
来看winston使用方式,

const winston = require('winston');
const { format } = winston;

const logger = winston.createLogger({
  format: format.combine(
    format.colorize(),
    format.simple()
  ),
  transports: [
    new winston.transports.Console()
  ]
});

logger.log('info', 'some message');
logger.info('some info');
logger.error('some error');

输出内容:

$ node app
info: some message
info: some info
error: some error

Log调用方法

logger.log('info', 'hello world');

logger.log({
  level: 'info',
  message: 'hello world'
});

// string interpolation
// format中必须开启format.splat()
logger.log('info', 'hello world, %s, %s', 'hui', 'http://nonocast.cn');

logger.log({
  level: 'info',
  message: 'hello world, %s, %s',
  splat: ['hui', 'http://nonocast.cn']

});


logger.log({
  level: 'info',
  message: 'hello world, %s',
  splat: ['hui'],
  tag: 'rx'
});

logger.log({
  level: 'info',
  message: 'hello world',
  user: {
    name: 'hui',
    homepage: 'http://nonocast.cn'
  }
});

注:

  • splat 就是对应 string interpolation, 实现字符串格式化。
  • level: error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6。

The info parameter provided to a given format represents a single log message. The object itself is mutable. Every info must have at least the level and message properties.

format

format是一个对info处理的chain,类似middleware:

new winston.transports.File({
  filename: 'app.log',
  format: format.combine(
    format(info => {
      info.message = strip(info.message);
      return info;
    })(),
    format.json()
  )
})

transport

transport表达的就是记录通道,这个比较容易理解。

container

借由winson.loggers这个container管理多个logger。
比如一个category给service,一个给web controller。

winston.loggers.add('category1', { format: ..., transports: ... });
winston.loggers.add('category2', { format: ..., transports: ... });

let logger = winson.loggers.get('category2');

error

format.errors({ stack: true })

然后如果 info(new Error(...)) 就会输出stack

输出的stack内容,

{"level":"info","message":"Yo, it's on fire","stack":"Error: Yo, it's on fire\n at Object. (/Users/nonocast/Desktop/hello-winston/app.js:34:20)\n at Module._compile (internal/modules/cjs/loader.js:701:30)\n at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)\n at Module.load (internal/modules/cjs/loader.js:600:32)\n at tryModuleLoad (internal/modules/cjs/loader.js:539:12)\n at Function.Module._load (internal/modules/cjs/loader.js:531:3)\n at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)\n at startup (internal/bootstrap/node.js:283:19)\n at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)"}

query

可以通过logger查询记录,这个可以的。

const options = {
  from: moment().subtract(1, 'hours').unix(),
  until: moment().unix(),
  limit: 10,
  start: 0,
  order: 'desc',
  fields: ['message']
};

//
// Find items logged between today and yesterday.
//
logger.query(options, function (err, results) {
  if (err) {
    /* TODO: handle me */
    throw err;
  }

  console.log(results);
});

Koa 整合

先来看koa-logger,

const logger = require('koa-logger');
app.use(logger());

一个请求输出两个,但是信息量实在太少了,

 <-- GET /
  --> GET / 200 9ms 12b

然后logger可以拦截,

  .use(logger((str, args) => {
    // redirect koa logger to other output pipe
    // default is process.stdout(by console.log function)

}));
  • Param str is output string with ANSI Color, and you can get pure text with other modules like strip-ansi. 带颜色的字符串。
  • Param args is a array by [format, method, url, status, time, length]

整个koa-logger连注释157行,简单看一下他是怎么拦截请求的:

  • 在middleware先行log请求
  • 然后在await next()等同于所有middleware结束以后再记录结束log

所以我们借用这个套路配合koa-requestid写一个koa-winston的middleware,

const chalk = require('chalk');

module.exports = (logger) => {
  return async function (ctx, next) {
    let child = logger.child({ requestId: ctx.state.id });

    ctx.logger = child;

    ctx.logger.info(`${chalk.gray('<--')} ${chalk.bold(ctx.method)} ${chalk.gray(ctx.originalUrl)}`);

    try {
      await next()
    } catch (err) {
      ctx.logger.warn(err);
      throw err
    }

    ctx.logger.info(`${chalk.gray('-->')} ${chalk.bold(ctx.method)} ${chalk.gray(ctx.originalUrl)}`);
  }
}

React 运行机制

不谈具体源代码,尝试总结一下React运行机制,很多都是推理和猜测,仅供参考:

  • react事实上提供了一层UI抽象, 非常类似WPF的logical tree和visual tree,对应vdom和dom
  • App是React Component, 是React Element
  • 聚焦react, react-dom这两个package
  • react提供多种方式(class, function)组装和转换为React Element
  • react-dom负责从reactElement(vdom)渲染到落地的dom, element的create和render都是油react-dom负责
  • 整个render过程可以简单分为几步:
    • 根节点或者发生state变化的element起开始render, 当一个element render时他下属所有节点都会遍历收到render,除非should中明确告诉他我拒绝render
    • element的render生成vdom片段,然后和当前的vdom diff,最后进而生成dom
    • render中所写的JSX本身是一种syntax sugar(语法糖),其实质是react.createElement(这部分的转换由babel plugin完成),所以在动态时完全可以采用动态createElement的方式取代JSX
    • render不能有任何副作用 (Must not have side effect)

实践中:

  • 应该为每个使用 class 声明的组件添加 shouldComponentUpdate,否则一旦接受新的 props/state 就可能进行不必要的 re-render
  • 如果是array给定固定key

参考:

什么是单点登录?

OAuth 2.0 是目前最流行的授权机制,用来授权第三方应用,获取用户数据。

阮一峰的文章写的已经非常清楚了,我简单总结一下我对SSO的观点。

为什么需要单点登录?

简单阐述一下动机,比如你要开发一个邮件App,用户通过"登录"可以获取自己gmail邮件,最简单的就是你给用户一个登录界面输入用户在google的用户名和密码,但这个时候用户就会担心现在你(App的供应商)有了用户名和密码,就可以用这个密码去gmail登录进而看到所有用户邮件,这个肯定不行, 但其实google也希望第三方App在不接触最终用户的用户名和密码的情况下来扩展自身生态。

基于资源方、第三方(ISV)和终端用户的几方面诉求,就产生了OAuth (RFC6749),现在的OAuth2.0在2012年定稿,所以已经非常成熟了。

但很多人接触OAuth是来自于另外一个场景,公司需要开发两个网站共享一套登录,所以很多人会认为OAuth是用来做身份验证的。

其实不管哪种场景,殊途同归,就好比刀可以切菜,也可以砍人。你把底层的原理想明白就ok。

服务接口的登录设计

我是在做前后端分离的时候才真正开始接触单点登录,当时后端采用node,前端采用react,如果在不借助cookie和session的方式下如何设计登录和授权方式。最终兜兜转转才发现OAuth2就是最好的解决方案:

  • backend service: 对应资源方
  • react webapp: 对应第三方App

你就会发现原来是react这个webapp需要通过OAuth方式去获取backend service中的数据,这就回到文中第一句的定义。

所以一切需要问service获取数据的登录授权才会归结为OAuth这个标准下,那为什么OAuth会有多种模式呢?

  • Authorization Code (授权码)
  • Implicit (隐藏方式)
  • Password (密码)
  • Client Credentials (客户端凭证)
  • Device Code (设备码)

最常见的是授权码方式,第三方引用先申请一个授权码,然后再用该码获取令牌,大致流程如下:

  • 访问webapp.com,然后点击登录,此时跳转到service的authorize,这里要注意,浏览器会在跳转到service时携带service域的cookie
  • 如果检查到cookie就表示用户之前已经登录,302到用户注册的callback或携带过来的callback参数,即返回到第三方应用的后台,url中会携带授权码(code),第三方应用后台根据code获取service token,这个步骤在第三方后台完成,后台根据token来获取用户信息和授权数据,渲染页面返回浏览器。(第三方应用的后台和页面的关系可由他们的cookie/session来维系,这个不在OAuth的范围内)
  • 如果检查到没有service cookie, 则给出login页面,用户登录后POST回service authorize进行验证,通过则callback回第三方应用后台。
  • 借助service域cookie的延续性实现了“记住密码”功能

这里需要注意2点:

  • 302是发生在用户侧,即浏览器,浏览器拿到302的http response后进行重定向
  • cookie是不安全的,即使是SSO也不能保存password,不管是明文还是密文,一定会被劫持,所以只能保存有时效性的token/refresh token来实现"记住密码"的机制

在充分理解授权码方式后,就引入了第二个问题,如果webapp是一个react,你也可以理解为html中包含了Javascript, 即第三方应用没有后台,只有前端,所以这个时候service在得到cookie后应直接callback回前端页面,而不是第三方应用的后端。

所以在开发前后端分离的应用时应该选择Implicit方式。

最后来看Password和Credentials,事实上是作为前面的补充,比如你做一个Digital Signage (信息发布)或者kiosk, 只是用来展示信息的应用,就需要用到Credentials方式,后台需要管理设备对应的凭证。

而Password更多的是用在命令行和scripting,即非浏览器环境,用户可以确保安全的场景下使用,所以很多第三方的服务是关闭Password这种模式的。

参考阅读:

Linux C - serial port

参考阅读:

#include <errno.h> /* Error number definitions */
#include <fcntl.h> /* File control definitions */
#include <stdio.h> /* Standard input/output definitions */
#include <stdlib.h>
#include <string.h>  /* String function definitions */
#include <termios.h> /* POSIX terminal control definitions */
#include <unistd.h>  /* UNIX standard function definitions */

#define FALSE -1
#define TRUE 0

/*
 * 'open_port()' - Open serial port 1.
 *
 * Returns the file descriptor on success or -1 on error.
 */

int open_port(void) {
  int fd; /* File descriptor for the port */
  // char *dev = "/dev/tty.usbserial-14310";
  char *dev = "/dev/cu.SLAB_USBtoUART";
  // char *dev = "/dev/cu.usbserial-0001";

  fd = open(dev, O_RDWR | O_NOCTTY | O_NDELAY);
  if (fd == -1) {
    perror("open_port: Unable to open port");
  } else {
    fcntl(fd, F_SETFL, 0);
  }

  return (fd);
}

/**
 *@brief  设置串口通信速率
 *@param  fd     类型 int  打开串口的文件句柄
 *@param  speed  类型 int  串口速度
 *@return  void
 */
void set_speed(int fd) {
  int i;
  int status;
  struct termios Opt;
  tcgetattr(fd, &Opt);
  tcflush(fd, TCIOFLUSH);
  cfsetispeed(&Opt, B57600);
  cfsetospeed(&Opt, B57600);
  status = tcsetattr(fd, TCSANOW, &Opt);
  if (status != 0) {
    perror("tcsetattr fd");
    return;
  }
  tcflush(fd, TCIOFLUSH);
}

/**
 *@brief   设置串口数据位,停止位和效验位
 *@param  fd     类型  int  打开的串口文件句柄
 *@param  databits 类型  int 数据位   取值 为 7 或者8
 *@param  stopbits 类型  int 停止位   取值为 1 或者2
 *@param  parity  类型  int  效验类型 取值为N,E,O,,S
 */
int set_Parity(int fd, int databits, int stopbits, int parity) {
  struct termios options;
  if (tcgetattr(fd, &options) != 0) {
    perror("SetupSerial 1");
    return (FALSE);
  }
  options.c_cflag &= ~CSIZE;
  switch (databits) /*设置数据位数*/
  {
  case 7:
    options.c_cflag |= CS7;
    break;
  case 8:
    options.c_cflag |= CS8;
    break;
  default:
    fprintf(stderr, "Unsupported data size\n");
    return (FALSE);
  }
  switch (parity) {
  case 'n':
  case 'N':
    options.c_cflag &= ~PARENB; /* Clear parity enable */
    options.c_iflag &= ~INPCK;  /* Enable parity checking */
    break;
  case 'o':
  case 'O':
    options.c_cflag |= (PARODD | PARENB); /* 设置为奇效验*/
    options.c_iflag |= INPCK;             /* Disnable parity checking */
    break;
  case 'e':
  case 'E':
    options.c_cflag |= PARENB;  /* Enable parity */
    options.c_cflag &= ~PARODD; /* 转换为偶效验*/
    options.c_iflag |= INPCK;   /* Disnable parity checking */
    break;
  case 'S':
  case 's': /*as no parity*/
    options.c_cflag &= ~PARENB;
    options.c_cflag &= ~CSTOPB;
    break;
  default:
    fprintf(stderr, "Unsupported parity\n");
    return (FALSE);
  }
  /* 设置停止位*/
  switch (stopbits) {
  case 1:
    options.c_cflag &= ~CSTOPB;
    break;
  case 2:
    options.c_cflag |= CSTOPB;
    break;
  default:
    fprintf(stderr, "Unsupported stop bits\n");
    return (FALSE);
  }
  /* Set input parity option */
  if (parity != 'n')
    options.c_iflag |= INPCK;
  tcflush(fd, TCIFLUSH);
  options.c_cc[VTIME] = 150; /* 设置超时15 seconds*/
  options.c_cc[VMIN] = 0;    /* Update the options and do it NOW */
  if (tcsetattr(fd, TCSANOW, &options) != 0) {
    perror("SetupSerial 3");
    return (FALSE);
  }
  return (TRUE);
}

unsigned char *bin_to_strhex(const unsigned char *bin, unsigned int binsz,
                             unsigned char **result) {
  unsigned char hex_str[] = "0123456789abcdef";
  unsigned int i;

  if (!(*result = (unsigned char *)malloc(binsz * 2 + 1)))
    return (NULL);

  (*result)[binsz * 2] = 0;

  if (!binsz)
    return (NULL);

  for (i = 0; i < binsz; i++) {
    (*result)[i * 2 + 0] = hex_str[(bin[i] >> 4) & 0x0F];
    (*result)[i * 2 + 1] = hex_str[(bin[i]) & 0x0F];
  }
  return (*result);
}

int main(void) {
  int fd;
  int nread;
  char buff[512];
  // char sendBuffer[5] = "\x04\x00\x01\xdb\x4b";
  unsigned char sendBuffer[5] = {0x04, 0x00, 0x01, 0xdb, 0x4b};

  fd = open_port();

  set_speed(fd);
  if (set_Parity(fd, 8, 1, 'N') == FALSE) {
    perror("Set Parity Error\n");
    exit(0);
  }

  printf("serial port open OK\n");

  int ret = write(fd, sendBuffer, 5);
  printf(">>> %d\n", ret);

  while (1) {
    while ((nread = read(fd, buff, 512)) > 0) {
      printf("nread: %d\n", nread);
      // buff[nread] = 0x00;
      // printf("%s", buff);
      unsigned char *result;
      printf("result : %s\n",
             bin_to_strhex((unsigned char *)buff, nread, &result));
      free(result);
    }
  }

  close(fd);
  return 0;
}

Mac OSX 终端走shadowsocks代理

默认打开ss后只有浏览器的http走ss,而终端不会走ss,可以通过外部服务交叉验证,浏览器打开ip.cn显示和终端查看会显示你在两个zone,

~ curl ipinfo.io
{
  "ip": "10*.8*.*3.9",
  "city": "Shanghai",
  "region": "Shanghai",
  "country": "CN",
  "loc": "****, ****",
  "org": "AS4812 China Telecom (Group)",
  "timezone": "Asia/Shanghai",
  "readme": "https://ipinfo.io/missingauth"
}
~ curl myip.ipip.net
当前 IP:10*.8*.*3.9  来自于:** 上海 上海  电信

配置shell通过sock连上ss,

.zshrc

# proxy list
alias proxy='export all_proxy=socks5://127.0.0.1:1086'
alias unproxy='unset all_proxy'

注:

  1. 保存后source, 然后在shell中通过proxy, unproxy进行切换
  2. 1086的端口需要在ss的sock5中确认
~ curl myip.ipip.net
当前 IP:10*.10*.6.*4  来自于:日本 东京都 东京  xxxxxxxx

参考阅读:

rtmp and hls in nginx

prepare

# check system version
$ lsb_release -ds
Debian GNU/Linux 10 (buster)

$ sudo apt install -y build-essential git tree software-properties-common dirmngr apt-transport-https ufw
# optional dependencies
$ sudo apt install -y perl libperl-dev libgd3 libgd-dev libgeoip1 libgeoip-dev geoip-bin libxml2 libxml2-dev libxslt1.1 libxslt1-dev

# PCRE version 8.42
$ wget https://ftp.pcre.org/pub/pcre/pcre-8.42.tar.gz && tar xzvf pcre-8.42.tar.gz

# zlib version 1.2.11
$ wget https://www.zlib.net/zlib-1.2.11.tar.gz && tar xzvf zlib-1.2.11.tar.gz

# OpenSSL version 1.1.1a
$ wget https://www.openssl.org/source/openssl-1.1.1a.tar.gz && tar xzvf openssl-1.1.1a.tar.gz

compile

$ mkdir nginx && cd $_
$ wget http://nginx.org/download/nginx-1.17.4.tar.gz
$ tar -zxvf nginx-1.17.4.tar.gz
$ ./configure --help
$ ./configure --with-pcre=../pcre-8.42 --with-zlib=../zlib-1.2.11 --with-openssl=../openssl-1.1.1a
$ make
$ sudo make install

说明:

  • configure --help可以查看编译配置,打开关闭了哪些模块
  • --prefix=PATH 设置程序安装路径, 默认/usr/local/nginx
  • --sbin-path=PATH 设置可执行文件路径, 默认prefix/sbin/nginx
  • --config-path=PATH 设置配置文件路径, 默认prefix/conf/nginx.conf

install以后安装/usr/local/nginx

$ pwd
/usr/local/nginx
$ ls
conf  html  logs  sbin
$ nginx tree -L 2 .
.
|-- conf
|   |-- fastcgi.conf
|   |-- fastcgi.conf.default
|   |-- fastcgi_params
|   |-- fastcgi_params.default
|   |-- koi-utf
|   |-- koi-win
|   |-- mime.types
|   |-- mime.types.default
|   |-- nginx.conf
|   |-- nginx.conf.default
|   |-- scgi_params
|   |-- scgi_params.default
|   |-- uwsgi_params
|   |-- uwsgi_params.default
|   `-- win-utf
|-- html
|   |-- 50x.html
|   `-- index.html
|-- logs
`-- sbin
    `-- nginx

4 directories, 18 files

$ /usr/local/nginx/sbin/nginx -v
nginx version: nginx/1.17.4

# link到系统路径下
$ sudo ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx 
$ sudo nginx -v
nginx version: nginx/1.17.4

# START
$ sudo nginx

然后浏览器 http://ip:port 就可以看到Welcome to nginx!

也可以用nc(netcat)来检查

~ nc localhost 80
GET /
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

NRM (nginx-rtmp-module)

NGINX-based Media Streaming Server

Features

  • RTMP/HLS/MPEG-DASH live streaming
  • RTMP Video on demand FLV/MP4, playing from local filesystem or HTTP
  • Stream relay support for distributed streaming: push & pull models
  • Recording streams in multiple FLVs
  • H264/AAC support
  • Online transcoding with FFmpeg
  • HTTP callbacks (publish/play/record/update etc)
  • Running external programs on certain events (exec)
  • HTTP control module for recording audio/video and dropping clients
  • Advanced buffering techniques to keep memory allocations at a minimum level for faster streaming and low memory footprint
  • Proved to work with Wirecast, FMS, Wowza, JWPlayer, FlowPlayer, StrobeMediaPlayback, ffmpeg, avconv, rtmpdump, flvstreamer -and many more
  • Statistics in XML/XSL in machine- & human- readable form
  • Linux/FreeBSD/MacOS/Windows

简单来说这个模块就是nginx+ffmpeg, 实现了rtmp, hls, mpeg-dash直播, 支持rtmp, hls点播, 支持录播。

$ git clone https://github.com/arut/nginx-rtmp-module.git

# 重新回炉编译
$ ./configure --with-pcre=../pcre-8.42 --with-zlib=../zlib-1.2.11 --with-openssl=../openssl-1.1.1a --add-module=../nginx-rtmp-module 
# 这时会在ngx_rmtp_eval.c:170:13处有一个warning, 所以需要加上忽略warning的设置
$ ./configure --with-pcre=../pcre-8.42 --with-zlib=../zlib-1.2.11 --with-openssl=../openssl-1.1.1a --add-module=../nginx-rtmp-module --with-debug --with-cc-opt="-Wimplicit-fallthrough=0"

# configure中会显示module信息
configuring additional modules
adding module in ../nginx-rtmp-module
 + ngx_rtmp_module was configured
 
$ make
$ sudo make install
$ sudo nginx -V
nginx version: nginx/1.17.4
built by gcc 8.3.0 (Debian 8.3.0-6) 
built with OpenSSL 1.1.1a  20 Nov 2018
TLS SNI support enabled
configure arguments: --with-pcre=../pcre-8.42 --with-zlib=../zlib-1.2.11 --with-openssl=../openssl-1.1.1a --add-module=../nginx-rtmp-module --with-debug --with-cc-opt=-Wimplicit-fallthrough=0

VOD

nginx.conf

rtmp { 
  server { 
    listen 1935; 
    chunk_size 4096; 
    application vod { 
      play /var/video; 
    }
  } 
}     

sudo nginx -s reload后vlc rtmp://192.168.3.132/vod/envoy.mp4 就可以播放视频了。

ffplay也ok,

# fs means fullscreen
ffplay -fs rtmp://192.168.3.132/vod/envoy.mp4
ffplay version 4.0 Copyright (c) 2003-2018 the FFmpeg developers
  built with Apple LLVM version 9.1.0 (clang-902.0.39.1)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/4.0 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-gpl --enable-ffplay --enable-libmp3lame --enable-libx264 --enable-libxvid --enable-opencl --enable-videotoolbox --disable-lzma
  libavutil      56. 14.100 / 56. 14.100
  libavcodec     58. 18.100 / 58. 18.100
  libavformat    58. 12.100 / 58. 12.100
  libavdevice    58.  3.100 / 58.  3.100
  libavfilter     7. 16.100 /  7. 16.100
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  1.100 /  5.  1.100
  libswresample   3.  1.100 /  3.  1.100
  libpostproc    55.  1.100 / 55.  1.100
Input #0, flv, from 'rtmp://192.168.3.132/vod/envoy.mp4':0B f=0/0   
  Metadata:
    displayWidth    : 1280
    displayHeight   : 720
  Duration: 00:02:20.44, start: 0.000000, bitrate: N/A
    Stream #0:0: Video: h264 (High), yuv420p(tv, bt709, progressive), 1280x720 [SAR 1:1 DAR 16:9], 24.42 fps, 23.98 tbr, 1k tbn, 47.95 tbc
    Stream #0:1: Audio: aac (LC), 48000 Hz, stereo, fltp

LIVE

rtmp { 
  server { 
    listen 1935; 
    chunk_size: 4096; 
    application vod { 
      play /var/video; 
    }
    application live {
      live on;
    }
  } 
}     

通过ffmpeg推流

$ ffmpeg -i envoy.mp4 -vcodec libx264 -acodec aac -f flv rtmp://192.168.3.132:1935/live/envoy

-f flv是指rtmp
然后再次通过vlc/ffplay验证即可。

注: 这个1935可以省略, nginx会自动根据protocol判定。

HLS

# HLS

# For HLS to work please create a directory in tmpfs (/tmp/hls here)
# for the fragments. The directory contents is served via HTTP (see
# http{} section in config)
#
# Incoming stream must be in H264/AAC. For iPhones use baseline H264
# profile (see ffmpeg example).
# This example creates RTMP stream from movie ready for HLS:
#
# ffmpeg -loglevel verbose -re -i movie.avi  -vcodec libx264
#    -vprofile baseline -acodec libmp3lame -ar 44100 -ac 1
#    -f flv rtmp://localhost:1935/hls/movie
#
# If you need to transcode live stream use 'exec' feature.
#
application hls {
    live on;
    hls on;
    hls_path /tmp/hls;
}

server {
  location /hls {
    # Serve HLS fragments
    types {
        application/vnd.apple.mpegurl m3u8;
        video/mp2t ts;
    }
    root /tmp;
    add_header Cache-Control no-cache;
  }
}

rtmp/application/hls对应rtmp, 而server/location/hls则对应http

重新推一下rtmp, 注意下推流的application name,这里对应hls

$ ffmpeg -i Teams.mp4 -vcodec libx264 -acodec aac -f flv rtmp://192.168.3.132/hls/teams

然后检查/tmp/hls

# ls /tmp/hls
teams-0.ts  teams-1.ts	teams-2.ts  teams-3.ts	teams-4.ts  teams-5.ts	teams-6.ts  teams-7.ts	teams.m3u8

电脑上检查,

ffplay http://192.168.3.132/hls/teams.m3u8

最后一步,掏出手机,打开safari访问http://192.168.3.132/hls/teams.m3u8,就可以看到直播的视频。
补充一句, hls的兼容性问题:

  • iOS和Mac OSX的safari默认支持
  • Chrome插件方式
  • 通过video.js适配, 无敌

STAT

在server中加入,

# This URL provides RTMP statistics in XML
location /stat {
  rtmp_stat all;

  # Use this stylesheet to view XML as web page
  # in browser
  rtmp_stat_stylesheet stat.xsl;
}

location /stat.xsl {
  # XML stylesheet to view RTMP stats.
  # Copy stat.xsl wherever you want
  # and put the full directory path here
  root /path/to/stat.xsl/;
}

防盗链和鉴权

还记得前面feature中的

  • HTTP callbacks (publish/play/record/update etc)

通过play的callback到nodejs api来验证token有效性即可。

on_publish url
on_play url

nginx-rtmp-module 权限控制 - iam_shuaidaile的博客 - CSDN博客
nginx+rtmp | Hexo&Magic
nginx rtmp module添加鉴权机制 - 程序园

在live中配置on_play url后,nginx会POST到这个url上,将rtmp的参数传入ctx.request.body

比如rtmp://....?token=123456, koa中ctx.request.body.token就是123456

所以,可以通过redis设计一个一次性token,播放一次就把token删除,

访问时,

let token = nanoid(8);
await ctx.redis.set(`token:${token}`, null, 'EX', 180);

let viewdata = {
	title: session.title,
	location: session.location,
	source: `${session.source}?token=${token}`,
};
await ctx.render('live', viewdata);

验证时,

router.post('/live/auth', async ctx => {
	let { token } = ctx.request.body;
	if (!token) ctx.throw(401);

	let exists = await ctx.redis.exists(`token:${token}`);
	if (!exists) ctx.throw(401);

	await ctx.redis.del(`token:${token}`);

	ctx.status = 200;
});

on_play只针对1935的rtmp,如果需要对hls做认证需要拦截http请求,可以通过ngx_http_auth_request_module

参考阅读:

Node HA 高可用

双活 (Active-Active)

nodejs

采用pm2直接开启集群(cluster)

$ pm2 start app.js -i 4

-i --instances : launch [number] instances (for networked app)(load balanced)

i为0时, PM2会根据你CPU核心的数量来生成对应的进程,所以默认是为0即可。

➜  service git:(develop) ✗ yarn pm2:local
yarn run v1.21.1
$ pm2 start src -i 0
[PM2] Starting /Users/nonocast/Developer/projects/s365/service/src in cluster_mode (0 instance)
[PM2] Done.
┌────┬─────────────────────────┬─────────┬─────────┬──────────┬────────┬──────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name                    │ version │ mode    │ pid      │ uptime │ ↺    │ status   │ cpu      │ mem      │ user     │ watching │
├────┼─────────────────────────┼─────────┼─────────┼──────────┼────────┼──────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 0  │ s365                    │ 0.1.1   │ cluster │ 34887    │ 19m    │ 0    │ online   │ 0%       │ 28.6mb   │ nonocast │ disabled │
│ 1  │ s365                    │ 0.1.1   │ cluster │ 34888    │ 19m    │ 0    │ online   │ 0%       │ 30.0mb   │ nonocast │ disabled │
│ 2  │ s365                    │ 0.1.1   │ cluster │ 34891    │ 19m    │ 0    │ online   │ 0%       │ 28.3mb   │ nonocast │ disabled │
│ 3  │ s365                    │ 0.1.1   │ cluster │ 34898    │ 19m    │ 0    │ online   │ 0%       │ 28.3mb   │ nonocast │ disabled │
└────┴─────────────────────────┴─────────┴─────────┴──────────┴────────┴──────┴──────────┴──────────┴──────────┴──────────┴──────────┘```

app.js
```js
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = process.pid;
});

app.listen(3000);

pid会依次输出34887, 34888, 34891, 34898。换句话说, pm2会代理3000端口然后依次负载到4个进程上。

redis

mongodb

node soap

const debug = require('debug')('test');
const soap = require('soap');
const chai = require('chai');
const should = chai.should();
chai.use(require('chai-uuid'));

describe('Sinopharm WebService Test', async () => {
  let ws = 'http://xxxxxx?wsdl';

  let getContacts = async () => {
    let client = await soap.createClientAsync(ws);
    let auth = { username: 'xxx', password: 'xxx' };
    let tokenResponse = await client.GetTokenAsync(auth);
    let token = JSON.parse(tokenResponse[0].GetTokenResult).token;

    let result = await client.GetDateSourceAsync({ token });
    let message = JSON.parse(result[0].GetDateSourceResult);
    debug(message.EmplMessage.length);
    debug(message.DeptMessage.length);

    debug(message.DeptMessage[0]);
    debug(message.EmplMessage[0]);
  };

  it('test data source', async () => {
    let contacts = await getContacts();
  });
});

线性代数入门

从代数开始

这段摘自MIT牛人解说数学体系:

如果说古典微积分是分析的入门,那么现代代数的入门点则是两个部分:线性代数(linear algebra)和基础的抽象代数(abstract algebra)——据说国内一些教材称之为近世代数。

代数——名称上研究的似乎是数,在我看来,主要研究的是运算规则。一门代数, 其实都是从某种具体的运算体系中抽象出一些基本规则,建立一个公理体系,然后在这基础上进行研究。一个集合再加上一套运算规则,就构成一个代数结构。在主要的代数结构中,最简单的是群(Group)——它只有一种符合结合率的可逆运算,通常叫“乘法”。如果,这种运算也符合交换率,那么就叫阿贝尔群 (Abelian Group)。如果有两种运算,一种叫加法,满足交换率和结合率,一种叫乘法,满足结合率,它们之间满足分配率,这种丰富一点的结构叫做环(Ring), 如果环上的乘法满足交换率,就叫可交换环(Commutative Ring)。如果,一个环的加法和乘法具有了所有的良好性质,那么就成为一个域(Field)。基于域,我们可以建立一种新的结构,能进行加法和数乘,就构成了线性代数(Linear algebra)。

代数的好处在于,它只关心运算规则的演绎,而不管参与运算的对象。只要定义恰当,完全可以让一只猫乘一只狗得到一头猪:-)。基于抽象运算规则得到的所有定理完全可以运用于上面说的猫狗乘法。当然,在实际运用中,我们还是希望用它 干点有意义的事情。学过抽象代数的都知道,基于几条最简单的规则,比如结合律,就能导出非常多的重要结论——这些结论可以应用到一切满足这些简单规则的地 方——这是代数的威力所在,我们不再需要为每一个具体领域重新建立这么多的定理。

Spark & Shine在文中的一段解释也很精彩,

线性代数是抽象代数特殊的一类,其代数结构为:向量空间(vector spaces,也叫线性空间) + 线性变换(linear mappings)。很容易将线性代数和矩阵理论等同起来,但其实是不一样的,讨论线性变换是基于选定一组基的前提下。摘抄mathoverflow上的一个回答(原文在这里):

When you talk about matrices, you’re allowed to talk about things like the entry in the 3rd row and 4th column, and so forth. In this setting, matrices are useful for representing things like transition probabilities in a Markov chain, where each entry indicates the probability of transitioning from one state to another.

具体操作层面就不做更多的展开了, 最核心的两个概念就是向量空间和线性变换,直接看视频:

这个系列15个chapters, 看完你会怀疑人生, 以前学校里学的是什么鬼。

C动态链接库

创建so

read.h

#ifndef READ_H
#define READ_H

int read();

#endif

read.c

#include "read.h"

int read() { return 123; }

Makefile

CC = gcc

build: read.o
	$(CC) -shared -o libReader330.so read.o

read.o: read.c
	$(CC) -c -o read.o read.c

.PHONY: build
$ make
gcc -c -o read.o read.c
gcc -shared -o libReader330.so read.o

然后就得到了so, 将此lib link到path中,

$ ln -s /.../.../libReader330.so /usr/local/lib

验证so

另起一个目录,

app.c

#include <stdio.h>

int read();

int main(void) {
  int ret = read();
  printf("ret: %d\n", ret);
  return 0;
}

Makefile

CC = gcc

build: 
	$(CC) -o app -lReader330 app.c

.PHONY: build

非常方便易懂:

$ make 
gcc -o app -lReader330 app.c
$ ./app
ret: 123

仔细考虑内存分配问题

read.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char *SHGBIT_330_read(unsigned char **result) {
  char *message = "hello world, 330";
  int len = strlen(message);
  *result = (unsigned char *)malloc(len + 1);
  strcpy((char *)*result, message);
  *result[len] = 0x00;
  return *result;
}

app.c

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

unsigned char *SHGBIT_330_read(unsigned char **result);

int main(void) {
  unsigned char *result;
  printf("%s\n", SHGBIT_330_read(&result));
  free(result);
  return 0;
}

so负责分配(malloc)内存,调用方负责释放(free)内存。

java application

初始化
gradle init --type java-application

运行
gradle run

打包

gradle.build 加入jar metainfo

jar {
    manifest {
        attributes 'Main-Class': 'com.shgbit.App'
    }
}

gradle jar

然后直接运行
$ build/libs/ > java -jar hello.jar

在resources下增加配置文件config.properties,

MESSAGE=hello world

App.java

package hello;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.Properties;

public class App {
    public static void main(String[] args) throws IOException {
        String message = "";
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        try (InputStream input = loader.getResourceAsStream("config.properties")) {
            Properties prop = new Properties();
            prop.load(input);
            message = prop.getProperty("MESSAGE");
        }
        System.out.println(message);
    }
}

然后gradle build会在build/libs下生成hello.jar,通过java -jar hello.jar运行,输出hello world

通过zipinfo查看jar

~ zipinfo hello.jar 
Archive:  hello.jar
Zip file size: 1536 bytes, number of entries: 5
drwxr-xr-x  2.0 unx        0 b- defN 20-Sep-25 01:01 META-INF/
-rw-r--r--  2.0 unx       48 b- defN 20-Sep-25 00:46 META-INF/MANIFEST.MF
drwxr-xr-x  2.0 unx        0 b- defN 20-Sep-25 01:00 hello/
-rw-r--r--  2.0 unx     1602 b- defN 20-Sep-25 01:00 hello/App.class
-rw-r--r--  3.0 unx       15 tx stor 20-Sep-25 01:12 config.properties
5 files, 1665 bytes uncompressed, 948 bytes compressed:  43.1%

如果需要修改内部配置文件,可以直接vi hello.jar,然后编辑config.properties后修改,这个让我纠结了好久,没想到这么简单。

如何描述和验证 Javascript 类型?

我们都知道, javascript是弱类型的, 常规来说可以通过typescript或者flow来解决类型问题, 不过这里还是保留javascript原生态, 通过json schema来做类型检查。

JSON Schema is a powerful tool for validating the structure of JSON data.

最简单的schema是{}, 所以任何对象都可以通过这个schema的验证, 这里用ajv来验证一下,

require('dotenv').config();
const debug = require('debug')('app');
const ajv = new require('ajv')();

let schema = {};

debug(ajv.validate(schema, "hello world"));

=> true

加上type,

let schema = {
  type: 'number'
};

debug(ajv.validate(schema, "hello world"));

=> false

这就是最简单的schema验证方法。
来看一个一般一点的schema, 另外补充一句, schema可以同时服务后端和前端,

it('simple object', () => {
  let schema = {
    $schema: "http://json-schema.org/draft-07/schema#",
    $id: "http://space365.live/schemas/user.json",
    title: 'user-form',
    type: 'object',
    additionalProperties: false,
    required: ['user', 'pass', 'userType'],
    properties: {
      user: { type: 'string' },
      pass: { type: 'string' },
      userType: ['guest', 'normal', 'admin']
    }
  };

  let data = {
    user: 'nonocast',
    pass: '123456',
    userType: 'admin'
  }

  ajv.validate(data).should.eq(true);
});
  • $schema 不是必填项, 但官网在real world中始终建议添加, 标识schema的版本号
  • $id 不是必填项, 通常用url来标识schema
  • additionalProperties 为false时表示出现properties外的属性时会验证失败
  • type 必须是"integer", "string", "number", "object", "array", "boolean", "null"中的一个, 或者是一个array, 表示枚举 (所以这里就不建议对象中有type属性)
  • 其中number包含integer和number, 区别是integer只能约束整数
it('integer and number', () => {
  let numberSchema = { type: 'number' };
  let integerSchema = { type: 'integer' };

  ajv.validate(numberSchema, 1).should.eq(true);
  ajv.validate(integerSchema, 1).should.eq(true);
  ajv.validate(numberSchema, 1.5).should.eq(true);
  ajv.validate(integerSchema, 1.5).should.eq(false);
});

来看属性验证,

it('property formats', () => {
  let schema = {
    type: 'object',
    additionalProperties: false,
    properties: {
      email: { type: 'string', format: 'email' },
      address: { type: 'string', format: 'ipv4' },
      host: { type: 'string', format: 'hostname' },
      homepage: { type: 'string', format: 'uri' },
      date: { type: 'string', format: 'date-time' },
      timestamp: { type: 'number', minimum: 0, maximum: 2461449600 },
      mobile: { type: 'string', pattern: "^[1]([3-9])[0-9]{9}$" }
    }
  };

  let data = {
    email: '[email protected]',
    address: '192.168.1.1',
    host: 'nonocast.cn',
    homepage: 'https://nonocast.cn',
    date: '2018-11-13T20:20:39+00:00',
    timestamp: 1577818716,
    mobile: '13817100000',
  };

  ajv.validate(schema, data).should.eq(true);
});

当需要复用一个定义的时候就需要用到$ref,

it('reuse', () => {
  let schema = {
    $schema: "http://json-schema.org/draft-07/schema#",
    definitions: {
      user: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          email: { type: 'string', format: 'email' },
          gender: { type: 'boolean' }
        },
        required: ['name', 'email']
      },
      timestamp: {
        type: 'integer',
        minimum: 0,
        maximum: 2461449600
      }
    },

    type: "object",
    properties: {
      owner: { $ref: '#/definitions/user' },
      organizer: { $ref: '#/definitions/user' },
      begin: { $ref: '#/definitions/timestamp' },
      end: { $ref: '#/definitions/timestamp' },
    },
    allOf: [
      {
        not: {
          properties: {
            owner: { type: 'null' }
          }
        }
      },
      {
        not: {
          properties: {
            organizer: { type: 'null' }
          }
        }
      }
    ],
    required: ['owner']
  };

  ajv.validate(schema, { 
    owner: null, 
    organizer: null 
  }).should.eq(false);

  ajv.validate(schema, { 
    owner: { 
      name: 'x', 
      email: '[email protected]' 
    }, 
    organizer: null }
  ).should.eq(false);

  ajv.validate(schema, { 
    owner: null, 
    organizer: { 
      name: 'x', 
      email: '[email protected]' 
    } 
  }).should.eq(false);

  let data = {
    owner: {
      name: 'nonocast',
      email: '[email protected]'
    },
    organizer: {
      name: 'icy',
      email: '[email protected]'
    },
    begin: moment().unix(),
    end: moment().unix()
  };

  ajv.validate(schema, data).should.eq(true);
});

这里也同时用到了allOf和not的组合,用来做对象的约束,可以用到的还有anyOf, oneOf, 参考: Combining schemas — Understanding JSON Schema 7.0 documentation

$ref中的'#'称为json pointer,

The value of $ref is a URI, and the part after # sign (the “fragment” or “named anchor”) is in a format called JSON Pointer.

#ref也可以通过$id进行定位。

it('reuse by id', () => {
  let schema = {
    $schema: "http://json-schema.org/draft-07/schema#",
    definitions: {
      user: {
        $id: '#user',
        type: 'object',
        properties: {
          name: { type: 'string' },
          email: { type: 'string', format: 'email' },
          gender: { type: 'boolean' }
        },
        required: ['name', 'email']
      },
    },

    type: "object",
    properties: {
      owner: { $ref: '#user' },
      organizer: { $ref: '#/definitions/user' },
    },
    required: ['owner']
  };

  let data = {
    owner: {
      name: 'nonocast',
      email: '[email protected]'
    }
  };

  ajv.validate(schema, data).should.eq(true);
});

Zoom Meeting

{
  "Meeting": {
    "description": "Meeting object.",
    "allOf": [
      {
        "type": "object",
        "description": "Base object for sessions.",
        "properties": {
          "topic": {
            "type": "string",
            "description": "Meeting topic."
          },
          "type": {
            "type": "integer",
            "description": "Meeting Type:<br>`1` - Instant meeting.<br>`2` - Scheduled meeting.<br>`3` - Recurring meeting with no fixed time.<br>`8` - Recurring meeting with fixed time.",
            "default": 2,
            "enum": [1, 2, 3, 8],
            "x-enum-descriptions": [
              "Instant Meeting",
              "Scheduled Meeting",
              "Recurring Meeting with no fixed time",
              "Recurring Meeting with fixed time"
            ]
          },
          "start_time": {
            "type": "string",
            "format": "date-time",
            "description": "Meeting start time. When using a format like \"yyyy-MM-dd'T'HH:mm:ss'Z'\", always use GMT time. When using a format like \"yyyy-MM-dd'T'HH:mm:ss\", you should use local time and specify the time zone. This is only used for scheduled meetings and recurring meetings with a fixed time."
          },
          "duration": {
            "type": "integer",
            "description": "Meeting duration (minutes). Used for scheduled meetings only."
          },
          "timezone": {
            "type": "string",
            "description": "Time zone to format start_time. For example, \"America/Los_Angeles\". For scheduled meetings only. Please reference our [time zone](https://marketplace.zoom.us/docs/api-reference/other-references/abbreviation-lists#timezones) list for supported time zones and their formats."
          },
          "password": {
            "type": "string",
            "description": "Password to join the meeting. Password may only contain the following characters: [a-z A-Z 0-9 @ - _ *]. Max of 10 characters."
          },
          "agenda": {
            "type": "string",
            "description": "Meeting description."
          },
          "tracking_fields": {
            "type": "array",
            "description": "Tracking fields",
            "items": {
              "properties": {
                "field": {
                  "type": "string",
                  "description": "Tracking fields type"
                },
                "value": {
                  "type": "string",
                  "description": "Tracking fields value"
                }
              }
            }
          },
          "recurrence": {
            "type": "object",
            "description": "Recurrence object.",
            "properties": {
              "type": {
                "type": "integer",
                "description": "Recurrence meeting types:<br>`1` - Daily.<br>`2` - Weekly.<br>`3` - Monthly.",
                "enum": [1, 2, 3],
                "x-enum-descriptions": ["Daily", "Weekly", "Monthly"]
              },
              "repeat_interval": {
                "type": "integer",
                "description": "At which interval should the meeting repeat? For a daily meeting there's a maximum of 90 days. For a weekly meeting there is a maximum of 12 weeks. For a monthly meeting there is a maximum of 3 months."
              },
              "weekly_days": {
                "type": "string",
                "description": "Days of the week the meeting should repeat. \nNote: Multiple values should be separated by a comma. <br>`1`  - Sunday. <br>`2` - Monday.<br>`3` - Tuesday.<br>`4` -  Wednesday.<br>`5` -  Thursday.<br>`6` - Friday.<br>`7` - Saturday.",
                "enum": [1, 2, 3, 4, 5, 6, 7],
                "x-enum-descriptions": [
                  "Sunday",
                  "Monday",
                  "Tuesday",
                  "Wednesday",
                  "Thursday",
                  "Friday",
                  "Saturday"
                ]
              },
              "monthly_day": {
                "type": "integer",
                "description": "Day in the month the meeting is to be scheduled. The value range is from 1 to 31."
              },
              "monthly_week": {
                "type": "integer",
                "description": "The week a meeting will recur each month.<br>`-1` - Last week.<br>`1` - First week.<br>`2` - Second week.<br>`3` - Third week.<br>`4` - Fourth week.",
                "enum": [-1, 1, 2, 3, 4],
                "x-enum-descriptions": [
                  "Last week",
                  "First week",
                  "Second week",
                  "Third week",
                  "Fourth week"
                ]
              },
              "monthly_week_day": {
                "type": "integer",
                "description": "The weekday a meeting should recur each month.<br>`1` - Sunday.<br>`2` - Monday.<br>`3` - Tuesday.<br>`4` -  Wednesday.<br>`5` - Thursday.<br>`6` - Friday.<br>`7` - Saturday.",
                "enum": [1, 2, 3, 4, 5, 6, 7],
                "x-enum-descriptions": [
                  "Sunday",
                  "Monday",
                  "Tuesday",
                  "Wednesday",
                  "Thursday",
                  "Friday",
                  "Saturday"
                ]
              },
              "end_times": {
                "type": "integer",
                "description": "Select how many times the meeting will recur before it is canceled. (Cannot be used with \"end_date_time\".)",
                "default": 1,
                "maximum": 50
              },
              "end_date_time": {
                "type": "string",
                "description": "Select a date the meeting will recur before it is canceled. Should be in UTC time, such as 2017-11-25T12:00:00Z. (Cannot be used with \"end_times\".)",
                "format": "date-time"
              }
            }
          },
          "settings": {
            "type": "object",
            "description": "Meeting settings.",
            "properties": {
              "host_video": {
                "type": "boolean",
                "description": "Start video when the host joins the meeting."
              },
              "participant_video": {
                "type": "boolean",
                "description": "Start video when participants join the meeting."
              },
              "cn_meeting": {
                "type": "boolean",
                "description": "Host meeting in China.",
                "default": false
              },
              "in_meeting": {
                "type": "boolean",
                "description": "Host meeting in India.",
                "default": false
              },
              "join_before_host": {
                "type": "boolean",
                "description": "Allow participants to join the meeting before the host starts the meeting. Only used for scheduled or recurring meetings.",
                "default": false
              },
              "mute_upon_entry": {
                "type": "boolean",
                "description": "Mute participants upon entry.",
                "default": false
              },
              "watermark": {
                "type": "boolean",
                "description": "Add watermark when viewing a shared screen.",
                "default": false
              },
              "use_pmi": {
                "type": "boolean",
                "description": "Use a personal meeting ID. Only used for scheduled meetings and recurring meetings with no fixed time.",
                "default": false
              },
              "approval_type": {
                "type": "integer",
                "default": 2,
                "description": "`0` - Automatically approve.<br>`1` - Manually approve.<br>`2` - No registration required.",
                "enum": [0, 1, 2],
                "x-enum-descriptions": [
                  "Automatically Approve",
                  "Manually Approve",
                  "No Registration Required"
                ]
              },
              "registration_type": {
                "type": "integer",
                "description": "Registration type. Used for recurring meeting with fixed time only. <br>`1` Attendees register once and can attend any of the occurrences.<br>`2` Attendees need to register for each occurrence to attend.<br>`3` Attendees register once and can choose one or more occurrences to attend.",
                "default": 1,
                "enum": [1, 2, 3],
                "x-enum-descriptions": [
                  "Attendees register once and can attend any of the occurrences",
                  "Attendees need to register for each occurrence to attend",
                  "Attendees register once and can choose one or more occurrences to attend"
                ]
              },
              "audio": {
                "type": "string",
                "description": "Determine how participants can join the audio portion of the meeting.<br>`both` - Both Telephony and VoIP.<br>`telephony` - Telephony only.<br>`voip` - VoIP only.",
                "default": "both",
                "enum": ["both", "telephony", "voip"],
                "x-enum-descriptions": [
                  "Both Telephony and VoIP",
                  "Telephony only",
                  "VoIP only"
                ]
              },
              "auto_recording": {
                "type": "string",
                "description": "Automatic recording:<br>`local` - Record on local.<br>`cloud` -  Record on cloud.<br>`none` - Disabled.",
                "default": "none",
                "enum": ["local", "cloud", "none"],
                "x-enum-descriptions": [
                  "Record to local device",
                  "Record to cloud",
                  "No Recording"
                ]
              },
              "enforce_login": {
                "type": "boolean",
                "description": "Only signed in users can join this meeting."
              },
              "enforce_login_domains": {
                "type": "string",
                "description": "Only signed in users with specified domains can join meetings."
              },
              "alternative_hosts": {
                "type": "string",
                "description": "Alternative host's emails or IDs: multiple values separated by a comma."
              },
              "close_registration": {
                "type": "boolean",
                "description": "Close registration after event date",
                "default": false
              },
              "waiting_room": {
                "type": "boolean",
                "description": "Enable waiting room",
                "default": false
              },
              "global_dial_in_countries": {
                "type": "array",
                "description": "List of global dial-in countries",
                "items": {
                  "type": "string"
                }
              },
              "global_dial_in_numbers": {
                "type": "array",
                "description": "Global Dial-in Countries/Regions",
                "items": {
                  "type": "object",
                  "properties": {
                    "country": {
                      "type": "string",
                      "description": "Country code. For example, BR."
                    },
                    "country_name": {
                      "type": "string",
                      "description": "Full name of country. For example, Brazil."
                    },
                    "city": {
                      "type": "string",
                      "description": "City of the number, if any. For example, Chicago."
                    },
                    "number": {
                      "type": "string",
                      "description": "Phone number. For example, +1 2332357613."
                    },
                    "type": {
                      "type": "string",
                      "description": "Type of number. ",
                      "enum": ["toll", "tollfree"]
                    }
                  }
                }
              },
              "contact_name": {
                "type": "string",
                "description": "Contact name for registration"
              },
              "contact_email": {
                "type": "string",
                "description": "Contact email for registration"
              },
              "registrants_confirmation_email": {
                "type": "boolean",
                "description": "Send confirmation email to registrants"
              },
              "registrants_email_notification": {
                "type": "string",
                "description": "registrants email notification"
              },
              "meeting_authentication": {
                "type": "boolean",
                "description": "Only authenticated users can join meetings"
              },
              "authentication_option": {
                "type": "string",
                "description": "Meeting authentication option id"
              },
              "authentication_domains": {
                "type": "string",
                "description": "Meeting authentication_domains"
              }
            }
          }
        }
      }
    ],
    "type": "object"
  }
}

重点观察如何实现周期性会议,

{
  "recurrence": {
    "type": "object",
    "description": "Recurrence object.",
    "properties": {
      "type": {
        "type": "integer",
        "description": "Recurrence meeting types:<br>`1` - Daily.<br>`2` - Weekly.<br>`3` - Monthly.",
        "enum": [1, 2, 3],
        "x-enum-descriptions": ["Daily", "Weekly", "Monthly"]
      },
      "repeat_interval": {
        "type": "integer",
        "description": "At which interval should the meeting repeat? For a daily meeting there's a maximum of 90 days. For a weekly meeting there is a maximum of 12 weeks. For a monthly meeting there is a maximum of 3 months."
      },
      "weekly_days": {
        "type": "string",
        "description": "Days of the week the meeting should repeat. \nNote: Multiple values should be separated by a comma. <br>`1`  - Sunday. <br>`2` - Monday.<br>`3` - Tuesday.<br>`4` -  Wednesday.<br>`5` -  Thursday.<br>`6` - Friday.<br>`7` - Saturday.",
        "enum": [1, 2, 3, 4, 5, 6, 7],
        "x-enum-descriptions": [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]
      },
      "monthly_day": {
        "type": "integer",
        "description": "Day in the month the meeting is to be scheduled. The value range is from 1 to 31."
      },
      "monthly_week": {
        "type": "integer",
        "description": "The week a meeting will recur each month.<br>`-1` - Last week.<br>`1` - First week.<br>`2` - Second week.<br>`3` - Third week.<br>`4` - Fourth week.",
        "enum": [-1, 1, 2, 3, 4],
        "x-enum-descriptions": [ "Last week", "First week", "Second week", "Third week", "Fourth week" ]
      },
      "monthly_week_day": {
        "type": "integer",
        "description": "The weekday a meeting should recur each month.<br>`1` - Sunday.<br>`2` - Monday.<br>`3` - Tuesday.<br>`4` -  Wednesday.<br>`5` - Thursday.<br>`6` - Friday.<br>`7` - Saturday.",
        "enum": [1, 2, 3, 4, 5, 6, 7],
        "x-enum-descriptions": [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]
      },
      "end_times": {
        "type": "integer",
        "description": "Select how many times the meeting will recur before it is canceled. (Cannot be used with \"end_date_time\".)",
        "default": 1,
        "maximum": 50
      },
      "end_date_time": {
        "type": "string",
        "description": "Select a date the meeting will recur before it is canceled. Should be in UTC time, such as 2017-11-25T12:00:00Z. (Cannot be used with \"end_times\".)",
        "format": "date-time"
      }
    }
  }
}

HDMI CEC

上周家里换了一台SONY的电视,然后发现SONY的遥控器可以直接控制小米盒子,后来简单查了一下发现是通过HDMI-CEC实现的,简单来说, CEC (Consumer Electronics Control) 是HDMI的一个选项,允许用户通过一个遥控器控制多个设备。

所以,CEC的设计初衷还是针对消费级,SONY就用的很好,PS开机后直接能透过HDMI-CEC告知电视开机并切换到PS的channel,同时电视机的遥控器可以直接控制PS,电视关机时PS同步关闭,非常流畅的设计。

从另外一个角度来说,每个厂商实现的CEC不完全兼容,还是存在指令上的差异,所以CEC更多时候作为厂商建立生态的工具。你买PS最好一并买SONY的TV,可以达到最好的体验。

然后我们转换到会议场景,CRESTRON全系列产品都支持CEC控制,所以透过CEC我们可以控制电视的POWER, 音量和通道,要比IR控制功能强太多,毕竟IR是没有反馈的。BUT, 事实上在企业用户中更多的采用商业级显示屏,所以相比CEC,绝大多数控制还是采用RS232或者IP来实现。毕竟这里有一个很现实的情况,不是所有显示屏都支持CEC。

所以在控制的时候会采用如下策略: 首先是RS232或IP,对IT不靠谱的项目RS232更好,实在不行才用CEC,最后才是红外IR

  • RS232/IP就是设计用来做控制的
  • CEC是为了统一遥控器,或者多设备内部协商设计的
  • IR就是为了省成本给遥控器设计的

然后在搜索的过程中,发现Zoom Room通过NUC + Pulse-Eight adapter来实现电视的开关,比如booking的时间到了会议拉起来同时把电视打开,但前提是电视要支持CEC。

这里就引出了第三个问题,通过PC/HTPC是否可以通过HDMI-CEC反向控制电视?

First check if your graphics card (hardware) supports HDMI CEC. Then also the drivers must support it. But according to this review, very few cards have CEC support.

For PCs without CEC support, there exists various products which add CEC support. They connect between the PC and TV on HDMI cable plus via USB to PC. The software sends CEC commands to adapter via USB. One example of such product is: USB HDMI CEC adapter from Pulse-eight.

hdmi - How to use CEC from windows? - Super User

电脑显卡一般来说不支持HDMI-CEC,因为没有这个需求,反而HTPC因为和电视相连接,所以会考虑CEC,比如Intel NUC

英特尔® NUC 的 HDMI CEC 信息

但这个支持也仅仅局限在BIOS中,你可以设置触发或者响应,但依然没有提供Application Level API。查下来发现其实也就Pulse-Eight他是通过usb连接到主机,实现了软件控制CEC,除此以外,相关资料非常欠缺,听说同事有一个在吃灰,改天拿来看看怎么调用后再补充更新。

Control your TV from Kodi, or vice versa! USB - CEC Adapter

npm 学习

npm (Node Packaged Modules) 由3个不同的部分组成:

  • the website: discover packages, set up profiles, and manage other aspects of your npm experience.
  • the Command Line Interface (CLI): most developers interact with npm
  • the registry: The registry is a large public database of JavaScript software and the meta-information surrounding it.

CLI

安装路径

~ npm config get prefix
/usr/local

配置文件

  • npmrc
  • npm的配置是一组 key = value
  • 4个level
    • per-project config file (/path/to/my/project/.npmrc)
    • per-user config file (~/.npmrc)
    • global config file ($PREFIX/etc/npmrc)
    • npm builtin config file (/path/to/npm/npmrc)

global config file

~ npm config get globalconfig
/usr/local/etc/npmrc
~ npm config --global edit

per-user config file

~ npm config get userconfig
/Users/nonocast/.npmrc
~ npm config edit

list config

~ npm config ls

# Show all the config settings. Use -l to also show defaults.
~ npm config ls -l

set and delete

# set registry value
~ npm config set registry "https://skimdb.npmjs.com/registry"
# revert change back to default
~ npm config delete registry

缓存

全局npm安装路径

~ npm root -g
/usr/local/lib/node_modules
➜  ~ ls /usr/local/lib/node_modules 
@nestjs           dva-cli           http-server       pm2 ...

本地缓存

~ npm config get cache
/Users/nonocast/.npm

Register

TO BE WRITTEN

参考阅读

什么是单片机?

  • 一般将性能相对较低的嵌入式处理器归为微控制器(Micro Control Unit, MCU), 也就说我们说的单片机。
  • 微处理器(Microprocessor, 也被称为Center Process Unit, 即CPU)和微控制器(MCU)的主要区别是CPU不包含Memory和I/O, 我们之前说过一个计算机系统需要有CPU, Memory和I/O组成, 而MCU就是在一个片上同时集成了CPU, Memory和I/O, 满足了特定系统的需要
  • CPU, MCU从根本上来说都是集成电路(IC, integrated circuit), 或称微电路(microcircuit), 微芯片(microchip), 芯片(chip), 换句话说, 芯片(chip)就是内含集成电路的硅片, 从抽象层面可以理解为将原本在电路板(PCB)实现的电路缩小在一个晶片上
  • 集成电路(IC)对于离散晶体管有2个主要优势: 成本和性能。成本低是由于芯片把所有的元件通过照相平版技术,作为一个单位印刷,而不是在一个时间只制作一个晶体管。性能高是由于元件快速开关,消耗更低能量,因为元件很小且彼此靠近。2006年,芯片面积从几平方毫米到350 mm²,每mm²可以达到一百万个晶体管
  • 晶体管(transistor)被认为是现代历史中最伟大的发明之一, 可能是二十世纪最重要的发明, 它原本需要几个房间那么大的机器变得只有信用卡那么大, 好比说手机。所以说晶体管是现代计算机的最小单位 二极管、三极管、场效应管的原理和特征

8051

The Intel 8051AH is a MCS-51 NMOS single-chip 8-bit microcontroller with 32 I/O lines, 2 Timers/Counters, 5 Interrupts/2 priority levels 4 KB ROM, 128 Bytes on-chip RAM.

早在1981年, Intel就生产了8位微控制器, 命名为8051。8051中集成了18B RAM, 4KB ROM, 两个定时器, 一个串口以及4个端口(每个端口8位), 8位以为着CPU一次只能处理8个bit。

当时因为版权问题所以每家生产的机器指令不相同也能相同, 但是后来Intel忙着去开发更高级的CPU, 也没心思管了, 就将8051授权给没能力设计芯片的制造商去生产, 也就意味着只要是为一家8051芯片编程, 就同时可以运行在其他制造商生产的8051上运行。

8051是MCS-51中最初的一个产品, 这个系列也包括了8052, 8031等MCU。

STC89C52RC

国内现在接触最多的应该是STC的单片机, 如果想有一块单片机最简单的方法就是京东上直买《51单片机项目教程(C语言版)(赠单片机开发板)》- 京东图书 79.60直接送一块板子,物超所值。

硬件和软件不一样, 要了解一个单片机最好的方式就是学习它的Datasheet, 直接去宏晶的网站下载就行。

STC89C52RC

  • 2004年推出增强型8051单片机, 6时钟每机器周期和12时钟每机器周期可任意选择, 指令代码完全兼容传统8051
  • 工作频率范围: 035MHz, 相当于普通8051的070MHz, 实际工作频率可达42MHz, 具体的机器周期取决于给MCU搭配的晶振
  • STC: 宏晶科技
  • 89: 表示采用6T/12T 8051
  • C: 5.5V ~ 3.8V
  • 52: 8k字节的ROM和512B的RAM
  • RC: 自带RC时钟振荡电路

结合stcgal交叉观察,

$ stcgal -P stc89 -p /dev/tty.usbserial-14310
Target model:
  Name: STC89C52RC/LE52R
  Magic: F002
  Code flash: 8.0 KB
  EEPROM flash: 6.0 KB
Target frequency: 11.016 MHz
Target BSL version: 4.3C

在18,19引脚外接11.0592MHz的晶振得到的信息如下:

  • CPU:
    • 8bit (ALU, register, data bus的bit width都是8bit,所以说是8位CPU, address bus是16bit)
    • Clock cycle (时钟周期): 11.0592MHz, 即1个cycle是1/12us (微秒), 每秒输出1200万次 (i7已经每秒4亿次了)
    • Instruction cycle (指令周期): 12 x Clock cycle = 1/12usx12=1us, 正好1个us
  • RAM: SRAM, 512B (物理和逻辑上都分为两个地址空间: 内部256B, 内部扩展256B, 不同型号的会有所区别,但是内部固定256B)
  • ROM: 也叫做Code flash,程序存储区, 采用EEPROM flash 8K (address: 0000H-1FFFH)
  • STORAGE: 采用EEPROM flash,容量为6KB (address: 2000H-AFFFH)
  • I/O: 32pin
  • UART: 1

上面说的RAM和ROM构成主存/内存, 而STORAGE可以理解为外部设备, 辅助存储。

RAM和寄存器

按一般逻辑来说, 寄存器(Register)和CACHE都是在CPU内部的, 但现在是MCU, 所以寄存器就共用了RAM的介质。

STC89C52RC有512B的RAM,其中内部256B,内部又分为3个部分:

  • 低128B (与传统8051兼容), 也称为通用RAM区
  • 高128B (Intel在8052扩展了高128B)
  • sfr (特殊功能寄存器)
    高128B和sfr物理上是2个空间,反正这里我也没整明白,难道有2个128B的物理空间, 共享地址这个比较容易理解,sfr必须直接寻址,而高128B则需要间接寻址。
    低128B的前32B包含4组通用寄存器组,每组8个B,分别是R0-R7,然后从20H-2FH (16B, 128b) 可按位寻址,

特殊寄存器就是单片机打通硬件和软件的核心区域,书上最多的就是点亮LED灯,

app.c

sbit light = 0x90;
void main() {
  light = 0;
  while(1) {}
}

这个0x90就是对应到RAM的第90个bit, 将这个bit设置为0, 进而触发第一个单片机引脚低电压, 在VCC之间形成电压差产生电流。

仿真后也可以直观的看到D:90H从FF变为FE, 最低位置0。

对应的汇编其实就更加直白,

app.asm

ORG 0H
CLR 90H
HERE: SJMP HERE
END

是不是更加直接?
你可以观察一下生成的二进制(hex), 一共就4个byte

C2 90 80 FE

然后对应STC89C52RC中的Datasheet p87

CLR bit
功能: 清零指定的位
指令长度: 2个字节
二进制编码: 1100 0010 [bit address]

我们可以看到上面的C2 90, C2对应的bit: 1100 0010, 后面的90就是P1_1, 机器指令也够直接把。

SJMP的指令2个字节, 1000 0000 [rel.address]
第三个字节80就是1000 0000, FE对应1111 1110, 就是-1, 因为SJMP的范围是[-127, 128], PC运行后会自动+1,现在再把你减回来等于无限循环。

所以一个最简单的hex就是80 FE, 亮灯就是C2 90,直接写了发下去。
hey.ihx

:04002000C29080FE0C
:00000001FF

然后下发,

stcgal --debug -P stc89 -p ${DEVICE} app.ihx

如果是Windows就直接hexedit写四个字节就完事了。

这一圈算是把STC89C52RC看了一下,单片机内部的硬件我们放一放,等到看具体功能再一个个看,这是我的弱项,什么锁存器,什么上拉电阻,头晕。

最后补充一下,windows和mac两个环境,

  • mac可以下发但是不能仿真 (sdcc/stcgal)
  • windows可以仿真但是不能下发 (keil/stcisp)
    所以挺好, 两边的环境都接触一下,anyway, ihx和hex一定是一样的,只是当中的C和汇编语法有所区别。

到了这里往下就是proteus, 随便画个图把刚才的程序导入进去就可以运行

Screen Shot 2019-10-06 at 9 09 29 PM

ES6 Iterator, generator and yield

先来复习一下for..in和for..of, 简单来说, for..in在ES5中定义,for..of则是在ES6定义, for..in是遍历对象属性, for..of是遍历对象元素, for..of就用到了Iterator。

let items = ['foo', 'bar'];
for (let item of items) {
  debug(item);
}

从iterator角度来看一下for..of干了什么,

let it = items[Symbol.iterator]();
debug(it.next());       // { value: "foo", done: false }
debug(it.next());       // { value: "bar", done: false }
debug(it.next());       // { value: undefined, done: true }

用while实现如下:

let it = items[Symbol.iterator]();
let item = null;
while (!(item = it.next()).done) {
  debug(item.value);
}

我们可以通过Symbol.iterator来检查对象是否支持iterator,

let isIterable = obj => { return typeof obj[Symbol.iterator] === 'function'; }

isIterable([1, 2, 3]).should.be.true;
isIterable('hello').should.be.true;
isIterable(new Map()).should.be.true;
isIterable(new Set()).should.be.true;
isIterable({}).should.be.false;
isIterable(7).should.be.false;

这里需要值得注意的是,it是不可逆, 一次性的"玩意"。items[Symbol.iterator]是一个创建iterator的function, 可以理解为一个生成器,每次需要时即时生成一个新的iterator, 用完也就丢了, 因为再怎么调用都是返回{ value: undefined, done: true }而已。

由此可见:

  • array作为"provider"在对象上提供了一个方法支持创建iterator
  • 这个创建迭代器的方法称之为generator
  • for..of作为"consumer"使用iterator进行迭代
  • 所以完成for..of就同时需要iterator和generator的配合
  • 而iterator之所以next()每次可以返回不同的值,其中关键就是内部通过yield实现的

现在我们就可以通过generator和yield来亲自DIY下,

function* createIterator() {
  while (true) { yield 1; }
}

let it = createIterator();
it.next().value.should.eq(1);
it.next().value.should.eq(1);
it.next().value.should.eq(1);

May it helps.

参考阅读:

如何推进项目?

需求

开发

  • Git 流程
  • Code Review

部署

  • Javascript Webpack

测试

  • Service: 自动化测试
  • Webap: 手工+自动化测试 (Selenium)

工具

  • 建议采用trello这种类型工具

Guru99: Software Testing Tutorials for Beginners 读书笔记

source: Software Testing Tutorials for Beginners

What is software testing?

Software testing is a process used to identify the correctness, completeness and quality of developed computer software. It includes a set of activities conducted with the intent of finding errors in software so that it could be corrected before the product is released to the end users.

软件测试是一个识别软件正确性、完整性和质量的过程,在发布出去之前,提前发现软件错误并修复的一系列活动。

In simple words, software testing is an activity to check that the software system is defect free.

defect free就是没有缺陷的意思。

Why software testing is important?

没好好测试搞不好会死人的。飞机失事,医疗设备放射,火箭发射等等。

7 testing principles

  • P1: Testing shows presence of defects
  • P2: Exhaustive testing is impossible
  • P3: Early Testing
  • P4: Defect Clustering
  • P5: Pesticide Paradox
  • P6: Testing is context dependent
  • P7: Absence of errors - fallacy

Software Development Life Cycle (SDLC)

Waterfall Method

  • Requirements: Gather as much information as possible about the details & specifications of the desired software from the client
  • Design: Plan the programing languages like java, php, .net; database like oracle, mysql etc which wold be suited for the project
  • Build: Actually code the software
  • Test: Test the software to verify that it is built as per the specifications given by the client
  • Maintenance

每个阶段称之为Stage, 瀑布肯定是不灵的,所以形成两种拆解,1是水平拆分: V模型,2是垂直拆分: 按迭代拆分形成小单元的Waterfall

V-Model of Testing

  • Requirement Analysis - User Acceptance testing
  • Functional Specifications - System Testing
  • High level Designs - Integration Testing
  • Detailed Designs / program specification - Unit Test
  • Code

测试模型需要匹配产品开发模型

  • Unit Test: 针对独立功能的黑盒测试,这里有别于代码的unit test
  • Integration Testing: 通过Stub来隔离测试
  • Acceptance Testing (验收测试)
    • 在客户实际环境中
    • Alpha Testing: A small set of employees of the client
    • Beta Testing: A small set of customers
  • Smoke / Sanity Testing: Quick and non-exhaustive. 强调程序的主要功能进行的验证,而不会对具体功能进行更深入的测试。抓住主要矛盾,在有限的时间内先解决重要问题。Smoke的目的是确保系统可用(health),而不是为了defect free. 冒烟不通过就不用网下面测了,直接打回去。
  • Regression Testing: 确保系统某个部分的变化不会引起系统中其他地方的现有功能。就是差异比对,确保修改的内容只会改变当前问题域。
  • Non-Functional Testing: performance, usability , load, scalabilityetc.

Testing Types

  • Functional
    • Unit Testing
    • Integration Testing
    • Smoke / Sanity
    • User Acceptance
    • Localization
    • Globalization
    • Interoperability
  • Non-Functional
    • Performance
    • Endurance
    • Load
    • Volume
    • Scalability
  • Maintenance
    • Regression
    • Maintenance

Test Scenarios

  • Check Login
  • Check New Order
  • Check Open Order
  • Check Fax Order
  • Check Help
  • Check About

Test Case

一个Scenario包含一组具体的Test Cases

举例来说,

  • Test Scenario: "Check the Login Functionality" 对应的一组Test Case如下:
  1. Check system behavior when valid email id and password is entered.
  2. Check system behavior when invalid email id and valid password is entered.
  3. Check system behavior when valid email id and invalid password is entered.
  4. Check system behavior when invalid email id and invalid password is entered.
  5. Check system behavior when email id and password are left blank and Sign in entered.
  6. Check Forgot your password is working as expected
  7. Check system behavior when valid/invalid phone number and password is entered.
  8. Check system behavior when "Keep me signed" is checked

所以一个完整的Expected Result如下:

Test Scenario Test Case Pre Conditions Test Steps Test Data Expected Result Actual Result Pass/Fail
Check Login Functionality Check response on Entering valid Agent Name & Password Agent Name: guru99 Password: MERCURY Login must be successful Login Successful Pass

Traceability

  • Business Requirement: B-26
  • Functional Design: F-48
  • High Level Design: H-99
  • Detail Design: D-102
  • Test Cases: T-185, T-200

Defect Report (Bug Report)

  • Defect_ID
  • Defect Description
  • Version
  • Steps
  • Date Raised
  • Reference
  • Detected By
  • Status
  • Fixed by
  • Date closed
  • Severity (严重程度): describes the impact of the defect on the application
  • Priority: is related to defect fixing urgency.

比如公司名字错了,那么这个Severity是LOW, 但是Priority则是HIGH

Software Testing Life Cycle (STLC)

  • Requirement Analysis (Deliverables: Requirements Traceability Matrix (RTM) )
  • Test Planning (Deliverables: Test plan document)
  • Test Case Development (Deliverables: Test cases/scripts and Test data)
  • Test Environment Setup (Deliverables: Environment ready with test data set up.)
  • Test Execution (Deliverables: RTM with execution status, Test cases updated with results, Defect reports)
  • Test Cycle Closure: (Deliverables: Test Closure report, Test metrics)

读书笔记: 图解密码技术

第一章: 环游密码世界

  • 口令: password, passcode, pin
  • 编码: encoding
  • 密码: cryptography
  • 过程: 将明文(plaintext)通过加密算法(encryption algorithm)加密(encrypt)后得到密文(ciphertext), 然后解密(decrypt)再次得到明文(plaintext)
  • 密码算法: 用于解决复杂问题的步骤称之为算法(algorithm)。加密,解密的算法和在一起统称为密码算法

注:

  • PIN(personal identification number),多指数字密码,银行卡的6位数字密码,包括iPhone锁屏都是PIN code,而普通字母的都是叫做Password。

密钥

  • 密钥 (key): 和现实世界的🔑类似, 密码算法都需要密钥
  • 对称密码 (symmetric cryptography): 加密和解密时使用相同的密钥
  • 非对称密码 (asymmetric cryptography): 加密和解密使用不同的密钥,也称为公钥密码(public-key cryptography), 非对称密码出现在20世纪70年代, 这种方法在密码界引发了一场大变革。现在计算机和互联网的安全体系很大程度依赖于非对称密码

密码学家的工具箱 (6种)

  • 对称密码
  • 公钥密码
  • 单向散列函数 (one-way hash function): 验证完整性
  • 消息认证码 (message authentication code): 认证机制
  • 数字签名 (digital signature): 确保内容完整性,提供认证并防止否认的密码技术
  • 伪随机数生成器 (Pseudo Random Number Generator: PRNG)

其中对称和公钥主要针对内容加密,防窃听;单向散列是防篡改, 保证数据完整性;消息认证码用于认证;数字签名针对篡改、伪装和防否认。

第二章: 历史上的密码

历史上的几种著名密码:

  • 凯撒密码 (Caesar cipher): 字母表平移, a>D, b>E, c>F, 平移的字母数量相当于密钥(key)
  • 简单替换密码 (simple substitution cipher): a>X, b>C, d>Q, 替换表相当于密钥
  • Enigma:

注: 密码算法和数学模型一样,需要算法和参数两个部分才能确定最终的变换方式。现实世界中锁的设计是公开的,但是钥匙是保密的。

第三章: 对称密码

XOR 运算

异或也称作二进制半加,即不进位加法。

比如1010 xor 1111 = 0101, 这时候将结果0101 xor 1111 = 1010, 你会发现两次异或可以还原明文。所以只需要选择一个合适的key (就比如刚才的1111) 就能形成一个高强度密码。

根据这个原理,就可以生成一个和明文长度相同的bits作为密钥, 然后通过xor得到密文, 然后再次xor就能解密,这个算法称之为一次性密码本 (也成为Vernam cipher),不过由于key过于长,虽然可以说是绝对安全,但是实际上是没有实用性。

DES

DES (Data Encryption Standard) 是1977年美国联邦信息处理标准(FIPS)中所采用的一种对称密码(FIPS 46-3)。DES一直以来被美国以及其他国家的政府和银行等广泛使用。DES的密文在现在已经可以在短时间破译,所以不应该再继续使用DES,了解一下原理即可。

DES通过一个64bit的key可实现对64bit的明文加密为64bit的密文,对于超过64bit的明文需分组加密,密码算法采用了Feistel 网络。

AES

主流对称算法: AES (Advanced Encryption Standard) 通过竞赛方式最终采用了Rijndael的对称密码算法。

第四章: 分组密码的模式

前面讲到DES/AES都是分组加密,这种分组模式被称为ECB(Electronic CodeBook),很容易被攻击,所以CBC, CFB等都采用一个初始化向量(iv, Initialization Vector)驱动后续加密依赖前一组的密文,这里需要注意的是iv需要每次加密随机产生不同的比特序列,否则一样容易被攻击。简单来说,不推荐ECB,推荐CBC,CFB,OFB,CTR这类带iv的。

尤其是在跨语言加密解密时,需要明确算法/模式/填充量,比如说AES/ECB/PKCS5Padding,AES是算法,ECB是模式,PKCS5Padding则是填充量,否则你怎么都对不上。

nodejs从10.0.0已经deprecate createCipher,转而使用createCipheriv,见[这里](Crypto | Node.js v14.1.0 Documentation),不过网上到处都是createCipher的文章,还是要懂点原理才能混。

参考:

第五章: 公钥密码

以投币储物柜来说,硬币是关闭储物柜的密钥,而钥匙则是打开储物柜的密钥,这就是现实世界对非对称一个很好的比喻。

公钥密码 (public-key cryptography)

从概念上来说,密钥(key)分为加密密钥和解密密钥,加密密钥是公开的,解密密钥只有自己持有,谁都可以拥有加密密钥对数据加密,但是只有我才能解密。

所谓的公钥(public key)就是指加密密钥,对应的私钥(private key)指解密密钥。public key和private key是一一配对的,一对public key和private key称为密钥对(key pair),在数学上,一次同时生成key pair,无法独立生成其中一个。

假设,A向B传送信息,则B生成一对key pair,将public key给到A,A将消息通过public key加密后传输给B,整个过程中即使存在中间人都没有关系,因为public key无法对密文解密,只有B通过private key才能解密。

这中间关键的算法就是RSA, RSA的加密过程就是对明文作E次方然后mod N,所以public key就是E和N两个参数,换句话说E和N构成了public key,也可以表示为public key {E, N}。解密则是对密文进行D次方然后mod N。从数学角度来说,通过质数的特性产生了E, D, N,形成了一组key pair,我觉得到这就可以关闭了,后面都是大量数学道理,大家自己学习。

第二部分: 认证

第七章: 单向散列函数

单向散列函数 (one-way hash function) 的输入是消息(message),输出是散列值(hash value)。以SHA-256为例,不论输入有多长,输出的散列值长度固定为256bits,即32个bytes。

单向散列函数几种实现:

  • MD4: Rivest于1990年实现, 128bits, 不安全
  • MD5: Rivest于1991年实现, 128bits, 也已经被攻破, 不安全, MD: Message Digest
  • SHA-1: NIST于1993年发布, 160bits, 不安全, 2005年被攻破
  • SHA-2 (SHA-256, SHA-384, SHA-512): 安全
  • SHA-3 (Keccak)

第八章: 消息认证码

消息认证码 (Message Authentication Code) 是一种确认完整性并进行认证的技术,简称MAC。简单来说MAC是一种带key的one-way hash。因为发送和接收方共同持有key,这样就可以验证消息来源。

第九章: 数字签名

数字签名 (digital signature) 是一种将相当于现实世界中的盖章,签字的功能在数字世界中进行实现的技术。使用数字签名可以识别篡改和伪装,还可以防止否认。

MAC是对称算法,而数字签名则是非对称算法,和之前的public-key相反,通过private-key作加密,通过public-key作解密,这样就实现了无法抵赖的功能。换句话任何人持有public-key都可以解密,但只有持有private-key的人才能加密。

A对文件X签名的基本流程如下:

  • A生成key pair (private-key, public-key)
  • 首先计算文件X的one-way hash, 比如SHA-256,然后对hash通过private-key加密得到签名
  • 将文件X和签名两个东西同时发给B
  • B通过public-key解密得到A计算的hash,和文件X的hash进行比对,如果一致就可以认定是A的签名

第十章: 证书

这里的证书指公钥证书(Public-Key Certificate, PKC), 对public-key的一个标准封装,比如X.509,同时认证机构(Certification Authority, Certifying Authority, CA)对此进行数字签名,以此表明这是这个证书内的pubilc-key确实是某某某的public key。

如果没有CA背书(签名),则可以随意欺骗用户说这个public-key是某某某的,所以需要CA居中协调。

公钥基础设施 (Public-Key Infrastructure) 是为了运用公钥制定的一系列规范和规格的总称,简称为PKI,其中包括了:

  • 用户: 注册公钥,使用公钥
  • 认证机构: CA
  • 仓库: 保存证书的数据库,即证书目录

小结

参考阅读:

gitstats 使用

install

git clone git://github.com/hoxu/gitstats.git

然后根据系统作link

ln -s ./gitstats/gitstats /usr/local/bin/gitstats

第一个参数是source, clone下来中的gitstats

usage

gitstats <your-project-dir> <your-project-dir>/stats
  • 第一个参数是项目目录
  • 第二个参数是gitstats html的生成目录

如果直接在项目目录中可以直接gitstats . stats

生成后open <your-project-dir>/stats/index.html即可

Screen Shot 2020-02-23 at 1 17 25 AM

待解决问题:

  • 生成后缺少gitstats.css和sortable.js,没去找解决办法,直接从repo中复制过来

Zoom API 学习

zoom提供的文档质量和可读性都非常高,首先给出了swagger,然后有逐一给出API说明。

Authentication

Every HTTP request made to Zoom API must be authenticated by Zoom. Zoom supports the following two primary means for request authentication:

  • OAuth2
  • JWT

Pagination

  • page_size: 这个根据不同的API有所区别
  • page_number: 1-based
  • 支持next_page_token

Error Definitions

Status Code Description Most Likely Cause
2XX Successful Request  
400 Bad Request Invalid/missing data
401 Unauthorized Invalid/missing credentials
404 Not Found The resource dosen’t exists, ex. invalid/non-existent user id
409 Conflict Trying to overwrite a resource, ex. when creating a user with an email that already exists
429 Too Many Requests Hit an API rate limit

Error response example

{
  "code": 300,
  "message": "Request Body should be a valid JSON object."
}

Error response when sending invalid fields

{
  "code": 300,
  "message": "Validation Failed.",
  "errors": [
    {
      "field": "user_info.email",
      "message": "Invalid field."
    },
    {
      "field": "user_info.type",
      "message": "Invalid field."
    }
  ]
}

这里的code是应用内错误,这个需要具体来对应API

Rate Limits

对于API设定Rate Limits,比如1 request/second,超出后返回429.

App Permissions

Permissions - Authorization - Documentation - Zoom Developer - Technical Documentation and Reference

Scope Description Accessible APIs
meeting:read View the user’s meetings GET Meeting APIs
meeting:write View and manage the user’s meetings All Meeting APIs
recording:read View the user’s recordings GET Cloud Recording APIs
recording:write View and manage the user’s recordings All Cloud Recording APIs

GET /users/{userId}/meetings

List all the meetings that were scheduled for a user (meeting host).

Scopes: meeting:read:admin meeting:read

Request Parameters

  • Path Parameter
    • userId string required (The user ID or email address of the user)
  • Query Parameter
    • type string [scheduled | live | upcoming]
    • page_size integer
    • page_number integer

Responses

  • 200
    • Example
    • Schema
  • 404
    • HTTP Status Code and message
    • Error Code and message

Send a Test Request

最后给出一个测试,对开发者太好。

Swagger

这个明确告诉你zoom内部是用swagger,我理解是document-first先出swagger,然后开发,最后根据swagger来做API Refenerce,因为毕竟swagger太结构化,不够友好,整个swagger.json文件居然56828行,WTF。

/users/{userId}/meetings:
  get:
    summary: List Meetings
    description: "List all the meetings that were scheduled for a user (meeting host).<br><br>\n**Scopes:**
      `meeting:read:admin` `meeting:read`<br>\n "
    tags:
    - Meetings
    operationId: meetings
    parameters:
    - in: path
      name: userId
      description: The user ID or email address of the user.
      type: string
      required: true
    - in: query
      name: type
      description: 'The meeting types: <br>`scheduled` - All the scheduled meetings.<br>`live`
        - All the live meetings.<br>`upcoming` - All the upcoming meetings.'
      type: string
      default: live
      enum:
      - scheduled
      - live
      - upcoming
      x-enum-descriptions:
      - all the scheduled meetings
      - all the live meetings
      - all the upcoming meetings
    - in: query
      name: page_size
      description: The number of records returned within a single API call.
      type: integer
      default: 30
      maximum: 300
    - in: query
      name: page_number
      description: The current page number of returned records.
      type: integer
      default: 1
    responses:
      '200':
        description: |-
          **HTTP Status Code:** `200`<br>
          List of meeting objects returned.
        schema:
          title: Group List
          description: List of meetings.
          allOf:
          - type: object
            description: Pagination Object.
            properties:
              page_count:
                type: integer
                description: The number of pages returned for the request made.
              page_number:
                type: integer
                description: The page number of the current results.
                default: 1
              page_size:
                type: integer
                description: The number of records returned with a single API call.
                default: 30
                maximum: 300
              total_records:
                type: integer
                description: The total number of all the records available across
                  pages.
          - properties:
              meetings:
                type: array
                description: List of Meeting objects.
                items:
                  allOf:
                  - properties:
                      uuid:
                        type: string
                        description: Unique Meeting ID. Each meeting instance will
                          generate its own Meeting UUID.
                      id:
                        type: string
                        description: Meeting ID - also known as the meeting number.
                      host_id:
                        type: string
                        description: ID of the user who is set as the host of the
                          meeting.
                      topic:
                        type: string
                        description: Meeting topic.
                      type:
                        type: integer
                        description: Meeting Types:<br>`1` - Instant meeting.<br>`2`
                          - Scheduled meeting.<br>`3` - Recurring meeting with no
                          fixed time.<br>`8` - Recurring meeting with fixed time.
                        enum:
                        - 1
                        - 2
                        - 3
                        - 8
                        x-enum-descriptions:
                        - Instant Meeting
                        - Scheduled Meeting
                        - Recurring Meeting with no fixed time
                        - Recurring Meeting with fixed time
                      start_time:
                        type: string
                        format: date-time
                        description: Meeting start time.
                      duration:
                        type: integer
                        description: Meeting duration.
                      timezone:
                        type: string
                        description: 'Timezone to format the meeting start time. '
                      created_at:
                        type: string
                        format: date-time
                        description: 'Time of creation. '
                      join_url:
                        type: string
                        description: Join URL.
                      agenda:
                        type: string
                        description: Meeting description. The length of agenda gets
                          truncated to 250 characters when you list all meetings for
                          a user. To view the complete agenda of a meeting, retrieve
                          details for a single meeting [here](https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meeting).
                  type: object
          type: object
        examples:
          application/json:
            page_count: 1
            page_number: 1
            page_size: 30
            total_records: 4
            meetings:
            - uuid: mlghmfghlBBB
              id: 11111
              host_id: abckjdfhsdkjf
              topic: Zoom Meeting
              type: 2
              start_time: '2019-08-16T02:00:00Z'
              duration: 30
              timezone: America/Los_Angeles
              created_at: '2019-08-16T01:13:12Z'
              join_url: https://zoom.us/j/11111
            - uuid: J8H8eavweUcd321==
              id: 2222
              host_id: abckjdfhsdkjf
              topic: TestMeeting
              type: 2
              start_time: '2019-08-16T19:00:00Z'
              duration: 60
              timezone: America/Los_Angeles
              agenda: RegistrationDeniedTest
              created_at: '2019-08-16T18:30:46Z'
              join_url: https://zoom.us/j/2222
            - uuid: SGVTAcfSfCbbbb
              id: 33333
              host_id: abckjdfhsdkjf
              topic: My Meeting
              type: 2
              start_time: '2019-08-16T22:00:00Z'
              duration: 60
              timezone: America/Los_Angeles
              created_at: '2019-08-16T21:15:56Z'
              join_url: https://zoom.us/j/33333
            - uuid: 64123avdfsMVA==
              id: 44444
              host_id: abckjdfhsdkjf
              topic: MyTestPollMeeting
              type: 2
              start_time: '2019-08-29T18:00:00Z'
              duration: 60
              timezone: America/Los_Angeles
              created_at: '2019-08-29T17:32:33Z'
              join_url: https://zoom.us/j/4444
      '404':
        description: |-
          **HTTP Status Code:** `404`<br>
          User ID not found.<br>
          **Error Code:** `1001`<br>
          User {userId} not exist or not belong to this account.<br>
    security:
    - OAuth: []

写的非常详细,仔细观察里面的内容和后面的出来的页面基本是完成一致的,我基本判断页面就是通过swagger套壳生成的,所以你就找葫芦画瓢基本就80分了。

Git内部是如何工作的?

先来看一个视频Git内部是如何工作的?Git的内部数据结构

What is git?

Git is a fast, scalable, distributed revision control system with an unusually rich command set that provides both high-level operations and full access to internals.

ref: https://github.com/git/git/blob/master/README.md

  • Porcelain (厕所的瓷砖, 指露在外面的意思, 或者说已经组合好的,面向用户的指令)
    • user-friendly commands: init, add, commit, branch, merge, etc.
  • Plumbing (修水管的工具, 指藏在后面的意思, 面向开发者的组件)
    • bunch of verbs that do low level work: hash-object, update-index, write-tree, etc.

补充:

  • 定义: git 本身并不单纯是revision control system, 更主要的使用来做content tracker, 只是很多人,包括作者本身用它来做revision control
  • Code Review: 需要工具加持

发展过程:

  • diff and patch
  • Tranditional SCM, Server Repo
  • Distributed SCM, Git

Git Database

  • Content addressable filesystem
  • Simple key-value data store
  • Key: SHA-1 hash (Everything is hashed)
    • 20 bytes, 40 hex, 160 bit, 2.9e48 distinct keys
  • Value: binary files
    • Commits: Actual git commits
    • Trees: Structure of file system
    • Blobs: content of files/data

git init

➜  hello-git mkdir demo1
➜  hello-git cd demo1 
➜  demo1 git init 
Initialized empty Git repository in /Users/nonocast/Desktop/hello-git/demo1/.git/
➜  demo1 git:(master) git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)
➜  demo1 git:(master) tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── ...
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

8 directories, 15 files

等同于

➜  demo2 mkdir .git
➜  demo2 echo 'ref: refs/heads/master' > .git/HEAD
➜  demo2 mkdir -p .git/objects/
➜  demo2 mkdir -p .git/refs/
➜  demo2 git:(master) git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

手动的方式你会发现,其实最小最小的git repo是1个文件加2个目录。

git add

➜  demo1 git:(master) echo 'Hello World' > hello.txt
➜  demo1 git:(master) git add hello.txt

你可以观察到.git/objects下会多出一个目录和一个文件,这个称为刚才文件内容的blob, 目录加上文件名40个字符构成SHA1 hash, 可以根据这个SHA1 hash查看对应文件的内容,

➜  demo1 git:(master) ✗ git cat-file -p 557d
Hello World

也可以尝试根据文件内容获取SHA1 hash, 与文件名和文件信息无关

➜  demo1 git:(master) ✗ echo "Hello World" | git hash-object --stdin
557db03de997c86a4a028e1ebd3a1ceb225be238

文件信息和内容是分开的,

➜  demo1 git:(master) git ls-files --stage
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 0	hello.txt

Area & Workflow

Area:

  • Working Directory
  • Staging Area
  • .git directory (Repository)

Workflow:

  • git add: Working > Staging
  • git commit: Staging > Repository
  • git checkout: Working < Repository

Staging Area对应到.git/index

➜  demo1 git:(master) ✗ file .git/index
.git/index: Git index, version 2, 1 entries
➜  demo1 git:(master) ✗ git ls-files --stage
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 0	hello.txt

如果需要从操作index, 即操作Staging Area

git rm --cached hello.txt
git update-index --add --cacheinfo 100644 <hash> hello.txt

现在回过来看如何手动git add

➜  demo2 git:(master) tree .git
.git
├── HEAD
├── objects
│   └── 55
│       └── 7db03de997c86a4a028e1ebd3a1ceb225be238
└── refs

3 directories, 2 files
➜  demo2 git:(master) git update-index --add --cacheinfo 100644 557db03de997c86a4a028e1ebd3a1ceb225be238 hello.txt
➜  demo2 git:(master) ✗ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   hello.txt

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	deleted:    hello.txt

➜  demo2 git:(master) ✗ git checkout -- hello.txt
➜  demo2 git:(master) ✗ cat hello.txt 
Hello World

显示deleted: hello.txt, 是因为Staging存在的hello.txt不存在于Working Area中, 然后通过checkout把这个文件从Repository中写到Working Area中。

git commit

git commit 后会生成两个文件, 一个tree, 一个commit

➜  demo1 git:(master) git log                          
commit 1a4dac33dcf0f20af514f0302a439765162cc75b (HEAD -> master)
Author: nonocast <[email protected]>
Date:   Wed Feb 12 02:50:28 2020 +0800

    initial
➜  demo1 git:(master) git cat-file
usage: ...

<type> can be one of: blob, tree, commit, tag
    -t                    show object type
    -s                    show object size
    -e                    exit with zero when there's no error
    -p                    pretty-print object's content
    ...

➜  demo1 git:(master) git cat-file -t 1a4d
commit
➜  demo1 git:(master) git cat-file -p 1a4d
tree 97b49d4c943e3715fe30f141cc6f27a8548cee0e
author nonocast <[email protected]> 1581447028 +0800
committer nonocast <[email protected]> 1581447028 +0800

initial

➜  demo1 git:(master) git cat-file -t 97b4
tree
➜  demo1 git:(master) git cat-file -p 97b4
100644 blob 557db03de997c86a4a028e1ebd3a1ceb225be238	hello.txt

查看所有objects,

➜  demo1 git:(master) git cat-file --batch-check --batch-all-objects      
1a4dac33dcf0f20af514f0302a439765162cc75b commit 166
557db03de997c86a4a028e1ebd3a1ceb225be238 blob 12
97b49d4c943e3715fe30f141cc6f27a8548cee0e tree 37

git logs 的起点是 HEAD -> refs/master -> commit

Commit again

➜  demo1 git:(master) mkdir src
➜  demo1 git:(master) echo "echo 'hello world'" > src/hello.sh
➜  demo1 git:(master) ✗ tree .
.
├── hello.txt
└── src
    └── hello.sh

1 directory, 2 files
➜  demo1 git:(master) ✗ git add .
➜  demo1 git:(master) ✗ git ls-files --stage 
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 0	hello.txt
100644 59c1fdfa8cd0b24760f0577903d0ac6cbb9b35e2 0	src/hello.sh
➜  demo1 git:(master) ✗ git commit -m 'add shell script'
[master 5708e5b] add shell script
 1 file changed, 1 insertion(+)
 create mode 100644 src/hello.sh

➜  demo1 git:(master) git log 
commit 5708e5b09d2d36cc1f0452932925266e3dd9d4a5 (HEAD -> master)
Author: nonocast <[email protected]>
Date:   Wed Feb 12 03:10:35 2020 +0800

    add shell script

commit 1a4dac33dcf0f20af514f0302a439765162cc75b
Author: nonocast <[email protected]>
Date:   Wed Feb 12 02:50:28 2020 +0800

    initial

➜  demo1 git:(master) git cat-file -p 5708
tree 46dc4ca4ec01d621e4a9e81f114a7b85474b0c22
parent 1a4dac33dcf0f20af514f0302a439765162cc75b
author nonocast <[email protected]> 1581448235 +0800
committer nonocast <[email protected]> 1581448235 +0800

add shell script
➜  demo1 git:(master) git cat-file -p 46dc
100644 blob 557db03de997c86a4a028e1ebd3a1ceb225be238	hello.txt
040000 tree c139cb17fbafa806c82102a32f640e94e54aa93d	src

➜  demo1 git:(master) git cat-file -p c139
100644 blob 59c1fdfa8cd0b24760f0577903d0ac6cbb9b35e2	hello.sh

➜  demo1 git:(master) git ls-files --stage
100644 557db03de997c86a4a028e1ebd3a1ceb225be238 0	hello.txt
100644 59c1fdfa8cd0b24760f0577903d0ac6cbb9b35e2 0	src/hello.sh

index需要等到下次add会做lazy update, commit并不会clean index, 这个不很重要, 知道就行。

所以:

  • File content -> blob
  • Directory -> tree (藏在tree的文件路径中,不单独保存)
  • snapshot of current file system -> commit

Directed Acyclic Graph (有向无环图)

  • N-ary tree (N叉树)
  • Linked List (链表)

Rollback

git revert会增加一个commit表示rollback, 重点是可以rollback这个rollback

➜  demo1 git:(master) git revert HEAD

➜  demo1 git:(master) git log   
commit 774072600e135ba902565c17cdfe23645dc17214 (HEAD -> master)
Author: nonocast <[email protected]>
Date:   Wed Feb 12 03:24:52 2020 +0800

    Rollback Second Commit
    Revert "add shell script"
    
    This reverts commit 5708e5b09d2d36cc1f0452932925266e3dd9d4a5.

commit 5708e5b09d2d36cc1f0452932925266e3dd9d4a5
Author: nonocast <[email protected]>
Date:   Wed Feb 12 03:10:35 2020 +0800

    add shell script

commit 1a4dac33dcf0f20af514f0302a439765162cc75b
Author: nonocast <[email protected]>
Date:   Wed Feb 12 02:50:28 2020 +0800

    initial
➜  demo1 git:(master) 

留下git reset的三种方式, mixed, soft和hard。

Branch

branch就是一个refs, 非常cheap.

Merge (Important!)

merge的过程:

  • Find intersection of 2 linked lists (找到两个linked list共同的base(commit节点))
  • 找到两个轨迹中hash不同的文件,然后做diff, 然后apply到Working Area
  • 然后产生Conflicts, 这时候Repository不会发生任何变化
  • merge成功后会产生一个commit, 这个commit有两个parent

Git is the stupid content tracker

In many ways you can just see git as a filesystem - it's content-
addressable, and it has a notion of versioning, but I really really
designed it coming at the problem from the viewpoint of a filesystem
person (hey, kernels is what I do), and I actually have absolutely zero
interest in creating a traditional SCM system.

--- Linus Torvalds

https://marc.info/?l=linux-kernel&m=111314792424707

Babel 7 学习

v2-7e9c591eb2fad6e1738ca0227e217592_1440w

阮一峰把Javascript历程分析的很清楚了,

我理解如下:

  • ECMAScript是标准
  • Google, Microsoft, Apple 等各个浏览器厂商自行实现
  • Node是基于Google V8
  • 大家都说支持ES6, 但是都是支持其中的一部分, 具体是兼容性要具体查
Ver 版本 发布时间 关键特性
5 ES5 2009 扩展了Object、Array、Function的功能等
6 ES2015 (ES6) 2015 类,模块化,箭头函数,函数参数默认值等
7 ES2016 2016 includes,指数操作符
8 ES2017 2017 async/await,Object.values(),Object.entries(),String padding等
9 ES2018 2018 异步循环,生成器,新的正则表达式特性和 rest/spread 语法
10 ES2019 2019 -

Node和ES标准的对应关系

安装es-checker

 ~ node -v 
v10.15.3

~ es-checker

ECMAScript 6 Feature Detection (v1.4.2)

Variables
  √ let and const
  √ TDZ error for too-early access of let or const declarations
  √ Redefinition of const declarations not allowed
  √ destructuring assignments/declarations for arrays and objects
  √ ... operator

Data Types
  √ For...of loop
  √ Map, Set, WeakMap, WeakSet
  √ Symbol
  √ Symbols cannot be implicitly coerced

Number
  √ Octal (e.g. 0o1 ) and binary (e.g. 0b10 ) literal forms
  √ Old octal literal invalid now (e.g. 01 )
  √ Static functions added to Math (e.g. Math.hypot(), Math.acosh(), Math.imul() )
  √ Static functions added to Number (Number.isNaN(), Number.isInteger() )

String
  √ Methods added to String.prototype (String.prototype.includes(), String.prototype.repeat() )
  √ Unicode code-point escape form in string literals (e.g. \u{20BB7} )
  √ Unicode code-point escape form in identifier names (e.g. var \u{20BB7} = 42; )
  √ Unicode code-point escape form in regular expressions (e.g. var regexp = /\u{20BB7}/u; )
  √ y flag for sticky regular expressions (e.g. /b/y )
  √ Template String Literals

Function
  √ arrow function
  √ default function parameter values
  √ destructuring for function parameters
  √ Inferences for function name property for anonymous functions
  × Tail-call optimization for function calls and recursion

Array
  √ Methods added to Array.prototype ([].fill(), [].find(), [].findIndex(), [].entries(), [].keys(), [].values() )
  √ Static functions added to Array (Array.from(), Array.of() )
  √ TypedArrays like Uint8Array, ArrayBuffer, Int8Array(), Int32Array(), Float64Array()
  √ Some Array methods (e.g. Int8Array.prototype.slice(), Int8Array.prototype.join(), Int8Array.prototype.forEach() ) added to the TypedArray prototypes
  √ Some Array statics (e.g. Uint32Array.from(), Uint32Array.of() ) added to the TypedArray constructors

Object
  √ __proto__ in object literal definition sets [[Prototype]] link
  √ Static functions added to Object (Object.getOwnPropertySymbols(), Object.assign() )
  √ Object Literal Computed Property
  √ Object Literal Property Shorthands
  √ Proxies
  √ Reflect

Generator and Promise
  √ Generator function
  √ Promises

Class
  √ Class
  √ super allowed in object methods
  √ class ABC extends Array { .. }

Module
  × Module export command
  × Module import command


=========================================
Passes 39 feature Detections
Your runtime supports 92% of ECMAScript 6
=========================================

你可以发现Node虽然支持ES6,但不支持其中的import/export,这个时候就需要Babel上场来弥补这个缺失,反过来说,如果你避开了import/export,你完全可以认为Node基本就支持了ES6。

换句话说,如果你可以node直接运行,那么webpack出来就可以直接运行,因为他们支持的ECMAScript Level是一致的。

认证和授权有什么区别?

  • Authentication (认证): 你是谁?
  • Authorization (授权): 你能干什么?

比如你去酒店开房,你需要拿身份证办理Check in,这是一个认证过程,身份证和密码的功效是一样的证明了你是谁,前台给你的房卡表示授权你开302房间,你不能用身份证去开别人房间吧,这就是认证和授权的区别。

整个授权过程有2个重要的概念:

  • role
  • scope (permission) 两个词汇同一概念,我们暂时用scope来表示

从零开始

我们写了两个api, 分别都需要认证,

let users = [
  { id: 1, name: 'foo' },
  { id: 2, name: 'bar' }
];

/**
 * List all users
 * auth: required
 */
router.get('/users', auth(), async ctx => {
  ctx.body = users;
});

/**
 * Update user
 * auth: required
 */
router.patch('/users/:id', auth(), async ctx => {
  let target = null;
  try {
    target = _.find(users, x => x.id === Number.parseInt(ctx.params.id));
    target.name = target.name.toUpperCase();
    ctx.body = target;
  } catch (error) {
    ctx.throw(404, error);
  }
});

这里mock了一个auth的middleware,完成用户foo的认证,

let auth = () => {
  return async (ctx, next) => {
    ctx.user = { name: 'foo' };
    await next();
  }
};

这个时候我们来了一个需求,普通用户可以查看所有用户,但只有管理员可以修改用户,给user加上role [user | admin], 然后在api中判断一下就搞定了,

router.patch('/users/:id', auth(), async ctx => {
  if (ctx.user.role !== 'admin') {
    ctx.throw(404);
  }
}

这个时候我们就把角色(role)做了硬编码,假设我们还有一个更新产品的功能,我也一样会判断角色是否是admin,但问题是用户可能需要自己管理角色,或者说他说管理员A负责用户管理,管理员B负责产品管理,这个时候你就懵b了吧,所以我们新建一层抽象,这个时候才引出了scope,我们基于scope做授权管理,而role和scope关系开放给用户去设定。想明白底层逻辑,这个时候你去看RBAC(Role based Access Control)就会比较好的理解。

ok, 我们来实现一下,首先建立user, role和scope关系,

let users = [
  { id: 1, name: 'foo', roles: ['user'] },
  { id: 2, name: 'bar', roles: ['user', 'admin'] }
];

let roles = [
  { name: 'user', scopes: ['user:read'] },
  { name: 'admin', scopes: ['user:read', 'user:write'] },
];

然后在auth中根据user的roles得到对应的scopes合集,

let auth = () => {
  return async (ctx, next) => {
    ctx.scopes = [];
    try {
      // ?name=foo
      // ?name=bar
      ctx.user = _.find(users, x => x.name === ctx.query.name);
      ctx.scopes = _
        .chain(ctx.user.roles)
        .map(roleName => _.find(roles, role => role.name === roleName).scopes)
        .flatten()
        .uniq()
        .value();
      debug(ctx.scopes);
    } catch (error) {
      ctx.throw(401);
    }
    await next();
  }
};

最后就是应用,

/**
 * Update user
 * auth: required
 * scope: user:write
 */
router.patch('/users/:id', auth(), async ctx => {
  if (!ctx.scopes.includes('user:write')) {
    ctx.throw(404);
  }

  let target = null;
  try {
    target = _.find(users, x => x.id === Number.parseInt(ctx.params.id));
    target.name = target.name.toUpperCase();
    ctx.body = target;
  } catch (error) {
    ctx.throw(404, error);
  }
});

测试一下,

➜  curl -XPATCH 'http://localhost:3000/users/1?name=foo' 
Not Found%                                                              
➜  curl -XPATCH 'http://localhost:3000/users/1?name=bar'
{
  "id": 1,
  "name": "FOO",
  "roles": [
    "user"
  ]
}

接着来考虑一个问题,普通用户随便不能随便改其用户信息,但他应该可以改自己的用户信息吧,那么普通用户是否需要user:write的scope呢?

这里假设修改本人信息的api为PATCH /user或者PATCH /users/me,那么在这个route需要scope吗?实际情况是,每个验证过身份的用户都可以修改自己的用户信息,所以就根本不需要scope,

/**
 * Update the authenticated user
 * auth: required
 * scope: /
 */
router.patch('/user', auth(), async ctx => {
  ctx.body = { name: ctx.user.name.toUpperCase() };
});

重构

我们通过auth这个middleware同时整合Authentication和Authorization两个过程。

改造后的调用更加简洁,

/**
 * Update user
 * auth: required
 * scope: user:write
 */
router.patch('/users/:id', auth('user:write', 'user'), async ctx => {
  try {
    let target = _.clone(_.find(users, x => x.id === Number.parseInt(ctx.params.id)));
    target.name = target.name.toUpperCase();
    ctx.body = target;
  } catch (error) {
    ctx.throw(404, error);
  }
});

对应的auth

let auth = (...scopes) => {
  return async (ctx, next) => {
    ctx.scopes = [];
    try {
      // ?name=foo
      // ?name=bar
      ctx.user = _.find(users, x => x.name === ctx.query.name);
      if (!ctx.user) { ctx.throw(401); }

      ctx.scopes = _
        .chain(ctx.user.roles)
        .map(roleName => _.find(roles, role => role.name === roleName).scopes)
        .flatten()
        .uniq()
        .value();
      // debug(ctx.scopes);

      if (scopes.length > 0) {
        // 检查授权
        if (_.intersection(scopes, ctx.scopes).length > 0) {
          await next();
        }
      } else {
        await next();
      }

    } catch (error) {
      ctx.throw(401);
    }
  }
};

补充话题: Decorator?

先来认识一下babel 6中的decorator, 也可以看阮一峰写的文章

@testable
class App {
  hi() {
    debug('hello world');
  }
}

function testable(target) {
  target.isTestable = true;
}

debug(App.isTestable);

运行这段代码,需要做点配置,

yarn add babel-cli babel-plugin-transform-decorators-legacy -D

.babelrc

{ "plugins": [ "transform-decorators-legacy" ] }

run
➜ npx babel decorator.js | node -

有了decorator的基础就可以了解一下gwuhaolin/koa-router-decorator: @route decorator for koa-router

@route('/monitor')
export default class MonitorCtrl{

  @route('/alive', HttpMethod.GET)
  async alive(ctx) {
    ctx.body = {
      data: true,
    };
  }
}

好不容易摆脱了Spring,没想到吧。

关于React App如何部署在子目录下的说明

首先npx create-react-app app然后把build出来的内容ln -s到/var/www/foo/bar/app
然后配置nginx

server {
  listen 80;
  server_name space.io;
  root /var/www/foo;

  location / {
    index index.html;
  }
}

浏览器访问http://space.io/bar/app,显示报错,无法获取

[Error] 404 (Not Found) (http://space.io/static/css/main.e977c7a5.chunk.css)
[Error] 404 (Not Found) (http://space.io/static/js/2.567b0d4d.chunk.js, line 0)
[Error] 404 (Not Found) (http://space.io/static/js/main.872637aa.chunk.js, line 0)
[Error] 404 (Not Found) (http://space.io/favicon.ico, line 0)

显然我们的css/js都在http://space.io/bar/app/而非http://space.io,这里出现了第一次mismatch.

通过curl可以发现返回的html内容如下:

<!doctype html>
  <html lang="en">
  <head>
  <meta charset="utf-8"/>
  <link rel="shortcut icon" href="/favicon.ico"/>
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
  <meta name="theme-color" content="#000000"/>
  <meta name="description" content="Web site created using create-react-app"/>
  <link rel="apple-touch-icon" href="logo192.png"/>
  <link rel="manifest" href="/manifest.json"/>
  <title>React App</title>
  <link href="/static/css/main.e977c7a5.chunk.css" rel="stylesheet">
  </head>
  <body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root">
  </div>
  <script>...</script>
  <script src="/static/js/2.567b0d4d.chunk.js"></script>
  <script src="/static/js/main.872637aa.chunk.js"></script>
  </body>
  </html>

这个就是build下的index.html,原始文件是app/public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <link rel="apple-touch-icon" href="logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

这里可以看到%PUBLIC_URL%, 可以理解为默认情况直接替换为'/', 如果需要替换就需要修改package.json中的homepage

"homepage": "."

重新yarn build后, 页面就OK了, 你可以看到之前的'/'改为'./',

<script src="./static/js/2.567b0d4d.chunk.js"></script>

从浏览器中访问,会基于当前path发出请求,即./可以翻译为http://space.io/bar/app/static/js/xxx, 看上去一切ok。

但是加上router后就会有问题,

import { BrowserRouter as Router, Route, Link } from "react-router-dom";

function Index() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about/">About</Link>
            </li>
          </ul>
        </nav>

        <Route path="/" exact component={Index} />
        <Route path="/about/" component={About} />
      </div>
    </Router>
  );
}

现在访问http://space.io/bar/app/
能render出li, 但是不能匹配/, 因为页面得到的window.location.pathname的/bar/app/
点击build出来后的href是http://space.io/
所以你要告诉react-router你的基础路径是什么,这时候就需要用到router的basename

第一个里程碑

到这里出现了第一个里程碑,就是通过:

  • "homepage": "."
  • router basename="/bar/app/"

可以正常访问http://space.io/bar/app/, 同时, 点link也都能正确路由, 浏览器上的url也ok。

到这里有2个问题:

  1. 如果直接复制这个链接或者刷新http://space.io/bar/app/about/就直接显示404了
  2. router的basename写死了,不能自适应

一个个问题来解决,
首先显示404的原因很简单,因为nginx找不到/bar/app/about这个页面,刚才输入的是/bar/app/所以映射到index.html,但是在/bar/app/about/下没有index,你可以在build里mkdir about实验一下。
这个也容易, 在nginx通过try file就行了。
通过try file后,homepage的.就不能再用了,因为会引导到http://space.io/bar/app/about/static/....,所以也必须写死'/bar/app'

React Router 是建立在 history 之上的,history 监听浏览器地址栏的变化并解析URL转化为location对象,然后router使用它匹配到路由,正确地渲染对应的组件。

第二个里程碑

借助后端的nginx配合

location / {
  index index.html;
  try_files $uri /bar/app/index.html;
}

然后在编译前就要确定部署时的subdirectory, 然后在package.json中配置

"homepage": "/bar/app/"

vscode还提示我这里homepage要写完整, 但如果要编译时还不知道部署到哪呢?
同时router采用同样的basename,

import pkg from "../package.json"
<Router basename={pkg.homepage}>
   ...
</Router>

react router v4/v5支持3种router:

  • BrowserRouter
  • HashRouter
  • MemoryRouter

在v4以后,应该来说肯定首推BrowserRouter, 因为看上去更好更自然, 如果用hash, url就会变成http://space.io/bar/app#/about, hash不需要做任何配置,不需要服务器后台介入try_files, 缺点就是不够好看, SEO不友好, 在极端情况还是会有些问题。

A that uses the hash portion of the URL (i.e. window.location.hash) to keep your UI in sync with the URL.IMPORTANT NOTE: Hash history does not support location.key or location.state. In previous versions we attempted to shim the behavior but there were edge-cases we couldn’t solve. Any code or plugin that needs this behavior won’t work. As this technique is only intended to support legacy browsers, we encourage you to configure your server to work with instead.

第三个里程碑

权衡利弊,还是BrowserRouter更好些, 麻烦也就麻烦一点, 需要做两件事情:

  • 后端: 针对每个app写下location block做对应的try_files
  • 前端: 编译前确定hompage, 写下相对路径 (/bar/app/)

如果一定需要编译后决定,就在index.html中增加类似的config

<script>
  System.config({ baseURL: '/' });
</script>

疑难杂症:
Q: Uncaught SyntaxError: Unexpected token '<'
A: 在这个上下文中就是因为加载的js因为路径原因不存在,又因为try_files的关系给出了一个html, js引擎因为不能解析html的<!doctype html>引起的错误。

参考文档:

Google 软件测试之道 读书笔记

Publisher: Addison-Wesley Professional; 1 edition (April 2, 2012)

序 -- 谷歌工程总监 Alberto Savoia

2001年,Google大概有200名开发人员,但只有区区3位测试人员! 那个时候,开发人员已经开始做自己代码的测试了,但由于测试驱动才刚刚开始,而且像JUnit还没有大规模使用。当时的测试主要是在做一些随机测试 (ad-hoc testing),其好坏取决于编写代码开发者的责任心。但即使那样也是可以接受的,因为,当时正处在创业阶段,必须快速前进并勇于创新,否则就无法和那个时代已经非常强大的对手竞争。

开发人员发现,为了测试充分,他们不得不针对每一行功能代码,写两到三行的单元测试代码,而且这些测试代码和功能代码一样都需要维护,且有着相同的出错概率。而且大家也意识到,仅做单元测试是不够的,仍然需要集成测试、系统测试、用户界面等方面的测试。当真正开始要去做测试的时候,会发现测试工作量变得非常大(且需要很多知识的学习),并要求在很短的时间内完成测试,要以“迅雷不及掩耳”之势完成。

我们为什么要在很短的时间内迅速地完成测试? 我一直这么认为,对于一个坏点子或考虑欠周的产品,即使再多的测试,也无法把它变成一个成功的产品。但如果测试方法不当,却会扼杀一个本来有机会成功的产品或公司,至少会拖慢这个产品的速度,让竞争对手有机可乘。

序 -- Patrick Copeland

2005年,工程团队还不足1000人,测试团队大概有50名全职人员和一些临时工,测试团队当时的称谓是"测试服务", 工作重点在UI的验证傻姑娘,随时响应不同项目的测试需求。

第1章 Google 软件测试介绍

Google 是如何测试的?

在Google,软件测试团队归属于"工程生产力"(Engineering Productive)的中心组织部门,这个部门的职责横跨开发测试人员使用工具的研发,产品发布和各种级别的测试,从单元级别的测试到探索性级别的测试。

当有人来问我,Google 成功的关键是什么,我的第一个建议就是,不要招聘太多的测试人员。正如Larry Page所说,“少则清晰”。在通往成功的道路上,Google的测试团队并非雄兵百万,我们更像是小而精的特种部队,我们依靠的是出色的战术和高级武器。

Google在测试人员如此缺乏的情况下,是如何应对的呢?简单地说,在Google,写代码的开发人员也承担了质量的重任。质量从来就不仅仅是一些测试人员的问题。在Google,每个写代码的开发者本身就是测试者。

1.1 质量不等于测试

质量不是被测试出来的。如果在最开始的设计创建的时候就是错的,那它永远不会变成正确的。从另外一方面来说,虽然质量不是被测出来的,但同样有证据表明,未经测试也不可能开发出有质量的软件。如果连测试都没有做,如何保证你的软件具有很高的质量呢?

角色

  • 软件开发工程师 (SWE, Software Engineer),传统上的开发角色,实现最终用户所使用的功能代码,创建设计文档,选择最优的数据结构和整体架构,并且花费大量时间在代码实现和代码审核上。SWE还需要编写和测试代码,包括测试驱动的设计、单元测试、参与构建各种大小规模的测试等。

  • 软件测试开发工程师 (SET, Software Engineer in Test),也是一个开发角色,只是工作重心在可测试性和通用测试基础框架上。他们参与设计评审,非常近距离观察代码质量和风险。为了增加可测试性,他们甚至会对代码进行重构,并编写单元测试框架和自动化测试框架。SET是SWE在代码库上的合作伙伴,相比较SWE是在增加功能性代码或是提高性能的代码,SET更加关注质量提升和测试覆盖率的增加。SET同样会花费几乎所有时间在编写代码上。他们这样做的目的是为了质量服务,而SWE则更加关注客户使用功能的开发实现上。

  • 测试工程师 (TE, Test Engineer),是一个和SET关系密切的角色,有自己不同的关注点--把用户放在第一位来思考,代表用户利益。TE直至整体质量实践,分析解释测试运行结果,驱动测试执行,构建端到端的自动化测试。TE扮演双重确认的角色,一方面确认开发人员在测试方面的工作是否到位,任何明显的bug都会表明早起开发人员所做的测试工作存在不足或比较马虎。当这些明显的bug变少时,TE会把注意力转移到常见用户使用场景中去,是否满足性能期望,在安全性、国际化、访问权限等方面是否满足用户的要求。

组织结构

Google的测试团队不隶属于项目团队,而是独立存在的部门,是与专注领域部门平行的部门(横跨各个产品专注领域),我们称为工程生产力团队。测试人员基本是以租借的方式进入产品团队,去做提高质量相关的事情,寻找一些测试不足的地方,或者公开一些不可接受的缺陷率数据。

这种借调模式可以让SET和TE时刻保持新鲜感并且总是很忙绿,另外还能保证一个好的测试方法可以快速在公司内部蔓延。

爬、走、跑

Google从来不会在一次产品发布种包含大量的功能。实际上,我们的做法恰恰嫌犯,在一个产品的基本核心功能实现之后,就立刻对外发布使用,然后从用户那里得到真实的反馈,在进行迭代开发。

一个产品在发布给用户使用之前,一般都要经历金丝雀版本、开发版本、测试版本、beta或正式发布版本。

  • 金丝雀版本 (Canary Channel):即每日构建版本,用来排出过滤一些明显不适宜的版本。使用金丝雀版本需要极度的容忍度,而且在这个版本下可能无法使用应有的基本功能。一般来说,只有这个产品的工程师(开发或测试人员)和管理人员才会安装使用金丝雀版本。

  • 开发版本 (Dev Channel): 这是开发人员日常使用的版本,一般是每周发布一个。该版本具有一定的功能并通过了一系列的测试。所有这个产品下的工程师都会被要求安装这个版本,并在日常工作中真正使用它,这样可以持续对这个版本进行测试。如果一个开发版本不能够满足日常真实工作的需求,那么它将会被打回为金丝雀版本。发生这种情况不但令人郁闷,工程团队也需要在花费大量的时间去重新评估。

  • 测试版本 (Test Channel): 这是一个通过了持续测试的版本。这个版本基本上是最近一个月里的最佳版本了,也是工程师日常工作中使用的最稳定最信任的一个版本。测试版本可以被挑选作为内部尝鲜 (dog food) 版本,如果该版本有比较持续良好表现,也是作为beta测试的候选版本。

  • beta或发布版本 (Release Channel): 这个版本是由非常稳定的测试版本演变而来,并经历了内部使用和通过所有质量考核的一个版本,也是对外发布的第一个版本。

注: 这里不是Web项目,而是Chrome,所以才会说每个人都要装Dev。

测试类型

Google并没有使用代码测试、集成测试、系统测试这些命名方式,而是使用了小型测试、中型测试、大型测试这样的叫法,这种强调测试的范畴(Test Size)而非形式。

  • 小型测试 (Small Test): 单元测试,快速执行,由SWE负责,主要解决“这些代码是否按照预期的方式运行”。
  • 中型测试 (Medium Test): 通常都是自动化实现的,该测试一般会涉及两个或两个以上,或更多的模块之间的交互。测试重点在于验证这些“功能近邻区”之间的交互是否正确。主要有SWE和SET负责。
  • 大型测试 (Large Test): 涵盖三个或更多的功能模块,使用真实用户使用场景和实际用户数据,一般可能需要消耗数个小时或更长的时间才能运行完成。更倾向于结果驱动,验证软件是否满足最终用户的需求。主要由TE和SET负责。

后面的内容就是展开来说每种角色的具体工作,值得一读。

如何写文档?

很多年以来都很烦写注释和文档,其中一个观点是: 注释和文档都是因为代码烂,换句话说,如果代码可读性高,思路清晰就不需要注释和文档,你可以用好的变量名称或者Extract Method将方法名称来代替注释。

但是这两年在写javascript,也可能是因为弱类型的关系,有时候一个object可以直接穿越几个layer,你看着接口都不知道他从哪里来,他要去哪里,也越发觉得注释,文档和单元测试的重要性。

文档的类型从读者来说可以分为User和Developer, User可以理解为外部文档(External),他们是交付物的消费者,消费者也有各种类型,前端人员要知道接口如何调用,测试人员要知道怎么测试,实施人员要知道怎么配置等等; 而Developer就可以理解为内部文档(Internal),是写给开发团队看的,方便别人正确理解代码,有时候其实更多的时候是帮助自己回忆。

内部文档 (Internal)

  • 产品需求 (PRD): 由项目经理编写,描述目标系统的业务功能
  • 界面设计 (UI Proposal): 由交互设计人员编写,用PPT形式表达界面逻辑
  • 设计文档 (Design): 由项目负责人编写,描写技术架构,业务模型,表达清楚如何组织即可
  • 代码注释 (Comment): 由开发人员编写

外部文档 (External)

  • 功能介绍 (Feature): 由项目负责人编写,写系统功能和特性
  • 外部接口文档 (API Documentation): 由开发人员编写,后台服务就是描述HTTP API或者GraphQL API, 如果是前端webapp,这个应该就可以省略了
  • 安装配置文档
  • 测试文档

实践

Markdown

除了UI Proposal这种必须是slides形式,其他的文档建议有可能的情况下采用Markdown,主要是可以直接在IDE,如vscode中直接编辑,更为重要的是可以加进git作版本管理。

GraphQL API

GraphQL是自描述的,所以可以直接生成强类型文档,同时辅以docsify来作为补充,这个方式典型就是Github v4。

Restful API

说实话,Restful API描述也没有一个标准方法,Github v3, Office365, Slack, Zoom各有不同,其中Zoom对外公开了Swagger接口。大体上来说分为3种类型,

  • 独立文档 (强类型): Swagger
  • 独立文档 (弱类型): docsify
  • 混合在代码文件中: apidoc

首先,放弃的就是apidoc这种类型,这种类型很尴尬,注释比代码还多,标准不如swagger这种规范,生成的页面不支持oauth调用,虽然是最方便的,但是质量不够。

Swagger的优势是足够强势,支持oauth直接调用,但是给你的空间不够多,需要花挺多时间去了解他,某种程度上变成了一个负担,随着接口增多文件巨长无比。vscode可以通过插件openapi缓解一下。

docsify就比较方便,只要规定一种格式,其他随便发挥,缺少调用,所以需要swagger或者postman辅助,但是其实测试调用还是用postman这种更为方便。

swagger和docsify本身都无法跟代码同步,所以需要靠开发团队重视。

写在最后

文档只是思考和实践的附属品,不能用文档代替思考和实践,千万不要为了文档而放弃思考和实践。

后台服务接口权限的设计

token 和 scope

后台服务权限的根本逻辑就是 tokenscope 的关系。

  • token: 请求标识, 每个请求都需要携带
  • scope: 每个所需要的授权, 和方法形成1对多

所以形成: token 1 - * scope 1 - * action

注: token为什么不是user呢? 因为不是所有的请求都是用户访问, 还要考虑设备和服务, 所以将标识统一抽象为token。

怎么获取 token 呢?

OAuth 2.0的5种方式:

  • Authorization Code (授权码) => user => role => token
  • Implicit (隐藏方式) => user => role => token
  • Password (密码) => user => role => token
  • Client Credentials (客户端凭证) => client => token
  • Device Code (设备码) => device => token

其中前3种都是通过用户名密码验证用户(user)身份最终得到user,进而获取token; Client Credentials则是通过appkey/secret获取客户端(client)或者应用(app); Device Code则是根据device code获取设备(device)。

Role

如果有1000个用户难道需要设置1000次用户和scope的关系吗? 通过建立类似"普通用户", "管理员"这样的角色(role),就形成了用户自定义的权限组实现快速分配。

那怎么解决前端菜单的权限呢?

我们可以设定一些内置的role来和前端的菜单进行匹配。比如webapp:menu:foo:bar

最终就形成了一个user * - * role * - * scope,其中用户自定义role可以包含系统内置role, 即:

  • tony

    • 普通用户
      • webapp:menu:item1
      • webapp:menu:item2
      • webapp:menu:item3
  • peter

    • 管理员
      • webapp:menu:item1
      • webapp:menu:item2
      • webapp:menu:item3
      • webapp:menu:item4
      • webapp:menu:item5

注: 在多租户的情况下,可以将用户role指向company, 菜单role指向client, 所有租户共享client的菜单role。

时机问题

写入

  • 通过Authentication就会得到一个token,进而根据5种策略获取与之对应的scope,将其写入redis

验证

  • 当接受到请求时根据token从redis提取出对应的user/device/client和scopes, scope控制是否可以访问方法,而user/device/client更多的是为了二层逻辑,比如限制数据的范围。

Token or JWT

首先, JWT (Json Web Token) 是一个token。只是JWT自身包含了userid之类的信息,可以理解为身份证和普通RFID卡的区别。JWT的优势在于token即信息,目的就是省去了redis,将redis的token主体前置到用户的cookie中进行持久化,缺点就是票据的维护,更新的代价。

但是每次想到每个request都需要带这么多bits,总觉得有点浪费,目前的情况感觉更倾向token+redis的简洁,以后的事情以后再说吧。

如何设计Restful API的分页?

需要考虑的问题:

  • 请求方式
  • 回复方式

请求方式

可考虑的传递通道:

  • url query, /foo?page=1
  • url path, /foo/page/1
  • http request json body
  • http header

比较:

  • page不是resource,所以不应该设计在url path中
  • 需要考虑到url复制分享和传统a标签兼容问题的情况,这里还不谈及SEO,所以应该来说url query还是最优选择

数据格式:

  • 基于page, [page:4, per_page:50]
  • 基于offset, [offset:200, limit: 50]
  • 基于cursor, [cursor: timestamp, limit: 50]

page和offset比较简单,但是在large datasets查询时会效率比较低,没有索引,所以才会有cursor方式,但是考虑到github, zoom,同时兼顾client的友好性,采用page-based还是比较折中的选择。

  • ?page=2&size=100: 指定第几页,以及每页的记录数
  • ?offset=100&limit=10: 指定返回记录的数量
  • ?sort=name+,group-: 多属性排序

回复方式

  • json
  • http header

github比较特殊,采用了将pagination信息已link方式放在了http header中,http body就是比较纯粹的array,所以github翻页也只有上一页,下一页。

一般来说,用json更多一些,回复内容包括:

  • total: 总数
  • page: 当前页数 (1-based)
  • size: 当前每页数量
  • items: 具体记录数组

Webpack 学习

什么是webpack?

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

简单来说就是一个打包工具, 某种程度就是传统语言中的linker,所以只要你写javascript就不可能离开webpack。

当前webpack版本: 4.42.0

从最小化开始

yarn init -y 起步, 然后开一个src/index.js

index.js

console.log('hello world');

直接运行webpack, 默认entry为'./src',即./src/index.js, 默认output是./dist/main.js,然后就能生产一行超长的js。

默认情况下等同于如下:

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  }
};

文档中指出,首先需要了解几个核心概念:

Entry & Output

Entry就是webpack构建dependency Graph的起点,即程序入口。Output就是打包后的产物。

支持多entry,这个后面再说。

Loaders

Hey webpack compiler, when you come across a path that resolves to a '.txt' file inside of a require()/import statement, use the raw-loader to transform it before you add it to the bundle.

从entry开始,webpack会追踪所有依赖,但默认的webpack只跟进javascript和json的依赖,将其打包,webpack并不认识css, svg,所以需要loader来指引webpack如何转换(transform)

const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }

这里表示当依赖碰到'.txt'结尾的文件交由raw-loader进入webpack

Plugins

如果说loader是根据文件名来做分类处理,Plugins就如同koa/express中的middleware, 对整体输出做transform。

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins

module.exports = {
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

Mode

可以将Mode理解为webpack采用的configuration,默认是production

index.js

console.log(process.env.NODE_ENV);

直接运行NODE_ENV为undefined,通过webpack后运行输出'production',观察./dist/main.js,你会发现在bundle的过程中webpack直接将process.env.NODE_ENV替换为'production', 不再依赖于外部的环境变量。

NODE_ENV=test node ./dist/main

此时输出的依然是production。

除了对环境变量的影响,更多是采用的优化策略不同,具体见这里

看下面这句代码:

process.env.NODE_ENV = process.env.NODE_ENV || 'development';

首先肯定是不推荐去写process.env.NODE_ENV,但这句在非webpack下是没问题的,但是如果webpack后运行会报错,

"development" = "development" || false;

换句话说,写代码的时候需要考虑一下webpack的感受,否则怎么死的都不知道。

此外,__dirname会被替换为/, 而__filename的路径也变成/,所以这些都是打包后带来的副作用。

Browser Compatibility

webpack supports all browsers that are ES5-compliant (IE8 and below are not supported). webpack needs Promise for import() and require.ensure(). If you want to support older browsers, you will need to load a polyfill before using these expressions.

Node部分则是需要8.x以上即可。


在基本了解了这几个概念以后,我们尝试将mode改为development,你会得到的main.js如下

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("console.log(\"hello world\");\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

正好100行,强烈建议在debug一下这个100行,你大概就能理解一个基本的webpack输出的routine了,这个文件是不依赖任何外部文件就可以运行,webpack将node的require翻译成了自己的方式进行管理。

Q: 为什么将conole.log封装到eval中呢?

A: 添加sourcemap,通过devtool可以在运行bundle代码是定位到source file, 具体可以查看JavaScript Source Map 详解 - 阮一峰的网络日志

我们来增加一个内部依赖,

app.js

module.exports = {
  message: 'hello webpack'
}

index.js

const app = require('./app');
console.log(app.message);

webpack后的./dist/main.js

/******/ (function(modules) { // webpackBootstrap
/******/ 此处省略webpackBootstrap,每个输出都一样
/******/ ({

/***/ "./src/app.js":
/***/ (function(module, exports) {

eval("module.exports = {\n  message: 'hello webpack'\n}\n\n//# sourceURL=webpack:///./src/app.js?");

/***/ }),

/***/ "./src/index.js":
/***/ (function(module, exports, __webpack_require__) {

eval("const app = __webpack_require__(/*! ./app */ \"./src/app.js\");\n\nconsole.log(app.message);\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

很容易理解吧,再来看外部依赖,

index.js

const _ = require('lodash');
_.times(3, i => console.log(i));

main.js

/******/ (function(modules) { // webpackBootstrap
/******/ 省略
/******/ })
/************************************************************************/
/******/ ({

/***/ "../../../../usr/local/lib/node_modules/webpack/buildin/global.js":
/***/ (function(module, exports) {

eval("...//# sourceURL=webpack:///(webpack)/buildin/global.js?");

/***/ }),

/***/ "../../../../usr/local/lib/node_modules/webpack/buildin/module.js":
/***/ (function(module, exports) {

eval("...//# sourceURL=webpack:///(webpack)/buildin/module.js?");

/***/ }),

/***/ "./node_modules/lodash/lodash.js":
/*!***************************************!*\
  !*** ./node_modules/lodash/lodash.js ***!
  \***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("...//# sourceURL=webpack:///./node_modules/lodash/lodash.js?");

/***/ }),

/***/ "./src/index.js":
/***/ (function(module, exports, __webpack_require__) {

eval("const _ = __webpack_require__(/*! lodash */ \"./node_modules/lodash/lodash.js\");\n\n// _.times(3, i => console.log(i));\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

先不说原理,看结果来说,就是webpack碰到外部lodash时,这时会先导入自身的globals和modules两个模块,然后导入lodash, 最后导入index.js

这样的好处是生成的js解决了所有依赖问题,直接复制到服务器上就可以运行,坏处也很明显,就是当文件多了以后这个文件要爆炸,单单是加了lodash就从3.8K变为552K, production为62K,看情况取舍。

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies), 这时候就需要用到externals配置。

const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  externals: {
    lodash: 'commonjs lodash'
  }
};

此时生成main.js

{

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("const _ = __webpack_require__(/*! lodash */ \"lodash\");\n_.times(3, i => console.log(i));\n\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ }),

/***/ "lodash":
/*!*************************!*\
  !*** external "lodash" ***!
  \*************************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("module.exports = require(\"lodash\");\n\n//# sourceURL=webpack:///external_%22lodash%22?");
}

你会发现其实外部依赖就是直接利用node的require进行动态加载。

我试了一下,也是OK的,其实就是一个意思,当碰到lodash时如何处理,

externals: {
  lodash: "require('lodash')",
}

当你希望排除所有依赖包的时候,你不可能一个个来写,所以可以直接利用externals-dependencies, 源代码只有21行,

import fs from 'fs'
import path from 'path'
export default function (arr = []){
    var packageJson,externals = {};
    try {
        var packageJsonString = fs.readFileSync(path.join(process.cwd(), './package.json'), 'utf8');
        packageJson = JSON.parse(packageJsonString);
    } catch (e){
        throw 'can not find package.json'
    }
    var sections = ['dependencies'].concat(arr);
    sections = new Set(sections)

    var deps = {};
    sections.forEach(function(section){
        Object.keys(packageJson[section] || {}).forEach(function(dep){
            externals[dep] = 'commonjs ' + dep;
        });
    });
    return externals
}

实质就是遍历所有node_modules然后返回一个array, pkg => 'commonjs pkg'。

官网上也说externals更多是服务library developers, 避免重复加载相同的依赖,我感觉如果是node落地服务,更多的倾向全量打包,一个js搞定,很多时候服务器是没有网络无法做npm install,这个时候就特别有价值。

到这里我们就可以玩一个花出来,比如说在bundle后如何混合加载unbundle的javascript file,

externals: {
  lodash: "commonjs lodash",
  plugins: "require('./plugins')"
}

index.js

const plugins = require('plugins');
console.log(plugins);

./dist/plugins/index.js

module.exports = [
  'plugin A',
  'plugin B'
]

再来,在Module Resolution中指出webpack会通过分析require/import指令来构建dependency graph,但是支持的require也是有要求的:

  • Absolute paths: require('/path/to/module');
  • Relative paths: require('./module');
  • Module paths: require('lodash');

比如说 index.js

let x = './app';
const app = require(x);
console.log(app.message);

node直接运行没问题,但是webpack后运行报错Error: Cannot find module './app'

但是,

let x = 'app';
const app = require('./' + x);
console.log(app.message);

这样就ok,原因是require不能单单是变量,一定要有一部分字符串才能引导进require.context方式。

如果想加载多个文件,

const fs = require('fs');
const path = require('path');

['card1', 'card2'].forEach(file => {
  console.log(require('./cards/' + file));
});

如果是目录对应

const fs = require('fs');
const path = require('path');

fs.readdir('./src/cards', (error, files) => {
  if (error) throw error;
  files.forEach(file => {
    console.log(require('./cards/' + file));
  });
});

EOF

参考:

小朋友的学习方法

  • 建立框架: 建立学习体系,把核心知识点逐一列出
  • 公式推导: 深刻理解公式背后的逻辑,千万不要死背公式
  • 不断复述: 理解后能用自己的语言表达,语言要精简,要有逻辑性
  • 仔细读题: 建立题目和知识点的对应关系,理解题目的意图

关于考试 - 补充于 2020/4/6
这次疫情难得有机会观察了小朋友在线考试的情况, 发现小朋友考试做题和平时做题完全是两种状态,体现在考试的时候因为有时间的压力显得非常着急,很多都是不过脑子直接输出,另外会被难的题目卡住很长时间。

所以事后我跟她讨论了一下:

  1. 时间和节奏问题,平时要加强时间锻炼,模拟考试的情况,能否在规定的时候解题,关键是看理解和熟悉程度。学会评估节奏,比如45分钟的考试,留出15分钟检查和难题,假如一共18题,那么将剩余的30分钟分为3块,每块对应6题,以此评估自己的速度是否合理,每10分钟评估一次。
  2. 草稿纸,分题分区。

如何表达时间? (Unix Timestamp)

The unix time stamp is a way to track time as a running total of seconds. This count starts at the Unix Epoch on January 1st, 1970 at UTC.

unix time stamp 是数字,时区无关,建议全程在系统内部,包括接口也应该采用unix time stamp,只有在需要人机交互时采用UTC或GMT来做format。

几个问题需要注意:

  1. 2^32最多上限只能到2038/1/19, 超过以后overflow,时间回到1970/1/1,所以最好用2^64存储
  2. 如果时间精度需要上升为毫秒(ms)时,就是增加10^3

约定

  • unix timestamp: 表示秒
  • unix ms timestamp: 表示毫秒

momentjs

Get

  • moment().unix() => unix timestamp
  • moment().valueOf() => unix ms timestamp

Set

  • moment(unix ms timestamp)
  • moment.unix(unix timestamp)

JNI 学习

首先,明确一个概念,jni所对应的xxxx.jnilib是native code, 某种程度上就是so, dll, 所以是平台相关, 你如果在x86上编译出来的jnilib到MIPS的机器上是不能运行的, 需要交叉编译或者到目标机器上编译。

基本的操作流程:

先在java中定义所需要的目标方法,比如:

package com.shgbit;

public class App {
    public static native String read();

    static {
        System.loadLibrary("Reader330");
    }

    public void run() {
        System.out.println(read());
    }

    public static void main(String[] args) {
        new App().run();
    }
}

比如我们希望read()返回一个字符串。

然后通过javah生成C Header

javah -jni  -classpath ./build/classes/java/main -d ./jni com.shgbit.App

我们现在需要在java path中提供一个jnilib只需要满足这个方法签名即可,所以随便你怎么搞,这里写了一个读卡的功能:

#include "com_shgbit_App.h"
#include <errno.h> /* Error number definitions */
#include <fcntl.h> /* File control definitions */
#include <stdio.h> /* Standard input/output definitions */
#include <stdlib.h>
#include <string.h>  /* String function definitions */
#include <termios.h> /* POSIX terminal control definitions */
#include <unistd.h>  /* UNIX standard function definitions */

#define FALSE -1
#define TRUE 0

/*
 * 'open_port()' - Open serial port 1.
 *
 * Returns the file descriptor on success or -1 on error.
 */

int open_port(void) {
  int fd; /* File descriptor for the port */
  // char *dev = "/dev/tty.usbserial-14310";
  char *dev = "/dev/cu.SLAB_USBtoUART";
  // char *dev = "/dev/cu.usbserial-0001";

  fd = open(dev, O_RDWR | O_NOCTTY | O_NDELAY);
  if (fd == -1) {
    perror("open_port: Unable to open port");
  } else {
    fcntl(fd, F_SETFL, 0);
  }

  return (fd);
}

/**
 *@brief  设置串口通信速率
 *@param  fd     类型 int  打开串口的文件句柄
 *@param  speed  类型 int  串口速度
 *@return  void
 */
void set_speed(int fd) {
  int i;
  int status;
  struct termios Opt;
  tcgetattr(fd, &Opt);
  tcflush(fd, TCIOFLUSH);
  cfsetispeed(&Opt, B57600);
  cfsetospeed(&Opt, B57600);
  status = tcsetattr(fd, TCSANOW, &Opt);
  if (status != 0) {
    perror("tcsetattr fd");
    return;
  }
  tcflush(fd, TCIOFLUSH);
}

/**
 *@brief   设置串口数据位,停止位和效验位
 *@param  fd     类型  int  打开的串口文件句柄
 *@param  databits 类型  int 数据位   取值 为 7 或者8
 *@param  stopbits 类型  int 停止位   取值为 1 或者2
 *@param  parity  类型  int  效验类型 取值为N,E,O,,S
 */
int set_Parity(int fd, int databits, int stopbits, int parity) {
  struct termios options;
  if (tcgetattr(fd, &options) != 0) {
    perror("SetupSerial failed");
    return (FALSE);
  }
  options.c_cflag &= ~CSIZE;
  switch (databits) /*设置数据位数*/
  {
  case 7:
    options.c_cflag |= CS7;
    break;
  case 8:
    options.c_cflag |= CS8;
    break;
  default:
    fprintf(stderr, "Unsupported data size\n");
    return (FALSE);
  }
  switch (parity) {
  case 'n':
  case 'N':
    options.c_cflag &= ~PARENB; /* Clear parity enable */
    options.c_iflag &= ~INPCK;  /* Enable parity checking */
    break;
  case 'o':
  case 'O':
    options.c_cflag |= (PARODD | PARENB); /* 设置为奇效验*/
    options.c_iflag |= INPCK;             /* Disnable parity checking */
    break;
  case 'e':
  case 'E':
    options.c_cflag |= PARENB;  /* Enable parity */
    options.c_cflag &= ~PARODD; /* 转换为偶效验*/
    options.c_iflag |= INPCK;   /* Disnable parity checking */
    break;
  case 'S':
  case 's': /*as no parity*/
    options.c_cflag &= ~PARENB;
    options.c_cflag &= ~CSTOPB;
    break;
  default:
    fprintf(stderr, "Unsupported parity\n");
    return (FALSE);
  }
  /* 设置停止位*/
  switch (stopbits) {
  case 1:
    options.c_cflag &= ~CSTOPB;
    break;
  case 2:
    options.c_cflag |= CSTOPB;
    break;
  default:
    fprintf(stderr, "Unsupported stop bits\n");
    return (FALSE);
  }
  /* Set input parity option */
  if (parity != 'n')
    options.c_iflag |= INPCK;
  tcflush(fd, TCIFLUSH);
  options.c_cc[VTIME] = 150; /* 设置超时15 seconds*/
  options.c_cc[VMIN] = 0;    /* Update the options and do it NOW */
  if (tcsetattr(fd, TCSANOW, &options) != 0) {
    perror("SetupSerial 3");
    return (FALSE);
  }
  return (TRUE);
}

unsigned char *bin_to_strhex(const unsigned char *bin, unsigned int binsz,
                             unsigned char **result) {
  unsigned char hex_str[] = "0123456789abcdef";
  unsigned int i;

  if (!(*result = (unsigned char *)malloc(binsz * 2 + 1)))
    return (NULL);

  (*result)[binsz * 2] = 0;

  if (!binsz)
    return (NULL);

  for (i = 0; i < binsz; i++) {
    (*result)[i * 2 + 0] = hex_str[(bin[i] >> 4) & 0x0F];
    (*result)[i * 2 + 1] = hex_str[(bin[i]) & 0x0F];
  }
  return (*result);
}

JNIEXPORT jstring JNICALL Java_com_shgbit_App_read(JNIEnv *env, jclass cls) {
  jstring result;
  int fd;
  char response[64];
  unsigned char request[5] = {0x04, 0x00, 0x01, 0xdb, 0x4b};

  fd = open_port();

  set_speed(fd);
  if (set_Parity(fd, 8, 1, 'N') == FALSE) {
    perror("Set Parity Error\n");
    exit(0);
  }

  int ret = write(fd, request, 5);

  int n = read(fd, response, 512);
  unsigned char *hexString;
  hexString = bin_to_strhex((unsigned char *)response, n, &hexString);

  result = (*env)->NewStringUTF(env, (char *)hexString);
  free(hexString);
  close(fd);

  return result;
}

读取后将返回的byte[]转换为hex string返回给java上层。

运行时,只需要jar和jnilib共目录即可,或者jnilib在系统path中都行。

最后,在调试的时候,可以通过gradle中加入:

run {
    systemProperty "java.library.path", "$projectDir/jni"
}

参考阅读:

swf in react

如果需要在react中通过swf播放rtmp就会用到如下代码:

function App() {
  const [source, setSource] = useState('rtmp://202.69.69.180:443/webcast/bshdlive-pc');
  let buttonStyle = { padding: '10 5', margin: 10, fontSize: 18 };

  return (
    <object type="application/x-shockwave-flash" id="SampleMediaPlayback" name="SampleMediaPlayback" data="SampleMediaPlayback.swf" width={800} height={500} >
      <param name="quality" value="high" />
      <param name="bgcolor" value="#000000" />
      <param name="allowfullscreen" value={true} />
      <param name='flashvars' value={`&src=${source}&autoHideControlBar=true&streamType=live&autoPlay=false&verbose=true`} />
    </object>
  )
}

现在的问题是我需要动态改变src,按button1放这个rtmp,按button2放那个rtmp,所以这个时候就需要借助jquery和swfobject动态生成object element,因为上面的写法只是第一次有效,后期就算改了也无法传递到swf中,除非借助externalInterface,不过SimpleMediaPlayback不支持EI,所以,

class App extends React.Component {
  preview(src) {
    let flashvars = { src, autoPlay: true, verbose: true };
    window.swfobject.embedSWF("SampleMediaPlayback.swf", 'player', 600, 300, 10, null, flashvars);
  }

  render() {
    let buttonStyle = { padding: '10 5', margin: 10, fontSize: 18 };

    return (
      <div className="App">
        <div style={{ margin: 20 }}>
          <button style={buttonStyle} onClick={() => {
            this.preview('');
          }}>null</button>
          <button style={buttonStyle} onClick={() => {
            this.preview('rtmp://202.69.69.180:443/webcast/bshdlive-pc');
          }}>source1</button>
          <button style={buttonStyle} onClick={() => {
            this.preview('rtmp://58.200.131.2:1935/livetv/hunantv');
          }}>source2</button>
        </div>
        <div ref='playerContainer'>
          <div id='player' />
        </div>
      </div>
    )
  }
}

最后需要在index.html中导入swfobject.js

代码参考: nonocast/swf-in-react

临八大山人小楷

八大山人原帖:
IMG_8455

承至言于先圣,受真教于上贤。探赜妙门,精穷奥业。一乘五津之道,驰骤于心田;八藏三箧之文,波涛于口海。爰自所历之国,总将三藏要文凡六百五十七部,译布中夏,宣扬胜业。引慈云于西极,注法雨于东垂。圣教缺而复合,苍生罪而还福。

  • 承(至言)于(先圣),受(真教)于(上贤):承对受,至言对真教,先圣对上贤。承受(传承)了古代先圣的至理名言,同时也受教于佛教的贤哲活佛。
  • (探赜)妙门,(精穷)奥业:探赜对精穷,表示探究深奥的道理,搜索隐秘的事情。
  • (一乘)(五津)之道,驰骤于心田,(八藏cáng)(三箧qiè)之文,波涛于口海:一乘即切眾生皆成佛道,五津即为五味。
  • 爰(yuán)自所历之国,总将三藏要文凡六百五十七部,译布中夏,宣扬胜业:胜业即宏大的功业。
  • 引(慈云)于(西极),注法雨于东垂。圣教缺而复合,苍生罪而还福:西极指天竺(即古印度),将西边慈悲云引入东土,功德无量的佛法象及时雨一样遍洒在大唐的国土上。

大概的意思就是: 法门的领袖人物玄奘法师,就是西游记的唐僧,他在先贤圣人那里接受了深奥的学问。对于"一乘", "五律"的佛学教说,他很快就牢记在心中,对“八藏”“三箧 ”的佛学理论,他讲起来就象波涛流水,滔滔不绝。于是玄奘从所经过的大小国家中,总共搜集吸取了三藏主要著作。一共六百五十七部,翻译成汉文后在中原传布,从此这宏大的功业得以宣扬。将佛法传入东土,给人民带来幸福。

湿火宅之干焰,共拔迷途;朗爱水之昏波,同臻彼岸。是知恶因业坠,善以缘升。升坠之端,惟人所托。譬夫桂生高岭,云露方得泫其花;莲出渌波,飞尘不能污其叶。非莲性自洁而桂质本贞,良由所附者高,则微物不能累;所凭者净,则浊类不能沾。夫以卉木无知,犹资善而成善;况乎人伦有识,不缘庆而求庆?方翼兹经流施,将日月而无穷;斯福遐敷,与乾坤而永大。

  • 湿(火宅)之干焰,共拔迷途;朗(爱水)之昏波,同臻彼岸:熄灭了火屋里燃烧的熊熊烈火,(解救众苍生于水深火热之中),从此不再迷失方向)。朗对湿,即照亮的意思。佛光普照,驱散了昏暗,照耀着众生到达超脱生死的彼岸。
  • 是知恶因业坠,善以缘升:因此懂得了做恶必将因果报应而坠入苦海,行善也必定会凭着佛缘而升入天堂。
  • 升坠之端,惟人所托:天堂还是地狱,都是看各人的所作所为。
  • 譬夫桂生高岭,云露方得泫其花;莲出渌波,飞尘不能污其叶:比如桂花长在高高的山岭上,天上的雨露才能够滋润它的花朵,莲花出自清澈的湖水,飞扬的尘土就不会玷污它的叶子。
  • 非莲性自洁而桂质本贞,良由所附者高,则微物不能累;所凭者净,则浊类不能沾:这并不是说莲花原本洁净,桂花原本贞洁,而是因为它们所依附的条件本来就高,所以那些卑贱的东西不能玷污它们。
  • 夫以卉木无知,犹资善而成善;况乎人伦有识,不缘庆而求庆?:我以为花花草草它们是无知的,但是它们都知道依靠好的东西就会有好的回报。何况人呢?这里庆对应善,指幸福。
  • 方翼兹经流施,将日月而无穷;斯福遐敷,与乾坤而永大:所以希望这部《大唐三藏圣教》能够得以流传广布,像日月一样,永放光芒,将福祉与天地共存,发扬光大。

字:

  • 於:于
  • 赜 (zé):深奥;玄妙
  • 箧 (qiè): 小竹籍,就是现在的行李箱
  • 藏 (zàng): 作名词,表示藏(cáng)书的地方。cáng为动词。
  • 爰 (yuán): 于是

注:

  • 摘录自《大唐三藏圣教序》碑文

参考阅读:

MQTT 学习

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。

MQTT官网是mqtt.org, 目前主流版本是3.1.1 (2014年), 最新版本是5.0 (2019年), 协议文档可以看mcxiaoke/mqtt,翻译的非常好。

Broker

我们称mqtt server为broker,在此之前先搞清楚broker, agent, proxy 3个概念:

  • Broker (经纪人): 是为促成他人交易,充当订约居间人,为委托方提供订约的信息、机会和条件的主体。Broker是一个独立主体,但没有自主决策能力,只负责订约过程。
  • Agent (代理人): 是行使被代理者的权力,完成相关的使命或者任务主体。Agent是一个独立主体,负责完成任务但不负责执行任务,Agent具有一定的自主决策能力,如对服务请求的选择。
  • Proxy (代理): 是指行为代理,不是一个主体。Proxy是完全的传递者,如请求和响应的转发,操作控制的传递。

以买卖房子为例, proxy只是个简单的传话筒, broker就是房产中介撮合买卖双方, agent是卖方或者买方全权委托的代理人, 有权决定卖不卖或者买不买。

MQTT首先是消息队列 (message queue), 消息队列就一定有消费者和生产者两个角色, 消息队列在这两个角色之间扮演了拉皮条的角色,所以用borker。什么是server? 一个web server更多的是单边操作,只需要处理browser发过来的请求,这就是server和broker的区别。

在系统设计时,可以将mqtt broker看作和rabbitmq, zeromq等方式处理,目前比较流行的是:

前者是C++, 后者是Erlang。

如果你需要将broker整合进node service,之前社区用的多的是mosca, 后来作者更新为aedes, 改进了性能和插件体系。

const debug = require('debug')('app');
const aedes = require('aedes')();
const logging = require('aedes-logging');
const server = require('net').createServer(aedes.handle);
const port = 1883;

// logging({
//   instance: aedes,
//   server: server
// });

aedes.on('clientReady', client => {
  debug('client connected', client.id);
});

aedes.on('clientDisconnect', client => {
  debug('client disconnected', client.id);
});

aedes.on('publish', (packet, client) => {
  debug('publish', new String(packet.payload));
});

server.listen(port, function () {
  debug('server started and listening on port', port);
})

直接开个MQTTX模拟一下客户端就能测试了,非常方便。

参考:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.