git 是 Linus Torvalds 设计的源代码版本控制系统,以其方便简洁的使用,高效的操作,分布式的架构 已经替换掉了原来的SVN,成为了主流的版本控制系统。随着我对 git 的不断使用,我逐渐对 git 的原理产生了浓厚的兴趣,所以本文会着重分析 git 原理,并指导更加清晰、高效地使用 git。

git 数据类型

基本数据类型

  1. blob 存储 文件内容
  2. tree 存储目录下文件的文件名,权限
  3. commit 存储提交信息

blob 作为文件的基本元素,存储文件,blob一旦生成,不可修改。新添加的文件或者修改文件,会生成新的blob。tree表示文件目录,一个tree下面可以有tree和blob。一个commit对象存储提交信息,用户每次提交,都会形成新的tree,生成新的commit指向这个新tree。

空间维度 x 时间维度

在空间维度上,blob 和 tree 足够表达源代码的目录结构。但是对于版本管理系统来说,我们还需要记录每个提交点的快照(snapshot),所以需要时间维度的抽象数据结构,commit 就是时间维度上的数据结构,commit的之间的引用,也形成了一个时间线上 tree 结构(DAG,有向无环图)。

git objects database

content-addressed filesystem

git 是一个 content-addressed filesystem 基于内容寻址的系统。git的三种基本对象 blob,tree,commit,存储的位置都是基于内容的hash算出来的。

hash 生成方法

1
2
header = "<type> " + content.length + "\0"
hash = sha1(header + content)
  1. 生成header, 为 blob,tree,commit,三个种字符串,content.length 是内容(文件的内容,tree对象的内容,commit的内容)的字节长度,末尾加上 \0 字符和内容分开
  2. header + content 是二进制拼接,而不是文本拼接
  3. sha1 就是常用的hash 算法 SHA-1,生成40位hash值

对象存储

  1. 存储路径 .git/objects/hash[0, 2]/hash[2, 40] , 40位 hash 前两位为目录名,后38位为 文件名
  2. 存储的内容 是把字节内容经过zlib deflate 压缩后的结果

tag 和 branch

  1. tag 就是commit的别名,tag 指向的 commit 不变。
  2. branch 本质也是commit的别名,只是这个 commit 会一直跟着开发的提交在变。但是 branch 一直指向最顶端的 commit

tag 存储方式

运行

1
git tag firsttag

会在.git/refs/tags 生成 firsttag文件,文件内容存储的是 commit的hash

branch 存储方式

.git 目录中 .git/HEAD 文件存储的是当前用户工作的 branch

refs/heads/ 目录下存储以 branch 名字命名的文本文件,文件内容 存储的是 commit 的 hash。

通过 git show <commit_hash> 就可以查看 commit的 内容。

1
git show d8a5333173c2bf5de7853b11b24839715275cce2

  1. 用户从 master checkout 到 firstbranch, 会更新 .git/HEAD 文件的内容,写入内容 ref: refs/heads/firstbranch
  2. 用户在 firstbranch branch 做commit 操作,会更新 refs/heads/firstbranch 文件的内容,写进新的 commit hash

所以本质上branch 也是个 commit的别名,只是这个commit会随着开发提交而改变。

git 存储流程

一个commit 的最终分四种状态, 提交分为三个阶段

modified

所有修改完的代码,在你本机源代码的路径下,也就是你的workspace 里,目前和git 还没关系

added

1
git add xxx

跑完了 add 命名。这个阶段后,git才开始工作,你修改的文件 会生成 git 的 blob object 存储在 .git/objects

committed

1
git commit -m "xxx"

跑完 commit 命令,git生成 tree object,指向当前的 snapshot,生成 commit object 保存提交信息,并且这个commit是指向tree的。

pushed

git push 则是把修改真正的提交到远程仓库。

使用进阶

怎样恢复代码?

  1. 你代码commit 了吗?没有commit, 又没有手动备份,无法恢复。
  2. 你已经commit,但是找不到这个commit 了。执行 git reflog , 按照你操作的时间点排查,应该是哪个commit
1
git reflog

怎样回滚代码?

  1. 你提交到中心仓库了吗?如果提交了 你应该 git revert <commit-hash> 生成一个新的 reverted-commit,你再push这个commit 到线上

  2. 你没有提交到中心仓库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 回滚 commit
    git reset --hard HEAD~ # HEAD 指你当前branch的最上方一个commit,HEAD~ 是最上方开始的第二个commit,HEAD~~ 是最上方开始的第三个commit,以此类推。--hard 是指 不要commit 信息,也不要文件修改后的内容。整个这一句连起来就是 回滚到 让让前branch 删除 最新的一个提交
    git reset --soft HEAD~ # 只清除commit 信息,不回滚文件修改的内容

    # 回滚文件
    git checkout -- xx/xxx # 当一个文件为commit时,重置一个单独的文件, 回滚当前 branch的HEAD时的内容
    # 假设当前 branch 为 master
    git checkout master~ xx/xxx # 回滚文件 xx/xxx 到 HEAD 算起的 第二个commit
    git checkout master~2 xx/xxx # 回滚文件 xx/xxx 到 HEAD 算起的 第三个 commit

怎样排查是哪一个commit 引入了某个bug?

二分查找法

1
2
3
4
5
6
7
git bisect start [终点commit] [起点commit] # 执行后 当前branch 被reset到 两个commit的 最中间的那个commit
# 到达二分之一处,如果代码没有 bug,则执行 git bisect good
# 到达二分之一处,如果代码有 bug,则执行 git bisect bad
# 假设有bug
git bisect bad # 说明bug出现两个提交的前半段,git 会reset当前branch 到 前半段的 1/2 处 就是 最开始的 1/4 处
# 继续不断的 bisect bad / good 直到出现
# <xxx> is the first bad commit

怎样暂存当前的提交?

比如当你本地代码没改完,但是需要马上 pull 最新代码时,可以用

1
2
3
git stash # 暂存所有的修改,不commit
git pull # 拉取所有修改
git stash pop # pop出刚才的 暂存
1
git stash list # 查看 所有暂存的列表, 是一个stack 结构

怎样 merge?

假如面临这样一种情况, 你在你本机的 devel branch 开发好了两个 commit ,但是 你想 merge 到 master 时,发现别人也提交了一个commit,你要怎么merge?

直接 merge 不合并要 merge 的 commit,不改变原有 commit history

1
2
3
4
5
git checkout master
git pull
git merge devel
git branch -D devel
git push origin master

megrge with squash,把要 merge 的 commit,合并成一个commit history

1
2
3
4
5
git checkout master
git pull
git merge --squash devel
git branch -D devel
git push origin master

rebase 通过 rebase -i 调整 commit, 重新编辑 commit history

1
2
3
4
5
6
git checkout master
git pull
git checkout devel
git rebase -i master
git checkout master
git merge devel

总结

git的设计是精巧的,很值得学习。一个好的系统不仅是表象上功能强大,更是内部设计的精巧。这种精巧来源于对应用场景深入洞察,对各种数据结构、编程技术的巧妙应用。git 命令是很丰富的,一般我们用的比较多的是 high level 命令,其实 git 还有很多 low level 命令,多去探索,你也会发现很多乐趣。最后分享 Linus 的关于程序设计的一段话。

“Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”

–Linus Torvalds

参考文章

  1. 一文讲透 Git 底层数据结构和原理
  2. git architecture
  3. Git内部原理之Git对象哈希

软件包升级

1
2
3
4
5
6
7
8
9
10
"html-webpack-plugin": "^3.2.0",
"vue-loader": "^15.9.0",
"webpack": "^4.42.0",
"copy-webpack-plugin": "^5.1.1",
"file-loader": "^5.1.0",
"inline-manifest-webpack-plugin": "^4.0.2",
"svg-sprite-loader": "^4.2.1",
"terser-webpack-plugin": "^2.3.5",
"webpack-dev-middleware": "^3.7.2",
"webpack-dev-server": "^3.10.3"

CommonChunksPlugin升级

Webpack 4 兼容性升级,Webpack4 不再支持 CommonChunksPlugin这个插件了,使用了新的splitChunks

重要的区别是

  1. 新的配置放在了optimization下面了,不再放在plugins里
  2. 配置cacheGroups的逻辑和原来CommonChunksPlugin不完全一样,需要去理解文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
optimization: {
splitChunks: {
cacheGroups: {
uilib: {
name:'uilib',
chunks: 'initial',
test: /node_modules.\_?(mint-ui|cube-ui)/,
priority: -10,
},
vendor: {
name:'vendor',
chunks: 'initial',
test: /node_modules./,
priority: -20,
}
}
},
runtimeChunk: {
name: "manifest"
},
},

UglifyJSPlugin升级

Webpack 4 兼容性升级,Webpack4 不再支持UglifyJSPlugin,需要改为TerserPlugin

重要的区别是

  1. UglifyJSPlugin 不支持ES6了,所以用TerserPlugin
  2. TerserPlugin要放在 .optimization.minimizer 下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
config.optimization.minimizer.push(new TerserPlugin({
terserOptions: {
output: {
comments: false,
},
compress: {
collapse_vars: true,
reduce_vars: true
},
warnings: false,
},
parallel: true,
sourceMap: false,
cache: true,
}));

Webpack 4 添加mode

Webpack 4 兼容性升级,需要设置mode

正式包就用 production

1
2
3
4
5
6
7
8
9
const config = merge(baseConfig, {
name: plugin,
mode: 'production',
output: {
path: urls.moduleDistPath,
filename: "[name].[chunkhash:8].bundle.js",
chunkFilename: "[name].[chunkhash:8].bundle.js",
}
})

调试时用 development

1
this.config.mode = 'development';

Vue-Loader 升级

Vue-Loader 15 兼容性升级,需要显式导入 VueLoaderPlugin

必须在处理html之前,设置好VueLoaderPlugin去解析vue文件。https://vue-loader.vuejs.org/migrating.html#a-plugin-is-now-required

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: htmlPath,
inject: true,
chunks: ['uilib', 'vendor', 'manifest', 'app']
}),
new webpack.HashedModuleIdsPlugin(),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"windows.jQuery": "jquery"
})
],

Vue-Loader 15 兼容性升级,必须显式的指定less loader

相关文档 https://vue-loader.vuejs.org/migrating.html#loader-inference

1
2
3
4
5
6
7
8
9
10
11
12
13
{
test: /\.less$/,
use: [{
loader: "style-loader" // creates style nodes from JS strings
},{
loader: "css-loader",
options: {
esModule: false,
},
}, {
loader: "less-loader" // compiles Less to CSS
}]
}

Vue-Loader 14 兼容性升级,默认输入ES6 module,并且移除了esModule:false 支持

解决方案:各个Loader需要兼容esModule

针对Vue文件

对于Vue-Loader生成的 文件,需要静态替换源代码, 所以自己编写require替换插件 compatible-es-module.js 如下:

目的是把 require(‘./xx.vue’) 替换为 require(‘./xx.vue’).default。

1
2
3
4
5
6
7
8
module.exports = function(content) {
return content.replace(
new RegExp(/require\(('|")(.*)[/\w\.]+\.vue(.*)('|")\)(\.default)?/,'g'),
function(res) {
const result = /(\.default)$/.test(res)? res: (res + '.default');
return result;
});
};

当Vue文件引用Vue文件时:

1
2
3
4
5
test: /\.vue$/,
use: [
'vue-loader',
path.resolve(path.join(urls.localNodeModulePath, 'compatible-es-module')),
]

针对JS文件

1
2
3
4
5
6
7
8
9
10
11
12
13
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['es2015', 'stage-2'],
comments: true,
cacheDirectory: true
}
},{
loader: path.resolve(path.join(urls.localNodeModulePath, 'compatible-es-module')),
}
]

针对CSS文件

处理css esModule

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
{
test: /\.styl$|\.stylus$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
esModule: false,
},
},
'stylus-loader'
]
}, {
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
esModule: false,
},
},
"postcss-loader"
]
},

针对图片文件

处理图片 esModule

1
2
3
4
5
6
7
8
9
10
11
12
13
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: 'images/[hash:7].[name].[ext]',
esModule: false,
}
}
]
},

针对字体文件

处理字体 esModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
test: /\.ttf$/,
loader: 'url-loader',
options: {
//不设置大小,存为base64
name: 'font/[name].[ext]',
esModule: false,
}
}, {
test: /\.(eot|svg|woff|woff2)(\?\S*)?$/,
loader: 'file-loader',
options: {
esModule: false,
},
}]

升级后注意

webpack4 升级后。vue-loader 更改了模块导出格式。

从原来的CommonJS标准改为了ES2015 Module。ES2015 Module的关键字是import,CommonJS的关键字是require,而import 是JS语言的标准,require是社区的标准。

所以建议

  1. 尽量优先用 import。在确定要用require的时候用require。

  2. 需要require 一个vue 文件时,请务必加上后缀(为了兼容自定义的 compatible-es-module.js 插件)

    比如要require Test.vue 不要写成Vue.component(‘test’, require(‘./Test’)) 请写成 Vue.component(‘test’, require(‘./Test.vue’))

近一年陆续开发了一些 iOS 上的H5页面,总结一下遇到的兼容性问题。

常见类别总结

  1. iOS 对 fixed 布局支持不佳, 容易产生元素位置和光标位置异常
  2. overflow 为 scroll 的容器内 放置 input 或 textarea元素 产生光标消失和列表异常滚动问题
  3. iOS 和 Android 对 input 的键盘支持不一致

最易产生 bug 情形

input放置在一个 position: fixed 或者 overflow:scroll 的元素内部

问题列表

position: fixed 布局相关

input 标签放置在 fixed 布局的容器内部,产生光标问题

overflow: scroll相关

input 元素放在 overflow: scroll的元素内部, 产生光标错位

input 键盘类型相关

使用 input 的 pattern 显示九宫格键盘

其他

iOS 不支持 background-attachment: fixed

NPM

0. NPM 是什么

  1. JavaScript Package Manager
  2. 只是一个node.js 项目,npm lib 下的一个单独项目

1. NPM basic commands

列出所有可用命令

1
npm -l

查找 help

1
npm help <xxx>

npm install

1
2
3
4
5
npm install # install all base on package.json and package-lock.json
npm install <xxx> # default install
npm install <xxx> --save #npm 5.0.0 之后这个参数就不必要了
npm install <xxx> --no-save # install and not modify package.json
npm i <xxxx> --save # i same with install
1
2
npm unistall <xxx>
npm rm <xxx>

npm update

1
2
npm update #
npm update <xxx>

npm outdated

1
2
npm outdated 
npm outdated -g

npm config

1
2
3
4
npm config set registry https://registry.npm.taobao.org 
npm config list
npm config delete registry
npm config ls

npmrc

2. NPM package.json

描述信息类的

  1. name
  2. version
  3. description
  4. keywords

版本管理

  1. repository {type: , url: }

入口

  1. main (唯一必要的字段)

依赖库

  1. dependencies
  2. devDependencies

依赖库的格式

  1. 一个包含 package.json的文件夹
  2. 一个包含(1)的 gzipped 文件
  3. 一个 指向(2)的 url
  4. 一个 注册在 registry 的 <name>@<version> 标识,由这个标识可以找到(3)的 url
  5. 一个 指向 (4)的标识 <name>@<tag>
  6. 一个 tag 为 latest 指向(5)<name> 标识
  7. 一个 git repo 的 url,能clone 后内容为(1)的文件夹
git url 的格式

脚本

  1. scripts
1
2
3
4
5
6
"scripts": {
"init": "npm install --registry https://registry.npm.taobao.org",
"help": "node build/tools/help.js",
"route": "node build/router.js",
"new": "node build/tools/new.js"
},
1
2
3
npm help run # 所有的 script 都是被 run的,查看 run 的文档,可以知道详细用法
npm run start <xxx>
npm start <xxx> # 省略掉 run 也可以

3. NPM node_modules

什么是 node_modules?

一个可以被node.js require 函数引用的的文件夹或者文件。

满足 node_modules 的几种情况

  1. 一个包含 package.json 的文件夹,且 package.json 必须要有 main 字段
  2. 一个包含 index.js 的文件夹
  3. 一个 单个的 js 文件

4. 语义化版本号

主版本号.次版本号.修订号

  • 主版本号:当你做了不兼容的 API 修改,(major)
  • 次版本号:当你做了向下兼容的功能性新增,(minor)
  • 修订号:当你做了向下兼容的问题修正。(patch)

package.json 中指定版本

指定修订号不变

1
2
3
1.0 #不指定修订号,则随着 npm update 自动增加
1.0.x # 用 x 指代修订号
~1.0.4 # 用~ 符号来控制只更新修订号

指定次版本号不变

1
2
3
1 # 只指定主版本号
1.x # x 指代次版本号
^1.0.4 # ^ 符号控制次版本号变更

不限制版本号

1
2
* #是使用星好表示不限制
x #使用 x 表示不限制

npm version

1
2
3
npm version major # 为自己的库增加主版本号
npm version minor # 为自己的库增加次版本号
npm version patch # 为自己的库增加修订号

锁定当前项目的所有依赖的版本号

1
npm shrinkwrap #锁定后 npm install 不再去找更新了

Additionally, if both package-lock.json and npm-shrinkwrap.json are present in a package root, package-lock.json will be ignored in favor of this file.

npm-shrinkwrap.json

参考

语义化版本号
npm 语义化版本号文档
npm semver calculator

5. 其他常见问题

(1) node_modules 的查找顺序 的顺序

加入 用户的项目开发目录为 '/home/ry/projects/foo.js', 需要找 require 的为

  1. 先查找项目下的 node_modules
1
/home/ry/projects/node_modules/bar.js
  1. 再依次查找用户的目录
1
2
3
4
5
6
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
$HOME/.node_modules # $HOME变量下的目录
$HOME/.node_libraries
$PREFIX/lib/node # $PREFIX is Node.js's configured node_prefix.

Node.js modules

(2) Vue.js 使用 的 import from

  1. ./ ../ 开头的是文件夹路径
  2. 直接目录名或者文件名开头的,是注册了 webpack 的 reslove alias 的库

第三方库 通过 npm 安装到node_modules 下,所以 webpack 的配置文件里,并没有用 ./node_modules 开头,而是直接使用 vue 、vue-router等库的名字开头,不在 node_modules 目录下,则需要配置绝对路径。

总结就是 npm 下载 -》webpack 注册 -》vue 代码中再 import

JavaScript import

Webpack Relove alias

(3) Resolving EACCES permissions errors when installing packages globally

排查错误

1
2
3
4
10754 ◯ : which npm                                                                                                         
/Users/fosteryin/n/bin/npm
10764 ◯ : npm root
/Users/fosteryin/node_modules

解决方案:

  1. 使用一个 node.js version manager 来安装 node.js
  2. 手动修改安装路径到有权限的目录

Using a node version manager to install nodejs and npm

今天碰到个问题,要修改混淆过的js代码。index.min.js 代码长下面这个样子.

全是a b c d 一脸懵逼啊。

看了大半天实在没看得懂,尝试debugger,就是没进去。

然后,哈哈,我看到了一段可以代码,有了灵感,

代码竟然是eval,貌似真实的代码再最后一行的字符串里,哈哈。那就打印出eval的结果吧

1
2
3
4
5
6
7
while (c--) {
if (k[c]) {
p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c])
}
}
console.log(p) //我加的这句
return p

然后我就看下Chrome的Console,好像接近原始代码了

:):) 这不是没有混淆的代码吗?看到 Copy 按钮没?点一下

把代码复制到在线格式化的网站 http://tool.oschina.net/codeformat/js

代码就这样被格式化了。

然后把格式化的结果保存到文件index.js 里。让html引用这个文件。

跑跑试试。

然后就报错了

咿呀,缺啥啊?

我们再看看原始文件,找线索。

看看eval上有个分号。吼吼,这个文件莫不是拼接的吧?

好吧,我把混淆的文件index.min.js 上面的部分保存为swiper.min.js,再引入html。

然后呢,呵呵,然后就OK啦。

总结

今天一不小心黑科技了一把,不过也是有理由的:

  1. 利用好js的动态性,它是动态执行的,我就能看到动态的结果
  2. 要找到一个线索,顺杆爬,然后想想自己再正向写代码时会用哪些技术和方式操作,再去尝试。

大家在开发 iOS/macOS程序中,基本都是直接用 Xcode来开发,很少接触 xcodebuild,但是如果命令运行 xcode 的项目(比如自建 CI),那么 xcodebuild 是绕不过去的,目前网上关于 xcodebuild 的使用文章很多,但是详细理清的很少。本文尝试理清讲下xcodebuild 以飨来者。

查看手册

有任何 xcodebuild的使用问题, 搜索之后找不到答案的,建议看手册

1
$ man xcodebuild

安装命令行工具

xcodebuild是打包在Command Line Tools中, Xcode.app 依赖 Command Line Tools。如果没有安装该工具,则需要从Apple Developer网站下载

装好了后,可以运行which检查下。

1
2
$ which xcodebuild                                                                                                                     
/usr/bin/xcodebuild

查看版本号

由于 xcodebuild 和 XCode 往往是一对一版本绑定的,所以这个命令显示的是 XCode 的版本号

1
2
3
$ xcodebuild -version                                                                                                                   
Xcode 9.3.1
Build version 9E501

查看SDK

查看已安装的SDK

1
2
3
4
5
6
7
8
9
10
11
12
13
$ xcodebuild -showsdks                                                                                                                   {14:16}
iOS SDKs:
iOS 11.3 -sdk iphoneos11.3

iOS Simulator SDKs:
Simulator - iOS 11.3 -sdk iphonesimulator11.3

macOS SDKs:
macOS 10.13 -sdk macosx10.13

tvOS SDKs:
tv
...

详细版,可以看到 SDK 安装路径等信息

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
$ xcodebuild  -version -sdk                                                                                                              {14:16}
iPhoneOS11.3.sdk - iOS 11.3 (iphoneos11.3)
SDKVersion: 11.3
Path: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk
PlatformVersion: 11.3
PlatformPath: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform
BuildID: C689C3CA-2814-11E8-B440-EB6E943B87E0
ProductBuildVersion: 15E217
ProductCopyright: 1983-2018 Apple Inc.
ProductName: iPhone OS
ProductVersion: 11.3

iPhoneSimulator11.3.sdk - Simulator - iOS 11.3 (iphonesimulator11.3)
SDKVersion: 11.3
Path: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator11.3.sdk
PlatformVersion: 11.3
PlatformPath: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform
BuildID: C689C3CA-2814-11E8-B440-EB6E943B87E0
ProductBuildVersion: 15E217
ProductCopyright: 1983-2018 Apple Inc.
ProductName: iPhone OS
ProductVersion: 11.3

...

Xcode 9.3.1
Build version 9E501

编译

xcodebuild 是依赖于 Xcode 的project 文件的,所以它的 build 参数也是和 Xcode 上的操作一一对应。

项目级别的参数

参数 说明
-workspace 指定 workspace 文件
-project 指定 proj 文件
-target 指定 target
-scheme 指定 scheme

这四个概念大家在操作时容易囫囵吞枣地使用,下面稍作解释

  1. workspace 是一个项目的总文件,里面可以包含一个或者几个project, 一般大家会把项目依赖的 project,放在一个 workspace中,比如 Cocoapods 这个工具就把依赖放在了一个单独的叫Podsproject,让后通过 workspace 把项目组织在一起

  2. project 一个组织项目里代码和资源的文件。project 是必不可少,workspace是可选的。如果你只有一个project,并且不依赖其他project则是不需要workspace的。

  3. target 定义编译时需要哪些文件和资源,对环境有哪些要求,编译中要不要加入什么自定义的步骤。一个 project 可以有多个文件target, 比如在iOS和 macOS 共用代码的项目里,可以分别有 iOS 和 macOS 两个不同的target 每个 target 包含整个项目里自己需要的文件和设置。

  4. scheme 定义了你怎样使用target的方式,相当于在target外观有包了一层,属于定义target的外部环境。在什么环境下使用 target,是 Build,run,Test,Profile 等等。build 这个 target 要不要 Debug 的符号信息,跑起来编译好的二进制文件时,要不要带参数,带什么参数,二进制文件跑起来后应该生活在什么样的环境?系统语言是什么,Metal 要不要开启?二进制跑起来后要不要对程序的内存进行监控,比如Zombie Objects等等。
    因为scheme 是为target服务的,所以 Xcode 是创建target 是默认创建对应的scheme

好下面开始讲讲 xcodebuild 的用法

最简单的不要参数是可以直接工作的,他会找到默认的 workspaceprojectscheme

1
$ xcodebuild

一般情况我们明确指定,workspacescheme这两个基本参数就可以,没有 workspace 的则指定project

1
xcodebuild -workspace TestProject.xcworkspace -scheme TestProject

配置级别的参数

这一级别的参数,是为了覆盖项目文件已经设置的值

参数 说明
-sdk
-configuration
-destination
-arch

以上仅列出,具体可参考man page,深入看看细节

打包

1
xcodebuild -exportArchive -archivePath <xcarchivepath> -exportPath <destinationpath> -exportOptionsPlist <plistPath>

archive path 和 destination path只需要制定文件夹路径就可以。plist path 需要制定一个定制话的plist 来定义导出什么样的package,最简单的如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>teamID</key>
<string>MYTEAMID123</string>
<key>method</key>
<string>app-store</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>

其他

xcodebuild本身是系统黑盒工具,我们不能方便的完全掌控。 但是关于自动话build这个话题,还有很多开源的build工具,比如xctool,fastlane,我们可以很方便的使用和定制,后面有机会再讲。

前言

wordpress是应用很广泛的CMS系统。无论是用作博客或者是安装自定义的主题来建站,用起来都是很简单,而且自定义主题的开发也容易上手。近期因工作需要开发一个wordpress的主题来建站,发现wordpress代码比较老,需要兼容的情况很多。API一致性很差,经常做同一个功能有多个相似的api,而且名字看起来差不多,但参数却差别很大。这些都还算好的,遇到一个比较大的问题是,wordpress里必须配置主站的域名(也就是在wp-admin需要配置的url),而且mysql的数据里充斥着hard code的url。这给数据迁移和反向代理带来了很大的不便。这次是由于安全的原因,我只能把wordpress假设在反代后面,就这么一个小小的改变,我发现wordpress死活水土不服。

下面记录下配置的要点,方便以后查看。我使用的服务器是nginx,反向代理的服务器也是nginx。

一般情况

一般情况我们架设wordpress 的结构是这样的:

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
                    HTTP请求
+
|
+--------------------------------------------+
| | |
| +-----------------v----------------+ |
| | | |
| | Nginx | |
| | | |
| +-----------------+----------------+ |
| | |
| +-----------------v----------------+ |
| | | |
| | wordpress | |
| | | |
| | | |
| | | |
| | | |
| +-----------------+----------------+ |
| | |
| +-----------------v----------------+ |
| | mysql | |
| | | |
| +----------------------------------+ |
| |
| |
+--------------------------------------------+

而配置域名方面也比较简单,主要是两处

  1. nginx 的配置 nginx.conf
1
server_name www.example.com
  1. wordpress wp-admin中的设置

wordpress域名设置

其实这第二步就稍显多余,为什么wordpress不能都使用相对路径?如果大家多次修改后wp-admin进不去,可参见附录直接修改数据库。

反向代理的情况

这个时候wordpress的架构是这样:

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
                    HTTP请求
+
|
|
|
+--------------------------------------------+
| | |
| +-----------------+---------------+ |
| | Nginx Reverse Proxy | |
| | | |
| +-----------------+---------------+ |
| | |
+--------------------------------------------+
|
|
|
|
+--------------------------------------------+
| | |
| +-----------------v----------------+ |
| | | |
| | Nginx | |
| | | |
| +-----------------+----------------+ |
| | |
| +-----------------v----------------+ |
| | | |
| | wordpress | |
| | | |
| | | |
| | | |
| | | |
| +-----------------+----------------+ |
| | |
| +-----------------v----------------+ |
| | mysql | |
| | | |
| +----------------------------------+ |
| |
| |
+--------------------------------------------+

理论上我不就是在外面加了个反向代理,我就把两个nginx的配置改下应该就可以了吧?

  1. Nginx Reverse Proxy
1
2
3
4
5
6
7
8
9
10
server_name www.example.com

location / {
proxy_pass http://<Wordpress-Server-IP>:<Port>

#Proxy Settings
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
  1. wordpress 服务器的Nginx 配置
1
2
listen <Port>
server_name localhost

可惜只是理论上应该这样。只配置了这两个,wordpress 是仍然不能工作的。经过反复调试,不是反向代理服务器返回500错误,就是wordpress的redirect不正常。

经过多次搜索研究,还要配置下面两步

  1. 清理mysql的数据

    把hard code的数据url 给改成过来,确保不存在域名不正确的url。比如: localhost:<Port>/xxx 这样的url在数据库, 都需要改成 www.example.com/xxx。大家可以使用Search-Replace-DB这个工具,来替换数据库。

  2. 改PHP代码手动设置反向代理的HOST,因为wordpress的核心开发者说:让wordpress支持反向代理不是他们的责任

    核心思想就是要让 PHP代码知道反向代理转发的域名是什么,然后强制地在每个页面里动态设置。

第一步 Nginx Reverse Proxy 配置改为

1
2
3
4
5
6
7
8
9
10
11
server_name www.example.com

location / {
proxy_pass http://<Wordpress-Server-IP>:<Port>

#Proxy Settings
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

加了X-Forwarded-For

第二步 修改wp-config.php的代码,添加如下部分

1
2
3
if ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) {
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST'];
}

修改了以上的内容,wordpress 的反向代理才能真正工作起来。

总结

综上所述:让wordpress支持反向代理一共有五步

  1. 反向代理服务器需要配置域名,并且需要配置X-Forwarded-For
1
2
3
4
5
6
7
8
9
10
11
server_name www.example.com

location / {
proxy_pass http://<Wordpress-Server-IP>:<Port>

#Proxy Settings
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
  1. wordpress的服务器,配置server_name 为localhost 和端口号。
1
2
listen <Port>
server_name localhost
  1. wordpress 的wp-admin里要设置成域名,而不是localhost或者wordpress server的IP。

wordpress域名设置

  1. 要在wp-config.php修改里 HTTP_HOST
1
2
3
if ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) {
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST'];
}
  1. 【可选的】用Search-Replace-DB来检查替换数据库,确保数据库里不存在域名不正确的url。

附录

在wp-admin进不去的情况下,操作 wp_options 数据库表, 来更新wordpress的域名

1
2
3
4
5
msyql -u xxx -p # 连接数据库。
> use wordpress; # 使用数据库
> update wp_options SET option_value='http://www.example.com' WHERE option_name='home';
> update wp_options SET option_value='http://www.example.com' WHERE option_name='siteurl';
> quit

近期接手到公司一些项目是使用SVN管理代码,对于我这样一个git命令行的重度使用者,实在是颇感不便。比较了svn的命令行和和客户端,发现svn的客户端是比较友好的。但是我是个命令使用者,怎么能用客户端呢?(捂脸,主要是没钱买个好用的mac客户端)。所以就乖乖总结下svn命令行的使用。

下面对标git常用命令,看下在svn里是怎么实现相应的功能。

下载代码

git

1
git clone git://gcc.gnu.org/git/gcc.git

svn

1
svn checkout svn://gcc.gnu.org/svn/gcc/trunk gcc

比较总结

git clone的是下载的是一个仓库的所有branch和tag的元数据,同时下载了master branch的所有文件,然后checkout到master branch。svn checkout则是checkout trunk目录下所有的文件。所以同一个项目里.svn 文件夹是要比.git文件夹小。

查看日志

git

1
git log

svn

1
svn log

比较总结

git log默认是输出到pager里的,但是svn log是不通过pager的,一股脑输出所有信息。因此只要日志超过一页,git log里用户最先看到的较新的提交记录,svn log里用户最先看到的是较旧的提交记录,会有些不自然。

可以在shell里给svn log 也默认加个pager,比如在zsh里给svn log加pager

1
2
3
4
5
6
7
8
svn() {
if [ "$1" = "log" ]
then
command svn "$@" | less -FX
else
command svn "$@"
fi
}

查看当前状态

git

1
git status

svn

1
svn status

比较总结

对于 svn status的结果解释可以查看书籍Subversion 版本控制中的示例:

如果在工作副本的根目录不加任何参数地执行 svn status, Subversion 就会检查并报告所有 文件和目录的修改.

1
2
3
4
5
6
7
$ svn status
? scratch.c
A stuff/loot
A stuff/loot/new.c
D stuff/old.c
M bar.c
$

在默认的输出模式下,其中最常的几种字符或状态是:

? item

文件, 目录或符号链接 item 不在版本 控制的名单中.

A item

文件, 目录或符号链接 item 是新增的, 在下一次提交时就会加入到仓库中.

C item

文件 item 有未解决的冲突, 意思是说从 服务器收到的更新和该文件的本地修改有所重叠,    
Subversion 在处理 这些重叠的修改时发生了冲突. 用户必须解决掉冲突后才能向仓库 提交修改.

D item

文件, 目录或符号链接 item 已被删除, 在下一次提交时就会从仓库中删除 item.

M item

文件 item 的内容被修改.

git status 则更直观一些,没有使用字母的缩写。比如书籍Pro git中的示例:

1
2
3
4
5
6
7
8
9
10
11
12
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: README
modified: CONTRIBUTING.md

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

modified: CONTRIBUTING.md

获取更新

git

1
git pull

svn

1
svn update

添加文件

git

1
git add xxx

svn

1
svn add xxx

提交修改

git

1
2
git commit -m "xxx"
git push origin

svn

1
svn commit -m "xxx"

比较总结

因为git是分布式,本地有全量的仓库信息,所以设计上git commit是commit到本地的repo,git还需要通过push推送其他服务器,比如公司的中心服务器。svn commit是直接提交到中心服务器的。

查看diff

git

1
git diff HEAD

svn

1
svn diff

比较总结

我现在使用的是最常用的一种情况,比较当前未提交的修改跟上一个commit的区别。git要指定当前修改与哪个commit比较(比如HEAD),svn则不用。

revert 文件

git

1
2
git checkout xxxfile # revert 一个文件
git reset --hard # revert 当前所有修改

svn

1
2
svn revert xxxfile # revert 一个文件
svn revert xxxDirectory -R # revert 文件夹下所有修改

比较总结

对于revert文件,svn命令看起来要自然些,git用checkout表示使用该文件最新commit中的版本,自然的本地为提交的修改就被revert了。对于revert当前所有的修改,git 可以通过reset来实现。svn 则可以通过revert当前文件夹下所有为提交的修改来实现。另外git可以本地提交或者stash来暂存修改,而svn中没有。

创建分支

git

1
git branch xxx

svn

1
svn copy http://example.com/repos/myproject/trunk http://example.com/repos/myproject/branches/releaseForAug -m 'create branch for release on August'

比较总结

git中创建branch是元数据的修改,不需要拷贝项目文件。svn中创建branch是把项目文件和再拷贝一份,同时把新文件对应到旧的元数据上去。

创建tag

git

1
git tag xxx

svn

1
svn copy http://example.com/repos/myproject/trunk http://example.com/repos/myproject/tags/releaseForAug -m 'create branch for release on August'

比较总结

git中创建tag是元数据的修改,只生成一个commit的引用。svn中创建tag和branch是一样的,都是复制。

结语

svn和git从设计上就有根本的不同,svn是集中式的,git是分布式。比如svn的copy有点像git的fork,但是由于svn是集中式,所以svn的copy只能在同一个repo下进行,这就限制了svn项目的分享,这也解释了为什么git出现后,github出现了。不过虽然两个项目设计上有如此大的不同,但是从开发流程上,我们仍然可以使用svn做我们想做的各种事情,比如提交,查看log,branch,tag等,所以svn还能用。两个不同的项目,殊途同归吧。

当我们开始学习一门新技术的时候,我们往往是从配置开发环境开始的,而也就是这一步,总有些难迈过。我们配置环境时,往往不熟悉这门技术,所以才要去配置环境来开发学习。所以这个配置的过程,总是会带有稀里糊涂“折腾”的成分。

折腾有时是乐趣,有时却是沮丧。所以折腾不一定必要,为了避免不必要的折腾,我总结了一些配置开发环境的经验,以供参考。

第一步 明确配置什么

配置wordpress的主题开发环境。

第一步很简单,但是要明确,避免你配置环境时,配着配着,因为某些问题越走越远。

第二步 找清依赖

配置环境,其实就是配置依赖。 你在用别人开发好的工具或者库时你要明确,你要用的技术到底依赖哪些工具和库。这一步最好看官方文档。wordpress 是基于php开发的,所以肯定需要php,那还需要什么呢?wordpress 是一个web程序,所以我们需要一个server。还有呢?

根据文档我们需要以下几个组件:

  • PHP解释器

  • Web Server

  • Database

知道要什么组件后,但是方案有很多种类。 由于每个人对某个领域的经验不一样,配好后要用在的环境不一样,所以技术上选择的粒度控制会不一样
比如PHP, 安装时需不需要关注PHP版本,用PHP5.6 还是PHP7.0,有没有什么PHP的plugin需要安装? 所以这个没有统一的答案。

但是技术选择的基线要搞清楚,防止自己犯低级错误。比如wordpress 4.9 对PHP的最低的版本要求是多少?database选择Mysql的最低版本多少?选择MariaDB作为wordpress数据库,有特别要注意的地方吗?

而且不同组件之间的关系要搞清楚,他们是怎么协同工作的,怎么通信。比如PHP 和 Web Server 是怎么协同工作的?都装上去就行吗?

我准备安装wordpress的当前最新版本4.9.4,查看文档发现wordpress对环境的兼容性很好, 所以对各个组件的版本要求很低,并且他有推荐的配置。

所以安装组件最低要求是:

  • PHP 5.2.4以上

  • MySQL 5.6 以上或者 MariaDB 10.0 以上

  • 推荐使用Apache 和 Nginx

由于我有配置Apache 和Nginx的经验,我感到这两个软件,配置时还不够方便,所以我选择了使用足够简单的且稳定的Caddy。而且我要配置的是本地开发环境,对稳定性要求没那么高,所以我会倾向于选择较新的配置,这样可以使用较新的软件特性。

我的配置是:

  • PHP 7.0

  • MariaDB 10.0

  • Caddy 最新版

我是在macOS下开发,有一个好用的程序管理器是很有必要的比如homebrew这个类似于Ubuntu下apt的管理程序软件。

没有安装homebrew的运行下面的命令来安装

1
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

第三步 逐个安装,逐步验证

好,开始正题

安装 PHP

1
brew install php70 	--with-fpm

–with-fpm 是什么稍后解释。

测试PHP

1
php -v

MariaDB

1
brew install mariadb

测试 MariaDB

1
2
mysql.server start
mysql -u root -p

也可以用brew services,像使用linux中的systemctl一样方便开启服务

1
brew services start mariadb

测试PHP和MariaDB协同工作

创建数据库

1
2
3
4
mysql -u root -p
mysql
>CREATE DATABASE wordpress;
>quit

使用PHP测试脚本连接数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
# Fill our vars and run on cli
# $ php -f db-connect-test.php
$dbname = 'wordpress';
$dbuser = 'root';
$dbpass = '';
$dbhost = 'localhost';
$link = mysqli_connect($dbhost, $dbuser, $dbpass) or die("Unable to Connect to '$dbhost'");
mysqli_select_db($link, $dbname) or die("Could not open the db '$dbname'");
$test_query = "SHOW TABLES FROM $dbname";
$result = mysqli_query($link, $test_query);
$tblCnt = 0;
while($tbl = mysqli_fetch_array($result)) {
$tblCnt++;
#echo $tbl[0]."<br />\n";
}
if (!$tblCnt) {
echo "There are no tables<br />\n";
} else {
echo "There are $tblCnt tables<br />\n";
}

安装Caddy

Caddy的安装很简单,甚至不需要安装,只需要下载即可。下载时选择操作系统,插件和License,下载Caddy的zip文件,解压之后,里面有个caddy的binary文件,这个binary文件就是Caddy的全部,这也是使用它的简单之处。

你有两种方式使用这个caddy binary

  1. 把caddy放到系统的PATH目录下,这样可以全局访问到这个binary
  2. 放到项目的文件夹下直接使用。

我选择2。

第四步 遇到问题,理清组件职责

进行到这一步大部分软件都安装完了,但是wordpress还是没办法工作。有很多问题缠绕着我们?而且主要是出在Caddy的配置上。我们先把问题理清楚列出来。理清组件的职责,是要调整哪些组件,哪些组件时无关的。

  1. wordprss代码放在哪里?
  2. 用Caddy怎么配置wordpress?

一个一个解决:

  1. wordpress代码放在哪儿?

    这个很简单,直接建folder就行

    1
    2
    3
    4
    5
    mkdir wordpress-caddy-template # 假设是wordpress-caddy-template你的工作目录s
    cd wordpress-caddy-template
    wget https://cn.wordpress.org/wordpress-4.9.4-zh_CN.zip
    unzip wordpress-4.9.4-zh_CN.zip
    rm wordpress-4.9.4-zh_CN.zip

    这时wordpress就在 wordpress-caddy-template/wordpress 下面。

  2. 用Caddy怎么配置wordpress?

    要回答这个问题,还得往下分析,得想清楚Caddy是干什么的。

    1. 接收url请求(我们的网站url是什么?)
    2. 根据url请求找到PHP代码的。(PHP代码在哪里?)
    3. 运行PHP代码(Caddy怎么运行PHP?)
    4. 返回PHP代码的运行结果

Caddy的配置只需要一个文件,如下的Caddyfile可以解决上述1 和 2:

1
2
localhost:9998
root wordpress/

对于3,Caddy怎么运行PHP代码?Caddy并不能直接运行PHP代码,Caddy通过PHP-FPM和PHP解释器进行进程间通信,Caddy把PHP代码通过PHP-FPM发给PHP解释器,PHP解释器解释好PHP代码后,把解释的结果通过PHP—FPM再发给Caddy。所以要安装PHP-FPM

默认PHP-FPM是和PHP一起安装,如果你像我一样安装到一半才意识到没有安装PHP-FPM,可以先卸载php70,再重新安装带PHP-FPM的php70

1
2
3
4
brew uninstall php70
rm -rf /usr/local/etc/php/7.0
brew doctor # 查看是否有问题
brew install php70 --with-fpm

安装好后,开启php-fpm后台运行

1
brew services start php@7.0

进程间通信可以通过socket通信,也可以通过端口通信. 那macOS系统下PHP-FPM的默认方式时是什么呢? PHP-FPM的配置文件是/usr/local/etc/php/7.0/php-fpm.d/www.conf,它的相关配置如下

1
listen = 127.0.0.1:9000

所以加上了PHP-FPM的Caddyfile配置如下

1
2
3
localhost:9998
root wordpress/
fastcgi / 127.0.0.1:9000 php

这就是Caddy的配置了,最基本的只需要三行。

第五部 集成测试,优化配置

目前整个项目的目录如下

1
2
3
-rw-r--r--  1 fosteryin staff       59 Apr  5 16:18 Caddyfile
-rwxr-xr-x 1 fosteryin staff 16737696 Apr 5 15:34 caddy
drwxr-xr-x 21 fosteryin staff 672 Feb 8 12:53 wordpress

运行起来也很简单

1
./caddy

由于我想开发wordpress主题,所以我关心的只是主题的代码,所以我想把自己的代码和wordpress框架隔离,但是默认情况下主题代码是放在wordpress/wp-content/themes下的,这样每次找都不方便。而且我想用git来对代码进行版本管理,这个时候把自己的代码放在 wordpress文件夹下的子文件夹就更不方便了。

所以我在最外层目录新建了我的主题文件夹,并且把它软链接到wordpress的的主题文件夹中。

1
2
3
mkdir example-theme
cd wordpress/wp-content/themes
ln -s ../../../example-theme

使用git管理文件,并设置.gitignore 来隔离自己的代码和wordpress的代码。

1
2
3
4
cd ../../..
git init
touch .gitignore
open .gitignore

我ignore掉wordpress和caddy这个binary,caddy这种大一点binary也不适合放在git中管理。.gitignore 文件内容如下:

1
2
wordpress/
caddy

现在工作目录下所有文件为:

1
2
3
4
5
6
7
8
9
10
drwxr-xr-x 10 fosteryin staff      320 Apr  5 16:33 .
drwxr-xr-x 5 fosteryin staff 160 Apr 5 15:34 ..
-rw-r--r-- 1 fosteryin staff 6148 Apr 5 16:22 .DS_Store
drwxr-xr-x 9 fosteryin staff 288 Apr 5 16:35 .git
-rw-r--r-- 1 fosteryin staff 16 Apr 5 16:33 .gitignore
drwxr-xr-x 3 fosteryin staff 96 Apr 5 16:25 .idea
-rw-r--r-- 1 fosteryin staff 59 Apr 5 16:18 Caddyfile
-rwxr-xr-x 1 fosteryin staff 16737696 Apr 5 15:34 caddy
drwxr-xr-x 2 fosteryin staff 64 Apr 5 16:30 example-theme
drwxr-xr-x 21 fosteryin staff 672 Feb 8 12:53 wordpress

这样一个基本的wordpress开发环境就搭建成功了。

下面给Caddyfile添加一些log设置和wordpress常用的permalinks。

1
2
3
4
5
6
7
8
9
10
localhost:9998
root wordpress/
fastcgi / 127.0.0.1:9000 php

rewrite {
if {path} not_match ^\/wp-admin
to {path} {path}/ /index.php?{query}
}

errors errors.log

.gitignore改为

1
2
3
wordpress/
caddy
*.log

使用Caddy是不是很简单? 所有代码在github上, 有兴趣可以下载wordpress-caddy-template。真正配置环境时,不一定需要这么一板一眼的分析配置,大家搜索一下教程,然后不停的运行命令就可以了。然而当大家真的遇到问题,遇到教程里没讲到的问题时,这上面的分析思路就很有用了,只有定位到问题本身才能解决问题,而我们往往看到的是问题的表象。

缘由

现在NoSQL流行,有一个原因也是因为不需要去刻意处理table的schema,直接存储数据,这样简单!所以也不会有数据库表的迁移问题。数据库表迁移这一块儿一直是一个麻烦点,但我最近用了sqlite3做了个小项目,所以总结下数据库迁移的方案。

原理

  1. 每一次数据表改动,都对应一个数据库版本号
  2. 数据迁移是渐进式的,比如把数据库版本从1 升级到n,那么就升级n-1次,版本1到2,2到3,直到n-1到n。

实施

  1. 使用sqlite3的user_version 存贮自定义的数据库版本

    1
    2
    3
    4
    /*设置版本号*/
    PRAGMA user_version=1;
    /*读取版本号*/
    PRAGMA user_version;
  2. 所有的数据库升级文件,放在一个文件中,都直接使用sql文件,方便直接查看管理。文件结构如下

    文件结构设计

    1. v1.sql v2.sql, v3.sql等 是每个数据库版本,完整的数据库定义文件
    2. v1tov2.sql, v2tov3.sql等 是间隔版本数据库升级文件。一个数据m到n升级的过程就是,运行 v[m]tov[m+1].sql, v[m+1]tov[m+2].sql, 直到 v[n-1]tov[n].sql
    3. run.sh 就是每次要跑的数据迁移脚本,包括了当前的版本号和迁移逻辑
    4. 其中的v2.sql 到v[n].sql 不是必须的,只是为了方便查看当前最新的数据表设计,如果存在v[n].sql 那么创建新数据库也可以直接从这个文件来创建
  3. 迁移脚本如下, 具体逻辑注释中已经写明

  4. v[n].sql 和v[n-1]tov[n].sql 文件的最后都去需要通过user_version来设置数据版本为n,一个v2tov3.sql 的demo如下:

总结

使用场景

目前这套方案适合数据量小,对停机维护可以接受的业务情况,因为需要停机升级,但是这个方案,足够简单清晰且能满足所有不同版本间的数据升级。

不足与展望

  1. 这个方案没有考虑到数据升级失败的回滚。由于是小业务,所以考虑更多的是简单易维护。所以针对这种情况的,首先要保证升级脚本经过了足够的线上数据测试,经的起考验。其次,一旦发生问题,线上可以直接操作维护,写脚本。这样说,因为你都没有测试到这种需要rollback的case,你也不能在写升级脚本的时候,知道这种case是怎样,所以提前写好rollback的逻辑不一定可行或者合适。
  2. 部署时,数据迁移之前要备份,数据量较大些,用增量备份,节省时间。备份有成熟的工具,而且备份方便升级失败时rollback。部署的步骤应该是: 拉代码build(或者拉docker镜像)-> 备份数据库 -> 升级数据库 -> 跑新的代码
  3. 对于Android,iOS的设备中使用sqlite3的情况,数据迁移的逻辑是一样。sql文件结构设计可以重用,也可以写到代码里去管理。迁移脚本需要转换成native的Java或者Objective-C,Swift的代码。
  4. 对于更大的业务,多实例的的数据库迁移可以使用Flyway