Vue動態載入非同步元件
背景:
目前我們專案都是按元件劃分的,然後各個元件之間封裝成產品。目前都是採用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不是通用的,但同樣可以達到打包複用頁面。
原文地址:https://segmentfault.com/a/1190000017059266