The introduction

Due to business needs, recently the team will build a set of UI component library of its own, and the framework is still Vue. There are already some mature UI libraries in the industry, such as ElementUI, AntDesign, Vant, etc.

With the framework Vue, we chose to build on ElementUI. But building a wheel is not easy. First, you need to understand the entire construction process, catalog design, etc.

This article through the analysis of ElementUI complete build process, finally give build a complete component library need to do some work, hope for want to understand ElementUI source code or also have build UI component library requirements, you can provide some help!

Let’s first look at the directory structure of the source code for ElementUI.

Directory structure resolution

  • Github: Hosts Element UI contribution guides, issues, and PR templates

  • Build: Stores configuration files related to packaging

  • Examples: Examples of components
  • Packages: Component source code
  • SRC: Stores entry files and some utility functions
  • Test: Unit test related files, which are also a must for a good open source project
  • Types: type declaration file

Having said that, there are a few files left (common.babelrc,.eslintcThis will not be explained here), which is unusual in business code:

  • .travis. Yml: Continuous Integration (CI) configuration file
  • CHANGELOG: Update log, hereElement UIIn four different languages, which is also very thoughtful
  • Components. json: specifies the file path of the component for conveniencewebpackObtain the file path of the component when packaging.
  • FAQ. Md:ElementUIDeveloper answers to frequently asked questions.
  • LICENSE: Open source LICENSE.Element UIUsing theMITagreement
  • The Makefile:MakefileIs one that applies toC/C++The tools in possessionmakeEnvironment directory, if there is oneMakefileFile. Then the inputmakeThe command will be executedMakefileA target command in the file.

Before delving into the build process, let’s take a look at some of the major file directories in the ElementUI source code, which will be helpful to explore the complete ElementUI process later.

package.json

Json file. This file contains key information about the project’s version, entry point, scripts, dependencies, and so on.

Here I take out a few key fields, one by one to analyze and explain his meaning.

main

The entry file for the project

When you import an Element from ‘element-ui’, you import the file in main

Lib/ element-ui.com/mon.js is the CommonJS specification, while lib/index.js is the UMD specification, which I will describe in detail in a later packaging module.

files

Specify the file/directory that NPM Publish needs to include when sending packets.

typings

TypeScript entry file.

home

The online address of the project

unpkg

When you publish a package on NPM, it should also be available on UNpkg. That is, your code can be executed in both NodeJs and browser environments. To do this you need to pack in umD format. Lib /index.js is the UMD specification, generated by webpack.conf.js.

style

Declare the style entry file, in this case lib/ theme-Chalk /index.css, as described later.

scripts

Develop, test, produce build, package, deploy, test case and related scripts.scriptsapackage.jsonThe most important part of the program, and I’ll go over the key instructions one by one.

bootstrap

"bootstrap": "yarn || npm i"
Copy the code

To install dependencies on yarn, the official recommendation is to use yarn first (🤔). Bootstrap is the same UI library used before.

build:file

This directive is mainly used to automate the generation of some files.

"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js"
Copy the code

This command is longer, so let’s break it down:

build/bin/iconInit.js

Parse icon. SCSS and put all the icon names in icon.json and then hang the $icon on the Vue prototype.

Finally, through traversalicon.jsonTo get this effect on the official website:

build/bin/build-entry.js

The SRC /index.js file is generated from the component. json file. The core is the use of the json-templater/string plug-in.

Let’s look at the SRC /index.js file, which corresponds to the entry file for the project, with this sentence at the top:

/* Automatically generated by './build/bin/build-entry.js' */
Copy the code

That is, the SRC /index.js file is automatically built by the build/bin/build-entry.js script. Let’s look at the source code:

// Generate SRC /index.js from components. Json

// Import dependencies for all components
var Components = require('.. /.. /components.json');
var fs = require('fs');
/ / https://www.npmjs.com/package/json-templater can make a few content string combined with variable output var render = require('json-templater/string'); / / https://github.com/SamVerschueren/uppercamelcase into hump foo - bar > > FooBar var uppercamelcase = require('uppercamelcase'); var path = require('path'); // The os.eol property is a constant that returns a newline for the current operating system (\r\n for Windows, \n for other systems). var endOfLine = require('os').EOL;  // The name and path of the generate file var OUTPUT_PATH = path.join(__dirname, '.. /.. /src/index.js'); var IMPORT_TEMPLATE = 'import {{name}} from \'.. /packages/{{package}}/index.js\'; '; var INSTALL_COMPONENT_TEMPLATE = ' {{name}}'; // var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */  // ...   // Get the names of all components and store them in an array var ComponentNames = Object.keys(Components);  var includeComponentTemplate = []; var installTemplate = []; var listTemplate = [];  ComponentNames.forEach(name= > {  var componentName = uppercamelcase(name);   includeComponentTemplate.push(render(IMPORT_TEMPLATE, {  name: componentName,  package: name  }));   if (['Loading'.'MessageBox'.'Notification'.'Message'.'InfiniteScroll'].indexOf(componentName) === - 1) {  installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {  name: componentName,  component: name  }));  }   if(componentName ! = ='Loading') listTemplate.push(` ${componentName}`); });  var template = render(MAIN_TEMPLATE, {  include: includeComponentTemplate.join(endOfLine),  install: installTemplate.join(', ' + endOfLine),  version: process.env.VERSION || require('.. /.. /package.json').version,  list: listTemplate.join(', ' + endOfLine) });  // Output the result to SRC /index.js fs.writeFileSync(OUTPUT_PATH, template); console.log('[build entry] DONE:', OUTPUT_PATH); Copy the code

Generate SRC /index.js from components. Json.

build/bin/i18n.js

Examples /i18n/page. Json and templates are used to generate demos in different languages.

ElementUIThe template for internationalizing the website isexamples/pages/template, and generate different files for different languages:It’s all in here.tplFiles, each file corresponds to a template, and eachtplThe documents are all consistentSFCSpecification of theVueFile.

Let’s open a random file:

export default {
    data() {
      return {
        lang: this.$route.meta.lang,
        navsData: [
 {  path: '/design'. name: '< % = 1 >'  },  {  path: '/nav'. name: '< % = 2 >'  }  ]  };  } }; Copy the code

They all have numbers that indicate where you need to be internationalized.

All internationalization-related fields on the home page are stored inexamples/i18n/page.jsonIn:

Finally, the official website shows the page after the above international processing:Supports switching between different languages.

What does build/bin/i18n.js do for us?

Let’s consider a question: how can the home page presentation generate different VUE files according to different languages?

This is what build/bin/i18n.js does for us.

Take a look at the corresponding source:

'use strict';

var fs = require('fs');
var path = require('path');
var langConfig = require('.. /.. /examples/i18n/page.json');
 langConfig.forEach(lang= > {  try {  fs.statSync(path.resolve(__dirname, `.. /.. /examples/pages/${ lang.lang }`));  } catch (e) {  fs.mkdirSync(path.resolve(__dirname, `.. /.. /examples/pages/${ lang.lang }`));  }   Object.keys(lang.pages).forEach(page= > {  var templatePath = path.resolve(__dirname, `.. /.. /examples/pages/template/${ page }.tpl`);  var outputPath = path.resolve(__dirname, `.. /.. /examples/pages/${ lang.lang }/${ page }.vue`);  var content = fs.readFileSync(templatePath, 'utf8');  var pairs = lang.pages[page];   Object.keys(pairs).forEach(key= > {  content = content.replace(new RegExp(`<%=\\s*${ key }\\s*>`.'g'), pairs[key]);  });   fs.writeFileSync(outputPath, content);  }); }); Copy the code

The process is simple: we walk through examples/i18n/page.json, match the TPL file flags according to different data structures, and replace them with our own predefined fields.

So the internationalization of the homepage of the official website is completed.

build/bin/version.js

Json from version in package.json, generate examples/ version. json, which correspond to a complete list of versions

build:theme

Handle style dependencies.

"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib  lib/theme-chalk",Copy the code

Again, this one has multiple operations associated with it, so let’s break them down.

build/bin/gen-cssfile

This step is to generate package/ theme-Chalk /index. SCSS file according to components.json, and import the styles of all components into index. SCSS.

You do an automatic import, so every time you add a component, you don’t have to manually import the style of the new component.

gulp build –gulpfile packages/theme-chalk/gulpfile.js

We all know that ElementUI can be introduced in two ways when used:

  • Introduced the global
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);  new Vue({  el: '#app'. render: h= > h(App) }); Copy the code
  • According to the need to introduce
import Vue from 'vue';
import { Pagination, Dropdown } from 'element-ui';

import App from './App.vue';

Vue.use(Pagination) Vue.use(Dropdown)  new Vue({  el: '#app'. render: h= > h(App) }); Copy the code

There are two ways to introduce an Element and two ways to package it.

For example, compile all SCSS files in packages/ theme-Chalk to CSS. When you need to import global files, import index. SCSS files. When you need to import, import the corresponding component SCSS file.

How to compile all SCSS files under Packages/theme-Chalk to CSS?

In normal development, we tend to pack, compress and so on the work of WebPack to deal with, but, for the above problem, we use gulp based on workflow to solve the problem is more convenient.

Gulp-related processing is available in packages/theme-chalk/gulpfile.js:

'use strict';

const { series, src, dest } = require('gulp');
const sass = require('gulp-sass');  // Compile the gulp tool
const autoprefixer = require('gulp-autoprefixer');  // Add the vendor prefix
const cssmin = require('gulp-cssmin'); / / compress CSS  function compile() {  return src('./src/*.scss') // all SCSS files under SRC  .pipe(sass.sync()) // Compile the SCSS file into CSS  .pipe(autoprefixer({ // Add the vendor prefix based on the target browser version  browsers: ['ie > 9'.'last 2 versions']. cascade: false  }))  .pipe(cssmin()) / / compress CSS  .pipe(dest('./lib')); // Output to lib }  function copyfont() {  return src('./src/fonts/**') // Read all files under SRC /fonts  .pipe(cssmin())  .pipe(dest('./lib/fonts')); // Output under lib/fonts }  exports.build = series(compile, copyfont); Copy the code

After processing, the corresponding style file is finally packaged

cp-cli packages/theme-chalk/lib lib/theme-chalk

Cp-cli is a cross-platform copy tool similar to CopyWebpackPlugin

Copy the file to lib/ theme-Chalk.

We’ve mentioned components. Json several times, so let’s take a look at it.

components.json

This file actually records the path of the component and is used when automating the file and entry:

{
  "pagination": "./packages/pagination/index.js".  "dialog": "./packages/dialog/index.js".  "autocomplete": "./packages/autocomplete/index.js".  // ...
 "avatar": "./packages/avatar/index.js". "drawer": "./packages/drawer/index.js". "popconfirm": "./packages/popconfirm/index.js" } Copy the code

packages

Contains the source code of the component library and the component style file.

The following uses the Alert component as an example:

Alert folder

Here,main.vueThe corresponding component source code, andindex.jsIs the entry file:

import Alert from './src/main';

/* istanbul ignore next */
Alert.install = function(Vue) {
  Vue.component(Alert.name, Alert);
};  export default Alert;  Copy the code

Introduce the component, then provide the install method for the component to make Vue available via vue.use (Alert).

See the official documentation for install

packages/theme-chalk

This contains all the styles associated with each component, as explained above, including index. SCSS (used to export all component styles for global import) and SCSS files for each other (used to export component styles for on-demand import).

src

After a long time, I finally got around to the SRC folder.

The abovepackagesThe folder is to handle each component separately, whilesrcThe function is to do a unified processing of all components, including custom instructions, overall project entry, component internationalization, component mixins, animation encapsulation, and public methods.Let’s focus on the entry file, which issrc/index.js:

/* Automatically generated by './build/bin/build-entry.js' */
// Import all components under Packages
import Pagination from '.. /packages/pagination/index.js';
import Dialog from '.. /packages/dialog/index.js';
import Autocomplete from '.. /packages/autocomplete/index.js';
// ...  const components = [  Pagination,  Dialog,  Autocomplete,  // ... ];  // The install method is provided to help us mount some components and variables const install = function(Vue, opts = {}) {  locale.use(opts.locale);  locale.i18n(opts.i18n);  // Register all components with Vue  components.forEach(component= > {  Vue.component(component.name, component);  });   Vue.use(InfiniteScroll);  Vue.use(Loading.directive);   Vue.prototype.$ELEMENT = {  size: opts.size || ' '. zIndex: opts.zIndex || 2000  };   Vue.prototype.$loading = Loading.service;  Vue.prototype.$msgbox = MessageBox;  Vue.prototype.$alert = MessageBox.alert;  Vue.prototype.$confirm = MessageBox.confirm;  Vue.prototype.$prompt = MessageBox.prompt;  Vue.prototype.$notify = Notification;  Vue.prototype.$message = Message;  };  /* istanbul ignore if */ if (typeof window! = ='undefined' && window.Vue) {  install(window.Vue); } // Export the version number, install method (plug-in), and features such as internationalization export default {  version: '2.13.2'. locale: locale.use,  i18n: locale.i18n,  install,  Pagination,  Dialog,  Autocomplete,  // ... };  Copy the code

At the beginning of the file:

/* Automatically generated by './build/bin/build-entry.js' */
Copy the code

SRC /index.js is automatically generated by the build-entry script.

This file does the following:

  • To import thepackagesAll components under
  • Exposed to the publicinstallMethod to register all components withVueAbove, and atVueSome global variables and methods are mounted on the prototype
  • Will eventuallyinstallMethod, variable, method export

examples

To store theElementUIExamples of components for.In fact, from the directory structure, we can see that this is a complete and independentVueThe project. Mainly used for the display of official documents:So let’s focus on thatdocsFolders:ElementThe website supports 4 languages,docsThere are four folders, and the contents of each folder are basically the same.

We can see that there are all MD documents, and each MD document corresponds to the display page of the official website component.

In fact, now all the major component library documents are written using MD.

We have seen a few major file directories of the source code above, but they are scattered. Let’s take a look at the complete build process from build directives to new components, packaging processes, and publishing components.

Build process review

Build directives (Makefiles)

It is common practice to put the scripts used by your project in package.json scripts. But ElementUI also uses makefiles (there are many files, so I’ve chosen a few here) :

.PHONY: dist test
default: help

build-theme: npm run build:theme

install: npm install

install-cn: npm install --registry=http://registry.npm.taobao.org

dev: npm run dev

play: npm run dev:play

new: node build/bin/new.js @,$(MAKECMDGOALS))

dist: install npm run dist

deploy: @npm run deploy

pub: npm run pub

test: npm run test:watch

Copy the code

// Tip: // make new <component-name> [英 文] // 1, add new component to component.json // 2, add to index.scss // 3, add to element-ui.d.ts // 4, create package // 5. Add to nav.config.json

This is the first time I’ve seen it, so I went to Google, and the online definition for a Makefile looks something like this:

A Makefile is a C/C++ utility that appeared early on UNIX as an engineering tool that uses the make command to perform a series of build and join operations. If a Makefile exists in the directory where the make environment is located. Then typing make will execute one of the target commands in the Makefile.

The following uses make install as an example to describe the execution process:

  • performmakeCommand to be found in the directoryMakefileFile.
  • findMakefileCorresponding to command line parameters in the fileinstallThe target. The goal here isnpm install

Build entry file

Let’s look at the dev directive in scripts:

"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",
Copy the code

First, NPM Run bootstrap is used to install dependencies.

NPM Run build:file is also mentioned earlier and is used to automate the generation of files. Nodebuild /bin/build-entry.js is used to generate Element’s entry js: First, read the root directory components. Json, this json file maintains the component path mapping relationship, the key is the component name, the value is the component source entry file; Then iterate over the key value, import all components, expose the install method, and register all import components as global components through Vue.component(name, Component). And mount some of the popover class components onto the Vue prototype chain (as explained in the section on scripts above).

Webpack-dev-server is run after the SRC /index.js of the entry file is generated.

webpack-dev-server --config build/webpack.demo.js
Copy the code

This, as mentioned earlier, is used to run the basic configuration on the Element website.

The new component

As mentioned above, Element uses makefiles to write additional scripts for us.

Make new

[英 文] make new

When you run this command, you are actually running node build/bin/new.js.

Build /bin/new.js is simple, and the comments are clear. It helps us do the following things:

1. Add the newly created component to components.json

2. Create a component SCSS file under Packages/theme-Chalk/SRC and add it to packages/ theme-Chalk/SRC /index.scss

3, add to element-ui.d.ts, the corresponding type declaration file

4, create a package (we mentioned above that the source code for the component is stored in the package directory)

5. Add to nav.config.json (the menu on the left side of the official website component)

Analysis of packaging process

The script that ElementUI packages for execution is:

"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",
Copy the code

Here is the analysis:

NPM run clean

"clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",
Copy the code

Package build file before delete.

NPM run build:file

Generate the entry file SRC /index.js from component.json, and the i18n associated file. This has already been analyzed above, so I won’t expand on it here.

NPM Run Lint (code checking)

"lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",
Copy the code

Eslint detects projects, which is now a must for projects.

File packaging

webpack --config build/webpack.conf.js &&
webpack --config build/webpack.common.js &&
webpack --config build/webpack.component.js
Copy the code
build/webpack.conf.js

Generate umD js file (index.js)

build/webpack.common.js

Generate a JS file in commonJs format (element-ui.mon.js), which is loaded by default when require.

build/webpack.component.js

Using components. Json as the entry point, package each component into a file that can be loaded on demand.

NPM Run build:utils (translation tool method)

"build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",
Copy the code

Babel all files except the index.js entry file in the SRC directory and move them to the lib folder.

NPM Run build: UMD

"build:umd": "node build/bin/build-locale.js",
Copy the code

The language pack that generates the UMD module.

NPM run build:theme

"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib  lib/theme-chalk".Copy the code

Package/theme-Chalk /index. SCSS is generated from components. Json. Use the gulp build tool to compile SCSS, compress, and output CSS to the lib directory.

Finally, a diagram depicts the entire packaging process described above:

Release process

The packaging is complete, and the code is released. Publishing in Element is primarily done using shell scripts.

There are three parts to the Element release:

1. Git release

2. NPM release

3. Official website release

The corresponding script for publishing is:

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

Sh build/git-release.sh

Run git-release.sh to check for git conflicts. This is mainly to check for conflicts in the dev branch because Element is developed in the dev branch.

#! /usr/bin/env sh
# Switch to dev branch
git checkout dev
# Check whether there are any uncommitted files in the local and staging areas
if test -n "$(git status --porcelain)"; then
 echo 'Unclean working tree. Commit or stash changes first.'> & 2; exit 128; fi Check whether the local branch is in error if ! git fetch --quiet 2>/dev/null; then  echo 'There was a problem fetching your branch. Run `git fetch` to see more... '> & 2; exit 128; fi Check whether the local branch is behind the remote branch if test "0"! ="$(git rev-list --count --left-only @'{u}'... HEAD)"; then  echo 'Remote history differ. Please pull changes.'> & 2; exit 128; fi # If the above checks are passed, the code does not conflict echo 'No conflicts.'> & 2;Copy the code

Updated NPM && website

The dev branch code detects that there is no conflict, and then the release. Sh script is executed, merging the dev branch to master, updating the version number, pushing the code to the remote repository, and publishing to NPM (NPM Publish).

The update to the official website is basically: generate static resources into examples/ element-UI and put them in gh-pages branch so that they can be accessed via Github pages.

At this point the complete build process for ElementUI is analyzed.

UI component library setup refers to the north

From an analysis of the ElementUI source code files and the build process, we can summarize what it takes to build a complete UI component library.

The directory structure

Directory structure is especially important for large projects, and a clear structure makes sense for later development and extension. UI component library directory structure, I feel that ElementUI is good:

Element | -- - | -. Babelrc / / Babel related configuration. | - eslintignore. | - eslintrc / / eslint related configuration. | - gitattributes. | - gitignore | -. Travis. Yml/configuration/ci | -- CHANGELOG. En - US. Md | -- CHANGELOG. Es. The md | -- CHANGELOG. Fr - fr. Md | -- CHANGELOG. Useful - CN. / / md Version changes that | -- FAQ. Md / / QA | FAQ - LICENSE / / copyright agreement related | - Makefile/collection/script compilation (engineering) | -- README. | - md / / project description document Components. The json / / component configuration file | -- element_logo. SVG | -- package. Json | -- yarn. Lock. | - making / / contributor, issue, the PR template | | - CONTRIBUTING.en-US.md | |-- CONTRIBUTING.es.md | |-- CONTRIBUTING.fr-FR.md | |-- CONTRIBUTING.zh-CN.md | |-- ISSUE_TEMPLATE. Md | | - PULL_REQUEST_TEMPLATE. Md | | - stale. Yml | - build / / package | - examples | - packages / / / / sample code Component source | - SRC / / entry files and various auxiliary | - | test / / unit test file types / / type declarationsCopy the code

Component development

Like most UI component libraries, you can organize sample code under Examples and expose an entry, configure a dev-server using WebPack, and debug and run the components in this dev-server.

Unit testing

As UI components are highly abstract basic common components, it is necessary to write unit tests. Qualified unit testing is also a prerequisite for a mature open source project.

packaging

For the packaged files, keep them in the lib directory, and remember to include the lib directory in.gitignore to avoid committing the package results to the code base.

Both global import (UMD) and on-demand load packages are provided for different import modes.

The document

The documentation of the component library is generally externally accessible and therefore needs to be deployed to the server, as well as local preview.

release

When a version of the component library is developed, the package needs to be published to NPM. Release process:

  • Execute the test case
  • Pack to build
  • Updated version number
  • NPM package release
  • Play tag
  • Automatic deployment

maintenance

After the release, you need to maintain the old version. In general, you need to pay attention to the following points:

  • Issue (bug fixes)
  • Pull Request (Code PR)
  • Changelog.md (Version Change Record)
  • CONTRIBUTING (Project Contributors and specifications)

reference

  • https://segmentfault.com/a/1190000016419049
  • https://zhuanlan.zhihu.com/p/94920464

This article was typeset using MDNICE