Package: atila-vue
IntroductionAtla-Vue isAtilaextension package for
usingvue3-sfc-loaderandBootstrap 5.It will be useful for building simple web service at situation frontend developer
dose not exists.Due to thevue3-sfc-loader,
We can usevue single file componenton the fly without any compiling or
building process.Atila-Vue composes these things:VueJS 3VueRouterVuexOptional Bootstrap 5 for UI/UXFor injecting objects to Vuex, it usesJinja2template engine.Why Do I Need This?Single stack frontend developement, No 500Mnode_modulesOptimized multiple small SPA/MPAs in single web serviceSSR and SEO advantageFull ExampleSeeatila-vuerepository andatila-vue examplet.Launching Servermkdirmyservicecdmyserviceskitaid.py#! /usr/bin/env python3importskitaiimportatila_vuefromatilaimportAlliedimportbackendif__name__=='__main__':withskitai.preference()aspref:skitai.mount('/',Allied(atila_vue,backend),pref)skitai.run(ip='0.0.0.0',name='myservice')backend/__init__.pyimportskitaidef__config__(pref):pref.set_static('/',skitai.joinpath('backend/static'))pref.config.MAX_UPLOAD_SIZE=1*1024*1024*1024pref.config.FRONTEND={"googleAnalytics":{"id":"UA-XXX-1"}}def__app__():importatilareturnatila.Atila(__name__)def__mount__(context,app):importatila_vue@app.route("/api")defapi(context):return{'version':atila_vue.__version__}@app.route("/ping")defping(context):return'pong'Now you can startup service../serve/py--develThen your browser address bar, enterhttp://localhost:5000/ping.Site Templatebackend/templates/site.j2{%extends'atila-vue/bs5.j2'%}{%blocklang%}en{%endblock%}{%blockstate_map%}{{set_cloak(False)}}{%endblock%}Multi Page Appbackend/__init__.pydef__mount__(context,app):importatila_vue@app.route('/')@app.route('/mpa')defmpa(context):returncontext.render('mpa.j2',version=atila_vue.__version__)backend/templates/mpa.j2{%extends'site.j2'%}{%blockcontent%}{%include'includes/header.j2'%}<div class="container"><h1>Multi Page App</h1></div>{%endblockcontent%}Single Page Appbackend/__init__.pydef__mount__(context,app):importatila_vue@app.route('/spa/<path:path>')defspa(context,path=None):returncontext.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'%}{%blockcontent%}{%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/:idApp Layoutbackend/static/routes/spa/__layout.vue<template><router-viewv-slot="{ Component }"><transitionname="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:opacity0.3sease;}.fade-enter-from,.fade-leave-to{opacity:0;}</style><scriptsetup>constroute=useRoute()</script>Optional Component To Use In Pagesbackend/static/routes/spa/components/myComponent.vue<template>My Component</template><scriptsetup></script>Route Pagesbackend/static/routes/spa/index.vue<template><divclass="container"><h1>Main Page</h1><spanclass="example"><iclass="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><scriptsetup>importmyComponentfrom'/routes/spa/components/myComponent.vue'constmsg=ref('hello world!')</script>backend/static/routes/spa/sub.vue<template><divclass="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><divclass="container"><h1>Items</h1><ul><liv-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><divclass="container"><h1class='ko-b'>Item {{ item_id }}</h1></div></template><stylescoped>.example{color:v-bind('color');}</style><scriptsetup>constroute=useRoute()constitem_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>exportdefault{beforeRouteEnter(to,from,next){next()}}</script>Using Vuex: InjectionAdding Statesbackend/templates/mpa.j2.{%extends'__framework/bs5.j2'%}{%blockstate_map%}{{map_state('page_id',0)}}{{map_state('types',["todo","canceled","done"])}}{%endblock%}These will be injected toVuexthrough JSON.Now tou can use these state on your vue file withuseStore.<scriptsetup>const{state}=useStore()constpage_id=computed(()=>state.page_id)constmsg=ref('Hello World')</script>Cloaking Controlbackend/templates/mpa.j2.{%extends'__framework/bs5.j2'%}{%blockstate_map%}{{set_cloak(True)}}{%endblock%}index.vueor nay vue<scriptsetup>onMounted(async()=>{awaitsleep(10000)// 10 secset_cloak(false)})</script>State Injection Macrosmap_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 ControlBasic About APIAdding APIbackend/services/apis.pydef__mount__(app.mntopt):@app.route("")defindex(was):return"API Index"@app.route("/now")defnow(was):returnwas.API(result=time.time())Createbackend/services/__init__.pydef__setup__(app.mntopt):from.importapisapp.mount('/apis',apis)Then updatebackend/__init__.pyfor mountservices.def__app__():returnatila.Atila(__name__)def__setup__(app,mntopt):from.importservicesapp.mount('/',services)def__mount__(app,mntopt):@app.route('/')defindex(was):returnwas.render('main.j2')Now you can use API:http://localhost:5000/apis/now.Accessing API<scriptsetup>constmsg=ref('Hello World')constserver_time=ref(null)onBeforeMount(()=>{constr=await$http.get('/apis/now')server_time.value=r.data.result})</script>Vuex.state has$apispecsstate and it contains all API specification of server side. We made only 1 APIs for now.Notethat your exposed APIs endpoint should be/api.{APIS_NOW:{"methods":["POST","GET"],"path":"/apis/now","params":[],"query":[]}}You can make API url bybackend.endpointhelpers byAPI ID.constendpoint=backend.endpoint('APIS_NOW')// endpoint is resolved into '/apis/now'Access ControlCreating Server Side Token Providing APIUpdatebackend/services/apis.py.importtimeUSERS={'hansroh':('1111',['staff','user'])}defcreate_token(uid,grp=None):due=(3600*6)ifgrpelse(14400*21)tk=dict(uid=uid,exp=int(time.time()+due))ifgrp:tk['grp']=grpreturntkdef__mount__(app,mntopt):@app.route('/signin_with_id_and_password',methods=['POST','OPTIONS'])defsignin_with_uid_and_password(was,uid,password):passwd,grp=USERS.get(uid,(None,[]))ifpasswd!=password:raisewas.Error("401 Unauthorized","invalid account")returnwas.API(refresh_token=was.mkjwt(create_token(uid)),access_token=was.mkjwt(create_token(uid,grp)))@app.route('/access_token',methods=['POST','OPTIONS'])defaccess_token(was,refresh_token):claim=was.dejwt()atk=Noneif'err'notinclaim:atk=claim# valid tokenelifclaim['ecd']!=0:# corrupted tokenraisewas.Error("401 Unauthorized",claim['err'])claim=was.dejwt(refresh_token)if'err'inclaim:raisewas.Error("401 Unauthorized",claim['err'])uid=claim['uid']_,grp=USERS.get(uid,(None,[]))rtk=was.mkjwt(create_token(uid))ifclaim['exp']+7>time.time()elseNoneifnotatk:atk=create_token(uid,grp)returnwas.API(refresh_token=rtk,access_token=was.mkjwt(atk))You have responsabliity for these things.provideaccess tokenandrefresh tokenaccess tokenmust containstr uid,list grpandint exprefresh tokenmust containstr uidandint expNow reload page, you can seeVuex.state.$apispecslike 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 ControlWe provide user and grp base page access control.<script>exportdefault{beforeRouteEnter(to,from,next){permission_required(['staff'],{name:'signin'},next)}}</script>adminandstaffare pre-defined reserved grp name.Vuex.state contains$uidand$grpstate. Sopermission_requiredcheck with
this state and decide to allow access.And you should build sign in componentsignin.vue.Createbackend/static/routes/main/signin.vue.<template><div><h1>SignIn</h1><inputtype="text"v-model='uid'><inputtype="password"v-model='password'><button@click='signin ()'>SignIn</button></div></template><scriptsetup>const{state}=useStore()constuid=ref('')constpassword=ref('')asyncfunctionsignin(){constmsg=awaitbackend.signin_with_id_and_password('APIS_AUTH_SIGNIN_WITH_ID_AND_PASSWORD',{uid:uid.value,password:password.value})if(!!msg){returnalert(`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<scriptsetup>onBeforeMount(()=>{backend.refresh_access_token('APIS_ACCESS_TOKEN')})</script>This will check saved tokens at app initializing and do these things:updateVuex.state.$uidandVuex.state.$grpif access token is validif access token is expired, try refresh using refresh token and save credentialif refresh token close to expiration, refresh 'refresh token' itselfif refresh token is expired, clear all credentialFrom this moment,axiosmonitoraccess tokenwhenever you call APIs and automatically managing tokens.Then we must create 2 APIs - API IDAPIS_SIGNIN_WITH_ID_AND_PASSWORDandAPIS_AUTH_ACCESS_TOKEN.Server Side Access Controldef__mount__(app,mntopt):@app.route('/profiles/<uid>')@app.permission_required(['user'])defget_profile(was):icanaccess=was.request.user.uidreturnwas.API(profile=data)If request user is one ofuser,staffandadmingrp, access will be granted.And all claims of access token can be access viawas.request.userdictionary.@app.permission_requiredcangroupsandownerbased control.Also@app.login_requiredwhich is shortcut for@app.permission_required ([])- any groups will be granted.@app.identification_requiredis just createwas.request.userobject using access token only if token is valid.For more detail access control. seeAtila.AppendixJinja Template HelpersGlobalsraisehttp_error (status, *args): raise context.HttpErrorFiltersvue (val)summarize (val, chars = 60)attr (val)upselect (val, *names, **upserts)tojson_with_datetime (data)Macroscomponent (path, alias = none, _async = True)global_component (path, alias = none, _async = True)State Injection Macrosmap_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 HelpersAliases$http: axiosPrototype MethodsNumber.prototype.formatString.prototype.formatString.prototype.titleCaseDate.prototype.unixepochDate.prototype.formatString.prototype.repeatString.prototype.zfillNumber.prototype.zfillDevice DetectingdeviceandroidiosmobiletouchablerotatablewidthheightService Worker Syncswsyncasync 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 Authorizationbackendendpoint (name, args = [], _kargs = {})static (relurl)media (relurl)async signin_with_id_and_password (endpoint, payload)async refresh_access_token (endpoint): InonBeforeMountat__layout.vuecreate_websocket (API_ID, read_handler = (evt) => log (evt.data))push(msg)Logginglog (msg, type = 'info')traceback (e)Utilitiespermission_required (permission, redirect, next): InbeforeRouteEnterpermission_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)