---
# System prepended metadata

title: NodeJS(Max)第 7 節： The Model View Controller (MVC)
tags: [Node.js, Udemy]

---

# NodeJS(Max)第 7 節： The Model View Controller (MVC)

> Udemy課程：[NodeJS - The Complete Guide (MVC, REST APIs, GraphQL, Deno)
](https://www.udemy.com/course/nodejs-the-complete-guide/)

`20231211Mon.~20231214Thu.`

## 7-97. What is the MVC?
* Models:
    * Represnet your data in your code
    * Work with your data(eg. save, fetch)
* Views:
    * What the user sees
    * Decoupled from your applucation code
* Controllers:（主要就是**router**）
    * Connecting your Modles and your Views
    * Contains the "in-between" logic

![image](https://hackmd.io/_uploads/BJY2OZ4L6.png)

****

## 7-98. Adding Controllers
接著要把controller相關的程式碼給獨立出來，首先在專案根目錄底下建立資料夾"controllers"。

![2023-12-11 12-14-46 的螢幕擷圖](https://hackmd.io/_uploads/H1pT3WEUp.png)


至於controllers中想要如何分配檔案，是沒有一定規則的。例如說我可以就目前的routes去區分成兩個controllers admin跟shop，但我也可以拆成products來放所有跟商品有關的程式碼，再一個來放user相關logic的檔案等等，他是沒有標準答案的。

那這裡就照著老師的作法，先在"controllers"資料夾底下建立一個檔案叫做`products.js`，用來放一些跟product有關的logic。

**▎原先的admin.js**
![2023-12-11 12-13-59 的螢幕擷圖](https://hackmd.io/_uploads/ryIlAZEIp.png)

接著把其中的middleware function移至controllers底下統一管理。
![image](https://hackmd.io/_uploads/BJQ5gGNLp.png)


**▎利用controller的admin.js**
**products.js > 建立controller**
```javascript!
exports.getAddProduct = (req, res, next) => {
    res.render("add-product", {
        pageTitle: "Add Product",
        path: "/admin/add-product"
    })
}
```
**admin.js > 使用controller**
```javascript!
...略...
const productsController = require("../controllers/products");
// /admin/add-product => GET
router.get('/add-product', productsController.getAddProduct);
...略...
```
![image](https://hackmd.io/_uploads/rJxy3JfV86.png)

****

## 7-100. Adding a Product Model
再來要建立MVC中的models，我們先建立資料夾名為"models"，底下建立一個"product.js"的檔案，用來處理product邏輯。

**product.js**
```javascript!
const products = []; //暫時用變數取代資料庫

module.export = class Product{
    constructor(title){
        this.t = title;
    }

    save(){
        products.push(this);
    }
    static fetchAll(){
        return products;
    }
}
```

### **▊ static**
```javascript!
static fetchAll(){
    return products;
}
```

其中static的那行不太理解。

static老師的解釋：「I will add the static keyword which JS offers, which make sure that I can can call this method directly on the class ifself and not on an instantiated object。」
看起來static必須直接由class來調用，而不是由物件建立出來的實例來使用。因此有statice關鍵自為首的話，可以通過class來調用這些屬性或函式，而不需要先創建class的實例對象。

所以如果在static函式中使用this，this指向的會是class name，而非實例物件（instance）。

再看這篇文章[JavaScript static 关键字是干嘛的？](https://juejin.cn/post/6940556583482949663)，看他比較有用static以及沒用static函式的差異，我覺得在這裡`fectchAll()`會用static的原因，應該是因為我想要直接看到整個products陣列裡面有什麼東西，所以我不應該是透過class的實例來查看，而是直接從class來查看products陣列變數。


### **▊ 完成**

**▎models/product.js**
```javascript!
const products = []; //暫時用變數取代資料庫

module.exports = class Product{
    constructor(title){
        this.t = title;
    }

    save(){
        products.push(this);
    }
    static fetchAll(){
        return products;
    }
}
```

**▎controllers/product.js**
```javascript!
const Product = require("../models/product");

exports.getAddProduct = (req, res, next) => {
    res.render("add-product", {
        pageTitle: "Add Product",
        path: "/admin/add-product"
    })
}

exports.postAddProduct = (req, res, next) => {
    const product = new Product(req.body.title); //送出表單 > 發送POST request > 建立Product實例
    product.save(); // save()會把this（也就是自己，product）推進products陣列中
    res.redirect('/');
}

exports.getProducts = (req, res, next) => {
    const products = Product.fetchAll();
    res.render("shop", {
        prods: products,
        pageTitle: "shop", path: "/"
    });
}
```

## 7-101. Storing Data in Files Via the Model
接著要把data利用model存到一個檔案中，而非像先前只是存在變數裡。

**▎models/product.js**

看Product class其中的save function，這是用來儲存資料到一個檔案中的方法。
```javascript!
const fs = require("fs");
const path = require("path");

module.exports = class Product{
...略...
    save(){
        const p = path.join(path.dirname(require.main.filename), "data", "product.json");
        fs.readFile(p, (err, fileContent) => {
            let products = [];
            if(!err){
                products = JSON.parse(fileContent);
            }
            products.push(this);
            fs.writeFile(p, JSON.stringify(products), err => {
                console.log(err);
            });
        })
    }
...略...
}
```

### **▊ fs.readFile(file[, options], callback)**
> 參考資料：[Node.js官方文件](https://nodejs.org/dist/latest-v6.x/docs/api/fs.html#fs_fs_readfile_file_options_callback)

* file：給定要讀取的file位置。
* [option]：可不設定，預設為不encoding。
* callback：接收兩個參數err跟data，其中data代表file的內容。

```javascript!
fs.readFile(p, (err, fileContent) => {
            let products = [];
            if(!err){
                products = JSON.parse(fileContent);
            }
})
```

### **▊ fs.writeFile(file, data[, options], callback)**
> 參考資料：[Node.js官方文件](https://nodejs.org/dist/latest-v6.x/docs/api/fs.html#fs_fs_writefile_file_data_options_callback)

* file：給定file的位置，作為要將data寫進去的地方。
* data：要寫進file的內容，可以是string或是buffer。
* [option]：可不設定，預設為encode成"utf8"。
* callback：接收一個參數err，用來處理接error的方法。

```javascript!
fs.writeFile(p, JSON.stringify(products), err => {
    console.log(err);
});
```

### **▊ JSON.parse()**
會把一個JSON 字串轉換成JavaScript 的數值或是物件。

### **▊ JSON.stringify()**
將一個JavaScript 物件或值轉換為JSON 字串。

****

## 7-102. Fetching Data from Files Via the Model

> 參考資料：[JavaScript 中的同步與非同步（上）：先成為 callback 大師吧！](https://blog.huli.tw/2019/10/04/javascript-async-sync-and-callback/)

接著要把data利用model從一個檔案中取得（fetching data）。

我們可能會想著要照前一章節的寫法寫，所以寫出下圖的code，但這之中有個問題，就是下圖code寫法會照成error。
![2023-12-13 18-24-08 的螢幕擷圖](https://hackmd.io/_uploads/rJBDUWw8T.png)
![2023-12-13 17-56-07 的螢幕擷圖](https://hackmd.io/_uploads/Bk8Gx-PUp.png)

原因在於`fs.readfile()`屬於asynchronous code（非同步）。
![image](https://hackmd.io/_uploads/BkVZpQsEa.png)

以下是我自己的想法，如有錯誤歡迎指正！

1. 首先，fetchAll()被呼叫，被呼叫的函式會被放入stack中，直到函式執行結束，或是return值
![image](https://hackmd.io/_uploads/Syro3Zw8a.png)

2. 依序執行下來，所以先宣告p變數
![image](https://hackmd.io/_uploads/H1BR3bPUT.png)

3. fs.readFile()被呼叫，但他是非同步的，所以我們不會等他讀取完檔案，而是繼續往下一行看下去。

    於是readFile()被呼叫後放入stack中，他讀取檔案的工作會轉交給nodeapi處理，轉交工作後readFile()沒事了就可以pop出stack。
    
    readFile()與setTimeout()相似，皆為非同步的函式，而setTimeout()呼叫後會將計時工作轉交給webAPI處理。
    ![image](https://hackmd.io/_uploads/SJb4AWwIT.png)

4. 碰到了右括號，fetchAll()函式結束，因此可以pop出stack，但是...完全沒有return任何值，就有點像進去菜市場(stack)逛一圈，結果沒買到東西，就出來了(pop out)。

    即便等nodeAPI讀取完檔案，return了值，fetchAll()也永遠拿不到了（因為他已經離開菜市場）

    而這也是為什麼這樣的寫法會造成我們前面得到的error:
    ![2023-12-13 17-56-07 的螢幕擷圖](https://hackmd.io/_uploads/BkbCAZDL6.png)

    ![image](https://hackmd.io/_uploads/r1i0CZvIp.png)

～～～～～接下來看看正確的寫法！～～～～～

這裡會在fetchAll()運用到callback function，讓callback function 幫忙hold住後，並做render()。

![image](https://hackmd.io/_uploads/HylM7fPUp.png)


**▎models/product.js**

看Product class其中的static fetchAll function，這是用來從某個檔案取得資料的方法。
```javascript!
const fs = require("fs");
const path = require("path");

module.exports = class Product{
...略...
    
    static fetchAll(cb){
        const p = path.join(path.dirname(require.main.filename), "data", "product.json");
        fs.readFile(p, (err, fileContent) => {
            if(err){ //有err代表讀取不到p，可能是還未建立product.json
                cb([]);
            }
            cb(JSON.parse(fileContent));
        })
    }

...略...
}
```

**▎controllers/products.js**
```javascript!
...略...

exports.getProducts = (req, res, next) => {
    Product.fetchAll((products) => {
        res.render("shop", {
            prods: products,
            pageTitle: "shop", path: "/"
        });
    });
}

...略...
```

****

More on MVC: https://developer.mozilla.org/en-US/docs/Glossary/MVC