源於2019年11月16日成都Web全棧大會上尹吉峯老師的GraphQL的分享,讓我產生了濃厚的興趣。幾經研究、學習,作了個實踐的小項目。css
學習資料:html
https://typescript.bootcss.com/basic-types.htmlnode
https://www.apollographql.com/docs/react/react
就代碼作如下分析。webpack
項目分爲前端和後端兩部分(目錄client和server)。如圖所示。web
使用技術棧:mongodb
client: react hooks + typescript + apollo + graphql + antdtypescript
server: koa2 + graphql + koa-graphql + mongoose數據庫
使用的是mongodb數據庫,這裏對於該數據庫的安裝等不作贅述。
默認已經 具有mongodb的環境。啓動數據庫。
到mongodb安裝路徑下,如C:\Program Files\MongoDB\Server\4.2\bin
打開終端,執行命令:
mongod --dbpath=./data
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={{width:'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;
商品列表頁
新增商品
刪除商品
商品詳情