简单聊聊SystemJs

前言

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

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

SystemJs

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

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

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

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

基本使用

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

// 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。这个项目主要就是这份配置文件,其他文件,各位随便写点就好。笔者这边就仅写一个简单的入口文件。

const App = () => <div><h1>Hello World!</h1></div>;

export default App;

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

// package.json
"scripts": {
  "build": "webpack --env production"
},

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

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

我们先来看index.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

简单聊聊SystemJs

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

所以我们对index.html改造下

  • 先添加上SystemJs
<script src="https://cdn.bootcdn.net/ajax/libs/systemjs/6.10.1/system.min.js"></script>
  • 提供React和React-dom的加载链接
// 这是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方法。
<script>
  System.import('./index.js');
</script>

整个index.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文件。

简单聊聊SystemJs

页面中很成功的出现了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这两个核心方法。

简单实现

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

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

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

原文链接:https://juejin.cn/post/7347958786051538995 作者:猪头切图仔

(0)
上一篇 2024年3月20日 上午10:27
下一篇 2024年3月20日 上午10:38

相关推荐

发表评论

登录后才能评论