背景
- 产品要求公共功能全站统一
- 工程多、项目复杂,协调困难
- 自建ui库更新频繁时,很多项目不再跟进
- 域名多,跨域复用
最终达到目的,一处修改,全站复用
异步加载组件
- 利用vue组件的异步加载
- script跨域获取资源,缓存到_async_components_对象上
js
import AsyncComponent from './index.vue'
// 为组件提供 install 安装方法,供按需引入
AsyncComponent.install = function (Vue) {
Vue.component(AsyncComponent.name, AsyncComponent)
}
export default AsyncComponent
js
<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组件
js
<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组件如下
js
<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>
效果如下
异步组件工程
- 开发组件,本地调试支持
- 本地查看加载远程资源
- 发布AsyncComponent
js
"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"
},
webpack.conf.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,
},
};
webpack.dev.js
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 本地开发调试时选择加载服务器资源,不是相对引用
js
if (process.env.NODE_ENV === 'development') {
base = location.origin + this.path
}
entry多入口
webpack.conf.js
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';
}
webpack.dev.js
entry: {
main: './examples/main.js'
}
不能使用htmlwebpackplugin,它会将所有入口都加入html 所以使用CopyPlugin,模版中写死指定examples/main.js
父工程资源复用
这个最为重要,本异步组件要轻量化,不能最后变成一个独立项目
由下面三个截图可以看出,element-ui组件编译后成为一个标签,最终在父工程能被正常识别为element-ui组件