2023.10.21 - 2023.11.09 更新前端面试问题总结(19道题)
获取更多面试相关问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…
目录:
-
中级开发者相关问题【共计 11 道题】
- 608.[Webpack] 有哪些基础概念【热度: 595】【工程化】【出题公司: 阿里巴巴】
- 609.[Webpack] 如何配置多入口应用, 且区分公共依赖的?【热度: 124】【工程化】【出题公司: 阿里巴巴】
- 611.[Webpack] 通过 babel-loader 来编译 tsx 文件, 应该如何配置呢?【热度: 221】【工程化】【出题公司: 腾讯】
- 613.箭头函数的作用以及使用场景【热度: 760】【JavaScript】【出题公司: 小米】
- 615.介绍一下迭代器 Iterator, 以及有哪些用法【热度: 645】【JavaScript】【出题公司: 小米】
- 616.[Vue] ref、toRef 和 toRefs 有啥区别?【热度: 128】【web框架】【出题公司: 美团】
- 617.[Vue] computed 和 watch 有啥区别?【热度: 876】【web框架】【出题公司: 美团】
- 618.[Vue] 路由守卫【热度: 680】【web框架】【出题公司: 美团】
- 619.[React] 如何实现路由守卫【热度: 681】【web框架】【出题公司: 美团】
- 620.浏览器的存储有哪些【热度: 814】【浏览器】【出题公司: PDD】
- 621.IndexedDB 存储空间大小是如何约束的?【热度: 116】【浏览器】【出题公司: PDD】
-
高级开发者相关问题【共计 8 道题】
- 603.为何现在市面上做表格渲染可视化技术的,大多数都是 canvas , 而很少用 svg 的?【热度: 302】【web应用场景】【出题公司: 阿里巴巴】
- 604.[微前端] 设计原则有哪些?【热度: 1,060】【web框架】【出题公司: 阿里巴巴】
- 605.[微前端] 路由加载流程是如何的?【热度: 971】【web框架】【出题公司: 阿里巴巴】
- 606.[Webpack] chunk 是什么概念,介绍一下?【热度: 1,100】【工程化】【出题公司: 阿里巴巴】
- 607.[Webpack] 为什么选择 webpack?【热度: 515】【工程化】【出题公司: 阿里巴巴】
- 610.[Webpack] 如何打包运行时 chunk , 且在项目工程中, 如何去加载这个运行时 chunk ?【热度: 421】【工程化】【出题公司: 阿里巴巴】
- 612.[Webpack] 全面了解 tree shaking【热度: 790】【工程化】【出题公司: 阿里巴巴】
- 622.[Webpack] 有哪些优化项目的手段?【热度: 1,163】【工程化】【出题公司: 阿里巴巴】
中级开发者相关问题【共计 11 道题】
608.[Webpack] 有哪些基础概念【热度: 595】【工程化】【出题公司: 阿里巴巴】
关键词:webpack 作用、webpack 概念
Webpack是一个现代的JavaScript模块打包工具,它的核心概念包括以下几个方面:
- 入口(Entry):指定Webpack开始构建依赖图谱的起点。可以通过配置文件中的entry属性来指定入口文件,也可以指定多个入口文件。
- 输出(Output):指定Webpack打包后的文件输出的路径和文件名。可以通过配置文件中的output属性来定义输出路径和文件名的规则。
- 加载器(Loader):Webpack本身只能处理JavaScript文件,通过加载器,Webpack可以处理其他类型的文件,如CSS、图片、字体等。加载器会在打包过程中对文件进行转换和处理。
- 插件(Plugin):插件是Webpack的核心功能扩展机制,可以用于解决很多构建过程中的复杂问题或实现特定的需求。插件可以用于优化打包结果、自动生成HTML文件、提取CSS文件等。
- 模式(Mode):Webpack提供了两种模式,分别是开发模式(development)和生产模式(production)。开发模式会启用一些有助于开发调试的功能,而生产模式则会启用代码压缩、优化等功能。
- 代码分割(Code Splitting):Webpack支持将代码分割成多个块,实现按需加载和提高应用性能。可以使用动态导入、SplitChunks插件等方式进行代码分割。
- 解析(Resolve):Webpack会解析模块之间的依赖关系,通过解析规则来确定模块的依赖关系。可以通过配置resolve属性来设置模块的解析规则。
参考文档
webpack.docschina.org/concepts/
609.[Webpack] 如何配置多入口应用, 且区分公共依赖的?【热度: 124】【工程化】【出题公司: 阿里巴巴】
在Webpack中配置多入口应用并区分公共依赖,可以通过以下步骤进行配置:
- 在Webpack配置文件中,使用entry属性指定多个入口文件,并为每个入口文件命名一个唯一的键名。例如:
module.exports = {
entry: {
app1: './src/app1.js',
app2: './src/app2.js'
},
// 其他配置项...
};
上面的配置指定了两个入口文件app1.js和app2.js,并为它们分别指定了键名app1和app2。
- 使用SplitChunks插件进行公共依赖的提取。在Webpack配置文件中添加以下配置:
module.exports = {
// 其他配置项...
optimization: {
splitChunks: {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2
}
}
}
}
};
上面的配置中,我们使用optimization.splitChunks.cacheGroups选项配置了一个名为commons的缓存组。该缓存组将对公共依赖进行提取,name属性指定了提取后文件的名称,chunks属性指定了提取的范围为所有类型的块(入口文件和异步加载的块),minChunks属性指定了至少被引用两次的模块才会被提取为公共依赖。
- 添加output配置,指定打包后文件的输出路径和文件名。例如:
module.exports = {
// 其他配置项...
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
上面的配置中,使用[name]占位符来动态生成根据入口文件的键名生成对应的文件名。
通过以上配置,Webpack将会根据指定的多个入口文件进行打包,并在打包过程中自动提取公共依赖为一个独立的文件。例如,假设app1.js和app2.js都引用了lodash库,那么在打包后的结果中,lodash库将会被提取为commons.bundle.js文件,而app1.js和app2.js则分别生成对应的app1.bundle.js和app2.bundle.js。
追问
上面的配置, 最终会输出几个文件?
根据上述的打包配置,最终将会输出3个文件。假设配置的多入口应用有两个入口文件app1.js和app2.js,并且两个入口文件都引用了lodash库作为公共依赖。
根据上述的配置,Webpack将会进行以下操作:
- 根据entry配置,将app1.js和app2.js作为入口文件进行打包。
- 遇到公共依赖lodash库时,使用SplitChunks插件将其提取为独立的文件commons.bundle.js。
- 根据output配置,将app1.js打包后生成app1.bundle.js,将app2.js打包后生成app2.bundle.js,将commons.bundle.js生成commons.bundle.js。
- 最终,在输出路径下将会生成3个文件:app1.bundle.js、app2.bundle.js和commons.bundle.js。
611.[Webpack] 通过 babel-loader 来编译 tsx 文件, 应该如何配置呢?【热度: 221】【工程化】【出题公司: 腾讯】
如果你想使用 babel-loader 来编译 TypeScript 文件(.tsx),你需要在 babel.config.js 和 webpack.config.js 两个文件中进行相应的配置。
首先,在 babel.config.js 文件中,你需要添加 @babel/preset-typescript 预设,以便兼容 TypeScript:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript']
};
接下来,在 webpack.config.js 文件中,你需要对 .tsx 文件使用 babel-loader:
module.exports = {
// ...其他配置项
module: {
rules: [
// ...其他规则
{
test: /.(ts|tsx)$/,
exclude: /node_modules/,
use: 'babel-loader',
},
],
},
};
这样,当 webpack 执行构建时,babel-loader 将会使用 babel.config.js 中配置的预设来编译 .tsx 文件。同时,除了 TypeScript 文件,你还可以使用该配置来编译 JavaScript 文件(.js)和 React JSX 文件(.jsx)。
追问
'@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react' 这三个插件的作用是什么?
@babel/preset-env:它是 Babel 的一个预设,用于根据目标环境(浏览器、Node.js 等)自动确定需要转译的 JavaScript 特性,并将其转换为目标环境所支持的代码。它将根据你配置的目标环境和浏览器的使用情况,智能地选择需要转译的特性,以减小转译后的代码体积。它也包含了一些插件,例如转换箭头函数、解构赋值、模板字符串等。@babel/preset-typescript:它是 Babel 的一个预设,用于将 TypeScript 代码转译为 JavaScript 代码,以便在不支持 TypeScript 的环境中运行。它包含了一些插件,例如转换 TypeScript 的类型注解、类成员修饰符、泛型类型等。@babel/preset-react:它是 Babel 的一个预设,用于将 React 的 JSX 语法转译为普通的 JavaScript 代码,以便在不支持 JSX 的环境中运行。它也包含了一些插件,例如转换 JSX 语法、处理 React 的内置组件等。
613.箭头函数的作用以及使用场景【热度: 760】【JavaScript】【出题公司: 小米】
特点
- 简洁的语法形式:箭头函数使用了更简洁的语法形式,省略了传统函数声明中的
function关键字和大括号。它通常可以在更少的代码行数中表达相同的逻辑。 - 没有自己的this:箭头函数没有自己的
this绑定,它会捕获所在上下文的this值。这意味着箭头函数中的this与其定义时所在的上下文中的this保持一致,而不是在函数被调用时动态绑定。这可以避免传统函数中常见的this指向问题,简化了对this的使用和理解。 - 没有
arguments对象:箭头函数也没有自己的arguments对象。如果需要访问函数的参数,可以使用剩余参数(Rest Parameters)或使用展开运算符(Spread Operator)将参数传递给其他函数。 - 无法作为构造函数:箭头函数不能用作构造函数,不能使用
new关键字调用。它们没有prototype属性,因此无法使用new关键字创建实例。 - 隐式的返回值:如果箭头函数的函数体只有一条表达式,并且不需要额外的处理逻辑,那么可以省略大括号并且该表达式将隐式作为返回值返回。
- 不能绑定自己的this、super、new.target:由于箭头函数没有自己的
this绑定,也无法使用super关键字引用父类的方法,也无法使用new.target获取构造函数的引用。
作用
- 简化普通函数:箭头函数提供了更简洁的语法形式,可以在需要定义函数的地方使用更短的代码来表达同样的逻辑。这可以提高代码的可读性和维护性。
- 保留上下文:箭头函数没有自己的
this绑定,它会捕获所在上下文的this值。这意味着在箭头函数中,this的值是在函数定义时确定的,而不是在函数被调用时动态绑定。这种特性可以避免传统函数中的this绑定问题,并使代码更易于理解和维护。
使用场景
- 简化函数表达式:当需要定义一个简单的函数表达式时,可以使用箭头函数代替传统的函数表达式,减少代码量。
// 传统函数表达式
const sum = function(a, b) {
return a + b;
};
// 箭头函数
const sum = (a, b) => a + b;
- 箭头函数作为回调函数:当需要传递回调函数时,箭头函数可以提供更简洁的语法形式,同时保留外层上下文中的
this。
// 传统回调函数
someFunction(function() {
console.log(this); // 外层上下文的this
});
// 箭头函数作为回调函数
someFunction(() => {
console.log(this); // 外层上下文的this
});
- 简化函数中的
this绑定问题:由于箭头函数没有自己的this绑定,可以避免使用传统函数中常见的bind、call或apply等方法来绑定this。
// 传统函数中的this绑定
const obj = {
value: 42,
getValue: function() {
setTimeout(function() {
console.log(this.value); // undefined,因为此时this指向全局对象
}, 1000);
}
};
// 使用箭头函数避免this绑定问题
const obj = {
value: 42,
getValue: function() {
setTimeout(() => {
console.log(this.value); // 42,箭头函数捕获了外层上下文的this
}, 1000);
}
};
// ```
615.介绍一下迭代器 Iterator, 以及有哪些用法【热度: 645】【JavaScript】【出题公司: 小米】
关键词:迭代器 Iterator
1、Iterator 的概念
JavaScript 原有的表示 “ 集合 ” 的数据结构,主要是数组( Array )和对象( Object ), ES6 又添加了 Map 和 Set 。 这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是 Map , Map 的成员是对象。 这样就需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器( Iterator )就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环, Iterator 接口主要供for...of消费。
Iterator 的遍历过程是这样的。
- ( 1 )创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- ( 2 )第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- ( 3 )第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- ( 4 )不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。 具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
2、数据结构的默认 Iterator 接口
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
在 ES6 中,有三类数据结构原生具备 Iterator 接口:数组、某些类似数组的对象、 Set 和 Map 结构。
实例:
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
上面提到,原生就部署 Iterator 接口的数据结构有三类,对于这三类数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。
3、调用 Iterator 接口的场合
有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法),除了下文会介绍的for...of循环,还有几个别的场合。
3.1、解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
实例1:
let set = new Set().add('a').add('b').add('c');
let [x, y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
3.2、扩展运算符
扩展运算符( ... )也会调用默认的 iterator 接口。
实例2:
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
3.3、yield*
yield* 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
实例3:
let generator = function* () {
yield 1;
yield* [2, 3, 4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
3.4、其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet() (比如new Map([['a',1],['b',2]]))
- Promise.all()
- Promise.race()
4、Iterator 接口与 Generator 函数
Symbol.iterator方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
实例:
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
// 或者采用下面的简洁写法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// hello
// world
616.[Vue] ref、toRef 和 toRefs 有啥区别?【热度: 128】【web框架】【出题公司: 美团】
关键词:ref、toRef、toRefs 区别
在 Vue 3 中,ref、toRef 和 toRefs 是 Vue Composition API 提供的函数,用于处理响应式数据。
ref(value: T): Ref<T>:创建一个响应式数据引用。接收一个初始值作为参数,并返回一个包含该值的响应式引用。Ref是一个包装对象,它的.value属性用于访问和修改引用的值。
使用 ref 创建响应式数据引用:
import { ref } from 'vue';
const count = ref(0); // 创建一个初始值为 0 的响应式引用
console.log(count.value); // 输出: 0
count.value++; // 修改引用的值
console.log(count.value); // 输出: 1
toRef(object: object, key: string | symbol): ToRef:创建一个指向另一个响应式对象的响应式引用。接收一个响应式对象和其属性名作为参数,并返回一个指向该属性的响应式引用。ToRef是一个只读的响应式引用。
使用 toRef 创建指向另一个响应式对象的引用:
import { ref, reactive, toRef } from 'vue';
const state = reactive({
name: 'John',
age: 30
});
const nameRef = toRef(state, 'name'); // 创建指向 state.name 的引用
console.log(nameRef.value); // 输出: "John"
state.name = 'Mike'; // 修改原始对象的属性值
console.log(nameRef.value); // 输出: "Mike"
nameRef.value = 'Amy'; // 修改引用的值
console.log(state.name); // 输出: "Amy"
toRefs(object: T): ToRefs<T>:将一个响应式对象的所有属性转换为响应式引用。接收一个响应式对象作为参数,并返回一个包含所有属性的响应式引用对象。ToRefs是一个对象,每个属性都是一个只读的响应式引用。
使用 toRefs 将对象的所有属性转换为响应式引用:
import { reactive, toRefs } from 'vue';
const state = reactive({
name: 'John',
age: 30
});
const refs = toRefs(state); // 将 state 中的所有属性转换为响应式引用
console.log(refs.name.value); // 输出: "John"
console.log(refs.age.value); // 输出: 30
state.name = 'Mike'; // 修改原始对象的属性值
console.log(refs.name.value); // 输出: "Mike"
refs.age.value = 25; // 修改引用的值
console.log(state.age); // 输出: 25
这些函数是 Vue 3 Composition API 中用于创建和处理响应式数据的重要工具。通过它们,我们可以更灵活地管理和使用响应式数据。
617.[Vue] computed 和 watch 有啥区别?【热度: 876】【web框架】【出题公司: 美团】
关键词:computed 和 watch 区别
在 Vue 中,computed 和 watch 是两种用于监听和响应数据变化的方式。
computed 是计算属性,它是基于响应式数据进行计算得到的一个新的派生属性。计算属性可以接收其他响应式数据作为依赖,并且只有当依赖数据发生变化时,计算属性才会重新计算。计算属性的值会被缓存,只有在依赖数据变化时才会重新计算,这样可以提高性能。计算属性的定义方式是使用 computed 函数或者在 Vue 组件中使用 get 和 set 方法。
下面是一个使用计算属性的示例:
import { reactive, computed } from 'vue';
const state = reactive({
firstName: 'John',
lastName: 'Doe'
});
const fullName = computed(() => {
return `${state.firstName} ${state.lastName}`;
});
console.log(fullName.value); // 输出: "John Doe"
state.firstName = 'Mike'; // 修改firstName
console.log(fullName.value); // 输出: "Mike Doe"
watch 是用于监听特定响应式数据的变化,并在数据变化时执行相应的操作。watch 可以监听单个数据的变化,也可以监听多个数据的变化。当被监听的数据发生变化时,watch 的回调函数会被执行。watch 还支持深度监听对象的变化以及异步操作。
下面是一个使用 watch 的示例:
import { reactive, watch } from 'vue';
const state = reactive({
count: 0
});
watch(() => state.count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`);
});
state.count++; // 输出: "count 从 0 变为 1"
以上是 computed 和 watch 的基本用法。通过使用这两种方式,我们可以根据需要监听和响应数据的变化,实现更加灵活的逻辑和交互。
618.[Vue] 路由守卫【热度: 680】【web框架】【出题公司: 美团】
关键词:路由守卫
路由守卫是 Vue Router 提供的一种机制,用于在路由导航过程中对路由进行拦截和控制。通过使用路由守卫,我们可以在路由导航前、导航后、导航中断等不同的阶段执行相应的逻辑。
Vue Router 提供了三种类型的路由守卫:
- 全局前置守卫(Global Before Guards):在路由切换之前被调用,可以用于进行全局的权限校验或者路由跳转拦截等操作。
- 路由独享守卫(Per-Route Guards):在特定的路由配置中定义的守卫。这些守卫只会在当前路由匹配成功时被调用。
- 组件内的守卫(In-Component Guards):在组件实例内部定义的守卫。这些守卫可以在组件内部对路由的变化进行相应的处理。
- 全局前置守卫
router.beforeEach((to, from, next) => {
// to: 即将进入的目标
// from:当前导航正要离开的路由
return false // 返回false用于取消导航
return { name: 'Login' } // 返回到对应name的页面
next({ name: 'Login' }) // 进入到对应的页面
next() // 放行
})
- 全局解析守卫:类似beforeEach
router.beforeResolve(to => {
if (to.meta.canCopy) {
return false // 也可取消导航
}
})
- 全局后置钩子
router.afterEach((to, from) => {
logInfo(to.fullPath)
})
- 导航错误钩子,导航发生错误调用
router.onError(error => {
logError(error)
})
- 路由独享守卫,beforeEnter可以传入单个函数,也可传入多个函数。
function dealParams(to) {
// ...
}
function dealPermission(to) {
// ...
}
const routes = [
{
path: '/home',
component: Home,
beforeEnter: (to, from) => {
return false // 取消导航
},
// beforeEnter: [dealParams, dealPermission]
}
]
组件内的守卫
const Home = {
template: `...`,
beforeRouteEnter(to, from) {
// 此时组件实例还未被创建,不能获取this
},
beforeRouteUpdate(to, from) {
// 当前路由改变,但是组件被复用的时候调用,此时组件已挂载好
},
beforeRouteLeave(to, from) {
// 导航离开渲染组件的对应路由时调用
}
}
619.[React] 如何实现路由守卫【热度: 681】【web框架】【出题公司: 美团】
关键词:路由守卫
在 React 中,虽然没有内置的路由守卫(Route Guards)功能,但可以使用第三方库来实现类似的功能。最常用的第三方路由库是 React Router。
React Router 提供了一些组件和钩子函数,可以用于在路由导航过程中进行拦截和控制。
<Route>组件:可以在路由配置中定义特定路由的守卫逻辑。例如,可以设置render属性或者component属性来渲染组件,并在渲染前进行守卫逻辑的判断。useHistory钩子:可以获取当前路由的历史记录,并通过history对象进行路由导航的控制。可以使用history对象的push、replace方法来切换路由,并在切换前进行守卫逻辑的判断。useLocation钩子:可以获取当前的路由位置信息,包括路径、查询参数等。可以根据这些信息进行守卫逻辑的判断。
下面是一个使用 React Router 实现路由守卫的示例:
import { BrowserRouter as Router, Route, useHistory } from "react-router-dom";
function App() {
const history = useHistory();
const isAuthenticated = () => {
// 判断用户是否已登录
return true;
};
const requireAuth = (Component) => {
return () => {
if (isAuthenticated()) {
return <Component/>;
} else {
history.push("/login");
return null;
}
};
};
return (
<Router>
<Route path="/home" render={requireAuth(Home)}/>
<Route path="/login" component={Login}/>
<Route path="/dashboard" render={requireAuth(Dashboard)}/>
</Router>
);
}
在上述示例中,requireAuth 是一个自定义的函数,用于判断是否需要进行权限校验。在 render 属性中,我们调用 requireAuth 函数包裹组件,根据用户是否已登录来判断是否渲染该组件。如果用户未登录,则使用 history.push 方法进行路由跳转到登录页面。
通过使用 React Router 提供的组件和钩子函数,我们可以实现类似于路由守卫的功能,进行路由的拦截和控制。
参考文档
620.浏览器的存储有哪些【热度: 814】【浏览器】【出题公司: PDD】
关键词:浏览器存储
在浏览器中,有以下几种常见的存储方式:
-
Cookie:Cookie 是一种存储在用户浏览器中的小型文本文件。它可以用于存储少量的数据,并在浏览器与服务器之间进行传输。Cookie 可以设置过期时间,可以用于维持用户会话、记录用户偏好等功能。
-
Web Storage:Web Storage 是 HTML5 提供的一种在浏览器中进行本地存储的机制。它包括两种存储方式:sessionStorage 和 localStorage。
- sessionStorage:sessionStorage 用于在一个会话期间(即在同一个浏览器窗口或标签页中)存储数据。当会话结束时,存储的数据会被清除。
- localStorage:localStorage 用于持久化地存储数据,即使关闭浏览器窗口或标签页,数据仍然存在。localStorage 中的数据需要手动删除或通过 JavaScript 代码清除。
-
IndexedDB:IndexedDB 是一种用于在浏览器中存储大量结构化数据的数据库。它提供了一个异步的 API,可以进行增删改查等数据库操作。IndexedDB 可以存储大量的数据,并支持事务操作。
-
Cache Storage:Cache Storage 是浏览器缓存的一部分,用于存储浏览器的缓存资源。它可以用来缓存网页、脚本、样式表、图像等静态资源,以提高网页加载速度和离线访问能力。
-
Web SQL Database:Web SQL Database 是一种已被废弃但仍被一些浏览器支持的关系型数据库。它使用 SQL 语言来进行数据操作,可以存储大量的结构化数据。
追问:service worker 存储的内容是放在 哪儿的?
Service Worker 可以利用 Cache API 和 IndexedDB API 进行存储。具体来说:
- Cache API:Service Worker 可以使用 Cache API 将请求的响应存储在浏览器的 Cache Storage 中。Cache Storage 是浏览器的一部分,用于存储缓存的资源。通过 Cache API,Service Worker 可以将网页、脚本、样式表、图像等静态资源缓存起来,以提高网页加载速度和离线访问能力。
- IndexedDB API:Service Worker 还可以利用 IndexedDB API 在浏览器中创建和管理数据库。IndexedDB 是一种用于存储大量结构化数据的数据库,Service Worker 可以通过 IndexedDB API 进行数据的增删改查操作。通过 IndexedDB,Service Worker 可以将大量的数据进行持久化存储,以便在离线状态下仍然能够访问和操作数据。
Service Worker 存储的内容并不是放在普通的浏览器缓存或本地数据库中,而是放在 Service Worker 的全局作用域中。Service Worker 运行在独立的线程中,与浏览器主线程分离,因此能够独立地处理网络请求和数据存储,提供了一种强大的离线访问和缓存能力。
621.IndexedDB 存储空间大小是如何约束的?【热度: 116】【浏览器】【出题公司: PDD】
关键词:IndexedDB 存储空间大小设置
IndexedDB 有大小限制。具体来说,IndexedDB 的大小限制通常由浏览器实现决定,因此不同浏览器可能会有不同的限制。
一般来说,IndexedDB 的大小限制可以分为两个方面:
- 单个数据库的大小限制:每个 IndexedDB 数据库的大小通常会有限制,这个限制可以是固定的(如某些浏览器限制为特定的大小,如 50MB),也可以是动态的(如某些浏览器根据设备剩余存储空间来动态调整大小)。
- 整个浏览器的大小限制:除了每个数据库的大小限制外,浏览器还可能设置整个 IndexedDB 存储的总大小限制。这个限制可以根据浏览器的策略和设备的可用存储空间来决定。
需要注意的是,由于 IndexedDB 是在用户设备上进行存储的,并且浏览器对存储空间的管理可能会受到用户权限和设备限制的影响,因此在使用 IndexedDB 存储大量数据时,需要注意数据的大小和存储限制,以免超过浏览器的限制导致出错或无法正常存储数据。
追问:开发者是否可以通过JS代码可以调整 IndexedDB 存储空间大小?
实际上,在创建数据库时,无法直接通过 API 设置存储空间大小。
IndexedDB 的存储空间大小通常由浏览器的策略决定,并且在大多数情况下,开发者无法直接控制。浏览器会根据自身的限制和规则,动态分配和管理 IndexedDB 的存储空间。因此,将存储空间大小设置为期望的值不是开发者可以直接控制的。
开发者可以通过以下方式来控制 IndexedDB 的存储空间使用情况:
- 优化数据模型:设计合适的数据结构和索引,避免存储冗余数据和不必要的索引。
- 删除不再需要的数据:定期清理不再需要的数据,以减少数据库的大小。
- 压缩数据:对存储的数据进行压缩,可以减少存储空间的使用。
这些方法只能间接地影响 IndexedDB 的存储空间使用情况,具体的存储空间大小仍然由浏览器决定。
高级开发者相关问题【共计 8 道题】
603.为何现在市面上做表格渲染可视化技术的,大多数都是 canvas , 而很少用 svg 的?【热度: 302】【web应用场景】【出题公司: 阿里巴巴】
关键词:canvas使用场景、canvas可视化、svg使用场景
都用上了可视化技术做渲染, 在这个场景下, 大多数考虑的是性能;
所以主要基于几个方面去衡量技术方案的选择: 性能、动态交互、复杂图形支持
- 性能:Canvas 通常比 SVG 具有更好的性能。Canvas 是基于像素的绘图技术,而 SVG 是基于矢量的绘图技术。由于 Canvas 的绘图是直接操作像素,所以在大规模绘制大量图形时,Canvas 的性能优势更为明显。而 SVG 生成的图形是由 DOM 元素组成,每个元素都要进行布局和绘制,因此在处理大量图形时会有性能瓶颈。
- 动态交互:Canvas 更适合处理动态交互。由于 Canvas 绘制的图形是像素级别的,可以直接对图形进行像素级别的操作,可以方便地进行复杂的动画和交互效果。而 SVG 的图形是由 DOM 元素组成的,每个元素都要进行布局和绘制,所以在处理复杂的动态交互时,性能方面可能会受到限制。
- 复杂图形支持:Canvas 更适合处理复杂的图形。由于 Canvas 是像素级别的绘制,可以直接操作像素,因此可以实现更加灵活和复杂的图形效果,比如阴影、渐变等。而 SVG 的图形是基于矢量的,相对来说对复杂图形的支持可能会有一些限制。
追问: canvas 和 svg 在动态交互上有什么具体的区别?
元素操作:在 Canvas 中,绘制的图形被视为位图,无法直接访问和操作单个元素,需要通过 JavaScript 对整个画布进行操作。而在 SVG 中,每个图形元素都是 DOM 元素,可以直接访问和操作单个元素,比如修改属性、绑定事件等。
真是场景: 比如在 table 开发场景下, svg 能通过元素进行事件绑定进行用户操作事件驱动, 比较方便, 但是同样的用户操作, 用 canvas 去驱动, 显得并不是那么的方便。 这个问题 canvas 是如何解决的?
在 Table 开发场景下,SVG 确实更适合进行事件绑定和用户操作事件驱动。使用 SVG,可以直接操作每个图形元素,为其绑定事件处理程序,实现用户交互。
而 Canvas 在处理用户操作事件驱动方面相对不太方便,因为 Canvas 绘制的是位图,并不直接支持事件绑定。但是可以通过以下方式解决这个问题:
- 通过将 Canvas 元素放置在 HTML 元素之上,再利用 CSS 控制其位置和尺寸,实现与用户交互的感觉。然后通过监听 HTML 元素的事件,通过 JavaScript 判断用户操作的位置与 Canvas 上的图形元素是否相交,从而模拟出用户交互的效果。
- 使用第三方库或框架,如
Fabric.js、Konva.js等,它们提供了更高级的 API 和事件系统,使得在 Canvas 上进行用户交互更加方便。这些库可以处理用户操作事件,检测点击、拖拽、缩放等交互操作,并提供了事件绑定和管理的方法。
通过以上方式,可以在 Canvas 中实现一些基本的用户交互,但相比于 SVG 来说,Canvas 的事件处理和用户交互仍然相对繁琐一些。所以在需要大量的用户交互和事件处理的情况下,SVG 仍然是更好的选择。
追问: canvas 如何更为方便的提供事件处理能力?因为 canvas 不能进行事件绑定等, 显得就非常的不方便
在 Canvas 中提供事件处理能力,可以通过以下两种方式更为方便:
-
使用第三方库或框架:有一些流行的 Canvas 框架可以帮助简化事件处理,例如 Fabric.js、Konva.js 和 EaselJS 等。这些库封装了 Canvas 的底层API,提供了更高级的事件系统和方法,可以轻松地为图形元素绑定事件处理程序,实现用户交互。
-
手动实现事件处理:通过监听 HTML 元素的事件(例如鼠标点击、移动、滚轮等),再结合 Canvas 的绘制和坐标计算,可以手动实现事件处理。以下是基本的步骤:
- 获取鼠标或触摸事件的坐标。
- 判断坐标是否在 Canvas 绘制区域内。
- 找到被点击的图形元素(如果有)。
- 根据事件类型执行相应的操作,如拖拽、缩放、点击等。
追问: Fabric.js 是如何进行 canvas 底层事件 api 的封装的?
Fabric.js 是一个强大的 Canvas 库,它在提供图形绘制和交互能力的同时,也封装了 Canvas 的底层事件 API,简化了事件处理的流程。下面是 Fabric.js 如何封装 Canvas 底层事件 API 的一些主要方式:
- 事件监听:
Fabric.js提供了on方法,用于在 Canvas 上注册事件处理程序。可以监听各种事件,如鼠标点击、移动、滚动、键盘事件等。通过这个方法,可以为整个Canvas或图形元素绑定事件。 - 事件对象:在事件处理程序中,
Fabric.js将底层事件对象进行封装,提供了一个更高级的事件对象(fabric.Event),其中包含了更多有用的信息,如事件类型、触发坐标、关联的图形对象等。 - 坐标转换:
Fabric.js提供了一系列的方法来处理坐标转换,使得事件处理更加方便。可以通过getPointer方法获取相对于 Canvas 的坐标,通过localToGlobal和globalToLocal方法在不同坐标系之间进行转换。 - 交互操作:
Fabric.js提供了一些方便的方法来处理用户交互,如拖拽、缩放、旋转等。通过dragging、scaling、rotating等属性和方法,可以轻松地实现这些交互操作,并在事件处理程序中进行相应的处理。
604.[微前端] 设计原则有哪些?【热度: 1,060】【web框架】【出题公司: 阿里巴巴】
关键词:微前端设计原则
《微前端设计与实现》一书中作者卢卡·梅扎利拉提出的关于微前端的实践原则。一共有七条原则, 这些原则可以帮助团队更好地设计和实施微前端架构。
- 围绕业务领域建模:将前端应用程序按照业务领域进行划分,每个微前端子应用负责一个特定的业务领域。这样可以提高团队的独立性和聚焦性,降低开发和维护的复杂性。
- 自动化文化:建立自动化的开发和部署流程,包括自动化测试、持续集成和持续部署。这样可以提高开发效率和质量,并且减少人为错误。
- 隐藏实现细节:将微前端子应用的实现细节隐藏起来,提供简单的接口供其他子应用或者外部系统调用。这样可以减少依赖和耦合,提高系统的灵活性和可扩展性。
- 分布式治理:微前端架构中的各个子应用可以由不同的团队负责开发和维护。需要建立一套分布式治理机制,包括版本控制、协作沟通和问题解决等,保证各个子应用能够有效地协同工作。
- 独立部署:每个微前端子应用都可以独立地进行开发、测试和部署,而不会影响其他子应用。这样可以提高团队的独立性和灵活性,并且减少不同团队之间的交互和依赖。
- 故障隔离:微前端架构中的一个子应用出现故障时,不会影响其他子应用的正常运行。需要建立故障隔离机制,确保故障的影响范围最小化。
- 高度可观察:需要建立合适的监控和日志系统,对微前端架构中的各个子应用进行监测和分析。这样可以提前发现问题并进行及时处理,保证系统的稳定性和可靠性。
605.[微前端] 路由加载流程是如何的?【热度: 971】【web框架】【出题公司: 阿里巴巴】
关键词:微前端路由加载
微前端是一种架构模式,旨在将大型前端应用程序拆分为更小、更容易维护的独立部分。微前端的路由原理可以通过以下步骤概括:
- 主应用加载:用户访问主应用时,主应用负责加载,并决定加载哪些微前端应用。
- 路由分发:主应用根据当前URL路径,将请求分发给相应的微前端应用。
- 微前端应用加载:被分发的微前端应用根据接收到的请求加载自己的代码和资源。
- 渲染内容:微前端应用接收到请求后,将自己的内容渲染到主应用的容器中。
- 子应用间通信:如果不同微前端应用之间需要进行通信,可以使用共享的状态管理工具或事件总线。
- 事件处理:主应用和微前端应用都可以处理路由变化事件,以便更新页面内容。
606.[Webpack] chunk 是什么概念,介绍一下?【热度: 1,100】【工程化】【出题公司: 阿里巴巴】
在Webpack中,Chunk(代码块)是指Webpack在构建过程中生成的一个或多个独立的文件,它包含了一组相关的模块。每个Chunk都有一个唯一的标识符,可以通过该标识符来访问和加载对应的Chunk。
Webpack根据指定的入口文件和依赖关系图来确定需要生成哪些Chunk。入口文件是Webpack构建的起点,而依赖关系图描述了每个模块之间的依赖关系。Webpack根据这些信息将模块分割为不同的代码块,并生成相应的Chunk。
Chunk的主要作用是实现代码的分割和按需加载。通过将代码拆分为多个Chunk,Webpack可以进行按需加载,只有在需要时才加载对应的Chunk,从而减少了初始加载的大小和时间。这样可以提高应用程序的性能和加载速度。
Webpack提供了多种代码分割的方式,包括使用入口配置、使用动态导入语法(如import() )和使用Webpack插件(如SplitChunksPlugin)。这些方式可以帮助开发者将代码分割为不同的Chunk,并根据实际需求进行配置和优化。
具体而言,Webpack的代码分割机制通过两种方式来创建chunk:
- 静态代码分割(Static Code Splitting):在Webpack配置中使用
splitChunks或optimization.splitChunks选项,可以将第三方库、公共模块或重复的模块分割成独立的chunk。这些chunk可以在多个入口文件之间共享,从而减少重复加载的代码。 - 动态代码分割(Dynamic Code Splitting):通过使用动态导入(Dynamic Import)语法,可以将应用程序的不同部分分割成独立的chunk。例如,在React中可以使用
React.lazy()函数和Suspense组件来实现动态导入和渲染。
分割成的chunk可以通过Webpack的内置功能(如代码分割插件、懒加载等)实现按需加载,即当需要时才加载对应的chunk,从而减少初始加载时间并提高网页性能。
通过使用chunk,Webpack可以自动将代码分割成更小的部分,实现按需加载和并行加载,从而提高应用程序的性能和用户体验。
607.[Webpack] 为什么选择 webpack?【热度: 515】【工程化】【出题公司: 阿里巴巴】
关键词:webpack 作用
为什么选择 webpack
想要理解为什么要使用 webpack,我们先回顾下历史,在打包工具出现之前,我们是如何在 web 中使用 JavaScript 的。
在浏览器中运行 JavaScript 有两种方法。第一种方式,引用一些脚本来存放每个功能;此解决方案很难扩展,因为加载太多脚本会导致网络瓶颈。第二种方式,使用一个包含所有项目代码的大型 .js 文件,但是这会导致作用域、文件大小、可读性和可维护性方面的问题。
立即调用函数表达式(IIFE) - Immediately invoked function expressions
IIFE 解决大型项目的作用域问题;当脚本文件被封装在 IIFE 内部时,你可以安全地拼接或安全地组合所有文件,而不必担心作用域冲突。
IIFE 使用方式产生出 Make, Gulp, Grunt, Broccoli 或 Brunch 等工具。这些工具称为任务执行器,它们将所有项目文件拼接在一起。
但是,修改一个文件意味着必须重新构建整个文件。拼接可以做到很容易地跨文件重用脚本,但是却使构建结果的优化变得更加困难。如何判断代码是否实际被使用?
即使你只用到 lodash 中的某个函数,也必须在构建结果中加入整个库,然后将它们压缩在一起。如何 treeshake 代码依赖?难以大规模地实现延迟加载代码块,这需要开发人员手动地进行大量工作。
感谢 Node.js,JavaScript 模块诞生了
Node.js 是一个 JavaScript 运行时,可以在浏览器环境之外的计算机和服务器中使用。webpack 运行在 Node.js 中。
当 Node.js 发布时,一个新的时代开始了,它带来了新的挑战。既然不是在浏览器中运行 JavaScript,现在已经没有了可以添加到浏览器中的 html 文件和 script 标签。那么 Node.js 应用程序要如何加载新的代码 chunk 呢?
CommonJS 问世并引入了 require 机制,它允许你在当前文件中加载和使用某个模块。导入需要的每个模块,这一开箱即用的功能,帮助我们解决了作用域问题。
npm + Node.js + modules - 大规模分发模块 JavaScript 已经成为一种语言、一个平台和一种快速开发和创建快速应用程序的方式,接管了整个 JavaScript 世界。
但 CommonJS 没有浏览器支持。没有 live binding(实时绑定)。循环引用存在问题。同步执行的模块解析加载器速度很慢。虽然 CommonJS 是 Node.js 项目的绝佳解决方案,但浏览器不支持模块,因而产生了 Browserify, RequireJS 和 SystemJS 等打包工具,允许我们编写能够在浏览器中运行的 CommonJS 模块。
ESM - ECMAScript 模块 来自 Web 项目的好消息是,模块正在成为 ECMAScript 标准的官方功能。然而,浏览器支持不完整,版本迭代速度也不够快,目前还是推荐上面那些早期模块实现。
依赖自动收集 传统的任务构建工具基于 Google 的 Closure 编译器都要求你手动在顶部声明所有的依赖。然而像 webpack 一类的打包工具自动构建并基于你所引用或导出的内容推断出依赖的图谱。这个特性与其它的如插件 and 加载器一道让开发者的体验更好。
看起来都不是很好……
是否可以有一种方式,不仅可以让我们编写模块,而且还支持任何模块格式(至少在我们到达 ESM 之前),并且可以同时处理资源和资产?
这就是 webpack 存在的原因。它是一个工具,可以打包你的 JavaScript 应用程序(支持 ESM 和 CommonJS),可以扩展为支持许多不同的静态资源,例如:images, fonts 和 stylesheets。
webpack 关心性能和加载时间;它始终在改进或添加新功能,例如:异步地加载 chunk 和预取,以便为你的项目和用户提供最佳体验。
参考文档 webpack.docschina.org/concepts/wh…
610.[Webpack] 如何打包运行时 chunk , 且在项目工程中, 如何去加载这个运行时 chunk ?【热度: 421】【工程化】【出题公司: 阿里巴巴】
Webpack打包运行时chunk的方式可以通过optimization.runtimeChunk选项来配置。下面是一个示例的配置:
module.exports = {
// ...
optimization: {
runtimeChunk: 'single',
},
};
上述配置中,通过设置optimization.runtimeChunk为'single',将会把所有的webpack运行时代码打包为一个单独的chunk。
在项目工程中加载运行时chunk有两种方式:
- 通过script标签加载:可以使用HtmlWebpackPlugin插件来自动将运行时chunk添加到 HTML 文件中。在webpack配置文件中添加以下配置:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
// ...
chunks: ['runtime', 'app'],
}),
],
};
上述配置中,chunks选项指定了要加载的chunk,包括运行时chunk('runtime')和其他的业务代码chunk('app')。最终生成的HTML文件会自动引入这些chunk。
- 通过import语句动态加载:可以使用动态导入的方式来加载运行时chunk。在需要加载运行时chunk的地方,使用以下代码:
import(/* webpackChunkName: "runtime" */ './path/to/runtime').then((runtime) => {
// 运行时chunk加载完成后的逻辑
});
上述代码中,通过import()函数动态加载运行时chunk,通过webpackChunkName注释指定要加载的chunk名称(这里是'runtime')。加载完成后,可以进行相关逻辑处理。
总结:Webpack可以通过optimization.runtimeChunk选项配置打包运行时chunk,可以通过script标签加载或者使用动态导入的方式来加载运行时chunk。
追问
如果只想把某几个文件打包成运行时加载, 该如何处理呢?
如果你想将某几个文件打包成运行时加载,可以使用Webpack的entry配置和import()语法来实现。
首先,在Webpack的配置文件中,将这几个文件指定为单独的entry点。例如:
module.exports = {
// ...
entry: {
main: './src/main.js',
runtime: './src/runtime.js',
},
};
上述配置中,main.js是业务代码的入口文件,runtime.js是你想要打包成运行时加载的文件。
然后,在你的业务代码中,通过import()动态导入这些文件。例如:
function loadRuntime() {
return import('./runtime.js');
}
// 使用动态导入的方式加载运行时文件
loadRuntime().then(runtime => {
// 运行时文件加载完成后的逻辑
});
使用import()会返回一个Promise,可以通过.then()来处理文件加载完成后的逻辑。
最后,使用Webpack进行打包时,会根据配置的entry点和import()语法自动将这几个文件打包成运行时加载的模块。运行时模块会在需要时动态加载并执行。
注意:在使用import()动态导入文件时,需要确保你的环境支持Promise和动态导入语法。
作为上面回复的补充
除了 entry 的方式可以处理自己申明的 runtime 文件以外, 还可以直接在 import('xx') 的时候申明;
例如:
import(/* webpackChunkName: "runtime" */ './path/to/runtime').then((runtime) => {
// 运行时chunk加载完成后的逻辑
});
上面的方式, 可以在也可以达到同样的效果, 只是在 import 的时候申明runtime文件名称而已
612.[Webpack] 全面了解 tree shaking【热度: 790】【工程化】【出题公司: 阿里巴巴】
webpack 如何做 tree shaking
Webpack通过tree shaking技术实现了JavaScript代码的优化和精简。Tree shaking是指通过静态代码分析,识别和移除未被使用的代码(被称为"dead code"),从而减小最终打包后的文件大小。
下面是Webpack如何进行tree shaking的步骤:
- 代码标记:在代码中使用ES6模块化语法(如
import和export)来明确指定模块的依赖关系。 - 代码解析:Webpack会解析整个代码,并构建一个依赖图谱,记录模块之间的依赖关系。
- 标记未使用代码:在构建依赖图谱的过程中,Webpack会标记那些未被使用的模块,以及这些模块中的未被使用的函数、类、变量等。
- 无副作用标记:Webpack还会检查模块的副作用(例如对全局变量的修改、网络请求等),并将没有副作用的代码视为可安全移除的。
- 未使用代码移除:在代码打包阶段,Webpack会根据标记的未使用代码信息,从最终的打包结果中移除这些未被使用的代码。
通过tree shaking,Webpack可以减小打包后的文件大小,提高应用的加载速度和性能。但要注意的是,tree shaking只对ES6模块化的代码有效,对于CommonJS模块化的代码则无法进行优化。另外,有些代码可能由于复杂的依赖关系无法被正确地标记为未使用,这就需要开发者自己进行配置或使用其他工具进行优化。
webpack 处理 tree shaking 配置
要在Webpack中配置tree shaking,需要进行以下步骤:
- 在
webpack.config.js文件中,将mode设置为production,这会启用Webpack的优化功能,包括tree shaking。
module.exports = {
mode: 'production',
// 其他配置...
};
- 确保你的代码使用了ES6模块化语法(使用
import和export),以便Webpack能够正确地进行静态代码分析。 - 确保你的代码库中没有副作用。Webpack会假设没有副作用的代码可以安全地移除。如果你的代码确实有副作用,可以在webpack配置文件中的
optimization选项中设置sideEffects为false来告诉Webpack可以安全地进行tree shaking。
module.exports = {
mode: 'production',
optimization: {
sideEffects: false,
},
// 其他配置...
};
了解一下副作用
在计算机科学中,副作用是指函数或代码的执行对除了返回一个值之外的程序状态产生了可观察的变化。换句话说,副作用是指对外部环境产生了影响或产生了可观察的行为。
以下是一些常见的副作用示例:
- 修改全局变量或外部状态:函数修改了全局变量或外部状态,例如修改了一个共享的数组、对象或文件等。
- 发送网络请求:函数通过网络发送了一个HTTP请求,这会触发网络交互并产生副作用。
- 修改函数参数:函数修改了传入的参数值,这会影响函数外部的变量。
- 控制台打印:函数在执行过程中使用了
console.log()或其他打印语句,这会在控制台中产生可观察到的输出。 - 异步操作:函数中包含了异步操作,例如定时器、Promise或通过回调函数实现的异步操作。
如何申明代码是有副作用
某一些代码是是需要禁止被清理掉, 这个时候该如何处理呢?
有几个办法:
方法一:在配置文件中指定副作用
在Webpack配置文件中,可以使用sideEffects选项来指定哪些文件或模块具有副作用,不允许清理。sideEffects接受一个正则表达式、一个文件名或一个数组。例如:
module.exports = {
//...
optimization: {
usedExports: true
},
mode: 'production',
sideEffects: ["./src/some-module.js"]
};
在上面的例子中,sideEffects数组中的./src/some-module.js文件将会被标记为具有副作用,不会被清理。
请注意,为了使sideEffects选项生效,你需要在配置文件中启用optimization.usedExports选项,并将mode设置为production。
方法二:package.json 中配置 sideEffects 属性
可以在package.json文件中使用sideEffects字段来申明哪些文件或模块具有副作用,不允许被清理。
- 如果将
sideEffects设置为布尔值false,表示所有导入的文件都被认为没有副作用,可以被tree shaking清理。这在大多数情况下是默认的行为。
{
"name": "my-package",
"version": "1.0.0",
"sideEffects": false
}
- 如果设置为布尔值
true,表示所有导入的文件都被认为有副作用,不会被tree shaking清理。
{
"name": "my-package",
"version": "1.0.0",
"sideEffects": true
}
- 如果将
sideEffects设置为一个数组,数组中的每个元素可以是一个字符串或一个正则表达式,表示具有副作用的文件或模块。
{
"name": "my-package",
"version": "1.0.0",
"sideEffects": [
"./src/some-module.js",
"/.css$/"
]
}
在上述示例中,./src/some-module.js文件和所有以.css结尾的文件都被认为有副作用,不会被tree shaking清理。
如果我某一个文件配置了 sideEffects 申明该文件有副作用, 但是我又想清理其中的某个函数
魔法中的魔法注释: /*#__PURE__*/
通过上面的知识, 我们知道了, 如果是有如果被 sideEffects 申明了副作用的文件, 是不会被 tree shaking 清理掉的,但是也有例外。
/*#__PURE__*/这个注释的作用是告诉Webpack或Babel等构建工具,这一行代码是纯粹的,没有副作用,并且可以安全地进行tree shaking(摇树优化)。
对于一些库或框架,可能会有一些函数或类被导出,但实际上很少被使用,为了让构建工具知道这些代码可以被删除,可以在导出语句上添加/*#__PURE__*/注释。
例如,假设 src/myModule.js 文件有下面的代码:
export /*#__PURE__*/ function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
且 webpack 已经将 src/myModule.js 申明为了有副作用文件
module.exports = {
// ...
optimization: {
sideEffects: ["./src/myModule.js"],
},
};
虽然通过 sideEffects 配置申明了 ./src/myModule.js 文件是有副作用的,但是由于 add 方法前面有 /*#__PURE__*/ 注释标记,意味着这个方法被标记为纯函数,该方法是没有副作用。
因此最终通过 /*#__PURE__*/ 注释标记的 add 方法依然可以被 Webpack 的 Tree Shaking 清理。
commonjs 模块就真的不能被 tree shaking 了?
下面这段来自于 webpack 官网 参考文档: webpack.docschina.org/blog/2020-1…
Webpack 曾经不进行对 CommonJs 导出和 require() 调用时的导出使用分析。
Webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require() 调用中跟踪引用的导出名称。
支持以下构造:
-
exports|this|module.exports.xxx = ... -
exports|this|module.exports = require("...") (reexport) -
exports|this|module.exports.xxx = require("...").xxx (reexport) -
Object.defineProperty(exports|this|module.exports, "xxx", ...) -
require("abc").xxx -
require("abc").xxx() -
从 ESM 导入
-
require()一个 ESM 模块 -
被标记的导出类型 (对非严格 ESM 导入做特殊处理):
Object.defineProperty(exports|this|module.exports, "__esModule", { value: true|!0 })exports|this|module.exports.__esModule = true|!0
当检测到不可分析的代码时,webpack 会放弃,并且完全不跟踪这些模块的导出信息(出于性能考虑)。
终极必杀问:webpack tree-shaking 在什么情况下会失效
- 动态导入:如果你使用了动态导入(例如使用了 import() 或 require.ensure()),webpack 无法静态分析模块的导入和导出,因此无法进行 tree-shaking。
- 未使用 ES6 模块语法:tree-shaking 只能对 ES6 模块语法进行优化,如果你的代码中没有使用 ES6 模块语法,webpack 将无法进行 tree-shaking。
- 模块被动态引用或条件引用:如果模块的引用方式是动态的(例如在循环或条件语句中引用),或者通过字符串拼接来引用模块,webpack 无法确定哪些模块会被引用,因此无法进行 tree-shaking。
- 使用了副作用的代码:如果你的代码中包含有副作用的代码(例如在模块的顶级作用域中执行了一些操作),webpack 无法确定哪些代码是无用的,因此无法进行 tree-shaking。
可以参考这个回答:#523
622.[Webpack] 有哪些优化项目的手段?【热度: 1,163】【工程化】【出题公司: 阿里巴巴】
关键词:打包优化
围绕 webpack 做性能优化,分为两个方面:构建时间优化、构建产物优化
-
优化构建时间
构建时间优化
缩小范围
我们在使用 loader 时,可以配置 include、exclude缩小 loader 对文件的搜索范围,以此来提高构建速率。
像 /node_moudles 目录下的体积辣么大,又是第三方包的存储目录,直接 exclude 掉可以节省一定的时间的。
当然 exclude 和 include 可以一起配置,大部分情况下都是只需要使用 loader 编译 src 目录下的代码
module.exports = {
module: {
rules: [
{
test: /.(|ts|tsx|js|jsx)$/,
// 只解析 src 文件夹下的 ts、tsx、js、jsx 文件
// include 可以是数组,表示多个文件夹下的模块都要解析
include: path.resolve(__dirname, '../src'),
use: ['thread-loader', 'babel-loader'],
//当然也可以配置 exclude,表示 loader 解析时不会编译这部分文件
//同样 exclude 也可以是数组
exclude: /node_modules/,
}
]
}
}
还需注意一个点就是要确保 loader 的准确性,比如不要使用 less-loader 去解析 css 文件
文件后缀
resolve.extensions 是我们常用的一个配置,他可以在导入语句没有带文件后缀时,可以按照配置的列表,自动补上后缀。** 我们应该根据我们项目中文件的实际使用情况设置后缀列表,将使用频率高的放在前面、同时后缀列表也要尽可能的少,减少没有必要的匹配**。同时,我们在源码中写导入语句的时候,尽量带上后缀,避免查找匹配浪费时间。
module.export = {
resolve: {
// 按照 tsx、ts、jsx、js 的顺序匹配,若没匹配到则报错
extensions: ['.tsx', '.ts', '.jsx', '.js'],
}
}
别名
通过配置 resolve.alias 别名的方式,减少引用文件的路径复杂度
module.exports = {
resolve: {
alias: {
//把 src 文件夹别名为 @
//引入 src 下的文件就可以 import xxx from '@/xxx'
'@': path.join(__dirname, '../src')
}
}
}
// 引入 src 下的某个模块时
import XXX from '@/xxx/xxx.tsx'
缓存
在优化的方案中,缓存也是其中重要的一环。在构建过程中,开启缓存提升二次打包速度。
在项目中,js 文件是占大头的,当项目越来越大时,如果每次都需要去编译 JS 代码,那么构建的速度肯定会很慢的,所以我们可以配置 babel-loader 的缓存配置项 cacheDirectory 来缓存没有变过的 js 代码
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
}
]
}
]
}
}
上面的缓存优化只是针对像 babel-loader 这样可以配置缓存的 loader,那没有缓存配置的 loader 该怎么使用缓存呢,此时需要 cache-loader
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
'cache-loader',
"babel-loader"
],
}
]
}
}
编译后同样多一个 /node_modules/.cache/cache-loader 缓存目录
当然还有一种方式,webpack5直接提供了 cache 配置项,开启后即可缓存
module.exports = {
cache: {
type: 'filesystem'
}
}
编译后会多出 /node_modules/.cache/webpack 缓存目录
并行构建
首先,运行在Node里的webpack是单线程的,所以一次性只能干一件事,那如果利用电脑的多核优势,也能提高构建速度 ?thread-loader可以开启多进程打包
module.exports = {
module: {
rules: [
{
test: /.jsx?$/,
use: [
// 开启多进程打包。
{
loader: 'thread-loader',
options: {
workers: 3 // 开启 3个 进程
}
},
{
loader: 'babel-loader',
}
]
}
]
}
}
放置在这个 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool) 中运行。
每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。所以建议仅在耗时的 loader 上使用。若项目文件不算多就不要使用,毕竟开启多个线程也会存在性能开销。
定向查找第三方模块
resolve.modules 配置用于指定 webpack 去哪些目录下寻找第三方模块。默认值是 ['node_modules'] 。而在引入模块的时候,会以 node 核心模块 -----> node_modules ------> node全局模块 的顺序查找模块。
我们通过配置 resolve.modules 指定 webpack 搜索第三方模块的范围,提高构建速率
module.export = {
resolve: {
modules: [path.resolve(__dirname, 'node_modules')]
}
}
构建产物优化
压缩 js
webpack5的话通过 terser-webpack-plugin 来压缩 JS,但在配置了 mode: production 时,会默认开启
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
// 开启压缩
minimize: true,
// 压缩工具
minimizer: [
new TerserPlugin({}),
],
},
}
需要注意一个地方:生产环境会默认配置terser-webpack-plugin,所以如果你还有其它压缩插件使用的话需要将TerserPlugin显示配置或者使用...,否则terser-webpack-plugin会被覆盖。
const TerserPlugin = require("terser-webpack-plugin");
optimization: {
minimize: true,
minimizer
:
[
new TerserPlugin({}), // 显示配置
// "...", // 或者使用展开符,启用默认插件
// 其它压缩插件
new CssMinimizerPlugin(),
],
}
,
压缩 css
压缩 css 我们使用 css-minimizer-webpack-plugin
同时,应该把 css 提取成单独的文件,使用 mini-css-extract-plugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
// 提取成单独的文件
MiniCssExtractPlugin.loader,
"css-loader"
],
exclude: /node_modules/,
},
]
},
plugins: [
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "asset/css/main.css",
})
],
optimization: {
minimize: true,
minimizer: [
// 压缩 css
new CssMinimizerPlugin({}),
],
},
}
压缩 html
压缩 html 使用的还是 html-webpack-plugin 插件。该插件支持配置一个 minify 对象,用来配置压缩 html。
module.export = {
plugins: [
new HtmlWebpackPlugin({
// 动态生成 html 文件
template: "./index.html",
minify: {
// 压缩HTML
removeComments: true, // 移除HTML中的注释
collapseWhitespace: true, // 删除空⽩符与换⾏符
minifyCSS: true // 压缩内联css
},
})
]
}
压缩图片
可以通过 image-webpack-loader 来实现
module.exports = {
module: {
rules: [
{
test: /.(png|jpg|gif|jpeg|webp|svg)$/,
use: [
"file-loader",
{
loader: "image-webpack-loader",
options: {
mozjpeg: {
progressive: true,
},
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.9],
speed: 4,
},
gifsicle: {
interlaced: false,
},
},
},
],
exclude: /node_modules/, //排除 node_modules 目录
},
]
},
}
按需加载
很多时候我们不需要一次性加载所有的JS文件,而应该在不同阶段去加载所需要的代码。
将路由页面/触发性功能单独打包为一个文件,使用时才加载,好处是减轻首屏渲染的负担。因为项目功能越多其打包体积越大,导致首屏渲染速度越慢。
实际项目中大部分是对懒加载路由,而懒加载路由可以打包到一个 chunk 里面。比如某个列表页和编辑页它们之间存在相互跳转,如果对它们拆分成两个 import() js 资源加载模块,在跳转过程中视图会出现白屏切换过程。
因为在跳转期间,浏览器会动态创建 script 标签来加载这个 chunk 文件,在这期间,页面是没有任何内容的。
所以一般会把路由懒加载打包到一个 chunk 里面
const List = lazyComponent('list', () => import(/* webpackChunkName: "list" */ '@/pages/list'));
const Edit = lazyComponent('edit', () => import(/* webpackChunkName: "list" */ '@/pages/edit'));
但需要注意一点:动态导入 import() 一个模块,这个模块就不能再出现被其他模块使用 同步 import 方式导入。
比如,一个路由模块在注册 <Route /> 时采用动态 import() 导入,但在这个模块对外暴露了一些变量方法供其他子模块使用,在这些子模块中使用了同步 ESModule import 方式引入,这就造成了 动态 import() 的失效。
prload、prefetch
对于某些较大的模块,如果点击时再加载,那可能响应的时间反而延长。我们可以使用 prefetch、preload 去加载这些模块
prefetch:将来可能需要一些模块资源(一般是其他页面的代码),在核心代码加载完成之后带宽空闲的时候再去加载需要用到的模块代码。
preload:当前核心代码加载期间可能需要模块资源(当前页面需要的但暂时还没使用到的),其是和核心代码文件一起去加载的。
只需要通过魔法注释即可实现,以 prefetch 为例:
document.getElementById('btn1').onclick = function() {
import(
/* webpackChunkName: "btnChunk" */
/* webpackPrefetch: true*/
'./module1.js'
).then(fn => fn.default());
}
这行代码表示在浏览器空闲时加载 module1.js 模块,并且单独拆一个 chunk,叫做 btnChunk
可以看到,在head里面,我们的懒加载模块被直接引入了,并且加上了rel='prefetch'。
这样,页面首次加载的时候,浏览器空闲的会后会提前加载module1.js。当我们点击按钮的时候,会直接从缓存中读取该文件,因此速度非常快。
代码分割
在项目中,一般是使用同一套技术栈和公共资源。如果每个页面的代码中都有这些公开资源,就会导致资源的浪费。在每一个页面下都会加载重复的公共资源,一是会浪费用户的流量,二是不利于项目的性能,造成页面加载缓慢,影响用户体验。
一般是把不变的第三方库、一些公共模块(比如 util.js)这些单独拆成一个 chunk,在访问页面的时候,就可以一直使用浏览器缓存中的资源
webpack 里面通过 splitChunks 来分割代码
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 值有 `all`,`async` 和 `initial`
minSize: 20000, // 生成 chunk 的最小体积(以 bytes 为单位)。
minRemainingSize: 0,
minChunks: 1, // 拆分前必须共享模块的最小 chunks 数。
maxAsyncRequests: 30, // 按需加载时的最大并行请求数。
maxInitialRequests: 30, // 入口点的最大并行请求数。
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[/]node_modules[/]/, //第三方模块拆出来
priority: -10,
reuseExistingChunk: true,
},
util.vendors
:
{
test: /[/]utils[/]/, //公共模块拆出来
minChunks
:
2,
priority
:
-20,
reuseExistingChunk
:
true,
}
,
},
},
},
}
;
tree shaking
tree shaking 的原理细节可以看这篇文章:# webpack tree-shaking解析
tree shaking在生产模式下已经默认开启了
只是需要注意下面几点:
- 只对
ESM生效 - 只能是静态声明和引用的
ES6模块,不能是动态引入和声明的。 - 只能处理模块级别,不能处理函数级别的冗余。
- 只能处理
JS相关冗余代码,不能处理CSS冗余代码。
而可能样式文件里面有些代码我们也没有使用,我们可以通过purgecss-webpack-plugin 插件来对 css 进行 tree shaking
const path = require("path");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const glob = require("glob"); // 文件匹配模式
module.exports = {
//...
plugins: [
...
new PurgeCSSPlugin({
paths: glob.sync(`${PATH.src}/**/*`, { nodir: true }),
})
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
};
gzip
前端除了在打包的时候将无用的代码或者 console、注释剔除之外。我们还可以使用 Gzip 对资源进行进一步压缩。那么浏览器和服务端是如何通信来支持 Gzip 呢?
- 当用户访问 web 站点的时候,会在
request header中设置accept-encoding:gzip,表明浏览器是否支持Gzip。 - 服务器在收到请求后,判断如果需要返回
Gzip压缩后的文件那么服务器就会先将我们的JS\CSS等其他资源文件进行Gzip压缩后再传输到客户端,同时将response headers设置content-encoding:gzip。反之,则返回源文件。 - 浏览器在接收到服务器返回的文件后,判断服务端返回的内容是否为压缩过的内容,是的话则进行解压操作。
一般情况下我们并不会让服务器实时 Gzip 压缩,而是利用webpack提前将静态资源进行Gzip 压缩,然后将Gzip 资源放到服务器,当请求需要的时候直接将Gzip 资源发送给客户端。
我们只需要安装 compression-webpack-plugin 并在plugins配置就可以了
const CompressionWebpackPlugin = require("compression-webpack-plugin"); // 需要安装
module.exports = {
plugins: [
new CompressionWebpackPlugin()
]
}
作用域提升
Scope Hoisting 可以让 webpack 打包出来的代码文件体积更小,运行更快。
在开启 Scope Hoisting后,构建后的代码会按照引入顺序放到一个函数作用域里,通过适当重命名某些变量以防止变量名冲突,从而减少函数声明和内存花销。
需要注意:Scope Hoisting 需要分析模块之间的依赖关系,所以源码必须采用 ES6 模块化语法
Scope Hoisting 是 webpack 内置功能,只需要在plugins里面使用即可,或者直接开启生产环境也可以让作用域提升生效。
module.exports = {
//方式1
mode: 'production',
//方式2
plugins: [
// 开启 Scope Hoisting 功能
new webpack.optimize.ModuleConcatenationPlugin()
]
}