Project background

The GraphQL shared by Teacher Yin Jifeng in chengdu Web Stack Conference on November 16, 2019 aroused my strong interest. After several research, study, did a small project of practice.

Learning Materials:

graphql.cn/learn/

Typescript.bootcss.com/basic-types…

www.apollographql.com/docs/react/

Attached project address: github.com/zhangyanlin…

Do the following analysis of the code.

Project directory

The project is divided into front-end and back-end parts (directory client and server). As the picture shows,



Using the Technology stack:

Client: React hooks + typescript + Apollo + graphQL + ANTd

server:  koa2 + graphql + koa-graphql + mongoose

Project construction and source code implementation

Database section

The mongodb database is used. The installation of the database is not described here.

The mongodb environment is already available by default. Start the database.

Go to the mongodb installation directory, for example, C:\Program Files\ mongodb \Server\4.2\bin

Open the terminal and run the following command:

mongod --dbpath=./dataCopy the code

Create the main directory of the project: react-graphql-project and go to the directory

The backend part

1) Create a project

mkdir server && cd server
npm init -yCopy the code

2) Install project dependencies

yarn add koa koa-grphql koa2-cors koa-mount koa-logger graphqlCopy the code

3) Configure the startup command

Package. The json file

{
  "name": "server"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
    "start": "nodemon index.js"
  },
  "keywords": []."author": "zhangyanling"."license": "MIT"."dependencies": {
    "graphql": "^ 14.5.8"."koa": "^ 2.11.0"."koa-graphql": "^ 0.8.0"."koa-logger": "^ 3.2.1." "."koa-mount": "^ 4.0.0"."koa2-cors": "^ 2.0.6"."mongoose": "^ 5.7.11"}}Copy the code

4) Business development

Entry file index.js:

const Koa = require('koa');
const mount = require('koa-mount');
const graphqlHTTP = require('koa-graphql');
const cors = require('koa2-cors'); Const logger = require()'koa-logger'); // log output const myGraphQLSchema = require('./schema');

const app = new Koa();

app.use(logger())

app.use(cors({
  origin: The '*',
  allowMethods: ['GET'.'POST'.'DELETE'.'PUT'.'OPTIONS']
}))

app.use(mount('/graphql', graphqlHTTP({
  schema: myGraphQLSchema,
  graphiql: true// open playground})) app.listen(4000, () => {console.log()'server started on 4000')})Copy the code

Create model file model.js:

const mongoose = require('mongoose'); const Schema = mongoose.Schema; / / create a database connection const conn = mongoose. The createConnection ('mongodb://localhost/graphql',{ useNewUrlParser: true, useUnifiedTopology: true });

conn.on('open', () => console.log('Database connection successful! '));

conn.on('error', (error) => console.log(error)); Const CategorySchema = new Schema({name: String}); // Used to define table structure const CategorySchema = new Schema({name: String}); // const CategoryModel = conn.model('Category', CategorySchema);

const ProductSchema = new Schema({
  name: String,
  category: {
    type: schema.types.objectid, // foreign key ref:'Category'}}); const ProductModel = conn.model('Product', ProductSchema);

module.exports = {
  CategoryModel,
  ProductModel
}Copy the code

Schema. Js file:

const graphql = require('graphql');
const { CategoryModel, ProductModel } = require('./model');

const {
  GraphQLObjectType,
  GraphQLString,
  GraphQLSchema,
  GraphQLList,
  GraphQLNonNull
}  = graphql


const Category = new GraphQLObjectType({
  name: 'Category',
  fields: () => (
    {
      id: { type: GraphQLString },
      name: { type: GraphQLString },
      products: {
        type: new GraphQLList(Product),
        async resolve(parent){
          let result = await ProductModel.find({ category: parent.id })
          return result
        }
      }
    }
  )
})

const Product = new GraphQLObjectType({
  name: 'Product',
  fields: () => (
    {
      id: { type: GraphQLString },
      name: { type: GraphQLString },
      category: {
        type: Category,
        async resolve(parent){
          let result = await CategoryModel.findById(parent.category)
          return result
        }
      }
    }
  )
})


const RootQuery = new GraphQLObjectType({
  name: 'RootQuery',
  fields: {
    getCategory: {
      type: Category,
      args: {
        id: { type: new GraphQLNonNull(GraphQLString) }
      },
      async resolve(parent, args){
        let result = await CategoryModel.findById(args.id)
        return result
      }
    },
    getCategories: {
      type: new GraphQLList(Category),
      args: {},
      async resolve(parent, args){
        let result = await CategoryModel.find()
        return result
      }
    },
    getProduct: {
      type: Product,
      args: {
        id: { type: new GraphQLNonNull(GraphQLString) }
      },
      async resolve(parent, args){
        let result = await ProductModel.findById(args.id)
        return result 
      }
    },
    getProducts: {
      type: new GraphQLList(Product),
      args: {},
      async resolve(parent, args){
        let result = await ProductModel.find()
        return result 
      }
    }
  }
})

const RootMutation = new GraphQLObjectType({
  name: 'RootMutation',
  fields: {
    addCategory: {
      type: Category,
      args: {
        name: { type: new GraphQLNonNull(GraphQLString) }
      },
      async resolve(parent, args){
        let result = await CategoryModel.create(args)
        return result  
      }
    },
    addProduct: {
      type: Product,
      args: {
        name: { type: new GraphQLNonNull(GraphQLString) },
        category: { type: new GraphQLNonNull(GraphQLString) }
      },
      async resolve(parent, args){
        let result = await ProductModel.create(args)
        return result 
      }
    },
    deleteProduct: {
      type: Product,
      args: {
        id: { type: new GraphQLNonNull(GraphQLString) },
      },
      async resolve(parent, args){
        let result = await ProductModel.deleteOne({"_id": args.id})
        return result
      }
    }
  }
})

module.exports = new GraphQLSchema({
  query: RootQuery,
  mutation: RootMutation
})Copy the code

5) Start the project

yarn startCopy the code

Visit http://localhost:4000/graphql to see playground database operation interface. Can perform a range of database CRUD operations.

The front part

1) Create a project

npx create-react-app react-graphql-project --template typescriptCopy the code

Delete useless files after the project is built.

2) WebPack needs to be configured

yarn add react-app-rewired customize-craCopy the code

Change the scripts startup command for the package.json file

"scripts": {
  "start": "react-app-rewired start"."build": "react-app-rewired build"."test": "react-app-rewired test"
}Copy the code

Then create a new config-overrides. Js file in the root directory to do the webpack configuration.

Install the front-end UI component library antD and configure on-demand loading, path alias support, and so on.

yarn add antd babel-plugin-import Copy the code

The config – overrides. Js:

const { override, fixBabelImports, addWebpackAlias } = require('customize-cra');
const path = require('path')

module.exports = override(
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: 'css'
  }),
  addWebpackAlias({
    "@": path.resolve(__dirname, "src/")}))Copy the code

The tconfig.json file also needs to be configured because the TS cannot recognize it.

Create a paths.json file

{
  "compilerOptions": {
    "baseUrl": "."."paths": {
      "@ / *": ["src/*"]}}}Copy the code

Change the tconfig. Json

{
  "compilerOptions": {
    "target": "es5"."lib": [
      "dom"."dom.iterable"."esnext"]."allowJs": true."skipLibCheck": true."esModuleInterop": true."allowSyntheticDefaultImports": true."strict": true."forceConsistentCasingInFileNames": true."module": "esnext"."moduleResolution": "node"."resolveJsonModule": true."isolatedModules": true."noEmit": true."jsx": "react"
  },
  "include": [
    "./src/**/*"]."extends": "./paths.json"
}Copy the code

It takes effect after the project is restarted.

4) Business development

Entry file index.tsx:

import React from 'react';
import ReactDOM from 'react-dom';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';
import App from './router';
import * as serviceWorker from './serviceWorker'; // create Apollo client const client = new ApolloClient({uri:'http://localhost:4000/graphql'
})

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>, document.getElementById('root'));
serviceWorker.unregister();Copy the code

Routing file router.js:

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Spin } from 'antd'; // Layouts = lazy(() => import('@/components/layouts'));
const ProductList = lazy(() => import('@/pages/productlist'));
const ProductDetail = lazy(() => import('@/pages/productdetail'));


const RouterComponent = () => {
  return (
    <Router>
      <Suspense fallback={<Spin size="large" />}>
        <Layouts>
          <Switch>
            <Route path="/" exact={true} component={ProductList}></Route>
            <Route path="/detail/:id" component={ProductDetail}></Route>
          </Switch>
        </Layouts>
      </Suspense>
    </Router>
  )
};


export default RouterComponent;Copy the code

Define the type file types.tsx:

exportinterface Category{ id? : string; name? : string; products: Array<Product> }exportinterface Product{ id? :string; name? : string; category? : Category; categoryId? : string | []; }Copy the code

Develop the layout component SRC/Components /layouts

import React from 'react';
import { Layout, Menu } from 'antd';
import { Link } from 'react-router-dom';

const { Header, Content, Footer } = Layout

const Layouts: React.FC = (props) => (
  <Layout className="layout">
    <Header>
      <div className="logo" />
      <Menu
        theme="dark"
        mode="horizontal"
        defaultSelectedKeys={['1']}
        style={{ lineHeight: '64px' }}
      >
        <Menu.Item key="1"><Link to="/"></ Link></ menu. Item> </Menu> </Header> <Content style={{padding:'50px 50px 0 50px' }}>
      <div style={{ background: '#fff', padding: 24, minHeight: 280 }}>
        {props.children}
      </div>
    </Content>
    <Footer style={{ textAlign: 'center'}}> ©2019 Created by zhangyanling. </Footer> </Layout>)export default Layouts;Copy the code

Define GQL query statement file api.tsx:

import { gql } from 'apollo-boost';

exportconst GET_PRODUCTS = gql` query{ getProducts{ id name category{ id name products{ id name } } } } `; // Query all categories and products on the screenexportconst CATEGORIES_PRODUCTS = gql` query{ getCategories{ id name products{ id name } } getProducts{ id name category{ id name products{ id name } } } } `; // Add productsexport const ADD_PRODUCT = gql`
  mutation($name:String! .$categoryId:String!) { addProduct(name:$name, category: $categoryId){ id name category{ id name } } } `; // Delete the product by idexport const DELETE_PRODUCT = gql`
  mutation($id: String!) { deleteProduct(id:$id){ id name } } `; // Query the merchandise details and corresponding merchandise categories by idexport const GET_PRODUCT = gql`
  query($id: String!) { getProduct(id:$id){
      id
      name
      category{
        id
        name
        products{
          id
          name
        }
      }
    }
  }
`;Copy the code

Develop ProductList component:

You can display, delete, and add products in the product list.

import React, { useState } from 'react';
import { Table, Modal, Row, Col, Button, Divider, Tag, Form, Input, Select, Popconfirm } from 'antd';
import { Link } from 'react-router-dom';
import { useQuery, useMutation } from '@apollo/react-hooks';
import { CATEGORIES_PRODUCTS, GET_PRODUCTS, ADD_PRODUCT, DELETE_PRODUCT } from '@/api';
import { Product, Category } from '@/types'; const { Option } = Select; /** * const ProductList: react. FC = () => {/** * const ProductList: react. FC = () => {let [visible, setVisible] = useState<boolean>(false);
  let [pageSize, setPageSize] = useState<number|undefined>(10);
  let [current, setCurrent] = useState<number|undefined>(1)
  const { loading, error, data } = useQuery(CATEGORIES_PRODUCTS);
  const [deleteProduct] = useMutation(DELETE_PRODUCT);
 
  if(error) return<p> Load error </p>;if(loading) return<p> Loading... </p>; const { getCategories, getProducts } = data const confirm = async (event? :any, record? :Product) => { // console.log("Details", record); await deleteProduct({ variables: { id: record? .id }, refetchQueries: [{ query: GET_PRODUCTS }] })setCurrent(1)
  }
   
  const columns = [
    {
      title: "Product ID",
      dataIndex: "id"
    },
    {
      title: "Trade Name",
      dataIndex: "name"
    },
    {
      title: "Commodity Classification",
      dataIndex: "category",
      render: (text: any) => {
        let color = ' '
        const tagName = text.name;
        if(tagName === 'dress'){
          color = 'red'
        } else if(tagName === 'food') {
          color = 'green'
        } else if(tagName === 'digital'){
          color = 'blue'
        } else if(tagName === 'mother'){
          color = 'purple'
        }
        return (
          <Tag color={color}>{text.name}</Tag>
        )
      }
    },
    {
      title: "Operation",
      render: (text: any, record: any) => (
        <span>
          <Link to={`/detail/${record.id}'}> Details </Link> {/* <Dividertype="vertical" /> */}
          {/* <a style={{color: 'orange'}}> Modify </a> */} <Dividertype="vertical" />
          <Popconfirm
            title="Are you sure?"
            onConfirm={(event) => confirm(event, record)}
            okText="Sure"
            cancelText="Cancel"
          >
            <a style={{color:'red'</a> </Popconfirm> </span>)}]; const handleOk = () => {setVisible(false)
  }

  const handleCancel = () => {
    setVisible(false) } const handleChange = (pagination: { current? :number, pageSize? :number}) => { const { current, pageSize } = paginationsetPageSize(pageSize)
    setCurrent(current)
  }

  return (
    <div>
      <Row style={{padding: '0 0 20px 0'}}>
        <Col span={24}>
          <Button type="primary" onClick={() => setVisible(true<Row> <Row> <Col span={24}> <Table columns={columns} dataSource={getProducts} rowKey="id" 
            pagination={{
              current: current,
              pageSize: pageSize,
              showSizeChanger: true,
              showQuickJumper: true, total: data.length }} onChange={handleChange} /> </Col> </Row> { visible && <AddForm handleOk={handleOk} HandleCancel ={handleCancel} categories={getCategories} />} </div>)} /** * New Modal */ interface FormProps { handleOk: any, handleCancel: any, categories: Array<Category> } const AddForm:React.FC<FormProps> = ({handleOk, handleCancel, categories}) => {let [product, setProduct] = useState<Product>({ name: ' ', categoryId: [] });
  let[addProduct] = useMutation(ADD_PRODUCT); Const handleSubmit = async () => {// Get await addProduct({variables: product, refetchQueries: [{query: GET_PRODUCTS}]}) // Clear the formsetProduct({ name: ' ', categoryId: [] })
    handleOk()
  }
  
  return (
    <Modal
      title="New Product"
      visible={true}
      onOk={handleSubmit}
      okText="Submit"
      cancelText="Cancel"
      onCancel={handleCancel}
      maskClosable={false}
    >
      <Form>
        <Form.Item label="Trade Name">
          <Input 
            placeholder="Please enter" 
            value={product.name} 
            onChange={event => setProduct({ ... product, name: event.target.value })} /> </Form.Item> <Form.Item label="Commodity Classification">
          <Select 
            placeholder="Please select" 
            value={product.categoryId} 
            onChange={(value: string | []) => setProduct({ ... product, categoryId: value })} > { categories.map((item: Category) => ( <Option key={item.id} value={item.id}>{item.name}</Option> )) } </Select> </Form.Item> </Form> </Modal> ) }export default ProductList;Copy the code

Develop ProductDetail component:

Query the details of a commodity and all commodities under its category according to its ID.

import React from 'react';
import { Card, List } from 'antd';
import { useQuery } from '@apollo/react-hooks';
import { GET_PRODUCT } from '@/api';
import { Product } from '@/types';

const ProductDetail: React.FC = (props:any) => {
  let _id = props.match.params.id;
  let { loading, error, data } = useQuery(GET_PRODUCT,{
    variables: { id: _id }
  });

  if(error) return<p> Load error </p>;if(loading) return<p> Loading... </p>; const { getProduct } = data; const { id, name, category: { id: categoryId, name: categoryName, products }} = getProduct;return (
    <div>
      <Card title="Details of Commodity" bordered={false} style={{width:'100%'}} > < div > < p > < b > commodity ID: < / b > {ID} < / p > < p > < b > name of commodity: < / b > {name} < / p > < / div > < List header = {< div > < p > < b > category ID: </b>{categoryId}</ P >< p><b> </b>{categoryName}</p> </div> } footer={null} bordered dataSource={products} renderItem={(item:Product) => ( <List.Item>  <p>{item.name}</p> </List.Item> )} > </List> </Card> </div> ) }export default ProductDetail;Copy the code

Renderings are displayed

Merchandise list page



The new goods



Delete the goods



Goods details