vue-router란?
- SPA(Single Page Application)은 기본적으로 페이지가 하나이기 때문에 URI별로 다른 화면이 나타나도록 구현하기 힘듦
- 사용자가 요청한 URI 경로에 따라 각각 다른 화면이 랜더링이 필요
제공하는 기능
- 중첩된 경로, 뷰 매핑 가능
- 컴포넌트 기반 라우팅 구현
- Vue.js 의 전환효과(transition) 적용
- 히스토리모드, 해시모드 사용가능
- 쿼리스트링, 파라미터, 와일드카드 사용가능
vue-router의 기초
routertest 프로젝트 생성
vue create routertest
cd routertest
yarn add vue-router bootstrap@3.3.x
또는
npm install --save vue-router bootstrap@3.3.x
src/components/Home.vue ,About.vue, Contacts.vue
<template>
<div>
<h1>Abount</h1>
</div>
</template>
<script>
export default {
name : 'about'
}
</script>
src/App.vue
<template>
<div>
<div class="header">
<h1 class="headerText">(주)SSG</h1>
</div>
<nav>
<ul>
<li><router-link to="/home">Home</router-link></li>
<li><router-link to="/about">About</router-link></li>
<li><router-link to="/contacts">Contacts</router-link></li>
</ul>
</nav>
<div class="container">
<router-view></router-view>
</div>
</div>
</template>
<script>
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contacts from './components/Contacts.vue'
import VueRouter from 'vue-router';
const router = new VueRouter({
routes : [
{path :'/', component : Home},
{path :'/home', component : Home},
{path :'/about', component : About},
{path :'/contacts', component : Contacts}
]
});
export default {
name: 'App',
router
}
</script>
<style>
.header { background-color: aqua; padding: 10px 0 0 0;}
.headerText { padding: 0 20px;}
ul {
list-style-type: none; margin: 0; padding: 0;
overflow: hidden; background-color: purple;
}
li { float: left;}
li a {
display: block; color: yellow; text-align: center;
padding: 14px 16px; text-decoration: none;
}
li a:hover {background-color: aqua; color: black;}
</style>
src/main.js
import Vue from 'vue'
import App from './App.vue'
import 'bootstrap/dist/css/bootstrap.css';
import VueRouter from 'vue-router';
Vue.config.productionTip = false
Vue.use(VueRouter);
new Vue({
render: h => h(App),
}).$mount('#app')
동적 라우트
src/ContactList.js
var contactlist = {
contacts: [
{ no: 1001, name: '김유신', tel: '010-1212-3331', address: '경주' },
{ no: 1002, name: '장보고', tel: '010-1212-3332', address: '청해진' },
{ no: 1003, name: '관창', tel: '010-1212-3333', address: '황산벌' },
{ no: 1004, name: '안중근', tel: '010-1212-3334', address: '해주' },
{ no: 1005, name: '강감찬', tel: '010-1212-3335', address: '귀주' },
{ no: 1006, name: '정몽주', tel: '010-1212-3336', address: '개성' },
{ no: 1007, name: '이순신', tel: '010-1212-3337', address: '통제영' },
{ no: 1008, name: '김시민', tel: '010-1212-3338', address: '진주' },
{ no: 1009, name: '정약용', tel: '010-1212-3339', address: '남양주' }
]
};
export default contactlist;
src/components/Contacts.vue
<template>
<div>
<h1>연락처</h1>
<div class="wrapper">
<div class="box" v-for="c in contacts" :key="c.no">
<router-link v-bind:to="'/contacts/'+c.no">{{c.name}}</router-link>
</div>
</div>
</div>
</template>
<script>
import contactlist from '../ContactList';
export default {
name : 'contacts',
data() {
return {contacts: contactlist.contacts};
}
}
</script>
<style>
.wrapper { background-color: #fff; clear:both; display: table;}
.box {
float: left; background-color: aqua; border-radius: 5px;
padding: 10px; margin: 3px; text-align: center; font-size: 120%;
width: 100px; font-weight: bold;
}
a:link, a:visited {
text-align: center; text-decoration: none;
display: inline-block;
}
</style>
src/component/ContactByNo.vue
<template>
<div>
<h1>연락처 상세</h1>
<div>
<table class="detail table table-borderd">
<tbody>
<tr class="active">
<td>일련번호</td>
<td>{{contact.no}}</td>
</tr>
<tr class="active">
<td>이름</td>
<td>{{contact.name}}</td>
</tr>
<tr class="active">
<td>전화</td>
<td>{{contact.tel}}</td>
</tr>
<tr class="active">
<td>주소</td>
<td>{{contact.address}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import contactlist from '../ContactList';
export default {
name : 'contactbyno',
data(){
return {
no : 0,
contacts: contactlist.contacts
}
},
created : function() {
this.no = this.$route.params.no;
},
computed : {
contact : function(){
var no = this.no;
var arr = this.contacts.filter(function(item){
return item.no == no;
});
if(arr.length == 1) return arr[0];
else return {};
}
}
}
</script>
<style>
table.detail { width: 400px;}
</style>
src/App.vue
<script>
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contacts from './components/Contacts.vue'
import ContactByNo from './components/ContactByNo.vue'
import VueRouter from 'vue-router';
const router = new VueRouter({
routes : [
{path :'/', component : Home},
{path :'/home', component : Home},
{path :'/about', component : About},
{path :'/contacts', component : Contacts},
{path :'/contacts/:no', component : ContactByNo}
]
});
export default {
name: 'App',
router
}
</script>
중첩 라우트
하나의 컴포넌트가 다시 하위 컴포넌트를 포함하는 경우 라우팅 중첩이 가능해야 함
src/App.vue
<script>
....
const router = new VueRouter({
routes : [
{path :'/', component : Home},
{path :'/home', component : Home},
{path :'/about', component : About},
{
path :'/contacts', component : Contacts,
children : [
{path :'/contacts/:no', component : ContactByNo}
]
}
]
});
....
</script>
src/components/Contacts.vue
<template>
<div>
<h1>연락처</h1>
<div class="wrapper">
....
</div>
<router-view></router-view>
</div>
</template>
src/components/ContactByNo.vue
<template>
<div>
<hr class="divider" />
<h1>연락처 상세</h1>
....
</div>
</template>
<script>
import contactlist from '../ContactList';
export default {
name : 'contactbyno',
data(){
return {
no : 0,
contacts: contactlist.contacts
}
},
created : function() {
this.no = this.$route.params.no;
},
/*
이미 ContactByNo Vue 인스턴스가 생성되어 있는 상태이므로
더이상 created, mounted 이벤트 훅이 발생하지 않음
따라서 $route속성에 대한 관찰을 수행
- to : 현재의 라우트 객체
- from : 이전의 라우트 객체
*/
watch : {
'$route' : function(to) {
this.no = to.params.no;
}
},
computed : {
contact : function(){
var no = this.no;
var arr = this.contacts.filter(function(item){
return item.no == no;
});
if(arr.length == 1) return arr[0];
else return {};
}
}
}
</script>
<style>
table.detail { width: 400px;}
.devider {
height: 3px; margin-left: auto; margin-right: auto;
background-color: #ff0066; color: #ff0066; border: 0 none;
}
</style>
명명된 라우트
라우트에 고유한 이름을 부여하는 것
scr/App.vue
<template>
<div>
...
<nav>
<ul>
<li><router-link :to="{name : 'home'}">Home</router-link></li>
<li><router-link :to="{name : 'about'}">About</router-link></li>
<li><router-link :to="{name : 'contacts'}">Contacts</router-link></li>
</ul>
</nav>
...
</div>
</template>
<script>
...
import VueRouter from 'vue-router';
const router = new VueRouter({
routes : [
{path :'/', component : Home},
{path :'/home', name:'home', component : Home},
{path :'/about', name:'about',component : About},
{
path :'/contacts', name:'contacts',component : Contacts,
children : [
{path :'/contacts/:no', name:'contactbyno',component : ContactByNo}
]
}
]
});
...
</script>
src/components/Contacts.vue
전체 URI 경로 형태 | Router-link 예 |
/contacts/:no | <router-link v-bind:to="{name:'contacts, params: {no: 1003}'}">...</router-link> /contacts/1003 |
/contacts?pageno=:no | <router-link v-bind:to="{name:'contacts, query: {pageno: 2}'}">...</router-link> /contacts?query=2 |
<template>
<div>
<h1>연락처</h1>
<div class="wrapper">
<div class="box" v-for="c in contacts" :key="c.no">
<router-link v-bind:to="{nane : 'contactbyno',params:{no: c.no}}">{{c.name}}</router-link>
</div>
</div>
<router-view></router-view>
</div>
</template>
프로그래밍 방식의 라우트 제어
라우터 객체의 push 메서드
프로그래밍 방식으로 선언
- 링크를 클릭하면 바로 이동하는 것이 아니라 사용자의 확인을 받고 이동
- 이벤트처리르 이용해 이동하기 전에 다른 작업을 함께 수행하는 경우
Vue 인스턴스에서는 this.$router.push() 와 같이 이용
/*
- location: 이동하고자 하는 경로
- completeCallback : 네비게이션이 완료 되었을 때 호출되는 콜백 함수
- abortCallback : 네비게이션을 취소 했을 때 호출되는 콜백 함수
*/
push(location, [, completeCallback] [, abortCallback])
// 문자열 직접 전달
this.$router.push('/home');
// 객체 정보로써 전달
this.$router.push({path : '/about'});
// 명명된 라우트 사용
this.$router.push({name :'contacts', params: {no : 1002}});
// 쿼리 문자열 전달
this.$router.push({path :'/contacts', query: {pageno: 1, pagesize : 5}});
src/components/Contacts.vue
<template>
<div>
<h1>연락처</h1>
<div class="wrapper">
<div class="box" v-for="c in contacts" :key="c.no">
<span @click="navigate(c.no)" style="cursor:pointer">[ {{c.name}} ]</span>
</div>
</div>
<router-view></router-view>
</div>
</template>
<script>
import contactlist from '../ContactList';
export default {
...
methods: {
navigate(no) {
if(confirm("상세정보를 보시겠습니까?")) {
this.$router.push({name: 'contactbyno', params:{no: no}},function(){
console.log("/contacts/"+no + "로 이동완료!");
});
}
}
}
}
</script>
네비게이션 보호
다른 경로로 리디렉션하거나 네비게이션을 취소하여 애플리케이션의 네비게이션을 보호하는 기능
전역 수준 사용 방법
const router = new VueRouter({...});
/* 라우팅이 일어나기 전에 실행
- to : 이동하려는 대상 Route 객체
- from : 이동하기 전의 Route 객체
- next : 함수형
- next() : 다음 이벤트 훅으로 이동. 이함수를 호출하지 않으면 다음 이벤트 훅으로 이동하지 않음
- next(경로) : 지정된 경로로 리디렉션됨. 현재의 네비게이션이 중단되고 새로운 네비게이션이 시작
- next(error) : next에 전달된 인자가 Error객체라면 네비게이션이 중단되고 router.onError()를
이용해 등록된 콜백에러가 전달
*/
router.beforeEach((to, from, next) => {
});
// 라우팅이 일어난 후 실행
router.afterEach((to, from) => {
});
라우트 정보 수준 사용 방법
const router = new VueRouter({
route : [
{
path : '',
compoenents : ,
// 전역 수준의 beforEach 에서와 동일
beforeEnter : (to, from, next) => {
}
}
]
});
컴포넌트 수준 사용방법
const Foo = {
template : `...`,
/*
랜더링 하는 라우트 이전에 호출되는 훅.
호출 시점에는 Vue 인스턴스가 만들어지지 않았기 때문에
this를 이용할 수 없음에 주의
*/
beforeRouteEnter(to, from , next) {
},
/*
현재 경로에서 다른 경로로 빠져나갈 때 호출되는 훅
*/
beforeRouteLeave(to, from, next) {
},
/*
이미 렌더링된 컴포넌트의 경로가 변경될 때 호출되는 훅
이미 현재 컴포넌트의 Vue 인스턴스가 만들어져 있어서 재사용될 때는
beforeRouterEnter 훅은 호출되지 않고 beforeRouterUpdate 훅이 호출됨.
이 훅 대신에 $route 옵션에 관찰속성을 사용할 수 있음
*/
beforeRouteUpdate(to, from, next) {
}
}
- beforeRouteEnter에서 Vue 인스턴스를 이용하고 싶다면 콜백 함수를 이용해 비동기 처리 수행
beforeRouterEnter(to, from, next) {
next((vm) => {
//vm 이 생성된 vue 인스턴스 참조
});
}
전체 내비게이션 보호 기능의 실행흐름
- 내비게이션 시작
- 비활성화된 컴포넌트가 있다면 보호 기능 호출
- 전역 수준의 beforeEach 호출
- 재사용되는 컴포넌트의 beforeRouteUpdate 훅 호출
- 라우트 정보 수준의 beforeEnter 호출
- 활성화된 컴포넌트에서 beforeRouteEnter 훅 호출
- 네비게이션 완료
- 전역 afterEach 호출
- DOM 갱신 트리거됨
- 인스턴스들의 beforeRouterEnter 훅에서 next에 전달된 콜백 호출(콜백 사용 시에만)
src/components/ConstantByNo.vue
<script>
import contactlist from '../ContactList';
export default {
name : 'contactbyno',
...
created : function() {
this.no = this.$route.params.no;
},
beforeRouteUpdate(to, from, next){
console.log("** beforeRouteUpdate");
this.no = to.params.no;
next();
},
/*
watch : {
'$route' : function(to) {
this.no = to.params.no;
}
},
*/
...
}
</script>
src/App.vue
<script>
...
const router = new VueRouter({
routes : [
...
{
path :'/contacts', name:'contacts',component : Contacts,
children : [
{
path :'/contacts/:no', name:'contactbyno',component : ContactByNo,
beforeEnter : (to, from, next) => {
console.log("@@ beforeEnter! : " + from.path + "-->" + to.path);
next();
}
}
]
}
]
});
router.beforeEach((to, from, next) => {
console.log("** beforeEach!!");
next()
});
router.afterEach(() => {
console.log("** afterEach!!");
});
...
</script>
이동시 /contacts 나 /contacts/:no 형태의 URL만 이동. 라운트 수준의 beforeEnter 구현
- 조건이 만족하지 않으면 home 으로 이동
- 직접 url 기입 : /#/about --> /#/contacts/1004 --> /#/home 으로 리디렉션됨
<script>
...
const router = new VueRouter({
routes : [
...
{
path :'/contacts', name:'contacts',component : Contacts,
children : [
{
path :'/contacts/:no', name:'contactbyno',component : ContactByNo,
beforeEnter : (to, from, next) => {
console.log("@@ beforeEnter! : " + from.path + "-->" + to.path);
if(from.path.startWith('/contacts')){
next();
} else {
next('/home');
}
}
}
]
}
]
});
...
</script>
라우팅 모드
기본은 해시모드. 해시를 제거하려면 mode 옵션을 history 로 변경
src/App.vue
<script>
...
import NotFound from './components/NotFound';
const router = new VueRouter({
mode : 'history',
routes : [
...
,{path: "*", component: NotFound }
]
});
...
</script>
src/components/NotFound.vue
<template>
<h1>요청하신 경로는 존재하지 않습니다</h1>
</template>
라우트 정보를 속성으로 연결하기
- 컴포넌트를 라우트 객체에 의존적으로 사용하는 것은 재사용 측면에서 바람직하지 않음
- ContactByNo.vue 의 this.$route 에 의존적인 부분
- 라우트 경로에 params 정보를 속성에 연결
- 속성 정의 : props: true
- 속성 값 : this.$route.param 정보 할당
src/App.vue
- props: true 속성 추가
<script>
...
const router = new VueRouter({
mode : 'history',
routes : [
...
{
path :'/contacts', name:'contacts',component : Contacts,
children : [
{
path :'/contacts/:no', name:'contactbyno',component : ContactByNo,
props: true
}
]
}
,{path: "*", component: NotFound }
]
});
...
</script>
<!--
params 정보가 아닌 query 정보등이 속성에 부여되여야 한다면 함수의 리턴값이 할당되도록 변경
예) /contactbyno?no=1004
<script>
...
const router = new VueRouter({
mode : 'history',
routes : [
...
{
path :'/contacts', name:'contacts',component : Contacts,
children : [
{
path :'/contacts/:no', name:'contactbyno',component : ContactByNo,
props: function(route) {
return { no : route.query.no, path: route.path };
}
}
]
}
,{path: "*", component: NotFound }
]
});
...
</script>
-->
src/components/ContactByNo.vue
- props : true 인 경우에 route.params 정보에 동일한 속성 할당됨
- URI /contacts/:no 의 :no 는 props : ['no'] 속서에 전달됨
<script>
import contactlist from '../ContactList';
export default {
name : 'contactbyno',
props :['no'],
data(){
return {
// no : 0,
contacts: contactlist.contacts
}
},
/*
created : function() {
this.no = this.$route.params.no;
},
beforeRouteUpdate(to, from, next){
console.log("** beforeRouteUpdate");
this.no = to.params.no;
next();
},
*/
...
}
</script>
<!--
props: function(route) { return { no : route.query.no, path: route.path }; } 일경우
다음과 같이 속성이 부여됨
export default {
...
props :['no','path'],
...
}
-->
연락처 애플리케이션에 라우팅 기능을 제공
contactapp 프로젝트를 변경
라우팅 경로 정보
URI 경로 | 이름 | 컴포넌트 |
/ | Home.vue 컴포넌트(/home 리디렉션) | |
/home | home | Home.vue 컴포넌트 |
/about | about | About.vue 컴포넌트 |
/contacts | contacts | ContactList.vue 컴포넌트 |
/contacts/add | addcontact | ContactForm.vue 컴포넌트(입력) |
/contacts/update/:no | updatecontact | ContactForm.vue 컴포넌트(수정) |
/contacts/photo/:no | updatephoto | UpdatePhoto.vue 컴포넌트(사진변경) |
초기 설정 작업
src/components/Home.vue , About.vue
<template>
<div id="example">
<div class="panel panel-default">
<div class="panel-heading">About</div>
<div class="panel-body">About 화면입니다.</div>
</div>
</div>
</template>
<style>
#example {
margin: 10px auto; max-width: 820px; min-width: 820px;
padding: 0px; position: relative; font: 13px "verdana";
}
</style>
vue-router 패키지 추가
yarn add vue-router
또는
npm install --save vue-router
vuex 상태 관리 기능 변경
- vue-router 에서는 currentView, mode 모두 필요하지 않음
src/Constant.js
export default {
// 변이와 액션 모두 사용
...
FETCH_CONTACT_ONE: 'fetchContactOne', // 연락처 한건 조회
INITIALIZE_CONTACT_ONE: 'initializeContactOne', // 연락처 초기화
// 액션에서만 사용
...
}
src/store/state.js
import CONF from '../Config';
export default {
// currentView: null,
// mode: 'add',
contact: { no: 0, name: '', tel: '', address: '', photo: '' },
contactlist: { pageno: 1, pagesize: CONF.PAGESIZE, totalcount: 0, contacts: [] }
}
src/store/mutations.js
- axios를 이용해 외부 API를 요청한 후 수신한 데이터를 FETCH_CONTACTS 변이를 이용해 업데이트 하기 때문에 수장과 삭제에 관한 변이는 등록할 것이 없음
import Constant from '../Constant';
// 상태를 변경하는 기능만 구현
export default {
[Constant.FETCH_CONTACTS]: (state, payload) => {
state.contactlist = payload.contactlist;
},
[Constant.FETCH_CONTACT_ONE]: (state, payload) => {
state.contact = payload.contact;
},
[Constant.INITIALIZE_CONTACT_ONE]: (state) => {
state.contact = { no: '', name: '', tel: '', address: '', photo: '' };
}
}
src/store/actions.js
import Constant from '../Constant';
import axios from 'axios';
import CONF from '../Config';
export default {
[Constant.FETCH_CONTACTS]: (store, payload) => {
var pageno;
if (typeof payload === "undefined" || typeof payload.pageno === "undefined") {
pageno = 1;
} else {
pageno = payload.pageno;
}
var pagesize = store.state.contactlist.pagesize;
axios.get(CONF.FETCH, {
params: { pageno: pageno, pagesize: pagesize }
}).then((response) => {
store.commit(Constant.FETCH_CONTACTS, { contactlist: response.data });
});
},
[Constant.ADD_CONTACT]: (store) => {
axios.post(CONF.ADD, store.state.contact).then((response) => {
if (response.data.status === 'success') {
//store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: 1 });
} else {
console.log("연락처 추가 실패!! : " + response.data);
}
});
},
[Constant.UPDATE_CONTACT]: (store) => {
var currentPageNo = store.state.contactlist.pageno;
var contact = store.state.contact;
axios.put(CONF.UPDATE.replace('${no}', contact.no), contact).then((response) => {
if (response.data.status == 'success') {
//store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
} else {
console.log("연락처 변경 실패 : " + response.data);
}
});
},
[Constant.UPDATE_PHOTO]: (store, payload) => {
var currentPageNo = store.state.contactlist.pageno;
var data = new FormData();
data.append('photo', payload.file);
axios.post(CONF.UPDATE_PHOTO.replace("${no}", payload.no), data).then(() => {
//store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
});
},
[Constant.DELETE_CONTACT]: (store, payload) => {
var currentPageNo = store.state.contactlist.pageno;
axios.delete(CONF.DELETE.replace("${no}", payload.no)).then(() => {
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
});
},
// 새로 추가된 부분
[Constant.FETCH_CONTACT_ONE]: (store, payload) => {
axios.get(CONF.FETCH_ONE.replace("${no}", payload.no)).then((response) => {
store.commit(Constant.FETCH_CONTACT_ONE, { contact: response.data });
});
},
[Constant.INITIALIZE_CONTACT_ONE]: (store) => {
store.commit(Constant.INITIALIZE_CONTACT_ONE);
}
}
main.js 에 라우팅 기능 추가
import Vue from 'vue'
import App from './App.vue'
import store from './store';
import VueRouter from 'vue-router';
import Home from './components/Home';
import About from './components/About';
import ContactList from './components/ContactList';
import ContactForm from './components/ContactForm';
import UpdatePhoto from './components/UpdatePhoto';
import 'bootstrap/dist/css/bootstrap.css';
import ES6Promise from 'es6-promise';
ES6Promise.polyfill();
Vue.use(VueRouter);
Vue.config.productionTip = false;
var router = new VueRouter({
routes: [
{ path: '', redirect: '/home' },
{ path: '/home', name: 'home', component: Home },
{ path: '/about', name: 'about', component: About },
{ path: '/contacts',name: 'contacts',component: ContactList, children: [
{ path: '/add', name: 'addcontact', component: ContactForm },
{ path: '/update/:no', name: 'updatecontact', component: ContactForm, props: true },
{ path: '/photo/:no', name: 'updatephoto', component: UpdatePhoto, props: true }
]}
]
});
new Vue({
store,
router,
render: h => h(App),
}).$mount('#app')
App.vue 컴포넌트 변경
<template>
<div id="container">
<div class="page-header">
<h1 class="text-center">연락처 관리 애플리케이션</h1>
<p>(Dynamic Component + EventBus + Axios)</p>
</div>
<!--
<component :is="currentView"></component>
-->
<div class="btn-group">
<router-link to="/home" class="btn btn-info menu">Home</router-link>
<router-link to="/about" class="btn btn-info menu">About</router-link>
<router-link to="/contacts" class="btn btn-info menu">Contacts</router-link>
</div>
<router-view></router-view>
<!--
<contact-list></contact-list>
-->
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
ContactList.vue 컴포넌트 변경
<template>
<div>
<p class="addnew">
<router-link class="btn btn-primary" v-bind:to="{ name : 'addcontact'}" >
새로운 연락처 추가하기
</router-link>
</p>
<div id="example">
<table id="list" class="table table-striped table-bordered table-hover">
...
</table>
</div>
<paginate ref="pagebuttons"
:page-count="totalpage"
:page-range="7"
:margin-pages="3"
:click-handler="pageChanged"
:prev-text="'이전'"
:next-text="'다음'"
:container-class="'pagination'"
:page-class="'page-item'">
</paginate>
<router-view></router-view>
</div>
</template>
<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';
import Paginate from 'vuejs-paginate';
export default {
name : 'contactList',
components : { Paginate },
computed : {
totalpage : function() {
return Math.floor((this.contactlist.totalcount-1) / this.contactlist.pagesize) + 1;
},
...mapState(['contactlist'])
},
mounted : function(){
var page = 1;
if(this.$route.query && this.$route.query.page){
page = parseInt(this.$route.query.page);
}
this.$store.dispatch(Constant.FETCH_CONTACTS,{pageno:page});
this.$refs.pagebuttons.selected = page-1;
},
watch : {
'$route' : function(to, from) {
if(to.query.page && to.query.page != this.contactlist.pageno){
var page = to.query.page;
this.$store.dispatch(Constant.FETCH_CONTACTS, {pageno, page});
this.$refs.pagebuttons.selected = page-1;
}
}
},
methods : {
pageChanged : function(page) {
this.$router.push({name : 'contacts', query : {page: page}});
},
editContact : function(no) {
this.$router.push({name : 'updatecontact', params : {no : no}});
},
deleteContact : function(no) {
if(confirm("정말로 삭제하시겠습니까?")){
this.$store.dispatch(Constant.DELETE_CONTACT,{no:no});
//this.$router.push({name : 'contacts'});
this.$router.go(this.$router.currentRouter);
}
},
editPhoto : function(no) {
this.$router.push({name : 'updatephoto', params: {no:no }});
}
}
}
</script>
ContactForm.vue, UpdatePhoto.vue 컴포넌트 수정
ContactForm.vue
<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';
export default {
name : 'contactForm',
data() {
return {mode : 'add'};
},
props: ['no'],
computed : {
btnText : function() {
if(this.mode != 'update') return '추 가';
else return '수 정';
},
headingText : function() {
if(this.mode != 'update') return '새로운 연락처 추가';
else return '연락처 변경';
},
...mapState(['contact','contactlist'])
},
mounted : function() {
this.$refs.name.focus();
var cr = this.$router.currentRoute;
if(cr.fullPath.indexOf('/add') > -1){
this.mode ='add';
this.$store.dispatch(Constant.INITIALIZE_CONTACT_ONE);
} else if(cr.fullPath.indexOf('/update') > -1) {
this.mode ='update';
this.$store.dispatch(Constant.FETCH_CONTACT_ONE,{no : this.no});
}
},
methods : {
submitEvent : function() {
if(this.mode == 'update'){
this.$store.dispatch(Constant.UPDATE_CONTACT);
this.$router.push({name: 'contacts', query: {page: this.contactlist.pageno}});
}else {
this.$store.dispatch(Constant.ADD_CONTACT);
this.$router.push({name: 'contacts', query: {page: 1}});
}
},
cancelEvent : function() {
//this.$store.dispatch(Constant.CANCEL_FORM);
this.$router.push({name: 'contacts', query: {page: this.contactlist.pageno}});
}
}
}
</script>
UpdatePhoto.vue
<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';
export default {
name : 'updatePhoto',
props: ['no'],
computed : mapState(['contact', 'contactlist']),
mounted : function() {
this.$store.dispatch(Constant.FETCH_CONTACT_ONE,{no : this.no});
},
methods: {
cancelEvent : function() {
//this.$store.dispatch(Constant.CANCEL_FORM);
this.$router.push({name: 'contacts', query: {page: this.contactlist.pageno}});
},
photoSubmit : function() {
var file = this.$refs.photofile.files[0];
this.$store.dispatch(Constant.UPDATE_PHOTO,{no:this.contact.no, file:file});
this.$router.push({name: 'contacts', query: {page: this.contactlist.pageno}});
}
}
}
</script>
지연 시간에 대한 처리
API 호출 지연 시간 발생
src/Config.js 오래 시간이 걸리는 api로 변경
let BASE_URL = "/api";
export default {
// 한 페이지에 보여줄 페이지 사이즈
...
// 전체 연락처 데이터 요청
FETCH: BASE_URL + "/contacts_long",
...
}
스피너 컴포넌트 작성
yarn add vue-simple-spinner
또는
npm install --save vue-simple-spinner
Loading.vue
<template>
<div class="modal">
<spinner class="spinner" :line-size="10" :size="100">
</spinner>
</div>
</template>
<script>
import Spinner from 'vue-simple-spinner';
export default {
components : {
Spinner
}
}
</script>
<style scoped>
.modal {
display: block; position: fixed; z-index: 1;
left: 0; top: 0; width: 100%; height: 100;
overflow: auto; background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
}
.spinner {
position: absolute; left: 50%; top: 50%;
margin-top: -50px; margin-left: 50px;
}
</style>
Constant.js
export default {
// 변이와 액션 모두 사용
...
INITIALIZE_CONTACT_ONE: 'initializeContactOne', // 연락처 초기화
CHANGE_ISLOADING: 'changeIsLoading', // 스피너UI를 보여줄지 여부 결정
// 액션에서만 사용
...
}
src/store/state.js
import CONF from '../Config';
export default {
// currentView: null,
// mode: 'add',
isloading: false,
contact: { no: 0, name: '', tel: '', address: '', photo: '' },
contactlist: { pageno: 1, pagesize: CONF.PAGESIZE, totalcount: 0, contacts: [] }
}
src/store/mutations.js
import Constant from '../Constant';
// 상태를 변경하는 기능만 구현
export default {
[Constant.FETCH_CONTACTS]: (state, payload) => {
state.contactlist = payload.contactlist;
},
[Constant.FETCH_CONTACT_ONE]: (state, payload) => {
state.contact = payload.contact;
},
[Constant.INITIALIZE_CONTACT_ONE]: (state) => {
state.contact = { no: '', name: '', tel: '', address: '', photo: '' };
},
[Contant.CHANGE_ISLOADING]: (state, payload) => {
state.isloading = payload.isloading;
}
}
src/store/actions.js
import Constant from '../Constant';
import axios from 'axios';
import CONF from '../Config';
export default {
[Constant.CHANGE_ISLOADING]: (store, payload) => {
store.commit(Constant.CHANGE_ISLOADING, payload);
},
[Constant.FETCH_CONTACTS]: (store, payload) => {
var pageno;
if (typeof payload === "undefined" || typeof payload.pageno === "undefined") {
pageno = 1;
} else {
pageno = payload.pageno;
}
var pagesize = store.state.contactlist.pagesize;
store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
axios.get(CONF.FETCH, {
params: { pageno: pageno, pagesize: pagesize }
}).then((response) => {
store.commit(Constant.FETCH_CONTACTS, { contactlist: response.data });
store.dispatch(Constant.CHANGE_ISLOADING, { isloading: false });
});
},
[Constant.ADD_CONTACT]: (store) => {
store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
axios.post(CONF.ADD, store.state.contact).then((response) => {
if (response.data.status === 'success') {
//store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: 1 });
} else {
console.log("연락처 추가 실패!! : " + response.data);
}
});
},
[Constant.UPDATE_CONTACT]: (store) => {
var currentPageNo = store.state.contactlist.pageno;
var contact = store.state.contact;
store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
axios.put(CONF.UPDATE.replace('${no}', contact.no), contact).then((response) => {
if (response.data.status == 'success') {
//store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
} else {
console.log("연락처 변경 실패 : " + response.data);
}
});
},
[Constant.UPDATE_PHOTO]: (store, payload) => {
var currentPageNo = store.state.contactlist.pageno;
var data = new FormData();
data.append('photo', payload.file);
store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
axios.post(CONF.UPDATE_PHOTO.replace("${no}", payload.no), data).then(() => {
//store.dispatch(Constant.CANCEL_FORM);
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
});
},
[Constant.DELETE_CONTACT]: (store, payload) => {
var currentPageNo = store.state.contactlist.pageno;
store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
axios.delete(CONF.DELETE.replace("${no}", payload.no)).then(() => {
store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
});
},
// 새로 추가된 부분
[Constant.FETCH_CONTACT_ONE]: (store, payload) => {
store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
axios.get(CONF.FETCH_ONE.replace("${no}", payload.no)).then((response) => {
store.commit(Constant.FETCH_CONTACT_ONE, { contact: response.data });
store.dispatch(Constant.CHANGE_ISLOADING, { isloading: false });
});
},
[Constant.INITIALIZE_CONTACT_ONE]: (store) => {
store.commit(Constant.INITIALIZE_CONTACT_ONE);
}
}
src/App.vue
<template>
<div id="container">
....
<router-view></router-view>
<loading v-show="isloading"></loading>
</div>
</template>
<script>
import Loading from './components/Loading';
import {mapState} from 'vuex';
export default {
name: 'App',
components: {Loading},
computed : mapState(['isloading'])
}
</script>
'책 > Vue.js 퀵스타트' 카테고리의 다른 글
단위 테스트 (0) | 2020.11.28 |
---|---|
트랜지션 효과 (0) | 2020.11.28 |
Vuex를 이용한 상태관리 (0) | 2020.11.27 |
axios를 이용한 서버통신 (0) | 2020.11.25 |
컴포넌트 심화 (0) | 2020.11.23 |