最近在项目内部建立了一个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。