2018年10月30日 星期二

《面試官別再問》PHP實作CI3 + JQuery前後端分離

我剛開始接觸前後端分離的時候,正值它開始慢慢擴散的時候,也還沒有意識到它帶來的好處。覺得它甚是麻煩,當我改一個接口的時候,我需要同時修改兩部分的代碼,以及對應的測試。反而,還不如直接修改原有的模板來得簡單。
可是當我去使用這個,由前後端分離做成的單頁面應用時,我開始覺得這些是值得。當頁面加載完後,每打開一個新的鏈接時,不再需要等網絡返回給我結果;我也能快速的回到上一個頁面,像一個 APP 一樣的體現這樣的應用。整個過程裡,我們只是不斷地從後台去獲取數據,不需要重複地請求頁面——因為這些頁面的模板已經存在本地了,我們所缺少的只是實時的數據。
後來,當我從架構去考慮這件事時,我才發現這種花費是值得的。
Web研發模式演變


傳統的 MVC 架構裡,因為某些原因有相當多的商業邏輯,會被放置到View 層,也就是模板層裡。換句話來說,就是這些邏輯都會被放到前端。我們看到的可能就不是各種if 、else 還有簡單的equal 判斷,還會包含一些複雜的商業邏輯,比如說對某些產品進行特殊的處理。
如果這個時候,我們還需要做各種頁面交互,如填寫表單、 Popup 、動態數據等等,就不再是簡單的和模板引擎打交道了。我們需要編寫大量的JavaScript 代碼,因為業務的不斷增加,僅使用jQuery 無法管理如此復雜的代碼。

一、前端取得資料使用令牌進行身份驗證過程

客戶端接收到令牌後對其進行儲存,每次訪問時需攜帶的令牌
服務端在接受到客戶端請求時需驗證令牌,驗證成功則回傳資料。
生成與驗證令牌的方法有很多種,這裡使用的是jwt(Json Web Token)。
Use composer to manage your dependencies and download PHP-JWT:

composer require firebase/php-jwt


二、定義後端API接口

這是有可能的使用 HTTP 動詞(request method)去定義你的路由規則。這是特別有用的當建立 RESTful 應用程式的時後。你可以使用標準的 HTTP 動詞(GET、PUT、POST、DELETE、PATCH)或者客製化的動詞像是(e.g. PURGE)。 HTTP 動詞規則不區分大小寫。所有你需要做的路由,就是將動詞增加到你的陣列索引裡面。

例如:

$route['api']['put'] = 'product/insert';

上述例子,PUT 請求到 URI“products” 稱之為 Product::insert() 控制器方法。

$route['api/(:num)']['DELETE'] = 'product/delete/$1';

DELETE 請求到 URL“products”第一個片段,數字在第二個片段將會重新映射到 Product::delete() 方法,傳入數值到第一個參數上。


使用 HTTP 動詞當然是可選的(非必要)。




三、建立基本Layout前端HTML

JQueryBootstrapAwesomeDataTablesDatepicker

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Page Title</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>

</body>

</html>




四、非SPA(Single Page Application)也沒有Node.js 內建的Web Server

Web Server用的是XAMPP開發環境
前端使用Purl (A JavaScript URL parser)判斷uri決定js要Include哪一個HTML

$.get(`view/${uri}.html`,function(data){
$("body").html(data);
});

Codeigniter 3負責Layout的Server side render,還有api跟jwt管理

//簽發Token
public function getLssue()
{
$key = $this->CI->config->item('KEY'); //key
$time = time(); //當前時間
$token = [
'iss' => $this->CI->config->item('DOMAIN'), //簽發者 可選
'aud' => $this->CI->config->item('DOMAIN'), //接收該JWT的一方,可選
'iat' => $time, //簽發時間
'nbf' => $time , //(Not Before):某個時間點後才能訪問,比如設置time+30,表示當前時間30秒後才能使用
'exp' => $time+5, //過期時間,這裡設置5秒
];
echo JWT::encode($token, $key); //輸出Token
exit;
}
public function verification($jwt='')
{
$key = $this->CI->config->item('KEY'); //key
try {
JWT::$leeway = 60;//當前時間減去60,把時間留點餘地
$decoded = JWT::decode($jwt, $key, ['HS256']); //HS256方式,這裡要和簽發的時候對應
$arr = (array)$decoded;
return $arr;
} catch(\Firebase\JWT\SignatureInvalidException $e) { //簽名不正確
log_message('debug', $e->getMessage());
}catch(\Firebase\JWT\BeforeValidException $e) { // 簽名在某個時間點之後才能用
log_message('debug', $e->getMessage());
}catch(\Firebase\JWT\ExpiredException $e) { // token過期
log_message('debug', $e->getMessage());
}catch(Exception $e) { //其他錯誤
log_message('debug', $e->getMessage());
}
//Firebase定義了多個 throw new,我們可以捕獲多個catch來定義問題,catch加入自己的業務,比如token過期可以用當前Token刷新一個新Token
}
public function verToken()
{
$headers = $this->CI->input->request_headers();
$mate = preg_match('/Bearer\s(\S+)/', $headers['Authorization'], $matches); //判斷Authorization格式是否正確
if (!empty($mate)){
$auth = $this->verification(str_replace('Bearer ','',$headers['Authorization']));
return ($auth['aud']==$this->CI->config->item('DOMAIN')) ? 1 : 0; //判斷來源與Config設定值是否相同
}
}

is_ajax_request() 判斷是否由ajax發出的請求
Returns: TRUE if it is an Ajax request, FALSE if not
Return type: bool

過去測試都是用Postman發出請求看回傳值是否正確

這次直接用CI3提供的單元測試類別 驗證後端API是否能夠正確產出所需資料

$test = 1 + 1;
$expected_result = 2;
$test_name = 'Adds one plus one';
$this->unit->run($test, $expected_result, $test_name);


五、前端AJAX向後端發出請求並帶JWT令牌

$.ajax({
    type: "POST", //GET, POST, PUT
    url: '/authenticatedService' //the url to call
    data: yourData, //Data sent to server
    contentType: contentType,
    beforeSend: function (xhr) { //Include the bearer token in header

        xhr.setRequestHeader("Authorization", 'Bearer '+ jwt);

    }
}).done(function (response) {
    //Response ok. process reuslt
}).fail(function (err) {
    //Error during request
});

jquery的async:false,這個屬性
預設是true:非同步,false:同步。


六、表單Submit Form to Ajax Api 

let form = $(e.target);
$.ajax({
    url: form.attr("action"),
    type: form.attr("method"),
    data: form.serialize(),
cache: false,
beforeSend: function (xhr) {
    xhr.setRequestHeader("Authorization", "Bearer " + token);
},
    success: function (data) {
return true;
    },
    error: function(xhr) {
return false;
}
});

七、表單Submit Form to Ajax Api & File Upload

$.ajax({
url: form.attr("action"),
    type:form.attr("method"),
    data: new FormData($('form')[0]),
cache: false,
contentType : false,
processData : false,
beforeSend: function (xhr) {
    xhr.setRequestHeader("Authorization", "Bearer " + token);
},
    success: function (data) {
return true;
    },
    error: function(xhr) {
return false;
}
});

八、jQuery分頁組件 jqPaginator

$('#id').jqPaginator({
    totalPages: 100,
    visiblePages: 10,
    currentPage: 1,
    onPageChange: function (num, type) {
        $('#text').html('當前第' + num + '頁');
    }
});

九、Bootstrap 3 Modal(互動視窗)

Use Bootstrap’s JavaScript modal plugin to add dialogs to your site for lightboxes, user notifications, or completely custom content.

沒有留言:

張貼留言

網誌存檔