siwuxie

siwuxie 查看完整档案

其它编辑新疆艺术学院  |  404 编辑404  |  404 编辑 404 编辑
编辑

404

个人动态

siwuxie 提出了问题 · 2020-11-19

定义 .d.ts 时不能与 .ts 重名

|__any
    |__a.ts
    |__a.d.ts

项目目录结构如上,定义 .d.ts 时不能与 .ts 重名,否则,eslint 会显示如下错误信息,编译时,找不到 .d.ts 中的定义

Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.
The file does not match your project config: src\any\a.d.ts.
The file must be included in at least one of the projects provided.

这种场景没在官网看到要如何配置,关于 .d.ts 文件的位置和命名有什么限制吗?

关注 1 回答 0

siwuxie 赞了文章 · 2020-11-05

重新认识caniuse

困惑

相信大家都曾用caniuse网站查询过css、js的一些兼容性问题,并且都从它反馈的兼容性数据中获益,让我们的线上项目更加稳定、和谐的跑在用户电脑里。不过对于caniuse页面上的一些细节,我们可能会感到困惑或者模棱两可,今天就带着大家一起来重新认识caniuse这个网站,并对它的原理和细节做些探究。

1.1从babel-preset-env说起

babel-preset-env是babel6中极力推崇的一个preset,preset代表的是babel plugins的一个集合,相当于一堆plugins的一个统称。在babel最开始打江山的时候,es6标准也发布不久,babelrc的配置中只需要添加es2015这样的preset。但随着es2016、es2017的相继出现,babelrc很快就会变成一堆挂历式的集合体。所以babel给出了env这个杀器,既避免了es20xx的出现,又可以与caniuse的权威数据融合,让配置preseet科学而简单。

{
  "presets": [
    ["env", {
      "targets": {
        "browsers": ["last 2 versions", "safari >= 7"]
      }
    }]
  ]
}

这是babel官网给出的env配置方案,"last 2 versions", "safari >= 7",这两个条件是并集的关系,babel将会分别给出满足这两个条件的浏览器及版本,并会进行合并,最后算出一组浏览器及对应最低版本的数据。

babel是用来转换js语法的一个编译器,为什么还能知道满足env条件的浏览器跟版本,这要从browserslist这个库说起.

1.2 Browserslist

这个库不仅仅用在babel-preset-env中,像autoprefixer这样知名的库,也是用到了它。

last 1 version
> 1%
maintained node versions
not dead

browserslist能够把上面近似于人类语言的配置,转换成一组浏览器集合。不过它的主要职责就是转化上面的配置内容,按照正则过滤出正确浏览器列表内容,而它本身不提供浏览器列表的来源。

1.3 caniuse-lite

Browserslist的浏览器数据来源就是这个caniuse-lite,而它是caniuse-db库的精简版本,是从caniuse-db库衍化而来,只不过对caniuse-db数据按照一定规则做了简化,使得库的大小减少了许多,并且提供一些查询api供他人使用,每当caniuse-db更新时,也会跟着一起发布版本。

1.4 caniuse-db

caniuse的npm包,提供了caniuse网站查询所需的所有数据。


2.caniuse库的介绍

caniuse-db的github地址在此,caniuse鼓励大家去github上提交pr,经过审核之后就可以被录用到它的官方数据库中。

2.1如何为caniuse贡献数据

首先,它为我们准备了sample-data.json文件,按照此文件格式把需要增加的特性名称、介绍和浏览器兼容性情况填写清楚,保存并放到features-json文件夹中,最后提交pull request即可,审核完毕后会自动把这部分新增特性保存到data.json中。data.json就是caniuse官方的数据库导出文件,供其他库调用,每次json文件变化后,都会release一个新版本。

2.2 sample-data.json

作为新特性发布的样本文件,内容如下:

{
  "title":"Sample title",
  "description":"Sample description",
  "spec":"http://example.com/path/to/spec.html",
  "status":"wd",
  "links":[
    {
      "url":"http://example.com/path/to/link.html",
      "title":"Link title"
    }
  ],
  "bugs":[
    {
      "description":"Sample bug description"
    }
  ],
  "categories":[
    "CSS"
  ],
  "stats":{
    "ie":{
      ...
      "11":"u"
    },
    "edge":{
      ...
      "18":"u"
    },
    "firefox":{
      ...
      "67":"u"
    },
    "chrome":{
      ...
      "75":"u"
    },
    "safari":{
      ...
      "TP":"u"
    },
    "opera":{
      ...
      "58":"u"
    },
    "ios_saf":{
      ...
      "12.2":"u"
    },
    "op_mini":{
      "all":"u"
    },
    "android":{
      ...
      "67":"u"
    },
    "bb":{
      "7":"u",
      "10":"u"
    },
    "op_mob":{
      ...
      "46":"u"
    },
    "and_chr":{
      "71":"u"
    },
    "and_ff":{
      "64":"u"
    },
    "ie_mob":{
      ...
      "11":"u"
    },
    "and_uc":{
      "11.8":"u"
    },
    "samsung":{
      ...
      "8.2":"u"
    },
    "and_qq":{
      "1.2":"u"
    },
    "baidu":{
      "7.12":"u"
    }
  },
  "notes":"Sample notes for feature, explain partial support here",
  "notes_by_num":{
    "1":"First note..."
  },
  "usage_perc_y":0,
  "usage_perc_a":0,
  "ucprefix":false,
  "parent":"parentfeatureid",
  "keywords":"example,keywords",
  "shown":false,
  "ie_id":"",
  "chrome_id":"",
  "firefox_id":"",
  "webkit_id":""
}

简要介绍下其中的几个关键字段:

(1)title:特性名称
(2)description:特性介绍(搜索时的关键字)
(3)spec:跳转到详细介绍页面
(4)links:拓展内容介绍
(5)keywords:搜索时的关键字
(6)status:特性在标准中的状态

  • ls - 标准
  • rec - W3C 推荐
  • pr - W3C 建议
  • cr - W3C 候选
  • wd - W3C 手稿
  • other - 非W3C, 但流行的
  • unoff - 非官方

(7)categories:分类

  • HTML5
  • CSS
  • CSS2
  • CSS3
  • SVG
  • PNG
  • JS API
  • Canvas
  • DOM
  • Other
  • JS
  • Security

从上面分类可以看出,caniuse并不只是一个查询css兼容性的网站。

如果想查看目前caniuse已经支持了多少种特性,以及特性对应的分组信息,可以点击这个网址

(8)stats:浏览器对特性的支持情况

  • y - (Y)es, supported by default 完全支持
  • a - (A)lmost supported (aka Partial support) 部分支持
  • n - (N)o support, or disabled by default 不支持
  • p - No support, but has (P)olyfill 不支持,但有替代方案
  • u - Support (u)nknown 未知
  • x - Requires prefi(x) to work 需要加前缀
  • d - (D)isabled by default (need to enable flag or something)需要打flag
  • '#n' - Where n is a number, starting with 1, corresponds to the notes_by_num note. 支持,请看介绍第n条

(9)stats:浏览器列表

  • ie
  • edge
  • firefox
  • chrome
  • safari
  • opera
  • ios_saf
  • op_mini
  • android
  • bb
  • op_mob
  • and_chr
  • and_ff
  • ie_mob
  • and_uc
  • samsung
  • and_qq
  • baidu

以上浏览器列表是固定的,用户不能增加和缺少某个浏览器类型。

总结

每当增加一个新特性时,都要对以上浏览器列表以及对应版本列表进行实测,特性的测试可使用以下两个官方推荐的网站https://www.browserstack.comhttp://saucelabs.com
对于第二个网站,可用于因浏览器兼容性造成生产事故的还原测试,在其网站的虚拟机内完成特定浏览器特定版本的实测,在测试完成后可以观看操作视频,并支持导出功能,这对技术解决兼容性问题,提供了第一现场的操作流程,方便问题的解决。

3.caniuse网站介绍

3.1主页面介绍

clipboard.png
页面红字标注了4个地方
(1)代表了这个介绍框的内容隶属于一个特性,也就是我们在features-json看到的一个个跟特性相关的文件,没有#标志的不属于特性。
(2)代表这个特性在标准中所处的一个状态,具体参照前文对sample-data.json的介绍
(3)对于这个特性,在全球、中国所有浏览器中,分别有多少完全支持和部分支持,把两部分值加起来,得到总份额。
(4)浏览器基线,代表对应浏览器current状态的版本号。基线往上是该浏览器的低版本,并对相同支持情况的版本进行合并。基线往下是未来的三个版本,并进行状态合并。

3.2浏览器信息统计

caniuse关于浏览器的数据,主要都来源于statcounter,此网站统计了全球以及各国的浏览器使用情况。

上面提到的浏览器基线及版本号列表,都是基于statcounter上个月份的数据统计。
例如chrome v73在3月12号发布了版本,但在caniuse网站里,v72还是作为了current版本,就是因为caniuse的分析数据来源于2月份的统计数据,数据并不是实时更新。

3.3详情

clipboard.png
页面红字标注了4个地方
(1)浏览器对特性支持情况相同的版本区间
(2)对特性的支持情况
(3)火狐40-火狐64的发布时间
(4)火狐40-火狐64,在全球、中国的使用份额

4.想法

知道了caniuse的数据来源及原理之后,我们是否可以打造属于自己公司的caniuse,暂且就叫做caniuse-shein

(1)
目前,我司的前端只负责中后台系统,面向的用户群体有限。
而我手头上有一个专门为公司前端而打造的APM项目,里面包含了详尽的浏览器版本及份额数据,将APM项目中的浏览器数据与caniuse的特性数据相结合,可以制作出类似于caniuse官网的特性查询分析页面,但报表数据只关心我司的用户群里使用的浏览器,而非依据全球或者全国。

(2)
babel-preset-env这个插件也可以结合caniuse-shein的数据,给出对应浏览器份额的babel插件列表

招聘

  1. 有想找工作的前端朋友,可以投递【字节跳动】,base全国,目前海量HC,从速投递。请扫描下图获取所有岗位列表

image

2.另外有想了解招聘详情的同学也可加我微信 stoneyAllen

查看原文

赞 41 收藏 22 评论 2

siwuxie 赞了文章 · 2020-08-24

Vue组件库开发总结

Vue组件库开发总结

由于工作需要,最近在学习怎么开发一个Vue组件库。主要需要实现以下点:
1.组件使用npm包引入
2.实现按需引入及按需打包
项目中许多实现是参考的element-ui,特别是webpack打包部分

组织项目

项目生成

项目生成是直接用的vue-cli,在根目录下增加了一个index.js,用于组件打包的入口文件,两个webpack打包文件,以及一个组件的json文件,用于之后的按需引入的打包。组件放置在src/cmps中,目录结构如下图:
project-dir

组件结构

由于我的组件把样式都写在了vue里面,所以没有单独的样式文件,就是一个Vue文件和一个js入口文件
图片描述

组件编写

单个组件编写

vue组件的编写需要按照官方的vue插件开发规范来。为了实现后续的按需打包,在每一组件的入口文件中,都需要定义install方法,并随组件一同暴露出来

import Button from './index.vue'
Button.intall = function (vue) {
  vue.component(Button.name, Button)
}
export default Button

所有组件输出编写

所有组件的输出就是将所有组件暴露出去,并加上一个对所有组件的install方法。其中if(window && window.Vue) install(window.Vue)是用来实现script标签引入的方式的。

import Input from 'src/cmps/input/index.js'
import Toast from 'src/cmps/toast/index.js'
import Button from 'src/cmps/button/index.js'

const cmps = [
  Input,
  Toast,
  Button
]

const install = vue => {
  cmps.map(cmp => {
    vue.component(cmp.name, cmp)
  })
}
if(window && window.Vue) install(window.Vue)

export default {
  install,
  Input,
  Toast,
  Button
}

组件打包

全量加载的打包

全量加载的打包首先是把vue-cli生成的webpack文件改一下打包的出入口文件和路径就行了。为了方便之后的按需加载的打包,出口文件我的路径放在了lib目录下。
为了实现npm包、script标签等引入形式,libraryTarget选择了umd模式。library是npm包引入时的名称。entry的写法是我为了用dev在本地测试组件是否可用而写的。externals是为了去除在组件库和实际项目中会重复的库,比如vue

entry: ENV == 'dev'? path.resolve(__dirname, './src/main.js'): path.resolve(__dirname, './index.js'),
output: {
  path: path.resolve(__dirname, './lib'),
  publicPath: '/dist/',
  filename: 'input-ui.js',
  library: 'input-ui',
  libraryExport: 'default',
  libraryTarget: 'umd'
},
externals: {
  vue: 'vue'
}

package.json中需要加入对主入口的说明

"main": "lib/input-ui.js"

按需加载的打包

按需加载的打包主要是参考了element-ui的代码。需要对每个组件单独打包,以及对所有组件全量打包。其实我没有明白这里的全量打包和上面说的量加载的全量打包有什么区别。好像是libraryTarget不一样,element-ui中,全量打包的主入口文件的libraryTarget是commonjs2,而按需打包中的主入口文件的libraryTarget是umd。我这里的libraryTarget是随便写的,因为我其实并不需要用到采用script标签的引入方式。
按需加载的打包是将所有组件单独打包,和全量打包的差别也只是出入口的差别。

const entry = require('./comps.json')
module.exports = {
  entry ,
  output: {
    path: path.resolve(__dirname, './lib'),
    publicPath: '/dist/',
    filename: '[name].js',
    chunkFilename: '[id].js',
    libraryTarget: 'commonjs2'
  },

comps.json

{
  "input": "./src/cmps/input",
  "toast": "./src/cmps/toast",
  "button": "./src/cmps/button"
}

按需打包的使用

要使用按需打包,不仅组件库的打包需要做处理,项目中也需要做处理。通过上面的方式打包的组件库,在项目中可以使用babel-plugin-component来实现按需打包。
在.babelrc中加入这个组件。libraryName是需要按需打包的库名。由于我的简陋的组件中,没有引用单独的样式,所以style设成了false,不然在引用组件时,还需要获取一个样式文件。之前提到为了方便按需打包,所以打包后的目录是lib,这是由于babel-plugin-component默认的库的目录就是lib,如果需要修改目录只需要加入"libDir": "lib"具体的babel-plugin-component的使用可以官方的readme。

"plugins": [["component", {
  "libraryName": "input-ui",
  "style": false
}]]

然后就可以在项目中使用按需引入的方式来使用组件了

import {Input, Button} from 'input-ui'
Vue.component(Input.name, Input)
Vue.component(Button.name, Button)

npm包本地测试

在发布npm包之前,我们需要先在本地测试这个包是否到达我们预想的效果。这时,我们可以使用npm link把我们需要测试的包link到npm本地的全局。再在本地测试项目中通过npm link 本地测试的包名把我们要测试的本地包引入测试项目中,便可以对我们只做的组件库npm包进行本地测试了。

npm包发布

首先需要在npm官网注册账号。npmjs.com
然后添加用户npm adduser填入账号密码及邮箱。
通过npm publish发布包就行了。如果遇到报错说没有权限发布该包,基本上是因为这个包名已被使用。换个其他的名字就行了。而且每次发布包都需要修改版本号,不同发布两个相同的版本号。

最后

这是本人第一次写文章,而且本人水平有限,对webpack的打包,其实并不是很了解,所以写的有些乱,甚至可能有些错误,请大家及时指出,感谢。gitHub

查看原文

赞 3 收藏 1 评论 0

siwuxie 赞了文章 · 2020-08-23

如何开发一个基于 Vue 的 ui 组件库(一)

开发模式

预览 demo

在开发一个 ui 组件库时,肯定需要一边预览 demo,一边修改代码。

常见的解决方案是像开发一般项目一样使用 webpack-dev-server 预览组件,比如通过 vue-cli 初始化项目,或者自己配置脚本。

文艺一点儿地可能会用到 parcel 来简化 demo 的开发配置(比如 muse-ui)。

展示文档

作为一个 ui 组件库,也肯定要有自己的组件展示文档。

一般业界常见方案是自己开发展示文档...

但这样会带来一个组件库和文档如何同步的问题。

为何不用 vuepress?

由于 vuepress 支持在 markdown 中插入组件,所以我们其实可以很自然地边写文档边开发组件。

从开发步骤上来说,甚至可以先写文档说明,再具体地编写代码实现组件功能。这样一来文档即是预览 demo,与组件开发可以同步更新。

p.s. React 的组件文档可以试试这俩库:

类型声明

在开发和使用过程中如果对于一些对象、方法的参数能够智能提示,岂不美哉?

如何实现呢?

其实就是在相应文件夹中添加组件相关的类型声明(*.d.ts),并通过 src/index.d.ts 导出。

{
    "typings": "src/index.d.ts",
}
一开始将声明文件都放在 types/ 文件夹下,但在实践中觉得还是放在当前文件夹下比较好。一方面有利于维护,另一方面是读取源码时也有类型提示。

如何打包

打包工具

和打包库一样,选了 rollup。

单文件组件

在开发中用不用 *.vue 这样的单文件组件来开发呢?

  • muse-ui 完全不写 <template> 只使用 render 函数。
  • iviewelementvant 使用 .vue 文件,但样式单独写。
  • ant-design-vue 使用 .jsx 文件,样式也单独写。
  • vux 使用带 <style>.vue 文件,但在使用时必须用 vux-loader
  • cube-ui 使用带 <style>.vue 文件,但有一些配置

讲道理,完全不写 <template> 有点儿麻烦,所以添加了 rollup-plugin-vue 插件用于打包 .vue 文件。

但碰到一个问题:如何打包 <style> 中的样式?

  • 首先尝试不写 <style>,直接在 js 里 import scss 文件。没问题,但是写组件时不直观,同一组件的代码也分散在了两个地方
  • 接着尝试配置 rollup-plugin-vue,碰到一个 source-map 报错的问题。我提了个 issue

加载方式

区分场景

为了区分不同的场景使用不同的 js,所以一共打包了三份 js(commonJses moduleumd),以及一份压缩后的 css(dist/tua-ui.css)。

{
    "main": "dist/TuaUI.cjs.js",
    "module": "dist/TuaUI.es.js",
}

完整加载

大部分 ui 库都支持完整加载,和把大象装冰箱一样简单(但 vux 只支持按需加载):

  1. 引入 js
  2. 引入 css
  3. 安装插件
import TuaUI from '@tencent/tua-ui'
import '@tencent/tua-ui/dist/tua-ui.css'

Vue.use(TuaUI)
因缺思厅的是 cube-ui 把基础样式也写成 Vue 插件,导致按需引入的时候还要单独引入 Style,emmmmmmmmm...
import {
  /* eslint-disable no-unused-vars */
  Style, // <-- 不写这行按需引入时就没基础样式
  Button
} from 'cube-ui'

按需加载

ui 库若是只能完整加载,显然会打包多余代码。

所以各种库一般都支持按需加载组件,大概分以下几种。

tree-shaking

webpack 其实在打包的时候是支持 tree-shaking 的,那么我们能不能直接引用源码实现按需加载呢?

注意源码必须满足 es 模块规范(import、export)。
import { TuaToast } from '@tencent/tua-ui/src/'

Vue.use(TuaToast)

尝试打包,发现 tree-shaking 并没有起作用,还是打包了所有代码。

sideEffects

其实问题出在没有在 ui 库的 package.json 中声明 sideEffects 属性。

在一个纯粹的 ESM 模块世界中,识别出哪些文件有副作用很简单。然而,我们的项目无法达到这种纯度,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。 —— 《webpack 文档》

注意:样式部分是有副作用的!即不应该被 tree-shaking

若是直接声明 sideEffectsfalse,那么打包时将不包括样式!所以应该像下面这样配置:

{
    "sideEffects": [ "*.sass", "*.scss", "*.css" ],
}

vuepress 组件样式

用 vuepress 写文档的时候,一般会在 docs/.vuepress/components/ 下写一些全局组件。

开发时没啥问题,但是发现一个坑:打包文档时发现组件里的样式 <style> 全丢了。

猜一猜原因是什么?

这口锅就出在上一节的 sideEffects,详情看这个 issue。解决方案就是在 sideEffects 里加一条 "*.vue" 即可。

测试数据

下面咱们打包一下安装了 ui 库的项目,看看按需加载的效果怎么样。

  • Origin

    • dist/js/chunk-vendors.71ea9e72.js ----- 114.04 kb
  • TuaToast

    • dist/js/chunk-vendors.beb8cff5.js ----- 115.03 kb
    • dist/css/chunk-vendors.97c93b2d.css ----- 0.79 kb
  • TuaIcon

    • dist/js/chunk-vendors.25d8bdbd.js ----- 115.00 kb
    • dist/css/chunk-vendors.eab6517c.css ----- 6.46 kb
  • TuaUI

    • dist/js/chunk-vendors.6e0e6390.js ----- 117.39 kb
    • dist/css/chunk-vendors.7388ba27.css ----- 8.04 kb

总结一下就是:

  • 原始项目的 js 打包出来为 114.o4kb
  • 只添加 TuaToast 后 js 增加了 0.99kb,css 增加了 0.79kb
  • 只添加 TuaIcon 后 js 增加了 0.96kb,css 增加了 6.46kb
  • 添加完整 TuaUI 后 js 增加了 3.35kb,css 增加了 8.04kb

可以看出按需加载还是有效果的~

以上 to be continued...

参考资料

查看原文

赞 23 收藏 18 评论 2

siwuxie 赞了文章 · 2020-07-29

Vue 视图更新patch过程源码解析

在这篇文章深入源码学习Vue响应式原理讲解了当数据更改时,Vue是如何通知订阅者进行更新的,这篇文章讲得就是:视图知道了依赖的数据的更改,如何将新的数据反映在视图上。

Vnode Tree

在真实的HTML中有DOM树与之对应,在Vue中也有类似的Vnode Tree与之对应。

抽象DOM

jquery时代,实现一个功能,往往是直接对DOM进行操作来达到改变视图的目的。但是我们知道直接操作DOM往往会影响重绘和重排,这两个是最影响性能的两个元素。
进入Virtual DOM时代以后,将真实的DOM树抽象成了由js对象构成的抽象树。virtual DOM就是对真实DOM的抽象,用属性来描述真实DOM的各种特性。当virtual DOM发生改变时,就去修改视图。在Vue中就是Vnode Tree的概念

VNode

当修改某条数据的时候,这时候js会将整个DOM Tree进行替换,这种操作是相当消耗性能的。所以在Vue中引入了Vnode的概念:Vnode是对真实DOM节点的模拟,可以对Vnode Tree进行增加节点、删除节点和修改节点操作。这些过程都只需要操作VNode Tree,不需要操作真实的DOM,大大的提升了性能。修改之后使用diff算法计算出修改的最小单位,在将这些小单位的视图进行更新。

// core/vdom/vnode.js
class Vnode {
    constructor(tag, data, children, text, elm, context, componentOptions) {
        // ...
    }
}

生成vnode

生成vnode有两种情况:

  1. 创建非组件节点的vnode

    • tag不存在,创建空节点、注释、文本节点
    • 使用vue内部列出的元素类型的vnode
    • 没有列出的创建元素类型的vnode

<p>123</p>为例,会被生成两个vnode:

    • tagp,但是没有text值的节点
    • 另一个是没有tag类型,但是有text值的节点
    1. 创建组件节点的VNode

    组件节点生成的Vnode,不会和DOM Tree的节点一一对应,只存在VNode Tree

    // core/vdom/create-component
    function createComponent() {
        // ...
        const vnode = new VNode(
            `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
            data, undefined, undefined, undefined, context,
            { Ctor, propsData, listeners, tag, children }
        )
    }
    这里创建一个组件占位`vnode`,也就不会有真实的`DOM`节点与之对应  
    

    组件vnode的建立,结合下面例子进行讲解:

    <!--parent.vue-->
    <div classs="parent">
        <child></child>
    </div>
    <!--child.vue-->
    <template>
        <div class="child"></div>
    </template>

    真实渲染出来的DOM Tree是不会存在child这个标签的。child.vue是一个子组件,在Vue中会给这个组件创建一个占位的vnode,这个vnode在最终的DOM Tree不会与DOM节点一一对应,即只会出现vnode Tree中。

    /* core/vdom/create-component.js */
    export function createComponent () {
        // ...
         const vnode = new VNode(
        `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
        data, undefined, undefined, undefined, context,
        { Ctor, propsData, listeners, tag, children }
        )
    }

    那最后生成的Vnode Tree就大概如下:

    vue-component-${cid}-parent
        vue-component-${cid}-child
            div.child

    最后生成的DOM结构为:

    <div class="parent">
        <div class="child"></div>
    </div>

    在两个组件文件中打印自身,可以看出两者之间的关系
    chlid实例对象

    parent实例对象

    可以看到以下关系:

    1. vnode通过children指向子vnode
    2. vnode通过$parent指向父vnode
    3. 占位vnode为实例的$vnode
    4. 渲染的vnode为对象的_vnode

    patch

    在上一篇文章提到当创建Vue实例的时候,会执行以下代码:

    updateComponent = () => {
        const vnode = vm._render();
        vm._update(vnode)
    }
    vm._watcher = new Watcher(vm, updateComponent, noop)

    例如当data中定义了一个变量a,并且模板中也使用了它,那么这里生成的Watcher就会加入到a的订阅者列表中。当a发生改变时,对应的订阅者收到变动信息,这时候就会触发Watcherupdate方法,实际update最后调用的就是在这里声明的updateComponent
    当数据发生改变时会触发回调函数updateComponentupdateComponent是对patch过程的封装。patch的本质是将新旧vnode进行比较,创建、删除或者更新DOM节点/组件实例。

    // core/vdom/patch.js
    function createPatchFunction(backend) {
        const { modules, nodeOps } = backend;
        for (i = 0; i < hooks.length; ++i) {
            cbs[hooks[i]] = []
            for (j = 0; j < modules.length; ++j) {
              if (isDef(modules[j][hooks[i]])) {
                cbs[hooks[i]].push(modules[j][hooks[i]])
              }
            }
        }
        
        return function patch(oldVnode, vnode) {
            if (isUndef(oldVnode)) {
                let isInitialPatch = true
                createElm(vnode, insertedVnodeQueue, parentElm, refElm)
            } else {
                const isRealElement = isDef(oldVnode.nodeType)
                if (!isRealElement && sameVnode(oldVnode, vnode)) {
                    patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
                } else {
                    if (isRealElement) {
                        oldVnode = emptyNodeAt(oldVnode)
                    }
                    const oldElm = oldVnode.elm
                    const parentElm = nodeOps.parentNode(oldElm)
                    createElm(
                        vnode,
                        insertedVnodeQueue,
                        oldElm._leaveC ? null : parentELm,
                        nodeOps.nextSibling(oldElm)
                    )
                    
                    if (isDef(vnode.parent)) {
                        let ancestor = vnode.parent;
                        while(ancestor) {
                            ancestor.elm = vnode.elm;
                            ancestor = ancestor.parent
                        }
                        if (isPatchable(vnode)) {
                            for (let i = 0; i < cbs.create.length; ++i) {
                                cbs.create[i](emptyNode, vnode.parent)
                            }
                        }
                    }
                    if (isDef(parentElm)) {
                        removeVnodes(parentElm, [oldVnode], 0, 0)
                    } else if (isDef(oldVnode.tag)) {
                        invokeDestroyHook(oldVnode)
                    }
                }
            }
            
            invokeInsertHook(vnode, insertedVnodeQueue)
            return vode.elm
        }
    }
    • 如果是首次patch,就创建一个新的节点
    • 老节点存在

      • 老节点不是真实DOM并且和新节点相似

        • 调用patchVnode修改现有节点
      • 新老节点不相同

        • 如果老节点是真实DOM,创建对应的vnode节点
        • 为新的Vnode创建元素/组件实例,若parentElm存在,则插入到父元素上
        • 如果组件根节点被替换,遍历更新父节点element。然后移除老节点
    • 调用insert钩子

      • 是首次patch并且vnode.parent存在,设置vnode.parent.data.pendingInsert = queue
      • 如果不满足上面条件则对每个vnode调用insert钩子
    • 返回vnode.elm真实DOM内容

    nodeOps上封装了针对各种平台对于DOM的操作,modules表示各种模块,这些模块都提供了createupdate钩子,用于创建完成和更新完成后处理对应的模块;有些模块还提供了activateremovedestory等钩子。经过处理后cbs的最终结构为:

    cbs = {
        create: [
            attrs.create,
            events.create
            // ...
        ]
    }

    可以看到的是只有当oldVnodevnode满足sameVnode的时候,并且新vnode都是vnode节点,不是真实的DOM节点。 其他情况要么创建,要么进行删除。
    当下面情况时出现时就会出现根节点被替换的情况:

    <!-- parent.vue -->
    <template>
        <child></child>
    </template>
    <!-- child.vue -->
    <template>
        <div class="child">
            child
        </div>
    </template>

    这个时候parent生成的vnode.elm就是div.child的内容。
    patch函数最后返回了经过一系列处理的vnode.elm也就是真实的DOM内容。

    createElm

    createElm的目的创建VNode节点的vnode.elm。不同类型的VNode,其vnode.elm创建过程也不一样。对于组件占位VNode,会调用createComponent来创建组件占位VNode的组件实例;对于非组件占位VNode会创建对应的DOM节点。
    现在有三种节点:

    • 元素类型的VNode:

      • 创建vnode对应的DOM元素节点vnode.elm
      • 设置vnodescope
      • 调用createChildren遍历子节点创建对应的DOM节点
      • 执行create钩子函数
      • DOM元素插入到父元素中
    • 注释和本文节点

      • 创建注释/文本节点vnode.elm,并插入到父元素中
    • 组件节点:调用createComponent
    function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
        // 创建一个组件节点
        if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
            return
        }
        const data = vnode.data;
        const childre = vnode.children;
        const tag = vnode.tag;
        // ...
    
        if (isDef(tag)) {
            vnode.elm = vnode.ns
                ? nodeOps.createElementNS(vnode.ns, tag)
                : nodeOps.createElement(tag, vnode)
            setScope(vnode)
            if (isDef(data)) {
                invokeCreateHooks(vnode, insertedVnodeQueue)
            }
            createChildren(vnode, children, insertedVnodeQueue)  
        } else if (isTrue(vnode.isComment)) {
            vnode.elm = nodeOps.createComment(vnode.text);
        } else {
            vnode.elm = nodeOps.createTextNode(vnode.te)
        }
        insert(parentElm, vnode.elm, refElm)
    }

    createComponent的主要作用是在于创建组件占位Vnode的组件实例, 初始化组件,并且重新激活组件。在重新激活组件中使用insert方法操作DOMcreateChildren用于创建子节点,如果子节点是数组,则遍历执行createElm方法,如果子节点的text属性有数据,则使用nodeOps.appendChild()在真实DOM中插入文本内容。insert用将元素插入到真实DOM中。

    // core/vdom/patch.js
    function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
        // ...
        let i = vnode.data.hook.init
        i(vnode, false, parentElm, refElm)
        if (isDef(vnode.componentInstance)) {
            initComponent(vnode, insertedVnodeQueue)
            insert(parentElm, vnode.elm, refElm)
            return true;
        }
    }
    function initComponent(vnode, insertedVnodeQueue) {
        /* 把之前的已经存在的Vnode队列合并进去 */
        if (isDef(vnode.data.pendingInsert)) {
            insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
        }
        vnode.elm = vnode.componentInstance.$el;
        if (isPatchable(vnode)) {
            // 调用create钩子
            invokeCreateHooks(vnode, insertedVnodeQueue);
            // 为scoped css设置scoped id
            setScope(vnode)
        } else {
            // 注册ref
            registerRef(vnode);
            insertedVnodeQueue.push(vnode)
        }
    }
    • 执行init钩子生成componentInstance组件实例
    • 调用initComponent初始化组件

      • 把之前已经存在的vnode队列进行合并
      • 获取到组件实例的DOM根元素节点,赋给vnode.elm
      • 如果vnode是可patch

        • 调用create函数,设置scope
      • 如果不可patch

        • 注册组件的ref,把组件占位vnode加入insertedVnodeQueue
    • vnode.elm插入到DOM Tree

    createComponent中,首先获取
    在组件创建过程中会调用core/vdom/create-component中的createComponent,这个函数会创建一个组件VNode,然后会再vnode上创建声明各个声明周期函数,init就是其中的一个周期,他会为vnode创建componentInstance属性,这里componentInstance表示继承Vue的一个实例。在进行new vnodeComponentOptions.Ctor(options)的时候就会重新创建一个vue实例,也就会重新把各个生命周期执行一遍如created-->mounted

    init (vnode) {
        // 创建子组件实例
        const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
        chid.$mount(undefined)
    }
    function createComponentInstanceForVnode(vn) {
        // ... options的定义
        return new vnodeComponentOptions.Ctor(options)
    }

    这样child就表示一个Vue实例,在实例创建的过程中,会执行各种初始化操作, 例如调用各个生命周期。然后调用$mount,实际上会调用mountComponent函数。

    // core/instance/lifecycle
    function mountComponent(vm, el) {
        // ...
        updateComponent = () => {
            vm._update(vm._render())
        }
        vm._watcher = new Watcher(vm, updateComponent, noop)
    }

    在这里就会执行vm._render

    // core/instance/render.js
    Vue.propotype._render = function () {
        // ...
        vnode = render.call(vm._renderProxy, vm.$createElement)
        return vnode
    }

    可以看到的时候调用_render函数,最后生成了一个vnode。然后调用vm._update进而调用vm.__patch__生成组件的DOM Tree,但是不会把DOM Tree插入到父元素上,如果子组件中还有子组件,就会创建子孙组件的实例,创建子孙组件的DOM Tree。当调用insert(parentElm, vnode.elm, refElm)才会将当前的DOM Tree插入到父元素中。
    在回到patch函数,当不是第一次渲染的时候,就会执行到另外的逻辑,然后oldVnode是否为真实的DOM,如果不是,并且新老VNode不相同,就执行patchVnode

    // core/vdom/patch.js
    function sameVnode(a, b) {
        return (
            a.key === b.key &&
            a.tag === b.tag && 
            a.isComment === b.isComment &&
            isDef(a.data) === isDef(b.data) &&
            sameInputType
        )
    }

    sameVnode就是用于判断两个vnode是否是同一个节点。

    insertedVnodeQueue的作用

    在当前patch过程中,有一个数组insertedVnodeQueue,这是干嘛的,从单词上来看就是对这个队列中的vnode调用inserted钩子。在patch函数中最后调用了invokeInserthook

    function invokeInsertHook(vnode, queue, initial) {
        if (isTrue(initial) && isDef(vnode.parent)) {
            vnode.parent.data.pendingInsert = queue;
        } else {
            for (let i = 0; i < queue.length; ++i) {
                queue[i].data.hook.insert(queue[i])
            }
        }
    }

    当不是首次patch并且vnode.parent不存在的时候,就会对insertedVnodeQueuevnode进行遍历,依次调用inserted钩子。
    那什么时候对insertedVnodeQueue进行修改的呢。

    function createElm() {
        // ...
        if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
            return
        }
        if (isDef(tag)) {
            if (isDef(data)) {
                invokeCreateHooks(vnode, insertedVnodeQueue);
            }
        }
    }
    function initComponent(vnode, insertedVnodeQueue) {
        if (isDef(vnode.data.pendingInsert)) {
            insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
        }
        if (isPatchable) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
        } else {
            insertedVnodeQueue.push(vnode)
        }
    }
    function invokeCreateHooks(vnode, insertedVnodeQueue) {
        // ...
        insertedVnodeQueue.push(vnode);
    }

    在源码中可以看到在createElm中对组件节点和非组件节点都对insertedVnodeQueue进行了操作,每创建一个组件节点或非组件节点的时候就会往insertedVnodeQueuepush当前的vnode,最后对insertedVnodeQueue中所有的vnode调用inserted钩子。
    但是当子组件首次渲染完成以后,invokeInsertHook中不会立即调用insertedVnodeQueue中各个Vnodeinsert方法,而是将insertedVnodeQueue转存至父组件占位vnodevnode.data.pendingInert上,当父组件执行initComponent的时候,将子组件传递过来的insertedVnodeQueue和自身的insertedVnodeQueue进行连接,最后调用父组件的insertedVnodeQueue中各个vnodeinsert方法。

    Vnode的生命周期

    createPatchFunction中会传入参数backend

    function createPatchFunction (backend) {
        const { modules, nodeOps } = backend;
    }

    nodeOps是各种平台对DOM节点操作的适配,例如web或者weex
    modules是各种平台的模块,以web为例:
    Web平台相关模块:

    - `attrs`模块: 处理节点上的特性`attribute`
    - `klass`模块:处理节点上的类`class`
    - `events`模块: 处理节点上的原生事件
    - `domProps`模块: 处理节点上的属性`property`
    - `style`模块: 处理节点上的内联样式`style`特性
    - `trasition`模块

    核心模块:

    - `ref`模块:处理节点上的引用`ref`
    - `directives`模块: 处理节点上的指令`directives`

    每个功能模块都包含了各种钩子,用于DOM节点创建、更新和销毁。
    Vnode中存在各种生命周期如:

    - create:`DOM`元素节点创建时/初始化组件时调用
    - activate: 组件激活时调用
    - update: `DOM`节点更新时调用
    - remove: `DOM`节点移除时调用
    - destory: 组件销毁时调用

    那这些生命周期是如何加入的,回到最开始的地方:

    vnode = vm.render();
    Vue.prototype._render = function () {
        const vm = this;
        const {
            render,
        } = vm.$options;
        vnode = render.call(vm._renderProxy, vm.$createElement)
        return vnode;
    }

    vnode是由render.call(vm._renderProxy, vm.$createElement)生成的。
    这里的render有两种情况:

    1. 基于HTML的模板形式,即template选项
    2. 用于手写的render函数形式

    使用template形式的模板最终转换为render函数的形式。vm.$createElement返回的就是vnodecreateElementvdom/create-element中,对于真实的DOM还是组件类型用不同的方式创建相应的vnode

    1. 真实节点调用vnode = new VNode(tag, data, children, undefined, undefined, context)
    2. 组件节点调用createComponent(Ctor, data, context, children, tag)

    createComponent定义在vdom/create-component

    function createComponent(Ctor, data, context, children, tag) {
        mergeHooks();
    }
    const componentVnodeHooks = {
        init(){},
        prepatch(){},
        insert(){},
        destory(){}
    }
    function mergeHooks(data) {
        if (!data.hook) {
            data.hook = {}
        }
        const hooksToMerge = Object.keys(componentVNodeHooks)
        for (let i = 0; i < hooksToMerge.length; i++) {
            const key = hooksToMerge[i];
            const fromParent = data.hook[key]
            const ours = componentVNodeHooks[key];
            data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours;
        }
    }

    在这里就给vnode.data.hook上绑定了各种钩子initprepatchinsertdestroy。在patch过程中,就会调用对应的钩子。

    patchVnode

    如果符合sameVnode,就不会渲染vnode重新创建DOM节点,而是在原有的DOM节点上进行修补,尽可能复用原有的DOM节点。

    • 如果两个节点相同则直接返回
    • 处理静态节点的情况
    • vnode是可patch

      • 调用组件占位vnodeprepatch钩子
      • update钩子存在,调用update钩子
    • vnode不存在text文本

      • 新老节点都有children子节点,且children不相同,则调用updateChildren递归更新children(这个函数的内容放到diff中进行讲解)
      • 只有新节点有子节点:先清空文本内容,然后为当前节点添加子节点
      • 只有老节点存在子节点: 移除所有子节点
      • 都没有子节点的时候,就直接移除节点的文本
    • 新老节点文本不一样: 替换节点文本
    • 调用vnodepostpatch钩子
    function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
        if (oldVnode === vnode) return
        // 静态节点的处理程序
        const data = vnode.data;
        i = data.hook.prepatch
        i(oldVnode, vnode);
        if (isPatchable(vnode)) {
            for(i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
            i = data.hook.update
            i(oldVnode, vnode)
        }
        const oldCh = oldVnode.children;
        const ch = vnode.children;
        if (isUndef(vnode.text)) {
            if (isDef(oldCh) && isDef(ch)) {
                if (oldCh !== ch) updateChildren(elm, oldCh, ch insertedVnodeQueue, removeOnly)
            } else if (isDef(ch)) {
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
          } else if (isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
          } else if (isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, '')
          }
        } else if (oldVnode.text !== vnode.text) {
            nodeOps.setTextContent(elm, vnode.text)
        }
        i = data.hook.postpatch
        i(oldVnode, vnode)
    }

    diff算法

    patchVnode中提到,如果新老节点都有子节点,但是不相同的时候就会调用updateChildren,这个函数通过diff算法尽可能的复用先前的DOM节点。

    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
        let oldStartIdx = 0
        let newStartIdx = 0
        let oldEndIdx = oldCh.length - 1
        let oldStartVnode = oldCh[0]
        let oldEndVnode = oldCh[oldEndIdx]
        let newEndIdx = newCh.length - 1
        let newStartVnode = newCh[0]
        let newEndVnode = newCh[newEndIdx]
        let oldKeyToIdx, idxInOld, elmToMove, refElm 
        
        while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (isUndef(oldStartVnode)) {
                oldStartVnode = oldCh[++oldStartIdx]
            } else if (isUndef(oldEndVnode)) {
                oldEndVnode = oldCh[--oldEndIdx]
            } else if (sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
                oldStartVnode = oldCh[++oldStartIdx]
                newStartVnode = newCh[++newStartIdx]
            } else if (sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
                oldEndVnode = oldCh[--oldEndIdx]
                newEndVnode = newCh[--newEndIdx]
            } else if (sameVnode(oldStartVnode, newEndVnode)) {
                patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
                canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
                oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
            } else if (sameVnode(oldEndVnode, newStartVnode)) {
                patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
                canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
                oldEndVnode = oldCh[--oldEndIdx]
                newStartVnode = newCh[++newStartIdx]
            } else {
                if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
                idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
                if (isUndef(idxInOld)) {
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                    newStartVnode = newCh[++newStartIdx]
                } else {
                    elmToMove = oldCh[idxInOld]
                    if (sameVnode(elmToMove, newStartVnode)) {
                        patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
                        oldCh[idxInOld] = undefined
                        canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
                        newStartVnode = newCh[++newStartIdx]
                    } else {
                        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                        newStartVnode = newCh[++newStartIdx]
                    }
                }
            }
        }
        if (oldStartIdx > oldEndIdx) {
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else if (newStartIdx > newEndIdx) {
          removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
        }
    }

    算了这个图没画明白,借用网上的图


    oldStartIdxnewStartIdxoldEndIdx以及newEndIdx分别是新老两个VNode两边的索引,同时oldStartVnodenewStartVnodeoldEndVnodenew EndVnode分别指向这几个索引对应的vnode。整个遍历需要在oldStartIdx小于oldEndIdx并且newStartIdx小于newEndIdx(这里为了简便,称sameVnode为相似)

    1. oldStartVnode不存在的时候,oldStartVnode向右移动,oldStartIdx1
    2. oldEndVnode不存在的时候,oldEndVnode向右移动,oldEndIdx1
    3. oldStartVnodenewStartVnode相似,oldStartVnodenewStartVnode都向右移动,oldStartIdxnewStartIdx都增加1

    1. oldEndVnodenewEndVnode相似,oldEndVnodenewEndVnode都向左移动,oldEndIdxnewEndIdx都减1

    1. oldStartVnodenewEndVnode相似,则把oldStartVnode.elm移动到oldEndVnode.elm的节点后面。然后oldStartIdx向后移动一位,newEndIdx向前移动一位

    1. oldEndVnodenewStartVnode相似时,把oldEndVnode.elm插入到oldStartVnode.elm前面。同样的,oldEndIdx向前移动一位,newStartIdx向后移动一位。

    1. 当以上情况都不符合的时候

    生成一个key与旧vnode对应的哈希表

    function createKeyToOldIdx (children, beginIdx, endIdx) {
        let i, key
        const map = {}
        for (i = beginIdx; i <= endIdx; ++i) {
            key = children[i].key
            if (isDef(key)) map[key] = i
        }
        return map
    }

    最后生成的对象就是以childrenkey为属性,递增的数字为属性值的对象例如

    children = [{
        key: 'key1'
    }, {
        key: 'key2'
    }]
    // 最后生成的map
    map = {
        key1: 0,
        key2: 1,
    }

    所以oldKeyToIdx就是key和旧vnodekey对应的哈希表
    根据newStartVnodekey看能否找到对应的oldVnode

    • 如果oldVnode不存在,就创建一个新节点,newStartVnode向右移动
    • 如果找到节点:

      • 并且和newStartVnode相似。将map表中该位置的赋值undefined(用于保证key是唯一的)。同时将newStartVnode.elm插入啊到oldStartVnode.elm的前面,然后index向后移动一位
      • 如果不符合sameVnode,只能创建一个新节点插入到parentElm的子节点中,newStartIdx向后移动一位
    1. 结束循环后

      • oldStartIdx又大于oldEndIdx,就将新节点中没有对比的节点加到队尾中
    ![](https://user-gold-cdn.xitu.io/2019/11/19/16e83a83366194d3?w=784&h=373&f=png&s=73559)
    - 如果`newStartIdx > newEndIdx`,就说明还存在新节点,就将这些节点进行删除
    
    ![](https://user-gold-cdn.xitu.io/2019/11/19/16e83a871c34ea5f?w=836&h=367&f=png&s=77933)

    总结

    本篇文章对数据发生改变时,视图是如何更新进行了讲解。对一些细节地方进行了省略,如果需要了解更加深入,结合源码更加合适。我的github请多多关注,谢谢

    Log

    • 12-19: 更新patch的具体过程
    查看原文

    赞 10 收藏 6 评论 0

    siwuxie 关注了用户 · 2020-07-10

    张越 @zhangyue_59b6002eb8a88

    想去字节跳动,qq:1025873823

    关注 178

    siwuxie 提出了问题 · 2020-07-03

    用 typescript 写的例子,interface 定义类型会报错,type 不会

    typescript 3.9.5

    interface A {
      [name: string]: string
    }
    
    interface B {
      b: string
    }
    
    const b: B = { b: 'b' }
    
    const a: A = b // 不能将类型“B”分配给类型“A”。类型“B”中缺少索引签名。ts(2322)

    如代码所示,ts 检查报错了。将 interface B 改成 type B 就不报错了,两者用法有什么区别?

    关注 2 回答 0

    siwuxie 赞了回答 · 2020-06-03

    解决通过HTML的META设置expires和cache-control 控制缓存真的能生效吗?

    这个是 IE 时代的私有属性,在 IE9 以前支持的,而现在主流的 Chrome / Firefox / Safari,包括 IE9 ~ IE11 都不支持。

    这个东西是 HTTP/1.0 时代的产物,因为 HTTP/1.0 里关于缓存的可设定太少了,而 HTTP/1.1 刚出来的时候还不是所有浏览器都支持,所以把这个玩意儿放到了 HTML 页面里了。如果你的页面需要兼容 IE 低版本,那么可以加上。

    现在上哪找还只支持 HTTP/1.0 的 Web 服务器去啊,全都是 HTTP/1.1 了。而且随着 HTTPS 的普及,估计很快就该全面 HTTP/2 了。


    P.S.1 http-equiv 倒确实在 HTML 规范中有几个值可以设( content-security-policycontent-typedefault-stylex-ua-compatiblerefresh),但都跟缓存无关。


    P.S.2 中文网络的技术博客就是互相抄。

    关注 1 回答 1

    siwuxie 提出了问题 · 2020-06-03

    解决通过HTML的META设置expires和cache-control 控制缓存真的能生效吗?

    网上有很多帖子教通过HTML的META设置expires和cache-control设置缓存。

    <meta http-equiv="Cache-Control" content="max-age=7200" />
    <meta http-equiv="Expires" content="Sun Oct 15 2018 20:39:53 GMT+0800 (CST)" />

    但是,验证后并没有效果。这个方案真的有效吗?或者具体哪些场景下有效?

    关注 1 回答 1

    siwuxie 发布了文章 · 2020-06-03

    Caching in HTTP 笔记

    文章

    Caching in HTTP

    HTTP 缓存的作用

    HTTP is typically used for distributed information systems, where performance can be improved by the use of response caches.

    响应头未配置缓存的情况

    以下情况,浏览器不会对请求进行缓存

    没有配置 cache-control 或 last-modified、expires

    image.png

    只配置了 cache-control,且值为 public 或 private

    image.png

    只配置 last-modified 的情况

    • 未配置 expires
    • cache-control

      • 未配置
      • 或配置,值为 public 或 private

    将采用 Heuristic Expiration

    Since origin servers do not always provide explicit expiration times, HTTP caches typically assign heuristic expiration times, employing algorithms that use other header values (such as the Last-Modified time) to estimate a plausible expiration time. The HTTP/1.1 specification does not provide specific algorithms, but does impose worst-case constraints on their results. Since heuristic expiration times might compromise semantic transparency, they ought to used cautiously, and we encourage origin servers to provide explicit expiration times as much as possible.

    其他情况

    网上有很多介绍

    通过HTML的META设置expires和cache-control 控制缓存真的能生效吗?

    这是IE的私有属性

    有什么遗漏?

    • service work
    查看原文

    赞 0 收藏 0 评论 0

    认证与成就

    • 获得 75 次点赞
    • 获得 14 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 12 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    注册于 2015-11-07
    个人主页被 3.2k 人浏览