Deer-ui:一個簡單高效的react組件庫

一、前言

本身一直想搭建一套本身的博客系統,今年年底終於有時間開發,在開發後臺cms系統時,使用antd組件庫,忽然一天問本身,組件庫是怎麼實現的,內部是怎麼運行的,打包部署是怎麼個原理,帶着這些疑問,我開始萌生了本身開發一套組件庫的想法。說幹就幹,本身花費了大概2個多月的時間完成了deer-ui初版的組件開發,如今分享出來,記錄下開發過程踩過的坑。javascript

源碼deer-ui | 官網預覽地址css

二、組件庫開發準備工做

需求準備

首先肯定需求,初期先調研了流行的組件庫,如antdvantzarm-web,cuke-ui,iview等等,最後選定antd做爲ui參考。前端

技術準備

  • 組件庫預覽網站技術選擇,對比了幾個(docz,styleguidist,storebook),最後選擇storebook做爲展現網站,不只限於此,項目中還集成了docz,styleguidist打包,若有須要可實現。
  • 組件調試方案,最後肯定了兩種,可按需求。一、使用源碼中使用create-react-app搭建了一個react環境,在example文件夾下,使用npm run dev,便可打開調試環境,引入編寫的組件便可.

二、源碼中搭建了一套組件庫的文檔部署環境,使用命令npm run storybook,便可進入文檔模式,引入編寫的組件便可.java

  • 組件庫代碼打包,在參考幾大流行的組件庫後,決定本身手寫打包配置文件
  • 打包發版,組件庫引入自動化發版,運行腳本後直接發佈npm倉庫
  • 一期組件分類,因爲時間緣由不可能一次性開發徹底部的組件,因此一期準備開發下面16個組件

三、搭建項目

3.1 項目結構

  • .storebook storebook 的一些配置
  • components 放置全部組件
  • example 組件調試環境代碼
  • scripts 發佈,打包的腳本文件
  • stories 項目靜態文檔,負責 demo 演示
  • 以及一些配置文件

3.2 打包腳手架開發

使用webpack4完成組件庫的打包,組件庫用戶通常經過es modulecommonjs以及script腳本的方式引入,這就須要咱們的組件庫知足這些規範,一般使用webpack打包成umd規範便可知足上面不一樣的引入。node

...
  mode: "production",
  entry: {
    [name]: ["./components/index.js"]   
  },
  output: {
    path: path.join(process.cwd(), "dist"),
    library: name,
    libraryTarget: "umd",
    umdNamedDefine: true,
    filename: "index.js",
  },
  ...

打包的入口是components/index.js文件,主要是導出組件react

export { default as Button } from './button';
export { default as Tabs } from './tabs';
export { default as Icon } from './icons';

最後打包後會在dist目錄下生成index.js文件,爲全部打包後組件。webpack其餘配置不一一詳述了,若有須要查看scripts目錄。webpack

3.3按需加載組件打包

3.3.1 打包js文件

經過上述webpack打包的組件庫代碼,是全量引入的,全部的組件代碼都打包在一塊兒。爲了減少引入組件庫大小,通常都採用按需加載。按需加載的核心是要單獨將每一個組件打包出來,經過import方式單獨引入。git

經過 babel 打包js文件
"build:lib": "cross-env OUTPUT_MODULE=commonjs babel components -d lib" 
"build:es": "babel components -d es",

3.3.2 打包樣式文件

將各組件的樣式文件提取到lib,es文件對應的組件下面,css打包代碼以下,這塊是借鑑cuke-ui的配置,經過gulp流的方式,打包css文件。github

/**
 * @name gulpfile.js
 * @description 打包項目css依賴
 */

const path = require("path");
const gulp = require("gulp");
const concat = require("gulp-concat");
const less = require("gulp-less");
const autoprefixer = require("gulp-autoprefixer");
const cssnano = require("gulp-cssnano");
const size = require("gulp-filesize");
const sourcemaps = require("gulp-sourcemaps");
const rename = require("gulp-rename");
const { name,browserList } = require("../package.json");
const DIR = {
  less: path.resolve(__dirname, "../components/**/*.less"),
  buildSrc: [
    path.resolve(__dirname, "../components/**/style.less"),
    path.resolve(__dirname, "../components/**/index.less")
  ],
  lib: path.resolve(__dirname, "../lib"),
  es: path.resolve(__dirname, "../es"),
  dist: path.resolve(__dirname, "../dist")
};

gulp.task("copyLess", () => {
  return gulp
    .src(DIR.less)
    .pipe(gulp.dest(DIR.lib))
    .pipe(gulp.dest(DIR.es));
});

gulp.task("copyCss", () => {
  return gulp
    .src(DIR.buildSrc)
    .pipe(sourcemaps.init())
    .pipe(
      less({
        outputStyle: "compressed"
      })
    )
    .pipe(autoprefixer({ browsers: browserList }))
    .pipe(size())
    .pipe(cssnano())
    .pipe(gulp.dest(DIR.lib))
    .pipe(gulp.dest(DIR.es));
});

gulp.task("dist", () => {
  return gulp
    .src(DIR.buildSrc)
    .pipe(sourcemaps.init())
    .pipe(
      less({
        outputStyle: "compressed"
      })
    )
    .pipe(autoprefixer({ browsers: browserList }))
    .pipe(concat(`${name}.css`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))
    .pipe(sourcemaps.write())
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))

    .pipe(cssnano())
    .pipe(concat(`${name}.min.css`))
    .pipe(size())
    .pipe(gulp.dest(DIR.dist))
    .pipe(sourcemaps.write())
    .pipe(size())
    .pipe(gulp.dest(DIR.dist));
});

gulp.task("default", gulp.series(["copyLess", "copyCss", "dist"]));

3.3.3組件庫文檔搭建

文檔採用storybook來搭建,文檔地址預覽,具體配置以下web

import React from "react";
import { configure, addDecorator,addParameters } from '@storybook/react';
const { name, repository, version } = require("../package.json")
import { configureActions } from '@storybook/addon-actions';
import '@storybook/addon-console';
import '@storybook/addon-options/register';
import "../stories/style/index.less"
import "../stories/style/code.less"

function loadStories() {
  // 介紹
  require('../stories/index');
  //基礎組件
  require('../stories/basis');
  //數據展現
  require('../stories/showData');
  //操做反饋
  require('../stories/feedback');
  //交互組件
  require('../stories/interaction');
  //佈局組件
  require('../stories/layout');
}

configureActions({
  depth: 100
})


//加載配置
addParameters({
  options: {
    name: `${name} v${version}`,
    title: "Deer-ui",
    url: repository,
    showSearchBox: false,
    showPanel: false,
    enableShortcuts:false,
    isToolshown: false,
    selectedPanel: undefined,
    hierarchySeparator: null,
    hierarchyRootSeparator: null,
    showAddonPanel: false,
  }})
//中間content邊距
addDecorator(story => <div style={{ padding: "0 60px 50px" }}>{story()}</div>)
configure(loadStories, module);

編寫組件文檔

storiesOf("操做反饋", module)
  .add("Spin 加載中", () => (
    <div>
      <h4>基本使用</h4>
      <div style={{ marginBottom: "30px" }}>
        <Spin />
      </div>
      <CodeView
        value={`
        import { Spin } from 'deer-ui'
        <Spin />
      `}
      ></CodeView>
     
  ))

最後單獨給storybook配置webpack.config.js,注意配置格式和通常的webpack有點區別

const webpack = require("webpack");
module.exports = async ({ config, mode }) => {
  config.resolve = {
    extensions: [".js", ".jsx", ".json", ".jsx"]
  };

  config.module.rules.push({
    test: /\.(js|jsx)?$/,
    loaders: [require.resolve("@storybook/source-loader")],
    enforce: "pre",
    exclude: /node_modules/
  });

  config.module.rules.push({
    test: /\.scss$/,
    use: [
      "style-loader",
      "css-loader",
      "postcss-loader",
      {
        loader: "sass-loader"
      }
    ]
  },
  {
    test: /\.less$/,
    use: [
      "style-loader",
      "css-loader",
      "postcss-loader",
      {
        loader: "less-loader",
        options: {
          javascriptEnabled: true
        },
      }
    ]
  });
  config.plugins.push(
    new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn|en-gb/)
  );
  return config;
};

四、開發組件

項目框架已經搭建完成,在components目錄下就能夠開發組件了,例如button組件,刪減版

...省略
class Button extends Component {
    return (
      <div className={cls(prefixCls, {[`${prefixCls}-block`]:block})}>
        <button
          type="button"
          {...isDisabled}
          className={cls(`${prefixCls}-btn`,className,`${prefixCls}-btn-${type}`
          </button>
      </div>
    );
  }
}

export default Button;

在index.js文件中導出button組件

export { default as Button } from './button';

五、發佈

開發完成組件庫後,經過把代碼發佈到npm倉庫便可,默認你們都會發布npm包,若是有不瞭解請參考掘金上有關npm問題。框架提供了自動化發佈命令,打包,發版,lint,日誌等功能

5.1 發佈組件庫

npm run pub:prod    //自動完成css,js,es,lin,umd打包,自動生成changelog,發佈npm倉庫,爲修訂版版本號。1.0.*
    
    npm run pub:major  //都會完成上述不一樣,惟一區別是,打的npm版本號不一樣,此命令是打主版本號,不常常用 *.0.0
    
    npm run pub:minor  //都會完成上述不一樣,惟一區別是,打的npm版本號不一樣,此命令是打次版本號,不常常用 1.*.0

    "pub:prod": "npm run standard:patch && npm run build && npm publish --registry https://registry.npmjs.org && git push" //打版本號和發版

5.2 發佈組件庫文檔

組件庫提供兩種發佈方式

一、npm run pub:docs   採用storybook的方式去發佈,該方式須要在package.json中配置帳號信息。
"storybook-deployer": {
    "gitUsername": "deer-ui",
    "gitEmail": "your email",
    "commitMessage": "docs: deploy docs"
},

2.npm run deploy  //該命令會執行腳本deploy.sh文件,打包併發布組件庫文檔

#!/bin/bash

# 確保腳本拋出遇到的錯誤
set -e
echo "start build..."
# 打包文檔
npm run build:docs

echo "√ build success"

# 進入生成的文件夾
cd .docs

echo "start publish..."
# 提交到  gh-pages
git config  --get remote.origin.url
git init
git config user.name "xxxx"
git config user.email "xxxx"
git add .
git commit -m 'docs:publish'

git push --force --quiet git@github.com:zhangboyang123/deer-ui.git master:gh-pages
echo "√ publish success 🦌"

cd -

六、主題定製

6.一、主題變量

Deer-ui使用less做爲樣式開發語言,並定義了一系列全局/組件的樣式變量,你能夠根據需求進行相應調整。
如下是一些最經常使用的通用變量,全部樣式變量能夠在 這裏 找到。

@primary-color: #31c27c;   //全局色
@warning-color: #fca130;    //警告色
@error-color: #f93e3e;      //失敗色
@success-color: #35C613;    //成功色
@info-color: #61affe;       //信息展現色
@default-color: #d9d9d9;    //默認色
@border-color: #e8e8e8;     //邊框顏色
@border-radius: 4px;        //邊框圓角
@font-size: 14px;           //默認組件字體大小
@font-size-small: 12px;     //小字體
@font-size-large: 16px;     //大字體
@bg-color: #FAFAFA;         //組件背景色
@font-color: rgba(0, 0, 0, .65);    //字體顏色
@disabled-font-color: fade(@font-color, 30%);  //禁用字體顏色

6.2 主題定製原理

主題定製原理上是使用 less 提供的 modifyVars 的方式進行覆蓋變量。使用webpack中配置less-loaderoptions。注意javascriptEnabled要打開。

// webpack.config.js
module.exports = {
  rules: [{
    test: /\.less$/,
    use: [{
      loader: 'style-loader',
    }, {
      loader: 'css-loader', // translates CSS into CommonJS
    }, {
      loader: 'less-loader', // compiles Less to CSS
+     options: {
+       modifyVars: {
+         'primary-color': '#1DA57A',
+         'info-color': '#1DA57A',
+         'font-size': '12px',
+         // or
+         'hack': `true; @import "your-less-file-path.less";`, // 或者引用本地樣式文件覆蓋
+       },
+       javascriptEnabled: true,
+     },
    }],
  }],
}

注意,定製主題後,less-loader 的處理範圍不能過濾掉 node_modules 下的 deer-ui 包。

七、按需加載

7.1 單獨引入

import Button from 'deer-ui/es/button';
import 'deer-ui/es/button/style.less';

7.2 使用 babel-plugin-import

// 單獨使用在.babelrc.js中配置
module.exports = {
  plugins: [
    ["import", {
      "libraryName": "deer-ui",
      "libraryDirectory": "es",
      "style":true
    },"deer-ui"], 
  ]
}
// 多個組件庫,例如antd
module.exports = {
  plugins: [
    ["import", {
      "libraryName": "deer-ui",
      "libraryDirectory": "es",    
      "style": true            
    },'deer-ui'], 
    
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": true  
    },'antd'], 
  ]

### 八、後續計劃

    • 第一階段組件已經開發完畢,基本完成message,input,radio,button,table,checkbox,collapse,tabs,empty.loading,icon,divider等基礎組件的開發;完成Deer-ui組件庫框架搭建,實現自動化打包部署,增長stylelint,eslint,commitlint,自動生成changelog,組件庫測試環境搭建,組件庫官方文檔網站搭建,以及主題定製等功能。
    • 後續增長組件庫的自動化測試,國際化功能。
    • 繼續完成後面組件的開發。
    • 最後暢想下,使用ts完成組件庫的重構。

      九、總結

      開發完組件庫,也許沒啥意義, 可是經過這個組件庫, 讓我學到了不少平時 接觸不到的知識點,有時看着很簡單的東西,本身動手會發現裏面有好多坑。整體來講,去年給本身定的小目標已經實現,今年繼續在前端的路上不停的折騰,正所謂,生命不息,coding不止😝。

    相關文章
    相關標籤/搜索