GraphQL项目实操

461 阅读7分钟

这是一个发布news链接的应用,采用了react,Apollo Client,graphQL技术栈,可作为GraphQL的练手项目。

主要包括以下6个功能:

  1. 显示链接列表
  2. 注册和身份验证
  3. 创建新的链接
  4. 对链接进行投票
  5. 订阅实时更新
  6. 分页

项目搭建

  1. npm创建react项目

  2. 下载 Apollo Client和graphQL

    • Apollo Client提供了provider(用来包裹App)和一些hooks,如useQuery
  3. 配置ApolloClient

    • 引入依赖

      
      import {
        ApolloProvider,
        ApolloClient,
        createHttpLink,
        InMemoryCache,
        split,
      } from "@apollo/client";
      
    • 
      //1. 创建http链接来连接GraphQL API和ApolloClient实例
      const httpLink = createHttpLink({
        uri: "http://localhost:4000",
      });
      //2. 初始化ApolloClient
      const client = new ApolloClient({
        link, //C.确保用正确的link来实例化ApolloClient
        cache: new InMemoryCache(),
      });
      ​
      //...
      //3.包裹组件
        <ApolloProvider client={client}>
            <App />
          </ApolloProvider>
      
  4. 后端

    在git中执行


curl https://codeload.github.com/howtographql/react-apollo/tar.gz/starter | tar -xz --strip=1 react-apollo-starter/server

下载服务端代码,在项目中就会多出server文件夹,其中server/src/schema.graphql定义了schema,与前端密切相关,部分代码如下:

type Query {
  info: String!
  feed(filter: String, skip: Int, take: Int, orderBy: LinkOrderByInput): Feed!
  //在后端检索所有链接,该查询允许使用filter过滤 orderBy排序 分页参数skip  take
} 
​
type Feed {
  id: ID!
  links: [Link!]!
  count: Int!
}
​
type Mutation {
  post(url: String!, description: String!): Link!//创建新链接
  signup(email: String!, password: String!, name: String!): AuthPayload//为新用户创建账号
  login(email: String!, password: String!): AuthPayload//登录现有用户
  vote(linkId: ID!): Vote//允许已认证的用户投票
}
​
type Subscription {
  newLink: Link//在创建新链接时接收实时更新
  newVote: Vote//在投票提交时接收实时更新
}
​

比如,我们可以在playground中发送如下代码获取前10条links


{
  feed(skip:0,take:10){
    links {
      description
      url
      postedBy {
        name
      }
    }
 }
}

或使用Mutation中的signup创建一个新用户


mutation {
  signup(name:"Sarah",email: "sarah@prisma.io", password: "graphql"){
    token
    user {
      id
    }
  }
}
  1. 在开启服务器前先,创建数据库并生成Prisma客户端

cd server
npm install
npx prisma generate
npm run dev //启动服务器并打开一个playground让我们去测试API,这个playground类似Postman

playground在http://localhost:4000/

项目运行在http://localhost:3000/

使用Apollo发送查询请求:useQuery

const FEED_QUERY = gql`
  {
    feed {
      id
      links {
        id
        createdAt
        url
        description
      }
    }
  }
`;
​
// LinkList组件初次渲染的时候 data变量没有信息
// 一旦GraphQL解析了一些数据 LinkList组件就会重新渲染
const LinkList = () => {
  //useQuery返回3个元素
  // loading 当请求正在进行loading值为true
  //data 是从服务端接收到的实际数据
  const { data } = useQuery(FEED_QUERY);

Mutations: Creating Links

本小节将使用Apollo发送Mutations。和query语法类似

  • 用gql写mutation

    const CREATE_LINK_MUTATION = gql`
      mutation PostMutation($description: String!, $url: String!) {
        post(description: $description, url: $url) {
          id
          createdAt
          url
          description
        }
      }
    `;
    

  • 将CREATE_LINK_MUTATION发送给useMutation hook,使用useMutation hook发送mutation给服务器

     //解构出createLink函数
      const [createLink] = useMutation(CREATE_LINK_MUTATION, {
        variables: {
          description: formState.descrition,
          url: formState.url,
        },
      });
    

  • 调用解构出的函数

      <form
            onSubmit={(e) => {
              e.preventDefault();
              createLink(); //每当提交表单就调用createLink函数,创建新的连接
            }}
          >
    ​
    

使用Router和Apollo实现导航

  1. 在Header中写Link导航

  2. 在App.js中使用Routes和Route配置路由关系

  3. 在Index.js中使用BrowserRouter包裹App

  4. 添加新的连接后重定向到LinkList页面:

    • 导入router中的useNavigate
    • 
      const navigate = useNavigate();
       
       const [createLink] = useMutation(CREATE_LINK_MUTATION, {
         variables: {
           description: formState.descrition,
           url: formState.url,
         },
         //这个函数会在mutation完成后触发
         onCompleted: () => navigate("/"),
       });
      
    - 重定向后能看到新添加的数据
    

登录身份验证

  1. 创建Login组件 ,根据是否已经登录,条件渲染不同的UI

  2. 设置常量AUTH_TOKEN,保存本地存储

  3. 在header组件中添加Login的路由

  4. 使用认证的mutation

  5. 因为在服务器中定义了

    type Mutation {
      signup(email: String!, password: String!, name: String!): AuthPayload
      login(email: String!, password: String!): AuthPayload
    }
    

    所以如此定义SIGNUP_MUTATION:

    const SIGNUP_MUTATION = gql`
      mutation SignupMutation($email: String!, $password: String!, $name: String!) {
        signup(email: $email, password: $password, name: $name) {
          token
        }// 这个mutation返回了一个token 我们可以将其附加到后续请求,来验证用户身份
      }
    `;
    ​
    
  6. 解构出login函数,在点击登录按钮的时候调用该函数

    const [login] = useMutation(LOGIN_MUTATION, {
        //variables是来自表单的变量
        variables: {
          email: formState.email,
          password: formState.password,
        },
        onCompleted: ({ login }) => {
          localStorage.setItem(AUTH_TOKEN, login.token); //设置本地存储
          navigate("/");
        },
      });
    
  7. 将token附加到所有请求的API: 通过认证的token配置Apollo

    因为请求都是ApolloClient发送的,所以要让ApolloClient知道token存在。

    ApolloClient有中间件的概念,这个中间件的实现就是Apollo Link,它可以对所有请求进行身份验证

    这个中间件将在每次ApolloClient发送请求时被调用,Apollo Link允许我们创建中间件,在请求发送到服务器之前修改请求。

    让ApolloClient对所有请求做验证:

    
    const authLink = setContext((_, { headers }) => {
      const token = localStorage.getItem(AUTH_TOKEN); //a.从本地获取token
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : "", //b.将token拼贴到headers
        },
      };
    });
    ​
    const client = new ApolloClient({
      link: authLink.concat(httpLink), //c.确保用正确的link来实例化ApolloClient
      cache: new InMemoryCache(),
    });
    

投票功能

创建timeDifferenceForDate函数,获取从创建链接到现在的时间

修改Link组件

修改FEED_QUERY查询的字段

实现投票的mutation功能:

  1. 写gql语句,如果gql字段写错 Apollo服务器会报错400

    
    const VOTE_MUTATION = gql`
      mutation VoteMutation($linkId: ID!) {
        vote(linkId: $linkId) {
          id
          link {
            id
            votes {
              id
              user {
                id
              }
            }
          }
          user {
            id
          }
        }
      }
    `;
    ​
    
  2. 使用useMutation做投票

    
      const [vote] = useMutation(VOTE_MUTATION, {
        variables: {
          linkId: link.id,
        },
      });
    ​
    

  3.   在触发函数调用的时候使用onClick={vote},不用箭头函数
      <div
                  className="ml1 gray f11"
                  style={{ cursor: "pointer" }}
                  onClick={vote}
                >
    

新增投票数后更新缓存

执行影响数据列表的操作时,需要调用useMutation的update函数来更新缓存

 const [vote] = useMutation(VOTE_MUTATION, {
    variables: {
      linkId: link.id,
    },
    // 在mutation完成后,允许读取缓存,修改,提交改变
    update: (cache, { data: { vote } }) => {
      const { feed } = cache.readQuery({
        query: FEED_QUERY,
      }); //读取缓存
​
      //创建一个包含刚刚进行投票的新数据数组
      const updatedLinks = feed.links.map((feedLink) => {
        if (feedLink.id === link.id) {
          return {
            ...feedLink,
            votes: [...feedLink.votes, vote],
          };
        }
        return feedLink;
      });
​
      // 拥有投票列表后,使用writeQuery将更改提交到缓存中
      cache.writeQuery({
        query: FEED_QUERY,
        data: {
          feed: {
            links: updatedLinks,
          },
        },
      });
    },
  });

新增Link之后也要调用update函数,在CreateLink的useMutation的第二个对象参数里添加update属性,和vote同理

搜索--使用到graphQL的过滤功能

  1. 新建Search组件 并添加到App.js

  2. 定义gql查询语句 gql查询语句写在文件的顶部,import语句下

      //和LinkList组件的查询类似 不过多了参数来过滤
      const FEED_SEARCH_QUERY = gql`
        query FeedSearchQuery($filter: String!) {
          feed(filter: $filter) {
            id
            links {
              id
              url
              description
              createdAt
              postedBy {
                id
                name
              }
              votes {
                id
                user {
                  id
                }
              }
            }
          }
        }
      `;
    ​
    
  3. 在用户点击ok按钮时再加载数据。而不是组件初始加载时:使用useLazyQuery

      const [executeSearch, { data }] = useLazyQuery(FEED_SEARCH_QUERY);
     
     //...
     
       <button
            onClick={() => {
              executeSearch({ variables: { filter: searchFilter } });
            }}
          >
            OK
          </button>
           {/*useLazyQuery 从服务器拿到的data里面有Link ,记得要return*/}
          {data &&
            data.feed.links.map((link, index) => {
              return <Link key={link.id} link={link} index={index} />;
            })}
     
    

使用GraphQL的Subscriptions(订阅)实现实时更新

Subscriptions(订阅)允许服务器在特定事件发生时,主动将数据发送给客户端,无需客户端请求。订阅是通过webSocket实现的

使用Apollo订阅

配置ApolloClient

  1. 安装

    
    npm install subscriptions-transport-ws
    
  2. 在index.js中配置ApolloClient

    import { WebSocketLink, webSocketLink } from "@apollo/client/link/ws";
    import { getMainDefinition } from "@apollo/client/utilities";
    ​
    //...// A.实例化WebSocketLink,
    const wsLink = new WebSocketLink({
      uri: `ws://localhost:4000/graphql`, //订阅断电
      options: {
        reconnect: true,
        connectionParams: {
          authToken: localStorage.getItem(AUTH_TOKEN), //身份验证
        },
      },
    });
    ​
    //B. split用于将请求路由到特定的中间件链接,接收3个参数
    const link = split(
      ({ query }) => {
        const { kind, operation } = getMainDefinition(query);
        return kind === "OperationDefinition" && operation === "subscription";
      }, //测试函数,返回布尔值,如果为true,将请求转发到第二个参数彻底的链接;为false则转发到第三个参数
      wsLink,
      authLink.concat(httpLink)
    );
    ​
    //...const client = new ApolloClient({
      link, //C.确保用正确的link来实例化ApolloClient
      cache: new InMemoryCache(),
    });
    

    订阅Subscriptions使用的是webSocket协议,而Query/Mutations使用Http。

添加新链接实时更新LinkList:在LinkList组件中实现订阅

  1. 从useQuery中解构出subscribToMore函数

    
      const { data, loading, error, subscribeToMore } = useQuery(FEED_QUERY, {
       //...
      });
    
  2. subscribToMore接收一个对象作为参数,在这个对象要配置如何监听和响应订阅

     subscribeToMore({
        document: NEW_LINKS_SUBSCRIPTION, //NEW_LINKS_SUBSCRIPTION会监听新创建的链接
        //更新缓存
        updateQuery: (prev, { subscriptionData }) => {
          if (!subscriptionData.data) return prev;
          const newLink = subscriptionData.data.newLink;
          const exists = prev.feed.links.find(({ id }) => id === newLink.id);
          if (exists) return prev;
    ​
          return Object.assign({}, prev, {
            feed: {
              links: [newLink, ...prev.feed.links],
              count: prev.feed.links.length + 1,
              __typename: prev.feed.__typename,
            },
          });
        },
      });
    
  3. http://localhost:4000/中post新增link,http://localhost:3000/ 会实时显示新的链接

    
    mutation {
        post(url: "www.graphqlweekly.com", description: "A weekly newsletter about GraphQL") {
        id
      }
    }
    ​
    

订阅新的投票

  1. 在LinkList中添加另一个subscribeToMore

      subscribeToMore({
        document: NEW_VOTES_SUBSCRIPTION,
      });
    
  2. NEW_VOTES_SUBSCRIPTION是一个订阅,请求新的投票

Pagination分页

  1. 配置Route使得首页重定向到/new/1,而/new/:page重定向到LinkList

  2. 在header新增Top标签

  3. 重新定义LinkList中的FEED_QUERY,这次不再是简单拿到links,而是要传入take,skip和orderBy变量,根据getQueryVariables函数的条件过滤出links

      const getQueryVariables = (isNewPage, page) => {
        const skip = isNewPage ? (page - 1) * LINKS_PER_PAGE : 0;
        const take = isNewPage ? LINKS_PER_PAGE : 100;
        const orderBy = { createdAt: "desc" }; //确保最新的链接先展示
        return { take, skip, orderBy };
      };
    

    在调用useQuery的时候传入变量variables,好能够让FEED_QUERY 使用这些变量

    
    const { data, loading, error, subscribeToMore } = useQuery(FEED_QUERY, {
        variables: getQueryVariables(isNewPage, page),
    //...
      });
    
  4. 编写Pagination组件,使用navigate hook来导航

  5. 获取需要渲染的Links,在data.feed.links中拿到链接,再按投票的数量,按顺序显示

    
     {getLinksToRender(isNewPage, data).map((link, index) => {
                console.log("indexpageIndex", index + pageIndex);
                return <Link key={link.id} link={link} index={index + pageIndex} />;
              })}
    
  6. bug修复:update没有作用:在新增链接的时候,需要给Links和CreateLink中的useMutation中的update的readQuery和writeQuery新增variables

遇到的困难:

我有7条数据,每个分页展示5条,刷新http://localhost:3000/new/2的时候总是展示2条,这是对的,当我返回第一页,再点击next返回第二页的时候,就展示5条,和第一页一样,这是为什么

点击Next切换到第二页时,Apollo Client会从缓存中获取数据,而缓存中的数据是第一页的数据,所以第二页的数据和第一页一样

可以在切换页面的时候让Client发起新的网络请求,而不是使用缓存数据,可以在useQuery新增属性fetchPolicy

const { data, loading, error, subscribeToMore } = useQuery(FEED_QUERY, {
    variables: getQueryVariables(isNewPage, page),
    fetchPolicy: "network-only",
  });

参考文档: www.howtographql.com/react-apoll…