一、项目介绍
技术栈+主要完成的功能模块+项目技术亮点和难度+解决方案
二、Vite
面试题:谈谈你对vite的理解,最好对比webpack说明
webpack会先对整个项目文件进行打包,然后启动开发服务器,请求服务器时直接给予打包结果。 而vite是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。 由于现代浏览器本身就支持ES Module,会自动向依赖的Module发出请求。vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。 由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。 在HMR方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。 当需要打包到生产环境时,vite使用传统的rollup进行打包,因此,vite的主要优势在开发阶段。
Webpack的问题
大家熟悉的webpack在开发时需要启动本地开发服务器实时预览。因为需要对整个项目文件进行打包,开发时启动速度会随着项目规模扩大越来越缓慢。对于开发时文件修改后的热更新也存在同样的问题。Webpack 的热更新会以当前修改的文件为入口重新 build 打包,所有涉及到的依赖也都会被重新加载一次。虽然webpack 也采用的是局部热更新并且是有缓存机制的,但是还是需要重新打包所以很大的代码项目是真的有卡顿的现象。
Vite另辟蹊径
Vite
则很好地解决了上面的两个问题。启动一台开发服务器,并不对文件代码打包,根据客户端的请求加载需要的模块处理,实现真正的按需加载。对于文件更新,Vite
的HMR是在原生 ESM 上执行的。只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使HMR更新始终快速。Vite的优势主要在开发环境下利用浏览器去解析当前请求的模块是实现快速更新。
Vite和Webpack对比
Webpack
: 分析依赖=> 编译打包=> 交给本地服务器进行渲染。首先分析各个模块之间的依赖,然后进行打包,在启动webpack-dev-server,请求服务器时,直接显示打包结果。webpack打包之后存在的问题:随着模块的增多,会造成打出的 bundle 体积过大,进而会造成热更新速度明显拖慢。Vite
: 启动服务器=> 请求模块时按需动态编译显示。是先启动开发服务器,请求某个模块时再对该模块进行实时编译,因为现代游览器本身支持ES-Module,所以会自动向依赖的Module发出请求。所以vite就将开发环境下的模块文件作为浏览器的执行文件,而不是像webpack进行打包后交给本地服务器。热更新方面效率更高。当改动了某个模块的时候,也只用让浏览器重新请求该模块,不需要像webpack那样将模块以及模块依赖的模块全部编译一次。Webpack
通过先将整个应用打包,再将打包后代码提供给dev server
,开发者才能开始开发。Vite
直接将源码交给浏览器,实现dev server
秒开,浏览器显示页面需要相关模块时,再向dev server
发起请求,服务器简单处理后,将该模块返回给浏览器,实现真正意义的按需加载。
什么是vite
Vite作为一个基于浏览器原生ESM的构建工具,它省略了开发环境的打包过程,利用浏览器去解析imports,在服务端按需编译返回。同时,在开发环境拥有速度快到惊人的模块更新,且热更新的速度不会随着模块增多而变慢。
Vite由两个主要部分组成:
- dev server:利用浏览器的ESM(EMSAScript Modules)能力来提供源文件,具有丰富的内置功能并具有高效的HMR(Hot Module Replacement热更新)
- 生产构建:生产环境利用Rollup来构建代码,提供指令用来优化构建过程
在开发环境下
-
利用浏览器原生的
ES Module
编译能力,省略费时的编译环节,直给浏览器开发环境源码,dev server
只提供轻量服务。 -
当浏览器请求时,使用ES模块进行转换并提供一段应用程序代码。开始开发后,Vite将首先将JavaScript模块分为两类:依赖模块和应用程序模块。
- 依赖模块是从node_modules文件夹导入的JavaScript模块。这些模块将使用esbuild进行处理和绑定,esbuild是用Go编写的JavaScript绑定器,执行速度比Webpack快10到100倍。
- 应用程序模块是为应用程序编写的模块,通常涉及特定于库的扩展,如:jsx / vue 或 scss文件。
- 虽然基于捆绑程序的工作流(如Webpack)必须在单个浏览器请求之前处理整个JavaScript模块,但Vite仅在单个浏览器请求之前处理依赖模块。
-
关键变化是
index.html
中的入口文件导入方式<script type="module" src="/src/main.js"></script>
,这样main.js中就可以使用ES6 Module方式组织代码。在工程中不是所有的引用模块都是ES写法,可能是CommonJS 和 UMD 、AMD 等等,这个时候Vite 会进行预构建,将其转换为ESM模块,以支持Vite。 -
对于JSX、或者TS等需要编译的文件,Vite是用
esbuild
来进行编译的,esbuild
使用go编写,比一般node.js
编写的编译器快几个数量级。不同与Webpack的整体编译,Vite是在浏览器请求时,才对文件进行编译,然后提供给浏览器。因为esbuild编译够快,这种每次页面加载都进行编译的其实是不会影响加速速度的
在生产环境下
- 集成
Rollup
打包生产环境代码,依赖其成熟稳定的生态与更简洁的插件机制。
为什么生产环境仍需打包,为什么不直接将 entry.js 文件使用
- 尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)
- 从 entry.js 到所有依赖的模块代码,全部采用 ES Module 方案实现,我们的依赖管理是采用 npm 的,而 npm 包大部分是采用 CommonJS 标准而未兼容 ES 标准的。
三、Vue2和Vue3的区别
1.Composition API
在 Vue2 中,代码是 Options API 风格的,也就是通过填充 (option) data、methods、computed 等属性来完成一个 Vue 组件。这种风格使得 Vue 相对于 React极为容易上手,同时也造成了几个问题:
- 由于 Options API 不够灵活的开发方式,使得Vue开发缺乏优雅的方法来在组件间共用代码。
- Vue 组件过于依赖this上下文,Vue 背后的一些小技巧使得 Vue 组件的开发看起来与 JavaScript 的开发原则相悖,比如在methods 中的this竟然指向组件实例来不指向methods所在的对象。这也使得 TypeScript 在Vue2 中很不好用。
于是在 Vue3 中,舍弃了 Options API,转而投向 Composition API。Composition API本质上是将 Options API 背后的机制暴露给用户直接使用,这样用户就拥有了更多的灵活性,也使得 Vue3 更适合于 TypeScript 结合。
Composition API和Options API主要区别
Composition API
是一组API,包括:Reactivity API、生命周期钩子、依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API
则通过声明组件选项的对象形式编写组件。Composition API
最主要作用是能够简洁、高效复用逻辑。解决了过去Options API
中mixins
的各种缺点;另外Composition API
具有更加敏捷的代码组织能力,很多用户喜欢Options API
,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API
则可以将它们有效组织在一起。最后Composition API
拥有更好的类型推断,对ts支持更友好,Options API
在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API
时获得类型推断,然而还是没办法用在mixins和provide/inject上。- Vue3首推
Composition API
,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API
仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API
会获得更大收益。 Composition API
可以和Options API
一起使用,但不建议
代码示例:
<template>
<button @click="increment">
Count: {{ count }}
</button>
</template>
<script>
// Composition API 将组件属性暴露为函数,因此第一步是导入所需的函数
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// 使用 ref 函数声明了称为 count 的响应属性,对应于Vue2中的data函数
const count = ref(0)
// Vue2中需要在methods option中声明的函数,现在直接声明
function increment() {
count.value++
}
// 对应于Vue2中的mounted声明周期
onMounted(() => console.log('component mounted!'))
return {
count,
increment
}
}
}
</script>
2.监测机制改变
vue2.0是利用object.defineProperty
,vue3.0是利用Proxy和Reflect
来实现,最大的优势就是vue3.0可以监听到数组、对象新增/删除或多层嵌套数据结构的响应。
Vue2的基于依赖收集的观测机制原理:
- 将原生的数据改造成 “可观察对象”,通常为,调用defineProperty改变data对象中数据为存储器属性。一个可观察对象可以被取值getter,也可以被赋值setter。
- 在解析模板,也就是在watcher的求值过程中,每一个被取值的可观察对象都会将当前的watcher注册为自己的一个订阅者,并成为当前watcher的一个依赖。
- 当一个被依赖的可观察对象被赋值时,它会通知notify所有订阅自己的watcher重新求值,并触发相应的更新,即watcher对象中关联的DOM改变渲染。
Vue2 和 Vue3 的响应式实现原理
Vue2
Vue2 是通过 Object.defineProperty 将对象的属性转换成 getter/setter 的形式来进行监听它们的变化,当读取属性值的时候会触发 getter 进行依赖收集,当设置对象属性值的时候会触发 setter 进行向相关依赖发送通知,从而进行相关操作。
由于 Object.defineProperty 只对属性 key 进行监听,无法对引用对象进行监听,所以在 Vue2 中创建一个了 Observer 类对整个对象的依赖进行管理,当对响应式对象进行新增或者删除则由响应式对象中的 dep 通知相关依赖进行更新操作。
Object.defineProperty 也可以实现对数组的监听的,但因为性能的原因 Vue2 放弃了这种方案,改由重写数组原型对象上的 7 个能操作数组内容的变更的方法,从而实现对数组的响应式监听。
Vue3
Vue3 则是通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据,然后在副作用函数中读取响应式数据的时候,就会触发 Proxy 的 getter,在 getter 里面把对当前的副作用函数保存起来,将来对应响应式数据发生更改的话,则把之前保存起来的副作用函数取出来执行。
Vue3 对数组实现代理时,用于代理普通对象的大部分代码可以继续使用,但由于对数组的操作与对普通对象的操作存在很多的不同,那么也需要对这些不同的操作实现正确的响应式联系或触发响应。这就需要对数组原型上的一些方法进行重写。
比如通过索引为数组设置新的元素,可能会隐式地修改数组的 length 属性的值。同时如果修改数组的 length 属性的值,也可能会间接影响数组中的已有元素。另外用户通过 includes、indexOf 以及 lastIndexOf 等对数组元素进行查找时,可能是使用代理对象进行查找,也有可能使用原始值进行查找,所以我们就需要重写这些数组的查找方法,从而实现用户的需求。原理很简单,当用户使用这些方法查找元素时,先去响应式对象中查找,如果没找到,则再去原始值中查找。
另外如果使用 push、pop、shift、unshift、splice 这些方法操作响应式数组对象时会间接读取和设置数组的 length 属性,所以我们也需要对这些数组的原型方法进行重新,让当使用这些方法间接读取 length 属性时禁止进行依赖追踪,这样就可以断开 length 属性与副作用函数之间的响应式联系了。
3.数据响应方式
- vue3.0提供了
reactive
和ref
,一般ref
可以用来定义基础类型,也可以是引用类型,reactive
只能用来定义引用类型。 - ref的本质就是
reactive({value: 原始数据})
,例如Ref(10)=>Reactive({value:10})
; - 对于引用类型,什么时候用
ref
,什么时候用reactive
?简单说,如果你只打算修改引用类型的一个属性,那么推荐用reactive
,如果你打算变量重赋值,那么一定要用ref
。 ref
定义的变量通过变量.value = xxx
改变,reactive
定义的变量通过obj.xx = xx
即可。vue2.0
直接将数据放到了data中,通过this.xx = xx
来改变。
4.script setup语法糖
它是 Vue3 的一个新语法糖,在 setup 函数中。所有 ES 模块导出都被认为是暴露给上下文的值,并包含在 setup() 返回对象中。相对于之前的写法,使用后,语法也变得更简单。使用方式极其简单,仅需要在 script 标签加上 setup 关键字即可。组件只需引入不用注册,属性和方法也不用返回,也不用写setup函数,也不用写export default ,甚至是自定义指令也可以在我们的template中自动获得。
- 组件自动注册:在 script setup 中,引入的组件可以直接使用,无需再通过components进行注册,并且无法指定当前组件的名字,它会自动以文件名为主,也就是不用再写name属性了。
- 组件核心 API 的使用:
defineProps
:通过defineProps指定当前 props 类型,获得上下文的props对象。可用来接收父组件传来的 propsdefineEmit
:使用defineEmit定义当前组件含有的事件,并通过返回的上下文去执行 emit。可用于子组件向父组件事件传递,在子组件中defineEmits一个函数,父组件可以触发。defineExpose
:使用defineExpose
组件暴露出自己的属性,在父组件中可以拿到。传统的写法,我们可以在父组件中,通过 ref 实例的方式去访问子组件的内容,但在 script setup 中,该方法就不能用了,setup 相当于是一个闭包,除了内部的 template模板,谁都不能访问内部的数据和方法。
5. 生命周期函数
vue3中移除了beforeCreate 和 created,增加了setup函数。其他周期函数基本就是命名上在vue2.x的基础上加上on前缀,以驼峰命名方式命名,要写到setup函数里面。此外还增加了onRenderTracked和onRenderTriggered是用来调试的,这两个事件都带有一个DebuggerEvent,它使我们能够知道是什么导致了Vue实例中的重新渲染。
vue3.0采用函数式编程方式,打破了this的限制,能够更好的复用性,真正体现实现功能的高内聚低耦合,更利于代码的可扩展性和可维护性。
四、v-model 的原理
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
// 父组件:
<ModelChild v-model="message"></ModelChild>
// 子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},
五、角色权限管理:页面权限控制-路由动态注册
角色:用户端和管理员端,可能还会有超级管理员
不同角色所看到的路由界面是不同的,最优方案是放到后端配置角色列表,因为前端配置如果项目上线后需要增加角色不够灵活。后端配置,用户一旦登录后,后端接口直接返回该账号拥有的权限列表就行了,至于该账户属于什么角色以及角色拥有的页面权限合理方案应是后端处理。
如后端返回的账户信息结构如下:
{
user_id:1,
user_name:"刘某",
permission_list:["List","Detail","Manage"]
}
前端此时是不用在意改账户拥有哪些角色的,只需要把他拥有权限的页面给予展示就可以。
(1)将路由分成两部分:静态路由routes和动态路由dynamic_routes。静态路由里面的页面是所有角色都能访问的(登录页和主页),它里面主要区分登录访问和非登录访问。动态路由里面存放的是与角色定制化相关的的页面例如一些列表页,详情页和管理页等等
(2)先从vuex里面拿到当前用户的权限列表,然后遍历动态路由数组dynamic_routes,从里面过滤出允许访问的路由,最后将这些路由动态添加到路由实例里。
(3)这样就实现了用户只能按照他对应的权限列表里的规则访问到相应的页面,至于那些他无权访问的页面,路由实例根本没有添加相应的路由信息,因此即使用户在浏览器强行输入路径越权访问也是访问不到的。
(4)动态添加路由这部分代码最好单独封装起来,因为用户登录和刷新页面时都需要调用。
这种方式需要维护 dynamic_routes,当每次新增动态路由页面时,dynamic_routes 数组都需要新增,并且还需要保持和后端 permission_list 返回的数组里面的 name一致(若不一致则需要建立一个名称映射表)。此外,这种方法对于没有权限的路由来说,页面是被添加到 router 里面去的,当访问时则需要调转到 404 默认页面。
import store from "@/store";
export const invisible = [...]; //静态路由
export const dynamic_routes = [...]; //动态路由
const router = createRouter({ //创建路由对象
history: createWebHashHistory(),
routes,
});
//动态添加路由
if(store.state.user != null){ //从vuex中拿到用户信息
//用户已经登录
const { permission_list } = store.state.user; // 从用户信息中获取权限列表
const allow_routes = dynamic_routes.filter((route)=>{ //过滤允许访问的路由
return permission_list.includes(route.name);
})
allow_routes.forEach((route)=>{ // 将允许访问的路由动态添加到路由栈中
router.addRoute(route);
})
}
export default router;
六、登录权限控制-路由元信息的应用
登录权限控制,简而言之就是实现哪些页面能被未登录的用户访问,哪些页面只有用户登录后才能被访问。
现有三个页面:登录页、注册页和列表页。登录页和注册页所有人都可以访问,但列表页面需要登录后才能看到,给该路由添加一个meta对象,并将need_login置为true。
- 在代码层面,通过router.beforeEach可以轻松实现上述目标,每次页面跳转时都会调用router.beforeEach包裹的函数。
- to是要即将访问的路由信息,从其中拿到need_login的值可以判断是否需要登录。再从vuex中拿到用户的登录信息。
- 如果用户没有登录并且要访问的页面又需要登录时就使用next跳转到登录页面,并将需要访问的页面路由名称通过redirect_page传递过去,在登录页面就可以拿到redirect_page等登录成功后直接跳转。
router.beforeEach((to, from, next) => {
const { need_login = false } = to.meta;
const { user_info } = store.state; //从vuex中获取用户的登录信息
if (need_login && !user_info) {
// 如果页面需要登录但用户没有登录跳到登录页面
const next_page = to.name; // 配置路由时,每一条路由都要给name赋值
next({
name: 'Login',
params: {
redirect_page: next_page,
...from.params, //如果跳转需要携带参数就把参数也传递过去
},
});
} else {
//不需要登录直接放行
next();
}
});