阅读 9971

写给跨端玩家:支撑淘宝上亿日活的跨端框架—— Rax 的入门教程(附 TODO Demo)

一些废话

沉寂了两个月,我又回来了。

跟你们猜的一样,我已经到淘系实习了一段时间了,从上一篇文章之后就放了更多的心思在工作上。上篇文章发出去之后,我去腾讯实习了一段时间,等待阿里实习生入职流程开启。

收到淘系的实习生 offer 后,我买了人生中的第一张机票,第一次坐上了飞机,来到了一个陌生的城市——杭州。干净的街道、宽敞的沥青马路,吸引了我这个来自小城市的年轻人。

博客很久没更新了,不管是个人的网站还是掘金,都很少有更新了,偶尔上掘金看看一些好文,总想更新一下,但又没找到好的题材。现在实习了一段时间后,上手了淘系的开源跨端框架——Rax,新奇又好玩,也总结了一些很基础的开发技巧,补充一下官方文档的缺漏(官方文档对于新手来讲确实不太友好)。

基础部分演示项目 Git 仓库:Rax-TODO

进阶技巧演示项目 Git 仓库:Software-Engineering(同时也是我的课程设计,欢迎大伙点个 Star

前面的部分是针对只有一点 React 基础的同学的,高端玩家请点击:进阶技巧

项目环境

  1. node >=10.3.0
  2. npm >= 6.1.0

前置知识

  1. JSX
  2. Hooks
  3. TypeScript(可选)
  4. Jest(可选)
  5. Enzyme(可选)

创建项目

初始化项目

这部分官网文档比较详细了,先看一下官网文档:快速开始

官网文档提供的方案还挺多的,可能会选择困难,我们就从最基础的 TODO 开始,使用 Rax 搭建一个 Web 项目:

npm init rax todo-list
复制代码

选择项目类型

输入上面的命令之后,会使用 npx 安装 rax-cli 脚手架工具,安装完成后会弹出这样的界面:

added 106 packages from 53 contributors in 10.5s
? What's your project type? (Use arrow keys)
❯ App (Build universal application)
  Component (Build universal component)
  API (Build universal API library)
  Plugin (Build plugin for miniapp)
复制代码

使用 上下键 移动箭头,后面的操作同理。

这里我们就直接使用默认的选项,创建一个 APP,按下回车。

选择应用运行平台

? Choose targets your project want to run? (Press <space> to select, <a> to togg
le all, <i> to invert selection)
❯◉ Web
 ◯ Weex
 ◯ Kraken (Flutter)
 ◯ Alibaba MiniApp
 ◯ WeChat MiniProgram
复制代码

使用 空格键 选择,按下 字母a 可以全选,这里我们就先选择 Web,后续如果有编译为 Weex小程序 的需求可以在项目目录的 build.json 中添加。

编译为 Flutter 应用的功能目前还不稳定,且坑比较多,建议动手能力强的玩家尝试,小白就先绕道吧。

选择应用类型

? What's your application type? (Only valid in target: Web/Weex/Kraken) (Use arr
ow keys)
❯ Single-page application (SPA)
  Multi-page application (MPA)
  Create lite application (The simplest project setup)
复制代码

选择 APP 的类型,是 SPA 还是 MPA,这里的选项只在 Web/Weex/Kraken(Flutter) 应用中有效。

我们选择默认的选项 SPA,不明白 SPAMPA 的区别的同学,可以移步文章:认识单页应用(SPA)与多页应用(MPA)

输入作者名字

? What's author's name? (rax): 炽翎
复制代码

选择开发语言

? What type of language do you want to use? (Use arrow keys)
❯ JavaScript
  TypeScript
复制代码

这里我们就先选择 JavaScript,后续如果有引入 TypeScript 的需求,可以手动引入。

开启特性

? Do you want to enable these features? (Press <space> to select, <a> to toggle
all, <i> to invert selection)
❯◯ Server-side rendering (SSR) (Only valid in target: Web)
 ◯ Aliyun Function Compute (FaaS) (Only valid in target: Web)
 ◯ Compatibility with React
复制代码

这里我们就什么都不选。

当然,如果有需要我们可以选择 Compatibility with React 配置与 React 的兼容,也可以为我们的应用开启 服务端渲染(SSR)。如果有需求开启 Serverless,也可以选择开启 功能即服务(FaaS)SSRFaaS 都只能在 Web 应用中有效。

不懂 SSRServerless 的同学请自行百度(Google)哈。

自动安装依赖

? Do you want to install dependences automatically after initialization? (Y/n)
复制代码

这里就直接回车,这个选项的意思是询问是否在初始化完成后自动安装依赖。默认是 Yes,就不用我们手动进入项目文件夹执行 npm install 了。

等待依赖安装完成

To run your app:
   cd todo-list
   npm start
复制代码

当终端出现这一段文字的时候,就说明依赖安装完了,我们可以进入到项目的文件夹下,执行 npm start 跑起我们的项目。

启动项目

在执行 npm start 后,终端会显示:

Rax development server has been started:

[Web] Development server at:
    http://localhost:3333/

复制代码

这时候我们就可以在浏览器输入 Dev Server 的地址,查看我们的项目了。

Home

开始开发

随便选择一款自己喜欢的 IDE,进入项目的文件夹,我们会看到这样的一个目录结构:

.
├── README.md                   # 项目说明
├── build.json                  # 项目构建配置
├── package.json
└── src                         # 源码目录
    ├── app.js                  # 应用入口文件
    ├── app.json                # 应用配置,包括路由配置,小程序 window 配置等
    ├── public                  # (可选)静态资源目录,会拷贝内容至 build 目录
    ├── components              # 应用的公共组件
    │   └── Logo                # 组件
    │       ├── index.css       # Logo 组件的样式文件
    │       └── index.jsx       # Logo 组件 JSX 源码
    ├── document                # 页面的 HTML 模板
    │   └── index.jsx
    └── pages                   # 页面
        └── Home                # home 页面
            └── index.jsx
复制代码

开发之前,我们需要了解一些 Rax 下的一些小规矩:

  • rpx:默认以 750rpx 为屏幕宽度,即 1rpx = 1/750 * 屏幕宽度
  • 样式简写:在 Rax 中,由于目前兼容性的问题,不支持 部分 样式简写,例如:在写 border 样式时,应该将各个部分分开:border-widthborder-styleborder-color。在遇到属性简写在非 Web 平台不生效的问题时,尝试将属性分开或许就能解决问题。

删掉初始页面

我们要做的第一件事是删除初试化后默认的 Home 页面中的 Logo 组件,在 src/components 文件夹下,整个删除。

删除之后,src/pages/Home/index.jsx 肯定会报错,我们先稍作修改,改成 Hello World!

// index.jsx
import { createElement } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';

import './index.css';

export default function Home() {
  return (
    <View className="home">
      <Text>Hello World!</Text>
    </View>
  );
}
复制代码

这个时候会发现页面变成了 Hello World!,后面应该不用讲解太多,就是使用 React 应用开发的方式,开始愉快的 coding 过程。

可能你会发现,为什么在这里我们只能用 Rax 提供的 ViewText 组件?

因为为了使跨端显示效果一致,Rax 为开发者抹平了不同平台之间样式显示不一致的问题。

当然,如果你只是想用 Rax 做一个 Web 应用,那你可以使用 HTML 标签(汗)。

View 组件和 Text 组件的用法可以移步文档:基础组件-View基础组件-Text

创建 Item 组件

按照 耦合度,我们应该将组件放在 src/pages/Home 文件夹下。

在这个文件夹下,我们创建一个这样的目录:

src/pages/Home
└── components                  # 组件文件夹
    └── ListItem                # ListItem 组件
        ├── index.css           # CSS
        └── index.jsx           # JSX
复制代码

首先我们应该构思一下这个 ListItem 应该暴露给父组件哪些接口:

  1. 每一项对应的 id
  2. 每一项的内容
  3. 每一项的完成状态
  4. 每一项的 onClick 事件

明确了组件应该暴露的接口之后,就可以开始写代码了:

// index.jsx
import { createElement } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';

import './index.css';

const ListItem = (props) => {
  // 解构 id 完成状态 内容 onClick 事件
  const { id, done, content, onClick } = props;

  // 完成项文字样式
  const style = {
    fontSize: '64rpx',
    lineHeight: '96rpx',
    textDecoration: done && 'line-through'
  };

  return (
    <View className="list-item" onClick={() => onClick(id)}>
      <View className="list-dot"></View>
      <Text style={style}>{content}</Text>
    </View>
  );
};

export default ListItem;
复制代码
/* index.css */
.list-item {
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
  width: 100%;
  height: 100rpx;
}

.list-dot {
  width: 20rpx;
  height: 20rpx;
  margin-right: 20rpx;
  border-radius: 10rpx;
  background-color: #333;
}
复制代码

创建 List 组件

还是一样,先创建文件:

src/pages/Home
└── components                  # 组件文件夹
    ├── List                    # List 组件
    │   ├── index.css           # CSS
    │   └── index.jsx           # JSX
    └── ListItem                # ListItem 组件
        ├── index.css           # CSS
        └── index.jsx           # JSX
复制代码

创建一个 List 组件作为 ListItem 的容器管理所有的 Item,实现 添加/删除 的功能。

既然要做 添加 功能,那输入框肯定是必不可少的,在我们创建的项目里,默认是没有输入框组件的依赖的,所以我们要先安装 Rax 提供的输入框组件:

npm install rax-textinput --save
复制代码

组件的用法可以转战文档:基础组件-TextInput

// index.jsx
import { createElement, useState } from 'rax';
import View from 'rax-view';
import Text from 'rax-text';
import TextInput from 'rax-textinput';

import ListItem from '../ListItem';
import './index.css';

const List = () => {
  // 初始化 itemId 每次添加新列表项就 +1
  const [itemId, setItemId] = useState(0);
  // 初始化列表
  const [list, setList] = useState([]);
  // 初始化 TextInput 内容
  const [inputValue, setInputValue] = useState('');

  // 输入框输入事件
  const handleUserInput = (e) => {
    setInputValue(e.target.value);
  };

  // 添加按钮点击事件
  const handleAddButtonClick = () => {
    // 构造列表项数据结构
    const item = {
      id: itemId,
      content: inputValue,
      done: false
    };
    // immutable 思想 生成新的引用
    const newList = [...list, item];
    setList(newList);
    // 清空输入框
    setInputValue('');
    // itemId ++
    setItemId(itemId + 1);
  };

  // 列表项点击事件
  const handleItemClick = (id) => {
    // 遍历列表 当事件未完成时标记为已完成 当事件已完成时删除
    const newList = list.filter((item) => {
      if (item.id === id) {
        if (item.done) {
          return false;
        } else {
          item.done = true;
        }
      }
      return true;
    });
    setList(newList);
  };

  return (
    <View className="list">
      <View className="list-input-wrapper">
        <TextInput
          className="list-input"
          value={inputValue}
          onInput={handleUserInput}
        />
        <View className="list-add-button" onClick={handleAddButtonClick}>
          <Text>添加</Text>
        </View>
      </View>
      <View className="list-item-wrapper">
        {list.map((item) => (
          <ListItem
            key={item.id}
            id={item.id}
            content={item.content}
            done={item.done}
            onClick={handleItemClick}
          />
        ))}
      </View>
    </View>
  );
};

export default List;
复制代码
.list {
  align-items: center;
}

.list-input-wrapper {
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
}

.list-add-button {
  justify-content: center;
  align-items: center;
  width: 150rpx;
  height: 100rpx;
  margin-left: 20rpx;
  border-radius: 10px;
  background-color: #dcdcdc;
}

.list-input {
  width: 100%;
  height: 100rpx;
  line-height: 96rpx;
  font-size: 64rpx;
  border-width: 1px;
  border-color: #dcdcdc;
}

.list-item-wrapper {
  width: 100%;
}
复制代码

至此,组件的基本功能就完成了。

引入首页

现在我们要将组件引入首页 Home 中:

import { createElement } from 'rax';
import View from 'rax-view';

import List from './components/List';
import './index.css';

export default function Home() {
  return (
    <View className="home">
      <List />
    </View>
  );
}
复制代码

现在就能看到一个能完成基本功能的 TODO 应用了。

进阶技巧

我猜,有些高玩看到我前面写的部分,肯定会说:

啊~ 你这个这么简单,不是有手就行嘛~

有一说一,确实有手就行(误)。

前面的部分只是写给不爱读官方文档或者只有一点 React 基础的同学的,从这个部分开始,会开始引入一些高级的内容。

修改路由配置

在前面创建的项目中,我们会看到项目的文件夹下有一个 app.json 文件,打开它你会看到:

{
  "routes": [
    {
      "path": "/",
      "source": "pages/Home/index"
    }
  ],
  "window": {
    "title": "Rax App"
  }
}
复制代码

这个文件就是用来控制项目路由,如果后续的开发增加了更多的页面,就需要在 routes 下添加对象。path 就是你新加入的页面的访问路径(URL),source 就是你新添加的页面所在的文件目录。

修改为多页应用(MPA)

在项目文件夹下还有一个 build.json 文件,里面的内容是这样的:

{
  "plugins": [
    [
      "build-plugin-rax-app",
      {
        "targets": ["web"]
      }
    ]
  ]
}
复制代码

因为我们之前创建项目的时候,选择的是单页应用(SPA),我们想改成多页应用怎么办? 很简单:

先安装多页应用依赖:

npm install build-plugin-rax-multi-pages --save-dev
复制代码

然后在 targets 所在的对象下,增加一个键 "type",值为 "mpa",像这样:

{
  "plugins": [
    [
      "build-plugin-rax-app",
      {
        "targets": ["web"],
        "type": "mpa"
      }
    ]
  ]
}
复制代码

重启 Dev Server 后会发现页面变成了这样:

MPA

添加跨端支持

我们在最开始创建项目的时候,并没有选择多平台,但是 Rax 作为一个跨端开发框架,不添加跨端支持怎么行,这里会给一个教程教大家添加跨端支持(当然一开始就选择好更方便)。

还是 build.json 文件,有一个 targets 数组,想要添加跨端支持,就在数组中添加对应的内容即可:

{
  "plugins": [
    [
      "build-plugin-rax-app",
      {
        "targets": ["web", "weex", "miniapp", "wechat-miniprogram"],
        "type": "mpa"
      }
    ]
  ]
}
复制代码

weex 对应 Weex 平台,miniappwechat-miniprogram 对应 阿里小程序微信小程序

查看 Weex 效果

我们在 build.json 中使用了上面的配置后,页面会变成这个样子:

Weex

点开 Weex Preview 后,你会发现是一堆 JavaScript 代码,那怎么预览?

有一个稍微麻烦一点的方法,就是先确定你本机在内网的 IP 地址,将 URL 中的 localhost 改成内网 IP,然后将 URL 转换为二维码(Chrome 有很多插件可以实现)。

在手机上下载 Weex Playground 然后扫描二维码,就能在手机上预览到 APP 的效果。

Weex Playground 下载地址

TypeScript 支持

在前面的项目里,我们没有引入 TypeScript,那如果想引入应该怎么办?

很简单,在官网的文档中也有讲解:项目开发- TypeScript 支持

只需要先安装 rax-typestypescript

npm install rax-types typescript --save-dev
复制代码

在项目文件夹下创建 tsconfig.json 文件,使用以下配置:

{
  "compilerOptions": {
    "module": "esNext",
    "target": "es2015",
    "outDir": "build",
    "jsx": "preserve",
    "jsxFactory": "createElement",
    "moduleResolution": "node",
    "sourceMap": true,
    "alwaysStrict": true,
    "baseUrl": ".",
    "paths": {
      "rax": ["node_modules/rax-types"]
    }
  },
  "include": ["./src/**/*"]
}
复制代码

即可添加 TypeScript 支持。

引入自动化测试

这是一个比较大的坑,以至于让我摸索了一个上午才解决。

Rax 官方文档中并没有自动化测试相关配置的教程,以至于我只能直接在钉钉找负责 Rax 维护的师兄求助,历经千辛万苦终于将测试用例跑通。

首先,我们需要安装几个依赖,数量有点多,建议分开安装:

npm install jest --save-dev
npm install @types/jest --save-dev
npm install @babel/preset-env --save-dev
npm install babel-jest --save-dev
npm install rax-test-renderer --save-dev
npm install @babel/plugin-proposal-class-properties --save-dev
npm install @babel/plugin-proposal-decorators --save-dev
npm install @babel/plugin-proposal-export-default-from --save-dev
npm install @babel/preset-flow --save-dev
npm install @babel/preset-react --save-dev
npm install babel-plugin-transform-jsx-stylesheet --save-dev
复制代码

依赖安装好之后,在项目的目录下创建 .babelrc.jsBabel 进行配置:

module.exports = function (api) {
  // Cache the returned value forever and don't call this function again.
  if (api) api.cache(true);

  return {
    presets: [
      '@babel/preset-flow',
      [
        '@babel/preset-env',
        {
          loose: true
        }
      ],
      [
        '@babel/preset-react',
        {
          pragma: 'createElement'
        }
      ]
    ],
    plugins: [
      '@babel/plugin-proposal-export-default-from',
      ['@babel/plugin-proposal-class-properties', { loose: false }],
      'babel-plugin-transform-jsx-stylesheet',
      ['@babel/plugin-proposal-decorators', { legacy: true }]
    ],
    ignore: ['build', 'coverage', 'node_modules']
  };
};
复制代码

配置好之后,Jest 还是无法跑通的,要在 package.json 中添加启动脚本:

{
  "scripts": {
    "build": "build-scripts build",
    "start": "build-scripts start",
    "lint": "eslint --ext .js --ext .jsx ./",
    // 添加 jest 启动脚本
    "test": "jest"
  }
}
复制代码

同时,还要对 Jest 进行配置:

{
  "jest": {
    "collectCoverage": true,
    "moduleNameMapper": {
      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
      "\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
    }
  }
}
复制代码

配置添加完成之后,在项目的目录下创建一个文件夹:

.
└── __mocks__
    ├── fileMock.js          # 静态资源处理
    └── styleMock.js         # 样式处理
复制代码

分别在两个文件中加入添加代码:

// fileMock.js
module.exports = 'test-file-stub';
复制代码
// styleMock.js
module.exports = {};
复制代码

配置这个部分的目的是让 Jest 处理样式和静态资源时到 mock 的文件夹下找。因为在测试的时候,测试脚本并不会真的去访问引入的静态资源,遇到需要引用静态资源的地方就引导到 __mocks__ 文件夹下查找,就不会因为找不到资源而报错。配置来源于 Jest 官网处理静态文件

一切配置完成后,就可以在一个需要做测试的组件下创建 __test__ 文件夹并将测试脚本放在文件夹下了,或者你也可以直接使用 xxx.test.jsx 为文件命名,这些文件都会被 Jest 识别为测试脚本。

除了 Jest,Rax 团队还为 Rax 提供了 Enzyme 适配器,示例项目:raxjs/enzyme-adapter-rax,提供了 Enzyme 的使用示例。

总结

得益于淘系强大的技术实力,Rax 的相关生态相对比较完善。同时,性能也非常强大,这里我就不过多介绍了。

Rax 在小程序上的性能甚至优于市面上其他小程序框架:Rax ——完美融合编译时与运行时的双引擎小程序框架

其他的一些介绍 Rax 的文章可以移步官网的博客

文章的最后,推荐一下自己的公众号:Hello FE,是我和跟我一起实习的小伙伴们一起运营的,会定期分享一些干货。

同时,关注公众号还有前端书籍大全一份赠送,欢迎大家关注,也欢迎大家进群交流:

公众号