antd 源码解读 之 scripts中的 start
程序员文章站
2022-04-05 10:30:04
...
antd中的scripts 中的start 定义的脚本如下
"start": "rimraf _site && mkdir _site && node ./scripts/generateColorLess.js && cross-env NODE_ENV=development bisheng start -c ./site/bisheng.config.js",
可以按照他的执行顺序挨个来看
- rimraf _site
- mkdir _site
- node ./scripts/generateColorLess.js
- cross-env NODE_ENV=development bisheng start -c ./site/bisheng.config.js"
前两个没什么可说的清空然后创建_site目录
node ./scripts/generateColorLess.js
执行了scripts/generateColorLess.js
这个脚本 代码如下
const path = require('path');
const { generateTheme } = require('antd-theme-generator');
const options = {
stylesDir: path.join(__dirname, '../site/theme/static'),
antdStylesDir: path.join(__dirname, '../components'),
varFile: path.join(__dirname, '../components/style/themes/default.less'),
mainLessFile: path.join(__dirname, '../site/theme/static/index.less'),
themeVariables: ['@primary-color'],
outputFilePath: path.join(__dirname, '../_site/color.less'),
};
generateTheme(options);
关键的地方就是这个generateTheme 函数啥 其实翻开antd-theme-generator
这个包的介绍可以看到
This script generates color specific styles/less file which you can use to change theme dynamically in browser
他其实是为切换主题色服务的源码如下
const fs = require("fs");
const path = require("path");
const glob = require("glob");
const postcss = require("postcss");
const less = require("less");
// 把多个less 文件合并为一个
const bundle = require("less-bundle-promise");
const hash = require("hash.js");
// 从npm impirt less
const NpmImportPlugin = require('less-plugin-npm-import');
// const colorsOnly = require('postcss-colors-only');
const options = {
withoutGrey: true, // set to true to remove rules that only have grey colors
withoutMonochrome: true, // set to true to remove rules that only have grey, black, or white colors
};
let hashCache = "";
let cssCache = "";
// 生成随机色
function randomColor() {
return '#' + (Math.random() * 0xFFFFFF << 0).toString(16);
}
/*
Recursively get the color code assigned to a variable e.g.
@primary-color: #1890ff;
@link-color: @primary-color;
@link-color -> @primary-color -> #1890ff
Which means
@link-color: #1890ff
*/
function getColor(varName, mappings) {
const color = mappings[varName];
if (color in mappings) {
return getColor(color, mappings);
} else {
return color;
}
}
/*
Read following files and generate color variables and color codes mapping
- Ant design color.less, themes/default.less
- Your own variables.less
It will generate map like this
{
'@primary-color': '#00375B',
'@info-color': '#1890ff',
'@success-color': '#52c41a',
'@error-color': '#f5222d',
'@normal-color': '#d9d9d9',
'@primary-6': '#1890ff',
'@heading-color': '#fa8c16',
'@text-color': '#cccccc',
....
}
*/
function generateColorMap(content) {
return content
.split("\n")
.filter(line => line.startsWith("@") && line.indexOf(":") > -1)
.reduce((prev, next) => {
try {
const matches = next.match(
/(?=\S*['-])([@a-zA-Z0-9'-]+).*:[ ]{1,}(.*);/
);
if (!matches) {
return prev;
}
let [, varName, color] = matches;
if (color && color.startsWith("@")) {
color = getColor(color, prev);
if (!isValidColor(color)) return prev;
prev[varName] = color;
} else if (isValidColor(color)) {
prev[varName] = color;
}
return prev;
} catch (e) {
console.log("e", e);
return prev;
}
}, {});
}
/*
This plugin will remove all css rules except those are related to colors
e.g.
Input:
.body {
font-family: 'Lato';
background: #cccccc;
color: #000;
padding: 0;
pargin: 0
}
Output:
.body {
background: #cccccc;
color: #000;
}
*/
const reducePlugin = postcss.plugin("reducePlugin", () => {
const cleanRule = rule => {
// 清除掉.main-color .palatte- 开口的css 语句
if (rule.selector.startsWith(".main-color .palatte-")) {
rule.remove();
return;
}
let removeRule = true;
rule.walkDecls(decl => {
if (
!decl.prop.includes("color") &&
!decl.prop.includes("background") &&
!decl.prop.includes("border") &&
!decl.prop.includes("box-shadow")
) {
decl.remove();
} else {
removeRule = false;
}
});
if (removeRule) {
rule.remove();
}
};
return css => {
css.walkAtRules(atRule => {
atRule.remove();
});
css.walkRules(cleanRule);
//遍历容器的后代节点,为每个注释节点调用回调
css.walkComments(c => c.remove());
};
});
function getMatches(string, regex) {
const matches = {};
let match;
while ((match = regex.exec(string))) {
if (match[2].startsWith("rgba") || match[2].startsWith("#")) {
matches[`@${match[1]}`] = match[2];
}
}
return matches;
}
/*
This function takes less input as string and compiles into css.
*/
// 编译less文件输出css
function render(text, paths) {
return less.render.call(less, text, {
paths: paths,
javascriptEnabled: true,
plugins: [new NpmImportPlugin({ prefix: '~' })]
});
}
/*
This funtion reads a less file and create an object with keys as variable names
and values as variables respective values. e.g.
//variabables.less
@primary-color : #1890ff;
@heading-color : #fa8c16;
@text-color : #cccccc;
to
{
'@primary-color' : '#1890ff',
'@heading-color' : '#fa8c16',
'@text-color' : '#cccccc'
}
*/
// 转换 less文件中的less变量
function getLessVars(filtPath) {
const sheet = fs.readFileSync(filtPath).toString();
const lessVars = {};
const matches = sheet.match(/@(.*:[^;]*)/g) || [];
matches.forEach(variable => {
const definition = variable.split(/:\s*/);
const varName = definition[0].replace(/['"]+/g, "").trim();
lessVars[varName] = definition.splice(1).join(":");
});
return lessVars;
}
/*
This function take primary color palette name and returns @primary-color dependent value
.e.g
Input: @primary-1
Output: color(~`colorPalette("@{primary-color}", ' 1 ')`)
*/
function getShade(varName) {
let [, className, number] = varName.match(/(.*)-(\d)/);
if (/primary-\d/.test(varName)) className = '@primary-color';
return 'color(~`colorPalette("@{' + className.replace('@', '') + '}", ' + number + ")`)";
}
//验证字符串是否为颜色值
function isValidColor(color) {
if (!color || color.match(/px/g)) return false;
if (color.match(/colorPalette|fade/g)) return true;
if (color.charAt(0) === "#") {
color = color.substring(1);
return (
[3, 4, 6, 8].indexOf(color.length) > -1 && !isNaN(parseInt(color, 16))
);
}
return /^(rgb|hsl|hsv)a?\((\d+%?(deg|rad|grad|turn)?[,\s]+){2,3}[\s\/]*[\d\.]+%?\)$/i.test(
color
);
}
function getCssModulesStyles(stylesDir, antdStylesDir) {
const styles = glob.sync(path.join(stylesDir, './**/*.less'));
return Promise.all(
styles.map(p =>
less
.render(fs.readFileSync(p).toString(), {
paths: [
stylesDir,
antdStylesDir,
],
filename: path.resolve(p),
javascriptEnabled: true,
plugins: [new NpmImportPlugin({ prefix: '~' })],
})
.catch(() => '\n')
)
)
.then(csss => csss.map(c => c.css).join('\n'))
.catch(err => {
console.log('Error', err);
return '';
});
}
// 根据定义的主题变量生成对应的 less 文件 合并成一个文件输出
function generateTheme({
//包目录
antDir,
//样式文件目录
antdStylesDir,
// 输出样式的目录
stylesDir,
// 主入口样式文件
mainLessFile,
//自定义主题样式文件,
varFile,
// 写出样式文件的路径
outputFilePath,
cssModules = false,
themeVariables = ['@primary-color']
}) {
return new Promise((resolve, reject) => {
/*
Ant Design Specific Files (Change according to your project structure)
You can even use different less based css framework and create color.less for that
- antDir - ant design instalation path
- entry - Ant Design less main file / entry file
- styles - Ant Design less styles for each component
*/
let antdPath;
if (antdStylesDir) {
antdPath = antdStylesDir;
} else {
antdPath = path.join(antDir, 'lib');
}
// 项目入口文件
const entry = path.join(antdPath, './style/index.less');
// 所有less文件
const styles = glob.sync(path.join(antdPath, './*/style/index.less'));
/*
You own custom styles (Change according to your project structure)
- stylesDir - styles directory containing all less files
- mainLessFile - less main file which imports all other custom styles
- varFile - variable file containing ant design specific and your own custom variables
*/
//自定义主题样式文件
varFile = varFile || path.join(antdPath, "./style/themes/default.less");
// 读取主文件
let content = fs.readFileSync(entry).toString();
content += "\n";
// 引入 所有样式文件
styles.forEach(style => {
content += `@import "${style}";\n`;
});
// 引入 所有样式文件
if (mainLessFile) {
const customStyles = fs.readFileSync(mainLessFile).toString();
content += `\n${customStyles}`;
}
//如果文件内容没变的话
const hashCode = hash.sha256().update(content).digest('hex');
if(hashCode === hashCache){
resolve(cssCache);
return;
}
hashCache = hashCode;
let themeCompiledVars = {};
let themeVars = themeVariables || ["@primary-color"];
const lessPaths = [
path.join(antdPath, "./style"),
stylesDir
];
//处理文件中颜色相关的变量 输出css
return bundle({
src: varFile
})
.then(colorsLess => {
// 解析文件中的颜色变量
const mappings = Object.assign(generateColorMap(colorsLess),generateColorMap(mainLessFile));
return [ mappings, colorsLess ];
})
// 输出css
.then(([ mappings, colorsLess]) => {
let css = "";
themeVars = themeVars.filter(name => name in mappings);
themeVars.forEach(varName => {
const color = mappings[varName];
css = `.${varName.replace("@", "")} { color: ${color}; }\n ${css}`;
});
themeVars.forEach(varName => {
[1, 2, 3, 4, 5, 7].forEach(key => {
let name = varName === '@primary-color' ? `@primary-${key}` : `${varName}-${key}`;
css = `.${name.replace("@", "")} { color: ${getShade(name)}; }\n ${css}`;
});
});
css = `${colorsLess}\n${css}`;
return render(css, lessPaths).then(({ css }) => [
css,
mappings,
colorsLess
]);
})
.then(([css, mappings, colorsLess]) => {
css = css.replace(/(\/.*\/)/g, "");
const regex = /.(?=\S*['-])([.a-zA-Z0-9'-]+)\ {\n\ \ color:\ (.*);/g;
themeCompiledVars = getMatches(css, regex);
content = `${content}\n${colorsLess}`;
return render(content, lessPaths).then(({ css }) => {
return getCssModulesStyles(stylesDir, antdStylesDir).then(customCss => {
return [
`${customCss}\n${css}`,
mappings,
colorsLess
];
})
});
})
.then(([css, mappings, colorsLess]) => {
return postcss([reducePlugin])
// return postcss.use(colorsOnly(options))
.process(css, {
parser: less.parser,
from: entry
})
.then(({ css }) => [css, mappings, colorsLess]);
})
.then(([css, mappings, colorsLess]) => {
Object.keys(themeCompiledVars).forEach(varName => {
let color;
if (/(.*)-(\d)/.test(varName)) {
color = themeCompiledVars[varName];
varName = getShade(varName);
} else {
color = themeCompiledVars[varName];
}
color = color.replace('(', '\\(').replace(')', '\\)');
css = css.replace(new RegExp(`${color}`, "g"), varName);
});
css = `${colorsLess}\n${css}`;
themeVars.reverse().forEach(varName => {
css = css.replace(new RegExp(`${varName}(\ *):(.*);`, 'g'), '');
css = `${varName}: ${mappings[varName]};\n${css}\n`;
});
css = css.replace(/\\9/g, '');
if (outputFilePath) {
fs.writeFileSync(outputFilePath, css);
console.log(
`Theme generated successfully. OutputFile: ${outputFilePath}`
);
} else {
console.log(`Theme generated successfully`);
}
cssCache = css;
return resolve(css);
})
.catch(err => {
console.log("Error", err);
reject(err);
});
});
}
module.exports = {
generateTheme,
isValidColor,
getLessVars,
randomColor,
renderLessContent: render
};
代码的逻辑是根据入参把所有样式文件打包到一个less文件中,这样做的目的就是为了实现换肤的功能 ,这个部分其实是用了less 提供的接口实现的一个简单版本的演示如下
- 定义html如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet/less" type="text/css" href="./style.less" />
<script>
window.less = {
async: false,
env: 'production'
};
</script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js"></script>
</head>
<body>
<div class="box">
</div>
<button id="cut">切换</button>
<script>
document.getElementById('cut').addEventListener('click',function(){
less.modifyVars({
'@base': '#5B83AD'
});
})
</script>
</body>
</html>
- style.less 文件如下
@base:#00375B;
.box{
width:100px;
height:200px;
display: block;
background: @base;
}
需要注意的点就是link标签和script标签的顺序
未完待续…
上一篇: 出门忘记携带身份证?重庆率先实现电子身份凭证多场景落地
下一篇: 改变Dialog背景透明度