+2

Tạo một ứng dụng giống instagram với Node.js, React, Redux - phần 2

Phần trước các bạn xem tại Tạo một ứng dụng giống instagram với Node.js, React, Redux - phần 1

Add.jsx

Component cuối cùng quan tâm đến chế độ xem tải lên hình ảnh mới và đặt bộ lọc. Sao chép và dán đoạn mã sau vào src/components/add.jsx:

import React from 'react';
import { connect } from 'react-redux';
import Spinner from './spinner';
import {
    uploadImage,
    postImage,
    setFilters
} from '../action-creators';
 
export class Add extends React.Component {
 
    createFilterUrl() {
        const filterOptions = document.getElementsByClassName('filter-options');
        let selectedFilters = [];
        for(let i = 0; i < filterOptions.length; i++) {
            if(filterOptions[i].checked) {
                selectedFilters.push(filterOptions[i].value);
            }
        }
        this.props.setFilters(selectedFilters.join('/') + '/');
    }
    getImageUrl() {
        return this.props.image ? `https://process.filestackapi.com/${this.props.filter}${this.props.image}` : ``;
    }
 
    render() {
        return (
            <div>
                {this.props.isLoading ?
                    <Spinner /> :
                    <div className="row">
                        <div className="col-md-offset-2 col-md-8">
                            <div className="panel panel-default">
                                <div className="panel-heading">
                                    <h2 className="panel-title text-center">
                                        <span className="glyphicon glyphicon-upload" /> Upload an Image
                                    </h2>
                                </div>
                                <div className="panel-body">
                                    <form name="product-form" id="product-form" noValidate>
                                        <div className="form-group">
                                            <label >Filters</label>
                                            <div className="checkbox-group" onClick={() => this.createFilterUrl()}>
                                              <div className="checkbox"><label><input type="checkbox" className="filter-options" value="sharpen" />Sharpen</label></div>
                                              <div className="checkbox"><label><input type="checkbox" className="filter-options" value="blur" />Blur</label></div>
                                              <div className="checkbox"><label><input type="checkbox" className="filter-options" value="blackwhite" />Black & White</label></div>
                                              <div className="checkbox"><label><input type="checkbox" className="filter-options" value="sepia" />Sepia</label></div>
                                              <div className="checkbox"><label><input type="checkbox" className="filter-options" value="pixelate" />Pixelate</label></div>
                                              <div className="checkbox"><label><input type="checkbox" className="filter-options" value="oil_paint" />Oil Paint</label></div>
                                              <div className="checkbox"><label><input type="checkbox" className="filter-options" value="negative" />Negative</label></div>
                                              <div className="checkbox"><label><input type="checkbox" className="filter-options" value="modulate" />Modulate</label></div>
                                            </div>
                                        </div>
                                        <div className="form-group ">
                                  <label htmlFor="picture">Picture</label>
                                  <div className="text-center dropup">
                                    <button id="button-upload" type="button" className="btn btn-default filepicker" onClick={() => this.props.uploadImage()}>
                                      Upload <span className="caret" />
                                    </button>
                                  </div>
                                </div>
                                <div className="form-group text-center">
                                            <img className="img-responsive" src={this.getImageUrl()}></img>
                                        </div>
                                        <button type="button" className="btn btn-filestack btn-block" onClick={() => this.props.postImage()}>Submit</button>
                                    </form>
                                </div>
                            </div>
                        </div>
                    </div>
                }
            </div>
        );
    }
}
 
function mapStateToProps(state) {
  return {
    image: state.get('upload').get('handle'),
    filter: state.get('upload').get('filters'),
    isLoading: state.getIn(['view', 'isLoading'])
  };
}
 
function mapDispatchToProps(dispatch) {
    return {
        uploadImage: () => dispatch(uploadImage()),
        setFilters: filters => dispatch(setFilters(filters)),
        postImage: () => dispatch(postImage())
    }
}
export const AddContainer = connect(mapStateToProps, mapDispatchToProps)(Add);

Component Add có một vài function để lọc và cung cấp cái nhìn cho picture. createFilterUrl lấy các bộ lọc đã được kiểm tra và gửi chúng qua action để thêm chúng vào state. Để làm như vậy, đầu tiên nó tạo ra một chuỗi nơi mỗi bộ lọc được tách bằng một "/" làm cho nó dễ dàng hơn để soạn Filestack url.

URL Filestack yêu cầu áp dụng bộ lọc cho hình ảnh được hiển thị trong hàm getImageUrl . Chúng ta cần phải gọi https://process.filestackapi.com/ sau đó là danh sách các bộ lọc được phân tách bằng "/" và cuối cùng là xử lý tập tin, id cụ thể cho hình ảnh, cái mà chúng ta sẽ lưu khi hình ảnh được tải lên Filestack.

Lưu ý: Tôi không sử dụng tất cả các bộ lọc Filestack cung cấp, tôi chỉ giới hạn bản thân mình với những cái cho "cảm giác Instagram", đừng ngần ngại đọc tài liệu của bộ lọc để thử tất cả!

Chú ý chức năng mapDispatchToProps cho phép gửi các hành động tới các redux và sagas. Cụ thể, thuộc tính đối tượng thứ nhất và thứ ba cho phép component này gửi các hành động đến redux-saga trong khi setFilters thứ hai cho biết redux cập nhật trạng thái với các bộ lọc ưa thích của người dùng.

Tuyệt vời, chúng ta đã hoàn thành các component và chúng ta đã thấy rằng họ gửi hành động hướng đến redux và sagas, tuy nhiên chúng ta đã không nói về redux nào được nêu ra. Hãy di chuyển đến các action!

Action-creators.js

The file containing the actions is in the /components folder, you may have noticed that in all the components requiring actions we always import it, it’s action-creators.js. Tệp có chứa các hành động nằm trong thư mục /components, bạn có thể nhận thấy rằng trong tất cả các component đòi hỏi hành động chúng ta luôn nhập nó, nó là action-creators.js.

Mở và paste đoạn code sau:

export function getImages() {
    return {
        type: 'GET_IMAGES'
    }
}
 
export function uploadImage() {
    return {
        type: 'UPLOAD_IMAGE'
    }
}
 
export function postImage() {
    return {
        type: 'POST_IMAGE'
    }
}
 
export function setFilters(filters) {
    return {
        type: 'SET_FILTERS',
        payload: filters
    }
}

We have 4 functions which dispatch an action with a specific type to both our reducer and redux-saga watchers. The majority of these actions triggers some async behavior, for example fetching from the server, post a new image or upload a picture through Filestack. So we can expect these actions to be intercepted by the redux-saga watchers. However, the getImage function returns an action both reducer and redux-saga intercept, the reducer will change the isLoading value in the state to true. Changing the state, as you should know, re-render the components and shows the spinner in the page!

So let’s take a look at the reducer. Chúng tôi có 4 chức năng cho một hành động với một loại cụ thể để theo dõi cả hai reducer và redux-saga. Phần lớn các hành động này gây ra một số hành vi không đồng bộ, ví dụ lấy từ máy chủ, đăng hình ảnh mới hoặc tải lên một hình ảnh thông qua Filestack. Vì vậy, chúng ta có thể mong đợi những hành động này sẽ được chặn bởi các nhà quan sát redux-saga. Tuy nhiên, hàm getImage trả về một action reducer và redux-saga, trình giảm tốc sẽ thay đổi giá trị isLoading trong state thành true. Thay đổi state, như bạn đã biết, render lại các component và hiển thị spinner trong trang!

Vì vậy, chúng ta hãy xem xét các reducer.

Reducer.js

The reducer, for the purporse of the tutorial is just one but it could be splitted in many others and then combine them thanks to combineReducers function. Open /src/reducer.js and paste the following code: Reducer, chỉ dành cho các hướng dẫn chỉ là một, nhưng nó có thể được splitted ở nhiều người khác và sau đó kết hợp chúng nhờ chức năng combineReducers. Mở /src/reducer.js và dán đoạn mã sau:

import {
    Map,
    List 
} from 'immutable';
 
const INITIAL_STATE = Map({
    imageList: List([]),
    view: Map({
        isLoading: false
    }),
    upload: Map({
        handle: '',
        filters: '',
    })
});
 
export default function (state = INITIAL_STATE, action) {
    switch(action.type) {
        case 'GET_IMAGES':
                return state.merge({
                    view: {
                        isLoading: true
                    }
                });
        case 'GET_IMAGES_SUCCESS':
            return state.merge({
                imageList: action.payload,
                view: {
                    isLoading: false
                }
            });
        case 'UPLOAD_IMAGE_SUCCESS':
            return state.updateIn(
                ['upload', 'handle'],
                '',
                handle => action.payload
            );
        case 'POST_IMAGE':
            return state.merge({
                view: {
                    isLoading: true
                }
            });
        case 'POST_IMAGE_SUCCESS':
            return state.merge({
                upload: {
                    handle: '',
                    filters: ''
                },
                view: {
                    isLoading: false
                }
            });
        case 'SET_FILTERS':
            return state.updateIn(
                ['upload', 'filters'],
                '',
                filter => action.payload
            );
        default: return state;
    }
}

We are using immutable.js for the state, that makes sure our state is an immutable data-structure. The initial state helps to picture the state structure and at the beginning the imageList is nothing but an empty array, isLoading is set to false and the Filestack handle and filters are all empty values. Now, notice all the actions.type terminating with _SUCCESS. These actions are triggered from redux-sagas to tell the reducer to update the state. For instance, once the client received all the images from the server the generator function is going to put an action with type GET_IMAGES_SUCCESS and payload the array of images. The reducer intercepts the action and return a new state now containing the images and isLoading set to false.

To conclude, let’s take a look at the function generators. Chúng tôi đang sử dụng immutable.js cho state, đảm bảo rằng state của chúng ta là một cấu trúc dữ liệu không thay đổi. State ban đầu giúp hình dung cấu trúc state và khi bắt đầu imageList chỉ là một mảng trống, isLoading được đặt thành false, và handle Filestack và các bộ lọc đều là các giá trị rỗng. Bây giờ, lưu ý tất cả các action.type chấm dứt với _SUCCESS. Những action này được kích hoạt từ redux-sagas để nói với các reducer để cập nhật các state. Ví dụ, một khi khách hàng nhận được tất cả các hình ảnh từ máy chủ, chức năng sẽ đưa ra một hành động với loại GET_IMAGES_SUCCESS và tải các mảng của hình ảnh. Reducer chặn action và trả lại state mới hiện có chứa các hình ảnh và được đặt isLoading thành false.

Để kết luận, chúng ta hãy xem các bộ functions.

Sagas.js

Mở file src/sagas.js và copy và paste đoạn code sau:

import {
    takeLatest,
    delay
} from 'redux-saga';
 
import {
    put,
    select
} from 'redux-saga/effects';
 
const FILESTACK_URL = 'https://process.filestackapi.com/';
 
const getFromServer = () => {
    return fetch('/image')
        .then(response => response.json());
}
 
const postToServer = url => {
  return fetch('/image', {
    headers: new Headers({
      'Content-Type': 'application/json'
    }),
    method: 'POST',
    body: JSON.stringify(url)
  })
  .then(response => response.json());
}
 
const pick = () => {
   return new Promise(function (resolve, reject) {
    filepicker.pick (
      {
        mimetype: 'image/*',
        container: 'modal',
        services: ['COMPUTER', 'FACEBOOK', 'INSTAGRAM', 'URL', 'IMGUR', 'PICASA'],
        openTo: 'COMPUTER'
      },
      function (Blob) {
        console.log(JSON.stringify(Blob));
        const handler = Blob.url.substring(Blob.url.lastIndexOf('/') + 1);
        resolve(handler);
      },
      function (FPError) {
        console.log(FPError.toString());
        reject(FPError.toString());
      }
    );
  });
}
 
function* loadImages () {
  try {
    yield delay(1000);
    const imageList = yield getFromServer();
    yield put({ type: 'GET_IMAGES_SUCCESS', payload: imageList });
  } catch (error) {
    yield put({ type: 'GET_IMAGES_FAILURE' });
  }
}
 
function* postImage () {
  try {
    yield delay(1000);
    const state = yield select();
    const url = FILESTACK_URL + state.get('upload').get('filters') + state.get('upload').get('handle');
    const result = yield postToServer({ url });
    yield put({ type: 'POST_IMAGE_SUCCESS' });
  } catch (error) {
    yield put({ type: 'POST_IMAGE_FAILURE' });
  }
}
 
function* uploadImage () {
  try {
    const upload = yield pick();
    yield put({ type: 'UPLOAD_IMAGE_SUCCESS', payload: upload });
  } catch (error) {
    yield put({ type: 'UPLOAD_IMAGE_FAILURE' });
  }
}
 
function* watchGetImages () {
  yield takeLatest('GET_IMAGES', loadImages);
}
 
function* watchPostImage () {
  yield takeLatest('POST_IMAGE', postImage);
}
 
function* watchFilestack () {
  yield takeLatest('UPLOAD_IMAGE', uploadImage);
}
 
export default function* rootSaga () {
  yield [
    watchGetImages(),
    watchPostImage(),
    watchFilestack()
  ]
}

Có 3 functions lắng nghe các actions: watchGetImages, watchPostImagewatchFilestack sẽ thực hiện hành động mới nhất với kiểu định nghĩa bên trong mỗi takeLatest function.

Chúng ta hãy xem watchFilestack. Bất cứ khi nào người dùng muốn tải lên hình ảnh và nhấp vào nút tải lên trong component Add, một action UPLOAD_IMAGE được gửi và bị chặn bởi watchFilestack sẽ gửi nội dung lên uploadImage. uploadImage, như là một chức năng làm việc khá tốt với promises, đó là lý do tại sao trong dòng

const upload = yield pick();

Sẽ thấy một lời hứa trả về bởi chức năng pick(chọn). Pick có trách nhiệm giao tiếp với Filestack, nhờ vào chức năng của filepicker chúng ta có thể định nghĩa một số tùy chọn để tải lên, như thường lệ tôi đề nghị bạn kiểm tra tài liệu hướng dẫn để có cái nhìn tổng quan về khả năng vô tận để tùy chỉnh hành vi.

  • Trong trường hợp của chúng ta, tôi đã xác định các quy tắc sau: Mimetype bằng image/* để giới hạn tải lên các file hình ảnh.
  • Giao diện filestack là một phương thức được xác định thông qua vùng chứa thuộc tính.
  • Filestack cho phép người dùng tải lên hình ảnh từ nhiều nguồn, trong trường hợp của chúng ta, mảng ['COMPUTER', 'FACEBOOK', 'INSTAGRAM', 'URL', 'IMGUR', 'PICASA'] giới hạn các nguồn cho 6 lựa chọn này.
  • Cuối cùng, openTo cho thấy việc tải lên từ thiết bị vật lý là lựa chọn đầu tiên.

Bạn thực sự có thể thấy chúng ta dễ dàng thiết lập một người tải lên mà không cần quan tâm đến sự an toàn của nó, đó là điều mà Filestack sẽ làm cho chúng ta!

Cuối cùng, hàm filepicker.pick có các chức năng onSuccessonError để giải quyết hoặc từ chối promise. Filestack trả lại một đối tượng Blob trong mã tôi hiển thị trong bảng điều khiển. Đây là một ví dụ của nó

 
{
"url":"https://cdn.filestackcontent.com/o3NSspSQ1yVqiHYN3i1C",
"filename":"35828743-wallpapers-space.jpg",
"mimetype":"image/jpeg",
"Size":778842,
"Cropped":{
    "originalImageSize":[1920,1200],
    "cropArea":{
        "position":[0,0],
        "size":[1920,1200]
    }
},
"Converted":true,
"Id":1,
"Client":"computer",
"isWriteable":true
}
 

Url trả về một liên kết tới CDN trong khi o3NSspSQ1yVqiHYN3i1C là xử lý hình ảnh cụ thể, một trong những thứ chúng ta cần để xử lý hình ảnh và thêm các bộ lọc nhờ Filestack API.

Hướng dẫn đã hoàn tất! Hãy chạy:

npm run server

Trong trình duyệt chạy localhost:8080, bạn sẽ có thể sử dụng ứng dụng. Chúc mừng bạn đã hoàn thành hướng dẫn!

Redux dev-tools

Như đã nói, chúng ta đã kết nối ứng dụng của chúng ta với tiện ích mở rộng trình duyệt này. Nếu bạn chạy ứng dụng trong trang chủ chỉ cần mở công cụ dành cho nhà phát triển và chọn tab redux:

Ở bên trái, bạn có thể thấy các hành động được kích hoạt trong khi ở bên phải trạng thái hiện tại. Ngoài ra, nếu bạn bấm vào tab diff, bạn có thể thấy những thay đổi trong state khi một hành động được kích hoạt. Đó là một công cụ gỡ lỗi tuyệt vời bởi vì bạn thậm chí có thể nhấp vào một hành động và bỏ qua nó và state sẽ thay đổi cho phù hợp.

Kết luận

Trong hướng dẫn, chúng ta đã thực hành với Node.js, React và Redux để tạo một ứng dụng Instagram-alike cho người dùng tải lên và chia sẻ hình ảnh của chính họ. Ngoài người dùng này, bạn có thể chọn một số bộ lọc để làm đẹp hình ảnh của họ trước khi chia sẻ chúng trên web. Chúng ta bao gồm Filestack vì nó chứng tỏ rất tốt để quản lý các tập tin. Không cần phải quan tâm đến bảo mật và lưu trữ các tệp tải lên của chúng ta vì nó cho chúng ta. Nó cũng đi kèm với một API phong phú cho phép lọc và chuyển đổi hình ảnh, cũng như các hoạt động khác trên các loại tệp khác nhau.

Tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí