likes
comments
collection
share

跨域异步加载vue组件

作者站长头像
站长
· 阅读数 3

背景

  • 产品要求公共功能全站统一
  • 工程多、项目复杂,协调困难
  • 自建ui库更新频繁时,很多项目不再跟进
  • 域名多,跨域复用

最终达到目的,一处修改,全站复用

异步加载组件

  • 利用vue组件的异步加载
  • script跨域获取资源,缓存到_async_components_对象上
import AsyncComponent from './index.vue'

// 为组件提供 install 安装方法,供按需引入
AsyncComponent.install = function (Vue) {
  Vue.component(AsyncComponent.name, AsyncComponent)
}

export default AsyncComponent
<template>
  <component :is="name" v-bind="$attrs" v-on="$listeners">
    <template v-for="(_, name) in $scopedSlots" v-slot:[name]="data">
      <slot :name="name" v-bind="data || {}" />
    </template>
  </component>
</template>
<script>
/**
 * 异步组件
 * 默认从cdn加载
 */
export default {
  name: 'AsyncComponent',
  props: {
    name: { type: String, required: true },
    base: { type: String },
    path: { type: String, default: '/awesome/vue-async-components' }
  },
  created() {
    this.constructor.component(this.name, () => {
      // 这里模拟异步组件,实际场景,是使用微组件加载器去获取组件配置对象
      return {
        component: this.loadComponent()
        // loading: LoadingComp
      }
    })
  },
  methods: {
    loadScript(url) {
      return new Promise((resolve, reject) => {
        var script = document.createElement('script')
        script.type = 'text/javascript'
        if (script.readyState) { // IE
          script.onreadystatechange = function() {
            if (script.readyState === 'loaded' || script.readyState === 'complete') {
              script.onreadystatechange = null
              console.log('complete')
              resolve()
            }
          }
        } else { // Others
          script.onload = function() {
            console.log('complete')
            resolve()
          }
          script.onerror = function(e) {
            reject(e)
          }
        }
        script.src = url
        document.getElementsByTagName('body')[0].appendChild(script)
      })
    },
    async loadComponent() {
      if (!this.name) return
      try {
        const coms = window._async_components_ || (window._async_components_ = {})
        if (!coms[this.name]) {
          let base = this.base || `https://xxx.com`
          if (process.env.NODE_ENV === 'development') {
            base = location.origin + this.path
          }
          const url = base + '/' + this.name + '/index.min.js?t=' + new Date().getTime()
          await this.loadScript(url)
        }
        console.log(coms)
        return coms[this.name]
      } catch (error) {
        console.error(error)
      }
    }
  }
}
</script>

使用异步加载组件

AsyncComponent发布到npm 业务工程中引用Test组件

<template>
  <div>
    demo
    <AsyncComponent name="Test" id="111" @change="log">
      <div>test a test async component</div>
    </AsyncComponent>
  </div>
</template>
<script>
import AsyncComponent from 'async-component'

export default {
  components: {
    AsyncComponent
  },
  methods: {
    log(val) {
      console.log(val)
    }
  }
}
</script>

Test组件如下

<template>
  <div>
    <h1>Hello, World!</h1>
    <div class="test">props.id: {{ id }}</div>
    <el-card>
    13213465
    </el-card>
    <slot></slot>
  </div>
</template>
<script>

export default {
  name: 'Test',
  props: {
    id: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      value: ''
    }
  },
  mounted() {
    setTimeout(() => {
      this.$emit('change', 'test');
    }, 5000);
  }
}
</script>
<style lang="less" scoped>
.test {
  color: red;
}
</style>

效果如下

跨域异步加载vue组件

异步组件工程

  • 开发组件,本地调试支持
  • 本地查看加载远程资源
  • 发布AsyncComponent

跨域异步加载vue组件

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --config webpack.dev.js",
    "watch": "webpack --config webpack.dev.js --watch",
    "build": "webpack --config webpack.prod.js"
  },
const TerserPlugin = require('terser-webpack-plugin');
const { VueLoaderPlugin } = require("vue-loader");

const packages = ['AsyncComponent','Test'];
const entries = {};
for (const name of packages) {
  entries[`${name}/index`] = './packages/' + name + '/index.js';
  entries[`${name}/index.min`] = './packages/' + name + '/index.js';
}

// Less变量注入
// const GlobalJson = {};
// for (const name of packages) {
//   const global = require('./packages/' + name + '/global');
//   if (global) {
//     GlobalJson[name] = global;
//   }
// }

module.exports = {
  mode: 'production',
  target: ['web', 'es5'],
  entry: entries,
  output: {
    path: __dirname + '/awesome/vue-async-components',
    filename: '[name].js',
    // library: '_async_components_',
    libraryTarget: 'umd',
    umdNamedDefine: true,
    libraryExport: 'default',
    publicPath: '/awesome/vue-async-components',
  },
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        include: /\.min\.js$/,
        extractComments: false,
        parallel: true,
        // sourceMap: true,
        terserOptions: {
          ecma: 6,
          compress: {
            drop_console: true,
            drop_debugger: true,
            ecma: 6,
            passes: 3,
            pure_funcs: ['console.log'],
          },
        },
      }),
    ],
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader',
      },
      {
        test: /\.(?:js|mjs|cjs)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', 
                { targets: {"chrome": "58", "ie": "10"}, useBuiltIns: 'usage', corejs: { version: 3 } }
              ]
            ],
            // plugins: [
            //   ['@babel/plugin-transform-runtime', 
            //     { corejs: { version: 3 }, helpers: true, regenerator: true }
            //   ]
            // ]
          },
        },
      },
      {
        test: /\.(less|css)$/i,
        use: [
          // compiles Less to CSS
          'style-loader',
          'css-loader',
          {
            loader: 'less-loader',
            options: {
              additionalData: (content, loaderContext) => {
                // 更多可用的属性见 https://webpack.js.org/api/loaders/
                // const { resourcePath, rootContext } = loaderContext;
                // const relativePath = path.relative(rootContext, resourcePath);

                // for (let name of packages) {
                //   if (relativePath.includes(name)) {
                //     const json = GlobalJson[name];
                //     Object.keys(json).forEach((key) => {
                //       content = `@${key}:${json[key]};` + content;
                //     });
                //   }
                // }

                return content;
              },
            },
          },
        ],
      },
      {
        test: /\.(?:ico|gif|png|jpg|jpeg)/,
        type: 'asset/inline',
      },
      {
        test: /\.(ttf|eot|woff|woff2)$/,
        loader: 'file-loader',
        options: {
          name: 'fonts/[name].[ext]'
        }
      }
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
  ],
  // 关闭性能提示
  performance: {
    hints: false,
  },
};
const path = require('path');
const { merge } = require('webpack-merge');
const CopyPlugin = require('copy-webpack-plugin');

const conf = require('./webpack.conf.js');

module.exports = merge(conf, {
  mode: 'development',
  entry: {
    main: './examples/main.js'
  },
  // 关闭性能提示
  performance: {
    hints: false,
  },
  plugins: [
    // 复制静态资源
    new CopyPlugin({
      patterns: [
        { from: __dirname + '/examples/index.html' },
      ],
    }),
  ],
  devServer: {
    compress: true,
    hot: true,
    port: 9090,
    open: ['/awesome/vue-async-components/'],
  },
});

webpack.prod.js同webpack.conf.js

值得注意的点

1 本地开发调试时选择加载服务器资源,不是相对引用

   if (process.env.NODE_ENV === 'development') {
      base = location.origin + this.path
   }

跨域异步加载vue组件

entry多入口

const packages = ['AsyncComponent','Test'];
const entries = {};
for (const name of packages) {
  entries[`${name}/index`] = './packages/' + name + '/index.js';
  entries[`${name}/index.min`] = './packages/' + name + '/index.js';
}
entry: {
    main: './examples/main.js'
  }

不能使用htmlwebpackplugin,它会将所有入口都加入html 所以使用CopyPlugin,模版中写死指定examples/main.js

父工程资源复用

这个最为重要,本异步组件要轻量化,不能最后变成一个独立项目 由下面三个截图可以看出,element-ui组件编译后成为一个标签,最终在父工程能被正常识别为element-ui组件

跨域异步加载vue组件

跨域异步加载vue组件

跨域异步加载vue组件