博客项目 | Next.js+Tailwind css+MobX+egg.js+SSM

390 阅读5分钟

前台

Next.js

1. 路由拦截 — Middleware

Middleware enables you to use code over configuration. This gives you full flexibility in Next.js, because you can run code before a request is completed. Based on the user's incoming request, you can modify the response by rewriting, redirecting, adding headers, or even streaming HTML.

中间件使您能够使用代码来进行配置。这为您在Next.js中提供了完全的灵活性,因为您可以在请求完成之前运行代码。根据用户的传入请求,您可以通过重写、重定向、添加头文件、甚至是流媒体HTML来修改响应。

2. 获取数据 — getInitialProps

3. 接收query

  1. 使用withRouter
import { withRouter } from 'next/router'

function Page({ router }) {
  return <p>{router.pathname}</p>
}

export default withRouter(Page)
  1. 使用useRouter

动态引入(懒加载)

  1. 方式一:
const Fuse = (await import('fuse.js')).default
const fuse = new Fuse(names)
  1. 方式二:
import dynamic from 'next/dynamic'

const DynamicComponent = dynamic(() => import('../components/hello'))

function Home() {
  return (
    <div>
      <Header />
      <DynamicComponent />
      <p>HOME PAGE is here!</p>
    </div>
  )
}

export default Home

样式编写

  1. 支持 style jsx
  2. 支持 [name].module.css
  3. 支持内嵌样式
  4. 支持import引入全局,支持在应用程序中的任何位置从 node_modules 目录导入(import) CSS 文件了

选择编辑器(react-quill | ByteMD)

react-quill 使用

  1. 引入
npm i react-quill
  1. 代码
/**
 * @description 编辑文章
 */
import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic'
import { Col, Row, Input, Button, Select, Modal, message } from 'antd';
import 'react-quill/dist/quill.snow.css';
import { nanoid } from 'nanoid'
import { getArticle, editArticle } from '../../../config';
import { useRouter } from 'next/router';

const { TextArea } = Input;

const QuillNoSSRWrapper = dynamic(import('react-quill'), {
      ssr: false,
      loading: () => <p>Loading ...</p>,
})
const modules = {
      toolbar: {
            container: [
                  ['bold', 'italic', 'underline', 'strike'],
                  ['blockquote', 'code-block'],
                  [{ header: 1 }, { header: 2 }],
                  [{ list: 'ordered' }, { list: 'bullet' }],
                  [{ script: 'sub' }, { script: 'super' }],
                  [{ indent: '-1' }, { indent: '+1' }],
                  [{ direction: 'rtl' }],
                  [{ size: ['small', false, 'large', 'huge'] }], //字体设置
                  [
                        {
                              color: [],
                        },
                  ],
                  [
                        {
                              background: [],
                        },
                  ],
                  [{ font: [] }],
                  [{ align: [] }],
                  ['link', 'image'], // a链接和图片的显示
            ],
      },
};
const create: React.FC = () => {
      // 文章id
      const [articleId, setArticleId] = useState("");
      // 文章标题
      const [title, setTitle] = useState("用例标题")
      // 文章内容
      const [value, setValue] = useState("文章内容");
      // 文章简介
      const [intro, setIntro] = useState("内容简介");
      // 文章分类
      const [type, setType] = useState("6809635626879549454");
      // 控制弹窗
      const [isModalOpen, setIsModalOpen] = useState(false);
      const [confirmLoading, setConfirmLoading] = useState(false);
      const router = useRouter()
      useEffect(() => {
            const a = location.href.split("/")
            const id1 = a[a.length - 1]
            // getArticle(id1).then(res => {
            //       if (res.code === 200) {
            //             console.log(res.data);
            //             setArticleId(res.data.id)
            //             setTitle(res.data.title)
            //             setValue(res.data.articleContent)
            //             setIntro(res.data.introduce)
            //             setType(res.data.typeId)
            //       } else {
            //             console.log("获取失败edit", res);
            //       }
            // })
      }, [])

      const showModal = () => {
            setIsModalOpen(true);
      };

      const handleOk = () => {
            setConfirmLoading(true);
            let obj = {
                  id: articleId,
                  typeId: type,
                  framerId: localStorage.getItem("framer_id"),
                  title: title,
                  articleContent: value,
                  introduce: intro,
                  addTime: null,
                  updateTime: null,
                  viewCount: 0,
                  likeCount: 0,
                  collectCount: 0,
                  commentCount: 0
            }
            console.log(obj);
            // editArticle(obj).then(res => {
            //       if (res.code === 200) {
            //             message.success("修改成功")
            //       } else {
            //             message.error("修改失败")
            //             console.log("修改文章失败:", res);
            //       }
            // })
            setTimeout(() => {
                  setIsModalOpen(false);
                  setConfirmLoading(false);
            }, 2000);
      };

      const handleCancel = () => {
            setIsModalOpen(false);
      };

      const changeType = (value) => {
            setType(value)
      };

      // const changeIntro = (value) => {
      //       setI(value)
      // };

      return (
            <>
                  <Row>
                        <Col span={12}>
                              <Input placeholder="请输入文章标题..." bordered={false}
                                    style={{ fontSize: '24px', lineHeight: '56px', marginLeft: '10px' }}
                                    value={title}
                                    onChange={(e) => {
                                          setTitle(e.target.value);
                                    }} />
                              <QuillNoSSRWrapper theme="snow" value={value} onChange={setValue} modules={modules} style={{ height: "80vh", backgroundColor: '#f8f9fa' }}
                              />
                        </Col>
                        <Col span={12}>
                              <div>
                                    <Button type="primary" size="large" onClick={showModal} style={{ position: 'fixed', right: '25px', top: '10px' }}>发 布</Button>
                                    <Modal title="填写信息" open={isModalOpen} onOk={handleOk} onCancel={handleCancel} confirmLoading={confirmLoading}>
                                          <div>分类: </div>
                                          <Select
                                                size="large"
                                                value={type}
                                                onChange={changeType}
                                                style={{ width: 200 }}
                                                options={[
                                                      { value: '6809635626661445640', label: 'IOS' },
                                                      { value: '6809635626879549454', label: 'Android' },
                                                      { value: '6809637767543259144', label: '前端' },
                                                      { value: '6809637769959178254', label: '后端' },
                                                      { value: '6809637771511070734', label: '开发工具' },
                                                      { value: '6809637772874219534', label: '阅读' },
                                                      { value: '6809637773935378440', label: '人工智能' },
                                                ]}
                                          />
                                          <div>简介: </div>
                                          <TextArea
                                                value={intro}
                                                onChange={(e) => setIntro(e.target.value)}
                                                placeholder="请输入简介..."
                                                autoSize={{ minRows: 3, maxRows: 5 }}
                                          />
                                    </Modal>
                              </div>
                              <div
                                    style={{ border: '1px solid #cccccc', height: "88.6vh", borderLeft: 'none', marginTop: '64px', overflowY: 'auto', padding: '20px' }}
                                    dangerouslySetInnerHTML={{ __html: value }}></div>
                        </Col>
                  </Row>
            </>
      )
}

export default create;

ByteMD (掘金同款)

后端

egg.js

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("/list", controller.list.index);
};

egg-mysql

连接数据库

config/config.default.js

exports.mysql = {
  // database configuration
  client: {
    // host
    host: 'mysql.com',
    // port
    port: '3306',
    // username
    user: 'test_user',
    // password
    password: 'test_password',
    // database
    database: 'test',    
  },
  // load into app, default is open
  app: true,
  // load into agent, default is close
  agent: false,
};

app/controllar/dafault.js

//获取数据库信息

pages/index.js

//利用axios连接中台,渲染数据

RESTful接口

REST架构原则

  1. 网络上的所有事物都被抽象为资源
  2. 每个资源都有一个唯一的资源标识符
  3. 同一个资源具有多种表现形式(xml,json等)
  4. 对资源的各种操作不会改变资源标识符
  5. 所有的操作都是无状态的

符合REST原则的架构方式即可称为RESTful

编写路由守卫

  • 建文件middleware/adminauth.js
  • 在路由配置文件里使用路由守卫

后台

react-router-dom(6版本)

  • 安装 yarn add react-router-dom
  • 引入import { BrowserRouter as Router, Route,Routes } from "react-router-dom";
  • 使用
<Router>
   <Routes>
      <Route path="/login" element={<Login />} />
   </Routes>
</Router>
  • 多级路由配置
    • 第一种,配置两次Route
    //Main.js
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/admin/*" element={<AdminIndex />} />
      </Routes>
    </Router>
    //AdminIndex.js
    <Routes>
        <Route path="index" element={<Login />} />
    </Routes>
    
    • 第二种,在父组件,用<Outlet />给子组件留位置
    //Main.js
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/admin" element={<AdminIndex />} >
            <Route path="index" element={<AddArticle />}/>
        </Route>
      </Routes>
    </Router>
    //AdminIndex.js
    <Outlet />
    
  • 编程式路由跳转
import { useNavigate } from "react-router-dom";

const navigate = useNavigate();
navigate("/admin/index");

点击菜单跳转

  • 为Menu绑定onClick
  • onClick默认接收参数item
  • 根据item.key进行路由跳转

登录功能

步骤

  • 中台
    • 建表
    • 编写控制器,需要暴露
    • 配置路由router/admin.js,在router.js中引入
    • 改变config.default.js的配置
  • 前台
    • Login.js中为按钮绑定方法
    • 编写方法:判断用户输入的正确性,发送post请求,根据返回数据判断登录情况
    • 配置路由文件config/apiUrl.js
  • post请求
  • sql语句username和password的值加引号,有可能是中文

添加文章/更新文章

  • 中台
    • 接收数据
    • 存入数据库
    • 将id设置为自增(int)
  • 前台
    • 初始文章id为0,表示是新文章
    • 判断数据的正确性
    • 封装data数据
    • 调用后端接口
    • 将返回的文章id进行赋值
    • 再次发布文章,如果id不为0,就更新文章,调用更新接口

文章列表

  • 中台
    • 获取文章列表方法
    • 配置路由
  • 前台
    • 配置路由
    • 获取文章列表
    • 使用useEffect(()=>{getList()}.[]),在页面加载时首次渲染

删除文章

  • 中台
    • 获取文章列表方法
    • 配置路由
  • 前台
    • 编写删除方法
    • 配置路由

react为什么onclick都是使用箭头函数,为什么onClick会被调用,使用箭头函数就不会

修改文章

  • 中台
    • 将date数据转为年月日STR_TO_DATE(concat(YEAR(addTime),'-',MONTH(addTime),'-',DAY(addTime)),'%Y-%m-%d'),STR_TO_DATE是将数据转为data类,但是中台接收的数据就不会是年月日的形式,所以要把这个方法去掉
  • 前台
    • 配置路由,为添加文章addArticle多配置一个带有params的路由
    • 编写根据id获取文章信息的方法
    • 获取params使用useParams
    • 在useEffect中进行首次渲染操作