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

相关推荐
小高0071 小时前
🌐ES6 这 8 个隐藏外挂,知道 3 个算我输!
前端·javascript·面试
汤姆Tom1 小时前
Node.js 版本管理、NPM 命令、与 NVM 完全指南
前端·npm·node.js
Alan521591 小时前
Java 后端实现基于 JWT 的用户认证和权限校验(含代码讲解)
前端·后端
RoyLin2 小时前
TypeScript设计模式:策略模式
前端·后端·typescript
brzhang2 小时前
为什么说低代码谎言的破灭,是AI原生开发的起点?
前端·后端·架构
得物技术2 小时前
破解gh-ost变更导致MySQL表膨胀之谜|得物技术
数据库·后端·mysql
小桥风满袖3 小时前
极简三分钟ES6 - ES9中字符串扩展
前端·javascript
小码编匠3 小时前
WPF 中的高级交互通过右键拖动实现图像灵活缩放
后端·c#·.net
小Wang3 小时前
npm私有库创建(docker+verdaccio)
前端·docker·npm