实现类微前端的一种方案

83 阅读4分钟

在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以前基本用不到的东西,例如

  1. webpack_public_path,当你不确定你的output的publicPath是,用这个
  2. library,当libraryTarget为umd时,这个代表你要打包出来的umd模块名字,在window环境中可以直接在控制台查看到这个变量,例如4.x的antd,
  3. libraryExport,代表你打包出来的东西的最终导出,例如打包出是{ default: xxx, version: 1.0 },这时候,libraryExport: 'default',你引用的library就是xxx了

webpack5中,这些属性都被重写了名字,放在output.library中

总结

我们也看到了,这种方式,如果你的子项目有很多路由,但是会一次性加载进来,这样不太好,所以还是要做到更友好的按需