Egg + React 实战日记本(万字长文,望收藏点赞)

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 是啥呀?鸡蛋吗?开个小玩笑。Egg.js 是基于 Koa 的上层架构,简单说就是 Egg.js 是基于 Koa 二次开发的后端 node 解决方案。截止目前(2020-01-06) Egg 的最新版本为 v2.26.0Github 上的星星居高不下,目前已达到了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/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 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 编写日记界面

简介

自 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.jsxstyle.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 数据库

一、下载安装 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
复制代码

获取到详情以后,展现在输入页面。

整个项目先后端流程都已经跑通了,虽然数据库只有一张表,可是做为程序员,须要有触类旁通的能力。固然若是想要把项目作的更复杂些,须要一些数据库设计的基础。

总结

万字长文,看到最后的朋友想必也是热爱学习,但愿提升本身的人。全文涉及到的知识点可能会比较粗略,可是仍是那句老话,师父领进门,修行靠我的。更多好文能够关注个人 我的博客 还要个人 知乎专栏 。有问题能够添加个人我的博客里的微信群,学习讨论。这篇长文写到我吐血,但愿对你们有所帮助。

相关文章
相关标签/搜索