Учебник по Go GraphQL
Всем привет, меня зовут Се Вэй, я программист.
Сегодняшняя тема: Учебник по Go GraphQL.
RESTful API-дизайн
Общая веб-разработка заключается в использовании стиля RESTful для разработки API.Общий процесс этого стиля RESTful разработки API:
- анализ спроса
- дизайн модели
- Реализация кодирования
- Дизайн маршрутизации:
- Работа с параметрами: проверка, запрос
- Ответ: формат JSON, код состояния
Ресурс обычно может абстрагироваться от четырех типов маршрутов, таких как интерфейсы голосования:
# 获取所有投票信息
GET /v1/api/votes
# 获取单个投票信息
GET /v1/api/vote/{vote_id}
# 创建投票
POST /v1/api/vote
# 更新投票
PATCH /v1/api/vote/{vote_id}
# 删除投票
DELETE /v1/api/vote/{vote_id}
Соответственно, соответствующие получению, созданию, обновлению, удалению ресурсов.
Для бэкенд-разработчиков важно разрабатывать такие API таким образом, чтобы они соответствовали их потребностям.
Разработка таких API обычно требует решения следующих конкретных проблем:
- Дизайн модели в соответствии с требованиями: уровень модели, ядро дизайна модели соответствует таблицам базы данных, поэтому необходимо спроектировать поля, типы полей и отношения «многие ко многим» таблиц в соответствии с требованиями.
- Абстрагируйте объекты ресурсов и выполняйте операции добавления, удаления, изменения и запроса ресурсов.
- Возвращает ответ в формате JSON, код состояния или сообщение об ошибке.
Фронтенд или клиент, в соответствии с конкретными требованиями, вызывает интерфейс и обрабатывает поля, возвращаемые интерфейсом. Хотя иногда требования не требуют заполнения всех полей, а иногда требования требуют Вызов нескольких интерфейсов и сборка в большом формате для выполнения требований.
Сколько сущностей абстрагируется от бэкенда, соответственно будут разработаны интерфейсы различных сущностей ресурса. Последующие требования меняются, чтобы быть совместимыми, необходимо поддерживать все больше и больше интерфейсов.
Смотрите, вот такой дизайн интерфейса:
- Необходимо поддерживать несколько типов интерфейсов, требования постоянно меняются, и поддерживается все больше и больше интерфейсов.
- Приобретение полей не может быть определено фронтендом или клиентом, а возвращается все сразу, а затем обрабатывается соответствующими разработчиками
- Интерфейсная версия должна быть рассмотрена ...
GraphQL API
GraphQL — это язык запросов, специально используемый для API, он был запущен крупной компанией Facebook, но до сих пор GraphQLне привело к широкому использованию,
Большинство из них все еще разрабатываются в стиле RESTful API.
GraphQL пытается решить следующие проблемы:
- Синтаксис запроса и результаты запроса очень похожи
- Получить поля по запросу
- Получение множества запросов маршрутизации может привести
- Нет необходимости взаимодействовать на управление версиями
1
Поскольку это язык запросов, предназначенный для API, он должен иметь некоторые ограничения спецификации или синтаксиса. Какие знания содержит GraphQL?
- Схема — это набор типизированных языков, которые определяют конкретные операции (например: запрос, изменение) и информацию об объекте (например: поля ответа)
schema.graphql
type Query {
ping(data: String): Pong
}
type Mutation {
createVote(name: String!): Vote
}
type Pong{
data: String
code: Int
}
type Vote {
id: ID!
name: String!
}
Конкретно определяет коллекцию запроса: Запрос, изменение или создание коллекции: Мутация, определяет два типа объекта: Pong, Vote, объект содержит поля и типы.
Этот файл схемы является документом разработки для внутренних разработчиков и документом API для внешних или клиентских разработчиков.
Предполагая, что back-end разработчик завершил разработку по файлу схемы, как вызвать API?
Рекомендуется: почтальон
# ping 请求动作
query {
ping{
data
code
}
}
# mutation 更改动作
mutation {
createVote(name:"have a lunch") {
id
name
}
}
Можете ли вы найти некоторые закономерности?
- Файл схемы практически определяет конкретную форму запроса, формат запроса и формат ответа.
- Действия запроса API включают: тип операции (запрос, изменение, подписка), имя операции, имя запроса и поля запроса.
query HeartBeat {
ping{
data
code
}
}
- Тип операции: запрос
- Название операции: HeartBeat (название операции обычно не указывается)
- Имя запроса: пинг
- Ответ: объект Pong данных поля, код
GraphQL — это язык запросов, специализированный для API, с ограничениями синтаксиса.
В частности, включают:
- Псевдонимы: переименование полей или объектов, в основном для разрешения конфликтов.
- Фрагмент: Проще говоря, это извлечение общедоступных полей для повторного использования.
- Переменные: параметры запроса в виде переменных
- Директива: динамически отображать поля в соответствии с условиями: включает ли @include это поле, включает ли @skip это поле, отбрасывает ли @deprecate это поле
- Встроенный фрагмент: получение полей более низкого уровня в типе интерфейса или типе объединения.
- Мета-поле
- Определение типа, определение объекта
- Встроенные типы: ID, Int, Float, String, Boolean, другие типы могут использовать базовые типы для создания типов объектов.
- Перечисление: набор необязательных значений.
- Модификатор:
!
значит не пустой - Интерфейс: интерфейс
- Тип союза:
|
Состоит из типов объектов - Тип ввода: Для решения проблемы передачи сложных параметров
Сказав так много, на самом деле лучший способ — вызвать интерфейс самостоятельно, обратиться к официальной документации, попробовать каждый вызов и ознакомиться с этим набором спецификаций синтаксиса.
Лучше всего конечно: GraphQL API 4 на Github (developer.github.com/v4/)
- Знаком со спецификацией синтаксиса GraphQL.
- Изучите спецификацию дизайна GraphQL
Войдите в свою учетную запись: Доступ:developer.GitHub.com/V4/explorer…
Просто назову несколько примеров:
0. viewer: User!
- Название запроса: зритель
- Объект ответа: User не пуст, то есть будет возвращен объект User.Объект User состоит из ряда полей и объектов.
1. Основное действие запроса
{
viewer {
__typename
... on User {
name
}
}
}
// 结果
{
"data": {
"viewer": {
"__typename": "User",
"name": "XieWei"
}
}
}
2. Псевдонимы
{
AliasForViewer:viewer {
__typename
... on User {
name
}
}
}
# 结果
{
"data": {
"AliasForViewer": {
"__typename": "User",
"name": "XieWei"
}
}
}
3. Имена операций, переменные, инструкции
query PrintViewer($Repository: String!,$Has: Boolean!){
AliasForViewer:viewer{
__typename
... on User {
name
}
url
status{
createdAt
emoji
id
}
repository(name: $Repository) {
name
createdAt
description @include(if:$Has)
}
}
}
# 变量
{
"Repository": "2019-daily",
"Has": false
}
# 结果
{
"data": {
"AliasForViewer": {
"__typename": "User",
"name": "XieWei",
"url": "https://github.com/wuxiaoxiaoshen",
"status": null,
"repository": {
"name": "2019-daily",
"createdAt": "2019-01-11T15:17:43Z"
}
}
}
}
# 如果变量为:
{
"Repository": "2019-daily",
"Has": true
}
# 则结果为
{
"data": {
"AliasForViewer": {
"__typename": "User",
"name": "XieWei",
"url": "https://github.com/wuxiaoxiaoshen",
"status": null,
"repository": {
"name": "2019-daily",
"createdAt": "2019-01-11T15:17:43Z",
"description": "把2019年的生活过成一本书"
}
}
}
}
Попробуйте с документацией.
Вышеизложенное в основном касается синтаксиса операций запросов с использованием GraphQL.
2
Схема — это совокупность всех запросов, ответов и объявлений объектов, для бэкенда — основа разработки, а для фронтенда — документ API.
Как определить схему?
Вам просто нужно знать эти вещи:
- Встроенный скалярный тип: ID (сущность — строка, уникальный идентификатор), Boolean, String, Float
- модификатор
!
значит не пустой - Тип объекта:
type
ключевые слова - Тип перечисления:
enum
ключевые слова - Тип ввода:
input
ключевые слова
Чтобы привести конкретный пример: Мини-программа: Голосование Tencent
титульная страница
Подробности
Шаг 1: Определите поля объекта типа
Объект определенного типа и дизайн поля ответа почти одинаковы.
# 类似于 map, 左边表示字段名称,右边表示类型
# [] 表示列表
# ! 修饰符表示非空
type Vote {
id: ID!
createdAt: Time
updatedAt: Time
deletedAt: Time
title: String
description: String
options: [Options!]!
deadline: Time
class: VoteClass
}
type Options {
name: String
}
# 输入类型: 一般用户更改资源中的输入是列表对象,完成复杂任务
input optionsInput {
name:String!
}
# 枚举类型:投票区分:单选、多选两个选项值
enum VoteClass {
SINGLE
MULTIPLE
}
# 自定义类型,默认类型(ID、String、Boolean、Float)不包含 Time 类型
scalar Time
# 对象类型,用于检查服务是否完好
type Ping {
data: String
code: Int
}
Шаг 2: Определите тип операции: запрос используется для запроса, мутация используется для создания, изменения и удаления ресурсов.
# Query、Mutation 关键字固定
# 左边表示操作名称,右边表示返回的值的类型
# Query 一般完成查询操作
# Mutation 一般完成资源的创建、更改、删除操作
type Query {
ping: Ping
pinWithData(data: String): Ping
vote(id:ID!): Vote
}
type Mutation {
createVote(title:String!, options:[optionsInput],deadline:Time, description:String, class:VoteClass!): Vote
updateVote(title:String!, description:String!): Vote
}
Схема завершает определение типов объектов и некоторых операций.Это документ разработки для внутренних разработчиков и документ API для внешних разработчиков.
3
Как клиент использует: Go : (graphql-go)
Тема: Мини-программа Tencent Voting
Step0: Структура проекта
├── Makefile
├── README.md
├── cmd
│ ├── root_cmd.go
│ └── sync_cmd.go
├── main.go
├── model
│ └── vote.go
├── pkg
│ ├── database
│ │ └── database.go
│ └── router
│ └── router.go
├── schema.graphql
├── script
│ └── db.sh
└── web
├── mutation
│ └── mutation_type.go
├── ping
│ └── ping_query.go
├── query
│ └── query_type.go
└── vote
├── vote_curd.go
├── vote_params.go
└── vote_type.go
- cmd: файл командной строки: в основном используется для синхронизации структуры таблицы базы данных
- Основная запись функции main.go
- Определение модели модели, отдельно для каждого ресурса, например Vote.go
- Инфраструктура pkg: подключение к базе данных, проектирование маршрутизации
- Бизнес-путь веб-ядра, обычно разделенный на папки по ресурсам
- vote
- Добавление, удаление и модификация ресурсов voice_curd.go
- Параметры запроса voice_params.go
- Ресурс в схеме voice_type.go, то есть определение типа объекта
- query
- query.go
- mutation
- mutation.go
- vote
По сути, это та же структура, что и в предыдущем дизайн-проекте RESTful API.
Шаг 1: В соответствии с определением схемы: завершите определение модели базы данных.
type base struct {
Id int64 `xorm:"pk autoincr notnull" json:"id"`
CreatedAt time.Time `xorm:"created" json:"created_at"`
UpdatedAt time.Time `xorm:"updated" json:"updated_at"`
DeletedAt *time.Time `xorm:"deleted" json:"deleted_at"`
}
const (
SINGLE = iota
MULTIPLE
)
var ClassMap = map[int]string{}
func init() {
ClassMap = make(map[int]string)
ClassMap[SINGLE] = "SINGLE"
ClassMap[MULTIPLE] = "MULTIPLE"
}
type Vote struct {
base `xorm:"extends"`
Title string `json:"title"`
Description string `json:"description"`
OptionIds []int64 `json:"option_ids"`
Deadline time.Time `json:"deadline"`
Class int `json:"class"`
}
type VoteSerializer struct {
Id int64 `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Title string `json:"title"`
Description string `json:"description"`
Options []OptionSerializer `json:"options"`
Deadline time.Time `json:"deadline"`
Class int `json:"class"`
ClassString string `json:"class_string"`
}
func (V Vote) TableName() string {
return "votes"
}
func (V Vote) Serializer() VoteSerializer {
var optionSerializer []OptionSerializer
var options []Option
database.Engine.In("id", V.OptionIds).Find(&options)
for _, i := range options {
optionSerializer = append(optionSerializer, i.Serializer())
}
classString := func(value int) string {
if V.Class == SINGLE {
return "单选"
}
if V.Class == MULTIPLE {
return "多选"
}
return ""
}
return VoteSerializer{
Id: V.Id,
CreatedAt: V.CreatedAt.Truncate(time.Second),
UpdatedAt: V.UpdatedAt.Truncate(time.Second),
Title: V.Title,
Description: V.Description,
Options: optionSerializer,
Deadline: V.Deadline,
Class: V.Class,
ClassString: classString(V.Class),
}
}
type Option struct {
base `xorm:"extends"`
Name string `json:"name"`
}
type OptionSerializer struct {
Id int64 `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
}
func (O Option) TableName() string {
return "options"
}
func (O Option) Serializer() OptionSerializer {
return OptionSerializer{
Id: O.Id,
CreatedAt: O.CreatedAt.Truncate(time.Second),
UpdatedAt: O.UpdatedAt.Truncate(time.Second),
Name: O.Name,
}
}
По-прежнему сохраняйте стиль дизайна личной модели:
- Определить структуру, соответствующую таблице базы данных
- Определите сериализованную структуру, соответствующую ответу модели
- Одиночный выбор, множественный выбор, по сути он представлен 0, 1 в базе данных, а ответ выводится на китайском языке: одиночный выбор, множественный выбор
Шаг 2: описание файла query.go
var Query = graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"ping": &graphql.Field{
Type: ping.Ping,
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
return ping.Default, nil
},
},
},
})
func init() {
Query.AddFieldConfig("pingWithData", &graphql.Field{
Type: ping.Ping,
Args: graphql.FieldConfigArgument{
"data": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
if p.Args["data"] == nil {
return ping.Default, nil
}
return ping.MakeResponseForPing(p.Args["data"].(string)), nil
},
})
}
func init() {
Query.AddFieldConfig("vote", &graphql.Field{
Type: vote.Vote,
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
id := p.Args["id"]
ID, _ := strconv.Atoi(id.(string))
return vote.GetOneVote(int64(ID))
},
})
}
В основном то же самое, что и определение запроса в файле схемы:
type Query {
ping: Ping
pinWithData(data: String): Ping
vote(id:ID!): Vote
}
- Поля представляют поля объекта
- Тип указывает тип возвращаемого значения.
- Args представляет параметры
- Resolve представляет собой конкретную функцию обработки
Встроенные типы: (ID, String, Boolean, Float)
- graphql.ID
- graphql.String
- graphql.Boolean
- graphql.Float
...
Проще говоря: все объекты и поля должны иметь функции-обработчики.
var Query = graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"ping": &graphql.Field{
Type: ping.Ping,
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
return ping.Default, nil
},
},
},
})
func init() {
Query.AddFieldConfig("pingWithData", &graphql.Field{
Type: ping.Ping,
Args: graphql.FieldConfigArgument{
"data": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
if p.Args["data"] == nil {
return ping.Default, nil
}
return ping.MakeResponseForPing(p.Args["data"].(string)), nil
},
})
}
var Ping = graphql.NewObject(graphql.ObjectConfig{
Name: "ping",
Fields: graphql.Fields{
"data": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
if response, ok := p.Source.(ResponseForPing); ok {
return response.Data, nil
}
return nil, fmt.Errorf("field not found")
},
},
"code": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
if response, ok := p.Source.(ResponseForPing); ok {
return response.Code, nil
}
return nil, fmt.Errorf("field not found")
},
},
},
})
type ResponseForPing struct {
Data string `json:"data"`
Code int `json:"code"`
}
var Default = ResponseForPing{
Data: "pong",
Code: http.StatusOK,
}
func MakeResponseForPing(data string) ResponseForPing {
return ResponseForPing{
Data: data,
Code: http.StatusOK,
}
}
С клиентом Go Graphql-go большая часть работы заключается в определении объектов, определении типов полей, определении обработчиков полей и т. д.
- graphql.Object
- graphql.InputObject
- graphql.Enum
Шаг 3: описание файлаmutation.go
var Mutation = graphql.NewObject(graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
"createVote": &graphql.Field{
Type: vote.Vote,
Args: graphql.FieldConfigArgument{
"title": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"options": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.NewList(vote.OptionInput)),
},
"description": &graphql.ArgumentConfig{
Type: graphql.String,
},
"deadline": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"class": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(vote.Class),
},
},
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
log.Println(p.Args)
var params vote.CreateVoteParams
params.Title = p.Args["title"].(string)
if p.Args["description"] != nil {
params.Description = p.Args["description"].(string)
}
params.Deadline = p.Args["deadline"].(string)
params.Class = p.Args["class"].(int)
var options []vote.OptionParams
for _, i := range p.Args["options"].([]interface{}) {
var one vote.OptionParams
k := i.(map[string]interface{})
one.Name = k["name"].(string)
options = append(options, one)
}
params.Options = options
log.Println(params)
result, err := vote.CreateVote(params)
if err != nil {
return nil, err
}
return result, nil
},
},
"updateVote": &graphql.Field{
Type: vote.Vote,
Args: graphql.FieldConfigArgument{
"title": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"description": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
var params vote.UpdateVoteParams
id := p.Args["id"]
ID, _ := strconv.Atoi(id.(string))
params.Id = int64(ID)
params.Title = p.Args["title"].(string)
params.Description = p.Args["description"].(string)
return vote.UpdateOneVote(params)
},
},
},
})
Шаг 4: Создайте схему для запуска службы
func RegisterSchema() *graphql.Schema {
schema, err := graphql.NewSchema(
graphql.SchemaConfig{
Query: query.Query,
Mutation: mutation.Mutation,
})
if err != nil {
panic(fmt.Sprintf("schema init fail %s", err.Error()))
}
return &schema
}
func Register() *handler.Handler {
return handler.New(&handler.Config{
Schema: RegisterSchema(),
Pretty: true,
GraphiQL: true,
})
}
func StartWebServer() {
log.Println("Start Web Server...")
http.Handle("/graphql", Register())
log.Fatal(http.ListenAndServe(":7878", nil))
}
Шаг 5: запуск, вызов интерфейса
- Есть только один маршрут:
/graphql
- Управление версиями не требуется
- Все методы запроса:
POST
(Конечно, действие запроса также может использовать Get, что не удобно, когда есть много параметров запроса)
Примеры: (интерфейс вызывает документ запроса, в соответствии с потребностями вызывающего абонента выберите поле ответа)
mutation {
createVote(
title: "去哪玩?",
description:"本次团建去哪玩?",
options:[
{
name: "杭州西湖"
},{
name:"安徽黄山"
},{
name:"香港九龙"
}
],
deadline: "2019-08-01 00:00:00",
class: SINGLE
) {
id
title
deadline
description
createdAt
updatedAt
options{
name
}
class
classString
}
}
# 结果
{
"data": {
"vote": {
"class": "SINGLE",
"classString": "单选",
"createdAt": "2019-07-30T19:33:27+08:00",
"deadline": "2019-08-01T00:00:00+08:00",
"description": "本次团建去哪玩?",
"id": "1",
"options": [
{
"name": "杭州西湖"
},
{
"name": "安徽黄山"
},
{
"name": "香港九龙"
}
],
"title": "去哪玩?",
"updatedAt": "2019-07-30T19:33:27+08:00"
}
}
}
query{
vote(id:1){
id
title
deadline
description
createdAt
updatedAt
options{
name
}
class
classString
}
}
# 结果
{
"data": {
"createVote": {
"class": "SINGLE",
"classString": "SINGLE",
"createdAt": "2019-07-30T19:33:27+08:00",
"deadline": "2019-08-01T00:00:00+08:00",
"description": "本次团建去哪玩?",
"id": "1",
"options": {
{
"name": "杭州西湖"
},
{
"name": "安徽黄山"
},
{
"name": "香港九龙"
}
},
"title": "去哪玩?",
"updatedAt": "2019-07-30T19:33:27+08:00"
}
}
}
4
предположение:
- Design First: схема, руководство для разработчиков
- Если запросов или изменений слишком много, разделите их по функциям или ресурсам (структура проекта разделена по функциям, что помогает в определенной степени снизить нагрузку на размышления)
var Query = graphql.NewObject(graphql.ObjectConfig{}
func init(){
// 资源一
Query.AddFieldConfig("filedsName", &graphql.Field{})
}
func init(){
// 资源二
}
- Как обрабатывать сложные параметры запроса:
var Mutation = graphql.NewObject(graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
"createVote": &graphql.Field{
Type: vote.Vote,
Args: graphql.FieldConfigArgument{
"title": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"options": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.NewList(vote.OptionInput)),
},
"description": &graphql.ArgumentConfig{
Type: graphql.String,
},
"deadline": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"class": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(vote.Class),
},
},
Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
log.Println(p.Args)
var params vote.CreateVoteParams
params.Title = p.Args["title"].(string)
if p.Args["description"] != nil {
params.Description = p.Args["description"].(string)
}
params.Deadline = p.Args["deadline"].(string)
params.Class = p.Args["class"].(int)
var options []vote.OptionParams
for _, i := range p.Args["options"].([]interface{}) {
var one vote.OptionParams
k := i.(map[string]interface{})
one.Name = k["name"].(string)
options = append(options, one)
}
params.Options = options
log.Println(params)
result, err := vote.CreateVote(params)
if err != nil {
return nil, err
}
return result, nil
},
},
},
})
Args определяет все поля и типы для этого запроса. p.Args type (map[string]interface), можно получить параметры запроса. Возврат представляет собой интерфейс в соответствии с типом, определенным в Args, преобразование типов
5
Резюме: Эта статья кратко объясняет синтаксис GraphQL и пройти программирование для реализации операций GraphQL.
Как посоветуете учиться?
- Официальный китайский сайт:graphql.cn/
- С вызовами github api, знакомыми с синтаксисом
- Пример 1:GitHub.com/top Пример CEA Fury/…
- Пример 2:GitHub.com/ У Сяо Сяо сказал...