用 webpack 来更好地加载页面的数据吧

in FEcoding with 0 comment

前言

先说下当前普遍的在页面中加载表格数据的方式,常见的数据加载方式是在表格数据未加载前显示一个loading插件,当数据加载完后隐藏掉loading插件,显示已加载完的表格数据。
大概coding如下:

getData(params).then(res => {
      this.loading = false;
      // 接下来对数据处理
    }).catch(err => {
      this.loading = false;
    })

从上可以看出在coding的时候:
1.我们需要一个可复用的loading组件,可以在各个使用到的页面中用状态控制它的显示与隐藏。-
2.需要在每次获取数据的地方都加上catch判断,隐藏掉loading组件。

这样每次在拉取数据的时候都要加上catch语句,是比较麻烦的,其实我们可以通过自己加上一个webpack loader来自动对这些语句做处理,加上catch函数,执行then函数中的第一句语句。-
所以下文便从这两个角度出发来解决数据加载状态显示的问题~~

编写可复用的loading插件

Vue插件与use方法

要实现全局代码可复用,在vue中一般会使用添加全局方法、全局资源、mixin等,可以封装成插件的形式,插件的功能和范围没有严格的限制,一般有以下几种:

  1. 添加全局方法或者属性。如: vue-custom-element
  2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  3. 通过全局混入来添加一些组件选项。如 vue-router
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

Vue中是通过use方法来使用插件的,Vue.use的源码如下:

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 可以看出Vue.use方法会自动阻止多次注册插件
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // 注入参数,调用插件自定义的install方法
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

由此可见,自定义插件必须提供一个install方法来使自己被添加到Vue中。

自定义指令

由于我最近在跟进的项目是使用了element-ui组件库,里边已经包含了loading插件了,其中便使用了指令的方法,在vue中一般代码复用和抽象的主要形式是组件,但是像loading这种需要对普通dom元素进行底层操作的话(loading的覆盖范围与所使用的dom元素相关), 就需要用到自定义的指令。

// 注册一个自定义指令 v-loading
Vue.directive('loading', {
    bind: function(){},
    update: function(){},
    unbind: function(){}
})

自定义组件的生命周期如下:

Element的loading指令方式实现:

所以如果想添加一个全局的指令到Vue实例当中,需要loading插件提供一个install方法,在install中注册自定义指令v-loading,在指令的bind生命周期插入loading结点,在update生命周期中更新loading状态,unbind生命周期中销毁loading。

import Vue from 'vue';
    import Loading from './loading.vue';
    import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom';
    import { PopupManager } from 'element-ui/src/utils/popup';
    import afterLeave from 'element-ui/src/utils/after-leave';
    const Mask = Vue.extend(Loading);
    
    const loadingDirective = {};
    // 提供install方法
    loadingDirective.install = Vue => {
      if (Vue.prototype.$isServer) return;
      // 更新loading状态的方法
      const toggleLoading = (el, binding) => {
        // v-loading="true"时:
        if (binding.value) {
          // 放入异步更新队列中处理Loading状态更新事件
          Vue.nextTick(() => {
            if (binding.modifiers.fullscreen) {
              // 处理v-loading.fullscreen情况
              insertDom(el, el, binding);
            } else {
              // 处理指令修饰符不含fullscreen的情况
              removeClass(el.mask, 'is-fullscreen');
              if (binding.modifiers.body) {
                // 处理指令修饰符v-loading.body的情况
                insertDom(el, el, binding);
              } else {
                // 处理指令修饰符v-loading的情况
                el.originalPosition = getStyle(el, 'position');
                insertDom(el, el, binding);
              }
            }
          });
        } else {
          // 绑定loading隐藏时的回调函数
          afterLeave(el.instance, _ => {
           //.....
          }, 300, true);
          el.instance.visible = false;
          el.instance.hiding = true;
        }
      };
      const insertDom = (parent, el, binding) => {
       // 插入节点的方法....
      };
      // 钩子函数参数:
      // el:指令所绑定的dom元素
      // binding: 有关指令的属性,是一个对象,包含name,value,oldValue,expression,arg, modifier
      // vnode: vue编译生成的虚拟节点
      // oldVnode: 上一个虚拟节点
      Vue.directive('loading', {
        bind: function(el, binding, vnode) {
          const textExr = el.getAttribute('element-loading-text');
          const spinnerExr = el.getAttribute('element-loading-spinner');
          const backgroundExr = el.getAttribute('element-loading-background');
          const customClassExr = el.getAttribute('element-loading-custom-class');
          const vm = vnode.context;
          const mask = new Mask({
            el: document.createElement('div'),
            data: {
              text: vm && vm[textExr] || textExr,
              spinner: vm && vm[spinnerExr] || spinnerExr,
              background: vm && vm[backgroundExr] || backgroundExr,
              customClass: vm && vm[customClassExr] || customClassExr,
              fullscreen: !!binding.modifiers.fullscreen
            }
          });
          el.instance = mask;
          el.mask = mask.$el;
          el.maskStyle = {};
    
          binding.value && toggleLoading(el, binding);
        },
    
        update: function(el, binding) {
          el.instance.setText(el.getAttribute('element-loading-text'));
          if (binding.oldValue !== binding.value) {
            toggleLoading(el, binding);
          }
        },
    
        unbind: function(el, binding) {
          if (el.domInserted) {
            el.mask &&
            el.mask.parentNode &&
            el.mask.parentNode.removeChild(el.mask);
            toggleLoading(el, { value: false, modifiers: binding.modifiers });
          }
          el.instance && el.instance.$destroy();
        }
      });
    };
    
    export default loadingDirective;

ok~到目前为止,我们已经大概了解了loading指令是如何添加到全局Vue中了,接下来就可以直接在需要的组件上添加指令,如下:

    <el-table
        v-loading="loading"
        :data="tableData"
        >
        <!-- ... -->
    </el-table>

编写一个自定义的webpack loader

在组件上添加loading指令后,我们开始来考虑如何自动增加上catch语句
Webpack中有loader和plugin, loader更加专注于文件层次上的内容转换,比如对jsx文件的处理、css预处理等,plugin则不限于文件,可作用的范围也更加广,是用来拓展webpack功能的,比如对文件内容的压缩等。在这个场景,我们主要是对文件内容的增加,所以需要来编写一个自定义的loader.

配置使用

如何配置使用loader?

module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            use: 'babel-loader'
          }
        ]
      },

webpack的loader是支持链式调用的.

自定义loader

loader是一个node module,如下:

module.exports = function(source){
        //...
        return source;
    }

传入的source是包含了资源文件的字符串。
由于要实现自动代码注入,所以我们要做的事情大概分为以下这几步:

  1. 需要先使用parse方法把代码字符串转化为AST抽象语法树,
  2. 改造AST树:-
    2.1 遍历AST树,遍历then函数中的箭头函数。-
    2.2 去除箭头函数中的第一句-
    2.3 新建一个箭头函数,并赋值上一步取出的语句, 赋给catch函数-
    2.4 组合then函数和catch函数,替换原来的then函数。
  3. 再次使用print方法把抽象语法树转成代码字符串返回。

以下代码主要来自AST与工程化

const recast = require('recast')
    const {
      identifier: id, // 标志符
      memberExpression, // 成员表达式
      callExpression, // 函数调用表达式
      blockStatement, // 带大括号的表达式序列
      arrowFunctionExpression // 箭头函数表达式
    } = recast.types.builders
    const t = recast.types.namedTypes
    
    module.exports = function (source) {
      // parse函数将代码字符串转换为ast树,一般项目会用到babel-loader, 所以解析器也使用recast/parsers/babel
      const ast = recast.parse(source, {
        parser: require('recast/parsers/babel')
      })
    
      // visit遍历
      recast.visit(ast, {
        visitCallExpression (path) {
          const { node } = path
          const args = node.arguments
    
          let firstExp
    
          args.forEach(item => {
            // 箭头函数并含语句
            if (t.ArrowFunctionExpression.check(item) && item.body.body) {
              firstExp = item.body.body[0]
              // 检查是否为then函数所调用等条件
              if (
                recast.print(firstExp).code.includes('loading') &&
                t.ExpressionStatement.check(firstExp) &&
                t.Identifier.check(node.callee.property) &&
                node.callee.property.name === 'then'
              ) {
                const arrowFunc = arrowFunctionExpression([], blockStatement([firstExp]))
                const originFunc = callExpression(node.callee, node.arguments)
                const catchFunc = callExpression(id('catch'), [arrowFunc])
                // 组合新的成员表达式
                const newFunc = memberExpression(originFunc, catchFunc)
                // 替换
                path.replace(newFunc)
              }
            }
          })
    
          return false
        }
      })
      // 重新生成代码字符串并返回
      return recast.print(ast).code
    }

使用自定义的loader

{
            test: /\.js$/,
            use: [
              {
                loader: path.resolve(__dirname, '../src/loaders/catch.js')
              }
            ]
          }

总结

到这里本文也就结束啦,本文主要介绍了如何在增强交互的同时实现代码可复用性,希望能抛砖引玉,引发小伙伴更多的思考。如果你觉得我的文章对你有所帮助的话,可以点个赞鼓励下嘛(・ω・)ノ

头图来源

Responses