Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.145.206.169] |
|
Страницы: (3) 1 2 [3] все ( Перейти к последнему сообщению ) |
Сообщ.
#32
,
|
|
|
А почему это костыль то? Написано же best practice
Цитата Pagination The GraphQL type system allows for some fields to return lists of values, but leaves the pagination of longer lists of values up to the API designer. There are a wide range of possible API designs for pagination, each of which has pros and cons. Typically fields that could return long lists accept arguments "first" and "after" to allow for specifying a specific region of a list, where "after" is a unique identifier of each of the values in the list. Ultimately designing APIs with feature-rich pagination led to a best practice pattern called "Connections". Some client tools for GraphQL, such as Relay, know about the Connections pattern and can automatically provide automatic support for client-side pagination when a GraphQL API employs this pattern. |
Сообщ.
#33
,
|
|
|
Немного изменил пример и заюзал Relay connection вместо массива
Кроме того переписал код GraphQL-сервера с использованием GraphQL схемы (schema.graphql), для этого заюзал пакеты graphql-import и graphql-relay. Также пришлось немного изменить код app.js и menu.js в связи с использование вместо массива реле-соединения, которое юзает edges и node . Пагинацию не стал реализовывать, возможно потом, но заюзал один из параметров соединения а именно first для указания количества записей, которых в исходной базе тока две Почекал на получение 1 или 2 записей все пучком работает! schema.graphql: type Ingredient { name: String amount: Float measurement: String } type Query { # recipes: [Recipe]! recipes( first: Int, after: String, last: Int, before: String ): RecipeConnection } type Recipe { id: ID! name: String! ingredients: [Ingredient] steps: [String] rating: Int! } # A connection to a list of items. type RecipeConnection { # A list of edges. edges: [RecipeEdge] # Information to aid in pagination. pageInfo: PageInfo } # An edge in a connection. type RecipeEdge { # The item at the end of the edge. node: Recipe # A cursor for use in pagination. cursor: String } # Information about pagination in a connection. type PageInfo { # When paginating forwards, are there more items? hasNextPage: Boolean # When paginating backwards, are there more items? hasPreviousPage: Boolean # When paginating backwards, the cursor to continue. startCursor: String # When paginating forwards, the cursor to continue. endCursor: String } server.js import express from 'express'; import logger from 'morgan'; import Bourne from 'bourne'; import path from 'path'; import graphqlHTTP from 'express-graphql'; import {buildSchema} from 'graphql'; import {importSchema} from 'graphql-import'; //<-- 1 import {connectionFromArray} from 'graphql-relay'; //<-- 2 const app = express(); app.use(logger('dev')); app.use(express.static('public')); const recipes = new Bourne('db/recipes.json'); // Construct a schema, using GraphQL schema language const schemaDef = importSchema(path.resolve('schema.graphql')); //<-- 4 const schema = buildSchema(schemaDef); //<-- 5 // The root provides a resolver function for each API endpoint const root = { //<-- 6 recipes(connArgs) { let conn = {}; recipes.find((err, results) => { conn = connectionFromArray(results, connArgs); }); return conn; } }; app.use('/graphql', graphqlHTTP({ schema, rootValue: root, //<-- 7 graphiql: true })); app.listen(3000, () => console.log(`Application running at 'http://localhost:3000'`)); app.js: import React, {Component} from 'react'; import {render} from 'react-dom'; import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; import Menu from './components/menu'; import RecipeForm from './components/recipe-form'; import {QueryRenderer, graphql} from 'react-relay'; import environment from './environment'; const ITEMS_PER_PAGE = 10; class App extends Component { // state = { // recipes: [] // } constructor() { super(); this.doRate = this.doRate.bind(this); this.addRecipe = this.addRecipe.bind(this); } doRate(id, rating) { // TODO: } addRecipe(name, ingredients, steps) { // TODO: } // ТУТ МАГИЯ! render() { return ( <QueryRenderer environment={environment} query={graphql` query appQuery($count: Int!) { recipes(first: $count) { edges { node { id name ingredients { name amount measurement } steps rating } } } } `} variables={{ count: ITEMS_PER_PAGE }} render={({props}) => { return ( <div className="container"> <Menu recipes={props ? props.recipes.edges : []} onRate={this.doRate}/> <RecipeForm onNewRecipe={this.addRecipe}/> </div> ); }} /> ); } } render( <App/>, document.getElementById('app') ); menu.js: import React from 'react'; import Recipe from './recipe'; import Summary from './summary'; const Menu = ({recipes=[], onRate=()=>{}}) => { return ( <article> <header> <h1 className="text-white bg-dark">Delicious Recipes</h1> </header> <div className="recipes"> {recipes.map(({node}, i) => { // ^^^ return ( <div key={i}> <Recipe {...node} onRate={rating => onRate(node.id, rating)}/> <Summary ingredientsCount={node.ingredients.length} stepsCount={node.steps.length}/> <hr/> </div> ); })} </div> </article> ); } export default Menu; Прикреплённый файлreact_in_action_v4_2.zip (10,81 Кбайт, скачиваний: 123) |
Сообщ.
#34
,
|
|
|
Давайте немного отойдем от Реле и вернемся к варианту программы, где юзалось состояние (state), в котором сохранялись данные (см. пост #11)
Если вы его внимательно изучали, то вероятно заметили как компоненты передают свои данные головному компоненту App, они передают их через параметры колбэков. Ничего в этом плохого нет, но вот в случае сложной иерархии компонентов, легко потерять нить вызовов этих колбэков, как например в случае вызовы doRate! Если вкраце то там doRate передается по след цепочке Menu => Recipe => StarRating! Причем сама doRate никакого отношения не имеет ни к Menu ни к Recipe, его цель StartRating, зачем тогда передавать по цепочке его? Мы бы его и не передавали если бы состояние не принадлежало App, но поскольку нам нужно сохранять данные о рейтинге в состоянии App, то поэтому мы вынуждены передавать колбэки по цепочке вниз, с тем чтобы потом вернуть данные в состояние App Короче к чему я это? А к тому что с переносом данных на сервер и подключением Реле, мы естественным путем пришли к другому решению, которое заключается в том что теперь нам не надо передавать данные по цепочке колбэков, мы можем сразу обратиться к данным через запросы к БД. В нашем случае через Реле + Графкуль Смотрите изменения в нашей программе (см. комментарии в коде), пока тока без реализации самих методов class App extends Component { // выкидываем нафик все это //constructor() { // super(); // this.doRate = this.doRate.bind(this); // this.addRecipe = this.addRecipe.bind(this); //} //doRate(id, rating) { //...... //} //addRecipe(name, ingredients, steps) { //....... //} // остается тока рендер, кстати обычно я юзю функции вместо классов, если не надо работать с состоянием // так что можно заменить класс App на функцию render() { return ( <QueryRenderer environment={environment} query={graphql` query appQuery($count: Int!) { recipes(first: $count) { edges { node { id name ingredients { name amount measurement } steps rating } } } } `} variables={{ count: ITEMS_PER_PAGE }} render={({props}) => { return ( <div className="container"> <Menu recipes={props ? props.recipes.edges : []} /> // ^^^ удалили onRate <RecipeForm /> // ^^^ удалили onNewRecipe </div> ); }} /> ); } } const Menu = ({recipes=[]) => { return ( <article> <header> <h1 className="text-white bg-dark">Delicious Recipes</h1> </header> <div className="recipes"> {recipes.map(({node}, i) => { return ( <div key={i}> <Recipe {...node} /> // ^^^ удалили onRate <Summary ingredientsCount={node.ingredients.length} stepsCount={node.steps.length} /> <hr/> </div> ); })} </div> </article> ); } const Recipe = ({name, ingredients, steps, rating }) => { // ^^^ удалили OnRate // собственно тут все и происходит теперь не надо никуда ничего передавать const onRate = rating => { // TODO: call relay mutation here } return ( <section id={name.toLowerCase().replace(/\s/g, '-')}> <h2>{name}</h2> <IngredientsList list={ingredients} /> <Instructions title="Cooking Instructions" steps={steps} /> <StarRating starsSelected={rating} onRate={onRate}/> </section> ) } Тоже относится и к методу addRecipe, теперь он расположиться в компоненте RecipeForm и оттуда будет обращаться к мутациям Реле В итоге наши связи упрощаются. Конечно наше приложение не самое сложное, но даже оно я думаю вызвало головную боль, когда вы изучали код вызовов doRate и addRecipe особенно doRate пс. кстати это не заслуга Реле само по себе, это заслуга выноса состояния в отдельный объект, существуют много подобных решений, например Redux. С ним мы тоже поработаем когда закончим с Relay! |
Сообщ.
#35
,
|
|
|
Очередной релиз
Реализовал мутацию! Пока одну Мутация меняет рейтинг рецепта, теперь звездочки щелкают Поскольку теперь состояние находится не в App, удалил из свойств компонентов (не всех) колбэки, те что описал в предыдущем посте. По просьбе читателей заменил поля в запросах на реле-фрагменты (см. код Recipe, Ingredient) schema.graphql: ......... type Mutation { changeRecipeRate(input: ChangeRecipeRateInput!): Recipe } input ChangeRecipeRateInput { rating: Int! id: ID! } server.js: .......... // The root provides a resolver function for each API endpoint const root = { ......... // Mutations changeRecipeRate: ({input}) => { let recipe = {}; recipes.findOne({ id: input.id }, (err, result) => { recipe = { ...result, rating: input.rating }; recipes.update({id: recipe.id}, recipe); }); return recipe; } }; ............ app.js: const ITEMS_PER_PAGE = 10; const App = () => ( <QueryRenderer environment={environment} query={graphql` query app_Query($count: Int!) { recipes(first: $count) { edges { node { ...recipe_recipe } } } } `} variables={{ count: ITEMS_PER_PAGE }} render={({props}) => { return ( <div className="container"> <Menu recipes={props ? props.recipes.edges : []} /> <RecipeForm /> </div> ); }} /> ); recipe.js: const Recipe = ({recipe}) => { // ВЫЗЫВАЕМ МУТАЦИЮ! const onRate = rating => { changeRecipeRate(environment, rating, recipe.id); } const {name, ingredients, steps, rating} = recipe; return ( <section id={recipe.name.toLowerCase().replace(/\s/g, '-')}> <h2>{recipe.name}</h2> <IngredientsList list={ingredients}/> <Instructions title="Cooking Instructions" steps={steps}/> <StarRating starsSelected={rating} onRate={onRate}/> <Summary ingredientsCount={ingredients.length} stepsCount={steps.length}/> </section> ) } // РЕЛЕ-ФРАГМЕНТ! export default createFragmentContainer( Recipe, graphql` fragment recipe_recipe on Recipe { id name ingredients { ...ingredient_item } steps rating } ` ); change-recipe-rate.js: import {commitMutation, graphql} from 'react-relay'; export default (environment, rating, id) => { const variables = { input: { id, rating, } }; return commitMutation( environment, { mutation: graphql` mutation changeRecipeRate_Mutation($input: ChangeRecipeRateInput!) { changeRecipeRate(input: $input) { rating } } `, variables, onError: err => console.error(err) } ) } Что тут интересного? Интересное тут то что когда мы щелкаем по звездочкам рейтинга мы видим их реакцию на клики, т.е. интерфейс сам перерисовывается! Вы спросите как это происходит без React this.setState? Да хз Знаю то что все происходит в хранилище (Store) реле, но глубоко не капал. Думаю что где там делается вызов this.setState ну или вызов ReactDOM.render. пс. на самом деле с фрагментами и мутациями я долго мудохался там не все так ясно, как может показаться, поскольку многое скрывает от нас реле-компилятор. Лично я не сразу понял например как формировать имя фрагмента, я долго менял его имя и ловил ошибки то реле-компилятора, то рантайма. Также с мутациями, как реле обновляет интерфейс мне не ясно, предстоит еще разобраться с updater и optimisticUpdater для кастомной настройки обновления хранилища. Прикреплённый файлreact_in_action_v4_4.zip (11,55 Кбайт, скачиваний: 115) |
Сообщ.
#36
,
|
|
|
Все разобрался с updater и optimisticUpdater, первая вызывается когда приходит ответ с сервера, вторая - до прихода ответа
Я все думал от чего зависит автоматическое обновление реакт-компонентов, оказывается все просто, ну или почти просто дело в том что чтобы Реле вызвало автоматический апдейт надо чтобы мутационый запрос возвращал глобальный ID, вместе с данными, в моем случае как раз я и возвращал его в объекте Recipe при мутации changeRecipeRate. Ну а поскольку есть глобал ID, то Реле-хранилище имеет возможность обнаружить место куда вносить локальные изменения! Я решил проверить свою гениальную идею! Для этого немного изменил код сервера и схемы, возвращая не весь объект Recipe, а часть вернее тока поле rating, главное не возвращать глобал АЙДИ! server.js ........ const root = { ....... // Mutations changeRecipeRate: ({input}) => { let recipe = {}; recipes.findOne({ id: input.id }, (err, result) => { recipe = { ...result, rating: input.rating }; recipes.update({id: recipe.id}, recipe); }); // 1 // return recipe; return { // NO ID! rating: recipe.rating }; } }; schema.graphql: type Mutation { # 2 changeRecipeRate(input: ChangeRecipeRateInput!): ChangeRecipeRatePayload # changeRecipeRate(input: ChangeRecipeRateInput!): Recipe } # 3 type ChangeRecipeRatePayload { rating: Int # NO ID! } Скомпилирил запустил и вуаля нифига уже зведочки не кликаются К сведению, что такое глобал ID что это за зверь. Глобал ID это уникальный ID который дается объекту при его создании, например в нашем случае он есть у объекта Recipe (см. schema.graphql) Теперь к тому как нам в этом случае помогут updater и optimicticUpdater. Надо юзать методы Реле-хранилища, чтобы получить доступ к соотвествующим объектам хранилища, не буду ща их перечислять и что они делают, скажу лишь то что через store.getRootField('changeRecipeRate') получаем возращеный объект мутационого запроса, а через store.get(id) получаем доступ к существующему объекту в Реле-хранилище по его глобал ID, ну и меняем там нужные значения. Короче в коде все написано! change-recipe-rate.js: import {commitMutation, graphql} from 'react-relay'; export default (environment, rating, id) => { const variables = { input: { rating, id } }; return commitMutation( environment, { mutation: graphql` mutation changeRecipeRate_Mutation($input: ChangeRecipeRateInput!) { changeRecipeRate(input: $input) { rating } } `, variables, // 1 optimisticUpdater: store => { const recipe = store.get(id); recipe.setValue(rating, 'rating'); }, // 2 updater: store => { const root = store.getRootField('changeRecipeRate'); const rating = root.getValue('rating'); const recipe = store.get(id); recipe.setValue(rating, 'rating'); }, onError: err => console.error(err) } ) } Как вы уже догадались зведочки рейтинга закликали пс. поскольку кликать по звездам можно много и быстро, то можно легко посадить сервер! поэтому надо предусмотреть защиту от повторных запросов. Прикреплённый файлreact_in_action_v4_4_1.zip (11,9 Кбайт, скачиваний: 139) |
Сообщ.
#37
,
|
|
|
Добавил мутацию addRecipe! Пришлось попотеть надеюсь оцените
Кроме того изменил код GraphQL-сервера, поскольку допустил ряд ошибок и не учел асинхроность операций работы с БД. Код работал, потому что в моем случае все операции выполнялись синхронно и поэтому не было ошибок рантайма. В следущем релизе я учел асинхронность, заюзав промисификацию к БД методам и упорядочил их вызовы через async + await! Для того что работали async await надо подключить пакет babel-polyfill через webpack.config.js. server.js: import express from 'express'; import logger from 'morgan'; import Bourne from 'bourne'; import path from 'path'; import graphqlHTTP from 'express-graphql'; import {buildSchema} from 'graphql'; import {importSchema} from 'graphql-import'; import {connectionFromArray} from 'graphql-relay'; import {promisify} from 'util'; const app = express(); const recipes = new Bourne('db/recipes.json'); app.use(logger('dev')); app.use(express.static('public')); // Construct a schema, using GraphQL schema language const schemaDef = importSchema(path.resolve('server', 'schema.graphql')); const schema = buildSchema(schemaDef); // promisify db methods const findRecipes = promisify(recipes.find.bind(recipes)); const findOneRecipe = promisify(recipes.findOne.bind(recipes)); const updateRecipe = promisify(recipes.update.bind(recipes)); const insertRecipe = promisify(recipes.insert.bind(recipes)); // The root provides a resolver function for each API endpoint const root = { // Queries recipes: async (args) => { const conn = await findRecipes() .then(recipes => connectionFromArray(recipes, args)); return conn; }, // Mutations changeRecipeRate: async ({input}) => { const id = parseInt(input.id, 10); let recipe = await findOneRecipe({id}); recipe = await updateRecipe({id}, { ...recipe, rating: input.rating }).then(recipes => recipes[0]); return {recipe}; }, addRecipe: async ({input}) => { const {name, rating} = input; const ingredients = [...input.ingredients]; const instructions = [...input.instructions]; const recipe = await insertRecipe({ name, ingredients, instructions, rating }); return {recipe}; } }; app.use('/graphql', graphqlHTTP({ schema, rootValue: root, graphiql: true })); app.listen(3000, () => console.log(`Application running at 'http://localhost:3000'`)); остальные изменения см в архиве schema.graphql webpack.config.js server.js add-recipe.js recipe-form.js app.js С добавлением нового рецепта прошлось тоже мудохацца! То одно то другое то вапще жрать захотелось Объясняю по шагам, вы в надежных руках, ваш моск не пострадает 1. Пишем код root API для сервера, метод addRecipe 2. Добавляем в GraphQL схему новую мутацию addRecipe и сопутствующие типы (см schema.graphql) тестим мутацию addRecipe на localhost:3000/graphql, если запрос добавляет новую запись то продолжаем 3. Добавляем директиву @connection(key: "app_Query_recipes") в app.js (имя ключ произвольное) это необходимо если мы юзаем коннекшен 4. Пишем код мутации addRecipe (см add-recipe.js) согласно ее определению в схеме (см. schema.graphql) поскольку в отличие от мутации changeRecipeRate тут возвращется новый объект, Реле не сможет автоматом добавить его в хранилище, поэтому надо определить updater, который передается в commitMutation. ВСЕ! да еще надо изменить код submitRecipe в RecipeForm поскольку теперь мы не передаем колбэк через свойтсво onNewRecipe, а напрямую вызываем мутацию addRecipe предварительно импортировав ее. УФФ! Теперь ВСЕ! Прикреплённый файлreact_in_action_v4_5.zip (12,54 Кбайт, скачиваний: 112) пс. заметьте размер архива увеличивается постепенно |
Сообщ.
#38
,
|
|
|
Продолжаю расширять функционал нашего учебного примера и добавил мутацию removeRecipe!
Теперь можно добавлять и удалять записи рецептов, правда не стал писать предупреждение перед удалением, все это можно потом добавить, а наша задача разобраться в сути Реле не так ли? Также заюзал библу reactstrap, хотя можно обойтись и чисто bootstrap, но так для разминки заменил на reactstrap, вроде как чуть меньше кодить и запоминать атрибутов bootstrap. Немного изменил оформление приложения, как по мне оно получилось довольно презентабельно, хотя я еще тот спец по офомлению Правда при замене пришлось помудохаться (ну как же без этого ) с атрибутом ref в <Input>, точнее его отсутствием , как оказалось его надо заменить на innerRef, но это еще не все, прежняя форма записи <Input innerRef="_nameInput" /> не сработала и я никак не мог понять как запомнить ссылку на DOM элемент <input> и потом обращаться к ней через this.refs как раньше. Как оказалось надо юзать <Input innerRef={input=>this._nameInput=input}/> и потом обращаться напрямую через this._nameInput, хотя с <input> можно юзать оба способа. Не стал копаться в причинах этого поведения, хотя краем глаза глянул в исходники reactstrap его Input.js и как я понял он также допускает обе формы записи, т.е. в свойство innerRef можно передавать строку или функцию или объект см в архиве: schema.graphql server.js remove-recipe.js (реализация мутации на стороне клиента) recipe.js Прикреплённый файлreact_in_action_v4_6.zip (13,41 Кбайт, скачиваний: 110) |
Сообщ.
#39
,
|
|
|
Тебе бы посты на хабр или стримчки пилить
|
Сообщ.
#40
,
|
|
|
Заждались меня робяты
Или нет? да пофик о чем я хотел сегодня поговорить... ранее я запостил пост #10 про componentWillReceiveProps, так вот он уже устарел и вместо него в начиная с 16.3 версии юзается getDerivedStateFromProps, он вроде как более безопаснее, чем componentWillReceiveProps, в чем? да хз я сам запуталси вот ссылка сами читайте getDerivedStateFromProps и кроме того их вапще не рекомендуют юзать! типо есть альтернативы им см тут You Probably Dont Need Derived State Далее разобрался в непонятках с refs см Refs and the DOM Как оказалось refs тоже устарел и больше не юзается, вместо него надо юзать React.createRef() или колбэк как в моем примере из поста #38 Кроме того в статье описывается новая фича React.forwardRef() пока на практике мне не пригодилась, но смысл понял. Если вкраце, то это способ выставления ссылки на внутрений DOM элемент дочернего компонента для использования его из родительского компонета Вот цитирую Цитата Ref forwarding is a technique for automatically passing a ref through a component to one of its children. This is typically not necessary for most components in the application. However, it can be useful for some kinds of components, especially in reusable component libraries. The most common scenarios are described below. Перевожу Цитата Ref forwarding (пересылка ссылки ) - это способ автоматической передачи ref через компонент к одному из его дочерних элементов. Обычно это не требуется для большинства компонентов приложения. Однако это может быть полезно для некоторых видов компонентов, особенно в библиотеках компонентов многократного использования. Наиболее распространенные сценарии описаны ниже. Теперь понятно почему мне оно еще не пригодилось. Я не пишу библиотеки Что я пишу? Чат-боты для форум на исходниках Добавлено Цитата Serafim @ Тебе бы посты на хабр или стримчки пилить тут пока буду упражняться |
Сообщ.
#41
,
|
|
|
Цитата Cfon @ Я не пишу библиотеки А я вот пишу) Написал компиль для SDL У меня у енамов можно значения проставлять, а у тебя нет Кстати, а во что в JS собирается вот такой код? input A { a: B! = { b: { c: { a: {} } } } } input B { b: C! } input C { c: A! } Я просто довольно много попарился в компиляторе с детектом рекурсивных отношений во всяких инпутабл типах: Скрытый текст |
Сообщ.
#42
,
|
|
|
Цитата Serafim @ Кстати, а во что в JS собирается вот такой код? input A { a: B! = { b: { c: { a: {} } } } } input B { b: C! } input C { c: A! } у меня не собирается, ошибка компиляции в function isType(type) из node_modules/graphql/type/definition.js: RangeError: Maximum call stack size exceeded |
Сообщ.
#43
,
|
|
|
Цитата Cfon @ RangeError: Maximum call stack size exceeded |