Для платформ SaaS, поскольку для разных клиентов требуется набор платформ, потребуются разные темы для переключения. В этой статье в основном обсуждается, как реализовать такие требования в проекте Vue.
несколько вариантов
Если есть спрос на продукт, мы должны найти способ удовлетворить его с помощью технологий.Поиски, мы нашли следующие решения:
- Сценарий 1, определение
themeпараметр, черезpropВыпущено, подкомпоненты основаны наthemeсвязывать динамическиstyleспособ достижения. Для получения подробной информации см.:Нетрадиционный — VUE реализует переключение тем для определенных сценариев.. - Вариант второй, через
AjaxПолучатьcss, затем замените в нем переменную цвета, а затем передайтеstyleстили вставки теговDOM. Для получения подробной информации см.:Практика создания скинов Vue. - Вариант 3, используйте программу, которая может работать прямо в браузере
less, который динамически компилируется путем передачи переменных.Ant Design ProВот как работает тема онлайн-переключения . - Вариант четвертый, для всех
cssСелектор добавляет селектор класса с именем стиля и связывает это имя класса дляbodyэлемент, затем передатьDOM APIдля динамического переключения тем. Следующий код демонстрирует, как передатьlessКомпиляция единая для всехcssСелектор добавляет селектор класса для имени стиля.
.white(@v) when(@v = 1) {
@import "~@/assets/css/theme-white.less";
}
.dark(@v) when(@v = 2) {
@import "~@/assets/css/theme-dark.less";
}
.generate(@n) when (@n < 3) {
.theme-@{n} {
.white(@n);
.dark(@n);
.fn();
}
.generate(@n + 1);
}
.generate(1);
Вышеупомянутые решения могут достичь цели переключения тем, но мы все же можем подумать более глубоко, можем ли мы найти более совершенное решение?
уточнение сцены
- Вариативное решение не удовлетворяет сложным определениям темы, таким как изменение макета.
- Как избежать большого количества условных суждений, которые приводят к внесению в код большого количества нерелевантного для бизнеса шума и увеличению затрат на обслуживание?
- резервировать
defaultФункция, если новая тема не определяет стиль функционального модуля, этот функциональный модуль не должен влиять на использование функции с точки зрения макета и визуального стиля. Как и в случае, когда локализации недостаточно, все же необходимо иметь возможность отображать меню на английском языке, не затрагивая использование функций. - С точки зрения производительности файл стиля предпочтительно должен загружаться по запросу, и должна загружаться только соответствующая тема.
cssдокумент. - Для динамической маршрутизации скрипты модулей и соответствующие стили подгружаются также по запросу.Как в этом случае динамически переключать темы?
Видно, что после уточнения сцены ни одно из вышеперечисленных решений не может удовлетворить требованиям. Поэтому далее я представлюwebpackПодключаемый модуль для переключения тем проекта Vue.
анализ спроса
С точки зрения разработчиков (то есть целевых пользователей программы) мы шаг за шагом проанализируем процесс генерации этой программы.
Прежде всего, нам нужно иметь возможность легко получить текущую тему, чтобы судить о текущей форме отображения интерфейса. Конечно, чтобы добиться переключения в реальном времени, эта переменная должна быть «отзывчивой»! Например:
{
computed: {
lineStyle() {
let color;
// eslint-disable-next-line default-case
switch (this.$theme) {
case 'dark':
color = '#C0C4CC';
break;
case 'light':
default:
color = '#000000';
break;
}
return { color };
},
},
}
Во-вторых, лучше не делать много условных суждений в коде стилей, проще поддерживать стили одной темы вместе.
<style lang="less" theme="dark">
header {
nav {
background-color: #262990;
.brand {
color: #8183e2;
}
}
.banner {
background-color: #222222;
}
}
</style>
Наконец, лучше всегоcssдиалектно-агностический, т.е. используется ли онlessещеsassилиstylus, может поддерживать.
import 'element-ui/lib/theme-chalk/index.css';
import './styles/theme-light/index.less?theme=light';
import './styles/theme-dark/index.scss?theme=dark';
Реализация
Далее мы подробно расскажем о деталях реализации этой схемы.
стадия развития
На этапе разработки дляvueпроект, общая практика заключается в том, чтобы передать стиль черезvue-style-loaderизвлечены, а затем переданы<style>Динамическая вставка теговDOM.
просмотревvue-style-loaderизисходный кодзнать, стиль<style>вставки и обновления выполняются через/lib/addStylesClient.jsМетоды, представленные в этом файле, реализованы.
Во-первых, мы можем начать сthis.resourceQueryИмя темы, соответствующее стилю, анализируется для оценки при вставке последующих стилей.
options.theme = /\btheme=(\w+?)\b/.exec(this.resourceQuery) && RegExp.$1;
Таким образом, имя темы, соответствующее стилю, следует заoptionsобъект передается вaddStylesClientметод.
оthis.resourceQuery, вы можете просмотретьwebpackдокументация.
Затем мы переписываемaddStyleметод для загрузки соответствующего стиля в соответствии с текущей темой. При этом отслеживать событие смены названия темы, устанавливать стиль, соответствующий текущей теме, в функции обратного вызова и удалять стиль неактуальной темы.
if (options.theme && window.$theme) {
// 初次加载时,根据主题名称加载对应的样式
if (window.$theme.style === options.theme) {
update(obj);
}
const { theme } = options;
// 监听主题名称变化的事件,设置当前主题样式并删除非当前主题样式
window.addEventListener('theme-change', function onThemeChange() {
if (window.$theme.style === theme) {
update(obj);
} else {
remove();
}
});
// 触发hot reload的时候,调用updateStyle更新<style>标签内容
return function updateStyle(newObj /* StyleObjectPart */) {
if (newObj) {
if (
newObj.css === obj.css
&& newObj.media === obj.media
&& newObj.sourceMap === obj.sourceMap
) {
return;
}
obj = newObj;
if (window.$theme.style === options.theme) {
update(obj);
}
} else {
remove();
}
};
}
оtheme-changeсобытия, вы можете просмотреть следующиеРеализовать переключение тем.
Таким образом, мы поддерживаем переключение нескольких тем на этапе разработки.
онлайн-среда
Для онлайн-среды ситуация немного сложнее. потому что мы можем использоватьmini-css-extract-pluginБудуcssфайловое разделениеchunkэкспортировать в несколькоcssфайл и загружается динамически, поэтому нам нужно решить: как экспортировать файлы стилей по темам, как загружать динамически, какhtmlЗапись загружает только файлы стилей для текущей темы.
Кратко представимmini-css-extract-pluginэкспортcssРабочий процесс для файлов стилей:
Шаг первый: вloaderизpitchэтап, преобразовать стиль вdependency(Плагин использует расширение отwebpack.DependencyобычайCssDependency);
Шаг второй: вpluginизrenderManifestВ крючок, позвонитеrenderContentAssetспособ настройкиcssвывод файла. Этот метод поставитjsНесколько стилей зависимостей модулей экспортируются в одинcssв файле.
Шаг третий: вentryизrequireEnsureкрючок, судя поchunkIdнайти соответствующийcssссылка на файл, созданнаяlinkМетки загружаются динамически.здесьвставит абзац в исходный кодjsСкрипты используются для динамической загрузки стилейcssдокумент.
Следующий,html-webpack-pluginбудуentryсоответствующийcssвводить вhtml, чтобы обеспечить визуализацию стиля страницы входа.
Экспорт файлов стилей по теме
нам нужен макияжrenderContentAssetметод, добавленный в логику слияния файла стиляthemeсуждение. Основная логика выглядит следующим образом:
const themes = [];
// eslint-disable-next-line no-restricted-syntax
for (const m of usedModules) {
const source = new ConcatSource();
const externalsSource = new ConcatSource();
if (m.sourceMap) {
source.add(
new SourceMapSource(
m.content,
m.readableIdentifier(requestShortener),
m.sourceMap,
),
);
} else {
source.add(
new OriginalSource(
m.content,
m.readableIdentifier(requestShortener),
),
);
}
source.add('\n');
const theme = m.theme || 'default';
if (!themes[theme]) {
themes[theme] = new ConcatSource(externalsSource, source);
themes.push(theme);
} else {
themes[theme] = new ConcatSource(themes[theme], externalsSource, source);
}
}
return themes.map((theme) => {
const resolveTemplate = (template) => {
if (theme === 'default') {
template = template.replace(REGEXP_THEME, '');
} else {
template = template.replace(REGEXP_THEME, `$1${theme}$2`);
}
return `${template}?type=${MODULE_TYPE}&id=${chunk.id}&theme=${theme}`;
};
return {
render: () => themes[theme],
filenameTemplate: resolveTemplate(options.filenameTemplate),
pathOptions: options.pathOptions,
identifier: options.identifier,
hash: options.hash,
};
});
Здесь мы определяемresolveTemplateметод, для выводаcssимя файла поддерживается[theme]этот заполнитель. При этом в возвращаемом нами имени файла была строкаquery, это облегчает запрос информации, соответствующей файлу стиля после компиляции.
Динамически загружать стилиcssдокумент
Ключевым здесь являетсяchunkIdнайти соответствующийcssссылка на файл, наmini-css-extract-pluginВ реализации итоговая ссылка на файл может быть вычислена напрямую, но в нашем сценарии это неприменимо, так как на этапе компиляции мы не знаем, что загружатьthemeчто. Реальная идея состоит в том, чтобы вставитьresolveметод, во время выполнения в соответствии с текущимthemeразобрать полныйcssссылку на файл и вставить вDOMсередина. Здесь используем другую идею: собрать все темыcssадрес файла стилей и естьmap, при динамической загрузке, согласноchunkIdа такжеthemeотmapнайти финалcssссылка на файл.
Ниже приведена реализация внедренного кода на этапе компиляции:
compilation.mainTemplate.hooks.requireEnsure.tap(
PLUGIN_NAME,
(source) => webpack.Template.asString([
source,
'',
`// ${PLUGIN_NAME} - CSS loading chunk`,
'$theme.__loadChunkCss(chunkId)',
]),
);
Следующее находится в стадии выполнения в соответствии сchunkIdнагрузкаcssРеализация:
function loadChunkCss(chunkId) {
const id = `${chunkId}#${theme.style}`;
if (resource && resource.chunks) {
util.createThemeLink(resource.chunks[id]);
}
}
инъекцияentryсоответствующийcssссылка на файл
Потому что после разделения на несколько тем,entryможет генерировать несколько на основе нескольких темcssфайл, они вводятся вhtmlсреди них, поэтому нам нужно удалить тему не по умолчаниюcssссылка на файл.
html-webpack-pluginЧтобы помочь нам выполнять эти операции, предоставляются хуки, и на этот раз нам, наконец, не нужно менять исходный код плагина.
регистрalterAssetTagsОбратный вызов хука может поместить все темы не по умолчанию, соответствующиеlinkЯрлыки удалены:
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(PLUGIN_NAME, (data, callback) => {
data.head = data.head.filter((tag) => {
if (tag.tagName === 'link' && REGEXP_CSS.test(tag.attributes && tag.attributes.href)) {
const url = tag.attributes.href;
if (!url.includes('theme=default')) return false;
// eslint-disable-next-line no-return-assign
return !!(tag.attributes.href = url.substring(0, url.indexOf('?')));
}
return true;
});
data.plugin.assetJson = JSON.stringify(
JSON.parse(data.plugin.assetJson)
.filter((url) => !REGEXP_CSS.test(url) || url.includes('theme=default'))
.map((url) => (REGEXP_CSS.test(url) ? url.substring(0, url.indexOf('?')) : url)),
);
callback(null, data);
});
Реализовать переключение тем
инъекцияthemeПеременная
использоватьVue.util.defineReactive, вы можете определить «отзывчивую» переменную, чтобы поддерживать обновление вычисляемых свойств компонента и визуализацию компонента.
export function install(Vue, options = {}) {
Vue.util.defineReactive(theme, 'style');
const name = options.name || '$theme';
Vue.mixin({
beforeCreate() {
Object.defineProperty(this, name, {
get() {
return theme.style;
},
set(style) {
theme.style = style;
},
});
},
});
}
Получить и установить текущую тему
пройти черезObject.definePropertyПерехватывая операции значения и присваивания текущей темы, значение темы, выбранное пользователем, может быть сохранено в локальном кеше, а текущая установленная тема будет использоваться при следующем открытии страницы.
const theme = {};
Object.defineProperties(theme, {
style: {
configurable: true,
enumerable: true,
get() {
return store.get();
},
set(val) {
const oldVal = store.get();
const newVal = String(val || 'default');
if (oldVal === newVal) return;
store.set(newVal);
window.dispatchEvent(new CustomEvent('theme-change', { bubbles: true, detail: { newVal, oldVal } }));
},
},
});
Загрузите соответствующую темуcssдокумент
динамическая нагрузкаcssфайл черезjsСоздайтеlinkМетод метки может быть достигнут, единственное, что следует отметить, это то, что после переключения темыlinkОперация уничтожения тега. С учетом созданногоlinkЭтикетка тоже объект по сути, помните, что мы сохранилиcssадрес файла стиляmap? созданныйlinkСсылки на объекты метки могут также существовать в этомmap, чтобы можно было быстро найти соответствующую темуlinkпомечен.
const resource = window.$themeResource;
// NODE_ENV = production
if (resource) {
// 加载entry
const currentTheme = theme.style;
if (resource.entry && currentTheme && currentTheme !== 'default') {
Object.keys(resource.entry).forEach((id) => {
const item = resource.entry[id];
if (item.theme === currentTheme) {
util.createThemeLink(item);
}
});
}
// 更新theme
window.addEventListener('theme-change', (e) => {
const newTheme = e.detail.newVal || 'default';
const oldTheme = e.detail.oldVal || 'default';
const updateThemeLink = (obj) => {
if (obj.theme === newTheme && newTheme !== 'default') {
util.createThemeLink(obj);
} else if (obj.theme === oldTheme && oldTheme !== 'default') {
util.removeThemeLink(obj);
}
};
if (resource.entry) {
Object.keys(resource.entry).forEach((id) => {
updateThemeLink(resource.entry[id]);
});
}
if (resource.chunks) {
Object.keys(resource.chunks).forEach((id) => {
updateThemeLink(resource.chunks[id]);
});
}
});
}
остальная часть работы
мы проходимwebpackизloaderа такжеplugin, файл стиля разделен на одну тему в соответствии с темойcssфайл и реализуется через отдельный модульentryа такжеchunkСоответствующая темаcssЗагрузка файлов и динамическое переключение тем. Следующее, что нужно сделать, это ввестиcssсписок ресурсов в глобальную переменную, чтобыwindow.$themeВы можете найти стили через эту глобальную переменнуюcssдокумент.
На этом шаге мы по-прежнему используемhtml-webpack-pluginПредоставляет крючки, чтобы помочь нам с:
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(PLUGIN_NAME, (data, callback) => {
const resource = { entry: {}, chunks: {} };
Object.keys(compilation.assets).forEach((file) => {
if (REGEXP_CSS.test(file)) {
const query = loaderUtils.parseQuery(file.substring(file.indexOf('?')));
const theme = { id: query.id, theme: query.theme, href: file.substring(0, file.indexOf('?')) };
if (data.assets.css.indexOf(file) !== -1) {
resource.entry[`${theme.id}#${theme.theme}`] = theme;
} else {
resource.chunks[`${theme.id}#${theme.theme}`] = theme;
}
}
});
data.html = data.html.replace(/(?=<\/head>)/, () => {
const script = themeScript.replace('window.$themeResource', JSON.stringify(resource));
return `<script>${script}</script>`;
});
callback(null, data);
});
не идеально
Полная реализация кода может относиться кvue-theme-switch-webpack-plugin. Но этот метод изменяет дваwebpackПлагин пока не элегантен в реализации, а как его реализовать без модификации исходного кода плагина будет рассмотрено позже. Если у вас есть хорошая идея, вы можете обсудить ее вместе.