Vue 魔法师 —— 重构“布局”

简介: “你认为你目前所在 Vue 项目中的 layouts 设置有什么问题吗?”

image.png

正视布局



开篇想先问你一个问题:

“你认为你目前所在 Vue 项目中的 layouts 设置有什么问题吗?”

image.png


你可能会回答:

“没有啥问题啊。因为不就是简单的在外层套一个 Layout 组件吗?”


我想一定类似这样吧:

<template>
  <MyLayout>
    <h1>Here is my page content</h1>
  </MyLayout>
</template>
<script>
import MyLayout from '@/layouts/MyLayout.vue'
export default {
  name: 'MyPage',
  components: { MyLayout }
}
</script>


或者这样,以本瓜所在项目举例,Layout 设置:

  • 如果你用过 vue-element-admin 一定很熟悉这样的路由设置(业务组件是 Layout 组件的子组件)
const AdminLayout = () => import('@/views/admin/homepage/layout.vue')
const OrgList = () => import('@/views/admin/admOrg/orgList.vue')
const OrgDetail = () => import('@/views/admin/admOrg/orgDetail.vue')
export const admOrgRouters = {
  path: '/orgManage',
  component: AdminLayout,
  redirect: '/orgList',
  name: '组织管理',
  iconClass: 'orgManage',
  children: [
    orgList,
    orgDetail,
  ],
  meta: {
    roles: ['isAdmin', 'ordinaryAdmin'],
    title: '组织管理'
  }
}


nice!如果你的项目也类似这样的话,就可以继续看后文了~(不然就点赞👍再关闭❎退出啦😄)


发现痛点



这样在外层包裹设置 Layout 似乎也没什么不对,但是我们细想一下:

  1. 我们需要在不同的页面 import Layout,而“重复引入”永远是程序员最讨厌的事之一;
  2. 我们必须使 Layout 包裹着我们的内容,这多少会有限制;
  3. 这样做使我们的代码更重并且迫使组件担负起渲染布局的责任(组件和布局没有拆分的够开);


虽然这些其实也并不是一些什么大不了的点,但是由于受到 NuxtJS 的启发,所以咱们决定进行 breaking change,改变这一现状。


那 NuxtJS 究竟启发了些什么呢?简而言之,即:

你可以定义【布局】为组件的一个【属性】,而不是设置一个个布局父组件到你的应用中。


附:nuxtjs-layouts

让我们看看在我们的 Vue 项目中如何实现这一启发?


项目准备



我们依然用 Vue CLI 来快速构建我们的项目:


vue create vue-layouts


你可以选择 Vue2+ 或 Vue3+,本篇都会作相应介绍。

我们清除一些初始化带来的不必要的文件,比如 HelloWorld.vue,然后新建新文件,得此目录:


--src
----views
------About.vue
------Contacts.vue
------Home.vue
----App.vue
----main.js
----router.js


然后在 App.vue 中代码如下:

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/contacts">Contacts</router-link>
    </div>
    <router-view/>
  </div>
</template>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
#nav {
  padding: 30px;
}
#nav a {
  font-weight: bold;
  color: #2c3e50;
}
#nav a.router-link-exact-active {
  color: #42b983;
}
</style>


Home.vue 代码如下:


<template>
  <div>
    <h1>This is a home page</h1>
  </div>
</template>
<script>
export default {
  name: 'Home'
}
</script>


About.vue 代码如下:


<template>
  <div>
    <h1>This is an about page</h1>
  </div>
</template>
<script>
export default {
  name: 'About'
}
</script>


Contacts.vue 代码如下:


<template>
  <div>
    <h1>This is a contacts page</h1>
  </div>
</template>
<script>
export default {
  name: "Contacts"
}
</script>


router.js 代码如下:


import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue')
  },
  {
    path: '/contacts',
    name: 'Contacts',
    component: () => import('@/views/Contacts.vue')
  }
]
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
export default router


再在 main.js 调用 router


import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
  render: h => h(App),
  router: router
}).$mount('#app')


好了,准备工作已经做好了。我们可以得到这样的一个界面:


image.png


就是三个简单的路由跳转,组件切换。到这里肯定没啥问题~


构造布局



重点来辣!

我们新建一个 layouts/AppLayout.vue 组件。


  • Vue2
<template>
  <component :is="layout">
    <slot />
  </component>
</template>
<script>
const defaultLayout = 'AppLayoutDefault'
export default {
  name: "AppLayout",
  computed: {
    layout() {
      const layout = this.$route.meta.layout || defaultLayout
      return () => import(`@/layouts/${layout}.vue`)
    }
  }
}
</script


这一段代码看似简单,却是我们新布局系统的核心


在这个模板中,我们加入了动态组件 ,并且为之加入了名为 “layout” 的计算属性。

在计算属性中我们可以看到它会【根据路由】返回【对应的布局组件】并加载到【动态组件】中去,否则就启用默认布局 —— defaultLayout。


  • 在 Vue3 中代码如下:
<template>
  <component :is="layout">
    <slot />
  </component>
</template>
<script>
import AppLayoutDefault from './AppLayoutDefault'
export default {
  name: "AppLayout",
  data: () => ({
    layout: AppLayoutDefault
  }),
  watch: {
    $route: {
      immediate: true,
      async handler(route) {
        try {
          const component = await import(`@/layouts/${route.meta.layout}.vue`)
          this.layout = component?.default || AppLayoutDefault
        } catch (e) {
          this.layout = AppLayoutDefault
        }
      }
    }
  }
}
</script>


Vue 3 Composition API 的实现:

<template>
  <component :is="layout">
    <slot />
  </component>
</template>
<script>
import AppLayoutDefault from './AppLayoutDefault'
import { markRaw, watch } from 'vue'
import { useRoute } from 'vue-router'
export default {
  name: 'AppLayout',
  setup() {
    const layout = markRaw(AppLayoutDefault)
    const route = useRoute()
    watch(
      () => route.meta,
      async meta => {
        try {
          const component = await import(`@/layouts/${meta.layout}.vue`)
          layout.value = component?.default || AppLayoutDefault
        } catch (e) {
          layout.value = AppLayoutDefault
        }
      },
      { immediate: true }
    )
    return { layout }
  }
}
</script>


多种布局



有了上一节的精髓,再看我们如何完善我们的布局系统~


还记得我们在初始化时准备的三个核心组件:HomeAboutContacts,为此我们准备创建三种布局方式。(当然,你可以自定义任意多种布局方式,随你喜欢。)

在此之前我们对 App.vue 进行一个小重构:


新建一个文件:layouts/AppLayoutLinks.vue,把 App.vue 代码抽到此处,留下一个干净的 App.vue。

// 新建的 AppLayoutLinks.vue


<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> |
    <router-link to="/contacts">Contacts</router-link>
  </div>
</template>
<script>
export default {
name: "AppLayoutLinks"
}
</script>
<style scoped>
#nav {
  padding: 30px;
}
#nav a {
  font-weight: bold;
  color: #2c3e50;
}
#nav a.router-link-exact-active {
  color: #42b983;
}
</style>


// 干净的 App.vue


<template>
  <div id="app">
    <router-view/>
  </div>
</template>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>


然后新建三种布局方式文件~


  • 布局方式一:AppLayoutHome.vue
<template>
  <div>
    <header class="header" />
    <AppLayoutLinks />
    <slot />
  </div>
</template>
<script>
import AppLayoutLinks from '@/layouts/AppLayoutLinks'
export default {
  name: "AppLayoutHome",
  components: {
    AppLayoutLinks
  }
}
</script>
<style scoped>
.header {
  height: 5rem;
  background-color: green;
}
</style>


  • 布局方式二:AppLayoutAbout.vue
<template>
  <div>
    <header class="header" />
    <AppLayoutLinks />
    <slot />
  </div>
</template>
<script>
import AppLayoutLinks from '@/layouts/AppLayoutLinks'
export default {
  name: "AppLayoutAbout",
  components: {AppLayoutLinks}
}
</script>
<style scoped>
.header {
  height: 5rem;
  background-color: blue;
}
</style>


  • 布局方式三:AppLayoutContacts
<template>
  <div>
    <header class="header" />
    <AppLayoutLinks />
    <slot />
  </div>
</template>
<script>
import AppLayoutLinks from '@/layouts/AppLayoutLinks'
export default {
  name: "AppLayoutContacts",
  components: {
    AppLayoutLinks
  }
}
</script>
<style scoped>
.header {
  height: 5rem;
  background-color: red;
}
</style>


这里 demo 布局方式就简单的颜色区别作以区分,主要是“会意”。默认布局就不作颜色更改,不做赘述。


配置路由



如果你有仔细看 构造布局 这一精髓,你肯定看到了 this.$route.meta.layout 这一调用。所以我们需要返回路由进行设置,代码如下:

import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: {
      layout: 'AppLayoutHome'
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue'),
    meta: {
      layout: 'AppLayoutAbout'
    }
  },
  {
    path: '/contacts',
    name: 'Contacts',
    component: () => import('@/views/Contacts.vue'),
    meta: {
      layout: 'AppLayoutContacts'
    }
  }
]
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
export default router


打完收工



上面的都设置好了,最后我们需要将 AppLayout.vue 绑定在 App.vue 上:

<div id="app">
    <AppLayout>
      <router-view />
    </AppLayout>
  </div>

打完收工!

你可以把项目 download 到本地跑跑看:github 地址

image.png


如此,我们便实现了一个新的布局系统。灵感来自沙宣美发系列,哦,不,灵感来自 NuxtJS~你感受到了吗?


综上:我们以往的布局就是包裹在组件里面,或者包裹在路由里面,往往都需要多处引用。如果存在多种布局方式,就更为繁琐,没有一个抽离的布局系统作清晰的划分。本文通过 “动态组件”+“监听属性”+“路由配置”+“全局挂载” 的方式将布局系统抽离,免去多处引入,免去不清晰的目录结构。在构建项目初期,就搭建出这一套布局,会是相当明智的做法!如果是老项目,存在多种布局,有空也可以重构一下,感受感受。为什么不呢?


撰文不易✍,点赞鼓励👍,关注我的公众号【掘金安东尼】😎,诚挚输出中......


相关文章
|
1天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
1天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
6天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发
|
6天前
|
存储 JavaScript
Vue 状态管理工具vuex
Vue 状态管理工具vuex
|
13天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
12天前
|
JavaScript
如何在 Vue 中使用具名插槽
【10月更文挑战第25天】通过使用具名插槽,你可以更好地组织和定制组件的模板结构,使组件更具灵活性和可复用性。同时,具名插槽也有助于提高代码的可读性和可维护性。
15 2
|
12天前
|
JavaScript
Vue 中的插槽
【10月更文挑战第25天】插槽的使用可以大大提高组件的复用性和灵活性,使你能够根据具体需求在组件中插入不同的内容,同时保持组件的结构和样式的一致性。
14 2
|
12天前
|
前端开发 JavaScript 容器
在 vite+vue 中使用@originjs/vite-plugin-federation 模块联邦
【10月更文挑战第25天】模块联邦是一种强大的技术,它允许将不同的微前端模块组合在一起,形成一个统一的应用。在 vite+vue 项目中,使用@originjs/vite-plugin-federation 模块联邦可以实现高效的模块共享和组合。通过本文的介绍,相信你已经了解了如何在 vite+vue 项目中使用@originjs/vite-plugin-federation 模块联邦,包括安装、配置和使用等方面。在实际开发中,你可以根据自己的需求和项目的特点,灵活地使用模块联邦,提高项目的可维护性和扩展性。
|
13天前
|
缓存 JavaScript UED
Vue 中异步加载模块的方式
【10月更文挑战第23天】这些异步加载模块的方式各有特点和适用场景,可以根据项目的需求和架构选择合适的方法来实现模块的异步加载,以提高应用的性能和用户体验
|
13天前
|
JavaScript 测试技术 UED
解决 Vue 项目中 Tree shaking 无法去除某些模块
【10月更文挑战第23天】解决 Vue 项目中 Tree shaking 无法去除某些模块的问题需要综合考虑多种因素,通过仔细分析、排查和优化,逐步提高 Tree shaking 的效果,为项目带来更好的性能和用户体验。同时,持续关注和学习相关技术的发展,不断探索新的解决方案,以适应不断变化的项目需求。