如何快速為團隊打造自己的組件庫(下)—— 基於 element-ui 為團隊打造自己的組件庫

文章已收錄到 github,歡迎 Watch 和 Star。

簡介

在了解 Element 源碼架構 的基礎上,接下來我們基於 element-ui 為團隊打造自己的組件庫。

主題配置

基礎組件庫在 UI 結構上差異很小,一般只是在主題色上會有較大差異,畢竟每個團隊都有了 UI 風格。比如,我們團隊的 UI 設計稿其實是基於 Ant Design 來出的,而組件庫是基於 Element-UI 來開發,即使是這種情況,對組件本身的改動也很少。所以,基於開源庫打造團隊自己的組件庫時,主題配置就很重要了。

element-ui 的一大特色就是支援自定義主題,它通過在線主題編輯器、Chrome 插件或命令行主題工具這三種方式來訂製 element-ui 所有組件的樣式。那麼 element-ui 是怎麼做到這一點的呢?

因為 element-ui 組件樣式中的顏色、字體、線條等樣式都是通過變數的方式引入的,在 packages/theme-chalk/src/common/var.scss 中可以看到這些變數的定義,這就為自定義主題提供了方便,因為我們只需要修改這些變數,就可以實現組件主題的改變。

在線主題編輯器和 Chrome 插件支援實時預覽。並且可以下載訂製的樣式包,然後使用。在線主題編輯器和 Chrome 插件的優點是可視化,簡潔明了,但是有個最大的缺點就是,最後下載出來的是一個將所有組件樣式打包到一起的樣式包,沒辦法支援按需載入,不推薦使用。這裡我們使用命令行主題工具來訂製樣式。

命令行主題工具

  • 初始化項目目錄並安裝主題生成工具(element-theme)

    mkdir theme && cd theme && npm init -y && npm i element-theme -D
    
  • 安裝白堊主題

    npm i element-theme-chalk -D
    
  • 初始化變數文件

    node_modules/.bin/et -i
    

    命令執行以後可能會得到如下報錯資訊

    image-20220213193714430

    原因是 element-theme 包中依賴了低版本的 graceful-fs,低版本 graceful-fs 在高版本的 node.js 中不兼容,最簡單的方案是升級 graceful-fs。

    在項目根目錄下創建 npm-shrinkwrap.json 文件,並添加如下內容:

    {
       "dependencies": {
           "graceful-fs": {
               "version": "4.2.2"
           }
       }
    }
    

    運行 npm install 重新安裝依賴即可解決,然後重新執行 node_modules/.bin/et -i,執行完以後會在當前目錄生成 element-variables.scss 文件。

  • 修改變數

    直接編輯 element-variables.scss 文件,例如修改主題色為紅色,將文件中的 $--color-primary 的值修改為 red$--color-primary: red !default;

    文件中寫了很好的注釋,並且樣式程式碼也是按照組件來分割組織的,所以大家可以對照設計團隊給到的設計稿來一一修改相關的變數。如果實在覺得看程式碼比較懵,可以參照在線主題編輯器,兩邊的變數名是一致的。

    題外話:element-ui 還提供了兩個資源包,供設計團隊使用,所以最理想的是,讓設計團隊根據 element-ui 的資源包出設計稿,這樣兩邊就可以做到統一,研發團隊的工作量也會降低不少。比如我們團隊就不是這樣,設計團隊給到的設計稿是基於 Ant Design 出的,研發組件庫時改動的工作量和難度就會相對比較大。所以研發、設計、產品一定要進行很好的溝通。

  • 編譯主題

    修改完以後,保存文件,然後執行以下命令編譯主題,會產生一個 theme 目錄。生產出來都是 CSS 樣式文件,文件名和組件名一一對應,支援按需引入(指定組件的樣式文件)和全量引入(index.css)。

    • 生產未壓縮的樣式文件

      node_modules/.bin/et --out theme-chalk
      
    • 生產經過壓縮的樣式文件

      node_modules/.bin/et --minimize --out theme-chalk
      
    • 幫助命令

      node_modules/.bin/et --help
      
    • 啟用 watch 模式,實時編譯主題

      node_modules/.bin/et --watch --out theme-chalk
      
  • 使用自定義主題

    • 用新生成的主題目錄(theme-chalk)替換掉框架中的 packages/theme-chalk 目錄。重命名老的 theme-chalk 為 theme-chalk.bak,不要刪掉,後面需要用

      建議將生成主題時用到的 element-variables.scss 文件保存在項目中,因為以後你可能會需要重新生成主題

    • 修改 /examples/entry.js/examples/play.js/examples/extension/src/app.js 中引入的組件庫的樣式

      // 用新的樣式替換舊的默認樣式
      // import 'packages/theme-chalk/src/index.scss
      import 'packages/theme-chalk/index.css
      
    • 修改 /build/bin/iconInit.js 中引入的圖標樣式文件

      // var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8');
      var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/icon.css'), 'utf8');
      
    • 修改 /examples/docs/{四種語言}/custom-theme.md

      // @import "~element-ui/packages/theme-chalk/src/index";
      @import "~element-ui/packages/theme-chalk/index";
      
    • 執行 make dev 啟動開發環境,查看效果

    到這一步,主題配置就結束了,你會發現,element-ui 官網的組件樣式基本上和設計稿上的一致。但是仔細對比後,會發現有一些組件的樣式和設計稿有差異,這時候就需要對這些組件的樣式進行深度訂製,覆寫不一致的樣式。

    其實這塊兒漏掉了 /build/bin/new.js 中涉及的樣式目錄,這塊兒的改動會放到後面

樣式深度訂製

上一步的主題配置,只能解決主題相關的樣式,但是有些組件的有些樣式不屬於主題樣式,如果這部分樣式剛好又和設計稿不一致的話,那就需要重寫這部分樣式去覆蓋上一步的樣式。

以下配置還支援為自定義組件添加樣式

樣式目錄

  • 主題配置 步驟中備份的 /packages/theme-chalk.bak 重命名為 /packages/theme-lyn,作為覆寫組件和自定義組件的樣式目錄

  • 刪掉 /packages/theme-lyn/src 目錄的所有文件

  • 你會寫 scss ?

    • 忽略掉下一步,然後後續步驟你只需將對應的 less 操作換成 sass 即可
  • 你不會寫 scss,擴展其它語法,假設你會寫 less

    • 在項目根目錄執行以下命令,然後刪掉 gulp-sass

      npm i less less-loader gulp-less -D && npm uninstall gulp-sass -D
      

      如果一會兒啟動開發環境以後,報錯 「TypeError: this.getOptions is not a function」,則降級 less-loader 版本,比如我的版本是:[email protected][email protected]

    • /packages/theme-lyn 目錄下執行以下命令,然後刪掉 gulp-sass

      npm i gulp-less -D && npm uninstall gulp-sass -D
      
    • /packages/theme-lyn/gulpfile.js 更改為以下內容

      'use strict';
      
      /**
       *  將 ./src/*.less 文件編譯成 css 文件輸出到 ./lib 目錄
       *  將 ./src/fonts/中的所有字體文件輸出到 ./lib/fonts 中,如果你沒有覆寫字體樣式的需要,則刪掉拷貝字體樣式部分
       */
      const { series, src, dest } = require('gulp');
      const less = require('gulp-less');
      const autoprefixer = require('gulp-autoprefixer');
      const cssmin = require('gulp-cssmin');
      const path = require('path')
      
      function compile() {
        return src('./src/*.less')
          .pipe(less({
            paths: [ path.join(__dirname, './src') ]
          }))
          .pipe(autoprefixer({
            browsers: ['ie > 9', 'last 2 versions'],
            cascade: false
          }))
          .pipe(cssmin())
          .pipe(dest('./lib'));
      }
      
      function copyfont() {
        return src('./src/fonts/**')
          .pipe(cssmin())
          .pipe(dest('./lib/fonts'));
      }
      
      // 也可以在這裡擴展其它功能,比如拷貝靜態資源
      
      exports.build = series(compile, copyfont);
      
      
    • build/webpack.demo.js 中增加解析 less 文件的規則

      {
        test: /\.less$/,
        use: [
          isProd ? MiniCssExtractPlugin.loader : 'style-loader',
          'css-loader',
          'less-loader'
        ]
      }
      
  • 假如你要覆寫 button 組件的部分樣式

    • /packages/theme-lyn/src 目錄下新建 button.less 文件,編寫覆寫樣式時請遵循如下規則

      • 組件樣式的覆寫,最好遵循 BEM 風格,目的是提供良好的命名空間隔離,避免樣式打包以後發生意料之外的覆蓋
      • 只覆寫已有的樣式,可以在組件上新增類名,但不要刪除,目的是兼容線上程式碼
      // 這裡我要把主要按鈕的字型大小改大有些,只是為了演示效果
      .el-button--primary {
        font-size: 24px;
      }
      
  • 改造 build/bin/gen-cssfile.js 腳本

    /**
     * 將各個覆寫的樣式文件在 packages/theme-lyn/src/index.less 文件中自動引入
     */
    
    var fs = require('fs');
    var path = require('path');
    
    // 生成 theme-lyn/src 中的 index.less 文件
    function genIndexLessFile(dir) {
      // 文件列表
      const files = fs.readdirSync(dir);
      /**
       * @import 'x1.less';
       * @import 'x2.less;
       */
      let importStr = "/* Automatically generated by './build/bin/gen-cssfile.js' */\n";
    
      // 需要排除的文件
      const excludeFile = ['assets', 'font', 'index.less', 'base.less', 'variable.less'];
    
      files.forEach(item => {
        if (excludeFile.includes(item) || !/\.less$/.test(item)) return;
    
        // 只處理非 excludeFile 中的 less 文件
        importStr += `@import "./${item}";\n`;
      });
    
      // 在 packages/theme-lyn/src/index.less 文件中寫入 @import "xx.less",即在 index.less 中引入所有的樣式文件
      fs.writeFileSync(path.resolve(dir, 'index.less'), importStr);
    }
    
    genIndexLessFile(path.resolve(__dirname, '../../packages/theme-lyn/src/'));
    
    
  • 在項目根目錄下執行以下命令

    npm i shelljs -D
    
  • 新建 /build/bin/compose-css-file.js

    /**
     * 負責將打包後的兩個 css 目錄(lib/theme-chalk、lib/theme-lyn)合併
     * lib/theme-chalk 目錄下的樣式文件是通過主題配置自動生成的
     * lib/theme-lyn 是擴展組件的樣式(覆寫默認樣式和自定義組件的樣式)
     * 最後將樣式都合併到 lib/theme-chalk 目錄下
     */
    const fs = require('fs');
    const fileSave = require('file-save');
    const { resolve: pathResolve } = require('path');
    const shelljs = require('shelljs');
    
    const themeChalkPath = pathResolve(__dirname, '../../lib/theme-chalk');
    const themeStsUIPath = pathResolve(__dirname, '../../lib/theme-lyn');
    
    // 判斷樣式目錄是否存在
    let themeChalk = null;
    let themeStsUI = null;
    try {
      themeChalk = fs.readdirSync(themeChalkPath);
    } catch (err) {
      console.error('/lib/theme-chalk 不存在');
      process.exit(1);
    }
    try {
      themeStsUI = fs.readdirSync(themeStsUIPath);
    } catch (err) {
      console.error('/lib/theme-lyn 不存在');
      process.exit(1);
    }
    
    /**
     * 遍歷兩個樣式目錄,合併相同文件,將 theme-lyn 的中樣式追加到 theme-chalk 中對應樣式文件的末尾
     * 如果 theme-lyn 中的文件在 theme-chalk 中不存在(比如擴展的新組件),則直接將文件拷貝到 theme-chalk
     */
    const excludeFiles = ['element-variables.css', 'variable.css'];
    for (let i = 0, themeStsUILen = themeStsUI.length; i < themeStsUILen; i++) {
      if (excludeFiles.includes(themeStsUI[i])) continue;
    
      if (themeStsUI[i] === 'fonts') {
        shelljs.cp('-R', pathResolve(themeStsUIPath, 'fonts/*'), pathResolve(themeChalkPath, 'fonts'));
        continue;
      }
    
      if (themeStsUI[i] === 'assets') {
        shelljs.cp('-R', pathResolve(themeStsUIPath, 'assets'), themeChalkPath);
        continue;
      }
    
      if (themeChalk.includes(themeStsUI[i])) {
        // 說明當前樣式文件是覆寫 element-ui 中的樣式
        const oldFileContent = fs.readFileSync(pathResolve(themeChalkPath, themeStsUI[i]), { encoding: 'utf-8' });
        fileSave(pathResolve(themeChalkPath, themeStsUI[i])).write(oldFileContent).write(fs.readFileSync(pathResolve(themeStsUIPath, themeStsUI[i])), 'utf-8').end();
      } else {
        // 說明當前樣式文件是擴展的新組件的樣式文件
        // fs.writeFileSync(pathResolve(themeChalkPath, themeStsUI[i]), fs.readFileSync(pathResolve(themeStsUIPath, themeStsUI[i])));
        shelljs.cp(pathResolve(themeStsUIPath, themeStsUI[i]), themeChalkPath);
      }
    }
    
    // 刪除 lib/theme-lyn
    shelljs.rm('-rf', themeStsUIPath);
    
    
  • 改造 package.json 中的 scripts

    {
      "gen-cssfile:comment": "在 /packages/theme-lyn/src/index.less 中自動引入各個組件的覆寫樣式文件",
      "gen-cssfile": "node build/bin/gen-cssfile",
      "build:theme:comment": "構建主題樣式:在 index.less 中自動引入各個組件的覆寫樣式文件 && 通過 gulp 將 less 文件編譯成 css 並輸出到 lib 目錄 && 拷貝基礎樣式 theme-chalk 到 lib/theme-chalk && 拷貝 編譯後的 theme-lyn/lib/* 目錄到 lib/theme-lyn && 合併 theme-chalk 和 theme-lyn",
      "build:theme": "npm run gen-cssfile && gulp build --gulpfile packages/theme-lyn/gulpfile.js && cp-cli packages/theme-lyn/lib lib/theme-lyn && cp-cli packages/theme-chalk lib/theme-chalk && node build/bin/compose-css-file.js",
    }
    
  • 執行以下命令

    npm run gen-cssfile
    
  • 改造 /examples/entry.js/examples/play.js

    // 用新的樣式替換舊的默認樣式
    // import 'packages/theme-chalk/src/index.scss
    import 'packages/theme-chalk/index.css	// 在這行下面引入自定義樣式
    // 引入自定義樣式
    import 'packages/theme-lyn/src/index.less'
    
  • 訪問官網,查看 button 組件的覆寫樣式是否生效

自定義組件

組件庫在後續的開發和迭代中,需要兩種自定義組件的方式:

  • 增加新的 element-ui 組件

    element-ui 官網可能在某個時間點增加一個你需要的基礎組件,這時你需要將其集成進來

  • 增加業務組件

    基礎組件就緒以後,團隊就會開始推動業務組件的建設,這時候就會向組件庫中增加新的組件

新的 element-ui 組件

element-ui 提供了增加新組件的腳本,執行 make new <component-name> [中文名] 即可生成新組件所需的所有文件以及配置,比如:make new button 按鈕,有了該腳本可以讓你專註於組件的編寫,不需要管任何配置。

/build/bin/new.js

但是由於我們調整了框架主題庫的結構,所以腳本文件也需要做相應的調整。需要將 /build/bin/new.js 文件中處理樣式的程式碼刪掉,樣式文件不再需要腳本自動生成,而是通過重新生成主題的方式實現。

// /build/bin/new.js 刪掉以下程式碼
{
    filename: path.join('../../packages/theme-chalk/src', `${componentname}.scss`),
    content: `@import "mixins/mixins";
@import "common/var";

@include b(${componentname}) {
}`
},
  
// 添加到 index.scss
const sassPath = path.join(__dirname, '../../packages/theme-chalk/src/index.scss');
const sassImportText = `${fs.readFileSync(sassPath)}@import "./${componentname}.scss";`;
fileSave(sassPath)
  .write(sassImportText, 'utf8')
  .end('\n');

Makefile

改造 Makefile 文件,在 new 配置後面增加 && npm run build:file 命令,重新生成組件庫入口文件,不然不會引入新增加的組件。

new:
    node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS)) && npm run build:file

增加新組件

完成上述改動以後,只需兩步即可完成新 element-ui 組件的創建:

  • 執行 make new <component-name> [組件中文名] 命令新建新的 element-ui 組件

    這一步會生成眾多文件,你只需要從新的 element-ui 源碼中將該組件對應的程式碼複製過來填充到對應的文件即可

  • 重新生成主題,然後覆蓋現在的 /packages/theme-chalk

業務組件

新增的業務組件就不要以 el 開頭了,避免和 element 組件重名或造成誤會。需要模擬 /build/bin/new.js 腳本寫一個新建業務組件的腳本 /build/bin/new-lyn-ui.js,大家可以基於該腳本去擴展。

/build/bin/new-lyn-ui.js

'use strict';

/**
 * 新建組件腳本,以 lyn-city 組件為例
 * 1、在 packages 目錄下新建組件目錄,並完成目錄結構的基本創建
 * 2、創建組件文檔
 * 3、組件單元測試文件
 * 4、組件樣式文件
 * 5、組件類型聲明文件
 * 6、並將上述新建的相關資源自動添加的相應的文件,比如組件組件註冊到 components.json 文件、樣式文件在 index.less 中自動引入等
 * 總之你只需要專註於編寫你的組件程式碼即可,其它一概不用管
 */

console.log();
process.on('exit', () => {
  console.log();
});

if (!process.argv[2]) {
  console.error('[組件名]必填 - Please enter new component name');
  process.exit(1);
}

const path = require('path');
const fs = require('fs');
const fileSave = require('file-save');
const uppercamelcase = require('uppercamelcase');
// 組件名稱 city
const componentname = process.argv[2];
// 組件中文名 城市列表
const chineseName = process.argv[3] || componentname;
// 組件大駝峰命名 City
const ComponentName = uppercamelcase(componentname);
// 組件路徑:/packages/city
const PackagePath = path.resolve(__dirname, '../../packages', componentname);
const Files = [
  // packages/city/index.js 的內容
  {
    filename: 'index.js',
    content: `import ${ComponentName} from './src/main';

/* istanbul ignore next */
${ComponentName}.install = function(Vue) {
  Vue.component(${ComponentName}.name, ${ComponentName});
};

export default ${ComponentName};`
  },
  // packages/city/src/main.vue 組件定義
  {
    filename: 'src/main.vue',
    content: `<template>
  <div class="lyn-${componentname}"></div>
</template>

<script>
export default {
  name: 'Lyn${ComponentName}'
};
</script>`
  },
  // 組件中文文檔
  {
    filename: path.join('../../examples/docs/zh-CN', `${componentname}.md`),
    content: `## ${ComponentName} ${chineseName}`
  },
  // 組件單元測試文件
  {
    filename: path.join('../../test/unit/specs', `${componentname}.spec.js`),
    content: `import { createTest, destroyVM } from '../util';
import ${ComponentName} from 'packages/${componentname}';

describe('${ComponentName}', () => {
  let vm;
  afterEach(() => {
    destroyVM(vm);
  });

  it('create', () => {
    vm = createTest(${ComponentName}, true);
    expect(vm.$el).to.exist;
  });
});
`
  },
  // 組件樣式文件
  {
    filename: path.join(
      '../../packages/theme-lyn/src',
      `${componentname}.less`
    ),
    content: `@import "./base.less";\n\n.lyn-${componentname} {
}`
  },
  // 組件類型聲明文件
  {
    filename: path.join('../../types', `${componentname}.d.ts`),
    content: `import { LynUIComponent } from './component'

/** ${ComponentName} Component */
export declare class Lyn${ComponentName} extends LynUIComponent {
}`
  }
];

// 將新組件添加到 components.json
const componentsFile = require('../../components.json');
if (componentsFile[componentname]) {
  console.error(`${componentname} 已存在.`);
  process.exit(1);
}
componentsFile[componentname] = `./packages/${componentname}/index.js`;
fileSave(path.join(__dirname, '../../components.json'))
  .write(JSON.stringify(componentsFile, null, '  '), 'utf8')
  .end('\n');

// 在 index.less 中引入新組件的樣式文件
const lessPath = path.join(
  __dirname,
  '../../packages/theme-lyn/src/index.less'
);
const lessImportText = `${fs.readFileSync(
  lessPath
)}@import "./${componentname}.less";`;
fileSave(lessPath).write(lessImportText, 'utf8').end('\n');

// 添加到 element-ui.d.ts
const elementTsPath = path.join(__dirname, '../../types/element-ui.d.ts');

let elementTsText = `${fs.readFileSync(elementTsPath)}
/** ${ComponentName} Component */
export class ${ComponentName} extends Lyn${ComponentName} {}`;

const index = elementTsText.indexOf('export') - 1;
const importString = `import { Lyn${ComponentName} } from './${componentname}'`;

elementTsText =
  elementTsText.slice(0, index) +
  importString +
  '\n' +
  elementTsText.slice(index);

fileSave(elementTsPath).write(elementTsText, 'utf8').end('\n');

// 新建剛才聲明的所有文件
Files.forEach(file => {
  fileSave(path.join(PackagePath, file.filename))
    .write(file.content, 'utf8')
    .end('\n');
});

// 將新組建添加到 nav.config.json
const navConfigFile = require('../../examples/nav.config.json');

Object.keys(navConfigFile).forEach(lang => {
  const groups = navConfigFile[lang].find(item => Array.isArray(item.groups))
    .groups;
  groups[groups.length - 1].list.push({
    path: `/${componentname}`,
    title:
      lang === 'zh-CN' && componentname !== chineseName
        ? `${ComponentName} ${chineseName}`
        : ComponentName
  });
});

fileSave(path.join(__dirname, '../../examples/nav.config.json'))
  .write(JSON.stringify(navConfigFile, null, '  '), 'utf8')
  .end('\n');

console.log('DONE!');

Makefile

Makefile 中增加如下配置:

new-lyn-ui:
    node build/bin/new-lyn-ui.js $(filter-out $@,$(MAKECMDGOALS)) && npm run build:file
	
help:
    @echo "   \033[35mmake new-lyn-ui <component-name> [中文名]\033[0m\t---  創建新的 LynUI 組件 package. 例如 'make new-lyn-ui city 城市選擇'"

icon 圖標

element-ui 雖然提供了大量的 icon,但往往不能滿足團隊的業務需求,所有就需要往組件庫中增加業務 icon,這裡以 Iconfont 為例。不建議直接使用設計給的圖片或者 svg,太佔資源了。

  • 打開 iconfont

  • 登陸 -> 資源管理 -> 我的項目 -> 新建項目

    注意,這裡為 icon 設置前綴時不要使用 el-icon-,避免和 element-ui 中的 icon 重複。這個項目就作為團隊項目使用了,以後團隊所有的業務圖標都上傳到該項目,所以最好註冊一個團隊帳號。

  • 新建成功後,點擊 上傳圖標至項目 ,選擇 上傳圖標 ,上傳設計給的 svg(必須是 svg),根據需要選擇 保留顏色或不保留並提交

  • 上傳完畢,編輯、檢查沒問題後,點擊 下載至本地

  • 複製其中的 iconfont.ttficonfont.woff/packages/theme-lyn/src/fonts 目錄下

  • 新建 /packages/theme-lyn/src/icon.less 文件,並添加如下內容

    @font-face {
      font-family: 'iconfont';
      src: url('./fonts/iconfont.woff') format('woff'), url('./fonts/iconfont.ttf') format('truetype');
      font-weight: normal;
      font-display: auto;
      font-style: normal;
    }
    
    [class^="lyn-icon-"], [class*=" lyn-icon-"] {
      font-family: 'iconfont' !important;
      font-style: normal;
      font-weight: normal;
      font-variant: normal;
      text-transform: none;
      line-height: 1;
      vertical-align: baseline;
      display: inline-block;
    
      /* Better Font Rendering =========== */
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale
    }
    
    /**
     * 示例:
     * .lyn-icon-iconName:before {
     *   content: "\unicode 16進位碼" 
     * }
     * .lyn-icon-add:before {
     *   content: "\e606"
     * }
     */
    
  • 執行 npm run gen-cssfile

  • 更新 /build/bin/iconInit.js 文件為以下內容

    'use strict';
    
    var postcss = require('postcss');
    var fs = require('fs');
    var path = require('path');
    
    /**
     * 從指定的 icon 樣式文件(entry)中按照給定正則表達式(regExp)解析出 icon 名稱,然後輸出到指定位置(output)
     * @param {*} entry 被解析的文件相對於當前文件的路徑,比如:../../packages/theme-chalk/icon.css
     * @param {*} regExp 被解析的正則表達式,比如:/\.el-icon-([^:]+):before/
     * @param {*} output 解析後的資源輸出到相對於當前文件的指定位置,比如:../../examples/icon.json
     */
    function parseIconName(entry, regExp, output) {
      // 讀取樣式文件
      var fontFile = fs.readFileSync(path.resolve(__dirname, entry), 'utf8');
      // 將樣式內容解析為樣式節點
      var nodes = postcss.parse(fontFile).nodes;
      var classList = [];
    
      // 遍歷樣式節點
      nodes.forEach((node) => {
        // 從樣式選擇器中根據給定匹配規則匹配出 icon 名稱
        var selector = node.selector || '';
        var reg = new RegExp(regExp);
        var arr = selector.match(reg);
    
        // 將匹配到的 icon 名稱放入 classList
        if (arr && arr[1]) {
          classList.push(arr[1]);
        }
      });
    
      classList.reverse(); // 希望按 css 文件順序倒序排列
    
      // 將 icon 名稱數組輸出到指定 json 文件中
      fs.writeFile(path.resolve(__dirname, output), JSON.stringify(classList), () => { });
    }
    
    // 根據 icon.css 文件生成所有的 icon 圖標名
    parseIconName('../../packages/theme-chalk/icon.css', /\.el-icon-([^:]+):before/, '../../examples/icon.json')
    
    // 根據 icon.less 文件生成所有的 sts icon 圖標名
    parseIconName('../../packages/theme-lyn/src/icon.less', /\.lyn-icon-([^:]+):before/, '../../examples/lyn-icon.json')
    
    
  • 執行 npm run build:file,會看到在 /examples 目錄下生成了一個 lyn-icon.json 文件

  • /examples/entry.js 中增加如下內容

    import lynIcon from './lyn-icon.json';
    Vue.prototype.$lynIcon = lynIcon; // StsIcon 列表頁用
    
  • /examples/nav.config.json 中業務配置部分增加 lyn-icon 路由配置

    {
      "groupName": "LynUI",
      "list": [
        {
          "path": "/lyn-icon",
          "title": "icon 圖標"
        }
      ]
    }
    
  • 增加文檔 /examples/docs/{語言}/lyn-icon.md,添加如下內容

  • 查看官網 看圖標是否生效

  • 後續如需擴展新的 icon

    • 在前面新建的 iconfont 項目中上傳新的圖標,然後點擊 下載至本地,將其中的 iconfont.ttficonfont.woff 複製 /packages/theme-lyn/src/fonts 目錄下即可(替換已有的文件)

    • /packages/theme-lyn/src/icon.less 中設置新的 icon 樣式聲明

    • 執行 npm run build:file

    • 查看官網 看圖標添加是否成功

升級 Vue 版本

element-ui 本身依賴的是 vue@^2.5.x,該版本的 vue 不支援最新的 v-slot 插槽語法(v-slot 是在 2.6.0 中新增的),組件的 markdown 文檔中使用 v-slot 語法不生效且會報錯,所以需要升級 vue 版本。涉及三個包:vue@2.6.12、@vue/component-compiler-utils@3.2.0、vue-template-compiler@^2.6.12。執行以下命令即可完成更新:

  • 刪除舊包

    npm uninstall vue @vue/component-compiler-utils vue-template-compiler -D
    
  • 安裝新包

    npm install vue@^2.6.12 @vue/component-compiler-utils@^3.2.0 vue-template-compiler@^2.6.12 -D
    
  • 更新 package.json 中的 peerDependencies

    {
      "peerDependencies": {
        "vue": "^2.6.12"
      }
    }
    

擴展

到這裡,組件庫的架構調整其實已經完成了,接下來只需組織團隊成員對照設計稿進行組件開發就可以了。但是對於一些有潔癖的開發者來說,其實還差點。

比如:

  • 團隊的組件庫不想叫 element-ui,有自己的名稱,甚至整個組件庫的程式碼都不想出現 element 字樣

  • element-ui 的某些功能團隊不需要,比如:官網項目(examples)中的主題、資源模組、chrome 插件(extension)、國際化相關(只保留中文即可)

  • 靜態資源,element-ui 將所有的靜態資源都上傳到自己的 CDN 上了,我們去訪問其實優點慢,可以將相關資源挪到團隊自己的 CDN 上

  • 工程程式碼品質問題,element-ui 本身提供了 eslint,做了一點程式碼品質的控制,但是做的不夠,比如格式限制、自動格式化等,可以參考 搭建自己的 typescript 項目 + 開發自己的腳手架工具 ts-cli 中的 程式碼品質 部分去配置

  • 替換官網 logo、渲染資訊等

  • element-ui 樣式庫的優化,其實 element-ui 的樣式存在重複載入的問題

    雖然它通過 webpack 打包已經解決了一部分問題,但是某些情況還是會出現重複載入,比如 table 組件中使用 checkbox 組件,就會載入兩次 checkbox 組件的樣式程式碼。有精力的同學可以去研究研究

  • 你的業務只需要 element-ui 的部分基礎組件,把不需要的刪掉,可以降低組件庫的體積,提升載入速度

這些工作有一些是對官網項目(examples)的裁剪,有一些是項目整體優化,還有一些是潔癖,不過,相信凡是進行到這一步的同學,都已經為團隊構建出了自己的組件庫,解決以上列出的那些問題完全不再話下,這裡就不一一列出方法了。

鏈接

  • Element 源碼架構 思維導圖版
  • 組件庫專欄
    • 如何快速為團隊打造自己的組件庫(上)—— Element 源碼架構
    • Element 源碼架構 影片版,關注微信公眾號,回復: “Element 源碼架構影片版” 獲取

文章已收錄到 github,歡迎 Watch 和 Star。

Tags: