TypeScript, Node, Express 및 Vue를 사용한 Instagram 구축 - 섹션 5

5부로 구성된 튜토리얼의 5번째 부분이지만, 각 튜토리얼은 개별적으로 읽어 Node+Express+TypeScript+Vue API/Vue 웹 앱 설정의 여러 측면을 이해할 수 있다.

고급 Vue 템플릿 및 이미지를 Express에 업로드


이동/데스크톱 응용 프로그램을 배우고 싶으세요?여기에는 모바일 애플리케이션(Native Script)이나 데스크톱 애플리케이션(Electron)에 사용할 수 있는 기술과 개념이 기본입니다.나는 그것들을 후속 보도로 삼을 수도 있다.

다른 섹션으로 이동(섹션 5)





  • 고급 Vue 템플릿 및 이미지를 Express에 업로드
  • 없는 경우 Tutorial-part4 branch를 클론 및 체크 아웃하여 구축을 시작할 수 있습니다.
    git clone https://github.com/calvintwr/basicgram.git
    git checkout tutorial-part4
    
    이 자습서에서는 Basicgram 애플리케이션을 사용하여 이미지가 있는 게시물을 업로드하고, 이미지를 수신하기 위해 Express API 엔드포인트를 구축하고, 게시물 요약을 생성하기 위해 다른 엔드포인트를 구축하며, 마지막으로 전체 순환을 완성하기 위해 Vue 템플릿을 표시합니다.

    1. 이미지 크기 조정


    우선, 업로드하기 전에 클라이언트 이미지의 크기를 조정하고 싶습니다.이것은 자바스크립트 브라우저의 크기 조절기를 사용하는 것을 의미한다. 처음에는 나쁜 생각으로 들릴 수도 있지만, 이런 상황을 감안하면 사실은 그렇지 않다.전반적으로 클라이언트가 크기를 조정한 이미지는 더 빠른 업로드 시간을 허용하고 서버 대역폭 소모를 줄일 수 있으며 사용자가 실제적으로 DSLR에서 이미지를 직접 저장할 수 있게 한다.사실 그것은 그렇게 느리지 않고 이미지 효과가 매우 좋다 Blitz :
    npm install blitz-resize --save
    
    const Blitz = require('blitz-resize')
    let blitz = Blitz.create()
    
    blitz.resize({
        source: file or event,
        height: 640,
        width: 640,
        output: 'jpg', // or png or gif etc,
        outputFormat: image/canvas/data/blob/download,
        quality: 0.8 // 80%
    }).then(output => {}).catch(err => {})
    

    번개전 및 이미지 처리/업로드 정보


    이미지 처리 중의 데이터 형식은 보통 두 종류가 있다.먼저 dataURI입니다. 이렇게 <img>src에 연결할 수 있습니다.
    <!-- single quote due to XSS Markdown restrictions -->
    <img src=`data:image/png;base64,iVBORw0KGgo...`>
    
    두 번째는 Blob 형식으로 HTTP/HTTPS를 통해 업로드하는 데 사용됩니다.
    Blitzoutput: 'data' 또는 output: 'blob'를 통해 제공할 수 있지만, 이것은 어떻게 사용되는지 잠시 후에 볼 수 있습니다.

    2. 코딩 카메라.vue 발표 준비:


    <!-- camera.vue -->
    <template>
        <v-ons-page>
            <div class="container text-center  mx-auto p-1">
                <!-- attach the #readFile method to change event -->
                <input 
                    type="file" 
                    capture="camera" 
                    accept="image/*" 
                    id="cameraInput" 
                    name="cameraInput"
                    @change="readFile" 
                >
                <img class="py-2" ref="image">
                <!-- `ref` defined for this textarea is a Vue reference which will be handy -->
                <textarea 
                    class="py-2 w-full textarea" 
                    rows="3" 
                    placeholder="Write your caption"
                    ref="caption"
                ></textarea>
    
                <!-- #post is for uploading the post -->
                <button 
                    class="my-2 button"
                    @click="post" 
                    :disabled="buttonDisabled"
                >Post</button>
            </div>
    
        </v-ons-page>
    </template>
    
    <script lang="ts">
    import Vue from "vue"
    import Blitz = require('blitz-resize')
    import * as superagent from 'superagent'
    const blitz = Blitz.create()
    
    export default {
        props: {
            userName: {
                type: String
            },
            userID: {
                type: Number
            }
        },
        data() {
            return {
                image: { type: Blob }, // this is to store our image
                buttonDisabled: true // a flag to turn our button on/off
            }
        },
        methods: {
            readFile(event) {
                let file = event.srcElement.files[0] // this is where HTML file input puts the file
                let self = this
                let output;
    
                // super fast resizing 
                blitz({
                    source: file,
                    height: 640,
                    width: 640,
                    outputFormat: 'jpg',
                    // we will use data because we want to update the image in the DOM
                    output: 'data', 
                    quality: 0.8
                }).then(data => {
    
                    // update the image so that user sees it.
                    self.$refs["image"].src = data
    
                    // prepare the Blob. Blitz internally has a #dataURItoBlob method.
                    self.image = Blitz._dataURItoBlob(data) 
    
                    self.buttonDisabled = false
                }).catch(err => {
                    console.log(err)
                })
    
            },
            post(event) {
                let self = this
                this.buttonDisabled = true
                let caption = this.$refs["caption"].value // using Vue's $ref property to get textarea.
    
                // Note: To upload image, the request type will be "multipart"
                // Superagent automatically takes care of that and you need to
                // use `field` for text/plain info, and `attach` for files 
                superagent
                    .post('http://localhost:3000/posts/add')
                    .field('userID', this.userID)
                    .field('caption', caption)
                    .attach('photo', this.image)
                    .then((res: superagent.Response) => {
                        alert('Successful post. Go to your profile to see it.')
                    }).catch((err: Error) => {
                        this.buttonDisabled = false
                        alert(err)
                    })
            }
        }
    }
    </script>
    

    3. 게시물을 받을 API 준비


    현재 우리의 보기는 발표할 준비가 되어 있으며, localhost:3000/posts/add API 단점을 만들어야 합니다.
    코드를 작성하기 전에, 우리는 파일을 어디에 업로드할지 고려해야 한다.자연스러운 선택은'public/uploads'아래에 두는 것입니다. 그러나 에서'dist'폴더 전체를 삭제하기 위해 TypeScript 컴파일러를 설정한 다음 그것을 컴파일합니다.이것은 컴파일할 때마다 우리가 올린 모든 그림을 삭제합니다.
    따라서 "api"와 src와 같은 단계에서 볼 수 있도록 공용 폴더를 옮겨야 합니다. 아래와 같습니다.

    또한 Express에 공용 폴더가 변경되었음을 알려야 합니다. 기본적으로 이 폴더는 정적 파일을 제공하는 데 사용됩니다.
    /* api/src/app.ts */
    
    // change
    app.use(express.static(join(__dirname, 'public')))
    
    // to
    app.use(express.static(join(__dirname, '../public')))
    
    Express는 멀티 섹션 요청을 처리하지 않으므로 모듈이 필요합니다.가장 좋은 것은 formidable입니다.그리고multerbusboy,하지만 나는 formidable의 문법이 가장 우호적이라는 것을 발견했다.

    설치:


    npm install formidable --save
    npm install @types/formidable --save-dev
    
    Knowledge의 문법은 매우 유연하고 이벤트가 구동된다.그래서 우리의 생각은 함수를 이벤트에 추가하는 것이다.예를 들어 HTTP 수신이 모든 데이터 전송을 완료하면 Foregrouble에서 이벤트end를 보내며 이 이벤트를 사용합니다.
    const form = formidable()
    function formEndHandler():void { perform some action. }
    form.on('end', formEndHandler)
    
    이 점을 감안하여 우리는 routes/posts.ts:

    게시물.ts:


    import express from 'express'
    import { Fields, Files, File } from 'formidable' // typing
    import { join } from 'path' // we will use this for joining paths
    const formidable = require('formidable') // formidable
    
    const router = express.Router()
    const Not = require('you-are-not')
    const not = Not.create()
    const DB = require('../models/index')
    
    router.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
        // get all posts
    })
    
    router.post('/add', (req: express.Request, res: express.Response, next: express.NextFunction) => {
    
        const form = formidable({ multiples: true })
    
        let params: any
        form.parse(req, (err: Error, fields: Fields, files: Files) => {
            params = fields
    
            // use Not to sanitise our received payload
    
            // define a schema
            let schema = {
                userID: ['string', 'number'],
                caption: ['string']
            }
    
            // sanitise it
            let sanitised = Not.checkObject(
                'params',
                schema, 
                params, 
                { returnPayload: true }
            )
    
            // if sanitised is an array, we will throw it 
            if(Array.isArray(sanitised)) {
                throw Error(sanitised.join(' | ')) // join the errors
            }
            params = sanitised
        })
    
        let fileName: string;
        form.on('fileBegin', (name: string, file: File) => {
            fileName = name + (new Date().getTime()).toString() + '.jpg'
            file.path = join(__dirname, '../../public/uploads', fileName)
        })
    
        form.on('error', (err: Error) => {
            next(err) // bubbble the error to express middlewares
        })
    
        // we let the file upload process complete before we create the db entry.
        // you can also do it asynchronously, but will require rollback mechanisms
        // like transactions, which is more complicated.
        form.on('end', () => {
            return DB.Post.create({
                User_userID: params.userID,
                image: fileName,
                caption: params.caption
            }).then((post: any) => {
                console.log(post)
                res.status(201).send(post)
            }).catch((err: Error) => {
                next(err)
            })
        })
    })
    
    module.exports = router
    
    서버를 재부팅하고 보기로 이동하면 다음 작업을 수행할 수 있습니다.

    클라이언트 압축을 통해 파일 크기가 크게 줄어들었기 때문에 크기 조정이 매우 빠르고 업로드 시간도 매우 빠르다는 것을 깨닫게 된다Blitz.
    현재, 우리는 사용자를 위해 단점을 만들어서, 개인 정보 페이지에 사용하고, 홈페이지에 게시물 요약을 만들 수 있습니다.

    4. 프로필 페이지 프로필.vue 및 API 끝점


    너 이제 잘 될 거야.사용자의 모든 게시물의 끝점 가져오기(GET/posts/own으로 명명됨)는 어렵지 않습니다.


    /* routes/posts.ts */
    
    router.get('/own', (req: express.Request, res: express.Response, next: express.NextFunction) => {
    
        // we will receive userID as a string. We want to parse it and make sure
        // it's an integer like "1", "2" etc, and not "1.1", "false"
        Not.defineType({
            primitive: 'string',
            type: 'parseable-string',
            pass(id: string) {
                // TypeScript does not check at runtime. `string` can still be anything, like null or boolean.
                // so you need Notjs.
                return parseInt(id).toString() === id
            }
        })
    
        // for GET, the standard is to use querystring.
        // so it will be `req.query` instead of `req.body`
        not('parseable-string', req.query.userID)  
    
        DB.Post.findAll({
            where: {
                User_userID: req.query.userID
            },
            order: [ ['id', 'DESC'] ] // order post by id in descending, so the latest will be first.
        }).then((posts:any) => {
            res.send(posts)
        }).catch((err:Error) => {
            next(err)
        })
    })
    
    

    VueJS 갈고리의 아래쪽: #created(), #mounted() 등등...


    다음은profile.vue.
    VueJS는 several "hooks"를 제공하여 뷰를 준비합니다.그것들은 이렇게 보인다.
    <template>
        <div> {{ dataFromAPI }} </div>
    </template>
    <script>
    import Vue from 'vue'
    export default {
        data() {
            return {
                // this is bound to {{ dataFromAPI }} in the DOM
                dataFromAPI: 'Waiting for API call'
            }
        },
        // or created(), depending on when you want it.
        mounted() {
            // anything inside here gets called when this view is mounted
    
            // you will fetch some data from API.
    
            // suppose API results the results, then doing this:
            this.dataFromAPI = results
            // will update the value in {{ dataFromAPI }}
        }
    }
    </script>
    

    가장 자주 사용하는 것은created()와 mounted()입니다.우리는 설정 파일을 인코딩할 것이다.vue는 다음과 같습니다.


    <!-- profile.vue -->
    <template>
        <v-ons-page>
            <div class="content">
                <div class="w-full p-10" style="text-align: center">
                    {{ userName }}'s Profile
                </div>
    
                <!-- Three columns Tailwind class-->
                <div v-if="posts.length > 0" class="flex flex-wrap -mb-4">
                    <div 
                        class="w-1/3"
                        v-for="post in posts" 
                        :key="post.id"
                    ><img :src="'http://localhost:3000/uploads/' + post.image"></div>
                </div>    
            </div>
        </v-ons-page>
    </template>
    
    <script lang="ts">
    import Vue from 'vue'
    import * as superagent from 'superagent'
    
    export default {
        props: {
            userName: {
                type: String
            },
            userID: {
                type: Number
            }
        },
        data() {
            return {
                posts: { type: Array }
            }
        },
        mounted() {
            superagent
                .get('http://localhost:3000/posts/own')
                .query({ userID: this.userID })
                .then((res: superagent.Response) => {
                    // attach the results to the posts in our data
                    // and that's it! Vue will update the DOM because it's binded
                    this.posts = res.body
                }).catch((err: Error) => {
                    alert(err)
                })
        }
    }
    </script>
    
    설명: 뷰를 마운트할 때 Superagent 요청을 실행하라는 의미일 뿐입니다.

    Tip: For some very odd reasons, OnsenUI needs all your content to be wrapped in <div class="content">, if not things will start to behave funny.

    Tip: Notice that we wrap the posts with <div v-if="posts.length > 0">. This is to prevent Vue from rendering the DOMs which requires data but the API call has not yet completed. If you don't do that, nothing will break, just that you will see some pesky console log errors telling you that an image url is broken, for example.


    간단하게 보기 위해 게시물을 올릴 때 보기를 업데이트하는 트리거를 건너뛰겠습니다.지금 너는 반드시 전체 응용 프로그램을 새로 고쳐야 한다.
    너는 반드시 보아야 한다.


    우리는 홈페이지를 위해 유사한 일을 할 것이다.vue, #created () 를 사용하면 조금 일찍 호출됩니다.


    <template>
        <v-ons-page>
            <div class="content">
                <div v-if="posts.length > 0">
                    <v-ons-card v-for="post in posts" :key="post.id">
                        <img class="w-full" :src="'http://localhost:3000/uploads/' + post.image">
                        <div class="py-1 content">
                            <p class="text-xs font-bold py-2">{{ post.User.name }}<p>
                            <p class="text-xs text-gray-700">{{ post.caption }}</p>
    
                        </div>
                    </v-ons-card>
                </div>
            </div>
        </v-ons-page>
    </template>
    
    <script lang="ts">
    import Vue from 'vue'
    import * as superagent from 'superagent'
    
    export default {
        props: {
            userID: {
                type: Number
            }
        },
        data() {
            return {
                posts: { type: Array }
            }
        },
        created() {
            superagent
                .get('http://localhost:3000/posts/feed')
                .query({ userID: this.userID })
                .then((res: superagent.Response) => {
                    this.posts = res.body
                }).catch((err: Error) => {
                    alert(err)
                })
        }
    }
    </script>
    

    그리고 저희 "/posts/feed" 내부 노선/댓글.ts API:


    router.get('/feed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
    
        not('parseable-string', req.query.userID)  
    
        // user's feed is not his/her own posts
        DB.Post.findAll({
            where: {
                User_userID: {
                    // this is a Sequelize operator
                    // ne means not equal
                    // so this means from all post that
                    // doesn't belong to this user.
                    [DB.Sequelize.Op.ne]: req.query.userID
                }
            },
            // we want to include the User model for the name
            include: [ DB.User],
            order: [ ['id', 'DESC'] ] // order post by id in descending, so the latest will be first.
        }).then((posts:any) => {
            res.send(posts)
        }).catch((err:Error) => {
            next(err)
        })
    })
    
    그러나 이렇게 하면 응용 프로그램이 userID API를 보내지 않았다는 것을 알게 될 것이다.우리가 userID 아이템을 homepage.vue에 전달하지 않았기 때문이다.편집home.vue을 통해 이 문제를 해결할 수 있습니다.
    icon: 'fa-home',
    label: 'Home',
    page: homePage,
    key: "homePage",
    props: {
        userID: {
            type: Number // add the userID prop to homePage
        }
    }
    

    Tip: You will realise that your app quickly outgrows Vue's basic mechanism of passing data around via props and event emitters. This is why you almost always need Vuex for state management, to store data accessible by the whole app in one place.


    그것은 반드시 작용할 것이다.


    여기 있다!아주 대략적인 인스타그램.
    완료된 이 프로그램을 git repo로 복제하고 사용할 수 있습니다.
    git clone https://github.com/calvintwr/basicgram.git
    

    좋은 웹페이지 즐겨찾기