一些参考链接vue相关面试题参考以下内容即可:

vue2、vue3、vite、pina、ts等
virtul DOM就是用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化为对象操作,最终通过diff算法比对差异来进行更新DOM。2、虚拟DOM不依赖真实平台环境从而也可以实现跨平台。调用render函数,返回的对象就是虚拟dompatch过程中转化为真实的DOM。vue中的每个组件都有一个渲染函数watcher(vue2)、effect(vue3)。数据是响应式的,数据变化后会执行watcher或者effect。组件要合理的划分,如果不拆分组件,那更新的时候整个页面都要重新更新。如果过分拆分组件会导致watcher、effect产生过多也会造成性能浪费。

Object.defineProperty()重写属性,proxy是代理。
注意:
1、 数组
Object.defineProperty()是可以对数组实现监听操作的,但是vue并没有实现这个功能,因为数组长度不定而且数据可能会很多,如果对每一个数据都实现监听,性能代价太大。但是注意:数组中的元素是引用类型时是会被监听的(数组中如果是对象数据类型也会进行递归劫持)。vue对push,pop,splice等方法进行了hack,对于这些变异方法vue做了包裹,在原型上进行了拦截。
2、对象
Object.defineProperty()针对的是对象的某个属性,而且这个操作在vue的初始化阶段就完成了,所以新增的属性无法监听,通过set方法新增对象就相当于初始化阶段的数据响应式处理。
对象的替代方案:
// 新增
Vue.set(obj, newkey, newvalue)
vm.$set(obj, newkey, newvalue)
obj = Object.assign({}, obj, {newkey1: newvalue1, newkey2: newvalue2})
// 删除
Vue.delete(obj, key)
vm.$delete(obj, key)
delete 与 vm.$delete的区别?
1、删除对象结果一样:两者相同,都会把键名(属性/字段)和键值删除。
2、删除数组不同
delete,只是将要删除的变为undefined或者empty,不改变数组的键值名以及数组长度;
vm.$delete是直接将值完全删除掉,会改变数组的键值名以及数组长度;
Vue.set() 和 this.$set() 的区别?
import { set } from '../observer/index'
...
Vue.set = set
...
import { set } from '../observer/index'
...
Vue.prototype.$set = set
...
可以发现Vue.set()和this.$set()这两个 api 的实现原理基本一样,都是使用了set函数。
区别: Vue.set( ) 是将set函数绑定在 Vue 构造函数上,this.$set() 是将set函数绑定在 Vue原型上。
vm.$set 的实现原理
splice方法触发响应式;属性是否存在、对象是否是响应式,如果存在,直接进行赋值;如果不存在,则是通过调用 defineReactive方法进行响应式处理( defineReactive 方法就是 Vue在初始化对象时,给对象属性采用Object.defineProperty动态添加getter和setter的功能所调用的方法)
数组考虑性能原因没有使用defineProperty对数组的每一项进行拦截,而是选择重写数组(push等)方法;数组中如果是对象数据类型也会进行递归劫持;索引和长度变化·时无法监控到的。替代方案:
// 修改值
vm.$set(arr, index, newvalue)
arr.splice(index, 1, newvalue)
// 修改数组长度
arr.splice(newLen)
因为:调用数组的pop、push、shift、unshift、splice、sort、reverse等方法时是可以监听到数组的变化的
vue3中使用proxy方法进行代理。
通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」。
所依赖的watcher,当属性变化后会通知自己对应的watcher去更新;初始化时调用render函数,此时会触发属性依赖收集dep.depend;Map结构将属性和effect映射起来;初始化时调用render函数,此时会触发属性依赖收集track;trigger;
vue2中有三种watcher(渲染watcher、计算属性watcher、用户watcher)
vue3中有三种effect(渲染effect、计算属性effect、用户effect)
备注:计算属性watcher对应computed,用户watcher对应watch
计算属性仅当用户取值时才会执行对应的方法computed具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行
new proxy只能对对象处理基本数据类型,基本数据类型不行)
watch 和 watchEffect 的主要功能是相同的,都能响应式地执行回调函数。它们的区别是追踪响应式依赖的方式不同:
不会追踪在回调中访问到的东西;默认情况下,只有在数据源发生改变时才会触发回调;watch 可以访问侦听数据的新值和旧值。会初始化执行一次,在副作用发生期间追踪依赖,自动分析出侦听数据源;watchEffect 无法访问侦听数据的新值和旧值。简单一句话,watch 功能更加强大,而 watchEffect 在某些场景下更加简洁。
为什么转换的原因:我们在编写的时候不会直接编写虚拟DOM,我们可以直接使用模板编写,然后转换成render函数,我们调用render函数就可以生成对应的VDOM。
将template模板转化成ast语法树(逐词分析,词法分析、语法分析);对静态语法做静态标记。(这个过程为后续更新渲染可以直接跳过静态节点做优化)render字符串(增加with, new function),然后生成render函数。vue3的模板转化,做了更多优化操作。vue2仅仅是标记了静态节点而已。
在非父子组件通信时,可以使用eventBus或者状态管理工具,但是功能不复杂的时候我们可以考虑使用vue.observable。(vue3不再使用)
beforeCreate和created之前运行的,所以你可以用它来代替这两个钩子函数。on,但也有两个钩子函数发生了变化:BeforeDestroy变成了onBeforeUnmount,destroy变成了onUnmount。
vue基于虚拟DOM做更新。diff的核心就是比较两个虚拟节点的差异。vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。
key、tag 来进行比较是否是相同节点老的儿子是一个列表,新的儿子也是一个列表,updateChildren方法vue3中采用
最长递增子序列来实现diff算法。
同级比较的时候采用首尾指针法




安装vue插件,如果插件是一个对象,必须提供install方法;如果插件是一个函数,它会被作为install方法,install方法调用时,会将vue作为参数传入,这样插件中就不在需要依赖vue了。
本质就是:Vue-Router、Vuex、ElementUI三者都具有install方法,并且插件的运行依赖于install方法里的一些操作,才能正常运行,而axios没有install方法也能正常运行。
vuex通过Vue.use(Vuex)绑定vue实例上的,然后调用了install方法,通过applyMinxin(vue)在任意组件内执行this.$store就就可以访问到store对象。
如此,实现把store挂载到组件里面。vuex的state是响应式的,借助的就是vue的data。
vue中的构造器函数,可以创建一个子类,参数是一个包含组件选项的对象。
作用:
v-once是vue中的内置指令,只渲染元素和组件一次,之后的重新渲染会被视为静态内容而跳过,直接只用缓存。
slot插槽设计来源于web components,利用slot进行占位,在使用组件时,组件标签内部会分发到对应的slot中。默认插槽、具名插槽,插槽就是方便用户定制化组件
作用:
弹框、表格
原理:
默认插槽:
具名插槽:
作用域插槽:
普通插槽就是替换,父组件渲染完之后,替换子组件的slot占位符;作用域插槽会把我们的父组件渲染成一个函数,子组件会调用这个函数,并且把数据传递给他,我们会拿到函数的返回值来替换slot占位符。
vue中双向绑定考的是指令v-model,可以绑定一个动态值到视图上,同时修改视图能改变数据对应的值(能修改的视图就是表单组件),经常会听到一句话:v-model是value + input的语法糖。
名为value的prop和名为input的事件。对于组件而言,v-model就是value + input的语法糖。可用于组件中的数据的双向绑定。备注:组件中如果有多个数据想做数据绑定,vue2不支持使用多个v-model的。vue3中可以通过一下方式进行绑定:
在有些情况下,我们可能需要对一个prop进行双向绑定,这时可以使用.sync来实现。v-model默认只能双向绑定一个属性,这里就可以通过.sync修饰符绑定多个属性。vue3已经废除这个。
推荐的做法是将异步组件和webpack的code-splitting功能一起配合使用。
单页面应用的 html 是靠 js 生成,因为首屏需要加载很大的js文件(app.js vendor.js),所以当网速差的时候会产生一定程度的白屏
“打包慢”,是一个综合的因素,和vue关系不大。
loader范围缩小到src项目文件!一些不必要的loader能关就关了吧extensions,可以让 webpack 少做一点后缀匹配。使用插件直接拷贝静态文件‘eval’,或者‘cheap-module-eval-source-map’
$store.state.xx(在vue文件中监听vuex的数据变化)store.watch,(在非vue文件中监听vuex数据的变更)Vue 中监听Vuex数据的变化可以通过Vuex提供的subscribe方法来实现揭秘vuex是如何给每个组件挂载$store的
vue中如何监听vuex中的数据变化
vue 监听vuex数据变化
template
视图更新是异步的,使用nextTick方法可以保证用户定义的逻辑在更新之后执行;获取更新后的DOM,多次调用nextTick会被合并。document.getElementById('a').innerHTML原理:
总结:
keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated与deactivated);
属性:include、exclude、max
this.cache, 是一个对象,用来存储需要缓存的组件。name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode;key值,拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存;直接从缓存中拿vnode的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个。key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys中;this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉;beforeRouteEnter,重新获取数据;actived钩子,获取数据;
这一节可以好好学学

mergeOptions && mixin && extend的关系

3 种路由模式的说明如下:
hash: 使用 URL hash 值来作路由,支持所有浏览器,原理:hash + 通过监听hashChange事件,兼容性好,不够美观,hash服务端无法获取,不利于seo优化;history : 依赖 HTML5 History API 和服务器配置,原理:History API + 监听popState事件,美观,刷新会出现404;发现没有浏览器的 API,路由会自动强制进入这个模式.
history模式





VueX规定了单向数据流,VueX的State放到v-model双向绑定报错,本来就是代码问题。和冲突么关系。而且VueX的双向绑定就是利用了new Vue实现的。为了单项数据流设置了Flag作为标记。不应该是VueX和双向绑定的冲突。是coder的问题。
Vue3必会——组合API ~三分钟带你了解组合API的魅力
vue3 + ts + vite + pinna + eletron
相关链接:Vue3 + vite + Ts + pinia + 实战 + 源码 +electron



ref的原理
toRef 只能修改响应式对象的值,非响应式视图无变化;
当结构reactive的时候,一定加上toRefs,可以让解构出来的内容带上响应式;
vue在用v-if v-else渲染两个相同的按钮,一个绑定了事件,另外一个没有绑定事件。当渲染状态切换的时候,会导致未绑定事件的按钮也绑定上了事件。原因是有的vue版本在没给条件渲染的元素加上key标识时候会默认复用元素提升渲染能力,导致事件被错误的绑定上另一个按钮。解决方案:更换高版本vue,加上key标识两个按钮。
hooks是针对在使用react时存在以下问题而产生的:
组件之间复用状态逻辑很难,在hooks之前,实现组件复用,一般采用高阶组件;this的工作方式,不能忘记绑定事件处理器等操作,代码复杂且冗余。react hooks之后:
复用逻辑提取到组件顶层,而不是强行提升到父组件中。避免上面陈述的class组件带来的那些问题;
vue3的diff算法没有vue2的头尾、尾头之间的diff,对diff算法进行了优化,最长递归子序列。

优点:
逻辑复用:组合式 API 提供了更灵活的逻辑复用方式。模块化组织:组合式 API 将组件逻辑拆分为可组合的函数,而不再依赖于生命周期钩子函数。这种模块化组织方式使得代码更具可读性和可维护性,同时提供了更好的代码组织结构,使开发者能够更容易地理解和管理组件。更自由的 JavaScript 编程:组合式 API 使用普通的 JavaScript 函数来定义组件的逻辑,不再受限于特定的 Vue 实例上下文。这使得开发者能够更自由地使用传统的 JavaScript 工具和模式,例如条件语句、循环和函数调用,从而提高了开发的灵活性和可维护性。代码组织和可维护性:通过组合式 API,可以更好地组织和封装组件的逻辑。逻辑相关的代码可以放在同一个函数中,使得代码结构更清晰,易于维护和理解。这种方式也有助于减少代码冗余,提高代码复用性和可测试性。
缺点

ref会对基本数据进行包装,把一个基本类型变成一个对象类型。
vue2中的mixin缺点:命名冲突、数据来源不清晰
逻辑组织和逻辑复用方面,Composition API是优于Options API(vue3向下兼容,可以使用两种)会有更好的类型推断。tree-shaking友好,代码也更容易压缩this的使用,减少了this指向不明的情况vue2的问题:代码可读性随着组件变大而变差、每一种代码复用的方式都存在缺点、ts支持度较差
可以单独依赖这个响应式库而不用去依赖整个 Vue;2、Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导体积优化、编译优化、数据劫持优化diff算法优化;vue3在diff算法中相比vue2增加了静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较;静态提升;Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用;事件监听缓存。默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化。SSR优化;当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染;Tree shankingvue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter和setter,实现响应式;vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到;
而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中
import { nextTick, observable } from 'vue'
nextTick(() => {})
Tree shaking是基于ES6模板语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
Tree shaking无非就是做了两件事:
编译阶段利用ES6 Module判断哪些模块已经加载判断那些模块和变量未被使用或者引用,进而删除对应代码
Webpack的基本功能有哪些:代码转换、文件优化、代码分割、模块合并、自动刷新、代码校验。
有五大核心概念:入口(entry)、输出(output)、解析器(loader)、插件(plugin)、模式(mode)。
流程:
Webpack会读取项目根目录下的Webpack配置文件,解析其中的配置项,并根据配置项构建打包流程。
在前端项目中,会存在js、css、png、scss、html等文件,webpack首先会确定一个打包的入口,然后顺着我们入口文件的代码,根据代码中出现的import语句来解析推断出所依赖的资源模块,然后去解析每一个模块对应的依赖,最后生成了项目中用到文件的一个依赖关系的依赖树。确定这个依赖树之后,webpack会递归这个依赖树,找到每个节点对应的资源文件,然后在根据我们配置文件的rule属性找到这个模块的加载器去加载这个模块,(Webpack会在打包流程中执行一系列插件,插件可以用于完成各种任务,例如生成HTML文件、压缩代码等等。),最后会把加载的结果放到bundle.js中,从而实现整个项目的打包。
在webpack内部中,webpack只支持对js 和json文件打包,像css、sass、png等这些类型的文件的时候,webpack则无能为力,这时候就需要配置对应的loader进行文件内容的解析;
webpack中的大致可以分为三类:编译转换类、文件操作类、代码检查类;
将css添加到style标签里,存在于head标签中;目的就是将css文件转化为js模块。允许将css文件通过require的方式引入,并返回css代码(注意:css-loader只是负责将.css文件进行一个解析,而并不会将解析后的css插入到页面中,需要用到style-loader)file-loader:文件操作类,文件资源加载器, 分发文件到output目录并返回相对路径,过程:webpack遇到图片文件,根据配置文件中的的配置,匹配到对应的文件加载器,先将文件拷贝到输出目录,然后在导出输出目录的路径作为当前模块的返回值进行返回;url-loader: 和file-loader类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)。适合场景:项目中的小文件、小图片使用url-loader,减少请求次数。此外,大文件单独提取存放,提高加载速度;用babel来转换ES6文件到ES5;babel是基于插件机制去实现的,它仅仅就是个平台,它的核心机制不会转化代码,具体转化代码是通过插件来实现。@babel/preset-env是一个插件的集合,这是转化的核心,其实也不是preset-env转化的,具体的是@babel/plugin-transform-module-commonjs实现的,此外,在创建.babelrc文件存放"presets": ["@babel/preset-env"];eslint-loader:运行ESLint检查的loader;loader本质是一个函数,接受源代码作为参数,返回处理后的结果。举个最简单的例子:开发一个markdownloader,希望导出的是转化后的html文件。
const marked = require('marked'); // markdown解析的模块
module.exports = source => {
// return 'console.log("hello~")'; // 返回的是一个JavaScript代码
const html = marked(source); // 解析
return `module.exports = ${JSON.stringify(html)}`;
// 第二种方式
}
webpack加载资源的过程有点像工作管道,在这个过程中使用多个loader,但是经过管道处理的结果一定是一段JavaScript代码。
通过以上:我们知道loader内部工作原理就是负责资源文件从输入到输出的转换,是一种管道的概念,将此次处理的结果交给下一个loader,完成一个功能。
使用:在rules中进行配置,当检测到markdown文件时,执行上面的loader。
loader专注于资源模块的加载,从而实现整个项目的打包;Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
插件是解决项目除了资源加载以外的其他自动化的工作!基于事件流框架 Tapable
plugin赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在 webpack 的不同阶段(钩子 / 生命周期),贯穿了webpack整个编译周期;
clean-webpack-plugin,打包之前先清理dist目录下的文件,如果不清理,则之前打包的会一直存在; html-webpack-plugin,自动引入打包结果bundle.js的html文件;copy-webpack-plugin,复制文件或目录到执行区域,如我们将一些文件放到public的目录下,那么这个目录会被复制到dist文件夹中;mini-css-extract-plugin,提取CSS到一个单独的文件中DefinePlugin,允许在编译时创建配置的全局对象,定义全局常量,可以在代码中直接使用;webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积webpack的插件机制通过钩子机制实现:它类似于事件,webpack在工作过程中有很多的环节,为了插件的扩展,webpack几乎在每一个环节都埋下了一个的钩子,这样我们在开发插件的时候,通过在不同的节点上挂载不同的任务,就可以轻松的扩展webpack的能力。

注意📢:webpack要求我们的插件必须是一个函数或者是已给包含apply方法的对象。
// 开发一个插件,清除webpack打包生成的bundle文件的不必要的注释;
// 首先明确这个任务的执行时机,依据开发的插件可以在明确生成bundle文件内容之后实施响应的任务
// 在webpack文档中找到一个emit钩子,发现emit这个钩子是在webpack即将往输出目录输出文件的时候执行;
// 定义
class myPlugin {
// compiler是webpack的核心对象,包含webpack此次构建的所有信息,也是通过这个对象构造钩子函数
apply(compiler) {
// console.log('myPlugin 启动,这个方法会在webpack启动时自动调用');
// tap传递两个参数,插件名称,挂载在钩子的函数
compiler.hooks.emit.tap('myPlugin', compilation => {
// compilation => 可以理解为此次打包的上下文,此次打包的所有结果都会放到这个对象中
for(const name in compilation.assets) {
// console.log(name)
// 只对打包的js结尾的文件处理
if(name.endsWith('.js')) {
// 1、先获取文件内容
const contents = compilation.asserts[name].source();
// 2、接下来用正则替换文件中的注释:找到注释/**/替换''
// 3、替换完的结果替换原来文件中的内容
}
}
})
}
}
// 执行, 在plugin配置中
new myPlugin()
emit,在Webpack生成输出文件之前触发,可以用于修改输出文件或生成一些附加文件。done: 在Webpack完成构建时触发,可以用于生成构建报告或通知开发者构建结果。compilation: 在Webpack编译代码期间触发,可以用于监听编译过程或处理编译错误。其他:
Webpack的事件机制是基于Tapable实现的,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。Tapable是Webpack事件机制的核心类,它封装了事件的订阅和发布机制。在Webpack中,Compiler对象和Compilation对象都是Tapable类的实例对象。
Tapable的源码解读:记住重点,核心就是call和tap两个方法。
webpack它工作流程能将各个插件plugin串联起来的原因,而实现这一切的核心就是Tapable。流程:
plugin。plugin中所有tap事件收集到每个生命周期的hook中。 (可以看到每次调用tap,就是收集当前hook实例所有订阅的事件到taps数组。)call方法的顺序(也就是生命周期)。就可以把所有plugin执行了。监听文件变化,自动重新打包。 npm webpack --watch
browerSync
wepack推出的一个工具,集成了【自动编译】和【自动刷新浏览器】等功能。
devServer: {
}
webpack Dev Server支持配置代理,用于本地代理跨域。
运行代码和源代码完全不同,源代码地图,提供一个运行代码和源代码之间的映射,帮助开发者调试和定位错误。
配置开发过程中的辅助工具,就是与source map相关的一些配置
devtool: 'source-map',
找到bundle打包的文件,发现在文件最后一行显示,通过注释的形式引入了source-map文件
//# sourceMappingURL=app.927d40ffb38db55716fd.js.map
eval会执行字符串函数
问题核心:自动刷新导致的页面状态丢失。
目的:页面不刷新的前提下,模块也可以及时更新。
HMR是webpack中最强大的功能之一。
devServer: {
hot: true
}
plugins: [
new webpack.HotModuleReplacementPlugin()
]
webpack中的HMR并不是开箱即用。
Q1:为什么样式文件的热更新开箱即用?
因为样式是loader处理的,在style-loader中自动处理了样式的热更新。
Q2:我的项目手动处理js文件,照样可以热更新。
框架中提供了HMR方案。
webpack-merge合并公共配置和私有配置。
为代码注入全局成员。往我们代码中注入一个process.env.NODE_ENV的常量,很多第三方的模块根据这个常量判断是否做一些操作。
plugins: [
// 接受一个对象,对象里面的键值会注入到我们的代码当中
new webpack.DefinePlugin({
'process.env': env
API_BASE_URL: '"http://baidu.com"' // 传入一个字符串
// API_BASE_URL: JSON.stringify('http://baidu.com') // 使用这种方式
}),
]
在我们代码中执行
console.log(API_BASE_URL);
在bundle.js中显示替换之后的值:console.log(http://baidu.com),不带引号
tree shaking不是指某个配置选型,它是一组功能搭配使用后的优化效果。在生产模式自动开启。
原理:Tree-shaking依赖于ES6的模块机制,因为ES6模块是静态的,编译时就能确定模块的依赖关系。对于非ES6模块的代码或者动态引入的代码,无法被消除掉。
由于 tree shaking 只支持 esmodule ,如果你打包出来的是 commonjs,此时 tree-shaking 就失效了。不过当前大家都用的是 vue,react 等框架,他们都是用 babel-loader 编译,以下配置就能够保证他一定是 esmodule。
// optimization集中配置webpack优化的功能
optimization: {
usedExports: true, // 表示只导出外部使用的成员
minimize: true // 移除
}
usedExports相当于【负责标记‘枯树叶’】,minimize负责【摇掉】他们。
合并模块函数 concatenateModules, 又被成为Scope Hoisting,作用域提升,合并代码模块,尽可能的将所有模块合并输出到一个函数中,既提升了运行效率,又减小了代码的体积。
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true
}
背景:
所以把打包结果分离到不同的bundle上,``按需加载`,太大也不行,太小也不行,物极必反
至于如何拆分,方式因人而异,因项目而异。我个人的拆分原则是:
业务代码和第三方库分离打包,实现代码分割;公共业务模块提取打包到一个模块;把一些特别大的库分别独立打包,剩下的加起来如果还很大,就把它按照一定大小切割成若干模块。多入口打包动态导入多入口打包,往往会有公共的模块,这里我们需要提取公共模块 split chunks。
optimization: {
splitChunks: {
chunks: 'all' // 表示会把所有的公共模块都提取到单独的模块当中
}
}
webpack使用动态导入的模式来实现模块的按需加载。动态导入的模块会被自动分包。
将css从打包结果中提取出来,从而实现css的按需加载。
plugins: [
new MiniCssExtractPlugin()
]
style-loader的目的是将样式通过style标签注入,此时需要MiniCssExtractPlugin.loader替换style-loader,将css文件单独提取成一个文件,使用link来引入。
注意📢:如果css太小,不建议提取,大于150KB建议提取。
webpack仅支持对js的压缩,其他文件的压缩需要使用插件。
可以使用 optimize-css-assets-webpack-plugin压缩CSS代码。放到minimizer中,在生产模式下就会自动压缩。
optimization: {
minimizer: [
// 指定了minimizer说明要自定义压缩器,所以要把JS的压缩器指明,否则无法压缩
// 如果没有new TerseWebpackPlugin(),则webpack默认为要用自定义压缩器,覆盖内部的压缩器
new TerseWebpackPlugin(),
new OptimizeCssAssetWebpackPlugin()
]
}
生产模式下,文件名使用Hash
文件级别的hash,:8是指定hash长度 (推荐)
output: {
filename: '[name]-[contenthash:8].bundle.js'
},
webpack proxy,即webpack提供的代理服务,基本行为就是接收客户端发送的请求后转发给其他服务器,其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制),想要实现代理首先需要一个中间服务器,webpack中提供服务器的工具为webpack-dev-server;
工作原理: proxy工作原理实质上是利用http-proxy-middleware 这个http代理中间件,实现请求转发给其他服务器;
从基础到实战 手摸手带你掌握新版Webpack4.0详解 一起读文档
Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
webpack-dev-server创建两个服务器
express框架启动本地server,负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);server启动之后,再去启动websocket服务,通过websocket,可以建立本地服务和浏览器的双向通信;entry入口默默增加了 2 个文件,那就意味会一同打包到bundle文件中去,也就是线上运行时。
webpack-dev-server/client/index.js,一个是获取websocket客户端代码;webpack/hot/dev-server.js,这个文件主要是用于检查更新逻辑的;webpack编译结束,就会调用_sendStats方法通过websocket给浏览器发送通知,ok和hash事件,这样浏览器就可以拿到最新的hash值了,做检查更新逻辑。webpack编译结束,_sendStats方法就通过websoket给浏览器发送通知,检查下是否需要热更新。被打包进bundle.js中webpack-dev-server/client/index.js文件,运行在浏览器中,socket方法建立了websocket和服务端的连接,并注册了 2 个监听事件。
hash事件,更新最新一次打包后的hash值。ok事件,进行热更新检查。webpack/hot/dev-server.js,运行在浏览器中。核心逻辑:这里webpack监听到了webpackHotUpdate事件,并获取最新了最新的hash值,然后终于进行检查更新了。
module.hot.check方法。module.hot.check又是哪里冒出来了的!答案是HotModuleReplacementPlugin搞得鬼。HotModuleReplacementPlugin原来也是默默的塞了很多代码到bundle.js中呀。这和第 2 步骤很是相似哦!为什么,因为检查更新是在浏览器中操作呀。这些代码必须在运行时的环境。moudle.hot.check方法是如何来的啦。那都做了什么?之后的源码都是HotModuleReplacementPlugin塞入到bundle.js中的哦
xxx/hash.hot-update.json的ajax请求;xxx/hash.hot-update.js 请求,通过JSONP方式。hotApply方法了
modules 中__webpack_require__执行相关模块的代码细节点:
compiler.watch这个方法了。监听本地文件的变化主要是通过文件的生成时间是否有变化,这里就不细讲了。将编译后的文件打包到内存。这就是为什么在开发的过程中,你会发现dist目录没有打包后的代码,因为都在内存中。原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销,这一切都归功于memory-fs。JSONP获取最新代码?主要是因为JSONP获取的代码可以直接执行。为什么要直接执行?我们来回忆下/hash.hot-update.js的代码格式是怎么样的。可以发现,新编译后的代码是在一个webpackHotUpdate函数体内部的。也就是要立即执行webpackHotUpdate这个方法。优化打包结果的核心目标就是让打出来的包体积更小,让打包的最终结果在浏览器运⾏快速⾼效。
打包体积分析:一般脚手架里直接运行命令行就能生成打包体积图,然后可以根据包体积能定向优化。缩小打包作用域:exclude/include (确定 loader 规则范围)等压缩代码:CSS、JS代码压缩,删除多余的代码、注释。模块懒加载:可以使用Webpack的动态导入功能,使用模块懒加载之后,大js文件会分成多个小js文件,网页加载时会按需加载。利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。开启Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。提取公共代码: SplitChunks抽取公共模块,可以浏览器⻓期缓存这些⽆需频繁变动的公共代码。分离第三方库:将第三方库从应用程序代码中分离出来,单独打包,这样可以提高缓存效率并减小应用程序代码的大小。实现微前端的一种方式,模块联邦是webpack5引入的特性,能轻易实现在两个使用 webpack 构建的项目之间共享代码,甚至组合不同的应用为一个应用。
模块联邦是实现多个项目之间共享代码的机制。举个例子,假设我们有一个微前端应用,其中包含了一个商品管理应用和一个订单管理应用,这两个应用都需要使用到同一个UI组件库。
为了避免重复的代码,我们可以将UI组件库拆分成一个独立的子应用作为模块提供方,然后通过模块联邦的方式在商品管理应用和订单管理应用中动态加载该组件库。
webpack4 上需要下载安装 terser-webpack-plugin 插件;webpack5内部本身就自带 js 压缩功能,他内置了 terser-webpack-plugin 插件,我们不用再下载安装。而且在 mode=“production” 的时候会自动开启 js 压缩功能。
webpack5 内部内置了 cache 缓存机制,使用持久化缓存。直接配置即可。
asset/resource 替换 file-loader(发送单独文件)
asset/inline 替换url-loader(导出 url)
Webpack5进一步改进了Tree Shaking算法,可以更准确地判断哪些代码是无用的,从而更好地优化构建输出的文件大小。webpack5的 mode=“production” 自动开启 tree-shaking。
Webpack5在构建速度方面进行了大量优化,尤其是在开发模式下,构建速度有了明显提升。
启动方面,webpack启动的时候会将所有的文件进行编译打包,vite启动的时候不需要打包,当浏览器启动模块的时候,再对模块内容进行编译,按需编译,根据esModules会自动向依赖的module发送请求,按需动态编译;热更新方面,Vite是按需加载,webpack是全部加载。当改动了一个模块后,vite 仅需让浏览器重新请求该模块即可,不像 webpack 那样需要把该模块的相关依赖模块全部编译一次,效率更高;底层原理方面,vite 是基于了 esbuild预构建的,更快(esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器(webpack)预构建依赖快 10-100 倍,js的操作一般是毫秒级,go的操作是纳秒级)在浏览器支持 ES 模块之前,JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并
将我们的源码模块串联成可以在浏览器中运行的文件。诸如 webpack、Rollup 和 Parcel 等工具应运而生。
Vite 旨在利用生态系统中的新进展解决上述问题:
浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。
优点:
缺点:
webpack支持的更广。Vite 是基于ES Module,所以代码中不可以使用CommonJs;开发环境下懒加载变慢:由于 unbundle 机制,动态加载的文件,需要做 resolve 、 load 、 transform 、 parse 操作,并且还有大量的 http 请求,导致懒加载性能也受到影响。开发环境下首屏加载变慢:由于 unbundle 机制, Vite 首屏期间需要额外做其它工作。不过首屏性能差只发生在 dev server 启动以后第一次加载页面时发生。之后再 reload 页面时,首屏性能会好很多。原因是 dev server 会将之前已经完成转换的内容缓存起来纯业务代码,一般建议采用ESM写法。
如果引入的三方组件或者三方库采用了CJS写法,vite 在预构建的时候就会将CJS模块转化为ESM模块。
如果非要在业务代码中采用CJS模块,那么我们可以提供一个 vite 插件,定义 load hook,在 hook 内部识别是 CJS 模块还是 ESM 模块。如果是 CJS 模块,利用 esbuild 的 transfrom 功能,将 CJS 模块转化为 ESM 模块。
vite使用传统的rollup进行打包,因此,vite的主要优势在开发阶段。另外,由于vite利用的是ES Module,因此在代码中不可以使用CommonJS。esbuild用在开发环境,解析依赖,编译不同格式文件成可执行esm的js,rollup用于生产环境,也需要解析编译,但是产物是兼容性更好的原生js;Rollup使用新的ESM,而Webpack用的是旧的CommonJS。Rollup支持相对路径,webpack需要使用path模块。Rollup使用起来更简洁,并且Rollup打出更小体积的文件,所以Rollup更适合Vite。生产环境使用rollup打包可能会造成开发环境与生产环境的不一致。因为打包方式不一样,生产的打包方式是rollup 打包,开发打包是直接把转化后的 es module 的JavaScript,扔给浏览器,让浏览器根据依赖关系,自己去加载依赖。
IE11等低版本的浏览器,自然是可以使用的。bundle.js与 es module 两个版本的代码,根据浏览器的实际兼容性去动态选择导入哪个模块。 resolve.alias配置通过别名来将原导⼊路径映射成⼀个新的导⼊路径。resolve.extensions在导⼊语句没带⽂件后缀时,webpack会⾃动带上后缀后,去尝试查找⽂件是否存在。
如果我们没有配置一些规则,Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign )都不会转码。
当运行环境中并没有实现的一些方法,babel-polyfill 会给其做兼容。 但是这样做也有一个缺点,就是会污染全局变量,而且项目打包以后体积会增大很多,因为把整个依赖包也搭了进去。所以并不推荐在一些方法类库中去使用。
为了不污染全局对象和内置的对象原型,但是又想体验使用新鲜语法的快感。就可以配合使用babel-runtime和babel-plugin-transform-runtime。 比如当前运行环境不支持promise,可以通过引入babel-runtime/core-js/promise来获取promise, 或者通过babel-plugin-transform-runtime自动重写你的promise。
babel-runtime 不能转码实例方法,比如这样的代码:'hello'.includes('h');,这只能通过 babel-polyfill 来转码,因为 babel-polyfill 是直接在原型链上增加方法。
babel-runtime和 babel-plugin-transform-runtime的区别是,相当一前者是手动挡而后者是自动挡,每当要转译一个api时都要手动加上require(‘babel-runtime’)
随着历史进程的发展,新一代的babel-prenset-env 很强大,了解一下?
对babel-transform-runtime,babel-polyfill的一些理解
babel-polyfill VS babel-runtime
Pinia 的核心原理是将应用的状态分解为多个独立的 store,并通过 provide / inject 机制来将它们注入到 Vue 组件中。每个 store 由一个名为 defineStore 的工厂函数创建,它接收一个名为 id 的参数,用于标识该 store,以及一个名为 state 的对象,用于定义该 store 的状态。在组件中,我们可以使用 $store 访问这些 store,并通过 computed 属性来监听它们的变化。