# 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