[WeGit 微信小程序 5th] 小程序项目结构和框架

502 阅读5分钟

项目地址: github.com/mirrorhanyu…

扫码体验

mini-program-qrcode

小程序项目按照官方推荐的代码组织结构

WeGit
├── .editorconfig
├── .eslintrc
├── global.d.ts
├── package.json
├── project.config.json
├── tsconfig.json
├── config
│   ├── dev.js
│   ├── index.js
│   └── prod.js
└── src
    ├── app.scss
    ├── app.tsx
    ├── common.scss
    ├── index.html
    ├── actions
    ├── components
    │   ├── activity
    │   ├── common
    │   ├── footer
    │   ├── search
    │   ├── trending
    ├── constants
    ├── pages
    ├── reducers
    ├── store
    ├── types
    ├── utils
    └── wemark

生命周期和事件回调函数

Taro 遵循 React 语法规范,同样是采用组件化思想,并保持了一致的组件生命周期
同时,小程序每个页面指定页面的初始数据、生命周期回调、事件处理函数等,详细看参考官方文档

React 基础

在我们开始之前,我们可以简单的回顾一遍 React 基本概念。

元素 (Elements)

我们知道,在 React 应用中

Elements are the smallest building blocks of React apps.

翻译过来,element 就是 React 中最小的单元。 它描述了屏幕上应该展现什么内容。

const element = <h1>Hello, world</h1>;

element 是 immutable(不可变的),定义好后就决定了长什么模样。
如果把一个不断变化的 Component (比如一个倒计时的组件)看作一个不断放映的电影,element 就是其中的一帧,描述那一秒屏幕上该展示什么。
想要在屏幕上展现一个element ,用 ReactDOM.render(element, document.getElementById('root’)) 去更新就可以了。

组件化 (Components)

组件化的思想是指把所看到的 UI,拆分为独立的,可以复用的小组件。
Component 可以理解成一个函数,输入就是 props,输出就是上面提到的 elements。比如:

function Welcome(props) {
   return <h1>Hello, {props.name}</h1>;
}

如果采用 ES6 的语法:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

当我们想要渲染一个 Component 的时候,

const element = <Welcome name=“Han"/>;
ReactDOM.render(
  element,
  document.getElementById('root')
);

{name: 'Han'} 就是输入的 props,<h1>Hello, Han</h1> 就是输出,render函数会渲染在对应的节点上
React 只会更新需要更新的节点

Props

React 相当之灵活,但是有一个非常严格的规则:

All React components must act like pure functions with respect to their props.

React 要求我们不能改变 props,必须是一个纯函数 (pure)。输入不改变的话,如何做到改变输出,从而使 UI 动态改变呢?
于是我们就有了,

State

State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
State 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。
具体可以参考这个例子
需要注意的三点:

  1. setState() 去修改 state
  2. setState() 异步的,出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。
  3. setState() 合并是浅合并 因为State是私有的,如果想要传递 State,只能是”自上而下”的通过props作为输入参数向子组件传递。这种通常会成为单向数据流。

那么,如果两个组件都需要一个同样的数据源呢,或者兄弟组件会通信呢?
通常,我们可以可以将它提升至这些组件的最近共同父组件中。但是,还有别的方法吗?

Flux

Flux 是 facebook 提出的一种网页端设计架构(Architecture)
Flux 核心理念是实现了一种单向的数据流。简单来说,就是当用户进行操作的时候,会从组件 dispatch 一个 action,这个 action 流向 store,store 基于 action 对状态进行改动,然后 store 又触发组件基于新的状态重新渲染。
这样的实现,可以帮助视图和业务逻辑的分离。业务逻辑控制中心数据源,视图基于数据源渲染。
flux-unidirectional-data-flow

Redux

Redux 是基于 Flux 思想的一种实现。
Redux 的三个基本原则(Three Principles)

  1. 单一数据源 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
  2. State 是只读的 唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
  3. 使用纯函数来执行修改 为了描述 action 如何改变 state tree ,你需要编写 reducers。

Redux 的单项数据流是这样的:

  1. 调用 store.dispatch(action)
  2. Redux store 把当前的 state 树和 action 传给 reducer 函数做计算下一个 state。
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

同时,为了从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件, React Redux 库提供了 connect()方法。 connect 可接受如下参数

  1. mapStateToProps 这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中
  2. mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法

小程序首页数据是如何展示的

首先 connect 方法,mapStateToProps(),mapDispatchToProps()

@connect(({trending}) => ({
  trending
}), (dispatch) => ({
  fetchDevelopers(since, language) {
    dispatch(fetchDevelopers(since, language))
  },
  fetchRepositories(since, language) {
    dispatch(fetchRepositories(since, language))
  },
  fetchLanguages() {
    dispatch(fetchLanguages())
  }
}))

在 Component 的 componentDidMount 生命周期回调函数里获取 Trending language(火热的开发语言)

this.props.fetchLanguages()

dispatch 会分发对应的 action 出去

export const fetchLanguages = () => {
  return async (dispatch) => {
    try {
      dispatch({ type: FETCH_TRENDING_LANGUAGES_PENDING})
      const response = await Taro.request({
        url: 'https://example.com/trending-languages',
        method: 'GET'
      })
      dispatch({ type: FETCH_TRENDING_LANGUAGES_FULFILLED, payload: [ ...response.data] })
    } catch (e) {
      dispatch({ type: FETCH_TRENDING_LANGUAGES_REJECTED, payload: e })
    }
  }
}

对应的 reducer 会处理收到当前状态树和 action,从而计算下一个 state

export default function activity(state = {} as ActivityState, action) {
  switch (action.type) {
    case FETCH_ACTIVITIES_PENDING:
      return {
        ...state, isActivitiesUpdated: false
      }
    case FETCH_ACTIVITIES_REJECTED:
      return {
        ...state, isActivitiesUpdated: true
      }
    case FETCH_ACTIVITIES_FULFILLED:
      return {
        ...state,
        isActivitiesUpdated: true,
        activities: action.payload,
        username: action.addition.username,
        maxPagination: action.addition.maxPage,
        currentPagination: action.addition.currentPagination
      }
   }
}

还记得 connect 里的 mapStateToProps 吗?
当 state 变化,Component 里的 props 也会改变,从而使得 Component 重新渲染。

taro build

在本地开发的时候

yarn dev:weapp //taro build --type weapp —watch 会编译预览及打包,并会监听文件修改
yarn build:weapp //taro build --type weapp 不会监听文件修改,但会对代码进行压缩打包

实测过程中,build:weapp 把生成的小程序从1000+kb 压缩到 ~700kb
但是 build 之后可能会有些许区别,需要自行 End to end 测试
比如,build 出来的小程序 Taro response 中,header 中的 Max-Page 会解析成 max-page

持续优化的

  • 项目中 component 拆分可以更细致
  • actions 重复代码
  • sass 管理
  • connect 计算属性
  • 测试

参考链接

  1. Flux In-Depth Overview
  2. Redux Motivation
  3. Why Use React Redux
  4. React Performance Optimizing