BigXia

BigXia 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

BigXia 发布了文章 · 2019-06-12

配置 Supervisor,自动重启常驻进程

问题:PHP 常驻进程经常出现死掉的情况;
解决方案:使用 Supervisor, 当进程死掉时,可以重启,不局限于PHP进程,任何进程,都可以用Supervisor 进行重启。

1、安装

安装python应该就可以了,python 安装包自带 Supervisor
或者用pip安装Supervisor进程管理工具
参考链接:https://www.cnblogs.com/Dicky-Zhang/p/6171954.html

2、配置

2.1 生成配置文件
在Mac下,由于安全规则,默认不能在/etc/下增加配置,
不过可以在/usr/local/etc目录下创建配置文档,效果是一样的。

sudo echo_supervisord_conf > /usr/local/etc/supervisord.conf

可以使用上述命令,生成配置文件。
其中,echo_supervisord_conf命令可以打印所有的默认配置信息;

2.2 配置后台管理页面,解开下面的注释就可以了
[inet_http_server]
port=127.0.0.1:9001 
username=user
password=123

2.3 配置进程重新启动时的命令,在配置文件中,新增下面几行代码
[program:apple]
process_name=%(program_name)s_%(process_num)02d
command=php artisan apple
autostart=true
autorestart=true
;user=user
numprocs=8
redirect_stderr=true
stdout_logfile=/var/log/supervisor.log

3、启动supervisor的守护进程

sudo supervisord -c /usr/local/etc/supervisord.conf

其中,需要用 -c 参数指定配置文件。

4、管理常驻进程

supervisor守护启动成功后,要自动重启的命令就已经正常运行了。
通过本地浏览器,进入管理后台:http://127.0.0.1:9001/,打开效果图如下
可以在后台看到进程的相关信息,如运行了多长时间
也可以,做相关操作,如停止进程、重启进程、查看log、清除log

clipboard.png

查看原文

赞 0 收藏 0 评论 0

BigXia 发布了文章 · 2019-03-18

laravel 简单入门

0、最少必要知识


0.1 route 配置
/routes/web.php 文件
    Route::get('/','SitesController@index');
    
0.2 在 controller 的 index 方法体中与视图交互
// url 视图路径, 默认 prefix 为 resources/views/
// data 可传数组/ 二维用 compact('data')
return view(url, data)

0.3 视频里的流程控制与循环
    0.3.1 @if       @foreach
    0.3.2 @endif    @endforeach

0.4 Migration 数据库一键迁移,数据库生成通过migration进行

0.5 Eloquent ORM 对象关系映射,将数据表映射为对象

0.6 model 的两种常见用法
    0.6.1 setAttribute, 增加和修改用
    0.6.2 queryScope, 查询设置过滤条件
    
0.7 表单验证,可以通过 Request 类完成

0.8 编辑文章,Form::model()

0.9 Restful API,操作与之对应的 HTTP 传值方式
    增 post
    删 delete 
    改 patch/put
    查 get

1、从安装到启动(mac)


1.1 安装,版本号 Laravel Framework 5.8.3
    1.1.1 使用 composer 全局安装,会生成 laravel 命令
        composer global require "laravel/installer"
    
    1.1.2 Debug, 运行 laravel 命令
        如果找不到命令,创建软链接
        ln -s ~/.composer/vendor/bin/laravel /usr/local/bin/laravel

1.2 创建项目
    1.2.1 方法一:使用 laravel 安装
        laravel new my-project
    1.2.1 方法二:使用 composer 安装
        composer create-project laravel/laravel my-project
    
1.3 启动
    1.3.1 方法一:使用 php 内置服务器
        php -S localhost:8888 -t public
    1.3.2 方法二:使用 laravel 提供的命令行工具
        php artisan serve

2、路由及其他目录

| - routes
| - - web.php   web 的路由,可指指向控制器方法
| - app
| - - Http
| - - - Controllers
| - - - - SitesController   控制器
| - resources
| - - views 视图文件
| - env     配置参数,如 mysql 相关配置

3、可以将路由交给控制器,生成控件器


web.php 文件里的写法如下:

3.1 Route::get('/','SitesController@index');
3.2 注册标准路由
    Route::resource('articles', 'ArticlesController');
3.3 查看当前路由
    php artisan route:list

4、向视图传递变量,blade 模板的用法


4.1 controller 文件的方法中
    4.1.1 方法一,传键值
        view('index')->with(key, value);
    4.1.2 方法二,传一维数组,在模块中可以直接用其中的元素
        view('index', $data)    // $data=['name'=>'Pual', 'sex'=>'male']
    
    4.1.3 方法三,传二维数组,视图中需要先遍历
        view('index', compact($data))   

4.2 index.blade.php 在视图文件中使用变量
    4.2.1 转义,相当于 php 的单引号,类似于 Vue
        {{ $key }} 
    4.2.2 不转义,相当于 php 的双引号
        {!! $key !!}
    

5、模板


5.1 定义视图模板 app.blade.php

    @yield('content')


5.2 继承模板
    @extends('app')
    @section('content')
    This is a test
    @stop

6、模板判断

@if($name == 'Paul')
    This is Paul
@else
    This is not Pual
@endif

7、循环输出


@foreach($people as $person)
    {{$person}}
@endforeach

8、配置


8.1 文件位置
    .env

9、Migration 数据库的版本控制,针对数据的迁移


9.1 同步已经有的表
    php artisan migrate 

9.2 创建表
    php artisan make:migration create_articles_table --create='articles'   

9.3 为已有表添加字段
    php artisan make:migration add_intro_column_to_articles --table=articles

10、Eloquent 是 laravel 的 ORM(对象关系映射)


10.1 创建model
    php artisan make:model Articles

10.2 model 关联 数据库

11、简单 Blog,的几个知识点

11.1 视图中url链接,拼接跳转链接,并带参数
    {{ url('url', $para) }}
11.2 接收参数
    11.2.1 设置带参路由,web.php ,关键点:{id} 
        Route::get('articles/{id}', 'ArticlesController@show');
    11.2.2 在 controller 中接收参数
        function show($id){...}

12、laravel Forms 使用

12.1 安装 composer 依赖包,laravel 官方依赖包
     composer require laravelcollective/html
12.2 配置 config -> app.php
    12.2.1 prividers 数组追加
        Collective\Html\HtmlServiceProvider::class,
    12.2.2 aliases 数组追回
        'Form'=>Collective\Html\FormFacade::class,
        'Html'=>Collective\Html\HtmlFacade::class
 
12.3 使用
    12.3.1 表单开发提交数据
        {!! Form::open(['url'=>'orders']) !!}
        {!! Form::close() !!}
    12.3.2 表单绑定模型
        {!! Form::model($order,['method'=>'patch', 'url'=>'orders/'.$order->id]) !!}
        {!! Form::close() !!}
        
12.4 Form 静态方法有哪些?
    参考网址:https://github.com/LaravelCollective/laravel-docs/blob/5.6/html.md
        

13、setAttribute 与 queryScope,特定作用的函数

13.1 setAttribute 对数据库相关字段做预处理,插入或者更新数据的时候用
        public function setPublishedAtAttribute($data)
        {
            $this->attributes['published_at'] = Carbon::createFromFormat('Y-m-d', $data);
    
        }
12.2 queryScope 字段过滤条件,在查询时使用
    public function scopePublished($query)
    {
        $query->where('published_at', '<=', Carbon::now());
    }

14、Carbon 类 laravel 中默认的时间处理类

15、表单验证

15.1 使用 Request 类
    15.1.1 创建 Request 类
        php artisan make:request CreateOrderRequest
    15.1.2 rules 验证
        public function rules()
            {
                return [
                    // | 分隔符,min:3 最少3个长度
                    'thing'=>'required|min:3',
                    'price'=>'required',
                    'note'=>'required',
                ];
            }
    
15.2  validate 验证
    $this->validate($request, ['title'=>'required', 'content'=>'required']);
    

16、编辑文章


16.1 显示某条记录
    {!! Form::model($order,['method'=>'patch','url'=>'orders/'.$order->id]) !!}
    {!! Form::close() !!}
16.2 提交修改
    method:patch 相当于 put,专门用来做局部修改
    

17、用户注册和登录


17.1 安装 Auth 模块
    php artisan make:auth
17.2 访问登录
    url/login

参考教程:https://study.163.com/course/...

查看原文

赞 0 收藏 0 评论 0

BigXia 赞了文章 · 2019-03-12

高级开发人员必备技术:MQ

也许在你们公司从没有使用过MQ,也不知道这东西是用来干什么的,但是一旦你进入大公司你就会发现,这东西处处可见。今天就来说说MQ方面的东西,我公众号有activemq的 demo,大家可以自己去看看。

什么是MQ

Message Queue简称MQ,中文消息队列。

  • “消息”是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。
  • 消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。

你可以把它理解为一个中间件,帮你完成多个系统或应用间的消息传递。

为什么要使用MQ

首先它有3个核心,解耦,异步,削峰,因此我们可以想到以下使用场景:

  • 你的系统要和多个系统发生关系,别的系统要从你这获取一些数据,今天A系统和你要这样的数据,明天B系统说你的数据有问题,后天C系统让你加个别的数据。你一个人要维护解决很多问题,忙得不可开交。而有了MQ,你就可以通过Pub/Sub 发布订阅消息这么一个模型,系统就跟其它系统彻底解耦了。只要把消息放到队列里,其它系统就自己去处理自己需要的数据,自己不再考虑该给谁发送数据。比如:下完订单,不再去通知库存做同步处理。把该物品的信息放在队列中,库存自己去选择什么时候去处理计算自己的库存。
  • 还是上面的例子,之前的流程,user浏览器端发起购物请求3ms,订单系统处理数据库300ms,之后库存系统处理数据库300ms,这样同步情况下加起来就要603ms。即使前端有加载提示框,等待时间超过300ms,人眼是能感受到这种延迟的,体验很不好,速度越快才能留住user。现在使用MQ采用异步消息处理,假如消息放进队列需要3ms,那么最终的响应时间是3+3=6ms,对于创建订单和库存计算user并不关心,这样极大的提高了响应时间。
  • 一个大的网站或是应用,总会迎来访问量的高峰,可能是营销活动突然带来的大流量,或是节假日。比如双十一,购物人数突然猛增,并发数提高,数据库的压力突然增大,超出了每秒钟的处理能力,就会导致网站瘫痪。使用mq后,数据库可以不必立马处理这么多的请求,可以自己选择能承受的消息慢慢去处理。所有的消息积压在队列中,而不是同时积压到数据库。加入队列中积压了1亿条数据,而我的数据库只能每秒处理100万条数据,那我就每秒从队列中取出100万条来处理,始终不会超出阈值,这样数据库就不会被挤垮。把峰值慢慢消耗。

现在想想你为什么没有使用到mq吧?或是考略使用mq

使用后带来的威胁

任何事物都有它的两面性,既然有优点那也有缺点:

  • 系统可用性降低

万一mq挂了,队列里面的数据没有了,其它系统数据还没处理完,这可咋整?

  • 系统的复杂度提高了

你用个mq是爽了,其它系统也要对应的修改自己的系统,来消费队列中的消息。硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。

  • 一致性问题

你订单系统操作成功了,但是库存系统却失败了,这样导致了数据的不一致。

所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。

主流的MQ产品

  • Kafka
  • ActiveMQ
  • RabbitMQ
特性ActiveMQRabbitMQRocketMQKafka
单机吞吐量万级,比 RocketMQ、Kafka 低一个数量级同 ActiveMQ10 万级,支撑高吞吐10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景
topic 数量对吞吐量的影响topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topictopic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源
时效性ms 级微秒级,这是 RabbitMQ 的一大特点,延迟最低ms 级延迟在 ms 级以内
可用性高,基于主从架构实现高可用同 ActiveMQ非常高,分布式架构非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性有较低的概率丢失数据基本不丢经过参数优化配置,可以做到 0 丢失同 RocketMQ
功能支持MQ 领域的功能极其完备基于 erlang 开发,并发能力很强,性能极好,延时很低MQ 功能较为完善,还是分布式的,扩展性好功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用

上面表格来自:https://github.com/doocs/adva...

推荐使用

  • 早期大家都在使用ActiveMQ ,适合小型项目,如果你尝试使用MQ,你可以选择。
  • RabbitMQ社区活跃度比较高,开源,有问题可以在社区寻求帮助。但是底层使用了erlang 语言,不是小公司又能力掌控的 。
  • RocketMQ 阿里出品,是用的中国公司比较多,经历过使用场景的考验,且自家产品也在用,不用担心。但是社区活跃度不高。推荐使用。
  • 如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。
  • 所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
查看原文

赞 6 收藏 4 评论 0

BigXia 发布了文章 · 2019-03-12

php redis 扩展的安装

楔子:本文以 redis 的扩展安装为例,其他扩展也可以按此方法安装。

1、下载源码,-c 表示断点续传

wget -c https://nodeload.github.com/nicolasff/phpredis/zip/master

2、解压

tar -zxvf master

3、进入解压后的源码目录

cd phpredis-master

4、侦测环境,并生成相应的配置文件(包括 configure)phpize 为当前环境下的命令,与 php 是同一级别的

sudo phpize

5、检查配置与依赖

sudo ./configure

6、编译

*
* 会在 modules 目录下生成 redis.so 文件,
* 复制到 extension_dir 目录下,扩展会成功

sudo make

7、安装到相应的配置目录

sudo make install
查看原文

赞 1 收藏 1 评论 0

BigXia 发布了文章 · 2019-03-10

入门 Vue,只需要知道这 4 点就够了

1、说那四点前,先用 npm 构造项目,并看看目录结构。

1.1 用 npm 全局安装 vue 的命令行工具

npm install --global vue-cli

1.2 创建一个基于 webpack 模板的新项目
vue init webpack my-project

1.3 进行项目目录,并运行
cd my-project; vue run dev

1.4 目录结构
|
| - build : 项目构建(webpack)相关代码
| - config : 配置目录,可以在 index.js 文件中修改端口号
| - node_modules : npm 依赖模块
| - src : 要开发的目录,要做的事情基本都在这个目录里。包含:
| - - assets : 放图片
| - - components : 组件文件
| - - App.vue : 项目入口文件
| - - main.js : 项目的核心文件
| - static : 静态资源目录,如图片、字体
| - .xxxx 文件 : 配置文件、包括语法配置,git 配置等
| - index.html : 首页入口文件
| - package.json : 项目配置文件
| - README.md : 项目说明文档,markdown 模式

2、Vue 起步

2.1 Vue 实例

var vue = new Vue({
    var data = {msg: 'This is data'};

    el: '#app',
    data:data,
    methods:{
        details: function(){
            return "This is a method"
        }
    }

    document.write(vue.$data === data); // true
});

2.2 使用特点
    2.2.1 内容格式为 Json 格式;
    2.2.2 Vue 实例三部分构成
        第一部分:绑定元素字段 el:
            el: '选择器'
        第二部分:定义属性模块 
            data:{
                name: value
            }
        第三部分:定义方法模块 methods:
            methods:{
                // 变量函数
                方法名: 方法体
            }
    2.2.3 {{}} 
        用来标识要使用的变量或者方法
    2.2.4 可以通过变量来,访问 Vue 实例中的成员,需要用 $ 符号声明
        vue.$el

掌握这四个特点,就入门 Vue 了。

查看原文

赞 1 收藏 1 评论 0

BigXia 赞了文章 · 2019-03-08

版本控制工具——Git常用操作(下)

本文由云+社区发表

作者:工程师小熊

摘要:上一集我们一起入门学习了git的基本概念和git常用的操作,包括提交和同步代码、使用分支、出现代码冲突的解决办法、紧急保存现场和恢复现场的操作。学会以后已经足够我们使用Git参加协作开发了,但是在开发的过程中难免会出错,本文主要介绍版本控制的过程中出错了的场景,以及Git开发的一些技巧,让我们用的更流畅。

上集回顾:

  • Git的基本概念
  • 一个人使用Git时的代码版本控制--(提交、拉代码、分支操作)
  • 多人合作时的代码版本控制--(合并冲突、暂存代码)

本文核心:

  • 后悔药-各种后悔操作(撤消commit,回滚,回退远程仓库等)
  • 哎呀,提交的时候漏了文件
  • tag操作
  • git忽略不想提交的文件

后悔药

撤消当前commit

如果你发现刚刚的操作一不小心commit了,所幸你还没有推送到远程仓库,你可以用reset命令来撤消你的这次提交。

reset命令的作用:重置HEAD(当前分支的版本顶端)到另外一个commit。

我们的撤消当前提交的时候往往不希望我们此次提交的代码发生任何丢失,只是撤消掉commit的操作,以便我们继续修改文件。如果我们是想直接不要了这次commit的全部内容的任何修改我们将在下一小节讨论。

来,我们先说一句蠢话来diss老板

$ touch to_boss.txt

$ echo 'my boss is a bad guy!' > to_boss.txt

$ git add to_boss.txt

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   to_boss.txt

$ git commit -m "[+]骂了我的boss"
[master 3d113a7] [+]骂了我的boss
 1 file changed, 1 insertion(+)
 create mode 100644 to_boss.txt
  • 创建to_boss.txt文件,并向其写入了my boss is a bad guy!
  • add然后status查看新文件已经加入跟踪
  • commit提交了这次的修改

好了,刚刚我们“不小心”diss了我们的老板,要是被发现就完了,所幸还没有push,要快点撤消这些提交,再换成一些好话才行。

我们使用以下命令:

$ git reset --soft head^

$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   to_boss.txt

$ cat to_boss.txt
my boss is a bad guy!

$ echo 'my boss is a good boy!'
my boss is a good boy!

$ echo 'my boss is a good boy!' > to_boss.txt

$ cat to_boss.txt
my boss is a good boy!

$ git add to_boss.txt

$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   to_boss.txt
    
$ git commit -m "[*]夸了我的boss"
[master 8be46aa] [*]夸了我的boss
 1 file changed, 1 insertion(+)
 create mode 100644 to_boss.txt
  • git reset --soft head^撤消了本次提交,将工作区恢复到了提交前但是已经add的状态
  • to_boss.txt的内容改成了my boss is a good boy!
  • add然后commit提交

好了,有惊无险,这就是撤消commit的操作。另一种情况是如果你想撤消commit的时候支持舍弃这次全部的修改就把git reset --soft head^改成git reset --hard head^,这样你本地修改就彻底丢掉了(慎用),如果真用了想找回来怎么办?见救命的后悔药。

当然了,你只要开心不加softhard参数也是安全的(相当于使用了--mixed参数),只不过是撤消以后你的本次修改就会回到add之前的状态,你可以重新检视然后再做修改和commit

回退远程仓库

要是我们做的更过分一点,直接把这次commit直接给push怎么办?要是被发现就全完了,我们来看看github上的远程仓库。

imgupload successful

完了,真的提交了(我刚刚push的)让我们冷静下来,用撤消当前commit的方法先撤消本地的commit,这次我们来试试用hard参数来撤消

$ git reset --hard head^
HEAD is now at 3f22a06 [+]add file time.txt

$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean

$ git push origin master --force
Total 0 (delta 0), reused 0 (delta 0)
To github.com:pzqu/git_test.git
 + 3d113a7...3f22a06 master -> master (forced update)
  • 使用git reset --hard head^回滚到上一个commit
  • 使用git status查看现在的工作区情况,提示Your branch is behind 'origin/master' by 1 commit,代表成功表了上一次的提示状态,nothing to commit, working tree clean代表这次的修改全没了,清理的算是一个彻底。如果还想找回来怎么办,我们还真是有办法让你找回来的,见救命的后悔药。
  • git push origin master --force 命令强制提交到远程仓库(注意,如果是在团队合作的情况下,不到迫不得已不要给命令加--force参数) 让我们看看github

imgupload successful

真的撤消了远程仓库,长舒一口气。

暂存区(Stage)到工作区(Working Directory)

如果我们刚刚执行了git reset --soft或者add等的操作,把一些东西加到了我们的暂存区,比如日志文件,我们就要把他们从暂存区拿出来。

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   mysql.log
    
$ git reset -- mysql.log

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    mysql.log

nothing added to commit but untracked files present (use "git add" to track)
  • status查看暂存区,里面有一个mysql.log被放进去了
  • git reset -- mysql.logmysql.log取出来
  • status可以看到真的取出来了 然后如果不要想这个文件的话再rm掉就好啦,但是如果这些文件每次自动生成都要用这种方式取出暂存区真的好累,我们可以用 git忽略不想提交的文件

回滚文件到某个提交

当我们想要把某个文件任意的回滚到某次提交上,而不改变其他文件的状态我们要怎么做呢?

我们有两种情况,一种是,只是想在工作区有修改的文件,直接丢弃掉他现在的修改;第二种是想把这个文件回滚到以前的某一次提交。我们先来说第一种:

取消文件在工作区的修改

$ cat time.txt
10:41

$ echo 18:51 > time.txt

$ git status
On branch master
Your branch is up to date with 'origin/master'.

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

    modified:   time.txt

no changes added to commit (use "git add" and/or "git commit -a")

$ cat time.txt
18:51

$ git checkout -- time.txt

$ cat time.txt
10:41
  • 更新time.txt的内容,可以status看到他发生了变化
  • git checkout -- time.txt , 取消这次在工作区的修改,如果他已经被add加到了暂存区,那么这个命令就没有用了,他的意思是取消本次在工作区的修改,去上一次保存的地方。如果没有add就回到和版本库一样的状态;如果已经加到了暂存区,又做了修改,那么就回加到暂存区后的状态将文件回滚到任意的版本我们这里说的把文件回滚到以前的某个版本的状态,完整的含义是保持其他文件的内容不变,改变这个文件到以前的某个版本,然后修改到自己满意的样子和做下一次的提交。

核心命令

git checkout [<options>] [<branch>] -- <file>...

我们还是用time.txt这个文件来做试验,先搞三个版本出来,在这里我已经搞好了,来看看:

版本1,time.txt内容00:50

commit 35b66ed8e3ae2c63cc4ebf323831e3b917d2b1d4 (HEAD -> master, origin/master, origin/HEAD)
Author: pzqu <pzqu@example.com>
Date:   Sun Dec 23 00:51:54 2018 +0800
    [*]update time to 00:50

版本2,time.txt内容18:51

commit 856a74084bbf9b678467b2615b6c1f6bd686ecff
Author: pzqu <pzqu@example.com>
Date:   Sat Dec 22 19:39:19 2018 +0800
    [*]update time to 18:51

版本3,time.txt内容10:41

commit 3f22a0639f8d79bd4e329442f181342465dbf0b6
Author: pzqu <pzqu@example.com>
Date:   Tue Dec 18 10:42:29 2018 +0800
    [+]add file time.txt

现在的是版本1,我们把版本3检出试试。

$ git checkout 3f22a0639f8d -- time.txt

$ cat time.txt
10:41

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   time.txt
  • 使用checkout+commit id+-- filename的组合,横跨版本2把历史版本3的time.txt搞出来了
  • 查看状态,time.txt被改变了

我们来把time.txt恢复到版本1,同样的方法,因为版本1是上一次提交我们可以省略掉版本号

$ git checkout -- time.txt

$ cat time.txt
00:50

看到了吧!只要用git checkout commit_id -- filename的组合,想搞出哪个文件历史版本就搞出哪个。

到了这里,你可能会很懵比,resetcheckout命令真的好像啊!都可以用来做撤消

  • checkout语义上是把什么东西取出来,所以此命令用于从历史提交(或者暂存区域)中拷贝文件到工作目录,也可用于切换分支。
  • reset语义上是重新设置,所以此命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引。也用来在从历史仓库中复制文件到索引,而不动工作目录。

还想不通可以给我发邮件:pzqu@qq.com

救命的后悔药

来到这里我已经很清楚的你的现况了,你的代码丢了现在一定非常的着急,不要慌,总是有办法找回他们的。但是前提是要保证你的项目根目录下.git文件夹是完整的,要是手动删除了里面的一些东西那就真完了。还要保证一点,你的代码以前是有过git追踪的,最少add

找回你丢失的历史记录

Git提供了一个命令git reflog用来记录你的每一次命令,贴个图吧直观点:

imgupload successful

  • 有没有发现,git reflog里的全部都是和改变目录树有关的,比如commit rebase reset merge,也就是说一定要有改变目录树的操作才恢复的回来
  • 像add这种操作就不能恢复了吗?那肯定不是,只是要用更麻烦点的方式来恢复
  • git log是一样的,也可以看到所有分支的历史提交,不一样的是看不到已经被删除的 commit 记录和 reset rebase merge 的操作 我们可以看到git reflog前面的就是commit id,现在我们就可以用之前介绍过的方法来回滚版本了,撤消当前commit
$ git reset --hard 856a740
HEAD is now at 856a740 [*]update time to 18:51

$ git log -1
commit 856a74084bbf9b678467b2615b6c1f6bd686ecff (HEAD -> master)
Author: pzqu <pzqu@example.com>
Date:   Sat Dec 22 19:39:19 2018 +0800

    [*]update time to 18:51
 
$ git reset --hard 35b66ed
HEAD is now at 35b66ed [*]update time to 00:50

$ git log -2
commit 35b66ed8e3ae2c63cc4ebf323831e3b917d2b1d4 (HEAD -> master, origin/master, origin/HEAD)
Author: pzqu <pzqu@example.com>
Date:   Sun Dec 23 00:51:54 2018 +0800

    [*]update time to 00:50

commit 856a74084bbf9b678467b2615b6c1f6bd686ecff
Author: pzqu <pzqu@example.com>
Date:   Sat Dec 22 19:39:19 2018 +0800

    [*]update time to 18:51
  • 根据git reflog返回的结果,用git reset --hard commit_id回退到856a740这个版本
  • git log -1看近一行的日志,可以看到目前就在这了
  • 再根据git reflog的结果,用git reset --hard 35b66ed跑到这次提交
  • git log -2看到两次提交的日志,我们就这么再穿梭过来了,就是这么爽 但是我们如果只是想把此提交给找回来,恢复他,那还是不要用reset的方式,可以用cherry-pick或者merge来做合并

找回忘记提交的历史记录

你之前没有commit过的文件,被删除掉了,或者被reset --hard的时候搞没了,这种情况可以说是相当的难搞了,所幸你以前做过add的操作把他放到过暂存区,那我们来试试找回来,先来创建一个灾难现场

$ echo 'my lose message' > lose_file.txt

$ git add lose_file.txt

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   lose_file.txt

$ git reset --hard 35b66ed8
HEAD is now at 35b66ed [*]update time to 00:50

$ git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

$ ls
README.md      need_stash.txt share_file.txt time.txt
  • 创建一个叫lose_file.txt的文件并写入内容my lose message,并把他加到暂存区
  • git reset --hard 35b66ed8用丢弃一切修改的方式来使现在的工作区恢复到35b66ed8版本,因为还没提交所以也就是恢复到当前的(head)版本。
  • 我们用statusls再看,这个叫lose_file.txt的文件真的没了,完蛋了,第一反应用刚刚学到的命令git reflow会发现根本就不好使

核心命令:git fsck --lost-found,他会通过一些神奇的方式把历史操作过的文件以某种算法算出来加到.git/lost-found文件夹里

$ git fsck --lost-found
Checking object directories: 100% (256/256), done.
Checking objects: 100% (3/3), done.
dangling blob 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
dangling commit fdbb19cf4c5177003ea6610afd35cda117a41109
dangling commit 8be46aa83f0fe90317b0c6b9c201ad994f8caeaf
dangling blob 11400c1d56142615deba941a7577d18f830f4d85
dangling tree 3bd4c055afedc51df0326def49cf85af15994323
dangling commit 3d113a773771c09b7c3bf34b9e974a697e04210a
dangling commit bfdc065df8adc44c8b69fa6826e75c5991e6cad0
dangling tree c96ff73cb25b57ac49666a3e1e45e0abb8913296
dangling blob d6d03143986adf15c806df227389947cf46bc6de
dangling commit 7aa21bc382cdebe6371278d1af1041028b8a2b09

这里涉及到git的一些低层的知识,我们可以看到这里有blob、commit、tree类型的数据,还有tag等类型的。他们是什么含义呢?

imgupload successful

  • blob组件并不会对文件信息进行存储,而是对文件的内容进行记录
  • commit组件在每次提交之后都会生成,当我们进行commit之后,首先会创建一个commit组件,之后把所有的文件信息创建一个tree组件,所以哪个blob代表什么文件都可以在tree 里找到 我们来看看怎么恢复刚刚不见了的lose_file.txt文件,在上面执行完git fsck --lost-found命令,返回的第一行blob我们看看他的内容
git show 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
my lose message

git show 7f5965523d2b9e850b39eb46e8e0f7c5755f6719 > lose_file.txt

$ ls
README.md      lose_file.txt  need_stash.txt share_file.txt time.txt
  • 看到没有,就是我们丢失的文件内容,这样就找回来了! 我们再来看看commit tree的内容$ git cat-file -p fdbb19cf4c5177003ea6610afd35cda117a41109 tree 673f696143eb74ac5e82a46ca61438b2b2d3bbf4 parent e278392ccbf4361f27dc338c854c8a03daab8c49 parent 7b54a8ae74be7192586568c6e36dc5a813ff47cf author pzqu <pzqu@example.com> 1544951197 +0800 committer pzqu <pzqu@example.com> 1544951197 +0800 Merge branch 'master' of github.com:pzqu/git_test $ git ls-tree 3bd4c055afedc51df0326def49cf85af15994323 100644 blob c44be63b27a3ef835a0386a62ed168c91e680e87 share_file.txt
  • git cat-file -p可以看到commit的内容,可以选择把这个commit合并到我们的分支里,还是reset merge rebase cherry-pick这些命令来合commit
  • git ls-tree列出tree下面的文件名和id的记录信息,然后就可以根据这些来恢复文件了

后记:

如果你发现执行git fsck --lost-found的输出找不到你想要的,那么在执行完git fsck --lost-found后会出现一堆文件 在 .git/lost-found 文件夹里,我们不管他。可以用以下命令来输出近期修改的文件

$  find .git/objects -type f | xargs ls -lt | sed 3q
-r--r--r--  1 pzqu  staff    32 12 23 12:19 .git/objects/7f/5965523d2b9e850b39eb46e8e0f7c5755f6719
-r--r--r--  1 pzqu  staff    15 12 23 01:51 .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
-r--r--r--  1 pzqu  staff   162 12 23 00:51 .git/objects/35/b66ed8e3ae2c63cc4ebf323831e3b917d2b1d4

$ git cat-file -t 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
blob

$ git cat-file -p 7f5965523d2b9e850b39eb46e8e0f7c5755f6719
my lose message

$ git cat-file -t b2484b5ab58c5cb6ecd92dacc09b41b78e9b0001
tree

$ git cat-file -p b2484b5ab58c5cb6ecd92dacc09b41b78e9b0001
100644 blob f9894f4195f4854cfc3e3c55960200adebbc3ac5    README.md
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    need_stash.txt
100644 blob 83f50ec84c00f5935da8089bac192171cfda8621    share_file.txt
100644 blob f0664bd6a49e268d3db47c508b08d865bc25f7bb    time.txt
  • 这里用find .git/objects -type f | xargs ls -lt | sed 3q返回了近3个修改的文件,想要更多就改3q这个数值,比如你想输出100个就用100q
  • git cat-file -t 7f5965523d2b9e850b39eb46e8e0f7c5755f6719 就能看见文件类型 把最后一个/去掉 复制从objects/ 后面的所有东西放在-t后面
  • git cat-file -p id就能看见文件内容,是不是很爽

漏提交

有时候会碰到我们已经commit但是有修改忘记了提交,想把他们放在刚刚的commit里面,这种时候怎么做呢?

$ git log --name-status --pretty=oneline -1
35b66ed8e3ae2c63cc4ebf323831e3b917d2b1d4 (HEAD -> master, origin/master, origin/HEAD) [*]update time to 00:50
M       time.txt

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   lose_file.txt
    new file:   test_amend.txt
    
$ git commit --amend --no-edit
[master 31cc277] [*]update time to 00:50
 Date: Sun Dec 23 00:51:54 2018 +0800
 3 files changed, 2 insertions(+), 1 deletion(-)
 create mode 100644 lose_file.txt
 create mode 100644 test_amend.txt
 
$ git log --name-status --pretty=oneline -1
31cc2774f0668b5b7c049a404284b19e9b40dc5d (HEAD -> master) [*]update time to 00:50
A       lose_file.txt
A       test_amend.txt
M       time.txt
  • 查看文件提交日志只有time.txt
  • stage里还有新的修改在
  • 使用git commit --amend --no-edit合并到上一个提交里,如果不加--no-edit参数的话,会提示你来修改commit提示信息(这个命令也可以用在重复编辑commit message)。
  • 查看日志,合并提交成功!

tag标签

创建一个tag

标签是一个类似于快照的东西,常常用于测试和发布版本。所以我们常常把tag名以版本号来命名,比如:v1.0beat1这样

我们怎么创建标签呢?首先先切换到想打标签的分支,然后直接打就可以了。

$ git branch
  dev/pzqu
  master
* release_v1.0

$ git tag -a release_v1.0 -m "release v1.0"

$ git tag release_v1.1

$ git tag
release_v1.0
release_v1.1

$ git push --tags
Counting objects: 2, done.
Writing objects: 100% (2/2), 158 bytes | 158.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0)
To github.com:pzqu/git_test.git
 * [new tag]         release_v1.0 -> release_v1.0
 * [new tag]         release_v1.1 -> release_v1.1
  • 切换到想打tag的分支
  • 创建名为release_v1.0带有信息release v1.0tag
  • 创建的不带有tag的提交信息的release_v1.1
  • git tag查看tag
  • 推送本地全部tag

也可以推送单个tag

$ git push origin release_v1.1
Total 0 (delta 0), reused 0 (delta 0)
To github.com:pzqu/git_test.git
 * [new tag]         release_v1.1 -> release_v1.1

我们来删除tag

$ git tag -d release_v1.0
Deleted tag 'release_v1.0' (was eb5d177)

$ git push origin :refs/tags/release_v1.0
To github.com:pzqu/git_test.git
 - [deleted]         release_v1.0

$ git tag
release_v1.1
  • 本地删除名为release_v1.0tag
  • 远程删除名为release_v1.0tag

对历史提交打tag

先看看当前的log

31cc277 (HEAD -> release_v1.0, tag: release_v1.1, origin/release_v1.0, master) [*]update time to 00:50
856a740 [*]update time to 18:51
3f22a06 [+]add file time.txt
4558a25 (origin/dev/pzqu, dev/pzqu) [*]test stash
d9e018e [*]merge master to dev/pzqu

比方说要对[*]update time to 18:51这次提交打标签,它对应的commit id是856a740,敲入命令:

$ git tag v.9 856a740

$ git log --pretty=oneline --abbrev-commit
31cc277 (HEAD -> release_v1.0, tag: release_v1.1, origin/release_v1.0, master) [*]update time to 00:50
856a740 (tag: v0.9) [*]update time to 18:51
  • 成功打上

git忽略不想提交的文件

我们有两种情况,一种是我们根本就不想这些文件出现在git库里比如日志文件;另一种是git远程仓库里有这些文件,就像通用的配置文件,我们必须要在本地修改配置来适应运行环境,这种情况下我们不想每次提交的时候都去跟踪这些文件。

忽略自动生成的垃圾文件、中间文件、敏感信息文件

忽略文件的原则是:

  1. 忽略操作系统自动生成的文件,比如缩略图等;
  2. 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  3. 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

我们要怎么做呢?

在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。$ echo "*.log" > .gitignore$ touch test.log$ touch test2.log$ ls -a . .git README.md need_stash.txt test.log test_amend.txt .. .gitignore lose_file.txt share_file.txt test2.log time.txt$ git status On branch release_v1.0 nothing to commit, working tree clean 创建并写入忽略规则*.log忽略全部以.log为后缀的文件 创建了test.logtest2.log * status查看,真是工作区是clean,新创建的文件没有被跟踪

忽略远程存在,本地不想与远程同步的文件

添加跟踪忽略

核心命令:

git update-index —assume-unchanged 文件名

imgupload successful

  • 创建time.txt文件并写入10:41,提交到远程仓库
  • 使用命令git update-index —assume-unchangedtime.txt加到忽略名单里
  • 修改time.txt的内容为10:43
  • status查看确实没有被跟踪 看远程仓库

imgupload successful

取消跟踪忽略

核心命令:

git update-index —no-assume-unchanged 文件名

imgupload successful

  • pull同步远程仓库,真的没有更新刚刚被添加跟踪忽略的文件
  • git update-index —no-assume-unchanged取消跟踪忽略
  • status查看,出现文件的跟踪

查看跟踪记录

如果忘记了哪些文件被自己本地跟踪

imgupload successful

  • 使用命令git update-index —assume-unchangedtime.txt加到忽略名单里
  • 使用git ls-files -v| grep '^h\ '命令可以看到小写h代表本地不跟踪的文件

小结

学完本文章,你将学会

  • 撤消commit,回滚暂存区,回滚工作区、回退远程仓库
  • 两种方法找回不小心丢失的文件
  • 提交的时候漏了文件,修改commit的提交信息
  • tag操作,创建、创建有描述信息的tag、删除tag、删除远程tag、推送本地单个tag和全部tag
  • git忽略自动生成的垃圾文件、中间文件、敏感信息文件;忽略远程存在,本地不想与远程同步的文件并恢复跟踪和查看哪些文件被跟踪

注意事项

理论上,git日常用到的命令是 diff show fetch rebase pull push checkout commit status 等,这些命令都不会导致代码丢失,假如害怕代码丢失,可以预先commit一次,再进行修改,但切记

不可使用自己不熟悉的命令 任何命令,不要加上-f的强制参数,否则可能导致代码丢失

建议多使用命令行,不要使用图形界面操作

下集

引用

git官网

廖雪峰的官方网站-git篇

hexo博客部署到vps

关于git reset --hard这个命令的惨痛教训

Git 基础再学习之:git checkout -- file

如何理解git checkout -- file和git reset HEAD -- file

此文已由腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

查看原文

赞 24 收藏 22 评论 0

BigXia 赞了文章 · 2019-03-08

版本控制工具——Git常用操作(上)

本文由云+社区发表

作者:工程师小熊

摘要:用了很久的Git和svn,由于总是眼高手低,没能静下心来写这些程序员日常开发最常用的知识点。现在准备开一个专题,专门来总结一下版本控制工具,让我们从git开始。完成本系列博客的阅读以后,你将掌握git的基本概念与git的基本命令,可以在本地随心所欲的完成代码的提交撤销保存修改等操作、可以流畅的参与多人协作,本文致力于快速的入门,如果涉及到更高级的功能需要进行更深一步的学习。

本文核心点:

  • Git的基本概念
  • 一个人使用Git时的代码版本控制--(提交、拉代码、分支操作)
  • 多人合作时的代码版本控制--(合并冲突、暂存代码)

什么是Git

简介

git是世界上目前最先进的分布式版本控制系统,致力于团队、个人进行项目版本管理,完美的解决难以比较代码、难以合并代码、难以取消修改、难以在写当前代码的过程中保存未完成的修改去修改线上版本的bug等的痛点。

git是一个非常强大的工具,但作为一个git使用者来说,不用完全学习Git的知识点与命令,因为有的命令的使用频率非常的低甚至数年都不会用到,让我们来由浅入深进行学习。

git的历史

git是linux的创始人linus,在付费版本控制工具BitMover收回对Linux社区免费使用权利的时候,一怒之下花费两个星期的时间写出来的。(牛笔的人)

开始

安装git

选择自己的操作系统对应的git版本安装,安装成功后运行git version后,输出git版本则安装正确。

git 官方: https://git-scm.com/downloads

配置用户信息

使用git config命令来配置用户名和邮箱

git config --global user.name "pzqu" 
git config --global user.email pzqu@example.com
如果用了 --global 选项,那么更改的配置文件就是位于你用户主目录下的那个,以后你所有的项目都会默认使用这里配置的用户信息。如果要在某个特定的项目中使用其他名字或者电邮,只要去掉 --global选项重新配置即可,新的设定保存在当前项目的 .git/config 文件里。

使用git config user.namegit config user.email来检查是否成功,也可以直接用git config --list来列出全部git配置信息来查看

img

创建git托管的项目

假如我们创建一个项目叫make_money,先创建一个文件夹叫make_money,再使用git init命令创建git项目。

# pzqu @ pzqu-pc in ~/Documents/code/test [0:05:29]
$ mkdir make_money

# pzqu @ pzqu-pc in ~/Documents/code/test [0:06:24]
$ ls
make_money

# pzqu @ pzqu-pc in ~/Documents/code/test [0:06:29]
$ cd make_money

# pzqu @ pzqu-pc in ~/Documents/code/test/make_money [0:07:10]
$ git init
Initialized empty Git repository in /Users/pzqu/Documents/code/test/make_money/.git/

# pzqu @ pzqu-pc in ~/Documents/code/test/make_money on git:master o [0:07:12]
$ ls -al
total 0
drwxr-xr-x  3 pzqu  staff   96 11  7 00:07 .
drwxr-xr-x  3 pzqu  staff   96 11  7 00:06 ..
drwxr-xr-x  9 pzqu  staff  288 11  7 00:07 .git

创建成功以后,会出现一个叫.git的隐藏文件夹,这个就是你的git仓库,以后所有的git操作历史提交记录信息就全部记录在此了,只要这个文件夹在就可以记住我们的全部git操作

工作区和暂存区

在使用git的时候还要清楚暂存区和工作区的含义,参考廖雪峰的官方网站-git篇-工作区和暂存区

常见情况

提交代码

新文件与修改

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [11:37:50]
$ ls
README.md

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [11:42:02]
$ touch file1.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [11:42:15]
$ git add file1.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [11:42:23]
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   file1.txt
# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [11:56:38]
$ git commit -m "[+]add new file1.txt"
[master 66cc488] [+]add new file1.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file1.txt

上图操作包含:

  • 创建新文件file1.txt
  • add 添加修改的内容到索引
  • status 查看修改的内容
  • commit 把索引提交到本地分支

git add . :监控工作区的状态树,此命令会把工作时的所有变化提交到暂存区,包括文件内容修改(modified)以及新文件(new),但不包括被删除的文件。

git add -u:他仅监控已经被add的文件(即tracked file),他会将被修改的文件提交到暂存区。add -u 不会提交新文件(untracked file)。(git add --update的缩写)

git add -A :是上面两个功能的合集(git add --all的缩写)

imgupload successful

git show 列出最近一次的提交
对于commit:像这样,你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

删除文件

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [12:55:24]
$ ls
README.md file1.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [12:55:25]
$ git rm file1.txt
rm 'file1.txt'

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [12:55:30]
$ ls
README.md

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [12:55:32]
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    file1.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master x [12:55:40] C:128
$ git commit -m "[-]delete file1.txt"
[master e278392] [-]delete file1.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 file1.txt

上图操作包含:

  • 创建新文件file1.txt
  • git rm 删除file1.txt文件
  • status 查看修改的内容
  • commit 把索引提交到本地分支

tip1: 如果没有用git rm删除文件,在本地删除文件后,git add一下再提交可以达到同样的效果

tip2: 要是你加班太晚,头晕不小心删除了不想删除的文件怎么办?见

下一篇:版本控制工具——Git常用操作(下)-后悔药

拉代码

方法一 pull

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [17:01:13]
$ git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:pzqu/git_test
   5fd4d8f..7b54a8a  master     -> origin/master
Merge made by the 'recursive' strategy.
 share_file.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 share_file.txt

上图命令:

  • git pull

查看本地仓库变化git log

imgupload successful

上图可以看到向远程仓库pull的时候,出现了两个新的commit,commit 7b54a8ae74...的提交信息为Create share_file.txt,另一个commit fdbb19cf4c51770的提交信息为Merge branch 'master' of github.com:pzqu/git_test。事实上主线只有一个提交,为什么会出现这种情况? 是因为pull其实会做两个操作

  • 拉远程仓库代码到本地
  • 自动与当前分支合并并生成一个合并成功的提交

注意这里的第二个个步骤如果远程有人和你改了同一个文件就会出现一个冲突,这个时候git会提示你哪些文件有冲突,手动改了再提交一次就可以了。详情见合并冲突

方法二 fetch

我在远程修改了文件,向share_file.txt加了一行内容tom modify,此时拉代码。

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [21:07:21]
$ git fetch

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:master o [21:08:43]
$ git rebase origin/master
First, rewinding head to replay your work on top of it...
Applying: [+]add new file1.txt
Applying: [-]delete file1.txt

上图所示有以下两个操作

  • fetch 拉取远端代码到本地
  • rebase 把本地代码提交基于远端分支重新replay

效果如下:

imgupload successful

上图是git log所输出的提交内容,刚刚pull的时候忘记把pull自动产生的merge提交到远程,rebase的时候把本地的提交放到了远程提交之后,看起来就是一条直线,比较优雅,也是推荐的方式。

同样的,如果产生了冲突,详情见合并冲突

分支操作

创建分支

分支是多人协同最经典的地方所在,我们来创建一个分支

$ git checkout -b dev/pzqu origin/master
Branch 'dev/pzqu' set up to track remote branch 'master' from 'origin'.
Switched to a new branch 'dev/pzqu'

$ git branch
* dev/pzqu
  master
  • git checkout -b 分支名 其他分支,-b代表创建并切换到新建的分支,分支名代表新创建的分支叫什么名字,这里叫dev/pzqu其他分支代表基于哪一个分支来创建,这里基于远程的master分支origin/master,如果省略则代表基于当前分支
  • git branch展示本地的分支情况,加-a参数可以展示全部的分支,包括远程分支
  • *在分支前,指明了现在所在的分支是dev/pzqu

切换分支

$ git checkout -b dev/pzqu2
Switched to a new branch 'dev/pzqu2'

$ git branch
  dev/pzqu
* dev/pzqu2
  master

$ git checkout dev/pzqu
Switched to branch 'dev/pzqu'
Your branch is up to date with 'origin/master'.

$ git branch
* dev/pzqu
  dev/pzqu2
  master
  • 基于当前分支创建了一个新的分支并自动切换过去dev/pzqu2
  • git checkout 已存在的分支名切换分支回到dev/pzqu

删除分支

$ git branch
* dev/pzqu
  dev/pzqu2
  master
  
$ git branch -D dev/pzqu2
Deleted branch dev/pzqu2 (was 7c9be37).

$ git branch
* dev/pzqu
  master
  
  • 位于dev/pzqu,删除了dev/pzqu2分支

合并冲突

合并同一个分支的冲突(常见)

为了产生一个冲突,我在另一个地方向远程仓库提交了代码,更改share_file.txt文件,加了一行内容tom add for merge

本地修改同一个文件加了一行pzqu add for merge,并提交到本地,这样一来,本地和远程仓库的同一个文件就不一样了,一会拉代码一定会产生一个冲突。效果如下:

imgupload successful

  • 一般rebase或pull冲突的时候,都会出现提示,然后git status会出现上图图示
  • 这个时候不可以进行任何分支切换和commit操作,按照他提示进行处理
  • git status提示哪个文件是都被修改的,both modified,然后使用编辑器修改该文件,解决冲突
  • 解决完成后,git add 添加该冲突文件
  • git rebase --continue,并更新commit message,完成整个rebase流程 我们来看看这个冲突的文件:

imgupload successful

Git用<<<<<<<=======>>>>>>>标记出不同分支的内容,我们修改如下后保存:

imgupload successful

git addgit rebase --continue后完成rebase,效果如下,再push的远程仓库即可

imgupload successful

合并不同分支的代码产生冲突

关于怎么创建分支与切换分支见创建分支和切换分支,这里只讨论合并时产生的冲突的情况,我们已经基于master分支创建了一个dev/pzqu分支

$ git branch
* dev/pzqu
  master

切换到master分支,加一行master add for merge并提交,文件内容如下:

$ cat share_file.txt
tom add
tom modify
tom add for merge
pzqu add for merge
master add for merge

切换到dev/pzqu分支,向share_file.txt加入一行dev/pzqu add for merge并提交,现在share_file.txt内容如下:

$ cat share_file.txt
tom add
tom modify
tom add for merge
pzqu add for merge
dev/pzqu add for merge

现在两个分支的同一个文件内容不一样了,现在我们在dev/pzqu分支上进行合并:

$ git merge master
Auto-merging share_file.txt
CONFLICT (content): Merge conflict in share_file.txt
Automatic merge failed; fix conflicts and then commit the result.

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:dev/pzqu x [11:17:31] C:1
$ git status
On branch dev/pzqu
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:   share_file.txt

no changes added to commit (use "git add" and/or "git commit -a")

$ cat share_file.txt
tom add
tom modify
tom add for merge
pzqu add for merge
<<<<<<< HEAD
dev/pzqu add for merge
=======
master add for merge
>>>>>>> master

上图出现了一个冲突,是我们意料之中的,修改share_file.txt文件,解决此冲突:

$ cat share_file.txt
tom add
tom modify
tom add for merge
pzqu add for merge
dev/pzqu add for merge
master add for merge

$ git add share_file.txt

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:dev/pzqu x [11:22:40]
$ git commit -m "[*]merge master to dev/pzqu"
[dev/pzqu d9e018e] [*]merge master to dev/pzqu

# pzqu @ pzqu-pc in ~/Documents/code/test/git_test on git:dev/pzqu o [11:23:00]
$ git status
On branch dev/pzqu
Your branch is ahead of 'origin/master' by 3 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

冲突解决也提交了,看看我们现在的分支内容:

imgupload successful

上图我们可以看到:

  • master分支比远程origin/master分支多一次提交,dev/pzqu分支由于是基于origin/master分支,合并了master分支的提交和当前dev/pzqu分支的提交,超出本地master两个提交,致此我们把master合并到dev/pzqu的操作就完成了。
  • 通常我们开一个新的开发分支是为了在自己的分支上写代码,方便提交也不会把主线弄乱,现在我们用同样的方法将dev/pzqu合并到master分支,然后把两个分支都提交到远程。
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

$ git merge dev/pzqu
Updating 58f047a..d9e018e
Fast-forward
 share_file.txt | 1 +
 1 file changed, 1 insertion(+)

$ git push origin master
Total 0 (delta 0), reused 0 (delta 0)
To github.com:pzqu/git_test.git
   7c9be37..d9e018e  master -> master
   
$ git push origin dev/pzqu
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 887 bytes | 887.00 KiB/s, done.
Total 9 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), done.
remote:
remote: Create a pull request for 'dev/pzqu' on GitHub by visiting:
remote:      https://github.com/pzqu/git_test/pull/new/dev/pzqu
remote:
To github.com:pzqu/git_test.git
 * [new branch]      dev/pzqu -> dev/pzqu
  • 切换到master分支
  • 合并dev/pzqumaster分支
  • master推到远程仓库
  • 如果dev/pzqu要保留,就可以推送到远程仓库。

imgupload successful

  • 现在我们可以看到全部的分支都在一起了,强迫症都舒服了。

暂存代码保存现场

这种情况一般是出现在你正在完成一个功能,但是忽然线上发现了一个Bug,必须马上开一个新的分支来修复bug,但是现在的功能没写完不打算提交(commit),现在怎么办??不用怕暂存代码来帮助你。

$ git status
On branch dev/pzqu
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   need_stash.txt
    modified:   share_file.txt

$ git stash
Saved working directory and index state WIP on dev/pzqu: d9e018e [*]merge master to dev/pzqu

$ git stash list
stash@{0}: WIP on dev/pzqu: d9e018e [*]merge master to dev/pzqu

$ git status
On branch dev/pzqu
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean


//省略操作:去创建一个Bug分支,修复他并完成与主线的合并,删除Bug分支。
//省略操作:切回来当前分支继续开发
//下面来恢复现场


$ git stash apply stash@{0}
On branch dev/pzqu
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   need_stash.txt

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

    modified:   share_file.txt
  • status查看到有2个文件修改没有提交
  • stash把修改放到暂存区,并生成一个id
  • stash list列出暂存区所有内容
  • stash apply重新把暂存区内容放到本地

这里的stash apply成功的把暂存区的一次暂存恢复到了本地,但是暂存区还有会保存这次暂存,如果想删除这次暂存要用git stash drop来删除;也可以用git stash pop,恢复最后一次暂存的同时把stash内容也删了。

$ git stash drop stash@{0}
Dropped stash@{0} (bfdc065df8adc44c8b69fa6826e75c5991e6cad0)

$ git stash list

好了,暂存区清干净了。

​ 注意:要放到暂存区的文件一定要先通过git add加到index

小结

本文阅读结束以后,我们学会了

  • Git的基本概念,知道git的作用、历史;学会安装配置Git,使用Git创建项目托管以及工作区和暂存区的概念
  • 学会Git的本地操作,提交、拉代码、创建切换删除分支操作,
  • 多人合作时的代码版本控制,学会了不同情况下的合并冲突、暂存代码操作

下集预告

Git常用操作(下)我计划给大家介绍以下点:

  • 后悔药-各种后悔操作(撤消commit,回滚,回退远程仓库等)
  • 哎呀,提交的时候漏了文件
  • tag操作
  • git忽略不想提交的文件

注意事项

理论上,git日常用到的命令是 diff show fetch rebase pull push checkout commit status 等,这些命令都不会导致代码丢失,假如害怕代码丢失,可以预先commit一次,再进行修改,但切记

不可使用自己不熟悉的命令 任何命令,不要加上-f的强制参数,否则可能导致代码丢失

建议多使用命令行,不要使用图形界面操作

引用

git官网

廖雪峰的官方网站-git篇

hexo博客部署到vps

此文已由腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

查看原文

赞 33 收藏 31 评论 0

BigXia 赞了文章 · 2019-03-07

RESTful API 设计规范

RESTful API 设计规范

该仓库整理了目前比较流行的 RESTful api 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 Github,欢迎大家补充!!

Table of Contents

关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY)可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;
参见:RFC 2119

Protocol

客户端在通过 API 与后端服务通信的过程中,应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*
  • example.com/api/*
如果你的应用很庞大或者你预计它将会变的很庞大,那 应该API 放到子域下(api.example.com)。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号

api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息

Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境
  • 私有树(prs)主要表示没有商业发布的项目
  • 供应商树(vnd)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发,应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写
  • URL 中资源(resource)的命名 必须 是名词,并且 必须 是复数形式
  • 必须 优先使用 Restful 类型的 URL
  • URL 必须 是易读的
  • URL 一定不可 暴露服务器架构
至于 URL 是否必须使用连字符(-) 或下划线(_),不做硬性规定,但 必须 根据团队情况统一一种风格。

来看一个反例

再来看一个正列

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

其中

1 删除资源 必须DELETE 方法
2 创建新的资源 必须 使用 POST 方法
3 更新资源 应该 使用 PUT 方法
4 获取资源信息 必须 使用 GET 方法

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合

请求方法URL描述
GET/zoos列出所有的动物园(ID和名称,不要太详细)
POST/zoos新增一个新的动物园
GET/zoos/{zoo}获取指定动物园详情
PUT/zoos/{zoo}更新指定动物园(整个对象)
PATCH/zoos/{zoo}更新动物园(部分对象)
DELETE/zoos/{zoo}删除指定动物园
GET/zoos/{zoo}/animals检索指定动物园下的动物列表(ID和名称,不要太详细)
GET/animals列出所有动物(ID和名称)。
POST/animals新增新的动物
GET/animals/{animal}获取指定的动物详情
PUT/animals/{animal}更新指定的动物(整个对象)
PATCH/animals/{animal}更新指定的动物(部分对象)
GET/animal_types获取所有动物类型(ID和名称,不要太详细)
GET/animal_types/{type}获取指定的动物类型详情
GET/employees检索整个雇员列表
GET/employees/{employee}检索指定特定的员工
GET/zoos/{zoo}/employees检索在这个动物园工作的雇员的名单(身份证和姓名)
POST/employees新增指定新员工
POST/zoos/{zoo}/employees在特定的动物园雇佣一名员工
DELETE/zoos/{zoo}/employees/{employee}从某个动物园解雇一名员工
超出 Restful 端点的,应该 模仿上表的方式来定义端点。

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。
  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

所有 URL 参数 必须 是全小写,必须 使用下划线类型的参数形式。

分页参数 必须 固定为 pageper_page

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

GET /trades?status=closed&sort=sortby=name&order=asc

# 可为其定制快捷方式
GET /trades/recently_closed

Authentication

应该 使用 OAuth2.0 的方式为 API 调用者提供登录认证。必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}

客户端在请求需要认证的 API 时,必须 在请求头 Authorization 中带上 access_token

Authorization: Bearer token...

当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "error": "invalid_token"
}
Laravel 开发中,应该 使用 JWT 来为管理你的 Token,并且 一定不可api 中间件中开启请求 session

Response

所有的 API 响应,必须 遵守 HTTP 设计规范,必须 选择合适的 HTTP 状态码。一定不可 所有接口都返回状态码为 200HTTP 响应,如:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": 0,
    "msg": "success",
    "data": {
        "username": "username"
    }
}

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": -1,
    "msg": "该活动不存在",
}

下表列举了常见的 HTTP 状态码

状态码描述
1xx代表请求已被接受,需要继续处理
2xx请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx重定向
4xx客户端原因引起的错误
5xx服务端原因引起的错误
只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功。

必须强调的是,所有 API一定不可 返回 1xx 类型的状态码。当 API 发生错误时,必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部;

X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中;

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

{
    "message": "您查找的资源不存在",
    "error_code": 404001
}

其中错误码(error_code必须HTTP 状态码对应,也方便错误码归类,如:

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive

{"error_code":403002,"message":"用户已禁用"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

{
    "message": "直接展示给终端用户的错误信息",
    "error_code": "业务错误码",
    "error": "供开发者查看的错误信息",
    "debug": [
        "错误堆栈,必须开启 debug 才存在"
    ]
}

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功GET 请求中,必须 返回此状态码。HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "user": {
        "id":1,
        "nickname":"fwest",
        "username": "example"
    }
}

正确示例:

1、获取单个资源详情

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}

2、获取资源集合

[
    {
        "id": 1,
        "username": "godruoyi",
        "age": 88,
    },
    {
        "id": 2,
        "username": "foo",
        "age": 88,
    }
]

3、额外的媒体信息

{
    "data": [
        {
            "id": 1,
            "avatar": "https://lorempixel.com/640/480/?32556",
            "nickname": "fwest",
            "last_logined_time": "2018-05-29 04:56:43",
            "has_registed": true
        },
        {
            "id": 2,
            "avatar": "https://lorempixel.com/640/480/?86144",
            "nickname": "zschowalter",
            "last_logined_time": "2018-06-16 15:18:34",
            "has_registed": true
        }
    ],
    "meta": {
        "pagination": {
            "total": 101,
            "count": 2,
            "per_page": 2,
            "current_page": 1,
            "total_pages": 51,
            "links": {
                "next": "http://api.example.com?page=2"
            }
        }
    }
}
其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时,应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
    "id": 1,
    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
    "nickname": "fwest",
    "last_logined_time": "2018-05-29 04:56:43",
    "created_at": "2018-06-16 17:55:55",
    "updated_at": "2018-06-16 17:55:55"
}

也可以返回一个响应实体为空的 HTTP Response 如:

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive
这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;

返回该状态码时,响应实体 必须 为空。
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时,必须 返回该状态码
  • 使用 PUTPATCH 方法更新数据 成功 时,也 应该 返回此状态码
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive

3xx 重定向

所有 API不该 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=https://example.com" />

        <title>Redirecting to https://example.com</title>
    </head>
    <body>
        Redirecting to <a href="https://example.com">https://example.com</a>.
    </body>
</html>

所有 API一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能,可以 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API
  • access_token 无效/过期
客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

{"message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时,必须 返回该状态码。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"权限不足"}

404 Not Found

该状态码表示用户请求的资源不存在,如

  • 获取不存在的用户信息 (get /users/9999999)
  • 访问不存在的端点

必须 返回该状态码,若该资源已永久不存在,则 应该 返回 410 响应。

405 Method Not Allowed

当客户端使用的 HTTP 请求方法不被服务器允许时,必须 返回该状态码。

如客户端调用了 POST 方法来访问只支持 GET 方法的 API

该响应 必须 返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}

406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSONXML 输出的 API 被指定返回 YAML 格式的数据时。

Http 协议一般通过请求首部的 Accept 来指定数据格式

408 Request Timeout

客户端请求超时时 必须 返回该状态码,需要注意的时,该状态码表示 客户端请求超时,在涉及第三方 API 调用超时时,一定不可 返回该状态码。

409 Confilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须 返回此状态码。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手机号已存在"}

410 Gone

404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端 应该 停止再次请求该资源。

413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。

此种情况下,服务器可以关闭连接以免客户端继续发送此请求。

如果这个状况是临时的,服务器 应该 返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都 应该 返回该状态码。

该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时 应该 返回该状态码:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允许上传的图片格式"}

429 Too Many Requests

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都 应该 返回该状态码。并且也 应该 在响应首部中加上下列头部:

X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

列子

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}

必须 为所有的 API 设置 Rate Limit 支持。

500 Internal Server Error

该状态码 必须 在服务器出错时抛出,对于所有的 500 错误,都 应该 提供完整的错误信息支持,也方便跟踪调试。

503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都 应该 返回该状态码,其中若是主动关闭 API 服务,应该 在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服务维护中"}

其他 HTTP 状态码请参考 HTTP 状态码- 维基百科

版权声明

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

建议参考

restful-api-design-references

Principles of good RESTful API Design(译)

理解 RESTful 架构

RESTful API 设计指南

HTTP 状态码- 维基百科

LICENSE

MIT License

Copyright (c) 2018 godruoyi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

查看原文

赞 189 收藏 228 评论 34

BigXia 赞了文章 · 2019-03-06

Swoole 入门教程 —— 2小时入门Swoole

前言

  1. Swoole 是由 韩天峰 编写的一个 PHP 扩展, 主打异步/高并发/常驻服务特性. 适用于 HTTP/TCP/UDP/WEBSOCKET等多种伺服器应用场景.
  2. 本系列文章主要介绍 Swoole HTTP 方向的应用. 本系列文章使用原生 PHP+Swoole 扩展实现操作 [数据库操作 CURD 除外].
  3. 本系列文章将逐步更新, 预计2017/09前完成. 在完成更新前希望不要不带链接地转载, 给我一点修正错误的时间.

题外话

  1. Swoole != Swoole Framework
    Swoole 是扩展, Swoole Framework 是框架, 二者没有充分必要关系, 所以大家不要搞混了, 后面我们只介绍 Swoole 扩展, 不介绍 Swoole Framework.
  2. 关于 Swoole Framework 框架的一点带主观的思考
    Swoole Framework是个一塌糊涂的框架, Model ORM 写的真x蛋, 让我这个用惯 Laravel 的人实在无法忍受. 当然, 如果你能忍受 ->get(50, 'article_id') 这种写法的话欢迎你继续使用. 但这丝毫不影响 Swoole 扩展对我技术栈的冲击. 如果你用惯了 Laravel 框架, 你可以采用 https://github.com/scil/Larav... 这个方案在 Swoole 扩展上进行 Laravel 框架开发.

目录

  1. 初探 Swoole -- 用 Swoole 启动一个 hello world
  2. 内存的妙用 -- PHP终于可以 vs JAVA啦
  3. 初级应用 -- 实现用户登录
  4. 现代化编程 -- 在 Swoole 上开发 Laravel 框架的应用

便宜好用的SSL,通配符证书+SAN,IP证书,联系客服可试用,更有超值优惠可PY: https://www.digital-sign.com.cn/

查看原文

赞 84 收藏 197 评论 11

BigXia 赞了文章 · 2019-03-05

Web 应用安全性: 浏览器是如何工作的

图片描述

这本系列的第一篇,先解释浏览器的功能以及执行方式。由于大多数客户将通过浏览器与 web 应用程序进行交互,因此必须了解这些出色程序的基础知识。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

浏览器是一个渲染引擎,它的工作是下载一个web页面,并以人类能够理解的方式渲染它。

虽然这几乎是一种过于简单的过分简化,但我们现在需要知道的全部内容。

  • 用户在浏览器栏中输入一个地址。
  • 浏览器从该 URL 下载“文档”并渲染它。

图片描述

你可能习惯使用 Chrome,Firefox,Edge或Safari等流行的浏览器之一,但这并不意味着没有不同的浏览器。

例如,lynx 是一种轻量级的、基于文本的浏览器,可以在命令行中工作。lynx 的核心原理与其他“主流”浏览器的原理完全相同。用户输入 web 地址(URL),浏览器获取文档并呈现它——唯一的区别是 lynx 不使用可视化渲染引擎,而是使用基于文本的界面,这使得像谷歌这样的网站看起来像这样:

图片描述

我们大致了解浏览器的功能,但是让我们仔细看看这些机智的应用程序为我们所做的步骤。

浏览器做了什么?

长话短说,浏览器的工作主要包括:

  • DNS 解析
  • HTTP 交换
  • 渲染
  • 重复以下步骤

DNS 解析

这个过程确保一旦用户输入 URL,浏览器就知道它必须连接到哪个服务器。浏览器联系 DNS 服务器,发现google.com 翻译成 216.58.207.110,这是一个浏览器可以连接的 IP 地址。

HTTP 交换

一旦浏览器确定了哪个服务器将为我们的请求提供服务,它将启动与它的 TCP 连接并开始 HTTP 交换。 这只是浏览器与服务器通信所需内容以及服务器回复的一种方式。

HTTP 只是用于在 Web 上进行通信协议的名称,而浏览器一般通过 HTTP 与服务器进行通信。 HTTP 交换涉及客户端(我们的浏览器)发送请求,服务器回复响应。

例如,当浏览器成功连接到 google.com 背后的服务器后,它将发送一个如下所示的请求:

GET / HTTP/1.1
Host: google.com
Accept: */*

让我们一行一行地把请求分解:

  • GET / HTTP/1.1:在第一行中,并补充说其余请求将遵循 HTTP/1.1 协议(它也可以使用1.02
  • Host: google.com这是 HTTP/1.1 中唯一必须的 HTTP 报头。因为服务器可能服务多个域(google.com, google.co.uk) 。这里的客户端提到请求是针对特定的主机的。
  • Accept: */*:一个可选的标头,其中浏览器告诉服务器接受任何类型的响应。服务器可以拥有 JSON、XM L或HTML 格式的可用资源,因此它可以选择自己喜欢的格式。

作为客户端的浏览器发送请求之后,就轮到服务器进行响应了,这是响应的格式如下:

HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=1234; expires=Fri, 18-Jan-2019 18:25:04 GMT; path=/; domain=.google.com; HttpOnly
<!doctype html><html">
...
...
</html>

哇,有很多信息需要消化。服务器让我们知道请求是成功的(200 OK),并向响应中添加一些头部信息,例如,它告知哪个服务器处理了我们的请求(Server:gws),该响应的 X-XSS-Protection 策略是什么,等等。

现在,你不需要理解响应中的每一行,在本系列后面的文章中,我们将介绍 HTTP 协议及其头部等内容。

现在,你只需要了解客户端和服务器正在交换信息,并且它们是通过 HTTP 进行交换的。

渲染

<!doctype html><html">
...
...
</html>

在响应的主体中,服务器根据 Content-Type 头包括响应类型来表示。 在我们的例子中,内容类型设置为 text/ html,因此我们期待响应中的 HTML 标记 - 这正是我们在正文中找到的。

这才是浏览器真正的亮点所在。它解析 HTML,加载标记中包含的额外资源(例如,可能需要获取JavaScript文件或CSS文档),并尽快将它们呈现给用户。

最终的结果是普通人能够理解的:

图片描述

如果想要更详细地了解当我们在浏览器地址栏中按回车键时会发生什么,建议阅读“What happens when…”,这是一个非常精细的尝试来解释该过程背后的机制。

由于这是一个关注安全性的系列文章,从刚刚了解到的内容可以提到提示:攻击者可以轻松地利用 HTTP 交换和渲染部分中的漏洞谋生。漏洞和恶意用户也潜伏在其他地方,但是这些级别上更好的安全方法已经允许你在改进安全性方面取得进展。

供应商

4 个最流行的浏览器属于不同的公司:

  • 谷歌的 Chrome
  • Mozilla 的火狐
  • 苹果的 Safari
  • 微软的 Edge

除了为了增加市场渗透率而相互竞争之外,供应商也为了提高 web 标准而相互合作,这是对浏览器的一种“最低要求”。

W3C是标准开发的主体,但是浏览器开发自己的特性并最终成为 web 标准的情况并不少见,安全性也不例外。

例如,Chrome 51 引入了 SameSite cookie,该功能允许 Web 应用程序摆脱称为 CSRF 的特定类型的漏洞(稍后将详细介绍)。其他供应商认为这是一个好主意,并纷纷效仿,导致 SameSite 成为 web 标准:到目前为止,Safari 是唯一没有 SameSite cookie 支持的主流浏览器

图片描述

这告诉我们两件事:

  • Safari似乎并不关心用户的安全性(开玩笑:Safari 12中将提供SameSite cookie,这可能在你阅读本文时已经发布)
  • 修补一个浏览器上的漏洞并不意味着所有用户都是安全的

第一点是对 Safari 的一次尝试(正如我提到的,开玩笑的!),而第二点非常重要。在开发web应用程序时,我们不仅需要确保它们在不同的浏览器中看起来是相同的,还需要确保我们的用户在不同的平台上受到相同的保护。

你的网络安全策略应根据浏览器供应商允许我们执行的操作而有所不同。 如今,大多数浏览器都支持相同的功能集,并且很少偏离其常见的路线图,但是上面的实例仍然会发生,这是我们在定义安全策略时需要考虑的事情。

在我们的例子中,如果我们决定只通过 SameSite cookie 来减轻 CSRF 攻击,那么我们应该意识到我们正在将 Safari 用户置于危险之中。我们的用户也应该知道这一点。

最后但并非最不重要,你应该记住,你可以决定是否支持浏览器版本:支持每一个浏览器版本将是不切实际的(想想 Internet Explorer 6)。虽然确保最近几个版本的主流浏览器的支持通常是一个好的决定,但是如果你不打算在特定的平台上提供保护,一般建议让你的用户知道。

专业提示:你不应该鼓励你的用户使用过时的浏览器,或积极支持他们。尽管你可能已经采取了所有必要的预防措施,但是其他web开发人员可能没有。鼓励用户使用主流浏览器支持的最新版本。

供应商还是标准bug?

普通用户通过第三方客户端(浏览器)访问我们的应用程序这一事实增加了另一层次的间接性:浏览器本身可能存在安全漏洞。

供应商通常会向能够发现浏览器自身漏洞的安全研究人员提供奖励(即 bug奖金)。这些bug与你的实现无关,而是与浏览器本身处理安全性的方式有关。

例如,Chrome 奖励计划可让安全工程师与 Chrome 安全团队联系,报告他们发现的漏洞。 如果确认了这些漏洞,则会发布补丁,通常会向公众发布安全建议通知,研究人员会从该计划中获得(通常是财务上的)奖励。

像谷歌这样的公司在他们的Bug赏金项目中投入了相对较多的资金,这使得他们能够通过承诺在发现应用程序的任何问题时获得经济利益来吸引研究人员。

在一个漏洞赏金计划中,每个人都是赢家:供应商设法提高其软件的安全性,研究人员也因此获得报酬。我们将在后面讨论这些程序,因为我相信Bug赏金计划应该在安全领域有自己的一节。

Jake Archibald 是谷歌的一名开发人员,他最近发现了一个影响多个浏览器的漏洞。他在一篇有趣的博客文章中记录了他的努力,他如何接触不同的供应商,以及他们的反应,建议你阅读 这篇文章

开发人员的浏览器

到目前为止,我们应该理解一个非常简单但相当重要的概念:浏览器只是为普通网络冲浪者构建的 HTTP 客户端。

它们肯定比平台的纯HTTP客户端更强大(例如,考虑NodeJS的require(‘HTTP’)),但归根结底,它们“只是”更简单的 HTTP客户端的自然演化。

作为开发人员,我们选择的HTTP客户机可能是 Daniel Stenberg 的 cURL,他是 web 开发人员每天使用的最流行的软件程序之一。它允许我们通过从命令行发送 HTTP 请求来实时执行 HTTP 交换:

$ curl -I localhost:8080
HTTP/1.1 200 OK
server: ecstatic-2.2.1
Content-Type: text/html
etag: "23724049-4096-"2018-07-20T11:20:35.526Z""
last-modified: Fri, 20 Jul 2018 11:20:35 GMT
cache-control: max-age=3600
Date: Fri, 20 Jul 2018 11:21:02 GMT
Connection: keep-alive

在上面的示例中,我们在 localhost:8080/ 上请求了文档,本地服务器成功响应。

在这里,我们没有将响应的主体显示在命令行,而是使用了 -I 标志,它告诉 cURL 我们只对响应头感兴趣。更进一步,我们可以指示 cURL 显示更多的信息,包括它执行的实际请求,以便更好地查看整个HTTP交换。需要使用的选项是-v(详细):

$ curl -I -v localhost:8080
* Rebuilt URL to: localhost:8080/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> HEAD / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< server: ecstatic-2.2.1
server: ecstatic-2.2.1
< Content-Type: text/html
Content-Type: text/html
< etag: "23724049-4096-"2018-07-20T11:20:35.526Z""
etag: "23724049-4096-"2018-07-20T11:20:35.526Z""
< last-modified: Fri, 20 Jul 2018 11:20:35 GMT
last-modified: Fri, 20 Jul 2018 11:20:35 GMT
< cache-control: max-age=3600
cache-control: max-age=3600
< Date: Fri, 20 Jul 2018 11:25:55 GMT
Date: Fri, 20 Jul 2018 11:25:55 GMT
< Connection: keep-alive
Connection: keep-alive
<
* Connection #0 to host localhost left intact

主流浏览器通过它们的 DevTools 可以获得几乎相同的信息。

正如我们所见,浏览器只不过是精心设计的HTTP客户端。 当然,他们添加了大量的功能(想到凭据管理,书签,历史等),但事实是,它们是作为人类的 HTTP 客户端而诞生的。 这很重要,因为在大多数情况下,不需要使用浏览器来测试Web应用程序的安全性,因为你可以简单的通过 curl 命令来查看响应信息。

进入 HTTP 协议

正如我们所提到的,HTTP交换和渲染阶段是我们主要要涉及的阶段,因为它们为恶意用户提供了最大数量的攻击媒介。

在下一篇文章中,我们将深入研究HTTP协议,并尝试了解为了保护HTTP交换,我们应该采取哪些措施。

原文:

https://medium.freecodecamp.o...

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

赞 51 收藏 39 评论 1

认证与成就

  • 获得 7 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-02-26
个人主页被 251 人浏览