從零開始,手摸手搭建前端組件庫

  • 2019 年 11 月 11 日
  • 筆記

MI-vant組件庫

打造一個內部的組件庫,在我們進行程式碼的重構,以及開發新的功能的時候,抽離公共的組件,減少程式碼的復用,注重業務與組件的分離,簡化耦合度,便於開發與維護。

預覽地址

https://majunchang.github.io/mi.vant/#/quickStart

https://majunchang.github.io/mi.vant.storybook/?path=/story/mivantbutton–with-text

特性

  • 支援按需引入
  • 預覽模式
    • storybook預覽模式
    • 純markdown預覽模式
  • rem適配
  • 支援主題訂製
  • 較為完善的使用文檔和示例

技術選型

最初的時候 考慮過使用vue-cli3.0 vue-loader15+webpack4的配置 後來考慮到穩定性 暫時放棄

  • 使用babel7的插件和配置
  • 使用less作為項目中css的預處理語言
  • 增加rollup的打包方式
  • 引入storybook 來支援項目的預覽功能
  • 引入vue-loader15
  • 引入vue-markdown-loader等相關插件 支援文檔功能

babel7

為什麼要升級到babel7

全局配置 babel.config.js 里的配置默認對整個項目生效,包括node_modules。除非通過 exclude 配置進行剔除。換句話來說babel7擁有全局配置能力。是前端走向未來語法的一大步,改造為babel7 的時候,遇到了很多難以解決的問題。但是最終還是堅持下來了。

升級注意事項
  1. 從 babel7 開始,所有的官方插件和主要模組,都放在了 @babel 的命名空間下,從而可以避免在 npm 倉庫中 babel 相關名稱被搶注的問題
  2. Babel7 是對整個項目都生效的配置。
  3. 移除了之前的stage-x插件,廢棄babel-preset-es201x插件,
  4. 官方升級工具:babel-upgrade 之前配置的時候,不知道有這個工具,導致走了很多彎路。大家以後在做某個東西的時候,一定要先查查有沒有工具。避免重複造輪子的同時,也可以避免很多不必要的錯誤)。
  5. 優化程式碼與使用jsV8修補程式做效能調校,編譯速度更快。
  6. webpack中babel-loader的版本不低於@babel/core的版本,否則編譯會報錯

以vue-cli 2.9.6版 的版本舉?,默認是.babelrc。

.babelrc中的配置和相關的依賴

{    "presets": [      ["env", {        "modules": false,        "targets": {          "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]        }      }],      "stage-2",    ],    "plugins": [      "transform-vue-jsx",      "transform-runtime"    ],  }
  • package.json中的配置
    "babel-helper-vue-jsx-merge-props": "^2.0.3",      "babel-loader": "^7.1.1",      "babel-plugin-syntax-jsx": "^6.18.0",      "babel-plugin-transform-object-rest-spread": "^6.26.0",      "babel-plugin-transform-runtime": "^6.22.0",      "babel-plugin-transform-vue-jsx": "^3.5.0",      "babel-preset-env": "^1.3.2",      "babel-preset-es2015": "^6.24.1",      "babel-preset-stage-2": "^6.22.0",      "@babel/core": "^7.5.5",

mivant中最終版的babel.config.js中的配置和相關依賴

module.exports = function (api) {    api.cache(true);      const presets = [      "@babel/preset-env",      "@vue/babel-preset-app",      [        '@vue/babel-preset-jsx',        {          functional: false        }      ]];    const plugins = [      "@babel/plugin-transform-runtime",      '@babel/plugin-transform-object-assign',      ['import', {        libraryName: 'vant',        libraryDirectory: 'es',        style: true      }, 'vant']];      return {      presets,      plugins    };  }
參考資料

Babel7 發布

babelrc和babel.config.js 的區別

升級至babel7

babel7的簡單升級指南

一文讀懂 babel7 的配置文件載入邏輯

Babel快速上手使用指南

babel官網

組件全部載入與按需載入

組件是如何被載入?

解讀vue.use源碼

   Object.keys(components).forEach((key)=>{       Vue.component(components[key].name,components[key])     })

附index.js中的程式碼

  • 引入相關的組件
  • 提供 公共的install方法
  • 通過export default 實現全部載入,通過export 的方式實現按需載入
import MiButton from './Button/index'  import Modal from './Modal/index'      const components = [    MiButton,    Modal  ]    const version = '1.0.0'    const install = function (Vue) {    if (install.installed) return    components.forEach(item => {      Vue.component(item.name, item)    })  }      if (typeof window !== 'undefined' && window.Vue) {    console.log('運行環境為window');    install(window.Vue)  }    export {    MiButton,    Modal,    install  }    export default {    install,    version  }
按需載入的第一種方式
// 組件中內置了單個組件所需的樣式  無需配置babel-plugin-import   import { MiButton, Modal } from 'miVant'   import Vue from 'vue'     Vue.use(MiButton)   Vue.use(Modal)

babel-pluhin-import

按需載入的第二種方式
import MiButton from 'miVant/lib/Button'  import Modal from 'miVant/lib/Modal'  import Vue from 'vue'     Vue.use(MiButton)   Vue.use(Modal)

按需架載入的基礎

  • 組件中的index.js中引入相關的vue文件,提供install方法
  • XX.vue文件中 引入less文件,內置less
  • 打包的時候對於compont下的文件使用CopyWebpackPlugin複製到lib目錄下,也就是第二種按需載入的方式

less的使用

  • utils中配置less-loader 注意loader的解析規則

附錄一段less使用的示例

@hd: 1px; // 基本單位    // 支付寶錢包默認主題  // https://github.com/ant-design/ant-design-mobile/wiki/設計變數表及命名規範    // 色彩  // ---  // 文字色  @color-text-base: #000;                  // 基本  @color-text-base-inverse: #fff;          // 基本 - 反色  @color-text-secondary: #a4a9b0;          // 輔助色  @color-text-placeholder: #bbb;           // 文本框提示  @color-text-disabled: #bbb;              // 失效  @color-text-caption: #888;               // 輔助描述  @color-text-paragraph: #333;             // 段落  @color-link: @brand-primary;             // 鏈接      @defaultColor: #455a64;  @hoverColor:#1989fa;  @height: 60px;    .navTitle{    font-size:16px;    font-weight:600;    cursor: default;  }  .navItem {    color: @defaultColor;    font: 14px/24px PingFang SC;    padding: 10px 10px 10px 50px;    text-align: left;    cursor: pointer;  }      .doc-nav-title,  .doc-comp-title{    .navItem();    .navTitle();  }    .doc-nav-item{    .navItem()  }    .doc-comp-item{    .navItem()  }

引入storyBook 預覽功能

首先,storyBook是啥?
  1. Storybook是一個輔助UI控制項開發的工具。通過story創建獨立的控制項,讓每個控制項開發都有一個獨立的開發調試環境,可以單獨的查看每個組件的不同狀態,以及互動式開發和測試組件。
  2. Storybook的運行不依賴於項目,開發人員不用擔心由於開發環境、依賴問題導致不能開發控制項。
  3. Storybook支援很多主流的框架(React、Vue、Angular)。
  4. 2019年1月份,storybook 發布5.0版本,是自項目開始以來的第一次重大調整。改進了整個生態系統的視圖層,插件和集成
安裝使用
  1. 安裝參考指南storybook for vue
  2. 自定義的webpack配置,解決擴展名問題和less編譯問題 // 自定義webpack配置 const path = require('path'); module.exports = async ({ config, env }) => { // Extend it as you need. function resolve(dir) { return path.join(__dirname, '..', dir); } config.resolve = { extensions: ['.js', '.vue', '.json', '.jsx'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src') }, } config.module.rules.push({ test: /.stories.jsx?$/, loaders: [require.resolve('@storybook/addon-storysource/loader')], enforce: 'pre', }); config.module.rules.push( { test: /.(css|less)$/, use: [{ loader: 'style-loader', // creates style nodes from JS strings }, { loader: 'css-loader',// translates CSS into CommonJS }, { loader: 'postcss-loader' }, { loader: 'less-loader', options: { javascriptEnabled: true } // compiles Less to CSS }], exclude: /node_modules/ }) return config; };
  3. storybook 5.0 使用vue-loader15,默認使用babelrc進行解析和編譯,需要自定義babelrc { "presets": [ "@babel/preset-env", "@vue/babel-preset-app", [ "@vue/babel-preset-jsx", { "functional": false } ] ] }
如何為組件配置Storybook環境
  • stories目錄下 新建 xx.js文件,此處映射為預覽環境中的 左側預覽目錄
  • xx.js文件中 引入vue組件,編寫測試案例。通過addDecorator函數引入 插件的相關功能 import { storiesOf } from '@storybook/vue'; import { withKnobs } from '@storybook/addon-knobs'; import miVantButton from '../src/components/Button/Button.vue' import { withStorySource } from '@storybook/addon-storysource' import buttonText from '../docs/button.md' const simpleSourceCode = '<mi-vant-button>storyBook</mi-vant-button>' storiesOf('miVantButton', module) .addDecorator(withKnobs) .addDecorator(withStorySource(simpleSourceCode)) .addParameters({ readme: { sidebar: buttonText, }, }) .add('with text', () => { return { components: { miVantButton }, template: `<mi-vant-button>storyBook</mi-vant-button>`, } }, { notes: { markdown: buttonText } } )
  • 根目錄下的.storybook文件夾中
    • addons.js 中 註冊相關的插件
    • config.js中 配置允許環境,安裝全局插件。類似於vue項目的main.js import { configure, addDecorator, addParameters } from '@storybook/vue'; import { withNotes } from '@storybook/addon-notes' import { addReadme } from 'storybook-readme/vue'; import { setOptions } from '@storybook/addon-options' import { Button } from 'vant' import Vue from 'vue' Vue.use(Button) const req = require.context('../stories', true, /.js$/) function loadStories() { req.keys().forEach((filename) => req(filename)) } setOptions({ name: 'mi-Vant', url: '#', goFullScreen: false, showStoriesPanel: true, showAddonPanel: true, showSearchBox: true, addonPanelInRight: true, sortStoriesByKind: false, hierarchySeparator: null, hierarchyRootSeparator: null, sidebarAnimations: true, selectedAddonPanel: undefined, }) addParameters({ viewport: { defaultViewport: 'galaxys5' }, }) addDecorator(addReadme); addDecorator(withNotes) // require configure(loadStories, module);

遇到的問題

  • vue-loader的版本使用問題
    • 新版默認支援vue-loader15 而項目中vue-loader是13.3.0。 當時以為vue-loader15 是要搭配webpack4 一起使用的 所以降低了一下storybook的版本
    • 低版本的storybook 默認使用babel6 只能解析.babelrc 且需要自定義webpack的配置 所以只能使用storybook中提供的自定義babel和webpack配置
    • 基礎設置都配置好了,在引入插件的時候 發現插件不能用………. 不知名的報錯 讓人很蛋疼………..,會提示一個語法錯誤。而實際上我們配置的babel中已經解析了 但是 它還是會報錯。。。。 猜測與插件版本有關 ReferenceError: regeneratorRuntime is not defined
![image.png](https://upload-images.jianshu.io/upload_images/5703029-75b5cf4521ce4f48.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 按需引入vant庫的時候 報了一個css-loader的錯誤 解決辦法:增加exclude config.module.rules.push( { test: /.(css|less)$/, use: [{ loader: 'style-loader', // creates style nodes from JS strings }, { loader: 'css-loader',// translates CSS into CommonJS }, { loader: 'postcss-loader' }, { loader: 'less-loader', options: { javascriptEnabled: true } // compiles Less to CSS }], exclude: /node_modules/ })
  • 最終選擇了 目前的穩定版,更改了相關的配置 並引入相關的插件
相關的插件

插件名稱

功能

備註

@storybook/addon-notes

組件中添加notes,裝飾story

注釋文本資訊

@storybook/addon-actions

展示event數據

@storybook/addon-backgrounds

改變頁面的背景色

@storybook/addon-storysource

展示組件源碼

@storybook/addon-knobs

動態展示props

storybook-readme

將markdown導入為story

@storybook/addon-viewport/register

增加移動端預覽模式

@storybook/addon-options

配置面板選型

相關文檔

vue-loader升級方案

Storybook 3.2 引入 Vue.js 支援

storybook for vue 官網

@storybook/vue npm

Storybook 5.0 正式發布:有史以來變化最大的版本

引入Rollup打包

介紹

Rollup 是一個 JavaScript 模組打包器,可以將小塊程式碼編譯成大塊複雜的程式碼,例如 library 或應用程式。採用 es6 原生的模組機制進行模組的打包構建, 編譯之後包 體積會更小。

更多詳情 可以看我之前的一篇文章

rollup的初識

引入之後的問題
  • Cross-env報錯的問題
sudo npm install --global cross-env
  • Rollup 打包 ,如果使用babel.config.js+babel7的話,坑比較多…….有時候會出現一些不知名的錯誤

建議想嘗試的同學 使用babel6 + babelrc這樣的配置

https://chenshenhai.github.io/rollupjs-note/note/chapter00/01.html

  • rollup-plugin-vue在低版本0.68的時候,會報一個找不到input入口的錯,目前項目中的rollup版本是V1.19.3

附rollup.config.prod.js

import resolve from 'rollup-plugin-node-resolve';  import commonjs from 'rollup-plugin-commonjs';  import buble from 'rollup-plugin-buble'  import replace from 'rollup-plugin-replace';  import { uglify } from 'rollup-plugin-uglify';  import vue from 'rollup-plugin-vue'  import postcss from 'rollup-plugin-postcss';  const path = require('path');    const ENV = process.env.NODE_ENV;  const resolveFile = function (filePath) {    return path.join(__dirname, './', filePath)  }      export default {    input: resolveFile('src/components/index.js'),    output: {      dir: 'es',      format: 'umd',      name: 'miVant',      exports: 'named',    },    plugins: [      resolve({ extensions: ['.js', '.vue'] }),      postcss({        extensions: ['.less', '.css'],        use: [          ['less', {            javascriptEnabled: true          }]        ],        extract: true,        minimize: true,      }),      vue({        template: {          isProduction: true        },        css: false      }),      commonjs(),      buble({        objectAssign: 'Object.assign'      }),      replace({        exclude: 'node_modules/**',        ENV: JSON.stringify(process.env.NODE_ENV),      }),      (ENV === 'production' && uglify()),    ],  };
留一個問題: rollup完成按需載入的打包

vue的markdown解析

I want

  1. 將組件中的readme文檔改造為組件的使用文檔
  2. 類似於目前知名組件庫如 antd,element-ui,vant等支援程式碼庫高亮顯示,組件動態展示等效果,簡而言之一句話: 能夠在md中運行程式碼。
  3. 讓我們的組件庫看起來不那麼low?……….

由於之前沒有接觸過類似的功能,於是漫漫的調研之路開始了。。。。

  • Vue-press vue作者開發的 仿照vue的風格 適合靜態文檔 卻不能很好的展示預覽效果vuePress中文文檔
    • 類似於hexo 想搭建個人部落格的同學可以用一下
  • vue-markdown-loader 搭配 vue-loader可以實現動態編譯md文檔
    • 搭配vue-loader15版本的時候 需要注意採用兼容寫法
    • 搭配markdown-it系列能夠良好的擴展md
    • 需要注意的是 Vue-markdown-loader在搭配vue-loader15的時候 loader的寫法要注意下
    • 使用highlight.js的主題 支援主題的動態配置

附webpack中關於markdown的解析規則

{          test: /.md$/,          use: [            {              loader: 'vue-loader'            },            {              loader: 'vue-markdown-loader/lib/markdown-compiler',              options: {                raw: true,                preventExtract: true,                use: [                  [                    require('markdown-it-container'),                    'demo',                    {                      validate: function (params) {                        return params.trim().match(/^demos+(.*)$/)                      },                      render: function (tokens, idx) {                        if (tokens[idx].nesting === 1) {                          // 1.獲取第一行的內容使用markdown渲染html作為組件的描述                          let demoInfo = tokens[idx].info.trim().match(/^demos+(.*)$/)                          let description = demoInfo && demoInfo.length > 1 ? demoInfo[1] : ''                          let descriptionHTML = description ? markdownRender.render(description) : ''                          // 2.獲取程式碼塊內的html和js程式碼                          let content = tokens[idx + 1].content                          // 3.使用自定義開發組件【DemoBlock】來包裹內容並且渲染成案例和程式碼示例                          return `<demo-block>                          <div class="source" slot="source">${content}</div>                          ${descriptionHTML}                          <div class="highlight" slot="highlight">`                        } else {                          return '</div></demo-block>n'                        }                      }                    }                  ]                ]              }            }          ]        },

demo-block中手動補充copy功能

<template>    <div class="demo-block">      <div class="demo-block-source">        <slot name="source"></slot>        <span class="demo-block-code-icon" v-if="!$slots.default" @click="showCode=!showCode">          <img            alt="expand code"            src="https://gw.alipayobjects.com/zos/rmsportal/wSAkBuJFbdxsosKKpqyq.svg"            class="code-expand-icon-show"          />        </span>      </div>      <div class="demo-block-meta" v-if="$slots.default">        <slot></slot>        <span v-if="$slots.default" class="demo-block-code-icon" @click="showCode=!showCode">          <img            alt="expand code"            src="https://gw.alipayobjects.com/zos/rmsportal/wSAkBuJFbdxsosKKpqyq.svg"            class="code-expand-icon-show"          />        </span>      </div>      <div class="demo-block-code" v-show="showCode">        <p class="copy" @click="copy">複製</p>        <slot name="highlight"></slot>      </div>    </div>  </template>  <script type="text/babel">  export default {    data() {      return {        showCode: false      };    },    methods: {      copy(e) {        const hightext = e.target.nextElementSibling;        const input = document.createElement("input");        document.body.appendChild(input);        let value = hightext.innerText;        input.value = value;        input.select();        if (document.execCommand("copy")) {          document.execCommand("copy");          console.log("複製成功");        }        document.body.removeChild(input);      }    }  };  </script>  <style lang='less'>  @import "./less/demo-block.less";  .copy {    cursor: pointer;    position: absolute;    right: 10px;    top: 0;  }  </style>
相關文檔

VuePress 手摸手教你搭建一個類Vue文檔風格的技術文檔/部落格

從 Vue-cli 開始構建 UI 庫到 Markdown 生成文檔和演示案例

vue-markdown-loader

markdown-it-container

vue-markdown-loader error with vue Loader 15

rem的適配+訂製主題

rem的適配功能

  • 通過postcss-px2rem將px單位自動轉化為rem單位
  • 通過項目根目錄下的.postcssrc.js 設置轉化規則 // https://github.com/michael-ciniawsky/postcss-load-config module.exports = { "plugins": { "postcss-import": {}, "postcss-url": {}, // to edit target browsers: use "browserslist" field in package.json "autoprefixer": { browsers: ['Android >= 4.0', 'iOS >= 7'] }, "postcss-px2rem": { remUnit: 100 } } }
  • css中補充對應的font-size大小 @import './var.less'; html { font-size: 100px; /* no */ } h1{ font-size: 32px; } h2{ font-size: 24px; } h3{ font-size: 19px; } h4{ font-size: 16px; } h5{ font-size: 14px; } h6{ font-size: 13px; } li,p,th,td { font-size: 16px; }

訂製主題

miVant 使用了 Less 對樣式進行預處理,並內置了一些樣式變數,通過替換樣式變數即可訂製你自己需要的主題。

配置文件: ~/src/components/less/var.less

@primary-btn-color :#fbb212;

訂製方法

  1. 使用 less 提供的 modifyVars 即可對變數進行修改,下面是參考的 webpack 配置。
  2. 這裡以vue2.x版本的腳手架舉例 /build/utils目錄下
exports.cssLoaders = function (options) {    options = options || {}      const lessLoader = {      loader: 'less-loader',      options: {        sourceMap: options.sourceMap,        modifyVars: {          color: 'red'        }      }    }    const lessConfig = {      modifyVars: {        primary-btn-color: 'red'      }    };        const cssLoader = {      loader: 'css-loader',      options: {        sourceMap: options.sourceMap      }    }      const postcssLoader = {      loader: 'postcss-loader',      options: {        sourceMap: options.sourceMap      }    }      // generate loader string to be used with extract text plugin    function generateLoaders(loader, loaderOptions) {      const loaders = options.usePostCSS ? [cssLoader, postcssLoader, lessLoader] : [cssLoader, lessLoader]        if (loader) {        loaders.push({          loader: loader + '-loader',          options: Object.assign({}, loaderOptions, {            sourceMap: options.sourceMap          })        })      }        // Extract CSS when that option is specified      // (which is the case during production build)      if (options.extract) {        return ExtractTextPlugin.extract({          use: loaders,          fallback: 'vue-style-loader'        })      } else {        return ['vue-style-loader'].concat(loaders)      }    }      // https://vue-loader.vuejs.org/en/configurations/extract-css.html    return {      css: generateLoaders(),      postcss: generateLoaders(),      less: generateLoaders('less', lessConfig),      sass: generateLoaders('sass', { indentedSyntax: true }),      scss: generateLoaders('sass'),      stylus: generateLoaders('stylus'),      styl: generateLoaders('stylus')    }  }

項目啟動和運行

// 克隆項目到本地  git clone  https://github.com/majunchang/mi-vant.git  // 切換到master分支  // 安裝相關依賴   npm  i    //  npm 腳本  // storybook 預覽模式   "start": "npm run storybook",   "storybook": "start-storybook -p 9001 -c .storybook",  // 打包storybook靜態文件   "build-storybook": "build-storybook -c .storybook",  // 文檔預覽 埠是8081   "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",  // 打包之後的依賴包分析   "bundle-report": "webpack-bundle-analyzer --port 8123 dist/stats.json",  // webpack build   "build": "node build/build.js",  // rollup 編譯   "clean": "rimraf rollupDist",   "rollup": "cross-env NODE_ENV=production rollup --config=rollup.config.prod.js"