头条创作挑战赛#
本文同步本人掘金平台的文章:https://juejin.cn/post/6871478190037336078
上一篇文章讲的是后端渲染的项目 - Egg.js 试水 - 天气预报。但是没有引入数据库。这次的试水项目是文章的增删改查,将数据库引进,并且实现前后端分离。
项目的github地址是egg-demo/article-project。
下面直接进入正题~?
article-project
├── client
├── service
└── README.md
复制代码
因为是前后端分离的项目,那么我们就以文件夹client存放客户端,以文件夹service存放服务端。README.md是项目说明文件。
为了快速演示,我们使用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/,就会看到欢迎页面。
最后我们引入ant-design-vue。
$ npm install ant-design-vue
# 或
$ yarn add ant-design-vue
复制代码
在这里,我们全局引入ant-design-vue的组件。实际开发中,按需引入比较友好,特别是只是使用了该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命令行操作。
路由的跳转需要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/login',
// hidden: true,
children: [
{
path: 'login',
name: 'login',
component: ()=> import(/* webpackChunkName: "user" */ '@/views/user/login')
}
]
},
{
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/login') {
(new Vue()).$notification['error']({
message: '验证失效,请重新登录!'
})
}
if(whiteList.includes(to.name)) {
// 在免登录白名单,直接进入
next()
} else {
next({
path: '/user/login',
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,所以我们的配置如下:
// 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预处理器。
# 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来初始化项目。
$ mkdir service
$ cd service
$ npm init egg --type=simple
$ npm i
复制代码
启动项目:
$ npm run dev
复制代码
在浏览器中打开localhost:7001地址,我们就可以看到eggjs的欢迎页面。当然,我们这里基本上不会涉及到浏览器页面,因为我们开发的是api接口。更多的是使用postman工具进行调试。
这里使用的数据库是mysql,但是我们不是直接使它,而是安装封装过的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的话,你可以通过下面的方法进行安装:
brew install mysql
brew services start mysql
复制代码
window系统的话,可以考虑下载相关的安装包执行就行了,这里不展开说了。
数据库安装好后,我们管理数据库,可以通过控制台命令行进行控制,也可以通过图形化工具进行控制。我们推荐后者,我们下载了一个Navicat Premiun的工具。
Navicat Premiun 是一款数据库管理工具。
当然还可以下载phpstudy进行辅助开发。
配置数据库的基本信息,前提是我们已经创建好了这个数据库。假设我们创建了一个名为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语句创建。但是,我们直接使用迁移操作完成。
在项目中,我们希望将所有的数据库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内容如下:
// 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的架构,那么我们的现有代码逻辑自然会这样流向:
app/router.js 获取文章路由到 -> app/controller/article.js中对应的方法 -> 到app/service/article.js中的方法。那么,我们就主要展示在controller层和service层做的事情吧。毕竟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实例下。
// 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/,感兴趣可以进行扩展学习。
形被称作“完美的形状”,它可能是生物漫长进化过程的结晶。
phys.org网站当地时间8月27日报道,英国肯特大学领导的研究团队取得了重要突破,他们发现了一种通用的数学公式,可描述自然界中存在的任何蛋类外壳形状。直到不久前,这项工作还无人成功完成。
卵形被称作“完美的形状”,它一直吸引着数学家、工程师和生物学家的关注。卵形的特征对于胚胎孵化、有效脱离母体以及承受载荷等均有重要意义。研究人员使用四种几何图形来分析卵形:球形、椭球形、卵球形和梨形。然而,梨形的数学公式尚未导出。为了弥补这个缺陷,研究人员引入了一项额外的函数,开发了一个数学模型来拟合一种全新的几何形状,这一形状类似球体至椭球体演化的最后阶段。
新的卵形通用数学公式是基于四个参数建立的,包括:卵长、最大宽度、垂直轴的移动以及四分之一卵长直径。研究人员指出,这个公式不仅是人类认知卵形本身的重要工具,还有助于科学家了解卵形的进化原因与方式。
有关卵形的数学描述已经在食品研究、机械工程、农业、生物科学、建筑和航空学等领域得到了应用。新公式也在以下方面大有可为,例如:(1)优化对生物体的科学描述;(2)简化、精确化对生物体物理特性的测定。对于从事禽卵孵化、加工、储存和分类技术研究的工程师而言,卵的外部特性至关重要。使用体积、表面积、曲率半径等指标描述卵的轮廓,可降低识别过程的复杂性;(3)推动未来生物学相关工程的发展。卵形在建筑中应用广泛,伦敦市政厅的屋顶就采用了卵形设计,这不仅能提高最大负荷,还能减少材料消耗。
研究负责人、肯特大学遗传学教授Darren Griffin说:“正如这个新公式证明的那样,我们必须对卵的形成等生物进化过程进行数学描述,这是进化生物学研究的基石。新公式可应用于基础学科,特别是推动食品和家禽行业的发展。”肯特大学访问学者Michael Romanov补充说:“这个数学公式强调了数学和生物学之间的某种哲学和谐关系。由此出发,我们甚至能进一步理解宇宙。”
编译:雷鑫宇 审稿:西莫 责编:陈之涵
期刊来源:《纽约科学院年报》
期刊编号:0077-8923
原文链接:https://phys.org/news/2021-08-reveals-ancient-universal-equation-egg.html
中文内容仅供参考,一切内容以英文原版为准。转载请注明来源。
为一名前端开发者,在选择 Nodejs 后端服务框架时,第一时间会想到 Egg.js,不得不说 Egg.js 是一个非常优秀的企业级框架,它的高扩展性和丰富的插件,极大的提高了开发效率。开发者只需要关注业务就好,比如要使用 redis,引入 egg-redis 插件,然后简单配置就可以了。正因为如此,第一次接触它,我便喜欢上了它,之后也用它开发过不少应用。
有了如此优秀的框架,那么如何将一个 Egg.js 的服务迁移到 Serverless 架构上呢?
我在文章 基于 Serverless Component 的全栈解决方案 中讲述了,如何将一个基于 Vue.js 的前端应用和基于 Express 的后端服务,快速部署到腾讯云上。虽然受到不少开发者的喜爱,但是很多开发者私信问我,这还是一个 Demo 性质的项目而已,有没有更加实用性的解决方案。而且他们实际开发中,很多使用的正是 Egg.js 框架,能不能提供一个 Egg.js 的解决方案?
本文将手把手教你结合 Egg.js 和 Serverless 实现一个后台管理系统。
读完此文你将学到:
初始化 Egg.js 项目:
$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i
启动项目:
$ npm run dev
然后浏览器访问 http://localhost:7001,就可以看到亲切的 hi, egg 了。
关于 Egg.js 的框架更多知识,建议阅读 官方文档
对 Egg.js 有了简单了解,接下来我们来初始化我们的后台管理系统,新建一个项目目录 admin-system:
$ mkdir admin-system
将上面创建的 Egg.js 项目复制到 admin-system 目录下,重命名为 backend。然后将前端模板项目复制到 frontend 文件夹中:
$ git clone https://github.com/PanJiaChen/vue-admin-template.git frontend
说明: vue-admin-template 是基于 Vue2.0 的管理系统模板,是一个非常优秀的项目,建议对 Vue.js 感兴趣的开发者可以去学习下,当然如果你对 Vue.js 还不是太了解,这里有个基础入门学习教程 Vuejs 从入门到精通系列文章
之后你的项目目录结构如下:
.
├── README.md
├── backend // 创建的 Egg.js 项目
└── frontend // 克隆的 Vue.js 前端项目模板
启动前端项目熟悉下界面:
$ cd frontend
$ npm install
$ npm run dev
然后访问 http://localhost:9528 就可以看到登录界面了。
对于一个后台管理系统服务,我们这里只实现登录鉴权和文章管理功能,剩下的其他功能大同小异,读者可以之后自由补充扩展。
在正式开发之前,我们需要引入数据库插件,这里本人偏向于使用 Sequelize ORM 工具进行数据库操作,正好 Egg.js 提供了 egg-sequelize 插件,于是直接拿来用,需要先安装:
$ cd frontend
# 因为需要通过 sequelize 链接 mysql 所以这也同时安装 mysql2 模块
$ npm install egg-sequelize mysql2 --save
然后在 backend/config/plugin.js 中引入该插件:
module.exports={
// ....
sequelize: {
enable: true,
package: "egg-sequelize"
}
// ....
};
在 backend/config/config.default.js 中配置数据库连接参数:
// ...
const userConfig={
// ...
sequelize: {
dialect: "mysql",
// 这里也可以通过 .env 文件注入环境变量,然后通过 process.env 获取
host: "xxx",
port: "xxx",
database: "xxx",
username: "xxx",
password: "xxx"
}
// ...
};
// ...
系统将使用 JWT token 方式进行登录鉴权,安装配置参考官方文档,egg-jwt
系统将使用 redis 来存储和管理用户 token,安装配置参考官方文档,egg-redis
定义用户模型,创建 backend/app/model/role.js 文件如下:
module.exports=app=> {
const { STRING, INTEGER, DATE }=app.Sequelize;
const Role=app.model.define("role", {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(30),
created_at: DATE,
updated_at: DATE
});
// 这里定义与 users 表的关系,一个角色可以含有多个用户,外键相关
Role.associate=()=> {
app.model.Role.hasMany(app.model.User, { as: "users" });
};
return Role;
};
实现 Role 相关服务,创建 backend/app/service/role.js 文件如下:
const { Service }=require("egg");
class RoleService extends Service {
// 获取角色列表
async list(options) {
const {
ctx: { model }
}=this;
return model.Role.findAndCountAll({
...options,
order: [
["created_at", "desc"],
["id", "desc"]
]
});
}
// 通过 id 获取角色
async find(id) {
const {
ctx: { model }
}=this;
const role=await model.Role.findByPk(id);
if (!role) {
this.ctx.throw(404, "role not found");
}
return role;
}
// 创建角色
async create(role) {
const {
ctx: { model }
}=this;
return model.Role.create(role);
}
// 更新角色
async update({ id, updates }) {
const role=await this.ctx.model.Role.findByPk(id);
if (!role) {
this.ctx.throw(404, "role not found");
}
return role.update(updates);
}
// 删除角色
async destroy(id) {
const role=await this.ctx.model.Role.findByPk(id);
if (!role) {
this.ctx.throw(404, "role not found");
}
return role.destroy();
}
}
module.exports=RoleService;
一个完整的 RESTful API 就该包括以上五个方法,然后实现 RoleController, 创建 backend/app/controller/role.js:
const { Controller }=require("egg");
class RoleController extends Controller {
async index() {
const { ctx }=this;
const { query, service, helper }=ctx;
const options={
limit: helper.parseInt(query.limit),
offset: helper.parseInt(query.offset)
};
const data=await service.role.list(options);
ctx.body={
code: 0,
data: {
count: data.count,
items: data.rows
}
};
}
async show() {
const { ctx }=this;
const { params, service, helper }=ctx;
const id=helper.parseInt(params.id);
ctx.body=await service.role.find(id);
}
async create() {
const { ctx }=this;
const { service }=ctx;
const body=ctx.request.body;
const role=await service.role.create(body);
ctx.status=201;
ctx.body=role;
}
async update() {
const { ctx }=this;
const { params, service, helper }=ctx;
const body=ctx.request.body;
const id=helper.parseInt(params.id);
ctx.body=await service.role.update({
id,
updates: body
});
}
async destroy() {
const { ctx }=this;
const { params, service, helper }=ctx;
const id=helper.parseInt(params.id);
await service.role.destroy(id);
ctx.status=200;
}
}
module.exports=RoleController;
之后在 backend/app/route.js 路由配置文件中定义 role 的 RESTful API:
router.resources("roles", "/roles", controller.role);
通过 router.resources 方法,我们将 roles 这个资源的增删改查接口映射到了 app/controller/roles.js 文件。详细说明参考 官方文档
同 Role 一样定义我们的用户 API,这里就不复制粘贴了,可以参考项目实例源码 admin-system。
上面只是定义好了 Role 和 User 两个 Schema,那么如何同步到数据库呢?这里先借助 Egg.js 启动的 hooks 来实现,Egg.js 框架提供了统一的入口文件(app.js)进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。
我们在 backend 目录中创建 app.js 文件,如下:
"use strict";
class AppBootHook {
constructor(app) {
this.app=app;
}
async willReady() {
// 这里只能在开发模式下同步数据库表格
const isDev=process.env.NODE_ENV==="development";
if (isDev) {
try {
console.log("Start syncing database models...");
await this.app.model.sync({ logging: console.log, force: isDev });
console.log("Start init database data...");
await this.app.model.query(
"INSERT INTO roles (id, name, created_at, updated_at) VALUES (1, 'admin', '2020-02-04 09:54:25', '2020-02-04 09:54:25'),(2, 'editor', '2020-02-04 09:54:30', '2020-02-04 09:54:30');"
);
await this.app.model.query(
"INSERT INTO users (id, name, password, age, avatar, introduction, created_at, updated_at, role_id) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 20, 'https://yugasun.com/static/avatar.jpg', 'Fullstack Engineer', '2020-02-04 09:55:23', '2020-02-04 09:55:23', 1);"
);
await this.app.model.query(
"INSERT INTO posts (id, title, content, created_at, updated_at, user_id) VALUES (2, 'Awesome Egg.js', 'Egg.js is a awesome framework', '2020-02-04 09:57:24', '2020-02-04 09:57:24', 1),(3, 'Awesome Serverless', 'Build web, mobile and IoT applications using Tencent Cloud and API Gateway, Tencent Cloud Functions, and more.', '2020-02-04 10:00:23', '2020-02-04 10:00:23', 1);"
);
console.log("Successfully init database data.");
console.log("Successfully sync database models.");
} catch (e) {
console.log(e);
throw new Error("Database migration failed.");
}
}
}
}
module.exports=AppBootHook;
通过 willReady 生命周期函数,我们可以执行 this.app.model.sync() 函数来同步数据表,当然这里同时初始化了角色和用户数据记录,用来做为演示用。
注意:这的数据库同步只是本地调试用,如果想要腾讯云的 Mysql 数据库,建议开启远程连接,通过 sequelize db:migrate 实现,而不是每次启动 Egg 应用时同步,示例代码已经完成此功能,参考 Egg Sequelize 文档。 这里本人为了省事,直接开启腾讯云 Mysql 公网连接,然后修改 config.default.js 中的 sequelize 配置,运行 npm run dev 进行开发模式同步。
到这里,我们的用户和角色的 API 都已经定义好了,启动服务 npm run dev,访问 https://127.0.0.1:7001/users 可以获取所有用户列表了。
这里登录逻辑比较简单,客户端发送 用户名 和 密码 到 /login 路由,后端通过 login 函数接受,然后从数据库中查询该用户名,同时比对密码是否正确。如果正确则调用 app.jwt.sign() 函数生成 token,并将 token 存入到 redis 中,同时返回该 token,之后客户端需要鉴权的请求都会携带 token,进行鉴权验证。思路很简单,我们就开始实现了。
流程图如下:
首先,在 backend/app/controller/home.js 中新增登录处理 login 方法:
class HomeController extends Controller {
// ...
async login() {
const { ctx, app, config }=this;
const { service, helper }=ctx;
const { username, password }=ctx.request.body;
const user=await service.user.findByName(username);
if (!user) {
ctx.status=403;
ctx.body={
code: 403,
message: "Username or password wrong"
};
} else {
if (user.password===helper.encryptPwd(password)) {
ctx.status=200;
const token=app.jwt.sign(
{
id: user.id,
name: user.name,
role: user.role.name,
avatar: user.avatar
},
config.jwt.secret,
{
expiresIn: "1h"
}
);
try {
await app.redis.set(`token_${user.id}`, token);
ctx.body={
code: 0,
message: "Get token success",
token
};
} catch (e) {
console.error(e);
ctx.body={
code: 500,
message: "Server busy, please try again"
};
}
} else {
ctx.status=403;
ctx.body={
code: 403,
message: "Username or password wrong"
};
}
}
}
}
注释:这里有个密码存储逻辑,用户在注册时,密码都是通过 helper 函数 encryptPwd() 进行加密的(这里用到最简单的 md5 加密方式,实际开发中建议使用更加高级加密方式),所以在校验密码正确性时,也需要先加密一次。至于如何在 Egg.js 框架中新增 helper 函数,只需要在 backend/app/extend 文件夹中新增 helper.js 文件,然后 modole.exports 一个包含该函数的对象就行,参考 Egg 框架扩展文档
然后,在 backend/app/controller/home.js 中新增 userInfo 方法,获取用户信息:
async userInfo() {
const { ctx }=this;
const { user }=ctx.state;
ctx.status=200;
ctx.body={
code: 0,
data: user,
};
}
egg-jwt 插件,在鉴权通过的路由对应 controller 函数中,会将 app.jwt.sign(user, secrete) 加密的用户信息,添加到 ctx.state.user 中,所以 userInfo 函数只需要将它返回就行。
之后,在 backend/app/controller/home.js 中新增 logout 方法:
async logout() {
const { ctx }=this;
ctx.status=200;
ctx.body={
code: 0,
message: 'Logout success',
};
}
userInfo 和 logout 函数非常简单,重点是路由中间件如何处理。
接下来,我们来定义登录相关路由,修改 backend/app/router.js 文件,新增 /login, /user-info, /logout 三个路由:
const koajwt=require("koa-jwt2");
module.exports=app=> {
const { router, controller, jwt }=app;
router.get("/", controller.home.index);
router.post("/login", controller.home.login);
router.get("/user-info", jwt, controller.home.userInfo);
const isRevokedAsync=function(req, payload) {
return new Promise(resolve=> {
try {
const userId=payload.id;
const tokenKey=`token_${userId}`;
const token=app.redis.get(tokenKey);
if (token) {
app.redis.del(tokenKey);
}
resolve(false);
} catch (e) {
resolve(true);
}
});
};
router.post(
"/logout",
koajwt({
secret: app.config.jwt.secret,
credentialsRequired: false,
isRevoked: isRevokedAsync
}),
controller.home.logout
);
router.resources("roles", "/roles", controller.role);
router.resources("users", "/users", controller.user);
router.resources("posts", "/posts", controller.post);
};
Egg.js 框架定义路由时,router.post() 函数可以接受中间件函数,用来处理一些路由相关的特殊逻辑。
比如 /user-info,路由添加了 app.jwt 作为 JWT 鉴权中间件函数,至于为什么这么用,egg-jwt 插件有明确说明。
这里稍微复杂的是 /logout 路由,因为我们在注销登录时,需要将用户的 token 从 redis 中移除,所以这里借助了 koa-jwt2 的 isRevokded 参数,来进行 token 删除。
到这里,后端服务的登录和注销逻辑基本完成了。那么如何部署到云函数呢?可以直接使用 tencent-egg 组件,它是专门为 Egg.js 框架打造的 Serverless Component,使用它可以快速将我们的 Egg.js 项目部署到腾讯云云函数上。
我们先创建一个 backend/sls.js 入口文件:
const { Application }=require("egg");
const app=new Application();
module.exports=app;
然后修改 backend/config/config.default.js 文件:
const config=(exports={
env: "prod", // 推荐云函数的 egg 运行环境变量修改为 prod
rundir: "/tmp",
logger: {
dir: "/tmp"
}
});
注释:这里之所有需要修改运行和日志目录,是因为云函数运行时,只有 /tmp 才有写权限。
全局安装 serverless 命令:
$ npm install serverless -g
在项目根目录下创建 serverless.yml 文件,同时新增 backend 配置:
backend:
component: "@serverless/tencent-egg"
inputs:
code: ./backend
functionName: admin-system
# 这里必须指定一个具有操作 mysql 和 redis 的角色,具体角色创建,可访问 https://console.cloud.tencent.com/cam/role
role: QCS_SCFFull
functionConf:
timeout: 120
# 这里的私有网络必须和 mysql、redis 实例一致
vpcConfig:
vpcId: vpc-xxx
subnetId: subnet-xxx
apigatewayConf:
protocols:
- https
此时你的项目目录结构如下:
.
├── README.md // 项目说明文件
├── serverless.yml // serverless yml 配合文件
├── backend // 创建的 Egg.js 项目
└── frontend // 克隆的 Vue.js 前端项目模板
执行部署命令:
$ serverless --debug
之后控制台需要进行扫码登录验证腾讯云账号,扫码登录就好。等部署成功会发挥如下信息:
backend:
region: ap-guangzhou
functionName: admin-system
apiGatewayServiceId: service-f1bhmhk4
url: https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/
这里输出的 url 就是部署成功的 API 网关接口,可以直接访问测试。
注释:云函数部署时,会自动在腾讯云的 API 网关创建一个服务,同时创建一个 API,通过该 API 就可以触发云函数执行了。
当前默认支持 Serverless cli 扫描二维码登录,如果希望配置持久的环境变量/秘钥信息,也可以在项目根目录创建 .env 文件
在 .env 文件中配置腾讯云的 SecretId 和 SecretKey 信息并保存,密钥可以在 API 密钥管理 中获取或者创建.
# .env
TENCENT_SECRET_ID=123
TENCENT_SECRET_KEY=123
跟用户 API 类似,只需要复制粘贴上面用户相关模块,修改名称为 posts, 并修改数据模型就行,这里就不粘贴代码了。
本实例直接使用的 vue-admin-template 的前端模板。
我们需要做如下几部分修改:
首先删除 frontend/mock 文件夹。然后修改前端入口文件 frontend/src/main.js:
// 1. 引入接口变量文件,这个会依赖 @serverless/tencent-website 组件自动生成
import "./env.js";
import Vue from "vue";
import "normalize.css/normalize.css";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import locale from "element-ui/lib/locale/lang/en";
import "@/styles/index.scss";
import App from "./App";
import store from "./store";
import router from "./router";
import "@/icons";
import "@/permission";
// 2. 下面这段就是 mock server 引入,删除就好
// if (process.env.NODE_ENV==='production') {
// const { mockXHR }=require('../mock')
// mockXHR()
// }
Vue.use(ElementUI, { locale });
Vue.config.productionTip=false;
new Vue({
el: "#app",
router,
store,
render: h=> h(App)
});
修改 frontend/src/api/user.js 文件,包括登录、注销、获取用户信息和获取用户列表函数如下:
import request from "@/utils/request";
// 登录
export function login(data) {
return request({
url: "/login",
method: "post",
data
});
}
// 获取用户信息
export function getInfo(token) {
return request({
url: "/user-info",
method: "get"
});
}
// 注销登录
export function logout() {
return request({
url: "/logout",
method: "post"
});
}
// 获取用户列表
export function getList() {
return request({
url: "/users",
method: "get"
});
}
新增 frontend/src/api/post.js 文件如下:
import request from "@/utils/request";
// 获取文章列表
export function getList(params) {
return request({
url: "/posts",
method: "get",
params
});
}
// 创建文章
export function create(data) {
return request({
url: "/posts",
method: "post",
data
});
}
// 删除文章
export function destroy(id) {
return request({
url: `/posts/${id}`,
method: "delete"
});
}
因为 @serverless/tencent-website 组件可以定义 env 参数,执行成功后它会在指定 root 目录自动生成 env.js,然后在 frontend/src/main.js 中引入使用。 它会挂载 env 中定义的接口变量到 window 对象上。比如这生成的 env.js 文件如下:
window.env={};
window.env.apiUrl="https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/";
根据此文件我们来修改 frontend/src/utils/request.js 文件:
import axios from "axios";
import { MessageBox, Message } from "element-ui";
import store from "@/store";
import { getToken } from "@/utils/auth";
// 创建 axios 实例
const service=axios.create({
// 1. 这里设置为 `env.js` 中的变量 `window.env.apiUrl`
baseURL: window.env.apiUrl || "/", // url=base url + request url
timeout: 5000 // request timeout
});
// request 注入
service.interceptors.request.use(
config=> {
// 2. 添加鉴权token
if (store.getters.token) {
config.headers["Authorization"]=`Bearer ${getToken()}`;
}
return config;
},
error=> {
console.log(error); // for debug
return Promise.reject(error);
}
);
// 请求 response 注入
service.interceptors.response.use(
response=> {
const res=response.data;
// 只有请求code为0,才是正常返回,否则需要提示接口错误
if (res.code !==0) {
Message({
message: res.message || "Error",
type: "error",
duration: 5 * 1000
});
if (res.code===50008 || res.code===50012 || res.code===50014) {
// to re-login
MessageBox.confirm(
"You have been logged out, you can cancel to stay on this page, or log in again",
"Confirm logout",
{
confirmButtonText: "Re-Login",
cancelButtonText: "Cancel",
type: "warning"
}
).then(()=> {
store.dispatch("user/resetToken").then(()=> {
location.reload();
});
});
}
return Promise.reject(new Error(res.message || "Error"));
} else {
return res;
}
},
error=> {
console.log("err" + error);
Message({
message: error.message,
type: "error",
duration: 5 * 1000
});
return Promise.reject(error);
}
);
export default service;
关于 UI 界面修改,这里就不做说明了,因为涉及到 Vue.js 的基础使用,如果还不会使用 Vue.js,建议先复制示例代码就好。如果对 Vue.js 感兴趣,可以到 Vue.js 官网 学习。也可以阅读本人的 Vuejs 从入门到精通系列文章,喜欢的话,可以送上您宝贵的 Star (*^▽^*)
这里只需要复制 Demo 源码 的 frontend/router 和 frontend/views 两个文件夹就好。
因为前端编译后都是静态文件,我们需要将静态文件上传到腾讯云的 COS(对象存储) 服务,然后开启 COS 的静态网站功能就可以了,这些都不需要你手动操作,使用 @serverless/tencent-website 组件就可以轻松搞定。
修改项目根目录下 serverless.yml 文件,新增前端相关配置:
name: admin-system
# 前端配置
frontend:
component: "@serverless/tencent-website"
inputs:
code:
src: dist
root: frontend
envPath: src # 相对于 root 指定目录,这里实际就是 frontend/src
hook: npm run build
env:
# 依赖后端部署成功后生成的 url
apiUrl: ${backend.url}
protocol: https
# TODO: CDN 配置,请修改!!!
hosts:
- host: sls-admin.yugasun.com # CDN 加速域名
https:
certId: abcdedg # 为加速域名在腾讯云平台申请的免费证书 ID
http2: off
httpsType: 4
forceSwitch: -2
# 后端配置
backend:
component: "@serverless/tencent-egg"
inputs:
code: ./backend
functionName: admin-system
role: QCS_SCFFull
functionConf:
timeout: 120
vpcConfig:
vpcId: vpc-6n5x55kb
subnetId: subnet-4cvr91js
apigatewayConf:
protocols:
- https
执行部署命令:
$ serverless --debug
输出如下成功结果:
frontend:
url: https://dtnu69vl-470dpfh-1251556596.cos-website.ap-guangzhou.myqcloud.com
env:
apiUrl: https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/
host:
- https://sls-admin.yugasun.com (CNAME: sls-admin.yugasun.com.cdn.dnsv1.com)
backend:
region: ap-guangzhou
functionName: admin-system
apiGatewayServiceId: service-f1bhmhk4
url: https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/
注释:这里 frontend 中多输出了 host,是我们的 CDN 加速域名,可以通过配置 @serverless/tencent-website 组件的 inputs.hosts 来实现。有关 CDN 相关配置说明可以阅读 基于 Serverless Component 的全栈解决方案 - 续集。当然,如果你不想配置 CDN,直接删除,然后访问 COS 生成的静态网站 url。
部署成功后,我们就可以访问 https://sls-admin.yugasun.com 登录体验了。
本篇涉及到所有源码都维护在开源项目 tencent-serverless-demo 中 admin-system
本篇文章涉及到内容较多,推荐在阅读时,边看边开发,跟着文章节奏一步一步实现。如果遇到问题,可以参考本文源码。如果你成功实现了,可以到官网进一步熟悉 Egg.js 框架,以便今后可以实现更加复杂的应用。虽然本文使用的是 Vue.js 前端框架,但是你也可以将 frontend 更换为任何你喜欢的前端框架项目,开发时只需要将接口请求前缀使用 @serverless/tencent-website 组件生成的 env.js 文件就行。
*请认真填写需求信息,我们会在24小时内与您取得联系。