Bootstrap

vue 构建单页应用_使用Go和Vue构建单页应用

vue 构建单页应用

本文最初发布在Okta开发人员博客上 感谢您支持使SitePoint成为可能的合作伙伴。

单页面应用程序(SPA)通过提供丰富的UI交互,快速的反馈以及无需下载和安装传统应用程序的麻烦,改善了用户体验。 浏览器现在是操作系统,网站是应用程序。 尽管SPA并非总能解决问题,但对于依赖快速用户交互的应用程序,它们变得越来越普遍。

对于最终用户而言,精心设计的SPA就像彩虹和独角兽。 从开发人员的角度来看,现实可能恰好相反。 长期以来一直在后端解决的棘手问题,例如身份验证,路由,状态管理,数据绑定等,这些问题成为费时的前端挑战。 对我们来说幸运的是,存在诸如Vue,React和Angular之类JavaScript框架来帮助我们构建功能强大的应用程序,并将更多的时间投入到关键功能上,而不用浪费时间。

关于Vue.js

谁能比Vue的创造者Evan You更好地描述Vue?

Vue(发音为/vjuː/ ,类似于视图)是用于构建用户界面的渐进框架。 它是从头开始设计的,可以逐步采用,并且可以根据不同的用例在库和框架之间轻松扩展。 它包含一个仅着眼于视图层的可访问的核心库,以及一个可帮助您解决大型单页应用程序中的复杂性的支持库生态系统。

这是Vue的一些好处:

  • 柔和的学习曲线和较低的进入门槛
  • 提供使用vue-cli引导您的应用程序的能力,从而避免了设置Webpack和复杂的构建管道的麻烦
  • 社区爆炸式增长! 现在Vue在GitHub上的明星数量超过了React和Angular
  • 它足够灵活,可以以合理的速度逐个组件地采用

创建您的Vue + Go应用

在本教程中,您将创建一个单页应用程序,该应用程序显示对GitHub上的开源项目的热爱。 对于前端,您将使用Vue和流行的工具,如vuexvue-clivuetifyvue-router 。 在后端,您将使用Go编写REST API并将数据持久保存在MongoDB中。

身份验证和用户管理可能是一个主要的难题,因此,当您从SPA发出请求并使用Okta的Go JWT验证程序作为后端的中间件时,您将使用基于JSON Web令牌(JWT)的身份验证 ,以在每次请求时验证用户的令牌。

完成后,用户将能够通过OpenID Connect(OIDC)进行身份验证,在GitHub上搜索项目,收藏这些项目,甚至在需要的地方添加注释!

创建Vue and Go目录结构

为了简单起见,让我们从Go工作区中的项目目录开始,在同一项目中编写REST API和SPA。

Go项目位于环境变量$GOPATH指向的目录中。 为了找到当前的$GOPATH值,运行: go env GOPATH 。 要了解有关GOPATH的更多信息(包括如何自行设置),请参阅关于该主题的官方Go文档

如果您完全不熟悉Go,请查看本文 ,以了解如何在GOPATH目录中组织项目。

定义了GOPATH之后,您现在可以为项目创建目录:

mkdir -p $GOPATH/src/github.com/{YOUR_GITHUB_USERNAME}/kudo-oos

要快速启动SPA,请利用vue-cli的脚手架功能。 CLI将提示您一系列选项–选择适合该项目的技术: vue.jsvuexwebpack

通过运行以下命令安装vue-cli

yarn global add @vue/cli

然后,创建一个新的Vue项目:

mkdir -p pkg/http/web
cd pkg/http/web
vue create app

系统将提示您有关项目构建细节的一系列问题。 对于此应用,请选择所有默认选项。

使用vue-cli初始化程序

恭喜,您已经创建了Vue.js SPA! 通过运行尝试一下:

cd app
yarn install
yarn serve

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

在浏览器中打开以下URL: http:// localhost:8080 ,您应该看到以下内容。

Vue默认页面

接下来,让我们使用vuetify使您的SPA现代化并具有响应vuetify

添加Vuetify

Vuetify是Vue.js组件的集合,该组件抽象了Material Design的概念。 Vuetify提供了开箱即用的功能,包括网格系统,版式,基本布局,以及诸如卡片,对话框,芯片,选项卡,图标等组件。 Vuetify将为丰富的UI铺平道路!

安装vuetify时,系统会提示您一系列问题。 为了简单起见,请再次使用默认选项。

vue add vuetify

添加vueify

再次启动SPA,以查看是否有效。

yarn serve

验证页面

使用Okta向您的Vue应用添加身份验证

编写安全的用户身份验证和建立登录页面很容易出错,并且可能是新项目的失败。 Okta使快速,安全地实现所有用户管理功能变得简单。 通过注册免费的开发人员帐户并在Okta中创建OIDC应用程序来开始使用。

Okta注册页面

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

Okta添加应用

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

Okta创建应用程序

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

Okta应用程序设置

接下来,通过运行以下命令安装Okta Vue SDK:

yarn add @okta/okta-vue

创建您的Vue应用路线

对于此应用程序,您仅需要4条路由,除了登录路由外,所有这些路由均需要身份验证。

根路径/是我们的登录页面,将在其中显示登录组件。 用户通过身份验证后,我们会将其重定向到发生大量功能的/me路由:用户应该能够通过GitHub的REST API查询OSS项目,查询返回的喜欢的项目,请参阅有关项目,并留下说明为什么该项目对他们很重要的注释。

请注意, /merepo/:id都具有meta: { requiresAuth: true }属性,该属性指定用户必须经过身份验证才能访问应用程序的该区域。 如果未通过身份验证,Okta插件将使用它将用户重定向到Okta的登录页面。

首先,创建pkg/http/web/app/src/routes.js并定义以下路由:

import Vue from 'vue';
import VueRouter from 'vue-router';
import Auth from '@okta/okta-vue'

import Home from './components/Home';
import Login from './components/Login';
import GitHubRepoDetails from './components/GithubRepoDetails';

Vue.use(VueRouter);
Vue.use(Auth, {
  issuer: {ADD_YOUR_DOMAIN},
  client_id: {ADD_YOUR_CLIENT_ID},
  redirect_uri: 'http://localhost:8080/implicit/callback',
  scope: 'openid profile email'
})

export default new VueRouter({
 mode: 'history',
 routes: [
   { path: '/', component: Login },
   { path: '/me', component: Home, meta: { requiresAuth: true }},
   { name: 'repo-details', path: '/repo/:id', component: GitHubRepoDetails, meta: { requiresAuth: true } },
   { path: '/implicit/callback', component: Auth.handleCallback() }
 ]
});

确保在指定的位置添加您的domainclient_id –这些值可以在Okta开发人员控制台的应用程序概述页面上找到。 调用Vue.use(Auth, ...)会将authClient对象注入到Vue实例中,可以通过在Vue实例内的任何位置调用this.$auth来访问该对象。 这是用来确保用户登录和/或强制用户标识自己的方式!

创建Vue组件

vue-router库包含许多组件,可帮助开发人员创建动态且丰富的UI。 其中之一, router-view, renders the component for the matched route. In our case, when the user accesses the root route router-view, renders the component for the matched route. In our case, when the user accesses the root route /时, vue-router will render the routers.js`中component as configured in will render the Login component as configured in

打开./kudo-oos/pkg/http/web/app/src/components/App.vue并复制以下代码。

<template>
 <v-app>
   <router-view></router-view>
   <Footer />
 </v-app>
</template>

<script>
import Footer from '@/components/Footer.vue'

export default {
 name: 'App',
 components: { Footer },
 data() {
   return {}
 }
}
</script>

对于除匹配的路线组件以外的所有其他路线,Vue都将渲染Footer组件。 创建./kudo-oos/pkg/http/web/app/src/components/Footer.vue并复制以下代码以创建./kudo-oos/pkg/http/web/app/src/components/Footer.vue脚组件。

<template>
 <v-footer class="pa-3 white--text" color="teal" absolute>
   <div>
     Developed with ❤️  by {{YOUR_NAME}} &copy; {{ new Date().getFullYear() }}
   </div>
 </v-footer>
</template>

现在,您的目标网页应如下所示:

Vue登陆页面

呈现我们的登录组件后,单击“登录”按钮后,用户将被重定向到登录页面。

Okta登录

成功登录后,该用户将被重定向回您的应用程序,并配置为路由。 在我们的应用程序中,这就是/me路线。

Vue应用

/me路由配置为呈现Home组件,该组件又呈现Sidebar ,Kudos和Search vuetify tabs 。 每个选项卡都呈现一组特定的GitHubRepo

继续并创建./kudo-oos/pkg/http/web/app/src/components/Home.vue组件。

<template>
 <div>
   <SearchBar v-on:search-submitted="githubQuery" />
   <v-container grid-list-md fluid class="grey lighten-4" >
        <v-tabs
       slot="extension"
       v-model="tabs"
       centered
       color="teal"
       text-color="white"
       slider-color="white"
     >
       <v-tab class="white--text" :key="2">
         KUDOS
       </v-tab>
       <v-tab class="white--text" :key="1">
         SEARCH
       </v-tab>
     </v-tabs>
       <v-tabs-items style="width:100%" v-model="tabs">
         <v-tab-item :key="2">
           <v-layout row wrap>
             <v-flex v-for="kudo in allKudos" :key="kudo.id" md4 >
               <GitHubRepo :repo="kudo" />
             </v-flex>
           </v-layout>
         </v-tab-item>
         <v-tab-item :key="1">
           <v-layout row wrap>
             <v-flex v-for="repo in repos" :key="repo.id" md4>
               <GitHubRepo :repo="repo" />
             </v-flex>
           </v-layout>
         </v-tab-item>
       </v-tabs-items>
   </v-container>
 </div>
</template>

<script>
import SearchBar from './SearchBar.vue'
import GitHubRepo from './GithubRepo.vue'
import githubClient from '../githubClient'
import { mapMutations, mapGetters, mapActions } from 'vuex'

export default {
 name: 'Home',
 components: { SearchBar, GitHubRepo },
 data() {
   return {
     tabs: 0
   }
 },
 computed: mapGetters(['allKudos', 'repos']),
 created() {
   this.getKudos();
 },
 methods: {
   githubQuery(query) {
     this.tabs = 1;
     githubClient
       .getJSONRepos(query)
       .then(response => this.resetRepos(response.items) )
   },
   ...mapMutations(['resetRepos']),
   ...mapActions(['getKudos']),
 },
}
</script>

<style>
.v-tabs__content {
  padding-bottom: 2px;
}
</style>

SearchBarHome呈现的第一个组件。 当用户在Sidebar的文本输入中输入查询时,该组件将触发对Github API的调用。 SearchBar只是向其父级Home发出一个事件,该事件包含githubQuery

./kudo-oos/pkg/http/web/app/src/components/SearchBar.vue应该看起来像这样:

<template>
   <v-toolbar dark color="teal">
     <v-spacer></v-spacer>
     <v-text-field
       solo-inverted
       flat
       hide-details
       label="Search for your OOS project on Github + Press Enter"
       prepend-inner-icon="search"
       v-model="query"
       @keyup.enter="onSearchSubmition"
     ></v-text-field>
     <v-spacer></v-spacer>
     <button @click.prevent="logout">Logout</button>
   </v-toolbar>
</template>

<script>
export default {
   data() {
     return {
       query: null,
     };
   },
   props: ['defaultQuery'],
   methods: {
     onSearchSubmition() {
       this.$emit('search-submitted', this.query);
     },
     async logout () {
       await this.$auth.logout()
       this.$router.push('/')
   }
 }
}
</script>

感谢@keyup.enter="onSearchSubmition" ,每当用户点击输入时, onSearchSubmition就会发出带有查询值的search-submitted 。 我们如何捕获您可能会问的事件? 简单! 在Home组件上,安装Sidebar组件时,还添加了一个“侦听器” v-on:search-submitted="githubQuery"githubQuery在每个search-submitted事件上调用githubQuery

Sidebar还负责注销用户。 Okta Vue SDK提供了一种方便的方法,可以使用this.$auth.logout()方法清理会话。 每当用户注销时,都可以将其重定向到登录页面。

Home呈现的第二个组件是GithupRepo 。 该组件在两个选项卡中使用:第一个选项卡Kudos表示用户的收藏夹OSS项目,而“ Search选项卡呈现从GitHub返回的OSS项目。

<template>
 <v-card >
   <v-card-title primary-title>
     <div class="repo-card-content">
       <h3 class="headline mb-0">
         <router-link :to="{ name: 'repo-details', params: { id: repo.id }}" >{{repo.full_name}}</router-link>
       </h3>
       <div>{{repo.description}}</div>
     </div>
   </v-card-title>
   <v-card-actions>
     <v-chip>
       {{repo.language}}
     </v-chip>
     <v-spacer></v-spacer>
     <v-btn @click.prevent="toggleKudo(repo)"  flat icon color="pink">
       <v-icon v-if="isKudo(repo)">favorite</v-icon>
       <v-icon v-else>favorite_border</v-icon>
     </v-btn>
   </v-card-actions>
 </v-card>
</template>

<script>
import { mapActions } from 'vuex';

export default {
 data() {
   return {}
 },
 props: ['repo'],
 methods: {
   isKudo(repo) {
     return this.$store.getters.isKudo(repo);
   },
   ...mapActions(['toggleKudo'])
 }
}
</script>

<style>
.repo-card-content {
  height: 90px;
  overflow: scroll;
}
</style>

您的SPA使用vuex在所有组件均可访问的一个集中存储中管理状态。 Vuex还确保遵循一些规则以可预测的方式执行对商店的访问。 要读取状态,您需要定义getters ,必须通过mutations完成对状态的同步更改,而要通过actions完成异步更改。

要安装vuex,请运行:

yarn add vuex

现在,您需要创建包含actionsmutationsgetters ./kudo-oos/pkg/http/web/app/src/store.js 。 您的初始数据为{ kudos: {}, repos: [] }kudos将所有用户的收藏夹OSS项目保存为JavaScript对象,其中的键是项目ID,值是项目本身。 repos是一个保存搜索结果的数组。

在两种情况下,您可能需要改变状态。 首先,当你在用户登录需要从围棋服务器获取用户的收藏夹OSS项目,并设置repos通过调用在店里resetRepos 。 其次,当用户收藏或不喜欢OSS项目时,您需要通过调用resetKudos来反映服务器上的更改来更新存储中的kudos

resetKudos是一种同步方法,每次调用Go服务器后,异步函数中的actions都会调用该方法。

Home组件使用getters allKudosrepos来呈现Kudos和SearchResults的列表。 为了知道repo是否已被收藏,您的应用程序需要调用isKudo getter。

使用以下代码创建./kudo-oos/pkg/http/web/app/src/store.js

import Vue from 'vue';
import Vuex from 'vuex';

import APIClient from './apiClient';

Vue.use(Vuex);

const store = new Vuex.Store({
 state: {
   kudos: {},
   repos: [],
 },
 mutations: {
   resetRepos (state, repos) {
     state.repos = repos;
   },
   resetKudos(state, kudos) {
     state.kudos = kudos;
   }
 },
 getters: {
   allKudos(state) {
     return Object.values(state.kudos);
   },
   kudos(state) {
     return state.kudos;
   },
   repos(state) {
     return state.repos;
   },
   isKudo(state) {
     return (repo)=> {
       return !!state.kudos[repo.id];
     };
   }
 },
 actions: {
   getKudos ({commit}) {
     APIClient.getKudos().then((data) => {
       commit('resetKudos', data.reduce((acc, kudo) => {
                              return {[kudo.id]: kudo, ...acc}
                            }, {}))
     })
   },
   updateKudo({ commit, state }, repo) {
     const kudos = { ...state.kudos, [repo.id]: repo };

     return APIClient
       .updateKudo(repo)
       .then(() => {
         commit('resetKudos', kudos)
       });
   },
   toggleKudo({ commit, state }, repo) {
     if (!state.kudos[repo.id]) {
       return APIClient
         .createKudo(repo)
         .then(kudo => commit('resetKudos', { [kudo.id]: kudo, ...state.kudos }))
     }

     const kudos = Object.entries(state.kudos).reduce((acc, [repoId, kudo]) => {
                     return (repoId == repo.id) ? acc
                                                : { [repoId]: kudo, ...acc };
                   }, {});

     return APIClient
       .deleteKudo(repo)
       .then(() => commit('resetKudos', kudos));
   }
 }
});

export default store;

在内部actions您正在执行对Go服务器的Ajax调用。 对服务器的每个请求都必须经过身份验证,否则服务器将以客户端错误进行响应。 当用户登录时,将创建一个访问令牌,可以通过调用以下await Vue.prototype.$auth.getAccessToken()进行访问: await Vue.prototype.$auth.getAccessToken() 。 该异步函数返回将已认证的请求发送到服务器所需的访问令牌。

Go服务器公开了适用于kudo资源的REST API。 您将实现一些方法来进行ajax调用,以便使用createKudo创建,使用createKudo更新,使用updateKudo删除以及使用deleteKudo列出所有getKudos 。 请注意,这些方法通过传递端点和HTTP动词来调用perform方法。 perform会使用访问令牌填充请求Authorization标头,以便Go服务器可以验证请求。

使用以下代码创建./kudo-oos/pkg/http/web/app/src/apiClient.js

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

const BASE_URI = 'http://localhost:4444';

const client = axios.create({
  baseURL: BASE_URI,
  json: true
});

const APIClient =  {
  createKudo(repo) {
    return this.perform('post', '/kudos', repo);
  },

  deleteKudo(repo) {
    return this.perform('delete', `/kudos/${repo.id}`);
  },

  updateKudo(repo) {
    return this.perform('put', `/kudos/${repo.id}`, repo);
  },

  getKudos() {
    return this.perform('get', '/kudos');
  },

  getKudo(repo) {
    return this.perform('get', `/kudo/${repo.id}`);
  },

  async perform (method, resource, data) {
    let accessToken = await Vue.prototype.$auth.getAccessToken()
    return client({
      method,
      url: resource,
      data,
      headers: {
        Authorization: `Bearer ${accessToken}`
      }
    }).then(req => {
      return req.data
    })
  }
}

export default APIClient;

每个GithubRepo都有一个router-link /repo/:idrouter-link ,该router-link呈现了GithubRepoDetails组件。 GithubRepoDetails显示有关OSS项目的详细信息,例如该项目已GithubRepoDetails星标的次数以及未解决的问题的数量。 用户还可以通过单击“工藤”按钮留下说明为什么项目特别的注释。 通过调用updateKudo将消息发送到Go服务器按钮。

使用以下代码创建./kudo-oos/pkg/http/web/app/src/components/GithubRepoDetails.js

<template>
  <v-container grid-list-md fluid class="grey lighten-4" >
    <v-layout align-center justify-space-around wrap>
      <v-flex md6>
        <h1 class="primary--text">
          <a :href="repo.html_url">{{repo.full_name}}</a>
        </h1>

        <v-chip class="text-xs-center">
          <v-avatar class="teal">
            <v-icon class="white--text">star</v-icon>
          </v-avatar>
          Stars: {{repo.stargazers_count}}
        </v-chip>

        <v-chip class="text-xs-center">
          <v-avatar class="teal white--text">L</v-avatar>
          Language: {{repo.language}}
        </v-chip>

        <v-chip class="text-xs-center">
          <v-avatar class="teal white--text">O</v-avatar>
          Open Issues: {{repo.open_issues_count}}
        </v-chip>

        <v-textarea
          name="input-7-1"
          label="Show some love"
          value=""
          v-model="repo.notes"
          hint="Describe why you love this project"
        ></v-textarea>
        <v-btn @click.prevent="updateKudo(repo)"> Kudo </v-btn>
        <router-link tag="a" to="/me">Back</router-link>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';
import githubClient from '../githubClient';

export default {
  data() {
    return {
      repo: {}
    }
  },
  watch: {
    '$route': 'fetchData'
  },
  computed: mapGetters(['kudos']),
  created() {
    this.fetchData();
  },
  methods: {
    fetchData() {
      githubClient
        .getJSONRepo(this.$route.params.id)
        .then((response) => {
          this.repo = Object.assign(response, this.kudos[this.$route.params.id])
        })
    },
    ...mapActions(['updateKudo'])
  }
}
</script>

现在您的路由器,商店和组件已经安装到位,继续并修改./kudo-oos/pkg/http/web/app/src/main.js以正确初始化SPA。

import '@babel/polyfill'
import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import store from './store'
import router from './routes'

Vue.config.productionTip = process.env.NODE_ENV == 'production';

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

new Vue({
 store,
 router,
 render: h => h(App)
}).$mount('#app')

请注意,我们正在调用router.beforeEach(Vue.prototype.$auth.authRedirectGuard())来查找标记为meta: {requiresAuth: true}路由meta: {requiresAuth: true} ,如果用户未登录,则将其重定向到身份验证流。

使用Go创建REST API

现在用户可以在前端安全地进行身份验证了,您需要创建一个用Go语言编写的HTTP服务器来处理请求,验证用户是否已通过身份验证以及执行CRUD操作。

我喜欢使用dep工具来管理依赖项,因此请确保从此处安装它然后再继续。

dep init
dep ensure -add github.com/okta/okta-jwt-verifier-golang
dep ensure -add github.com/rs/cors
dep ensure -add github.com/globalsign/mgo

现在,您需要一个结构来表示GitHub存储库。 首先创建./kudo-oos/pkg/core/kudo.go然后定义以下结构来表示“ kudo”(有人为特定的仓库提供荣誉)。

package core

// Kudo represents a oos kudo.
type Kudo struct {
  UserID      string `json:"user_id" bson:"userId"`
  RepoID      string `json:"id" bson:"repoId"`
  RepoName    string `json:"full_name" bson:"repoName"`
  RepoURL     string `json:"html_url" bson:"repoUrl"`
  Language    string `json:"language" bson:"language"`
  Description string `json:"description" bson:"description"`
  Notes       string `json:"notes" bson:"notes"`
}

接下来,创建./kudo-oos/pkg/core/repository.go文件,并添加以下接口来表示您可能要使用的任何持久层的API。 在本文中,我们将使用MongoDB。

package core
// Repository defines the API a repository implementation should follow.
type Repository interface {
  Find(id string) (*Kudo, error)
  FindAll(selector map[string]interface{}) ([]*Kudo, error)
  Delete(kudo *Kudo) error
  Update(kudo *Kudo) error
  Create(kudo ...*Kudo) error
  Count() (int, error)
}

最后,创建实现刚刚创建的接口的MongoDB存储库。 创建./kudo-oos/pkg/storage/mongo.go并添加以下代码。

package storage

import (
  "log"
  "os"

  "github.com/globalsign/mgo"
  "github.com/globalsign/mgo/bson"
  "github.com/{YOUR_GITHUB_USERNAME}/kudo-oos/pkg/core"
)

const (
  collectionName = "kudos"
)

func GetCollectionName() string {
  return collectionName
}

type MongoRepository struct {
  logger  *log.Logger
  session *mgo.Session
}

// Find fetches a kudo from mongo according to the query criteria provided.
func (r MongoRepository) Find(repoID string) (*core.Kudo, error) {
  session := r.session.Copy()
  defer session.Close()
  coll := session.DB("").C(collectionName)

  var kudo core.Kudo
  err := coll.Find(bson.M{"repoId": repoID, "userId": kudo.UserID}).One(&kudo)
  if err != nil {
    r.logger.Printf("error: %v\n", err)
    return nil, err
  }
  return &kudo, nil
}

// FindAll fetches kudos from the database.
func (r MongoRepository) FindAll(selector map[string]interface{}) ([]*core.Kudo, error) {
  session := r.session.Copy()
  defer session.Close()
  coll := session.DB("").C(collectionName)

  var kudos []*core.Kudo
  err := coll.Find(selector).All(&kudos)
  if err != nil {
    r.logger.Printf("error: %v\n", err)
    return nil, err
  }
  return kudos, nil
}

// Delete deletes a kudo from mongo according to the query criteria provided.
func (r MongoRepository) Delete(kudo *core.Kudo) error {
  session := r.session.Copy()
  defer session.Close()
  coll := session.DB("").C(collectionName)

  return coll.Remove(bson.M{"repoId": kudo.RepoID, "userId": kudo.UserID})
}

// Update updates an kudo.
func (r MongoRepository) Update(kudo *core.Kudo) error {
  session := r.session.Copy()
  defer session.Close()
  coll := session.DB("").C(collectionName)

  return coll.Update(bson.M{"repoId": kudo.RepoID, "userId": kudo.UserID}, kudo)
}

// Create kudos in the database.
func (r MongoRepository) Create(kudos ...*core.Kudo) error {
  session := r.session.Copy()
  defer session.Close()
  coll := session.DB("").C(collectionName)

  for _, kudo := range kudos {
    _, err := coll.Upsert(bson.M{"repoId": kudo.RepoID, "userId": kudo.UserID}, kudo)
    if err != nil {
      return err
    }
  }

  return nil
}

// Count counts documents for a given collection
func (r MongoRepository) Count() (int, error) {
  session := r.session.Copy()
  defer session.Close()
  coll := session.DB("").C(collectionName)
  return coll.Count()
}

// NewMongoSession dials mongodb and creates a session.
func newMongoSession() (*mgo.Session, error) {
  mongoURL := os.Getenv("MONGO_URL")
  if mongoURL == "" {
    log.Fatal("MONGO_URL not provided")
  }
  return mgo.Dial(mongoURL)
}

func newMongoRepositoryLogger() *log.Logger {
  return log.New(os.Stdout, "[mongoDB] ", 0)
}

func NewMongoRepository() core.Repository {
  logger := newMongoRepositoryLogger()
  session, err := newMongoSession()
  if err != nil {
    logger.Fatalf("Could not connect to the database: %v\n", err)
  }

  return MongoRepository{
    session: session,
    logger:  logger,
  }
}

添加Go后端

在创建HTTP处理程序之前,您需要编写代码来处理传入的请求有效负载。

创建./kudo-oos/pkg/kudo/service.go并插入以下代码。

package kudo

import (
  "strconv"

  "github.com/{YOUR_GITHUB_USERNAME}/kudo-oos/pkg/core"
)

type GitHubRepo struct {
  RepoID      int64  `json:"id"`
  RepoURL     string `json:"html_url"`
  RepoName    string `json:"full_name"`
  Language    string `json:"language"`
  Description string `json:"description"`
  Notes       string `json:"notes"`
}

type Service struct {
  userId string
  repo   core.Repository
}

func (s Service) GetKudos() ([]*core.Kudo, error) {
  return s.repo.FindAll(map[string]interface{}{"userId": s.userId})
}

func (s Service) CreateKudoFor(githubRepo GitHubRepo) (*core.Kudo, error) {
  kudo := s.githubRepoToKudo(githubRepo)
  err := s.repo.Create(kudo)
  if err != nil {
    return nil, err
  }
  return kudo, nil
}

func (s Service) UpdateKudoWith(githubRepo GitHubRepo) (*core.Kudo, error) {
  kudo := s.githubRepoToKudo(githubRepo)
  err := s.repo.Create(kudo)
  if err != nil {
    return nil, err
  }
  return kudo, nil
}

func (s Service) RemoveKudo(githubRepo GitHubRepo) (*core.Kudo, error) {
  kudo := s.githubRepoToKudo(githubRepo)
  err := s.repo.Delete(kudo)
  if err != nil {
    return nil, err
  }
  return kudo, nil
}

func (s Service) githubRepoToKudo(githubRepo GitHubRepo) *core.Kudo {
  return &core.Kudo{
    UserID:      s.userId,
    RepoID:      strconv.Itoa(int(githubRepo.RepoID)),
    RepoName:    githubRepo.RepoName,
    RepoURL:     githubRepo.RepoURL,
    Language:    githubRepo.Language,
    Description: githubRepo.Description,
    Notes:       githubRepo.Notes,
  }
}

func NewService(repo core.Repository, userId string) Service {
  return Service{
    repo:   repo,
    userId: userId,
  }
}

定义Go HTTP处理程序

REST API公开了kudo资源以支持SPA等客户端。 普通的SPA将公开端点,以便客户端可以创建,更新,删除和列出资源。 例如,当用户登录时,将请求通过GET /kudos获取已认证用户的所有GET /kudos

# Fetches all open source projects favorited by the user
GET /kudos
# Fetches a favorited open source project by id
GET /kudos/:id
# Creates (or favorites)  a open source project for the logged in user
POST /kudos
# Updates  a favorited open source project
PUT /kudos/:id
# Deletes (or unfavorites) a favorited open source project
DELETE /kudos/:id

为此,您需要添加名为./kudo-oos/pkg/http/handlers.go新文件,并使用神话般的httprouter库定义HTTP处理程序。

package http

import (
  "encoding/json"
  "io/ioutil"
  "net/http"
  "strconv"

  "github.com/julienschmidt/httprouter"
  "github.com/{YOUR_GITHUB_USERNAME}/kudo-oos/pkg/core"
  "github.com/{YOUR_GITHUB_USERNAME}/kudo-oos/pkg/kudo"
)

type Service struct {
  repo   core.Repository
  Router http.Handler
}

func New(repo core.Repository) Service {
  service := Service{
    repo: repo,
  }

  router := httprouter.New()
  router.GET("/kudos", service.Index)
  router.POST("/kudos", service.Create)
  router.DELETE("/kudos/:id", service.Delete)
  router.PUT("/kudos/:id", service.Update)

  service.Router = UseMiddlewares(router)

  return service
}

func (s Service) Index(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
  service := kudo.NewService(s.repo, r.Context().Value("userId").(string))
  kudos, err := service.GetKudos()

  if err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  w.WriteHeader(http.StatusOK)
  json.NewEncoder(w).Encode(kudos)
}

func (s Service) Create(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
  service := kudo.NewService(s.repo, r.Context().Value("userId").(string))
  payload, _ := ioutil.ReadAll(r.Body)

  githubRepo := kudo.GitHubRepo{}
  json.Unmarshal(payload, &githubRepo)

  kudo, err := service.CreateKudoFor(githubRepo)

  if err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  w.WriteHeader(http.StatusCreated)
  json.NewEncoder(w).Encode(kudo)
}

func (s Service) Delete(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
  service := kudo.NewService(s.repo, r.Context().Value("userId").(string))

  repoID, _ := strconv.Atoi(params.ByName("id"))
  githubRepo := kudo.GitHubRepo{RepoID: int64(repoID)}

  _, err := service.RemoveKudo(githubRepo)
  if err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  w.WriteHeader(http.StatusOK)
}

func (s Service) Update(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
  service := kudo.NewService(s.repo, r.Context().Value("userId").(string))
  payload, _ := ioutil.ReadAll(r.Body)

  githubRepo := kudo.GitHubRepo{}
  json.Unmarshal(payload, &githubRepo)

  kudo, err := service.UpdateKudoWith(githubRepo)
  if err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  w.WriteHeader(http.StatusOK)
  json.NewEncoder(w).Encode(kudo)
}

使用Go验证JSON Web令牌(JWT)

这是REST API服务器中最关键的组件。 没有此中间件,任何用户都可以对数据库执行CRUD操作。

如果在HTTP授权标头中未提供有效的JWT,则API调用将中止,并且将错误返回给客户端。

创建./kudo-oos/pkg/http/middlewares.go并粘贴以下代码:

package http

import (
  "context"
  "log"
  "net/http"
  "strings"

  jwtverifier "github.com/okta/okta-jwt-verifier-golang"
  "github.com/rs/cors"
)

func OktaAuth(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    accessToken := r.Header["Authorization"]
    jwt, err := validateAccessToken(accessToken)
    if err != nil {
      w.WriteHeader(http.StatusForbidden)
      w.Write([]byte(err.Error()))
      return
    }
    ctx := context.WithValue(r.Context(), "userId", jwt.Claims["sub"].(string))
    h.ServeHTTP(w, r.WithContext(ctx))
  })
}

func validateAccessToken(accessToken []string) (*jwtverifier.Jwt, error) {
  parts := strings.Split(accessToken[0], " ")
  jwtVerifierSetup := jwtverifier.JwtVerifier{
    Issuer:           "{DOMAIN}",
    ClaimsToValidate: map[string]string{"aud": "api://default", "cid": "{CLIENT_ID}"},
  }
  verifier := jwtVerifierSetup.New()
  return verifier.VerifyIdToken(parts[1])
}

func JSONApi(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    h.ServeHTTP(w, r)
  })
}

func AccsessLog(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Printf("%s: %s", r.Method, r.RequestURI)
    h.ServeHTTP(w, r)
  })
}

func Cors(h http.Handler) http.Handler {
  corsConfig := cors.New(cors.Options{
    AllowedHeaders: []string{"Origin", "Accept", "Content-Type", "X-Requested-With", "Authorization"},
    AllowedMethods: []string{"POST", "PUT", "GET", "PATCH", "OPTIONS", "HEAD", "DELETE"},
    Debug:          true,
  })
  return corsConfig.Handler(h)
}

func UseMiddlewares(h http.Handler) http.Handler {
  h = JSONApi(h)
  h = OktaAuth(h)
  h = Cors(h)
  return AccsessLog(h)
}

如您所见,中间件OktaAuth使用OktaAuth -jwt-verifier-golang来验证用户的访问令牌。

定义您的Go REST API入口点

打开./kudo-oos/pkg/cmd/main.go并添加以下代码以启动Go Web服务器。

package main

import (
  "log"
  "net/http"
  "os"

  web "github.com/{YOUR_GITHUB_USERNAME}/kudo-oos/pkg/http"
  "github.com/{YOUR_GITHUB_USERNAME}/kudo-oos/pkg/storage"
)

func main() {
  httpPort := os.Getenv("PORT")

  repo := storage.NewMongoRepository()
  webService := web.New(repo)

  log.Printf("Running on port %s\n", httpPort)
  log.Fatal(http.ListenAndServe(httpPort, webService.Router))
}

运行Go + Vue SPA

有很多方法可以运行后端和前端应用程序。 (出于开发目的)最简单的方法是仅使用老式的Make

Makefile包含您网站的构建说明。 就像旧版本的gulpgrunt和更时髦的Node工具。 首先,在项目文件夹的根目录中创建一个名为Makefile的文件,然后复制以下代码。

setup: run_services
    @go run ./cmd/db/setup.go

run_services:
    @docker-compose up --build -d

run_server:
    @MONGO_URL=mongodb://mongo_user:[email protected]:27017/kudos PORT=:4444 go run cmd/main.go

run_client:
    @/bin/bash -c "cd $$GOPATH/src/github.com/klebervirgilio/kudo-oos/pkg/http/web/app && yarn serve"

创建一个Dockerfile

接下来,您将要创建一个Dockerfile。 该文件告诉Docker如何运行您的应用程序,并节省了部署真正的MongoDB实例进行测试的工作。

您需要做的就是创建一个名为docker-compose.yml的文件,并复制以下代码。

version: '3'
services:
  mongo:
    image: mongo
    restart: always
    ports:
     - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: mongo_user
      MONGO_INITDB_ROOT_PASSWORD: mongo_secret

您的应用现在可以测试了! 运行以下命令开始操作。

make setup
make run_server
make run_client

您的Go网络服务器应该正在监听0.0.0.0:4444而您的SPA应该正在从http://localhost:8080提供文件。 访问http://localhost:8080尝试使用您的新应用!

了解有关Go and Vue的更多信息

Vue.js是一个功能强大且直接的框架,具有惊人的采用率和社区发展。 在本教程中,您学习了使用Vue和Go构建功能齐全的安全SPA。
要了解有关Vue.js的更多信息,请访问https://vuejs.org或从@oktadev团队中查看以下其他重要资源:

如果您有任何疑问,请在评论中让我们知道,或关注并发推特@oktadev

翻译自: https://www.sitepoint.com/build-a-single-page-app-with-go-and-vue/

vue 构建单页应用

;