More articles

preface

Recently, I helped the company build a UI component library. The main considerations are as follows:

  • Precipitation of business components to avoid the loss of high quality components
  • Document, reduce the focus on the internal code of the component, and use the component through documentation demo and exposed property methods
  • Install components through NPM for better management of projects & components
  • Improve front-end component packaging capability

technology

At present, the vast majority of the company’s projects are vue2.0 + element2.x, which have been very mature. Most likely, vue3.0 will not be upgraded. The technologies and tools are as follows:

  • vue2.x
  • markdown-it
  • Markdown-it-container: Processes markDown blocks
  • Markdown – it – front – matter: processingfront matter
  • Highlight.js: Code highlighting
  • Standard-version: indicates the version and log
  • Rimraf: deletes a file
  • Gulp: Packages SCSS
  • Cp -cli: copies files

directory

UI presentation platform content is under SRC and component-related content is under Packages, separating the UI document platform from UI components

QJD - UI ├ ─ ─ build │ ├ ─ ─ md_loader markdown file parsing | | - lib. Config. Js component packaging configuration | | -- utils. Js tool ├ ─ ─ packages based & business component │ ├ ─ ─ │ ├─ SRC │ ├─ Components UI Component │ ├─ Demo - Block-vue md Demo template │ ├─ consts │ ├─ Exercises The slider. Js navigation routing information │ ├ ─ ─ docs components document │ │ ├ ─ ─ button. The md │ ├ ─ ─ pages UI platform page │ ├ ─ ─ the test test code │ ├ ─ ─ the router routing │ └ ─ ─ styles │ ├─ Bass Exercises ─ Utils UI │ ├─ Bass Exercises ─ GetTeStroutes.jsCopy the code

Markdown related

The document is written using Markdown, and the corresponding. Md file can be matched according to the routing information, as follows:

{
  path: `/main/${e.key}`.name: e.key,
  // Log in the root directory
  component: resolve= > e.key == 'CHANGELOG' ? require([`.. /.. /CHANGELOG.md`], resolve) : require([`@/docs/${e.key}.md`], resolve)
}
Copy the code

markdown-it

We need to use markdown-it and other tools to help vUE identify.md files:

// index.js
const markdown = require('markdown-it');
const hljs = require('highlight.js');
const md = markdown({
  html: true.typographer: true.// Handle code highlighting
  highlight: function (str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return `<pre class="hljs" v-pre>
          <code>
            ${hljs.highlight(lang, str, true).value}
          </code>
        </pre>`
      } catch (error) {
        console.log('error:' + error)
      }
    }
    return `<pre class="hljs">
      <code>${md.utils.escapeHtml(str)}</code>
    </pre>`}})const html = md.render(src)

return (
  `<template>\n
      <div class="markdown">
        ${html}
      </div>\n
  </template>\n
  `
)
Copy the code

markdown-it-container

Parse the demo using markdown-it-Container and insert it through the written slot (SRC /components/demo-block)

// containerjs
const container = require('markdown-it-container')

module.exports = md= >{ md.use(... createDemo('demo'))}function createDemo(kclass) {
  return [container, kclass, {
    validate: params= > params.trim().match(/^demo\s*(.*)$/),
    render: function (tokens, idx) {
      const token = tokens[idx]
      const info = token.info.replace('demo'.' ').trim()
      let desc = info ? `<div class="demo_desc" slot="desc" >${info}</div>` : null
      if (token.nesting === 1) {
        return `<demo-block>
          ${desc}
          <div slot="code">
        `
      }
      return '</div></demo-block>\n'}}}]// Then introduce it in index.js
const markdown = require('markdown-it');
const containers = require('./container');
constmd = markdown({... }).use(containers)Copy the code

And then you can write your demo like this in your.md file

< div class = "demo_block" > < / div > : I'm code: : demo ` ` ` HTML <! -- I'm code --> ' '::Copy the code

markdown-it-front-matter

Handle front matter with markdown-it-front-matter

use(require('markdown-it-front-matter'), function (fm) {
  const data = fm.split('\n')
  data.forEach((item, index) = > data[index] = item.split(':'))
  data.map(item= > result += `<span>${kinds[item[0]]}: ${item[1]}</span>`)});return (
    `<template>\n
        <div class="markdown">
          <div class="demo-info">
            ${result}
          </div>
          ${html}
        </div>\n
    </template>\n
    `
  )
Copy the code

You can then write front matter like this in the.md file

---
author: xxx
create: xxxx-xx-xx
update: xxxx-xx-xx
---
Copy the code

Pull away script&style

Without CSS and JS, a MarkDown file is only allowed to have one script and one style

/ / pull away
const scriptRe = /^
      (?>;
const styleRe = /^
      (?>;

md.renderer.rules.html_block = (tokens, idx) = > {
    const content = tokens[idx].content
    if (scriptRe.test(content.trim())) {
        scriptContent = content;
        return ' '
    } if(styleRe.test(content.trim())) {
        styleContent = content;
        return ' '
    } else {
        return content
    }
}
return (
  `<template>\n
      <div class="markdown">
        <div class="demo-info">
          ${result}
        </div>
        ${html}
      </div>\n
  </template>\n
  ${scriptContent}
  ${styleContent}
  `
)
Copy the code

Configure the loader

Finally, configure the loader

{
  test: /\.md$/,
  use: [
    { loader: 'vue-loader' },
    {
      loader: require.resolve('./md_loader')}}]Copy the code

After the above configuration is complete, you can write the documentation in the.md file

Complete md_loader

// index.js
const markdown = require('markdown-it');
const hljs = require('highlight.js');
const kinds = require('./consts');

const containers = require('./container');
const scriptRe = /^
      (?>;
const styleRe = /^
      (?>;

module.exports = function (src) {
  let result = ' ', scriptContent = ' ', styleContent = ' ';

  const md = markdown({
    html: true.typographer: true.highlight: function (str, lang) {
      if (lang && hljs.getLanguage(lang)) {
        try {
          return `<pre class="hljs" v-pre>
            <code>
              ${hljs.highlight(lang, str, true).value}
            </code>
          </pre>`
        } catch (error) {
          console.log('error:' + error)
        }
      }
      return `<pre class="hljs">
        <code>${md.utils.escapeHtml(str)}</code>
      </pre>`
    }
  })
    .use(containers)
    .use(require('markdown-it-front-matter'), function (fm) {
      const data = fm.split('\n')
      data.forEach(item= > {
        item = item.split(':')
        result += `<span>${kinds[item[0]]}: ${item[1]? item[1] : The '-'}</span>`})}); md.renderer.rules.html_block =(tokens, idx) = > {
    const content = tokens[idx].content
    if (scriptRe.test(content.trim())) {
      scriptContent = content;
      return ' '
    } if (styleRe.test(content.trim())) {
      styleContent = content;
      return ' '
    } else {
      return content
    }
  }

  const html = md.render(src)

  return (
    `<template>\n
        <div class="markdown">
          <div class="demo-info">
            ${result}
          </div>
          ${html}
        </div>\n
    </template>\n
    ${scriptContent}
    ${styleContent}
    `)}// container.js
const container = require('markdown-it-container')

module.exports = md= >{ md.use(... createDemo('demo'))}function createDemo(kclass) {
  return [container, kclass, {
    validate: params= > params.trim().match(/^demo\s*(.*)$/),
    render: function (tokens, idx) {
      const token = tokens[idx]
      const info = token.info.replace('demo'.' ').trim()
      let desc = info ? `<div class="demo_desc" slot="desc" >${info}</div>` : null
      if (token.nesting === 1) {
        return `<demo-block>
          ${desc}
          <div slot="code">
        `
      }
      return '</div></demo-block>\n'}}}]// consts.js
module.exports = {
  author: 'the writer'.create: 'Creation time'.update: 'Update time'
}

Copy the code

UI components

Components are written uniformly under Packages, styles are written under theme-default/ SRC, and styles are packaged through gulp

We will dynamically match the.vue component under packages when packaging, so each component must create the corresponding file and create the index.js component and expose the component, otherwise it will lead to the failure of importing on demand, even if the component like ButtonGroup, Although the implementation is under the button folder, we still need to create the button-group folder

entry

// Dynamically match components
exports.getEntries = () = > {
  const componentsContext = requireContext('.. /packages'.true./\.vue$/, __dirname).keys();
  const defaultCom = { "qjd-ui": "./packages/index.js" }; // Batch packing
  let coms = {}; // Store each component for individual packaging
  componentsContext.forEach(item= > {
    const keys = item ? item.split('\ \') : [];
    const key = keys[keys.length - 1]? keys[keys.length -1].split('. ') [0] : ' ';
    if (key) {
      coms[key] = `./packages/${key}`; }});return{... defaultCom, ... coms } }// Configure entry in WebPack
{
  entry: utils.getEntries(),
}
Copy the code

output

Our components will eventually be published to NPM, so unlike normal packaging, our entry will need to configure the library

{
  output: {
		path: path.resolve(__dirname, '.. /lib'),
		publicPath: '/'.filename: '[name].js'.// [Entry name].js
		library: '[name]'.// The name exposed
		libraryTarget: 'umd'.// Usually choose UMD, which supports various environments
		umdNamedDefine: true}}Copy the code

externals

Because it is a component that is twice encapsulated based on the base component of Element-UI, we usually do not package third-party tools when packaging. If we use the element-UI in the project, we need to install the element-UI and introduce the corresponding components in the project

externals: {
  vue: {
    root: 'Vue'.commonjs: 'vue'.commonjs2: 'vue'.amd: 'vue'
  },
  'element-ui': 'ELEMENT'
}
Copy the code

Bulk packaging

This is our entry file for bulk packaging components

import Button from './button'

const components = {
    Button,
}

const install = function (Vue) {
    if (install.installed) return
    Object.keys(components).forEach(key= > {
        Vue.component(components[key].name, components[key])
    })
}

if (typeof window! = ='undefined' && window.Vue) {
    install(window.Vue)
}

constAPI = { install, ... components }module.exports = API
Copy the code

A separate package

Here is the entry of each component when we separately package. To facilitate the introduction of components on demand, inject the install method for each group price separately:

// install.js
export default el => el.install = Vue= > Vue.component(el.name, el);
// Take button as an example
import install from '.. /utils/install'
install(Button)
Copy the code

Complete configuration

// lib.config.js
const path = require('path');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const utils = require('./utils')

module.exports = {
	entry: utils.getEntries(),
	output: {
		path: path.resolve(__dirname, '.. /lib'),
		publicPath: '/'.filename: '[name].js'.library: '[name]'.libraryTarget: 'umd'.umdNamedDefine: true
	},
	externals: {
		vue: {
			root: 'Vue'.commonjs: 'vue'.commonjs2: 'vue'.amd: 'vue'
		},
		'element-ui': 'ELEMENT'
	},
	resolve: {
		extensions: ['.js'.'.vue']},module: {
		loaders: [{
			test: /\.vue$/,
			loader: 'vue-loader'.options: {
				loaders: {
					css: 'vue-style-loader! css-loader'.sass: 'vue-style-loader! css-loader! sass-loader'
				},
				postLoaders: {
					html: 'babel-loader'}}}, {test: /\.js$/,
			loader: 'babel-loader'.exclude: /node_modules/
		}, {
			test: /\.css$/,
			use: [
				'style-loader'.'css-loader'] {},test: /\.(gif|jpg|png|woff|svg|eot|ttf)\?? . * $/,
			loader: 'url-loader? limit=8192'}},plugins: [
		new webpack.optimize.ModuleConcatenationPlugin(),
		new webpack.optimize.UglifyJsPlugin({
			uglifyOptions: {
				ie8: false.output: {
					comments: false.beautify: false,},mangle: {
					keep_fnames: true
				},
				compress: {
					warnings: false.drop_console: true}}}),new CopyWebpackPlugin([
			{
				from: `./packages`.to: `./packages`.ignore: [
						'theme-default/**']}]),]}// util.js
exports.getEntries = () = > {
  const componentsContext = requireContext('.. /packages'.true./\.vue$/, __dirname).keys();
  const defaultCom = { "qjdui": "./packages/index.js" }; // Batch packing
  let coms = {}; // Store each component for individual packaging
  componentsContext.forEach(item= > {
    const keys = item ? item.split('\ \') : [];
    const key = keys[keys.length - 1]? keys[keys.length -1].split('. ') [0] : ' ';
    if (key) {
      coms[key] = `./packages/${key}`; }});return{... defaultCom, ... coms } }Copy the code

Packaging orders

{
  "clean": "rimraf lib"."build": "node build/build.js"."build:theme": "gulp build --gulpfile packages/theme-default/gulpfile.js && cp-cli packages/theme-default/lib lib/theme-default"."build:packages": "webpack --config build/lib.config.js"."build:ui": "npm run clean && npm run build:theme && npm run build:packages"
}
Copy the code

UI display platform packaging results in dist folder, command:

npm run build
Copy the code

The package result of the component library is stored in the lib folder.

npm run build:ui
Copy the code

Commit specification

A good commit specification can improve team development efficiency. Using Angular’s Commit specification as a template, Commitizen can help constrain its own commit specification, and log generation will depend on the commit content

Type type

type describe
feat New features
fix Fix the bug
refactor Code refactoring
docs The document
test The test code
pref To optimize the
chore Changes to the build process or ancillary tools

The module

Modules involved in each commit

describe

Description of each change

The sample

A full commit looks like this:

Git commit -m 'feat(button): add disabled effect to a button 'git commit -m 'docs(button): Document the component of a button'Copy the code

Version & Log

Use standard-version to control the version and generate the log. If the commit type is feat and fix, the log will appear, corresponding to the Features and Bug Fixes modules, respectively

  • Major: Indicates the major version
  • Minor: Indicates a minor version
  • Patch: Revised version
  • NPM run release — 1.0.0: This command is executed. The custom version is 1.0.0
  • NPM run release:100: Run this command to upgrade to 2.0.0 if the current version is 1.0.0
  • NPM run release:010: Run this command to upgrade to 1.1.0 if the current version is 1.0.0
  • NPM run release:001: If the current version is 1.0.0, the version will be upgraded to 1.0.1

After the preceding command is executed, the system performs three actions: version generation, tag generation, and log generation. The logs are stored in changelog. md

The installation

npm install qjd-ui –save

use

Introduced the CDN

<! -- css --> <link rel="stylesheet" href="https://unpkg.com/qjd-ui/lib/theme-default/index.css">/ <! -- HTML --> <div id="app"> <xw-button> Default button </xw-button> <xw-button type="primary"> Main button </xw-button> <xw-button </xw-button> <xw-button type="info"> Information button </xw-button> <xw-button type="warning"> Warning button </xw-button> < xw - button type = "error" > dangerous button < / xw - button > < / div > <! -- js --> <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/qjd-ui/lib/qjdui.js"></script> <script> new Vue({ el: '#app' }) </script>Copy the code

Batch is introduced into

import qjdui from 'qjd-ui'

import 'qjd-ui/lib/theme-default/index.css'

Vue.use(qjdui)
Copy the code

According to the need to introduce

Method 1 (Manually import components & CSS)

import Button from 'qjd-ui/lib/button'

import 'qjd-ui/lib/theme-default/button.css'

Vue.component(Button.name, Button)
Copy the code

Way 2

/ / install Babel - plugin - import
yarn add babel-plugin-import -D
/ / configuration Babel. Config. Js
{
  "plugins": [["import",
      {
        libraryName: 'qjd-ui'.customStyleName: (name) = > {
          return `qjd-ui/lib/theme-default/${name}.css`; },},]]}Copy the code

The import mode is as follows after the configuration is complete:

import { Button, Input } from 'qjd-ui';

Vue.component(Button.name, Button)

Vue.component(Input.name, Input)
Copy the code

or

Vue.use(Button)

Vue.use(Input)
Copy the code

conclusion

At present, only the first version is completed, referring to the emelent- UI structure and CSS processing