部署上線的内容在付費的平台上,大家有興趣可以支援一波。
大綱
一、Egg.js 基礎入門
1、Egg.js 開發環境搭建及生成項目目錄講解
2、了解 Egg.js 的路由機制
3、編寫簡單的 GET 和 POST 接口
4、Egg.js 中如何使用前端模闆
二、React 編寫日記界面
1、React 開發環境搭建接入 Ant Design Mobile
2、通過 vw 适配移動端方案
3、日記清單頁開發
4、日記詳情頁開發
5、日記編輯頁面開發
三、Egg.js 服務端開發
1、本地安裝 Mysql 資料庫
2、Navicat 操作資料庫建立日記表
3、編寫添加日記接口、更新日記接口
4、編寫擷取日記清單接口、擷取日記詳情接口、删除日記接口
5、聯調接口
四、總結
Egg.js 基礎入門
簡介
Egg.js 是啥呀?雞蛋嗎?開個小玩笑。Egg.js 是基于 Koa 的上層架構,簡單說就是 Egg.js 是基于 Koa 二次開發的後端 node 解決方案。截止目前(2020-01-06) Egg 的最新版本為
v2.26.0
,Github 上的星星居高不下,目前已達到了14.6k+之多。可見大家對 Egg 的喜愛程度。
那麼為什麼我會選擇 Egg 作為服務端的開發架構,而不選擇 nest、Think.js、hapi等架構呢?首先 Egg 是阿裡團隊開發的,國内首屈一指的大廠。你不必擔心這個架構的生态,更不用擔心它會被停止維護,因為阿裡内部很多系統也是在使用這個架構制作的。其次 Egg 在文檔上做的不錯,中英文文檔對國人非常友好,說實話本人英文能力有限,雖說看看英文文檔問題不大,但是多少看起來還是有點吃力。遇到問題的時候,還能去社群或者技術群裡喊幾句,遇到類似問題的朋友也會不惜餘力的支援你。(普通小開發 不喜輕噴)
還有一個很重要的原因,Egg 繼承于 Koa,在它的基礎模型上,做了一些增強,在寫法上可以說是十分便捷。相比之下 Koa 還是基礎了,太多東西需要二次封裝。在之後的開發中你會見識到 Egg 的強大之處。
Egg.js 開發環境搭建及生成項目目錄講解
我的環境:
- 作業系統:macOS
- node版本:12.6.0
- npm 版本: 6.9.0
通過如下腳本初始化項目:
mkdir egg-demo && cd egg-demo
npm init egg
// 選擇 simple 模式的
npm install
如果 npm 不能使用的話建議安裝 yarn
初始化項目目錄如下如所示:
項目檔案結構分析
這裡我挑重要的講,因為有些開發中我們也不常去修改,不用浪費太多的精力去了解,當然有興趣的小夥伴自己可以研究透徹一些。
- app 檔案夾:我們的主邏輯幾乎都會在這個檔案夾内完成,controller 是控制器檔案夾,主要寫一些業務代碼,之後會在 app 檔案夾裡建立一個 service 檔案夾,專門用來操作資料庫,讓業務邏輯和資料庫操作分而治之,代碼會清晰很多。
- public檔案夾:公用檔案夾,把一些公用資源都放在這個檔案夾下。
- config 檔案夾: 這裡存放一些項目的配置,如跨域的配置、資料庫的配置等等。
- logs檔案夾: 日志檔案夾,正常情況下不用修改和檢視裡邊内容。
- run檔案夾:運作項目時,生成的配置檔案,基本不修改裡邊的檔案。
- test 檔案夾: 測試使用的一些配置檔案,測試接口的時候會用到。
- .auto.conf.js: 項目自動生成的檔案,一般不需要修改。
- .eslintignore和.eslintrc: 代碼格式化配置檔案。
- .gitignore: git 送出的時候忽略的檔案。
- package.json: 包管理和指令配置檔案,開發時需要經常修改。
Egg.js 目錄約定規範
Koa 之是以不适合團隊項目的開發,是因為它缺少規範。Egg.js 在基于 Koa 的基礎上制定了一些規範,是以我們放置一些腳本檔案的時候,是要按照 Egg.js 的規範來的。
app/router.js
是放置路由的地方
public
檔案夾放置一些公共資源如圖檔、公用的腳本等
app/service
檔案夾放置資料庫操作的内容
view
檔案夾自然是放置前端模闆的地方
middleware
是放置中間件的地方,這個很重要,鑒權等操作可以通過中間件的形式加入到路由,俗稱路由守衛
還有挺多一些規範就不在此一一例舉了,大家可以移步官方文檔中文文檔非常友好,向深入研究的同學可以挑燈夜讀一番。
說了這麼多好像忘記一件事情,咱們啟動一下項目看看呗。在啟動之前我們修改一點内容:
// /app/controller/home.js
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
ctx.body = 'hi, egg';
}
async test() {
const { ctx } = this;
ctx.body = '測試接口';
}
}
module.exports = HomeController;
// app/router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/test', controller.home.test);
};
到項目根目錄啟動項目,指令行如下:
npm run dev
// 或者
yarn dev
正常情況下,Egg.js 預設啟動 7001 端口,看到下圖所示說明項目啟動成功了。
我們通過浏覽器檢視如下所示:
我們在
/app/controller/home.js
檔案中寫的
test
方法成功被執行。
了解 Egg.js 的路由機制
路由(Router)主要用來描述請求 URL 和具體承擔執行的 Controller 的對應關系,Egg.js 約定了
app/router.js
檔案用于統一所有路由規則。
簡單來說,上述例子,我們在
app/controller/home.js
裡寫了
test
方法,然後在
app/router.js
檔案中将
test
方法以 GET 的形式抛出。這便是 URL 和 Controller 的對應關系。Egg.js 的友善就是展現在上下文已經為我們打通了,app 便是全局應用的上下文。路由和控制器都存放在全局應用上下文 app 裡,是以你隻需要關心你的業務邏輯和資料庫操作便可,無需再為其他瑣碎小事分心。
控制器(Controller)内主要編寫業務邏輯,我們來了解一下如何命名,比如我現在希望建立一個與使用者相關的控制器,我們可以這麼寫:
// 在 app/controller/ 下建立 user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async index() {
const { ctx } = this;
ctx.body = '使用者';
}
}
module.exports = UserController;
首字母大寫駝峰命名,UserController 繼承 Controller ,内部可以使用 async、await 的方式編寫函數。
編寫簡單的 GET 和 POST 接口
上面其實已經簡單的寫了如何編寫 GET 接口,我們在這裡就再加點别的知識點,擷取路由上的查詢參數,即
/user?username=nick
問好後面的便是查詢參數,通過如下代碼擷取:
// 在 app/controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async index() {
const { ctx } = this;
const { username } = ctx.query;
ctx.body = username;
}
}
module.exports = UserController;
注意需要添加路由參數
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/test', controller.home.test);
router.get('/user', controller.user.index);
};
再去浏覽器通路一下,看看能否展示查詢參數:
還有一種擷取申明參數,可以通過
ctx/params
的方式擷取到:
// 在 app/controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async index() {
const { ctx } = this;
const { username } = ctx.query;
ctx.body = username;
}
async getid() {
const { ctx } = this;
const { id } = ctx.params;
ctx.body = id;
}
}
module.exports = UserController;
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/test', controller.home.test);
router.get('/user', controller.user.index);
router.get('/getid/:id', controller.user.getid);
};
如圖所示,
getid/999
後面的 999,被作為
ctx.params
裡面的 id 被傳回給了網頁。
GET 講完我們再講講 POST,開發項目時,我們在需要操作内容的時候便會使用到 POST 形式的接口,因為我們可能要傳的資料包比較大,這裡就不細說 GET 和 POST 接口的差別了,不然就變成面試課程了。真的要說我就說一句,它們沒差別,都是基于 TCP 協定。
來看看 POST 接口在 Egg 中的應用,在上面說到的
app/controller/user.js
内添加一個方法:
...
async add() {
const { ctx } = this;
const { title, content } = ctx.request.body;
// 架構内置了 bodyParser 中間件來對這兩類格式的請求 body 解析成 object 挂載到 ctx.request.body 上
// HTTP 協定中并不建議在通過 GET、HEAD 方法通路時傳遞 body,是以我們無法在 GET、HEAD 方法中按照此方法擷取到内容。
ctx.body = {
title,
content,
};
}
...
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/test', controller.home.test);
router.get('/user', controller.user.index);
router.get('/getid/:id', controller.user.getid);
router.post('/add', controller.user.add);
};
浏覽器不友善請求 POST 接口,我們借助 Postman 來發送 POST 請求,沒有下載下傳的同學可以下載下傳一個,對于開發來說 Postman 可以說是必備的工具,測試接口非常友善。當你點選 Postman 發送請求的時候,你會接收不到傳回,因為請求跨域了,那麼我們需要通過
egg-cors
這個 npm 包來解決跨域問題。首先安裝它,然後在
config/plugin.js
中引入如下所示:
// config/plugin.js
'use strict';
exports.cors = {
enable: true,
package: 'egg-cors',
};
然後在
config/config.default.js
中加入如下代碼:
// config/config.default.js
config.security = {
csrf: {
enable: false,
ignoreJSON: true,
},
domainWhiteList: [ '*' ], // 配置白名單
};
config.cors = {
// origin: '*', //允許所有跨域通路,注釋掉則允許上面 白名單 通路
credentials: true, // 允許 Cookie 跨域
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
};
我目前配置的是全部可通路。然後再重新啟動項目,打開 Postman 請求 add 接口如下所示,注意請求體需要
JSON(Application/json)
形式:
說到這裡,不得不提 Service 服務。我們上面的接口業務邏輯都是放在 Controller 裡面,若是我需要操作資料庫的情況,我們就需要把操作資料庫的方法放在 Service 裡。
首先我們建立檔案夾
app/controller/service
,在檔案夾内建立
user.js
代碼如下:
'use strict';
const Service = require('egg').Service;
class UserService extends Service {
async user() {
return {
title: '你媽貴姓',
content: '免貴姓李',
};
}
}
module.exports = UserService;
然後去
app/controller/user.js
裡進行調用:
...
async index() {
const { ctx } = this;
const { title, content } = await ctx.service.user.user();
ctx.body = {
title,
content,
};
}
...
// app/router.js
...
router.post('/getUser', controller.user.index);
每次在控制器内新增方法,一定不要忘記在
router,js
内增加路由。
目前還沒連接配接資料庫,姑且先将就着這麼寫,真實連接配接資料庫,會在 service 檔案夾内建立一些資料庫相關操作的腳本,後續的内容會說明。
Egg.js 中如何使用前端模闆
若是有同學需要制作簡單的靜态頁,類似公司的官網、宣傳頁等,可以考慮使用前端模闆來編寫頁面。
首先我們安裝模闆插件
egg-view-ejs
:
npm install egg-view-ejs -save
然後在
config/plugin.js
裡面聲明需要用到的插件
exports.ejs = {
enable: true,
package: 'egg-view-ejs',
};
接着我們需要去
config/config.default.js
裡配置
ejs
,這一步我們會将
.ejs
的字尾改成
.html
的字尾。
config.view = {
mapping: {'.html': 'ejs'} //左邊寫成.html字尾,會自動渲染.html檔案
};
在
app
目錄下建立
view
檔案夾,并且建立一個
index.html
檔案如下所示:
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%-title%></title>
</head>
<body>
<!-- 使用模闆資料 -->
<h1><%-title%></h1>
</body>
</html>
修改
app/controller/home.js
腳本如下所示:
// app/controller/home.js
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
const { ctx } = this;
// index.html 預設回去 view 檔案夾尋找,Egg 已經封裝好這一層了
await ctx.render('index.html', {
title: '你媽貴姓',
});
}
async test() {
const { ctx } = this;
ctx.body = '測試接口';
}
}
module.exports = HomeController;
重新開機整個項目,浏覽器檢視
http://localhost:7001
如下圖所示:
title
變量已經被加載進來,模闆正常顯示。
到這一步同學們順利的跟下來,基本上對 Egg 有了一個大緻的了解,當然光了解這些基礎知識不足以完成整個項目的編寫,但是基礎還是很重要的嘛,畢竟 Egg 是基于 Koa 二次封裝的,很多内置的設定項需要通過小用例去熟悉,希望同學們不要偷懶,跟完上面的内容,最好是不要複制粘貼,逐行的去敲完才能真正的變成自己的知識。
React 編寫日記界面
簡介
自 React 16.8 釋出之後,React 引入了 Hooks 寫法,即函數元件内支援狀态管理。什麼概念呢,就是我們在用 React 寫代碼的時候,幾乎可以抛棄之前的 Class 寫法。之是以說是“幾乎”,是因為有些地方還是需要用到 Class 寫法,但是 React 的作者 Dan 說了,“Hooks 将會是 React 的未來” 。那麼我們這回就全程使用 Hooks 寫法,把日記項目敲一遍。
React 開發環境搭建接入 Ant Design Mobile
本次課程的 React 環境,我們采用官方提供的
create-react-app
來初始化,如果你的
npm
版本大于 5.2 ,那麼可以使用以下指令行初始化項目:
npx create-react-app diary
cd diary
npm run start
啟動成功的話,預設是啟動 3000 端口,打開浏覽器輸入 http://localhost:3000 會看到如下頁面:
清除
diary
項目
src
目錄下的一些檔案,最後的目錄結構如下圖所示:
下面我們來引入
Ant Design Mobile
,首先我們需要把它下載下傳到項目來,打開指令行工具再項目根目錄輸入下列指令:
npm install antd-mobile --save
然後在
diary/src/index.js
引入 and 的樣式檔案:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import 'antd-mobile/dist/antd-mobile.css';
ReactDOM.render(<App />, document.getElementById('root'));
然後在
diary/src/App.js
内引入一個元件測試一下:
// App.js
import React from 'react';
import { Button } from 'antd-mobile';
function App() {
return (
<div className="App">
<Button type='primary'>測試</Button>
</div>
);
}
export default App;
然後重新開機一下項目,打開浏覽器啟動移動端模式檢視效果:
移動端網頁在點選的時候,會有 300 毫秒延遲,是以我們需要在
diary/public/index.html
檔案内加入一段腳本代碼:
// index.html
...
<script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script>
<script>
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
if(!window.Promise) {
document.writeln('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"'+'>'+'<'+'/'+'script>');
}
</script>
...
antd 的樣式是可以通過按需加載的,如果想學習按需加載的同學,可以移步到 官網學習如何引入
通過 vw 适配移動端方案
衆所周知,移動端的分辨率千變萬化,我們很難去完美的适配到每一種分辨率下頁面能完美的展示。做不到完美,起碼也要努力的去做到一個大緻,通過 vw 去适配移動端的分辨率。它能将頁面内的 px 機關轉化為 vw vh,來适應手機多變的分辨率問題。不想做适配的同學也可以跳過這一步,繼續下面的學習。
首先我們需要将項目隐藏的 webpack 配置放出來,通過如下指令行:
npm run eject
運作完成之後,項目目錄結構如下圖所示:
多了兩個配置項,如圖所示。若是運作無法執行的話,建議先将項目的
npm run eject
檔案删除,
.git
,然後再次運作
rm -rf .git
。
npm run eject
然後再安裝幾個插件,指令如下所示:
npm install postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-cssnext postcss-viewport-units cssnano cssnano-preset-advanced
安裝完成之後,打開
diary/config/webpack.config.js
腳本,去修改
postcss
的 loader 插件。
首先引入上面安裝好的包,可以放在第 28 行下面:
// 28 行
const postcssNormalize = require('postcss-normalize');
const postcssAspectRatioMini = require('postcss-aspect-ratio-mini');
const postcssPxToViewport = require('postcss-px-to-viewport');
const postcssWriteSvg = require('postcss-write-svg');
const postcssCssnext = require('postcss-cssnext');
const postcssViewportUnits = require('postcss-viewport-units');
const cssnano = require('cssnano');
const appPackageJson = require(paths.appPackageJson);
然後去 100 行開始添加 postcss 的一些配置:
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
postcssAspectRatioMini({}),
postcssPxToViewport({
viewportWidth: 750, // 針對 iphone6 的設計稿
viewportHeight: 1334, // 針對 iphone6 的設計稿
unitPrecision: 3,
viewportUnit: 'vw',
selectorBlackList: ['.ignore', '.hairlines', 'am'], // 這裡添加 am 是因為引入了 antd-mobile 元件庫,否則元件庫内的機關都會被改為 vw 機關,樣式會亂
minPixelValue: 1,
mediaQuery: false
}),
postcssWriteSvg({
utf8: false
}),
postcssCssnext({}),
postcssViewportUnits({}),
cssnano({
preset: "advanced",
autoprefixer: false,
"postcss-zindex": false
})
],
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
添加完之後重新開機項目,通過浏覽器檢視機關是否變化:
同理,其他的元件庫也可以通過這種形式适配移動端項目,不過要注意一下 selectorBlackList 屬性需要添加一下相應的元件庫名字,避開轉化為 vw
日記清單頁開發
一頓操作之後,接下來将開發一些頁面,不過在開發頁面之前,我們需要添加路由機制。通過
react-router-dom
插件控制項目的路由,先來安裝它:
npm i react-router-dom -save
然後我們修改一下目錄結構,首先在
src
目錄下建立
Home
檔案夾,在檔案夾内建立
index.jsx
和
style.css
,内容如下:
// Home/index.jsx
import React from 'react'
import './style.css'
const Home = () => {
return (
<div>
Home
</div>
)
}
export default Home
接下來我們編輯路由配置頁面,路由的原理其實就是頁面通過浏覽器位址的變化,動态的加載浏覽器位址所對應的元件頁面。打個比方,我現在給
/
首頁配置一個
Home
元件,那麼當浏覽器通路
http://localhost:3000
的時候,頁面會渲染對應的
Home
元件。那麼我們先把
App.js
改為
Router.js
代碼如下:
// Router.js
import React from 'react';
import Home from './Home';
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
const RouterMap = () => {
return <Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
</Switch>
</Router>
}
export default RouterMap;
稍作解釋,
Switch
的表現和
JavaScript
中的
switch
差不多,即當比對到相應的路由時,不再往下比對。我們會在
src/index.js
腳本内引入這個
RouterMap
,具體代碼如下所示:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import RouterMap from './Router';
import 'antd-mobile/dist/antd-mobile.css';
ReactDOM.render(<RouterMap />, document.getElementById('root'));
然後重新開機項目,檢視浏覽器表現:
我們在
Home
元件内編寫日記項目的首頁,首頁我們會以一個清單的形式展示,那麼我們可以用到
antd
中的
Card
卡片元件,我們看看代碼如何實作:
// Home/index.jsx
import React from 'react'
import { Card } from 'antd-mobile'
import './style.css'
const list = [0,1,2,3,4,5,6,7,8,9]
const Home = () => {
return (
<div className='diary-list'>
{
list.map(item => <Card className='diary-item'>
<Card.Header
title="我和小明去捉迷藏"
thumb="https://gw.alipayobjects.com/zos/rmsportal/MRhHctKOineMbKAZslML.jpg"
extra={<span>晴天</span>}
/>
<Card.Body>
<div>{item}</div>
</Card.Body>
<Card.Footer content="2020-01-09" />
</Card>)
}
</div>
)
}
export default Home
// Home/style.css
.diary-list .diary-item {
margin-bottom: 20px;
}
.diary-item .am-card-header-content {
flex: 7 1;
}
可以通過浏覽器查詢元素如修改元件内部的樣式,如通過
.am-card-header-content
修改标題的寬度。元件庫的合理使用,有助于工作效率的提升。這個頁面雖然簡單,但是也算是一個抛磚引玉的作用,大家可以對
atnd
這一套元件庫進行細緻的研究,在工作中業務需求分析的時候,能做到融會貫通,升職加薪指日可待。
日記詳情頁開發
在
src
目錄下建立一個
Detail
檔案夾,我們來編寫詳情頁面:
// Detail/index.jsx
import React from 'react'
import { NavBar, Icon, List } from 'antd-mobile'
const Detail = () => {
return (<div className='diary-detail'>
<NavBar
mode="light"
icon={<Icon type="left" />}
onLeftClick={() => console.log('onLeftClick')}
>我和小明捉迷藏</NavBar>
<List renderHeader={() => '2020-01-09 晴天'} className="my-list">
<List.Item wrap>
今天我和小明去西湖捉迷藏,
小明會潛水,躲進了湖底,我在西湖邊找了半天都沒找到,
後來我就回家了,不跟他嘻嘻哈哈的了。
</List.Item>
</List>
</div>)
}
export default Detail
在頭部使用了
NavBar
導航欄标簽,展示标題以及傳回按鈕。内容選擇
List
清單元件,簡單的展示日記的内容部分。不要忘記了去
Router.js
路由腳本裡加上
Detail
的路由:
const RouterMap = () => {
return <Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route exact path="/detail">
<Detail />
</Route>
</Switch>
</Router>
}
浏覽器輸入
http://localhost:3000/detail
檢視效果:
我們将首頁清單和詳情頁面聯系在一起,實作點選首頁清單項,跳轉到對應的詳情頁面,将 id 參數帶到路由裡,然後在詳情頁面通過篩選拿到浏覽器查詢字元串的 id 參數。我們先修改首頁的代碼:
import React from 'react'
import { Card } from 'antd-mobile'
import { Link } from 'react-router-dom'
import './style.css'
const list = [0,1,2,3,4,5,6,7,8,9]
const Home = () => {
return (
<div className='diary-list'>
{
list.map(item => <Link to={{ pathname: 'detail', search: `?id=${item}` }}><Card className='diary-item'>
<Card.Header
title="我和小明去捉迷藏"
thumb="https://gw.alipayobjects.com/zos/rmsportal/MRhHctKOineMbKAZslML.jpg"
extra={<span>晴天</span>}
/>
<Card.Body>
<div>{item}</div>
</Card.Body>
<Card.Footer content="2020-01-09" />
</Card></Link>)
}
</div>
)
}
export default Home
引入
Link
标簽,将
Card
元件包裹起來,通過
to
屬性設定跳轉路徑和附帶在路徑上的參數如上述代碼所示。接下來我們在
Detail
元件内接受這個參數,我們通過編寫工具方法來擷取想要的參數,在
src
下建立一個檔案夾
utils
,在檔案夾内建立
index.js
腳本,代碼如下所示:
function getQueryString(name) {
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if(r != null) {
return unescape(r[2]);
} else{
return null
};
}
module.exports = {
getQueryString
}
此方法為擷取浏覽器查詢字元串的方法,接下來打開
Detail
元件,引入
utils
擷取
getQueryString
方法,同時我們在詳情頁裡需要點選回退按鈕,Hooks 寫法
react-router-dom
為我們提供了
useHistory
方法來實作回退,具體代碼圖下所示:
// Detail/index.jsx
import React from 'react'
import { NavBar, Icon, List } from 'antd-mobile'
import { useHistory } from 'react-router-dom'
import { getQueryString } from '../utils'
const Detail = () => {
const history = useHistory()
const id = getQueryString('id')
return (<div className='diary-detail'>
<NavBar
mode="light"
icon={<Icon type="left" />}
onLeftClick={() => history.goBack()}
>我和小明捉迷藏{id}</NavBar>
<List renderHeader={() => '2020-01-09 晴天'} className="my-list">
<List.Item wrap>
今天我和小明去西湖捉迷藏,
小明會潛水,躲進了湖底,我在西湖邊找了半天都沒找到,
後來我就回家了,不跟他嘻嘻哈哈的了。
</List.Item>
</List>
</div>)
}
export default Detail
擷取到
id
屬性後,将它顯示在标題上,我們來看看浏覽器的效果:
日記編輯頁面開發
和小明玩了十天捉迷藏之後,我覺得十分無聊。我們還是趕緊把編輯頁面寫了,加點有意思的日記資訊。老套路,我們在
src
目錄下建立
Edit
檔案夾,開始編寫我們的日記輸入元件:
// Detail/index.jsx
import React, { useState } from 'react'
import { List, InputItem, TextareaItem, DatePicker, ImagePicker } from 'antd-mobile'
import './style.css'
const Edit = () => {
const [date, setDate] = useState()
const [files, setFile] = useState([])
const onChange = (files, type, index) => {
console.log(files, type, index);
setFile(files)
}
return (<div className='diary-edit'>
<List renderHeader={() => '編輯日記'}>
<InputItem
clear
placeholder="請輸入标題"
>标題</InputItem>
<TextareaItem
rows={6}
placeholder="請輸入日記内容"
/>
<DatePicker
mode="date"
title="請選擇日期"
extra="請選擇日期"
value={date}
onChange={date => setDate(date)}
>
<List.Item arrow="horizontal">日期</List.Item>
</DatePicker>
<ImagePicker
files={files}
onChange={onChange}
onImageClick={(index, fs) => console.log(index, fs)}
selectable={files.length < 1}
multiple={false}
/>
</List>
</div>)
}
export default Edit
// Detail/style.css
.diary-edit {
height: 100vh;
background: #fff;
}
上述代碼,添加了四塊内容,分别是标題、内容、日期、圖檔。元件之間的搭配純屬自己安排,同學們可以按照自己喜歡的排版布局進行設定,注意編寫完之後一定要去路由頁面添加路由位址:
// Router.js
import React from 'react';
import Home from './Home';
import Detail from './Detail';
import Edit from './Edit';
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
const RouterMap = () => {
return <Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route exact path="/detail">
<Detail />
</Route>
<Route exact path="/edit">
<Edit />
</Route>
</Switch>
</Router>
}
export default RouterMap;
然後去浏覽器預覽一下界面如何:
接下來又可以記錄和小紅的快樂故事了呢~~
Egg.js 服務端開發
還記得最開始我們建立的
egg-demo
項目嗎?我們就用那個項目進行服務端開發的工作。我們第一件要做的事情就是在本地安裝一下
MySQL
資料庫,如何安裝傾聽我細細道來。
本地安裝 Mysql 資料庫
1、下載下傳安裝 MySQL
進入 MySQL 官網 下載下傳 MySQL 資料庫社群版
請選擇适合自己的版本,筆者是 MacOS 系統,是以選擇第一個安裝包,注意選擇不登入下載下傳
下載下傳完成之後,按照導航提示進行安裝,進行到 root 使用者配置密碼時,一定要記住密碼,後面會用到的:
安裝完成之後,可以進入系統便好設定這邊啟動資料庫:
Navicat 操作資料庫建立日記表
圖形界面對于新手來說,是非常友好的。對資料庫的可視化操作,能提高新手的工作效率,筆者使用的這款 Navicat for MySQL 是一款輕量級的資料庫可視化工具,這裡不提供下載下傳位址,因為怕被起訴侵權。大家可以去網上自己搜一下下載下傳資源,還是很多的,這點能力大家還是要培養起來。
在啟動資料庫的情況下,我們打開 Navicat 工具連結本地資料庫,如圖所示:
儲存之後,在左側清單會有測試資料庫項,連結資料庫成功後會變成綠色:
我們能看到,我本地資料庫的版本号和端口号,這樣我們就連結上了本地資料庫了,接下來我們開始建立 diary 資料庫和建立表:
建立表的時候大家注意,我們先填寫表的字段名稱,儲存之後再填寫表的名稱。在寫字端的時候,大家注意選擇字端的字元集,選擇
utf8mb4
,否則不支援中文輸入:
這裡一定要把 id 字端設定為自增,且作為主鍵:
然後點選左上角的儲存按鈕 ,儲存這張表。我們在 diary 表内添加一條記錄:
到這裡,我們的資料庫工作差不多結束了,有不明白的同學也可以私信我,我會親自為你們排憂解難。
接下來我們可以打開
egg-demo
項目,要連結資料庫的話,我們需要安裝一個
egg-mysql
包,在項目根目錄下運作如下指令行:
npm i --save egg-mysql
開啟插件:
// config/plugin.js
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
// config/config.default.js
exports.mysql = {
// 單資料庫資訊配置
client: {
// host
host: 'localhost',
// 端口号
port: '3306',
// 使用者名
user: 'root',
// 密碼
password: '******',
// 資料庫名
database: 'diary',
},
// 是否加載到 app 上,預設開啟
app: true,
// 是否加載到 agent 上,預設關閉
agent: false,
};
密碼需要填寫上面讓你記住的那個密碼
我們去
`server
檔案夾建立一個檔案
diary.js
添加一個搜尋清單的方法:
// server/diary.js
'use strict';
const Service = require('egg').Service;
class DiaryService extends Service {
async list() {
const { app } = this;
try {
const result = await app.mysql.select('diary');
return result;
} catch (error) {
console.log(error);
return null;
}
}
}
module.exports = DiaryService;
然後在
controller/home.js
裡引用添加一個新的擷取日記清單的方法:
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
async list() {
const { ctx } = this;
const result = await ctx.service.diary.list();
if (result) {
ctx.body = {
status: 200,
data: result,
};
} else {
ctx.body = {
status: 500,
errMsg: '擷取失敗',
};
}
}
}
module.exports = HomeController;
要注意,每次添加新的方法的時候,都需要去路由檔案裡添加相應的接口:
// router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/list', controller.home.list);
};
此時重新開機項目運作如下指令行:
npm run dev
順利啟動之後,去浏覽器擷取一下這個接口,看是否能請求到資料,成功的擷取如下:
這個時候,多少會有點成就感,那麼我們就一撮而就,把其他幾個接口都寫了。
添加日記接口
添加接口,我們需要使用 POST 的請求方式,前面已經說過了 POST 如何擷取請求體傳入的參數,這裡就不贅述了。我們直接來寫接口,首先打開
service/diary.js
腳本添加
add
方法:
async add(params) {
const { app } = this;
try {
const result = await app.mysql.insert('diary', params);
return result;
} catch (error) {
console.log(error);
return null;
}
}
然後再去
controller/home.js
腳本裡添加接口操作:
async add() {
const { ctx } = this;
const params = {
...ctx.request.body,
};
const result = await ctx.service.diary.add(params);
if (result) {
ctx.body = {
status: 200,
data: result,
};
} else {
ctx.body = {
status: 500,
errMsg: '添加失敗',
};
}
}
然後再去
router.js
路由腳本裡,加一個路由配置:
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/list', controller.home.list);
router.post('/add', controller.home.add);
};
POST 接口需要通過 Postman 測試:
添加成功之後,就傳回該條記錄相應的 id 等資訊,我們再來看看擷取清單是不是會有上面天添加的資料:
這個時候必然是成功的,添加接口就這樣完成了。
修改日記接口
首先我們分析一下,修改一篇日記的話,我們要先找到它的 id ,因為 id 是主鍵,通過 id 我們來更新該條記錄的字段。那麼我們先去
service/diary.js
添加一個資料庫操作的方法:
async update(params) {
const { app } = this;
try {
const result = await app.mysql.update('diary', params);
return result;
} catch (error) {
console.log(error);
return null;
}
}
然後打開
contoller/home.js
添加修改方法:
async update() {
const { ctx } = this;
const params = {
...ctx.request.body,
};
const result = await ctx.service.diary.update(params);
if (result) {
ctx.body = {
status: 200,
data: result,
};
} else {
ctx.body = {
status: 500,
errMsg: '編輯失敗',
};
}
}
最後去
router.js
添加接口配置:
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/list', controller.home.list);
router.post('/add', controller.home.add);
router.post('/update', controller.home.update);
};
去 Postman 修改第二條記錄:
成功修改第二條記錄。
擷取文章詳情接口
我們首先需要拿到 id 字段,去查詢相對應的 id 的記錄内容,還是去
service/diary.js
添加接口:
async diaryById(id) {
const { app } = this;
if (!id) {
console.log('id不能為空');
return null;
}
try {
const result = await app.mysql.select('diary', {
where: { id },
});
return result;
} catch (error) {
console.log(error);
return null;
}
}
controller/home.js
async getDiaryById() {
const { ctx } = this;
console.log('ctx.params', ctx.params);
const result = await ctx.service.diary.diaryById(ctx.params.id);
if (result) {
ctx.body = {
status: 200,
data: result,
};
} else {
ctx.body = {
status: 500,
errMsg: '擷取失敗',
};
}
}
router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/list', controller.home.list);
router.post('/add', controller.home.add);
router.post('/update', controller.home.update);
router.get('/detail/:id', controller.home.getDiaryById);
};
删除接口
删除接口就比較簡單了,找到對應的 id 記錄,删除即可:
service/diary.js
async delete(id) {
const { app } = this;
try {
const result = await app.mysql.delete('diary', { id });
return result;
} catch (error) {
console.log(error);
return null;
}
}
controller/home.js
async delete() {
const { ctx } = this;
const { id } = ctx.request.body;
const result = await ctx.service.diary.delete(id);
if (result) {
ctx.body = {
status: 200,
data: result,
};
} else {
ctx.body = {
status: 500,
errMsg: '删除失敗',
};
}
}
router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.get('/list', controller.home.list);
router.post('/add', controller.home.add);
router.post('/update', controller.home.update);
router.get('/detail/:id', controller.home.getDiaryById);
router.post('/delete', controller.home.delete);
};
删除之後,隻剩下 id 為 2 的記錄,那麼接口部分基本上都完成了,我們去前端對接相應的接口。
聯調接口
前端的老本行,調試接口來了。我們切換到
diary
前端項目,先安裝
axios
:
npm i axios --save
然後在
utils
檔案夾内添加一個腳本
axios.js
,我們來二次封裝一下它。之是以要二次封裝,是因為我們在統一處理接口傳回的時候,可以在一個地方處理,而不用到各個請求傳回的地方去修改。
// utils/axios.js
import axios from 'axios'
import { Toast } from 'antd-mobile'
// 根據 process.env.NODE_ENV 環境變量判斷開發環境還是生産環境,我們服務端本地啟動的端口是 7001
axios.defaults.baseURL = process.env.NODE_ENV == 'development' ? '//localhost:7001' : ''
// 表示跨域請求時是否需要使用憑證
axios.defaults.withCredentials = false
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
// post 請求是 json 形式的
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.interceptors.response.use(res => {
if (typeof res.data !== 'object') {
console.error('資料格式響應錯誤:', res.data)
Toast.fail('服務端異常!')
return Promise.reject(res)
}
if (res.data.status != 200) {
if (res.data.message) Toast.error(res.data.message)
return Promise.reject(res.data)
}
return res.data
})
export default axios
完成二次封裝之後記得将 axios
抛出來。
接下來就是去首頁請求清單接口了,打開
src/Home/index.jsx
:
// src/Home/index.jsx
import React, { useState, useEffect } from 'react'
import { Card } from 'antd-mobile'
import { Link } from 'react-router-dom'
import axios from '../utils/axios'
import './style.css'
const Home = () => {
// 通過 useState Hook 函數定義 list 變量
const [list, setList] = useState([])
useEffect(() => {
// 請求 list 接口,傳回清單資料
axios.get('/list').then(({ data }) => {
setList(data)
})
}, [])
return (
<div className='diary-list'>
{
list.map(item => <Link to={{ pathname: 'detail', search: `?id=${item.id}` }}><Card className='diary-item'>
<Card.Header
title={item.title}
thumb={item.url}
extra={<span>晴天</span>}
/>
<Card.Body>
<div>{item.content}</div>
</Card.Body>
<Card.Footer content={item.date} />
</Card></Link>)
}
</div>
)
}
export default Home
.diary-list .diary-item {
margin-bottom: 20px;
}
.diary-item .am-card-header-content {
flex: 7 1;
}
.diary-item .am-card-header-content img {
width: 30px;
}
打開浏覽器,輸入
http://localhost:3000
顯示如下圖所示:
詳情頁編寫
接下來我們來到詳情頁的編寫,打開
src/Detail/index.jsx
:
import React, { useState, useEffect } from 'react'
import { NavBar, Icon, List } from 'antd-mobile'
import { useHistory } from 'react-router-dom'
import { getQueryString } from '../utils'
import axios from '../utils/axios'
const Detail = () => {
const [detail, setDetail] = useState({})
const history = useHistory()
const id = getQueryString('id')
useEffect(() => {
axios.get(`/detail/${id}`).then(({ data }) => {
if (data.length) {
setDetail(data[0])
}
})
}, [])
return (<div className='diary-detail'>
<NavBar
mode="light"
icon={<Icon type="left" />}
onLeftClick={() => history.goBack()}
>{detail.title || ''}</NavBar>
<List renderHeader={() => `${detail.date} 晴天`} className="my-list">
<List.Item wrap>
{detail.content}
</List.Item>
</List>
</div>)
}
export default Detail
編輯頁面
添加文章頁面,我們打開
src/Edit/index.jsx
:
import React, { useState } from 'react'
import { List, InputItem, TextareaItem, DatePicker, ImagePicker, Button, Toast } from 'antd-mobile'
import moment from 'moment'
import axios from '../utils/axios'
import './style.css'
const Edit = () => {
const [title, setTitle] = useState('') // 标題
const [content, setContent] = useState('') // 内容
const [date, setDate] = useState('') // 日期
const [files, setFile] = useState([]) // 圖檔檔案
const onChange = (files, type, index) => {
console.log(files, type, index);
setFile(files)
}
const publish = () => {
if (!title || !content || !date) {
Toast.fail('請填寫必要參數')
return
}
const params = {
title,
content,
date: moment(date).format('YYYY-MM-DD'),
url: files.length ? files[0].url : ''
}
axios.post('/add', params).then(res => {
Toast.success('添加成功')
})
}
return (<div className='diary-edit'>
<List renderHeader={() => '編輯日記'}>
<InputItem
clear
placeholder="請輸入标題"
onChange={(value) => setTitle(value)}
>标題</InputItem>
<TextareaItem
rows={6}
placeholder="請輸入日記内容"
onChange={(value) => setContent(value)}
/>
<DatePicker
mode="date"
title="請選擇日期"
extra="請選擇日期"
value={date}
onChange={date => setDate(date)}
>
<List.Item arrow="horizontal">日期</List.Item>
</DatePicker>
<ImagePicker
files={files}
onChange={onChange}
onImageClick={(index, fs) => console.log(index, fs)}
selectable={files.length < 1}
multiple={false}
/>
<Button type='primary' onClick={() => publish()}>釋出</Button>
</List>
</div>)
}
export default Edit
注意,因為我沒買 cdn 服務,是以沒有資源上傳接口,故這裡的圖檔我們就采用 base64 存儲。
添加成功之後,浏覽清單頁面。
删除謀篇文章
我們需要在詳情頁加個按鈕,因為我們沒有背景管理系統,按理說這個删除按鈕需要放在背景管理頁面,但是為了友善我就都寫在一個項目裡了,因為日記都是給自己看的,這就是為什麼我說寫的是日記項目而不是部落格項目的原因,其實名字一變,這就是一個部落格項目。
我們将删除按鈕放在詳情頁看,打開
src/Detail/index.jsx
,在頭部的右邊位置加一個删除按鈕,代碼如下:
import React, { useState, useEffect } from 'react'
import { NavBar, Icon, List } from 'antd-mobile'
import { useHistory } from 'react-router-dom'
import { getQueryString } from '../utils'
import axios from '../utils/axios'
const Detail = () => {
const [detail, setDetail] = useState({})
const history = useHistory()
const id = getQueryString('id')
useEffect(() => {
axios.get(`/detail/${id}`).then(({ data }) => {
if (data.length) {
setDetail(data[0])
}
})
}, [])
const deleteDiary = (id) => {
axios.post('/delete', { id }).then(({ data }) => {
// 删除成功之後,回到首頁
history.push('/')
})
}
return (<div className='diary-detail'>
<NavBar
mode="light"
icon={<Icon type="left" />}
onLeftClick={() => history.goBack()}
rightContent={[
<Icon onClick={() => deleteDiary(detail.id)} key="0" type="cross-circle-o" />
]}
>{detail.title || ''}</NavBar>
<List renderHeader={() => `${detail.date} 晴天`} className="my-list">
<List.Item wrap>
{detail.content}
</List.Item>
</List>
</div>)
}
export default Detail
修改文章
修改文章,隻需拿到文章的 id ,然後将修改的參數一并傳給修改接口便可,我們先給詳情頁加一個修改按鈕,打開
src/Detail/index.jsx
,再加一段代碼
<NavBar
mode="light"
icon={<Icon type="left" />}
onLeftClick={() => history.goBack()}
rightContent={[
<Icon style={{ marginRight: 10 }} onClick={() => deleteDiary(detail.id)} key="0" type="cross-circle-o" />,
<img onClick={() => history.push(`/edit?id=${detail.id}`)} style={{ width: 26 }} src="//s.weituibao.com/1578721957732/Edit.png" alt=""/>
]}
>{detail.title || ''}</NavBar>
上述代碼加了一個 img 标簽,點選之後跳轉到編輯頁面,順便把相應的 id 帶上。我們可以在編輯頁面通過 id 去擷取詳情,指派給變量再進行編輯,我們打開
src/Edit/index.jsx
頁面:
import React, { useState, useEffect } from 'react'
import { List, InputItem, TextareaItem, DatePicker, ImagePicker, Button, Toast } from 'antd-mobile'
import moment from 'moment'
import axios from '../utils/axios'
import { getQueryString } from '../utils'
import './style.css'
const Edit = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [date, setDate] = useState('')
const [files, setFile] = useState([])
const id = getQueryString('id')
const onChange = (files, type, index) => {
console.log(files, type, index);
setFile(files)
}
useEffect(() => {
if (id) {
axios.get(`/detail/${id}`).then(({ data }) => {
if (data.length) {
setTitle(data[0].title)
setContent(data[0].content)
setDate(new Date(data[0].date))
setFile([{ url: data[0].url }])
}
})
}
}, [])
const publish = () => {
if (!title || !content || !date) {
Toast.fail('請填寫必要參數')
return
}
const params = {
title,
content,
date: moment(date).format('YYYY-MM-DD'),
url: files.length ? files[0].url : ''
}
if (id) {
params['id'] = id
axios.post('/update', params).then(res => {
Toast.success('修改成功')
})
return
}
axios.post('/add', params).then(res => {
Toast.success('添加成功')
})
}
return (<div className='diary-edit'>
<List renderHeader={() => '編輯日記'}>
<InputItem
clear
placeholder="請輸入标題"
value={title}
onChange={(value) => setTitle(value)}
>标題</InputItem>
<TextareaItem
rows={6}
placeholder="請輸入日記内容"
value={content}
onChange={(value) => setContent(value)}
/>
<DatePicker
mode="date"
title="請選擇日期"
extra="請選擇日期"
value={date}
onChange={date => setDate(date)}
>
<List.Item arrow="horizontal">日期</List.Item>
</DatePicker>
<ImagePicker
files={files}
onChange={onChange}
onImageClick={(index, fs) => console.log(index, fs)}
selectable={files.length < 1}
multiple={false}
/>
<Button type='primary' onClick={() => publish()}>釋出</Button>
</List>
</div>)
}
export default Edit
擷取到詳情之後,展示在輸入頁面。
整個項目前後端流程都已經跑通了,雖然資料庫隻有一張表,但是作為程式員,需要有舉一反三的能力。當然如果想要把項目做的更複雜些,需要一些資料庫設計的基礎。
總結
萬字長文,看到最後的朋友想必也是熱愛學習,希望提高自己的人。全文涉及到的知識點可能會比較粗略,但是還是那句老話,師父領進門,修行靠個人。更多好文可以關注我的 個人部落格 還要我的 知乎專欄 。有問題可以添加我的個人部落格裡的微信群,學習讨論。這篇長文寫到我吐血,希望對大家有所幫助。
轉自https://zhuanlan.zhihu.com/p/104136110