Commit e2c9da65 authored by 郭铭瑶's avatar 郭铭瑶 🤘

多实例

parent f0fd10d5
{
"presets": [
["env", {
"modules": false,
"useBuiltIns": "entry",
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": [
"transform-vue-jsx",
"transform-runtime",
["import", {
"libraryName": "ant-design-vue",
"LibraryDirectory": "es",
"style": "css"
}]
]
}
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
/build/
/config/
/dist/
/*.js
module.exports = {
// 当前文件为eslint的根配置文件
root: true,
// 解析器
parserOptions: {
parser: 'babel-eslint',
},
// 运行环境
env: {
browser: true,
},
// 规则继承,vue主要的额外规则是v-if等指令的检测
extends: ['plugin:vue/essential'],
// 使用eslint-plugin-vue插件帮忙检测.vue文件的代码
plugins: [
'vue'
],
/**
* 自定义规则
* "off" 或 0 : 关闭规则
* "warn" 或 1 : 触犯规则为警告(不会中止程序)
* "error" 或 2 : 触犯规则为错误(触发时会中止程序)
* 额外选项可以通过数组字面量指定,例子如下:
* "quotes": ["error", "single"]
*/
rules: {
"no-debugger": process.env.NODE_ENV === 'production' ? 2 : 0, // 生产环境禁止debugger
"no-console": process.env.NODE_ENV === 'production' ? 2 : 0, // 生产环境禁止console
"no-alert": process.env.NODE_ENV === 'production' ? 2 : 0, // 生产环境禁止alert
"no-shadow-restricted-names": 2, // 禁用关键字及保留字等
"dot-notation": 1, // 尽可能使用 . 来访问对象属性
"no-multi-spaces": 1, // 禁止使用多个空格
"brace-style": 1, // 大括号风格 - one true brace style
"no-var": 1, // 禁用var声明
"no-new-object": 1, // 禁止new Object
"no-array-constructor": 1, // 禁止new Array
"prefer-const": 1, // 要求使用 const 声明那些声明后不再被修改的变量
"prefer-destructuring": 1, // 优先使用数组和对象解构
"no-param-reassign": 1, // 禁止在函数中对函数参数重新赋值
"no-extra-semi": 1, // 禁用不必要的分号
"no-unused-vars": 1, // 禁止已声明但未使用的变量
"indent": [1, 2], // 使用2个空格缩进
"no-multiple-empty-lines": [1, {max: 1}], // 禁止连续出现2个及以上空行
"default-case": 1, // 要求switch语句必须有default分支
"key-spacing": [1, {"beforeColon": false, "afterColon": true}], // 冒号前不要空格,后需要空格
"comma-spacing": [1, {"before": false, "after": true}], // 逗号前不要空格,后需要空格
"arrow-spacing": [1, {"before": true, "after": true}], // 箭头函数中的箭头前后需要留空格
"quotes": [1, "single"], // 字符串使用单引号
"semi": [1, "never"], // 禁止使用分号
"linebreak-style": 0, // 强制使用一样的换行符风格
}
}
.DS_Store
node_modules/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}
MIT License
Copyright (c) 2020 Max Kwok
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# micro-frontend # 微前端项目用模板
微前端项目用模板 ---
\ No newline at end of file
> ## 主项目需自行定义、设置的地方
### 1. [main.js](/src/main.js)中的projects子项目信息(子项目生产地址和标识等)
### 2. [App.vue](/src/App.vue)中的getRouteMenu获取侧边路由菜单的方法
### 3. [/config/index.js](/config/index.js)中的build.assetsPublicPath设置为主项目的实际生产地址
<i style="color:red"> 记得主项目需将/config/index.js中的build.assetsPublicPath修改为主项目的实际生产地址,否则生产环境下在子项目刷新页面会空白。</i>
---
> ## 子项目接入须知
### 1. 建立子项目唯一标识,下面都将以'child'为例
### 2. 在/build/webpack.base.conf.js文件中的output对象添加如下配置:
```javascript
output: {
library: 'child',
libraryTarget: 'umd',
// 原来的配置项此处省略了,别删掉了
}
```
### 3. 在/router/index.js文件中的router实例添加如下配置:
```javascript
new Router({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? "/child/" : "/",
// 原来的配置项此处省略了,别删掉了
})
```
### 4. 在项目入口文件/src/main.js中引用[generate.js](/src/generate.js),并暴露出3个生命周期函数:
```javascript
// ... 上面的代码此处省略了,别删掉了 ...
// 原来的代码
/* eslint-disable no-new */
// new Vue({
// el: '#app',
// router,
// store,
// components: { App },
// template: '<App/>'
// })
// 替换为如下代码
import generate from './generate'
const {bootstrap, mount, unmount} = generate()
export {bootstrap, mount, unmount}
```
### 5. 如需要tab切换多实例页面的模式,generate方法需传参keepAlive,同时router-view需设置keep-alive包裹
```javascript
const {bootstrap, mount, unmount} = generate({keepAlive: true})
```
```html
<keep-alive>
<!-- 此处的key必不可少 -->
<router-view :key="$route.fullPath"/>
</keep-alive>
```
---
## Build Setup
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
```
For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
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'
))
})
})
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js',
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
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]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
// '/api': {
// target: 'http://localhost:7771',
// changOrigin: true,
// pathRewrite: {
// '^/api': ''
// }
// }
},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: 'http://scsd.tao.com/', // 设置生产地址,否则生产环境下在子项目刷新页面会空白
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}
'use strict'
module.exports = {
NODE_ENV: '"production"'
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>micfrontend-template</title>
</head>
<body>
<div id="portal"></div>
<!-- built files will be auto injected -->
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "micfrontend-template",
"version": "1.0.0",
"description": "微前端项目用模板",
"author": "GuoMingyao <missgmy@yahoo.com>",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --fix --ext .js,.vue src",
"build": "node build/build.js"
},
"dependencies": {
"ant-design-vue": "^1.4.3",
"axios": "^0.19.2",
"babel-polyfill": "^6.26.0",
"js-cookie": "^2.2.1",
"qiankun": "^2.0.16",
"qs": "^6.9.3",
"vue": "^2.5.2",
"vue-router": "^3.0.1",
"vuex": "^3.1.3"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-import": "^1.13.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eslint": "^4.15.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
<template>
<div id="root">
<Loader :value="$store.state.showLoading"/>
<router-view />
</div>
</template>
<script>
import Loader from '@/components/Layout/loader'
export default {
name: 'App',
components: {
Loader,
},
mounted() {
/** 持久化存储vuex 使其页面刷新后数据不丢失 (根据需求放开注释)*/
//在页面加载时读取sessionStorage里的状态信息
// if (sessionStorage.getItem('VuexStore')) {
// this.$store.replaceState(Object.assign({}, this.$store.state, JSON.parse(sessionStorage.getItem('VuexStore'))))
// }
// //在页面刷新时将vuex里的信息保存到sessionStorage里
// window.addEventListener('beforeunload', () => {
// sessionStorage.setItem('VuexStore', JSON.stringify(this.$store.state))
// })
this.getRouteMenu() //模拟异步获取路由列表形成侧边栏菜单
},
beforeDestroy() {
window.removeEventListener('beforeunload', () => {
sessionStorage.setItem('VuexStore', JSON.stringify(this.$store.state))
})
},
methods: {
getRouteMenu() {
const addRoutes = (data) => {
const {routes} = this.$router.options
const parent = routes.find(item => item.name === 'Layout')
parent.children.push(...data)
this.$router.addRoutes([parent])
}
if(this.$store.state.routes.length > 0) {
addRoutes(this.$store.state.routes)
return
}
// 模拟动态获取路由
setTimeout(() => {
const res = [
{
path: '/aaa',
name: 'aaa',
meta: {
title: 'aaa项目',
},
children: [
{
path: '/aaa/login',
name: 'aaa-login',
meta: {
title: '登录'
}
},
{
path: '/aaa/table',
name: 'aaa-table',
meta: {
title: '列表页'
}
},
{
path: '/aaa/table1',
name: 'aaa-table1',
meta: {
title: '列表页1'
}
},
{
path: '/aaa/table2',
name: 'aaa-table2',
meta: {
title: '列表页2'
}
},
{
path: '/aaa/table3',
name: 'aaa-table3',
meta: {
title: '列表页3'
}
},
{
path: '/aaa/table4',
name: 'aaa-table4',
meta: {
title: '列表页4'
}
},
{
path: '/aaa/table5',
name: 'aaa-table5',
meta: {
title: '列表页5'
}
},
{
path: '/aaa/table6',
name: 'aaa-table6',
meta: {
title: '列表页6'
}
},
{
path: '/aaa/table7',
name: 'aaa-table7',
meta: {
title: '列表页7'
}
},
{
path: '/aaa/table8',
name: 'aaa-table8',
meta: {
title: '列表页8'
}
},
{
path: '/aaa/table9',
name: 'aaa-table9',
meta: {
title: '列表页9'
}
},
{
path: '/aaa/table10',
name: 'aaa-table10',
meta: {
title: '列表页10'
}
},
{
path: '/aaa/table11',
name: 'aaa-table11',
meta: {
title: '列表页11'
}
},
]
},
{
path: '/bbb',
name: 'bbb',
meta: {
title: 'bbb项目',
},
children: [
{
path: '/bbb/bindPhone',
name: 'bbb-bindPhone',
meta: {
title: '绑定手机'
}
},
{
path: '/bbb/register',
name: 'bbb-register',
meta: {
title: '注册'
}
},
]
},
]
this.$store.commit('setRoutes', res)
addRoutes(res)
}, 3000)
}
},
}
</script>
<style>
#root {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 100%;
height: 100%;
overflow: hidden;
}
body {
overflow: hidden;
}
</style>
<template>
<a-breadcrumb>
<a-breadcrumb-item v-for="item in list" :key="`bread-crumb-${item.name}`">
<router-link v-if="item.to" :to="item.to">
{{showTitle(item)}}
</router-link>
<span v-else>{{showTitle(item)}}</span>
</a-breadcrumb-item>
</a-breadcrumb>
</template>
<script>
export default {
name: 'BreadCrumb',
computed: {
list() {
return this.$store.state.breadCrumbList
}
},
methods: {
showTitle(item) {
return (item.meta && item.meta.title) || item.name
},
},
}
</script>
<template>
<a-locale-provider :locale="zh_CN">
<a-layout id="layout">
<a-layout-sider v-model="collapsed" :trigger="null" breakpoint="lg" collapsible>
<div class="logo" />
<SideMenu ref="sideMenu"/>
</a-layout-sider>
<a-layout>
<a-layout-header class="layout-head">
<div>
<a-icon class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="collapsed = !collapsed" />
<BreadCrumb />
</div>
<div>
<a-badge :count="0" showZero><a><a-icon type="bell" /></a></a-badge>
<a-dropdown class="person-center">
<span>
<a-icon type="user" />
<span class="name">用户名</span>
<a-icon type="down" />
</span>
<a-menu slot="overlay" @click="onUserSelect">
<a-menu-item key="/info">用户信息</a-menu-item>
<a-menu-item key="/logout">退出登录</a-menu-item>
</a-menu>
</a-dropdown>
</div>
</a-layout-header>
<div class="tag-nav-wrapper">
<TagsNav :value="$route" @select="turnToPage" :list="tagNavList" @on-close="handleCloseTag"/>
</div>
<a-layout-content class="layout-content">
<!-- 子项目在此加载 -->
<div v-show="contentIsReady" id="contentView"/>
<template v-show="!contentIsReady">
<!-- 传统子项目在iframe中渲染 -->
<iframe v-if="webviewSrc" :src="webviewSrc" frameborder="0" style="width:100%;height:96%;overflow:hidden;" />
<!-- 本项目的子页面在此渲染 -->
<router-view v-else />
</template>
</a-layout-content>
</a-layout>
</a-layout>
</a-locale-provider>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
import SideMenu from './sidemenu'
import TagsNav from './tags-nav'
import BreadCrumb from './bread-crumb'
import zh_CN from 'ant-design-vue/lib/locale-provider/zh_CN'
import {routeEqual, getNewTagList, checkRouteChange, isInRoutes, getNextRoute} from '@/libs/util'
export default {
name: 'Layout',
components: {
SideMenu,
TagsNav,
BreadCrumb,
},
computed: {
...mapState([
'routes',
'contentIsReady',
'webviewSrc',
'tagNavList',
'homeRoute',
'instanceCollection',
])
},
data() {
return {
zh_CN,
collapsed: false,
}
},
mounted() {
this.init()
window.addEventListener('beforeunload', () => checkRouteChange(this.$route, window.location)) // 刷新页面时候检查路由和真实地址是否有变化
},
beforeDestroy() {
window.removeEventListener('beforeunload', () => checkRouteChange(this.$route, window.location))
},
methods: {
...mapMutations([
'addTag', // 添加标签
'setTagNavList', // 设置标签列表
'setBreadCrumb', // 设置面包屑列表
'setInstanceCollection', // 存储子项目beforeRouteEnter提交的销毁方法
]),
init() {
this.setTagNavList()
this.addTag({
route: this.homeRoute
})
this.setBreadCrumb(this.$route)
this.$global_state.onGlobalStateChange(state => {
// 监听子项目beforeRouteEnter时提交的组件销毁方法,并存入state
const {instanceInfo} = state
if (!instanceInfo) return
const {path} = instanceInfo
if (!path) return
this.setInstanceCollection({...this.instanceCollection, [path]: instanceInfo.destroyFn})
this.$global_state.setGlobalState({instanceInfo: null})
}, true)
},
onUserSelect({key}) {
this.$router.push({
path: key,
})
},
turnToPage(route) { // 点击标签跳转页面
if (typeof route === 'string') {
this.$router.push({name: route})
} else if (route._jump) { // 标签路由发生改变则重定向
this.$router.push({path: route._jump})
} else {
const {name, params, query} = route
this.$router.push({
name,
params,
query,
})
}
},
handleCloseTag(res, type, route) { // 关闭标签
if (routeEqual(this.$route, route)) { // 关闭的标签是当前激活中的标签
this.destroyTagPage(res, route, window.location.pathname + window.location.search) // 执行标签页面的的卸载
this.turnToPage(getNextRoute(this.tagNavList, route)) // 关闭触发中的标签页面则推至相邻标签页
} else {
this.destroyTagPage(res, route, null) // 执行标签页面的的卸载
}
this.setTagNavList(res)
},
destroyTagPage(tagNavs, {path, _jump}, location) {
Object.keys(this.instanceCollection).forEach(key => {
if ((path && path.includes(key)) || (_jump && _jump.includes(key)) || (location && location.includes(key))) {
const [node, destroyComponent, routerBase, destroyInstance] = this.instanceCollection[key] && this.instanceCollection[key]()
if (node && node.data.keepAlive) {
if (node.parent && node.parent.componentInstance && node.parent.componentInstance.cache) {
if (node.componentInstance) {
const nodeKey = node.key || (node.componentOptions.Ctor.cid + (node.componentOptions.tag ? `::${node.componentOptions.tag}` : ''))
const {cache, keys} = node.parent.componentInstance
if (cache[nodeKey]) {
if (keys.length) {
const index = keys.indexOf(nodeKey)
if (index >= 0) {
keys.splice(index, 1)
}
}
delete cache[nodeKey]
}
}
}
}
destroyComponent() // 清除页面组件实例
delete this.instanceCollection[key]
if (tagNavs.every(tag => !tag.path.startsWith(routerBase))) {
// 如果标签中已没有该子项目的任何页面,则清除整个子项目的实例
destroyInstance()
}
}
})
},
},
watch: {
'$route': {
handler(newRoute) { // 根据路由变化设置标签列表、面包屑、侧边菜单的选择状态
const {name, query, params, meta, path} = newRoute
/**
* 如果路径在路由列表中存在才添加标签
* (否则为没有配置在主项目的子项目的私有页面,例如详情页,则不再新开标签,继续在原标签页跳转显示)
*/
if (isInRoutes(path)) {
this.addTag({
route: {name, query, params, meta, path},
type: 'push',
})
}
this.setBreadCrumb(newRoute)
this.setTagNavList(getNewTagList(this.tagNavList, newRoute))
this.$refs.sideMenu.updateOpenKey(newRoute.name)
}
}
},
}
</script>
<style>
#contentView,
#contentView > div {
width: 100%;
height: 100%;
}
</style>
<style scoped>
#layout {
width: 100%;
height: 100%;
}
#layout .trigger {
font-size: 18px;
cursor: pointer;
transition: color 0.3s;
margin-right: 20px;
}
#layout .trigger:hover {
color: #1890ff;
}
#layout .logo {
height: 32px;
background: rgba(255, 255, 255, 0.2);
margin: 16px;
}
#layout .layout-head {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
padding: 0 1.5rem;
box-shadow: 0 0 10px 0 rgba(0,0,0,.5);
z-index: 1;
}
#layout .layout-head > div {
display: flex;
align-items: center;
}
#layout .layout-head .person-center {
margin-left: 20px;
}
#layout .layout-head .person-center .name {
margin: 0 10px;
}
#layout .layout-content {
height: 100%;
margin: 14px;
padding: 14px;
overflow-y: auto;
background: #fff;
}
.tag-nav-wrapper {
padding: 10px 0 0;
height: 40px;
}
</style>
<template>
<div class="loader" v-show="value">
<a-spin :tip="msg" :spinning="value" size="large"/>
</div>
</template>
<script>
export default {
name: 'Loader',
props: {
msg: {
type: String,
default: '加载中...',
},
value: {
type: Boolean,
default: false,
}
},
}
</script>
<style scoped>
.loader {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(255,255,255,0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
</style>
<template>
<a-menu
class="layout-side-menu"
mode="inline"
theme="dark"
:openKeys="openKeys"
v-model="selectedKeys"
@openChange="onOpenChange"
@select="onSelect"
>
<template v-if="menus && menus.length > 0">
<a-sub-menu v-for="menu in menus" :key="menu.key">
<span slot="title"><a-icon :type="menu.icon" /><span>{{menu.title}}</span></span>
<template v-if="menu.children && menu.children.length > 0">
<a-menu-item v-for="child in menu.children" :key="child.key">{{child.title}}</a-menu-item>
</template>
</a-sub-menu>
</template>
</a-menu>
</template>
<script>
import {homeName, isInRoutes} from '@/libs/util.js'
export default {
name: 'SideMenu',
data() {
return {
openKeys: [],
selectedKeys: [],
menus: [],
}
},
methods: {
// 点击菜单,收起其他展开的菜单
onOpenChange(keys) {
if (keys.length >= 2) {
this.openKeys = keys.slice(1)
} else {
this.openKeys = keys
}
},
// 更新打开及选中的菜单
updateOpenKey(name) {
if (name === homeName) this.openKeys = []
else if (!isInRoutes(this.$route.path)) {
const result = this.$store.state.tagNavList.find(tag => tag._jump && tag._jump.startsWith(this.$route.path))
this.openKeys = result._matched.map(item => item.name).filter(item => item !== name)
this.selectedKeys = [result.name]
} else {
this.openKeys = this.$route.matched.map(item => item.name).filter(item => item !== name)
this.selectedKeys = [name]
}
},
onSelect({key}) {
const result = this.$store.state.tagNavList.find(tag => tag.name === key)
if (result && result._jump) {
this.$router.push({
path: result._jump,
})
return
}
this.$router.push({
name: key,
})
}
},
computed: {
notInSideMenus() { // 不需要在侧边栏展示的路由
const {routes} = this.$router.options
const parent = routes.find(item => item.name === 'Layout')
return parent.children.filter(route => !route.children).map(item => item.name)
},
},
watch: {
$route(cur) {
if (this.notInSideMenus.indexOf(cur.name) >= 0) {
this.openKeys = []
this.selectedKeys = []
}
},
'$store.state.routes': { // 根据动态获取的路由生成菜单
handler(cur) {
this.menus = cur.map(route => {
if (route.children && route.children.length > 0) {
return {
key: route.name,
icon: route.meta.icon || 'folder',
title: route.meta.title,
children: route.children.map(child => {
return {
key: child.name,
title: child.meta.title,
}
})
}
}
})
},
immediate: true,
}
}
}
</script>
<style>
.layout-side-menu {
height: 90%;
overflow-y: auto;
scrollbar-arrow-color: #00284e; /*三角箭头的颜色*/
scrollbar-face-color: #00284e; /*立体滚动条的颜色(包括箭头部分的背景色)*/
scrollbar-3dlight-color: #00284e; /*立体滚动条亮边的颜色*/
scrollbar-highlight-color: #00284e; /*滚动条的高亮颜色(左阴影?)*/
scrollbar-shadow-color: #00284e; /*立体滚动条阴影的颜色*/
scrollbar-darkshadow-color: #00284e; /*立体滚动条外阴影的颜色*/
scrollbar-track-color: #000c17; /*立体滚动条背景颜色*/
scrollbar-base-color:#00284e; /*滚动条的基色*/
}
/* 设置滚动条的样式 */
.layout-side-menu::-webkit-scrollbar {
width: 10px;
}
/* 滚动条滑块 */
.layout-side-menu::-webkit-scrollbar-thumb {
background:#00284e;
}
</style>
<template>
<div class="tags-nav">
<div class="btn-con left-btn">
<a-button icon="left" size="small" @click="setScroll(240)"/>
</div>
<div class="btn-con right-btn">
<a-button icon="right" size="small" @click="setScroll(-240)"/>
</div>
<div class="scroll-outer" ref="scrollOuter" @DOMMouseScroll="handleScroll" @mousewheel="handleScroll">
<div class="scroll-body" ref="scrollBody" :style="{left: `${tagBodyLeft}px`}">
<transition-group name="tagList-moving-animation">
<a-tag
v-for="(item,index) in list"
:key="`tag-nav-${index}`"
ref="tagsPageOpened"
:name="item.name"
:data-route-item="item"
@close.stop="handleClose(item)"
@click.native.stop="handleClick(item)"
:closable="item.name !== 'home'"
:color="isCurrentTag(item) ? 'blue' : null"
>
{{showTitleInside(item)}}
</a-tag>
</transition-group>
</div>
</div>
</div>
</template>
<script>
import {routeEqual} from '@/libs/util.js'
export default {
name: 'TagsNav',
props: {
value: Object,
list: {
type: Array,
default() {
return []
}
}
},
data() {
return {
tagBodyLeft: 0,
outerPadding: 4,
}
},
mounted() {
setTimeout(() => this.getTagElementByName(this.$route), 200)
},
computed: {
currentRouteObj() {
const {name, params, query, path} = this.value
return {name, params, query, path}
},
},
methods: {
handleScroll(e) {
const {type} = e
let delta = 0
if (type === 'DOMMouseScroll' || type === 'mousewheel') {
delta = (e.wheelDelta) ? e.wheelDelta : -(e.detail || 0) * 40
}
this.setScroll(delta)
},
setScroll(offset) {
const outerWidth = this.$refs.scrollOuter.offsetWidth
const bodyWidth = this.$refs.scrollBody.offsetWidth
if (offset > 0) {
this.tagBodyLeft = Math.min(0, this.tagBodyLeft + offset)
} else {
if (outerWidth < bodyWidth) {
if (this.tagBodyLeft < -(bodyWidth - outerWidth)) {
this.tagBodyLeft = this.tagBodyLeft
} else {
this.tagBodyLeft = Math.max(this.tagBodyLeft + offset, outerWidth - bodyWidth)
}
} else {
this.tagBodyLeft = 0
}
}
},
handleClose(route) {
const res = this.list.filter(item => !routeEqual(route, item))
this.$emit('on-close', res, undefined, route)
},
handleClick(item) {
if (this.isCurrentTag(item)) return
this.$emit('select', item)
},
isCurrentTag(item) {
return routeEqual(this.currentRouteObj, item)
},
showTitleInside(item) {
return (item.meta && item.meta.title) || item.name
},
getTagElementByName (route) {
this.$nextTick(() => {
const refsTag = this.$refs.tagsPageOpened
refsTag.forEach((item, index) => {
if (routeEqual(route, item.$attrs['data-route-item'])) {
const tag = refsTag[index].$el
this.moveToView(tag)
}
})
})
},
moveToView (tag) {
const outerWidth = this.$refs.scrollOuter.offsetWidth
const bodyWidth = this.$refs.scrollBody.offsetWidth
if (bodyWidth < outerWidth) {
this.tagBodyLeft = 0
} else if (tag.offsetLeft < -this.tagBodyLeft) {
// 标签在可视区域左侧
this.tagBodyLeft = -tag.offsetLeft + this.outerPadding
} else if (tag.offsetLeft > -this.tagBodyLeft && tag.offsetLeft + tag.offsetWidth < -this.tagBodyLeft + outerWidth) {
// 标签在可视区域
this.tagBodyLeft = Math.min(0, outerWidth - tag.offsetWidth - tag.offsetLeft - this.outerPadding)
} else {
// 标签在可视区域右侧
this.tagBodyLeft = -(tag.offsetLeft - (outerWidth - this.outerPadding - tag.offsetWidth))
}
},
},
watch: {
$route(to) {
this.getTagElementByName(to)
},
}
}
</script>
<style>
.tags-nav .ant-tag {
display: inline-block !important;
height: 26px;
line-height: 26px;
border-radius: 2px;
user-select: none;
}
</style>
<style scoped>
.tags-nav {
position: relative;
border-top: 1px solid #F0F0F0;
border-bottom: 1px solid #F0F0F0;
}
.scroll-outer {
position: absolute;
left: 30px;
right: 61px;
top: 0;
bottom: 0;
box-shadow: 0 0 3px 2px rgba(100,100,100,.1) inset;
}
.scroll-body {
height: calc(100% - 1px);
display: inline-block;
padding: 0 4px;
position: absolute;
overflow: visible;
white-space: nowrap;
transition: left .3s ease;
}
.btn-con {
position: absolute;
top: 1px;
background: #fff;
z-index: 10;
height: calc(100% - 1px);
}
.btn-con button {
display: flex;
align-items: center;
justify-content: center;
border-radius: 0;
border: none;
box-shadow: 0 0 10px 0 rgba(0,0,0,0.2);
}
.btn-con.left-btn {
left: 0;
}
.btn-con.right-btn{
right: 0
}
</style>
/** Just For 微前端子项目 By Guo
* 目标是实现多实例切换的基础上尽量减少对子项目代码的侵入性修改
*/
import Vue from 'vue'
import App from './App'
import store from './store'
import router from '@/router/index'
/**
* @description 微前端子项目构造函数
* @param {Boolean} keepAlive 是否缓存(开启后适用于tab切换多实例页面)
* @param {String} el 实例化的容器
*/
export default ({keepAlive = false, el = '#app'} = {}) => {
/* eslint-disable no-new */
let instance = null
let setState = null
if (keepAlive) {
const mixin = {
beforeRouteEnter: function(to, from, next) {
next(vm => {
setState && setState({
// 将销毁组件实例和项目实例的方法暴露给主项目
instanceInfo: {
path: to.fullPath,
destroyFn: () => {
const node = vm.$vnode,
destroy = vm.$destroy.bind(vm),
{base} = router.options,
destroyInstance = () => {
if (instance) {
instance.$destroy.call(instance)
instance = null
}
}
return [node, destroy, base, destroyInstance]
}
}
})
})
},
}
Vue.mixin(mixin) // 拦截所有页面的beforeRouteEnter
}
/* eslint-disable no-underscore-dangle */
const render = () => {
if (window.__POWERED_BY_QIANKUN__ && window.__CACHE_INSTANCE_BY_QIANKUN__) {
const cachedInstance = window.__CACHE_INSTANCE_BY_QIANKUN__
// 从最初的Vue实例上获得_vnode
const cachedNode = cachedInstance._vnode
// 让当前路由在最初的Vue实例上可用
router.apps = [...cachedInstance.$router.apps]
instance = new Vue({
router,
store,
render: () => cachedNode
})
// 缓存最初的Vue实例
instance.cachedInstance = cachedInstance
} else { // 正常的初始化
instance = new Vue({
router,
store,
render: h => h(App)
})
}
instance.$mount(el)
}
const bootstrap = async () => {}
const mount = async (props) => {
if (keepAlive) {
render()
setState = props.setGlobalState
} else {
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(el)
}
}
const unmount = async () => {
if (!keepAlive) {
instance.$destroy()
instance = null
return
}
if (!instance) {
window.__CACHE_INSTANCE_BY_QIANKUN__ = null
return
}
const cachedInstance = instance.cachedInstance || instance
const cachedNode = cachedInstance._vnode
window.__CACHE_INSTANCE_BY_QIANKUN__ = cachedInstance
// 让keep-alive可用
cachedNode.data.keepAlive = true
cachedNode.data.hook.destroy(cachedNode)
// 卸载当前实例,缓存的实例由于keep-alive生效,将不会真正被销毁,从而触发activated与deactivated
if (instance.cachedInstance) {
instance.$destroy()
instance = null
}
}
window.__POWERED_BY_QIANKUN__
// eslint-disable-next-line no-undef
? __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
: mount()
return {
bootstrap,
mount,
unmount,
}
}
import store from '@/store/index'
import router from '@/router/index'
/** home页的路由名称,根据需要自行设置 */
export const homeName = 'home'
/**
* @description 从路由中拿取名称
* @param {*} route
*/
export const getRouteTitleHandler = route => {
const router = {...route}
const meta = {...route.meta}
let {title} = meta
if (title && typeof meta.title === 'function') {
title = meta.title(router)
}
meta.title = title
router.meta = meta
return router
}
/**
* @description 判断两个对象是否相等,这两个对象的值只能是数字或字符串
* @param {*} obj1 对象
* @param {*} obj2 对象
*/
export const objEqual = (obj1, obj2) => {
const keysArr1 = Object.keys(obj1)
const keysArr2 = Object.keys(obj2)
if (keysArr1.length !== keysArr2.length) return false
else if (keysArr1.length === 0 && keysArr2.length === 0) return true
/* eslint-disable-next-line */
else return !keysArr1.some(key => obj1[key] != obj2[key])
}
/**
* @description 重复执行函数
* @param {Number} times 回调函数需要执行的次数
* @param {Function} cb 回调函数
*/
export const doCustomTimes = (times, cb) => {
let i = -1
while (++i < times) {
cb(i)
}
}
/**
* @description 根据name、params、query判断两个路由对象是否相等
* @param {*} route1
* @param {*} route2
*/
export const routeEqual = (route1, route2) => {
if(!isInRoutes(route1.path)) { // 多余的逻辑,为了判断微前端子项目私有页面添加
return route2._jump && route2._jump.startsWith(route1.path)
}
const params1 = route1.params || {}
const params2 = route2.params || {}
const query1 = route1.query || {}
const query2 = route2.query || {}
return (route1.name === route2.name) && objEqual(params1, params2) && objEqual(query1, query2)
}
/**
* @description 判断打开的标签列表里是否已存在该路由对象
* @param {Array} tagNavList
* @param {*} routeItem
*/
export const routeHasExist = (tagNavList, routeItem) => {
const len = tagNavList.length
let res = false
doCustomTimes(len, index => {
if (routeEqual(tagNavList[index], routeItem)) res = true
})
return res
}
/**
* @description 如果newRoute已经存在则不再添加
* @param {*} list 现有标签导航列表
* @param {*} newRoute 新添加的路由原信息对象
*/
export const getNewTagList = (list, newRoute) => {
const {name, path, meta} = newRoute
const newList = [...list]
if (newList.findIndex(item => item.name === homeName) >= 0) return newList
else newList.push({name, path, meta})
return newList
}
/**
* @description 用于查找路由列表中的home路由对象
* @param {*} routers
* @param {*} home
*/
export const getHomeRoute = (routers, home = homeName) => {
let i = -1
const len = routers.length
let homeRoute = {}
while (++i < len) {
const item = routers[i]
if (item.children && item.children.length) {
const res = getHomeRoute(item.children, home)
if (res.name) return res
} else {
if (item.name === home) homeRoute = item
}
}
return homeRoute
}
/**
* @description 获取相邻的标签路由
* @param {*} list 当行标签列表
* @param {*} route 当前关闭的标签
*/
export const getNextRoute =(list, route) => {
let res = {}
if (list.length === 2) {
res = getHomeRoute(list)
} else {
const index = list.findIndex(item => routeEqual(item, route))
if (index === list.length - 1) res = list[list.length - 2]
else res = list[index + 1]
}
return res
}
/**
* @description 转化面包屑菜单
* @param {*} route 当前路由
* @param {*} homeRoute
*/
export const getBreadCrumbList = (route, homeRoute) => {
const homeItem = {...homeRoute}
let routeMatched = route.matched
if (!isInRoutes(route.path)) { // 多余的逻辑,为了微前端子项目私有页面添加
const result = store.state.tagNavList.find(tag => tag._jump && tag._jump.startsWith(route.path))
routeMatched = result._matched
}
if (routeMatched.some(item => item.name === homeRoute.name)) return [homeItem]
let res = routeMatched.filter(item => item.parent !== undefined).map(item => {
const meta = {...item.meta}
if (meta.title && typeof meta.title === 'function') meta.title = meta.title(route)
return {
name: item.name,
meta,
}
})
res = res.filter(item => !item.meta.hideInBread)
return [{...homeItem, to: homeRoute.path}, ...res]
}
// 本地存储和获取标签导航列表
export const setTagNavListInLocalstorage = list => {
localStorage.tagNavList = JSON.stringify(list)
}
export const getTagNavListFromLocalstorage = () => {
const list = localStorage.tagNavList
return list ? JSON.parse(list) : []
}
/**
* @description 判断路径是否在路由列表中
* @param {String} path 路径
*/
export const isInRoutes = (path) => {
const {routes} = router.options
const parentRoute = routes.find(item => item.name === 'Layout')
const result = []
const filter = (list) => {
if (!list || list.length === 0) return
list.forEach(item => {
result.push(item.path)
const {children} = item
return filter(children)
})
}
filter(parentRoute.children)
return result.indexOf(path) >= 0
}
/**
* @description 当离开时判断路由和window.location是否发生变化
* @param {*} route
* @param {*} window.location
*/
export const checkRouteChange = (route, {pathname, search}) => {
const newLocation = pathname + search
if (!newLocation.startsWith(route.path)) {
const tagList = [...store.state.tagNavList]
if (isInRoutes(pathname)) { // 如果变化后的路径在路由列表中已有定义,则不需要_jump和_matched
tagList.forEach(tag => {
if (tag.path === pathname) {
delete tag._jump
delete tag._matched
}
})
} else { // 否则则设置重定向跳转链接_jump及面包屑需要用到的_matched路由匹配列表
tagList.forEach(tag => {
if (tag.name === route.name) {
tag._jump = newLocation
tag._matched = route.matched.filter(item => item.parent !== undefined).map(item => ({name: item.name, meta: item.meta, parent: true}))
}
})
}
store.commit('setTagNavList', tagList)
}
}
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import 'babel-polyfill'
import Vue from 'vue'
import App from './App'
import store from './store'
import router from './router'
import jscookie from 'js-cookie'
import ajax from '@/server/ajax'
import api from '@/server/api'
import {registerMicroApps, start, initGlobalState} from 'qiankun'
import {LocaleProvider, Layout, Menu, Icon, Breadcrumb, Dropdown, Badge, Spin, Button, Tag} from 'ant-design-vue'
Vue.config.productionTip = false
Vue.prototype.$ajax = ajax
Vue.prototype.$api = api
Vue.prototype.$cookie = jscookie
Vue.prototype.$global_state = initGlobalState({instanceInfo: null})
Vue.use(LocaleProvider)
Vue.use(Layout)
Vue.use(Menu)
Vue.use(Icon)
Vue.use(Breadcrumb)
Vue.use(Dropdown)
Vue.use(Badge)
Vue.use(Spin)
Vue.use(Button)
Vue.use(Tag)
/* eslint-disable no-new */
const projects = [ // 子项目信息
{
name: 'aaa',
entry: 'http://localhost:7771',
container: '#contentView',
activeRule: '/aaa',
},
{
name: 'bbb',
entry: 'http://localhost:7772',
container: '#contentView',
activeRule: '/bbb',
},
]
registerMicroApps(projects, { // 注册子项目
beforeLoad: () => store.commit('setLoading', true),
beforeMount: () => store.commit('setContent', true),
afterMount: () => store.commit('setLoading', false),
beforeUnmount: () => store.commit('setContent', false),
})
new Vue({
router,
store,
render: h => h(App)
}).$mount('#portal')
start()
import Vue from 'vue'
import Router from 'vue-router'
import routes from './routes'
import {checkRouteChange} from '@/libs/util'
Vue.use(Router)
const router = new Router({
mode: 'history',
// base: process.env.NODE_ENV === 'development' ? '/' : '/portal/',
routes,
})
router.beforeEach((to, from, next) => {
checkRouteChange(from, window.location) // 页面跳转时候检查路由和真实地址是否有变化
if (to.path === '/' && to.name !== 'home') { // 默认引导到home页面
next('/portal-home')
}
next()
})
export default router
const Layout = () => import('@/components/Layout')
const Info = () => import('@/views/info')
const Home = () => import('@/views/home')
const ErrorPage = () => import('@/views/error')
export default [
{
path: '*',
name: 'Layout',
component: Layout,
children: [
{
path: '/portal-home',
name: 'home',
component: Home,
meta: {
title: '首页',
}
},
{
path: '/info',
name: 'info',
component: Info,
meta: {
title: '用户信息',
}
},
]
},
{
path: '/error',
name: 'error',
component: ErrorPage,
},
{
path: '/logout',
name: 'logout',
component: ErrorPage,
},
]
import axios from 'axios'
import qs from 'qs'
import api from './api'
import Store from '@/store'
const Axios = axios.create({
baseURL: api.BASE_URL,
timeout: 15000,
})
Axios.interceptors.request.use(config => {
// 此处添加token
// config.headers.Authorization = 'token'
return config
}, error => {
return Promise.reject(error)
})
Axios.interceptors.response.use(response => {
// TODO 返回的数据status判断错误操作等……
Store.commit('SET_LOADING', false)
return response.data
}, error => {
Store.commit('SET_LOADING', false)
return Promise.resolve(error.response)
})
/**
* 请求
* @param {String} method [请求方法]
* @param {String} url [请求地址]
* @param {Object} params [请求参数]
* @param {String} contentType [请求头,默认为'application/json;charset=UTF-8']
* @param {Boolean} showLoading [是否显示请求时的loading图,默认为true]
*/
const request = ({ method, url, params = {}, contentType = 'application/json;charset=UTF-8', showLoading = true }) => {
if (!url || typeof(url) != 'string') {
throw new Error('接口URL不正确')
}
let config = {
method,
url,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': contentType,
},
}
if (method === 'GET') {
config = Object.assign(config, { params })
} else {
if (contentType.toLowerCase().indexOf('x-www-form-urlencoded') >= 0) {
config = Object.assign(config, { data: qs.stringify(params) })
} else {
config = Object.assign(config, { data: params })
}
}
if (showLoading) {
Store.commit('SET_LOADING', true)
}
return Axios(config)
}
export default {
get(args) {
return request({ method: 'GET', ...args })
},
post(args) {
args.contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
return request({ method: 'POST', ...args })
},
put(args) {
return request({ method: 'PUT', ...args })
},
delete(args) {
return request({ method: 'DELETE', ...args })
},
all(...ajaxs) {
return Promise.all(ajaxs)
},
}
let BASE_URL = ''
switch (process.env.NODE_ENV) {
case 'production': // 生产环境请求地址
BASE_URL = ''
break
default: // 默认(开发环境)请求地址
BASE_URL = ''
}
export default {
BASE_URL,
}
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import actions from './actions'
import mutations from './mutations'
Vue.use(Vuex)
const isDev = process.env.NODE_ENV === 'development'
export default new Vuex.Store({
strict: isDev,
state,
actions,
mutations,
})
import {
getRouteTitleHandler,
routeHasExist,
setTagNavListInLocalstorage,
getTagNavListFromLocalstorage,
getBreadCrumbList,
} from '@/libs/util'
const homeName = 'home'
export default {
setContent(state, value) {
state.contentIsReady = value
},
setLoading(state, val) {
state.showLoading = val
},
setRoutes(state, data) {
state.routes = data
},
setWebviewSrc(state, src) {
state.webviewSrc = src
},
addTag(state, {route, type = 'unshift'}) {
const router = getRouteTitleHandler(route)
if (routeHasExist(state.tagNavList, router)) return
if (type === 'push') state.tagNavList.push(router)
else {
if (router.name === homeName) state.tagNavList.unshift(router)
else state.tagNavList.splice(1, 0, router)
}
setTagNavListInLocalstorage([...state.tagNavList])
},
setTagNavList(state, list) {
let tagList = []
if (list) tagList = [...list]
else tagList = getTagNavListFromLocalstorage()
if (tagList[0] && tagList[0].name !== homeName) tagList.shift()
const homeTagIndex = tagList.findIndex(item => item.name === homeName)
if (homeTagIndex > 0) {
tagList.unshift(tagList.splice(homeTagIndex, 1)[0])
}
state.tagNavList = tagList
setTagNavListInLocalstorage([...tagList])
},
setBreadCrumb(state, route) {
state.breadCrumbList = getBreadCrumbList(route, state.homeRoute)
},
setInstanceCollection(state, data) {
state.instanceCollection = data
},
}
import {homeName, getHomeRoute} from '@/libs/util'
import routers from '@/router/routes'
export default {
homeRoute: getHomeRoute(routers, homeName),
contentIsReady: false,
showLoading: false,
routes: [],
webviewSrc: null,
tagNavList: [],
breadCrumbList: [],
instanceCollection: {},
}
<template>
<div class="error-page">
<h1>网络错误</h1>
<div>
<a-button @click="$router.back()">返回上一页</a-button>
<a-button type="primary" @click="$router.replace({path: '/portal-home'})">返回首页</a-button>
</div>
</div>
</template>
<script>
export default {
name: 'ErrorPage',
}
</script>
<style scoped>
.error-page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
<template>
<div>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae nesciunt cupiditate eaque deserunt laboriosam eveniet quaerat doloribus expedita a harum dolores, aliquid provident alias excepturi reprehenderit repudiandae. Laudantium, provident tempore.
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {}
},
}
</script>
<style scoped>
</style>
<template>
<h1>
Person Info Center
</h1>
</template>
<script>
export default {
name: 'Info',
data() {
return {}
},
}
</script>
<style scoped>
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment