Vue 組件最優雅的用法,你見過嗎?

前言

這段時間使用 ts 和 vue 作了一個項目,項目從 0 開始搭建,在建設和優化的同時,實現了不少本身的想法,有那麼一兩個組件可能在我本人看來有意義,因此從頭回顧一下當初的想法,一樣也能夠作到一個記錄的做用。若是尚未使用過 ts 的同窗能夠經過 使用 Vue Cli3 + TypeScript + Vuex + Jest 構建 todoList 這篇文章開始你的 ts 之旅,後續代碼也是在 todoList 的結構上改進的。javascript

還在用 Vue Route 懶加載?你已經 out 了!

你真的用好了路由的懶加載嗎? css

在 2.x 的文檔中、cli 的初始化項目中都會默認生成一個路由文件,大體以下:html

{
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =>
        import(/* webpackChunkName: "about" */ './views/About.vue')
}
複製代碼

經過路由懶加載的組件會在 webpack 打包以後生成名爲 about 的 dist/js/about.39d4f7ae.js 文件。 可是在 react 中,react-loadable 可使路由在懶加載以前先加載一個其餘的組件(通常是 loading )過分這個加載的過程。vue

A higher order component for loading components with promises.java

其實這也就是 react 的高階組件 (HOC),那麼根據 HOC 的思想,咱們可否在 vue 中也實現這樣一個 HOC 呢?答案是 YESnode

讓咱們看一下官方的介紹:react

const AsyncComponent = () => ({
  // The component to load (should be a Promise)
  component: import('./MyComponent.vue'),
  // A component to use while the async component is loading
  loading: LoadingComponent,
  // A component to use if the load fails
  error: ErrorComponent,
  // Delay before showing the loading component. Default: 200ms.
  delay: 200,
  // The error component will be displayed if a timeout is
  // provided and exceeded. Default: Infinity.
  timeout: 3000
})
複製代碼

這個 2.3+ 新增的功能,使咱們的開始有了可能,咱們建立一個 loadable.ts 的高階組件,利用 render 函數生成組件並返回。webpack

import LoadingComponent from './loading.vue';

export default (component: any) => {
    const asyncComponent = () => ({
        component: component(),
        loading: LoadingComponent,
        delay: 200,
        timeout: 3000
    });
    return {
        render(h: any) {
            return h(asyncComponent, {});
        }
    };
};
複製代碼

在 routes 中使用該組件git

import loadable from './loadable';

const routes = [
  {
        path: '/about',
        name: 'about',
        // component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
        component: loadable( () => import(/* webpackChunkName: "about" */ './views/About.vue')
  }
]
複製代碼

看起來貌似已經成功了,可是在這當中還存在問題。github

關於 vue-router ,不可避免的會涉及到路由的鉤子函數,可是在以上用法中路由鉤子是失效的,why ?

路由鉤子只直接生效於註冊在路由上的組件。

那麼經過 loadable 生成渲染的組件中 About 組件已是一個子組件,因此拿不到路由鉤子。

組件必須保證使用上的健壯性,咱們換一種方案,直接返回這個組件。

const asyncComponent = importFunc => () => ({
    component: importFunc(),
    loading: LoadingComponent,
    delay: 200,
    timeout: 3000
});
複製代碼

咱們從新更換 routes :

const routes = [
  {
        path: '/about',
        name: 'about',
        // component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
        component: asyncComponent( () => import(/* webpackChunkName: "about" */ './views/About.vue')
  }
]
複製代碼

上述用法已經解決了路由鉤子的問題,可是仍然有兩點值得注意:

  • asyncComponent 接受的參數是一個 function , 若是直接寫成 import(/* webpackChunkName: "about" */ './views/About.vue'), 則 LoadingComponent 沒法生效。
  • AsyncComponent 還能夠添加一個 error 的組件,造成邏輯閉環。

看到 Vue SVG 的這種用法,我已經跪了…

能用 svg 的地方儘可能不使用圖片 筆者在使用 svg 的時候一開始是使用 vue-svg-loader, 具體用法,請自行查看。

可是在寫 sidebar 時,筆者想將 svg 經過配置文件的形式寫入,讓 sidebar 造成多層的自動渲染。 顯然 vue-svg-loader 的用法不合適。咱們先了解 svg 的用法,咱們能夠看一篇乃夫的介紹:SVG 圖標簡介

SVG symbol ,Symbols let you define an SVG image once, and reuse it in multiple places.

和雪碧圖原理相似,能夠將多個 svg 合成一個,可是這裏用 id 來語意化定位圖標

// 定義
<svg class="hidden">
  <symbol id="rectangle-1" viewBox="0 0 20 20">
    <rect x="0" y="0" width="300" height="300" fill="rgb(255,159,0)" />
  </symbol>
    <symbol id="reactangle-2" viewBox="0 0 20 20">
    <rect x="0" y="0" width="300" height="300" fill="rgb(255,159,0)" />
  </symbol>
</svg>

// 使用
<svg>
  <use xlink:href="#rectangle-1" href="#rectangle" />
</svg>
複製代碼

正好有這麼一個 svg 雪碧圖的 webpack loader,svg-sprite-loader,下面是代碼

  1. 首先根據官網修改配置:
// vue.config.js
        const svgRule = config.module.rule('svg');

        // 清除已有的全部 loader。
        // 若是你不這樣作,接下來的 loader 會附加在該規則現有的 loader 以後。
        svgRule.uses.clear();
        svgRule.exclude.add(/node_modules/);
        // 添加要替換的 loader
        // svgRule.use('vue-svg-loader').loader('vue-svg-loader');
        svgRule
            .test(/\.svg$/)
            .pre()
            .include.add(/\/src\/icons/)
            .end()
            .use('svg-sprite-loader')
            .loader('svg-sprite-loader')
            .options({
                symbolId: 'icon-[name]'
            });

        const imagesRule = config.module.rule('images');
        imagesRule.exclude.add(resolve('src/icons'));
        config.module.rule('images').test(/\.(png|jpe?g|gif|svg)(\?.*)?$/);
複製代碼
  1. 建立 ICON 文件夾,而後在文件夾中建立 svgIcon.vue 組件。
<template>
    <svg v-show="isShow" :class="svgClass" aria-hidden="true">
        <use :xlink:href="iconName" />
    </svg>
</template>
 
<script lang="ts"> import { Component, Vue, Prop } from 'vue-property-decorator'; @Component export default class SvgIcon extends Vue { @Prop({ required: true }) private readonly name!: string; @Prop({ default: () => '' }) private readonly className!: string; private get isShow() { return !!this.name; } private get iconName() { return `#icon-${this.name}`; } private get svgClass() { if (this.className) { return 'svg-icon ' + this.className; } else { return 'svg-icon'; } } } </script>
 
<style scoped> .svg-icon { width: 1em; height: 1em; fill: currentColor; overflow: hidden; } </style>
複製代碼
  1. 在當前目錄下建立 index.ts
import Vue from 'vue';
import SvgIcon from './svgIcon.vue'; // svg組件

// 註冊到全局
Vue.component('svg-icon', SvgIcon);

const requireAll = (requireContext: any) =>
    requireContext.keys().map(requireContext);
const req = require.context('./svg', false, /\.svg$/);
requireAll(req);

複製代碼
  1. 在當前目錄下新建 svg 文件夾,用於存放須要的 svg 靜態文件。
☁  icons [1.1.0] ⚡  tree -L 2
.
├── index.ts
├── svg
│   └── loading.svg
└── svgIcon.vue
複製代碼

使用:

<svg-icon name="loading"></svg-icon>
複製代碼

咱們來看一下原理和值得注意的幾點:

  • svg-sprite-loader 處理完經過 import 的 svg 文件後將其生成相似於雪碧圖的形式,也就是 symbol, 經過配置中的 .options({ symbolId: 'icon-[name]' }); 可使用 <use xlink:href="#symbolId" /> 直接使用這個 svg
  • 添加完 svg-sprite-loader 後,因爲 cli 默認對 svg 有處理,因此須要 exclude 指定文件夾的 svg。
  • 使用時因爲 svgIcon 組件的處理,只須要將 name 指定爲文件名便可。

那麼,咱們使用 iconfont 和 svg 有什麼關係呢?

iconfont 的使用方法有不少種,徹底看我的喜愛,可是其中一種使用方法,也是用到了 svg symbol 的原理,通常 iconfont 會默認導出這些文件。

☁  iconfont [1.1.0] ⚡  tree -L 2
.
├── iconfont.css
├── iconfont.eot
├── iconfont.js
├── iconfont.svg
├── iconfont.ttf
├── iconfont.woff
└── iconfont.woff2
複製代碼

咱們關注於其中的 js 文件, 打開文件,能夠看出這個 js 文件將全部的 svg 已經處理爲了 svg symbol,並動態插入到了 dom 節點當中。

而 iconfont 生成的 symbolId 也符合咱們 svg-icon 的 name 命名規則 因此咱們在項目的入口文件中引入這個 js 以後能夠直接使用。

手寫 back-to-up ,真香!

首先爲何會寫這個組件呢,本項目中使用的組件庫是 elementUI ,而 UI 庫中自帶 el-backtop,可是我能說很差用嗎? 或者說我太蠢了,在通過一番努力的狀況下我仍是沒能使用成功,因此本身寫了一個。

直接上代碼:

<template>
    <transition :name="transitionName">
        <div v-show="visible" :style="localStyle" class="back-to-ceiling" @click="backToTop">
            <slot>
                <svg viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="height: 16px; width: 16px;" >
                    <g>
                        <path d="M12.036 15.59c0 .55-.453.995-.997.995H5.032c-.55 0-.997-.445-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29c.39-.39 1.026-.385 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" fill-rule="evenodd" />
                    </g>
                </svg>
            </slot>
        </div>
    </transition>
</template>

<script lang="ts"> import { Component, Vue, Prop } from 'vue-property-decorator'; @Component export default class BackToTop extends Vue { @Prop({ default: () => 400 }) private readonly visibilityHeight!: number; @Prop({ default: () => 0 }) private readonly backPosition!: number; @Prop({ default: () => ({}) }) private readonly customStyle!: any; @Prop({ default: () => 'fade' }) private readonly transitionName!: string; private visible: boolean = false; private interval: number = 0; private isMoving: boolean = false; private detaultStyle = { width: '40px', height: '40px', 'border-radius': '50%', color: '#409eff', display: 'flex', 'align-items': 'center', 'justify-content': 'center', 'font-size': '20px', cursor: 'pointer', 'z-index': 5 }; private get localStyle() { return { ...this.detaultStyle, ...this.customStyle }; } private mounted() { window.addEventListener('scroll', this.handleScroll); } private beforeDestroy() { window.removeEventListener('scroll', this.handleScroll); if (this.interval) { clearInterval(this.interval); } } private handleScroll() { this.visible = window.pageYOffset > this.visibilityHeight; } private backToTop() { window.scrollTo({ left: 0, top: 0, behavior: 'smooth' }); } } </script>

<style scoped> .back-to-ceiling { background-color: rgb(255, 255, 255); box-shadow: 0 0 6px rgba(0, 0, 0, 0.12); background-color: '#f2f6fc'; position: fixed; right: 50px; bottom: 50px; cursor: pointer; } .back-to-ceiling:hover { background-color: #f2f6fc; } .fade-enter-active, .fade-leave-active { display: block; transition: display 0.1s; } .fade-enter, .fade-leave-to { display: none; } </style>

複製代碼

使用:

<back-to-top :custom-style="myBackToTopStyle" :visibility-height="300" :back-position="0">
            <i class="el-icon-caret-top"></i>
        </back-to-top>
複製代碼

custom-style 能夠自行定義,返回的圖標也能夠自由替換。

注意,在 safari 中動畫中動畫表現不一致,使用 requestAnimationFrame 以後仍然不一致。但願同窗們有時間能夠自由發揮一下。

總結

永遠抱着學習的心態去寫代碼,嘗試多種寫法,寫出你最優雅的那一種。

相關文章
相關標籤/搜索