UI組件庫開發-從開發到發佈到npm(更新用插件按需加載)

開始前

在參考過一些資料和開源的UI組件庫後,寫下了這篇文章,但願能給你們一些幫助。css

UI組件庫先後摸索了差很少近一個星期才勉強學會,其實組件庫開發不算特別複雜(複雜組件當我沒說 #滑稽保命),期間主要是卡在了目錄組織和打包的問題上,目錄組織的問題主要是涉及到按需加載的問題,若是一把梭所有引入反而沒那麼複雜。而後打包問題也是由於按需加載的須要,須要配置不一樣的webpack配置文件,這裏摸索了特別久,最後再看了vant、nutui這些開源的組件庫後,還算是入門了吧,這裏其實我主要參考了nutui的組織結構,webpack的配置基本也是這麼來的。文章雖然含金量很少,但願能幫助到須要本身學習組件庫開發的同窗。html

好了,廢話很少說,直接開始!vue

搭建項目

本次開發沒有基於Vue CLI來搭建開發環境,而是基於我上次webpack4本身搭建的環境來構建的環境,雖然官方CLI也能夠,並且也有本身的庫模式來發布組件庫。可是爲了可以更多的有本身自定義的選項,仍是沒選擇官方腳手架。若是你想要使用官方CLI來開發,那麼本文章雖然不能手把手教你,但應該仍是能夠起到拋磚引玉的做用。node

接下來咱們須要作這幾步,先把目錄結構定下來:webpack

1.png

examples文件夾下用來展現你的組件基本用法。其實至關於把src的文件目錄改成了examplesgit

packages文件夾下用來書寫你的組件。github

修改告終構後還涉及到一些細微的修改,好比文件夾改成你項目的名字(我這兒叫cookie-ui),package.json裏面的name根據本身的須要來修改。web

postcss.config.js裏面關於px轉rem和px轉vw|vh那個也要去掉,這兒咱們使用px單位來開發。npm

同時咱們須要修改咱們的入口,由於默認搭建出來的入口是src。在bulid目錄下修改webpack.config.js中如下代碼:json

module.exports = {
  /* 省略代碼 */
  entry: {
    main: path.resolve(__dirname, "../examples/main.js")
  },
  resolve: {
    /* 省略代碼 */
    '@': path.resolve(__dirname, '../examples')
    /* 省略代碼 */
  }
  /* 省略代碼 */
}
複製代碼

這樣一來,再把examples改造一下,便於咱們寫演示代碼。examples目錄以下圖:

2.png

這個文件夾目錄的結構就跟咱們平時作單頁面開發同樣,這裏我就再也不贅述各個文件夾的功能,其實這個結構也不是非得這樣來佈置,能夠根據本身的狀況來規劃目錄便可。

編寫組件庫

重點主要在packages目錄下組件庫的編寫,目前個人目錄結構以下所示:

3.png

  • components 存放你的組件。
  • style 存放你的相關的樣式,組件單獨的樣式我放在了組件目錄裏面管理了,好比圖片你面的button.scss。
  • index.js 組件庫的註冊和導出都在這裏。

style文件夾下的樣式你們根據本身須要來規劃就好,不必定按我這個方式來

一、普通組件

接下來我以一個button組件來舉例,先按圖建好相關文件,相關代碼我會加一些註釋,若是你不太清楚Vue插件開發,Sass等這些知識點建議先學習一下相關知識,這裏再也不展開。

4.png

  • button.scss
@import '../../style/common/variable.scss';
.cookie-button {
  position: relative;
  display: inline-block;
  width: 90px;
  height: 40px;
  line-height: 40px;
  border-radius: 4px;
  outline: 0;
  border: 0;
  appearance: none;
  color: #fff;
  font-size: 16px;
  &.cookie-button--primary {
    background-color: $button-primary;
    border: 1px solid $button-primary;
  }
  &.cookie-button--danger {
    background-color: $button-danger;
    border: 1px solid $button-danger;
  }
  &.cookie-button--warning {
    background-color: $button-warning;
    border: 1px solid $button-warning;
  }
  &.cookie-button--info {
    background-color: $button-info;
    border: 1px solid $button-info;
  }
}
/* 加這個代碼是爲了讓按鈕點擊看起有個反饋效果 */
.cookie-button::before {
  position: absolute;
  content: "";
  left: 50%;
  top: 50%;
  width: 100%;
  height: 100%;
  background-color: #000;
  opacity: 0;
  transform: translate(-50%, -50%);
  border: inherit;
  border-color: #000;
  border-radius: inherit;
}
.cookie-button:active::before {
  opacity: 0.1;
}
複製代碼
  • Button.vue
<template>
  <button :class="classSet" @click="handleClick">
    // 這裏使用了插槽知識
    <slot></slot>  
  </button>  
</template>


<script>
export default {
  name: 'ck-button',
  props: {
    type: {
      type: String,
      default: 'primary'
    }
  },
  computed: {
    classSet() {
      let classResult = `cookie-button cookie-button--${this.type}`;
      return classResult;
    }
  },
  methods: {
    handleClick() {
      this.$emit('click');
    }
  }
}
</script>
複製代碼
  • index.js
// 配置對外引用
import Button from './Button.vue';
import './button.scss';

// 提供install方法
// 這裏提供一次install是爲了便於單獨引入buttton組件時進行註冊
Button.install = function(Vue) {
  Vue.component(Button.name, Button);
};

// 默認導出方式導出
export default Button;
複製代碼

這樣咱們就實現了一個簡單的按鈕組件。 咱們到根目錄的index.js下進行install。在index.js中加入如下代碼:

/* 組件庫對外導出的組件集合,對整個組件進行導出 */

// 導入組件(用於註冊全部組件)
import Button from './components/button';


// 定義組件列表
const componentsList = [
  Button
];

const install = function(Vue) {
  // 判斷是否安裝過
  if(install.installed) return;

  // 註冊全部組件
  componentsList.map((component) => {
    Vue.component(component.name, component);
  })
}

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

export default {
  install,
  Button
}
複製代碼

而後咱們進入咱們的examples目錄來展現咱們的按鈕組件,在main.js中所有引入

// 引入組件(註冊全部)
import CookieUI from '../packages/index.js';
Vue.use(CookieUI);
複製代碼

在組件中使用按鈕:

<div class="box"><ck-button type="primary" @click="testClick">基本按鈕</ck-button></div>
<div class="box"><ck-button type="danger">危險按鈕</ck-button></div>
<div class="box"><ck-button type="warning">警告按鈕</ck-button></div>
<div class="box"><ck-button type="info">信息按鈕</ck-button></div>
複製代碼
  • 注意:<ck-button>這個跟你寫的組件的name屬性相關(好比我這裏就是Button.vue裏面的name屬性),名字只要符合規範便可。

5.gif

二、modal組件

這種組件不須要Vue.component()方法來註冊,好比常見的Toast、Dialog,我這兒是直接綁定到Vue原型上,在項目裏面能夠直接使用this調用。

6.png

  • toast.scss
// 定義的變量
@import '../../style/common/variable.scss';
// 使用的彈性佈局
@import '../../style/mixins/flex_style.scss';
// 動畫相關的樣式
@import '../../style/mixins/animation.scss';

.cookie-toast--mask {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 100;
  @include flex-all-center; // Sass的混入語法
  .cookie-toast--dialog {
    max-width: 80vw;
    background-color: rgba(0, 0, 0, 1);
    padding: 16px;
    box-sizing: border-box;
    border-radius: 4px;
    animation: zoomIn .3s ease-out 0s forwards;
  }
  .cookie-toast--content {
    font-size: $font-size-s;
    color: #fff;
  }
}

@include anima-zoomIn
複製代碼
  • Toast.vue
<template>
  <div v-if="show" class="cookie-toast--mask">
    <div class="cookie-toast--dialog">
      <p class="cookie-toast--content">{{ message }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ck-toast',
  data() {
    return {
      show: false,
      message: ''
    }
  },
  methods: {

  }
}
</script>
複製代碼
  • index.js
import Vue from 'vue';
import toastComponent from './Toast.vue';
import './toast.scss';

const toastConstructor =  Vue.extend(toastComponent);
let instance;

/**
 * 打開toast 
 * @param options {Object}  消息內容options.message,不可省略。停留時間options.duration,可省略,默認爲2000(毫秒)
**/
let toast = function(options = {}) {
  if(!instance) {
    instance = new toastConstructor({
      el: document.createElement('div')
    });
  }

  if(instance.show === true) return;

  instance.message = options.message;
  instance.show = true;
  document.body.appendChild(instance.$el)

  let timer = setTimeout(() => {
    instance.show = false;
    clearTimeout(timer);
  }, options.duration || 2000);
}

export default toast;
複製代碼

這樣,一個Toast就完成了,而後咱們須要到index.js裏面註冊

/* 組件庫對外導出的組件集合,對整個組件進行導出 */

// 導入組件(用於註冊全部組件)
import Button from './components/button';
import Toast from './components/toast';

// 定義組件列表
const componentsList = [
  Button
];

const install = function(Vue) {
  // 判斷是否安裝過
  if(install.installed) return;

  // 註冊全部組件
  componentsList.map((component) => {
    Vue.component(component.name, component);
  })

  Vue.prototype.$toast = Toast;
}

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

export default {
  install,
  Button,
  Toast,
}
複製代碼

這樣咱們就註冊好了toast,結合上面mian.js引入button的那段代碼,咱們能夠在項目裏面這樣使用:

this.$toast({message: 'Hello,Toast演示', duration: 1500});
複製代碼

6.gif

另付Dialog演示,代碼這裏再也不貼出來,後面我會把代碼傳到github,須要的自取。

8.gif

注:後期我把packages目錄下的index.js改成了cookieui.js。

webpack打包

組件庫開發完畢,咱們是須要發佈到npm上供其餘人使用的,否則單獨提出來的意義也不大,因此咱們首先要作的的就是將UI組件庫打包,這兒仍是藉助了webpack,它專門有針對library進行設置。

一、打包準備

這種方式是把全部相關的打包到js中,而後把樣式單獨抽離出來,造成css文件,最終你打包下來的目錄就是一個js和一個css,咱們把它發佈到npm,當別人下載下來事後,引入方式大概就變成這種樣子(這裏只是舉個例子):

import CookieUI from 'cookie-ui';
import '../cookie-ui/index.css';
Vue.use(CookieUI);
複製代碼

首先咱們在build的目錄下新建三個文件webpack.lib.base.jswebpack.lib.prod.jswebpack.lib.prod.disperse.js

  • webpack.lib.base.js 通用的基本配置
  • webpack.lib.prod.js 打包全部組件
  • webpack.lib.prod.disperse.js 分組件打包

  • webpack.lib.base.js
// 庫打包的主要配置
// 引入vue-loader插件
const VueLoaderPlugin = require('vue-loader/lib/plugin');
// 引入清除打包後文件的插件(最新版的須要解構,否則會報不是構造函數的錯,並且名字必須寫CleanWebpackPlugin)
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  // 咱們打包組件庫時不須要把Vue打包進去
  externals: {
    'vue': {
      root: 'Vue',
      commonjs: 'vue',
      commonjs2: 'vue',
      amd: 'vue',
    }
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'
          }	
        ]
      },
      {
        test: /\.vue$/,
        use: [
          {
            loader: 'vue-loader',
            options: {
              compilerOptions: {
                preserveWhitespace: false
              }
            }
          }
        ]
      },
      {
        test: /\.(jpe?g|png|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 5120,
              esModule: false,
              fallback: 'file-loader',
              name: 'images/[name].[ext]'
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new VueLoaderPlugin()
  ],
  resolve: {
		alias: {
      'vue$': 'vue/dist/vue.runtime.esm.js',
    },
    extensions: ['*', '.js', '.vue']
	}
};
複製代碼
  • webpack.lib.prod.js
// 打包全部
// node.js裏面自帶的操做路徑的模塊
const path = require("path");
const merge = require('webpack-merge');
const webpackLibBaseConfig = require('./webpack.lib.base.js');
// 用於提取css到文件中
const miniCssExtractPlugin = require('mini-css-extract-plugin');
// 用於壓縮css代碼
const optimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');

module.exports = merge(webpackLibBaseConfig, {
  mode: 'production',
  devtool: 'source-map',
  entry: {
    cookieui: path.resolve(__dirname, "../packages/cookieui.js")
  },
  output: {
    // 打包事後的文件的輸出的路徑
    path: path.resolve(__dirname, "../lib"),
    // 打包後生成的js文件
    filename: "[name].js",
    publicPath: "/",
    library: 'cookieui',
    libraryTarget: 'umd',
    libraryExport: 'default',
    umdNamedDefine: true
  },
  module: {
    rules: [
      {
        test: /\.(scss|sass)$/,
        use: [
          {
            loader: miniCssExtractPlugin.loader, // 使用miniCssExtractPlugin.loader代替style-loader
          },
          {
            loader: 'css-loader',
          },
          {
            loader: 'sass-loader',
            options: {
              implementation: require('dart-sass')
            }
          },
          {
            loader: 'postcss-loader'
          }
        ]
      },
    ]
  },
  plugins: [
    // 新建miniCssExtractPlugin實例並配置
    new miniCssExtractPlugin({
      filename: '[name].css'
    }),
    // 壓縮css
    new optimizeCssnanoPlugin({
      sourceMap: true,
      cssnanoOptions: {
        preset: ['default', {
          discardComments: {
            removeAll: true,
          },
        }],
      },
    }),
  ]
})
複製代碼
  • webpack.lib.prod.disperse.js
// 用於對組件單獨打包,便於按需加載
// 用於拷貝的插件
const copyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
const miniCssExtractPlugin = require('mini-css-extract-plugin');
const optimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
const merge = require('webpack-merge');
const webpackLibBaseConfig = require('./webpack.lib.base.js');
// 引入入口配置文件
const entryConfig = require('../packages/entry_config.js');

//定義入口
let entry =  {};
entryConfig.configList.map((item) => {
  let componentName = item.name.toLowerCase();
  entry[componentName] = path.resolve(__dirname, '../packages/components/' + componentName + '/index.js');
});

module.exports = merge(webpackLibBaseConfig, {
  mode: 'production',
  devtool: '#source-map',
  entry,
  output: {
    // 打包事後的文件的輸出的路徑
    path: path.resolve(__dirname, "../lib/packages"),
    // 打包後生成的js文件
    // 解釋下這個[name]是怎麼來的,它是根據你的entry命名來的,入口叫啥,出口的[name]就叫啥
    filename: "[name]/index.js",
    // 我這兒目前尚未資源引用
    publicPath: "/",
    library: '[name]',
    libraryTarget: 'umd',
    libraryExport: 'default',
    umdNamedDefine: true
  },
  module: {
    rules: [
      {
        test: /\.(scss|sass)$/,
        use: [
          {
            loader: miniCssExtractPlugin.loader, // 使用miniCssExtractPlugin.loader代替style-loader
          },
          {
            loader: 'css-loader',
          },
          {
            loader: 'sass-loader',
            options: {
              implementation: require('dart-sass')
            }
          },
          {
            loader: 'postcss-loader'
          }
        ]
      },
    ]
  },
  plugins: [
    // 新建miniCssExtractPlugin實例並配置
    new miniCssExtractPlugin({
      filename: '[name]/style.css'
    }),
    // 壓縮css
    new optimizeCssnanoPlugin({
      sourceMap: true,
      cssnanoOptions: {
        preset: ['default', {
          discardComments: {
            removeAll: true,
          },
        }],
      },
    }),
  ]
})
複製代碼

同時在packages目錄下新建一個entry_config.js,這個是用來單獨打包組件時的配置

module.exports = {
  configList: [
    {
      name: 'button',
      author: 'LEE'
    },
    {
      name: 'toast',
      author: 'LEE'
    },
    {
      name: 'dialog',
      author: 'LEE'
    }
  ]
}
複製代碼

注:上面涉及的相關的包若是你沒有安裝的話,手動安裝一次便可。

這樣一來,webpack就配置完畢了,如今咱們還須要修改package.json裏面的script,加上如下幾句:

"lib:all": "webpack --config ./build/webpack.lib.prod.js",
"lib:disp": "webpack --config ./build/webpack.lib.prod.disperse.js"
複製代碼

二、分別打包

接下來咱們分別執行上面的命令,而後你會發現目錄下有個lib目錄,生成項目以下:

9.png

npm發佈

一、發佈準備

  • 首先你得有一個npm的帳號,到官網註冊一個便可。npm

  • 修改你目錄下的package.json

這個根據你狀況來寫,通常來講name、version、main這幾個屬性不可省略。同時你得name不能跟npm上其它開發者發佈的包重名,像我這個cookie-ui就重複了,因此我改爲了vue-cookie-ui。-_-!。。。這裏我給個大概參數配置,須要看完整的到倉庫自取哈

{
  "name": "vue-cookie-ui",
  "version": "1.0.0",
  "description": "A Personal Learning UI library For Vue",
  "main": "lib/cookieui.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js",
    "lib:all": "webpack --config ./build/webpack.lib.prod.js",
    "lib:disp": "webpack --config ./build/webpack.lib.prod.disperse.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/cookiepool/cookie-ui.git"
  },
  "keywords": [
    "UI",
    "Vue",
    "UI-Library"
  ],
  "author": "LEE",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/cookiepool/cookie-ui/issues"
  },
  "homepage": "https://github.com/cookiepool/cookie-ui#readme",
  /* 省略代碼 */
}
複製代碼
  • 新建一個.npmignore文件,用來忽略不發佈的文件
touch .npmignore
複製代碼

加入如下代碼

.DS_Store
node_modules
/dist
/build

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

examples/
packages/
public/
babel.config.js
build
postcss.config.js
教程.MD
.gitignore
*.map
*.html
複製代碼

二、開始發佈

前面註冊好了和配置好package.json等工做後,在根目錄打開bash,輸入

npm login
複製代碼

這裏你按照提示登陸好帳號便可,登陸成功事後,再來一個發佈命令便可

npm publish
複製代碼

發佈成功事後你就能夠到npm查到本身的包了。

10.png

從npm下載並引用組件

咱們發佈到npm後就能夠從npm下載並使用了

npm i vue-cookie-ui
複製代碼

下載完成後去咱們的項目裏面引用(main.js)

所有引入

// 引入組件(註冊全部)
import CookieUI from 'vue-cookie-ui';
import 'vue-cookie-ui/lib/cookieui.css';
Vue.use(CookieUI);
複製代碼

11.gif

按需引入

// 按需加載
// 引入組件
import Button from 'vue-cookie-ui/lib/packages/button';
import 'vue-cookie-ui/lib/packages/button/style.css';
Vue.use(Button)
// 引入modal(Dialog和Toast都要這樣註冊)
import Toast from 'vue-cookie-ui/lib/packages/toast';
import 'vue-cookie-ui/lib/packages/toast/style.css';
Vue.prototype.$toast = Toast;
複製代碼

這裏我只按需引入了Toast、Button,Dialog沒有引入,演示你會發現Toast正常,Dialog沒法工做並出現了報錯。

12.gif

13.png

藉助babel-plugin-component按需引入

安裝依賴

npm install babel-plugin-component
複製代碼

在babel.config.js中加入如下代碼:

plugins: [[
  // 配置按需引入插件babel-plugin-component
  "component",
  {
    // 庫的名字爲VUI
    "libraryName": "vue-cookie-ui",
    // 存放庫文件的文件夾爲lib/packages
    "libDir": "lib/packages",
  }
]]
複製代碼

而後你就能夠這樣引入了,插件會自動幫你轉換路徑

// 使用babel-plugin-component
import { Button, Toast, Dialog } from 'vue-cookie-ui';
Vue.use(Button);
Vue.prototype.$toast = Toast;
Vue.prototype.$dialog = Dialog;
複製代碼

結語

到這裏就告一段落了,特別感謝nutui的源代碼,給了不少參考,若有錯誤,還請多多包涵,並指出錯誤。

前期在社區上也找了許多開發組件庫的文章,也感謝這些開源分享的大佬。若是幫助到了你點個贊再走吧!

這裏附上代碼的github地址:cookie-ui

相關文章
相關標籤/搜索