微信小程序开发如何使用npm包--labrador使用

相信做过微信小程序的都知道,官方给出的微信web开发工具上根本就无法加载node_modules包,即使可以加载,node_modules动辄几十M的大小,小程序的代码限制在1M以内,微信小程序的三个不足:

1无法调用npm包

2无法使用babel转吗

3无法重用组件(像react那样重用组件功能)

接下来给大家介绍一个相对完整的微信开发解决方案:

Labrador:(目前最新版本为:0.6.12)

github地址:https://github.com/maichong/labrador

特点:

1,使用Labrador框架可以使微信开发者工具支持加载海量NPM包

2,支持ES6/7标准代码,使用async/await能够有效避免回调地狱

3,组件重用,对微信小程序框架进行了二次封装,实现了组件重用和嵌套

4,自动化测试,非常容易编写单元测试脚本,不经任何额外配置即可自动化测试

5,使用Editor Config及ESLint标准化代码风格,方便团队协作

当然了也有缺点,你看完会发现缺点

首先系统全局安装nodejs和Labrador命令行工具。

npm install -g labrador-cli

查看当前labrador版本

labrador -V

新建一个目录,初始化项目

labrador create mylabrador        # 初始化labrador项目 mylabrador是你的项目名字


用Egret Wing3(这个IDE更适合开发微信小程序),打开labradordemo这个项目,

开启代码自动转换功能

labrador watch

然后用微信开发着工具打开labradordemo项目下面的dist文件


这个里面不需要做任何的编码工作,在下面的src目录作修改,会自动同步到微信开发者工具上面


在src/pages/index新增一个index.json文件,主要内容为设置页面的title

{
  "navigationBarTitleText": "主页",
  "enablePullDownRefresh": false
}

然后保存,会同步到微信开发者工具


labrador 库对全局的 wx 变量进行了封装,将所有wx 对象中的异步方法进行了Promise支持, 除了同步的方法,这些方法往往以on*create*stop*pause*close* 开头或以 *Sync 结尾。在如下代码中使用 labrador 库。


import wx, { Component, PropTypes }from'labrador';


wx.wx;          // 原始的全局 wx 对象

wx.app;         // 和全局的 getApp() 函数效果一样,代码风格不建议粗暴地访问全局对象和方法

wx.currentPages // 对全局函数 getCurrentPages() 优雅的封装

Component;      // Labrador 自定义组件基类

PropTypes;      // Labrador 数据类型校验器集合


wx.login;       // 封装后的微信登录接口

wx.getStorage;  // 封装后的读取缓存接口

//... 更多请参见 https://mp.weixin.qq.com/debug/wxadoc/dev/api/


我们建议不要再使用 wx.getStorageSync() 等同步阻塞方法,而在async 函数中使用await wx.getStorage() 异步非阻塞方法提高性能,除非遇到特殊情况。



app.js文件

import request from 'al-request';
import { setStore } from 'labrador-redux';
import { sleep } from './utils/utils';
import store from './redux';


if (__DEV__) {
  console.log('当前为开发环境');
}


// 向labrador-redux注册store
setStore(store);


export default class {
  async onLaunch() {
    try {
      await sleep(100);
      await request('api/start');
    } catch (error) {
      console.error(error);
    }
    this.timer();
  }


  async timer() {
    while (true) {
      console.log('hello');
      await sleep(10000);
    }
  }
}

代码中全部使用ES6/7标准语法。代码不必声明 use strict ,因为在编译时,所有代码都会强制使用严格模式。

代码中并未调用全局的 App() 方法,而是使用export 语法默认导出了一个类,在编译后,Labrador会自动增加App() 方法调用,所有请勿手动调用 App() 方法。这样做是因为代码风格不建议粗暴地访问全局对象和方法。


Labrador的自定义组件,是基于微信小程序框架的组件之上,进一步自定义组合,拥有逻辑处理、布局和样式。

项目中通用自定义组件存放在src/compontents 目录,一个组件一般由三个文件组成,*.js*.xml*.less 分别对应微信小程序框架的jswxmlwxss 文件。在Labardor项目源码中,我们特意采用了xml less 后缀以示区别。如果组件包含单元测试,那么在组件目录下会存在一个*.test.js 的测试脚本文件。

0.6 版本后,支持*.sass*.scss 格式样式文件。


自定义组件示例

下面是一个简单的自定义组件代码实例:


逻辑 src/compontents/todo/todo.js


import { Component, PropTypes } from 'labrador-immutable';


const { string, bool, func } = PropTypes;


class Todo extends Component {
  static propTypes = {
    id: string,
    title: string,
    createdAt: string,
    finished: bool,
    finishedAt: string,
    onRemove: func,
    onRestore: func,
    onFinish: func
  };


  constructor(props) {
    super(props);
    this.state = {
      icon: props.finished ? 'success_circle' : 'circle',
      className: props.finished ? 'todo-finished' : ''
    };
  }


  onUpdate(props) {
    this.setState({
      icon: props.finished ? 'success_circle' : 'circle',
      className: props.finished ? 'todo-finished' : ''
    });
  }


  handleRemove() {
    this.props.onRemove(this.props.id);
  }


  handleFinish() {
    if (this.props.finished) {
      this.props.onRestore(this.props.id);
    } else {
      this.props.onFinish(this.props.id);
    }
  }
}


export default Todo;

自定义组件的逻辑代码和微信框架中的page很相似,最大的区别是在js逻辑代码中,没有调用全局的 Page() 函数声明页面,而是用 export 语法导出了一个默认的类,这个类必须继承于Component 组件基类。

相对于微信框架中的page,Labrador自定义组件扩展了 propTypesdefaultPropsonUpdate()setState()children() 等方法和属性,children()方法返回当前组件中的子组件集合,此选项将在下文中叙述。

Labrador的目标是构建一个可以重用、嵌套的自定义组件方案,在现实情况中,当多个组件互相嵌套组合,就一定会遇到父子组件件的数据和消息传递。因为所有的组件都实现了setState 方法,所以我们可以使用this._children.foobar.setState(data)this.parent.setState(data) 这样的代码调用来解决父子组件间的数据传递问题,但是,如果项目中出现大量这样的代码,那么数据流将变得非常混乱。

我们借鉴了 React.js 的思想,为组件增加了 props 机制。子组件通过 this.props 得到父组件给自己传达的参数数据。父组件怎样将数据传递给子组件,我们下文中叙述。

onUpdate 生命周期函数是当组件的 props 发生变化后被调用,类似React.js中的 componentWillReceiveProps 所以我们可以在此函数体内监测props 的变化。

组件定义时的 propTypes 静态属性是对当前组件的props参数数据类型的定义。defaultProps 选项代表的是当前组件默认的各项参数值。propTypesdefaultProps 选项都可以省略,但是强烈建议定义 propTypes,因为这样可以使得代码更清晰易懂,另外还可以通过Labrador自动检测props值类型,以减少BUG。为优化性能,只有在开发环境下才会自动检测props值类型。

编译时默认是开发环境,当编译时候采用 -m 参数才会是生产模式,在代码中任何地方都可以使用魔术变量__DEV__ 来判断是否是开发环境。

组件向模板传值需要调用 setState 方法,换言之,组件模板能够读取到当前组件的所有内部状态数据。

0.6版本后,Component 基类中撤销了setData 方法,新增了setState 方法,这样做并不是仅仅为了像React.js,而是在老版本中,我们将所有组件树的内部状态数据和props全存放在page.data中,在组件更新时产生了大量的setData 递归调用,为了优化性能,必须将组件树的状态和page.data 进行了分离。


布局 src/compontents/todo/todo.xml


<view class="list-item flex-row items-center todo {{state.className}}">
  <icon class="todo-icon" type="{{state.icon}}" size="20" color="#999" catchtap="handleFinish"/>
  <view class="block flex todo-title {{state.className}}-title"> {{props.title}} </view>
  <view class="btn btn-small {{props.finished?'btn-gray-hollow':'btn-danger'}}" catchtap="handleRemove">删除</view>
</view>

XML布局文件和微信WXML文件语法完全一致,只是扩充了两个自定义标签 <component/><list/>,下文中详细叙述。

使用 {{}} 绑定变量时,以props.*state.* 开头,即XML模板文件能够访问组件对象的propsstate


样式 src/compontents/todo/todo.less

@import 'al-ui';


.todo {
  background: #fff;
  font-size: @font-size-medium;
}


.todo-icon {
  margin-right: 10px;
}


.todo-finished {
  background: @color-page;
}


.todo-finished-title {
  .gray;
  text-decoration: line-through;
}

虽然我们采用了LESS文件,但是由于微信小程序框架的限制,不能使用LESS的层级选择及嵌套语法。但是我们可以使用LESS的变量、mixin、函数等功能方便开发。


页面

我们要求所有的页面必须存放在 pages 目录中,每个页面的子目录中的文件格式和自定义组件一致,只是可以多出一个*.json 配置文件。


页面示例

下面是默认首页的示例代码:


逻辑 src/pages/index/index.js


import wx, { Component, PropTypes } from 'labrador-immutable';
import { bindActionCreators } from 'redux';
import { connect } from 'labrador-redux';
import Todo from '../../components/todo/todo';
import * as todoActions from '../../redux/todos';
import { sleep } from '../../utils/utils';


const { array, func } = PropTypes;


class Index extends Component {
  static propTypes = {
    todos: array,
    removeTodo: func,
    restoreTodo: func,
    createTodo: func,
    finishTodo: func
  };


  state = {
    titleInput: '',
    finished: 0
  };


  children() {
    let todos = this.props.todos || [];
    let unfinished = [];
    let finished = [];
    if (todos.length) {
      unfinished = todos.filter((todo) => !todo.finished);
      finished = todos.asMutable()
        .filter((todo) => todo.finished)
        .sort((a, b) => (a.finishedAt < b.finishedAt ? 1 : -1))
        .slice(0, 3);
    }
    return {
      list: unfinished.map((todo) => ({
        component: Todo,
        key: todo.id,
        props: {
          ...todo,
          onRemove: this.handleRemove,
          onRestore: this.handleRestore,
          onFinish: this.handleFinish
        }
      })),
      finished: finished.map((todo) => ({
        component: Todo,
        key: todo.id,
        props: {
          ...todo,
          onRemove: this.handleRemove,
          onRestore: this.handleRestore,
          onFinish: this.handleFinish
        }
      }))
    };
  }


  onUpdate(props) {
    let nextState = {
      finished: 0
    };
    props.todos.forEach((todo) => {
      if (todo.finished) {
        nextState.finished += 1;
      }
    });
    this.setState(nextState);
  }


  async onPullDownRefresh() {
    await sleep(1000);
    wx.showToast({ title: '刷新成功' });
    wx.stopPullDownRefresh();
  }


  handleCreate() {
    let title = this.state.titleInput;
    if (!title) {
      wx.showToast({ title: '请输入任务' });
      return;
    }
    this.props.createTodo({ title });
    this.setState({ titleInput: '' });
  }


  handleInput(e) {
    this.setState({ titleInput: e.detail.value });
  }


  handleRemove = (id) => {
    this.props.removeTodo(id);
  };


  handleFinish = (id) => {
    this.props.finishTodo(id);
  };


  handleRestore = (id) => {
    this.props.restoreTodo(id);
  };


  handleShowFinished() {
    wx.navigateTo({ url: 'finished' });
  }


  handleShowUI() {
    wx.navigateTo({ url: '/pages/ui/index' });
  }
}


export default connect(
  ({ todos }) => ({ todos }),
  (dispatch) => bindActionCreators({
    createTodo: todoActions.create,
    removeTodo: todoActions.remove,
    finishTodo: todoActions.finish,
    restoreTodo: todoActions.restore,
  }, dispatch)
)(Index);


页面代码的格式和自定义组件的格式一模一样,我们的思想是 页面也是组件

js逻辑代码中同样使用 export default 语句导出了一个默认类,也不能手动调用Page() 方法,因为在编译后,pages 目录下的所有js文件全部会自动调用Page() 方法声明页面。

我们看到组件类中,有一个对象方法 children() ,这个方法返回了该组件依赖、包含的其他自定义组件,在上面的代码中页面包含了三个自定义组件listtitlecounter ,这个三个自定义组件的key 分别为listmottocounter

children() 返回的每个组件的定义都包含两个属性,component 属性定义了组件类,props 属性定义了父组件向子组件传入的props 属性对象。

页面也是组件,所有的组件都拥有一样的生命周期函数onLoad, onReady, onShow, onHide, onUnload,onUpdate 以及setState函数。

componetspages 两个目录的区别在于,componets 中存放的组件能够被智能加载、重用,pages 目录中的组件在编译时自动加上 Page() 调用,所以,pages 目录中的组件不能被其他组件调用,否则将出现多次调用Page()的错误。如果某个组件需要重用,请存放在componets 目录或打包成NPM包。


注意 虽然页面也是组件,虽然页面的代码格式和组件一模一样,但是运行时,getCurrentPages() 得到的页面对象page 并非pages目录中声明的页面对象,page.root 才是pages目录中声明的页面对象,才是组件树的最顶端。这里我们用了组合 模式而非继承模式。

注意 所有组件的生命周期函数支持 async ,但默认是普通函数,如果函数体内没有异步操作,我们建议采用普通函数,因为async 函数会有一定的性能开销,并且无法保证执行顺序。当声明周期函数内需要异步操作,并且【不关心】各个生命周期函数的执行顺序时,可以采用async 函数。



布局 src/pages/index/index.xml


<view class="todo-list has-toolbar">
  <view class="list">
    <list key="list" name="todo"/>
  </view>
  <block wx:if="{{state.finished}}">
    <view class="group-header">已完成</view>
    <view class="list">
      <list key="finished" name="todo"/>
    </view>
    <view wx:if="{{state.finished>3}}" class="padding-h-xxlarge padding-top-large">
      <view class="btn btn-gray-hollow block" catchtap="handleShowFinished">查看全部已完成</view>
    </view>
  </block>
  <view wx:if="{{props.todos.length}}" class="gray padding text-right">总数 {{props.todos.length}} 已完成
    {{state.finished}}
  </view>
  <view wx:else class="msg">
    <icon class="msg-icon" type="info" size="80" color="#ccc"/>
    <view class="msg-title">当前没有任务</view>
    <view class="msg-desc">请在下方输入框中填入新任务然后点击新增</view>
  </view>
  <view class="toolbar toolbar-bottom padding-h-xsmall">
    <view class="form-group flex">
      <input class="form-control" type="text" placeholder="请输入新的任务" value="{{state.titleInput}}"
             bindinput="handleInput"/>
    </view>
    <view class="btn btn-success btn-toolbar" catchtap="handleCreate">新增</view>
  </view>
  <view class="footer">
    <view>Powered by Labrador</view>
    <text class="link" catchtap="handleShowUI">AL UI</text>
  </view>
</view>

XML布局代码中,使用了Labrador提供的 <component/> 标签,此标签的作用是导入一个自定义子组件的布局文件,标签有两个属性,分别为key (必选)和name (可选,默认为key的值)。key 与js逻辑代码中的组件key 对应,name 是组件的目录名。key 用来绑定组件JS逻辑对象的children 中对应的数据, name 用于在src/componetsnode_modules 目录中寻找子组件模板。


样式 src/pages/index/index.less


@import 'al-ui';
@import 'todo';


.todo-list {
  
}


LESS样式文件中,我们使用了 @import 语句加载所有子组件样式,这里的@import 'list' 语句按照LESS的语法,会首先寻找当前目录src/pages/index/ 中的 list.less 文件,如果找不到就会按照Labrador的规则智能地尝试寻找src/componetsnode_modules 目录中的组件样式。

接下来,我们定义了 .motto-title-text 样式,这样做是因为motto key 代表的title组件的模板中(src/compontents/title/title.xml)有一个view 属于title-text 类,编译时,Labrador将自动为其增加一个前缀motto- ,所以编译后这个view所属的类为title-text motto-title-text (可以查看dist/pages/index/index.xml)。那么我们就可以在父组件的样式代码中使用.motto-title-text 来重新定义子组件的样式。

Labrador支持多层组件嵌套,在上述的实例中,index 包含子组件listtitlelist 包含子组件title,所以在最终显示时,index 页面上回显示两个title 组件。


自定义组件列表


逻辑 src/components/list/list.js

import wx, { Component } from 'labrador';

import Title from'../title/title';

import Item from'../item/item';

import { sleep } from'../../utils/util';


export defaultclassListextendsComponent {


  constructor(props){

    super(props);

    this.state= {

      items: [

        { id:1, title:'Labrador' },

        { id:2, title:'Alaska' }

      ]

    };

  }


  children (){

    return {

      title:{

        component: Title,

        props: { text:'The List Title' }

      },

      listItems:this.state.items.map((item)=> {

        return {

          component: Item,

          key: item.id,

          props: {

            item: item,

            title: item.title,

            isNew: item.isNew,

            onChange: (title)=> {this.handleChange(item, title) }

          }

        };

      })

    };

  }


  asynconLoad() {

    awaitsleep(1000);

    this.setState({

      items: [{ id:3, title:'Collie', isNew:true }].concat(this.data.items)

    });

  }


  handleChange(item, title) {

    let items=this.state.items.map((i)=> {

      if(item.id== i.id){

        returnObject.assign({},i,{ title });

      }

      return i;

    });

    this.setState({ items });

  }

}

在上边代码中的 children() 返回的listItems 子组件定义时,是一个组件数组。数组的每一项都是一个子组件的定义,并且需要指定每一项的key 属性,key 属性将用于模板渲染性能优化,建议将唯一且不易变化的值设置为子组件的key,比如上边例子中的id


模板 src/components/list/list.xml

<viewclass="list">

  <componentkey="title"name="title"/>

  <listkey="listItems"name="item"/>

</view>

在XML模板中,调用 <list/> 标签即可自动渲染子组件列表。和<component/> 标签类似,<list/> 同样也有两个属性,keyname。Labrador编译后,会自动将 <list/> 标签编译成wx:for 循环。


自动化测试

我们规定项目中所有后缀为 *.test.js 的文件为测试脚本文件。每一个测试脚本文件对应一个待测试的JS模块文件。例如src/utils/util.jssrc/utils/utils.test.js 。这样,项目中所有模块和其测试文件就全部存放在一起,方便查找和模块划分。这样规划主要是受到了GO语言的启发,也符合微信小程序一贯的目录结构风格。

在编译时,加上 -t 参数即可自动调用测试脚本完成项目测试,如果不加-t 参数,则所有测试脚本不会被编译到dist 目录,所以不必担心项目会肥胖。


普通JS模块测试

测试脚本中使用 export 语句导出多个名称以test* 开头的函数,这些函数在运行后会被逐个调用完成测试。如果test测试函数在运行时抛出异常,则视为测试失败,例如代码:

// src/util.js

// 普通项目模块文件中的代码片段,导出了一个通用的add函数

export functionadd(a, b) {

  return a+ b;

}

// src/util.test.js

// 测试脚本文件代码片段


import assert from'assert';


//测试 util.add() 函数

export functiontestAdd(exports) {

  assert(exports.add(1,1)===2);

}

代码中 testAdd 即为一个test测试函数,专门用来测试add() 函数,在test函数执行时,会将目标模块作为参数传进来,即会将util.js 中的exports 传进来。


自定义组件测试

自定义组件的测试脚本中可以导出两类测试函数。第三类和普通测试脚本一样,也为 test* 函数,但是参数不是exports 而是运行中的、实例化后的组件对象。那么我们就可以在test函数中调用组件的方法或则访问组件的propsstate 属性,来测试行为。另外,普通模块测试脚本是启动后就开始逐个运行test* 函数,而组件测试脚本是当组件onReady 以后才会开始测试。

自定义组件的第二类测试函数是以 on* 开头,和组件的生命周期函数名称一模一样,这一类测试函数不是等到组件onReady 以后开始运行,而是当组件生命周期函数运行时被触发。函数接收两个参数,第一个为组件的对象引用,第二个为run 函数。比如某个组件有一个onLoad 测试函数,那么当组件将要运行onLoad 生命周期函数时,先触发onLoad 测试函数,在测试函数内部调用run() 函数,继续执行组件的生命周期函数,run() 函数返回的数据就是生命周期函数返回的数据,如果返回的是Promise,则代表生命周期函数是一个异步函数,测试函数也可以写为async 异步函数,等待生命周期函数结束。这样我们就可以获取run()前后两个状态数据,最后对比,来测试生命周期函数的运行是否正确。

第三类测试函数与生命周期测试函数类似,是以 handle* 开头,用以测试事件处理函数是否正确,是在对应事件发生时运行测试。例如:

// src/components/counter/counter.test.js


export functionhandleTap(c, run) {

  let num= c.data.num;

  run();

  let step= c.data.num- num;

  if (step!==1) {

    thrownewError('计数器点击一次应该自增1,但是自增了'+ step);

  }

}

生命周期测试函数和事件测试函数只会执行一次,自动化测试的结果将会输出到Console控制台。


项目配置文件

labrador create 命令在初始化项目时,会在项目根目录中创建一个.labrador 项目配置文件,如果你的项目是使用 labrador-cli 0.3 版本创建的,可以手动增加此文件。

配置文件为JSON5格式,默认配置为:

{

  "define":{

    "API_ROOT":"http://localhost:5000/"

  },

  "npmMap":{

    "lodash-es":"lodash"

  },

  "uglify":{

    "mangle": [],

    "compress": {

      "warnings":false

    }

  },

  "classNames": {

    "text-red":true

  },

  "env":{

    "development": {},

    "production": {

      "define":{

        "API_ROOT":"https://your.online.domain/"

      }

    }

  }

}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值