前端进阶
前端工程化
前端工程化的主要目标就是解放生产力、提高生产效率。通过制定一系列的规范,借助工具和框架解决前端开发以及前后端协作过程中的痛点和难度问题。
1、js 代码压缩 minify 的原理是什么?
我们知道
javascript代码经压缩 (uglify) 后,可以使体积变得更小,那它代码压缩的原理是什么。如果你来做这么一个功能的话,你会怎么去压缩一段
js代码的体积
1、通过 AST 分析,根据选项配置一些策略,来生成一颗更小体积的 AST 并生成代码。
常见用以压缩 AST 的几种方案如下:
-
去除多余字符: 空格,换行及注释
-
压缩变量名:变量名,函数名及属性名
-
解析程序逻辑:合并声明以及布尔值简化
// 压缩前const a = 3;const b = 4;// 压缩后const a = 3, b = 4; -
解析程序逻辑: 编译预计算
// 压缩前const ONE_YEAR = 365 * 24 * 60 * 60// 压缩后const ONE_YAAR = 31536000
2、package-lock.json 有什么作用,如果项目中没有它会怎么样
packagelock.json/yarn.lock用以锁定版本号,保证开发环境与生产环境的一致性,避免出现不兼容 API 导致生产环境报错
3、git hooks 原理是什么
git hooks: Git 能在特定的重要动作发生时触发自定义脚本
2、在开发员提交之前执行检查脚本
3、但是需要把脚本放在本地的 .git/hooks 目录下,不方便在团队内推广
- 因此就出现了husky
4、这样可以避免不符合规范的代码污染远程仓库
以一次commit为例,会先后触发 pre-commit、prepare-commit-msg、commit-msg和 post-commit等hooks。我们可以利用这些hooks做一些有趣的事。比如:我们可以利用 pre-commit进行代码校验,利用 commit-msg进行commit message的校验
4、如何使用 docker 部署前端
使用 docker 部署前端最大的好处是隔离环境,单独管理:
- 前端项目依赖于 Node v16,而宿主机无法满足依赖,使用容器满足需求
- 前端项目依赖于 npm v8,而宿主机无法满足依赖,使用容器满足需求
- 前端项目需要将 8080 端口暴露出来,而容易与宿主机其它服务冲突,使用容器与服务发现满足需求
5、什么是 AST,及其应用
AST(Abstract Syntax Tree)是源代码的抽象语法结构树状表现形式,Webpack、ESLint、JSX、TypeScript 的编译和模块化规则之间的转化都是通过 AST 来实现对代码的检查、分析以及编译等操作。
- 无论什么语言 【java、python】,都有一个共同的部分,将源代码作为纯文本解析成为抽象语法树AST的数据结构
- javascript世界中,你可以认为抽象语法树(AST)是最底层
- 因此了解 js 编译执行的原理,那么你就得了解 AST
AST应用
- Babel中ES6转化成ES5
- vue模板编译
- css预处理
- webpack插件编写
- UglifyJS来压缩js代码
- eslint检查 【将代码解析成抽象语法树,进行分析代码】
AST 如何生成
- js执行的第一步就是读取js文件中的字符流,然后通过词法分析器生成token,之后再通过语法分析生成AST, 最后生成机器码执行
实现词法分析器
- 词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。
6、browserslist 的意义
browserslist 是可以在不同的前端工具之间共用目标浏览器和 node 版本的配置工具。 相当于给 Babel、PostCSS、ESLint、StyleLint 等这些前端工具预设一个浏览器支持范围,这些工具转换或检查代码时会参考这个范围
"browserslist": [
"last 1 version", //每个浏览器支持的最新1个版本
"> 1%", // 兼容全球浏览器使用数量占比1%以上的类型
"maintained node versions", // 兼容所有仍被支持的NODE版本
"not dead" // 一年内官方维护更新的版本
]
browserslist 的原理:
- browserslist根据正则解析查询语句,对浏览器版本数据库进行查询,返回所得的浏览器版本列表。
7、开发组件库问题
1、按需引入 Tree Shaking问题:
- 如何让自己的组件库支持按需引入?
- 1、组件库就必须时基于ESM模块编写的
- 2、package.json的入口文件。增加module字段
- main是cjs模块的入口,module就是esm的入口了,一般的组件库都会配置这两个入口,同时支持cjs和esm两种模块
- 决定具体用哪一种模块,其实是和你的源代码使用哪种模块绑定的
- 如果使用esm语法,即import方式导入,就会去加载module入口文件
- Babel会导致Webpack的Tree-shaking失效吗?
- 是否成功取决于你的 babel 版本,简单的说,如果你使用的是 babel 6,那 Tree Shaking 不生效,如果使用了 Babel 7 及其以上版本,Tree Shaking 是生效的
- package.json的sideEffects要配置吗?
- 默认可以不写sideEffects,如果有需要某些文件不做tree shaking,可以加上
- tree-shaking失效的原因?
- 不要使用mode: development,tree-shaking是用于生产环境。
- webpack4.x的optimization的minimize不能设置为false,会导致失效
8、静态资源如何处理?
1、上传到oss的好处 【阿里云oss对象存储】
- OSS会自动帮你压缩 【使得你的页面加载速度极快】
- 成本低廉
- 减少带宽问题,提高用户体验
2、上传到CDN【内容分发网络】
- CDN主要是用于站点的加速,提高网站中的静态资源的访问性能,比如图片、视频、音频等
- 使用cdn加速oss
OSS和CDN有什么区别呢?使用场景是什么?
- OSS 是做存储的,CDN 是做内容加速的
- 打比方:OSS 是硬盘,用来存数据,那么 CDN 就是宽带,用来传输数据
- 一般二者是配合使用的,对象存储oss里面存的就是一些图片、视频、文件等等,都是静态数据,可以给用CDN做加速
OSS 和 CDN 能否相互替代?
- 不能
OSS和CDN如何配合使用?
- OSS可作为CDN的源站,通过CDN加速内容分发,用户通过CDN节点就近获得文件资源
- 可降低服务器负载
1、在CDN的控制台上,添加OSS的域名
2、获取CDNME, 配置CDNME
- CDNME:域名的别名,用于将域名解析到另一个域名
2、在OSS控制台上,打开加速域名的CDN缓存自动刷新开关
npm/yarn/pnpm架构
npm
package.json
-
private
- 如果你在你的package.json中设置了“private”:true,那么npm将拒绝发布它。 这是防止私人存储库意外发布的一种方法。【防止意外发布】
-
workspaces: yarm提供的,可共享node_mudules
-
browserslist: 指定了项目的目标浏览器的范围, 这个值会被 @babel/preset-env 和 Autoprefixer 用来确定需要转译的 JavaScript 特性和需要添加的 CSS 浏览器前缀。 【可以提供统一的配置。即共享项目中的目标浏览器环境信息】
- 参考vue-cli的browserslist配置
"browserslist": ["> 1%", //支持市场份额大于 1% 的浏览器。"last 2 versions", //支持各类浏览器最近的2个版本,当然这里的 2 是可变的数字"not dead" //not 是逻辑非操作符,即对 dead取反,而浏览器被认为是 dead 条件是:最新的两个版本中发现其市场份额已经低于 0.5% 并且 24 个月内没有任务官方支持和更新了] -
resolutions : yarn提供的resolutions统一依赖版本字段
-
license : 译为许可证【开源协议】。理解为协议或版权声明,违反协议的后果就是商业纠纷
- 参考阮一峰:http://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html
- 图里少了ISC, ISC和BSD差不多
- ISC: 人们可以对您的代码进行任何操作,并带有适当的署名且不提供担保。 ISC许可证在功能上等同于 BSD 2条款和 MIT 许可
- MIT:简单来说,他人允许修改源码且可以闭源,每修改过的文件,可以不用放置版权说明,广告之类的,可以使用作者名字促销
- 参考阮一峰:http://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html
lock文件
目前常见的两种lock文件:
packahe-lock.json是npm5之后默认生成的锁文件yarn.lock是yarn的锁文件
作用:
- 1、锁定安装时的包的版本号,并且需要上传到git,以保证其他人在yarn install/npm install时大家的依赖能保证一致
- 2、会避免由于开发人员意外更改或者更新版本导致错误
npm 缓存机制
对于一个依赖包的同一版本进行本地化缓存,是当代依赖包管理工具的一个常见设计。使用时要先执行以下命令:
- 查看缓存路径
npm config get cache
这些缓存如何被储存并被利用的呢?
这就和 npm install 机制联系在了一起。当 npm install 执行时,通过pacote把相应的包解压在对应的 node_modules 下面。npm 在下载依赖时,先下载到缓存当中,再解压到项目 node_modules 下。pacote 依赖npm-registry-fetch来下载包,npm-registry-fetch 可以通过设置 cache 属性,在给定的路径下根据IETF RFC 7234生成缓存数据。
- 在每次安装包的时候,根据 package-lock.json 中存储的 integrity、version、name 信息生成一个唯一的 key,这个 key 能够对应到目录下的缓存记录,如果发现有缓存资源,就会找到 tar 包的 hash,根据 hash 再去找缓存的 tar 包,并再次通过pacote把对应的二进制文件解压到相应的项目 node_modules 下面,省去了网络下载资源的开销。
npm install理解
如何加速 npm install
npm publish理解
发布文件配置
发布哪些文件?
- 发布一个包,考虑到别人的下载速度,包体积肯定需要尽量小,所以源文件最好不包括,那如何控制只发哪些文件呢?
files 属性用于描述你 npm publish 后推送到 npm 服务器的文件列表,如果指定文件夹,则文件夹内的所有内容都会包含进来。我们可以看到下载后的包是下面的目录结构:
另外,你还可以通过配置一个
.npmignore文件来排除一些文件, 防止大量的垃圾文件推送到npm, 规则上和你用的.gitignore是一样的。.gitignore文件也可以充当.npmignore文件。
- 一般发布是dist、lib 打包编译后的文件
{
"files": [
"bin",
"dist",
"lib"
]
}
发布版本
npm的发包需要遵循语义化版本,一个版本号包含三个部分: patch、minor、major
在修改 npm 包某些功能后通常需要发布一个新的版本,我们通常的做法是直接去修改 package.json 到指定版本。如果操作失误,很容易造成版本号混乱,我们可以借助符合 Semver 规范的命令来完成这一操作:
npm version patch: 升级修订版本号npm version minor: 升级次版本号npm version major: 升级主版本号
依赖类型理解
npm 设计了以下几种依赖类型声明:
dependencies 项目依赖
devDependencies 开发依赖
peerDependencies 同版本依赖
bundledDependencies 捆绑依赖
optionalDependencies 可选依赖
dependencies
dependencies 表示项目依赖(生产环境依赖),这些依赖都会成为线上生产环境中的代码组成部分。当npm 包被下载时【npm install】,这个包的dependencies 下的模块也会作为依赖,一起被下载。
devDependencies
表示开发依赖,不会被自动下载,因为 devDependencies 一般只在开发阶段起作用或只是在开发环境中需要用到。比如 Webpack,预处理器 babel-loader、scss-loader,测试工具 E2E、Chai 等,这些都是辅助开发的工具包,无须在生产环境使用。
注意:如果开发工具如脚手架命令行,webpack、babel、less等工具需要安装在生产环境,当用户下载npm install cli -g的时候(一般是安装在全局环境下,如果局部安装的话,需要安装在项目的开发环境中),也会把依赖下载到node_modules,如果安装在开发环境,使用这个包的时候如webpack、commander的时候会报错,显示找不到这个包
- 因为我们发布这个脚手架的时候,一般是发布打包编译后的代码 dist、lib,要把webpack、commamer等依赖也要打包进去做工具依赖使用
注意:
- typesscript和@type 类型文件不会被打包在最终的bundle中,所以安装在开发环境和生产环境都一样
peerDependencies
简单来说:peerDependencies就是用来防止多次引入相同的库,peerDependencies去声明一下需要依赖的插件和版本,如果出问题 npm 就会有警告来提醒使用者去解决版本冲突问题。
peer意思是同辈、同级,即peerDependencies理解为同级依赖
指定当前组件的依赖以其版本。如果组件使用者在项目中安装了其他版本的同一依赖,会提示报错。
- peerDependencies 表示同版本依赖,简单来说就是:如果你安装我,那么你最好也安装我对应的依赖。
peerDependencies 主要的使用场景。这类场景有以下特点:
- 插件不能单独运行
- 插件正确运行的前提是核心依赖库必须先下载安装
- 我们不希望核心依赖库被重复下载
- 插件 API 的设计必须要符合核心依赖库的插件编写规范
- 在项目中,同一插件体系下,核心依赖库版本最好相同
npm版本控制
语义版本控制的概念很简单:所有的版本都有 3 个数字:x.y.z。
- 第一个数字是主版本。
- 第二个数字是次版本。
- 第三个数字是修复版本。
~: 如果写入的是 〜1.13.0,则当运行 npm update 时,会更新到修复版本:即 1.13.1 可以,但 1.14.0 不可以
^: 如果写入的是 〜1.13.0,则当运行 npm update 时,会更新到次版本:即 1.x.x, 不能 2.x.x
npm发布包 废弃/删除
npm包发布后可以对包进行废弃或删除操作,废弃和删除的区别在于:
- 废弃不会将包或版本从npm仓库删除,仍然可以继续下载安装,并在安装的时候会有警示
- 删除会将包从npm彻底删除,无法被下载安装
无论是废弃还是删除,都包含两个层面:
- 版本的废弃/删除
- 包的废弃/删除
一、废弃
废弃原因:
- 版本:鼓励用户更新最新版本
- 包:此包内容已经过时,没有了维护的价值
二、删除
npm不鼓励任何形式的删除,主要因为我们发布的包可能已经被其他人引用,如果我们删除了此包,其他人在重新安装含有我们包的依赖的工程时,出现找不到包问题。
基于此,npm做了相关的删除限制:
删除的版本24小时后方可重发!只有发布72小时之内的包可以删除!
开发库主要注意的地方
- 生产环境依赖的问题:
- 如果当前库依赖这个包的版本,需要放在peerDependencies,因为业务方很可能也会使用安装这个包,会导致重复打包
- 使用webpack的话。可在 webpack 中加入 external 排除掉
yarn
Yarn Workspace
- yarn workspace是yarn的内置功能
- yarn workspace可以共享
node_modules - 在package.json设置workspaces字段
"workspaces": [
"packages/*"
]
pnpm
-
支持monorepo
- 替代lerna
-
速度快
-
节省磁盘空间
-
1、如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新 版本,并且新版本中只有一个文件有修改,则
pnpm update只需要添加一个 新文件到存储中,而不会因为一个文件的修改而保存依赖包的 所有文件 -
2、所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用 额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的 依赖包。
-
-
可以替代npm、yarn
pnpm 为何节省空间?
-
1、它解决了 npm/yarn 平铺 node_modules 带来的依赖项重复的问题 (doppelgangers)
-
2、所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用 额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的 依赖包。
webpack/rollup/ESbuild/Vite架构
webpack
原理
- 1、解析入口文件,获取AST抽象语法树
- 2、找出所有依赖模块
- 3、AST转化编码code
- 4、递归解析所有依赖项、生成依赖关系图
- 5、输出bundle
优化编译、性能优化
1、dll分包
-
DLLPlugin 是 webpack 官方提供的一个插件,也是用来分离代码的
-
dll是针对项目的性能优化的
-
使用 DLLPlugin 在打包的时候生成一个 manifest 文件,通过dll分包, webpack 会根据这个 manifest 文件的信息,分析出哪些模块无需打包
webpack.dll.config.js:
module.exports = {
name: 'vendor',
entry: ['lodash','axios','react','react-dom','mobx'], // 作为公共类库
output: {
path: path.resolve(__dirname, "dist/dll"),
filename: "vendor.js",
library: "vendor_[hash]" // 打包后对外暴露的类库名称
},
plugins: [
new webpack.DllPlugin({
name: 'vendor_[hash]',
path: path.resolve(__dirname, "dist/manifest.json"), // 使用 DLLPlugin 在打包的时候生成一个 manifest 文件
})
],
}
你会发现构建结果的应用代码中不包含 lodash,react,react-dom等公共类库 的代码内容,这一部分代码内容会放在 vendor.js 这个文件中,而你的应用要正常使用的话,需要在 HTML 文件中按顺序引用这两个代码文件,如:
<script src="vendor.js"></script>
<script src="main.js"></script>
在构建的时候,我们需要优先使用 webpack.dll.config.js 来打包,如 webpack -c webpack.dll.config.js --mode production,构建后生成公共代码模块的文件 vendor.js 和 manifest.json,然后再进行应用代码的构建
DLLPlugin 构建出来的内容无需每次都重新构建,后续应用代码部分变更时,你不用再执行配置为 webpack.dll.config.js 这一部分的构建,沿用原本的构建结果即可,所以相比 optimization.splitChunks,使用 DLLPlugin 时,构建速度是会有显著提高的
注意:
- 要使用dll分包,需要优先使用
webpack.dll.config.js来打包生成对应的manifest.json和vendor.js - DLLPlugin 构建出来的内容无需每次都重新构建
- 当你升级 lodash(即你的公共部分代码的内容变更)时,要重新去执行
webpack.dll.config.js这一部分的构建,不然沿用的依旧是旧的构建结果
2、如何代码分割/代码拆分,以及如何优化
为什么CSS 文件分离出来,而不是直接一起打包在 JS 中?
- 主要的原因是我们希望更好地利用缓存
1、假设静态资源都打包成一个js文件,加载页面时虽然只需要加载一个 JS 文件,但是我们的代码一旦改变了,用户访问新的页面时就需要重新加载一个新的 JS 文件。有些情况下,我们只是单独修改了样式,这样也要重新加载整个应用的 JS 文件,相当不划算
2、还有一种情况是我们有多个页面,它们都可以共用一部分样式(这是很常见的,CSS Reset、基础组件样式等基本都是跨页面通用),如果每个页面都单独打包一个 JS 文件,那么每次访问页面都会重复加载原本可以共享的那些 CSS 代码
如果分离开来,第二个页面就有了 CSS 文件的缓存,访问速度自然会加快。虽然对第一个页面来说多了一个请求,但是对随后的页面来说,缓存带来的速度提升相对更加可观
-
optimization.splitChunks
- 公共引用的模块(库)帮单独提出来做下代码分割
- 自动生成了一个
verdor~main.js文件
optimization: {splitChunks: {chunks: "all" //所有的 chunks 代码公共的部分分离出来成为一个单独的文件}}
我们需要在 HTML 中引用两个构建出来的 JS 文件,并且 commons.js 需要在入口代码之前。下面是个简单的例子:
<script src="commons.js" charset="utf-8"></script>
<script src="entry.bundle.js" charset="utf-8"></script>
使用了 html-webpack-plugin,那么对应需要的 JS 文件都会在 HTML 文件中正确引用
将公共使用的第三方类库配置为公共的部分
不应该由 webpack 自己去判断处理。因为公共的第三方类库通常升级频率相对低一些,这样可以避免因公共 chunk 的频繁变更而导致缓存失效
module.exports = {
entry: {
vendor: ["react", "lodash", "vue", ...], // 指定公共使用的第三方类库
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: "initial",
test: "vendor",
name: "vendor", // 使用 vendor 入口作为公共部分
enforce: true,
},
},
},
},
// ... 其他配置
}
// 或者
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /react|angluar|lodash/, // 直接使用 test 来做路径匹配
chunks: "initial",
name: "vendor",
enforce: true,
},
},
},
},
}
// 或者
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: "initial",
test: path.resolve(__dirname, "node_modules") // 路径在 node_modules 目录下的都作为公共部分
name: "vendor", // 使用 vendor 入口作为公共部分
enforce: true,
},
},
},
},
}
上述第一种做法是显示指定哪些类库作为公共部分,第二种做法实现的功能差不多,只是利用了 test 来做模块路径的匹配,第三种做法是把所有在 node_modules 下的模块,即作为依赖安装的,都作为公共部分。你可以针对项目情况,选择最合适的做法。
理解chunks
chunks 用以告诉 splitChunks 的作用对象,其可选值有 async、 initial 和 all。默认值是 async,也就是默认只选取异步加载的chunk进行代码拆分
chunks值为all时,会比较强大,一般设置为all即可,处理范围包括了初始chunk和异步chunk两种场景
架构问题
多进程打包有用过吗?
- webpack5内置terser-webpack-plugin,默认是启动多进程并行的,可以多进程执行代码压缩、提高构建速度,
- thread-loader : 多进程方式运行资源加载
知道sourceMap是什么吗?
sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率
如何使用 Webpack 持久化缓存大幅提升构建性能?
Webpack 都有哪些实现并行构建的方法?
插件开发进阶:如何提升插件健壮性?
插件架构:Hook 体系是如何影响 Webpack 架构的?
HMR:如何动态替换页面代码
Dependency Graph:如何管理模块间依赖?
谈谈Tree-Shaking 实现原理
- webpack2.x开始⽀支持 tree shaking概念,顾名思义,"摇树"
- 通常用于移除 JavaScript 上下文中的未引用代码(dead-code 无生命的代码)
- 摇树 可以理解为 把没有用到的代码给移除下去,消除无用代码,不进行打包,
- 只支持ES module的引入方式!!!!
原理
-
treeShaking依赖ES6的静态语法 【ES module】
-
ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块- import命令会被js引擎静态分析
-
基于 ESM 的模块导入导出规则,可以实现静态编译时处理模块间的依赖
-
静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码
如何使用tree-shaking?
1).确保代码是es6格式,即 export,import
2).package.json中,设置sideEffects
3).确保tree-shaking的函数没有副作用
4).babelrc中设置presets [["env", { "modules": false }]] 禁止转换模块,交由webpack进行模块化处理
webpack如何实现持久化缓存?
- 保证 hash 值的唯一性,即为每个打包后的资源生成一个独一无二的 hash 值,只要打包内容不一致,那么 hash 值就不一致。
- 保证 hash 值的稳定性,我们需要做到修改某个模块的时候,只有受影响的打包后文件 hash 值改变,与该模块无关的打包文件 hash 值不变。
为什么需要做持久化缓存
- 用户使用浏览器第一次访问我们的站点时,该页面引入了各式各样的静态资源,如果我们能做到持久化缓存的话,可以在 http 响应头加上 Cache-control 或 Expires 字段来设置缓存,浏览器可以将这些资源一一缓存到本地。
- 用户在后续访问的时候,如果需要再次请求同样的静态资源,且静态资源没有过期,那么浏览器可以直接走本地缓存而不用再通过网络请求资源。
遇到问题总结
1、如何解决webpack重复打包同名依赖包?
- 可用webpack配置的spiltchunk、SplitChunksPlugin、进行代码分离、代码拆分、分包,避免他们之间的重复依赖
- external 配置
external配置
- 防止将某些
import的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)- 比如可以从CDN去引入包,而不用打包
SplitChunks
- SplitChunksPlugin插件作用:简单的来说就是Webpack中一个提取或分离代码的插件,主要作用是提取公共代码,防止代码被重复打包、拆分过大的js文件,合并零散的js文件
2、不建议线上发布使用 DllPlugin 插件:
DllPlugin 本身有几个缺点:
- 首先你需要额外多配置一份 webpack 配置,增加额外工作量。
- 其中一个页面用到了一个体积很大的第三方依赖库而其它页面根本不需要用到,但若直接将它打包在 dll.js 里很不值得,每次页面打开都要去加载这段无用的代码,无法使用到 webpack2 的 Code Splitting 功能。
- 第一次打开的时候需要下载 dll 文件,因为你把很多库全部打在一起了,导致 dll 文件很大,首次进入页面加载速度很慢。
虽然你可以打包成 dll 文件,然后让浏览器去读取缓存,这样下次就不用再去请求,比如你用 lodash 其中一个函数,而你用dll会将整个 lodash 文件打进去,这就会导致你加载无用代码过多,不利于首屏渲染时间。
我认为的正确的姿势是:
- 像 React、Vue 这样整体性偏强的库,可以生成 vendor 第三方库来去做缓存,因为你一般技术体系是固定的,一个站点里面基本上都会用到统一技术体系,所以生成 vendor 库用于缓存。
- 像 antd、lodash 这种功能性组件库,可以通过 tree shaking 来进行消除,只保留有用的代码,千万不要直接打到 vendor 第三方库里,不然你将大量执行无用的代码。
vendor库的值是依赖的第三方库,不会经常变更的,如react、vue
3、external问题
- 指出应将哪些模块视为外部模块,外部模块不会被打包
- 防止将某些
import的包(package)打包到 bundle 中
- 防止将某些
external接受一个模块名称的数组或一个接受模块名称的函数,如果它被视为外部引用(externals)则返回 true
用法:
- 在index.html引入的脚本,如从CDN引入,或者已经打包好的js,这不需要重新打包
- 比如在inxex.html引入了lodash,external配置了lodash外部模块
- 则lodash不会被打包,可以在组件通过import引入lodash模块
rollup
1、js模块打包器
2、Rollup就是专门针对类库进行打包,它的优点是小巧而专注
3、Rollup打包出来的体积都比webpack略小一些
webpack和rollup对比:
Webpack对于代码分割和静态资源导入有着“先天优势”,并且支持热模块替换(HMR),而Rollup并不支持,所以当项目需要用到以上,则可以考虑选择Webpack
ESbuild
- Vite,snowpack使用了esbuild
Vite和snowpack底层都是用了esbuild。从Vite 的README上可以发现esbuild的执行速度非常快,typescript转义成javascript要比官方的tsc快20-30倍
ESbuild为什么这么快?
- js是单线程串行,esbuild是新开一个进程,然后多线程并行,充分发挥多核优势
- go是纯机器码,肯定要比JIT快
- 不使用 AST,优化了构建流程
ESbuild用来干嘛? 【功能、作用】
-
利用 esbuild 编译代码
- esbuild 提供了 writeFileSync/writeFile 对 code 进行编译
-
利用 esbuild 压缩代码体积
- esbuild 提供了一个 minify 配置允许用户去压缩代码体积
-
利用 esbuild 处理 jsx 代码
-
处理其他资源 【esbuild内置了一些文件处理的loader】
ESbuild 的缺点
- ESbuild不使用AST(抽象语法树)因此带来的一些缺点
babel的插件是通过AST来完成的,而esbuild没有使用ast,所以项目使用babel插件的话,是没有很好的迁移方案
为了保证 esbuild 的编译效率,esbuild 没有提供 AST 的操作能力。所以一些通过 AST 处理代码的 babel-plugin 没有很好的方法过渡到 esbuild 中(说的就是你 babel-plugin-import),如果你的项目使用了 babel-plugin-import, 或者一些自定义的 babel-plugin 。在目前来看是没有很好的迁移方案的
Vite
Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。还支持了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 Rollup 打包
- Vite 基于 ESM,因此实现了快速启动和即时模块热更新能力;
它主要具有以下特点:
- 快速的冷启动
- 即时的模块热更新
- 真正的按需编译 【Vite 在服务端实现了按需编译。】
- 基于ESM构建有vite、snowpack
- 简单来说:Vite 在开发环境下并没有打包和构建过程
基于浏览器原生 ES imports ,利用浏览器去解析,服务器去根据需要按需编译返回给浏览器
为什么 ESM 是前端模块化的未来?
vite的快体现在哪里?
- Vite 的快,主要体现在两个方面: 快速的冷启动和快速的热更新
快速的冷启动:
- Vite 之所以快,借助了浏览器对 ESM规范的支持
- vite不需要做
bundle操作,即不需要构建 - 模块之间的依赖关系的解析由浏览器实现
- 文件的转换由
dev server的middlewares实现并做缓存;
快速的热更新:
- 由于
Vite采用unbundle机制,所以dev server在监听到文件发生变化以后,只需要通过ws连接通知浏览器去重新加载变化的文件,剩下的工作就交给浏览器去做
vite的缺点有吗,说说什么方面?
- 首屏性能 【首次启动加载慢】
- 不对源文件做合并捆绑操作,导致大量的
http请求; dev server运行期间对源文件做resolve、load、transform、parse操作;- 预构建、二次预构建操作也会阻塞首屏请求,直到预构建完成为止;
- 不对源文件做合并捆绑操作,导致大量的
不过首屏性能差只发生在
dev server启动以后第一次加载页面时发生。之后再reload页面时,首屏性能会好很多。原因是dev server会将之前已经完成转换的内容缓存起来
- 懒加载性能
- 和首屏一样,由于
unbundle机制,动态加载的文件,需要做resolve、load、transform、parse操作,并且还有大量的http请求,导致懒加载性能也受到影响
- 和首屏一样,由于
如何优化?
- 可以通过
prefetch、持久化缓存等手段做优化
如何在 Vite 中处理各种静态资源?
Vite 的核心编译是怎么样的?
vite为什么需要依赖预构建?
-
【CommonJS 和 UMD 兼容性】:vite会将所有的代码都视为ESM模块,所以会把CommonJS 或 UMD 模块会统一转换ESM模块
-
【性能(减少模块间依赖引用导致过多的请求次数)】:Vite 会将有一些内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。
【模块联邦】如何实现优雅的跨应用代码共享?
【代码分割】打包完产物体积太大,怎么拆包?
react架构
1、React15架构/React16架构
React15架构可以分为两层:
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
React16架构可以分为三层:
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
相较于React15,React16中新增了Scheduler(调度器)
2、谈谈Fiber架构
fiber之前是怎么样的?
-
React会递归对比虚拟DOM,找出变动的节点,然后同步更新这些节点
-
这个过程中,React会一直占用着浏览器资源,
- 1、会导致用户触发事件得不到响应
- 2、导致掉帧,用户可能会感觉到卡顿
-
遍历递归过程中,执行栈会越来越深,就会很卡,而且不能中断,因为中断了,恢复就很难了
-
递归太消耗性能
-
为什么需要fiber?
-
为了使react渲染的过程可以被中断 【fiber可以打断diff算法】
- 可以将控制权交给浏览器
- 合理分配CPU资源,从而提高用户的响应速度
-
fiber可以优先处理优先级高的任务
-
对于计算量比较大的计算或者DOM的计算渲染,就不会显得卡顿
什么是fiber?
React Fiber是React核心算法的重新实现
最主要特点就是:使 Virtual DOM 可以进行增量式渲染,可以把一个大的渲染任务分为很多小阶段,按照优先级来执行这些任务分片,优先级高的会先执行
-
fiber是一个js是对象
-
React Fiber是React核心算法的重新实现
-
主要特点:使 Virtual DOM 可以进行增量式渲染,能够将渲染工作分割成多块,并将其分散到多个帧上
-
fiber也就是时间切片,合理分配CPU资源,从而提高用户的响应速度
-
时间切片:将长任务分割为一个个执行时间很短的任务
-
React Fiber把更新过程碎片化,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。
- fiber可以优先处理优先级高的任务
-
维护每一个分片的数据结构,就是Fiber
-
React Fiber会找出需要更新哪些DOM ,这个阶段【diif算法阶段】是可以被打断的
- fiber可以打断diff算法
-
React通过Fiber将树的遍历变成了链表的遍历
-
链表的优点: (链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据,另一个是存储指向下一个结点地址的指针)
- 插入(添加)和删除不需要移动其他元素,会节省时方便更新的时候暂停、终止,我们可以随时停止遍历并稍后恢复。
-
恢复的话:就是重新进入循环,不过需要在循环外保存上次退出时的状态
-
为什么时间切片不使用 generator?
主要是2个原因:
1、generator 必须将每个函数都包装在 Generator 堆栈中。这不仅增加了很多语法开销,而且还增加了现有实现中的运行时开销。虽然有胜于无,但是性能问题仍然存在。
2、最大的原因是生成器是有状态的。无法在其中途恢复。如果你要恢复递归现场,可能需要从头开始, 恢复到之前的调用栈。
如何判断当前是否有优先级高任务?
- 只能约定一个合理的执行时间,超过了这个执行时间,如果任务还没有执行,中断当前任务,将控制权交还给浏览器
requestIdleCallback:
利用requestIdleCallback的API可以让浏览器的空闲时段内执行回调排队
- 这个回调会传入一个参数,可以指定timeout超时时间,如果超过这个timeout时间还没有执行,那么就会把这个回调放入事件循环里去排队执行
- requestIdleCallback兼容性很差,react为了兼容,自己实现requestIdleCallback
如何判断浏览器有空闲时间?
- 需要知道浏览器在一帧内要做什么事情
js的执行、don构建、render布局,绘制
- 在一帧里浏览器做完本质工作之后,剩下时间就是有空闲的时间
如果浏览器一直没有空闲怎么办?
- 使用requestIdleCallback设置超时时间timeout, 超过超时时间,就把回调会放到事件循环里排队执行
React的五个优先级等级
- Immediate (立刻): 最高优先级,这个优先级需要马上执行,不能被中断
- UserBlocking(用户操作有关): 用户交互的结果,需要及时得到反馈给用户,第二优先级
- Normal :不需要用户立即就能感受到的变化,比如网络请求 (普通优先级)
- Low: 任务延后执行 ,但是需要执行(低优先级)
- Idle: 可以无限延后执行,就算不执行,也不影响
2、React的渲染原理
3、Hooks实现原理
-
hooks的实现原理 是 用了闭包 + 链表
-
React Hooks 是用链表来保存状态的
-
hooks 链表是保存在fiber节点上
- 链表的每个节点就是表示一个fiber
- fiber:是React核心算法的重新实现,fiber的出现让每个节点都有了实例,拥有了保存状态的能力
- fiber就是时间切片,合理分配CPU资源,从而提高用户的响应速度
4、React 在 setState 时发生了什么?
- 1、React 会为当前节点创建一个 updateQueue 的更新列队
- 2、然后会触发reconciler(协调器)—— 负责找出变化的组件
- 在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树
- 3、然后Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler,具体是执行 doWork 方法。
- 4、React 会执行一遍 updateQueue 中的方法,以获得新的节点
- 5、React 进入 commit 阶段
- 6、在 commit 阶段中,React 会根据前面为各个节点打的Tag,一次性更新整个 dom 元素
5、React调度器原理
- 1、react的调度是通过Scheduler包,是独立的包,独自承担起任务调度的作用
- 2、将任务和任务的优先级交给Scheduler调度,它就可以帮你管理任务,安排任务的执行
对于多个任务,它会先执行优先级高的。对于单个任务,它会有节制地去执行。换句话说,线程只有一个,它不会一直占用着线程去执行任务。而是执行一会,中断一下,如此往复。用这样的模式,来避免一直占用有限的资源执行耗时较长的任务,解决用户操作时页面卡顿的问题,实现更快的响应
- 3、任务优先级让任务按照自身的紧急程度排序,让优先级最高的任务最先被执行到
- 4、Scheduler调度有2个核心功能:任务队列管理、时间切片【可中断和恢复】
- 5、单个任务的中断以及恢复
- 如果某个任务执行时间过长,达到了时间片限制的时间,那么该任务必须中断,以便于让位给更重要的事情
- 执行时间分割, 让出主线程(把控制权归还浏览器, 浏览器可以处理用户输入, 浏览器绘制等紧急任务)
6、谈谈diff算法
6.1 什么diff算法
diff算法来源于:linux的基本命令
- 比如diff one.txt two.txt。结果是展示出两个文件的不同。
-
Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,对改变部分进行更新渲染,而不用重新渲染整个页面
-
根据真实dom结构生成=>虚拟dom【用对象的形式模拟dom结构】,进行比较
- 判断两个节点是否是同一个节点,如果是,那么就深入检查他们的子节点。如果两个节点不一样那就说明vnode完全被改变了,就可以直接替换oldVnode。
- 判断标签名是否相同,如果不同,直接删除,不做深度比较,如果标签名相同,key相同,就认为相同节点,不继续做深度比较
- 通过key可以精确找到相同节点,整个patch过程会非常高效
- 找到对应的真实dom,称为el,如果旧节点旧node有子节点而新Vnode没有,则删除el的子节点,如果oldVnode没有子节点而新Vnode有,则将新的子节点添加到el的。如果两者都有子节点,则执行updateChildren函数比较子节点
- 通过遍历比较
6.2 vdom为何用diff算法
- DOM操作是昂贵的,因此尽量减少DOM操作
- 因为DOM操作时损耗性能
- DOM的操作会触发浏览器的重绘和回流
- 找出本次DOM必须更新的节点来更新,其他的不更新
- 这个找出的过程,就是diff算法
6.3 diff算法的实现流程
DOM的diff就是一个函数,称为patch
diff核心2个patch函数:
- patch(container,vnode)
- 就是把vnode的节点信息,渲染到一个空容器中
- 初次渲染的时候,将vdom渲染成真正的DOM然后插入到容器里面
- patch(vnode,newVnode)
- 再次渲染的时候,将新的vnode和旧的vnode相对比,得到最小修改,然后之间差异更新到真正的DOM树上,
patch(container,vnode)
- 通过这个函数可以让VNode渲染成真正的DOM
function createElement(vnode) {
var tag = vnode.tag
var attrs = vnode.attrs || {}
var children = vnode.children || []
if (!tag) {
return null
}
// 创建真实的 DOM 元素
var elem = document.createElement(tag)
// 属性
var attrName
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
// 给 elem 添加属性
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 给 elem 添加子元素,如果还有子节点,则递归的生成子节点。
elem.appendChild(createElement(childVnode)) // 递归
}) // 返回真实的 DOM 元素
return elem
}
patch(vnode,newVnode)
- 这里我们只考虑vnode与newVnode如何对比的情况:
function updateChildren(vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
// 遍历现有的children
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index]
// 两者tag一样
if (childVnode.tag === newChildVnode.tag) {
// 深层次对比,递归
updateChildren(childVnode, newChildVnode)
} else {
// 两者tag不一样
replaceNode(childVnode, newChildVnode)
}
}
)}
7、谈谈虚拟DOM
vue架构
浏览器
浏览器是如何解析代码的?
-
解析HTML
- HTML的解析是逐行解析。
- 构建DOM树
-
解析css
- CSS选择器的读取顺序是从右向左
- 因此浏览器会按照从右向左的顺序去读取选择器
-
解析js
- 浏览器中有一个js解析器的工具,专门用来解析我们的js代码
- 解析器会找到js当中的所有变量、函数、参数等等
浏览器是如何解析JS代码和执行的
浏览器是多进程的
- 浏览器是多进程的
- 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
- 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程
浏览器都包含哪些进程? (列举主要进程)
1、Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载等
2、第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
3、GPU进程:最多一个,用于3D绘制等
4、浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为
- 页面渲染,脚本执行,事件处理等
5、定时器线程(setTimeout, setInterval)
6、事件线程(onclick,onchange,…)
7、JavaScript引擎线程,用于解析JavaScript代码
在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)
- 但是注意:有时候打开多个tab页后,可能有些进程被合并了,所以每一个Tab标签对应一个进程并不一定是绝对的
浏览器多进程的优势
多进程有如下优点:
- 避免某个页面如果奔溃了影响整个浏览器,体验会很差
- 避免第三方插件奔溃影响整个浏览器
- 多进程充分利用多核优势
- 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
浏览器工作原理
用户界面 - 包括地址栏、后退/前进按钮、书签目录等,也就是所看到的除了用来显示所请求页面的主窗口之外的其他部分
浏览器引擎 - 用来查询及操作渲染引擎的接口
渲染引擎 - 用来显示请求的内容,例如,如果请求内容为html,它负责解析html及css,并将解析后的结果显示出来。
网络 - 用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作。
用户界面后端(UI后端) - 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口。
JS解释器 - 用来解释执行JS代码。
数据存储 - 属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术
浏览器的渲染机制 【页面渲染html的过程】
构建DOM树(parse):渲染引擎解析HTML文档,首先将标签转换成DOM树中的DOM node
构建渲染树(construct):解析对应的CSS样式文件信息
布局渲染树 (reflow/layout):从根节点递归调用,计算每一个元素的大小、位
绘制渲染树(paint/repaint):遍历渲染树,使用UI后端层来绘制每个节点。
1、解析html构建dom树
2、解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
3、布局render树(Layout/reflow),负责各元素尺寸、位置的计算
4、绘制render树(paint),绘制页面像素信息
5、渲染树创建好了,浏览器就可以根据渲染树把页面绘制到屏幕上。
- 渲染好的东西绘制页面上面去显示给用户
浏览器遇到什么会停下来
阻塞发生的情况:
- 遇到script标签加载js的时候会加载js并且执行完毕才开始渲染
- 因为js会对DOM进行操作,所以js阻塞DOM树的构建
- 遇到js浏览器会停下来DOM构建,所以最好把js放在body底部
- 其中一个原因就是为了不阻塞浏览器DOM树的构建,这也是为什么放在body上面的JS常常取不到想要的DO M节点,因为这个时候DOM树的构建还没有完成就被阻塞了
- 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行
- 遇到alert会阻塞
- css也会阻塞
- css是由单独的下载线程异步下载的。
总结:
js会阻塞DOM的构建,一般js放在body底部
css加载不会阻塞DOM树的解析 css加载会阻塞渲染树(render树)的构建 css加载会阻塞后面js语句的执行
尽可能的提高css加载速度
- 使用CDN
- 对css进行压缩
- 减少http请求数
理解浏览器沙箱
- 沙箱:就是一个环境,黑盒,在一个虚拟的环境中模拟真实世界,不对外界进行干扰
沙箱,即sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响
- 举个简单的例子,其实我们的浏览器,Chrome 中的每一个标签页都是一个沙箱(sandbox)。渲染进程被沙箱(Sandbox)隔离,网页 web 代码内容必须通过 IPC 通道才能与浏览器内核进程通信,通信过程会进行安全的检查。沙箱设计的目的是为了让不可信的代码运行在一定的环境中,从而限制这些代码访问隔离区之外的资源
- IPC是Inter-Process Communication的缩写 ,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
JS实现沙箱两种方式
- 市面上现在流行两种沙箱模式,一种是使用iframe,还有一种是直接在页面上使用new Function + eval进行执行
为什么在一个浏览器中使用多个进程
每个进程内都有什么
在什么情况下浏览器会创建进程
浏览器安全控制
-
X-XSS-Protection
- 防止反射型XSS
-
Strict-Transport-Security
- 强制使用HTTPS通信
-
CSP: 使用 W3C 提出的
CSP (Content Security Policy,内容安全策略),定义域名白名单内容安全策略 (CSP, Content Security Policy) 是一个附加的安全层,用于帮助检测和缓解某些类型的攻 击,包括跨站脚本 (XSS) 和数据注入等攻击。 这些攻击可用于实现从数据窃取到网站破坏或作为恶意软 件分发版本等用途。
CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少XSS 攻击
- HTTP 响应头 Content-Security-Policy 允许站点管理者在指定的页面控制用户代理的资源。除了少数例外,这条政策将极大地指定服务源 以及脚本端点。这将帮助防止跨站脚本攻击(Cross-Site Script) (XSS).
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
浏览器内核的理解
主要分成两部分:渲染引擎(layout engineer或Rendering Engine)和JS引擎。 渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入CSS等),以及计算网页的显示方式,然后会输出至显示器或打印机。
浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。
JS引擎则:解析和执行javascript来实现网页的动态效果。 最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎。
谈谈你对重绘和回流的理解。
回流也叫重排: 简单来说,就是当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。- 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,元素的位置信息不需要更新. 浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。
这也是为什么操作DOM是昂贵的,因为会触发浏览器的渲染 【重绘和回流】,更加消耗性能
触发回流
1、一个 DOM 元素的几何尺寸变化,常见的几何属性有 width、height、padding、margin、left、top、border 等等, 这个很好理解。
2、使 DOM 节点发生 增减或者 移动。
3、读写 offset族、scroll族和 client族属性的时候,浏览器为了获取这些值,需要进行回流操作。
4、调用 window.getComputedStyle 方法。
- 包含的任何基本计算后元素的所有CSS属性的值
知道了重绘和回流,在开发中有什么意义呢?
由此我们可以看出, 重绘不一定导致回流,回流一定会导致重绘, 但这两个说到底都是吃性能的 ,我们在开发中,要从代码层面出发,尽可能把回流和重绘的次数最小化。
当一个DOM元素的几何属性发生变化时,所有和它相关的节点(比如父子节点、兄弟节点等)的几何属性都需要进行重新计算,它会带来巨大的计算量。
当你要用到像这样的属性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 时,你就要注意了!
- (获取元素布局宽度/高度,获取元素相对视口的位置,获取屏幕宽度/高度)
- 因为获取这些值,浏览器是要通过即时计算的,因此会触发回流
- 除此之外,当我们调用了 getComputedStyle 方法,或者 IE 里的 currentStyle 时,也会触发回流。原理是一样的
-
避免频繁使用 style,而是采用修改
class的方式。- 因为每次使用style修改样式,都会去触发一次渲染树的更改,从而导致相应的回流与重绘过程
- 而我们把所有样式操作合并在一个class中,等于我们将所有的更改一次性发出,只触发一次渲染树
-
使用
createDocumentFragment进行批量的 DOM 操作。 -
对于 resize、scroll 等进行防抖/节流处理。
-
添加 will-change: tranform ,让渲染引擎为其单独实现一个图层,当这些变换发生时,仅仅只是利用合成线程去处理这些变换,而不牵扯到主线程,大大提高渲染效率。当然这个变化不限于
tranform, 任何可以实现合成效果的 CSS 属性都能用will-change来声明。这里有一个实际的例子,一行will-change: tranform拯救一个项目,点击直达 -
使用translate替代position
- 改变 position 是会导致浏览器进行重排,浏览器会重新计算受到影响的元素的几何位置,这种计算量是非常巨大的且慢的
- translate不会触发回流
浏览器的Flush队列
因为现代浏览器是很聪明的。浏览器自己也清楚,如果每次 DOM 操作都即时地反馈一次回流或重绘,那么性能上来说是扛不住的。于是它自己缓存了一个 flush 队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队
- 何为 “不得已”的时候 ? 上面我们提到一些属性,即当你要用到像这样的属性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 时,你就要注意了!,它们有很强的
即时性,当我们访问这些属性时,浏览器会为了获得此时此刻的、最准确的属性值,而提前将 flush 队列的任务出队——这就是所谓的“不得已”时刻
浏览器垃圾回收
浏览器怎么进行垃圾回收
标记清除算法,GC会检测当前对象有没有被变量所引用,如果没有就回收
从三个点来回答什么是垃圾、如何捡垃圾、什么时候捡垃圾。
- 什么是垃圾
- 不再需要,即为垃圾
- 全局变量随时可能用到,所以一定不是垃圾
- 如何捡垃圾(遍历算法)
- 标记空间中「可达」值。
- 从根节点(Root)出发,遍历所有的对象。
- 可以遍历到的对象,是可达的(reachable)。
- 没有被遍历到的对象,不可达的(unreachable)
- 回收「不可达」的值所占据的内存。
- 做内存整理。
- 标记空间中「可达」值。
- 什么时候捡垃圾
- 前端有其特殊性,垃圾回收的时候会造成页面卡顿。
- 分代收集、增量收集、闲时收集。
浏览器中不同类型变量的内存都是何时释放?
Javascritp 中类型:值类型,引用类型。
- 引用类型
- 在没有引用之后,通过 V8 自动回收。
- 值类型
- 如果处于闭包的情况下,要等闭包没有引用才会被 V8 回收。
- 非闭包的情况下,等待 V8 的新生代切换的时候回收。
哪些情况会导致内存泄露?如何避免?
内存泄露是指你「用不到」(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。
以 Vue 为例,通常有这些情况:
- 监听在
window/body等事件没有解绑 - 绑在
EventBus的事件没有解绑 Vuex的$store,watch了之后没有unwatch- 使用第三方库创建,没有调用正确的销毁函数
解决办法:beforeDestroy 中及时销毁
- 绑定了
DOM/BOM对象中的事件addEventListener,removeEventListener。 - 观察者模式
$on,$off处理。 - 如果组件中使用了定时器,应销毁处理。
- 如果在
mounted/created钩子中使用了第三方库初始化,对应的销毁。 - 使用弱引用
weakMap、weakSet。
闭包会导致内存泄露吗?
顺便说一个我在了解垃圾回收之前对闭包的误解。
闭包会导致内存泄露吗?正确的答案是不会。
内存泄露是指你「用不到」(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。
闭包里面的变量就是我们需要的变量,不能说是内存泄露。
这个误解是如何来的? 因为 IE。IE 有 bug,IE 在我们使用完闭包之后,依然回收不了闭包里面引用的变量。 这是 IE 的问题,不是闭包的问题。 参考这篇
浏览器数据库 IndexedDB
- IndexedDB 就是浏览器提供的本地数据库
- 在用户的浏览器内持久化存储数据的方法
- 允许储存大量数据
浏览器缓存方案
- 强缓存
- 协商缓存
- localStorage与sessionStorage
- IndexedDB浏览器数据库,存储空间大小是根据你当前硬盘内存来决定
浏览器的SameSite
Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF【跨站请求伪造】 攻击和用户追踪http
前端性能优化
从输入 URL 到页面加载完成发生了什么
1、首先输入地址 2、通过这个地址进行DNS解析成ip、把ip地址建立TCP连接,向服务器发送HTTP请求 【tcp:保证数据通信的完整性】
【减少DNS解析时间:浏览器 DNS 缓存和 DNS prefetch】
4、服务器发送永久重定向给浏览器响应 【即使网址更换域名, 通过301永久重定向将旧域名重定向至新域名,挽回流量损失和SEO,优化了SEO】 5、服务器处理请求 6、服务器返回HTTP响应 7、浏览器解析渲染页面 【浏览器渲染机制】 (构建dom树 -> 渲染render树 -> 布局render树 -> 绘制render树) 8、连接结束
所谓性能优化,就是上面的步骤加一起, 我们任何一个用户端的产品, 都需要把这 5 个过程考虑到自己的性能优化方案内、反复权衡,从而打磨出用户满意的速度
1、DNS 解析花时间,能不能尽量减少解析次数或者把解析前置?
-
能——浏览器 DNS 缓存和 设置 DNS prefetch (DNS预获取)
-
DNS 缓存可以帮助减少延迟
- 缓存策略是浏览器自动执行的
-
DNS prefetch
- DNS 请求需要的带宽非常小,但是延迟却有点高,预读取 DNS 能让延迟明显减少一些
- 可以在页面的link标签中添加dns-prefetch告诉浏览器对指定域名预解析,如下:
- 淘宝网站都是这样处理
<link rel="dns-prefetch" href="//domain.com">
-
2、TCP 每次的三次握手都急死人,有没有解决方案?
-
有——长连接、预连接、接入 SPDY 协议 (SPDY为speedy(单词原意:快速的)的缩写,读音也就是speedy。)
[SPDY对比HTTP的优势]
- 复用连接,可在一个TCP连接上传送多个资源。应对了TCP慢启动的特性。
- 请求分优先级,重要的资源优先传送。
- HTTP头部数据也被压缩,省流量。
- 服务器端可主动连接客户端来推送资源(Server Push)。
3、如果说上这两个过程的优化往往需要我们和团队的服务端工程师协作完成,前端单方面可以做的努力有限,那么 HTTP 请求呢?
- 在减少请求次数和减小请求体积方面
- 服务器越远,一次请求就越慢,静态资源放在离我们更近的 CDN
web缓存方案
1、浏览器缓存
浏览器本地缓存最常用的是cookie、localStroage、sessionStroage、webSql、indexDB。
cookie、localStroage、sessionStroage
生命周期:
-
localStorage(本地存储)生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。仅在客户端(即浏览器)中保存,不参与和服务器的通信
-
sessionStorage(会话存储)生命周期为页面会话期间可用,关闭页面或浏览器之后,那么所有通过sessionStorage存储的数据也就被清空了。
作用域不同:不同浏览器无法共享localStorage或sessionStorage中的信息。
-
cookie: 两种方式定义
- 会话期 Cookie 是最简单的 Cookie:浏览器关闭之后它会被自动删除, 会话期Cookie不需要指定过期时间(
Expires)或者有效期(Max-Age) - 持久性 Cookie: 生命周期取决于过期时间(
Expires)或有效期(Max-Age)指定的一段时间
不设置过期时间,则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失
- 会话期 Cookie 是最简单的 Cookie:浏览器关闭之后它会被自动删除, 会话期Cookie不需要指定过期时间(
存储大小:
-
cookie:4KB左右
localStorage和sessionStorage:可以保存5MB的信息。
存储位置:
- cookie、localStorage和sessionStorage都保存在客户端,不与服务器进行交互通信。
- session才是存储在服务器
http请求:
cookie:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
localStorage和sessionStorage:仅在客户端(即浏览器)中保存,不参与和服务器的通信
存储内容类型:
- localStorage和sessionStorage只能存储字符串类型,对于复杂的对象可以使用ECMAScript提供的JSON对象的stringify和parse来处理;
获取方式:localStorage:window.localStorage;sessionStorage:window.sessionStorage;
安全:
-
cookie的安全
1、开启 Cookie 的 HttpOnly参数
可以避免客户端恶意的js脚本无法读取到cookie信息
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly")2、Cookie数据加密。加密后数据就很难篡改了,但是加解密过程会带来性能损耗
3、不放重要数据,重要数据放Session,session是存放在服务端
-
如果一个站点容易受到 XSS 攻击,LocalStorage 是不安全的
-
开发者无法控制本地存储的数据
- 使用本地存储,没有服务器端存储,没有开发人员可以控制的数据库
应用场景:
-
localStoragese:
1、常用于长期登录(+判断用户是否已登录),适合长期保存在本地的数据;
2、存储一些内容稳定的资源,比如官网的
logo,存储Base64格式的图片资源,用户信息、因此利用 localStorage -
sessionStorage:
1、敏感账号一次性登录。
2、记录历史浏览记录次数
- 可以用它存储本次浏览记录。如果关闭页面后不需要这些记录,用
sessionStorage就再合适不过了。事实上微博就采取了这样的存储方式。
3、表单信息临时存储
- 可以用它对表单信息进行维护,将表单信息存储在里面,可以保证页面即使刷新也不会让之前的表单信息丢失。
- 可以用它存储本次浏览记录。如果关闭页面后不需要这些记录,用
-
cookie
1、多个子域名可以通过cookie共享数据, 单点登录
cookie的domain属性设置为父级域名,父级的域名cookie会被子域所共享,拿到父级域名的登录状态,利用这个cookie的特点,可以实现单点登录
cookie的domain属性设置为父级域名,并且父域的cookie会被子域所共享。path属性默认为web应用的上下文路径利用
Cookie的这个特点,没错,我们只需要将Cookie的domain属性设置为父域的域名(主域名),同时将Cookie的path属性设置为根路径,将Session ID(或Token)保存到父域中。这样所有的子域应用就都可以访问到这个Cookie不过这要求应用系统的域名需建立在一个共同的主域名之下,如
tieba.baidu.com和map.baidu.com,它们都建立在baidu.com这个主域名之下,那么它们就可以通过这种方式来实现单点登录
2、存放不敏感数据,如果存放敏感数据,可能会被非法js攻击获取到 (XSS攻击)
3、保存用户的登录状态,登录是否过期,可以配置过期时间,控制cookie的生命周期
缺点:
- 每次请求cookie都在请求头中,如果数据太多,会造成性能问题
- 大小是4kb
2、强缓存 (本地缓存)
- 缓存数据生效的情况下,不需要和服务器交互,可以直接使用
- 直接从本地缓存中读取文件并返回Status Code: 200 OK
- 强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的 【都需要在服务端配置】
- 浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信
应用场景:
- 1、客户端请求服务端图片 【可以设置缓存策略,cache-control字段】
- no-cache: 向服务端去确认该资源是否过期 (每次使用缓存资源前都必须重新验证其有效性)
- no-store: 不使用任何缓存策略, 缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存
- max-age (秒): 控制资源最大有效时间
服务端配置:
- 在服务端的响应头,第一次请求的时候,告诉客户端,该资源什么时候会过期
- Exprires的缺陷是必须保证服务端时间和客户端时间严格同步
- Cache-control:max-age:表示该资源多少时间后过期,解决了客户端和服务端时间必须同步的问题
res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString())
res.setHeader('Cache-Control','max-age=10')
3、(协商缓存)弱缓存
- 在本地缓存中找到该资源,发送一个请求到服务器,服务器判断这个资源没有被改动过,则返回304,让浏览器使用该资源 ( 304文件未修改,可以直接使用缓存的文件)
- 协商缓存依赖于服务端与浏览器之间的通信。
配置:
res.setHeader('max-age': '3600 public')
res.setHeader("etag": '5c20abbd-e2e8')
res.setHeader('last-modified': Mon, 24 Dec 2018 09:49:49 GMT)
协商缓存和强缓存区别
1、强缓存不发请求到服务器,协商缓存会发请求到服务器。
2、在 中强缓存(虽然没有发出真实的 http 请求)的请求状态码返回是 200 ;而协商缓存的话,请求的状态码是 304 。
3、强缓存利用http头的Expires 和 Cache-Control 两个字段来控制的,协商缓存利用http头的Last-Modified 到 Etag
4、CDN缓存
- CDN缓存也叫网关缓存
- 用户在浏览网站的时候,浏览器能够在本地保存静态资源的副本
- 减少了下载量,提高了页面加载速度、增强用户访问体验
- 如果数据已经过期,CDN发出回源请求,来拉取最新的数据
- 回源:就会回到当前最新的服务器去获取资源
5、DNS 缓存和 设置 DNS prefetch (DNS预获取)
-
DNS 缓存可以帮助减少延迟
- 缓存策略是浏览器自动执行的
如何利用好缓存
-
资源使用强缓存,设置cache-contro的no-cache
-
文件资源可以使用协商缓存,设置ETag
- ETag 是由服务器为每个资源生成的唯一的标识字符串,只要文件内容不同,它们对应的 Etag 就是不同的
- Etag 能够精准地感知文件的变化
-
设置DNS预获取
- DNS 请求需要的带宽非常小,但是延迟却有点高,预读取 DNS 能让延迟明显减少一些
-
静态资源放在CDN
- CDN可以缩减用户和服务器的距离, 提升加载效率
- 浏览器对一个域名的并发数有限制,所以用CDN域名可以专门对加载静态资源
CDN 的缓存与回源机制解析
CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。用于更加有效的向用户发布内容, 这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。 CDN 提供快速服务,较少受高流量影响。
为什么要用 CDN
- 提升首次请求的响应能力,借助 CDN 的能力是更好不过了
- 使用CDN,分散服务器压力 !! !!
- 如果应用程序web服务器离用户更近,那么一个http请求的响应时间将缩短.
- 增强用户访问体验
CDN的内部原理
- CDN就是把一些静态资源分散在各个区域的服务器
- 用户访问的时候,返回离用户最近的资源
CDN是如何工作?
1、用户在自己的浏览器中输入要访问的网站域名。
2、浏览器向本地DNS服务器请求对该域名的解析。
3、本地DNS服务器中如果缓存有这个域名的解析结果,则直接响应用户的解析请求。
4、本地DNS服务器中如果没有关于这个域名的解析结果的缓存,则以迭代方式向整个DNS系统请求解析,获得应答后将结果反馈给浏览器。
5、浏览器得到域名解析结果,就是该域名相应的服务设备的IP地址 。
6、浏览器获取IP地址之后,经过标准的TCP握手流程,建立TCP连接。
7、浏览器向服务器发起HTTP请求。
8、服务器将用户请求内容传送给浏览器。
9、经过标准的TCP挥手流程,断开TCP连接。
CDN的缓存和回源
- CDN 的核心点有两个,一个是缓存,一个是回源。(CDN有个源站的概念,源站就是提供内容的站点(网站的真实服务器), 从源站取内容的过程叫做回源。)
- “缓存”就是说我们把资源 copy 一份到 CDN 服务器上这个过程,“回源”就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),转头向根服务器(或者它的上层服务器)去要这个资源的过程。
CDN 与前端性能优化
- CDN 往往被用来存放静态资源
- 所谓“静态资源”,就是像 JS、CSS、图片等不需要业务服务器进行计算即得的资源
- 而'动态资源',顾名思义是需要后端动态生成的资源
浏览器缓存机制和缓存策略剖析
浏览器缓存主要有以下几个优点:
- 减少重复数据请求,避免通过网络再次加载资源,节省流量。
- 降低服务器的压力,提升网站性能。
- 加快客户端加载网页的速度, 提升用户体验。
缓存可以说是性能优化中简单高效的一种优化方式了。缓存可以减少网络 IO 消耗,提高访问速度。浏览器缓存是一种操作简单、效果显著的前端性能优化手段。对于这个操作的必要性,Chrome 官方给出的解释似乎更有说服力一些:
通过网络获取内容既速度缓慢又开销巨大。较大的响应需要在客户端与服务器之间进行多次往返通信,这会延迟浏览器获得和处理内容的时间,还会增加访问者的流量费用。因此,缓存并重复利用之前获取的资源的能力成为性能优化的一个关键方面。
很多时候,大家倾向于将浏览器缓存简单地理解为“HTTP 缓存”。但事实上,浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:
- Memory Cache
- Service Worker Cache
- HTTP Cache
- Push Cache
HTTP缓存机制
- 它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。
强缓存的特征
- 强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信
- http 头 ( http headers)中的 Expires : expires属性是在服务端配置的, expires添加的是该资源过期的日期,浏览器会根据该过期日期与客户端时间对比,如果过期时间还没到,则会去缓存中读取该资源,如果已经到期了,则浏览器判断为该资源已经不新鲜要重新从服务端获取
- http 头中的 Cache-Control : Cache-Control属性值也是在server端配置的,不同的服务器有不同的配置
expires
- expires :在 Response Headers 中将过期时间写入 expires 字段,expires 是一个时间戳,再向服务器请求资源时,浏览器就会先对比本地时间(客户端时间)和 expires 的过期时间戳,如果本地时间小于 expires 设定的过期时间 (过期时间还没到),那么就直接去缓存中取这个资源。如果已经到期了,则浏览器判断为该资源已经不新鲜要重新从服务端获取
- 这样其实会导致一个问题,它最大的问题在于对“本地时间”的依赖,如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。
- 考虑到 expires 的局限性,HTTP1.1版本中被抛弃了。之后HTTP1.1 新增了
Cache-Control字段来完成 expires 的任务。
Cache-Control
-
它和
Expires本质的不同在于它并没有采用具体的过期时间点这个方式,而是采用过期时长来控制缓存,对应的字段是max-age表示过期时长 -
在 Cache-Control 中,我们通过
max-age来控制资源的有效期,max-age 表示缓存过期时长,在max-age的范围时间内资源都是有效的,超过这个最大缓存时间,就会拉去最新资源- Cache-Control 相对于 expires 更加准确,它的优先级也更高。当 Cache-Control 与 expires 同时出现时,我们以 Cache-Control 为准。
-
Cache-Control 的神通,可不止于这一个小小的 max-age。如下的用法也非常常见:
cache-control: max-age=3600, s-maxage=31536000- s-maxage 优先级高于 max-age,两者同时出现时,优先考虑 s-maxage。如果 s-maxage 未过期,则向代理服务器请求其缓存内容。
- 这个 s-maxage 不像 max-age 一样为大家所熟知。的确,在项目不是特别大的场景下,max-age 足够用了。但在依赖各种代理的大型架构中,我们不得不考虑代理服务器的缓存问题。s-maxage 就是用于表示 cache 服务器上(比如 cache CDN)的缓存的有效时间的,并只对 public 缓存有效。
- 简单来说:s-maxage仅在代理服务器中生效,客户端中我们只考虑max-age。
public 与 private
- public 与 private 是针对资源是否能够被代理服务缓存而存在的一组对立概念
如果我们为资源设置了 public,那么它既可以被浏览器缓存,也可以被代理服务器缓存;如果我们设置了 private,则该资源只能被浏览器缓存,中间的代理服务器不能缓存。private 为默认值。但多数情况下,public 并不需要我们手动设置
no-store与no-cache
- no-cache 绕开了浏览器:我们为资源设置了 no-cache 后,每一次发起请求都不会再去询问浏览器的缓存情况,而是**直接向服务端去确认该资源是否过期(**即走我们下文即将讲解的协商缓存的路线)
- no-store 比较绝情,顾名思义就是不使用任何缓存策略。在 no-cache 的基础上,它连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。
Cache-Control: no-cache, no-store
思考如下问题
-
问题1:可能存在客户端时间跟服务端时间不一致的问题。
解决:建议Expires结合Cache-Control一起使用
-
问题2:浏览器缓存的资源,若又想更新资源,如何实现?
解决:通过修改该资源的名称来实现。修改了资源名称,浏览器会当做不同的资源。
协商缓存
-
协商缓存依赖于服务端与浏览器之间的通信。
-
协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。
- 如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304 ( 304文件未修改,可以直接使用缓存的文件)
协商缓存的实现:从 Last-Modified 到 Etag
-
Last-Modified 即最后修改时间, 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 响应头 返回:
Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT -
浏览器接收到后,随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:
If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT -
服务器接收到这个If-Modified-Since时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回 304 响应,Response Headers 不会再添加 Last-Modified 字段。
-
使用 Last-Modified 存在一些弊端,这其中最常见的就是这样两个场景:
- 我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。
- 当我们修改文件的速度过快时(比如花了 100ms 完成了改动),由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了。
这两个场景其实指向了同一个 bug——服务器并没有正确感知文件的变化。为了解决这样的问题,Etag 作为 Last-Modified 的补充出现了。
Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,反之亦然。因此 Etag 能够精准地感知文件的变化。
Etag 和 Last-Modified 类似,当首次请求时,我们会在响应头里获取到一个最初的标识符字符串,举个
ETag: W/"2a3b-1602480f459"
那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对了:
If-None-Match: W/"2a3b-1602480f459"
Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。因此启用 Etag 需要我们审时度势。正如我们刚刚所提到的——Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充和强化存在。 Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
Last-Modified和Etag区别
1、在 精准度上,ETag优于 Last-Modified。优于 ETag 是按照内容给资源上标识,因此能准确感知资源的变化。而 Last-Modified 就不一样了,它在一些特殊的情况并不能准确感知资源变化,主要有两种情况:
- 编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。
- Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。
2、在性能上,Last-Modified优于 ETag,也很简单理解,Last-Modified仅仅只是记录一个时间点,而 Etag需要根据文件的具体内容生成哈希值。
HTTP缓存流程图
- reusable response : 可重用的响应
- revalidate each time : 每次重新验证
- cacheable by intermediate caches :可由中间缓存
当我们的资源内容不可复用时,直接为 Cache-Control 设置 no-store,拒绝一切形式的缓存;否则考虑是否每次都需要向服务器进行缓存有效确认,如果需要,那么设 Cache-Control 的值为 no-cache;否则考虑该资源是否可以被代理服务器缓存,根据其结果决定是设置为 private 还是 public;然后考虑该资源的过期时间,设置对应的 max-age 和 s-maxage 值;最后,配置协商缓存需要用到的 Etag、Last-Modified 等参数。
强缓存和协商缓存的区别
1、强缓存不发请求到服务器,协商缓存会发请求到服务器。
2、在 中强缓存(虽然没有发出真实的 http 请求)的请求状态码返回是 200 ;而协商缓存的话,请求的状态码是 304 。
3、强缓存利用http头的Expires 和 Cache-Control 两个字段来控制的,协商缓存利用http头的Last-Modified 到 Etag
Memory Cache
- Memory Cache指的是内存缓存,从效率上讲它是最快的。但是从存活时间来讲又是最短的,当渲染进程结束后,也就是 tab 关闭以后, 内存缓存也就不存在了。
- 那么哪些文件会被放入内存呢? (内存是有限的,很多时候需要先考虑即时呈现的内存余量)
- Base64 格式的图片,几乎永远可以被塞进 memory cache,这可以视作浏览器为节省渲染开销的“自保行为”;此外,体积不大的 JS、CSS 文件,也有较大地被写入内存的几率——相比之下,较大的 JS、CSS 文件就没有这个待遇了,内存资源是有限的,它们往往被直接甩进磁盘。
Service Worker Cache
- Service Worker 借鉴了 Web Worker的 思路,即Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM
- 绝对不会阻碍当前js线程的执行,可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为 Service Worker Cache。
- Service Worker 的生命周期包括 install、active、working 三个阶段
- Server Worker 对协议是有要求的,必须以 https 协议为前提。
- Service Worker 同时也是 PWA 的重要实现机制
Push Cache
- 即推送缓存,这是浏览器缓存的最后一道防线。它是
HTTP/2中的内容,Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。 - Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
- 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
DOM优化原理和基本实践
-
浏览器的重绘和重排
-
事件委托
- 通过事件委托,减少对DOM事件绑定的操作
- 大量的事件监听、大量事件处理函数会产生大量的消耗内存
- 只绑定一次事件,无频繁访问DOM,性能较高
-
减少for循环嵌套
- 最好不要超过2层循环嵌套,循环遍历会到来性能损耗,算法的复杂度会以平方次数叠加
-
减少dom的操作
- 缓存已经访问过的元素;
- 使用 DocumentFragment 暂存 DOM,整理好以后再插入 DOM 树;
- 使用 class类方式 来操纵元素的样式;
- 避免使用 JavaScript 修复布局。
- 不占用JS主线程
-
position属性为
absolute或fixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响。- 因为脱离了文档流
Webpack性能优化
-
优化构建速度
-
-
分析影响打包速度环节
- 1、找到入口文件,分析内容
- 优化解析时间
- 2、找出所有模块依赖, 拿到依赖路径,转换代码
- 优化搜索时间
- 3、把所有的依赖模块输出源代码,把bundle.js输出到
dist目录中 - 把源代码进行压缩,通常 webpack 需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时,打包就容易卡住。
- 优化压缩时间。
-
-
减少包体积
- tree Shaking
- code Splitting代码分割
-
热更新
- HotModuleReplacementPlugin是webpack的内置插件
- 启用热替换模块Hot Module Replacement,也被称为 HMR。
图片优化
Base64
Base64 并非一种图片格式,而是一种编码方式
将编码结果写入 HTML 或者写入 CSS,从而减少 HTTP 请求的次数
应用场景:
1、图片的实际尺寸很小
2、图片无法以雪碧图的形式与其它小图结合 【雪碧图也叫CSS精灵, 是一CSS图像合成技术】
3、图片的更新频率非常低(不需我们重复编码和修改文件内容,维护成本较低)
骨架屏+延迟加载
骨架屏: 骨架屏(Skeleton Screen)是指在页面数据加载完成前,先给用户展示出页面的大致结构(灰色占位图),在拿到接口数据后渲染出实际页面内容然后替换掉
方案:
- 1、使用图片、svg 或者手动编写骨架屏代码:使用 HTML + CSS 的方式,我们可以很快的完成骨架屏效果,但是面对视觉设计的改版以及需求的更迭,我们对骨架屏的跟进修改会非常被动,这种机械化重复劳作的方式此时未免显得有些机动性不足
- 2、通过预渲染手动书写的代码生成相应的骨架屏 : vue-skeleton-webpack-plugin
- 3、饿了么内部的生成骨架页面的工具:该方案通过一个 webpack 插件 page-skeleton-webpack-plugin 的方式与项目开发无缝集成
svg
体积小、不失真、兼容性好
SVG 与 PNG 和 JPG 相比,文件体积更小
- SVG 的局限性
- 渲染成本比较高,这点对性能来说是很不利的
雪碧图(CSS精灵)
- 即多张图合并为一张整图,然后再利用background-position进行背景图定位的一种技术
理解serviceWorker
-
参考:
-
我们用vue或者react创建项目的时候,在入口文件都有serviceWorker
-
通俗的理解:serviceWorker就是客户端和服务器之间的代理
-
基于h5的web worker,所以绝对不会阻碍当前js线程的执行,serviceWorker最重要的工作原理就是
1、后台线程:独立于当前网页线程;
2、网络代理:在网页发起请求时代理,来缓存文件;
serviceWorker可以做什么
serviceWorker可以进行缓存
1、你可以请求Service Worker作为中间人检查事件收发,请求Service Worker在缓存里保存完整的资源。当缓存的项目被请求,Service Worker可以在缓存中请求这些数据而不用通过HTTP。这些资源被缓存了,浏览器就可以在网络不通的情况下展示内容。
推送提醒
“Service Worker可以在浏览器窗口关闭时保持活动状态”这一奇迹,你可以实现类似推送提醒的功能。
后台数据同步
可以在浏览器窗口关闭时保持活动状态也意味着Service Worker可以在后台运作(如用户切换到其他标签时)。假如当浏览器离线时,你要发送一些文件,Service Worker 可以帮你在网络接通时上传。
懒加载
简单来说:懒加载其实就是延时加载,即当需要用到的时候再去加载,是一种很好优化网页性能的方式
懒加载的优点
1、能提升用户的体验,不妨设想下,用户打开像手机淘宝长页面的时候,如果页面上所有的图片都需要加载,由于图片数目较大,等待时间很长,用户难免会心生抱怨,这就严重影响用户体验。
2、减少无效资源的加载,这样能明显减少了服务器的压力和流量,也能够减小浏览器的负担。
3、防止并发加载的资源过多会阻塞js的加载,影响网站的正常使用。
路由懒加载
在单页应用中,如果没有应用懒加载,运用webpack打包后的文件将会异常的大, 造成进入首页时,需要加载的内容过多,延时过长,不利于用户体验, 运用懒加载可以将页面进行划分,按需加载页面,可以分担首页所承担的加载压力,减少加载用时。
图片懒加载
原理:
用户滚动到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。常适用图片很多,页面很长的电商网站场景中。
1、首先将页面上的图片的 src 属性设为空字符串,
2、而图片的真实路径则设置自定义属性中如data-src
3、 当页面滚动的时候需要去监听scroll滚动事件,在scroll滚动事件的回调中,判断我们的懒加载的图片是否进入可视区域,如果图片在可视区内将图片的 src 属性设置为我们自定义属性的data-src的值,这样就可以实现延迟加载。
注意:图片要指定宽高
函数节流性能优化
- 当页面滚动时,函数会被高频触发,这非常影响浏览器的性能。
- 防抖和节流在效果上的不同: 防抖要等到停止滚动的时候才执行handleScroll事件; 而节流是隔一段固定的时间就执行一次handleScroll事件,不管有没有滚动。
- 因此对于图片懒加载是使用节流最适合了,而不是等到停止滚动的时候才会执行
预加载
资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源
预加载的优点
在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验,减少等待的时间。否则,如果一个页面的内容过于庞大,没有使用预加载技术的页面就会长时间的展现为一片空白,直到所有内容加载完毕。
实现预加载的办法
方式一
-
用CSS和JavaScript实现预加载
-
在该脚本的第一部分,我们获取使用类选择器的元素,并为其设置了background属性,以预加载不同的图片。
该脚本的第二部分,我们使用addLoadEvent()函数来延迟preloader()函数的加载时间,直到页面加载完毕。
如果JavaScript无法在用户的浏览器中正常运行,会发生什么?很简单,图片不会被预加载,当页面调用图片时,正常显示即可。
-
懒加载和预加载的对比
两者都是提高页面性能有效的办法,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
函数节流和防抖
- 性能优化思想:降低代码执行频率
- 这两个东西都以闭包的形式存在。
区别:防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行
节流
函数节流是指一定时间内js方法只跑一次。比如人的眨眼睛,就是一定时间内眨一次。这是函数节流最形象的解释。
- 节流:简单来说就是节省资源的目的(降低频率),在一段时间内,核心代码只执行一次
- 限制一个函数在一定时间内只能执行一次。
节流的应用
1、滚动的变色
2、每间隔某个时间去执行某函数,避免函数的过多执行
3 、滚动加载,加载更多或滚到底部监听
4、搜索框,搜索联想功能
5、高频点击提交,表单重复提交
6、图片懒加载
防抖
- 防抖的中心思想在于:我会等你到底。在某段时间内,不管你触发了多少次回调,我都只认最后一次。
- 防抖指频繁触发的情况下,只有足够的空闲时间,才执行代码一次。比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。
- 防抖作用:防止频繁触发带来的DOM抖动
- 简单来说:防抖无论你触发多少次,只在最后一次事件后触发
防抖的应用
用于:
1 、一般可以使用在用户输入完毕之后请求,而不是每次输入都去请求
-
比如10个字符,10次请求
-
用户停止输入才触发查询事件
2、 给按钮加函数防抖防止表单多次提交。
3、手机号、邮箱验证输入检测
4、窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染
手写一个防抖函数
// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
// last为上一次触发回调的时间, timer是定时器
let last = 0, timer = null
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = +new Date()
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last < delay) {
// 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
} else {
// 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
last = now
fn.apply(context, args)
}
}
}
// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
注意的地方
- 1、 保留调用时的this上下文 (因为DOM事件是this是指向当前对象的),如果没有保留上下文this对象,则this会指向window
- 2、通过apply改变当前对象,因此this指向的时当前DOM事件 绑定元素对象
惰性载入函数
- 惰性函数就是解决每次都要进行判断的这个问题,解决原理很简单,重写函数
- 惰性载入,指函数执行的分支只会发生一次
- 有两种实现惰性载入函数的方式,第一种是在函数被调用时再处理,在第一次调用中,该函数会覆盖为另外一个按合适方式执行的函数,这样任何对函数的调用都不用再经过执行的分支了。第二种实现惰性载入的方式是在声明函数时就制定适当的函数,这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点儿性能。
使用场景:
- 在某个场景下我们的函数中有判断语句,这个判断依据在整个项目运行期间一般不会变化,所以判断分支在整个项目运行期间只会运行某个特定分支,那么就可以考虑惰性载入函数
let type = '1'
let doAction = () => {
if (type === '1') {
doAction = () => console.log('111')
} else if (type === '2') {
doAction = () => console.log('222')
} else {
doAction = () => console.log('333')
}
return doAction()
}
doAction()
- 那么第一次运行之后就会覆写这个方法,下一次再运行的时候就不会执行判断了。当然现在只有一个判断,如果判断很多,分支比较复杂,那么节约的资源还是可观的。
SEO优化
- 合理的
title 、 description 、 keywords:搜索对着三项的权重逐个减小,title值强调重点即可,重要关键词出现不要超过2次,而且要靠前,不同页面title要有所不同;description把页面内容高度概括,长度合适,不可过分堆砌关键词,不同页面description有所不同;keywords列举出重要关键词即可 - 语义化的
HTML代码,符合W3C规范:语义化代码让搜索引擎容易理解网页 - 重要内容
HTML代码放在最前:搜索引擎抓取HTML顺序是从上到下,有的搜索引擎对抓取长度有限制,保证重要内容一定会被抓取 - 重要内容不要用
js输出:爬虫不会执行js获取内容 - 控制页面的大小,减少http请求,提高网站的加载速度。
- 少用
iframe:搜索引擎不会抓取iframe中的内容
时间切片
时间切片:长任务分割为一个个执行时间很短的任务
解决同步更新的方案之一就是时间切片:把更新过程碎片化,把一个耗时长的任务分成很多小片
性能测试
-
方式一 : 【启动一个计时器来跟踪某一个操作的占用时长】
- console.time和console.timeEnd方式可以测试出程序执行的时间
- 启动一个以入参作为特定名称的计时器 console.time('time') 结束特定的 计时器 并以豪秒打印其从开始到结束所用的时间 console.timeEnd('time') 打印计时器 所运行的时间 console.timeLog('time')
-
方式二:
- console.profile()和console.profileEnd() 是启动浏览器内置的分析器,可以更精准获取当前程序每一个步骤所消耗的时间
在实际开发中,console.time更常用
HTTP
陈述HTTP
常见的状态码
200 OK 客户端请求成功。【请求成功并且服务器创建了新的资源】
204 请求收到,但返回信息为空
301 Moved Permanently 请求永久重定向。 (永久重定向 浏览器会对301资源进行缓存)
302 Moved Temporarily 请求临时 重定向。 (临时重定向 不会进行缓存)
比如你的网站从 HTTP 升级到了 HTTPS 了,以前的站点再也不用了,应当返回
301,这个时候浏览器默认会做缓存优化,在第二次访问的时候自动访问重定向的那个地址。而如果只是暂时不可用,那么直接返回
302即可,和301不同的是,浏览器并不会做缓存优化。303:表示服务器要将浏览器重定向到另一个资源
- 这个资源的 URI 会被写在响应 Header 的 Location 字段
304 Not Modified 文件未修改,可以直接使用缓存的文件
- 如果客户端发送请求,而请求的内容并没有改变,则服务器应当返回这个状态码,服务器告诉客户端,原来缓冲的文档还可以继续使用。
- POST 请求重定向到 GET 请求
400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized 请求未经授权,无法访问。
403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因。
404 Not Found 请求的资源不存在,比如输入了错误的URL。
422:状态码是指请求格式正确,但是由于含有语义错误,无法响应
500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求。
503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常。
开发中经常用到的状态码
- 登录成功/注册成功=> 返回200 (服务器成功处理请求) 【强缓存返回200】
- 文件未修改,可以直接使用缓存的文件=>返回304 【协商缓存】
- 用户/邮箱不存在 =>返回 404 (服务器找不到请求页面)
- 服务器错误 => 返回500
- 登录失败/授权失败/用户名或密码不正确 =>返回401 (未授权,需要进行身份认证)
- 201:请求成功并且服务器创建了新的资源
- 422:状态码是指请求格式正确,但是由于含有语义错误,无法响应。
302和301的区别?
参考:https://zhuanlan.zhihu.com/p/267124206
301:永久重定向
- 301重定向会将浏览器访问的旧地址重定向到新地址
- 比如 通过301永久重定向将旧域名重定向至新域名,挽回流量损失和SEO
- 浏览器会对301资源进行缓存
- 可以通过请求头来控制是否缓存,在使用 301 时,我们将其设置为不缓存就可以了。比如设置
Cache-Control: no-store或Cache-Control: no-cache
如何清除 301 重定向缓存?
- 浏览器清除历史记录
- Network 面板清除缓存
- 对某个请求,右键选择Clear Browser Cache 清除浏览器缓存
302:临时重定向
- 请求的资源临时搬到了其他位置
HTTP请求中的Form Data与Request Payload的区别
POST类型的请求会附带请求数据。而常用的两种传参方式为:Form Data 和 Request Payload。
POST请求当参数数量较多,且对数据有一定安全性要求时,会考虑用post请求传递参数数据。POST请求的参数数据是在请求体中。
方式一:Form Data形式 当POST请求的请求头里设置Content-Type: application/x-www-form-urlencoded(默认),
方式二:Request Payload形式
如果使用AJAX原生POST请求,请求头里设置Content-Type:application/json,请求的参数会显示在Request Payload中,参数格式为JSON格式:{"key":"value","key":"value"...},这种方式可读性会更好。
Form Data 和 Request Payload 区别
- 如果请求头里设置Content-Type: application/x-www-form-urlencoded,那么这个请求被认为是表单请求,参数出现在Form Data里,格式为key=value&key=value&key=value...
- 原生的AJAX请求头里设置Content-Type:application/json,或者使用默认的请求头Content-Type:text/plain;参数会显示在Request payload块里提交。
HTTP 缓存的首部字段
参考:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/496
常见的HTTP 缓存首部字段有:
- Expires:响应头,代表该资源的过期时间
- Cache-Control:请求/响应头,缓存控制字段,精确控制缓存策略
- If-Modified-Since:请求头,资源最近修改时间,由浏览器告诉服务器
- Last-Modified:响应头,资源最近修改时间,由服务器告诉浏览器
- Etag:响应头,资源标识,由服务器告诉浏览器
- If-None-Match:请求头,缓存资源标识,由浏览器告诉服务器
其中, 强缓存 :
- Expires(HTTP/1.0)
- Cache-Control(HTTP/1.1)
协商缓存:
- Last-Modified 和 If-Modified-Since(HTTP/1.0)
- ETag 和 If-None-Match(HTTP/1.1)
HTTP1
- HTTP 1.x 的文本格式传输
- HTTP1可以开启多个TCP连接
HTTP1的缺点:
-
安全因素
- HTTP1是文本格式传输,且传输的内容都是明文的方式,无法保证数据的安全性
-
性能开销大
- header携带内容过大
-
连接无法复用
- 传输数据时,每次都需要重新建立连接
HTTP2
- HTTP/2引入了“服务端推送(server push)”的概念,它允许服务端在客户端需要数据之前就主动地将数据发送到客户端缓存中,从而提高性能。
- 服务器主动推动到数据到客户端缓存中
- HTTP/2提供更多的加密支持
- HTTP/2使用多路技术,允许多个消息在一个连接上传输。
- 只通过一个TCP连接就可以传输多个请求数据
- 解决了浏览器限制同一个域名下的请求数量问题
- 它增加了头压缩(header compression),因此即使非常小的请求,其请求和响应的header都只会占用很小比例的带宽
- 减少了header的大小
- HTTP/2 采用二进制格式传输数据,而非HTTP 1.x 的文本格式
- 二进制协议解析起来更高效
- HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。
HTTP3
由于HTTP2使用了多路技术,一个TCP连接可以传输多个请求数据,但是如果这个连接出现了丢包的情况,那么整个TCP都要等待重新传输,也就导致了后面的所有数据都被阻塞了
- HTTP/3 最大的改造就是使用了 QUIC
- QUIC(读作“quick”):可以允许多个数据流独立到达
- QUIC与http2的多路复用连接协同工作,允许多个数据流独立到达所有端点,因此不受涉及其他数据流的丢包影响
- 丢包也不会造成阻塞
HTTP/3新特性,为什么选择使用UDP协议
参考:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/492
- TCP连接时间长,UDP是无连接
- TCP连接容易丢包
- HTTP/2 使用了多路复用, 一个连接可以传输多个请求数据,但是如果某个连接出现丢包,整个TCP都要等待重新传输
HTTP 1.0/1.1/2.0在并发请求上主要区别是什么?
HTTP/1.0
- 每次TCP连接只能发送⼀个请求,当服务器响应后就会关闭这次连接,下⼀个请求需要再次建 ⽴TCP连接.
HTTP/1.1
- 默认采⽤持续连接 keep alive
- 管道机制:在同⼀个TCP连接⾥,允许多个请求同时发送,增加了并发性
- 默认采⽤持续连接(TCP连接默认不关闭,可以被多个请求复⽤,不⽤声明Connection: keepalive). 增加了管道机制,在同⼀个TCP连接⾥,允许多个请求同时发送,增加了并发性,进⼀步改善 了HTTP协议的效率, 但是同⼀个TCP连接⾥,所有的数据通信是按次序进⾏的。回应慢,会有许多请求排队,造 成”队头堵塞”。
- 简单来说,HTTP 1.1长连接就是一个TCP连接可以允许多个请求同时发送 (并发性),但是这些所有请求都是有次序进行的,就是上一个请求响应后,才能处理下一个请求,会有多个请求排队,容易造成堵塞
如何在http1.0使用持久连接 (支持keep alive)?
- 需要在请求头header中加入Connection:keep-alive
- setRequestHeader("Connection" , "Keep-Alive" / "close") 来控制是否保持连接。
- 由于HTTP1.1默认是持久连接,那如何恢复到HTTP1.0这种状态呢 (服务器响应后关闭连接这种状态)
- 在请求头 Connection字段设置close即可
HTTP/2.0
1.加了双⼯模式,即不仅客户端能够同时发送多个请求,服务端也能同时处理多个请求,解决了 队头堵塞的问题。
2.使⽤了多路复⽤的技术,做到同⼀个连接并发处理多个请求,⽽且并发请求的数量⽐HTTP1.1 ⼤了好⼏个数量级。
3.增加服务器推送的功能,不经请求服务端主动向客户端发送数据
HTTP/1.1长连接和HTTP/2.0多路复用的区别?
HTTP/1.1:同⼀时间⼀个TCP连接只能处理⼀个请求, 采⽤⼀问⼀答的形式, 上⼀个请求响应后 才能处理下⼀个请求. 由于浏览器最⼤TCP连接数的限制, 所以有了最⼤并发请求数的限制.
HTTP/2.0:同域名下所有通信都在一个连接里完成,消除了因多个 TCP 连接⽽带来的延时和 内存消耗。单个连接上可以并⾏交错的请求和响应,之间互不⼲扰
http的缺点
- 通信使用明文可能会被窃听
- 不验证通信方的身份就可能遭遇伪装
- 无法证明报文的完整性,可能已遭篡改
HTTPS有了解吗
- HTTPS是在HTTP上建立SSL加密层,并对传输数据进行加密,是HTTP协议的安全版。现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。
HTTPS主要作用是:
(1)对数据进行加密,并建立一个信息安全通道,来保证传输过程中的数据安全;
(2)对网站服务器进行真实身份认证。
为什么不一直使用https?
原因:
- 与http纯文本通信相比,加密通信会消耗服务器的CPU和内存资源,如果每次通信都加密,会消耗相当多的资源
- 因此,一般地,非敏感信息使用http通信,只有包含个人信息等敏感数据时候,才利用https加密通信
- 仅在那些需要信息隐藏时才会加密,来节约资源
- 购买证书的开销也是的原因之一
网站为什么设置301重定向吗
重定向原因:
(1)网站调整(如改变网页目录结构)
(2)网页被移到一个新地址;
(3)网页扩展名改变(如应用需要把.php改成.Html或.shtml)。 这种情况下,如果不做重定向,则用户收藏夹或搜索引擎数据库中旧地址只能让访问客户得到一个404页面错误信息,访问流量白白丧失;再者某些注册了多个域名的网站,也需要通过重定向让访问这些域名的用户自动跳转到主站点等。
即使网址更换域名, 通过301永久重定向将旧域名重定向至新域名,挽回流量损失和SEO,优化了SEO
put、post、 get 、delete
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
- DELETE(DELETE):从服务器删除资源。
POST和PUT的区别容易被简单地误认为“POST表示创建资源,PUT表示更新资源.但实际上两者都可以用来创建或是更新数据.单从技术上来说,他们并没有什么区别.但是在HTTP规范中POST是非等幂的,多次调用会产生不同的结果,比如:创建一个用户,由于网络原因或是其他原因多创建了几次,那么将会有多个用户被创建.而PUT id/456则会创建一个id为456的用户,多次调用还是会创建的结果是一样的,所以PUT是等幂的
区别:
- PUT 这个协议本身不存在验证机制,任何人都可以上传文件,存在安全性问题
- 可通过中间件来验证参数
- delete则可以删除文件,造成恶意攻击
get和post区别
一次完整的HTTP事务是怎样的一个过程?
a. 域名解析
b. 发起TCP的3次握手
c. 建立TCP连接后客户端发起http请求
d. 服务器端响应http请求,浏览器得到html代码
e. 浏览器解析html代码,并请求html代码中的资源
f. 浏览器对页面进行渲染呈现给用户
网络协议
1、TCP状态 (SYN, FIN, ACK, PSH, RST, URG)
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议
TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包
在TCP层,有个FLAGS字段 (FLAGS意思为标识),这个字段有以下几个标识
- SYN, FIN, ACK, PSH, RST, URG
- SYN表示建立连接 (SYN:同步序列编号(Synchronize Sequence Numbers))
- FIN表示关闭连接,
- ACK表示响应, (ACK (Acknowledge character)即是确认字符)
- PSH表示有 DATA数据传输
- RST表示连接重置。
2、TCP连接的建立与终止
参考:https://zhuanlan.zhihu.com/p/86426969
2.1 三次握手
- 第一次握手:客户端发送一个SYN(同步序列号)码给服务器,表示要发起连接请求;
- 第二次握手: 服务器收到客户端的SYN报文段,需要对这个SYN报文段进行回复确认,将SYN位置为1, 把SYN+ACK(确认包)发送给客户端,可以建立连接
- 第三次握手: 客户端收到服务端ACK确认码之后, (为了证明客户端发送数据正常)客户端需要再回复一次报文, 将服务端ACK序列号+1,服务端收到报文后没问题之后,就建立连接;
2.2 四次挥手
- 第一次挥手: 客户端发送FIN(结束)报文,通知服务器数据已经传输完毕;
- 第二次挥手: 服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了
- (此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。)
- 第三次挥手: 服务器已经传输完毕,再次发送FIN通知客户端,数据已经传输完毕,等待客户端的确认
- 第四次挥手: 客户端收到 FIN 之后,客户端再次发送ACK作为应答,进入TIME_WAIT状态;服务器和客户端关闭连接;
四次握手就是中间多了一层:等待服务器传输数据, 传输完成之后再一次响应回复相关数据的过程
2.3为什么建立连接是三次握手,而断开连接是四次挥手呢
简单来说:(因为二次挥手的时候当服务端收到客户端FIN报文时,需要先向客户端发送ACK码进行回复确认,但是此时的服务端并不会关闭socket【Socket是基于TCP】的连接),而是处于等待关闭的状态,因为服务端还有数据没有发送完毕,当服务端发送数据完毕之后,再向客户端发送FIN报文
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
- 答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
3、输入url。按下回车键后发生了什么
1、首先输入地址 2、通过这个地址进行DNS解析成ip 3、把ip地址建立TCP连接,向服务器发送HTTP请求 【保证数据通信的完整性】 4、服务器发送永久重定向给浏览器响应 【即使网址更换域名, 通过301永久重定向将旧域名重定向至新域名,挽回流量损失和SEO,优化了SEO】 5、服务器处理请求 6、服务器返回HTTP响应 7、浏览器解析渲染页面 【浏览器渲染机制】 (构建dom树 -> 构建渲染render树 -> 布局render树 -> 绘制render树) 8、连接结束
1、URL 解析
1、首先判断你输入的是一个合法的 URL 还是一个待搜索的关键词,并且根据你输入的内容进行自动完成、字符编码等操作。
2、HSTS (HTTP严格传输安全协议)
- 由于安全隐患,会使用 HSTS 强制客户端使用 HTTPS 访问页面
3、其他操作
- 浏览器还会进行一些额外的操作,比如安全检查、访问限制
4、检查缓存
- 如果缓存中有,会直接在屏幕中显示页面内容
2、DNS解析
1、解析获取相应的IP地址
3、TCP 连接
浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手
4、服务器处理请求
服务器处理请求并返回 HTTP 报文,将数据返回至浏览器
5、浏览器接受响应
浏览器接收到来自服务器的响应资源后,会对资源进行分析。
- 浏览器解码响应,如果响应可以缓存,则存入缓存
6、渲染页面
1、首先,在浏览器地址栏中输入url,先解析url,检测url地址是否合法 2、浏览器先查看浏览器缓存-系统缓存-路由器缓存,如果缓存中有,会直接在屏幕中显示页面内容。若没有,则跳到第三步操作。 浏览器缓存:浏览器会记录DNS一段时间,因此,只是第一个地方解析DNS请求; 操作系统缓存:如果在浏览器缓存中不包含这个记录,则会使系统调用操作系统,获取操作系统的记录(保存最近的DNS查询缓存); 路由器缓存:如果上述两个步骤均不能成功获取DNS记录,继续搜索路由器缓存; ISP缓存:若上述均失败,继续向ISP搜索。 3、在发送http请求前,需要域名解析(DNS解析),解析获取相应的IP地址。 4、浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手。 5、握手成功后,浏览器向服务器发送http请求,请求数据包。 6、服务器处理收到的请求,将数据返回至浏览器 7、浏览器收到HTTP响应 8、浏览器解码响应,如果响应可以缓存,则存入缓存。 9、 浏览器发送请求获取嵌入在HTML中的资源(html,css,javascript,图片,音乐······),对于未知类型,会弹出对话框。 10、 浏览器发送异步请求。 11、页面全部渲染结束
4、TCP和UDP的区别
UDP:
- 用户数据报文协议(UDP,User Datagram Protocol)
- UDP 是⼀个⾯向报⽂(报⽂可以理解为⼀段段的数据)的协议。意思就是UDP 只是报⽂的搬运⼯,不会对报⽂进⾏任何拆分和拼接操作
具体来说:
- 在发送端,应⽤层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加⼀个 UDP 头标识下是 UDP 协议,然后就传递给⽹络层了 在接收端,⽹络层将数据传递给传输层, UDP 只去除 IP 报⽂头就传递给应⽤层,不会 任何拼接操作
TCP:
- TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议
- TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包
区别:
连接性:
- UDP 是⽆连接的,也就是说通信不需要建⽴和断开连接。
- UDP只提供端到端的数据传递,不做可靠性校验,因此不需要连接去确认
- 因此UDP的缺点不可靠,不稳定。
- TCP是面向连接,也就是说客户和服务器交换数据前,必须现在双方之间建立一个TCP连接,之后才能传输数据
- TCP协议需要保证数据传输可靠,那么我们通过连接(三次握手)这个过程确定,双方都可以收发数据。如果连接都连接不上,那么就可以确定双方的通信不可达,建立连接失败,没办法可靠的传输。
可靠性:
- TCP是可靠,UDP不可靠
报文:
- TCP面向字节流,UDP面向报文
效率:
- TCP传输效率低,UDP传输效率高
- 因为UDP支持一对一,一对多,多对一和多对多交互通信,而TCP连接只能一对一通信
- .TCP为了可靠性保证,增加了3次握手4次挥手,复杂的拥塞控制,以及流量控制,让网络传输的延迟进一步增加。
- TCP头部的大小比UDP复杂,也会影响传输的数据量
场景
-
UDP 应用场景:
- 当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用 UDP。
- 语音、视频
- 当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用 UDP。
-
TCP的应用场景:
- 当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方
- 文件传输、浏览器的HTTP协议
- 当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方
5、WebSocket 使用的是 TCP 还是 UDP 协议
- websSocket是基于TCP的协议的,不可能是UDP协议,UDP 是⽆连接的,也就是UDP说通信不需要建⽴和断开连接。而webSocket是客户端和服务器之间的连接
网络安全
网络安全一般都是后端来做,前端来做是没有意义的,不过前端还是需要了解的,需要与后端沟通和配合,前端面试是会问到的
同源策略
如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。
- 同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。
- 同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。
- 同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。
同源策略限制内容有:
吴瀚清所著《白帽子讲web安全》中有关浏览器安全的一段话,对其中“不同于XMLHttpRequest的是,通过src属性加载的资源,浏览器限制了JavaScript的权限,使其不能读写返回的内容”
JavaScript在读写数据时受到同源策略的限制,不可以读写其他域的数据
(同源策略限制了客户端javascript代码的部分行为)
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
解决同源策略的方法:
跨文档消息机制:可以通过 window.postMessage 的 JavaScript 接口来和不同源的 DOM 进行通信。跨域资源共享(CORS):跨域资源在服务端设置允许跨域,就可以进行跨域访问控制,从而使跨域数据传输得以安全进行。内容安全策略(CSP):主要以白名单的形式配置可信任的内容来源,在网页中,能够使白名单中的内容正常执行(包含 JS,CSS,Image 等等),而非白名单的内容无法正常执行。
跨域
什么是跨域?
跨域,即浏览器试图执行其他网站的脚本。但是由于同源策略的限制,导致我们无法实现跨域。
关于跨域的几个问题
为什么a.wang.com访问wang.com也算跨域?
因为历史上,出现过不同的公司共用域名,a.wang.com和 wang.com不一定是同一个网站,浏览器谨慎起见,认为这是不同的源。
为什么不同端口也算跨域?
原因同上,一个端口一个公司的情况也不是没有的。
记住:安全链条的强度取决于最弱的一环,所有和安全相关的问题都要谨慎对待。
为什么两个网站的IP一样,也算跨域?
原因同上,因为 IP也是可以共用的。
为什么可以跨域使用CSS、JS和图片等?
同源策略限制的是数据访问,我们引用 CSS、JS和图片的时候,其实并不知道其内容,我们只是在引用。
<script>、<img>、<ifram>、<link>等标签都可以跨域加载资源
通过JS设置document.domain属性:
<script type="text/javascript">
document.domain = 'example.com';
</script>
跨域的解决方案
1、JSONP
- JSONP是JSON with padding(填充式JSON或参数式JSON)的简写
利用
<script>标签没有同源限制的漏洞,向服务器请求JSON数据。JSONP请求一定需要对方的服务器做支持才可以
JSONP和AJAX对比
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
JSONP优缺点
- JSONP优点是简单兼容性好 ,可用于解决主流浏览器的跨域数据访问的问题。缺点是不够安全,仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
- jsonp的缺点:只能发送get一种请求。
JSONP实现原理
JSONP的实现原理很简单,利用<script>标签没有同源限制的特点,也就是<script>的src链接可以访问不同源的。不受同源限制的还有<img>、<iframe>、<link>,对应这两个标签实现的跨域
- JSONP只能发送get请求
2、CORS
- CORS,跨域资源共享,它需要浏览器和服务器同时支持
它使用额外的HTTP头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源
3、服务器代理 (nginx反向代理)
4、webpack的proxy
5、nodejs请求转发跨域
跨域请求怎么携带cookie?
1、前端请求的时候,设置withCredentials为true,就会在请求头携带Cookie
2、服务端在response的header中配置"Access-Control-Allow-Origin"配置跨域
3、服务端在response的header中配置"Access-Control-Allow-Credentials", "true"
4、在报文里面设置set-cookie, SameSite属性设置none,前提是必须同时设置Secure属性 【也就是必须使用https】: Set-Cookie: SameSite=None; Secure=true
常见Web攻击
1、XSS
2、CSRF
3、点击劫持
4、SQL注入
5、OS注入
6、请求劫持
7、DDOS
XSS攻击
Cross Site Scripting
跨站脚本攻击
XSS (Cross-Site Scripting),跨站脚本攻击,因为缩写和 CSS(层叠样式表(英文全称:Cascading Style Sheets))重叠,所以只能叫 XSS。跨站脚本攻击是指通过 存在安全漏洞的Web网站注册用户的浏览器内运行非法的非本站点HTML标签或JavaScript进行的一种攻击。
- 注意:XSS攻击防护手段是 后端来操作
XSS会造成的影响
-
跨站脚本攻击有可能造成以下影响: 利用虚假输入表单骗取用户个人信息。
- 插入一段恶意
JavaScript脚本,窃取你的隐私信息、冒充你的身份进行操作
- 插入一段恶意
-
利用脚本窃取用户的Cookie值,被害者在不知情的情况下,帮助攻击者发送恶意请求。
- 拿到用户的Cookie,就可以干很多事情了,如何网站是通过cookie来判断是否登录的话,就不需要用户的登录和密码,拿到cookie值就会自动登录了
- 显示伪造的文章或图片。
1、获取页面数据
2、获取cookies
3、劫持前端逻辑
4、发生请求
5、偷取网站的任意数据
6、偷取用户的资料、偷取用户的登录状态、欺骗用户
7、非法转账
XSS如何产生
- 在浏览器内运行非法的非本站点HTML标签或JavaScript进行的一种攻击。
- 因为浏览器无法区分脚本是被恶意注入的还是正常的内容,它都会执行,况且
HTML非常灵活,可以在任何时候对它进行修改。
XSS攻击分类
反射型:
-
url参数直接注入
当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web服务器将注入脚本,比如一个错误信息,搜索结果等,未进行过滤直接返回到用户的浏览器上。
// 普通
http://localhost:3000/?from=china
// alert尝试 http://localhost:3000/?from=<script>alert(3)</script>
// 获取Cookie
http://localhost:3000/?from=<script src="http://localhost:4000/hack.js"></script>
// 短域名伪造 https://dwz.cn/
// 伪造cookie入侵 chrome document.cookie="kaikeba:sess=eyJ1c2VybmFtZSI6Imxhb3dhbmciLCJfZXhwaXJlIjoxNTUzNTY1MDAxO DYxLCJfbWF4QWdlIjo4NjQwMDAwMH0="
存储型:
(存储型就比反射性更厉害了,因为是长期保存在数据库)
- 存储到DB后读取时注入
又叫持久型 XSS,顾名思义,黑客将恶意
JavaScript脚本长期保存在服务端数据库中,用户一旦访问相关页面数据,恶意脚本就会被执行。常见于搜索、微博、社区贴吧评论等。
如:留言板或评论功能
- 提交留言 / 评论-> 存储到数据库 -> 读取数据 ->渲染到页面
DOM型
- 其实DOM型也属于反射性的一种,不过比较特殊,所以一般也当作一种
- 也是通过url来注入,通过JS来修改DOM
DOM 型 XSS 攻击,实际上就是前端
JavaScript代码不够严谨,把不可信的内容插入到了页面。在使用.innerHTML、.outerHTML、.appendChild、document.write()等API时要特别小心,不要把不可信的数据作为 HTML 插到页面上,尽量使用.innerText、.textContent、.setAttribute()等。
XSS攻击如何进行防护?
-
1、黑名单:一切用户输入永远都不可信,
在输出时进行验证(最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠)function escape(str) {str = str.replace(/&/g, '&')str = str.replace(/</g, '<')str = str.replace(/>/g, '>')str = str.replace(/"/g, '&quto;')str = str.replace(/'/g, ''')str = str.replace(/`/g, '`')str = str.replace(/\//g, '/')return str}富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情 况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在 太多,更加推荐使用白名单的方式
const xss = require('xss')let html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>')// -> <h1>XSS Demo</h1><script>alert("xss");</script> console.log(html)这个示例使用了xss库可以实现,可以看到在输出中保留了h1标签且过滤了script标签
-
2、将
HTML 元素内容、属性以及 URL 请求参数、CSS 值进行编码 -
3、当编码影响业务时,使用
白名单规则进行检测和过滤 -
4、使用 W3C 提出的
CSP (Content Security Policy,内容安全策略),定义域名白名单内容安全策略 (CSP, Content Security Policy) 是一个附加的安全层,用于帮助检测和缓解某些类型的攻 击,包括跨站脚本 (XSS) 和数据注入等攻击。 这些攻击可用于实现从数据窃取到网站破坏或作为恶意软 件分发版本等用途。
CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少XSS 攻击
// 只允许加载本站资源Content-Security-Policy: default-src 'self'// 只允许加载 HTTPS 协议图片Content-Security-Policy: img-src https://*// 不允许加载任何来源框架Content-Security-Policy: child-src 'none' -
5、设置
Cookie 的 HttpOnly属性这是预防XSS攻击窃取用户cookie最有效的防御手段。Web应 用程序在设置cookie时,将其属性设为HttpOnly,就可以避免该网页的cookie被客户端恶意JavaScript窃取,保护用户cookie
信息
(即:cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息)
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly") -
6、(浏览器自带防御)设置HEAD
ctx.set('X-XSS-Protection', 0) // 禁用XSS过滤X-XSS-Protection: 0禁止XSS过滤。X-XSS-Protection: 1启用XSS过滤(通常浏览器是默认的)。 如果检测到跨站脚本攻击,浏览器将清除页面(删除不安全的部分)。X-XSS-Protection: 1; mode=block启用XSS过滤。 如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。X-XSS-Protection: 1; report=<reporting-uri>启用XSS过滤。 如果检测到跨站脚本攻击,浏览器将清除页面并使用CSP report-uri指令的功能发送违规报告
CSRF攻击
CSRF(Cross Site Request Forgery),即跨站请求伪造,是一种常见的Web攻击,它利用用户已登录的身份, 在用户毫不知情的情况下,以用户的名义完成非法操作,发送恶意的请求,类似于钓鱼
- 钓鱼网站。这个网站上面可能因为某个图片吸引你,你去点击一下,此时可能就会触发一个的点击事件请求,构造一个发帖、发评论的请求,由于你已经登录了,利用你当前的登录状态,去执行相关功能操作
-
用户已经登录了站点 A,并在本地记录了 cookie ,并创建了session
-
在用户没有登出站点 A 的情况下(也就是 cookie 生效的情况下),点击访问了恶意攻击者提供的引诱危险站点 B (B 站点要求访问站点A)。(利用用户的登录状态来进行相关的请求 -> 就可以进行相关的功能操作了)
- 这个时候就会有一个疑问,浏览器本身有同源策略啊,为什么在网站b还可以请求网站a的api,要记住浏览器对img、iframe和script的src是没有同源限制的!所以黑客完全可以利用动态添加这些标签的方法来实现跨站请求伪造。
- CSRF并没有打破同源策略。同源策略限制了站点不能读取不同源的资源例如cookie,但是没有限制不能向不同源站点发送请求
- 这个时候就会有一个疑问,浏览器本身有同源策略啊,为什么在网站b还可以请求网站a的api,要记住浏览器对img、iframe和script的src是没有同源限制的!所以黑客完全可以利用动态添加这些标签的方法来实现跨站请求伪造。
-
站点 A 没有做任何 CSRF 防御
CSRF的攻击方式
-
HTML CSRF
-
通过HTML标签可以发起get请求
-
<link href=''><img src='' /><frame src='' /><script src='' /><video src='' />
-
-
CSS 的背景图片
background-image:url('') -
JSON HiJacking :JSON 劫持
-
HiJacking:劫持
-
构造自定义的回调函数
-
JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,而数据就是传入回调函数中的JSON数据。
-
<script>function hiJack(data){console.log(data)}<script/><script scr='http://www.a.com/json?callback=hijack'><script/>
-
-
Flash CSRF
- 通过Flash来实现跨域请求
- 利用flash的http请求
- 构造的Flash文件
- flash文件(.swf)包含着json格式的数据(攻击者想要发送给目标的数据),并链接到某个放在服务器上的文件
- 通过Flash来实现跨域请求
CSRF会造成影响
- 1、利用用户登录态 用户
- 2、不知情 完成业务请求
- 3、盗取用户资金(转账,消费)
- 4、冒充用户发帖背锅 损害网站声誉
CSRF的预防措施
-
验证码
- 这个方案的思路是:每次的用户提交都需要用户在表单中填写一个图片上的随机字符串,这个方案可以完全解决CSRF
-
所有表单都包含同一个伪随机值
-
增加请求参数token
- 可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。
- 但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上
<input type="hidden" name="csrftoken" value="tokenvalue"/> - 常使用的方法就是在每次页面加载时,使用 javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 html 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token。
-
检查请求来源
-
一般地,网络请求头都会有了Referer,可以告诉服务器我是从哪个页面链接过来的(请求的来源是哪里),服务器就可以获得一些信息用于处理检查来源
-
检查是否从本域名链接过来的,如果不是,则拒绝请求,也就说任何请求只能在本域执行
-
Referer 的正确英语拼法是
referrer。由于早期HTTP规范的拼写错误,为了保持向后兼容就将错就错了。其它网络技术的规范企图修正此问题,使用正确拼法,所以目前拼法不统一。还有它第一个字母是大写
-
XSS和CSRF的区别
- XSS:利用对用户的输入的不严谨然后执行非法的JS
- CSRF:CSRF可以不用JS就能达到目的
- CSRF需要登陆后操作,XSS不需要
点击劫持 (clickjacking)
点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中, 并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击
典型点击劫持攻击流程
1、攻击者构建了一个非常有吸引力的网页【不知道哪些内容对你们来说有吸引力,我就不写页面了,偷个懒】
2、将被攻击的页面放置在当前页面的 iframe标签 中
3、使用样式将 iframe 叠加到非常有吸引力内容的上方
4、将iframe设置为100%透明
你被诱导点击了网页内容,你以为你点击的是,而实际上,你成功被攻击了
预防策略:
1、服务端添加 X-Frame-Options 响应头,这个 HTTP 响应头是为了防御用 iframe 嵌套的点击劫持攻击。 这样浏览器就会阻止嵌入网页的渲染。
该响应头有三个值可选,分别是
- DENY,DENY意思是不允许、拒绝,表示页面不允许通过 iframe 的方式展示
- SAMEORIGIN 表示页面可以在相同域名下通过 iframe 的方式展示
- ALLOW-FROM 表示页面可以在指定来源的 iframe 中展示
ctx.set('X-FRAME-OPTIONS', 'DENY')
2、JS 判断顶层视口的域名是不是和本页面的域名一致,不一致则不允许操作,top.location.hostname === self.location.hostname;
3、敏感操作使用更复杂的步骤(验证码、输入项目名称以删除)。
SQL注入
拼接 SQL 时未仔细过滤,黑客可提交畸形数据改变语义。比如查某个文章,提交了这样的数据
id=-1 or 1=1等。1=1 永远是true,导致where语句永远是ture.那么查询的结果相当于整张表的内容,攻击者就达到了目的。或者,通过屏幕上的报错提示推测 SQL 语句等。
// 填入特殊密码
1'or'1'='1
// 拼接后的SQL
SELECT *
FROM test.user
WHERE username = 'laowang' AND password = '1'or'1'='1'
防止SQL注入
所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌 入到 SQL 语句中,即不要直接拼接 SQL 语句。例如 Node.js 中的 mysqljs 库的 query 方法中的 ? 占位符号参 数。
// 错误写法
const sql = `
SELECT *
FROM test.user
WHERE username = '${ctx.request.body.username}' AND password = '${ctx.request.body.password}'
`
console.log('sql', sql) res = await query(sql)
// 正确的写法 const sql = ` SELECT *
FROM test.user WHERE username = ? AND password = ?
`
console.log('sql', sql, )
res = await query(sql,[ctx.request.body.username, ctx.request.body.password])
OS命令注入
OS命令注入和SQL注入差不多,只不过SQL注入是针对数据库的,而OS命令注入是针对操作系统的。OS命令注入攻击指 通过Web应用,执行非法的操作系统命令达到攻击的目的。只要在能调用Shell函数的地方就有存在被攻击的风险。倘若调用Shell时存在疏漏,就可以执行插入的非法命令。
// 以 Node.js 为例,假如在接口中需要从 github 下载用户指定的 repo const exec = require('mz/child_process').exec;
let params = {/* 用户输入的参数 */};
exec(`git clone ${params.repo} /some/path`);
- 如果传入的参数是会怎样
https://github.com/xx/xx.git && rm -rf /* &&
请求劫持
DNS劫持:
顾名思义,DNS服务器(DNS解析各个步骤)被篡改,修改了域名解析的结果,使得访问到的不是预期的ip
HTTP劫持 :运营商劫持,此时大概只能升级HTTPS了
DDOS
- 阮一峰:http://www.ruanyifeng.com/blog/2018/06/ddos.html
- distributed denial of service
DDOS 里面的 DOS 是 denial of service(停止服务)的缩写,表示这种攻击的目的,就是使得服务中断。最前面的那个 D 是 distributed (分布式),表示攻击不是来自一个地方,而是来自四面八方,因此更难防
- DDOS 不是一种攻击,而是一大类攻击的总称。它有几十种类型,新的攻击方法还在不断发明出来。网站运行的各 个环节,都可以是攻击目标。只要把一个环节攻破,使得整个流程跑不起来,就达到了瘫痪服务的目的
常见攻击方式
- SYN Flood
此攻击通过向目标发送具有欺骗性源IP地址的大量TCP“初始连接请求”SYN数据包来利用TCP握手。目标机器 响应每个连接请求,然后等待握手中的最后一步,这一步从未发生过,耗尽了进程中的目标资源。
- 简单来说:服务器TCP连接资源耗尽,停止响应正常的TCP连接请求。
- HTTP Flood
此攻击类似于同时在多个不同计算机上反复按Web浏览器中的刷新 - 大量HTTP请求泛滥服务器,导致拒绝服 务
防御手段
- 备份网站
备份网站不一定是全功能的,如果能做到全静态浏览,就能满足需求。最低限度应该可以显示公告,告诉用户,网 站出了问题,正在全力抢修。
- HTTP 请求的拦截 高防IP -靠谱的运营商 多个 Docker
硬件 服务器 防火墙
- 带宽扩容 + CDN
提高犯罪成本
防御手段
1、密码安全 - 密码加固
2、密码学 (对称与非对称)
3、传输安全 -HTPPS
4、Node.js安全框架hemelt
5、CSP策略 content-security-policy
密码安全
- 密码安全
- 泄露渠道
- 数据库被偷
- 服务器被入侵
- 通讯被窃听
- 内部人员泄露
- 其他网站(撞库)
- 防御
- 严禁明文存储
- 单向变换
- 变换复杂度要求
- 密码复杂度要求
- 加盐(防拆解)
- 哈希算法
- 明文 - 密文 - 一一对应
- 雪崩效应 - 明文小幅变化 密文剧烈变化
- 密文 -明文无法反推
- 密文固定长度 md5 sha1 sha256
- 密码传输安全
- https传输
- 频次限制
- 前端加密意义有限 - 传输层加密 不会泄露 但不代表不能登录
- 摘要加密的复杂度
- md5反查
思考如下问题?
1、2次MD5是否可取?
- 答案: 不可取, 因为2次MD5并不能解决被反推明文的本质性问题,这是因为你明文相对简单,如果明文相对复杂的话,基本上不能反推明文了
2、只加盐好不好?
- 答案:不好
- 数据库泄露了,盐就会被暴露了
- 要防止盐泄露的情况
3、盐泄露是否会泄露密码?
- 答案: 不会
- 在不知道程序的情况下,是不会泄露
4、密码很复杂还需要保证密码的复杂性吗?
- 答案:需要
- 如果你的数据库和程序都被攻破了,那唯一保证密码不被丢失的就是密码复杂度
说说彩虹表攻击?
-
彩虹表:庞大的,预先计算好的哈希值集合,即明文和密文对应的表
-
彩虹表(Rainbow Table)是一种破解哈希算法的技术,是一款跨平台密码破解器,主要可以破解MD5、HASH等多种密码。它的性能非常让人震惊
- 拿到密码的哈希值时,从哈希值反查原始密码
人机验证与验证码
滑动验证码实现原理 1.服务端随机生成抠图和带有抠图阴影的背景图片,服务端保存随机抠图位置坐标; 2.前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值; 3.前端将用户滑动距离值传入服务端,服务端校验误差是否在容许范围内
关于Web密码学你了解哪些呢?
- 对称加密算法
- 对称加密算法就是加密和解密使用同一个密钥,简单粗暴
- 常见的经典对称加密算法有
DES、AES(AES-128)、IDEA、国密SM1、国密SM4
- 非对称加密算法
- 非对称加密就是加密和解密使用不同的密钥。发送方使用公钥对信息进行加密,接收方收到密文后,使用私钥进行解密。
- 主要解决了密钥分发的难题
- 我们常说的签名就是私钥加密
- 常见的经典非对称加密算法有
RSA、ECC和国密SM2
- 散列算法(加盐处理)
- 不可逆性、鲁棒性、唯一性
MD5、SHA(SHA-256)、国密SM3- 使用时记得加盐 (原来的密码里,加入一些其他的字符串,并且我们可以自己设置加入字符串的强度)
AES 是国际上最认可的密码学算法,只要算力没有极大的突破性进展,这种算法在可预期的未来都是安全的。
ECC 是目前国际上加密强度最高的非对称加密算法。
MD5 和 SHA 的唯一性被破解了,但是大部分场景下,不会构成安全问题。一般使用 SHA-256 加盐即可满足大部分使用场景。
网络请求
并行请求
- 简单来说就是同一个接口,我们需要维护多个请求状态
并行请求有几个特点
- 删除 n 个不同的用户,则需要维护 n 个请求状态。
- 多次删除同一个用户,则只需要维护最后一个请求。
解决:
- 可对请求进行分类。相同分类的请求,只会维护一份状态。不同分类的请求,则会维护多份状态
防抖 & 节流
- 通常在边输入边搜索的场景中,我们会用到防抖功能,以节省不必要的网络请求。
屏幕聚焦重新请求
- 它可以保证多个 tab 间数据的同步性。也可以解决长间隔之后重新打开网站的数据新鲜度问题。
轮询
- 对于需要保持新鲜度的数据,我们通常需要不断发起网络请求以更新数据
缓存 & SWR & 预加载
swr 是
stale-while-revalidate的简称,最主要的能力是:我们在发起网络请求时,会优先返回之前缓存的数据,然后在背后发起新的网络请求,最终用新的请求结果重新触发组件渲染。swr 特性在特定场景,对用户非常友好
在 SWR 场景下,我们会对接口数据进行缓存,当下次请求该接口时,我们会先返回缓存的数据,同时,在背后发起新的网络请求,待新数据拿到后,重新触发渲染。
options请求了解过吗
- option方法对服务器发起请求,可以检测服务器支持哪些 HTTP 方法
options请求自动发起
MDN的CORS一文中提到:
浏览器必须首先使用options 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。
所以这个跨域请求触发了浏览器自动发起OPTIONS请求,看看此次跨域请求具体触发了哪些条件。
总结:
options请求即预检请求,可用于检测服务器允许的http方法。当发起跨域请求时,由于安全原因,触发一定条件时浏览器会在正式请求之前自动先发起OPTIONS请求,即CORS预检请求,服务器若接受该跨域请求,浏览器才继续发起正式请求
优化options请求
- options预检请求的结果可以被缓存
- Access-Control-Max-Age这个响应首部表示 preflight request (预检请求)的返回结果(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息) 可以被缓存的最长时间,单位是秒。(MDN)
Referer理解
- Referer 请求头,表示当前页面是通过来源页面的链接进去的,可以告诉服务器该网页时从哪个页面链接过来的
- 服务端一般使用
Referer请求头识别访问来源
一般来说,页面发起的 http 请求,referer字段的配置,通过前端代码是无法干预的,是由浏览器自主设置添加的,当然了,如果不想携带referer字段,可在浏览器的设置中关掉即可
作用
-
防盗链
- 将这个http请求发给服务器后,如果服务器要求必须是某个地址或者某几个地址才能访问,而你发送的referer不符合他的要求,就会拦截或者跳转到他要求的地址,然后再通过这个地址进行访问
-
防止恶意请求
- 必须规定指向某一个来源地址才能请求接口
Docket
为什么需要学习Docker?
- 部分企业都在采用Docker来实现软件开发部署中的自动化和部署效率安全等问题
- 需要掌握一定的Docker技术来更好的配合后端和运维来推进这一过程
简介
参考:https://jspang.com/detailed?id=75#toc246
-
doker是一个容器,会把应用程序放在一个独立运作的容器中
-
有了docker,不用在意目标环境的情况,只需要保证docker容器运行正常,就可以将它放在任何机器中
简单来说:**容器就想一个打包工具,打包的不仅是你的程序,也包括运行环境。**就好比你把你的开发主机和开发程序完全复制了一份,别人拿到后就不需要搭建环境,也不需要根据环境进行调试,直接就可以运行了
安装:
- 1、直接去Docker官网进行下载 https://docker.com
- 2、进入官网后,选择
Products(产品),然后选择Docker Desktop,进入下载页面,点击进行下载
注意:
- window需要开启BIOS虚拟化设置、Hyper-V和容器
原理:
docker命令
- 安装好docker,查看版本
docker -v
- 创建容器 e.g: docker container run nginx
docker container run < image name >
- 查看所有容器
docker container ls -a
- 获取容器日志
- -f : 跟踪日志输出
docker logs -f gitlab
- 从 Dockerfile 构建镜像 参考:https://docs.docker.com/engine/reference/commandline/image_build/
docker image build -t xx
- 运行创建docker容器
- -i : 以交互模式运行容器,通常与 -t 同时使用 【交互interactive】
- -t : 为容器重新分配一个伪输入终端,通常与 -i 同时使用 【终端terminal】
- -d : 以守护进程的方式让容器在后台运行 【守护defend】
- --name : 为容器指定名称
- --privileged: 让容器的用户在容器内能获取完全root权限
- -p: 指定端口映射,格式为:主机(宿主)端口:容器端口
- /bin/bash : 在容器内执行/bin/bash命令
docker run -it -d --name myvue2 --privileged -p 8081:8080 -v /Users/eric/my-repository/my-app-vue2:/app/vue node:10.16.2 /bin/bash -c "cd /app/vue && node -v && npm install && npm run serve"
- 查看现在拥有的镜像列表
docker image ls
- 获取镜像
# 查看命令语法
docker pull
# 格式 docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
docker pull ubuntu:18.04
docker pull nginx
- 查看可用镜像
# 输入docker search命令搜索 Docker Hub 上可用的nginx、Jenkins 镜像:
docker search nginx
docker search jenkins
- 镜像推送到Cockerhub上, 其他人就可以使用你制作的镜像了
# 先登录Dockerhub账号
docker login
# 把q/node镜像推送到社区
docker image push q/node
创建容器
镜像和容器
镜像:就是像是我们装机时候需要的系统盘或者系统镜像文件,这里它负责创建docker容器的,有很多官方现成的镜像:node、mysql、monogo、nginx可以从远程仓库下载,可以把镜像看成面向对象编程中的只读类(read-only Class)。
容器和镜像几乎一模一样,唯一的区别是镜像是只读的,而容器上面有一个可读写层。所以容器=镜像+读写层。可以比拟成一个迷你的系统,例如一个只安装mysql5.7的linux最小系
创建容器
image代表一个镜像的名称
docker container run < image name >
如果你想使用的镜像名称是nginx,就可以写成下面的样子。
docker container run nginx
如果是使用ubuntu镜像,命令就变成了下面的写法。
docker container run ubuntu
输入完成后,直接回车。如果系统中没有这个镜像,Docker会自动去Docker Hub上拉取对应的镜像到本地,然后执行对应的Shell脚本,脚本会把镜像自动安装到Doker容器里,并最终启动对于的镜像服务。
Docker Hub 是Docker官方的镜像和社区,里边有很多开发者制作好的镜像,我们可以直接使用这些镜像。如果你有能力,也可以制作镜像,并上传到Docker Hub。
运行docker容器
run命令运行创建docker容器
- -i : 以交互模式运行容器,通常与 -t 同时使用 【交互interactive】
- -t : 为容器重新分配一个伪输入终端,通常与 -i 同时使用 【终端terminal】
- -d : 以守护进程的方式让容器在后台运行 【守护defend】
- --name : 为容器指定名称, 默认是随机
- --privileged: 让容器的用户在容器内能获取完全root权限
- -p: 指定端口映射,格式为:主机(宿主)端口:容器端口
- /bin/bash : 是在让容器分配的虚拟终端以 bash 模式执行命令
- -v :绑定一个卷 【volume】
- -c: CPU 份额
docker run -it -d --name myvue2 --privileged -p 8081:8080 -v /Users/eric/my-repository/my-app-vue2:/app/vue node:10.16.2 /bin/bash -c "cd /app/vue && node -v && npm install && npm run serve"
docker run只能创建启动一个docker容器,我们可以用docker-compose来一次启动多个容器,常用于单机下安装多个服务
Dockerfile理解
Dockerfile是一个包含用于组合映像的命令的文本文档。可以使用在命令行中调用任何命令。 Docker通过读取Dockerfile中的指令自动生成映像。
可以简单总结为下面三点:
- Dockerfile是用于构建docker镜像的文件
- Dockerfile里包含了构建镜像所需的”指令“
- Dockerfile有其特定的语法规则(重要学习)
Dockerfile语法
- FROM: 指定基础镜像
- RUN:执行命令
# 基于ubuntu最新的版本镜像
FROM ubuntu:latest
# 下载安装Python环境
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y python3.9 python3-pip python3.9-dev
# 添加脚本文件
ADD jspang.py /
# 运行脚本
CMD ["python3","jspang.py"]
文件操作
COPY和ADD
COPY和ADD命令,在复制普通文件的时候,并没有什么太大的不同,两个命令都可以把本地文件,复制到镜像里,ADD 构建镜像和COPY最直观的一点不同,是ADD命令可以直接解压gzip压缩文件,这当我们有很多文件要上传操作的时候,就会变的简单很多。 【可以说ADD 命令基本上是 COPY 命令的超集】
- COPY
FROM node:14
# 把index.js文件,拷贝到app目录下面
COPY index.js /app/index.js
- ADD
- ADD 命令可以完成 COPY 命令的所有功能
- ADD还可以支持解压gzip压缩文件
FROM node:14
# 把index.tar解压拷贝到app目录下面
ADD index.tar /app/
ARG 和ENV
ARG和ENV是经常容易被混淆的两个Dockerfile语法,它们都可以用来设置一个“变量”
ARG和ENV的不同点:
总的来说ARG和ENV有两点不同,第一点是声明变量的作用域不同,第二点是ARG声明后,可以在构建时修改变量。
构建镜像
- 社区上如果没有合适的镜像,我们通常用Dockerfile来构建自定义镜像
构建的镜像,镜像里面包含所需要的环境
编写Dockerfile
Dockerfile文件(不需要后缀),键入以下指令
# 基于node11基础镜像
FROM node:11
COPY . ./demo
#类似于 `cd /demo`
WORKDIR /demo
# 一些元数据,比如作者信息
LABEL maintainer="deepred5 <deepred5@gamil.com>"
# 安装pm2
RUN npm install pm2 -g --registry=https://registry.npm.taobao.org
#容器启动后执行`node index.js`命令
CMD node index.js
# 暴露容器的端口
EXPOSE 80 443
FROM node:11就是说我们的镜像会继承 node:11 这个镜像WORKDIR /demo类似于cd /demo。CMD node index.js容器启动后执行node index.js命令。
创建镜像
build命令创建镜像
-
**-f :**指定要使用的Dockerfile路径;
-
--tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签
基于这个Dockerfile创建我们自己的镜像deepred5/node-pm2
docker build -t demo .
注意最后有一个.
查看我们自己的镜像
# 可以看到demo镜像了
docker images
基于demo镜像启动一个容器
docker run -it demo
进入容器后,我们运行pm2 -v,可以看见pm2已经安装成功了
上传镜像
我们本地构建的镜像如果希望可以被其他人使用,就需要把镜像上传到仓库。登录dockerhub,注册一个账户。
# 登入账户,输入用户名和密码
docker login
# 上传镜像
docker push deepred5/node-pm2:1.0
volume持久化
docker run的--volume/-v或--mount选项来创建带有数据卷的容器
--volume(或简写为-v)只能创建bind mount--mount : 参数
--mount默认情况下用来挂载volume,但也可以用来创建bind mount和tmpfs。如果不指定type选项,则默认为挂载volume,volume是一种更为灵活的数据管理方式,volume可以通过docker volume命令集被管理
volume命令
# 查看下主机上的卷
docker volume ls
# docker run命令启动一个新的容器,并且挂载 myvolume 卷
docker run -d --name=nginx --mount source=myvolume,target=/usr/share/nginx/html nginx
Docker Compose
- 可以运行多个Docker容器的应用
- 可以通过一个单独的
docker-compose.yml模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。- 执行这个docker-compose.yml文件就可以启动我们想要的容器
在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等
docker-compose 中有两个重要的概念:
- 服务 (
service):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。 - 项目 (
project):由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yml文件中定义。
docker-compose命令:
# 启动容器
docker-compose up
# 停止容器
docker-compose stop
安装
- 如果你是Windows或者Mac安装了docker 桌面版本(desktop)以后,就默认安装了docker-compose工具了。可以直接进行使用
docker-compose --version
- linux用户可在官方推荐方法安装
Linux系统默认是没有安装docker-compose工具的,可以进入下面的网址。
https://docs.docker.com/desktop/
Product Manuals—>Docker compose—>Liunx后,可以看到三条命令,依次执行就可以安装docker-compose工具了
docker-compose.yml说明
云服务
在镜像仓库,把手动创建的镜像,部署到云端上
云服务:
- daocloud
docker资源限制配置
docker运行nginx
nginx -g "daemon off;"理解
前台运行(daemon off)、后台运行(daemon on)
1、docker容器跑着为啥会挂掉?
- docker 容器默认会把容器内部第一个进程,也就是pid=1的程序作为docker容器是否正在运行的依据,如果docker 容器pid挂了,那么docker容器便会直接退出。
2、为什么要加 nginx -g "daemon off;" ?
-
当运行nginx的时候,nginx的pid进程为1,执行其他命令之后,pid为1进程不再是nginx,nginx会在后台运行,一旦执行完其他自定义命令之后,nginx容器也就退出了
-
因为要让容器能持续运行, 必须要有前台进程,常规的nginx是后台守护进程,设置为daemon off将nginx在前台运行, 让Docker 可以正确跟踪进程
遇到问题
解决部署时间长
构建镜像时间过长、构建镜像大小过大
- 利用镜像缓存
1、相对于项目的源文件来讲,package.json 是相对稳定的。如果没有新的安装包需要下载,则再次构建镜像时,无需重新构建依赖。则可以在 npm install 上节省一半的时间
2、对于 ADD 来讲,如果需要添加的文件内容的 checksum 没有发生变化,则可以利用缓存。把 package.json/package-lock.json 与源文件分隔开写入镜像是一个很好的选择。目前,如果没有新的安装包更新的话,可以节省一半时间
- CI 环境下的优化
- 多阶段构建
- 使用对象存储服务 (OSS)
总结:
1、镜像中使用基于 alpine 的镜像,减小镜像体积。 2、镜像中需要锁定 node 的版本号,尽可能也锁定 alpine 的版本号,如 node:10.19-alpine3.11。(我示例代码中未如此详细地指出) 3、选择合适的环境变量 NODE_ENV 及 PROJECT_ENV,如在测试环境下进行构建 4、npm ci 替代 npm i,避免版本问题及提高依赖安装速度 5、package.json 单独添加,充分利用镜像缓存 6、使用多阶段构建,减小镜像体积
7、如有必要,静态资源请上 CDN
应用场景
构建自己的前端开发镜像
创建一个react项目,如使用create-react-app来搭建,在项目根目录创建Dockerfile文件
#基于node:15.10.0-slim镜像
FROM node:15.10.0-slim镜像
# 将当前目录拷贝到镜像的/demo目录
COPY . ./demo
# 类似于 cd /demo
WORKDIR /demo
# 执行命令
RUN npm install
# 容器启动后执行命令
CMD npm run start
存在问题?
可以看到,按开发的模式来说,这样是存在问题的。比如:
- 将代码打包进镜像中,每次修改代码都必须要
重新构建镜像 - 打包出来的镜像指包含node环境,
缺少开发时需要的依赖
我们希望的是,通过容器来提供开发时需要的依赖,然后通过vscode修改的代码实时跑在容器中。
构建自己开发镜像
要让我们的镜像提供开发时需要的依赖,其实就是让我们的镜像包含node(typescript)、git就行了。
FROM node:15.10.0-slim
RUN apt-get update && apt-get install -y git
RUN npm install -g typescript
构建镜像:docker image build -t frontend-dev .。
运行容器: docker container run -it frontend-dev /bin/bash。
进入到容器,就可以看到我们安装的程序了。
挂载点:
- docker提供了挂载点,可以让容器访问我们本机的文件系统。
只需要在我们的项目中启动容器时指定-v 参数就行,即
- -v是绑定一个卷 【volume】
- 运行一个容器并且将当前机器的"${pwd}:/demo"目录绑定到容器的匿名卷中
- 启动frontend-dev容器,并绑定到容器的"${pwd}:/demo"目录中
`docker container run -v "${pwd}:/demo" -it frontend-dev /bin/bash`。
frontend-dev启动的容器多了/demo目录
配合vscode使用容器开发
-
使用Remote-Containers插件
-
只要机器装了docker,启动了容器,就可以使用了
-
Remote-Containers插件会打开你启动的容器镜像
使用 docker 高效部署你的前端应用
安装 docker-compose
docker-compose可以运行多个docker容器的应用
安装 nginx及jenkins 镜像
# 输入docker search命令搜索 Docker Hub 上可用的nginx、Jenkins 镜像:
docker search nginx
docker search jenkins
编写Dockerfile
- 构建docker镜像
- 多阶段镜像构建
- 将构建分为两个阶段,第一阶段基于 node 镜像,第二阶段基于 nginx 镜像
# build stage
# 基于node版本, 将镜像命名为build-state
FROM node:latest as build-stage
# 类似于 `cd /app` 切换到app目录中
WORKDIR /app
# 把package.json文件拷贝到app目录中
COPY package.json .
# 在容器里执行脚本命令
RUN npm install
# 复制当前目录下的所有文件到app目录下,分两次拷贝是因为保持 node_modules 一致
COPY . .
# 运行脚本编译
RUN npm run build
# production stage
# 构建镜像基于nginx:latest镜像 ,将镜像命名为build-state
FROM nginx:latest as production-stage
# 通过 --form 参数可以引用 build-stage 阶段生成的产物拷贝到nginx的目录里
COPY --from=build-stage /app/dist /usr/share/nginx/html
# 暴露端口 如果docker run -P 就会指定该端口
EXPOSE 80
# 让nginx容器持续运行
CMD ["nginx", "-g", "daemon off;"]
docker部署jenkins自动化部署
基于dokcer的pipline流水线
构建应用镜像到镜像仓库管理
解决不同项目安装不同版本
传统方式直接安装的话,虽然简单粗暴,但是会污染全局环境、多版本情况不好处理
- 项目1: node10 + webpack
- 项目2: node12 + vite
- 项目3:node 11 + mysq8
- 项目4:node7+mysql5.7
写好配置后,它一个命令就在电脑上同时安装不同版本的nodej、msyql、nginx 等而且是互相隔离独立同时运行,这时候还不需要你手动来回切换、
使用Nginx做一个接口转发
我们需要在开发中需要用到该接口:https://yapi.thisjs.com/mock/21/antd/games,我需要使用nginx对其进行一些代理配置,这样我们只需要创建一个.conf文件,然后使用Docker启动一个Nginx挂载该conf文件到conf.d/目录下即可。
基于docker构建ci/cd平台
docker-compose一键部署启动
- 使用docker-compose可以同时管理多个服务,可以启动后端项目、前端项目、数据库
面试问题
为什么需要 Docker 部署?
用以隔离环境。
假设你有三个后端服务,分别用 Java、Go、Node 编写,你需要在服务器分别安装三者的环境,才能运行所有语言编写的代码,这对于开发者而言非常麻烦。
假设你有三个 Node 服务,分别用 node10、node12、node14 编写,你需要在服务器分别安装三个版本 nodejs 才能运行各个版本 nodejs 编写的代码,对于开发者而言也非常麻烦。
而有了 Docker,就没有这种问题,它可单独提供某种语言的运行环境,并同时与宿主机隔离起来。
Docker 在 CI/CD 中的作用?
Docker可以 贯穿CI/CD中整个流程- 可以使用
Docker将应用打包成一个镜像,交给Kubernetes【k8s】去部署在目标服务集群。并且可以将镜像上传到自己的镜像仓库,做好版本分类处理
k8s和docker的理解
k8s 实际上是一个使用 Docker 容器进行编排的系统,主要围绕 pods 进行工作。Pods 是 k8s 生态中最小的调度单位,可以包含一个或多个容器
没有 k8s 可以使用 docker 吗
可以。实际上一些小型公司,在业务不太复杂的情况下都是直接使用 Docker。尽管 k8s 有很多好处,但是众所周知它非常复杂,业务比较简单可以放弃使用 k8s。
没有 Docker 可以使用 k8s 吗?
k8s 只是一个容器编排器,没有容器拿什么编排?!
k8s 经常与 Docker 进行搭配使用,但是也可以使用其他容器,如RunC、Containerted 等。
docker的多阶段构建镜像
推动CI/CD前端工作流
具备CI/CD前端自动化部署工作流能力
可基于docker构建ci/cd平台
- Continuous Integration (CI) 持续集成
- 让产品可以快速迭代,同时还能保证高质量,
- Continuous Delivery (CD) 持续交付
- 持续交付可以看做持续集成的下一步,它强调的是不论怎么更新,软件都是随时随地可以交付的
- Continuous Deployment (CD) 持续部署
- 持续部署是持续交付的下一步,持续部署的目标是,软件在任何时刻都是可以部署的,可以进入生产阶段
-
前端具备自己的构建部署系统,便于更好的流程控制
-
减少重复劳动力
-
更好,更快,更加安全,更加稳定的交付
-
在CI的过程中可以进行代码质量的自动检测,减少人工检查的劳动力
-
打包环境一致,不会出现编译后代码异常
-
减少等待时间
-
推动CI/CD前端工作流
具备CI/CD前端自动化部署工作流能力
- 前端具备自己的构建部署系统,便于更好的流程控制
- 减少重复劳动力
- 更好,更快,更加安全,更加稳定的交付
- 在CI的过程中可以进行代码质量的自动检测,减少人工检查的劳动力
- 打包环境一致,不会出现编译后代码异常
- 减少等待时间
Jenkins CI/C
Jenkins具有自动化构建、部署等能力
- 安装Jenkins
- 创建任务
- 配置git钩子功能
- 添加触发器
参考:
- docker jenkins文档: https://hub.docker.com/_/jenkins
安装jenkins
docker run -u root -itd --name jenkins -p 9000:8080 -v $(which docker):/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock -v /volume1/docker/jenkins:/var/jenkins_home jenkins/jenkins
启动命令含义 -u root: 权限等级为root -p 9000:8080 Jenkins默认网页访问端口为8080,将端口映射到外部主机端口9000 -v $(which docker):/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock使Jenkins内部可以使用docker命令 -v /volume1/docker/jenkins:/var/jenkins_home 将Jenkins的配置映射到外部主机卷,容器删除仍可保留配置
等浏览器初始化jenkins成功后,会提示需要一个初始密码,这个时候,我们可以
到/volume1/docker/jenkins/secrets/initialAdminPassword 查看密码
启动jenkins
# 启动jenkins
docker start jenkins
# 查看密码
cat /volume1/docker/jenkins/secrets/initialAdminPassword
# 查找jenkins配置文件
find / -name jenkins.xml
# 修改jenkins的端口,进入配置文件
vim jenkins配置文件
注意,服务器需要设置开放端口,才能访问,在服务器-防火墙-添加规则
配置jenkins、部署项目
-
创建用户: 当一个团队在使用Jenkins时,一般都会为不同角色创建不同账号,从而给为每个账号设置不同权限 ,这样就能相对安全的管理多个用户
- Configure System->Manges User
-
添加插件: 使用Jenkins时需要自己安装一些插件,以满足工作需要
-
创建任务
-
配置git、绑定仓库地址
-
构建触发器
-
构建环境
-
构建步骤、执行shell脚本
- 把打包好的目录映射到nginx目录中
docker run -itd --name nginx -v /volume1/docker/jenkins/workspace/react-demo/dist:usr/share/nginx/html -p 80:80 nginx -
开始构建
-
完成构建,可以到对应的地址+端口号访问你的前端项目
gitLab CI/C
GitLab CI/CD 是一个内置在GitLab中的工具,用于通过持续方法进行软件开发:
gitlab仓库一旦收到任何推送,GitLab将立即查找
.gitlab-ci.yml配置文件,执行里面的编写的脚本内容,并可以完整的走一边install、eslint检查、编译、部署服务器的流程
ci/cd: 简单来说就是代码推送到仓库的时候,会自动触发流程,这个流程会执行一系列的任务
- 一系列任务组织起来就是
pipelines流水线,把一系列任务安装一定顺序运行 - 一条流水线包含多个stage: 阶段,每个阶段都有多个job任务
通过 .gitlab-ci.yml配置任务, GitLab CI使用YAML文件(.gitlab-ci.yml)来管理项目配置。该文件存放于项目仓库的根目录,它定义该项目如何构建。
- 当有新内容 push 到仓库,或者有代码合并后, GitLab 会查找是否有 .gitlab-ci.yml 文件,如果文件存在, Runners 将会根据该文件的内容开始 build 本次 commit
- .gitlab-ci.yml 使用 YAML 语法, 你需要格外注意缩进格式,要用空格来缩进,不能用 tabs 来缩进。
.gitlab-ci.yml有以下几种能力
- 1、运行的shell脚本,通过script文件
- 2、引用的其他模版文件和配置文件,通过include指令
- 3、定义缓存和依赖,通过cache指令
- 4、定义要执行的任务和执行的顺序,pipeline、jobs
- 5、应用部署的位置,自定定义
- 6、启动CI的方式,API和UI
安装gitlab
# 寻找gitlab的镜像
docker search gitlab
# 拉取gitlab镜像
docker pull gitlab/gitlab-ce
# linux启动docker服务端,window只需要打开docker desktop
sudo systemctl start docker
# 进入gitlab终端内部
docker exec -it gitlab/gitlab-ce /bin/bash
#进入gitlab终端,输入gitlab-ctl命令启动
# 执行生成服务配置文件命令
gitlab-ctl reconfigure
启动gitlab服务
- 查看宿主机的IP地址,比如我的虚拟机IP是192.168.123.152,启动一个 GitLab 服务:
在设置其他所有内容之前,配置一个新的环境变量 $GITLAB_HOME 指向配置、日志和数据文件所在的目录
#linux用户设置
export GITLAB_HOME=/srv/gitlab
sudo docker run --detach \
--hostname 127.0.0.1 \
--publish 443:443 --publish 80:80 --publish 22:22 \
--name gitlab \
--restart always \
--volume $GITLAB_HOME/config:/etc/gitlab \
--volume $GITLAB_HOME/logs:/var/log/gitlab \
--volume $GITLAB_HOME/data:/var/opt/gitlab \
gitlab/gitlab-ce:latest
参数说明: --detach: 设置容器后台运行 --hostname: 设置容器的域名 hostname,如果是本地localhost ,否则使用外网ip
- 需要配域名的解析,才能直接访问
--publish: 端口转发规则(80:Http 访问端口,443:Https 访问端口,10080:主机的 ssh 访问端口,22:Docker 容器中 ssh 访问端口)
- -publish 443:443 :将 Container 的 443 Port 映射到主机的 443 Port (前面代表主机,后面代表容器)
- -publish 80:80 :将 Container 的 80 Port 映射到主机的 80 Port (前面代表主机,后面代表容器)
- -publish 22:22 :将 Container 的 22 Port 映射到主机的 22 Port (前面代表主机,后面代表容器)
--name:容器名称 --restart always:每次启动容器就重启GitLab --volume: 共享目录挂载,即 docker 容器内外数据共享
- 将 gitlab 的配置文件目录映射到 $GITLAB_HOME/config 目录中
- 将 gitlab 的log文件目录映射到 $GITLAB_HOME/logs 目录中
- 将 gitlab 的数据文件目录映射到 $GITLAB_HOME/data 目录中
--e:配置 Gitlab 运行的环境变量
# 查看gitlab的日志,是否启动成功,-f 实时跟踪日志输出
docker logs -f gitlab
登录gitlab
- 使用
ip addr命令查看系统的 IP 地址,然后在浏览器输入地址即可访问 - 通过域名进行访问,启动的服务指定了hostname的域名地址,需要域名解析
ip addr
问题:
- 访问 http://192.168.17.129/users,出现502-Whoops, GitLab is taking too much time to respond
- gitlab还没启动完成,需要稍等一会
#实时查看某个容器日志
sudo docker logs -f 容器id
sudo docker logs -f gitlab
- 端口号被占用,已存在容器
# 查询22端口被占用的进程id
sudo netstat -pna | grep 22
# 杀死某个进程
kill -9 xxx
# 查看所有容器
docker ps -a
# 删除容器id
docker rm xxx
配置GitLab Runner跑CI
GitLab Runner简单来说就是运行Job的容器或管理者
GitLab Runne的作用是每次代码发生变更的时候gitlab CI会根据
.gitlab-ci.yml,配置文件执行流水线(Pipeline)中每个阶段Stages中的Jobs,并将结果发送回gitLabci/cd的操作就是gitlab runner里执行的,简单来说gitlab runner给ci/cd 提供一个运行的环境
安装gitlab-runner
gitLab Runner是基于gitLab CI的API进行构建的相互隔离的机器,所以gitLab Runner 与gitlab本身不需要安装在同一台机器上,并且考虑到执行Runner对资源的消耗,以及安全性问题,所以并不建议安装在同一台机器上
- 首先下载
gitlab-runner的Docker镜像,可选用alpine-bleeding比较轻巧
docker pull gitlab/gitlab-runner:alpine-bleeding
- 运行docker-runner
- 会发现如下错误,
config.toml文件找不到,这个问题不必担心,当我们将gitlab-runner注册到Gitlab时,会自动生成该文件
- 会发现如下错误,
docker run gitlab/gitlab-runner:alpine-bleeding
注册gitlab-runner
-
安装好
gitlab-runner需要注册到Gitlab,打开Project->Settings->CI/CD功能 -
注册一个runner,获取到runner注册需要使用的地址和token,并且URL指定为xxx,token指定为xxx
-
进入
gitlab-runner容器的内部;docker exec -it gitlab-runner /bin/bash
gitlab-ci.yml 配置说明
| 关键字 | 描述 |
|---|---|
| script | 必须参数,运行器需要执行的脚本 |
| image | 使用Docker image镜像 |
| services | 使用Docker services镜像 |
| before_script | 作业执行前需要执行的命令 |
| after_script | 作业执行后需要执行的命令 |
| stages | 定义流水线所有的阶段 |
| stage | 定义作业所处流水线的阶段(默认test阶段) |
| only | 限制作业在什么时候创建 |
| except | 限制作业在什么时候不创建 |
| tags | 作用使用的Runner运行器的标签列表 |
| allow_failure | 允许作业失败,失败的作业不影响提交的状态 |
| when | 什么时候运行作业 |
| environment | 作用部署的环境名称 |
| cache | 指定需要在job之间缓存的文件或目录 |
| artifacts | 归档文件列表,指定成功后应附加到job的文件和目录的列表 |
| dependencies | 当前作业依赖的其他作业,你可以使用依赖作业的归档文件 |
| coverage | 作业的代码覆盖率 |
| retry | 作业失败时,可以自动执行多少次 |
| parallel | 指定并行运行的作业实例 |
| trigger | 定义下游流水线的触发器 |
| include | 作业加载其他YAML文件 |
| extends | 控制实体从哪里继承 |
| pages | 上传GitLab Pages的结果 |
| retry | 作业失败时,可以自动执行多少次 |
| variables | 定义环境变量 |
gitlab-ci.yml定义了一个Pipeline, 分三个阶段步骤执行:
image: docker:stable # Pipeline中各个步骤阶段的构建镜像没有指定时, 默认使用docker:stable镜像
stages:
- package # 源码打包阶段
- docker_build # 镜像构建和打包推送阶段
- deploy_k8s # 应用部署阶段
variables:
KUBECONFIG: /etc/deploy/config # 定义全局变量KUBECONFIG
-
镜像构建和打包推送阶段。
docker_build_job: # job名称image: docker:latest # 本阶段构建使用的构建镜像# pipelines阶段stage: docker_build # 关联的阶段名称# 指定哪些runner适用该jobtags: # GitLab Runner的tag- k8s-runner# 只针对哪个分支only:- master# 除了哪个分支以外except:- develop# 要跑的脚本或命令列表script:- docker login -u $REGISTRY_USERNAME -p $REGISTRY_PASSWORD registry.cn-beijing.aliyuncs.com # 登录镜像仓库- mkdir target- cp /opt/cache/demo.war target/demo.war- docker build -t registry.cn-beijing.aliyuncs.com/haoshuwei24/gitlabci-java-demo:$CI_PIPELINE_ID . # 打包Docker镜像,使用的tag为本次Pipeline的ID- docker push registry.cn-beijing.aliyuncs.com/haoshuwei24/gitlabci-java-demo:$CI_PIPELINE_ID # 推送Docker镜像
gitLab持续集成,只要在仓库的根目录添加
.gitlab-ci.yml文件,并且配置了gitLab runner(运行器),每次代码发生变成的时候就会执行.gitlab-ci.yml中的配置
如何落实CI/CD
Jenkins vs GitLab CI/CD选择
CI/CD如何优化
优化自动化部署速度
可能出现问题:
- 流水线任务时间过长
- install重复、频繁install存在失败的概率
- npm源网络差
- 其他阻塞进程操作
- 磁盘读写过慢、占用过多问题
- runner性能跑满,服务器压力过高,导致整体变慢问题
解决:
-
使用淘宝源,包管理可使用yarn来替代npm
-
使用gitlab的cache, 实际上并不需要每次都打包
-
解决catch问题,主要是缓存了node_modules
cache:
key: your-project-name
paths:
- node_modules/
前端埋点
前端埋点方法有三种:手动埋点、可视化埋点和无埋点
目的:
- 获取用户的基本信息、行为
- 跟踪产品在用户端使用的情况
- 针对分析用户的行为,指明产品的优化
- 了解用户行为轨迹,看哪些素材元素以及引导点能够吸引用户
- 性能监控、数据监控
- 拿到页面在各种网络下的加载时间,再比如拿到用户在某个页面的停留时间!
- 为了运营以及开发人员采集用户行为数据
手动埋点
优势:
- 可自定义属性,自定义事件
- 可以更加细化需求
- 相比其他埋点方式减少服务器压力
- 配置比较灵活,控制精准,想埋哪,想什么时候埋都可以
缺陷:
- 工程量大的话,手动埋点会出现疏漏,不方便审查。
- 需求变更要重新埋点,成本高。
- 埋点代码入侵业务代码,后期难以维护
- 每次需求变更都要重新发布版本,对线上系统稳定性有一定风险
可视化埋点
- 可视化埋点中多数基于Xpath的方案,XPath 是一门在 XML 文档中查找信息的语言,XPath 可用来在 XML 文档中对元素和属性进行遍历
- xpath方案分为精确路径和概略路径两种做法
- 精确路径 : body>div[0]>div[3]>ul>li[5]>a[0] ,从被点击的元素不断向上冒泡查找到根节点,并记录过程中每个节点 【如果用户阻止了冒泡会导致失效】
- 概略路径:概略路径是在前者的基础仅仅向上查找白名单中的节点。但是如果DOM节点有任何变化,对应的xpath数组就是被打乱,很容易影响采集
XPath:全称 XML Path Language,即 XML 路径语言
- XPath可以用于XML文档搜索,也可以用于HTML文档搜索
- XPath可以对文档的元素和属性进行遍历
优点:
- 解决了纯手动埋点的开发成本和更新成本
- 解决埋点方案变更重走发版流程的问题,方便产品的迭代
缺点:
-
只能读取页面上标签元素展示出来的属性,不会像代码埋点那样,能够获取上下文(通常内存里)的一些属性
-
页面的结构发生变化时,需重新标记操作
说说你项目是如何使用的?
-
系统接入sdk后,不用进行代码开发即可以进行埋点数据的采集,需要埋点信息管理平台,对埋点配置进行开发、配置、调试、部署
-
通过iframe接入可视化埋点的实施系统,埋点平台与sdk之间可以使用postmessage互通消息
-
通过鼠标可视化圈选需要埋点的dom节点,配置参数等信息完成埋点开发
前端监控 (报错监控)设计
报错监控的意义在哪里
- 收集线上错误日志
- 帮助优化产品的稳定性
- 收集错误数据用于分析
- 监控第三方资源/ CDN稳定性
- 前端监控的主要目的都是为了获取用户行为以及跟踪产品在用户端的使用情况
有哪些常见的监控指标?
- PV、UV : 页面的浏览访问量和用户点击量
各常见指标和计算方式
| 指标 | 计算方式 | 说明 |
|---|---|---|
| 页面加载总耗时 | loadEventEnd - startTime | 指页面完全加载完所用的时间,这时候触发完成了 onload 事件 |
| DNS解析耗时 | domainLookupEnd - domainLookupStart | 指通过域名解析服务(DNS),将指定的域名解析成IP地址所消耗的时间 |
| TCP连接耗时 | connectEnd - connectStart | 指浏览器和WEB服务器建立TCP/IP连接所用的时间 |
| SSL连接耗时 | location.protocol === 'https:' ? connectEnd - secureConnectionStart | 只在 HTTPS 下有效,属于TCP连接耗时的一部分,指安全连接握手耗时 |
| 网路请求耗时 | responseStart - requestStart | 指开始发送请求到服务器返回第一个字节所需要的时间 |
| 数据传输耗时 | responseEnd-responseStart | 指服务器端返回第一个字节到最后一个字节所需要的时间 |
| DOM解析耗时 | domContentLoadedEventEnd - responseEnd | 指页面请求完成(responseEnd)后,到整个 DOM 解析完所用的时间,页面的复杂度决定了 DOM 解析耗时 |
| 资源加载耗时 | loadEventEnd - domContentLoadedEventEnd | 指 DOM 解析完成后到页面完全加载完所用的时间 |
| 首包时间 | responseStart - startTime | 指从页面请求到浏览器开始接收到数据所用的时间 |
| 页面渲染耗时 | loadEventEnd - responseEnd | 等于页面完全加载时间 - HTML 加载完成时间(见下面指标) |
| 页面完全加载时间 | loadEventEnd - startTime | 指页面完全加载完所用的时间,这时候触发完成了 onload 事件 |
| 白屏时间 | 优先使用最新标准 performance.getEntriesByType('paint')[0].startTime,不支持的话使用 Chrome、IE 提供的 firstPaintTime,chrome.loadTimes().firstPaintTime 或 performance.msFirstPaint,还没有获取,取 domInteractive - startTime但是实际上报取的值是:loadEventEnd - startTime | 首次渲染时间,指页面出现第一个文字或图像所花费的时间 |
| 页面加载完时间 | loadEventEnd - startTime | 指页面完全加载完所用的时间,这时候触发完成了 onload 事件 |
| HTML加载完时间 | responseEnd - startTime | 指页面所有 HTML 加载完成(不包括页面渲染时间),即包括 DNS、TCP、Request 和 Response |
| 首次交互时间 | domInteractive - startTime | 指页面 DOMContentLoaded 事件触发的开始时间,这时候页面可以交互 |
| 首屏时间 | 有两种方式:第一种通过计算首屏区域内的所有图片加载时间,然后取其最大值;第二种方式:通过 window.MutationObserver 来监听首屏所有元素变化情况,并记录时间,最后取其最大值(会去掉得分相同重复的值),算出的时间需要加上 domInteractive(可交互时间),目前系统采用第二种实现方式,如果第二种取不到,取 domInteractive,但该值会比实际首屏时间要小 | 首屏时间,也称用户完全可交互时间,即整个页面首屏完全渲染出来,用户完全可以交互,一般首屏时间小于页面完全加载时间,该指标值可以衡量页面访问速度 |
部署
说说你是如何进行部署的
1、配置超长时间的本地缓存 —— 节省带宽,提高性能、
2、采用内容摘要作为缓存更新依据 —— 精确的缓存控制
3、静态资源CDN部署 —— 优化网络请求、
4、更资源发布路径实现非覆盖式发布 — — 平滑升级、
使用docket部署
- 使用docket自动化部署
使用Jenkins配置
- Jenkins是一个非常方便强大的CI/CD工具,
- Jenkins是一个自包含的、开源的自动化服务器,可以用于自动化与构建、测试、交付或部署软件相关的各种任务。
- Jenkins可以通过本机系统包、Docker安装,甚至可以由任何安装了Java运行时环境(JRE)的机器独立运行
部署jenkins是怎么做的?
发起一个git提交,剩下的单元测试,打包构建,代码部署,邮件提醒等全部jenkins自动化完成。
基本步骤
-
可以在window/linux安装jenkins,启动tomcat服务器 【因为Jenkins是基于java环境的】
-
jenkins建立任务项目
-
进行了初始化配置
-
实现git的钩子功能
- 刚创建的任务,选择配置,添加远程仓库地址,配置登录名及密码及分支
- 在源码管理栏,中选择git,添加仓库地址 (在公司的话,选择公司的私仓)
- 刚创建的任务,选择配置,添加远程仓库地址,配置登录名及密码及分支
-
实现自动化构建 【构建触发器】
-
在jenkins里面执行npm命令
- 先要在jenkins里面配置node的环境
- 可以通过配置环境变量的方式引入node,也可以通过安装插件的方式,这里使用了插件的方式
- 打开刚刚的jenkins任务,点击配置里面的构建环境,并指定一个node版本
- 点击构建,把要执行的命令输进去,多个命令使用&&分开。
自动化的构建任务可以有很多种
如:安装依赖包->单元测试->打包
npm installnpm run testnpm run build
-
服务端
1、cookie和session的区别
- 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。
- 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
- 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
- cookie的过期时间:1.不设置过期时间,会存在内存里,浏览器关闭即失效。2.设置过期时间,会存在硬盘里,一直到过期时间才失效。
- session的过期时间如果Session对象设置了超时时间,Session对象超过此时间会被销毁。如果没有设置默认30分钟会被销毁(服务器默认配置)
- 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
- 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。
- cookie不是很安全,用来存放用户不太敏感的数据信息,考虑到安全性的话应使用session
为什么需要 Cookie 和 Session,他们有什么关联?
1、说起来为什么需要 Cookie ,这就需要从浏览器开始说起,我们都知道浏览器是没有状态的(HTTP 协议无状态),这意味着浏览器并不知道是张三还是李四在和服务端打交道。这个时候就需要有一个机制来告诉服务端,本次操作用户是否登录,是哪个用户在执行的操作,那这套机制的实现就需要 Cookie 和 Session 的配合。
2、用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录到SessionID 属于哪个域名。
3、当用户第二次/n次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
Nginx
- Nginx是一个HTTP服务器和反向代理服务器。
使用nginx的场景
反向代理
- 客户端发送的请求,想要访问服务器上的内容。发送的内容被发送到代理服务器上,这个代理服务器再把请求发送到设置好的内部服务器上,而用户真实想获得的内容就在这些设置好的服务器上)
- 反向代理为服务器做代理
http://nginx.qiuweikang.com来测试一下
server{
listen 80; // nginx的端口号
server_name nginx.qiuwiekang.com; //服务器的地址
location / {
proxy_pass http://baidum.com; // 要代理的地址
}
}
反向代理还有些常用的指令
- proxy_set_header :在将客户端请求发送给后端服务器之前,更改来自客户端的请求头信息。
- proxy_connect_timeout:配置Nginx与后端代理服务器尝试建立连接的超时时间。
- proxy_read_timeout : 配置Nginx向后端服务器组发出read请求后,等待相应的超时时间。
- proxy_send_timeout:配置Nginx向后端服务器组发出write请求后,等待相应的超时时间。
- proxy_redirect :用于修改后端服务器返回的响应头中的Location和Refresh。
负载均衡
- 负载均衡就是一个网站的内容被部署在若干服务器上
- 可以把这些机子看成一个集群,那Nginx可以将接收到的客户端请求“均匀地”分配到这个集群中所有的服务器上,从而实现服务器压力的平均分配,也叫负载均衡
设置Gzip压缩
Gzip是网页的一种网页压缩技术,经过gzip压缩后,页面大小可以变为原来的30%甚至更小。更小的网页会让用户浏览的体验更好,速度更快。gzip网页压缩的实现需要浏览器和服务器的支持
配置:
- 在 nginx.conf 的配置项,这是nginx的总配置项,因此在这里配置gzip
cd etc/nginx
vim nginx.conf
http {
.....
gzip on; // 启用gzip模块
gzip_types text/plain application/javascript text/css;
.....
}
说说如何进行nginx部署
nginx配置
-
配置
expires- expires起到控制页面缓存的作用,合理的配置expires可以减少很多服务器的请求
- 这样浏览器访问该页面的时候会使用304缓存策略。
monorepo
- Monorepe 就是多个项目放在一个仓库里面
- 一般 Monorepo 的目录如下所示
一般 Monorepo 的目录如下所示,在 packages 存放多个子项目,并且每个子项目都有自己的package.json:
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
- monorepo的特点
- 复用更加容易,由于所有的项目放在一个仓库当中,复用起来非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况
- 团队协作也更加容易,一方面大家都在一个仓库开发,能够方便地共享和复用代码,方便检索项目源码
lerna
- lerna是一个优化基于git+npm的多packaga项目的管理工具
- 用于管理具有多个包的 JavaScript 项目的工具
- lerna采用了 Monorepo的方式
- 不需要手动去维护每个包的依赖关系,当发布时,会自动更新相关包的版本号,并自动发布
- 可使用lerna管理微应用
- 可减少重复操作
- 提示操作的标准化
- 标准的发布流程、发布版本号
lerna遇到的问题
发布失败后的解决方案
lerna 作为多个包依赖的管理解决方案,确实解决了很多痛点,我目前在工作和生活中已经大量使用了lerna,但同时也会遇到一些问题,比如发布失败后的问题是遇到比较频繁的问题
发布包的问题,有可能你的网络情况有问题,有可能你没有登录npm(包括公司内部的 registry ),导致发布失败。这时候不能重新发版,因为已经发版过了,没有修改过记录发版不了,但是上一次又发版失败了
解决方式:
- 1、可以重新修改过记录,重新发版 【会比较麻烦】
- 2、可以手动回退之前版本,并且删除相应的git tag标签
git reset --hard HEAD~1 && git tag -d v3.1.0
WebAssembly?
- WebAssembly 或者 wasm 是一个可移植、体积小、加载快并且兼容 Web 的全新格式
什么时候需要WebAssembly?
- 对性能有很高要求web应用
微前端
微前端:将多个独立的前端应用聚合在一起组成一个全新的应用,各个应用直接彼此独立,互不干扰
如果前端项目过于庞大, 所有功能都在一个项目,而客户又比较多,每个客户需要的功能又差异,有的客户还需源码,按目前架构开发很难满足不同客户不同功能以及不同源码版本的业务需求,需要一种更高维度的解耦方案-微前端(前端微服务化),按业务拆分为不同的微前端模块
前端微服务化后的优势
- 复杂度可控: 每一个UI业务模块可由独立的前端团队开发, 避免代码巨无霸, 保持开发时的高速编译, 保持较低的复杂度,便于维护与开发效率。
- 独立部署: 每一个模块可单独部署,颗粒度可小到单个组件的UI独立部署,不对其他模块有任何影响。 技术选型灵活: 也是最具吸引力的,在同一项目下可以使用如今市面上所有前端技术栈,也包括未来的前端技术栈。
- 容错: 单个模块发生错误,不影响全局。
- 扩展: 每一个服务可以独立横向扩展以满足业务伸缩性,与资源的不必要消耗
缺点
- 体验差,每个独立应用加载时间较长;
qiankun
参考:https://zhuanlan.zhihu.com/p/339904439
qiankun相比iframe的优势?
iframe的缺点:
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
注册子应用
registerMicroApps(apps, lifeCycles?)
应用通信
-
可以通过url来传递参数,但是传递信息的能力很弱
- url的长度有限,传递的数据也会受到限制
-
基于props主子应用通信, initGlobalState定义全局状态【一般在主应用中使用】, 微应用通过 props 获取通信方法
-
使用全局变量 ,在window上挂载属性
initGlobalState(state)
-
本质就是 EventBus
- 事件总线
- 事件的传递,通过事件来实现组件的通信
原理:发布订阅模式
- 发布订阅者模式最大的特点就是实现了松耦合,也就是说你可以让发布者发布消息、订阅者接受消息
主应用:
import { initGlobalState, MicroAppStateActions } from 'qiankun';
const state = { user: 'qiankun' };
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
子应用:
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
props.setGlobalState(state);
}
公共依赖
可以把通用的模块抽离出来
-
公共依赖配置成
external, 通过cdn方式加载这个包- 防止将某些依赖包打包到bundle中,而是在运行时再去从外部获取这些扩展依赖
- 例如可以在cdn引入包,而不是将它打包
externals: {vue: 'Vue',react:'react',lodash: {commonjs: 'lodash',amd: 'lodash',root: '_', // 指向全局变量},} - 防止将某些依赖包打包到bundle中,而是在运行时再去从外部获取这些扩展依赖
-
webpack5提供了联邦模块
- 父调子的模块,子也可以调父的模块
css隔离
每个子应该都有一个命名空间,不会冲突
A应用有自己的样式,B应用有自己的样式,每一个独立的子应用间及子应用间的如何保证不会冲突?样式不会相互覆盖?可以通过每个「子应用」通过 Shadow DOM 进行隔离
- 而qiankun也是默认开启了sandbox,即开启了沙箱,这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点
子应用之间样式隔离
- 动态样式表,当应用切换的时候,移除老应用的样式,添加新应用样式
主应用和子应用之间的样式隔离
-
css-mudules:利用hash规则编译为 独一无二的类名
-
Shadow DOM :真正意义上的隔离
- MDN原话:可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁
- ShadowDOM最大的用处应该是隔离外部环境用于封装组件, 可以将「HTML 标签结构、CSS 样式、行为」隐藏起来,并从页面上的其他代码中分离开来,这样不同的功能不会混在一起
var header = document.createElement('header');var shadowRoot = header.attachShadow({mode: 'open'}); //模式如何为closed,外界不能访问var paragraphElement = document.createElement('p');paragraphElement.innerText = 'Shadow DOM';shadowRoot.appendChild(paragraphElement);
js沙箱
- 子应用之间互不影响
- 变量名不会冲突
如何实现js沙箱的?
js沙箱机制,在微前端环境中,子应用都可能用到window全局对象,因此在不同环境中可能对全局对象造成污染,怎么创造一个状态隔离的js运行环境呢?
- 利用闭包的能力、利用变量作用域去模拟沙箱环境
qiankun沙箱
-
ProxySandbox 【代理沙箱】
当有多个实例的时候,比如有
A、B两个应用,A应用就活在A应用的沙箱里面,B应用就活在B应用的沙箱里面,A和B无法互相干扰,这样的沙箱就是代理沙箱,这个沙箱的实现思路其实也是通过ES6的 proxy,通过代理特性实现的- 是基于 ES6的 Proxy 实现的沙箱。
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 简单来说就是,可以在对目标对象设置一层拦截。无论对目标对象进行什么操作,都要经过这层拦截使用proxy的特点:
- 监听数组的变化
- 可以监听整个对象的变化
-
snapshotSandBox【快照沙箱】
快照沙箱就是在应用沙箱挂载和卸载的时候记录快照,在应用切换的时候依据快照恢复环境
问题
微应用加载出现404?
- webpack 加载资源时未使用正确的
publicPath。 - webpack 提供一个非常有用的配置,该配置能帮助你为项目中的所有资源指定一个基础路径。 它被称为 公共路径(publicPath) 。
单页应用和多页应用
渲染:
- 多页应用是由服务端进行 HTML 模板拼接的,各个页面间没有直接关系;
- 单页应用便是将页面内容的控制权交给前端来处理,由前端进行渲染
首屏时间:
- 多页应用首屏时间快
- 单页应用首屏慢,首屏时需要请求一次html,同时还要发送一次js请求 【单页应用则需要自行进行计算和组装HTML,中间过程很可能还涉及数据的二次请求】,两次请求回来了,首屏才会展示出来
- 请求html => 解析html => 请求css和js => 渲染 => 解析和执行js => 请求数据 => 渲染 => 下载图片 => 渲染 => 页面显示可交互
前端路由库设计原理
实现前端路由,离不开浏览器提供的 History API、Location API 这些 API
前端路由的实现,包括两种模式
- Histrory模式
- Hash模式
History 的模式 : 依赖了一个关键的属性window.history,该属性可用来获取用于操作浏览器历史记录的 History 对象
- History 的路由模式需要依赖 HTML5 History API(IE10 以上),以及服务器的配置来支持
- 所以不少的开发者会使用 Hash 模式来管理 Web 应用的路由
Hash 模式 : Hash 模式使用的是从井号(#)开始的 URL(锚)片段,主要依赖 Location 对象的 hash 属性
单点登录
单点登录的英文名叫做:Single Sign On(简称SSO)。
单点登录就是在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。
-
session
- SessionID 怎么知道是哪个用户?
- 多个服务器之间,sessionid不一致
-
jwt
-
redis
- 把session数据存放在redis,统一管理,向外提供服务接口,redis可以设置过期时间,对应session的失效时间
同域名下的单点登录
cookie的 domain属性设置为当前域的父域,并且父域的 cookie会被子域所共享。path属性默认为 web应用的上下文路径
利用 Cookie 的这个特点,没错,我们只需要将 Cookie的 domain属性设置为父域的域名(主域名),同时将 Cookie 的 path属性设置为根路径,将 Session ID(或 Token)保存到父域中。这样所有的子域应用就都可以访问到这个 Cookie
不过这要求应用系统的域名需建立在一个共同的主域名之下,如 tieba.baidu.com 和 map.baidu.com,它们都建立在 baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录
不同域名下的单点登录(一)
如果是不同域的情况下,Cookie是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web 服务
- 认证中心是一个独立的系统,在这个系统里登录之后,其他系统拿到认证中心的cookie就自动登录
用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token 写入 Cookie(注意这个 Cookie 是认证中心的,应用系统是访问不到的)
应用系统检查当前请求有没有 Token,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心
由于这个操作会将认证中心的 Cookie 自动带过去,因此,认证中心能够根据 Cookie 知道用户是否已经登录过了
如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录
如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL ,并在跳转前生成一个 Token,拼接在目标 URL 的后面,回传给目标应用系统
应用系统拿到 Token 之后,还需要向认证中心确认下 Token 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token 写入 Cookie,然后给本次访问放行。(注意这个 Cookie 是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 Token,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了
此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法
不同域名下的单点登录(二)
可以选择将 Session ID (或 Token )保存到浏览器的 LocalStorage 中,让前端在每次向后端发送请求时,主动将 LocalStorage的数据传递给服务端
这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID (或 Token )放在响应体中传递给前端
单点登录完全可以在前端实现。前端拿到 Session ID (或 Token )后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中
resefull规范
需要满足什么条件才是restful?
restful规范的话 就是post提交数据,put用来更新数据
- API路径地址
在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数
举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。
- http请求方法
对于资源的具体操作类型,由HTTP动词表示。
- GET(SELECT):从服务器取出资源(一项或多项)。
- POST(CREATE):在服务器新建一个资源。
- PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
- PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
- DELETE(DELETE):从服务器删除资源。
还有2个不常用的Http动词
- HEAD:类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头元信息
- OPTIONS:用于检测服务器允许的http方法
- 状态码
服务器向用户返回状态码和提示信息
- 错处处理
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可
- 返回结果
针对不同操作,服务器向用户返回的结果应该符合以下规范。
restful接口架构的优缺点?
- 优点:
- 具体操作类型,由HTTP动词表示,表示操作的目的
restful接口架构会导致什么安全问题,具体怎么解决?
- PUT 这个协议本身不存在验证机制,任何人都可以上传文件,存在安全性问题
- 可通过中间件来验证参数
- delete则可以删除文件,造成恶意攻击
前端代码规范的建议?
- HTML规范
- css规范
- javascript规范
- (变量)命名规范
说说研发发布流程
- 1、相关的产品需求,文档,和UI,使用的是禅道,需求和任务上传到禅道里,对需求和任务进行管理
- 2、创建对应的分支, 代码提交到git的版本控制的仓库,使用的是gitea
- 3、对提交的代码进行审核,review
- 4、审核完成没问题之后进行合并,再发布到测试环境中,去测试
- 5、进行bug的清单,有专门的bug管理,使用的是禅道,把bug上传到禅道去跟踪
- 6、改完bug之后,给到测试进行回归测试 【回归测试:改了代码后,重新进行测试以确认修改没有引入新的的bug】
- 7、上线到正式环境中
- 8、正式环境中,给到客户去使用,反馈问题
- 9、对客户反馈的问题进行修改调整完善
敏捷开发,项目管理的流程工具,PM会在上面进行实时跟踪,就在指定的节点之前完成对应的开发,push到指定的分支 进行测试,联调,灰度,上线等,上线之后还有一定的回归,回归比较久,开发之前,会有一个设计的过程,设计的过程会 比较久,因为涉及到不确定性,也有可能突然这个需求不做了,一些细节讨论确认之后才开始开发
回归测试:对新版本测试时,重复执行之前的版本功能测试用例是否还存在有bug 目的: 1.验证之前版本产生的缺陷已全部被修复 2.确认修复这些缺陷没有引发新的缺陷bugs
开发流程
一般开发流程:
- 敏捷开发,项目管理的流程工具,PM会在上面进行实时跟踪,就在指定的节点之前完成对应的开发,push到指定的分支 进行测试,联调,灰度,上线等,上线之后还有一定的回归,回归比较久,开发之前,会有一个设计的过程,设计的过程会 比较久,因为涉及到不确定性,也有可能突然这个需求不做了,一些细节讨论确认之后才开始开发
回归测试:对新版本测试时,重复执行之前的版本功能测试用例是否还存在有bug
- 目的: 1.验证之前版本产生的缺陷已全部被修复 2.确认修复这些缺陷没有引发新的缺陷bug
项目中遇到的问题
1、动态分成多个ajax请求来提交数据,可提高性能
如果一次post的提交的数据太多,后端对于一个请求绑定到一个进程只能利用单核来处理,无法利用并发性能。而如果你分成多个请求了,后端就能自动利用好多核多进程来并发处理,而且每个请求数据不会太多延时也少了,大大的提高了优化性能,实现了并发处理,服务器就可以同时处理多个请求了
遇到的坑:
- 我是利用了递归,来实现动态请求ajax
- 在返回数据的时候,发现有延迟的情况,导致获取到数据不完整
- 因为我当时是执行完一次请求,再继续递归执行下一次请求,这样很显然由于异步会导致下一次请求返回的数据有延迟的情况
解决的方法:
-
利用了promise对象的all方法来处理多个异步请求 (具有并发执行异步任务的能力),这样就保证了所有返回的结果同在一个实例中
-
完善:使用了ES2020新特性的 Promise.allSettled,这个方法来弥补Promise.all缺陷
- Promise.all最大问题就是如果其中某个任务出现异常(reject),所有任务都会挂掉
- 而 Promise.allSettled只有等到所有这些参数实例都返回结果,不管是
fulfilled还是rejected,包装实例才会结束
2、树状结构展开大量数据时,出现卡顿情况
解决的方案
方案1、虚拟列表滚动,滚动到可视区域的时候 就会添加到节点,不在可视区域的话 就会删掉节点
如果你的应用存在非常长或者无限滚动的列表,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list 和 vue-virtual-scroller 来优化这种无限列表的场景的。
虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。- 假设有1万条记录需要同时渲染,我们屏幕的
可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。
3、树状节点发生变化动态更新
解决的思路是:
1、得到选中的节点
2、将选中节点的子节点全部删除
3、将选中节点的子节点数据手动刷到该节点内
这样的话,数据就会发生变化,vue就会通过watcher来检测数据发生变化,再通过update来更新视图
4. 拖拽问题
-
draggable属性:元素如果要设为可拖拽,必须给它添加一个
draggable属性-
用于标识元素是否允许使用 拖拽的api
-
取值如下
- true,表示元素可以被拖动
- false,表示元素不可以被拖动
-
-
拖拽事件:
- dragstart: 在拖拽刚开始时触发。它主要用于将拖拽的组件信息传递给元素
- drop: 在拖拽结束时触发。主要用于接收拖拽的组件信息。
-
保存拖拽过程中的数据
-
dataTransfer对象,这个对象可以从所有拖动事件
drag events的dataTransfer属性上获取。 -
dataTransfer对象中的方法
- setDate(): 传输数据
- 使用:setDate("index","传过来的数据")
- getData(): 接收传输过来的索引数据
- 使用:setDate("index")
- setDate(): 传输数据
-
拖拽遇到什么问题?
- 拖拽过程中,需要对某一些组件进行交互效果显示,性能方面卡顿
- 使用了translate位移替代了absolute绝对定位
- 列表之间拖拽,数据量过多,造成页面卡顿
- 虚拟滚动列表
5. 文件上传
文件上传,我们需要设置请求头为 content-type:multipart/form-data
上传文件慢如何解决?
文件切片
- 大文件进行分片上传 【解决了大文件上传超时和服务器的限制】
- 对文件进行切片
- 将切片传输给服务端
断点续传
为什么慢?
- 文件大小超出限制,后端对单个请求大小做了限制,一般 nginx 和 server 都会做这个限制
- 大文件上传可能会导致内存溢出
- 网络原因上传失败
服务端怎么去处理?
服务端怎么处理文件流?
文件上传一部分怎么处理?
- 当有一个分片上传失败时,就标记,给个提示重新上传,只需要上传失败的切片
- 如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始
文件切片思路:
前端:
- 对文件进行切片,比如100 MB 的文件会被分成 10 个切片
- Blob 对象中的 slice 方法可以对文件进行切割,File 对象是继承 Blob 对象的,因此 File 对象也有 slice 方法
- 给每一个切片加一个标识,方便后端可以知道当前切片是第几个切片,用于之后的合并切片
- 每一个切片都返回一个promise对象
- 最后调用Promise.all并发处理上传所有的切片
后端:
断点续传的思路
断点续传的原理在于前端/服务端需要 记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能
- 前端使用 localStorage 记录已上传的切片 hash
- 服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片
第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失去了记忆的效果,所以这里选取后者
- 使用 spark-md5 库根据文件内容算出文件 hash
- 如果使用文件名来生成hash, 文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是
根据文件内容生成 hash
- 如果使用文件名来生成hash, 文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是
- 通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)
- 通过 XMLHttpRequest 的 abort 方法暂停切片的上传
- 上传前服务端返回已经上传的切片名,前端跳过这些切片的上传
6.处理excel表数据
- 要支持导入excel,进行预览,并且支持导出excel的数据
- 主要是对excel表导出的时候样式丢失,标题居中,加粗等等这些问题
解决:
- 一开始使用 xlsx来实现前端导出Excel的功能。然而社区版并不支持样式的添加
- 使用exceljs库或者xlsx-populate。,用相关api可以获取样式并且设置具体的样式
7.使用qiankun微服务遇到什么问题?
8.使用leran管理多个包
9、白屏如何解决?
白屏是一个怎样的过程呢?
- 1.将地址通过DNS解析成ip 【可利用DNS解析优化】
- 2.将ip建立TCP请求连接 【TCP连接优化】
- 3.服务端请求处理响应
- 4.浏览器接受响应
- 5.渲染页面
如何解决
DNS解析优化
-
DNS 缓存可以帮助减少延迟
- 缓存策略是浏览器自动执行的
-
DNS prefetch
- DNS 请求需要的带宽非常小,但是延迟却有点高,预读取 DNS 能让延迟明显减少一些
- 可以在页面的link标签中添加dns-prefetch告诉浏览器对指定域名预解析,如下:
- 淘宝网站都是这样处理
TCP连接优化
每次连接都需要3次握手,会带来延迟,消耗时间
- 可以建立长连接
- TCP连接的重复使用
服务端处理优化
- 缓存,可使用redis缓存
webpack/nginx开启gzip压缩
开启gzip压缩可以有效压缩资源体积,可以大大节省服务器的网络带宽,提高资源获取的速度
- 将css,js压缩
script脚本阻塞DOM渲染
- 因为js会对DOM进行操作,所以js阻塞DOM树的构建
懒加载
- 如果图片过多,开启图片懒加载
cdn
- 静态资源,可以使用cdn
10、react函数中防抖失效解决
原因:每次render 都会导致debounce第一个参数(如异步请求函数)更新,进而导致debounce的闭包失效
解决:
- 在class中,可在构造函数绑定防抖函数,确保render渲染不会重新创建函数,只在组件实例化执行一次绑定防抖函数
- hook中,可使用useCallback,防止重复渲染
11、确保精度不缺失
- data.toFixed(data.length)
(0.07/100).toFixed(String(0.07).length) //确保精度不丢失
12、iframe形式嵌套,如何通信?
- 父子级跨域通信使用postMessage
- window.postMessage()方法可以安全地实现跨源通信
- postMessage()方法允许来自不同源的脚本采用异步方式进行有效的通信
- postMessage跨域通信的一种有效的解决方案
13、form表单赋值问题
- 不能再表单渲染之前给表单项赋值,会报错警告
- 解决:需要等到表单渲染完之后,才进行赋值
- 可使用定时器,延迟调用
- 在vue可使用nextTick
Warning: You cannot set a form field before rendering a field associated with the value.
14、JSON中key不能用number类型
- JSON中使用数字作为Key带来的问题,浏览器解析JSON会报错,JSON中key不能用number类型
- json格式如何出现number作为key的话,解析会出现问题
JSON序列化的时候,使用JSON.parse解析会报错
15、虚拟列表滚动实现
虚拟列表定义:只渲染可视区域的列表,非可见区域中的数据不渲染,大大的提高了渲染的性能
实现虚拟列表就是在处理用户滚动时,要改变列表在可视区域的渲染部分
明确两个概念:可视区域、可滚动区域
可滚动区域 = 计算真实容器高度 = 数据总数 * 每条 item 的高度
-
首先需要用变量来保存当前完整的列表数据、以及渲染的列表数据
-
1、需要计算可视区域起始数据的位置(可视区域第一项位置)startIndex
- 假设元素的高度是固定的:那么startIndex = scrollTop (滚动条的高度) / itemHeight (列表项的高度)
-
2、需要计算可视区域结束数据(可视区最后一个数据的位置)endIndex
- endIndex = startIndex +(clientHeight/itemHeight)
-
3、计算当前可视区域的数据,渲染到可视区域中
-
4、计算可视区域第一项位置 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
-
刚开始渲染的时候,startOffset默认是0
-
当开始滚动的时候,startOffset = 可视区域第一个元素 到 列表的第一个元素 的距离
-
-
5、计算 endIndex 对应的数据在整个列表中的偏移位置 endOffset,并设置到列表上
- 刚开始渲染的时候,endOffset默认是0
- 当开始滚动的时候,endOffset =(整个列表的数据长度 - endIndex (可视区域最后一个元素的位置) )* height(列表高度)
startOffset和endOffset是一个隐藏的区域,会撑开容器元素的内容高度,让其可持续的滚动
- 刚开始渲染的时候,endOffset默认是0
注意:
如果元素高度不是固定,那么如何计算startIndex?
- 给每一个item绑定一个方法(可以利用事件委托 【将一个元素的响应事件委托到父元素上或者外层元素】)
- 当节点加载渲染后调用此方法来动态获取元素高度,并且将startIndex开始位置和height高度信息缓存保存起来
- 缓存的作用是 避免重复计算
- 当节点加载渲染后调用此方法来动态获取元素高度,并且将startIndex开始位置和height高度信息缓存保存起来