使用 Vue.js 和 Node 构建基本的 CRUD 应用程序

2025-05-25

使用 Vue.js 和 Node 构建基本的 CRUD 应用程序

多年来,我一直在 JavaScript 框架之间辗转,从 jQuery 开始,然后是 Angular。在 Angular 的复杂性让我感到沮丧之后,我发现了 React,并以为我已经完全搞定了。表面上看似简单的东西,最终却变成了令人沮丧的混乱局面。后来我发现了 Vue.js。感觉很棒。它运行正常,速度很快。文档非常棒。模板功能也很丰富。在如何处理状态管理、条件渲染、双向绑定、路由等方面,大家达成了一致的共识。

本教程将逐步指导您搭建 Vue.js 项目,将安全身份验证转移至Okta 的 OpenID Connect API (OIDC),锁定受保护的路由,并通过后端 REST API 服务器执行 CRUD 操作。本教程将使用以下技术,但无需深入了解即可学习:

关于 Vue.js

Vue.js 是一个强大而简单的 JavaScript 框架。它是所有现代框架中入门门槛最低的框架之一,同时提供了高性能 Web 应用程序所需的所有功能。

Vue.js 主页

本教程涵盖两个主要构建:一个前端 Web 应用和一个后端 REST API 服务器。前端将是一个单页应用程序 (SPA),包含主页、登录/注销以及帖子管理器。

Okta 的 OpenID Connect (OIDC)将通过使用Okta 的 Vue SDK来处理我们 Web 应用的身份验证。如果未经身份验证的用户导航到帖子管理器,Web 应用应尝试对该用户进行身份验证。

该服务器将运行Express以及SequelizeEpilogue。从高层次上讲,使用 Sequelize 和 Epilogue,您只需几行代码即可快速生成动态 REST 端点。

您将在从 Web 应用发出请求时使用基于 JWT 的身份验证,并使用 Express 中间件中的Okta JWT 验证器来验证令牌。您的应用将公开以下端点,所有请求都需要有效的访问令牌。

- GET /posts
- GET /posts/:id
- POST /posts
- PUT /posts/:id
- DELETE /posts/:id
Enter fullscreen mode Exit fullscreen mode

创建你的 Vue.js 应用

为了快速启动项目,您可以利用vue-cli的脚手架功能。在本教程中,您将使用渐进式 Web 应用程序 (PWA) 模板,该模板包含一些功能,包括webpack热重载、CSS 提取和单元测试。

如果您不熟悉 PWA 的原则,请查看我们的渐进式 Web 应用程序终极指南

要安装vue-cli运行:

npm install -g vue-cli@2.9.3
Enter fullscreen mode Exit fullscreen mode

接下来,你需要初始化你的项目。运行vue init命令时,只需接受所有默认值即可。

vue init pwa my-vue-app
cd ./my-vue-app
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

将您最喜欢的浏览器指向http://localhost:8080,您应该会看到您的劳动成果:

欢迎使用您的 Vue.js PWA

额外积分:查看其他可用的模板vue-cli

安装 Bootstrap

让我们安装bootstrap-vue,这样您就可以利用各种预制组件(另外,您可以将重点放在功能上而不是自定义 CSS 上):

npm i bootstrap-vue@2.0.0-rc.7 bootstrap@4.1.0
Enter fullscreen mode Exit fullscreen mode

要完成安装,请修改./src/main.js以包含bootstrap-vue并导入所需的 CSS 文件。您的./src/main.js文件应如下所示:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue)
Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: { App }
})
Enter fullscreen mode Exit fullscreen mode

使用 Okta 添加身份验证

处理 Web 应用中的身份验证是每个开发者的痛点。Okta 正是为此而生,它能以最少的代码保护您的 Web 应用。首先,您需要在 Okta 中创建一个 OIDC 应用。注册一个永久免费的开发者账户(如果您已有账户,请直接登录)。

Okta 开发者注册

登录后,单击“添加应用程序”创建新的应用程序。

添加应用程序

选择“单页应用程序”平台选项。

新的应用程序选项

默认应用程序设置应与图示相同。

Okta 应用程序设置

要安装 Okta Vue SDK,请运行以下命令:

npm i @okta/okta-vue@1.0.0
Enter fullscreen mode Exit fullscreen mode

打开./src/router/index.js并用以下代码替换整个文件。

import Vue from 'vue'
import Router from 'vue-router'
import Hello from '@/components/Hello'
import PostsManager from '@/components/PostsManager'
import Auth from '@okta/okta-vue'

Vue.use(Auth, {
  issuer: 'https://{yourOktaDomain}.com/oauth2/default',
  client_id: '{yourClientId}',
  redirect_uri: 'http://localhost:8080/implicit/callback',
  scope: 'openid profile email'
})

Vue.use(Router)

let router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Hello',
      component: Hello
    },
    {
      path: '/implicit/callback',
      component: Auth.handleCallback()
    },
    {
      path: '/posts-manager',
      name: 'PostsManager',
      component: PostsManager,
      meta: {
        requiresAuth: true
      }
    }
  ]
})

router.beforeEach(Vue.prototype.$auth.authRedirectGuard())

export default router
Enter fullscreen mode Exit fullscreen mode

您需要替换{yourOktaDomain}和 ,{yourClientId}它们可以在 Okta 开发者控制台的应用程序概览页面中找到。这会将一个对象注入到您的 Vue 实例中,您可以通过在 Vue 实例中的任何位置authClient调用来访问该对象。this.$auth

Vue.use(Auth, {
  issuer: 'https://{yourOktaDomain}.com/oauth2/default',
  client_id: '{yourClientId}',
  redirect_uri: 'http://localhost:8080/implicit/callback',
  scope: 'openid profile email'
})
Enter fullscreen mode Exit fullscreen mode

Okta 身份验证流程的最后一步是使用 URL 中的令牌值将用户重定向回您的应用。SDKAuth.handleCallback()中包含的组件负责处理重定向并将令牌保留在浏览器中。

{
  path: '/implicit/callback',
  component: Auth.handleCallback()
}
Enter fullscreen mode Exit fullscreen mode

您还需要锁定受保护的路由,以防止未经身份验证的用户访问。这可以通过实现导航守卫来实现。顾名思义,导航守卫主要用于通过重定向或取消来保护导航。

SDK 附带一种方法auth.authRedirectGuard(),用于检查匹配路由的元数据中的密钥requiresAuth,如果用户未通过身份验证,则将用户重定向到身份验证流程。

router.beforeEach(Vue.prototype.$auth.authRedirectGuard())
Enter fullscreen mode Exit fullscreen mode

安装此导航保护后,任何具有以下元数据的路线都将受到保护。

meta: {
  requiresAuth: true
}
Enter fullscreen mode Exit fullscreen mode

在 Vue 中自定义你的应用布局

Web 应用的布局位于 组件 中./src/App.vue。您可以使用router-view组件根据给定的路径渲染匹配的组件。

对于主菜单,您需要根据以下状态更改某些菜单项的可见性activeUser

  • 未认证:仅显示登录
  • 已验证:仅显示注销

您可以使用 Vue.js 中的指令来切换这些菜单项的可见性,v-if该指令检查组件上是否存在activeUser。当组件加载(调用created())或路由更改时,我们希望刷新activeUser

打开./src/App.vue并复制/粘贴以下代码。

<template>
  <div id="app">
    <b-navbar toggleable="md" type="dark" variant="dark">
      <b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
      <b-navbar-brand to="/">My Vue App</b-navbar-brand>
      <b-collapse is-nav id="nav_collapse">
        <b-navbar-nav>
          <b-nav-item to="/">Home</b-nav-item>
          <b-nav-item to="/posts-manager">Posts Manager</b-nav-item>
          <b-nav-item href="#" @click.prevent="login" v-if="!activeUser">Login</b-nav-item>
          <b-nav-item href="#" @click.prevent="logout" v-else>Logout</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <!-- routes will be rendered here -->
    <router-view />
  </div>
</template>

<script>

export default {
  name: 'app',
  data () {
    return {
      activeUser: null
    }
  },
  async created () {
    await this.refreshActiveUser()
  },
  watch: {
    // everytime a route is changed refresh the activeUser
    '$route': 'refreshActiveUser'
  },
  methods: {
    login () {
      this.$auth.loginRedirect()
    },
    async refreshActiveUser () {
      this.activeUser = await this.$auth.getUser()
    },
    async logout () {
      await this.$auth.logout()
      await this.refreshActiveUser()
      this.$router.push('/')
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

每次登录都必须有一个注销操作。以下代码片段将注销用户,刷新活动用户(现在为空),然后将用户重定向到主页。当用户点击导航栏中的注销链接时,将调用此方法。

async logout () {
  await this.$auth.logout()
  await this.refreshActiveUser()
  this.$router.push('/')
}
Enter fullscreen mode Exit fullscreen mode

组件是 Vue.js 中的构建块。您的每个页面都将在应用中定义为一个组件。由于 vue-cli webpack 模板使用了vue-loader,因此您的组件源文件具有一个将模板、脚本和样式区分开的约定(参见此处)。

现在您已经添加了 vue-bootstrap,请修改./src/components/Hello.vue以删除 vue-cli 生成的样板链接。

<template>
  <div class="hero">
    <div>
      <h1 class="display-3">Hello World</h1>
      <p class="lead">This is the homepage of your vue app</p>
    </div>
  </div>
</template>

<style>
  .hero {
    height: 90vh;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
  }
  .hero .lead {
    font-weight: 200;
    font-size: 1.5rem;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

此时,您可以创建帖子管理器页面来测试身份验证流程。确认身份验证成功后,您就可以开始构建对帖子模型执行 CRUD 操作所需的 API 调用和组件了。

创建一个新文件./src/components/PostsManager.vue并粘贴以下代码:

<template>
  <div class="container-fluid mt-4">
    <h1 class="h1">Posts Manager</h1>
    <p>Only authenticated users should see this page</p>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

试用 Vue.js 前端和身份验证流程

在终端运行npm run dev(如果尚未运行)。导航到http://localhost:8080,您应该会看到新的主页。

你好世界

点击“帖子管理器”“登录”后,您将被引导至 Okta 的流程。输入您的 Okta 开发帐户凭证。

注意:如果您已登录 Okta 开发者帐户,您将自动重定向回应用程序。您可以使用隐身或隐私浏览模式进行测试。

Okta 登录

如果成功,您应该返回到登录的主页。

登录后进入首页

单击帖子管理器链接应该会呈现受保护的组件。

帖子管理器

添加后端 REST API 服务器

现在用户可以安全地进行身份验证,您可以构建 REST API 服务器来对帖子模型执行 CRUD 操作。将以下依赖项添加到您的项目:

npm i express@4.16.3 cors@2.8.4 @okta/jwt-verifier@0.0.11 sequelize@4.37.6 sqlite3@4.0.0 epilogue@0.7.1 axios@0.18.0
Enter fullscreen mode Exit fullscreen mode

然后,创建文件./src/server.js并粘贴以下代码。

const express = require('express')
const cors = require('cors')
const bodyParser = require('body-parser')
const Sequelize = require('sequelize')
const epilogue = require('epilogue')
const OktaJwtVerifier = require('@okta/jwt-verifier')

const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: '{yourClientId}',
  issuer: 'https://{yourOktaDomain}.com/oauth2/default'
})

let app = express()
app.use(cors())
app.use(bodyParser.json())

// verify JWT token middleware
app.use((req, res, next) => {
  // require every request to have an authorization header
  if (!req.headers.authorization) {
    return next(new Error('Authorization header is required'))
  }
  let parts = req.headers.authorization.trim().split(' ')
  let accessToken = parts.pop()
  oktaJwtVerifier.verifyAccessToken(accessToken)
    .then(jwt => {
      req.user = {
        uid: jwt.claims.uid,
        email: jwt.claims.sub
      }
      next()
    })
    .catch(next) // jwt did not verify!
})

// For ease of this tutorial, we are going to use SQLite to limit dependencies
let database = new Sequelize({
  dialect: 'sqlite',
  storage: './test.sqlite'
})

// Define our Post model
// id, createdAt, and updatedAt are added by sequelize automatically
let Post = database.define('posts', {
  title: Sequelize.STRING,
  body: Sequelize.TEXT
})

// Initialize epilogue
epilogue.initialize({
  app: app,
  sequelize: database
})

// Create the dynamic REST resource for our Post model
let userResource = epilogue.resource({
  model: Post,
  endpoints: ['/posts', '/posts/:id']
})

// Resets the database and launches the express app on :8081
database
  .sync({ force: true })
  .then(() => {
    app.listen(8081, () => {
      console.log('listening to port localhost:8081')
    })
  })
Enter fullscreen mode Exit fullscreen mode

确保使用 Okta 中 OIDC 应用程序中的值替换上述代码中的变量{yourOktaDomain}和。{clientId}

添加 Sequelize

Sequelize是一个基于 Promise 的 Node.js ORM。它支持 PostgreSQL、MySQL、SQLite 和 MSSQL 等方言,并具有可靠的事务支持、关系、读取复制等功能。

为了方便本教程,您将使用 SQLite 来限制外部依赖项。以下代码使用 SQLite 作为驱动程序初始化 Sequelize 实例。

let database = new Sequelize({
  dialect: 'sqlite',
  storage: './test.sqlite'
})
Enter fullscreen mode Exit fullscreen mode

每篇文章都有一个titlebody。(字段createdAt、 和updatedAt由 Sequelize 自动添加)。使用 Sequelize,您可以通过调用define()实例来定义模型。

let Post = database.define('posts', {
  title: Sequelize.STRING,
  body: Sequelize.TEXT
})
Enter fullscreen mode Exit fullscreen mode

添加结语

Epilogue在 Express 应用中基于 Sequelize 模型创建灵活的 REST 端点。如果您曾经编写过 REST 端点代码,您就会知道其中有多少重复。DRY FTW!

// Initialize epilogue
epilogue.initialize({
  app: app,
  sequelize: database
})

// Create the dynamic REST resource for our Post model
let userResource = epilogue.resource({
  model: Post,
  endpoints: ['/posts', '/posts/:id']
})
Enter fullscreen mode Exit fullscreen mode

验证您的 JWT

这是 REST API 服务器中最关键的组件。如果没有此中间件,任何用户都可以对我们的数据库执行 CRUD 操作。如果没有授权标头,或者访问令牌无效,API 调用将失败并返回错误。

// verify JWT token middleware
app.use((req, res, next) => {
  // require every request to have an authorization header
  if (!req.headers.authorization) {
    return next(new Error('Authorization header is required'))
  }
  let parts = req.headers.authorization.trim().split(' ')
  let accessToken = parts.pop()
  oktaJwtVerifier.verifyAccessToken(accessToken)
    .then(jwt => {
      req.user = {
        uid: jwt.claims.uid,
        email: jwt.claims.sub
      }
      next()
    })
    .catch(next) // jwt did not verify!
})
Enter fullscreen mode Exit fullscreen mode

运行服务器

打开一个新的终端窗口并使用命令运行服务器node ./src/server。您应该看到来自 Sequelize 的调试信息以及在端口 8081 上监听的应用程序。

完成帖子管理器组件

现在 REST API 服务器已经完成,您可以开始连接帖子管理器来获取帖子、创建帖子、编辑帖子和删除帖子。

我总是将 API 集成集中到一个辅助模块中。这样可以使组件中的代码更加简洁,并且当你需要对 API 请求进行任何更改时,可以提供单一位置。

创建一个文件./src/api.js并将以下代码复制/粘贴到其中:

import Vue from 'vue'
import axios from 'axios'

const client = axios.create({
  baseURL: 'http://localhost:8081/',
  json: true
})

export default {
  async execute (method, resource, data) {
    // inject the accessToken for each request
    let accessToken = await Vue.prototype.$auth.getAccessToken()
    return client({
      method,
      url: resource,
      data,
      headers: {
        Authorization: `Bearer ${accessToken}`
      }
    }).then(req => {
      return req.data
    })
  },
  getPosts () {
    return this.execute('get', '/posts')
  },
  getPost (id) {
    return this.execute('get', `/posts/${id}`)
  },
  createPost (data) {
    return this.execute('post', '/posts', data)
  },
  updatePost (id, data) {
    return this.execute('put', `/posts/${id}`, data)
  },
  deletePost (id) {
    return this.execute('delete', `/posts/${id}`)
  }
}
Enter fullscreen mode Exit fullscreen mode

使用 OIDC 进行身份验证时,访问令牌会本地保存在浏览器中。由于每个 API 请求都必须有访问令牌,因此您可以从身份验证客户端获取它并将其设置在请求中。

let accessToken = await Vue.prototype.$auth.getAccessToken()
return client({
  method,
  url: resource,
  data,
  headers: {
    Authorization: `Bearer ${accessToken}`
  }
})
Enter fullscreen mode Exit fullscreen mode

通过在 API 帮助器内创建以下代理方法,帮助器模块外部的代码保持干净和语义。

getPosts () {
  return this.execute('get', '/posts')
},
getPost (id) {
  return this.execute('get', `/posts/${id}`)
},
createPost (data) {
  return this.execute('post', '/posts', data)
},
updatePost (id, data) {
  return this.execute('put', `/posts/${id}`, data)
},
deletePost (id) {
  return this.execute('delete', `/posts/${id}`)
}
Enter fullscreen mode Exit fullscreen mode

现在,您已拥有连接帖子管理器组件并通过 REST API 进行 CRUD 操作所需的所有组件。打开./src/components/PostsManager.vue并复制/粘贴以下代码。

<template>
  <div class="container-fluid mt-4">
    <h1 class="h1">Posts Manager</h1>
    <b-alert :show="loading" variant="info">Loading...</b-alert>
    <b-row>
      <b-col>
        <table class="table table-striped">
          <thead>
            <tr>
              <th>ID</th>
              <th>Title</th>
              <th>Updated At</th>
              <th>&nbsp;</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="post in posts" :key="post.id">
              <td>{{ post.id }}</td>
              <td>{{ post.title }}</td>
              <td>{{ post.updatedAt }}</td>
              <td class="text-right">
                <a href="#" @click.prevent="populatePostToEdit(post)">Edit</a> - 
                <a href="#" @click.prevent="deletePost(post.id)">Delete</a>
              </td>
            </tr>
          </tbody>
        </table>
      </b-col>
      <b-col lg="3">
        <b-card :title="(model.id ? 'Edit Post ID#' + model.id : 'New Post')">
          <form @submit.prevent="savePost">
            <b-form-group label="Title">
              <b-form-input type="text" v-model="model.title"></b-form-input>
            </b-form-group>
            <b-form-group label="Body">
              <b-form-textarea rows="4" v-model="model.body"></b-form-textarea>
            </b-form-group>
            <div>
              <b-btn type="submit" variant="success">Save Post</b-btn>
            </div>
          </form>
        </b-card>
      </b-col>
    </b-row>
  </div>
</template>

<script>
import api from '@/api'
export default {
  data () {
    return {
      loading: false,
      posts: [],
      model: {}
    }
  },
  async created () {
    this.refreshPosts()
  },
  methods: {
    async refreshPosts () {
      this.loading = true
      this.posts = await api.getPosts()
      this.loading = false
    },
    async populatePostToEdit (post) {
      this.model = Object.assign({}, post)
    },
    async savePost () {
      if (this.model.id) {
        await api.updatePost(this.model.id, this.model)
      } else {
        await api.createPost(this.model)
      }
      this.model = {} // reset form
      await this.refreshPosts()
    },
    async deletePost (id) {
      if (confirm('Are you sure you want to delete this post?')) {
        // if we are editing a post we deleted, remove it from the form
        if (this.model.id === id) {
          this.model = {}
        }
        await api.deletePost(id)
        await this.refreshPosts()
      }
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

列出帖子

您将使用


 to fetch posts from your REST API server. You should refresh the list of posts when the component is loaded and after any mutating operation (create, update, or delete).



```javascript
async refreshPosts () {
  this.loading = true
  this.posts = await api.getPosts()
  this.loading = false
}
Enter fullscreen mode Exit fullscreen mode

切换该属性this.loading是为了使 UI 能够反映待处理的 API 调用。由于 API 请求未发送到互联网,您可能看不到加载消息。

创建帖子

savePosts()组件中包含一个用于保存帖子的表单。表单提交时会调用该函数,其输入信息会绑定到model组件上的对象。

savePost()被调用时,它会根据 的存​​在执行更新或创建model.id。这主要是为了避免定义两个单独的创建和更新表单。

async savePost () {
  if (this.model.id) {
    await api.updatePost(this.model.id, this.model)
  } else {
    await api.createPost(this.model)
  }
  this.model = {} // reset form
  await this.refreshPosts()
}
Enter fullscreen mode Exit fullscreen mode

更新帖子

更新帖子时,必须先将帖子加载到表单中。这model.id将设置触发更新的位置savePost()

async populatePostToEdit (post) {
  this.model = Object.assign({}, post)
}
Enter fullscreen mode Exit fullscreen mode

重要提示:调用Object.assign()复制的是 post 参数的值,而不是引用。在 Vue 中处理对象的变异时,应始终设置为值,而不是引用。

删除帖子

要删除帖子,只需调用api.deletePost(id)。删除前最好先确认,因此我们添加一个原生确认警告框,以确保点击是有意为之。

async deletePost (id) {
  if (confirm('Are you sure you want to delete this post?')) {
    await api.deletePost(id)
    await this.refreshPosts()
  }
}
Enter fullscreen mode Exit fullscreen mode

测试你的 Vue.js + Node CRUD 应用

确保服务器和前端都在运行。

1 号航站楼

node ./src/server
Enter fullscreen mode Exit fullscreen mode

2 号航站楼

npm run dev
Enter fullscreen mode Exit fullscreen mode

导航至http://localhost:8080并尝试一下。

新帖

新的 Hello World 帖子

删除帖子

使用 Vue 做更多事情!

正如我在本文开头所说,我认为 Vue 比其他框架更胜一筹。以下是五个简单原因:

我在本教程中涵盖了很多内容,但如果您第一次没有掌握所有内容,也不必感到沮丧。您使用这些技术越多,就会越熟悉它们。

要了解有关 Vue.js 的更多信息,请访问https://vuejs.org或查看@oktadev 团队提供的其他精彩资源:

您可以在https://github.com/oktadeveloper/okta-vue-node-example找到本文中开发的应用程序的源代码

如果有任何问题,请在评论中向我提出,并且一如既往地在 Twitter 上关注@oktadev以查看我们的开发团队正在创建的所有精彩内容。

文章来源:https://dev.to/oktadev/build-a-basic-crud-app-with-vuejs-and-node-4cl8
PREV
使用 Node.js 构建一个简单的发票生成器
NEXT
DeepCode:你的编码和代码分析 AI 助手简介