Представление технологии Element-UI (2) — Общий дизайн библиотеки компонентов

Vue.js внешний фреймворк

анализ спроса

Когда мы реализуем библиотеку компонентов, мы не просто кодируем ее, как только она появляется, но относимся к ней как к продукту и думаем о потребностях нашей библиотеки компонентов. тогда дляelement-ui, помимо разработки компонентов на основе стека технологий Vue.js, какие еще у него требования?

  1. Богатая функция: богатый набор компонентов, настраиваемые темы, международные.
  2. Документация и демонстрация: предоставьте удобную документацию и демонстрацию с низкими затратами на обслуживание и поддержкой нескольких языков.
  3. Установка и импорт: поддержка методов npm и cdn, а также поддержка импорта по запросу.
  4. Инжиниринг: разработка, тестирование, сборка, развертывание, непрерывная интеграция.

Раз есть спрос, то нужно думать, как его удовлетворить.Эта статья будет основана наelement-uiИсходный код версии 2.11.1 для анализа реализации этих требований. Конечно,element-uiЭтого не должно было быть в первые дни.Проанализированная нами версия была оптимизирована для многих итераций.Если вы хотите понять процесс ее разработки, вы можете найти ее историческую версию на GitHub.

богатые возможности

богатые компоненты

Ядром библиотеки компонентов является компонент, давайте сначала посмотримelement-uiПринципы проектирования компонентов: согласованность, обратная связь, эффективность, управляемость. Конкретное объяснение доступно на официальном сайте, поэтому я не буду публиковать больше.element-uiЗа командой разработчиков стоит сильная команда дизайнеров, которая также выигрывает отelement-uiОсновательsofishВ компании голос и статус, чтобы получить такой хороший ресурс. такelement-uiВнешний вид, соответствие цветов и взаимодействие компонентов — все сделано очень хорошо.

Другим важным аспектом базовой библиотеки компонентов является большое разнообразие компонентов.element-uiВ настоящее время официальное приложение имеет 55 компонентов, которые разделены на 6 категорий, а именно: базовые компоненты, компоненты форм, компоненты данных, компоненты подсказок, компоненты навигации и другие типы компонентов. Эти богатые базовые компоненты вполне могут удовлетворить большинство потребностей в развитии бизнеса от ПК до B.

Разработка такого количества компонентов требует много времени и усилий, так что спасибо здесьelement-uiКоманда предоставила нам эти базовые компоненты, а мы на их основе сделали вторичную разработку, что сэкономило много времени.

element-uiИсходный код компонента находится вpackagesхраниться в каталоге, а не вsrcв каталоге. Это не для использования монорепозитория, и я не нашел инструмент управления пакетами lerna.Я предполагаю, что цель этого состоит в том, чтобы позволить каждому компоненту быть упакованным отдельно для поддержки введения по требованию. Но на самом деле для достижения этой цели не обязательно так организовывать и поддерживать код, я предпочитаю размещать код компонента в библиотеке компонентов вsrc/componentsОн хранится в каталоге, а затем, изменив сценарий конфигурации веб-пакета, каждый компонент может быть упакован отдельно и может быть импортирован по запросу.Исходный код помещается вsrcКаталоги всегда разумнее.

пользовательская тема

element-uiОдной из основных функций является то, что он поддерживает пользовательские темы, вы можете использовать онлайн-редактор тем, вы можете изменять токены дизайна всех глобальных переменных и компонентов пользовательского элемента, и вы можете легко просматривать визуальные эффекты после изменения стиля в режиме реального времени. В то же время он также может генерировать полный пакет файлов стилей на основе нового пользовательского стиля для прямой загрузки, так как же он это делает?

element-uiСтили и публичные стили компонентов все естьpackages/theme-chalkфайл, и его можно распространять независимо.element-uiЦвета, шрифты, линии и т. д. в стилях компонентов вводятся переменными,packages/theme-chalk/src/common/var.scssМы можем видеть определения этих переменных в , что обеспечивает удобство для создания нескольких тем, потому что пока я изменяю эти переменные, тема компонента может быть изменена.

После понимания основных принципов заменить тему онлайн несложно.Я не буду подробно рассказывать о интерфейсной части интерактивной пользовательской онлайн-темы.Заинтересованные студенты могут сами посмотреть исходный код.examplesВ каталоге я говорю только об основных принципах.

Для онлайн-обработки скинов и предварительного просмотра в реальном времени вам потребуется помощь сервера. Например, тему можно поддерживать с помощью конфигурации. После того, как пользователь выполнит ряд операций, будет сгенерирована новая конфигурация темы, а конфигурация будет отправлено через интерфейсный сервер, а затем сервер вернется для создания нового CSS в соответствии с этой конфигурацией (конкретная схема реализации не является открытым исходным кодом, и грубо будет выполнять некоторую замену переменных, а затем компилировать), новый стиль CSS будет перезаписать стиль по умолчанию и достичь цели переключения темы.

Мы можем открыть сетевую панель на странице редактирования темы, и мы видим, что есть 2 запроса xhr, как показано на рисунке:

в,updateVaribleЭто запрос POST, он отправит вашу измененную конфигурацию темы на внутренний сервер, и вы сможете просмотреть его полезные данные запроса для отправленных данных.Этот запрос POST вернет фрагмент текста CSS, который затем будет динамически вставлен в тег головы. Внизу, чтобы переопределить стиль по умолчанию, вы можете увидеть, проверив элемент, что нижняя часть головы будет динамически вставлять идентификаторchalk-styleТег.

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

Соответствующий код находится вexamples/components/theme/loader/index.vueсередина.

    onAction() {
      this.triggertProgressBar(true);
      const time = +new Date();
      updateVars(this.userConfig)
        .then(res => {
          this.applyStyle(res, time);
        })
        .catch(err => {
          this.onError(err);
        })
        .then(() => {
          this.triggertProgressBar(false);
        });
    },
    applyStyle(res, time) {
      if (time < this.lastApply) return;
      this.updateDocs(() => {
        updateDomHeadStyle('chalk-style', res);
      });
      this.lastApply = time;
    }

onActionв функцииupdateVarsотправить запрос POST иapplyStyleФункция состоит в том, чтобы изменить и переопределить стиль по умолчанию,updateDocsФункция обновит цвет темы по умолчанию,updateDomHeadStyleСтиль добавит или изменит идентификатор наchalk-styleНазначение тега стиля — переопределить стиль по умолчанию и применить новый стиль темы.

updateVarsЗапрос выполняется при загрузке страницы, а также после изменения конфигурации темы.

посмотри сноваgetVaribleзапрос, это запрос GET, возвращаемый контент является источником данных панели конфигурации в правой части страницы конфигурации темы, как показано на следующем рисунке:

Панель конфигурации темы создается на основе этого источника данных, и когда вы переходите к редактированию одного из элементов, она снова срабатывает.updateVarsЗапрос на почту, отправьте обновленную конфигурацию, а затем бэкэн вернет новые CSS и вступит в силу на Frontend.

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

Однако реализация такого количества тем не идеальна, поэтому для ускорения компиляцииelement-uiСтилевая часть извлекается из отдельного файла, что доставляет большие неудобства учащимся, разрабатывающим компонент: при написании стиля компонента нужно переключаться между несколькими файлами, а это не соответствует ближайший компонент принципы управления. Но если стили написаны в компоненте, время компиляции и генерации отдельного файла стилей на стороне сервера увеличится (CSS нужно извлекать из компонента), так что это вопрос компромисса.

глобализация

Когда дело доходит до решения интернационализации Vue, его легко представить.vue-i18nстроить планы,element-uiне представленvue-i18n, но он хорошо работает сvue-i18nСовместимый.

Все схемы интернационализации используют языковые пакеты, которые обычно возвращают данные в формате JSON.element-uiЯзыковой пакет библиотеки компонентов находится вsrc/locale/langкаталоге, в качестве примера возьмем английский языковой пакет:

export default {
  el: {
    colorpicker: {
      confirm: 'OK',
      clear: 'Clear'
    }
    // ...
  }
}

существуетpackages/color-picker/src/components/picker-dropdown.vue, мы можем увидеть использование этого языкового пакета в разделе шаблонов:

<el-button
  size="mini"
  type="text"
  class="el-color-dropdown__link-btn"
  @click="$emit('clear')">
  {{ t('el.colorpicker.clear') }}
</el-button>
<el-button
  plain
  size="mini"
  class="el-color-dropdown__btn"
  @click="confirmValue">
  {{ t('el.colorpicker.confirm') }}
</el-button>

используется в шаблонеtфункция, которая определена вsrc/mixins/locale.jsсередина:

import { t } from 'element-ui/src/locale';

export default {
  methods: {
    t(...args) {
      return t.apply(this, args);
    }
  }
};

по фактуsrc/locale/index.jsопределено вtфункция:

export const t = function(path, options) {
  let value = i18nHandler.apply(this, arguments);
  if (value !== null && value !== undefined) return value;

  const array = path.split('.');
  let current = lang;

  for (let i = 0, j = array.length; i < j; i++) {
    const property = array[i];
    value = current[property];
    if (i === j - 1) return format(value, options);
    if (!value) return '';
    current = value;
  }
  return '';
};

Эта функция основана на входящихpathпуть, как в нашем примереel.colorpicker.confirm, найдите соответствующую копию из языкового пакета. вi18nHandlerЯвляетсяi18nФункция обработки, эта логика используется для совместимости с внешнимиi18nСхема такая какvue-i18n.

let i18nHandler = function() {
  const vuei18n = Object.getPrototypeOf(this || Vue).$t;
  if (typeof vuei18n === 'function' && !!Vue.locale) {
    if (!merged) {
      merged = true;
      Vue.locale(
        Vue.config.lang,
        deepmerge(lang, Vue.locale(Vue.config.lang) || {}, { clone: true })
      );
    }
    return vuei18n.apply(this, arguments);
  }
};

export const i18n = function(fn) {
  i18nHandler = fn || i18nHandler;
};

export const use = function(l) {
  lang = l || lang;
};

можно увидетьi18nHandlerПо умолчанию он попытается найти прототип Vue.$tфункция, котораяvue-i18@5.xРеализация , будет смонтирована на прототипе Vue$tметод.

Кроме того, он также раскрываетi18nметод, который может быть передан извне другимi18nметод, переопределитьi18nHandler.

Если нет снаружи снаружиi18nметод, а затем непосредственно найти текущий языковой пакетlet current = lang;, следующая логика состоит в том, чтобы прочитать соответствующее строковое значение из этого объекта языкового пакета, конечно, если строку необходимо отформатировать, вызовитеformatФункция, учащиеся, интересующиеся этой частью логики, могут прочитать ее самостоятельно.

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

import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale'

// 设置语言
locale.use(lang)

Таким образом, английский языковой пакет регистрируется, и вы можете нормально использовать его в шаблоне и найти соответствующий язык.

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

Документация и демонстрация

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

element-uiДокументация и демонстрация интегрированы.Когда мы открываем его документацию, мы видим, что документация не только знакомит с тем, как используется каждый компонент, но также показывает различные примеры компонента, и вы можете четко видеть детали каждого примера. код очень удобный. Такelement-uiКак написать эти демо и документы внутри? На самом деле документация и демонстрации для каждого компонента доступны через отдельный.mdфайл, так как же это сделать?

element-uiИсходный код демо находится вexamplesТехническое обслуживание каталогов, когда мы находимся вelement-uiработать в рамках проектаnpm run dev, он начнет режим разработки и отладки, а также запустит официальные документы и демоверсии.

посмотриnpm scripts:

"scripts": {
    "bootstrap": "yarn || npm i",
    "build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",
    "dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
}

Остальные скрипты опускаем, сосредоточимся наdevи связанные несколько команд, которыеbootstrapРоль заключается в установке зависимостей,build:fileэто бежатьbuildНесколько команд в каталоге, в том числеicon,entry,i18n,versionДождитесь инициализации. после выполненияbootstrapа такжеbuild:fileПосле этого черезwebpack-dev-serverбегатьbuild/webpack.demo.js, это ключевой момент, давайте посмотрим на конфигурационный файл этого вебпака.

const webpackConfig = {
  mode: process.env.NODE_ENV,
  entry: isProd ? {
    docs: './examples/entry.js',
    'element-ui': './src/index.js'
  } : (isPlay ? './examples/play.js' : './examples/entry.js'),
  output: {
    path: path.resolve(process.cwd(), './examples/element-ui/'),
    publicPath: process.env.CI_ENV || '',
    filename: '[name].[hash:7].js',
    chunkFilename: isProd ? '[name].[hash:7].js' : '[name].js'
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: config.alias,
    modules: ['node_modules']
  },
  devServer: {
    host: '0.0.0.0',
    port: 8085,
    publicPath: '/',
    hot: true
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          compilerOptions: {
            preserveWhitespace: false
          }
        }
      },
      {
        test: /\.md$/,
        use: [
          {
            loader: 'vue-loader',
            options: {
              compilerOptions: {
                preserveWhitespace: false
              }
            }
          },
          {
            loader: path.resolve(__dirname, './md-loader/index.js')
          }
        ]
      }
    ]
  }
};

Поскольку содержимое всего файла конфигурации относительно длинное, я оставлю только ключевые части и рассмотрю ключевые моменты.entryа такжеmoduleвнизrules.

element-uiОфициальный сайт по сути являетсяvueразработанное приложение, когда мы запускаемnpm run devКогда входной файлexamplesв каталогеentry.js:

import Vue from 'vue';
import entry from './app';
import VueRouter from 'vue-router';
import Element from 'main/index.js';
import hljs from 'highlight.js';
import routes from './route.config';
import demoBlock from './components/demo-block';
import MainFooter from './components/footer';
import MainHeader from './components/header';
import SideNav from './components/side-nav';
import FooterNav from './components/footer-nav';
import title from './i18n/title';

import 'packages/theme-chalk/src/index.scss';
import './demo-styles/index.scss';
import './assets/styles/common.css';
import './assets/styles/fonts/style.css';
import icon from './icon.json';

Vue.use(Element);
Vue.use(VueRouter);
Vue.component('demo-block', demoBlock);
Vue.component('main-footer', MainFooter);
Vue.component('main-header', MainHeader);
Vue.component('side-nav', SideNav);
Vue.component('footer-nav', FooterNav);

const globalEle = new Vue({
  data: { $isEle: false } // 是否 ele 用户
});

Vue.mixin({
  computed: {
    $isEle: {
      get: () => (globalEle.$data.$isEle),
      set: (data) => {globalEle.$data.$isEle = data;}
    }
  }
});

Vue.prototype.$icon = icon; // Icon 列表页用

const router = new VueRouter({
  mode: 'hash',
  base: __dirname,
  routes
});

router.afterEach(route => {
  // https://github.com/highlightjs/highlight.js/issues/909#issuecomment-131686186
  Vue.nextTick(() => {
    const blocks = document.querySelectorAll('pre code:not(.hljs)');
    Array.prototype.forEach.call(blocks, hljs.highlightBlock);
  });
  const data = title[route.meta.lang];
  for (let val in data) {
    if (new RegExp('^' + val, 'g').test(route.name)) {
      document.title = data[val];
      return;
    }
  }
  document.title = 'Element';
  ga('send', 'event', 'PageView', route.name);
});

new Vue({ // eslint-disable-line
  ...entry,
  router
}).$mount('#app');

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

Здесь мы хотим сосредоточиться на части маршрутизации, конфигурация маршрутизации находится вexamples/route.config.jsсередина:

import navConfig from './nav.config';
import langs from './i18n/route';

const LOAD_MAP = {
  'zh-CN': name => {
    return r => require.ensure([], () =>
      r(require(`./pages/zh-CN/${name}.vue`)),
    'zh-CN');
  },
  'en-US': name => {
    return r => require.ensure([], () =>
      r(require(`./pages/en-US/${name}.vue`)),
    'en-US');
  },
  'es': name => {
    return r => require.ensure([], () =>
      r(require(`./pages/es/${name}.vue`)),
    'es');
  },
  'fr-FR': name => {
    return r => require.ensure([], () =>
      r(require(`./pages/fr-FR/${name}.vue`)),
    'fr-FR');
  }
};

const load = function(lang, path) {
  return LOAD_MAP[lang](path);
};

const LOAD_DOCS_MAP = {
  'zh-CN': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/zh-CN${path}.md`)),
    'zh-CN');
  },
  'en-US': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/en-US${path}.md`)),
    'en-US');
  },
  'es': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/es${path}.md`)),
    'es');
  },
  'fr-FR': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/fr-FR${path}.md`)),
    'fr-FR');
  }
};

const loadDocs = function(lang, path) {
  return LOAD_DOCS_MAP[lang](path);
};

const registerRoute = (navConfig) => {
  let route = [];
  Object.keys(navConfig).forEach((lang, index) => {
    let navs = navConfig[lang];
    route.push({
      path: `/${ lang }/component`,
      redirect: `/${ lang }/component/installation`,
      component: load(lang, 'component'),
      children: []
    });
    navs.forEach(nav => {
      if (nav.href) return;
      if (nav.groups) {
        nav.groups.forEach(group => {
          group.list.forEach(nav => {
            addRoute(nav, lang, index);
          });
        });
      } else if (nav.children) {
        nav.children.forEach(nav => {
          addRoute(nav, lang, index);
        });
      } else {
        addRoute(nav, lang, index);
      }
    });
  });
  function addRoute(page, lang, index) {
    const component = page.path === '/changelog'
      ? load(lang, 'changelog')
      : loadDocs(lang, page.path);
    let child = {
      path: page.path.slice(1),
      meta: {
        title: page.title || page.name,
        description: page.description,
        lang
      },
      name: 'component-' + lang + (page.title || page.name),
      component: component.default || component
    };

    route[index].children.push(child);
  }

  return route;
};

let route = registerRoute(navConfig);

const generateMiscRoutes = function(lang) {
  let guideRoute = {
    path: `/${ lang }/guide`, // 指南
    redirect: `/${ lang }/guide/design`,
    component: load(lang, 'guide'),
    children: [{
      path: 'design', // 设计原则
      name: 'guide-design' + lang,
      meta: { lang },
      component: load(lang, 'design')
    }, {
      path: 'nav', // 导航
      name: 'guide-nav' + lang,
      meta: { lang },
      component: load(lang, 'nav')
    }]
  };

  let themeRoute = {
    path: `/${ lang }/theme`,
    component: load(lang, 'theme-nav'),
    children: [
      {
        path: '/', // 主题管理
        name: 'theme' + lang,
        meta: { lang },
        component: load(lang, 'theme')
      },
      {
        path: 'preview', // 主题预览编辑
        name: 'theme-preview-' + lang,
        meta: { lang },
        component: load(lang, 'theme-preview')
      }]
  };

  let resourceRoute = {
    path: `/${ lang }/resource`, // 资源
    meta: { lang },
    name: 'resource' + lang,
    component: load(lang, 'resource')
  };

  let indexRoute = {
    path: `/${ lang }`, // 首页
    meta: { lang },
    name: 'home' + lang,
    component: load(lang, 'index')
  };

  return [guideRoute, resourceRoute, themeRoute, indexRoute];
};

langs.forEach(lang => {
  route = route.concat(generateMiscRoutes(lang.lang));
});

route.push({
  path: '/play',
  name: 'play',
  component: require('./play/index.vue')
});

let userLanguage = localStorage.getItem('ELEMENT_LANGUAGE') || window.navigator.language || 'en-US';
let defaultPath = '/en-US';
if (userLanguage.indexOf('zh-') !== -1) {
  defaultPath = '/zh-CN';
} else if (userLanguage.indexOf('es') !== -1) {
  defaultPath = '/es';
} else if (userLanguage.indexOf('fr') !== -1) {
  defaultPath = '/fr-FR';
}

route = route.concat([{
  path: '/',
  redirect: defaultPath
}, {
  path: '*',
  redirect: defaultPath
}]);

export default route;

Этот файл конфигурации маршрутизации обеспечивает настройку нескольких страниц маршрутизации, таких как руководства, компоненты, темы, ресурсы и т. д., и поддерживает несколько языков. Мы сосредоточимся на том, как генерируется маршрутизация компонентов.registerRoute(navConfig)метод генерации.

вnavConfigчитатьexamples/nav.config.jsonфайл, этот файл конфигурации слишком длинный, и я не буду его публиковать.Он включает в себя конфигурацию нескольких языков и поддерживает отношение сопоставления путей меню навигации левого компонента.

registerRouteВнутри функция должна пройтиnavConfig, генерируйте конфигурацию маршрутизации на основе структуры данных его внутренних элементов, если данные имеютchildrenСоздается подмаршрут.

Мы знаем, что сущность Vue Router основана на разных путях URL,<router-view>Компоненты сопоставляются с соответствующими компонентами маршрутизации.Для маршрутизации каждого компонента он проходит черезaddRoute(nav, lang, index)генерируется методом, который снова вызывается внутри методаloadDocs(lang, page.path)Получите соответствующий компонент маршрутизации.

const loadDocs = function(lang, path) {
  return LOAD_DOCS_MAP[lang](path);
};

const LOAD_DOCS_MAP = {
  'zh-CN': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/zh-CN${path}.md`)),
    'zh-CN');
  },
  'en-US': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/en-US${path}.md`)),
    'en-US');
  },
  'es': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/es${path}.md`)),
    'es');
  },
  'fr-FR': path => {
    return r => require.ensure([], () =>
      r(require(`./docs/fr-FR${path}.md`)),
    'fr-FR');
  }
};

Взяв в качестве примера китайский язык, мы получаем определенноеpathКомпонент маршрутизации ниже является фабричной функцией, и соответствующий путь загруженного компонентаexmaples/docs/zh-CN/${path}.md. Здесь следует отметить, что, в отличие от нашего обычного асинхронного метода загрузки компонентов, здесь загружается фактически.mdфайл, а не.vueфайл, но может.vueФайл также можно преобразовать в компонент Vue, как это сделать?

Мы знаем, что философия webpack заключается в том, что все ресурсы могут бытьrequire, если соответствующий загрузчик настроен. назадbuild/webpack.demo.js, находим, что для.mdфайле настраиваем соответствующий загрузчик:

  {
    test: /\.md$/,
    use: [
      {
        loader: 'vue-loader',
        options: {
          compilerOptions: {
            preserveWhitespace: false
          }
        }
      },
      {
        loader: path.resolve(__dirname, './md-loader/index.js')
      }
    ]
  }

для.mdфайл, здесь настраиваются 2 элемента в массиве использования, и порядок их выполнения обратный, то есть они выполняются первымиmd-loader, а затем выполнитьvue-loader,md-loaderКод находится вbuild/md-loader/index.jsсередина:

const {
  stripScript,
  stripTemplate,
  genInlineComponentText
} = require('./util');
const md = require('./config');

module.exports = function(source) {
  const content = md.render(source);

  const startTag = '<!--element-demo:';
  const startTagLen = startTag.length;
  const endTag = ':element-demo-->';
  const endTagLen = endTag.length;

  let componenetsString = '';
  let id = 0; // demo 的 id
  let output = []; // 输出的内容
  let start = 0; // 字符串开始位置

  let commentStart = content.indexOf(startTag);
  let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
  while (commentStart !== -1 && commentEnd !== -1) {
    output.push(content.slice(start, commentStart));

    const commentContent = content.slice(commentStart + startTagLen, commentEnd);
    const html = stripTemplate(commentContent);
    const script = stripScript(commentContent);
    let demoComponentContent = genInlineComponentText(html, script);
    const demoComponentName = `element-demo${id}`;
    output.push(`<template slot="source"><${demoComponentName} /></template>`);
    componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},`;

    // 重新计算下一次的位置
    id++;
    start = commentEnd + endTagLen;
    commentStart = content.indexOf(startTag, start);
    commentEnd = content.indexOf(endTag, commentStart + startTagLen);
  }

  // 仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
  // todo: 优化这段逻辑
  let pageScript = '';
  if (componenetsString) {
    pageScript = `<script>
      export default {
        name: 'component-doc',
        components: {
          ${componenetsString}
        }
      }
    </script>`;
  } else if (content.indexOf('<script>') === 0) { // 硬编码,有待改善
    start = content.indexOf('</script>') + '</script>'.length;
    pageScript = content.slice(0, start);
  }

  output.push(content.slice(start));
  return `
    <template>
      <section class="content element-doc">
        ${output.join('')}
      </section>
    </template>
    ${pageScript}
  `;
};

Принцип работы загрузчика webpack очень прост: на вход поступает исходное содержимое файла, а возвращаемое содержимое — это содержимое, обработанное загрузчиком. дляmd-loader, ввод.mdдокумент, вывод представляет собой строку в формате Vue SFC, поэтому его вывод можно использовать в качестве следующегоvue-loaderВвод обрабатывается.

Давайте кратко рассмотримmd-loaderПромежуточная обработка. первый казненныйmd.render(source)правильноmdПарсинг документов, извлечение документов из:::demo {content} :::Контент, сгенерируйте несколько строк шаблона Vue, затем выполните цикл из этой строки шаблона.<!--element-demo:а также:element-demo-->Содержимое пакета, из которого извлекается строка шаблона дляoutput, извлеките скрипт вcomponenetsString, а затем построитьpageScript, окончательный возвращаемый контент:

  return `
    <template>
      <section class="content element-doc">
        ${output.join('')}
      </section>
    </template>
    ${pageScript}
  `;

Окончательная сгенерированная строка удовлетворяет тому, что мы обычно пишем.vueФормат SFC, он будет использоваться в качестве следующегоvue-loaderввода, так что таким образом мы эквивалентны загрузке.mdКомпонент Vue загружается в виде файла в формате.

Есть еще много и.mdдетали парсинга файла, если выoutputа такжеpageScriptКакой код интересует, предлагаю отладить самостоятельно.

element-uiэтот документ иdemoМетод реализации очень оригинален, что значительно снижает стоимость обслуживания демо и документации, а также очень удобен для пользователя.Если вы также создаете документацию для своей собственной библиотеки, вы можете сослаться на ее реализацию.

установить и импортировать

Обычно библиотеки JS поддерживают два метода установки npm и CDN,element-uiне исключено.

Давайте сначала поговорим о методе установки CDN, на самом делеelement-uiВсе компоненты будут упакованы для создания CSS и JS, официальный также предоставляет примеры:

<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

У метода установки CDN есть свои плюсы, он не требует инструментов для сборки и может использоваться из коробки, но и недостатки очевидны, все компоненты вводятся в полном объеме, а объем очень большой.

Поскольку большинство людей разрабатывают проекты Vue на основеvue-cliСкаффолдинг инициализирует проект, поэтому для установки рекомендуется использовать метод npm.

npm i element-ui -S

Говоря об установке npm, я должен упомянутьelement-uiПредусмотрено 2 типа методов импорта компонентов: полный импорт и частичный импорт.

Очень легко поддерживать полный импорт, упаковывать все компоненты в CSS и JS иpackage.jsonСредняя конфигурация:

"main": "lib/element-ui.common.js"

Это выполняетсяimport ElementUI from 'element-ui'Когда можно полностью внедрить JS-код компонента. Как мы уже говорили,element-uiCSS публикуется отдельно, поэтому вам также понадобитсяimport 'element-ui/lib/theme-chalk/index.css'.

Преимуществом полного внедрения является удобство, его можно использовать полностью всего с 2 строками кода.element-uiВсе компоненты, но недостатки тоже очевидны.Пакет импортируемых компонентов очень большой.Обычно проект не использует все компоненты,и будет трата ресурсов.

Поэтому лучше всего импортировать по требованию:

import Vue from 'vue'
import { Button } from 'element-ui'

Vue.component(Button.name, Button)

Большинство людей воспринимают это как должное, когда они используют его таким образом.Интересно, задумывались ли вы когда-нибудь об этом: почему этот способ введения может быть введен по требованию? Чтобы понять эту проблему, мы должны понятьimport { Button } from 'element-ui'Что происходит за этим.

На самом деле на официальном сайте уже есть ответ, при использовании импорта по требованию нужно использоватьbabel-plugin-componentплагин webpack и настройте.babelrc:

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

На самом деле этоimport { Button } from 'element-ui'преобразуется в:

var button = require('element-ui/lib/button')
require('element-ui/lib/theme-chalk/button.css')

Таким образом, мы точно вводим соответствующую библиотеку подButtonКод JS и CSS компонента уже импортирован, поэтому его можно импортировать по запросу.Buttonкомпоненты.

element-uiХотя этот метод внедрения по требованию удобен, за ним стоит несколько проблем, которые необходимо решить: поскольку мы поддерживаем возможность импорта каждого компонента отдельно, как решить проблему избыточности кода, если зависимости компонентов генерируются и импортируются по требованию в то же время. Например, вelement-uiсередина,Tableкомпонент зависит отCheckBoxкомпонент, то когда я также представлюTableкомпоненты иCheckBoxКогда компонент будет временем, будет ли он генерировать избыточность кода?

import { Table, CheckBox } from 'element-ui'

Если вы ничего не делаете, ответ — да, пакет, который вы в конечном итоге импортируете, будет иметь 2 копии.CheckBoxкод. Такelement-uiКак решить эту проблему? На самом деле решается только частично, настраивается в конфигурационном файле вебпакаexternals,существуетbuild/config.jsМы можем увидеть эти конкретные конфигурации в:

var externals = {};

Object.keys(Components).forEach(function(key) {
  externals[`element-ui/packages/${key}`] = `element-ui/lib/${key}`;
});

externals['element-ui/src/locale'] = 'element-ui/lib/locale';
utilsList.forEach(function(file) {
  file = path.basename(file, '.js');
  externals[`element-ui/src/utils/${file}`] = `element-ui/lib/utils/${file}`;
});
mixinsList.forEach(function(file) {
  file = path.basename(file, '.js');
  externals[`element-ui/src/mixins/${file}`] = `element-ui/lib/mixins/${file}`;
});
transitionList.forEach(function(file) {
  file = path.basename(file, '.js');
  externals[`element-ui/src/transitions/${file}`] = `element-ui/lib/transitions/${file}`;
});

externals = [Object.assign({
  vue: 'vue'
}, externals), nodeExternals()];

externalsЭто может предотвратить упаковку этих импортированных пакетов в пакеты, а затем получение этих зависимостей расширения извне во время выполнения.

Давайте посмотрим на упаковкуlib/table.jsПозже мы можем увидеть компиляциюtable.jsправильноCheckBoxВведение зависимости компонента:

module.exports = require("element-ui/lib/checkbox");

Таким образом, он не будет упакован для создания 2 копий.CheckBoxJS часть кода, но для части CSS,element-uiИзбыточность не обрабатывается, как видитеlib/theme-chalk/checkbox.cssа такжеlib/theme-chalk/table.cssБудутCheckBoxСтили CSS для компонента.

На самом деле решить проблему избыточности JS и CSS, вводимых по требованию, несложно.Можно использовать идею пост-компиляции, то есть полагаться на пакеты для предоставления исходного кода, и компилировать в приложение для обработки , так что не только не будет избыточного кода компонентов, но даже не будет избыточного кода для компиляции, ведь мы основываемся наelement-uiбиблиотека компонентов форкаzoom-uiПрименяется технология посткомпиляции и библиотека компонентов с открытым исходным кодом, ранее разработанная Didi.cube-uiБиблиотеки компонентов делают то же самое. Для получения дополнительных сведений, связанных с компиляцией, см.эта статья.

Инжиниринг

Передняя часть предъявляет все более высокие требования к инженерии,element-uiКак библиотека компонентов, что она делает с точки зрения разработки?

Первый — этап разработки.Чтобы обеспечить единообразие стиля кода у всех, используется ESLint, и даже специально написанныйeslint-config-elemefeВ качестве расширения правило конфигурации ESint; для облегчения локальной разработки и отладки используется webpack и настраивается Hot Reload; используя идею модульной разработки, некоторые общие модули, от которых зависят компоненты, помещаются вsrcкаталог и разделение в соответствии с функциейdirectives,locale,mixins,transitions,utilsи другие модули.

Во-вторых, это аспект тестирования с использованием среды тестирования кармы, написания модульных тестов для каждого компонента и интеграции тестов с Travis CI.

Затем идет строительный аспект,element-uiНаписал много скриптов npm дляdistПример этого скрипта:

 "dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme"

Он последовательно выполнит несколько команд и в конечном итоге сгенерируетlibКаталоги и упакованные файлы. Я не собираюсь знакомить со всеми командами, желающие могут заниматься самостоятельно, здесь я хотел бы представитьbuild:fileЧто делает этот скрипт:

"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",

Здесь будет выполняться последовательноbuild/binНекоторые скрипты Node в каталоге, даicon,entry,i18n,versionПосле выполнения ряда работ по инициализации их содержание состоит в том, чтобы выполнить файловый ввод-вывод в соответствии с некоторыми правилами.Преимущество этого заключается в том, что файл создается автоматически с помощью инструментов, что надежнее и эффективнее, чем ручная работа.Эта волна работы очень стоит.Мы учимся и применяем.

Наконец, разверните черезpubЭтот скрипт npm завершает:

 "pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js && sh build/deploy-faas.sh"

В основном за счет запуска серии сценариев bash он реализует отправку кода, слияние, управление версиями, выпуск npm, выпуск официального веб-сайта и т. Д., Так что весь процесс выпуска автоматизирован.Студенты, которые заинтересованы в конкретном содержании сценария, могут просмотреть его самостоятельно.

Суммировать

Слишком далеко,element-uiПосле введения общего дизайна библиотеки компонентов Много технических деталей, чтобы копаться.

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

Следующее превью: Element-UI Technology Reveal (3) — Дизайн и реализация компонентов макета макета.

Кроме того, я только недавно открыл публичный аккаунт "Фронтенд-частная кухня Лао Хуанга". Серия статей "Раскрытая технология Element-UI" будет обновлена ​​и опубликована в публичном аккаунте как можно скорее. Кроме того, я буду часто делитесь передовыми передовыми знаниями, галантерейными товарами и иногда делитесь некоторыми навыками мягкого качества, приглашаем всех обратить внимание ~