V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Terry05
V2EX  ›  程序员

Vue 兼容 ie9 的全面解决方案

  •  2
     
  •   Terry05 ·
    TerryZ · 2018-06-19 10:37:00 +08:00 · 4767 次点击
    这是一个创建于 2350 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    背景情况

    • vue - 2.5.11
    • vue-cli 使用模板 webpack-simple
    • http 请求:axios

    Vue 官方对于 ie 浏览器版本兼容情况的描述是 ie9+,即是 ie9 及更高的版本。经过测试,Vue 的核心框架 vuejs 本身,以及生态的官方核心插件( VueRouter、Vuex 等)均可以在 ie9 上正常使用。

    Vue 的作者尤雨溪对于 Vue 的学习建议 中有提及为了将项目更好的生态化 /工程化,要尽可能学习及使用新的 ECMAScript 规范。目前 ES6/ES2015 是可用度和稳定度较高的规范,文档齐全,国内还有 阮一峰 《 ECMAScript 6 入门》 做了大量的文档翻译,开发环境可谓完善。然而版本较旧的浏览器并不支持 es6 规范,尤其是 ie 浏览器,即使是最高的 ie11 版本,对于 es6 规范也支持得并不全。如此则需要对所有原生不支持 ES6 特性的浏览器做兼容性处理。

    本文将针对使用 Vue 生态开发完成的网站,以 ie9 版本为基础兼容目标,实现全功能正常使用的全面兼容解决方案。

    ES6 兼容

    在 ie9 的环境上,es6 的部分新对象、表达式,并不支持,解决方案是使用 babel-polyfill 组件,它可以将 es6 的代码翻译成低版本浏览器可以识别的 es5 代码

    npm i babel-polyfill --save-dev
    

    安装完成后,在项目的主入口文件 main.js 的首行就可以直接引用

    import 'babel-polyfill';
    

    在项目使用 vue-cli 生成的代码中,根目录有一个 .babelrc 文件,这是项目使用 babel 的配置文件。在默认生成的模板内容中,增加 "useBuiltIns": "entry" 的设置内容,这是一个指定哪些内容需要被 polyfill(兼容) 的设置

    useBuiltIns 有三个设置选项

    • false - 不做任何操作
    • entry - 根据浏览器版本的支持,将 polyfill 需求拆分引入,仅引入有浏览器不支持的 polyfill
    • usage - 检测代码中 ES6/7/8 等的使用情况,仅仅加载代码中用到的 polyfill

    这里推荐设置为 entry ,完整的 .babelrc 内容如下:

    {
      "presets": [
        [
          "env",
          {
            "modules": false,
            "useBuiltIns": "entry"
          }
        ],
        "stage-3"
      ]
    }
    
    

    加入这些代码后,工程里的大部分内容已可兼容到 ie9 版本

    Number 对象

    即使在使用 babel-polyfill 做代码翻译后,发现还是有一些 es6 的新特性并没有解决,比如说 Number 对象的 parseIntparseFloat 方法

    es6 将全局方法 parseInt()parseFloat() ,移植到 Number 对象上面,行为完全保持不变。这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

    解决这个问题不需要引入包来解决,同样在项目主入口文件 main.js 加入以下代码(代码尽可能靠前,最好是在引用 babel-polyfill 之后 )

    if (Number.parseInt === undefined) Number.parseInt = window.parseInt;
    if (Number.parseFloat === undefined) Number.parseFloat = window.parseFloat;
    

    requestAnimationFrame 方法

    window.requestAnimationFrame 是浏览器用于定时循环操作的一个接口,类似于 setTimeout,主要用途是按帧对网页进行重绘。

    requestAnimationFrame 的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率( 60Hz 或 75Hz ),也就是说,每秒最多只能重绘 60 次或 75 次,requestAnimationFrame 的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个 API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了 CPU、GPU 和电力。

    不过有一点需要注意,requestAnimationFrame 是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame 的动画效果会大打折扣。

    window.requestAnimationFrame() 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。

    有部分第三方组件就使用了这个方法,例如部分文件上传、图片处理类的组件;那么在这类型的组件在 ie9 下使用时,会报出

    SCRIPT5007: Expected object.
    

    window.requestAnimationFrame() 的最低兼容 ie 版本为 10,那么在 ie9 上做兼容就需要制作 requestAnimationFrame polyfill

    (function() {
        var lastTime = 0;
        var vendors = ['ms', 'moz', 'webkit', 'o'];
        for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
            window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
            window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                       || window[vendors[x]+'CancelRequestAnimationFrame'];
        }
     
        if (!window.requestAnimationFrame)
            window.requestAnimationFrame = function(callback, element) {
                var currTime = new Date().getTime();
                var timeToCall = Math.max(0, 16 - (currTime - lastTime));
                var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
                  timeToCall);
                lastTime = currTime + timeToCall;
                return id;
            };
     
        if (!window.cancelAnimationFrame)
            window.cancelAnimationFrame = function(id) {
                clearTimeout(id);
            };
    }());
    

    Gist:requestAnimationFrame polyfill

    这部分代码同样是尽可能在网站入口处就执行

    http 网络请求(跨域)

    在大多数的 Web 项目中(以 JavaWeb 为例),网站的页面和服务(至少是 controller 层)在同一个工程进行开发和部署,在大前端的新型模式下,我们建议尽可能对网站的前端和后端进行完全分离,前后端分离的好处和意义这里不再赘述。

    既然是前后端分离,那么部署也必然是各自独立部署,不同的访问路径,就会产生跨域访问的问题(同一站点,不同端口号也是跨域)

    在此设定背景情况:

    • 服务端已完整开启 CROS 跨域支持
    • http 组件使用 axios
    • axios 设置 withCredentials 为 true 开启跨域访问时携带 cookie 数据

    高版本浏览器( ie10+ 或 chrome, ff )仅需要完成背景情况中的功能,即可支持跨域数据请求功能

    axios 进行数据请求时,默认使用 XMLHttpRequest 对象,在检测到当前请求是跨域访问时,axios 会测试浏览器是否支持 XDomainRequest 对象,若支持则优先使用。

    ie8 / ie9 的 XMLHttpRequest 对象,不支持跨域访问,该对象在 ie10 后才原生支持跨域访问。微软的解决方案是在 ie8 / ie9 中提供了 XDomainRequest(XDR) 对象来进行解决跨域问题,虽然使用该对象可以跨域访问成功,并返回数据,但它却依然是一个功能不完整的半成品,它的使用有诸多限制:

    • XDR 仅支持 GET 与 POST 两种请求方式
    • XDR 不支持自定义的请求头,若服务端使用 header 的自定义参数进行做身份验证,则不可用
    • 请求头的 Content-Type 只允许设置为 text/plain
    • XDR 不允许跨协议的请求,如果网页在 HTTP 协议下,就只能请求 HTTP 协议下的接口,不能访问 HTTPS 接口
    • XDR 只接受 HTTP/HTTPS 的请求
    • 发起请求的时候,不会携带 authenticationcookies

    微软虽然提供了解决方案,但却是不折不扣的鸡肋,根本无法胜任系统中各种场景的数据请求需求,至此,axios 对 ie9 的跨域数据请求已无能为力。

    完美解决方案:代理(proxy)

    虽然 axios 对 ie9 跨域已无能为力,但前端项目打包的解决方案 webpack 提供了一个优雅而彻底解决问题的方式:代理

    devServer.proxy

    webpack 的 devServer.proxy 的功能是由 http-proxy-middleware 项目来实现的

    实现原理是将目标位置的请求代理为前端服务本地的请求,既然是代理成为本地的请求,就不存在跨域的问题,axios 就会用回 XMLHttpRequest 对象进行数据请求,一切都恢复正常了,header、cookies、content-type、authentication 等内容都被正确传递到服务端。

    项目中 webpack.config.js 的配置

    devServer: {
        historyApiFallback: true,
        noInfo: true,
        overlay: true,
        proxy: {
            '/api': {
                target: 'http://localhost:8081/myserver',
                pathRewrite: {
                    '^/api': ''
                }
            }
        }
    }
    

    配置中指定了将 http://localhost:8081/myserver 服务的位置代理为本地前端服务的 http://localhost:8080/api。例如需要读取用户信息的原请求是 http://localhost:8081/myserver/user/zhangsan,代理后,就变为 http://localhost:8080/api/user/zhangsan

    即是 /api 的前缀代表了服务端,所以在使用 axios 时,需要对每个服务端请求都增加上 /api 的前缀;通常在项目开发中,需要对数据请求组件 axios 进行二次封装,以达到统一设置默认参数,统一数据请求入口等目的,那么此时就只需要在二次封装的文件里统一调整请求前缀即可。

    不过,webpack 的 devServer.proxy 仅在开发模式下可用,生产模式下无法使用。开发模式下,调试服务可以读取 webpack.config.js 中的配置内容进行实时代理,而项目在部署到生产环境前,需要将工程进行编译转换成静态的 js 文件,没有调试服务的支撑自然是无法进行请求代理的。

    nginx 配置

    虽然 devServer.proxy 的功能仅能工作于开发模式,那么在生产模式下,自然也是有解决方案的;通常 Vue 的项目在编译成最终的 js 文件后,仅需要静态服务器即可,这其中又以 nginx 为最优选择方案,轻量、高性能、高并发、反向代理服务等均为其优点,这里需要做的数据请求代理的功能就使用到了 nginx 的 反向代理 功能

    conf/nginx.conf 文件配置增加以下内容

    location /api/ {
        proxy_pass http://localhost:8081/myserver/;
    }
    

    该配置同样是将 http://localhost:8081/myserver/ 的目标服务端位置代理为本地服务的 /api 路径,如此,生产环境下的数据请求问题也得以解决

    个人原创内容,转载请说明出处

    完整内容:https://github.com/TerryZ

    17 条回复    2018-06-19 14:56:32 +08:00
    e8c47a0d
        1
    e8c47a0d  
       2018-06-19 11:35:25 +08:00   ❤️ 1
    连 IE 11 都被我放弃了……(以下省略一万字)
    cnbobolee
        2
    cnbobolee  
       2018-06-19 11:52:07 +08:00   ❤️ 2
    放弃 IE
    Terry05
        3
    Terry05  
    OP
       2018-06-19 12:24:17 +08:00
    回楼上的各位,做大型系统是没办法这么任性的
    k9982874
        4
    k9982874  
       2018-06-19 12:25:57 +08:00
    @Terry05 #3 兼容个 IE6 ?
    Eoss
        5
    Eoss  
       2018-06-19 12:29:25 +08:00 via iPhone   ❤️ 1
    珍爱头发,远离 IE。
    thread2
        6
    thread2  
       2018-06-19 12:32:09 +08:00 via Android
    有些前端直接推荐使用 Chrome,火狐也不考虑下
    murmur
        7
    murmur  
       2018-06-19 12:35:24 +08:00
    @k9982874 钱给够自然有人做 IE6 兼容 不需要键盘侠嘲讽 招个 IE6 年代过来的人就行
    OSF2E
        8
    OSF2E  
       2018-06-19 12:37:04 +08:00
    好文章,支持楼主
    Terry05
        9
    Terry05  
    OP
       2018-06-19 12:38:23 +08:00
    @k9982874 抬杠了不是
    OSF2E
        10
    OSF2E  
       2018-06-19 12:40:18 +08:00
    @Terry05 那位是资深杠精
    k9982874
        11
    k9982874  
       2018-06-19 12:55:35 +08:00
    @murmur #7
    @Terry05 #9 IE6 的梗娱乐一下不要鸡冻

    @OSF2E #10 我都这么有名了啊,不到一个月的新会员都认识我,骄傲一下~
    OSF2E
        12
    OSF2E  
       2018-06-19 13:00:58 +08:00
    @k9982874 谁没个马甲……手滑忘了 @ ,我在#10 说的是 @murmur,让你白骄傲了
    superkey
        13
    superkey  
       2018-06-19 13:04:31 +08:00
    ie 不支持 vue-router history 模式,挺折腾人.
    Mutoo
        14
    Mutoo  
       2018-06-19 13:37:46 +08:00
    @Terry05 以前见过非常任性的大型系统(银行)只支持 IE6
    ryuzaki113
        15
    ryuzaki113  
       2018-06-19 14:11:47 +08:00
    IE9 就好好用 jQuery,用什么 vue
    zhlssg
        16
    zhlssg  
       2018-06-19 14:16:33 +08:00
    文章不错
    wu67
        17
    wu67  
       2018-06-19 14:56:32 +08:00
    其实你可以考虑用 beta, 正式版的最后一次发布距今已经很久了
    npm install --save @babel/polyfill
    然后在 main.js 顶部引入就行了, 貌似不用改 webpack 配置吧
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   6020 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 02:16 · PVG 10:16 · LAX 18:16 · JFK 21:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.