第2期 - 钢之炼金术士

1,169 阅读7分钟

开篇

本周开始五一假期,计划回家一趟,所以在回家前把上周割的内容补一下,之后可能会把文章整体搬迁,具体计划还在考虑中。

对于内容,也在考虑怎么才能让阅读过程所花费的时间变得有价值(不仅是对于自己,也对于所有花费时间来看的人)。当然内容主要还是自我总结和探讨为主,本期开始会追加动漫的推荐以点题。

技术

qiankun

基于single-spaqiankun是目前微前端(容器)届比较亮的那颗星了,最近的工作中也应用qiankun踩了一些网上不常见的坑,简单总结几点:

1. 关于angularjs和qiankun的整合

子项目是基于angularjs的项目,使用fis构建(百度出品,非webpack)。对于非webpack打包工程(例如:jquery),qiankun官方提供了qiankun的标准方案 qiankun非webpack构建应用

但是对于angularjs使用了require.js,在require.js中定义了requiredefine的变量,但是qiankun的沙箱机制导致这些变量的有效范围发生了变化,需要在window上挂载requiredefine,使得变量挂载到子应用到全局,这部分改动可以补充在require.js底部,不过,建议可以用另一个文件引用require.js,将这部分代码追加到新文件中。

// require.polyfill.js

include('require.js');

window.require = require;
window.requirejs = requirejs;
window.define = define;

解决掉方法挂载,会发现require.js依赖的模块加载,仍然提示define未定义,然后审查页面元素,会发现插入部分的脚本脱离了qiankun的沙箱机制机制,为什么呢?

这里涉及两个知识点:

1)requirejs的脚本插入

require.js中插入脚本的时候会先去查找<head>节点,之后查找<base>节点,如果找到了<base>节点,会把<base>节点的父节点作为插入位置,之后调用insertBefore插入脚本

// Line:1802
if (isBrowser) {
    head = s.head = document.getElementsByTagName('head')[0];
    //If BASE tag is in play, using appendChild is a problem for IE6.
    //When that browser dies, this can be removed. Details in this jQuery bug:
    //http://dev.jquery.com/ticket/2709
    baseElement = document.getElementsByTagName('base')[0];
    if (baseElement) {
        head = s.head = baseElement.parentNode;
    }
}
// Line: 1904
if (baseElement) {
    head.insertBefore(node, baseElement);
} else {
    head.appendChild(node);
}

2)qiankun沙箱处理机制

qiankun利用import-html-entry的包,对所有加载入口文件index.html进行了加载,对HTMLHeadElementinsertBeforeappendChild进行了重载,并对HTMLBodyElementappendChild进行了重载

// common.ts  Line: 305  patchHTMLDynamicAppendPrototypeFunctions
if (
    HTMLHeadElement.prototype.appendChild === rawHeadAppendChild &&
    HTMLBodyElement.prototype.appendChild === rawBodyAppendChild &&
    HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore
  ) {
    HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadAppendChild,
      containerConfigGetter,
      isInvokedByMicroApp,
    }) as typeof rawHeadAppendChild;
    HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawBodyAppendChild,
      containerConfigGetter,
      isInvokedByMicroApp,
    }) as typeof rawBodyAppendChild;

    HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any,
      containerConfigGetter,
      isInvokedByMicroApp,
    }) as typeof rawHeadInsertBefore;
  }

重载过程主要是处理<script><style>其中:getOverwrittenAppendChildOrInsertBefore方法源码,以上两个部分在结合使用到时候出现了一个问题,angular中的<base>原本是在<head>

<html>
    <head>
        <base href="/" />
    </head>
</html>

而在子应用插入qiankun的时候,默认会移除掉<html>节点和<head>节点(暂时没看到这是在哪一步完成的),从而<base>的父节点从HTMLHeadElement变成了HTMLBodyElement

<div id="__qiankun_microapp_wrapper_for_xxxxx">
    <base href="/" />
</div>

刚才代码中我们看到qiankun没有重载HTMLBodyElementinsertBefore方法(原因不明),导致该脚本直接被插入到了基座应用中,基座应用当然不包含之前暴露的window.define导致报错。

因此解决方案,要么修改require.js的查找机制,要么处理<base>节点位置,最后和有经验的同事商量后,决定在require.js加载前,动态在插入<base>结点到基座的<head>中,解决问题:

<script type="text/javascript">
    if (!document.getElementsByTagName('head')[0].querySelector('base')) {
        document.getElementsByTagName('head')[0].appendChild(document.getElementsByTagName('base')[0]);
    }
</script>

2. 关于静态资源加载

qiankun沙箱对于style的处理逻辑,会将link的子应用作为<style>插入基座应用中,如果子应用的CSS中使用了相对路径(例如:background:url(../xxx),字体文件等),则此时相对路径会被变成相对基座应用的路径,从而无法找到引用的文件,此时可以使用官方提供的方案: 资源加载404

简单来说,方案是将相对路径变成绝对路径,例如:webpack打包的可以设置publicPath进行路径替换

3. 其他的改造

main.js是核心的改造,angularjsbootstrap只允许调用一次,那么两个angularjs应用切换的过程,需要进行额外的处理(render方法中的逻辑),具体可参见以下内容:

function render(props) {
    // Require JS  Config File
    require.config(__inline('require.config.json'));

    const staticHTML = `<div ng-controller="AppCtrl">
        <div ui-view></div>
        <div uix-notify></div>
    </div>`
    // check angular frame is bootstrap, it need bootstrap only once
    if (__state.instance) {
        angular.element(__state.el).html(staticHTML);
        const link = __angular__.$compile(angular.element(__state.el).contents())
        link(__state.$scope);
        getContainerEl().appendChild(__state.el);
    } else {
        const el = document.createElement('div');
        el.innerHTML = staticHTML;
        __state.el = el;
        getContainerEl().appendChild(__state.el);

        require(['appInit'], function (app) {
            __state.instance = angular.bootstrap(el, [app.name], {
                strictDi: true
            });

            // __state.$scope is just $rootScope
            __state.$scope = angular.element(el).scope();

        });
    }
}

const mount = () => {
    return new Promise((resolve, reject) => {
        getHorn().then(() => {
            render();
            resolve();
        });
    });
}
const unmount = () => {
    return new Promise((resolve, reject) => {
        // don't destroy $rootScope, ui-xg components would not work, maybe some other unknown affects?
        // just clear AppCtrl
        angular.element(__state.el.querySelector('div[ng-controller=AppCtrl]')).scope().$destroy();

        __state.el.innerHTML = '';
        getContainerEl().removeChild(__state.el);

        resolve();
    });
}

const getContainerEl = () => {
    let element = document.getElementById('app');
    if (!element) {
        throw new Error(`domElementGetter did not return a valid dom element`);
    }
    return element;
}

if (!window.__POWERED_BY_QIANKUN__) {
    getHorn().then(() => {
        render();
    });
} else {
    ((global) => {
        global['/xxxxx'] = {
            bootstrap: () => {
                return Promise.resolve();
            },
            mount: mount,
            unmount: unmount,
        };
    })(window);
}

整个改造过程花费了比较多的时间,也爬了比较多的坑,但是对于自身来说,收益是很明显的,很喜欢这个解决问题的整个过程和思路,也很感谢Leader对我的帮助。

非技术

1. Duet

一款很不错的分屏工具,可以将电脑屏幕通过数据线分屏到iphone,pad等设备,看到有人使用以后觉得真的这个十分赞, Duet

动漫推荐

《钢制炼金术士》

本期是动漫推荐的第一期,《钢之炼金术师》简称《钢炼》,被bones(骨头社)制作了两个版本,分别是03版本FA版本,其中的03版本使用了原创结局,两个版本都很出色,b站最近限免中。

世界观

钢炼基于等价交换的这一偏哲学意味的主题,所有的一切都是围绕此展开的,在一个虚构的世界,描绘了生活在这里的形形色色的正/反双方的观点对冲。

剧情走向

从艾尔利克兄弟为了复活病逝的母亲,尝试了禁忌的人体炼成,兄弟两人分别被人体炼成夺取了等价之物,为了夺回弟弟的身体,两人踏上了探索世界的道路。

推荐理由

推荐钢炼的理由有三点:

1)完整的人物刻画

钢炼是我看过的对于人物刻画特别完整的动漫,这里的完整是指人物前后行为逻辑的自洽,每个角色的行为都是其性格的体现。

每一个人物的出场和退场,都让这个故事的整体主线变得丰满,没有不必要的人,每个人在某个时刻总会发挥着特别的作用。

正如我对社会的认识一样,人类社会就是复杂的人类综合体,每个人都在其中推进着世界的进步,你可能不是明面上的主角,但是你一定是你自己的主角。

2)没有无限膨胀的剧情和战力

大部分少年漫特点就是战力崩坏,大家也喜欢各种傲天的类型(我也喜欢),而钢炼很好的控制了这个问题,从始至终,战力就是很平衡的状态,主角从来没有经历什么变得强的一发不可收拾,相反,你可以看到其实两兄弟一直在吃瘪。

没有传统少年漫的爽快感,反而更能整个剧情的饱满,剧情的推进不是依赖打怪升级,而是对这个世界的探索,对“真理”的探索一步一步推进的,牛姨想要表达的世界观。

3)唯一的世界法则

等价交换的法则,在看完03版本以后,我自身把这一法则认为了是唯一真理。所有的行为,都会导致其结果,其结果必定是合理的等价,只是这里的等价并不是以个人的意识为转移,而是真正的等价,也就是说,艾尔利克兄弟付出代价复活了母亲,只是这里的母亲并不等于他们所以想要的母亲,但是从原则上来说辅助的代价和母亲确实是等价的,简单来说,等价交换的产物不一定是你想要的,但是一定是对等的。

关于钢炼的整体在动漫领域的分析,其实很多人都在吹,但是动漫和其他的文艺作品一样,更多的还是要自己去体会,也许对于你来说他很不好,也正是大家能有各自的意见而且也能容纳他人的看法,社会才一直在进步

尾声

五一期间除了休息,经历了很难受的事情,也有反思,反思自己的问题,处理事情的方式和方法。希望大家能假期快乐!