HTTP/2.0 简单总结

如何使用上 HTTP/2.0

  1. 需要浏览器的支持,目前最新版的 Chrome、Opera、 FireFox、 IE11、 edge 都已经支持了
  2. 需要 WEB 服务器的支持,比如 Nginx , H20

如果浏览器或服务器有一方不支持,那么会自动变成 Http/1.1

HTTP/1.1 时代

HTTP/1.1 对 HTTP/1.0 做了许多优化,也是当今使用得最多的 HTTP 协议:

  1. 持久化连接以支持连接重用
  2. 分块传输编码以支持流式响应
  3. 请求管道以支持并行请求处理
  4. 字节服务以支持基于范围的资源请求
  5. 改进的更好的缓存机制

持久连接

在 HTTP/1.0 时代,每一个请求都会重新建立一个 TCP 连接,一旦响应返回,就关闭连接。 而建立一个连接,则需要进行三次握手(https的话则是9次握手),这极大的浪费了性能

因此 HTTP/1.1 新增了「keep-alive」功能,当浏览器建立一个 TCP 连接时,多个请求都会使用这条连接。(现如今大多数浏览器默认都是开启的)

PipeLining 管道

持久连接解决了连接复用问题,但还是存在着一个问题:在一个 TCP 连接中,同一时间只能够发送一个请求,并且需要等响应完成才能够发送第二个请求。

因此 HTTP/1.1 制订了 PipeLining 管道,通过这个管道,浏览器的多个请求可以同时发到服务器,但是服务器的响应只能够一个接着一个的返回 ( 但各大浏览器有些不支持/默认关闭,因此这功能可以说是鸡肋)

因为每一条连接同时只能够返回一个响应,因此浏览器为了改善这种情况,会同时开启4~8个 TCP 连接进行发送请求。

小结

在 HTTP/1.1 时代主要增加了:

  1. keep-alive 选项,建立连接后,在一定时间内不会断开,其他请求都可以使用这条连接。
  2. pipelining 管道,通过这个管道,浏览器的多个请求可以同时发到服务器,但是服务器的响应只能够一个接着一个的返回 ( 但各大浏览器有些不支持/默认关闭,因此这功能可以说是鸡肋)

HTTP/1.1 时代的优化

1、连接和拼接

连接或拼接JS和CSS文件,雪碧图,以减少HTTP请求,同时浏览器可缓存这些静态资源,为下次访问节约时间。但是这样带来的副作用是,维护成本高,其中某一个小改动都会使得整个拼接后的文件发生改变,重新缓存。

当然并不是说无止境的拼接,建议大小为: 30~50 KB

2、域名分区

由于浏览器的限制,同一个域下最多只能建立6个连接。我们通常使用子域名来减少所有资源在只有一个连接时的产生的排队延迟。这个显然不适用在HTTP2中,因为不同的域需要建立不同的连接。

3、资源内嵌

对于不常用的,较小大资源内嵌在文档中,比如base64的图片,以减少HTTP请求,但是这样的资源不能在浏览器中缓存,也不可能被其他页面共享,同时还有可能编码之后的资源变等更大了。在HTTP2中,这样的资源就可以使用SERVER PUSH来推送。

建议:

  1. 只考虑嵌入1~2 KB 以下的资源,因为小于这个标准的资源经常会导致比它自身更高的HTTP 开销
  2. 如果文件很小,而且只有个别页面使用,可以考虑嵌入。理想情况下,最好是只用一次的资源
  3. 如果文件很小,但需要在多个页面中重用,应该考虑集中打包
  4. 如果小文件经常需要更新,就不要嵌入了
  5. 通过减少 HTTP cookie的大小将协议开销最小化

SPDY 时代

由于现代网页的不断丰富, HTTP/1.1 协议的性能也逐渐吃不消,因此2012年google如一声惊雷提出了SPDY的方案,实际上,HTTP/2.0 也是以 SPDY 作为原型进行开发的。

SPDY基础功能

多路复用(multiplexing)

多路复用通过多个请求stream共享一个tcp连接的方式,解决了http1.x holb(head of line blocking)的问题,降低了延迟同时提高了带宽的利用率。

请求优先级(request prioritization)

多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。

header压缩

前面提到过几次http1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。SPDY对header的压缩率可以达到80%以上,低带宽环境下效果很大。

SPDY 现已经被大多数浏览器以及 WEB 服务器所支持,但为了推进 HTTP/2.0, Google 已经宣布在 2016年对其停止开发。

HTTP/2.0 时代

2015年5月, HTTP/2.0 在万众瞩目下以RFC 7540正式发表。(热烈鼓掌~啪啪啪啪~~)

二进制分帧

在应用层与传输层之间增加一个二进制分帧层,以此达到“在不改动HTTP的语义,HTTP 方法、状态码、URI及首部字段的情况下,突破HTTP1.1的性能限制,改进传输性能,实现低延迟和高吞吐量。”

在二进制分帧层上,HTTP2.0会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。

压缩头部

HTTP/2.0规定了在客户端和服务器端会使用并且维护「首部表」来跟踪和存储之前发送的键值对,对于相同的头部,不必再通过请求发送,只需发送一次

事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。

如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP2.0的连接存续期内始终存在,由客户端和服务器共同渐进地更新。

多路复用

HTTP/2.0 时代拥有了「多路复用」功能,意思是: 在一条连接上,我可以同时发起无数个请求,并且响应可以同时返回。(这个难点终于被解决了)

客户端和服务器可以把HTTP消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。注意,同一链接上有多个不同方向的数据流在传输。客户端可以一边乱序发送stream,也可以一边接收者服务器的响应,而服务器那端同理。

也就是说,HTTP2.0通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。就好比,我请求一个页面http://www.qq.com%E3%80%82%E9%A1%B5%E9%9D%A2%E4%B8%8A%E6%89%80%E6%9C%89%E7%9A%84%E8%B5%84%E6%BA%90%E8%AF%B7%E6%B1%82%E9%83%BD%E6%98%AF%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%B8%8E%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%8A%E7%9A%84%E4%B8%80%E6%9D%A1TCP%E4%B8%8A%E8%AF%B7%E6%B1%82%E5%92%8C%E5%93%8D%E5%BA%94%E7%9A%84%EF%BC%81

注意,对一个域名,只需要开启一条 TCP 连接,请求都在这条 TCP 连接上干活。

因此在 HTTP/2.0 时代,之前的合并 JS、CSS 文件技巧,反而不适用了。

请求优先级

既然所有资源都是并行发送,那么就需要「优先级」的概念了,这样就可以对重要的文件进行先传输,加速页面的渲染。

服务器推送

在 HTTP2.0中,服务器推送是指在客户端请求之前发送数据的机制。如果一个请求是由你的主页发起的,服务器很可能响应主页内容、logo以及样式表,因为它知道客户端会用到这些东西。这相当于在一个 HTML 文档内集合了所有的资源,不过与之相比,服务器推送有一个很大的优势:可以缓存!

强制 SSL

虽然 HTTP/2.0 协议并没声明一定要用 SSL,但是 Google Chrome 等浏览器强制要求使用 HTTP/2.0 必须要用上 SSL, 也就是说必须要: https://

http:// 将继续使用 http/1.0

对优化的影响:

  1. 因为“所有的HTTP2.0的请求都在一个TCP链接上”,“资源合并减少请求”,比如CSS Sprites,多个JS文件、CSS文件合并等手段没有效果,或者说没有必要。
  2. 因为“多路复用”,采用“cdn1.cn,cdn2.cn,cdn3.cn,打开多个TCP会话,突破浏览器对同一域名的链接数的限制”的手段是没有必要的。因为因为资源都是并行交错发送,且没有限制,不需要额外的多域名并行下载。
  3. 因为“服务器推送”,内嵌资源的优化手段也变得没有意义了。而且使用服务器推送的资源的方式更加高效,因为客户端还可以缓存起来,甚至可以由不同的页面共享(依旧遵循同源策略)

React 总结

前言

啊,React 的项目完结到现在已经有一个月了,之前一直唠叨着说昨晚项目要写篇总结,无奈一直拖拖拖拖,拖到现在,还是写了它吧。

项目

这个前端项目用的是:

  1. React
  2. Redux
  3. React Router
  4. ES6
  5. Babel

React

React是Facebook开发的一款JS库, 目的是为了构建那些数据会随着时间改变的大型应用。

主要原理则是虚拟 DOM 以及 Component 组件

React有个diff算法,更新Virtual DOM并不保证马上影响真实的DOM,React会等到事件循环结束,然后利用这个diff算法,通过当前新的dom表述与之前的作比较,计算出最小的步骤更新真实的DOM。

Redux

Redux 是应用状态管理服务,用来管理 React 中的 state,应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。

React 算是传统 MVVM 意义上的 View 层,负责来渲染页面,数据管理方面则需要搭配其他的库,比如出名的是 Facebook 出的 flux 以及 Redux, 但目前来说,Redux 使用的人要多于 flux

React Router

React Router 是完整的 React 路由解决方案,可以根据不同的URL来加载不同的component

要点

基本知识在这里就不啰嗦了,在线看文档就够了。在这里写下之前所记的一些要点吧。

1、action

action 用来表达发生的事件,如果要改变state,必须统一通过发action

2、reducer

reducer用来接收之前的 state 以及 action, 从而返回新的state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      });
    case COMPLETE_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      });
    default:
      return state;
  }
}

默认应该返回旧state,注意在返回新state时,不要对旧state做出改变。

3、component

创建组件,就相当于 class xx extends Component

1
2
3
4
5
6
7
# 创建组件
React.createClass()

# 相当于
class ChainSelectBox extends Component {

}

4、connect()

任何一个 connect() 的组件,都可以得到 dispatch 方法作为组件的 props, 以及得到全局的 state.一次我们一般会在较顶级的组件进行connect()操作,并且将数据传入子组件进行使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ApprovalChainManage extends Component {

    render() {
        return <ChildComponent></ChildComponent>
    }
}

# 当 state 有更新的时候,就会被调用
const mapStateToProps = ( state ) => {
  return {
    approvalChainEntities:       state.ApprovalChainEntity
  }
};

# 将 actions 传入 props
const mapDispatchToProps = ( dispatch ) => ( {
  approvalChainActions:       bindActionCreators( approvalChainActions, dispatch )
} );


export default connect( mapStateToProps, mapDispatchToProps )( ApprovalChainManage );

如果父组件以及子组件都有connect()的操作,那么当state有变化时,两者的mapStateToProps()方法都会被调用。具体的说应该是,当state有变化时,所有正在被加载的connect()过的组件,内的mapStateToProps()方法都会被调用

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
# 子组件
class ChildComponent extends Component {

    render() {
        # JSX 语法
        return (
            <div>
                <span>Yo yo</span>
            </div>
        )
    }
}

const mapStateToProps = ( state ) => {
  return {
    approvalChainEntities:       state.ApprovalChainEntity
  }
};

const mapDispatchToProps = ( dispatch ) => ( {
   studentActions:       bindActionCreators( StudentActions, dispatch )
} );


export default connect( mapStateToProps, mapDispatchToProps )( ApprovalChainManage );


# 父组件
class ApprovalChainManage extends Component {

    render() {
        return <ChildComponent></ChildComponent>
    }
}

# 当 state 有更新的时候,就会被调用
const mapStateToProps = ( state ) => {
  return {
    approvalChainEntities:       state.ApprovalChainEntity
  }
};

# 将 actions 传入 props
const mapDispatchToProps = ( dispatch ) => ( {
  approvalChainActions:       bindActionCreators( approvalChainActions, dispatch )
} );


export default connect( mapStateToProps, mapDispatchToProps )( ApprovalChainManage );

5、Store

当用户进行刷新操作时,Store内的数据是会被清空的,所有组件重新加载,相当于第一次打开页面,这点要切记。

Store 内的值是所有人共享的,如果你在组件 A 执行了 action, 更新了一些数据,并且跳往其他页面,此时这些数据也是一直存在的。

6、mapStateToProps

该函数返回的值会植入该组件的props,并且当state有更新时,会调用该方法。

不过细心的人会发现,第一次打开页面,mapStateToProps()会被执行两次

1
2
3
4
5
6
7
# 第一次是因为初始化,第二次是因为connect
const mapStateToProps = ( state ) => {
  console.log("开始 state cashview")
  return {
    entities: state.cashEntity
  }
};

7、通过路由来统一管理加载页面

1
2
3
4
5
6
7
 <Route path='/' component={Root}>
    <IndexRoute component={LoginView} />
      <Route path='cash' component={CashView}>
        <Route path='product/:id' component={CashProductDetail}></Route>
        <Route path='index' component={CashIndex}></Route>
      </Route>
 </Route>

一般来说几个类似的页面会统一挂在某个view下,作为子组件,比如登陆页有自己的view,商品列表``商品详情``商品购买会挂载某个view下,订单列表``订单详情会挂载某个view下。

并且我们会在viewconnect()获取数据,接着将数据传入子组件。这样整个结构就比较清晰。

上述例子,除去顶级组件connect()外,我们还会在cash这个view上进行connect()操作。

此时URLlocalhost:3000/cash/index时,便会加载Root``CashView``CashIndex组件

此时URLlocalhost:3000/cash/product/1时,便会加载Root``CashView``CashProductDetail组件

详情页以及首页列表在进行切换时,仅仅只是替换了各自的组件,不会影响到CashView层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CashView extends Component {

  render () {
    const {
      entities, actions, children, publicEntities, publicActions
    } = this.props;

    return (
      <div>
        <div className="er-header">
          <Header navBarIndex={1}></Header>
        </div>
        # 这里来根据路由加载不同的子组件
        # 使用 cloneElement 的原因是为了将数据传入子组件
        # cloneElement 并非是真正的 clone, 只是将我们传入的数据进行合并罢了
        {cloneElement(children, { entities, actions, publicEntities, publicActions })}
        <Footer/>
      </div>
    );
  }
}

8、重新渲染页面

现在有这样的一个需求:

点击按钮后,该按钮需要变色

  1. 组件加载完成,页面渲染完毕
  2. 单击按钮,发送action修改state内的值,此时重新render(),按钮成功变色 A
  3. 单击第二个按钮,发送action,修改state,重新render()后,此时第二个按钮变色B,第一个按钮的颜色还是 A

就是说,渲染是基于最新的 dom 元素来进行渲染的,而不是重新完全渲染

9、Redux 的 store 以及 React 的 state

由于 Store 就是来管理 React 的数据的,所以很多人会有这样的误解:

Store是在管理操作React内的State

其实不是,ReduxstoreReactstate并没有什么关系。只是我们用前者来代替后者而已。

所以,React内的state我们也是可以正常使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Teacher extends Component {
    construtor() {
        # 由于使用了 ES6 语法,因此不能用 defaultState 属性来进行初始化
        this.state = {
            name: 'fancy'
        }
    }

    componentWillMount(){
        if(this.props.rightLinkTxt){
          # 更改该组件的 state, 此时该组件会重新 render, 父级组件不受影响
          this.setState({
            name: 'nancy'
          });
        }
    }
}

所以在较小的组件内,比如单选框,要通过state来改变样式的话,就可以直接使用Reactstate,更新状态只在该组件内,而不是使用reduxstore,勾选后发送action, reducer 处理数据, 造成所有正在被加载的组件都重新 render。

这样结构就比较清晰,将小组件的影响分离了出来。

10、直接在 URL 上修改参数

1
2
3
4
5
6
7
# 以下按顺序发生
1. url
2. url?params=123   => 直接按enter => 刷新动作
3. url?params=12 => 直接按enter => update
4. url?params=112 => 直接按enter => update
5. url?params=122 => 直接按enter => update
6. url?params= => 直接按enter => 刷新动作

11、Reducer 与 entites

1、我们把所有的 reducer都合并成了一个 rootRecuder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default combineReducers({
  router: routerStateReducer,
  loginEntity,
  homeEntity
})

// 实际上转换的是:

export default {
  router: routerStateReducer(state.router, action),
  // 注意这里的 state.loginEntity,这就是为什么我们在自己的 reducer 只能够拿到自己相关 entites 内容的原因。
  loginStzEntity: loginStzEntity(state.loginStzEntity, action),
  homeEntity: homeEntity(state.homeEntity, action),
}

// 根rootReducer 返回所有 state 的内容

2、将 rootRecuder 跟 store 进行绑定

1
2
3
4
5
6
// 这样子以后发的action通通都会被 rootReducer 所接收到
const store = createStoreWithMiddleware( createStore )(
    rootReducer, initialState
);

// 也就是说一个 action 会被里面所有的子 reducer 所接收~

12、render 的时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Father extends Component(){

    render() {
        return ( <Children></Children> )
    }
}

class Children extends Component(){
    
    doSomething() {
        # setState 后, this.state 的值并不会立马更新!
        this.setState({
            name: "fancy"
        })
        # 这里会执行完,才进行重新 render
        console.log("yo");
    }
    
    render() {
        return ( <div> Yo </div> )
    }
}

13、简易流程

  1. 将 reducer 注册到 store
  2. 用connect 将组件连接 redux
  3. 利用redux从 根组件 注入的 dispatch 去触发 action
  4. 此时 state 和 action 就会传入 reducer

14、优化

React 的机制是这样的,一个父组件下面一大堆子组件,如果父组件进行re-render,那么旗下的所有子组件都会re-render,但是子组件的state``props在未改变的情况下也会re-render,虽然 React 使用了 虚拟 DOM, 但render那么多次总会浪费性能不是么?

所以我们可以用 Pure render decorator,他能够在props未改变的情况下,阻止组件进行re-render,原理就是利用shouldComponentUpdate

1
2
3
4
5
6
7
8
function shouldComponentUpdate(nextProps, nextState) {
  return shallowCompare(this, nextProps, nextState);
}

function pureRende(component) {
  component.prototype.shouldComponentUpdate = shouldComponentUpdate;
}
module.exports = pureRender;

但这种方案还是有缺憾:

当 props 有复杂类型时,我们会遇到两种情况

情况一,我修改detail的内容,而不改detail的引用

这样就会引起一个bug,比如我修改detail.name,因为detail的引用没有改,所以 props.detail ===nextProps.detail 还是为true。。 所以我们为了安全起见必须修改detail的引用,(redux的reducer就是这么做的)

情况二,我修改detail的引用

这种虽然没有bug,但是容易误杀,比如如果我新旧两个detail的内容是一样的,岂不是还要,render。。所以还是不完美,,你可能会说用 深比较就好了,,但是 深比较及其消耗性能,要用递归保证每个子元素一样,

解决方案是使用 immutable.js

暂时先这样吧,有空再补上其他的。

Promise 总结

什么是 Promise

简单来说, Promise 是把类似的异步处理对象和处理规则进行规范化,并按照统一的接口来编写,这样我们就能够避免写很多的 callback ( 相信有不少人已经对 callback 有恐惧心理了 )

比如正常 callback 写法:

1
2
3
4
5
6
getAsync("fileA.txt", function(error, result){
    if(error){// 取得失败时的处理
        throw error;
    }
    // 取得成功时的处理
});

Promise 写法:

1
2
3
4
5
6
var promise = getAsyncPromise("fileA.txt");
promise.then(function(result){
    // 获取文件内容成功时的处理
}).catch(function(error){
    // 获取文件内容失败时的处理
});

相比之下,Promise 更加清晰

如何支持 Promise

目前来说,上图的相应浏览器已经原生支持 ES6 Promise 标准了,就是说你可以不用加任何库就可以在浏览器使用Promise

那么在不支持Promise的浏览器上该怎么办呢?我们则需要引入Promise的实现库 Polyfill

jakearchibald/es6-promise

一个兼容 ES6 Promises 的Polyfill类库。 它基于 RSVP.js 这个兼容 Promises/A+ 的类库, 它只是 RSVP.js 的一个子集,只实现了Promises 规定的 API。

yahoo/ypromise

这是一个独立版本的 YUI 的 Promise Polyfill,具有和 ES6 Promises 的兼容性。 本书的示例代码也都是基于这个 ypromise 的 Polyfill 来在线运行的。

getify/native-promise-only

以作为ES6 Promises的polyfill为目的的类库 它严格按照ES6 Promises的规范设计,没有添加在规范中没有定义的功能。 如果运行环境有原生的Promise支持的话,则优先使用原生的Promise支持。

如果是使用了Babel来编译ES6的话,则需要使用babel-polyfill

基本用法

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
function asyncFunction() {
    return new Promise(function (resolve, reject) {
        var num = Math.random() * 100;
        if (num > 50) {
            // 成功事件
            resolve('yo');
        } else {
            // 失败事件
            reject('sad');
        }
    });
}

// 第一种写法
asyncFunction().then(function (value) {
    console.log(value);    // => 'yo'
}).catch(function (error) {
    console.log(error);    // => 'sad'
});

// 第二种写法
asyncFunction().then(function (value) {
    console.log(value);    // => 'yo'
}, function (error) { // 注意这里是个逗号
    console.log(error);    // => 'sad'
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# then() 方法可以同时处理成功状态以及失败状态
promise.then(function onResolve(){}, function onReject(){})

# 不过最好还是使用下面这种方法
# 因为这样在 then() 中发生的错误也可以在 catch() 内捕捉到
promise.then(
    function onResolve(){}
).catch(
    function onReject(){}
)

# promise.catch() 相当于
promise.then(undefined, onReject(){
    ...
});

Promise.resolve

一般来说我们会用 new Promise() 来创建 promise, 但也可以用其他方法

1
Promise.resolve(42)

可以认为是下面的语法糖

1
2
3
4
# 直接将状态变为成功状态
new Promise(function(resolve){
    resolve(42);
})

Promise.resolve(42) 返回的也是一个 promise 对象,因此我们可以直接连用then方法

1
2
3
Promise.resolve(42).then(function (value){
    console.log(value)
})

Promise.reject

相应的,Promise.reject也是一个快捷方式

1
Promise.reject(42)

相当于

1
2
3
new Promise(function(resolve, reject){
    reject(42);
})

那么我们可以直接:

1
2
3
Promise.reject(new Error("BOOM!")).catch(function(error){
    console.error(error);
});

异步调用

在使用Promise.resolve()时,promise对象立马进入了 resolve 状态,那then()的方法是不是立马同步调用呢?

1
2
3
4
5
6
7
8
var promise = new Promise(function (resolve){
    console.log("inner promise"); // 1
    resolve(42);
});
promise.then(function(value){
    console.log(value); // 3
});
console.log("outer promise"); // 2

也许你会以为输出结果是:

1
2
3
inner promise // 1
42            // 2
outer promise // 3

但其实是:

1
2
3
inner promise // 1
outer promise // 2
42            // 3

说明了then()方法并不是同步调用的,而是异步调用。

因为同步与异步一起使用的话,会造成混乱,因为 Promise 规范也指明了:Promise 只能够使用异步调用方式

绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。

如果对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料之外的后果。

对异步回调函数进行同步调用,还可能导致栈溢出或异常处理错乱等问题。

如果想在将来某时刻调用异步回调函数的话,可以使用 setTimeout 等异步API。

Effective JavaScript — David Herman

方法链

想必使用过 JQuery 的人都已经使用过方法链了。

1
2
3
4
5
6
7
promise.then(function taskA(value){
    return 'hello'
}).then(function taskB(value){
    console.log(value)   // => 'hello'
}).catch(function onRejected(error){
    console.log(error);
});

then()catch()都会返回一个新的promise对象,这也是为什么可以使用方法链的原因。

前一个then()的回调函数中,返回的值不同也会有不同的情况:

  1. 回调函数返回的是非 promise 对象,那么该返回值将会传给第二个then()resolved回调函数中(即 promise 默认为 resolved 状态)
  2. 回调函数返回的是promise 对象 A,那么then()生成的新 promise B将会把A的状态以及返回值占为己有。
  3. 没有返回值,then()返回的默认为 resolved 状态的 promise

Reject or Throw?

Promise的构造函数,以及被 then 调用执行的函数基本上都可以认为是在 try...catch 代码块中执行的,所以在这些代码中即使使用 throw ,程序本身也不会因为异常而终止。

1
2
3
4
5
6
var promise = new Promise(function(resolve, reject){
    throw new Error("message");
});
promise.catch(function(error){
    console.error(error);// => "message"
});

但是一般来说我们使用Reject会更符合我们想把promise置为reject的意思。

1
2
3
4
5
6
var promise = new Promise(function(resolve, reject){
    reject(new Error("message"));
});
promise.catch(function(error){
    console.error(error);// => "message"
})

那么在then中如何使用reject呢?我们可以在回调函数中返回rejected状态的promise对象,那么此时promise就会变成rejected状态了。

1
2
3
4
5
var onRejected = console.error.bind(console);
var promise = Promise.resolve();
promise.then(function () {
    return Promise.reject(new Error("this promise is rejected"));
}).catch(onRejected);

Then Or Catch ?

前面我们说过,catch(function(){})其实就是等于then(undefined, function(){})

那为何我们建议用catch()来进行捕获rejected状态的 promise 呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function throwError(value) {
    // 抛出异常
    throw new Error(value);
}
// <1> onRejected不会被调用
function badMain(onRejected) {
    return Promise.resolve(42).then(throwError, onRejected);
}
// <2> 有异常发生时onRejected会被调用
function goodMain(onRejected) {
    return Promise.resolve(42).then(throwError).catch(onRejected);
}
// 运行示例
badMain(function(){
    console.log("BAD");
});
goodMain(function(){
    console.log("GOOD");
});

就是说,当then(resolve(){}, reject(){}) 内的resolve(){}方法发生错误的时候,此时reject(){}函数并不能够去捕捉到。而使用catch()则可以。

Promise.all

Promise.all 接收一个 promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用 .then 方法。

1
2
3
4
5
6
7
8
9
first_promise = new Promise(function(resolve, reject){
    ...
})
second_promise = new Promise(function(resolve, reject){
    ...
})
Promise.all([first_promise, second_promise]).then(function(results)){
    console.log(results)  // 按照数组顺序返回的值
}

需要注意的是,数组内的 promise 是同时执行的。

Promise.race

Promise.all 在接收到的所有的对象promise都变为 FulFilled 或者 Rejected 状态之后才会继续进行后面的处理, 与之相对的是 Promise.race 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    });
}
// 任何一个promise变为resolvereject 的话就执行then()方法
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function (value) {
    console.log(value);    // => 1
});

关于 IE8

现在的浏览器基本都是基于 ECMAScript 5 的,但是 IE8 以及以下的浏览器都是基于 ECMAScript 3 的,而 catchECMAScript 3是保留字,所以使用.catch()会报错

解决方法:

1
2
3
4
var promise = Promise.reject(new Error("message"));
promise["catch"](function (error) {
    console.error(error);
});

OVER the WALL

OVER THE WALL

1
2
3
4
5
小时候,自由是一张破旧的五角钱,我在这头,美食在外头。

长大了,自由是一纸灼人的高考卷,我在这头,大学在外头。

后来啊,自由是一道宏伟的柏林墙,我在这头,世界在外头。

“子轩,把那个什么门发我下。”

犹记得第一次接触「wall」的概念,还是大学第二学期,在听子轩介绍完 “自由门” 的神奇之后,就立马让他发给了我,从此打开了 “新世界” 的大门~

工具

现在的翻墙方式挺多的,主流的有:

  1. VPN
  2. 代理
  3. ShadowSocks

VPN

VPN 相当于虚拟出一个虚拟网卡,流量全部走这个虚拟网卡,而如果做了智能分流(修改路由表)的话,访问国内网站会走真实网卡,其他的走虚拟网卡。

VPN 推荐:云梯

代理

顾名思义,将要访问的网站将会通过「代理服务器」来进行中转,操作简单,只需设置下代理地址即可。

代理 推荐:轻云

ShadowSocks

SS 则是通过 SOCKS5 代理的方式,如果你是用 Mac 的话,点开 Wifi 设置 => 高级,就会看到代理的选项。

能否代理要看使用的软件支不支持 SOCKS5 代理,有些软件只支持 HTTP 代理,那就没辙了

1
2
3
Chrome : 点开设置你会发现它是默认是用系统代理设置的
QQ : 不使用系统代理设置,可以自定义代理设置
邮件: 默认系统代理设置

SS 推荐: crola.xyz ( 现在注册需要邀请码,有需要可以找我拿 )

SHELL 无法代理?

由于 VPN 是全局流量,因此 SHELL 的流量也是走 VPN 的,因此也处于「翻墙状态」

但 代理 & ShadowSocks 的方式不一样,需要程序支持

而SHELL 只支持 HTTP 代理,并且需要如下设置:

1
2
# .zshrc / .bashrc 等配置文件之一
export http_proxy=proxy_addr:port

如此 SHELL 也处于了「翻墙」的状态了。

但是 ShadowSocks 是通过 SOCKS5 的方式,并不支持 HTTP 流量,因此我们需要工具来将流量转换成 SOCKS5 并且转发给 SS 代理服务。

常见的工具有 ProxyChains-NG、polipo

ProxyChains-NG

项目主页: https://github.com/rofl0r/proxychains-ng

安装

1
brew install proxychains-ng

配置

1
$ vim /usr/local/etc/proxychains.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
# edit
strict_chain
proxy_dns
remote_dns_subnet 224
tcp_read_time_out 15000
tcp_connect_time_out 8000
localnet 127.0.0.0/255.0.0.0
quiet_mode

[ProxyList]
socks5  127.0.0.1 1080

# 本地开启的服务正是 1080 端口

使用

1
2
3
4
5
6
7
proxychains4 -q curl 'google.com'

## 添加 alias(在 .zshrc / .bashrc ....)
alias ss="proxychains4 -q"

## 简化
ss curl 'google.com'

过程

1
2
3
SHELL 发出 request => ProxyChains-NG  HTTP 流量转换成 SOCKS5 流量 =>

本地 SS 服务 转发请求到 远程 SS 服务器 => SS 服务器请求实际 request,  response 返回

10.11

由于 Mac OSX 10.11 增加了安全机制,导致无法使用,唯一方法就是关闭 SIP

  1. 首先重启系统,在开机时按住 Command+R 来进入 Recovery 模式
  2. 菜单栏中找到 terminal, 打开
  3. 执行: csrutil disable
  4. 如果想恢复,可以执行: csrutil enable

polipo

1
brew install polipo
1
vim /usr/local/opt/polipo/homebrew.mxcl.polipo.plist
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
   <dict>
     <key>Label</key>
     <string>homebrew.mxcl.polipo</string>
     <key>RunAtLoad</key>
     <true/>
     <key>KeepAlive</key>
     <true/>
     <key>ProgramArguments</key>
     <array>
       <string>/usr/local/opt/polipo/bin/polipo</string>
       <string>socksParentProxy=localhost:1080</string>
     </array>
     <!-- Set `ulimit -n 65536`. The default macOS limit is 256, that's
          not enough for Polipo (displays 'too many files open' errors).
          It seems like you have no reason to lower this limit
          (and unlikely will want to raise it). -->
     <key>SoftResourceLimits</key>
     <dict>
       <key>NumberOfFiles</key>
       <integer>65536</integer>
     </dict>
   </dict>
 </plist>

开机自启动

1
2
ln -sfv /usr/local/opt/polipo/*.plist ~/Library/LaunchAgents
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.polipo.plist
1
vim ~/.zshrc
1
2
alias proxy="export http_proxy=http://localhost:8123;export https_proxy=http://localhost:8123"  # 开启
alias unproxy="unset http_proxy"  # 关闭
1
2
3
4
5
6
7
8
# 开始使用代理
$ proxy
$ todo ...


# 取消使用代理
$ unproxy
$ todo ...

Ping 命令

1
2
# 有人会发现 ping 无法翻墙
ss ping google.com

因为 Ping 命令是使用 ICMP 协议,而 ProxyChains 只支持 TCP and DNS, So ~~

http://proxychains.sourceforge.net/

Rails’s Association

Rails 中的关联

前言

這篇文章是好久好久好久之前所做的笔记,Po 上来吧

多对多

1、使用 has_and_belongs_to_many

1
2
3
4
5
6
7
8
9
10
11
12
13
class Teacher < ActiveRecord::Base
    has_and_belongs_to_many :students
end

class Student < ActiveRecord::Base
    has_and_belongs_to_many :Teacher
end

# 手动创中间表(migration,注意该表没有主键)
create table :teachers_students, id: false do |t|
    t.integer :teacher_id
    t.integer :student_id
end

这种方式是最简单的,不依靠第三个 model 来进行关联。但也仅仅局限于此,其他复杂的关联的都干不了,所以一般不会去用这关联

2、使用 has_many :through

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 用户
class User < ActiveRecord::Base
  has_many :comments
  # 如果用户不需要找到帖子, 那么可以不用写关联
  has_many :posts, through: :comments
end

# 帖子
class Post < ActiveRecord::Base
  has_many :comments
  has_many :users, through: :comments
end

# 评论
class Comment < ActiveRecord::Base
  belongs_to :posts
  belongs_to :users
end

在这里,用户与帖子就是多对多的关系,通过comment这 model 来进行关联。

总结: 当需要做数据验证``回调``关联模型需要用到其他属性``关联需要作为一个独立实体则要用has_many :through

has_and_belongs_to_many已经不建议使用了,因此直接用has_many :through便是。

一对一

1、has_one :through 是个挺好用的特性。

Example: 每个供应商有一个账号,每个账号有一个历史账号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 供应商
class Supplier < ActiveRecord::Base
  has_one :account
end

# 账号
class Account < ActiveRecord::Base
  belongs_to :supplier
  has_one :account_history
end

# 历史账号
class AccountHistory < ActiveRecord::Base
  belongs_to :account
end

如果是这么写的话,只能这么操作:

1
2
# 供应商通过账号来查找历史账号
supplier.account.account_history

加上 through:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 供应商
class Supplier < ActiveRecord::Base
  has_one :account
  has_one :account_history, through: account
end

# 账号
class Account < ActiveRecord::Base
  belongs_to :supplier
  has_one :account_history
end

# 历史账号
class AccountHistory < ActiveRecord::Base
  belongs_to :account
end

这样供应商直接找到历史账号:

1
supplier.account_history

多态关联

有时我们会有这样的需求:帖子需要图片的关联,评论需要图片的关联

1
2
3
4
5
6
7
8
9
10
11
class Picture < ActiveRecord::Base
  belongs_to :imageable, polymorphic: true
end

class Post < ActiveRecord::Base
  has_many :pictures, as: :imageable
end

class Comment < ActiveRecord::Base
  has_many :pictures, as: :imageable
end

这样图片这个model就可以同时属于Post以及Comment了。

1
2
3
4
5
6
# 获取子对象
post.pictures
comment.pictures

# 获取父对象
picture.post

Migration 如下:

1
2
3
4
5
6
7
8
9
10
11
12
class CreatePictures < ActiveRecord::Migration
  def change
    create_table :pictures do |t|
      t.string  :name
      # 外键id
      t.integer :imageable_id
      # 具体是属于哪个父对象
      t.string  :imageable_type
      t.timestamps
    end
  end
end

别名

1、has_many :through

有时我们关联的 model 名会跟想要使用的名称不一样,比如: Genre、 App 与关联类 GenreRecommend (分类,应用,分类下的推荐应用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Genre < ActiveRecord::Base
  has_many :genre_recommends
  # 找到该分类下的推荐 apps
  has_many :apps, through: :genre_recommend
end

class App < ActiveRecord::Base
  has_many :genre_recommends
  # 这里并不需要找到 genre, 因此可以不写
  has_many :genres, through: :genre_recommend
end

class GenreRecommend < ActiveRecord::Base
  belongs_to :genre
  belongs_to :app
end
1
2
3
4
5
# 一般使用(但 apps 有可能会跟其他冲突, 比如找出该分类下的所有 apps)
@genre.apps

# 实际上这么使用会更友好
@genre.recommends

修改后:( 通过 source )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Genre < ActiveRecord::Base
  has_many :genre_recommends
  # 找到该分类下的推荐 apps
  has_many :recommends, source: :app, through: :genre_recommend
end

class App < ActiveRecord::Base
  has_many :genre_recommends
end

class GenreRecommend < ActiveRecord::Base
  belongs_to :genres
  belongs_to :app
end

2、belongs_to``has_one``has_many

这三者都是通过:class_name来实现别名的。

1
2
3
class Customer < ActiveRecord::Base
  has_many :orders, class_name: "Transaction"
end

我猜是因为source主要是跟through的第三个 model 中belongs_to对应

class_name就仅仅只是指定类名了。

关联删除

一对多关系中,往往删除主对象,我们还需要顺便删除所有子对象的需求。

Example: 一个人拥有几个联系方式

1
2
3
4
5
6
7
8
class Person < ActiveRecord::Base
  # 这样删除一个 person 时,还会删除名下的所有 contacts
  has_many :contacts, dependent: :destroy
end

class Contact < ActiveRecord::Base
  belongs_to :person
end

自动保存对象

1
2
3
4
5
6
7
class Person < ActiveRecord::Base
  has_many :contacts, dependent: :destroy
end

class Contact < ActiveRecord::Base
  belongs_to :person
end
1
2
3
4
5
6
7
@person = Person.first
c1 = Contact.new
c2 = Contact.new

# 此时会自动保存 c1 c2,官方说因为 c1 c2 要更新外键 ( 这点不太明 )
@person.contacts << c1
@person.contacts << c2

将对象赋给has_many``has_one的主对象都会被自动保存。

当然,主对象必须是有保存过的才行。(存在于数据库)

counter_cache

Example:

帖子下有多个评论,显示时经常需要显示评论的个数。评论的个数经常会变动,如果直接post.comments.size难免效率会低。

1
2
3
4
5
6
7
8
9
class Post < ActiveRecord::Base
  has_many :comments
  # 只读,否则被串改就不好了。
  attr_readonly :comments_count
end

class Comment < ActiveRecord::Base
  belongs_to :post, counter_cache: true
end

创建字段

1
2
3
def change
  add_column :posts, :comments_count, :integer
end

这样声明后,Rails 会及时更新缓存,调用 size 方法时,会返回缓存中的值 ( 官方 guide 是说缓存,不过我认为仅仅只是将值放在该 model 的字段中,并无缓存 )

1
@post.comments.size

点击数、阅读数

文章的阅读数``点击数也是经常变化,有无更好的方法呢?例如存入缓存?

可以使用我自己写的一个 GEM

https://github.com/linjunzhu/counter-cache-credis

如何使用monit监控进程

前言

前阵子服务器上的 puma 偶尔会挂掉,puma 不像 unicorn 有 master 进程保护着。因此就需要用 monit 来监控进程,当 puma 挂掉时自动重启

安装

Ubuntu:

1
aptitude install monit

其他系统的安装方式: 官网 Wiki

基本操作

monit status : 查看 monit 监控的服务状态 ( 需要开启 web 服务)

monit reload : 重新加载配置文件

monit -t : 检测配置文件是否正确

monit quit : 杀掉进程

monit -c /etc/monit/monitrc : 启动 monit

monit start server_name : 启动某个服务

其他的输入 monit -h 查看即可。

介绍

配置文件位于:/etc/monit/monitrc

打开 Web 服务,查看监控情况

1
2
3
4
5
6
 set httpd port 2812 and
    use address 172.16.10.6
    allow localhost   # 允许本地访问
    allow 172.16.20.0/255.255.255.0 #允许本 IP 段访问
    allow admin:monit      # require user 'admin' with password 'monit'

说下配置文件默认配置

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
<!-- 检查周期,单位为秒-->
set daemon 120

<!-- 日志文件位置 -->
set logfile /var/log/monit.log

<!-- 存储 monit 实例的唯一 ID-->
set idfile /var/lib/monit/id

<!-- 存放监控进程的情况 -->
set statefile /var/lib/monit/state

<!-- 设置邮件-->
set mailserver smtp.aa.com port 25 USERNAME "aa@aa.com" PASSWORD "123456"

# 如果没配置 email, 会自动丢弃 alert
# 该设置可以保存 alert
set eventqueue
      basedir /var/lib/monit/events
      slots 100


<!-- Email 格式 -->
set mail-format {
     from: monit@$HOST
  subject: monit alert --  $EVENT $SERVICE
  message: $EVENT Service $SERVICE
                Date:        $DATE
                Action:      $ACTION
                Host:        $HOST
                Description: $DESCRIPTION

           Your faithful employee,
           Monit
}

# 制定报警邮件的格式
set mail-format {
  from: aa@aa.com
  subject: $SERVICE $EVENT at $DATE
  message: Monit $ACTION $SERVICE at $DATE on $HOST: $DESCRIPTION.
}

<!-- Email 接收人-->
 set alert sysadm@foo.bar with reminder on 3 cycles

<!-- 开启 http 服务,查看监控情况-->

set httpd port 2812 and
    use address 172.16.10.6  # only accept connection from localhost
    #allow localhost
    #allow 172.16.20.0/255.255.255.0
    #allow 10.0.0.0        # allow localhost to connect to the server and
    allow admin:monit      # require user 'admin' with password 'monit'
   # allow @monit           # allow users of group 'monit' to connect (rw)
   # allow @users readonly  # allow users of group 'users' to connect readonly

include /etc/monit/conf.d/*

我们会在在monitrc 文件中的末尾看到这句

1
2
# 包含该文件夹下的配置
include /etc/monit/conf.d/*

因此我们可以给每个要监控的进程写个conf配置文件。

重载配置

1
2
# 检查语法
sudo monit -t
1
2
# 重载配置
sudo monit reload

监控硬盘

/etc/monit/conf.d下新建文件filesystem.conf

1
2
3
check filesystem datafs with path /dev/vdb
  if space usage > 50% then alert
  group server

监控 Nginx

/etc/monit/conf.d下新建文件nginx.conf

1
2
3
4
check process nginx with pidfile /opt/nginx/logs/nginx.pid
  start program = "/etc/init.d/nginx start"
  stop program  = "/etc/init.d/nginx stop"
  group www-data

监控 Puma

/etc/monit/conf.d下新建文件puma.conf

1
2
3
4
check process puma_glassx
  with pidfile "/mnt/glassx/shared/tmp/pids/puma.pid"
  start program = "/usr/bin/sudo -u deployer /bin/bash -c 'cd /mnt/glassx/current && ~/.rvm/bin/rvm ruby-2.1.2 do bundle exec puma -C /mnt/glassx/shared/puma.rb --daemon'"
  stop program = "/usr/bin/sudo -u deployer /bin/bash -c 'cd /mnt/glassx/current && ~/.rvm/bin/rvm ruby-2.1.2 do bundle exec pumactl -S /mnt/glassx/shared/tmp/pids/puma.state stop'"

当我们手动 kill 掉进程,过一会就会发现进程又自动启动了~

环境

monit 用的是最基础的环境,个人的环境变量是没加载的,因此如果要执行一些命令,但却找不到命令时,需要注意环境变量的问题。

1
2
3
4
5
6
# 这里使用 /bin/su - deploy ,以用户deploy的环境去执行后续命令
check process process_server
  with pidfile "xxxx.pid"
  start program = "/bin/su - deploy  -c 'ruby xxx"
  stop program = "/bin/su - deploy  -c 'ruby xxx'"
 if 5 restarts with in 10 cycles then timeout

搭配 Capistrano

打开capfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Load DSL and Setup Up Stages
require 'capistrano/setup'

require 'capistrano/deploy'
#
require 'capistrano/rvm'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano/sidekiq'
# 打开 monit 的监控 tasks
require 'capistrano/sidekiq/monit'
require 'capistrano/puma'
# 打开 monit 的监控 tasks
require 'capistrano/puma/monit'

# Loads custom tasks from `lib/capistrano/tasks' if you have any defined.
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

由于sidekiq会在跑deploy的时候就执行 monit 相关任务,并且需要sudo权限,很麻烦,所以我们可以手动关掉,等到部署完成后再执行任务上传 monit 脚本

打开deploy.rb

1
set :sidekiq_default_hooks, false

跑完deploy后,执行以下两条命令

1
2
3
# 将 monit 的监控配置上传到服务器
cap production puma:monit:config
cap production sidekiq:monit:config

这两条命令在执行时需要sudo权限,原因是调用了

1
2
sudo mv /tmp/monit.conf /etc/monit/conf.d/puma_glassx.conf
sudo mv /tmp/monit.conf /etc/monit/conf.d/sidekiq_glassx.conf

因为我们只是上传有关 monit 的配置脚本而已,只需执行一次,因此我们在执行失败后,进入服务器手动输入这两条命令,即可完成。

Monit 自启动

如果服务器重启了,也要自启动 monit 才行,我们可以使用 Ubuntu 的 upstart 工具

/etc/init/monit.conf

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
 # This is an upstart script to keep monit running.
 # To install disable the old way of doing things:
 #
 #   /etc/init.d/monit stop && update-rc.d -f monit remove
 #
 # then put this script here:
 #
 #   /etc/init/monit.conf
 #
 # and reload upstart configuration:
 #
 #   initctl reload-configuration
 #
 # You can manually start and stop monit like this:
 # 
 # start monit
 # stop monit
 #

 description "Monit service manager"

 limit core unlimited unlimited

 start on runlevel [2345]
 stop on starting rc RUNLEVEL=[016]

 expect daemon
 respawn

 exec /usr/local/bin/monit -c /etc/monit/monitrc

 pre-stop exec /usr/local/bin/monit -c /etc/monit/monitrc quit
1
2
3
4
# 加载配置文件
initctl reload-configuration
# 启动 monit
start monit

详细资料

官网 WIKI:

https://mmonit.com/wiki/Monit/ConfigurationExamples#NginX

https://mmonit.com/monit/documentation/monit.html#CONFIGURATION-EXAMPLES

Rails中「错误信息」的实践规划

前言

在我还是一个 Rails 新手的时候,曾纠结过,如何更好的规划显示错误信息,以及实现 I18N。前几天整理一个实习生的代码,发现他对错误信息的规划毫无章法,所以就写下这 blog, Rails 新手可以看看。

基本概念

1
2
3
4
5
6
7
8
9
10
11
12
13
class Account < ActiveRecord::Base
  validates_presence_of :email, :message => "Email 不得为空"
end

account = Account.new

# 此时会调用 account.valid?,该 valid? 会调用所有validate,通过则 true,否则为 false
account.save   => false
account.valid? => false
account.errors.empty?  => false
account.email = 'linjunzhugg@gmail.com'
account.valid? => true
account.save   => true

来看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# File activemodel/lib/active_model/validations.rb, line 331
def valid?(context = nil)
  current_context, self.validation_context = validation_context, context
  errors.clear
  run_validations!
ensure
  self.validation_context = current_context
end

# File activemodel/lib/active_model/validations.rb, line 394
def run_validations!
  _run_validate_callbacks
  errors.empty?
end

从上面可以看到,valid? 会将验证结果放在对象的 errors中,如果对象的 errors 没东西,那么会返回 true,验证通过,返回 false,则验证不通过,save 会失败。

开始实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# user.rb
class Account < ActiveRecord::Base
  # 不要在这里手动写 message,应该在 locals 中的 yml 文件写
  # validates_presence_of :email, :message => "Email 不得为空"
  validates_presence_of :email
end

# users_controller.rb
def create
  @user = User.new(user_params)
  if @user.save
    render :index
  else
    # save 失败后会将错误信息存入  @user.errors
    # 就不用自己在这里写错误信息了
    render :new
  end
end

Rails 有默认的错误信息,我们可以根据需要自定义错误信息

对应错误信息 Key

观看 Wiki: http://guides.ruby-china.org/i18n.html

自定义错误信息

validates_presence_of的错误信息 key 是 blank,Active Record 会按照下面的顺序寻找blank:

1
2
3
4
5
activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank

如果有继承链,则会往继承链上找:

1
2
3
class Admin < User
  validates :name, presence: true
end
1
2
3
4
5
6
7
activerecord.errors.models.admin.attributes.name.blank
activerecord.errors.models.admin.blank
activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank

那么我们可以在 locales 内的 yml 文件中做文章:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
zh-CN:
  activerecord:
    attributes:
      user:
        email: 'Email'
        password: '密码'
      work_order:
        title: '标题'
        question: '问题'
    errors:
      # 执行 errors.full_message 会按照此格式输出
      format: ! '%{attribute}%{message}'
      # 这里作为通用的错误信息
      messages:
        blank: "%{attribute} 不能为空"
        invalid: "%{attribute} 格式不正确"
        taken: "%{attribute} 已经被使用"
      # 这里作为针对性的 model 错误信息
      models:
        user:
          attributes:
            password:
              too_short: '密码 不得小于6位'

Tips

1
2
3
4
5
6
7
8
9
10
11
12
zh-CN:
  activerecord:
    attributes:
      user:
        email: 'Email'

  avtiverecord:
    errors:
      messages:
        blank: "%{attribute} 不能为空"

不要这么写,这样后面的 activerecord 会覆盖掉前面的东西,而不是作为一个 namespace 的存在。

显示错误信息

我们回到 controller 再看下:

1
2
3
4
5
6
7
8
9
10
# users_controller.rb
def create
  @user = User.new(user_params)
  if @user.save
    render :index
  else
    # 直接在 view 层去显示错误信息
    render :new
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# users_helper.rb
def show_error model, column
  if model.send('errors')[column][0]
    sanitize "<div class='error-msg'>* #{model.send('errors')[column][0]}</div>"
  end
end

# users/new.html.erb

<html>
<body>
  oooooooooxxxxxxxxx
  # 此时这里就可以按照需求显示出我们在 yml 中自定义的错误信息了
  show_error(@user, :avatar)
</body>
</html>

非 Active Record 操控错误信息

1
2
3
4
5
6
7
8
9
10
11
# users_controller.rb

def update
  if user_pass?
    # 自己在 yml 中定义错误信息
    flash[:success] =  I18n.t("web.messages.success")
  else
    flash[:error] =  I18n.t("web.messages.failed")
  end

end

自定义 validate 该如何定义错误信息

1
2
3
4
5
6
7
8
9
10
11
12
class User < ActiveRecord::Base
  validate :file_size

  private

  def file_size
    if avatar.file.size.to_f/(1000) > 500
      errors[:avatar] << I18n.t("activerecord.errors.models.user.attribuets.avatar.too_long")
    end
  end
end

为何在这里调用 errors[:avatar],错误信息就会放到 @user 中呢?

因为validate是在@user进行save时,才会调用,可以说是实例方法

1
2
3
  def file_size
    p self   => 会发现其实是当前实例 @user
  end

这种方式只是简单的将错误信息放入errors,而且在yml并不能够获取%{attribute}参数

1
 errors[:avatar] << I18n.t("activerecord.errors.models.user.attribuets.avatar.too_long")

我们来更进一步:

1
2
3
4
5
6
7
def file_size
  if avatar.file.size.to_f/(1000) > 500
    # 这样就会调用到`model`的错误信息 (:column, :event_key)
    # 也不用写那么长了
    errors[:avatar] << errors.generate_message(:avatar, :too_long)
  end
end

移除column错误产生的field_with_errors

在 form 表单中,如果某个 column 有相关的 errors 信息, Rails 会自动在该column相关的元素父层加多一个 div

1
2
3
4
5
6
<div class="field_with_errors">
  <label for="job_client_email">Email: </label>
</div>
<div class="field_with_errors">
  <input type="email" value="wrong email" name="job[client_email]" id="job_client_email">
</div>

重写掉config/application.rb

1
2
3
4
5
6
7
8
9
# 原先
@field_error_proc = Proc.new{ |html_tag, instance|
  "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe
}

# 重写后(当前也可以根据需求自己改造)
config.action_view.field_error_proc = Proc.new { |html_tag, instance|
  html_tag
}

Capistrano3+Puma+Nginx部署应用

前言

Puma 是支持 Ruby 的一个 Web 应用服务器,基于多线程。

使用环境

  1. Capistrano 3.x
  2. Rails4.2
  3. Ruby2.2
  4. RVM
  5. Puma
  6. Nginx

安装

gemfile中添加支持的 gem

1
2
3
4
5
6
7
8
9
10
11
group :development do
  gem 'capistrano'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano-rvm'
end

group :production do
  # puma 是一个 Gem,服务器不需要安装其他的
  gem 'puma
end

初始化

1
$ cap install

配置

capfile

1
2
3
4
5
6
7
8
9
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/rvm'
require 'capistrano/bundler'
require 'capistrano/puma'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

deploy.rb

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
# config valid only for current version of Capistrano
lock '3.4.0'

set :application, 'xgroup'
set :repo_url, 'git@github.com:linjunzhugg/xx.git'

set :deploy_to, '/mnt/xgroup'

set :scm, :git

# Default value for :linked_files is []
set :linked_files, %w{config/database.yml config/oauth2.yml config/redis.yml config/secrets.yml}

# Default value for linked_dirs is []
set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle}

set :keep_releases, 5

namespace :deploy do

  after :restart, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      # Here we can do anything such as:
      # within release_path do
      #   execute :rake, 'cache:clear'
      # end
    end
  end

  after :finishing, 'deploy:cleanup'

end

production.rb

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
# Simple Role Syntax

set :rvm_type, :user
set :rvm_ruby_string, 'ruby-2.2.1'

set :branch, "master"

server '0.0.0.0', user: 'deployer', roles: %w{app}

# 针对 puma 的配置
set :puma_rackup, -> { File.join(current_path, 'config.ru') }
set :puma_state, "#{shared_path}/tmp/pids/puma.state"
set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock"    #accept array for multi-bind
set :puma_conf, "#{shared_path}/puma.rb"
set :puma_access_log, "#{shared_path}/log/puma_error.log"
set :puma_error_log, "#{shared_path}/log/puma_access.log"
set :puma_role, :app
set :puma_env, fetch(:rack_env, fetch(:rails_env, 'production'))
set :puma_threads, [0, 120]
set :puma_workers, 0
set :puma_worker_timeout, nil
set :puma_init_active_record, false
set :puma_preload_app, true

# 最终会创建 shared/puma.rb,将配置写进去,然后调用

envrioments/production.rb

1
2
3
4
5
6
# Disable serving static files from the `/public` folder by default sinc
# Apache or NGINX already handles this.
config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present

# important!!!
# 在 production.rb 有这么个配置,决定服务器是否伺服静态文件,默认服务器没设这个变量,因此为 false, 默认不伺服静态文件

服务器配置

xgroup.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
upstream xgroup {
   server unix:/mnt/xgroup/shared/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name xx
  root /mnt/xgroup/current/public

  # 由于服务器不伺服静态文件,所以自己需要手动配置
  location ~* /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://xgroup;
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 也可以这么做:

upstream xgroup {
   server unix:/mnt/xgroup/shared/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name xx
  root /mnt/xgroup/current/public

  # 先去访问机器上对应的路径文件,访问不了再去 @xgroup
  # 如果不加这句,会访问不了静态文件  
  try_files $uri/index.html $uri @xgroup;
  location @xgroup {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://xgroup;
  }

}

Over~

Content-type与json

前言

最近有两个 Android 同事在调用我写的 API 时,向我反映调用有问题,仔细一查,发现是客户端所设置的 Content-Type 与 Accept 有关系。所以写一下,后续有同事还有疑问,就可以丢这文章给他了~~

Content-Type

该 header 头是用来告诉服务器,请求体是如何进行编码的,以便服务器进行相对应的解码。

一般来说,有如下几种:

  1. application/x-www-form-urlencoded
  2. multipart/form-data
  3. application/json
  4. application/xml
  5. text/html
  6. text/plain

等等

application/x-www-form-urlencoded

这是我们最常见的 content-type 头了,我们使用浏览器原生 form 表单,提交时便会自动设置 content-type 为这个,其中提交的数据按照:key1=val1&key2=val2进行编码,key val 会进行 URL 转码

注意:content-type 头仅仅只是告诉服务器要如何解码,跟浏览器发送请求时对请求体如何编码无关。

也就是说,此时用 form 表单提交,但是 content-type 手动设置为 application/json,那么浏览器还是会以 form 形式对请求体编码。

multipart/form-data

当我们需要上传文件时,就会用到这种方式。

1
2
3
4
5
6
7
8
9
10
11
12
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA

------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"

title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png

PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

浏览器会以multipart/form-data形式进行编码,同时设置 content-type

application/json

这种方式在当今 restful 盛行的年代,特别常用。

我们只需要将 json 格式的参数附带到请求体便是,同时手动设置 content-type 为 application/json ( jquery 中的 ajax,设置某个参数为 json即可,还有其他的,主要看框架是否会自动处理,否则就手动设置)

Accept

该 header 头是告诉服务器,客户端需要什么格式的响应。

  1. Accept : application/json
  2. Accept : text/html

要求服务器返回响应的格式

一、

平常客户端设置该 Accept 参数,即可要求服务端返回响应类型的数据。

二、

直接在 URL 尾部添加 .json,这种方式需要服务端的支持,告诉服务端,我需要json的响应

在 Rails 中,如果附带.json,会自动解析成一个参数 params[:format] = ‘json’,之后便能通过代码针对不同解析返回不同数据

1
2
3
4
5
# Rails 会自动处理 Accept 以及 .json 两种情况
respond_to do |format|
  format.html { redirect_to groups_url }
  format.json
end

三、

在 Rails 中设置默认 format

1
resources :users, defaults: {format: :json}

这样服务器就会默认有 format 为 json 的参数

那我同时设置了Accept以及format怎么办?

在 Rails 中,format的优先级高于Accept

同事遇到的问题

1、调用接口时,反应接口返回了500,看了下,发现是服务器接收到的请求头为:content-type: application/json,但是请求体却是以application/x-www-form-urlencoded的形式进行编码的,因此两者不对称,解析出错。

2、调用接口时,返回的并不是 json 数据,check 后,发现调用接口时,没加 Accept: application/json,并且 URL 尾部没加.json,因此服务器就果断返回text/html对应的数据回去啦~~~

需注意的点

如果没有手动指定 Accept头,一些 HTTP 请求包装库会手动给 Accept 头加上一些信息。如:

1
Accept: application/json,text/plain, */*;

这种情况,Rails 是不认前面的 application/json的,最终 request.format 只会返回 text/html.

原因如下:

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
 def format(view_path = [])
   formats.first || Mime::NullType.instance
 end

 def formats
   fetch_header("action_dispatch.request.formats") do |k|
     params_readable = begin
                         parameters[:format]
                       rescue ActionController::BadRequest
                         false
                       end

     v = if params_readable
       Array(Mime[parameters[:format]])
     elsif use_accept_header && valid_accept_header
       accepts
     elsif extension_format = format_from_path_extension
       [extension_format]
     elsif xhr?
       [Mime[:js]]
     else
       [Mime[:html]]
     end
     set_header k, v
   end
 end

 # 这里一旦匹配到  ,*/* | */*, 就会忽视掉整个 Accept 了。
 BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/

def valid_accept_header # :doc:
    (xhr? && (accept.present? || content_mime_type)) ||
       (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
end

使用Circleci

这货棒棒的好用!

我们每次部署任务时,都会跑测试,测试通过再手动跑部署命令。而有了这货,就可以自动跑测试,跑完自动部署。

第一步

先上Circleci官网注册账号。默认有一个free container,如果该项目是public的,那么有四个free container。也就是你可以同时 hold 四个项目

第二步

选择账号,选择仓库,建立容器container

第三步

只要在 master 上面有新的 commit

就会自动直接跑了!!! 卧槽!!! 什么配置都不用啊!!!!

(当然只是build项目,跑测试,部署到哪里还是要自己添加命令的)

因为 Circleci 会自动检测你是什么项目,然后采取不同的默认配置进行跑你的项目。

circle.yml

通过该配置文件你可以做很多事情,默认可以不用去动他,如果想添加自己的东西,可以写一些参数覆盖掉默认配置。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
machine:
  ruby:
    version: 2.1.2
  services:
    - redis
general:
  branches:
    only:
      - master
      - staging
deployment:
  production:
    branch: master
    commands:
      - bundle exec cap production deploy

指明了需要什么 branch,如何部署,该跑什么 services.默认的services有mysql``Postgres``Redis``MongoDB

容器

每一个项目它都会给我们生成一个 VM,相当于一个虚拟机,因此我们还可以自己ssh上 VM,进行查看问题。