Element package.json 中的 scripts 分析 —— "build:file"

閒不住的李先森發表於2018-10-22

build:file 指令碼的執行目的是生成包括 icon, 入口檔案, i18n 國際化, version 在內的檔案, 內容為:

   node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js
複製程式碼

node build/bin/iconInit.js

執行這個指令碼的作用是:讀取 packages/theme-chalk/src/icon.scss 檔案, 對檔案中的所有類似於 el-icon-close 這樣的圖示類名進行正則匹配,把所有符合正則的圖示類名組成一個圖示陣列,最後把圖示陣列寫入到 example/icon.json

前提知識點

postcss

PostCSS 是一個允許使用 JS 外掛轉換樣式的工具。 PostCSS 接受一個 CSS 檔案並提供了一個 API 來分析、修改它的規則(通過把 CSS 轉換成一個 AST 抽象語法樹的方式。 PostCSS 更多說明, PostCSS API)。

postcss.parse

解析一個 css 檔案, 返回檔案中所包含的 css 節點。

<!-- icon.scss -->

.el-icon-info:before { content: "\e61a"; }

複製程式碼
const postcss = require('postcss')
const fs = require('fs')
const fontFile = fs.readFileSync('./testIcon.scss', 'utf-8')
const nodes = postcss.parse(fontFile).nodes

console.log(nodes)
複製程式碼

列印出來的結果為:

[
    Rule {
		raws: {
			before: '\r\n\r\n',
			between: ' ',
			semicolon: true,
			after: '\r\n'
		},
		type: 'rule',
		nodes:[...],
		parent:
		Root {
			raws: [Object],
			type: 'root',
			nodes: [Circular],
			source: [Object]
		},
		source: {
			start: [Object],
			input: [Object],
			end: [Object]
		},
		selector: '[class^="el-icon-"], [class*=" el-icon-"]'
	}
]

複製程式碼

對上面的前提知識瞭解後,開始看執行的指令碼程式碼邏輯

iconInit.js 邏輯分析


var postcss = require('postcss');
var fs = require('fs');
var path = require('path');
var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8');
var nodes = postcss.parse(fontFile).nodes;
var classList = [];

nodes.forEach((node) => {
  var selector = node.selector || '';
  var reg = new RegExp(/\.el-icon-([^:]+):before/);
  var arr = selector.match(reg);

  if (arr && arr[1]) {
    classList.push(arr[1]);
  }
});

fs.writeFile(path.resolve(__dirname, '../../examples/icon.json'), JSON.stringify(classList), () => {});

複製程式碼

迴圈中的邏輯:

  1. 使用 forEach 遍歷取到的所有 css 檔案節點。
  2. 首先拿到節點的 選擇器 屬性(包含 class 類、 id) selector
  3. 使用正規表示式 /\.el-icon-([^:]+):before/ 匹配所有滿足 Element iocn 命名規則的 selector。 這裡使用 字串的 match 方法。它將返回所有匹配項。
  4. 如果匹配結果為真(match匹配不到結果時返回 null),並且匹配的第一個結果也為真時。 為了避免重複 只將第一個匹配結果放在 classList 陣列中。

最後將 生成的 classList 以字串的形式寫入到 examples/icon.js

node build/bin/build-entry.js

執行這個檔案的目的是自動生成 整個 Elemnet 框架的入口檔案。 這個入口檔案需要暴露 一個 預設物件, 這個物件上包括 install 方法, install 方法中需要在傳入的 Vue 引數上新增 全部的元件、指令、全域性掛載的方法。除了 install 方法外,為了支援單元件的使用, 還需要在這個預設物件上面新增 全部的元件作為該物件的屬性。( 關於 vue 外掛的開發 , 和 Element 入口檔案的分析)。

前提知識

json-templater/String

一種模板語言實現。可以預先寫一個字串模板,在這個字串模板中可以存在 以 {{var}} 包裹的變數, 使用該方法可以將字串中的變數替換為其他值。

    var render = require('json-templater/string');
    let template = `A {{platform}} UI Library `
    render(template, { platform: 'Desktop'});

複製程式碼

uppercamelcase

將給定字串轉換成 駝峰寫法

os.EOL

一個字串常量,定義作業系統相關的行末標誌:

  • \n 在 POSIX 系統上
  • \r\n 在 Windows系統上

依賴檔案分析

components.json

以每一個元件名為物件的 key, 以該元件的所在目錄為 value 組成的物件。

 "pagination": "./packages/pagination/index.js",
  "dialog": "./packages/dialog/index.js",
  "autocomplete": "./packages/autocomplete/index.js",
  "dropdown": "./packages/dropdown/index.js",
  "dropdown-menu": "./packages/dropdown-menu/index.js",
  ......
複製程式碼

build-entry.js

// 元件-元件地址 json 物件
var Components = require('../../components.json');
var fs = require('fs');
// 替換 json 模板中的變數的方法
var render = require('json-templater/string');
// 字串轉換為 駝峰寫法
var uppercamelcase = require('uppercamelcase');
var path = require('path');
// 當前作業系統的 換行符
var endOfLine = require('os').EOL;


// 將字串檔案模板輸出的檔案地址, 這個地址就是專案的入口檔案的地址
var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
// 檔案模板頭部的 import 引用 字串模板
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
// 檔案模板中的 component 的字串模板
var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';
// 整個檔案模板中 主體字串模板 裡面有四個替代變數,分別是 include、 install、version、list
var MAIN_TEMPLATE = `......` 


delete Components.font;

var ComponentNames = Object.keys(Components);
// 替換 include 的變數陣列
var includeComponentTemplate = [];
// 替換 install 的變數陣列
var installTemplate = [];
// 替換 list 的變數陣列
var listTemplate = [];

// 迴圈 component.json 中的 key 組成的物件
ComponentNames.forEach(name => {
    // 元件名 轉換為 駝峰寫法
  var componentName = uppercamelcase(name);
    // include 變數陣列中新增 變數替換後的  IMPORT_TEMPLATE 字串
  includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
    name: componentName,
    package: name
  }));
 // install 變數陣列中新增 變數替換後的  INSTALL_COMPONENT_TEMPLATE 字串 這裡排除 'Loading', 'MessageBox', 'Notification', 'Message' 是因為這幾個元件將會全域性掛載到 Vue 的例項上。
  if (['Loading', 'MessageBox', 'Notification', 'Message'].indexOf(componentName) === -1) {
    installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
      name: componentName,
      component: name
    }));
  }
    // list 的變數陣列 中新增 元件的 key 的駝峰寫法的字串
  if (componentName !== 'Loading') listTemplate.push(`  ${componentName}`);
})

// 最終將 MAIN_TEMPLATE 模板中的變數進行替換,生成最終的 框架入口檔案的 字串模板 
var template = render(MAIN_TEMPLATE, {
  include: includeComponentTemplate.join(endOfLine),
  install: installTemplate.join(',' + endOfLine),
  version: process.env.VERSION || require('../../package.json').version,
  list: listTemplate.join(',' + endOfLine)
});

// 將生成的 字串模板寫入到 框架入口檔案
fs.writeFileSync(OUTPUT_PATH, template);

複製程式碼

關於 MAIN_TEMPLATE 字串模板的寫法分析,可以看 Element UI 專案分析

node build/bin/i18n.js

這個 檔案中是通過迴圈已經配置好的 i18n 形式的資料字典, 對每一個資料字典中語種物件都生成一個 語種目錄,在每一個目錄中, 根據 模板引擎 生成 不同語種對應的模板。 之後 根據資料字典中的 pages 屬性裡面的配置,替換掉模板中的變數。 最後寫入到 example/pages/。 這樣每一個語種都有一套對應的 .vue 程式碼。

依賴檔案

page.json

這個檔案裡面配置了 i18n 的資料字典陣列。

[
     {
    "lang": "zh-CN",
    "pages": {
      "index": { },
      "component": {},
      "changelog": {},
      "design": {},
      "guide": {},
      "nav": {},
      "resource": {}
    }
  }
  ...
]
複製程式碼

i18n.js

var fs = require('fs');
var path = require('path');
var langConfig = require('../../examples/i18n/page.json');

langConfig.forEach(lang => {
    // 在../../examples/pages 資料夾下 讀取 與 `lang.lang` (指 zh-CN、 en-US、 es ) 對應的檔案資訊, 如果丟擲異常,說明該資料夾下沒有改檔案,就新建一個對應 `lang.lang` 的檔案
  try {
    fs.statSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
  } catch (e) {
    fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
  }

// pages 裡面包含 index component changelog design guide nav resource
  Object.keys(lang.pages).forEach(page => {
      // 模板地址
    var templatePath = path.resolve(__dirname, `../../examples/pages/template/${ page }.tpl`);
    // 輸出地址
    var outputPath = path.resolve(__dirname, `../../examples/pages/${ lang.lang }/${ page }.vue`);

    var content = fs.readFileSync(templatePath, 'utf8');
    var pairs = lang.pages[page]; // 就是本次迴圈裡的 page 

    // 將 page 再次遍歷, 並將 與 page 變數相對應的 模板中的 變數 替換成 page 物件中的 value
    Object.keys(pairs).forEach(key => {
      content = content.replace(new RegExp(`<%=\\s*${ key }\\s*>`, 'g'), pairs[key]);
    });
    
    // 最終將 替換後的 模板寫入到 輸入地址
    fs.writeFileSync(outputPath, content);
  });
});
複製程式碼

node build/bin/version.js

這個檔案是根據 命令列引數 process.env.VERSION 或者 package.json 中的 version 的值, 來生成 框架的版本物件,並最終在 examples 下生成 version.json 檔案

var fs = require('fs');
var path = require('path');
var version = process.env.VERSION || require('../../package.json').version;
var content = { '1.4.13': '1.4', '2.0.11': '2.0', '2.1.0': '2.1', '2.2.2': '2.2', '2.3.9': '2.3' };
if (!content[version]) content[version] = '2.4';
fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));
複製程式碼

相關文章