vue-router && vuex

记录一些没有使用过的vue框架中的功能,路由模式、路由配置、导航守卫、路由元信息、Cube-UI、axios 拦截器、vuex的订阅、VUE 插件开发、vue 自定义使用api调用的全局组件、仿照cube-ui的create-api,封装一个非常简单的

Posted by ddxg on September 11, 2019

vue-router && vuex 未使用功能记录

路由模式

router.js 中的 mode: "history"

  • hash ,原理是url中的‘#’hash部分,没有兼容性问题。
  • history ,原理是HTML5 中的 history api,有兼容性问题,移动端还好。需要服务端对路由做一些处理。

我做过的vue项目的路由模式都是使用 ‘hash’ 的。 使用’history’模式的话,url看起来会自然一些,对SEO也有帮助。

路由配置

路由配置中还可以配置props,props可以设置为组件属性。

function func({ params, query }) {
  return {
    id: params.id,
    msg: params.msg,
    foo: query.foo
  };
}

children: [
    { path: "static", component: Page1, props: { foo: "bar" } }, // 给组件传静态值
    { path: "page1/:foo", component: Page1, props: true }, // 将route.params
    {
      path: "page2/:id/:msg",
      name: "page2",
      component: Page2,
      props: func
    }
]

导航守卫

在路由前后跳转的时候都可以进行一些操作,功能强大,常见的是可以处理是否登录、页面权限设置等。

vue-导航守卫

// 全局路由守卫
router.beforeEach((to,from,next)=>{
  console.log(to);
  if (to.path !== '/login') { // 要求登录
    if (isLogin) {
      next();
    } else {
      next('/login?redirect='+to.path);
    }
  } else {
    next();
  }
  next();
})

路由元信息

可以在路由中设置一些额外的信息供导航守卫处理

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // 设置一个meta字段,表示请求时需要校验登录
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

Cube-UI

滴滴公司的vue UI组件库,传送门

axios 拦截器

请求发出之前 和 请求会来之前 可以做一些统一的操作,比如可以统一在请求发出之前做统一的header处理。

Interceptors

类似代码:

axios.interceptors.request.use(config => {
  if (store.state.sign) {
    // 统一设置headers信息
    config.headers._sign = store.state.sign;
  }
  return config;
});

// 响应拦截器,可以提前处理一些公共逻辑
axios.interceptors.response.use(
  response => {
    // 如果status == -1,sign校验错误,需要重新登录
    if (response.status == 200) {
      const {status} = response.data;
      if (+status === -1) {
        loginHandler()
      }
    }
    return response;
  },
  err => {
      if (err.response.status === 401) { // 未授权
        loginHandler()
      }
  }
);

vuex的订阅 subscribe

订阅 store 的 mutation。每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数。

可以统一处理多个 mutation 完成后的逻辑。

// 订阅
store.subscribe((mutation, state) => {
  switch (mutation.type) {
    case "addBookshelf":
      localStorage.setItem("token", JSON.stringify(state.book));
      break;
    case "addCart":
      localStorage.setItem("cart", JSON.stringify(state.cart));
      break;
  }
});

除了可以订阅 mutation ,也可以订阅 store 的 action

VUE 插件

开发插件

这个博客记录得比较详细

Vue.js 的插件最重要的是要暴露一个 install 方法。第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。然后使用Vue.use(xxx) 进行挂载,vue就会去调用install 方法了。

// 记录浏览器历史栈的小插件
const History = {
    _history: [], // 历史记录堆栈
    install(Vue) {
        // 在VUE的原型上挂一个$routerHistory字段用于快速获取历史对象
        Object.defineProperty(Vue.prototype, '$routerHistory', {
            get() {
                return History;
            }
        })
        
        /* 也可以用这种方式在vue原型上面挂 */
        // Vue.prototype.$routerHistory = History
        
        /* 也可以在这里定义全局组件 */
        Vue.component('my-component-name', {
            // ... 选项 ...
        })
    },
    push(path) { // 页面跳转时推入栈
        this._history.push(path);
    },
    pop() { // 页面回退出栈
        this._history.pop();
    },
    canBack(){ // 只有栈中有历史记录才能回退
        return this._history.length > 1;
    }
}
export default History;

vue 自定义使用api调用的全局组件

像Loading、弹窗和Toast这些使用频率比较组件做成全局组件也不算很方便,要用的地方还是得import一下vue文件或者写显示的写html代码。 我之前的方式是将Loading、弹窗和Toast组件都在App.vue文件中引入,其他组件通过$EventBus$emit 来调用,这样就不会到处引入公共组件了。 现在介绍一些UI库常用的方式,就是通过api调用的方法调用公共组件,像 this.$Toast({...}) 这样子调用。

这里举一个 Toast 组件为例。Toast.vue 方面的东西就不说了,主要记录一下是怎么实现可以用api的方式调用 Toast 的。

大概流程是这样子的:

  • 创建一个新的Vue实例,用于渲染 Toast 组件。
  • 使用 render 函数对 Toast 进行渲染,使用 $mount 方法进行挂载。
  • 获取 Toast 实例,就可进行Toast操作了。
import Toast from "@/components/Notice.vue";
import Vue from "vue";

// 给Toast添加一个创建组件实例的方法,可以动态编译自身模板并挂载
Toast.getInstance = props => {
  // 创建一个Vue实例
  const instance = new Vue({
    render(h) {
      // h 是 createElement 方法的缩写
      // 渲染函数:用于渲染Toast模板为虚拟dom
      return h(Toast);
    }
  }).$mount(); // 执行挂载,若不指定选择器,则模板将被渲染为文档之外的元素

  // 由于上面的$mount()没有指定挂载的选择器
  // 必须使用原生dom api把它插入文档中
  // $el指的是渲染的Toast中真实dom元素
  document.body.appendChild(instance.$el); // $mount()指定了选择器,则不需要这一行代码了

  // 获取Toast实例,$children指的是当前Vue实例中包含的所有组件实例
  const toast = instance.$children[0];
  return toast;
};

// 设计单例模式,全局范围唯一创建一个Toast实例,反复利用已经存在的Toast实例
let msgInstance = null;
function getInstance() {
  msgInstance = msgInstance || Toast.getInstance();
  return msgInstance;
}

// 暴露接口
export default {
  info({ duration = 2, content = "" }) {
    getInstance().add({
      content,
      duration
    });
  }
};


// main.js中
import Toast from '../components/Toast.vue';
Vue.prototype.$toast = Toast.info;


// 在其他组件中使用
this.$toast({
    content: '123',
    duration: 2
});

也可以使用 Vue.extend vue构造器,创建一个‘子类’的方式来挂载 Toast 组件。

// 改动代码
Toast.getInstance = props => {
  const Profile  = Vue.extend(Toast)
  const instance = new Profile().$mount()

  // 由于上面的$mount()没有指定挂载的选择器
  // 必须使用原生dom api把它插入文档中
  // $el指的是渲染的Toast中真实dom元素
  document.body.appendChild(instance.$el); // $mount()指定了选择器,则不需要这一行代码了
  return instance;
};

参考:Vue 自定义全局消息框组件

仿照cube-ui的create-api,封装一个非常简单的

按照上面的 Toast 的话,如果需要增加一个弹窗组件,那又要写一遍类似的 toast.js 代码, 看到cube-ui中封装了一个create-api 模块,功能非常强大,也很实用。就照着写了个很简单的。

// myCreate.js

// 给component添加一个创建组件实例的方法,可以动态编译自身模板并挂载
function myRender(Vue, component, props) {
  // 创建一个Vue实例
  const instance = new Vue({
    render(h) {
      // h 是 createElement 方法的缩写
      // 渲染函数:用于渲染Toast模板为虚拟dom
      // 第二个对象是模板中属性对应的数据对象,see:https://cn.vuejs.org/v2/guide/render-function.html#createElement-%E5%8F%82%E6%95%B0
      return h(component, {props});
    }
  }).$mount(); // 执行挂载,若不指定选择器,则模板将被渲染为文档之外的元素

  // 由于上面的$mount()没有指定挂载的选择器
  // 必须使用原生dom api把它插入文档中
  // $el指的是渲染的Toast中真实dom元素
  document.body.appendChild(instance.$el); // $mount()指定了选择器,则不需要这一行代码了

  // 获取实例,$children指的是当前Vue实例中包含的所有组件实例
  const thisComponent = instance.$children[0];

  // 为实例添加销毁方法,防止内存泄漏
  thisComponent.remove = function() {
    // 使用Vue的销毁实例的方法,see:https://cn.vuejs.org/v2/api/#vm-destroy
    console.log('destroy', thisComponent.$destroy);
    instance.$el.remove(); // 先移除DOM节点,再销毁实例
    instance.$destroy(); 
  }

  return thisComponent;
};

function myCreateAPI(Vue, component) {
    let componentName = component.name;
    let apiName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
    console.log('apiName', apiName);
    // 在Vue上面挂一个['$myCreate' + apiName]方法。
    Vue.prototype['$myCreate' + apiName] = function (props) {
        // 这里只实现了传props,没有实现slot插槽等其他方法
        return myRender(Vue, component, props);
    };
}

// 暴露接口
export default myCreateAPI;

// ------------------------

// main.js 中
// 使用cube的creameAPI添加加入购物车小球动画组件
createAPI(Vue, BallAmin, ['transitionend']); // 定义一个动画结束的回调事件名称

// 自己封装的createAPI,
myCreateAPI(Vue, BallAmin);

// ------------------------

// 在组件中调用

// 使用cube-ui的createAPI显示加购球动画
  const ballAminComp = this.$createBallAmin({
    el,
    onTransitionend() {
      // 动画回调事件,销毁实例
      console.log('销毁实例');
      ballAminComp.remove();
    }
  });
  ballAminComp.show();

// 使用自己封装的create api 添加加入购物车小球动画组件
  const ballAminComp = this.$myCreateBallAmin({el});
  console.log('ballAminComp',ballAminComp);
  ballAminComp.show();
  ballAminComp.$on('transitionend', function(){
    console.log('-删除-');
    ballAminComp.remove();
  });

跟cube-ui相比差太远了,不断完善中。。。