NTUEE+ documentation

回MD首頁

起源

試著回想18歲的自己,未來的人脈往往成為促使我們選擇台大電機的原因,然而曾經我們引以為傲的人脈資產,現在卻如此凋零。
一瞥世界上知名大學,他們都擁有一個共通點:人脈網絡。哈佛大學的老爺爺願意為了甫錄取的學弟妹提點長談,史丹佛大學的前輩也不遺餘力提拔後進。相比之下,我們認為系上一直缺乏專屬平台供系友建立緊密的網路,遂使人脈日益薄弱。
近年創立的NTUEE Chain已經輔導眾多學生申請上國外一流大學,我們更希望延續EE Chain的初衷,讓這份互相傳承聯絡的心拓展到所有系友。繼承著B03~B06學長姐們的意志,我們希望這個聯絡網能成為一個整合式的社群網路,讓NTUEErs聚在一起;秉持著恢復人脈網的精神,讓NTUEE能在世界上有更大的影響力;建立一個連結電機系的共同回憶,讓系友們有專屬的家!

這並不是一個網頁養成班,沒辦法讓所有人把寫網頁的所有技能練習套精熟,主要學習內容是依照本網站的需求進行延伸,會從最基本的寫起,並漸漸加深加廣,目標寫出一個完整、讓人驚艷的網頁^_^


背景知識

環境安裝

如果有漏掉的隨時回報

  • 程式運作
    • 選擇要存放檔案的資料夾
    • ctrl+`打開terminal,輸入
    ​​​​$ git clone https://github.com/chinyi0523/NTUEE_Plus_website.git 
    ​​​​$ cd NTUEE_Plus_website
    ​​​​$ npm install
    ​​​​$ npm run install-client
    ​​​​$ npm start
    

git

一樣在vscode的terminal即可

  • 初始設定,在repo上才知道誰是誰
# 輸入使用者名稱
$ git config --global user.name "YourName"
# 輸入帳號email
$ git config --global user.email "YourMail@gmail.com"
# 查看使用者名稱
$ cat ~/.gitconfig
  • git status
    • 用git add <filename>站存
    • 用git commit -m 'commit message'存檔

  • 檔案管理(local)
    • add:把特定檔案加到暫存區
    ​​​​# 暫存所有檔案(我都是用這個)
    ​​​​$ git add . 或 git add -a 或 git add --all 
    ​​​​# 暫存特定檔案(善用*,例如*.txt)
    ​​​​$ git add <filename>
    ​​​​# 暫存所有modified的檔案(無視untrack),但建議用.gitignore
    ​​​​$ git add -u
    
    • .gitignore:裡面的檔名會被git add .無視
    • commit:存檔,commit message說明這次存檔變更了甚麼,建議英文寫(我也不知道為啥)
    ​​​​# 存檔,'commit message'換成你的簡述
    ​​​​$ git commit -m 'commit message'
    ​​​​#如果覺得commit -m太長,可以在config設定縮寫cm
    ​​​​$ git config --global alias.cm 'commit -m' #以後打git cm取代git commmit -m
    
    • 刪除和改名
    ​​​​# 變成untrack file(建議)
    ​​​​$ git rm <filename> --cached
    ​​​​# 直接從電腦刪除
    ​​​​$ git rm <filename>
    ​​​​# 改檔名(不用重新add)
    ​​​​$ git mv <oldname> <newname>
    
  • 檔案上傳(local↔remote)
    remote為遠端程式庫,當local的程式有更新就可以上傳到remote上,預設只有一個版本叫做origin(也就是clone的來源,github的網址),也可以自己設定其他遠端版本的連結
# 從origin更新程式
$ git pull
# 把local的更動上傳到origin
$ git push
  • 分支branch
    善用分支讓大家的程式不打架

    • 分支管理
    ​​​​#看目前所在的branch和local所有的branch
    ​​​​$ git branch
    ​​​​$ git branch -a #茶和local+remote所有分支
    ​​​​$ git checkout <branchname> #切換到其他branch
    ​​​​#新增branch並切換過去,注意!他會以當前branch為根源
    ​​​​$ git checkout -b <branchname>
    
    • 與遠端互動
    ​​​​# 抓遠端master
    ​​​​$ git pull origin master
    ​​​​#把local當前的branch發到remote的branch上
    ​​​​$ git push -u origin <branch name>
    ​​​​# 之後在這個branch pull、push都是對origin branch做事
    
    • merge(重要!
    1. 與master merge的規則:
      在自己的branch做事,確保城市沒炸掉後才可以merge。
      把自己的branch發到remote,發pr。
    2. 小組功能的merge:
      之後可能會讓幾個人一起開發一個功能,所以可能有一個branch開了3個小branch的情況,例如:
      Recruitment下有Recruitment-backend、Recruitment-frontend。
      方法1:git merge
    ​​​​​# 如果後端完成事情要要merge
    ​​​​​$ git checkout Recruitment
    ​​​​​$ git merge Recruitment-backend
    ​​​​​# 如果前端想要拿後端merge到Recruitment的資料
    ​​​​​$ git checkout Recruitment-frontend
    ​​​​​$ git merge Recruitment
    

    方法2:rebase

    通常用在自己的branch上,特點是保持分支樹乾淨(最終會merge成一條),
    例如Recruitment-backend開了個子branch <testAwait>

    ​​​​$ git checkout Recruitment-backend
    ​​​​$ git rebase testAwait
    

網站架構

front-end

react
state
axios

back-end

檔案架構
├── backend/routes/
    ├── api.js
    ### doc相關 ###
    ├── apidoc.json  #生成readme的rule
    ├── README.MD    #在/backend跑yarn doc生成
    ├── docTemplate/ #README的template
    ### middleware相關 ###
    ├── error/
        ├── index.js
            ├── ErrorHandler #throw它可以指定狀態碼(404,500,...)
            ├── handleError  #api.js最底下呼叫,處理所有error
            ├── dbCatch      #mongoose相關的error handling
    ├── middleware/
        ├── mail/            #寄信
        ├── validation/      #判斷req格式
        ├── fileProcess.js   #把formdata塞進req.body
    ### 主要程式碼 ###
    ├── Schemas #資料庫相關
        ### 特殊 ###
        ├── db.js            #與資料庫連線的基本設定
        ├── preload.js       #docker建好後在/跑yarn reset-db
        ├── query.js         #把req變成mongoose的query
        ### collections ###
        ├── ...
    ├── srcs    #api相關
        ├── in/
            ├── abroadinfo   #留學資訊
            ├── account
                ├── auth/    #管理員相關的帳號設定
                ├── ...
            ├── auth         #用session檢查權限
                ├── isAuth.js
                ├── isUser.js
            ├── career       #就是recruitment
            ├── column
            ├── profile_new
            ├── recommendation
            ├── study        #留學配對
        ├── out/
            ├── account      #註冊登入那些的
            ├── dashboard
            ├── forget       #忘記密碼
nodejs

obj小教學:

const obj1 = {a:1,b:2}
const {a,b} = obj1//a=1,b=2

const c = 3
const d=4
//{c:3,d:4}
const obj2 = {c,d}
express
intro

Express.js是後端Node.js的開發框架之一,框架有點像是一個工具包,開發者能夠使用 npm(Node Package Manager) 安裝所需要的工具包到專案資料夾,輕鬆完成專案的使用環境設定,讓你專注在程式碼的撰寫與實際運行上。

//index.js

//開啟app應用程式,套用express框架
const express = require('express');
const app = express();
...
//設定前後端路由(下面介紹)
app.use("/api", require("./backend/routes/api"));
app.use(express.static('./client/dist'));
...
//在http://localhost:1993監聽此應用程式
app.listen(process.env.PORT||1993,function(){
    console.log('server connect');
    console.log('port name: ',process.env.PORT||1993);
})
route

路由是指判斷應用程式如何回應用戶端對特定端點的要求,而這個特定端點是一個 URI(或路徑)與一個特定的 HTTP 要求方法(GET、POST 等)。
每一個路由可以有一或多個處理程式函式,當路由相符時,就會執行這些函式。
路由定義的結構如下:

app.METHOD(PATH, HANDLER);
//METHOD 是 HTTP 要求方法。
//PATH 是伺服器上的路徑。
//HANDLER 是當路由相符時要執行的函數,或稱中介軟體

如果想要用一個path管理很多個路由,可以用express.Router,可以想成是迷你應用程式。
例如後端的程式由api.js控管,並輸出成一個router在/api下運作。

//routes/api.js
const express = require("express");
const router = express.Router();

router.post(url1,function1);
router.post(url2,function2);
...
module.exports = router;
//index.js
...
app.use("/api", require("./routes/api"));
...
中介軟體

中介軟體函數是寫在route中的函式,他的input固定為「要求物件 (req)」、「回應物件 (res)」 和「執行下一個中介軟體(next)」,常見的形式為:

router.post(url,function(req, res, next){
    //do something
    next()
},function(req,res,next){})
  • 要求物件req (Object)
    它是http request,內含呼叫這個url時夾帶的參數。常用的有:
    • req.body (Object):
      前端發post時夾帶的data
      ​​​​​​​​//client
      ​​​​​​​​axios.post(url,{account:"b07901029",username:"huho"},config)
      ​​​​​​​​->
      ​​​​​​​​//backend
      ​​​​​​​​req.body.account //"b07901029"
      
    • req.query (Object):
      前端發get時夾帶的data
      ​​​​​​​​//client
      ​​​​​​​​GET /url?account=b07901029&username=均府
      ​​​​​​​​->
      ​​​​​​​​//backend
      ​​​​​​​​req.query.account //"b07901029"
      
    • req.protocol+"://"+req.get('host')+"/someURL"
      拿到client的網址,考慮到上架後的網址不再是localhost,要這樣才能拿到最正確的網址,當然網路上也一定查的到其他方法。
  • 回應物件res (Object)
    當獲得http request後,express就會發出http response。
    • res.header
      設定http參數
    • res.status(httpCode).send(obj or string)
      回傳資料給前端。
      http code
      成功通常是用200或201
      失敗通常是4xx或5xx
    • res.status(httpCode).end()
      和send差不多,但不回傳資料。
  • 下一個中介軟體(next) (function)
    在route中可以執行很多個funciton,用
    ​​​​router.METHOD(url,
    ​​​​    function(req,res,next){},
    ​​​​    function(req,res,next){},...)
    
    預設會執行第一個函式,當第一個函式呼叫了next(),就會執行第二個函數。
    一般來說如果router呼叫同一個url兩次,則只有第一個(在檔案比較前面的)會被執行,但如果呼叫next('route'),就會切出這個route執行第二個route
    ​​​​router.post('/login',function(req,res,next){
    ​​​​    next('route');
    ​​​​},function(req,res,next){
    ​​​​    console.log('我不會被執行')
    ​​​​})
    ​​​​router.post('/login',function(req,res,next){
    ​​​​    console.log('我會被執行')
    ​​​​})
    
    善用中介軟體可以把許多路由都用到的function寫在外面呼叫,讓程式更簡潔。

一些express常見的應用

  • router.use(function)
    強制執行所有路由都要經過他(好像要注意priority?)
    通常會用來設定參數(例如index.js中的session或urlencoded或res header),或執行一些大家統一要用的參數。

    • session
    • urlencoded、json
  • Auth
    驗證使用者是否有登入

    ​​​​//api.js
    ​​​​const Auth = require('/in/Auth');
    ​​​​axios.post(url,Auth,...);
    ​​​​
    ​​​​//Auth.js
    ​​​​module.export = funciton(req,res,next){
    ​​​​    if(req.session){
    ​​​​        next()
    ​​​​    }else{
    ​​​​        //開發階段這裡呼叫next();
    ​​​​        res.status(403).send({description:"請登入"});    
    ​​​​    }
    ​​​​}
    
  • multer
    專門處理檔案上傳,在/register、/chVisual、/getImg有應用在照片處理
    不想深入研究的話就直接套用:

    1. 存單張照片
    ​​​​//client
    ​​​​let data = new FormData();
    ​​​​data.append('avatar',this.state.file);
    ​​​​//用append設定其他要post的資料,下面介紹更多關於formData的注意事項
    ​​​​const config = {headers:
    ​​​​    {'content-type':'multipart/form-data'}
    ​};
    ​​​​axios.post(url,data,config);
    ​​​​->
    ​​​​//backend
    ​​​​const parseFile = require('./middleware/multer.js');
    ​​​​router.post(url,parseFile('avatar'),...); 
    ​​​​//ImgGet裡輸入要讀取的檔案的key
    ​​​​//之後的function就可以在req.file裡拿到{buffer,mimetype(img/png之類的)}
    
    • formData注意事項
    ​​​​//frontend
    ​​​​//要傳object時請用json
    ​​​​let data = new FormData();
    ​​​​const obj = {data:"b07901029",show:true}
    ​​​​data.append("account",JSON.stringify(obj))
    ​​​​//要傳array key請加[]
    ​​​​const arr = ["a","b","c"]
    ​​​​arr.forEach(item=>{
    ​​​​    //如果item是obj請參考上面解法
    ​​​​    data.append("fakeArr[]",item)
    ​​​​})
    ​​​​->
    ​​​​//backend
    ​​​​const {data,show} = req.body["account"] //{"b07901029",true}
    ​​​​//用forEach拿array
    ​​​​req.body["fakeArr"].forEach(item=>{
    ​​​​    console.log(item);
    ​​​​    //"a"\n"b"\n"c"
    ​​​​})
    
    1. 若要讀取多個檔案請仿照middleware/multer.js,但upload.single改成upload.array。
    • 可以參考study/runMatch/parseExcel
    1. 拿單張照片
    ​​​​//backend
    ​​​​//這裡req.file應該變成我們存放mimetype和buffer的地方
    ​​​​const prefix="data:"+req.file.mimetype+";base64,"
    ​​​​const img = new Buffer(req.file.buffer, 'binary').toString('base64');
    ​​​​const userimage = prefix+img;
    ​​​​res.send({userimage})
    ​​​​->
    ​​​​//client
    ​​​​//拿到的userimage用this.setState({userimage}),然後可直接設成<img>的value:
    ​​​​<img value={this.state.userimage}>
    
  • validation
    在執行主程式前用express-validator驗證資料格式,目的是不要讓使用者傳奇怪的資料到後端

    ​​​​//基本架構:
    ​​​​const {check} = require('express-validator')
    ​​​​router.post(url,[check(key1),check(key2),...],authController,function,...)
    
  • express-validator中有三種常用函式:body、cookie、check,分別會檢驗req.body、req.cookies、req.{body,cookies,header,param,query}下指定key對應的value有沒有符合格式。

  • 若沒有符合格式,消息會被存在req中並呼叫下一個函式:authController,express-validator下還有validationResult這個函式,用validationResult(req)可以抓到錯誤並回傳res.status(400)。

  • 以下是我們的版本(以login為例),require validation,然後指定要檢查的欄位

const valid = require('../../../middleware/validation')
const rules = ['account', 'password']
module.exports = [valid(rules), asyncHandler(login)]
mongoDB and mongoose
intro

mongoDB是一個文件導向的資料庫(database),一個資料庫底下可以有多個集合(collections),每個集合下有多個文檔(documentions)。
以我們的專案為例,我們的database存放在mongoDB online上,名叫eeplus。當中有以下collections:

user_logins
user_visuals
columns
...

而user_logins有數個documentation,格式類似json

{_id:ObjectID(b6e5rb48e6), account:"b07901029", userpsw:"f2hoifpew"},
{_id:ObjectID(ber561r65b), account:"b07901014", userpsw:"vewoivnro"},
...

mongoose是nodejs中用來與mongoDB溝通的套件。

  • 基本設定:連線與監聽
//routes/Schema/db.js
const mongoose = require('mongoose');
mongoose.connect(DB_URL);
//form like mongodb://<user>:<psw>@host:port/dbname
const db = mongoose.connection(); //背景監聽
db.on('disconnected', function(){
    console.log('disconnected!');
});
  • 模型(model)與綱要(schema)
    一個model對應到mongoDB中的一個collection,透過schema定義格式。我們可以透過model進行doc的創建、查找、更新、刪除。
    ​​​​const mongoose = require("./db")
    ​​​​const Schema = mongoose.Schema;
    
    ​​​​//在此設定model的type、驗證、虛擬屬性(我沒用過,感覺很好玩!)
    ​​​​const User_login_Schema = new Schema({
    ​​​​  //透過:TYPE設定類型
    ​​​​  username: String,
    ​​​​  //也可以規定資料的內容,例如enum:['dog','cat']限定內容選項
    ​​​​  account: {type:String, require:True},
    ​​​​  ...,
    ​​​​  //可以設定多層的資料,也可以設定[]
    ​​​​  img: {
    ​​​​    data: { type: Buffer },
    ​​​​    contentType: { type: String }
    ​​​​  }
    ​​​​});
    
    ​​​​//把schema編譯成model,之後就可以對model進行操作
    ​​​​//之後save的檔案會存在user_logins(好像會自動加s)這個collection中
    ​​​​module.exports = mongoose.model("User_login", User_login_Schema);
    
    • schema tips
      1. type and some attribute
      2. population:{_id(docID),ref(collectionName)},與其他collecton連結
      3. instance method(doc's function)。針對單一doc做事,用this拿到該doc的屬性。
        ​​​​​​​​​​​​  // define a schema
        ​​​​​​​​​​​​  const animalSchema = new Schema({ name: String, type: String });
        
        ​​​​​​​​​​​​  // assign a function to the "methods" object of our animalSchema
        ​​​​​​​​​​​​  animalSchema.methods.findSimilarTypes = function(cb) {
        ​​​​​​​​​​​​    return mongoose.model('Animal').find({ type: this.type }, cb);
        ​​​​​​​​​​​​  };
        
      4. static(model's function)。自訂model相關函式,用this拿到該model的屬性(例如.find)。
        ​​​​​​​​​​​​  animalSchema.statics.findByName = function(name) {
        ​​​​​​​​​​​​    return this.find({ name: new RegExp(name, 'i') });
        ​​​​​​​​​​​​  };
        
      5. virtual type:可以根據doc自訂回傳格式(例如doc → res.json),不會存入資料庫。有getter和setter屬性。
  • model usage
    • insert
      ​​​​​​​​const doc = await new Login({
      ​​​​​​​​    account:"b07901029"
      ​​​​​​​​}).save().catch(dbCatch)//throw error if error occur
      
    • find
      query and selector rule
      ​​​​​​​​//只找一個
      ​​​​​​​​const doc = await Login
      ​​​​​​​​    .findOne(query, selector)
      ​​​​​​​​    .catch(err=>{})
      ​​​​​​​​//找多個
      ​​​​​​​​const docs = await Login
      ​​​​​​​​    .find(query,selector)
      ​​​​​​​​    .catch(err=>{})
      
    • update
      update rule
      ​​​​​​​​const {updateQuery} = require(../Schemas/query)
      ​​​​​​​​//updateQuery會把特定的object轉成mongoose看得懂的格式
      ​​​​​​​​//簡單說就是依據value是不是空字串,把value塞進{$set,$unset}
      ​​​​​​​​
      ​​​​​​​​const {title} = req.body
      ​​​​​​​​const update = updateQuery({'title.title':title})
      ​​​​​​​​//update = {$set:{'title.title':title}}
      ​​​​​​​​const {n,nModified} = await Recruitment
      ​​​​​​​​    .updateOne(query,update,option)
      ​​​​​​​​    .catch(err=>{})
      ​​​​​​​​/*{
      ​​​​​​​​    n, //number of matched doc
      ​​​​​​​​    nModified //number of modified doc
      ​​​​​​​​}*/
      
    • delete
      ​​​​​​​​loginModel.deleteMany(query,function(err){});
      

其他

res.send

  • http code
  • 可以用throw ErrorHandler(404,'any msg u want')觸發error router

api doc

  • 在/backend跑yarn doc根據每個檔案的註解生成README

References

Select a repo