systemjs使用及原理
搭建项目
初始化项目
bash
npm init -y
安装依赖
bash
npm i react react-dom
npm i @babel/core @babel/preset-env @babel/preset-react babel-loader html-webpack-plugin webpack webpack-cli webpack-dev-server -D
配置webpack
.config.js
javascript
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = (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
package.json
json
{
"scripts": {
"dev": "webpack serve",
"build": "webpack --env production"
},
}
.babelrc
javascript
{
"presets": [
"@babel/preset-env",
["@babel/preset-react",{
"runtime":"automatic"
}]
]
}
src/index.js
javascript
import ReactDOM from "react-dom/client";
import App from "./App";
// 渲染App组件
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
src/App.js
javascript
function App() {
return (
<div>
<h1>Hello, World!</h1>
</div>
);
}
export default App;
public/index.html
html
<!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>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
启用项目
bash
npm run dev
打包项目
npm run build
生成dits/index.js
javascript
System.register(["react-dom","react"], function(__WEBPACK_DYNAMIC_EXPORT__, __system_context__) {
//...
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(){
// 页面渲染
}
}
实现systemjs
dist/index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport">
<title>Document</title>
</head>
<body>
主应用 - 基座 - 用来加载子应用的 webpack importMap
<div id="root"></div>
<script type="systemjs-importmap">
{
"imports":{
"react-dom":"https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js",
"react":"https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.development.js"
}
}
</script>
<script>
// 直接加载子应用, 导入打包后的包 来进行加载, 采用的规范 system规范
// 这个地方是自己实现systemjs
// 1) systemjs 是如何定义的 先看打包后的结果 System.register(依赖列表,后调函数返回值一个setters,execute)
// 2) react , react-dom 加载后调用setters 将对应的结果赋予给webpack
// 3) 调用执行逻辑 执行页面渲染
// 模块规范 用来加载system模块的
const newMapUrl = {};
// 解析 importsMap
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 {
import(id) { // 这个id原则上可以是一个第三方路径cdn
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').then(() => {
console.log('模块加载完毕')
})
// 本质就是先加载依赖列表 再去加载真正的逻辑
// (内部通过script脚本加载资源 , 给window拍照保存先后状态)
// JSONP
// single-spa 如何借助了 这个system 来实现了模块的加载
</script>
</body>
</html>