react storybook使用typescript

在前面的文章有介绍在storybook中使用antd
之前开发js是使用的flow来作类型检查,最近想尝试一下typescript。
如果是从头创建一个空的typescript项目则相对就比较简单,但是现在我们是需要将之前的create-react-app项目迁移到typescript。

在create-react-app中添加typescript

参考 https://facebook.github.io/create-react-app/docs/adding-typescript
react-scripts从2.1.0版本开始就支持typescript了,这里我们先将项目的react-scripts依赖升级到最新版本:

1
yarn add --exact react-scripts@2.1.8

然后再添加typescript依赖:

1
yarn add typescript @types/node @types/react @types/react-dom @types/jest

然后将 src/index.js 更名为 src/index.tsx
接着创建 tsconfig.json 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"compilerOptions": {
"module": "esnext",
"target": "es5",
"lib": ["es5", "es6", "es7", "es2017", "dom"],
"sourceMap": true,
"allowJs": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"skipLibCheck": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve"
},
"include": [
"src/**/*"
]
}

在storybook中添加typescript

参考 https://storybook.js.org/docs/configurations/typescript-config/
同样也先将storybook升级到最新版:

1
yarn add --dev --exact @storybook/addons@5.0.1 @storybook/react@5.0.1

然后再添加依赖:
1
yarn add --dev awesome-typescript-loader @types/storybook__react @storybook/addon-info react-docgen-typescript-webpack-plugin ts-jest

接着修改webpack配置,对应.storybook/webpack.config.js文件:

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
const path = require("path");
const antdTheme = {
'@primary-color': '#846bc1',
}

module.exports = ({ config, mode }) => {
config.module.rules.push({
test: /\.less$/,
use: [{
loader: "style-loader"
}, {
loader: "css-loader"
}, {
loader: "less-loader",
options: {
modifyVars: antdTheme, // 如果要自定义主题样式
javascriptEnabled: true
}
}]
});
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve('awesome-typescript-loader'),
options: { configFileName: path.resolve(__dirname, './tsconfig.json') }
});
config.resolve.extensions.push('.ts', '.tsx');

return config;
};

以及babel配置,对应.storybook/babelrc.js文件:

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
module.exports = function(api) {
api.cache.forever();
return {
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"browsers": [
">1%",
"last 4 versions",
"Firefox ESR",
"not ie < 11"
]
}
}
],
"@babel/preset-react"
],
"plugins": [
[
"import",
{
"libraryName": "antd",
"style": true
}
]
]
}
}

同时修改storybook配置.storybook/config.js,让其支持.tsx文件:
将之前的:

1
const req = require.context('../src/components', true, /\.stories\.js$/)

修改为:
1
const req = require.context('../src/components', true, /\.stories\.[jt]sx?$/)

最后再创建 .storybook/tsconfig.json 文件:

1
2
3
4
5
6
7
8
{
"extends": "../tsconfig",
"compilerOptions": {
"jsx": "react",
"isolatedModules": false,
"noEmit": false
}
}

至此一切都完成了。

运行storybook时若出现错误: Error: Cannot find module '@emotion/core/package.json',则手动安装一下: yarn add --dev @emotion/core

在树莓派3上运行Android6

最近需要在 android 上进行测试,无奈在电脑上使用模拟器运行速度太慢了。
正好我还有几个闲置的树莓派3设备,于是就尝试在树莓派上运行android看看。

刻录系统到SD卡

1.先下载 android6 的树莓派镜像: https://pan.baidu.com/s/1YHrmjN3be7UaLAdBJr-YhQ

2.下载完成后解压,然后开始执行写入操作: sudo dd if=andrpi3-20160626.img of=/dev/disk3 bs=4096000

这里我用的读卡器,被识别为 disk3。文件比较大,请耐心等待。我写入差不多花了一个小时的时间。

运行系统

将SD卡插回到树莓派中,并接上鼠标、键盘、显示器,然后开机。
这里稍微注意一下,树莓派3需要 5V/2A 的电源才能工作。
刚开始我试了一下开不了机,结果才发现是电源电压不足。

运行效果如图:

连接上网络之后使用 adb 进行操作: adb connect 192.168.0.101
这里我的树莓派的ip为 192.168.0.101

在storybook中使用antd

最近一个 react 的项目有用到了 antd 这个 ui 库。这里作个笔记记录一下如何在 storybook 中显示 antd 的组件。

项目是使用 create-react-app 创建的,项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
├── .storybook/
├── README.md
├── antd-theme.js
├── node_modules
├── package.json
├── public
└── src
├── App.js
├── components
│   ├── button.js
│   └── button.stories.js
├── index.js
└── registerServiceWorker.js

这里创建了一个 button 组件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/components/button.js
import React, { Component, Fragment } from 'react';
import { Button } from 'antd';

class ButtonGhost extends Component {
render() {
return (
<Fragment>
<h3 className="ex-title">Ghost Button</h3>
<div style={{ background: 'rgb(47, 45, 165)', padding: '26px 16px 16px' }}>
<Button type="primary">Primary</Button>
<Button className="ml20" ghost>Default</Button>
<Button className="ml20" type="dashed" ghost>Dashed</Button>
<Button className="ml20" type="danger" ghost>danger</Button>
</div>
</Fragment>
);
}
}

export default ButtonGhost;

对应的 storybook 案例如下:

1
2
3
4
5
6
7
// src/components/button.stories.js
import React from 'react';
import { storiesOf } from '@storybook/react';
import Button from './button';

storiesOf('General', module)
.add('Button', () => <Button />)

然后 storybook 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
// .storybook/config.js
import React from 'react'
import { configure } from '@storybook/react'
import { ThemeProvider } from 'styled-components'

const req = require.context('../src/components', true, /\.stories\.js$/)

function loadStories() {
req.keys().forEach((filename) => req(filename))
}

configure(loadStories, module)

然后运行 storybook : start-storybook -p 6006 -c .storybook,效果如下:

这是由于 antd 的 css 没有加载,因此所有按钮的样式都没有。
参考 https://ant.design/docs/react/use-with-create-react-app-cn 的说明,修改 babel 和 webpack 的配置。
参考 https://storybook.js.org/configurations/custom-webpack-config 的说明,修改 storybook 的 webpack 配置。

创建文件 .storybook/.babelrc

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
{
"presets": [
[
"env",
{
"modules": false,
"targets": {
"browsers": [
">1%",
"last 4 versions",
"Firefox ESR",
"not ie < 11"
]
}
}
],
"react",
"stage-3"
],
"plugins": [
[
"import",
{
"libraryName": "antd",
"style": true
}
]
]
}

创建文件 .storybook/webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: [{
loader: "style-loader"
}, {
loader: "css-loader"
}, {
loader: "less-loader",
options: {
//modifyVars: antdTheme, // 如果要自定义主题样式
javascriptEnabled: true
}
}]
}
]
}
};

然后在 .storybook/config.js 文件添加 import 'antd/dist/antd.less';
最后的效果如下:

使用python编写游戏修改器

最近比较怀旧,在玩一个比较老的PC游戏。由于游戏难度太高了,于是就打算自己写一个修改器。
通过查阅资料,在 Windows 下的修改器主要需要用到四个函数:OpenProcess, CloseHandle, WriteProcessMemory, ReadProcessMemory。

这几个都是C++的函数,在Python中可以通过ctypes来直接调用。
然后接下来介绍一下基本的操作流程。

1.通过 任务管理器 或者其他方式得到需要修改的游戏进程。然后通过 OpenProcess 注入该进程。

1
2
3
4
5
6
7
8
9
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_OPERATION = 0x0008
PROCESS_VM_READ = 0x0010
PROCESS_VM_WRITE = 0x0020

hProcess = ctypes.windll.kernel32.OpenProcess(
PROCESS_QUERY_INFORMATION|PROCESS_VM_READ|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,
False, pid
)

2.然后可以通过 ReadProcessMemory 来扫描游戏的内存找到需要修改的数值项的内存地址。

1
2
3
4
5
6
7
8
9
buf = ctypes.c_int32()
nread = ctypes.c_size_t()
ret = ctypes.windll.kernel32.ReadProcessMemory(
hProcess,
base_addr,
ctypes.byref(buf),
ctypes.sizeof(buf),
ctypes.byref(nread)
)

这里是读取 base_addr 地址之后的4个字节的内容。可以通过循环来遍历游戏的内存,找到需要修改的地址。
当然,为了方便也可以直接使用 Cheat Engine 之类的软件来查找,然后把找到的内存地址记录下来即可。

3.得到需要修改的内存地址之后,就可以 WriteProcessMemory 来修改该地址保存的值。

1
2
3
4
5
6
7
8
9
buf = ctypes.c_int32(value)
nread = ctypes.c_size_t()
ret = ctypes.windll.kernel32.WriteProcessMemory(
hProcess,
base_addr,
ctypes.byref(buf),
ctypes.sizeof(buf),
ctypes.byref(nwrite)
)

这里是往 base_addr 这个地址写入值为 value 的4字节内容。

4.最后如果不再需要修改了的话,就通过 CloseHandle 关闭该注入操作。

1
ctypes.windll.kernel32.CloseHandle(hProcess)

以上都是针对 Windows 系统的,对于 Linux 系统的话 可以通过 ptrace (http://man7.org/linux/man-pages/man2/ptrace.2.html) 操作实现。由于我没有 Linux 的游戏就没有研究了。

上面修改器的完整源代码,如有需要可通过以下链接获取:
https://github.com/wusuopu/cheat_engine_caesar3

使用pytest测试flask应用

python 本身就有 unittest 单元测试框架,但是觉得它并不是很好用,我更倾向于使用 pytest 。

下面通过一个例子来介绍如何使用 pytest 对 flask 应用进行单元测试。

首先新建一个 flask 应用,并针对根路径创建一条路由。代码如下:

1
2
3
4
5
6
# server.py
app = flask.Flask(__name__)

@app.route('/')
def home():
return 'ok'

然后针对首页编写单元测试,代码如下:

1
2
3
4
5
6
# tests/test_app.py

def test_home_page(client):
rv = client.get('/')
assert rv.status_code == 200
assert rv.data == b'ok'

然后执行命令运行该测试用例: pytest -s tests/test_app.py

在 pytest 中编写测试用例就只需要新建一个以 test_ 开头的函数即可。

以上是针对flask路由作的最基本测试。接下来编写一个新的路由,该页面只有用户登录之后才能访问。代码如下:

1
2
3
4
5
6
# server.py
@app.route('/member')
@flask_security.decorators.login_required
def member():
user = flask_security.core.current_user
return str(user.id)

要对该路由进行测试,则需要先创建一个用户。

1
2
3
4
5
6
7
8
9
# tests/test_app.py
def setup_module(module):
App.testing = True
fixture.setup()


def teardown_module(module):
"""
"""

上面的 setup_moduleteardown_module 函数分别是在所有的测试用例执行之前与执行之后执行。在这里我们通过 setup_module 在执行测试之前先创建一个用户。然后再创建一个 pytest 的 fixture:
1
2
3
4
5
6
7
8
# tests/conftest.py

@pytest.fixture
def auth_client(client):
with client.session_transaction() as sess:
sess['user_id'] = str(fixture.users[0].id)

yield client

这里创建了一个 auth_client fixture,之后所有以 auth_client 发起的请求都是登录状态的。

最后再针对 /member 路由编写两个测试用例,分别是未登录状态与登录状态下的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test_member_page_without_login(client):
"""
没有登录则跳转到登录页面
"""
rv = client.get('/member')
assert rv.headers['Location'] == 'http://localhost/login?next=%2Fmember'
assert rv.status_code == 302


def test_member_page_with_login(auth_client):
"""
已经登录则返回当前用户id
"""
rv = auth_client.get('/member')
assert rv.status_code == 200
assert rv.data.decode('utf8') == str(fixture.users[0].id)

以上就是一个简单的 flask 应用了。但是有时一个稍微复杂一点的应用会用到一些第三方的api。这时针对这种情况编写测试用例时就需要用到 mock 功能了。再编写一个新的路由页面:

1
2
3
4
5
6
7
8
# server.py

@app.route('/movies')
def movies():
data = utils.fetch_movies()
if not data:
return '', 500
return flask.jsonify(data)

1
2
3
4
5
6
7
8
9
# utils.py

def fetch_movies():
try:
url = 'http://api.douban.com/v2/movie/top250?start=0&count=1'
res = requests.get(url, timeout=5)
return res.json()
except Exception as e:
return {}

请求该路由会返回豆瓣top250的电影信息。然后再编写两个测试用例分别模拟api调用成功与失败的情况。

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
# tests/test_app.py
def test_movies_api(client):
"""
调用豆瓣api成功的情况
"""
fetch_movies_patch = mock.patch('utils.fetch_movies')

func = fetch_movies_patch.start()
func.return_value = {'start': 0, 'count': 0, 'subjects': []}

rv = client.get('/movies')
assert rv.status_code == 200
assert func.called

fetch_movies_patch.stop()


def test_movies_api_with_error(client):
"""
调用豆瓣api出错的情况
"""
fetch_movies_patch = mock.patch('utils.fetch_movies')

func = fetch_movies_patch.start()
func.return_value = None

rv = client.get('/movies')
assert rv.status_code == 500
assert func.called

fetch_movies_patch.stop()

这里使用 python 的 mock 模块来模拟让某个函数返回固定的结果。

完整的代码请访问: https://github.com/wusuopu/flask-test-example

HAProxy+Nginx+gunicorn获取真实ip

之前在部署在 nginx + uwsgi 应用时都是通过如下方法来获取真实的客户端ip的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
upstream app_server {
server unix:///tmp/gunicorn.sock fail_timeout=0;
}

server {
listen 80;
server_name localhost;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;

proxy_pass http://app_server;
}
}

这样在 uwsgi 的应用程序中只需要读取 http headers 中的 X-Forwarded-For 字段即可。

但是最近由于运维架构的是采用 haproxy + nginx + uwsgi 是形式,导致了在 uwsgi 应用程序中获取到的 ip 都是 haproxy 的。
为了要获取到真实的ip地址,需要由 haproxy 将 ip 传给 nginx,再由 nginx 传给 uwsgi。
在网上搜索了半天 haproxy 的相关配置,感觉太复杂了。因此还是决定从 nginx 入手。

经过实验将 nginx 的配置改为如下即可:

1
2
3
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;

在Docker中运行X11程序

如果是Linux系统的话,相对比较方便。先构建一个带gui各应的docker image,然后将
本机的X11 sock挂载到container内,
docker run -ti --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix chrome

如果是mac OS系统的话,相对麻烦一些。

1.安装所需的软件:

1
2
brew install socat
brew cask install xquartz

2.依次运行刚刚安装的两个程序:

1
2
socat TCP-LISTEN:6000,reuseaddr,fork UNIX-CLIENT:\"$DISPLAY\"
open -a XQuartz

3.设置X11, XQuartz -> Preference -> Security -> Allow connections from network clients

4.docker run --rm -e DISPLAY=ifconfig | grep “inet\ “ | tail -1 | cut -d “ “ -f 2:0 chrome

对于已经启动了的container,可以在container内执行命令 export DISPLAY=<ip>:0 来设置 DISPLAY 从而使用本机的 X11 服务。

倘若当前你的 mac OS 没有连接网络,那么可能就没有ip地址供container内访问。
此时也许可以执行命令: sudo ifconfig lo0 alias 10.200.10.1/24 来手动设置一个ip。
然后在container内再设置 export DISPLAY=10.200.10.1:0

由octpress迁移到了hexo

这两天将博客的系统由原来的ruby octpress 迁移到了 nodejs hexo。

决定要进行迁移主要是两个原因:
1.使用 octpress 来生成页面感觉越来越慢了;
2.octpress 的页面样式表不知怎么的突然就坏掉了,整个页面显示都不正常了。这个是最主要的原因。

迁移的工作还算是比较顺利,只是体验了一下 hexo 感觉 bug 也不少。只得自己写些 patch,然后将就着用吧。

通过串口连接raspberry pi

最近在玩树莓派,有时没有网络,也没有显示器,此时如果想要连接树莓派执行一些操作的话会很麻烦。
因为之前玩过 ARM 的开发板编程,因此想能不能通过串口登录到 pi 呢。于是网上查了一下,还真的可以哦。
以下就作为备忘笔记记录一下操作过程。

1.首先需要一根 USB 转串口的线,如果没有的话可以去某宝上买一根吧,反正也不贵。我选的是 PL2303。
再根据系统以及芯片的不同而下载安装不同的驱动程序。 对于 mac OS 用户执行命令: ls /dev/ | grep tty.usb
如果驱动都安装正确的话应该是会有输出结果的。

2.然后在 pi 的系统上启用 serial。我安装的是 debian 系统,执行命令: sudo raspi-config
选择 advanced options -> serial 进行启用 serial。

3.串口连接

对于 Raspberry Pi3 的 GPIO 引脚如下:
pi3_gpio

串口线与 pi 的连接方式为: GND -> GND, RXD -> TXD, TXD -> RXD, 如图:
pi3-board

最后在电脑上使用串口连接软件进行连接,对应的串口设置为 115200 8N1,如图:
raspberry-pi-serial

参考资料: http://elinux.org/RPi_Serial_Connection

使用CodePush对ReactNative进行热更新

CodePush是微软提供的可用于对 Cordova 和 ReactNative 进行代码热更新的库。
在其官方的文档中已经写得很详细了,按照其说明来配置即可。我这里只是对在使用过程中遇到的一些坑作为总结。

创建应用

首先注册一个账号并创建一个 CodePush 的应用:

1
2
3
npm install -g code-push-cli
code-push register
code-push app add <appName>

安装配置CodePush

按照说明 https://github.com/Microsoft/react-native-code-push#getting-started,使用 rnpm 进行安装即可:

1
2
npm install --save react-native-code-push
rnpm link react-native-code-push

安装完成之后还需要再进行一些配置,对于 iOS 需要将 AppDelegate.m 文件中的 jsCodeLocation 修改为: jsCodeLocation = [CodePush bundleURL];
同时再在 Info.plist 文件中添加一项 CodePushDeploymentKey,其值为 CodePush 应用的 Deployment Key。

对于 android 需要在 MainActivity 类的 getPackages 方法中设置 Deployment Key。同时根据 ReactNative 的版本不同而使用不同的方法来设置 getJSBundleFile
参考: https://github.com/Microsoft/react-native-code-push#android-setup

程序更新

在安装、配置完成之后,即可以使用CodePush进行程序的更新操作了。
根据官方的说明只需要调用 CodePush.sync() 即可完成自动更新操作。
我针对自己的情况再进行封装了一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function syncCodePush() {
NetInfo.fetch().done(
(reach) => {
// 检查网络环境
if (_.includes(['wifi', 'WIFI', 'VPN'], reach)) {
CodePush.sync().done(
() => {
// 检查更新成功
},
(err) => {
// 更新失败
}
);
}
}
);
}

以上函数是保证只有在wifi的网络环境下才进行更新操作,同时由于 CodePush.sync() 返回的是一个 Promise 对象,
在这里我就遇到了由于网络异常而下载出错,从而导致 app 崩溃。因此需要处理 reject 的情况。

有时在程序更新之后的首次运行时可能会需要作一些迁移的操作,这里可以使用 getUpdateMetadata 来检查程序是不是首次运行。

1
2
3
4
5
6
7
Codepush.getUpdateMetadata().then(
(update) => {
if (update && update.isFirstRun) {
// 首次运行执行一些操作
}
}
).done( callback );

发布更新

在 app 发布安装包发布出去之后,已经有用户下载安装了。此时如果再有 js 代码更新或者图片文件的改动的话,可以使用 CodePush 进行发布。
进入 ReactNative 的项目根目录,使用 code-push 命令进行发布更新。例如:

1
code-push release-react DemoApp ios -m -d Staging --des "更新描述" -t "~2.0.0"

以上命令是发布一个紧急更新到 Staging ,只有 ios appp 的版本为 2.0~3.0 的才会下载该更新包。
由于是紧急更新,app在下载安装完成之后会自动重启应用该更新包。否则的话就需要用户下次手动启动app时该更新包才会生效。

在 CodePush 中针对 ios 和 android 可以共用一个应用,只是我个人感觉这样在管理 deployment history 时不太方便。
因此我通常会创建两个应用,例如: DemoApp-iOS、DemoApp-Android 这样的。

需要注意的是,由于 CodePush 的 server 是在国外,因此下载的速度会比较慢。

最后我自己使用 Electron + Vue.js 开发了一个 CodePush 的简易管理工具,https://github.com/wusuopu/code-push-gui
可以对 CodePush 的 app 跟 deployment 进行简单的管理。