在qiankun出来之前,我们总有很多方案来实现微前端,微前端的好处肯定就不用多说了。 (有一种说法,只要你肯下决心去研究webpack,那么web的一切东西你都不会放在眼里,react源码也是) 这个其实更类似是一种拆包方式+微前端
main
把子环境需要变量,通过挂载到window上,例如React,组件库(如antd,自己封装的统一视图),redux(CONNECT,但是这个其实可以不用的,子项目的redux只要在main项目的store之内就行,能自己定义子store,在父级引入的时候用父的connect关联上就行)。在使用到子路由的时候,通过异步路由请求,找到制定的js,css资源,加载到head中,使用onload出发promise的resolve.。
import React from 'react';
import {
AutoComplete,
Input,
InputNumber,
Button,
Switch,
Radio,
Checkbox,
Slider,
TimePicker,
DatePicker,
Upload,
Cascader,
Select,
TreeSelect,
Icon,
Form,
Pagination,
Table,
Popconfirm,
Modal,
message,
Tooltip,
Spin,
Tabs,
Empty,
Tree,
Alert,
Drawer,
} from 'antd';
import { connect } from 'dva';
import GetFormItem from '@components/GetFormItem';
import config from '@utils/config';
window.React = React;
window.CONNECT = connect;
window.GetFormItem = GetFormItem;
window.Antd_NNUO = {
AutoComplete,
Input,
InputNumber,
Button,
Switch,
Radio,
Drawer,
Checkbox,
Slider,
Tabs,
TimePicker,
DatePicker,
Upload,
Cascader,
Select,
TreeSelect,
Icon,
Form,
Pagination,
Table,
Tooltip,
Popconfirm,
Modal,
Spin,
Empty,
message,
Tree,
Alert,
};
const configUrl = {
development: 'http://bendi:3001',
test: 'https://测试环境',
product: 'https://线上环境',
};
const url = window.location.href;
let env = 'product';
if (/127.0.0.1/.test(url) && !config.prodEnv) {
env = 'development';
} else if (/test.cn/.test(url)) {
env = 'test';
}
const mapStateToProps = (state) => {
const currentState = state.adManage;
const { userAuth, userInfo } = state.app;
return { ...currentState, userAuth, userInfo };
};
const insertScript = (e, isStyle = false) => {
return new Promise((reslove, reject) => {
const i = isStyle ? document.createElement('link') : document.createElement('script');
isStyle || i.setAttribute('type', 'text/javascript');
isStyle || i.setAttribute('src', e);
isStyle && i.setAttribute('href', e);
isStyle && i.setAttribute('rel', 'stylesheet');
isStyle && i.setAttribute('type', 'text/css');
function onload() {
if (!(this.readyState && this.readyState !== 'loaded' && this.readyState !== 'complete')) {
i.onload = null;
i.onreadystatechange = i.onload;
reslove();
}
}
function onerror(ee) {
reject(ee);
}
i.onreadystatechange = onload;
i.onload = onload;
i.onerror = onerror;
document.querySelector('head').appendChild(i);
});
};
function ERROR() {
return '加载静态资源失败,请稍后重试';
}
const subappRoutes = [];
const AyncComponent = async (pathname) => {
const id = pathname;
// 子工程资源是否加载完成
let ayncLoaded = false;
if (subappRoutes[id]) {
// 如果已经加载过该子工程的模块,则不再加载,直接取缓存的routes
ayncLoaded = true;
} else if (window.SONLIB && window.SONLIB[pathname]) {
const res = await window.SLOTP[pathname]();
subappRoutes[id] = res.default;
ayncLoaded = true;
} else {
try {
await insertScript(`${configUrl[env]}/index.js`);
if (window.SONLIB && window.SONLIB[pathname]) {
const res = await window.SONLIB[pathname]();
subappRoutes[id] = res.default;
ayncLoaded = true;
}
} catch (error) {
console.log('加载广告js失败', error);
}
}
return ayncLoaded ? connect(mapStateToProps)(subappRoutes[id]) : ERROR;
};
export default [
{
name: '资源管理',
path: 'advertiseMedia',
component: () => AyncComponent('advertiseMedia'),
},
{
name: '投放管理',
path: 'advertiseTask',
component: () => AyncComponent('advertiseTask'),
}
];
Sub
接下来看下子项目。
import routes from './ziplt.routes';
/* eslint-disable */
const configUrl = {
development: 'http://bendi:3001',
test: 'https://测试环境',
product: 'https://线上环境',
};
const url = window.location.href;
let env = 'product';
if (/127.0.0.1|172.30|192.168/.test(url)) {
env = 'development';
if (process.env.NODE_ENV !== 'development') {
env = 'jenkins';
}
} else if (/nntest.cn/.test(url)) {
env = 'test';
}
// eslint-disable-next-line no-undef
__webpack_public_path__ = `${configUrl[env]}/ziplt/`;
if (module.hot) {
module.hot.accept('./ziplt.routes', () => {
console.log(arguments);
// window.SLOTP(routes, true); // 支持子工程热加载的信息传递
});
}
export default routes;
'use strict';
const path = require('path');
const fs = require('fs');
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
var _interopRequireDefault = require('@babel/runtime/helpers/interopRequireDefault');
var _cssSplitWebpackPlugin = _interopRequireDefault(require('css-split-webpack-plugin'));
var _miniCssExtractPlugin = _interopRequireDefault(require('mini-css-extract-plugin'));
var _webpackPluginHashOutput = _interopRequireDefault(require('webpack-plugin-hash-output'));
var _htmlWebpackPlugin = _interopRequireDefault(require("html-webpack-plugin"));
var _friendlyErrorsWebpackPlugin = _interopRequireDefault(
require('friendly-errors-webpack-plugin'),
);
var _optimizeCssAssetsWebpackPlugin = _interopRequireDefault(
require('optimize-css-assets-webpack-plugin'),
);
var _terserWebpackPlugin = _interopRequireDefault(require('terser-webpack-plugin'));
var _lodashWebpackPlugin = _interopRequireDefault(require('lodash-webpack-plugin'));
var _progressBarWebpackPlugin = _interopRequireDefault(require('progress-bar-webpack-plugin'));
var _ndkLogger = _interopRequireDefault(require('@nuofe/ndk-logger'));
var _webpack = _interopRequireDefault(require('webpack'));
var _copyWebpackPlugin = _interopRequireDefault(require('copy-webpack-plugin'));
var _path = _interopRequireDefault(require('path'));
// var ziplt = require('../src/ziplt.json');
var ziplt = './src/ziplt.js';
const args = process.argv.slice(2);
const notMini = args.includes('--not-mini');
var _transformError = (cwd) => (error) => {
if (error.webpackError) {
const message =
typeof error.webpackError === 'string' ?
error.webpackError :
error.webpackError.message || '';
const match = message.match(/Entry module not found: Error: Can't resolve '([^']+)'/);
if (match) {
const relativePath = _path.default.relative(cwd, match[1]);
return {
...error,
message: ` Can't resolve '${relativePath}'.`,
name: 'Entry module not found',
};
}
if (!error.message) {
return {
...error,
message: ` Unknown webpack error:\n${message}`,
name: 'Unknown webpack error',
};
}
}
return error;
};
const EMPTY = 'empty';
const MOCK = 'mock';
const hash = '.[hash:8]';
const chunkhash = '.[chunkhash]';
const contenthash = '.[contenthash:8]';
const lintRegex = /\.(mjs|js|json|jsx|vue|ts|tsx)$/;
const scriptRegex = /\.(mjs|js|jsx|ts|tsx)$/;
const jsonRegex = /\.json$/;
const styleRegex = /\.(css|less)$/;
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;
const fontRegex = /\.(eot|otf|ttf|woff2?)$/;
const imageRegex = /\.(gif|jpe?g|png)$/;
const svgRegex = /\.svg$/;
const mediaRegex = /\.(aac|flac|mp3|mp4|ogg|wav|webm)$/;
const htmlRegex = /\.html?$/;
const ejsRegex = /\.ejs$/;
const localIdentName = '[local]_[contenthash:base64:5]';
const theme = {
'primary-color': '#20A0FF',
'table-padding-vertical': '6px',
'table-padding-horizontal': '16px',
'tabs-card-height': '36px',
'font-size-base': '13px',
'form-item-margin-bottom': '18px',
};
const getStyleLoaders = (isDevelopment, isLess, cssModules = false) => {
const forIE9 = false;
const sourceMap = false;
return [
{
loader: _miniCssExtractPlugin.default.loader,
options: {
esModule: false,
hmr: isDevelopment,
},
},
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1 + (isLess ? 1 : 0) + 0,
modules: cssModules && {
localIdentName,
},
sourceMap,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
ident: 'postcss',
plugins: [require('postcss-flexbugs-fixes'), require('autoprefixer')],
sourceMap,
},
},
isLess && {
loader: require.resolve('less-loader'),
options: {
lessOptions: {
javascriptEnabled: true,
modifyVars: {
...theme
},
},
sourceMap,
},
},
].filter(Boolean);
};
module.exports = (isDevelopment, cwd) => {
return {
entry: {index:ziplt},
output: {
chunkFilename: `[name].chunk${chunkhash}.js`,
hashDigestLength: 8,
path: resolveApp('./dist/ziplt'),
// publicPath: 'http://172.30.5.178:8097/ziplt/',
filename: '[name].js',
libraryExport: 'default',
library: 'SLOTP',
// library: ['SLOTP', '[name]'],
libraryTarget: 'umd',
umdNamedDefine: true,
},
externals: {
'react': 'React',
'antd': 'Antd_NNUO',
// 'react-dom':'ReactDOM'
},
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.jsx', '.vue', '.mjs', 'ts', 'tsx'],
alias: {
'@components': resolveApp('src/components'),
'@hooks': resolveApp('src/hooks'),
'@redux': resolveApp('src/redux'),
'@services': resolveApp('src/services'),
'@static': resolveApp('src/static'),
'@const': resolveApp('src/const'),
'@utils': resolveApp('src/utils'),
'@views': resolveApp('src/views'),
},
},
module: {
noParse: /^(js-base64|lodash|moment)$/,
rules: [{
oneOf: [{
test: scriptRegex,
exclude: /node_modules/,
use: [{
loader: require.resolve('thread-loader'),
},
{
loader: require.resolve('babel-loader'),
options: {
plugins: [
// [
// require.resolve('babel-plugin-import'),
// {
// libraryName: 'antd',
// libraryDirectory: 'es',
// style: true,
// },
// 'antd',
// ],
[require.resolve('babel-plugin-lodash')],
],
presets: [
[
require.resolve('@nuofe/babel-preset-ndk'),
{
commonJS: false,
debug: false,
modules: false,
react: true,
removePropTypes: !isDevelopment,
},
],
],
},
},
],
},
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders(isDevelopment, false, false),
},
{
test: cssModuleRegex,
use: getStyleLoaders(isDevelopment, false, true),
},
{
test: lessRegex,
exclude: lessModuleRegex,
oneOf: [{
exclude: [new RegExp('node_modules')],
use: getStyleLoaders(isDevelopment, true, false),
},
{
include: [new RegExp('node_modules')],
use: getStyleLoaders(isDevelopment, true, false),
},
],
},
{
test: lessModuleRegex,
use: getStyleLoaders(isDevelopment, true, true),
},
{
test: fontRegex,
loader: require.resolve('url-loader'),
options: {
esModule: false,
limit: 4096,
name: `font/[name]${hash}.[ext]`,
},
},
{
test: imageRegex,
loader: require.resolve('url-loader'),
options: {
esModule: false,
limit: 8192,
name: `image/[name]${hash}.[ext]`,
},
},
{
test: svgRegex,
loader: require.resolve('file-loader'),
options: {
esModule: false,
name: `image/[name]${hash}.[ext]`,
},
},
{
test: mediaRegex,
loader: require.resolve('file-loader'),
options: {
esModule: false,
name: `media/[name]${hash}.[ext]`,
},
},
{
test: htmlRegex,
loader: require.resolve('html-loader'),
},
{
test: ejsRegex,
loader: require.resolve('ejs-loader'),
options: {
esModule: false,
},
},
{
loader: require.resolve('file-loader'),
exclude: [
scriptRegex,
jsonRegex,
styleRegex,
fontRegex,
imageRegex,
svgRegex,
mediaRegex,
htmlRegex,
ejsRegex,
],
options: {
esModule: false,
name: `file/[name]${hash}.[ext]`,
},
},
],
}, ],
},
node: {
module: EMPTY,
dgram: EMPTY,
dns: MOCK,
fs: EMPTY,
http2: EMPTY,
net: EMPTY,
tls: EMPTY,
child_process: EMPTY,
setImmediate: false,
},
plugins: [
new _webpackPluginHashOutput.default(),
new _cssSplitWebpackPlugin.default({
size: 3000,
filename: 'css/[name].[part].[ext]',
imports: true,
}),
false &&
new _copyWebpackPlugin.default({
patterns: [{
from: 'src/static/file/*',
cacheTransform: true,
flatten: true,
force: true,
to: _path.default.join(resolveApp('./dist/ziplt'), '/file'),
}, ],
}),
new _friendlyErrorsWebpackPlugin.default({
additionalTransformers: [_transformError(cwd)],
}),
new _lodashWebpackPlugin.default({
collections: true,
paths: true,
}),
new _miniCssExtractPlugin.default({
chunkFilename: `css/[name].chunk${contenthash}.css`,
filename: `css/[name].css`,
ignoreOrder: true,
}),
new _progressBarWebpackPlugin.default({
format: `[:bar] ${_ndkLogger.default.chalk.green.bold(':percent')} (:msg)`,
}),
new _webpack.default.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn/),
new _webpack.default.HashedModuleIdsPlugin(),
// new _htmlWebpackPlugin.default(),
].filter(Boolean),
optimization: {
minimize: !notMini,
minimizer: [
new _terserWebpackPlugin.default({
cache: false,
extractComments: true,
parallel: true,
sourceMap: !isDevelopment,
terserOptions: {
compress: {
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {
safari10: true,
},
output: {
comments: false,
ascii_only: true,
},
},
}),
new _optimizeCssAssetsWebpackPlugin.default(),
],
},
};
};
这里就用到了很多webpack以前基本用不到的东西,例如
- webpack_public_path,当你不确定你的output的publicPath是,用这个
- library,当libraryTarget为umd时,这个代表你要打包出来的umd模块名字,在window环境中可以直接在控制台查看到这个变量,例如4.x的antd,
- libraryExport,代表你打包出来的东西的最终导出,例如打包出是{ default: xxx, version: 1.0 },这时候,libraryExport: 'default',你引用的library就是xxx了
webpack5中,这些属性都被重写了名字,放在output.library中
总结
我们也看到了,这种方式,如果你的子项目有很多路由,但是会一次性加载进来,这样不太好,所以还是要做到更友好的按需