前言
微前端 —— 理论篇
上一篇介绍了微前端的理念,本片将开始介绍 portal 项目。
portal 项目介绍
portal 项目包括两个功能:
1. 路由分发与应用加载;
2. 抽离公共依赖;
目录结构如下:
具体实现
- 新建一个文件夹,命名随便,我的是 portal
执行npm init -y
初始化package.json
现在我们要做什么呢?肯定是先引入依赖包啊
webpack 打包相关和 babel 和简单几个需要的 loader 就行了
npm i webpack webpack-cli webpack-dev-server style-loader css-loader copy-webpack-plugin clean-webpack-plugin babel-loader babel-core @babel/preset-env @babel/plugin-syntax-dynamic-import @babel/core -D
由于我们还需要同时运行公共依赖引入和应用加载两个服务,所以再npm i concurrently -D
-
在引入所需的依赖之后,开始实现相关功能。先实现公共依赖的引入吧。在上一步我们没有引入
single-spa
的依赖包,是因为 single-spa 的依赖包是作为公共依赖导入的。
先创建一个公共依赖的文件夹,这个文件夹在任何地方都可以,因为我们是通过远程链接引入的,要是你认为的 single-spa.js 文件可以通过远程 cdn 或者其他链接取到,可以跳过这一步了
,下面是我创建的公共依赖库文件夹。里面全是我的公共依赖文件
在 portal 下新建 src 文件夹,新建文件index.html
,作为我们整个项目的页面文件。代码如下<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title></title> </head> <body> // 此处为从远程导入各个项目的 js 文件 //(react、vue 项目打包和运行都会产生一个 js 文件,此文件就是动态渲染页面用的)// config.js 是 portal 项目产生的,用来进行路由分发与组件状态管理等 <script type="systemjs-importmap"> { "imports": { "@portal/config": "http://localhost:8233/config.js", "@portal/menu": "http://localhost:8235/menu.js", "@portal/project1": "http://localhost:8236/project1.js", "@portal/project2": "http://localhost:8237/project2.js" } } </script> <!-- 这里是引入我们的 systemjs,在公共依赖文件夹中,通过 http-server 开启的本地服务,进行远程访问 --> <script src='http://localhost:8000/systemjs/system.js'></script> <script src='http://localhost:8000/systemjs/amd.js'></script> <script src='http://localhost:8000/systemjs/named-exports.js'></script> <script src='http://localhost:8000/systemjs/use-default.js'></script> <!-- 执行 common-deps.js--> <!-- Load the common deps--> <script src="http://localhost:8234/common-deps.js"></script> <!-- Load the application --> <script> // 执行 config.js,进行页面的动态渲染 System.import('@portal/config') </script> <!-- Static navbar --> <!-- 引入样式文件 --> <link rel="stylesheet" href="http://localhost:8233/styles.css"> <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet"> <!-- 菜单容器 --> <div id='menu'></div> <div id='content'> <!-- react 应用容器 --> <div id="react"></div> <!-- vue 应用容器 --> <div id="vue"></div> </div> </body> </html>
接下来实现
common-deps.js
,将我们所有的公共依赖文件统一引入,并命名window.SystemJS = window.System function insertNewImportMap(newMapJSON) {const newScript = document.createElement('script') newScript.type = 'systemjs-importmap' newScript.text = JSON.stringify(newMapJSON) const allMaps = document.querySelectorAll('script[type="systemjs-importmap"]') allMaps[allMaps.length - 1].insertAdjacentElement( 'afterEnd', newScript ) } const devDependencies = { imports: { react: 'http://localhost:8000/react.development.js', 'react-dom': 'http://localhost:8000/react-dom.development.js', 'react-dom/server': 'http://localhost:8000/react-dom-server.browser.development.js', 'single-spa': 'http://localhost:8000/single-spa.min.js', lodash: 'http://localhost:8000/lodash.js', rxjs: 'http://localhost:8000/rxjs.umd.js', } } const prodDependencies = { imports: { react: 'http://localhost:8000/react.production.min.js', 'react-dom': 'http://localhost:8000/react-dom.production.min.js', 'react-dom/server': 'http://localhost:8000/react-dom-server.browser.production.min.js', 'single-spa': 'http://localhost:8000/single-spa.min.js', lodash: 'http://localhost:8000/lodash.min.js', rxjs: 'http://localhost:8000/rxjs.umd.min.js', } } const devMode = true // you will need to figure out a way to use a set of production dependencies instead if (devMode) {insertNewImportMap(devDependencies) } else {insertNewImportMap(prodDependencies) }
公共依赖的抽取代码已经实现了,下面就配置 webpack,将这些依赖进行打包,在项目根目录创建
webpack.common-deps.config.js
、webpack.common-deps.config.dev.js
webpack.common-deps.config.js
const path = require('path') const CleanWebpackPlugin = require('clean-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { entry: './src/common-deps.js', output: { filename: 'common-deps.js', path: path.resolve(__dirname, 'build/common-deps'), chunkFilename: '[name].js', }, mode: 'production', node: {fs: 'empty',}, resolve: { modules: [ __dirname, 'node_modules', ], }, devtool: 'sourcemap', plugins: [new CleanWebpackPlugin(['build/common-deps/']), CopyWebpackPlugin([{from: path.resolve(__dirname, 'src/common-deps.js')} ]), ], module: { rules: [{parser: {System: false}}, { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: {loader: 'babel-loader',} } ] } }
webpack.common-deps.config.dev.js
/* eslint-env node */ const config = require('./webpack.common-deps.config.js'); const webpack = require('webpack'); config.plugins.push(new webpack.NamedModulesPlugin()); config.plugins.push(new webpack.HotModuleReplacementPlugin()); config.devServer = { headers: {"Access-Control-Allow-Origin": "*",}, } config.mode = 'development' module.exports = config;
上面配置 dev-server 跨域,很重要!!!否则会因为无法跨域,访问不到 js 文件
最后在
package.json
的scripts
中添加命令"scripts": { "test": "echo \"Error: no test specified\"&& exit 1", "start:common-deps": "webpack-dev-server --config ./webpack.common-deps.config.dev.js --port 8234", "build:common-deps": "webpack --config ./webpack.common-deps.config.js -p" },
至此,公共依赖的抽取已经完成,执行
npm run start:common-deps
,公共依赖就被打包到了 portal 项目下的build/common-deps/
目录,通过 import 也可以正常导入使用(归功于 system) -
公共依赖抽取实现、配置完毕之后,开始利用
single-spa
构建我们的路由分发系统在 src 中创建文件
activityFns.js
,实现路由的正确匹配,内容如下:export function prefix(location, ...prefixes) { return prefixes.some( prefix => (location.href.indexOf(`${location.origin}/${prefix}`) !== -1 ) ) } // return true 则加载 false 则不加载 // 这里的 menu 是菜单,按理应该一直加载出来的,因此 return true export function menu(location) {return true} export function project1(location) {return prefix(location, '','page1','page2') } export function project2(location) {return prefix(location, 'page3', 'page4') }
在 src 中创建文件
config.js
,通过single-spa
实现路由的注册import * as isActive from './activityFns.js' import * as singleSpa from 'single-spa' singleSpa.registerApplication('menu', () => SystemJS.import('@portal/menu'), isActive.menu) singleSpa.registerApplication('project1', () => SystemJS.import('@portal/project1'), isActive.project1) singleSpa.registerApplication('project2', () => SystemJS.import('@portal/project2'), isActive.project2) singleSpa.start()
配置 webpack 打包,在项目根目录创建文件
webpack.config.config.js
,内容如下:/* eslint-env node */ const webpack = require('webpack') const path = require('path'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { entry: './src/config.js', output: { filename: 'config.js', library: 'config', libraryTarget: 'amd', path: path.resolve(__dirname, 'build'), }, mode: 'production', module: { rules: [{parser: {System: false}}, { test: /\.js?$/, exclude: [path.resolve(__dirname, 'node_modules')], loader: 'babel-loader', }, { 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: /\.css$/, include: [path.resolve(__dirname, 'node_modules')], exclude: [/\.krem.css$/], use: ['style-loader', 'css-loader'], }, ], }, resolve: { modules: [ __dirname, 'node_modules', ], }, plugins: [ CopyWebpackPlugin([{from: path.resolve(__dirname, 'src/index.html')}, {from: path.resolve(__dirname, 'src/styles.css')}, ]), new CleanWebpackPlugin(['build']), ], devtool: 'source-map', externals: [ /^lodash$/, /^single-spa$/, /^rxjs\/?.*$/, ], };
上面的配置中,最后的
externals 属性值配置不需要 webpack 打包的依赖
,因为这些在公共依赖中已经打包好了,不需要单独打包在项目根目录创建文件
webpack.config.config.dev.js
,内容如下:const config = require('./webpack.config.config.js') const webpack = require('webpack') config.plugins.push(new webpack.NamedModulesPlugin()); config.plugins.push(new webpack.HotModuleReplacementPlugin()); config.devServer = { contentBase: './build', historyApiFallback: true, headers: {"Access-Control-Allow-Origin": "*",}, proxy: { "/common/": { target: "http://localhost:8234", pathRewrite: {"^/common" : ""} } } } config.mode = 'development' module.exports = config;
同样配置跨域
在package.json
中添加命令,最终内容如下{ "name": "portal", "version": "1.0.0", "description": "","main":"index.js","scripts": {"test":"echo \"Error: no test specified\" && exit 1","start":"concurrently \"npm run start:config\" \"npm run start:common-deps\"", "start:config": "webpack-dev-server --config ./webpack.config.config.dev.js --port 8233", "start:common-deps": "webpack-dev-server --config ./webpack.common-deps.config.dev.js --port 8234", "build": "npm run build:config && npm run build:common-deps", "build:config": "webpack --config ./webpack.config.config.js -p", "build:common-deps": "webpack --config ./webpack.common-deps.config.js -p" }, "keywords": [], "author": "","license":"ISC","devDependencies": {"@babel/core":"^7.4.3","@babel/plugin-syntax-dynamic-import":"7.0.0","@babel/preset-env":"^7.4.3","babel-core":"6.26.3","babel-loader":"8.0.0","clean-webpack-plugin":"0.1.19","concurrently":"^4.1.1","copy-webpack-plugin":"4.5.2","css-loader":"1.0.0","style-loader":"0.23.0","webpack":"4.17.1","webpack-cli":"3.1.0","webpack-dev-server":"^3.1.14"} }
至此,整个 portal 项目就搭建完毕了,下一篇,我们实现 menu 项目和 project1 项目
项目源码
Portal 源码
微前端 —— 理论篇
微前端 —— menu&&project1(React)
微前端 —— project2 项目(VUE)