前言
最近在学习 Rax 官方文档,本人自打前端以来就紧跟尤大大的脚本,对于 react 是零基础。本篇记录个人阅读 Rax 官方文档需要学习的地方,以及记录实战过程遇到的情况。
在此之前跟着 写给跨端玩家:支撑淘宝上亿日活的跨端框架—— Rax 的入门教程(附 TODO Demo) 文章(这文章名字是真的长)过了一次入门案例。
基础指南
目录结构
.
├── .rax/ # 运行时生成的临时目录
├── build/ # 构建产物目录
├── public # 本地静态资源
│ └── favicon.png
├── src
│ ├── app.json # 路由及页面配置
│ ├── app.ts # [小程序|SPA]应用入口
│ ├── miniapp-native/ # [小程序]小程序原生代码
│ ├── components/ # 自定义业务组件
│ ├── pages/ # 页面
│ ├── models/ # 应用级数据状态
├── build.json # 工程配置
├── package.json
└── tsconfig.json
复制代码
-
git 提交时,应忽略
.rax/
临时目录 -
pages
:项目的页面文件目录,推荐的目录形式如下。Home/ # Home 页面 ├── model.ts # 页面级数据状态 ├── index.tsx # 页面入口 └── index.module.scss # 页面样式文件 复制代码
- 注意页面样式文件为
index.module.scss
而非index.scss
- 在当前页面中创建
components
目录存放当前页面级的组件。
- 注意页面样式文件为
-
/src/components
:目录存放当前项目级的公共组件,供其他页面或其他组件使用。 -
models/
:项目的全局数据模型目录,通常包含多个 model 文件。-
models/ ├── foo.ts └── bar.ts 复制代码
-
-
app.json:负责应用的运行时相关配置,如 routes、小程序 window 配置等。
-
build.json:是整个应用构建相关的配置文件,相关配置可参考工程配置。
-
tsconfig.json:TypeScript 编译所需的配置文件。
调试
package.json 会配置以下命令
{
"scripts": {
"start": "rax-app start",
...
}
}
复制代码
-
执行
npm run start
即可进行项目开发 -
正常情况下执行命令后默认自动打开浏览器
http://localhost:3333
进行页面预览 -
热更新:修改源码后浏览器会自动刷新页面。
-
start 命令支持的完整参数如下:
-
$ rax-app start --help Usage: rax-app start [options] Options: -p, --port <port> 服务端口号 -h, --host <host> 服务主机名 --config <config> 指定配置文件 --https 支持开启 https --analyzer-target 指定需要进行构建分析的任务,例如 `--analyzer-target=web` --analyzer-port <port> 支持定制构建分析端口 --disable-reload 禁用热更新模块 --disable-mock 禁用 mock 服务 --disable-open 禁止浏览器默认打开行为 --enable-assets 开启命令行输出 webpack assets 信息 --mpa-entry 指定多页应用需要编译的页面 复制代码
-
例子:修改 package.json 中的命令
"start": "rax-app start --port 3000"
-
当端口占用冲突时,rax 会提示是否运行在下一个端口号。
-
构建
package.json 会配置以下命令
{
"scripts": {
"build": "rax-app build",
...
}
}
复制代码
-
执行
npm run build
进行项目构建,构建产物默认输出到./build
目录下。 -
build 命令支持的完整参数如下:
-
$ rax-app build --help Usage: rax-app build [options] Options: --analyzer-target 指定需要进行构建分析的任务,例如 `--analyzer-target=web` --analyzer-port <port> 支持定制构建分析端口 --config <config> 指定配置文件 复制代码
-
问题
- 首次执行
npm run build
遭遇报错:PostCSS plugin postcss-discard-comments requires PostCSS 8.
:错误:postss插件postss discard comments需要postss 8。
这个错误就是工程化化插件的
postcss-discard-comments
和postcss
的版本不匹配,因此有两种解决方案:
- 降低
postcss-discard-comments
版本- 需要知道该插件对应的低版本,不确定多低的版本可以打包构建
- 安装最新版本的
postcss
npm i postcss -S
- 结果再次执行
npm run build
,可以打包了,但是node
插件构建 postcss 时报错。
查询了好久的资料,在 github.com/mrnocreativ… 这里找到了答案。
大抵原因的意思是:PostCSS8 与 node 的 npm 不兼容,下面有个回答说
npm is still using postcss 6.0.1
,npm 当时的版本才对标postcss 6.0.1
版本
最后我去找 Rax 的钉钉社区询问了一下,很快得到了答案:cnpm 已经锁了版本,可以使用 cnpm 来进行安装插件
。也就是说 cnpm 安装的插件对其 css 的插件进行了降级处理,就处理了兼容问题。
解决
- 删除 node_modules 文件夹。
- 使用
cnpm install
进行依赖安装。 - 安装成功后执行
npm run build
小插曲
我在利用谷歌直接打开 build 打包好的 index.html 文件时,报错 from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.
跨域问题。
原因是谷歌
file
协议访问本地的文件时都会有跨域的问题
解决方法如下有以下几种。
-
搭建本地服务器,跑在服务器上访问本地文件。
-
利用 idea 插件打开,如 webStrom 中打开 html 文件时,会自动开启服务。
-
anywhere
插件开启服务: -
全局安装 anywhere 插件:
npm i anywhere
-
进入对应文件夹目录下跑动服务:
anywhere
-
将当前目录变成一个静态文件服务器的根目录,即可解决浏览器访问本地文件的跨域问题。
工程配置
rax-app 工程构建相关的配置默认都收敛在项目根目录的 build.json
文件中, 默认采用 JSON 格式配置。
详情于 官方文档
环境配置
Rax App 支持区分不同环境,开发者可根据环境区分运行时配置。
设置环境
默认情况下支持 start/build
两个环境。对应的即 rax-app start/build
两个命令,开发者可以通过 --mode
参数来扩展环境:
{
"scripts": {
"start": "rax-app start --mode local",
"build:daily": "rax-app build --mode daily",
"build": "rax-app build --mode prod"
}
}
复制代码
区分工程配置
定义好环境,我们就要根据环境来区分配置。
在
build.json
通过modeConfig
属性
{
"modeConfig": {
"prod": {
"define": {},
"minify": false
},
"daily": {
"define": {},
"minify": true
}
}
}
复制代码
同时在本地插件 build.plugin.js
也可以从 context 上获取到当前 mode:
module.exports = ({ context }) => {
const { command, commandArgs } = context;
const mode = commandArgs.mode || command;
}
复制代码
区分运行时配置
在定义好环境之后,前端代码中即可通过 APP_MODE
拿到当前环境:
import { APP_MODE } from 'rax-app';
console.log('APP_MODE', APP_MODE);
复制代码
当然大多数时候你都不需要关心 APP_MODE
这个变量,只要按照约定的方式区分不同环境的配置即可。在 src/config.ts
中编写不同环境的配置:
// src/config.ts
export default {
// 默认配置
default: {
appId: '123',
baseURL: '/api'
},
local: {
appId: '456',
},
daily: {
appId: '789',
},
prod: {
appId: '101',
}
}
复制代码
配置之后框架会自动根据当前环境将配置进行合并覆盖,开发者只需要在代码中直接使用 config
即可:
import { config } from 'rax-app';
console.log(config.appId);
复制代码
在期间遇到了 eslint 报错
Unexpected console statement no-console
,需要设置 eslint"no-console":"off"
或window.console.log()
,不知道怎么配置 Rax 框架中的.eslintrc.js
文件。待有更好的方法学习记录
动态扩展运行时环境
应用有日常/预发两个运行环境,但实际上只能进行一次构建任务,此时则可以通过运行时扩展环境来支持不同配置。
在 src/config.ts
中通过域名来扩展环境:
// src/config.ts:动态扩展环境:两种方式
// 方式 1. 通过服务端输出到页面上的全局变量
window.__app_mode__ = window.g_config.faasEnv; // window.g_config.faasEnv 也可能是 window.__env__,具体看服务端怎么约定
// 方式 2. 通过 url 地址动态判断
if (/pre.example.com/.test(window.location.host)) {
window.__app_mode__ = 'pre';
} else if (/daily.example.com/.test(window.location.host)) {
window.__app_mode__ = 'daily';
} else if (/example.com/.test(window.location.host)) {
window.__app_mode__ = 'prod';
} else {
window.__app_mode__ = 'local';
}
export default {
default: {},
daily: {},
pre: {},
prod: {}
};
复制代码
无接口,暂时做不了实操,设置好全局变量 __app_mode__
即可。
编写组件
Component
组件,在概念上类似于 Javascript 函数。它接受从父级元素传入的数据(即 Props),并返回用于描述页面展示内容的 Rax 元素。组件使得我们的UI界面分成独立的、可重用的部分,并且,每个部分可以单独维护。
定义组件有两种方式,分别为 Function Component
, 和 Class Component
。
Function Component
是定义组件最简单的方式,这种方式会让代码变得更加简洁,并且可以使用 Hooks 编写更易维护和扩展的组件。我们推荐用 Function Component 来定义组件:
import { createElement } from 'rax';
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
复制代码
另外,我们也可以使用 ES6 的 class 来编写 Class Component:
import { createElement, Component } from 'rax';
class Welcome extends Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
复制代码
需要注意的是,Rax 组件在命名时首字母必须大写。首字母为小写的组件会被视为原生 DOM 标签。
实操
两个组件定义方式
-
Function Component 实操案例参考: 写给跨端玩家:支撑淘宝上亿日活的跨端框架—— Rax 的入门教程(附 TODO Demo)
-
Class Component:自己封装 Class 的案例
ListItem2
ListItem2/index.tsx
import { createElement, Component } from 'rax'; import View from 'rax-view'; import Text from 'rax-text'; class ListItem2 extends Component<{ // eslint-disable-line id?: Number; done?: Boolean; content?: String; onClick: (id?: Number) => void; }> { style = { fontSize: '64rpx', lineHeight: '96rpx', textDecoration: this.props.done && 'line-through', }; render() { return ( <View className="list-item" onClick={() => this.props.onClick(this.props.id)}> <View className="list-dot" /> <Text style={this.style}>{this.props.content}</Text> </View> ); } } export default ListItem2; 复制代码
List/index.tsx
调用
... import ListItem2 from '../ListItem2'; .... <View className="list-item-wrapper"> {list.map((item) => ( <ListItem2 key={item.id} id={item.id} content={item.content} done={item.done} onClick={handleItemClick} /> ))} </View> 复制代码
实现效果:
BUG
- node 版本不匹配
需要
node 版本 >= 13.9.0
esLinst
严格规则警告
因为
esLint
不推荐使用类组件,咱们只是一个实操,主要几个方法让 esLint 忽视文件中第五行的 class 声明即可。
Props
Props 为组件接受的入参,通过 Props, 组件可以接收来自父级节点的数据。Props 都是只读的,不能在当前组件中修改 Props 的值。
State
State 是组件的私有变量,由组件自身控制,并与 Props 一起控制组件的渲染。我们使用 useState
这一 Hook 来进行定义和管理 State 。在下面的例子里,我们使用了 useState
来实现了一个计数器:
import { createElement, useState } from 'rax';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
复制代码
useState
参数是初始 state,会返回一个数组。数组第一个值是 state,第二个值是改变 state 的函数。上述例子中 count 与 setCount 是一一配对的,count 的值只能通过 setCount 修改。
Fragment
Fragment 是一个特殊组件,可以在不创建额外 DOM 元素的情况下,将多个元素组合在一起。在 rax 组件渲染时,若需要渲染多个元素,需要使用 Fragment 将元素包裹在内。
import { createElement, Fragment } from 'rax';
function Hello() {
return (
<Fragment>
<h1>Hello</h1>
<h1>World</h1>
</Fragment>
);
}
复制代码
Fragment 组件只支持 key
属性 和 部分 JSX+ 属性即 x-if
x-for
x-slot
,不支持其他属性。当 Fragment 组件不需要填写属性时,我们也可以使用<></>
来代替。
import { createElement } from 'rax';
function Hello() {
return (
<>
<h1>Hello</h1>
<h1>LinM</h1>
</>
);
}
复制代码
<></>
标签需在最外面,不能被其他标签所包裹。否则如下返回:
Hooks
Hooks 是 Rax 1.0 新增的特性,它可以让函数组件(Function Component)使用 状态
和 生命周期
。Rax 在实现上遵循了 React Hooks 的标准。
常用 Hooks
useState
useState 主要用来定义和管理本地状态。在上面的例子里,我们使用了 useState
来实现了一个计数器:
useState
参数是初始 state,会返回一个数组。- 数组第一个值是 state,第二个值是改变 state 的函数。
- 上述例子中 count 与 setCount 是一一配对的,count 的值只能通过 setCount 修改。
useEffect
useEffect 的代码在每次渲染后运行,包括第一次渲染:
import { createElement, useState, useEffect } from 'rax';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
复制代码
useEffect
它跟类组件(Class Component)中的componentDidMount
、componentDidUpdate
和componentWillUnmount
具有相同的用途,只不过被合并成了一个 API。跟 Vue3 的
useEffect
API 效果十分相似,我也就进行了类比。
Hooks 使用规则
Hooks 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只在函数最顶层调用 Hooks,不要在循环、条件判断或者子函数中调用。
- 只在 Rax 函数组件 和 自定义 Hooks 中调用 Hooks,不要在其他 JavaScript 函数中调用。
JSX
const element = <p>Hello, world!</p>;
复制代码
这种类似于 HTML 标签的语法被称为 JSX。JSX 是一种 JavaScript 的语法扩展,在 Rax 中 我们使用 JSX 来描述页面结构。下面简单介绍 JSX 的基本使用方法。
JSX 的声明方式和普通 HTML 标签一样,用 <>
标签包裹,也可以嵌套:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
复制代码
假如标签内没有子元素,可以使用 />
来闭合标签,也就是单标签:
const element = <img src="url" />
复制代码
表达式
JSX 中可以插入任意 JavaScript 表达式。JSX中的表达式必须写在大括号 {}
中:
const element = <h1>{'Hello' + ',' + 'world!'}</h1>;
复制代码
表达式中不能使用 if else
语句,但可以使用三元运算符 a ? b : c
来实现条件选择。
const element = <h1>{true ? 'True!' : 'False!'}</h1>
复制代码
使用 JSX 数组
可以在一个 JSX 元素中直接嵌套包含多个元素的数组,数组内的 JSX 元素会被逐个渲染:
const arr = [
<span>Hello, world!</span>,
<span>Hello, Rax!</span>,
];
const element = <p>{arr}</p>;
复制代码
在 JSX 中注释
JSX 注释和表达式一样,必须写在大括号 {}
中:
const element = <p>{/*注释...*/} Hello, world!</p>;
复制代码
实操
import { createElement } from 'rax';
import View from 'rax-view';
function Jsx() {
const element = <h1>{'Hello, world!'}</h1>;
const arr = [
<span>Hello, world! </span>,
<span>Hello, Rax!</span>,
];
const comment = <p> { /* 注释... */ } Hello, world!</p>;
return (
<View>
普通声明:
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
表达式:
<div>
{element}
</div>
使用 JSX 数组:
<div>
{arr}
</div>
在 JSX 中注释:
<div>
{comment}
</div>
</View>
);
}
export default Jsx;
复制代码
JSX+ 语法
Rax 支持了一种 JSX 扩展语法 JSX+,它能帮助业务开发者更爽更快地书写 JSX。JSX+ 不是一种新的概念,它是 JSX 基础上的扩展指令概念。具体语法如下:
1. 条件判断
语法:
<View x-if={condition}>Hello</View>
<View x-elseif={anotherCondition}></View>
<View x-else>NothingElse</View>
复制代码
注: x-elseif
可以多次出现,但是顺序必须是 x-if -> x-elseif -> x-else,且这些节点是兄弟节点关系,如顺序错误则指令被忽略。
2. 循环列表
语法:
{/* Array or Plain Object*/}
<tag x-for={item in foo}>{item}</tag>
<tag x-for={(item, key) in foo}>{key}: {item}</tag>
复制代码
说明: 1. 若循环对象为数组,key 表示循环索引,其类型为 Number。 2. 当 x-for
与 x-if
同时作用在同一节点上时,循环优先级大于条件,即循环的 item
和 index
可以在子条件判断中使用。
3. 单次渲染
仅在首次渲染时会触发 createElement
并将其引用缓存,re-render 时直接复用缓存,用于提高不带绑定节点渲染效率和 Diff 性能。
语法:
<p x-memo>this paragragh {mesasge} content will not change.</p>
复制代码
4. 插槽指令
类似 WebComponents 的 slot 概念,并提供插槽作用域。
语法:
<tag x-slot:slotName="slotScope" />
复制代码
示例:
// Example
<Waterfall>
<view x-slot:header>header</view>
<view x-slot:item="props">{props.index}: {props.item}</view>
<view x-slot:footer>footer</view>
</Waterfall>
<slot name="header" /> // 槽位
复制代码
5. Fragment 组件
使用 x-if
x-for
x-slot
指令时,若不希望产生无意义的元素,我们可以使用 Fragment 组件。
使用:
<Fragment x-if={condition}>
<div />
</Fragment>
复制代码
6. 类名绑定
语法:
<div x-class={{ item: true, active: val }} />
复制代码
参考实现:
<div className={classnames({ item: true, active: val})} />
复制代码
classnames
方法能力参考同名 npm 包。
自定义 Hooks
通过自定 Hooks 能够将组件逻辑提取到可重用的函数中。
import { createElement, useState, useEffect } from 'rax';
const useDocumentTitle = function(title) {
useEffect(
() => {
document.title = title;
},
[title]
);
};
function Example() {
const [count, setCount] = useState(0);
useDocumentTitle(`You clicked ${count} times`)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
复制代码
上述示例中 useDocumentTitle 是一个自定义 Hooks,可以在其他组件中执行设定 title 的逻辑。如果函数的名字以 “use
” 开头并调用其他 Hooks,我们就说这是一个自定义 Hooks。
Hooks 与类组件生命周期的对应关系
至此相信您已经对 Hooks 有了基本的了解。对照 类组件的生命周期,可通过以下 Hooks 实现对应的生命周期:
- useEffect
- componentWillUpdate
- componentDidMount
- componentWillUnmount
- useLayoutEffect
- 与 useEffect 一致,用于获取元素渲染结果
- useMemo、useCallback
- shouldComponentUpdate
使用 useEffect 完成 componentWillUpdate 的效果,示例:
function DemoComponent() {
useEffect(() => {
// ...
});
return <p>Hello hooks!</p>
}
复制代码
使用 useEffect 可以实现 componentDidMount 的功能,示例:
function DemoComponent() {
useEffect(() => {
// useEffect 的第二个参数为[]时,表示这个 effect 只会在 componentDidMount、componentWillUnMount 的时候调用。
}, []);
return <p>Hello hooks!</p>
}
复制代码
使用 useEffect 可以实现 componentWillUnmount 的功能,示例:
function DemoComponent({source}) {
useEffect(() => {
const subscription = source.subscribe();
return () => {
// componentWillUnMount 调用的是第一个参数返回的回调。
subscription.unsubscribe();
};
}, [source]); // 表示 source 改变时就是执行一遍
return <p>Hello hooks!</p>
}
复制代码
使用 memo 可以实现 shouldComponentUpdate 的功能,示例:
function DemoComponent(props) {
return <h1>I am {props.name}. hi~</h1>;
}
memo(DemoComponent, (prevProps, nextProps) => prevProps.name === nextProps.name);
// 添加第二个参数来指定采用旧 props 和新 props 的自定义比较函数。如果返回 true,则跳过更新。
复制代码
更多
样式方案
非内联样式(CSS)
对于投放到 Web 或者小程序的应用,我们强烈推荐使用标准的 CSS 来编写样式。
全局样式
对于整个项目的全局样式,推荐统一定义在 src/global.[css|less|scss]
文件中,框架会自动引入该文件:
body {
-webkit-font-smoothing: antialiased;
}
a {
color: red;
}
复制代码
组件样式
对于页面级和组件级的样式,我们推荐使用 CSS Modules 的方案,这能很好的解决样式开发中全局污染以及命名冲突的问题。
具体规范规则如下:
- 文件名:约定文件名格式为
xxx.module.[css|less|scss]
- 模块化:一个页面或者一个组件对应一个样式文件
Home
├── index.module.css
└── index.tsx
复制代码
首先编写样式文件内容:
/** ./pages/Home/index.module.css */
.container {
background: #fff;
width: 750rpx;
}
/* 也可通过 CSS Modules 的 :global 语法定义全局样式 */
:global {
body {
a {
color: blue;
}
}
}
复制代码
在文件中引入对应的样式文件,并将 className
与对应样式关联:
// ./pages/Home/index.tsx
import styles from './index.module.css';
function Home() {
return (
<View className={styles.container}>
<View>CSS Modules</View>
</View>
);
}
复制代码
使用该方案之后,上文中的 className 都会被编译为唯一性的名字,避免因为重名 className 而产生样式冲突,如果在浏览器里查看这个示例的 dom 结构,你会发现实际渲染出来是这样的:
<View class="container--1DTudAN">title</View>
复制代码
内联样式
如果在 build.json
里配置了 inlineStyle: true
则说明整个项目使用内联样式。对于 Weex、Kraken 这些暂不支持 CSS 的渲染引擎,只支持内联样式(即通过元素的 style 属性设置样式)。
const myStyle = {
fontSize: '24px',
color: '#FF0000'
};
const element = <View style={myStyle}>Hello Rax</View>;
复制代码
注意:内联样式无法支持 CSS 中 @keyframes 等相关能力
同时也支持在 CSS 文件中编写样式,然后通过编译工具最终生成内联的样式,具体使用方式如下:
index.css
/** index.css */
.container {
background: #f40;
padding: 30rpx;
}
复制代码
index.tsx
import { createElement } from 'rax';
import './index.css';
export default function Title() {
return (<View className="container">...</View>);
}
复制代码
内联场景下也可以使用 CSS Modules 的文件命名规范以及写法,这样可以保证两种模式下样式的写法是基本一致的。
自适应单位 rpx
Rax 采用 rpx(responsive pixel) 作为跨端的样式单位,它可以根据屏幕宽度进行自适应。我们规定屏幕宽度为 750rpx,以 iPhone6 为例,它的屏幕宽度为 375px,则 750rpx = 375px = 100vw,所以在 iPhone6 中,1rpx = 0.5px = 100/750vw。
设备 | rpx 换算 px (屏幕宽度 / 750) | px 换算 rpx (750 / 屏幕宽度) |
---|---|---|
iPhone5 | 1rpx = 0.42px = 100/750vw | 1px = 2.34rpx = 234/750vw |
iPhone6 | 1rpx = 0.5px = 100/750vw | 1px = 2rpx = 200/750vw |
iPhone7 Plus | 1rpx = 0.552px = 100/750vw | 1px = 1.81rpx = 181/750vw |
建议开发 Rax 页面时设计师用 750 作为设计稿的标准
从内联样式迁移到非内联样式
某些 Web 应用可能因为一些历史原因导致使用了内联样式的方案,对于此类应用我们推荐迁移到非内联样式,这样可以使用更加强大的 CSS 能力。
移除 inlineStyle
属性或将其置为 false
:
// build.json
{
- "inlineStyle": true
}
复制代码
确保原先的 className 是否有冲突的情况,如果有则需要修正或者迁移到上述 CSS Modules 的写法。
CSS 预处理器
Rax App 默认支持了 Less 和 Sass 预处理器,你只需要按照 .less
或 .scss
的规则命名文件即可。如果使用 Sass 你还需要将 sass 的编译器比如社区推荐的 dart-sass:
$ npm i --save-dev sass
复制代码
Dark mode 适配
可通过 media 设置对应的主题色。
@media (prefers-color-scheme: dark) {
.container {
background-color: #111;
}
}
复制代码
CSS Modules 文件类型报错
如果在编辑器中遇到 ts 错误 找不到模块“./index.module.css”或其相应的类型声明
,需要在 src/
下新建 typings.d.ts
文件:
// src/typings.d.ts
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
复制代码
进阶指南
框架 API
框架 API 通常情况下可以通过
rax-app
导入,如:import { runApp } from 'rax-app';
基础
runApp
对于 SPA 应用或者小程序应用,src/app.tsx
即整个应用的入口,通过 runApp()
运行应用:
import { runApp } from 'rax-app';
runApp({});
复制代码
当在
app.ts
中包含JSX
元素的时候,需要将app.ts
更名为app.tsx
。
通过 app.getInitialData()
可以在应用启动前获取一些异步状态,比如登录态判断、用户信息获取等。
import { runApp } from 'rax-app';
const appConfig = {
+ app: {
+ getInitialData: async () => {
+ // const data = await fetch('/api/data');
+ return { userId: '123' };
+ }
+ },
};
runApp(appConfig);
复制代码
对于获取到的初始数据,可以通过 getInitialData
API 在任何地方消费:
import { createElement } from 'rax';
+import { getInitialData } from 'rax-app';
export default = () => {
+ const initialData = getInitialData();
console.log(initialData.userId);
};
复制代码
APP_MODE
获取应用环境。
ErrorBoundary
用于错误边界的组件。
IAppConfig
appConfig 的类型定义。
+import { runApp, IAppConfig } from 'rax-app';
+const appConfig: IAppConfig = {
app: {}
+}
runApp(appConfig);
复制代码
状态管理
可以通过在 build.json 中设置 store: false
关闭状态管理。
store
应用级别的 store 实例。
定义类型
IStoreModels
定义模型的类型。
import { IStoreModels, IStoreDispatch, IStoreRootState } from 'rax-app';
// 定义模型的类型。
interface IAppStoreModels extends IStoreModels {
};
const models = {};
// 定义 Dispatch 的类型。
export type IRootDispatch = IStoreDispatch<typeof models>;
// 定义 RootState 的类型。
export type IRootState = IStoreRootState<typeof models>;
复制代码
路由
getHistory
getHistory 用于获取 history 实例。
import { getHistory } from 'rax-app';
function HomeButton() {
const history = getHistory();
function handleClick() {
history.push('/home');
}
return (
<button type='button' onClick={handleClick}>
Go home
</button>
);
}
复制代码
getSearchParams
仅支持 SPA 和小程序,不支持 MPA 应用
用于解析 url 参数。假设当前 URL 为 https://example.com?foo=bar
,解析查询参数如下:
// src/components/Example
import { getSearchParams } from 'rax-app';
function Example() {
const searchParams = getSearchParams()
// console.log(searchParams); => { foo: 'bar' }
}
复制代码
withRouter
通过在 Class 组件上添加 withRouter
装饰器,可以获取到路由的 history
、location
、match
对象。
import { createElement } from 'rax';
import { withRouter } from 'rax-app';
function ShowTheLocation(props) {
const { history, location } = props;
const handleHistoryPush = () => {
history.push('/new-path');
};
return (
<div>
<div>当前路径: {location.pathname}</div>
<button onClick={handleHistoryPush}>点击跳转新页面</button>
</div>
);
}
export default withRouter(ShowTheLocation);
复制代码
history
获取应用的路由实例。
import { history } from 'rax-app';
// 用于获取 history 栈里的实体个数
console.log(history.length);
// 用于获取 history 跳转的动作,包含 PUSH、REPLACE 和 POP 三种类型
console.log(history.action);
// 用于获取 location 对象,包含 pathname、search 和 hash
console.log(history.location);
// 用于路由跳转
history.push('/home');
// 用于路由替换
history.replace('/home');
// 用于跳转到上一个路由
history.goBack();
复制代码
安全区域适配
安全区域(Safe Area),指屏幕内不受圆角(corners)、刘海(sensor housing)、底部小黑条(Home Indicator)等元素影响的可视窗口。
WebKit 从 iOS11 起引入一系列 API,来获取安全区域的位置。比如通过环境变量 safe-area-inset-top
可以获取安全区域距离视口顶部的距离,即刘海区域的高度;通过 safe-area-inset-bottom
可以获取安全区域距离视口底部的距离,即小黑条区域的高度。
刘海屏适配
获取刘海高度,首先需要设置 viewport-fit
,调整可视窗口的布局方式。当且仅当 viewport-fit
设置为 cover
时,可以进一步设置页面的安全区域范围。
<meta name="viewport" content="width=device-width, viewport-fit=cover">
复制代码
然后,结合 env()
方法,可以获取 safe-area-inset-top
值,并将其作为容器节点的 padding-top 值。在 iOS 11.2 之前的版本,需使用 constant()
方法。
.root {
padding-top: constant(safe-area-inset-top); /* 兼容 iOS < 11.2 */
padding-top: env(safe-area-inset-top); // /* iOS > 11.2 */
}
复制代码
小黑条适配
和刘海屏适配的原理一致,小黑条适配可以通过调整 tabbar 的 padding-bottom 值,增加空白区域来实现。通过设置 tabbar 的 padding 值来调整安全区域:
.tabbar {
padding-bottom: 0; /* 无小黑条的情况下,无需额外设置 */
padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */
padding-bottom: env(safe-area-inset-bottom); // /* iOS > 11.2 */
}
复制代码
动画方案
对于基础的动效,推荐的做法是结合 CSS 属性 transition 或 animation 实现。
Transitions
CSS transitions 提供了一种在更改CSS属性时控制动画速度的方法。它可以让属性变化成为一个持续一段时间的过程,而不是立即生效的。比如,将一个元素的颜色从白色改为黑色,通常这个改变是立即生效的,使用 CSS transitions 后该元素的颜色将逐渐从白色变为黑色,按照一定的曲线速率变化,这个过程可以自定义。
示例:
当 hover 到节点时,节点的宽高、背景色及 transform 值发生了渐变。
/* index.module.css */
.box {
border-style: solid;
border-width: 1px;
width: 100rpx;
height: 100rpx;
background-color: #0000FF;
-webkit-transition:width 2s, height 2s,
background-color 2s, -webkit-transform 2s;
transition:width 2s, height 2s, background-color 2s, transform 2s;
}
.box:hover {
background-color: #FFCCCC;
width:200rpx;
height:200rpx;
-webkit-transform:rotate(180deg);
transform:rotate(180deg);
}
// index.jsx
import styles from './index.module.css';
export default () => {
return <div className={styles.box} />;
};
复制代码
Animations
CSS animations 提供了通过 关键帧
创建简单动画的能力。通过使用 @keyframes 指定动画开始、结束以及中间点的关键帧,在每一帧上描述了动画元素在给定的时间点上应该如何渲染(CSS样式配置), 就能够非常容易地创建动画序列。
示例:通过 animation 实现节点显示时,从屏幕右侧划入的效果。
/* index.module.css */
.box {
width: 100rpx;
height: 100rpx;
background-color: #0000FF;
animation-duration: 3s;
animation-name: slidein;
}
@keyframes slidein {
from {
margin-left: 100%;
}
to {
margin-left: 0%;
}
}
// index.jsx
import { createElement, useState } from 'rax';
import View from 'rax-view';
import styles from './index.module.css';
export default function Home() {
const [showBox, setBox] = useState(false);
function onClick() {
setBox(true);
}
return (
<View>
<View className={styles.buttom} onClick={onClick}>show box</View>
{
showBox && <View className={styles.box}></View>
}
</View>
);
}
复制代码
运行时静态配置
src/app.json
用于对应用进行全局配置,设置路由、窗口表现、渲染方式等。默认配置示例:
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
}
],
"window": {
"title": "Rax App 1.0"
}
}
复制代码
单页应用配置路由
路由配置
在 src/app.tsx
中,我们可以配置路由的类型和基础路径等路由信息,具体配置如下:
import { runApp } from 'rax-app';
const appConfig = {
router: {
type: 'browser',
basename: '/seller',
fallback: <div>loading...</div>
}
};
runApp(appConfig);
复制代码
页面路由
对于单页应用,通过 src/app.json
中的 routes
用于指定应用的页面:
path
: 指定页面对应的路由地址source
: 指定页面组件地址,必须写成pages/[PAGE_NAME]/index
格式,暂不支持嵌套式路由targets
指定页面需要构建的端,默认为build.json
所配置的targets
的值window
: 指定该页面的窗体表现,可以覆盖全局的窗口设置
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
},
{
"path": "/about",
"source": "pages/About/index",
"targets": ["web"]
}
]
}
复制代码
框架默认开启路由切割,每个页面会打包出一个独立 bundle
window
可以设置应用的窗口表现,同时也支持针对每个页面设置窗口表现。目前已经支持的参数的有:
属性 | 描述 |
---|---|
title | 页面标题 |
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
},
{
"path": "/about",
"source": "pages/About/index",
"window": {
"title": "关于 Rax"
}
}
],
"window": {
"title": "应用默认 title"
}
}
复制代码
tabBar
如果你的应用是一个多 tab 应用(底部栏可以切换页面),那么可以通过 tabBar
配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
}
],
"tabBar": {
"textColor": "#999",
"selectedColor": "#666",
"backgroundColor": "#f8f8f8",
"items": [
{
"text": "home",
"pageName": "/",
"icon": "https://gw.alicdn.com/tfs/TB1ypSMTcfpK1RjSZFOXXa6nFXa-144-144.png",
"activeIcon": "https://gw.alicdn.com/tfs/TB1NBiCTgHqK1RjSZFPXXcwapXa-144-144.png"
}
]
}
}
复制代码
tabBar 配置项如下:
属性 | 类型 | 是否必填 | 描述 |
---|---|---|---|
textColor | HexColor | 否 | 文字颜色 |
selectedColor | HexColor | 否 | 选中文字颜色 |
backgroundColor | HexColor | 否 | 背景色 |
items | Array | 是 | 每个 tab 配置 |
tab item 配置项如下:
属性 | 类型 | 必填 | 描述 |
---|---|---|---|
pageName | String | 是 | 设置页面路径,值为 routes 中配置的路由 |
text | String | 是 | tab item 上显示的文本 |
icon | String | 否 | 非选中状态图标路径 |
activeIcon | String | 否 | 选中状态图标路径 |
路由跳转
可以通过 history
获取路由实例。
import { history } from 'rax-app';
// 用于获取 history 跳转的动作,包含 PUSH、REPLACE 和 POP 三种类型
console.log(history.action);
// 用于获取 location 对象,包含 pathname、search 和 hash
console.log(history.location);
// 用于路由跳转
history.push('/home');
// 用于路由替换
history.replace('/home');
// 用于跳转到上一个路由
history.goBack();
复制代码
更多 history API
路由参数获取
可以通过 getSearchParams
获取路由参数。假设当前 URL 为 https://example.com?foo=bar
,解析查询参数如下:
// src/components/Example
import { getSearchParams } from 'rax-app';
function Example() {
const searchParams = getSearchParams()
// console.log(searchParams); => { foo: 'bar' }
}
复制代码
常见问题
HashHistory 与 BrowserHistory
前端路由通常有两种实现方式:HashHistory 和 BrowserHistory,路由都带着 #
说明使用的是 HashHistory。这两种方式优缺点:
特点\方案 | HashRouter | BrowserRouter |
---|---|---|
美观度 | 不好,有 # 号 | 好 |
易用性 | 简单 | 中等,需要 server 配合 |
依赖 server | 不依赖 | 依赖 |
跟锚点功能冲突 | 冲突 | 不冲突 |
兼容性 | IE8 | IE10 |
开发者可以根据自己的实际情况选择对应方案。
如何使用 BrowserRouter
本地开发时,只需要在 src/app.tsx
中增加以下配置即可:
import { runApp } from 'rax-app';
const appConfig = {
router: {
+ type: 'browser',
}
};
runApp(appConfig);
复制代码
线上运行时需要服务端支持,否则会出现刷新 404 问题,具体方案请参考社区文档:
静态资源使用
让静态资源不经过构建
你可以将无需经过 webpack 构建的静态资源添加到 public
文件夹。
如果将文件放入 public
文件夹,webpack 将 不会 处理它。而是它将被复制到构建文件夹中。要引用 public
文件夹中的资源,需要使用名为 process.env.PUBLIC_URL
的特殊变量,这个值会根据工程配中的 publicPath
变化:
render() {
// 注意:这是一个 escape hatch,应该谨慎使用!
// 通常我们建议使用`import`来获取资源的 URL
return <img src={process.env.PUBLIC_URL + '/img/logo.png'} />;
}
复制代码
注意这种方法的缺点:
public
文件夹中的所有文件都不会进行后处理或压缩- 在编译时不会调用丢失的文件,并且会导致用户出现 404 错误
- 构建产物的文件名不包含内容哈希值,因此你需要添加查询参数或在每次更改时重命名它们(以便清除浏览器缓存)
一般情况下,我们建议你在 JavaScript 文件中 import
资源,这种机制提供了许多好处:
- 脚本和样式被压缩并打包在一起,以避免额外的网络请求
- 缺少文件会导致编译错误,而不是给用户 404 错误
- 构建产物的文件名包含内容哈希,因此你无需担心浏览器会缓存旧版本
何时使用 public 文件夹
通常我们建议从 JavaScript 导入 stylesheets,图片和字体。 public
文件夹可用作许多不常见情况的变通方法:
- 你需要在构建输出中具有特定名称的文件,例如
manifest.json
- 你有数千张图片,需要动态引用它们的路径
- 你希望在打包代码之外包含一个无需走构建逻辑的小脚本
- 在工程中,某些库可能与 webpack 不兼容,你没有其他选择,只能将其放在
public
中引入
代码质量保障
为了保证代码质量,我们推荐使用 lint 相关的工具对代码进行检测,同时为了降低常规 lint 工具的使用成本,我们封装了 @iceworks/spec 这个 npm 包,基础的 eslint 规则与阿里巴巴前端规范保持一致。
安装依赖
$ npm i --save-dev @iceworks/spec eslint stylelint @commitlint/cli
复制代码
引入配置文件
eslint
JavaScript 工程:
// .eslintrc.js
const { getESLintConfig } = require('@iceworks/spec');
module.exports = getESLintConfig('rax');
复制代码
TypeScript 工程:
// .eslintrc.js
const { getESLintConfig } = require('@iceworks/spec');
module.exports = getESLintConfig('rax-ts');
复制代码
stylint
stylelint 用来检测样式代码的风格,新建配置文件 .stylelintrc.js
引入 lint 规则:
// .stylelintrc.js
const { getStylelintConfig } = require('@iceworks/spec');
module.exports = getStylelintConfig('rax');
复制代码
commitlint
用于规范 commit message 的规范,防止全是 fix
这种无意义的 commit message 导致历史记录追溯比较麻烦,新建配置文件 .commitlintrc.js
引入规则:
// .commitlintrc.js
const { getCommitlintConfig } = require('@iceworks/spec');
module.exports = getCommitlintConfig('rax');
复制代码
配置命令行
通过 npm scripts
配置命令:
// package.json
"scripts": {
"lint": "npm run eslint && npm run stylelint",
"eslint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./",
"stylelint": "stylelint **/*.{css,scss,less}"
}
复制代码
这样通过 npm run lint
就可以运行 lint 任务了。
工具保证
推荐使用 VS Code 进行开发和调试,推荐安装 Iceworks 和 ESLint 两款 VS Code 插件。 安装 VS Code 插件后相关 lint 错误便可在开发调试中实时反馈:
Typescript 无效问题
如果遇到 js 项目 ESLint 问题可以实时反馈而 ts 项目无效时,可以打开 VS Code setting ,在搜索框中输入 ‘ eslint validate ’ ,勾选相关选项。
迁移
从自定义配置迁移
- 移除项目中的各类 ESLint plugin config 及 parser。
- 安装 @iceworks/spec。
- 参考上述文档修改 lint 配置。
注:一定要清除项目之前的 ESLint 的 plugin config 及 parser 相关包。
代码切割
代码切割(Code Splitting) 能够把代码切割到不同的 bundle 中,实现按需或并行加载。代码切割可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大减少首屏资源加载时间。
dynamic import
使用 import()
,webpack 会在编译阶段对引入的资源进行代码切割,即只有当运行时逻辑执行到 import()
调用点时才会加载对应的资源,该函数返回值是 Promise
,开发者可以在链式回调中获取到引用的模块资源。
示例:
import { isWeb } from '@uni/env';
if (isWeb) {
import('./fetch').then(fetch => {
fetch('m.taobao.com');
}).catch(err => {
console.error('模块引入失败!');
});
}
复制代码
rax-use-import
函数式组件本身不能是一个异步函数,所以在使用 import()
动态加载子组件时会存在异步更新视图的处理,为了简化对应场景,我们提供了 rax-use-import
可以在函数组件中用同步的写法快速使用动态加载子组件的能力。
示例:
import { createElement } from 'rax';
+import useImport from 'rax-use-import';
export default function App() {
+ const [Bar, error] = useImport(() => import(/* webpackChunkName: "bar" */ './Bar'));
if (error) {
return <p>error</p>;
} else if (Bar) {
return <Bar />
} else {
return <p>loading</p>;
}
}
复制代码
唤起客户端
Web 页面中唤端
当我们的 Web 页面在 App 以外被用户访问时,我们希望在一定的场景或者在 App 外不支持某些功能的情况下,将用户带入 App 中,以提供更好的访问体验。为此我们需要调用 Web 唤端能力。而在 Android 和 iOS 下,均提供了多种唤端方式使得 Web 页面可以唤起 App ,Starlink.js 对这些唤端方式进行了包装,可以让 Web 页面快速简单的对接唤端能力。
引入 Starlink JS SDK
在你的 HTML 代码中,像这样引入 starlink sdk,其中 ${star_id}
为你在平台申请的 star_id ,一个 star_id 对应一个“海关”平台的唤端策略配置。
Rax App 里通过配置 app.json 里的 scripts
字段即可:
// src/app.json
{
"routes": [],
"scripts": [
+ "<script src="https://assets.taobao.com/app/starlink/core/index.js" id="${star_id}"></script>"
]
}
复制代码
配置页面唤端策略
默认 SDK 接入后就已经可以使用唤端了,不过有时候我们还需要做一些业务定制。
在“海关”平台中进行唤端配置
你可以在“海关”平台上进行唤端策略的配置,而如果“海关”平台的配置不能满足你的个性化特殊定制需求,可以看“通过 JS API 唤端”和“通过 DOM 属性配置唤端”
通过 JS API 进行唤端
$slk.callApp(config)
复制代码
Starlink JS 在 Web 环境下会在全局暴露一个 $slk
的对象,你可以通过调用 $slk.callApp(config)
来进行唤端调用,如果不传 config 对象,则默认使用海关平台返回的配置进行唤端。
config 参数说明:
参数名 | 是否必填 | 类型 | 可选值 | 参数说明 |
---|---|---|---|---|
targetUrl | 否 | String | 页面 URL 地址 | 表示此次唤端在端内需要打开的页面,默认为服务端返回值 |
clipboardCnt | 否 | String | 任意文本 | 唤端的同时,如果设置了 clipboardCnt 参数,clipboardCnt的内容会被写入剪贴板中,如果不设置,不做剪切板写入,唤淘宝App的特殊逻辑:如果在唤起淘宝时设置 clipboardCnt,会覆盖原有默认的剪切板内容,如果不设置,使用默认淘宝的剪切板配置。 |
timeout | 否 | Number | 单位毫秒(ms),发起唤端后的等待时间,如设置了 1000 ,表示发起唤端后 1000ms 后触发唤端失败逻辑 | |
extraProtocolParams | 否 | Object | { bc_fl_src: ‘xxx’ } | 表示在唤端时,需要往唤端的 scheme 协议上透传的参数。 |
targetApp(不建议手动配置此值) | 否 | String | ‘taobao’、’ltao’、’eleme’ | 表示此次唤端要唤起的 App,将覆盖服务端的目标 App 配置 |
通过设置 DOM 属性配置唤端
你也可以在页面 dom 节点(目前仅支持 A 链接)上增加 data-callapp
参数,效果为点击该 A 链接会直接在端内唤起该 A 链接的页面地址(如果 A 链接 href 为非链接地址则不唤起)。
参数 data-callapp 说明:
参数值 | 含义 |
---|---|
无(默认) | 点击后会拉起服务端star_id配置的 App 和 目标页 |
config(JSON对象) | 通过 config JSON 控制具体的唤端配置 |
config JSON 配置字段说明:
字段名称 | 字段值示例 | 含义 |
---|---|---|
targetUrl | '?k1=v1&k2=v2' / 'https://pageurl.com/xxxx' |
如果值为'?k1=v1&k2=v2' 的格式,表示使用此参数覆盖当前页面 url的同名参数,如果格式为 'https://pageurl.com/xxxx' ,表示在端内打开此 url 的页面 |
targetApp(不建议手动配置此值) | ‘taobao’ / ‘ltao’ / ‘eleme’ | 指定要唤起的 app |
data-callapp 示例:
默认
<a data-callapp href="https://xxx.taobao.com/xxxxx"></a>
<!-- 点击 A 链接在 App 内打开 xxx.taobao.com/xxxxx -->
复制代码
自定义唤端配置
<a data-callapp='{"targetUrl":"https://pageurl.com/xxxx", "targetApp": "taobao"}'></a>
<!-- 点击 A 链接在 taobao 内打开 https://pageurl.com/xxxx -->
复制代码
全局唤端功能
用户可以在“海关”平台配置当前 star_id 开启“全局唤端功能”,此时,页面上的 A 链接点击都会变成唤端,效果为尝试在端内打开当前 A 链接页面(同时在当前浏览器也会跳转到该 A 链接地址)
注:如果你希望对部分链接关闭全局唤端,可以在A 链接上增加 ignore-callapp=”true” 参数
<a ignore-callapp="true" ...>这是一个链接,不会进行唤端</a>
<!-- 点击 A 链接不触发全局唤端 -->
复制代码
在唤端过程中插入业务自定义逻辑
starlink.js 也允许业务在唤端执行过程中,做一些自定义业务逻辑,业务侧可以通过绑定事件的方式,在唤端执行过程中执行自己的业务逻辑。
window.$slk.on('evoke', fn) // fn 里面为业务自定义逻辑
复制代码
目前支持的事件及其含义:
"evoke"
– 发起唤端
更多信息请见《Starlink JS SDK 官方文档》。
后言
自己只是简单熟悉下官网文档内容,还有很多很多不够的地方,后续计划:
- 对 Ract 官网 进行详细学习阅读,并且也做自己的学习笔记。
- 将官网小案例手打一遍,记录实操过程的问题及解决。
- 做个 Rax 小项目(吐槽 Rax 线上没有较好的免费实战项目,准备根据 Ract 项目来自己封装 Rax 框架。)
在线征求大佬指教学习路径!有不足或完善地方,望提出!
嘿嘿