MaE4Nnh

MaE4Nnh 查看完整档案

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

个人动态

MaE4Nnh 收藏了文章 · 9月14日

彻底理清前端单页面应用(SPA)的实现原理 【精读源码】

clipboard.png

随着React Vue前端框架的兴起,出现了Vue-router,react-router-dom等前端路由管理库,利用他们构建出来的单页面应用,也是越来越接近原生的体验,再也不是以前的点击标签跳转页面,刷新整个页面了,那么他们的原理是什么呢?

优质gitHub开源练手项目:

先说说原始的MPA多页面应用:

文末还有新建的QQ以及微信群哦~ 欢迎大家加入~~

传统的多页面应用构建方式:

  • 纯服务端渲染,前后端不分离,使用jsp,jade,'ejs','tempalte'等技术在后台先拼接成对应的HTML结构,然后转换成字符串,在每个对应的路由返回对应的数据(文件)即可
Jade模版服务端渲染,代码实现:
const express= require('express')
const app =express()
const jade = require('jade')
const result = ***
const url path = *** 
const html = jade.renderFile(url, { data: result, urlPath })//传入数据给模板引擎
app.get('/',(req,res)=>{
    res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回给客户端
}) //RestFul接口 

app.listen(3000,err=>{
    //do something
})
  • 使用jQuery等传统库绘制的前端页面

传统前后端不分离,服务端渲染的优缺点:

优点:

  • SEO友好,因为返回给前端的是渲染好的HTML结构,里面的内容都可以被爬虫抓取到。
  • 对于一些应用性能等要求不高的项目,比如某个公司的静态网页,内容很少的情况下,直接一把梭就好,不用再搭建工程化的环境等
  • 对于后端程序员(全干工程师)来说,不用去特意学习前端框架,公司也不用特意去招聘前端
  • 兼容性好,传统服务端渲染多页面应用吐出来的都是字符串,HTML结构

缺点:

  • 如果项目很大,不利于维护,据我所知,目前很多云计算公司,还有不少都是使用非单页面应用,例如一个几十万行的项目是用jQuery写的,如果注释和文档不是非常齐全,那么真的会无从下手
  • 性能和用户体验,不能跟单页面应用相比
  • 后期迭代,升级空间不大,目前大部分写得比较好的库,都建立vue,react等框架基础上,他们都有一套自己的运行机制,有自己的生命周期,并且不像传统的应用,还加上了一层虚拟DOM以及diff算法
  • 现在类似Ant-Design-pro这样的开箱即用的库已经很多,单页面应用的学习和开发成本已经很低很低,如果还在使用传统的技术去开发新的应用,对于开发人员多内心来说也是一种折磨。
这里并不是说多页面应用不好,只能说各有各自的好,单页面应用如果通过大量的极致优化手段,是可以从不少方面跟原生一拼。

clipboard.png

目前的单页面应用:

  • 只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站
  • 其实只有一个空的DIV标签,其他都是js动态生态的内容

单页面应用实现步骤:

代码实现:

  • 首先是一个静态模板文件 index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="root"></div>
</body>
<script>

</script>

</html>
  • vue react框架的入口文件中指定对应的渲染元素:
import React from 'react;
import ReactDOM from 'react-dom';

ReactDOM.render(
<App/>,
document.querySelector("#root")
)
  • 引入react-router或者 react-router-dom,dva等路由跳转的库
  • 配置路由跳转
<HashRouter>//这里使用HashRouter
      <ErrorBoundary>//React错误边界
        <Switch>
          <Route path="/login" component={Login} />
          <Route path="/home" component={Home} />
          <Route path="/" component={NotFound} />//404路由或者重定向都可以
        </Switch>
      </ErrorBoundary>
</HashRouter>

单页面应用所谓路由跳转,其实最终结果就是:

  • 浏览器的url地址发生变化,但是其实并没有发送请求,也没有刷新整个页面
  • 根据我们配置的路由信息,每次点击切换路由,会切换到不同的组件显示,类似于选项卡功能的实现,但是同时url地址栏会变化
  • 分为HashRouterBrowserRouter两种模式

自己实现一个粗略的路由跳转:

自己实现传统的Hash模式跳转:

hash 就是指 url 后的 # 号以及后面的字符。例如www.baidu.com/#segmentfault,那么#segmentfault就是hash
  • 需要用到的几个知识点:

    • window.location.hash = '**'; // 设置当前的hash值
    • const hash = window.location.hash 获取当前的hash值
    • hash改变会触发windowhashchange事件
window.onhashchange=function(e){
    let newURL = e.newURL; // 改变后的新 url地址
    let oldURL = e.oldURL; // 改变前的旧 url地址
}
这里特别注意,hash改变并不会发送请求

开始实现Hash模式跳转:

使用类似发布订阅模式的方式,使用ES6的class实现:

  • 初始订阅,每个不同的hash值,对应不同的函数调用处理。
class Router {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
  }
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }
  updateView() {
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl] && this.routes[this.currentUrl]();
  }
  init() {
    window.addEventListener('load', this.updateView.bind(this), false);
    window.addEventListener('hashchange', this.updateView.bind(this), false);
  }
}
  • routes 用来存放不同路由对应的回调函数
  • init 用来初始化路由,在 load 事件发生后刷新页面,并且绑定 hashchange 事件,当 hash 值改变时触发对应回调函数

开始使用:

<div id="app">
  <ul>
    <li>
      <a href="#/">home</a>
    </li>
    <li>
      <a href="#/about">about</a>
    </li>
    <li>
      <a href="#/topics">topics</a>
    </li>
  </ul>
  <div id="content"></div>
</div>
<script data-original="js/router.js"></script>
<script>
  const router = new Router();
  router.init();
  router.route('/', function () {
    document.getElementById('content').innerHTML = 'Home';
  });
  router.route('/about', function () {
    document.getElementById('content').innerHTML = 'About';
  });
  router.route('/topics', function () {
    document.getElementById('content').innerHTML = 'Topics';
  });
</script>
这样一个简单的hash模式路由就做好了,剩下的就是路由嵌套,以及错误边界的处理

History模式实现:

  • History来自Html5的规范
  • History模式,url地址栏的改变并不会触发任何事件
  • History模式,可以使用history.pushState,history.replaceState来控制url地址,history.pushState() 和 history.replaceState() 的区别在于:

    • history.pushState() 在保留现有历史记录的同时,将 url 加入到历史记录中。
    • history.replaceState() 会将历史记录中的当前页面历史替换为 url。
  • History模式下,刷新页面会404,需要后端配合匹配一个任意路由,重定向到首页,特别是加上Nginx反向代理服务器的时候
我们需要换个思路,我们可以罗列出所有可能触发 history 改变的情况,并且将这些方式一一进行拦截,变相地监听 history 的改变。

对于一个应用而言,url 的改变(不包括 hash 值得改变)只能由下面三种情况引起:

  • 点击浏览器的前进或后退按钮
  • 点击 a 标签
  • 在 JS 代码中触发 history.push(replace)State 函数
只要对上述三种情况进行拦截,就可以变相监听到 history 的改变而做出调整。针对情况 1,HTML5 规范中有相应的 onpopstate 事件,通过它可以监听到前进或者后退按钮的点击,值得注意的是,调用 history.push(replace)State 并不会触发 onpopstate 事件。

开始实现:

class Router {
  constructor() {
    this.routes = {};
    this.currentUrl = '';
  }
  route(path, callback) {
    this.routes[path] = callback || function() {};
  }
  updateView(url) {
    this.currentUrl = url;
    this.routes[this.currentUrl] && this.routes[this.currentUrl]();
  }
  bindLink() {
    const allLink = document.querySelectorAll('a[data-href]');
    for (let i = 0, len = allLink.length; i < len; i++) {
      const current = allLink[i];
      current.addEventListener(
        'click',
        e => {
          e.preventDefault();
          const url = current.getAttribute('data-href');
          history.pushState({}, null, url);
          this.updateView(url);
        },
        false
      );
    }
  }
  init() {
    this.bindLink();
    window.addEventListener('popstate', e => {
      this.updateView(window.location.pathname);
    });
    window.addEventListener('load', () => this.updateView('/'), false);
  }
}

Router 跟之前 Hash 路由很像,不同的地方在于:

  • init 初始化函数,首先需要获取所有特殊的链接标签,然后监听点击事件,并阻止其默认事件,触发 history.pushState 以及更新相应的视图。
  • 另外绑定 popstate 事件,当用户点击前进或者后退的按钮时候,能够及时更新视图,另外当刚进去页面时也要触发一次视图更新。

实际使用:

<div id="app">
  <ul>
    <li><a data-href="/" href="#">home</a></li>
    <li><a data-href="/about" href="#">about</a></li>
    <li><a data-href="/topics" href="#">topics</a></li>
  </ul>
  <div id="content"></div>
</div>
<script data-original="js/router.js"></script>
<script>
  const router = new Router();
  router.init();
  router.route('/', function() {
    document.getElementById('content').innerHTML = 'Home';
  });
  router.route('/about', function() {
    document.getElementById('content').innerHTML = 'About';
  });
  router.route('/topics', function() {
    document.getElementById('content').innerHTML = 'Topics';
  });
</script>
  • 跟之前的 html 基本一致,区别在于用 data-href 来表示要实现软路由的链接标签。
  • 当然上面还有情况 3,就是你在 JS 直接触发 pushState 函数,那么这时候你必须要调用视图更新函数,否则就是出现视图内容和 url 不一致的情况。
setTimeout(() => {
  history.pushState({}, null, '/about');
  router.updateView('/about');
}, 2000);

React-router-dom源码:

Router组件:


export class Route extends Component {
  componentWillMount() {
    window.addEventListener('hashchange', this.updateView, false);
  }
  componentWillUnmount() {
    window.removeEventListener('hashchange', this.updateView, false);
  }
  updateView = () => {
    this.forceUpdate();
  }
  render() {
    const { path, exact, component } = this.props;
    const match = matchPath(window.location.hash, { exact, path });
    if (!match) {
      return null;
    }
    if (component) {
      return React.createElement(component, { match });
    }
    return null;
  }
}
  • 组件挂载监听hash change原生事件,将要卸载时候移除事件监听防止内存泄漏
  • 每次hash改变,就触发所有对应hash的回掉,所有的Router都去更新视图
  • 每个Router组件中,都去对比当前的hash值和这个组件的path属性,如果不一样,那么就返回null,·否则就渲染这个组件对应的视图

History模式的实现:

clipboard.png

实现History

这里想多留些时间写其他源码,这篇文章写得非常好,大家也可以去看看,本文很多借鉴他的。

withRouter高阶函数的源码:

var withRouter = function withRouter(Component) {
  var C = function C(props) {
    var wrappedComponentRef = props.wrappedComponentRef,
        remainingProps = _objectWithoutProperties(props, ["wrappedComponentRef"]);

    return _react2.default.createElement(_Route2.default, {
      children: function children(routeComponentProps) {
        return _react2.default.createElement(Component, _extends({}, remainingProps, routeComponentProps, {
          ref: wrappedComponentRef
        }));
      }
    });
  };

  C.displayName = "withRouter(" + (Component.displayName || Component.name) + ")";
  C.WrappedComponent = Component;
  C.propTypes = {
    wrappedComponentRef: _propTypes2.default.func
  };

  return (0, _hoistNonReactStatics2.default)(C, Component);
};
  • 传入一个组件,返回一个新的组件,并且给这个组件赋予全局属性,拥有路由组件的三大属性

Switch组件:

Switch.prototype.render = function render() {
    var route = this.context.router.route;
    var children = this.props.children;

    var location = this.props.location || route.location;

    var match = void 0,
        child = void 0;
    _react2.default.Children.forEach(children, function (element) {
      if (match == null && _react2.default.isValidElement(element)) {
        var _element$props = element.props,
            pathProp = _element$props.path,
            exact = _element$props.exact,
            strict = _element$props.strict,
            sensitive = _element$props.sensitive,
            from = _element$props.from;

        var path = pathProp || from;

        child = element;
        match = (0, _matchPath2.default)(location.pathname, { path: path, exact: exact, strict: strict, sensitive: sensitive }, route.match);
      }
    });

    return match ? _react2.default.cloneElement(child, { location: location, computedMatch: match }) : null;
  };
  • 遍历所以传入的子元素
  • 如果有符合的路由对应的元素,那么就返回,而且只匹配这一个路由。不再继续往下匹配
  • 如果第二条没有找到符合的元素,那么抛出错误
如果觉得写得好,记得点个赞哦,另外新建了微信和QQ群,欢迎各位小哥哥小姐姐入驻~
  • 微信群:

clipboard.png

  • QQ群

clipboard.png

查看原文

MaE4Nnh 收藏了文章 · 8月27日

开始使用GraphQL

为什么要用graphql?

让我们先回顾一下我们现在所使用的API设计风格

纯rest:一个endpoint对应一个资源

优点:灵活、解构
缺点:由于一个endpoint对应一个资源所以需要很多次请求

类rest:一个endpoint对应一个视图

优点:一次请求、所得即所需
缺点:不够灵活、高度耦合、很高的维护成本、迭代慢

上面是我们两种常用的接口方式,两种都有各自的优缺点,有没有可以包揽所有优点的方案呢?我们需要一个标准的API层,那这就是GraphQL,请注意GraphQL是一个规范,是由facebook倡导的一个规范,不是一个实现。
GraphQL有下面三个定义:

  1. 一个用来描述数据类型和关系的API定义语言
  2. 一个可以描述具体需要获取哪些数据的查询语言
  3. 一个可以resolve到数据单个属性的可执行模型

GraphQL是长什么样子的呢?
图片描述

可能这样看起来还比较难理解,没事,我们直接coding。

GraphQL实践

由于GraphQL是一种规范,它不是一种实现,如果要自己实现还是比较难的,不用担心,强大的开源社区已经帮我们准备好了,这就是Apollo开源项目。Apollo提供了丰富的后端实现(node支持:express、koa、hapi、restify等框架)和前端(React、RN、Angular、IOS、Android等)实现。官方文档:http://dev.apollodata.com/too...。下面的实践都是基于Apollo以nodejs的Express框架来实现的。
Demo代码:https://github.com/jasondu/ap...

一、如何搭建GraphQL服务端

步骤

  1. 搭建服务器

import express from 'express';
import { 
  graphqlExpress, 
  graphiqlExpress, 
} from 'apollo-server-express';
import bodyParser from 'body-parser';
import schema from './data/schema';    // 定义GraphQL查询格式

const GRAPHQL_PORT = 3002;

const graphQLServer = express();

graphQLServer.use('/graphql', bodyParser.json(), graphqlExpress({ schema }));    // 实现GraphQL接口功能
graphQLServer.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }));    // 实现GraphQL浏览器调试界面

graphQLServer.listen(GRAPHQL_PORT, () => console.log(
  `GraphiQL is now running on http://localhost:${GRAPHQL_PORT}/graphiql`
));

懂Express的童鞋应该都可以看到上面的代码,我做一下解释:

  • apollo-server-express 是由Apollo提供在express环境下实现grapql的库,这里使用了里面两个类
  • graphqlExpress是实现grapql接口功能的类
  • graphiqlExpress是实现grapql浏览器调试界面(An in-browser IDE for exploring GraphQL.)的类,就多了一个“i”,这个调试界面可以在后面看到
  • schema就是上文讲的是定义GraphQL查询格式的
  1. 编写Schema

    让我们看看Schema.js是怎么写的

import {
  makeExecutableSchema,
} from 'graphql-tools';
import resolvers from './resolvers';

// 定义schema
const typeDefs = `
type Author {   # 作者的字段有:id,名字,还有 发表的帖子
  id: Int
  firstName: String
  lastName: String
  posts: [Post]
}
type Post {    # 帖子的字段有下面这些,包括 这个帖子是哪个作者写的
  id: Int
  title: String
  text: String
  views: Int
  author: Author
}
type Query {    # 定义查询内容
  author(firstName: String, lastName: String): Author # 查询作者信息
  getFortuneCookie: String
}
`;

const schema = makeExecutableSchema({ typeDefs, resolvers });

export default schema;

这里用到Apollo提供的makeExecutableSchema方法,这个方法是将Schema结构的数据转换成GraphQLSchema实例。
typeDefs里面定义了三个格式Author,Post,Query,这里Query就是查询的时候返回的结构,Author,Post是解释了在Query中的结构类型。
接下来,我们就可以编写具体的实现了。

  1. 编写Resolvers

const resolvers = {
  Query: {
    author(root, args){    // args就是上面schema中author的入参
      return { id: 1, firstName: 'Hello', lastName: 'World' };
    },
  },
  Author: {
    // 定义author中的posts
    posts(author){
      return [
        { id: 1, title: 'A post', text: 'Some text', views: 2},
        { id: 2, title: 'Another post', text: 'Some other text', views: 200}
      ];
    },
  },
  Post: {
    // 定义Post里面的author
    author(post){
      return { id: 1, firstName: 'Hello', lastName: 'World' };
    },
  },
};

export default resolvers;

上面这段代码比较简单,就不做解释了。
至此,我们就完成了一个GraphQL服务端的开发,加下来我们npm i & npm start
图片描述
呐,这样就启动啦!打开http://localhost:3002/graphiql就可以看到刚才前面说的graphiql,就是GraphQL浏览器调试界面。

graphiql可以支持联想功能,可以非常快的书写查询语句。
Tb8cIS3jlC.gif

二、如何在客户端查询GraphQL数据(以react为例)

步骤

  1. 使用create-react-app创建一个项目,并且按照Apollo客户端解决方案库:react-apollo

create-react-app client & yarn add react-apollo
  1. 修改App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

// -------- 添加内容 -------- //
import {
  ApolloClient,
  ApolloProvider,
  createNetworkInterface,
  gql,
  graphql,
} from 'react-apollo';

// 设置接口地址
const networkInterface = createNetworkInterface({ uri: 'http://localhost:3002/graphql' });

const client = new ApolloClient({
  networkInterface,
});

const Test = ({ data: { loading, error, author } }) => {
  if (loading) {
    return <p>Loading ...</p>;
  }
  if (error) {
    return <p>{error.message}</p>;
  }

  return (
    <h3>{author.firstName} {author.lastName}</h3>
  );
};
// 查询语句
const query = gql`
  query AuthorQuery {
    author (firstName: "firstName", lastName: "lastName") {
      firstName,
      lastName
    }
  }
`;

const Gtest = graphql(query)(Test);

// -------- 添加内容 -------- //

class App extends Component {
  render() {
    return (
      <ApolloProvider client={client}>
        <div className="App">
          <div className="App-header">
            <img data-original={logo} className="App-logo" alt="logo" />
            <Gtest />
          </div>
          <p className="App-intro">
            To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        </div>
      </ApolloProvider>
    );
  }
}


export default App;

这里的写法跟redux类似,使用<ApolloProvider>包裹项目,通过graphql方法将数据注入到组件中。

然后执行yarn start 这样项目就启动了,如下图

注:这里存在跨域问题,所以服务器端需要使用cors解决跨域问题,具体看代码。

下面是我收集的相关学习资料:

https://dev-blog.apollodata.c...

《Tutorial: How to build a GraphQL server》讲解了如何搭建node GraphQL服务器,如何定义schema,还有如何链接以前的SQL数据库,rest等,入门必读

https://dev-blog.apollodata.c...

《Full-stack React + GraphQL Tutorial》讲解如何和客户端结合起来,还有如果实现ws实时通信等

https://launchpad.graphql.com...

这个是apollo提供的线上编辑器,可以在线上编写schema和resolve,然后可以下载下来部署

https://www.howtographql.com/

这个网站详细讲解了如何在各种服务器客户端使用graphql

http://taobaofed.org/blog/201...

《Node.js 服务端实践之 GraphQL 初探》阿里在15年写的文章

http://graphql.org/官网

查看原文

MaE4Nnh 收藏了文章 · 8月27日

切图仔的 Nginx 小书?

切图仔的 Nginx 小书

本文陆续介绍 Nginx 的功能、配置、及一些实用场景(待完善...)。

一、介绍 Nginx

1. Nginx 是什么?

Nginx,很多工程师喜欢读成'恩基克思'。Nginx 是一款高性能的HTTP和反向代理服务器软件,第一个开源版本诞生于2004年,虽然诞生较晚但经过十多年的发展,已经成为非常流行的 web 服务器软件。其发展速度和流行程度已经远远超过其它同类软件,成为大型网站和高并发网站的首选。

2. Nginx 为什么流行

Nginx 之所以能够脱颖而出,一方面是因为市场往往会选择简单实用的技术,另一方面是因为web服务器的高并发已经成为趋势,而高并发又要求架构具有健壮性和可伸缩性。Nginx 的特性迎合了市场的这个发展趋势,它是为性能而生,从发布以来一直侧重于高性能,高并发,低 CPU 内存消耗;在功能方面:负载均衡,反向代理,访问控制,热部署,高扩展性等特性又十分适合现代的网络架构。更可贵的是配置简单文档丰富,大大降低了学习的门槛。这样稳定,性能强,功能丰富又简单的产品当然会受欢迎了。

3. 为什么选择 Nginx?

Apache 自1990年发布以来,一直是 web 服务器市场的霸主。Nginx 虽然发布较晚,但是却因为在高并发下卓越的表现而迅速崭露头角。最初,Nginx 只是作为 Apache 在高并发场景下的补充,两者结合辅助使用。而现在,Nginx 随着迭代功能,在极多数场合已经可以抛弃老大哥来独当一面了。

80端口之争

Nginx 和 Apache 相同点:

  • 同是 Http 服务器软件,都采用模块化结构设计
  • 支持通用语言接口,如 PHP,Python 等。
  • 支持正向代理和反向代理。
  • 支持虚拟主机以及 SSL 加密传输
  • 支持缓存以及压缩传输
  • 支持 URL 重写
  • 模块多,扩展性强
  • 多平台支持

Nginx 的优势:

  • 轻量级,安装简单、配置文件简洁,运行时 CPU 内存使用率低。
  • 性能强 支持多核,处理静态文件效率高。
  • 支持热部署,启动速度快,可以在不间断服务情况下对软件和配置进行升级
  • 负载均衡 支持容错和健康检查
  • 代理功能强大 支持无缓存的方向代理,同时支持 IMAP/POPS/SMTP 的代理

Nginx 的劣势:

  • 相对于老大哥 Apache 模块要少一些
  • 对动态请求的支持不如 Apache
  • Windows 版本功能有限,受限于 Windows 的特性,支持最好的还是 *unix 系统

4. Nginx 的工作原理

Nginx 由内核和一系列模块组成,内核提供 Web Server 的基本功能,如启动网络协议,创建运行环境,接收和分配客户端请求,处理模块之间的交互。Nginx 的各种功能和操作都由模块来实现。
Nignx 的模块从结构上分为:

  • 核心模块:HTTP 模块、EVENT 模块和 MAIL 模块
  • 基础模块:HTTP Access 模块、HTTP FastCGI 模块、HTTP Proxy 模块和 HTTP Rewrite 模块
  • 第三方模块:HTTP Upstream Request Hash 模块、Notice 模块和HTTP Access Key 模块以及其它自开发模块。

这样的设计使 Nginx 方便开发和扩展,也正因此才使得 Nginx 功能如此强大。Nginx 的模块默认编译进 Nginx 中,如果需要增加或删除模块,需要重新编译 Nginx ,这一点不如 Apache 的动态模块加载方便。(如果需动态加载模块,可以使用兼容 Nginx 的web服务器 Tengine )

5. Nginx 的请求处理

Nginx 使用一个多进程模型来应对外来需求,其中一个 master 进程,多个 worker 进程。master 进程负责管理 Nginx 本身和其他 worker 进程。

所有实际上的业务处理逻辑都在 worker 进程,其内有一个无限循环执行的函数,不断处理来自客户端的请求,并进行处理,直到整个 Nginx 服务停止。

worker 进程中,ngx_worker_process_cycle() 就是这个无线循环的函数。内部对一个请求的简单处理流程如下:

  1. 操作系统提供的机制产生相关的事件
  2. 接受和处理这些事件,如果是接收数据,则产生更高层的request对象
  3. 处理 request 的 header 和 body
  4. 产生响应,并发送回客户端
  5. 完成 request 的处理
  6. 重新初始化定时器及其他事件

如图展示了 Nginx 模块常规的HTTP请求和响应的过程:
htpp流程

二、Nginx 配置

#运行用户
user nobody;
#启动进程,通常设置成和cpu的数量相等
worker_processes  1;
 
#全局错误日志及PID文件
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
 
#pid        logs/nginx.pid;
 
#工作模式及连接数上限
events {
    #单个后台worker process进程的最大并发链接数    
    worker_connections  1024;
}
 
 
http {
    #设定mime类型,类型由mime.type文件定义
    include    mime.types;
    default_type  application/octet-stream;
    #设定日志格式
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
 
    access_log  logs/access.log  main;
 
    #sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,
    #对于普通应用,必须设为 on,
    #如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,
    #以平衡磁盘与网络I/O处理速度,降低系统的uptime.
    sendfile     on;
    #tcp_nopush     on;
 
    #连接超时时间
    #keepalive_timeout  0;
    keepalive_timeout  65;
    
    #开启gzip压缩
    #gzip  off;
 
    #设定虚拟主机配置
    server {
        #侦听80端口
        listen    80;
        #定义使用 www.nginx.cn访问
        server_name  www.nginx.cn;
 
        #定义服务器的默认网站根目录位置
        root html;
 
        #设定本虚拟主机的访问日志
        access_log  logs/nginx.access.log  main;
 
        #默认请求
        location / {
            
            #定义首页索引文件的名称
            index index.php index.html index.htm;   
 
        }
 
        # 定义错误提示页面
        error_page   500 502 503 504 /50x.html;
        location = /50x.html {
        }
 
        #静态文件,nginx自己处理
        location ~ ^/(images|javascript|js|css|flash|media|static)/ {
            
            #过期30天,静态文件不怎么更新,过期可以设大一点,
            #如果频繁更新,则可以设置得小一点。
            expires 30d;
        }
 
        #PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置.
        location ~ .php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
 
        #禁止访问 .htxxx 文件
            location ~ /.ht {
            deny all;
        }
 
    }
}

三、Nginx 命令行

不像其他系统软件,Nginx 仅由几个命令行参数

-c </path/to/config> // 为Nginx指定一个配置文件,来代替缺省的
-t // 不运行,仅仅测试配置。
-v // 显示Nginx的版本
-V // 显示Nginx的版本,编译器版本和配置参数。

Nginx 启动、停止、重启命令

// 启动
sudo nginx
// 或者
sudo /usr/local/Cellar/nginx/1.12.0_1/bin/nginx // 如果不知道实际路径,可以nginx —V查看

// 停止命令
ps -ef | grep nginx // 查看占用进程
kill -QUIT nginx主进程号 // 从容停止
kill -TERM nginx主进程号 // 快速停止
kill -9 nginx主进程号 // 强制停止
// 或者
kill -QUIT `cat /usr/local/var/run/nginx.pid` // 如果不知道实际路径,可以nginx —V查看
// 重启命令
kill -QUIT `cat /usr/local/var/run/nginx.pid`
sudo /usr/local/Cellar/nginx/1.12.0_1/bin/nginx

四、Nginx 实用

场景1:适配PC与移动web

一般门户网站在访问时,会有 PC 和 H5、Pad 几个适配版本,我们常会有这样的需求,在网站被访问时候,服务端来识别用户是 PC 设备还是移动设备,跳转返回相应适配版本的页面。

第一步通常是判断 HTTP 请求头的 User-Agent ,基本原理是通过正则匹配判断,有一套开源的解决方案可以直接使用:http://detectmobilebrowsers.com/,下载 Nginx 配置即可。
第二步就是通过之前对设备的判断,来反向代理到不同的版本。

location / {
    proxy_pass http://leju.com
    if ($mobile_rewrite = perform) {  
        proxy_pass http://m.leju.com/  # 手机版  
    }     
}

第三步,因为可能错误判断设备,或者用户就想指定访问某种设备版本,在页面底部,通常会有链接跳转其他版本。
foot

<div class="ll_btn">
            <a href="http://www.leju.com#ln=index_fdh">电脑版</a>
            <a href="http://pad.leju.com/?source=chuping#ln=index_fdh">PAD版</a>
            <a href="http://m.leju.com/touch/app/app_download.html?source=chupinghp#ln=index_fdh">客户端</a>
</div>

同时在 Nginx 中加入判断,如果包含指定 source 参数,则指定进入某个版本。

场景2:前端环境切换

前端开发中,我们经常需要在多个环境(开发、内测、外测、预发、正式环境)进行切换。
我们通常通过切换 host 指向搭配机器绑定不同域名的方式去实现:比如测试环境是dev.j.esf.sina.com,正式环境是j.esf.sina.com,搭配不同的 host 指向,可以形成多种组合。

而通过反向代理的Nginx,更容易处理这种代理转发的问题:

  • 我们通过点击页面的环境按钮,让绑定的 javascript 代码往域名下种入带有 IP 信息的 cookie,同时刷新页面。
  • Nginx 接收新的请求,读取请求的 cookie ,如果包含指定的键名和值,则代理到这个 IP 地址。(http header 也可以)
  • 如果没有,则代理到默认的线上环境。
set $env_id “123.59.190.206”;
if( $http_cookie~* "host_id(\S+)(;.*|$)"){
    set $env_id $1;
}

location / {
    proxy_set_header Host $host;
    proxy_pass   http://$env_id:80;
}

...场景待续

查看原文

MaE4Nnh 收藏了文章 · 8月27日

《前端竹节》(1)【跨域通信】

一、同源策略

用户浏览网站时难免需要将一些经常用到的信息,缓存在本地以提升交互体验,避免一些多余的操作。那么这些信息中难免有些就会涉及用户的隐私,怎么保证用户的信息不在多个站点之间共享,以达到可用的最小范围,这边引出了浏览器的同源策略。那么...

什么是同源策略?
同时满足以下三个条件的网页称为同源:

  1. 协议相同
  2. 域名相同
  3. 端口相同

非同源的网页将受到以下限制:

  1. Cookie、LocalStorage 和 IndexDB 无法读取。
  2. DOM 无法获得
  3. AJAX 请求不能发送

同源策略是必要的,但这些限制有时也会对一些合理的使用带来不便,这便引出了跨域通信的需求。

二、跨域通信

常见的跨域通信方式有如下集中:

  1. JSONP
  2. Hash
  3. Postmessage
  4. WebSocket
  5. CORS

2.1 JSONP

JSONP的原理是,首先客户端动态添加一个<script>元素,向服务器请求JSON数据,在请求的URL中添加search字段,指定回调函数名;服务器在收到请求后,会将数据放在指定名字的回调中传回来。实现源码:

 /**
  * jsonp
  * @param  {[type]} url      [description]
  * @param  {[type]} onsucess [description]
  * @param  {[type]} onerror  [description]
  * @param  {[type]} charset  [description]
  * @return {[type]}          [description]
  */
const jsonp = function(url, onsucess, onerror, chartset){
    let callbackName = getName('tmp_jsonp')
    window[callbackName] = function(){
        if(onsucess && onsucess instanceof Function){
            onsucess(arguments[0])
        }
    }
    let script = createScript(url + '&callback=' + callbackName, charset)
    script.onload = script.onreadystatechange = function () {
        // script标签加载完成(即jsonp请求完成)后移除创建的临时标签
        if(!script.readyState || /loaded|complete/.test(script.readyState)){
            script.onload = script.onreadystatechange = null
            // 移除该script的 DOM 对象
            if (script.parentNode) {
                script.parentNode.removeChild(script)
            }
            window[callbackName] = null
        }
    }
    script.onerror = function () {
        if (onerror && util.isFunction(onerror)) {
            onerror();
        }
    }
    document.getElementsByTagName('head')[0].appendChild(script)
}
 /**
  * 获取一个随机的5位字符串
  * @param  {string} prefix [字符前缀]
  * @return {string}        [字符集]
  */
const getName = function (prefix) {
    return prefix + Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5)
}
 /**
  * 在页面中创建script
  * @param  {string} url     [url]
  * @param  {string} charset [字符集]
  * @return {string}         [创建完成的script标签]
  */
const createScript = function (url, charset) {
    let script = document.createElement('script')
    script.setAttribute('type', 'text/javascript')
    charset && script.setAttribute('charset', charset)
    script.setAttribute('src', url)
    script.async = true
    return script
}

2.2 hash

hash方式主要用在内嵌iframe的父子窗口间的通信,原理是URL的#后部分的改变不会使页面刷新。
(1)父窗口向子窗口发送数据

// 父窗口向子窗口发送数据
let src = orignURL + '#' + data
document.getElementById('myIframe').src = src
// 子窗口接收父窗口发送的数据
window.onhashchange = function(){
    let message = window.location.hash
    ...
}

(2)子窗口向父窗口发送数据,也同理

parent.location.herf = target + '#' + hash

2.3 postmessage

首先创建目标窗口的window对象,然后通过该对象的postmessage方法传递数据,最后目标窗口通过监听message事件来接受数据。

// 发送数据
let bWindow = window.open('http://b.com', 'title')
bWindow.postmessage('hello message', 'http://b.com')
// 接受数据
window.addEventListener('message', function(e){
    // e.source 发送消息窗口的引用
    // e.origin 发送消息的网址
    // e.data 发送消息的内容
})

使用postmessage时需要注意的问题:

  1. 接受方打开了对message事件的响应,接收时需要对发送方进行验证
  2. 发送消息时需要指定确定的接收方,不可指定为*进行群发

2.4 websocket

// ws和wss的区别就是加不加密
let ws = new WebSocket('ws://xx.com')
ws.onopen = function(e){}

ws.onmessage = function(e){}

ws.onclose = function(e){}

2.5 CORS

可以简单理解为跨域通信的ajax,需要浏览器和服务器同时支持,主要关键还是须在浏览器端进行配置。可参考CORS配置说明

三、前后端如何通信

除了跨域通信,前端最常接触的便是前后端通信,常见的前后端通信方式有以下三种:

  1. ajax/fetch
  2. websocket
  3. cors

四、如何创建ajax

4.1 Vue框架实现ajax

Vue是个没什么入侵性的框架,所以在ajax方面可以有很多选择:

  1. 使用原生js的XHR实现
  2. 引入jquery
  3. Vue2.0官方推荐axios.js
  4. fetch API

4.2 原生js实现ajax

/**
 * 原生JS实现ajax
 * @param  {object} options [传入参数]
 * @return {[type]}         [description]
 */
const ajax = function(options){
    var opt = {
        url: '',
        type: 'get',
        data: {},
        success: function () {},
        error: function () {},
    }
    Object.assign(opt, options)
    if(opt.url){
        let xhr = XMLHttpRequest
            ? new XMLHttpRequest()
            : new ActiveXObject('Microsoft.XMLHTTP')
        let data = opt.data,
            url = opt.url,
            type = opt.type.toUpperCase(),
            dataArr = []
        for(let k in data){
            dataArr.push(k + '=' + data[k])
        }
        if(type == 'GET'){
            url = url + '?' + dataArr.join('&')
            xhr.open(type, url.replace(/\?$/.g, ''), true)
            xhr.send()
        }
        if(type == 'POST'){
            xhr.open(type, url, true)
            xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
            xhr.send(dataArr.join('&'))
        }
        xhr.onload = () => {
            if(xhr.status === 200 || xhr.status === 304 || xhr.status === 206){
                let res
                if(opt.success && opt.success instanceof Function){
                    res = xhr.responseText
                    if(typeof res === 'string'){
                        res = JSON.parse(res)
                        opt.success.call(xhr, res)
                    }
                }
            }else{
                if(opt.error && opt.error instanceof Function){
                    opt.error.call(xhr, res)
                }
            }
        }
    }
}
查看原文

MaE4Nnh 收藏了文章 · 8月27日

杂谈数据类型获取

前言

在js中数据我们经常需要判断或者获取数据类型,大部分时候我们都是通过type加instanceof来组合判断数据类型来实现,大部分代码中对于数据类型的获取处理都比较丑陋,前段时间看了一下Q的源代码中对数据类型的判断与获取处理,看起来相当简洁也比较好用,这篇文章来进行一下发散。

typeof

在js中我们判断数据类型经常会用到typeof,比如判断一个数据是否是一个数字类型

var n = 99;
typeof(n) === 'number';    // true

这么做有一个缺点,typeof只能判断js中的基础数据类型undefined、String、Number、Boolean、Object。如果需要判断一个数据是不是Array类型,这个时候instanceof就派上用场了。

instanceof

官方对于instanceof说明: 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性(翻译成人话:判断对象指向构造函数名称是否与构造函数名一致),如图:

clipboard.png

判断一个数据是否是时间类型,一般我们都这样写:

var list = [];
list instanceof Date;    // false

instanceof有一个缺点,只能针对对象类型的数据进行处理,因为只有对象中才包含原型链prototype,当然平常我们用到的function数据类型也是对象的一种,还是一图解千言,看一下js中的数据类型大概明了。

clipboard.png

判断null的数据类型

在js中null也是Object中的一个子类型(关于null,可以看这篇文章),但是我们不能通过instanceof去获取,因为null中并没有原型链prototype,于是我们有了这样的代码:

var str = 'null';
str === null;    // false

基础版本获取数据类型

当我们并不知道数据类型,但是需要获这个数据的类型时,大部分童鞋的代码里面都是这样写的:

classString(obj) {
    if (obj && (obj.__proto__ || obj.prototype)) {
        if (obj instanceof Array) {
            return 'array';
        }
        if (obj instanceof Function) {
           return 'function';
        } 
        // 所有obj的衍生数据都判断一遍 ...
    } else if (obj === null) {
        return 'null';    // 返回字符串
    } else {
        return typeof(obj);
    }
}

这样的代码很繁杂,这么多的”if else“(俗称面条代码),既不美观,也不实用。

升级版本获取数据类型

function classString(obj) {
    return ({}).toString.call(obj);
}
classString(null);    // [object Null]
classString('string');    // [object String]
classString(function(){});    // [object Function]
classString(new Date());    // [object Function]

有两个关键方法:一个是call,另一个是Object.prototype.toString方法进行处理。

先看call方法简短描述:

fun.call(this, arg1, arg2, ...)
call: 调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)

再看一下关于call参数说明:

在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为null和undefined的this值会自动指向全局对象,同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。

只有当this不存在上下文的时候,this才会指向全局对象(浏览器中就是window对象)。

”包装对象“,并不是一种数据类型,原始数据类型中:字符串、数字、布尔值可以转换成相应的Number、String、Boolean对应的原生对象(注意是对象,不是值),具体在call中的表现形式如下(左边等价于右边):

clipboard.png

再举个栗子?:

999 instanceof Number;    // false
new Number(999) instanceof Number;    // true

关于Object toString方法的简短说明:

默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

这里的type并不是js5中基础数据类型相关的type,而是当前对象中__proto__中指向的构造函数名(关于__proto__

自定义数据类型

我们在开发的时候经常需要自定义数据,比如说我们自己创建了一个构建函数F,当我们获取数据类型的时候期待返回的结果为”[object F]“,代码如下

var F = function() {}
var f1 = new F();
classString(f1);    // [object Object]

期待的结果并未返回,原因是因为Object.prototype.toString方法只定义了自带的对象类型返回,EcmaScript关于Object toString 规范

clipboard.png

我们可以直接重定义Object.prototype.toString:

Object.prototype.toString = function(){
    // Do something ...
}

这并不是一种很好的行为,一方面容易造成全局污染,另一方面也不利于进行排错;所以还是老老实实的在方法中判断吧:

function classString(obj, customize) {
    if (customize && obj && obj.__proto__.constructor.name) {
        return '[object ' + obj.__proto__.constructor.name + ']';
    }
    return ({}).toString.call(obj);
}
var F = function() {}
var f1 = new F();
classString(f1, true);    // [object F]
classString(f1);    // [object Object]

为什么可以通过__proto__.constructor.name来获取构造函数名,object与function并不会造成混乱,object与function中的 proto 指向并不是相同的,这里不细讲,还是参考关于__proto__

优化返回格式

对于”[object type]“数据返回我们只需要获取type即可,可以通过正则表达获取type指,这里不做代码说明。

其他

1、关于call、toString方法平常都是信手拈来使用,没有深入探究过,探究起来和以前自己脑海中的理解还是有些不同的。
2、 如果项目中需要频繁的进行数据类型的判断与获取可以考虑进行封装,简单的处理typeof与instanceof已足够。
3、上述代码中只是简单的示例,部分地方并不是十分严谨。


参考资料

EcmaScript
MDN instanceof
MDN call
MDN types Array
undefined与null有什么区别
关于__proto__与prototype
EcmaScript Object toString

查看原文

MaE4Nnh 收藏了文章 · 8月27日

JS设计模式--装饰者模式

欢迎关注我的公众号睿Talk,获取我最新的文章:
clipboard.png

一、前言

所谓装饰者模式,就是动态的给类或对象增加职责的设计模式。它能在不改变类或对象自身的基础上,在程序的运行期间动态的添加职责。这种设计模式非常符合敏捷开发的设计思想:先提炼出产品的MVP(Minimum Viable Product,最小可用产品),再通过快速迭代的方式添加功能。

二、传统面向对象语言的实现方式

var Car = function() {}
Car.prototype.drive = function() {
    console.log('乞丐版');
}

var AutopilotDecorator = function(car) {
    this.car = car;
}
AutopilotDecorator.prototype.drive = function() {
    this.car.drive();
    console.log('启动自动驾驶模式');
}

var car = new Car();
car = new AutopilotDecorator(car);
car.drive();    //乞丐版;启动自动驾驶模式;

这种方式的实现要点是装饰器类要维护目标对象的一个引用,同时要实现目标类的所有接口(这个例子里的drive方法,如果还有其它方法,比如brake,AutopilotDecorator也要实现)。调用方法时,先执行目标对象原有的方法,再执行自行添加的特性。

当接口比较多,装饰器也比较多时,可以独立抽取一个装饰器父类,实现目标类的所有接口,再创建真正的装饰器来继承这个父类。

var Car = function() {}
Car.prototype.drive = function() {
    console.log('乞丐版');
}
/* 多了一个刹车方法 */
Car.prototype.brake = function() {
    console.log('刹车');
}

/* 实现所有接口的装饰器父类 */
var CarDecorator = function(car) {
    this.car = car;
}
CarDecorator.prototype = {
    drive: function() {
        this.car.drive();
    },
    brake: function() {
        this.car.brake();
    }
}

/* 真正的装饰器 */
var AutopilotDecorator = function(car) {
    CarDecorator.call(this, car);
}
AutopilotDecorator.prototype = new CarDecorator();
AutopilotDecorator.prototype.drive = function() {
    this.car.drive();
    console.log('启动自动驾驶模式');
}

/* 真正的装饰器 */
var HybridDecorator = function(car) {
    CarDecorator.call(this, car);
}
HybridDecorator.prototype = new CarDecorator();
HybridDecorator.prototype.brake = function() {
    this.car.brake();
    console.log('启动充电模式');
}

var car = new Car();
car = new AutopilotDecorator(car);
car = new HybridDecorator(car);
car.drive();    //乞丐版;启动自动驾驶模式;
car.brake();    //刹车;启动充电模式;

三、JS基于对象的实现方式

var car = {
    drive: function() {
        console.log('乞丐版');
    }
}

var driveBasic = car.drive;

var autopilotDecorator = function() {
    console.log('启动自动驾驶模式');
}

var carToDecorate = Object.create(car);

carToDecorate.drive = function() {
    driveBasic();
    autopilotDecorator();
}

carToDecorate.drive();    //乞丐版;启动自动驾驶模式;

这种实现方式完全是基于JS自身的语言特点做考量。定义类的目的是实现代码的封装和复用,而JS这门语言是没有类的概念的。它只有2种数据类型:基本类型和对象类型。实现逻辑的封装和代码的重用只需要通过对象来组织代码,然后利用原生提供的克隆机制(Object.create)来达到目的。

从代码的角度看,如果想扩展drive方法,只需要用一个变量来保存原函数的引用,然后再重写drive方法就可以了。在重写的方法里面,只要记得调用方法原有的行为就行。

另外,我们可以通过以下的工具函数,达到装饰函数的目的:

Function.prototype.after = function(afterfn) {
    var _self = this;
    return function() {
        var ret = _self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}

var car = {
    drive: function() {
        console.log('乞丐版');
    }
}

var autopilotDecorator = function() {
    console.log('启动自动驾驶模式');
}

var carToDecorate = Object.create(car);

carToDecorate.drive = car.drive.after(autopilotDecorator);

carToDecorate.drive();    //乞丐版;启动自动驾驶模式;

通过在Function的原型链上定义after函数,给所有的函数都赋予了被扩展的功能,当然也可以根据需要定义一个before的函数,在函数执行前去做一些操作。这种实现方式借鉴了AOP(Aspect Oriented Programming,面向切面编程)的思想。

四、ES7的实现方式

ES7提供了一种类似的Java注解的语法糖decorator,来实现装饰者模式。使用起来非常简洁:

function autopilotDecorator(target, key, descriptor) {
    const method = descriptor.value;
    
    descriptor.value = () => {
        method.apply(target);
        console.log('启动自动驾驶模式');
    }
    
    return descriptor;
}

class Car {
    @autopilotDecorator
    drive() {
        console.log('乞丐版');
    }
}

let car = new Car();
car.drive();    //乞丐版;启动自动驾驶模式;

decorator的实现依赖于ES5的Object.defineProperty方法。defineProperty所做的事情是为一个对象增加新的属性,或者更改某个已存在的属性。调用方式是Object.defineProperty(obj, prop, descriptor)

var o = {}; // 创建一个新对象

// 在对象中添加一个属性
Object.defineProperty(o, "name", {
  value : "Dickens",
  writable : true,
  enumerable : true,
  configurable : true
});

// 在对象中添加一个方法
Object.defineProperty(o, "sayHello", {
  value : function() {
        console.log('Hello, my name is: ', this.name)
  },
  writable : true,
  enumerable : true,
  configurable : true
});

o.sayHello()    //Hello, my name is:  Dickens

decorator的参数跟defineProperty是完全一样的,含义也类似,通过修改descripter,就能达到扩展功能的目的。

五、总结

本文介绍了装饰者模式的基本概念,并通过不同的实现方式来介绍使用方法。对于不同的使用方法,也作了比较透彻的解释,让大家不但知其然,还知其所以然。

装饰者模式是一种十分常用且功能强大的模式,利用ES7的语法糖,我们能用非常简洁的方式来表达装饰的意图,推荐大家在实际项目中用起来。

查看原文

MaE4Nnh 收藏了文章 · 8月27日

Nginx 反向代理常用配置

什么是代理

在计算机中,客户端A与服务端B进行通信,中间加入中介C进行数据传递,则形成了代理。来个浅显易懂的比喻,三年级二班小明和小花谈恋爱,由于小明和小花座位离的太远,这时候需要小王在中间传递纸条,在这个爱情故事中小王在中间充当了代理作用。

什么是正向代理呢

正向代理中,客户端通过代理服务器从服务端收发数据,即为正向代理。
举个栗子:有一个局域网,但是只有主机局域访问外部网络的权限,现在所有的机器都需要访问外部网络,通过将主机设置为代理服务器,让所有的机器通过主机可以访问外部网络,即称为正向代理。
clipboard.png

反向代理又是什么

反向代理中,代理服务器替服务端收发数据,所有的请求与相应都经过反向代理,可以用来实现数据缓存,负载均衡等。
举个栗子: 张三开了一个保险公司(服务端),一开始公司业务小,一个人接电话就够了,后来随着公司扩张,每天无数电话打进来,张三招了一个客服李四(代理),张三把和客户对接的事情都交给了李四,形成了反向代理。
clipboard.png

反向代理常用配置

server {
    listen       80;
    server_name  localhost;  #实际情况可以写域名    
    location / {
         index  index.html index.htm;
         proxy_set_header Host $host:$server_port;
         proxy_pass http://127.0.0.1:9992;
         rewrite ^/(.*)  /index.php?s=$1;    
    }
}

配置项很多,挑几个几个常用的配置依次介绍:location、proxy_set_header、rewrite、proxy_pass

代理目录匹配 location

# 匹配所有根目录
location /
# 字符串匹配, 表示匹配所有“/static”开头的目录
location /static
# ~ 匹配符合表达式目录比如代理目录中存在“static/(js|images)”的目录
location ~ /static/(js|images)/
# ~* 加上 * 表示不区分大小写
location ~ /static/(js|images)/
# = 表示精确匹配, 只有"/index"路径才会被代理,"/index/test"将不会被代理
location = /index

当然还有”!“、”^“匹配,用的比较少,这里不做说明

设置代理请求头 proxy_set_header

# 设置代理请求服务器请求头host
proxy_set_header Host $host
# 设置代理请求的ip地址
proxy_set_header X-Forwarded-Ip $remote_addr  
# 设置代理请求自定义数据
proxy_set_header test test  

这里还有很多数据,不一一说明

请求代理服务器 proxy_pass

# 从 “127.0.0.1”这台服务器收发数据,当然也可以直接写域名
proxy_pass http://127.0.0.1:8080
# 从服务端机器data目录收发数据
proxy_pass http://127.0.0.1:81/data;
# 动态配置数据,$scheme表示用户请求是http还是https,$host表示客户端请求头host,$args表示客户端请求参数
proxy_pass $scheme://$host/$request_uri/?$args

url重定向规则 rewrite

包含3个参数:

rewrite 匹配规则 重定向规则 重定向类型;

用法示例:

# /a.html 的时候,url重定向路径 /b.html 中
rewrite /a.html /b.html last;
# break 与 last的区别是,当存在多条rewrite规则时last会继续往下匹配,break不会继续往下匹配,而是将匹配到的重定向路径当做最终路径
rewrite /a.html /b.html break;
# 当然重定向规则也是可以写正则表达式的 例如:/static/images/a.png => /local/images/a.png
rewrite ^/static/images/(.*)$ /local/images/$1 break;
# redirect 表示302重定向
rewrite /a.html /b.html redirect;
# permanent 表示301重定向
rewrite /a.html /b.html permanent;

301重定向表示永久性重定向,对于SEO相较302来说比较友好,这里不做过多说明。

部分参考

nginx doc
nginx rewrite 配置

查看原文

MaE4Nnh 收藏了文章 · 8月27日

Styled-Components

Styled-Components

它是通过JavaScript改变CSS编写方式的解决方案之一,从根本上解决常规CSS编写的一些弊端。
通过JavaScript来为CSS赋能,我们能达到常规CSS所不好处理的逻辑复杂、函数方法、复用、避免干扰。
尽管像SASS、LESS这种预处理语言添加了很多用用的特性,但是他们依旧没有对改变CSS的混乱有太大的帮助。因此组织工作交给了像 BEM这样的方法,虽然比较有用,但是它完全是自选方案,不能被强制应用在语言或者工具层面。
他搭配React可能将模块化走向一个更高的高度,样式书写将直接依附在JSX上面,HTML、CSS、JS三者再次内聚。

基本

安装

npm install --save styled-components

除了npm安装使用模块化加载包之外,也支持UMD格式直接加载脚本文件。

<script data-original="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>

入门

styled-components使用标签模板来对组件进行样式化。

它移除了组件和样式之间的映射。这意味着,当你定义你的样式时,你实际上创造了一个正常的React组件,你的样式也附在它上面。

这个例子创建了两个简单的组件,一个容器和一个标题,并附加了一些样式。

// Create a Title component that'll render an <h1> tag with some styles
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`;

// Create a Wrapper component that'll render a <section> tag with some styles
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`;

// Use Title and Wrapper like any other React component – except they're styled!
render(
  <Wrapper>
    <Title>
      Hello World, this is my first styled component!
    </Title>
  </Wrapper>
);
注意
CSS规则会自动添加浏览器厂商前缀,我们不必考虑它。

透传props

styled-components会透传所有的props属性。

// Create an Input component that'll render an <input> tag with some styles
const Input = styled.input`
  padding: 0.5em;
  margin: 0.5em;
  color: palevioletred;
  background: papayawhip;
  border: none;
  border-radius: 3px;
`;

// Render a styled text input with a placeholder of "@mxstbr", and one with a value of "@geelen"
render(
  <div>
    <Input placeholder="@mxstbr" type="text" />
    <Input value="@geelen" type="text" />
  </div>
);

基于props做样式判断

模板标签的函数插值能拿到样式组件的props,可以据此调整我们的样式规则。

const Button = styled.button`
  /* Adapt the colours based on primary prop */
  background: ${props => props.primary ? 'palevioletred' : 'white'};
  color: ${props => props.primary ? 'white' : 'palevioletred'};

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

render(
  <div>
    <Button>Normal</Button>
    <Button primary>Primary</Button>
  </div>
);

样式化任意组件

// This could be react-router's Link for example
const Link = ({ className, children }) => (
  <a className={className}>
    {children}
  </a>
)

const StyledLink = styled(Link)`
  color: palevioletred;
  font-weight: bold;
`;

render(
  <div>
    <Link>Unstyled, boring Link</Link>
    <br />
    <StyledLink>Styled, exciting Link</StyledLink>
  </div>
);

扩展样式

我们有时候需要在我们的样式组件上做一点扩展,添加一些额外的样式:
需要注意的是.extend在对样式组件有效,如果是其他的React组件,需要用styled样式化一下。

// The Button from the last section without the interpolations
const Button = styled.button`
  color: palevioletred;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

// We're extending Button with some extra styles
const TomatoButton = Button.extend`
  color: tomato;
  border-color: tomato;
`;

render(
  <div>
    <Button>Normal Button</Button>
    <TomatoButton>Tomato Button</TomatoButton>
  </div>
);

在极少特殊情况下,我们可能需要更改样式组件的标签类型。我们有一个特别的API,withComponent可以扩展样式和替换标签:

const Button = styled.button`
  display: inline-block;
  color: palevioletred;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

// We're replacing the <button> tag with an <a> tag, but reuse all the same styles
const Link = Button.withComponent('a')

// Use .withComponent together with .extend to both change the tag and use additional styles
const TomatoLink = Link.extend`
  color: tomato;
  border-color: tomato;
`;

render(
  <div>
    <Button>Normal Button</Button>
    <Link>Normal Link</Link>
    <TomatoLink>Tomato Link</TomatoLink>
  </div>
);

添加attr

我们可以使用attrsAPI来为样式组件添加一些attr属性,它们也可以通过标签模板插值函数拿到props传值。

const Input = styled.input.attrs({
  // we can define static props
  type: 'password',

  // or we can define dynamic ones
  margin: props => props.size || '1em',
  padding: props => props.size || '1em'
})`
  color: palevioletred;
  font-size: 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;

  /* here we use the dynamically computed props */
  margin: ${props => props.margin};
  padding: ${props => props.padding};
`;

render(
  <div>
    <Input placeholder="A small text input" size="1em" />
    <br />
    <Input placeholder="A bigger text input" size="2em" />
  </div>
);

动画

带有@keyframes的CSS animations,一般来说会产生复用。styled-components暴露了一个keyframes的API,我们使用它产生一个可以复用的变量。这样,我们在书写css样式的时候使用JavaScript的功能,为CSS附能,并且避免了名称冲突。

// keyframes returns a unique name based on a hash of the contents of the keyframes
const rotate360 = keyframes`
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
`;

// Here we create a component that will rotate everything we pass in over two seconds
const Rotate = styled.div`
  display: inline-block;
  animation: ${rotate360} 2s linear infinite;
  padding: 2rem 1rem;
  font-size: 1.2rem;
`;

render(
  <Rotate>&lt; 💅 &gt;</Rotate>
);

支持 React Native

高级特性

Theming

styled-components暴露了一个<ThemeProvider>容器组件,提供了设置默认主题样式的功能,他类似于react-rudux的顶层组件Provider,通过context实现了从顶层到底层所有样式组件的默认主题共用。

const Button = styled.button`
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border-radius: 3px;
  
  /* Color the border and text with theme.main */
  color: ${props => props.theme.main};
  border: 2px solid ${props => props.theme.main};
`;

Button.defaultProps = {
  theme: {
    main: 'palevioletred'
  }
}
// Define what props.theme will look like
const theme = {
  main: 'mediumseagreen'
};

render(
  <div>
    <Button>Normal</Button>
    <ThemeProvider theme={theme}>
      <Button>Themed</Button>
    </ThemeProvider>
  </div>
);

Refs

通常我们在给一个非原生样式组件添加ref属性的时候,其指向都是该组件实例的索引,我们通过用innerRef可以直接拿到里面的DOM节点。

const AutoFocusInput = styled.input`
  background: papayawhip;
  border: none;
`;

class Form extends React.Component {
  render() {
    return (
      <AutoFocusInput
        placeholder="Hover here..."
        innerRef={x => { this.input = x }}
        onMouseEnter={() => this.input.focus()}
      />
    );
  }
}

Security

因为styled-components允许我们使用任意输入作为CSS属性值,一旦意识到这一点,我们马上明白要对输入做安全性校验了,因为使用用户外部的输入样式可以导致用户的浏览器被CSS注入攻击。CSS注入攻击可能不明显,但是我们还是得小心一点,某些IE浏览器版本甚至允许在URL声明中执行任意的JS。

这个例子告诉我们外部的输入甚至可能在CSS内调用一个API网络请求。

// Oh no! The user has given us a bad URL!
const userInput = '/api/withdraw-funds';

const ArbitraryComponent = styled.div`
  background: url(${userInput});
  /* More styles here... */
`;

CSS.escape这个未来API标准可净化JS中的CSS的问题。但是浏览器兼容性目前还不是太好,所以我们建议在项目中使用polyfill by Mathias Bynens

CSS共存

如果我们打算把styled-components和现有的css共存的话,我们需要注意两个实现的细节问题:

styled-components也会生成真实的样式表,并通过className属性链接生成的样式表内容。在JS运行时,他会生成一份真实的style节点插入到document的head内。

注意的一个小地方:

// MyComponent.js
const MyComponent = styled.div`background-color: green;`;

// my-component.css
.red-bg {
  background-color: red;
}

// For some reason this component still has a green background,
// even though you're trying to override it with the "red-bg" class!
<MyComponent className="red-bg" />

我们styled-components生成的style样式表一般是在head头部的最底下,同等CSS优先级条件下是会覆盖默认前者css文件的样式的。这个插入顺序使用webpack来调整是比较难得。所以,我们一般都这样通过调整css优先级来改变显示:

/* my-component.css */
.red-bg.red-bg {
  background-color: red;
}

Media Templates

媒体查询是开发响应式web应用不可或缺的存在,这是一个简单的例子:

const Content = styled.div`
  background: papayawhip;
  height: 3em;
  width: 3em;

  @media (max-width: 700px) {
    background: palevioletred;
  }
`;

render(
  <Content />
);

因为媒体查询语句很长,并且经常在整个应用程序中重复使用,所以为此创建一些模板来复用是很有必要的。

使用JS的功能特性,我们可以轻松定义一份可配置的语句,包装媒体查询和样式。

const sizes = {
  desktop: 992,
  tablet: 768,
  phone: 376
}

// Iterate through the sizes and create a media template
const media = Object.keys(sizes).reduce((acc, label) => {
  acc[label] = (...args) => css`
    @media (max-width: ${sizes[label] / 16}em) {
      ${css(...args)}
    }
  `

  return acc
}, {})

const Content = styled.div`
  height: 3em;
  width: 3em;
  background: papayawhip;

  /* Now we have our methods on media and can use them instead of raw queries */
  ${media.desktop`background: dodgerblue;`}
  ${media.tablet`background: mediumseagreen;`}
  ${media.phone`background: palevioletred;`}
`;

render(
  <Content />
);

这太cool了,不是吗?

Tagged Template Literals

标签模板是ES6的一个新特性,这是我们styled-components创建样式组件的方式和规则。

const aVar = 'good';

// These are equivalent:
fn`this is a ${aVar} day`;
fn([ 'this is a ', ' day' ], aVar);

这看起来有点麻烦,但是这意味着我们可以在styled-components生成样式组件中接受变量、函数、minxins,并将其变为纯css。

这篇文章可以了解更多:The magic behind 💅 styled-components

Server Side Rendering

styled-components很好地支持SSR。

一个例子:

import { renderToString } from 'react-dom/server'
import { ServerStyleSheet } from 'styled-components'

const sheet = new ServerStyleSheet()
const html = renderToString(sheet.collectStyles(<YourApp />))
const styleTags = sheet.getStyleTags() // or sheet.getStyleElement()

也可以这样组件化包裹,只要在客户端不这么使用:

import { renderToString } from 'react-dom/server'
import { ServerStyleSheet, StyleSheetManager } from 'styled-components'

const sheet = new ServerStyleSheet()
const html = renderToString(
  <StyleSheetManager sheet={sheet.instance}>
    <YourApp />
  </StyleSheetManager>
)

const styleTags = sheet.getStyleTags() // or sheet.getStyleElement()

sheet.getStyleTags()返回一个style标签数组。具体styled-components关于SSR更深入的操作,不在这里继续讨论了,还可以告知他兼容Next.js关于SSR的解决方案。

Referring to other components

styled-components提供了component selector组件选择器模式来代替我们以往对class名的依赖,解决得很干净。这下我们不必为命名和选择器冲突而苦恼了。

const Link = styled.a`
  display: flex;
  align-items: center;
  padding: 5px 10px;
  background: papayawhip;
  color: palevioletred;
`;

const Icon = styled.svg`
  transition: fill 0.25s;
  width: 48px;
  height: 48px;

  ${Link}:hover & {
    fill: rebeccapurple;
  }
`;

const Label = styled.span`
  display: flex;
  align-items: center;
  line-height: 1.2;

  &::before {
    content: '◀';
    margin: 0 10px;
  }
`;

render(
  <Link href="#">
    <Icon viewBox="0 0 20 20">
      <path d="M10 15h8c1 0 2-1 2-2V3c0-1-1-2-2-2H2C1 1 0 2 0 3v10c0 1 1 2 2 2h4v4l4-4zM5 7h2v2H5V7zm4 0h2v2H9V7zm4 0h2v2h-2V7z"/>
    </Icon>
    <Label>Hovering my parent changes my style!</Label>
  </Link>
);

注意:

class A extends React.Component {
  render() {
    return <div />;
  }
}

const B = styled.div`
  ${A} {
  }
`;

这个例子是不可以的,因为A继承ReactComponent,不是被styled构造过的。我们的组件选择器只支持在Styled Components创建的样式组件。

class A extends React.Component {
  render() {
    return <div className={this.props.className} />;
  }
}

const StyledA = styled(A)``;

const B = styled.div`
  ${StyledA} {
  }
`;

API文档

基本

  • styled
  • .attrs
  • ``字符模板
  • ThemeProvider

助手

  • css
  • keyframes
  • injectGlobal
  • isStyledComponent
  • withTheme

支持CSS

在样式组件中,我们支持所有CSS加嵌套。因为我们生成一个真实的stylesheet而不是内联样式,所以CSS中的任何工作都在样式组件中工作!

(&)被我们所生成的、唯一的类名替换给样式组件,使其具有复杂的逻辑变得容易。

支持flow和typescript

更多工具

Babel Plugin

Test Utilities

Jest Styled Components,基于jest,可对styled-components做单元测试

demo

Stylelint

使用stylelint 检查我们的styled-components样式书写规范。

Styled Theming 语法高亮显示

在模板文本中写入CSS时丢失的一个东西是语法高亮显示。我们正在努力在所有编辑器中实现正确的语法高亮显示。支持大部分编辑器包括Visual Studio Code、WebStorm。

总结

下面简单总结一下 styled-components 在开发中的表现:

  • 提出了 container 和 components 的概念,移除了组件和样式之间的映射关系,符合关注度分离的模式;
  • 可以在样式定义中直接引用到 js 变量,共享变量,非常便利,利用js的特性为css附能,帅毙了!
  • 支持组件之间继承,方便代码复用,提升可维护性;
  • 兼容现有的 className 方式,升级无痛;
  • 这下写CSS也乐趣十足了。
  • styled-components的最基本思想就是通过移除样式和组件之间的映射来执行最佳实践
  • 一个让styled-components很容易被接受的特性:当他被怀疑的时候,你同样可以使用你熟悉的方法去使用它!

当然,styled-components 还有一些优秀的特性,比如服务端渲染和 React Native 的支持。



题外:styled-components的魔法

如果你从来没看见过styled-components,下面是一个简单的样式组件的例子:

const Button = styled.button`
  background-color: papayawhip;
  border-radius: 3px;
  color: palevioletred;
`

现在可以像使用普通React组件一样渲染使用。

<Button>Hi Dad!</Button>

那么,这是怎么工作的呢?这个过程中到底发生了什么魔法?

标签模板

实际上, style.button` `是JavaScript的新语法特性,属于ES6的标签模板功能。

本质上, styled.button` styled.button()`是一样的。他们的差异只在传递参数时就变得可见了。

styled-components利用模板字符串的用处在于可以给内部props赋值。

const Button = styled.button`
  font-size: ${props => props.primary ? '2em' : '1em'};
`
// font-size: 2em;
<Button primary />
查看原文

MaE4Nnh 收藏了文章 · 8月27日

深入理解css盒子模型

css是一门具象语言,并不像js那样具有逻辑性,因此,就算入行了前端很久的工程师,也觉得css难以掌握。下面我们就一步一步揭开css的神秘面纱,深入理解css盒模型,这对我们在布局上会有一个质的提升。

盒子模型

图片描述
相信很多人对这幅图都不陌生,盒子模型简单点理解就是外边距(margin)+边框(border)+内边距(padding)+内容(content),页面所呈现的效果其实就是一个个盒子堆叠而成的。每一个元素其实是包含了一个“外在盒子”和一个“内在盒子”,其中“外在盒子”负责元素是一行显示还是换行显示,而“内在盒子”则负责宽高、内容展现。我们都知道inline-block(inline对应于“外在盒子”,block对应于“内在盒子”),而block可以简单地理解为block-block,table为block-table(因为还有一个inline-table)。

内联盒模型

  • 内容区域(content area)
  • 内联盒子(inline box)
  • 行框盒子(line box)
  • 包含盒子(containing box)

内容区域(content area)。内容区域指的是一种围绕文字看不见的盒子,其大小仅受字符本身特性控制,本质上是一个字符盒子(character box);但是图片这样的替换元素,其显示内容不是文字,因此内容区域可以看成是元素自身。

内联盒子(inline box)。“内联盒子”不会让内容成块显示,而是排成一行,这里的内联盒子指的是元素的“外在盒子”,用来决定元素是内联还是块级。该盒子又可以细分为“内联盒子”和“匿名内联盒子”。如下:
图片描述

行框盒子(line box)。每一行就是一个行框盒子,每个行框盒子都是由一个个内联盒子组成,注意:line-height是作用在行框盒子上的,并最终决定高度(替换元素除外,后面会讲解什么是替换元素)。

包含盒子(containing box)。此盒子由一行一行的“行框盒子”组成(css规范中,并没有“包含盒子”的说法,更准确的称呼是“包含块”(containing block)。

width

width的默认值是auto,但很多人却都不理解这个值是什么意思,因为auto在不同场景会有不同的表现:

  • fill-available
  • fit-content
  • min-content
  • max-content

fill-available:充分利用可用空间,例如div、p这些元素的宽度是默认100%于父级容器的。但是width: auto却不同于width: 100%,这是很多人不理解的地方。如果你设置了width: 100%,这里指的是内容区域100%,即css3中的content-box,这时如果你设置了padding、border或者margin,元素都会撑破父元素,从而破坏布局。你当然可以设置box-sizing: border-box,但可惜的是css3中没有margin-box,这时候你如果设置了margin,依然会撑破父元素,但是width: auto却不会,如下所示:

图片描述

图片描述

fit-content:收缩到合适,典型代表浮动、绝对定位(有例外,设置了对立属性:left、right、top、bottom时,宽度和高度由祖先元素position非static的元素决定,但是替换元素除外:img、video、canvas等)、inline-block、table。利用这个特性我们可以实现,文字整体居中,多行则居左显示,如下:

图片描述

图片描述

图片描述

min-content:收缩到最小。在表格中最常见,当每一列空间都不够的时候,文字能断则断,中文随便断,英文单词不能断。可以根据这个特性实现凹凸图形等效果,如下:

图片描述

图片描述

图片描述

max-content:超出容器限制,内容很长的连续英文或数字,或者内联元素被设置为了white-space: nowrap。

图片描述

图片描述

图片描述

height

height的默认值也是auto,指的是其高度由内部元素堆叠而成,内部元素盒子有多高,元素就有多高。但在绝对定位中,若同时设置了top与bottom,则其高度由父盒子高度减去top与bottom。

height: 100%。如果父元素height为auto,则子元素height:100%是无效的,要想子元素height: 100%生效,则:

  • 父元素设定显式高度值
  • 使用绝对定位(绝对定位元素的百分比是根据padding box计算的,非绝对定位元素百分比是根据content box计算的)

替换元素

由于替换元素在很多表现上都与普通内联元素不一样,因此在这里着重介绍一下替换元素。

  • 根据“外在盒子”是内联还是块级,我们把元素分为内联元素和块级元素,而根据内容是否可替换,我们把元素分为可替换元素和非替换元素。
  • <img>,<video>,<canvas>,<input>,<textarea>,<iframe>都是替换元素。
  • 替换元素外观不受页面css的影响,有自己的尺寸,一般为300 * 150,在很多css属性上有自己的一套表现规则,例如vertical-align默认就是元素下边缘对齐,而不是基线对齐。
  • 替换元素尺寸计算规则:css尺寸 > html尺寸 > 固有尺寸
  • 内联替换元素和块级替换元素规则一致,即display: block,其宽度也不会100%。
  • 替换元素固有尺寸无法更改,width和height改变的是content-box的宽高,而默认替换元素的object-fit是fill,也就是会填充content-box,因此看上去像是改变了固有尺寸。
  • 替换元素before和after伪元素无效。

padding

  • padding与内联元素
  • padding的百分比值

padding与内联元素。padding作用在块级元素上会影响盒子的宽高,但是如果作用在内联元素上(不包括替换元素),似乎就只能作用在水平方向上,垂直方向上就没看到任何影响。但事实并不是没有影响,只是视觉上我们觉得没有影响而已。因为内联元素没有可视宽度和可视高度的说法(clientWidth和clientHeight永远是0),垂直方向完全受line-height和vertical-align的影响,视觉上并没有改变上一行和下一行内容的间距,因此,给我们的感觉就是垂直方向上padding没有起作用。利用这个特性,我们可以在垂直方向上增大可点击区域,这样既不会破坏现有布局,也能很好地响应用户的点击。特别是在移动端,一个关闭的“x”如果太小,用户就很难点击到,调大字体又会影响布局,这时候就可以用到padding。

padding的百分比值。padding不支持负值,padding百分比无论宽高都是相对于width来说的,另外padding区域是跟着行框盒子走的。因此,如果padding作用于内联元素,则宽度和高度细节有差异,并且padding会断行,其原因在于strut,意思是说每一个行框盒子前面都有一个不可见的盒子,其line-height和font-size都继承于父元素,称为strut。利用padding的这些特性,我们可以实现如下效果:

  • 利用padding实现一个正方形

图片描述

图片描述

图片描述

  • 内联元素padding高度差异(只需把font-size设为0即可变为正方形)

图片描述

图片描述

图片描述

  • padding断行(由于padding作用在行框盒子上,因此文字换行,padding也跟着换行,后面的背景盖住了前面的,就形成了这种效果)

图片描述

图片描述

margin

  • margin: auto
  • margin改变元素尺寸
  • margin负值
  • margin合并
  • margin无效的情况

margin: auto生效的前提是元素在width和height为auto的时候能够自动填充容器,这样,在设置width或height的值时,如果还有剩余尺寸,margin: auto就可以利用剩余尺寸。因此在绝对定位元素设置了top、bottom、left、right的情况下,就可以很方便地实现水平垂直居中,如下:

图片描述

图片描述

图片描述

margin改变元素尺寸。在元素width为auto的情况下,margin正值和负值都能改变元素的尺寸。如下:

图片描述

图片描述

图片描述

margin负值。margin支持负值,并且用途十分广泛,例如,在等宽的盒子中,最后一个元素不因margin-right而折行;实现等高布局等。如下:

盒子并列占满父元素:

图片描述

图片描述

图片描述

图片描述

等高布局,其原理是利用padding撑开一片足够大的高度,再用margin负值将顶下去的元素收回来:

图片描述

图片描述

图片描述

margin合并。块级元素的上外边距与下外边距有时会合并为单个外边距,这种现象称为“margin合并”。一般会有以下三种:

1、相邻兄弟元素margin合并

2、父级和第一个/最后一个子元素合并

margin-top合并,解决方案:

    父元素设置为块状格式化上下文元素
    父元素设置border-top值
    父元素设置padding-top值
    父元素和第一个子元素之间添加内联元素进行分隔

margin-bottom合并,解决方案:

    父元素设置为块状格式化上下文元素
    父元素设置border-bottom值
    父元素设置padding-bottom值
    父元素和最后一个子元素之间添加内联元素进行分隔
    父元素设置 height、min-height 或 max-height。

3、空块级元素的margin合并,即自身有margin-top和margin-bottom,但元素是空的,此时会合并为一个margin。

margin无效。margin在某些场景下会失效,但有些“失效”只是视觉上的表现而已。如下:

  • display 计算值 inline 的非替换元素的垂直 margin 是无效的,虽然规范提到有 渲染,但浏览器表现却未寻得一点踪迹,这和 padding 是有明显区别的。对于内联替换元素, 垂直 margin 有效,并且没有 margin 合并的问题,所以图片永远不会发生 margin 合并。
  • 表格中的<tr>和<td>元素或者设置 display 计算值是 table-cell 或 table-row 的元素的 margin 都是无效的。但是,如果计算值是 table-caption、table 或者 inline-table 则没有此问题,可以通过 margin 控制外间距,甚至::first-letter 伪元素也可以解析 margin。
  • margin合并的时候,更改margin值可能无效。因为垂直方向上会发生margin合并。
  • 绝对定位元素非定位方位的margin值“无效”(其实margin是有效的,只是元素绝对定位了,并不影响其相邻元素的渲染)。
  • 定高容器的子元素的margin-bottom或者宽度定死的子元素的margin-right的定位“失效”。这里的失效也是假的,原因跟绝对定位的margin无效类似,在一个默认流下,其定位方向是左侧和上方,此时只有margin-left和margin-top可以影响其定位,而margin-right和margin-bottom则只会影响其相邻元素,若此时没有相邻元素,则看上去像是margin无效。
  • 内联特性导致的margin值无效。一个div元素中有一个img图片,我们对img使用margin-top负值,当margin-top负值达到一定值的时候,再往上图片也不会上移。

border

  • 制作图形
  • 等高布局

相信不少同学都使用过border来制作图形,例如三角形、圆形等等,此处就不举例子,主要讲讲等高布局,代码和效果如下所示:

图片描述

图片描述

图片描述

其原理就是父元素撑开一个border-left,菜单栏左浮动,并且宽度跟父元素border保持一致,通过margin-left负值往左偏移到border位置,另外父元素设置伪元素after来清除浮动,这样就可以实现左侧固定,右侧自适应的两栏等高布局。

参考资料:
《CSS世界》

查看原文

MaE4Nnh 收藏了文章 · 8月27日

Http协议构成

什么是协议

词条解释:经过谈判、协商而制定的共同承认、共同遵守的规定与条款。
标准协议:买票上车,司机与乘客都认同协议,只要乘客买了票,司机必须让乘客上车。
流氓协议:生米煮成熟饭。

什么是Http协议

Http协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW)服务器传输超文本到本地浏览器的传送协议。
客户端与服务端都是认同的,简单一点理解就是客户端找服务端要东西,也就是请求,服务端必须给客户端东西,也就是响应。

在浏览器端输入域名,会发生什么

发生了什么:

搜索本地Dns > 本地host > 向宽带运营商服务器发起Dns解析请求 > 建立TCP连接(三次握手) > 数据传输 > TCP连接断开(也就是挥手)

什么是Dns

Dns是domain name service的缩写,它的作用是将域名翻译成ip地址。服务器或者应用,对于域名是无感知的,它们只会IP地址查找网络节点,Dns其实就是一个翻译,将服务器看不懂的域名地址翻译成Ip地址,这样用户在浏览器中输入域名,服务器就可以通过dns知道用户请求的是哪个网站,然后才将对应的网站内容返回给用户。
Dns找不到的情况下会从本地host中查找;
chrome查看本地Dns缓存 chrome://net-internals/#dns

Http协议分为三部分,Http状态行、Http请求头,Http响应

Http状态行

必应的:
clipboard.png

百度的:
clipboard.png

上面的图片是firefox的,chrome的长这样
clipboard.png

状态行由包含了请求方式,请求路径,协议版本等数据构成
常见属性:

clipboard.png

Request URL:请求Url
Request Method: 请求方式,除了常用的GET、POST外还有PUT与DELETE
Status Code: 常用的状态码有如下几种

1xx 信息处理状态码

表示数据正在处理中

2xx 成功状态码
* 200:数据被正常处理
* 204: 服务器接受数据并请求成功处理,但没有资源可返回
3xx 重定向状态码
* 302:临时性重定向
* 301:永久重定向,与302的区别是对于SEO更加友好,搜索引擎到页面后爬虫会记录重定向的地址
* 304:重定向到本地缓存,浏览器中存在访问页面时会用到
4xx 客户端错误
* 400:错误的请求,比如本地开发nginx配置错误后,访问某个本地站点返回400
* 403:服务端收到了请求,但拒绝访问,比如普通员工请求管理员数据提示403
* 404:找不到该路径对应的资源
5xx 服务端错误
* 500:服务器端执行请求时发生错误
* 503:服务器处于超负载或者正在停机维护,现在无法处理请求  

当然Http状态码包含六七十种,具体可以看W3C关于状态码的规范)

Remote Address: 请求远程Ip地址
Referrer Policy: Referrer策略,当我们在页面中请求图片、Js、接口的时候浏览器一般会加上表示来源的Reffer数据,该数据有5个值。

* no-referrer: 所有情况下都不发送referrer
* no-referrer-when-downgrade: 协议降级时不发送 Referrer 信息,比如Https页面中请求http资源,大部分浏览器默认所采用的
* Origin Only: 发送只包含host部分的Reffer信息,比如“101.101.0.3/main的referrer”值就是“101.101.0.3”
* origin-when-crossorigin: 只有跨域是才发送只包含host部分的reffer值,(域名,协议,端口其中任何一个值不一致则被认为是跨域)
* unsafe-url: 无论任何情况下都发送reffer信息,很少使用

Http请求头

包含3部分,请求行、请求首部、请求体。

请求行

clipboard.png

类似于上图的一串数据,默认为空

请求首部

clipboard.png

请求首部就是客户端向服务器提供的额外信息,比如User-Agent表明客户端身份,表明客户端的身份,常见的请求首部如下:

  • Accept: 告知服务端客户端接受的MIME类型数据,比如“text/html,image/jpeg”表示接受“html”文本与“jpeg”格式图片文件
  • Accept-Charset: 浏览器接受字符集类型,默认“utf-8”,很多中文网页中是“gb2312”
  • Accept-Encoding: 浏览器能解压数据的解码方式,常见的有“gzip、deflate、 br”,存在多个的情况下使用逗号隔开,在浏览器支持的情况下优先使用后置解压方式,如“gzip,deflate,br”三个中优先使用br解码
  • Connection: 连接方式,默认“Keep-Alive”,表示需要持续连接,但此处的持续连接指的并不是永久连接,而是指在一定时间内如果存在数据请求则会复用通一个Tcp容器传输数据,否则断开
  • Content-Length: 设置请求正文长度,对于Post请求无效
  • Scheme: 使用协议,默认Http/Https,默认Http
  • Host:浏览器请求的主机与端口,与Scheme配合可以在服务端进行第三方接口转发代理
  • User-Agent:浏览器信息,可以用来进行获取访问浏览器类型占比分析。“GOOGLEBOT-IMAGE”是google的图片爬虫User-Agent信息,可以用来分析网页图片被爬虫抓取次数。
  • Content-Type: 内容格式,比如“text/html”表示html文本,“Application/json”表示传输json数据。
  • Cache-control: 缓存控制,这里不做细讲,详细原理可以看这篇博文

请求体

clipboard.png

Get类型的请求请求内容是有长度限制的,比如IE中限制是2KB,Post中无限制。

Http响应

HTTP响应是服务器在接收客户端发送请求后经过一些处理而做出的响应,Http响应与Http请求类似,也由三个部分组成,分别是:状态行,响应头,响应正文

状态行

与Http请求状态行一致,不深究

响应头

clipboard.png

响应头就是服务端想客户端提供的额外信息,常见的响应头如下:

  • Connection: 连接方式,默认“Keep-Alive”,如果服务端不支持长连接方式则会返回close
  • Content-Type: 内容格式,比如“text/html”表示html文本,“Application/json”表示传输json数据。
  • Set-Cookie: 设置cookie数据,包含cookie主体内容,过期时间,作用域等信息
  • ...很多数据与Http请求头一致,数据在来回传递

响应正文

与Http请求正文一致,区别是数据由服务度发送给客户端

会话跟踪

由于Http协议是一个无状态协议,该协议不能保存客户信息,一旦数据传输完毕,下次数据请求需要重新连接,所以需要会话跟踪

Cookie

简单一点理解就是Cookie是服务端颁发给客户端的一个通行证,用来确认客户信息,浏览器得到这个通行证后,会在本地保存起来,当浏览器再次请求该网站是,浏览器将这个通行证一并提交给服务器,服务器检查该通行证,一次来确认用户信息。
Cookie在请求头和响应头之间来回传递,所以客户端与服务端都能获取到Cookie数据。

Session

在不理解Http协议是一个无状态协议的情况下,初学者往往会有一个误区,认为Session是保存在服务端的,当服务端与客户端连接关闭的时候Session会被清空。
正确的理解应该是Session其实是一个特殊的Cookie。

  • 当服务端需要未客户客户端的请求创建一个Session的时候,首先会检查客户端是否存在sessionId标识,存在SessionId的情况下,则说明以前已经为此客户端创建过Session,服务器就按照SessionId把这个Session检索出来使用。
  • 客户端不存在SessionId的情况下,服务端会新建一个Session,并且生成一个与该Session相关联的SessionId,SessionId一般都是经过加密计算,很难找到规律的字符串,
  • 这个session id将被在本次响应中返回给客户端保存。
  • 客户端保持SessionId的方式仍然是采用Cookie进行保存,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。
  • 般这个cookie的名字都是类似于“SEEESIONID”,而。例如:xxxSESSIONID=KI98uvjFDJu671F7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWKjHb!-135785664。
  • 当服务端在设置的时间段内没有与客户端进行Session数据的传输情况下,会自动清除服务端Session对象。

举个例子:
clipboard.png

上图在koaJs中添加session

clipboard.png

上图是服务端返回的cookie,这个cookie与一般的cookie的区别是是带一串session标识加密字符串(koa:sess.sig=Dp_qBm3JsJX1rxvx9kgEQi64TV0)
clipboard.png

上图是客户端发送的cookie,与一般的cookie的区别也是呆了一串session标识加密字符串

对比cookie:

  • 如果浏览器禁用了Cookie,Session同样不可使用。
  • 对比于Cookie,Session对于服务器消耗更大。

Http1.1缺点

目前大部分网站使用的都是Http1.1协议,Http1.1协议存在以下缺点

  • Http1.1是文本协议,对与人类来说是很容易理解的,但对于计算力来说及其不友好,转换效率低
  • Tcp启动与连接都很慢,虽然可以使用keep-alive复用Tcp链接(这也是为什么网页中的图片资源为什么需要使用精灵图的原因)。
  • 当网页中资源较多的情况下,浏览器处于资源控制对于单个域名会有最多同时发起6-8个连接请求限制
  • 统一域名下Http请求头重复
  • 资源加载不能设置优先级

Http2

了解Http2一开始可能会有两个误区

  • 很多教程中都把Https当做Http2来讲解,╯□╰,Https是针对网页数据加密传输的一种认证机制。
  • Http2协议并不是Http2.0协议,后续官方把后面的版本叫做http3.0了,所以2就是2了,22222222~

Http2协议专门针对Http1.1协议缺点做了改进

二进制文本协议

HTTP/2 采用二进制格式传输数据,而非HTTP1.1 中的文本格式,对于计算机解析起来会更高效二进制协议解析起来更高效。

单一连接

在同一个域名下,Http2只会创建一个Tcp或者Tls连接,减少了连接数,对于多个资源的页面Http1.1中会创建6~8个连接。
并且由于Tcp的滑动连接(连接刚开始数据传输很慢,后续会越来越快),Http1.1中多个Tcp连接会导致每个资源的请求都相当慢,Http2中由于只创建一个连接,完美的避免了这种情形。
Http2中的这个连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息由个帧(HTTP2中数据通信的最小单位)组成。多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。
关于帧的格式(官方)如下:

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+

多路复用

多路复用是通过在一个流上分配多个HTTP请求响应交换来实现的。流在很大程度上是相互独立的,因此一个请求上的阻塞或终止并不会影响其他请求的处理。下图一目了然
clipboard.png

请求头优化

HTT2在客户端和服务器端使用“请求对照表”来跟踪和存储之前发送的请求头key-value,对于相同的数据,不再通过每次请求和响应发送;
在一定的时间内,在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值
并且Http2对请求头进行数据进行“HPACK”数据压缩,╯□╰说实话我也不知道这个是个什么鬼~

服务端推送

服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 再发送这些请求。
该如何开启,如何设置,好像没有比较好的教程~

资源传输优先级

资源传输设置优先级,比如优先加载js脚本,让页面尽快出现动态效果
还没研究过怎么进行设置~

clipboard.png

参考资料

http状态码:https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
浏览器缓存:https://segmentfault.com/a/1190000012233230
Session原理:https://zhuanlan.zhihu.com/p/33925382

查看原文

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 7月10日
个人主页被 57 人浏览