阅读视图

发现新文章,点击刷新页面。

http缓存

概述

浏览器资源请求的时候,必不可少肯定会对资源进行缓存,这是对性能的一种必不可少的策略,为的就是带给用户更好的用户体验。

缓存

为什么缓存?

减少网络请求(网络请求不稳定性),让页面渲染更快

哪些资源可以被缓存?

静态资源(js css img)webpack/vite打包加contenthash根据内容生成hash

http缓存策略

强缓存

服务端在Response Headers中返回给客户端缓存标识字段(Cache-ControlExpires

Cache-Control的值取值

  • max-age:(常用)缓存的内容将在max-age秒后失效
  • no-cache:(常用)不要本地强制缓存,正常向服务端请求(只要服务端最新的内容)。需要使用协商缓存来验证缓存数据(Etag Last-Modified)
  • no-store: 不要本地强制缓存,也不要服务端做缓存,所有内容都不会缓存,强制缓存和协商缓存都不会触发
  • public: 所有内容都将被缓存(客户端和代理服务器都可缓存)
  • private: 所有内容只有客户端可以缓存

Expires

  • Expires:Thu, 31 Dec 2037 23:55:55 GMT(过期时间)
  • 已被Cache-Control代替

强制缓存的流程

  • 浏览器第一次请求资源,服务器返回资源和Cache-Control Expires
  • 浏览器第二次请求资源,会带上Cache-Control Expires,服务器根据这两个值判断是否命中强制缓存
  • 命中强制缓存,直接从缓存中读取资源,返回给浏览器
  • 未命中强制缓存,会带上If-Modified-Since If-None-Match,服务器根据这两个值判断是否命中协商缓存
  • 命中协商缓存,返回304,浏览器直接从缓存中读取资源
  • 未命中协商缓存,返回200,浏览器重新请求资源

流程图

协商缓存

属于服务端缓存策略,服务端判断客户端资源,是否和服务端资源一样如果判断一致则返回304(走缓存),否则返回200和最新资源, 服务端在Response Headers中返回给客户端缓存标识字段(Last-ModifiedEtag

  • Last-Modified和Etag会优先使用Etag,Last-Modified只能精确到秒级,如果资源被重复生成而内容不变,则Etag更准确
  • Last-Modified 服务端返回的资源的最后修改时间
  • If-Modified-Since 客户端请求时,携带的资源的最后修改时间(即Last-Modified的值)

协商缓存流程

image.png

image.png

示例

  • 通过Etag或Last-Modified命中缓存,没有返回资源,返回304,体积非常小

概览图

image.png

注意

强制缓存的优先级高于协商缓存

Jst执行上下文栈和变量对象

概述

在使用javascript编写代码的时候, 我们知道, 声明一个变量用var(早期), 定义一个函数用function,虽然现在我们声明变量已经不推荐使用var了,但是对于早些年的声明变量的var对应我们理解js程序运行过程理解很重要。

变量声明提升

首先是用var定义一个变量的时候, 例如:

var a = 10;

大部分的编程语言都是先声明变量再使用, 但是javascript有所不同, 上面的代码, 实际相当于这样执行:

var a;
a = 10;

因此有了下面这段代码的执行结果:

console.log(a); // 声明,先给一个默认值undefined;
var a = 10; // 赋值,对变量a赋值了10
console.log(a); // 10

上面的代码在第一行中并不会报错Uncaught ReferenceError: a is not defined, 是因为声明提升, 给了a一个默认值.

这就是最简单的变量声明提升.

函数声明提升

定义函数也有两种方法:

  • 函数声明: function foo () {};
  • 函数表达式: var foo = function () {}.

第二种函数表达式的声明方式更像是给一个变量foo赋值一个匿名函数.

那这两种在函数声明的时候有什么区别吗?

案例一:

console.log(f1) // function f1(){}
function f1() {} // 函数声明
console.log(f2) // undefined
var f2 = function() {} // 函数表达式

可以看到, 使用函数声明的函数会将整个函数都提升到作用域(后面会介绍到)的最顶部, 因此打印出来的是整个函数;

而使用函数表达式声明则类似于变量声明提升, 将var f2提升到了顶部并赋值undefined.


我们将案例一的代码添加一点东西:

案例二:

console.log(f1) // function f1(){...}
f1(); // 1
function f1() { // 函数声明
console.log('1')
}
console.log(f2) // undefined
f2(); // 报错: Uncaught TypeError: f2 is not a function
var f2 = function() { // 函数表达式
console.log('2')
}

虽然f1()在function f1 () {...}之前,但是却可以正常执行;

而f2()却会报错, 原因在案例一中也介绍了是因为在调用f2()时, f2还只是undifined并没有被赋值为一个函数, 因此会报错.

声明优先级: 函数大于变量

通过上面的介绍我们已经知道了两种声明提升, 但是当遇到函数和变量同名且都会被提升的情况时, 函数声明的优先级是要大于变量声明的.

  • 变量声明会被函数声明覆盖
  • 可以重新赋值

案例一:

console.log(f1); // f f1() {...}
var f1 = "10";
function f1() {
console.log('我是函数')
}
// 或者将 var f1 = "10"; 放到后面

案例一说明了变量声明会被函数声明所覆盖.

案例二:

console.log(f1); // f f1() { console.log('我是新的函数') }
var f1 = "10";

function f1() {
console.log('我是函数')
}

function f1() {
console.log('我是新的函数')
}

案例二说明了前面声明的函数会被后面声明的同名函数给覆盖.

如果你搞懂了, 来做个小练习?

练习

function test(arg) {
console.log(arg);
var arg = 10;
function arg() {
console.log('函数')
}
console.log(arg)
}
test('LinDaiDai');

答案

function test(arg) {
console.log(arg); // f arg() { console.log('函数') }
var arg = 10;
function arg() {
console.log('函数')
}
console.log(arg); // 10
}
test('LinDaiDai');
  1. 函数里的形参arg被后面函数声明的arg给覆盖了, 所以第一个打印出的是函数;
  2. 当执行到var arg = 10的时候, arg又被赋值了10, 所以第二个打印出10.

执行上下文栈的变化

先来看看下面两段代码, 在执行结果上是一样的, 那么它们在执行的过程中有什么不同呢

var scope = "global";
function checkScope () {
var scope = "local";
function fn () {
return scope;
}
return fn();
}
checkScope();
var scope = "global"
function checkScope () {
var scope = "local"
function fn () {
return scope
}
return fn;
}
checkScope()();

答案是 执行上下文栈的变化不一样。

在第一段代码中, 栈的变化是这样的:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();

可以看到fn后被推入栈中, 但是先执行了, 所以先被推出栈;


而在第二段中, 栈的变化为:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

由于checkscope是先推入栈中且先执行的, 所以在fn被执行前就被推出了.

VO/AO

接下来要介绍两个概念:

  • VO(变量对象) , 也就是variable object, 创建执行上下文时与之关联的会有一个变量对象,该上下文中的所有变量和函数全都保存在这个对象中。
  • AO(活动对象) , 也就是``activation object`,进入到一个执行上下文时,此执行上下文中的变量和函数都可以被访问到,可以理解为被激活了。

活动对象和变量对象的区别在于:

  • 变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。
  • 当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。

执行过程

首先来看看一个执行上下文(EC) 被创建和执行的过程:

  1. 创建阶段:
  • 创建变量、参数、函数arguments对象;
  • 建立作用域链;
  • 确定this的值.
  1. 执行阶段:

变量赋值, 函数引用, 执行代码.

进入执行上下文

在创建阶段, 也就是还没有执行代码之前

此时的变量对象包括(如下顺序初始化):

  1. 函数的所有形参(仅在函数上下文): 没有实参, 属性值为undefined;
  2. 函数声明:如果变量对象已经存在相同名称的属性,则完全替换这个属性;
  3. 变量声明:如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性

示例:

function fn (a) {
var b = 2;
function c () {};
var d = function {};
b = 20
}
fn(1)

对于上面的例子, 此时的AO是:

AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c() {},
d: undefined
}

可以看到, 形参arguments此时已经有赋值了, 但是变量还是undefined.

代码执行

到了代码执行时, 会修改变量对象的值, 执行完后AO如下:

AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 20,
c: reference to function c() {},
d: reference to function d() {}
}

在此阶段, 前面的变量对象中的值就会被赋值了, 此时变量对象处于激活状态.

总结

  • 全局上下文的变量对象初始化是全局对象, 而函数上下文的变量对象初始化只有Arguments对象;
  • EC创建阶段分为创建阶段和代码执行阶段;
  • 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
  • 在代码执行阶段,会再次修改变量对象的属性值.

js上下文

概述

js程序执行的时候,必然涉及到执行上下文那么什么叫“执行上下文”呢?

本文介绍如下

  • 执行上下文的类型
  • 执行上下文特点
  • 执行栈
  • 执行上下文的生命周期

概念

举个例子,生活中,相同的话在不同的场合说可能会有不同的意思,而这个说话的场合就是我们说话的语境。

同样对应在编程中, 对程序语言进行“解读”的时候,也必须在特定的语境中,这个语境就是javascript中的执行上下文。

一句话概括:

执行上下文就是javascript代码被解析和执行时所在环境的抽象概念。

执行上下文的类型

在js中,执行上下文分为以下三种:

  • 全局执行上下文:只有一个,也就是浏览器对象(即window对象),this指向的就是这个全局对象。
  • 函数执行上下文:有无数个,只有在函数被调用时才会被创建,每次调用函数都会创建一个新的执行上下文。
  • Eval函数执行上下文:js的eval函数执行其内部的代码会创建属于自己的执行上下文, 很少用而且不建议使用。

执行上下文的特点

  1. 单线程,只在主线程上运行;
  2. 同步执行,从上向下按顺序执行;
  3. 全局上下文只有一个,也就是window对象;
  4. 函数执行上下文没有限制;
  5. 函数每调用一次就会产生一个新的执行上下文环境。

JS如何管理多个执行上下文

通过上面介绍,我们知道了js代码在运行时可能会产生无数个执行上下文,那么它是如何管理这些执行上下文的呢?

同时由于js是单线程的,所以不能同时干两件事,必须一个个去执行,那么这么多的执行上下文是按什么顺序执行的呢?

执行栈

接下来就对上面的问题做出解答,管理多个执行上下文靠的就是执行栈,也被叫做调用栈

特点:后进先出(LIFO)的结构。

作用:存储在代码执行期间的所有执行上下文。

(LIFO: last-in, first-out,类似于向乒乓球桶中放球,最先放入的球最后取出)

js在首次执行的时候,会创建一个全局执行上下文并推入栈中。

每当有函数被调用时,引擎都会为该函数创建一个新的函数执行上下文然后推入栈中。

当栈顶的函数执行完毕之后,该函数对应的执行上下文就会从执行栈中pop出,然后上下文控制权移到下一个执行上下文。

例子:

var a = 1; // 1. 全局上下文环境
function bar (x) {
    console.log('bar')
    var b = 2;
    fn(x + b); // 3. fn上下文环境
}
function fn (c) {
    console.log(c);
}
bar(3); // 2. bar上下文环境

如下图:

image.png

执行上下文的生命周期

执行上下文的生命周期也非常容易理解, 分为三个阶段:

  1. 创建阶段
  2. 执行阶段
  3. 销毁阶段

创建阶段

创建阶段, 主要有是有这么几件事:

  1. 确定this的值, 也就是绑定this (This Binding);
  2. 词法环境(LexicalEnvironment) 组件被创建;
  3. 变量环境(VariableEnvironment) 组件被创建.

伪代码

ExecutionContext = {  
  ThisBinding = <this value>,     // 确定this 
  LexicalEnvironment = { ... },   // 词法环境
  VariableEnvironment = { ... },  // 变量环境
}

This Binding

通过上面的介绍我们知道实际开发主要用到两种执行上下文为全局函数, 那么绑定this在这两种上下文中也不同.

  • 全局执行上下文中, this指的就是全局对象, 浏览器环境指向window对象, nodejs中指向这个文件的module对象.
  • 函数执行上下文较为复杂, this的值取决于函数的调用方式. 具体有: 默认绑定、隐式绑定、显式绑定、new绑定、箭头函数.

词法环境

如上图, 词法环境是由两个部分组成的:

  1. 环境记录: 存储变量和函数声明的实际位置;
  2. 对外部环境的引用: 用于访问其外部词法环境.

同样的, 词法环境也主要有两种类型:

  1. 全局环境: 拥有一个全局对象(window对象)及其关联的所有属性和方法(比如数组的方法splice、concat等), 同时也包含了用户自定义的全局变量. 但是全局环境中没有外部环境的引用, 也就是外部环境引用为null.
  2. 函数环境: 用户在函数中自定义的变量和函数存储在环境记录中, 包含了arguments对象. 而对外部环境的引用可以是全局环境, 也可以是另一个函数环境(比如一个函数中包含了另一个函数).

变量环境

变量环境其实也是一个词法环境, 因此它具有上面定义的词法环境的所有属性.

在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var ) 绑定。

变量提升

在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 let 和 const 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

执行阶段

执行阶段主要做三件事情:

  1. 变量赋值
  2. 函数引用
  3. 执行其他的代码

注意

如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。

销毁阶段

执行完毕出栈,等待回收被销毁

结合Worker通知应用更新

概述

项目部署上线后,特别是网页项目,提示正在操作系统的用户去更新版本非常 important。一般我们都会用“刷新大法”来清理缓存,但是对于正在操作网页的用户,不造系统更新了,请求的还是老版本的资源。

为了确保用户能够及时获得最新的功能和修复的 bug,我们需要通知用户刷新页面获取最新的代码。

方案

每次打包时,都生成一个时间戳,作为系统的伪版本,放到JSON文件中,通过对比文件的响应头Etag判断是否有更新。具体步骤如下:

1: 在public文件夹下加入manifest.json文件,里面存放两个字段:更新内容、更新时间戳

2: 前端打包的时候向manifest.json写入当前时间戳信息

3: 在入口文件main.js中引入检查版本更新的逻辑,有更新则提示更新。

路由守卫router.beforeResolve(vite+vue),检查更新,对比manifest.json文件的响应头Etag判断是否有更新

通过Worker轮询,检查更新,对比manifest.json文件的响应头Etag判断是否有更新。Worker线程并不影响其他线程的逻辑。

流程如下:

实现

注意:以下为vite+vue为例

新建manifest.json

public目录下新建manifest.json

{
  "timestamp":21312321311,
  "msg":"更新内容如下:\n--1.添加系统更新提示机制"
}

vite.config.js配置

配置vite打包输出更新mainfest.json

import { defineConfig, type AliasOptions } from 'vite'
import { fileURLToPath, URL } from 'node:url';

import vue from '@vitejs/plugin-vue'

import path from 'path'
import { readFile, writeFile } from 'fs'

// 获取路径
const filePath = path.resolve(`./public`, 'manifest.json')
// 读取文件内容
readFile(filePath, 'utf8', (err, data) => {

  if (err) {
    console.error('读取文件时出错:', err)
    return
  }
  // 将文件内容转换JSON
  const dataObj = JSON.parse(data)
  //修改时间戳
  dataObj.timestamp = new Date().getTime()
  // 将修改后的内容写回文件
  writeFile(filePath, JSON.stringify(dataObj), 'utf8', err => {
    if (err) {
      console.error('写入文件时出错:', err)
      return
    }
  })
})


const alias: AliasOptions = {
  '@': fileURLToPath(new URL('./src', import.meta.url)),
};

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      ...alias,
    },
  },
})

新建src/checkUpdate.js

检查更新文件

  1. 客户端发起请求,请求中包含上次获取的资源的ETag。
  2. 服务器收到请求后,比较客户端提供的ETag与当前资源的ETag是否一致。 5.如果一致,则返回HTTP 304 Not Modified响应,表示资源未发生变化,客户端可以使用缓存的版本。
  3. 如果不一致,服务器返回最新的资源内容,同时更新ETag。
  4. 客户端收到响应后,更新本地缓存的资源内容和ETag。
import router from "./router/index.ts";
//上次的Etag
let lastEtag = "";
//是否更新
let hasUpdate = false;
//创建worker线程
// const worker = new Worker();
const worker =  new Worker(new URL("./checkUpdate.worker.js", import.meta.url))


console.log(worker, "worker");



//检查版本更新
async function checkUpdate() {
  try {
    // 检测前端资源是否有更新
    let response = await fetch(`/manifest.json?v=${Date.now()}`, {
      method: "head",
    });
    // 获取最新的etag
    let etag = response.headers.get("etag");
    hasUpdate = lastEtag && etag !== lastEtag;
    lastEtag = etag;
    console.log((lastEtag = etag), "lastEtag = etag");
  } catch (e) {
    return Promise.reject(e);
  }
}

async function confirmReload(msg = "", lastEtag) {
  worker &&
    worker.postMessage({
      type: "pause",
    });
  try {
    console.log("版本更新了");
  } catch (e) {}
}
// 路由拦截
router.beforeEach(async (to, from, next) => {
  next();
  try {
    await checkUpdate();
    if (hasUpdate) {
      worker.postMessage({
        type: "destroy",
      });
      location.reload();
    }
  } catch (e) {}
});

worker.postMessage({
  type: "check",
});

worker.onmessage = ({ data }) => {
  console.log(data, "data");
  if (data.type === "hasUpdate") {
    hasUpdate = true;
    confirmReload(data.msg, data.lastEtag);
  }
};

新建src/checkUpdate.worker.js

let lastEtag
let hasUpdate = false
let intervalId = ''
async function checkUpdate() {
 
  try {
    // 检测前端资源是否有更新
    let response = await fetch(`/manifest.json?v=${Date.now()}`, {
      method: 'get'
    })
    // 获取最新的etag和data
    let etag = response.headers.get('etag')
    let data = await response.json()
    hasUpdate = lastEtag !== undefined && etag !== lastEtag
  
    if (hasUpdate) {
      postMessage({
        type: 'hasUpdate', 
        msg: data.msg,
        lastEtag: lastEtag,
        etag: etag
      })
    }
    lastEtag = etag
  } catch (e) {
    return Promise.reject(e)
  }
}

// 监听主线程发送过来的数据
addEventListener('message', ({ data }) => {
  console.log(data,'消息')
  if (data.type === 'check') {  
     console.log('checkcheckcheck')
    // 每5分钟执行一次
    // 立即执行一次,获取最新的etag,避免在setInterval等待中系统更新,第一次获取的etag是新的,但是lastEtag还是undefined,不满足条件,错失刷新时机
    // checkUpdate()
    intervalId = setInterval(()=>{
      checkUpdate()
      console.log('检查版本更新')
    },  3 * 1000)
  }
  if (data.type === 'recheck') {
    // 每5分钟执行一次
    hasUpdate = false
    lastEtag = data.lastEtag
    intervalId = setInterval(()=>{
      checkUpdate()
      console.log('检查版本更新')
    },  3 * 1000)
    console.log('recheckrecheckrecheck')
  }
  if (data.type === 'pause') {
    clearInterval(intervalId)
  }
  if (data.type === 'destroy') {
    clearInterval(intervalId)
    close()
  }
})


入口文件

示例:main.ts

import "./checkUpdate.js"

注意

在工程化项目中,由于要打包后部署,因此work线程文件需要处理资源路径

//创建worker线程
// const worker = new Worker();
const worker =  new Worker(new URL("./checkUpdate.worker.js", import.meta.url))

文件操作:showDirectoryPicker

概述

在传统的前端开发中,处理用户文件一直是个棘手的问题。虽然 <input type="file"> 提供了基本的文件选择功能,但当需要处理整个目录结构时,开发者往往需要借助复杂的后端支持或Electron等桌面框架。showDirectoryPicker() 的出现彻底改变了这一局面,它为Web应用提供了原生的目录访问能力,文件操作体验。

showDirectoryPicker

showDirectoryPicker() 是 File System Access API 的核心方法之一,它允许Web应用通过用户授权的方式访问整个目录结构,而不仅仅是单个文件。这意味着开发者现在可以在浏览器中实现以前只能在桌面应用中才能完成的文件操作。

核心特性

  • 完整的目录访问:读取、遍历、修改目录内容
  • 权限持久化:用户授权后可保存访问权限
  • 安全沙箱:在严格的用户控制下运行
  • 现代化API:基于Promise的异步设计

浏览器支持情况

只有部分浏览器支持,并且版本比较新,希望以后能够更好支持 image.png

基础使用与语法

 基本调用方式

async function selectDirectory() {
  try {
    const directoryHandle = await window.showDirectoryPicker();
    console.log('选择的目录:', directoryHandle.name);
    return directoryHandle;
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('用户取消了选择');
    } else {
      console.error('发生错误:', err);
    }
  }
}

配置选项

const options = {
  id: 'projectFolder', // 标识符,用于记住用户选择
  mode: 'readwrite',   // 权限模式:read 或 readwrite
  startIn: 'documents' // 起始目录:desktop, documents, downloads等
};

const directoryHandle = await showDirectoryPicker(options);

权限模式

  • read :仅读取权限,可以列出文件和读取内容
  • readwrite :读写权限,可以创建、修改、删除文件

实际应用场景

遍历文件内容

async function* walkDirectory(directoryHandle, path = '') {
  for await (const entry of directoryHandle.values()) {
    const entryPath = `${path}/${entry.name}`;
    
    if (entry.kind === 'directory') {
      yield* await walkDirectory(entry, entryPath);
    } else {
      yield {
        handle: entry,
        path: entryPath,
        kind: 'file'
      };
    }
  }
}

// 使用示例
async function printDirectoryTree() {
  const directoryHandle = await showDirectoryPicker();
  
  for await (const entry of walkDirectory(directoryHandle)) {
    console.log(entry.path);
  }
}

printDirectoryTree()

解析本地文件夹可视化到线上

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

    <button>打开文件</button>


    <script>

        const btn = document.querySelector('button');

        btn.addEventListener('click', async () => {

            async function readDirectoryRecursive(directoryHandle, path = '') {
                const results = {
                    path: path || directoryHandle.name,
                    files: [],
                    directories: [],
                    complete: false,
                    totalFiles: 0
                };

                try {
                    for await (const entry of directoryHandle.values()) {
                        const entryPath = `${path}/${entry.name}`;

                        if (entry.kind === 'file') {
                            results.files.push({
                                name: entry.name,
                                path: entryPath,
                                handle: entry
                            });
                            results.totalFiles++;

                        } else if (entry.kind === 'directory') {
                            // 递归读取子目录
                            const subDirResult = await readDirectoryRecursive(entry, entryPath);
                            results.directories.push(subDirResult);
                            results.totalFiles += subDirResult.totalFiles;
                        }
                    }

                    results.complete = true;
                    return results;

                } catch (error) {
                    results.error = error;
                    results.complete = false;
                    throw error;
                }
            }

            // 使用示例
            async function main() {
                const directoryHandle = await showDirectoryPicker();
                const result = await readDirectoryRecursive(directoryHandle);

                if (result.complete) {
                    console.log(`递归读取完成,共找到 ${result.totalFiles} 个文件`);
                    console.log('目录结构:', result);

                    processFile(result);
                    async function processFile(result) {
                        if (result.files && result.files.length) {
                            for (let i = 0; i < result.files.length; i++) {
                                const fileHandle = result.files[i].handle;
                                const file = await fileHandle.getFile();
                                console.log("file----", file);
                                const fileReader = new FileReader();

                                fileReader.onload = (e) => {
                                    console.log("fileReader.result---", e.target.result);
                                }
                                fileReader.readAsText(file);
                            }
                        }

                        if (result.directories && result.directories.length) {
                            for (let i = 0; i < result.directories.length; i++) {
                                const dirRes = result.directories[i];
                                processFile(dirRes);
                            }
                        }

                    }
                }
            }
            main()




        });
    </script>
</body>

</html>

❌