简单聊聊SystemJs

前言

回顾一下, 断更了有快两个月了,最近属实是忙过头了,又是找工作准备面试,又是接了个大需求,做的痛不欲生的。在这种节奏下,更文就被落下了。

好了,回归正题,今天我们来聊一聊SystemJs。

SystemJs

对于SystemJs,或许很多人并没有听说过,甚至很多人其实有在使用它,但却没见过它。

SystemJs是一个极其灵活的模块加载器,也可以认为它是一种规范,因为使用它去加载模块,需要符合一定的规范。

它的主要用途就是加载模块,在微前端领域中,single-spa就是依赖于它的,而我们都知道,qiunkun是基于single-spa的,那么上面说的很多人正在使用SystemJs却没见过它,指的就是很多qiankun用户啦,毕竟SystemJs只是默默地在底层工作。

ok,我们继续聊聊它的使用,以及它是如何加载模块的。

基本使用

我们使用webpack起一个很简单的项目,项目主要文件内容如下。

js 复制代码
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");

module.exports = (env) => {
  return {
    mode: "development",
    output: {
      filename: "index.js",
      path: path.resolve(__dirname, "dist"),
      // 注意,这个libraryTarget是必须的
      libraryTarget: env.production ? "system" : "",
    },
    module: {
      rules: [
        {
          test: /.js$/,
          use: { loader: "babel-loader" },
          exclude: /node_modules/,
        },
      ],
    },
    plugins: [
      new HtmlWebpackPlugin({
        template: "./src/index.html",
      })
    ],
    // 这里不为空就行,如若是vue,改为[vue]即可
    externals: env.production ? ["react", "react-dom"] : [],
  };
};

这个配置文件中,其他配置都比较随意,但libraryTarget是必须的,且webpack版本必须为4.30以上,指定为system之后,它会打包出一个符合SystemJs要求的产物。

ok。这个项目主要就是这份配置文件,其他文件,各位随便写点就好。笔者这边就仅写一个简单的入口文件。

js 复制代码
const App = () => <div><h1>Hello World!</h1></div>;

export default App;

然后我们添加一行打包命令。

js 复制代码
// package.json
"scripts": {
  "build": "webpack --env production"
},

执行build命令,看下我们打包后的产物。

如果和笔者这边的配置一样的,dist文件夹中会有两个文件,一个index.js,另一个是index.html

我们先来看index.js

js 复制代码
System.register(
  ["react-dom", "react"],
  function (__WEBPACK_DYNAMIC_EXPORT__, __system_context__) {
    var __WEBPACK_EXTERNAL_MODULE_react_dom__ = {};
    var __WEBPACK_EXTERNAL_MODULE_react__ = {};
    Object.defineProperty(__WEBPACK_EXTERNAL_MODULE_react_dom__, "__esModule", {
      value: true,
    });
    Object.defineProperty(__WEBPACK_EXTERNAL_MODULE_react__, "__esModule", {
      value: true,
    });
    return {
      setters: [
        function (module) {
          Object.keys(module).forEach(function (key) {
            __WEBPACK_EXTERNAL_MODULE_react_dom__[key] = module[key];
          });
        },
        function (module) {
          Object.keys(module).forEach(function (key) {
            __WEBPACK_EXTERNAL_MODULE_react__[key] = module[key];
          });
        },
      ],
      execute: function() { 这里是项目的打包后可执行的代码 },
    }
  },
);

经过很轻微地简化,就是如上代码了,这个文件就一个register函数,这就是SystemJs的要求,这个脚本的内容改为被一个register函数包裹,然后第一个参数就是不被打包进来但通过特定方式进行加载(下文会讲到这个特定的加载方式),第二个参数一个函数,主要用来返回我们的项目代码,其中__WEBPACK_EXTERNAL_MODULE_react_dom__和__WEBPACK_EXTERNAL_MODULE_react__则分别对应第一个参数中的需要特定方式加载回来的模块。

看到此处,相信各位还是比较懵的,没关系,我们继续往下走。

我们来看index.html

内容非常简单,一个div盒子,加上一个script标签,这个script加载的就是上面的index.js,我们直接用浏览器去打开这个index.html文件,自然会报错的,因为在index.js中需要调用的System.register这个方法,但我们并没有System更别谈register了。

所以我们对index.html改造下

  • 先添加上SystemJs
js 复制代码
<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
  • 提供React和React-dom的加载链接
js 复制代码
// 这是systeJs规定的写法
<script type="systemjs-importmap">
  {
    "imports": {
      "react": "https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js",
      "react-dom": "https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"
    }
  }
</script>
  • 然后删掉加载index.js的那行script,并采用SystemJs的import方法。
js 复制代码
<script>
  System.import('./index.js');
</script>

整个index.html改造后的样子

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  主应用 - 基座 - 用来加载子应用的
  <script type="systemjs-importmap">
    {
      "imports": {
        "react": "https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js",
        "react-dom": "https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"
      }
    }
  </script>
  <div id="root"></div>
  <script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
  <script>
    System.import('./index.js');
  </script>
</body>

最后我们使用浏览器打开这个html文件。

页面中很成功的出现了hello world。

结合改造后的html文件和js文件,现在大家应该比较清晰了,整个流程可以总结为以下这几步。

  1. 打包生成System.register(依赖列表, 回调函数返回值有一个setters和execute)
  2. react,react-dom 加载后调用setters 将对应的结果赋予给webpack
  3. 调用execute,执行页面渲染

再往细了的讲,我们调用setters,其实就是将需要特定方式加载的包的内容保存到一个execute能调用到的地方。

我们把这种加载机制往微前端方向去思考下,我们将html文件和excute类比为基座,将额外加载的react和react-dom比作子应用,excute调用react/react-dom类比做渲染子应用的内容,是不是觉得微前端通了?事实上,single-spa正是这么做的。

不过本文不讲single-spa,所以发散到此就ok了,下面我们来简单写一下SystemJs的register和import这两个核心方法。

简单实现

代码不多且比较简单,各处都标了注释,这里就直接贴全代码了。

js 复制代码
    const newMapUrl = {};
    // 解析script标签,获取模块加载连接
    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;
          });
        }
      })
    }

    let set = new Set();
    // 先保存window上的属性  给window拍照
    function saveGlobalProperty() {
      for (let k in window) {
        set.add(k);
      }
    }
    saveGlobalProperty();
    // 看下window上新增的属性
    function getLastGlobalProperty() {
      for (let k in window) {
        if (set.has(k)) continue;
        set.add(k);
        return window[k];
      }
    }
  
    let lastRegister;
    function load(id) {
      return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        // 支持cdn
        script.src = newMapUrl[id] || id;
        script.async = true;
        document.head.appendChild(script);
        script.addEventListener('load', () => {
          const _lastRegister = lastRegister;
          lastRegister = null;
          resolve(_lastRegister);
        });
      })
    }

    // 模块规范 用来加载System模块的
    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);
          }
        }).then((id) => {
          let execute;
          // 根据路径加载资源
          return load(id).then((register) => {
            const { setters, execute: exe } = register[1](() => {});
            execute = exe;
            return [register[0], setters];
          }).then(([registration, setters]) => {
            return Promise.all(registration.map((dep, i) => {
              return load(dep).then(() => {
                // setters[i]拿到的是函数,加载资源后将加载后的模块传递给这个setter
                // 加载完毕后,会在window上增添属性 例如React就会在window.React上,ReactDOM会在window.ReactDOM上
                // window新增的属性
                const property = getLastGlobalProperty();
                setters[i](property);
              });
            }));
          }).then(() => {
            execute();
          });
        })
      }

      register(deps, declare) {
        // 将回调的结果保存起来
        lastRegister = [deps, declare];
      }
    }
    const System = new SystemJS();
    System.import('./index.js').then(() => {
      console.log('模块加载完毕');
    });

以上代码的主要内容大概为这些:

  1. 解析script标签,获得模块加载的链接
  2. 加载模块并放到window上
  3. 获取到setters和execute
  4. 执行setters
  5. 执行execute

至于细节部分,大家可复制上面的代码进行调试~

最后我们看下结果:

结尾

本文主要讲了systemJs是什么,怎么用,以及如何在微前端中发挥作用,最后还简单实现了下SystemJs两个核心方法import和register,希望本文对各位读者有所帮助。

最后,觉得本文不错的话,请不要吝啬手中的赞哦🌹🌹🌹

相关推荐
Martin -Tang24 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发25 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html