開始搭建以前要明確須要支持什麼能力,再逐個考慮要如何實現。本項目搭建時計劃須要支持如下功能: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",
&&
能夠串聯執行腳本,前一個命令執行完纔會執行下一個腳本,能夠將一組有先後關係的腳本組合在一塊兒