分类 前端技术 下的文章

在React或Vue中使用前端路由的history模式时,由于服务端会配合使用URL Rewrite技术,产生的效果就是,SPA页面多个path的请求实际返回的是同一个入口文件。

GET https://idealecho.cn/spa/apath
GET https://idealecho.cn/spa/bpath

Response https://idealecho.cn/spa/index.html

在页面采用强缓存策略的情况下,当服务端文件更新后,用户需要手动刷新页面强行使用协商缓存机制获取最新版本,并更新本地缓存副本。而浏览器对页面进行缓存,使用的key是请求中的path值,这就导致本地会产生多份入口文件的缓存副本,并且版本可能并不一致。

在上例中,浏览器为/spa/apath/spa/bpath两个path分别缓存index.html文件副本,last-modifiedetag可能不同。

除非用户对多个path分别手动刷新,否则很容易造成通过链接在多个path间跳转时,页面在新旧版本间变换。

尤其当开发者对某个path做了自动重定向redirect,让用户很难对原始path手动刷新,也就没法更新对应的缓存。这样即便对其他所有path都进行了刷新,但只要通过前述path进入页面,都会得到页面的旧版本。

解决办法:

第一种,对入口文件index.html设置Cache-Control: no-cache以禁用本地缓存;

第二种,服务端保存一个前端最新产出的版本号,页面定时拉取这个版本号与自身版本号对比,发现过期即刷新页面。

Vue SSR开发环境中涉及到的数据API代理设置解决方案,整理总结如下。
此方案采用vue-cli@2 + webpack@3 + node + express + axios架构。

一、devServer代理设置

用devServer作为CSR模式的本地开发服务器时,数据请求由浏览器发出,devServer响应,proxy直接配置在webpack.dev.conf中即可。

// config/index.js
module.exports = {
    dev: {
        proxyTable: {
            '/api': {
                target: 'http://dev.wrrok.com:8080/',
                changeOrigin: true
            }
        },
        apiProxy: 'http://dev.wrrok.com:8080/'
    }
};
// build/webpack.dev.conf.js
const devWebpackConfig = merge(baseWebpackConfig, {
    devServer: {
        proxy: config.dev.proxyTable
    }
});

二、express服务器代理设置

用express作为SSR模式的本地开发服务器时,客户端渲染期间的数据请求由浏览器发出,express响应,proxy需要配置在express中。

// server.js
const {createProxyMiddleware} = require('http-proxy-middleware');
app.use('/api', createProxyMiddleware({
    target: 'http://dev.wrrok.com:8080/',
    changeOrigin: true,
    ws: true
}));

三、服务端数据预取请求Mock

在Vue的SSR服务端渲染期间,服务端数据预取请求由node发出,通过以上两种方式配置的API代理地址均不起作用。如果本地不存在对应的API,会报Error: connect ECONNREFUSED 127.0.0.1:80错误。解决方法是通过设置axiosbaseURL将API请求Mock到数据服务器地址,此处用到了webpack的DefinePlugin插件,具体实现如下:

// webpack.server.conf.js
module.exports = merge(baseWebpackConfig, {
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': '"development"',
            'process.env.VUE_ENV': '"server"',
            'process.env.API_BASE_URL': JSON.stringify(config.dev.apiProxy)
        })
    ]
});
// api.js
import http from 'axios';
if (process.env.NODE_ENV === 'development' && process.env.VUE_ENV === 'server') {
    http.defaults.baseURL = process.env.API_BASE_URL;
}

这样就可以根据Vue的实际运行环境,在服务端数据预取时自动加上API代理地址,客户端渲染时仍然按照devServer的proxy进行请求。

参考链接:axios的Config Defaults webpack的DefinePlugin插件

检测设备是否是触屏设备的方法,Modernizr是这样实现的:

// Modernizr现在使用的方法
('ontouchstart' in window)
|| window.TouchEvent
|| window.DocumentTouch && document instanceof DocumentTouch

// Modernizr早期使用的方法
function isTouchDevice() {
  try {
    document.createEvent('TouchEvent');
    return true;
  } catch (e) {
    return false;
  }
}

但是随便在Mac上的Chrome78.0中验证了一下,发现是存在window.TouchEvent的,所以所谓的触屏检测方法其实并不可靠,可以看一下这篇You Can't Detect A Touchscreen

参考链接:Modernizr源码

下面这道js面试题目,主要考察对闭包概念的理解,用了好几年,在一面中能完全答对的面试者寥寥。这几天恰巧又在面试,想起这道题目,贴出来分析一下。

var a = 0, b = 0;
function A(a) {
  A = function B(b) {    
    alert(a+b++);
  }
  alert(a++);
}
A(1);
A(2);

把代码粘到浏览器控制台执行,很容易得出输出结果是1和4。

首先,这段代码只有两次输出,说明每次调用函数A,只执行了一次alert。很多同学会认为有四次输出,这里涉及的考点是函数声明和命名函数表达式。第3行的赋值语句,使用函数表达式更新了函数A为函数B,于是第二次调用函数A,实际执行的是函数B中的代码,所以每次调用只有一个alert执行。

其次是本道题目的重点,闭包变量a。第一次调用函数A,第6行的alert输出1,参数a在函数执行结束后的值是2,而这个a此时作为闭包变量留在了命名函数表达式中。第二次调用函数A,传入的2实际对应函数B的参数b,而此时函数B中的变量a的值也是2,于是第4行的alert执行,输出4。

最后,考察对i++++i的理解,还有一元运算符与二元运算符的运算顺序,这里就不细说了。

计算机中的字符串排序算法是直接比较字符的ASCII顺序,这样得出的排序结果有时候并不符合人类思维。比如下面这个文件名排序的例子。

一组文件1.jpg 2.jpg 10.jpg 100.jpg,排序后会得到这样的结果1.jpg 10.jpg 100.jpg 2.jpg,显然2没有10和100大,应该排在它们的前面才对。Windows资源管理器中的按名称排序就是这样实现的(Mac修正了这个问题)。

对字符串排序时能够智能地识别出其中的数字,归属于自然排序问题。对于自然排序问题,大多数编程语言都没有原生实现。PHP中有原生方法natsort()strnatcmp()提供支持,但是JavaScript却无能为力。

localeCompare方法可以按照本地语言习惯对字符串进行比较,能够实现汉字按音序排序,但是直接使用依然无法解决包含数字的字符串的自然排序问题。幸好,新的localeCompare标准增加了numeric属性,用于指定是否启用数字排序。虽然这一标准目前还没有得到所有浏览器的支持,但前景是好的,语法如下:

strA.localeCompare(strB, undefined, {numeric: true})

利用localeCompare这个新特性,我们可以很容易实现包含数字的字符串的自然排序,代码如下。

function naturalCmp(a, b) {
    return a.localeCompare(b, undefined, {numeric: true, sensitivity: 'base'});
}
var a = ["1.a", "100.a", "10.a", "2.a.10.b", "2.a.2.b"];
a.sort(naturalCmp);
// ["1.a", "2.a.2.b", "2.a.10.b", "10.a", "100.a"]

这种方法的局限性是,它只能分段识别字符串中的独立数字并进行比较,如果想更智能地区分浮点数、科学计数法或者日期的话,则可以使用这个javascript-natural-sort

在收集资料的过程中发现,早在1997年,David Koelle实现了Alphanum算法,解决了字母数字组合字符串的自然排序问题,相比上文localeCompare的实现方式,该算法把点号无条件当做数字中的小数点来参与数值比较,而不是智能地适时当做分隔符来处理,最终的效果不尽完美。毕竟是20多年前的实现嘛,但该算法的思想还是很值得我们借鉴的。Brian Huisman基于该算法的JS实现代码摘录如下。

Array.prototype.alphanumSort = function(caseInsensitive) {
  for (var z = 0, t; t = this[z]; z++) {
    this[z] = new Array();
    var x = 0, y = -1, n = 0, i, j;

    while (i = (j = t.charAt(x++)).charCodeAt(0)) {
      var m = (i == 46 || (i >=48 && i <= 57));
      if (m !== n) {
        this[z][++y] = "";
        n = m;
      }
      this[z][y] += j;
    }
  }

  this.sort(function(a, b) {
    for (var x = 0, aa, bb; (aa = a[x]) && (bb = b[x]); x++) {
      if (caseInsensitive) {
        aa = aa.toLowerCase();
        bb = bb.toLowerCase();
      }
      if (aa !== bb) {
        var c = Number(aa), d = Number(bb);
        if (c == aa && d == bb) {
          return c - d;
        } else return (aa > bb) ? 1 : -1;
      }
    }
    return a.length - b.length;
  });

  for (var z = 0; z < this.length; z++)
    this[z] = this[z].join("");
}

参考链接:MDN上的localeCompare文档 Alphanum Algorithm