最近在項目內部建立了一個vue組件庫,但願經過組件庫的形式,統一項目中組件的邏輯和樣式,讓代碼的複用性更強。css
這篇文章主要是梳理組件庫的整個結構和構建過程。html
首先在這裏介紹一下組件庫的代碼結構,上面是總體代碼目錄結構,每一個目錄的做用以下:vue
這裏再詳細說一下packages,先看一下packges的目錄結構:node
packages中子目錄的名字就是組件的名稱,每一個組件會有index.vue和index.sass做爲組件入口和樣式入口。
同時,在packages根目錄,index.js做爲全局註冊組件入口,會引入全部組件,而後調用Vue.component
註冊爲全局組件。webpack
ok,目錄結構梳理清楚,但這也只是開發過程的一部分,至於最終的輸出內容,還須要基於具體使用場景來編譯,下面是目前組件庫支持的使用方式和具體的編譯方法。git
瀏覽器引入必然包括script和link,因此對應的,咱們須要打包出包含全部組件須要的js和css文件。
對於script這種使用場景,須要把全部代碼都打包到一個文件中,那麼經過webpack的libraryTarget: 'window'
模式,就能達到咱們的要求。
再配合ExtractTextPlugin,便可獲取全部的css內容。
webpack入口文件就是packages/index.js
,最終編譯的文件,就是整個文件pirate.js和樣式文件pirate.css。github
webpack配置以下:web
"use strict"; const path = require("path"); const webpack = require("webpack"); const ExtractTextPlugin = require("extract-text-webpack-plugin"); function resolve(dir) { return path.join(__dirname, "..", dir); } module.exports = { entry: resolve("packages/index.js"), externals: { vue: { root: "Vue", commonjs: "vue", commonjs2: "vue", amd: "vue" } }, output: { path: resolve("lib"), filename: "pirate.js", library: "pirate", libraryTarget: "window", }, resolve: { extensions: [".js", ".vue"] }, module: { rules: [{ test: /\.vue$/, loader: "vue-loader", options: { js: { loader: "babel-loader", options: {} }, scss: { loader: ["css-loader", "scss-loader"] }, extractCSS: true } }, { test: /\.js$/, loader: "babel-loader", include: [resolve("package")] }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: "url-loader", options: { limit: 10000 } } ] }, plugins: [ new ExtractTextPlugin("pirate.css"), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) ] };
至於按需加載,默認的方式,固然能夠直接經過import Xxx from "pirate/lib/xxx/index.js"
的方式去加載js,同時還須要經過@import ~pirate/lib/index.css
手動加載樣式。npm
可是這裏建議配合babel-plugin-import這個插件來使用,代碼會更加簡潔溫馨。
那麼根據babel-plugin-import的要求,index.css會生成在lib/xxx/style
目錄下,這樣的話,按需加載就須要一行代碼:import { Xxx } from 'pirate'
。json
回到編譯階段,天然的會想到用webpack來編譯,每一個組件就是一個入口,而後使用webpack多入口的模式來編譯。
首先,前置一個自動化收集組件目錄的工做,生成components.json
用來做爲webpack入口,實現的build/component.js
代碼以下:
const fs = require('fs-extra'); const path = require('path'); function isDir(dir) { return fs.lstatSync(dir).isDirectory(); } const json = {}; const dir = path.join(__dirname, '../packages'); const files = fs.readdirSync(dir); files.forEach(file => { const absolutePath = path.join(dir, file); if (isDir(absolutePath)) { json[file] = `./packages/${file}`; } }); console.log(JSON.stringify(json));
而後經過node build/components.js > components.json
生成components.json
,經過webpack編譯便可,webpack代碼以下:
'use strict' const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const components = require('../components.json'); function resolve(dir) { return path.join(__dirname, '..', dir) } module.exports = { entry: components, externals: { vue: { root: 'Vue', commonjs: 'vue', commonjs2: 'vue', amd: 'vue' }, }, output: { path: resolve('lib'), filename: '[name]/index.js', library: 'pirate', libraryTarget: 'umd', umdNamedDefine: true, }, resolve: { extensions: ['.js', '.vue'], }, module: { rules: [{ test: /\.vue$/, loader: 'vue-loader', options: { js: { loader: 'babel-loader', options: {}, }, scss: { loader: ['css-loader', 'scss-loader'], }, extractCSS: true, } }, { test: /\.js$/, loader: 'babel-loader', include: [resolve('package')], }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, } }, ] }, plugins: [ new ExtractTextPlugin('[name]/style/index.css'), ], }
可是這裏有一個問題,或者說是一個優化點,就是經過webpack生成的入口代碼,都會包了一層webpack的啓動器(想了解能夠看我以前的文章webpack模塊化原理-commonjs、webpack模塊化原理-ES module、webpack模塊化原理-Code Splitting),而一般做爲按需加載來講,用戶會有本身的webpack,那麼組件庫須要作的就是把vue文件編譯成js,僅此而已(甚至vue文件也是能夠的,可是考慮到更通用的場景,js仍是更合適)。
因此,這裏可使用vue官方提供的vue-template-compiler,他的工做是把vue模板編譯成獨立的vue對象。這裏我會使用同事開發的vue-sfc-compiler來作編譯,vue-sfc-compiler底層封裝了vue-template-compiler,上層提供了babel的支持,使用起來會更加方便,不過目的是同樣的。
那麼,基於上面webpack編譯的文件,我會用vue-sfc-compiler編譯出的更小的文件作覆蓋,具體代碼以下:
const fs = require('fs-extra'); const compiler = require('vue-sfc-compiler'); const path = require('path'); function isDir(dir) { return fs.lstatSync(dir).isDirectory(); } function compile(dir) { const files = fs.readdirSync(dir); files.forEach(file => { const absolutePath = path.join(dir, file); if (isDir(absolutePath)) { return compile(absolutePath); } if (/\.vue|.js$/.test(file)) { const source = fs.readFileSync(absolutePath, 'utf-8'); const content = compiler(source).js; const outputPath = absolutePath.replace('packages', 'lib').replace('.vue', '.js'); fs.outputFileSync(outputPath, content); } }); } const dir = path.join(__dirname, '../packages'); compile(dir);
對於全局組件註冊的方式,我會把這個入口做爲整個module的入口,也就是默認的使用方式。
在上一步,按需加載階段,其實已經把每一個組件編譯好了,那麼入口文件,其實只要用babel作個轉換就能夠了,這裏用到gulp來操做,代碼以下:
const gulp = require('gulp'); const babel = require('gulp-babel'); const path = require('path'); gulp.task('default', () => gulp.src(path.join(__dirname, '../packages/index.js')) .pipe(babel({ presets: ['env'] })) .pipe(gulp.dest(path.join(__dirname, '../lib'))) );
最後還有一個遺憾,目前文檔化還沒完成,那麼這裏先描述一下目前的設想,等實現後再寫一篇分享。
如今計劃是每一個組件目錄增長一個demo.vue和doc.md,demo.vue用來演示當前組件功能,doc.md做爲文檔內容。而後經過一個自動化工具,把全部組件demo和doc合併到一塊兒,生成一個html。