【小哥哥, 跨域要不要了解下】ServerProxy

圈圈的圈發表於2019-03-01

系列文章:

在系列文章的第一篇我們談到過跨域問題產生的原因是瀏覽器的同源策略. 那麼伺服器之間通訊就不會受到相關條件的限制. 那麼是不是我們可以通過同域伺服器幫助訪問其他域名的 api 呢? 如果可以的話, 那豈不是可以想訪問誰就訪問誰? 限制, 不存在的…

2018-12-12-23-32-26

ps: 本文涉及到部分後端知識, 需要有一丟丟的 nodejs koa 基礎. 主要用於搭建一個 web 伺服器, 當然沒有基礎也沒啥關係, 先去 node koa 官網看看. 回不回來???

隨你咯 ?

建立專案目錄

繼續上一步, 本文只會建立一個後端專案. 所以不需要在 ./fe 目錄下建立前端專案啦, 專案目錄如下.

2018-12-12-23-50-07

其中, serverProxy 目錄是專案的主目錄. www 目錄即為前端靜態檔案的託管目錄. base.js 為後端主程式, add.js subtract.js 分別表示兩個第三方服務, 分別提供了計算加法和減法的能力.

安裝 koa

  • 首先執行 cd be/serverProxy 將路徑切換到 serverProxy
  • 執行 npm init -y 初始化為一個 node 專案
  • 執行 npm i koa -S 完成 koa 的安裝

驗證 koa 安裝是否完成

  • 編輯 base.js 寫入以下內容
const Koa = require(`koa`);

const app = new Koa();
const PORT = 1234;

app.use((ctx) => {
  ctx.body = `Hello World`;
});

app.listen(PORT, () => {
  console.log(`the server is listen: `, PORT);
});
複製程式碼
  • 完成後執行 node base.js 看到命令列中輸出了 the server is listen: 1234 說明啟動成功
  • 瀏覽器訪問 localhost
2018-12-13-00-05-48

此時程式碼

引入 koa-static 模組

在之前文章中, 我們總是要通過 live-server 啟動一個本地的靜態資源服務. 用於託管前端靜態檔案. koa 生態中有現成的中介軟體koa-static可以提供直接在後端專案中建立靜態資源服務的能力.

  • 首先執行 npm i koa-static -S 安裝 koa-static
  • 調整 base.js
const Koa = require(`koa`);

// 引入 koa-static
const koaStatic = require(`koa-static`);

const app = new Koa();
const PORT = 1234;

// 使用 koa-static 中介軟體, 並指定靜態檔案目錄為 www
app.use(koaStatic(`./www`));

app.use((ctx) => {
  console.log(ctx.req.url);
  ctx.body = `Hello World`;
});

app.listen(PORT, () => {
  console.log(`the server is listen: `, PORT);
});
複製程式碼
  • 編寫前端 index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ServerProxy 實現跨域</title>
</head>
<body>
    ServerProxy 實現跨域
</body>
</html>
複製程式碼
2018-12-13-00-26-23

之前準備的 html 頁面赫然在目 ?. 至此, 靜態檔案服務就搭建成功了(相當於我們自己實現了一個 live-server)程式碼地址

通過 ajax 訪問當前後端介面

通過 koa-static 中介軟體, 我們搭建了一個自己的靜態檔案伺服器. 接下來演示一個不跨域的請求…

2018-12-15-21-51-02
  • 首先修改後端程式碼
const Koa = require(`koa`);
const koaStatic = require(`koa-static`);

const app = new Koa();
const PORT = 1234;

app.use(koaStatic(`./www`));

app.use((ctx) => {
  let ret;
  // 獲取本次接收的請求的請求路徑
  const path = ctx.req.url;

  // 如果請求路徑以api開頭, 那麼作為介面請求處理
  if (path.startsWith(`/api`)) {
    // 這樣實現的路由不是很優雅, 但是能用 ?
    switch (path) {
      case `/api/getFriend`:
        ret = { name: `quanquan`, friend: `gl` };
        break;
      default:
        ret = { errno: 1, errmsg: `未知介面` };
        break;
    }
  }
  ctx.body = ret;
});

app.listen(PORT, () => {
  console.log(`the server is listen: `, PORT);
});
複製程式碼

上述程式碼中定義了 /api/getFriend 介面, 通過瀏覽器訪問的如下圖:

2018-12-15-22-09-17

ps: 需要執行 node base.js 重啟後端專案

接下來修改前端程式碼. 通過 ajax 的方式訪問該介面

修改前端程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ServerProxy 實現跨域</title>
</head>
<body>
    ServerProxy 實現跨域

    <script>
        // 一個常規的 ajax, 感興趣的兄弟們也看看. 手寫 ajax 好多面試官還在考
        var xhr = new XMLHttpRequest()
        xhr.open(`GET`, `/api/getFriend`)
        xhr.onreadystatechange = function() {
            if(xhr.readyState === 4 && xhr.status === 200) {
                console.log(`介面返回的資料為: `, xhr.responseText)
            }
        }
        xhr.send()
    </script>
</body>
</html>
複製程式碼

重新整理瀏覽器, 控制檯展示如下. 沒有報錯, 返回的資訊前端直接拿到了.

2018-12-15-22-18-18

原來前後端同域時資料互動這麼的簡單.

2018-12-15-22-22-07

前後端跑通階段程式碼

完善第三方服務

專案開發中經常會用到一些基礎服務, 比如天氣資訊, 地理位置資訊等等. 這些服務能力一般是通過呼叫第三方的介面來實現的(你開發一個網站, 先發射一顆氣象衛星到天上也不太現實). 這一步我們建立兩個第三方服務, 分別提供加法和減法運算.

加法運算服務 add.js

const Koa = require(`koa`);

const app = new Koa();
const PORT = 1111;


app.use((ctx) => {
  // 獲取引數
  const { a, b } = ctx.query;
  // 嘗試將引數轉化為數字後進行加法操作
  const result = Number(a) + Number(b);
  ctx.body = { result };
});

app.listen(PORT, () => {
  console.log(`the server is listen: `, PORT);
});
複製程式碼

執行命令 node add.js 啟動程式, 然後瀏覽器端訪問localhost得到的結果如下, 說明加法計算服務啟動成功.

2018-12-15-23-18-53

減法運算服務 subtract.js

const Koa = require(`koa`);

const app = new Koa();
const PORT = 2222;


app.use((ctx) => {
  // 獲取引數
  const { a, b } = ctx.query;
  // 嘗試將引數轉化為數字後進行減法操作
  const result = Number(a) - Number(b);
  ctx.body = { result };
});

app.listen(PORT, () => {
  console.log(`the server is listen: `, PORT);
});
複製程式碼

執行命令 node subtract.js 啟動程式, 然後瀏覽器端訪問localhost得到的結果如下, 說明減法計算服務啟動成功.

2018-12-15-23-23-55

目前程式碼

通過後端代理訪問第三方服務

建立完加法和減法服務, 我們還是有僥倖心理忍不住在前端專案裡訪問一下試試, 萬一能通了呢? 就不用費事兒研究跨域了, 嘗試一下

2018-12-16-00-57-43

修改前端程式碼中的介面地址 xhr.open(`GET`, `http://localhost:1111/?a=1&b=2`) 完整程式碼, 之後直接重新整理瀏覽器(請思考, 為什麼修改了 js 檔案需要執行 node ... 重啟服務, 而修改了 html 檔案只需要重新整理瀏覽器就可以了呢?).

2018-12-15-23-34-33

還是之前的報錯, 還是熟悉的味道. 不好使…

回想一下之前的思路. 瀏覽器有同源策略的限制伺服器沒有. 我們的前端專案託管在後端專案中所以訪問我們自己的後端不跨域. 我們的後端請求第三方服務沒有限制. 那麼 ^_^

2018-12-15-23-41-03
  • 執行 npm i axios -S 安裝 axios, 後端通過它來請求目標伺服器
  • 修改程式碼

base.js

const Koa = require(`koa`);
const koaStatic = require(`koa-static`);
const axios = require(`axios`);

const app = new Koa();
const PORT = 1234;

app.use(koaStatic(`./www`));

app.use(async (ctx) => {
  let ret;
  // 獲取本次接收的請求的請求路徑
  const path = ctx.req.url.split(`?`)[0];
  console.log(`ctx.query.server`, ctx.query.server);
  // 如果請求路徑以api開頭, 那麼作為介面請求處理
  if (path.startsWith(`/api`)) {
    // 這樣實現的路由不是很優雅, 但是能用 ?
    switch (path) {
      case `/api/getFriend`:
        ret = { name: `quanquan`, friend: `gl` };
        break;
      // 如果介面需要代理介面路徑為 /api/proxy
      case `/api/proxy`:
        // axios 直接訪問前端給出的目標伺服器url, 並將目標伺服器返回的資料直接返回給前端
        ret = (await axios.get(ctx.query.server)).data;
        break;
      default:
        ret = { errno: 1, errmsg: `未知介面` };
        break;
    }
  }
  ctx.body = ret;
});

app.listen(PORT, () => {
  console.log(`the server is listen: `, PORT);
});
複製程式碼

前端程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ServerProxy 實現跨域</title>
</head>
<body>
    <h3>ServerProxy 實現跨域</h3>

    a: <input type="text" id="a" value="1">
    b: <input type="text" id="b" value="2">
    <button id="add">計算加法</button>
    <button id="subtrnct">計算減法</button>

    <div>計算結果為: <span id="ret"></span></div>
    <script>
        var aDom = document.getElementById(`a`)
        var bDom = document.getElementById(`b`)
        var addBtn = document.getElementById(`add`)
        var subtrnctDom = document.getElementById(`subtrnct`)
        var retDom = document.getElementById(`ret`)

        function add() {
            if(!a.value.trim() || !b.value.trim()) return
            var xhr = new XMLHttpRequest()
            xhr.open(`GET`, `/api/proxy` + `?server=` + encodeURIComponent(`http://localhost:1111/?a=`+ a.value +`&b=` + b.value))
            xhr.onreadystatechange = function() {
                if(xhr.readyState === 4 && xhr.status === 200) {
                    console.log(`介面返回的資料為: `, xhr.responseText)
                    retDom.innerHTML = JSON.parse(xhr.responseText).result
                }
            }
            xhr.send()
        }

        function subtrnct() {
            if(!a.value.trim() || !b.value.trim()) return
            var xhr = new XMLHttpRequest()
            xhr.open(`GET`, `/api/proxy` + `?server=` + encodeURIComponent(`http://localhost:2222/?a=`+ a.value +`&b=` + b.value))
            xhr.onreadystatechange = function() {
                if(xhr.readyState === 4 && xhr.status === 200) {
                    console.log(`介面返回的資料為: `, xhr.responseText)
                    retDom.innerHTML = JSON.parse(xhr.responseText).result
                }
            }
            xhr.send()
        }

        addBtn.addEventListener(`click`, add)
        subtrnctDom.addEventListener(`click`, subtrnct)
    </script>
</body>
</html>
複製程式碼

最後結果:

server-proxy-dfasdfasfas

結語: ServerProxy 的原理大概就是這個樣子的啦, 通過 ajax 訪問同域後端服務, 後端服務訪問目標服務並將目標服務返回的內容透傳給前端. 當然實際操作起來不會像例子這麼簡單. 我的另一個系列文章【手把手帶你擼一個介面測試工具】將會詳細介紹複雜一些的情況, 包括不同的請求型別, 請求頭設定以及響應頭獲取等等. 希望感興趣的小夥伴繼續關注.

關於跨域的其他方式: document.domain 一行程式碼可以搞定, 適合同主域名不同子域名的情況. postMessage 需要新增額外 iframe, 整體實現較為簡單, 一個 API 搞定, 感興趣的同學可以看看文件. 還有一些比較小眾的做法 flash CSST 前端打點嚐嚐用到的 img 標籤等等, 這裡就不一一列舉了, 學無止境…

2018-12-16-01-38-47

相關文章