Element UI框架项目结构解析

前言

因为对于工程化相关的了解涉猎很少,今后也是打算对webpack等工程化构建加深下了解。

这篇文章就是详细分析下Element UI项目组成以及相关的打包步骤等,旨在加深对工程化构建相关的知识点了解。

Element UI项目结构

|-- Element
​ |-- .babelrc // babel配置文件
​ |-- .eslintignore // eslint校验忽略文件
​ |-- .eslintrc // eslint配置文件
​ |-- .gitattributes // git属性配置文件
​ |-- .gitignore // git忽略追踪管理的文件
​ |-- .travis.yml // 持续集成配置文件
​ |-- CHANGELOG.en-US.md // 版本改变点log文件-英文版
​ |-- CHANGELOG.es.md
​ |-- CHANGELOG.zh-CN.md // 版本改变点log文件-中文版
​ |-- FAQ.md // 常见问题说明文档
​ |-- LICENSE // 开源协议
​ |-- Makefile // 自动化编译配置文件
​ |-- README.md // 项目说明文件
​ |-- components.json // 组件列表(所有组件)文件
​ |-- element_logo.svg // logo svg文件
​ |-- package.json
​ |-- postcss.config.js // postcss配置文件
​ |-- yarn.lock // yarn配置文件
​ |-- build // webpack编译配置文件目录
​ |-- examples // element ui官方主页项目目录
​ |-- lib // 打包后的文件目录
​ |-- packages // 组件源码目录
​ |-- src // 项目使用到的公共指令、工具集等源码存放目录
​ |-- test // 单元测试相关
​ |-- types // typescript相关文件包

上面结构是Element框架项目最顶级的目录结构,一些目录下的子文件会在下面梳理过程中提及的。

首先就上面的目录结构所涉及到的不常见的几个点做下解释:

  • .gitattributes

    git属性配置文件可以自定义Git,element框架项目这里该文件的配置就一条

    test/**/*.js linguist-language=Vue
    

    配置element框架文件的使用语言,具体其他的配置你可以阅读自定义Git

  • Makefile

    Linux环境下的GUN make工具,make是一个命令工具,它解释了Makefile配置的规则,可以用来实现自动化编译,在前端项目中常用于命令别名,例如

    // Makefile

    build:

    ​ npm run buid

    你可以使用:make buid来执行npm run build命令了,其他高级使用请参考Makefile

  • postcss

    使用JS插件转换CSS的工具,常使用Autoprefixer插件,提及此就涉及到CSS in JS和CSS Module相关思想,具体了解可以参考阮一峰相关文章

弄清楚每个目录作用以及相关概念之后,就主要看package.json和Makefile这两个文件了,这两个文件中是涉及到打包等相关命令的配置。

package.json相关分析

Element框架中package.json文件的配置有几处与入口相关的需要关注的,如下:

  • main:指定了程序的主入口文件
  • files:发布需要包含的文件内容
  • typings:针对TypeScript的主入口
  • scripts:定义命令集合
  • unpkg:使用cdn相关的配置
  • style:对导入CSS包非常有用(style的详细说明

这里主要看scripts中定义的相关命令,一条一条看:

bootstrap命令
"bootstrap": "yarn || npm i"

执行依赖安装的命令,即npm install

build:file命令
"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js",

从上面可以看出该命令实际上执行了4个js文件:

  • iconInit
  • build-entry
  • il8n
  • version

上面4个文件都在build/bin目录下,具体看看每个文件的执行逻辑。

iconInit.js

该文件主要用于icon初始化工作,是为examples官网服务的,主要的代码如下:

var fontFile = fs.readFileSync(path.resolve(__dirname, '../../packages/theme-chalk/src/icon.scss'), 'utf8');
fs.writeFile(path.resolve(__dirname, '../../examples/icon.json'), JSON.stringify(classList), () => {});

从packages/theme-chalk/src目录下读取icon.scss文件,经过相关处理之后,写入到examples下的icon.json文件中

build-entry.js

该文件是用于生成src/index.js文件内容的,该文件的主要几点处理逻辑如下:

// 导入组件JSON列表
var Components = require('../../components.json');
// src/index.js的绝对路径
var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
var INSTALL_COMPONENT_TEMPLATE = '  {{name}}';
// 相关处理之后写入
fs.writeFileSync(OUTPUT_PATH, template);

il8n.js

顾名思意,国际化相关的,为examples项目服务的

version.js

根据package.json中version或process.env.VERSION动态生成versions.json文件,依旧是为examples项目服务的

build:theme命令
"node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",

这个命令需要注意的是使用了node、gulp、cp-cli这两个命令,这里就需要具体展开看看了。

gen-cssfile.js

使用node命令执行gen-cssfile.js文件,该文件的功能是批量导入指定目录下的scss文件构成index.scss,主要处理逻辑如下:

// 所有组件列表
var Components = require('../../components.json');
var basepath = path.resolve(__dirname, '../../packages/');
var themes = [
  'theme-chalk'
];
var fileName = key + (isSCSS ? '.scss' : '.css');
indexContent += '@import "./' + fileName + '";\n';
// packages/theme-chalk/src/${fileName}
var filePath = path.resolve(basepath, theme, 'src', fileName);
fs.writeFileSync(filePath, '', 'utf8');

// packages/theme-chalk/src/index.scss
fs.writeFileSync(path.resolve(basepath, theme, 'src', isSCSS ? 'index.scss' : 'index.css'), indexContent);

从上面主要逻辑点中,可以看出该文件是创建packages/theme-chalk/src下的index.scss文件的。

gulpfile.js

使用gulp(类webpack工具)来执行gulpfile.js配置文件,该文件位于packages/theme-chalk下,

使用gulp build命令,具体看看该文件的配置:

var autoprefixer = require('gulp-autoprefixer');
var cssmin = require('gulp-cssmin');
gulp.task('compile', function() {
  // 导入theme-chalk/src下所有scss文件,进行相关的处理
  return gulp.src('./src/*.scss')
    .pipe(sass.sync())
    // CSS兼容处理
    .pipe(autoprefixer({
      browsers: ['ie > 9', 'last 2 versions'],
      cascade: false
    }))
    // CSS压缩
    .pipe(cssmin())
    // 最后输出到lib目录下(打包后的目录)
    .pipe(gulp.dest('./lib'));
});

gulp.task('copyfont', function() {
  return gulp.src('./src/fonts/**')
    .pipe(cssmin())
    .pipe(gulp.dest('./lib/fonts'));
});
gulp.task('build', ['compile', 'copyfont']);

从上面逻辑可以知道:转换scss为css文件和处理fonts字体输出到packages/theme-chalk/lib目录下

cp-cli packages/theme-chalk/lib lib/theme-chalk

从最后的结果来看是,cp-cli是将指定目录下所有内容复制到另外目录下,即将packages/theme-chalk/lib目录所有内容复制到lib/theme-chalk目录下

build:utils命令
"build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",

cross-env跨平台使用Unix方式设置环境变量,上面的命令意思是:

设置BABEL_ENV=utils环境变量

使用babel转换src下处理index.js外所有js文件,转换输出到lib下

build:umd命令
"build:umd": "node build/bin/build-locale.js",

执行build-locale.js文件,该文件的主要处理逻辑是:

var localePath = resolve(__dirname, '../../src/locale/lang');

var transform = function(filename, name, cb) {
  // 将ES6模块转换为umd模块方式
  require('babel-core').transformFile(resolve(localePath, filename), {
    plugins: [
      'add-module-exports',
      ['transform-es2015-modules-umd', {loose: true}]
    ],
    moduleId: name
  }, cb);
};
// 相关处理后保存在lib/umd/locale目录下
save(resolve(__dirname, '../../lib/umd/locale', file)).write(code);

该文件就是读取src/locale下的lang文件所有js文件,将其转换成UMD模块的方式,输出效果形同:

(function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define('element/locale/ar', ['module', 'exports'], factory);
  } else if (typeof exports !== "undefined") {
    factory(module, exports);
  } else {
    var mod = {
      exports: {}
    };
    factory(mod, mod.exports);
    global.ELEMENT.lang = global.ELEMENT.lang || {}; 
    global.ELEMENT.lang.ar = mod.exports;
  }
})(this, function (module, exports) {
  'use strict';

  exports.__esModule = true;
  exports.default = {
  };
  module.exports = exports['default'];
});
clean命令
"clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",

rimraf插件实现rm -rf的效果,即该命令就是删除lib、packages子级目录下lib、test下的coverage文件夹的

deploy命令
"deploy": "npm run deploy:build && gh-pages -d examples/element-ui --remote eleme && rimraf examples/element-ui",

三个功能:

  • 执行deploy:build命令
  • gh-pages包来实现发布官网项目element-ui到eleme
  • rimra来删除examples/element-ui
deploy:build命令
"deploy:build": "npm run build:file && cross-env NODE_ENV=production webpack --config build/webpack.demo.js && echo element.eleme.io>>examples/element-ui/CNAME",

将上面的命令分解开来:

npm run build:file
cross-env NODE_ENV=production webpack --config build/webpack.demo.js
echo element.eleme.io>>examples/element-ui/CNAME
  • 执行build:file命令,之前就分析过实际上就iconinit、i18n、version、build-entry文件的执行,这边可以去上面去看

  • echo element.eleme.io>>examples/element-ui/CNAME:读取element.eleme.io内容重定向到执行文件中

  • webpack --config build/webpack.demo.js:使用webpack执行wbpack.demo.js文件

      output: {
        path: path.resolve(process.cwd(), './examples/element-ui/'),
        publicPath: process.env.CI_ENV || '',
        filename: '[name].[hash:7].js',
        chunkFilename: isProd ? '[name].[hash:7].js' : '[name].js'
      },
    

    上面是该配置文件的输出配置,可以看出是为element-ui官网项目服务的,这里就不展开说明了。

dev、dev:play命令
"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",

这两个命令就放在一起,也是为element-ui官网项目服务的,webpack-dev-server --config build/webpack.demo.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",

该命令就是常使用的build命令的功能,即编译打包element框架源码,将上面命令拆分出来看看都做了什么,如下:

- 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

实际上上面的命令有几个之前就已做了分析,这里就不在重复了,主要看看没有分析的,也就下面4个命令了:

  • npm run lint
  • webpack --config build/webpack.common.js
  • webpack --config build/webpack.component.js
  • webpack --config build/webpack.conf.js

npm run lint执行eslint规则校验,之后的三个命令都是webpack按照指定的配置文件编译打包源码。

下面会对这三个配置文件是整个打包的配置核心,这里会做详细的梳理分析。

webpack.common.js配置文件

主要的配置详情如下:

// config配置文件中相关配置
const config = require('./config');
module.exports = {
  // 入口文件:src/index.js
  entry: {
    app: ['./src/index.js']
  },
  // 输出路径配置:/lib/element-ui.commom.js
  output: {
    path: path.resolve(process.cwd(), './lib'),
    publicPath: '/dist/',
    filename: 'element-ui.common.js',
    // 非入口chunk文件打包名称
    chunkFilename: '[id].js',
    libraryTarget: 'commonjs2'
  },
  resolve: {
    // 省略文件后缀
    extensions: ['.js', '.vue', '.json'],
    // 别名定义
    alias: config.alias,
    // 告诉webpack解析模块时应搜索的目录
    modules: ['node_modules']
  },
  // 用于描述外部 library 所有可用的访问方式(此部分不会被webpack打包)
  externals: config.externals,
  // 加载器配置
  module: {
    rules: [
      // 相关loader配置
    ]
  },
  plugins: [
    // webpack打包进度插件
    new ProgressBarPlugin(),
    // 适用于开发版本同线上版本在某些常量上有区别的场景。
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    // 用于将webpack1迁移到webpack2
    new webpack.LoaderOptionsPlugin({
      // loader 是否要切换到优化模式
      minimize: true
    })
  ]
};
config.js配置文件
var path = require('path');
var fs = require('fs');
var nodeExternals = require('webpack-node-externals');
// 所有组件列表
var Components = require('../components.json');
// 配置BEM相关的
var saladConfig = require('./salad.config.json');

// 读取src下的utils、mixins、transtions目录
var utilsList = fs.readdirSync(path.resolve(__dirname, '../src/utils'));
var mixinsList = fs.readdirSync(path.resolve(__dirname, '../src/mixins'));
var transitionList = fs.readdirSync(path.resolve(__dirname, '../src/transitions'));
var externals = {};

// 对每一个组件都构建一个externals值
Object.keys(Components).forEach(function(key) {
  externals[`element-ui/packages/${key}`] = `element-ui/lib/${key}`;
});

externals['element-ui/src/locale'] = 'element-ui/lib/locale';
utilsList.forEach(function(file) {
  file = path.basename(file, '.js');
  externals[`element-ui/src/utils/${file}`] = `element-ui/lib/utils/${file}`;
});
mixinsList.forEach(function(file) {
  file = path.basename(file, '.js');
  externals[`element-ui/src/mixins/${file}`] = `element-ui/lib/mixins/${file}`;
});
transitionList.forEach(function(file) {
  file = path.basename(file, '.js');
  externals[`element-ui/src/transitions/${file}`] = `element-ui/lib/transitions/${file}`;
});

// nodeExternals()指代node_modules中所有模块
externals = [Object.assign({
  vue: 'vue'
}, externals), nodeExternals()];
// 暴露externals,这里externals包含了所有组件以及src下locale、mixins、transitions、utils每个文件
exports.externals = externals;

// 别名
exports.alias = {
  main: path.resolve(__dirname, '../src'),
  packages: path.resolve(__dirname, '../packages'),
  examples: path.resolve(__dirname, '../examples'),
  'element-ui': path.resolve(__dirname, '../')
};
// 定义vue的在不同模块系统的访问形式
exports.vue = {
  // 通过全局变量
  root: 'Vue',
  // CommonJs模块系统
  commonjs: 'vue',
  commonjs2: 'vue',
  // AMD模块系统
  amd: 'vue'
};

// 打包忽略的js文件
exports.jsexclude = /node_modules|utils\/popper\.js|utils\/date.\js/;

// postcss
exports.postcss = function(webapck) {
  saladConfig.features.partialImport = {
    addDependencyTo: webapck
  };
  return [
    require('postcss-salad')(saladConfig)
  ];
};

webpack.component.js配置文件
const path = require('path');
const webpack = require('webpack');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
// 所有组件列表
const Components = require('../components.json');
const config = require('./config');

const webpackConfig = {
  // 多入口配置,即每个组价的入口文件配置,每个组件独立打包
  entry: Components,
  output: {
    // 输出到lib目录下
    path: path.resolve(process.cwd(), './lib'),
    publicPath: '/dist/',
    filename: '[name].js',
    chunkFilename: '[id].js',
    libraryTarget: 'commonjs2'
  },
  // 跟webpack.common.js相同配置
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: config.alias,
    modules: ['node_modules']
  },
  externals: config.externals,
  module: {
    rules: [
      // 相关loader配置
    ]
  },
  // 同webpack.common.js
  plugins: [
    new ProgressBarPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ]
};

module.exports = webpackConfig;

webpack.conf.js配置文件
const path = require('path');
const webpack = require('webpack');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');

const config = require('./config');

module.exports = {
  entry: {
    app: ['./src/index.js']
  },
  // 打包到:lib/index.js
  output: {
    path: path.resolve(process.cwd(), './lib'),
    publicPath: '/dist/',
    filename: 'index.js',
    chunkFilename: '[id].js',
    // 支持AMD、CommonJS、CMD、ES6模块、全局所有引用方式
    libraryTarget: 'umd',
    // 让library在各种用户环境中可用,就需要配置library属性
    library: 'ELEMENT',
    umdNamedDefine: true
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: config.alias
  },
  // 定义vue在不同模块系统的引用方式
  externals: {
    vue: config.vue
  },
  module: {
    rules: [
        // 相关loader配置
    ]
  },
  plugins: [
    new ProgressBarPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    // 压缩打包,去除警告、注释信息
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      output: {
        comments: false
      },
      sourceMap: false
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ]
};

对上面的几种三个配置文件主要的配置都有了比较详细的说明,可以看出每个配置文件的主要作用了:

  • config.js:配置文件通用的设置都在此,主要是设置externals外部扩展相关的
  • webpack.common.js:入口文件是src/index,打包生成/lib/element-ui.commom.js
  • webpack.component.js:用于每个组件独立打包的
  • webapck.conf.js:打包src/index.js,用于生产环境支持不同的模块系统

实际上对于webpack.common.js与webpack.conf.js这两个区别认知不是太清晰,还需要深入了解下webpack相关配置。

pub命令
"pub": "npm run bootstrap && sh build/git-release.sh && sh build/release.sh && node build/bin/gen-indices.js && sh build/deploy-faas.sh"

上面命令拆分成具体的命令,这样便于具体分析pub命令具体都做了哪些处理,拆分如下:

- npm run bootstrap
- sh build/git-release.sh
- sh build/release.sh
- node build/bin/gen-indices.js
- sh build/deploy-faas.sh

pub命令中有3个是shell脚本的执行,具体看看这边的shell脚本的逻辑:

git-release.sh
#!/usr/bin/env sh
# 切换到dev分支
git checkout dev

# git status获取当前暂存区状态
# 	--porcelain:等价于--short的输出,提供易于解析格式专门用于scripts
# 字符串长度不为0就为真, >&2标准错误输出
if test -n "$(git status --porcelain)"; then
  echo 'Unclean working tree. Commit or stash changes first.' >&2;
  exit 128;
fi

if ! git fetch --quiet 2>/dev/null; then
  echo 'There was a problem fetching your branch. Run `git fetch` to see more...' >&2;
  exit 128;
fi

if test "0" != "$(git rev-list --count --left-only @'{u}'...HEAD)"; then
  echo 'Remote history differ. Please pull changes.' >&2;
  exit 128;
fi

echo 'No conflicts.' >&2;
release.sh
# 切换到master分支,合并dev分支
git checkout master
git merge dev

#!/usr/bin/env sh
# 版本号交互设置
set -e
echo "Enter release version: "
read VERSION

read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r
echo    # (optional) move to a new line
if [[ $REPLY =~ ^[Yy]$ ]]
then
  echo "Releasing $VERSION ..."

  # build
  VERSION=$VERSION npm run dist

  # publish theme
  echo "Releasing theme-chalk $VERSION ..."
  cd packages/theme-chalk
  npm version $VERSION --message "[release] $VERSION"
  if [[ $VERSION =~ "beta" ]]
  then
    npm publish --tag beta
  else
    npm publish
  fi
  cd ../..

  # commit提交
  git add -A
  git commit -m "[build] $VERSION"
  npm version $VERSION --message "[release] $VERSION"

  # publish发布
  git push eleme master
  git push eleme refs/tags/v$VERSION
  git checkout dev
  git rebase master
  git push eleme dev

  if [[ $VERSION =~ "beta" ]]
  then
    npm publish --tag beta
  else
    npm publish
  fi
fi

deploy-faas.sh
#! /bin/sh
mkdir temp_web
npm run deploy:build
cd temp_web
# 只clone gh-pages分支下最近一次提交的代码
git clone --depth 1 -b gh-pages --single-branch https://github.com/ElemeFE/element.git && cd element

# build sub folder
SUB_FOLDER='2.4'
mkdir $SUB_FOLDER
rm -rf *.js *.css *.map static
rm -rf $SUB_FOLDER/**
cp -rf ../../examples/element-ui/** .
cp -rf ../../examples/element-ui/** $SUB_FOLDER/
cd ../..

# deploy domestic site
faas deploy alpha
rm -rf temp_web

可以看出这边的shell脚本是关于分支合并、npm发布、element-ui官网部署更新相关的。

Makefile

该文件实际上就是定义了相关命名的别名而已,例如:


install:
	npm install

install-cn:
	npm install --registry=http://registry.npm.taobao.org

dev:
	npm run dev

play:
	npm run dev:play

总结

从整个工程化构建机制中,可以清楚知道element框架项目主要的构建流程了:

  • webpack负责JS的打包压缩
  • glup负责css的压缩打包
  • postcss负责将对应的scss文件构建成JS可以处理ast,这点在gen-cssfile.js这个文件中相关处理逻辑中可以体现
  • 最重要的两个命令就是dist和pub了,dist命令开启编译,pub命令负责发布
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值