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命令负责发布
更多推荐
所有评论(0)