上帝何解

上帝何解 查看完整档案

武汉编辑武汉科技大学  |  信息安全 编辑  |  填写所在公司/组织填写个人主网站
编辑

不忘初心,方得始终!!!!

个人动态

上帝何解 赞了回答 · 2020-06-09

cross-env 不起作用怎么办?

自问自答,原因是中间不能有&&&&这样会划分出前后两个环境,导致后一个语句的环境没有NODE_ENV

关注 5 回答 3

上帝何解 关注了用户 · 2020-05-13

fenbox @fenbox

主业设计,副业写代码

          =,    (\_/)    ,=
           /`-'--(")--'-'\
      woo /     (___)     \
         /.-.-./ " " \.-.-.\

08 年参与 Typecho 开源博客项目
11 年参与 SegmentFault 开发者问答项目
12 年创业,与 joyqi、sunny 驻扎莲花街

关注 490

上帝何解 赞了文章 · 2020-02-06

React + GraphQL + apollo-client技术栈简要介绍(基于官方文档v2.5)

欢迎进入我的博客阅览此文章。


本文档诸多例子来源于官方文档,但也在编写过程中添加了我对这套技术栈的一些理解,如果你更喜欢看官方文档,请移步官网/官方文档

为什么要使用apollo?
没有redux繁琐的action、reducer、dispatch……让全局管理store变得简单、直白!

使用redux管理状态,重心是放在如何去拿数据上;而apollo把重心放在需要什么数据上。理解这一点非常重要!

好了,废话不多说,我们立即开始!

准备工作

创建React项目

  1. 你可以使用create-react-app快速创建一个React应用,不熟悉create-react-app的小伙伴可以先行了解。
npm i create-react-app -g

create-react-app react-apollo-client-demo --typescript

cd react-apollo-client-demo

npm start
  1. 也可以在codesandbox上在线搭建React项目。方便快捷!

搭建GraphQL服务

  1. 你可以在github上fork graphpack项目,然后使用github账号登录codesandbox并导入该项目,即可零配置搭建一个在线的GraphQL服务。
    本文档在编写时在codesandbox上搭建了一个服务,可供参考:https://kdvmr.sse.codesandbox...
  2. 也可以在本地搭建自己的GraphQL服务,因不在本文档讨论范围,所以暂不提供搭建步骤。

安装需要的包

既然本文讲的是graphql + react + apollo开发React App,所以需要安装以下包来支撑,以前使用的redux、react-redux等包可以丢到一边了。

PS:在apollo 1.0时代,本地状态管理功能(本文档后面作了介绍)还依赖于redux等相关技术。但现在apollo已经升级到2.0时代,已经完全抛弃了redux的依赖。
npm install apollo-boost react-apollo graphql --save

我们来看一下这三个包的作用:

  • apollo-boost:包含设置Apollo Client所需的核心包。如果你需要按自己的意愿定制化项目,可以自行选择安装单独的包:
    apollo-client:apollo客户端包

    • apollo-cache-inmemory:官方推荐的缓存包
    • apollo-link-http:用于获取远程数据的包
    • apollo-link-error:用于处理错误的包
    • apollo-link-state:本地状态管理的包(2.5版本已集成到apollo-client
  • react-apollo:react的图层集成(用react的组件方式来使用apollo)
  • graphql:解析GraphQL查询

实例化Apollo客户端

需要注意一点的是apollo-boostapollo-client都提供了ApolloClient,但是两者需要的参数有一点差别。具体见各自API:

  • apollo-boost导出的Apollo Client对象(详细API):集成官方核心功能的一个大集合对象
  • apollo-client导出的Apollo Client对象(详细API):默认为App所在的同一主机上的GraphQL端点
    要自定义uri还需引入apollo-link-http包。如果你使用的是apollo v1.x,可直接从apollo-client包内导出createNetworkInterface方法,用法请见(1.x迁移至2.x指南)[https://github.com/apollograp...]

我们看一下使用apollo-boost实例化Apollo客户端:

import ApolloClient from 'apollo-boost'

const client = new ApolloClient({
    // 如果你实在找不到现成的服务端,可以使用apollo官网提供的:https://48p1r2roz4.sse.codesandbox.io或者本教程的服务:https://kdvmr.sse.codesandbox.io/
    uri: '你的GraphQL服务链接'
})

以及使用apollo-client实例化Apollo客户端:

import ApolloClient from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'

const client = new ApolloClient({
  link: createHttpLink({ 
    uri: '你的GraphQL服务链接' 
  })
})

编写GraphQL查询语句

如果对GraphQL语法不是很了解,请先移步graphQL基础实践

为了演示GraphQL查询,我们暂且使用普通的请求看一段代码:

import { gql } from 'apollo-boost'

// 实例化 Apollo 客户端

client.query({
  query: gql`
    {
      rates(currency: "CNY") {
        currency
      }
    }
  `
}).then(result => console.log(result));

除了从apollo-boost导入gql,你还可以从graphql-tag这个包导入:

import gql from 'graphql-tag';

gql`...`

显而易见,gql()的作用是把查询字符串解析成查询文档。

连接Apollo客户端到React

// ...
import React from 'react'
import { ApolloProvider } from 'react-apollo'

const App: React = () => {
  // ...
  
  return (
    <ApolloProvider client={client}>
      <div>App content</div>
    </ApolloProvider>
  )
}

export default App
ApolloProvider(详细API)有一个必需参数client

和redux一样(redux使用<Provider/>组件包裹React App),react-apollo需要<ApolloProvider />组件来包裹整个React App,以便将实例化的client放到上下文中,就可以在组件树的任何位置访问到它。
另外,还可以使用withApollo来包裹组件,以在组件内部获取到client实例(还有很多获取实例的方法,文档后面有介绍),
详情请参考withApollo()

Query组件与Mutation组件

在graphql中,query操作代表查询,mutation操作代表增、删和改,他们对应REST API的GET与POST请求,但要注意在实际的请求过程中Query或许并不是GET请求,这里只是为了方便大家理解做的假设!

获取数据——Query组件

import { Query } from "react-apollo";
import { gql } from "apollo-boost";

const ExchangeRates = () => (
  <Query
    query={gql`
      {
        rates(currency: "USD") {
          currency
          rate
        }
      }
    `}
  >
    {
      ({ loading, error, data }) => {
        if (loading) return <p>Loading...</p>;
        if (error) return <p>Error :(</p>;
    
        return data.rates.map(({ currency, rate }) => (
          <div key={currency}>
            <p>{currency}: {rate}</p>
          </div>
        ));
      }
    }
  </Query>
);

恭喜,你刚刚创建了第一个React Query组件!🎉🎉🎉

代码解析:

可以看到,在Query组件内,有一个匿名函数,这个匿名函数有一些参数,最常用的有:loadingerrordata
它们分别代表组件的加载状态、组件的加载错误提示、以及组件加载到的数据。

Query组件是从react-apollo导出的React组件,它使用render prop模式与UI共享GraphQL数据。(即我们可以从组件的props获取到GraphQL查询返回的数据)
Query组件还有很多其他props,上面就展示了一个query属性,其他的如:

  • children(根据查询结果显示要渲染的UI)
  • variables(用来传递查询参数到gql())
  • skip(跳过这个查询,比如登录时,验证失败,我们使用skip跳过这个查询,则登录失败)

更多props详见Query API

更新数据——Mutation组件

更新数据包括新增、修改和删除,这些操作统一使用Mutation组件。
Mutation组件和Query组件一样,使用render prop模式,但props有差别,Mutation API

import gql from 'graphql-tag';
import { Mutation } from "react-apollo";

const ADD_TODO = gql`
  mutation AddTodo($type: String!) {
    addTodo(type: $type) {
      id
      type
    }
  }
`;

const AddTodo = () => {
  let input;

  return (
    <Mutation mutation={ADD_TODO}>
      {
        (addTodo, { data }) => (
          <div>
            <form
              onSubmit={e => {
                e.preventDefault();
                addTodo({ variables: { type: input.value } });
                input.value = "";
              }}
            >
              <input
                ref={node => {
                  input = node;
                }}
              />
              <button type="submit">Add Todo</button>
            </form>
          </div>
        )
      }
    </Mutation>
  );
};

我们来梳理一下代码:

  • 首先,创建用于mutation(突变)的GraphQL,mutation需要一个字符串类型的参数type。它将用于mutation的GraphQL语句包装在gql方法中,并将其传递给Mutation组件props
  • Mutation组件内需要一个匿名函数作为子函数(也称为render prop函数),同Query组件,但参数有差异。
  • render prop函数的第一个参数是Mutation组件内部定义的mutate()函数。为了提高代码可读性,这里取名为addTodo
    也可以直接用“mutate”表示mutate函数,通过调用它来告诉Apollo Client,接下来要触发mutation(即触发提交表单的POST请求,在onSubmit事件里面可以看见addTodo函数被调用了)。
  • render prop函数的第二个参数是一个对象,这个对象有多个属性,包括data(mutation的结果,POST请求的返回值)、loading(加载状态)和error(加载过程中的错误信息),同Query组件

mutate函数(也就是上面命名的addTodo函数)可选地接受变量,如:

  • optimisticResponse
  • refetchQueries和update(这些函数就是后面用来更新缓存的)
  • ignoreResults:忽略mutation操作返回的结果(即忽略POST请求的返回值)

你也可以将这些值作为props传递给Mutation组件。详细的介绍请移步mutate函数 API

到这里,我们能发出客户端请求,也能得到服务器返回的结果,那接下来就着手怎么处理这些数据,然后渲染到UI上。我们看一下redux在这一步是怎么处理的:

  • dispatch触发数据请求
  • reducer根据之前定义的action处理得到的新数据,把数据保存到store中
  • react-redux的connect连接store与React组件
  • mapStateToProps/mapDisToProps完成render prop。

以上步骤,全靠一行一行的代码手动实现,我们再来看一下apollo是怎么处理的:

  • cache.writeQuery()

没错,你没看错,就是这一个API,搞定以上redux需要一大堆代码才能完成的数据更新!writeQuery相当于通过一种方式来告诉Apollo Client:
我们已经成功发出POST请求并得到了返回的结果了,现在把结果给你,你更新一下本地的缓存吧!
并且如果你的数据写得很规范(呃,其实它叫范式化,不要急,后面有介绍),甚至连这一句话都不用写,当你执行query或mutation后,UI便会自动根据新的数据更新UI!!

更新缓存——mutation后内部自动query

有时,当执行mutation时,GraphQL服务器和Apollo缓存会变得不同步。当执行的更新取决于本地缓存中已有的数据时,会发生这种情况。例如,本地缓存了一张列表,
当删除列表中的一项或添加一项新的数据,当我们执行mutation后,graphql服务端和本地缓存不一致,我们需要一种方法来告诉Apollo Client去更新项目列表的查询,
以获取我们mutation后新的项目列表数据;又或者我们仅仅使用mutation提交一张表单,本地并没有缓存这张表单的数据,所以我们并不需要新的查询来更新本地缓存。

下面来看一段代码:

import gql from 'graphql-tag';
import { Mutation } from "react-apollo";

const ADD_TODO = gql`
  mutation AddTodo($type: String!) {
    addTodo(type: $type) {
      id
      type
    }
  }
`;

const GET_TODOS = gql`
  query GetTodos {
    todos
  }
`;

const AddTodo = () => {
  let input;

  return (
    <Mutation
      mutation={ADD_TODO}
      update={(cache, { data: { addTodo } }) => {
        const { todos } = cache.readQuery({ query: GET_TODOS });
        cache.writeQuery({
          query: GET_TODOS,
          data: { todos: todos.concat([addTodo]) },
        });
      }}
    >
      {addTodo => (
        <div>
          <form
            onSubmit={e => {
              e.preventDefault();
              addTodo({ variables: { type: input.value } });
              input.value = "";
            }}
          >
            <input
              ref={node => {
                input = node;
              }}
            />
            <button type="submit">Add Todo</button>
          </form>
        </div>
      )}
    </Mutation>
  );
};

通过这段代码可以看见,update()函数可以作为props传递给Mutation组件,但它也可以作为prop传递给mutate函数,即:

// 借用上面的mutate(重命名为addTodo)函数来举例
addTodo({
  variables: { type: input.value },
  update: (cache, data: { addTodo }) => {
    // ...
  }
})

update: (cache: DataProxy, mutationResult: FetchResult):用于在发生突变(mutation)后更新缓存

参数:

  • cache,这个参数详细讲又可以讲几节课,所以这里只简单介绍一下,详细API

    • cache通常是InMemoryCache的一个实例,在创建Apollo Client时提供给Apollo Client的构造函数(怎么创建的Apollo Client?请返回创建一个apollo客户端复习一下)
    • InMemoryCache来自于一个单独的包apollo-cache-inmemory。如果你使用apollo-boost,这个包已经被包含在里面了,无需重复安装。
    • cache有几个实用函数,例如cache.readQuerycache.writeQuery,它们允许您使用GraphQL读取和写入缓存。
    • 另外还有其他的方法,例如cache.readFragmentcache.writeFragmentcache.writeData,详细API)。
  • mutationResult,一个对象,对象里面的data属性保存着执行mutation后的结果(POST请求后得到的数据),详细API

    • 如果指定乐观响应,则会更新两次update函数:一次是乐观结果,另一次是实际结果。
    • 您可以使用您的变异结果来使用cache.writeQuery更新缓存。

对于update函数,当你在其内部调用cache.writeQuery时,更新操作会触发Apollo内部的广播查询(broadcastQueries),而广播查询又会触发缓存中与本次mutation相关的数据的自动更新——自动使用受影响组件的GraphQL进行查询并更新UI。
因此当执行mutation后,我们不必手动去执行相关组件的查询,Apollo Client在内部已经做好了所有工作,这区别于redux在dispatch后所做的一切处理数据的工作。

有时,update函数不需要为所有mutation更新缓存(比如提交了一张表单)。所以,Apollo提供单独的cache.writeQuery方法,来触发相关缓存的查询,以更新本地缓存。
所以需要注意:仅仅只在update函数内部调用cache.writeQuery()才会触发广播行为。在其他任何地方,cache.writeQuery只会写入缓存,并且所做的更改不会广播到视图层。
为了避免给代码造成混淆,推荐在未使用update函数时,使用Apollo Client实例对象clientclient.writeQuery方法将数据写入缓存。

解析代码:

由于我们需要更新显示TODOS列表的查询,因此首先使用cache.readQuery从缓存中读取数据。
然后,我们将mutation后得到的新todo与现有todo列表合并起来,并使用cache.writeQuery将查询到的数据写回缓存。
既然我们已经指定了一个update函数,那么一旦新的todo从服务器返回,我们的用户界面就会用它进行响应性更新(广播给其他与此缓存数据有关的组件的GraphQL查询,让他们及时更新更新相关缓存到UI上)。

Apollo还提供一种的方法来及时地修改本地缓存以快速渲染UI并触发相关缓存的查询,待查询返回新的数据后再真正更新本地缓存,详见乐观更新

基于乐观UI,如果您运行相同的查询两次,则不会看到加载指示符(Apollo Client返回的loading字段)。apollo会检测当前的请求参数是否变化,然后判断是否向服务器发送新的请求。

Apollo范式化缓存 API

import gql from 'graphql-tag';
import { Mutation, Query } from "react-apollo";

const UPDATE_TODO = gql`
  mutation UpdateTodo($id: String!, $type: String!) {
    updateTodo(id: $id, type: $type) {
      id
      type
    }
  }
`;

// 注意:这里通过graphql得到的todos数据是一个包含id和type字段的对象的数组,与 UPDATE_TODO 里面的字段(主要是id)对应
const GET_TODOS = gql`
  query GetTodos {
    todos
  }
`;

const Todos = () => (
  <Query query={GET_TODOS}>
    {({ loading, error, data }) => {
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error :(</p>;

      return data.todos.map(({ id, type }) => {
        let input;

        return (
          <Mutation mutation={UPDATE_TODO} key={id}>
            {updateTodo => (
              <div>
                <p>{type}</p>
                <form
                  onSubmit={e => {
                    e.preventDefault();
                    updateTodo({ variables: { id, type: input.value } });

                    input.value = "";
                  }}
                >
                  <input
                    ref={node => {
                      input = node;
                    }}
                  />
                  <button type="submit">Update Todo</button>
                </form>
              </div>
            )}
          </Mutation>
        );
      });
    }}
  </Query>
);

注意:这一次在mutate函数(这里命名为updateTodo)里并没有调用update函数,在也没有传递update函数给Mutation组件,但是UI会立即更新。这就是范式化缓存的魅力了。

范式化缓存——InMemoryCache在将数据保存到存储之前对数据进行范式化,方法是将结果拆分为单个对象,为每个对象创建唯一标识符,并将这些对象存储在展平的数据结构中(创建的唯一标识符为这些对象的键,成为缓存键)。
默认情况下,InMemoryCache将尝试使用常见的id_id的主键作为唯一标识符(如果它们与对象上的__typename字段一起存在)。

如果未指定id_id,或者未指定__typename,则InMemoryCache将按照查询到对象的层级关系依次回退到根查询为止。

例如ROOT_QUERY.allPeople.0将作为数据中allPeople[0]对象的缓存键(cache key)被存储到缓存的根查询(ROOT_QUERY)下。(在展平的数据结构中,所有对象都在ROOT_QUERY下):

即使我们不打算在我们的UI中使用mutation返回的结果,我们仍然需要返回更新的ID和属性,以便我们的UI进行自动更新。

以上代码中,我们不需要指定update函数,因为TODOS查询将使用缓存中更新的TODO数据自动重建查询结果。

结合上一节介绍的update函数那样——并非每次mutation都需要使用update函数——其原因就是依据Apollo Cache的范式化数据结构,
在尽量减少手动操作数据的情况下自动更新UI,当前后端都规范化数据后(特别是唯一标识符id的统一, __typename字段的定义),
querymutation操作后,我们几乎不用手动处理数据,就能实现UI的自动更新。

例如:如果只需要更新缓存里面的单条数据,只需要返回这条数据的ID和要更新的属性即可,这种情况下通常不需要使用update函数。

如果想要自定义唯一标识符,即不用默认的ID来生成缓存键,可以使用InMemoryCache构造函数的dataIdFromObject函数:

const cache = new InMemoryCache({
  dataIdFromObject: object => object.key || null
});

在指定自定义dataIdFromObject时,Apollo Client不会将类型名称添加到缓存键,因此,如果您的ID在所有对象中不唯一,则可能需要在dataIdFromObject中包含__typename

在谷歌浏览器中安装apollo devtools扩展(需要科学上网),可以清晰看到这种范式化缓存的存储状态。

中场休息

使用redux管理状态,重心是放在如何去拿数据上;而apollo把重心放在需要什么数据上。理解这一点非常重要!

还记得这句话吗?我们在本文档开篇的时候介绍过。现在理解了吗?现在,我们回过头来梳理一下自己学到的知识点:

  • 当学习了怎样去获取数据(query)以及更新数据和修改数据(mutation)后,原来Apollo和React结合,原来组件可以这么简单的与数据交互!
  • 当学习了Apollo缓存后,我们对Apollo数据存储的理解又上升了一个台阶,把所有查询回来的对象一一拆分,通过唯一标识符的形式把一个深层级的对象展平,直观展现在缓存的根查询中。
  • 当学习了Apollo的范式化缓存后,我们才知道,原来自动更新UI可以如此优雅!我们甚至不需要管理数据,只需按照规范传递数据即可!

本地状态管理 详情

上半场我们接触了本地与服务端的远程数据交互,接下来,我们将进入本地的状态管理

Apollo Client在2.5版本具有内置的本地状态处理功能,允许将本地数据与远程数据一起存储在Apollo缓存中。要访问本地数据,只需使用GraphQL查询即可。

而在2.5版本之前,如果想要使用本地状态管理,必须引入已经废弃的一个包apollo-link-state(API),
这个包在2.5版本已被废弃,因为从2.5版本开始,这个包的功能已经集成到apollo的核心之中,不再额外维护一个单独的包。而在apollo的1.x版本,如果要实现本地状态管理,依然得引入redux。

Apollo Client有两种主要方法可以执行局部状态突变:

  • 第一种方法是通过调用cache.writeData直接写入缓存。
    更新缓存那一节,我们已经详细介绍过cache.writeData的用法,以及其余update函数的搭配使用。
  • 第二种方法是创建一个带有GraphQL突变(mutation)的Mutation组件,该组件调用本地客户端解析器(resolvers)。
    如果mutation依赖于缓存中的现有值,我们建议使用解析器(resolvers,后面两节将介绍,目前只需知道它的存在,它和apollo-server端的resolver完全相同)。

直接写入缓存

import React from 'react';
import { ApolloConsumer } from 'react-apollo';

import Link from './Link';

const FilterLink = ({ filter, children }) => (
  <ApolloConsumer>
    {client => (
      <Link
        onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
      >
        {children}
      </Link>
    )}
  </ApolloConsumer>
);
Apollo在ApolloConsumer组件(API)或Query组件的render prop中注入了Apollo Client实例,
所以当使用这些组件时,我们可以直接从组件的props中拿到client实例

直接写入缓存不需要GraphQLmutate函数或resolvers函数。因此我们在上面的代码中没有使用它们,我们直接在onClick事件函数里面调用client.writeData来写入缓存。

但是只建议将直接写入缓存用于简单写入,例如写入字符串或一次性写入。
重要的是要注意直接写入并不是作为GraphQL突变实现的,因此不应将它们包含在复杂的开发模式之中。
它也不会验证你写入缓存的数据是否为有效GraphQL数据的结构。
如果以上提到的任何一点对您很重要,则应选择使用本地resolvers

@client 指令

上一节提到过,Query组件的render prop同样包含client实例。所以配合@client指令,我们可以在Query组件中轻松地从cacheresolvers获取本地状态。
或许换个方式介绍大家能理解得更透彻:配合@client指令,我们可以在Query组件中轻松地从cache获取本地状态,或者通过resolverscache获取本地状态。

import React from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';

import Link from './Link';

const GET_VISIBILITY_FILTER = gql`
  {
    visibilityFilter @client
  }
`;

const FilterLink = ({ filter, children }) => (
  <Query query={GET_VISIBILITY_FILTER}>
    {({ data, client }) => (
      <Link
        onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
        active={data.visibilityFilter === filter}
      >
        {children}
      </Link>
    )}
  </Query>
);

代码解读:

@client指令告诉Apollo Client在本地获取数据(cache或resolvers),而不是将其发送到graphql服务器。
在调用client.writeData后,render prop函数上的查询结果将自动更新。同时所有缓存的写入和读取都是同步的,所以不必担心加载状态(loading)。

本地解析器——resolvers

终于见到了你——resolvers!前面几节都一笔带过了resolvers(解析器),现在,我们来了解它到底有什么强大的功能。

如果要依赖本地状态实现GraphQL的突变,我们只需要在本地resolvers对象中指定一个与之对应的函数即可。
在Apollo Client的实例化中,resolvers映射为一个对象,这个对象中保存着每一个用于本地突变的resolver函数
当在GraphQL的字段上找到@client指令时,Apollo Client会在resolvers对象中寻找与之对应的resolver函数,这个对应关系是通过resolvers的键来关联的。

即:当执行没有加@client指令的查询或突变时,GraphQL文档中的字段已预定义在了服务端,所以我们只需在查询或突变时按照服务端定义的字段编写GraphQL文档即可;
当加上@client指令后,Apollo Client不会向服务端发送请求,转而在自己内部寻找GraphQL文档内指定的字段。但是,我们怎么去访问本地的这些字段呢?或许他们根本就不存在。
(关于@client的运作方式,请参考官方文档中关于——本地数据查询流程的部分,由于篇幅原因,本文档不再详细介绍。)
这时,我们就需要自己定义可以访问这些字段的方式——resolver,在解析器对象(resolvers)中定义一个解析函数(resolver),以供GraphQL查询或突变在使用了@client指令时调用,
这样就建立了GraphQL查询或突变与Apollo Client之间的联系,通过这个函数可以解析有@client指令控制的查询或突变,因此这个函数被命名为解析函数,意指从本地解析函数中寻找GraphQL文档中指定的字段的值。

那解析函数定义在哪里呢?

其实它在ApolloClient的构造函数中,也就是说我们实例化Apollo Client时,需要传递resolvers给它。

解析器,它和client-server的resolver函数完全相同:

  fieldName: (obj, args, context, info) => result;

obj {object}: 包含父字段上resolver函数返回的结果的对象,或者为DOM树最顶层的查询或突变的ROOT_QUERY对象
args {object}: 包含传递到GraphQL文档中的所有参数的对象。例如,如果使用updateNetworkStatus(isConnected:true)触发查询或突变,则args{isConnected:true}
context {object}: React组件与Apollo Client网络堆栈之间共享的上下文信息的对象。除了可能存在的任何自定义context属性外,本地resolvers始终会收到以下内容:

  • context.client: Apollo Client的实例
  • context.cache: Apollo Cache的实例

    context.cache.readQuery, .writeQuery, .readFragment, .writeFragment, and .writeData: 一系列用于操作cache的[API](https://www.apollographql.com/docs/react/essentials/local-state/#managing-the-cache)
  • context.getCacheKey: 使用__typenameid从cache中获取key

info {object}: 有关查询执行状态的信息。实际中,你可能永远也不会使用到这个参数。

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const client = new ApolloClient({
  cache: new InMemoryCache(),
  resolvers: {
    Mutation: {
      toggleTodo: (_root, variables, { cache, getCacheKey }) => {
        const id = getCacheKey({ __typename: 'TodoItem', id: variables.id })
        const fragment = gql`
          fragment completeTodo on TodoItem {
            completed
          }
        `;
        const todo = cache.readFragment({ fragment, id });
        const data = { ...todo, completed: !todo.completed };
        cache.writeData({ id, data });
        return null;
      },
    },
  },
});

代码解析:

为了切换todo的状态,首先需要查询缓存以找出todo当前状态的内容,然后通过使用cache.readFragment从缓存中读取片段来实现此目的。
此函数采用fragment和id,它对应于item的缓存键(cache key)。我们通过调用context中的getCacheKey并传入项目的__typenameid来获取缓存键。

一旦读取了fragment,就可以切换todo的已完成状态并将更新的数据写回缓存。由于我们不打算在UI中使用mutation的返回结果,因此我们返回null,因为默认情况下所有GraphQL类型都可以为空。

下面,我们来看一下怎么调用这个toggleTodo解析函数(触发toggleTodo突变):

import React from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

const TOGGLE_TODO = gql`
  mutation ToggleTodo($id: Int!) {
    toggleTodo(id: $id) @client
  }
`;

const Todo = ({ id, completed, text }) => (
  <Mutation mutation={TOGGLE_TODO} variables={{ id }}>
    // 特别注意,此toggleTodo非解析器里面的toggleTodo,这个toggleTodo是我们之前介绍过的mutate函数,这里被更名为‘toggleTodo’而已,不要混淆了
    {toggleTodo => (
      <li
        onClick={toggleTodo}
        style={{
          textDecoration: completed ? 'line-through' : 'none',
        }}
      >
        {text}
      </li>
    )}
  </Mutation>
);

代码解析:

首先,我们创建一个GraphQL突变文档,它将我们想要切换的item的id作为唯一的参数。我们通过使用@client指令标记GraphQLtoggleTodo字段来指示这是一个本地突变。
这将告诉Apollo Client调用我们本地突变解析器(resolvers)里面的toggleTodo解析函数来解析该字段。然后,我们创建一个Mutation组件,就像我们操作远程突变一样。
最后,将GraphQL突变传递给组件,并在render prop函数的UI中触发它。

查询本地状态

查询本地数据与查询GraphQL服务器非常相似。唯一的区别是本地查询在字段上添加了@client指令,以指示它们应该从Apollo Client cache或resolvers中解析。

我们来看一个例子:

import React from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';

import Todo from './Todo';

const GET_TODOS = gql`
  {
    todos @client {
      id
      completed
      text
    }
    visibilityFilter @client
  }
`;

const TodoList = () => (
  <Query query={GET_TODOS}>
    {
      ({ data: { todos, visibilityFilter } }) => (
        <ul>
          {
            getVisibleTodos(todos, visibilityFilter).map(todo => (
              <Todo key={todo.id} {...todo} />
            ))
          }
        </ul>
      )
    }
  </Query>
);

代码解析:

创建GraphQL查询并将@client指令添加到GraphQL文档的todos和visibilityFilter字段。
然后,我们将查询传递给Query组件@client指令让Query组件知道应该从Apollo Client缓存中提取todos和visibilityFilter,或者使用预定义的本地resolver解析。

由于上面的查询在安装组件后立即运行,如果cache中没有item或者没有定义任何本地resolver,我们该怎么办?

我们需要在运行查询之前将初始状态写入缓存,以防止错误输出。

初始化本地状态

通常,我们需要将初始状态写入缓存,以便在触发mutation之前查询数据的所有组件都不会出错。
要实现此目的,可以使用cache.writeData为初始值准备缓存。

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: { /* ... */ },
});

cache.writeData({
  data: {
    todos: [],
    visibilityFilter: 'SHOW_ALL',
    networkStatus: {
      __typename: 'NetworkStatus',
      isConnected: false,
    },
  },
});

注意:Apollo v2.4和v2.5写入初始本地状态的方式不一样,详情参考官方API

重置本地状态/缓存

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: { /* ... */ },
});

const data = {
  todos: [],
  visibilityFilter: 'SHOW_ALL',
  networkStatus: {
    __typename: 'NetworkStatus',
    isConnected: false,
  },
};

cache.writeData({ data });

client.onResetStore(() => cache.writeData({ data }));

使用client.onResetStore方法可以重置缓存。

同时请求本地状态和远程数据

mutation ToggleTodo($id: Int!) {
  toggleTodo(id: $id) @client
  getData(id: $id) {
    id,
    name
  }
}

只需在需要从本地查询的字段后面加上@client指令即可。

使用@client字段作为变量

在同一个graphql语句中,还可以将从本地查到的状态用于下一个查询,通过@export指令

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';

const query = gql`
  query currentAuthorPostCount($authorId: Int!) {
    currentAuthorId @client @export(as: "authorId")
    postCount(authorId: $authorId)
  }
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
  cache,
  resolvers: {},
});

cache.writeData({
  data: {
    currentAuthorId: 12345,
  },
});

// ... run the query using client.query, the <Query /> component, etc.

在上面的示例中,currentAuthorId首先从缓存加载,然后作为authorId变量(由@export(as:“authorId”)指令指定)传递到后续postCount字段中。
@export指令也可用于选择集中的特定字段,如:

@export指令还可以用于选择集中的特定字段

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';

const query = gql`
  query currentAuthorPostCount($authorId: Int!) {
    currentAuthor @client {
      name
      authorId @export(as: "authorId")
    }
    postCount(authorId: $authorId)
  }
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
  cache,
  resolvers: {},
});

cache.writeData({
  data: {
    currentAuthor: {
      __typename: 'Author',
      name: 'John Smith',
      authorId: 12345,
    },
  },
});

// ... run the query using client.query, the <Query /> component, etc.

@export指令使用不仅限于远程查询;它还可以用于为其他@client字段或选择集定义变量:(注意以下代码中GraphQL文档的currentAuthorIdpostCount字段之后都有@client指令)

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';

const query = gql`
  query currentAuthorPostCount($authorId: Int!) {
    currentAuthorId @client @export(as: "authorId")
    postCount(authorId: $authorId) @client
  }
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: {
    Query: {
      postCount(_, { authorId }) {
        return authorId === 12345 ? 100 : 0;
      },
    },
  },
});

cache.writeData({
  data: {
    currentAuthorId: 12345,
  },
});

// ... run the query using client.query, the <Query /> component, etc.

动态注入resolver

有时,当我们在APP中使用了代码拆分,如使用react-loadable时,我们并不是很希望所有的resolver都在初始化Apollo客户端的统一写在一起,而是希望单独拆分到各自的模块中,这样在APP编译后,
每个模块各自resolver将包含在自己的包中,这样也有助于减少入口文件的大小,使用addResolverssetResolvers即可办到(API),
例如以下代码:

import Loadable from 'react-loadable';

import Loading from './components/Loading';

export const Stats = Loadable({
  loader: () => import('./components/stats/Stats'),
  loading: Loading,
});
import React from 'react';
import { ApolloConsumer, Query } from 'react-apollo';
import gql from 'graphql-tag';

const GET_MESSAGE_COUNT = gql`
  {
    messageCount @client {
      total
    }
  }
`;

const resolvers = {
  Query: {
    messageCount: (_, args, { cache }) => {
      // ... calculate and return the number of messages in
      // the cache ...
      return {
        total: 123,
        __typename: 'MessageCount',
      };
    },
  },
};

const MessageCount = () => {
  return (
    <ApolloConsumer>
      {(client) => {
        client.addResolvers(resolvers);
        return (
          <Query query={GET_MESSAGE_COUNT}>
            {({ loading, data: { messageCount } }) => {
              if (loading) return 'Loading ...';
              return (
                <p>
                  Total number of messages: {messageCount.total}
                </p>
              );
            }}
          </Query>
        );
      }}
    </ApolloConsumer>
  );
};

export default MessageCount;

脱离React标签的写法

由于编程习惯的不同,有些人(比如我),并不是很喜欢(或者说成习惯)把逻辑代码React标签混合写在一起,就如我们从本文档开始一路看来所有的示例代码那样!
个人觉得在一个大型的项目中把Query标签Mutation标签以及其他的各种标签层层嵌套,再加上各种逻辑实现代码,全部挤在React组件中,真的是一件糟糕的事情。
虽然官方推崇这种写法(以上绝大部分代码是官方文档的示例),他们给出的理由是这样写更方便,更简单!

因人而异吧!

我个人更倾向于把GraphQL和Apollo的逻辑部分React组件分离开,我们可以使用react-apollo库提供的graphqlcompose方法做到分离。
当然,在react-apollo这个包中并不止这两个方法,还有其他的方法,请参考React Apollo API

下面展示一段分离开的代码示例:

import { Avatar, Card, Icon } from 'antd'
import gql from 'graphql-tag'
import * as React from 'react'
import { useEffect, useRef } from 'react'
import { compose, graphql } from 'react-apollo'
import { QueryState, Typename } from 'src/config/clientState'
import { GET_COMPONENTS_LIST, GET_QUERY_STATE } from 'src/graphql'
import { getCorrectQueryState } from 'src/util'
import Pagination from '../pagination'
import './index.scss'

const { Meta } = Card

type Type = {
  id: number,
  name: string
}

type Author = {
  username: string,
  email: string,
  avatar: string
}

type Component = {
  id: number,
  name: string,
  version: string,
  chineseName: string,
  description: string,
  type: Type,
  url: string,
  author: Author,
  previewUrl: string,
  isOwn: boolean,
  isStored: boolean
}

type ListComponent = {
  data: {
    components: Component[],
    compCount: number,
  },
  componentsCollect: ([id, isCollect]: [number, boolean]) => void,
  queryState: QueryState
}

const ListComponent = (props: ListComponent) => {
  const cardList: React.MutableRefObject<HTMLDivElement | null> = useRef(null)
  const { data, componentsCollect, queryState } = props

  // 加载时组件卡片的动画效果,配合React的ref和key属性使用
  useEffect(() => {
    if(cardList.current) {
      cardList.current.childNodes.forEach((element: HTMLElement, index: number) => {
        setInterval(() => {
          element.classList.add('card-load-anim')
        }, index * 40)
      })
    }
  })

  return (
    <div className='m-list'>
      <div className='m-cards' ref={cardList} key={Math.random()}>
        {
          !data || !data.components
            ? null
            : data.components.map((o: Components, i: number) => {
              return (
                <Card
                  key={`m-list-btn-${i}`}
                  hoverable={true}
                  cover={<img alt='example' data-original='https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png' />}
                  actions={
                    [
                      <a
                        className={`${o.isOwn ? 'm-list-btn-text-disabled' : 'm-list-btn-text'}`}
                        key={`m-list-btn-${i}-1`}
                        href='javascript:void(0)'
                        onClick={!o.isOwn ? componentsCollect.bind(null, [o.id, !o.isStored]) : null}
                      >
                        <Icon type='copy' />
                        {o.isStored ? '取消收藏' : '收藏'}
                      </a>,
                      <a
                        className='m-list-btn-text'
                        key='m-list-btn-2'
                        href='javascript:void(0)'
                      >
                        <Icon type='file-search' />
                        文档
                      </a>
                    ]
                  }
                >
                  <Meta
                    avatar={<Avatar data-original='https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' />}
                    title={o.name}
                    description={o.description}
                  />
                </Card>
              )
            })
        }
      </div>
      <Pagination totalPages={Math.ceil(data.compCount / queryState.pagination.size)} total={data.compCount} />
    </div>
  )
}

export default compose(
  graphql(
    gql(GET_QUERY_STATE),
    {
      props: ({ data: { queryState } }: any) => ({ queryState })
    }
  ),
  graphql(
    gql(GET_COMPONENTS_LIST),
    {
      options: ({ queryState }: any) => ({
        variables: {
          ...getCorrectQueryState(queryState)
        }
      })
    }
  ),
  graphql(gql`
    mutation ($id: Int!, $isCollect: Boolean!){
      storeComponent(id: $id, isStore: $isCollect)
    }
  `, {
    props: ({ mutate }: any) => ({
      componentsCollect: ([id, isCollect]: [number, boolean]) => {
        debugger
        mutate({
          variables: { id, isCollect },
          optimisticResponse: {
            __typename: Typename.Mutation,
            storeComponent: {
              __typename: Typename.Component,
              id,
              isStored: isCollect
            }
          }
        })
      }
    })
  })
)(ListComponent)

写在最后:

如果认真学习完这篇文档,相信你对Apollo技术栈开发React应用已经算是入门了,今后开发时遇到问题,多看一看官方文档,相信你会很快掌握它。

由于水平有限,这篇文章是我自己一边翻译一边加入自己的理解而写成的,其中肯定少不了一些不妥或错误的地方,欢迎大家指正!

查看原文

赞 17 收藏 11 评论 2

上帝何解 赞了文章 · 2019-08-16

Nginx常见配置

二话不说,直接进入配置主题,若对nginx毫无了解的请跳转Nginx入门到实战(1)基础篇

在此之前,先把配置参数所在位置分为四层

1. conf 全局层
2. http 服务器层
3. server 虚拟主机层
4. location 定位层

一、全局层

#有1个工作的子进程,可以自行修改,但太大无益,因为要争夺CPU,一般设置为 CPU数*核数
worker_processes 1; 

#一般是配置nginx连接的特性,如1个子进程能同时允许多少连接
Event {
    
    #这是指一个子进程最大允许连1024个连接
    worker_connections  1024;
}

#这是配置http服务器的主要段
http { 
     
     #这是虚拟主机段
     Server1 {
      
            #定位,把特殊的路径或文件再次定位,如image目录单独处理,如.php单独处理
            Location {  
            ...    
            }
     }

     Server2 {
         ...
     }
}

二、HTTP服务器层

1. 网页内容的压缩编码与传输速度优化

请求:
Accept-Encoding:gzip,deflate,sdch
响应:
Content-Encoding:gzip
Content-Length:36093

原理:
浏览器---请求----> 声明可以接受 gzip压缩 或 deflate压缩 或compress 或 sdch压缩。

从http协议的角度看--请求头 声明 acceopt-encoding: gzip deflate sdch (是指压缩算法,其中sdch是google倡导的一种压缩方式,目前支持的服务器尚不多)

服务器-->回应---把内容用gzip方式压缩---->发给浏览器
浏览<-----解码gzip-----接收gzip压缩内容

gzip    #配置的常用参数
gzip on|off;    #是否开启gzip
gzip_buffers 32 4K | 16 8K   #缓冲(压缩在内存中缓冲几块? 每块多大?)
gzip_comp_level  #[1-9] #推荐6 压缩级别(级别越高,压的越小,越浪费CPU计算资源)
gzip_disable    #正则匹配UA 什么样的Uri不进行gzip
gzip_min_length 200     #开始压缩的最小长度(再小就不要压缩了,意义不在)
gzip_http_version 1.0|1.1   # 开始压缩的http协议版本(可以不设置,目前几乎全是1.1协议)
gzip_proxied          #设置请求者代理服务器,该如何缓存内容
gzip_types text/plain  application/xml  #对哪些类型的文件用压缩如txt,xml,html ,css ,若不知道类型名称,可以查看nginx下conf文件夹的mime.types
gzip_vary on|off    #是否传输gzip压缩标志

注意:
图片/mp3这样的二进制文件,不必压缩因为压缩率比较小,比如100->80字节,而且压缩也是耗费CPU资源的。比较小的文件不必压缩。

2. 缓存设置

对于网站的图片,尤其是新闻站,图片一旦发布,改动的可能是非常小的。我们希望 能否在用户访问一次后,图片缓存在用户的浏览器端,且时间比较长的缓存。
可以, 用到nginx的expires设置。

#在location或if段里
    expires 30s;
    expires 30m;
    expires 2h;
    expires 30d;
    
location ~ image {
    expires 1d;
}

另: 304 也是一种很好的缓存手段

原理是: 服务器响应文件内容是,同时响应etag标签(内容的签名,内容一变,他也变), 和 last_modified_since 2个标签值。浏览器下次去请求时,头信息发送这两个标签, 服务器检测文件有没有发生变化,如无,直接头信息返回 etag,last_modified_since
浏览器知道内容无改变,于是直接调用本地缓存。
这个过程,也请求了服务器,但是传着的内容极少。
对于变化周期较短的,如静态html,js,css,,比较适于用这个方式

三、虚拟主机层

#基于域名的虚拟主机
server {
    listen 80;  #监听端口
    server_name a.com; #监听域名

    location / {
            root /var/www/a.com;   #根目录定位
            index index.html;
    }
}

#基于端口的虚拟主机配置 访问 192.xxx.xx.xxx:8080
server {
    listen 8080;    #监听8080端口
    server_name 192.xxx.xx.xxx;    #服务器IP地址

    location / {
            root /var/www/html8080;
            index index.html;
    }
}

1. 日志管理

#不同的server可以使用不同的log
#此处定义了日志格式,最好定位在顶层,方便其他server公用
log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

$remote_addr                        用户ip
$remote_user [$time_local]          用户访问时间
$request                            请求类型 get,post...
$status                             请求状态 200 304...
$body_bytes_sent                    请求的内容有多少字节
$http_referer                       上一个页面来自哪里(从哪里跳转过来)
$http_user_agent                    用户代理(用了什么浏览器访问)



#这说明 该server, 它的访问日志的文件,使用的格式main格式.
access_log  logs/host.access.log  main;

写一个sh脚本,每天半夜切分log日志,避免log每天累积造成文件过大

#!/bin/bash
base_path='/usr/local/nginx/logs'
log_path=$(date -d yesterday +"%Y%m")
day=$(date -d yesterday +"%d")
mkdir -p $base_path/$log_path
mv $base_path/access.log $base_path/$log_path/access_$day.log
#echo $base_path/$log_path/access_$day.log
kill -USR1 `cat /usr/local/nginx/logs/nginx.pid`

#Crontab 编辑定时任务
01 00 * * * /xxx/path/b.sh  每天0时1分(建议在02-04点之间,系统负载小)

四、定位层

1. location定位

location 有“定位”的意思,根据Uri来进行不同的定位,在虚拟主机的配置中,是必不可少的。location可以把网站的不同部分,定位到不同的处理方式上。

当我们碰到“.php”, 如何调用PHP解释器? --这时就需要location

#location 的语法
location [=|~|~*|^~] patt {
    ...
}
#中括号可以不写任何参数,此时称为一般匹配
#也可以写参数
#因此,大类型可以分为3种
#首先看有没有精准匹配,如果有,则停止匹配过程,若没有向下匹配到最符合的location
location = patt {} #[精准匹配]
location patt{}  #[一般匹配]
location ~ patt{} #[正则匹配]

匹配顺序实例1

location / {
            root   /usr/local/nginx/html;
            index  index.html index.htm;
        }

location ~ image {
           root /var/www/image;
           index index.html;
}
#如果我们访问  http://xx.com/image/logo.png
#此时, “/” 与”/image/logo.png” 匹配
#同时,”image”正则 与”image/logo.png”也能匹配,谁发挥作用?
#正则表达式的成果将会使用.
#图片真正会访问 /var/www/image/logo.png 
#注意,若在roo最后加了'/',那么将访问/var/www/image/image/logo.png 

匹配顺序实例2

location / {
             root   /usr/local/nginx/html;
             index  index.html index.htm;
         }
 
location /foo {
            root /var/www/html;
             index index.html;
}
#我们访问 http://xxx.com/foo
#对于uri “/foo”,   两个location的patt,都能匹配他们
#即 ‘/’能从左前缀匹配 ‘/foo’, ‘/foo’也能左前缀匹配’/foo’,
#此时, 真正访问 /var/www/html/index.html 
#原因:’/foo’匹配的更长,因此使用之

总结:

  1. 先判断精准命中,如果命中,立即返回结果并结束解析过程
  2. 判断普通命中,如果有多个命中,记录下最长的命中结果为准(记录但不结束)
  3. 继续判断正则表达式的解析结果,按配置里的正则表达式顺序为准,由上往下,一旦匹配成功1个,立即返回结果,并结束解析

分析:

  1. 普通命中顺序无所谓,按命中长短来决定
  2. 正则命中,按顺序

2. rewrite 重写

#重写中用到的指令
if  (条件) {}  #设定条件,再进行重写 
set #设置变量
return #返回状态码 
break #跳出rewrite
rewrite #重写

#If  语法格式
If 空格 (条件) {
    重写模式
}

#条件语法
1: “=”来判断相等, 用于字符串比较
2: “~” 用正则来匹配(此处的正则区分大小写)
   ~* 不区分大小写的正则
3: -f -d -e来判断是否为文件,为目录,是否存在.

例子

location / {
    #当访问ip相等时,返回403
    if  ($remote_addr = 192.xxx.xx.xx) { 
        return 403;
    }
    
    #如果是IE浏览器访问 
    if ($http_user_agent ~ MSIE) {
        rewrite ^.*$ /ie.htm;
        break; #若不brea,重定向后又会匹配到IE浏览器,又走到这一步,会循环重定向
    }
    
    #若访问目录、文件不存在,重定向到404页面
    if (!-e $document_root$fastcgi_script_name) {
        rewrite ^.*$ /404.html break;
    } 
    root html;
    index index.html
}

xx.com/dsafsd.html这个不存在页面为例,我们观察访问日志,日志中显示的访问路径,依然是GET /dsafsd.html HTTP/1.1。提示:服务器内部的rewrite和302跳转不一样。
跳转的话URL都变了,变成重新http请求404.html,而内部rewrite, 上下文没变,
就是说 fastcgi_script_name 仍然是 dsafsd.html,因此会循环重定向。

set 是设置变量用的, 可以用来达到多条件判断时作标志用,达到apache下的 rewrite_condition的效果

#使用set方式防止重定向死循环
if ($http_user_agent ~* msie) {
    set $isie 1;
}

if ($fastcgi_script_name = ie.html) {
    set $isie 0;
}

if ($isie 1) {
    rewrite ^.*$ ie.html;
}

3. Nginx与PHP配合

#当碰到访问 .php 的时候时候
location ~ \.php$ {
    root html;
    #把请求的信息转发给9000端口的PHP进程
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    #告诉php进程想运行哪个php文件 
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
}

#1: 碰到php文件,
#2: 把根目录定位到 html,
#3: 把请求上下文转交给9000端口PHP进程,
#4: 并告诉PHP进程,当前的脚本是 $document_root$fastcgi_scriptname
# (注:PHP会去找这个脚本并处理,所以脚本的位置要指对)

4. 反向代理 + 负载均衡

用nginx做反向代理和负载均衡非常简单。

只需要两个配置, 1个proxy, 1个upstream,分别用来做反向代理,和负载均衡。

以反向代理为例,nginx不自己处理php的相关请求,而是把php的相关请求转发给apache来处理。

#将php程序交给8080端口的apache处理,实现动静分离
location ~ \.php$ {
    proxy_pass  http://xxx.xxx.xx:8080 
}
http {
    ...
    #负载均衡服务器池
    upstream xxx {
        server 127.xx.xx.xx1;
        server 127.xx.xx.xx2;
    }
    server {
        liseten 80;
        server_name localhost;
        location / {
            #用户真实IP
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://xxx     #upstream 对应自定义名称
            include proxy.conf;  
        }
    }
}

有收获的朋友记得点个 收藏 哦~

查看原文

赞 33 收藏 73 评论 0

上帝何解 赞了文章 · 2019-08-14

PHP 运行模式

SAPI

这里所说的 PHP 运行模式, 其实指的是 SAPI (Server Application Programming Interface,服务端应用编程端口 )。SAPI 为 PHP 提供了一个和外部通信的接口, PHP 就是通过这个接口来与其它的应用进行数据交互的。针对不同的应用场景, PHP 也提供了多种不同的 SAPI ,常见的有:apache、apache2filter、apache2handler、cli、cgi、embed 、fast-cgi、isapi 等等。

图片描述

php_sapi_name() — 返回 web 服务器和 PHP 之间的接口类型。可能返回的值包括了 aolserver、apache、 apache2filter、apache2handler、 caudium、cgi (直到 PHP 5.3), cgi-fcgi、cli、 cli-server、 continuity、embed、fpm-fcgi、 isapi、litespeed、 milter、nsapi、 phttpd、pi3web、roxen、 thttpd、tux 和 webjames。

目前 PHP 内置的很多 SAPI 实现都已不再维护或者变的有些非主流了,PHP 社区目前正在考虑将一些 SAPI 移出代码库。 社区对很多功能的考虑是除非真的非常必要,或者某些功能已近非常通用了,否则就在 PECL 库中。

接下来会对其中五个比较常见的运行模式进行说明。

CLI 模式

CLI( Command Line Interface ), 也就是命令行接口,PHP 默认会安装。通过这个接口,可以在 shell 环境下与 PHP 进行交互 。在终端里输入 php -v,会得到类似下图的结果(安装了 PHP 前提下):

图片描述

因为有 CLI 的存在,我们可以直接在终端命令行里运行 PHP 脚本,就像使用 shell、Python 那样,不用依赖于 WEB 服务器。比如 Laravel 框架中的 Artisan 命令行工具,它其实就是一个 PHP 脚本,用来帮助我们快速构建 Laravel 应用的。

CGI 模式

CGI(Common Gateway Interface,通用网关接口)是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI 描述了服务器和请求处理程序之间传输数据的一种标准。

WEB 服务器只是内容的分发者。比如 Nginx,如果客户端请求了 /index.html,那么 Nginx 会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据;如果客户端现在请求的是 /index.php,根据配置文件,Nginx 知道这个不是静态文件,需要去找 PHP 解析器来处理,那么它会把这个请求经过简单处理后交给PHP 解析器。Nginx 会传哪些数据给 PHP 解析器呢?url 要有吧,查询字符串也得有吧,POST 数据也要有,HTTP 请求头 不能少吧,好的,CGI 就是规定要传哪些数据、以什么样的格式传递给后方处理这个请求的协议。

CGI 模式运行原理:当 Nginx 收到浏览器 /index.php 这个请求后,首先会创建一个对应实现了 CGI 协议的进程,这里就是 php-cgi(PHP 解析器)。接下来 php-cgi 会解析 php.ini 文件,初始化执行环境,然后处理请求,再以 CGI 规定的格式返回处理后的结果,退出进程。最后,Nginx 再把结果返回给浏览器。整个流程就是一个 Fork-And-Execute 模式。当用户请求数量非常多时,会大量挤占系统的资源如内存、CPU 时间等,造成效能低下。所以在用 CGI 方式的服务器下,有多少个连接请求就会有多少个 CGI 子进程,子进程反复加载是 CGI 性能低下的主要原因。

图片描述

CGI 模式的好处就是完全独立于任何服务器,仅仅是做为一个中介:提供接口给 WEB 服务器和脚本语言或者是完全独立编程语言。它们通过 CGI 协议搭线来完成数据传递。这样做的好处了尽量减少它们之间的关联,使得各自更加独立、互不影响。

CGI 模式已经是比较古老的模式了,这几年都很少用了。

FastCGI 模式

FastCGI(Fast Common Gateway Interface,快速通用网关接口)是一种让交互程序与 Web 服务器通信的协议。FastCGI 是早期通用网关接口(CGI)的增强版本。FastCGI 致力于减少网页服务器与 CGI 程序之间交互的开销,从而使服务器可以同时处理更多的网页请求。

根据定义可以知道,FastCGI 也是一种协议,实现了 FastCGI 协议的程序,更像是一个常驻型(long-live)的 CGI 协议程序,只要激活后,它可以一直执行着,不会每次都要花费时间去 fork 一次。

FastCGI 模式运行原理:FastCGI 进程管理器启动之后,首先会解析 php.ini 文件,初始化执行环境,然后会启动多个 CGI 协议解释器守护进程 (进程管理中可以看到多个 php-cig 或 php-cgi.exe),并等待来自 WEB 服务器的连接;当客户端请求到达 WEB 服务器时,FastCGI 进程管理器会选择并连接到一个 CGI 解释器, WEB 服务器将 CGI环境变量和标准输入发送到 FastCGI 的子进程 php-cgi 中; php-cgi 子进程完成处理后便将标准输出和错误信息返回给 WEB 服务器;此时 php-cgi 子进程就会关闭连接,该请求便处理结束,接着继续等待并处理来自 FastCGI 进程管理器的下一个请求连接。

图片描述

FastCGI 模式采用了 C/S 结构,可以将 WEB 服务器和脚本解析服务器分开,同时在脚本解析服务器上启动一个或者多个脚本解析守护进程。当 WEB 服务器每次遇到动态程序时,可以将其直接交付给 FastCGI 进程来执行,然后将得到的结果返回给浏览器。这种方式可以让 WEB 服务器专一地处理静态请求或者将动态脚本服务器的结果返回给客户端,这在很大程度上提高了整个应用系统的性能。

另外,在 CGI 模式下,php-cgi 在 php.ini 配置变更后,需要重启 php-cgi 进程才能让新的 php-ini 配置生效,不可以平滑重启。而在 FastCGI 模式下,PHP-FPM 可以通过生成新的子进程来实现 php.ini 修改后的平滑重启。

PHP-FPM(PHP-FastCGI Process Manager)是 PHP 语言中实现了 FastCGI 协议的进程管理器,由 Andrei Nigmatulin 编写实现,已被 PHP 官方收录并集成到内核中。

FastCGI 模式的优点:

  1. 从稳定性上看,FastCGI 模式是以独立的进程池来运行 CGI 协议程序,单独一个进程死掉,系统可以很轻易的丢弃,然后重新分配新的进程来运行逻辑;
  2. 从安全性上看,FastCGI 模式支持分布式运算。FastCGI 程序和宿主的 Server 完全独立,FastCGI 程序挂了也不影响 Server;
  3. 从性能上看,FastCGI 模式把动态逻辑的处理从 Server 中分离出来,大负荷的 IO 处理还是留给宿主 Server,这样宿主 Server 可以一心一意处理 IO,对于一个普通的动态网页来说, 逻辑处理可能只有一小部分,大量的是图片等静态。

FastCGI 模式是目前 PHP 主流的 WEB 服务运行模式,拥有高效可靠的性能,推荐大家使用。

Module 模式

PHP 常常与 Apache 服务器搭配形成 LAMP 配套的运行环境。把 PHP 作为一个子模块集成到 Apache 中,就是 Module 模式,Apache 中的常见配置如下:

LoadModule php5_module modules/mod_php5.so

这使用了 LoadModule 命令,该命令的第一个参数是模块的名称,名称可以在模块实现的源码中找到。第二个选项是该模块所处的路径。如果需要在服务器运行时加载模块,可以通过发送信号 HUP 或者 AP_SIG_GRACEFUL 给服务器,一旦接受到该信号,Apache 将重新装载模块,而不需要重新启动服务器。通过注册到 apache2 的 ap_hook_post_config 挂钩,在 Apache 启动的时候启动此模块以接受 PHP 文件的请求。

例如,当客户端访问 PHP 文件时,Apache 就会调用 php5_module 来解析 PHP 脚本。Apache 每接收到一个请求,都会产生一个进程来连接 PHP 完成请求。在 Module 模式下,有时候会因为把 PHP 作为模块编进 Apache,而导致出现问题时很难定位是 PHP 的问题还是 Apache 的问题。

过去,凭借着丰富的模块和功能,企业往往将 Apache 作为 WEB 服务器,于是以 Module 模式运行的 PHP + Apache 的组合很常见。近些年,以异步事件驱动、高性能的 Nginx 服务器的崛起,市场份额快速增长,以 FastCGI 模式运行的 PHP + Nginx 组合,拥有更佳的性能,有赶超 Apache 的趋势。

ISAPI 模式

ISAPI(Internet Server Application Program Interface)是微软提供的一套面向 Internet 服务的 API 接口,一个 ISAPI 的 DLL,可以在被用户请求激活后长驻内存,等待用户的另一个请求,还可以在一个 DLL 里设置多个用户请求处理函数,此外,ISAPI 的 DLL 应用程序和 WEB 服务器处于同一个进程中,效率要显著高于 CGI。由于微软的排他性,只能运行于 Windows 环境。

用的比较少,在这里就不做详细介绍了。

以上内容整理自网络,参考文章:

  1. SAPI概述
  2. PHP的运行模式
  3. PHP 运行模式
  4. CGI、FastCGI和PHP-FPM关系图解
  5. 搞不清FastCgi与PHP-fpm之间是个什么样的关系
查看原文

赞 24 收藏 49 评论 2

上帝何解 赞了文章 · 2019-06-03

Shiro用starter方式优雅整合到SpringBoot中

Shiro用starter方式优雅整合到SpringBoot中

网上找到大部分文章都是以前SpringMVC下的整合方式,很多人都不知道shiro提供了官方的starter可以方便地跟SpringBoot整合。本文介绍我的3种整合思路:1.完全使用注解;2.完全使用url配置;3.url配置和注解混用,url配置负责鉴权控制,注解负责权限控制。三种方式各有优劣,需考虑实际应用场景使用。

代码

Talk is cheap, show you my code: elegant-shiro-boot
这个工程使用gradle构建,有三个子工程:

  • demo1演示只用注解来做鉴权授权
  • demo2演示只用url配置来做鉴权授权
  • demo3演示两种方式结合,url配置负责控制鉴权,注解配置负责控制授权。

如何整合

请看shiro官网关于springboot整合shiro的链接:Integrating Apache Shiro into Spring-Boot Applications

可笑的是,我自己直接上去官网找,找来找去都找不到这一页的文档,而是通过google找出来的。

这篇文档的介绍也相当简单。我们只需要按照文档说明,引入shiro-spring-boot-starter,然后在spring容器中注入一个我们自定义的Realm,shiro通过这个realm就可以知道如何获取用户信息来处理鉴权(Authentication),如何获取用户角色、权限信息来处理授权(Authorization)

ps:鉴权可以理解成判断一个用户是否已登录的过程,授权可以理解成判断一个已登录用户是否有访问权限的过程。

整合过程:
1.引入starter,我的是用gradle做项目构建的,maven也是引入对应的依赖即可:

dependencies {
    //spring boot的starter
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.boot:spring-boot-starter-aop'
    compile 'org.springframework.boot:spring-boot-devtools'
    testCompile 'org.springframework.boot:spring-boot-starter-test'
    //shiro
    compile 'org.apache.shiro:shiro-spring-boot-web-starter:1.4.0'
}

2.编写自定义realm

User.java(其它RBAC模型请看github上的代码com.abc.entity包下的类)

public class User {

    private Long uid;       // 用户id
    private String uname;   // 登录名,不可改
    private String nick;    // 用户昵称,可改
    private String pwd;     // 已加密的登录密码
    private String salt;    // 加密盐值
    private Date created;   // 创建时间
    private Date updated;   // 修改时间
    private Set<String> roles = new HashSet<>();    //用户所有角色值,用于shiro做角色权限的判断
    private Set<String> perms = new HashSet<>();    //用户所有权限值,用于shiro做资源权限的判断
    //getters and setters...
}

UserService.java

@Service
public class UserService {

    /**
     * 模拟查询返回用户信息
     * @param uname
     * @return
     */
    public User findUserByName(String uname){
        User user = new User();
        user.setUname(uname);
        user.setNick(uname+"NICK");
        user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");//密码明文是123456
        user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");//加密密码的盐值
        user.setUid(new Random().nextLong());//随机分配一个id
        user.setCreated(new Date());
        return user;
    }
}

RoleService.java

@Service
public class RoleService {

    /**
     * 模拟根据用户id查询返回用户的所有角色,实际查询语句参考:
     * SELECT r.rval FROM role r, user_role ur
     * WHERE r.rid = ur.role_id AND ur.user_id = #{userId}
     * @param uid
     * @return
     */
    public Set<String> getRolesByUserId(Long uid){
        Set<String> roles = new HashSet<>();
        //三种编程语言代表三种角色:js程序员、java程序员、c++程序员
        roles.add("js");
        roles.add("java");
        roles.add("cpp");
        return roles;
    }

}

PermService.java

@Service
public class PermService {

    /**
     * 模拟根据用户id查询返回用户的所有权限,实际查询语句参考:
     * SELECT p.pval FROM perm p, role_perm rp, user_role ur
     * WHERE p.pid = rp.perm_id AND ur.role_id = rp.role_id
     * AND ur.user_id = #{userId}
     * @param uid
     * @return
     */
    public Set<String> getPermsByUserId(Long uid){
        Set<String> perms = new HashSet<>();
        //三种编程语言代表三种角色:js程序员、java程序员、c++程序员
        //js程序员的权限
        perms.add("html:edit");
        //c++程序员的权限
        perms.add("hardware:debug");
        //java程序员的权限
        perms.add("mvn:install");
        perms.add("mvn:clean");
        perms.add("mvn:test");
        return perms;
    }

}

CustomRealm.java


/**
 * 这个类是参照JDBCRealm写的,主要是自定义了如何查询用户信息,如何查询用户的角色和权限,如何校验密码等逻辑
 */
public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermService permService;

    //告诉shiro如何根据获取到的用户信息中的密码和盐值来校验密码
    {
        //设置用于匹配密码的CredentialsMatcher
        HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
        hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        hashMatcher.setStoredCredentialsHexEncoded(false);
        hashMatcher.setHashIterations(1024);
        this.setCredentialsMatcher(hashMatcher);
    }


    //定义如何获取用户的角色和权限的逻辑,给shiro做权限判断
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        User user = (User) getAvailablePrincipal(principals);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        System.out.println("获取角色信息:"+user.getRoles());
        System.out.println("获取权限信息:"+user.getPerms());
        info.setRoles(user.getRoles());
        info.setStringPermissions(user.getPerms());
        return info;
    }

    //定义如何获取用户信息的业务逻辑,给shiro做登录
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();

        // Null username is invalid
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }

        User userDB = userService.findUserByName(username);


        if (userDB == null) {
            throw new UnknownAccountException("No account found for admin [" + username + "]");
        }

        //查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方
        //SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息,包括角色和权限
        Set<String> roles = roleService.getRolesByUserId(userDB.getUid());
        Set<String> perms = permService.getPermsByUserId(userDB.getUid());
        userDB.getRoles().addAll(roles);
        userDB.getPerms().addAll(perms);

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());
        if (userDB.getSalt() != null) {
            info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt()));
        }

        return info;

    }

}

3.使用注解或url配置,来控制鉴权授权

请参照官网的示例:

//url配置
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
    
    // logged in users with the 'admin' role
    chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]");
    
    // logged in users with the 'document:read' permission
    chainDefinition.addPathDefinition("/docs/**", "authc, perms[document:read]");
    
    // all other paths require a logged in user
    chainDefinition.addPathDefinition("/**", "authc");
    return chainDefinition;
}
//注解配置
@RequiresPermissions("document:read")
public void readDocument() {
    ...
}

4.解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring aop的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射,需加入以下配置:

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
         * 加入这项配置能解决这个bug
         */
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }

思路1:只用注解控制鉴权授权

使用注解的优点是控制的粒度细,并且非常适合用来做基于资源的权限控制。

关于基于资源的权限控制,建议看看这篇文章:The New RBAC: Resource-Based Access Control

只用注解的话非常简单。我们只需要使用url配置配置一下所以请求路径都可以匿名访问:

    //在 ShiroConfig.java 中的代码:
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
        // 由于demo1展示统一使用注解做访问控制,所以这里配置所有请求路径都可以匿名访问
        chain.addPathDefinition("/**", "anon"); // all paths are managed via annotations

        // 这另一种配置方式。但是还是用上面那种吧,容易理解一点。
        // or allow basic authentication, but NOT require it.
        // chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
        return chain;
    }

然后在控制器类上使用shiro提供的种注解来做控制:

注解功能
@RequiresGuest只有游客可以访问
@RequiresAuthentication需要登录才能访问
@RequiresUser已登录的用户或“记住我”的用户能访问
@RequiresRoles已登录的用户需具有指定的角色才能访问
@RequiresPermissions已登录的用户需具有指定的权限才能访问

代码示例:(更详细的请参考github代码的demo1)

/**
 * created by CaiBaoHong at 2018/4/18 15:51<br>
 *     测试shiro提供的注解及功能解释
 */
@RestController
@RequestMapping("/t1")
public class Test1Controller {

    // 由于TestController类上没有加@RequiresAuthentication注解,
    // 不要求用户登录才能调用接口。所以hello()和a1()接口都是可以匿名访问的
    @GetMapping("/hello")
    public String hello() {
        return "hello spring boot";
    }

    // 游客可访问,这个有点坑,游客的意思是指:subject.getPrincipal()==null
    // 所以用户在未登录时subject.getPrincipal()==null,接口可访问
    // 而用户登录后subject.getPrincipal()!=null,接口不可访问
    @RequiresGuest
    @GetMapping("/guest")
    public String guest() {
        return "@RequiresGuest";
    }

    // 已登录用户才能访问,这个注解比@RequiresUser更严格
    // 如果用户未登录调用该接口,会抛出UnauthenticatedException
    @RequiresAuthentication
    @GetMapping("/authn")
    public String authn() {
        return "@RequiresAuthentication";
    }

    // 已登录用户或“记住我”的用户可以访问
    // 如果用户未登录或不是“记住我”的用户调用该接口,UnauthenticatedException
    @RequiresUser
    @GetMapping("/user")
    public String user() {
        return "@RequiresUser";
    }

    // 要求登录的用户具有mvn:build权限才能访问
    // 由于UserService模拟返回的用户信息中有该权限,所以这个接口可以访问
    // 如果没有登录,UnauthenticatedException
    @RequiresPermissions("mvn:install")
    @GetMapping("/mvnInstall")
    public String mvnInstall() {
        return "mvn:install";
    }

    // 要求登录的用户具有mvn:build权限才能访问
    // 由于UserService模拟返回的用户信息中【没有】该权限,所以这个接口【不可以】访问
    // 如果没有登录,UnauthenticatedException
    // 如果登录了,但是没有这个权限,会报错UnauthorizedException
    @RequiresPermissions("gradleBuild")
    @GetMapping("/gradleBuild")
    public String gradleBuild() {
        return "gradleBuild";
    }

    // 要求登录的用户具有js角色才能访问
    // 由于UserService模拟返回的用户信息中有该角色,所以这个接口可访问
    // 如果没有登录,UnauthenticatedException
    @RequiresRoles("js")
    @GetMapping("/js")
    public String js() {
        return "js programmer";
    }

    // 要求登录的用户具有js角色才能访问
    // 由于UserService模拟返回的用户信息中有该角色,所以这个接口可访问
    // 如果没有登录,UnauthenticatedException
    // 如果登录了,但是没有该角色,会抛出UnauthorizedException
    @RequiresRoles("python")
    @GetMapping("/python")
    public String python() {
        return "python programmer";
    }

}

思路2:只用url配置控制鉴权授权

shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限:

配置缩写对应的过滤器功能
anonAnonymousFilter指定url可以匿名访问
authcFormAuthenticationFilter指定url需要form表单登录,默认会从请求中获取usernamepassword,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。
authcBasicBasicHttpAuthenticationFilter指定url需要basic登录
logoutLogoutFilter登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreationNoSessionCreationFilter禁止创建会话
permsPermissionsAuthorizationFilter需要指定权限才能访问
portPortFilter需要指定端口才能访问
restHttpMethodPermissionFilter将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释
rolesRolesAuthorizationFilter需要指定角色才能访问
sslSslFilter需要https请求才能访问
userUserFilter需要已登录或“记住我”的用户才能访问

在spring容器中使用ShiroFilterChainDefinition来控制所有url的鉴权和授权。优点是配置粒度大,对多个Controller做鉴权授权的控制。下面是例子,具体可以看github代码的demo2:

@Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();

        /**
         * 这里小心踩坑!我在application.yml中设置的context-path: /api/v1
         * 但经过实际测试,过滤器的过滤路径,是context-path下的路径,无需加上"/api/v1"前缀
         */

        //访问控制
        chain.addPathDefinition("/user/login", "anon");//可以匿名访问
        chain.addPathDefinition("/page/401", "anon");//可以匿名访问
        chain.addPathDefinition("/page/403", "anon");//可以匿名访问
        chain.addPathDefinition("/t4/hello", "anon");//可以匿名访问

        chain.addPathDefinition("/t4/changePwd", "authc");//需要登录
        chain.addPathDefinition("/t4/user", "user");//已登录或“记住我”的用户可以访问
        chain.addPathDefinition("/t4/mvnBuild", "authc,perms[mvn:install]");//需要mvn:build权限
        chain.addPathDefinition("/t4/gradleBuild", "authc,perms[gradle:build]");//需要gradle:build权限
        chain.addPathDefinition("/t4/js", "authc,roles[js]");//需要js角色
        chain.addPathDefinition("/t4/python", "authc,roles[python]");//需要python角色

        // shiro 提供的登出过滤器,访问指定的请求,就会执行登录,默认跳转路径是"/",或者是"shiro.loginUrl"配置的内容
        // 由于application-shiro.yml中配置了 shiro:loginUrl: /page/401,返回会返回对应的json内容
        // 可以结合/user/login和/t1/js接口来测试这个/t4/logout接口是否有效
        chain.addPathDefinition("/t4/logout", "anon,logout");

        //其它路径均需要登录
        chain.addPathDefinition("/**", "authc");

        return chain;
    }

思路3:二者结合,url配置控制鉴权,注解控制授权

就个人而言,我是非常喜欢注解方式的。但是两种配置方式灵活结合,才是适应不同应用场景的最佳实践。只用注解或只用url配置,会带来一些比较累的工作。

我举两个场景:
场景1
假如我是写系统后台管理系统的,而且我的java后台是一个纯粹返回json数据的后台,不会做页面跳转的工作。那我们后台管理系统一般都是全部接口都需要登录才能访问。如果只用注解,我需要在每个Controller上加上@RequiresAuthentication来声明每个Controller下每个方法都需要登录才能访问。这样显得有点麻烦,而且日后再加Controller,还是要加上这个注解,万一忘记加了就会出错。这时候其实用url配置的方式就可以配置全部请求都需要登录才能访问:chain.addPathDefinition("/**", "authc");

场景2
假如我是写商城的前台的,而且我的java后台是一个纯粹返回json数据的后台,但是这些接接口中,在同一个Controller下,有些是可以匿名访问的,有些是需要登录才能访问的,有些是需要特定角色、权限才能访问的。如果只用url配置,每个url都需要配置,而且容易配置错,粒度不好把控。

所以我的想法是:用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制

下面是示例代码(详细的请看github代码的demo3):
ShiroConfig.java

@Configuration
public class ShiroConfig {

    //注入自定义的realm,告诉shiro如何获取用户信息来做登录或权限控制
    @Bean
    public Realm realm() {
        return new CustomRealm();
    }

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
         * 加入这项配置能解决这个bug
         */
        creator.setUsePrefix(true);
        return creator;
    }

    /**
     * 这里统一做鉴权,即判断哪些请求路径需要用户登录,哪些请求路径不需要用户登录。
     * 这里只做鉴权,不做权限控制,因为权限用注解来做。
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
        //哪些请求可以匿名访问
        chain.addPathDefinition("/user/login", "anon");
        chain.addPathDefinition("/page/401", "anon");
        chain.addPathDefinition("/page/403", "anon");
        chain.addPathDefinition("/t5/hello", "anon");
        chain.addPathDefinition("/t5/guest", "anon");

        //除了以上的请求外,其它请求都需要登录
        chain.addPathDefinition("/**", "authc");
        return chain;
    }
}

PageController.java

@RestController
@RequestMapping("/page")
public class PageController {

    // shiro.loginUrl映射到这里,我在这里直接抛出异常交给GlobalExceptionHandler来统一返回json信息,
    // 您也可以在这里json,不过这样子就跟GlobalExceptionHandler中返回的json重复了。
    @RequestMapping("/401")
    public Json page401() {
        throw new UnauthenticatedException();
    }

    // shiro.unauthorizedUrl映射到这里。由于demo3统一约定了url方式只做鉴权控制,不做权限访问控制,
    // 也就是说在ShiroConfig中如果没有roles[js],perms[mvn:install]这样的权限访问控制配置的话,
    // 是不会跳转到这里的。
    @RequestMapping("/403")
    public Json page403() {
        throw new UnauthorizedException();
    }

    @RequestMapping("/index")
    public Json pageIndex() {
        return new Json("index",true,1,"index page",null);
    }


}

GlobalExceptionHandler.java

/**
 * 统一捕捉shiro的异常,返回给前台一个json信息,前台根据这个信息显示对应的提示,或者做页面的跳转。
 */
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //不满足@RequiresGuest注解时抛出的异常信息
    private static final String GUEST_ONLY = "Attempting to perform a guest-only operation";


    @ExceptionHandler(ShiroException.class)
    @ResponseBody
    public Json handleShiroException(ShiroException e) {
        String eName = e.getClass().getSimpleName();
        log.error("shiro执行出错:{}",eName);
        return new Json(eName, false, Codes.SHIRO_ERR, "鉴权或授权过程出错", null);
    }

    @ExceptionHandler(UnauthenticatedException.class)
    @ResponseBody
    public Json page401(UnauthenticatedException e) {
        String eMsg = e.getMessage();
        if (StringUtils.startsWithIgnoreCase(eMsg,GUEST_ONLY)){
            return new Json("401", false, Codes.UNAUTHEN, "只允许游客访问,若您已登录,请先退出登录", null)
                    .data("detail",e.getMessage());
        }else{
            return new Json("401", false, Codes.UNAUTHEN, "用户未登录", null)
                    .data("detail",e.getMessage());
        }
    }

    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public Json page403() {
        return new Json("403", false, Codes.UNAUTHZ, "用户没有访问权限", null);
    }

}

TestController.java

@RestController
@RequestMapping("/t5")
public class Test5Controller {

    // 由于ShiroConfig中配置了该路径可以匿名访问,所以这接口不需要登录就能访问
    @GetMapping("/hello")
    public String hello() {
        return "hello spring boot";
    }

    // 如果ShiroConfig中没有配置该路径可以匿名访问,所以直接被登录过滤了。
    // 如果配置了可以匿名访问,那这里在没有登录的时候可以访问,但是用户登录后就不能访问
    @RequiresGuest
    @GetMapping("/guest")
    public String guest() {
        return "@RequiresGuest";
    }

    @RequiresAuthentication
    @GetMapping("/authn")
    public String authn() {
        return "@RequiresAuthentication";
    }

    @RequiresUser
    @GetMapping("/user")
    public String user() {
        return "@RequiresUser";
    }

    @RequiresPermissions("mvn:install")
    @GetMapping("/mvnInstall")
    public String mvnInstall() {
        return "mvn:install";
    }

    @RequiresPermissions("gradleBuild")
    @GetMapping("/gradleBuild")
    public String gradleBuild() {
        return "gradleBuild";
    }


    @RequiresRoles("js")
    @GetMapping("/js")
    public String js() {
        return "js programmer";
    }


    @RequiresRoles("python")
    @GetMapping("/python")
    public String python() {
        return "python programmer";
    }

}
查看原文

赞 91 收藏 80 评论 26

上帝何解 回答了问题 · 2019-04-04

解决TypeScript number[][] 是什么意思?

定义了一个 number 类型的 二维数组啊

关注 3 回答 2

上帝何解 赞了回答 · 2019-04-03

解决Nginx 启用 h2 Chrome 报 net::ERR_SPDY_PROTOCOL_ERROR错误

主要由于http2对一个域名只会发出一次tcp请求,如果你图片很多的话,在nginx默认的时间内,肯定是一些图片数据是没有被服务端发送过来,所以造成超时报错。nginx设置服务端向客户端传输数据的超时时间。

send_timeout  300s;#客户端传输数据的超时时间

设置后我的请求时间已经超出了默认时间(大概是1.5min)

关注 6 回答 5

上帝何解 赞了回答 · 2019-01-25

用img和用script发送get请求,后者貌似要稍微快一点,这是为什么?

这和浏览器的渲染机制有关,并不是说script比img快 更准确的说法其实是script比img更加优先请求
因为考虑到script 可能会改变dom内容 因此script 标签是作为关键渲染路径来进行加载的
除非你给script加defer和asynac标签 否则只有script加载之后才会渲染页面,
而img则不是 只有如同css html script只类的资源请求完成才会去请求img资源
如果你有兴趣了解浏览器的渲染机制和先后顺序 可以看下:
https://blog.csdn.net/riddle1...

关注 5 回答 4

上帝何解 赞了文章 · 2019-01-06

Babel的使用

Babel介绍

Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。

15 年 11 月,Babel 发布了 6.0 版本。相较于前一代 Babel 5,新一代 Babel 更加模块化, 将所有的转码功能以插件的形式分离出去,默认只提供 babel-core。原本只需要装一个 babel ,现在必须按照自己的需求配置,灵活性提高的同时也提高了使用者的学习成本。

npm i babel
已经弃用,你能下载到的仅仅是一段 console.warn,告诉你 babel 6 不再以大杂烩的形式提供转码功能了。

例如,Babel 能够将新的 ES2015 箭头函数语法:

let fun = () => console.log('babel')

转译为:

"use strict";
var fun = function fun() {
  return console.log('babel');
};

不过 Babel 的用途并不止于此,它支持语法扩展,能支持像 React 所用的 JSX 语法,更重要的是,Babel 的一切都是简单的插件,谁都可以创建自己的插件,利用 Babel 的全部威力去做任何事情。
再进一步,Babel 自身被分解成了数个核心模块,任何人都可以利用它们来创建下一代的 JavaScript 工具。

使用 Babel

babel-cli

Babel 的 CLI 是一种在命令行下使用 Babel 编译文件的简单方法。

让我们先全局安装它来学习基础知识。

$ npm install --global babel-cli

我们可以这样来编译我们的第一个文件:

$ babel my-file.js

这将把编译后的结果直接输出至终端。使用 --out-file 或着 -o 可以将结果写入到指定的文件。

$ babel example.js --out-file compiled.js
# 或
$ babel example.js -o compiled.js

如果我们想要把一个目录整个编译成一个新的目录,可以使用 --out-dir 或者 -d。.

$ babel src --out-dir lib
# 或
$ babel src -d lib

babel-core

如果你需要以编程的方式来使用 Babel,可以使用 babel-core 这个包。

babel-core 的作用是把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。
首先安装 babel-core。.

$ npm install babel-core
var babel = require("babel-core");

字符串形式的 JavaScript 代码可以直接使用 babel.transform 来编译。

babel.transform("code();", options);
// => { code, map, ast }

如果是文件的话,可以使用异步 api:

babel.transformFile("filename.js", options, function(err, result) {
  result; // => { code, map, ast }
});

或者是同步 api:

babel.transformFileSync("filename.js", options);
// => { code, map, ast }

其他用法

还可以通过babel-registerbabel-node使用Babel,但由于这两种用法不适合生产环境故省略。

配置 Babel

你或许已经注意到了,目前为止通过运行 Babel 自己我们并没能“翻译”代码,而仅仅是把代码从一处拷贝到了另一处。原因就是从Babel 6以后, 默认的插件被移除, 如果没有指定一个插件,Babel将会原样输出, 不会进行编译。

你可以通过安装插件(plugins)或预设(presets,也就是一组插件)来指示 Babel 去做什么事情。

插件只是单一的功能,例如

  • es2015-arrow-functions

  • es2015-classes

  • es2015-for-of

  • es2015-spread

以下是安装箭头函数的插件方式

npm install --save-dev babel-plugin-transform-es2015-arrow-functions

如果我们一个一个引人功能单一的插件的话显得特别麻烦,通常我们用的更多的是预设。插件和预设通常写入到配置文件中。可以将配置写入package.json的‘babel’属性里,或者是一个单独的.babelrc文件。

.babelrc

在我们告诉 Babel 该做什么之前,你需要做的就是在项目的根路径下创建 .babelrc 文件。然后输入以下内容作为开始:

{
  "presets": [],
  "plugins": []
}

这个文件就是用来让 Babel 做你要它做的事情的配置文件。

babel-preset-es2015

预设 babel-preset 系列打包了一组插件,类似于餐厅的套餐。如 babel-preset-es2015 打包了 es6 的特性,babel-preset-stage-0 打包处于 strawman 阶段的语法

我们需要安装 "es2015" Babel 预设:

$ npm install --save-dev babel-preset-es2015

我们修改 .babelrc 来包含这个预设。

{
    "presets": [
+     "es2015"
    ],
    "plugins": []
  }

同样的,还有babel-preset-2016babel-preset-2017

babel-preset-latest

latest是一个特殊的presets,包括了es2015,es2016,es2017的插件(目前为止,以后有es2018也会包括进去)。即总是包含最新的编译插件。

babel-preset-env

上面提到的各种preset的问题就是: 它们都太”重”了, 即包含了过多在某些情况下不需要的功能. 比如, 现代的浏览器大多支持ES6的generator, 但是如果你使用babel-preset-es2015, 它会将generator函数编译为复杂的ES5代码, 这是没有必要的。但使用babel-preset-env, 我们可以声明环境, 然后该preset就会只编译包含我们所声明环境缺少的特性的代码,因此也是比较推荐的方式。

安装babel-preset-env

npm install babel-preset-env --save-dev

添加配置

{
  "presets": ["env"]
}

当没有添加任何的配置选项时,babel-preset-env默认行为是和babel-preset-latest是一样的。
下面我们通过一些例子来看babel-preset-env的配置是如何使用的:

  • 指定支持主流浏览器最新的两个版本以及IE 7+:

"presets": [
    [
      "env",
      {
        "targets": {
          "browsers": ["last 2 versions", "ie >= 7"]
        }
      }
    ]
  ]
}
  • 支持超过市场份额5%的浏览器:

"targets": {
  "browsers": "> 5%"
}
  • 某个固定版本的浏览器:

"targets": {
  "chrome": 56
}

更多的配置请查看官方文档

babel-preset-stage-x

官方预设(preset), 有两种,一个是按年份(babel-preset-2017),一个是按阶段(babel-preset-stage-0)。 这主要是根据TC39 委员会ECMASCRPIT 发布流程来制定的。TC39 委员会决定,从2016年开始,每年都会发布一个版本,它包括每年期限内完成的所有功能,同时ECMAScript的版本号也按年份编制,就有了ES2016, ES2017。所以也就有了babel-present-2016, babel-preset-2017, 对每一年新增的语法进行转化。babel-preset-latest 就是把所有es2015, es2016, es2017 全部包含在一起了。

最终在阶段 4 被标准正式采纳。
以下是4 个不同阶段的(打包的)预设:

  • babel-preset-stage-0

  • babel-preset-stage-1

  • babel-preset-stage-2

  • babel-preset-stage-3

注意 stage-4 预设是不存在的因为它就是上面的 es2017 预设。

以上每种预设都依赖于紧随的后期阶段预设,数字越小,阶段越靠后,存在依赖关系。也就是说stage-0是包括stage-1的,以此类推。也就是说这些stage包含的特性是比latest更新的特性但还未被写入标准进行发布。

使用的时候只需要安装你想要的阶段就可以了:

$ npm install --save-dev babel-preset-stage-2

然后添加进你的 .babelrc 配置文件。但是要注意如果没有提供es2017相关的预设,preset-stage-X 这种阶段性的预设也不能用。

执行 Babel 生成的代码

Babel 几乎可以编译所有时新的 JavaScript 语法,但对于 APIs 来说却并非如此。例如: Promise、Set、Map 等新增对象,Object.assign、Object.entries等静态方法。

为了达成使用这些新API的目的,社区又有2个实现流派:babel-polyfill和babel-runtime+babel-plugin-transform-runtime。

这两个模块功能几乎相同,就是转码新增 api,模拟 es6 环境,但实现方法完全不同。babel-polyfill 的做法是将全局对象通通污染一遍,比如想在 node 0.10 上用 Promise,调用 babel-polyfill 就会往 global 对象挂上 Promise 对象。对于普通的业务代码没有关系,但如果用在模块上就有问题了,会把模块使用者的环境污染掉。

babel-runtime 的做法是自己手动引入 helper 函数,还是上面的例子,const Promise = require('babel-runtime/core-js/promise') 就可以引入 Promise。

但 babel-runtime 也有问题,第一,很不方便,第二,在代码中中直接引入 helper 函数,意味着不能共享,造成最终打包出来的文件里有很多重复的 helper 代码。所以,babel 又开发了 babel-plugin-transform-runtime,这个模块会将我们的代码重写,如将 Promise 重写成 _Promise(只是打比方),然后引入_Promise helper 函数。这样就避免了重复打包代码和手动引入模块的痛苦。

babel-polyfill

为了解决这个问题,我们使用一种叫做 Polyfill(代码填充,也可译作兼容性补丁) 的技术。 简单地说,polyfill即是在当前运行环境中用来复制(意指模拟性的复制,而不是拷贝)尚不存在的原生 api 的代码。能让你提前使用还不可用的 APIs,Array.from 就是一个例子。
Babel 用了优秀的 core-js 用作 polyfill,并且还有定制化的 regenerator 来让 generators(生成器)和 async functions(异步函数)正常工作。
要使用 Babel polyfill,首先用 npm 安装它:

$ npm install --save babel-polyfill

然后只需要在文件顶部导入 polyfill 就可以了:

import "babel-polyfill";

babel-runtime

与 babel-polyfill 一样,babel-runtime 的作用也是模拟 ES2015 环境。只不过,babel-polyfill 是针对全局环境的,引入它,我们的浏览器就好像具备了规范里定义的完整的特性 – 虽然原生并未实现。
babel-runtime 更像是分散的 polyfill 模块,我们可以在自己的模块里单独引入,比如 require(‘babel-runtime/core-js/promise’) ,它们不会在全局环境添加未实现的方法,只是,这样手动引用每个 polyfill 会非常低效。我们借助 Runtime transform 插件来自动化处理这一切。
通过安装 babel-plugin-transform-runtimebabel-runtime 来开始。

$ npm install --save-dev babel-plugin-transform-runtime
$ npm install --save babel-runtime

然后更新 .babelrc

    {
    "plugins": [
      "transform-runtime",
      "transform-es2015-classes"
    ]
  }

现在,Babel 会把这样的代码:

class Foo {
  method() {}
}

编译成:

import _classCallCheck from "babel-runtime/helpers/classCallCheck";
import _createClass from "babel-runtime/helpers/createClass";

let Foo = function () {
  function Foo() {
    _classCallCheck(this, Foo);
  }

  _createClass(Foo, [{
    key: "method",
    value: function method() {}
  }]);

  return Foo;
}();

这样就不需要把 _classCallCheck_createClass 这两个助手方法放进每一个需要的文件里去了。

那什么时候用 babel-polyfill 什么时候用 babel-runtime 呢?如果你不介意污染全局变量(如上面提到的业务代码),放心大胆地用 babel-polyfill ;而如果你在写模块,为了避免污染使用者的环境,没的选,只能用 babel-runtime + babel-plugin-transform-runtime

options

很多预设和插件都有选项用于配置他们自身的行为。 例如,很多转换器都有“宽松”模式,通过放弃一些标准中的行为来生成更简化且性能更好的代码。

要为插件添加选项,只需要做出以下更改:

{
    "plugins": [
      "transform-runtime",
-     "transform-es2015-classes",
+     ["transform-es2015-classes", { "loose": true }]
    ]
}

plugins/presets排序:

  • 具体而言,plugins优先于presets进行编译。

  • plugins按照数组的index增序(从数组第一个到最后一个)进行编译。

  • presets按照数组的index倒序(从数组最后一个到第一个)进行编译。因为作者认为大部分会把presets写成["es2015", "stage-0"]。具体细节可以看这个。

webpack 中定义 babel-loader

很少有大型项目仅仅需要 babel,一般都是 babel 配合着 webpack 或 glup 等编译工具一起上的。
为了显出 babel 的能耐,我们分别配个用 babel-polyfillbabel-runtime 、支持 react 的webpack.config.js
先来配使用 babel-runtime 的:
首先安装:

npm install babel-loader babel-core babel-preset-es2015 babel-plugin-transform-runtime webpack --save-dev
npm install babel-runtime --save

然后配置

module: {
  loaders: [{
    loader: 'babel',
    test: /\.jsx?$/,
    include: path.join(__dirname, 'src'),
    query: {
      plugins: ['transform-runtime'],
      presets: [
        ["env", {
          "targets": {
            "chrome": 52
          },
          "modules": false,
          "loose": true
        }],
        'stage-2',
        'react'
      ],
    }
  }]
}

需要注意的是,babel-runtime 虽然没有出现在配置里,但仍然需要安装,因为 transform-runtime 依赖它。
再来个 babel-polyfill 的:

entry: [
  'babel-polyfill',
  'src/index.jsx',
],

module: {
  loaders: [{
    loader: 'babel',
    test: /\.jsx?$/,
    include: path.join(__dirname, 'src'),
    query: {
      presets: [
        ["env", {
          "targets": {
            "chrome": 52
          },
          "modules": false,
          "loose": true
        }],
        'stage-2',
        'react',
      ],
    }
  }]
}

参考文档:
http://babeljs.io/
https://github.com/thejamesky...
https://excaliburhan.com/post...
https://icyfish.me/2017/05/18...

查看原文

赞 57 收藏 85 评论 10

认证与成就

  • 获得 24 次点赞
  • 获得 72 枚徽章 获得 3 枚金徽章, 获得 21 枚银徽章, 获得 48 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-01-01
个人主页被 2.6k 人浏览