JoyLau's Blog

JoyLau 的技术学习与思考

前言

效果展示

JoyMedia - Search

在线地址

解释

  • 正如文章图片那样,在搜索框中输入想听的音乐/歌手/专辑
  • 在输入过程中及输入完成后,显示搜索结果的列表供用户选择

材料

  • REST 接口
  • jquery-autocomplete插件

优美的开始

准备工作

  • 引入插件 css: jquery.autocomplete.css
  • 引入插件 js : jquery.autocomplete.min.js
  • 写一个数据返回的 REST 接口

开始操作

  • 定义搜索的 input 的 id 值
1
2
3
4
5
6
7
8
9
10
<div class="navbar-form navbar-left input-s-lg m-t m-l-n-xs hidden-xs">
<div class="form-group" style="display: inline">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-sm bg-white btn-icon rounded"><i class="fa fa-search"></i></button>
</span>
<input id="keywords" type="text" class="form-control input-sm no-border rounded" placeholder="搜索 单曲/歌手/专辑...">
</div>
</div>
</div>
  • 这里我定义的是 keywords
  • 接下来在我们的 js 文件里调用 : $(“#keywords”).autocomplete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$("#keywords").autocomplete("/music/neteaseCloud/search", {
width : 350, // 提示的宽度,溢出隐藏
max : 30,// 显示数量
scrollHeight: 600,
resultsClass: "ac_results animated fadeInUpBig",
autoFill : false,//自动填充
highlight : false,
highlightItem: true,
scroll : true,
matchContains : true,
multiple :false,
matchSubset: false,
dataType: "json",
formatItem: function(row, i, max) {
//自定义样式
},
formatMatch: function(row, i, max) {
return row.name + row.id;
},
formatResult: function(row) {
return row.id;
},
parse:function(data) {
//解释返回的数据,把其存在数组里
if (data.data.length === 0) {
return [];
}else {
return $.map(data.data, function(row) {
return {
data: row
}
});
}

}
}).result(function(event, row, formatted) {
jQuery(this).val(row.name + ' ' + row.author);
addSearchResult(row.id);
});

接下来重点解释这个配置项

  • autocomplete 的第一个参数是url, 值得注意的是,这个 url 我们返回的结果数据是 JSON
  • 后面要专门针对返回的 JSON 数据进行解析
  • 再往后面来,看到的是一些配置项参数,一些简单的我就不在这多解释了,我这边主要说下我觉得比较重要的
  • resultsClass : 这个参数是生成的候选项的父 DIV,如下图所示:

JoyMedia - AutoComplate-Div

  • 默认提供的样式很不好看,默认提供的样式都写在 jquery.autocomplete.css 里面
  • 在这里面,能看到刚才截图的 div : ac_results
  • 那么我们要美化的就是 这个 div 和其子元素 li 的样式了
  • 为了跟契合本站的主题,我采用的黑色主题风格
  • 给ac_results添加了黑色背景色:background-color: #232c32
  • 在js文件里搜索ac_results,添加动画效果,并将这个配置写到配置项里:resultsClass: “ac_results animated fadeInUpBig”
  • ul 里的 li 是交替的样式的,class 分别为ac_odd和 ac_even,鼠标滑上去的效果为 ac_over,这几个地方自定义下样式
  • 还有一个配置: matchSubset,设置为 false ,可以避免输入大小写转换的js错误
  • formatItem : 返回的每一个结果都会再次处理,这里要做的事是以自己想要的样式显示出来
  • formatMatch : 匹配自己在结果集中想要的属性
  • formatResult : 自己最终要取的数据是什么
  • parse : 针对返回的JSON 数据进行转换,这里通过$. map 转化为 数组
  • result : 点击了列表项以后要做什么事情

完美的结束

欢迎大家来听听试试看!😘 http://music.joylau.cn (当前版本 v1.3)

前言

在线地址

Node.js 的学习

  • 入门是从这本书上开始的
  • 结合Node中文网的文档开始探索开发

说明

  • 利用 Node 来解析网易云音乐,其实质就是 跨站请求伪造 (CSRF),通过自己在本地代码中伪造网易云的请求头,来调用网易云的接口

分析

以获取歌曲评论来分析

  • 我们打开其中一首音乐,抓包看一下

JoyMedia - Node

  • 绝大部分的请求都是 POST 的
  • 我们找到其中关于评论的请求,如上图所示
  • 链接中间的部分是歌曲的 id 值
  • 在返回的 JSON 数据中包含了热评和最新评论
  • 评论过多的话是分页来展示的
  • 通过参数 limit 来显示评论数量, offset 来控制分页

JoyMedia - Node

  • 再来看,这是我本地浏览器中的 cookies 值,现在为止知道有个 csrf 值用来加密

JoyMedia - Node

  • 每个请求后面都会跟上csrf_token 值,其他的参数还有params 和 encSecKey
  • 这些值的加密算法无非是2种,一种是前台 js 加密生成的,另一种是将参数传往后台,由后台加密完再传回来
  • 想要测试一下很简单,将里面的值复制一下在 xhr 里找一下就知道了
  • 推测是是 js 加密的,加密的 js 简直不能看,如下图

JoyMedia - Node

  • 看到很多请求后面都返回了 md5 那么 md5 加密是肯定有的
  • 其实仔细看加密的参数,很多都能靠猜出来
  • 本地需要创建一个私钥secKey,十六位,之后aes加密生成,在通过rsa吧secKey加密作为参数一起传回
  • 那么下面贴出加密代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7';
const nonce = '0CoJUm6Qyw8W8jud';
const pubKey = '010001';
function createSecretKey(size) {
const keys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let key = "";
for (let i = 0; i < size; i++) {
let pos = Math.random() * keys.length;
pos = Math.floor(pos);
key = key + keys.charAt(pos)
}
return key
}

function aesEncrypt(text, secKey) {
const _text = text;
const lv = new Buffer('0102030405060708', "binary");
const _secKey = new Buffer(secKey, "binary");
const cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv);
let encrypted = cipher.update(_text, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted
}

function zfill(str, size) {
while (str.length < size) str = "0" + str;
return str
}

function rsaEncrypt(text, pubKey, modulus) {
const _text = text.split('').reverse().join('');
const biText = bigInt(new Buffer(_text).toString('hex'), 16),
biEx = bigInt(pubKey, 16),
biMod = bigInt(modulus, 16),
biRet = biText.modPow(biEx, biMod);
return zfill(biRet.toString(16), 256)
}

function Encrypt(obj) {
const text = JSON.stringify(obj);
const secKey = createSecretKey(16);
const encText = aesEncrypt(aesEncrypt(text, nonce), secKey);
const encSecKey = rsaEncrypt(secKey, pubKey, modulus);
return {
params: encText,
encSecKey: encSecKey
}
}
  • 挺复杂的,很多我也是参考网络上其他人的加密方式

伪造网易云头部请求

  • 这一步就很简单了,主要需要注意的就是 referer 的地址一定要是网易云的地址
  • 其他的想 cookie 和 User-Agent 直接复制浏览器的即可
  • 那我们构造一个 POST 的请求
  • 需要都回到函数和错误返回回调函数
  • 贴下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const Encrypt = require('./crypto.js');
const http = require('http');
function createWebAPIRequest(host, path, method, data, cookie, callback, errorcallback) {
let music_req = '';
const cryptoreq = Encrypt(data);
const http_client = http.request({
hostname: host,
method: method,
path: path,
headers: {
'Accept': '*/*',
'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'http://music.163.com',
'Host': 'music.163.com',
'Cookie': cookie,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36',

},
}, function (res) {
res.on('error', function (err) {
errorcallback(err)
});
res.setEncoding('utf8');
if (res.statusCode !== 200) {
createWebAPIRequest(host, path, method, data, cookie, callback);

} else {
res.on('data', function (chunk) {
music_req += chunk
});
res.on('end', function () {
if (music_req === '') {
createWebAPIRequest(host, path, method, data, cookie, callback);
return
}
if (res.headers['set-cookie']) {
callback(music_req, res.headers['set-cookie'])
} else {
callback(music_req)
}
})
}
});
http_client.write('params=' + cryptoreq.params + '&encSecKey=' + cryptoreq.encSecKey);
http_client.end()
}
  • 那么再结合我们刚才分析的评论API, 发出该请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const express = require("express");
const router = express();
const { createWebAPIRequest } = require("../common");

router.get("/", (req, res) => {
const rid=req.query.id;
const cookie = req.get('Cookie') ? req.get('Cookie') : '';
const data = {
"offset": req.query.offset || 0,
"rid": rid,
"limit": req.query.limit || 20,
"csrf_token": ""
};
createWebAPIRequest(
'music.163.com',
`/weapi/v1/resource/comments/R_SO_4_${rid}/?csrf_token=`,
'POST',
data,
cookie,
music_req => {
res.send(music_req)
},
err => res.status(502).send('fetch error')
)
});

module.exports = router;
  • 值得注意的是,这里我的 node 模板选择的 EJS 所使用的 js 语法格式也比较新,你需要将你 WebStorm 的 js 编译器的版本提升到ECMAScript 6,否则的话会报错,如下图所示:
    JoyMedia - Node

封装

  • 我们写一个入口文件,可以直接运行期容器,以及提供 APIs
  • 那么,这个就跟简单了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const express = require('express');
const http = require('http');
const app = express();


const port = 3000;

const v = '/apis/v1';

app.listen(port, () => {
console.log(`server starting ${port}`)
});

/*APIs 列表*/
app.use(express.static('public'));


//推荐歌单
app.use(v + "/personalized", require("./apis/personalized"));

//歌单评论
app.use(v + '/comment/playlist', require('./apis/comment_playlist'));

//获取歌单内列表
app.use(v + '/playlist/detail', require('./apis/playlist_detail'));

//获取音乐详情
app.use(v + '/song/detail', require('./apis/song_detail'));

//单曲评论
app.use(v + '/comment/music', require('./apis/comment_music'));

//获取音乐 url
app.use(v + '/music/url', require('./apis/musicUrl'));

// 获取歌词
app.use(v + '/lyric', require('./apis/lyric'))


process.on('uncaughtException', function (err) {
//打印出错误的调用栈方便调试
console.log(err.stack);
});


module.exports = app;
  • 引用 http 模块,开启 node 的默认3000 端口
  • 目前提供了上述注释里所写的 APIs
  • 每一个 API 都会单独写一个模块,以在此调用
  • 有一个地方值得注意的事
  • node 是单线程的异步 IO,这使得他在高并发方面得到很快相应速度,但是也有缺点
  • 当其中一个操作出错异常了,就会导致整个服务挂掉
  • 我在此的处理方式是:监听全局异常,捕到异常后将错误的堆栈信息打印出来,这样使得后续的操作不得进行以至于使整个服务挂掉
  • 当然,还有其他的方式来处理,可以通过引用相应的模块,来守护 node 的进程,简单的来说就是挂掉我就给你重启
  • 我觉得第二种方式不是我想要的,我是采取的第一种方式
  • 况且我还真想看看是什么错误引起的
  • 最后发现都是网络原因引起的错误 🤣🤣🤣🤣😂😂😂😂😂

运行

  • npm install
  • node app.js

查看效果

JoyMedia - Node

JoyMedia - Node

欢迎大家来听听试试看!😘 http://music.joylau.cn (当前版本 v1.3)

JoyMedia - Beta - 预览图
JoyMedia - Beta - 系统结构

在线地址

JoyMedia - Beta 预览版

项目介绍

实现目的

  • 本人经常在写代码或者没事的时候会听一些音乐
  • 以前大部分会选择本地安装客户端
  • 其中最喜欢的认为做的比较好的音乐客户端实属网易云音乐了
  • 无论是从 Mac 版的客户端,还是 IOS 版的客户端,界面都非常优美,简直是极客和码农的必备
  • 最主要是的网易云的歌曲推荐功能,很强大,我一度认为2个人的歌单相似度超过90%,就可以在一起了,这样再也不怕找不到对象了,😆
  • 但也有些问题,网易云有一些版权音乐,是无法听到的,有时候昨天还在听得音乐,今天就听不了了
  • 这就很烦了

自己的想法

  • 最初想把这个版权音乐的 mp3 地址解析出来,这样就可以直接听了
  • 恩,想法很 nice
  • 那么,照着这个想法做吧

项目实现

总体架构

  • 正如上述系统结构所示
  • 我自己有2台云服务器,一台阿里云的,另一台是腾讯云的
  • 这2台服务器,我是这样分配的: 阿里云只提供 WEB 服务,腾讯云为 WEB 访问提供各种服务
  • 当然服务器上我还跑了其他服务

阿里云服务器

  • Nginx 主要负责了 JoyMedia 的 负载均衡,在该台服务器上,我用 部署了2个 spring-boot 项目,以权重的方式配置了负载均衡,这样我在更新项目的时候可以保证另一个服务的可用性
  • 当然 Nginx 还有个反向代理的作用, upstream 配置了其他项目的访问
  • 还有台 Redis 服务了,爬到的数据会存到 Redis 了,以供 WEB 服务迅速读取,当然在有些地方不会读取 Redis ,比如单曲歌曲播放的 mp3地址的获取
  • 在最开始的时候我会先在后台解析出来再存到 Redis 里,但是发现网易云的歌曲 mp3 地址失效太快了,有时会导致播放异常,不如实时解析来的实在
  • 在比如单首歌曲的评论的获取,这个得是实时解析的

腾讯云服务器

  • 提供网易云音乐解析的是一个 Node 服务,这个 Node 服务是如何解析地址的,这个需要单独再写一篇文章,先知道这个 Node 服务是干嘛的就好
  • 然后部署了3个spring-boot服务,分别提供了各自的服务,有定时爬去网易云音乐的推荐歌单,爬取歌单的歌曲列表,爬取歌单评论
  • 由于爬到的音乐信息很快就会失效,这个服务都要定时的爬取
  • 爬取到的数据的落地存储,我是存到的MongoDB中,在这篇文章中:重剑无锋,大巧不工 SpringBoot — 整合使用MongoDB , 我说明了为什么要选择 MongoDB
  • 这3个服务爬到的数据会实时存到 Redis 中,另一方面,会异步存到 MongoDB 中,我想着这些数据或许还能做什么数据分析之类的,😄

初版完成后

等我搭建完这个服务后,发现了问题

  • 有版权控制的音乐根本解析不到 mp3 的实际地址
  • 那么我想听的音乐,听不到还是听不到,突然变得很尴尬

又有了想法

  • 一般情况下,我们在一家音乐网站上找不到自己想要的音乐,就回去其他音乐网站上找
  • 恩,就这么干
  • 网易云找不到的音乐,我就去虾米音乐,去 QQ 音乐找
  • 这2个网站的音乐我都小试了下,都是可以的
  • 于是我现在把这些功能集中在页面的搜索框中,搜索这3个音乐网站的结果,然后实施解析来播放
  • 这是我下步要做的事情

有些地方还有 BUG

  • 有些地方还是有 BUG 的,需要修复

有些地方功能还没写好

  • 比如右上角的用户登录,现在的想法是使用第三方登录,比如 QQ, 微信…,但是是登录网易云音乐呢,还是登录网站呢?
  • 要是登录网易云音乐的话,估计账号安全是个问题,而且登录接口不能频繁调用
  • 要是登录网站的,好像没什么卵用
  • 再比如左下角的歌词界面,虽然能获取到歌词,但是怎么做到歌词随着歌曲的播放实时滚动,这个现在还没有头绪…

还在继续开发中…

前言

MongoDB 安装

  • yum install mongodb-server mongodb
  • systemctl start mongod
  • whereis mongo

MongoDB 配置文件

  • 修改 bind_ip为 0.0.0.0 即可外网可访问
  • 修改 fork 为 true 即可后台运行
  • 修改 auth为 true 即访问连接时需要认证
  • 修改 port 修改端口号

开始使用

引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

配置文件

mongoDB配置
还有种配置url方式: spring.data.mongodb.uri=mongodb://name:pass@host:port/db_name

相比这种方式,我觉得第一种截图的方式要更直观一些

在 SpringBoot 项目中使用

  • 主要的一个接口MongoRepository<T,ID>,第一个是要存储的实体类,第二个参数是 ID 类型
  • 自定义一个接口实现上述接口
  • 定义实体类
  • 自定义实现类可直接注入使用
  • 默认的已经存在了增删改查的方法了,可以直接使用
  • 想要更多的功能可以在接口中实现更多的自定义
  • 下面截图所示:

自定义一个 DAO :
mongoDB-DAO

查看如何使用 :
mongoDB-method
有个 username 忘了配置了,得加上的

使用起来就是如此简单,感觉使用起来很像 mybatis 的 mapper 配置

有一些注解的配置

有时候使用起来会有一些问题

  • 在默认策略下, Java 实体类叫什么名字,生成后的表名就叫什么,但我们可能并不想这样
  • 同样的道理,有时属性名和字段也并不想一样的
  • 有时一些属性我们也并不想存到 MongoDB

注解解决这些问题

  • @Id : 标明表的 ID , 自带索引,无需维护
  • @Document : 解决第一个问题
  • @Field : 解决第二个问题
  • @Transient : 解决第三个问题

此外,还有其他的注解

可能并不常用,在此也说明下

  • @Indexed(unique = true) : 加在属性上,标明添加唯一索引
  • @CompoundIndex : 复合索引

预览

查看下刚爬的网易云官网的歌曲信息吧

![歌曲信息](//s3.joylau.cn:9000/blog/springboot-mongoDB-preview.gif)

前言

  • ZeroC Ice 的背景我就不介绍了
  • ZeroC Ice 环境安装搭建,概念原理,技术基础,这些网络上都有,再介绍的话就是copy过来了,没有多大意义,不再赘述了
  • 下面我们开始实战

开始动手

  • 首先我们需要几个ice接口文件,比如说这几个:
    Ice 文件展示
  • 我们来看一下其中一个ice文件定义的接口说明
    Ice接口文件说明
    文件里定义了5个接口,可以很明显的的看到是区间的增删改查接口
    刚好很适合我们对外提供增删改查的RESTFul API 接口
    这里在对外提供 RESTFul API 是可以很清楚的 使用 POST GET PUT DELETE
    可以说这里很好的提供了这样一个例子
  • 命令 slice2java xxx.ice 生成 java 的 client,server类
    生成的Java类
    生成的Java文件很多,这个不用管,更不必更改里面的代码内容
    你要是有兴趣的话,也可以将这些文件分为 client 和 server 分门别类的归纳好
    打开看一下,里面的代码很混乱,无论是代码风格,样式,变量命名,对于我来说,简直不忍直视
    生成的Java代码
  • 编写client类
    client类
    代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Data
@Component
@ConfigurationProperties(prefix = "ice")
public class Client {
private String adapterName;
private String host;
private int port;

private Logger _logger = LoggerFactory.getLogger(Client.class);

/**
* 执行操作
*
* @param command 命令体
* @return Result
*/
public Result execute(CommandBody command) {
Ice.Communicator ic = null;
try {
//初使化通信器
ic = Ice.Util.initialize();
//传入远程服务单元的名称、网络协议、IP及端口,获取接口的远程代理,这里使用的stringToProxy方式
Ice.ObjectPrx base = ic.stringToProxy(getStringProxy());
//通过checkedCast向下转换,获取接口的远程,并同时检测根据传入的名称获取的服务单元是否代理接口,如果不是则返回null对象
ZKRoadRangeAdminPrx interfacePrx = ZKRoadRangeAdminPrxHelper.checkedCast(base);
if (interfacePrx == null) {
return new Result(false, "Invalid proxy");
}
//把接口的方法传给服务端,让服务端执行
Result result = executeCommand(command, interfacePrx);
if (result == null) {
return new Result(false, "暂无此操作命令");
}
return result;
} catch (Exception e) {
_logger.info(e.getMessage(), e);
return new Result(false, "连接错误!" + e);
} finally {
if (ic != null) {
ic.destroy();
}
}
}

/**
* 执行操作命令
*
* @param command 命令体
* @param interfacePrx 接口
* @return ProgramResponse
*/
private Result executeCommand(CommandBody command, ZKRoadRangeAdminPrx interfacePrx) {
CommandType type = command.getCommandType();
if (type.equals(CommandType.addRange)) {
return returnMessage(interfacePrx.AddRange(command.getZkRoadRange()));
} else if (type.equals(CommandType.updateRange)) {
return returnMessage(interfacePrx.UpdateRange(command.getZkRoadRange()));
} else if (type.equals(CommandType.removeRange)) {
return returnMessage(interfacePrx.RemoveRange(command.getZkRoadRange().code));
} else if (type.equals(CommandType.getRange)) {
return new Result(true, JSONObject.toJSONString(interfacePrx.GetRange(command.getZkRoadRange().code)));
} else if (type.equals(CommandType.listRanges)) {
return new Result(true, JSONObject.toJSONString(interfacePrx.ListRanges()));
}
return null;
}


/**
* 获取配置的地址信息
*
* @return String
*/
private String getStringProxy() {
return adapterName + ":tcp -h " + host + " -p " + port;
}


private Result returnMessage(boolean result) {
return result ? new Result(true, "success") : new Result(false, "failure");
}

}
  • 需要三个配置: 适配器名,IP地址,端口号,配置在SpringBoot项目里,如下:
    ICE配置信息

再封装一下

  • 封装返回消息体
    ICE配置信息
  • 封装执行命令体
    ICE配置信息

重要

  • 调用 ice 里的接口方法:获取远程代理的 checkedCast
  • 获取远程接口的 interfacePrx 可直接调用 ice 文件里的方法
  • 服务端的 Ice 版本最好和 客户端的版本相同
  • 服务端提供服务时需要创建一个 servant ,一般的我们会在接口名后面加一个I,以此命名作为Java文件类名
  • 该servant继承 接口文件的Disp类,并重写接口中定义的方法,实现具体的业务逻辑
  • Server端创建一个适配器 adapter,将servant 放进去
  • 服务退出前,一直对请求持续监听

听首歌回忆下

实验步骤

  • 新建一个项目
  • 可先分别在码云和 GitHub 上建好仓库<可选>
  • 将项目提交的码云上
  • 项目提交到另一个仓库的时候重新 define remote <可选>
  • 之后每次先提交到本地仓库,可以根据每次提交到本地仓库的不同,来选择定义的 remote 来分别提交
  • 每次 pull 也可以选择仓库

遇到个问题

问题

  • 在我新建好码云的仓库后,提交项目,遇到 Git Pull Failed: fatal: refusing to merge unrelated histories

原因

  • 原因:git拒绝合并两个不相干的东西

解决

  • 此时在命令行输入 : git pull origin master –allow-unrelated-histories
  • 要求我输入提交信息
  • 输入完成后,按一下Esc,再输入:wq,然后回车就OK了
  • 再回来提交就可以了

系统工具

  • BetterZip : mac上面的最好的解压工具
  • CHM View : 查看chm类型的开发文档
  • Easy New File Free : 右击桌面,可以像win一样新建文件
  • Bartender 2 : 任务栏menu图标整理
  • iStat Menus : 系统网速、cpu、内存监控工具
  • SwitchResX : 外接显示器,调节DPI
  • Go2Shell : 在finder的任意文件夹下打开终端
  • Aria2GUI : 突破百度限速
  • Alfred 3 : 效率神器,谁用谁知道
  • PDF Expert : 查看pdf
  • 远程桌面连接 : mac电脑上远程连接windows,网址: https://rink.hockeyapp.net/apps/5e0c144289a51fca2d3bfa39ce7f2b06 (2017年10月26日加)

播放器

  • 网易云音乐 :这个必备啊
  • 优酷 :这个可以免费看1080P视频,没广告,有时候出抽风的时候还可以看会员视频
  • OBS : 视频直播、录制软件
  • Movist : 视频播放器,支持的格式很多

小工具

  • CleanMyMac 3 : 清理mac电脑垃圾
  • ShadowsocksX : 翻墙必备
  • TeamView : 桌面远程软件
  • MacDown : 开源的markdown编辑器
  • Path Finder : Finder增强版
  • Parallels Desktop : 虚拟机
  • FileZilla : ftp工具
  • Foxmail : 邮箱客户端
  • Folx : 下载工具

开发工具

  • FireFox : 火狐
  • Google Chrome : 必备
  • IntelliJ IDEA : 必备IDE
  • WebStorm : web开发必备
  • DataGrip : 数据库管理软件
  • Navicat Premium : 已经使用习惯的MySQL连接工具,也支持其他数据库
  • XShell : SSH远程连接工具,我还是比较喜欢终端下的ssh命令连接,虽然有一个家族的系列产品
  • Sublime Text3 : 文本编辑器
  • Beyond Compare : 文本比较工具
  • GitHub Desktop : github GUI客户端
  • rdm : redis可视化GUI界面
  • HBuilder : h5开发工具
  • iTerm : 终端

自己暂时使用的工具都已归纳出来,以后有新的好用的工具,会加上的,Mac下大部分工具都是收费的,你可以偷偷点一下 xclient.info

官方视频

开始拆箱

MacBook Pro

先来看一下刚拿到手的包装是什么样的

一台主机

我在官网订购了一个 USB-typeC 转 USB 的转接口

那个小盒子就是

MacBook Pro

打开主机纸盒

MacBook Pro

掰开这个直接就可以把里面的主机盒抽出来,很方便

两边都是这样设计的

MacBook Pro

就2样东西

都摆放好了

准备拿剪刀拆开

MacBook Pro

来一张侧面照

MacBook Pro

拆开盒子保护膜

打开镂空设计的上盖,看到我们的主机真容

MacBook Pro

这样一看,真的很薄,起码比我以前用过得笔记本都要薄多了

MacBook Pro

2端都是 USB-C 接口的充电线

适配器感觉好大啊

MacBook Pro

靠近点看下USB-C的充电线

MacBook Pro

然后就什么都没有了

底下的盒子也打不开

MacBook Pro

苹果的LOGO贴纸

说明书

三包凭证

MacBook Pro

开始正式拆开主机的包装纸

MacBook Pro

一睹真容
15.6寸的

进入系统

MacBook Pro

盖子一打开就开机了

屏幕与键盘之间隔了一张纸

让我们拿开他

MacBook Pro

很快就进入了系统

MacBook Pro

重新设计的蝴蝶键盘

键程很短

按键很紧凑

MacBook Pro

来一张键盘的整体照

上面是全新的 Multi-Touch Bar ,替换了以前的一排功能按键,许多mac内置的应用在Touch Bar上都有支持

MacBook Pro

触摸板的占比实在是太大了

看我一只手放上去,刚好差不多

手有点丑,请忽略

MacBook Pro

迫不及待的想进入系统尝试一下了

先来连接家里的WIFI

MacBook Pro

老套路了

都是下一步

再下一步

MacBook Pro

在电源键上提供了和iPhone上一个的指纹支持

MacBook Pro

来录入我自己的指纹

不知道用的是什么材料,在这个TouchBar上面滑来滑去很舒服,很有感觉

MacBook Pro

正在设置指纹

稍等一下

MacBook Pro

终于正式进入系统了

屏幕的显示效果很震撼

特效动画的帧数很高,给人感觉很流畅

系统信息

MacBook Pro

看一下系统信息

MacBook Pro

显示器信息

2G独立显存

个人感受

使用它也有一周多了,说一下自己的整体感受吧

  • Retina显示器的显示效果真的很好,真是惯坏了眼睛,现在再去看普通的显示器,就感觉有很强的颗粒感
  • macOS High Sierra字体渲染的很棒,系统中有很多适合编程的字体,在 IntelliJ IDEA 中编码很爽
  • 更大的分辨率能看到更多的内容
  • 系统安装软件什么的很方便,没有想Windows下那么碎片化
  • Multi-Touch Bar 有很多有意思的功能,除了官方宣传的和MacOS本身自带的,想滑动查看照片,添加emoji小表情,控制亮度。。。之类的,大量第三方的软件也进行了适配,网易云音乐,搜狗输入法就适配的很不错
  • 系统触摸板真的是Windows平台无法比拟的,有很多手势,编码什么的,完全可以不用鼠标
  • 键盘旁边2个喇叭的音质效果很震撼,而且声音特别大,看电影,听音乐很有感觉
  • 耗电也比Windows系统的笔记本少多了,充满电的话,就拿我平时工作情况来说,开多个IDEA,起多个服务,多个浏览器,多个编辑器。。。什么什么的,大概能撑个8,9个小时,上班一天不充电….
  • 颜值好,很符合现代化审美

缺点也还是有的

  • 太贵
0%