組件庫搭建總結

開始搭建以前要明確須要支持什麼能力,再逐個考慮要如何實現。本項目搭建時計劃須要支持如下功能:css

  • 支持組件測試/demo
  • 支持不一樣的引入方式 : 所有引入 / 按需加載
  • 支持主題定製
  • 支持文檔展現

組件測試/demo

本項目是 vue 組件庫,組件開發過程當中的測試能夠直接使用 vue-cli 腳手架,在項目增長了/demos目錄,用來在開發過程當中調試組件和開發完成後存放各個組件的例子. 只須要修改在vue.config.js中入口路徑,便可運行 demoshtml

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

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

打包

js 打包暫時用的仍是 webpack, 樣式處理使用的是 gulp, 考慮支持兩種引入方式,所有引入按需加載,兩種場景會有不一樣的打包需求。node

所有引入

支持所有引入,須要有一個入口文件,暴露並能夠註冊全部的組件。 /src/index.ts 就是所有組件的入口,它導出了全部組件,還有一個install函數能夠遍歷註冊全部組件(爲何是 install?詳見 vue 插件 )。還須要加一些對script引入狀況的處理 —— 直接註冊全部組件。webpack

打包的時候須要以入口文件爲打包入口,所有組件一塊兒打包git

按需加載

顧名思義,使用者能夠只加載使用到的組件的 js 及 css,且不論他經過何種方式來按需引入,就組件庫而言,咱們須要在打包時將各個組件的代碼分開打包,這樣是他可以按需引入的前提。這樣的話,咱們須要以每一個組件做爲入口來分別打包。github

按需加載的實現能夠簡單的使用require來實現,雖然有點粗暴,須要使用者require對應的組件 js 和 css。查看了一些資料和開源庫的作法,發現了更人性化的作法,使用 babel 插件輔助,能夠幫咱們把import語法轉換成require語法,這樣使用者在寫法上會更加簡單。web

好比babel-plugin-component插件,能夠查看文檔,會幫咱們進行語法轉換vue-cli

import { SectionWrapper } from "xxx";

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

那咱們須要在按需加載打包時,按照必定的目錄結構來放置組件的 js 和 css 文件,方便使用者用 babel 插件來進行按需加載typescript

樣式打包

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

所有引入時,全部的樣式文件(組件樣式,公共樣式)打包成一份文件,使用時引入一次便可。

按需加載時,樣式文件須要分組件來打包,每一個組件須要生產一份樣式文件,使用時才能分開加載,只引入須要的資源,由於要使用 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/aimport B from 'xxx/src/utils/b',打包以後,會到 'xxx/lib/utils/' 下去找對應的資源

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

不須要被打包的依賴

本項目中會使用到ant-design-vuevue庫,可是都不須要被打包,這應該是由使用者本身引入的。

webpack.externals 在上面有用到過,在打包時能夠排除依賴

peerDependencies 能夠保證所須要的依賴被安裝,詳見文檔

這兩個配合就能夠實現不打包ant-design-vuevue不被打包,也不會影響組件庫的運行

實現

綜上,簡單總結下,咱們在打包時須要作的事情

  • 所有引入和按需加載須要分開打包
  • 支持所有引入須要,以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.externalsloaderplugins

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'
  },
}

按需引入

如下是按需的入口和輸出,入口是解析到全部的組件路徑,outputlibraryTarget 也不一樣,由於按需加載無法支持瀏覽器加載,因此不須要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",

&& 能夠串聯執行腳本,前一個命令執行完纔會執行下一個腳本,能夠將一組有先後關係的腳本組合在一塊兒

相關文章
相關標籤/搜索