Tạo một ứng dụng giống instagram với Node.js, React, Redux - phần 1
Hãy xem chúng ta dễ dàng kết hợp Node.js và React để xây dựng một ứng dụng web trong khi xử lý các hình ảnh tải lên với Filestack. Vì vậy, không có nghi ngờ Javascript hiện là ngôn ngữ hot nhất trong cộng đồng phát triển web. Các packages được cập nhật hàng ngày và những packages mới được ...
Hãy xem chúng ta dễ dàng kết hợp Node.js và React để xây dựng một ứng dụng web trong khi xử lý các hình ảnh tải lên với Filestack.
Vì vậy, không có nghi ngờ Javascript hiện là ngôn ngữ hot nhất trong cộng đồng phát triển web. Các packages được cập nhật hàng ngày và những packages mới được đưa ra, chắc chắn là không dễ để cập nhật! Nghĩ về Angular, React và bây giờ Angular 2, không phải để nói về Node.js đã có trong phiên bản 7. Javascript, cú pháp của nó, thư viện và các frameworks đang phát triển với tốc độ ánh sáng. Đó là lý do tại sao hướng dẫn này đi theo hướng này vì chúng ta sẽ viết một ứng dụng Instagram sử dụng Javascript với Node.js và React / Redux.
Làm thế nào nó hoạt động? Vâng, Node.js sẽ phụ trách cho máy chủ API và hiển thị trang đầu tiên trong khi React đảm nhận font-end với Redux xử lý trạng thái. Để làm cho chúng giao tiếp với nhau, tức là tạo ra các yêu cầu HTTP, chúng ta sẽ sử dụng Redux-saga.
Đừng lo lắng về điều này, đầu đề làm nó có vẻ khó khăn hơn, chỉ cần làm theo hướng dẫn này và bạn sẽ có thể nhận được thành quả của tất cả các công nghệ này!
Bạn cũng có thể tham khảo ứng dụng trong github của tôi.
Filestack
Để tải lên hình ảnh và áp dụng bộ lọc, chúng tôi sẽ tận dụng Filestack.
Filestack không chỉ là một dịch vụ để tải lên các tệp của bạn mà còn cung cấp một số API để áp dụng chuyển đổi hình ảnh, chuyển mã âm thanh và video, ... hãy xem tài liệu hướng dẫn chi tiết. Ngoài ra, nó rất dễ dàng để tích hợp vào bất kỳ ứng dụng nào trong khi nó cung cấp tùy biến hoàn chỉnh của người tải lên của nó.
Tuy nhiên, theo tôi, điều tuyệt nhất là bằng cách sử dụng các nhà phát triển dịch vụ Filestack không cần phải bảo mật file tải lên và lưu trữ dữ liệu nữa, đây là điều mà Filestack sẽ làm. Không có rắc rối!
Do đó, trong hướng dẫn này, tôi sẽ chỉ cho bạn cách thiết lập một tài khoản miễn phí, lấy chìa khóa API và bắt đầu sử dụng nó trong ứng dụng Instagram của chúng tôi.
Prerequisites
Đây không phải là nghĩa vụ phải là một hướng dẫn cơ bản về React hoặc Node.js vì vậy tôi giả định rằng tất cả các bạn đều biết những điều cơ bản của hai công nghệ này. Hơn nữa, tôi sẽ viết Javascript ES6 và transpile nó với Babel ... Sau khi tất cả thời điểm này là cuối năm 2016!
Về Redux, tôi cũng giả sử bạn biết về nó và tại sao tôi để cho nó xử lý các state của ứng dụng.
Lưu ý: Tôi sẽ không cover phần css, chỉ cần nhấp vào liên kết git ở trên và sao chép từ /dist/css.
Thiết lập project
Điều đầu tiên chúng ta sẽ làm là thiết lập dự án, hãy để tôi chỉ cho bạn cách tôi tổ chức thư mục dự án:
Cấu trúc thư mục:
├── src/ │ ├── components/ │ │ ├── add.jsx │ │ ├── detail.jsx │ │ ├── home.jsx │ │ ├── image-container.jsx │ │ ├── image.jsx │ │ ├── layout.jsx │ │ ├── profile-header.jsx │ │ └── spinner.jsx │ │ │ ├── action-creators.jsx │ ├── reducer.jsx │ └── sagas.jsx │ ├── dist │ ├── css │ │ └── style.css │ │ │ ├── Index.html │ └── bundle.js │ ├── .babelrc ├── server.js ├── package.json ├── webpack.config.js └── webpack-loaders.js
Về cơ bản, tất cả logic và giao diện kinh doanh phía trước đều được xử lý bởi React và Redux trong thư mục /src. Thư mục /dist chứa các tệp tin tĩnh và gói bundle.js được tạo bởi webpack trong khi máy chủ chính nó nằm bên trong tệp server.js.
Cài đặt các gói
Hãy bắt đầu tạo tệp package.json bằng cách gõ vào thư mục gốc của dự án.
npm init -y
-y giúp chúng ta định nghĩa cấu trúc cơ bản của file ở đâu. Bây giờ, hãy mở file package.json và thay đổi thuộc tính main thành server.js
"main": "server.js"
Thêm command để chạy server
"scripts": { "server": "webpack && babel-node server.js" }
Như đó là tên tệp cho máy chủ node.js của chúng ta! Tại thời điểm này chúng ta có thể cài đặt các gói của server
npm install --save express body-parser morgan
express là một web framework cho Node.js, chúng ta sử dụng để viết phần server, body-parser giúp chúng ta lấy dữ liệu gọi qua phương thức HTTP và morgan giúp ta thấy log trong phần console.
Bây giờ chúng ta hãy cài đặt React, Redux và tất cả các gói liên quan
npm install --save react react-dom react-redux react-router redux redux-saga
Mặc dù phần lớn các gói này không cần bất kỳ lời giải thích nào tôi muốn nói thêm về redux-saga nó cung cấp các thao tác để xử lý tất cả các hiệu ứng phụ (ví dụ như các cuộc gọi async đến máy chủ) được gọi là hiệu ứng. Redux-saga sử dụng rộng rãi các chức năng cho phép các nhà phát triển viết mã async giống như nó đồng bộ. Như tôi đã nói trước khi chúng tôi sử dụng Webpack để tạo ra bó cho ứng dụng của chúng tôi, chúng ta hãy tiếp tục và chạy
npm install --save-dev webpack webpack-dev-server webpack-merge webpack-validator style-loader css-loader react-hot-loader
Gói của chúng tôi cũng bao gồm tệp css, đó là lý do tại sao chúng tôi đang cài đặt css-loader và style-loader. Cuối cùng, chúng ta sẽ viết javascript ES6 nhưng một số trình duyệt không hỗ trợ nó để Babel tiện dụng như transpiler:
npm install --save-dev babel-cli babel-core babel-loader babel-polyfill babel-preset-es2015 babel-preset-react
Hãy sửa package.json và thêm đoạn sau:
"babel": { "presets": [ "es2015" ] }
Cuối cùng, tạo một tệp có tên .babelrc trong thư mục gốc và dán mã sau
{ "presets": [ "es2015", "react" ], "plugins": ["react-hot-loader/babel"] }
Lưu ý: react-hot-loader và webpack-dev-server được sử dụng ban đầu để giúp phát triển phục vụ các trang web của ứng dụng cũng như xây dựng lại khi có sự thay đổi mã code. Trong hướng dẫn chúng tôi sẽ không sử dụng chúng tuy nhiên tôi đã để lại cấu hình một tham chiếu để thiết lập các dự án trong tương lai.
Webpack
Để viết ứng dụng này lần đầu tiên tôi đã viết phần server và client riêng nên tôi đã sử dụng webpack-dev-server để phục vụ client. Tuy nhiên, mặc dù ngay bây giờ chúng tôi không cần cấu hình này nhưng tôi để lại mã gốc để hoàn thành.
Bên trong webpack.config.js dán đoạn mã sau:
const path = require('path'); const merge = require('webpack-merge'); const validate = require('webpack-validator'); const parts = require('./webpack-loaders'); const PATHS = { src: path.join(__dirname, 'src'), dist: path.join(__dirname, 'dist'), css: path.join(__dirname, 'dist/css') }; const common = { entry: { app: PATHS.src }, output: { path: PATHS.dist, filename: 'bundle.js' }, module: { loaders: [{ test: /.jsx?$/, exclude: /node_modules/, loader: 'babel' }] }, resolve: { extensions: [', '.js', '.jsx'] } }; let config; switch(process.env.npm_lifecycle_event) { case 'server': config = merge( common, { devtool: 'source-map' }, parts.setupCSS(PATHS.css) ); break; default: config = merge( common, { devtool: 'eval-source-map' }, parts.setupCSS(PATHS.css), parts.devServer({ host: process.env.host, port: 3000 }) ); } module.exports = validate(config);
Như bạn nhận thấy tôi đã sử dụng webpack-merge để chuyển đổi giữa các cấu hình nhưng trong hướng dẫn tôi chỉ bao gồm một máy chủ mà chạy khi chúng tôi gọi npm run server. Điều cuối cùng cần làm là dán mã sau vào webpack-loaders.js:
const webpack = require('webpack'); exports.devServer = function(options) { return { devServer:{ historyApiFallback: true, hot: true, inline: true, stats: 'errors-only', host: options.host, port: options.port, contentBase: './dist', }, plugins: [ new webpack.HotModuleReplacementPlugin({ multistep: true }) ] }; } exports.setupCSS = function(paths) { return { module: { loaders: [ { test: /.css$/, loaders: ['style', 'css'], include: paths } ] } }; }
Tôi sẽ không dành thời gian giải thích cả hai cấu hình nhưng đối với bất cứ ai quan tâm đến đào sâu vào tôi đề nghị đi đến survival.js như tác giả đã viết một cuốn sách điện tử rất hay về webpack tôi đọc trước đây.
Time to code!
The Server
Hãy bắt đầu viết mã cho phần server. Trong server.js copy và dán mã sau đây:
import express from 'express'; import morgan from 'morgan'; import bodyParser from 'body-parser'; const app = express(); const port = process.env.PORT || 8080; // Log with Morgan app.use(morgan('dev')); // Accept encoded data as well as json format app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // Static files app.use(express.static(__dirname + '/dist')); const imageList = [ { key: 0, url: "https://process.filestackapi.com/sharpen/negative/sb5RRdoQiiy5l5JUglB1" }, { key: 1, url: "https://process.filestackapi.com/sharpen/oil_paint/urjTyRrAQA6sUzK2qIsd" }, { key: 2, url: "https://process.filestackapi.com/sepia/modulate/wxYyL4yQyyRH1RQLZ6gL" }, { key: 3, url: "https://process.filestackapi.com/blur/pixelate/O9vo0AynTNaNZlRyRBUm" }, { key: 4, url: "https://process.filestackapi.com/blackwhite/kcirovLQC2eJmA6pkrMD" }, { key: 5, url: "https://process.filestackapi.com/sharpen/modulate/5V2ZH22ZTWGXv2lMvvVT" } ]; app.route('/image') .get((req, res) => res.json(imageList)) .post((req, res) => { const { url } = req.body; imageList.push({ key: imageList.length, url }); res.json({ success: 1, message:'Image Successfully added!' }); }); app.listen(port); console.log(`listening on port ${port}`);
Đối với bất kỳ ai quen thuộc với node.js, mã này khá đơn giản. Chúng tôi đã nhập tất cả các gói máy chủ mà chúng tôi đã cài đặt trước đó và thêm chúng vào máy chủ. imageList là một mảng các hình ảnh để hiển thị tại trang chủ ứng dụng. Có hai routes, một GET và POST /image phụ trách để trả lại imageList cho khách hàng cũng như thêm một hình ảnh mới vào mảng.
Thật tuyệt với vì trong vài dòng code chúng ta đã làm xong phần server.
Trước khi chuyển đến máy khách, chúng tôi thực sự có thể chạy server và kiểm tra các routes. Trong trường hợp của tôi tôi sử dụng Postman bởi vì nó rất dễ sử dụng nhưng bất kỳ khách hàng khác là tốt.
Trong terminal chạy lệnh
npm run server
Để bắt đầu server và bây giờ bạn có thể truy vấn nó! Vì vậy, đây là kết quả của tôi sau khi GET và POST đến localhost:8080/image:
Vì vậy, bây giờ chúng ta có thể làm một yêu cầu GET và kiểm tra xem hình ảnh giả mạo mới đã được thêm vào hay không: Đó là tuyệt vời nó hoạt động
The client
Trước khi đào sâu vào mã, hãy tạo một tài khoản miễn phí trên Filestack bằng cách nhấp vào nút dùng thử miễn phí trên trang web. Sau đó chỉ nhập thông tin của bạn vào biểu mẫu (không có gì đặc biệt).
Và bây giờ trong cổng nhà phát triển bạn sẽ nhận được đoạn trích của họ ngay lập tức! Đó là rất tốt đẹp, nhưng chúng tôi không cần nó, trên thực tế chúng tôi sẽ viết lên tải lên của riêng của chúng tôi. Vì vậy, chỉ cần lưu API key vì chúng tôi sẽ thêm nó vào client rất sớm.
Ok, bây giờ chúng ta hãy bắt đầu viết cho client!
Trước tiên, hãy thêm mã vào /dist/index.html, do đó hãy sao chép và dán mã sau:
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="awidth=device-awidth, initial-scale=1"> <meta name="description" content="Instagram alike app"> <meta name="author" content="Sam Zaza"> <title>Instastack</title> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div id="app"></div> <script src="//api.filestackapi.com/filestack.js"></script> <script src="./bundle.js"></script> </body> </html>
Điều đó rất đơn giản, đó là mã ban đầu được gửi bởi máy chủ của chúng tôi. Lưu ý rằng chúng ta đã xác định thuộc tính id của chúng ta tới div nơi mà các quan điểm của chúng ta sẽ hiển thị và chúng ta thêm vào Filestack làm script.
Vì vậy, chúng tôi đã đề cập đến cách nhìn nhưng làm thế nào để ứng dụng của chúng tôi trông như thế? Điều quan trọng là hiển thị ba chế độ xem trước khi bắt đầu viết các thành phần. Trang chủ cho thấy hồ sơ của Han Solo với danh sách tất cả các hình ảnh của anh ấy:
localhost:8080
Bằng cách nhấp vào bất kỳ hình ảnh nào, người dùng được chuyển đến trang chi tiết, nơi hình ảnh được hiển thị trên một kích thước lớn hơn:
Cuối cùng, nút tải lên chuyển hướng đến biểu mẫu mà người dùng có thể tải lên hình ảnh và thêm bộ lọc:
localhost:8080/#/add
Vì vậy, từ giao diện người dùng, chúng ta có thể suy ra một vài điều:
- Trong trang chủ client tạo request GET đến server để liệt kê tất cả các hình ảnh.
- Mặt khác, chế độ xem tải lên là một trong những yêu cầu POST.
- React-router sẽ xử lý 3 lần.
- Về redux, rõ ràng là các action thay đổi trạng thái đều liên quan đến lấy hình ảnh từ server, đăng hình mới và thay đổi bộ lọc.
Trong /src/index.js sao chép và dán đoạn mã sau:
import 'babel-polyfill'; // for redux-saga import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, hashHistory } from 'react-router'; import { createStore, applyMiddleware, compose } from 'redux'; import reducer from './reducer'; import { Provider } from 'react-redux'; import createSagaMiddleware from 'redux-saga' import rootSaga from './sagas' // our components import Layout from './components/layout'; import { HomeContainer } from './components/home'; import { DetailContainer } from './components/detail'; import { AddContainer } from './components/add'; // app css import '../dist/css/style.css'; // Filestack API requires to set a key filepicker.setKey("YOUR_API_KEY"); const sagaMiddleware = createSagaMiddleware(); const store = createStore( reducer, compose( applyMiddleware(sagaMiddleware), window.devToolsExtension ? window.devToolsExtension() : f => f // connect to redux devtools ) ); sagaMiddleware.run(rootSaga); // the 3 paths of the app const routes = <Route component={Layout}> <Route path="/" component={HomeContainer} /> <Route path="/detail/:id" component={DetailContainer} /> <Route path="/add" component={AddContainer} /> </Route>; // add provider as first component and connect the store to it ReactDOM.render( <Provider store={store}> <Router history={hashHistory}>{routes}</Router> </Provider>, document.getElementById('app') );
- Đầu tiên chúng ta đã nhập tất cả các gói và các thành phần chúng ta cần cho react-router.
- Đây cũng là nơi bạn phải nhập khóa API Filestack, vì vậy hãy thay thế YOUR_API_KEY.
- Chúng tôi tạo ra redux store và áp dụng middleware saga cũng như các công cụ dev kết nối redux với mã của chúng tôi.
- Chúng tôi xác định các route và gói route với một thành phần Nhà cung cấp kết nối store đến react.
NB: khi bạn nhận thấy tôi đã kết nối mã của chúng tôi đến DevTools Redux, mà có thể được tải về như phần mở rộng trình duyệt. Điều này rất thuận lợi vì chúng ta có thể xem các action và state ứng dụng vào đúng thời điểm bạn đang lướt ứng dụng. Tôi sẽ nói về nó một lần nữa vào cuối hướng dẫn.
Thư mục /src/components là nơi mà chúng ta phải lưu các thành phần của ứng dụng nên chúng ta hãy bắt đầu viết chúng:
Layout.jsx
Layout.jsx chứa thành phần nội dung cho header và footer. Trong src/index.js bạn có thể đã nhận thấy rằng đó là thành phần chính của chế độ xem của chúng tôi được xác định trong route. Vì vậy, tạo tệp tin và sao chép và dán đoạn mã sau:
import React from 'react'; import { Link } from 'react-router'; export default class Layout extends React.Component { render() { return ( <div> <nav className="navbar navbar-white navbar-fixed-top"> <div className="container p-y-1"> <div className="navbar-header"> <button type="button" className="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar" > <span className="sr-only">Toggle navigation</span> <span className="icon-bar" /> <span className="icon-bar" /> <span className="icon-bar" /> </button> <p className="navbar-brand title"> <Link to="/">Instastack</Link> </p> </div> <div id="navbar" className="collapse navbar-collapse"> <ul className="nav navbar-nav navbar-right"> <li><Link to="/add">Upload</Link></li> <li> <a href="http://blog.filestack.com/working-with-filepicker/create-an-instagram-app-to-upload-and-share-images/" target="_blank">Blog</a> </li> </ul> </div> </div> </nav> <div className="container m-t-3"> {this.props.children} </div> </div> ); } }
Layout hiển thị header, footer của trang và chế độ xem 3 như bạn thấy từ dòng:
{<span class="hljs-keyword">this</span>.props.children}
Đó là khá nhiều cho các thành phần Layout vì vậy chúng ta hãy di chuyển đến 3 children.
Home.jsx
Đó là thành phần kết nối đầu tiên chúng ta thấy trong hướng dẫn. Kết nối nghĩa là gì? Vâng, nó có nghĩa là chúng tôi có thể nhận được thông tin state bên trong props của nó cũng như gửi action. Hãy kiểm tra mã, sao chép và dán đoạn mã sau:
import React from 'react'; import { connect } from 'react-redux'; import ProfileHeader from './profile-header'; import ImageContainer from './image-container'; import Spinner from './spinner'; import { getImages } from '../action-creators'; export class Home extends React.Component { componentDidMount() { this.props.getImages(); } render() { return ( <div> <ProfileHeader /> {this.props.isLoading ? <Spinner /> : <ImageContainer imageList={this.props.imageList} /> } </div> ); } } function mapStateToProps(state) { return { imageList: state.get('imageList').toJS(), isLoading: state.getIn(['view', 'isLoading']) }; } function mapDispatchToProps(dispatch) { return { getImages: () => dispatch(getImages()) } } export const HomeContainer = connect(mapStateToProps, mapDispatchToProps)(Home);
Vì vậy, chúng tôi exporting các HomeContainer đó là kết nối phiên bản của Home component. Từ state chúng tôi nhận được imageList và boolean isLoading bởi vì chúng tôi muốn hiển thị một máy quay tốt đẹp trong trường hợp chúng tôi đang lấy từ máy chủ! Nếu bạn xem một jsx đã trả về bên trong hàm render, bạn có thể nhận ra logic mà tôi vừa mô tả. Bên cạnh đó, chúng tôi gặp phải ba thành phần khác là ProfileHeader, Spinner và ImageContainer.
ProfileHeader.jsx
Đây là một component đơn giản thể hiện các hình ảnh và thông tin cá nhân của người sử dụng ở trên cùng của trang. Sao chép và dán đoạn mã sau vào /src/components/profile-header.jsx:
import React from 'react'; export default class ProfileHeader extends React.Component { render() { return ( <div className="row user-header p-y-2"> <div className="col-md-offset-2 col-md-8 p-y-4"> <div className="media"> <div className="media-left"> <a href="#"> <img className="media-object" src="https://process.filestackapi.com/crop_faces=mode:fill/rounded_corners=radius:max/Qc3KntVRMCp55EJTAvyg" alt="profile-pic" /> </a> </div> <div className="media-body p-l-6"> <h2 className="media-heading m-t-15">Han.Solo</h2> <h4><strong>Han Solo</strong> Smuggler & Gambler</h4> <ul className="header-ul"> <li><strong>50</strong> posts</li> <li><strong>300k</strong> followers</li> <li><strong>180</strong> following</li> </ul> </div> </div> </div> </div> ); } }
Spinner.jsx
Điều đó rất dễ, chỉ cần vài dòng mã để hiển thị gif động. Lưu ý: spinner này sẽ được sử dụng trong suốt hướng dẫn ở mỗi trang.
import React from 'react'; export default class Spinner extends React.Component { render() { return( <div className="row"> <div className="col-md-12 m-t-3"> <img className="img-responsive center-block" src="https://cdn.filestackcontent.com/WANCBKNFS2i9DZremSFd" /> </div> </div> ); } }
Image-container.jsx
Image-container nhận imageList từ Home của nó và gọi phương thức map của imageList để tạo image component. Sao chép và dán đoạn mã sau vào /components/image-container.jsx:
import React from 'react'; import Image from './image'; export default class ImageContainer extends React.Component { render() { const { imageList } = this.props; return ( <div className="row"> {imageList.map(image => <Image id={image.key} {...image} />)} </div> ); } }
Và đó là tất cả cho chế độ xem trang chủ! Bây giờ, chúng ta hãy kiểm tra trang chi tiết mà là khá dễ dàng.
Detail.jsx
Detail.jsx export một component được kết nối, DetailContainer, và nó cũng có thể làm cho spinner. Tại sao vậy?
Giả sử người dùng bắt đầu lướt từ chi tiết của một trong những hình ảnh thay vì bắt đầu từ trang chủ, điều này có nghĩa là ứng dụng chưa tìm nạp máy chủ, vì vậy chúng tôi luôn phải kiểm tra kỹ xem có hiện diện các hình ảnh trong state hay không. Do đó, imageUrl chức năng tìm kiếm hình ảnh với một phím cụ thể, nếu nó không thể tìm thấy, một yêu cầu GET được thực hiện. Đây là mã để dán vào /src /components/detail:
import React from 'react'; import { connect } from 'react-redux'; import { getImages } from '../action-creators'; import Spinner from './spinner'; export class Detail extends React.Component { imageUrl() { if (this.props.imageList.length === 0) { this.props.getImages(); return '; } else { const { id } = this.props.params; return this.props.imageList[id].url; } } render() { return( <div> {this.props.isLoading ? <Spinner/> : <div className="row m-t-4"> <div className="col-md-12"> <img className="img-responsive center-block" src={this.imageUrl()} /> </div> </div> } </div> ); } } function mapStateToProps(state) { return { imageList: state.get('imageList').toJS(), isLoading: state.getIn(['view', 'isLoading']) }; } function mapDispatchToProps(dispatch) { return { getImages: () => dispatch(getImages()) } } export const DetailContainer = connect(mapStateToProps, mapDispatchToProps)(Detail);
Vì bài dài nên tôi sẽ dịch thành 2 phần. Chúng ta kết thúc phần 1 ở đây. Phần 2 chúng ta sẽ cùng tìm hiểu tiếp
Tham khảo
http://www.eloquentwebapp.com/instagram-app-node-react-redux/