หรือที่เรียกกันว่า MEAN Stack หนึ่ง

การก้าวกระโดดจากการเขียน "สวัสดีชาวโลก" ในคอนโซลของคุณไปเป็นการสร้างแอปพลิเคชันหน้าเดียวมักจะดูน่ากังวล แม้ว่าเส้นทางนี้จะเป็นเส้นทางที่มีลมแรงและมีทางเบี่ยง กับดักทราย และแหล่งข้อมูลที่ดูเหมือนไม่มีที่สิ้นสุด แต่ก็เป็นเส้นทางที่เข้าใจง่าย เมื่อคุณมีความเข้าใจเกี่ยวกับการโต้ตอบระหว่างไคลเอ็นต์-เซิร์ฟเวอร์-ฐานข้อมูลแล้ว สิ่งที่คุณต้องทำก็แค่นำงานของคุณไปใช้ ในซีรีส์แอปหน้าเดียวนี้ เราจะสร้างแอปพลิเคชันเดียวกันโดยใช้เฟรมเวิร์ก JavaScript ที่หลากหลาย ในการทำเช่นนั้น ฉันหวังว่าจะชี้แจงความเข้าใจของคุณเกี่ยวกับ "กรอบงาน MVC" รวมทั้งให้พื้นฐานแก่คุณในการช่วยพัฒนาแอปพลิเคชันของคุณ โดยสรุปสั้นๆ กรอบงาน MVC ช่วยให้โปรแกรมเมอร์มีวิธี “แยกข้อกังวล” เพื่อแบ่งส่วนส่วนประกอบและฟังก์ชันต่างๆ อย่างชัดเจน ในการทำเช่นนั้น เราสามารถสร้างแอปที่ชัดเจนในแอปพลิเคชันและฟังก์ชันได้ ขั้นแรก เราจะอธิบายวิธีสร้างแอปพลิเคชัน Angular หน้าเดียวพื้นฐานโดยใช้สแต็ก MEAN (MongoDB สำหรับฐานข้อมูลของเรา NodeJS/Express สำหรับเซิร์ฟเวอร์ และ AngularJS สำหรับเฟรมเวิร์กส่วนหน้าของเรา) โพสต์นี้เป็นโพสต์แรกในชุดโพสต์เกี่ยวกับวิธีสร้างรายการสิ่งที่ต้องทำโดยใช้เฟรมเวิร์กส่วนหน้าและส่วนหลังที่หลากหลาย หากต้องการดูวิธีสร้างแอปแบบเต็มสแต็กโดยใช้ React คลิกที่นี่

เครื่องมือและการขึ้นต่อกัน
ในการเริ่มต้น คุณจะต้องติดตั้งการขึ้นต่อกันต่อไปนี้:
- Node
- Express
- MongoDB< br /> - AngularJS

คุณสามารถทำได้โดยการรันคำสั่งต่อไปนี้ในเทอร์มินัลของคุณ:

npm install angular express body-parser ejs mongojs 
npm install bootstrap mongodb --save
npm install -g nodemon

คำสั่งเหล่านี้จะติดตั้งและบันทึกการขึ้นต่อกันลงใน repo ของคุณ (ในกรณีนี้ ejs จะถูกใช้เป็นระบบเทมเพลตของเราสำหรับเซิร์ฟเวอร์ในการแสดงผลส่วนประกอบส่วนหน้า) Nodemon เป็นส่วนขยายเรียบร้อยที่จะโหลดเซิร์ฟเวอร์ของคุณทุกครั้งที่มีการเปลี่ยนแปลงไฟล์ ดังนั้นคุณจึงไม่จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์ตลอดเวลา

โครงสร้างแอปพลิเคชัน
เมื่อสิ้นสุดบทแนะนำนี้ โครงสร้างแอปพื้นฐานของเราควรมีลักษณะดังนี้:

หากคุณต้องการอ้างอิงผลิตภัณฑ์สำเร็จรูป ณ เวลาใดก็ตาม คุณสามารถคัดลอก "เทมเพลต Angular ที่นี่" ได้ตามใจชอบ

ขั้นตอนที่ 1 — สร้างเซิร์ฟเวอร์

อ่า ผืนผ้าใบว่างเปล่า นี่เป็นส่วนที่น่าตื่นเต้นที่สุดของกระบวนการนี้ เราจะเริ่มต้นแอปพลิเคชันหน้าเดียวโดยการสร้างไฟล์ server.js พื้นฐาน เนื่องจากเราจะใช้ Node กับ Express สำหรับเซิร์ฟเวอร์ของเรา เราจะต้องนำเข้าการอ้างอิงที่เกี่ยวข้องทั้งหมดก่อน

// server.js
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const index = require('./routes/index');
const tasks = require('./routes/tasks'); 

const app = express();
let port = process.env.PORT || 3000 // sets a relative port

เมื่อเราสร้างอินสแตนซ์เซิร์ฟเวอร์ Node/Express ของเราแล้ว เราจะต้องตั้งค่าพาธของไฟล์ไปยังไดเร็กทอรีไคลเอนต์ของเรา และใช้ bodyParser เพื่อเข้ารหัสคำขอและการตอบกลับของเราอย่างถูกต้อง นอกจากนี้เรายังต้องการตั้งค่าเอ็นจิ้นการดูของเราเพื่อระบุเส้นทางที่ควรแสดงผลเมื่อได้รับคำขอ API เฉพาะ

// server.js
// Setup View Engine
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs'); //specifies the engine we want to use
app.engine('html', require('ejs').renderFile); //renders files with html extension
// Set Static Folder
app.use(express.static(__dirname, 'client')); 
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use('/', index); //sets our home page route
app.use('/api', tasks); //sets our api call routes
//Starts our server
app.listen(port, function() {
  console.log('We have successfully connected to port: ', port);
});

ขั้นตอนที่ 2 — การกำหนดเส้นทางพื้นฐาน

เราได้สร้างเซิร์ฟเวอร์ของเราแล้ว แต่จะมีความหมายน้อยมากหากเราไม่มีการตั้งค่าไฟล์และเส้นทางด้วย มาสร้างโฟลเดอร์ใหม่ชื่อ routes พร้อมไฟล์ index.js และไฟล์ tasks.js ไฟล์เหล่านี้จะดูค่อนข้างคล้ายกันในตอนนี้:

// index.js
const express = require('express');
const router = express.Router();
// Set Route for Home 
router.get('/', function(req, res, next) {
  res.render('index.html');
});
module.exports = router;
// tasks.js
const express = require('express');
const router = express.Router();
// Set Route for Tasks
router.get('/tasks', function(req, res, next) {
  res.send('this is our tasks API');
});
module.exports = router;

ในการเรนเดอร์ข้อมูลเฉพาะและเชื่อมต่อไคลเอนต์ของเรากับเซิร์ฟเวอร์ เราจะต้องสร้างโฟลเดอร์ views ของเราด้วย โฟลเดอร์นี้จะมีไฟล์ index.html หลัก

//index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>My AngularJS App</title>
    <link rel="stylesheet" href="./node_modules/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="./styles/styles.css">
    <script src="./node_modules/angular/angular.js"></script>
  </head>
  <body>
    <app ng-app="angular-app">
      <h5><br/><br/>this message will disappear when <em>app</em> component is correctly rendered<br/><br/><br/></h5>
    </app>
    <script src="./client/components/app.js"></script>
    <script src="./client/components/addTask.js"></script>
    <script src="./client/components/taskList.js"></script>
    <script src="./client/components/taskListEntry.js"></script>
  </body>
</html>

ตอนนี้เราจะต้องสร้างไฟล์ในโฟลเดอร์ client/components โดยมีไฟล์ html ที่เกี่ยวข้องแต่ละไฟล์ใน client/templates

ขั้นตอนที่ 3 —สร้างส่วนหน้า

เราจะเริ่มต้นด้วยไฟล์ app.js ของเรา ตามแบบแผนเชิงมุม ไฟล์ app.js นี้เป็นองค์ประกอบหลักของส่วนประกอบอื่นๆ ทั้งหมด สิ่งแรกที่เราต้องทำคือตั้งค่าโมดูลเชิงมุมของเราเป็น ng-app ที่เราประกาศในไฟล์ index.html ของเรา การตั้งค่า "โมดูลเชิงมุม" ที่ด้านบนของส่วนประกอบแต่ละส่วนจะทำให้เราสามารถเข้าถึงและเชื่อมโยงข้อมูลเฉพาะส่วนประกอบได้ทุกที่ที่เราต้องการ เมื่อเราทำสิ่งนี้เสร็จแล้ว เราก็สามารถเริ่มตั้งค่าส่วนประกอบแต่ละส่วนของเราได้ เรารู้ว่าแต่ละไฟล์ส่วนประกอบจะมี templateUrl เชื่อมโยงอยู่ด้วย นอกจากนี้เรายังทราบด้วยว่าแต่ละส่วนประกอบมีตัวควบคุมเฉพาะของตัวเอง และส่วนประกอบย่อยแต่ละส่วนจะสืบทอดข้อมูลจากส่วนประกอบหลักผ่านวิธีการรวม เนื่องจากเราจะสร้างรายการสิ่งที่ต้องทำ เราจึงรู้ว่าเราจำเป็นต้องมีวิธีจัดเก็บข้อมูลของเรา ด้วยเหตุนี้ app.js ของเราจึงควรมีลักษณะดังนี้:

//app.js
angular.module('angular-app', [])
.component('app', {
  templateUrl: 'client/templates/app.html',
  controller: function($http) {
    //ensures that the this scope isn't lost
    let $ctrl = this;
    $ctrl.tasks = [];
    $http.get('/api/tasks').then(function(res) {
      res.data.forEach((obj) => {
        $ctrl.tasks.push(obj);
      })
    })
  }
});
//app.html
<div id="app container" >
  <nav class="navbar">
    <div class="col-md-6 col-md-offset-3">
      <add-task tasks="$ctrl.tasks"><h5><em>addTask</em> component goes here</h5></add-task>
    </div>
  </nav>
  <div class="row">
    <div class="col-md-7">
      <task-list tasks="$ctrl.tasks"><h5><em>taskList</em> component goes here</h5></task-list>
    </div>
  <div>
</div>

เทมเพลตนี้มีข้อมูลที่ยังไม่เกี่ยวข้องมากนัก แต่จะเป็นเมื่อเราเริ่มต้นกับส่วนประกอบย่อยของเรา สังเกตว่าเราส่งผ่านอาเรย์งานที่เราตั้งค่าไว้ในไฟล์ app.js ของเราอย่างไร เพื่อให้คอมโพเนนต์ลูกสามารถเข้าถึงและแก้ไขอาเรย์ได้เมื่อจำเป็น หากเราต้องเริ่มต้นเซิร์ฟเวอร์และเปิด localhost:3000 เราจะไม่เห็นข้อความจากไฟล์ index.html ของเราอีกต่อไป แต่เราจะเห็นข้อความใหม่สองข้อความที่เรามีในไฟล์ app.html ของเราแทน นอกจากนี้ หากเราเปิดคอนโซลนักพัฒนาซอฟต์แวร์ เราจะเห็นข้อความจากคำขอรับของเราระบุว่า this is our API

ยอดเยี่ยม! ตอนนี้เราได้ตั้งค่าพื้นฐานของแอปแล้ว เราก็สามารถเริ่มเจาะลึกลงไปอีกหน่อยได้ เริ่มต้นด้วยการสร้างไฟล์ addTask.js ในโฟลเดอร์ส่วนประกอบของเรา และไฟล์ addTask.html ที่เกี่ยวข้องในโฟลเดอร์เทมเพลต ไฟล์นี้จะแชร์คุณลักษณะต่างๆ มากมายเช่นเดียวกับไฟล์ app.js ของเรา แต่เราจะต้องตรวจสอบให้แน่ใจว่าได้ตั้งค่าการเชื่อมโยงอย่างถูกต้อง เพื่อที่เราจะได้สามารถทำการเปลี่ยนแปลงในรายการของเราได้โดยตรง ไฟล์นี้จะจัดการกับการเพิ่มงานในรายการของเราเป็นหลัก ด้วยเหตุนี้ เราจะต้องเพิ่มฟังก์ชันเฉพาะลงในคอนโทรลเลอร์ของเรา เรายังต้องการให้แน่ใจว่าไม่สามารถส่งงานที่ว่างเปล่าได้ และงานนั้นไม่ควรทำซ้ำ ในการทำเช่นนั้น เราจะต้องสร้างฟังก์ชันตัวช่วยเล็กๆ น้อยๆ เพื่อทำการตรวจสอบขั้นพื้นฐานเหล่านี้:

//addTask.js
angular.module('angular-app')
.component('addTask', {
  templateUrl: 'client/templates/addTask.html',
  controller: function($http) {
    let $ctrl = this;
    let dup = false;
    // checks our list for duplicates
    let checkList = function(tasks, task) {
      tasks.forEach((item) => {
        if (item.task === task) {
          dup = true;
        }
      });
    }
    $ctrl.addTask = (task) => {
      // checks if the task is blank
      if (!task) {
        return alert('please enter a task');
      }
      checkList($ctrl.tasks, task);
      // will return a true dup if the input already exists
      if (dup) {
        dup = false;
        return alert('that task already exists');
      }
      // sets our task to an object that can be posted
      task = {
        task: task,
        editing: false //we will use this to edit tasks later
      };
      $http.post('/api/tasks', task)
      .then((res) => {
        $ctrl.tasks.push(res.data); //adds a task to the task list
        $ctrl.task = ''; //sets the input field blank again
      })
    }
  },
  bindings: {
    tasks: '<' 
  }
});
//addTask.html
<div class="search-bar form-inline">
  <input ngclass="form-control" placeholder="Add a task here!" ng-model="$ctrl.task" type="text" />
  <button class="btn" ng-click="$ctrl.addTask($ctrl.task)">
    <span class="glyphicon glyphicon-plus-sign"></span> add task
  </button>
</div>

ในไฟล์ html ของเรา เรากำลังประกาศโมเดลอินพุตของเราเพื่อที่เราจะได้เข้าถึงค่าอินพุตในฟังก์ชัน $ctrl.addTask ที่เราประกาศในคอมโพเนนต์ของเรา สำหรับข้อมูลเพิ่มเติมเกี่ยวกับส่วนประกอบ การผูก ฯลฯ... ลองดูที่ "โพสต์ที่ยอดเยี่ยม" นี้!

แม้ว่าเราจะสร้างวิธีการโพสต์ของเราแล้ว คุณจะสังเกตเห็นว่าข้อมูลไม่ได้ถูกแสดงผล นั่นเป็นเพราะว่าเราไม่ได้ให้โพสต์ของเราแสดงผลบนเพจได้ เรายังไม่ได้ตั้งค่าให้แอปของเราคงข้อมูลที่โพสต์ไว้บนฐานข้อมูล ดังนั้นคำขอ $http.post ของเราจะไม่ไปไหน ก่อนอื่นเราจะแก้ไขปัญหาส่วนหน้า เมื่อเราทำเสร็จแล้วเราจะไปยังส่วนหลังของเรา

ย้อนกลับไปในโฟลเดอร์ส่วนประกอบของเรา เราจะต้องสร้างไฟล์ taskList.js ของเราในโฟลเดอร์ส่วนประกอบ รวมถึงไฟล์ taskList.html ที่เกี่ยวข้องในโฟลเดอร์เทมเพลต ไฟล์เหล่านี้จะค่อนข้างพื้นฐาน เนื่องจากเป็นเพียงวิธีง่ายๆ สำหรับเราในการส่งข้อมูลไปยังแต่ละรายการงาน:

//taskList.js
angular.module('angular-app')
.component('taskList', {
  templateUrl: 'client/templates/taskList.html',
  controller: function() {},
  bindings: {
    tasks: '<'
  }
});
<ul class="task-list">
    <task-list-entry ng-repeat="task in $ctrl.tasks track by task._id" tasks="$ctrl.tasks" task="task"></task-list-entry>
</ul>

อย่างที่คุณเห็น ไฟล์เหล่านี้ทำหน้าที่ถ่ายโอนข้อมูลเฉพาะไปยังรายการสิ่งที่ต้องทำแต่ละรายการ ด้วยเหตุนี้ เราจึงสามารถสร้างองค์ประกอบสุดท้ายของเราได้ นั่นคือไฟล์ taskListEntry.js และไฟล์ taskListEntry.html ในที่นี้เราจะระบุว่าเราต้องการเรนเดอร์รายการสิ่งที่ต้องทำของเราอย่างไร รวมถึงคุณสมบัติที่เราต้องการให้แต่ละรายการมี เรารู้ว่าเราต้องการแก้ไขรายการสิ่งที่ต้องทำที่มีอยู่ และเรายังต้องการลบรายการใดๆ ที่เราได้ทำเสร็จแล้วด้วย นี่คือจุดที่คุณสมบัติการแก้ไขงานของเราจะเข้ามามีบทบาท:

//taskListEntry.js
angular.module('angular-app')
.component('taskListEntry', {
  templateUrl: 'client/templates/taskListEntry.html',
  controller: function($http) {
    $ctrl = this;
    $ctrl.toggleEdit = function(task) {
      //sets the edit value to the opposite of what it currently is
      task.editing = !task.editing;
    }
    $ctrl.updateTask = function(task) {
      // checks for empty edit values
      if (!task.task) {
        return alert('please enter a task');
      }
      task.editing = false;
      task = {
        task: task
      }
      // send our update request
      $http.put('/api/tasks/' + task.task._id, task).then((res) => {
        $ctrl.toggleEdit(task);
      })
    }
    // deletes specific tasks
    $ctrl.deleteTask = function(task) {
      $http.delete('/api/tasks/' + task._id)
      .then((res) => {
        let i = $ctrl.tasks.indexOf(task);
        // removes the task from the task list
        $ctrl.tasks.splice(i, 1);
      })
    }
  },
  bindings: {
    task: '<', //binding the individual task
    tasks: '<' //binding the entire task list
  }
});

ขั้นตอนที่ 4 — สร้างฐานข้อมูล

ตอนนี้เราได้ตั้งค่าส่วนหน้าของเราแล้ว เราสามารถดำเนินการต่อและขอคำขอ GET, POST, PUT และ DELETE เหล่านั้นได้ สิ่งแรกที่เราต้องทำคือซิงค์ฐานข้อมูลของเราเพื่อให้คำขอเหล่านี้ได้รับการจัดการอย่างถูกต้อง เมื่อเราทำเสร็จแล้ว เราก็สามารถเขียนฟังก์ชันตัวจัดการคำขอเฉพาะของเราได้:

// tasks.js
const express = require('express');
const router = express.Router();
const mongojs = require('mongojs');
const db = mongojs('tasks', ['tasks']);
// Get All Tasks
router.get('/tasks', function(req, res, next) {
  db.tasks.find(function(err, tasks) {
    if (err) {
      res.status(404);
      res.send(err);
    }
    res.json(tasks);
  });
});

นี่ไม่ได้แตกต่างจากที่เราเคยมีมาก่อน แต่อย่างที่คุณเห็น แทนที่จะส่งข้อความตอบกลับ เราจะส่งคืนงานทั้งหมดที่บันทึกไว้ในฐานข้อมูลงาน ตอนนี้เราได้สร้างคำขอ GET ของเราแล้ว เราก็สามารถเริ่มต้นใน POST ของเราได้:

// Save Tasks
router.post('/tasks', function(req, res, next) {
  let task = req.body;
  if (!task) {
    res.status(404);
    res.json({
      error: 'information is invalid'
    });
  } else {
    db.tasks.save(task, function(err, task) {
      if (err) {
        res.status(404);
        res.send(err);
      }
      res.json(task);
    });
  }
});

ต่างจากคำขอ GET ของเรา คำขอ POST ไม่ต้องการให้เราตรวจสอบฐานข้อมูล สิ่งเดียวที่เราจะตรวจสอบคือรายการที่ถูกต้อง เมื่อเราดำเนินการตามคำขอ POST สำเร็จแล้ว เราก็สามารถเริ่มต้นคำขอ DELETE และ PUT ของเราได้

// Delete Task
router.delete('/tasks/:id', function(req, res, next) {
  db.tasks.remove({_id: mongojs.ObjectId(req.params.id)}, function(err, task) {
    if (err) {
      res.status(404);
      res.send(err);
    }
    res.json(task);
  });
});
// Update Task
router.put('/tasks/:id', function(req, res, next) {
  let task = req.body.task;
  let updatedTask = {};
  if (task) {
    updatedTask = task;
    //this is necessary to prevent overwriting errors
    delete updatedTask._id;
  }
  db.tasks.update({_id: mongojs.ObjectId(req.params.id)}, updatedTask, {}, function(err, task) {
    if (err) {
      res.status(404);
      res.send(err);
    }
    res.json(task);
  });
});
module.exports = router;

หากคุณสงสัยว่ารหัสเฉพาะนั้นมาจากไหน สิ่งสำคัญคือต้องทราบว่าแต่ละรายการที่เพิ่มลงในฐานข้อมูลของเราจะได้รับรหัสที่ไม่ซ้ำกัน ด้วยเหตุนี้ เราจึงสามารถค้นหางานของเราเพื่อหารหัสเฉพาะที่ตรงกับรหัสใดก็ตามที่เราส่งผ่านไปยังเส้นทางของเรา ในกรณีของคำขอลบ เราจะลบรายการใดก็ตามในฐานข้อมูลของเราที่มีรหัสที่ตรงกัน อย่างไรก็ตาม การอัปเดตรายการต้องการให้เราส่งผ่านออบเจ็กต์ที่เราต้องการให้ออบเจ็กต์ดั้งเดิมของเราอัปเดตไป สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการ mongojs โปรดดูที่ เอกสารประกอบที่นี่

และคุณก็ได้แล้ว! คุณสามารถปรับแต่งและสร้างสไตล์ของคุณเอง หรือคุณสามารถใช้สไตล์ชีตนี้:

@import url("http://netdna.bootstrapcdn.com/bootstrap/3.0.0-rc2/css/bootstrap-glyphicons.css");
body {
  background: #f1f1f1;
}
.navbar {
  background: #fff;
  margin-bottom: 20px;
}
.navbar img {
  height: 40px;
  width: 50px;
}
.search-bar {
  border-radius: 1px;
  background: #fff;
}
.search-bar .form-control {
  border-radius: 2px;
  width: 80%
}
.search-bar .btn {
  border-radius: 2px;
  border-color: #d3d3d3;
  background: #f8f8f8;
  width: 20%;
  margin-left: -1px;
}
.search-bar .btn:hover {
  border-color: #c6c6c6;
  background: #f0f0f0;
  box-shadow: 0 1px 0 rgba(0,0,0,0.10);
}
.task-player {
  margin-left: 20px;
  margin-right: 20px;
}
.task-player-details {
  background: #fff;
  padding: 8px;
  border: 0;
  box-shadow: 0 1px 2px rgba(0,0,0,.1);
  margin-top: 10px;
}
.task-list {
  background: #fff;
  padding: 8px;
  border: 0;
  box-shadow: 0 1px 2px rgba(0,0,0,.1);
}
.task-list-entry {
  font-size: 16px;
  padding: 8px;
}
.task-list-entry-title {
  font-weight: bold;
}
.task-list-entry-title:hover {
  color: #e62117;
  cursor: pointer;
}
.task-list-entry-detail {
  font-size: 12px;
  color: #999;
}
.loading {
  padding-left: 20px;
}
.media-body {
  padding-left: 20px;
}
input {
  border-radius: 5px;
}
h5 {
  border: 2px solid red;
  text-align: center;
  padding: 10px;
}

ขอบคุณสำหรับการตรวจสอบโพสต์นี้ หากคุณพบว่ามีประโยชน์ โปรดอ่านโพสต์ถัดไปเกี่ยวกับวิธีตั้งค่าแอป React แบบเต็มสแต็ก