安迪桑的笔记


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

日志收集

php错误日志

php.ini

log_erros
log_erros_max_len
error_log
display_errors

php-fpm.conf

slowlog
request_slowlog_timeout
access.log

ngin访问日志

日志切割

awk

logrotate [ˈroʊteɪt]

日志收集

ELK

kafka[‘ka:fka:]

Scribe[skraɪb]

flume[flum]

日志存储

Hadoop

Spark

HBase

Go学习01 -- 环境搭建

安装下载

官网 下载自己机器系统对应的二进制安装包,解压到相应目录.

解压目录说明:

  • Linux 和 Mac OS 系统通常解压到 /usr/local/go
  • Windows 系统通常解压到 c:\Go 目录

本次安装过程实际在 Ubuntu 机器上执行的命令如下:

1
2
#tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
sudo tar -C /usr/local -xzf go1.7.4.linux-amd64.tar.gz

环境变量设置

  • 在系统 PATH 环境变量里添加  /usr/local/go/bin
  • GOROOT变量指向go安装目录,默认通常为 /usr/local/go 或 c:\Go,需要根据实际安装情况自行调整
  • GOPATH 是用来设置包加载路径的重要变量,当在命令行运行 go build, go get 等命令时会需要该变量。这个变量通常指向你的工作空间(go项目代码存放位置)目录,这里假设你的工作空间为 $HOME/work (实际开发过程中为了方便管理项目,通常在项目自定义脚本中设置 GOPATH 和编译打包,省去每新增一个项目都需要修改 GOPATH 变量的麻烦,参见下文)

本将安装过程 Ubuntu 机器上新建 /etc/profile.d/go.sh 文件内容如下:

1
2
3
export GOROOT=/usr/local/go
export GOPATH=$HOME/work
export PATH=$PATH:$GOROOT/bin

创建完go.sh文件后,注销用户重新登录或是运行

1
source /etc/profile

注:关于环境变量相关的设置,需要了解各平台相关知识,这里不细述,自行百度或谷歌

Go 项目目录结构

一个 Go 程序项目一般包含三个目录

  • src 目录存放go程序源文件,程序源文件按包组织(通常每个目录都对应一个包)
  • pkg 目录包含包对象
  • bin 目录包含可执行命令

一个工作空间目录结构看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bin/
streak # 可执行命令
todo # 可执行命令
pkg/
linux_amd64/
code.google.com/p/goauth2/
oauth.a # 包对象
github.com/nf/todo/
task.a # 包对象
src/
code.google.com/p/goauth2/
.hg/ # mercurial 代码库元数据
oauth/
oauth.go # 包源码
oauth_test.go # 测试源码
github.com/nf/
streak/
.git/ # git 代码库元数据
oauth.go # 命令源码
streak.go # 命令源码
todo/
.git/ # git 代码库元数据
task/
task.go # 包源码
todo.go # 命令源码

此工作空间包含三个代码库(goauth2、streak 和 todo),两个命令(streak 和 todo) 以及两个库(oauth 和 task)。

项目 GOPATH 设置脚本

前面介绍环境变量 GOPATH 的时候提到,为了避免每次新建一个项目都去修改系统环境变量的麻烦。我们可以使用一个脚本来设置特定项目的 GOPATH 变量。

install.sh 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
if [ ! -f install.sh ]; then
echo '安装脚本必须在项目根目录中运行' 1>&2
exit 1
fi
#获取当前项目目录
CURDIR=`pwd`
#获取系统设置的 GOPATH
OLDGOPATH="$GOPATH"
#将GOPATH设置为当前项目
export GOPATH="$CURDIR"
#以下为特定项目编译需要运行的命令
gofmt -w src
go build -o bin/hello src/hello.go
#以上为特定项目命令
#还原系统GOPATH变量值
export GOPATH="$OLDGOPATH"
echo '编译完成'

Windows 下的 install.bat 脚本(未验证):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@echo off
setlocal
if exist install.bat goto ok
echo 安装脚本 install.bat 必须在项目根目录中运行
goto end
: ok
set OLDGOPATH=%GOPATH%
set GOPATH=%~dp0
gofmt -w src
go build -o bin/hello src/hello.go
:end
echo 编译完成

hello,world

通过之前的介绍,现在我们来写一个hello,world的例子

  • 项目目录结构如下:
1
2
3
4
5
6
7
helloworld/
├── bin
│   └── hello
├── install.sh
├── pkg
└── src
└── hello.go
  • 编写 src\hello.go 文件
1
2
3
4
5
6
7
package main
import "fmt"
func main() {
fmt.Printf("你好,世界.\n")
}
  • 编写 install.sh 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash
if [ ! -f install.sh ]; then
echo '安装脚本必须在项目根目录中运行' 1>&2
exit 1
fi
#获取当前项目目录
CURDIR=`pwd`
#获取系统设置的 GOPATH
OLDGOPATH="$GOPATH"
#将GOPATH设置为当前项目
export GOPATH="$CURDIR"
#以下为特定项目编译需要运行的命令
gofmt -w src
go build -o bin/hello src/hello.go
#以上为特定项目命令
#还原系统GOPATH变量值
export GOPATH="$OLDGOPATH"
echo '编译完成'
  • 编译
    编写完上面的文件之后,让我们进到项目根目录运行 helloworld程序
1
2
3
4
5
6
7
cd helloworld
#编译
sh install.sh
#运行
./bin/hello
#此时你应该能在终端看到如下输出
你好,世界.

本地运行Go自带的学习资源

  • 离线运行官方文档

    1
    godoc -http=:8080 -v
  • 离线运行官方入门学习教程

    1
    go tool tour

参考资源:
官方文档中文版
Go项目的目录结构

React基础--key

React列表

CentOS 7 使用 pptpd 搭建 VPN 的 iptables 配置

pptpd设置

主要会操作三个配置文件:

  • 主服务配置文件 /etc/pptpd.conf
  • DNS等选项配置文件 /etc/ppp/options.pptpd
  • 用户授权配置文件 /etc/ppp/chap-secrets

pptpd服务配置

编辑 /etc/pptpd.conf 去掉前面的#去掉:

1
2
localip 192.168.1
remoteip 192.168.1.234-238,192.168.1.245

解释下: localip是pptp使用的ip, 可以随意; remoteip链接到vpn的用户分配到ip的访问, 和localip同一个网段即可.

DNS设置

编辑 /etc/ppp/options.pptpd 去掉ms-dns前面的#,修改成下面的数据:

1
2
ms-dns 8.8.8.8
ms-dns 8.8.4.4

解释: 设置链接到vpn的用户如果访问网络时使用的dns, 和他们自己电脑与服务器设置的dns没任何关系.

VPN账号和密码

编辑 /etc/ppp/chap-secrets,直接输入如下字段,vpsma可以换成其他字段,
格式: 用户名 pptpd 密码 IP 的形式编写,如果需要多个账号就写多行,一行一个

1
test pptpd 1234 *

解释: 这是链接vpn的用户密码, 每行一个, 代表一个用户.
格式说明: 第一列为用户, 依次是 服务器名称, 密码和ip, 中间使用一个空格或者tab隔开.
用户和密码可随意。服务器名(pptpd)不要改,如果修改请保证和/etc/pptpd.conf中的name 保持一致。后面的*代表ip由pptpd自动分配

开启ip转发

编辑 /etc/sysctl.conf 文件

1
2
3
4
5
6
7
#将“net.ipv4.ip_forward”改为1,开启ip转发。这个不是必须的
net.ipv4.ip_forward=1
#同时注释掉 “net.ipv4.tcp_syncookies = 1” (前面加#)
# net.ipv4.tcp_syncookies = 1
#保存退出`sysctl.conf`文件编辑后,运行下面的命令,能让设置立即生效
sysctl -p

配置完成后,重启pptpd服务并设置pptpd开机自启动

1
2
3
4
5
service pptpd start
systemctl enabled pptpd
#启动服务后,可能通过系统日志查看运行情况
tailf /var/log/messages

iptables 配置

iptables 命令知识

ipdables 配置文件位于 /etc/sysconfig/iptables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#带行号查看当前所有规则
iptables -L -n --line-numbers
#清除所有规则
iptables -F
#删除指定行号(以下命令中的“5”为指定行号)规则
iptables -D 5
#保存当前配置;相当于旧版/etc/init.d/iptables save
service iptables save
#重启iptables;相当于旧版本/etc/init.d/iptables restart
service iptables restart
#注册iptables服务;相当于旧版 chkconfig iptables on
systemctl enable iptables.service
#开启服务
systemctl start iptables.service
#查看状态
systemctl status iptables.service

允许连接PPTP服务,1723为pptp服务端口

1
2
iptables -I INPUT -p tcp --dport 1723 -j ACCEPT
iptables -A INPUT -p tcp -m state --state NEW,RELATED,ESTABLISHED -m tcp --dport 1723 -j ACCEPT

允许建立VPN隧道,否则无法验证用户名及密码

1
2
iptables -I INPUT -p gre -j ACCEPT
iptables -A INPUT -p gre -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT

建立NAT转换规则,否则拨上也无法通过远程网关连上公网

1
2
3
4
5
6
7
#这里一定看清楚,里面的ip“192.168.1.0/24”要和 /etc/pptpd.conf 的“localip”配置网段对应,还要注意网卡eth0,如果你的网卡不是eth0,就改成你相应的网卡名
#OpenVZ系统用此命令,1.1.1.1为你的VPS的IP地址
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j SNAT --to 1.1.1.1
#XEN系统用这个命令
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE

如果某些网站不能访问,加上

1
iptables -I FORWARD -p tcp --syn -i ppp+ -j TCPMSS --set-mss 1356

原因分析

linux下pptp搭建的vpn代理上网很慢解决方法。
在pptp所在的linux服务的iptables的*filter表中加入

1
-I FORWARD -p tcp --syn -i ppp+ -j TCPMSS --set-mss 1356

或者在命令提示符运行

1
/sbin/iptables -I FORWARD -p tcp --syn -i ppp+ -j TCPMSS --set-mss 1356

拨通vpn,在服务器上用netstat –i查看接口,得到

1
2
3
4
5
Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0 1500 0 102528561 0 0 0 194391413 0 0 0 BRU
eth1 1500 0 519820535 954 11553 924 208798037 0 0 0 BRU
lo 16436 0 151062 0 0 0 151062 0 0 0 LRU
ppp0 1396 0 19 0 0 0 8 0 0 0 OPRU

可知ppp的最大mtu为1396,当然,对应的mss应为(mtu-20字节的IP头部+20字节的TCP 头部=)1356

  • 1、计算机网络中的MSS:
    MSS: Maximum Segment Size 最大分段大小
    MSS最大传输大小的缩写,是TCP协议里面的一个概念。

MSS就是TCP数据包每次能够传输的最大数据分段。为了达到最佳的传输效能,TCP协议在建立连接的时候通常要协商双方的MSS值,这个值TCP协议在实现的时候往往用MTU值代替(需要减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes)所以往往MSS为1460。通讯双方会根据双方提供的MSS值得最小值确定为这次连接的最大MSS值。

  • 2、mtu是网络传输最大报文包。
    mss是网络传输数据最大值。mss加包头数据就等于mtu.
    简单说拿TCP包做例子。
    报文传输1400字节的数据的话,那么mss就是1400,再加上20字节IP包头,20字节tcp包头,那么mtu就是1400+20+20.

当然传输的时候其他的协议还要加些包头在前面,总之mtu就是总的最后发出去的报文大小。mss就是你需要发出去的数据大小。

假设PC建立了到SERVER的HTTP连接,PC希望从SERVER下载一个大的网页。SERVER接收到PC的请求后开始发送大网页文件,其IP的DF位置1,不允许分片,IP报文长度为1500字节。到达VPN网关2的外网口(以太)后,VPN网关2发现其长度超过了1500个字节,于是将其丢弃,并给SERVER发回一个目的地址不可达的ICMP信息,同时指出“MTU of next hop: 1500”。PC接收到该消息后,又按照1500字节对外发送,又被丢弃,于是就形成了循环,无法通讯。

根据上述的分析,很容易得到如下解决方式,在VPN网关2的出接口设置MTU为1500-4-20=1476,这样VPN网关2返回ICMP不可达消息时将给出”MTU of next hop: 1476”。SERVER将以1476作为自己的最大MTU对外发送,到达VPN网关1,封装GRE和外层IP头后就不会超过1500而顺利发到对端。

1
-I FORWARD -p tcp --syn -i ppp+ -j TCPMSS --set-mss 1356

因为mss是在TCP连接建立开始时,通过带有syn标志的IP数据包进行传输的,所以我们在iptables里面规定,在转发数据时,只要发现产生于ppt的带有 syn标志数据包时,将其mss设定为1356字节,这样就与ppp0接口的路径MTU向匹配了,数据自然就可以畅通无阻啦。
(注,vpn拨入一个,则建立一个ppt
的虚拟设备,这个可以再linux上用ifcpnfig看到,第一个为ppp1,第二个为ppp2……)

  • 3、在iptables里面加入一条规则
1
iptables -A FORWARD -p tcp --syn -s 10.87.200.0/31 -j TCPMSS --set-mss 1356

因为mss是在TCP连接建立开始时,通过带有syn标志的IP数据包进行传输的,所以我们在iptables里面规定,在转发数据时,只要发现带有 syn标志并且源地址为主机B的IP数据包时,将其mss设定为1356字节,这样就与ppp0接口的路径MTU向匹配了,数据自然就可以畅通无阻啦。

因为mss是在TCP连接建立开始时,通过带有syn标志的IP数据包进行传输的,所以我们在iptables里面规定,在转发数据时,只要发现带有 syn标志并且源地址为主机B的IP数据包时,将其mss设定为1356字节,这样就与ppp0接口的路径MTU向匹配了,数据自然就可以畅通无阻啦。

此次在实际 CentOS 环境中完整配置规则

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
#允许连接PPTP服务
iptables -I INPUT -p tcp --dport 1723 -j ACCEPT
#允许建立VPN隧道以验证用户名密码
iptables -I INPUT -p gre -j ACCEPT
#建立NAT转换规则
iptables -t nat -I POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
#允许pptpd转发
iptables -I FORWARD -i ppp+ -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
#优化网络数据传输速度
iptables -I FORWARD -p tcp --syn -i ppp+ -j TCPMSS --set-mss 1356
echo "完成"

Babel CLI 安装及使用

背景

本地项目部分页面使用 react ,通常就是一两个jsx文件需要编译。所以不打算使用 webpack 等打包工具。只需要最简单的能通过命令行编译 ES6 语法编写的 jsx 源文件为普通js文件

Babel CLI 的安装

在官方网站安装说明部分,建议安装 babel CLI 使用本地方式,而非全局方式。一是因为本地开发中,各个项目依赖的 Babel 版本不尽相同,另外一方面是让项目更加独立,不依赖于具体的机器环境

There are two primary reasons for this.

  • Different projects on the same machine can depend on different versions of Babel allowing you to update one at a time.
  • It means you do not have an implicit dependency on the environment you are working in. Making your project far more portable and easier to setup.

在项目根目录中运行

1
$ npm install --save-dev babel-cli babel-preset-latest

因为全局安装运行 Babel 弊大于利,如果你之前已经全局安装过 Babel,可以使用如下命令卸载全局的 Babel

1
$ npm uninstall --global babel-cli

使用 ES6 开发 React 需要安装的其它模块

  • babel-preset-es2015 (ES2015(ES6) 语法转换支持)
  • babel-preset-react (JSX 语法转换支持)

在项目根目录中运行

1
2
$ npm install --save-dev babel-preset-es2015
$ npm install --save-dev babel-preset-react

监听并实时编译指定文件 

准备工具完成后,我们可以开始编写代码了。在代码编写过程中,通过如下命令实时监控代码变动并编译生成转换文件

1
babel 源文件名 --watch --presets es2015,react --out-file 转换后的文件名

我们也可以使用如下命令,将 src 目录下的所有文件编译到 lib 目录下

1
babel src --watch --presets es2015,react --out-dir lib

参考链接

  • 走进Babel 6.0 全新特性解析
  • Babel5 升级到 Babel6 总结
  • bablejs 官网
  • ECMAScript 6 入门

在多机部署时 Yii 的 assetManager 资源发布目录不一致问题

Yii 提供了 assetManager 来管理相对独立的资源内容,通过 assetManager 可以很方便地将相关功能的 js,css,图片等资源进行管理和二次发布。当我们的资源放置位置不是位于网络可访问目录中时,Yii 的 assetManager 会自动将这些资源自动发布到 @web/assets 目录中,并且随机生成一个资源文件夹名称。当我们的程序是单机部署时,没有问题。而当我们进行多机部署时,会发现在在每台机器上生成的资源文件夹名称不一致的情况。这将导致页面上部分资源文件无法加载,报 404 错误。

为了解决这个问题,我们先来看一下 Yii2 中关于资源文件夹目录名称生成的源码片断(文件位于 web/AssetManager.php)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
public function publish($path, $options = [])
{
$path = Yii::getAlias($path);
if (isset($this->_published[$path])) {
return $this->_published[$path];
}
if (!is_string($path) || ($src = realpath($path)) === false) {
throw new InvalidParamException("The file or directory to be published does not exist: $path");
}
if (is_file($src)) {
return $this->_published[$path] = $this->publishFile($src);
} else {
return $this->_published[$path] = $this->publishDirectory($src, $options);
}
}

在Yii内部,资源发布的时候调用的就是这个 publish 函数,可以看到,这里面主要有两个相关函数 publishFile 和 publishDirectory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
protected function publishFile($src)
{
$dir = $this->hash($src);
$fileName = basename($src);
$dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir;
$dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName;
if (!is_dir($dstDir)) {
FileHelper::createDirectory($dstDir, $this->dirMode, true);
}
if ($this->linkAssets) {
if (!is_file($dstFile)) {
symlink($src, $dstFile);
}
} elseif (@filemtime($dstFile) < @filemtime($src)) {
copy($src, $dstFile);
if ($this->fileMode !== null) {
@chmod($dstFile, $this->fileMode);
}
}
return [$dstFile, $this->baseUrl . "/$dir/$fileName"];
}
protected function publishDirectory($src, $options)
{
$dir = $this->hash($src);
$dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir;
if ($this->linkAssets) {
if (!is_dir($dstDir)) {
FileHelper::createDirectory(dirname($dstDir), $this->dirMode, true);
symlink($src, $dstDir);
}
} elseif (!empty($options['forceCopy']) || ($this->forceCopy && !isset($options['forceCopy'])) || !is_dir($dstDir)) {
$opts = array_merge(
$options,
[
'dirMode' => $this->dirMode,
'fileMode' => $this->fileMode,
]
);
if (!isset($opts['beforeCopy'])) {
if ($this->beforeCopy !== null) {
$opts['beforeCopy'] = $this->beforeCopy;
} else {
$opts['beforeCopy'] = function ($from, $to) {
return strncmp(basename($from), '.', 1) !== 0;
};
}
}
if (!isset($opts['afterCopy']) && $this->afterCopy !== null) {
$opts['afterCopy'] = $this->afterCopy;
}
FileHelper::copyDirectory($src, $dstDir, $opts);
}
return [$dstDir, $this->baseUrl . '/' . $dir];
}

通过源码我们可以看到,这两个函数在生成随机目录名 dir 时实际上都调用了一个 hash方法,让我们来看一下这个方法:

1
2
3
4
5
6
7
8
9
<?php
protected function hash($path)
{
if (is_callable($this->hashCallback)) {
return call_user_func($this->hashCallback, $path);
}
$path = (is_file($path) ? dirname($path) : $path) . filemtime($path);
return sprintf('%x', crc32($path . Yii::getVersion()));
}

到这里,应该能很明白的看到是什么原因导致了在多机器上部署会导致文件名不一致了。核心原因就在于 filemtime($path) 这个部分。filemtime() 函数的作用是返回文件内容上次的修改时间。多机器部署的时候,我们通常不能保证同一个文件在每台机器上的这个时间一致。所以导致最终计算出来的名称不一致。

现在让我们来看一下,如何解决这个问题,在上面的代码中,我们看到有一个 hashCallBack 属性,这个属性值是一个可执行的自定义资源目录生成函数。

解决方法一: 配置文件中全局设置 assetManager 组件

1
2
3
4
5
6
7
8
9
<?php
components => [
'assetManager' => [
'hashCallback' => function ($path) {
$path = (is_file($path) ? dirname($path) : $path);
return sprintf('%x', crc32($path . Yii::getVersion()));
},
],
]

解决方法一: 局部动态设置

1
2
3
4
<?php
Yii::$app->getAssetManager()->hashCallback = function ($path) {
return 'datatable';
}

React开发技术栈

概述

React本身仅只是一个视图层的解决方案,真实项目中还需要配合其它库来进行开发。这里介绍的是一个可进行完整项目开发的技术栈。将介绍以下这些库及它们之间如何配合来完成一个完整的项目开发。

  • react 处理视图
  • react-router 前端路由
  • redux 应用状态管理,即数据获取及处理
  • react-redux react官方redux绑定
  • redux-thunk 扩展redux,方便处理异步请求

react

react的核心是组件,了解组件的核心是搞清楚组件生命周期。组件之后,需要了解的是react组件之间信息流的传递,主要是通过props(包含回调函数,组件渲染所需要的数据等)

组件定义,组件通过 React.createClass()创建,包含以下一些定义

  • render()
  • getInitialState()
  • getDefaultProps()
  • propTypes
  • mixins
  • statics

react组件生命周期函数:

  • componentWillMount()
  • componentDidMount()
  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • componentDidUpdate()
  • componentWillUnmount()

包含所有定义和生命周期函数的组件定义看起来像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Home = React.createClass({
mixins:[],
statics: {},
propTypes:{},
getInitialState: function(){},
getDefaultProps: function(){},
render: function(){},
componentWillMount: function(){},
componentDidMount: function(){},
componentWillReceiveProps: function(){},
shouldComponentUpdate: function(){},
componentWillUpdate: function(){},
componentDidUpdate: function(){},
componentWillUnmount: function(){}
});

ES6的写法略有不同,看起来像这样

1
2
3
4
5
6
7
8
class Home extends Reac.Component{
constructor(props){
super(props);
this.state = {}; //取代getDefaultState()
}
render(){
}
}

react-router

没有太多学习成本,主要功能就是提供路由和组件的绑定。

1
2
3
4
5
6
7
8
9
ReactDOM.render((
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="about" component={About}/>
</Route>
<Route path="/other" component={Other}>
</Router>
), document.getElementById('react-content'))

解释,上面的代码定义了一个路由规则

  • 访问 / 路径的时候,渲染Home组件
  • 访问 /about 路径的时候,渲染About组件
  • 访问 /other 路径的时候,渲染Other组件

需要注意到Home和About被包含在App组件中,这样定义实现的效果是,Home和About组件的渲染会包裹在App组件中,实际上相当于我们平时开发流程中使用的layout视图层,这里的App组件就相当于Home和About的layout。来看一下App组件的render()方法

1
2
3
4
5
6
7
8
9
10
11
class App extends React.Component {
render() {
return (
<div>
<Header/>
{this.props.children}
<Footer/>
</div>
)
}
}

上面的代码,在App组件中,我们通过 this.props.children 来获取当前路由对应的子组件(本例中的Home或About),可以看出,在App组件中,我们还增加了Header和Footer两个组件,给About和Home加上了页头页尾

另外需要注意的是history这个props,它有三个选项

  • browserHistory 基于浏览器URL(使链接更有意义,符合通常的认识,一般需要服务端支持url重写)
  • hashHistory 基于url的hash值(url中#之后的部分)
  • createMemoryHistory

其它常用的组件:

  • <Link>
  • <Redirect>

以上是简单介绍,更多信息可访问官方文档,查看说明

redux 和 react-redux

redux是JavaScript状态容器,用于管理应用的状态(个人理解实际就是应用的数据和引起数据变更的操作,在react应用开发里,就是指定渲染react组件需要的数据和各种回调函数)

redux基础概念

API

  • createStore
  • Store
  • combineReducers
  • applyMiddleware
  • bindActionCreators
  • compose

state

前端各种组件在特定的state下渲染会有所不同,一个展示组件就是通过state的变化来渲染界面的。

1
2
3
4
5
6
7
8
state = {
todoReducer: {
todos: [
{text:'还书',completed: false},
{text:'买菜',completed: true}
]
}
}

store

Store就是用来维持应用所有的state树的一个对象。应用的所有state以单一的对象存在一个单一的store中.可以理解为应用的数据库,存放渲染应用需要的所有数据。通过dispatch一个action来修改store中的state

Store对象的方法

  • getState() 返回应用当前的 state 树。
  • dispatch(action) 分发 action。这是触发 state 变化的惟一途径
  • subscribe(listener) 添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用 getState() 来拿到当前 state。
  • replaceReducer(nextReducer) 替换 store 当前用来计算 state 的 reducer。

action

实际就是一个对像,必须包含一个type字段,type字段相当于定义了action的名字。通常长下面这个样子

1
2
3
4
5
const ADD_TODO = "ADD_TODO"
{
type: ADD_TODO
text: ''
}

通常我们通过一个函数来封装action

1
2
3
4
5
6
function addTodo(todo){
return {
type: ADD_TODO,
text: todo
}
}

reducers

实际就是回调函数的处理逻辑,响应的是action定义的操作,当action被调用的时候,触发指定的reducers函数,函数接受旧的state和action作为参数,返回一个新的state,新的state引起页面组件发生变化,从而使页面展示的信息发生变化

需要特别理解的是,reducers函数必须返回一个新的state对象

1
2
3
4
5
6
7
8
9
function todoReducer(state,action){
switch(action.type){
case ADD_TODO:
return Object.assign({},state,{text:action.text,completed:false}); //合并新的
break;
default:
return state;
}
}

dispatch

有了action和reducers定义,那怎么把它们联系起来呢,这时候就要用到dispatch了。通常就是下面这个样子

1
2
3
4
dispatch({
type: ADD_TODO,
text: '喝水'
})

dispatch 的参数必须是一个action 对象(稍后会在react-thunk中再提到这个),调用dispatch后,指定的reducers将接收到这个调用,然后处理调用,返回新的状态。我们执行上面的dispatch后,state会变成下面这个样子

1
2
3
4
5
6
7
8
9
state = {
todoReducer: {
todos: [
{text:'还书',completed: false},
{text:'买菜',completed: true},
{text:'喝水',completed: false} //这一行是新添加的
}
}

整体过一遍

  • 定义好action和reducers
  • 通过将reducers传递给`createStore()`来创建一个store对象
  • 在应用中通过dispatch方法触发action,改变state

react-redux

通过提供容器组件把sotre和dispatch绑定到真正展示用的组件中去

API

  • <Provider
  • connect()
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'
import ReactDOM from 'react-dom'
import {createStore} from 'redux'
import {Provider} from 'react-redux'
import todoReducer from './reducers/todoReducer'
let store = createStore(todoReducer)
ReactDOM.render((
<Provider store={store}>
<App/>
</Provider>
), document.getElementById('react-content'));

上面的代码中,我们通过传入`todoReducer`给redux库的createStoreAPI函数,创建了App组件的唯一`store`对象。
然后再通过`react-redux`的Provider组件使得Provider之下的组件都能通过 connect() 方法获得 Redux store。

在App组件中

1
2
3
4
5
6
7
8
9
10
11
12
13
import Reactfrom 'react'
import Header from '../pub/Header'
export default class App extends React.Component {
render() {
return (
<div>
<Header/>
{this.props.children}
<Footer/>
</div>
)
}
}

因为App组件包裹在Provider之下,所以App下的Header组件也能通过connect获取store对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, {PropTypes} from 'react'
import {connect} from 'react-redux'
import {initMenu} from '../actions/Action'
import MainMenu from './MainMenu'
class Header extends React.Component {
componentDidMount() {
let {dispatch} = this.props.store
dispatch(addTodo())
}
render() {
return (
<header id="header"></header>
)
}
}
function mapStateToProps(store) {
return {store: store}
}
export default connect(mapStateToProps)(Header)

react-thunk

上面谈dispatch的时候,我们说到,`dispatch`必须接受一个`action`对象作为参数,像如下这样

1
2
3
4
5
6
function addTodo(todo){
return {
type: ADD_TODO,
text: todo
}
}

react-thunk是redux中间件,让dispatch能使用除了action以外的其它内容作为参数.比如下面的例子,我们定义一个fetchTodo函数,从某个远程接口获取一条todo数据,然后把这条数据通过dispatch添加到新的todo列表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function addTodo(todo){
return {
type: ADD_TODO,
text: todo
}
}
function fetchTodo(){
return dispatch => {
$.get("http://api.com/gettodo",function(todo){
dispatch(addTodo(todo));
})
}
}

Node开发技术栈

package

  • cross-env 跨平台设置NODE_ENV

package.json文件中命令设置环境变量时
windows 环境下报错: ‘NODE_ENV’ 不是内部或外部命令,也不是可运行的程序或批处理文件。

1
2
3
4
"scripts": {
"start": " NODE_ENV=development webpack-dev-server --host 0.0.0.0 --devtool eval --progress --color --profile",
"deploy": "NODE_ENV=production webpack -p --progress"
}
  • rimraf nodejs版 rm -rf命令
    尤其在window下,当删除node_modules文件夹时,会报目录层次太深删除报错的问题

Redux概念

Redux

state

当前应用的数据状态,决定了UI应该如何展示,state变化通常将导致UI变化

action

描述“发生了什么的”对象,用于数据传递。必须包含一个type字段用于表示将执行的动作.
通常被 store.dispatch()调用

例如以下action可用于描述id为42的文章被点赞这个行为

1
{type: 'LIKE_ARTICLE', articleId: 42 };

reducer

计算数据,将变化的数据合并或去除,返回变化后的state。实际上就是响应store.dispatch()调用。
处理action返回新的state

store

存放应用中所有的state,将action和reducer联系在一起。通过dispatch一个action触发reducer,从而改变应用的state

Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器。

Redux函数和react-redux组件

Redux

  • createStore(reducer, [initialState]) 创建一个 Redux store 来以存放应用中所有的 state  

    • 参数

      • reducer (Function):

        接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。

      • [initialState] (any):

        初始时的 state。在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 reducer 可理解的内容。

  • combineReducers(reducers)

    随着应用变得复杂,需要对 reducer 函数 进行拆分,拆分后的每一块独立负责管理 state 的一部分。

    combineReducers 辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore。

    合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定。

react-redux

  • <Provider store={}>

    对组件注入Redux Store

  • connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

    连接 React 组件与 Redux store

    参数

    • mapStateToProps(state, [ownProps]): stateProps

      如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。如果指定了该回调函数中的第二个参数 ownProps,则该参数的值为传递到组件的 props,而且只要组件接收到新的 props,mapStateToProps 也会被调用。

    • mapDispatchToProps(dispatch, [ownProps]): dispatchProps

      如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,而且这个对象会与 Redux store 绑定在一起,其中所定义的方法名将作为属性名,合并到组件的 props 中。如果传递的是一个函数,该函数将接收一个 dispatch 函数,然后由你来决定如何返回一个对象,这个对象通过 dispatch 函数与 action creator 以某种方式绑定在一起(提示:你也许会用到 Redux 的辅助函数 bindActionCreators())。如果你省略这个 mapDispatchToProps 参数,默认情况下,dispatch 会注入到你的组件 props 中。如果指定了该回调函数中第二个参数 ownProps,该参数的值为传递到组件的 props,而且只要组件接收到新 props,mapDispatchToProps 也会被调用。

    • mergeProps(stateProps, dispatchProps, ownProps): props

      如果指定了这个参数,mapStateToProps() 与 mapDispatchToProps() 的执行结果和组件自身的 props 将传入到这个回调函数中。该回调函数返回的对象将作为 props 传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props 来筛选部分的 state 数据,或者把 props 中的某个特定变量与 action creator 绑定在一起。如果你省略这个参数,默认情况下返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的结果。

    • options 如果指定这个参数,可以定制 connector 的行为。

      • pure = true

        如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比 mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。

      • withRef = false

        如果为 true,connector 会保存一个对被包装组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false

步骤

  • 使用Redux的createStore()创建一个包含了action和reducer的store
  • 将应用根组件嵌套到react-redux的 <Provider store> 组件中,从页实现绑定
  • 在应用根组件中使用react-redux的connect()实现连接,此时应用根组件的props可以接收到connect函数mapStateToProps参数中指定的store数据和方法

参考网站

  • http://cn.redux.js.org

解决php的xdebug和composer冲突

composer

Composer 是 PHP 的一个依赖管理工具。它允许你申明项目所依赖的代码库,它会在你的项目中为你安装他们

xdebug

Xdebug是一个开放源代码的PHP程序调试器(即一个Debug工具),可以用来跟踪,调试和分析PHP程序的运行状况

问题

如果php安装了xdebug扩展,当使用composer的时候,会有如下警告提示。提示信息里的连接给出了官方解决方案 (这个地址竟然也被天朝墙,还让不让干活了)

引起的主要问题就是当你使用composer的时候,下载速度会被得奇慢无比

You are running composer with xdebug enabled. This has a major impact on runtime performance. See https://getcomposer.org/xdebug

官方的链接给出了以下几个解决方案

  • 禁用xdebug,在*nix系统下,再利用别名来使得命令行直接调用php可加载xdebug。通过以下两步实现

    • php.ini配置里禁用xdebug

      1
      ;zend_extension = "/path/to/my/xdebug.so"
    • *nix系统里定义命令别名

      1
      2
      3
      4
      # 直接调用php命令时加载xdebug
      alias php='php -dzend_extension=xdebug.so'
      # PHPUnit需要使用xdebug.那么定义一个别名来处理
      alias phpunit='php $(which phpunit)'
  • 新建一个禁用了xdebug的xdebug-disabled-php.ini文件,还是使用别名,将composer命令重新定义

    1
    2
    3
    4
    # Without php.ini
    alias comp='php -n /path/to/composer.phar'
    # Or with an xdebug-disabled php.ini
    alias comp='php -c /path/to/xdebug-disabled-php.ini /path/to/composer.phar'

…官方给出了另外的复杂方案,太麻烦,就不贴了,有兴趣到这里自行查看

我的解决方案

针对Linux平台,因为我自己开发机器用的是php-fpm启动脚本

主要诉求是希望我机器上开机默认启动php的时候,能加载xdebug扩展,命令行下运行composer调用php命令的时候,不加载。解决思路是,在php.ini文件中禁用xdebug扩展,然后在php-fpm启动脚本中增加参数,加载xdeubg

具体操作就是直接修改启动脚本/etc/init.d/php-fpm下的参数,增加-dzend_extension=xdebug.so(注意,这里你可能需要传入xdebug.so文件的绝对路径)

1
php-fpm -dzend_extension=xdebug.so
12
sandy1890

sandy1890

12 日志
3 分类
10 标签
© 2017 sandy1890