Создание одностраничного приложения с микроинтерфейсом

внешний интерфейс JavaScript React.js CSS

предисловие

микро интерфейсПредложенная ThoughtWorks в 2016 году концепция внутренних микросервисов применяется к стороне браузера, то есть веб-приложение преобразуется из единого монолитного приложения в приложение, которое объединяет несколько небольших внешних приложений в одно.

Meituan уже является крупной интернет-компанией с десятками тысяч человек, поэтому очень важно повысить общую эффективность, для поддержки которой требуется множество внутренних и внешних систем управления. Поскольку между этими системами существует множество требований к подключению и взаимодействию, мы надеемся объединить эти системы в одну или несколько комплексных систем в соответствии с пользователями и сценариями использования.

Мы называем такое одностраничное приложение, объединенное несколькими микроинтерфейсами, «квазиодностраничным приложением», и система управления персоналом Meituan реализована на основе этого дизайна. Система Meituan HR состоит из более чем 30 микроинтерфейсных приложений, в том числе более 1000 страниц и более 300 пунктов навигационного меню. Для пользователей HR-система представляет собой одностраничное приложение, и весь процесс взаимодействия проходит очень плавно, для разработчиков каждое приложение можно самостоятельно разрабатывать, тестировать и выпускать самостоятельно, что значительно повышает эффективность разработки.

Далее в этой статье будут представлены некоторые методы «микроинтерфейсного построения одностраничных приложений» в системе управления персоналом Meituan. В то же время мы также делимся некоторыми своими мыслями и опытом, надеясь вдохновить всех.

Микро интерфейсный дизайн HR-системы

Поскольку система управления персоналом Meituan включает в себя множество проектов, в настоящее время за нее отвечают три команды. Среди них: команда OA отвечает за такие функции, как посещаемость, контракты и процессы, команда HR отвечает за такие функции, как поступление, регуляризация, перевод и увольнение, а команда Шанхая отвечает за такие функции, как производительность и найм. . Такое разделение команд и функций делает каждую систему относительно независимой, с независимыми доменными именами, независимым дизайном пользовательского интерфейса и независимыми технологическими стеками. Однако это приведет к таким проблемам, как нечеткое распределение обязанностей между командами разработчиков и плохой пользовательский опыт, поэтому необходимо срочно преобразовать HR-систему в систему только с одним доменным именем и одним набором стилей представления.

Для того, чтобы соответствовать требованиям развития бизнеса компании, мы сделали страницу HR-портала, связав воедино входы различных подсистем. Однако мы обнаружили, что HR-портал имеет очень мало значения: после того, как пользователь дважды прыгнул, он не знал, куда прыгнуть. Поэтому мы решаем вышеуказанные проблемы, интегрируя HR-систему в приложение.

Вообще говоря, есть два основных способа реализовать «одностраничное приложение»:

  1. встроенный iframe
  2. Одностраничное приложение с объединенным интерфейсом Micro

Среди них метод встраивания iframe относительно прост в реализации, но в процессе практики он приносит следующие проблемы:

  • Подпроект нуждается в дооснащении и должен предоставить набор функций без навигации
  • Размер области отображения, встроенной в iframe, контролировать непросто, и существуют определенные ограничения.
  • Запись URL-адреса полностью недействительна, обновление страницы невозможно запомнить, и обновление вернется на домашнюю страницу.
  • Переход между функциями iframe недопустим.
  • отображение стиля iframe, совместимость и т. д. имеют ограничения

Учитывая эти проблемы, встраивание iframe не может удовлетворить потребности нашего бизнеса, поэтому мы начали строить HR-систему с микроинтерфейсом.

В этом решении для микроинтерфейса есть несколько проблем, которые мы должны решить:

  1. Внешний интерфейс должен соответствовать нескольким внутренним интерфейсам.
  2. Предоставьте набор механизмов регистрации приложений для полной интеграции приложений.
  3. Интеграция приложений во время сборки и независимый от приложений выпуск и развертывание

Только решая вышеуказанные проблемы, наша интеграция может быть эффективной и действительно реализуемой Далее мы подробно объясним идеи реализации этих проблем.

Один интерфейс соответствует нескольким бэкэндам

Система управления персоналом, наконец, запускает одностраничное приложение онлайн, и приложение должно быть независимым в разработке проекта, поэтому мы создали новый входной проект для интеграции различных приложений. В нашей практике этот проект называется «Портальный проект» или «основной проект», а бизнес-приложение — «подпроект».Вся схема структуры проекта выглядит следующим образом:

前后端分离图

"Портальный проект" является особым. Он представляет собой контейнер на стадии разработки и не содержит никакого бизнеса. В дополнение к предоставлению функций регистрации и слияния "подпроектов", он также может предоставлять некоторую общественную поддержку на системном уровне, Такие как:

  • Механизм входа пользователя
  • Получить права доступа к меню
  • глобальная обработка исключений
  • глобальное управление данными

«Подпроект» не требует входной HTML-страницы для внешнего вывода, требуются только выходные файлы ресурсов.Файлы ресурсов включают js, css, шрифты и imgs.

Система управления персоналом запускает интерфейсную службу (Node Server) в режиме онлайн, которая используется для ответа на вход пользователя в систему, аутентификацию и запросы ресурсов. Запрос данных системы управления персоналом не передается прозрачно через интерфейсную службу, а перенаправляется на внутренний сервер с помощью Nginx.Конкретное взаимодействие показано на следующем рисунке:

前后端分离图

Правило пересылки ограничивает формат запроса данных.系统名+Api做前缀Это гарантирует, что запросы между различными системами могут быть полностью изолированы. Среди них пример конфигурации Nginx выглядит следующим образом:


server {
    listen          80;
    server_name     xxx.xx.com;

    location  /project/api/ {
        set $upstream_name "server.project";
        proxy_pass  http://$upstream_name;
    }
    ...

    location  / {
        set $upstream_name "web.portal";
        proxy_pass  http://$upstream_name;
    }
}

Мы передаем единый вход и аутентификацию пользователей в SSO, а внутренние серверы всех проектов должны получать доступ к SSO для проверки статуса входа, чтобы обеспечить согласованность аутентификации безопасности пользователей между бизнес-системами.

Как происходит слияние приложений после того, как структура проекта определена? Поэтому мы начали разрабатывать механизм регистрации приложений.

Механизм регистрации приложений

«Проект портала» предоставляет интерфейс регистрации, а «подпроект» регистрируется и, наконец, объединяется в одностраничное приложение. Во всей совокупности механизмов ядром является механизм регистрации роутинга, маршрутизация «подпроекта» должна контролироваться им самим, а навигация всей системы обеспечивается «Портальным проектом».

регистрация маршрута

Управление маршрутизацией состоит из трех частей: дерева меню разрешений, дерева навигации и дерева маршрутизации.Компонент App инкапсулирован в «Проект портала», и вся страница создается в соответствии с деревом меню и деревом маршрутизации. Код для подключения маршрута к дереву DOM выглядит следующим образом:

let Router = <Router
            fetchMenu = {fetchMenuHandle}
            routes = {routes}
            app = {App}
            history = {history}
            >
ReactDOM.render(Router,document.querySelector("#app"));

Маршрутизатор — это уровень инкапсуляции, основанный на реакции-маршрутизаторе, и, наконец, генерирует дерево маршрутизации, как показано ниже, через меню и маршруты:

  <Router>
    <Route path="/" component={App}>
      <Route path="/namespace/xx" component={About} />
      <Route path="inbox" component={Inbox}>
        <Route path="messages/:id" component={Message} />
      </Route>
    </Route>
  </Router>

Конкретная регистрация использует глобальныйwindow.app.routes, "Портальный проект" отwindow.app.routesПолучите маршрут, «подпроект» добавляет маршрут, который необходимо зарегистрировать.window.app.routes, подпроекты регистрируются следующим образом:

let app = window.app = window.app || {}; 
app.routes = (app.routes || []).concat([
{
  code:'attendance-record',	
  path: '/attendance-record',
  component: wrapper(() => async(require('./nodes/attendance-record'), 'kaoqin')),
}]);

Когда маршруты объединяются, конкретные функции также упоминаются и связываются, и тогда всеми функциями и маршрутами можно управлять во время построения. Как контролировать объем проекта? Мы требуем, чтобы «подпроекты» были изолированы друг от друга, чтобы избежать загрязнения стиля и осуществлять независимое управление потоками данных. Для решения этих проблем мы используем метод содержания проекта.

Контроль содержания проекта

При управлении маршрутизацией мы упомянулиwindow.app, мы также используем это глобальное приложение для управления масштабами проекта.window.appСодержит следующие части:

let app = window.app || {};
app = {
    require:function(request){...},
    define:function(name,context,index){...},
    routes:[...],
    init:function(namespace,reducers){...}       
};

Основные функции window.app:

  • define определяет публичную библиотеку проекта, которая в основном используется для решения проблемы управления публичной библиотекой JS.
  • require ссылается на базовую библиотеку своего собственного определения и использует ее с define
  • маршруты используются для хранения глобальных маршрутов, а маршруты подпроектов добавляются в window.app.routes для завершения регистрации маршрутов.
  • инициализируйте регистрационную запись, добавьте идентификатор пространства имен в подпроект и зарегистрируйте редукторы, которые управляют потоком данных подпроекта.

Полная регистрация подпроекта выглядит следующим образом:

import reducers from './redux/kaoqin-reducer';
let app = window.app = window.app || {}; 
app.routes = (app.routes || []).concat([
{
  code:'attendance-record',	
  path: '/attendance-record',
  component: wrapper(() => async(require('./nodes/attendance-record'), 'kaoqin')),
  // ... 其他路由
}]);
 
function wrapper(loadComponent) {
  let React = null;
  let Component = null;
  let Wrapped = props => (
    <div className="namespace-kaoqin">
      <Component {...props} />
    </div>
  );
  return async () => {
    await window.app.init('namespace-kaoqin',reducers);
    React = require('react');
    Component = await loadComponent();
    return Wrapped;
  };
}

Что делает следующие вещи:

  1. Добавьте маршрут в window.app
  2. Выполняется при первом вызове бизнес-функцииwindow.app.init(namespace,reducers), зарегистрировать редьюсеры для масштаба проекта и потока данных
  3. Оберните корневой узел для узла монтирования бизнес-функции:ComponentПрикреплено кclassNameзаnamespace-kaoqinизdivпод

На этом регистрация «подпроекта» завершена, а внешним результатом «подпроекта» является входной файл и ряд файлов ресурсов, которые генерируются сборкой веб-пакета.

Что касается области действия CSS, используйте webpack, чтобы добавить собственную область видимости для всех CSS бизнеса на этапе сборки.Конфигурация сборки выглядит следующим образом:

//webpack打包部分,在postcss插件中 添加namespace的控制
config.postcss.push(postcss.plugin('namespace', () => css =>
  css.walkRules(rule => {
    if (rule.parent && rule.parent.type === 'atrule' && rule.parent.name !== 'media') return;
    rule.selectors = rule.selectors.map(s => `.namespace-kaoqin ${s === 'body' ? '' : s}`);
  })
));

Обработка CSS использует postcss-loader, а postcss-loader использует postcss.Мы добавляем плагины обработки postcss и добавляем имя для каждого селектора CSS..namespace-kaoqinКорневой селектор для финального упакованного CSS выглядит так:

.namespace-kaoqin .attendance-record {
    height: 100%;
    position: relative
}

.namespace-kaoqin .attendance-record .attendance-record-content {
    font-size: 14px;
    height: 100%;
    overflow: auto;
    padding: 0 20px
}
... 

После того, как проблема со стилем CSS решена, давайте посмотрим, что делает инициализация, предоставляемая Portal.

let inited = false;
let ModalContainer = null;
app.init = async function (namespace,reducers) {
  if (!inited) {
    inited = true;
    let block = await new Promise(resolve => {
      require.ensure([], function (require) {
        app.define('block', require.context('block', true, /^\.\/(?!dev)([^\/]|\/(?!demo))+\.jsx?$/));
        resolve(require('block'));
      }, 'common');
    });
    ModalContainer = document.createElement('div');
    document.body.appendChild(mtfv3ModalContainer);
    let { Modal} = block;
    Modal.getContainer = () => ModalContainer;
  }
  ModalContainer.setAttribute('class', `${namespace}`);
  mountReducers(namepace,reducers)
};

Метод init в основном делает две вещи:

  1. Смонтируйте редукторы «подпроекта» и смонтируйте поток данных «подпроекта» на редуксе.
  2. Все всплывающие окна «подпроекта» монтируются в глобальный div, и в этот div добавляется соответствующая область проекта, а CSS, созданный «подпроектом», используется для обеспечения правильного стиля всплывающее окно.

Также видно в приведенном выше кодеapp.defineОн в основном используется для управления общедоступными библиотеками JS, такими как блок библиотеки компонентов, который мы используем, ожидая, что версия каждого «подпроекта» унифицирована. Поэтому нам нужно решить проблему унифицированной версии общей библиотеки JS.

Единая версия публичной библиотеки JS

Чтобы не вторгаться в "подпроект", мы делаем это, подменяя его в процессе сборки. "Портальный проект" вводит общую библиотеку, переопределяет ее, а затем проходитwindow.app.requireСпособ ссылки при компиляции «подпроекта» на код, который ссылается на общую библиотеку изrequire('react')заменить все наwindow.app.require('react'), чтобы версию публичной библиотеки JS можно было передать "Проекту портала" на управление.

Код и пример определения выглядят следующим образом:

/**
* 重新定义包
* @param name  引用的包名,例如 react
* @param context 资源引用器 实际上是 webpackContext(是一个方法,来引用资源文件)
* @param index 定义的包的入口文件
*/
app.define = function (name, context, index) {
  let keys = context.keys();
  for (let key of keys) {
    let parts = (name + key.slice(1)).split('/');
    let dir = this.modules;
    for (let i = 0; i < parts.length - 1; i++) {
      let part = parts[i];
      if (!dir.hasOwnProperty(part)) {
        dir[part] = {};
      }
      dir = dir[part];
    }
    dir[parts[parts.length - 1]] = context.bind(context, key);
  }
  if (index != null) {
    this.modules[name]['index.js'] = this.modules[name][index];
  }
};
//定义app的react 
//定义一个react资源库:把原来react根目录和lib目录下的.js全部获取到,绑定到新定义的react中,并指定react.js作为入口文件
app.define('react', require.context('react', true, /^.\/(lib\/)?[^\/]+\.js$/), 'react.js');
app.define('react-dom', require.context('react-dom', true, /^.\/index\.js$/));

Сборка «подпроекта» использует внешние элементы веб-пакета (внешние расширения) для замены ссылок:

/**
 * 对一些公共包的引用做处理 通过webpack的externals(外部扩展)来解决
 */
const libs = ['react', 'react-dom', "block"];

module.exports = function (context, request, callback) {
    if (libs.indexOf(request.split('/', 1)[0]) !== -1) {
        //如果文件的require路径中包含libs中的 替换为 window.app.require('${request}'); 
        //var在这儿是声明的意思 
        callback(null, `var window.app.require('${request}')`);
    } else {
        callback();
    }
};

Таким образом, регистрация проекта завершена, и есть еще некоторые места, которые необходимо изменить самому «подпроекту», например, локальному запуску необходимо загрузить навигацию «Проекта портала», и фиктивные данные должны быть сделаны.

Регистрация проекта завершена, как нам его опубликовать и развернуть?

Интеграция после сборки и автономное развертывание

В процессе интеграции системы управления персоналом этап разработки является «нулевым вмешательством» в «подпроекты», и мы надеемся сделать то же самое на этапе выпуска.

Процесс развертывания у нас примерно такой:

部署过程图

Шаг 1. На машине выпуска получите код, установите зависимости и выполните сборку; Шаг 2: Загрузите результат сборки на сервер; Шаг 3: Выполнить на сервереnode index.jsЗапустите службу.

Файловая структура после создания «Проекта портала» выглядит следующим образом:

主项目构建结果图

Структура файла после сборки «подпроекта» выглядит следующим образом:

子项目构建结果图

Файловая структура онлайн-операции выглядит следующим образом:

运行文件结构图

Загрузите файл сборки «подпроекта» в соответствующий каталог файлов «подпроекта» на сервере, а затем интегрируйте и объедините файлы ресурсов «подпроекта» для создания файлов в каталоге .dist, которые предоставляются пользователям для онлайн доступ.

С каждым выпуском мы в основном делаем три вещи:

  1. Публикация последних файлов статических ресурсов
  2. Восстановить entry-xx.js и index.html (обновить ссылку на запись)
  3. Перезапустить интерфейсные службы

Если это чисто статическая служба, может быть достигнуто горячее развертывание, а эталонная связь может динамически обновляться без перезапуска службы. Поскольку мы выполняли некоторые общедоступные службы на уровне службы Node, мы решили перезапустить службу. Мы использовали базовую службу компании и PM2 для обеспечения теплого перезапуска.

Для исторических файлов нам нужно сделать контроль версий, чтобы обеспечить нормальную работу предыдущего доступа. Кроме того, чтобы обеспечить высокую доступность сервиса, мы запустили 4 машины и развернули их в двух машинных залах соответственно для повышения отказоустойчивости HR-системы.

Суммировать

Выше приведена бизнес-система управления персоналом, похожая на одностраничное приложение, которую мы построили с использованием стека технологий React и микро-интерфейса. Если рассматривать это техническое решение, весь фреймворк показан на следующем рисунке:

架构流程图

На уровне продукта "микроинтерфейсные одностраничные приложения" ломают концепцию независимых проектов. Мы можем свободно собирать наши страничные приложения в соответствии с потребностями пользователей. Например, мы можем поставить посещаемость, отпуск, одобрение ОД, финансовые высокочастотные функции, такие как возмещение расходов, объединяются. Вы даже можете позволить пользователям настраивать свои собственные функции, чтобы пользователи действительно чувствовали, что мы — система.

Решение "микроинтерфейс для создания одностраничного приложения" разработано на основе стека технологий React. Если механизм управления маршрутизацией и механизм регистрации извлечены в виде общедоступной библиотеки, их можно инкапсулировать в независимое от бизнеса общее решение. основанный на webpack., и он очень удобен в использовании.

На сегодняшний день система управления персоналом работает стабильно более года, мы выделили следующие три преимущества:

  1. Одностраничное приложение лучше, загрузка по запросу, плавное взаимодействие
  2. Проект является микро-интерфейсом, разделение бизнеса, стабильность гарантирована, а степень детализации проекта легко контролировать.
  3. Надежность проекта относительно хорошая, регистрация проекта только увеличивает размер входного файла, а более 30 проектов в настоящее время имеют размер всего 12 КБ.

об авторе

Цзя Чжао, который присоединился к Meituan в 2014 году, успешно руководил созданием внешнего интерфейса для проектов открытого доступа, управления персоналом, финансов и других корпоративных проектов, самостоятельно разработал блок библиотеки компонентов React, унифицированный технологический стек для всей корпоративной платформы на единой платформе. на основе Block и стремится повысить производительность команды исследований и разработок.

Категории