基于React和GraphQL的黛夢(mèng)設(shè)計(jì)與實(shí)現(xiàn)
寫(xiě)在前面
這是筆者在中秋無(wú)聊寫(xiě)著玩的,假期閑暇之余憋出來(lái)的帖子。麻雀雖小,但五臟俱全,涉及到的方方面面還是蠻全的。所以就設(shè)計(jì)了一個(gè)黛夢(mèng)(demo)------ 打通了GraphQL的接口與前端交互的流程,并且將數(shù)據(jù)存入MYSQL,分享下React和GraphQL的使用,大致內(nèi)容如下:
GraphQL的增刪改查接口設(shè)計(jì)與實(shí)現(xiàn) CRUD包mysql的使用 React 和 React Hooks的使用
因?yàn)樯婕暗絉eact、GraphQL,還有MySQL的一張用戶(hù)表User,所以我本來(lái)是想起一個(gè)“搞人實(shí)驗(yàn)”的名字,后來(lái)斟酌了一下,啊著,太粗暴了。還是文藝點(diǎn),詩(shī)意點(diǎn),就叫它”黛夢(mèng)“吧,哈哈哈哈哈哈。
這邊文章著重介紹GraphQL的使用,關(guān)于它的一些概念煩請(qǐng)看我去年寫(xiě)的這篇文章,GraphQL的基礎(chǔ)實(shí)踐------ https://segmentfault.com/a/1190000021895204
技術(shù)實(shí)現(xiàn)
技術(shù)選型
最近在用taro寫(xiě)h5和小程序,混個(gè)臉熟,所以前端這邊我選用React,因?yàn)轺靿?mèng)也不是很大,所以沒(méi)必要做前后端分離,用html刀耕火種意思下得了。后端這塊是Node結(jié)合express和GraphQL做的接口,數(shù)據(jù)庫(kù)用的是MySQL。
GraphQL的接口設(shè)計(jì)
我們先拋開(kāi)GraphQL,就單純的接口而言。比如說(shuō)抽象出一個(gè)User類(lèi),那么我們對(duì)其進(jìn)行的操作不外乎增刪改查對(duì)吧。然后我們?cè)賻螱raphQL,結(jié)合已知的業(yè)務(wù)邏輯去熟悉新技術(shù)那么我們可以這么一步一步來(lái),一口氣是吃不成胖子的。
先定義用戶(hù)實(shí)體和相應(yīng)的接口,不做細(xì)節(jié)實(shí)現(xiàn),訪問(wèn)相應(yīng)的接口能返回相應(yīng)的預(yù)期 定義一個(gè)全局變量(或者寫(xiě)進(jìn)一個(gè)文件)去模仿數(shù)據(jù)庫(kù)操作,返回相應(yīng)的結(jié)果 結(jié)合數(shù)據(jù)庫(kù)去實(shí)現(xiàn)細(xì)節(jié),訪問(wèn)相應(yīng)的接口能返回相應(yīng)的預(yù)期
全局變量Mock數(shù)據(jù)庫(kù)的實(shí)現(xiàn)
第一步:導(dǎo)包
const express = require('express');
const { buildSchema } = require('graphql');
const { graphqlHTTP } = require('express-graphql');上面分別導(dǎo)入了相應(yīng)的包,express用來(lái)創(chuàng)建相應(yīng)的HTTP服務(wù)器,buildSchema用來(lái)創(chuàng)建相應(yīng)的類(lèi)型、Query和Mutation的定義。graphqlHTTP用來(lái)將相應(yīng)的實(shí)現(xiàn)以中間件的形式注入到express中。
第二步:定義全局變量
const DB = {
userlist: [],
};這里定義一個(gè)全局變量去模仿數(shù)據(jù)庫(kù)操作
第三步:定義相應(yīng)的Schema
const schema = buildSchema(`
input UserInput {
name: String
age: Int
}
type User {
id: ID,
name: String,
age: Int
}
type Query {
getUsers: [User]
}
type Mutation {
createUser(user: UserInput): User
updateUser(id: ID!, user: UserInput): User
}
`);這里定義了用戶(hù)輸入的類(lèi)型以及用戶(hù)的類(lèi)型,然后Query中的getUsers模擬的是返回用戶(hù)列表的接口,返回User實(shí)體的列表集。Mutation是對(duì)其進(jìn)行修改、刪除、新增等操作。這里createUser接收一個(gè)UserInput的輸入,然后返回一個(gè)User類(lèi)型的數(shù)據(jù),updateUser接受一個(gè)ID類(lèi)型的id,然后一個(gè)UserInput類(lèi)型的user
第四步:對(duì)樓上Schema的Query和Mutation的實(shí)現(xiàn)
const root = {
getUsers() {
return DB.userlist || [];
},
createUser({ user }) {
DB.userlist.push({ id: Math.random().toString(16).substr(2), ...user });
return DB.userlist.slice(-1)[0];
},
updateUser({ id, user }) {
let res = null;
DB.userlist.forEach((item, index) => {
if (item.id === id) {
DB.userlist[index] = Object.assign({}, item, { id, ...user });
res = DB.userlist[index];
}
});
return res;
},
};第五步:創(chuàng)建服務(wù)器并暴露想要的端口
const app = express();
app.use(
'/api/graphql',
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
})
);
app.listen(3000, () => {
console.log('server is running in http://localhost:3000/api/graphql');
});文件地址:https://gitee.com/taoge2021/study-nodejs/blob/master/07-graphql/express/01-graphql/server-3.js
打開(kāi) http://localhost:3000/api/graphql,可以在playground粘貼下樓下的測(cè)試用例試一下
query {
getUsers {
id
name
age
}
}
mutation {
createUser(user: {name: "ataola", age: 18}) {
id
name
age
}
}
mutation {
updateUser(id: "5b6dd66772afc", user: { name: "daming", age: 24 }) {
id,
name,
age
}
}文件地址:https://gitee.com/taoge2021/study-nodejs/blob/master/07-graphql/express/01-graphql/server-3.query
結(jié)合MySQL的實(shí)現(xiàn)
這里就不像樓上一樣展開(kāi)了,直接貼代碼吧
const express = require('express');
const { buildSchema } = require('graphql');
const { graphqlHTTP } = require('express-graphql');
const { cmd } = require('./db');
const schema = buildSchema(`
input UserInput {
"姓名"
name: String
"年齡"
age: Int
}
type User {
"ID"
id: ID,
"姓名"
name: String,
"年齡"
age: Int
}
type Query {
"獲取所有用戶(hù)"
getUsers: [User]
"獲取單個(gè)用戶(hù)信息"
getUser(id: ID!): User
}
type Mutation {
"創(chuàng)建用戶(hù)"
createUser(user: UserInput): Int
"更新用戶(hù)"
updateUser(id: ID!, user: UserInput): Int
"刪除用戶(hù)"
deleteUser(id: ID!): Boolean
}
`);
const root = {
async getUsers() {
const { results } = await cmd('SELECT id, name, age FROM user');
return results;
},
async getUser({ id }) {
const { results } = await cmd(
'SELECT id, name, age FROM user WHERE id = ?',
[id]
);
return results[0];
},
async createUser({ user }) {
const id = Math.random().toString(16).substr(2);
const data = { id, ...user };
const {
results: { affectedRows },
} = await cmd('INSERT INTO user SET ?', data);
return affectedRows;
},
async updateUser({ id, user }) {
const {
results: { affectedRows },
} = await cmd('UPDATE user SET ? WHERE id = ?', [user, id]);
return affectedRows;
},
async deleteUser({ id }) {
const {
results: { affectedRows },
} = await cmd('DELETE FROM user WHERE id = ?', [id]);
return affectedRows;
},
};
const app = express();
app.use(
'/api/graphql',
graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
})
);
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));
app.listen(3000, () => {
console.log('server is running in http://localhost:3000/api/graphql');
});
這里跟全局變量不同的是,我這邊對(duì)所有字段和方法增加了相應(yīng)的注釋?zhuān)℅raphQL就是好, 接口即文檔),然后封裝了mysql數(shù)據(jù)庫(kù)的操作方法,引入后去實(shí)現(xiàn)相關(guān)的接口。
MYSQL增刪改查的封裝
這里簡(jiǎn)單點(diǎn),我們期望是傳入一條SQL和相應(yīng)的參數(shù),返回相應(yīng)的執(zhí)行結(jié)果。
const mysql = require('mysql');
const pool = mysql.createPool({
host: '122.51.52.169',
port: 3306,
user: 'ataola',
password: '123456',
database: 'test',
connectionLimit: 10,
});
function cmd(options, values) {
return new Promise((resolve, reject) => {
pool.getConnection(function (err, connection) {
if (err) {
reject(err);
} else {
connection.query(options, values, (err, results, fields) => {
if (err) {
reject(err);
} else {
resolve({ err, results, fields });
}
connection.release();
});
}
});
});
}
module.exports = {
cmd,
};
這里導(dǎo)入了Mysql這個(gè)npm包,在它的基礎(chǔ)上創(chuàng)建了一個(gè)連接池,然后暴露一個(gè)cmd方法,它返回一個(gè)Promise對(duì)象,是我們上面?zhèn)魅雜ql和參數(shù)的結(jié)果。
文件地址如下:https://gitee.com/taoge2021/study-nodejs/blob/master/07-graphql/express/01-graphql/db.js
有的時(shí)候我們寫(xiě)代碼,不可能一次就寫(xiě)成我們想要的結(jié)果,比如可能寫(xiě)錯(cuò)了一個(gè)單詞啊,或者參數(shù)什么,所以這里需要對(duì)增刪改查的sql做測(cè)試,具體的如下:
const { cmd } = require('./db');
// insert
// (async () => {
// const res = await cmd('INSERT INTO user SET ?', {
// id: 'beb77a48b7f9f',
// name: '張三',
// age: 100,
// });
// console.log(res);
// })();
// {
// error: null,
// results: OkPacket {
// fieldCount: 0,
// affectedRows: 1,
// insertId: 0,
// serverStatus: 2,
// warningCount: 0,
// message: '',
// protocol41: true,
// changedRows: 0
// },
// fields: undefined
// }
// delete
// (async () => {
// const res = await cmd('DELETE FROM user WHERE id = ?', ['beb77a48b7f9f']);
// console.log(res);
// })();
// {
// error: null,
// results: OkPacket {
// fieldCount: 0,
// affectedRows: 1,
// insertId: 0,
// serverStatus: 2,
// warningCount: 0,
// message: '',
// protocol41: true,
// changedRows: 0
// },
// fields: undefined
// }
// update
// (async () => {
// const res = await cmd('UPDATE user SET ? where id = ?', [
// { name: '大明', age: 25 },
// 'beb77a48b7f9f',
// ]);
// console.log(res);
// })();
// {
// error: null,
// results: OkPacket {
// fieldCount: 0,
// affectedRows: 1,
// insertId: 0,
// serverStatus: 2,
// warningCount: 0,
// message: '(Rows matched: 1 Changed: 1 Warnings: 0',
// protocol41: true,
// changedRows: 1
// },
// fields: undefined
// }
// select
// (async () => {
// const res = await cmd('SELECT id, name, age FROM user');
// console.log(res);
// })();
// {
// error: null,
// results: [ RowDataPacket { id: 'beb77a48b7f9f', name: '大明', age: 25 } ],
// fields: [
// FieldPacket {
// catalog: 'def',
// db: 'test',
// table: 'user',
// orgTable: 'user',
// name: 'id',
// orgName: 'id',
// charsetNr: 33,
// length: 765,
// type: 253,
// flags: 20483,
// decimals: 0,
// default: undefined,
// zeroFill: false,
// protocol41: true
// },
// FieldPacket {
// catalog: 'def',
// db: 'test',
// table: 'user',
// orgTable: 'user',
// name: 'name',
// orgName: 'name',
// charsetNr: 33,
// length: 765,
// type: 253,
// flags: 0,
// decimals: 0,
// default: undefined,
// zeroFill: false,
// protocol41: true
// },
// FieldPacket {
// catalog: 'def',
// db: 'test',
// table: 'user',
// orgTable: 'user',
// name: 'age',
// orgName: 'age',
// charsetNr: 63,
// length: 11,
// type: 3,
// flags: 0,
// decimals: 0,
// default: undefined,
// zeroFill: false,
// protocol41: true
// }
// ]
// }
// select
(async () => {
const res = await cmd('SELECT id, name, age FROM user WHERE id = ?', [
'beb77a48b7f9f',
]);
console.log(res);
})();
// {
// error: null,
// results: [ RowDataPacket { id: 'beb77a48b7f9f', name: '大明', age: 25 } ],
// fields: [
// FieldPacket {
// catalog: 'def',
// db: 'test',
// table: 'user',
// orgTable: 'user',
// name: 'id',
// orgName: 'id',
// charsetNr: 33,
// length: 765,
// type: 253,
// flags: 20483,
// decimals: 0,
// default: undefined,
// zeroFill: false,
// protocol41: true
// },
// FieldPacket {
// catalog: 'def',
// db: 'test',
// table: 'user',
// orgTable: 'user',
// name: 'name',
// orgName: 'name',
// charsetNr: 33,
// length: 765,
// type: 253,
// flags: 0,
// decimals: 0,
// default: undefined,
// zeroFill: false,
// protocol41: true
// },
// FieldPacket {
// catalog: 'def',
// db: 'test',
// table: 'user',
// orgTable: 'user',
// name: 'age',
// orgName: 'age',
// charsetNr: 63,
// length: 11,
// type: 3,
// flags: 0,
// decimals: 0,
// default: undefined,
// zeroFill: false,
// protocol41: true
// }
// ]
// }
在測(cè)試完成后,我們就可以放心地引入到express和graphql的項(xiàng)目中去了。額,這里的服務(wù)器我就不避諱打星號(hào)了,快到期了,有需要的同學(xué)可以連上去測(cè)試下,這里用的也是測(cè)試服務(wù)器和賬號(hào)哈哈哈,沒(méi)關(guān)系的。
相關(guān)的query文件在這:https://gitee.com/taoge2021/study-nodejs/blob/master/07-graphql/express/01-graphql/server-4.query
貼張圖

React的前端設(shè)計(jì)
關(guān)于React項(xiàng)目的搭建,可以看下我之前寫(xiě)的這篇文章:https://www.cnblogs.com/cnroadbridge/p/13358136.html
在React中,我們可以通過(guò)Class和Function的方式創(chuàng)建組件,前者通過(guò)Class創(chuàng)建的組件,具有相應(yīng)的生命周期函數(shù),而且有相應(yīng)的state, 而后者通過(guò)Function創(chuàng)建的更多的是做展示用。自從有了React Hooks之后,在Function創(chuàng)建的組件中也可以用state了,組件間的復(fù)用更加優(yōu)雅,代碼更加簡(jiǎn)潔清爽了,它真的很靈活。Vue3中的組合式API,其實(shí)思想上有點(diǎn)React Hooks的味道。
構(gòu)思頁(yè)面
根據(jù)后端這邊提供的接口,這里我們會(huì)有張頁(yè)面,里面有通過(guò)列表接口返回的數(shù)據(jù),它可以編輯和刪除數(shù)據(jù),然后我們有一個(gè)表單可以更新和新增數(shù)據(jù),簡(jiǎn)單的理一下,大致就這些吧。
增刪改查接口的query
function getUser(id) {
const query = `query getUser($id: ID!) {
getUser(id: $id) {
id,
name,
age
}
}`;
const variables = { id };
return new Promise((resolve, reject) => {
fetch('/api/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
.then((res) => res.json())
.then((data) => {
resolve(data);
});
})
}
function getUsers() {
const query = `query getUsers {
getUsers {
id,
name,
age
}
}`;
return new Promise((resolve, reject) => {
fetch('/api/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
query,
}),
})
.then((res) => res.json())
.then((data) => {
resolve(data)
});
});
}
function addUser(name, age) {
const query = `mutation createUser($user: UserInput) {
createUser(user: $user)
}`;
const variables = {
user: {
name, age
}
};
return new Promise((resolve, reject) => {
fetch('/api/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
query,
variables
}),
})
.then((res) => res.json())
.then((data) => {
resolve(data)
});
});
}
function updateUser(id, name, age) {
const query = `mutation updateUser($id: ID!, $user: UserInput) {
updateUser(id: $id, user: $user)
}`;
const variables = {
id,
user: {
name, age
}
};
return new Promise((resolve, reject) => {
fetch('/api/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
query,
variables
}),
})
.then((res) => res.json())
.then((data) => {
resolve(data)
});
});
}
function deleteUser(id) {
const query = `mutation deleteUser($id: ID!) {
deleteUser(id: $id)
}`;
const variables = {
id
};
return new Promise((resolve, reject) => {
fetch('/api/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
query,
variables
}),
})
.then((res) => res.json())
.then((data) => {
resolve(data)
});
})
}上面通過(guò)自帶的fetch請(qǐng)求,分別實(shí)現(xiàn)了對(duì)給出的graphql接口的相關(guān)請(qǐng)求
UserPage頁(yè)面組件
// 頁(yè)面
const UserPage = () => {
const [userList, setUserList] = React.useState([]);
const [userForm, setUserForm] = React.useState({ id: '', name: '', age: '', type: 'add' });
const [isReload, setReload] = React.useState(false)
const [id, setId] = React.useState('');
React.useEffect(() => {
refreshUserList();
}, []);
React.useEffect(() => {
if (isReload) {
refreshUserList();
}
setReload(false);
}, [isReload]);
React.useEffect(() => {
if (id) {
getUser(id).then(res => {
const { data: { getUser: user } } = res;
setUserForm({ type: 'edit', ...user });
})
}
}, [id]);
function refreshUserList() {
getUsers().then(res => {
const { data: { getUsers = [] } } = res;
setUserList(getUsers);
})
}
return (<div>
<UserList userList={userList} setReload={setReload} setId={setId} />
<UserOperator setUserForm={setUserForm} userForm={userForm} setReload={setReload} />
</div>);
};這里用了兩個(gè)React Hooks的鉤子, useState使得函數(shù)組件可以像Class組件一樣可以使用state, useEffect它接受兩個(gè)參數(shù),第一個(gè)是函數(shù),第二個(gè)是一個(gè)數(shù)組,數(shù)組中的元素的變化會(huì)觸發(fā)這個(gè)鉤子的函數(shù)的執(zhí)行。
UserList列表組件
const UserList = (props) => {
const { userList, setReload, setId } = props;
const userItems = userList.map((user, index) => {
return <UserItem key={user.id} user={user} setReload={setReload} setId={setId} />
});
return (<ul>{userItems}</ul>);
};UserItem單條數(shù)據(jù)項(xiàng)組件
// 數(shù)據(jù)項(xiàng)
const UserItem = (props) => {
const { user, setReload, setId } = props;
function handleDelete(id) {
deleteUser(id).then(res => {
const { data: { deleteUser: flag } } = res;
if (flag) {
setReload(true);
}
})
}
function handleEdit(id) {
setId(id);
}
return (<li>
{user.name}: {user.age}歲
<span className="blue pointer" onClick={() => handleEdit(user.id)}>編輯</span>
<span className="red pointer" onClick={() => handleDelete(user.id)}>刪除</span>
</li>);
};UserOperator 操作組件
// 新增
const UserOperator = (props) => {
const [id, setId] = React.useState('');
const [name, setName] = React.useState('');
const [age, setAge] = React.useState('');
const { setUserForm, userForm, setReload } = props;
function handleChange(e, cb) {
cb(e.target.value)
}
function handleSubmit() {
const { type } = userForm;
if (type === 'edit') {
updateUser(id, name, Number(age)).then(res => {
const { data: { updateUser: flag } } = res;
if (flag) {
setReload(true);
setId('');
setName('');
setAge('');
} else {
alert('更新失敗');
}
})
} else if (type === 'add') {
if (name && age) {
addUser(name, Number(age)).then(res => {
const { data: { createUser: flag } } = res;
if (flag) {
setReload(true);
setId('');
setName('');
setAge('');
} else {
alert('添加失敗');
}
});
}
}
setUserForm({ ...userForm, type: 'add' })
}
React.useEffect(() => {
const { id, name, age } = userForm
setId(id);
setName(name);
setAge(age);
}, [userForm]);
return (<div>
<span>姓名:</span><input type="text" value={name} onChange={e => handleChange(e, setName)} />
<span>年齡:</span><input type="number" value={age} onChange={e => handleChange(e, setAge)} />
<button onClick={() => handleSubmit()}>{BUTTON_MAP[userForm.type]}</button>
</div>)
}
根組件
const App = (props) => {
return (<div><h2>{props.title}</h2><UserPage /></div>);
};
const root = document.getElementById('root');
ReactDOM.render(<App title="A Simple GraphQL Demo With React Design By ataola, Have Fun!" />, root);
文件如下:https://gitee.com/taoge2021/study-nodejs/blob/master/07-graphql/express/01-graphql/public/index.html

總結(jié)
刀耕火種的時(shí)代已然是離我們很遠(yuǎn),人類(lèi)文明發(fā)展到現(xiàn)在已然是可以用微波爐煤氣灶燒飯做菜,上面的例子只是介紹了GraphQL的使用,并且結(jié)合React打通了這樣一個(gè)流程。實(shí)際上在開(kāi)發(fā)中,我們往往會(huì)采用社區(qū)一些成熟的技術(shù)棧,比如你需要進(jìn)一步了解GraphQL,可以去了解下Apollo這個(gè)庫(kù)。那么前后端的架構(gòu)就可以是 react-apollo,vue-apollo, 后端的話比如express-apollo,koa-apollo等等。我們?cè)趯W(xué)開(kāi)車(chē)的時(shí)候,往往是學(xué)手動(dòng)擋的帕薩特,而在買(mǎi)汽車(chē)的時(shí)候,往往是喜歡買(mǎi)自動(dòng)擋的輝騰,因?yàn)樗容^符合人類(lèi)文明的發(fā)展趨勢(shì),雖然外表上看上去和帕薩特差不多,但是自動(dòng)擋著實(shí)是文明的進(jìn)步?。?/p>
