视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Vue动态加载异步组件的方法
2020-11-27 22:03:37 责编:小采
文档


背景:

目前我们项目都是按组件划分的,然后各个组件之间封装成产品。目前都是采用iframe直接嵌套页面。项目中我们还是会碰到一些通用的组件跟业务之间有通信,这种情况下iframe并不是最好的选择,iframe存在跨域的问题,当然是postMessage还是可以通信的,但也并非是最好的。目前有这么一个场景:门户需要制作通用的首页和数据概览页面,首页和数据概览页面通过小部件来自由拼接。业务组件在制作的时候只需要提供各个模块小部件的url就可以了,可是如果小部件之间还存在联系呢?那么iframe是不好的。目前采用Vue动态加载异步组件的方式来实现小组件之间的通信。当然门户也要提供一个通信的基线:Vue事件总线(空的Vue实例对象)。

内容:

使用过vue的都应该知道vue的动态加载组件components:

Vue通过is来绑定需要加载的组件。那么我们现在需要的就是如何打包组件,如果通过复制业务组件内部的代码,那么这种就需要把依赖全部找齐,并复制过去(很多情况下会漏下某个图片或css等),这种方式是比较low的,不方便维护。因此我们需要通过webpack来打包单个vue文件成js,这边一个vue打包成一个js,不需压代码分割,css分离。因为component加载时只需要加载一个文件即可。打包文件配置如下:

首先在package.json加入打包命令:

"scripts": {
 ...
 "build-outCMP": "node build/build-out-components.js"
 },

Build-out-components.js文件:

'use strict'
require('./check-versions')()

process.env.NODE_ENV = 'production'

const ora = require('ora')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const webpackConfig = require('./webpack.out-components.prod.conf')

const spinner = ora('building for sync-components...')
spinner.start()

webpack(webpackConfig, function (err, stats) {
 spinner.stop()
 if (err) throw err
 process.stdout.write(stats.toString({
 colors: true,
 modules: false,
 children: false,
 chunks: false,
 chunkModules: false
 }) + '\n\n')

 if (stats.hasErrors()) {
 console.log(chalk.red(' Build failed with errors.\n'))
 process.exit(1)
 }

 console.log(chalk.cyan(' Build complete.\n'))
 console.log(chalk.yellow(
 ' Tip: built files are meant to be served over an HTTP server.\n' +
 ' Opening index.html over file:// won\'t work.\n'
 ))
})

webpack.out-components.prod.conf.js文件配置如下

const webpack = require('webpack');
const path = require('path');
const utils = require('./utils');
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const {entry, mkdirsSync} = require('./out-components-tools')

function resolve(dir) {
 return path.join(__dirname, '..', dir)
}

mkdirsSync(resolve('/static/outComponents'))

module.exports = {
 entry: entry,
 output: {
 path: resolve('/static/outComponents'),
 filename: '[name].js',
 },
 resolve: {
 extensions: ['.js', '.vue', '.json'],
 alias: {
 'vue$': 'vue/dist/vue.esm.js',
 '@': resolve('src'),
 }
 },
 externals: {
 vue: 'vue',
 axios: 'axios'
 },
 module: {
 rules: [
 {
 test: /\.vue$/,
 loader: 'vue-loader',
 options: {
 esModule: false, // vue-loader v13 更新 默认值为 true v12及之前版本为 false, 此项配置影响 vue 自身异步组件写法以及 webpack 打包结果
 loaders: utils.cssLoaders({
 sourceMap: true,
 extract: false // css 不做提取
 }),
 transformToRequire: {
 video: 'src',
 source: 'src',
 img: 'src',
 image: 'xlink:href'
 }
 }
 },
 {
 test: /\.js$/,
 loader: 'babel-loader',
 include: [resolve('src'), resolve('test')]
 },
 {
 test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
 loader: 'url-loader',
 options: {
 limit: 10000,
 name: utils.assetsPath('img/[name].[hash:7].[ext]')
 }
 },
 {
 test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
 loader: 'url-loader',
 options: {
 limit: 10000,
 name: utils.assetsPath('media/[name].[hash:7].[ext]')
 }
 },
 {
 test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
 loader: 'url-loader',
 options: {
 limit: 10000,
 name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
 }
 }
 ]
 },
 plugins: [
 new webpack.DefinePlugin({
 'process.env.NODE_ENV': '"production"'
 }),
 // UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
 new webpack.optimize.UglifyJsPlugin({
 compress: false,
 sourceMap: true
 }),
 // Compress extracted CSS. We are using this plugin so that possible
 // duplicated CSS from different components can be deduped.
 new OptimizeCSSPlugin({
 cssProcessorOptions: {
 safe: true
 }
 })
 ]
};

out-components-tools.js文件配置如下:

const glob = require('glob')
const fs = require('fs');
const path = require('path');
// 遍历要打包的组件
let entry = {}
var moduleSrcArray = glob.sync('./src/out-components/*')
for(var x in moduleSrcArray){
 let fileName = (moduleSrcArray[x].split('/')[3]).slice(0, -4)
 entry[fileName] = moduleSrcArray[x]
}

// 清理文件
function mkdirsSync(dirname) {
 if (fs.existsSync(dirname)) {
 deleteall(dirname)
 return true;
 } else {
 if (mkdirsSync(path.dirname(dirname))) {
 fs.mkdirSync(dirname);
 return true;
 }
 }
}
// 删除文件下的文件
function deleteall(path) {
 var files = [];
 if(fs.existsSync(path)) {
 files = fs.readdirSync(path);
 files.forEach(function(file, index) {
 var curPath = path + "/" + file;
 if(fs.statSync(curPath).isDirectory()) { // recurse
 deleteall(curPath);
 } else { // delete file
 fs.unlinkSync(curPath);
 }
 });
 }
};

exports.entry = entry
exports.mkdirsSync = mkdirsSync

build-out-components是打包的入口文件,webpack.out-components.prod.conf.js是webpack打包的配置文件,out-components-tools.js是工具库,这边是打包的entry自动获取(默认为src/out-components),还有自动删除之前打包的文件。

目前的文件目录为

通过打包生产文件:

在static下outComponents文件夹内的js文件。(最终打包需要打包到dist下面,这边做测试先打包在static文件下,方便后续动态组件ajax获取组件使用)

门户的小部件是通过配置url,和调整布局来生产的。因此业务组件至此已经完成了。只需要提供对门户暴露的url即可。
接下来就是门户这边加载动态组件的实现了。门户这边就相对简单了。看如下图配置:

门户通过component的动态组件来实现加载异步组件,通过ajax请求刚才打包的url,然后实例化函数new Function来赋值给mode(new Function之所以分成2部,是因此效验规则的问题,可忽略)。这样就实现了动态加载异步组件了。门户和业务组件可以各个开发,任何业务开发数据概览,门户都不需要改代码,只需要界面上配置url即可。这个异步加载组件已经结束了。这边门户需要封装一封实现异步组件。父级只需要传入url即可。这边还有个可以优化的是,可以把mode优先缓存,那么不需要每次都去加载请求。如下:

我们可以看到在门户的一个数据概览页面上加载了多个异步组件,那么异步组件之间也是可能存在通信的,这样该如何做呢?因为现在已经不是iframe嵌套了,可以通过监听一个组件,然调用另一个组件的方法,这样确实可以实现平级组件间的通信,但这样势必不可取的,因为一旦这样做了门户必须要根据业务来辅助,修改代码来实现功能。因此这边借用门户来生成vue事件总线(空的vue实例)来实现。

门户代码如下: 在this.$root上挂在一个事件总线:

 created () {
 if (!this.$root.eventBus) {
 this.$root.eventBus = new Vue()
 }
 }

然后业务组件之间就可以根据自己的业务实现通信:

组件一和组件二代码如下:

 <template>
 <div class="test1">
 这是一个外部组件a1
 <hello-word></hello-word>
 </div>
 </template>
 
 <script>
 import helloWord from '../components/HelloWorld'
 export default {
 data () {
 return {
 i: 0
 }
 },
 components: {
 helloWord
 },
 mounted () {
 setInterval(() => {
 this.i++
 if (this.i < 10) {
 this.test()
 }
 }, 1000)
 },
 methods: {
 test () {
 this.$root.eventBus.$emit('childEvent', this.i)
 }
 }
 }
 </script>
 <template>
 <div class="test1">
 这也是外部组件哦
 <div >
 这是a1传来的{{a1}}
 </div>
 </div>
 </template>
 
 <script>
 export default {
 data () {
 return {
 a1: 0
 }
 },
 created () {
 this.$root.eventBus.$on('childEvent', this.change)
 },
 methods: {
 change (i) {
 this.a1 = i
 }
 }
 }
 </script>

业务组件就可以根据this.$root.eventBus和vue上的事件传递($emit, $on)来实现相互的通信。

总结:本篇主要借助vue的动态组件和webpack打包单文件来实现动态加载异步组件,通过vue的事件总线挂载在this.$root上来实现平级组件之间的通信。

拓展方向:这个方式不仅仅可以应用在门户单个页面上的小部件上,同样如果某个项目中的页面文件需要复用时,不想通过代码的复制,同样可以再那个文件配置打包单文件配置,打包出的文件在领一个项目中动态加载出来即可。这种模式与通用组件的install模式是有点类似的,只是这个单文件vue不是通用的,但同样可以达到打包复用页面。

下载本文
显示全文
专题