1、Egg.js 基礎入門javascript
一、Egg.js 開發環境搭建及生成項目目錄講解css
二、理解 Egg.js 的路由機制html
三、編寫簡單的 GET 和 POST 接口前端
四、Egg.js 中如何使用前端模板java
2、React 編寫日記界面node
一、React 開發環境搭建接入 Ant Design Mobilemysql
二、經過 vw 適配移動端方案react
三、日記列表頁開發webpack
四、日記詳情頁開發ios
五、日記編輯頁面開發
3、Egg.js 服務端開發
一、本地安裝 Mysql 數據庫
二、Navicat 操做數據庫建立日記表
三、編寫添加日記接口、更新日記接口
四、編寫獲取日記列表接口、獲取日記詳情接口、刪除日記接口
五、聯調接口
4、總結
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 的強大之處。
個人環境:
經過以下腳本初始化項目:
mkdir egg-demo && cd egg-demo
npm init egg
// 選擇 simple 模式的
npm install
複製代碼
若是 npm 不能使用的話建議安裝 yarn
初始化項目目錄以下如所示:
項目文件結構分析
這裏我挑重要的講,由於有些開發中咱們也不常去修改,不用浪費太多的精力去了解,固然有興趣的小夥伴本身能夠研究透徹一些。
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
方法成功被執行。
路由(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 接口,咱們在這裏就再加點別的知識點,獲取路由上的查詢參數,即 /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/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-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 lang="en">
<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 16.8 發佈以後,React 引入了 Hooks 寫法,即函數組件內支持狀態管理。什麼概念呢,就是咱們在用 React 寫代碼的時候,幾乎能夠拋棄以前的 Class 寫法。之因此說是「幾乎」,是由於有些地方仍是須要用到 Class 寫法,可是 React 的做者 Dan 說了,「Hooks 將會是 React 的將來」 。那麼咱們這回就全程使用 Hooks 寫法,把日記項目敲一遍。
本次課程的 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 去適配移動端的分辨率。它能將頁面內的 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-demo
項目嗎?咱們就用那個項目進行服務端開發的工做。咱們第一件要作的事情就是在本地安裝一下 MySQL
數據庫,如何安裝傾聽我細細道來。
一、下載安裝 MySQL
進入 MySQL 官網 下載 MySQL 數據庫社區版
請選擇適合本身的版本,筆者是 MacOS 系統,因此選擇第一個安裝包,注意選擇不登陸下載
下載完成以後,按照導航提示進行安裝,進行到 root 用戶配置密碼時,必定要記住密碼,後面會用到的:
安裝完成以後,能夠進入系統便好設置這邊啓動數據庫:
圖形界面對於新手來講,是很是友好的。對數據庫的可視化操做,能提升新手的工做效率,筆者使用的這款 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
複製代碼
獲取到詳情以後,展現在輸入頁面。
整個項目先後端流程都已經跑通了,雖然數據庫只有一張表,可是做爲程序員,須要有觸類旁通的能力。固然若是想要把項目作的更復雜些,須要一些數據庫設計的基礎。
萬字長文,看到最後的朋友想必也是熱愛學習,但願提升本身的人。全文涉及到的知識點可能會比較粗略,可是仍是那句老話,師父領進門,修行靠我的。更多好文能夠關注個人 我的博客 還要個人 知乎專欄 。有問題能夠添加個人我的博客裏的微信羣,學習討論。這篇長文寫到我吐血,但願對你們有所幫助。