apollo-graphql 自己使用的一点姿势

3,604 阅读5分钟
  1. apollo 能干什么


  1. 怎么用(react版)

  • 2.1 引入方式
// index.tsx
import { LocaleProvider, message } from 'antd'
import zhCN from 'antd/lib/locale-provider/zh_CN';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient  } from 'apollo-client'
import { ApolloLink, NextLink, Observable, Operation } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { onError } from 'apollo-link-error';
import * as React from 'react';
import { ApolloProvider } from 'react-apollo'
import * as ReactDOM from 'react-dom';
import App from './App';
import './index.scss';
import registerServiceWorker from './registerServiceWorker';

// 请求拦截器
const request = async (operation: Operation) => {
  // 可以设置token
  operation.setContext({
    headers: {}
  })
  return Promise.resolve()
}

const requestLink = new ApolloLink((operation: Operation, forward: NextLink) => {
  return new Observable(observer => {
    let handle: any;
    Promise.resolve(operation)
      .then(oper => request(oper))
      .then(() => {
        handle = forward(operation).subscribe({
          next: observer.next.bind(observer),
          error: observer.error.bind(observer),
          complete: observer.complete.bind(observer),
        });
      })
      .catch(observer.error.bind(observer));
      
      return () => {
        if (handle) {
          handle.unsubscribe()
        }
      }

  })
}) 

const client = new ApolloClient({
  link: ApolloLink.from([
    onError(({ graphQLErrors }) => {
      // 全局错误处理
      if (Array.isArray(graphQLErrors)) {
        message.error(graphQLErrors[0].message)
      }
    }),
    requestLink,
    new BatchHttpLink({ uri: 'http://localhost:7001/graphql' }),
  ]),
  cache: new InMemoryCache(),
})

ReactDOM.render(
  <ApolloProvider client={client}>
    <LocaleProvider locale={zhCN}>
      <App />
    </LocaleProvider>
  </ApolloProvider>,
  document.getElementById('root') as HTMLElement
);
registerServiceWorker();
  • 2.2 官方集成包(apollo-boost, 强烈不推荐,因为没有集成batch)

    • 2.2.1 没有使用batch的情况
    • 2.2.2 使用了batch的情况
      目前应用场景应该是在减少短效token,换长效token时的并发问题
  • 2.3 ajax交互(apollo也提供了组件化, 把原本的service服务变成了一个组件)

// apollo component
import { Query } from 'react-apollo'
import { IArticle } from '../../interface/Article'

interface IData {
  getArticleList: {
    list: IArticle[];
    pagination: {
      page: number;
      totalPage: number;
      totalCount: number;
      perPage: number;
    };
  }
}

interface IParams {
  page: number;
  perPage: number;
}

export default class QueryArticleList extends Query<IData, IParams> {}

页面使用

// apollo query
export const articleList = gql`
  query getArticleList (
      $page: Int
      $perPage: Int
    ) {
      getArticleList (
        page: $page
        perPage: $perPage
      ) {
        list {
          _id
          title
          tags {
            name
            color
          }
        }
        pagination {
          page
          totalPage
          totalCount
          perPage
        }
    }
  }
`


// page
import * as React from 'react'
import { List, Spin, Tag } from 'antd'
import { CSSTransition } from 'react-transition-group'
import Blank from '../../components/blank/blank'
export default class ArticleListPage extends React.Component {
    public render(): JSX.Element {

    const { pagination } = this.state;
    return (
        <QueryArticleList 
        query={articleList}
        variables={{
          page: pagination.current,
          perPage: pagination.pageSize,
        }}
        >
        { 
          (({ data, error, loading, fetchMore }) => {
            if (loading) {
              return <Spin size="large" />
            }
            if (error) {
              return <p>{error}</p>
            }
            if (data) {   
                // 渲染列表
                return (
                    <CSSTransition
                      in={loading}
                      classNames="list-move"
                      timeout={500}>
                      <List
                        size="large"
                        itemLayout="vertical"
                        split={false}
                        pagination={resPagination}
                        dataSource={data.getArticleList.list}
                        renderItem={this.renderListItem} />
                    </CSSTransition>
                )
            }
            return <Blank />
          })
        }
        </QueryArticleList>
    )
    }
}
  • 2.4 分页(结合了antd)
// 上面page里(列表渲染部分替换)

   const resPagination: IStatePagination = {
        pageSize: pagination.pageSize,
        current: data.getArticleList.pagination.page,
        onChange: (pageNumber: number) => {
          fetchMore({
            variables: {
              page: pageNumber
            },
            updateQuery: (prev, { fetchMoreResult }) => {
              if (!fetchMoreResult) {
                return prev;
              } else {
                return fetchMoreResult;
              }
            }
          })
        },
        total: data.getArticleList.pagination.totalPage,
    }
    return (
        <CSSTransition
          in={loading}
          classNames="list-move"
          timeout={500}>
          <List
            size="large"
            itemLayout="vertical"
            split={false}
            pagination={resPagination}
            dataSource={data.getArticleList.list}
            renderItem={this.renderListItem} />
        </CSSTransition>
    )
  • 2.5 读写缓存(状态管理)
// 读还是跟网络请求时候的一样, apollo默认启用了cache
// 不想启用,需要配置在请求时配置fetchPolicy字段
// 写的话需要用到updateQuery字段
updateQuery: (prev, { fetchMoreResult }) => {
  if (!fetchMoreResult) {
    return prev;
  } else {
    return fetchMoreResult;
  }
} 
  1. 怎么用(angular版)

  • 3.1 引入apollo(这里推荐apollo-angular这个集成包)
// app.module.ts 部分代码
import { ApolloModule, Apollo } from 'apollo-angular';
import { HttpBatchLinkModule, HttpBatchLink } from 'apollo-angular-link-http-batch';
import { ApolloLink, from } from 'apollo-link';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { onError } from 'apollo-link-error';

@NgModule({
     declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    AppRoutingModule,
    ApolloModule,
    HttpBatchLinkModule,
    NgbModule.forRoot(),
    ThemeModule.forRoot(),
    CoreModule.forRoot(),
  ],
  bootstrap: [AppComponent],
})
export class AppModule {
  constructor (
    public apollo: Apollo,
    public httpLink: HttpBatchLink,
  ) {
      const http = httpLink.create({
        uri: 'http://localhost:7001/graphql',
        batchInterval: 500,
      });

    /**
     * @name 请求拦截器
     * @date 2019-01-15
     * */
    const authMiddleware = new ApolloLink((operation, forward) => {
      // add the authorization to the headers
      operation.setContext({
        headers: new HttpHeaders().set('Authorization', localStorage.getItem('token') || null),
      });

      return forward(operation);
    });

    /**
     * 还可以继续追加拦截器
     */
    /**
     * @name 响应拦截器
     * @date 2019-01-15
     **/
    const logoutLink = onError(({ networkError }) => {
      window.console.log('networkError', networkError);
    });
    apollo.create({
      link: from([authMiddleware, http, logoutLink]),
      cache: new InMemoryCache(),
    });
  }
}

  • 3.1 ajax交互(service)
// 部分截图 (没有做分页。。)
// graphql query基本上给react 的一样

import { Apollo } from 'apollo-angular';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { IArticle, articleList, article, addArticle, updateArticle } from '../../interface/Article';
import { IPagination } from '../../interface/Pagination';
import { DatePipe } from '../../pipes/date.pipe';

interface IArticleList {
  list: IArticle[];
  pagination: IPagination;
}


@Injectable()
export class ArticleService {

  constructor(private apollo: Apollo) {
  }
  getArticleList(): Observable<IArticle[]> {
    return this.apollo
      .watchQuery<{ getArticleList: IArticleList }, { page: number, perPage: number }>({
        query: articleList,
        variables: {
          page: 1,
          perPage: 999,
        },
        fetchPolicy: 'network-only',
      })
      .valueChanges
      .pipe(
        map(res => {
          res.data.getArticleList.list.forEach(item => {
            item.createTime = new DatePipe().transform(item.createTime);
            item.updateTime = new DatePipe().transform(item.updateTime);
          });
          return res.data.getArticleList.list;
        }),
      );
    }
}
  • 3.2 页面使用服务
import { ArticleService } from '../../../@core/data/articles.service';

@Component({
  selector: 'ngx-article-list',
  templateUrl: './article-list.component.html',
  styles: [`
    nb-card {
      transform: translate3d(0, 0, 0);
    }
  `],
})
export class ArticleListComponent implements OnInit {
    constructor(
        private service: ArticleService,
        private router: Router,
    ) {}    
    
  ngOnInit() {
    this.service.getArticleList().subscribe((data) => this.source.load(data));
  }
}
  • 3.3 读写缓存
// 依旧是service文件
// update 字段用来更新数据

@Injectable()
export class Live2dService {
  constructor(private apollo: Apollo) {
  }
  updateLive2d(name: string): Observable<ILive2d> {
    interface IRes {
      data: {
        updateLive2d: ILive2d;
      };
    }
    return this.apollo
      .mutate<{ updateLive2d: ILive2d }, { name: string }>({
        mutation: updateLive2d,
        variables: {
          name,
        },
        update (proxy, res: IRes) {
          // 取缓存
          const live2dName: string = res.data.updateLive2d.name;
          const data = proxy.readQuery({ query: admin }) as { admin: IUser };
          data.admin.live2d = live2dName;
          // 写缓存
          proxy.writeQuery({ query: admin, data });
        },
      })
      .pipe(
        map(res => res.data.updateLive2d),
      );
  }
}
  1. 服务端使用(egg)

  • 4.1 引入apollo
// extends/application.ts
import { Application } from "egg";
import GraphQL from "../graphql";
    
const TYPE_GRAPHQL_SYMBOL = Symbol("Application#TypeGraphql");
export default {
  get graphql(this: Application): GraphQL {
    if (!this[TYPE_GRAPHQL_SYMBOL]) {
      this[TYPE_GRAPHQL_SYMBOL] = new GraphQL(this);
    }
    return this[TYPE_GRAPHQL_SYMBOL];
  }
};


// app.ts
import "reflect-metadata";
import { Application } from "egg";

export default async (app: Application) => {
  await app.graphql.init();
  app.logger.info("started");
}

// graphql/index.ts
import * as path from "path";
import * as jwt from 'jsonwebtoken';

import { ApolloServer, AuthenticationError } from "apollo-server-koa";
import { Application } from "egg";
import { GraphQLSchema } from "graphql";
import { buildSchema } from "type-graphql";

export interface GraphQLConfig {
  router: string;
  graphiql: boolean;
}

export default class GraphQL {
  private readonly app: Application;
  private graphqlSchema: GraphQLSchema;
  private config: GraphQLConfig;

  constructor(app: Application) {
    this.app = app;
    this.config = app.config.graphql;
  }

  getResolvers() {
    const isLocal = this.app.env === "local";
    return [path.resolve(this.app.baseDir, `app/graphql/schema/**/*.${isLocal ? "ts" : "js"}`)];
  }

  async init() {
    this.graphqlSchema = await buildSchema({
      resolvers: this.getResolvers(),
      dateScalarMode: "timestamp"
    });
    const server = new ApolloServer({
      schema: this.graphqlSchema,
      tracing: false,
      context: async ({ ctx }) => {
        // token验证放在这里
        // 将 egg 的 context 作为 Resolver 传递的上下文
        return ctx
      },  
      playground: {
        settings: {
          "request.credentials": "include"
        }
      } as any,
      introspection: true
    });
    server.applyMiddleware({
      app: this.app,
      path: this.config.router,
      cors: true
    });
    this.app.logger.info("graphql server init");
  }

  get schema(): GraphQLSchema {
    return this.graphqlSchema;
  }
}

interface IJwt {
  exp: string | number,
  data: string
}
  • 4.2 编写graphql类型系统
 // enum类型
 import { registerEnumType } from 'type-graphql';
 export enum ImgType {
   // banner图片
   'banner' = 'banner',
   // 文章图片
   'article' = 'article',
   // 其他
   'other' = 'other'
 }

registerEnumType(ImgType, {
  name: 'ImgType',
  description: '图片类型'
})

 // 这里推荐下type-graphql
 import { ObjectType, Field, ID, InputType }  from 'type-graphql';
import { ImgType } from '../enum/imgType';
import { Status } from '../enum/status';
import { IsString, IsNotEmpty } from 'class-validator';


@ObjectType({ description: 'image model' })
export class Image {
  @Field(() => ID, { nullable: true })
  _id?: number;

  @Field({ description: '链接地址', nullable: true })
  url?: string;

  @Field(() => ImgType, { description: '图片类型', nullable: true })
  type?: ImgType;

  @Field(() => Status, { description: '图片启用状态', nullable: true })
  status?: Status;

  @Field({ description: '创建时间', nullable: true })
  createTime?: string;

  @Field({ description: '更新时间', nullable: true })
  updateTime?: string;
}

@InputType({ description: 'add image model' })
export class AddImage {
  @Field({ description: '链接地址' })
  @IsString()
  @IsNotEmpty()
  url: string;

  @Field(() => ImgType, { description: '图片类型' })
  @IsNotEmpty()
  type: ImgType;


  @Field(() => Status, { description: '图片启用状态', nullable: true })
  status?: Status;
}

@InputType({ description: 'update image model' })
export class UpdateImage extends AddImage {
  @Field(() => ID)
  @IsNotEmpty()
  _id: number;
} 

  • 4.3 graphql服务
import { Resolver, Query, ID, Arg, Ctx, Mutation } from 'type-graphql';
import { ImgType } from '../../enum/imgType';
import { Image, AddImage, UpdateImage } from '../../interface/image';
import { Context } from 'egg'

@Resolver()
export default class ImageResolve {
  @Query(() => [Image], { description: '获取图片列表' })
  async getImageList (
    @Arg("type", () => ImgType) type: ImgType,
    @Ctx() ctx: Context
  ): Promise<Image[] | Error> {
    try {
      const imgList = await ctx.model.Image.find({ type })
      return imgList as Image[]
    } catch (e) {
      ctx.logger.error(e)
      return Error('系统异常')
    }
  }
  
  @Mutation(() => Image, { description: '添加图片' })
  async addImage (
    @Arg('data') image: AddImage,
    @Ctx() ctx: Context,
  ): Promise<Image | Error> {
    try {
      const newImage = new ctx.model.Image(image)
      return await newImage.save() as Image
    } catch (e) {
      ctx.logger.error(e)
      return Error('系统异常')
    }
  }
  • 4.4 接口测试
  • 4.5 接口文档