Atila Extension For VueJS 2 SFC and SSR
Project description
Introduction
Atla-Vue is Atila extension package for using vue3-sfc-loader and Bootstrap 5.
It will be useful for building simple web service at situation frontend developer dose not exists.
Due to the vue3-sfc-loader, We can use vue single file component on the fly without any compiling or building process.
Atila-Vue composes these things:
- VueJS 3
- VueRouter
- Vuex
- Optional Bootstrap 5 for UI/UX
For injecting objects to Vuex, it uses Jinja2 template engine.
Why Do I Need This?
- Single stack frontend developement, No 500M
node_modules
- Optimized multiple small SPA/MPAs in single web service
- SSR and SEO advantage
Full Example
See atila-vue repository and atila-vue examplet.
Launching Server
mkdir myservice
cd myservice
skitaid.py
#! /usr/bin/env python3
import skitai
import atila_vue
from atila import Allied
import backend
if __name__ == '__main__':
with skitai.preference () as pref:
skitai.mount ('/', Allied (atila_vue, backend), pref)
skitai.run (ip = '0.0.0.0', name = 'myservice')
backend/__init__.py
import skitai
def __config__ (pref):
pref.set_static ('/', skitai.joinpath ('backend/static'))
pref.config.MAX_UPLOAD_SIZE = 1 * 1024 * 1024 * 1024
pref.config.FRONTEND = {
"googleAnalytics": {"id": "UA-XXX-1"}
}
def __app__ ():
import atila
return atila.Atila (__name__)
def __mount__ (context, app):
import atila_vue
@app.route ("/api")
def api (context):
return {'version': atila_vue.__version__}
@app.route ("/ping")
def ping (context):
return 'pong'
Now you can startup service.
./serve/py --devel
Then your browser address bar, enter http://localhost:5000/ping
.
Site Template
backend/templates/site.j2
{% extends 'atila-vue/bs5.j2' %}
{% block lang %}en{% endblock %}
{% block state_map %}
{{ set_cloak (False) }}
{% endblock %}
Multi Page App
backend/__init__.py
def __mount__ (context, app):
import atila_vue
@app.route ('/')
@app.route ('/mpa')
def mpa (context):
return context.render (
'mpa.j2',
version = atila_vue.__version__
)
backend/templates/mpa.j2
{% extends 'site.j2' %}
{% block content %}
{% include 'includes/header.j2' %}
<div class="container">
<h1>Multi Page App</h1>
</div>
{% endblock content %}
Single Page App
backend/__init__.py
def __mount__ (context, app):
import atila_vue
@app.route ('/spa/<path:path>')
def spa (context, path = None):
return context.render (
'spa.j2',
vue_config = dict (
use_router = context.baseurl (spa),
use_loader = True
),
version = atila_vue.__version__
)
backend/templates/spa.j2
{% extends 'site.j2' %}
{% block content %}
{% include 'includes/header.j2' %}
{{ super () }}
{% endblock %}
As creating vue files, vue-router will be automatically configured.
backend/static/routes/spa/index.vue
: /spabackend/static/routes/spa/sub.vue
: /spa/subbackend/static/routes/spa/items/index.vue
: /spa/itemsbackend/static/routes/spa/items/_id.vue
: /spa/items/:id
App Layout
backend/static/routes/spa/__layout.vue
<template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in" appear>
<div :key="route.name">
<keep-alive>
<component :is="Component" />
</keep-alive>
</div>
</transition>
</router-view>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<script setup>
const route = useRoute ()
</script>
Optional Component To Use In Pages
backend/static/routes/spa/components/myComponent.vue
<template>
My Component
</template>
<script setup>
</script>
Route Pages
backend/static/routes/spa/index.vue
<template>
<div class="container">
<h1>Main Page</h1>
<span class="example">
<i class="bi-alarm"></i>{{ msg }}</span>
<div><router-link :to="{ name: 'sub'}">Sub Page</router-link></div>
<div><router-link :to="{ name: 'items'}">Items</router-link></div>
<div><my-component></my-component></div>
</div>
</template>
<script setup>
import myComponent from '/routes/spa/components/myComponent.vue'
const msg = ref ('hello world!')
</script>
backend/static/routes/spa/sub.vue
<template>
<div class="container">
<h1>Sub Page</h1>
<div><router-link :to="{ name: 'index'}">Main Page</router-link></div>
</div>
</template>
backend/static/routes/spa/items/index.vue
<template>
<div class="container">
<h1>Items</h1>
<ul>
<li v-for="index in 100" :key="index">
<router-link :to="{ name: 'items/:id', params: {id: index}}">Item {{ index }}</router-link>
</li>
</ul>
</div>
</template>
backend/static/routes/spa/items/_id.vue
<template>
<div class="container">
<h1 class='ko-b'>Item {{ item_id }}</h1>
</div>
</template>
<style scoped>
.example {
color: v-bind('color');
}
</style>
<script setup>
const route = useRoute ()
const item_id = ref (route.params.id)
onActivated (() => {
item_id.value = route.params.id
})
watch (() => item_id.value,
(to, from) => {
log (`item changed: ${from} => ${to}`)
}
)
</script>
<script>
export default {
beforeRouteEnter (to, from, next) {
next ()
}
}
</script>
Using Vuex: Injection
Adding States
backend/templates/mpa.j2
.
{% extends '__framework/bs5.j2' %}
{% block state_map %}
{{ map_state ('page_id', 0) }}
{{ map_state ('types', ["todo", "canceled", "done"]) }}
{% endblock %}
These will be injected to Vuex
through JSON.
Now tou can use these state on your vue file with useStore
.
<script setup>
const { state } = useStore ()
const page_id = computed ( () => state.page_id )
const msg = ref ('Hello World')
</script>
Cloaking Control
backend/templates/mpa.j2
.
{% extends '__framework/bs5.j2' %}
{% block state_map %}
{{ set_cloak (True) }}
{% endblock %}
index.vue
or nay vue
<script setup>
onMounted (async () => {
await sleep (10000) // 10 sec
set_cloak (false)
})
</script>
State Injection Macros
map_state (name, value, container = '', list_size = -1)
map_dict (name, **kargs)
map_text (name, container)
map_html (name, container)
set_cloak (flag = True)
map_route (**kargs)
JWT Authorization And Access Control
Basic About API
Adding API
backend/services/apis.py
def __mount__ (app. mntopt):
@app.route ("")
def index (was):
return "API Index"
@app.route ("/now")
def now (was):
return was.API (result = time.time ())
Create backend/services/__init__.py
def __setup__ (app. mntopt):
from . import apis
app.mount ('/apis', apis)
Then update backend/__init__.py
for mount services
.
def __app__ ():
return atila.Atila (__name__)
def __setup__ (app, mntopt):
from . import services
app.mount ('/', services)
def __mount__ (app, mntopt):
@app.route ('/')
def index (was):
return was.render ('main.j2')
Now you can use API: http://localhost:5000/apis/now.
Accessing API
<script setup>
const msg = ref ('Hello World')
const server_time = ref (null)
onBeforeMount ( () => {
const r = await $http.get ('/apis/now')
server_time.value = r.data.result
})
</script>
Vuex.state has $apispecs
state and it contains all API specification of server side. We made only 1 APIs for now.
Note that your exposed APIs endpoint should be /api
.
{
APIS_NOW: { "methods": [ "POST", "GET" ], "path": "/apis/now", "params": [], "query": [] }
}
You can make API url by backend.endpoint
helpers by API ID
.
const endpoint = backend.endpoint ('APIS_NOW')
// endpoint is resolved into '/apis/now'
Access Control
Creating Server Side Token Providing API
Update backend/services/apis.py
.
import time
USERS = {
'hansroh': ('1111', ['staff', 'user'])
}
def create_token (uid, grp = None):
due = (3600 * 6) if grp else (14400 * 21)
tk = dict (uid = uid, exp = int (time.time () + due))
if grp:
tk ['grp'] = grp
return tk
def __mount__ (app, mntopt):
@app.route ('/signin_with_id_and_password', methods = ['POST', 'OPTIONS'])
def signin_with_uid_and_password (was, uid, password):
passwd, grp = USERS.get (uid, (None, []))
if passwd != password:
raise was.Error ("401 Unauthorized", "invalid account")
return was.API (
refresh_token = was.mkjwt (create_token (uid)),
access_token = was.mkjwt (create_token (uid, grp))
)
@app.route ('/access_token', methods = ['POST', 'OPTIONS'])
def access_token (was, refresh_token):
claim = was.dejwt ()
atk = None
if 'err' not in claim:
atk = claim # valid token
elif claim ['ecd'] != 0: # corrupted token
raise was.Error ("401 Unauthorized", claim ['err'])
claim = was.dejwt (refresh_token)
if 'err' in claim:
raise was.Error ("401 Unauthorized", claim ['err'])
uid = claim ['uid']
_, grp = USERS.get (uid, (None, []))
rtk = was.mkjwt (create_token (uid)) if claim ['exp'] + 7 > time.time () else None
if not atk:
atk = create_token (uid, grp)
return was.API (
refresh_token = rtk,
access_token = was.mkjwt (atk)
)
You have responsabliity for these things.
- provide
access token
andrefresh token
access token
must containstr uid
,list grp
andint exp
refresh token
must containstr uid
andint exp
Now reload page, you can see Vuex.state.$apispecs
like this.
{
APIS_NOW: { "methods": [ "POST", "GET" ], "path": "/apis/now", "params": [], "query": [] },
APIS_ACCESS_TOKEN: { "methods": [ "POST", "OPTIONS" ], "path": "/apis/access_token", "params": [], "query": [ "refresh_token" ] },
APIS_SIGNIN_WITH_ID_AND_PASSWORD: { "methods": [ "POST", "OPTIONS" ], "path": "/apis/signin_with_id_and_password", "params": [], "query": [ "uid", "password" ] }
}
Client Side Page Access Control
We provide user and grp base page access control.
<script>
export default {
beforeRouteEnter (to, from, next) {
permission_required (['staff'], {name: 'signin'}, next)
}
}
</script>
admin
and staff
are pre-defined reserved grp name.
Vuex.state contains $uid
and $grp
state. So permission_required
check with
this state and decide to allow access.
And you should build sign in component signin.vue
.
Create backend/static/routes/main/signin.vue
.
<template>
<div>
<h1>Sign In</h1>
<input type="text" v-model='uid'>
<input type="password" v-model='password'>
<button @click='signin ()'>Sign In</button>
</div>
</template>
<script setup>
const { state } = useStore ()
const uid = ref ('')
const password = ref ('')
async function signin () {
const msg = await backend.signin_with_id_and_password (
'APIS_AUTH_SIGNIN_WITH_ID_AND_PASSWORD',
{uid: uid.value, password: password.value}
)
if (!!msg) {
return alert (`Sign in failed because ${ msg }`)
}
alert ('Sign in success!')
permission_granted () // go to origin route
}
</script>
And one more, update /backend/static/routes/main/__layout.vue
<script setup>
onBeforeMount ( () => {
backend.refresh_access_token ('APIS_ACCESS_TOKEN')
})
</script>
This will check saved tokens at app initializing and do these things:
- update
Vuex.state.$uid
andVuex.state.$grp
if access token is valid - if access token is expired, try refresh using refresh token and save credential
- if refresh token close to expiration, refresh 'refresh token' itself
- if refresh token is expired, clear all credential
From this moment, axios
monitor access token
whenever you call APIs and automatically managing tokens.
Then we must create 2 APIs - API ID APIS_SIGNIN_WITH_ID_AND_PASSWORD
and
APIS_AUTH_ACCESS_TOKEN
.
Server Side Access Control
def __mount__ (app, mntopt):
@app.route ('/profiles/<uid>')
@app.permission_required (['user'])
def get_profile (was):
icanaccess = was.request.user.uid
return was.API (profile = data)
If request user is one of user
, staff
and admin
grp, access will be granted.
And all claims of access token can be access via was.request.user
dictionary.
@app.permission_required
can groups
and owner
based control.
Also @app.login_required
which is shortcut for @app.permission_required ([])
- any groups will be granted.
@app.identification_required
is just create was.request.user
object using access token only if token is valid.
For more detail access control. see Atila.
Appendix
Jinja Template Helpers
Globals
raise
http_error (status, *args)
: raise context.HttpError
Filters
vue (val)
summarize (val, chars = 60)
attr (val)
upselect (val, *names, **upserts)
tojson_with_datetime (data)
Macros
component (path, alias = none, _async = True)
global_component (path, alias = none, _async = True)
State Injection Macros
map_state (name, value, container = '', list_size = -1)
map_dict (name, **kargs)
map_text (name, container)
map_html (name, container)
set_cloak (flag = True)
map_route (**kargs)
Javascript Helpers
Aliases
$http
: axios
Prototype Methods
Number.prototype.format
String.prototype.format
String.prototype.titleCase
Date.prototype.unixepoch
Date.prototype.format
String.prototype.repeat
String.prototype.zfill
Number.prototype.zfill
Device Detecting
device
android
ios
mobile
touchable
rotatable
width
height
Service Worker Sync
swsync
async add_tag (tag, min_interval_sec = 0)
async unregister_tag (tag)
: periodic onlyasync periodic_sync_enabled ()
async is_tag_registered (tag)
Backend URL Building and Authorization
backend
endpoint (name, args = [], _kargs = {})
static (relurl)
media (relurl)
async signin_with_id_and_password (endpoint, payload)
async refresh_access_token (endpoint)
: InonBeforeMount
at__layout.vue
create_websocket (API_ID, read_handler = (evt) => log (evt.data))
push(msg)
Logging
log (msg, type = 'info')
traceback (e)
Utilities
permission_required (permission, redirect, next)
: InbeforeRouteEnter
permission_granted ()
: go to original requested route after signing inbuild_url (baseurl, params = {})
push_alarm (title, message, icon, timeout = 5000)
load_script (src, callback = () => {})
: load CDN jsset_cloak (flag)
async sleep (ms)
async keep_offset_bottom (css, margin = 0, initial_delay = 0)
async keep_offset_right (css, margin = 0, initial_delay = 0)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
File details
Details for the file atila_vue-0.4.0-py3-none-any.whl
.
File metadata
- Download URL: atila_vue-0.4.0-py3-none-any.whl
- Upload date:
- Size: 148.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.7.12
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0b8d6028b029c6350cba69842073d3fdc0a14c606c68be377c78f2b102e2be3a |
|
MD5 | 9683e7b2ed100ae597ed40aae5bb2cd7 |
|
BLAKE2b-256 | 9c492b3ec1bbfbc9970d262346e4f69b812fa220a7079735179f3da3ddf87a81 |