systemJS 使用及其原理解抛


是一个通用的模块加载器,它能在浏览器上动态加载模块。微前端的核心就是加载微应用,我们将应用打包成模块,在浏览器中通过 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()

总结

  1. 调用 System.import(“./index.js”)开始加载

  2. 解析 importmap 资源映射表

  3. 根据加载的文件获取要加载资源的绝对路径

  4. 使用 JSONP 加载资源

  5. 执行加载后的代码,调用System.register(deps,declare)方法

  6. 将回调的结果保存起来, lastRegister = [deps, declare],回传给 import 方法

  7. 执行 register 第二个参数 获取 setters 和 execute 属性

    1. setters 是用来保存加载后的资源,加载资源调用 setters
    2. execute 是真正执行的渲染逻辑
  8. 加载 register 的提前注册的模块,加载完成后会在 window 上增添全局属性

  9. 获取 window 上最后添加的是属性(快照的方式:先取一次,再取一次)

  10. 都加载完执行 execute 是真正执行的渲染逻辑


文章作者: 高红翔
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 高红翔 !
  目录