zoukankan      html  css  js  c++  java
  • 基于graphql+react hooks+apollo+antd(前端)、后端koa+mongodb+graphql的项目实践

    1、项目背景

    源于2019年11月16日成都Web全栈大会上尹吉峰老师的GraphQL的分享,让我产生了浓厚的兴趣。几经研究、学习,做了个实践的小项目。

    学习资料:

    https://graphql.cn/learn/

    https://typescript.bootcss.com/basic-types.html

    https://www.apollographql.com/docs/react/

    就代码做以下分析。

    2、项目目录

    项目分为前端和后端两部分(目录client和server)。如图所示。

     使用技术栈:

     client: react hooks + typescript + apollo + graphql + antd

     server: koa2 + graphql + koa-graphql + mongoose

    3、项目搭建及源码实现

    数据库部分

    使用的是mongodb数据库,这里对于该数据库的安装等不做赘述。

    默认已经 具备mongodb的环境。启动数据库。

    到mongodb安装路径下,如C:Program FilesMongoDBServer4.2in

    打开终端,执行命令:

    mongod --dbpath=./data

    创建项目总目录:react-graphql-project,并进入目录。

    后端部分

    1)创建项目

    mkdir server && cd server
    npm init -y

    2) 安装项目依赖

    yarn add koa koa-grphql koa2-cors koa-mount koa-logger graphql

    3) 配置启动命令

    package.json文件

    {
      "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"
      }
    }

    4)业务开发

    入口文件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'); // 日志输出
    const myGraphQLSchema = require('./schema');
    
    const app = new Koa();
    
    app.use(logger())
    
    app.use(cors({
      origin: '*',
      allowMethods: ['GET', 'POST', 'DELETE', 'PUT', 'OPTIONS']
    }))
    
    app.use(mount('/graphql', graphqlHTTP({
      schema: myGraphQLSchema,
      graphiql: true // 开启graphiql可视化操作ide
    })))
    
    app.listen(4000, () => {
      console.log('server started on 4000')
    })

    数据库连接,创建model文件 model.js:

    const mongoose = require('mongoose');
    
    const Schema = mongoose.Schema;
    // 创建数据库连接
    const conn = mongoose.createConnection('mongodb://localhost/graphql',{ useNewUrlParser: true, useUnifiedTopology: true });
    
    conn.on('open', () => console.log('数据库连接成功!'));
    
    conn.on('error', (error) => console.log(error));
    
    // 用于定义表结构
    const CategorySchema = new Schema({
      name: String
    });
    // 增删改查
    const CategoryModel = conn.model('Category', CategorySchema);
    
    const ProductSchema = new Schema({
      name: String,
      category: {
        type: Schema.Types.ObjectId, // 外键
        ref: 'Category'  
      }
    });
    
    const ProductModel = conn.model('Product', ProductSchema);
    
    module.exports = {
      CategoryModel,
      ProductModel
    }

    schema.js文件:

    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
    })

    5)启动项目

    yarn start

    访问 http://localhost:4000/graphql 看到数据库操作playground界面。可进行一系列数据库crud操作。

    前端部分

    1)创建项目

    npx create-react-app react-graphql-project --template typescript

    生成项目后删除无用的文件。

    2)  需要配置webpack

    yarn add react-app-rewired customize-cra

    更改package.json文件的scripts启动命令

    "scripts": {
        "start": "react-app-rewired start",
        "build": "react-app-rewired build",
        "test": "react-app-rewired test"
      }

    然后在根目录下新建config-overrides.js文件,以做webpack的相关配置。

    安装前端UI组件库antd,并配置按需加载、路径别名支持等。

    yarn add antd babel-plugin-import 

    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/")              
      })
    )

    因为ts无法识别,还需配置tconfig.json 文件。

    新建paths.json文件

    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "@/*": ["src/*"]
        }
      }
    }

    更改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"
    }

    重启项目后生效。

    3)安装其他项目依赖

    yarn add graphql apollo-boost @apollo/react-hooks
    yarn add react-router-dom @types/react-router-dom

    4) 业务开发

    入口文件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';
    // 创建apollo客户端
    const client = new ApolloClient({
      uri: 'http://localhost:4000/graphql'
    })
    
    ReactDOM.render(
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>, document.getElementById('root'));
    serviceWorker.unregister();

    路由文件router.js:

    import React, { Suspense, lazy } from 'react';
    import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
    import { Spin } from 'antd';
    
    // 懒加载组件
    const 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;

    定义类型文件types.tsx:

    export interface Category{
      id?: string;
      name?: string;
      products: Array<Product>
    }
    
    export interface Product{
      id?:string;
      name?: string;
      category?: Category;
      categoryId?: string | [];
    }

    开发布局组件 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;

    定义gql查询语句文件 api.tsx:

    import { gql } from 'apollo-boost';
    
    export const GET_PRODUCTS = gql`
      query{
        getProducts{
          id
          name
          category{
            id
            name
            products{
              id
              name
            }
          }
        }
      }
    `;
    
    // 查询所有的上屏分类和产品
    export const CATEGORIES_PRODUCTS = gql`
      query{
        getCategories{
          id
          name
          products{
            id
            name
          }
        }
        getProducts{
          id
          name
          category{
            id
            name
            products{
              id
              name
            }
          }
        }
      }
    `;
    
    // 添加产品
    export const ADD_PRODUCT = gql`
      mutation($name:String!, $categoryId:String!){
        addProduct(name: $name, category: $categoryId){
          id
          name
          category{
            id
            name
          }
        }
      }
    `;
    
    // 根据id删除产品
    export const DELETE_PRODUCT = gql`
      mutation($id: String!){
        deleteProduct(id: $id){
          id,
          name
        }
      }
    `;
    
    // 根据id查询商品详情及相应商品分类及所属分类全部商品
    export const GET_PRODUCT = gql`
      query($id: String!){
        getProduct(id: $id){
          id,
          name,
          category{
            id,
            name,
            products{
              id,
              name
            }
          }
        }
      }
    `;

    开发商品列表组件ProductList:

    已经实现商品列表展示、删除商品、新增商品等功能。

    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 = () => {
      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>加载发生错误</p>;
      if(loading) return <p>加载中...</p>;
      
      const { getCategories, getProducts } = data
    
      const confirm = async (event?:any, record?:Product) => {
        // console.log("详情",  record);
        await deleteProduct({
          variables: {
            id: record?.id
          },
          refetchQueries: [{
            query: GET_PRODUCTS
          }]
        })
        setCurrent(1)
      }
       
      const columns = [
        {
          title: "商品ID",
          dataIndex: "id"
        },
        {
          title: "商品名称",
          dataIndex: "name"
        },
        {
          title: "商品分类",
          dataIndex: "category",
          render: (text: any) => {
            let color = ''
            const tagName = text.name;
            if(tagName === '服饰'){
              color = 'red'
            } else if(tagName === '食品') {
              color = 'green'
            } else if(tagName === '数码'){
              color = 'blue'
            } else if(tagName === '母婴'){
              color = 'purple'
            }
            return (
              <Tag color={color}>{text.name}</Tag>
            )
          }
        },
        {
          title: "操作",
          render: (text: any, record: any) => (
            <span>
              <Link to={`/detail/${record.id}`}>详情</Link>
              {/* <Divider type="vertical" /> */}
              {/* <a style={{color: 'orange'}}>修改</a> */}
              <Divider type="vertical" />
              <Popconfirm
                title="确定删除吗?"
                onConfirm={(event) => confirm(event, record)}
                okText="确定"
                cancelText="取消"
              >
                <a style={{color:'red'}}>删除</a>
              </Popconfirm>
            </span>
          )
        }
      ];
    
      const handleOk = () => {
        setVisible(false)
      }
    
      const handleCancel = () => {
        setVisible(false)
      }
    
      const handleChange = (pagination: { current?:number, pageSize?:number}) => {
        // console.log(pagination)
        const { current, pageSize } = pagination
        setPageSize(pageSize)
        setCurrent(current)
      }
    
      return (
        <div>
          <Row style={{padding: '0 0 20px 0'}}>
            <Col span={24}>
              <Button type="primary" onClick={() => setVisible(true)}>新增</Button>
            </Col>
          </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>
      )
    }
    
    /**
     * 新增产品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 () => {
        // 获取表单的值
        await addProduct({
          variables: product,
          refetchQueries: [{
            query: GET_PRODUCTS
          }]
        })
        // 清空表单
        setProduct({ name: '', categoryId: [] })
        handleOk()
      }
      
      return (
        <Modal
          title="新增产品"
          visible={true}
          onOk={handleSubmit}
          okText="提交"
          cancelText="取消"
          onCancel={handleCancel}
          maskClosable={false}
        >
          <Form>
            <Form.Item label="商品名称">
              <Input 
                placeholder="请输入" 
                value={product.name} 
                onChange={event => setProduct({ ...product, name: event.target.value })} 
              />
            </Form.Item>
            <Form.Item label="商品分类">
              <Select 
                placeholder="请选择" 
                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;

    开发商品详情组件ProductDetail:

    根据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>加载发生错误</p>;
      if(loading) return <p>加载中...</p>;
     
      const { getProduct } = data; 
      const { id, name, category: { id: categoryId, name: categoryName, products }} = getProduct;
      
      return (
        <div>
          <Card title="商品详情" bordered={false} style={{'100%'}}>
            <div>
              <p><b>商品ID:</b>{id}</p>
              <p><b>商品名称:</b>{name}</p>
            </div>
            <List
              header={
                <div>
                  <p><b>分类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;

    4、图片展示

    商品列表页

    新增商品

     删除商品

     商品详情

  • 相关阅读:
    3里氏代换原则LSP
    2单一职责原则SRP
    1开放封闭原则OCP
    24访问者模式Visitor
    python json模块,处理json文件的读写
    python zip 绑定多个list
    python 字符串重复多次的技巧 *操作符
    python 刷新缓冲区,实时监测
    python os.getcwd 获取工作目录
    python datetime 获取时间
  • 原文地址:https://www.cnblogs.com/zyl-Tara/p/12103352.html
Copyright © 2011-2022 走看看