如何将react-native的style样式转换成css样式

1,935 阅读5分钟

背景: 我们总是倾向于一套代码走天下,正所谓一招鲜,吃遍天。刚接触RN项目的时候,常常为RN style样式的写法而头痛,等到熟悉了RN样式写法时,一个web端项目从天而降,于是,你又不得不操练起日渐陌生的css写法。更过分的是,有时你还得在RN样式和css样式之间来回切换,时刻处于水深火热之中。抬首间,不禁叹息一声:人间不值得。

一、准备工作

本文中详细讲解sass样式的转换,其它诸如less、css、PostCss的转换请参考:(https://github.com/kristerkari/react-native-css-modules) 这里面有较为详细说明。

我们需要准备四个依赖:

二、 创建一个React-Native APP

参考官方文档创建即可。

三、安装依赖

yarn add babel-plugin-react-native-classname-to-style babel-plugin-react-native-platform-specific-extensions react-native-sass-transformer node-sass --dev

四、设置babel配置

对于React Native v0.57 或者更新版本

.babelrc (or babel.config.js)

{
  "presets": ["module:metro-react-native-babel-preset"],
  "plugins": [
    "react-native-classname-to-style",
    [
      "react-native-platform-specific-extensions",
      {
        "extensions": ["scss", "sass"]
      }
    ]
  ]
}

对于React Native v0.57以下版本

{
  "presets": ["react-native"],
  "plugins": [
    "react-native-classname-to-style",
    [
      "react-native-platform-specific-extensions",
      {
        "extensions": ["scss", "sass"]
      }
    ]
  ]
}

五、设置Metro配置

在项目根目录下新增一个metro.config.js的文件

const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
  const {
    resolver: { sourceExts }
  } = await getDefaultConfig();
  return {
    transformer: {
      babelTransformerPath: require.resolve("react-native-sass-transformer")
    },
    resolver: {
      sourceExts: [...sourceExts, "scss", "sass"]
    }
  };
})();

对于React Native v0.57以下版本,在根目录下新增rn-cli.config.js文件

module.exports = {
  getTransformModulePath() {
    return require.resolve("react-native-sass-transformer");
  },
  getSourceExts() {
    return ["js", "jsx", "scss", "sass"];
  }
};

六、接下来你就可以愉快的使用sass来写样式

style.scss

.container {
  flex: 1;
  justify-content: center;
  align-items: center;
  background-color: #f5fcff;
}

.blue {
  color: blue;
  font-size: 30px;
}

你既可以使用className来写样式,也可以使用style

import React, { Component } from "react";
import { Text, View } from "react-native";
import styles from "./styles.scss";

const BlueText = () => {
  return <Text className={styles.blue}>Blue Text</Text>;
};

export default class App extends Component<{}> {
  render() {
    return (
      <View style={styles.container}>
        <BlueText />
      </View>
    );
  }
}

七、为sass配置TypeScript

在ts项目中,为sass配置类型提示很有必要。首先我们需要把在第三步第五步中把react-native-sass-transformer依赖替换成react-native-typed-sass-transformer

为了让className 属性正常工作,我们还需要安装下面的依赖包:

对于React Native v0.57 或者更新版本

yarn add typescript --dev

老版本:

yarn add react-native-typescript-transformer typescript --dev

在package.json中添加下面依赖,然后运行yarn命令

"@types/react-native": "^0.57.55",

如果版本versions >=0.52.4

"@types/react-native": "kristerkari/react-native-types-for-css-modules#v0.57.55",

你也可以删掉版本号,但是不建议这样做

"@types/react-native": "kristerkari/react-native-types-for-css-modules",

如果你使用的rn版本>=0.57,这样就OK了,如果不是,请参照文档:github.com/kristerkari…

八、原生提供的属性和方法如何添加到scss文件中,如何做不同机型的适配?

我们需要自定义一个transform用于sass文件的转换。

metro.config.js文件中,修改如下:

const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
  const {
    resolver: { sourceExts }
  } = await getDefaultConfig();
  return {
    transformer: {
      babelTransformerPath: require.resolve("./transformer.js")
    },
    resolver: {
      sourceExts: [...sourceExts, "scss", "sass"]
    }
  };
})();

metro.config.js

const upstreamTransformer = require("metro-react-native-babel-transformer");
const sassTransformer = require("react-native-typed-sass-transformer");
const DtsCreator = require("typed-css-modules");
const css2rn = require("css-to-react-native-transform").default;

const creator = new DtsCreator();

/** 引入原生的属性和方法 */
const preImport = `
import { PixelRatio, Dimensions, StatusBar, Platform } from 'react-native';
let DEVICE_WIDTH = Dimensions.get('window').width;
let DEVICE_HEIGHT = Dimensions.get('window').height;
let S=(designPx) => {
  return PixelRatio.roundToNearestPixel((designPx / 750) * DEVICE_WIDTH);
}
`

function renderCSSToReactNative(css) {
  return css2rn(css, { parseMediaQueries: true });
}

/** px转换成pt,做一个标记 */
function pxToPtForMark(code){
  let newCode=code;
  try {
    newCode=code.replace(/([0-9]+)px/g,(...arg)=>{
      const px=Number(arg[1]);
      return `${px}pt`;
    })
  } catch (error) {
    throw Error('样式解析错误');
  }
  return newCode;
}

/** px 或者 pt单位的适配 需要注意正负值 */
function unitAdaption(code){
  let newCode=code;
  try {
    newCode=code.replace(/"([-+]{0,1})([0-9]+)pt"/g,(...arg)=>{
      const px=arg[1]+arg[2];
      return `S(${px})`;
    })
  } catch (error) {
    throw Error('样式解析错误');
  }
  return newCode;
}

/** vh和vw的适配 */
function vhAndVwAdaption(code){
  let newCode=code;
  try {
    newCode=code.replace(/"([0-9]+)vw"/g,(...arg)=>{
      const vw=Number(arg[1]);
      return `${vw/100} * DEVICE_WIDTH`;
    }).replace(/"([0-9]+)vh"/g,(...arg)=>{
      const vh=Number(arg[1]);
      return `${vh/100} * DEVICE_HEIGHT`;
    });

  } catch (error) {
    throw Error('样式解析错误');
  }
  return newCode;
}

function isPlatformSpecific(filename) {
  var platformSpecific = [".native.", ".ios.", ".android."];
  return platformSpecific.some(name => filename.includes(name));
}

module.exports ={
  transform:async function({ src, filename, options }) {
    if (filename.endsWith(".scss") || filename.endsWith(".sass")) {

      let newSrc=pxToPtForMark(src);

      let css =await sassTransformer.renderToCSS({ src:newSrc, filename, options });
      let cssObject = renderCSSToReactNative(css);
      let cssObjectStr=JSON.stringify(cssObject);

      cssObjectStr=unitAdaption(cssObjectStr);

      cssObjectStr=vhAndVwAdaption(cssObjectStr);

      //特殊文件直接return
      if (isPlatformSpecific(filename)) {
        return upstreamTransformer.transform({
          src: preImport+";module.exports = " + cssObjectStr,
          filename,
          options
        });
      }

      //一般文件创建types文件之后再return
      return creator.create(filename, css).then(content => {
        return content.writeFile().then(() => {
          return upstreamTransformer.transform({
            src: preImport+";module.exports = " + cssObjectStr,
            filename,
            options
          });
        });
      });
    } else {
      return upstreamTransformer.transform({ src, filename, options });
    }
  }
}

在scss文件中,px单位转换成style对象时,会自动去掉,如下:

.unpaidRemind {
  position: absolute;
  bottom: 56px;
  right: 28px;
  background-color: #999;
  padding: 20px;
  border-radius: 16px;
}
.unpaidRemindText {
  color: rgba(255, 255, 255, 0.9);
  font-size: 28px;
}

转换之后变成

{
    unpaidRemind: {
    position: 'absolute',
    bottom: 56,
    right: 28,
    backgroundColor: '#999',
    padding: 20,
    borderRadius: 16,
  },
  unpaidRemindText: {
    color: 'rgba(255,255,255,0.9)',
    fontSize: 28,
  },
}

我们的目标是在在转后之后把所有px都换成我们的适配方法:

{
    unpaidRemind: {
    position: 'absolute',
    bottom: S(55),
    right: S(28),
    backgroundColor: '#999',
    padding: S(20),
    borderRadius: S(16),
  },
  unpaidRemindText: {
    color: 'rgba(255,255,255,0.9)',
    fontSize: S(28),
  },
}

最终拿到的代码类似于这样,它是可以直接执行的,同样的道理,我们可以注入更多的RN属性到我们的文件中,这取决于我们是否需要这些属性。

import { PixelRatio, Dimensions, StatusBar, Platform } from 'react-native'; 
let DEVICE_WIDTH = Dimensions.get('window').width; 
let DEVICE_HEIGHT = Dimensions.get('window').height; 
let S=(designPx) => { return PixelRatio.roundToNearestPixel((designPx / 750) * DEVICE_WIDTH); } 
module.exports ={
    unpaidRemind: {
    position: 'absolute',
    bottom: S(55),
    right: S(28),
    backgroundColor: '#999',
    padding: S(20),
    borderRadius: S(16),
  },
  unpaidRemindText: {
    color: 'rgba(255,255,255,0.9)',
    fontSize: S(28),
  },
}

pxToPtForMark方法将px转换成pt,这一步主要是方便我们后续把pt转成S(28)这种形式,unitAdaption方法就是实现这一功能。为什么不是直接把px转成S(28)这种形式?renderCSSToReactNative会把px转没掉,我们无法区分flex:1这种属性和fontSize:28的区别,但是它不会吧pt转没,而是变成fontSize:"28pt".

为了使vhvw这两个单位能够生效,我们使用vhAndVwAdaption方法做了处理,width:100vw最后会变成width:100/100 * DEVICE_WIDTH,其中DEVICE_WIDTH就是我们前面注入的设备宽度这个变量。

九、referenceError:'xx' is not defined 报错

const Button=(props)=>{ 
    const {style}=props; 
    return <View className={`${styles.default} ${style}`}></View> 
} 
const Page=()=>{ 
    return <Button className={`${styles.pageBtn}`}></Button> 
} 

//以上写法会导致报referenceError:'xx' is not defined,而且是非必现,偶尔会报 
//要这样写 
const Button=(props)=>{ 
    const {style}=props; 
    return <View 
            className={`${styles.default}`} 
            style={style}></View> 
} 
const Page=()=>{ 
    return <Button className={`${styles.pageBtn}`}></Button> 
}

参考文档: 1.(https://github.com/kristerkari/react-native-css-modules)