- 인피니티 스크롤
- window에서 스크롤 하는 경우 (search.vue)
- div에서 스크롤 하는 경우 (article.vue, exam.vue)
- axios
- api 응답오기 전에 다른 api 요청을 날리는 경우 cancel 처리
- visual viewport
- 모바일에서 가상키보드 활성화 이벤트가 따로 없어서 visual viewport로 감지
- dialog 특징
- menu: 특정 컴포넌트를 기준으로 좌표설정해서 띄움 (position, x, y prop 확인)
- bottomsheet: 스크롤 가능한 컴포넌트
- vue에서 컴포넌트 생성(created) 시 window.addEventListener('scroll', this.loadMore)
- this.loadMore
- getScrollTop로 위에서부터 scroll한 px
- getScrollThreshold로 document의 offsetHeight - innerheight - 600
- scrollTop이 getScrollThreshold보다 크고 this.isFetched가 true면 다시 fetchLectures를 한다
- fetchLectures
- axios.CancelToken : Axios의 취소 토큰 - 요청을 취소할 수 있다
cf. CancelToken.source() 팩토리를 사용하여 취소 토큰을 만들 수 있다
cancelToken 사용법 예시
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 에러 핸들링
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// 요청 취소하기 (메시지 파라미터는 옵션입니다)
source.cancel('Operation canceled by the user.');
1) views/pages/lecture/search.js
: window.addEventListener('scroll', this.loadMore)
created(){
const previouspageCache = window.sessionStorage.getItem('previouspage')
//현재 페이지
const pageCache = getPageCache()
//JSON.parse(window.sessionStorage.getItem(key))
if(/^\/lecture\/view/.test(previouspageCache) && pageCache !== null){
//현재 페이지가 lecture/view + pageCache에 뭐가 들어가있으면
this.lectures = pageCache.lectures;
this.campusId = pageCache.campusId;
window.addEventListener('scroll', this.loadMore)
return;
}
window.addEventListener('scroll', this.loadMore)
}
destroyed(){
window.removeEventListener('scroll', this.loadMore)
}
methods:{
async fetchLectures({refresh} = {}) {
if (this.cancelSource) { //cancelSource에 취소토큰을 할당했으면
this.cancelSource.cancel(); //요청취소하기
this.cancelSource = undefined; //원상복구
}
if (refresh) { //refresh하라고 했으면 빈 배열로
this.lectures = [];
}
const {campusId, limit} = this;
const {query} = this.$route;
const keyword = query.keyword.trim();
if (keyword.replace(/\s/g, '').length === 0) {
return;
}
this.isFetched = false;
const data = {
campusId,
field: query.condition,
keyword,
limit,
offset: this.lectures.length
};
this.cancelSource = this.$axios.CancelToken.source();
//axios 요청을 취소할 수 있는 취소토큰을 CancelToken.source()를 이용해서 만든다
const cancelToken = this.cancelSource.token;
//
try {
const {data: {result, status}} = await this.$http.post('/find/lecture/list/keyword', data, {cancelToken});
//요청 취소. cancelToken이 undefined여도 괜찮은걸까?
this.cancelSource = undefined;
//원상복구
if (status === 'success') {
//성공했으면
const {lectures} = result;
//결과 중 lectures받아서
if (lectures.length === 0) {
window.removeEventListener('scroll', this.loadMore);
//길이가 0이면 삭제
} else {
//길이가 0이 아니면
const addHightlightTag = (text) => {
if (!text) {
return text;
}
return text.replace(keyword, `<span class="highlight">${keyword}</span>`);
//
};
this.lectures = this.lectures.concat(lectures.map(lecture => {
const name = query.condition === 'name' ? addHightlightTag(lecture.name.replace(/</g, '<')) : lecture.name.replace(/</g, '<');
const professor = query.condition === 'professor' ? addHightlightTag(lecture.professor?.replace(/</g, '<')) : lecture.professor?.replace(/</g, '<');
return {
...lecture,
name,
professor
};
}));
window.addEventListener('scroll', this.loadMore);
setPageCache({
campusId,
lectures: this.lectures
});
}
}
this.isFetched = true;
//post를 해서 데이터를 다 받아오면 true로
} catch (err) {
if (!this.$axios.isCancel(err)) {
throw err;
}
}
},
loadMore: throttle(function(){
const scrollTop = getScrollTop();
const scrollThreshold = getScrollThreshold(600)
if(scrollTop > scrollThreshold && this.isFetched){
this.fetchLectures();
}, 100)
}
})
}
2) views/service.js : setPageCache, getPageCache
export function setPageCache(data) {
Object.keys(window.sessionStorage)
.filter(key => key.startsWith('pagecache_'))
.forEach(key => window.sessionStorage.removeItem(key));
const {pathname, search} = window.location;
const key = `pagecache_${pathname}${search}`;
window.sessionStorage.setItem(key, JSON.stringify(data));
}
//remove를 해서 다시 set
function getPageCache(){
const {pathname, search} = window.location
const key = `pagecache_${pathname}${search}`
try{
return JSON.parse(window.sessionStorage.getItem(key))
}catch{
return null
}
}
export function getScrollThreshold(offset = 0) {
const {offsetHeight} = document.scrollingElement || document.documentElement;
//returns the height of an element, including vertical padding and borders, as an integer.
return offsetHeight - window.innerHeight - offset;
//innerHeight property of the Window interface : returns the interior height of the window in pixels, including the height of the horizontal scroll bar, if present.
}
function getScrollTop(){
const {scrollTop} = document.scrollingElement || document.documentElement; //문서 불러온다
return scrollTop
//scrollTop은 위에서부터 몇 px인지
}
3)throttle.js
//함수를 실행하기 전에 기다릴 시간을 정해준다
export default function throttle(func, wait, options){
var timeout, context, args, result;
var previous = 0;
if(!options) options = {};
var later = function(){
previous = options.leading === false ? 0 : now();
timeout = null;
result = func.apply(context, args)
if(!timeout) context = args = null;
}
var throttled = function(){
var _now = now();
if(!previous && options.leading === false) previous = _now;
var remaining = wait - (_now - previous);
context = this;
args = arguments;
if(remaining <= 0 || remaining > wait){
if(timeout){
clearTimeout(timeout);
timeout = null;
}
previous = _now;
result = func.apply(context, args)
if(!timeout)context = args = null;
}
}
}
1.article.vue
- 상위 컴포넌트에서 하위 컴포넌트 사용 :
- v-bind="menu.rate"
- v-slot="{item:{count, icon, title}}"
//js에서 넘기고 있는 값을 하위 컴포넌트인 menu.vue로 넘긴다
//props.item의 작업을 미리해서 바로 count, icon, title 사용 가능
- article.js
- data(){return menu{rate: undefined}}
- methods:{handleClickSortButton(){
...
this.menu.sort = {
items : this.sortOptions,
x:targetDom.right,
y:targetDom.bottom + 8,
position: 'topright'
}
}
- menu.js
props:{items, x, y, position}
//props로서 각각의 조건 정의 ex. required : true/false- menu.vue
//이 자리에 상위 컴포넌트에서 v-slot으로 넘긴 값들이 들어온다
touchmove : The touchmove event is fired when one or more touch points are moved along the touch surface.
wheel : The wheel event fires when the user rotates a wheel button on a pointing device (typically a mouse).
vue 이벤트 발생
: 컴포넌트 통신 방법 중 하위 컴포넌트에서 상위 컴포넌트로 통신하는 방식
이벤트 발생 코드 형식
하위 컴포넌트의 메서드나 라이프 사이클 훅과 같은 곳에 아래와 같은 코드 추가
//하위 컴포넌트의 내용
this.$emit('이벤트 명');
해당 이벤트를 수신하기 위해 상위 컴포넌트의 템플릿에 아래와 같이 구현
:v-on:이벤트 명="상위 컴포넌트의 실행할 메서드 명 또는 연산"
<!-- 상위 컴포넌트의 템플릿 -->
<div id="app">
<child-component v-on:이벤트명A="함수명A"></child-component>
</div>
ex.
// 하위 컴포넌트 : childComponent
var childComponent = {
methods: {
함수명A: function() {
this.$emit('이벤트명A ex.click');
}
}
}
// 상위 컴포넌트 : root 컴포넌트
new Vue({
el: '#app',
components: {
'child-component': childComponent
},
methods: {
함수명B: function() {
alert('event received');
}
}
})
<div id="app">
<child-component v-on:이벤트명A="함수명B"></child-component>
</div>
childComponent -> sendEvent() 실행 시 update이벤트 발생 -> 상위 컴포넌트인 루트 컴포넌트의 v-on 디렉티브로 이벤트를 받아 show