项目背景
源于2019年11月16日成都Web全栈大会上尹吉峰老师的GraphQL的分享,让我产生了浓厚的兴趣。几经研究、学习,做了个实践的小项目。
学习资料:
typescript.bootcss.com/basic-types…
www.apollographql.com/docs/react/
附项目地址: github.com/zhangyanlin…
就代码做以下分析。
项目目录
项目分为前端和后端两部分(目录client和server)。如图所示,
使用技术栈:
client:react hooks + typescript + apollo + graphql + antd
server: koa2 + graphql + koa-graphql + mongoose
项目搭建及源码实现
数据库部分
使用的是mongodb数据库,这里对于该数据库的安装等不做赘述。
默认已经 具备mongodb的环境。启动数据库。
到mongodb安装路径下,如C:\Program Files\MongoDB\Server\4.2\bin
打开终端,执行命令:
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可视化操作playground
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"
}复制代码
重启项目后生效。
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" />
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{
category{
products{
// 查询所有的上屏分类和产品
export const CATEGORIES_PRODUCTS = gql`
query{
getCategories{
products{
getProducts{
category{
products{
// 添加产品
export const ADD_PRODUCT = gql`
mutation($name:String!, $categoryId:String!){
addProduct(name: $name, category: $categoryId){
category{
// 根据id删除产品
export const DELETE_PRODUCT = gql`
mutation($id: String!){
deleteProduct(id: $id){
// 根据id查询商品详情及相应商品分类及所属分类全部商品
export const GET_PRODUCT = gql`
query($id: String!){
getProduct(id: $id){
category{
products{
`;复制代码
开发商品列表组件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) => (
<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}) => {
const { current, pageSize } = pagination
setPageSize(pageSize)
setCurrent(current)
return (
<Row style={{padding: '0 0 20px 0'}}>
<Col span={24}>
<Button type="primary" onClick={() => setVisible(true)}>新增</Button>
</Col>
</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.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 (
<Card title="商品详情" bordered={false} style={{width:'100%'}}>
<p><b>商品ID:</b>{id}</p>
<p><b>商品名称:</b>{name}</p>
</div>
header={
<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;复制代码
效果图展示
商品列表页
新增商品
删除商品
商品详情