用react写微信小程序

2018-07-18 13:55:46 weixin_34329187 阅读数 337

公司使用微信小程序做了不少东西,发现的痛点越来越多。没有组件化,配置繁锁,生命周期名字不一。基于它才有了vue系列的解决方案,我在想,能不能搞一套React的解决方案呢。毕竟以React为技术栈的公司不在少数,销路肯定很好。

我首先从定义组件着手。微信小程序是存在自定义组件的机制,但不能使用继承。并且一个组件也拆得好碎,分成四块。JS定义,CSS,模板与配置。

JS定义

微信小程序是没有组件机制,只是提供了一个工厂方法Component给你定义组件。如果模糊来看,Page也相当于组件,因为它也有data与setData方法。这相当于React的state与setState。App相当于React中的无状态组件,它可以定义一个所有页面都能访问的globalData对象,还有一些事件。我们发现App,Page,Component的一些生命周期是与路由器挂钩的,不像React,React只针对自身。

我们用React的概念来类似小程序吧,这样对没有接触过小程序的同学比较好理解


Component({
  behaviors: [], //相当于mixin,不建议使用
  properties: { //相当于propTypes
    myProperty: 1111
    myProperty2: 222 
  },
  data: {}, //  相当于state
  created(){},   //componentWillMount或是getDerivedStateFromProps
  attached(){}, //componentDidMount
  moved(){},   //componentDidUpdate可以模拟它
  detached(){}, //componentWillUnmount
  methods: {//放事件回调及其他方法,已经bind this, 现在也可以不用特意放在这个对象上,这其实是早期抄袭vue的东西
  }
})

小程序没有shouldComponentUpdate, 我们可以对setData的第一个参数做一些手脚,如果为false,返回一个空对象。

this.setData( (function(self){


   //调用原来的shouldComponentUpdate逻辑
      if(shouldComponentUpdate(self.properties, self.data ) ){   
         return newData;
      } else{
         return {};
      }
})(this),function(){
   //调用原来的componentDidUpdate逻辑
   setStateCb && setStateCb.call(this)
   componentDidUpdate && componentDidUpdate.call(this)
})

如此一来,React的生命周期就能一一对上号了,当然如果大家想一套代码共用touch与weixin,建议不要用shouldComponentUpdate,这个模拟成本很高,可能与React的效果差距很大。

CSS

CSS,在小程序中叫WXSS,是一个弱化版的CSS。文档中也介绍不要使用id,说明它无法做到scoped。但如果纯是写CSS没什么意义,我们公司已经大量使用less, sass等预处理语言,因此未来也会向这个方向发展。

模板

小程序将组件的模板独立出来,使用经典的JSP风格嵌入变量,还添加了wx:if, wx:for这些指令实现常规的if, for操作。因此许多人就将它与vue类似起来。但它与vue来比,还是很弱,首先,它没有双向绑定(美曰其名为单向流动),其次事件绑定时只能使用方法名,不能动态生成函数,也不能指定一个函数,再次也混杂了一些奇怪的标签,template, block, slot。template是应该是web component的东西,block是后端模块的东西, slot是vue的。


<view class="wrapper">
  <view>这里是组件的内部节点</view>
  <slot></slot>
</view>

相当于React这样的代码

<div class="wrapper">
 <div>这里是组件的内部节点</div>
  {this.props.children}
</div>

相对于React, vue,它没有ref这种指令,它是不想让我们得到元素节点,但我们可以定义一些data-*属性,然后在组件的attached钩子中通过this.dataset拿到它们。但相当于React, 我们是拿不到真正的props。在小程序中, properties与data是同一个东西

Component({

   data:{a: 1},
   attached: function(){
      console.log(this.data === this.properties) //true
   }
})

jsx中的this.xxx, this.props.xxx要统一进行去“this.”操作,这个用 babel 处理没什么难度。

配置

App,Page, Component都有相应的json对象,主要是定义弹窗的颜色,页面的颜色及一些子组件的引用。这些可以抽取成组件的静态属性,这样代码就更加内聚。

在我动手之前,业界其实已经有相关的方案出来,比如 weact, taro了。我所组织的技术群,也有一帮同好在做这东西,看来潮流不可逆转,小程序这种反人类的粗糙滥造之物必须再封装才方便我们迅速推进业务。就像sass, less于之css, typescript于之es5。


原文发布时间为:2018年07月04日
原文作者:掘金
本文来源: 掘金 如需转载请联系原作者


2019-05-22 17:20:00 weixin_34037515 阅读数 468

都说react和微信小程序很像,但是像在什么部分呢,待我稍作对比。

生命周期

1.React
React的生命周期在16版本以前与之后发生了重大变化,原因在于引入的React Fiber,Fiber的引入是为了解决庞大的组件树在更新的时候产生的性能问题。我们知道,组件树是一层一层的,在更新的时候,同样也是一层一层深入的,对于层级特别深的组件树,无疑需要耗费大量的时间,用户若在这段时间内进行操作,由于主线程用于UI更新,会无暇顾及用户的操作。而Fiber将一个耗时很长的任务分解成一个一个小片,每完成一个小片就去检查现在是否有需要执行的紧急任务,而Fiber就是维护分片的数据结构。
但是Fiber的出现会造成反复渲染的情况,所以生命周期需要作出改变

0*OoDfQ7pzAqg6yETH.

图片来自于 https://medium.com/@baphemot/understanding-react-react-16-3-component-life-cycle-23129bc7a705

  • getDerivedStateFromProps为一个纯函数,可以进行无副作用的操作
  • ajax一类的操作放在componentDidUpdate中

2.微信小程序

微信小程序的生命周期我们可以从文档中略知一二
page-lifecycle.2e646c86.png
我们在新建一个页面的时候,会实例化一个page,里面有onLoad等等的函数

事件处理

  1. React
    React的事件处理并非同步的,这也是使用setState的原因。根据变量isBatchingUpadates判断为直接更新还是放在队列中,默认状态为false,也就是同步更新
    2.微信小程序
    微信小程序中使用setData更新数据,基本格式相同

组件

两者都有组件化的概念,不过在学习中,小程序涉及的好像并不多。

转载于:https://www.cnblogs.com/yuyuan-bb/p/10907032.html

2019-06-19 18:08:34 tzllxya 阅读数 353

React转微信小程序:双模板联动

 

这是本系列的最后一篇。小程序封死了操作DOM的可能性,并且也不让我们操作视图,所有与视图有关的东西一律接触不了。而它的自定义组件是非常恶心,基本不配叫组件,不能继承叫什么组件。因此我们使用它更早期的动态模板技术,template。

我的思路如下,通过编译组件的render方法,将里面的自定义组件变成template类,然后在template类中自己初始化,得到props, state再传给小程序的template标签。换言之,有两套模板。

 

//源码
import { Page } from "../wechat";
import "./page.css";
import Dog from "../components/dog/dog";
const e = "e";
class P extends Page {
  constructor(props) {
    super(props);
    this.state = {
      name: 'hehe',
      array: [
        {name: "dog1",text: "text1"},
        {name: "dog2",text: "text2"},
        {name: "dog3",text: "text3"},
      ]
    };
  }

  onClick() {
    console.log("test click1" + e);
  }
  render() {
    return (
      <div>
        <div>
          {this.state.array.map(function(el) {
            return <Dog name={el.name}>{el.text}</Dog>;
          })}
        </div>
        <Dog name={this.state.name} />
      </div>
    );
  }
}
export default P;

我们先不管Dog组件长得怎么样。

为了让它同时支持小程序与React的render函数,我们需要对render进行改造。将Dog,div等改造成小程序能能认识的类型,如

 <view>
    <view>
      {this.state.array.map(function(el) {
        return <template is={Dog} name={el.name}>{el.text}</template>;
      })}
    </view>
    <template is="Dog" name={this.state.name} />
  </view>

这个转译是如何实现呢,我们可以通一个插件 syntax-jsx, 它会在visitor遍历出JSX的开标签,闭标签,属性及{}容器。

 

但React无法认识template标签,因此还要改造

//React专用
 <view>
    <view>
      {this.state.array.map(function(el) {
        return <React.template is={Dog} name={el.name}>{el.text}</React.template>;
      })}
    </view>
    <React.template is={Dog} name={this.state.name} />
  </view>

现在看小程序这边

小程序无法认识{},需要改变成wx:for指令

//小程序专用
 <view>
    <view>
      <block wx:for="{{this.state.array}}" wx:for-item="el">
         <template is="Dog" name={el.name}>{el.text}</template>;
      </block>
    </view>
    <template is="Dog" name={this.state.name} />
  </view>

小程序的template有个缺憾,它无法认识name这样的属性的,因此我们需要一个东西装着它。那么我们动态创建一个数组吧,改一改React那边:

//React专用
 <view>
    <view>
      {this.state.array.map(function(el) {
        return <React.template is={Dog} name={el.name} templatedata="data123124342">{el.text}</React.template>;
      })}
    </view>
    <React.template is={Dog} name={this.state.name} templatedata="data34343433" />
  </view>

templatedata这个属性及它的值是babel在编译时创建的,React.template到时会在this.data.state添加data123124342数组,内容为一个个对象,这些对象是通过Dog.props, Dog.defaultProps, Dog.state组成。结构大概是{ props: {}, state: {} }

那么小程序的模板则改成这样,去掉各种template不认识的东西,加上wx:for属性,它对应React方的templatedata值。然后template有一个data属性,通过对象解构,完美把所有属性(除方法)传到dog的模板中。

//小程序专用
<import src="../../components/dog/dog.wxml" />
 <view>
     <view>
      <block wx:for="{{this.state.array}}" wx:for-item="el">
         <template is="Dog" wx:for="data123124342" wx:for-item="data" data="{{...data}}"></template>;
      </block>
    </view>
    <template is="Dog" wx:for="data34343433" wx:for-item="data" data="{{...data}}" />
  </view>

然后我们再把React的render方法改成React.createElement形式:

import { Page } from "../wechat";
import "./page.css";
import Dog from "../components/dog/dog";
const e = "e";
class P extends Page {
  constructor(props) {
    super(props);
    this.state = {
      name: 'hehe',
      array: [
        {name: "dog1",text: "text1"},
        {name: "dog2",text: "text2"},
        {name: "dog3",text: "text3"},
      ]
    };
  }

  onClick() {
    console.log("test click1" + e);
  }
  render() {
    return (
      React.createElement(
          "div",
          null,
          React.createElement(
            "div",
            null,
            this.state.array.map(function(el) {
              return React.createElement(React.template, {
                name: el.name,
                children: el.text,
                is: Dog,
                templatedata:"data34343433"
              });
            })
          ),
          React.createElement(React.template, {
            is: Dog,
            name: this.state.name,
            templatedata:"data34343433"
          })
        );
}
export default P;

上面的转译工作可以通过transform-react-jsx babel插件实现

class P extends Page这种es6定义类的方式,小程序可能也不认识,或者通过babel编译后也太复杂。比如说taro将Dog这个类变成这样:

 

有长有臭,因此我们最好在React中提供一个定义类的方法,叫miniCreateClass。如此一来我们就能将Dog转换得很简洁

var React = require("../../wechat");
var Component = React.Component
var miniCreatClass = React.miniCreatClass

function Dog() {}

let Dog = miniCreatClass(Dog, Component, {
  render: function () {
    return React.createElement("view", null, this.state.name )
  }
}, {});
module.exports.default = Dog;

我们再看Page类。小程序定义页面是通过 Page 工厂实现的,大概是Page({data: {}})。小程序在这里的令计很方便我们进行hack,因为一个Page类只会有一个实例。

Page(createPage(P))

createPage的实现大既这样:

function createPage(PageClass) {
  var instance = ReactDOM.render(React.createElement(PageClass), {
    type: "div",
    root: true
  });
  var config = {
    data: {
      state: instance.state,
      props: instance.props
    },
    onLoad: function() {
      instance.$wxPage = this;
    },
    onUnload: function() {
      instance.componentWillUnmount && instance.componentWillUnmount();
    }
  };
  instance.allTemplateData.forEach(function(el) {
    if (config.data[el.templatedata]) {
      config.data[el.templatedata].push(el);
    }else{
      config.data[el.templatedata] = [el];
    }
  });
  return config;
}

最后是React.template的实现,它负责组装给template的数据,这个template是小程序的标签。

React.template = function(props){
//这是一个无状态组件,负责劫持用户传导下来的类,修改它的原型
   var clazz = props.is;
   var a = classzz.prototype;
   var componentWillMount = a.componentWillMount;
   a.componentWillMount = function(){
    
     var ref = this._reactInternalRef;
     var arr = ref._owner.allTemplateData || (ref._owner.allTemplateData = []);
     arr.push({
       props: this.props,
       state: this.state,
       templatedata: props.templatedata
     })
     componentWillMount && componentWillMount.call(this)
   }
  var componentWillUpdate = a.componentWillUpdate;
   //...再上面一样
   return React.createElement(clazz, props)
}
2017-11-04 22:44:28 a_zhon 阅读数 3134

一款纯React Native原生代码 和 微信小程序 编写的app

React Native源码地址:https://github.com/azhon/Time

微信小程序源码地址:https://github.com/azhon/Time/tree/WeChatApp在微信中进入小程序搜索 直接搜索怡笑院或者扫描下方二维码

动态效果图看这里

微信小程序效果图:

  
  

React Native效果图:

模块一:笑话 — 谜语

      

模块二:鸡汤

  

模块三:福利

  

模块四:关于

  

模块五:详情

  

app下载体验

Android 下载             iOS下载
        正在打包中…

关于app

  • 使用易源Api(SHOWAPI)接口
  • 开发时使用的版本
Environment:
  OS:  macOS Sierra 10.12.6
  Node:  8.4.0
  Yarn:  1.0.0
  npm:  5.5.1
  Watchman:  4.9.0
  Xcode:  Xcode 8.3.3 Build version 8E3004b
  Android Studio:  3.0 AI-171.4408382

Packages: (wanted => installed)
    react: "16.0.0-beta.5",
    react-native: "0.49.5",
    react-navigation: "^1.0.0-beta.15"
    react-native-easy-toast: "^1.0.8",

使用到的Library

$ npm install --save react-navigation
$ npm install react-native-easy-toast --save

遇到的问题:

  • TabNavigator嵌套TabNavigator:
//第二个TabNavigator需要设置如下属性
//参考Main.js中的代码
animationEnabled: false,

swipeEnabled: false,
2018-07-18 14:09:32 weixin_34258838 阅读数 240

这是本系列的第二篇,过去两周,已经有相当成果出来。本文介绍其中一部分可靠的思路,这个比京东的taro更具可靠性。如果觉得看不过瘾,可以看anu的源码,里面包含了miniapp的转换器。

微信小程序是面向配置对象编程,不暴露Page,App,Component等核心对象的原型,只提供三个工厂方法,因此无法实现继承。App,Page,Component所在的JS的依赖处理也很弱智,你需要声明在同一目录下的json文件中。

比如说

Component({
  properties: {},
  data: {},
  onClick: function(){}
})

properties与data都是同一个东西,properties只是用来定义data中的数据的默认值与类型,相当于React的defaultProps与propTypes。如何转换呢?

import {Component} form "./wechat"
Class AAA extends Component{
  constructor(props){
     super(props);
     this.state = {}
  }
  static propTypes = {}
  static defaultProps = {}
  onClick(){}
  render(){}
}
export AAA;

首先我们要提供一个wechat.js文件,里面提供Component, Page, App 这几个基类,现在只是空实现,但已经足够了,保证它在调试不会出错。我们要的是`Class AAA extends Component`这个语句的内容。学了babel,对JS语法更加熟悉了。这个语句在babel6中称为ClassExpression,到babel7中又叫ClassDeclaration。babel有一个叫"babel-traverse"的包,可以将我们的代码的AST,然后根据语法的成分进行转换(详见这文章 yq.aliyun.com/articles/62…)。ClassDeclaration的参数为一个叫path的对象,我们通过 path.node.superClass.name 就能拿到Component这个字样。如果我们的类定义是下面的这样,path.node.superClass.name 则为App。

Class AAA extends App{
  constructor(props){
     super(props);
     this.state = {}
  }
}

App, Page, Component对应的json差异很大,拿到这个可以方便我们区别对待。

然后我们继续定义一个ImportDeclaration处理器,将import语句去掉。

定义ExportDefaultDeclaration与ExportNamedDeclaration处理器,将export语句去掉。

到这里我不得不展示一下我的转码器的全貌了。我是通过rollup得到所有模块的路径与文件内容,然后通过babel进行转译。babel转换是通过babel.transform。babel本来就有许多叫babel-plugin-transform-xxx的插件,它是专门处理那些es5无法识别的新语法。我们需要在这后面加上一个新插件叫miniappPlugin

// https://github.com/RubyLouvre/anu/blob/master/packages/render/miniapp/translator/transform.js

const syntaxClassProperties = require("babel-plugin-syntax-class-properties")
const babel = require('babel-core')
const visitor = require("./visitor");

var result = babel.transform(code, {
        babelrc: false,
        plugins: [
            'syntax-jsx',
            //  "transform-react-jsx",
            'transform-decorators-legacy',
            'transform-object-rest-spread',
            miniappPlugin,
        ]
})
function miniappPlugin(api) {
    return {
        inherits: syntaxClassProperties,
        visitor: visitor
    };
}

miniappPlugin的结构异常简单,它继承一个叫syntaxClassProperties的插件,这插件原来用来解析es6 class的属性的,因为我们的目标也是抽取React类中的defaultProps, propsTypes静态属性。

visitor的结构很简单,就是各种JS语法的描述。


const t = require("babel-types");

module.exports = {
   ClassDeclaration: 抽取父类的名字与转换构造器,
   ClassExpression: 抽取父类的名字与转换构造器,
   ImportDeclaration(path) {
     path.remove() //移除import语句,小程序会自动在外面包一层,变成AMD模块
   },
   ExportDefaultDeclaration(path){
     path.remove() //AMD不认识export语句,要删掉,或转换成module.exports
   },
   ExportNamedDeclaration(path){
     path.remove() //AMD不认识export语句,要删掉,或转换成module.exports
   }
}

我再介绍一下visitor的处理器是怎么用的,处理器其实会执行两次。我们的AST树每个节点会被执行两次,如果学过DFS的同学会明白,第一次访问后,做些处理,然后进行它内部的节点,处理后再访问一次。于是visitor也可以这样定义。

ClassDeclaration:{
   enter(path){},
   exit(path){}
}

如果以函数形式定义,那么它只是作为enter来用。

AST会从上到下执行,我们先拿到类名的名字与父类的名字,我们定义一个modules的对象,保存信息。

enter(path) {
 let className = path.node.superClass ? path.node.superClass.name : "";
 let match = className.match(/\.?(App|Page|Component)/);
 if (match) {
 //获取类的组件类型与名字
      var componentType = match[1];
      if (componentType === "Component") {
        modules.componentName = path.node.id.name;
      }
      modules.componentType = componentType;
   }
},
我们在第二次访问这个类定义时,要将类定义转换为函数调用。即

Class AAA extends Component   ---> Component({})
实现如下,将原来的类删掉(因此才在exit时执行),然后新建一个函数调用语句。我们可以通过babel-types这个句实现。具体看这里。比如说:

const call = t.expressionStatement(
     t.callExpression(t.identifier("Component"), [ t.objectExpression([])])
);
path.replaceWith(call);

就能产生如下代码,将我们的类定义从原位置替换掉。

Component({})

但我们不能是一个空对象啊,因此我们需要收集它的方法。

我们需要在visitors对象添加一个ClassMethod处理器,收集原来类的方法。类的方法与对象的方法不一样,对象的方法叫成员表达式,需要转换一下。我们首先弄一个数组,用来放东西。

var methods = []
module.exports= {
  ClassMethod: {
    enter(path){
       var methodName = path.node.key.name
       var method = t.ObjectProperty(
            t.identifier(methodName),
            t.functionExpression(
                null,
                path.node.params,
                path.node.body,
                path.node.generator,
                path.node.async
            )
        );
       methods.push(method)
   }
} 
然后我们在ClassDeclaration或ClassExpression的处理器的exit方法中改成:

const call = t.expressionStatement(
     t.callExpression(t.identifier("Component"), [ t.objectExpression(methods)])
);
path.replaceWith(call);
于是函数定义就变成

Component({
   constructor:function(){},
   render:function(){},
   onClick: function(){}
})

到这里,我们开始另一个问题了。小程序虽然是抄React,但又想别出心裁,于是一些属性与方法是不一样的。比如说data对应state, setData对应setState,早期的版本还有forceUpdate之类的。data对应一个对象,你可以有千奇百怪的写法。

this.state ={  a: 1}
this["state"] = {b: 1};
this.state = {}
this.state.aa = 1
你想hold住这么多奇怪的写法是很困难的,因此我们可以对constructor方法做些处理,然后其他方法做些约束,来减少转换的成本。什么处理constructor呢,我们可以定义一个onInit方法,专门劫持constructor方法,将this.state变成this.data。

function onInit(config){
    if(config.hasOwnProperty("constructor")){
       config.constructor.call(config);
    }
    config.data = config.state|| {};
    delete config.state
    return config;
}
Component(onInit({
   constructor:function(){},
   render:function(){},
   onClick: function(){}
}))

具体实现参这里,本文就不贴上来了。

RubyLouvre/anu

那this.setState怎么转换成this.setData呢。这是一个函数调用,语法上称之为**CallExpression**。我们在visitors上定义同名的处理器。


 CallExpression(path) {
    var callee = path.node.callee || Object;
    if ( modules.componentType === "Component" ) {
       var property = callee.property;
      if (property && property.name === "setState") {
          property.name = "setData";
      }
    }
  },

至少,将React类定义转换成Component({})调用方式 成功了。剩下就是将import语句处理一下,因为要小程序中,如果这个组件引用了其他组件,需要在其json中添加useComponens对象,将这些组件名及链接写上去。换言之,小程序太懒了,处处都要手动。有了React转码器,这些事可以省掉。

其次是render方法的转换,怎么变成一个wxml文件呢,`{}单花括号的内容要转换成`"{{}}"`双引号+双花括号 ,wx:if, wx:for的模拟等等,且听下回分解。


原文发布时间为:2018年07月04日
原文作者:掘金
本文来源: 掘金 如需转载请联系原作者