組件庫構建過程

最近在項目內部建立了一個vue組件庫,但願經過組件庫的形式,統一項目中組件的邏輯和樣式,讓代碼的複用性更強。css

這篇文章主要是梳理組件庫的整個結構和構建過程。html

結構

圖片描述

首先在這裏介紹一下組件庫的代碼結構,上面是總體代碼目錄結構,每一個目錄的做用以下:vue

  1. packages:組件源碼位置,每一個組件做爲一個子目錄;同時提供packages/index.js做爲全局組件的入口(具體內容後面會介紹)
  2. lib:存放編譯後的代碼
  3. build:構建工具相關(後面構建過程當中會重點介紹)
  4. config:環境配置相關
  5. examples:doc文檔相關
  6. test:單元測試代碼
  7. 其餘:eslint、babel、git相關的配置文件

這裏再詳細說一下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模塊化原理-commonjswebpack模塊化原理-ES modulewebpack模塊化原理-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。

相關文章
相關標籤/搜索