开始搭建以前要明确须要支持什么能力,再逐个考虑要如何实现。本项目搭建时计划须要支持如下功能:css
本项目是 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资源(方法/指令)是多个组件都会用到的,不能直接打包到组件中,不然按需加载多个组件时会出现多份重复的资源。因此考虑让组件不打包这些资源,要用到 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/a
中import B from 'xxx/src/utils/b'
,打包以后,会到 'xxx/lib/utils/'
下去找对应的资源
{ plugins: [ ['module-resolver', { root: ['xxx'], alias: { 'xxx/src': 'xxx/lib' } }] ] }
本项目中会使用到ant-design-vue
和vue
库,可是都不须要被打包,这应该是由使用者本身引入的。
webpack.externals
在上面有用到过,在打包时能够排除依赖
peerDependencies
能够保证所须要的依赖被安装,详见文档
这两个配合就能够实现不打包ant-design-vue
和vue
不被打包,也不会影响组件库的运行
综上,简单总结下,咱们在打包时须要作的事情
src/index.ts
为入口进行打包,而且须要打包出一份包含全部样式的 css 文件须要两份不一样的打包,分别对应所有引入和按需加载的打包
"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.externals
、 loader
、 plugins
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' }, }
如下是按需的入口和输出,入口是解析到全部的组件路径,output
的 libraryTarget
也不一样,由于按需加载无法支持浏览器加载,因此不须要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",
&&
能够串联执行脚本,前一个命令执行完才会执行下一个脚本,能够将一组有先后关系的脚本组合在一块儿