webpack的ProvidePlugin 和 简单的babel插件 在 vue 业务项目的落地

241 阅读3分钟

image.png

这个文件夹为国际化文件夹。如果不做任何处理,在后续我们需要使用到国际化的文件,每个文件、每个组件都需要import进来,为了简化不需要每个组件都写下面的代码

import locales from 'locales/index'

我们使用 vue-router 和 vuex 源码挂载 router 和 store 的思想 (组件树深度优先遍历规则,所以我们可以在 组件内部使用 this.$storethis.$router this.$route),在初始化Vue的时候,传入 i18n

import i18n from '@/locales';
import mixinsGlobal from 'utils/mixinsGlobal'
new Vue({
    router,
    store,
    i18n,
    render: h => h(App)
}).$mount('#app');
mixinsGlobal()
import Vue from 'vue';

const mixinsGlobal = () => {
  Vue.mixin({
    beforeCreate() {
      if (this.$options && this.$options.i18n) { //根组件
        this.cusI18n = this.$options && this.$options.i18n;
      } else {
        // 深度先续遍历
        this.cusI18n = this.$parent && this.$parent.cusI18n;
      }
    }
  });
};

export default mixinsGlobal;
  <el-select
    v-model="form.status"
    :placeholder="cusI18n.t('chooseStatus')"
    style="width: 100%"
    size="small"
  >
    <el-option
      :label="statusValObj[statusEnmu.statusYes]"
      :value="statusEnmu.statusYes"
    ></el-option>
  </el-select>

通过上述操作代码,我们已经成功在每个 vue文件都注入了i18n,在每个组件都可以通过 this.cusI18n 直接访问到了 i18n,相当于挂载到每个vue组件的 this 上面了 (每个 vue组件对应 源码其实都是一个 vue 实列,只不过挂载通过 $mount 手动挂载,挂载给父组件)

此时。我们已经可以在每个组件通过 this.cusI18n访问到 i18n,并且在 vue 的template 里面可以直接 通过 cusI18n.t 直接使用 {{cusI18n.t('inputappname')}} (默认 vue 的tempalte 编译出来是一个render 函数,挂载在 options 上, 并且 此 render函数通过 with{} 绑定 tempalte 上面写的变量,其实就是通过 new Function 加上 with 创建一个沙箱环境,并且绑定了作用域的 this)

此时看上去已经ok了,但是存在一个问题。我们如果在非 vue文件需要使用 i18n, 还是需要在每个文件里面使用

import locales from 'locales/index'

因为上面的 demo 展示的是 在每个 vue文件成功绑定了 i18n。 此时我们想到了 webpack的 ProvidePlugin

   config.plugin('provider').use(webpack.ProvidePlugin, [{
      providerI18n:  [ path.resolve(path.join(__dirname, 'src/locales/index.js')), 'default']
   }]);

通过上面的配置已经成功在每个webpack 的 module 绑定了 providerI18n(相当于此项目的每个文件都可以之间通过providerI18n 访问到 i18n)。

此处埋个坑,后面写一写webpack 原理文章,讲一讲 module chunk bundle 之间的关系,现在先理解 module 就是项目里面的每个文件

此时,内心窃喜(我们做到了 don't repeat youself),但是这时候存在一个问题。因为vue tempalte 的问题 看下面的代码

  <el-select
    v-model="form.status"
    :placeholder="providerI18n.t('chooseStatus')"
    style="width: 100%"
    size="small"
  >
    <el-option
      :label="statusValObj[statusEnmu.statusYes]"
      :value="statusEnmu.statusYes"
    ></el-option>
  </el-select>

如果此时我们这样写,在vue 的tempalte 里面之间用 providerI18n.t , 此时被 vue-loader 编译出来之后我们发现 placeholder 这里变成了 _vm.providerI18n.t('chooseStatus'), 默认被绑定了 _vm, 这是因为上面提到的 vue template 被编译为 render 函数的愿意(new Function + with 构建了一个沙箱环境,默认用 _vm 在template上面绑定了使用到的变量),此时运行肯定报错,因为 _vm上面没有 providerI18n, 这个 providerI18n是一个__webpack__require内部的一个全局变量,可以直接使用。

此时运行肯定报错,_vm.providerI18n.t is not a function. 这时我们想到使用 babel 来帮我们处理一下 这个vue-loader 处理的结果

babel插件 parseTemplatei18

module.exports = function ({ types }) {
    return {
        visitor: {
            CallExpression(path, state) {
                let options = state.opts;
                let calleeObjectCode = path.get('callee').toString()
                if (types.isMemberExpression(path.node.callee) && calleeObjectCode === options.calleeSourceCode ) {
                    const newNode = types.identifier(options.calleeTargetCode)
                    path.get("callee").get('object').replaceWith(newNode)
                }
            }

        }
    }
}

babel.config.js 配置

const parseTemplatei18 = require('./parseTemplatei18')
module.exports = {
  presets: [
  ],
  plugins: [
    [parseTemplatei18, { calleeSourceCode: '_vm.providerI18n.t', calleeTargetCode: 'providerI18n' }]
  ]
};

此时在编译的时候就会把 vue tempalte 编译的结果 _vm.providerI18n.t('chooseStatus') 替换为 providerI18n.t('chooseStatus')。因为使用了 types.identifier(options.calleeTargetCode) 替换了 老的 path.node.callee.

image.png

至此,我们完成了功能 (不需要在所有模块导入locales/index, 规避了don't repeat youself,借助了 webpack的ProvidePlugin + babel 插件实现了功能)。

因为这边文章涉及部分原理知识,后续会断断续续补充完整 (vue 编译原理, vue 组件渲染原理,webpack module chunk bundle tapable 原理, babel 插件原理,后续还有一个增强的 remove-console babel插件)。