# vue实战总结

# 1、开发技巧

# 1、attrs和inheritAttrs

通过arrtrs和inheritAttrs实现属性透传:

  • $props:props声明的属性
  • $attrs:非申明props的属性,用于组件封装属性透传,除style和class外,两者依然绑定在根组件上
  • inheritAttrs:根元素不继承父元素设置的属性
<!-- 子组件LinkButton -->
<template>
    <div class="link-button">
        <a :href="linkHref" v-bind="$attrs"><slot></slot></a>
    </div>
</template>
<script>

export default {
    //知识点1:通过arrtrs和inheritAttrs实现属性透传:
    //$props props声明的属性
    //$attrs 非申声明props的属性,用于组件封装属性透传,除去style和class仍然绑定在根组件上
    inheritAttrs: false, //根元素不继承父元素设置属性
    props: {
        linkHref: {
            type: String,
            require: true
        }
    }
}
</script>

<!-- 父组件使用LinkButton -->
<!-- 解析结果:设置inhertAttrs为false时,target、rel属性会透传至a元素上,class会作用在根元素上 -->
<!-- 最终结果:
<div class="button link-button">
    <a href="/" target="_blank" rel="nofollow noreferrer noopener">热点</a>
</div> -->
<link-button linkHref="/" target="_blank" rel="nofollow noreferrer noopener" class="button">热点</link-button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

解析:a标签的属性noopener, noreferrer 及 nofollow,参考资料:noopener等使用 (opens new window)

a标签通常搭配:noopener、noreferre、nofollow属性使用,保证安全:

  • noopener:给链接加上target="_blank"后,目标网页会在新链接中打开,此时新网页可通过window.opener获取到源页面的window对象,存在安全隐患。eg:第三方页面可通过window.opener.href = "xxx" 进行钓鱼。 设置后,第三方页面window.opener=null

  • noreferrer:与noopener类似,同时新开页面中还无法获取 document.referrer 信息, 该信息包含了来源页面的地址。通常 noopener 和 noreferrer 会同时设置, rel="noopener noreferrer"。为了浏览器兼容,老的浏览器不支持noopener

  • nofollow 不要继续爬虫;搜索引擎对页面的权重计算中包含一项页面引用数 (backlinks),告诉搜索引擎, 本次链接不为上述排名作贡献

# 2、异步事件问题

单值异步问题:点击按钮1,再点击按钮2,应该展示2,因为1回调展示了按钮1

  • 方案一:数据结构变更

  • 方案二:提取异步函数,点击按钮1,再点击2时,停止按钮1的事件,发起按钮2的事件

    <template>
    	<button @click="changeMsg(1, 2000)">按钮1</button>
    	<button @click="changeMsg(2)">按钮2</button>
    </template>
    <script>
    export default {
      data(){
        return {
          message: ''
        }
      },
      methods:{
        changeMsg(val, time=100){
          setTimeout(()=>{
            this.message = val
          }, time)
        }
      }
    };
    </script>
    
    <!-- 方案一:数据结构转变 -->
    <template>
    	<button @click="changeMsg(msg.btn1, 1, 2000)">按钮1</button>
    	<button @click="changeMsg(msg.btn2, 2)">按钮2</button>
    </template>
    <script>
    export default {
      data(){
        return {
          message: '',
          msg: {
            btn1: '',
            btn2: ''
          }
        }
      },
      methods:{
        changeMsg(key, value, time=100){
          this.message=key
          setTimeout(()=>{
            key = value
          }, time)
        }
      }
    };
    </script>
    
    <!-- 方案二:提取函数进行防抖处理 -->
    <script>
    export default {
      data(){
        this.timer = null
        return {
          message: ''
        }
      },
      methods:{
        changeMsg(val, time=100){
          clearTimeout(this.timer)
          this.timer = setTimeout(()=>{
            this.message = val
          }, time)
        }
      }
    };
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67

# 2、性能优化

# 1、FP首屏加载优化

# 1、减小vendor大小

可使用大文件分析插件查看占用资源:

// npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}
1
2
3
4
5
6
7
  • cdn加速(剥离外部引用)

引入工程中的所有js、css文件,编译时会打包进vendor.js,vendor太大加载时间较长,影响首屏体验。

解决方法:将外部js、css文件剥离,用资源的形式引用,浏览器可以使用多个异步线程将vendor.js 、外部的js等进行价值,达到加速首开目的。外部文件可以使用cdn资源,或别的服务器资源。

//1、vue.config.js配置
//添加externals,忽略不需要打包的库
//'vue-router':'VueRouter'解析:
// 其中'vue-router'为引入的资源的名字,import vueRouter from 'vue-router'
// 其中'VueRouter'为该模块提供给外部引用的名字,由对应的库自定,可在min.js中查看
configureWebpack: {
  externals:{
     'vue':'Vue',
     'vue-router':'VueRouter',
     'vuex':'Vuex'
  }
}

//2、html引入静态资源
<script src="//cdn.bootcss.com/vue-router/2.3.0/vue-router.min.js"></script>

//3、去掉import引用
//import Router from 'vue-router'
//Vue.use(Router)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 路由懒加载
export default [
    {
        path: "/list",
        name: "List",
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () =>
            import(/* webpackChunkName: "list" */ "@/views/list/List.vue"),
    },
]
1
2
3
4
5
6
7
8
9
10
11
  • 组件按需引用
//1、安装babel-plugin-component
//npm install babel-plugin-component -D

//2、修改babel配置:.babelrc、babel.config.js
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

//3、组件按需使用
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';

Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
 * Vue.use(Button)
 * Vue.use(Select)
 */

new Vue({
  el: '#app',
  render: h => h(App)
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 2、服务器开启gizp

参考资料:Vue项目 webpack优化 compression-webpack-plugin 开启gzip (opens new window)

Gzip是Gun zip的缩写,它将浏览器请求的文件先在服务器端进行压缩,然后传递给浏览器,浏览器解压之后再进行页面的解析工作。在服务端开启Gzip支持后,我们前端需要提供资源压缩包,对于的gzip包,浏览器如果支持gzip,会自动查找有没有gz文件,找到了就加载gz然后本地解压执行。

  • 使用compression-webpack-plugin插件,对每个js和css文件增加输出gz后缀的文件。
  • 配置服务器支持gzip包
//1、安装插件
// npm install --save-dev compression-webpack-plugin

//2、配置插件:生产环境,开启js\css压缩
const CompressionPlugin = require('compression-webpack-plugin')
chainWebpack: config => {
  if (process.env.NODE_ENV === 'production') {
    config.plugin('compressionPlugin').use(new CompressionPlugin({
      test: /\.(js|css)$/, // 匹配文件名
      threshold: 10240, // 对超过10k的数据压缩
      minRatio: 0.8,
      deleteOriginalAssets: false // 删除源文件
    }))
    }
  }
}

//3、nginx服务器配置gzip支持
gzip on;
gzip_static on;
gzip_min_length 1k;
gzip_buffers 4 32k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6].";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 2、非响应式处理

对于从始至终都未改变的死数据,比如写死的下拉框和表格数据,无需进行响应式处理,减少性能消耗。

  • 将数据定义在data之外
  • 使用object.freeze()
data(){
  this.list1 = {xxx}
  this.list2 = {xxx}
  return {
    xxxx
  }
}

data(){
  return {
    list1: Object.freeze({xxx})
    list2: Object.freeze({xxx})
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

解析:Object.freeze() 作用冻结一个对象, 返回和传入的参数相同的对象,一个被冻结的对象再也不能被修改

  • 不能向这个对象添加新的属性,不能删除已有属性

  • 不能修改已有属性的值

  • 不能修改该对象已有属性的可枚举性、可配置性、可写性

  • 冻结一个对象后该对象的原型也不能被修改。

new Vue({
    data: {
      // vue不会对list里的object做getter、setter绑定
      list: Object.freeze([
        { value: 1 },
        { value: 2 }
      ])
    },
    created () {
      // 界面不会有响应
      this.list[0].value = 100;

      // 下面两种做法,界面都会响应
      this.list = [
        { value: 100 },
        { value: 200 }
      ];
      this.list = Object.freeze([
        { value: 100 },
        { value: 200 }
      ]);
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

参考资料:Vue性能提升之Object.freeze() (opens new window)

# 3、工程化实践

# 1、模块解耦

将全局的store、router、mock、i18n公共文件离散化处理剥离在各业务模块内,提高模块的内聚程度,方便功能移植,实现模块解耦

# 1、require.context

使用webpack的api实现代码自动导入,初始化时自动解析导入,实现route、store等解耦拆分

require.context在webpack解析打包时自动执行。

//入参解析
require.context(
  directory,    							 //读取文件的路径
  (useSubdirectories = true),  //是否遍历文件的子目录
  (regExp = /^\.\/.*$/),       //要匹配文件的正则 
  (mode = 'sync')
);

//输出解析
/* 
1、输出为函数 webpackContext(req)
1.1、webpackContext函数,入参为路径,出参为模块,可取出defalut模块
1.2、函数也是对象,可有属性
2、输出的函数有三个属性,keys、id、resolve
2.1、resolve{Function} -接受一个参数request,request为test文件夹下面匹配文件的相对路径,返回这个匹配文件相对于整个工程的相对路径
2.2、keys {Function} -返回匹配成功模块的名字组成的数组
2.3、id {String} -执行环境的id,返回的是一个字符串,主要用在module.hot.accept
*/

//示例:index.js  ./modules/home.js
const modulesFiles = require.context('./modules', true, /\.js$/)

console.log(modulesFiles)
/*
ƒ webpackContext(req) {
	var id = webpackContextResolve(req);
	return __webpack_require__(id);
}
*/

console.dir(modulesFiles)
/*
ƒ webpackContext(req)
id: "./src/store/modules sync recursive \\.js$"
keys: ƒ webpackContextKeys()
resolve: ƒ webpackContextResolve(req)
*/

console.log(modulesFiles.keys())
//["./home.js"]

console.log(modulesFiles.id)
//./src/store/modules sync recursive \.js$

console.log(modulesFiles.resolve('./home.js'))
//./src/store/modules/home.js

console.log(modulesFiles('./home.js'))
/*
Module
default: {namespaced: true, state: {…}, getters: {…}, mutations: {…}}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  • store解耦合
import Vue from "vue";
import Vuex from "vuex";
import actions from "./actions.js"
import state from "./state.js"
import mutations from "./mutations.js"

Vue.use(Vuex);

let store = {
  state,
  actions,
  mutations
}
const modulesFiles = require.context('./modules', true, /\.js$/
const modules = modulesFiles.keys().reduce((modules, modulePath)=>{
  		//['./home.js']  ./home.js -> home
      const moduleName = modulePath.replace(/^\.\/(.*)\.js$/, '$1')
      const module = modulesFiles(modulePath).default
      modules[moduleName] = module
      return modules
}, {})
store = {...store, modules}
export default new Vuex.Store(store);

/*
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
1、total 必需,初始值, 或者计算结束后的返回值
2、currentValue	必需,当前元素
3、currentIndex	可选,当前元素的索引
4、arr	可选,当前元素所属的数组对象
5、initialValue 可选,传递给函数的初始值
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 2、使用register方法

重写vue类,子组件使用重写类,重写beforeCreate和destoryed方法,store、i18n、router通过参数传入后在beforeCreate注入,在destoryed中销毁,实现模块的解耦。

//方法调用
/**
 * 功能:vuee重写vue,注入store、i18n
 * 输入:vuee(option, fieldMap, state)
 * vuee({
 *    data:{
 *    }
 * },
 * {
 *  state: {
 *     'self': ['a', 'b'],
 *     'global': ['c']
 *  },
 *  mutations: {
 *  },
 *  getters: {
 *  },
 *  actions: {
 *  },
 *  {
 *    mName: 'role',
 *    store,
 *    i18nEN,
 *    i18nZH
 *  }
 * }
 * )
 */
import {mapState, mapActions, mapMutations, mapGetters} from 'vuex'

//propsData: 创建实例时传入的props
const getNamespaceProp = data => {
    return (!data || !data.propsData) ? '' : data.propsData.namespace
}

/**
 * store数据的通用map方法
 * @param {*} mName 视图的store模块名称
 * @param {*} mapData 需要映射的字段map
 * @param {*} type 映射的类型,包括state、action、getter和mutation
 * eg: mapExt(name, fieldMap.state, 'state')
 * fieldMap.state三种入参
 * ['a', 'b', 'c']
 * {
 *  state: {
 *     'self': ['a', 'b'],
 *     'global': ['c']
 *  }
 * }
 * {
 *  state: {
 *    'getTime': 'time'
 *  }
 * }
 */
const mapExt = (mName, mapData, type){
    if(!mapData) return {}
    let methodMap = {
        state: mapState,
        action: mapActions,
        mutation: mapMutations,
        getter: mapGetters
    }
    if(Array.isArray(mapData)){
        //mapActions('moduleA', ['getInfo', 'getTime']) //子模块
        //mapGetters(['getInfo', 'getTime'])  全局
        return mName ? methodMap[type](mName, mapData) : methodMap[type](mapData)
    }else if(typeof mapData === 'object'){
        let simpleImport = false
        let ret = {}
        for (let key of Object.keys(mapData)){
            if (typeof mapData[key] === 'string'){
                simpleImport = true
                break
            }else{
                if(key === 'global'){
                    ret = {
                        ...ret,
                        ...methodMap[type](mapData[key], {root: true})
                    }
                }else{
                    ret = {
                        ...ret,
                        ...methodMap[type](key === 'self'? mName : key, mapData[key])
                    }
                }
            }
        }
        if(simpleImport){
            return mName ? methodMap[type](mName, mapData) : methodMap[type](mapData)
        }
        return ret
    }

}

const vuee = (options = {}, fieldMap, state) => {
    let comInstance = { ...options }
    if(state){
        let {mName, store, i18nEN, i18nZH} = state
        let {beforeCreate, destroyed} = options
        let ns = mName || getNamespaceProp(this.$options)
        //namespace属性为true,namespace为ns/,否则为ns
        let namespace = store.namespace ? ns + '/' : ns
        let module = this.$store._modulesNamespaceMap[namespace]

        //重写beforeCreate
        comInstance.beforeCreate = function(){
            if(module){
      //新旧页面切换时,存在引入公用模块相同,新页面会丢失公用模块的数据状态,旧页面destoryed过程会发生在新页面beforeCreate之后
      //通过给模块设置覆盖标记解决,只有模块状态未被覆盖时才允许卸载模块绑定数据
                module.$$overide = true
            }else{
                this.$store.registerModule(ns, store)
            }

            //i18扩展
            let message = this.$i18n.message
            message.en = {...message.en, ...i18nEN}
            message.zh = {...message.en, ...i18nZH}
            this.$i18n.message = message

            beforeCreate && beforeCreate.call(this)
        }

        //重写destoryed
        comInstance.destroyed = function(){
            if(module){
                if(module.$$overide){
                    module.$$overide = false
                }else{
                    this.$store.unregisterModule(ns)
                }
            }else{
                this.$store.unregisterModule(ns)
            }
            destroyed && destroyed.call(this)
        }
    }

    if(fieldMap){
        let name = state ? mName : null
        comInstance.computed = {
            ...mapExt(name, fieldMap.state, 'state'),
            ...mapExt(name, fieldMap.getter, 'getter'),
            ...options.computed
        }
        comInstance.methods = {
            ...mapExt(name, fieldMap.mutations, 'mutation'),
            ...mapExt(name, fieldMap.actions, 'action'),
            ...options.methods
        }
    }

    options = null //释放空间
    return comInstance
}

export default vuee;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

# 3、插件化机制

将公用组件及模块配置成插件包,可通过动态安装插件实现功能扩展,参照umi结构

  • 使用自动注册插件自动安装相关插件包或手动引入
  • 子定义插件
    • npm包形式
      • 创建插件包,以@plugin开头,eg:@plugin/demo
      • 插件开发目录包含src目录,放置es6编写的index.js文件
      • 插件开发目录建立lib目录,放置babel编译过的index.js文件
    • 本地方式:src/utils/plugin内新增插件
//插件接口:xPlugin.init(Vue, router, store, settings)
//一、使用:main.js
import Vue from 'Vue';
import router from './router';
import store from './store';
import settings from './settings';

//1、手动导入
import xPlugin from '@plugin/xPlugin';
xPlugin.init(Vue, router, store, settings);

//2、自动导入:直接使用registerPlugin即可
import registerPlugin from '@plugin/register';
registerPlugin(Vue, router, store, settings); //会自动引入所有插件

//二、插件结构:xPlugin
class xPlugin {
  static init(Vue, router, store, settings){
    this.router(router, store, settings)
    this.store(store, settings)
  }
  static router(router, store, settings){
    ......
  }
  static store(store, settings){
    ......
  }
}

module.exports = xPlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 1、自动注入插件
  • 注册npm包插件:读取@plugin域下面的插件,并调用插件的init()方法执行,完成注册
  • 注册本地插件,执行插件的init()方法
/* 目录结构
-@plugin
	-register
	  -lib
	  -src
	  .babelrc
	  package.json
*/

//.babelrc
{
  "presets": ["es2015"]
}

//.package.json
{
  "main":"/lib/index.js",
  "scripts":{
    "babel": "babel -d lib/ src/"
  },
  "devDependencies":{
    "babel":"^6.x.x",
    "babel-cli":"^6.x.x",
    "babel-preset-es2015":"^6.x.x",
  }
}

// src/index.js
const registerPlugin = (Vue, router, store, settings) => {
  const modulesFiles = require.context('@plugin', true, /index.js$/);
  //注册npm包插件
  modulesFiles.keys().reduce((modules, modulePath) => {
    if(modulePath !== './register/index.js'){
      	const plugin = modulesFiles(modulePath)
        plugin.init(Vue, router, store, settings)
    }
  }, {});
  //注册本地插件
  try{
    const utilFiles = require.context('@/utils/plugin', true, /\.js$/);
  	utilFiles.keys().reduce((modules, modulePath) => {
      	const plugin = utilFiles(modulePath)
        plugin.init(Vue, router, store, settings)
  	}, {}); 
  }catch(e){
    if(e.code !== "MODULE_NOT_FOUND"){
      throw new Error(e)
    }
  }
}

module.exports = registerPlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 2、iframe插件
  • 缓存用iframe标签加载的页面,避免切换路由时重复加载浪费性能
  • 可以使用传参来决定是否刷新当前页面,若不刷新,会缓存上一次组件实例,不毁摧毁
  • 原理
    • 在布局时新建一个id为iframe-root的Dom节点,此节点与右侧内容区是兄弟节点
    • 把访问过的每一个iframe页面都当成一个子节点appendChild到iframe-root上
    • 切换路由时控制iframe页面节点的display属性为block或者none
//1、安装插件:npm install @plugin/iframe

//2、注册插件:main.js
import IframePlugin from '@plugin/iframe'
IframePlugin.init(Vue, router, store, settings);

//3、使用插件
//3.1、添加iframe-root的dom节点: main.vue
<!-- 内包含:router-view元素 -->
<main-app id="main-app"></main-app>
<div id="iframe-root"></div>

//3.2、配置参数: setting.js
export default {
  iframeRoot: "iframe-root", 						//iframe容器标签id,用于append元素
  iframeComponent: "iframeComponent",		//iframe模块路由前缀
  routerView: "main-app",               //路由页面出口dom的id
}

//3.3、组件中调用setIframeUrl方法
methods:{
  showIframe(){
    this.$store.dispatch('iframe/setIframeUrl', {
      url: 'http://www.xxx.com',           //iframe地址
      refresh: false,                      //是否刷新
      oUrl: '/ifremeComponent/iframePage'  //当前路由
    })
  }
}

//4、插件细节,结构同上,src/index.js
class IframePlugin {
  static init(Vue, router, store, settings){
    this.router(router, store, settings)
    this.store(store, settings)
  }
  static router(router, store, settings){
    //router-view和iframe容器相互切换展示
    //命中iframe的路由则展示iframe容器,router-view隐藏
    router.beforeEach((to, from, next)=>{
      const isIframe = to.path.includes(settings.iframeComponent)
      if (from.path.includes(settings.iframeComponent) !== isIframe){
        store.commit('iframe/switchDisplay', isIframe)
      }
      next()
    })
  }
  static store(store, settings){
    const state = {
      iframeSet: []
    }
    const mutations = {
      addIframe: (state, newIframe) => {
        const iframeDom = document.createElement('iframe')
        iframeDom.src = newIframe.url
        iframeDom.oUrl = newIframe.oUrl
        iframeDom.className = 'iframe'
        document.getElementById(settings.iframeRoot).appendChild(iframeDom)
        state.iframeSet.push({
          path: newIframe.url,
          oUrl: newIframe.oUrl
        })
      },
      deleteSingleIframe: (state, view){
      	const iframes = document
      				.getElementById(settings.iframeRoot)
              .getElementByTagName('iframe')
    		for(let i=0; i<iframes.length; i++){ //remove dom
          if(iframes[i].oUrl === view.path){
            iframes[i].parentNode.removeChild(iframes[i])
          }
        }
    		for(const [i, v] of state.iframeSet.entries()){
          if(v.oUrl === view.path){
            state.iframeSet.splice(i, 1)
            break
          }
        }
    	},
      switchIframe: (state, newIframe) => {
        const iframes = document
      				.getElementById(settings.iframeRoot)
              .getElementByTagName('iframe')
        for(let i=0; i< iframes.length; i++){
          if(iframes[i].oUrl === newIframe.oUrl){
            iframes[i].style.display = 'block'
          }else{
            iframes[i].style.display = 'none'
          }
        }
      },
      switchDisplay: (state, isIFrame) => {
        const display = bool => (bool ? 'block' : 'none')
        const iframeRoot = document.getElementById(settings.iframeRoot)
        const routerView = document.getElementById(settings.routerView)
        iframeRoot && (iframeRoot.style.display = display(isIframe))
        routerView && (routerView.style.display = display(!isIframe))
      }
    }
    const actions = {
      setIframeUrl({commit, state}, iframe) {
        //some监测数组中是否有元素符合条件
        const hasIframeUrl = state.iframeSet.some(el => {
          return el.oUrl === iframe.oUrl
        })
        if(!hasIframeUrl){//not exist
          commit('addIframe', iframe)
        }else if(iframe.refresh){//exist, refresh needed
          commit('deleteSingleIframe', {path: iframe.oUrl})
          commit('addIframe', iframe)
        }
        commit('switchIframe', iframe)
      }
    }
    store.registerModule('iframe', {
      namespaced: true,
      state,
      mutations,
      actions
    })
  }
}
module.exports = IframePlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# 3、微前端插件
  • 原理

    • 主应用初始化会加载配置文件解析得到所有子应用文件所在的节点列表
    • 进行功能切换时,主应用根据要跳转的路由在上述节点清单上找到对应子应用的节点入口并加载子应用静态文件,通过解析子应用静态文件通过调用Vuex等插件的api将子应用的信息注册至主应用中的vue实例上
    • 子应用信息注册完成后,主应用会加载对应组件并跳转至相应页面
  • 主应用

    • 使用微前端插件,将加载的子工程的静态资源地址赋值给script标签的src属性并append到html的body上
    • 将主工程的状态、路由、多语言文件挂接至Vue.prototype供子应用使用
// 1、安装插件:npm install @plugin/microfrontendLoad

// 2、配置及加载插件:main.js、settings.js、microFrontEndConfig.js
// 2.1、main.js
import microfrontendLoad from '@plugin/microfrontendLoad'
microfrontendLoad.init(Vue, router, store, settings);
// 2.2、settings.js
import i18n from '@/lang'
import request from './utils/request'
import Message from 'element-ui/packages/message/src/main'
import {MICROAPPCONFIG} from './microFrontEndConfig'
export default {
  microfrontentConfig: {
    i18n: i18n,
    request: request,
    MICRO_APP_CONFIG: MICROAPPCONFIG
  }
}
// 2.3、microFrontEndConfig.js
window._baseUrl = ''
const MICROAPPCONFIG = {
  devMenu: [
    {
      id: 'childA',   //子应用唯一标识,用于避免重复加载资源
      name: 'childA',
      path: '/childA', //子应用路由前缀
      urlPath: '/app.js', //子应用入口文件
      origin: 'http://localhost:8081' 
      //子应用静态资源所在路径,通常配合nginx反向代理使用,使整体项目处于同一域名
    }
  ],//开发环境
  normalMenu: [
    {
      id: 'childA',
      name: 'childA',
      path: '/childA',
      urlPath: '/app.js',
      origin: window._baseUrl + '/childA/js'
    }
  ] //生产环境,同上
}
export {MICROAPPCONFIG}

// 3、插件结构同上,/src/index.js
function appendScript(id, url){
  const script = document.createElement('script')
  script.src = url + '?t=' + new Date().getTime()
  script.id = id
  document.body.appendChild(script)
}

class microFrontendLoad {
  static init(Vue, router, store, settings){
    this.mount(Vue, router, store, settings)
    const config = this.loadChild(Vue, router, store, settings)
    config.loadAll()
  }
  //挂载至全局供子应用使用
  static mount(Vue, router, store, settings){
    const {i18n, request} = settings.microfrontendConfig
    Vue.prototype._router = router
    Vue.prototype._store = store
    Vue.prototype._i18n = i18n
    Vue.prototype.$request = request
  }
  static loadChild(Vue, router, store, settings){
    const {MICRO_APP_CONFIG} = settings.microfrontendConfig
    const online = process.env.NODE_ENV === 'producting'
    const devMenu = MICRO_APP_CONFIG.devMenu
    const normalMenu = MICRO_APP_CONFIG.normalMenu
    const config = {
      menu: online ? normalMenu : devMenu,
      current: null,
      loadQueen: {},
      load: function(item = this.current){
        item = item || this.menu[0]
        if(!item){
          console.log("当前path,未匹配到路由菜单")
          return
        }
        this.current = item
        //在主项目或非当前子项目 启动时才需要动态加载
        const isCurrentProject = location.origin === item.origin
        if(isCurrentProject) return;
        if(!this.loadQueen[item.id]){
          this.loadQueen[item.id] = true
          appendScript(item.id, item.origin + item.urlPath)
        }
      },
      loadAll: function(){
        this.menu.map(item => thi.load(item))
      },
      getCurrent: function(){
        this.current = this.menu.find(x => x.path === location.hash)
        return this.current
      },
      addWatch: function(){
        router.beforeEach((to, from, next) => {
          const item = this.menu.find(x => to.path.indexOf(x.path) !== -1)
          //当访问路径属于某个子工程并且未加载时
          if(item && !this.loadQueen[item.id]){
            this.load(item)
          }
          if(to.matched.length === 0){
            console.log("该功能正在加载或暂不可用,请稍后再试")
            next(false)
          }else{
            next()
          }
        })
      }
    }
    
    config.getCurrent()
    window.addEventListener("DOMContentLoaded", ()=>{
      config.addWatch()
    })
    return config
  }
}
module.exports = microFrontendLoad;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
  • 子应用:通过Vue.prototype注册信息,包括状态、多语言、路由
Vue.prototype._router.addRoutes([...])
Vue.prototype._store.registerModule(appName, module)
Vue.prototype._i18n.mergeLocaleMessage(key, lang[key])
1
2
3
  • 应用间通讯
    • 单页面应用只有一个Vue实例,所以应用间通讯依赖Vue的原型
    • 对于主应用和子应用,运行时共享页面的location、window、localStorage等全局对象,可通过这些来实现

# 2、mock方案

参考资料1:vue中mock使用 (opens new window)

参考资料2:axios和mock封装使用 (opens new window)

# 1、mock.js

开发环境使用mock.js库,对请求进行拦截,无实际请求内容。

Mock.mock( rurl?, rtype?, template ) )
Mock.mock( rurl, rtype, function( options ) )

/* 参数解析
1、rurl 可选
表示要拦截的url,可以使字符串,也可以是正则

2、rtype 可选
表示要拦截的ajax请求方式,如get、post

3、template 可选
数据模板,可以是对象也可以是字符串

4、function(option) 可选
表示用于生成响应数据的函数
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//main.js
if(process.env.NODE_ENV === 'development'){
  require("../mock/mock.js")
}

//mock.js
import Mock from "mockjs"

const mocks = require.context("@/views", true, /.+\/mock\/.*\.js/);
mocks.keys().map((mockPath)=>{
    const mock = mocks(mockPath).default
    Mock.mock(mock.url, mock.method, mock.result);
})

// @/views/../mock/index.js
const listData = [
    {
        date: "2016-05-02",
        name: "王小虎",
        address: "上海市普陀区金沙江路 1518 弄",
    },
    {
        date: "2016-05-04",
        name: "王小虎",
        address: "上海市普陀区金沙江路 1517 弄",
    },
    {
        date: "2016-05-01",
        name: "王小虎",
        address: "上海市普陀区金沙江路 1519 弄",
    },
    {
        date: "2016-05-03",
        name: "王小虎",
        address: "上海市普陀区金沙江路 1516 弄号",
    },
]

export default {
    url: '/user/info',
    method: 'get',
    result: () => ({
        code: 0,
        info: listData
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# 2、后端mock

  • 可对接第三方mock服务
  • 使用devserver的before进行数据mock:beforeafter 配置用于在 webpack-dev-server 定义额外的中间件
    • before 在 webpack-dev-server 静态资源中间件处理之前,可以用于拦截部分请求返回特定内容,或者实现简单的数据 mock
    • after 在 webpack-dev-server 静态资源中间件处理之后,比较少用到,可以用于打印日志或者做一些额外处理
    • before和after参数:function (app, server, compiler)

思路:

  1. webpack-dev-server本质是一个express服务器,使用原生before勾子注册路径实现数据mock,使用chokidar进行数据检测实现热更新加载。
  2. before勾子中使用webpack-api-mocker中间件实现数据mock和热更新。
/*
apiMocker(app, mockerFilePath[, options])
apiMocker(app, Mocker[, options])
*/

//vue.config.js
before: (app) => {
  apiMocker(app, path.resolve('./mock/mock-server.js'))
}

//mock-server.js
module.exports = {
		'GET /api/login': {
        success: appData.login.success,
        message: appData.login.message
    },
    'GET /api/list': [{
            id: 1,
            username: 'kenny',
            sex: 6
        },
        {
            id: 2,
            username: 'kenny',
            sex: 6
        }
    ],
    'POST /api/post': (req, res) => {
        res.send({
            status: 'error',
            code: 403
        });
    },
    'DELETE /api/remove': (req, res) => {
        res.send({
            status: 'ok',
            message: '删除成功!'
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Last Updated: 11/10/2021, 8:09:16 PM