SQLFE:网页版数据库(VUE3+Node.js)

在现代开发中,"看代码说话"往往比抽象描述更有说服力。今天,让我们深入SQLFE的代码世界,通过真实的代码片段,探索这个数据库管理工具是如何将复杂操作转化为优雅体验的。

前端核心:Vue 3的魔法实现

1. 数据库连接对话框:ConnectionDialog.vue

vue 复制代码
<template>
  <el-dialog :title="title" v-model="visible" width="500px">
    <el-form :model="form" label-width="120px" ref="formRef" :rules="rules">
      <el-form-item label="数据库类型" prop="type">
        <el-select v-model="form.type" style="width: 100%">
          <el-option label="MySQL" value="mysql" />
        </el-select>
      </el-form-item>
      
      <el-form-item label="主机" prop="host">
        <el-input v-model="form.host" placeholder="例如:localhost" />
      </el-form-item>
      
      <el-form-item label="端口" prop="port">
        <el-input v-model.number="form.port" placeholder="例如:3306" />
      </el-form-item>
      
      <el-form-item label="用户名" prop="user">
        <el-input v-model="form.user" />
      </el-form-item>
      
      <el-form-item label="密码" prop="password">
        <el-input v-model="form.password" type="password" show-password />
      </el-form-item>
      
      <el-form-item label="数据库名" prop="database">
        <el-input v-model="form.database" placeholder="可选" />
      </el-form-item>
    </el-form>
    
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="closeDialog">取消</el-button>
        <el-button type="primary" @click="handleTest" :loading="testing">测试连接</el-button>
        <el-button type="success" @click="handleSave" :loading="saving">保存连接</el-button>
      </span>
    </template>
    
    <div v-if="testResult" class="test-result" :class="{'success': testResult.success, 'error': !testResult.success}">
      {{ testResult.message }}
    </div>
  </el-dialog>
</template>

<script setup>
import { ref, defineEmits, defineProps } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'

const emit = defineEmits(['close', 'save'])
const props = defineProps({
  connection: Object
})

const visible = ref(true)
const title = props.connection ? '编辑数据库连接' : '新建数据库连接'
const form = ref({
  type: 'mysql',
  host: 'localhost',
  port: 3306,
  user: '',
  password: '',
  database: ''
})
const testing = ref(false)
const saving = ref(false)
const testResult = ref(null)

// 如果是编辑模式,填充表单
if (props.connection) {
  form.value = { ...props.connection }
}

const rules = {
  host: [{ required: true, message: '请输入主机地址', trigger: 'blur' }],
  port: [{ required: true, message: '请输入端口号', trigger: 'blur' }],
  user: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}

const formRef = ref(null)

const handleTest = async () => {
  try {
    testing.value = true
    const validate = await formRef.value.validate()
    if (!validate) return
    
    const result = await api.testConnection(form.value)
    testResult.value = result
    
    if (result.success) {
      ElMessage.success('连接测试成功!')
    } else {
      ElMessage.error(result.message || '连接测试失败')
    }
  } catch (error) {
    testResult.value = { success: false, message: error.message }
    ElMessage.error('连接测试失败: ' + error.message)
  } finally {
    testing.value = false
  }
}

const handleSave = async () => {
  try {
    saving.value = true
    const validate = await formRef.value.validate()
    if (!validate) return
    
    const result = await api.saveConnection(form.value)
    
    if (result.success) {
      ElMessage.success('连接保存成功!')
      emit('save')
      closeDialog()
    } else {
      ElMessage.error(result.message || '保存失败')
    }
  } catch (error) {
    ElMessage.error('保存失败: ' + error.message)
  } finally {
    saving.value = false
  }
}

const closeDialog = () => {
  visible.value = false
  emit('close')
}
</script>

**代码亮点解析:

  • 使用Vue 3的<script setup>语法,代码更加简洁
  • 表单验证使用Element Plus的内置验证规则
  • api.testConnection()api.saveConnection()封装了与后端的通信
  • 响应式状态管理(testing, saving, testResult)提供流畅的用户反馈

2. 数据库导航树:DatabaseTree.vue

vue 复制代码
<template>
  <div class="database-tree">
    <el-button type="primary" @click="openConnectionDialog" icon="Plus">添加连接</el-button>
    
    <el-tree
      :data="treeData"
      :props="treeProps"
      node-key="id"
      :expand-on-click-node="false"
      :default-expanded-keys="expandedKeys"
      @node-click="handleNodeClick"
      :load="loadNode"
      :lazy="true"
    >
      <template #default="{ node, data }">
        <span class="custom-tree-node">
          <el-icon v-if="data.icon" :name="data.icon" class="node-icon" />
          <span>{{ node.label }}</span>
        </span>
      </template>
    </el-tree>
    
    <ConnectionDialog 
      v-if="dialogVisible" 
      :connection="editingConnection" 
      @close="dialogVisible = false" 
      @save="handleConnectionSaved"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api'
import ConnectionDialog from './ConnectionDialog.vue'

const emit = defineEmits(['database-selected', 'table-selected'])
const treeData = ref([])
const expandedKeys = ref([])
const dialogVisible = ref(false)
const editingConnection = ref(null)
const loading = ref(false)

const treeProps = {
  label: 'label',
  children: 'children',
  isLeaf: 'isLeaf'
}

// 初始化加载所有连接
const loadConnections = async () => {
  try {
    loading.value = true
    const connections = await api.getConnections()
    
    treeData.value = connections.map(conn => ({
      id: `conn_${conn.id}`,
      label: conn.name || `${conn.host}:${conn.port}`,
      type: 'connection',
      connection: conn,
      children: []
    }))
    
    // 默认展开第一个连接
    if (treeData.value.length > 0) {
      expandedKeys.value = [treeData.value[0].id]
      loadDatabases(treeData.value[0])
    }
  } catch (error) {
    ElMessage.error('加载连接失败: ' + error.message)
  } finally {
    loading.value = false
  }
}

// 加载数据库
const loadDatabases = async (node) => {
  try {
    const databases = await api.getDatabases(node.connection.id)
    node.children = databases.map(db => ({
      id: `db_${node.connection.id}_${db}`,
      label: db,
      type: 'database',
      connectionId: node.connection.id,
      database: db,
      children: []
    }))
  } catch (error) {
    ElMessage.error('加载数据库失败: ' + error.message)
  }
}

// 加载表
const loadTables = async (node) => {
  try {
    const tables = await api.getTables(node.connectionId, node.database)
    node.children = tables.map(table => ({
      id: `table_${node.connectionId}_${node.database}_${table}`,
      label: table,
      type: 'table',
      connectionId: node.connectionId,
      database: node.database,
      table: table,
      children: []
    }))
  } catch (error) {
    ElMessage.error('加载表失败: ' + error.message)
  }
}

// 加载列
const loadColumns = async (node) => {
  try {
    const columns = await api.getColumns(node.connectionId, node.database, node.table)
    node.children = columns.map(column => ({
      id: `column_${node.connectionId}_${node.database}_${node.table}_${column.Field}`,
      label: `${column.Field} (${column.Type})`,
      type: 'column',
      connectionId: node.connectionId,
      database: node.database,
      table: node.table,
      column: column.Field,
      children: []
    }))
  } catch (error) {
    ElMessage.error('加载列失败: ' + error.message)
  }
}

// 懒加载节点
const loadNode = async (node, resolve) => {
  if (node.level === 0) {
    // 根节点,已经加载过
    return resolve(node.data.children)
  }
  
  if (node.level === 1) {
    // 连接节点,加载数据库
    await loadDatabases(node.data)
    return resolve(node.data.children)
  }
  
  if (node.level === 2) {
    // 数据库节点,加载表
    await loadTables(node.data)
    return resolve(node.data.children)
  }
  
  if (node.level === 3) {
    // 表节点,加载列
    await loadColumns(node.data)
    return resolve(node.data.children)
  }
  
  resolve([])
}

// 节点点击处理
const handleNodeClick = (node) => {
  if (node.type === 'table') {
    emit('table-selected', {
      connectionId: node.connectionId,
      database: node.database,
      table: node.table
    })
  } else if (node.type === 'database') {
    emit('database-selected', {
      connectionId: node.connectionId,
      database: node.database
    })
  }
}

// 打开连接对话框
const openConnectionDialog = (connection = null) => {
  editingConnection.value = connection
  dialogVisible.value = true
}

// 连接保存处理
const handleConnectionSaved = () => {
  loadConnections()
}

onMounted(() => {
  loadConnections()
})
</script>

代码亮点解析:

  • 使用el-tree组件实现树形结构,支持懒加载(lazy="true")
  • loadNode函数实现按需加载,优化性能
  • 响应式数据绑定,自动更新UI
  • 清晰的节点类型管理(connection, database, table, column)
  • 节点点击事件触发相应的视图更新

后端核心:Node.js的API实现

1. 服务启动:index.js

javascript 复制代码
const express = require('express')
const cors = require('cors')
const databaseRoutes = require('./routes/database')
const app = express()
const port = 3001

// 中间件
app.use(cors())
app.use(express.json({ limit: '50mb' }))
app.use(express.urlencoded({ extended: true, limit: '50mb' }))

// 健康检查
app.get('/', (req, res) => {
  res.json({ 
    message: '数据库可视化管理工具API服务', 
    status: 'running',
    version: '1.0.0'
  })
})

// 数据库相关路由
app.use('/api/database', databaseRoutes)

// 全局错误处理中间件
app.use((err, req, res, next) => {
  console.error('全局错误:', err.stack)
  res.status(500).json({ 
    success: false, 
    message: '服务器内部错误',
    error: process.env.NODE_ENV === 'development' ? err.message : undefined
  })
})

// 404处理
app.use((req, res) => {
  res.status(404).json({ 
    success: false, 
    message: '接口不存在',
    path: req.path
  })
})

// 启动服务
app.listen(port, () => {
  console.log(`\n🚀 数据库可视化管理工具后端服务运行在 http://localhost:${port}`)
  console.log('📦 支持的API:')
  console.log('   - POST /api/database/test      测试数据库连接')
  console.log('   - POST /api/database           保存数据库连接')
  console.log('   - GET /api/database            获取所有连接')
  console.log('   - DELETE /api/database/:id     删除连接')
  console.log('   - GET /api/database/:id/databases  获取数据库列表')
  console.log('   - POST /api/database/:id/query   执行SQL查询\n')
})

module.exports = app;

代码亮点解析:

  • 使用cors中间件解决跨域问题
  • 配置了合理的请求体大小限制(limit: '50mb')
  • 详细的启动日志,方便开发者了解服务状态
  • 全局错误处理,避免服务崩溃
  • 清晰的API文档输出

2. 数据库API路由:database.js

javascript 复制代码
const express = require('express')
const router = express.Router()
const dbManager = require('../config/db')

// 测试数据库连接
router.post('/test', async (req, res) => {
  try {
    console.log('[API] 测试数据库连接:', {
      host: req.body.host,
      port: req.body.port,
      user: req.body.user,
      database: req.body.database
    })
    
    const result = await dbManager.testConnection(req.body)
    res.json(result)
  } catch (error) {
    console.error('[API] 测试连接失败:', error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message,
      code: error.code
    })
  }
})

// 保存数据库连接
router.post('/', async (req, res) => {
  try {
    console.log('[API] 保存数据库连接:', {
      host: req.body.host,
      port: req.body.port,
      user: req.body.user
    })
    
    const id = Date.now().toString()
    const result = await dbManager.addConnection(id, req.body)
    
    if (result.success) {
      res.status(201).json({ 
        success: true, 
        id, 
        message: '连接已保存',
        connection: {
          id,
          ...req.body,
          name: req.body.name || `${req.body.host}:${req.body.port}`
        }
      })
    } else {
      res.status(400).json({ 
        success: false, 
        message: result.message 
      })
    }
  } catch (error) {
    console.error('[API] 保存连接失败:', error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 获取所有连接
router.get('/', (req, res) => {
  try {
    console.log('[API] 获取所有连接')
    const connections = dbManager.getAllConnections()
    res.json({ 
      success: true, 
      data: connections 
    })
  } catch (error) {
    console.error('[API] 获取连接失败:', error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 删除连接
router.delete('/:id', async (req, res) => {
  try {
    const { id } = req.params
    console.log(`[API] 删除连接: ${id}`)
    
    const result = await dbManager.removeConnection(id)
    
    if (result) {
      res.json({ 
        success: true, 
        message: '连接已删除',
        id 
      })
    } else {
      res.status(404).json({ 
        success: false, 
        message: '连接不存在',
        id 
      })
    }
  } catch (error) {
    console.error(`[API] 删除连接 ${req.params.id} 失败:`, error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 获取数据库列表
router.get('/:connectionId/databases', async (req, res) => {
  try {
    const { connectionId } = req.params
    console.log(`[API] 获取连接 ${connectionId} 的数据库列表`)
    
    const databases = await dbManager.getDatabases(connectionId)
    res.json({ 
      success: true, 
      data: databases 
    })
  } catch (error) {
    console.error(`[API] 获取连接 ${req.params.connectionId} 的数据库列表失败:`, error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 获取表列表
router.get('/:connectionId/:database/tables', async (req, res) => {
  try {
    const { connectionId, database } = req.params
    console.log(`[API] 获取连接 ${connectionId} 数据库 ${database} 的表列表`)
    
    const tables = await dbManager.getTables(connectionId, database)
    res.json({ 
      success: true, 
      data: tables 
    })
  } catch (error) {
    console.error(`[API] 获取表列表失败:`, error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

// 执行SQL查询
router.post('/:connectionId/query', async (req, res) => {
  try {
    const { connectionId } = req.params
    const { sql } = req.body
    
    console.log(`[API] 执行查询 (连接: ${connectionId}):`, sql)
    
    if (!sql || typeof sql !== 'string') {
      return res.status(400).json({ 
        success: false, 
        message: 'SQL查询不能为空' 
      })
    }
    
    // 简单的SQL注入防护
    const lowerSql = sql.toLowerCase();
    if (lowerSql.includes('drop') || lowerSql.includes('truncate') || 
        lowerSql.includes('delete') && !lowerSql.includes('from')) {
      return res.status(403).json({ 
        success: false, 
        message: '禁止执行可能有害的SQL语句' 
      })
    }
    
    const result = await dbManager.executeQuery(connectionId, sql)
    res.json({ 
      success: true, 
      ...result 
    })
  } catch (error) {
    console.error(`[API] 执行SQL查询失败:`, error.message)
    res.status(500).json({ 
      success: false, 
      message: error.message 
    })
  }
})

module.exports = router;

代码亮点解析:

  • 详细的API日志记录,便于调试
  • 完善的错误处理和状态码
  • 简单的SQL注入防护机制
  • RESTful风格的API设计
  • 清晰的请求参数验证

3. 数据库连接管理:db.js

javascript 复制代码
const mysql = require('mysql2/promise')
const connections = new Map()

// 测试数据库连接
exports.testConnection = async (config) => {
  try {
    const connection = await mysql.createConnection({
      host: config.host,
      port: config.port,
      user: config.user,
      password: config.password,
      database: config.database || '',
      connectTimeout: 5000
    })
    
    await connection.query('SELECT 1')
    await connection.end()
    
    return { 
      success: true, 
      message: '连接测试成功' 
    }
  } catch (error) {
    console.error('数据库连接测试失败:', error.message)
    return { 
      success: false, 
      message: error.message 
    }
  }
}

// 添加连接
exports.addConnection = async (id, config) => {
  try {
    const connection = await mysql.createConnection({
      host: config.host,
      port: config.port,
      user: config.user,
      password: config.password,
      database: config.database || '',
      connectTimeout: 5000
    })
    
    // 存储连接信息(不存储密码)
    connections.set(id, {
      connection,
      config: {
        host: config.host,
        port: config.port,
        user: config.user,
        database: config.database
      }
    })
    
    return { 
      success: true 
    }
  } catch (error) {
    console.error(`添加连接 ${id} 失败:`, error.message)
    return { 
      success: false, 
      message: error.message 
    }
  }
}

// 获取所有连接(仅返回基本信息)
exports.getAllConnections = () => {
  return Array.from(connections.entries()).map(([id, conn]) => ({
    id,
    ...conn.config
  }))
}

// 移除连接
exports.removeConnection = async (id) => {
  if (connections.has(id)) {
    const { connection } = connections.get(id)
    try {
      await connection.end()
    } catch (error) {
      console.error(`关闭连接 ${id} 时出错:`, error.message)
    }
    connections.delete(id)
    return true
  }
  return false
}

// 获取数据库列表
exports.getDatabases = async (connectionId) => {
  if (!connections.has(connectionId)) {
    throw new Error('连接不存在')
  }
  
  const { connection } = connections.get(connectionId)
  const [rows] = await connection.query('SHOW DATABASES')
  return rows.map(row => row.Database)
}

// 获取表列表
exports.getTables = async (connectionId, database) => {
  if (!connections.has(connectionId)) {
    throw new Error('连接不存在')
  }
  
  const { connection } = connections.get(connectionId)
  await connection.query(`USE \`${database}\``)
  const [rows] = await connection.query('SHOW TABLES')
  return rows.map(row => Object.values(row)[0])
}

// 获取列信息
exports.getColumns = async (connectionId, database, table) => {
  if (!connections.has(connectionId)) {
    throw new Error('连接不存在')
  }
  
  const { connection } = connections.get(connectionId)
  await connection.query(`USE \`${database}\``)
  const [rows] = await connection.query(`DESCRIBE \`${table}\``)
  return rows
}

// 执行SQL查询
exports.executeQuery = async (connectionId, sql) => {
  if (!connections.has(connectionId)) {
    throw new Error('连接不存在')
  }
  
  const { connection } = connections.get(connectionId)
  const [rows, fields] = await connection.query(sql)
  
  // 转换结果为更友好的格式
  return {
    data: rows,
    columns: fields.map(field => ({
      name: field.name,
      type: field.type,
      length: field.length,
      flags: field.flags
    }))
  }
}

代码亮点解析:

  • 使用Map存储连接,便于管理
  • 使用mysql2/promise实现基于Promise的异步操作
  • 连接池管理,避免频繁创建/销毁连接
  • 结果格式化,便于前端使用
  • 详细的错误处理

前后端协作:一次完整的查询之旅

让我们通过一个完整的示例,看看用户从点击表到看到数据的全过程:

1. 前端触发查询

javascript 复制代码
// DatabaseView.vue 中的部分代码
const executeQuery = async (sql = null) => {
  if (!currentConnection.value || !currentDatabase.value) return
  
  const queryToExecute = sql || queryEditor.value
  if (!queryToExecute.trim()) {
    ElMessage.warning('请输入SQL查询')
    return
  }
  
  try {
    loading.value = true
    queryResult.value = null
    queryError.value = null
    
    const result = await api.executeQuery(
      currentConnection.value.id,
      queryToExecute
    )
    
    queryResult.value = {
      data: result.data,
      columns: result.columns.map(col => col.name),
      rowCount: result.data.length,
      executionTime: '0.02s' // 实际应该从后端获取
    }
    
    // 如果是SELECT查询,保存到历史记录
    if (/^\s*SELECT/i.test(queryToExecute)) {
      saveToHistory(queryToExecute)
    }
  } catch (error) {
    queryError.value = error.message
    ElMessage.error('查询执行失败: ' + error.message)
  } finally {
    loading.value = false
  }
}

2. 后端处理请求

javascript 复制代码
// routes/database.js 中的部分代码
router.post('/:connectionId/query', async (req, res) => {
  // ...其他代码
  
  const result = await dbManager.executeQuery(connectionId, sql)
  res.json({ 
    success: true, 
    ...result 
  })
})

3. 数据库操作

javascript 复制代码
// config/db.js 中的部分代码
exports.executeQuery = async (connectionId, sql) => {
  // ...其他代码
  
  const [rows, fields] = await connection.query(sql)
  
  return {
    data: rows,
    columns: fields.map(field => ({
      name: field.name,
      type: field.type,
      length: field.length,
      flags: field.flags
    }))
  }
}

4. 前端渲染结果

vue 复制代码
<!-- DatabaseView.vue 中的查询结果展示 -->
<div v-if="queryResult" class="query-result">
  <div class="result-header">
    <span>返回 {{ queryResult.rowCount }} 条记录</span>
    <el-button type="primary" size="small" @click="exportToCSV">导出CSV</el-button>
  </div>
  
  <el-table :data="queryResult.data" style="width: 100%" max-height="500">
    <el-table-column 
      v-for="(col, index) in queryResult.columns" 
      :key="index" 
      :prop="col" 
      :label="col"
      :width="getColumnWidth(col)"
    />
  </el-table>
</div>

从代码到体验:技术如何创造价值

通过这些精心设计的代码,SQLFE实现了:

  1. 直观的连接管理:用户无需记忆复杂参数,通过简单的表单即可建立数据库连接
  2. 流畅的导航体验:树形结构清晰展示数据库层次,懒加载确保大型数据库也能快速响应
  3. 安全的查询执行:基本的SQL注入防护,保护数据库安全
  4. 高效的性能:连接池管理避免频繁创建连接,提升响应速度
  5. 友好的错误处理:清晰的错误提示,帮助用户快速定位问题

结语

通过这些真实的代码示例,我们可以看到SQLFE是如何将抽象的技术概念转化为具体的用户体验的。每一行代码背后,都是对开发者体验的深思熟虑。

求人不如靠自己,我命由我不由天。在这个快速变化的技术世界中,理解代码的本质比盲目依赖框架更重要。SQLFE的每一行代码都经过精心设计,不是简单的复制粘贴,而是对问题的深入思考和解决方案的精准实现。

当你在使用SQLFE时,不妨思考一下背后的实现原理。这不仅会让你更好地使用这个工具,也会提升你解决其他问题的能力。毕竟,真正的技术力量,来自于对原理的理解,而非对工具的依赖。

相关推荐
涡能增压发动积2 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o2 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨2 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz2 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg3213212 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶2 天前
前端交互规范(Web 端)
前端
tyung2 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald2 天前
SpringBoot - 自动配置原理
java·spring boot·后端
CHU7290352 天前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing2 天前
Page-agent MCP结构
前端·人工智能