当前位置 : 主页 > 网页制作 > React >

前端微服务+React解决方案。

来源:互联网 收集:自由互联 发布时间:2021-06-15
因为项目有很多互不依赖的模块,但每次发版却要一次打包都发上去,所以项目组决定进行分模块发版,看了一篇微服务前端的解决方案,还不错,但是还是不那么全面,试着用了一下

    因为项目有很多互不依赖的模块,但每次发版却要一次打包都发上去,所以项目组决定进行分模块发版,看了一篇微服务前端的解决方案,还不错,但是还是不那么全面,试着用了一下,并且发布了一下,没什么太大问题,可能需要继续优化一下,简单介绍一下。

   首先就是搭建主要的架构:

        1.webpack.config.js的初始化

         

const path = require(‘path‘);
const CleanWebpackPlugin = require(‘clean-webpack-plugin‘);
const CopyWebpackPlugin = require(‘copy-webpack-plugin‘);
const WebpackBar = require(‘webpackbar‘);
const autoprefixer = require(‘autoprefixer‘)
const { resolve } = path;
module.exports = {
    devtool: ‘source-map‘,
    entry: path.resolve(__dirname, ‘../src/index.js‘),
    output: {
        filename: ‘output.js‘,
        library: ‘output‘,
        libraryTarget: ‘amd‘,
        path: resolve(__dirname, ‘../public‘)
    },
    mode: ‘production‘,
    externals: {
        react: ‘React‘,
        ‘react-dom‘: ‘ReactDOM‘,
        jquery: ‘jQuery‘
    },
    module: {
        rules: [
            { parser: { System: false } },
            {
                test: /\.js?$/,
                exclude: [path.resolve(__dirname, ‘node_modules‘)],
                loader: ‘babel-loader‘,
            },
            {
                test: /\.less$/,
                use: [
                    //MiniCssExtractPlugin.loader,
                    {
                        loader: ‘css-loader‘,
                        options: {
                            sourceMap: true,
                        },
                    },
                    {
                        loader: ‘postcss-loader‘,
                        options: Object.assign({}, autoprefixer({ overrideBrowserslist: [‘last 2 versions‘, ‘Firefox ESR‘, ‘> 1%‘, ‘ie >= 9‘] }), { sourceMap: true }),
                    },
                    {
                        loader: ‘less-loader‘,
                        options: {
                            javascriptEnabled: true,
                            sourceMap: true,
                        },
                    },
                ],
            },
            {
                test: /\.css$/,
                exclude: [path.resolve(__dirname, ‘node_modules‘), /\.krem.css$/],
                use: [
                    ‘style-loader‘,
                    {
                        loader: ‘css-loader‘,
                        options: {
                            localIdentName: ‘[path][name]__[local]‘,
                        },
                    },
                    {
                        loader: ‘postcss-loader‘,
                        options: {
                            plugins() {
                                return [
                                    require(‘autoprefixer‘)
                                ];
                            },
                        },
                    },
                ],
            },
            {
                test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
                loader: ‘url-loader?limit=8192&name=images/[hash:8].[name].[ext]‘
            }
        ],
    },
    resolve: {
        modules: [
            __dirname, ‘node_modules‘
        ],
    },
    plugins: [
        new CleanWebpackPlugin([‘build‘], { root: path.resolve(__dirname, ‘../‘) }),
        CopyWebpackPlugin([{ from: path.resolve(__dirname, ‘../public/index.html‘) }]),
        new WebpackBar({
            name: ‘?? 主模块:‘,
            color: ‘#2f54eb‘,
        })
    ]
}

配置基本上一样,主要是出口那里要选择amd。

下面配置开发和上产两套启动方式:

      开发环境:

      

/* eslint-env node */
const config = require(‘./webpack.config.js‘);
const clearConsole = require(‘react-dev-utils/clearConsole‘);
const WebpackDevServer = require(‘webpack-dev-server‘);
const webpack = require(‘webpack‘);
const path = require(‘path‘);
config.mode = ‘development‘;

config.plugins.push(new webpack.NamedModulesPlugin());
config.plugins.push(new webpack.HotModuleReplacementPlugin());

const webpackConfig = webpack(config);
const devServer = new WebpackDevServer(webpackConfig, {
    
    contentBase: path.resolve(__dirname, ‘../build‘),
    compress: true,
    port: 3000,
    stats:{
        warnings: true,
        errors: true,
        children:false
    },
    historyApiFallback:true,
    clientLogLevel: ‘none‘,
    proxy: {
        ‘/‘: {
            header: { "Access-Control-Allow-Origin": "*" },
            target:‘http://srvbot-core-gat-bx-stg1-padis.paic.com.cn‘,//‘http://srvbot-dev.szd-caas.paic.com.cn‘, 
            changeOrigin: true,
            bypass: function (req) {
                if (/\.(gif|jpg|png|woff|svg|eot|ttf|js|jsx|json|css|pdf)$/.test(req.url)) {
                    return req.url;
                }
            }
        }
    }
});

devServer.listen(3000, process.env.HOST || ‘0.0.0.0‘, (err) => {
    if (err) {
        return console.log(err);
    }
    clearConsole();
});

生产环境:

      

process.env.NODE_ENV = ‘production‘;
process.env.BABEL_ENV = ‘production‘;


const webpack = require(‘webpack‘);
const path = require(‘path‘);
const chalk = require(‘chalk‘);
const webpackConfig = require(‘./webpack.config‘);
const util = require(‘./util‘);
webpackConfig.mode = ‘production‘;
const {emptyFolder,createFolder,copyFolder,notice,isCurrentTime,copyFile} = util;

createFolder();
emptyFolder(‘../build‘)

webpack(webpackConfig).run((err, options) => {
    if (err) {
        console.error(‘错误信息:‘, err);
        return;
    }
    if (err || options.hasErrors()) {
        if (options.compilation.warnings) {
            options.compilation.warnings.forEach(item => {
                console.log(chalk.green(‘?? 警告:‘, item.message.replace(‘Module Warning (from ./node_modules/happypack/loader.js):‘,‘‘).replace(‘(Emitted value instead of an instance of Error)‘,‘‘)), ‘\n‘);
            })
        }
        console.log(chalk.red(‘? 错误信息:‘));
        console.log(chalk.yellow(options.compilation.errors[0].error.message.replace(‘(Emitted value instead of an instance of Error)‘,‘‘)), ‘\n‘);
        notice(‘?? 警告:‘ + options.compilation.errors[0].error.message)
        return;
    }
    
    copyFolder(path.resolve(__dirname, ‘../public‘), path.resolve(__dirname, ‘../build‘));
    

    const { startTime, endTime } = options;
    const times = (endTime - startTime) / 1e3 / 60;
    console.log(chalk.bgGreen(‘开始时间:‘, isCurrentTime(new Date(startTime))), ‘\n‘);
    console.log(chalk.bgGreen(‘结束时间:‘, isCurrentTime(new Date(endTime))), ‘\n‘);
    console.log(chalk.yellowBright(‘总共用时:‘, `${parseFloat(times).toFixed(2)}分钟`), ‘\n‘);
})

这里是打包完成后,将打包过后的放进build文件夹。顺便贴一下node的文件夹方法,拿起即用:

const notifier = require(‘node-notifier‘);
const fs = require(‘fs‘);
const fe = require(‘fs-extra‘);
const path = require(‘path‘);
/**
 * Author:zhanglei185
 *
 * @param {String} str 
 * @returns {void} 
 */
function emptyFolder (str){
    fe.emptyDirSync(path.resolve(__dirname, str))
}

/**
 * Author:zhanglei185
 *
 * @param {String} message 
 * @returns {void} 
 */
function notice(message) {
    notifier.notify({
        title: ‘ServiceBot‘,
        message,
        icon: path.join(__dirname, ‘../public/img/8.jpg‘),
        sound: true,
        wait: true
    });
}

notifier.on(‘click‘, function (notifierObject, options) {
    // Triggers if `wait: true` and user clicks notification
});

notifier.on(‘timeout‘, function (notifierObject, options) {
    notice()
});

/**
 * Author:zhanglei185
 *
 * @param {String} src 
 * @param {String} tar 
 * @returns {void}
 */
function copyFolder(src, tar) {
    fs.readdirSync(src).forEach(path => {
        const newSrc = `${src}/${path}`;
        const newTar = `${tar}/${path}`
        const st = fs.statSync(newSrc);
        console.log(newTar)
        if (st.isDirectory()) {
            fs.mkdirSync(newTar)
            return copyFolder(newSrc, newTar)
        }
        if (st.isFile()) {
            fs.writeFileSync(newTar, fs.readFileSync(newSrc))
        }
    })
}

/**
 * Author:zhanglei185
 *
 * @returns {void}
 */
function createFolder() {
    if (!fs.existsSync(path.resolve(__dirname, ‘../build‘))) {
        fs.mkdirSync(path.resolve(__dirname, ‘../build‘))
    }
}

/**
 * Author:zhanglei185
 *
 * @param {Date} time 
 * @returns {void}
 */
function isCurrentTime(time) {
    const y = time.getFullYear();
    const month = time.getMonth() + 1;
    const hour = time.getHours();
    const min = time.getMinutes();
    const sec = time.getSeconds();
    const day = time.getDate();
    const m = month < 10 ? `0${month}` : month;
    const h = hour < 10 ? `0${hour}` : hour;
    const mins = min < 10 ? `0${min}` : min;
    const s = sec < 10 ? `0${sec}` : sec;
    const d = day < 10 ? `0${day}` : day;
    return `${y}-${m}-${d} ${h}:${mins}:${s}`
}

module.exports={
    isCurrentTime,
    emptyFolder,
    createFolder,
    copyFolder,
    notice,
}

2.接下来经过运行上面的开发环境,会生成一个output.js。现在增加一个html页面用来加载js

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <title>Servicebot</title>
  <link rel="stylesheet" href="./css/antd.css">
</head>

<body>
  <div id="root"></div>
  <div id="login"></div>
  <div id="base">
    <divid="uioc"></div>
  </div>
  <script type="systemjs-importmap">
      {"imports": {
        "output!sofe": "./output.js",
      }}
  </script>
  <!-- <script src=‘./react-dom.production.min.js‘></script>
  <script src=‘./react.production.min.js‘></script> -->
  <script src=‘./common/system.js‘></script>
  <script src=‘./common/amd.js‘></script>
  <script src=‘./common/named-exports.js‘></script>
  <script src="./common/common-deps.js"></script>
  <script>
    System.import(output!sofe)
  </script>
</body>

</html>

主要是引入打包过后的js,摒弃引入几个必要的js。那么主要模块启动就完成了。

3.现在开发每个单独模块

  webpack的搭建,仿照上面的就可以,但是端口号需要切换成不同的以方便,主模块加载各个模块的js,另外还需要将代理设置为跨域的,不然是不允许访问的

headers: { "Access-Control-Allow-Origin": "*" },
出口换成不同名,比如单独打包了登陆,那么出口为login.js。

那么我们在主模块怎么加载这个js呢

我们知道,主模块的入口文件是index.js

那么我们看一下这个index.js都做了什么
import * as isActive from ‘./activityFns‘
import * as singleSpa from ‘single-spa‘
import { registerApp } from ‘./Register‘
import { projects } from ‘./project.config‘
const env = process.env.NODE_ENV;

function domElementGetterCss({name,host}) {
    const getEnv = (env) => {
        if (env === ‘development‘) {
            return `http://localhost:${host}/${name}.css`
        } else if (env === ‘production‘) {
            return `./css/${name}.css`
        }
    }
    let el = document.getElementsByTagName("head")[0];
    const link = document.createElement(‘link‘);
    link.rel = "stylesheet"
    link.href = getEnv(env)
    el.appendChild(link);
    return el
}

function createCss(){
    const arr = [
        {
            name:‘login‘,
            host:3100,
        },
        {
            name:‘base‘,
            host:3200,
        },
        {
            name:‘uioc‘,
            host:3300,
        }
    ]

    arr.forEach(item =>{
        domElementGetterCss(item)
    })
}



async function bootstrap() {
    createCss()
    const SystemJS = window.System;

    projects.forEach(element => {
        registerApp({
            name: element.name,
            main: element.main,
            url: element.prefix,
            store: element.store,
            base: element.base,
            path: element.path
        });
    });

    singleSpa.start();
}

bootstrap()



//singleSpa.start()

里边有system和spa两个js的方法,我们在bootstarp这个方法里 加入不同服务下的css和js。

project.config.js
const env = process.env.NODE_ENV;
console.log(env)
const getEnv = (name,env) =>{
    if(env === ‘development‘){
        return `http://localhost:${host(name)}/${name}.js` 
    }else if(env === ‘production‘){
        console.log(env)
        return `./js/${name}.js` 
    }
}

function host(name){
    switch(name){
        case‘login‘:return ‘3100‘;
        case‘base‘:return ‘3200‘;
        case‘uioc‘:return ‘3300‘;
    }
}

export const projects = [
    {
        "name": "login", //模块名称
        "path": "/", //模块url前缀
        "store": getEnv(‘login‘,env),//模块对外接口
    },
    {
        "name": "base", //模块名称
        "path": "/app", //模块url前缀
        "store": getEnv(‘base‘,env),//模块对外接口
    },
    {
        "name": "uioc", //模块名称
        "path": ["/app/uiocmanage/newuioc","/app/uiocmanage/myuioc","/app/uiocmanage/alluioc"], //模块url前缀
        "store": getEnv(‘uioc‘,env),//模块对外接口
    },
]

引入js和css都需要判断当前的环境,因为生产环境不需要本地服务

registry.js

import * as singleSpa from ‘single-spa‘;

import { GlobalEventDistributor } from ‘./GlobalEventDistributor‘
const globalEventDistributor = new GlobalEventDistributor();
const SystemJS = window.System
// 应用注册
const arr = [];
export async function registerApp(params) {
    let storeModule = {}, customProps = { globalEventDistributor: globalEventDistributor };
    try {
        storeModule = params.store ? await SystemJS.import(params.store) : { storeInstance: null };
    } catch (e) {
        console.log(`Could not load store of app ${params.name}.`, e);
        return
    }

    if (storeModule.storeInstance && globalEventDistributor) {
        customProps.store = storeModule.storeInstance;
        globalEventDistributor.registerStore(storeModule.storeInstance);
    }
    
    customProps = {
        store: storeModule,
        globalEventDistributor: globalEventDistributor
    };
    window.globalEventDistributor = globalEventDistributor
    singleSpa.registerApplication(
        params.name,
        () => SystemJS.import(params.store),
        (pathPrefix(params)),
        customProps
    );
}


function pathPrefix(params) {
    return function () {
        let hash = window.location.hash.replace(‘#‘, ‘‘);
        let isShow = false;
        if (!(hash.startsWith(‘/‘))) {
            hash = `/${hash}`
        }
        //多个地址共用的情况
        if (isArray(params.path)) {
            isShow = params.path.some(element => {
                return element!==‘/app‘ &&  hash.includes(element)
            });
        }
        if (hash === params.path) {
            isShow = true
        }
        if (params.name === ‘base‘ && hash !== ‘/‘) {
            isShow = true
        }
        // console.log(‘【localtion.hash】: ‘, hash)
        // console.log(‘【params.path】: ‘, params.path)
        // console.log(‘【isShow】: ‘, isShow)
        // console.log(‘ ‘)
        return isShow;
    }
}

function isArray(arr) {
    return Object.prototype.toString.call(arr) === "[object Array]"
}

在将每一个模块注册进的时候,将路由用到的history也注入。并且将redux注册为全局的,

不知道其他人怎么用的,不过我用了一个方法就是替换原来的connect,从新包装一个:

import * as React from ‘react‘

export function connect(fnState, dispatch) {
    const getGlobal = window.globalEventDistributor.getState();
    const obj = {
        login: getGlobal.login && getGlobal.login.user,
        sider: {},
        serviceCatalog: {}
    }
    //获取
    const app = fnState(obj)
    //发送
    const disProps = function () {
        return typeof dispatch === ‘function‘ && dispatch.call(getGlobal.dispatch,getGlobal.dispatch);
    }

    return function (WrappedComponent) {
        return class UserUM extends React.Component {
            render() {
                return (
                    <WrappedComponent {...disProps()} {...app} {...this.props} />
                )
            }
        }
    }
} 

通过全局,先拿到每个模块的 storeInstance,通过全局获取到,然后写一个高阶组件包含两个方法state,和dispatch,以保持connect原样,以方便不要修改太多地方。

然后通过props传递到组件内部,组件依然可以像原来一样拿到state和方法。

4.每个模块需要一个单独的入口文件

import React from ‘react‘
import ReactDOM from ‘react-dom‘
import singleSpaReact from ‘single-spa-react‘
import { Route, Switch, HashRouter } from ‘react-router-dom‘;
import { LocaleProvider } from ‘antd‘;
import zh_CN from ‘antd/lib/locale-provider/zh_CN‘;
import { Provider } from ‘react-redux‘
const createHistory = require("history").createHashHistory
const history = createHistory()


import NewUioc from ‘../src/uiocManage/startUioc‘
import MyUioc from ‘../src/uiocManage/myUioc/myuioc.js‘
import AllUioc from ‘../src/uiocManage/allUioc/alluioc.js‘
import UiocTicket from ‘../src/uiocManage/components‘
import UiocTaskTicket from ‘../src/uiocManage/components/taskTicket.js‘
import NewUiocIt from ‘../src/itManage/startUioc‘


const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: (spa) => {
    return (
      <Provider store={spa.store.storeInstance} globalEventDistributor={spa.globalEventDistributor}>
        <HashRouter history={spa.store.history}>
          <Switch>
            <Route exact path="/app/uiocmanage/newuioc" component={NewUioc} />
            <Route exact path="/app/uiocmanage/myuioc" component={MyUioc} />
            <Route exact path="/app/uiocmanage/alluioc" component={AllUioc} />
            <Route exact path="/app/uiocmanage/alluioc/:ticketId" component={UiocTicket} />
            <Route exact path="/app/uiocmanage/alluioc/:ticketId/:taskId" component={UiocTaskTicket} />
          </Switch>
        </HashRouter>
      </Provider>
    )
  },
  domElementGetter
})

export const bootstrap = [
  reactLifecycles.bootstrap,
]

export const mount = [
  reactLifecycles.mount,
]

export const unmount = [
  reactLifecycles.unmount,
]

export const unload = [
  reactLifecycles.unload,
]

function domElementGetter() {
  let el = document.getElementById("uioc");
  if (!el) {
    el = document.createElement(‘div‘);
    el.id = ‘uioc‘;
    document.getElementById(‘base‘).querySelector(‘.zl-myContent‘).appendChild(el);
  }

  return el;
}

import { createStore, combineReducers } from ‘redux‘

const initialState = {
  refresh: 20
}

function render(state = initialState, action) {
  switch (action.type) {
    case ‘REFRESH‘:
      return {
        ...state,
        refresh: state.refresh + 1
      }
    default:
      return state
  }
}

export const storeInstance = createStore(combineReducers({ namespace: () => ‘uioc‘, render, history }))
export { history }

在这个页面需要生成一个id ,去渲染这个模块的js,并且将这个模块的storeInstance传出,一个单独的模块就打包完了。

完事之后,在单独模块打包完成后需要将这个模块的js和css复制到主模块的build文件夹相应的位置,这样,直接全部发布的时候不需要再自己移动。

copyFile(path.resolve(__dirname, ‘../build/uioc.js‘), path.resolve(__dirname, ‘../../../../build/js/uioc.js‘)); copyFile(path.resolve(__dirname, ‘../build/uioc.css‘), path.resolve(__dirname, ‘../../../../build/css/uioc.css‘)); 之后打包出来的样子就变成这个样子。  

当然,再加上happypack会更快一下打包。之后会将eslint加上,目前发现新版的eslint不支持箭头函数,不知道谁有好的办法,

网友评论