如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

NO IMAGE

目標:搭建一個基於vue的ui腳手架,庫的加載方式支持script標籤模塊化加載,組件引入方式支持按需加載和全量加載。
腳手架鏈接

目錄、文件約定

完整的一個組件庫腳手架不但應該有合理的組件存放目錄,也應該擁有組件demo展示頁方便組件的開發和展示,所以需要我們提供兩種webpack配置,一個用於組件打包,一個用於組件單頁應用,然後,慢慢加上gulp/rollup。

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

出於開發調試方便的考慮,現在直接把包打進了node_modules中去,正式使用時按需修改build/const.jsLIB_ROOT的值。

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

正確的調試順序:

  1. npm install
  2. npm run build:lib 這個命令會先把目錄清空,所以放在build:es前
  3. npm run build:es
  4. npm run dev

如何使用webpack打包類庫

平時使用webpack打包應用程序較多,如何使用它來打包工具庫ui庫

祕密藏在ouput配置項裡,webpack既可以打包一般的網站應用,也可以打包出支持多種環境下使用的類庫。libraryTarget聲明打包出來的模塊類型,library跟模塊導出命名相關。

簡單來說,我們平時打包應用就是生成多個提供給script標籤引用的文件。配置了上面兩個字段,生成出來的文件就可以被script/require來使用了。而其他配置大概一致,也是按你的文件類型,生成物要求添加對應的loader、plugin。譬如需要vue-loader處理.vue文件,使用了vue-jsx需要安裝@vue/babel-preset-jsx相關插件支持,babel-loader等。

output: {
path: path.resolve(__dirname, '../', LIB_DIR),
// filename: '[name]/index.js',
library: 'wind-ui-vue',
libraryTarget: 'umd',
},

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

組件庫組件的開發模式,書寫模板及全量打包

組件庫的組件如何被使用

平時做應用,我們一個組件就是一個.vue文件,需要的時候就import進來。但是開發組件庫是不太一樣的,因為組件庫環境沒有Vue變量,需要開發者提供,所以需要做一層包裝去獲取實際開發環境才提供的Vue變量,每個組件都被自己的index.js包裹一層。使用打包器時,這裡用到了Vue.use的功能,它會調用對象參數提供的install方法。只要我們在方法裡添加了註冊函數,就自動完成組件的全局註冊了。

        import Button from './Button.vue'
Button.install = Vue => {
Vue.component(Button.name, Button)
}
if (typeof window != undefined && window.Vue) {
Button.install(Vue)
}
export default Button
        import Vue from 'vue'
import { Button } from 'wind-ui-vue'
Vue.use(Button)

如何全量引入組件

既然每個組件有自己的註冊文件了,那麼只要再搞一個入口文件,把每個組件都引入進去。

    import Button from './components/button/index'
import './components/button/index.scss'
import Card from './components/card/index'
import './components/card/index.scss'
const install = (Vue) => {
Vue.use(Button)
Vue.use(Card)
}
if (typeof window != undefined && window.Vue) {
install(Vue)
}
export {
Button,
Card
}
export default {
install
}

全量打包來了

全量組件的入口文件已經寫好,這時把它做成一個webpack入口,那麼就能把所有的組件打包成一個大包了。

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

組件如何打包以支持按需加載 —— webpack 多入口

組件庫越來越大,如何讓每個組件單獨打包,一個組件一個包

按需加載的前提是什麼?

是全部組件不能打包到一起。webpack是一個入口對應一個出口,所以我們需要為每個組件生成一個入口。

主要是用node的fs模塊讀寫文件操作居多,把所有組件的入口文件index.js找出來,做成webpack入口,再配置對應的出口路徑,我們設計成源碼和打包代碼的目錄結構大致相同。

const getFiles = function (dirs, fileReg) {
if (!Array.isArray(dirs)) {
dirs = [ dirs ]
}
return dirs.reduce((arr, dir) => {
let files = fs.readdirSync(dir)
if (!files.length) return [] 
files = files.reduce((arr, file) => {
let res = []
const filePath = path.join(dir, file)
fileReg.test(file) && res.push(filePath)
// if is directory
if (fs.statSync(filePath).isDirectory()) {
res = res.concat(getFiles(filePath, fileReg))
}
return arr.concat(res)
}, [])
return arr.concat(files)
}, [])
}
const entries =  (() => {
let files = getFiles('./src/components', /^index\.js$/)
files = files.reduce((obj, file) => {
return Object.assign(obj, {
[file.replace('src', '').replace('.js', '')]: './' + file
})
}, {})
return files
})()
module.exports = merge(baseConf, {
mode: 'development',
entry: {
...entries,
'index': './src/index.js'
},
output: {
path: path.resolve(__dirname, '../', LIB_DIR),
// filename: '[name]/index.js',
library: 'wind-ui-vue',
libraryTarget: 'umd',
},
plugins: [
new CleanWebpackPlugin()
]
})

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

看起來,類庫的配置相比於一般的應用配置複雜度可以降低一點,因為不用考慮緩存,開發打包效率等。

如何按需加載?

每個組件自己一個文件這個前提有了。接下來呢?簡單啊!!譬如需要Button組件就import Button from 'wind-ui-vue/lib/components/button/index.js'這樣引入,和我們平時在項目裡引入文件一樣的道理。但這麼寫多了,不得不說有點累,而且需要記住組件的目錄位置,誰會去node_modules裡找啊。這時該babel-import-plugin出場了,它能幫我們優化這麼煩人的操作,看看文檔。

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

get到了沒?它在幫我們替換源碼!!!把es6的import語法轉換到對應的路徑下去找組件。雖然這不是tree-shaking,是掩眼法。但是確實幫我們大忙了。可是我的Button不是在wind-ui-vue/lib/button,而是在wind-ui-vue/lib/components下,怎麼辦?其實還有個字段libraryDirectory可以聲明組件的路徑前綴。按實際情況處理就可以了。

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

webpack打包後的文件結構

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

組件樣式與組件邏輯分離,單獨打包 —— gulp

不同的組件的樣式分開打包,組件的樣式表可單獨打包,不與組件js邏輯捆綁。看看以下幾種使用姿勢

按需加載組件

import { Button } from 'wind-ui-vue',理想情況下當然希望組件對應的樣式一併加載進來。對於vue組件,可以直接把<style>寫進.vue文件是可以的。但如果想考慮得通用些,希望把樣式表單獨拿出來,這樣樣式就與邏輯分離了。

現在考慮的問題:樣式分離後如何被自動地被組件引用?上面提到的babel-import-plugin插件提供瞭解決辦法,看下圖,style字段看起來是聲明組件樣式表所在的目錄(true時為style目錄,其他值表示子路徑)。

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

因此我們要把樣式文件放在每個組件的style目錄中去。這裡我們可以生成一個.js文件,在裡面去引入對應的.scss文件。該文件會被插件自動插入到組件中去。

需要注意的是,這種思路下樣式文件是與組件邏輯相互獨立的文件,因此庫打包時樣式文件沒經過webpack的處理,是不是更乾淨?只有在應用打包時再由webpack處理這些scss文件。gulp處理後的文件結構如圖所示。

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

核心的代碼就這個,也是在聲明目錄下找出所有scss文件進行加工。

    gulp.task('esScss', () => {
return scssTask(ES_DIR)
})
function scssTask (DIR) {
// 把所有組件的樣式表找出來
const files = getFiles(['./src/components'], /^index\.scss$/)
files.forEach(file => {
const {
dir,
base
} = path.parse(file);
// 拷貝庫的一些公用的樣式
fse.copySync(
path.resolve(__dirname, 'src/style'),
path.resolve(__dirname, DIR, 'style')
)
// generate a .js file to require matched .scss file in every component directory and copy it
const destPath = path.join(__dirname,
DIR,
dir.replace(/[^\/]+\//, '')
)
// 把.scss源碼文件也拷貝過去
fse.copyFileSync(
path.resolve(__dirname, file),
path.resolve(
destPath,
base
)
)
// 生成一個引入了scss文件的js文件,給babel-import-plugin用的
mkdirp.sync(path.resolve(destPath, 'style'))
fs.writeFileSync(path.resolve(destPath, 'style', 'index.js'), `require ('../index.scss')`)
})
}

全量加載組件

import win from 'wind-ui-vue',此時沒有了babel插件的幫忙,我們只能手動引入全部的組件樣式,在入口文件挨個地import .scss文件就是了,和import .js文件一個道理

    import Button from './components/button/index'
import './components/button/index.scss'
import Card from './components/card/index'
import './components/card/index.scss'
const install = (Vue) => {
Vue.use(Button)
Vue.use(Card)
}
if (typeof window != undefined && window.Vue) {
install(Vue)
}
export {
Button,
Card
}
export default {
install
}

考慮到umd這種模塊化方案使用,需要提供.css文件。

  • 組件單獨引用時,每個組件的樣式是單獨的樣式文件,用gulp處理。
    gulp.task('lib', ['libScss'], () => {
return gulp.src('src/**/*.scss')
.pipe(sass.sync().on('error', sass.logError))
.pipe(postcss([ autoprefixer() ]))
.pipe( gulp.dest(LIB_DIR, { sourcemaps: true }) )
})
  • 全量引入組件時,將所有組件樣式打包成一份.css。回頭看看我們註冊了所有組件的index.js文件裡已經引入了所有的組件樣式,所以把它們抽離出來就是了。
    • webpack打包器使用mini-css-extract-plugin插件(自己控制生成目錄)

      const MiniCssExtractPlugin = require('mini-css-extract-plugin');
      module.exports = {
      module: {
      rules: [
      {
      test: /\.scss$/,
      use: [
      {
      loader: MiniCssExtractPlugin.loader,
      },
      'css-loader',
      'postcss-loader',
      {
      loader: 'sass-loader',
      }
      ]
      }
      ]
      },
      plugins: [
      new MiniCssExtractPlugin()
      ]
      }
      
    • rollup打包器使用rollup-plugin-scss插件(自己控制生成目錄)

      import scss from 'rollup-plugin-scss'
      module.exports = {
      input: './' + file,
      output: {
      file: file.replace('src', ES_DIR),
      format: 'esm'
      },
      plugins: [
      scss(),
      ]
      }
      

提供ui庫的es6模塊版本 —— rollup

像上面打包成umd規範的模塊,雖然提供了所有的環境支持,但是依然無法通過 import win from 'wind-ui' 這種es6的語法引入,即不支持esModule。另外,由於webpack兼容上的考慮,打包組件上加入了自己一些多餘的編譯代碼,造成冗餘。那有沒辦法單獨打包es6的版本呢?可以試試 rollup

emmmm… rollup-plugin-vue相當於webpack的vue-loaderoutput.format配置為esm即為esModulebabel.runtimeHelpers設為true可以減少內聯的runtime代碼,讓打出來的包更精簡。

思路和webpack一致,也是把所有組件文件找出來,進行轉碼後輸出。rollup因為目標更單一,所以輸出文件體量更可觀。和webpack互為補充吧。

import commonjs from 'rollup-plugin-commonjs' 
import VuePlugin from 'rollup-plugin-vue'
import resolve from '@rollup/plugin-node-resolve';
import babel from 'rollup-plugin-babel';
import scss from 'rollup-plugin-scss'
const babelConf = require('./babel.config')
const { getFiles } = require('./build/util')
const { ES_DIR } = require('./build/const')
const entries =  (() => {
let files = getFiles('./src/components', /^index\.js$/)
console.log('files', files)
files.push('src/index.js')
files = files.map(file => {
return {
input: './' + file,
output: {
file: file.replace('src', ES_DIR),
format: 'esm'
},
// .replace('.js', '')
plugins: [
scss(),
resolve(),
babel({
runtimeHelpers: true,
exclude: 'node_modules/**'
}),
commonjs(),
VuePlugin()
]
}
})
console.log('files', files)
return files
})()
module.exports = entries

執行npm run build:es後,webpack生成的Lib目錄同級多了一個es目錄了。當然對生成資源還可以視實際情況進行更多的發揮。

如何用上webpack/gulp/rollup,搭建一個基於Vue的UI庫腳手架

在項目上使用ui庫

具體在demo頁上查看效果,源碼在site/src/main.js(省略了<script>標籤引入的展示)

// esModule方式全量引入
import win from 'wind-ui-vue/es'
import 'wind-ui-vue/es/index.css'
Vue.use(win)
// commonjs module方式全量引入
const win = require('wind-ui-vue').default
import 'wind-ui-vue/lib/index.css'
Vue.use(win)
// 按需加載,需配合`babel-import-plugin`使用
// 此時Card組件並沒有打包進去,Button組件的樣式已自動加載
import { Button, Card } from 'wind-ui-vue'
Vue.use(Button)

參考資料

  • vant-ui 之前項目用了vant,本組件庫搭建腳手架的思路也多參考於vant,謝謝開源

相關文章

SpringBoot系列JPA錯誤姿勢之Entity映射

Python植物大戰殭屍代碼實現:圖片加載和顯示切換

《碼了4個小時》SonarQube+Jenkins代碼質量檢查工具攻略大全

從客戶端角度窺探小程序架構