使用Django + GraphQL + Vue.js + TypeScript创建应用程序的经验总结
在制作自己的工具时,我将整理和记录所调查到的知识。对于解释性的细节,我更多地提供官方文档和参考文章的介绍。
创作物
-
- HotTip
https://github.com/namutaka/hottip
技术要素 – 技术的要点
服务器端
-
- Django v2.2
https://docs.djangoproject.com/
Graphql
https://graphql.org/learn/
Graphene
https://docs.graphene-python.org/
PythonのGraphQLサーバー実装用ライブラリ
Advanced Python Scheduler
https://apscheduler.readthedocs.io/
Pythonでの定期処理実行ライブラリ
前端
-
- TypeScript
https://www.typescriptlang.org/
Vue.js (vue-cli)
https://jp.vuejs.org/
vue-class-component
https://github.com/vuejs/vue-class-component
Vue.jsをTypeScriptのクラスベースで実装するためのデコレータ
vue-apollo
https://vue-apollo.netlify.com/
VueでApolloを使うためのライブラリ
TypeScript対応可
apollo
https://www.apollographql.com/
GraphQLクライアントライブラリ
vuetify
https://vuetifyjs.com/
MaterialDesignのVueコンポーネントライブラリ
关于Django
如何实现Django的自定义字段方法。
可以自定义Django模型字段的类型。请参考官方提供的以下页面,创建继承自django.db.models.Field的自定义类。
-
- Writing custom model fields | Django
https://docs.djangoproject.com/en/2.2/howto/custom-model-fields/
在这个新创建的应用中,我想要使用JSON类型的字段,但是在模型的字段中却没有找到相应的选项,所以我自己编写了一个。(作为表单功能,有一个JSON类型的字段类)
具体实现示例可参考:https://github.com/namutaka/hottip/blob/master/hottip/fields.py
实施的重点如下:
在自定义的Field类实现中,编写序列化和反序列化Python对象的过程是主要的任务。
在这个过程中,需要进行两种类型的序列化,一种是用于存储在数据库中的序列化,另一种是用于Web页面中的表单使用的序列化,需要分别进行相应的处理。
这就是为什么在Field类中存在名字不同但末尾带有_db_的方法,比如get_db_prep_value() 和 get_prep_value()。
如果DB和表单使用相同的序列化方法,以下内容就足够了。
from_db_value(self, value, expression, connection) をオーバーライドする
DBから取得した値を、Pythonオブジェクトにデシリアライズするメソッド
Formと同じデシリアライズ変換でよければto_ptyhonを呼び出すように実装するだけでよい
実際にはFieldクラスにはこのメソッドの定義は無く、以下で使われるようになっている
https://docs.djangoproject.com/en/2.2/_modules/django/db/models/fields/#Field.get_db_converters
to_python(self, value)をオーバーライドする
Formの入力値を、Pythonオブジェクトにデシリアライズするメソッド
このメソッドのvalueにはデシリアライズ済みの値がくることがある
get_prep_value(self, value)をオーバーライドする
Pythonオブジェクトを、Form向けにシリアライズをするメソッド
このメソッドのvalueにはシリアライズ済みの値がくることがある。Formでバリデーションエラーした場合にFormの入力値が渡されるときなど
PythonオブジェクトからDB向けのシリアライズは、get_db_prep_valueメソッドにて行うが、Fieldクラスのこのメソッド自体がget_prep_valueを使うようになっているのでそれに任せておけばいい
https://docs.djangoproject.com/en/2.2/_modules/django/db/models/fields/#Field.get_db_prep_value
从Django本身提供静态文件,并使用Django生成Docker镜像的方法。
Django内部有一个功能可以提供静态文件的服务,但该功能只在开发模式下有效。官方文档介绍了使用nginx或Apache搭建静态文件服务的方法。
-
- Managing static files (e.g. images, JavaScript, CSS) | Django documentation | Django
https://docs.djangoproject.com/en/2.2/howto/static-files/
Deploying static files | Django documentation | Django
https://docs.djangoproject.com/en/2.2/howto/static-files/deployment/
虽然以上未提及,但在实际运行应用程序的生产模式中,uWSGI也有提供服务静态文件的功能,所以如果是一个简单的应用程序,我认为可以只用这个就足够了。
具体的使用方法可以参考以下介绍Docker化的文章,通过指定UWSGI_STATIC_MAP和UWSGI_STATIC_EXPIRES_URI环境变量来实现。
-
- Here’s a Production-Ready Dockerfile for Your Python/Django App | Caktus Group
https://www.caktusgroup.com/blog/2017/03/14/production-ready-dockerfile-your-python-django-app/
# uWSGI configuration
# '/app/static/'フォルダを http://ホスト/static/ からアクセスできるようにする
ENV \
UWSGI_STATIC_MAP="/static/=/app/static/" \
UWSGI_STATIC_EXPIRES_URI="/static/.*\.[a-f0-9]{12,}\.(css|js|png|jpg|jpeg|gif|ico|woff|ttf|otf|svg|scss|map|txt) 315360000"
在uWSGI的官方文档中,关于此配置有以下说明,但文档中似乎没有关于UWSGI_STATIC_MAP环境变量本身的说明。
-
- Serving static files with uWSGI (updated to 1.9) — uWSGI 2.0 documentation
https://uwsgi-docs.readthedocs.io/en/latest/StaticFiles.html
GraphQL服务器的实现
在Django中实现GraphQL服务器。
通过使用Python的graphene库,可以实现GraphQL服务器。
如果参考官方文档来使用,应该可以很容易地进行实现。本次实现是通过直接使用Django的Model,但也可以使用Django的Form类进行实现。
如果先阅读GraphQL官方文档中的以下内容,实现方法会更容易理解。
-
- Excecution | GraphQL
https://graphql.org/learn/execution/
然而,由于文件中缺少对细节部分的解释,因此在此补充以下内容。
定义变异方法
当在Graphene中实现mutation处理时,官方文档的解释中将mutate方法作为实例方法来实现。
-
- Graphene-Python
https://docs.graphene-python.org/en/latest/types/mutations/
然而,实际上使用这种方法会将None传递给self。正如下面的问题所述,似乎方法的定义有所不同。
-
- Confusing documentation on self argument of mutate method of Mutations · Issue #951 · graphql-python/graphene
https://github.com/graphql-python/graphene/issues/951
而且,在mutate处理中,如果想要直接使用Mutation的定义类本身,可以通过@classmethod来定义以获取类对象。在Graphene中,这方面的规范显得有些模糊。
用Graphene定义ID列
在GraphQL中,建议将模型的id(ID类型的值)设为全局唯一值,包括所有类型的模型(https://graphql.org/learn/caching/#globally-unique-ids)。
在Graphene中,模型的id列(通常为数字)会自动转换为GraphQL的ID类型(内部是字符串)。因此,如果客户端需要原始的id值,需要自定义一个字段来获取该值。
class ChannelNode(DjangoObjectType):
class Meta:
model = Channel
# DjangoのModelのidカラム
raw_id = graphene.Int()
@staticmethod
def resolve_raw_id(channel, info):
""" channel: Djangoのモデルインスタンス """
return channel.id
另外,在某些情况下,例如mutation过程中,需要将GraphQL的ID类型转换为Django模型的id列的值,可以使用graphql_relay.node.node.from_global_id(String)方法。然而,由于这个方法在官方文档中没有详细解释,只是在StackOverflow中有一些评论,所以考虑到包的复杂性,这可能是一个库使用者不太可能使用的功能。
from graphql_relay.node.node import from_global_id
class UpdateHogeMutation(graphene.Mutation):
@classmethod
def mutate(cls, root, info, **fields):
"""Mutationクエリの処理
cls: このクラス自体
root: このmutation処理の親コンテキストのノード
info: 処理中のコンテキスト
fields: mutationクエリに渡されたフィールド群
"""
# ID型の値を モデルのidカラム値に変換
id = fields.pop('id')
obj_id = from_global_id(id)[1] # [ 型名, id値 ]というタプルが返されるので、2要素目を取得
# idカラム値を使ってDB操作
obj = Hoge.objects.get(pk=obj_id)
for key, value in model_fields.items():
setattr(obj, key, value)
obj.full_clean()
obj.save(update_fields=model_fields.keys())
# クエリの結果を返答
return cls(**{cls.field_name: obj})
在GraphQL中处理mutation验证错误。
由于不知道如何在GraphQL的mutation中返回验证错误,所以我做了以下处理。
-
- mutationクエリのレスポンスにerrorsというフィールドでカラムごとのバリデーションエラーを返答
-
- バリデーション成功時は、更新されたモデルオブジェクトを返すと共に、errorsは空を返答
- バリデーションエラー時は、モデルオブジェクトは返さず、errorsのみを返答
选择这种方法的原因:
-
- バリデーションエラーはmutation操作の正当な返答のパターンの一つとして考えて、GraphQLとしてのエラー返答にはしない方がいいと判断
-
- GraphQLのエラー返答は、HTTPでの500エラー系相当とイメージ
- 自前でバリデーションエラーの構造を定義する方が実装しやすいとも思う
# mutation クエリ例
mutation createHoge($name: String!, $description: String) {
createHoge(name: $name, description: $description) {
hoge {
id
name
description
createdAt
updatedAt
}
errors {
field
messages
}
}
}
class ErrorType(graphene.ObjectType):
""" フィールドごとのエラー情報を持つモデル """
field = graphene.String(required=True)
messages = graphene.List(graphene.NonNull(graphene.String), required=True)
@staticmethod
def list_from_dict(error_dict):
"""DjangoのValidationErrorからエラーのリストを生成する"""
return [ErrorType(field=key, messages=value)
for key, value in error_dict.items()]
class CreateHogeMutation(graphene.Mutation):
""" Hoge を新規作成するmutationクエリ定義 """
hoge = graphene.Field(Hoge)
errors = graphene.List(graphene.NonNull(ErrorType), required=True)
@classmethod
def mutate(cls, root, info, **fields):
newHoge = cls.model(**fields)
try:
# バリデーションしてから保存する
newHoge.full_clean()
newHoge.save()
return cls(hoge=newHoge, errors=[])
except ValidationError as e:
# バリデーションエラーの情報を返答
return cls(
errors=ErrorType.list_from_dict(e.message_dict))
Vue.js + TypeScript(类样式)
用TypeScript(类风格)实现Vue.js的方法
如果您希望在使用vue-cli生成应用程序模板时使用TypeScript并选择类样式选项,我们建议您参考以下文章以获取详细说明。
-
- Vue CLI 3.0 で TypeScript な Vue.js プロジェクトをつくってみる – Qiita
https://qiita.com/nunulk/items/7e20d6741637c3416dcd
vue.js + typescript = vue.ts ことはじめ – Qiita
https://qiita.com/nrslib/items/be90cc19fa3122266fd7
在课堂上实现Vue.js组件,可以使用以下装饰器。建议阅读此README,以确认有哪些装饰器可用。
-
- vuejs/vue-class-component: ES / TypeScript decorator for class-style Vue components.
https://github.com/vuejs/vue-class-component
若在创建组件时,存在未定义装饰器的元素,则可以像普通的Vue一样,在@Component的参数中进行记述。
@Component({
// デコレータのサポート外の要素
apollo: {
my_data: GRAPHQL_QUERY
}
})
export default class MyApp extends Vue {
}
通过使用TypeScript的类型定义,可以实现这一点,但是在跨组件参数的数据交互部分,类型检查无法很好地应用。
<template>
<div>
<!-- MyCompの my-valueプロパティの型と、myValue値の型が違うかも -->
<MyComp my-value="myValue" />
</div>
</template>
用Vue.js和TypeScript实现对话框表单的方法。
为了在进行初始化时打开对话框,并以调用方法的形式打开对话框(而不是切换dialog属性的true/false),可以参考以下文章。
-
- Vuetify Confirm Dialog component that can be used locally or globally
https://gist.github.com/eolant/ba0f8a5c9135d1a146e1db575276177d
作为一种方法,可以在对话框组件的标签上指定ref属性,并通过this.$refs.hogeDialog来访问组件实例并执行对话框启动方法。
然而,如果想在TypeScript中实现这一点,则需要明确指定组件的类型,否则会在TypeScript的类型检查中报错。因此,需要明确指定this.$refs.hogeDialog的类型进行适配。
<template>
<div>
<!--
refを指定してコンポーネントインスタンスにアクセスできるように。
フォームの決定操作のコールバックを指定する。
-->
<TipForm
ref="tipForm"
@change-tip="changeTip" />
<div>
<button @on="add">ADD</button>
<button @on="edit(tip)">EDIT</button>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import TipForm from '@/components/TipForm.vue';
import { Tip } from '@/types/models';
@Component({
components: {
TipForm,
},
})
export default class TipView extends Vue {
// this.$refs.tipForm の型を明示
$refs!: {
tipForm: TipForm;
};
@Prop() private tip!: Tip;
add() {
// tipFormが TipForm型として認識され、openメソッドを使える
this.$refs.tipForm.open();
}
edit(tip: Tip) {
this.$refs.tipForm.open(tip); // ダイアログの初期値を渡せる
}
changeTip(newTip: Tip) {
// 新しいtipを使って更新
this.tip = {...this.tip, ...newTip};
}
}
</script>
对话框的实现如下所示
略
<button @on="close">OK</button>
请用中文将以下内容进行释义:
Please paraphrase the following natively in Chinese, only one option needed.
ちなみに、VueでformタグなどのDOMをrefで使用するときも同様に`HTMLFormElement`クラスであることを明示する必要がある。
```typescript
@Component({})
export default class ChannelForm extends Vue {
$refs!: {
form: HTMLFormElement;
};
save() {
if (this.$refs.form.validate()) {
// save処理など
}
}
}
全球对话框的实施方法
以下的文章可以作为创建一个包含“确定”和“取消”选项的通用对话框的参考文章。
-
- Vue.jsで独自の確認ダイアログ(confirm)をつくる – Qiita
https://qiita.com/totto357/items/6e5df072fdb0ccbe8c51
在这里,我们定义了一个从`$root`中启动对话框的方法。
对话框组件是在方法调用时通过`new`关键字创建的,然后手动添加到DOM并进行挂载。
关于组件的propsData
在这个例子中,使用propsData在组件的创建过程中进行了一些设置,但这似乎是vue.js官方为单元测试而提供的功能。我们不确定在实际开发中可以使用到什么程度。
-
- API — Vue.js
https://jp.vuejs.org/v2/api/index.html#propsData
另外,需要注意的是,如果在类内部指定了属性的初始值,则初始值会优先于propsData。
<script lang="ts">
import { Component, Prop } from 'vue-property-decorator';
@Component()
export default class Comp extends Vue {
@Prop() private value = "initial";
}
</script>
funtion somefunc() {
let comp = new Comp({
propsData: {
value: "new value";
}
});
// このとき`comp`の中の`value`の値は`"initial"`になってしまう
}
顺便说一下
如果按照上述的方法,在ConfirmMixin.ts文件中定义要添加到$root的方法的类型,但将其写入src/types.d.ts中会导致问题。
首先,添加的$confirm会被视为未定义,而且尽管在vue-apollo中已经定义了Vue.$apollo的类型,但它也会被视为未定义。
declare module 'vue/types/vue' {
interface Vue {
$confirm(message: string): Promise<boolean>;
}
}
正确地识别.vue文件组件的类型
根据import的文件不同,*.vue文件中的Vue组件会被识别为不同的类。
如果在另一个*.vue文件中进行import,它将被正确地视为创建的组件类。然而,如果在*.ts文件中进行import,它将被视为Vue(与VueConstructor等效)类。
在此*.ts文件中,*.vue文件的类是在使用vue-cli创建vue.js + TypeScript环境时生成的以下文件中定义的。
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
以下总结了由于组件无法正常处理原始类型而导致的问题以及对应的解决方法。
如何使用单元测试来测试组件的方法
以下是关于Vue组件测试的官方文档中的说明方法。
-
- ガイド | Vue Test Utils
https://vue-test-utils.vuejs.org/ja/guides/#%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B
在上述文件的说明中,只记录了组件如何作为HTML进行渲染的断言方法。然而,这样会导致测试案例涵盖了组件的实现和渲染部分的综合操作验证,所以一个测试案例的范围变得很广。这并不适合用来验证组件的单独方法的独立操作。
当尝试测试单个方法的操作时,会出现在前面提到的*.ts文件中处理类的问题。
import MyComp from '@/components/MyComp.vue';
describe('MyComp', () => {
it('myFunc', () => {
let myComp = new MyComp();
expect(myComp.myFunc('10')).toBe(true);
// => [Error] MyCompが、`VueConstructor`と扱われるので、`myFunc`が未定義扱いになる
});
});
要解决这个问题,需要将实施测试的文件命名为.vue扩展名。
具体来说,将文件重命名为〜〜.spec.vue,并在测试执行配置文件中将*.vue文件视为测试代码。
<!-- ファイル名を変更し、scriptタグで囲う -->
<script lang="ts">
import MyComp from '@/components/MyComp.vue';
describe('MyComp', () => {
it('myFunc', () => {
let myComp = new MyComp();
expect(myComp.myFunc('10')).toBe(true);
// => [Error] MyCompが、`VueConstructor`型と扱われるので、`myFunc`が見定義扱いになる
});
});
</script>
改变测试运行器的设置(以下是使用Jest的示例)
module.exports = {
(略)
testMatch: [
// 拡張子の *.spec.vue も対象に加える
- "<rootDir>/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
+ "<rootDir>/tests/unit/**/*.spec.(js|jsx|ts|tsx|vue)|**/__tests__/*.(js|jsx|ts|tsx|vue)"
],
(略)
};
即使在上述的操作中,测试代码变为了*.vue文件,也可以通过vue-cli的测试执行(vue-cli-service test:unit命令)来执行测试。
如果想要在.ts文件中使用组件,
就像之前的全局对话框实现示例一样,有时候我们希望在Vue的整体初始化等方面使用*.ts文件来调用组件。在这种情况下,当我们尝试使用该组件的方法时,方法会被视为未定义,如上所述。
在这种情况下,我们需要自己创建一个*.d.ts文件来对组件进行定义以明确说明。
例如,考虑将自身组件中的打开全局对话框的方法设定。
<script lang="ts">
(略)
@Component({})
export default class Confirm extends Vue {
(略)
// Confirmダイアログを開く
static open(message: string): Promise<boolean> {
return new Promise((resolve) => {
new Confirm({
propsData: {
message,
success: () => resolve(true),
failure: () => resolve(false),
},
});
});
}
};
</script>
在ConfirmMixin.ts文件中使用Confirm.open方法
import Confirm from '/path/to/Confirm.vue';
export default class {
static install(Vue: VueConstructor) {
Vue.mixin({
methods: {
// `Confirm.d.ts`ファイルが無いと、
// `Confirm`が`VueConstructor`型と扱われて`open`が未定義扱いになる
$confirm: Confirm.open
}
});
}
}
然后,创建以下的Confirm.d.ts文件来声明Confirm.open函数的存在。
export = Confirm;
declare class Confirm {
static open(message: string): Promise<boolean>;
}
这样就可以直接在*.ts文件中操作组件的功能了。
Vue.js Apollo(GraphQL客户端)的使用方法
从GraphQL查询结果生成TypeScript类型
生成模型的方法
在TypeScript中,可以参考以下文章来创建GraphQL结果的类型定义的方法。
apollo-cli で GraphQL Schema から TypeScript/Flow/Scala/Swift のコードを生成する – Qiita
https://qiita.com/mizchi/items/82138e98bccab60aeaac
Automatically Generate TypeScript Definitions for GraphQL Queries with Apollo Codegen
https://medium.com/open-graphql/automatically-generate-typescript-definitions-for-graphql-queries-with-apollo-codegen-e73eae72b561
总结一下以上文章中提到的,在这次的Django + Graphene + Vue.js环境中的方法如下。
在以下情况下
-
- DjangoでGraphQLサーバーを実装済み
- TypeScript側でアプリ中に利用するGraphQLクエリをgraphql-tagを使って実装済み
<任务>
第一步
- Django環境で以下コマンド実行
# Grapheneが定義する graphql_schema コマンドを実行
python manage.py graphql_schema \
--schema myapp.schema.schema \
--out schema.json
-
- Django + Grapheneで、GraphQLサーバーのスキーマ定義をschema.jsonファイルに書き出す
-
- コマンドの詳細は以下参照
Graphene-Python
https://docs.graphene-python.org/projects/django/en/latest/introspection/#usage
步骤2
- TypeScript環境で以下コマンド実行
# apolloモジュールが定義する apollo コマンド実行
yarn run apollo codegen:generate \
--localSchemaFile=../schema.json \
--target=typescript \
--includes='src/graphql/*.ts' \
--tagName=gql \
--no-addTypename \
types
-
- 先に作ったschema.jsonとGraphQLが定義されたtsファイル(src/graphql/*.ts)を基にして、srcフォルダ下のtypesフォルダに型定義ファイル一式を生成する
–no-addTypename指定で、型内に__typenameフィールドの生成を抑止している
这样一来,将会生成src/types/{查询名称}.ts文件,并定义如下类型。
// src/types/allChannels.ts
// `query allChannels { 〜〜 }` というクエリから生成された
// 一部省略
export interface allChannels_allChannels {
// --no-addTypenameオプションにより以下のような型名称の定義を止めている
// __typename: "ChannelNodeConnection";
edges: (allChannels_allChannels_edges | null)[];
pageInfo: allChannels_allChannels_pageInfo;
}
export interface allChannels {
allChannels: allChannels_allChannels | null;
}
在这里生成的类型定义是”查询结果”的类型,而不是”GraphQL服务器端的模型定义”的类型。这是因为GraphQL通过查询描述来规定获取对象的结构的机制,所以查询结果的结构不一定等于模型的结构。
换句话说,在前端需要与服务器端的模型等同的模型定义时,需要额外自行定义。在这种情况下,我认为基本上要确保模型类型和查询结果类型是相等的。
注意,为了不干扰类型的等价性判断,我们不生成__typename。
有关在TypeScript中进行类型相等判断的规范,请参考以下内容:
-
- Type Compatibility · TypeScript
https://www.typescriptlang.org/docs/handbook/type-compatibility.html
每次进行服务器端或查询修正时,建议将上述命令定义为pipenv的子命令,并执行。
# Pipfile
[scripts]
# `pipenv run update_gql`で実行
update_gql = "bash -xc 'python manage.py graphql_schema --schema myapp.schema.schema --out schema.json && (cd frontend; yarn run apollo codegen:generate --localSchemaFile=../schema.json --target=typescript --includes=src/graphql/*.ts --tagName=gql --no-addTypename types)'"
如何使用型
型的使用方法如下:
如果要从 GraphQL 获取 Component 持有的信息:
import { Component, Vue } from 'vue-property-decorator';
// GraphQLのqueryクエリを定義した定数
import { CHANNEL } from '@/graphql/queries';
// コマンドで生成された型定義ファイルからをimport
import { channel_channel } from '@/graphql/types/channel';
@Component({
apollo: {
channel: {
// GraphQLのクエリを指定(定数にて定義しておいた)
query: CHANNEL,
},
},
})
export default class ChannelPage extends Vue {
// コマンドで生成したGraphQLのクエリ結果の型を利用
private channel!: channel_channel;
}
遺傳突變中的類型使用。
import { Component, Emit, Vue } from 'vue-property-decorator';
import { QueryResult } from 'vue-apollo/types/vue-apollo';
// GraphQLのmutationクエリを定義した定数
import { CREATE_CHANNEL } from '@/graphql/queries';
// コマンドで生成したクエリ結果の型
import { createChannel } from '@/graphql/types/createChannel';
@Component({})
export default class ChannelForm extends Vue {
save() {
// mutation処理のPromiseの型は以下のようになる
let mutation: Promise<QueryResult<createChannel>>;
// mutateメソッドに genericsの型を明示すると結果の型を指定できる
mutation = this.$apollo.mutate<createChannel>({
mutation: CREATE_CHANNEL,
variables: {
...this.editedChannel,
},
fetchPolicy: 'no-cache',
});
mutation
// createChannel型の内容を展開して取得しても使える
// resultにも型が紐付いている
.then(({ data: { result } }) => {
if (!result) { throw 'result is null'; }
return result;
})
.then(({ channel, errors }) => {
if (channel) {
this.close();
this.changeChannel(channel);
} else { // バリデーションエラー
this.valid = false;
this.errors = errors;
}
});
}
}
应对Apollo缓存机制
阿波罗具有内部缓存机制,可以保存GraphQL的响应结果。然而,这个特性可能过于强大,使用时需要小心。
import { Component, Vue } from 'vue-property-decorator';
import gql from 'graphql-tag';
@Component({
apollo: {
channel: {
query: gql`
query channel() {
channel() {
id
name
tips {
id
title
}
}
}
`,
},
},
})
export default class TestPage extends Vue {
private channel!: any;
updateTip() {
// リスト中のTipを更新
let tip = this.channel.tips[1];
let updatedTip = JSON.parse(JSON.stringify(tip)); // deepcopy
updatedTip.title = "title" + new Date();
// mutationで更新
let mutation = this.$apollo.mutate({
mutation: gql`
mutation updateTip(
$id: ID!
$title: String
) {
updateTip(id: $id, title: $title) {
tip {
id
title
}
}
}
`,
variables: updatedTip,
// fetchPolicy: 'no-cache',
});
}
}
假设我们有一个通过查询获取到的频道中的一个提示通过变更操作进行更新的处理。在这种情况下,当从GraphQL API返回变更查询的结果时,channel.tips[1]的内容将自动更新。
这是Apollo缓存机制的工作原理,可以描述如下:
-
- 在Component中持有的查询结果(channel),与apollo中缓存的对象相互关联。
- 当API返回带有GraphQL的ID列的数据时,它会在缓存中更新具有相同ID的数据,包括对象的内部结构。(由于GraphQL的ID列在整个模型中是唯一的特性)
使用这个选项可能会认为无需使用回调等来更新突变的结果,但我个人认为这可能会导致意外的行为,因此我希望自己进行更新处理。
要停止这个行为,需要在执行突变时明确表示不使用缓存。具体来说,可以取消上面示例代码中已注释的 fetchPolicy: ‘no-cache’ 并激活它。
如果不希望整体使用缓存,可以参考以下内容进行默认设置。
-
- Disable cache?? · Issue #354 · Akryum/vue-apollo
https://github.com/Akryum/vue-apollo/issues/354
在使用vue-apollo对Django API进行访问时,处理CSRF检查的方法是怎样的?
当在使用 vue-apollo 自定义 API 调用处理时,可以通过组合 ApolloLink 来实现。请参考官方文档的以下说明:
-
- Composing Links – Apollo Docs
https://www.apollographql.com/docs/link/composition
要对这个ApolloLink的设置进行配置,需要修改一个名为 “plugins/vue-apollo.ts” 的配置文件。(vue-apollo.ts 是在使用 vue-cli 创建环境并添加 vue-apollo 插件时自动生成的)
以下是一部分抜粋的內容,但是在傳遞給crateApolloClient方法的defaultOptions的link選項將被設置為ApolloLink的位置。
在這裡指定的ApolloLink將與createApolloClient內的HttpLink結合在一起,以實際進行Http API調用。
(例如,option.link.concat(new HttpLink(graphql_url)))
setContent方法使用參數中的function來生成ApolloLink對象,以修改HTTP請求的內容。在這裡,我們修改了HTTP請求標頭的內容。
import { setContext } from 'apollo-link-context';
import { createApolloClient } from 'vue-cli-plugin-apollo/graphql-client';
import Cookies from 'js-cookie'; // yarn add js-cookie
const defaultOptions = {
link:
// httpヘッダーを追加する
setContext((_, { headers }) => {
return {
headers: {
...headers,
// Djangoの仕様にあわせて cookieのCSRFトークンをHTTPヘッダーに追加
'X-CSRFToken': Cookies.get('csrftoken'),
// 以下は、DjangoにHTTPリクエストがAjaxに寄るものであることを指定するため
// CSRFとは関係ないが、Django側の `request.is_ajax()`の判定のため
'X-Requested-With': 'XMLHttpRequest',
},
};
})
(略)
}
export function createProvider(options = {}) {
const { apolloClient } = createApolloClient({
...defaultOptions,
...options,
});
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
});
return apolloProvider;
}
new Vue({
router,
apolloProvider: createProvider(),
render: h => h(App),
}).$mount('#app');
使用vue-apollo进行API错误处理。
要在vue-apollo中处理GraphQL API的错误,需要利用上述提到的ApolloLink功能进行实现。
官方文档中的说明如下:
-
- Error handling – Apollo Docs
https://www.apollographql.com/docs/react/features/error-handling#network
创建用于错误处理的ApolloLink的onError方法已经准备好,因此可以使用它进行实现。将生成的用于错误处理的ApolloLink对象指定为之前创建的createApolloClient方法的link选项。
如果将先前的CSRF处理整合到一起,情况如下。
// error handling
const errorLink = onError(({ graphQLErrors, networkError }) => {
// API返答はあるが、中身がGraphQLとしてのエラー返答の場合。
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
// HTTP APIそのもののエラー(statusが200系でない、など)
if (networkError) {
// networkErrorは、Unionクラスで statusCode があることが確定しないため、型チェックを除外
// @ts-ignore
const statusCode = networkError.statusCode || -1;
console.log(`[Network error]: status=${statusCode} ${networkError}`);
// 非ログイン時の対応例
// 認証の場合に403返答を返すこととし、その場合にログイン画面に強制遷移させる
if (statusCode == 403) {
const url = window.location.pathname + window.location.search;
router.push({ name: 'login', query: { next: url } });
}
}
});
const defaultOptions = {
// エラー対応のlinkと、CSRF対応のsetContextで生成されるlinkを結合する
link: errorLink.concat(
setContext((_, { headers }) => {
return {
headers: {
...headers,
'X-CSRFToken': Cookies.get('csrftoken'),
'X-Requested-With': 'XMLHttpRequest',
},
};
})
),
}
根据 GraphQL 的错误响应 (https://graphql.org/learn/serving-over-http/#response) 和作为 HTTP 错误,需分别进行相应处理。
另外,在vue-cli的vue-apollo插件生成的plugins/vue-apollo.ts文件中,还生成了用于错误处理的逻辑。然而,这个errorHandler似乎是作为”VueApollo”的错误处理,仅适用于组件中分配的数据查询query(@Component({apollo: hoge: { query: gql 〜〜} }))的执行时错误。因此,如果要自行执行mutation查询(如在组件中使用this.$apollo.mutate({ mutation: gql 〜〜})的情况),它不提供支持。因此,我认为使用onError和ApolloLink方式进行错误处理会更好。
(抜粋)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
errorHandler (error) {
// eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
},
})
(https://github.com/Akryum/vue-cli-plugin-apollo/blob/master/generator/templates/vue-apollo/default/src/vue-apollo.js#L71)的代码中指定了在Apollo客户端初始化时使用的HTTP链接。