SegmentFault 全栈编程最新的文章
2023-10-08T23:55:00+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
简单、稳定、概念前卫且易看懂的JSON库——QJSON
https://segmentfault.com/a/1190000044283281
2023-10-08T23:55:00+08:00
2023-10-08T23:55:00+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<h2>QJSON</h2><h3>介绍</h3><p>QJSON 是 <a href="https://link.segmentfault.com/?enc=UBBg%2FB7maD%2BFq7cGfSN%2BgA%3D%3D.AK3YGVT7A0kp7J1ce4C14RkxikdEP7IXHqVnYwqpgC3g%2BO9kgsI7fPa%2B89NnKs3n" rel="nofollow">ZJSON</a>的替代库。 <br>ZJSON已经开发出来有一段时间了,也进行了一些应用,效果还不错,但现在存在些问题。</p><ul><li>字符串解析为json对象时,当初借鉴于json11,一直没时间去换成状态机模式</li><li>没有进行大规模数据验证</li><li>大量使用递归算法,没时间组织测试</li><li>C++要求至少为 C++17 版本</li></ul><p>因此花了一个国庆假期,封装QT5:Core中的相关Json库,保持与ZJSON相同的外部接口,以求解决以上问题。</p><h3>设计思路</h3><p>简单的接口函数、简单的使用方法、增加支持链式操作。 <br>使用模板技术,使用给Json对象增加值的方法只需要一个 --- add。底层使用QT5:Core中的相关Json库,保证稳定性,C++版本要求高于c++11。 <br>使用简单,项目为单文件结构,只需要引入qjson.h,并链接上QT5:Core即可。 <br>完全符合Json标准,对外一个Json对象,要么为Object,要么为Array。但Json对象实际上可以有 纯值 类型(包括Error类型),这是为了适应链式操作的场景,并且让查找等操作可以统一返回Json对象,让所有的操作尽量达到一致性。</p><h3>项目进度</h3><p>项目目前完成大部分主要功能,具体情况请看任务列表,同时支持windws、linux和mac主流操作系统。 <br>任务列表:</p><ul><li>[x] 构造函数(Object & Array)</li><li>[x] 构造函数(值)</li><li>[x] JSON字符串反序列化构造函数</li><li>[x] 复制构造函数</li><li>[x] initializer_list构造函数</li><li>[x] 析构函数</li><li>[x] operator=</li><li>[x] operator==</li><li>[x] operator!=</li><li>[x] operator[]</li><li>[x] contains</li><li>[x] getValueType</li><li>[x] getAllKeys</li><li>[x] add(为Json对象增加子对象,为数组快速增加元素)</li><li>[x] toString(生成json字符串)</li><li>[x] toInt、toDouble、toFalse 等值类型转换</li><li>[x] toVector 数组类型转换</li><li>[x] isError、isNull、isArray 等节点类型判断</li><li>[x] parse, 从json字符串生成Json对象</li><li>[x] extend Json - 扩展对象</li><li>[x] concat Json - 数组扩展</li><li>[x] push_front - 数组压入队首</li><li>[x] push_back - 数组压入队尾</li><li>[x] pop_front - 数组弹出队首</li><li>[x] pop_back - 数组弹出队尾</li><li>[x] take - 获取并删除</li><li>[x] first - 获取数组第一个</li><li>[x] last - 获取数组最后一个</li><li>[x] slice - 数组取出子数组</li><li>[x] insert - 数组插入</li><li>[x] clear - 清空</li><li>[x] Remove - 删除</li><li>[x] RemoveFirst - 删除数组第一个</li><li>[x] RemoveLast - 删除数组最后一个</li><li>[x] std::move语义</li></ul><h4>Json 节点类型定义</h4><p>(内部使用,数据类型只在Json类内部使用)</p><pre><code>enum Type {
Error, //错误,查找无果,这是一个无效Json对象
False, //Json值类型 - false
True, //Json值类型 - true
Null, //Json值类型 - null
Number, //Json值类型 - 数字,库中以double类型存储
String, //Json值类型 - 字符串
Object, //Json类对象类型 - 这是Object嵌套,对象型中只有child需要关注
Array //Json类对象类型 - 这是Array嵌套,对象型中只有child需要关注
};</code></pre><h4>Json 节点定义</h4><pre><code>class Json {
Type type;
QJsonDocument* _obj_; //data of object or array
QString vdata; //data of number or string
}</code></pre><h3>接口说明</h3><p>公开的对象类型,json只支持Object与Array两种对象,与内部类型对应(公开类型)。</p><pre><code>enum class JsonType
{
Object = 6,
Array = 7
};</code></pre><h3>编程示例</h3><p>简单使用示例</p><pre><code> Json subObject{{"math", 99},{"str", "a string."}}; //initializer_list方式构造Json对象
//initializer_list方式构造Json对象, 并且可以嵌套
Json mulitListObj{{"fkey", false},{"strkey","ffffff"},{"num2", 9.98}, {"okey", subObject}};
Json subArray(JsonType::Array); //数组对象以initializer_list方式增加元素
subArray.add({12,13,14,15}); //快速生成 [12,13,14,15] array json
Json ajson(JsonType::Object); //新建Object对象,输入参数可以省略
std::string data = "kevin";
ajson.add("fail", false) //增加false值对象
.add("name", data) //增加字符串值对象
.add("school-en", "the 85th."); //链式调用
ajson.add("age", 10); //增加number值对象,此处为整数
ajson.add("scores", 95.98); //增加number值对象,此处为浮点数,还支持long,long long
.add("nullkey", nullptr); //增加null值对象,需要送入nullptr, NULL会被认为是整数0
Json sub; //新建Object对象
sub.add("math", 99);
ajson.addValueJson("subJson", sub); //为ajson增加子Json类型对象,完成嵌套需要
Json subArray(JsonType::Array); //新建Array对象,输入参数不可省略
subArray.add("I'm the first one."); //增加Array对象的字符串值子对象
subArray.add("two", 2); //增加Array对象的number值子对象,第一个参数会被忽略
Json sub2;
sub2.add("sb2", 222);
subArray.addValueJson("subObj", sub2); //为Array对象增加Object类子对象,完成嵌套需求
ajson.addValueJson("array", subArray); //为ajson增加Array对象,且这个Array对象本身就是一个嵌套结构
std::cout << "ajson's string is : " << ajson.toString() << std::endl; //输出ajson对象序列化后的字符串, 结果见下方
string name = ajson["name"].toString(); //提取key为name的字符串值,结果为:kevin
int oper = ajson["sb2"].toInt(); //提取嵌套深层结构中的key为sb2的整数值,结果为:222
Json operArr = ajson["array"]; //提取key为array的数组对象
string first = ajson["array"][0].toString(); //提取key为array的数组对象的序号为0的值,结果为:I'm the first one.</code></pre><p>mulitListObj序列化后结果为:</p><pre><code>{
"fkey": false,
"strkey": "ffffff",
"num2": 9.98,
"okey": {
"math": 99,
"str": "a string."
}
}</code></pre><p>ajson序列化后结果为:</p><pre><code>{
"fail": false,
"name": "kevin",
"school-en": "the 85th.",
"age": 10,
"scores": 95.98,
"nullkey": null,
"subJson": {
"math": 99
},
"array": [
"I'm the first one.",
2,
{
"sb2": 222
}
]
}</code></pre><p>详情请参看相应的测试用例代码。</p><h3>项目地址</h3><pre><code>https://gitee.com/zhoutk/qjson
或
https://github.com/zhoutk/qjson</code></pre><h3>运行方法</h3><p>该项目在vs2019, gcc7.5, clang12.0下均编译运行正常。</p><pre><code>git clone https://github.com/zhoutk/qjson
cd qjson
cmake -Bbuild .
---windows
cd build && cmake --build .
---linux & mac
cd build && make
run qjson or ctest</code></pre><h3>相关项目</h3><blockquote><a href="https://link.segmentfault.com/?enc=8wK1lvJJRJQNdLGcUlKVew%3D%3D.lNZe7zGCjC6vm4zyKVbrCvnlEZfkyzTH%2F6%2B4yiU%2Fja67GIdi87src2Y0z%2BXuKKow" rel="nofollow">zjson</a> (与本项目保持相同的接口,可互换替换)</blockquote><pre><code>https://gitee.com/zhoutk/zjson
或
https://github.com/zhoutk/zjson</code></pre>
node.js基于 cmake-js 进行插件开发实战
https://segmentfault.com/a/1190000043263956
2023-01-07T09:18:36+08:00
2023-01-07T09:18:36+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<p>以前工作在node.js环境下,做微服务产品; 三年前转回到C++环境,已经有一些代码积攒。我将以往基于node.js与C++的相关项目结合起来(C++代码以addon插件嵌入),实现了一个微服务快速(rest api service)开发框架。该框架以关系数据库为基础,现在支持(mysql、sqlite3、postgres),同时支持windows, linux, macos。本文以该项目为蓝本,来说明使用C++为node.js开发插件的实践经验。</p><h2>项目结构</h2><ul><li>addon : C++插件封装代码目录,这是一个node.js与C++的适配器,具体的C++功能都在thirds目录中</li><li>src : node.js源码目录,一套完整的智能微服务代码,基于关系数据,提供标准的rest api service, 不需要写一行代码,详见 <a href="https://link.segmentfault.com/?enc=sA%2B48WusvIP1BHGWy8aysw%3D%3D.4wZ9%2FrFKeU6VL%2B1IWCdUDcj9CWGsBPU0uhM7rXakjA0%3D" rel="nofollow">gels项目</a></li><li>test : 单元测试代码目录,提供了全面测试,同时也是很好的示例代码</li><li><p>thirds : C++项目都放在这个目录下</p><pre><code>|-- CMakeLists.txt
|-- addon //c++插件封装
| |-- export.cc
| |-- index.cc
| `-- index.h
|-- package.json
|-- src //node.js核心源码
| |-- config //只列出了目录
| |-- dao
| |-- db
| |-- inits
| |-- middlewares
| `-- routers
|-- test //rest api 测试
| `-- test.js
|-- thirds //依赖的c++项目
|-- package.json
`-- tsconfig.json</code></pre></li></ul><h2>Nodejs扩展基本开发</h2><h3>编译扩展,两种方式</h3><ul><li>node-gyp</li><li>cmake-js</li></ul><h3>开发环境</h3><p>因为,本人的C++项目都使用cmake进行项目管理的,所以我选择使用cmake-js来进行node.js的扩展开发。开发环境:</p><ul><li>windows : cmake > 3.18, node.js >= 16, visual studio >= 2019; 若使用vs2022, windows SDK 必须安装10.的版本,只装11版本的话,编译会出错</li><li>linux : cmake > 3.18, node.js >= 16, gcc >= 7.5</li><li>macOs : cmake > 3.18, node.js >= 16, clang >= 12</li></ul><h3>项目依赖</h3><p>项目依赖,请参看package.json中相关小节,与插件开发相关的主要是以下三个项目:</p><ul><li>cmake-js</li><li>bindings</li><li>node-addon-api</li></ul><h3>CMakeLists.txt关键点说明</h3><p>完整的代码请自行到项目中去获取,我再这里只是节选,并进行一些说明</p><blockquote>c++版本指定,因为依赖库Zjson最低需要c++17</blockquote><pre><code>set (CMAKE_CXX_STANDARD 17)
SET(CMAKE_CXX_FLAGS "-D_GLIBCXX_USE_CXX17_ABI=0")</code></pre><blockquote>windows必须增加如下的参数设定,必须将动态链接库的内存与主程序融合</blockquote><pre><code>set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd")</code></pre><blockquote>linux下的特殊要求, 其它环境不设这个变量,在链接的时候就只有linux会加上 dl这个参数</blockquote><pre><code>set(dlLinkParam dl)
...
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB} ${MysqlDll} ${pqName} ${sqliteName} ${dlLinkParam})</code></pre><blockquote>cmake-js最基本的编译设定</blockquote><pre><code>set(NODE_LINK_LIBS "")
set(NODE_EXTERNAL_INCLUDES "")
FILE(GLOB_RECURSE SOURCE_FILES "./addon/*.cc")
FILE(GLOB_RECURSE HEADER_FILES "./addon/*.h")
add_library(${PROJECT_NAME} SHARED ${HEADER_FILES} ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
message("-------- CMAKE_JS_INC -------" ${CMAKE_JS_INC})
# Include Node-API wrappers
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/node_modules/node-addon-api
${CMAKE_SOURCE_DIR}/node_modules/node-addon-api/src
${CMAKE_JS_INC})
target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB})</code></pre><p><strong>注: sqlite3 必须以动态链接库的形式接入,直接将.c和.h加入到主程序中,能编译通过,也能运行,但查询系统表的时候会出现异常</strong></p><h3>插件开发代码解析</h3><blockquote>addon 目录下是与C++项目适配的代码,C++的功能,先写成cmake管理的项目,放到thirds目录,再适配进addon插件,这样能做到相对的独立<br>一般需要三个文件:export.cc, index.h, index.cc</blockquote><p><strong>export.cc</strong></p><pre><code>#include "index.h"
//导出接口
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
return Zorm::Init(env, exports);
}
NODE_API_MODULE(Zorm, InitAll)</code></pre><p><strong>index.h</strong></p><pre><code>#include <napi.h> //node.js插件开发头文件
#include "Idb.h" //数据库通用接口头文件
class Zorm : public Napi::ObjectWrap<Zorm>{
public:
//导出函数
static Napi::Object Init(Napi::Env env, Napi::Object exports);
static Napi::FunctionReference constructor;
//构造函数,生成一个orm对象,保存到 成员变量 db 中
Zorm(const Napi::CallbackInfo& info);
//公用的类方法,要实现数据库通用接口的所有方法适配
Napi::Value select(const Napi::CallbackInfo& info);
...
private:
ZORM::Idb* db; //成员变量
};</code></pre><p><strong>index.cc</strong></p><blockquote>初始化函数,定义所有成员方法</blockquote><pre><code>Napi::Object Zorm::Init(Napi::Env env, Napi::Object exports)
{
Napi::HandleScope scope(env);
Napi::Function func =
DefineClass(env, "Zorm", //除了这个函数,其它基本都是规定写法
{ //定义外部能调用的所有成员方法
InstanceMethod("select", &Zorm::select),
...
});
constructor = Napi::Persistent(func);
constructor.SuppressDestruct();
exports.Set("Zorm", func);
return exports;
}</code></pre><blockquote>构造函数适配</blockquote><pre><code>
Zorm::Zorm(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Zorm>(info), db(nullptr)
{
int len = info.Length();
Napi::Env env = info.Env();
if (len < 2 || !info[0].IsString()) { //函数参数解析,json对象我都用字符串进行传递;二进制使用Napi::Array jsNativeArray接收C++的char*
Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException();
}
std::string dbDialect = info[0].As<Napi::String>().ToString();
std::string opStr = info[1].As<Napi::String>();
ZJSON::Json options(opStr);
db = new ZORM::DbBase(dbDialect, options);
}</code></pre><blockquote>成员方法示例</blockquote><pre><code>Napi::Value Zorm::select(const Napi::CallbackInfo& info)
{
int len = info.Length();
Napi::Env env = info.Env();
if (len < 1 || !info[0].IsString()) {
Napi::TypeError::New(env, "String expected").ThrowAsJavaScriptException();
}
std::string tableName = info[0].As<Napi::String>().ToString().Utf8Value();
ZJSON::Json params;
if(len >= 2){
params.extend(ZJSON::Json(info[1].As<Napi::String>().ToString().Utf8Value()));
}
std::string fieldStr;
if(len >= 3){
fieldStr = info[2].As<Napi::String>().ToString().Utf8Value();
}
ZJSON::Json rs = db->select(tableName, params, ZORM::DbUtils::MakeVector(fieldStr));
return Napi::String::New(info.Env(), rs.toString());
}</code></pre><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/zrest
或
https://github.com/zhoutk/zrest</code></pre><h2>安装运行</h2><ul><li><p>新建配置文件,./src/config/configs.ts, 指定数据库:</p><pre><code>export default {
inits: {
directory: {
run: false,
dirs: ['public/upload', 'public/temp']
},
socket: {
run: false
}
},
port: 12321,
db_dialect: 'sqlite3', //数据库选择,现支持 sqlite3, mysql, postgres
db_options: {
DbLogClose: false, //是否显示SQL语句
parameterized: false, //是否进行参数化查询
db_host: '192.168.0.12',
db_port: 5432,
db_name: 'dbtest',
db_user: 'root',
db_pass: '123456',
db_char: 'utf8mb4',
db_conn: 5,
connString: ':memory:', //内存模式运行
}
}</code></pre></li><li><p>在终端(Terminal)中依次运行如下命令</p><pre><code>git clone https://gitee.com/zhoutk/zrest
cd ztest
npm i -g yarn
yarn global add typescript eslint nodemon
yarn
tsc -w //或 command + shift + B,选 tsc:监视
yarm configure //windows下最低vs2019, gcc 7.5, macos clang12.0
yarn compile //编译c++插件, 若有问题,请参照 [Zorm](https://gitee.com/zhoutk/zorm) 文档,特别是最后的注释
yarn start //或 node ./dist/index.js
export PACTUM_REQUEST_BASE_URL=http://127.0.0.1:12321
yarn test //运行rest api接口测试,请仔细查看测试文件,其中有相当完善的使用方法
//修改配置文件,可以切换不同的数据,运行测试;使用mysql或postgres时,请先手动建立dbtest数据,编码使用Utf-8</code></pre></li><li>测试运行结果图<br>测试运行输出<br><img src="/img/bVc5G56" alt="" title=""><br>项目日志(包括请求和sql语句)<br><img src="/img/bVc5G57" alt="" title=""></li></ul><h2>相关项目</h2><ul><li><a href="https://link.segmentfault.com/?enc=Gul75uOYZNg6zdwa2anabg%3D%3D.xn1hd2QudJuXXZ0SWr7pZEiJSb8xydXHE1hjoaonIOg%3D" rel="nofollow">Zrest</a> node.js嵌入c++插件项目,实现跨平台多数据库无缝切换的微服务开发框架</li><li><a href="https://link.segmentfault.com/?enc=muRWcqTyU4ULt9zdJkVIqA%3D%3D.jNETLMdXemphoxH3LugG2cuBzLLhg1yU4TB47QSL918%3D" rel="nofollow">gels</a> node.js项目,基于koa2实现的rest api服务框架,功能齐全; 以gels为入口,实现本项目,c++项目以插件方式集成</li><li><a href="https://link.segmentfault.com/?enc=QMYOF2it0Q4oYMy3ncWFKg%3D%3D.0oHqExrDthbNq%2Fs1OZyMb1by%2FwAFV53frSx5UATtQGU%3D" rel="nofollow">Zjson</a> c++项目,实现简单高效的json处理</li><li><a href="https://link.segmentfault.com/?enc=CHzvKNwrX1pgEhCEJn0Nvg%3D%3D.DuSci2iJiJrYY2eQnYmQi%2Bdv6z1BOpfn%2FmUuxHBoBGk%3D" rel="nofollow">Zorm</a> c++项目,以json对象为媒介,实现了一种ORM映射;设计了通用数据库操作接口规范,能无缝的在多种数据库之间切换</li></ul>
通用ORM的设计与实现
https://segmentfault.com/a/1190000042513101
2022-09-20T09:00:53+08:00
2022-09-20T09:00:53+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>介绍</h2><p>我们通用的ORM,基本模式都是想要脱离数据库的,几乎都在编程语言层面建立模型,由程序去与数据库打交道。虽然脱离了数据库的具体操作,但我们要建立各种模型文档,用代码去写表之间的关系等等操作,让初学者一时如坠云雾。我的想法是,将关系数据库拥有的完善设计工具之优势,来实现数据设计以提供结构信息,让json对象自动映射成为标准的SQL查询语句。只要我们理解了标准的SQL语言,我们就能够完成数据库查询操作。</p><h2>依赖关系</h2><p>本项目依赖 本人的 另一个项目 Zjson,此项目提供简洁、方便、高效的Json库。该库使用方便,是一个单文件库,只需要下载并引入项目即可。具体信息请移步 <a href="https://link.segmentfault.com/?enc=xzdXYytUzYHtJz5cOaK6Rw%3D%3D.ywCChLci%2F5OT2pxroIrM6C1FBweKlyP7nl0SmfV%2Fof6LBWQya31kfkYqwIuv6Gh4" rel="nofollow">gitee-Zjson</a> 或 <a href="https://link.segmentfault.com/?enc=mvdH%2FPPjhIJ844%2BUStQihQ%3D%3D.EVVTwGgr2uzn5dwQLu%2BM%2BiFsOLyELrqOvVXeI6ReUjkgtoR6cm6AmlxG53kAkncT" rel="nofollow">github-Zjson</a> 。</p><h2>设计思路</h2><p>ZORM 数据传递采用json来实现,使数据标准能从最前端到最后端达到和谐统一。此项目目标,不但在要C++中使用,还要作为动态链接库与node.js结合用使用,因此希望能像javascript一样,简洁方便的操作json。所以先行建立了zjson库,作为此项目的先行项目。设计了数据库通用操作接口,实现与底层实现数据库的分离。该接口提供了CURD标准访问,以及批量插入和事务操作,基本能满足平时百分之九十以上的数据库操作。项目基本目标,支持Sqlite3,Mysql,Postges三种关系数据库,同时支持windows、linux和macOS。</p><h2>项目进度</h2><p>现在已经实现了sqlit3与mysql的所有功能,postgres也做了技术准备。 <br> 我选择的技术实现方式,基本上是最底层高效的方式。sqlit3 - sqllit3.h(官方的标准c接口);mysql - c api (MySQL Connector C 6.1);postgres - pqxx 。</p><p>任务列表:</p><ul><li><p>[x] Sqlite3 实现</p><ul><li>[x] linux</li><li>[x] windows</li><li>[x] macos</li></ul></li><li><p>[x] Mysql 实现</p><ul><li>[x] linux</li><li>[x] windows</li><li>[x] macos</li></ul></li><li><p>[x] Pstgre 实现</p><ul><li>[x] linux</li><li>[ ] windows</li><li>[x] macos</li></ul></li></ul><h2>数据库通用接口</h2><blockquote>应用类直接操作这个通用接口,实现与底层实现数据库的分离。该接口提供了CURD标准访问,以及批量插入和事务操作,基本能满足平时百分之九十以上的数据库操作。</blockquote><pre><code> class ZORM_API Idb
{
public:
virtual Json select(string tablename, Json& params, vector<string> fields = vector<string>(), Json values = Json(JsonType::Array)) = 0;
virtual Json create(string tablename, Json& params) = 0;
virtual Json update(string tablename, Json& params) = 0;
virtual Json remove(string tablename, Json& params) = 0;
virtual Json querySql(string sql, Json params = Json(), Json values = Json(JsonType::Array), vector<string> fields = vector<string>()) = 0;
virtual Json execSql(string sql, Json params = Json(), Json values = Json(JsonType::Array)) = 0;
virtual Json insertBatch(string tablename, Json& elements, string constraint = "id") = 0;
virtual Json transGo(Json& sqls, bool isAsync = false) = 0;
};</code></pre><h2>实例构造</h2><blockquote>全局查询开关变量:</blockquote><ul><li>DbLogClose : sql 查询语句显示开关</li><li>parameterized : 是否使用参数化查询</li></ul><blockquote>Sqlite3:</blockquote><pre><code> Json options;
options.addSubitem("connString", "./db.db"); //数据库位置
options.addSubitem("DbLogClose", false); //显示查询语句
options.addSubitem("parameterized", false); //不使用参数化查询
DbBase* db = new DbBase("sqlite3", options);</code></pre><blockquote>Mysql:</blockquote><pre><code> Json options;
options.addSubitem("db_host", "192.168.6.6"); //mysql服务IP
options.addSubitem("db_port", 3306); //端口
options.addSubitem("db_name", "dbtest"); //数据库名称
options.addSubitem("db_user", "root"); //登记用户名
options.addSubitem("db_pass", "123456"); //密码
options.addSubitem("db_char", "utf8mb4"); //连接字符设定[可选]
options.addSubitem("db_conn", 5); //连接池配置[可选],默认为2
options.addSubitem("DbLogClose", true); //不显示查询语句
options.addSubitem("parameterized", true); //使用参数化查询
DbBase* db = new DbBase("mysql", options);</code></pre><h2>智能查询方式设计</h2><blockquote>查询保留字:page, size, sort, fuzzy, lks, ins, ors, count, sum, group</blockquote><ul><li><p>page, size, sort, 分页排序<br> 在sqlit3与mysql中这比较好实现,limit来分页是很方便的,排序只需将参数直接拼接到order by后就好了。 <br> 查询示例:</p><pre><code>Json p;
p.addSubitem("page", 1);
p.addSubitem("size", 10);
p.addSubitem("size", "sort desc");
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users ORDER BY age desc LIMIT 0,10</code></pre></li><li><p>fuzzy, 模糊查询切换参数,不提供时为精确匹配<br> 提供字段查询的精确匹配与模糊匹配的切换。</p><pre><code>Json p;
p.addSubitem("username", "john");
p.addSubitem("password", "123");
p.addSubitem("fuzzy", 1);
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users WHERE username like '%john%' and password like '%123%'</code></pre></li><li><p>ins, lks, ors<br> 这是最重要的三种查询方式,如何找出它们之间的共同点,减少冗余代码是关键。</p><ul><li><p>ins, 数据库表单字段in查询,一字段对多个值,例: <br> 查询示例:</p><pre><code>Json p;
p.addSubitem("ins", "age,11,22,36");
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users WHERE age in ( 11,22,26 )</code></pre></li><li><p>ors, 数据库表多字段精确查询,or连接,多个字段对多个值,例: <br> 查询示例:</p><pre><code>Json p;
p.addSubitem("ors", "age,11,age,36");
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users WHERE ( age = 11 or age = 26 )</code></pre></li><li><p>lks, 数据库表多字段模糊查询,or连接,多个字段对多个值,例:<br> 查询示例:</p><pre><code>Json p;
p.addSubitem("lks", "username,john,password,123");
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users WHERE ( username like '%john%' or password like '%123%' )</code></pre></li></ul></li><li><p>count, sum<br> 这两个统计求和,处理方式也类似,查询时一般要配合group与fields使用。</p><ul><li><p>count, 数据库查询函数count,行统计,例:<br> 查询示例:</p><pre><code>Json p;
p.addSubitem("count", "1,total");
(new DbBase(...))->select("users", p);
生成sql: SELECT *,count(1) as total FROM users</code></pre></li><li><p>sum, 数据库查询函数sum,字段求和,例:<br> 查询示例:</p><pre><code>Json p;
p.addSubitem("sum", "age,ageSum");
(new DbBase(...))->select("users", p);
生成sql: SELECT username,sum(age) as ageSum FROM users</code></pre></li></ul></li><li><p>group, 数据库分组函数group,例: <br> 查询示例:</p><pre><code>Json p;
p.addSubitem("group", "age");
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users GROUP BY age</code></pre></li></ul><blockquote>不等操作符查询支持</blockquote><p>支持的不等操作符有:>, >=, <, <=, <>, =;逗号符为分隔符,一个字段支持一或二个操作。 <br>特殊处:使用"="可以使某个字段跳过search影响,让模糊匹配与精确匹配同时出现在一个查询语句中</p><ul><li><p>一个字段一个操作,示例:<br> 查询示例:</p><pre><code>Json p;
p.addSubitem("age", ">,10");
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users WHERE age> 10</code></pre></li><li><p>一个字段二个操作,示例:<br> 查询示例:</p><pre><code>Json p;
p.addSubitem("age", ">=,10,<=,33");
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users WHERE age>= 10 and age<= 33</code></pre></li><li><p>使用"="去除字段的fuzzy影响,示例:<br> 查询示例:</p><pre><code>Json p;
p.addSubitem("age", "=,18");
p.addSubitem("username", "john");
p.addSubitem("fuzzy", "1");
(new DbBase(...))->select("users", p);
生成sql: SELECT * FROM users WHERE age= 18 and username like '%john%'</code></pre><p>具体使用方法,请参看uint test。</p></li></ul><h2>单元测试</h2><p>有完整功能的单元测试用例,请参见tests目录下的测试用例。</p><blockquote>测试用例运行结果样例<br><img src="/img/bVc2xLL" alt="" title=""></blockquote><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/zorm
或
https://github.com/zhoutk/zorm</code></pre><h2>运行方法</h2><p>该项目在vs2019, gcc7.5, clang12.0下均编译运行正常。</p><pre><code>git clone https://github.com/zhoutk/zorm
cd zorm
cmake -Bbuild .
---windows
cd build && cmake --build .
---linux & macos
cd build && make
run zorm or ctest</code></pre><p>注在linux下需要先行安装mysql开发库, 并先手动建立数据库 dbtest。 <br>在ubuntu下的命令是: apt install libmysqlclient-dev</p><h2>相关项目</h2><p>会有一系列项目出炉,网络服务相关,敬请期待...</p><p><a href="https://link.segmentfault.com/?enc=khP5XLyyDdD0y0WyLjKuZw%3D%3D.8AnDa2npd3fksp3XtdaGmu4Nfa7HKba%2B%2Byv5f5glN7r5Fa9EdNMPc2iJo%2BK7pI97" rel="nofollow">gitee-Zjson</a> <br><a href="https://link.segmentfault.com/?enc=OvI1EVskusbILu1N4%2FfSiQ%3D%3D.IWbE2gEyldIX4fsKmLTiwmJru%2BRyCPBI3WK%2F8kNqvnFHph9PLF6g941VXi%2BbXh9M" rel="nofollow">github-Zjson</a></p>
使用C++17手撸JSON库
https://segmentfault.com/a/1190000041881571
2022-05-23T00:13:14+08:00
2022-05-23T00:13:14+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>zjson</h2><h3>介绍</h3><p>从node.js转到c++,特别怀念在js中使用json那种畅快感。在c++中也使用过了些库,但提供的接口使用方式,总不是习惯,很烦锁,接口函数太多,不直观。参考了很多库,如:rapidjson, cJson, CJsonObject, drleq-cppjson, json11等,受cJson的数据结构启发很大,决定用C++手撸一个。最后因为数据存储需要不区分型别,又要能知道其型别,所以选择了C++17才支持的std::variant以及std::any,最终,C++版本定格在c++17,本库设计为单头文件,且不依赖c++标准库以外的任何库。</p><h3>项目名称说明</h3><p>本人姓名拼音第一个字母z加上josn,即得本项目名称zjson,没有其它任何意义。我将编写一系列以z开头的相关项目,命名是个很麻烦的事,因此采用了这种简单粗暴的方式。</p><h3>设计思路</h3><p>简单的接口函数、简单的使用方法、灵活的数据结构、尽量支持链式操作。使用模板技术,使用给Json对象增加值的方法只有两个,AddValueBase和AddValueJson。采用链表结构(向cJSON致敬)来存储Json对象,请看我下面的数据结构设计,表头与后面的结点,都用使用一致的结构,这使得在索引操作([])时,可以进行链式操作。</p><h3>项目进度</h3><p>项目目前完成一半,可以新建Json对象,增加数据,按key(Object类型)或索引(Array类型)提取相应的值或子对象,生成json字符串。 <br>已经做过内存泄漏测试,析构函数能正确运行,百万级别生成与销毁未见内存明显增长。 <br>任务列表:</p><ul><li>[x] 构造函数、复制构造函数、析构函数</li><li>[x] AddValueBase(为Json对象增加值类型)、AddValueJson(为Json对象增加对象类型)</li><li>[x] operator=、operator[]</li><li>[x] toString(生成json字符串)</li><li>[x] toInt、toDouble、toFalse 等值类型转换</li><li>[x] isError、isNull、isArray 等节点类型判断</li><li>[ ] parse, 从json字符串生成Json对象;相应的构造函数</li><li>[ ] Extend Json - 扩展对象</li><li>[ ] Remove[All] key - 删除数据, 因为Json对象允许重复的key</li><li>[ ] findAll - 查找全部, 因为Json对象允许重复的key</li><li>[ ] std::move语义</li></ul><h3>数据结构</h3><h4>Json 节点类型定义</h4><p>(内部使用,数据类型只在Json类内部使用)</p><pre><code>enum Type {
Error, //错误,查找无果,这是一个无效Json对象
False, //Json值类型 - false
True, //Json值类型 - true
Null, //Json值类型 - null
Number, //Json值类型 - 数字,库中以double类型存储
String, //Json值类型 - 字符串
Object, //Json类对象类型 - 这是Object嵌套,对象型中只有child需要关注
Array //Json类对象类型 - 这是Array嵌套,对象型中只有child需要关注
};</code></pre><h4>Json 节点定义</h4><pre><code>class Json {
Json* brother; //与cJSON中的next对应,值类型才有效,指向并列的数据,但有可能是值类型,也有可能是对象类型
Json* child; //孩子节点,对象类型才有效
Type type; //节点类型
std::variant <int, bool, double, string> data; //节点数据
string name; //节点的key
}</code></pre><h3>接口说明</h3><p>公开的对象类型,json只支持Object与Array两种对象,与内部类型对应(公开类型)。</p><pre><code>enum class JsonType
{
Object = 6,
Array = 7
};</code></pre><p>接口列表</p><ul><li>Json(JsonType type = JsonType::Object) //默认构造函数,生成Object或Array类型的Json对象</li><li>Json(const Json& origin) //复制构造函数</li><li>Json& operator = (const Json& origin) //赋值操作</li><li>Json operator[](const int& index) //Json数组对象元素查询</li><li>Json operator[](const string& key) //Json Object 对象按key查询</li><li>bool AddValueJson(Json& obj) //增加子Json类对象类型, 只面向Array</li><li>bool AddValueJson(string name, Json& obj) //增加子Json类对象类型,当obj为Array时,name会被忽略</li><li>template<typename T> bool AddValueBase(T value) //增加值对象类型,只面向Array</li><li>template<typename T> bool AddValueBase(string name, T value) //增加值对象类型,当this为Array时,name会被忽略</li><li>string toString() //Json对象序列化为字符串</li><li>bool isError() //无效Json对象判定</li><li>bool isNull() //null值判定</li><li>bool isObject() //Object对象判定</li><li>bool isArray() //Array对象判定</li><li>bool isNumber() //number值判定,Json内使用double型别存储number值</li><li>bool isTrue() //true值判定</li><li>bool isFalse() //false值判定</li><li>int toInt() //值对象转为int</li><li>float toFloat() //值对象转为float</li><li>double toDouble() //值对象转为double</li><li>bool toBool() //值对象转为bool</li></ul><h3>编程示例</h3><p>简单使用示例</p><pre><code> Json ajson(JsonType::Object); //新建Object对象,输入参数可以省略
std::string data = "kevin";
ajson.AddValueBase("fail", false); //增加false值对象
ajson.AddValueBase("name", data); //增加字符串值对象
ajson.AddValueBase("school-en", "the 85th.");
ajson.AddValueBase("age", 10); //增加number值对象,此处为整数
ajson.AddValueBase("scores", 95.98); //增加number值对象,此处为浮点数,还支持long,long long
ajson.AddValueBase("nullkey", nullptr); //增加null值对象,需要送入nullptr, NULL会被认为是整数0
Json sub; //新建Object对象
sub.AddValueBase("math", 99);
ajson.AddValueJson("subJson", sub); //为ajson增加子Json类型对象,完成嵌套需要
Json subArray(JsonType::Array); //新建Array对象,输入参数不可省略
subArray.AddValueBase("I'm the first one."); //增加Array对象的字符串值子对象
subArray.AddValueBase("two", 2); //增加Array对象的number值子对象,第一个参数会被忽略
Json sub2;
sub2.AddValueBase("sb2", 222);
subArray.AddValueJson("subObj", sub2); //为Array对象增加Object类子对象,完成嵌套需求
ajson.AddValueJson("array", subArray); //为ajson增加Array对象,且这个Array对象本身就是一个嵌套结构
std::cout << "ajson's string is : " << ajson.toString() << std::endl; //输出ajson对象序列化后的字符串, 结果见下方
string name = ajson["name"].toString(); //提取key为name的字符串值,结果为:kevin
int oper = ajson["sb2"].toInt(); //提取嵌套深层结构中的key为sb2的整数值,结果为:222
Json operArr = ajson["array"]; //提取key为array的数组对象
string first = ajson["array"][0].toString(); //提取key为array的数组对象的序号为0的值,结果为:I'm the first one.</code></pre><p>ajson序列化后结果为:</p><pre><code>{
"fail": false,
"name": "kevin",
"school-en": "the 85th.",
"age": 10,
"scores": 95.98,
"nullkey": null,
"subJson": {
"math": 99
},
"array": [
"I'm the first one.",
2,
{
"sb2": 222
}
]
}</code></pre><p>详情请参看demo.cpp或tests目录下的测试用例</p><h3>项目地址</h3><pre><code>https://gitee.com/zhoutk/zjson
或
https://github.com/zhoutk/zjson</code></pre><h3>运行方法</h3><p>该项目在vs2019, gcc7.5, clang12.0下均编译运行正常。</p><pre><code>git clone https://github.com/zhoutk/zjson
cd zjson
cmake -Bbuild .
---windows
cd build && cmake --build .
---linux & mac
cd build && make
run zjson or ctest</code></pre><h3>相关项目</h3><p>会有一系列项目出炉,网络服务相关,敬请期待...</p>
Python3编程实战Tetris机器人(ORM)
https://segmentfault.com/a/1190000040292331
2021-07-05T16:17:04+08:00
2021-07-05T16:17:04+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>设计思路</h2><p>我们通用的ORM,基本模式都是想要脱离数据库的,几乎都在编程语言层面建立模型,由程序去与数据库打交道。虽然脱离了数据库的具体操作,但我们要建立各种模型文档,用代码去写表之间的关系等等操作,让初学者一时如坠云雾。我的做法是把逻辑加入到Python的字典中,程序将对象自动映射成为标准的SQL查询语句。只要我们理解了标准的SQL语言,我们就能够完成数据库查询操作。</p><h2>智能查询方式设计</h2><blockquote>查询保留字:page, size, sort, search, lks, ins, ors, count, sum, group</blockquote><h3>page, size, sort, 分页排序</h3><p>查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"page": 1, "size":10, "sort":"age desc"})
print(rs)
生成sql: SELECT * FROM users ORDER BY age desc LIMIT 0,10</code></pre><h3>search, 模糊查询切换参数,不提供时为精确匹配</h3><pre><code>提供字段查询的精确匹配与模糊匹配的切换。
```
dao = BaseDao()
rs = dao.select("users",{"username": "john", "password":"123", "search":"1"})
print(rs)
生成sql: SELECT * FROM users WHERE username like '%john%' and password like '%123%'
```</code></pre><h3>ins, lks, ors</h3><p>这是最重要的三种查询方式,如何找出它们之间的共同点,减少冗余代码是关键。</p><ul><li><p>ins, 数据库表单字段in查询,一字段对多个值,例: <br> 查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"ins":["age", 11,22,26]})
print(rs)
生成sql: SELECT * FROM users WHERE age in ( 11,22,26 )</code></pre></li><li><p>ors, 数据库表多字段精确查询,or连接,多个字段对多个值,例: <br> 查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"ors":["age", 11,26]})
print(rs)
生成sql: SELECT * FROM users WHERE ( age = 11 or age = 26 )</code></pre></li><li><p>lks, 数据库表多字段模糊查询,or连接,多个字段对多个值,例:<br> 查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"lks":["username", "john","password","123"]})
print(rs)
生成sql: SELECT * FROM users WHERE ( username like '%john%' or password like '%123%' )</code></pre></li></ul><h3>count, sum</h3><p>这两个统计求和,处理方式也类似,查询时一般要配合group与fields使用。</p><ul><li><p>count, 数据库查询函数count,行统计,例:<br> 查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"count":["1", "total"]})
print(rs)
生成sql: SELECT *,count(1) as total FROM users</code></pre></li><li><p>sum, 数据库查询函数sum,字段求和,例:<br> 查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"sum":["age", "ageSum"]})
print(rs)
生成sql: SELECT username,sum(age) as ageSum FROM users</code></pre></li></ul><h3>group, 数据库分组函数group,例:</h3><p>查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"group":"age"})
print(rs)
生成sql: SELECT * FROM users GROUP BY age</code></pre><h3>不等操作符查询支持</h3><p>支持的不等操作符有:>, >=, <, <=, <>, =;逗号符为分隔符,一个字段支持一或二个操作。 <br>特殊处:使用"="可以使某个字段跳过search影响,让模糊匹配与精确匹配同时出现在一个查询语句中</p><ul><li><p>一个字段一个操作,示例:<br> 查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"age":">,10"})
print(rs)
生成sql: SELECT * FROM users WHERE age> 10</code></pre></li><li><p>一个字段二个操作,示例:<br> 查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"age":">=,10,<=33"})
print(rs)
生成sql: SELECT * FROM users WHERE age>= 10 and age<= 33</code></pre></li><li><p>使用"="去除字段的search影响,示例:<br> 查询示例:</p><pre><code>dao = BaseDao()
rs = dao.select("users",{"age":"==,18","username":"john","search":"1"})
print(rs)
生成sql: SELECT * FROM users WHERE age= 18 and username like '%john%'</code></pre></li></ul><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(数据库操作)
https://segmentfault.com/a/1190000040279296
2021-07-02T16:14:53+08:00
2021-07-02T16:14:53+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>设计思路</h2><p>将用户手动玩和AI自动玩游戏的历史记录下来,存入数据库,供后面进行分析。为了不依赖某个特定的数据系统,设计了一个通用数据库操作接口,以方便在应用层面切换不同的数据库。</p><h2>接口设计</h2><pre><code>class BaseDao(object):
def select(self, tablename, params={}, fields=None): # 查询接口,参数:数据表名;查询参数(ORM规则融入字典中,请参看下一篇日志);返回数据字段
fields = [] if fields == None else fields # Python的默认参数行为很是不同,会记录上一次调用的结果,有点象其它语言中的静态变量
return dbhelper.select(tablename, params, fields) # 真正的查询实现,dbhelper是针对特定数据库的接口实现
def insert(self, tablename, params={}, fields=[]): # 新增接口,CURD函数的参数形式一致,这种设计方便作rest微服务时,由http的不同访问方式直接选择后台操作方法
if '_id_' in params and len(params) < 2 or '_id_' not in params and len(params) < 1: # 要求提供_id_,约定_id_为所有表的主键
return {"code": 301, "err": "The params is error."}
return dbhelper.insert(tablename, params)
def update(self, tablename, params={}, fields=[]):
if '_id_' not in params or len(params) < 2:
return {"code": 301, "err": "The params is error."}
return dbhelper.update(tablename, params)
def delete(self, tablename, params={}, fields=[]):
if '_id_' not in params:
return {"code": 301, "err": "The params is error."}
return dbhelper.delete(tablename, params)
def querySql(self, sql, values = [], params = {}, fields = []): # 手写查询接口
return dbhelper.querySql(sql, values, params, fields)
def execSql(self, sql, values = []): # 手写非查询接口
return dbhelper.exec_sql(sql, values)
def insertBatch(self, tablename, elements : List): # 批量写入接口
return dbhelper.insertBatch(tablename,elements)
def transGo(elements = [], isAsync = False): # 事务接口,待实现
pass</code></pre><h2>具体实现(Sqlit3)</h2><p>首先实现了对Sqlit3的操作接口。</p><h3>直接面对Sqlit3的函数</h3><pre><code>def exec_sql(sql, values, opType = 0): # opType : 0 - 单条SQL语句; 1 - 批量操作语句;2 - 查询返回数据集;
try: # 所有与数据操作都通过这个函数,需要用异常处理封装
flag = False # 是否出错标识变量
error = {}
if not os.path.exists("./dist"): # 存储位置目录存在判断
os.mkdir("dist")
conn = dbHandle.connect("./dist/log.db") # 连接数据库或新建
cur = conn.cursor()
if opType == 1: # 批量操作
num = cur.executemany(sql, values)
else: # 单条语句
num = cur.execute(sql, values)
if opType == 2: # 有结果集返回
result = cur.fetchall()
else:
conn.commit()
# print('Sql: ', sql, ' Values: ', values)
except Exception as err: # 出错
flag = True
error = err
print('Error: ', err)
finally:
conn.close() # 结束处理,并格式化返回结果
if flag:
return False, error, num if 'num' in dir() else 0
return True, result if 'result' in dir() else [], len(result) if opType == 2 else num.rowcount if 'num' in dir() else 0</code></pre><h3>查询函数</h3><p>这里讲解函数主干,关于在字典中融入ORM的解析,请参看下篇日志。</p><pre><code>def select(tablename, params={}, fields=None, sql = None, values = None):
where = ""
AndJoinStr = ' and '
reserveKeys = {}
for rk in ["sort", "search", "page", "size", "sum", "count", "group"]:
# 提取保留关键字
for k, v in params.items():
whereExtra = ""
if k == "ins":
# 保留关键字ins,lks,ors处理
else:
flag = False
if type(v) == "str":
# 不等查询操作处理
elif reserveKeys.get('search'):
# 精确查询与模糊查询处理
else:
whereExtra += k + " =? "
values.append(v)
where += whereExtra
# 排序、统计、分组和分页等操作处理
rs = exec_sql(sql, values, 2)
return {"code": 200, "rows": rs[1], "total": rs[2]}</code></pre><h3>插入函数</h3><p>删除和更新与插入类似,这里讲解插入函数</p><pre><code>def insert(tablename, params={}):
sql = "insert into %s ( " % tablename # 主干
ks = params.keys()
vs = []
ps = ""
for al in ks: # 解析参数
sql += al + "," # 按插入语句拼接每一个参数
ps += "?," # python的sqlit3封装没法送入list对象来实现元组数据的写入,只能拆开
vs.append(params[al])
sql = sql[:-1] + ") values (" + ps[:-1] + ")" # 去掉最后的逗号,并完成sql语句
rs = exec_sql(sql, vs) # 执行,vs参数中不能嵌套list,比起C++版本的实现,这里有些别扭
if rs[0]: # 返回结果
return {"code": 200, "info": "create success.", "total": rs[2]}
else:
return {"code": 701, "error": rs[1].args[0], "total": rs[2]}</code></pre><h3>批量插入</h3><p>事实证明,一条条的插入效率太低,AI运行时,数据写入跟不上节奏。</p><pre><code>def insertBatch(tablename, elements : List):
if len(elements) == 0: # 无输入元素,直接退出
return {"code": 201, "info": "There is no elements exist.", "total": 0}
elif len(elements) == 1: # 只有一个元素,调用insert实现来完成
return insert(tablename, elements[0])
sql = "insert into %s ( " % tablename
isFirst = True # 在循环的第一次,要处理操作字段
vs = []
ps = ""
for ele in elements:
if isFirst:
isFirst = False
ks = ele.keys()
for al in ks: # 操作字段,只需处理一次
sql += al + ","
ps += "?," # 参数批占位符
items = []
for bl in ks: # 按key的顺序逐个添加写入参数值,字典的访问顺序是不一定的
items.append(ele[bl])
vs.append(items)
sql = sql[:-1] + ") values (" + ps[:-1] + ")" # 最后的拼接
rs = exec_sql(sql, vs, 1) # 执行
if rs[0]: # 返回结果
return {"code": 200, "info": "create success.", "total": rs[2]}
else:
return {"code": 701, "error": rs[1].args[0], "total": rs[2]}</code></pre><h3>内容预告</h3><p>下一篇日志讲解融入字典中的ORM规则设计及使用方法,有了这一套规则,无需要再写sql语句。欲后事如何,请持续关注,谢谢!</p><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(游戏暂停)
https://segmentfault.com/a/1190000040233872
2021-06-24T15:09:11+08:00
2021-06-24T15:09:11+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>设计思路</h2><p>游戏暂停功能比较简单,主要是控制gameRunningStatus变量的值与界面的控制统一起来,游戏暂停了,键盘的响应也要停止。另,gameRunningStatus变量的改变也不能直接操作,需生成一个暂停命令单元,送入队列中,由工作任务线程去处理。</p><h2>具体实现</h2><h3>界面控制</h3><p>开始按钮会在 “Start”、“Rause”和“Resume”之间切换。</p><pre><code>def btnStartClicked(self):
if self.game.getGameRunningStatus() == 0: # 点击时游戏处于未开始状态
self.btnStartVar.set("Pause")
self.game.start()
elif self.game.getGameRunningStatus() == 1: # 点击时游戏处于游戏中
self.btnStartVar.set("Resume")
self.game.pause()
elif self.game.getGameRunningStatus() == 5: # 点击时游戏处于暂停状态
self.btnStartVar.set("Pause")
self.game.resume()
self.gameCanvas.focus_set() # 设置游戏空间为当前活动控件</code></pre><p>游戏结束时,恢复初始状态</p><pre><code>self.app.setStartButtonText("Start")</code></pre><h3>暂停操作</h3><pre><code>def pause(self):
opQueue.put(("pause",5))</code></pre><h3>恢复操作</h3><p>游戏处于暂停中,所以直接修改变量值,没有设计命令单元了。</p><pre><code>def resume(self):
self.gameRunningStatus = 1
self.canvas.after(self.gameSpeedInterval * (0 if self.isAutoRunning else 1), self.tickoff)</code></pre><p>每一个任务单元都是一个二元元组(方便数据解构),第一个是字符串,为命令;第二个是元组,是数据包(也按方便解构的方式去设计),由每个命令自行定义。</p><h3>工作线程增加暂停处理</h3><pre><code>...
elif cmd == "pause":
self.gameRunningStatus = data
...</code></pre><h3>键盘响应改造</h3><pre><code>def processKeyboardEvent(self, ke):
if self.game.getGameRunningStatus() == 1 and self.game.isAutoRunning == 0:
if ke.keysym == 'Left':
opQueue.put(('Left',()))
...</code></pre><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(多线程问题)
https://segmentfault.com/a/1190000040218933
2021-06-22T15:31:18+08:00
2021-06-22T15:31:18+08:00
zhoutk
https://segmentfault.com/u/zhoutk
2
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>发现问题</h2><p>在测试过程中,发现程序出错,但关闭定时器,不进行自动下落就不会有问题。原因是Timer会新开一个线程,线程和主线会产生资源冲突。</p><h2>解决方案</h2><p>首先想到的是加锁,游戏逻辑很简单,加锁应该很容易解决问题。但不管我粗粒度加,还是尽量细粒度加,最后都会死锁。最后进行打印,发现程序停在了tkinter.Canvas.move处,个人认为这是tkinter的bug。 <br>此路不通,换个思路。开一个工作线程,来完成所有的操作,主线程与定时器操作,都只是往工作线程中提交任务。也就是只让一个工作线程来做任务,这样就把资源冲突的问题避开了。</p><h2>加锁</h2><p>加锁方案分析</p><h3>键盘响应加锁</h3><pre><code>tickLock[0] = True
with curTetrisLock:
print("-------+++---00000000--- get lock", tickLock)
if ke.keysym == 'Left':
self.game.moveLeft()
if ke.keysym == 'Right':
self.game.moveRight()
if ke.keysym == 'Up':
self.game.rotate()
if ke.keysym == 'Down':
self.game.moveDown()
if ke.keysym == 'space':
self.game.moveDownEnd()
print("-------+++---00000000--- lose lock", tickLock)</code></pre><h3>定时器响应加锁</h3><pre><code>def tickoff(self):
if self.gameRunningStatus == 1:
if not tickLock[0]:
with curTetrisLock:
print("------------------ get lock", tickLock[1])
self.moveDown()
print("================== lose lock", tickLock[1])
self.tick = Timer(self.gameSpeedInterval / 1000, self.tickoff)
self.tick.start()</code></pre><h3>问题定位</h3><p>程序最后停在了Block类中的tkinter.Canvas.move处,每次都由定时器触发,无法释放。<br><img src="/img/bVcSUS8" alt="" title=""><br>有兴趣的同学可以到项目中,切换到lockbug分支去研究,我写了很多打印输出方便问题定位。</p><h2>增加工作线程</h2><h3>任务单元设计</h3><p>新增一个Queue,键盘响应与定时器响应往队列中增加任务单元,工作线程逐一处理这些任务。任务单元如下设计:</p><pre><code>("cmd",(data))</code></pre><p>每一个任务单元都是一个二元元组(方便数据解构),第一个是字符串,为命令;第二个是元组,是数据包(也按方便解构的方式去设计),由每个命令自行定义。</p><h3>工作线程</h3><pre><code>def opWork(self):
while True:
if not opQueue.empty():
cmd,data = opQueue.get()
if op == "Left":
self.moveLeft()
elif op == "Right":
self.moveRight()
elif op == "Up":
self.rotate()
elif op == "Down":
self.moveDown()
elif op == "space":
self.moveDownEnd()
elif op == "quit":
break
else:
time.sleep(0.01)</code></pre><h3>键盘响应改造</h3><pre><code>def processKeyboardEvent(self, ke):
if self.game.getGameRunningStatus() == 1:
if ke.keysym == 'Left':
opQueue.put(('Left',()))
if ke.keysym == 'Right':
opQueue.put(('Right',()))
if ke.keysym == 'Up':
opQueue.put(('Up',()))
if ke.keysym == 'Down':
opQueue.put(('Down',()))
if ke.keysym == 'space':
opQueue.put(('space',()))</code></pre><h3>定时器改造</h3><p>游戏控制主要函数,在方块下落到底部后,进行消层、统计得分、速度等级判定、游戏是否结束判定以及将下一方块移入游戏空间并再生成一个方块显示在下一方块显示空间中。</p><pre><code>def tickoff(self):
if self.gameRunningStatus == 1:
opQueue.put(('Down'),())
self.tick = Timer(self.gameSpeedInterval / 1000, self.tickoff)
self.tick.start()</code></pre><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(game类)
https://segmentfault.com/a/1190000040211839
2021-06-21T17:03:50+08:00
2021-06-21T17:03:50+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>game类</h2><p>游戏逻辑控制类,是界面与Tetris类之间的粘合者,接受界面的鼠标及键盘事件,操作Tetris类,实现游戏逻辑。单个方块的操作,在Tetris中已经实现,game类主要是实现消行算法、新方块的产生、游戏速度控制等。</p><h2>设计思路</h2><p>消层算法简单的处理就是发现一行,消除一行。本项目使用一点技巧,先找出所有可消除行,把行号存入数组中,一次性消除。游戏速度的控制,使用定时器来实现,但发现python的定时器与其它语言有些差别,会不断产生新的定时器对象,开始感觉有些不对劲,但也没有重视。后来确认,这会产生内存泄漏,后使用tkinter.after替换了。</p><h2>相关常数</h2><pre><code>SCORES = (0,1,3,7,10) # 消层分值设定
STEPUPSCORE = 50 # 每增长50分,速度加快一个等级
STEPUPINTERVAL = 100 # 每增长一个等级,定时器间隔时间减少100毫秒</code></pre><h2>具体实现</h2><h3>游戏状态变量</h3><p>game.gameRunningStatus</p><ul><li>0 : 游戏未开始</li><li>1 : 手动游戏</li><li>2 : 游戏回放</li><li>5 : 游戏暂停</li></ul><h3>开始游戏</h3><pre><code>def start(self):
self.gameRunningStatus = 1
self.gameSpeedInterval = 1000 # 初始游戏速度
self.gameSpeed = 1 # 游戏速度等级
self.gameLevels = 0 # 消层数
self.gameScores = 0 # 总得分
self.app.updateGameInfo(1,0,0) # 初始化界面信息
self.canvas.delete(ALL) # 清空游戏空间
self.nextCanvas.delete(ALL) # 下一方块空间清空
initGameRoom() # 初始化游戏空间数据
self.tetris = Tetris(self.canvas, 4, 0, random.randint(0,6)) # 随机生成第一个方块
for i in range(random.randint(0,4)): # 旋转随机次数,方块出场式
self.tetris.rotate()
self.nextTetris = Tetris(self.nextCanvas, 1, 1, random.randint(0,6)) # 随机生成下一方块
for i in range(random.randint(0,4)): # 下一方块初始形态(随机)
self.nextTetris.rotate()
self.tick = Timer(self.gameSpeedInterval / 1000, self.tickoff) # 控制游戏速度定时器
self.tick.start()</code></pre><h3>生成下一方块</h3><p>游戏控制主要函数,在方块下落到底部后,进行消层、统计得分、速度等级判定、游戏是否结束判定以及将下一方块移入游戏空间并再生成一个方块显示在下一方块显示空间中。</p><pre><code>def generateNext(self):
cleanLevels = self.clearRows() # 统计可消除层数
if cleanLevels > 0: # 有可消层,计算分值
self.gameLevels += cleanLevels
self.gameScores += SCORES[cleanLevels]
if self.gameScores / STEPUPSCORE >= self.gameSpeed:
self.gameSpeed += 1
self.gameSpeedInterval -= STEPUPINTERVAL
self.app.updateGameInfo(self.gameSpeed, self.gameLevels, self.gameScores)
self.tetris = Tetris(self.canvas, 4, 0, self.nextTetris.getTetrisShape()) # 复制nexTetris到游戏空间
for i in range(self.nextTetris.getRotateCount()):
if not self.tetris.rotate():
break
if self.tetris.canPlace(4, 0): # 判定游戏是否结束
self.nextCanvas.delete(ALL) # 游戏未结束,生成新的方块放入下一方块空间
self.nextTetris = Tetris(self.nextCanvas, 1, 1, random.randint(0,6))
for i in range(random.randint(0,4)):
self.nextTetris.rotate()
else: # 游戏结束
self.gameRunningStatus = 0
self.canvas.create_text(150, 200, text = "Game is over!", fill="white", font = "Times 28 italic bold")
self.app.setStartButtonText("Start")
print("game is over!")</code></pre><h3>统计可消除层</h3><p>clearRows函数查找能消除的层,将其消除,返回可消除层总数。</p><pre><code>def clearRows(self):
occupyLines = [] # 存储可消除层行号
h = 20
while h > 0:
allOccupy = 0
for i in range(1, 11):
if GameRoom[h][i]:
allOccupy += 1 # block统计
if allOccupy == 10: # 行满
occupyLines.append(h) # 存储行号
elif allOccupy == 0: # 有一个空位,跳过些行
break
h -= 1
if len(occupyLines) > 0: # 有可消层
self.doCleanRows(occupyLines) # 消除可消层
return len(occupyLines)</code></pre><h3>消层函数</h3><p>消层函数,根据clearRows函数统计的可消层行号,消除游戏空间的满行。算法的难点在于要同时控制两个变量,一个是从下到上遍历游戏空间,另一方面要将满行以上的空间数据下移,下移的步长为已经消除的行数。</p><pre><code>def doCleanRows(self, lines):
index = 0 # 存储已经消除了多少行
h = lines[index] # 满行行号数据
while h >= 0: # 只需要从最下面一满行开始即可
if index < len(lines) and h == lines[index]: # 找到一可消行
index += 1 # 已消行总数加1
for j in range(1, 11):
GameRoom[h][j] = 0 # 游戏空间数据消行
for b in self.canvas.find_closest(\ # Canvas元件消除
j * BLOCKSIDEWIDTH - HALFBLOCKWIDTH, \
h * BLOCKSIDEWIDTH - HALFBLOCKWIDTH):
self.canvas.delete(b)
else: # 移动游戏空间数据
count = 0 # 空位统计,全空,可以提前结束循环
for j in range(1, 11):
if GameRoom[h][j] == 1:
count += 1
GameRoom[h + index][j] = GameRoom[h][j] # 注意index变量,这是移动步长,与已经消除行数有关
GameRoom[h][j] = 0
for b in self.canvas.find_closest(j * BLOCKSIDEWIDTH - HALFBLOCKWIDTH, h * BLOCKSIDEWIDTH - HALFBLOCKWIDTH):
self.canvas.move(b, 0, index * BLOCKSIDEWIDTH)
if count == 0: # 发现整行位全空,提前退出
break
h -= 1</code></pre><h3>方块控制</h3><p>方块控制已经在Tetris类实现,在game类中,只是转发事件到当前方块即可。唯一多了一个moveDownEnd - 方块直落函数。</p><pre><code>def moveDownEnd(self):
while self.moveDown(): # 循环下落,直到不能再落
pass</code></pre><h3>游戏速度控制</h3><p>游戏速度控制实现很容易,只需要定时触发一次down函数就可以了。</p><pre><code>def tickoff(self):
if self.gameRunningStatus == 1:
self.moveDown()
self.tick = Timer(self.gameSpeedInterval / 1000, self.tickoff)
self.tick.start()</code></pre><h3>内容预告</h3><p>定时器的使用会引入新线程,会出现资源冲突问题,下单将解决线程冲突。</p><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(移动与旋转)
https://segmentfault.com/a/1190000040206237
2021-06-20T19:33:51+08:00
2021-06-20T19:33:51+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>方块移动</h2><p>为了方便,设计了绝对定位函数,变相对运动方式为绝对定位。</p><h3>方块绝对定位函数</h3><p>我们Block类中,对blog的移动使用的是tkinter.move函数,该函数提供的是相对距离方式,我们需要计算出位移差。该方法相当于把相对距离移动方式,变成了方块的绝对定位函数,所有移动操作都使用这个函数。</p><pre><code>def relocate(self, x, y):
for block in self.objs: # 遍历方块的block
block.relocate(x - self.x, y - self.y) # 移动操作
self.x = x
self.y = y</code></pre><h3>方块左移</h3><pre><code>def moveLeft(self):
if self.canPlace(self.x - 1, self.y): # 判断是否能左移
self.relocate(self.x - 1, self.y) # 移动
return True
else:
return False</code></pre><h3>方块右移</h3><pre><code>def moveRight(self):
if self.canPlace(self.x - 1, self.y): # 判断是否能右移
self.relocate(self.x + 1, self.y) # 移动
return True
else:
return False</code></pre><h3>方块下移</h3><p>方块下移,需要判断是否已经到底,到底后,方块位置将固定下来。</p><pre><code>def moveDown(self):
if self.canPlace(self.x, self.y + 1): # 判断是否能下移
self.relocate(self.x, self.y + 1) # 下移
return True
else: # 已经到底
for i in range(TETRISDIMENSION):
for j in range(TETRISDIMENSION):
if self.data[i][j]: # 固定方块,改游戏空间格局
GameRoom[self.y + i][self.x + j] = 1
return False</code></pre><h2>方块旋转</h2><p>方块旋转,实质上是矩形的旋转,我们设定按一次方向上键,方块进行一次顺时针90度旋转。</p><h3>算法原理</h3><p>如图,按下标对矩阵进行轮换即可。正规的矩阵旋转算法是先交换行再转置。我们要控制方块连续顺时针旋转,因为初始状态固定,只要记住旋转次数就能知道方块的实时形态,所以选择轮换算法。<br><img src="/img/bVcSRDE" alt="" title=""></p><h3>算法公式</h3><pre><code> a[i,j] -------------> a[j,N-i-1]
↑ ↓
↑ ↓
a[N-j-1,i] <------------- a[N-i-1,N-j-1]</code></pre><h2>算法实现</h2><pre><code>def rotate(self):
for i in range(TETRISDIMENSION // 2):
lenJ = TETRISDIMENSION - i - 1 # 增加变量,简化下标
for j in range(i, lenJ):
lenI = TETRISDIMENSION - j - 1
t = self.data[i][j]
self.data[i][j] = self.data[lenI][i] # 按公式轮换
self.data[lenI][i] = self.data[lenJ][lenI]
self.data[lenJ][lenI] = self.data[j][lenJ]
self.data[j][lenJ] = t
self.rotateCount += 1 # 旋转次数记录
self.redraw() # 重绘</code></pre><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(Tetris类)
https://segmentfault.com/a/1190000040204101
2021-06-19T23:36:29+08:00
2021-06-19T23:36:29+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>Tetris类</h2><p>组合Block类,实现俄罗斯方块的绘制及移动、旋转等所有操作。这是Tetris游戏的业务核心,第一步先实现手动玩的需求,以后AI自动玩时,还会改造这个类。在所有的逻辑里面,特别注意旋转(rotate)操作,后面解决的不少的bug被证明都是由于rotate操作考虑不全面所引起的。</p><h2>设计思路</h2><p>Tetris类通过组合Block类来实现屏幕的绘制,并与tkinter库进行解耦。因为tkinter库的设计,我们的界面使用了两个Canvas来分别实现游戏空间和下一方块的显示,因此一个方块有可能显示在不同的Canvas中。当一个方块放置后,要从nextCanvas中取出下一个方块,放置到游戏空间的上方。这个操作我没有找到简单的方法来实施跨Canvas移动元件。我的实现方法是,重新在游戏空间生成一个与nextCanvas中一样的方块,因为我每一个方块的初始形态是固定的,只是让它实现了几次随机的旋转,因此我只需查询next Tetris中的形状和旋转次数就可以复制了。</p><h2>相关常数</h2><pre><code>GameRoom = [[0 for i in range(12)] for i in range(22)] # 游戏空间定义 10x20
TETRISAHPES = ( # 方块的形态定义
(1, 1, 1, 1), # 方块一共有六种形态
(0, 1, 1, 1, 0, 1), # 其它形态都可以通过几次旋转来得到
(1, 1, 1, 0, 0, 0, 1), # 我定义旋转为顺时针旋转
(0, 1, 1, 0, 0, 1, 1), # 每一个方块的形态最多旋转四次就回到初始形态
(1, 1, 0, 0, 0, 1, 1),
(0, 1, 1, 0, 1, 1),
(0, 1, 0, 0, 1, 1, 1)
)
TETRISCOLORS = ( # 方块形态与颜色的绑定
"red",
"magenta",
"darkMagenta",
"gray",
"darkGreen",
"darkCyan",
"darkBlue"
)</code></pre><h2>具体实现</h2><h3>构造函数</h3><pre><code>def __init__(self, canvas, x, y, shape):
self.x = x # 方块在游戏空间的横坐标位置 1-10
self.y = y # 方块在游戏空间的纵坐标位置 1-20
self.canvas = canvas # 方块绘制的空间
self.objs = [] # 组合Block类对象
self.rotateCount = 0 # 方块旋转次数
self.shape = shape # 方块初始形态
self.color = TETRISCOLORS[shape] # 方块颜色
self.data = [ # 方块形态数据
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
curShape = TETRISAHPES[shape % len(TETRISAHPES)]
for i, b in enumerate(curShape): # 绘制方块初始形态
if b:
self.data[1 + i // TETRISDIMENSION][i % TETRISDIMENSION] = 1 # 形态数据初始化
self.objs.append(Block(canvas, self.x + i % TETRISDIMENSION, \ # 组合Block并绘制
self.y + 1 + i // TETRISDIMENSION, self.color))</code></pre><h3>判断游戏空间某个位置是否有Block</h3><p>这个函数很重要,判断是否越界、方块是否能移动都需要它。游戏空间比实际空间大一圈,最外围数据都初始化为1,作为越界哨兵。</p><pre><code>def hasBlock(self, x, y):
if x < 1 or x > 10 or y > 20:
return True
if GameRoom[y][x] == 1:
return True
else:
return False</code></pre><h3>判断一个方块(Tetris)是否能放置</h3><p>移动、旋转以及游戏是否结束的判断都要用到</p><pre><code>def canPlace(self, x, y):
for i in range(TETRISDIMENSION):
for j in range(TETRISDIMENSION):
if self.data[i][j] and GameRoom[y + i][x + j]:
return False
return True</code></pre><h3>清除方块</h3><p>清除操作由组合的Block类自行完成。</p><pre><code>def clean(self):
for block in self.objs:
block.clean()
self.objs.clear()</code></pre><h3>方块重绘</h3><p>旋转的时候使用,因为Block类的relocate使用是tkinter.move来实现的,而它的参数是相对距离。因此旋转的重绘比较麻烦,我采用了相对简单粗暴的方法来实现。</p><pre><code>def redraw(self):
self.clean() # 整体清除
for i in range(TETRISDIMENSION): # 生成全新Tetris
for j in range(TETRISDIMENSION): # 方法很简单、有效
if self.data[i][j]: # 但因为tkinter的问题,后来发现它加剧了内存泄漏问题
self.objs.append(Block(self.canvas, self.x + j, self.y + i, self.color))</code></pre><h3>内容预告</h3><p>移动和旋转放到下一篇中。 <br>因为tkinter和Timer的问题,后面在AI实现后,发现程序有严重的内存泄漏问题,这个问题把我好一阵折磨,欲后事如何,请持续关注,谢谢!</p><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(block类)
https://segmentfault.com/a/1190000040148599
2021-06-09T14:39:18+08:00
2021-06-09T14:39:18+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>block类</h2><p>在屏幕上绘制小方块,以及方块的移动与清除。根据tkinter库的函数特点,选择canvas.create_rectangle进行方块绘制,canvas.move移动方块,canvas.delete清除方块。画布操作函数只使用了上述三个。</p><h2>设计思路</h2><p>tkinter库提供的函数是组件式的,不是画一个图形,而是生成一个图元对象。可以获得这个图元的“句柄”,然后对其进行移动,删除。因为block类设计为构造、定位和清除三个方法,就足够完成我们的Tetris游戏。</p><h2>相关常数</h2><pre><code>TETRISDIMENSION = 4 # Tetris方块维度
BLOCKSIDEWIDTH = 30 # 最小方块边长设定
HALFBLOCKWIDTH = BLOCKSIDEWIDTH // 2 # 最小方块边长一半
CANVASOFFSET = 4 # 绘制偏移量,与游戏边框的距离
TETRISCOLORS = ( # Tetris方块的颜色设定
"red",
"magenta",
"darkMagenta",
"gray",
"darkGreen",
"darkCyan",
"darkBlue"
)</code></pre><h2>具体实现</h2><h3>构造函数</h3><pre><code>def __init__(self, canvas, x, y, color = "red") -> None:
self.canvas = canvas # 所在画布,gameCanvas or nextCanvas
self.x = x # 横会标位置,游戏空间位置,x > 0 and x < 11
self.y = y # 纵会标位置,游戏空间位置,y > 0 and y < 21
self.obj = canvas.create_rectangle((x - 1) * \ # 绘制小正方形,并存储,供清除用
BLOCKSIDEWIDTH + CANVASOFFSET, (y - 1) * \ # 游戏坐标与空间坐标对应,起始点
BLOCKSIDEWIDTH + CANVASOFFSET, \
x * BLOCKSIDEWIDTH + CANVASOFFSET, \ # 结束点
y * BLOCKSIDEWIDTH + CANVASOFFSET, \
fill = color, outline = "yellow") # 填充颜色与边框颜色
</code></pre><h3>移动函数</h3><pre><code>def relocate(self, detaX, detaY): # 参数为移动距离差
self.canvas.move(self.obj, \ # 移动到新位置
detaX * BLOCKSIDEWIDTH, \
detaY * BLOCKSIDEWIDTH)
self.x += detaX # 存储新的位置坐标
self.y += detaY</code></pre><h3>清除函数</h3><p>Tetris中组合了block,屏幕上的清除由block自己完成。</p><pre><code>def clean(self):
self.canvas.delete(self.obj)</code></pre><p>``</p><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(界面设计)
https://segmentfault.com/a/1190000040142790
2021-06-08T16:40:17+08:00
2021-06-08T16:40:17+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>界面设计</h2><p>本项目注重算法实现,为了安装简单且具有强跨平台性,所以选择python内置的tkinter。这个界面库是有些问题,比如后面发现其与线程锁配合有问题,会莫名死掉。项目中的lockbug分支,我已经把问题定位了,进行了一系列的日志输出,有兴趣的同学可以去研究一下具体问题。这个问题可以避开,本项目使用的界面特性很少,我的主张够用即可。</p><h2>设计思路</h2><p>使用两个canvas来显示方块,一个是游戏空间,另一个是下一方块显示。因为界面比较简单,分成三部分,一是游戏空间,二是下一方块显示,三是信息即操作按钮。这三大块直接使用place绝对定位,第三大块使用frame对Label,Button等元素进行组合。</p><h2>界面效果</h2><p><img src="/img/bVcSA6x" alt="" title=""></p><h2>具体实现</h2><h3>窗口设定</h3><pre><code>def start():
root = Tk()
root.title("Tetris")
root.geometry('470x630') # 窗口大小,中间是字母x表示乘号
root.resizable(0, 0) # 窗口大小固定
App(root)
root.mainloop()</code></pre><h3>两个canvas</h3><pre><code>from tkinter import *
from tkinter import ttk
# 常数CANVASOFFSET,目的是留一点边框空隙,在不同的操作系统上表现不一致,用空隙协调
self.gameCanvas = Canvas(root, bg='black', height=600 + CANVASOFFSET * 2, width=300 + CANVASOFFSET * 2)
self.gameCanvas.place(x=12, y=10) # 绝对定位
# 父窗口 背景设为黑色
self.nextCanvas = Canvas(root, bg='black', height=120 + CANVASOFFSET * 2, width=120 + CANVASOFFSET * 2)
self.nextCanvas.place(x = 330, y = 10)</code></pre><h3>信息操作单元</h3><p>使用frame来组合画布对象,使用anchor方式来布局,这块不能用side方式,注意两种方式的区别。</p><pre><code>frame = Frame(root)
frame.place(x = 330, y = 160)
Label(frame, text = "SPEED:").pack(anchor="w") # 使用anchor方式布局
self.speedVar = StringVar() # 游戏速度显示变量,注意这有圆括号,与后面的下拉框比较
speed = Label(frame, height=1, width=12, relief=SUNKEN, bd=1, textvariable=self.speedVar)
speed.pack(anchor="w")</code></pre><p>复选框与按钮:</p><pre><code>Checkbutton(frame, text = "AutoPlay").pack(anchor="w") # 复选框使用方式
self.btnStartVar = StringVar()
self.btnStartVar.set("Start") # 开始游戏按钮
Button(frame, height=1, width=10, command=self.btnStartClicked, textvariable=self.btnStartVar).pack(anchor="w")
# 绑定点击事件</code></pre><p>下拉框选择回放参数:</p><pre><code>coboxVar = StringVar # 注意这没有圆括号,与前面的游戏速度参数比较
cobox = ttk.Combobox(frame, textvariable=coboxVar, height=5, width=8, state="readonly")
cobox.pack(anchor="w") # 只能选择,不能修改
cobox["value"] = ("last", "one", "two", "three")
cobox.current(0) # 当前选择第一项
cobox.bind("<<ComboboxSelected>>", self.comboxClicked) # 绑定单击事件</code></pre><h3>游戏空间绑定键盘响应:</h3><pre><code>self.gameCanvas.bind(sequence="<Key>", func=self.processKeyboardEvent) # 绑定键盘响应
self.game = Game(self.gameCanvas, self.nextCanvas, self) # 初始化游戏对象
self.gameCanvas.focus_set() # 设置游戏空间为焦点对象</code></pre><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(项目结构)
https://segmentfault.com/a/1190000040122431
2021-06-04T16:35:55+08:00
2021-06-04T16:35:55+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<h2>系列文章入口</h2><p><a href="https://segmentfault.com/a/1190000040121026">《Python3编程实战Tetris机器人》</a></p><h2>项目结构</h2><p>项目的结构如下,以目录形式的模块包来组织代码,同时支持以目录或包来执行,两种方式具有统一入口。</p><pre><code>\ptetris
|---- .editorconfig
|---- .gitignore
|---- .vscode
| |---- launch.json
| |---- settings.json
|---- LICENSE.rst
|---- README.md
|---- tetris
| |---- app.py
| |---- block.py
| |---- config.py
| |---- game.py
| |---- tetris.py
| |---- __init__.py
| |---- __main__.py</code></pre><h2>实现细节</h2><p>我们的程序位于tetris目录(包)中,可以作为一个文件夹来执行:</p><pre><code>python tetris</code></pre><p>也可以作为一个包(Package)来执行:</p><pre><code>python -m tetris</code></pre><h3>__init__.py</h3><p>若要 python 将一个文件夹作为 Package 对待,那么这个文件夹中必须包含一个名为 __init__.py 的文件,即使它是空的。<br>我们在这个文件中定义main(),作为软件的统一入口。</p><pre><code>from tetris.app import start # 这个tetris目录下的文件都位于tetris包中了
def main():
print("Hi I'm Tetris!")
start() #真实入口在app.py中</code></pre><h3>__main__.py</h3><p>若要 python 运行一个文件夹,这个文件夹中必须包含一个名为 __main__.py 的文件<br>在__main__.py中调用__init__.py中的main(),统一程序入口。</p><pre><code>import tetris
tetris.main()</code></pre><p>这时以包方式运行没有问题,但以目录方式运行会出错:</p><pre><code>Traceback (most recent call last):
File "C:\Users\zhoutk\AppData\Local\Programs\Python\Python39\lib\runpy.py", line 197, in _run_module_as_main
return _run_code(code, main_globals, None,
File "C:\Users\zhoutk\AppData\Local\Programs\Python\Python39\lib\runpy.py", line 87, in _run_code
exec(code, run_globals)
File "d:\codes\ptetris\tetris\__main__.py", line 7, in <module>
import tetris
File "d:\codes\ptetris\tetris\tetris.py", line 2, in <module>
from tetris.config import *
ModuleNotFoundError: No module named 'tetris.config'; 'tetris' is not a package</code></pre><p>原因在于,sys.path 的第一个搜索路径,一个是包,一个是空字符串。对于 python -m tetris 的调用方式来说,由于 __init__.py 被事先载入,此时 python 解释器已经知道了这是一个包,<br>因此当前路径(空字符串)被包含在 sys.path 中。然后再调用 __main__.py ,这时 import pkg 这个包就没有问题了。 <br>而对于 python tetris的调用方式来说,python 解释器并不知道自己正在一个 Package 下面工作。默认的,python 解释器将 __main__.py 的当前路径 tetris加入 sys.path 中,然后在这个路径下面寻找 tetris这个模块,因此出错。 <br>在__main__.py开头加入如下代码,就能解决这个问题:</p><pre><code>import os, sys
if not __package__:
path = os.path.join(os.path.dirname(__file__), os.pardir)
sys.path.insert(0, path)</code></pre><h3>vscode的配置</h3><p>vscode来写python还是很不错的,做两步操作,就可以很好的编写python程序了。</p><h4>安装插件</h4><p>只要安装ms-python.python插件就好了,相关的另外几个会自动安装的。<br><img src="/img/bVcSvPN" alt="" title=""></p><h4>配置launch.json</h4><pre><code>"configurations": [
{
"name": "Tetris",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/tetris",
"console": "integratedTerminal"
}
]</code></pre><p>这样就可以用vscode打开ptetris目录,按F5运行程序了。</p><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
Python3编程实战Tetris机器人(序)
https://segmentfault.com/a/1190000040121026
2021-06-04T14:49:50+08:00
2021-06-04T14:49:50+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>背景</h2><p>本系列文章,使用Python3一步步记录Tetris游戏的编写全过程,游戏功能包括手动游戏、游戏回放(数据库操作)、自动游戏(AI机器人)、强化学习、优化AI机器人。已经完成C++版本,<a href="https://segmentfault.com/a/1190000039952879">Qt5之QGraphicsItem编写Tetris俄罗斯方块游戏</a>。</p><h2>规划</h2><ul><li><a href="https://segmentfault.com/a/1190000040122431">项目结构</a></li><li><a href="https://segmentfault.com/a/1190000040142790">界面设计</a></li><li><a href="https://segmentfault.com/a/1190000040148599">block类,最小方块定义</a></li><li><a href="https://segmentfault.com/a/1190000040204101">tetris类,俄罗斯方块定义</a></li><li><a href="https://segmentfault.com/a/1190000040206237">tetris方块的移动与旋转</a></li><li><a href="https://segmentfault.com/a/1190000040211839">game类,游戏流程控制</a></li><li><a href="https://segmentfault.com/a/1190000040218933">多线程改造</a></li><li><a href="https://segmentfault.com/a/1190000040233872">游戏暂停功能</a></li><li><a href="https://segmentfault.com/a/1190000040279296">设计通用数据库操作封装,基于sqlite3</a></li><li><a href="https://segmentfault.com/a/1190000040292331">设计ORM实现自动查询</a></li><li>存储历史数据,实现游戏回放</li><li>简单AI</li><li>内存泄漏修正</li><li>强化学习</li><li>加强版AI</li></ul><h2>设计思路</h2><p>该游戏尽量不使用第三方库,主要注重算法,因此界面库选择python内置的tkinter。设计思想也采用传统的方式,用一个二维数组来控制游戏空间,类似迷宫的方式。选择这种方式有一个好处是,游戏的数据直观存在,容易获取。</p><h2>效果图</h2><p><img src="/img/bVcSQTB" alt="" title=""></p><blockquote>游戏空间大小</blockquote><p>10 x 20</p><blockquote>得分设计:</blockquote><ul><li>消一层得一分</li><li>消二层得三分</li><li>消三层得七分</li><li>消四层得十分</li></ul><blockquote>控制键位</blockquote><ul><li>方向上键 : 旋转</li><li>方向左键 : 左移</li><li>方向右键 : 右移</li><li>方向下键 : 下移</li><li>空格按键 : 下移到底</li></ul><h2>项目进度</h2><p>已经完成手动游戏、游戏回放、简单AI、手动自动随时切换。</p><h2>项目标签(里程牌)</h2><ul><li>v1.0.1(manual-play) - 手动游戏功能 里程碑版</li><li>v1.1(auto-play) - 自动游戏功能 里程碑版,发现内存泄漏</li><li>v1.2(less-block) - 解决内存泄漏</li></ul><h2>项目地址</h2><pre><code>https://gitee.com/zhoutk/ptetris
或
https://github.com/zhoutk/ptetris</code></pre><h2>运行方法</h2><pre><code>1. install python3, git
2. git clone https://gitee.com/zhoutk/ptetris (or download and unzip source code)
3. cd ptetris
4. python3 tetris
This project surpport windows, linux, macOs
on linux, you must install tkinter first, use this command:
sudo apt install python3-tk</code></pre><h2>相关项目</h2><p>已经实现了C++版,项目地址:</p><pre><code>https://gitee.com/zhoutk/qtetris</code></pre>
QT5写Tetris之AI机器人玩游戏
https://segmentfault.com/a/1190000040047974
2021-05-22T22:51:14+08:00
2021-05-22T22:51:14+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>背景</h2><p>使用Qt5.12.9的QGraphicsItem来实现俄罗斯方块,使用简单的评估函数,实现AI机器人玩俄罗斯方块游戏。这是AI机器人的第一步,这个算法很简单,但很有效,大多数情况能消5百层以上,最近的为数不多的测试中,最高纪录已经消了超过2500层。在这个基础上,可以方便的积累原始数据,我希望能抽取模式,进行模式识别及至机器学习。</p><h2>思路</h2><p>在手动游戏基础上进行改造,借鉴回放的经验,只需要加入一个评估算法,为每一个新方块找出一个放置的姿态(旋转次数)和最终位置坐标就可以了。我的算法设计也很简单,就是为每一个方块穷举其放置方法,使用一个紧密程度的评估算法进行评分,取出最高分的操作,若有相同得分的操作,用随机数二一添做五。</p><h2>效果图</h2><p><img src="/img/bVcScka" alt="" title=""></p><h2>关键代码分析</h2><h3>流程控制</h3><p>界面操作控制变量,做到随时可以在手动与自动两种模式之间进行切换。</p><pre><code> if (isAutoRunning) { //自动模式
autoProcessCurBlock(); //处理当前方块,使用评估函数确定方块的最终姿态与位置
block->relocate(curPos); //放置
block->setBlockNotActive(); //固定方块
generateNextBlock(); //取下一个方块,游戏继续
}else //手动模式
this->moveBlockDown();
...</code></pre><h3>方块放置评分函数</h3><p>我的设计思想很直观,俄罗斯方块就是要尽量紧密的堆积在一起,所以对每一个组成方块的block都检测一个它周围的四个位置,看是否有block(包括边界)存在,若有就加1分,没有不加分。这块的加分,并没有区别组成方块自身的block和外界的block,因为每个方块都是与自己进行比较,所以区分与不区分效果是一样的。起始分由深度确定,越深我认为效果越好。另个,block的垂直下方最好不要有空洞,若有会减分。</p><pre><code>int Game::evaluate(Tetris* t)
{
QPoint pos = t->getPos();
int ct = pos.y(); //深度为基础分
int cct = t->cleanCount();
if (cct > 1) //能消层,加分
ct += 10 * (cct - 1);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (t->data[i][j]) {
ct += t->hasTetrisBlock(pos.x() + j + 1, pos.y() + i) ? 1 : 0; //检测block右边的位置
ct += t->hasTetrisBlock(pos.x() + j - 1, pos.y() + i) ? 1 : 0; //检测block左边的位置
ct += t->hasTetrisBlock(pos.x() + j, pos.y() + i + 1) ? 1 : 0; //检测block下方的位置
ct += t->hasTetrisBlock(pos.x() + j, pos.y() + i - 1) ? 1 : 0; //检测block上方的位置
if (i == 3 || t->data[i + 1][j] == 0) {
if (!t->hasTetrisBlock(pos.x() + j, pos.y() + i + 1)) { //block下方的紧临空洞
ct -= 4;
}
else {
int k = 2;
while (pos.y() + i + k <= 19) {
if (!t->hasTetrisBlock(pos.x(), pos.y() + i + k)) { //block下方的非紧临空洞
ct -= 1;
break;
}
k++;
}
}
}
}
}
}
return ct;
}</code></pre><h3>穷举方块的所有放置方式</h3><p>一个方块最多只有四种姿态,把方块的每一种姿态都从左到右moveDownEnd一次,进行评分,取得分最高的方案。</p><pre><code>void Game::autoProcessCurBlock()
{
int max = 0;
QPoint initPos = block->getPos();
Tetris* tmp = new Tetris(initPos, block->getShape(), -1); //构造当前方块的替身,blockType为-1,这种方块不会显示
int rotateCt = block->getRotateNum(); //同步替身初始姿态
for (int k = 0; k < rotateCt; k++)
tmp->rotate();
rotateCt = 0; //用于保存方块的最终姿态
for (int r = 0; r < 4; r++) { //四种姿态遍历,其实可以优化,有的方块不需要四次
if (r > 0) {
tmp->relocate(initPos); //注意,旋转要在方块进入游戏界面的地方旋转,不然可能旋转不成功
tmp->rotate();
}
while (tmp->moveLeft()); //从最左边开始
do {
tmp->moveDownEnd();
tmp->setBlockNotActive(); //固定方块,以便进行评分
int score = evaluate(tmp); //评分
if (score > max) { //找到当前最优方案
max = score;
curPos = tmp->getPos();
rotateCt = r;
}
else if (score == max) { //出现相等评分,随机取
if (qrand() % 2 == 1) {
curPos = tmp->getPos();
rotateCt = r;
}
}
//initPos.setX(tmp->getPos().x());
tmp->relocate(QPoint(tmp->getPos().x(), initPos.y())); //返回到游戏空间上方
tmp->setBlockTest(); //方块恢复到测试状态
} while (tmp->moveRight()); //方块右移,直到不能移动
}
delete tmp; //销毁测试方块,突然想到这块可以优化,只需要建七个方块就好,这样就不用不断的创建和销毁了
for (int k = 0; k < rotateCt; k++)
block->rotate();
}</code></pre><h3>下一步的设想</h3><p>使用python重新实现所有功能,也不再用Qt,就用python自带的tkinter就好。把重点放在模式提取,让AI自动玩游戏,写个算法,提取优秀的操作模式。然后使用模式匹配或机器学习算法来优化AI。现在还没有具体的想法,只有这么个大概的设想。</p><h2>源代码及运行方法</h2><p>项目采用cmake组织,请安装cmake3.10以上版本。下面脚本是windows下基于MSVC的,其它操作系统上基本类似,或者使用qtcreator打开进行操作。</p><pre><code>cmake -A win32 -Bbuild .
cd build
cmake --build . --config Release</code></pre><p>注:本项目采用方案能跨平台运行,已经适配过windows,linux,mac。</p><p>源代码:</p><pre><code>https://gitee.com/zhoutk/qtetris.git</code></pre><p>或</p><pre><code>https://gitee.com/zhoutk/qtdemo/tree/master/tetrisGraphicsItem</code></pre><p>或</p><pre><code>https://github.com/zhoutk/qtDemo/tree/master/tetrisGraphicsItem</code></pre>
QT5写Tetris之使用Sqlite3实现游戏回放
https://segmentfault.com/a/1190000040047828
2021-05-22T21:54:06+08:00
2021-05-22T21:54:06+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>背景</h2><p>使用Qt5.12.9的QGraphicsItem来实现俄罗斯方块,使用Sqlit3存储数据来进行游戏的回放,既然已经使用QT,就尽量用其组件,重写了原来的JSON封装及数据库操作接口实现。</p><h2>思路</h2><p>尽量复用已经实现的代码,所以只记录了每个方块的形状与姿态(旋转次数)及最终位置。与真实游戏的区别只是在于方块的来源一个是随机生成,一个是数据库。记录的ID我引用了原来使用的snowflake模型,但却使用了QString类型,无它,只是长整型在编程的过程中总是不好控制,莫名的被改成科学计数法或溢出。数据库操作使用C++对sqlit3库进行封装并结合Qjson实现ORM自动操作。Sqlit3的动态链接库,在windows、linux、mac各平台下需要重新编译。</p><h2>效果图</h2><p><img src="/img/bVcScqP" alt="" title=""></p><h2>关键代码分析</h2><h3>cmake之操作系统选择</h3><pre><code>if(MSVC) #windows
set(EnclibName Sqlit3.dll)
elseif(APPLE) #mac
set(EnclibName sqlite3.o)
elseif(UNIX) #linux
set(EnclibName libsqlite3.so)
endif()</code></pre><h3>Qjson.h - json操作封装</h3><p>我的数据操作实现中的信息传递,都是以json对象形式进行的,封装QJsonObject、QJsonArray等,提供方便的json操作接口,实现参数传递与SQL自动生成。我的实现Object是一等公民,Array是二等公民只作为Qjson的一个子项。这样既能满足应用又把设计大大的简化了,规范使用,避免误用。</p><pre><code>class Qjson {
private:
QJsonObject* json; //真实的json对象,我只是对其封装,简化操作或转化成简单的操作
bool _IsObject_; //是否是正确的json对象,构造函数中对其进行判断,表示json对象的有效性
public:
Qjson();
Qjson(const Qjson& origin); //复制构造函数
QString operator[](QString key); //下标操作,取特定key对应的值,使用QString返回,可以方便进行类型转换
Qjson& operator = (const Qjson& origin); //赋值构造函数
bool HasMember(QString key) ;
Qjson ExtendObject(Qjson obj); //合并两个Object,第一个中同名key会被覆盖
template<typename T> void AddValueBase(QString k, T v); //key - value 插入模板函数,基础类型插入它就能搞定
void AddValueObjectsArray(string k, QVector<Qjson>& arr) ; //Array类型值插入
QString GetJsonString(); //取得json的字符串
void GetValueAndTypeByKey(QString key, QString* v, int* vType); //取值的真正操作函数
QStringList GetAllKeys(); //取得json对象的所有key,用于对象遍历
bool IsObject();
private:
QJsonObject* GetOriginRapidJson(); //取得真实的json对象,类内部使用
};</code></pre><h3>数据库通用接口 - Idb.h</h3><p>经实践总结,数据库操作有以下接口,百分之九十以上的需求就都能满足。</p><pre><code>class Idb
{
public:
virtual Qjson select(QString tablename, Qjson& params, QStringList fields = QStringList(), int queryType = 1) = 0;
virtual Qjson create(QString tablename, Qjson& params) = 0;
virtual Qjson update(QString tablename, Qjson& params) = 0;
virtual Qjson remove(QString tablename, Qjson& params) = 0;
virtual Qjson querySql(QString sql, Qjson params = Qjson(), QStringList filelds = QStringList()) = 0;
virtual Qjson execSql(QString sql) = 0;
virtual Qjson insertBatch(QString tablename, QVector<Qjson> elements, QString constraint = "id") = 0;
virtual Qjson transGo(QStringList sqls, bool isAsync = false) = 0;
};</code></pre><h3>数据库操作标准实现 - DbBase.h</h3><p>我们的上层应用都是操作这个实现,这个类组合了一个Idb的具体实现,从而达到与具体的数据库解耦的目的,可以轻松的切换不同的数据库实例。</p><pre><code>class DbBase
{
public:
DbBase(QString connStr, QString dbType = "sqlit3") : connStr(connStr) {
dbType.toLower();
if (dbType.compare("sqlit3") == 0)
db = new Sqlit3::Sqlit3Db(connStr);
else {
throw "Db Type error or not be supported. ";
}
};
...
};</code></pre><h3>Sqlit3的C++封装类 - Sqlit3Db.h</h3><p>实现了Sqlit3的Idb接口,下面抽了几个关键点进行一些说明:</p><h4>std::unique_ptr</h4><p>数据连接使用了std::unique_ptr,在gcc下一定要引入 memory 头文件,这个问题折腾了我好久,在windows下没出问题,但在linux下一直报错。</p><h4>sql语句中中文的支持</h4><p>要保证送入底层的sql语句的编码为Utf-8,为了保证对所有操作系统的支持,选择使用QString::fromUtf8(szU8)来进行转换。</p><pre><code>Qjson ExecNoneQuerySql(QString aQuery) {
Qjson rs = Utils::MakeJsonObjectForFuncReturn(STSUCCESS);
sqlite3_stmt* stmt = NULL;
sqlite3* handle = getHandle();
char * u8Query = Utils::UnicodeToU8(aQuery); //编码转换,函数中的主要功能由QString::fromUtf8完成
const int ret = sqlite3_prepare_v2(handle, u8Query, strlen(u8Query), &stmt, NULL);
if (SQLITE_OK != ret)
{
QString errmsg = sqlite3_errmsg(getHandle());
rs.ExtendObject(Utils::MakeJsonObjectForFuncReturn(STDBOPERATEERR, errmsg));
}
else {
sqlite3_step(stmt);
}
sqlite3_finalize(stmt);
qDebug() << "SQL: " << aQuery << endl; //日志输入使用未转换的
return rs;
}</code></pre><h4>批量插入操作</h4><p>游戏记录必须使用批量插入操作,不能一条一条插入。sqlit3的批量操作与mysql有些不同,用的是select ... union all,其实也只是按规定作好SQL语句拼接就可以了。</p><pre><code>Qjson insertBatch(QString tablename, QVector<Qjson> elements, QString constraint) {
QString sql = "insert into ";
if (elements.empty()) {
return Utils::MakeJsonObjectForFuncReturn(STPARAMERR);
}
else {
QString keyStr = " (";
keyStr.append(elements[0].GetAllKeys().join(',')).append(" ) "); //取出参数第一个元素的所有key,组装数据库字段
for (size_t i = 0; i < elements.size(); i++) {
QStringList keys = elements[i].GetAllKeys(); //取出参数的所有key,实现对json对象的遍历
QString valueStr = " select ";
for (size_t j = 0; j < keys.size(); j++) {
valueStr.append("'").append(elements[i][keys[j]]).append("'");
if (j < keys.size() - 1) {
valueStr.append(",");
}
}
if (i < elements.size() - 1) { //拼接下一条记录
valueStr.append(" union all ");
}
keyStr.append(valueStr);
}
sql.append(tablename).append(keyStr);
}
return ExecNoneQuerySql(sql);
}</code></pre><h4>JOSN-ORM的使用方法</h4><p>数据查询使用的智能的ORM实现,具体方法请参照前文<a href="https://segmentfault.com/a/1190000023023978">《c++关系数据库访问通用接口设计》</a></p><h3>Playback 功能实现</h3><p>回放设置了几个参数</p><ul><li>last : 最后一次游戏的回放</li><li>one :积分排行第一的游戏</li><li>two :积分排行第二的游戏</li><li>three :积分排行第三的游戏</li></ul><h2>源代码及运行方法</h2><p>项目采用cmake组织,请安装cmake3.10以上版本。下面脚本是windows下基于MSVC的,其它操作系统上基本类似,或者使用qtcreator打开进行操作。</p><pre><code>cmake -A win32 -Bbuild .
cd build
cmake --build . --config Release</code></pre><p>注:本项目采用方案能跨平台运行,已经适配过windows,linux,mac。</p><p>源代码:</p><pre><code>https://gitee.com/zhoutk/qtetris.git</code></pre><p>或</p><pre><code>https://gitee.com/zhoutk/qtdemo/tree/master/tetrisGraphicsItem</code></pre><p>或</p><pre><code>https://github.com/zhoutk/qtDemo/tree/master/tetrisGraphicsItem</code></pre>
Qt5之QGraphicsItem编写Tetris俄罗斯方块游戏
https://segmentfault.com/a/1190000039952879
2021-05-06T16:09:45+08:00
2021-05-06T16:09:45+08:00
zhoutk
https://segmentfault.com/u/zhoutk
3
<h2>背景</h2><p>使用Qt5.12.9的QGraphicsItem来实现俄罗斯方块,现在是C++版本,下来还会有python版本,以及方便的接口,来接入算法,由机器人玩俄罗斯方块。</p><h2>思路</h2><ul><li>CustomGraphBase类继承自QGraphicsObject,提供必要的虚函数。</li><li>CustomGraphTetrisBlock类继承自CustomGraphBase,实现最小方块,分边框类型(0)与方块类型(1)。</li><li>CustomGraphTetrisText类继承自CustomGraphBase,显示文字,类型为5。</li><li>Tetris类组合CustomGraphTetrisBlock,显示俄罗斯方块。</li><li><p>Game类为游戏逻辑控制类。</p><p>该游戏传统的编程方式,是用一个二维数组来控制游戏空间,类似迷宫的方式。其实选择QGraphicsItem来实现就是一种很另类的选择,其实用gdi来做更方便,这种规模,QGraphicsItem没有优势,只是个人学习探索的选择。 <br> 我没有用二维数组来控制游戏空间,而是在边沿上用了一圏CustomGraphTetrisBlock来定义游戏空间,因为所有的items都能方便的在scene上检索到,所以看一个方块是否能移动,就需要检索自己的周围是否已经被其它方块占据。这里有一点,在方块进行旋转的时候,就要判断区分组成自己的block和别人的方块。</p></li></ul><h2>效果图</h2><p><img src="/img/bVcRNIL" alt="" title=""><br><img src="/img/bVcSckC" alt="" title=""></p><h2>关键代码分析</h2><p>功能尽量内聚,类CustomGraphTetrisBlock封装小方块,Tetris类组合了Block,封装了俄罗斯方块的绝大部分操作,类Game游戏的整体流程。</p><h3>CustomGraphBase自定义图元基类</h3><pre><code>class CustomGraphBase : public QGraphicsObject
{
Q_OBJECT
public:
CustomGraphBase();
public:
virtual QRectF boundingRect() const = 0; //占位区域,必须准确,才能很好的显示与清除
virtual int type() const = 0;
virtual void relocate() = 0; //移动,重定位
virtual bool isActive() { return false; };//未落地的方块
virtual int getBlockType() { return 0; }; //方块类型,主要区别边沿方块
};</code></pre><h3>CustomGraphTetrisBlock 最小方块,组成俄罗斯方块的基本元素</h3><p>paint 重绘操作,需要操作边沿方块,边沿方块只占位,不显示。要注意prepareGeometryChange()函数的使用,不能放在这个函数中,不然会不停的重绘,占用大量CPU资源。具体原理我还没研究透,我将其放到relocateb函数中了。</p><pre><code>void CustomGraphTetrisBlock::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget /*= nullptr*/)
{
if (blockType) {
painter->drawRoundedRect(
0,
0,
BLOCKSIDEWIDTH,
BLOCKSIDEWIDTH,
2, 2
);
}
//prepareGeometryChange();
}</code></pre><p>relocate元素重定位,只需将其放到scene上正确的坐标</p><pre><code>void CustomGraphTetrisBlock::relocate()
{
this->setPos(pos * BLOCKSIDEWIDTH);
prepareGeometryChange();
}</code></pre><h3>Tetris类,俄罗斯方块类</h3><p>七类方块的定义</p><pre><code>QVector<QVector<int>> SHAPES = {
{1, 1, 1, 1},
{0, 1, 1, 1, 0 , 1},
{1, 1, 1, 0, 0, 0, 1},
{0, 1, 1, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1},
{0, 1, 1, 0, 1, 1},
{0, 1, 0, 0, 1, 1, 1}
};</code></pre><p>俄罗斯方块的构建</p><pre><code>QVector<int> curShape = SHAPES[shape % SHAPES.size()];
for (int i = 0; i < curShape.size(); i++) {
if (curShape[i]) {
data[1 + i / sideLen][i % sideLen] = true;
CustomGraphTetrisBlock* block = new CustomGraphTetrisBlock(pos + QPoint(i % sideLen, 1 + i / sideLen), 2, shape);
blocks.push_back(block); //存储组成该方块的所有元素,在落到底之前需要由Tetris类控制其运动
MainWindow::GetApp()->GetScene()->addItem(block); //加入block到scene,显示方块
}
}</code></pre><p>hasTetrisBlock函数检测位置上是否有方块</p><pre><code>CustomGraphTetrisBlock* Tetris::hasTetrisBlock(int x, int y)
{
auto items = MainWindow::GetApp()->GetScene()->items(QPointF((x + 0.5) * BLOCKSIDEWIDTH, (y + 0.5) * BLOCKSIDEWIDTH));
foreach (auto al , items)
{
if (!(((CustomGraphBase*)al)->isActive()) && (((CustomGraphBase*)al)->type()) == TETRISBLOCKTYPE) { //要区别组合俄罗斯方块本身的block与其它的block
return (CustomGraphTetrisBlock*)al; //返回方块,提供给清除行操作用
}
}
return nullptr;
}</code></pre><p>rotate函数进行俄罗斯方块的旋转</p><pre><code>bool Tetris::rotate()
{
int i, j, t, lenHalf = sideLen / 2, lenJ;
for (i = 0; i < lenHalf; i++)
{
lenJ = sideLen - i - 1;
for (j = i; j < lenJ; j++)
{ //先行判断是否能旋转,要移动的点不为0时,判断目标点是否已经有block存在
int lenI = sideLen - j - 1;
if (data[i][j] && this->hasTetrisBlock(pos.x() + lenJ, pos.y() + j) ||
data[lenI][i] && this->hasTetrisBlock(pos.x() + j, pos.y() + i) ||
data[lenJ][lenI] && this->hasTetrisBlock(pos.x() + i, pos.y() + lenI) ||
data[j][lenJ] && this->hasTetrisBlock(pos.x() + lenI, pos.y() + lenJ)){
return false;
}
}
}
for (i = 0; i < lenHalf; i++)
{ //选择了顺时针90度旋转,使用了螺旋移动算法,网上可以容易搜索到说明。
lenJ = sideLen - i - 1;
for (j = i; j < lenJ; j++)
{
int lenI = sideLen - j - 1;
t = data[i][j];
data[i][j] = data[lenI][i];
data[lenI][i] = data[lenJ][lenI];
data[lenJ][lenI] = data[j][lenJ];
data[j][lenJ] = t;
}
}
this->relocate();
return true;
}</code></pre><p>cleanRow函数实现行清除</p><pre><code>int Tetris::cleanRow()
{ //该清除算法效率不高,是以一行来处理的,这块以后可以优化。
int h = 19, levelCount = 0;
while (h >= 0) {
int count = 0;
for (int i = 0; i < 10; i++) { //判断是否行满
if (!this->hasTetrisBlock(i, h)) {
count++;
}
}
if (count == 0) { //行满,需要清除并整体下移
int level = h;
levelCount++;
bool first = true;
while (level >= 0) {
int ct = 0;
for (int j = 0; j < 10; j++) {
if(first) //第一个外循环删除满行上的图元,后面是整体下移
this->erase(j, level);
CustomGraphTetrisBlock* block = this->hasTetrisBlock(j, level - 1);
if (!block) {
ct++;
}
else {
block->relocate(QPoint(j, level)); //下移一个位置
}
}
first = false;
if (ct == 10) { //一行上都没有图元,工作完成,提前结束
break;
}
else {
level--;
}
}
}
else if (count == 10) {
break;
}
else {
h--;
}
}
return levelCount;
}</code></pre><h2>源代码及运行方法</h2><p>项目采用cmake组织,请安装cmake3.10以上版本。下面脚本是windows下基于MSVC的,其它操作系统上基本类似,或者使用qtcreator打开进行操作。</p><pre><code>cmake -A win32 -Bbuild .
cd build
cmake --build . --config Release</code></pre><p>注:本项目采用方案能跨平台运行,已经适配过windows,linux,mac。</p><p>源代码:</p><pre><code>https://gitee.com/zhoutk/qtetris.git</code></pre><p>或</p><pre><code>https://gitee.com/zhoutk/qtdemo/tree/master/tetrisGraphicsItem</code></pre><p>或</p><pre><code>https://github.com/zhoutk/qtDemo/tree/master/tetrisGraphicsItem</code></pre>
QFtp源码学习及目录下载
https://segmentfault.com/a/1190000039184378
2021-02-06T16:49:36+08:00
2021-02-06T16:49:36+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>背景</h2><p>需要在QT5中进行FTP文件下载,并需要支持整目录下载,经过对比选择,最后决定使用Qt4中的QFtp来完成我们的需求。因此决定学习源码,看清结构,做到能真正解决所要面对的问题。</p><h2>分解源码</h2><p>Qftp一共只有四个文件,主要文件是qftp.cpp,这个文件中,有太多的类,首先按类分解到各自文件中,这样利用官方的示例代码,跑起来后,可以方便的查看代码。</p><h3>类说明</h3><ul><li>class QFtpCommand : 此类是对FTP命令的封装,将命令与QIODevice设备关联起来,并返回一个唯一的标识ID。</li><li>class QFtpPI : 此类是对FTP协议的封装,processReply是主要函数,应答服务端响应。</li><li>class QFtpDTP : 此类是数据操作封装,数据读取、解析、存储都是在此类中处理。</li><li>class QFtpPrivate : 此类是QFtp的实际操作类,被组合到QFtp类中,是逻辑处理中心。</li><li>class QFtp : 此类是外壳,用户直接面对。</li><li>class QUrlInfo : 此为信息类,存储接收到的每一条文件数据信息。</li></ul><h3>运行流程</h3><p>所有的客户端命令被压入到命令堆栈。一个命令运行有两个入口:一是命令被压入堆栈时,若堆栈中只有一条命令,即被运行;二是作响应服务端响应时,类型为idle或not waiting。<br>每个命运被构造时,都会返回唯一ID,这是很重要的一点,因为命令大多关联着一本地IO设备,在清理IO时,要注意与命令对应,因为所有的操作都是异步的。</p><h3>改造list响应</h3><p>QFtp列当前目录的原有逻辑是取一条数据就发送一条文件或目录的消息,这样在我们连续遍历目录时,无法分清楚是哪个目录下的数据,无法进行正确的递归。这样改造效率应该会好一些且完全控制整个目录的显示,比如,目录显示在上面。当然也可以将递归放到commandFinished消息响应中去。 </p><p>QFtpDTP::socketReadyRead() - 修改读取目录列表时的信号发送方式</p><pre><code>if (pi->currentCommand().startsWith(QLatin1String("LIST"))) {
QVector<QUrlInfo> infos; //增加vector来存储整个目录信息
while (socket->canReadLine()) {
QUrlInfo i;
QByteArray line = socket->readLine();
if (parseDir(line, QLatin1String(""), &i)) {
infos.push_back(i);
//emit listInfo(i); //原来在循环内,读一条数据发送一个listInfo信号
}
else {
if (line.endsWith("No such file or directory\r\n"))
err = QString::fromLatin1(line);
}
}
emit listInfos(infos); //改为在循环外发送新增的listInfos信号
}</code></pre><p>FtpWindow::addToList(const QVector<QUrlInfo>& urlInfos) - listInfos响应修改</p><pre><code> for (int i = 0; i < urlInfos.size(); i++)
{
QTreeWidgetItem* item = new QTreeWidgetItem;
QUrlInfo urlInfo = urlInfos[i];
if (urlInfo.name().compare(".") != 0) {
item->setText(0, urlInfo.name().toLatin1());
item->setText(1, QString::number(urlInfo.size()));
item->setText(2, QString::number(urlInfo.isDir()));
item->setText(3, urlInfo.owner());
item->setText(4, urlInfo.group());
item->setText(5, urlInfo.lastModified().toString("MMM dd yyyy"));
QPixmap pixmap(urlInfo.isDir() ? ":/images/dir.png" : ":/images/file.png");
item->setIcon(0, pixmap);
isDirectory[urlInfo.name()] = urlInfo.isDir();
fileList->addTopLevelItem(item);
}
}</code></pre><h3>目录下载</h3><p>将FtpWindow::downloadFile() slot分解成两个函数,增加downAllFile(QString rootDir)来完成目录递归。</p><pre><code>void FtpWindow::downloadFile()
{
files.clear(); //初始化本地设备
downDirs.clear(); //清空需要下载的目录堆栈
downAllFile(currentPath); //下载具体操作,另一个入口在list的响应中
showProgressDialog(); //进度条显示
}</code></pre><p>下载的真实操作函数</p><pre><code>void FtpWindow::downAllFile(QString rootDir) {
QString thisRoot(rootDir + "/"); //要下载的父目录
QList<QTreeWidgetItem*> selectedItemList = fileList->selectedItems();
for (int i = 0; i < selectedItemList.size(); i++)
{
QString fileName = selectedItemList[i]->text(0);
if (isDirectory.value(fileName)) { //若是子目录,组合完成的目录,压入待下载目录堆栈
if(fileName != "..")
downDirs.push(thisRoot + fileName);
}
else {
downloadTotalBytes += selectedItemList[i]->text(1).toLongLong(); //统计需要下载的字节量
...
QFile* file = new QFile(dirTmp.append("/").append(fileName));
//文件下载请求,是异步操作
int id = ftp->get(QString::fromLatin1((selectedItemList[i]->text(0)).toStdString().c_str()), file);
files.insert(id, file); //本地IO设备与其命令绑定并存储
}
}
if (downDirs.size() > 0) { //待下载目录堆栈不空,处理一条
enterSubDir = true; //表示正在下载目录
QString nextDir(downDirs.pop()); //取需要处理的下一个目录
ftp->cd(nextDir); //切换到这个目录
currentDownPath = nextDir;
ftp->list(); //列目录,在其响应中将再递归调用本函数~~~~
}
}</code></pre><p>list响应的递归处理部分</p><pre><code> if (!enterSubDir) { //下载的文件中没有目录
...
}
else { //正处理于目录下载中
fileList->selectAll(); //选中列表中所有
downAllFile(currentDownPath); //递归调用下载处理函数
}</code></pre><h2>项目地址</h2><pre><code>https://github.com/zhoutk/qtDemo</code></pre><h2>命令行编译</h2><pre><code>git clone https://github.com/zhoutk/qtDemo
cd qtDemo/ftpClient & mkdir build & cd build
cmake ..
cmake --build . </code></pre><p>编译时注意:cmake默认为x86架构,需要与你安装的Qt版本对应;编译好了,运行前,请注意目录结构是否正确。</p><h2>小结</h2><p>我选择的这种目录下载方式比较麻烦,没有放到后台再开一个进程去处理,试图做整体考虑,且整个运行过程都是异步的,调试也比较难,其中进度条控件控制还有些坑,需要小心处理。过程艰难,收获颇多。</p>
QT5编译使用QFtp
https://segmentfault.com/a/1190000039151470
2021-02-02T13:17:57+08:00
2021-02-02T13:17:57+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h3>背景</h3><p>使用 QNetworkAccessManager 可以实现 Ftp 的上传/下载功能,但它没有提供例如list、cd、remove、mkdir、rmdir、rename 等功能。这种情况下,我们可以使用QFtp,需要下载源码、编译并处理一些坑。</p><h3>下载</h3><p>从 GitHub 下载 QFtp:</p><pre><code>https://github.com/qt/qtftp</code></pre><h3>编译</h3><ul><li>修改 qftp/qftp.pro,删除最后一行,module_qtftp_tests。不然编译会有错误,这个是测试子项目,暂时去除,先编译使用。</li><li>修改 qftp/src/qftp/qftp.h 第47行 #include <QtFtp/qurlinfo.h> => #include <qurlinfo.h></li><li>修改 qftp/src/qftp/qftp.pro 第4,5行的+,-号互换,生成*.dll</li><li>修改第4行为 CONFIG += staticlib,生成<em>.lib和</em>.prl</li></ul><p>用qtcreator打开qftp/qftp.pro,编译生成库文件。</p><h3>放入QT5安装目录中</h3><p>我以Qt5.5.1为例说明,其它版本类似</p><ul><li>将 Qt5Ftpd.lib、Qt5Ftp.lib、Qt5Ftpd.prl、Qt5Ftp.prl 拷贝至 D:\Qt\Qt5.5.1\5.5\msvc2010\lib。</li><li>将Qt5Ftpd.dll、Qt5Ftp.dll 拷贝至D:\Qt\Qt5.5.1\5.5\msvc2010\bin。</li><li>将qftp.h、qurlinfo.h 拷贝至 D:\Qt\Qt5.5.1\5.5\msvc2010\include\QtNetwork,并新建一个名为 QFtp 的文件(没有后缀名),然后用本写入 <code>#include "qftp.h"</code>。</li></ul><h3>运行示例项目</h3><ol><li>作为qftp/qftp.pro 项目的子项目,直接编译examples项目,只需要更改qftp\examples\qftp\ftpwindow.cpp中的43行,将#include <QtFtp>改为#include <QFtp>,即可以编译并运行。</li><li>作为独立项目,除了修改1中的这项,还需要修改qftp\examples\qftp\qftp.pro中ftp库的加载方式,从第五行中删除ftp,然后增加如下代码:</li></ol><pre><code>CONFIG(debug, debug|release) {
LIBS += -lQt5Ftpd
} else {
LIBS += -lQt5Ftp
}</code></pre><p>也就是说,ftp的加载方式还不能与Qt5的原生库完全一致,如何做到这一点,我还需要时间研究。</p><h3>示例项目改进</h3><p>修正进度条的提前显示,对progressDialog新对象进行如下设置,去掉了取消操作,取消操作有问题,暂时屏蔽。</p><pre><code> progressDialog = new QProgressDialog("download...", nullptr, 0, 100, this);
progressDialog->setWindowModality(Qt::WindowModal);
auto winFlags = windowFlags() & ~Qt::WindowMinMaxButtonsHint;
progressDialog->setWindowFlags(winFlags &~ Qt::WindowCloseButtonHint); //去掉窗口的默认按钮
progressDialog->reset(); //避免提前显示
progressDialog->setAutoClose(false);
progressDialog->setAutoReset(false);</code></pre><p>屏蔽取消按钮的消息链接。</p><pre><code>//connect(progressDialog, SIGNAL(canceled()), this, SLOT(cancelDownload()));</code></pre><p>支持多文件下载<br>首先,在QTreeWidget生成后,设置其可以选中多行。</p><pre><code>fileList->setSelectionMode(QAbstractItemView::ExtendedSelection);</code></pre><p>修改downloadFile函数,支持多文件下载。</p><pre><code>QList<QTreeWidgetItem*> selectedItemList = fileList->selectedItems();
for (int i = 0; i < selectedItemList.size(); i++)
{
QString fileName = selectedItemList[i]->text(0);
if (QFile::exists(fileName)) {
QMessageBox::information(this, tr("FTP"),
tr("There already exists a file called %1 in the current directory.").arg(fileName));
return;
}
file = new QFile(fileName);
if (!file->open(QIODevice::WriteOnly)) {
QMessageBox::information(this, tr("FTP"),
tr("Unable to save the file %1: %2.").arg(fileName).arg(file->errorString()));
delete file;
return;
}
ftp->get(fileName, file);
progressDialog->setLabelText(tr("Downloading %1...").arg(fileName));
downloadButton->setEnabled(false);
progressDialog->exec();
}</code></pre><h2>项目地址</h2><pre><code>https://github.com/zhoutk/qtDemo</code></pre><h2>命令行编译</h2><pre><code>git clone https://github.com/zhoutk/qtDemo
cd qtDemo/qftp & mkdir build & cd build
cmake ..
cmake --build . </code></pre><p>编译时注意:cmake默认为x86架构,需要与你安装的Qt版本对应;编译好了,运行前,请注意目录结构是否正确。</p><h2>小结</h2><p>上面是正统方法在qt5中使用qftp,还可以直接把其源代码纳入你的应用项目中,因为一共只有四个文件,稍作修改就可以使用。我发现该项目的问题,主要是cancelDownload会出让程序崩溃,感觉问题出在本地文件已经被清除,还有后续的数据到来,结果就异常了。有时间再来研究,看能不能把协议学透,自己造个轮子出来。</p>
Qt 插件编程实践
https://segmentfault.com/a/1190000039114769
2021-01-28T14:51:19+08:00
2021-01-28T14:51:19+08:00
zhoutk
https://segmentfault.com/u/zhoutk
2
<h3>缘由</h3><p>最近在用Qt做项目,在网上找插件编写的资料,没有完整的代码,要下载的资源都被传到需要积分的网站上了,感觉很不爽。因此把插件示例项目编写完整,并在github上开了一个qtDemo项目,写了这篇文章。<br>作为一个拖砖项目,望大家在学习同时,不要忘记了分享的精神。这个项目我会把学习Qt的代码不断更新上来,若有同道者,请pull request给我,本项目收集Qt示例程序,谢谢!</p><h3>技术选择</h3><p>我的项目最低支持msvc 10.0 x86,g++ 4.8.5,Qt5.5.1,因为需要跨平台,项目管理使用cmake(最低支持3.10),现在同时支持windows和linux,IDE我使用qcreator,在windows下你也可以选择visual studio 2019。</p><h3>Cmake介绍</h3><p>使用cmake主要是因为它的跨平台性好,可以脱离操作系统与IDE的束缚,而且已经被广泛支持了。 <br>设置cmake所需最低版本号</p><pre><code>cmake_minimum_required(VERSION 3.10)</code></pre><p>项目及版本号,项目名称可以通过${PROJECT_NAME}取得</p><pre><code>project(plg1 VERSION 0.1.0)</code></pre><p>将当前目录加入文件包含搜索目录,若不设置,vs2019与qcreator的当前目录会不兼容。</p><pre><code>set(CMAKE_INCLUDE_CURRENT_DIR ON)</code></pre><p>打开全局moc与全局uic</p><pre><code>set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)</code></pre><p>查找系统中已安装的Qt版本,需要的库。 <br>最好每一个库都要写,Qt也会根据依赖关系自动添加</p><pre><code>find_package(Qt5 REQUIRED Widgets)
find_package(Qt5Widgets)
find_package(Qt5Core)
find_package(Qt5Gui)</code></pre><p>需要建立名称为QTDIR的系统环境变量,指定安装的Qt目录。</p><p>收集我的们源文件,这有很多方法,大家可以去了解并使用自己喜欢的方式。</p><pre><code>FILE(GLOB SRC_FILES "*.cpp" "*.h" "*.ui")</code></pre><p>创建工程文件</p><pre><code>add_executable(${PROJECT_NAME} ${SRC_FILES}) #可执行文件创建方式
add_library(${PROJECT_NAME} SHARED ${SRC_FILES}) #动态链接库创建方式</code></pre><p>添加子项目,也就是我们的插件</p><pre><code>add_subdirectory(sub1)
add_subdirectory(sub2)</code></pre><p>添加Qt5依赖项</p><pre><code>target_link_libraries(${PROJECT_NAME} Qt5::Widgets Qt5::Core Qt5::Gui)</code></pre><h3>Qt5插件</h3><h4>项目结构</h4><p>本示例项目包括三个工程,一个主工程,两插件工程。cmake的项目是以目录为基础的,每个工程的目录下会有一个cmakelists.txt工程文件。</p><h4>插件基类</h4><p>主工程中的plugindemoplugin.h是所有插件的基类,我们的每个插件都继承自这个类,它做了插件基础声明及我们的插件能做的行为。本示例每一个插件会给主程序返回一个widget作为centerWidget显示,并提供一个name和information的查询接口,提供插件必要的信息。</p><pre><code>class QtPluginDemoInterface
{
public:
virtual ~QtPluginDemoInterface() {}
virtual QString name() = 0;
virtual QString information() = 0;
virtual QWidget *centerWidget() = 0; //返回一个Widget设置到centerwidget中进行显示
};
//s声明接口
#define PluginDemoInterface_iid "com.Interface.MainInterface"
Q_DECLARE_INTERFACE(QtPluginDemoInterface, PluginDemoInterface_iid)</code></pre><h4>插件定义</h4><p>我们这只对sub1进行一下说明,sub2是类似的,请自行阅读代码。 <br>子项目的工程文件(cmakelists.txt)与主项目的主要的差别是一个是创建可执行文件,一个是创建动态链接库。<br>头文件plugindemo.h :</p><pre><code>#include "../plugindemoplugin.h"
class pluginDemo : public QObject, QtPluginDemoInterface
{
Q_OBJECT //Qt类的标识宏,初学Qt的小伙伴要注意,这行是Qt类必须的
Q_INTERFACES(QtPluginDemoInterface) //这两行声明是插件要求的
Q_PLUGIN_METADATA(IID PluginDemoInterface_iid)
public:
pluginDemo(){};
... //方法声明,省略
};
</code></pre><p>源文件plugindemo.cpp :</p><pre><code>QWidget *pluginDemo::centerWidget()
{
auto btn = new QPushButton("One"); //我们返回一个按钮,作为简单的widget示例
return btn;
}
... //其它的省略了,只是固定信息返回</code></pre><h4>主程序中加载插件</h4><p>mainwindow.h定义:</p><pre><code>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
int MainWindow::loadPlugins();
void MainWindow::populateMenus(QObject * pluginInterface,QtPluginDemoInterface*i );
void MainWindow::slt_WidgetActionTriggered();
private:
Ui::MainWindow *ui;
};</code></pre><p>通过loadPlugins函数加载插件:</p><pre><code>int MainWindow::loadPlugins()
{
QDir pluginsDir = QDir(QCoreApplication::applicationDirPath()); //这里要注意路径需要配置好,把子工程的输出目录配置到主工程
if(!pluginsDir.cd("plugins")) return -1; //下的plugins目录中
foreach (QString fileName, pluginsDir.entryList(QDir::Files))
{
QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
QObject *plugin = pluginLoader.instance();
if(plugin)
{
auto centerInterface = qobject_cast<QtPluginDemoInterface*>(plugin);
if(centerInterface)
{
populateMenus(plugin,centerInterface); //将插件作为菜单中的一项
}
}
}
return count;
}</code></pre><p>生成菜单函数:</p><pre><code>void MainWindow::populateMenus(QObject * pluginInterface,QtPluginDemoInterface*i )
{
static auto menu = menuBar()->addMenu("widgets"); //建立一个菜单项
auto act = new QAction(i->name(),pluginInterface); //建立action,取得的插件对象被绑定在其上
connect(act,&QAction::triggered,this,&MainWindow::slt_WidgetActionTriggered); //事件链接
menu->addAction(act);
}</code></pre><p>菜单点击事件响应:</p><pre><code>void MainWindow::slt_WidgetActionTriggered()
{
QtPluginDemoInterface * plg = qobject_cast<QtPluginDemoInterface*>(sender()->parent()); //取得插件对象
auto centerWidget = plg->centerWidget(); //取得插件中返回的widget
//我们返回的widget其实是QPushButton,用其配置信息为其设置显示内容
(qobject_cast<QPushButton*>(centerWidget))->setText(plg->information());
setCentralWidget(centerWidget);
}</code></pre><h2>项目地址</h2><pre><code>https://github.com/zhoutk/qtDemo</code></pre><h2>命令行编译</h2><pre><code>git clone https://github.com/zhoutk/qtDemo
cd qtDemo/plugin & mkdir build & cd build
cmake ..
cmake --build . </code></pre><p>编译时注意:cmake默认为x86架构,需要与你安装的Qt版本对应;编译好了,运行前,请注意目录结构是否正确。</p><h2>小结</h2><p>抛砖引玉,不吝赐教,谢谢阅读!</p>
sqlit3 数据库操作的实现与解析
https://segmentfault.com/a/1190000023040242
2020-06-28T20:38:32+08:00
2020-06-28T20:38:32+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>设计思想</h2>
<p>选择官方c接口,实现Idb通用接口。具体的数据库操作,主要由两个函数ExecQuerySql和ExecNoneQuerySql来封装,底层的操作,主要使用sqlite3_prepare_v2来实现。</p>
<h2>具体实现</h2>
<h3>数据库连接处理</h3>
<p>因为Sqlit3是文件型数据库,所以用智能指针来保存打开数据库的句柄,防止内存泄漏。</p>
<pre><code>std::unique_ptr<sqlite3, Deleter> mSQLitePtr;</code></pre>
<p>对应的释放结构</p>
<pre><code>struct Deleter
{
void operator()(sqlite3* apSQLite) {
const int ret = sqlite3_close(apSQLite);
(void)ret;
SQLITECPP_ASSERT(SQLITE_OK == ret, "database is locked");
};
};</code></pre>
<h3>打开数据库连接</h3>
<pre><code>Sqlit3Db(const char* apFilename,
const int aFlags = OPEN_READWRITE,
const int aBusyTimeoutMs = 0,
const char* apVfs = nullptr) : mFilename(apFilename)
{
sqlite3* handle;
const int ret = sqlite3_open_v2(apFilename, &handle, aFlags, apVfs); //打开数据库句柄
mSQLitePtr.reset(handle); //保存到智能指针中
... //错误处理,略
};</code></pre>
<h3>执行返回结果集的查询</h3>
<pre><code>Rjson ExecQuerySql(string aQuery, vector<string> fields) {
Rjson rs = Utils::MakeJsonObjectForFuncReturn(STSUCCESS); //默认返回成功,使用utils.h中的json组装函数
sqlite3_stmt* stmt = NULL;
sqlite3* handle = getHandle(); //取得打开的数据库句柄
string u8Query = Utils::UnicodeToU8(aQuery);
const int ret = sqlite3_prepare_v2(handle, u8Query.c_str(), static_cast<int>(u8Query.size()), &stmt, NULL);
if (SQLITE_OK != ret)
{
string errmsg = sqlite3_errmsg(getHandle()); //取得错误信息,重置返回信息
rs.ExtendObject(Utils::MakeJsonObjectForFuncReturn(STDBOPERATEERR, errmsg));
}
else {
... //处理获取表字段名称的sql语句,略
sqlite3_get_table(handle, aQueryLimit0.c_str(), &pRes, &nRow, &nCol, &pErr);
for (int j = 0; j < nCol; j++)
{
string fs = *(pRes + j);
if (find(fields.begin(), fields.end(), fs) == fields.end()) {
fields.push_back(fs); //保存数据表字段名称
}
}
...
vector<Rjson> arr; //存储查询结果集的json对象数组
while (sqlite3_step(stmt) == SQLITE_ROW) {
Rjson al;
for (int j = 0; j < nCol; j++)
{
string k = fields.at(j);
int nType = sqlite3_column_type(stmt, j); //取得字段值的类型
if (nType == 1) { //SQLITE_INTEGER
al.AddValueInt(k, sqlite3_column_int(stmt, j)); //数值类型处理
}
else if (nType == 2) { //SQLITE_FLOAT
}
else if (nType == 3) { //SQLITE_TEXT 字符串类型处理
al.AddValueString(k, Utils::U8ToUnicode((char*)sqlite3_column_text(stmt, j)));
}
else if (nType == 4) { //SQLITE_BLOB
}
else if (nType == 5) { //SQLITE_NULL
} //暂时只处理了数值与字符串,其它暂未用到
}
arr.push_back(al);
}
if (arr.empty()) //结果集为空,返回查询空
rs.ExtendObject(Utils::MakeJsonObjectForFuncReturn(STQUERYEMPTY));
rs.AddValueObjectArray("data", arr); //增加结果集,类型为数组
}
sqlite3_finalize(stmt);
cout << "SQL: " << aQuery << endl;
return rs;
}</code></pre>
<h3>执行无结果集的查询</h3>
<pre><code>Rjson ExecNoneQuerySql(string aQuery) {
Rjson rs = Utils::MakeJsonObjectForFuncReturn(STSUCCESS);
sqlite3_stmt* stmt = NULL;
sqlite3* handle = getHandle();
string u8Query = Utils::UnicodeToU8(aQuery);
const int ret = sqlite3_prepare_v2(handle, u8Query.c_str(), static_cast<int>(u8Query.size()), &stmt, NULL);
if (SQLITE_OK != ret)
{
string errmsg = sqlite3_errmsg(getHandle());
rs.ExtendObject(Utils::MakeJsonObjectForFuncReturn(STDBOPERATEERR, errmsg));
}
else {
sqlite3_step(stmt); //只需要执行一次就好
}
sqlite3_finalize(stmt);
cout << "SQL: " << aQuery << endl;
return rs;
}</code></pre>
<h3>智能查询实现</h3>
<p>提取保留关键字,它们是fuzzy,sort,page,size,sum,count,group,完成排序、分页、分组等特殊查询操作。</p>
<pre><code>string fuzzy = params.GetStringValueAndRemove("fuzzy"); //精确匹配与模糊查询切换开关
string sort = params.GetStringValueAndRemove("sort"); //排序
int page = atoi(params.GetStringValueAndRemove("page").c_str()); //分页
int size = atoi(params.GetStringValueAndRemove("size").c_str()); //分页大小
string sum = params.GetStringValueAndRemove("sum"); //字段求和
string count = params.GetStringValueAndRemove("count"); //字段统计
string group = params.GetStringValueAndRemove("group"); //分组</code></pre>
<p>处理关键字 ins,lks,ors ,完成in查询,多字段模糊与查询,多字段精确或查询。</p>
<pre><code>if (k.compare("ins") == 0) {
string c = ele.at(0); //ins参数处理,第一个是字段名,后面是多个查询值
vector<string>(ele.begin() + 1, ele.end()).swap(ele); //拼接in查询sql语句
whereExtra.append(c).append(" in ( ").append(Utils::GetVectorJoinStr(ele)).append(" )");
}
else if (k.compare("lks") == 0 || k.compare("ors") == 0) {
whereExtra.append(" ( ");
for (size_t j = 0; j < ele.size(); j += 2) { //lks与ors参数处理,(字段,值)的重复
if (j > 0) {
whereExtra.append(" or ");
}
whereExtra.append(ele.at(j)).append(" "); //拼接sql语句
string eqStr = k.compare("lks") == 0 ? " like '" : " = '";
string vsStr = ele.at(j + 1);
if (k.compare("lks") == 0) {
vsStr.insert(0, "%");
vsStr.append("%");
}
vsStr.append("'");
whereExtra.append(eqStr).append(vsStr);
}
whereExtra.append(" ) ");
}</code></pre>
<p>处理值,不等操作是在值中加入了不等符号;fuzzy开关。</p>
<pre><code>if (Utils::FindStartsCharArray(QUERY_UNEQ_OPERS, (char*)v.c_str())) {
vector<string> vls = Utils::MakeVectorInitFromString(v);
if (vls.size() == 2) { //一个不等条件处理
where.append(k).append(vls.at(0)).append("'").append(vls.at(1)).append("'");
}
else if (vls.size() == 4) { //一对不等条件处理
where.append(k).append(vls.at(0)).append("'").append(vls.at(1)).append("' and ");
where.append(k).append(vls.at(2)).append("'").append(vls.at(3)).append("'");
}
}
else if (!fuzzy.empty() && vType == kStringType) { //精确查询与模糊查询切换
where.append(k).append(" like '%").append(v).append("%'");
}
else {
if (vType == kNumberType) //数值类型不加单引号
where.append(k).append(" = ").append(v);
else //字符类型要加单引号
where.append(k).append(" = '").append(v).append("'");
}</code></pre>
<p>分页处理</p>
<pre><code>if (page > 0) {
page--;
querySql.append(" limit ").append(Utils::IntTransToString(page * size)).append(",").append(Utils::IntTransToString(size));
}</code></pre>
<h3>批量插入</h3>
<p>多值插入,sqlit3采用标准sql语法,insert into users (field1,field2) select 'val1','val2' union all select 'val3','val4' union all select ...</p>
<pre><code>Rjson insertBatch(string tablename, vector<Rjson> elements) {
string sql = "insert into ";
string keyStr = " (";
keyStr.append(Utils::GetVectorJoinStr(elements[0].GetAllKeys())).append(" ) "); //拼接表字段
for (size_t i = 0; i < elements.size(); i++) {
vector<string> keys = elements[i].GetAllKeys();
string valueStr = " select ";
for (size_t j = 0; j < keys.size(); j++) { //组装一条纪录
valueStr.append("'").append(elements[i][keys[j]]).append("'");
if (j < keys.size() - 1) {
valueStr.append(",");
}
}
if (i < elements.size() - 1) { //准备拼接下一条纪录
valueStr.append(" union all ");
}
keyStr.append(valueStr);
}
sql.append(tablename).append(keyStr);
return ExecNoneQuerySql(sql); //调用无结果集操作
}</code></pre>
<h3>事务操作</h3>
<pre><code>Rjson transGo(vector<string> sqls, bool isAsync = false) {
char* zErrMsg = 0;
bool isExecSuccess = true;
sqlite3_exec(getHandle(), "begin;", 0, 0, &zErrMsg); //开始事务处理
for (size_t i = 0; i < sqls.size(); i++) {
string u8Query = Utils::UnicodeToU8(sqls[i]); //处理中文
int rc = sqlite3_exec(getHandle(), u8Query.c_str(), 0, 0, &zErrMsg); //处理一个操作
if (rc != SQLITE_OK)
{
isExecSuccess = false;
cout << "Transaction Fail, sql " << i + 1 << " is wrong. Error: " << zErrMsg << endl;
sqlite3_free(zErrMsg);
break;
}
}
if (isExecSuccess) //成功,提交到数据库
{
sqlite3_exec(getHandle(), "commit;", 0, 0, 0);
sqlite3_close(getHandle());
cout << "Transaction Success: run " << sqls.size() << " sqls." << endl;
return Utils::MakeJsonObjectForFuncReturn(STSUCCESS, "insertBatch success.");
}
else //有失败的,回滚
{
sqlite3_exec(getHandle(), "rollback;", 0, 0, 0);
sqlite3_close(getHandle());
return Utils::MakeJsonObjectForFuncReturn(STDBOPERATEERR, zErrMsg);
}
}</code></pre>
<h2>项目地址</h2>
<pre><code>https://github.com/zhoutk/Jorm</code></pre>
<h2>系列文章规划</h2>
<ul>
<li>c++操作关系数据库通用接口设计(JSON-ORM c++版)</li>
<li>Rjson -- rapidjson代理的设计与实现</li>
<li>sqlit3 数据库操作的实现与解析</li>
<li>mysql 数据库操作的实现与解析</li>
<li>postgres 数据库操作的实现与解析</li>
<li>oracle 数据库操作的实现与解析</li>
<li>mssql 数据库操作的实现与解析</li>
<li>总结(如果需要的话)</li>
</ul>
<h2>感受</h2>
<p>批量插入的标准sql竟然很陌生,平时被mysql的方言惯坏了...</p>
封装rapidjson用于数据库及网络数据传输
https://segmentfault.com/a/1190000023031831
2020-06-28T09:00:00+08:00
2020-06-28T09:00:00+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>背景</h2>
<p>我要完成以json为数据媒介,来操作数据库和网络传输。查资料,发现rapidjson是比较流行的json库,并且速度快。但以我的使用方式,用起来非常麻烦,而且我的目的是数据交换。rapidjson非常普通看起来应该是值传输的操作,其实都是内存移动。这虽然能达到高效率的目的,但一不小心就会出错,而且写出来看着非常丑陋,所以我写了个代理类,来达到我的需求。</p>
<h2>设计思想</h2>
<p>只是增加一些方便的操作方法,提供多种构造函数来方便的创建对象,提供复制构造函数,提供方便的增加元素的接口,提供对象的遍历方法等等。内部实际上是一个rapidjson对象,所有的实际操作都在基于rapidjson的,Rjson只是一个代理,改变了使用的方式,提供了值传输,会有一定的性能下降,但这是适应我的需求所作的必要改变。</p>
<h2>具体实现</h2>
<h3>类结构</h3>
<p>Document * json 是实际的json对象,我进行封装,提供新的访问接口,对外不暴露其它细节,比如:Value对象。因此Rjson对象的遍历设计就比较麻烦,最终我选择了类似ES6的方式,先提供一个GetAllKeys方法,再逐一访问每个value的方式来进行遍历。</p>
<pre><code>class Rjson {
private:
Document* json;
public:
Rjson();
Rjson(const char* jstr);
Rjson ExtendObject(Rjson& obj);
void AddValueInt(string k, int v);
void AddValueString(string k, string v) ;
...
}</code></pre>
<h3>默认构造函数</h3>
<pre><code>Rjson() {
json = new Document(); //rapidjosn建立Document对象后,必须要增加Value或调用SetObject()来形成一个空json,不然就会报错
json->SetObject(); //我合并两个操作,建立一个空json
}</code></pre>
<h3>构造函数,接受char*参数</h3>
<pre><code>Rjson(const char* jstr) { //注意这里是const char *,不然从string.c_str()传递过来就要强转,不然会被匹配也string参数的重载构造函数上去。
json = new Document(); //合并两步操作,简单的处理,可以方便很多创建代码的编写
json->Parse(jstr);
}</code></pre>
<h3>构造函数,接受string参数</h3>
<pre><code>Rjson(string jstr) { //注意构造函数中调用重载构造函数的方法
new (this)Rjson(jstr.c_str());
}</code></pre>
<h3>复制构造函数</h3>
<pre><code>Rjson(const Rjson& origin) {
json = new Document(); //使用CopyFrom实现复制
json->CopyFrom(*(origin.json), json->GetAllocator());
}</code></pre>
<h3>赋值操作</h3>
<pre><code>Rjson& operator = (const Rjson& origin) {
new (this)Rjson(origin); //利用复制构造函数来实现
return(*this);
}</code></pre>
<h3>重载[]运算符</h3>
<pre><code>string operator[](string key) { //值都以字符串形式返回
string rs = "";
if (json->HasMember(key.c_str())) {
int vType;
GetValueAndTypeByKey(key.c_str(), &rs, &vType);
}
return rs;
}</code></pre>
<h3>增加数值类型</h3>
<pre><code>void AddValueInt(string k, int v) {
string* newK = new string(k); //必须新建
Value aInt(kNumberType);
aInt.SetInt(v);
json->AddMember(StringRef(newK->c_str()), aInt, json->GetAllocator()); //addMember方法是地址传递
}</code></pre>
<h3>增加字符串类型</h3>
<pre><code> void AddValueString(string k, string v) {
string* newK = new string(k);
Value aStr(kStringType); //必须新建
aStr.SetString(v.c_str(), json->GetAllocator());
json->AddMember(StringRef(newK->c_str()), aStr, json->GetAllocator());
}</code></pre>
<h3>增加对象数组</h3>
<pre><code> void AddValueArray(string k, vector<string>& arr) {
string* newK = new string(k);
int len = arr.size();
Value rows(kArrayType);
for (int i = 0; i < len; i++) {
Value al(kStringType); //必须新建
al.SetString(arr.at(i).c_str(),json->GetAllocator());
rows.PushBack(al, json->GetAllocator());
}
json->AddMember(StringRef(newK->c_str()), rows, json->GetAllocator());
}</code></pre>
<h3>取得所有键</h3>
<pre><code>vector<string> GetAllKeys() {
//不想显露Value对象,使用这个接口加[]运算符来完成遍历,若需要值类型,则与GetValueAndTypeByKey方法配合。
vector<string> keys;
for (auto iter = json->MemberBegin(); iter != json->MemberEnd(); ++iter)
{
keys.push_back((iter->name).GetString());
}
return keys;
}</code></pre>
<h3>取得指定值及其类型</h3>
<p>rapidjson一共定义了七种值类型,需要一一对应处理</p>
<pre><code>enum Type {
kNullType = 0, //!< null
kFalseType = 1, //!< false
kTrueType = 2, //!< true
kObjectType = 3, //!< object
kArrayType = 4, //!< array
kStringType = 5, //!< string
kNumberType = 6 //!< number
};</code></pre>
<p>暂时只处理了数值、字符串及数组类型</p>
<pre><code>void GetValueAndTypeByKey(string key, string* v, int* vType) {
Value::ConstMemberIterator iter = json->FindMember(key.c_str());
if (iter != json->MemberEnd()) {
*vType = (int)(iter->value.GetType());
if (iter->value.IsInt()) {
std::stringstream s;
s << iter->value.GetInt();
*v = s.str();
}
else if (iter->value.IsString()) {
*v = iter->value.GetString();
}
else if (iter->value.IsArray()) {
*v = GetJsonString((Value&)iter->value);
}
else {
*v = "";
}
}
else {
*vType = kStringType;
*v = "";
}
}</code></pre>
<h3>获取json字符串</h3>
<pre><code>string GetJsonString() {
StringBuffer strBuffer;
Writer<StringBuffer> writer(strBuffer);
json->Accept(writer);
return strBuffer.GetString();
}</code></pre>
<h3>扩展json对象</h3>
<pre><code>Rjson ExtendObject(Rjson& obj) {
Document* src = obj.GetOriginRapidJson();
for (auto iter = src->MemberBegin(); iter != src->MemberEnd(); ++iter)
{
if (json->HasMember(iter->name)) { //键存在,更新
Value& v = (*json)[iter->name];
v.CopyFrom(iter->value, json->GetAllocator());
//v = (Value&)std::move(vTmp);
}
else { //键不存在,新增
string* newK = new string(iter->name.GetString());
Value vTmp;
vTmp.CopyFrom(iter->value, json->GetAllocator());
json->AddMember(StringRef(newK->c_str()), vTmp, json->GetAllocator());
}
}
return *(this);
}</code></pre>
<h2>使用示例</h2>
<pre><code>Rjson obj;
obj.AddValueString("username", "插入测试");
obj.AddValueInt("password", 3245);
cout << obj.GetJsonString() << endl;
Rjson obj2(obj);
Rjson obj3("{\"user\":\"bill\",\"age\":12}");</code></pre>
<h2>项目地址</h2>
<pre><code>https://github.com/zhoutk/Jorm</code></pre>
<h2>系列文章规划</h2>
<ul>
<li>c++操作关系数据库通用接口设计(JSON-ORM c++版)</li>
<li>Rjson -- rapidjson代理的设计与实现</li>
<li>sqlit3 数据库操作的实现与解析</li>
<li>mysql 数据库操作的实现与解析</li>
<li>postgres 数据库操作的实现与解析</li>
<li>oracle 数据库操作的实现与解析</li>
<li>mssql 数据库操作的实现与解析</li>
<li>总结(如果需要的话)</li>
</ul>
<h2>感受</h2>
<p>这是封装数据库通用访问接口的第一步,提供方便的json对象操作,不然写个json对象得郁闷死。很怀念在javascript中操作json的感觉,如丝般润滑......</p>
c++关系数据库访问通用接口设计(JSON-ORM c++版)
https://segmentfault.com/a/1190000023023978
2020-06-26T13:01:34+08:00
2020-06-26T13:01:34+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<p>重操C++旧业,习惯通常的数据库操作方式,因此抽时间,把JSON-ORM封装了一个C++版,现支持sqlit3与mysql,postgres已经做好了准备。</p>
<h2>设计思路</h2>
<p>我们通用的ORM,基本模式都是想要脱离数据库的,几乎都在编程语言层面建立模型,由程序去与数据库打交道。虽然脱离了数据库的具体操作,但我们要建立各种模型文档,用代码去写表之间的关系等等操作,让初学者一时如坠云雾。我的想法是,将关系数据库拥有的完善设计工具之优势,来实现数据设计以提供结构信息,让json对象自动映射成为标准的SQL查询语句。只要我们理解了标准的SQL语言,我们就能够完成数据库查询操作。</p>
<h2>技术选择</h2>
<h3>json库</h3>
<p>JSON-ORM 数据传递采用json来实现,使数据标准能从最前端到最后端进行和谐统一。我选择了rapidjson,但rapidjson为了执行效率,直接操作内存,并且大量使用std::move,在使用的时候有很多限制,一不小心还会出现内存访问冲突。 因此我封装了它,提供了一个容易操作的Rjson代理类。</p>
<h3>数据库通用接口</h3>
<blockquote>应用类直接操作这个通用接口,实现与底层实现数据库的分离。该接口提供了CURD标准访问,以及批量插入和事务操作,基本能满足平时百分之九十以上的数据库操作。</blockquote>
<pre><code> class Idb
{
public:
virtual Rjson select(string tablename, Rjson& params, vector<string> fields = vector<string>(), int queryType = 1) = 0;
virtual Rjson create(string tablename, Rjson& params) = 0;
virtual Rjson update(string tablename, Rjson& params) = 0;
virtual Rjson remove(string tablename, Rjson& params) = 0;
virtual Rjson querySql(string sql, Rjson& params = Rjson(), vector<string> filelds = vector<string>()) = 0;
virtual Rjson execSql(string sql) = 0;
virtual Rjson insertBatch(string tablename, vector<Rjson> elements) = 0;
virtual Rjson transGo(vector<string> sqls, bool isAsync = false) = 0;
};</code></pre>
<h3>底层数据库访问实现</h3>
<p>现在已经实现了sqlit3与mysql的所有功能,postgres与oracle也做了技术准备,应该在不久的将来会实现(取决于是否有时间),mssql除非有特别的需求,短期内不会去写。 <br> 我选择的技术实现方式,基本上是最底层高效的方式。sqlit3 - sqllit3.h(官方的标准c接口);mysql - c api (MySQL Connector C 6.1);postgres - pqxx;oracle - occi;mssql - ?</p>
<h3>智能查询方式设计</h3>
<blockquote>查询保留字:page, size, sort, fuzzy, lks, ins, ors, count, sum, group</blockquote>
<ul>
<li>
<p>page, size, sort, 分页排序<br> 在sqlit3与mysql中这比较好实现,limit来分页是很方便的,排序只需将参数直接拼接到order by后就好了。 <br> 查询示例:</p>
<pre><code>Rjson p;
p.AddValueInt("page", 1);
p.AddValueInt("size", 10);
p.AddValueString("size", "sort desc");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users ORDER BY age desc LIMIT 0,10</code></pre>
</li>
<li>
<p>fuzzy, 模糊查询切换参数,不提供时为精确匹配<br> 提供字段查询的精确匹配与模糊匹配的切换。</p>
<pre><code>Rjson p;
p.AddValueString("username", "john");
p.AddValueString("password", "123");
p.AddValueString("fuzzy", "1");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users WHERE username like '%john%' and password like '%123%'</code></pre>
</li>
<li>
<p>ins, lks, ors<br> 这是最重要的三种查询方式,如何找出它们之间的共同点,减少冗余代码是关键。</p>
<ul>
<li>
<p>ins, 数据库表单字段in查询,一字段对多个值,例:</p>
<pre><code>查询示例:</code></pre>
<pre><code>Rjson p;
p.AddValueString("ins", "age,11,22,36");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users WHERE age in ( 11,22,26 )</code></pre>
</li>
<li>
<p>ors, 数据库表多字段精确查询,or连接,多个字段对多个值,例:</p>
<pre><code>查询示例:</code></pre>
<pre><code>Rjson p;
p.AddValueString("ors", "age,11,age,36");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users WHERE ( age = 11 or age = 26 )</code></pre>
</li>
<li>
<p>lks, 数据库表多字段模糊查询,or连接,多个字段对多个值,例:</p>
<pre><code>查询示例:</code></pre>
<pre><code>Rjson p;
p.AddValueString("lks", "username,john,password,123");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users WHERE ( username like '%john%' or password like '%123%' )</code></pre>
</li>
</ul>
</li>
<li>
<p>count, sum<br> 这两个统计求和,处理方式也类似,查询时一般要配合group与fields使用。</p>
<ul>
<li>
<p>count, 数据库查询函数count,行统计,例:</p>
<pre><code>查询示例:</code></pre>
<pre><code>Rjson p;
p.AddValueString("count", "1,total");
(new BaseDb(...))->select("users", p);
生成sql: SELECT *,count(1) as total FROM users</code></pre>
</li>
<li>
<p>sum, 数据库查询函数sum,字段求和,例:</p>
<pre><code>查询示例:</code></pre>
<pre><code>Rjson p;
p.AddValueString("sum", "age,ageSum");
(new BaseDb(...))->select("users", p);
生成sql: SELECT username,sum(age) as ageSum FROM users</code></pre>
</li>
</ul>
</li>
<li>
<p>group, 数据库分组函数group,例: <br> 查询示例:</p>
<pre><code>Rjson p;
p.AddValueString("group", "age");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users GROUP BY age</code></pre>
</li>
</ul>
<blockquote>不等操作符查询支持</blockquote>
<p>支持的不等操作符有:>, >=, <, <=, <>, =;逗号符为分隔符,一个字段支持一或二个操作。 <br>特殊处:使用"="可以使某个字段跳过search影响,让模糊匹配与精确匹配同时出现在一个查询语句中</p>
<ul>
<li>
<p>一个字段一个操作,示例:<br>查询示例:</p>
<pre><code>Rjson p;
p.AddValueString("age", ">,10");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users WHERE age> 10</code></pre>
</li>
<li>
<p>一个字段二个操作,示例:<br>查询示例:</p>
<pre><code>Rjson p;
p.AddValueString("age", ">=,10,<=,33");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users WHERE age>= 10 and age<= 33</code></pre>
</li>
<li>
<p>使用"="去除字段的search影响,示例:<br>查询示例:</p>
<pre><code>Rjson p;
p.AddValueString("age", "=,18");
p.AddValueString("username", "john");
p.AddValueString("fuzzy", "1");
(new BaseDb(...))->select("users", p);
生成sql: SELECT * FROM users WHERE age= 18 and username like '%john%'</code></pre>
</li>
</ul>
<h3>系列文章规划</h3>
<ul>
<li>c++操作关系数据库通用接口设计(JSON-ORM c++版)</li>
<li>Rjson -- rapidjson代理的设计与实现</li>
<li>sqlit3 数据库操作的实现与解析</li>
<li>mysql 数据库操作的实现与解析</li>
<li>postgres 数据库操作的实现与解析</li>
<li>oracle 数据库操作的实现与解析</li>
<li>mssql 数据库操作的实现与解析</li>
<li>总结(如果需要的话)</li>
</ul>
<h3>项目地址</h3>
<pre><code>https://github.com/zhoutk/Jorm</code></pre>
<h2>感受</h2>
<p>多年没写C++了,恢复了一段时间,了解了新的C++标准,感觉世界变化真快。回顾这些年,我选择学习技术比较前卫,这些年主做node.js(Typescript),偶尔写python, go。已经习惯了动态语言,函数式编程。突然回归C++,发现好多普通的想法,实现起来还真麻烦。这就是人们的选择吧,有得就有失。想当年我是疯狂的迷恋C、C++,因此它们不限制我选择编程范式。这些年,随着硬件技术的发展,软件业的思维也突飞猛进,一般情况下不再为内存,算法效率而忧心,我们更注重开发效率和程序代码的人性化。回到C++的怀抱,看着我半墙的C++书箱,熟悉新c++标准,我选定了主攻方向,模板、函数式编程......</p>
Graphql实战系列(下)
https://segmentfault.com/a/1190000018852116
2019-04-12T23:15:37+08:00
2019-04-12T23:15:37+08:00
zhoutk
https://segmentfault.com/u/zhoutk
7
<h2>前情介绍</h2>
<p>在<a href="https://segmentfault.com/a/1190000018837788">《Graphql实战系列(上)》</a>中我们已经完成技术选型,并将graphql桥接到<a href="https://link.segmentfault.com/?enc=pzpVDBM921iCw3EwUbOoJg%3D%3D.W0RBh03AsWyZXt4ej%2BSmL2ip%2FHNsbPA6r4G3V6qrIiw%3D" rel="nofollow">凝胶gels</a>项目中,并动手写了schema,并可以通过 <a href="https://link.segmentfault.com/?enc=3oBo2vz5EMKYJJTgHxkO5Q%3D%3D.4oxTQ8tyOR7r6mX1TBJSSDKRCJ2xiGONkDnkps2nIrE%3D" rel="nofollow">http://localhost</a>:5000/graphql 查看效果。这一节,我们根据数据库表来自动生成基本的查询与更新schema,并能方便的扩展schema,实现我们想要的业务逻辑。</p>
<h2>效果图</h2>
<p>用navicat在数据库中设计的表<br><img src="/img/bVbrg99?w=2612&h=1288" alt="图片描述" title="图片描述"><br>自动生成的graphql测试<br><img src="/img/bVbrhak?w=3812&h=2304" alt="图片描述" title="图片描述"></p>
<h2>设计思路</h2>
<p>对象定义在apollo-server中是用字符串来做的,而Query与Mutation只能有一个,而我们的定义又会分散在多个文件中,因此只能先以一定的形式把它们存入数组中,在生成schema前一刻再组合。</p>
<h3>业务逻辑模块模板设计:</h3>
<pre><code>const customDefs = {
textDefs: `
type ReviseResult {
id: Int
affectedRows: Int
status: Int
message: String
},
queryDefs: [],
mutationDefs: []
}
const customResolvers = {
Query: {
},
Mutation: {
}
}
export { customDefs, customResolvers }</code></pre>
<h3>schema合并算法</h3>
<pre><code>let typeDefs = []
let dirGraphql = requireDir('../../graphql') //从手写schema业务模块目录读入文件
G.L.each(dirGraphql, (item, name) => {
if (item && item.customDefs && item.customResolvers) {
typeDefs.push(item.customDefs.textDefs || '') //合并文本对象定义
typeDefObj.query = typeDefObj.query.concat(item.customDefs.queryDefs || []) //合并Query
typeDefObj.mutation = typeDefObj.mutation.concat(item.customDefs.mutationDefs || []) //合并Matation
let { Query, Mutation, ...Other } = item.customResolvers
Object.assign(resolvers.Query, Query) //合并resolvers.Query
Object.assign(resolvers.Mutation, Mutation) //合并resolvers.Mutation
Object.assign(resolvers, Other) //合并其它resolvers
}
})
//将query与matation查询更新对象由自定义的数组转化成为文本形式
typeDefs.push(Object.entries(typeDefObj).reduce((total, cur) => {
return total += `
type ${G.tools.bigCamelCase(cur[0])} {
${cur[1].join('')}
}
`
}, ''))</code></pre>
<h3>从数据库表动态生成schema</h3>
<p>自动生成内容:</p>
<ul>
<li>一个表一个对象;</li>
<li>每个表有两个Query,一是单条查询,二是列表查询;</li>
<li>三个Mutation,一是新增,二是更新,三是删除;</li>
<li>关联表以上篇中的Book与Author为例,Book中有author_id,会生成一个Author对象;而Author表中会生成一个对象列表[Book]</li>
</ul>
<h4>mysql类型 => graphql 类型转化常量定义</h4>
<p>定义一类型转换,不在定义中的默认为String。</p>
<pre><code>const TYPEFROMMYSQLTOGRAPHQL = {
int: 'Int',
smallint: 'Int',
tinyint: 'Int',
bigint: 'Int',
double: 'Float',
float: 'Float',
decimal: 'Float',
}</code></pre>
<h4>从数据库中读取数据表信息</h4>
<pre><code> let dao = new BaseDao()
let tables = await dao.querySql('select TABLE_NAME,TABLE_COMMENT from information_schema.`TABLES` ' +
' where TABLE_SCHEMA = ? and TABLE_TYPE = ? and substr(TABLE_NAME,1,2) <> ? order by ?',
[G.CONFIGS.dbconfig.db_name, 'BASE TABLE', 't_', 'TABLE_NAME'])</code></pre>
<h4>从数据库中读取表字段信息</h4>
<pre><code>tables.data.forEach((table) => {
columnRs.push(dao.querySql('SELECT `COLUMNS`.COLUMN_NAME,`COLUMNS`.COLUMN_TYPE,`COLUMNS`.IS_NULLABLE,' +
'`COLUMNS`.CHARACTER_SET_NAME,`COLUMNS`.COLUMN_DEFAULT,`COLUMNS`.EXTRA,' +
'`COLUMNS`.COLUMN_KEY,`COLUMNS`.COLUMN_COMMENT,`STATISTICS`.TABLE_NAME,' +
'`STATISTICS`.INDEX_NAME,`STATISTICS`.SEQ_IN_INDEX,`STATISTICS`.NON_UNIQUE,' +
'`COLUMNS`.COLLATION_NAME ' +
'FROM information_schema.`COLUMNS` ' +
'LEFT JOIN information_schema.`STATISTICS` ON ' +
'information_schema.`COLUMNS`.TABLE_NAME = `STATISTICS`.TABLE_NAME ' +
'AND information_schema.`COLUMNS`.COLUMN_NAME = information_schema.`STATISTICS`.COLUMN_NAME ' +
'AND information_schema.`STATISTICS`.table_schema = ? ' +
'where information_schema.`COLUMNS`.TABLE_NAME = ? and `COLUMNS`.table_schema = ?',
[G.CONFIGS.dbconfig.db_name, table.TABLE_NAME, G.CONFIGS.dbconfig.db_name]))
})</code></pre>
<h4>几个工具函数</h4>
<p>取数据库表字段类型,去除圆括号与长度信息</p>
<pre><code> getStartTillBracket(str: string) {
return str.indexOf('(') > -1 ? str.substr(0, str.indexOf('(')) : str
}</code></pre>
<p>下划线分隔的表字段转化为big camel-case</p>
<pre><code> bigCamelCase(str: string) {
return str.split('_').map((al) => {
if (al.length > 0) {
return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase()
}
return al
}).join('')
}</code></pre>
<p>下划线分隔的表字段转化为small camel-case</p>
<pre><code> smallCamelCase(str: string) {
let strs = str.split('_')
if (strs.length < 2) {
return str
} else {
let tail = strs.slice(1).map((al) => {
if (al.length > 0) {
return al.substr(0, 1).toUpperCase() + al.substr(1).toLowerCase()
}
return al
}).join('')
return strs[0] + tail
}
}</code></pre>
<h4>字段是否以_id结尾,是表关联的标志</h4>
<p>不以_id结尾,是正常字段,判断是否为null,处理必填</p>
<pre><code>typeDefObj[table].unshift(`${col['COLUMN_NAME']}: ${typeStr}${col['IS_NULLABLE'] === 'NO' ? '!' : ''}\n`)</code></pre>
<p>以_id结尾,则需要处理关联关系</p>
<pre><code> //Book表以author_id关联单个Author实体
typeDefObj[table].unshift(`"""关联的实体"""
${G.L.trimEnd(col['COLUMN_NAME'], '_id')}: ${G.tools.bigCamelCase(G.L.trimEnd(col['COLUMN_NAME'], '_id'))}`)
resolvers[G.tools.bigCamelCase(table)] = {
[G.L.trimEnd(col['COLUMN_NAME'], '_id')]: async (element) => {
let rs = await new BaseDao(G.L.trimEnd(col['COLUMN_NAME'], '_id')).retrieve({ id: element[col['COLUMN_NAME']] })
return rs.data[0]
}
}
//Author表关联Book列表
let fTable = G.L.trimEnd(col['COLUMN_NAME'], '_id')
if (!typeDefObj[fTable]) {
typeDefObj[fTable] = []
}
if (typeDefObj[fTable].length >= 2)
typeDefObj[fTable].splice(typeDefObj[fTable].length - 2, 0, `"""关联实体集合"""${table}s: [${G.tools.bigCamelCase(table)}]\n`)
else
typeDefObj[fTable].push(`${table}s: [${G.tools.bigCamelCase(table)}]\n`)
resolvers[G.tools.bigCamelCase(fTable)] = {
[`${table}s`]: async (element) => {
let rs = await new BaseDao(table).retrieve({ [col['COLUMN_NAME']]: element.id})
return rs.data
}
}</code></pre>
<h4>生成Query查询</h4>
<p>单条查询</p>
<pre><code> if (paramId.length > 0) {
typeDefObj['query'].push(`${G.tools.smallCamelCase(table)}(${paramId}!): ${G.tools.bigCamelCase(table)}\n`)
resolvers.Query[`${G.tools.smallCamelCase(table)}`] = async (_, { id }) => {
let rs = await new BaseDao(table).retrieve({ id })
return rs.data[0]
}
} else {
G.logger.error(`Table [${table}] must have id field.`)
}</code></pre>
<p>列表查询</p>
<pre><code> let complex = table.endsWith('s') ? (table.substr(0, table.length - 1) + 'z') : (table + 's')
typeDefObj['query'].push(`${G.tools.smallCamelCase(complex)}(${paramStr.join(', ')}): [${G.tools.bigCamelCase(table)}]\n`)
resolvers.Query[`${G.tools.smallCamelCase(complex)}`] = async (_, args) => {
let rs = await new BaseDao(table).retrieve(args)
return rs.data
}</code></pre>
<h4>生成Mutation查询</h4>
<pre><code> typeDefObj['mutation'].push(`
create${G.tools.bigCamelCase(table)}(${paramForMutation.slice(1).join(', ')}):ReviseResult
update${G.tools.bigCamelCase(table)}(${paramForMutation.join(', ')}):ReviseResult
delete${G.tools.bigCamelCase(table)}(${paramId}!):ReviseResult
`)
resolvers.Mutation[`create${G.tools.bigCamelCase(table)}`] = async (_, args) => {
let rs = await new BaseDao(table).create(args)
return rs
}
resolvers.Mutation[`update${G.tools.bigCamelCase(table)}`] = async (_, args) => {
let rs = await new BaseDao(table).update(args)
return rs
}
resolvers.Mutation[`delete${G.tools.bigCamelCase(table)}`] = async (_, { id }) => {
let rs = await new BaseDao(table).delete({ id })
return rs
}</code></pre>
<h2>项目地址</h2>
<pre><code>https://github.com/zhoutk/gels</code></pre>
<h2>使用方法</h2>
<pre><code>git clone https://github.com/zhoutk/gels
cd gels
yarn
tsc -w
nodemon dist/index.js</code></pre>
<p>然后就可以用浏览器打开链接:<a href="https://link.segmentfault.com/?enc=R1VB6g5Vi51qeEqv1wgQ2g%3D%3D.Hgch9HOq2D5wo3sRX5kjzoBrSkzQNIoCpYW4JK2Cfgc%3D" rel="nofollow">http://localhost</a>:5000/graphql 查看效果了。</p>
<h2>小结</h2>
<p>我只能把大概思路写出来,让大家有个整体的概念,若想很好的理解,得自己把项目跑起来,根据我提供的思想,慢慢的去理解。因为我在编写的过程中还是遇到了不少的难点,这块既要自动化,还要能方便的接受手动编写的schema模块,的确有点难度。</p>
Graphql实战系列(上)
https://segmentfault.com/a/1190000018837788
2019-04-11T19:29:33+08:00
2019-04-11T19:29:33+08:00
zhoutk
https://segmentfault.com/u/zhoutk
5
<h2>背景介绍</h2>
<p>graphql越来越流行,一直想把我的凝胶项目除了支持restful api外,也能同时支持graphql。由于该项目的特点是结合关系数据库的优点,尽量少写重复或雷同的代码。对于rest api,在做完数据库设计后,百分之六十到八十的接口就已经完成了,但还需要配置上api文档。而基于数据库表自动实现graphql,感觉还是有难度的,但若能做好,连文档也就同时提供了。</p>
<p>不久前又看到了一句让我深以为然的话:No program is perfect, even the most talented engineers will write a bug or two (or three). By far the best design pattern available is simply writing less code. That’s the opportunity we have today, to accomplish our goals by doing less. </p>
<p>so, ready go...</p>
<h2>基本需求与约定</h2>
<ul>
<li>根据数据库表自动生成schema</li>
<li>充分利用已经有的支持restful api的底层接口</li>
<li>能自动实现一对多的表关系</li>
<li>能方便的增加特殊业务,只需要象rest一样,只需在指定目录,增加业务模块即可</li>
<li>测试表有两个,book & author</li>
<li>book表字段有:id, title, author_id</li>
<li>author表字段有: id, name</li>
<li>数据表必须有字段id,类型为整数(自增)或8位字符串(uuid),作为主键或建立unique索引</li>
<li>表名为小写字母,使用名词单数,以下划作为单词分隔</li>
<li>表关联自动在相关中嵌入相关对象,Book对象增加Author对象,Author对象增加books列表</li>
<li>每个表会默认生成两个query,一个是以id为参数进行单条查询,另一个是列表查询;命名规则;单条查询与表名相同,列表查询为表名+s,若表名本身以s结尾,则变s为z</li>
</ul>
<h2>桥接库比较与选择</h2>
<p>我需要在koa2上接入graphql,经过查阅资料,最后聚焦在下面两个库上:</p>
<ul>
<li>kao-graphql</li>
<li>apollo-server-koa</li>
</ul>
<h2>kao-graphql实现</h2>
<p>开始是考虑简单为上,试着用kao-graphql,作为中间件可以方便的接入,我指定了/gql路由,可以测试效果,代码如下:</p>
<pre><code>import * as Router from 'koa-router'
import BaseDao from '../db/baseDao'
import { GraphQLString, GraphQLObjectType, GraphQLSchema, GraphQLList, GraphQLInt } from 'graphql'
const graphqlHTTP = require('koa-graphql')
let router = new Router()
export default (() => {
let authorType = new GraphQLObjectType({
name: 'Author',
fields: {
id: { type: GraphQLInt},
name: { type: GraphQLString}
}
})
let bookType = new GraphQLObjectType({
name: 'Book',
fields: {
id: { type: GraphQLInt},
title: { type: GraphQLString},
author: {
type: authorType,
resolve: async (book, args) => {
let rs = await new BaseDao('author').retrieve({id: book.author_id})
return rs.data[0]
}
}
}
})
let queryType = new GraphQLObjectType({
name: 'Query',
fields: {
books: {
type: new GraphQLList(bookType),
args: {
id: { type: GraphQLString },
search: { type: GraphQLString },
title: { type: GraphQLString },
},
resolve: async function (_, args) {
let rs = await new BaseDao('book').retrieve(args)
return rs.data
}
},
authors: {
type: new GraphQLList(authorType),
args: {
id: { type: GraphQLString },
search: { type: GraphQLString },
name: { type: GraphQLString },
},
resolve: async function (_, args) {
let rs = await new BaseDao('author').retrieve(args)
return rs.data
}
}
}
})
let schema = new GraphQLSchema({ query: queryType })
return router.all('/gql', graphqlHTTP({
schema: schema,
graphiql: true
}))
})() </code></pre>
<blockquote>这种方式有个问题,前面的变量对象中要引入后面定义的变量对象会出问题,因此投入了apollo-server。但apollo-server 2.0网上资料少,大多是介绍1.0的,而2.0变动又比较大,因此折腾了一段时间,还是要多看英文资料。<br>apollo-server 2.0集成很多东西到里面,包括cors,bodyParse,graphql-tools 等。</blockquote>
<h2>apollo-server 2.0实现静态schema</h2>
<p>通过中间件加载,放到rest路由之前,加入顺序及方式请看app.ts,apollo-server-kao接入代码:</p>
<pre><code>//自动生成数据库表的基础schema,并合并了手写的业务模块
import { getInfoFromSql } from './schema_generate'
const { ApolloServer } = require('apollo-server-koa')
export default async (app) => { //app是koa实例
let { typeDefs, resolvers } = await getInfoFromSql() //数据库查询是异步的,所以导出的是promise函数
if (!G.ApolloServer) {
G.ApolloServer = new ApolloServer({
typeDefs, //已经不需要graphql-tools,ApolloServer构造函数已经集成其功能
resolvers,
context: ({ ctx }) => ({ //传递ctx等信息,主要供认证、授权使用
...ctx,
...app.context
})
})
}
G.ApolloServer.applyMiddleware({ app })
}</code></pre>
<p>静态schema试验,schema_generate.ts</p>
<pre><code>const typeDefs = `
type Author {
id: Int!
name: String
books: [book]
}
type Book {
id: Int!
title: String
author: Author
}
# the schema allows the following query:
type Query {
books: [Post]
author(id: Int!): Author
}
`
const resolvers = {
Query: {
books: async function (_, args) {
let rs = await new BaseDao('book').retrieve(args)
return rs.data
},
author: async function (_, { id }) {
let rs = await new BaseDao('author').retrieve({id})
return rs.data[0]
},
},
Author: {
books: async function (author) {
let rs = await new BaseDao('book').retrieve({ author_id: author.id })
return rs.data
},
},
Book: {
author: async function (book) {
let rs = await new BaseDao('author').retrieve({ id: book.author_id })
return rs.data[0]
},
},
}
export {
typeDefs,
resolvers
}
</code></pre>
<h2>项目地址</h2>
<pre><code>https://github.com/zhoutk/gels</code></pre>
<h2>使用方法</h2>
<pre><code>git clone https://github.com/zhoutk/gels
cd gels
yarn
tsc -w
nodemon dist/index.js</code></pre>
<p>然后就可以用浏览器打开链接:<a href="https://link.segmentfault.com/?enc=g1i2w6zOAVHDtNBsmWGTuQ%3D%3D.syI8sW0jOrMvFCWfxfAgnhEzhAdzBEBxWUoH28OXl28%3D" rel="nofollow">http://localhost</a>:5000/graphql 查看效果了。</p>
<h2>小结</h2>
<p>这是第一部分,确定需求,进行了技术选型,实现了接入静态手写schema试验,下篇将实现动态生成与合并特殊业务模型。</p>
leetcode-019-删除链表倒数第N个结点(Remove Nth Node From End of List)
https://segmentfault.com/a/1190000017800786
2019-01-08T00:30:32+08:00
2019-01-08T00:30:32+08:00
zhoutk
https://segmentfault.com/u/zhoutk
7
<h2>leetcode第19题</h2>
<p>Given a linked list, remove the n-th node from the end of list and return its head.<br>给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。</p>
<h2>题中的坑</h2>
<p>这个题要注意的是:网站定义的链表结构head指向第一个有效元素,没有纯正意义上的头结点,我前两次提交就是因为这个问题没过。因为,若有一个真正的头结点,则所有的元素处理方式都一样。但以第一个有效元素为头结点,就导致算法的不一致,需要单独处理第一个有效元素(头结点)。</p>
<h2>题目的额外限制</h2>
<p>Could you do this in one pass?<br>你能尝试使用一趟扫描实现吗?</p>
<p>还好,题目约束,给定n值一定会是有效的(Given n will always be valid.),这可以少写好多的边界判断。</p>
<h2>我的解答(javascript)</h2>
<h3>思路</h3>
<p>n值一定有效,又限定在一趟扫描过程中完成,那就是要在扫描的过程中,保存删除操作的所有信息。很容易想到,链表的长度一定大于n,我们迭代处理的是当前元素,故只需要记住当前元素往前的第n+1个元素(即被删除元素的前一个)就可以了。</p>
<h3>链接结点定义</h3>
<pre><code>function ListNode(val) {
this.val = val
this.next = null;
}</code></pre>
<h3>单链接生成器(用于本地测试)</h3>
<pre><code>function build(arr) {
if(arr.length === 0) //注意:leetcode的空链表逻辑是head=null
return null
let rs = new ListNode(arr[0])
let head = rs
for(let i = 1; i < arr.length; i++) {
let newOne = new ListNode(arr[i])
rs.next = newOne
rs = newOne
}
return head
}</code></pre>
<h3>本地测试代码</h3>
<pre><code> let rs = removeNthFromEnd(build([1,2,3,4,5]), 2)
let cur = rs
let str = ''
while(cur !== null) {
str += `${cur.val} ${cur.next ? '->' : ''} `
cur = cur.next
}
console.log(str)</code></pre>
<h3>解答算法</h3>
<pre><code>var removeNthFromEnd = function(head, n) {
let cur = head //迭代处理当前元素
let m = 0 //偏移量,用来指示要删除的元素
let del = head //要删除的元素
while (cur !== null) {
if(m > n) { //达到并偏移指示窗口
del = del.next
} else {
m++
}
cur = cur.next
}
if (del === head && m === n) //注意,删除头元素与其它元素是不一样的
head = head.next //测试用例:[1] 1; [1,2] 2
else
del.next = del.next.next
return head
};</code></pre>
<h3>leetcode提交结果</h3>
<p>Runtime: 56 ms, faster than 100.00% of JavaScript online submissions for Remove Nth Node From End of List.</p>
<p>我的第一个运行速度超过所有提交者的解答,^_^</p>
<h2>完整代码</h2>
<pre><code>https://github.com/zhoutk/leetcode/blob/master/javascript/qs019_removeNthFromEnd_1.js</code></pre>
<h2>小结</h2>
<p>本文主要标记leetcode中的链表结构的特殊性,head直接指向第一个有效元素。</p>
typescript 3.2 新编译选项strictBindCallApply
https://segmentfault.com/a/1190000017720368
2019-01-02T21:33:22+08:00
2019-01-02T21:33:22+08:00
zhoutk
https://segmentfault.com/u/zhoutk
3
<h2>突发错误</h2>
<p>我的gels项目(<a href="https://link.segmentfault.com/?enc=LyYRQKfk6b%2FHiwGaKQ8Xlw%3D%3D.198Qjb74lJ1KslD7qlXn%2FeolEuFGh5jDd8qrM36yd64%3D" rel="nofollow">https://github.com/zhoutk/gels</a>),几天没动,突然tsc编译出错,信息如下:</p>
<pre><code>src/app.ts:28:38 - error TS2345: Argument of type 'any[]' is not assignable to parameter of type '[Middleware<ParameterizedContext<any, {}>>]'.
Property '0' is missing in type 'any[]' but required in type '[Middleware<ParameterizedContext<any, {}>>]'.
28 m && (app.use.apply(app, [].concat(m)))</code></pre>
<p>我的源代码,是动态加载Koa的中间件,app是koa2实例</p>
<pre><code> for (let m of [].concat(middleware)) {
m && (app.use.apply(app, [].concat(m)))
}</code></pre>
<h2>问题分析</h2>
<p>几天前还是正常编译、正常运行的项目,突然出错,应该是环境变了。经查找,发现全局typescript已经升级到了最新版本,3.2.2,而项目中的版本是3.0.3。 <br>将全局版本换回3.0.3,编译通过,问题找到。</p>
<h2>问题定位</h2>
<p>上typescrpt的github主页,找发布日志,发现3.2的新功能,第一条就是:</p>
<pre><code>TypeScript 3.2 introduces a new --strictBindCallApply compiler option (in the --strict family of options) with which the bind, call, and apply methods on function objects are strongly typed and strictly checked.</code></pre>
<p>大概意思是:TypeScript 3.2引入一个新的编译选项 --strictBindCallApply,若使用这个选项,函数对象的bind,call和apply方法是强类型的,并进行严格检测。</p>
<h2>解决方案</h2>
<p>因为是动态参数,想了半天我没法明确声明类型。因此,我在tsconfig.json配置文件中,设置"strictBindCallApply": false,将这个编译选项关闭,这样就可以将typescript升级到3.2.2了。<br>哪位朋友若有能打开strictBindCallApply选项,又能通过编译的方案,烦请告知一下,谢谢!我若哪天找到方法,会马上更新本文章。</p>
简单易用的leetcode开发测试工具(npm)
https://segmentfault.com/a/1190000017356892
2018-12-12T22:59:58+08:00
2018-12-12T22:59:58+08:00
zhoutk
https://segmentfault.com/u/zhoutk
6
<h2>描述</h2>
<p>最近在用es6解leetcode,当问题比较复杂时,有可能修正了新的错误,却影响了前面的流程。要用通用的测试工具,却又有杀鸡用牛刀的感觉,所以就写了个简单易用的leetcode开发测试工具,分享与大家。</p>
<h2>工具安装</h2>
<p>npm i leetcode_test</p>
<h2>使用示例1 (问题010)</h2>
<p>codes:</p>
<pre><code>let test = require('leetcode_test').test
/**
* @param {string} s
* @param {string} p
* @return {boolean}
*/
var isMatch = function (s, p) {
if (p.length === 0) {
return s.length === 0
}
firstMath = s.length > 0 &&
(p[0] === s[0] ||
p[0] === '.')
if (p.length >= 2 && p[1] === '*') {
//下面两部分的顺序不能交换
return firstMath && isMatch(s.substring(1), p) || isMatch(s, p.substring(2))
} else {
return firstMath && isMatch(s.substring(1), p.substring(1))
}
};
let cases = [ // [[[],''],], //第一个参数是空数组
[['abbabaaaaaaacaa', 'a*.*b.a.*c*b*a*c*'], true],
[['aaa', 'a*ac'], true], //故意写错答案,展示测试失败输出效果
[['a', '..*'], true],
]
test(isMatch, cases)</code></pre>
<blockquote>测试用例编写说明</blockquote>
<p>leetcode要测试的都是函数,参数个数不定,但返回值是一个。因此,我设计用例的输入形式为一个用例就是一个两个元素的数组,第一个元素是一个数组:对应输入参数;第二个元素是一个值。<br>上面例子的输入参数是([2, 7, 11, 15], 91),第一个参数是数组,第二个参数是数值;返回值是一个数组([0, 1])。 如果要测试的函数的输入参数就是一个数组,要注意输入形式,比如,求[1,2,3,4]平均值,要这样输入测试用例: [[[1,2,3,4]],2.5]</p>
<p>out:</p>
<pre><code>test [1] success, Input: ('abbabaaaaaaacaa','a*.*b.a.*c*b*a*c*'); Expected: true; Output: true
test [2] fail, Input: ('aaa','a*ac'); Expected: true; Output: false
test [3] success, Input: ('a','..*'); Expected: true; Output: true
Result: test 3 cases, success: 2, fail: 1
running 5 ms</code></pre>
<h2>使用示例2 (问题015)</h2>
<p>codes:</p>
<pre><code>let test = require('leetcode_test').test
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function (nums) {
nums = nums.sort((a,b) => a - b);
const rs = [];
let i = 0;
while (i < nums.length) {
let one = nums[i];
let two = i + 1; //从队列头部开始
let three = nums.length - 1; //从队列尾部开始
while (two < three) {
let sum = one + nums[two] + nums[three];
if (sum === 0) {
rs.push([one,nums[two],nums[three]]);
two++;
three--;
while (two < three && nums[two] === nums[two - 1]) {
two++;
}
while (two < three && nums[three] === nums[three + 1]) {
three--;
}
} else if (sum > 0) three--;
else two++;
}
i++;
while (i < nums.length && nums[i] === nums[i - 1]) i++;
}
return rs;
};
let cases = [ // [[[],''],], //第一个参数是空数组
[[[]],[]],
[[[1,-1,-1,0]],[-1,0,1]],
[[[-1,0,1,0]],[[-1,0,1]]],
[[[0,0,0,0]],[0,0,0]],
[[[-1,2,-1]],[-1,-1,2]],
[[[0,0,0]],[0,0,0]],
[[[-1,0,1,2,-1,-4]],[[-1,-1,2],[-1,0,1]]], //answer's sequence is not important
[[[-1,0,1,2,-1,-4]],[[-1,0,1],[-1,-1,2]]], //answer's sequence is not important
[[[-4,-2,-2,-2,0,1,2,2,2,3,3,4,4,6,6]],[[-4,-2,6],[-4,0,4],[-4,1,3],[-4,2,2],[-2,-2,4],[-2,0,2]]],
[[[-4,-2,1,-5,-4,-4,4,-2,0,4,0,-2,3,1,-5,0]],[[-5,1,4],[-4,0,4],[-4,1,3],[-2,-2,4],[-2,1,1],[0,0,0]]],
]
test(threeSum,cases)</code></pre>
<blockquote>测试用例编写说明</blockquote>
<p>测试用例的7与8,期待结果的数组元素顺序并不影响答案的判定。</p>
<p>out:</p>
<pre><code>test [1] success, Input: ([]); Expected: []; Output: []
test [2] success, Input: ([-1,-1,0,1]); Expected: [-1,0,1]; Output: [[-1,0,1]]
test [3] success, Input: ([-1,0,0,1]); Expected: [[-1,0,1]]; Output: [[-1,0,1]]
test [4] success, Input: ([0,0,0,0]); Expected: [0,0,0]; Output: [[0,0,0]]
test [5] success, Input: ([-1,-1,2]); Expected: [-1,-1,2]; Output: [[-1,-1,2]]
test [6] success, Input: ([0,0,0]); Expected: [0,0,0]; Output: [[0,0,0]]
test [7] success, Input: ([-4,-1,-1,0,1,2]); Expected: [[-1,-1,2],[-1,0,1]]; Output: [[-1,-1,2],[-1,0,1]]
test [8] success, Input: ([-4,-1,-1,0,1,2]); Expected: [[-1,-1,2],[-1,0,1]]; Output: [[-1,-1,2],[-1,0,1]]
test [9] success, Input: ([-4,-2,-2,-2,0,1,2,2,2,3,3,4,4,6,6]); Expected: [[-2,-2,4],[-2,0,2],[-4,-2,6],[-4,0,4],[-4,1,3],[-4,2,2]]; Output: [[-2,-2,4],[-2,0,2],[-4,-2,6],[-4,0,4],[-4,1,3],[-4,2,2]]
test [10] success, Input: ([-5,-5,-4,-4,-4,-2,-2,-2,0,0,0,1,1,3,4,4]); Expected: [[-2,-2,4],[-2,1,1],[-4,0,4],[-4,1,3],[-5,1,4],[0,0,0]]; Output: [[-2,-2,4],[-2,1,1],[-4,0,4],[-4,1,3],[-5,1,4],[0,0,0]]
Result: test 10 cases, success: 10, fail: 0</code></pre>
<h2>项目地址</h2>
<p>工具地址:<a href="https://link.segmentfault.com/?enc=SstzHr8Td9Lc4R2BGK7Zzw%3D%3D.tE7vEtzl4fHDWfg0ntNJ6V9HVEj8a942WlkL09OCQknb07E0OtDTwYOAl0OkkaL2" rel="nofollow">https://github.com/zhoutk/lee...</a><br>解答地址:<a href="https://link.segmentfault.com/?enc=dMhE8cpK5HZN0Fsx%2Blw6rg%3D%3D.JMfkwBL3hiTXUhYp8dYv7XnRnBBPki3ASCKqMJLZmtVQ1rrHruH4SD9avlXGnlQp" rel="nofollow">https://github.com/zhoutk/lee...</a></p>
<p>最近一直在用,已经把输出的样子调得还能看过眼了,答案对比算法,也改进了。遇到问题,我会持续改进,大家遇到问题也可提bug给我,我会尽快处理。</p>
一种巧妙的对象映射关系设计--JSON-ORM
https://segmentfault.com/a/1190000017113544
2018-11-23T10:50:54+08:00
2018-11-23T10:50:54+08:00
zhoutk
https://segmentfault.com/u/zhoutk
7
<h2>项目介绍</h2>
<p>这是标准数据库封装的上半部分,智能查询(JSON-ORM)的实现。完整代码:<a href="https://link.segmentfault.com/?enc=XOuiQJC9rx4SgaQMpzVSww%3D%3D.jiRs57UgbdjOYOl6aJkrxLRu5av5ADa%2FZXcL1MTZyEI%3D" rel="nofollow">https://github.com/zhoutk/gels</a></p>
<h2>设计思路</h2>
<p>我们通用的ORM,基本模式都是想要脱离数据库的,几乎都在编程语言层面建立模型,由程序去与数据库打交道。虽然脱离了数据库的具体操作,但我们要建立各种模型文档,用代码去写表之间的关系等等操作,让初学者一时如坠云雾。我的想法是,将关系数据库拥有的完善设计工具之优势与微服务结合起来,数据设计提供结构信息;前端送到后端的json对象自动映射成为标准的SQL查询语句。只要我们理解了标准的SQL语言,我们就能够完成数据库查询操作。我的这种ORM方式,服务端不需要写一行代码,只需完成关系数据库的设计,就能为前端提供标准服务接口。并且遵循一套统一的接口(已经实践检验,满足百分之九九的查询需求)来实现数据库封装,达到业务层可以随意切换数据库的目的。</p>
<h2>数据库查询操作接口。</h2>
<pre><code>export default interface IDao {
select(tablename: string, params: object, fields?: Array<string>): Promise<any>; //自动生成sql语句
execSql(sql: string, values: Array<any>, params: object, fields?: Array<string>): Promise<any>; //执行手动sql语句
}</code></pre>
<h2>智能查询(JSON-ORM)</h2>
<blockquote>查询保留字:fields, page, size, sort, search, lks, ins, ors, count, sum, group</blockquote>
<ul>
<li>
<p>fields, 定义查询结果字段,支持数组和逗号分隔字符串两种形式<br>由前端来确定返回的数据库字段信息,这样后端的设计可以适用面更广泛,而不会造成网络带宽的浪费。<br>在KOA2的框架下,GET请求要支持输入数组,只能把同一个key多次输入,如:age=11&age=22。这样很不方便,我实现了一个参数转换函数,针对数组提供多种输入形式。<br>arryParse</p>
<pre><code> arryParse(arr): Array<any>|null { //返回值为数据或空值
try {
if (Array.isArray(arr) || G.L.isNull(arr)) { //如果输入是数组或空,直接返回
return arr
} else if (typeof arr === 'string') { //若是字符串
if (arr.startsWith('[')) { //数组的字符串形式,进行转换
arr = JSON.parse(arr)
} else {
//逗号拼接的字符串,mysql的驱动同时支持参数以字符串形式或数组形式提供,
//所以这里可以不加判断,直接用split函数将字符串转化为数组
arr = arr.split(',')
}
}
} catch (err) {
arr = null //数组的字符串形式转换失败,刘明输入参数是错误的
}
return arr
}</code></pre>
<p>查询示例:</p>
<pre><code>请求URL: /rs/users?username=white&age=22&fields=["username","age"]
生成sql: SELECT username,age FROM users WHERE username = ? and age = ?</code></pre>
</li>
<li>
<p>page, size, sort, 分页排序<br>在mysql中这比较好实现,limit来分页是很方便的,排序只需将参数直接拼接到order by后就好了。 <br>查询示例:</p>
<pre><code>请求URL: /rs/users?page=1&size=10&sort=age desc
生成sql: SELECT * FROM users ORDER BY age desc LIMIT 0,10</code></pre>
</li>
<li>
<p>search, 模糊查询切换参数,不提供时为精确匹配<br>提供字段查询的精确匹配与模糊匹配的切换,实现过程中,注意参数化送入参数时,like匹配,是要在参数两边加%,而不是在占位符两边加%。<br>另外,同一个字段匹配两次模糊查询,需要特别处理,我提供了一种巧妙的方法:</p>
<pre><code>//将值用escape编码,数组将转化为逗号连接的字符串,用正则全局替换,变成and连接
value = pool.escape(value).replace(/\', \'/g, "%' and " + key + " like '%")
//去掉两头多余的引号
value = value.substring(1, value.length - 1)
//补齐条件查询语句,这种方式,比用循环处理来得快捷,它统一了数组与其它形式的处理方式
where += key + " like '%" + value + "%'" </code></pre>
<p>查询示例</p>
<pre><code>请求URL: /rs/users?username=i&password=1&search</code></pre>
</li>
<li>
<p>ins, lks, ors<br>这是最重要的三种查询方式,如何找出它们之间的共同点,减少冗余代码是关键。</p>
<ul>
<li>
<p>ins, 数据库表单字段in查询,一字段对多个值,例: <br>查询示例:</p>
<pre><code>请求URL: /rs/users?ins=["age",11,22,26]
生成sql: SELECT * FROM users WHERE age in ( ? )</code></pre>
</li>
<li>
<p>ors, 数据库表多字段精确查询,or连接,多个字段对多个值,支持null值查询,例: <br>查询示例:</p>
<pre><code>请求URL: /rs/users?ors=["age",1,"age",22,"password",null]
生成sql: SELECT * FROM users WHERE ( age = ? or age = ? or password is null )</code></pre>
</li>
<li>
<p>lks, 数据库表多字段模糊查询,or连接,多个字段对多个值,支持null值查询,例:<br>查询示例:</p>
<pre><code>请求URL: /rs/users?lks=["username","i","password",null]
生成sql: SELECT * FROM users WHERE ( username like ? or password is null )</code></pre>
</li>
</ul>
</li>
<li>
<p>count, sum<br>这两个统计求和,处理方式也类似,查询时一般要配合group与fields使用。</p>
<ul>
<li>
<p>count, 数据库查询函数count,行统计,例:<br>查询示例:</p>
<pre><code>请求URL: /rs/users?count=["1","total"]&fields=["username"]
生成sql: SELECT username,count(1) as total FROM users</code></pre>
</li>
<li>
<p>sum, 数据库查询函数sum,字段求和,例:<br>查询示例:</p>
<pre><code>请求URL: /rs/users?sum=["age","ageSum"]&fields=["username"]</code></pre>
</li>
</ul>
</li>
<li>
<p>group, 数据库分组函数group,例: <br>查询示例:</p>
<pre><code>请求URL: /rs/users?group=age&count=["*","total"]&fields=["age"]
生成sql: SELECT age,count(*) as total FROM users GROUP BY age</code></pre>
</li>
</ul>
<blockquote>不等操作符查询支持</blockquote>
<p>支持的不等操作符有:>, >=, <, <=, <>, =;逗号符为分隔符,一个字段支持一或二个操作。 <br>特殊处:使用"="可以使某个字段跳过search影响,让模糊匹配与精确匹配同时出现在一个查询语句中</p>
<ul>
<li>
<p>一个字段一个操作,示例:<br>查询示例:</p>
<pre><code>请求URL: /rs/users?age=>,10
生成sql: SELECT * FROM users WHERE age> ?</code></pre>
</li>
<li>
<p>一个字段二个操作,示例:<br>查询示例:</p>
<pre><code>请求URL: /rs/users?age=>,10,<=,35
生成sql: SELECT * FROM users WHERE age> ? and age<= ?</code></pre>
</li>
<li>
<p>使用"="去除字段的search影响,示例:<br>查询示例:</p>
<pre><code>请求URL: /rs/users?age==,22&username=i&search
生成sql: SELECT * FROM users WHERE age= ? and username like ?</code></pre>
</li>
</ul>
<h2>相关视频课程</h2>
<p><a href="https://segmentfault.com/l/1500000016954243">运用typescript进行node.js后端开发精要</a> <br><a href="https://segmentfault.com/l/1500000017034959">nodejs实战之智能微服务快速开发框架</a> <br><a href="https://segmentfault.com/l/1500000017108031">JSON-ORM(对象关系映射)设计与实现</a></p>
<h2>资源地址</h2>
<p>凝胶(gels)项目: <a href="https://link.segmentfault.com/?enc=%2Fz4UyWEsHWREafvzSCDNQQ%3D%3D.v4ye6gPgad8kIBpgTpqfm2AY5wU0yd7Nl1TtIi6kYe0%3D" rel="nofollow">https://github.com/zhoutk/gels</a><br>视频讲座资料: <a href="https://link.segmentfault.com/?enc=lea9Chy7SU9JvY3gXEK%2BQQ%3D%3D.MUl5bQ%2FVBHuSvgWOnWVlJ9m9qce4%2FSt5zxVkN98QupE%3D" rel="nofollow">https://github.com/zhoutk/sifou</a><br>个人博客: <a href="https://segmentfault.com/blog/zhoutk">https://segmentfault.com/blog...</a></p>
学习编程并不是学习编程语言
https://segmentfault.com/a/1190000017073204
2018-11-20T12:29:18+08:00
2018-11-20T12:29:18+08:00
zhoutk
https://segmentfault.com/u/zhoutk
18
<blockquote>作者:zooboole<br>英文原文:<a href="https://link.segmentfault.com/?enc=%2F%2Bh%2BO4cZ%2FfxaSWjY9buREg%3D%3D.5oM2VGxkqQvF%2FPKrKpG9Bx%2B%2BL0Fzbydtdk%2F6yoFGAyIduGhoRKADhsGQigQ%2BFWQGdA6Br3Cj6izjiipiLrUatVR1y09WVV7FC95ZS%2Fe5Qs80RQbBUdem3nY7rCcElwFT44jzuvPwgVrmmZXfjhe3tw%3D%3D" rel="nofollow">《Learning programming is different from learning a programming language》</a>
</blockquote>
<p>我们都是程序员,也是学习者。令人惊讶的是,如此多的人以为自己在学习编程,却已经步入歧途。</p>
<h2>你可能正在学习编程语言,而不是编程本身</h2>
<p>大家都知道计算机科学不是研究计算机,它反倒是利用计算机研究自动解决问题的。问题解决是计算机科学,不是编程。这就许多计算机科学专业的学生似乎不理解他们为什么要学习算法或数学的原因。</p>
<p>如果你以前上过计算机科学课,你就应该知道我在说什么。因为你会注意到编程与编程语言几乎没有关系。问问自己为什么伪代码在这些课程中如此常见。</p>
<p>但是,大多数自以为是的程序员总是落入陷阱。在意识到进行编程时到底什么是应该要做的之前,我们学习了几十年的编程语言。我自己也是受害者。</p>
<p>我花了十多年的时间一点一点地学习各种编程语言。我学的越多,就越难以简单的方式解决问题。我以为是没有找到合适的工具。但问题是,当我甚至还不知道这个工作要做什么时,就去寻找合适的工具,而忘记了找出真正的工作是该做什么。</p>
<p>编程语言的奇怪之处在于它们总是在不断发展。编程语言几乎每天都在变化,跟进很难。而<strong>大多数优秀的程序只使用了编程语言的一小部分</strong>。</p>
<p>首先,学习编程语言的问题就像在学习木工之前学习如何使用木工锯,锤子和各种切割机器。木工需要注意:想法,可行性分析,测量,测试,客户行为。资深木匠感兴趣的事物不止于锤子和钉子。在他对这项工作的研究中,还需要时间来检查钉子、着色剂、木材等的质量。</p>
<h2>学习编程和学习编程语言的区别是什么呢?</h2>
<p>编程是通过一次下达指令来设置一个系统自动运行。我们每天都这样做。我们教我们的孩子,命令我们的士兵,服务我们的客户。我们给予或收到指示,以自由/独立的方式生活。父母不需要跟随并指导你在生活中所做的每一个动作。他们可能已经在生活的许多方面为你编程了。</p>
<p>大多数学校和教学网站都会教授编程语言的语法。他们可以添加一些设计模式(当你忽略究竟是什么设计时)、一些算术计算。教你如何声明变量以及如何使用它们;教你如何声明数据类型以及创建它们。</p>
<p>这并不能教你推理。但后来,您将会遇见推理方法。使用那些方法来学习,会让你觉得是浪费生命或者花了很多时间来学习编程。</p>
<p><strong>我们用编程来解决问题,编程语言是帮助我们达到目的工具。</strong></p>
<p><strong>它们就象工具箱,我们称之为框架,帮助你组织你的思维。</strong></p>
<p>如果你正在学习编程且仍然无法设计和编写真实应用程序,那么这就意味着你正在学习编程语言而不是编程。</p>
<p>我们经常会遇到想知道如何创建程序的学习者。对于程序员来说,程序是一个问题求解。在使用任何编程语言之前,他通过关键分析解决了问题。当你解决任何问题时,你可以用任何编程语言来编码。我们来看看平方求解的案例。为了求解平方,我们将它与自己相乘。我们可以用各种语言实现它,例如:</p>
<p>C语言</p>
<pre><code>function square(int * x) {
return x * x;
}</code></pre>
<p>PHP语言</p>
<pre><code>function square ($x){
return $x * $x;
}</code></pre>
<p>Javascript语言</p>
<pre><code>function square(x){
return x * x
}</code></pre>
<p>Scheme(a Lisp dialect)语言</p>
<pre><code>(define (square x) (* x x))</code></pre>
<p>您应该注意到实现中只有语法是不一样的,解决方案是一样的。这也是你几乎可以使用任何编程语言的主要原因之一,在这种语言中你可以更轻松地构建任何类型的软件。</p>
<h2>编程可以让你更容易理解一门语言</h2>
<p>通常,问题出在人类语言,它充满了局限和错误。人类语言不能用来指令机器,因为它们不理解。</p>
<p>你学习编程时,是在学习一种新术语和工具,来帮助你以计算机或其他程序员可以理解和同意的方式编写逻辑。</p>
<p>通常,你将从简单且类似人类语言的符号--伪代码开始。它是从人类语言到计算机编程语言的良好过渡工具。这通常是为了避免浪费时间在具体的编程语言上,这样你可以完全专注于推理。通过它,你将发现构成良好编程工具(语言)的核心部分。你知道了真正需要的是什么、掌握了编程语言的核心目标。在编程实践过程中,你会不知不觉地就学会了这门编程语言。</p>
<h2>相关文章</h2>
<p><a href="https://segmentfault.com/a/1190000016960085">如何学习一门计算机编程语言</a></p>
智能微服务的设计与开发(node.js)
https://segmentfault.com/a/1190000017041045
2018-11-16T17:11:33+08:00
2018-11-16T17:11:33+08:00
zhoutk
https://segmentfault.com/u/zhoutk
18
<h2>设计目标</h2>
<p>基于koa2、关系数据库(暂时只支持mysql)建立的智能微服务快速开发框架,将同时支持graphql与rest标准,使用typescript语言编写,力求安全、高效。 </p>
<p>相关开源项目(gels -- 凝胶),希冀该项目能成为联结设计、开发,前端、后端的“强力胶水”,成为微服务快速开发的有力框架。 <br><strong>项目地址:</strong><a href="https://link.segmentfault.com/?enc=woY2Efa0zPV7QttTmlYvTw%3D%3D.RXBfi6dv%2BQNzFGSl23o7Kb5jcO8R2fwToUKIvlBy%2FHY%3D" rel="nofollow">https://github.com/zhoutk/gels</a></p>
<h2>设计思路</h2>
<p>中小型企业,更多的是注重快速开发、功能迭代。关系数据库为我们提供了很多有用的支持,我试图把数据库设计与程序开发有机的结合起来,让前端送到后端的json对象自动映射成为标准的SQL查询语句。我的这种ORM方式,服务端不需要写一行代码,只需完成关系数据库的设计,就能为前端提供标准服务接口。 <br>我设计了一套数据库访问标准接口,在实践中已经得到很好的运用。我已经在es6, typescript, java, python & go中实现;下一步是对数据库支持的扩展,准备支持流行的关系数据库(Mssql, sqlite3, prostgres等),有选择支持一些nosql,比如:mongo。</p>
<h2>数据库接口设计</h2>
<ul>
<li>
<p>事务元素接口,sql参数用于手动书写sql语句,id会作为最后一个参数被送入参数数组。</p>
<pre><code>export default interface TransElement {
table: string;
method: string;
params: object | Array<any>;
sql?: string;
id?: string | number;
}</code></pre>
</li>
<li>
<p>数据库操作接口,包括基本CURD,两个执行手写sql接口,一个批量插入与更新二合一接口,一个事务操作接口。实践证明,下面八个接口,在绝大部分情况下已经足够。</p>
<pre><code>export default interface IDao {
select(tablename: string, params: object, fields?: Array<string>): Promise<any>;
insert(tablename: string, params: object): Promise<any>;
update(tablename: string, params: object, id: string|number): Promise<any>;
delete(tablename: string, id: string|number): Promise<any>;
querySql(sql: string, values: Array<any>, params: object, fields?: Array<string>): Promise<any>;
execSql(sql: string, values: Array<any>): Promise<any>;
insertBatch(tablename: string, elements: Array<any>): Promise<any>;
transGo(elements: Array<TransElement>, isAsync?: boolean): Promise<any>;
}</code></pre>
</li>
<li>
<p>BaseDao,为业务层提供标准数据库访问的基类,是自动提供标准rest微服务的关键</p>
<pre><code>import IDao from './idao'
let dialect = G.CONFIGS.db_dialect //依赖注入
let Dao = require(`./${dialect}Dao`).default
export default class BaseDao {
private table: string
static dao: IDao //以组合的模式,解耦业务层与数据库访问层
constructor(table?: string) {
this.table = table || ''
if (!BaseDao.dao) {
BaseDao.dao = new Dao()
}
}
async retrieve(params = {}, fields = [], session = {userid: ''}): Promise<any> {
let rs
try {
rs = await BaseDao.dao.select(this.table, params, fields)
} catch (err) {
err.message = `data query fail: ${err.message}`
return err
}
return rs
}
async create(params = {}, fields = [], session = {userid: ''}): Promise<any> {
let rs
try {
rs = await BaseDao.dao.insert(this.table, params)
} catch (err) {
err.message = `data insert fail: ${err.message}`
return err
}
let { affectedRows } = rs
return G.jsResponse(200, 'data insert success.', { affectedRows, id: rs.insertId })
}
async update(params, fields = [], session = { userid: '' }): Promise<any> {
params = params || {}
const { id, ...restParams } = params
let rs
try {
rs = await BaseDao.dao.update(this.table, restParams, id)
} catch (err) {
err.message = `data update fail: ${err.message}`
return err
}
let { affectedRows } = rs
return G.jsResponse(200, 'data update success.', { affectedRows, id })
}
async delete(params = {}, fields = [], session = {userid: ''}): Promise<any> {
let id = params['id']
let rs
try {
rs = await BaseDao.dao.delete(this.table, id)
} catch (err) {
err.message = `data delete fail: ${err.message}`
return err
}
let {affectedRows} = rs
return G.jsResponse(200, 'data delete success.', { affectedRows, id })
}
}</code></pre>
</li>
</ul>
<h2>默认路由</h2>
<ul>
<li>
<p>/op/:command,只支持POST请求,不鉴权,提供登录等特定服务支持</p>
<ul><li>login,登录接口;输入参数{username, password};登录成功返回参数:{status:200, token}</li></ul>
</li>
<li>/rs/:table[/:id],支持四种restful请求,GET, POST, PUT, DELELTE,除GET外,其它请求检测是否授权</li>
</ul>
<h2>中间件</h2>
<ul>
<li>globalError,全局错误处理中间件</li>
<li>router,路由中间件</li>
<li>logger,日志,集成log4js,输出系统日志</li>
<li>
<p>session,使用jsonwebtoken,实现鉴权;同时,为通过的鉴权的用户生成对应的session</p>
<ul><li>用户登录成功后得到的token,在以后的ajax调用时,需要在header头中加入token key</li></ul>
</li>
</ul>
<h2>restful_api</h2>
<p>数据表库设计完成后,会自动提供如下形式的标准restful api,多表关系可用关系数据库的视图来完成。</p>
<ul>
<li>[GET] /rs/users[?key=value&...], 列表查询,支持各种智能查询</li>
<li>[GET] /rs/users/{id}, 单条查询</li>
<li>[POST] /rs/users, 新增记录</li>
<li>[PUT] /rs/users/{id}, 修改记录</li>
<li>[DELETE] /rs/users/{id}, 删除记录</li>
</ul>
<h2>智能查询</h2>
<blockquote>查询保留字:fields, page, size, sort, search, lks, ins, ors, count, sum, group</blockquote>
<ul>
<li>
<p>fields, 定义查询结果字段,支持数组和逗号分隔字符串两种形式</p>
<pre><code>查询示例: /rs/users?username=white&age=22&fields=["username","age"]
生成sql: SELECT username,age FROM users WHERE username = ? and age = ?</code></pre>
</li>
<li>page, 分页参数,第几页</li>
<li>size, 分页参数,每页行数</li>
<li>
<p>sort, 查询结果排序参数</p>
<pre><code>查询示例: /rs/users?page=1&size=10&sort=age desc
生成sql: SELECT * FROM users ORDER BY age desc LIMIT 0,10</code></pre>
</li>
<li>
<p>search, 模糊查询切换参数,不提供时为精确匹配</p>
<pre><code>查询示例: /rs/users?username=i&password=1&search
生成sql: SELECT * FROM users WHERE username like ? and password like ?</code></pre>
</li>
<li>
<p>ins, 数据库表单字段in查询,一字段对多个值,例:</p>
<pre><code>查询示例: /rs/users?ins=["age",11,22,26]
生成sql: SELECT * FROM users WHERE age in ( ? )</code></pre>
</li>
<li>
<p>ors, 数据库表多字段精确查询,or连接,多个字段对多个值,支持null值查询,例:</p>
<pre><code>查询示例: /rs/users?ors=["age",1,"age",22,"password",null]
生成sql: SELECT * FROM users WHERE ( age = ? or age = ? or password is null )</code></pre>
</li>
<li>
<p>lks, 数据库表多字段模糊查询,or连接,多个字段对多个值,支持null值查询,例:</p>
<pre><code>查询示例: /rs/users?lks=["username","i","password",null]
生成sql: SELECT * FROM users WHERE ( username like ? or password is null )</code></pre>
</li>
<li>
<p>count, 数据库查询函数count,行统计,例:</p>
<pre><code>查询示例: /rs/users?count=["1","total"]&fields=["username"]
生成sql: SELECT username,count(1) as total FROM users</code></pre>
</li>
<li>
<p>sum, 数据库查询函数sum,字段求和,例:</p>
<pre><code>查询示例: /rs/users?sum=["age","ageSum"]&fields=["username"]
生成sql: SELECT username,sum(age) as ageSum FROM users</code></pre>
</li>
<li>
<p>group, 数据库分组函数group,例:</p>
<pre><code>查询示例: /rs/users?group=age&count=["*","total"]&fields=["age"]
生成sql: SELECT age,count(*) as total FROM users GROUP BY age</code></pre>
</li>
</ul>
<blockquote>不等操作符查询支持</blockquote>
<p>支持的不等操作符有:>, >=, <, <=, <>, =;逗号符为分隔符,一个字段支持一或二个操作。 <br>特殊处:使用"="可以使某个字段跳过search影响,让模糊匹配与精确匹配同时出现在一个查询语句中</p>
<ul>
<li>
<p>一个字段一个操作,示例:</p>
<pre><code>查询示例: /rs/users?age=>,10
生成sql: SELECT * FROM users WHERE age> ?</code></pre>
</li>
<li>
<p>一个字段二个操作,示例:</p>
<pre><code>查询示例: /rs/users?age=>,10,<=,35
生成sql: SELECT * FROM users WHERE age> ? and age<= ?</code></pre>
</li>
<li>
<p>使用"="去除字段的search影响,示例:</p>
<pre><code>查询示例: /rs/users?age==,22&username=i&search
生成sql: SELECT * FROM users WHERE age= ? and username like ?</code></pre>
</li>
</ul>
<h2>高级操作</h2>
<ul>
<li>
<p>新增一条记录</p>
<ul><li>url</li></ul>
<pre><code> [POST]/rs/users</code></pre>
<ul><li>header</li></ul>
<pre><code> Content-Type: application/json
token: eyJhbGciOiJIUzI1NiIsInR...</code></pre>
<ul><li>输入参数</li></ul>
<pre><code> {
"username":"bill",
"password":"abcd",
"age":46,
"power": "[\"admin\",\"data\"]"
}</code></pre>
<ul><li>返回参数</li></ul>
<pre><code> {
"affectedRows": 1,
"id": 7,
"status": 200,
"message": "data insert success."
}</code></pre>
</li>
<li>
<p>execSql执行手写sql语句,供后端内部调用</p>
<ul><li>使用示例</li></ul>
<pre><code> await new BaseDao().execSql("update users set username = ?, age = ? where id = ? ", ["gels","99","6"])</code></pre>
<ul><li>返回参数</li></ul>
<pre><code> {
"affectedRows": 1,
"status": 200,
"message": "data execSql success."
}</code></pre>
</li>
<li>
<p>insertBatch批量插入与更新二合一接口,供后端内部调用</p>
<ul><li>使用示例</li></ul>
<pre><code> let params = [
{
"username":"bill2",
"password":"523",
"age":4
},
{
"username":"bill3",
"password":"4",
"age":44
},
{
"username":"bill6",
"password":"46",
"age":46
}
]
await new BaseDao().insertBatch('users', params)</code></pre>
<ul><li>返回参数</li></ul>
<pre><code> {
"affectedRows": 3,
"status": 200,
"message": "data batch success."
}</code></pre>
</li>
<li>
<p>tranGo事务处理接口,供后端内部调用</p>
<ul><li>使用示例</li></ul>
<pre><code> let trs = [
{
table: 'users',
method: 'Insert',
params: {
username: 'zhou1',
password: '1',
age: 1
}
},
{
table: 'users',
method: 'Insert',
params: {
username: 'zhou2',
password: '2',
age: 2
}
},
{
table: 'users',
method: 'Insert',
params: {
username: 'zhou3',
password: '3',
age: 3
}
}
]
await new BaseDao().transGo(trs, true) //true,异步执行;false,同步执行</code></pre>
<ul><li>返回参数</li></ul>
<pre><code> {
"affectedRows": 3,
"status": 200,
"message": "data trans success."
}</code></pre>
</li>
</ul>
<h2>安装运行</h2>
<ul>
<li>
<p>运行数据脚本</p>
<pre><code>SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for `users`
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`power` json DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of `users`
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES ('1', 'white', '123', '22', null), ('2', 'john', '456i', '25', null), ('3', 'marry', null, '22', null), ('4', 'bill', '123', '11', null), ('5', 'alice', '122', '16', null), ('6', 'zhoutk', '123456', '26', null);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;</code></pre>
</li>
<li>
<p>配置文件示例,./src/config/configs.ts</p>
<pre><code>export default {
inits: {
directory: {
run: false,
dirs: ['public/upload', 'public/temp']
}
},
port: 5000,
db_dialect: 'mysql',
dbconfig: {
db_host: 'localhost',
db_port: 3306,
db_name: 'strest',
db_user: 'root',
db_pass: '123456',
db_char: 'utf8mb4',
db_conn: 10,
},
jwt: {
secret: 'zh-tf2Gp4SFU>a4bh_$3#46d0e85W10aGMkE5xKQ',
expires_max: 36000 //10小时,单位:秒
},
}</code></pre>
</li>
<li>
<p>在终端(Terminal)中依次运行如下命令</p>
<pre><code>git clone https://github.com/zhoutk/gels
cd gels
npm i -g yarn
yarn global install typescript tslint nodemon
yarn install
tsc -w //或 command + shift + B,选 tsc:监视
yarn start //或 node ./dist/index.js</code></pre>
</li>
</ul>
<h2>项目结构</h2>
<pre><code>├── package.json
├── src //源代码目录
│ ├── app.ts //koa配置及启动
│ ├── common //通用函数或元素目录
│ │ ├── globUtils.ts
│ ├── config //配置文件目录
│ │ ├── configs.ts
│ ├── db //数据封装目录
│ │ ├── baseDao.ts
│ ├── globals.d.ts //全局声明定义文件
│ ├── index.ts //运行入口
│ ├── inits //启动初始化配置目录
│ │ ├── global.ts
│ │ ├── index.ts
│ │ ├── initDirectory.ts
│ ├── middlewares //中间件目录
│ │ ├── globalError.ts
│ │ ├── logger.ts
│ │ ├── router
│ │ └── session.ts
│ └── routers //路由配置目录
│ ├── index.ts
│ └── router_rs.ts
├── tsconfig.json
└── tslint.json</code></pre>
<h2>相关资源地址</h2>
<p>凝胶(gels)项目: <a href="https://link.segmentfault.com/?enc=OAzdVSUFWxRum6Bv7%2FwdPw%3D%3D.BRV7DzUb%2FXLo21JjQyAlWjQXUqViCV299Pu4qP8dfJA%3D" rel="nofollow">https://github.com/zhoutk/gels</a><br>视频讲座资料: <a href="https://link.segmentfault.com/?enc=zK%2Baq4%2BHsY%2BmahvuAH56Fw%3D%3D.q6BlM7rFLXgTyUr25nyrIC5%2FA9XSs4dAuDyK53zEdIo%3D" rel="nofollow">https://github.com/zhoutk/sifou</a><br>个人博客: <a href="https://segmentfault.com/blog/zhoutk">https://segmentfault.com/blog...</a></p>
<h2>相关视频课程</h2>
<ol>
<li><a href="https://segmentfault.com/l/1500000016954243">运用typescript进行node.js后端开发精要</a></li>
<li>
<p><a href="https://segmentfault.com/l/1500000017034959">nodejs实战之智能微服务快速开发框架</a> <br>视频2亮点:</p>
<ul>
<li>成熟项目经验,经过总结、提炼、精简、重构,带领大家一步步实现一个智能微服务框架,这是一个完整的实用项目从无到有的实现过程。</li>
<li>课程内容与结构经过精心准备,设计合理、节奏紧凑、内容翔实。真实再现了思考、编码、调试、除 错的整个开发过程。</li>
<li>购置了新的录屏软件和录音设备,去除了上个视频中的杂音,清晰度更高,视频比较长,有三个小时。</li>
</ul>
</li>
</ol>
如何学习一门计算机编程语言
https://segmentfault.com/a/1190000016960085
2018-11-09T11:13:01+08:00
2018-11-09T11:13:01+08:00
zhoutk
https://segmentfault.com/u/zhoutk
39
<h2>序言</h2>
<p>计算机编程是一个实践性很强的“游戏”,对于新入门者,好多人都在想,哪一门编程语言最好,我该从哪开始呢?我的回答是:语言不重要,理解编程思想才是最关键的!所有编程语言都支持的那一部分语言特性(核心子集)才是最核心的部分。所以从实际情况出发,选一门你看着顺眼,目前比较贴近你要做的工作或学习计划的计算机语言,开始你的编程之旅吧。</p>
<h2>观点阐述</h2>
<h3>语言的核心子集包括哪些部分</h3>
<ul>
<li>基本数据类型及运算符,这包括常量、变量、数组(所有的语言都支持一种基本数据结构)的定义与使用;数学运算符与逻辑运行符等知识。</li>
<li>分支与循环,这是一门语言中的流程控制部分。</li>
<li>基本库函数的使用,编程不可能从零开始,每门语言都有一个基本函数库,帮我们处理基本输入输出、文件读写等能用操作。</li>
</ul>
<p>业界有一个二八规律,其实编程也一样,大家回头看看,我们写了那么多代码,是不是大部分都属于这门语言的核心子集部分?也就是说,我们只要掌握了一门语言的核心子集,就可以开始工作了。</p>
<h3>常用编程范式</h3>
<ul>
<li>面向过程编程(最早的范式,即命令式)</li>
<li>面向对象编程(设计模式的概念是从它的实践活动中总结出来的)</li>
<li>函数式编程(以纯函数基础,可以任意组合函数,实现集合到集合的流式数据处理)</li>
<li>声明式编程(以数据结构的形式来表达程序执行的逻辑)</li>
<li>事件驱动编程(其分布式异步事件模式,常用来设计大规模并发应用程序)</li>
<li>面向切面编程(避免重复,分离关注点)</li>
</ul>
<p>我们要尽量多的了解不同的编程范式,这样能拓展我们的思路。学习语言的时候,有时可以同时学时两门编程语言,对比学习两门语言的同一概念,让我们能够更容易且深入的理解它。我学习javascript的闭包时,开始怎么也理解不了;我就找了本python书,对比着学,才慢慢的理解了。</p>
<h3>编程语言分类</h3>
<ul>
<li>
<p>编译型语言 VS 解释型语言</p>
<ul>
<li>编译型:C、C++、Pascal、Object-C、swift</li>
<li>解释型:JavaScript、Python、Erlang、PHP、Perl、Ruby</li>
<li>混合型:java、C#,C#,javascript(基于V8)</li>
</ul>
</li>
<li>
<p>动态结构语言 VS 静态结构语言</p>
<ul>
<li>动态语言:Python、Ruby、Erlang、JavaScript、swift、PHP、SQL、Perl</li>
<li>静态语言:C、C++、C#、Java、Object-C</li>
</ul>
</li>
<li>
<p>强类型语言 VS 弱类型语言</p>
<ul>
<li>强类型:Java、C#、Python、Object-C、Ruby</li>
<li>弱类型:JavaScript、PHP、C、C++(C、C++,有争议,介于强弱之间)</li>
</ul>
</li>
</ul>
<p>各种类型的语言,我们都要有所了解,这样才能够全面的理解编程语言中的各种特性,在面对特定的问题时,才能做出正确的选择。</p>
<h2>通过实际项目来学习语言(以Typescript为例)</h2>
<p>项目需求:统一处理不同图形(圆形、长方形、矩形等)的面积计算。</p>
<h3>面向对象三大原则</h3>
<p>1.Circle类讲解数据封装概念,将半径与名称封装在类内部,并提供访问方法</p>
<pre><code>export default class Circle {
private r: number
private name: string
constructor(r: number) {
this.r = r
this.name = 'Circle'
}
getName(): string {
return this.name
}
area(): number {
return Math.pow(this.r, 2) * PI
}
}</code></pre>
<p>2.长方形与矩形讲解继承概念</p>
<pre><code>//rectangle.ts
export default class Rectangle {
private a: number
private b: number
private name: string
constructor(a: number, b: number, name?: string) {
this.a = a
this.b = b
if (name === undefined)
this.name = 'Rectangle'
else
this.name = name
}
getName(): string {
return this.name
}
area(): number {
return this.a * this.b
}
}
//square.ts
export default class Square extends Rectangle {
constructor(a: number) {
super(a, a, 'Square')
}
}</code></pre>
<p>3.实例统一处理不同的形状一起计算面积,讲解多态概念</p>
<pre><code>let shapes = Array<any>()
shapes.push(new Circle(2))
shapes.push(new Rectangle(5, 4))
shapes.push(new Square(3))
shapes.forEach((element) => {
console.log(`shape name: ${element.getName()}; shape area: ${element.area()}`)
})</code></pre>
<h3>接口概念阐述</h3>
<p>加入接口,规范形状对外部分操作要求,让错误提早到编译阶段被发现</p>
<pre><code>export default interface IShape {
getName(): string;
area(): number
}</code></pre>
<h3>函数式编程讲解</h3>
<p>用实例来说明怎样理解函数是一等公民,去掉我们习以为常的函数外层包裹</p>
<pre><code>let printData = function(err: any, data: string): void {
if (err)
console.log(err)
else
console.log(data)
}
let doAjax = function (data: string, callback: Function): void {
callback(null, data)
}
//我们习以为常的使用方式
doAjax('hello', function(err, result){
printData(null, result)
})
//真正理解了函数是一等公民后,你会这样用
doAjax('hello', printData)</code></pre>
<h3>异步处理中的经验分享</h3>
<p>在实践过程,处理异步调用容易误解的一个重要概念,异步函数执行的具体流程是什么样的?</p>
<pre><code>let pf = function(data: string, n: number, callback: Function) {
console.log(`begin run ${data}`)
setTimeout(() => {
console.log(`end run ${data}`)
callback(null, data)
}, n)
}
let p = Promise.promisify(pf);
(async () => {
let ps = Array<any>()
ps.push(p('1111', 2000))
ps.push(p('2222', 1000))
ps.push(p('3333', 3000))
await Promise.all(ps)
})()</code></pre>
<h2>视频课程地址</h2>
<p>以上是《运用typescript进行node.js后端开发精要》视频课程的概要,有兴趣的童鞋可以去观看视频。<br>传送门: <a href="https://segmentfault.com/l/1500000016954243?r=bPcCat">快来学习Typescript,加入会编程、能编程、乐编程的行列吧!</a></p>
<h2>资源地址</h2>
<p><a href="https://link.segmentfault.com/?enc=QxTe09xksSmQE848FHXhsg%3D%3D.myPy5xQ0KL6Z%2Fe%2FJCLim%2BWcN4NrxztQJuB4REIxs0RY%3D" rel="nofollow">https://github.com/zhoutk/sifou</a></p>
typescript模块导入与全局变量踩坑日志
https://segmentfault.com/a/1190000016485619
2018-09-21T16:18:25+08:00
2018-09-21T16:18:25+08:00
zhoutk
https://segmentfault.com/u/zhoutk
6
<h2>背景</h2>
<p>在调整typescript项目结构,全局变量尽量少用,但还是必不可少的,既要合理的引入,又要能用上vscode的智能提示。上篇日志已经记录了,在vscode中开发,全局变量的定义与声名是分开的,要做好对应。</p>
<h2>需求描述</h2>
<ul>
<li>全局自定义工具类,本团队自主开发的小工具,需要能有智能提示</li>
<li>加入常用的第三方工具到全局,也需要能有智能提示</li>
<li>用bluebird替换全局Promise</li>
</ul>
<h2>源代码目录结构</h2>
<ul>
<li>src/common/globUtils.ts,这是本团队自主开发小工具</li>
<li>src/inits/global.ts,这是全局变量的代码定义文件</li>
<li>src/globals.d.ts,这是全局变量声名文件</li>
</ul>
<pre><code>├── src
│ ├── app.ts
│ ├── common
│ │ └── globUtils.ts
│ ├── config
│ │ └── log4js.ts
│ ├── globals.d.ts
│ ├── index.ts
│ └── inits
│ ├── global.ts
│ └── tasks.ts
├── tsconfig.json
├── tslint.json</code></pre>
<h3>自定义工具模板</h3>
<pre><code>export default class GlobUtils {
isDev() {
return global.NODE_ENV !== 'prod'
}
}</code></pre>
<p>注意事项:</p>
<ul>
<li>导出时必须给出类名称, 不然vscode会提示出错。</li>
<li>其实这最好用静态方法,但vscode的智能提示会忽略掉静态方法,放弃。</li>
<li>在全局定义时,因为此处给出了类名,导入的是类,不是实例。</li>
</ul>
<h3>全局变量代码定义</h3>
<pre><code>import * as lodash from 'lodash'
import * as Bluebird from 'bluebird'
import GlobUtils from '../common/globUtils'
export default {
async init() {
Object.assign(global, {
ROOT_PATH: process.cwd(),
NODE_ENV: process.env.NODE_ENV || 'dev', //dev - 开发; prod - 生产; test - 测试;
Promise: Bluebird,
__: lodash,
globUtils: new GlobUtils(),
})
}
}</code></pre>
<p>注意事项:</p>
<ul>
<li>import引入形式必须为 import * as lodash from 'lodash',vscode建议的形式会出错</li>
<li>用Object.assign来将新属性加到global上</li>
<li>globUtils需要的是实例,注意几种导入形式,及默认导出时,类名给不给出,导入的结果是不同的,这里是类,需要new出实例来。</li>
</ul>
<h3>全局变量声名文件</h3>
<pre><code>import { Logger } from 'log4js'
import GlobUtils from './common/globUtils'
import * as lodash from 'lodash'
type LODASH = typeof lodash
declare global {
namespace NodeJS {
interface Global {
logger: Logger,
NODE_ENV: string,
ROOT_PATH: string,
globUtils: GlobUtils,
__: LODASH,
}
}
}</code></pre>
<p>注意事项:</p>
<ul>
<li>第一种导入方式,Logger引入类,直接声明为全局变量,import后已经实例化</li>
<li>第二种导入方式,lodash为命名空间,type LODASH声明为全局变量</li>
<li>第三种导入方式,GlobUtils为默认命名导出,import后为类,还未实例化</li>
</ul>
<h3>用bluebird替换全局的Promise</h3>
<p>用@types/bluebird-global替换@types/bluebird,即可完成替换。我们只需要在代码定义中增加它的定义就好了。<br>注意事项:</p>
<ul><li>Promise直接调用,其它全局变量在global下,虽然在实际环境中,两个都存在,智能检测只认一边,这也有利于我们编码时区分全局变量。</li></ul>
<h2>代码地址</h2>
<p>代码是这个项目的基础,此项目我准备将express+mysql的成功经验移植到koa2中来。</p>
<pre><code>https://github.com/zhoutk/gels</code></pre>
<h2>使用方法</h2>
<pre><code>git clone https://github.com/zhoutk/gels
cd gels
git checkout 9ea084f
yarn
tsc -w
用vscode打开项目,并按F5运行 </code></pre>
<h2>小结</h2>
<p>终于迈入typescript坑中,痛并快乐着!</p>
使用vscode写typescript(node.js环境)起手式
https://segmentfault.com/a/1190000016305647
2018-09-07T10:21:07+08:00
2018-09-07T10:21:07+08:00
zhoutk
https://segmentfault.com/u/zhoutk
15
<h2>动机</h2>
<p>一直想把typescript在服务端开发中用起来,主要原因有:</p>
<ul>
<li>javascript很灵活,但记忆力不好的话,的确会让你头疼,看着一月前自己写的代码,一脸茫然。</li>
<li>类型检查有利有敝,但在团队开发中,限制个人的天马行空还是很有效的;</li>
<li>node对模块等es6特性的支持不尽人意,目前我只用node长期支持版所能支持的特性,个人不愿用babel之类的工具;</li>
<li>开始用webstorm开发,结果它象visual studio系列一样,越来越臃肿;转而用vscode,但它的绝配是typescript;</li>
</ul>
<h2>问题</h2>
<p>之前也陆陆续续试着用了,但总被一些困难绊住了,主要有以下几点:</p>
<ul>
<li>vscode开发调试typescript环境的搭建,因为vscode版本更新也快,网上资料繁杂;</li>
<li>tsconfig.json的配置</li>
<li>tslint.json的配置</li>
<li>全局变量的使用与设定;</li>
</ul>
<h2>解决</h2>
<p>经过多次的不断尝试,今天终于达到自己满意的地步了。</p>
<h3>环境搭建,基于最新版(1.26.1)</h3>
<blockquote>安装node.js</blockquote>
<p>从官网下载对应操作系统的安装包,按说明安装。</p>
<blockquote>全局安装typescript</blockquote>
<pre><code> npm i -g typescript</code></pre>
<blockquote>生成并配置tsconfig.json</blockquote>
<p>运行命令:</p>
<pre><code> tsc --init</code></pre>
<p>配置文件:</p>
<pre><code>{
"compilerOptions": {
"target": "es2017", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"emitDecoratorMetadata": true, // 为装饰器提供元数据的支持
"experimentalDecorators": true, // 启用装饰器
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"alwaysStrict": true, // 以严格模式检查没个模块,并在没个文件里加入 'use strict'
"sourceMap": true,
"noEmit": false, // 不生成输出文件
"removeComments": true, // 删除编译后的所有的注释
"importHelpers": true, // 从 tslib 导入辅助工具函数
"strictNullChecks": true, // 启用严格的 null 检查
"lib": ["es2017"], // 指定要包含在编译中的库文件
"typeRoots": ["node_modules/@types"],
"types": [
"node",
],
"outDir": "./dist",
"rootDir": "./src"
},
"include": [ // 需要编译的ts文件一个*表示文件匹配**表示忽略文件的深度问题
"./src/*.ts",
"./src/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"public"
]
}</code></pre>
<blockquote>生成项目配置package.json</blockquote>
<p>运行命令:</p>
<pre><code> npm init</code></pre>
<p>配置文件:</p>
<pre><code> {
"name": "arest",
"version": "0.1.0",
"description": "a rest server use kao2, typescript & mongo db.",
"main": "app.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/zhoutk/arest.git"
},
"keywords": [
"rest",
"koa2",
"typescript",
"mongo"
],
"dependencies": {
"koa": "^2.5.2"
},
"author": "zhoutk@189.cn",
"license": "MIT",
"bugs": {
"url": "https://github.com/zhoutk/arest/issues"
},
"homepage": "https://github.com/zhoutk/arest#readme",
"devDependencies": {
"@types/koa": "^2.0.46",
"@types/node": "^10.9.4",
"tslint": "^5.11.0",
"typescript": "^3.0.3"
}
}</code></pre>
<blockquote>监测文件改动并编译</blockquote>
<p>运行命令:</p>
<pre><code> tsc -w</code></pre>
<blockquote>配置vscode项目启动launch.json</blockquote>
<p>配置好后,按F5即可开始调试运行程序</p>
<pre><code>{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"program": "${workspaceFolder}/dist/app.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}</code></pre>
<h3>tslint的配置与生效</h3>
<p>使用tslint可以定制团队共同使用的一些编码规则,这里需要注意的是,不但要全局安装typescript,tslint,还要在项目中安装,不然不能生效。这个鬼折腾了我好久!</p>
<pre><code>{
"rules": {
"class-name": true,
"comment-format": [
false,
"check-space"
],
"indent": [
true,
"spaces"
],
"no-duplicate-variable": true,
"no-eval": true,
"no-internal-module": true,
"no-trailing-whitespace": false,
"no-var-keyword": true,
"one-line": [
true,
"check-open-brace",
"check-whitespace"
],
"quotemark": [
true,
"single"
],
"semicolon": [true, "never"],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [
true,
"ban-keywords"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}</code></pre>
<h3>全局变量的使用</h3>
<p>全局变量虽然不能滥用,便也不能没有,以下几点是关键:</p>
<ul>
<li>tsconfig.json中加入 "types": [ "node"]</li>
<li>npm i @types/node</li>
<li>建立globals.d.ts(文件名可以随意取),在其中声名全局变量(这是让ide知道)</li>
<li>在入口文件(app.ts)中引入全局模块并赋值给node的全局变量global(这是让代码知道)</li>
</ul>
<h2>项目地址</h2>
<p>这个项目我准备将express+mysql的成功经验移植到koa2+mongo中来。</p>
<pre><code>https://github.com/zhoutk/arest</code></pre>
<h2>使用方法</h2>
<pre><code>git clone https://github.com/zhoutk/arest
cd arest
npm i
tsc -w
用vscode打开项目,并按F5运行 </code></pre>
<h2>小结</h2>
<p>终于迈入typescript坑中,痛并快乐着!</p>
golang实现rest server框架(二)
https://segmentfault.com/a/1190000015484267
2018-07-05T08:28:12+08:00
2018-07-05T08:28:12+08:00
zhoutk
https://segmentfault.com/u/zhoutk
4
<h2>第二篇:golang数据库增删改操作具体实现(mysql)</h2>
<h3>背景</h3>
<p>这篇文章是golang针对数据库增删改(非查询结果集,查询语句的自动生成比较复杂,下篇文章专门解析)操作具体实现,包括了自动生成sql与自定义sql相关函数,以及指的插入与更新,同时实现了异常处理。</p>
<h3>一些关键点</h3>
<ol>
<li>利用panic与recover实现数据库异常处理。</li>
<li>函数可变参数的解析。</li>
<li>批量插入与更新使用同一个函数。</li>
<li>所有更新sql语句参数化。</li>
</ol>
<h3>代码解析</h3>
<p>按功能模块对核心代码进行说明</p>
<h4>异常处理</h4>
<blockquote>golang语言没有异常处理,但可以通过panic、recover及defer来实现,值得注意的一点是,如何在defer中返回相应的信息给上层函数。</blockquote>
<pre><code> //rs要在这定义,defer中修改rs的信息才能返回到上层调用函数
func execute(sql string, values []interface{}) (rs map[string]interface{}) {
log.Println(sql, values)
rs = make(map[string]interface{}) //我原本rs是在这声明并定义的,结果返回为空
defer func() {
if r := recover(); r != nil {
rs["code"] = 500 //仔细想来,两个返回路径,一个是正常return,一个是声明中的rs返回值
rs["err"] = "Exception, " + r.(error).Error()
}
}()
...
//这其中的代码若引发了panic,在返回上层调用函数前会执行defer
...
return rs
}</code></pre>
<h4>非查询操作的底层封装函数(execute)</h4>
<blockquote>golang的数据库操作分返回查询结果集的和无查询结果集的,没找到能统一处理的API,象java、node.js一样,我只能分开封装了,这里实现execute。</blockquote>
<pre><code>func execute(sql string, values []interface{}) (rs map[string]interface{}) {
log.Println(sql, values)
...
//异常处理与数据配置文件读取
...
//连接数据库
dao, err := mysql.Open(dialect, dbUser+":"+dbPass+"@tcp("+dbHost+":"+dbPort+")/"+dbName+"?charset="+dbCharset)
stmt, _ := dao.Prepare(sql) //预处理
ers, err := stmt.Exec(values...) //提供参数并执行
if err != nil {
rs["code"] = 204 //错误处理
rs["err"] = err.Error()
} else {
id, _ := ers.LastInsertId() //自动增长ID的最新值,若插入
affect, _ := ers.RowsAffected() //影响的行数
rs["code"] = 200
rs["info"] = sql[0:6] + " operation success."
rs["LastInsertId"] = id
rs["RowsAffected"] = affect
}
return rs //成功返回
}</code></pre>
<blockquote>参数说明:</blockquote>
<ul>
<li>sql,调用这个函数时,要么已经自动生成了标准的sql,要么就自定义的sql,所有的语句要求是参数化形式的</li>
<li>values,参数列表,与sql中的占位符一一对应</li>
</ul>
<h4>新增函数的实现(Insert)</h4>
<blockquote>数据新增操作的具体实现,这个是根据用户提交的json数据自动生成标准sql的函数。</blockquote>
<pre><code>func Insert(tablename string, params map[string]interface{}) map[string]interface{} {
values := make([]interface{}, 0)
sql := "INSERT INTO `" + tablename + "` (" //+strings.Join(allFields, ",")+") VALUES ("
var ks []string
var vs []string
for k, v := range params { //注意:golang中对象的遍历,字段的排列是随机的
ks = append(ks, "`" + k + "`") //保存所有字段
vs = append(vs, "?") //提供相应的占位符
values = append(values, v) //对应保存相应的值
}
//生成正常的插入语句
sql += strings.Join(ks, ",") + ") VALUES (" + strings.Join(vs, ",") + ")"
return execute(sql, values)
}</code></pre>
<h4>修改函数的实现(Update)</h4>
<blockquote>数据修改操作的具体实现,这个是根据用户提交的json数据自动生成标准sql的函数。</blockquote>
<pre><code>func Update(tablename string, params map[string]interface{}, id string) map[string]interface{} {
values := make([]interface{}, 0)
sql := "UPDATE `" + tablename + "` set " //+strings.Join(allFields, ",")+") VALUES ("
var ks string
index := 0
psLen := len(params)
for k, v := range params { //遍历对象
index++
values = append(values, v) //参数
ks += "`" + k + "` = ?" //修改一个key的语句
if index < psLen { //非最后一个key,加逗号
ks += ","
}
}
values = append(values, id) //主键ID是单独的
sql += ks + " WHERE id = ? "
return execute(sql, values)
}</code></pre>
<h4>删除函数的实现(Delete)</h4>
<blockquote>数据删除操作的具体实现。</blockquote>
<pre><code>func Delete(tablename string, id string) map[string]interface{} {
sql := "DELETE FROM " + tablename + " where id = ? " //只支持单个ID操作,这是自动化的接口,批量操作走其它接口
values := make([]interface{}, 0)
values = append(values, id)
return execute(sql, values)
}</code></pre>
<h4>批量新增与修改函数的实现(InsertBatch)</h4>
<blockquote>数据批量新增与修改操作的具体实现,两种方式在同一接口中实现。</blockquote>
<pre><code>func InsertBatch(tablename string, els []map[string]interface{}) map[string]interface{} {
values := make([]interface{}, 0)
sql := "INSERT INTO " + tablename
var upStr string
var firstEl map[string]interface{} //第一个插入或修改的对象
lenEls := len(els) //因为golang对象遍历的随机性,我们取出第一个对象先分析,去除随机性
if lenEls > 0 {
firstEl = els[0]
}else { //一个元素都没有,显然调用参数不对
rs := make(map[string]interface{})
rs["code"] = 301
rs["err"] = "Params is wrong, element must not be empty."
return rs
}
var allKey []string //保存一个对象的所有字段,对象访问时就按这个顺序
eleHolder := "("
index := 0
psLen := len(firstEl)
for k, v := range firstEl {
index++
eleHolder += "?" //占位符
upStr += k + " = values (" + k + ")" //更新操作时的字段与值对应关系
if index < psLen { //非最后一个key
eleHolder += ","
upStr += ","
}else{
eleHolder += ")"
}
allKey = append(allKey, k) //key
values = append(values, v) //value
}
//批量操作的第一个对象语句的自动生成
sql += " ("+strings.Join(allKey, ",")+") values " + eleHolder
for i := 1; i < lenEls; i++ { //依据对第一个对象的分析,生成所有的后续对象
sql += "," + eleHolder
for _, key := range allKey {
values = append(values, els[i][key])
}
}
//当主键或唯一索引存在时,进行更新操作的sql语句生成
sql += " ON DUPLICATE KEY UPDATE " + upStr
return execute(sql, values)
}</code></pre>
<h4>bock.go(程序入口)</h4>
<blockquote>这里提供一些用于测试的代码。</blockquote>
<p>用于测试的数据表结构</p>
<pre><code>CREATE TABLE `books` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一性索引',
`name` varchar(64) DEFAULT '' COMMENT '名称',
`isbn` varchar(64) DEFAULT '' COMMENT '图书ISBN',
`u_id` int(11) DEFAULT '0' COMMENT '用户ID',
`status` tinyint(4) DEFAULT '1' COMMENT '状态:0-禁;1-有效;9删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uuid` (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='表';</code></pre>
<p>新增测试</p>
<pre><code> params := make(map[string] interface{})
args := make(map[string] interface{})
session := make(map[string] interface{})
session["userid"] = "112"
args["session"] = session
params["name"] = "golang实战"
params["isbn"] = "41563qtrs5-X"
params["status"] = 1
db := &table
rs := db.Create(params, args)
fmt.Println(rs)</code></pre>
<p>修改测试</p>
<pre><code> params = make(map[string] interface{})
args = make(map[string] interface{})
args["id"] = 2
params["name"] = "golang实战,修改了"
params["status"] = 3
rs = db.Update(params, args)
fmt.Println(rs)</code></pre>
<p>删除测试</p>
<pre><code> args = make(map[string] interface{})
args["id"] = 1
rs = db.Delete(nil, args)
fmt.Println(rs)</code></pre>
<p>批量测试</p>
<pre><code> vs := make([]map[string]interface{}, 0)
params := make(map[string] interface{})
params["name"] = "golang批量11213" //第一个对象
params["isbn"] = "4156s5"
params["status"] = 5
params["id"] = 9
vs = append(vs, params)
params = make(map[string] interface{})
params["name"] = "golang批量22af24" //第二个对象
params["isbn"] = "xxfqwt325rqrf45"
params["status"] = 2
params["id"] = 10
vs = append(vs, params)
db := &table
rs := db.InsertBatch("books", vs)
fmt.Println(rs)</code></pre>
<h3>项目地址</h3>
<pre><code>https://github.com/zhoutk/goTools</code></pre>
<h3>使用方法</h3>
<pre><code>git clone https://github.com/zhoutk/goTools
cd goTools
go get
go run bock.go
go buid bock.go
./bock </code></pre>
<h3>小结</h3>
<p>经过多种方案的对比,发现go语言作为网络服务的吞吐率是最棒的,所以有了将以往在其它平台上的经验(node.js,java,python3),用go来实现,期望有惊喜,写代码我是认真的。</p>
golang实现rest server框架(一)
https://segmentfault.com/a/1190000015467790
2018-07-03T19:26:09+08:00
2018-07-03T19:26:09+08:00
zhoutk
https://segmentfault.com/u/zhoutk
13
<h2>第一篇:用golang对数据库标准操作进行封装(mysql)</h2>
<h3>背景</h3>
<p>用golang对数据库标准操作进行封装,为后面的rest server提供数据库访问层。实现的目标是:能根据rest请求参数自动生成数据库操作语句,提供增、删、改、查、批量写入、事务等必要的数据库操作封装。并可以方便的扩展到多种数据库,让所有的数据库操作对于rest server来说表现为一致的访问接口。</p>
<h3>一些关键点</h3>
<ol>
<li>接口设计做到恰到好处,够用且不繁杂。</li>
<li>函数参数的设计,go不支持函数重载,如何善用interface{}。</li>
<li>用map[string]interface{}来处理rest的json请求参数,并自动生成相应的sql。</li>
<li>数据库查询结果能方便的转化为json,让rest server返回给用户。</li>
</ol>
<h3>代码解析</h3>
<p>按功能模块对核心代码进行说明</p>
<h4>IBock.go</h4>
<blockquote>数据库标准操作接口定义,根据我的实践经验,以下的接口设计已经能够很好的支持大部分的数据库操作,这些操作包括了根据json参数自动完成的CURD、手写sql支持、批量插入(更新)心及事务操作。</blockquote>
<pre><code>type IBock interface{
//根据参数,自动完成数据库查询
Retrieve(params map[string]interface{}, args ...interface{}) map[string]interface{}
//根据参数,自动完成数据库插入
Create(params map[string]interface{}, args ...interface{}) map[string]interface{}
//根据参数,自动完成数据库更新(只支持单条)
Update(params map[string]interface{}, args ...interface{}) map[string]interface{}
//根据参数,自动完成数据库删除(只支持单条)
Delete(params map[string]interface{}, args ...interface{}) map[string]interface{}
//手写查询sql支持
QuerySql(sql string, values []interface{}, params map[string]interface{}) map[string]interface{}
//手写非查询sql支持
ExecSql(sql string, values []interface{}) map[string]interface{}
//批量插入或更新
InsertBatch(tablename string, els []interface{}) map[string]interface{}
//事务支持
TransGo(objs map[string]interface{}) map[string]interface{}
}</code></pre>
<blockquote>参数说明</blockquote>
<ul>
<li>params, 对应rest server接收到用户数据,由json对象转换而来。</li>
<li>args,这个参数的目标是接收id(信息ID),fields(表字段数组),session(用户session)这三个参数,这样做的初衷是既要统一接口函数形式,又可以在编码时少传入作为点位符的nil</li>
<li>values,为sql查询参数化提供的参数列表</li>
<li>els,批量插入的每一行数据对象集</li>
<li>objs,事务对象集</li>
<li>返回参数为go的映射,很容易转化为json。</li>
</ul>
<h4>Bock.go</h4>
<blockquote>接口的具体实现,本文是对mysql的实现,暂只实现了基本的CURD,项目中会逐步完善。</blockquote>
<pre><code>//我们把操作对象定义在一个表上
type Bock struct {
Table string
}
//parseArgs函数的功能是解析args参数中包括的可变参数,实现在下面
func (b *Bock) Retrieve(params map[string]interface{}, args ...interface{}) map[string]interface{} {
//查询时我们一般只关注查询哪些表字段
_, fields, _ := parseArgs(args)
//调用具体的查询接口,查询接口将根据输入参数params自动实现sql查询语句,支持多样的查询定义,如:lks(从多个字体查询相同内容),ors(或查询),ins(in查询)等
return Query(b.Table, params, fields)
}
func (b *Bock) Create(params map[string]interface{}, args ...interface{}) map[string]interface{} {
//新建接口,一般都会关注用户在session的ID
_, _, session := parseArgs(args)
uId := session["userid"].(string)
params["u_id"] = uId
//调用具体的插入接口
return Insert(b.Table, params)
}
func (b *Bock) Update(params map[string]interface{}, args ...interface{}) map[string]interface{} {
//只支持单个更新,所以ID必须存在
id, _, _ := parseArgs(args)
if len(id) == 0 {
rs := make(map[string]interface{})
rs["code"] = 301
rs["err"] = "Id must be input."
return rs
}
return Update(b.Table, params)
}
func (b *Bock) Delete(params map[string]interface{}, args ...interface{}) map[string]interface{} {
//只支持单个删除,所以ID必须存在
id, _, _ := parseArgs(args)
if len(id) == 0 {
rs := make(map[string]interface{})
rs["code"] = 301
rs["err"] = "Id must be input."
return rs
}
return Delete(b.Table, params)
}</code></pre>
<blockquote>parseArgs函数的实现</blockquote>
<pre><code>func parseArgs(args []interface{}) (string, []string, map[string]interface{}) {
//解析指定的参数
var id string //信息ID
var fields []string //查询字段集
var session map[string]interface{} //用户session对象
for _, vs := range args {
switch vs.(type) {
case map[string]interface{}: //只接收指定类型
for k, v := range vs.(map[string]interface{}) {
if k == "id" {
id = v.(string)
}
if k == "fields" {
fields = v.([]string)
}
if k == "session" {
session = v.(map[string]interface{})
}
}
default:
}
}
return id, fields, session //返回解析成功的参数
}</code></pre>
<h4>Helper.go</h4>
<blockquote>数据操作的具体实现,大多是伪代码,项目后续会逐步完善,查询接口最重要,后面会有单独文章进行解析</blockquote>
<pre><code>func Query(tablename string, params map[string]interface{}, fields []string ) map[string]interface{} {
//调用具体实现的私用函数,接口中分自动和手动两个函数,在私用函数中屏蔽差异内聚功能
return query(tablename, params, fields, "", nil)
}
func Insert(tablename string, params map[string]interface{}) map[string]interface{} {
sql := "Insert into " + tablename
values := make([]interface{},0)
return execute(sql, values)
}
func Update(tablename string, params map[string]interface{}) map[string]interface{} {
sql := "Update " + tablename + " set "
values := make([]interface{},0)
return execute(sql, values)
}
func Delete(tablename string, params map[string]interface{}) map[string]interface{} {
sql := "Delete from " + tablename + " where"
values := make([]interface{},0)
return execute(sql, values)
}</code></pre>
<blockquote>私用查询函数定义</blockquote>
<pre><code>//五个输入参数,分别适配自动与手动查询
func query(tablename string, params map[string]interface{}, fields []string, sql string, vaules []interface{}) map[string]interface{} {
if vaules == nil {
vaules = make([]interface{},0)
}
//调用真正的数据库操作函数
return execQeury("select "+ strings.Join(fields, ",")+" from " + tablename, vaules)
}</code></pre>
<blockquote>非查询类具体操作函数</blockquote>
<pre><code>//因为golang把有结果集的和无结果集的操作是分开的,不象在java或node.js中,可以有高级函数进行统一操作,只能分开。
func execute(sql string, values []interface{}) map[string]interface{} {
//返回json对象,以map形式表达
rs := make(map[string]interface{})
rs["code"] = 200
return rs
}</code></pre>
<blockquote>查询类具体操作(已经实现),结果集以json对象封装,存储在map中</blockquote>
<pre><code>func execQeury(sql string, values []interface{}) map[string]interface{} {
var configs interface{}
...//省略数据配置获取代码,请参照以前的文章
dao, err := mysql.Open(dialect, dbUser + ":"+dbPass+"@tcp("+dbHost+":"+dbPort+")/"+dbName+"?charset="+dbCharset)
stmt, err := dao.Prepare(sql)
rows, err := stmt.Query(values...)
columns, err := rows.Columns() //取出字段名称
vs := make([]mysql.RawBytes, len(columns))
scans := make([]interface{}, len(columns))
for i := range vs { //预设取值地址
scans[i] = &vs[i]
}
var result []map[string]interface{}
for rows.Next() {
_ = rows.Scan(scans...) //塡入一列值
each := make(map[string]interface{})
for i, col := range vs {
if col != nil {
each[columns[i]] = string(col) //增值
}else{
each[columns[i]] = nil
}
}
result = append(result, each)
}
rs["code"] = 200
//data, _ := json.Marshal(result) //这样就能转换为json
rs["rows"] = result
return rs
}</code></pre>
<blockquote>数据库的批量操作,在前面的文章中已经用golang实现,只是还未封装,有兴趣的朋友可以看我前面的文章。</blockquote>
<h4>bock.go(程序入口)</h4>
<blockquote>最终目标的入口将是一个网络服务,提供标准的restful服务,现在只是用来测试,再这说明一下愿景。</blockquote>
<pre><code> table := Bock.Bock{ //上体实例
Table: "role", //对role表时行操作
}
var params map[string] interface{} //模拟json参数
args := make(map[string] interface{}) //其它参数
db := make([]DB.IBock, 1) //对接口编程
db[0] = &table //接口指向实例对象,这里可以现时处理多个不同的实例
fields := []string {"id", "name"}
args["fields"] = fields
rs, _ := db[0].Retrieve(params, args) //在这可以循环处理多个不同的实例,我们最终的目标就是在这接受用户的http请求,由路由自动分发不同的请求,我们的数据库封装自动生成sql语句完成用户的基本需求。
fmt.Println(rs)</code></pre>
<h3>项目地址</h3>
<pre><code>https://github.com/zhoutk/goTools</code></pre>
<h3>使用方法</h3>
<pre><code>git clone https://github.com/zhoutk/goTools
cd goTools
go get
go run bock.go
go buid bock.go
./bock </code></pre>
<h3>小结</h3>
<p>经过多种方案的对比,发现go语言作为网络服务的吞吐率是最棒的,所以有了将以往在其它平台上的经验(node.js,java,python3),用go来实现,期望有惊喜,写代码我是认真的。</p>
golang实现抓取IP地址的蜘蛛程序
https://segmentfault.com/a/1190000015418170
2018-06-28T16:55:17+08:00
2018-06-28T16:55:17+08:00
zhoutk
https://segmentfault.com/u/zhoutk
13
<h2>背景</h2>
<p>要做IP地址归属地查询,量比较大,所以想先从网上找到大部分的分配数据,写个蜘蛛程序来抓取入库,以后在程序的运行中不断进行维护、更新、完善。</p>
<h2>一些关键点</h2>
<ol>
<li>goroutine的使用,让程序并行运行。</li>
<li>正则表达式分组信息提取的使用,正确的提取我们关注的信息。</li>
<li>数据库批量插入操作。</li>
<li>数据库批量更新操作。</li>
</ol>
<h2>代码解析</h2>
<p>按功能模块对核心代码进行说明</p>
<h3>ip.go</h3>
<blockquote>主进程,实现goroutine的调用。</blockquote>
<pre><code>func main() {
//利用go基本库封装的网页抓取函数,后面有说明
ctx := common.HttpGet("http://ips.chacuo.net/")
//正则表达式,有两个分组(两组小括号),分别取城市信息与url,具体分析代码后面有说明
reg := regexp.MustCompile(`<li><a title="[\S]+" href='([^']+?)'>([^<]+?)</a></li>`)
//取得页面上所有的城市及相应url
ips := reg.FindAllStringSubmatch(string(ctx), -1)
ch := make(chan string) //建立无缓冲字符串通道
for _, el := range ips { //一个协程处理一个具体页面
go ipSpider.SpiderOnPage(el[1], el[2], ch)
}
for range ips { //阻塞等待所有抓取工作全部完成
fmt.Println(<-ch)
}
}</code></pre>
<blockquote>正则表达式说明</blockquote>
<ul><li>主进程针对所有省有入口页面,取得每省的入口分配给一个协程去处理,每一个入口是这个样子</li></ul>
<pre><code><a title="北京最新IP地址段" href="http://ips.chacuo.net/view/s_BJ">北京</a></code></pre>
<ul>
<li>请注意,这里面变化只有三个部分(title内容,href内容,链接显示内容),其中两个部分是我们需要的</li>
<li>title内容对应正则为 <code>[\S]+</code> ,非空白符</li>
<li>href内容对应的正则为 <code>([^']+?)</code> ,第一次遇到单引号结束,问号表示非贪婪匹配,括号是分组,能方便取出所匹配信息</li>
<li>链接显示内容对应的正则为 <code>([^<]+?)</code> , 第一次遇到<时结束,第二个分组</li>
<li>FindAllStringSubmatch函数可以取出所有子分组,子分组从下标1开始,0为正则整体匹配的字符串</li>
</ul>
<blockquote>goroutine 流程</blockquote>
<ul>
<li>建立一个无缓冲字符串通道,作为所有协程与主进程通信通道</li>
<li>循环正则匹配结果,为每一个省的页面分配一个协程</li>
<li>协程获取数据成功并批量写数据库,返回成功信息到通道</li>
<li>协程处理失败,反回失败信息到通道</li>
<li>主进程阻塞等所有协程成功或失败返回,并打印成功或失败信息</li>
</ul>
<h3>获取ip地址信息</h3>
<blockquote>与主进程类似,注意无信息时处理。</blockquote>
<p>IpSpider.go</p>
<pre><code> //获取页面数据
ctx := common.HttpGet(url)
//reg := regexp.MustCompile(`<li><a title="[\S]+" href='([^']+?)'>([^<]+?)</a></li>`)
//两个分组分别对应IP段开始与结束
reg := regexp.MustCompile(`<dd><span class="v_l">([^<]+?)</span><span class="v_r">([^<]+?)</span><div class="clearfix"></div></dd>`)
//<dd><span class="v_l">49.64.0.0</span><span class="v_r">49.95.255.255</span><div class="clearfix"></div></dd>
//取得所有匹配的分组信息
ip := reg.FindAllStringSubmatch(string(ctx), -1)
//没有取得任何信息,提前返回,很重要,不然主进程会一直等待结束不了
if len(ip) == 0 {
ch <- "There are no data exist."
return nil
}</code></pre>
<h3>数据库表结构生成语句</h3>
<pre><code>CREATE TABLE `ip_addr_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '索引,自动增长',
`ip_addr_begin` varchar(32) NOT NULL DEFAULT '' COMMENT 'ip地址段开始',
`ip_addr_end` varchar(32) DEFAULT '' COMMENT 'ip地址段结束',
`province` varchar(32) DEFAULT '' COMMENT '所属省',
`ip_comp` varchar(32) DEFAULT '' COMMENT '运营商',
PRIMARY KEY (`id`),
UNIQUE KEY `ip_addr` (`ip_addr_begin`,`ip_addr_end`)
) ENGINE=InnoDB AUTO_INCREMENT=7268 DEFAULT CHARSET=utf8 COMMENT='表';
</code></pre>
<h3>批量写入数据库</h3>
<blockquote>循环处理抓取数据,生成批量写入语句及输入参数,请签到出到afc9ebd版本。</blockquote>
<pre><code> var vs [] interface{} //存储输入参数的接口数组
var vss string //待拼接的输入参数占位符字符串
for _, el := range ip { //处理所有的数据
vs = append(vs, el[1], el[2], province) //每一列包括开始地址、结束地址与省份
vss += "(?,?,?)," //占位符
}
vss = vss[0:len(vss) -1] //去掉最后的逗号
var configs interface{} //从配置文件取数据库信息
fr, err := os.Open("./configs.json") //配置文件内容请参照上篇文章《golang实现mysql数据库备份》
if err != nil {
ch <- err.Error()
return err
}
decoder := json.NewDecoder(fr)
err = decoder.Decode(&configs)
confs := configs.(map[string]interface{})
dialect := confs["database_dialect"].(string)
dbConf := confs["db_"+dialect+"_config"].(map[string]interface{})
dbHost := dbConf["db_host"].(string)
dbPort := strconv.FormatFloat(dbConf["db_port"].(float64), 'f', -1, 32)
dbUser := dbConf["db_user"].(string)
dbPass := dbConf["db_pass"].(string)
dbName := dbConf["db_name"].(string)
dbCharset := dbConf["db_charset"].(string)
dao, err := mysql.Open(dialect, dbUser + ":"+dbPass+"@tcp("+dbHost+":"+dbPort+")/"+dbName+"?charset="+dbCharset)
defer dao.Close()
if err != nil {
ch <- err.Error()
return err
}
//批量插入语句拼接
sqlstr := "insert into ip_addr_info (ip_addr_begin,ip_addr_end,province) values " + vss
stmt, err := dao.Prepare(sqlstr) //预处理带参数的sql语句
rs, err := stmt.Exec(vs...) //带参数执行sql语句
if err != nil { //出错,返回错误信息
ch <- err.Error()
return err
}else { //成功,返回成功信息
affect, _ := rs.RowsAffected()
ch <- "Province: " + province + ", affect: " + strconv.FormatInt(affect, 10)
return nil
}
</code></pre>
<h3>批量修改数据库</h3>
<blockquote>数据库中的ip_comp字段,是代表运营商信息,需要从运营商页面进入进行数据获取,只需改一下入口url重新运行程序就能正确抓取,但这时入库就不是新增了,而是更新,请签出到4729e66版本。</blockquote>
<pre><code> //前提数据库表定义要设定唯一索引,主键或其它定义的unique索引
...
sqlstr := "insert into ip_addr_info (ip_addr_begin,ip_addr_end,ip_comp) values " + vss +
//提供更新(唯一索引冲突时)时要对应原字段与值
" ON DUPLICATE KEY UPDATE ip_addr_begin = values(ip_addr_begin), ip_addr_end = values(ip_addr_end), ip_comp = values(ip_comp)"
stmt, err := dao.Prepare(sqlstr)
rs, err := stmt.Exec(vs...)
if err != nil {
ch <- err.Error()
return err
}else {
affect, _ := rs.RowsAffected()
ch <- "Province: " + province + ", affect: " + strconv.FormatInt(affect, 10)
return nil
}</code></pre>
<h3>待改进的方面</h3>
<p>把入口url提到配置中,使用策略模式,让匹配规则抽象成策略,目标是不改程序,调整配置文件就可以抓取不同的网页。</p>
<h2>项目地址</h2>
<pre><code>https://github.com/zhoutk/goTools</code></pre>
<h2>使用方法</h2>
<pre><code>git clone https://github.com/zhoutk/goTools
cd goTools
go get
go run ip.go
go buid ip.go
./ip </code></pre>
<h2>小结</h2>
<p>熟悉了golang语言,了解了一种全新的并发编程模式,熟悉了具体的数据库操作方法,给自己生成了一个方便的工具。</p>
golang实现mysql数据库备份
https://segmentfault.com/a/1190000015382281
2018-06-25T17:13:31+08:00
2018-06-25T17:13:31+08:00
zhoutk
https://segmentfault.com/u/zhoutk
18
<h2>背景</h2>
<p>navicat是mysql可视化工具中最棒的,但是,在处理视图的导入导出方面,它是按照视图名称的字母顺序来处理的,若视图存在依赖,在导入过程中就会报错。前面已经用python写了一个,但在使用过程中,遇到xfffd编码,python的pymysql会直接崩溃。发现golang没有这个问题,正好用go重写,来熟悉golang。</p>
<h2>一些关键点</h2>
<ol>
<li>map & json,在处理主键与外键信息时,需要用到json数据结构来存储中间结果,因为要灵活处理,在golang中只能用map[string]interface{}来处理。</li>
<li>interface{} 相当于java中的object,能接受任意数据类型,方便但在使用时要做到心中有数,不然一旦数据类型不匹配,程序就会崩溃。</li>
<li>xfffd ,utf8中的占位符,超出范围的utf8mb4入库后,会被存储为xfffd,数据导出时,需要过滤掉。</li>
<li>goroutine, golang的并发支持很独特,我们的工具支持多个库同时备份,很容易使用goroutine来实现并行。</li>
</ol>
<h2>代码解析</h2>
<p>按功能模块对核心代码进行说明</p>
<h3>main.go,并发、命令行参数</h3>
<p>使用命令行参数,接受一个参数,来指定备份的内容<br>package common</p>
<pre><code>type OpFlag struct {
Tables bool //表结构
Datum bool //表结构和数据
Views bool //视图
Funcs bool //函数与存储过程
}</code></pre>
<p>main.go,程序入口,处理命令行参数</p>
<pre><code> if len(os.Args) > 1 {
flag = common.OpFlag{
Tables: false,
Datum: false,
Views: false,
Funcs: false,
}
switch os.Args[1] { //接受一个参数
case "table":
flag.Tables = true //根据参数设定标识量
case "data":
flag.Tables = true
flag.Datum = true
case "views":
flag.Views = true
case "funcs":
flag.Funcs = true
default: //参数不正确,报错退出
log.Fatal("You arg must be in : table, data, views or funcs.")
}
}else{ //无参数,默认导出所有
flag = common.OpFlag{
Tables: true,
Datum: true,
Views: true,
Funcs: true,
}
}
err := backUp.Export(flag) 根据参数进行数据库备份</code></pre>
<h3>Export.go</h3>
<p>备份主流程,根据configs.json生成goroutine来备份数据库,并等待完成。</p>
<pre><code>var configs interface{}
fr, err := os.Open("./configs.json")
if err != nil {
return err
}
decoder := json.NewDecoder(fr) //解析配置文件
err = decoder.Decode(&configs)
confs := configs.(map[string]interface{})
workDir := confs["workDir"].(string)
ch := make(chan string) //通道变量
for key, value := range confs {
if strings.HasPrefix(key, "db_") {
dbConf := value.(map[string]interface{})
dbConn := common.DbConnFields{ //数据库相应配置
DbHost: dbConf["db_host"].(string),
DbPort: int(dbConf["db_port"].(float64)),
DbUser: dbConf["db_user"].(string),
DbPass: dbConf["db_pass"].(string),
DbName: dbConf["db_name"].(string),
DbCharset: dbConf["db_charset"].(string),
}
if dbConf["file_alias"] != nil { //生成sql备份文件的命名
dbConn.FileAlias = dbConf["file_alias"].(string)
}
go ExportOne(dbConn, workDir, ch, flag) //创建协程
}
}
for key := range confs { //阻塞主进程,待所有协程完成工作
if strings.HasPrefix(key, "db_") {
fmt.Print( <- ch )
}
}
return nil</code></pre>
<p>你需要编写如下的配置文件来描述你要备份的数据库:</p>
<pre><code>{
"db_name1": {
"db_host": "192.168.1.8",
"db_port": 3306,
"db_user": "root",
"db_pass": "123456",
"db_name": "name1",
"db_charset": "utf8mb4",
"file_alias": "file name1"
},
"db_name2": {
"db_host": "localhost",
"db_port": 3306,
"db_user": "root",
"db_pass": "123456",
"db_name": "name2",
"db_charset": "utf8mb4"
},
"database_dialect": "mysql",
"workDir": "/home/zhoutk/gocodes/goTools/"
}</code></pre>
<h3>ExportOne.go</h3>
<p>备份一个数据库</p>
<pre><code> fileName := fields.FileAlias
setSqlHeader(fields, fileName) //设置导出文件说明
if flag.Tables { //如果表设置为真,导出表结构
err := exportTables(fileName, fields, flag) //具体算法请参照源代码
if err != nil {
ch <- fmt.Sprintln("Error: ", fields.DbName, "\t export tables throw, \t", err)
return
}
}
if flag.Views { //如果视图设置为真,导出视图
err := exportViews(fileName, fields)//具体算法请参照源代码,或python算法
if err != nil {
ch <- fmt.Sprintln("Error: ", fields.DbName, "\t export views throw, \t", err)
return
}
}
if flag.Funcs { //如果函数设置为真,导出函数和存储过程
err := exportFuncs(fileName, fields)//具体算法请参照源代码
if err != nil {
ch <- fmt.Sprintln("Error: ", fields.DbName, "\t export funcs throw, \t", err)
return
}
}
//导出工作完成,向通道输入信息
ch <- fmt.Sprintln("Export ", fields.DbName, "\t success at \t", time.Now().Format("2006-01-02 15:04:05")) </code></pre>
<h3>MysqlDao.go</h3>
<p>数据库查询通用封装,此工具只使用了ExecuteWithDbConn。灵活的使用map与interface{},将结果转化为键值对象返回。</p>
<pre><code>func ExecuteWithDbConn(sql string, values []interface{}, fields common.DbConnFields) (map[string]interface{}, error) {
rs := make(map[string]interface{})
dao, err := mysql.Open("mysql", fields.DbUser + ":"+fields.DbPass+"@tcp("+fields.DbHost+":"+
strconv.Itoa(fields.DbPort)+")/"+fields.DbName+"?charset="+fields.DbCharset)
defer dao.Close()
if err != nil {
rs["code"] = 204
return rs, err
}
stmt, err := dao.Prepare(sql)
if err != nil {
rs["code"] = 204
return rs, err
}
rows, err := stmt.Query(values...)
if err != nil {
rs["code"] = 204
return rs, err
}
columns, err := rows.Columns() //取出字段名称
vs := make([]mysql.RawBytes, len(columns))
scans := make([]interface{}, len(columns))
for i := range vs { //预设取值地址
scans[i] = &vs[i]
}
var result []map[string]interface{}
for rows.Next() {
_ = rows.Scan(scans...) //塡入一列值
each := make(map[string]interface{})
for i, col := range vs {
if col != nil {
each[columns[i]] = FilterHolder(string(col)) //过滤/xfffd
}else{
each[columns[i]] = nil
}
}
result = append(result, each)
}
rs["code"] = 200
//data, _ := json.Marshal(result)
rs["rows"] = result
return rs, err
}</code></pre>
<h2>项目地址</h2>
<pre><code>https://github.com/zhoutk/goTools</code></pre>
<h2>使用方法</h2>
<pre><code>git clone https://github.com/zhoutk/goTools
cd goTools
go get
go run main.go
go buid main.go
./main #export all things of database
./main table #export tables
./main data #export tables & data
./main views #export views
./main funcs #export funcs & stored procedures</code></pre>
<h2>小结</h2>
<p>熟悉了golang语言,了解了一种全新的并发编程模式,给自己生成了一个方便的工具。</p>
ubuntu下安装java10
https://segmentfault.com/a/1190000014930359
2018-05-19T08:26:39+08:00
2018-05-19T08:26:39+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<p>最近需要用java,已经使用ubuntu和node.js多年,一装才知道java9已经不被支持了,度娘搜不到想要的信息,最后在添加java8的ppa的时候,仔细看了一下信息,给出了java10的相关信息,记录如下:</p>
<h3>安装java10信息页面</h3>
<pre><code>https://www.linuxuprising.com/2018/04/install-oracle-java-10-in-ubuntu-or.html</code></pre>
<h3>安装步骤</h3>
<ol>
<li>
<p>add the LinuxUprising Java PPA repository</p>
<pre><code>sudo add-apt-repository ppa:linuxuprising/java
sudo apt update
sudo apt install oracle-java10-installer</code></pre>
</li>
<li>
<p>make Oracle JDK 10 as default</p>
<pre><code>sudo apt install oracle-java10-set-default</code></pre>
</li>
<li>
<p>Oracle Java 10 installed but not set it as the default Java</p>
<pre><code>sudo apt remove oracle-java10-set-default</code></pre>
</li>
<li>
<p>checking your current Java version</p>
<pre><code>java -version
javac -version</code></pre>
</li>
</ol>
用python解决mysql视图导入导出依赖问题
https://segmentfault.com/a/1190000012434905
2017-12-15T08:42:07+08:00
2017-12-15T08:42:07+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<blockquote>navicat是mysql可视化工具中最棒的,但是,在处理视图的导入导出方面,它是按照视图名称的字母顺序来处理的,若视图存在依赖,在导入过程中就会报错。这个问题一直困绕我,一度因为我使用docker来部署mysql而绕过了这个问题。最近不得不直面这个问题,因此,写了一个小工具来解决它。</blockquote>
<h2>整体思路</h2>
<p>在mysql很容易查出所有视图和其定义,因此可以写一个视图导出工具,存储时对各视图的保存位置进行调整,处理好它们之间的依赖关系,被依赖的放前面,这样就解决了导入时的依赖问题。</p>
<h2>获取视图信息</h2>
<p>运行以下查询语句,就能获得该数据库中所有视图的信息。</p>
<pre><code>select * from information_schema.VIEWS where TABLE_SCHEMA = DatabaseName </code></pre>
<p>查询结果字段说明:</p>
<ul>
<li>TABLE_NAME : 数所库中视图名称</li>
<li>VIEW_DEFINITION : 视图的定义代码,只有查询语句部分</li>
<li>DEFINER : 视图定义(建立)者名称</li>
<li>SECURITY : 安全级别</li>
</ul>
<p>总之,所有视图的信息都在这个表中保存,我要完成任务,只需要TABLE_NAME和VIEW_DEFINITION就可以了。</p>
<h2>算法描述</h2>
<ul>
<li>将查询结果放到dict中,视图名称为key;视图定义为value;</li>
<li>
<p>编写处理依赖关系的函数process_rely,输入参数中的rely_old为保存所有视图名称的数组;返回参数为按依赖关系调整顺序后的视图名称数组。之所以这样做,是一开始考虑到,依赖关系复杂时,可能一次迭代处理不好,需要递归调用或多次调用。</p>
<blockquote>process_rely函数算法描述:</blockquote>
<ul>
<li>
<p>第一层循环,从rely_old中取一个视图名称</p>
<ul>
<li>
<p>第二层循环,从dict中取出一个键值</p>
<ul><li>
<p>若键值被第一层元素的定义所依赖</p>
<ul>
<li>
<p>若键值还不在结果数组中</p>
<ul>
<li>
<p>若第一层元素不在结果数组中</p>
<ul><li>追加键值到结果数组中</li></ul>
</li>
<li>
<p>第一层元素在结果数组中</p>
<ul><li>将键值插入到第一层元素前</li></ul>
</li>
</ul>
</li>
<li>
<p>键值在结果数组中</p>
<ul><li>
<p>第一层元素在结果数组中</p>
<ul>
<li>查找各自在结果数组中的位置</li>
<li>
<p>若第一层元素在键值的后</p>
<ul><li>将键值移动到第一层元素前</li></ul>
</li>
</ul>
</li></ul>
</li>
</ul>
</li></ul>
</li>
<li>
<p>第二层循环结束时,若第一层元素还不在结果集中</p>
<ul><li>将第一层元素追加到结果集中</li></ul>
</li>
</ul>
</li>
<li><p>返回结果集</p></li>
</ul>
</li>
</ul>
<p>上面的说明,是按python代码模式给出的。很幸运,算法一次就能将复杂的依赖关系处理好了。我在编写的过程中,刚开始依赖算法不完善时,通过多次迭代也能处理好复杂的依赖关系。因此,坚定了必胜的信心,完成了这个任务。</p>
<h2>完整代码</h2>
<pre><code>import pymysql
conn = pymysql.connect(host='172.17.0.1', port=3306, user='root',
passwd='123456', db='database', charset='utf8mb4')
def process_rely(parmas={}, rely_old=[]):
_rely = []
_keys = list(parmas.keys())
for k in rely_old:
for bl in _keys:
if str(parmas[k]).find(bl) > -1:
if bl not in _rely:
if k not in _rely:
_rely.append(bl)
else:
i = _rely.index(k)
_rely.insert(i, bl)
else:
if k in _rely:
i = _rely.index(k)
j = _rely.index(bl)
if i < j:
del _rely[j]
_rely.insert(i, bl)
if k not in _rely:
_rely.append(k)
return _rely
cur = conn.cursor()
cur.execute('select TABLE_NAME, VIEW_DEFINITION from information_schema.VIEWS where TABLE_SCHEMA = %s ', 'database')
rs = cur.fetchall()
cur.close()
conn.close()
ps = {}
for al in rs:
ps['`' + al[0] + '`'] = al[1]
rely = process_rely(ps, list(ps.keys()))
# rely = process_rely(ps, rely1)
file_object = open('view.sql', 'w')
for al in rely:
file_object.write('DROP VIEW IF EXISTS ' + al + ';\n')
file_object.write('CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW ' + al +
' AS ' + ps[al] + ';\n\n')
file_object.close()</code></pre>
<h2>小结</h2>
<p>思路要清晰,代码要一步步的向最终目标靠近,积跬步以至千里。在做这个工具时,一开始觉得很麻烦,依赖关系若是深层次的,可能一次处理不好,正因为采用的迭代的思想,最后才完成了一次迭代解决问题的完美结局。</p>
用python写通用restful api service(二)
https://segmentfault.com/a/1190000012353350
2017-12-09T11:41:17+08:00
2017-12-09T11:41:17+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<blockquote><p>今天项目已经能够做一个简单的后端服务了,在mysql中新建一个表,就能自动提供restful api的CURD服务了。</p></blockquote>
<h2>关键点</h2>
<ul>
<li>根据REST的四种动词形式,动态调用相应的CURD方法;</li>
<li>编写REST与基础数据库访问类之间的中间层(baseDao),实现从REST到数据访问接口之间能用业务逻辑处理;</li>
<li>编写基础数据库访问类(dehelper),实现从字典形式的参数向SQL语句的转换;</li>
</ul>
<h2>实现的rest-api</h2>
<p>实现了如下形式的rest-api</p>
<pre><code>[GET]/rs/users/{id}
[GET]/rs/users/key1/value1/key2/value2/.../keyn/valuen
[POST]/rs/users
[PUT]/rs/users/{id}
[DELETE]/rs/users/{id}</code></pre>
<h2>基础数据库访问类</h2>
<p>该类实现与pymysql库的对接,提供标准CURD接口。</p>
<h3>准备数据库表</h3>
<p>在数据库对应建立users表,脚本如下:</p>
<pre><code>CREATE TABLE `users` (
`_id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '标题名称',
`phone` varchar(1024) DEFAULT '',
`address` varchar(1024) DEFAULT NULL,
`status` tinyint(4) DEFAULT '1' COMMENT '状态:0-禁;1-有效;9删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`_id`),
UNIQUE KEY `uuid` (`_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='表';</code></pre>
<h3>新建数据库配置文件(configs.json)</h3>
<p>数据连接配置,不入版本库。</p>
<pre><code>{
"db_config": {
"db_host": "ip",
"db_port": 1234,
"db_username": "root",
"db_password": "******",
"db_database": "name",
"db_charset": "utf8mb4"
}
}</code></pre>
<h3>对接pymysql接口</h3>
<p>用函数exec_sql封装pymysql,提供统一访问mysql的接口。is_query函数用来区分是查询(R)还是执行(CUD)操作。出错处理折腾了好久,插入异常返回的错误形式与其它的竟然不一样!返回参数是一个三元组(执行是否成功,查询结果或错误对象,查询结果数或受影响的行数)</p>
<pre><code>with open("./configs.json", 'r', encoding='utf-8') as json_file:
dbconf = json.load(json_file)['db_config']
def exec_sql(sql, values, is_query=False):
try:
flag = False #是否有异常
error = {} #若异常,保存错误信息
conn = pymysql.connect(host=dbconf['db_host'], port=dbconf['db_port'], user=dbconf['db_username'],
passwd=dbconf['db_password'], db=dbconf['db_database'], charset=dbconf['db_charset'])
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
num = cursor.execute(sql, values) #查询结果集数量或执行影响行数
if is_query: #查询取所有结果
result = cursor.fetchall()
else: #执行提交
conn.commit()
print('Sql: ', sql, ' Values: ', values)
except Exception as err:
flag = True
error = err
print('Error: ', err)
finally:
conn.close()
if flag:
return False, error, num if 'num' in dir() else 0
return True, result if 'result' in dir() else '', num</code></pre>
<h3>查询接口</h3>
<p>pymysql的查询接口,可以接受数组,元组和字典,本查询接口使用数组形式来调用。现在此接口只支持与条件组合参数。</p>
<pre><code>def select(tablename, params={}, fields=[]):
sql = "select %s from %s " % ('*' if len(fields) == 0 else ','.join(fields), tablename)
ks = params.keys()
where = ""
ps = []
pvs = []
if len(ks) > 0: #存在查询条件时,以与方式组合
for al in ks:
ps.append(al + " =%s ")
pvs.append(params[al])
where += ' where ' + ' and '.join(ps)
rs = exec_sql(sql+where, pvs, True)
print('Result: ', rs)
if rs[0]:
return {"code": 200, "rows": rs[1], "total": rs[2]}
else:
return {"code": rs[1].args[0], "error": rs[1].args[1], "total": rs[2]}</code></pre>
<h3>插入接口</h3>
<p>以数组形式提供参数,错误信息解析与其它接口不同。</p>
<pre><code>def insert(tablename, params={}):
sql = "insert into %s " % tablename
ks = params.keys()
sql += "(`" + "`,`".join(ks) + "`)" #字段组合
vs = list(params.values()) #值组合,由元组转换为数组
sql += " values (%s)" % ','.join(['%s']*len(vs)) #配置相应的占位符
rs = exec_sql(sql, vs)
if rs[0]:
return {"code": 200, "info": "create success.", "total": rs[2]}
else:
return {"code": 204, "error": rs[1].args[0], "total": rs[2]}</code></pre>
<h3>修改接口</h3>
<p>以字典形式提供参数,占位符的形式为:%(keyname)s,只支持按主键进行修改。</p>
<pre><code>def update(tablename, params={}):
sql = "update %s set " % tablename
ks = params.keys()
for al in ks: #字段与占位符拼接
sql += "`" + al + "` = %(" + al + ")s,"
sql = sql[:-1] #去掉最后一个逗号
sql += " where _id = %(_id)s " #只支持按主键进行修改
rs = exec_sql(sql, params) #提供字典参数
if rs[0]:
return {"code": 200, "info": "update success.", "total": rs[2]}
else:
return {"code": rs[1].args[0], "error": rs[1].args[1], "total": rs[2]}</code></pre>
<h3>删除接口</h3>
<p>以字典形式提供参数,占位符的形式为:%(keyname)s,只支持按主键进行删除。</p>
<pre><code>def delete(tablename, params={}):
sql = "delete from %s " % tablename
sql += " where _id = %(_id)s "
rs = exec_sql(sql, params)
if rs[0]:
return {"code": 200, "info": "delete success.", "total": rs[2]}
else:
return {"code": rs[1].args[0], "error": rs[1].args[1], "total": rs[2]}</code></pre>
<h2>中间层(baseDao)</h2>
<p>提供默认的操作数据库接口,实现基础的业务逻辑,单表的CURD有它就足够了。有复杂业务逻辑时,继承它,进行扩展就可以了。</p>
<pre><code>import dbhelper
class BaseDao(object):
def __init__(self, table):
self.table = table
def retrieve(self, params={}, fields=[], session={}):
return dbhelper.select(self.table, params)
def create(self, params={}, fields=[], session={}):
if '_id' in params and len(params) < 2 or '_id' not in params and len(params) < 1: #检测参数是否合法
return {"code": 301, "err": "The params is error."}
return dbhelper.insert(self.table, params)
def update(self, params={}, fields=[], session={}):
if '_id' not in params or len(params) < 2: #_id必须提供且至少有一修改项
return {"code": 301, "err": "The params is error."}
return dbhelper.update(self.table, params)
def delete(self, params={}, fields=[], session={}):
if '_id' not in params: #_id必须提供
return {"code": 301, "err": "The params is error."}
return dbhelper.delete(self.table, params)
</code></pre>
<h2>动态调用CURD</h2>
<p>根据客户调用的rest方式不同,动态调用baseDao的相应方法,这个很关键,实现了它才能自动分配方法调用,才能只需要建立一个数据表,就自动提供CURD基本访问功能。还好,动态语言能很方便的实现这种功能,感慨一下,node.js更方便且符合习惯^_^</p>
<pre><code> method = {
"GET": "retrieve",
"POST": "create",
"PUT": "update",
"DELETE": "delete"
}
getattr(BaseDao(table), method[request.method])(params, [], {})</code></pre>
<p>说明:</p>
<ul>
<li>table是前一章中解析出来的数据表名,这块就是users;</li>
<li>method应该是定义一个常量对象,对应rest的动词,因为对ypthon不熟,定义了一个变量先用着,查了下常量说明,看着好复杂;</li>
<li>request.method 客户请求的实际rest动词;</li>
<li>params是前一章中解析出来的参数对象;</li>
</ul>
<h2>完整代码</h2>
<pre><code>git clone https://github.com/zhoutk/pyrest.git
cd pyrest
export FLASK_APP=index.py
flask run</code></pre>
<h2>小结</h2>
<p>至此,我们已经实现了基本的框架功能,以后就是丰富它的羽翼。比如:session、文件上传、跨域、路由改进(支持无缝切换操作数据库的基类与子类)、参数验证、基础查询功能增强(分页、排序、模糊匹配等)。<br>感慨一下,好怀念在node.js中json对象的写法,不用在key外加引号。</p>
<h2>补丁</h2>
<p>刚把基础数据库访问类中的insert方法的参数形式改成了字典,结果异常信息也正常了,文章不再改动,有兴趣者请自行查阅源代码。</p>
用python写通用restful api service(一)
https://segmentfault.com/a/1190000012264993
2017-12-04T08:00:00+08:00
2017-12-04T08:00:00+08:00
zhoutk
https://segmentfault.com/u/zhoutk
7
<blockquote><p>一直在用node.js做后端,要逐步涉猎大数据范围,注定绕不过python,因此决定把一些成熟的东西用python来重写,一是开拓思路、通过比较来深入学习python;二是有目标,有动力,希望能持之以恒的坚持下去。</p></blockquote>
<h2>项目介绍</h2>
<p>用python语言来写一个restful api service,数据库使用mysql。因为只做后端微服务,并且ORM的实现方式,采用自动生成SQL的方式来完成,因此选择了轻量级的flask作为web框架。如此选择,主要目的是针对中小规模的网络应用,能充分利用关系数据库的种种优势,来实现丰富的现代互联网应用。</p>
<h2>restful api</h2>
<p>restful api 的概念就不介绍了。这里说一下我们实现协议形式:</p>
<pre><code>[GET]/rs/user/{id}/key1/value1/key2/value2/.../keyn/valuen
[POST]/rs/user[/{id}]
[PUT]/rs/user/{id}
[DELETE]/rs/user/{id}/key1/value1/key2/value2/.../keyn/valuen</code></pre>
<blockquote><p>说明:</p></blockquote>
<ul>
<li>rs为资源标识;</li>
<li>第二节,user,会被解析为数据库表名;</li>
<li>查询时,id为空或0时,id会被忽略,即为列表查询;</li>
<li>新建和修改,除接收form表单外,url中的id参数也会被合并到参数集合中;</li>
<li>删除同查询。</li>
</ul>
<h2>让flask支持正则表达式</h2>
<blockquote><p>flask默认路由不支持正则表达式,而我需要截取完整的URL自己来解析,经查询,按以下步骤很容易完成任务。</p></blockquote>
<ul>
<li>使用werkzeug库 :from werkzeug.routing import BaseConverter</li>
<li>定义转换器:</li>
</ul>
<pre><code>class RegexConverter(BaseConverter):
def __init__(self, map, *args):
self.map = map
self.regex = args[0]</code></pre>
<ul>
<li>注册转换器 : app.url_map.converters['regex'] = RegexConverter</li>
<li>用正则来截取url : @app.route('/rs/<regex(".*"):query_url>', methods=['PUT', 'DELETE', 'POST', 'GET'])</li>
</ul>
<p>几点疑问:</p>
<ol>
<li>正则(.*)理论上应该是匹配任何除回车的所有字符,但不知道为什么,在这里不识别问号(?)</li>
<li>我用request.data来取表单数据,为何request.form取不到?</li>
<li>'/rs/<regex(".<em>"):query_url>'后若加个反斜杠('/rs/<regex(".</em>"):query_url>/'),request.data就取不到数据,为什么?</li>
</ol>
<h2>解析json数据</h2>
<blockquote><p>解析json数据很容易,但我需要对客户端送上来的数据进行校验,下面是用异常处理又只解析一次的解决方案。</p></blockquote>
<pre><code>def check_json_format(raw_msg):
try:
js = json.loads(raw_msg, encoding='utf-8')
except ValueError:
return False, {}
return True, js</code></pre>
<h2>URL解析</h2>
<blockquote><p>按既定协议解析URL,提取表名,为生成sql组合参数集合。</p></blockquote>
<pre><code>@app.route('/rs/<regex(".*"):query_url>', methods=['PUT', 'DELETE', 'POST', 'GET'])
def rs(query_url):
(flag, params) = check_json_format(request.data)
urls = query_url.split('/')
url_len = len(urls)
if url_len < 1 or url_len > 2 and url_len % 2 == 1:
return "The params is wrong."
ps = {}
for i, al in enumerate(urls):
if i == 0:
table = al
elif i == 1:
idd = al
elif i > 1 and i % 2 == 0:
tmp = al
else:
ps[tmp] = al
ps['table'] = table
if url_len > 1:
ps['id'] = idd
if request.method == 'POST' or request.method == 'PUT':
params = dict(params, **{'table': ps.get('table'), 'id': ps.get('id')})
if request.method == 'GET' or request.method == 'DELETE':
params = ps
return jsonify(params)</code></pre>
<h2>pycharm项目配置</h2>
<p>配置好Run/Debug Configurations才能在IDE中运行并单步调试,可以很熟悉flask框架的运行原理。</p>
<ul>
<li>Script path : /usr/local/bin/flask</li>
<li>Parameters : run</li>
<li>
<p>环境变量</p>
<ul>
<li>FLASK_APP = index.py</li>
<li>LC_ALL = en_US.utf-8</li>
<li>LANG = en_US.utf-8</li>
</ul>
</li>
</ul>
<p>本以为配置完上面三条就能运行了,因为在终端模拟器上就已经能正常运行。结果在IDE中出现了一堆莫名的错误,仔细看,大概是编码配置的问题。经搜索,还需要配置后面两个环境变量才能正常运行,大概原因是python版本2与3之间的区别。</p>
<h2>完整代码</h2>
<pre><code>git clone https://github.com/zhoutk/pyrest.git
cd pyrest
export FLASK_APP=index.py
flask run</code></pre>
<h2>小结</h2>
<p>今天利用flask完成了web基础架构,能够正确解析URL,提取客户端提交的数据,按请求的不同方式来组合我们需要的数据。</p>
python读excel写入mysql小工具
https://segmentfault.com/a/1190000012041140
2017-11-16T23:03:44+08:00
2017-11-16T23:03:44+08:00
zhoutk
https://segmentfault.com/u/zhoutk
5
<h2>背景</h2>
<p>需要导入全国高校名录到数据库,从教委网站下到了最新的数据,是excel格式,需要做一个工具进行导入,想试用一下python,说干就干。</p>
<h2>库</h2>
<ul>
<li><p>xlrd : excel读写库</p></li>
<li><p>pymysql : mysql数据库驱动库,纯python打造</p></li>
<li><p>re : 正则表达式库,核心库</p></li>
</ul>
<p>前两个用pip轻松完成安装,本人是在mac pro是进行的,过程很顺利,以前在mac上装mysqlclient一直安装不上,所以一度放弃使用python,但我在linux下安装mysqlclient却没有任何问题。</p>
<h2>源代码</h2>
<p>很简单的小脚本,留存纪念。值得注意的一点,数据库连接字段串中要设定字符编码,不然默认是lanti-1,写入会出错。</p>
<pre><code>import xlrd
import pymysql
import re
conn = pymysql.connect(host='database connect address', port=1234, user='root',
passwd='****', db='database name', charset='utf8mb4')
p = re.compile(r'\s')
data = xlrd.open_workbook('./W020170616379651135432.xls')
table = data.sheets()[0]
t = table.col_values(1)
nrows = table.nrows
for i in range(nrows):
r1 = table.row_values(i)
if len(r1[2]) == 10:
cur = conn.cursor()
cur.execute('insert into `university` (`id`, `name`, `ministry`, `city`, `level`, `memo`) \
values (%s, %s, %s, %s, %s, %s)',
(r1[2], p.sub('', r1[1]), p.sub('', r1[3]), p.sub('', r1[4]), r1[5], r1[6]))
conn.commit()
cur.close()
conn.close()</code></pre>
<h2>心得</h2>
<p>写惯了类C的语言,不太习惯python,想同时掌握两种风格的编程语言,好痛苦啊。python编程效率的确不错,这是我第一次用python写实用小程序,连查带写带调试,一共也就花了一个来小时。python库与资料丰富,不愁找不到合适的^_^</p>
<h2>数据库写入优化</h2>
<p>早上闲来无事,用批量写入优化了一下,任务秒完成,比一条条写入快了很多, 比我预想的差别还要大。看来,没有不好的工具,只是我们没有用好啊!</p>
<pre><code>import xlrd
import pymysql
import re
conn = pymysql.connect(host='database connect address', port=1234, user='root',
passwd='****', db='database name', charset='utf8mb4')
p = re.compile(r'\s')
data = xlrd.open_workbook('./W020170616379651135432.xls')
table = data.sheets()[0]
t = table.col_values(1)
nrows = table.nrows
ops = []
for i in range(nrows):
r1 = table.row_values(i)
if len(r1[2]) == 10:
ops.append((r1[2], p.sub('', r1[1]), p.sub('', r1[3]), p.sub('', r1[4]), r1[5], r1[6]))
cur = conn.cursor()
cur.executemany('insert into `university_copy` (`id`, `name`, `ministry`, `city`, `level`, `memo`) \
values (%s, %s, %s, %s, %s, %s)', ops)
conn.commit()
cur.close()
conn.close()</code></pre>
基于react-native & antd-mobile进行三端开发
https://segmentfault.com/a/1190000010313569
2017-07-23T22:54:00+08:00
2017-07-23T22:54:00+08:00
zhoutk
https://segmentfault.com/u/zhoutk
10
<blockquote><p>要做移动端应用,同时要适配ios、android和微信。搜索、试验、思考...几天内进行了好几轮,最终决定采用react-native & antd-mobile来实现我们的目的。</p></blockquote>
<h2>思路&选择</h2>
<p>在网上搜索,看到了多种方案。第一种,利用redux,共享业务逻辑,自己维护两套UI组件;第二种,利用react-native-web,先写移动端,再将移动端转换成H5;第三种:利用styled-components来封装UI组件,也要维护两套UI;第四种:利用antd-mobile来适配三端。<br>最终决定选择antd-mobile方式,因为其本身就是一套很好的解决方案,文档较全,实现方式简单,虽然是两套代码,但现有组件已经很多,也容易扩展。我已经修复了一个小bug,自行发布到了npm,并替换到项目中,这样能够快速方便的实现自己想要的组件。</p>
<h2>代码编写原则</h2>
<p>所有的界面元素都使用antd-mobile的组件来实现,不够用的,不符合要求的,直接改动antd-mobile。</p>
<h2>关键步骤</h2>
<h3>webpack2配置</h3>
<blockquote><p>antd-mobile要支持H5,在要webpack中进行配置,打包web版的代码。</p></blockquote>
<p>import antd-mobile</p>
<pre><code> {
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [{
loader:'babel-loader',
options:{
presets: ['es2015', "stage-2", 'react'],
plugins: [ ["transform-runtime"],["import", {libraryName: "antdm", style: true}]
],
}
}],
},</code></pre>
<p>resolve web.*</p>
<pre><code>resolve: {
mainFiles: ["index.web","index"],// 这里哦
modules: ['app', 'node_modules', path.join(__dirname, '../node_modules')],
extensions: [
'.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.ts', '.tsx',
'.js',
'.jsx',
'.react.js',
],
mainFields: [
'browser',
'jsnext:main',
'main',
],
},</code></pre>
<h3>布局组件</h3>
<blockquote><p>antd-mobile文档中只提了复杂的组件,但我们在H5中经常用的div与native中的View应该如何处理呢?看文档、搜索,都没有找到我想要的方法;在github中看别人家的代码,发现都是直接用了div或native的View,不能同时适配三端。以至于我一度想引入styled-components来封装,但总觉得引入styled-components只用来处理几个基本元素,不划算。最后是想起来要看看antd-mobile的源码,在antd-mobile中来做引入styled-components做的事,不就可以了吗!结果发现antd-mobile已经有封装了,就是View。</p></blockquote>
<p>问题基本解决了,但运行时会有如下的警告信息:</p>
<pre><code>Warning: Unknown prop `Component` on <div> tag. Remove this prop from the element. For details, see https://fb.me/react-unknown-prop
in div (created by View)
in View (at Root.js:15)
in Provider (at Root.js:14)
in Root (at index.js:20)
in AppContainer (at index.js:19)</code></pre>
<p>看源代码,是有个小bug,顺手修改了,编译,运行,问题解决。提交pull requests,人家不可能很快的更新,而且可能有的改动只是为了适应我们自己的项目,因此发布到npm中,名字叫antdm。</p>
<h3>程序入口</h3>
<blockquote><p>入口文件会有三个,原则是尽量保持简单</p></blockquote>
<p>ios:index.ios.js</p>
<pre><code>import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import Root from './src/containers/Root';
import configureStore from './src/store/configureStore.js';
const store = configureStore();
class T3 extends Component {
render() {
return (
<Root store={store}/>
);
}
}
AppRegistry.registerComponent('t3', () => T3);</code></pre>
<p>web:/src/web/index.js</p>
<pre><code>import React from 'react';
import { render } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import Root from '../containers/Root';
import configureStore from '../store/configureStore';
const store = configureStore();
render(
<AppContainer>
<Root store={store} />
</AppContainer>,
document.getElementById('root')
)</code></pre>
<p>android:index.android.js</p>
<pre><code>还未编写,应该与ios差不多。</code></pre>
<h2>项目地址</h2>
<pre><code>https://git.oschina.net/zhoutk/t3.git
https://github.com/zhoutk/t3.git</code></pre>
<h2>使用方法</h2>
<pre><code>git clone https://git.oschina.net/zhoutk/t3.git
or git clone https://github.com/zhoutk/t3.git
cd t3
npm i
ios: npm run ios
web: npm run web</code></pre>
<h2>小结</h2>
<p>利用react-native和antd-mobie基本达到移动端适配三端的要求,但在做项目的同时,可能需要基于antd-mobile逐步建立起一套适合自己的UI组件。谢谢阿里的兄弟们!</p>
reactjs前端实践|第四篇:TodoList示例rudex、immutable-js
https://segmentfault.com/a/1190000009325250
2017-05-07T23:21:41+08:00
2017-05-07T23:21:41+08:00
zhoutk
https://segmentfault.com/u/zhoutk
2
<h2>实践四</h2>
<blockquote><p>延续Todo List示例,使用redux & immutabel-js对项目进行改造。</p></blockquote>
<h2>遵循原则</h2>
<ul>
<li><p>单一数据源(整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中)</p></li>
<li><p>State 是只读的(惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象)</p></li>
<li><p>使用纯函数来执行修改(为了描述 action 如何改变 state tree ,需要编写 reducers)</p></li>
<li><p>Dumb组件存入于components目录中,用函数式组件来实现;Smart组件存放于containers目录中。</p></li>
<li><p>constants目录存放所有action type常量定义,actions目录存放所有action creators</p></li>
</ul>
<h2>代码分析</h2>
<blockquote><p>我第一步在没有使用immutable-js的情况下完成了引入redux的改造,然后再加入了immutable-js,逐步体验了各个库引入带来的变化与好处。</p></blockquote>
<h3>引入redux</h3>
<p>新增constants/ActionTypes.js</p>
<pre><code>export const ADD_TODO = 'ADD_TODO'</code></pre>
<p>新增actions/index.js</p>
<pre><code>import * as types from '../constants/ActionTypes'
export const addTodo = text => ({ type: types.ADD_TODO, text })</code></pre>
<p>新增reducers/index.js, 统一加载reducer处理入口,方便分文件编写相同模块的处理函数。</p>
<pre><code>import { combineReducers } from 'redux'
import todos from './todos'
const rootReducer = combineReducers({
todos
})
export default rootReducer</code></pre>
<p>新增reducer/todos.js, todos模块的处理函数。</p>
<pre><code>import { ADD_TODO } from '../constants/ActionTypes'
import * as __ from 'lodash';
const initialState =
{
items:[]
};
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
let newState = __.cloneDeep(state); //深拷贝,不能直接改原state
newState.items.push({key:new Date().getTime(),text:action.text});
return newState;
default:
return state
}
}
</code></pre>
<p>修改App.js</p>
<pre><code>import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as TodoActions from '../actions'; //集中放置,方便统一导入
class App extends Component {
static propTypes = { //类静态变量的方式
todos: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
componentDidMount (){ //具有生命周期的组件不能函数组件实现
this.InputComponent.focus();
};
render() {
var {todos,actions} = this.props; //解构属性
return (
<div>
<InputAndButton ref={comp => { this.InputComponent = comp; }} onSave={actions.addTodo}/>
<LiList items={todos}/>
</div>
);
};
}
const mapStateToProps = state => ({
todos: state.todos.items //注意,这块是state.todos.items,中间的todos是因底层封装被带入,不能少,多用工具观察state结构,能明白不少问题。
})
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(TodoActions, dispatch) //统一引入,这块能很简洁
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(App)</code></pre>
<p>修改index.js</p>
<pre><code>import { createStore } from 'redux'
import { Provider } from 'react-redux'
import reducer from './reducers'
const store = createStore(reducer) //初始化state
render(
<Provider store={store}> //加载应用
<App />
</Provider>,
document.getElementById('root')
);</code></pre>
<h3>引入immutable-js</h3>
<blockquote><p>为避免对象的深拷贝,使用Immutable,它提供了简洁高效的判断数据是否变化的方法,可以极大提高性能。我使用redux-immutable完成改造。</p></blockquote>
<p>修改src/reducers/index.js</p>
<pre><code>-import { combineReducers } from 'redux'
+import { combineReducers } from 'redux-immutable' //切换组合函数
import todos from './todos'
const rootReducer = combineReducers({ //其余地方无需改变
...</code></pre>
<p>修改src/reducers/todos.js</p>
<pre><code> import { ADD_TODO } from '../constants/ActionTypes'
-import * as __ from 'lodash'; //不再需要辅助函数
+import {Map, List} from 'immutable'; //引入immutable的相应数据结构
-const initialState =
- {
- items:[]
- };
+let initialState = Map({ //使用immutable-js数据结构
+ items: List([])
+ });
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
- let newState = __.cloneDeep(state);
- newState.items.push({key:new Date().getTime(),text:action.text});
- return newState;
+ return state.update('items', value => value.push({key:new Date().getTime(),text:action.text})); //操作更加简洁
default:
return state
}</code></pre>
<p>修改src/containers/App.js</p>
<pre><code> const mapStateToProps = state => ({
- todos: state.todos.items
+ todos: state.get('todos').get('items') //使用immutable-js的Map操作接口
})</code></pre>
<h3>项目地址</h3>
<pre><code>https://git.oschina.net/zhoutk/reactodo.git
https://github.com/zhoutk/reactodo.git</code></pre>
<h3>使用方法</h3>
<pre><code>git clone https://git.oschina.net/zhoutk/reactodo.git
or git clone https://github.com/zhoutk/reactodo.git
cd reactodo
npm i
git checkout todo_list_react
npm start</code></pre>
<h2>小结</h2>
<p>这次实践,理解了redux的原理,学会了如何利用redux来改造我们的项目;并且利用immutable-js优化了state的操作及性能。</p>
reactjs前端实践|第三篇:TodoList示例事件、state、props、refs
https://segmentfault.com/a/1190000009223348
2017-04-27T17:56:35+08:00
2017-04-27T17:56:35+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>实践三</h2>
<blockquote><p>Todo List示例,未使用redux。内容涉及到展示组件与容器组件的合理使用,事件处理,参数传递,控件访问,组件功能设计等方面。其中遇到的坎,设置输入焦点,由于使用了styled-components而有不同;每个组件应试包含哪些state,是否应该进行状态提升;子组件如何向父组件传递数据。</p></blockquote>
<h2>代码分析</h2>
<blockquote><p>组件拆分:Li单个信息条目、LiList信息条目列表、InputAndButton输入组件、App容器组件。App的State包含一个item数组,存放信息列表;InputAndButton的State包含一个inputValue变量,存入用户本次输入的信息。其它组件不含State,为展示组件。</p></blockquote>
<h3>展示组件</h3>
<p>Li.js,展示一条信息,数据来自父组件</p>
<pre><code>const Li = (props) => {
return (
<TodoLi>{props.text}</TodoLi> //此处不能加key(但父组件要向它传递),否则会有警告
);
};
export default Li;</code></pre>
<p>LiList.js,展示信息列表,因为输入组件也会修改数据,所以state提升到容器组件中。</p>
<pre><code>const LiList = (props)=>{
return (
<TodoUl>
{props.items.map((item)=>{ //map方法来完成列表构建
return <Li {...item}></Li>; //item中要包含一个key字段,否则会有警告
})}
</TodoUl>
);
};
export default LiList;</code></pre>
<h3>输入组件</h3>
<blockquote><p>输入组件自己控制输入框的数据显示及清除,开始我把一切事件处理全放到App中了,这样方便了数据传递,但破坏了输入组件的内聚性。正确的方式是:输入组件自己控制界面的显示,只有在回车键按下或鼠标点击add按钮时,才将输入框中的数据传递给父组件(App)。</p></blockquote>
<p>InputAndButton.js</p>
<pre><code>class InputAndButton extends Component {
constructor(props) {
super(props);
this.state={
inputValue:'' //用来处理鼠标点击时取得输入数据
};
}
onChange = e => {
this.setState({
inputValue:e.target.value //按键时保存输入数据
})
};
addItem = e => {
if(e.which === 13 || e.type === 'click'){ //回车或鼠标点击
if(this.state.inputValue.trim().length > 0) { //输入不为空
this.props.onSave(this.state.inputValue.trim()); //调用父组件提供的函数,向父组件传递数据。
e.target.value = ''; //清空输入
this.setState({
inputValue:''
})
}
}
};
render = () => {
return (
<div>
<TodoInput onChange={this.onChange}
onKeyUp={this.addItem}
placeholder="enter task"
value={this.state.inputValue}>
</TodoInput>
<TodoButton onClick={this.addItem} type="submit">add</TodoButton>
</div>
);
}
}
export default InputAndButton;</code></pre>
<h3>容器组件</h3>
<pre><code>class App extends Component {
constructor(props){
super(props);
this.state={
items:[] //信息列表数据
};
}
handleSave(text){ //传递到子组件的函数,接收数据并追加条目
if(text.length !== 0){
this.state.items.push({key: new Date().getTime(), text}); //key设定唯一值,不作说明,网上资料很多
this.setState({
items: this.state.items,
})
}
};
render() {
return (
<div>
<InputAndButton onSave={this.handleSave.bind(this)}/> //处理函数以props传给子组件
<LiList items={this.state.items}/>
</div>
);
};
}
export default App;</code></pre>
<h3>输入焦点控制</h3>
<blockquote><p>利用特殊的属性 Ref来访问子组件中的控件,并控制输入焦点,需要注意的是:styled-components对这个属性有些改变,要用innerRef来替代ref,不然拿到的是一个 StyledComponent包装的对象,而不是DOM结点。有疑问,去看其文档的Tips and tricks部分之Refs to DOM nodes。</p></blockquote>
<p>App.js中输入组件修改如下:</p>
<pre><code><InputAndButton ref={comp => { this.InputComponent = comp; }} onSave={this.handleSave.bind(this)}/></code></pre>
<p>App.js中增加componentDidMount处理,在组件加载完成时设置输入焦点。</p>
<pre><code> componentDidMount (){
this.InputComponent.focus();
};</code></pre>
<p>InputAndButton.js中修改input如下:</p>
<pre><code><TodoInput innerRef={el=> { this.el = el; }} ...</code></pre>
<p>InputAndButton.js中增加函数,供父组件中调用。</p>
<pre><code> focus = () => {
this.el.focus();
};</code></pre>
<h3>项目地址</h3>
<pre><code>https://git.oschina.net/zhoutk/reactodo.git
https://github.com/zhoutk/reactodo.git</code></pre>
<h3>使用方法</h3>
<pre><code>git clone https://git.oschina.net/zhoutk/reactodo.git
or git clone https://github.com/zhoutk/reactodo.git
cd reactodo
npm i
git checkout todo_list_react
npm start</code></pre>
<h2>小结</h2>
<p>这次实践,学会了事件,数据传递,组件划分,功能设计等基本方法,可以去折腾redux了。</p>
reactjs前端实践|第二篇:单页应用示例路由及样式的使用
https://segmentfault.com/a/1190000009191616
2017-04-25T16:51:33+08:00
2017-04-25T16:51:33+08:00
zhoutk
https://segmentfault.com/u/zhoutk
2
<h2>实践二</h2>
<blockquote><p>简单单页应用,使用react-router v4.1.1,styled-components来实现。应用功能:三个导航标签(Home,Stuff,Contact),点击标签切换页面内容。</p></blockquote>
<h2>难点分析</h2>
<ul>
<li><p>react-router v4 与以前版本变化较大,做web app时应引入react-router-dom,react-router现在成了其核心库,只包含最基本的功能;</p></li>
<li><p>styled-components的合理应用,资料少,只能看着官方文档,一点点的试验,一点点的理解;</p></li>
</ul>
<h2>项目结构</h2>
<p>增加styled目录,自定义样式组件,界面元素几乎都在这个目录中,只有index.html中还引入了一个css文件,定义基本元素的通用设定。</p>
<pre><code>-public
--index.html
--index.css
-src
--components
---Contact.js
---Home.js
---Stuff.js
--containers
---App.js
--styled
---Content.js
---Header.js
--index.js</code></pre>
<h2>代码分析</h2>
<blockquote><p>路由使用了react-router v4,因此引入的是react-router-dom,开始使用react-router,这现在已经是其核心库,有些组件已经被剥离出去,比如:history,开始一直报错,说:history是必需的,却未定义。若想只用核心库,就要自己安装并配置history。</p></blockquote>
<h3>路由设置</h3>
<pre><code>import {HashRouter as Router,Route} from 'react-router-dom';
ReactDOM.render(
<Router>
<App> //react-router v4允许在Router下放任意标签,也可象我这样放自定义组件
<Route exact path="/" component={Home} /> //exact是指path进行精确匹配,若不加,根目录会与任何路径都匹配。
<Route path="/stuff" component={Stuff} />
<Route path="/contact" component={Contact} />
</App>
</Router>,
document.getElementById('root')
);</code></pre>
<p>若使用核心组件,必需配置history,如下:</p>
<pre><code>import createHistory from 'history/createBrowserHistory'
const history = createHistory()
ReactDOM.render(
<Router history={history}>
......
</Router>
);</code></pre>
<h3>styled-components</h3>
<blockquote><p>styled-components将样式组件化,我将基础元素的样式设置放在/public/index.css中,这是一个基调,styled的基本元素会继承这些样式。合理的使用styled-components的继承,能让组件设计更清晰、重用率更高。</p></blockquote>
<p>样式基础设定(index.css)</p>
<pre><code>body { //页面的基础设定
background-color: #FFCC00;
padding: 20px;
margin: 0;
}
h1, h2, p, ul, li{
font-family: Helvetica, Arial, sans-serif; //设定通用字体
}</code></pre>
<p>自定义样式(Header.js)</p>
<pre><code>import styled from 'styled-components';
import {NavLink} from 'react-router-dom'; //第三方组件
const HeaderUl = styled.ul` //基本使用方法
background-color: #111;
padding: 0;
`;
......
const HeaderA = styled(NavLink)` //继承第三方组件
color: #FFF;
font-weight: bold;
text-decoration: none;
padding: 20px;
display: inline-block;
&:hover { //鼠标悬停事件定义
color: yellow;
}
`;
export {HeaderUl, HeaderLi, HeaderA};</code></pre>
<p>页面框架组织(App.js)</p>
<pre><code>import {HeaderUl, HeaderLi, HeaderA} from '../styled/Header';
class App extends Component {
render() {
const activeStyle = {backgroundColor: "#0099FF"}; //选中样式对象使用内嵌方式,让组件逻辑内聚
return (
<div>
<h1>Simple SPA</h1>
<HeaderUl className="header">
<HeaderLi><HeaderA exact to="/" activeStyle={activeStyle}>Home</HeaderA></HeaderLi> //放弃activeClassName,使用activeStyle,让本组件的数据完全内聚
......
</HeaderUl>
......
</div>
);
}
}
export default App;</code></pre>
<p>页面内容组织(Home.js)</p>
<pre><code>import {ContentDivH2} from '../styled/Content'; //引入样式组件
class Home extends Component {
render() {
return (
<div>
<ContentDivH2>HELLO</ContentDivH2> //使用样式组件
<p>text content row one.</p>
<p>text content row tow.</p>
</div>
);
}
}
export default Home;</code></pre>
<h3>项目地址</h3>
<pre><code>https://git.oschina.net/zhoutk/reactodo.git
https://github.com/zhoutk/reactodo.git</code></pre>
<h3>使用方法</h3>
<pre><code>git clone https://git.oschina.net/zhoutk/reactodo.git
or git clone https://github.com/zhoutk/reactodo.git
cd reactodo
npm i
git checkout spa-web-app
npm start</code></pre>
<h2>小结</h2>
<p>这次实践,使用了路由来实现一个简单的单页应用,并使用styled-components库来组织我们的样式,让样式也组件化,从而达到更好的封装效果,提高代码的重用率。</p>
reactjs前端实践|第一篇:调色板示例组件的使用
https://segmentfault.com/a/1190000009182089
2017-04-24T22:35:26+08:00
2017-04-24T22:35:26+08:00
zhoutk
https://segmentfault.com/u/zhoutk
1
<h2>背景</h2>
<blockquote><p>最近终于开始折腾前端,经过大量的阅读和研究,决定使用react.js及相关产品来架构我的前端技术。我本是个纯后端,喜欢算法,几年前,发现了node.js,因为它开源,底层是C++,正中我下怀,呵呵!</p></blockquote>
<h2>原因</h2>
<ul>
<li><p>react.js以javascript为主,将HTML放置在JS中,与Angular、 Ember和Knockout相反。感谢<a href="https://link.segmentfault.com/?enc=001GgVRbSycsr39KMEqklg%3D%3D.ubKh%2BDgGE%2FTq3nro%2FiU7Gqbc9RXod74zOTQn%2FINEnz6wxwNHwNS%2FwrcoerCX89ZlpR2TmbGtPxIqlTQZATU3A4MBhZl1wRMkF3LEPIpk54o%3D" rel="nofollow">《react: the other side of the coin》</a>这篇文章的作者与译者;</p></li>
<li><p>组件编程,提倡组件化到最小粒度,重用率比较高,长期积累,可形成自己的组件库;</p></li>
<li><p>开源、轻量库,生态圈好;</p></li>
</ul>
<h2>技术栈</h2>
<ul>
<li><p>es6</p></li>
<li><p>react.js</p></li>
<li><p>react-router-dom v4 (第四版,有好多改变,花了不少时间)</p></li>
<li><p>redux</p></li>
<li><p>redux-saga (还在犹豫,是否用它)</p></li>
<li><p>styled-components</p></li>
</ul>
<h2>工具</h2>
<ul><li><p>create-react-app</p></li></ul>
<p>在学习、开发阶段,有它真好。我发现,拖了这久我才行动起来,原来是我没有遇见create-react-app这个神器啊,呵呵。</p>
<h2>实践一</h2>
<blockquote><p>color-card(调色板)示例</p></blockquote>
<h3>示例描述</h3>
<p>显示一块调色板,上面一个正方形展现真实颜色,下面一个文字标签显示颜色的16进制编码,色值由父类单入口传递。</p>
<h3>代码结构</h3>
<p>整个App分解为三个组件,一个父组件(Card)、两个子组件(Square、Label),颜色值由Card组件的Props向子组件单向传递,样式使用styled-components来定义。</p>
<h3>代码分析</h3>
<p>父组件(Card)</p>
<pre><code>import React, { Component } from 'react';
import styled from 'styled-components';
import Square from './Square';
import Label from './Label';
const CardDiv = styled.div` //定义卡片div
height: 200px;
width: 150px;
padding: 0,
background-color: #FFF;
-webkit-filter: drop-shadow(0px 0px 5px #666); //这行可以删除
filter: drop-shadow(0px 0px 5px #666);
`;
class Card extends Component {
render() {
return (
<CardDiv>
<Square color={this.props.color}/> //传递Props
<Label color={this.props.color}/>
</CardDiv>
);
}
}
export default Card;</code></pre>
<p>子组件(Square)</p>
<pre><code>import React, { Component } from 'react';
import styled from 'styled-components';
const SquareDiv = styled.div`
height: 150px;
background-color: ${props => props.color}; //styled-components中接收父组件传递来的参数的方式
`;
class Square extends Component {
render() {
return (
<SquareDiv {...this.props}></SquareDiv> //延展符的使用,传递属性到styled-components中
);
}
}
export default Square;</code></pre>
<p>子组件(Label)</p>
<pre><code>import React, { Component } from 'react';
import styled from 'styled-components';
const P = styled.p`
font-family: sans-serif;
font-weight: bold;
padding: 13px;
margin: 0;
`;
class Label extends Component {
render() {
return (
<P>{this.props.color}</P>
);
}
}
export default Label;</code></pre>
<p>组件加载(index.js)</p>
<pre><code>import Card from './components/Card';
ReactDOM.render(
<Card color="#FFA737"/>,
document.getElementById('root')
);</code></pre>
<h3>项目地址</h3>
<pre><code>https://git.oschina.net/zhoutk/reactodo.git
https://github.com/zhoutk/reactodo.git</code></pre>
<h3>使用方法</h3>
<pre><code>git clone https://git.oschina.net/zhoutk/reactodo.git
or git clone https://github.com/zhoutk/reactodo.git
cd reactodo
npm i
git checkout color-card
npm start</code></pre>
<h2>小结</h2>
<p>这是第一篇,主要是技术选型思考,样式的处理思考了很久,最后决定用style-components,希望它不要让我失望。难点:父组件的属性传递到styled-components组件中的方式,它以子组件的方式来组织的,并采用了回调函数的方式,与JSX的直接使用方式有所不同,试验了好多次,才理解。后来想想,还是本人对前端的思维不熟练导致的,要加强实践,加油!</p>
基于Debian系统安装node运行环境(docker、canvas)
https://segmentfault.com/a/1190000008540487
2017-03-02T12:10:46+08:00
2017-03-02T12:10:46+08:00
zhoutk
https://segmentfault.com/u/zhoutk
0
<p>以前的软件部暑的docker镜像一直用ubuntu14.04来制作的,综合考虑,决定将系统切换到debian8.7(stable)下。</p>
<h2>难点</h2>
<p>因为我们的系统使用了canvas插件,这个插件依赖库比较多,安装较为麻烦,还好ubuntu与debian是一个系列的,有ubuntu下的经验,处理起来还是有信心的。</p>
<h2>docker宿主环境安装</h2>
<p>因docker是基于ubuntu开发的,因此宿主机还是选择了ubuntu16.04长效版。安装最新版docker命令:</p>
<pre><code>curl -s https://get.docker.com | sudo sh</code></pre>
<p>下载debian官方镜像及启动一个容器:</p>
<pre><code>docker pull debian:8
docker run -it --name base debian:8 /bin/bash</code></pre>
<p>这样就进入了一个debian系统中,在此上进行运行环境的安装,最后将生成为我们的镜像,就可以重复使用了。</p>
<h2>运行环境的安装</h2>
<h3>第一步,基本编辑器的安装,切换合适的更新源。</h3>
<pre><code>echo "deb http://mirrors.163.com/debian/ jessie main non-free contrib" >> /etc/apt/sources.list
apt-get update
apt-get install vim
vim /etc/apt/sources.list</code></pre>
<p>切换到如下更新源:</p>
<pre><code>deb http://mirrors.163.com/debian/ jessie main non-free contrib
deb http://mirrors.163.com/debian/ jessie-updates main non-free contrib
deb http://mirrors.163.com/debian/ jessie-backports main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie-updates main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie-backports main non-free contrib
deb http://mirrors.163.com/debian-security/ jessie/updates main non-free contrib
deb-src http://mirrors.163.com/debian-security/ jessie/updates main non-free contrib</code></pre>
<p>这时运行apt-get update,若出现错误:</p>
<pre><code>W: GPG error: http://ftp.cn.debian.org jessie InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 5C808C2B65558117</code></pre>
<p>使用下面的命令把公钥导入(替换相应的PUBKEY):</p>
<pre><code>gpg --keyserver pgpkeys.mit.edu --recv-key 5C808C2B65558117
gpg -a --export 5C808C2B65558117 | apt-key add -
重新更新源:
apt-get update
apt-get upgrade -y</code></pre>
<h3>第二步,安装canvas插件依赖库</h3>
<p>ubuntu下的libjpeg8-dev替换成libjpeg-dev即可成功安装相应的依赖库。</p>
<pre><code>sudo apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++ -y</code></pre>
<h3>第三步,安装node.js,pm2</h3>
<p>上官网,获取node.js的安装包下载链接,用以下命令进行下载及安装。</p>
<pre><code>apt-get install curl xz-utils -y
curl -O https://nodejs.org/dist/v6.10.0/node-v6.10.0-linux-x64.tar.xz
xz -d node-v6.10.0-linux-x64.tar.xz
tar -xvf node-v6.10.0-linux-x64.tar
mv node-v6.10.0-linux-x64 node
ln -s /home/tlwl/softs/node/bin/node /usr/bin
ln -s /home/tlwl/softs/node/bin/npm /usr/bin
npm i -g pm2
ln -s /home/tlwl/softs/node/bin/pm2 /usr/bin</code></pre>
<h3>第四步,设置正确的时区</h3>
<pre><code>date -R //显示时区信息
tzselect //生成选定时区配置文件
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime //新时区设置生效
ntpdate s1a.time.edu.cn //网络对时(需安装)</code></pre>
<h3>第五步,生成镜像</h3>
<p>基础环境已经安装完成,退出容器,生成镜像即大功告成。</p>
<pre><code>docker commit 7c988bb6e1ca node:6.10</code></pre>
<p>使用docker iamges命令就可以看到我们生成的镜像了。</p>
<h2>小结</h2>
<p>宿主操作系统选择ubuntu,容器操作系统使用debian,这样的选型让整体更稳定。</p>