寫在前面

這是筆者在中秋無聊寫著玩的,假期閒暇之餘憋出來的帖子。麻雀雖小,但五臟俱全,涉及到的方方面面還是蠻全的。所以就設計了一個黛夢(demo)------ 打通了GraphQL的介面與前端互動的流程,並且將資料存入MYSQL,分享下React和GraphQL的使用,大致內容如下:

  • GraphQL的增刪改查介面設計與實現
  • CRUD包mysql的使用
  • React 和 React Hooks的使用

因為涉及到React、GraphQL,還有MySQL的一張使用者表User,所以我本來是想起一個“搞人實驗”的名字,後來斟酌了一下,啊著,太粗暴了。還是文藝點,詩意點,就叫它”黛夢“吧,哈哈哈哈哈哈。

這邊文章著重介紹GraphQL的使用,關於它的一些概念煩請看我去年寫的這篇文章,GraphQL的基礎實踐------ https://segmentfault.com/a/1190000021895204

技術實現

技術選型

最近在用taro寫h5和小程式,混個臉熟,所以前端這邊我選用React,因為黛夢也不是很大,所以沒必要做前後端分離,用html刀耕火種意思下得了。後端這塊是Node結合express和GraphQL做的介面,資料庫用的是MySQL。

GraphQL的介面設計

我們先拋開GraphQL,就單純的介面而言。比如說抽象出一個User類,那麼我們對其進行的操作不外乎增刪改查對吧。然後我們再帶上GraphQL,結合已知的業務邏輯去熟悉新技術那麼我們可以這麼一步一步來,一口氣是吃不成胖子的。

  • 先定義使用者實體和相應的介面,不做細節實現,訪問相應的介面能返回相應的預期
  • 定義一個全域性變數(或者寫進一個檔案)去模仿資料庫操作,返回相應的結果
  • 結合資料庫去實現細節,訪問相應的介面能返回相應的預期

全域性變數Mock資料庫的實現

  • 第一步:導包

    const express = require('express');
    const { buildSchema } = require('graphql');
    const { graphqlHTTP } = require('express-graphql');

    上面分別倒入了相應的包,express用來建立相應的HTTP伺服器,buildSchema用來建立相應的型別、Query和Mutation的定義。graphqlHTTP用來將相應的實現以中介軟體的形式注入到express中。

  • 第二步: 定義全域性變數

    const DB = {
    userlist: [],
    };

    這裡定義一個全域性變數去模仿資料庫操作

  • 第三步:定義相應的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
    }
    `);

    這裡定義了使用者輸入的型別以及使用者的型別,然後Query中的getUsers模擬的是返回使用者列表的介面,返回User實體的列表集。Mutation是對其進行修改、刪除、新增等操作。這裡createUser接收一個UserInput的輸入,然後返回一個User型別的資料,updateUser接受一個ID型別的id,然後一個UserInput型別的user

  • 第四步:對樓上Schema的Query和Mutation的實現

    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;
    },
    };
  • 第五步: 建立伺服器並暴露想要的埠

    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

    開啟 http://localhost:3000/api/graphql,可以在playground貼上下樓下的測試用例試一下

    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

結合MySQL的實現

這裡就不像樓上一樣展開了,直接貼程式碼吧

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 {
"獲取所有使用者"
getUsers: [User]
"獲取單個使用者資訊"
getUser(id: ID!): User
}
type Mutation {
"建立使用者"
createUser(user: UserInput): Int
"更新使用者"
updateUser(id: ID!, user: UserInput): Int
"刪除使用者"
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');
});

這裡跟全域性變數不同的是,我這邊對所有欄位和方法增加了相應的註釋(GraphQL就是好, 介面即文件),然後封裝了mysql資料庫的操作方法,引入後去實現相關的介面。

MYSQL增刪改查的封裝

這裡簡單點,我們期望是傳入一條SQL和相應的引數,返回相應的執行結果。

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,
};

這裡匯入了Mysql這個npm包,在它的基礎上建立了一個連線池,然後暴露一個cmd方法,它返回一個Promise物件,是我們上面傳入sql和引數的結果。

檔案地址如下:https://gitee.com/taoge2021/study-nodejs/blob/master/07-graphql/express/01-graphql/db.js

有的時候我們寫程式碼,不可能一次就寫成我們想要的結果,比如可能寫錯了一個單詞啊,或者引數什麼,所以這裡需要對增刪改查的sql做測試,具體的如下:

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
// }
// ]
// }

在測試完成後,我們就可以放心地引入到express和graphql的專案中去了。額,這裡的伺服器我就不避諱打星號了,快到期了,有需要的同學可以連上去測試下,這裡用的也是測試伺服器和賬號哈哈哈,沒關係的。

相關的query檔案在這:https://gitee.com/taoge2021/study-nodejs/blob/master/07-graphql/express/01-graphql/server-4.query

貼張圖

React的前端設計

關於React專案的搭建,可以看下我之前寫的這篇文章:https://www.cnblogs.com/cnroadbridge/p/13358136.html

在React中,我們可以通過Class和Function的方式建立元件,前者通過Class建立的元件,具有相應的生命週期函式,而且有相應的state, 而後者通過Function建立的更多的是做展示用。自從有了React Hooks之後,在Function建立的元件中也可以用state了,元件間的複用更加優雅,程式碼更加簡潔清爽了,它真的很靈活。Vue3中的組合式API,其實思想上有點React Hooks的味道。

構思頁面

根據後端這邊提供的介面,這裡我們會有張頁面,裡面有通過列表介面返回的資料,它可以編輯和刪除資料,然後我們有一個表單可以更新和新增資料,簡單的理一下,大致就這些吧。

  • 增刪改查介面的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)
    });
    })
    }

    上面通過自帶的fetch請求,分別實現了對給出的graphql介面的相關請求

  • UserPage頁面元件

      // 頁面
    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>);
    };

    這裡用了兩個React Hooks的鉤子, useState使得函式元件可以像Class元件一樣可以使用state, useEffect它接受兩個引數,第一個是函式,第二個是一個數組,陣列中的元素的變化會觸發這個鉤子的函式的執行。

  • 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單條資料項元件

      // 資料項
    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

總結

刀耕火種的時代已然是離我們很遠,人類文明發展到現在已然是可以用微波爐煤氣灶燒飯做菜,上面的例子只是介紹了GraphQL的使用,並且結合React打通了這樣一個流程。實際上在開發中,我們往往會採用社群一些成熟的技術棧,比如你需要進一步瞭解GraphQL,可以去了解下Apollo這個庫。那麼前後端的架構就可以是 react-apollo,vue-apollo, 後端的話比如express-apollo,koa-apollo等等。我們在學開車的時候,往往是學手動擋的帕薩特,而在買汽車的時候,往往是喜歡買自動擋的輝騰,因為它比較符合人類文明的發展趨勢,雖然外表上看上去和帕薩特差不多,但是自動擋著實是文明的進步啊!