[关闭]
@EncyKe 2018-02-23T14:18:17.000000Z 字数 9231 阅读 2960

手记:iframe、postMessage 及其它跨域通信实践

#手记



1. 缘起

一般而言,对于一套有用户登录需求的网站,从安全性、拓展性、低耦合等方面来考虑,主要展示页面用户登录等个人信息相关页面会被设计成两套不同的站点,比如:www.domain.com/ 以及 user.domain.com/;甚至协议也会不同了,比如:http://www.domain.com/ 以及 https://user.domain.com/

这类似于前台和后台的概念:前台对于未登录和已登录的用户均开放,可能表现形式稍有不同;后台则必须以用户登录作为前置条件方可开放,展示一些个人信息、设置、消息等。下文将以前台后台分别称呼这两个站点,以探求两者通信的解决方案。

2. iframe 框架结构

未登录用户从前台使用登录/注册功能时,此时若使之跳转至一个单独的后台登录/注册页面,并让用户在此页面上完成登录/注册流程时,未免使得用户体验变差,并且成功后的跳转可能也难以实现。

理想的方式可以是:点击前台的「登录/注册」按钮 => 弹出或下拉登录/注册弹窗(以及遮罩层) => 完成账号密码输入并提交 => 去往指定页面(一般刷新当前页面即可,由何处登录即返回原处)。

而要把一个单独的后台登录页面变成前台页面的假弹窗,使用 iframe 嵌入无疑会是一个比较好的方法。

分析前台页面和后台页面的角色和功能,可以得出如下需求——

从插件的易用性和扩展性角度讲,把遮罩层整合到后台页面中是比较合理的做法。事实上,前台页面要做的仅仅只是给相应的「登录」按钮绑定打开 iframe 的事件,剩下的一切操作逻辑都交由前台页面来完成。

3. iframe 的同源通信

3.1. 前台页面

若前台页面中登录的应用场景并不太多,那么用 JS 拼接输出 HTML 片段是可以接受的——

  1. // === jQuery ===
  2. var frameURL = './back.html';
  3. $('button.js-login')
  4. .off('click')
  5. .on('click', function() {
  6. $(document.body).prepend(
  7. '<iframe class="js-frame" src="' + frameURL + '"></iframe>'
  8. );
  9. });

3.2. iframe 父子通信操作

当前后台两个站点同源时,可以直接用 JS 或 jQuery 互相操作页面元素通信,达到我们想要的效果。在本例中,前台页面为父页面,后台页面为子页面

父页面获取子页面 document 对象

  1. // === JS ===
  2. document.getElementsByTagName('iframe')[0].contentWindow.document;
  3. window.frames[0].document;
  4. // === jQuery ===
  5. $('iframe').contents();

子页面获取父页面 document 对象

  1. // === JS ===
  2. window.parent.document;
  3. // === jQuery ===
  4. $('<selector>', parent.document);

3.3. 后台页面

我们让前台页面仅触发 iframe,而其它操作均由后台页面来处理,主要包括单击关闭按钮和遮罩层关闭 iframe,提交表单后刷新前台页面,关键代码如下——

  1. // === jQuery ===
  2. $('.js-cover, .js-close')
  3. .off('click')
  4. .on('click', function() {
  5. $('.js-frame', window.parent.document).remove();
  6. });
  7. $('.js-submit')
  8. .off('click')
  9. .on('click', function() {
  10. window.parent.location.reload();
  11. });

4. iframe 跨域通信

跨域通信时,可以用 HTML5 的新特性 window.postMessage 来发送、接收数据,实现通信。

我们不妨指定前台页面的域名为 http://www.domain.com,后台页面域名为 https://user.domain.com

4.1. 后台页面发送数据

window.postMessage 这个新 API 的用法非常简单——

  1. objectWindow.postMessage(message, domain);
  1. // === jQuery ===
  2. var frontDomain = 'http://www.domain.com';
  3. $('.js-cover, .js-close')
  4. .off('click')
  5. .on('click', function() {
  6. window.parent.postMessage('close', frontDomain);
  7. });
  8. $('.js-submit')
  9. .off('click')
  10. .on('click', function() {
  11. window.parent.postMessage('reload', frontDomain);
  12. });

4.2. 前台页面接收数据

接收端通过监听 onmessage 事件即可实现接收。可接收信息内容 (event.data) 以及消息来源地址 (event. origin) 等。

  1. // === jQuery ===
  2. var onmessage = function (event) {
  3. var backDomain = 'https://user.domain.com';
  4. var data = event.data;
  5. var origin = event.origin || event.originalEvent.origin;
  6. if (origin !== backDomain) {
  7. return;
  8. } else {
  9. if (data === 'reload') {
  10. location.reload();
  11. } else if (data === 'close') {
  12. $('.js-frame').remove();
  13. };
  14. };
  15. };
  16. if (typeof window.addEventListener !== 'undefined') {
  17. window.addEventListener('message', onmessage, false);
  18. } else if (typeof window.attachEvent !== 'undefined') {
  19. window.attachEvent('onmessage', onmessage);
  20. };

4.3. 细节处理

通过上述代码可见,domainevent.origin 是消息收发两端互相确认身份的重要设置。为了提高可扩展性,我们可以把前台的域名挂在弹窗的 URL 里 var frameURL = 'http://www.domain.com/?callback_url=http://www.domain.com';。在后台页面里截取这个 callback_url 作为发送消息的 domain——

  1. var href = location.href;
  2. var arg = 'callback_url=http';
  3. if (!!href.match(arg)) {
  4. var domain = 'http' + href.split(arg)[1].split('&')[0];
  5. $('.js-cover, .js-close')
  6. .off('click')
  7. .on('click', function() {
  8. window.parent.postMessage('close', domain);
  9. });
  10. $('.js-submit')
  11. .off('click')
  12. .on('click', function() {
  13. window.parent.postMessage('reload', domain);
  14. });
  15. };

5. 跨域 API 调用

在跨域调用的 API 需要读取 cookie 时,此时是无法奏效的。解决方法是设置 new XMLHttpRequest().withCredentials = true 同步传入 cookie,或者jQuery 设置 xhrFields: { withCredentials: true }。具体代码如下——

  1. // === jQuery ===
  2. $.ajax({
  3. url: '/path/to/file',
  4. type: 'default GET (Other values: POST)',
  5. xhrFields: {
  6. withCredentials: true
  7. },
  8. dataType: 'default: Intelligent Guess (Other values: xml, json, script, or html)',
  9. data: {param1: 'value1'},
  10. })
  11. .done(function() {
  12. console.log("success");
  13. })
  14. .fail(function() {
  15. console.log("error");
  16. })
  17. .always(function() {
  18. console.log("complete");
  19. });

6. 页面源码

6.1. 同源前台页面

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>Front</title>
  6. <style type="text/css">
  7. body {
  8. margin: 0;
  9. padding: 0;
  10. border: 0;
  11. outline: 0;
  12. }
  13. nav {
  14. height: 50px;
  15. line-height: 50px;
  16. background: rgba(0, 0, 255, .4);
  17. text-align: center;
  18. }
  19. iframe {
  20. position: fixed;
  21. width: 100%;
  22. height: 100%;
  23. top: 0;
  24. bottom: 0;
  25. left: 0;
  26. right: 0;
  27. border: 0;
  28. overflow: hidden;
  29. }
  30. </style>
  31. </head>
  32. <body>
  33. <nav>
  34. <button class="js-login">登录</button>
  35. </nav>
  36. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
  37. <script type="text/javascript">
  38. var frameURL = './back.html';
  39. $('button.js-login')
  40. .off('click')
  41. .on('click', function() {
  42. $(document.body).prepend(
  43. '<iframe class="js-frame" src="' + frameURL + '"></iframe>'
  44. );
  45. });
  46. </script>
  47. </body>
  48. </html>

6.2. 同源后台页面

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>Back</title>
  6. <style type="text/css">
  7. form {
  8. position: absolute;
  9. z-index: 20;
  10. top: 50%;
  11. left: 50%;
  12. transform: translate(-50%, -50%);
  13. padding: 20px 30px;
  14. box-shadow: 0 0 10px rgba(0, 0, 0, .2);
  15. border-radius: 5px;
  16. }
  17. input {
  18. display: block;
  19. box-sizing: border-box;
  20. margin: 15px auto;
  21. padding: 0 10px;
  22. width: 200px;
  23. line-height: 30px;
  24. border-radius: 5px;
  25. border: 1px solid rgba(0, 0, 0, .2);
  26. }
  27. .cover {
  28. position: fixed;
  29. width: 100%;
  30. height: 100%;
  31. top: 0;
  32. bottom: 0;
  33. left: 0;
  34. right: 0;
  35. background-color: rgba(255, 255, 255, .8);
  36. z-index: 10;
  37. }
  38. .close {
  39. position: absolute;
  40. right: 0;
  41. top: 0;
  42. padding: 5px;
  43. color: rgba(0, 0, 0, .6);
  44. cursor: pointer;
  45. }
  46. </style>
  47. </head>
  48. <body>
  49. <div class="cover js-cover"></div>
  50. <form>
  51. <span class="close js-close">x</span>
  52. <input type="text" name="user" placeholder="用户名" />
  53. <input type="password" name="password" placeholder="密码" />
  54. <input type="button" name="submit" value="登录" class="js-submit" />
  55. </form>
  56. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
  57. <script type="text/javascript">
  58. $('.js-cover, .js-close')
  59. .off('click')
  60. .on('click', function() {
  61. $('.js-frame', window.parent.document).remove();
  62. });
  63. $('.js-submit')
  64. .off('click')
  65. .on('click', function() {
  66. window.parent.location.reload();
  67. });
  68. </script>
  69. </body>
  70. </html>

6.3. 跨域前台页面

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>Front</title>
  6. <style type="text/css">
  7. body {
  8. margin: 0;
  9. padding: 0;
  10. border: 0;
  11. outline: 0;
  12. }
  13. nav {
  14. height: 50px;
  15. line-height: 50px;
  16. background: rgba(0, 0, 255, .4);
  17. text-align: center;
  18. }
  19. iframe {
  20. position: fixed;
  21. width: 100%;
  22. height: 100%;
  23. top: 0;
  24. bottom: 0;
  25. left: 0;
  26. right: 0;
  27. border: 0;
  28. overflow: hidden;
  29. }
  30. </style>
  31. </head>
  32. <body>
  33. <nav>
  34. <button class="js-login">登录</button>
  35. </nav>
  36. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
  37. <script type="text/javascript">
  38. var frameURL = 'http://www.domain.com/?callback_url=http://www.domain.com';
  39. var backDomain = 'https://user.domain.com';
  40. $('button.js-login')
  41. .off('click')
  42. .on('click', function() {
  43. $(document.body).prepend(
  44. '<iframe class="js-frame" src="' + frameURL + '"></iframe>'
  45. );
  46. });
  47. var onmessage = function (event) {
  48. var data = event.data;
  49. var origin = event.origin || event.originalEvent.origin;
  50. if (origin !== backDomain) {
  51. return;
  52. } else {
  53. if (data === 'reload') {
  54. location.reload();
  55. } else if (data === 'close') {
  56. $('.js-frame').remove();
  57. };
  58. };
  59. };
  60. if (typeof window.addEventListener !== 'undefined') {
  61. window.addEventListener('message', onmessage, false);
  62. } else if (typeof window.attachEvent !== 'undefined') {
  63. window.attachEvent('onmessage', onmessage);
  64. };
  65. </script>
  66. </body>
  67. </html>

6.4. 跨域后台页面

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>Back</title>
  6. <style type="text/css">
  7. form {
  8. position: absolute;
  9. z-index: 20;
  10. top: 50%;
  11. left: 50%;
  12. transform: translate(-50%, -50%);
  13. padding: 20px 30px;
  14. box-shadow: 0 0 10px rgba(0, 0, 0, .2);
  15. border-radius: 5px;
  16. }
  17. input {
  18. display: block;
  19. box-sizing: border-box;
  20. margin: 15px auto;
  21. padding: 0 10px;
  22. width: 200px;
  23. line-height: 30px;
  24. border-radius: 5px;
  25. border: 1px solid rgba(0, 0, 0, .2);
  26. }
  27. .cover {
  28. position: fixed;
  29. width: 100%;
  30. height: 100%;
  31. top: 0;
  32. bottom: 0;
  33. left: 0;
  34. right: 0;
  35. background-color: rgba(255, 255, 255, .8);
  36. z-index: 10;
  37. }
  38. .close {
  39. position: absolute;
  40. right: 0;
  41. top: 0;
  42. padding: 5px;
  43. color: rgba(0, 0, 0, .6);
  44. cursor: pointer;
  45. }
  46. </style>
  47. </head>
  48. <body>
  49. <div class="cover js-cover"></div>
  50. <form>
  51. <span class="close js-close">x</span>
  52. <input type="text" name="user" placeholder="用户名" />
  53. <input type="password" name="password" placeholder="密码" />
  54. <input type="button" name="submit" value="登录" class="js-submit" />
  55. </form>
  56. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
  57. <script type="text/javascript">
  58. var href = location.href;
  59. if (!!href.match(arg)) {
  60. var arg = 'callback_url=http';
  61. var domain = 'http' + href.split(arg)[1].split('&')[0];
  62. $('.js-cover, .js-close')
  63. .off('click')
  64. .on('click', function() {
  65. window.parent.postMessage('close', domain);
  66. });
  67. $('.js-submit')
  68. .off('click')
  69. .on('click', function() {
  70. window.parent.postMessage('reload', domain);
  71. });
  72. };
  73. </script>
  74. </body>
  75. </html>

附:参考

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注