NAV
Go C Shell Python Javascript SQL

An Introduction

We trust you have received the usual lecture from the local System Administrator. It usually boils down to these three things:

Agile

CI

Continuous Integration

持续集成已经是敏捷编程的基石之一. 在 TW 网站 上列出了比较流行的 CI Server. 在现在流行的 Java 领域中, 比较流行和有名的 CI Server 有:

Contents

CruiseControl

CruiseControl by Thought Works,

Hudson

Maybe the mostly used CI Server now.

LuntBuild

QuickBuild

Business version of LuntBuild. Now it has a community Edition, full functional, but limited on configuration-number.

TeamCity

TeamCity by Jetbrains. Most used among propriety CI Servers. It also has a free version.

AntHillPro

Bamboo

Bamboo by Atlassian, who developed Jira and Wiki Confluence.


Functions

The basic functionality of CI Server is a 定时调度器. Set a timer, and do some job. This function can be achieved by home-made tools, like cron + Ant/Maven.

The extra functions of CI Server used in corporation: - 统一管理: 100+ 项目 - 流程管理: Daily Build -> QA Build -> Release Build - 权限管理: connect to Active Directory, managing user authorization - 持续测试: Continuous Testing, 产生详尽的报告, 收集测试统计数据以分析和考量项目 - 持续质检: Continuous Code Quality Analysis, 处理项目产生的 Coverage 报告, 代码静态分析报告 - SCM集成: 与 VCS 集成, 如 ClearCase, Subversion, StarTeam - 工具集成: bug tracking, IDE, etc

config

一般使用 Project, Project Group 管理. TeamCity 的导航最方便. LuntBuild 和 QuickBuild 使用 Configuration/配置 的概念, 一个配置处理一个最基本的流程, 如 CheckOut, Build, Publish Artifacts; 这两者的人性化方面有所不足.

QuickBuild 唯一支持配置树, 使用 Configuration 而不是 Project, 对多项目最为实用. QB 使用继承关系, 完成基本配置后, 其它的修改一下参数即可.

TeamCity 支持 Project Group 和 Project 两层关系, 也没有继承关系, 只有 copy project 功能. 而 QB 可以拷贝整个子树, 因而可以制作配置模板, 在不同部门和项目间使用变量来控制.

TeamCity 配置起来最方便. 另外它有 JUnit, NUnit 类的 Tests, 不用写 Ant 脚本, 可以直接找出项目里的 unit tests, 这个是它独有的.

CruiseControl, Hudson, Bamboo 没有特别的优点.

Daily Build, QA Build, Integration Build, Release Build. 每天, 每个 Sprint 做 QA 和 Integration.

TBD

如果团队使用 XP 编程, 要做 Commit Test Build, 每个提交自动触发一个 build, 保证构建和unit-tests和code-quality不受破坏. 即 Continuous testing 和 continuous code quality analysis, 这要利用 JUnit, NUnit, CheckStyle, PMD, Cobertura, FxCop 等工具实现.

Scrum

Agile & Lean

Lean –> Lean Software –> Scrum <– Design Pattern v v –> Kanban <– Agile <– eXtreme Programming –> Lean Startup

Agile: 质量, 生产率, 价值 Lean: 消除浪费

Agile: Scrum, 框架和过程; XP, 实践; FDD Kanban, Lean DSDM, crystal

[Video]: Jeff Sutherland breaks down the structure of scrum [Docu]: Scrum Guide by Ken Schwaher

Meeting: Beginning, End, Daily Role: Scrum Master, Product Owner, Team Reporting: Burn-down chart, self-reporting

Daily Meeting:

问题-措施-改进 的闭环改进

v Product Backlog, 梳理 v 工作计划, 规划 v 冲刺列表 v 每日列表 v 冲刺执行 v 产品增量, 潜在可交付 v 冲刺评审 v inspect adapt v 冲刺回顾

Scrum Guide

Annually Release

Scrum 定义了角色, 事件, 工件, 和它们的组织规则.

透明性是一个学习, 说服, 改变的过程, 不会一步完成
3-6 个月来有效使用 scrum, 按其机制运转

迭代增量: 优化可预测性和风险管理

Scrum Master 服务于 PO, 团队, 组织

团队规模 3-9 人, 人少时没有足够互动, sprint 中技能受限; 人多需要过多协调沟通, 复杂性太高, 不便于经验管理. 团队要拥有开发产品增量的全部技能.

事件. 每个 sprint 作为一个项目, 周期在一个月内, 一般两周. 长度和时间节点必须固定, 完成的内容可调整.

工作

监控实际目标的进度

Software Development

软件开发中要考虑的工程因素有进度, 成本, 质量, 维护 等等. 而现在软件开发中要面对需求变更, 不定需求, 无用户需求. 为解决这些问题, 软件行业在以下几个方面做出了成功的尝试: 流程工具, 设计, 测试, 人力.

对于团队, 要做人员备份周期轮换.

历史

产品实现的开发管理中需求, 设计, 实现, 验证, 维护所需要投入比成为了 7:6:7:13:67, 使软件开发难以为继.

开发中目前的问题有需求和市场两方面. 需求通常是不确定的, 会不断变动的, 在开发初期无法掌握细节, 容易造成过度设计, 过度开发. 市场中则充满(同质)竞争, 市场的快速变化带来需求的快速变化, 需要开发过程能够快速响应.

需求的不明确和频繁变化 -> 功能变更 -> 交付时间短 -> 测试不充分 -> 交付质量差 -> 修改成本 需求的不明确和频繁变化 -> 功能变更 -> 设计修改 -> 设计质量低 -> 修改成本 | 变更

现在的开发是价值导向的. 团队的稳定性, 才可有效保持开发效率的可预测性. 资源是确定的, 时间是确定的, 根据资源和时间来调整需要产出的功能.

敏捷开发中, 下列左侧比右侧有更大价值. 个体和交互 > 过程和工具 可以工作的软件 > 面面俱到的文档 客户合作 > 合同谈判 响应变化 > 遵循计划

individuals and interactions working software customer collaboration responding to change

12 Rules of Agile 1. 尽早, 持续, 交付 2. 欢迎改变需求 3. 可工作的软件, 经常交付 4. 业务人员和开发人员一起工作 5. 个体构建项目 6. 团队内与团队间面对面交流 7. working software 8. 可持续 9. 优秀技能, 好的设计 10. 简单 11. 自组织团队 12. 定时反省

Agile 是要做有价值的事, 是理念, 优秀实践, 具体应用. 基本特征: 迭代开发, 增量交付, 客户互动, 持续集成, 自我管理. 方法: Scrum, XP, FDD, 看板(限制在制品)

迭代一般是十四天 fortnight. 开发时有计划, 结束时有回顾.

基本特征: 价值流, 尊重一线人员, 消除浪费(库存, 次品, 传送), 拉动式生产, 质量内嵌(生产环节早发现问题), 整体优化.

价值, 持续的短交付周期, 尊重他人, 持续改善. - 不给”客户”带来麻烦 - 先发展员工, 再构建产品 - 无浪费性工作

14 原则

研发管理体系现状: IDD, CMMI, 敏捷, 精益.

以人为本, 持续改善. There is no silver bullets.

Unit Test

Questions

About Unit Test

WHAT

对一个明确的功能/函数, 测试其在给定条件下的工作是否正确.

Unit test > Integration test > Automated Gui-tesst > Manual Test

Unit, Components: 成本低, 效率高, 缺陷晚定位 Acceptance, API User Interface, Manual: 反映真实需求, 接近业务

WHY

Value

HOW

Java: JUnit, Hamcrest, Mockito

Test

单元测试要点: Fast, Isolated, Repeatable, Self-verifying, Timely

Given, When, Then 模式

One test, One function, 一个测试测一个方法

Mock

适用于所 mock 的对象

TDD, Test Driven Developing

一种增量式软件开发方法

实践: If tests pass, write no codes.

XP, eXtreme Programming

Algorithm

Huffman Encoding

class Node(object):
    def __init__(self, symbol = '', weight = 0, left = None, right = None):
        self.symbol, self.weight, self.left, self.right = symbol, weight, left, right
        if (left and right):
            self.weight = left.weight + right.weight

    def result(self, code = ''):
        r = []
        self.symbol and r.append((self.symbol, self.weight, code))
        self.left and r.extend(self.left.result(code + '0'))
        self.right and r.extend(self.right.result(code + '1'))
        return r


def genHuffman(char, freq):
    nodeList = []
    for i in range(len(char)):
        nodeList.append(Node(char[i], freq[i]))

    while (len(nodeList) >= 2):
        nodeList.sort(key = lambda x: x.weight, reverse = True)
        nodeList.append(Node(left = nodeList.pop(), right = nodeList.pop()))

    return nodeList[0] if nodeList else Node()


def getFrequency(freq):
    total = sum(freq)
    return list(map(lambda x:x/total, freq))

# test
symbol = ['A', 'B', 'C', 'D', 'E']
counts = [15, 7, 6, 6, 5]
print(genHuffman(symbol, counts).result())

# output
# space	7	111
# a	4	010
# e	4	000
# f	3	1101
# h	2	1010
# i	2	1000
# m	2	0111
# n	2	0010
# s	2	1011
# t	2	0110
# l	1	11001
# o	1	00110
# p	1	10011
# r	1	11000
# u	1	00111
# x	1	10010

symbol = ['space','a','e','f','h','i','m','n','s','t','l','o','p','r','u','x']
counts = [7, 4, 4, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1]
for e in sorted(genHuffman(symbol, counts).result(), key = lambda x:x[1], reverse = True):
    print (e[0], e[1], e[2])

Huffman encoding goes from leaves to root, unlike Shanno-Fano encoding which goes from root to leaves.

A simple alg using a priority queue.

  1. Create a leaf-node for each symbol, with the info of the symbol’s frequency.
  2. Loop while there are more than 1 node in the queue
    • Replace the 2 nodes of lowest probability, with 1 node that takes the 2 nodes as children and its weight is the sum of the 2 nodes’ weight.
  3. The last one node is the root of the completed tree.

C

Memory

#include <string.h>
int g_a = 0; // global, initialized
int* g_p;    // global, uninitialized
int main() {
    int a;           // stack
    char s[] = "ab"; // s: stack, "ab" constant
    char *p1;        // static
    char *p2 = "12"; // p2: stack, "12" constant
    static int b = 0;// static
    g_p = (char *)malloc(8); // heap
    p1 = (char *)malloc(8);  // heap
    strcpy(g_p, "12"); // "12" constant
                       // the memory might be optimized by compiler and use the same memory as p2
    return 0;
}

内存分配方式中的 栈、堆-自由存储区、全局与静态存储区、常量存储区

堆栈的区别

堆和栈的区别是大家常说起的一个问题。

#include <stdlib.h>
int main() {
    int* a = (int*) malloc(4);
    free(a);
    return 0;
}

指针 a 在栈上分配了一段内存,malloc 在堆上分配了一段4个整型大小的内存,然后把这段内存的首地址返回给指针 a。

堆和栈的区别主要有5点。(中文中“堆栈”指栈。)

  1. 管理方式
    栈由编译器自动管理;堆由用户程序主动管理,显示进行分配和释放。

  2. 空间大小
    栈一般有一定的空间大小,而且默认的空间大小较小,例如1M;而堆在32位系统下的地址空间就可以达到4G。

  3. 碎片
    栈是 FILO 队列,永远不会从中间 pop 内存块。而堆在大量的 malloc/free, new/delete 内存空间会产生许多的不连续,生成内存碎片。

  4. 分配方向
    堆地址的分配是从小往大,向着内存地址增加的方向进行;栈地址的分配是从大往小,向内存地址减小的方向进行。

  5. 分配方式 这部分解释有问题
    栈的静态分配由编译器完成。堆是动态分配的,没有静态分配。 栈的动态分配,使用 malloc,但由编译器进行释放。

  6. 分配效率
    栈比堆的效率更高。栈在系统层有更多的支持,有专门的指令进行压栈和出栈,有专门的寄存器存放栈地址。堆由函数库提供支持。 在堆上分配一块内存时,库函数会按照特定算法在堆中查找足够大小的可用空间,如果由碎片过多等原因没有足够大小的空间,则调用系统功能去增加程序数据段的空间,以产生足够大小的内存。

和栈相比,堆没有专门的系统层支持,可能引发用户态和核心态的切换(什么意思)使内存申请的代价更大,大量的分配和释放操作后容易产生大量内存碎片。在效率更重要的地方,比如函数调用,是利用栈来完成的。函数调用中的参数,返回地址,EBP,局部变量都由栈来存放。而堆比栈更灵活(怎么灵活的),在分配大量内存空间时,优先使用堆。

堆和栈都是对存空间的操作,要防止内存越界问题。内存越界会使程序产生意外的结果,或使程序崩溃。

静态变量

public 的静态变量可以用 ClassName::staticMemberName 引用。

Design

Design Pattern

设计模式是经过验证的代码设计经验的总结。其关注点是代码的可重用、易理解、可靠性。

设计模式的基本类型有:创建、结构、行为。

设计原则

设计模式是设计原则的实现,以达到代码的复用和可维护。

在解决问题时,设计模式可以帮忙我们在更高的抽象层次上工作。先是对问题的分析,了解问题中的因果关系,看使用模式的先决条件是否满足。然后根据分析给出解决方案。解决方案不是具体的设计和实现,而是对问题做抽象描述,表明如何用元素的组合而解决问题。

创建

结构

行为

Reactive System

响应式系统,响应式编程

事件驱动实现响应式编程,消息驱动实现响应式系统。

响应式编程强调的是数据流而非控制流。

例子:

JVM 中支持 非阻塞式反压异步流,响应式编程 的流行库有:Akka Streams, Ratpack, Reactor, ExJava, Vert.x

响应式编程的基本好处是:提高多核和多 CPU 硬件的计算资源利用率;通过减少序列化点来提高性能(阿姆达尔定律,古瑟通用可伸缩定律 Amdahl, Guenther)。

响应式编程中,active 活动组件间一般不需要明确的协作,而避开了传统的编程范式的做法:尽力提供一个简单直接的可持续的方法来处理异步非阻塞计算和 I/O。

响应式编程的价值在于组件的创建和工作流的组合。在异步执行上加入反压以避免过度使用/无限度地消耗资源。

为在更高层次上理清一个系统,设计响应式系统,需要为其设计响应式架构。响应式编程仅是一种编程范式,要注意它的适用条件和情形。

  1. 事件驱动与消息驱动

    响应式编程的着眼点在短时数据流链条上的计算,因而使用事件驱动;响应式系统关注于通过分布式系统的通信和协作所得到的弹性和韧性,使用消息驱动 messaging。

    事件驱动的数据流驱动模型,拥有 long-live addressable 长期存活可寻址 组件的消息驱动系统,两者的不同在于,消息具有固定的导向,事件没有;消息会有一个明确的去向,而事件只是一个等待被观察的信息。消息式结构更适用于异步,因为消息的发送与接收和发送者与接收者是分离的。

    一条消息就是一则被送往一个明确目的地的数据。一个事件则是达到某个给定状态的组件发出的一个信号。在一个消息驱动的系统中,可寻址到的接收者等待消息的到来然后响应它,否则保持休眠状态。在一个事件驱动系统中,通知的监听者被绑定到消息源上,这样当消息被发出时它就会被调用。这意味着一个事件驱动系统专注于可寻址的事件源,而消息驱动系统专注于可寻址的接收者。

    分布式系统需要通过消息在网络上传输进行交流,以实现其沟通基础,而事件的发出则是本地的。常见的做法是在底层通过发送包裹着事件的消息来搭建跨网络的事件驱动系统,这样能维持在分布式环境下事件驱动编程模型的相对简易,可以用在合理范围内的特殊案例上。

    分布式环境下的事件驱动在编程模型的抽象性和简易性上有好处,但在控制性上有欠缺。消息强迫我们去拥抱分布式系统的真实性和一致性。你需要去考虑局部错误,错误侦测,丢弃/复制/重排序消息 (partial failures, failure detection, dropped/duplicated/reordered),还有一致性问题,管理多个并发真实性(并发真实性?)。你需要面对它们,处理它们,而不是藏在低劣的抽象层后,假装网络并不存在,像是 EJB, RPC, CORBA, XA。

    在设计中,这种语义和适用性上的不同对应用有深刻的影响,包括分布式系统的复杂性中的弹性,韧性,移动性,位置透明和管理。

    在使用了响应式编程技术的响应式系统中,有用于沟通的消息,也不呈现现实的事件。

  2. 响应式系统

  3. 响应式程序与系统

  4. 弹性

  5. 韧性

  6. 生产效率

  7. 编程与系统关联

  8. 总结


流,轻量,实时

分布式环境下,实现技术,工具,设计模式

SCALE, 负载平衡,主动执行

FRP(FP) is misused

RP,由有效信息推动,而非执行流程/线程推动的控制流。信息实现异步非阻塞与 IO 非绑定。

Dist

Kafka

Elastic Compute

Editor

Emacs

Emacs Config File

init.el

VIM Shortcuts

退出

窗口操作

移动

h,j,k,l: 左,下,上,右。
w: 下一个词的词首。
e:下一个词的词尾。
b:上一个词的词首。
<>: v 模式选中后进行缩进。

跳转

%: 可以匹配{},"",(),[]之间跳转。
H、M、L:直接跳转到当前屏幕的顶部、中部、底部。
#H:跳转到当前屏的第#行。
#L:跳转到当前屏的倒数第#行。
zt: 当前编辑行置为屏顶。
zz: 当前编辑行置为屏中。
zb: 当前编辑行置为屏底。
G:直接跳转到文件的底部。
gg: 跳转到文件首。
():跳转到当前的行首、行尾。
{}:向上、向下跳转到最近的空行。
[{:跳转到目前区块开头。
]}:跳转到目前区块结尾。
0: 跳转到行首。
$: 跳转到行尾。
2$: 跳转到下一行的行尾。
#:跳转到该行的第#个位置。
#G: 15G,跳转到15行。
:#:跳转到#行。
f'n':跳转到下一个"n"字母后。
ctrl+b: 向后翻一页。
ctrl+f:向前翻一页。
ctrl+u: 向后翻半页。
ctrl+d: 向前翻半页。
ctry+e: 下滚一行。

选择

1.V: 选择一行。
2.^V: 矩形选择。
3.v3w: 选择三个字符。  

编辑

1. 新增:
    i: 光标前插入。
    I: 在当前行首插入。
    a: 光标后插入。
    A: 当前行尾插入。
    O: 在当前行之前插入新行。
    o: 在当前行之后插入新行。
2. 修改 c(change) 为主:
    r: 替换光标所在处的字符。
    R:替换光标所到之处的字符。
    cw: 更改光标所在处的字到字尾处。
    c#w: c3w 修改3个字符。
    C:修改到行尾。
    ci':修改配对标点符号中的文本内容。
    di':删除配对标点符号中的文本内容。
    yi':复制配对标点符号中的文本内容。
    vi':选中配对标点符号中的文本内容。
    s:替换当前一个光标所处字符。
    #S:删除 # 行,并以新文本代替。
3. 删除 d(delete) 为主:
    D:删除到行尾。
    X: 每按一次,删除光标所在位置的前面一个字符。
    x: 每按一次,删除光标所在位置的后面一个字符。
    #x: 删除光标所在位置后面6个字符。
    d^: 删至行首。
    d$: 删至行尾。
    dd:(剪切)删除光标所在行。        
    dw: 删除一个单词/光标之后的单词剩余部分。
    d4w: 删除4个word。
    #dd: 从光标所在行开始删除#行。
    daB: 删除{}及其内的内容。
    diB: 删除{}中的内容。
    n1,n2 d:将n1,n2行之间的内容删除。
4. 查找:
    /: 输入关键字,发现不是要找的,直接在按n,向后查找直到找到为止。
    ?: 输入关键字,发现不是要找的,直接在按n,向前查找直到找到为止。
    *: 在当前页向后查找同一字。
    #: 在当前页向前查找同一字。
5. 复制 y(yank)为主:
    yw: 将光标所在之处到字尾的字符复制到缓冲区中。
    #yw: 复制#个字到缓冲区。
    Y:相当于yy, 复制整行。
    #yy:表示复制从光标所在的该行往下数#行文字。
    p: 粘贴。所有与y相关的操作必用p来结合粘贴。
    n1,n2 co n3:复制第n1行到第n2行之间的内容到第n3行后面。
6. 大小写转换:
    gUU: 将当前行的字母改为大写。
    guu: 将当前行的字母改为小写。
    gUw: 将当前光标下的单词改为大写。
    guw: 将当前光标下的单词改为小写。
    a. 整篇大写:
    ggguG
    gg: 光标到文件第一个字符。
    gu: 把选择范围全部小写。
    G: 到文件结束。
    b. 整篇小写:gggUG
7.  其它:
    J:当前行和下一行合并成一行。
8.  移动:
    n1,n2 m n3:将n1行到n2行之间的内容移至n3行下。

Errors

The Kittn API uses the following error codes:

Error Code Meaning
400 Bad Request – Your request sucks
401 Unauthorized – Your API key is wrong
403 Forbidden – The kitten requested is hidden for administrators only
404 Not Found – The specified kitten could not be found
405 Method Not Allowed – You tried to access a kitten with an invalid method
406 Not Acceptable – You requested a format that isn’t json
410 Gone – The kitten requested has been removed from our servers
418 I’m a teapot
429 Too Many Requests – You’re requesting too many kittens! Slow down!
500 Internal Server Error – We had a problem with our server. Try again later.
503 Service Unavailable – We’re temporarially offline for maintanance. Please try again later.

Golang

The Design

Go 看上去是 C 而不是 C++。我自己在学习和项目使用时,并没有很在意 C 和 C++ 的区别,但看到 Go 时,发现了许多不同。

package foobar

type Bar struct {
    n int
}

func (b *Bar) foo() int {
    return b.n
}

Go 语言在设计中的感觉是追求显式表达,避免隐式表达

class Bar(object):
    n = "a local variable"
    def __init__(self):
        self.n = 0
    def foo(self):
        pass

Python 中也有相似的地方,在类中显式地写出self,对self的操作才是对对象的操作。
但 Go 中向对象增加方法method也要显式地写出对象。

Go 中不支持参数默认值,函数重载。C 也不支持,是 C++ 支持。
参数默认值其实是一种隐式表达。调用者仅看名字,不去查看默认参数时,有时会遇到参数默认值导致问题,给调试带来不少麻烦。

一个表达的意义应该是唯一的,没有二义性。凡是可能导致二义性的行为都应是禁止或尽量避免的。比如函数默认参数,比如类的默认拷贝构造函数。还有一个书写方便但让项目更难调试的功能:自动类型转换。

进一步,一种表达的写法也应尽可能是唯一的。在 Go 语言的写法中,就没有单行 if 语句要不要加括号/braces 这样的问题。if elseelse 的位置也是固定的。另一方面,你可以用无条件的 switch 来更清楚地表达 if-then-else

引用某匿名用户的话:“语法糖好吃不一定有益。”

Object Orientation

Golang 中没有 class。它的面向对象的实现,可以用 struct 来做。

type Record struct {
    Title string
    Dept string
    value int
}

Golang 中变量和函数首字母大写相当于 public, 小写相当于 private,在包和结构体中都是如此。

func (r Record) Publish() { fmt.Println(r.Title) }
func (r *Record) setTitle(s string) { r.Title = s }

Golang 中结构体的方法声明与众不同,是普通函数的方式,并在 func 后加上函数操作的对象,对象加 * 则传指针,不加时传值。

record := &Record{}
record.Title = "New Time"
record2 := &Record{Title: "Two"}
record3 := new(Record)
record3.Title = "Three"
record4 := Record{}
record4.Title = "Four"
record5 := Record{Title: "Five"}

对象实例化。加 &new 时,生成指针对象。一般当对象较小时,传值较好;对象较大时,传指针较好。

func NewRecord(param string, p ...interface{}) *Record
func NewRecord(title string) (r *Record) {
    r = &Record{}
    r.Title = title
    return
}
record := NewRecord("Initialized")

Golang 并没有构造函数,一般也不要用。如果想在初始化时额外做些事的话,要自己写方法来生成实例。

type Record struct {
    Title string
    Dept string
    value int
}
type Archive struct {
    Record
    Dept string
    Location string
}
archive := &Archive{}
archive.Title = "Archive"
archive.Dept = "A-20"
archive.Record.Dept = "Center"
// partial print
&{{ Center } A-20 }

Golang 的继承/组合,cmoposition。在属性冲突时,使用 Archive 中的,而 Record 中的用 archive.Record.Dept 访问。

Golang 不支持重载,重载属于 redeclared 错误,但可以使用不定参数来处理不同参数调用。
func (r *Record) bar(args ...interface{}) { fmt.Println(args) }

接口

TBC

Javascript

Thoughts

Why JavaScript

I use JavaScript to develop products because it seems faster and easier. Use web browsers as JVM, use DOM to build UI, and sue JS to implement business logic.

React

React provides a different way of organize frontend codes. And I like it very much.

Before writing WebApp, my programming practice uses Cpp mostly. So class and components are very handy, and very easy to understand. Different from the noodle of Vanilla JS in complex application, React divides the system to proper components, and makes the data flow in one direction only.

Since more services are moved to Cloud, there are more business-oriented applications which are more complex than client/consumer oriented apps. React can make some real differences in these fields.

Vanilla JS

Some time it seems Vanilla JS is not important, bacause lib like jQuery wrapped up many things. During the early stage of developing an app, you might need to find certain libs to solve the problems you are facing. When you put those codes together, you might encounter problems, and to handle those problems, Vanilla JS is very helpful in locating the problem and finger out what’s wrong.

Another thing is if you need to glue some modules together, sometimes you need to use Vanilla JS to do the job, instead of find another lib to glue modules.

React createClass vs Component

ES6 class might be just a syntactic sugar instead of real class, but things are headed towards this way. I choose to write my code the ES6 way when it’s convenient. As to Component, it’s neater than createClass. No trailing comma, less extra parentheses, and easier to read.

Ref

Introduction

Approaches to Create Objects

let str = 'direct assigned';
let newstr = new String("String instance initialization")
let fn = new Function("arg_name", "console.log(arg_name)");
let obj = new Object();
let foo = {k:'record', v:[1,2,3]}
let bar = new Object();
bar.k = 'record';
function Foo() { this.k = 'record'; this.v = 1234; }
function Bar() {}
Bar.prototype.k = 'record';
Bar.prototype.v = 1234;

Number

Integer

环境 64位 Linux, nodejs v7.2.1

delete operator in JS

Ref

The delete operator removes a property from an object.

delete expression

where expression should evaluate to a property reference.

delete object.property
delete object['property']

Parameters

object The name of an object, or an expression evaluating to an object.

property The property to delete.

N.B., the delete operator has nothing to do with directly freeing memory. Memory management is done indirectly via breaking references.

Array

Array Loop

let arr = [];

for (let i in arr) ;

for (let i of arr) ;

arr.forEach((e) => {});

arr.map((e) => {return e});

arr.every((e) => {return e});

arr.some((e) => {return e});

JS Development Environment

随着前端和 Node.JS 的发展, 以及页面的APP化, JS 成为开发应用的不错的选择. 前后通吃, 语法灵活, 发展迅速, 对提高开发效率很有帮助.

JS 开发发展迅速带来的一个问题是工具和库众多, 开发中选择什么工具成了一个问题. 我从两年前真正开始写JS开始尝试了一些工具和库, 试过之后才知道什么更适合自己.

编辑器

单就文本编辑器而言, 当然还是首推 Emacs 和 Vim. 除这两者之外, 还有许多不错和编辑器和IDE.

打包

打包包括了开发上线所需的预编译, 压缩, 转换, 优化, 构建任务.

现在使用的是 Webpack. 既然 Webpack 有丰富的loader, 方便的 API, 良好的生态环境, 为什么不用它呢?

由于接触JS工程比较晚, 有 Webpack 可用, 对早期的 Grunt 和后来的 Gulp 并不了解. 大家似乎是觉得 Grunt 配置麻烦, 效率不高, 于是转向 Gulp. 还有就是使用 NPM 的构建方式, 我不怎么会用, 感觉和上面三种打包工具不属一类工具. 在自己的工程中尝试使用过 Grunt 和 Gulp, 由于工程较小, 使用起来并没有感觉方便. 在压缩js, 分发文件方面, 比自己写 Python 脚本更复杂.

Grunt 要完成配置之后, 在日常开发中, 通过一个命令就能自动完成文件合并压缩, 上线构建, 在线重载(live-reload). 在项目变得复杂之后, 仅靠配置难以满足需求, 只有增加插件配置项. 而越来越多的配置项使得Grunt的配置变得过于麻烦.

Gulp 则是提供类 Linux 的文件流操作, 以编程的方式组建任务, 来解决越来越多的配置项的问题.


webpack + vue 心得

(ref: wechat 医小生与程序猿 (doctor_programmer))

  1. 要单页面还是多页面。我们的项目比较大,目录又多又深,如果用配合vue-router做成整体的单页面应用,必然会因多级嵌套路由而造成各种困扰。所以最终决定一个功能模块一个页面,各模块自己做成SPA。这个方案也为后面进行多入口打包带来了麻烦。

  2. 公共框架如何处理。 所谓公共框架就是像vue、jquery、bootstrap这的第三方框架。 它们基本不会改动,所以肯定是要单独打成一个包的,充分利用缓存。另外一个问题就是,这些框架的代码放哪里好,有人用bower管理,有人用npm管理,或者是像我们之前那样在项目中放一个lib目录,下载好框架代码后就放进去不动了。不过现在业界普遍都推commonjs规范了,所以用npm来管理是个趋势。

  3. 项目的公用组件如何打包。 既然是用vue,那肯定会写很多公共的组件了,这些组件肯定不能和第三方库打包到一起,也不要和业务代码打包在一起,因为业务代码总改动,而公共组件相对改动也比较少。最好是把所有的组件打成一个包。然而webpack的设计思想并不是这样,它是完全按照模块的依赖树来分析,根据依赖进行打包。我们的组件之间如果没有依赖,是没法打到一起的。

  4. CommonsChunkPlugin的硬伤。 关于公用组件的处理,其实可以用CommonsChunkPlugin这个插件,它能自动分析出公共模块并打成一个包。但是这个插件有个硬伤,如果我写了一个名为popbox的组件,本意是想给项目公用,但是目前只有一个模块用到了它,那么插件并不能知道它是公用插件,而会把它和业务代码打包到一起。倘若将来有第二个模块用到了popbox,就又会把它打进公共包里,这显然是很弱智的行为。

  5. entry只干入口的事。 我一开始想,把公共组件放entry中打包一下行不行,像这样配置:components: ['loading', 'menu', 'box']。结果证明是不能的,文件虽然能打包出来,但是没法用,就是你<script>标签把文件加到页面也不行,别的模块用require无法引用到。原因就是entry打包出的文件作为入口文件,必须包含直接运行的代码。

  6. webpack-stream是个鸡肋。 一开始我被entry和output的各种配置整的摸不着头脑,因为项目目录多,想要更精细的控制,却发现output总是无法按我的需求输出。后来我看到了有webpack-stream这个东西,而且是官方推荐的。简单来讲,它就是实现了文件流接口,从而能与gulp配合工作,我一看这是个好东西啊。研究了一番,发现也没什么功能,就是能用gulp.src代替entry,用gulp.dest代替output,对于多入口打包,就完全没什么用了,还得在webpack中配置,所以简直就是个鸡肋。直接弃用。

  7. 这么多require用哪个。 一开始用webpack构建应用的时候,每当写require的时候我是懵逼的,有commonjs风格的require用法、AMD风格的require加回调用法,以及webpack提供的require.ensure用来打包异步加载的模块。另外还有ES6的import,既然都用vue了,我们肯定得用ES6嘛。这么多引入模块的方法,你得保持头脑清醒了,他们都有什么区别,什么场景下用哪个。

  8. dist目录不是给人看的。 鉴于之前用gulp的习惯,打包后的目录也是有很清晰的结构,而且打包前我们src目录下的代码也是直接可运行的。但是有了webpack之后,首先src下的代码未经打包不能在开发环境运行,其次output选项只能指定一个输出目录,无法再按你的想法再进行组织。唯一有点用的方法是在entry中,把入口文件名字写成这样foo/bar/baz。勉强能在输出目录中新建出文件夹。但是总体来看,dist目录还是一团糟,尤其是异步的chunk文件,只能是id+hash这样的名字。所以我也明白了,要用webpack就别打算去看dist目录了,只能用sourcemap在浏览器看。

Core Features of ES2015 (ES6)

Note on Adrain Mejia’s article

History

ES6, a.k.a. ES2015+, ECMAScript6.

Javascript is evolving since it was created, and ES6 is now well supported in all modern browsers.

(compatibility ref)

ES6 core features

block-scoped variables

We know that var is not block-scoped, but function scoped. In ES6, let and const is used instead of var to limit variable scoped to block. let is the better var A variable must be defined before use when it’s defined by let, and it exists only inside its block, like a for-loop or if-block.

Hoist

var x = 'outer';
function test(inner) {
  if (inner) {
    var x = 'inner';
    return x;
  }
  return x;
}
test(false);
// <- undefined

Using var, even the if clause was not executed, the x inside it was hoisted. But only the declaration was hoisted, not the initialization.

Temporal Dead-zone

In ES6, let hoists a variable to the top of the block, and from the top on the block to the let declaration is a “temporal dead zone”, where a variable is not usable (ReferenceError).

No more IIFE

We all familiar with IIFE (Immediately-Invoked Function Expression), and write it everyday. Because without IIFE, the function names and variables will pollute everywhere.

(function() {
  var bar = 'ES5';
  function foo() {}
}());

With let grammar, IIFE is not essential anymore.

{
  let bar = 'ES6';
  let foo = () => {};
}

const

When a re-assignation is necessary, ust let. When the value of a variable should not be changable, use const.

const _version = '0.1';

Text Template

// ES5
var firstName = 'Adrian';
var lastName = 'Mejia';
console.log('Your name is ' + firstName + ' ' + lastName + '.');

Now ` and ${} is available.

// ES6
const firstName = 'Adrian';
const lastName = 'Mejia';
console.log(`Your name is ${firstName} ${lastName}`);

Multi-line String

// ES5
var template = '<li *ngFor="let todo of todos" [ngClass]="{completed: todo.isDone}" >n' +
'  <div class="view">n' +
'    <input class="toggle" type="checkbox" [checked]="todo.isDone">n' +
'    <label></label>n' +
'    <button class="destroy"></button>n' +
'  </div>n' +
'  <input class="edit" value="">n' +
'</li>';
console.log(template);

Just using `, you can write multiple string directly.

// ES6
const template = `<li *ngFor="let todo of todos" [ngClass]="{completed: todo.isDone}" >
  <div class="view">
    <input class="toggle" type="checkbox" [checked]="todo.isDone">
    <label></label>
    <button class="destroy"></button>
  </div>
  <input class="edit" value="">
</li>`;
console.log(template);

Deconstruction and Assignment

Get array elements.

// ES5
var array = [1, 2, 3, 4];
var first = array[0];
var third = array[2];
console.log(first, third); // 1 3
// ES6
const array = [1, 2, 3, 4];
const [first, ,third] = array;
console.log(first, third); // 1 3

Switch values.

// ES5
var a = 1;
var b = 2;
var tmp = a;
a = b;
b = tmp;
console.log(a, b); // 2 1
// ES6
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1

Return multiple value

// ES5
function margin() {
  var left=1, right=2, top=3, bottom=4;
  return { left: left, right: right, top: top, bottom: bottom };
}
var data = margin();
var left = data.left;
var bottom = data.bottom;
console.log(left, bottom); // 1 4
// ES6
function margin() {
  const left=1, right=2, top=3, bottom=4;
  return { left, right, top, bottom }; // grammar sugar {left:left} -> {left}
}
const { left, bottom } = margin();
console.log(left, bottom); // 1 4

Parameter match

// ES5
var user = {firstName: 'Adrian', lastName: 'Mejia'};
function getFullName(user) {
  var firstName = user.firstName;
  var lastName = user.lastName;
  return firstName + ' ' + lastName;
}
console.log(getFullName(user)); // Adrian Mejia
// ES6
const user = {firstName: 'Adrian', lastName: 'Mejia'};
function getFullName({ firstName, lastName }) {
  return `${firstName} ${lastName}`;
}
console.log(getFullName(user)); // Adrian Mejia

Deep match

// ES5
function settings() {
  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };
}
var tmp = settings();
var displayColor = tmp.display.color;
var keyboardLayout = tmp.keyboard.layout;
console.log(displayColor, keyboardLayout); // red querty
// ES6
function settings() {
  return { display: { color: 'red' }, keyboard: { layout: 'querty'} };
}
const { display: { color: displayColor }, keyboard: { layout: keyboardLayout }} = settings();
console.log(displayColor, keyboardLayout); // red querty

Deconstruction is useful, and helpful in forming a good coding style. Use array deconstrunction to avoid temporary variables. Use Object deconstrunction to deal with multiple return values.

Class and Object

// ES5
var Animal = (function () {
  function MyConstructor(name) {
    this.name = name;
  }
  MyConstructor.prototype.speak = function speak() {
    console.log(this.name + ' makes a noise.');
  };
  return MyConstructor;
})();
var animal = new Animal('animal');
animal.speak(); // animal makes a noise.

ES6 provides grammar sugar to define a class object. With class, constructor, you can define speak() instead of constructor.prototype.speak = function(). Use class and avoid deal with prototype. The same functionality, more elegant.

// ES6
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}
const animal = new Animal('animal');
animal.speak(); // animal makes a noise.

Avoid empty constructor. A class has a default constructor itself.

Inherit

Inheritance in ES5 is complex.

// ES5
var Lion = (function () {
  function MyConstructor(name){
    Animal.call(this, name);
  }
  // prototypal inheritance
  MyConstructor.prototype = Object.create(Animal.prototype);
  MyConstructor.prototype.constructor = Animal;
  MyConstructor.prototype.speak = function speak() {
    Animal.prototype.speak.call(this);
    console.log(this.name + ' roars ');
  };
  return MyConstructor;
})();
var lion = new Lion('Simba');
lion.speak(); // Simba makes a noise.
// Simba roars.
// ES6
class Lion extends Animal {
  speak() {
    super.speak();
    console.log(this.name + ' roars ');
  }
}
const lion = new Lion('Simba');
lion.speak(); // Simba makes a noise.
// Simba roars.

ES6 has extends and super, makes inheritance much clearer.

Promise

promise is used to eliminate the callback hell.

// ES5
function printAfterTimeout(string, timeout, done){
  setTimeout(function(){
    done(string);
  }, timeout);
}
printAfterTimeout('Hello ', 2e3, function(result){
  console.log(result);
  // nested callback
  printAfterTimeout(result + 'Reader', 2e3, function(result){
    console.log(result);
  });
});

This function exec a callback after done(), twice. More layer of callbacks, it will become a mess. With promise:

// ES6
function printAfterTimeout(string, timeout){
  return new Promise((resolve, reject) => {
    setTimeout(function(){
      resolve(string);
    }, timeout);
  });
}
printAfterTimeout('Hello ', 2e3).then((result) => {
  console.log(result);
  return printAfterTimeout(result + 'Reader', 2e3);
}).then((result) => {
  console.log(result);
});

Use promise, another function can be called by then after a function is executed without nesting.

Arrow Function

In ES5, this point to the environment in which a function is executed, instead of in which it is defined.

// ES5
var _this = this; // need to hold a reference
$('.btn').click(function(event){
  _this.sendData(); // reference outer this
});
$('.input').on('change',function(event){
  this.sendData(); // reference outer this
}.bind(this)); // bind to outer this

With ()=>{}, you don’t need to use a temporary variable to hold this or use bind.

// ES6
// this will reference the outer one
$('.btn').click((event) =>  this.sendData());
// implicit returns
const ids = [291, 288, 984];
const messages = ids.map(value => `ID is ${value}`);

for … of

default value of function arguments

Just like in C, Python, and any other languages.

… remaining arguments

A argumnets like operation.

function printf(format, ...params) {
  console.log('params: ', params);
  console.log('format: ', format);
}
printf('%s %d %.2f', 'adrian', 321, Math.PI);

open/spread operator (??? which terminology)

In ES5, apply()

Math.max(2,100,1,6,43) // <- 100
Math.max([2,100,1,6,43]) // <- NaN
Math.max.apply(Math, [2,100,1,6,43]) // <- 100

In ES6, just spread the array.

Math.max(...[2,100,1,6,43]) // 100

In ES6, concat() arrays.

var array1 = [2,100,1,6,43];
var array2 = ['a', 'b', 'c', 'd'];
var array3 = [false, true, null, undefined];
console.log(array1.concat(array2, array3));

ES6 spreads nested arrays.

// ES6
const array1 = [2,100,1,6,43];
const array2 = ['a', 'b', 'c', 'd'];
const array3 = [false, true, null, undefined];
console.log([...array1, ...array2, ...array3]);

Note on Express Basic

Routing

A route method is derived from one of the HTTP methods, and is attached to an instance of the express class.

Route methods

Express supports the following routing methods that correspond to HTTP methods: get, post, put, head, delete, options, trace, copy, lock, mkcol, move, purge, propfind, proppatch, unlock, report, mkactivity, checkout, merge, m-search, notify, subscribe, unsubscribe, patch, search, and connect.

To route methods that translate to invalid JavaScript variable names, use the bracket notation. app['m-search']('/', function(){})

An extra routing method app.all() is used for loading middleware functions at a path for all request methods.

app.get('/', function(req, res) {
})
app.post('/', function(req, res) {
})
app.all('/', function(req, res, next) {
    ;
    next()
})

Route paths

Route path supports string, string pattern with *? + * ()*, and regular expression. In string, hyphen and dot are literal characters.

app.get('/ab*c(de)?f', function(req, res) {})
app.get(/.*foo$/, function(req, res) {})

Route parameters

Express captures named URL segments into req.params object. > The name of route parameters must be made up of “word characters” ([A-Za-z0-9_]).

Route path: /users/:userId/books/:bookId
Request URL: http://localhost:3000/users/34/books/8989
req.params: { "userId": "34", "bookId": "8989" }

Route path: /flights/:from-:to
Request URL: http://localhost:3000/flights/LAX-SFO
req.params: { "from": "LAX", "to": "SFO" }

Route path: /plantae/:genus.:species
Request URL: http://localhost:3000/plantae/Prunus.persica
req.params: { "genus": "Prunus", "species": "persica" }

Route handlers

Route handlers are used to handle a request, and they’re chained by next(). You can use functions, and arrays of functions, and their mix.

var cb0 = function (req, res, next) {
  console.log('CB0')
  next()
}

var cb1 = function (req, res, next) {
  console.log('CB1')
  next()
}

app.get('/example', [cb0, cb1], function (req, res, next) {
  console.log('the response will be sent by the next function ...')
  next()
}, function (req, res) {
  res.send('Hello from D!')
})

Response methods

A response method sends a response to the client, and terminates the request-response cycle. If no response method is called, the client request will be left hanging.

- **Method**    **Description**
- res.download()    Prompt a file to be downloaded.
- res.end()         End the response process.
- res.json()        Send a JSON response.
- res.jsonp()       Send a JSON response with JSONP support.
- res.redirect()    Redirect a request.
- res.render()      Render a view template.
- res.send()        Send a response of various types.
- res.sendFile()    Send a file as an octet stream.
- res.sendStatus()  Set the response status code and send its string representation as the response body.

app.route()

It creates a handler-chain for a given route path.

app.route('/book')
  .get(function (req, res) {
    res.send('Get a random book')
  })
  .post(function (req, res) {
    res.send('Add a book')
  })
  .put(function (req, res) {
    res.send('Update the book')
  })

express.Router

Use the express.Router class to create modular, mountable route handlers. A Router instance is a complete middleware and routing system; for this reason, it is often referred to as a “mini-app”.

The following example creates a router as a module, loads a middleware function in it, defines some routes, and mounts the router module on a path in the main app.

Create a router file named birds.js in the app directory, with the following content:

var express = require('express')
var router = express.Router()

// middleware that is specific to this router
router.use(function timeLog (req, res, next) {
  console.log('Time: ', Date.now())
  next()
})
// define the home page route
router.get('/', function (req, res) {
  res.send('Birds home page')
})
// define the about route
router.get('/about', function (req, res) {
  res.send('About birds')
})

module.exports = router

Then, load the router module in the app:

var birds = require('./birds')

// ...

app.use('/birds', birds)

The app will now be able to handle requests to /birds and /birds/about, as well as call the timeLog middleware function that is specific to the route.


MySQL in NodeJS

1. 建立数据库连接 createConnection(Object)

该方法接受一个对象作为参数,该对象有四个常用的属性host,user,password,database。与php中链接数据库的参数相同。属性列表如下:

host: 连接数据库所在的主机名. (默认: localhost)
port: 连接端口. (默认: 3306)
localAddress: 用于TCP连接的IP地址. (可选)
socketPath: 链接到unix域的路径在使用host和port时该参数会被忽略.
user: MySQL用户的用户名.
password: MySQL用户的密码.
database: 链接到的数据库名称 (可选).
charset: 连接的字符集. (默认: 'UTF8_GENERAL_CI'.设置该值要使用大写!)
timezone: 储存本地时间的时区. (默认: 'local')
stringifyObjects: 是否序列化对象. See issue #501. (默认: 'false')
insecureAuth: 是否允许旧的身份验证方法连接到数据库实例. (默认: false)
typeCast: 确定是否讲column值转换为本地JavaScript类型列值. (默认: true)
queryFormat: 自定义的查询语句格式化函数.
supportBigNumbers: 数据库处理大数字(长整型和含小数),时应该启用 (默认: false).
bigNumberStrings: 启用 supportBigNumbers和bigNumberStrings 并强制这些数字以字符串的方式返回(默认: false). 
dateStrings: 强制日期类型(TIMESTAMP, DATETIME, DATE)以字符串返回而不是一javascript Date对象返回. (默认: false)
debug: 是否开启调试. (默认: false)
multipleStatements: 是否允许在一个query中传递多个查询语句. (Default: false)
flags: 链接标志.
// 还可以使用字符串连接数据库
var connection = mysql.createConnection('mysql://user:pass@host/db?debug=true&charset=BIG5_CHINESE_CI&timezone=-0700');

2 结束数据库连接 end() destroy()

end()接受一个回调函数,并且会在query结束之后才触发,如果query出错,仍然会终止链接,错误会传递到回调函数中处理。

destroy()立即终止数据库连接,即使还有query没有完成,之后的回调函数也不会在触发。

3. 创建连接池 createPool(Object)

Object和createConnection参数相同 可以监听connection事件,并设置session值

pool.on('connection', function(connection) {  
    connection.query('SET SESSION auto_increment_increment=1')  
});

connection.release()释放链接到连接池。如果需要关闭连接并且删除,需要使用connection.destroy()
pool除了接受和connection相同的参数外,还接受几个扩展的参数

createConnection: 用于创建链接的函数. (Default: mysql.createConnection)  
waitForConnections: 决定当没有连接池或者链接数打到最大值时pool的行为. 为true时链接会被放入队列中在可用是调用为false时会立即返回error. (Default: true)  
connectionLimit: 最大连接数. (Default: 10)  
queueLimit: 连接池中连接请求的烈的最大长度超过这个长度就会报错值为0时没有限制. (Default: 0)  

4. 连接池集群

允许不同的host链接

// create
var poolCluster = mysql.createPoolCluster();

poolCluster.add(config); // anonymous group
poolCluster.add('MASTER', masterConfig);
poolCluster.add('SLAVE1', slave1Config);
poolCluster.add('SLAVE2', slave2Config);

// Target Group : ALL(anonymous, MASTER, SLAVE1-2), Selector : round-robin(default)
poolCluster.getConnection(function (err, connection) {});

// Target Group : MASTER, Selector : round-robin
poolCluster.getConnection('MASTER', function (err, connection) {});

// Target Group : SLAVE1-2, Selector : order
// If can't connect to SLAVE1, return SLAVE2. (remove SLAVE1 in the cluster)
poolCluster.on('remove', function (nodeId) {
console.log('REMOVED NODE : ' + nodeId); // nodeId = SLAVE1 
});

poolCluster.getConnection('SLAVE*', 'ORDER', function (err, connection) {});

// of namespace : of(pattern, selector)
poolCluster.of('*').getConnection(function (err, connection) {});

var pool = poolCluster.of('SLAVE*', 'RANDOM');
pool.getConnection(function (err, connection) {});
pool.getConnection(function (err, connection) {});

// destroy
poolCluster.end();

链接集群的可选参数

canRetry: 值为true时允许连接失败时重试(Default: true)  
removeNodeErrorCount: 当连接失败时 errorCount 值会增加. 当errorCount 值大于 removeNodeErrorCount 将会从PoolCluster中删除一个节点. (Default: 5)  
defaultSelector: 默认选择器. (Default: RR)  
RR: 循环. (Round-Robin)  
RANDOM: 通过随机函数选择节点.  
ORDER: 无条件地选择第一个可用节点.  

5. 切换用户 / 改变连接状态

Mysql允许在比断开连接的的情况下切换用户

connection.changeUser({user : 'john'}, function(err) {  
    if (err) throw err;  
});  
// 参数  
user: 新的用户 (默认为早前的一个).  
password: 新用户的新密码 (默认为早前的一个).  
charset: 新字符集 (默认为早前的一个).  
database: 新数据库名称 (默认为早前的一个).  

6. 处理服务器连接断开

var db_config = {  
        host: 'localhost',  
        user: 'root',  
        password: '',  
        database: 'example'  
    };  
  
var connection;  

function handleDisconnect() {  
  connection = mysql.createConnection(db_config); // Recreate the connection, since  
                                                  // the old one cannot be reused.  

  connection.connect(function(err) {              // The server is either down  
    if(err) {                                     // or restarting (takes a while sometimes).  
      console.log('error when connecting to db:', err);  
      setTimeout(handleDisconnect, 2000); // We introduce a delay before attempting to reconnect,  
    }                                     // to avoid a hot loop, and to allow our node script to  
  });                                     // process asynchronous requests in the meantime.  
                                          // If you're also serving http, display a 503 error.  
  connection.on('error', function(err) {  
    console.log('db error', err);  
    if(err.code === 'PROTOCOL_CONNECTION_LOST') { // Connection to the MySQL server is usually  
      handleDisconnect();                         // lost due to either server restart, or a  
    } else {                                      // connnection idle timeout (the wait_timeout  
      throw err;                                  // server variable configures this)  
    }  
  });  
}  

handleDisconnect();  

7. 转义查询值

为了避免SQL注入攻击,需要转义用户提交的数据。可以使用connection.escape() 或者 pool.escape()

var userId = 'some user provided value';  
var sql    = 'SELECT * FROM users WHERE id = ' + connection.escape(userId);  
connection.query(sql, function(err, results) {  
  // ...  
});  
或者使用作为占位符  
connection.query('SELECT * FROM users WHERE id = ?', [userId], function(err, results) {  
  // ...  
});  
不同类型值的转换结果  
Numbers 不变  
Booleans 转换为字符串 'true' / 'false'   
Date 对象转换为字符串 'YYYY-mm-dd HH:ii:ss'  
Buffers 转换为是6进制字符串  
Strings 不变  
Arrays => ['a', 'b'] 转换为 'a', 'b'  
嵌套数组 [['a', 'b'], ['c', 'd']] 转换为 ('a', 'b'), ('c', 'd')  
Objects 转换为 key = 'val' pairs. 嵌套对象转换为字符串.  
undefined / null ===> NULL  
NaN / Infinity 不变. MySQL 不支持这些值,  除非有工具支持,否则插入这些值会引起错误.  
转换实例  
var post  = {id: 1, title: 'Hello MySQL'};  
var query = connection.query('INSERT INTO posts SET ?', post, function(err, result) {  
  // Neat!  
});  
console.log(query.sql); // INSERT INTO posts SET `id` = 1, `title` = 'Hello MySQL'  
或者手动转换  
var query = "SELECT * FROM posts WHERE title=" + mysql.escape("Hello MySQL");  

console.log(query); // SELECT * FROM posts WHERE title='Hello MySQL'  

8. 转换查询标识符

如果不能信任SQL标识符(数据库名、表名、列名),可以使用转换方法mysql.escapeId(identifier);

var sorter = 'date';  
var query = 'SELECT * FROM posts ORDER BY ' + mysql.escapeId(sorter);  

console.log(query); // SELECT * FROM posts ORDER BY `date`  
支持转义多个  
var sorter = 'date';  
var query = 'SELECT * FROM posts ORDER BY ' + mysql.escapeId('posts.' + sorter);  

console.log(query); // SELECT * FROM posts ORDER BY `posts`.`date`  
可以使用??作为标识符的占位符  
var userId = 1;  
var columns = ['username', 'email'];  
var query = connection.query('SELECT ?? FROM ?? WHERE id = ?', [columns, 'users', userId], function(err, results) {  
  // ...  
});  
  
console.log(query.sql); // SELECT `username`, `email` FROM `users` WHERE id = 1  

9. 准备查询

可以使用mysql.format来准备查询语句,该函数会自动的选择合适的方法转义参数。

var sql = "SELECT * FROM ?? WHERE ?? = ?";  
var inserts = ['users', 'id', userId];  
sql = mysql.format(sql, inserts);  
10自定义格式化函数  
connection.config.queryFormat = function (query, values) {  
  if (!values) return query;  
  return query.replace(/\:(\w+)/g, function (txt, key) {  
    if (values.hasOwnProperty(key)) {  
      return this.escape(values[key]);  
    }  
    return txt;  
  }.bind(this));  
};  

connection.query("UPDATE posts SET title = :title", { title: "Hello MySQL" });  

11. 获取插入行的id

当使用自增主键时获取插入行id,如:

connection.query('INSERT INTO posts SET ?', {title: 'test'}, function(err, result) {  
  if (err) throw err;  

  console.log(result.insertId);  
});  

12. 流处理

有时你希望选择大量的行并且希望在数据到达时就处理他们,你就可以使用这个方法

var query = connection.query('SELECT * FROM posts');  
query  
    .on('error', function(err) {  
      // Handle error, an 'end' event will be emitted after this as well  
    })  
    .on('fields', function(fields) {  
      // the field packets for the rows to follow  
    })  
    .on('result', function(row) {  
      // Pausing the connnection is useful if your processing involves I/O  
      connection.pause();  
  
      processRow(row, function() {  
        connection.resume();  
      });  
    })  
    .on('end', function() {  
      // all rows have been received  
    });  

13. 多语句/混合查询

因为混合查询容易被SQL注入攻击,默认是不允许的,可以使用var connection = mysql.createConnection({multipleStatements: true});开启该功能。 混合查询实例:

connection.query('SELECT 1; SELECT 2', function(err, results) {  
  if (err) throw err;  

  // `results` is an array with one element for every statement in the query:  
  console.log(results[0]); // [{1: 1}]  
  console.log(results[1]); // [{2: 2}]  
});  

// 同样可以使用流处理混合查询结果:
var query = connection.query('SELECT 1; SELECT 2');  
query  
    .on('fields', function(fields, index) {  
      // the fields for the result rows that follow  
    })  
    .on('result', function(row, index) {  
      // index refers to the statement this result belongs to (starts at 0)  
    });  

如果其中一个查询语句出错,Error对象会包含err.index指示错误语句的id,整个查询也会终止。 混合查询结果的流处理方式是做实验性的,不稳定。

14. 事务处理

connection级别的简单事务处理

connection.beginTransaction(function(err) {  
  if (err) { throw err; }  
  connection.query('INSERT INTO posts SET title=?', title, function(err, result) {  
    if (err) {   
      connection.rollback(function() {  
        throw err;  
      });  
    }  

    var log = 'Post ' + result.insertId + ' added';  

    connection.query('INSERT INTO log SET data=?', log, function(err, result) {  
      if (err) {   
        connection.rollback(function() {  
          throw err;  
        });  
      }    
      connection.commit(function(err) {  
        if (err) {   
          connection.rollback(function() {  
            throw err;  
          });  
        }  
        console.log('success!');  
      });  
    });  
  });  
});  

15. 错误处理

err.code = string  
err.fatal => boolean  

Object-Oriented Programming of Javascript

notes of 1 2 3

JS 的对象

JS 本身是基于对象的语言,在 JS 中所操作的基本都是对象。但在 ES2015 之前它又没有 class 的类。

如果要从原型对象生成实例对象,要把属性和方法封装到对象里。properties and methods

let record = { name: "", nation: "" };

Schema 规格

let record1 = { name: "Alpha", nation: "China" };
let record2 = { name: "Beta, nation: "Denmark" };

给对象赋予属性和方法

这可以看作最简单的封装,但原型和实例间并不存在内在联系。

function Record(name, nation) {
    return {
        name: name,
        nation: nation,
    };
}
let record1 = Record("Alpha", "China");
let record2 = Record("Beta", "Denmark");

用函数调用返回对象方式可以在原型和实例间建立一定联系,但两个实例间没有内在联系。

function Record(name, nation) {
    this.name = name;
    this.nation = nation;
}
let record1 = new Record("Alpha", "China");
let record2 = new Record("Beta", "Denmark");

Javascript 提供了 Constructor 构造函数模式。构造函数是使用了 this 变量的普通函数,在使用 new 运算符生成实例时,this 会绑定到对象实例上,使 foo.constructor == Foo。JS 中的 instanceof 运算符可以验证原型与实例对象间的关系,(foo instanceof Foo) == true

function Record(name, nation) { this.name = name; this.nation = nation; }
Record.prototype.species = "human";
Record.prototype.greet = function() { console.log("hello") };
let record1 = new Record("Alpha", "China"); // with `species` and `greet()`
let record2 = new Record("Beta", "Denmark");

构造函数模式的问题在于原型中的属性和方法在每个实例中都要创建。其中重复的内容就额外地占用的资源。JS 中每个构造函数都有 prototype 属性,指向另一个对象;所指向对象的属性和方法都被构造函数继承。

这样,不变的属性和方法可以直接定义在 prototype 对象上。在例子中,所有实例的 species 和 greet() 方法都指向相同的地址,即 prototype 对象。

JS 中有一些对应 prototype 模式的方法

继承对象

function Record() { this.title = "Record" }
function Personnel(name, dept) { this.name = name; this.dept = dept; }

对象间继承的方法。

// YUI extend function
function extend(Child, Parent) {
    let F = function() {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
}
extend(Personnel, Record);

可以对这个方式进行封装。如 YUI 库实现继承的方法。

function Record() {}
Record.prototype.title = "Record";
function extend(Child, Parent) 
    for (let i in Parent.prototype)
        Child.prototype[i] = Parent.prototype[i];
    Child.prototype.uber = Parent.prototype;
}

一般对象继承

无构造函数的普通对象的继承。 let Record = {title: "Record"};

function object(obj) {
    function F() {}
    F.prototype = obj;
    return new F();
}
let Personnel = object(Record);
function extend(obj) {
    let c = {};
    for (let i in obj)
        c[i] = obj[i];
    c.uber = obj;
    return c;
}
let Personnel = extend(Record);

浅复制在父对象的成员也是对象时,会把对象地址复制给子类,造成子类可以修改父类。

function deepCopy(Parent, Child) {
    let Child = Child || {};
    for (let i in Parent) {
        if (typeof Parent[i] === "object") {
            Child[i] = (Parent[i].constructor === Array) ? [] : {};
            deepCopy(Parent[i], Child[i]);
        } else {
            Child[i] = Parent[i];
        }
    }
    return Child;
}

Practice of JavaScript Performance

Since JavaScript becomes a fullstack programming language, in certain cases the performance of JS is critical. Here are some practice of JS, with some understanding of JS implementation.

Data Store

How a variable is create will effect its performance. - Use {} instead of new Object, use [] instead of new Array, use plain string instead of an object. - Use local temporary variable to store variable deep upon the data-chain, and values require many calculation. - dot-present has a similar performance as member operator, and it’s faster in Safari. object.name vs object[name], object.name is always faster.

Loop

In Chromium v54, for-in is slower, and for (var i = 0; i < length; ++i), [].forEach(()=>{}) have similar performance. forEach is much faster than it was in earlier browser.

Event Delegate

It will cause too much loads and occupy too many memory binding the same event to massive elements in the page. It better to bind event to the outer layer, and deal with all events of its child elements.

document.getElementById('content').addEventListener('click', function(evt) {
    evt = evt || window.event;
    let target = evt.target || evt.srcElement;
    if (target.nodeName.toLowerCase() !== 'a')
        return;
    console.log(target.href);
});

Re-Render

The source files create 2 trees in browser: DOM tree and Render tree. Render tree deal with DOM geometry attributions. When the style of DOM changes, browser re-render the DOM.

bodystyle = document.body.style; 
bodystyle.color = red; 
bodystyle.height = 1000px; 
bodystyke.width = 100%;

Three times of modification will cause 3 re-renders. Under certain circumstances, reduce the times of modification will improve rendering performance.

bodystyle = document.body.style; 
bodystyle.cssText 'color:red;height:1000px;width:100%';

JavaScript Load

<script> tag might block something from downloading, so it’s a good practice to put more script tag at the bottom of DOM, to avoid the time of empty page displayed to users.

JavaScript Deployment

Reduce the number of requirements and the file sizes will accelerate the page loading speed. For example, use webpack and uglify.

Cache JS Files

Cached files will improve the speed of page loading. Web servers use “Expires HTTP Resonse Header” to tell browser how long a resource should be cached. To avoid the problem of using old version of file, you can change the filename of static resource, like bundle-201612242359.js, using a timestamp to force browser to load the new js file.

An Introduction to Webpack

Webpack is a bundler for javascript, and related resources. All resources are treated as modules, and with various loaders, webpack can handle modules of CommonJs, AMD, ES6, CSS, Image, JSON, Coffeescript, LESS, etc.

// what a webpack config file 'webpack.config.js' looks like
module: {
  loaders: [
    { test: /\.css$/, loader: 'style!css' }, // use ! to chain loaders
    { test: /\.js$/, loader: 'jsx?harmony!babel', exclude: /node_modules/ }
    // loaders can be written like 'jsx-loader' or 'jsx'
    // loaders can take parameters as a querystring
  ]
}

Entrance and output.

// webpack.config.js
module.exports = {
  entry: './entry.js',
  output: {
    filename: 'bundle.js'
  }
};

Filename extensions.

// webpack.config.js
module.exports = {
  entry: 'entry.js',
  output: {
    filename: 'bundle.js'
  },
  resolve: {
    extensions: ['', '.js]
    // require('./foo') will search module with filename './foo' and './foo.js'
  }
}

require a module

require('foo') // from module directory
require('./bar') // from relative path

define (require, exports.module) ->
  require('foo')
  require('./bar)

module shim

To require modules from window, you might need expose-loader. Take jQuery for example

{ test: require.resolve('jquery'), loader: 'expose?jQuery' }

CSS and Image

Write loader and require them as normal modules.

// webpack.config.js
module.exports = {
  entry: 'entry.js',
  output: {
    path: './build', // for output processed images and resources
    publicPath: 'http://mycdn.com/', // for generate URLs to resources
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      { test: /\.js$/, loader: 'jsx?harmony!babel', exclude: /node_modules/ }
      { test: /\.css$/, loader: 'style!css },
      { test: /\.less$/, loader: 'style!css!less },
      { test: /\.(png|jpg)$/, loader: 'url?limit=8192' }
      // inline base64 URLs for <=8k images, direct URLs for the rest
    ]
  }
};
require('./bootstrap.css');
require('./style.less');

let img = document.createElement('img');
img.src = require('./logo.png');

Multi-package and shared package

Shared packages are handled by commonsPlugin.

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3"
    },
    output: {
        filename: "[name].entry.chunk.js"
    },
    plugins: [
        new CommonsChunkPlugin("commons.chunk.js")
    ]
}

Separated CSS package

Hash js files

One hash for the bundle webpack ./entry output.[hash].bundle.js

{
    output: {
        path: path.join(__dirname, "assets", "[hash]"),
        publicPath: "assets/[hash]/",
        filename: "output.[hash].bundle.js",
        chunkFilename: "[id].[hash].bundle.js"
    }
}

One hash per chunk --output-chunk-file [chunkhash].js

output: { chunkFilename: "[chunkhash].bundle.js" }

Get filenames from stats

plugins: [
  function() {
    this.plugin("done", function(stats) {
      require("fs").writeFileSync(
        path.join(__dirname, "..", "stats.json"),
        JSON.stringify(stats.toJson()));
    });
  }
]

Options for Product

You can specify a different config file for product-level bundling. webpack --config webpack.min.js

Compress the code. Loaders are switched into minimizing mode. Uglify

plugins: [
   new webpack.optimize.MinChunkSizePlugin(minSize)
]
new webpack.optimize.UglifyJsPlugin([options])
// e.g.
new webpack.optimize.UglifyJsPlugin({
    compress: {
        warnings: false
    }
})

You can/should use normal npm React package instead of the precompiled library. Webpack can compress it to smaller size than react.min.js.

new webpack.DefinePlugin({
  "process.env": {
    NODE_ENV: JSON.stringify("production")
  }
})

Hot Replace

Include webpack/hot/dev-server in your project code, edit config file.

  entry: {
    main: ['webpack/hot/dev-server', './entry],
  },

Then start the server, and visit it at [http://localhost:8080/webpack-dev-server/].

webpack-dev-server --hot --quiet

And there is a react-hot-loader.

entry: [
  'webpack-dev-server/client?http://0.0.0.0:8080', // WebpackDevServer host and port
  'webpack/hot/only-dev-server',
  './scripts/index' // Your appʼs entry point
]

As for only-dev-server, the issue is replied here.

Miscellaneous

When webpack reports an error, but you cannot find out what’s wrong, try this.

webpack --display-error-details

[Webpack list of loaders] http://webpack.github.io/docs/list-of-loaders.html optimization [long term caching]http://webpack.github.io/docs/long-term-caching.html

Another Webpack Note

其实Webpack的入门指导文章非常多,配置方式也各有各样,这里我推荐题叶大神的入门级指南–Webpack 入门指迷,如果不知道Webpack是什么或者不是很清楚各项配置含义的开发者,可以看此文章扫扫盲。毕竟我这篇文章并不是特别基础。

1. base.js

var path = require(‘path’) var baseConfig = { resolve: { extensions: [“, ‘.js’], fallback: [path.join(dirname, ‘../node_modules’)], alias: { ‘src’: path.resolve(dirname, ‘../src’), ‘assets’: path.resolve(dirname, ‘../src/assets’), ‘components’: path.resolve(dirname, ‘../src/components’) } }, module: { loaders: [{ test: /.js$/, loader: ‘babel’, exclude: /node_modules/ }, { test: /.(png|jpe?g|gif|svg|woff2?|eot|ttf|otf)(\?.*)?$/, loader: ‘url?limit=8192&context=client&name=[path][name].[hash:7].[ext]’ }, { test: /.css$/, loader: ‘style!css!autoprefixer’, }, { test: /.scss$/, loader: ‘style!css!autoprefixer!sass’ }] } };

module.exports = baseConfig;

` 解读下这个基本配置:

1、resolve 解析模块依赖的时候,受影响的配置项。

extensions 决定了哪些文件后缀在引用的时候可以省略点,Webpack帮助你补全名称。 fallback 当webpack在 root(默认当前文件夹,配置时要绝对路径) 和 modulesDirectories(默认当前文件夹,相对路径)配置下面找不到相关modules,去哪个文件夹下找modules alias 这个大家应该比较熟悉,requirejs之类的都有,就是别名,帮助你快速指向文件路径,少写不少代码,而且不用关心层级关系,需要注意的是:在scss之类的css预编译中引用要加上~,以便于让loader识别是别名引用路径。

2、module 解析不同文件使用哪些loader,这个比较简单,很多文章都有,就不多说了,注意的是,这里的scss可以换成你自己的预编译器,例如:sass、less、stylus等,或者直接用postcss都行,当然还可以用一种通用方法,后面补上。 `

2. 开发环境配置 config

var webpack = require(‘webpack’); var path = require(‘path’) var merge = require(‘webpack-merge’) var baseConfig = require(‘./webpack.base’) var getEntries = require(‘./getEntries’)

var hotMiddlewareScript = ‘webpack-hot-middleware/client?reload=true’;

var assetsInsert = require(‘./assetsInsert’)

module.exports = merge(baseConfig, { entry: getEntries(hotMiddlewareScript), devtool: ‘#eval-source-map’, output: { filename: ‘./[name].[hash].js’, path: path.resolve(‘./dist’), publicPath:‘./dist’ }, plugins: [ new webpack.DefinePlugin({ ‘process.env’: { NODE_ENV: ‘“development”’ } }), new webpack.optimize.OccurenceOrderPlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin(), new assetsInsert() ] })

` 说说这个配置中的一些难点:

1、getEntries 是用来配置入口文件,一般很多人是自己手写,或者SPA页面,只有一个入口, 很容易就写出来,但是公司中,很多情况,是需要多入口,也就是多路由的Url,这个时候入口的配置就比较麻烦,我这里是放单独一个文件里面配置,我们公司是靠规定来执行,也就是一个文件夹所有的main.js都认为是入口文件,其他都忽略。 `

function getEntry(hotMiddlewareScript) { var pattern = paths.dev.js + ‘project/**/main.js’; var array = glob.sync(pattern); var newObj = {};

array.map(function(el){
    var reg = new RegExp('project/(.*)/main.js','g');
    reg.test(el);
    if (hotMiddlewareScript) {
        newObj[RegExp.$1] = [el, hotMiddlewareScript];
    } else {
        newObj[RegExp.$1] = el;
    }
});
return newObj;

}

2、assetsInsert 是用来做模板替换的,一个小插件把template里面的值替换成打包后的css或者js。

3. 打包环境配置 production

var webpack = require(‘webpack’); var path = require(‘path’) var merge = require(‘webpack-merge’) var baseConfig = require(‘./webpack.base’) var getEntries = require(‘./getEntries’) var ExtractTextPlugin = require(‘extract-text-webpack-plugin’); var assetsInsert = require(‘./assetsInsert’)

var productionConf = merge(baseConfig, { entry: getEntries(), output: { filename: ‘./[name].[hash].js’, path: path.resolve(‘./public/dist’), publicPath: ‘./’ }, plugins: [ new webpack.DefinePlugin({ ‘process.env’: { NODE_ENV: ‘“production”’ } }), new ExtractTextPlugin(‘./[name].[hash].css’, { allChunks: true }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }), new webpack.optimize.OccurenceOrderPlugin(), new assetsInsert() ] })

productionConf.module.loaders = [ { test: /.js$/, loader: ‘babel’, exclude: /node_modules/ }, { test: /.(png|jpe?g|gif|svg|woff2?|eot|ttf|otf)(\?.*)?$/, loader: ‘url?limit=8192&context=client&name=[path][name].[hash:7].[ext]’ }, { test: /.css$/, loader: ExtractTextPlugin.extract(‘style’, ‘css’), }, { test: /.scss$/, loader: ExtractTextPlugin.extract(‘style’, ‘css!sass’) }]

module.exports = productionConf

` 基本跟开发的差不多,差别在于:

1、使用ExtractTextPlugin 来打包css,所以要干掉原来base的loaders,重新写了一个,在最下面。

2、UglifyJsPlugin 给js压缩代码。其他没有什么好解释的了,一样的。 `

4. 构建命令

require(‘shelljs/global’) env.NODE_ENV = ‘production’ var ora = require(‘ora’) var webpack = require(‘webpack’) var webpackConfig = require(‘./webpack.production.config’)

var spinner = ora(‘building for production…’) spinner.start()

var staticPath = __dirname + ‘/../public/dist/’ rm(‘-rf’, staticPath) mkdir(‘-p’, staticPath)

webpack(webpackConfig, function (err, stats) { spinner.stop() if (err) throw err process.stdout.write(stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + ‘\n’) })

` 写一个build.js,然后在package.json里面添加 script 参数

“build”: “node build.js”//这里记得写自己build.js路径 `

5. 甜点(马卡龙) 有点腻

上面的配置是可以更改的,例如你在loaders 里面加上 { test: /.vue$/, loader: ‘vue’ } ` 就可以变成支持.vue文件的vuejs打包构建,同理,修改下支持jsx,和添加一些reactjs的module,就可以用来跑Reactjs的东西。

还有可以随意更改Css预编译器的类型,用你自己喜欢就行,或者跟我们前面提到的方法,把所有类型都配置上, ` var path = require(‘path’) var config = require(‘../config’) var ExtractTextPlugin = require(‘extract-text-webpack-plugin’)

exports.assetsPath = function (_path) { return path.posix.join(config.build.assetsSubDirectory, _path) }

exports.cssLoaders = function (options) { options = options || {} // generate loader string to be used with extract text plugin function generateLoaders (loaders) { var sourceLoader = loaders.map(function (loader) { var extraParamChar if (/\?/.test(loader)) { loader = loader.replace(/\?/, ‘-loader?’) extraParamChar = ‘&’ } else { loader = loader + ‘-loader’ extraParamChar = ‘?’ } return loader + (options.sourceMap ? extraParamChar + ‘sourceMap’ : “) }).join(‘!’)

if (options.extract) {
  return ExtractTextPlugin.extract('vue-style-loader', sourceLoader)
} else {
  return ['vue-style-loader', sourceLoader].join('!')
}

}

// http://vuejs.github.io/vue-loader/configurations/extract-css.html return { css: generateLoaders([‘css’]), postcss: generateLoaders([‘css’]), less: generateLoaders([‘css’, ‘less’]), sass: generateLoaders([‘css’, ‘sass?indentedSyntax’]), scss: generateLoaders([‘css’, ‘sass’]), stylus: generateLoaders([‘css’, ‘stylus’]), styl: generateLoaders([‘css’, ‘stylus’]) } }

// Generate loaders for standalone style files (outside of .vue) exports.styleLoaders = function (options) { var output = [] var loaders = exports.cssLoaders(options) for (var extension in loaders) { var loader = loaders[extension] output.push({ test: new RegExp(’\.’ + extension + ‘$’), loader: loader }) } return output }

这就是把所有的css预编译的都加到配置里面了。

Network

Cloud

云计算,是用互联网来接入存储或者运行在远程服务器端的应用,数据,服务。所以云计算行业包括了使用基于互联网的方法来计算,存储和开发的公司,或说是在互联网上提供其服务的公司。

IaaS, PasS, SaaS

云计算可以分为不同层次:基础设施-平台-软件,即 Infrastructure as a Service, Platform as a Service, Software as a Service。

Proxy

正向代理与反向代理区别

正向代理是位于客户端和原始服务器(origin server)之间的服务器。客户端向原始服务器请求内容时,向代理发送一个指定了目标的请求,由代理向原始服务器转发请求,然后将获得的内容返回客户端。客户端需要对发出的请求进行处理,使代理服务器了解请求的目标。

反向代理中,代理过程完全由客户端访问的服务器完成,代理过程对客户端不可见。客户端发出普通请求,反向代理服务器判断应向哪里的原始服务器转发请求,并将返回的内容作为响应转发回客户端。

正向代理中,客户端通过代理服务器隐藏了自身,如果需要安全性,可以在客户端连接代理服务器时进行鉴权。

用途

Norm

Code Review

为什么做 Code Review

做 Code review 是为了改进代码质量. 通过对代码, 测试过程和注释进行检查, 来确认方案设计和代码实现.

Code review 的主要目的:

  1. 保证代码质量
  1. 提高参与者开发水平
  2. 传递作者的意图和想法,使其他人可以更加熟悉代码,利于维护代码和改进代码
  3. 评审过程使大家更多的理解系统,重构思路,
  4. 确定作者的设计和实现清晰,简单
  5. 帮助初级开发人员学习高级开发人员的经验,达到知识共享, 相互学习,提高开发者水平

次要目的: - 查找程序的bug - 保证代码风格和编码标准 - 编码风格 - 避免开发人员犯一些很常见,很普通的错误 - 在项目早期就能够发现代码中的BUG

消除 Bug 主要靠测试. 由单元测试,功能测试,性能测试,回归测试来消除Bug。 单元测试为主。单元测试最接近bug,bug没有扩散的地方。 在 code review 之前,要有单元测试和单元测试报告,

代码风格和编码标准是确定的, 是靠个人可以做到的, 所以在代码提交时就应该是符合规范的。 代码规范,有作者自己和一些检查代码规范的工具。

规范性 - 注释格式 - 命名规范

在开发过程中, 对源代码进行系统检查. 查找代码缺陷, 检查功能实现, 编码合理性, 性能优化.

整理思路,流程的实现方法, 代码的组织方法(不是风格)

CR 的过程

完整的 Code Review 包括三个阶段: 准备, 执行, 追踪

  1. 准备
    1. 代码
      控制所要 review 的代码数量. 如果代码量比较大, 可对代码按功能模块进行分解, 确定(各部分的)关键代码, 对关键代码进行 review
    2. review 的重点
    3. 规范
      review 的规范用于交流时的统一, 有标准可遁, 提高 review 沟通的效率效果
    4. 确定参与者
      把 review 的代码, 重点, 规范和流程发给所有参与者
    5. 执行方式 git 式, meeting 式, 1对1 / 多对1 式
  2. implement 执行
    1. 准确记录
      对于CR过程发现的问题,我们必须清晰准确的记录,可以使用问题点记录单,明确记录的项目和内容。
    2. 讲解与提问 CR过程中,要采用代码作者讲解和审查者提问方式。审查者不能只在发现问题时提问,同时也要根据本次审查的内容要求代码作者对某个特定问题的讲解。
    3. 逐项审查
      对事前确定的审查内容,要逐项审查,不能因为时间不足等因素一扫而过。
  3. 追踪
    1. 确认问题信息
      • 问题的影响范围, 解决的难易程度
      • 解决问题的时限
      • 由谁来解决, 由谁来确认
    2. 解决过程记录
      • 问题的原因
      • 解决对策
      • 修改的内容
    3. 确认结果
      按时限对修改结果进行全面确认

说明

  1. 经常做 Code Review, 保持较短的 review 间隔
    • 一个人写代码的时间长了, 会在代码中加入越来越多的个性化的东西 :p
    • 一次 review 的代码越多, 要修改重构的也越多, 会在讨论中产生太多口水
    • 问题解决得越靠前, 总体上所要做的修改就越少
  2. Code Review 要快速, 不要拘泥形式

    • 严格按形式进行 Code Review 的问题:
      • 只有在 checklist 上的项目才会被 review
      • reviewer 在形式上花的精力多了, 在实际思考代码上的精力和耐心就会少
    • 可以花5分钟, 给人讲一下你的代码, 然后花5分钟, 听听他对你代码的意见和想法

    • 觉得有用处的地方, 再做成材料

    • 要能放松, 再放松, 把真正想说的说出来

2.0. code review 的过程是讨论问题, 解决问题
记住Review只不过是一种形式,而只有在相互信任中通过相互的讨论得到了有意义和有建设性的建议和意见,那才是最实在的。不然,作者和评审者的关系就会变成小偷和警察的关系。

  1. 每人都需要让不同的人 review 你的代码

    • 每次可以找两三个人来做 review, 这样3,4个人可以围在一起讨论, 每个人都能充分参与, 也不会因为意见太多降低执行的效率
    • 不同的人有不同的思考方式, 对同一功能和设计的实现有不同的想法, 可以从不同方向更全面地来过你的代码
    • 参与的人可以更好地理解你的代码, 团队中每个人的思考可以更同步, 在日后代码维护时相互帮助也更容易
  2. Enjoy 学会享受
    傲慢是程序员三大美德之一. 当看别人的代码时, 或者代码被人修改时, 自负的情绪会让人不容易接受不同的想法. Code review 需要一种积极的态度提意见和接受意见, 不要抨击别人的代码, 不要质疑别人的能力, 给同事的建议和同事给的建议, 都是为了大家能做得更好.
    Code review 的核心是讨论问题, 解决问题. 团队里人人都喜欢讨论问题, 解决问题, 那每个人都能写高质量的代码, 大家相互学习, 相互帮助, 每个人都能更好地进化, 团队能快速适应变化.
    关键是, 开心的才是更好的 :-)

准备工作

  1. 参与人对自己在 code review 要做什么, 要学什么有基本的了解
  2. 代码能正确运行/编译, 功能基本正确
  3. 代码能通过单元测试和测试用例
  4. 做 review 的人对代码有基本的了解, 代码的功能是什么, 涉及什么系统的什么方面, 对性能,稳定等方面有什么特别的需求, 好有针对性地检查代码

Code review 前代码的语法和功能问题应该已经解决, review 的重点在代码质量上.

GIT 提交

  1. 提交日志有明确的含义,明确此次提交的内容或目的 像“修改了某文件”,“更行了某文件”提供的信息太少。

写日志时考虑log的目的是在查找修改bug,版本回滚,代码评审等过程中提供有效的信息。

  1. 提交的大小 提交的代码太多,不便于代码评审和版本控制. 提交太多太小的修改,会影响版本和日志的可读性。 可以参考的做法是,对一个模块完成修改,昨晚单元测试后,压缩commit之后提交。

避免的做法:提交未测试的代码,出现问题后提交很多小的修改。

Review 项目

基本方面 - 代码一致性 - 代码安全问题 - 代码冗余 - 设计的正确性 - 性能与功能需求

代码中使用的格式、符号、结构等风格是否保持一致

  1. 完整性 completeness
    1. 完全实现设计文档中的功能需求
    2. 创建并初始化了所需要的数据库
    3. 去除了未定义或未使用的变量, 常量, 类
  2. 一致性 consistency
    1. 代码逻辑与文档中逻辑是否一致
    2. 代码中的算法符合文档中的数学模型
  3. 正确性 correctness
    1. 注释准确完整
    2. 函数调用参数传递正确
    3. 变量定义和使用方式正确
    4. 代码组织方式符合制定的标准
  4. 清晰性 clearness
    1. 所需的数据字典, 接口说明, 描述如何访问常量, 变量, 功能
    2. 配置, 定义文件是否易于理解, 修改
    3. 功能模块功能的入口和出口明确, 唯一
    4. 函数的出口唯一 (异常处理除外)
    5. 每个功能都是一个可辩识的代码块
  5. 可预测性 predictability
    1. 代码语言中常见陷阱已检测并排除
    2. 是否有依赖于开发工具环境语言缺省提供的功能
    3. 代码中没有死循环, 无穷递归
  6. 健壮性 robustness
    • 采取措施避免运行时错误: 数组边界溢出, 被零除, 值越界, 堆栈溢出, etc
  7. 可追溯性 traceability
    1. 每个程序有唯一标识
    2. 代码和开发文档之间有交叉引用(框架)可以相互对应
    3. 代码版本管理中对代码的修改和原因都有易于理解的记录
  8. 可读性 readability
    1. 注释清晰描述了每个子程序
    2. 复杂代码有必要的使用理由, 注释清楚明确
    3. 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度
    4. 变量, 函数的命名清楚反映含义, 便于记忆, 书写
    5. 变量都有合理合法的取值范围
  9. 可验证性 verifiability
    • 代码中功能的实现方式便于测试

Review 步骤

  1. 作者向参与review的人按用例或功能依次讲解自己的代码功能, 相关逻辑
  2. reviewer 提出疑问, 组织者对存在的问题做记录
  3. reviewer 自己对代码进行审核, 并把问题和修改建议记录在表中
  4. 组织者把各reviewer的代码问题记录表汇总成”review报告”, 记录发现的问题和修改建议,然后把”review报告”发送给相关人员
  5. 作者根据”review报告”的修改意见修改代码, 有不清楚的地方和reviewer进行交流
  6. 作者在过完 issue 之后作出反馈
  7. 组织者把 Code Review 中发现的有价值的问题更新到 “code review 规范” 文档中
  8. 对值得提醒和注意的问题可 email 给所有技术人员

所需文档

ref

相关资料 5个开源的代码审查工具

资料来源

SQL

Declare a Variable in MySQL

(ref)

There are mainly three types of variables in MySQL:

User-defined variables (prefixed with @):

You can access any user-defined variable without declaring it or initializing it. If you refer to a variable that has not been initialized, it has a value of NULL and a type of string.

SELECT @var_any_var_name You can initialize a variable using SET or SELECT statement:

SET @start = 1, @finish = 10;
or

SELECT @start := 1, @finish := 10;

SELECT * FROM places WHERE place BETWEEN @start AND @finish; User variables can be assigned a value from a limited set of data types: integer, decimal, floating-point, binary or nonbinary string, or NULL value.

User-defined variables are session-specific. That is, a user variable defined by one client cannot be seen or used by other clients.

They can be used in SELECT queries using Advanced MySQL user variable techniques. Local Variables (no prefix) :

Local variables needs to be declared using DECLARE before accessing it.

They can be used as local variables and the input parameters inside a stored procedure:

DELIMITER //

CREATE PROCEDURE sp_test(var1 INT) 
BEGIN   
    DECLARE start  INT unsigned DEFAULT 1;  
    DECLARE finish INT unsigned DEFAULT 10;

    SELECT  var1, start, finish;

    SELECT * FROM places WHERE place BETWEEN start AND finish; 
END; //

DELIMITER ;

CALL sp_test(5);

If the DEFAULT clause is missing, the initial value is NULL.

The scope of a local variable is the BEGIN … END block within which it is declared. Server System Variables (prefixed with @@):

The MySQL server maintains many system variables configured to a default value. They can be of type GLOBAL, SESSION or BOTH.

Global variables affect the overall operation of the server whereas session variables affect its operation for individual client connections.

To see the current values used by a running server, use the SHOW VARIABLES statement or SELECT @@var_name.

SHOW VARIABLES LIKE ‘%wait_timeout%‘;

SELECT @@sort_buffer_size;

They can be set at server startup using options on the command line or in an option file. Most of them can be changed dynamically while the server is running using SET GLOBAL or SET SESSION:

-- Syntax to Set value to a Global variable:
SET GLOBAL sort_buffer_size=1000000;
SET @@global.sort_buffer_size=1000000;

-- Syntax to Set value to a Session variable:
SET sort_buffer_size=1000000;
SET SESSION sort_buffer_size=1000000;
SET @@sort_buffer_size=1000000;
SET @@local.sort_buffer_size=10000;

Tools

Makefile

Make Basic

make is designed to manage the compiling process based on dependencies. The other functions of make is designed to make it easier to write Makefile.


When we deal with a complex project, there might be lots of source files, header files, lib files, target files. The files have a certain dependency relationship.

$ gcc -c -o test.o test.c
$ gcc -o helloworld test.o

The dependency line is: test.c -> test.o -> helloworld.

When dealing with many files, the compiler is called many times. We build the files on which is depended, then build target files on top of that.

And make is a tool to automatically record and process the dependency. The dependency relationship is written in the Makefile file, and make will compile the files w.r.t. the dependency described in Makefile.

Dependency

Here is a basic Makeifle

# target binary file: helloworld
helloworld: test.o
	echo "good"
	gcc -o helloworld test.o

test.o: test.c
	gcc -c -o test.o test.c

make is a recursive process.

  • If the current dependency has not prerequisite, then execute operations directly.
  • If the current dependency has prerequisite, and the required files exist and have not been changed since the last make (judged by write-timestamp), then execute operations directly.
  • If the required files of the current dependency do not exist, or have been changed, then use the required files as target files, and search their dependency, create target files.

Macro

Macro in make is text-type variable.

CC = gcc
helloworld: test.o
  echo "good"
  $(CC) -o helloworld test.o

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

Inner Macro

CC = gcc
helloworld: test.o
  echo $@
  $(CC) -o $@ $^

test.o: test.c
  $(CC) -c -o $@ $^

Suffix Dependency

CC = gcc

.SUFFIXES: .c .o

.c.o:
	$(CC) -c -o $@ $^

#--------------------------
helloworld: test.o
	echo $@
	$(CC) -o $@ $^

test.o: test.c

.c.o means .c is prerequisite, and .o is target. make will find test.o and test.c has dependency, and will execute operation for this dependency.

Suffix dependency is very handy is big projects.

Misc

Special dependency in makefile.

all: if make was followed by no filename, “all” will be chosen.

clean: it is used to clean legacy files.

CC = gcc

.SUFFIXES: .c .o

.c.o:
        $(CC) -c -o $@ $^

#--------------------------

all: helloworld
        @echo "ALL"

helloworld: test.o
        @echo $@
        $(CC) -o $@ $^

test.o: test.c

clean:
        -rm helloworld *.o

’@’ will hide the following command itself. ‘-’ will ignore the stderr of the following command.

Python

Class 自带函数

class Foor(object):
    def __init__(self, *args, **kwargs):
        print('init')
        super(Bar, self).__init__(*args, **kwargs)
    
    def __new__(cls, *args, **kwargs):
        print('new')
        return super(Bar, cls).__new__(cls, *args, **kwargs)

    def __call__(self, *args, **kwargs):
        print('call')
        
foo = Bar()
foo()

# Result:
new
init
call

用自带函数实现单例

class MySingleton(object):
    def __new__(cls, *args, **kwargs):
        if not '_instance' in vars(cls):
        cls._instance = super(MySingleton, cls).__new__(cls, *args, **kwargs)
    return cls._instance

review 记录:作者记录,表格记录,checkout -b review, checkout -b review_by_Foo, git pull && checkout -b review_by_Foo

http://blog.csdn.net/haoel/article/details/4469526

http://blog.csdn.net/haoel/article/details/4469462

http://www.cnblogs.com/lhb25/p/15-best-code-review-tools-for-developers.html

Code Review中的几个提示

Code Review应该是软件工程最最有价值的一个活动,之前,本站发表过《简单实用的Code Review工具》,那些工具主要是用来帮助更有效地进行这个活动,这里的这篇文章,我们主要想和大家分享一下Code Review代码审查的一些心得。

首先,我们先来看看Code Reivew的用处:

Code reviews 中,可以通过大家的建议增进代码的质量。 Code reviews 是一个传递知识的手段,可以让其它并不熟悉代码的人知道作者的意图和想法,从而可以在以后轻松维护代码。 Code reviews 也鼓励程序员们相互学习对方的长处和优点。 Code reviews 也可以被用来确认自己的设计和实现是一个清楚和简单的。 你也许注意到了在上面的Code Reivew中的诸多用处中,我们没有提到可以帮助找到程序的bug和保证代码风格和编码标准。这是因为我们认为:

Code reviews 不应该承担发现代码错误的职责。Code Review主要是审核代码的质量,如可读性,可维护性,以及程序的逻辑和对需求和设计的实现。代码中的bug和错误应该由单元测试,功能测试,性能测试,回归测试来保证的(其中主要是单元测试,因为那是最接近Bug,也是Bug没有扩散的地方)

所以,在今天,请不要把上面的那两件事分散了Code Review的注意力,取而代之的是,对于Bug,程序的作者要在Review前提交自己的单元测试报告(如:XUnit的测试结果),对于代码规范,这是程序作者自己需要保证的,而且,有一些工具是可以帮你来检查代码规范的。

不是说不能在Code Review中报告一个程序的bug或是一个代码规范的问题。只是说那并不是Code Review的意图。

1.- 经常进行Code Review 以前经历过几个相当痛苦的Code Review,那几次Code Review都是在程序完成的时候进行的,当你面对那近万行的代码,以前N我掺和在一起的功能,你会发现,整个Code Review变得非常地艰难,用不了一会儿,你就会发现大家都在拼命地打着哈欠,但还是要坚持,有时候,这样的Review会持续3个小时以上,相当的夸张。而且,会议上会出现相当多的问题和争论,因为,这就好像,人家都把整个房子盖好了,大家Review时这挑一点那挑一点,有时候触动地基或是承重墙体,需要大动手术,让人返工,这当然会让盖房的人一下就跳起来极力地维护自己的代码,最后还伤了团队成员的感情。

所以,千万不要等大厦都盖好了再去Reivew,而且当有了地基,有了框架,有了房顶,有了门窗,有了装修,的各个时候循序渐进地进行Review,这样反而会更有效率,也更有帮助。

下面是一些观点,千万要铭记:

要Review的代码越多,那么要重构,重写的代码就会越多。而越不被程序作者接受的建议也会越多,唾沫口水战也会越多。 程序员代码写得时候越长,程序员就会在代码中加入越来越多的个人的东西。 程序员最大的问题就是“自负”,无论什么时候,什么情况下,有太多的机会会让这种“自负”澎涨开来,并开始影响团队影响整个项目,以至于听不见别人的建议,从而让Code Review变成了口水战。 越接近软件发布的最终期限,代码也就不能改得太多。 我个人的习惯,并且也是对团队成员的要求是——先Review设计实现思路,然后Review设计模式,接着Review成形的骨干代码,最后Review完成的代码,如果程序复杂的话,需要拆成几个单元或模块分别Review。当然,最佳的practice是,每次Review的代码应该在1000行以内,时间不能超过一部电影的时间——1.5小时(因为据说那个一个正常人的膀胱可以容纳尿液的最长限度)

当然,在敏捷开发中,他们不需要Code Reivew,其实,敏捷开发中使用更为极端的“结对编程”(Pair-Programming)的方法 —— 一种时时刻刻都在进行Code Review的方法,个人感觉在实际过程中,这种方法有点过了。另外,大家可以看看本站的另一篇文章《结对编程的利与弊》来了解一下这种方法的问题。

3.- 尽可能的让不同的人Reivew你的代码 这是一个好主意,如果可能的话,不要总是只找一个人来Review你的代码,不同的人有不同的思考方式,有不同的见解,所以,不同的人可以全面的从各个方面评论你的代码,有的从实现的角度,有的从需求的角度,有的从用户使用的角度,有的从算法的角度,有的从性能效率的角度,有的从易读的角度,有的从扩展性的角度……,啊,好多啊,还让不让人活了。不管怎么说,多找一些不同的人会对你很有好处。当然,不要太多了,人多嘴杂反而适得其反,基本上来说,不要超过3个人,这是因为,这是一个可以围在一起讨论的最大人员尺寸。

从不同的方向评审代码总是好的。 会有更多的人帮你在日后维护你的代码。 这也是一个增加团队凝聚力的方法。

Code Review中文应该译作“代码审查”或是“代码评审”,这是一个流程,当开发人员写好代码后,需要让别人来review一下他的代码,这是一种有效发现BUG的方法。由此,我们可以审查代码的风格、逻辑、思路……,找出问题,以及改进代码。因为这是代码刚刚出炉的时候,所以,这也是代码重构,代码调整,代码修改的最佳时候。所以,Code Review是编码实现中最最重要的一个环节。

长时间以来,Code Review需要有一些有效的工具来支持,这样我们就可以更容易,更有效率地来进行代码审查工作。下面是5个开源的代码审查工具,他们可以帮助你更容易地进行这项活动。

  1. Review board: Review board 是一个 基于web 的工具,主要设计给 django 和python的用户。 Review board 可以帮助我们追踪待决代码的改动,并可以让Code-Review更为容易和简练。尽管Review board 最初被设计在VMware项目中使用,但现在其足够地通用。当前,其支持这些代码版本管理软件: SVN, CVS, Perforce, Git, Bazaar, 和Mercurial.

Yahoo 是review-board的其中一个用户。

“Review board 已经改变了代码评审的方式,其可以强迫高质量的代码标准和风格,并可以成为程序员编程的指导者。每一次,当你访问search.yahoo.com 时,其代码都是使用 Review board工具Review过的。 We’re great fans of your work!” – Yahoo! Web Search

Detailed review requests Powerful diff viewer

  1. Codestriker: Codestriker 也是一个基于Web的应用,其主要使用 GCI-Perl 脚本支持在线的代码审查。Codestriker 可以集成于CVS, Subversion, ClearCase, Perforce 和Visual SourceSafe。并有一些插件可以提供支持其它的源码管理工具。

David Sitsky 是 Codestriker 的作者,并也是最活跃的开发人员之一。 Jason Remillard 是另一个活路的开发者,并给这个项目提供了最深远最有意义的贡献。大量的程序员贡献他们的代码给 Codestriker 项目,导致了这个项目空前的繁荣。

http://codestriker.sourceforge.net/viewtopicdetail.png

  1. Groogle: Groogle 是一个基于WEB的代码评审工具。 Groogle 支持和 Subversion 集成。它主要提供如下的功能:

各式各样语言的语法高亮。 支持整个版本树的比较。 支持当个文件不同版本的diff功能,并有一个图形的版本树。 邮件通知所有的Reivew的人当前的状态。 认证机制。 Screenshot

  1. Rietveld: Rietveld 由Guido van Rossum 开发(他是Python的创造者,现在是Google的员工),这个工具是基于Mondrian 工具,作者一开始是为了Google 开发的,并且,它在很多方面和Review board 很像。它也是一个基于Web的应用,并可以Google App Engine 当主机。它使用了目前最流行的Web开发框架 django 并支持 Subversion 。当前,任何一个使用 Google Code 的项目都可以使用 Rietveld 并且使用 python Subversion 服务器。当然,它同样支持其它的Subversion服务器。

  2. JCR JCR 或者叫做 JCodeReview 也是一个基于WEB界面的最初设计给Reivew Java 语言的一个工具。当然,现在,它可以被用于其它的非Java的代码。

JCR 主要想协助:

审查者。所有的代码更改都会被高亮,以及大多数语言的语法高亮。Code extracts 可以显示代码评审意见。如果你正在Review Java的代码,你可以点击代码中的类名来查看相关的类的声明。 项目所有者。可以 轻松创建并配置需要Review的项目,并不需要集成任何的软件配置管理系统(SCM)。 流程信仰者。 所有的评语都会被记录在数据库中,并且会有状态报告,以及各种各样的统计。 架构师和开发者。 这个系统也可以让我们查看属于单个文件的评语,这样有利于我们重构代码。 JCR 主要面对的是大型的项目,或是非常正式的代码评审,从这方面看来,他并不像上面的那些工具。

Screenshot

Jupiter:最后我们要提一下Jupiter,这是另一个代码review的工具你可以去考虑使用的,它是一个Eclipse IDE 的插件。

new T1 算法 Schedule 48

所有时间估计实施时长乘系数 0.5~3

程序

apps 23.5 + 3*?

models 8

单元用例 12

集成用例 4

解决方案 8

超过8小时就不做,先确定问题

指定单据退货fifo 指定单据退货avg 不指定单据退货manual 不指定单据退货fifo 不指定单据退货avg 修改单据