Vue3+MemFireDb+TypeScript的尝试记录

Vue3+MemFireDb+TypeScript的尝试记录


运行环境:
  • Windows10 、 VSCode
  • Nodejs v16.13.2、npm 8.1.2
  • Vue ^3.2.37、Vite ^3.0.4、Element-Plus ^2.2.12

尝试本项目的目的

学习 TypeScript 和 Vue3,
Vue3 中对象绑定、Emits、Props,
export 、export default 、export type 、import 和 import type
尝试 MemFireDb 的使用

本次尝试不包含MemFire Clound 的用户登录功能,使用来宾用户进行操作

遇到的相关问题:mounted、filters、methods 的变化, 数据绑定、数组对象赋值问题

TypeScript 中的强类型限制在刚开始尝试时一直不习惯,整个页面全是红线,虽然页面数据能正常加载出来,但是总会有警告出现

访问路径中不是 /#/ 的路径格式上传到静态托管后不能显示页面


创建 MemFire 应用并创建数据表

详细文档在
https://docs.memfiredb.com/posts/db-introduction.html
Vue3新手入门 https://docs.memfiredb.com/base/example/QuickstartsVue3.html

创建一个MemFire Cloud应用(目前还是内测阶段)

点击 “创建应用” 填写相关项目

管理应用

创建好应用后,就可以到进入应用管理了
点击 应用仪表盘 右上角 3个点 ,再点击 进入应用
应用管理界面

创建数据表
  1. 点击左侧的【 数据表】进入数据表管理页
  2. 接下来创建一个数据表:organizes

默认提供3个字段【id、created_at、updated_at】

再随便新增几个字段【org_name:text、org_desc:text、tel:text、email:text、address:text】

至此,已经拥有了一个可以供后面使用的应用和数据源


建立 Vite + Vue3 项目

创建项目
npm init vite vue3_memfiredb
cd vue3_memfiredb
npm install
code .

注:最后一条命令是调用vscode打开项目文件夹
如果vscode安装路径没在系统环境变量的path中,此命令无法正常执行,使用手动打开文件夹功能打开项目

安装使用的 package
# 安装 MemFire Cloud 接口调用组件
npm i @supabase/supabase-js
# 安装 Element Plus UI组件
npm i element-plus
配置项目引入组件
  1. 引入 element-plus

main.ts 文件内容如下

import { createApp } from 'vue'
import App from './App.vue'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//import zhCn from 'element-plus/es/locale/lang/zh-cn'
createApp(App)
    .use(ElementPlus)//引用UI组件
    //.use(ElementPlus, { locale: zhCn })//使用中文环境
    .mount('#app')

至此,项目中使用的环境已经初始化完成

  1. 组织项目结构并建立相关文件

项目结构如下

+ src
| + api
| | - database.ts
| + views
| | - index.vue
| | - organize.vue
| - App.vue
...

src/views/organize.vue 为前面创建的数据表调用功能实现文件
src/views/index.vue 主页面的实现文件
src/api/database.ts 数据表操作界面功能页


编写功能
  • database.ts
  1. 建议数据结构
/*数据库结构定义*/
interface drv_md_model {
    id?: number;
    created_at: Date;
    updated_at?: Date;
    created_by: number;
    updated_by?: number;
}
abstract class drv_md_base implements drv_md_model {
    id?: number = undefined;
    created_at: Date = new Date();
    updated_at?: Date | undefined;
}
class drv_md_organize extends drv_md_base {
    constructor() {
        super()
    }
    org_name: string = ""
    org_desc: string = ""
    tel: string = ""
    email: string = ""
    address: string = ""
}
/*数据库表名称定义*/
const tableName = {
    org_db: "drv_md_organizes"
}
/*不想在功能页面每次都引入数据表操作 将数据表的操作功能进行一次封装*/


export type { drv_md_model };
export {
    drv_md_organize
};
export { tableName };

  1. 封装数据表操作
/*不想在功能页每次都导入接口操作组件,所以这里将数据表操作组件直接引入并封装,以后每次使用直接和数据结构等数据一块引入 */
import { createClient, PostgrestError, PostgrestFilterBuilder   } from '@supabase/supabase-js';

// 项目总览面中的内容
const supabaseUrl: string = "来自 配置 -> 网址"
const supabaseAnonKey: string = "来自 项目API密钥 中的公开anon,按文档说明,使用此密钥连接,可不用登录,为一个来宾密钥"

const supabase = createClient(supabaseUrl, supabaseAnonKey)

/**
 * 
 * @param table 获取数据
 * @param callback 
 * @param where 
 * @param order 
 * @param range
 */
async function tableSelect<T>(table: string,
    callback: (result: T[], error: PostgrestError | null) => void,
    where?: (select: PostgrestFilterBuilder<T>) => PostgrestFilterBuilder<T>,
    order?: (select: PostgrestFilterBuilder<T>) => PostgrestFilterBuilder<T>,
    range?: () => { from: number, to: number }) {
    ...
}
/**
 * 创建数据
 * @param table 
 * @param items 
 * @param callback 
 */
async function tableInsert<T>(table: string, items: T[],
    callback: (result: T[], error: PostgrestError | null) => void) {
    ...
}
/**
 * 修改数据
 * @param table 
 * @param item 
 * @param where 
 * @param callback 
 */
async function tableUpdate<T>(table: string, item: T,
    where: (select: PostgrestFilterBuilder<T>) => PostgrestFilterBuilder<T>,
    callback: (result: T[], error: PostgrestError | null) => void) {
    ...
}
/**
 * 插入更新 数据
 * @param table 
 * @param items 
 * @param callback 
 */
async function tableUpsert<T>(table: string, items: T[],
    callback: (result: T[], error: PostgrestError | null) => void) {
    ...
}
/**
 * 删除数据
 * @param table 
 * @param where 
 * @param callback 
 */
async function tableDelete<T>(table: string,
    where: (select: PostgrestFilterBuilder<T>) => PostgrestFilterBuilder<T>,
    callback: (result: T[], error: PostgrestError | null) => void) {
    ...
}

/*导出相关定义*/

export default {}

export {
    drv_md_organize
};
export { tableName };
export {
    tableSelect,
    tableInsert,
    tableUpdate,
    tableDelete,
    tableUpsert
};
  • App.vue

只有一个页面,也就没尝试使用 Router

<script setup lang="ts">
</script>

<template>
  <index></index>
</template>

<script lang="ts" scope>
//引入主页index
import Index from './views/index.vue'
export default {
  name: 'App',
  components: { Index },
}
</script>
  • organize.vue
<script lang="ts" scoped>
import { Delete, Edit, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { defineComponent, PropType, reactive } from 'vue'
import {
  drv_md_organize,
  tableDelete,
  tableInsert,
  tableName,
  tableUpdate,
} from '../api/database'

export default defineComponent({
  name: 'drv_md_organize',
  components: {},
  emits: {
    OrgDelete: () => {}, //当成功删除记录后通知父页面重新加载数据
  },  
  props: {
    //供父页面来提供绑定的数据记录
    orgs: {
      type: [] as PropType<Array<drv_md_organize>>,//使用具体类型定义
      default: [],
    },
    //供父页面提供数据加载状态的绑定
    orgs_loading: { type: Boolean, default: false },
  },
  setup(props, context) {
    /**
     * vue2 中可直接定义供绑定的数据项
     *
     * vue3 中需要用到 ref 和 reactive toRef toRefs
     */

    const states = reactive({
      /**
       * 编辑器可视状态
       */
      show_editor: false,
      /**
       * 数据是否正在提交中
       */
      commiting: false,
      /**
       * 1 add 2 edit 3 delete
       */
      edit_mode: 1,
    })
    //供数据编辑器绑定对象
    const edit_form: drv_md_organize = reactive(new drv_md_organize())
    //记录当前使用对象,不做界面绑定使用
    let current_org: drv_md_organize

    /**
     * 同步对象属性值同步到同类型的目标对象中
     * @param src 源对象
     * @param target 目标对象
     */
    function syncObj<T>(src: T, target: T) {
      for (var key in src) {
        if (typeof src[key] == 'string' || typeof src[key] == 'number') {
          target[key] = src[key]
        }
      }
    }
    //显示新增界面
    function toAdd() {
      states.edit_mode = 1
      states.show_editor = true

      var tmpObj = new drv_md_organize()
      syncObj(tmpObj, edit_form)
    }
    //显示编辑界面
    function toEdit(idx: number, org: drv_md_organize) {
      states.edit_mode = 2
      states.show_editor = true
      current_org = org
      syncObj(org, edit_form)
    }
    //删除记录
    function toDelete(idx: number, org: drv_md_organize) {
      states.edit_mode = 3
      current_org = org
      commit()
    }
    //提交操作
    function commit() {
      try {
        if (states.edit_mode == 1) {
          let tempOrg = new drv_md_organize()
          syncObj(edit_form, tempOrg)
          delete tempOrg.id
          if (!tempOrg.org_name) {
            ElMessage.warning('请填写好名称。')
            return
          }
          states.commiting = true
          //调用数据表插入操作
          tableInsert<drv_md_organize>(
            tableName.org_db,
            [tempOrg],
            (result, error) => {
              if (error == null) {
                //将新增的记录push时界面绑定的数据列表供显示用
                result.forEach((item) => props.orgs.push(item))
                states.show_editor = false
              } else {
                ElMessage.error(error.details)
              }
              states.commiting = false
            }
          )
        } else if (states.edit_mode == 2) {
          let tempOrg = new drv_md_organize()
          syncObj(edit_form, tempOrg)
          delete tempOrg.id
          tempOrg.updated_at = new Date()

          if (!tempOrg.org_name) {
            ElMessage.warning('请填写好名称。')
            return
          }
          states.commiting = true
          //调用数据表更新操作
          tableUpdate<drv_md_organize>(
            tableName.org_db,
            tempOrg,
            (select) => {
              return select.match({ id: edit_form.id })
            },
            (result, error) => {
              if (error == null) {
                //更新界面绑定数据
                if (result.length > 0) syncObj(result[0], current_org)
                states.show_editor = false
              } else {
                ElMessage.error(error.details)
              }
              states.commiting = false
            }
          )
        } else if (states.edit_mode == 3) {
          if (current_org == undefined) return
          states.commiting = true
          //调用数据表删除记录操作
          tableDelete<drv_md_organize>(
            tableName.org_db,
            (where) => where.match({ id: current_org.id }),
            (result, error) => {
              if (error == null) {
                ElMessage.success('Success!')
                //通知父页面刷新一次最新列表
                context.emit('OrgDelete')
              } else {
                ElMessage.error(error.details)
              }
              states.commiting = false
            }
          )
        }
      } catch (err) {
        ElMessage.error('' + err)
      } finally {
        states.commiting = false
      }
    }

    //日期时间格式化,vue2里可以在filter里实现,vue3里没有了filter功能
    //此处定义成一个方法,变相实现filter 
    function dateTimeFormat(dt: number | string | Date) {
      if (!dt) return dt

      var date: Date
      if (typeof dt != typeof String || typeof dt == typeof Number)
        date = new Date(dt)
      else date = dt as Date

      var y = date.getFullYear()
      var m = date.getMonth() + 1
      var d = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()

      var h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
      var min =
        date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
      var sec =
        date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()

      return `${y}-${m}-${d} ${h}:${min}:${sec}`
    }
    return {
      toAdd,
      toEdit,
      toDelete,
      commit,
      edit_form,
      states,
      dateTimeFormat,
      icons: { Edit, Delete, Plus },
    }
  },
})
</script>

<template>
  <div>
    <el-form inline>
      <el-form-item style="float:right">
        <el-button-group size="default">
          <el-button type="success"
                     :icon="icons.Plus"
                     @click="toAdd"
                     :disabled="states.commiting">新增</el-button>
        </el-button-group>
      </el-form-item>
    </el-form>
    <el-table :data="orgs"
              v-loading="orgs_loading"
              size="small"
              border
              stripe>
      <el-table-column label="ID"
                       prop="id"
                       width="70"></el-table-column>
      <el-table-column label="名称"
                       prop="org_name"
                       width="160"></el-table-column>
      <el-table-column label="说明"
                       prop="org_desc"></el-table-column>
      <el-table-column label="电话"
                       prop="tel"
                       width="120"></el-table-column>
      <el-table-column label="邮箱"
                       prop="email"
                       width="180"></el-table-column>
      <el-table-column label="地址"
                       prop="address"
                       width="300"></el-table-column>
      <el-table-column label="创建时间"
                       prop="created_at"
                       width="140">
        <template #default="scope">
          <span>{{dateTimeFormat(scope.row.created_at)}}</span>
        </template>
      </el-table-column>
      <el-table-column label="修改时间"
                       prop="updated_at"
                       width="140">
        <template #default="scope">
        <!-- filter的变相实现方法 -->
          <span>{{dateTimeFormat(scope.row.updated_at)}}</span>
        </template>
      </el-table-column>
      <el-table-column width="90"
                       fixed="right"
                       label="操作">
        <template #default="scope">
          <el-button-group size="small">
            <el-button size="small"
                       type="primary"
                       :icon="icons.Edit"
                       :disabled="states.commiting"
                       @click="toEdit(scope.$index, scope.row)"></el-button>
            <el-button size="small"
                       type="danger"
                       :icon="icons.Delete"
                       :disabled="states.commiting"
                       @click="toDelete(scope.$index, scope.row)">
            </el-button>
          </el-button-group>
        </template>
      </el-table-column>
    </el-table>

    <el-drawer v-model="states.show_editor">
      <template #header>
        <div>
          {{states.edit_mode==1?"新增":states.edit_mode==2?"编辑":""}}
        </div>
      </template>
      <el-form ref="editor"
               v-model="edit_form"
               label-width="70px"
               label-suffix=":">
        <el-form-item label="名称">
          <el-input v-model="edit_form.org_name"></el-input>
        </el-form-item>
        <el-form-item label="描述">
          <el-input v-model="edit_form.org_desc"></el-input>
        </el-form-item>
        <el-form-item label="电话">
          <el-input v-model="edit_form.tel"></el-input>
        </el-form-item>
        <el-form-item label="邮箱">
          <el-input v-model="edit_form.email"></el-input>
        </el-form-item>
        <el-form-item label="地址">
          <el-input v-model="edit_form.address"></el-input>
        </el-form-item>
      </el-form>
      <template #footer>
        <div>
          <el-button @click="states.show_editor=false"
                     :disabled="states.commiting">取消</el-button>
          <el-button @click="commit()"
                     type="primary"
                     :disabled="states.commiting">确定</el-button>
        </div>
      </template>
    </el-drawer>
  </div>
</template>

  • index.vue
<script lang="ts">
import { defineComponent, onMounted, reactive } from 'vue'
import { tableName, tableSelect } from '../api/database'

import { drv_md_organize } from '../api/database'
import Organize from './organize.vue'

export default defineComponent({
  name: 'home',
  components: { Organize },
  data() {
    return { tabKey: 'org' }
  },
  setup(props, context) {
    //页面状态相关量的定义
    const states = reactive({
      //标记 organize 数据是否正在加载中
      org_loading: false,
    })
    const datas = reactive({
      orgs: Array<drv_md_organize>(), //获取到的数据表记录对象缓存绑定列表
    })

    //实现 organizes 数据表的记录获取
    //为在测试 父子组件的数据传递绑定功能,所以在主页面中进行数据获取
    function refreshOrgs() {
      states.org_loading = true
      tableSelect<drv_md_organize>(tableName.org_db, (result) => {
        //因为vue3里的对象是Proxy型的,所以只能通过此方法来清空数组
        datas.orgs.length = 0
        //将查询结果push进缓存对象
        result.forEach((item) => datas.orgs.push(item))
        states.org_loading = false
      })
    }
    /* vue3 中 mouned 的功能*/
    onMounted(() => {
      refreshOrgs()
    })

    return {
      states,
      datas,
      refreshOrgs,
    }
  },
})
</script>
<style>
html,
body {
  padding: 0px !important;
  margin: 0px !important;
  width: 100%;
  height: 100%;
}
#app {
  height: 100%;
}
</style>
<style scoped>
section.el-container,
section.is-vertical {
  height: 100% !important;
}
</style>
<template>
  <el-container>
    <el-main>
       <Organize :orgs="datas.orgs"
                 @org-delete="refreshOrgs"
                 :orgs_loading="states.org_loading"></Organize>
    </el-main>
    <el-footer>
      <el-alert title="Vite + Vue3 + TypeScript + Element Plus + MemFireDB 的一次尝试"
                type="success"
                :closable="false" />
    </el-footer>
  </el-container>
</template>

编写完毕,可以运行一下进行测试

npm run dev
将项目发布到

官方操作文档页 https://docs.memfiredb.com/base/static-website-hosting.html 重点提到要使用 相对路径进行发布,因为项目会发布到它的虚拟目录下

为了不用每次进行相对路径的替换,我直接将项目的打包路径设置成了相对路径
vite.config.ts 的defineConfig配置项里加入 base: ‘./’

// https://vitejs.dev/config/
export default defineConfig({
  base: './',
 
....

项目在本地已经测试完成,现在进行打包上传

npm run build

进入生成后的dist目录,然后将所有内容打包为 zip

打开 MemFire Clound 的 静态托管页面,然后将刚才 的 zip 包进行上传,完成后在页面上方提供的 访问地址配置给应用 【认证管理->认证设置 : 网站网址】

现在就可以使用 静态托管上面提供的链接访问上传的静态页面了


  • 后期补充:解决发布后的登录功能跳转过来不能正常进入主页面的问题
    因为静态托管后它的访问路径是二级目录,所以使用 VITE_PUBLIC_PATH 项进行线上路径配置,将路径配置为 /子目录即可

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值