React 與 Webpack4 專案建置全實戰 (3)

這篇文章應該是這個系列的最後一篇,之後如果有用到的話還是會慢慢分享 webpack 相關的設定方式。

由於使用 React 通常會搭配 JSX 語法來做開發,而這樣的撰寫方式並不能直接被瀏覽器所解析,所以我們就會需要用到 Babel 來幫助我們轉譯成瀏覽器看得懂的 JS 語法。 因此這篇文章主要會解說如何在 Webpack 裡面 Babel 相關的設定,應該不會太長,讓我們繼續看下去吧!

安裝及設定 babel-loader

前幾章節有說過 loader 是一系列用來轉譯的套件,所以要在 webpack 裡面使用 babel 我們也必須使用 babel-loader,參考 Github 上的安裝指令:

npm install -D babel-loader @babel/core @babel/preset-env webpack

其中 @babel/core 是babel 的主要部分,而 preset-env 是針對那些 JS 最新公佈但瀏覽器還沒有支援的語法做轉譯。

然後只要在 module 裡面加入以下程式碼,就可以開始設定跟使用 babel 囉:

module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

其中 option 內的 presets 是為了告訴babel想要用什麼模組去做轉譯,babel 把可能的轉換整理成一系列的 preset ,當然也包含 react 的preset 啦,等等就會看到。

安裝 babel-preset-react

如上提到的,babel 也提供了 react 轉譯相關的模組,在 webpack 的設定方式也不難,先安裝:

npm install --save-dev @babel/preset-react

然後ㄧ樣在 preset 裡面加上 ‘@babel/preset-react’, webpack 就會自動套用到啦,然後因為這個 preset也包含 JSX 的轉譯,所以就不用另外安裝相關模組了。到這邊你應該可以開始使用 react 跟 jsx 來做開發了,後面會陸續分享 webpack 相關,比較細節的其他設定,請大家耐心等候囉。

延伸文章

Vue.js 井字遊戲 - OOXX

大家好,我在地下城8樓,題目是3x3井字遊戲,直覺想到最簡單的方式就是暴力檢查,每個Row跟Column去檢查有沒有相等的,最後加上對角線兩條。也看到別樓有分享Alex1&10解法,覺得很高明。

不過我想看看有沒有不一樣的做法,相信一定有。剛剛講到Row跟Column,就讓我想到二維陣列。沒錯,整個井字窗格根本可以用座標來看,今天這個題目的解法會以二維陣列為主軸來解說作法,切版的部分則比較繁瑣且不難所以不會多提,有問題可以再跟我討論。

你可以在這裡看到我的成果,或是參考原始碼

規則

  • 先手為 O,後手為 X,某方獲勝時,上方會紀錄各方的獲勝戰績

  • 每回合結束後,會判定結果頁(平手、O獲勝、X獲勝)

  • 需符合 RWD,能在低螢幕解析度也能遊玩,介面不能超出 x 軸,至少在以下解析度能夠遊玩:

    1. iPhone SE 320px
    2. iPhone 8 375px
    3. iPhone PLUS 414px
  • 請使用瀏覽器離線儲存技術,將戰績保留起來

資料結構

為了讓對應到畫面的資料可以更直觀的操作跟查詢,我是這樣訂定格式的:

data(){
    return {
    ...
     currentPlayer:0,
     gameStatus:[[null,null,null],
                 [null,null,null],   
                 [null,null,null]]
            }}

陣列第0個元素代表的是第一個Row,Row本身也是陣列,剛好由左到右對應到0、1、2的位置,(0,0)對應到最左上,(2,2)對應到最右下,這樣直觀多了,如下圖所示:

然後用0表示O、1表示X,與上述資料結構的currentPlayer一樣,null則表示尚未下棋的位置,不過這邊不建議用0來做任何的判斷,原因稍後再提。

判斷勝利者

因為我用的資料表示是: O為0,X為1,因此如果整列、整行或交叉相加都是0,或是3,代表有人勝出,這時我只要取得currentPlayer就知道獲勝的是誰了。

判斷勝利邏輯

要判斷目前賽況是否有人勝出,我們需要檢查的部分有:

  • 每個Row(橫排)是否有連線
  • 每個Column(直行)是否有連線
  • 對角線是否有連線

判斷對角線的部分比較麻煩,但你應該可以看出,使用陣列的方式可以清楚區分出Row跟Column,讓檢查更方便,檢查的時間點是每次使用者點完要下的棋格之後的瞬間,檢查完最後切換使用者(玩家)。

methods:{
...
onSectionClick(x,y){
         if (this.gameStatus[y][x]!==null) return //防止重複下棋
         this.gameStatus[y][x] = this.currentPlayer//下棋
         this.checkResult()//檢查賽況
         this.togglePlayer()//<---下完後切換使用者

       },}

因為是二維陣列所以要兩層for迴圈,注意這邊因為要先取用Row,所以陣列的操作必須先從y (也就是直排) 開始選取,而不是一般座標的表示方式(x,y),當然可以透過改變資料結構做出比較直觀的使用方式,不過因為沒有很困難,且為了解說方便,這邊就不做調整。

  let status = this.gameStatus
  for(let y=0; y<status.length; y++){
    for(let x=0; x<status[y].length; x++ ){
      // 用status[y][x]取得棋格元素 <-----重要
    }
   }
   

知道取得陣列的方式之後就可以開始逐行逐列檢查啦!

檢查Row / Column是否有連線

想要取得Row整行的和,每次的y必須固定,我是在x為0的時候,取得後面兩個x+1、x+2元素,相加後算出總和,。

methods:{
...
getSumOfRow(x,y){
            let status = this.gameStatus
            if(x!==0 ) return 
             return status[y][x]+
                       status[y][x+1]+
                       status[y][x+2]
        },}

Coloum總和則為x y與上述相反:

 getSumOfColumn(x,y){
            let status = this.gameStatus
            if(y!==0 ) return 
            return   status[y][x]+
                     status[y+1][x]+
                     status[y+2][x]             
        },

取得總合後判斷是否有0或是3就知道有無勝出。

檢查對角線是否有連線

一樣是在迴圈一開始x為0以及y為0的時候做判斷,在取得總和之前,我先看看是不是整個對角線都已經被下過棋了,如果沒有,則直接return 跳出不做任何動作。

methods:{
getSumOfNegativeSlash(x,y){
            let status = this.gameStatus
            if(x!==0 || y!==0 ) return 
            if(  !(this.isNumber(status[y][x]) && 
                  this.isNumber(status[y+1][x+1]) &&
                  this.isNumber(status[y+2][x+2]))) return 
                        
            return   status[y][x]+ status[y+1][x+1]+status[y+2][x+2]
        },
        
}

另一個方向的對角線是同樣的做法,看到這裡的你應該可以推算出邏輯,如果還是不懂,可以參考文章開頭處的專案原始碼。

整合所有的判斷邏輯 - checkResult method

最後整個判斷邏輯如下,我把三種情況(Row / Column / 對腳線)個別寫下條件之後用OR串起來,只要狀況符合其中一種條件,就會結束賽局。

         checkResult(){
            
            let status = this.gameStatus
           for(let y=0; y<status.length; y++){
               for(let x=0; x<status[y].length; x++ ){
                   let sumOfCol = this.getSumOfColumn(x,y) 
                   let sumOfRow = this.getSumOfRow(x,y) 
                   if(
                        (   this.isRowAllNumber(x,y) && 
                            sumOfRow=== 0||sumOfRow=== 3
                        )||(
                            this.isColumnAllNumber(x,y) && 
                            sumOfCol === 0||sumOfCol === 3
                        )||(
                           this.getSumOfNegativeSlash(x,y)===0 || 
                           this.getSumOfNegativeSlash(x,y)===3 || 
                           this.getSumOfPositiveSlash(x,y)===0 || 
                           this.getSumOfPositiveSlash(x,y)===3
                        )){
                      this.setScore()  
                      this.goResultPage(this.currentPlayer)
                    }
               }
           }
           this.checkIfFinish()
        },

這邊有一個蠻麻煩的雷點就是js裡面null+0居然還是0,害我沒辦法直接正確判斷結果,導致我還寫了一個isRowAllNumber來判斷該列/欄是否全是數字,所以不推薦用0跟null搭配來當做相加的判斷。(你可以打開開發者工具試試看)

使用localStorage紀錄分數

這邊我使用localStorage來永久儲存比數,localStorage操作方式有以下幾種:

  • localStorage.setItem(‘key’,‘content’)
  • localStorage.getItem(‘key’)
  • localStorage.removeItem(‘key’)

瀏覽器上有另外一個操作方式很類似的api叫做sessionStorage,差別就在於localStorage除非主動刪除,否則會永久保留在瀏覽器、sessionStorage則是在分頁關閉後就會被刪除。

我把上面三個api另外包成三個function,讓我在各個元件可以方便使用,統一使用方式,以下是setItem的包裝:

export const setStorage = (name, content) => {
    if (!name) return;
    if (typeof content !== "string") {
      content = JSON.stringify(content);
    }
    window.localStorage.setItem(name, content);
  };

getStorage、removeStorage則是類似的方式,在此就不多提。

多思考一些 - 重置分數按鈕

這邊思考的是,既然可以儲存戰績,那如果可以讓玩家手動清除分數,應該會更好用,很多遊戲也有這種功能。所以我將原本入口的START按鈕改為CONTINUE跟RESTART,如果兩邊分數為0才只顯示START。

雖然是小地方但可以增加樂趣,算是一點小優化,給有興趣的人參考。

Vue.js小遊戲實作-60秒算數挑戰

大家好,我在六角地下城6f,這樓的挑戰是實作小遊戲,題目的邏輯是在六十秒內會產生隨機兩個數字去隨機做加減乘除, 答對就加一分,答錯就扣一分,秒數越少,題目越困難,得分就越高分。

題目詳細規則如下:

  • 0~20 秒為 1位數計算 (ex. 5-3)
  • 21~40 秒為 2 位數計算 (ex. 30*19)
  • 41~60 秒為 3 位數計算 (ex. 332+312)
  • 加減乘除規則請用隨機產生,不可寫死題目,60 秒內可無限次數答題。
  • 0~40 秒答對加一分
  • 41~60 秒答對加五分
  • 答錯扣一分,最多僅能扣到零分

我打算在這個專案做邏輯梳理的練習,因此會盡量把有機會重複使用的部分拆成methods,這個專案只會講述核心部分的邏輯,也就是如何產生題目,以及一些可能的問題點,其他細節部分希望你自己做做看,你可以在這裡看到我實做的成果。或是也可以直接參考原始碼

規則流程-運算:

  1. 產生隨機兩個數字
  2. 產生運算符號(加減乘除)
  3. 等待使用者輸入
  4. 檢查正確答案與使用者輸入是否符合
  5. 產生下一道題目

規則流程-時間:

  1. 挑戰者按下開始後,切換頁面並開始倒數
  2. 倒數60秒
  3. 檢查現在秒數,根據不同秒數決定不同難度的題目及得分
  4. 60秒倒數完畢後,切換顯示頁面,不讓挑戰者繼續輸入
  5. 挑戰者查看得分,等待按下「再次挑戰」後重新此流程

需特別注意的規則細節:

因為數字是隨機產生,因此在某些情況下要注意,否則會有負數產生:

  1. 減法時,會有前後數字大小的問題
  2. 除法時,會有數字大小以及除不盡的問題
  3. 最後挑戰者重新開始時記得clearInterval

專案實作 - 基本配置

這個專案我是使用vue-cli,因為規模並不是很大,也不能跳頁,因此不打算另外切出Component。

資料結構的部分,因為會有重置資料的需求,我把原本習慣寫在data()裡面的資料拉出來,放在獨立的function回傳,這樣我就可以輕易的拿到預設的資料。

// outside Vue instance
function initData(){
    return {
     pages:['start','game','result'],
     operatorList:['+','-','x','÷'],
    operator:'+',
    currentPage:'start',
    score: '000',
    time_remain:60,
    currentTime:'00:00',
    quizNumbers:[],    }
}

// inside Vue instance
export default {
  name: 'app',
  data(){
    return initData()
  }
}

已知狀態有三種,因此我直接根據不同狀態去顯示不同頁面,這邊寫法可以依照自己習慣去更改:

 <div :class="pages[0]" v-if="currentPage==pages[0]"  >
 ...
 </div>
 
  <div :class="pages[1]" v-if="currentPage==pages[1]" >
  ...
  </div>

  <div :class="pages[2]" v-if="currentPage==pages[2]" >
    ...
  </div>

專案實作 - 邏輯實現

倒數方法 - 使用setTimeInterval

要能夠時線倒數,代表必須每隔一秒改變畫面上的時間值,這裡用setTimeInterval最適合不過了,他會每隔一段時間去執行寫入的方法:

methods:{
    ...
     timeReducer(){
         let timer =  setInterval(()=>{
          if( this.time_remain>0){
            this.currentTime = this.convertSeconds(this.time_remain -=1 )
          }else{
            clearInterval(timer)
            this.currentPage= this.pages[2]
          }
        }, 1000)
        return timer
    },
    ...
}

但在使用完後記得要清除這個interval,因為他會一直執行,如果沒有清除,在挑戰者再次開始遊戲時,如果沒有清楚,就會有兩個interval重疊,遊戲就無法順利進行,這邊我是在倒數完之後在setTimeInterval裡面直接使用clearInterval清除。

隨機產生運算子

因為加減乘除運算子必須隨機產生,所以我將這四種運算方式寫在陣列裡面(operatorList),然後用JS的random方法去隨機挑選這個陣列的元素:


methods:{
    ...
    randomOperator(){
      let order = Math.floor(Math.random()*100) % 4 
      this.operator = this.operatorList[order]
    }
    ...
}

取得不同位數的數字

依照上面整理的規則,我們第一個會需要處理的部份是隨機產生數字,JS裡面可以用Math.random() 去解決,但要注意這個方法回傳的值是介於0~1之間的浮點數,後面要自己去調整才能拿到自己想要的格式。

以及要考量到可能會有需要拿不同位數數字的需求,所以在這個method我用digits當作參數來表示欲取得的位數:

 // inside Vue instance 
 ...methods:{
     ...
     getDigits(digits){
      switch(digits){
        case 1 : return Math.floor((Math.random() * 10) + 1) ; 
        case 2 : return Math.floor((Math.random() * 100) + 10) ; 
        case 3 : return Math.floor((Math.random() * 100) + 100) ; 
      }
    },
 } 

取得數字之後的處理

加上setQuizNumber方法,根據不同時間取得不同位數的數字:

    //setQuizNumber
    let digit;
      switch (true){
        case (this.time_remain>=40 && this.time_remain<=60): {
              digit = 1
              break; 
            }
        case (this.time_remain>=20 && this.time_remain<=40): {
              digit = 2
              break;
            }
        case (this.time_remain>=0 && this.time_remain<=20): {
              digit = 3
            break;
        }    
      }
      
      let result = []
      let firstNum=this.getDigits(digit); 
      let secondNum=this.getDigits(digit); 

我們不想要答案有負數或小數點產生,所以在減法時,必須注意第一個數字是否大於第二個,至於加法跟乘法就沒有這個問題,所以我加上判斷,如果第一個數大於第二個,就做swap交換:

    //setQuizNumber
      if(firstNum < secondNum){
        let temp = firstNum
        firstNum = secondNum 
        secondNum = temp 
      }

最後為了在除法時,讓兩數是可以整除的,所以必須確保第二個數字是第一個數字的因數,以及不能有質數出現,我加入的判斷質數的方法isPrime跟取得數字所有因數的方法getFactors,以及後面會有從因數裡面雖機挑選一個數字的需求,所已加入陣列隨機挑選元素的方法getArrayRandomItem:

methods:{
    ...
    isPrime(num) {
      for(var i = 2; i < num; i++)
        if(num % i === 0) return false;
      return num > 1;
    },
    getFactors(number,digits){
      let factors =  Array
        .from(Array(number + 1), (_, i) => i)
        .filter(i => number % i === 0)

      if(digits){
        factors = factors.filter(factor=>(factor+'').length===digits)
      }
        return factors
    },
    getArrayRandomItem(array){
      return  array[Math.floor(Math.random()*array.length)];
    },
    ... 
}

最後過濾掉質數後,從第一個數字的因數(陣列)裡面去隨機選出一個數當作第二個數字,就可以送出了:

 //setQuizNumber
 if(this.operator=='÷' && ( firstNum % secondNum!==0)){
        while(this.isPrime(firstNum)) firstNum = this.getDigits(digit); 
        while(this.isPrime(secondNum)) secondNum = this.getDigits(digit); 
        
        secondNum = this.getArrayRandomItem(this.getFactors(firstNum,digit))
        
      } 
      result = result.concat([firstNum,secondNum]) 
      
      this.quizNumbers = result

做到這裡產生題目的邏輯就完成了。

再次挑戰- 重置所有資料

還記得我一開始把原始狀態拉出來單獨放在function裡嗎?現在就可以拿來用了,使用Object.assign,可以直接對目標物件赴值,如果屬性重複,以參數越後面屬性的值為主(後面寫入的會蓋掉前面的),可以參考 官方文件,順帶一提Object.assign不支援深度拷貝,如果你要複製的物件裡面還要物件,要特別注意,他只會複製那個物件的參考,後面你只要改動到該子物件,則所有該物件的參考都會跟著一起被改變。(參考官方文件

methods:{
    ...
    onResetClick(){
      Object.assign(this.$data, initData());
      this.currentPage = this.pages[0]
    },
    ... 
}

this.$data是Vue裡面提供取得data物件的api,詳細可參考官方文件
到這邊所有核心的邏輯都講完了,再提一次我並沒有從頭到尾講得很詳細,希望你試著自己去理清楚來龍去脈,自己識做看看,若還是看不懂也沒關係,可以參考我的原始碼,在自己做做看,有任何問題可以一起討論,感謝你的收看,下次見啦。

Vue.js + Node.js + OpenAPI 帶你一次了解 CORS 跨域請求

本篇文章是我在六角學院地下城活動5F關卡的實作紀錄,會描述如何架設簡單的Proxy Server去跟政府的公開API拉取資料回來給前端Vue.js介面做互動。

成果可參考這裏(heroku開機要一段時間如果拉不到資料請多重整幾次。)

使用技術及觀念:

  • Vue.js (with vue-cli3)
  • Node.js (express with express-generator)
  • Heroku
  • CORS Headers
  • Jsonp (僅技術講解)

Outline

  • 同源政策
  • Node.js — express generator
  • HTTP 的 CORS Request
  • 修改 Response Header 允許跨域
  • 部署到Heroku
  • Vue-Cli dev 模式下的proxy
  • 什麼是Jsonp
  • 寫在最後

同源政策

一般開發網站的時候,如果是採前後端分離的架構,就會遇到跨域問題。就像今天這個專案,我的目標是抓取台灣空氣品質的開放API,回來做成介面給使用者搜尋,但是在利用axios直接打API的時候,遇到了下面的狀況:


這是因為網頁在傳遞資料的時候,不管是透過傳統XMLHttpRequest(常見的ajax方式) 或是Fetch,都會遵循同源政策(Same Origin Policy),「同源」指的是同個域名底下的資源,因為只能存取相同來源的資料,所以那些跨域的請求就會被阻擋掉。

這是瀏覽器的安全機制,當然並非如此一來就無法跨域存取了,我想到的解決有以下兩種:

  • Jsonp (Json with padding)
  • 使用Proxy 代理伺服器存取該目標API

因為jsonp的相關知識文章應該有不少,且有安全性疑慮(最後面會說明)。本篇會以Node實做Proxy為主來講解。

Node.js — express generator

跟前端有vue-cli 、 react-create-app 等方便又快速的手腳架一樣,許多後端框架也有類似的功能,express-generator就是其中一個。安裝方式非常簡單,如果對接下來流程有任何疑問,可以參考官方文件,有很詳細的使用說明。

在terminal輸入以下指令以安裝

npm install express-generator -g
  1. 輸入 express [你的專案名稱] 就可以產生出以express為基底的專案架構:
express  proxy-server

接下來就可以進入專案去新增我們要的api了。

HTTP 的 CORS Request

就像前面說的,CORS Request並非完全不可行,只是在Server端必須要有一些設定。既然我們不可能修改OpenAPI的Server內容,又因為後端資料交互是不會碰到瀏覽器的(因此就不會因為跨域問題被阻擋),我們就自己架一個Proxy Server來修改Http相關的設定,讓前端可以順利拿到資料。

CORS Request / Response流程

你在 example.com.tw 送出request給某網站的時候,在request body裡面會夾帶一個Origin的header,內容是你網站的Domain名稱:

Origin: http://www.foo.com

而在後端伺服器收到request並且回傳resonse到client的時候,瀏覽器會去看response裡面的header–***「Access-Control-Allow-Origin」***是不是包含剛剛發出request寫的Origin 域名,如果有,資料才會允許被回傳。

修改 Response Header 允許跨域

先到剛剛創造的express專案底下。

先找到我們的Open API URL,等等會用到:

http://opendata.epa.gov.tw/webapi/api/rest/datastore/355000000I-000259

###安裝所需套件
因為我們要利用後端去跟遠端Open API互動,所以這邊我們使用node-rest-client來達成這部分功能,node-rest-client官方文件:

npm install node-rest-client

新增API路由

在剛剛創造的express專案底下的主程式routes資料夾新增 air.js :

air.js

var express = require('express');
var router = express.Router();
var Client = require('node-rest-client').Client;
var client = new Client();
/* GET users listing. */
router.get('/', function(req, res, next) {
    let queries=req.query
    console.log('query: ', queries);
    let url='http://opendata.epa.gov.tw/webapi/api/rest/datastore/355000000I-000259'
    var options = {
        host: url,
        method: 'GET'
      };
    
    client.get(url, function (data, response) { 
        res.json({...data})
    });
});
module.exports = router;

router.get() 是express的router基本寫法,最後將這個包含自定義route的Router實體export出去,之後必須在主程式app.js引入才會有效,可以參考官方文件,可以看到在rest-client抓完資料的callback,我用json方式回傳了Open API 回傳的結果。

  ...
  client.get(url, function (data, response) { 
      res.json({...data})
  });
  ...

主程式app.js路由引入
接下來在app.js裡面將剛剛新增的router引入:

var airRouter = require('./routes/air');
app.use('/air', airRouter);

記得在var app=express()之後才做app.use()這件事,否則會讀不到app這個變數。

關鍵步驟: Response header 設定

將設定用middle的方式寫入,Express內,如果你直接在app.use()裡面傳入一個

function(req, res, next) {
     next()d
 }

那是middleware的意思,即所有request都會經過你傳入的function,且直到呼叫next()之後才會繼續執行,我們在這裡面設定Response 的Header,如此一來所有的Request都會得到相同的Header設定值(也可以在單獨某隻router寫入設定,可視需求改變)。

let allowCrossDomain = function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
  next();
}
app.use(allowCrossDomain)

我在Response Header裡面加入了

  • "Access-Control-Allow-Methods"代表允許讓設定的Http Method通過。
  • “Access-Control-Allow-Origin”,其值為’*’。

注意,剛剛說Response回傳到Client之前瀏覽器會先檢查這個Header有沒有跟來源Origin的值ㄧ樣如果是一般前後端分離的sercer這部分的值一般會xxx.com.tw,不會公開。如果你這樣寫,代表大家都可以對你的Server做跨域請求,這邊因為是open API 且是示範性質所以才這樣做。

到這邊其實已經完成了,如果順利的話你的Server應該已經可以拿到遠端API的資料,你可以用

npm run start  
// then go to  http://localhost:3000/air

在本地架起來用瀏覽器或Postman去測試看看。

部署到Heroku

想要在Heroku 架起一台node Server非常簡單,流程大致上是註冊帳號->安裝heroku cli-> 登入-> 到你的專案輸入幾個指令-> 最後 git push heroku master,他自己就會幫你在heroku提供的空間下npm install安裝好需要的套件並且架起來。

這邊就不多做說明,可參考官方教學,相信我,不會花掉你太多時間的,可先用官方提供的專案做練習。需注意的是因為是免費的服務,所以只要半小時沒在使用,他會暫時關閉你的機器,所以有時候連線會比較慢,可不要以為壞掉了。

可參考我的實作成果。

Vue-Cli dev 模式下的proxy

Vue-cli因為是基於webpack,其實有proxy的設定,你可以參考官方文件。但因為這樣只解決開發環境的問題,所以後來我才打算自己架設Proxy。

另外,前端的部分因為比較基礎,就沒有太詳細解說,你可以看看我的Repo,如果還是有不懂的地方,隨時可以聯絡我。

什麼是JSONP

因為傳統 Ajax無法跨域,早期工程師們找到了替代解決方案—< script> tag 的src引用是可以跨域的,於是利用這個原理的jsonp就這麼誕生了(想想你在引入一些js函式庫如lodash的時候是不是可以用cdn方式直接引入執行?)

Jsonp會做什麼事情

  1. 創造一個 < script > tag
  2. 根據你指定的Jsonp位置,設定這個tag的src
  3. 把這個< script > 加到瀏覽器DOM的head
  4. 一但資料載入完畢,會將回傳的資料傳給設定的callback並執行這些內容

Jsonp的安全隱患

如果你使用JsonP,那你帶進來的這些內容,跟你自己寫的jsㄧ樣,可以去存取你的DOM等網頁內容,這種情況下如果無法保證server的安全性,將會是潛在的問題。

寫在最後

前端介面成果:http://underground-air.surge.sh/
Proxy Server成果: https://mu-air-proxy.herokuapp.com/air

到這邊,如果你可以很順的理解以上的內容,那你應該對CORS Request流程有一定的了解了。其實因為時間跟篇幅的關係,有些實作部分我並沒有講的很仔細,但是我都有提供相關文件在該段落的附近,因為我覺得寫程式常常需要的是閱讀文件的能力,希望各位可以學著找到自己所需的資源並補上。在「尋找資源-實作-找問題修Bug」的循環過程中其實也在訓練自己的理解跟整合能力,才會讓你慢慢進入學習的正向循環。

當然在這過程中有任何的不懂或是覺得有筆誤的話都可以隨時聯絡我,我很樂意跟你分享我所知道的知識。

參考文章

STOF上關於CORS的討論

CORS PROTOCAL

how jsonp works?

CORS流程圖

JS世界時間表

這篇講解的是我如何從頭開始研究Javascript時區轉換相關API,以及一些時區相關的奇怪專有詞,最後做出世界時間表,我會盡使用淺顯易懂的方式來說明,如果你看完還是對內容有疑問,可以直接聯絡我 ,一起交流。順帶一提,你沒看錯現在是早上4:30 (X_X)。

Outline

  • TimeStamp是什麼?
  • GTM+8 與 UTC+8 ? 差在哪?
  • toLocaleTimeString的使用
  • 補充:DOM 操作 — cloneNode

TimeStamp是什麼?

TimeStamp 可以想成是某個執行動作的時間點,經常我們在操作資料的時候(最常見新增/修改),會希望留下時間紀錄,以免出問題的時候可以追查。就像公家單位的公文在蓋章的時候都會有時間戳記,系統也是ㄧ樣,只不過有時候不是我們常見的日期格式。

要用js觀察TimeStamp,可以在瀏覽器下:

Date.now(); //1549041946540

你應該可以得到奇怪的一串數字,類似這樣

1549041946540

這個是什麼意思呢?是指從 1970/01/01開始至今的秒數,也就是TimeStamp。
為什麼是1970年?據說那是工程師們自己訂的,Unix系統的誕生日期。一樣透過Date物件可以把這串秒數轉成一般的TimeString:

new Date(Date.now()) // Sat Feb 02 2019 01:32:14 GMT+0800 (台北標準時間)

TimeString 也是String,只不過有一定的格式,才能給Date相關api讀取。
有興趣的人可參考這篇 。

GTM+8 與 UTC+8 ? 差在哪?

其實這兩種格式在一般使用的情況下沒有差別,只是GTM是歷史沿革,UTC是後來科學家發現一種可以更準確測量時間的方式而訂出的格式,他們之間的時間差只在區區幾秒之間(但這足以讓某些科學家抓狂),總之,因為GTM這個格式已經被國際使用許久,所以就算UTC出現以後,還是沒有消失 (跟IEㄧ樣,號稱時代的眼淚,但你不消失我才會流淚)。

W3School的解釋:

The UTC() method returns the number of milliseconds between a specified date and midnight of January 1, 1970, according to universal time. Tip: The Universal Coordinated Time (UTC) is the time set by the World Time Standard. Note: UTC time is the same as GMT time.

基本上把他看做ㄧ樣就行了(除非你真的無法無視閏秒XD)我自己一般還是習慣使用UTC格式。

new Date(Date.now()).toUTCString() // 拿到目前UTC+0 的時間

toLocaleTimeString的使用

Js裡面時間相關api有很多,要算出不同時區的時間其實有不只一種方式,這裡用我覺得比較直觀的toLocaleTimeString(),第一個參數是語系,在後面的Option裡面可以設定很多格式,timeZone就是用來設定時區:

let now = new Date(Date.now())
let month= now.toLocaleDateString(‘en-US’,{timeZone:’Your Time Zone’’,month:’long’})

TimeZone 的格式可以參考這個列表,我拿到時區後,直接綁在html上面,這樣在一開始渲染時,就可以直接拿到,非常直接。

 <li class=”zone London bg-white” id=”London” time-zone=”Europe/London” >…</li>

option 兩個常用設定值

  1. 在Option裡面除了設定timeZone,也可以設定時制
{…hour12:false} //24小時制
  1. 甚至可以直接拿到完整的月份名字,如此一來就不需要另外寫月份的mapping表:
let now = new Date(Date.now())
let month= now.toLocaleTimeString(‘en-US’,{timeZone,month:’long’})
 //”February, 3:44:46 PM”

補充:DOM 操作 — cloneNode

這邊比較特別的是,因為重複的版型我習慣利用類似template的方式去做,所以我在底下區塊把重複的結構拉出來:

 <div id=”zone-template” style=”display:none”>
 <div class=”country”>
 <div class=”name”>NEW YORK</div>
 <i class=”date”>27 JAN,2019</i>
 </div>
 <div class=”time”>02:46 </div>
 </div>

然後在畫面啟動的時候在去跑回圈一個一個掛到<li>裡面,記得使用cloneNode,確保這個版型能夠成功複製到每個區塊,如果單純用appendChild可能會有問題,詳情可以參考這篇。

製作 Base on Cookie 的購物車

瀏覽器Cookie 為無狀態的Http協議帶來方便快速的解決方案,雖然太過依賴Cookie會產生潛在的風險,但用在一些不那麼需要注重安全的應用上還是非常適合,。

這篇文章會從認識cookie是什麼開始,到解說如何搭配cookie來製作商務網站常見的購物車。

大綱:
- Cookie是什麼? 
- Client 端設計流程
- 存取Cookie 
- 撰寫更友善的JS Cookie Setter/Getter

Cookie是什麼?

Cookie是瀏覽器的預設機制,提供4MB的空間去給網站暫存後續還會用到的資料,讓資料在其他頁面也能夠被取得,如果我們開啟開發者工具來看,就可以看到目前client所有的cookie:

Cookie預設在每次網站送出Request的時候,就會被跟著夾帶到Server,而Server回傳Response的時候,也會把Cookie一起帶下來到瀏覽器端,看一下瀏覽器發出Request時的Header就可以發現這件事情:(下圖是Facebooke發出請求時的內容)

也可以從server端設定cookie,方法是在Response裡面夾帶名為 Set-Cookie的Header:

Client 端設計流程

可以想見購物車資料被修改的時機是在使用者將商品加入購物車的那一刻,這時候我們只要把商品數量跟id紀錄起來就可以了,因為使用者還有可能會購買其他商品,所以還不需要太詳細的商品資料:

直到使用者進入購物車準備結帳時,就會需要跟後端調用商品確切價錢、確認有無優惠等動作:

由於cookie是會隨著送出的request一起到後端Server,因此我在後端可以直接拿到設定的cookie內容,然後去資料庫找到我要的相關資料,包成物件後再回傳到客戶端。

而直到使用者離開購物車頁面進入結帳頁面的最後一刻,我才會把剛剛使用者儲存的購物車資料存到Session,給訂單頁面的controller處理,避免使用者在結帳時發生商品資料與購買金額不一致的情形。

存取Cookie

cookie 文件: https://curl.haxx.se/rfc/cookie_spec.html

瀏覽器有提供api讓開發者使用js拿到cookie,直接使用document.cookie就可以拿到了:

拿到Cookie

 document.cookie 

設定cookie

設定cookie的方式並不是像操作js物件的Key/Value那麼直觀,而是要使用一定格式的字串"name=xxx" 但是後面會講到,可以利用js來包裝程簡潔好用function讓我們去設定它。

 document.cookie = "name=thecontentIwanttosave; path=/; expires=Thu, 18 Feb 2019 12:00:00 GMT";

要注意設定cookie時一起設定的path值會影響你在該頁面可以存取的cookie。
那麼path又是什麼?path是cookie的選項設定之一,跟domain可以搭配,domain是告訴瀏覽器這個cookie只能在這個被設定的域名下面使用,以防止別的網站可以拿到我的機密資訊。

**Cookie path syntax : **

The path attribute is used to specify the subset of URLs in a domain for which the cookie is valid. If a cookie has already passed domain matching, then the pathname component of the URL is compared with the path attribute, and if there is a match, the cookie is considered valid and is sent along with the URL request. The path “/foo” would match “/foobar” and “/foo/bar.html”. The path “/” is the most general path.

當我跟瀏覽器拿到cookie的時候,可能拿到的東西是這樣一整個字串:

瀏覽器沒有提供key value存取,所以我們只能操作字串,既然cookie是用分號去分隔,那用js的split就可以解決:

    var value = "; " + document.cookie;
	var parts = value.split("; " + name + "=");
	parts.pop().split(";").shift(); 

但也不可能每次要拿個cookie都要操作字串,所以為了讓開發上能夠更順暢,我們可以把這段code包成function,減少重複的code。

這裡還有兩件事要注意:

  1. 空白字元在每個瀏覽器跟cookie的版本的表現可能都不一樣,因此以防萬一我們會將它轉為其他字元代替。
  2. 為了避免編碼問題,我們統一將cookie轉為URI編碼。

Getter

    function setCookie(name,value){
	value = encodeURIComponent(JSON.stringify(value).replaceAll(' ','+'))
        document.cookie=`${name}=${value}; path=/;`
        return getCookie(name)
}

Setter

    function getCookie(name) {
	var value = "; " + document.cookie;
	var parts = value.split("; " + name + "=");
	let res
	if (parts.length == 2){
		res= parts.pop().split(";").shift(); 
		return JSON.parse(decodeURIComponent(res).replaceAll('+',' '))
	}else{
		return undefined
	}
}

做到這裡,就剩下畫面呈現跟功能操作了,你應該已經可以很順暢的開始開發購物車,如果有任何其他問題,非常樂意你聯絡我跟我討論:

Gmail : moojitsai@gmail.com

初探Regex 正規表達式

正規表達式英文全名Regular Expression,
想必一定曾是每個工程師的惡夢,在新手眼裡彷彿精靈語一樣神秘又難親近,
Regex是一把複雜卻威力強大的武器。如果可以活用,在很多場合可以幫助你少寫很多的if判斷式,今天這篇文章就要來幫助理解正規表達式。

為什麼需要 Regular Expression?

Regex可以實用的情境大致有幾種:

  • 尋找匹配的字串
  • 取代匹配的字串
  • 驗證使用者輸入資料欄位
  • 擷取某段想要的資訊

舉例Js為例子來說,如果我想要看看資料裡面有沒有某個字元"M",一個簡單的方式是這樣寫:

    let stringMatch = "This is Mujing.".some(s=>{
        return s=="M"
    }) // Check if there is M in my data.

不過如果今天,當條件變得更多更複雜的時候這時候正規表達式就派上用場了。
(字串裡面必須包含一個數字或一個大寫字母):

    let re = ""
    let result = "uSer0910".match(/([0-9]|[A-Z])/g) //result.length = 1 

Regular Expression 規則說明

正規表達式使用時是將規則寫在兩個正的斜線裡面,並且撰寫的時候有幾種類型的匹配方式:

  1. 直接匹配: 例如直接輸入字元"abc".match(/a/) 就可以匹配到a字元。

  2. 使用跳脫字元匹配:有些英文字在Regex裡面代表的特殊意義,但因為是用英文字元代表的,所以在使用時要用“\”跳脫字元來告知這不是一個直接匹配的字元。

  3. 特殊字元匹配:跟第二種相反,有一些特殊符號一但出現,就代表某種意義,如“[]“就代表任意匹配,如果要單純匹配 “[” 字元,則必須用反斜線告知。

JS 的Regular Expression寫法

JS裡面寫正規表達式的時候你可以直接用兩個斜線代表,或是new一個RegExp物件並將要寫的規則寫入建構子,不要用這種做法要注意,RegExp預設會直接幫你跳脫特殊字元,如果你要在裡面寫跳脫字元規則的話,記得寫兩個反斜線來告訴他不要跳脫:

例如:

   new RegExp('\d') // return /d/ (變成匹配d這個字元了)
   new RegExp('\\d') // return /\d/ (是我要的數字匹配規則)

RegExp物件底下有一個test方法,用法如下:

    /a/.test('An Apple a day.') // return true 

就可以很快的測試自己寫的方法對不對了,這個方法用在表單檢查也非常好用。
正規表達式還可以用在很多地方,如match、split、replace等操作字串的方法,
學會之後想怎麼用就怎麼用。

以下整理正規表達式常用的特殊規則,或可參考JS官方文件

跳脫字元規則

這邊有個好記的規則,小寫的

符號 說明
\d 匹配任意數字
\D 匹配任意非數字字元
\w 匹配所有文字字元 (a-z、A-Z、0-9、_ )
\W 匹配所有“非”文字字元 (標點符號、特殊字元)
\s 匹配空白字元
\S 匹配“非”空白字元
\b 匹配字元邊界 /(空格或開頭/)
\B 匹配字元邊界

特殊符號

符號 匹配說明
\ 反斜線,跳脫特殊字元,例如想尋找"/"的時候
. 任意字元
$ 字元結尾
^ 字元開頭
[] 中括號,比對中括號裡面的任一字元,可以用範圍匹配:[A-Z]、[a-z]、[0-9]
[^] ^代表「反」,比對中括號裡面"以外"的任一字元
| 同程式常見的OR邏輯
() 群組

指定匹配次數

因為一個字元只會匹配一次,如果要多次匹配,就可以用指定字數的規則。

需注意:這些次數針對的是的是該該符號擺放位置的前一個匹配規則。

符號 匹配次數
* 0次或更多
+ 1次或更多
? 0次或1次
{m} m次
{n,} 最少n次
{m,n} 從m次到n次
{m,n}? 從m次到n次,選到匹配最少次的

基礎範例:

其實懂了基本邏輯之後,剩下的就是組合而已,匹配的規則要用在查就可以了,因為比較常用的寫法都差不多。

1.驗證手機號碼:

手機號碼有10個數字,要怎麼寫呢?你可能第一個會想到[0-9],方向對了,
但[0-9]只會匹配一次,如果要匹配10次可以這麼寫:

 /[0-9]{10}/ 

2.商品型號:

一般電商在後台上架商品的時候,一個常見的需求是輸入商品型號,為了減少使用者錯誤率也為了提高效率,我們可以用正規表達式解決,假設今天商品型號的規則是:

三個英文字母 + “-” + 五個數字 (例如ABC-90006 )

那麼其實搭配上面講的匹配次數就可以:

    /[A-Z]{3}-[0-9]{5}/

3.驗證密碼(至少包含一個大寫字母以及一個數字):

這個情況想必跟[A-Z]以及[0-9]脫不了關係,但會遇到順序性的問題,直接給[A-Z][0-9]代表的是先一個大寫字母在加上一個數字,不能夠反過來,這時就會需要最少匹配一次的"+"跟最寬容的"*"。

拆解各種情況:

[A-Z][0-9] :匹配一個A-Z的字母跟一個0-9的數字,而且不能顛倒
[A-Z]+ :代表匹配大寫字母最少一次
[A-Z]* :代表匹配A-Z0次或更多(這樣就代表可有可無,不是我們想要的)
[A-Z]+[0-9]+ :匹配[A-Z]最少一次之後在匹配0-9最少一次(如:AABB909)

看完上面四個範例你應該就會理解剛剛講的順序性是什麼,根據這個條件,可能會有的情況有幾種:

1.  [任意字串] + [至少一個A-Z字元]+[至少一個數字]+[任意字串]
2.  [A-Z字元在開頭]+[任意字串]+[至少一個數字]

簡單來說就是要考慮我要匹配的字元是在開頭以及結尾,我們可以用
“.*”來達成 [ 任意字符 ]的[ 匹配任意次(包含0次)],以及
"+" 達成最少一次的匹配:

.*[A-Z]+ :表示至少有一個A-Z字元,前面可以有其他「任意次」個「任意字元」。

組合!!

.*[A-Z]+.*[0-9]+.*

這樣就可以達到不管前面有什麼,最後有什麼,只要有一個A-Z跟一個數字,就符合規則啦!
但最後還要考慮順序的問題,可以用 OR 邏輯將數字跟字母對調,就完美符合我們的需求啦!

.*[A-Z]+.*[0-9]+.* | .*[0-9]+.*[A-Z]+.*

幾個練習Regex的網站:

比較推薦第一個,他是互動式的,一步一步帶你了解正規表達式的使用方式,說明的很詳細。
剩下兩個則是自己在測試的時候很好用,寫完表達式右邊還有規則的說明,非常方便。

  1. RegexOne
  2. Regex101
  3. Regexr
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×