加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

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

发布时间:2020-12-15 09:35:52 所属栏目:百科 来源:网络整理
导读:? ? 因为项目有很多互不依赖的模块,但每次发版却要一次打包都发上去,所以项目组决定进行分模块发版,看了一篇微服务前端的解决方案,还不错,但是还是不那么全面,试着用了一下,并且发布了一下,没什么太大问题,可能需要继续优化一下,简单介绍一下。 ?

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

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

? ? ? ? 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$/,‘node_modules‘),/.krem.css$/],use: [
                    ‘style-loader‘,{
                        loader: ‘css-loader‘,options: {
                            localIdentName: ‘[path][name]__[local]‘,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‘,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,}

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‘,{
        "name": "uioc",//模块名称
        "path": ["/app/uiocmanage/newuioc","/app/uiocmanage/myuioc","/app/uiocmanage/alluioc"],//模块url前缀
        "store": getEnv(‘uioc‘,]

引入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‘),‘../../../../build/js/uioc.js‘));
copyFile(path.resolve(__dirname,‘../build/uioc.css‘),‘../../../../build/css/uioc.css‘));
之后打包出来的样子就变成这个样子。
?

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

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读