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应用(目前还是内测阶段)
管理应用
创建好应用后,就可以到进入应用管理了
创建数据表
- 点击左侧的【 数据表】进入数据表管理页
- 接下来创建一个数据表: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
配置项目引入组件
- 引入 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')
至此,项目中使用的环境已经初始化完成
- 组织项目结构并建立相关文件
项目结构如下
+ 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
- 建议数据结构
/*数据库结构定义*/
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 };
- 封装数据表操作
/*不想在功能页每次都导入接口操作组件,所以这里将数据表操作组件直接引入并封装,以后每次使用直接和数据结构等数据一块引入 */
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 项进行线上路径配置,将路径配置为 /子目录即可