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

李永寧發表於2022-02-14

文章已收錄到 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 版本,比如我的版本是:less@3.7.1、less-loader@7.3.0

    • /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。

相關文章