欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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",

可以按照他的执行顺序挨个来看

  1. rimraf _site
  2. mkdir _site
  3. node ./scripts/generateColorLess.js
  4. 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 提供的接口实现的
一个简单版本的演示如下

  1. 定义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>
  1. style.less 文件如下
@base:#00375B;
.box{
    width:100px;
    height:200px;
    display: block;
    background: @base;
}

需要注意的点就是link标签和script标签的顺序

未完待续…

相关标签: antd