Element-UI 源码简析——源码调试篇

·  阅读 511

26d8116a7588ac4a91a8398280e436fe.png

序言

ElementUI 作为当前运用的最广的 Vue PC 端组件库,很多 Vue 组件库的架构都是参照 ElementUI 做的。作为一个有梦想的前端(咸鱼),当然需要好好学习一番这套比较成熟的架构。

同时Element是一款Vue的UI框架,它可以将CSS,JavaScript,Vue知识糅合在一起。

本篇文章简单的分析一下Element安装 和 脚本命令!

(1) Element-ui 源码地址

element-ui 2.0 版本地址:github.com/ElemeFE/ele…

(2) Element-ui 脚本命令解析

对于项目来说,除了对整体结构的浏览,第一步要做的事,就是查看项目的核心文件,package.json。

如果说package.json是学习框架的第一部分,那么其中scripts对象则是重中之重.script里边指定了项目的生命周期个各个环节需要执行的命令,体现了项目的开发、测试、打包、部署等架构关系

从下面的代码块可以看出,scripts 下很多内容,我们需要关心的主要有两个,一个dist(项目构建,生成打包文件,类似vue-cli 的 npm run build),一个dev(同 vue-cli 的 npm run dev)

"scripts": {
    "bootstrap": "yarn || npm i",
    // 下载依赖
    "build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",
    // 编译icon文件,编译源码入口文件,编译i18n文件,编译版本信息文件
    "build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",
    // SCSS生成CSS并创建入口文件
    "build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",
    // 编译工具文件
    "build:umd": "node build/bin/build-locale.js",
    // 编译umd风格国际化文件
    "clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",
    // 清除生成的文件
    "deploy:extension": "cross-env NODE_ENV=production webpack --config build/webpack.extension.js",
    // 部署并编译文件
    "dev:extension": "rimraf examples/extension/dist && cross-env NODE_ENV=development webpack --watch --config build/webpack.extension.js",
    "dev": "npm run bootstrap && npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
    //  官网开发模式
    "dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js",
    // 组件开发模式
    "dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme",
    // 打包生成最终文件
    "i18n": "node build/bin/i18n.js",
    // 生成 examples/pages下国际化相关vue文件
    "lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",
    // 检查以下文件目录下的文件是否符合语法规则
    "pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js",
    //  发布版本
    "test": "npm run lint && npm run build:theme && cross-env CI_ENV=/dev/ BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
    // CI环境的单元测试,会启动浏览器
    "test:watch": "npm run build:theme && cross-env BABEL_ENV=test karma start test/unit/karma.conf.js"
    // 仅单元测试
  },
复制代码

首先咱们先看dist

       "dist": "
       npm run clean && 
       npm run build:file && 
       npm run lint && 
       webpack --config build/webpack.conf.js && 
       webpack --config build/webpack.common.js && 
       webpack --config build/webpack.component.js && 
       npm run build:utils && 
       npm run build:umd && 
       npm run build:theme"
复制代码

这里发现element在 dist 这一步做了好多事情呀 有很多的脚本 这里我们大致的分析一下

1.npm run clean : 执行clean命令 会删除掉之前打包的文件

   "clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage"
复制代码

2.npm run build:file :build后会执行以下四个脚本

    node build/bin/iconInit.js &  //生成 examples/icon.json
    node build/bin/build-entry.js & //生成 src/index.js
    node build/bin/i18n.js &  //生成 examples/pages下国际化相关vue文件
    node build/bin/version.js //生成 examples/versions.json
复制代码
node build/bin/iconInit.js

通过 postcss 解析 icon.scss ,筛选出类名并最终导出到 icon.json 文件

var postcss = require('postcss');
var fs = require('fs'); // Nodejs内置的fs模块用于文件系统模块,负责读写文件
var path = require('path'); // Nodejs内置的path模块用于处理文件路径
var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8');
//同步的读取到icon.scss这个文件
var nodes = postcss.parse(fontFile).nodes;
//解析源 css 并返回一个新的根Document节点,其中包含源 CSS 节点。
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]);
  }
});
//通过postcss解析后样式表后,通过正则匹配el-icon-(x):before中的内容,并存入数组中
classList.reverse(); // 希望按 css 文件顺序倒序排列

fs.writeFile(path.resolve(__dirname, '../../examples/icon.json'), JSON.stringify(classList), () => {});
// 把转换为json后的样式表 放到了examples/icon.json下
// icon.json文件格式如下
// ["info","error","success","warning",......]
复制代码
node build/bin/i18n.js

生成 examples/pages下国际化相关vue文件

var fs = require('fs');
var path = require('path');
var langConfig = require('../../examples/i18n/page.json'); 
//以 i18n/page.json 作为数据,再以 pages/templates 作为模版来生成 pages 目录下的多语言版本

langConfig.forEach(lang => {
  try {
  // 判断当前文件是否存在
    fs.statSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
  } catch (e) {
  // 创建新的文件
    fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${ lang.lang }`));
  }

 Object.keys(lang.pages).forEach(page => {
    var templatePath = path.resolve(__dirname, `../../examples/pages/template/${ page }.tpl`);
    // 对应国际化版本的vue文件
    var outputPath = path.resolve(__dirname, `../../examples/pages/${ lang.lang }/${ page }.vue`);
    // 读取
    var content = fs.readFileSync(templatePath, 'utf8');
    // 获取内容对象
    var pairs = lang.pages[page];

    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

记录 Element 版本号到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', '2.4.11': '2.4', '2.5.4': '2.5', '2.6.3': '2.6', '2.7': '2.7.2' };
// 写入文件
if (!content[version]) content[version] = '2.8';
// 写入文件
fs.writeFileSync(path.resolve(__dirname, '../../examples/versions.json'), JSON.stringify(content));

写入完后 会再 examples/components/header里面渲染出来
复制代码
node build/bin/build-entry.js

构建 src/index.js 这个文件可能随着组件的增加删除会经常变动,故用脚本来产生

var Components = require('../../components.json');// 所有可用组件的映射表 组件
var fs = require('fs'); 
var render = require('json-templater/string');// z字符串模板生成工具
var uppercamelcase = require('uppercamelcase'); // 转驼峰 a-bc =>ABc
var path = require('path'); 
var endOfLine = require('os').EOL; 

var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';
var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */

{{include}}
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';

const components = [
{{install}},
  CollapseTransition
];

const install = function(Vue, opts = {}) {
  locale.use(opts.locale);
  locale.i18n(opts.i18n);

  components.forEach(component => {
    Vue.component(component.name, component);
  });

  Vue.use(Loading.directive);

  Vue.prototype.$ELEMENT = {
    size: opts.size || '',
    zIndex: opts.zIndex || 2000
  };

  Vue.prototype.$loading = Loading.service;
  Vue.prototype.$msgbox = MessageBox;
  Vue.prototype.$alert = MessageBox.alert;
  Vue.prototype.$confirm = MessageBox.confirm;
  Vue.prototype.$prompt = MessageBox.prompt;
  Vue.prototype.$notify = Notification;
  Vue.prototype.$message = Message;

};

/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  version: '{{version}}',
  locale: locale.use,
  i18n: locale.i18n,
  install,
  CollapseTransition,
  Loading,
{{list}}
};
`;

delete Components.font;

var ComponentNames = Object.keys(Components);

var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];

ComponentNames.forEach(name => {
  var componentName = uppercamelcase(name);

  includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
    name: componentName,
    package: name
  }));

  if (['Loading', 'MessageBox', 'Notification', 'Message'].indexOf(componentName) === -1) {
    installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
      name: componentName,
      component: name
    }));
  }

  if (componentName !== 'Loading') listTemplate.push(`  ${componentName}`);
});

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);
console.log('[build entry] DONE:', OUTPUT_PATH);
复制代码

3.npm run lint :执行脚本中的lint命令,eslint检查

  "lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",
复制代码

4.webpack

webpack --config build/webpack.conf.js :
        webpack打包,lib目录下打包 index.js文件 (浏览器使用的js包).
webpack --config build/webpack.common.js :
        webpack打包,lib目录下生成element-ui.common.js 文件)
webpack --config build/webpack.component.js :
        webpack打包,lib下生成组件的 js 文件
复制代码

7.npm run build:utils :执行 build:utils 命令 ,babel 打包src目录下文件至lib,忽略index.js

 "build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js"
复制代码

8.npm run build:umd :执行 build:umd 命令 ,lib/locale下生成国际化相关文件

"build:umd": "node build/bin/build-locale.js"
复制代码

9.npm run build:theme:执行 build:theme 命令 ,生成样式

 "build:theme": "
  node build/bin/gen-cssfile && 
  gulp build --gulpfile packages/theme-chalk/gulpfile.js && 
  cp-cli packages/theme-chalk/lib lib/theme-chalk"
复制代码

首先咱们先看div

  "dev": "
  npm run bootstrap && 
  npm run build:file && 
  cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js &
  node build/bin/template.js",
  
复制代码
1.npm run bootstrap : 安装依赖包 同理 "yarn || npm i"
2.npm run build:file : 和上面的dist的一样 4个步骤
3.node build/bin/template.js 监听template下文件的变化并且重新生成多语言版本的pages
const path = require('path');
const templates = path.resolve(process.cwd(), './examples/pages/template');
// process.cwd() 运行 node 命令时所在的文件夹的绝对路径,保证了文件在不同的目录下执行时,路径始终不变
// 常用的 __dirname 获得当前执行文件所在目录的完整目录名 类似./
const chokidar = require('chokidar');
// 监听文件变化
let watcher = chokidar.watch([templates]);

watcher.on('ready', function() {
  watcher
    .on('change', function() {
      exec('npm run i18n');
    });
});

function exec(cmd) {
  return require('child_process').execSync(cmd).toString().trim();
                                  子进程      转字符串      去空格
}
复制代码

(3) 启动成功

26d8116a7588ac4a91a8398280e436fe.png

注意事项

这里有一点 要注意了 Element这里抛出了一个http://0.0.0.0:8085 直接访问这个地址是访问不到 
必须访问http://localhost:8085/ 才行! 这个坑我踩了好久才踩掉的😓
复制代码

51ac1202894c104394192ca280d1de85.png

招聘广告

言重式招聘 寻人!!!寻志同道合之人、寻竭忠尽智之人、寻深思远虑之人、寻勤恳至诚之人
浙江大华技术股份有限公司-软研-智慧城市产品研发部招聘高级前端!!!!!
欢迎大家来聊,有意向可发送简历到chen_zhen@dahuatech.com

一个人的价值,应该看他贡献什么,而不应当看他取得什么 ———— 无奖竞猜 
复制代码
分类:
前端
标签:
分类:
前端
标签: