赞
踩
缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3中keep-alive有比较大的更新,能说的点比较多。
开发中缓存组件使用keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
<keep-alive>
<component :is="view"></component>
</keep-alive>
结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive
包裹router-view
,现在需要反过来用router-view
包裹keep-alive
:
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
缓存后如果要获取数据,解决方案可以有以下两种:
beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
actived:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
activated(){
this.getData() // 获取数据
},
测试缓存特性,test-v3.html
问我们template到render过程,其实是问vue编译器
工作原理。
vue3编译过程窥探:
https://github1s.com/vuejs/core/blob/HEAD/packages/compiler-core/src/compile.ts#L61-L62
测试,test-v3.html
还是问新特性,陈述典型新特性,分析其给你带来的变化即可。
从以下几方面分门别类阐述:易用性、性能、扩展性、可维护性、开发体验等
v-model
在Vue3中变成了Vue2中v-model
和sync
修饰符的结合体,用户不用区分两者不同,也不用选择困难。类似的简化还有用于渲染函数内部生成VNode的h(type, props, children)
,其中props
不用考虑区分属性、特性、事件等,框架替我们判断,易用性大增。Teleport
传送门、Fragments
、Suspense
等都会简化特定场景的代码编写,SFC Composition API
语法糖更是极大提升我们开发体验。reactivity
模块,custom renderer
API等Composition API
,更容易编写高复用性的业务逻辑。还有对TypeScript支持的提升。Proxy
的响应式系统Proxy
和defineProperty
有什么不同?这是一道综合实践题目,写过一定数量的代码之后小伙伴们自然会开始关注一些优化方法,答得越多肯定实践经验也越丰富,是很好的题目。
根据题目描述,这里主要探讨Vue代码层面的优化
我这里主要从Vue代码编写层面说一些优化手段,例如:代码分割、服务端渲染、组件缓存、长列表优化等
最常见的路由懒加载:有效拆分App尺寸,访问时才异步加载
const router = createRouter({
routes: [
// 借助webpack的import()实现异步组件
{ path: '/foo', component: () => import('./Foo.vue') }
]
})
keep-alive
缓存页面:避免重复创建组件实例,且能保留缓存组件状态
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
使用v-show
复用DOM:避免重复创建组件
<template>
<div class="cell">
<!-- 这种情况用v-show复用DOM,比v-if效果好 -->
<div v-show="value" class="on">
<Heavy :n="10000"/>
</div>
<section v-show="!value" class="off">
<Heavy :n="10000"/>
</section>
</div>
</template>
v-for
遍历避免同时使用 v-if
:实际上在Vue3中已经是个错误写法
<template>
<ul>
<li
v-for="user in activeUsers"
<!-- 避免同时使用,vue3中会报错 -->
<!-- v-if="user.isActive" -->
:key="user.id">
{{ user.name }}
</li>
</ul>
</template>
<script>
export default {
computed: {
activeUsers: function () {
return this.users.filter(user => user.isActive)
}
}
}
</script>
v-once和v-memo:不再变化的数据使用v-once
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
按条件跳过更新时使用v-momo
:下面这个列表只会更新选中状态变化项
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
<p>...more child nodes</p>
</div>
https://vuejs.org/api/built-in-directives.html#v-memo
长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
<recycle-scroller
class="items"
:items="items"
:item-size="24"
>
<template v-slot="{ item }">
<FetchItemView
:item="item"
@vote="voteItem(item)"
/>
</template>
</recycle-scroller>
事件的销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。
export default {
created() {
this.timer = setInterval(this.refresh, 2000)
},
beforeUnmount() {
clearInterval(this.timer)
}
}
图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。
<img v-lazy="/static/img/1.png">
参考项目:vue-lazyload
第三方插件按需引入
像element-plus
这样的第三方组件库可以按需引入避免体积太大。
import { createApp } from 'vue';
import { Button, Select } from 'element-plus';
const app = createApp()
app.use(Button)
app.use(Select)
子组件分割策略:较重的状态组件适合拆分
<template>
<div>
<ChildComp/>
</div>
</template>
<script>
export default {
components: {
ChildComp: {
methods: {
heavy () { /* 耗时任务 */ }
},
render (h) {
return h('div', this.heavy())
}
}
}
}
</script>
但同时也不宜过度拆分组件,尤其是为了所谓组件抽象将一些不需要渲染的组件特意抽出来,组件实例消耗远大于纯dom节点。参考:https://vuejs.org/guide/best-practices/performance.html#avoid-unnecessary-component-abstractions
服务端渲染/静态网站生成:SSR/SSG
如果SPA应用有首屏渲染慢的问题,可以考虑SSR、SSG方案优化。参考SSR Guide
这题现在有些落伍,vue3
已经不用一个根了。因此这题目很有说头!
vue2直接报错,test-v2.html
new Vue({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).$mount('#app')
vue3中没有问题,test-v3.html
Vue.createApp({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).mount('#app')
vue3
解决方法原理vue2
中组件确实只能有一个根,但vue3
中组件已经可以多根节点了。vdom
是一颗单根树形结构,patch
方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
,自然应该满足这个要求。vue3
中之所以可以写多个根节点,是因为引入了Fragment
的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新。这是基本应用能力考察,稍微上点规模的项目都要拆分vuex模块便于维护。
https://vuex.vuejs.org/zh/guide/modules.html
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action
modules
选项组织起来:createStore({modules:{...}})
store.state.a.xxx
,但同时getters
、mutations
和actions
又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace
选项,此时再访问它们就要加上命名空间前缀。这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效。
// 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import('./views/UserDetails')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
参考https://router.vuejs.org/zh/guide/advanced/lazy-loading.html
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。
一般来说,对所有的路由都使用动态导入是个好主意。
给component
选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:
{ path: '/users/:id', component: () => import('./views/UserDetails') }
结合注释() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
可以做webpack代码分块
vite中结合rollupOptions定义分块
路由中不能使用异步组件
这是Vue3
数据响应式中非常重要的两个概念,自然的,跟我们写代码关系也很大。
ref:https://vuejs.org/api/reactivity-core.html#ref
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
reactive:https://vuejs.org/api/reactivity-core.html#reactive
const obj = reactive({ count: 0 })
obj.count++
ref
接收内部值(inner value)返回响应式Ref
对象,reactive
返回响应式代理对象ref
通常用于处理单值的响应式,reactive
用于处理对象类型的数据响应式ref
主要解决原始值的响应式问题.value
才能访问其值,在视图中使用会自动脱ref,不需要.value
;ref可以接收对象或数组等非原始值,但内部依然是reactive
实现响应式;reactive内部如果接收Ref对象会自动脱ref;使用展开运算符(…)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。我们经常性需要侦测响应式数据的变化,vue3中除了watch之外又出现了watchEffect,不少同学会混淆这两个api。
watchEffect
立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。
Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
count.value++
// -> logs 1
watch
侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
Watches one or more reactive data sources and invokes a callback function when the sources change.
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
watchEffect
立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch
侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
watchEffect(effect)
是一种特殊watch
,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect
就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch。
watchEffect
在使用时,传入的函数会立刻执行一次。watch
默认情况下并不会执行回调函数,除非我们手动设置immediate
选项。
从实现上来说,watchEffect(fn)
相当于watch(fn,fn,{immediate:true})
我们现在编写的Vue、React和Angular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别。
这是一道API题,我们可能写的自定义指令少,但是我们用的多呀,多举几个例子就行。
定义一个包含类似组件生命周期钩子的对象,钩子函数会接收指令挂钩的dom元素:
const focus = {
mounted: (el) => el.focus()
}
export default {
directives: {
// enables v-focus in template
focus
}
}
<input v-focus />
<input v-focus />
v-mode
l或v-for
,同时Vue也允许用户注册自定义指令来扩展Vue能力API考察,但$attrs和 l i s t e n e r s 是比较少用的边界知识,而且 v u e 3 有变化, listeners是比较少用的边界知识,而且vue3有变化, listeners是比较少用的边界知识,而且vue3有变化,listeners已经移除,还是有细节可说的。
一个包含组件透传属性的对象。
An object that contains the component’s fallthrough attributes.
<template>
<child-component v-bind="$attrs">
将非属性特性透传给内部的子组件
</child-component>
</template>
查看透传属性foo和普通属性bar,发现vnode结构完全相同,这说明vue3中将分辨两者工作由框架完成而非用户指定:
<template>
<h1>{{ msg }}</h1>
<comp foo="foo" bar="bar" />
</template>
<template>
<div>
{{$attrs.foo}} {{bar}}
</div>
</template>
<script setup>
defineProps({
bar: String
})
</script>
_createVNode(Comp, {
foo: "foo",
bar: "bar"
})
v-once
是Vue中内置指令,很有用的API,在优化方面经常会用到,不过小伙伴们平时可能容易忽略它。
仅渲染元素和组件一次,并且跳过未来更新
Render the element and component once only, and skip future updates.
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
v-once
是什么v-memo
v-once
是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。v-once
,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段。v-memo
指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。下面例子使用了v-once:
<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1 v-once>{{ msg }}</h1>
<input v-model="msg">
</template>
我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:
// ...
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
// 从缓存获取vnode
_cache[0] || (
_setBlockTracking(-1),
_cache[0] = _createElementVNode("h1", null, [
_createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
]),
_setBlockTracking(1),
_cache[0]
),
// ...
递归组件我们用的比较少,但是在Tree、Menu这类组件中会被用到。
组件通过组件名称引用它自己,这种情况就是递归组件。
An SFC can implicitly refer to itself via its filename.
<template>
<li>
<div> {{ model.name }}</div>
<ul v-show="isOpen" v-if="isFolder">
<!-- 注意这里:组件递归渲染了它自己 -->
<TreeItem
class="item"
v-for="model in model.children"
:model="model">
</TreeItem>
</ul>
</li>
<script>
export default {
name: 'TreeItem',
// ...
}
</script>
name
属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。resolveComponent
,这样实际获取的组件就是当前组件本身。递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
就是在传递maybeSelfReference
export function resolveComponent(
name: string,
maybeSelfReference?: boolean
): ConcreteComponent | string {
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}
resolveAsset中最终返回的是组件自身:
if (!res && maybeSelfReference) {
// fallback to implicit self-reference
return Component
}
因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。
大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
In large applications, we may need to divide the app into smaller chunks and only load a component from the server when it’s needed.
import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定义异步组件
const AsyncComp = defineAsyncComponent(() => {
// 加载函数返回Promise
return new Promise((resolve, reject) => {
// ...可以从服务器加载组件
resolve(/* loaded component */)
})
})
// 借助打包工具实现ES模块动态导入
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiAsyncComponent.ts#L43-L44
这是一个综合应用题目,在项目中我们常常需要将App的异常上报,此时错误处理就很重要了。
这里要区分错误的类型,针对性做收集。
然后是将收集的的错误信息上报服务器。
接口异常
"和“代码逻辑异常
”接口异常
是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但是返回的是错误状态。以Axios为例,这类异常我们可以通过封装Axios,在拦截器中统一处理整个应用中请求的错误。代码逻辑异常
是我们编写的前端代码中存在逻辑上的错误造成的异常,vue应用中最常见的方式是使用全局错误处理函数app.config.errorHandler
收集错误。axios拦截器中处理捕获异常:
// 响应拦截器
instance.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
// 存在response说明服务器有响应
if (error.response) {
let response = error.response;
if (response.status >= 400) {
handleError(response);
}
} else {
handleError(null);
}
return Promise.reject(error);
},
);
vue中全局捕获异常:
import { createApp } from 'vue'
const app = createApp(...)
app.config.errorHandler = (err, instance, info) => {
// report error to tracking services
}
处理接口请求错误:
function handleError(error, type) {
if(type == 1) {
// 接口错误,从config字段中获取请求信息
let { url, method, params, data } = error.config
let err_data = {
url, method,
params: { query: params, body: data },
error: error.data?.message || JSON.stringify(error.data),
})
}
}
处理前端逻辑错误:
function handleError(error, type) {
if(type == 2) {
let errData = null
// 逻辑错误
if(error instanceof Error) {
let { name, message } = error
errData = {
type: name,
error: message
}
} else {
errData = {
type: 'other',
error: JSON.strigify(error)
}
}
}
}
这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题。
参考文档:https://staging.vuejs.org/guide/components/props.html#one-way-data-flow
综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。
权限管理一般需求是页面权限和按钮权限的管理
具体实现的时候分后端和前端两种方案:
前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个asyncRoutes
数组,需要认证的页面在其路由的meta
中添加一个roles
字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)
方式动态添加路由即可。
后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes
动态添加路由信息
按钮权限的控制通常会实现一个指令,例如v-permission
,将按钮要求角色通过值传给v-permission指令,在指令的moutned
钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。
纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
类似Tabs
这类组件能不能使用v-permission
指令实现按钮权限控制?
<el-tabs>
<el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane>
<el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane>
</el-tabs>
服务端返回的路由信息如何添加到路由器中?
// 前端组件名和组件映射表
const map = {
//xx: require('@/views/xx.vue').default // 同步的⽅式
xx: () => import('@/views/xx.vue') // 异步的⽅式
}
// 服务端返回的asyncRoutes
const asyncRoutes = [
{ path: '/xx', component: 'xx',... }
]
// 遍历asyncRoutes,将component替换为map[component]
function mapComponent(asyncRoutes) {
asyncRoutes.forEach(route => {
route.component = map[route.component];
if(route.children) {
route.children.map(child => mapComponent(child))
}
})
}
mapComponent(asyncRoutes)
综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可。
从0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
目前vue3项目我会用vite或者create-vue创建项目
接下来引入必要插件:路由插件vue-router、状态管理vuex/pinia、ui库我比较喜欢element-plus和antd-vue、http工具我会选axios
其他比较常用的库有vueuse,nprogress,图标可以使用vite-svg-loader
下面是代码规范:结合prettier和eslint即可
最后是提交规范,可以使用husky,lint-staged,commitlint
目录结构我有如下习惯:
.vscode
:用来放项目中的 vscode 配置
plugins
:用来放 vite 插件的 plugin 配置
public
:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下
src
:用来放项目代码文件
api
:用来放http的一些接口配置
assets
:用来放一些 CSS 之类的静态资源
components
:用来放项目通用组件
layout
:用来放项目的布局
router
:用来放项目的路由配置
store
:用来放状态管理Pinia的配置
utils
:用来放项目中的工具方法类
views
:用来放项目的页面文件
查看vue官方文档:
风格指南:https://vuejs.org/style-guide/
性能:https://vuejs.org/guide/best-practices/performance.html#overview
安全:https://vuejs.org/guide/best-practices/security.html
访问性:https://vuejs.org/guide/best-practices/accessibility.html
发布:https://vuejs.org/guide/best-practices/production-deployment.html
我从编码风格、性能、安全等方面说几条:
template: <div> + userProvidedString + </div>
挂载过程完成了最重要的两件事:
把这两件事说清楚即可!
这是一道工具类的原理题目,相当有深度,具有不错的人才区分度。
使用官方提供的SFC playground可以很好的体验vue-loader。
有了vue-loader加持,我们才可以以SFC的方式快速编写代码。
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!',
}
},
}
</script>
<style>
.example {
color: red;
}
</style>
vue-loader
是什么东东vue-loader
是做什么用的vue-loader
何时生效vue-loader
如何工作vue-loader
是用于处理单文件组件(SFC,Single-File Component)的webpack loadervue-loader
,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为、vue-loader
会调用@vue/compiler-sfc
模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面:// source.vue被vue-loader处理之后返回的代码
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
script.render = render
export default script
<script lang="ts">
被作为ts处理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script
的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC的import script from 'source.vue?vue&type=script'
将被展开为:
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,下面的代码:
<style scoped lang="scss">
vue-loader将会返回给我们下面请求:
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
然后webpack会展开如下:
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
<script>
块,处理到这就可以了,但是<template>
和 <style>
还有一些额外任务要做,比如:<style scoped>
中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:
// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'
// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
这是一道工具类的原理题目,相当有深度,具有不错的人才区分度。
使用官方提供的SFC playground可以很好的体验vue-loader。
有了vue-loader加持,我们才可以以SFC的方式快速编写代码。
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!',
}
},
}
</script>
<style>
.example {
color: red;
}
</style>
vue-loader
是什么东东vue-loader
是做什么用的vue-loader
何时生效vue-loader
如何工作vue-loader
是用于处理单文件组件(SFC,Single-File Component)的webpack loadervue-loader
,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为、vue-loader
会调用@vue/compiler-sfc
模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面:// source.vue被vue-loader处理之后返回的代码
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
script.render = render
export default script
<script lang="ts">
被作为ts处理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script
的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC的import script from 'source.vue?vue&type=script'
将被展开为:
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,下面的代码:
<style scoped lang="scss">
vue-loader将会返回给我们下面请求:
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'
然后webpack会展开如下:
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
<script>
块,处理到这就可以了,但是<template>
和 <style>
还有一些额外任务要做,比如:<style scoped>
中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:
// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'
// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
API题目,考查基础能力,不容有失,尽可能说的详细。
User
组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User }
,其中:id
就是路径参数:
表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params
的形式暴露出来。/users/:username/posts/:postId
;除了 $route.params
之外,$route
对象还公开了其他有用的信息,如 $route.query
、$route.hash
等。https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E5%93%8D%E5%BA%94%E8%B7%AF%E7%94%B1%E5%8F%82%E6%95%B0%E7%9A%84%E5%8F%98%E5%8C%96
https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E8%B7%AF%E7%94%B1%E6%88%96-404-not-found-%E8%B7%AF%E7%94%B1
这个题目很有难度,首先思考vuex
解决的问题:存储用户全局状态并提供管理状态API。
vuex
需求分析官方说vuex
是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
:
Store
存储全局状态commit(type, payload)
, dispatch(type, payload)
实现Store
时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commit和dispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件。
commit(type, payload)
方法中可以获取用户传入mutations
并执行它,这样可以按用户提供的方法修改状态。 dispatch(type, payload)
类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果。
Store的实现:
class Store {
constructor(options) {
this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {
this.options.mutations[type].call(this, this.state, payload)
}
}
Vuex中Store的实现:
https://github1s.com/vuejs/vuex/blob/HEAD/src/store.js#L19-L20
mutations
和actions
是vuex
带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。
我们只需记住修改状态只能是mutations
,actions
只能通过提交mutation
修改状态即可。
看下面例子可知,Action
类似于 mutation
,不同在于:
Action
提交的是 mutation
,而不是直接变更状态。Action
可以包含任意异步操作。const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
mutation
,mutation
非常类似于事件:每个 mutation
都有一个字符串的类型 (type)**和一个**回调函数 (handler)。Action
类似于 mutation
,不同在于:Action
可以包含任意异步操作,但它不能修改状态, 需要提交mutation
才能变更状态。{commit, dispatch, state}
,从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果。options.mutations[type](state)
;dispatch(type)
方法相当于调用options.actions[type](store)
,这样就很容易理解两者使用上的不同了。我们可以像下面这样简单实现commit
和dispatch
,从而辨别两者不同:
class Store {
constructor(options) {
this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {
// 传入上下文和参数1都是state对象
this.options.mutations[type].call(this.state, this.state, payload)
}
dispatch(type, payload) {
// 传入上下文和参数1都是store本身
this.options.actions[type].call(this, this, payload)
}
}
企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。
v-once
方式只渲染一次v-for
使用,避免数据变化时不必要的VNode创建vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。
既然状态都是响应式的,那自然可以watch
,另外vuex也提供了订阅的API:store.subscribe()
。
总述知道的方法
分别阐述用法
选择和场景
我知道几种方法:
watch选项方式,可以以字符串形式监听$store.state.xx
;subscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。
watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中。
watch方式
const app = createApp({
watch: {
'$store.state.counter'() {
console.log('counter change!');
}
}
})
subscribe方式:
store.subscribe((mutation, state) => {
if (mutation.type === 'add') {
console.log('counter change in subscribe()!');
}
})
vue-router中两个重要组件router-link
和router-view
,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度哪!
router-link
和router-view
,分别起到路由导航作用和组件内容渲染作用Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。