是一个通用的模块加载器,它能在浏览器上动态加载模块。微前端的核心就是加载微应用,我们将应用打包成模块,在浏览器中通过 systemJS 来加载模块。
1. 基本使用
搭建 react 开发环境
npm init -y
npm install webpack webpack-cli webpack-dev-server babel-loader
@babel/core @babel/preset-env @babel/preset-react html-webpack-plugin -D
npm install react react-dom
webpack.config.js
微前端的公共模块 必须采用 cdn 的方式
生产模式下需要打包成一个模块给别人使用 不用打包 index.html、react 和 react-dom
const HtmlWebpackPlugin = require("html-webpack-plugin")
const path = require("path")
module.exports = (env) => {
console.log("env=>", env)
return {
// 1.为了更好的看到打包后的代码,统一设置mode为开发模式
mode: "development",
output: {
filename: "index.js",
path: path.resolve(__dirname, "dist"),
// 2.指定生产模式下采用systemjs 模块规范
libraryTarget: env.production ? "system" : "",
},
module: {
// 3.使用babel解析js文件
rules: [
{
test: /\.js$/,
use: { loader: "babel-loader" },
exclude: /node_modules/,
},
],
},
plugins: [
// 4.生产环境下不生成html
!env.production &&
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
].filter(Boolean),
// 5.生产环境下不打包react,react-dom。(这里也可以打包到当前项目下均可)
externals: env.production ? ["react", "react-dom"] : [],
// 打包的时候 1) 考虑公共模块是否要打包进去 2) 打包后的资源大小
}
}
// 我们将子应用 打包成类库,在主应用中加载这个库(systemjs)
// system 模块规范 umd amd esModule commonjs
.babelrc
{
"presets": [
"@babel/preset-env",
["@babel/preset-react",{
"runtime":"automatic"
}]
]
}
src 文件
App.js
function App() {
return (
<div>
<h1>Hello, World!</h1>
</div>
)
}
export default App
index.js
import ReactDOM from "react-dom/client"
import App from "./App"
// 渲染App组件
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)
打包后的结果
dist/index.js
System.register(["react-dom","react"], function () { ... });
浏览器加载模块(dist/index.html)
systemjs-importmap 公共资源配置
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>systemJS</title>
<script defer src="index.js"></script>
</head>
<body>
主应用 - 基座 - 用来加载子应用的 webpack importMap
<div id="root"></div>
<script type="systemjs-importmap">
{
"import": {
react":"https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.pro
duction.min.js",
"react-dom":"https://cdn.bootcdn.net/ajax/libs/react-
dom/17.0.2/umd/react-dom.production.min.js"
}
}
</script>
<div id="root"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
<script>
// 表示可以动态加载模块
//加载模块的时候会提示加载react和react-dom 会自动在上边加载systemjs-importmap 中配置的要加载的模块
// 可以加载远程连接
// 类似AMD的前置依赖 引入index.js的时候需要先加载 react和 react-dom
System.import("./index.js")
</script>
</body>
</html>
2. 手动实现 system 原理
- systemjs 是如何定义的 先看打包后的结果 System.register(依赖列表,后调函数返回值一个 setters,execute)
- react , react-dom 加载后调用 setters 将对应的结果赋予给 webpack
- 调用执行逻辑 执行页面渲染
const newMapUrl = {} //存储依赖的模块地址
/**
* 解析 importsMap,将需要提前加载的模块存储到newMapUrl对象
*/
function processScripts() {
Array.from(document.querySelectorAll("script")).forEach((script) => {
if (script.type === "systemjs-importmap") {
const imports = JSON.parse(script.innerHTML).imports
Object.entries(imports).forEach(([key, value]) => (newMapUrl[key] = value))
}
})
}
// 加载资源
function load(id) {
return new Promise((resolve, reject) => {
const script = document.createElement("script")
script.src = newMapUrl[id] || id // 支持cdn的查找
script.async = true
document.head.appendChild(script)
// 此时会执行代码
script.addEventListener("load", function () {
let _lastRegister = lastRegister
lastRegister = undefined
resolve(_lastRegister)
})
})
}
let set = new Set()
// 1)先保存window上的属性
function saveGlobalProperty() {
for (let k in window) {
set.add(k)
}
}
saveGlobalProperty()
function getLastGlobalProperty() {
// 看下window上新增的属性
for (let k in window) {
if (set.has(k)) continue
set.add(k)
return window[k] // 我通过script新增的变量
}
}
let lastRegister
class SystemJs {
// 这个id原则上可以是一个第三方路径cdn
import(id) {
return Promise.resolve(processScripts())
.then(() => {
// 1)去当前路径查找 对应的资源 ./index.js
const lastSepIndex = location.href.lastIndexOf("/")
const baseURL = location.href.slice(0, lastSepIndex + 1)
if (id.startsWith("./")) {
return baseURL + id.slice(2)
}
// http https
})
.then((id) => {
// 根据文件的路径 来加载资源
let execute
return load(id)
.then((register) => {
let { setters, execute: exe } = register[1](() => {})
execute = exe
// execute 是真正执行的渲染逻辑
// setters 是用来保存加载后的资源,加载资源调用setters
// console.log(setters,execute)
return [register[0], setters]
})
.then(([registeration, setters]) => {
return Promise.all(
registeration.map((dep, i) => {
return load(dep).then(() => {
const property = getLastGlobalProperty()
// 加载完毕后,会在window上增添属性 window.React window.ReactDOM
setters[i](property)
})
// 拿到的是函数,加载资源 将加载后的模块传递给这个setter
})
)
})
.then(() => {
execute()
})
})
}
register(deps, declare) {
// 将回调的结果保存起来
lastRegister = [deps, declare]
}
}
const System = new SystemJs()
总结
调用 System.import(“./index.js”)开始加载
解析 importmap 资源映射表
根据加载的文件获取要加载资源的绝对路径
使用 JSONP 加载资源
执行加载后的代码,调用
System.register(deps,declare)
方法将回调的结果保存起来,
lastRegister = [deps, declare]
,回传给 import 方法执行 register 第二个参数 获取 setters 和 execute 属性
- setters 是用来保存加载后的资源,加载资源调用 setters
- execute 是真正执行的渲染逻辑
加载 register 的提前注册的模块,加载完成后会在 window 上增添全局属性
获取 window 上最后添加的是属性(快照的方式:先取一次,再取一次)
都加载完执行 execute 是真正执行的渲染逻辑