头图

前端项目本地调试方案

记得要微笑

背景

前端项目在本地调试时都难以跳过登录环节,如果每次都要通过登录去调试就太麻烦了,有没有什么方法避免调试时还需要跳往登录页登录呢?文本梳理出下面三种方案供大家参考:

Cookie复制

Cookie复制是一种最简单粗暴的纯手工方案,步骤如下:

  • 先在测试环境进行登录,在控制台中拿到Cookie中的令牌
  • 本地启动前端项目,打开控制台Cookie存储,将刚刚拿到的令牌复制粘贴到该域名下的Cookie存储中,然后刷新页面

这种纯手工的方案也是挺麻烦的,每次也需要去Cookie中找令牌,然后复制,还会出现复制粘贴错误内容。既然手工复制不好,那我们能不能自动化复制呢?答案是可以的,谷歌拓展插件可以做到这一点

谷歌拓展插件

一个应用(扩展)其实是压缩在一起的一组文件,包括HTML、CSS、JavaScript脚本、图片文件及其它任何需要的文件。 应用(扩展)本质上来说就是web页面。

文件

每个应用(扩展)都应该包含下面的文件:

  • 一个manifest文件
  • 一个或多个HTML文件(除非这个应用是一个皮肤)
  • 可选的一个或多个JavaScript文件
  • 可选的任何需要的其他文件,例如图片

在开发应用(扩展)时,需要把这些文件都放到同一个目录下。发布应用(扩展)时,这个目录全部打包到一个应用(扩展)名是 .crx的压缩文件中。如果使用谷歌浏览器应用开放平台或Chrome Developer Dashboard上传应用(扩展),可以自动生成 .crx 文件。

弹窗的架构

背景页面并不是应用(扩展)中唯一的页面。例如,一个browser action可以包含一个弹窗(popup),而弹窗就是用html页面实现的。

应用(扩展)里面的HTML页面可以互相访问各自DOM树中的全部元素,或者互相调用其中的函数。

下图显示了一个browser action的弹窗的架构。弹窗的内容是由HTML文件(popup.html)定义的web页面。它不必复制背景页面(background.html)里的代码,因为它可以直接调用背景页面中的函数。

image.png

复制Cookie

思路如下:

  • 获取地址栏地址域名下所有Cookie
  • Cookie写入调试地址域名下
  • 打开调试页面

涉及到的API:

  • chrome.cookies.getAll(object details, function callback) 从一个cookie存储获取与给定信息匹配的所有cookies。所获取cookies将按照最长路径优先排序。当多个cookies有相同长度路径,最早创建的cookie在前面。
  • chrome.cookies.set(object details) 用给定数据设置一个cookie。如果相同的cookie存在,它们可能会被覆盖。
  • chrome.tabs.getSelected(integer windowId, function callback) 获取特定窗口指定的标签。

项目结构

debug
 ├── iamges
     └── icon.png
 ├── manifest.json  
 ├── popup.css
 ├── popup.html
 └── popup.js

配置文件

{
  "manifest_version": 2,
  "name": "debug",
  "description": "前端项目调试工具",
  "version": "1.0.0",
  "icons": {
    "16": "/images/icon16.png",
    "32": "/images/icon32.png",
    "48": "/images/icon48.png",
    "128": "/images/icon128.png"
  },
  "permissions": [
    "cookies", // 访问Cookie的权限
    "tabs", // 访问标签的权限
    "http://*/*",
    "https://*/*",
    "storage"
  ],
  "browser_action": {
    "default_icon": {
      "16": "/images/icon16.png",
      "32": "/images/icon32.png",
      "48": "/images/icon48.png",
      "128": "/images/icon128.png"
    },
    "default_popup": "popup.html"
  }
}

弹窗页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="./popup.css">
  <title>Document</title>
</head>
<body>
  <input type="text" name="redirect_url" id="redirect_url">
  <button id="debug_btn">debug</button>
  <script src="./popup.js"></script>
</body>
</html>

弹窗脚本

/** 按钮DOM */
const debug_btn = document.getElementById('debug_btn');

/** 重定向 */
const redirectTo = (url) => {
  window.open(url);
}

/** 获取地址栏 */
const getUrl = () => {
  return new Promise((resolve) => {
    chrome.tabs.getSelected(null, resolve)
  })
}

/** 获取Cookie */
const getCookie = () => {
  return new Promise(async (resolve) => {
    const tab = await getUrl();
    // alert('tab' + JSON.stringify(tab))
    chrome.cookies.getAll({ url: tab.url }, resolve)
  })
}

/** 设置Cookie */
const setCookie = () => {
  return new Promise(async () => {
    const cookies = await getCookie();
    // alert('cookie' + JSON.stringify(cookie))
    const url = document.getElementById('redirect_url').value;
    const url_reg = /^https?:\/\/*\/*/;
    if (url_reg.test(url)) {
      cookies.forEach((cookie) => {
        const { name, value, path, secure, expirationDate, storeId } = cookie;
        chrome.cookies.set({ url, name, value, path, secure, expirationDate, storeId, domain: 'localhost' });
      })
      redirectTo(url);
    } else {
      alert('输入url不正确!')
    }
  })
}

/** 监听点击事件 */
debug_btn.addEventListener('click', async () => {
  setCookie();
})

添加到拓展程序中

  • 打开谷歌浏览器拓展程序,路径:菜单->更多工具->拓展程序。或者通过chrome://extensions链接打开
  • 单击“加载已解压的拓展程序”,选择我们搭建的拓展目录

image.png

效果

[image.png

image.png

代理服务

思路:在浏览器与本地前端项目服务之间搭建一个代理服务,代理服务主要帮助我们完成登录拿到令牌,转发浏览器的请求,向本地前端项目服务拉取静态资源(比如静态页面、样式文件等),和向资源服务器获取受保护资源

非微前端调试流程.png

实践

首先搭建以下四个服务:

  • 代理服务localhost:2000(proxy)
  • SSO服务localhost:8888(SSO)
  • 本地前端服务localhost:3304(fontend)
  • 资源服务localhost:8080(backend)

代理服务proxy
代理服务主要有以下几个职责:

  • 启动代理服务时向SSO服务登录获取令牌,并存在服务内存中
  • 向本地服务拉取静态资源文件,比如index.html、图片、样式文件等
  • 转发获取受保护资源的请求

调试配置文件:

module.exports = {
  user: {
    username: 'ddxyq',
    password: '123456',
  },
  proxy: [
    {
      path: ['/api'],
      target: 'http://localhost:8080', // 资源服务
    },
  ],
  html: 'http://localhost:3304', // 前端本地服务
  frontPage: true,
};

具体实现:

const express = require('express');
const request = require('request');
const config = require('./config/debug');
const app = express();
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
/** 管理cookie */
var cookie = null;

/** 请求转发url,判断是获取静态资源页面还是获取受保护资源 */
const getProxy = (path) => {
  const { proxy, html } = config;
  const t = proxy.filter(p => p.path.some((s) => path.includes(s)));
  return t.length ? t[0].target + path : html + path;
}

/** 请求转发 */
app.all('*', async (req, res) => {
  const options = {
    url: getProxy(req.path),
    headers: {
      Cookie: cookie,
      'Content-Type': req.headers['content-type'] || req.headers['Content-Type'],
      'User-Agent': userAgent,
    },
    body: req.body,
  };
  const result = request(options)
  if (result instanceof Promise) {
    const { result: r } = await result; 
    res.send(r);
    return;
  }
  result.pipe(res);
  return;
})

/** 自动登录 */
const login = () => {
  const options = {
    method: 'POST',
    url: 'http://localhost:8888/passport/login',
    headers: {
      'Content-Type': 'application/json',
      'User-Agent': userAgent,
    },
    json: true,
    body: config.user,
  };
  request(options, (error, res, body) => {
    if (error) throw new Error(error);
    if (body === 'OK') {
      const c = res.headers['set-cookie'];
      request({
        method: 'GET',
        url: 'http://localhost:3304',
        headers: {
          Cookie: c,
          'User-Agent': userAgent,
        },
      }, (e, r, b) => {
        if (!!b) {
          cookie = c;
          console.log('登录成功!'); 
        }
      })
    }
  })
}
login();

app.listen(process.env.PORT, () => {
  console.log(`Example app listening at http://localhost:${process.env.PORT}`)
})

SSO服务
SSO服务主要是校验用户登录信息后下发令牌

const express = require('express');
const bodyParser=require('body-parser');
const jwt = require('jsonwebtoken');
const cors = require("cors");

const app = express();
const SECRET = 'SDKHASUDFADFUASHDVAB';

/** 中间件 */
app.use(cors());
app.use(bodyParser.json({ extended: false }));

/** 登录下发token */
app.post('/passport/login', (req, res) => {
  const { username, password } = req.body;
  console.log('username, password',username, password)
  if (username && password) {
    const payload = { username };
    const token = jwt.sign(payload, SECRET, { expiresIn: '1day' });
    /** 设置Cookie */
    res.cookie('security_context', token, { domain: 'localhost', path: '/', httponly: true });
    res.send(200);
  } else {
    res.json(500, { error: '账户密码不正确!' });
  }
})

app.listen(process.env.PORT, () => {
  console.log(`Example app listening at http://localhost:${process.env.PORT}`)
})

本地前端服务(fontend)
我们用create-react-app脚手架搭建一个react应用,并在应用中请求受保护资源

测试组件代码:

import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
import axios from 'axios';

function App() {

  const [data, setData] = useState([]);

  useEffect(() => {
    axios.get(`/api/quotestore/123456`).then((res: any) => {
      const { code, data } = res.data;
      if (code === 200) {
        setData(data);
      }
    })
  }, []);
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
        {
          data.map((d: any) => (
            <div>
              <span>{d.storeName}</span>
            </div>
          ))
        }
      </header>
    </div>
  );
}

export default App;

资源服务(backend)
资源服务也就是我们的后端服务,处理前端请求响应数据

校验令牌中间件

const jwt = require('jsonwebtoken');
const secret = 'SDKHASUDFADFUASHDVAB';

const tokenMiddleWare = (req, res, next) => {
  const security_context = req.cookies['security_context'];
  jwt.verify(security_context, secret, (error, decoded) => {
    if (error) {
      console.log(error.message)
      res.redirect(302, 'http://localhost:7777/login');
    }
    console.log(decoded)
    next();
  })
}

module.exports = tokenMiddleWare

主要代码:

const express = require('express');
const cors = require('cors');
const bodyParser=require('body-parser');
const cookieParser = require('cookie-parser');
const tokenMiddleWare = require('./tokenMiddleWare');
const app = express();

/** 中间件 */
app.use(cors());
app.use(bodyParser.json({ extended: false }));
app.use(cookieParser());

app.get('/api/quotestore/:id', tokenMiddleWare, (req, res) => {
  const { id } = req.params;
  console.log(`id: ${id}, ${req.cookies['security_context']}`);
  if (!!id) {
    return res.json({
      code: 200,
      data: [
        {
          storeName: 'ddxyq',
          storeId: 'nsifghe'
        },
        {
          storeName: 'kkzjx',
          storeId: 'etyjdsdrf'
        },
        {
          storeName: 'hlhjx',
          storeId: 'eryeryjadf'
        },
      ],
      errorMessage: null
    })
  } else {
    res.json({
      code: 500,
      data: null,
      errorMessage: 'id不存在'
    })
  }
})

app.listen(process.env.PORT, () => {
  console.log(`Example app listening at http://localhost:${process.env.PORT}`)
})

image.png

通过代理服务(localhost:2000)向SSO服务(localhost:8888)请求登录拿到令牌后,可以访问访问前端本地服务(localhost:3304)页面,并向资源服务(localhost:8080)请求受保护资源

最后,代理服务可以封装成一个命令行工具库,发布到公司的仓库中,就可以帮助前端同学优雅的调试了~

参考文章:
chrome扩展开发中文教程
谷歌插件开发官方文档

阅读 912
avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

1k 声望
1.9k 粉丝
0 条评论
你知道吗?

avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

1k 声望
1.9k 粉丝
宣传栏