Ideal Ark - 前端技术
type: channel
link: https://www.idealark.cn/category/fontend/
items: SPA前端路由易引发缓存混乱
items: Vue SSR开发环境API代理解决方案
items: Modernizr触屏检测方法
items: 一道考察js闭包的面试题目
items: 用localeCompare实现字符串自然排序
items: 图片索引页左右对齐布局的实现

SPA前端路由易引发缓存混乱
type: item
date: 2022-03-25T10:30:00+08:00
description: 在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-modified或etag可能不同。除非用户对多个path分别手动刷新，否则很容易造成通过链接在多个path间跳转时，页面在新旧版本间变换。尤其当开发者对某个path做了自动重定向redirect，让用户很难对原始path手动刷新，也就没法更新对应的缓存。这样即便对其他所有path都进行了刷新，但只要通过前述path进入页面，都会得到页面的旧版本。解决办法：第一种，对入口文件index.html设置Cache-Control: no-cache以禁用本地缓存；第二种，服务端保存一个前端最新产出的版本号，页面定时拉取这个版本号与自身版本号对比，发现过期即刷新页面。

Vue SSR开发环境API代理解决方案
type: item
date: 2020-09-14T11:20:00+08:00
description: 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错误。解决方法是通过设置axios的baseURL将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触屏检测方法
type: item
date: 2020-03-28T20:52:00+08:00
description: 检测设备是否是触屏设备的方法，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闭包的面试题目
type: item
date: 2020-03-27T20:06:00+08:00
description: 下面这道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的理解，还有一元运算符与二元运算符的运算顺序，这里就不细说了。

用localeCompare实现字符串自然排序
type: item
date: 2020-03-20T15:14:00+08:00
description: 计算机中的字符串排序算法是直接比较字符的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

图片索引页左右对齐布局的实现
type: item
date: 2020-03-16T10:56:00+08:00
description: 最近写了个目录直读的相册程序，用于展示日常拍摄的照片，直接通过FTP上传，免去后台开发和维护的成本，使用起来也很方便。大图展示采用lightbox灯箱插件，效果很不错。但是索引页的图片怎么布局好看呢，参考图虫网的实现方式，每行图片数目固定，行内图片左右对齐，行高固定，单张图片的宽度根据实际比例计算得出。做出来的效果确实不错，计算过程也不复杂。效果图如下。程序是用PHP实现的，后端直接计算好尺寸输出HTML，但实际上这个布局实现过程由前端来实现更科学，于是用js改写了计算方法，代码如下。const containerWidth = 1200; const column = 4; const imagesRow = [ {width: 1200, height: 750, info: {}}, {width: 801, height: 1200, info: {}}, {width: 1200, height: 1200, info: {}}, {width: 1200, height: 675, info: {}} ]; function fixImageSize(imagesRow, containerWidth, column) { let heights = imagesRow.map(image => image.height); let minHeight = Math.min.apply(null, heights); let minWidth = containerWidth / column; // 第一次循环，统一高度 for (let i = 0; i < imagesRow.length; i++) { if (imagesRow[i].height === minHeight) { minWidth = imagesRow[i].width; } else if (imagesRow[i].height > minHeight) { imagesRow[i].width = imagesRow[i].width * minHeight / imagesRow[i].height; imagesRow[i].height = minHeight; } } containerWidth = containerWidth * imagesRow.length / column; let widths = imagesRow.map(image => image.width); let zoom = widths.reduce((prev, next) => prev + next) / containerWidth; let fixedHeight = minHeight / zoom; // 第二次循环，压缩宽度 for (let i = 0; i < imagesRow.length; i++) { imagesRow[i].width = parseInt(imagesRow[i].width / zoom, 10); imagesRow[i].height = parseInt(fixedHeight, 10); } return imagesRow; } let ret = JSON.stringify(fixImageSize(imagesRow, containerWidth, column), null ,2); /*[ { "width": 380, "height": 237, "info": {} }, { "width": 158, "height": 237, "info": {} }, { "width": 237, "height": 237, "info": {} }, { "width": 422, "height": 237, "info": {} } ]*/相册写好后，先把大学期间上传到人人网的照片备份了一下，说不定哪天人人网又不行了，还能留个怀念，地址