We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
上一篇文章讲的是后端渲染的项目 - Egg.js 试水 - 天气预报。但是没有引入数据库。这次的试水项目是文章的增删改查,将数据库引进,并且实现前后端分离。
项目的github地址是egg-demo/article-project。
github
下面直接进入正题~🙆
article-project ├── client ├── service └── README.md
因为是前后端分离的项目,那么我们就以文件夹client存放客户端,以文件夹service存放服务端。README.md是项目说明文件。
client
service
README.md
为了快速演示,我们使用vue-cli脚手架帮我们生成项目,并引入了vue-ant-design。
vue-cli
vue-ant-design
推荐使用yarn进行包管理。
$ npm install -g @vue/cli # 或者 $ yarn global add @vue/cli
然后新建一个项目。
$ vue create client
接着我们进入项目并启动。
$ cd client $ npm run serve # 或者 $ yarn run serve
此时,我们访问浏览器地址http://localhost:8080/,就会看到欢迎页面。
http://localhost:8080/
最后我们引入ant-design-vue。
ant-design-vue
$ npm install ant-design-vue # 或 $ yarn add ant-design-vue
在这里,我们全局引入ant-design-vue的组件。实际开发中,按需引入比较友好,特别是只是使用了该UI框架部分功能组件的时候。
UI
// src/main.js import Vue from 'vue' import App from './App.vue' import Antd from 'ant-design-vue' import 'ant-design-vue/dist/antd.css' Vue.use(Antd) Vue.config.productionTip = false; new Vue({ render: h => h(App), }).$mount('#app');
当然,在此项目中,还牵涉到几种npm包,之后只写yarn或者npm命令行操作。
npm
yarn
路由的跳转需要vue-router的协助。
vue-router
# 路由 $ yarn add vue-router # 进度条 $ yarn add nprogress
这里只用到登录页,首页,文章列表页面和文章的新增/编辑页面。所以我的路由配置如下:
// src/router/index.js import Vue from 'vue' import Router from 'vue-router' import Index from '@/views/index' import { UserLayout, BlankLayout } from '@/components/layouts' import NProgress from 'nprogress' // progress bar import 'nprogress/nprogress.css' // progress bar style const whiteList = ['login'] // no redirect whitelist import { getStore } from "@/utils/storage" Vue.use(Router) const router = new Router({ routes: [ { path: '/', name: 'index', redirect: '/dashboard/workplace', component: Index, children: [ { path: 'dashboard/workplace', name: 'dashboard', component: () => import('@/views/dashboard') }, { path: 'article/list', name: 'article_list', component: () => import('@/views/article/list') }, { path: 'article/info', name: 'article_info', component: () => import('@/views/article/info') } ] }, { path: '/user', component: UserLayout, redirect: '/user/#', // hidden: true, children: [ { path: 'login', name: 'login', component: () => import(/* webpackChunkName: "user" */ '@/views/user/#') } ] }, { path: '/exception', component: BlankLayout, redirect: '/exception/404', children: [ { path: '404', name: '404', component: () => import(/* webpackChunkName: "user" */ '@/views/exception/404') } ] }, { path: '*', component: () => import(/* webpackChunkName: "user" */ '@/views/exception/404') } ], // base: process.env.BASE_URL, scrollBehavior: () => ({ y: 0 }), }) router.beforeEach((to, from, next) => { NProgress.start() // start progress bar if(getStore('token', false)) { // 有token if(to.name === 'index' || to.path === '/index' || to.path === '/') { next({ path: '/dashboard/workplace'}) NProgress.done() return false } next() } else { if(to.path !== '/user/#') { (new Vue()).$notification['error']({ message: '验证失效,请重新登录!' }) } if(whiteList.includes(to.name)) { // 在免登录白名单,直接进入 next() } else { next({ path: '/user/#', query: { redirect: to.fullPath } }) NProgress.done() } } next() }) router.afterEach(route => { NProgress.done() }) export default router
接口请求使用了axios,我们来集成下。
# axios $ yarn add axios
我们即将要代理的后端服务的地址是127.0.0.1:7001,所以我们的配置如下:
127.0.0.1:7001
// vue.config.js ... devServer: { host: '0.0.0.0', port: '9008', https: false, hotOnly: false, proxy: { // 配置跨域 '/api': { //要访问的跨域的api的域名 target: 'http://127.0.0.1:7001/', ws: true, changOrigin: true }, }, }, ...
我们封装下请求🙂
// src/utils/request.js import Vue from 'vue' import axios from 'axios' import store from '@/store' import notification from 'ant-design-vue/es/notification' import { ACCESS_TOKEN } from '@/store/mutation-types' import { notice } from './notice'; const err = (error) => { if (error.response) {} return Promise.reject(error) } function loginTimeOut () { notification.error({ message: '登录信息失效', description: '请重新登录' }) store.dispatch('user/logout').then(() => { setTimeout(() => { window.location.reload() }, 1500) }) } // 创建 auth axios 实例 const auth = axios.create({ headers: { 'Content-Type': 'application/json;charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' }, baseURL: '/', // api base_url timeout: 10000 // 请求超时时间 10秒钟 }) // request interceptor auth.interceptors.request.use(config => { const token = Vue.ls.get(ACCESS_TOKEN) if (token) { config.headers[ 'Authorization' ] = 'JWT '+ token // 让每个请求携带自定义 token 请根据实际情况自行修改 } return config }, err) // response interceptor auth.interceptors.response.use( response => { if (response.code === 10140) { loginTimeOut() } else { return response.data } }, error => { // 错误处理 console.log(error.response, 'come here') if(error.response && error.response.status === 403) { notice({ title: '未授权,你没有访问权限,请联系管理员!', }, 'notice', 'error', 5) return } notice({ title: (error.response && error.response.data && error.response.data.msg) || (error.response && `${error.response.status} - ${error.response.statusText}`), }, 'notice', 'error', 5) } ) export { auth }
当然,为了更好的管理你的页面样式,建议还是添加一种CSS预处理器。这里我选择了less预处理器。
CSS预处理器
# less 和 less-loader $ yarn add less --dev $ yarn add less-loader --dev
仅仅是安装还不行,我们来配置下。
// vue.config.js ... css: { loaderOptions: { less: { modifyVars: { blue: '#3a82f8', 'text-color': '#333' }, javascriptEnabled: true } } }, ...
文章列表页的骨架:
<!--src/views/article/list.vue--> <template> <div class="article-list"> <a-table style="border: none;" bordered :loading="loading" :rowKey="row => row.id" :columns="columns" :data-source="data" :pagination="pagination" @change="change"/> </div> </template>
文章编辑/新增页的骨架:
<!--src/views/article/info.vue--> <template> <div class="article-info"> <a-spin :spinning="loading"> <a-row style="display: flex; justify-content: flex-end; margin-bottom: 20px;"> <a-button type="primary" @click="$router.go(-1)">返回</a-button> </a-row> <a-form :form="form" v-bind="formItemLayout"> <a-form-item label="标题"> <a-input placeholder="请输入标题" v-decorator="[ 'title', {rules: [{ required: true, message: '请输入标题'}]} ]"/> </a-form-item> <a-form-item label="分组"> <a-select showSearch v-decorator="[ 'group', {rules: [{ required: true, message: '请选择分组'}]} ]" placeholder="请选择分组"> <a-select-option value="分组1">分组1</a-select-option> <a-select-option value="分组2">分组2</a-select-option> <a-select-option value="分组3">分组3</a-select-option> <a-select-option value="分组4">分组4</a-select-option> </a-select> </a-form-item> <a-form-item label="作者"> <a-input placeholder="请输入作者" v-decorator="[ 'author', {rules: [{ required: true, message: '请输入作者'}]} ]"/> </a-form-item> <a-form-item label="内容"> <a-textarea :autosize="{ minRows: 10, maxRows: 12 }" placeholder="请输入文章内容" v-decorator="[ 'content', {rules: [{ required: true, message: '请输入文章内容'}]} ]"/> </a-form-item> </a-form> <a-row style="margin-top: 20px; display: flex; justify-content: space-around;"> <a-button @click="$router.go(-1)">取消</a-button> <a-button type="primary" icon="upload" @click="submit">提交</a-button> </a-row> </a-spin> </div> </template>
前端的项目有了雏形,下面搭建下服务端的项目。
这里直接使用eggjs框架来实现服务端。你可以考虑使用typescript方式的来初始化项目,但是我们这里直接使用javascript而不是它的超级typescript来初始化项目。
typescript
javascript
$ mkdir service $ cd service $ npm init egg --type=simple $ npm i
启动项目:
$ npm run dev
在浏览器中打开localhost:7001地址,我们就可以看到eggjs的欢迎页面。当然,我们这里基本上不会涉及到浏览器页面,因为我们开发的是api接口。更多的是使用postman工具进行调试。
localhost:7001
eggjs
api接口
postman
这里使用的数据库是mysql,但是我们不是直接使它,而是安装封装过的mysql2和egg-sequelize。
mysql2
egg-sequelize
在 Node.js 社区中,sequelize 是一个广泛使用的 ORM 框架,它支持 MySQL、PostgreSQL、SQLite 和 MSSQL 等多个数据源。它会辅助我们将定义好的 Model 对象加载到 app 和 ctx 上。
# 安装mysql $ yarn add mysql2 # 安装sequelize $ yarn add egg-sequelize
当然,我们需要一个数据库进行连接,那就得安装一个数据库,如果你使用的是mac os的话,你可以通过下面的方法进行安装:
mac os
brew install mysql brew services start mysql
window系统的话,可以考虑下载相关的安装包执行就行了,这里不展开说了。
window
数据库安装好后,我们管理数据库,可以通过控制台命令行进行控制,也可以通过图形化工具进行控制。我们推荐后者,我们下载了一个Navicat Premiun的工具。
Navicat Premiun
Navicat Premiun 是一款数据库管理工具。
当然还可以下载phpstudy进行辅助开发。
phpstudy
配置数据库的基本信息,前提是我们已经创建好了这个数据库。假设我们创建了一个名为article的数据库,用户是reng,密码是123456。那么,我们就可以像下面这样连接。
article
reng
123456
// config/config.default.js ... config.sequelize = { dialect: 'mysql', host: '127.0.0.1', port: 3306, database: 'article', username: 'reng', password: '123456', operatorsAliases: false }; ...
当然,这是通过包egg-sequelize处理的,我们也要将其引入,告诉eggjs去使用这个插件。
// config/plugin.js ... sequelize: { enable: true, package: 'egg-sequelize', }, ...
你可以直接通过控制台命令行执行mysql语句创建。但是,我们直接使用迁移操作完成。
mysql
在项目中,我们希望将所有的数据库Migrations相关的内容都放在database目录下面,所以我们在根目录下新建一个.sequelizerc配置文件:
Migrations
database
.sequelizerc
// .sequelizerc 'use strict'; const path = require('path'); module.exports = { config: path.join(__dirname, 'database/config.json'), 'migrations-path': path.join(__dirname, 'database/migrations'), 'seeders-path': path.join(__dirname, 'database/seeders'), 'models-path': path.join(__dirname, 'app/model'), };
初始化Migrations配置文件和目录。
npx sequelize init:config npx sequelize init:migrations
更加详细内容,可见eggjs sequelize章节。
我们按照官网上的操作初始化了文章列表的数据库表articles。对应的model内容如下:
articles
model
// app/model/article.js 'use strict'; module.exports = app => { const { STRING, INTEGER, DATE, NOW, TEXT } = app.Sequelize; const Article = app.model.define('articles', { id: {type: INTEGER, primaryKey: true, autoIncrement: true},//记录id title: {type: STRING(255)},// 标题 group: {type: STRING(255)}, // 分组 author: {type: STRING(255)},// 作者 content: {type: TEXT}, // 内容 created_at: {type: DATE, defaultValue: NOW},// 创建时间 updated_at: {type: DATE, defaultValue: NOW}// 更新时间 }, { freezeTableName: true // 不自动将表名添加复数 }); return Article; };
上面服务端的工作,已经帮我们做好编写接口的准备了。那么,下面结合数据库,我们来实现下文章增删改查的操作。
增删改查
我们使用的是MVC的架构,那么我们的现有代码逻辑自然会这样流向:
MVC
app/router.js 获取文章路由到 -> app/controller/article.js中对应的方法 -> 到app/service/article.js中的方法。那么,我们就主要展示在controller层和service层做的事情吧。毕竟router层没啥好讲的。
app/router.js
app/controller/article.js
app/service/article.js
controller
router
[get] /api/get-article-list
// app/controller/article.js ... async getList() { const { ctx } = this const { page, page_size } = ctx.request.query let lists = await ctx.service.article.findArticle({ page, page_size }) ctx.returnBody(200, '获取文章列表成功!', { count: lists && lists.count || 0, results: lists && lists.rows || [] }, '00000') } ...
// app/service/article.js ... async findArticle(obj) { const { ctx } = this return await ctx.model.Article.findAndCountAll({ order: [['created_at', 'ASC']], offset: (parseInt(obj.page) - 1) * parseInt(obj.page_size), limit: parseInt(obj.page_size) }) } ...
[get] /api/get-article
// app/controller/article.js ... async getItem() { const { ctx } = this const { id } = ctx.request.query let articleDetail = await ctx.service.article.getArticle(id) if(!articleDetail) { ctx.returnBody(400, '不存在此条数据!', {}, '00001') return } ctx.returnBody(200, '获取文章成功!', articleDetail, '00000') } ...
// app/service/article.js ... async getArticle(id) { const { ctx } = this return await ctx.model.Article.findOne({ where: { id } }) } ...
[post] /api/post-article
// app/controller/article.js ... async postItem() { const { ctx } = this const { author, title, content, group } = ctx.request.body // 新文章 let newArticle = { author, title, content, group } let article = await ctx.service.article.addArticle(newArticle) if(!article) { ctx.returnBody(400, '网络错误,请稍后再试!', {}, '00001') return } ctx.returnBody(200, '新建文章成功!', article, '00000') } ...
// app/service/article.js ... async addArticle(data) { const { ctx } = this return await ctx.model.Article.create(data) } ...
[put] /api/put-article
// app/controller/article.js ... async putItem() { const { ctx } = this const { id } = ctx.request.query const { author, title, content, group } = ctx.request.body // 存在文章 let editArticle = { author, title, content, group } let article = await ctx.service.article.editArticle(id, editArticle) if(!article) { ctx.returnBody(400, '网络错误,请稍后再试!', {}, '00001') return } ctx.returnBody(200, '编辑文章成功!', article, '00000') } ...
// app/service/article.js ... async editArticle(id, data) { const { ctx } = this return await ctx.model.Article.update(data, { where: { id } }) } ...
[delete] /api/delete-article
// app/controller/article.js ... async deleteItem() { const { ctx } = this const { id } = ctx.request.query let articleDetail = await ctx.service.article.deleteArticle(id) if(!articleDetail) { ctx.returnBody(400, '不存在此条数据!', {}, '00001') return } ctx.returnBody(200, '删除文章成功!', articleDetail, '00000') } ...
// app/service/article.js ... async deleteArticle(id) { const { ctx } = this return await ctx.model.Article.destroy({ where: { id } }) } ...
在完成接口的编写后,你可以通过postman 应用去验证下是否返回的数据。
接下来就得切回来client文件夹进行操作了。我们在上面已经简单封装了请求方法。这里来编写文章CRUD的请求方法,我们为了方便调用,将其统一挂载在Vue实例下。
CRUD
Vue
// src/api/index.js import article from './article' const api = { article } export default api export const ApiPlugin = {} ApiPlugin.install = function (Vue, options) { Vue.prototype.api = api // 挂载api在原型上 }
// src/api/article.js ... export function getList(params) { return auth({ url: '/api/get-article-list', method: 'get', params }) } ...
// src/views/article/list.vue ... getList() { let vm = this vm.loading = true vm.api.article.getList({ page: vm.pagination.current, page_size: vm.pagination.pageSize }).then(res => { if(res.code === '00000'){ vm.pagination.total = res.data && res.data.count || 0 vm.data = res.data && res.data.results || [] } else { vm.$message.warning(res.msg || '获取文章列表失败') } }).finally(() => { vm.loading = false }) } ...
// src/api/article.js ... export function getItem(params) { return auth({ url: '/api/get-article', method: 'get', params }) } ...
// src/views/article/info.vue ... getDetail(id) { let vm = this vm.loading = true vm.api.article.getItem({ id }).then(res => { if(res.code === '00000') { // 数据回填 vm.form.setFieldsValue({ title: res.data && res.data.title || undefined, author: res.data && res.data.author || undefined, content: res.data && res.data.content || undefined, group: res.data && res.data.group || undefined, }) } else { vm.$message.warning(res.msg || '获取文章详情失败!') } }).finally(() => { vm.loading = false }) }, ...
// src/api/article.js ... export function postItem(data) { return auth({ url: '/api/post-article', method: 'post', data }) } ...
// src/views/article/info.vue ... submit() { let vm = this vm.loading = true vm.form.validateFields((err, values) => { if(err){ vm.loading = false return } let data = { title: values.title, group: values.group, author: values.author, content: values.content } vm.api.article.postItem(data).then(res => { if(res.code === '00000') { vm.$message.success(res.msg || '新增成功!') vm.$router.push({ path: '/article/list' }) } else { vm.$message.warning(res.msg || '新增失败!') } }).finally(() => { vm.loading = false }) }) }, ...
// src/api/article.js ... export function putItem(params, data) { return auth({ url: '/api/put-article', method: 'put', params, data }) } ...
// src/views/article/info.vue ... submit() { let vm = this vm.loading = true vm.form.validateFields((err, values) => { if(err){ vm.loading = false return } let data = { title: values.title, group: values.group, author: values.author, content: values.content } vm.api.article.putItem({id: vm.$route.query.id}, data).then(res => { if(res.code === '00000') { vm.$message.success(res.msg || '新增成功!') vm.$router.push({ path: '/article/list' }) } else { vm.$message.warning(res.msg || '新增失败!') } }).finally(() => { vm.loading = false }) }) } ...
// src/api/article.js ... export function deleteItem(params) { return auth({ url: '/api/delete-article', method: 'delete', params }) } ...
// src/views/article/list.vue ... delete(text, record, index) { let vm = this vm.$confirm({ title: `确定删除【${record.title}】`, content: '', okText: '确定', okType: 'danger', cancelText: '取消', onOk() { vm.api.article.deleteItem({ id: record.id }).then(res => { if(res.code === '00000') { vm.$message.success(res.msg || '删除成功!') vm.handlerSearch() } else { vm.$message.warning(res.msg || '删除失败!') } }) }, onCancel() {}, }) } ...
在egg-demo/article-project/client/前端项目中,页面包含了登录页面,欢迎页面和文章页面。
欢迎页面忽略不计
至此,整个项目已经完成。代码仓库为egg-demo/article-project/,感兴趣可以进行扩展学习。
下一篇文章,我们讲讲mysql。
更多的内容见Jimmy Github。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
上一篇文章讲的是后端渲染的项目 - Egg.js 试水 - 天气预报。但是没有引入数据库。这次的试水项目是文章的增删改查,将数据库引进,并且实现前后端分离。
项目的
github
地址是egg-demo/article-project。下面直接进入正题~🙆
项目结构
因为是前后端分离的项目,那么我们就以文件夹
client
存放客户端,以文件夹service
存放服务端。README.md
是项目说明文件。客户端初始化
为了快速演示,我们使用
vue-cli
脚手架帮我们生成项目,并引入了vue-ant-design
。项目初始化
推荐使用yarn进行包管理。
$ npm install -g @vue/cli # 或者 $ yarn global add @vue/cli
然后新建一个项目。
接着我们进入项目并启动。
此时,我们访问浏览器地址
http://localhost:8080/
,就会看到欢迎页面。最后我们引入
ant-design-vue
。$ npm install ant-design-vue # 或 $ yarn add ant-design-vue
在这里,我们全局引入
ant-design-vue
的组件。实际开发中,按需引入比较友好,特别是只是使用了该UI
框架部分功能组件的时候。路由设置
路由的跳转需要
vue-router
的协助。这里只用到登录页,首页,文章列表页面和文章的新增/编辑页面。所以我的路由配置如下:
接口请求设置
接口请求使用了axios,我们来集成下。
# axios $ yarn add axios
我们即将要代理的后端服务的地址是
127.0.0.1:7001
,所以我们的配置如下:我们封装下请求🙂
样式预处理器
当然,为了更好的管理你的页面样式,建议还是添加一种
CSS预处理器
。这里我选择了less预处理器。# less 和 less-loader $ yarn add less --dev $ yarn add less-loader --dev
仅仅是安装还不行,我们来配置下。
布局文章页面
文章列表页的骨架:
文章编辑/新增页的骨架:
前端的项目有了雏形,下面搭建下服务端的项目。
服务端初始化
这里直接使用eggjs框架来实现服务端。你可以考虑使用
typescript
方式的来初始化项目,但是我们这里直接使用javascript
而不是它的超级typescript
来初始化项目。初始化项目
$ mkdir service $ cd service $ npm init egg --type=simple $ npm i
启动项目:
在浏览器中打开
localhost:7001
地址,我们就可以看到eggjs
的欢迎页面。当然,我们这里基本上不会涉及到浏览器页面,因为我们开发的是api接口
。更多的是使用postman
工具进行调试。引入数据库
这里使用的数据库是mysql,但是我们不是直接使它,而是安装封装过的
mysql2
和egg-sequelize
。当然,我们需要一个数据库进行连接,那就得安装一个数据库,如果你使用的是
mac os
的话,你可以通过下面的方法进行安装:window
系统的话,可以考虑下载相关的安装包执行就行了,这里不展开说了。数据库安装好后,我们管理数据库,可以通过控制台命令行进行控制,也可以通过图形化工具进行控制。我们推荐后者,我们下载了一个
Navicat Premiun
的工具。当然还可以下载
phpstudy
进行辅助开发。连接数据库
配置数据库的基本信息,前提是我们已经创建好了这个数据库。假设我们创建了一个名为
article
的数据库,用户是reng
,密码是123456
。那么,我们就可以像下面这样连接。当然,这是通过包
egg-sequelize
处理的,我们也要将其引入,告诉eggjs
去使用这个插件。创建数据库表
你可以直接通过控制台命令行执行
mysql
语句创建。但是,我们直接使用迁移操作完成。在项目中,我们希望将所有的数据库
Migrations
相关的内容都放在database
目录下面,所以我们在根目录下新建一个.sequelizerc
配置文件:初始化
Migrations
配置文件和目录。更加详细内容,可见eggjs sequelize章节。
我们按照官网上的操作初始化了文章列表的数据库表
articles
。对应的model
内容如下:API的CRUD
上面服务端的工作,已经帮我们做好编写接口的准备了。那么,下面结合数据库,我们来实现下文章
增删改查
的操作。我们使用的是
MVC
的架构,那么我们的现有代码逻辑自然会这样流向:app/router.js
获取文章路由到 ->app/controller/article.js
中对应的方法 -> 到app/service/article.js
中的方法。那么,我们就主要展示在controller
层和service
层做的事情吧。毕竟router
层没啥好讲的。获取文章列表
获取文章详情
添加文章
编辑文章
删除文章
在完成接口的编写后,你可以通过
postman
应用去验证下是否返回的数据。前端对接接口
接下来就得切回来
client
文件夹进行操作了。我们在上面已经简单封装了请求方法。这里来编写文章CRUD
的请求方法,我们为了方便调用,将其统一挂载在Vue
实例下。获取文章列表
获取文章详情
添加文章
编辑文章
删除文章
效果图
在egg-demo/article-project/client/前端项目中,页面包含了登录页面,欢迎页面和文章页面。
登录页
文章列表
文章编辑
后话
至此,整个项目已经完成。代码仓库为egg-demo/article-project/,感兴趣可以进行扩展学习。
下一篇文章,我们讲讲
mysql
。更多的内容见Jimmy Github。
The text was updated successfully, but these errors were encountered: