最近接到一個需求,針對公司的組件庫開發一個文檔。剛開始接到這個需求的時候一頭霧水。但仔細分析會發現,一個ui庫主要有三部分組成:javascript
可是這樣的組件庫要怎麼開發呢,難道針對「使用文檔」和「示例」還要單獨開發一個項目。固然這種作法是不可取的,否則咱們每次增長一個新的組件都要到「使用文檔」和「示例」項目去編寫對應的功能,並且這樣的代碼也不易維護。那麼有沒有可能咱們在一個組件目錄下完成組件的編寫,使用文檔和示例,由於畢竟組件的開發的人也是對組件最瞭解的人,使用文檔和示例也應有開發相應組件的人來編寫和維護。css
因而上網查閱了一些開源組件庫。當看到Vant-ui的時候,深深的被它精美的頁面設計吸引了。因而看了一下它的源碼結構,果真好看的外表下也有一顆有趣的靈魂。html
項目結構:vue
project
├─ src # 組件源代碼
│ ├─ button # button 組件源代碼
│ └─ dialog # dialog 組件源代碼
│
├─ docs # 靜態文檔目錄
│ ├─ home.md # 文檔首頁
│ └─ changelog.md # 更新日誌
│
├─ babel.config.js # Babel 配置文件
├─ vant.config.js # Vant Cli 配置文件
├─ pacakge.json
└─ README.md
複製代碼
組件結構:java
button
├─ demo # 示例目錄
│ └─ index.vue # 組件示例
├─ index.vue # 組件源碼
└─ README.md # 組件文檔
複製代碼
深深的被這樣的清晰的結構折服,但這華麗的外表下,底層作了大量的轉換工做。webpack
vant組件庫,主要是由他們內部開發的vant-cli進行編譯打包。vant-cli對於開發環境和生產環境的構建是不同的。nginx
啓動服務用於展現UI庫文檔,方便用戶開發組件,編寫組件文檔和示例
先來看下vant庫的文檔web
文檔分爲三部分:編程
模板風格在vant-cli腳手架中定義好的,可在源碼@vant\cli\site\desktop\components\index.vue中能夠找到json
van-doc組件
<div class="van-doc">
<doc-header
:lang="lang"
:config="config"
:versions="versions"
:lang-configs="langConfigs"
@switch-version="$emit('switch-version', $event)"
/>
<doc-nav :lang="lang" :nav-config="config.nav" />
<doc-container :has-simulator="!!simulator">
<doc-content>
//組件文檔slot
<slot />
</doc-content>
</doc-container>
//示例模擬器
<doc-simulator v-if="simulator" :src="simulator" />
</div>
<van-doc
:lang="lang"
:config="config"
:versions="versions"
:simulator="simulator"
:lang-configs="langConfigs"
>
//經過不一樣的路由展現不一樣的組件文檔
<router-view />
</van-doc>
複製代碼
slot部分,是由組件外部經過<router-view />
動態傳入的,咱們就能夠切換不一樣的路由對應不一樣的"組件文檔.md",可是md文檔怎麼就能經過vue組件的形式引入進來的呢?
vant-cli在開發環境時構建的時,在webpack配置中使用了添加了VantCliSitePlugin插件,
plugins: [
new vant_cli_site_plugin_1.VantCliSitePlugin(),
],
複製代碼
在這個插件中genSiteEntry方法中作了以下兩件事
async function genSiteEntry() {
...
gen_site_mobile_shared_1.genSiteMobileShared(); //生成引入全部"組件文檔.md"的入口文件
gen_site_desktop_shared_1.genSiteDesktopShared(); // 生成引入全部"示例.vue"的入口文件
...
}
複製代碼
genSiteDesktopShared生成的入口文件code,以下
"import config from 'C:/Users/hongjunjie/Desktop/vant/vant-demo2/vant.config';
import Home from 'C:/Users/hongjunjie/Desktop/vant/vant-demo2/docs/home.md';
import Quickstart from 'C:/Users/hongjunjie/Desktop/vant/vant-demo2/docs/quickstart.md';
import DemoButton from 'C:/Users/hongjunjie/Desktop/vant/vant-demo2/src/demo-button/README.md';
import DemoUtils from 'C:/Users/hongjunjie/Desktop/vant/vant-demo2/src/demo-utils/README.md';
export { config };
export const documents = {
Home,
Quickstart,
DemoButton,
DemoUtils
};
export const packageVersion = '1.0.0';
"
複製代碼
這個時候只是引入了"組件文檔.md"的文件,何時會轉成"vue"文件呢,這種場景咱們很天然的會想到loader,正如"vue-loader"能夠解析"vue"文件,
在webpack配置能夠發現
{
test: /\.md$/,
use: [CACHE_LOADER, 'vue-loader', '@vant/markdown-loader'],
},
@vant/markdown-loader能夠幫助咱們將"md"轉成"vue"文件
複製代碼
爲了更好的演示效果,組件示例應該是能夠單獨的運行在手機端的,因此應該抽離成單獨的一個頁面。在UI庫文檔中,經過iframe的形式加載進來。
//模塊器組件
<div :class="['van-doc-simulator', { 'van-doc-simulator-fixed': isFixed }]">
<iframe ref="iframe" :src="src" :style="simulatorStyle" frameborder="0" />
</div>
複製代碼
"src"是外部傳入的一個單獨的頁面地址,因此webpack構建的時候要對示例演示部分進行單獨打包。webpack多入口文件配置,以下
entry: {
'site-desktop': [path_1.join(__dirname, '../../site/desktop/main.js')],
'site-mobile': [path_1.join(__dirname, '../../site/mobile/main.js')],
},
output: {
chunkFilename: '[name].js',
},
plugins: [
new html_webpack_plugin_1.default({
title,
logo: siteConfig.logo,
description: siteConfig.description,
chunks: ['chunks', 'site-desktop'],
template: path_1.join(__dirname, '../../site/desktop/index.html'),
filename: 'index.html',
baiduAnalytics,
}),
new html_webpack_plugin_1.default({
title,
logo: siteConfig.logo,
description: siteConfig.description,
chunks: ['chunks', 'site-mobile'],
template: path_1.join(__dirname, '../../site/mobile/index.html'),
filename: 'mobile.html',
baiduAnalytics,
}),
],
複製代碼
由於在上文VantCliSitePlugin插件中genSiteDesktopShared方法生成了引入了全部的"組件示例.vue"入口文件,經過遍歷組件示例動態配置路由實如今模擬器切換到不一樣路由展現組件示例。
import { demos, config } from 'site-mobile-shared';
names.forEach(name => {
const component = decamelize(name);
...
routes.push({
name,
path: `/${component}`,
component: demos[name],
meta: {
name,
},
});
});
複製代碼
編譯組件庫,供不一樣模塊加載方式使用
主要分析下es模塊的編輯結果
在生產環境構建時,會依次執行如下任務
const tasks = [
{
text: 'Build ESModule Outputs',
task: buildEs,
},
{
text: 'Build Commonjs Outputs',
task: buildLib,
},
{
text: 'Build Style Entry',
task: buildStyleEntry,
},
{
text: 'Build Package Entry',
task: buildPacakgeEntry,
},
{
text: 'Build Packed Outputs',
task: buildPackages,
},
];
async function buildEs() {
common_1.setModuleEnv('esmodule');
//講編寫的src下目錄編寫的組件,copy到es文件夾下
await fs_extra_1.copy(constant_1.SRC_DIR, constant_1.ES_DIR);
await compileDir(constant_1.ES_DIR);
}
async function compileDir(dir) {
const files = fs_extra_1.readdirSync(dir);
await Promise.all(files.map(filename => {
const filePath = path_1.join(dir, filename);
//刪除Demo和Test文件夾
if (common_1.isDemoDir(filePath) || common_1.isTestDir(filePath)) {
return fs_extra_1.remove(filePath);
}
//遞歸處理子文件夾
if (common_1.isDir(filePath)) {
return compileDir(filePath);
}
//編譯不一樣文件
return compileFile(filePath);
}));
}
async function compileFile(filePath) {
//編譯vue文件:將vue文件處理成js文件,template=>render
if (common_1.isSfc(filePath)) {
return compile_sfc_1.compileSfc(filePath);
}
//編譯js文件
if (common_1.isScript(filePath)) {
return compile_js_1.compileJs(filePath);
}
//編譯css|less|scss文件
if (common_1.isStyle(filePath)) {
return compile_style_1.compileStyle(filePath);
}
//刪除多餘文件:如md
return fs_extra_1.remove(filePath);
}
複製代碼
編譯後的組件以下
button
├─ index.js # 組件編譯後的 JS 文件
├─ index.css # 組件編譯後的 CSS 文件
├─ index.less # 組件編譯前的 CSS 文件,能夠爲 less 或 scss
└─ style # 按需引入樣式的入口
├─ index.js # 按需引入編譯後的樣式
└─ less.js # 按需引入未編譯的樣式,可用於主題定製
複製代碼
全部的組件都編譯完後,還缺乏一個主入口文件,導出全部組件
async function buildPacakgeEntry() {
...
const esEntryFile = path_1.join(constant_1.ES_DIR, 'index.js');
生成主入口文件,導出全部組件
gen_package_entry_1.genPackageEntry({
outputPath: esEntryFile,
pathResolver: (path) => `./${path_1.relative(constant_1.SRC_DIR, path)}`,
});
...
}
複製代碼
function genPackageEntry(options) {
const names = common_1.getComponents();
const version = process.env.PACKAGE_VERSION || constant_1.getPackageJson().version;
const components = names.map(common_1.pascalize);
//主入口文件code
const content = `${genImports(names, options)}
const version = '${version}';
function install(Vue) {
//全局註冊全部組件
components.forEach(item => {
if (item.install) {
Vue.use(item);
} else if (item.name) {
Vue.component(item.name, item);
}
});
}
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export {
install,
version,
${genExports(components)} //導出全部組件
};
export default {
install,
version
}`
common_1.smartOutputFile(options.outputPath, content);
}
複製代碼
最後構建完成是的目錄結構
project
├─ es # es 目錄下的代碼遵循 esmodule 規範
├─ button # button 組件編譯後的代碼目錄
├─ dialog # dialog 組件編譯後的代碼目錄
└─ index.js # 引入全部組件的入口,支持 tree shaking
複製代碼
統一的組件的管理入口,包含組件、文檔說明、示例,針對不用環境構建用於不一樣目的的文件,構建過程封閉,咱們只用關心符合目錄結構的組件、文檔、示例編寫,方便維護和擴展。
感謝vant團隊的開源,不只減小重複造輪子的時間,也讓咱們學會了更加寶貴的代碼編程思想和規範。最後,謝謝你們的觀看。