// 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)css
2.3 ajax交互(apollo也提供了組件化, 把本來的service服務變成了一個組件)html
// 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> {}
複製代碼
頁面使用react
// 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>
)
}
}
複製代碼
// 上面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>
)
複製代碼
// 讀仍是跟網絡請求時候的同樣, apollo默認啓用了cache
// 不想啓用,須要配置在請求時配置fetchPolicy字段
// 寫的話須要用到updateQuery字段
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return prev;
} else {
return fetchMoreResult;
}
}
複製代碼
// 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(),
});
}
}
複製代碼
// 部分截圖 (沒有作分頁。。)
// 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;
}),
);
}
}
複製代碼
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));
}
}
複製代碼
// 依舊是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),
);
}
}
複製代碼
// 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
}
複製代碼
// 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;
}
複製代碼
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('系統異常')
}
}
複製代碼