This commit is contained in:
Sergey Yarkov 2020-09-26 14:17:56 +03:00
parent f0b960eb22
commit 5778d12166
20 changed files with 193 additions and 136 deletions

View File

@ -25,14 +25,14 @@ tags:
## Содержание
* [Описание списка зависимостей](#Описание-списка-зависимостей)
* [Структура проекта](#Структура-проекта)
* [Предварительная настройка](#Предварительная-настройка)
* [Создание Type Definitions](#Создание-Type-Definitions)
* [Создание Resolvers](#Создание-Resolvers)
* [Создание схемы](#Создание-схемы)
* [Использование Apollo Server Express](#Использование-Apollo-Server-Express)
* [Заключение](#Заключение)
- [Описание списка зависимостей](#Описание-списка-зависимостей)
- [Структура проекта](#Структура-проекта)
- [Предварительная настройка](#Предварительная-настройка)
- [Создание Type Definitions](#Создание-Type-Definitions)
- [Создание Resolvers](#Создание-Resolvers)
- [Создание схемы](#Создание-схемы)
- [Использование Apollo Server Express](#Использование-Apollo-Server-Express)
- [Заключение](#Заключение)
💡 Данное руководство расчитано на тех разработчиков, кто уже имел небольшой опыт с использованием технологий **Apollo Server** и **TypeScript**.
@ -56,9 +56,11 @@ npm install @types/graphql-depth-limit apollo-server-express cors dotenv express
// инициализировать файл с настройками typescript
npx tsc --init
```
Теперь по порядку узнаем для чего нужны эти пакеты:
**devDependencies:**
1. **@typesnode** - устанавливаем type definitions node.js для нашего typescript **~717 kB**
2. **prettier** - этот пакет нужен для форматирования вашего кода, вы можете установить его <ins>по желанию</ins> **~9.08 MB**
3. **ts-node** - данные пакет предназначен для транспиляции typescript кода в javascript **~377 kB**
@ -66,6 +68,7 @@ npx tsc --init
5. **typescript** - сам typescript **~56.8 MB**
**dependencies:**
1. **apollo-server-express** - GraphQL сервер вместе в веб-фреймворком Express.js **~70.1 kB**
2. **express** - веб-фреймворк Express.js **~208 kB**
3. **graphql** - среда выполнения запросов к нашей API **~1.92 MB**
@ -78,6 +81,7 @@ npx tsc --init
## Структура проекта
Теперь после того как все пакеты установлены, определимся со структурой проекта:
```text
.
└── typescript-apollo-server
@ -103,13 +107,16 @@ npx tsc --init
## Предварительная настройка
Перед тем как начать разрабатывать наш сервер, зайдем в **package.json** и установим скрипты запуска:
```text
"scripts": {
"start": "ts-node index.ts",
"server": "ts-node-dev --respawn index.ts"
},
```
Теперь необходимо создать схему, создаем папку **graphql** и в ней также создаем еще две папки **resolvers** и **typeDefs**, также не забываем создать файл **schema.ts**:
```bash
mkdir graphql
cd ./graphql
@ -134,6 +141,7 @@ touch Book.graphql
Далее напишем наши типы. Обратите внимание что для удобства и разделения кода мы будем использовать импорты. Возможность использовать импорты нам предоставляет пакет **graphql-tools**. Для того чтобы импортировать файл **.graphql** ставится знак **#** и указывается путь.
Главный файл **schema.graphql**:
```graphql
# ./graphql/typeDefs/schema.graphql
@ -147,6 +155,7 @@ type Query {
```
Файл **Book.graphql**:
```graphql
# ./graphql/typeDefs/Book.graphql
@ -157,6 +166,7 @@ type Book {
```
Файл **User.graphql**:
```graphql
# ./graphql/typeDefs/User.graphql
@ -191,10 +201,11 @@ cd ..
Наконец прописываем обработчики:
Файл **Books.ts**:
```js
// ./graphql/resolvers/Books/Books.ts
import { IResolvers } from 'graphql-tools';
import { IResolvers } from "graphql-tools"
interface BookType {
id: string;
@ -202,17 +213,17 @@ interface BookType {
}
const books = [
{ id: '1', title: 'First Book' },
{ id: '2', title: 'Second Book' },
];
{ id: "1", title: "First Book" },
{ id: "2", title: "Second Book" },
]
const booksResolver: IResolvers = {
Query: {
books: (): BookType[] => books,
},
};
}
export default booksResolver;
export default booksResolver
```
Файл **Users.ts**:
@ -220,7 +231,7 @@ export default booksResolver;
```js
// ./graphql/resolvers/Users/Users.ts
import { IResolvers } from 'graphql-tools';
import { IResolvers } from "graphql-tools"
interface UserType {
id: string;
@ -228,17 +239,17 @@ interface UserType {
}
const users = [
{ id: '1', name: 'John' },
{ id: '2', name: 'Ivan' },
];
{ id: "1", name: "John" },
{ id: "2", name: "Ivan" },
]
const usersResolver: IResolvers = {
Query: {
users: (): UserType[] => users,
},
};
}
export default usersResolver;
export default usersResolver
```
Резолверы получились довольно простые и понятные, теперь нужно собрать их в один массив для дальнейшего экспорта. В файле **index.ts** прописываем следующее:
@ -246,17 +257,18 @@ export default usersResolver;
```js
// ./graphql/resolvers/index.ts
import { IResolvers } from 'graphql-tools';
import { IResolvers } from "graphql-tools"
import booksResolver from './Books/Books';
import usersResolver from './Users/Users'
import booksResolver from "./Books/Books"
import usersResolver from "./Users/Users"
const resolvers: IResolvers[] = [usersResolver, booksResolver];
const resolvers: IResolvers[] = [usersResolver, booksResolver]
export default resolvers;
export default resolvers
```
## Создание схемы
Как говорилось ранее, пакет **graphql-tools** предназначен для создания схемы. Теперь перейдем в наш файл **schema.ts** и соберем нашу схему.
Раньше для того чтобы работали наши graphql импорты мы применяли пакет **graphql-import**, но теперь его не нужно устанавливать так как он уже входит в состав **graphql-tools**. Подробнее об этом можно прочитать [здесь](https://www.graphql-tools.com/docs/migration-from-import).
@ -265,20 +277,26 @@ export default resolvers;
```js
// ./graphql/schema.ts
import { mergeResolvers, loadSchemaSync, loadFilesSync, GraphQLFileLoader, addResolversToSchema } from 'graphql-tools';
import { GraphQLSchema } from 'graphql';
import {
mergeResolvers,
loadSchemaSync,
loadFilesSync,
GraphQLFileLoader,
addResolversToSchema,
} from "graphql-tools"
import { GraphQLSchema } from "graphql"
const schema = loadSchemaSync(`${__dirname}/typeDefs/schema.graphql`, {
loaders: [new GraphQLFileLoader()],
});
})
const resolvers = loadFilesSync(`${__dirname}/resolvers`);
const resolvers = loadFilesSync(`${__dirname}/resolvers`)
const schemaWithResolvers: GraphQLSchema = addResolversToSchema({
schema,
resolvers: mergeResolvers(resolvers),
});
})
export default schemaWithResolvers;
export default schemaWithResolvers
```
## Использование Apollo Server Express
@ -294,38 +312,38 @@ touch index.ts
```js
// ./index.ts
import express, { Application, Request, Response } from 'express';
import cors from 'cors';
import depthLimit from 'graphql-depth-limit';
import { ApolloServer } from 'apollo-server-express';
import schema from './graphql/schema';
import express, { Application, Request, Response } from "express"
import cors from "cors"
import depthLimit from "graphql-depth-limit"
import { ApolloServer } from "apollo-server-express"
import schema from "./graphql/schema"
const PORT = process.env.PORT || 4000;
const PORT = process.env.PORT || 4000
const startServer = (): void => {
try {
const app: Application = express();
const app: Application = express()
const server: ApolloServer = new ApolloServer({
schema,
validationRules: [depthLimit(10)], // выставляем правило deph limit = 10
playground: true,
});
})
app.use('*', cors());
app.get('/', (req: Request, res: Response) => res.send('GraphQL API'));
app.use("*", cors())
app.get("/", (req: Request, res: Response) => res.send("GraphQL API"))
server.applyMiddleware({ app });
server.applyMiddleware({ app })
app.listen({ port: PORT }, () =>
console.log(`🚀 Server ready on port: ${PORT}`)
);
)
} catch (err) {
console.log(`❌ Something went wrong: \n ${err}`);
console.log(`❌ Something went wrong: \n ${err}`)
}
};
}
startServer();
startServer()
```
Как видим, ничего сложного нет, просто применяем функцию [applyMiddleware](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#applymiddleware) для того чтобы соедениеть Apollo Server с фрэймворком Express.js. Далее можно протестировать наш сервер, запускаем команду **npm run start** и переходим по адресу **/graphql**. Для тестирования можно выполнить вот такой запрос:
@ -372,7 +390,6 @@ startServer();
}
```
## Заключение
Как видим, использование Apollo Server вместе с TypeScript оказалась совсем не сложной задачей, так как и Apollo Server и пакет graphql-import имеют поддержку typescript из коробки.

View File

@ -3,7 +3,7 @@ import React from "react"
import Layout from "./src/components/Layout/Layout"
import "prismjs/themes/prism-tomorrow.css"
import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
import "prismjs/plugins/line-numbers/prism-line-numbers.css"
import "./src/styles/global.scss"
import "./src/components/Header/Header.scss"
@ -30,8 +30,12 @@ export const shouldUpdateScroll = ({ routerProps: { location } }) => {
}
export const wrapPageElement = ({ element, props }) => {
const { location: { pathname } } = props
pathname.includes('/amp/article/') && props.pageContext.slug ? props.amp = true : props.amp = false
const {
location: { pathname },
} = props
pathname.includes("/amp/article/") && props.pageContext.slug
? (props.amp = true)
: (props.amp = false)
return <Layout {...props}>{element}</Layout>
}

View File

@ -161,12 +161,12 @@ module.exports = {
{
resolve: `gatsby-plugin-amp`,
options: {
canonicalBaseUrl: 'https://devthread.ru/',
components: ['amp-sidebar', 'amp-accordion', 'amp-form'],
excludedPaths: ['/404*', '/'],
pathIdentifier: '/amp/',
relAmpHtmlPattern: '{{canonicalBaseUrl}}{{pathIdentifier}}{{pathname}}',
relCanonicalPattern: '{{canonicalBaseUrl}}{{pathname}}/',
canonicalBaseUrl: "https://devthread.ru/",
components: ["amp-sidebar", "amp-accordion", "amp-form"],
excludedPaths: ["/404*", "/"],
pathIdentifier: "/amp/",
relAmpHtmlPattern: "{{canonicalBaseUrl}}{{pathIdentifier}}{{pathname}}",
relCanonicalPattern: "{{canonicalBaseUrl}}{{pathname}}/",
useAmpClientIdApi: false,
},
},

View File

@ -40,7 +40,7 @@ const OnDark = () => {
}
export const onRenderBody = ({ pathname, setPreBodyComponents }) => {
if (pathname.includes('/amp/article/')) {
if (pathname.includes("/amp/article/")) {
return
}
@ -48,8 +48,12 @@ export const onRenderBody = ({ pathname, setPreBodyComponents }) => {
}
export const wrapPageElement = ({ element, props }) => {
const { location: { pathname } } = props
pathname.includes('/amp/article/') && props.pageContext.slug ? props.amp = true : props.amp = false
const {
location: { pathname },
} = props
pathname.includes("/amp/article/") && props.pageContext.slug
? (props.amp = true)
: (props.amp = false)
return <Layout {...props}>{element}</Layout>
}

View File

@ -18,7 +18,7 @@ import FacebookIcon from "../../images/facebook-icon__large.svg"
const Article = ({
data: { slug, title, image, description, category, tags, time, date },
html,
amp
amp,
}) => {
const {
siteMetadata: {
@ -40,10 +40,12 @@ const Article = ({
<div className="article-info__time">
<span>~ {time} мин</span>
</div>
{!amp ? <div className="article-info__views">
{!amp ? (
<div className="article-info__views">
<ViewsIcon />
<ViewCounter id={pathname} />
</div> : null}
</div>
) : null}
</div>
<div className="right-info">
<div className="article-info__date">
@ -93,19 +95,20 @@ const Article = ({
))}
</div>
<div className="article-image">
{!amp
? <Img
{!amp ? (
<Img
loading="eager"
fadeIn={false}
fluid={image.childImageSharp.fluid}
alt={title}
/>
: <img
) : (
<img
src={image.childImageSharp.fluid.src}
height='350'
height="350"
alt={title}
/>
}
)}
</div>
<div className="article-inner inner">
<div dangerouslySetInnerHTML={{ __html: html }}></div>

View File

@ -6,21 +6,26 @@ import { Link } from "gatsby"
const ArticleCard = ({
data: { slug, title, image, category, description, tags },
amp
amp,
}) => {
return (
<article className="article-card">
<div className="article-card__image">
<Link to={`/article/${slug}/`}>
{!amp
? <Img
{!amp ? (
<Img
loading="eager"
fadeIn={false}
fluid={image.childImageSharp.fluid}
alt={title}
/>
: <img src={image.childImageSharp.fluid.src} height='380' alt={title} />
}
) : (
<img
src={image.childImageSharp.fluid.src}
height="380"
alt={title}
/>
)}
</Link>
</div>
<div className="article-card__heading">

View File

@ -90,4 +90,3 @@ export default Footer
Footer.propTypes = {
amp: PropTypes.bool,
}

View File

@ -2,10 +2,9 @@ import React from "react"
import Header from "../Header/Header"
import Footer from "../Footer/Footer"
import { HeaderAMP } from '../../lib/amp'
import { HeaderAMP } from "../../lib/amp"
const Layout = ({ children, amp }) => {
/*
amp is accelerated mobile pages
*/

View File

@ -3,17 +3,21 @@ import React from "react"
const Newsletter = () => {
const submitHandler = e => {
e.preventDefault()
}
return (
<div className="newsletter">
<div className="newsletter-heading"><span role="img" aria-label="hot">📫</span>{" "}Новостная рассылка</div>
<div className="newsletter-heading">
<span role="img" aria-label="hot">
📫
</span>{" "}
Новостная рассылка
</div>
<div className="newsletter-content">
<p>
Подпишитесь на новостную рассылку чтобы не пропустить вышедшие статьи
</p>
<form onSubmit={submitHandler} action="#" target='_blank'>
<form onSubmit={submitHandler} action="#" target="_blank">
<div className="email-field">
<input placeholder="ваш e-mail" type="email" name="email" />
<button>Подписаться</button>

View File

@ -13,7 +13,7 @@ const SEO = ({
type,
canonical,
url,
amp
amp,
}) => {
const { pathname } = useLocation()
const { siteMetadata } = useSiteMetadataQuery()
@ -71,7 +71,8 @@ const SEO = ({
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#191919" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="theme-color" content="#ffffff" />
{!amp ? <script type="application/ld+json">
{!amp ? (
<script type="application/ld+json">
{JSON.stringify({
"@context": "http://schema.org",
"@type": "Organization",
@ -79,7 +80,8 @@ const SEO = ({
url: siteMetadata.siteUrl,
sameAs: [siteMetadata.social.twitter],
})}
</script> : null}
</script>
) : null}
</Helmet>
)
}

View File

@ -17,7 +17,7 @@ const useMoreArticlesQuery = () => {
title
image {
childImageSharp {
fluid(maxWidth: 650 quality: 100) {
fluid(maxWidth: 650, quality: 100) {
...GatsbyImageSharpFluid_withWebp
}
}

View File

@ -1,8 +1,8 @@
import React from 'react';
import { Helmet } from 'react-helmet'
import useSiteMetadataQuery from '../hooks/useSiteMetadataQuery'
import React from "react"
import { Helmet } from "react-helmet"
import useSiteMetadataQuery from "../hooks/useSiteMetadataQuery"
import LogoIcon from '../images/logo-icon.svg'
import LogoIcon from "../images/logo-icon.svg"
import TelegramIcon from "../images/telegram-icon.svg"
import TwitterIcon from "../images/twitter-icon.svg"
import YadzenIcon from "../images/yadzen-icon.svg"
@ -25,7 +25,7 @@ const HeaderAMP = () => {
height: 100%;
padding: 0px 24px;
padding-top: 25px;
background-color: rgba(7,11,14,.99);
background-color: rgba(7, 11, 14, 0.99);
width: 320px;
}
amp-sidebar nav li {
@ -79,9 +79,14 @@ const HeaderAMP = () => {
<div className="header-content amp">
<div className="header-content__logo">
<LogoIcon />
<a href='/'>devthread.ru</a>
<a href="/">devthread.ru</a>
</div>
<button tabindex="0" on='tap:sidebar-header' aria-label="menu" className="header-content__button mobile">
<button
tabindex="0"
on="tap:sidebar-header"
aria-label="menu"
className="header-content__button mobile"
>
<span />
<span />
<span />
@ -90,7 +95,13 @@ const HeaderAMP = () => {
</header>
<amp-sidebar id="sidebar-header" layout="nodisplay" side="left">
<nav>
<div role='button' tabindex="0" on='tap:sidebar-header.toggle' aria-label="menu" className='amp-sidebar__toggleBtn'>
<div
role="button"
tabindex="0"
on="tap:sidebar-header.toggle"
aria-label="menu"
className="amp-sidebar__toggleBtn"
>
✕
</div>
<span>{title}</span>

View File

@ -138,7 +138,7 @@ export const query = graphql`
title
image {
childImageSharp {
fluid(maxWidth: 650 quality: 100) {
fluid(maxWidth: 650, quality: 100) {
...GatsbyImageSharpFluid_withWebp
}
}

View File

@ -43,7 +43,7 @@ const IndexPage = ({ data: { allMarkdownRemark: edges } }) => {
<Col lg={9} xs={12}>
<Articles data={edges} limit={limit} setLimit={setLimit} />
<Snippets>
{snippets.length <= 0 ? null : <h2>Сниппеты</h2> }
{snippets.length <= 0 ? null : <h2>Сниппеты</h2>}
{snippets.map(({ node: { frontmatter } }, i) => (
<SnippetItem
key={i}
@ -78,7 +78,7 @@ export const query = graphql`
title
image {
childImageSharp {
fluid(maxWidth: 650 quality: 100) {
fluid(maxWidth: 650, quality: 100) {
...GatsbyImageSharpFluid_withWebp
}
}

View File

@ -24,7 +24,7 @@ const SnippetsPage = ({
Сниппеты это готовые решения некоторых проблем, которые можно
скопировать и вставить
</p>
{snippets.length <= 0 ? <p>Soon...</p> : null }
{snippets.length <= 0 ? <p>Soon...</p> : null}
{snippets.map(({ node: { frontmatter } }, i) => (
<SnippetItem
key={i}

View File

@ -1,5 +1,5 @@
import React from "react"
import { parse } from 'node-html-parser'
import { parse } from "node-html-parser"
import { Main } from "../ui/ui"
import { Grid, Row, Col } from "react-flexbox-grid"
import SEO from "../components/SEO/SEO"
@ -16,12 +16,17 @@ const articleTemplateAmp = ({
},
},
}) => {
// gatsbyImage to default image
const parsedHTML = parse(html, { pre: true })
const pictures = [...parsedHTML.querySelectorAll('.gatsby-resp-image-link')]
const pictures = [...parsedHTML.querySelectorAll(".gatsby-resp-image-link")]
pictures.forEach(picture => picture.parentNode.parentNode.set_content(`<img src="${picture.getAttribute('href')}" alt="${picture.querySelector('.gatsby-resp-image-image').getAttribute('alt')}" height="350" />`))
pictures.forEach(picture =>
picture.parentNode.parentNode.set_content(
`<img src="${picture.getAttribute("href")}" alt="${picture
.querySelector(".gatsby-resp-image-image")
.getAttribute("alt")}" height="350" />`
)
)
return (
<>
@ -40,7 +45,11 @@ const articleTemplateAmp = ({
<Grid fluid style={{ maxWidth: 800 }}>
<Row>
<Col xs={12}>
<Article data={frontmatter} html={parsedHTML.toString().replace(/loading="[^"]*"/, "")} amp={true} />
<Article
data={frontmatter}
html={parsedHTML.toString().replace(/loading="[^"]*"/, "")}
amp={true}
/>
<Newsletter />
<MoreArticles amp={true} />
</Col>
@ -61,7 +70,7 @@ export const pageQuery = graphql`
title
image {
childImageSharp {
fluid(maxWidth: 1400 quality: 100) {
fluid(maxWidth: 1400, quality: 100) {
...GatsbyImageSharpFluid_withWebp
}
}

View File

@ -54,7 +54,7 @@ export const pageQuery = graphql`
title
image {
childImageSharp {
fluid(maxWidth: 1200 quality: 100) {
fluid(maxWidth: 1200, quality: 100) {
...GatsbyImageSharpFluid_withWebp
}
}

View File

@ -83,7 +83,7 @@ export const pageQuery = graphql`
title
image {
childImageSharp {
fluid(maxWidth: 650 quality: 100) {
fluid(maxWidth: 650, quality: 100) {
...GatsbyImageSharpFluid_withWebp
}
}

View File

@ -80,7 +80,7 @@ export const pageQuery = graphql`
title
image {
childImageSharp {
fluid(maxWidth: 650 quality: 100) {
fluid(maxWidth: 650, quality: 100) {
...GatsbyImageSharpFluid_withWebp
}
}