предисловие
Недавно я построил фреймворк для нового фонового проекта компании с помощью qiankun.Когда я посмотрел документ, я обнаружил, что параметры запуска метода запуска приложения включают такие параметры:
sandbox - `boolean` | `{ strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean }` // 可选,是否开启沙箱,默认为 `true`
В сочетании с описанием на официальном сайте strictStyleIsolation означает строгую изоляцию стилей, что на самом деле является использованиемshadowDomОберните каждое подприложение, и ExperimentStyleIsolation добавит текущий контейнер монтирования перед всеми селекторами стилей.Это легко понять, взглянув на пример с официального сайта:
// 假设应用名是 react16
.app-main {
font-size: 14px;
}
div[data-qiankun-react16] .app-main {
font-size: 14px;
}
Любопытство заставило меня исследовать, как это достигается.Давайте посмотрим, как достигается цянькунь.
реализация цянькунь
Получить все теги стиля и обработать каждый тег
Если вы установите изоляцию стилей, все теги стилей qiankun попадут в подприложения, затем волна пройдет его операцию, следующим шагом будет серия ключевых документов css.js, логика, функция css.process:
// loader.js
var styleNodes = appElement.querySelectorAll('style') || [];
_forEach(styleNodes, function (stylesheetElement) {
css.process(appElement, stylesheetElement, appName);
});
// css.js
export var process = function process(appWrapper, stylesheetElement, appName) {
// ...
// 如果是外部样式表就给个提示,不处理这种
if (stylesheetElement.tagName === 'LINK') {
console.warn('Feature: sandbox.experimentalStyleIsolation is not support for link element yet.');
}
// ...
var tag = (mountDOM.tagName || '').toLowerCase();
// 当样式是内联样式时对齐进行进一步的处理
if (tag && stylesheetElement.tagName === 'STYLE') {
var prefix = "".concat(tag, "[").concat(QiankunCSSRewriteAttr, "=\"").concat(appName, "\"]");
processor.process(stylesheetElement, prefix);
}
};
function process(styleNode) {
var _this = this;
// 获取函数的第二个参数作为前缀
var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var _a;
// 当该style标签内有内容时进行操作
if (styleNode.textContent !== '') {
var textNode = document.createTextNode(styleNode.textContent || '');
// 这里的swapNode是之前创建好的空style,起到工具人的作用,完事后会把里面的内容清空
this.swapNode.appendChild(textNode);
// 通过StyleElement.sheet API获取样式表对象CSSStyleSheet
var sheet = this.swapNode.sheet;
// 从CSSStyleSheet的rules或cssRules字段获取所有样式规则,并将这个伪数组转为数组
var rules = arrayify((_a = sheet === null || sheet === void 0 ? void 0 : sheet.cssRules) !== null && _a !== void 0 ? _a : []);
// 给每条css规则的选择器增加前缀后返回修改后的css
var css = this.rewrite(rules, prefix); // eslint-disable-next-line no-param-reassign
// 将处理后的css放到传入的style元素中
styleNode.textContent = css; // cleanup
// 工具人完成了使命,用完清理干净
this.swapNode.removeChild(textNode);
return;
}
}
function process(styleNode) {
var _this = this;
// 获取函数的第二个参数作为前缀
var prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var _a;
var mutator = new MutationObserver(function (mutations) {
var _a;
for (var i = 0; i < mutations.length; i += 1) {
var mutation = mutations[i];
// ScopedCSS.ModifiedTag是之前已经设置好的Symbol对象 'Symbol(style-modified-qiankun)' 值应该是起到内部节点变更后重复触发mutaionObserver监听的作用
if (ScopedCSS.ModifiedTag in styleNode) {
return;
}
if (mutation.type === 'childList') {
// 下面的代码和前半段一样,只是少了工具人的参与,为什么呢🤔?
var _sheet = styleNode.sheet;
var _rules = arrayify((_a = _sheet === null || _sheet === void 0 ? void 0 : _sheet.cssRules) !== null && _a !== void 0 ? _a : []);
var _css = _this.rewrite(_rules, prefix); // eslint-disable-next-line no-param-reassign
styleNode.textContent = _css; // eslint-disable-next-line no-param-reassign
styleNode[ScopedCSS.ModifiedTag] = true;
}
}
});
// 源码在这里加了段注释,大意说因为节点删除后监听会被移除,因此就没有弄一个清理的函数
// 官网地址: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/disconnect
// 监听当前style标签
mutator.observe(styleNode, {
childList: true
});
}
MutaionObserverЧтобы прослушивать пустые теги стиля и выполнять работу по добавлению префиксов к селекторам css при изменении узла.
Обработать каждый стиль в узле стиля
В функции перезаписи она проходит через все стили и определяет, какой тип селектора (обычный селектор, @media, @supports селектор) в настоящее время основан на CSSStyleRule.type каждого стиля, и рекурсивно использует перезапись для селекторов @media и @supports. Для внутренних узлов в основном обратите внимание на функцию ruleStyle, которая обрабатывает общие стили:
function ruleStyle(rule, prefix) {
var rootSelectorRE = /((?:[^\w\-.#]|^)(body|html|:root))/gm;
var rootCombinationRE = /(html[^\w{[]+)/gm;
var selector = rule.selectorText.trim();
var cssText = rule.cssText;
// 下面两个判断是针对根选择器(body、html、:root)的判断的,处理逻辑是会将根选择器替换为prefix,也就是当前子应用挂载容器的选择器
if (selector === 'html' || selector === 'body' || selector === ':root') {
return cssText.replace(rootSelectorRE, prefix);
}
// 如果选择器是html开头并且带有后代元素
if (rootCombinationRE.test(rule.selectorText)) {
// 如果是不标准的用法,如html + body就不会对根选择器做处理,否则会把html删除
var siblingSelectorRE = /(html[^\w{]+)(\+|~)/gm;
if (!siblingSelectorRE.test(rule.selectorText)) {
cssText = cssText.replace(rootCombinationRE, '');
}
}
cssText = cssText.replace(/^[\s\S]+{/, function (selectors) {
// 以字符串或,开始,到下一个逗号之前结束,第一个参数是匹配到的整个字符串
// 第二个参数是空或者, 第三个参数是,之后到下一个逗号或结尾的内容
return selectors.replace(/(^|,\n?)([^,]+)/g, function (item, p, s) {
// 处理带有根选择器的分组选择器 如:div,body,span { ... }
if (rootSelectorRE.test(item)) {
return item.replace(rootSelectorRE, function (m) {
// 处理body、html 及 *:not(:root) 这样的情况,将根选择器直接转为前缀
var whitePrevChars = [',', '('];
if (m && whitePrevChars.includes(m[0])) {
return "".concat(m[0]).concat(prefix);
}
return prefix;
});
}
// 在选择器之前加上前缀,并且删除之前选择器前面的所有空格
return "".concat(p).concat(prefix, " ").concat(s.replace(/^ */, ''));
});
});
return cssText;
}
Основная логика состоит в том, чтобы использовать регулярные выражения для HTML, Body, Coot roots Эти селекторы, замененные на входящий префикс (фактически селектор CSS CSS CSS CSS-контейнера под приложениями), до обычных селекторов со своими собственными монтажами уникальным префиксом контейнера, А затем верните CSS, а затем лечение, чтобы вы могли сделать стиль текущих подположений, не повлияет на другие поддержанные приложения и родительское приложение.
резюме
Основная логика обработки параметра ExperimentStyleIsolation в qiankun несложна: пройтись по всем тегам стиля, получить все селекторы стиля и текст css в каждом теге соответственно (используйте StyleElement.sheet для получения CSSStyleSheet, чтобы получить конкретную информацию о каждом стиле). ), и для его обработки корневой селектор заменяется префиксом, а обычный селектор имеет префикс напрямую.
Используйте postcss для обработки css
Принцип qiankun заключается в том, чтобы запрашивать соответствующие ресурсы подприложения после загрузки основного приложения и монтировать подприложение в одну из его собственных точек монтирования.На основе этого механизма во время выполнения могут обрабатываться только DOM или строки. . Если мы столкнемся с необходимостью добавления префиксов ко всем css-селекторам в приложении при разработке (например, при добавлении плагина на другую страницу, чтобы написанный нами плагин не влиял на исходный стиль страницы), мы можем использовать postcss для генерации css ast для выполнения связанных операций с css.
Введение в ast из css
Подобно babel, который может генерировать ast для js, postcss также может генерировать ast для css изОфициальный сайтОбъяснение типов переносимых узлов:
-
Root
: корневой узел ast, представляющий каждый файл css. -
AtRule
Каждое правило, где селектор начинается с @ -
Rule
: селекторы и объявления CSS вместе образуют правило CSS, напримерinput, button {font-size: 20px;}
Который должен объявить брекеты -
Declaration
: Следуйте за оператором, каждое правило в задней части брекетов селектора -
Comment
: комментарии, которые могут существовать в селекторах, параметрах @selectors и значениях пар "ключ-значение" в стиле css. Комментарии в узлах будут сохранены в свойстве raws узла.
Различные узлы имеют некоторые свойства и API, включая родитель, тип, во-первых, текст, сырье, клон (), заправленные с помощью (Newnode), WalkRules (правило) и тому подобное, могут ссылаться на конкретныеофициальная документация
-
Тип: тип узла
-
clone: метод для каждого из узлов, упомянутых выше, функция состоит в том, чтобы скопировать узел
-
replaceWith: у каждого узла также есть способ заменить текущий узел входящим узлом.
-
walkRules: ast обход каждого узла Правило
Если вы используете плагин postcss только в среде узла, вы можете написать это так (см. Пример с официального сайта):
const postcss = require('postcss')
const plugin = () => {
return {
postcssPlugin: 'to-red', // 必须有
Rule (rule) { // 遍历每个css文件的Rule节点
console.log(rule.toString())
},
Declaration (decl) { // 遍历每个css文件的Declaration节点
console.log(decl.toString())
decl.prop === 'color' && (decl.value = 'red')
}
}
}
plugin.postcss = true // 必须有
postcss([plugin]).process('a { color: black; font-size: 100px; }').then(res => {
console.log(res.toString()) // 将转换后的结果输出
})
// 输出:'a { color: red; font-size: 100px; }'
Если вы хотите написать плагин PostCSS, который можно поместить в WebPack.config.js, вам нужно использовать метод postcss.clugin для его реализации (см. Официальный сайтПоследний метод записи плагинаЯ всегда сообщаю об ошибке, поэтому использую старый способ написания), непосредственно в код, который добавляет префикс к каждому селектору css:
module.exports = postcss.plugin('postcss-add-css-prefix', function(opts = {}) {
const {
prefix = ''
} = opts
// 接收两个参数,第一个是每个css文件的ast,第二个参数中可获取转换结果相关信息(包括当前css文件相关信息)
function plugin(css, result) {
if (!prefix) return; // 没传入prefix,不执行下面的逻辑
css.walkRules(rule => { // 遍历当前ast所有rule节点
const {
selector
} = rule
// 只有当节点是ast根节点直属子节点时才添加前缀
// 简单做了容错处理,只要带有根选择器的都不添加前缀,本身带有前缀了也不添加
// 加了个flag,防止节点更新后重复执行该逻辑进入死循环
if (rule.parent.type === 'root' && !(selector.includes(':root') || selector.includes('body') || selector.includes('html') || selector.includes(prefix)) && !rule.flag) {
rule.flag = true
const clone = rule.clone()
clone.selector = `${prefix} ${selector}`
rule.replaceWith(clone)
}
})
}
return plugin
})
postcss.plugin получает два параметра, первый — это имя плагина, второй — функция обратного вызова, функция обратного вызова получает параметры, переданные при настройке в плагине postcss, и возвращает функцию, возвращаемый плагин функции получает два параметра: во-первых, это ast после анализа каждого файла css, а во-вторых, информация о результате после преобразования ast. Функция плагина использует walkRules для обработки всех узлов Rule ast (walkDecls обрабатывает все узлы объявлений, walk обрабатывает все типы узлов, см.официальная документация), получить селектор из узла Rule и заменить предыдущий узел измененным узлом после модификации. Кроме того, вы можете получить текущую информацию о CSS-файле по второму параметру плагина (поле result.opts.file) и добавить разные префиксы к селекторам в каждом CSS-файле.
Наконец, зарегистрируйте только что написанный плагин в конфигурации postcss.config.js или webpack.config.js и других файлах (в качестве примера возьмем postcss.config.js):
// postcss.config.js
const addCssPrefix = require('./addCssPrefix')
module.exports = {
plugins: [
addCssPrefix({
prefix: 'body'
})
]
}