Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react-native 与 react-web 的融合 #20

Open
6174 opened this issue Apr 21, 2015 · 16 comments
Open

react-native 与 react-web 的融合 #20

6174 opened this issue Apr 21, 2015 · 16 comments

Comments

@6174
Copy link

6174 commented Apr 21, 2015

关于

对于react-native在实际中的应用, facebook官方的说法是react-native是为多平台提供共同的开发方式,而不是说一份代码,多处使用。 然后一份代码能够多处使用还是很有意义的,我所了解到的已经在尝试做这件事情的:

  1. modularise-css-the-react-way
  2. react-style
  3. native-css

现阶段大家都是在摸索中,且react-native 还不够成熟,为此我也想通过一个实际的例子提前探究一下共享代码的可行性。

下面是我以SampleApp做的一个简单demo, 先奉献上截图:

web 版本

react-native版本

初步想法

组件

react-native基本上是View套上Text这样来布局,为了做web和native的兼容,我们得提供继承版本的View ,针对不同的平台返回不同做兼容,我们将提供:

  1. Share.View -> View (reac-native = View , web = div)
  2. Share.P + Share.Span -> Text (Text在react-native中分为块级别和inline级别所以得用两个元素来区分)

样式

我们知道react-native的样式是css很小的一个子集,大概支持50种属性,为了做到web和native使用同样地样式,那么我的想法是:

  1. 使用css文件来编写样式,通过编译的方式生产不同平台需要的样式
  2. 对于web,使用auto-prefixel处理,生产web兼容的css代码
  3. 对于react-native,生成对应的styles.js
  4. css的写法用OOCSS的方式

这样做的另外一个原因是,因为css是全集,react-native是子集,全集到子集可以通过删减来处理,但是如果想通过子集到全集就会很麻烦(react-style就是通过react-native来生成css)。 且这样做还有很多好处,例如我们可以支持react-native里边不支持的css写法,例如padding: a b c d; 这种写法很容易得到兼容。

其实这里,无论react-native还是react-web都支持style={}这样的写法. 上面例子中的web截图其实是没有引用css的,但inline样式对于web来说并不是优选。 后面也做了通过react-native的css到web的css的尝试, 那种方案在样式上可以完全基于react-native来写,直接兼容web。

实现思路

首先大概整理一下我们需要解决的问题:

  1. 如何区分web和native
  2. js如何对应不同的平台来编译,因为react-native使用的是自己的依赖管理packager
  3. css如何编译为js
  4. 代码结构应该是怎样的

问题一 : 如何区分web和native

react-native 里边会有window变量吗?我试了一下,是有的,那window变量里边不可能有location,document之类的吧, 借着这种想法,可用如下方法来区分native和web

 var isNative = !window.location;

问题二:如何对应不同平台打包

对于react-native,是通过packager来打包的,具体的实现和逻辑可以随时查看packager的readme文档。 那我们怎么将适用于native的代码打包成web的代码,首先想到的是browserify, webpack。 都是遵循commonJs规范,个人更喜欢前者, 用它来应该足以满足需求。

问题三: css如何编译为js

前面提到了native-css , 可以用它来帮助我们完成打包。

问题四:代码结构应该是怎样的

web和native的代码都写在同一个地方,如何做区分呢? 这个问题当然最好就是不做区分,或者就像女生的衣服,期望是越少越好,但永远不可能木有(猥琐了:-】)。

我设想中的一个最简模型的目录结构,web和ios有不同的入口,web和ios有单独的目录, 组件共享, 如下:

├── compo.js            // 我们会使用到得公共组件
├── styles.css          // compo的样式文件
├── index.web.js        // web 入口
├── index.ios.js        // ios 入口
├── shared.js           // 做兼容的共享变量文件
├── ios                 // ios 目录
└── web                 // web 目录
    ├── index.html      // web 页面
    ├── index.web.js    // 打包过后的js
    └── react.js        // react.js依赖

好像很复杂的样子, 其实相对于原本的SampleApp,只是多了index.web.js , web目录, shared三者。 然后style通过style.css来描述。

具体实现

我们已经整理了具体的实现思路,下面是我就会直接吐出我的实现代码, 重点的地方都会在源码里边有注释

先看应用代码:

ios入口:index.ios.js

    /**
     * Sample React Native App
     * https://github.com/facebook/react-native
     */
    'use strict';
    var React = require('react-native');
    var Compo = require('./compo');
    React.AppRegistry.registerComponent('ShareCodeProject', () =>  Compo);

web入口:index.web.js

    /**
     * for web
     */
    var Compo = require('./compo');
    React.render(<Compo />, document.getElementById('App'));

样例组件:compo.js

    // 依赖的公共库,通过它获取兼容的组件
    var Share = require('./shared');
    // styles是style.css build过后生成的style.js
    var styles = require('./styles');
    var React = Share.React;
    var {
      View,
      P,
      Span
    } = Share;

    var Compo = React.createClass({
      render: function() {
        return (
          <View style={styles.container}>
            <P style={styles.welcome}>
              Welcome to React Native!
            </P>
            <P style={styles.instructions}>
              To get started, edit index.ios.js
            </P>
            <P style={styles.instructions}>
              Press Cmd+R to reload,{'\n'}
              Cmd+Control+Z for dev menu
            </P>
          </View>
        );
      }
    });

    module.exports = Compo;

组件样式: style.css

    /**
     * 大家可能发现了css的写法还是小驼峰,是的不是横杠,暂时我们还是以这种方式处理
     * native-css 目测不支持横杠,(自己重写native-css相对来说是比较容易的,完全可以做到css兼容到react-native的css子集)
     */
    .container {
        flex: 1;
        justifyContent: center;
        alignItems: center;
        backgroundColor: #F5FCFF;
    }

    .welcome {
        fontSize: 20;
        textAlign: center;
        margin: 10;
    }

    .instructions {
        textAlign: center;
        color: #333333;
        marginBottom: 5;
    }

index.html

    <!DOCTYPE html>
    <html>
      <head>
        <title>Hello React!</title>
        <script src="./react.js"></script>
        <!-- No need for JSXTransformer! -->
      </head>
      <body>
        <div id="App"></div>
        <script src="./index.web.js"></script>
      </body>
    </html>

Share部分的处理

shared.js

    var Share = {};
    var React = require('react-native');
    var isNative = !window.location;
    /**
     * 判断是web的时候,重新赋值React
     */
    if (window.React) {
        React = window.React;
    } 
    Share.React = React;

    /**
     * 做底层的兼容, 当然这里只是做了一个最简demo,具体实现的时候可能会对props做各种兼容处理
     */
    if (!isNative) {

        Share.View = React.createClass({
            render: function() {
                return <div {...this.props}/>
            }
        });

        Share.P = React.createClass({
            render: function() {
                return <p {...this.props}/>
            }
        });

        Share.Span = React.createClass({
            render: function() {
                return <span {...this.props}/>
            }
        });
    } else {
        // alert('isNative')
        Share.View = React.View;
        Share.P = React.Text;
        Share.Span = React.Text;
        Share.Text = React.Text;
    }

    module.exports = Share;

build打包程序

    var fs = require('fs');
    var nativeCSS = require('native-css'),
    var cssObject = nativeCSS.convert('./styles.css');

    toStyleJs(cssObject, './styles.js');
    buildWebReact();

    /**
     * native-css获取到得是一个对象,需要将cssObject转化为js代码
     */
    function toStyleJs(cssObject, name) {
        console.log('build styles.js \n');
        var tab = '    ';
        var str = '';

        str += '/* build header */\n';
        str += 'var styles = {\n';

        for(var key in cssObject) {
            var rules = cssObject[key];
            str += tab + key + ': {\n';
            for(var attr in rules) {
                var rule = rules[attr];
                str += tab + tab + attr + ': ' + format(rule) + ',\n'
            }
            str += tab + '},\n' 
        }

        str += '};\n'
        str += 'module.exports = styles;\n'

        fs.writeFile(name, str)
        function format(rule) {
            if (!isNaN(rule - 0)) {
                return rule;
            }
            return '"' + rule + '"';
        }
    }

    /**
     * 构造web使用的react
     */
    function buildWebReact() {
        console.log('build web bundle');
        var browserify = require('browserify');
        var b = browserify();
        b.add('./index.web.js');

        // 添加es6支持
        b.transform('reactify', {'es6': true});

        // ignore掉react-native 
        b.ignore('react-native')
        var wstream = fs.createWriteStream('./web/index.web.js');
        b.bundle().pipe(wstream);
    }

也尝试一下由react-native 到react-web的兼容方案

问题

  1. flexbox的写法在react-native上面我们会发现, 不用在父元素上声明display: flex; 在web上必须要做这样的声明, 所以我们需要让设置了flex:*的元素的父元素display: flex;
  2. flexbox在android上是由很多bug的,所以必须要解决兼容性问题webkit-box

解决方案

1. nested 的style写法

    styles = StyleSheet.create({
        mod: {
            flexDirection: 'row',
            item: {
                flex: 1
            }
        }
    });

这样的写法有些像less,我们可以知道元素的层级关系, 这样我可以遍历这个对象,查找子元素有设置flex的,父元素加上display:flexbox

2. 通过自定义元素

 var GridSystem = require('GridSystem');
 var {
    Row,
    Grid,
    Grid6,
    Grid4
 } = GridSystem;
 <Row ...>
    <Grid/>
    <Grid/>
 </Row>

通过标签的方式, 相当于给react-native或者react添加了一个网格系统,同时我们可以直接在Row上设置display:flex.

3. 遍历查找

完全同react-native原生的写法,直接在web中兼容,遍历所有有flex样式的节点,直接做兼容。

    componentDidMount: function() {
        var $node = this.getDOMNode();
        var $parent = $node.parentNode;
        var $docfrag = document.createDocumentFragment();
        $docfrag.appendChild($node);

        var treeWalker = document.createTreeWalker($node, NodeFilter.SHOW_ELEMENT, { 
            acceptNode: function(node) { 
                return NodeFilter.FILTER_ACCEPT; 
            } 
        }, false);

        while(treeWalker.nextNode()) {
            var node = treeWalker.currentNode;
            if (node.style.flex) {
                flexChild(node);
                flexParent(node.parentNode);
            }
        };

        $parent.appendChild($docfrag);
    }

    function flexChild(node) {
        if (node.__flexchild__) {
            return;
        }
        node.__flexchild__ = true;
        var flexGrow = node.style.flexGrow;
        addStyle(node, `
            -webkit-box-flex: ${flexGrow};
            -webkit-flex: ${flexGrow};
            -ms-flex: ${flexGrow};
            flex: ${flexGrow};
        `);
        node.classList.add('mui-flex-cell');
    }

    function flexParent(node) {
        if (node.__flexparentd__) {
            return;
        }
        node.__flexparentd__ = true;
        node.classList.add('mui-flex');
    }
    .mui-flex {
        display: -webkit-box!important;
        display: -webkit-flex!important;
        display: -ms-flexbox!important;
        display: flex!important;
        -webkit-flex-wrap: wrap;
        -ms-flex-wrap: wrap;
        flex-wrap: wrap;
        -webkit-box-orient: vertical;
        -webkit-box-direction: normal;
        -webkit-flex-direction: column;
        -ms-flex-direction: column;
        flex-direction: column;
    }

    .mui-flex-cell {
        -webkit-flex-basis: 0;
        -ms-flex-preferred-size: 0;
        flex-basis: 0;
        max-width: 100%;
        display: block;
        position: relative;
    }

总结

这个demo很简单,实际应用中应该会有很多地方的坑, 比如:

  1. 模块中依赖只有native才有的组件
  2. Native模块的事件处理和web大不相同
  3. 现实环境中的模块更多,更复杂,如何做模块的管理

对于write once, run anywhere 这个观点. 相信不同的人会有不同的看法,但无论如何,如果兼容成本不大,这样的兼容技术方案对业务开发是有极大意义的。

ps0: 这里仅仅做可行性方案的分析,不代表我认同或不认同这种方案。
ps1: 大家如果有更好的方案,求教,求讨论。

@hkongm
Copy link

hkongm commented Apr 22, 2015

虽然都是React一家子,但是,个人认为这俩家庭属于远亲……
想要融合应该是FB去思考的问题,等将来Android再整进来就更麻烦了……
毕竟这东西原理比Cordova之类是完全不一样的

@punkwang
Copy link

我用native-css 在iOS模拟器里运行 一直提示 module ‘fs’ 无法找到 是什么情况。。。。

@punkwang
Copy link

snip20150422_3
snip20150422_4

我用native-css 在iOS模拟器里运行 一直提示 module ‘fs’ 无法找到 是什么情况。。。。

能帮我看下嘛 是什么原因

@6174
Copy link
Author

6174 commented Apr 23, 2015

@punkwang native-css 可以当一个预编译工具,不能动态的在react-native中引用

@okoala
Copy link

okoala commented Apr 23, 2015

可以实现一套IOS,Android,Hybrid (Web) 共通的UI层和接口层。普通的开发者在Android上可以先使用Hybrid App,就像ionic或者reapp一样。等10月份Android版出来,实现接口之后就可以直接切过去,做到神不知鬼不觉 (^__^) 嘻嘻……

@leecade
Copy link

leecade commented Apr 23, 2015

👍

@kinglion
Copy link

@punkwang 缺少module,估计是你npm库没下全了,运行

npm install -S fs

@litten
Copy link

litten commented Apr 24, 2015

感觉新一轮“重复造轮子”大赛要开始了,不过这是好事。

@okoala
Copy link

okoala commented Apr 24, 2015

如果开了chrome的调试工具,

 var isNative = !window.location;

这个判断就不准了~

@6174
Copy link
Author

6174 commented Apr 24, 2015

@okoala 还真没试过这样的场景,多些指正,或者可以尝试通过 React = require('react-native') ,判断React是否为空, 因为在浏览器里边打包的时候react-native是被ignore掉的。

@binlaniua
Copy link

@okoala 你好, 业务逻辑都在js里面了, 直接解压岂不是直接就可以看了, 有没有好的规避方案.

@ovaldi
Copy link

ovaldi commented Oct 13, 2015

mark

1 similar comment
@jincan39
Copy link

mark

@byk04712
Copy link

markdown

@daweilv
Copy link

daweilv commented Mar 2, 2016

var isNative = !window.location;
巧妙:)

@liangxin123
Copy link

在android系统中,“build打包程序” 这部分代码写到哪个文件里

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests