详解Vue 项目中的几个实用组件(ts)

前言

这段时间使用 ts 和 vue 做了一个项目,项目从 0 开始搭建,在建设和优化的同时,实现了很多自己的想法,有那么一两个组件可能在我本人看来有意义,所以从头回顾一下当初的想法,同样也可以做到一个记录的作用。如果还没有使用过 ts 的同学可以通过使用 Vue Cli3 + TypeScript + Vuex + Jest 构建 todoList  这边文章开始你的 ts 之旅,后续代码也是在 todoList 的结构上改进的

vue 路由中的懒加载

你真的用好了路由的懒加载吗?

在 2.x 的文档中、cli 的初始化项目中都会默认生成一个路由文件,大致如下:

{
   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 )过度这个加载的过程。

A higher order component for loading components with promises.

其实这也就是 react 的高阶组件 (HOC),那么根据 HOC 的思想,我们能否在 vue 中也实现这样一个 HOC 呢?答案是 YES

让我们看一下官方的介绍:

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 函数生成组件并返回。

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 中使用该组件

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\')
 }
]

看起来貌似已经成功了,但是在这当中还存在问题。

关于 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 的组件,形成逻辑闭环。

SVG 、Iconfont 在 vue 项目中最优雅的用法

能用 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\" rel=\"external nofollow\" href=\"#rectangle\" rel=\"external nofollow\" />
</svg>

正好有这么一个 svg 雪碧图的 webpack loader,svg-sprite-loader,下面是代码

首先根据官网修改配置:

  // 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)(\\&;.*)?$/);

创建 ICON 文件夹,然后在文件夹中创建 svgIcon.vue 组件。

<template>
  <svg v-show=\"isShow\" :class=\"svgClass\" aria-hidden=\"true\">
    <use :xlink:href=\"iconName\" rel=\"external nofollow\" />
  </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>

在当前目录下创建 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);

在当前目录下新建 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\” rel=\”external nofollow\” /> 直接使用这个 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 之后仍然不一致。希望同学们有时间可以自由发挥一下。

总结

永远抱着学习的心态去写代码,尝试多种写法,写出你最优雅的那一种。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容