Vue根据标签实现按需加载

实际开发中发现一个项目中会使用到多个组件库,对于这些组件库有一些使用的是按需加载,但是也有一些使用的是全局注册。
对于一些大的第三方组件库需要按需导入。

如:import ElButton from 'element-ui/lib/button;
但是这种按需导入在页面使用组件非常多的场景时,开发繁琐,体验不友好。
当然这些组件库也会推荐一些babel插件来提升开发体验和性能优化。
babel-plugin-componentbabel-plugin-importbabel-plugin-transform-imports等。
babel-plugin-transform-imports可以通过自己的配置来实现以下效果。

import { Row, Grid as MyGrid } from react-bootstrap'; 
import { merge } from 'lodash';
↓ ↓ ↓ ↓ ↓ ↓
import Row from 'reactbootstrap/lib/Row';
import MyGrid from 'react-bootstrap/lib/Grid';
import merge from 'lodash/merge';`

babel-plugin-transform-imports同样有一些缺陷:它无法由一个导入生成多个导入。

这也就意味着使用babel-plugin-transform-imports无法对antdelement-ui这样的UI组件库进行模块按需导入:这些UI组件库,除了js模块的导入外,往往还有一个样式模块。

详情请参考https://bitbucket.org/amctheatres/babel-transform-imports/src/master/

因此参考babel-plugin-transform-imports实现一个vue项目的组件按需导入。

1. 获取页面所有标签。

Vue中的每个页面其实都是一个.vue的文件,这种文件,Vue称之为组件页面,其使用vue-loader来进行解析。

当解析某个vue页面时通过node-html-parser来获取到当前页面的所有标签。使用sync-disk-cache来将获取到的标签保存在本地。

image.png

image.png

页面源码会被vue-loader解析成上图所示,其中包含四种类型的代码块。

一、因此根据loaderresourceQuery属性是否含有type=template来获取template块。

二、使用node-html-parsertemplate代码块进行解析(深层遍历,去重)

三、创建sync-disk-cache对象,暴露getsetclear方法

2. 使用babel在代码编译时将目标标签按照特定规则进行引入、并将组件进行注册。

一、babel插件暴露lib(配置组件导入地址)、style(配置样式导入地址)方法。

二、在visitor指定 Program节点,并根据路径获取sync-disk-cache保存的本地标签。

三、在export default语句中完成剩下的逻辑, 也就是表达式ExportDefaultDeclaration的访问器中:

因为组件最终导入的形式为:

import Comp1 from 'path/to/comp1';
import Comp2 from 'path/to/comp2';

vue文件中,script里面一定会导出一个对象

export default {
   data() {
       return {};
   },
 };

此时要将Comp1Comp2注册则需要写成以下这种形式。

export default {
   components: {
       Comp1: Comp1,
       Comp2: Comp2,
   },
   data() { 
       return {};
   },
};
   

理论来说需要这样,但是这样去改太复杂了,虽然有babel的帮助,至少要考虑:

  1. 当前组件的配置对象里面是否有components这个选项,没有还需要特殊的复杂处理
  2. 当前组件的配置对象里面有components这个选项,低于注册的那个组件,他是不是已经注册过了,如果是,还有可能语法报错。
  3. 他是不是复杂的组件配置对象的写法,比如
const config = {
   components: {},
   data: ...
   // ....
};
export default config;

还有可能是这样的:

const compontns = { ... };
export default {
   components: compontns,
   data: ...
   // ....
};

要处理的情况太多了,从另一个角度想,它最终导出的一定是一个组件的配置对象,因此可以把它解出来。
比如 export default ....
不管他这个....是一个字面量还是一个变量都转换成

const _thisCpnponentConfig = ...
export _thisCpnponentConfig;

在这种情况下如果要给他注册组件只需扩展这个_thisCpnponentConfig里面的components选项就好了。
因此上面的代码可以写成

const _thisCpnponentConfig = ...;
// 先初始化一下,避免他开始没有
thisCpnponentConfig.components = thisCpnponentConfig.components || {};
thisCpnponentConfig.components['Comp1'] = Comp1;
export _thisCpnponentConfig;

3. 如何使用

npm i vue-template-label-loader;
npm i babel-plugin-vue-auto-import;

3.1.1. 在webpack配置中配置该loader:

const { clear } = require('vue-template-label-loader/lib/store');
// 在每次构建时, 都清空上一次存储信息。
clear();
module.exports = {
   module: {
       rules: [{
           test: /\.vue$/,
           use: [{
               loader: 'vue-template-label-loader',
               options: {}
               exclude: {}
           },{
               loader: 'vue-loader',
               options: {
               //...
               },
           }]
       },
   ]}
}; 

3.1.2. 在vue.config.js中

const { clear } = require('vue-template-label-loader/lib/store');
// 在每次构建时, 都清空上一次存储信息。
clear();
module.exports = {
   chainWebpack: (config) => {
       config.module
           .rule('vue')
           .set('exclude', [/node_modules/])
           .use('vue-template-label-loader')
           .loader('vue-template-label-loader')
           .end()
       },
};

3.2. 配置babelrc.js

该工具需要配合 vue-template-label-loader 使用

function isCapitalStart(string) {
  if (!string) {
    return false
  }
  const first = string[0];
  const reg = new RegExp(/[A-Z]/);
  return reg.test(first);
}


function toLine(string) {
  return string.replace(/([A-Z][a-z]*)([A-Z][a-z]*)/g,"$1-$2").toLowerCase();
}

function kebabCase(str) {
  var hyphenateRE = /([^-])([A-Z])/g;
  return str
    .replace(hyphenateRE, '$1-$2')
    .replace(hyphenateRE, '$1-$2')
    .toLowerCase();
}

module.exports = {
  presets: ['@vue/cli-plugin-babel/preset'],
  plugins: [
    [
      'babel-plugin-vue-auto-import',
      {
        // excludeTags: ['List', 'HelloWorld'],
        lib(tag) {
          // 如果某个标签需要自动导入,请返回导入路径, 不需要则返回null
          if (tag.startsWith('el-')) {
            return `element-ui/lib/${tag.replace('el-', '')}`;
          }
          if (tag.startsWith('a-')) {
            return `ant-design-vue/lib/${tag.replace('a-', '')}`;
          }
          if(isCapitalStart(tag) || tag.startsWith('i-')) {
            const tagName = tag.startsWith('i-') ? tag.replace('i-', '') : toLine(tag)
            return `view-design/src/components/${toLine(tagName)}/index.js`
          }
          return null
        },
        style(tag) {
          // 如果某个标签需要自动样式文件,请返回导入路径,无则返回null
          if (tag.startsWith('el-')) {
            const label = tag.replace('el-', '');
            return `element-ui/lib/theme-chalk/${label}.css`;
          }
          if (tag.startsWith('a-')) {
            const tagName = tag.replace('a-', '');
            return `ant-design-vue/lib/${tagName}/style`;
          }
          return null;
        },
      },
    ],
  ],
};

4. 注意事项

因为element-ui部分组件有使用到icon但是组件不会去自动导入icon,因此icon需要手动进行全局导入。

import Vue from 'vue'
import { Icon } from 'element-ui'
import 'element-ui/lib/theme-chalk/icon.css';
Vue.component(Icon.name, Icon)

ant-desgin-vuemodel组件使用时会报错,具体原因是,按需引入的常用写法中没有调用到Vue.use所执行的自定义指令。解决方案如下

// main.js
import { Modal }from 'ant-design-vue';
Modal.install(Vue)` 

view-desgin 根据官方文档,需要在main.js将样式全部导入

import 'view-design/dist/styles/iview.css';

具体使用可以参考https://github.com/dexterBo/vue-auto-tag

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容