頭條創作挑戰賽#
本文同步本人掘金平臺的文章: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 || '新增失?。?#39;)
}
}).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說:“正如這個新公式證明的那樣,我們必須對卵的形成等生物進化過程進行數學描述,這是進化生物學研究的基石。新公式可應用于基礎學科,特別是推動食品和家禽行業的發展?!笨咸卮髮W訪問學者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小時內與您取得聯系。