✍️
cssadvand
  • [BEM] Exploring SMACSS: Scalable and Modular Architecture for CSS
  • 😀CSS-only infinite scrolling carousel animation (ok)
  • css carousel
  • 🤡 Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 1)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 2)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 3)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 4)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 5)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 6)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 7)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 8)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 9)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 10)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 11)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 12) [Phần đặc biệt]
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 13)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 14)
  • 😘Thuộc tính tabindex="-1" không ngờ lại nguy hiểm vậy đọc bài dưới 😌
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 15)
  • 🥸CSS tạo theme cho dark mode đọc bài (Phần 16)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 16)
  • Mẹo để optimize Google Fonts API mà ít dev để ý đọc (Phần 17)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 17)
  • Thật may nhờ có white-space: pre-line đọc (Phần 18)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 18)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 19)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 20)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 21)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 22)
  • Thuộc tính image-rendering nhờ bài (Phần 23) mà phát hiện ra cách làm bức ảnh mờ trở nên rõ nét
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 23)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết 🤡
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 24)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 25)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 26)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 27)
  • 😘mix-blend-mode pha trộn giữa 1 đối tượng với đối tượng bên dưới đó (Phần 28)
    • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 28)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 29)
  • 🥸aspect-ratio (Phần 30)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 30)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 31)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 32)
  • Một vài thủ thuật CSS mà chính Frontend có thể còn chưa biết (Phần 33)
  • 😘CSS Scroll Triggered Animations Full (ok)
  • 🥹Difference between animation and transition in CSS
  • 😄animation-timeline có phải nó làm việc với thanh cuộn (ok)
  • 😍Từ bài animation-timeline làm việc với thanh cuộn tìm ra cách sử dụng background-clip: text;
  • background-clip: text;
  • 😅Sử dụng list-style-type để tạo icon đẹp (ok)
  • 😁Convert css to scss good (ok)
  • 😁CSS - Hướng dẫn: Tạo ảnh động với hàm steps()
  • 😆Multiple image cross fading in CSS - without (java) khá đẹp script (ok)
  • 😇Function css full 🤩
  • 😅[SVG] SVG viewBox Attribute (ok)
  • 😅Tailwind CSS (ok)
  • 😍Thật không thể tin được sử dụng filter: brightness(0) invert(1); để chuyển màu ảnh 🤣
  • 😅Background text matter.vn (ok)
  • 😆Chọn màu, color, color contrast ratio (ok)
  • 😁Tổng hợp Animating SVG text cực đẹp (ok)
  • 😂Chuyển động tròn :(
  • 😆MOVE-BG mepop.vn (ok)
  • 😆Counter Increment list, number xuongkhopbacninh.com (ok)
  • Boxes That Fill Height, full Height(Or More) (and Don’t Squish) (ok)
  • Equal Height (chiều cao bằng nhau) (oK)
  • 😅Sử dụng nodejs và scss cấu trúc thư mục tốt P.1(ok)
  • 🥲Sử dụng nodejs và js cấu trúc thư mục tốt P.2 (ok)
  • === START STUDY P.1 && P.2 SỬ DỤNG NODEJS VÀ CẤU TRÚC ===
  • 😉1 Giao diện sử dụng thuộc tính data-coreui-toggle="dropdown" (ok)
  • === END STUDY P.1 && P.2 SỬ DỤNG NODEJS VÀ CẤU TRÚC ===
  • 😆Css box-shadow đẹp (ok)
  • === START Tutorials Classical BEM stack ===
  • 😀HTML with BEM (ok)
  • 😀CSS with BEM (ok)
  • === END Tutorials Classical BEM stack ===
  • === START SMACSS ===
  • Categorizing CSS Rules
  • Base Rules
  • Layout Rules
  • Module Rules
  • State Rules
  • Theme Rules
  • Changing State
  • === END SMACSS ===
  • All CSS Grid Properties (ok)
  • 😇Column Grid full example(Responsive)
  • === Start Điều quan trọng nhất cách chia cột và thay đổi vị trí ===
  • CSS Grid căn bản - Phần 1
  • CSS Grid căn bản - Phần 2
  • CSS Grid: Holy Grail Layout
  • Sử dụng CSS Grid để xây dựng web layout
  • Luyện tập CSS Grid qua bài tập tạo layout Airbnb, youtube, Pinterest
  • === End Điều quan trọng nhất cách chia cột và thay đổi vị trí ===
  • Overflow:hidden dots at the end full (ok)
  • Gulp để viết Sass && cai dat
  • Cách nhúng font vào svg (ok)
  • font-face Hướng dẫn conver font và nhúng sử dụng online (ok)
  • 😇Create CSS Animations on Scroll (ok)
  • Hiệu ứng hover bằng js hiện phần description (ok)
  • Text Link on Hover phần 1 (ok)
  • Kết hợp transition & transform-origin phần 2 (ok)
  • Text Hover Phần 3 (ok)
  • Social Media Icons hover effect (ok)
  • css rotate a pseudo :after or :before content:""
  • Hiệu ứng ảnh tự zoom, animation, scale phần 1 full (ok)
  • CodePen Home Image zoom on hover - auto run - view (ok)
  • scroll-padding (ok)
  • Tạo vòm giống styleathome (ok)
  • Tổng hợp những css đẹp để làm trang trí (ok)
  • Cách thay đổi màu svg full (ok_)
  • Disable Auto Zoom in Input “Text” tag - Safari on iPhone (ok)
  • function calc css (ok)
  • Get the scroll distance from bottom to scroll, var distanceFromBottom (ok) vinmec.com (ok)
  • ---------- Start CSSscan không sử dụng ----------------------
  • Unused CSSscan your website for unused CSS selectors (ok)
  • PurifyCSS OnlineRemove unused CSS code from your stylesheets (ok)
  • Remove unused CSS styles from Bootstrap using PurgeCSS (ok)
  • 😆Thêm column, media bootstrap, custom bootstrap (ok)
  • ---------- End CSSscan không sử dụng ----------------------
  • Xây dựng blog đơn giản (Node + React)
  • Các tham số của Gulp
  • File đã thực hành với Gulp (ok
  • gulp-livereload (chưa đọc)
  • Tối ưu hiệu năng Google Fonts (ok)
  • Học CSS GRID thông qua những layout phức tạp
  • [GRID] sử dụng grid xây dựng layout giống Masonry (ok)
  • PageSpeed Insights Chrome Extension (ok)
  • Tăng tốc website bằng cách cải thiện front-end
  • Code chuẩn SEO là gì
  • 10 CSS3 Animation Tools phổ biến
  • Trang check SEO OKE
  • Làm sao để SEO từ khóa?
  • Kiểm soát web typography với việc hiển thị CSS font (ok)
  • Các mẫu phông chữ hiển thị và thuộc tính hiển thị phông chữ của CSS
  • Một số mẹo tối ưu hóa HTML/CSS/JS đúng chuẩn
  • Web Performance
  • Preload, Prefetch, Preconnect (ok)
  • Tối ưu hóa phần Front end cho trình duyệt (part 1)
  • Tối ưu hóa phần Front end cho trình duyệt (part 2)
  • Sử dụng thuộc tính attr html && css (ok)
  • Disable Auto Zoom in Input “Text” tag - Safari on iPhone (ok)
  • CSS submit button weird rendering on iPad/iPhone submit color (ok)
  • Turn off iPhone/Safari input element rounding (ok)
  • scroll bongda.com.vn (ok)
  • Làm chiếc xe ô tô chuyển động giống 2020.yp.vn (ok)
  • Sự khác biệt giữa :empty và :blank trong CSS (ok)
  • Is there a CSS parent selector? (ok)
Powered by GitBook
On this page
  • Xây dựng blog đơn giản (Node + React)
  • Backend (Nodejs)
  • Tạo Model
  • Thiết lập Routing
  • Frontend (React)
  • Wrapper up

Was this helpful?

Xây dựng blog đơn giản (Node + React)

https://viblo.asia/p/xay-dung-blog-don-gian-node-react-djeZ1VMjlWz

Previous---------- End CSSscan không sử dụng ----------------------NextCác tham số của Gulp

Last updated 4 years ago

Was this helpful?

Xây dựng blog đơn giản (Node + React)

Dạo gần đây mình khá hứng thú với Nodejs và React. Đối với cá nhân mình, khi học công nghệ mới không nhất thiết phải xây dựng một ứng dụng cao siêu nào cả. Thì đó là cũng lý do, mình quyết định xây dựng một blog cá nhân đơn giản nhất. I love basic, mình rất thích những cái đơn giản vì sự đơn giản giúp mình có hứng thú, động lực hơn, không gây cho mình cảm giác chán nản. Còn hơn là đặt mục tiêu xây dựng ứng dụng lớn rồi cũng chẳng đến đâu. Một blog cá nhân đơn giản thì có cái gì để học chứ, mọi người đừng có mindset như vậy nhé. Sẽ có nhiều vấn đề lắm đấy . Đây là để tham khảo.

Backend

  1. Node server

  2. Express Framework của Nodejs khá nổi tiếng

  3. Basic về MongoDB (NoSQL)

  4. Học cách quản lí MongoDB object modeling

  5. Xây dựng API theo chuẩn RESTful

Frontend

  1. Cấu hình config của webpack

  2. React với các thư viện như Redux, React-Router

  3. Một chút về SASS Đây là những công nghệ sẽ dùng xuyên suốt khi xây dựng blog cá nhân. Let's GOOO!

Demo:

Backend (Nodejs)

const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const cors = require('cors');
const errorHandler = require('errorhandler');
const mongoose = require('mongoose');

mongoose.promise = global.Promise;

const isProduction = process.env.NODE_ENV === 'production';

const app = express();

app.use(cors());
app.use(require('morgan')('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({ secret: 'LightBlog', cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false }));

if(!isProduction) {
  app.use(errorHandler());
}

mongoose.connect('mongodb://localhost/lightblog');
mongoose.set('debug', true);

// Add models
// Add routes
app.use(require('./routes'));

app.use((req, res, next) => {
  const err = new Error('Not Found');
  err.status = 404;
  next(err);
});

if (!isProduction) {
  app.use((err, req, res) => {
    res.status(err.status || 500);

    res.json({
      errors: {
        message: err.message,
        error: err,
      },
    });
  });
}

app.use((err, req, res) => {
  res.status(err.status || 500);

  res.json({
    errors: {
      message: err.message,
      error: {},
    },
  });
});

const server = app.listen(8000, () => console.log('Server started on http://localhost:8000'));

Chúng ta chắc chắn một điều là tất cả các thư viện cũng cài trong thư mực server với command như sau:

npm i -S path express body-parser express-session cors errorhandler mongoose morgan

Cuối cùng chúng ta cài đặt Nodemon, ở đây mình cài global với câu lệnh như sau:

npm install --g nodemon

Với Nodemon là công cụ khá tuyệt vời, nó giúp chúng ta không phải node app.js nhiều lần khi chúng ta thay đổi nội dung file nào đó. Mọi phiền phức như vậy đã có Nodemon, chúng ta không cần phải re-start nhiều lần khi phát triển sản phẩm. nodemon app.js để restart server.

Tạo Model

Chúng ta tạo model Article có các thuộc tính như sau: timestamp, author, body & title. Trong folder server/models ta tạo file Articles.js với nội dung như sau:

const mongoose = require('mongoose');

const { Schema } = mongoose;

const ArticlesSchema = new Schema({
  title: String,
  body: String,
  author: String,
}, { timestamps: true });

ArticlesSchema.methods.toJSON = function() {
  return {
    _id: this._id,
    title: this.title,
    body: this.body,
    author: this.author,
    createdAt: this.createdAt,
    updatedAt: this.updatedAt,
  };
};

mongoose.model('Articles', ArticlesSchema);

Sau đó chúng ta cần đăng kí model vừa tạo đến server/app.js:

...
mongoose.connect('mongodb://localhost/lightblog');
mongoose.set('debug', true);

// Add models
require('./models/Articles');
// Add routes

app.use((req, res, next) => {
  const err = new Error('Not Found');
  err.status = 404;
  next(err);
});
...

Thiết lập Routing

Việc thiết lập model và server coi như đã xong. Chúng ta cần xây dựng API routing cho Article model với trúc trúc thư mục như sau:

server/
..routes/
....index.js
....api/

Trong file routes/index.js có nội dung như sau:

const express = require('express');
const router = express.Router();

router.use('/api', require('./api'));

module.exports = router;

Sau đó chúng cần tạo file routes/api/index.js nhiệm vụ của file này là đăng kí router cho toàn bộ cấu trúc API của hệ thống. Mỗi model sẽ có API tương ứng, một ứng dụng sẽ có rất nhiều Model nên việc tạo file routes/api/index.js sẽ giúp mình quản lí router trong hệ thống một cách hiệu quả hơn.

const router = require('express').Router();

router.use('/articles', require('./articles'));

module.exports = router;

Trong ta cần tạo routing cho Article nữa là coi như xong, chúng ta cần tạo file routes/api/articles.js:

const mongoose = require('mongoose');
const router = require('express').Router();
const Articles = mongoose.model('Articles');

router.post('/', (req, res, next) => {
  const { body } = req;

  if(!body.title) {
    return res.status(422).json({
      errors: {
        title: 'is required',
      },
    });
  }

  if(!body.author) {
    return res.status(422).json({
      errors: {
        author: 'is required',
      },
    });
  }

  if(!body.body) {
    return res.status(422).json({
      errors: {
        body: 'is required',
      },
    });
  }

  const finalArticle = new Articles(body);
  return finalArticle.save()
    .then(() => res.json({ article: finalArticle.toJSON() }))
    .catch(next);
});

router.get('/', (req, res, next) => {
  return Articles.find()
    .sort({ createdAt: 'descending' })
    .then((articles) => res.json({ articles: articles.map(article => article.toJSON()) }))
    .catch(next);
});

router.param('id', (req, res, next, id) => {
  return Articles.findById(id, (err, article) => {
    if(err) {
      return res.sendStatus(404);
    } else if(article) {
      req.article = article;
      return next();
    }
  }).catch(next);
});

router.get('/:id', (req, res, next) => {
  return res.json({
    article: req.article.toJSON(),
  });
});

router.patch('/:id', (req, res, next) => {
  const { body } = req;

  if(typeof body.title !== 'undefined') {
    req.article.title = body.title;
  }

  if(typeof body.author !== 'undefined') {
    req.article.author = body.author;
  }

  if(typeof body.body !== 'undefined') {
    req.article.body = body.body;
  }

  return req.article.save()
    .then(() => res.json({ article: req.article.toJSON() }))
    .catch(next);
});

router.delete('/:id', (req, res, next) => {
  return Articles.findByIdAndRemove(req.article._id)
    .then(() => res.sendStatus(200))
    .catch(next);
});

module.exports = router;

Công việc xây dựng phía backend coi như là hoàn chỉnh, công việc tiếp theo là test API hoạt động như thế nào nhé. POST localhost:8000/api/articles/ để tạo một blog với body như nhau:

{
  "title": "Test title",
  "author": "Erdeljac",
  "body": "Test desc",
}

PATCH localhost:8000/api/articles/:id để update blog với body tương tự với như tạo blog

GET localhost:8000/api/articles/:id Hiển thị một blog cụ thể với id

Frontend (React)

Ở phần trên, chúng ta đã tạo API server và được test bằng PostMan. Phần server coi như là ổn, phần tiếp theo chúng ta sẽ xây dựng cấu trúc cho client. Một công việc không hề dễ dàng tí nào. Phía Frontend mình thấy khó khăn nhất là việc quản lí state, việc quản lí state hiệu quả là bài toán đau đầu cho tất cả Frontend Developer, khi mà state bắt đầu phình to ra. Theo như mình được biết thì khi làm việc react thì pattern redux đã làm rất tốt điều này. Việc kết hợp với giữa react với react-redux tạo nên sức mạnh tuyệt vời khi phát triển ứng dụng. Bắt đầu vào xây dựng phía client thôi (go).

Init NPM Config

Chúng ta cài đặt một số thư viên để phục vụ cho quá trình xây dựng ứng dụng, đây là những thư viện cần cài đặt, có khá nhiều thư viện mình cần dùng, lưu ý là câu lệnh phía dưới --save-dev có nghĩa là những thư viện này dành cho môi trường development. Hay nói cách khác là toàn bộ tên thư viện được lưu dưới dạng json với key là devDependencies trong file package.json:

npm install --save-dev node-sass html-webpack-plugin style-loader sass-loader css-loader file-loader html-loader webpack webpack-dev-server webpack-cli babel babel-loader babel-plugin-transform-object-rest-spread babel-preset-env babel-preset-react

Trong quá trình cài đặt nếu có phát sinh lỗi thì chạy câu lệnh sau:

npm i -D extract-text-webpack-plugin@next

Sau cùng là chúng ta cần đặt những thư viện phục vụ cho môi trường Development. Chúng ta cần cài đặt dependencies. Sự khác nhau giữa devDependencies và dependencies trong npm. Mình có thể giải thích đơn giản như dependencies là những thư viện bắt buộc phải cài đặt, devDependencies là những thư viện phục vụ cho môi trường Production.

npm i -S react moment bootstrap babel-polyfill history prop-types react-dom react-redux react-router-dom react-scripts redux axios

Init Webpack Config

Tạo file webpack.config.js trong thư mục client như sau:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: [
    'babel-polyfill',
    './src/index.js',
  ],

  output: {
    publicPath: '/',
    filename: './main.js',
  },

  resolve: {
    extensions: ['.js', '.jsx'],
  },

  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },

      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        use: {
          loader: 'file-loader',
          options: {
            name: 'public/img/[name].[ext]',
            outputPath: 'dist/img/',
          },
        },
      },

      {
        test: /\.scss$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [{ loader: 'css-loader', options: { minimize: true } }, 'sass-loader'],
        }),
      },
      {
        test: /\.html$/,
        use: {
          loader: 'html-loader',
          options: {
            minimize: true,
          },
        },
      },
      {
        test: /\.(otf|ttf|eot|woff|woff2)$/,
        loader: 'file-loader',
        options: {
          name: 'public/fonts/[name].[ext]',
          outputPath: 'dist/fonts',
        },
      },
    ],
  },

  plugins: [
    new ExtractTextPlugin({ filename: 'style.css' }),
    new HtmlWebpackPlugin({
      template: './resources/index.html',
      filename: './index.html',
      hash: true,
    }),
  ],

  devServer: {
    historyApiFallback: true,
    publicPath: '/',
    contentBase: './dist',
  },
};

Sau đó chúng ta tạo file .babelrc với nội dung như sau:

{
  "presets": ["env", "react"],
  "plugins": ["transform-object-rest-spread"]
}

Chúng ta cần tạo một số câu lệnh script trong file package.json

...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production",
    "start": "webpack-dev-server --mode development --open"
  },
...

Khi chạy:

  1. npm run build: dùng cho môi trường production

  2. npm run start: dùng cho môi trường development

Tạo Resources

Việc cấu hình webpack coi như là xong, bây giờ chúng ta tạo folder resource để quản lí các file html,css. Chúng ta folder resources chứa folder scss và file index.html, trong folder scss tạo file style.scss file này có nhiệm vụ khai báo global style cho css, với nội dung như sau:

@import url('https://fonts.googleapis.com/css?family=Nunito|Poppins:500|Open+Sans');
@import "~bootstrap/scss/bootstrap";

body, html {
  height: 100%;
  background-color: rgba(0,0,0,.018);
  color: rgba(0,0,0,.75);
  font-family: 'Nunito';
}

React components

Trong folder client tạo folder src chứa file index.js, file này là nơi để viết react hoặc import các file css,scss.. để tạo bundle file là main.js trong file webpack.config.js đã cấu hình. File này có nội dung như sau:

import React from 'react';
import createHistory from 'history/createBrowserHistory';
import ReactDOM from 'react-dom';
import { Route, Switch, Router } from 'react-router-dom';

import { App } from './components';

import '../resources/scss/style.scss';


ReactDOM.render(
  <Router history={createHistory()}>
    <Switch>
      <Route path="/" component={App} />
    </Switch>
  </Router>,
  document.getElementBimport React from 'react';

const App = (props) => {
  return (
    <p>Hello App</p>
  )
}

export default App;yId('root'),
);

Cài đặt router & App component

Tiếp theo, tạo folder components có file index.jsx, file này có nhiệm vụ là đăng kí toàn bộ router component cho ứng dụng với nội dung như sau:

export { default as App } from './App';
import React from 'react';

const App = (props) => {
  return (
    <p>Hello App</p>
  )
}

export default App;

Bây giờ, edit file App/index.jsx để thêm route cho App/index.jsx:

import React from 'react';
import { withRouter, Switch, Route } from 'react-router-dom';

import { Home } from '../../components';

const App = (props) => {
  return (
    <Switch>
      <Route exact path="/" component={Home} />
    </Switch>
  )
}

export default withRouter(App);

Tiếp theo tạo file index.jsx trong directory components/Home như sau:

import React from 'react';

class Home extends React.Component {
  render() {
    return (
      <div className="container">
        <h1>LightBlog</h1>
      </div>
    );
  }
}

export default Home;

Sau đó đăng kí components ở components/index.jsx nữa là xong:

export { default as App } from './App';
export { default as Home } from './Home';

Component trên chỉ là để test xem thử hoạt động hay chưa thôi, bây giờ cập nhập nội dung của Home component, nhiệm vụ của Home component là sử dụng comnponent con là Form component để render ra nội dụng của Home:

import React from 'react';

import { Form } from '../../components/Article';

class Home extends React.Component {
  render() {
    return (
      <div className="container">
        <div className="row pt-5">
          <div className="col-12 col-lg-6 offset-lg-3">
            <h1 className="text-center">LightBlog</h1>
          </div>
          <Form />
        </div>
      </div>
    );
  }
}

export default Home;

Article/Form/index.jsx:

import React from 'react';

class Form extends React.Component {
  render() {
    return (
      <div className="col-12 col-lg-6 offset-lg-3">
        <input className="form-control my-3" placeholder="Article Title" />
        <textarea className="form-control my-3" placeholder="Article Description">
        </textarea>
        <input className="form-control my-3" placeholder="Article Author" />
        <button className="btn btn-primary float-right">Submit</button>
      </div>
    )
  }
}

export default Form;

Article/index.js:

export { default as Form } from './Form';

Edit components/index.js:

export { default as App } from './App';
export { default as Home } from './Home';
export { default as Article } from './Article';

Xong phần view của form, bây giờ chúng ta xử lí thao tác trên form, component Form sẽ có nhiệm vụ submit gửi request đến server API để tạo một blog với nội dung tương ứng. Endpoint để tạo blog là POST localhost:8000/api/articles, ta phải edit Article/Form/index.jsx:

import React from 'react';
import axios from 'axios';

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      title: '',
      body: '',
      author: '',
    }

    this.handleChangeField = this.handleChangeField.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(){
    const { title, body, author } = this.state;

    return axios.post('http://localhost:8000/api/articles', {
      title,
      body,
      author,
    });
  }

  handleChangeField(key, event) {
    this.setState({
      [key]: event.target.value,
    });
  }

  render() {
    const { title, body, author } = this.state;

    return (
      <div className="col-12 col-lg-6 offset-lg-3">
        <input
          onChange={(ev) => this.handleChangeField('title', ev)}
          value={title}
          className="form-control my-3"
          placeholder="Article Title"
        />
        <textarea
          onChange={(ev) => this.handleChangeField('body', ev)}
          className="form-control my-3"
          placeholder="Article Body"
          value={body}>
        </textarea>
        <input
          onChange={(ev) => this.handleChangeField('author', ev)}
          value={author}
          className="form-control my-3"
          placeholder="Article Author"
        />
        <button onClick={this.handleSubmit} className="btn btn-primary float-right">Submit</button>
      </div>
    )
  }
}

export default Form;

Adding Redux

Ở trên chúng ta tạo app chưa có quản lí state, ở đây chúng ta sử dụng redux để làm điều này, bài viết này hạn chế nên mình không thể giải thích hết được, bài viết này chỉ là việc áp dụng redux cho ứng dụng.

src/store.js

import { createStore, combineReducers } from 'redux';

import { home } from './reducers';

const reducers = combineReducers({
  home,
});

const store = createStore(reducers);

export default store;

Tiếp theo chúng ta tạo reducer cho home src/reducers/index.js

export { default as home } from './home';

Cũng giống như folder components tạo file index.js để quản lí toàn bộ component của ứng dụng, cũng giống như reducer vậy.

src/reducers/home.js

export default (state={}, action) => {
  switch(action.type) {
    case 'HOME_PAGE_LOADED':
      return state;
    default:
      return state;
  }
};

Việc đăng kí redux cho ứng dụng nên chúng ta cần thay đổi nội dung của src/index.js như sau:

import React from 'react';
import createHistory from 'history/createBrowserHistory';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Route, Switch, Router } from 'react-router-dom';

import store from './store';
import { App } from './components';

import '../resources/scss/style.scss';


ReactDOM.render(
  <Router history={createHistory()}>
    <Provider store={store}>
      <Switch>
        <Route path="/" component={App} />
      </Switch>
    </Provider>
  </Router>,
  document.getElementById('root'),
);

Hiển thị tất cả các blog

Công việc bây giờ chúng ta ứng dụng redux vào cho Home component thôi nào. Edit src/components/Home/index.jsx:

import React from 'react';
import axios from 'axios';
import { connect } from 'react-redux';

import { Form } from '../../components/Article';

class Home extends React.Component {
  componentDidMount() {
    const { onLoad } = this.props;

    axios('http://localhost:8000/api/articles')
      .then((res) => onLoad(res.data));
  }
  render() {
    const { articles } = this.props;

    return (
      <div className="container">
        <div className="row pt-5">
          <div className="col-12 col-lg-6 offset-lg-3">
            <h1 className="text-center">LightBlog</h1>
          </div>
          <Form />
        </div>
        <div className="row pt-5">
          <div className="col-12 col-lg-6 offset-lg-3">
            {articles.map((article) => {
              return (
                <div className="card my-3">
                  <div className="card-header">
                    {article.title}
                  </div>
                  <div className="card-body">
                    {article.body}
                  </div>
                  <div className="card-footer">
                    <i>{article.author}
                      <p className="float-right">
                        {new Date(article.createdAt).toLocaleDateString()}
                      </p>
                    </i>
                  </div>
                </div>
              )
            })}
          </div>
        </div>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  articles: state.home.articles,
});

const mapDispatchToProps = dispatch => ({
  onLoad: data => dispatch({ type: 'HOME_PAGE_LOADED', data }),
});

export default connect(mapStateToProps, mapDispatchToProps)(Home);

Edit src/reducers/home.js

export default (state={articles: []}, action) => {
  switch(action.type) {
    case 'HOME_PAGE_LOADED':
      return {
        ...state,
        articles: action.data.articles,
      };
    default:
      return state;
  }
};

Tạo Blog

Xong hiển thị blog, bây giờ sẽ tạo một blog kết hợp với redux để thay đổi state cho ứng dụng. Edit src/components/Article/Form/index.jsx:

import axios from 'axios';
import React from 'react';
import { connect } from 'react-redux';

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      title: '',
      body: '',
      author: '',
    }

    this.handleChangeField = this.handleChangeField.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(){
    const { onSubmit } = this.props;
    const { title, body, author } = this.state;

    return axios.post('http://localhost:8000/api/articles', {
      title,
      body,
      author,
    })
      .then((res) => onSubmit(res.data));
  }

  handleChangeField(key, event) {
    this.setState({
      [key]: event.target.value,
    });
  }

  render() {
    const { title, body, author } = this.state;

    return (
      <div className="col-12 col-lg-6 offset-lg-3">
        <input
          onChange={(ev) => this.handleChangeField('title', ev)}
          value={title}
          className="form-control my-3"
          placeholder="Article Title"
        />
        <textarea
          onChange={(ev) => this.handleChangeField('body', ev)}
          className="form-control my-3"
          placeholder="Article Body"
          value={body}>
        </textarea>
        <input
          onChange={(ev) => this.handleChangeField('author', ev)}
          value={author}
          className="form-control my-3"
          placeholder="Article Author"
        />
        <button onClick={this.handleSubmit} className="btn btn-primary float-right">Submit</button>
      </div>
    )
  }
}

const mapDispatchToProps = dispatch => ({
  onSubmit: data => dispatch({ type: 'SUBMIT_ARTICLE', data }),
});

export default connect(null, mapDispatchToProps)(Form);

Thêm action cho home reducer

export default (state={articles: []}, action) => {
  switch(action.type) {
    case 'HOME_PAGE_LOADED':
      return {
        ...state,
        articles: action.data.articles,
      };
    case 'SUBMIT_ARTICLE':
      return {
        ...state,
        articles: ([action.data.article]).concat(state.articles),
      }
    default:
      return state;
  }
};

Redux libraries are like glasses: you’ll know when you need them Vô cùng hack não (yaoming)

Deleting & Editing Blog

Edit src/components/Home/index.jsx: để style giao diện cho edit và delete blog:

import React from 'react';
import axios from 'axios';
import moment from 'moment';
import { connect } from 'react-redux';

import { Form } from '../../components/Article';

class Home extends React.Component {
  constructor(props) {
    super(props);

    this.handleDelete = this.handleDelete.bind(this);
    this.handleEdit = this.handleEdit.bind(this);
  }

  componentDidMount() {
    const { onLoad } = this.props;

    axios('http://localhost:8000/api/articles')
      .then((res) => onLoad(res.data));
  }

  handleDelete(id) {
    const { onDelete } = this.props;

    return axios.delete(`http://localhost:8000/api/articles/${id}`)
      .then(() => onDelete(id));
  }

  handleEdit(article) {
    const { setEdit } = this.props;

    setEdit(article);
  }

  render() {
    const { articles } = this.props;

    return (
      <div className="container">
        <div className="row pt-5">
          <div className="col-12 col-lg-6 offset-lg-3">
            <h1 className="text-center">LightBlog</h1>
          </div>
          <Form />
        </div>
        <div className="row pt-5">
          <div className="col-12 col-lg-6 offset-lg-3">
            {articles.map((article) => {
              return (
                <div className="card my-3">
                  <div className="card-header">
                    {article.title}
                  </div>
                  <div className="card-body">
                    {article.body}
                    <p className="mt-5 text-muted"><b>{article.author}</b> {moment(new Date(article.createdAt)).fromNow()}</p>
                  </div>
                  <div className="card-footer">
                    <div className="row">
                      <button onClick={() => this.handleEdit(article)} className="btn btn-primary mx-3">
                        Edit
                      </button>
                      <button onClick={() => this.handleDelete(article._id)} className="btn btn-danger">
                        Delete
                      </button>
                    </div>
                  </div>
                </div>
              )
            })}
          </div>
        </div>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  articles: state.home.articles,
});

const mapDispatchToProps = dispatch => ({
  onLoad: data => dispatch({ type: 'HOME_PAGE_LOADED', data }),
  onDelete: id => dispatch({ type: 'DELETE_ARTICLE', id }),
  setEdit: article => dispatch({ type: 'SET_EDIT', article }),
});

export default connect(mapStateToProps, mapDispatchToProps)(Home);

Edit components/Article/Form/index.jsx:

import axios from 'axios';
import React from 'react';
import { connect } from 'react-redux';

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      title: '',
      body: '',
      author: '',
    }

    this.handleChangeField = this.handleChangeField.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  componentWillReceiveProps(nextProps) {
    if(nextProps.articleToEdit) {
      this.setState({
        title: nextProps.articleToEdit.title,
        body: nextProps.articleToEdit.body,
        author: nextProps.articleToEdit.author,
      });
    }
  }

  handleSubmit(){
    const { onSubmit, articleToEdit, onEdit } = this.props;
    const { title, body, author } = this.state;

    if(!articleToEdit) {
      return axios.post('http://localhost:8000/api/articles', {
        title,
        body,
        author,
      })
        .then((res) => onSubmit(res.data))
        .then(() => this.setState({ title: '', body: '', author: '' }));
    } else {
      return axios.patch(`http://localhost:8000/api/articles/${articleToEdit._id}`, {
        title,
        body,
        author,
      })
        .then((res) => onEdit(res.data))
        .then(() => this.setState({ title: '', body: '', author: '' }));
    }
  }

  handleChangeField(key, event) {
    this.setState({
      [key]: event.target.value,
    });
  }

  render() {
    const { articleToEdit } = this.props;
    const { title, body, author } = this.state;

    return (
      <div className="col-12 col-lg-6 offset-lg-3">
        <input
          onChange={(ev) => this.handleChangeField('title', ev)}
          value={title}
          className="form-control my-3"
          placeholder="Article Title"
        />
        <textarea
          onChange={(ev) => this.handleChangeField('body', ev)}
          className="form-control my-3"
          placeholder="Article Body"
          value={body}>
        </textarea>
        <input
          onChange={(ev) => this.handleChangeField('author', ev)}
          value={author}
          className="form-control my-3"
          placeholder="Article Author"
        />
        <button onClick={this.handleSubmit} className="btn btn-primary float-right">{articleToEdit ? 'Update' : 'Submit'}</button>
      </div>
    )
  }
}

const mapDispatchToProps = dispatch => ({
  onSubmit: data => dispatch({ type: 'SUBMIT_ARTICLE', data }),
  onEdit: data => dispatch({ type: 'EDIT_ARTICLE', data }),
});

const mapStateToProps = state => ({
  articleToEdit: state.home.articleToEdit,
});

export default connect(mapStateToProps, mapDispatchToProps)(Form);

Sau cùng chúng add action cho reducers nữa là xong:

export default (state={articles: []}, action) => {
  switch(action.type) {
    case 'HOME_PAGE_LOADED':
      return {
        ...state,
        articles: action.data.articles,
      };
    case 'SUBMIT_ARTICLE':
      return {
        ...state,
        articles: ([action.data.article]).concat(state.articles),
      };
    case 'DELETE_ARTICLE':
      return {
        ...state,
        articles: state.articles.filter((article) => article._id !== action.id),
      };
    case 'SET_EDIT':
      return {
        ...state,
        articleToEdit: action.article,
      };
    case 'EDIT_ARTICLE':
      return {
        ...state,
        articles: state.articles.map((article) => {
          if(article._id === action.data.article._id) {
            return {
              ...action.data.article,
            }
          }
          return article;
        }),
        articleToEdit: undefined,
      }
    default:
      return state;
  }
};

Wrapper up

Trong bài viết này, sẽ không dùng Express application generator thay vào đó chúng ta nên tự cấu hình config cho riêng mình. Chứ khi bắt đầu chúng ta cần tạo folder server và trong folder server tạo file app.js có nội dụng như sau:

Ở đoạn code trên mình đã tạo xong server chạy bằng express. Ở bài viết này, mình xem như các bạn đã biết về express rồi nhé . Ở đoạn code trên có một đoạn chúng ta sẽ ít gặp đó là app.use(require('morgan')('dev'));. Mình giới thiệu sơ qua về thư viện này một chút, thực chất là HTTP request logger middleware. Thư viện khá thích hợp cho môi trường development, với morgan sẽ có rất nhiều option để log. Chẳng hạn như nếu có lỗi xảy ra sẽ log error đến Apache file access.log hoặc STDOUT hoặc là một file chỉ định nào đó cũng được. Bạn có thể tìm hiểu nhiều hơn về thư viện này. Đối với app.use(require('morgan')('dev')); sẽ responses đến console . Khá thú vị phải không?

GET localhost:8000/api/articles Liệt kê tất cả các danh số blog đã tạo

DELETE localhost:8000/api/articles/:id Xóa một blog với tham số id

Trước tiên, chúng ta cần tạo folder client cùng cấp với folder với server:

Sau đó chúng ta run npm init để khởi tạo npm để quản lí package:

Việc config bên phía client coi như là xong, công việc bây giờ là viết react trên nền những config ở trên thôi nào. (go) (go).

Một điều lưu ý là khi trong lệnh npm thì phải chạy trong folder client

Trong folder components tạo tiếp folder App có file index.jsx với nội dung:

Nếu hoàn thành bước trên, chúng ta sẽ được kết quả mong đợi như sau:

Nhái hàng một chút khi thành công nào:

Việc tạo form được xem như là hoàn tất:

Khi nhập nội dung title, body & author, khi submit tạo blog. Nếu thành công kết quả khi test ở debug của chorme như sau:

Nếu bạn có trong database có dữ liệu thì kết quả sẽ trong như thế này, hiển tại tất cả các blog với redux, mỗi action trong sẽ tương ứng với việc thay đổi state khác nhau. Hưởng thụ kết quả:

Nhập nội dung của form và submit form xem nào. Mình tin kết quả sẽ không khác kết quả nhau là mấy khi dùng và không dùng redux là mấy, nhưng cấu trúc cách thức sẽ hoàn toàn khác nhau, với redux khi ứng dụng scale up mỗi lúc một lớn, vì dùng redux sẽ đở nhọc nhằng hơn rất nhiều về việc quản lí state đấy. Không phải lúc nào cũng đã dùng react là phải dùng redux đâu nhé . Đây là câu quote của Dan Abramov là creator của Redux có nói một câu:

Giao diện sẽ đơn giản như sau:

Như vậy chúng ta đã hoàn thành xây dựng blog đơn giản, chúng ta học rất nhiều thứ từ front-end đến back-end. Cố gắng hoàn thành 1 chức năng đơn giản nhưng không hề đơn giản với những người bắt đầu đâu nhé, trong quá trình làm theo bài viết sẽ có rất nhiều nút thắt cần được giải quyết và research rất nhiều mới hiểu được các vấn đề. Đừng xem nhẹ những ứng dụng nhỏ nhỏ nhé . Happy Coding.

generator
Node.js
React-Redux
ReactJS
MongoDB
Github repo
https://youtu.be/BowA_m083aU
morgan
😄
😄
😄
😄
😄
😄
Demo