天天看點

元件庫搭建總結

開始搭建之前要明确需要支援什麼能力,再逐個考慮要如何實作。本項目搭建時計劃需要支援以下功能:

  • 支援元件測試/demo
  • 支援不同的引入方式 : 全部引入 / 按需加載
  • 支援主題定制
  • 支援文檔展示

元件測試/demo

本項目是

vue

元件庫,元件開發過程中的測試可以直接使用

vue-cli

腳手架,在項目增加了

/demos

目錄,用來在開發過程中調試元件和開發完成後存放各個元件的例子. 隻需要修改在

vue.config.js

中入口路徑,即可運作 demos

index: {
        entry: 'demos/main.ts',
  }
           
"serve": "cross-env BABEL_ENV=dev vue-cli-service serve",
           

運作時傳入了一個 babel 變量 是用來區分 babel 配置的,後面會有詳細說明。

打包

js 打包暫時用的還是

webpack

, 樣式處理使用的是

gulp

, 考慮支援兩種引入方式,全部引入和按需加載,兩種場景會有不同的打包需求。

全部引入

支援全部引入,需要有一個入口檔案,暴露并可以注冊所有的元件。

/src/index.ts

就是全部元件的入口,它導出了所有元件,還有一個

install

函數可以周遊注冊所有元件(為什麼是 install?詳見 vue 插件 )。還需要加一些對

script

引入情況的處理 —— 直接注冊所有元件。

打包的時候需要以入口檔案為打包入口,全部元件一起打包。

按需加載

顧名思義,使用者可以隻加載使用到的元件的 js 及 css,且不論他通過何種方式來按需引入,就元件庫而言,我們需要在打包時将各個元件的代碼分開打包,這樣是他能夠按需引入的前提。這樣的話,我們需要以每個元件作為入口來分别打包。

按需加載的實作可以簡單的使用

require

來實作,雖然有點粗暴,需要使用者

require

對應的元件 js 和 css。檢視了一些資料和開源庫的做法,發現了更人性化的做法,使用 babel 插件輔助,可以幫我們把

import

文法轉換成

require

文法,這樣使用者在寫法上會更加簡單。

比如

babel-plugin-component

插件,可以檢視文檔,會幫我們進行文法轉換

import { SectionWrapper } from "xxx";

// 轉換成
require("xxx/lib/section-wrapper");
require("xxx/lib/css/section-wrapper.css");
           

那我們需要在按需加載打包時,按照一定的目錄結構來放置元件的 js 和 css 檔案,友善使用者用 babel 插件來進行按需加載

樣式打包

同樣的,全部引入的樣式打包和按需加載的樣式打包也有所不同。

全部引入時,所有的樣式檔案(元件樣式,公共樣式)打包成一份檔案,使用時引入一次即可。

按需加載時,樣式檔案需要分元件來打包,每個元件需要生産一份樣式檔案,使用時才能分開加載,隻引入需要的資源,因為要使用 babel 插件,是以還要控制樣式檔案的位置。

是以樣式在編寫時,就需要公共/元件分開檔案,這樣友善後面打包處理,考慮目錄結構如下:

│  └─ themes                                                   
│     ├─ src               // 公共樣式                                    
│     │  ├─ base.less                                          
│     │  ├─ mixins.less                                        
│     │  └─ variable.less                                      
│     ├─ form-factory.less // 元件樣式                                    
│     ├─ index.less        // 所有樣式入口
           

themes/index.less

會引入所有元件的樣式及公共樣式

themes/components-x.less

隻包含元件的樣式

公共資源

元件之間公用的方法/指令/樣式,當然希望能在使用時隻加載一份。

公共樣式

全部引入時沒有問題,所有的樣式檔案都會一起引入。

按需加載時,不能在元件樣式檔案中都打包進一份公共樣式,這樣引入多個元件時,重複的樣式太多。考慮把公共樣式單獨打包出來,按需引入的時候,單獨引入一次公共樣式檔案。這次引入也可以通過

babel-plugin-component

插件幫我們實作,詳見文檔中的相關配置。

公共 JS

有些js資源(方法/指令)是多個元件都會用到的,不能直接打包到元件中,否則按需加載多個元件時會出現多份重複的資源。是以考慮讓元件不打包這些資源,要用到

webpack.externals

配置,

webpack.externals

可以從輸出的 bundle 中排除依賴,在運作時會從使用者環境中擷取,詳見文檔。

這裡需要考慮的時,如何辨識哪些是公共js,以及在使用者環境中要去哪裡擷取? , 這裡是參考

element-ui

的做法

公共JS通過目錄來約定,

src/utils/directives

下為公共指令,

src/utils/tools

下為公共方法,同樣的,引入公共資源的時候也約定好方式,按照配置的

webpack.resolve.alias

, 這樣在可以友善配置

webpack.externals

// webpack.resolve.alias
  {
    alias: {
      'xxx': resolve('.')
    }
  }

  // 引入資源通過  xxx/src/...
  import ClickOutside from 'xxx/src/utils/directives/clickOutside'

  // 配置`webpack.externals`
  const directivesList = fs.readdirSync(resolve('/src/utils/directives'))
  directivesList.forEach(function(file) {
    const filename = path.basename(file, '.ts')
    externals[`xxx/src/utils/directives/${filename}`] = `yyy/lib/utils/directives/${filename}`
  })
           

至于要如何在使用者環境中擷取,在打包時會吧

utils

中資源也一起打包釋出,是以通過 釋出的包名(package.json 中的 name)來擷取,也就是上面示例代碼中的

yyy

下一步就是要考慮如何處理

utils

中的檔案?,

utils

中的資源也可能會互相應用,比如方法A中使用了方法B,也需要在處理的時候,要避免互相引入,也要每個單獨處理(babel)成單個檔案,因為使用者會在使用者環境中尋找單個的資源。

直接使用bable指令行來處理會更加友善

"build:utils": "cross-env BABEL_ENV=utils babel src/utils --out-dir lib/utils --extensions '.ts'",

會對每個檔案進行babel相關的處理,生成的檔案會在

lib/utils

中,和上面的

webpack.externals

配置時對應的

另外還要使用

babel-plugin-module-resolver

插件,檢視 文檔,這裡的作用是讓打包之後到新的地方去找檔案。比如在

utils/tools/a

import B from 'xxx/src/utils/b'

,打包之後,會到

'xxx/lib/utils/'

下去找對應的資源

{
  plugins: [
    ['module-resolver', {
      root: ['xxx'],
      alias: {
        'xxx/src': 'xxx/lib'
      }
    }]
  ]
}

           

不需要被打包的依賴

本項目中會使用到

ant-design-vue

vue

庫,但是都不需要被打包,這應該是由使用者自己引入的。

webpack.externals

在上面有用到過,在打包時可以排除依賴

peerDependencies

可以保證所需要的依賴被安裝,詳見文檔

這兩個配合就可以實作不打包

ant-design-vue

vue

不被打包,也不會影響元件庫的運作

實作

綜上,簡單總結下,我們在打包時需要做的事情

  • 全部引入和按需加載需要分開打包
  • 支援全部引入需要,以

    src/index.ts

    為入口進行打包,并且需要打包出一份包含所有樣式的 css 檔案
  • 支援按需加載需要,以每個元件為入口打包出獨立的檔案,并且需要單獨打包出每個元件的樣式檔案和一份公共樣式檔案。之後需要按照對應的目錄結構放好檔案,友善配合 babel 插件實作按需加載
  • 排除不需要被打包的依賴

需要兩份不同的打包,分别對應全部引入和按需加載的打包

"build:main": "cross-env BABEL_ENV=build webpack --config build/webpack.main.config.js",
    "build:components": "cross-env BABEL_ENV=build webpack --config build/webpack.components.config.js",
           

以下是兩種打包方式都需要做的事情

配置

webpack.externals

loader

plugins

function getUtilsExternals() {
    const externals = {}

    const directivesList = fs.readdirSync(resolve('/src/utils/directives'))
    directivesList.forEach(function(file) {
      const filename = path.basename(file, '.ts')
      externals[`xxx/src/utils/directives/${filename}`] = `xxx/lib/utils/directives/${filename}`
    })
    const toolsList = fs.readdirSync(resolve('src/utils/tools'))
    toolsList.forEach(function(file) {
      const filename = path.basename(file, '.ts')
      externals[`xxx/src/utils/tools/${filename}`] = `xxx/lib/utils/tools/${filename}`
    })

    return externals
  }


  // webpack配置
  {
    mode: 'production',
    devtool: false,
    externals: {
      ...getUtilsExternals(),
      vue: {
        root: 'Vue',
        commonjs: 'vue',
        commonjs2: 'vue',
        amd: 'vue'
      },
      'ant-design-vue': 'ant-design-vue'
    },
    module:{
      // 相關loader
      rules: [
        {
          test: /\.vue$/,
          loader: 'vue-loader',
          options: {
            loaders: {
              ts: 'ts-loader',
              tsx: 'babel-loader!ts-loader'
            }
          }
        },
        {
          test: /\.tsx?$/,
          exclude: /node_modules/,
          use: [
            'babel-loader',
            {
              loader: 'ts-loader',
              options: { appendTsxSuffixTo: [/\.vue$/] }
            }
          ]
        }
      ]
    },
    plugins: [
      new ProgressBarPlugin(),
      new VueLoaderPlugin() // vue loader的相關插件
    ]
  }
           

全部引入

以下是全部引入的入口和輸出,這裡打包輸出到lib目錄下,lib目錄是打包後的目錄。

這裡需要注意的是同時要配置

package.json

中的相關字段(

main

,

module

),這樣釋出之後,使用者才知道入口檔案是哪個,詳見 文檔

這裡還需要注意

output.libraryTarget

的配置,要根據需求來配置對應的值,詳見文檔

{
  entry: {
  index: resolve('src/index.ts')
  },
  output: {
    path: resolve('lib'),
    filename: '[name].js',
    libraryTarget: 'umd',
    libraryExport: 'default',
    umdNamedDefine: true,
    library: 'xxx'
  },
}
           

按需引入

以下是按需的入口和輸出,入口是解析到所有的元件路徑,

output

libraryTarget

也不同,因為按需加載沒法支援浏覽器加載,是以不需要

umd

模式

// 解析路徑函數
function getComponentEntries(path) {
  const files = fs.readdirSync(resolve(path))
  const componentEntries = files.reduce((ret, item) => {
    if (item === 'themes') {
      return ret
    }
    const itemPath = join(path, item)
    const isDir = fs.statSync(itemPath).isDirectory()
    if (isDir) {
      ret[item] = resolve(join(itemPath, 'index.ts'))
    } else {
      const [name] = item.split('.')
      ret[name] = resolve(`${itemPath}`)
    }
    return ret
  }, {})
  return componentEntries
}
// webpack配置
{
  entry: {
    // 解析每個元件的入口
    ...getComponentEntries('components')
  },
  output: {
    path: resolve('lib'),
    filename: '[name]/index.js',
    libraryTarget: 'commonjs2',
    chunkFilename: '[id].js'
  },
}

           

樣式處理

使用

gulp

處理樣式,對入口樣式(所有樣式)/ 元件樣式 / 公共樣式 進行相關處理(less -> css, 字首,壓縮等等),然後放在對應的目錄下

// ./gulpfile.js
function compileComponents() {
  return src('./components/themes/*.less') // 入口樣式,元件樣式
    .pipe(less())
    .pipe(autoprefixer({
      cascade: false
    }))
    .pipe(cssmin())
    .pipe(dest('./lib/css'))
}

function compileBaseClass() {
  return src('./components/themes/src/base.less') // 公共樣式
    .pipe(less())
    .pipe(autoprefixer({
      cascade: false
    }))
    .pipe(cssmin())
    .pipe(dest('./lib/css'))
}
           

主題定制

實作主題定制,主要的思路是樣式變量覆寫,比如本項目中使用的是

less

來書寫樣式,而在

less

中,同名的變量,後面的會覆寫前面的,詳見 文檔

作為元件庫,支援主題定制,需要做兩點:

  • 會把可能需要變化的樣式定義成樣式變量,并告訴使用者相關的變量名
  • 提供

    .less

    類型的樣式引入方式

項目中的樣式本就是通過

.less

格式編寫的,且定義了部分可修改的變量名

components\themes\src\variable.less

,需要提供引入less樣式的方式即可,要将将

less

樣式整體複制到

lib

// ./gulpfile.js
function copyLess() {
  return src('./components/themes/**')
    .pipe(cssmin())
    .pipe(dest('./lib/less'))
}

           

需要自定義樣式時,需要使用者,引入

less

樣式檔案。如果此時需要按需引入的話,要

require

對應的元件js檔案,不能通過babel插件來實作,因為後者會引入預設的元件樣式,和less樣式互相影響且重複。

文檔化

考慮能有一個門戶網站,能包含元件庫的所有示例和使用文檔。

本項目使用了

storybook

來實作,詳見 文檔。

所有的内容都在

.storybook/

目錄中,需要為每一個元件都編寫一個對應的 story

類型檔案

本項目本身是采用ts編寫的,本來考慮采用取巧的方式,通過 typescript編譯器 自動生成類型檔案的

獨立有一份

tsconfig.json

,配置了需要生成類型檔案

"declaration": true,
    "declarationDir": "../types",
    "outDir": "../temp",
           

"types": "rimraf types && tsc -p build && rimraf temp"

,運作時會把.ts編譯為.js,随便生成類型檔案,然後删掉生成的js檔案即可,這樣就隻會留下

.d.ts

類型檔案。

但是這種方式生成的類型檔案有點亂,有的還需要自己調整,是以就還是手寫。除了檢視 typescript官網外,還可以檢視 文檔

目錄結構

最終,整體的目錄結構是

xxx                             
├─ build                                 webpack配置                                                       
│  ├─ config.js                                                
│  ├─ tsconfig.json                                            
│  ├─ utils.js                                                 
│  ├─ webpack.components.config.js                             
│  └─ webpack.main.config.js                                   
├─ components                            元件源碼                                       
│  ├─ form-factory                                          
│  │  ├─ formFactory.tsx                                       
│  │  └─ index.ts                                                                               
│  └─ themes                             元件樣式                     
│     ├─ src                                                   
│     │  ├─ base.less                                          
│     │  ├─ mixins.less                                        
│     │  └─ variable.less                                      
│     ├─ form-factory.less                                     
│     ├─ index.less                                                                            
├─ demos                                  調試檔案                                                                  
├─ dist                                   storybook打包目錄                                                  
├─ lib                                    元件庫打包目錄                   
│  ├─ css                                                      
│  │  ├─ base.css                                              
│  │  ├─ form-factory.css                                      
│  │  ├─ index.css                                                                              
│  ├─ form-factory                                             
│  │  └─ index.js                                              
│  ├─ less                                                     
│  │  ├─ src                                                   
│  │  │  ├─ base.less                                          
│  │  │  ├─ mixins.less                                        
│  │  │  └─ variable.less                                      
│  │  ├─ form-factory.less                                     
│  │  ├─ index.less                                                                       
│  ├─ section-wrapper                                          
│  │  └─ index.js                                              
│  └─ index.js                                                 
├─ public                                                                                                  
├─ src
│  ├─ utils                               工具函數                    
│  │  ├─ directives                                         
│  │  ├─ tools                                                                                                  
│  ├─ global.d.ts                                              
│  ├─ index.ts                            元件庫入口                          
│  └─ shims-tsx.d.ts                                           
├─ tests                                  測試檔案                                                       
├─ types                                  類型檔案                                                              
├─ babel.config.js                        babel配置                   
├─ gulpfile.js                            gulp配置                     
├─ jest.config.js                         jest配置                                                            
├─ package.json                                                
├─ readme.md                                                   
├─ tsconfig.json                          typescript配置                    
└─ vue.config.js                          vue-cli配置                    

           

釋出

釋出時需要注意的是

package.json

的相關配置,除了上面提到的

main

,

module

外,還需要配置以下字段

{
    "name": "xxx",
    "version": "x.x.x",
    "typings": "types/index.d.ts", // 類型檔案 入口路徑
    "files": [ // 釋出時需要上傳的檔案
      "lib",
      "types",
      "hcdm-styles"
    ],
    "publishConfig": { //釋出位址
      "registry": "http://xxx.xx.x/"
    }
}
           

其他

環境變量的使用

通過

cross-env

在執行腳本時可以傳入變量來做一些事情,本項目用到了兩處

  • 通過

    BABEL_ENV

    來讓

    babel.config.js

    配置來區分環境;vue-cli中提供的

    @vue/cli-plugin-babel/preset

    裡面配置的東西太多了,導緻元件庫打包出來體積增大,是以隻在變量為

    dev

    的時候使用,

    build

    的時候使用更簡單的必要配置,如下:
module.exports = {
  env: {
    dev: {
      presets: [
        '@vue/cli-plugin-babel/preset'
      ]
    },
    build: {
      presets: [
        [
          '@babel/preset-env',
          {
            loose: true,
            modules: false
          }
        ],
        [
          '@vue/babel-preset-jsx'
        ]
      ]
    },
    utils: {
      presets: [
        ['@babel/preset-typescript']
      ],
      plugins: [
        ['module-resolver', {
          root: ['xxx'],
          alias: {
            'xxx/src': 'yyy/lib'
          }
        }]
      ]
    }
  }
}
           
  • 通過

    BUILD_TYPE

    來控制是否需要引入打包分析插件
if (process.env.BUILD_TYPE !== 'build') {
  configs.plugins.push(
    new BundleAnalyzerPlugin({
      analyzerPort: 8123
    })
  )
}
           

&&

串聯執行腳本

"build:lib": "npm run clean &&cross-env BUILD_TYPE=build npm run build:main && cross-env BUILD_TYPE=build npm run build:components && gulp",

&&

可以串聯執行腳本,前一個指令執行完才會執行下一個腳本,可以将一組有前後關系的腳本組合在一起

繼續閱讀