因為項目有很多互不依賴的模塊,但每次發版卻要一次打包都發上去,所以項目組決定進行分模塊發版,看了一篇微服務前端的解決方案,還不錯,但是還是不那么全面,試着用了一下,並且發布了一下,沒什么太大問題,可能需要繼續優化一下,簡單介紹一下。
首先就是搭建主要的架構:
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文件夾相應的位置,這樣,直接全部發布的時候不需要再自己移動。
當然,再加上happypack會更快一下打包。之后會將eslint加上,目前發現新版的eslint不支持箭頭函數,不知道誰有好的辦法,
