技术栈+主要完成的功能模块+项目技术亮点和难度+解决方案
webpack会先对整个项目文件进行打包,然后启动开发服务器,请求服务器时直接给予打包结果。 而vite是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。 由于现代浏览器本身就支持ES Module,会自动向依赖的Module发出请求。vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。 由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。 在HMR方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。 当需要打包到生产环境时,vite使用传统的rollup进行打包,因此,vite的主要优势在开发阶段。
大家熟悉的webpack在开发时需要启动本地开发服务器实时预览。因为需要对整个项目文件进行打包,开发时启动速度会随着项目规模扩大越来越缓慢。对于开发时文件修改后的热更新也存在同样的问题。Webpack 的热更新会以当前修改的文件为入口重新 build 打包,所有涉及到的依赖也都会被重新加载一次。虽然webpack 也采用的是局部热更新并且是有缓存机制的,但是还是需要重新打包所以很大的代码项目是真的有卡顿的现象。
Vite
则很好地解决了上面的两个问题。启动一台开发服务器,并不对文件代码打包,根据客户端的请求加载需要的模块处理,实现真正的按需加载。对于文件更新,Vite
的HMR是在原生 ESM 上执行的。只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使HMR更新始终快速。Vite的优势主要在开发环境下利用浏览器去解析当前请求的模块是实现快速更新。
Webpack
: 分析依赖=> 编译打包=> 交给本地服务器进行渲染。首先分析各个模块之间的依赖,然后进行打包,在启动webpack-dev-server,请求服务器时,直接显示打包结果。webpack打包之后存在的问题:随着模块的增多,会造成打出的 bundle 体积过大,进而会造成热更新速度明显拖慢。Vite
: 启动服务器=> 请求模块时按需动态编译显示。是先启动开发服务器,请求某个模块时再对该模块进行实时编译,因为现代游览器本身支持ES-Module,所以会自动向依赖的Module发出请求。所以vite就将开发环境下的模块文件作为浏览器的执行文件,而不是像webpack进行打包后交给本地服务器。热更新方面效率更高。当改动了某个模块的时候,也只用让浏览器重新请求该模块,不需要像webpack那样将模块以及模块依赖的模块全部编译一次。Webpack
通过先将整个应用打包,再将打包后代码提供给dev server
,开发者才能开始开发。Vite
直接将源码交给浏览器,实现dev server
秒开,浏览器显示页面需要相关模块时,再向dev server
发起请求,服务器简单处理后,将该模块返回给浏览器,实现真正意义的按需加载。Vite作为一个基于浏览器原生ESM的构建工具,它省略了开发环境的打包过程,利用浏览器去解析imports,在服务端按需编译返回。同时,在开发环境拥有速度快到惊人的模块更新,且热更新的速度不会随着模块增多而变慢。
Vite由两个主要部分组成:
利用浏览器原生的ES Module
编译能力,省略费时的编译环节,直给浏览器开发环境源码,dev server
只提供轻量服务。
当浏览器请求时,使用ES模块进行转换并提供一段应用程序代码。开始开发后,Vite将首先将JavaScript模块分为两类:依赖模块和应用程序模块。
关键变化是index.html
中的入口文件导入方式
,这样main.js中就可以使用ES6 Module方式组织代码。在工程中不是所有的引用模块都是ES写法,可能是CommonJS 和 UMD 、AMD 等等,这个时候Vite 会进行预构建,将其转换为ESM模块,以支持Vite。
对于JSX、或者TS等需要编译的文件,Vite是用esbuild
来进行编译的,esbuild
使用go编写,比一般node.js
编写的编译器快几个数量级。不同与Webpack的整体编译,Vite是在浏览器请求时,才对文件进行编译,然后提供给浏览器。因为esbuild编译够快,这种每次页面加载都进行编译的其实是不会影响加速速度的
Rollup
打包生产环境代码,依赖其成熟稳定的生态与更简洁的插件机制。在 Vue2 中,代码是 Options API 风格的,也就是通过填充 (option) data、methods、computed 等属性来完成一个 Vue 组件。这种风格使得 Vue 相对于 React极为容易上手,同时也造成了几个问题:
于是在 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上。Composition API
,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API
仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API
会获得更大收益。Composition API
可以和Options API
一起使用,但不建议代码示例:
vue2.0是利用object.defineProperty
,vue3.0是利用Proxy和Reflect
来实现,最大的优势就是vue3.0可以监听到数组、对象新增/删除或多层嵌套数据结构的响应。
Vue2的基于依赖收集的观测机制原理:
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 属性与副作用函数之间的响应式联系了。
reactive
和ref
,一般ref
可以用来定义基础类型,也可以是引用类型,reactive
只能用来定义引用类型。reactive({value: 原始数据})
,例如Ref(10)=>Reactive({value:10})
;ref
,什么时候用reactive
?简单说,如果你只打算修改引用类型的一个属性,那么推荐用reactive
,如果你打算变量重赋值,那么一定要用ref
。ref
定义的变量通过变量.value = xxx
改变,reactive
定义的变量通过 obj.xx = xx
即可。vue2.0
直接将数据放到了data中,通过this.xx = xx
来改变。它是 Vue3 的一个新语法糖,在 setup 函数中。所有 ES 模块导出都被认为是暴露给上下文的值,并包含在 setup() 返回对象中。相对于之前的写法,使用后,语法也变得更简单。使用方式极其简单,仅需要在 script 标签加上 setup 关键字即可。组件只需引入不用注册,属性和方法也不用返回,也不用写setup函数,也不用写export default ,甚至是自定义指令也可以在我们的template中自动获得。
defineProps
:通过defineProps指定当前 props 类型,获得上下文的props对象。可用来接收父组件传来的 propsdefineEmit
:使用defineEmit定义当前组件含有的事件,并通过返回的上下文去执行 emit。可用于子组件向父组件事件传递,在子组件中defineEmits一个函数,父组件可以触发。defineExpose
:使用defineExpose
组件暴露出自己的属性,在父组件中可以拿到。传统的写法,我们可以在父组件中,通过 ref 实例的方式去访问子组件的内容,但在 script setup 中,该方法就不能用了,setup 相当于是一个闭包,除了内部的 template模板,谁都不能访问内部的数据和方法。vue3中移除了beforeCreate 和 created,增加了setup函数。其他周期函数基本就是命名上在vue2.x的基础上加上on前缀,以驼峰命名方式命名,要写到setup函数里面。此外还增加了onRenderTracked和onRenderTriggered是用来调试的,这两个事件都带有一个DebuggerEvent,它使我们能够知道是什么导致了Vue实例中的重新渲染。
vue3.0采用函数式编程方式,打破了this的限制,能够更好的复用性,真正体现实现功能的高内聚低耦合,更利于代码的可扩展性和可维护性。
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
以 input 表单元素为例:
相当于
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
// 父组件:
// 子组件:
{{value}}
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((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();
}
});