前端中代码最骚的

前端中代码最骚的 查看完整档案

北京编辑北京语言大学  |  计算机科学与技术 编辑非正常人类研究中心  |  前端工程师 编辑 lijinglun.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

前端中代码最骚的 收藏了文章 · 2020-02-27

V8引擎的内存管理

本文首发于公众号:符合预期的CoyPan
这是一篇译文,有部分删减

原文地址:https://deepu.tech/memory-man...

原文标题:Visualizing memory management in V8 Engine (JavaScript, NodeJS, Deno, WebAssembly)

在本章中,我们将介绍用于ECMAScript和WebAssembly的V8引擎的内存管理,这些引擎用于NodeJS、Deno&Electron等运行时,以及Chrome、Chromium、Brave、Opera和Microsoft Edge等web浏览器。由于JavaScript是一种解释性语言,它需要一个引擎来解释和执行代码。V8引擎解释JavaScript并将其编译为机器代码。V8是用C++编写的,可以嵌入任何C++应用程序中。

首先,我们来看看V8引擎的内存结构。由于JavaScript是单线程语言,所以V8为每一个JavaScript上下文使用一个进程。如果你使用service worker,V8会为每个service worker开启一个新的进程。在V8进程中,一个正在运行的程序总是由一些分配的内存来表示,这称为常驻集Resident Set)。可以进一步划分以下不同的部分:

0.png

这和我们在上一篇文章中提到的JVM有些相似。我们来看一看每一个部分都是做什么的:

堆内存(Heap memory)

这是V8存储对象和动态数据的地方。这是内存中区域中最大的块,也是垃圾回收(GC)发生的地方。整个堆内存不是垃圾回收的,只有新旧空间(New space、Old space)是垃圾回收管理的。堆内存可以进一步划分为以下几部分:

  1. 新空间(New space)

新空间(或者说叫:新生代),是存储新对象的地方,并且大部分对象的声明周期都很短。这个空间很小,有两个半空间,类似于JVM中的S0,S1。这片空间是由Scavenger(Minor GC)来管理的,稍后会介绍。新生代空间的大小可以由--min_semi_space_size(初始值) 和 --max_semi_space_size(最大值) 两个V8标志来控制。

  1. 老空间(Old space)

老空间(或者说叫:老生代),存储的是在新生代空间中经过了两次Minor GC后存活下来的数据。这片空间是由Major GC(Mark-Sweep & Mark-Compact)”管理的,稍后会介绍。老生代空间的大小可以--initial_old_space_size(初始值) and --max_old_space_size(最大值) 两个V8标志来控制。这片空间被分成了两个部分:

  • 老指针空间(Old pointer space):包含了存活下来的包含指向其他对象指针的对象。
  • 老数据空间(Old data space):包含了仅保存数据的对象(没有指向其他对象的指针)。字符串,已装箱的数字,未装箱的双精度数组,在新生代空间经过两轮Minor GC后存活下来的,会被移到老数据空间。
  1. 大对象空间(Large object space)

这是大于其他空间大小限制的对象存储的地方。每个对象都有自己的内存区域。大对象是不会被垃圾回收的。

  1. 代码空间(Code-space)

这就是即时(JIT)编译器存储编译代码块的地方。这是唯一有可执行内存的空间(尽管代码可能被分配在“大对象空间”中,它们也是可执行的)。

  1. 单元空间、属性单元空间、映射空间(Cell space, property cell space, and map space)

这些空间分别包含Cell,PropertyCell 和 Map. 这些空间中的每一个都包含相同大小的对象,并且对它们指向的对象类型有一些限制,这简化了收集。

每个空间都由一组页组成。页是使用 mmap从操作系统分配的连续内存块。每页大小为1MB,但大对象空间较大。

栈(Stack)

这是栈内存区域,每个V8进程有一个栈。这里存储静态数据,包括方法/函数框架、原语值和指向对象的指针。栈内存限制可以使用--stack_size V8标志设置。

V8的内存使用(栈 VS 堆)

既然我们已经清楚了内存是如何组织的,让我们看看在执行程序时如何使用其中最重要的部分。

让我们使用下面的JavaScript程序,代码没有针对正确性进行优化,因此忽略了不必要的中间变量等问题,重点是可视化栈和堆内存的使用情况。

class Employee {
    constructor(name, salary, sales) {
        this.name = name;
        this.salary = salary;
        this.sales = sales;
    }
}

const BONUS_PERCENTAGE = 10;

function getBonusPercentage(salary) {
    const percentage = (salary * BONUS_PERCENTAGE) / 100;
    return percentage;
}

function findEmployeeBonus(salary, noOfSales) {
    const bonusPercentage = getBonusPercentage(salary);
    const bonus = bonusPercentage * noOfSales;
    return bonus;
}

let john = new Employee("John", 5000, 5);
john.bonus = findEmployeeBonus(john.salary, john.sales);
console.log(john.bonus);

可以通过下面的ppt看一下在上面的代码执行的过程中,栈内存和堆内存是如何使用的。

ppt-1.png
ppt-2.png
ppt-3.png

如你所见:

  1. 全局作用域保存在栈上的全局框架(Global frame)中。
  2. 每个函数调用都作为帧块添加到堆栈内存中。
  3. 所有局部变量(包括参数和返回值)都保存在栈的函数框块中。
  4. 像int&string这样的所有基元类型都直接存储在栈上。这同样适用于全局作用域。
  5. 当前函数调用的函数将被推到栈的顶部。
  6. 当函数返回时,它的框架帧块将被移除。
  7. 一旦主进程完成,堆上的对象就不再有来自栈的指针,成为孤立的对象。
  8. 除非显式复制,否则其他对象中的所有对象引用都是使用引用指针完成的。

如你所见,栈是由操作系统自动管理的,而不是V8。因此,我们不必太担心栈。另一方面,堆并不是由操作系统自动管理的,因为堆是最大的内存空间,并保存动态数据,它可能会随着时间的推移呈指数增长,导致我们的程序内存耗尽。随着时间的推移,它也变得支离破碎,减慢了应用程序的速度。这就是为什么需要垃圾回收。

区分堆上的指针和数据对于垃圾收集很重要,V8使用“标记指针”方法来实现这一点。在这种方法中,它在每个单词的末尾保留一个位,以指示它是指针还是数据。这种方法需要有限的编译器支持,但实现起来很简单,同时效率也相当高。

V8内存管理 - 垃圾回收(GC)

现在我们知道了V8如何分配内存,让我们看看它如何自动管理堆内存,这对应用程序的性能非常重要。当一个程序试图在堆上分配比自由可用的更多的内存(取决于V8标志集)时,我们会遇到内存不足的错误。错误管理的堆也可能导致内存泄漏。

V8通过垃圾收集来管理堆内存。简单地说,它释放孤立对象(即不再直接或间接从堆栈中引用的对象(通过另一个对象中的引用)使用的内存,以便为创建新对象腾出空间。

Orinoco是V8 GC项目的代码名,用于使用并行、增量和并发的垃圾回收技术来释放主线程。

V8中的垃圾回收器负责回收未使用的内存,供V8进程重用。

V8垃圾回收器是分代的(堆中的对象按其年龄分组并在不同阶段清除)。V8有两个阶段和三种不同的垃圾收集算法:

Minor GC (Scavenger)

这种类型的GC保持新生代空间的紧凑和清洁。对象被分配到相当小的空间(1到8MB之间,取决于行为启发)。新生代空间的分配成本很低:有一个分配指针,每当我们想为新对象保留空间时,它都会递增。当分配指针到达新生代空间的末尾时,将触发次Minor GC。这个过程被称为Scavenger,实现了“切尼算法”。Minor GC经常出现并使用并行的辅助线程,而且速度非常快。

让我们来看一看Minor GC的过程:

新生代空间被分成两个大小相等的半空间:from-space和to-space。大多数分配都是在to-space中进行的(除了某些类型的对象,例如总是在老生代空间中分配的可执行代码)。当to-space填满时,将触发Minor GC。完成过程如下:

  1. 当我们开始时,假设to-space里已经有对象了。
  2. 进程创建了一个新的对象。
  3. V8试图从to-space获取所需的内存,但其中没有可用空间来容纳我们的对象,因此V8触发了Minor GC。
  4. Minor GC交换to-space和from-space,所有对象现在都在from-space中,to space为空。
  5. Minor GC递归地从堆栈指针(GC根)开始遍历from-space中的对象图,以查找已使用或活动的对象(已用内存)。这些对象将移动到to-space的页中。由这些对象引用的任何对象也会在to-space中移动到此页,并且它们的指针会更新。重复此操作,直到from-space中的对象都被扫描一次。最终,to-space被自动压缩以减少碎片。
  6. Minor GC现在清空from-space,因为这里的任何剩余对象都是垃圾。
  7. 新对象被分配到to-space的内存空间中。
  8. 让我们假设过了一段时间,to-space中的对象更多了。
  9. 应用又新建了一个对象。
  10. V8试图从to-space获取所需的内存,但其中没有可用空间来容纳我们的对象,因此V8触发了第二次Minor GC。
  11. 重复上述过程,并将第二个Minor GC中幸存的任何活动对象移动到老生代空间。第一次Minor GC的幸存者被转移到to-space,剩余的垃圾从from-space中被清除。
  12. 新对象被分配到to-space的内存空间中。

我们看到了Minor GC如何从新生代内存空间那里回收空间并使其保持紧凑的。这个过程虽然会停止其他操作,但是这个过程是十分迅速而有效的,大部分时候都微不足道。由于此进程不扫描老生代空间中的对象以获取新生代空间中的任何引用,因此它使用从老生代空间到新生代空间的所有指针的寄存器。这将由一个名为write barriers的进程记录到存储缓冲区。

Major GC

这种类型的GC保持了老生代空间的紧凑和干净。当V8根据动态计算的限制确定没有足够的老生代空间时,就会触发此操作,因为它是从Minor GC周期中填充的。

Scavenger算法非常适合于较小的数据量,但对于较大的老生代空间来说是不实际的,因为它有内存开销,因此主要的GC是使用Mark-Sweep-Compact算法完成的。它使用三色(白灰黑)标记系统。因此,Major GC是一个三步过程,第三步是根据分段启发执行的。

15.gif

  • 标记:第一步,两种算法都通用,其中垃圾回收器标识哪些对象正在使用,哪些对象未在使用。递归地从GC根(栈指针)中使用中或可访问的对象被标记为活动的。从技术上讲,这是对堆的深度优先搜索,可以看作是有向图。
  • 清理:垃圾回收器遍历堆并记录任何未标记为活动的对象的内存地址。这些空间现在在空闲列表中被标记为空闲,可用于存储其他对象。
  • 压缩:清理后,如果需要,将所有剩下的对象移动到一起。这将减少碎片并提高向较新对象分配内存的性能。

这种类型的GC也称为stop-the-world GC,因为它们在执行GC的过程中引入了暂停时间。为了避免这个V8使用了如下技术:

16.png

  • 增量GC:GC是以多个增量步骤而不是一个增量步骤完成的。
  • 并发标记:标记是在不影响主JavaScript线程的情况下使用多个辅助线程并发完成的。Write barriers用于跟踪JavaScript在帮助程序并发标记时创建的对象之间的新引用。
  • 并发扫描/压缩:扫描和压缩在助手线程中同时完成,而不影响主JavaScript线程。
  • 延迟清理:延迟清理,包括延迟删除页中的垃圾,直到需要内存为止。

让我们来看一下 major GC的过程:

  1. 让我们假设许多Minor GC周期已经过去,旧空间几乎满了,V8决定触发一个Major GC
  2. Major GC从栈指针开始递归地遍历对象图,以标记在老生代空间中用作活动(已用内存)和剩余对象作为垃圾(孤立)的对象。这是使用多个并发助手线程完成的,每个助手都跟随一个指针。这不会影响主JS线程。
  3. 当并发标记完成或达到内存限制时,GC使用主线程执行标记终结步骤。这将引入一个小的暂停时间。
  4. Major GC现在使用并发扫描线程将所有孤立对象的内存标记为空闲。并行压缩任务也会被触发,以将相关内存块移动到同一页以避免碎片化。在这些步骤中会更新指针。

结论

本文将为您提供V8内存结构和内存管理的概述。这里没有做到面面俱到的,还有很多更高级的概念,您可以从v8.dev中了解它们。但是对于大多数JS/WebAssembly开发人员来说,这一级别的信息就足够了,我希望它能帮助您编写更好的代码,考虑到这些因素,对于更高性能的应用程序,记住这些可以帮助您避免下一个可能遇到的内存泄漏问题。

最新的.png

查看原文

前端中代码最骚的 收藏了文章 · 2020-01-08

动图学 JavaScript 之: JS 引擎原理

前言

JS 实在是太酷了(认真脸),那你有没有想过机器是怎么解析 JS 代码的?作为一个 JS 开发者,一般我们不需要直接跟编译器打交道,但是如果可以了解其中的基本原理,相信会对以后的工作和学习都有帮助的!

本篇介绍的知识主要基于 Node.js 和基于 Chromium 的浏览器所用的 V8 引擎

生成抽象语法树

HTML 解析器在遇到 script 标签时,便会加载其中的代码。代码可能是从 网络请求缓存 或者 Service Worker 中加载的。由于代码是以 字节流 的形式响应回来的,所以当代码下载完成后就会交给 字节流解码器

1-byte-stream.gif

词法分析

生成抽象语法树的 第一个阶段是分词(tokenize),又叫词法分析

字节流解码器会先从代码字节流中创建 令牌 (token)

注:令牌可以理解为语法上不可能再分的,最小的单个字符或字符串)。

如:0066 解码为 f0075 解码为 u0063 解码为 c0074 解码为 t0069 解码为 i006f 解码为 o006e 解码为 n 同时后面跟一个空格。然后你就得到了关键字 function

每当一个 令牌 创建后,就会被传递给 解析器(parser)。具体见下图:

2-to-parser.gif

语法分析

第二个阶段是解析(parse),也叫语法分析

引擎其实使用了两个解析器。一个是 预解析器,一个是 解析器

预解析器会先检查源码是否符合语法规则,如果不符合就直接抛出错误。这个提前检查机制可以提高解析器的效率。

如果没有错误,解析器便会根据传过来的令牌创建出 抽象语法树 (Abstract Syntax Tree) 并生成 执行上下文 (关于执行上下文的知识我们有机会再讲)

3-ast.gif

生成字节码

AST 被生成之后,接下来就要交给 解释器(interpreter) 了。解释器会遍历整个 AST,并生成 字节码。当字节码生成后,AST 便会被删除以节省内存空间。最终我们得到了更贴近 机器码字节码

这里的 字节码 是介于 AST机器码 之间的一种代码,它还是需要通过 解释器 将其转换为 机器码 后才能执行

4-byte-code.gif

执行代码

生成了字节码之后,就可以进入执行阶段了。执行阶段过程中引擎会做一些优化操作,一个是 即时编译,一个是 内联缓存

即时编译

尽管 字节码 很快,但是它还可以更快!解释器在逐条解释执行字节码时,会分析是否有某段代码被多次执行,这样的代码被称为 热点代码

热点代码 和生成的 类型反馈 (type feedback) 会被发送到一个称为 优化编译器 的东西中,然后由它转换为可以直接被电脑执行的 机器码,这样在下次执行这段代码的时候就不需要再编译了,从而大大提升了代码的执行效率。

这种技术也被称为 即时编译(JIT:Just In Time),而上面所说的 优化编译器 也叫 JIT 编译器

5-jit.gif

内联缓存

JavaScript 是一种动态类型的语言,这意味着数据类型可以不断变化。如果 JS 引擎每次都要检查数据的类型,那速度将会非常慢。

所以引擎就使用了一种叫做 内联缓存 (inline caching) 的技术。它将代码缓存在内存中,以便将来可以针对相同的行为直接返回缓存的值。比如你有一个函数调用了 100 次,每次都返回同一个值,那么引擎就会假定在 101 次时也返回该值。

假设我们有一个求和函数 sum,每次都接收两个数字:

6-sum.png

上面的函数返回值为 3!下次我们调用它时,引擎会假定我们还是传入两个数字类型的参数。

如果假设正确,就省去了动态查询阶段。引擎就可以直接使用存储在内存中的结果。否则,引擎会还原到原始字节码处解释执行,而不是使用优化过的机器码。

比如,下次我们要调用求和函数时,传入了一个字符串和一个数字,由于 JS 是动态类型的,所以不会报任何错误。

7-sum-2.png

这就意味着数字 2 会被转换成字符串,最终的结果将会变成 "12"。引擎会还原之前优化过的 只接收两个数字 的类型反馈,并重新返回到字节码处运行。


全文就到这里啦~本文是翻译的系列文章:

v8 部分的内容有参考极客时间的一个专栏 《浏览器工作原理与实践》:

专栏链接:浏览器工作原理与实践

如果你要买专栏的话,可以关注笔者的公众号,回复「极客时间」,我的返利全部返还哈~ 直接注册也能免费看五讲的~

参考链接


本文首发于公众号:码力全开(codingonfire)

本文随意转载哈,注明原文链接即可,公号文章转载联系我开白名单就好~

codingonfire.jpg

查看原文

前端中代码最骚的 关注了专栏 · 2020-01-08

码力全开

尽我所能为大家带来有用的东西~ 欢迎关注公众号:「码力全开」

关注 3775

前端中代码最骚的 收藏了文章 · 2019-11-01

vue项目前端知识点整理

vue项目前端知识点整理

微信授权后还能通过浏览器返回键回到授权页

在导航守卫中可以在next({})中设置replace: true来重定向到改路由,跟router.replace()相同

router.beforeEach((to, from, next) => {
  if (getToken()) {
    ...
  } else {
    // 储存进来的地址,供授权后跳回
    setUrl(to.fullPath)
    next({ path: '/author', replace: true })
  }
})

路由切换时页面不会自动回到顶部

const router = new VueRouter({
  routes: [...],
  scrollBehavior (to, from, savedPosition) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({ x: 0, y: 0 })
      }, 0)
    })
  }
})

ios系统在微信浏览器input失去焦点后页面不会自动回弹

初始的解决方案是input上绑定onblur事件,缺点是要绑定多次,且有的input存在于第三方组件中,无法绑定事件。
后来的解决方案是全局绑定focusin事件,因为focusin事件可以冒泡,被最外层的body捕获。

util.wxNoScroll = function() {
    let myFunction
    let isWXAndIos = isWeiXinAndIos()
    if (isWXAndIos) {
        document.body.addEventListener('focusin', () => {
            clearTimeout(myFunction)
        })
        document.body.addEventListener('focusout', () => {
            clearTimeout(myFunction)
            myFunction = setTimeout(function() {
                window.scrollTo({top: 0, left: 0, behavior: 'smooth'})
            }, 200)
        })
    }
   
    function isWeiXinAndIos () {
        let ua = '' + window.navigator.userAgent.toLowerCase()
        let isWeixin = /MicroMessenger/i.test(ua)
        let isIos = /\(i[^;]+;( U;)? CPU.+Mac OS X/i.test(ua)
        return isWeixin && isIos
    }
}

在子组件中修改父组件传递的值时会报错

vue中的props是单向绑定的,但如果props的类型为数组或者对象时,在子组件内部改变props的值控制台不会警告。因为数组或对象是地址引用,但官方不建议在子组件内改变父组件的值,这违反了vue中props单向绑定的思想。所以需要在改变props值的时候使用$emit,更简单的方法是使用.sync修饰符。

// 在子组件中
this.$emit('update:title', newTitle)

//在父组件中
<text-document :title.sync="doc.title"></text-document>

使用微信JS-SDK上传图片接口的处理

首先调用wx.chooseImage(),引导用户拍照或从手机相册中选图。成功会拿到图片的localId,再调用wx.uploadImage()将本地图片暂存到微信服务器上并返回图片的服务器端ID,再请求后端的上传接口最后拿到图片的服务器地址。

chooseImage(photoMustTake) {
    return new Promise(resolve => {
        var sourceType = (photoMustTake && photoMustTake == 1) ? ['camera'] : ['album', 'camera']
        wx.chooseImage({
            count: 1, // 默认9
            sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
            sourceType: sourceType, // 可以指定来源是相册还是相机,默认二者都有
            success: function (res) {
                // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
                wx.uploadImage({
                    localId: res.localIds[0],
                    isShowProgressTips: 1,
                    success: function (upRes) {
                        const formdata={mediaId:upRes.serverId}
                        uploadImageByWx(qs.stringify(formdata)).then(osRes => {
                            resolve(osRes.data)
                        })
                    },
                    fail: function (res) {
                    //   alert(JSON.stringify(res));
                    }
                });
            }
        });
    })
}

聊天室断线重连的处理

由于后端设置了自动断线时间,所以需要socket断线自动重连。
data如下几个属性,beginTime表示当前的真实时间,用于和服务器时间同步,openTime表示socket创建时间,主要用于分页,以及重连时的判断,reconnection表示是否断线重连。

data() {
    return {
        reconnection: false,
        beginTime: null,
        openTime: null
    }
}

初始化socket连接时,将openTime赋值为当前本地时间,socket连接成功后,将beginTime赋值为服务器返回的当前时间,再设置一个定时器,保持时间与服务器一致。

发送消息时,当有多个用户,每个用户的系统本地时间不同,会导致消息的顺序错乱。所以需要发送beginTime参数用于记录用户发送的时间,而每个用户的beginTime都是与服务器时间同步的,可以解决这个问题。
聊天室需要分页,而不同的时刻分页的数据不同,例如当前时刻有10条消息,而下个时刻又新增了2条数据,所以请求分页数据时,传递openTime参数,代表以创建socket的时间作为查询基准。

// 创建socket
createSocket() {
    _that.openTime = new Date().getTime() // 记录socket 创建时间
    _that.socket = new WebSocket(...)
}

// socket连接成功 返回状态
COMMAND_LOGIN_RESP(data) {
    if(10007 == data.code) { // 登陆成功
        this.page.beginTime = data.user.updateTime // 登录时间
        this.timeClock()
    }
}
// 更新登录时间的时钟
timeClock() {
    this.timer = setInterval(() => {
        this.page.beginTime = this.page.beginTime + 1000
    }, 1000)
}

当socket断开时,判断beginTime与当前时间是否超过60秒,如果没超过说明为非正常断开连接不做处理。

_that.socket.onerror = evt => {
    if (!_that.page.beginTime) {
        _that.$vux.toast.text('网络忙,请稍后重试')
        return false
    }
    // 不重连
    if (this.noConnection == true) {
        return false
    }
    // socket断线重连
    var date = new Date().getTime()
    // 判断断线时间是否超过60秒
    if (date - _that.openTime > 60000) {
        _that.reconnection = true
        _that.createSocket()
    }
}

发送音频时第一次授权问题

发送音频时,第一次点击会弹框提示授权,不管点击允许还是拒绝都会执行wx.startRecord(),这样再次调用录音就会出现问题(因为上一个录音没有结束), 由于录音方法是由touchstart事件触发的,可以使用touchcancel事件捕获弹出提示授权的状态。

_that.$refs.btnVoice.addEventListener("touchcancel" ,function(event) {
    event.preventDefault()
    // 手动触发 touchend
    _that.voice.isUpload = false
    _that.voice.voiceText = '按住 说话'
    _that.voice.touchStart = false
    _that.stopRecord()
})

组件销毁时,没有清空定时器

在组件实例被销毁后,setInterval()还会继续执行,需要手动清除,否则会占用内存。

mounted(){
    this.timer = (() => {
        ...
    }, 1000)
},
//最后在beforeDestroy()生命周期内清除定时器
 
beforeDestroy() {
    clearInterval(this.timer)       
    this.timer = null
}

watch监听对象的变化

watch: {
    chatList: {
        deep: true, // 监听对象的变化
        handler: function (newVal,oldVal){
            ...
        }
    }
}

后台管理系统模板问题

由于后台管理系统增加了菜单权限,路由是根据菜单权限动态生成的,当只有一个菜单的权限时,会导致这个菜单可能不显示,参看模板的源码:

  <router-link v-if="hasOneShowingChildren(item.children) && !item.children[0].children&&!item.alwaysShow" :to="resolvePath(item.children[0].path)">
    <el-menu-item :index="resolvePath(item.children[0].path)" :class="{'submenu-title-noDropdown':!isNest}">
      <svg-icon v-if="item.children[0].meta&&item.children[0].meta.icon" :icon-class="item.children[0].meta.icon"></svg-icon>
      <span v-if="item.children[0].meta&&item.children[0].meta.title" slot="title">{{generateTitle(item.children[0].meta.title)}}</span>
    </el-menu-item>
  </router-link>

  <el-submenu v-else :index="item.name||item.path">
    <template slot="title">
      <svg-icon v-if="item.meta&&item.meta.icon" :icon-class="item.meta.icon"></svg-icon>
      <span v-if="item.meta&&item.meta.title" slot="title">{{generateTitle(item.meta.title)}}</span>
    </template>

    <template v-for="child in item.children" v-if="!child.hidden">
      <sidebar-item :is-nest="true" class="nest-menu" v-if="child.children&&child.children.length>0" :item="child" :key="child.path" :base-path="resolvePath(child.path)"></sidebar-item>

      <router-link v-else :to="resolvePath(child.path)" :key="child.name">
        <el-menu-item :index="resolvePath(child.path)">
          <svg-icon v-if="child.meta&&child.meta.icon" :icon-class="child.meta.icon"></svg-icon>
          <span v-if="child.meta&&child.meta.title" slot="title">{{generateTitle(child.meta.title)}}</span>
        </el-menu-item>
      </router-link>
    </template>
  </el-submenu>

其中v-if="hasOneShowingChildren(item.children) && !item.children[0].children&&!item.alwaysShow"表示当这个节点只有一个子元素,且这个节点的第一个子元素没有子元素时,显示一个特殊的菜单样式。而问题是item.children[0]可能是一个隐藏的菜单(item.hidden === true),所以当这个表达式成立时,可能会渲染一个隐藏的菜单。参看最新的后台源码,作者已经修复了这个问题。

<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
  <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
    <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
      <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
    </el-menu-item>
  </app-link>
</template>
  methods: {
    hasOneShowingChild(children = [], parent) {
      const showingChildren = children.filter(item => {
        if (item.hidden) {
          return false
        } else {
          // Temp set(will be used if only has one showing child)
          this.onlyOneChild = item
          return true
        }
      })
      // When there is only one child router, the child router is displayed by default
      if (showingChildren.length === 1) {
        return true
      }
      // Show parent if there are no child router to display
      if (showingChildren.length === 0) {
        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
        return true
      }
      return false
    }
  }

动态组件的创建

有时候我们有很多类似的组件,只有一点点地方不一样,我们可以把这样的类似组件写到配置文件中,动态创建和引用组件

var vm = new Vue({
  el: '#example',
  data: {
    currentView: 'home'
  },
  components: {
    home: { /* ... */ },
    posts: { /* ... */ },
    archive: { /* ... */ }
  }
})
<component v-bind:is="currentView">
  <!-- 组件在 vm.currentview 变化时改变! -->
</component>

动态菜单权限

由于菜单是根据权限动态生成的,所以默认的路由只需要几个不需要权限判断的页面,其他的页面的路由放在一个map对象asyncRouterMap中,
设置role为权限对应的编码

export const asyncRouterMap = [
    {
        path: '/project',
        component: Layout,
        redirect: 'noredirect',
        name: 'Project',
        meta: { title: '项目管理', icon: 'project' },
        children: [
            {
                path: 'index',
                name: 'Index',
                component: () => import('@/views/project/index'),
                meta: { title: '项目管理', role: 'PRO-01' }
            },

导航守卫的判断,如果有token以及store.getters.allowGetRole说明用户已经登录,routers为用户根据权限生成的路由树,如果不存在,则调用store.dispatch('GetMenu')请求用户菜单权限,再调用store.dispatch('GenerateRoutes')将获取的菜单权限解析成路由的结构。

router.beforeEach((to, from, next) => {
    if (whiteList.indexOf(to.path) !== -1) {
        next()
    } else {
        NProgress.start()
        // 判断是否有token 和 是否允许用户进入菜单列表
        if (getToken() && store.getters.allowGetRole) {
            if (to.path === '/login') {
                next({ path: '/' })
                NProgress.done()
            } else {
                if (!store.getters.routers.length) {
                    // 拉取用户菜单权限
                    store.dispatch('GetMenu').then(() => {
                        // 生成可访问的路由表
                        store.dispatch('GenerateRoutes').then(() => {
                            router.addRoutes(store.getters.addRouters)
                            next({ ...to, replace: true })
                        })
                    })
                } else {
                    next()
                }
            }
        } else {
            next('/login')
            NProgress.done()
        }
    }
})

store中的actions

// 获取动态菜单菜单权限
GetMenu({ commit, state }) {
    return new Promise((resolve, reject) => {
        getMenu().then(res => {
            commit('SET_MENU', res.data)
            resolve(res)
        }).catch(error => {
            reject(error)
        })
    })
},
// 根据权限生成对应的菜单
GenerateRoutes({ commit, state }) {
    return new Promise(resolve => {
        // 循环异步挂载的路由
        var accessedRouters = []
        asyncRouterMap.forEach((item, index) => {
            if (item.children && item.children.length) {
                item.children = item.children.filter(child => {
                    if (child.hidden) {
                        return true
                    } else if (hasPermission(state.role.menu, child)) {
                        return true
                    } else {
                        return false
                    }
                })
            }
            accessedRouters[index] = item
        })
        // 将处理后的路由保存到vuex中
        commit('SET_ROUTERS', accessedRouters)
        resolve()
    })
},

项目的部署和版本切换

目前项目有两个环境,分别为测试环境和生产环境,请求的接口地址配在\src\utils\global.js中,当部署生产环境时只需要将develop分支的代码合并到master分支,global.js不需要再额外更改地址

查看原文

前端中代码最骚的 收藏了文章 · 2019-07-17

Vue项目实现简单的权限控制

在Vue项目中实现权限控制管理

对于一般稍大一些的后台管理系统,往往有很多个人员需要使用,而不同的人员也对应了不同的权限系统,后端的权限校验保障了系统的安全性,而前端的权限校验则提供了优秀的交互体验。

校验方式

前端对用户的权限信息进行校验往往在两个方面进行限制

  • 路由不可见
  • 元素不可见

通过以上两个方式,来将用户权限之外的内容隐藏掉。

路由不可见实现方法

在router.js中的meta字段中加入该路由的访问权限列表auths字段。

{
    path: 'edit',
    name: 'edit',
    meta: {
        title: '编辑账户',
        auths:['edit_account']
    },
    component: () => import('pathToComponent/component.vue'),
},

Vue.router中提供了导航守卫,我们这里使用全局前置守卫对路由跳转进行权限校验
router.beforeEach(to,from,next)
参数to是即将进入的路由对象,我们可以在对象中拿到之前在router.js中定义的route对象,并获得auths字段

router.beforeEach((to,from,next)=>{
    const hasAuth = function(needAuths,haveAuths){     //判断用户是否拥有权限的function
        // implement 
    }
    const havaAuths = []; // 用户拥有的权限列表
    if(!hasAuth(to.meta.auths,haveAuths)){
        //没有权限重定位到其他页面,往往是401页面
        next({replace:true,name:'otherRouteName'})
    }
    //权限校验通过,跳转至对应路由
    next();
})

在有侧边栏的后台管理中,还需要对侧边栏的路由导航进行隐藏,这里同样是通过拿到route.meta.auths字段进行过滤。

元素不可见实现方法

因为某些页面中会有一些特殊的接口调用或数据展示受到权限控制显示。前端通过控制元素的展示来隐藏掉用户不具有权限的元素,避免点击了某一个button导致接口401报错这样不友好的交互体验。
全局注册一个directive

//acl.js
const aclDirective = {
    inserted:function(el,binding){ // 在被绑定的元素插入到dom中时
        const hasAuth = function(needAuths,haveAuths){ //判断用户是否拥有权限的function
            // implement 
        }
        const havaAuths = []; // 用户拥有的权限列表
        if(!hasAuth(binding.value,haveAuths)){ //binding.value 可以获得绑定指令时传入的参数
            el.style = "display:none"; //修改元素的可见状态
        }
    }
}
//main.js
Vue.directive('acl',aclDirective); //全局注册指令

在需要控制显示的组件上我们就可以通过v-acl进行权限控制

<button v-acl="['edit_access']">编辑账户</button>
查看原文

前端中代码最骚的 赞了文章 · 2019-07-17

Vue项目实现简单的权限控制

在Vue项目中实现权限控制管理

对于一般稍大一些的后台管理系统,往往有很多个人员需要使用,而不同的人员也对应了不同的权限系统,后端的权限校验保障了系统的安全性,而前端的权限校验则提供了优秀的交互体验。

校验方式

前端对用户的权限信息进行校验往往在两个方面进行限制

  • 路由不可见
  • 元素不可见

通过以上两个方式,来将用户权限之外的内容隐藏掉。

路由不可见实现方法

在router.js中的meta字段中加入该路由的访问权限列表auths字段。

{
    path: 'edit',
    name: 'edit',
    meta: {
        title: '编辑账户',
        auths:['edit_account']
    },
    component: () => import('pathToComponent/component.vue'),
},

Vue.router中提供了导航守卫,我们这里使用全局前置守卫对路由跳转进行权限校验
router.beforeEach(to,from,next)
参数to是即将进入的路由对象,我们可以在对象中拿到之前在router.js中定义的route对象,并获得auths字段

router.beforeEach((to,from,next)=>{
    const hasAuth = function(needAuths,haveAuths){     //判断用户是否拥有权限的function
        // implement 
    }
    const havaAuths = []; // 用户拥有的权限列表
    if(!hasAuth(to.meta.auths,haveAuths)){
        //没有权限重定位到其他页面,往往是401页面
        next({replace:true,name:'otherRouteName'})
    }
    //权限校验通过,跳转至对应路由
    next();
})

在有侧边栏的后台管理中,还需要对侧边栏的路由导航进行隐藏,这里同样是通过拿到route.meta.auths字段进行过滤。

元素不可见实现方法

因为某些页面中会有一些特殊的接口调用或数据展示受到权限控制显示。前端通过控制元素的展示来隐藏掉用户不具有权限的元素,避免点击了某一个button导致接口401报错这样不友好的交互体验。
全局注册一个directive

//acl.js
const aclDirective = {
    inserted:function(el,binding){ // 在被绑定的元素插入到dom中时
        const hasAuth = function(needAuths,haveAuths){ //判断用户是否拥有权限的function
            // implement 
        }
        const havaAuths = []; // 用户拥有的权限列表
        if(!hasAuth(binding.value,haveAuths)){ //binding.value 可以获得绑定指令时传入的参数
            el.style = "display:none"; //修改元素的可见状态
        }
    }
}
//main.js
Vue.directive('acl',aclDirective); //全局注册指令

在需要控制显示的组件上我们就可以通过v-acl进行权限控制

<button v-acl="['edit_access']">编辑账户</button>
查看原文

赞 33 收藏 28 评论 10

前端中代码最骚的 赞了文章 · 2019-07-15

Vue组件通信

父子通信

props和emit

父组件通过props传递数据给子组件,子组件通过emit发送事件传递给父组件。

// 父组件
<div>
    <child :data="child" @send="getFromChild"></child>
</div>

data(){
    return{
        toChild: '大儿子',
        fromChild: ''
    }
},
methods: {
    getFromChild(val){
        this.fromChild=val
    }
}
// 子组件
<div @click="toParent">{{data}}</div>

props:[data],
methods: {
    toParent(){
        this.$emit('send', '给父亲')
    }
}

v-model

v-model其实是props,emit的语法糖,v-model默认会解析成名为value的prop和名为input的事件。

// 父组件
<children v-model="msg"></children>
<p>{{msg}}</p>

data(){
    return{
        msg:'model'
    }
}
// 子组件
<input :value="value" @input="toInput" />

props: ['value'],
methods: {
    toInput(e){
        this.$emit('input', e.target.value)
    }
}

如果想改变默认解析值,请使用model

Vue.component('base-checkbox', { 
    model: { prop: 'checked', event: 'change' }, 
    props: { checked: Boolean }, 
    template: \` <input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)" > \` 
})

$children和$parent

在父组件使用$children访问子组件,在子组件中使用$parent访问父组件

// 父组件
<child />

data(){
    return {
        msg: '父组件数据'
    }
},
methods: {
    test(){
        console.log('我是父组件的方法,被执行')
    }
},
mounted(){
    console.log(this.$children[0].child_msg); // 执行子组件方法
}
// 子组件
<div>{{$parent.msg}}</div>

data(){
    return{
        child_msg: '子组件数据'
    }
},
mounted(){
    // 子组件执行父组件方法
    this.$parent.test(); 
}

.sync方式

在vue1.x中是对prop进行双向绑定,在vue2只允许单向数据流,也是一个语法糖

// 父组件
<child :count.sync="num" />

data(){
    return {
        num: 0
    }
}
// 子组件
<div @click="handleAdd">add</div>

data(){
    return {
        counter: this.count
    }
},
props: ["count"],
methods: {
    handleAdd(){
        this.$emit('update:count', ++this.counter)
    }
}

插槽

父组件:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

 <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>
  
  <template #default="slotProps"> 
    {{ slotProps.user.name }}
  </template>
</base-layout>

子组件:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <slot :user="user">
    {{ user.name }}
  </slot>
</div>
v-slot只能在组件或template标签上使用
v-slot缩写为#

跨多层次组件通信

依赖注入

可以使用provide/inject,虽然文档中不推荐直接使用在业务中。
假设有父组件A,然后有一个跨多层次的子组件B

// 父组件A
export default{
    provide: {
        data: 1
    }
}
// 子组件B
export default{
    inject: ['data'],
    mounted(){
        // 无论跨几层都能获取父组件的data属性
        console.log(this.data); // 1
    }
}

$listeners和$attrs

$attrs--包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。
inheritAttrs--默认值true,继承所有父组件属性(除props),为true会将attrs中的属性当做html的data属性渲染到dom根节点上
$listeners--包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器,v-on="$listeners"将所有事件监听器指向这个组件的某个特定子元素

// 父组件
<children :child1="child1" :child2="child2" @test1="onTest1"
@test2="onTest2"></children>

data(){
    return {
        child1: 'childOne',
        child2: 'childTwo'
    }
},
methods: {
    onTest1(){
        console.log('test1 running')
    },
    onTest2(){
        console.log('test2 running')
    }
}

// 子组件
<p>{{child1}}</p>
<child v-bind="$attrs" v-on="$listeners"></child>

props: ['child1'],
mounted(){
    this.$emit('test1')
}

// 孙组件
<p>{{child2}</p>
<p>{{$attrs}}</p>

props: ['child2'],
mounted(){
    this.$emit('test2')
}

任意组件

Event Bus

1.新建一个bus.js文件

import Vue from 'vue';
export default new Vue();

2.使用它

<div @click="addCart">添加</div>
import Bus from 'bus.js';
export default{
    methods: {
        addCart(event){
            Bus.$emit('getTarget', event.target)
        }
    }
}
// 另一组件
export default{
    created(){
        Bus.$on('getTarget', target =>{
            console.log(target)
        })
    }
}

Vue.observable

可以作为最小化跨组件状态化管理。
使用Vue.observable()进行状态管理

Vuex方式

参考链接:
https://juejin.im/post/5c776e...
https://segmentfault.com/a/11...

查看原文

赞 13 收藏 8 评论 1

前端中代码最骚的 收藏了文章 · 2019-05-19

通过HTTP Header控制缓存

我们经常通过缓存技术来加快网站的访问速度,从而提升用户体验。HTTP协议中也规定了一些和缓存相关的Header,来允许浏览器或共享高速缓存缓存资源。这些Header包括:

  • Last-Modified 和 If-Modified-Since
  • ETag 和 If-None-Match
  • Expires
  • Cache-Control

以上Header又可以分成两种类型:

  • 协商缓存:浏览器发送验证到服务器,由服务器决定是否从缓存中读取,如 1 和 2 。
  • 强缓存:浏览器验证缓存的有效性,然后决定是否从缓存中读取数据,如 3 和 4 。

本文将会分别介绍这四种配置的作用以及可能产生的影响。

1、Last-Modified 和 If-Modified-Since

Last-Modified:服务器在响应请求时,告知浏览器资源的最后修改时间。

If-Modified-Since:浏览器再次发送请求时,会通过此Header通知服务器在上次请求时所得到的资源最后修改时间。服务器会将If-Modified-Since与被请求资源的最后修改时间进行比对。若资源的最后修改时间晚于If-Modified-Since,表示资源已被改动,则响最新的资源,返回200状态码;若资源的最后修改时间早于或等于If-Modified-Since,表示浏览器端的资源已经是最新版本,响应304状态码,通知浏览器继续使用缓存中的资源。

2、ETag 和 If-None-Match

ETag:服务器分配给资源的唯一标识符,资源被修改后,ETag也会随之发生变化。

If-None-Match:浏览器再次发送请求时,会通过此Header通知服务器已缓存资源的ETag。服务器会将If-None-Match与被请求资源的最新ETag进行比对。若不相同,表示资源已被改动,则响应最新的资源,返回200状态码;若值相同,则直接响应304状态码,通知浏览器继续使用缓存中的资源。

3、Expires

服务器可以通过此Header向浏览器传递一个具体的时间(格林威治格式,例如:Thu, 19 Jul 2018 07:43:05 GMT) ,来明确地宣告资源的有效期。在资源过期之前,浏览器不再发送请求,而是直接从缓存中读取数据。只有当资源过期之后,浏览器才会再次向服务器请求该资源。

4、Cache-Control

服务器使用此Header来向客户端建议缓存策略,它有一下几个可选值:

max-age=秒:告知浏览器缓存的有效时长,在该时间内浏览器将直接从缓存中读取数据。

s-maxage=秒:作用同max-age,但是只对共享高速缓存(如CDN)有效,对浏览器无效。

no-cache:告知浏览器不要直接使用缓存,而是必须向服务器发送请求。

no-store:告知浏览器不要缓存本次请求和响应的任何信息。

public:宣告任何缓存媒介都可以缓存该响应。

private:宣告该响应只允许个体客户端(如浏览器)去缓存,而不允许共享高速缓存(如CDN)去缓存。

在上面的介绍中我们了解到浏览器会根据max-age设置的时间进行缓存。而通过研究发现CDN也会识别源站响应头中Cache-Control属性,根据max-age设置的时间进行缓存,但是,如果源站同时设置了s-maxage和max-age,那么CDN会优先采用s-maxage。

下面通过图例来展示一下这些可选值的效果。

首先了解一下浏览器是怎样根据max-age进行缓存的:

clipboard.png

从上图不难发现,服务器在Header中返回了Cache-Control: max-age=100后,浏览器成功缓存100秒,该时间段内的请求都从直接以本地缓存来响应。

那么,服务器在Header中返回Cache-Control:s-maxage=100时,又会对浏览器产生什么样的影响呢?

clipboard.png

如上图所示,浏览器没有采取任何缓存策略,这是因为s-maxage面向的是共享高速缓。

上面这两个例子很容易理解,在现实世界中,为了加快网站响应速度,我们可能会在浏览器和服务器之间引入CDN服务。浏览器的请求会先到达CDN,然后CDN判断是从缓存中读取数据还是回源到服务器。接下来,让我们看看max-age和s-maxage会对CDN的缓存策略带来哪些影响。

clipboard.png

可以看出CDN也会利用max-age来缓存,所以在100秒内强制刷新浏览器时,CDN会直接用缓存来响应。

如果服务器使用了s-maxage又会如何呢?

clipboard.png

不难发现CDN对max-age和s-maxage采取了同样的缓存策略,但浏览器并不会根据s-maxage来进行缓存。

CDN供应商的特殊规则

我们分别测试了阿里云和腾讯云的CDN对Cache-Control的支持情况,发现他们都有一些独特的规则。

阿里云CDN可以在控制台里设置Cache-Control,该设置会覆盖源服务器的Cache-Control。

腾讯云CDN虽然没有再控制台提供覆盖Cache-Control的功能,但其规则却一点也不简单,在使用的时候一定要特别注意:

  • 服务器和CDN均不对缓存进行配置时,CDN会采用默认的缓存机制(静态文件缓存30天,动态请求不缓存);
  • CDN配置缓存机制(但并未开启高级缓存配置)且服务器设置Cache-Control: s-maxage=200,max-age=100时,CDN会按照其控制台设置的规则进行缓存,浏览器则按照max-age进行缓存;
  • 服务器不设置Cache-Control时,CDN会自动在响应的Header中添加Cache-Control: max-age=600,这就会让浏览器将该资源缓存600秒;
  • 服务器设置为禁用缓存时,CDN和浏览器均不进行缓存;
  • 服务器设置Cache-Control: s-maxage=200,max-age=100并开启CDN的高级缓存配置时,CDN会从s-maxage和控制台中设置的缓存时间中选择最小值来作为缓存时间,而浏览器则始终使用max-age;
  • 服务器设置Cache-Control:max-age=100并开启CDN的高级缓存配置时,CDN会从max-age和控制台中设置的缓存时间中选择最小值来作为缓存时间,不影响浏览器的缓存策略。

组合使用

如果同时设置了这些Header,浏览器和高速共享缓存会按照下面的优先级进行缓存:

Cache-Control > Expires > ETag > Last-Modified

也就是说,Cache-Control不仅是强缓存,而且拥有最高的优先级,我们可以为不经常发生变化的资源应用该Header来提升响应时间。

在Ada中使用缓存

Ada提供了UI脚手架和API脚手架,这两类脚手架的服务器端入口文件分别为index.server.js和index.js,我们只需要在入口文件的请求处理函数中为响应添加适当的Header,即可通知客户端进行响应的缓存,比如:

// 设置CDN缓存300秒,浏览器缓存200秒
ctx.response.headers.set('Cache-Control', public,s-maxage=300,max-age=200)
在为请求添加缓存Header之前,应该先为其制定适当的缓存策略,需要考虑该URL是否适合缓存(数据是否特定于用户)以及需要缓存的时长等等。

总结
通过使用这些HTTP Header,我们可以主动影响浏览器甚至CDN的缓存策略,从而减少请求数量,提升网页性能,减轻服务器压力。

Ada的灵活机制能让我们为不同的URL设置不同的缓存策略,能够更有针对性地进行主动缓存。

查看原文

前端中代码最骚的 收藏了文章 · 2019-05-19

Nginx 内容缓存及常见参数配置

原文链接:何晓东 博客

使用场景:项目的页面需要加载很多数据,也不是经常变化的,不涉及个性化定制,为每次请求去动态生成数据,性能比不上根据请求路由和参数缓存一下结果,使用 Nginx 缓存将大幅度提升请求速度。
基础

只需要配置 proxy_cache_pathproxy_cache 就可以开启内容缓存,前者用来设置缓存的路径和配置,后者用来启用缓存。

http {
    ...
    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        proxy_cache mycache;
        location / {
            proxy_pass http://localhost:8000;
        }
    }
}

对应参数说明:

1.用于缓存的本地磁盘目录是 /path/to/cache/

2.levels 在 /path/to/cache/ 设置了一个两级层次结构的目录。将大量的文件放置在单个目录中会导致文件访问缓慢,所以针对大多数部署,我们推荐使用两级目录层次结构。如果 levels 参数没有配置,则 NGINX 会将所有的文件放到同一个目录中。

3.keys_zone 设置一个共享内存区,该内存区用于存储缓存键和元数据,有些类似计时器的用途。将键的拷贝放入内存可以使 NGINX 在不检索磁盘的情况下快速决定一个请求是 HIT 还是 MISS,这样大大提高了检索速度。一个 1MB 的内存空间可以存储大约 8000 个 key,那么上面配置的 10MB 内存空间可以存储差不多 80000 个key。

4.max_size 设置了缓存的上限(在上面的例子中是 10G)。这是一个可选项;如果不指定具体值,那就是允许缓存不断增长,占用所有可用的磁盘空间。当缓存达到这个上线,处理器便调用 cache manager 来移除最近最少被使用的文件,这样把缓存的空间降低至这个限制之下。

5.inactive 指定了项目在不被访问的情况下能够在内存中保持的时间。在上面的例子中,如果一个文件在 60 分钟之内没有被请求,则缓存管理将会自动将其在内存中删除,不管该文件是否过期。该参数默认值为 10 分钟(10m)。注意,非活动内容有别于过期内容。NGINX 不会自动删除由缓存控制头部指定的过期内容(本例中Cache-Control:max-age=120)。过期内容只有在 inactive 指定时间内没有被访问的情况下才会被删除。如果过期内容被访问了,那么 NGINX 就会将其从原服务器上刷新,并更新对应的 inactive 计时器。

6.NGINX 最初会将注定写入缓存的文件先放入一个临时存储区域, use_temp_path=off 命令指示 NGINX 将在缓存这些文件时将它们写入同一个目录下。我们强烈建议你将参数设置为 off 来避免在文件系统中不必要的数据拷贝。use_temp_path 在 NGINX1.7 版本和 NGINX Plus R6 中有所介绍。

最终,proxy_cache 命令启动缓存那些 URL 与 location 部分匹配的内容(本例中,为/)。你同样可以将 proxy_cache 命令添加到 server 部分,这将会将缓存应用到所有的那些 location 中未指定自己的 proxy_cache 命令的服务中。

Nginx 缓存相关进程

缓存中还涉及两个额外的NGINX进程:

  • cache manager 周期性地启动,检查高速缓存的状态。如果高速缓存大小超过 proxy_cache_path 中 max_size 参数设置的限制,则高速缓存管理器将删除最近访问过的数据。在两次缓存管理器启动的间隔,缓存的数据量可能短暂超过配置的大小。
  • cache loader 只运行一次,NGINX 开始之后。它将有关以前缓存的数据的元数据加载到共享内存区域。一次加载整个缓存可能会消耗足够的资源来在启动后的最初几分钟内降低 NGINX 的性能。要避免这种情况,请通过在 proxy_cache_path 指令中包含以下参数来配置缓存的迭代加载:

    • loader_threshold - 迭代持续时间,以毫秒为单位(默认情况下 200)
    • loader_files - 一次迭代期间加载的最大项目数(默认情况下 100)
    • loader_sleeps - 迭代之间的延迟,以毫秒为单位(默认情况下 50)

在以下示例中,迭代持续数300 毫秒或直到 200 个项目被加载进去:

proxy_cache_path /data/nginx/cache keys_zone=one:10m loader_threshold=300 loader_files=200;
其他常用参数

配置示例:

proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

server {
    ...
    location / {
        proxy_cache my_cache;
        # proxy_cache_key "$host$request_uri$cookie_user";
        proxy_cache_min_uses 3;
        proxy_cache_methods GET HEAD POST;
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404      1m;
        # proxy_cache_valid any 5m;
        proxy_pass http://localhost:8000;
    }
}

对应参数说明:

proxy_cache_key 为更改计算密钥时使用的请求特征,指定缓存的 key,这个不推荐,示例是使用域名,请求url,用户 cookie 来当作 key,意味着一个页面将为不同的用户缓存 n 次,绝大多数情况不需要这样的操作。

proxy_cache_min_uses 为在缓存响应之前必须使用相同密钥的请求的最小次数。

proxy_cache_methods 为指定要被缓存的请求方式的响应值,默认为 GET 和 HEAD,新增其他的需要一起列出来,如上示例所示。

proxy_cache_valid 为响应状态码的缓存时间,示例可以为每个状态码缓存指定时间,也可以使用 any 进行全部状态码的缓存。

清除缓存

需要提前加一个配置,用于标识使用 HTTP PURGE 方法的请求并删除匹配的 URL 对应的缓存。<br/>
1.在 http {} 上下文中创建新变量,例如 $purge_method, 他依赖于 $request_method 变量:

http {
    ...
    map $request_method $purge_method {
        PURGE 1;
        default 0;
    }
}

2.在 location {} 块中,已经配置缓存的前提下,引入 proxy_cache_purge 参数来指定清除缓存请求的条件。例如在上一步指定的 $request_method

server {
    listen      80;
    server_name www.example.com;

    location / {
        proxy_pass  https://localhost:8002;
        proxy_cache mycache;

        proxy_cache_purge $purge_method;
    }
}

配置完并使之生效之后,就可以发送一条 purge 请求来让缓存失效了,例如:

curl -X PURGE -D – "https://www.example.com/*"

在该示例中,将清除具有公共 URL 部分(由星号通配符指定)的资源。但这些缓存条目不会从缓存中完全删除:它们会保留在磁盘上,直到它们被视为不活动(由proxy_cache_path 中的 inactive参数决定)的时候才完全删除,或缓存清除器(由 proxy_cache_path 中的 purge 决定),或客户端尝试访问它们的时候。

参考链接:

  1. Nginx 缓存使用官方指南
  2. Nginx 内容缓存文档

更多文章会更新在个人博客上。

最后恰饭 阿里云全系列产品/短信包特惠购买 中小企业上云最佳选择 阿里云内部优惠券

查看原文

前端中代码最骚的 收藏了文章 · 2019-05-16

VsCode从零开始配置一个属于自己的Vue开发环境

原文地址:https://liubing.me/vscode-vue...

VsCode算是比较热门的一个代码编辑器了,全名Visual Studio Code,微软出品,下载地址:点我去下载
插件众多,功能齐全,平常开发过程中都是用的它,整理了下日常使用的插件及配置供大家参考,废话就不多说了,直接进入正题。

相关插件

Vetur

插件文档地址:https://marketplace.visualstudio.com/items?itemName=octref.vetur
Vetur不用说了吧,开发Vue必装的一个插件
未安装之前vue文件显示这样的
image.png
安装完成后显示这样的,看着舒服多了
image.png

Vue 2 Snippets

插件文档地址:https://marketplace.visualstudio.com/items?itemName=hollowtree.vue-snippets
主要加强vue的便捷写法
demo1.gif

demo2.gif

language-stylus

插件文档地址:https://marketplace.visualstudio.com/items?itemName=sysoev.language-stylus
写stylus用的,如果项目用的stylus写样式推荐安装,其他Sass、LESS等同理安装相应的插件即可。

Auto Close Tag

插件文档地址:https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-close-tag
自动闭合标签所用
demo3.gif
配合快捷键Alt+. (Command+Alt+. for Mac)特别好使。
demo4.gif

Auto Rename Tag

插件文档地址:https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag
自动修改重命名配对的标签

Bookmarks

插件文档地址:https://marketplace.visualstudio.com/items?itemName=alefragnani.Bookmarks
可以对成片的代码做一些书签标记,方便后续查看。
demo5.gif

Bracket Pair Colorizer

插件文档地址:https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer
对括号进行着色,方便区分,下面的图分别是安装前后的比较
image.png

image.png

Copy Relative Path

插件文档地址:https://marketplace.visualstudio.com/items?itemName=alexdima.copy-relative-path
用于复制文件的完整路径和相对路径,有时候我们可能需要复制一些文件的路径,该插件就很方便了。
demo6.gif

Path Intellisense

插件文档地址:https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense
路径自动感知,在配置文件中配置@后我们就可以很方便快捷的引用各种文件了

"path-intellisense.mappings": {
    "@": "${workspaceRoot}/src"
}

demo7.gif

Document This

插件文档地址:https://marketplace.visualstudio.com/items?itemName=joelday.docthis
主要用于方法的注释,选中方法名,按住Ctrl+Alt后按两次D,即可快速生成标准的注释
demo8.gif

psioniq File Header

插件文档地址:https://marketplace.visualstudio.com/items?itemName=psioniq.psi-header
按住Ctrl+Alt后按两次H既可快速在文件的头部生成注释信息,如果对默认的注释模板不满意的话,可以在配置文件中自定义注释模板

"psi-header.templates": [
  {
    "language": "*",
    "template": [
      "FileName: <<filename>>",
      "Remark: <<filename>>",
      "Project: <<projectname>>",
      "Author: <<author>>",
      "File Created: <<filecreated('dddd, Do MMMM YYYY h:mm:ss a')>>",
      "Last Modified: <<dateformat('dddd, Do MMMM YYYY h:mm:ss a')>>",
      "Modified By: <<author>>"
    ]
  }
]

demo9.gif

demo10.gif

Vue Peek

插件文档地址:https://marketplace.visualstudio.com/items?itemName=dariofuzinato.vue-peek
用于Vue快速查看组件定义以及组件跳转,具体使用见插件文档地址中的使用方法。
demo11.gif

JavaScript (ES6) code snippets

插件文档地址:https://marketplace.visualstudio.com/items?itemName=xabikos.JavaScriptSnippets
用于快速生成ES6代码片段
demo12.gif

Material Icon Theme

插件文档地址:https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme
Material风格的icon文件图标,可以看下安装前后的区别。
image.pngimage.png

ESLint

插件文档地址:https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint
这个不用多说了,规范代码格式的。

StandardJS - JavaScript Standard Style

插件文档地址:https://marketplace.visualstudio.com/items?itemName=chenxsan.vscode-standardjs
作为一个合格的前端开发,得遵循一定得代码规范,这里推荐StandardJS,配合该插件可以自动将你的代码格式化成规范的代码。

vue-cli-3脚手架创建项目的时候,可以选择这个规范。

demo13.gif

Beautify

插件文档地址:https://marketplace.visualstudio.com/items?itemName=HookyQR.beautify
主要拿它来格式话html的,也可以格式话vue`template中的html<br />配合vetur插件,需要做些设置,格式化html支持以下四种模式auto|force|force-aligned|force-expand-multiline`

"vetur.format.defaultFormatterOptions": {
  //beautify设置
  "js-beautify-html": {
    "wrap_attributes_indent_size": 2,
      "wrap_attributes": "force-expand-multiline" // auto|force|force-aligned|force-expand-multiline
  }
}

auto模式

image.png

force模式

demo15.gif

force-aligned模式

demo16.gif

force-expand-multiline模式

demo17.gif

vscode-element-helper

插件文档地址:https://marketplace.visualstudio.com/items?itemName=ElemeFE.vscode-element-helper
用element-ui的,应该都知道这个插件,功能看图就知道了。

Version Lens

插件文档地址:https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens
显示npm,jspm,bower,dub和dotnet核心的软件包版本信息

image.png

One Dark Pro

插件文档地址:https://marketplace.visualstudio.com/items?itemName=zhuangtongfa.Material-theme
一款热门的主题,安装前后比较。
image.png

image.png

相关配置

{
    // 编辑器默认设置
    "editor.tabSize": 2, // 设置2个空格统一tabSize
    "javascript.validate.enable": false, // 关闭默认的校验
    "workbench.editor.enablePreview": false, // 关闭文件预览
    // 引用路径设置
    "path-intellisense.mappings": {
        "@": "${workspaceRoot}/src",
        "~": "${workspaceRoot}/src",
        "src": "${workspaceRoot}/src"
    },
    // standard自动保存
    "standard.autoFixOnSave": true,
    // psioniq File Header设置
    "psi-header.templates": [{
        "language": "*",
        "template": [
            "FileName: <<filename>>",
            "Remark: <<filename>>",
            "Project: <<projectname>>",
            "Author: <<author>>",
            "File Created: <<filecreated('dddd, Do MMMM YYYY h:mm:ss a')>>",
            "Last Modified: <<dateformat('dddd, Do MMMM YYYY h:mm:ss a')>>",
            "Modified By: <<author>>"
        ]
    }],
    // vetur设置
    "vetur.format.defaultFormatter.html": "js-beautify-html",
    "vetur.format.defaultFormatter.js": "none",
    "vetur.format.defaultFormatterOptions": {
        //beautify设置
        "js-beautify-html": {
            "wrap_attributes_indent_size": 2,
            "wrap_attributes": "force-expand-multiline" // auto|force|force-aligned|force-expand-multiline
        }
    },
    // eslint设置
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {
            "language": "vue",
            "autoFix": true
        }
    ],
    // 保存后自动格式化
    "eslint.autoFixOnSave": true,
    "editor.formatOnSave": true,

    "workbench.iconTheme": "material-icon-theme", // icon图标
    "workbench.colorTheme": "One Dark Pro" // 编辑器主题
}

备份及同步

忙活半天把上面的插件都装上及配置好,下次换新电脑的时候总不能重新再来一遍吧,所以压轴插件登场
Settings Sync
插件文档地址:https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync
安装完成后需要做些其他事情:

创建GitHub Gist ID

点我去创建
image.png
创建完成后有个提示,一定要将红色框所示的token记录下来,不然下次就看不到了。
image.png

上传设置

Shift + Alt + U,会出现一个出入token的框,将上面我们创建的token输入后回车。
image.png
完成后会给你生成一个GIST ID,将这个GIST ID记录下来,下次需要在其他电脑上恢复数据的时候需要用到。
image.png

完整步骤Gif

下载设置

Shift + Alt + D,它将询问您的GitHub Gist ID(我们在步骤创建GitHub Gist ID中生成的一个ID)
在窗口中输入该GitHub Gist ID,然后回车。

提示需要输入您的Gist ID(上面上传设置后生成的一个ID)

下载可能会需要点时间,完整过程Gif

插件配置

最后可以按照自己的需求配置自动上传与自动下载

"sync.autoDownload": false,// 是否自动下载
"sync.autoUpload": false// 是否自动上传

结语

至此教程就结束了,后面有其他问题或者有用的插件会补充进去。

查看原文

认证与成就

  • 获得 23 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-08-31
个人主页被 1.2k 人浏览