React-原生-Web-组建构建指南-三-

60 阅读4分钟

React 原生 Web 组建构建指南(三)

原文:Building Native Web Components

协议:CC BY-NC-SA 4.0

十、发布

恭喜你!我们现在有了自己的 web 应用,使用 Vue.js 和 Web 组件,但我们希望与全世界分享我们的应用。Firebase 托管可以帮助我们公开我们的应用,Firebase 身份验证允许我们添加一个身份验证系统,并注册用户和每个用户的相关注释。

添加 Firebase 身份验证

通过其软件开发工具包,Firebase Authentication 为我们提供了一种在 JavaScript 应用中添加身份验证的安全方法。但是,我们必须选择我们的应用将支持的登录提供商。你可以选择最流行的,如脸书和谷歌,或将登录集成到一个自定义的认证系统。为了简单起见,我们将只选择电子邮件和密码。

首先,我们将转到 Firebase web 控制台并选择身份验证(参见图 10-1 )。

img/494195_1_En_10_Fig1_HTML.jpg

图 10-1

Firebase web 控制台中的身份验证链接

在身份验证中(图 10-2 ,我们将选择登录方式。

img/494195_1_En_10_Fig2_HTML.jpg

图 10-2

Firebase web 控制台中的身份验证部分

现在启用电子邮件/密码(图 10-3 )。

img/494195_1_En_10_Fig3_HTML.jpg

图 10-3

在 Firebase web 控制台中启用电子邮件/密码身份验证

好了,web 控制台到此为止。

回到我们的代码。我们需要一个表单的新组件,我们可以输入电子邮件和密码,以执行身份验证过程。

创建一个新的components/Login.vue文件并添加一个模板,如清单 10-1 所示。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div class="formContainer">
    <div class="formLogin">
      <div class="formFields">
        <h1>{{actionText}}</h1>
        <mwc-textfield
            id="text-email"
            outlined
            minlength="3"
            label="Email"
            required>
          </mwc-textfield>
        </div>
        <div class="formFields">
          <mwc-textfield
            id="text-password"
            type="password"
            outlined
            minlength="3"
            label="Password"
            required>
          </mwc-textfield>
        </div>
        <div class="actionButtons">
          <mwc-button
            id="link-action-button"
            class="actionButton"
            slot="primaryAction"
            @click="goToLink">
            {{linkButton}}
          </mwc-button>
        </div>
        <div class="actionButtons">
          <mwc-button
            raised
            id="primary-action-button"
            class="actionButton"
            slot="primaryAction"
            @click="submitAction">
            {{actionButton}}
          </mwc-button>
          <mwc-button
            raised
            slot="secondary-action-button"
            class="actionButton"
            @click="handleClear">
            Clear
          </mwc-button>
        </div>
      </div>
  </div>
</template>
<script>
import '@material/mwc-textfield';
import '@material/mwc-button';
export default {
  name: 'Login',
  data() {
    return {
      actionText: 'Login',
      actionButton: 'Send',
      linkButton: 'Register'
    }
  },
  methods: {
    goToLink () {
      switch(this.linkButton) {
      case 'Login':
        this.actionText= 'Login';
        this.actionButton= 'Send';
        this.linkButton = 'Register';
        break;
      case 'Register':
        this.actionText= 'Register';
        this.actionButton= 'Register';
        this.linkButton = 'Login';
        break;
      }
    },

}
</script>
<style scoped>
  .formContainer {
    display:flex;
  }

  .formLogin {
    margin:auto;
  }

  .formFields {
    margin: 15px;
  }

  .actionButtons {
    text-align:center;
  }

  .actionButton {
    margin: 10px;
  }
</style>

Listing 10-1Adding a Template in Login.vue

这样,我们就可以添加一个表单来登录和注册用户。我们对两种操作使用相同的视图,actionTextactionButtonlinkButton切换用户在视图中可以看到的文本,如图 10-4 所示。

img/494195_1_En_10_Fig4_HTML.jpg

图 10-4

登录组件

下一步是添加认证特性(清单 10-2 )。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div class="formContainer">
    <div class="formLogin">
      <div class="formFields">
        <h1>{{actionText}}</h1>
        <mwc-textfield
            id="text-email"
            outlined
            minlength="3"
            label="Email"
            required>
          </mwc-textfield>
        </div>
        <div class="formFields">
          <mwc-textfield
            id="text-password"
            type="password"
            outlined
            minlength="3"
            label="Password"
            required>
          </mwc-textfield>
        </div>
        <div class="actionButtons">
          <mwc-button
            id="link-action-button"
            class="actionButton"
            slot="primaryAction"
            @click="goToLink">
            {{linkButton}}
          </mwc-button>
        </div>
        <div class="actionButtons">
          <mwc-button
            raised
            id="primary-action-button"
            class="actionButton"
            slot="primaryAction"
            @click="submitAction">
            {{actionButton}}
          </mwc-button>
          <mwc-button
            raised
            slot="secondary-action-button"
            class="actionButton"
            @click="handleClear">
            Clear
          </mwc-button>
        </div>
      </div>
  </div>
</template>
<script>
import '@material/mwc-textfield';
import '@material/mwc-button';
import { fireApp } from '../firebase';

const auth = fireApp.auth();

export default {
  name: 'Login',
  data() {
    return {
      actionText: 'Login',
      actionButton: 'Send',
      linkButton: 'Register'
    }
  },
  methods: {
    goToLink () {
      switch(this.linkButton) {
      case 'Login':
        this.actionText= 'Login';
        this.actionButton= 'Send';
        this.linkButton = 'Register';
        break;
      case 'Register':
        this.actionText= 'Register';
        this.actionButton= 'Register';
        this.linkButton = 'Login';
        break;
      }
    },
    goToDashboard () {
      this.$router.push('/dashboard');
    },
    signInUser (email, password) {
      auth.signInWithEmailAndPassword(email,password)
      .then(
        () => {
        this.goToDashboard();
        }
      )
      .catch(
        // eslint-disable-next-line
        (error) => {console.log('Something happened.', error)}
      );
    },
    signUpUser (email, password) {
      auth.createUserWithEmailAndPassword(email,password)
      .then(
        // eslint-disable-next-line
        (user) => {console.log('User registered.', user)}
      )
      .catch(
        // eslint-disable-next-line
        (error) => {console.log('Something happened.', error)}
      );
    },
    submitAction () {

      let txtEmail = this.$el.querySelector('#text-email');
      let txtPassword = this.$el.querySelector('#text-password');
      const isValid = txtEmail.checkValidity() && txtPassword.checkValidity();

      if(isValid) {
        switch(this.actionText) {
          case 'Login':
            this.signInUser (txtEmail.value, txtPassword.value);

            txtEmail.value = '';
            txtPassword.value ='';
            break;
          case 'Register':
            this.signUpUser (txtEmail.value, txtPassword.value);

            txtEmail.value = '';
            txtPassword.value ='';
            break;
      }
      }
    }
  },
}
</script>
<style scoped>
  .formContainer {
    display:flex;
  }

  .formLogin {
    margin:auto;
  }

  .formFields {
    margin: 15px;
  }

  .actionButtons {
    text-align:center;
  }

  .actionButton {
    margin: 10px;
  }
</style>

Listing 10-2Adding a Firebase Authentication in Login.vue

这里,我们使用submitAction()方法来检查我们是否在应用中创建新用户或登录,并传递用户在表单中输入的电子邮件和密码。如果用户想要登录 app,我们使用 Firebase auth.signInWithEmailAndPassword()方法来检查用户是否存在于 Firebase 中。如果密码正确,我们将被重定向到仪表板视图。如果用户正在创建一个新帐户,我们使用 Firebase auth.createUserWithEmailAndPassword()方法将这个新用户添加到 Firebase。

最后,我们将修改我们的文件router/index.js来添加这个新组件(清单 10-3 )。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'

const routes = [
  {
    path: '',
    component: Login
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/Dashboard.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
export default router

Listing 10-3Adding the Login Component in the Vue Router

从 GitHub 库( https://github.com/carlosrojaso/apress-book-web-components )可以在$git checkout v1.0.8访问相关代码。

添加防护装置

现在,在App.vue中,我们必须添加一个机制,防止未登录的用户直接从 URL 访问Dashboard.vue。为了实现这一点,我们将使用 Vue 路由的一个名为 Guards 的功能。守卫主要用于通过重定向或取消导航来保护导航。可以在 https://router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards 处了解更多守卫信息。我们将把这个机制添加到我们的router/index.js文件中(清单 10-4 )。

import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'
import { fireApp } from '../firebase';

const routes = [
  {
    path: '',
    component: Login
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ '../views/Dashboard.vue'),
    meta: {
      requiresAuth: true
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

router.beforeEach(async (to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (requiresAuth && !await fireApp.getCurrentUser()) {
    next('Login');
  } else {
    next();
  }
})

export default router

Listing 10-4Adding Navigation Guards in the Vue Router

在这里,我们在/Dashboard中使用一个路由元字段来加强这个路由中的认证,并且我们在 guard 中检查这一点。此外,我们正在检查来自fireAppgetCurrentUser()方法。我们必须在 firebase.js 中添加这个方法(清单 10-5 )。

import Firebase from 'firebase';

let config = {
  apiKey: "xxx",
  authDomain: "xxx",
  databaseURL: "xxx",
  projectId: "xxx",
  storageBucket: "xxx",
  messagingSenderId: "xxx",
  appId: "xxx"
};

Firebase.initializeApp(config)

Firebase.getCurrentUser = () => {
  return new Promise((resolve, reject) => {
      const unsubscribe = Firebase.auth().onAuthStateChanged(user => {
          unsubscribe();
          resolve(user);
      }, reject);
  })
};
export const fireApp = Firebase;

Listing 10-5Adding the getCurrentUser() Method

in firebase.js

getCurrentUser方法为我们提供了关于当前用户或拒绝的信息。最后,我们将在顶部添加一个注销链接,供用户关闭他们的会话。

App.vue中,我们将在<mwc-top-app-bar>(列表 10-6 )中添加一个链接。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <mwc-top-app-bar>
    <div slot="title"><router-link to="/">VueNoteApp</router-link></div>
    <div slot="actionItems"><router-link to="/About">About</router-link></div>
    <div slot="actionItems" v-if="logged" @click="handleLogout">Logout</div>
    <div><router-view/></div>
  </mwc-top-app-bar>
</template>

<script>
import '@material/mwc-top-app-bar';

import { fireApp } from './firebase';
const auth = fireApp.auth();

export default {
  name: 'App',
  data() {
    return {
      logged: false
    }
  },
  mounted() {
    fireApp.getCurrentUser()
      .then((user)=> {
        this.logged = user;
        this.$router.push('Dashboard');
        })
      .catch(() => {
        this.logged = false;
      });
  },
  methods: {
    handleAbout() {
      this.$router.push('About');
    },
    handleLogout() {
      auth.signOut()
        .then(()=>{
          this.$router.push('/login');
          this.logged= false;
          })
        .catch((error)=> {
          // eslint-disable-next-line
          console.log('error', error)
        });
    }
  },
}
</script>

<style>
  a, a:visited {
    color:white;
    text-decoration:none;
    padding: 5px;
  }
</style>

Listing 10-6Adding Logout Mechanism in App.vue

在这里,我们使用fireApp.currentUser()来检查在挂载Login.vue时用户是否登录(),我们是否可以直接跳转到Dashboard.vue,或者我们是否要求用户输入凭证。此外,我们创建了handleLogout(),这是一个使用auth.signOut关闭会话并将用户重定向到Login.vue的方法。当用户登录时,我们使用v-if指令显示注销链接,然后,仅当用户已经登录时,我们向用户显示该链接(图 10-5 )。

img/494195_1_En_10_Fig5_HTML.jpg

图 10-5

注销链接

从 GitHub 库( https://github.com/carlosrojaso/apress-book-web-components )可以在$git checkout v1.0.9访问相关代码。

向数据中添加用户

如果您创建几个用户并添加或删除注释,您可以看到这些注释在所有帐户中共享。这是因为我们没有为每个用户过滤笔记。为了实现这一点,我们必须采取一些额外的步骤。

首先,我们将在用户创建的每个便笺中添加用户 ID,如清单 10-7 所示。

...
    handleAddNote() {
      const formDialog = this.$el.querySelector('#dialog');
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const isValid = txtTitle.checkValidity() && txtDescription.checkValidity();

      if(isValid) {
        const newIndex = uuidv4();
        const newItem = {
          id: newIndex,
          title: txtTitle.value,
          description: txtDescription.value,
          userId: this.user.uid
        };
        this.notes.push(newItem);
        db.push(newItem);

        txtTitle.value ='';
        txtDescription.value = '';
        formDialog.close();
      }
    },
...

Listing 10-7Adding userId Property

in New Notes

现在,数据在每个笔记和创建该笔记的用户之间有了关系(图 10-6 )。

img/494195_1_En_10_Fig6_HTML.jpg

图 10-6

Firebase web 控制台数据库数据

现在我们必须返回 Firebase web 控制台,更新我们的安全规则(图 10-7 )。

img/494195_1_En_10_Fig7_HTML.jpg

图 10-7

Firebase web 控制台数据库安全规则

这样,只有登录的会话才能在我们的数据库中读写数据。此外,我们用userId创建一个索引,以便更快地执行搜索/排序。

现在我们必须过滤笔记(清单 10-8 )。

<!-- eslint-disable vue/no-deprecated-slot-attribute -->
<template>
  <div>
    <mwc-list v-for="(note) in notes" :key="note.id" multi>
      <mwc-list-item twoline hasMeta>
        <span>{{note.title}}</span>
        <span slot="meta" class="material-icons" @click="handleDelete(note.id)">delete</span>
        <span slot="secondary">{{note.description}}</span>
      </mwc-list-item>
      <li divider padded role="separator"></li>
    </mwc-list>
    <mwc-fab class="floatButton" @click="handleAdd" mini icon="add"></mwc-fab>
    <mwc-dialog id="dialog" heading="Add Note">
      <div class="formFields">
        <mwc-textfield
          id="text-title"
          outlined
          minlength="3"
          label="Title"
          required>
        </mwc-textfield>
      </div>
      <div class="formFields">
        <mwc-textfield
          id="text-description"
          outlined
          minlength="3"
          label="Description"
          required>
        </mwc-textfield>
      </div>
      <div>
        <mwc-button
          id="primary-action-button"
          slot="primaryAction"
          @click="handleAddNote">
          Add
        </mwc-button>
        <mwc-button
          slot="secondaryAction"
          dialogAction="close"
          @click="handleClose">
          Cancel
        </mwc-button>
      </div>
    </mwc-dialog>
  </div>
</template>
<script>
import '@material/mwc-list/mwc-list';
import '@material/mwc-list/mwc-list-item';
import '@material/mwc-fab';
import '@material/mwc-button';
import '@material/mwc-dialog';
import '@material/mwc-textfield';
import { fireApp } from'../firebase'
import { v4 as uuidv4 } from 'uuid';

const db = fireApp.database().ref();
const auth = fireApp.auth();

export default {
  name: 'Dashboard',
  data() {
    return {
      notes: [],
      user: null
    }
  },
  mounted() {
    this.isUserLoggedIn()
    .then(
      (user) => {
        this.user = user;
        this.updateLogged();
        this.getUserNotes();
      }
    )
    .catch(
      () => {
        this.$router.push('/login');
      }
    )
    ;
  },
  methods: {
    getUserNotes() {
      db.orderByChild('userId')
        .equalTo(this.user.uid)
        .once("value")
        .then(
          (notes) => {
            notes.forEach((note) => {
              this.notes.push({
                id: note.child('id').val(),
                title: note.child('title').val(),
                description: note.child('description').val(),
                userId: note.child('userId').val(),
                ref: note.ref
              })
            })
          }
        );
    },
    handleDelete(id) {
      const noteToDelete = this.notes.findIndex((item) => (item.id === id));
      const noteRef = this.notes[noteToDelete].ref;
      if(noteRef) {
        noteRef.remove();
      }
      this.notes.splice(noteToDelete, 1);
    },
    handleAdd() {
      const formDialog = this.$el.querySelector('#dialog');
      formDialog.show();
    },
    handleAddNote() {
      const formDialog = this.$el.querySelector('#dialog');
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const isValid = txtTitle.checkValidity() && txtDescription.checkValidity();

      if(isValid) {
        const newIndex = uuidv4();
        const newItem = {
          id: newIndex,
          title: txtTitle.value,
          description: txtDescription.value,
          userId: this.user.uid
        };
        this.notes.push(newItem);
        db.push(newItem);

        txtTitle.value ='';
        txtDescription.value = '';
        formDialog.close();
      }
    },
    handleClose() {
      let txtTitle = this.$el.querySelector('#text-title');
      let txtDescription = this.$el.querySelector('#text-description');
      const formDialog = this.$el.querySelector('#dialog');

      txtTitle.value ='';
      txtDescription.value = '';
      formDialog.close();
    },
    updateLogged() {
      this.$emit("update-logged", true);
    },
    isUserLoggedIn () {
        return new Promise(
          (resolve, reject) => {
            auth.onAuthStateChanged(function(user) {
              if (user) {
                resolve(user);
              }
              else {
                reject(user);
              }
            })
          }
        )
        ;
    }
  },
}
</script>
<style scoped>
  .floatButton {
    position: fixed;
    bottom: 20px;
    right: 20px;
  }

  .formFields {
    margin: 15px;
  }
</style>

Listing 10-8Adding a Login in the Vue Router

这里,在getUserNotes()方法中,我们获取与一个用户相关联的注释,用户 ID 为db.orderByChild('userId').equalTo(this.user.uid).once("value")。这样,我们解决了笔记的问题,用户可以在每个帐户中拥有自己的笔记。

从 GitHub 库( https://github.com/carlosrojaso/apress-book-web-components )可以在$git checkout v1.0.10访问相关代码。

发送到 Firebase 主机

首先,我们必须用我们的 Firebase 帐户验证我们的 Firebase CLI。为此,请运行以下命令:

$ firebase login

成功认证后,您可以开始使用 Firebase 工具将您的应用与 Firebase 连接起来。下一步是创建我们的产品包。来做这个实验

$ npm run build

这个命令创建了dist/文件夹,其中包含了我们所有优化的应用。

现在我们必须运行 Firebase CLI 向导来连接 VueNoteApp 和 Firebase(图 10-8 )。奔跑

img/494195_1_En_10_Fig8_HTML.jpg

图 10-8

Firebase CLI 选择要配置的服务

$ firebase init

选择主机(参见图 10-9 )。

img/494195_1_En_10_Fig9_HTML.jpg

图 10-9

Firebase CLI 选择 Firebase 项目设置

选择您在firebase.google.com中创建的项目——在我的例子中是apress-book-webcomponents(图 10-10 )。

img/494195_1_En_10_Fig10_HTML.jpg

图 10-10

Firebase CLI 选择公共目录

公共目录是dist/

好的。这样,我们就可以将我们的应用发送到 Firebase 主机了。为此,请运行以下命令:

$ npm run build
$ Firebase deploy

您将看到如图 10-11 所示的进度。

img/494195_1_En_10_Fig11_HTML.jpg

图 10-11

Firebase CLI 部署和获取公共 URL

最后,你会得到一个托管 URL。这是你的网址。现在可以用 https://apress-book-webcomponents.web.app 试试。

摘要

在本章中,您学习了

  • 如何在 VueNoteApp 中使用导航卫士

  • 如何在 Firebase 中启用电子邮件/密码验证

  • 如何将数据与用户帐户相关联

  • 如何准备 web 应用并将其发送到 Firebase 主机

最后的想法

如果你已经到了这一步,恭喜你!您现在知道如何在任何现有的 web 应用中构建、设计和使用 Web 组件。

如果您有任何意见或反馈,请随时通过iam@carlosrojas.dev联系我。

如果你有关于代码的问题,不要犹豫,在 https://github.com/carlosrojaso/apress-book-web-components/issues 查询这本书的 GitHub 知识库。

另外,为了更新,经常检查代码的官方存储库( https://github.com/carlosrojaso/apress-book-web-components )。

回头见,继续编程!