跳到主要内容

7 篇博文 含有标签「知识点」

查看所有标签

今天学习到原来JPEG文件有两种保存方式他们分别是Baseline JPEG(标准型)和Progressive JPEG(渐进式)。两种格式有相同尺寸以及图像数据,他们的扩展名也是相同的,唯一的区别是二者显示的方式不同。

Baseline JPEG与Progressive JPEG的差异

Baseline JPEG

这种类型的JPEG文件存储方式是按从上到下的扫描方式,把每一行顺序的保存在JPEG文件中。打开这个文件显示它的内容时,数据将按照存储时的顺序从上到下一行一行的被显示出来,直到所有的数据都被读完,就完成了整张图片的显示。如果文件较大或者网络下载速度较慢,那么就会看到图片被一行行加载的效果,这种格式的JPEG没有什么优点,因此,一般都推荐使用Progressive JPEG。

img

Progressive JPEG

和Baseline一遍扫描不同,Progressive JPEG文件包含多次扫描,这些扫描顺寻的存储在JPEG文件中。打开文件过程中,会先显示整个图片的模糊轮廓,随着扫描次数的增加,图片变得越来越清晰。这种格式的主要优点是在网络较慢的情况下,可以看到图片的轮廓知道正在加载的图片大概是什么。在一些网站打开较大图片时,你就会注意到这种技术。

img

图片如何保存为Progressive JPEG?

说了这边多下面就改讲讲怎么讲图片保存为或者转化为Progressive JPEG了。

PhotoShop

在PS中有“存储为web所用格式”,打开后选择“连续”就是渐进式JPEG。

img

Linux

检测是否为progressive jpeg : identify -verbose filename.jpg | grep Interlace(如果输出 None 说明不是progressive jpeg;如果输出 Plane 说明是 progressive jpeg。)

将basic jpeg转换成progressive jpeg:> convert infile.jpg -interlace Plane outfile.jpg

PHP

使用imageinterlaceimagejpeg函数我们可以轻松解决转换问题。

<?php
$im = imagecreatefromjpeg('pic.jpg');
imageinterlace($im, 1);
imagejpeg($im, './php_interlaced.jpg', 100);
imagedestroy($im);
?>

Python

import PIL
from exceptions import IOError

img = PIL.Image.open("c:\\users\\biaodianfu\\pictures\\in.jpg")
destination = "c:\\users\\biaodianfu\\pictures\\test.jpeg"
try:
img.save(destination, "JPEG", quality=80, optimize=True, progressive=True)
except IOError:
PIL.ImageFile.MAXBLOCK = img.size[0] * img.size[1]
img.save(destination, "JPEG", quality=80, optimize=True, progressive=True)

C#

using (Image source = Image.FromFile(@"D:\temp\test2.jpg")) { 
ImageCodecInfo codec = ImageCodecInfo.GetImageEncoders().First(c => c.MimeType == "image/jpeg");
EncoderParameters parameters = new EncoderParameters(3);
parameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 100L);
parameters.Param[1] = new EncoderParameter(System.Drawing.Imaging.Encoder.ScanMethod, (int)EncoderValue.ScanMethodInterlaced);
parameters.Param[2] = new EncoderParameter(System.Drawing.Imaging.Encoder.RenderMethod, (int)EncoderValue.RenderProgressive);
source.Save(@"D:\temp\saved.jpg", codec, parameters);
}

鲸落知识点图片阅读需 3 分钟

前言

前端优化大概可以有以下几个方向:

  • 网络优化
  • 页面渲染优化
  • JS优化
  • 图片优化
  • webpack打包优化
  • React优化
  • Vue优化

网络优化

DNS预解析

link标签的rel属性设置dns-prefetch,提前获取域名对应的IP地址

使用缓存

减轻服务端压力,快速得到数据(如强缓存协商缓存等)

使用 CDN(内容分发网络)

用户与服务器的物理距离对响应时间也有影响。

内容分发网络(CDN)是一组分散在不同地理位置的 web 服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。

压缩响应

压缩组件通过减少 HTTP 请求产生的响应包的大小,从而降低传输时间的方式来提高性能。从 HTTP1.1 开始,Web 客户端可以通过 HTTP 请求中的 Accept-Encoding 头来标识对压缩的支持(这个请求头会列出一系列的压缩方法)

如果 Web 服务器看到请求中的这个头,就会使用客户端列出的方法中的一种来压缩响应。Web 服务器通过响应中的 Content-Encoding 头来告知 Web 客户端使用哪种方法进行的压缩

目前许多网站通常会压缩 HTML 文档,脚本和样式表的压缩也是值得的(包括 XML 和 JSON 在内的任何文本响应理论上都值得被压缩)。但是,图片和 PDF 文件不应该被压缩,因为它们本来已经被压缩了。

使用多个域名

Chrome 等现代化浏览器,都会有同域名限制并发下载数的情况,不同的浏览器及版本都不一样,使用不同的域名可以最大化下载线程,但注意保持在 2~4 个域名内,以避免 DNS 查询损耗。

避免图片src为空

虽然 src 属性为空字符串,但浏览器仍然会向服务器发起一个 HTTP 请求:

IE 向页面所在的目录发送请求; Safari、Chrome、Firefox 向页面本身发送请求; Opera 不执行任何操作。

页面渲染优化

避免css阻塞

css影响renderTree的构建,会阻塞页面的渲染,因此应该尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)的将css资源加载

降低css选择器的复杂度

浏览器读取选择器,遵循的原则是从选择器的右边到左边读取。

  • 减少嵌套:最多不要超过三层,并且后代选择器的开销较高,慎重使用
  • 避免使用通配符,对用到的元素进行匹配即可
  • 利用继承,避免重复匹配和定义
  • 正确使用类选择器和id选择器

避免使用CSS 表达式

css 表达式会被频繁地计算。

避免js阻塞

js可以修改CSSOM和DOM,因此js会阻塞页面的解析和渲染,并且会等待css资源的加载。也就是说js会抢走渲染引擎的控制权。所以我们需要给js资源添加defer或者async,延迟js脚本的执行。

使用外链式的js和css

在现实环境中使用外部文件通常会产生较快的页面,因为 JavaScript 和 CSS 有机会被浏览器缓存起来。对于内联的情况,由于 HTML 文档通常不会被配置为可以进行缓存的,所以每次请求 HTML 文档都要下载 JavaScript 和 CSS。所以,如果 JavaScript 和 CSS 在外部文件中,浏览器可以缓存它们,HTML 文档的大小会被减少而不必增加 HTTP 请求数量。

使用字体图标 iconfont 代替图片图标

  • 图片会增加网络请求次数,从而拖慢页面加载时间
  • iconfont可以很好的缩放并且不会添加额外的请求

首屏加载优化

  • 使用骨架屏或者动画优化用户体验
  • 资源按需加载,首页不需要的资源延迟加载

减少重绘和回流

  • 增加多个节点使用documentFragment:不是真实dom的部分,不会引起重绘和回流

  • 用 translate 代替 top ,因为 top 会触发回流,但是translate不会。所以translate会比top节省了一个layout的时间

  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局);opacity 代替 visiabilityvisiability会触发重绘(paint),但opacity不会。

  • 把 DOM 离线后修改,比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改 100 次,然后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量

    javascript复制代码for (let i = 0; i < 1000; i++) {
    // 获取 offsetTop 会导致回流,因为需要去获取正确的值
    console.log(document.querySelector('.test').style.offsetTop)
    }
  • 尽量少用table布局,table布局的话,每次有单元格布局改变,都会进行整个tabel回流重绘;

  • 最好别频繁去操作DOM节点,最好把需要操作的样式,提前写成class,之后需要修改。只需要修改一次,需要修改的时候,直接修改className,做成一次性更新多条css DOM属性,一次回流重绘总比多次回流重绘要付出的成本低得多;

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame

  • 每次访问DOM的偏移量属性的时候,例如获取一个元素的scrollTop、scrollLeft、scrollWidth、offsetTop、offsetLeft、offsetWidth、offsetHeight之类的属性,浏览器为了保证值的正确也会回流取得最新的值,所以如果你要多次操作,最取完做个缓存。更加不要for循环中访问DOM偏移量属性,而且使用的时候,最好定义一个变量,把要需要的值赋值进去,进行值缓存,把回流重绘的次数减少;

  • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层。

JS中的性能优化

使用事件委托

防抖和节流

尽量不要使用JS动画

css3动画和canvas动画都比JS动画性能好

多线程

复杂的计算开启webWorker进行计算,避免页面假死

计算结果缓存

减少运算次数,比如vue中的computed、react中的useMemo

图片的优化

雪碧图

借助减少http请求次数来进行优化

图片懒加载

在图片即将进入可视区域的时候进行加载

使用CSS3代替图片

有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好

图片压缩

压缩方法有两种,一是通过在线网站进行压缩,二是通过 webpack 插件 image-webpack-loader

使用渐进式jpeg

使用渐进式jpeg,会提高用户体验 参考文章

使用 webp 格式的图片

webp 是一种新的图片文件格式,它提供了有损压缩和无损压缩两种方式。在相同图片质量下,webp 的体积比 png 和 jpg 更小。

webpack打包优化

抽离css

借助mini-css-extract-plugin:本插件会将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
{
test: /\.less$/,
use: [
// "style-loader", // 不再需要style-loader,⽤MiniCssExtractPlugin.loader代替
MiniCssExtractPlugin.loader,
"css-loader", // 编译css
"postcss-loader",
"less-loader" // 编译less
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name]_[contenthash:6].css",
chunkFilename: "[id].css"
})
]

代码压缩

  • JS代码压缩:mode:production,使用的是terser-webpack-plugin

    module.exports = {
       // ...
       optimization: {
           minimize: true,
           minimizer: [
               new TerserPlugin({}),
          ]
      }
    }
  • CSS代码压缩:css-minimizer-webpack-plugin

    module.exports = {
       // ...
       optimization: {
           minimize: true,
           minimizer: [
               new CssMinimizerPlugin({})
          ]
      }
    }
  • HTML代码压缩:设置了minify,实际会使用另一个插件html-minifier-terser

    module.exports = {
       ...
       plugin:[
           new HtmlwebpackPlugin({
               ...
               minify:{
                   minifyCSS:false, // 是否压缩css
                   collapseWhitespace:false, // 是否折叠空格
                   removeComments:true // 是否移除注释
              }
          })
      ]
    }
  • 文件大小压缩:对文件的大小进行压缩,减少http传输过程中宽带的损耗

    new ComepressionPlugin({
       test:/.(css|js)$/,  // 哪些文件需要压缩
       threshold:500, // 设置文件多大开始压缩
       minRatio:0.7, // 至少压缩的比例
       algorithm:"gzip", // 采用的压缩算法
    })
  • 图片压缩

    module: {
     rules: [
      {
         test: /.(png|jpg|gif)$/,
         use: [
          {
             loader: 'file-loader',
             options: {
               name: '[name]_[hash].[ext]',
               outputPath: 'images/',
            }
          },
          {
             loader: 'image-webpack-loader',
             options: {
               // 压缩 jpeg 的配置
               mozjpeg: {
                 progressive: true,
                 quality: 65
              },
               // 使用 imagemin**-optipng 压缩 png,enable: false 为关闭
               optipng: {
                 enabled: false,
              },
               // 使用 imagemin-pngquant 压缩 png
               pngquant: {
                 quality: '65-90',
                 speed: 4
              },
               // 压缩 gif 的配置
               gifsicle: {
                 interlaced: false,
              },
               // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
               webp: {
                 quality: 75
              }
            }
          }
        ]
      },
    ]
    }

Tree shaking 去除死代码

Tree Shaking 是一个术语,在计算机中表示消除死代码,依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)

webpack实现Tree shaking有两种不同的方案:

  • usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的
  • sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用

css也可以进行tree shaking优化:安装PurgeCss插件

减少ES6转化ES5的冗余

Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数。在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。

代码分离

将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件

默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度

代码分离可以分出更小的bundle,以及控制资源加载优先级,提供代码的加载性能

可以通过splitChunksPlugin来实现,该插件webpack已经默认安装和集成,只需要配置即可

VUE

  • v-for添加key
  • 路由懒加载
  • 第三方插件按需引入
  • 合理使用computed和watch
  • v-for的同时避免使用v-if
  • destory时销毁事件:比如addEventListener添加的事件、setTimeout、setInterval、bus.$on绑定的监听事件等

React

  • map循环展示添加key
  • 路由懒加载
  • 第三方插件按需引入
  • 使用scu,memo或者pureComponent避免不必要的渲染
  • 合理使用useMemo、memo、useCallback

参考链接

当面试官问我前端可以做的性能优化有哪些 - 掘金 (juejin.cn)


鲸落知识点js阅读需 11 分钟

什么是跨域

什么是同源策略及其限制内容

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取
  • Ajax请求发送不出去

跨域

当协议、域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。

注意:跨域请求产生时,请求是发出去了,也是有响应的,仅仅是浏览器同源策略,认为不安全,拦截了结果,不将数据传递我们使用罢了

URL说明是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js
同一域名下允许
http://www.a.com/lab/a.js
http://www.a.com/script/b.js
同一域名下不同文件夹允许
http://www.a.com:8000/a.js
http://www.a.com/b.js
同一域名,不同端口不允许
http://www.a.com/a.js
https://www.a.com/b.js
域名和域名对应ip不允许
http://www.a.com/a.js
http://script.a.com/b.js
主域相同,子域不同不允许
(cookie这种情况下也不允许访问)
http://www.a.com/a.js
http://a.com/b.js
同一域名,不同二级域名不允许
(cookie这种情况下也不允许访问)
http://www.cnblogs.com/a.js
http://www.a.com/b.js
不同域名不允许

解决方案如下

cors

概念

  • CORS 需要浏览器和后端同时支持
  • 浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
  • 虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

简单请求

只要同时满足以下两大条件,就属于简单请求

  • 条件1:使用下列方法之一:

    • GET

    • HEAD

      • POST
  • 条件2:HTTP的头信息不超出以下几种字段

    • Accept

    • Accept-Language

    • Content-Language

    • Last-Event-ID

    • Content-Type 的值仅限于下列三者之一:

      • text/plain

      • multipart/form-data

      • application/x-www-form-urlencoded

复杂请求

不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

JSONP

JSONP原理

利用 <script> 标签没有跨域限制,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

JSONP和AJAX对比

JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

JSONP缺点

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

nginx反向代理

服务器配置代理服务器

#如果监听到请求接口地址是 www.xxx.com/api/page ,nginx就向http://www.yyy.com:9999/api/page这个地址发送请求
server {
listen 80;
server_name www.xxx.com;
#判过滤出含有api的请求
location /api/ {
proxy_pass http://www.yyy.com:9999; #真实服务器的地址
}
}

proxy代理服务器

// vue.config.js/webpack.config.js 
// 优点:可以配置多个代理,且可灵活控制请求是否走代理
// 缺点:配置繁琐,发起代理请求时必须加上配置好的前缀
module.exports={
devServer:{
proxy:{
'/api01':{
target:'http://xxx.xxx.xxx:5000',
changeOrigin:true,
// 重写请求,根据接口详情,判断是否需要
pathRewrite:{
'^/api01':''
}
},
'/api02':{
target:'http://xxx.xxx.xxx:5001',
changeOrigin:true,
// 重写请求,根据接口详情,判断是否需要
pathRewrite:{
'^/api02':''
}
}
}
}
}
// changeOrigin设置为true时,服务器收到的请求头的host与服务器地址相同
// changeOrigin设置为false时,服务器收到的请求头的host与前端地址相同

websocket

WebSocket 协议是一种基于 TCP 的通信协议,与 HTTP 协议不同,它不受同源策略的限制,没有使用HTTP响应头,因此也没有跨域的限制

window.name + iframe

window.name属性可设置或者返回存放窗口名称的一个字符串。他的神器之处在于name值在不同页面或者不同域下加载后依旧存在,没有修改就不会发生变化,并且可以存储非常长的name(2MB)

其中a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

 // a.html(http://localhost:3000/b.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false;
}else{
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
}
}
</script>

b.html为中间代理页,与a.html同域,内容为空。

 // c.html(http://localhost:4000/c.html)
<script>
window.name = '我不爱你'
</script>

location.hash + iframe

原理就是通过 url 带 hash ,通过一个非跨域的中间页面来传递数据

一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。 同样的,a.html 和 b.htm l 是同域的,都是 http://localhost:8000,而 c.html 是http://localhost:8080

// a.html
<iframe src="http://localhost:8080/hash/c.html#name1"></iframe>
<script>
  console.log(location.hash);
  window.onhashchange = function() {
    console.log(location.hash);
  };
</script>
// b.html
<script>
  window.parent.parent.location.hash = location.hash;
</script>
// c.html
<body></body>
<script>
  console.log(location.hash);
  const iframe = document.createElement("iframe");
  iframe.src = "http://localhost:8000/hash/b.html#name2";
  document.body.appendChild(iframe);
</script>

document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。 只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值

// a.html
<body>
helloa
<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'zf1.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body>
// b.html
<body>
hellob
<script>
document.domain = 'zf1.cn'
var a = 100;
</script>
</body>

不允许iframe的设置

添加X-Frame-Options响应头,值为deny


鲸落知识点js阅读需 6 分钟

for 循环100000次

最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的

const renderList = async () => {
console.time('列表时间')
const list = await getList()
list.forEach(item => {
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
})
console.timeEnd('列表时间')
}
renderList()

setTimeout分页渲染(时间分片)

这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了

const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
}, 0)
}
render(page)
console.timeEnd('列表时间')
}

requestAnimationFrame

requestAnimationFrame 和 setTimeout 的区别:

  • requestAnimationFrame的调用频率通常为每秒60次。这意味着我们可以在每次重绘之前更新动画的状态,并确保动画流畅运行,而不会对浏览器的性能造成影响。
  • setIntervalsetTimeout它可以让我们在指定的时间间隔内重复执行一个操作,不考虑浏览器的重绘,而是按照指定的时间间隔执行回调函数,可能会被延迟执行,从而影响动画的流畅度。

使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame

requestAnimationFrame不是定时器!不是定时器!!!只是一个用作定时器的帧函数

const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
// 使用requestAnimationFrame代替setTimeout
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}

文档碎片 + requestAnimationFrame

先解释一下什么是 DocumentFragment ,文献引用自MDN

DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的`Document`使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为`DocumentFragment`不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。可以使用`document.createDocumentFragment`方法或者构造函数来创建一个空的`DocumentFragment

文档碎片的好处

  • 之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能
  • 页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片
const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
requestAnimationFrame(() => {
// 创建一个文档碎片
const fragment = document.createDocumentFragment()
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
// 先塞进文档碎片
fragment.appendChild(div)
}
// 一次性appendChild
container.appendChild(fragment)
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}

懒加载

一句话解释:最开始不渲染所有数据,只展示视图上可见的数据,当滚动到页面底部时,加载更多数据

  • 位置计算 + 滚动事件 (Scroll) + DataSet API

    • clientTopoffsetTopclientHeight 以及 scrollTop 各种关于图片的高度作比对

    • 监听 window.scroll 事件

    • // 控制图片懒加载
      <img data-src="shanyue.jpg" />
      // 首先设置一个临时 Data 属性 data-src,控制加载时使用 src 代替 data-src,可利用 DataSet API 实现
      img.src = img.datset.src
  • getBoundingClientRect API + Scroll with Throttle + DataSet API

    • 如何判断图片出现在了当前视口?
    • Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。img.getBoundingClientRect().top < document.documentElement.clientHeight;
  • IntersectionObserver api

    • 上一个方案使用的方法是: window.scroll 监听 Element.getBoundingClientRect() 并使用 _.throttle 节流
    • 浏览器出了一个三合一事件: IntersectionObserver API,一个能够监听元素是否到了当前视口的事件,一步到位!

虚拟列表

概念

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。

假设有1万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。为了防止滑动过快导致的白屏现象,我们可以使用预加载的方式多加载一些数据出来。

image-20230810165500313

实现

实现屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

  • 计算当前可视区域起始数据索引(startIndex)
  • 计算当前可视区域结束数据索引(endIndex)
  • 计算当前可视区域的数据,并渲染到页面中
  • 计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上

image-20230810170401342

由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
  • infinite-list-container可视区域的容器
  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条
  • infinite-list 为列表项的渲染区域

接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

  • 假定可视区域高度固定,称之为screenHeight
  • 假定列表每项高度固定,称之为itemSize
  • 假定列表数据称之为listData
  • 假定当前滚动位置称之为scrollTop

则可推算出:

  • 列表总高度listHeight = listData.length * itemSize
  • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
  • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
  • 数据的结束索引endIndex = startIndex + visibleCount
  • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

  • 偏移量startOffset = scrollTop - (scrollTop % itemSize)

简易代码

最终的简易代码如下:

<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div ref="items"
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"
>{{ item.value }}</div>
</div>
</div>
</template>

<script>
export default {
name:'VirtualList',
props: {
//所有列表数据
listData:{
type:Array,
default:()=>[]
},
//每项高度
itemSize: {
type: Number,
default:200
}
},
computed:{
//列表总高度
listHeight(){
return this.listData.length * this.itemSize;
},
//可显示的列表项数
visibleCount(){
return Math.ceil(this.screenHeight / this.itemSize)
},
//偏移量对应的style
getTransform(){
return `translate3d(0,${this.startOffset}px,0)`;
},
//获取真实显示列表数据
visibleData(){
return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
}
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
data() {
return {
//可视区域高度
screenHeight:0,
//偏移量
startOffset:0,
//起始索引
start:0,
//结束索引
end:null,
};
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemSize);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemSize);
}
}
};
</script>

多大量数据进行处理 — Web Worker

因为js是单线程运行的,在遇到一些需要处理大量数据的js时,可能会阻塞页面的加载,造成页面的假死。

在HTML5的新规范中,实现了 Web Worker 来引入 js 的 “多线程” 技术, 可以让我们在页面主运行的 js 线程中,加载运行另外单独的一个或者多个 js 线程

一句话: Web Worker专门处理复杂计算的,从此让前端拥有后端的计算能力

参考链接

后端一次给你10万条数据,如何优雅展示 - 掘金 (juejin.cn)

高性能渲染十万条数据(虚拟列表) - 掘金 (juejin.cn)


鲸落知识点js阅读需 8 分钟

写在前面

我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。

但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。

但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。

Session 、 Cookie 和 token 的主要目的就是为了弥补 HTTP 的无状态特性。

什么是cookie

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的靠的是 domain)

    • Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如 www.example.com 会设为 example.com,而且以后如果访问example.com的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。

什么是session

  • session 是另一种记录服务器和客户端会话状态的机制
  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中

session验证流程

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

cookie和session的区别

  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
  • 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
  • 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效
  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

什么是token

Token是在身份验证和授权过程中广泛使用的一种机制,用于确认用户的身份并获得权限。它通常是一个字符串,由服务器生成并返回给客户端(例如,Web浏览器或移动应用程序)。Token在客户端和服务器之间进行传递,用于识别和验证用户。

token身份验证流程

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
  4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
  • 每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
  • 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
  • token 完全由应用管理,所以它可以避开同源策略

token和session的区别

  1. 定义和作用:
    • Token(令牌):Token是在身份验证和授权过程中用于确认用户身份和获得权限的一种机制。它通常是一个字符串,由服务器生成并返回给客户端,用于在客户端和服务器之间传递身份验证和授权信息。
    • Session(会话):Session是服务器端用于维护用户状态和身份信息的一种机制。服务器为每个客户端创建一个会话,用于跟踪用户的状态,通过Session ID标识不同的会话。
  2. 存储位置:
    • Token:Token通常存储在客户端,比如浏览器的Cookie或本地存储中,以便客户端可以将其附加到后续请求中。
    • Session:Session数据通常存储在服务器端,而不是在客户端。服务器通过Session ID来标识每个客户端的会话状态。
  3. 状态维护:
    • Token:Token是无状态的,服务器不需要在后端存储Token的状态信息,因为所有必要的信息都包含在Token本身中。
    • Session:Session是有状态的,服务器需要在后端维护会话状态信息,以便跟踪用户状态和保存临时数据。
  4. 应用场景:
    • Token:Token在Web API、移动应用和分布式系统中广泛使用,特别适合跨服务器进行身份验证和授权。
    • Session:Session通常在传统的Web应用中使用,用于跟踪用户状态和保存用户相关的数据。
  5. 安全性:
    • Token:由于Token包含所有必要的验证信息,因此必须谨慎处理,确保其不被非法获取或篡改,通常通过加密和签名来增加安全性。
    • Session:Session数据存储在服务器端,相对较安全,但仍然需要采取措施防止会话劫持和其他安全漏洞。

Cookie 作为 HTTP 规范,其出现历史久远,因此存在一些历史遗留问题,比如跨域限制等,并且 Cookie 作为 HTTP 规范中的内容,其存在默认存储以及默认发送的行为,存在一定的安全性问题。相较于 Cookie,token 需要自己存储,自己进行发送,不存在跨域限制,因此 Token 更加的灵活,没有 Cookie 那么多的“历史包袱”束缚,在安全性上也能够做更多的优化。

Token 有什么 优势?

Token的无状态特性、安全性、可扩展性以及跨平台和跨语言的支持,使得它成为现代Web应用和API身份验证和授权的首选机制。


鲸落知识点js阅读需 7 分钟

单页面程序(SPA)

概念

单页应用程序(SPA)全称是: Single-page application,SPA应用是在客户端渲染的(我们称之为CSR

  • SPA应用默认只返回一个空HTML页面,如body只有<div id= "app”></div>
  • 而整个应用程序的内容都是通过Javascript动态加载,包括应用程序的逻辑、UI以及与服务器通信相关的所有数据。
  • 常见的SPA应用框架有Vue、React等

优点

  • 只需加载一次:SPA应用程序只需要在第一次请求时加载页面,页面切换不需重新加载,而传统的Web应用程序必须在每次请求时都得加载页面,需要花费更多时间。因此,SPA页面加载速度要比传统 Web应用程序更快。

  • 更好的用户体验

    • SPA提供类似于桌面或移动应用程序的体验。用户切换页面不必重新加载新页面
    • 切换页面只是内容发生了变化,页面并没有重新加载,从而使体验变得更加流畅
  • 可轻松的构建功能丰富的Web应用程序

缺点

  • SPA应用默认只返回一个空HTML页面,不利于SEO
  • 首屏加载的资源过大时,一样会影响首屏的渲染
  • 也不利于构建复杂的项目,复杂Web应用程序的大文件可能变得难以维护

客户端渲染(CSR)

渲染流程:浏览器请求url --> 服务器返回index.html(空body、白屏) --> 再次请求bundle.js、路由分析 --> 浏览器渲染

bundle.js体积越大,会导致浏览器白屏时间越长。

image-20230714162005631

静态站点生成(SSG)

概念

静态站点生成(SSG)全称是: Static Site Generate,是预先生成好的静态网站。

  • SSG应用一般在构建阶段就确定了网站的内容。
  • 如果网站的内容需要更新了,那必须得重新再次构建和部署。
  • 构建SSG应用常见的库和框架有: Vue Nuxt、React Next.js 等。

优点

  • 访问速度非常快,因为每个页面都是在构建阶段就已经提前生成好了。
  • 直接给浏览器返回静态的HTML,也有利于SEO
  • SSG应用依然保留了SPA应用的特性,比如:前端路由、响应式数据、虚拟DOM等

缺点

  • 页面都是静态,不利于展示实时性的内容,实时性的更适合SSR。
  • 如果站点内容更新了,那必须得重新再次构建和部署。

服务器端渲染(SSR)

概念

服务器端渲染全称是: Server Side Render,在服务器端渲染页面,并将渲染好HTML返回给浏览器呈现。

  • SSR应用的页面是在服务端渲染的,用户每请求一个SSR页面都会先在服务端进行渲染,然后将渲染好的页面,返回给浏览器呈现。
  • 构建SSR应用常见的库和框架有: Vue Nuxt、React Next.js等(SSR应用也称同构应用)。

优点

  • 更快的首屏渲染速度
    • 浏览器显示静态页面的内容要比JavaScript动态生成的内容快得多。
    • 当用户访问首页时可立即返回静态页面内容,而不需要等待浏览器先加载完整个应用程序。
  • 更好的SEO
    • 爬虫是最擅长爬取静态的HTML页面,服务器端直接返回一个静态的HTML给浏览器。
    • 这样有利于爬虫快速抓取网页内容,并编入索引,有利于SEO。
    • SSR应用程序在Hydration 之后依然可以保留Web应用程序的交互性。比如:前端路由、响应式数据、虚拟DOM等。

缺点

  • SSR通常需要对服务器进行更多API调用,以及在服务器端渲染需要消耗更多的服务器资源,成本高。
  • 增加了一定的开发成本,用户需要关心哪些代码是运行在服务器端,哪些代码是运行在浏览器端。
  • SSR配置站点的缓存通常会比SPA站点要复杂一点。

渲染流程

  • 阶段一:浏览器请求url --> 服务器路由分析、执行渲染 --> 服务器返回index.html(实时渲染的内容,字符串) --> 浏览器渲染
  • 阶段二:浏览器请求bundle.js --> 服务器返回bundle.js --> 浏览器路由分析、生成虚拟DOM --> 比较DOM变化、绑定事件 --> 二次渲染

image-20230714162121945

image-20230714162203910


鲸落知识点SPASSR阅读需 4 分钟

前言

个人平时比较喜欢捣鼓一些前端的技术,在和群友聊天的时候,了解到了微前端这个概念

然后在实习的时候公司也用到了微前端的相关概念,使用iframe将多个子应用嵌套在一起

在维护项目的时候,感觉不管是启动还是加应用的速度都是比较慢的

在后面有时间的时候我就捣鼓捣鼓了当前比较流行的微前端框架:qiankun

今天来纪录一些关于微前端的一些知识

如果你想了解我关于微前端的简单应用,直达链接:qiankun的使用 - 鲸落 (xiaojunnan.cn)

为什么要使用微前端

当前,基于 vuereactangular单页应用开发模式已经成为业界主流。受益于它们丰富的生态,我们可以使用这些技术快速构建一个新的应用,迅速响应市场。随着公司业务的不断发展,应用开始变得庞大臃肿,逐渐成为一个巨石应用,难以维护不说,每次开发、上线新需求时还需要花费不少的时间来构建项目,对开发人员的开发效率和体验都造成了不好的影响。因此将一个巨石应用拆分为多个子应用势在必行。

一般情况下,我们会基于业务来拆分应用。每个应用都有一个自己的仓库,独立开发独立部署独立访问独立维护,还可以根据团队的特点自主选择适合自己的技术栈,极大的提升了开发人员的效率和体验。

应用拆分能给我们带来便利,但同时也给我们带来了新的挑战,那就是应用的聚合。对于客户来说,他们在使用我们的产品时,更希望呈现在自己面前的是一个完整的应用,而不是分散的多个子应用。因此我们需要选择一个合适的方案,能兼容不同的技术栈,将已经拆分的子应用重新聚合。

微前端,正是这样一个合适的方案,来帮助我们面对上述挑战。

什么是微前端

微前端,早已是一个老生常谈的概念,它于 2016 年首次出现在 ThoughtWorks Technology Radar 上,将后端微服务的概念扩展到了前端世界。

微服务,维基上对其定义为:一种软件开发技术- 面向服务的体系结构(SOA)架构样式的一种变体,将应用程序构造为一组松散耦合的服务,并通过轻量级的通信协议组织起来。具体来讲,就是将一个单体应用,按照一定的规则拆分为一组服务。这些服务,各自拥有自己的仓库,可以独立开发、独立部署,有独立的边界,可以由不同的团队来管理,甚至可以使用不同的编程语言来编写。但对前端来说,仍然是一个完整的服务。

微服务,主要是用来解决庞大的一整块后端服务带来的变更和扩展的限制

同样的,面对越来越重的前端应用,可将微服务的思想照搬到前端,就有了微前端的概念。像微服务一样,一个前端应用,也可以按照一定的规则,拆分为不同的子应用,独立开发,独立部署,然后聚合成一个完整的应用面对客户。

微前端能给我们带来什么

  • 独立开发、独立部署
  • 技术栈无关
  • 简单、分离、松耦合的代码仓库
  • 遗留系统迁移
    • 对于一些使用老技术栈开发的应用,我们没有理由浪费时间和精力,可以通过微前端方案直接整合到新的应用中。
  • 技术栈升级
    • 我们可以重起一个应用,循序渐进的重构应用,然后使用微前端方案将新旧应用聚合在一起。

使用微前端面临的挑战

微前端方案给我们带来巨大便利的同时,也给我们带来了新的挑战。在实现微前端应用时,我们必须要考虑以下问题:

  • 子应用切换
  • 应用相互隔离,互不干扰
  • 子应用之间通信
  • 多个子应用并存
  • 用户状态的存储 - 免登

微前端常用技术方案

总结

目前,业界主流的微前端实现方案主要有:

  • 路由分发式微前端
  • iframe
  • single-spa
  • qiankun
  • webpack5:module federation
  • Web Component

路由分发式微前端

路由分发式微前端,即通过路由将不同的业务分发到不同的独立前端应用上。最常用的方案是通过 HTTP 服务的反向代理来实现。

在部署服务器的时候设置代理,请求不通的路径

 http {
server {
listen 80;
server_name xxx.xxx.com;
location /api/ {
proxy_pass http://localhost:3001/api;
}
location /web/admin {
proxy_pass http://localhost:3002/api;
}
location / {
proxy_pass /;
}
}
}

优点

  • 实现简单
  • 不需要对现有应用进行改造
  • 完全技术栈无关

缺点

  • 用户体验不好,每次切换应用时,浏览器都需要重新加载页面
  • 多个子应用无法并存
  • 局限性比较大
  • 子应用之间的通信比较困难
  • 子应用切换时需要重新登录

iframe

iframe 作为一项非常古老的技术,也可以用于实现微前端。通过 iframe,我们可以很方便的将一个应用嵌入到另一个应用中,而且两个应用之间的 css 和 javascript 是相互隔离的,不会互相干扰。

优点

  • 实现简单
  • css 和 js 天然隔离,互不干扰
  • 完全技术栈无关
  • 多个子应用可以并存
  • 不需要对现有应用进行改造

缺点

  • 用户体验不好,每次切换应用时,浏览器需要重新加载页面
  • UI 不同步,DOM 结构不共享
  • 全局上下文完全隔离,内存变量不共享,子应用之间通信、数据同步过程比较复杂
  • 对 SEO 不友好
  • 子应用切换时可能需要重新登录,体验不好

single-spa

官网:single-spa

路由转发模式iframe 模式尽管可以实现微前端,但是体验不好。我们每次切换回已经访问过的子应用时,都需要重新加载子应用,对性能有很大的影响。

我们知道,现在前端应用开发的主流模式为基于 vue / react/ angular单页应用开发模式。在这种模式下,我们需要维护一个路由注册表,每个路由对应各自的页面组件 url切换路由时,如果是一个新的页面,需要动态获取路由对应的 js 脚本,然后执行脚本并渲染出对应的页面;如果是一个已经访问过的页面,那么直接从缓存中获取已缓存的页面方法,执行并渲染出对应的页面。

那么,微前端也有没有类似的实现方案,来获得和单页应用一样的用户体验呢?

答案是有的。 single-spa 提供了新的技术方案,可以帮忙我们实现类似单页应用的体验。

single-spa 方案中,应用被分为两类:基座应用子应用。其中,子应用就是文章上面描述的需要聚合的子应用;而基座应用,是另外的一个单独的应用,用于聚合子应用

和单页应用的实现原理类似,single-spa 会在基座应用中维护一个路由注册表每个路由对应一个子应用。基座应用启动以后,当我们切换路由时,如果是一个新的子应用,会动态获取子应用的 js 脚本,然后执行脚本并渲染出相应的页面;如果是一个已经访问过的子应用,那么就会从缓存中获取已经缓存的子应用,激活子应用并渲染出对应的页面。

优点

  • 切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验
  • 完全技术栈无关
  • 多个子应用可并存
  • 生态丰富

缺点

  • 需要对原有应用进行改造,应用要兼容接入 sing-spa 和独立使用
  • 有额外的学习成本
  • 使用复杂,关于子应用加载、应用隔离、子应用通信等问题,需要框架使用者自己实现
  • 子应用间相同资源重复加载
  • 启动应用时,要先启动基座应用

qiankun

官网:qiankun

single-spa 一样,qiankun 也能给我们提供类似单页应用的用户体验。qiankun 是在 single-spa 的基础上做了二次开发,在框架层面解决了使用 single-spa 时需要开发人员自己编写子应用加载、通信、隔离等逻辑的问题

优点

  • 切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验
  • 相比 single-spa,解决了子应用加载、应用隔离、子应用通信等问题,使用起来相对简单
  • 完全和技术栈无关
  • 多个子应用可并存

缺点

  • 需要对原有应用进行改造,应用要兼容接入 qiankun 和独立使用
  • 有额外的学习成本
  • 相同资源重复加载
  • 启动应用时,要先启动基座应用

webpack5:module federation

webpack5,提供了一个新的特性 - module federation。基于这个特性,我们可以在一个 javascript 应用中动态加载并运行另一个 javascript 应用的代码,并实现应用之间的依赖共享

通过 module federation,我们可以在一个应用里面动态渲染另一个应用的页面,这样也就实现了多个子应用的聚合

优点

  • 不需要对原有应用进行改造,只需改造打包脚本
  • 切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验
  • 多个子应用可并存
  • 相同资源不需要重复加载
  • 开发技术栈无关
  • 应用启动后,无需加载与自己无关的资源
  • 免登友好

缺点

  • 构建工具只能使用 webpack5
  • 有额外的学习成本
  • 对老项目不友好,需要对 webpack 进行改造

Web Component

案例:京东的开源微前端框架 MicroApp (micro-zoe.github.io)

基于 Web ComponentShadow Dom 能力,我们也可以实现微前端,将多个子应用聚合起来。

优点

  • 实现简单
  • css 和 js 天然隔离,互不干扰
  • 完全技术栈无关
  • 多个子应用可以并存
  • 不需要对现有应用进行改造

缺点

  • 主要是浏览器兼容性问题
  • 开发成本较高

写在最后

还有许多优秀的微前端框架,如字节跳动的Garfish, 阿里飞冰团队的icestark 等等。有需要了解的可以深入了解一下

参考链接

微前端学习系列(一):微前端介绍 - 掘金 (juejin.cn)


鲸落知识点微前端阅读需 10 分钟