做者:Michael Wanyoike
原文:www.sitepoint.com/pusher-vue-…javascript
现现在,即时通迅已经愈来愈广泛,而且用户体验也愈来愈天然和流畅。css
本文将使用ChatKit增强过的Vue.js建立一个实时聊天应用,ChatKit服务为咱们提供了一个建立聊天应用的后端,而且能够运行于任何设备上,让咱们只需关注前端用户接口,这个接口经过ChatKit client包链接到ChatKit服务。前端
这是一篇中到高级的教程,理解本文须要对如下概念都比较熟悉:vue
还须要安装Node.js,能够直接在官网上下载安装包。 最后须要使用如下命令安装全局的Vue CLI。java
npm install -g @vue/cli
复制代码
在写这篇文章的时候Node版本是 10.14.1,Vue CLI的最新版本是 3.2.1。node
咱们要建立一个基础的聊天应用,应用须要有以下功能:git
就像先前提到的,这里只建立前端,ChatKit服务有个能够管理用户、受权和房间的后端接口。github
能够在GitHub上找到完整的代码。web
建立ChatKit实例,相似于建立服务端实例。进入Puser网站的ChatKit页面,先注册,完成登陆后进入Pusher的仪表板,而后选择ChatKit产品。vue-router
再建立三个房间并指定相应的用户。例如:
最后,控制台界面是这样子:
打开终端,像下面这样建立项目
vue create vue-chatkit
复制代码
选择Manually select features而且像下面这样选择相关问题。
对于loading-btn.css他loading.css这两个文件,能够在loading.io上找到,这两件文件没法经过npm仓库获取,因此须要本身手动下载,而后放在项目中。在此最好知道这两个文件是作什么的,怎样定制化加载条。
下面,安装下面依赖:
npm i @pusher/chatkit-client bootstrap-vue moment vue-chat-scroll vuex-persist
复制代码
能够点击连接看看每一个包都是作什么的,怎样配置。
如今配置Vue.js项目。打开src/main.js更新代码为以下内容:
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import VueChatScroll from 'vue-chat-scroll'
import App from './App.vue'
import router from './router'
import store from './store/index'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import './assets/css/loading.css'
import './assets/css/loading-btn.css'
Vue.config.productionTip = false
Vue.use(BootstrapVue)
Vue.use(VueChatScroll)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
复制代码
更新src/router.js:
import Vue from 'vue'
import Router from 'vue-router'
import Login from './views/Login.vue'
import ChatDashboard from './views/ChatDashboard.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'login',
component: Login
},
{
path: '/chat',
name: 'chat',
component: ChatDashboard,
}
]
})
复制代码
更新src/store/index.js:
import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
import mutations from './mutations'
import actions from './actions'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
const vuexLocal = new VuexPersistence({
storage: window.localStorage
})
export default new Vuex.Store({
state: {
},
mutations,
actions,
getters: {
},
plugins: [vuexLocal.plugin],
strict: debug
})
复制代码
Vue-persist是为了让Vuex的state在页面刷新和从新加载的时候可以保存下来。
目前,代码应该是可以编译并无错误的,但如今不执行,还须要建立用户界面。
如今开始更新src/App.vue:
<template>
<div id="app">
<router-view/>
</div>
</template>
复制代码
接下来须要定义UI组件运行所须要的Vuex store的state ,经过进入src/store/index.js,更新一下state和getters部分,像下面这样:
state: {
loading: false,
sending: false,
error: null,
user: [],
reconnect: false,
activeRoom: null,
rooms: [],
users: [],
messages: [],
userTyping: null
},
getters: {
hasError: state => state.error ? true : false
},
复制代码
这是这个聊天应用所须要的全部的state变量了,loading state用于在UI上决定是否显示CSS 加载条。error state用于存储刚发生的错误信息,其余的变量会在用到的时候再解释。
接下来打开src/view/Login.vue更新以下:
<template>
<div class="login">
<b-jumbotron header="Vue.js Chat"
lead="Powered by Chatkit SDK and Bootstrap-Vue"
bg-variant="info"
text-variant="white">
<p>For more information visit website</p>
<b-btn target="_blank" href="https://pusher.com/chatkit">More Info</b-btn>
</b-jumbotron>
<b-container>
<b-row>
<b-col lg="4" md="3"></b-col>
<b-col lg="4" md="6">
<LoginForm />
</b-col>
<b-col lg="4" md="3"></b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import LoginForm from '@/components/LoginForm.vue'
export default {
name: 'login',
components: {
LoginForm
}
}
</script>
复制代码
而后,向src/components/LoginForm.vue插入以下代码:
<template>
<div class="login-form">
<h5 class="text-center">Chat Login</h5>
<hr>
<b-form @submit.prevent="onSubmit">
<b-alert variant="danger" :show="hasError">{{ error }} </b-alert>
<b-form-group id="userInputGroup"
label="User Name"
label-for="userInput">
<b-form-input id="userInput"
type="text"
placeholder="Enter user name"
v-model="userId"
autocomplete="off"
:disabled="loading"
required>
</b-form-input>
</b-form-group>
<b-button type="submit"
variant="primary"
class="ld-ext-right"
v-bind:class="{ running: loading }"
:disabled="isValid">
Login <div class="ld ld-ring ld-spin"></div>
</b-button>
</b-form>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
name: 'login-form',
data() {
return {
userId: '',
}
},
computed: {
isValid: function() {
const result = this.userId.length < 3;
return result ? result : this.loading
},
...mapState([
'loading',
'error'
]),
...mapGetters([
'hasError'
])
}
}
</script>
复制代码
正如先前提到的,这是高级教程,若是理解这些代码有任何问题,能够看准备条件或项目依赖的相关信息。
如今能够经过npm run serve启动Vue dev服务端来确认一下应用执行没有任何兼容性问题。
打开src/vie/ChatDashboard.vue插入如下代码:
<template>
<div class="chat-dashboard">
<ChatNavBar />
<b-container fluid class="ld-over" v-bind:class="{ running: loading }">
<div class="ld ld-ring ld-spin"></div>
<b-row>
<b-col cols="2">
<RoomList />
</b-col>
<b-col cols="8">
<b-row>
<b-col id="chat-content">
<MessageList />
</b-col>
</b-row>
<b-row>
<b-col>
<MessageForm />
</b-col>
</b-row>
</b-col>
<b-col cols="2">
<UserList />
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import ChatNavBar from '@/components/ChatNavBar.vue'
import RoomList from '@/components/RoomList.vue'
import MessageList from '@/components/MessageList.vue'
import MessageForm from '@/components/MessageForm.vue'
import UserList from '@/components/UserList.vue'
import { mapState } from 'vuex';
export default {
name: 'Chat',
components: {
ChatNavBar,
RoomList,
UserList,
MessageList,
MessageForm
},
computed: {
...mapState([
'loading'
])
}
}
</script>
复制代码
ChatDashboard至关于下面子组件的一个用于布局的父页面。
<template>
<b-navbar id="chat-navbar" toggleable="md" type="dark" variant="info">
<b-navbar-brand href="#">
Vue Chat
</b-navbar-brand>
<b-navbar-nav class="ml-auto">
<b-nav-text>{{ user.name }} | </b-nav-text>
<b-nav-item href="#" active>Logout</b-nav-item>
</b-navbar-nav>
</b-navbar>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'ChatNavBar',
computed: {
...mapState([
'user',
])
},
}
</script>
<style>
#chat-navbar {
margin-bottom: 15px;
}
</style>
复制代码
向src/components/RoomList.vue添加以下样例代码:
<template>
<div class="room-list">
<h4>Channels</h4>
<hr>
<b-list-group v-if="activeRoom">
<b-list-group-item v-for="room in rooms"
:key="room.name"
:active="activeRoom.id === room.id"
href="#"
@click="onChange(room)">
# {{ room.name }}
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'RoomList',
computed: {
...mapState([
'rooms',
'activeRoom'
]),
}
}
</script>
复制代码
向src/components/UserList.vue添加以下样例代码:
<template>
<div class="user-list">
<h4>Members</h4>
<hr>
<b-list-group>
<b-list-group-item v-for="user in users" :key="user.username">
{{ user.name }}
<b-badge v-if="user.presence"
:variant="statusColor(user.presence)"
pill>
{{ user.presence }}</b-badge>
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'user-list',
computed: {
...mapState([
'loading',
'users'
])
},
methods: {
statusColor(status) {
return status === 'online' ? 'success' : 'warning'
}
}
}
</script>
复制代码
向src/components/MessageList.vue添加以下样例代码:
<template>
<div class="message-list">
<h4>Messages</h4>
<hr>
<div id="chat-messages" class="message-group" v-chat-scroll="{smooth: true}">
<div class="message" v-for="(message, index) in messages" :key="index">
<div class="clearfix">
<h4 class="message-title">{{ message.name }}</h4>
<small class="text-muted float-right">@{{ message.username }}</small>
</div>
<p class="message-text">
{{ message.text }}
</p>
<div class="clearfix">
<small class="text-muted float-right">{{ message.date }}</small>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'message-list',
computed: {
...mapState([
'messages',
])
}
}
</script>
<style>
.message-list {
margin-bottom: 15px;
padding-right: 15px;
}
.message-group {
height: 65vh !important;
overflow-y: scroll;
}
.message {
border: 1px solid lightblue;
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
}
.message-title {
font-size: 1rem;
display:inline;
}
.message-text {
color: gray;
margin-bottom: 0;
}
.user-typing {
height: 1rem;
}
</style>
复制代码
向src/components/MessageForm.vue添加以下样例代码:
<template>
<div class="message-form ld-over">
<small class="text-muted">@{{ user.username }}</small>
<b-form @submit.prevent="onSubmit" class="ld-over" v-bind:class="{ running: sending }">
<div class="ld ld-ring ld-spin"></div>
<b-alert variant="danger" :show="hasError">{{ error }} </b-alert>
<b-form-group>
<b-form-input id="message-input"
type="text"
v-model="message"
placeholder="Enter Message"
autocomplete="off"
required>
</b-form-input>
</b-form-group>
<div class="clearfix">
<b-button type="submit" variant="primary" class="float-right">
Send
</b-button>
</div>
</b-form>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
name: 'message-form',
data() {
return {
message: ''
}
},
computed: {
...mapState([
'user',
'sending',
'error',
'activeRoom'
]),
...mapGetters([
'hasError'
])
}
}
</script>
复制代码
检查一下代码确保没有什么是很神秘的。导航到http://localhost:8080/chat检查一下全部内容都能正常执行。检查一下终端和浏览器的控制面板确保在这里没有错误,那么如今页面看起来是下图这个样子的。
state: {
loading: false,
sending: false,
error: 'Relax! This is just a drill error message',
user: {
username: 'Jack',
name: 'Jack Sparrow'
},
reconnect: false,
activeRoom: {
id: '124'
},
rooms: [
{
id: '123',
name: 'Ships'
},
{
id: '124',
name: 'Treasure'
}
],
users: [
{
username: 'Jack',
name: 'Jack Sparrow',
presence: 'online'
},
{
username: 'Barbossa',
name: 'Hector Barbossa',
presence: 'offline'
}
],
messages: [
{
username: 'Jack',
date: '11/12/1644',
text: 'Not all treasure is silver and gold mate'
},
{
username: 'Jack',
date: '12/12/1644',
text: 'If you were waiting for the opportune moment, that was it'
},
{
username: 'Hector',
date: '12/12/1644',
text: 'You know Jack, I thought I had you figured out'
}
],
userTyping: null
},
复制代码
保存这个文件后,就能够看到下图的内容了。
state: {
loading: false,
sending: false,
error: null,
user: null,
reconnect: false,
activeRoom: null,
rooms: [],
users: [],
messages: [],
userTyping: null
}
复制代码
如今开始实现具体特性,从登陆表单开始。
这部分将引入一个无密码非安全的认证系统。本文不涉及合适的安全认证方面。首先,须要开始构建本身的接口,它将经过@pusher/ ChatKit -client包与ChatKit服务进行交互。
回到ChatKit控制面板,将原来提到的instance和测试token参数拷到项目根目录下的.env.local文件中并保存:
VUE_APP_INSTANCE_LOCATOR=
VUE_APP_TOKEN_URL=
VUE_APP_MESSAGE_LIMIT=10
复制代码
咱们添加了MESSAGE_LIMIT参数,这个值是限制聊天应用将获取的消息数量。而后确保把credentials选项卡中的其余参数也填上了。
接下来,进入src/chatkit.js开始构建聊天应用的基础:
import { ChatManager, TokenProvider } from '@pusher/chatkit-client'
const INSTANCE_LOCATOR = process.env.VUE_APP_INSTANCE_LOCATOR;
const TOKEN_URL = process.env.VUE_APP_TOKEN_URL;
const MESSAGE_LIMIT = Number(process.env.VUE_APP_MESSAGE_LIMIT) || 10;
let currentUser = null;
let activeRoom = null;
async function connectUser(userId) {
const chatManager = new ChatManager({
instanceLocator: INSTANCE_LOCATOR,
tokenProvider: new TokenProvider({ url: TOKEN_URL }),
userId
});
currentUser = await chatManager.connect();
return currentUser;
}
export default {
connectUser
}
复制代码
注意咱们须要将常量MESSAGE_LIMIT转换成数值,由于默认状况下process.env对象会强制全部的属性是字符串类型的。 向src/store/mutations插入以下代码:
export default {
setError(state, error) {
state.error = error;
},
setLoading(state, loading) {
state.loading = loading;
},
setUser(state, user) {
state.user = user;
},
setReconnect(state, reconnect) {
state.reconnect = reconnect;
},
setActiveRoom(state, roomId) {
state.activeRoom = roomId;
},
setRooms(state, rooms) {
state.rooms = rooms
},
setUsers(state, users) {
state.users = users
},
clearChatRoom(state) {
state.users = [];
state.messages = [];
},
setMessages(state, messages) {
state.messages = messages
},
addMessage(state, message) {
state.messages.push(message)
},
setSending(state, status) {
state.sending = status
},
setUserTyping(state, userId) {
state.userTyping = userId
},
reset(state) {
state.error = null;
state.users = [];
state.messages = [];
state.rooms = [];
state.user = null
}
}
复制代码
mutations中的代码至关简单,就是一堆setters,在后面的几节里,你很快就会理解每一个mutation函数的用途。接下来,更新src/store/actions.js的代码:
import chatkit from '../chatkit';
// Helper function for displaying error messages
function handleError(commit, error) {
const message = error.message || error.info.error_description;
commit('setError', message);
}
export default {
async login({ commit, state }, userId) {
try {
commit('setError', '');
commit('setLoading', true);
// Connect user to ChatKit service
const currentUser = await chatkit.connectUser(userId);
commit('setUser', {
username: currentUser.id,
name: currentUser.name
});
commit('setReconnect', false);
// Test state.user
console.log(state.user);
} catch (error) {
handleError(commit, error)
} finally {
commit('setLoading', false);
}
}
}
复制代码
像下面这样更新src/components/LoginForm.vue的内容:
import { mapState, mapGetters, mapActions } from 'vuex'
//...
export default {
//...
methods: {
...mapActions([
'login'
]),
async onSubmit() {
const result = await this.login(this.userId);
if(result) {
this.$router.push('chat');
}
}
}
}
复制代码
为了加载env.local的数据须要重启Vue.js服务,若是看到任何未使用变量的错误,先忽略它们,一旦完成这些,导航到http://localhost:8080/测试一下登陆功能:
如今已经成功验证过登陆功能,须要将用户重定向到ChatDashboard视图。使用this.$router.push('chat');进行跳转。然而login操做须要返回一个Boolean值来决定何时是能够跳转到ChatDashboard视图,还须要从ChatKit服务上获取实际的数据填充RoomList和UserList组件。
更新src/chatkit.js的代码:
//...
import moment from 'moment'
import store from './store/index'
//...
function setMembers() {
const members = activeRoom.users.map(user => ({
username: user.id,
name: user.name,
presence: user.presence.state
}));
store.commit('setUsers', members);
}
async function subscribeToRoom(roomId) {
store.commit('clearChatRoom');
activeRoom = await currentUser.subscribeToRoom({
roomId,
messageLimit: MESSAGE_LIMIT,
hooks: {
onMessage: message => {
store.commit('addMessage', {
name: message.sender.name,
username: message.senderId,
text: message.text,
date: moment(message.createdAt).format('h:mm:ss a D-MM-YYYY')
});
},
onPresenceChanged: () => {
setMembers();
},
onUserStartedTyping: user => {
store.commit('setUserTyping', user.id)
},
onUserStoppedTyping: () => {
store.commit('setUserTyping', null)
}
}
});
setMembers();
return activeRoom;
}
export default {
connectUser,
subscribeToRoom
}
复制代码
若是看过hooks这一节,就知道ChatKit服务有用于和客户端应用进行通迅的事件处理器,能够在这里看完整的文档。我将快速的总结一下每一个钩子方法的做用:
用下面的代码更新src/store/actions.js中的login函数:
//...
try {
//... (place right after the `setUser` commit statement)
// Save list of user's rooms in store const rooms = currentUser.rooms.map(room => ({ id: room.id, name: room.name })) commit('setRooms', rooms); // Subscribe user to a room const activeRoom = state.activeRoom || rooms[0]; // pick last used room, or the first one commit('setActiveRoom', { id: activeRoom.id, name: activeRoom.name }); await chatkit.subscribeToRoom(activeRoom.id); return true; } catch (error) { //... } 复制代码
在保存代码以后,回到登陆页,再输入正确的用户名,应该是看到下面这样的页面。
若是遇到了问题,能够尝试如下操做:
这部分很是简单,由于基础已经打好了。首先,建立一个容许用户切换房间的方法,打开src/store/actions.js在login方法处理器后添加该函数:
async changeRoom({ commit }, roomId) {
try {
const { id, name } = await chatkit.subscribeToRoom(roomId);
commit('setActiveRoom', { id, name });
} catch (error) {
handleError(commit, error)
}
},
复制代码
接下来,打开src/componenents/RoomList.vue更新script部分代码以下:
import { mapState, mapActions } from 'vuex'
//...
export default {
//...
methods: {
...mapActions([
'changeRoom'
]),
onChange(room) {
this.changeRoom(room.id)
}
}
}
复制代码
回想一下,已经在b-list-group-item元素中定义了@click="onChange(room)",点击RoomList组件中的项测试一下这个新功能。
点击每一个房间,UI应该都会更新,每次选择房间,MessageList和UserList组件都应该显示正确的信息。下一节,将一次实现多个功能。
你可能注意到了,当对store/index.js作一些更新,或者刷新页面的时候,会出现以下 错误:Cannot read property 'subscribeToRoom' of null,这是由于应用的state进行了重置。幸亏,在页面刷新时,vuex-persist包将Vuex state维护在了浏览器的本地存储里。
链接应用和ChatKit服务端的引用也被置回了null值,为了解决这个问题,须要执行重连操做。同时须要一种方式告诉应用页面进行过刷新,为了继续进行正常的功能应用须要重连。在src/components/ChatNavbar.vue中实现了这部分的代码,更新脚本以下:
<script>
import { mapState, mapActions, mapMutations } from 'vuex'
export default {
name: 'ChatNavBar',
computed: {
...mapState([
'user',
'reconnect'
])
},
methods: {
...mapActions([
'logout',
'login'
]),
...mapMutations([
'setReconnect'
]),
onLogout() {
this.$router.push({ path: '/' });
this.logout();
},
unload() {
if(this.user.username) { // User hasn't logged out this.setReconnect(true); } } }, mounted() { window.addEventListener('beforeunload', this.unload); if(this.reconnect) { this.login(this.user.username); } } } </script> 复制代码
分析一下事件的顺序,以便可以理解从新链接到ChatKit服务背后的逻辑:
1.unload 当页面刷新时,该方法会被调用,它先检查user.username state是否进行过设置,若是是,意味着用户没有登出,reconnect state设置为true
2. mounted 每次ChatNavbar.vue完成渲染该方法就会被调用,它先向事件监听器分派一个处理器(unload),在页面卸载前调用这个处理器(unload)。mounted内还检查了若是 state.reconnect是true的话,登陆程序会被执行,经过这样将聊天应用重连到ChatKit服务上。
还有个Logout功能,后面会细述这个功能。
作了以上更新以后,再试关刷新一下页面,会看到页面会自动,由于重连的过程是在后台完成的,当切换房间的时候,也能完美的运行。
先添加以下代码来实现以上功能:
//...
async function sendMessage(text) {
const messageId = await currentUser.sendMessage({
text,
roomId: activeRoom.id
});
return messageId;
}
export function isTyping(roomId) {
currentUser.isTypingIn({ roomId });
}
function disconnectUser() {
currentUser.disconnect();
}
export default {
connectUser,
subscribeToRoom,
sendMessage,
disconnectUser
}
复制代码
函数sendMessage和disconnectUser会打包在ChatKit的模块里,isTyping函数会被单独export出来。这是为了容许MessageForm在不涉及Vuex存储的状况下直接发送键入事件。
对于sendMessage和disconnectUser,须要更新存储以知足错误处理和加载状态通知等要求。打开src/store/actions.js在changeRoom后插入以下代码:
async sendMessage({ commit }, message) {
try {
commit('setError', '');
commit('setSending', true);
const messageId = await chatkit.sendMessage(message);
return messageId;
} catch (error) {
handleError(commit, error)
} finally {
commit('setSending', false);
}
},
async logout({ commit }) {
commit('reset');
chatkit.disconnectUser();
window.localStorage.clear();
}
复制代码
对于logout函数,咱们调用commit('reset')来将state重置为原始state。这是一个基础的从浏览器移除用户信息和消息的安全功能。
下面开始更新src/components/MessageForm.vue内的表单文本框,经过添加@input指令来触发键入事件。
<b-form-input id="message-input"
type="text"
v-model="message"
@input="isTyping"
placeholder="Enter Message"
autocomplete="off"
required>
</b-form-input>
复制代码
如今更新src/components/MessageForm.vue中的script部分,为了处理消息发送和触发键入事件。更新以下:
<script>
import { mapActions, mapState, mapGetters } from 'vuex'
import { isTyping } from '../chatkit.js'
export default {
name: 'message-form',
data() {
return {
message: ''
}
},
computed: {
...mapState([
'user',
'sending',
'error',
'activeRoom'
]),
...mapGetters([
'hasError'
])
},
methods: {
...mapActions([
'sendMessage',
]),
async onSubmit() {
const result = await this.sendMessage(this.message);
if(result) {
this.message = '';
}
},
async isTyping() {
await isTyping(this.activeRoom.id);
}
}
}
</script>
复制代码
还有在src/MessageList.vue中:
import { mapState } from 'vuex'
export default {
name: 'message-list',
computed: {
...mapState([
'messages',
'userTyping'
])
}
}
复制代码
如今发送消息的功能应该实现了。为了显示另外用户的输入,须要一个显示这些信息的元素。在src/components/MessageList.vue的template中添加以下代码片断,添加到message-troup div以后。
<div class="user-typing">
<small class="text-muted" v-if="userTyping">@{{ userTyping }} is typing....</small>
</div>
复制代码
为了测试这一功能,只须要使用另一个浏览器登陆其余用户并开始输入内容,会看到在其余用户的聊天窗口中有通知出现。
<b-nav-item href="#" @click="onLogout" active>Logout</b-nav-item>
复制代码
这样就能够了,如今能够登出而后再用另外的用户登陆。
终于到了文章的最后,ChatKit API让咱们能在很短的时间内快速的建立一个聊天应用。若是要重头构建一个聊天程序可能须要好几周的时间,由于咱们还得把后台补上。这个解决方案的优势是咱们没必要处理托管、数据库管理和其余基础设施问题。咱们能够构建并发布前端代码到web、Android和IOS平台的客户端设备上。