整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Egg.js試水 - 文章增刪改查「前后端分離」

          Egg.js試水 - 文章增刪改查「前后端分離」

          頭條創作挑戰賽#

          本文同步本人掘金平臺的文章: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;
          };
          復制代碼

          API的CRUD

          上面服務端的工作,已經幫我們做好編寫接口的準備了。那么,下面結合數據庫,我們來實現下文章增刪改查的操作。

          我們使用的是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 實現一個后臺管理系統。

          讀完此文你將學到:

          1. Egg.js 基本使用
          2. 如何使用 Sequelize ORM 模塊進行 Mysql 操作
          3. 如何使用 Redis
          4. 如何使用 JWT 進行用戶登錄驗證
          5. Serverless Framework 的基本使用
          6. 如何將本地開發好的 Egg.js 應用部署到騰訊云云函數上
          7. 如何基于云端對象存儲快速部署靜態網站

          Egg.js 入門

          初始化 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 就可以看到登錄界面了。

          開發后端服務

          對于一個后臺管理系統服務,我們這里只實現登錄鑒權和文章管理功能,剩下的其他功能大同小異,讀者可以之后自由補充擴展。

          1. 添加 Sequelize 插件

          在正式開發之前,我們需要引入數據庫插件,這里本人偏向于使用 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"
            }
            // ...
          };
          // ...
          

          2. 添加 JWT 插件

          系統將使用 JWT token 方式進行登錄鑒權,安裝配置參考官方文檔,egg-jwt

          3. 添加 Redis 插件

          系統將使用 redis 來存儲和管理用戶 token,安裝配置參考官方文檔,egg-redis

          4. 角色 API

          定義用戶模型,創建 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 文件。詳細說明參考 官方文檔

          5. 用戶 API

          同 Role 一樣定義我們的用戶 API,這里就不復制粘貼了,可以參考項目實例源碼 admin-system。

          6. 同步數據庫表格

          上面只是定義好了 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 可以獲取所有用戶列表了。

          7. 用戶登錄/注銷 API

          這里登錄邏輯比較簡單,客戶端發送 用戶名 和 密碼 到 /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 項目部署到騰訊云云函數上。

          1. 準備

          我們先創建一個 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

          2. 配置 Serverless

          在項目根目錄下創建 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 前端項目模板

          3. 執行部署

          執行部署命令:

          $ 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 就可以觸發云函數執行了。

          4. 賬號配置(可選)

          當前默認支持 Serverless cli 掃描二維碼登錄,如果希望配置持久的環境變量/秘鑰信息,也可以在項目根目錄創建 .env 文件

          在 .env 文件中配置騰訊云的 SecretId 和 SecretKey 信息并保存,密鑰可以在 API 密鑰管理 中獲取或者創建.

          # .env
          TENCENT_SECRET_ID=123
          TENCENT_SECRET_KEY=123

          5. 文章 API

          跟用戶 API 類似,只需要復制粘貼上面用戶相關模塊,修改名稱為 posts, 并修改數據模型就行,這里就不粘貼代碼了。

          前端開發

          本實例直接使用的 vue-admin-template 的前端模板。

          我們需要做如下幾部分修改:

          1. 刪除接口模擬:更換為真實的后端服務接口
          2. 修改接口函數:包括用戶相關的 frontend/src/api/user.js 和文章相關接口 frontend/src/api/post.js。
          3. 修改接口工具函數:主要是修改 frontend/src/utils/request.js 文件,包括 axios請求的 baseURL 和請求的 header。
          4. UI 界面修改:主要是新增文章管理頁面,包括列表頁和新增頁。

          1. 刪除接口模擬

          首先刪除 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)
          });
          

          2. 修改接口函數

          修改 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"
            });
          }
          

          3. 修改接口工具函數

          因為 @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;
          

          4. UI 界面修改

          關于 UI 界面修改,這里就不做說明了,因為涉及到 Vue.js 的基礎使用,如果還不會使用 Vue.js,建議先復制示例代碼就好。如果對 Vue.js 感興趣,可以到 Vue.js 官網 學習。也可以閱讀本人的 Vuejs 從入門到精通系列文章,喜歡的話,可以送上您寶貴的 Star (*^▽^*)

          這里只需要復制 Demo 源碼 的 frontend/router 和 frontend/views 兩個文件夾就好。

          前端部署

          因為前端編譯后都是靜態文件,我們需要將靜態文件上傳到騰訊云的 COS(對象存儲) 服務,然后開啟 COS 的靜態網站功能就可以了,這些都不需要你手動操作,使用 @serverless/tencent-website 組件就可以輕松搞定。

          1. 修改 Serverless 配置文件

          修改項目根目錄下 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

          2. 執行部署

          執行部署命令:

          $ 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 文件就行。


          主站蜘蛛池模板: 日韩一区精品视频一区二区| 久久婷婷色综合一区二区| 一本AV高清一区二区三区| 一区 二区 三区 中文字幕| 无码人妻AⅤ一区二区三区水密桃 无码欧精品亚洲日韩一区夜夜嗨 无码毛片一区二区三区中文字幕 无码毛片一区二区三区视频免费播放 | 无码人妻精品一区二区三区99不卡 | 免费人妻精品一区二区三区| 国产一区二区女内射| 成人区人妻精品一区二区不卡| 冲田杏梨高清无一区二区| 精品无码国产一区二区三区51安| 一区二区三区在线| 国模大胆一区二区三区| 91视频国产一区| 亚洲国产一区国产亚洲| 精品国产天堂综合一区在线| 精品3d动漫视频一区在线观看| 国产精品自拍一区| 亚洲中文字幕无码一区二区三区 | 亚洲国产美国国产综合一区二区| 精品一区二区ww| 在线精品视频一区二区| 国产一区视频在线| 四虎精品亚洲一区二区三区| 国产福利91精品一区二区 | chinese国产一区二区| 日本免费一区二区三区最新| 免费无码A片一区二三区| 无码国产精品久久一区免费| 亚洲熟妇成人精品一区| 日韩人妻精品无码一区二区三区| 久久亚洲一区二区| 精品视频在线观看你懂的一区 | 国产精品亚洲一区二区无码| 国精产品一区二区三区糖心| 亚洲色偷精品一区二区三区| 日本成人一区二区三区| 国产成人av一区二区三区在线| 无码免费一区二区三区免费播放| 国产一区二区三区在线免费观看| 久久久国产精品亚洲一区|