Open WangShuXian6 opened 5 years ago
虽然 this.props 由 Taro 本身设置以及 this.state 具有特殊的含义,但如果需要存储不用于视觉输出的东西,则可以手动向类中添加其他字段。
如果你不在 render() 中使用某些东西,它就不应该在状态中。
Taro.getEnv() 与 process.env.TARO_ENV https://github.com/NervJS/taro/issues/1080
Taro.getEnv() 返回 'WEAPP' | 'WEB' | 'RN' | 'SWAN' | 'ALIPAY'
process.env.TARO_ENV 返回 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn'
>Taro.getEnv() 这个 API 设计的本意应该是用来运行时环境判断, 而 process.env.TARO_ENV 是在编译时替换成字符串
***
>横向滚动
![scrollx](https://user-images.githubusercontent.com/30850497/49996332-b382c000-ffc9-11e8-9869-adc683b4f6d3.jpg)
JSX 是 React 的核心组成部分,React 认为组件化才是正确的代码分离方式,它要比模板与组件逻辑分开的方式更好,所以就有了 JSX 语法。它把 HTML 模板直接嵌入到 JS 代码里面,这样就做到了模板和组件关联。JSX 允许在 JS 中直接使用 XML 标记的方式来声明界面
render 函数返回了一些用括号包住的 XML 结构的界面描述,这其实就是该组件的界面描述。里面的写法和 HTML 并没有多大的差别。不同的地方主要是可以在里面进行事件绑定,表达变量,实现简单 JS 逻辑等,即在 JS 里写 HTML。而变量、简单 JS 逻辑都是需要用 {} 包裹起来。另外 HTML 的 class 属性因为是 Javascript 的保留字,所以需要写成 className。
在 JSX 里使用 JS 是有限制的,只能使用一些表达式,不能定义变量,使用 if/else 等,你可以用提前定义变量;用三元表达式来达到同样的效果。
列表渲染,一般是用数组的 map 函数。正如上面的例子,把需要列表渲染的数据使用 map 函数,返回所需要的 JSX 代码。而在事件绑定上,使用 on + 事件名称
父组件传给子组件的数据,会挂载在子组件的 this.props
state 与 props 不同,是属于组件自己内部的数据状态,一般在 constructor 构造函数里初始化定义 state
class Welcome extends React.Component { constructor(props) { super(props); this.state = {name: 'aotu,taro!'}; } render() { return <h1>Hello, {this.state.name}</h1>; } }
当 state 需要变化时,是不允许随便更改的,需要调用 this.setState 来进行更改,否则视图没法进行更新 只把跟组件内部视图有关联的数据,变量放在 state 里面,以此避免不必要的渲染。
当 state 需要变化时,是不允许随便更改的,需要调用 this.setState 来进行更改,否则视图没法进行更新 ``
组件的生命周期,指的是一个 React 组件从挂载,更新,销毁过程中会执行的生命钩子函数
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentWillMount() {}
componentDidMount() {}
componentWillUpdate(nextProps, nextState) {}
componentWillReceiveProps(nextProps) {}
componentDidUpdate(prevProps, prevState) {}
shouldComponentUpdate(nextProps, nextState) {}
componentWillUnmount() {}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
constructor,顾名思义,组件的构造函数。一般会在这里进行 state 的初始化,事件的绑定等等
componentWillMount,是当组件在进行挂载操作前,执行的函数,一般紧跟着 constructor 函数后执行
componentDidMount,是当组件挂载在 dom 节点后执行。一般会在这里执行一些异步数据的拉取等动作
shouldComponentUpdate,返回 false 时,组件将不会进行更新,可用于渲染优化
componentWillReceiveProps,当组件收到新的 props 时会执行的函数,传入的参数就是 nextProps ,你可以在这里根据新的 props 来执行一些相关的操作,例如某些功能初始化等
componentWillUpdate,当组件在进行更新之前,会执行的函数
componentDidUpdate,当组件完成更新时,会执行的函数,传入两个参数是 prevProps 、prevState
componentWillUnmount,当组件准备销毁时执行。在这里一般可以执行一些回收的工作,例如 clearInterval(this.timer) 这种对定时器的回收操作
小程序的生命周期分为页面的生命周期和整个应用的生命周期。
应用的生命周期主要有onLaunch、onShow、onHide
onLaunch 是当小程序初始化完成时,会触发 onLaunch(全局只触发一次); onShow 是当小程序启动,或从后台进入前台显示,会触发 onShow; onHide 是当小程序从前台进入后台,会触发 onHide;
页面的生命周期会比较多一些,有onLoad、onReady、onShow、onHide、onUnload
onLoad 是监听页面加载的函数 onReady 是监听页面初次渲染完成的函数 onShow 是监听页面显示的函数 onHide 是监听页面隐藏的函数 onUnload 是监听页面卸载的函数
为了更方便地使用 Redux,Taro 提供了与 react-redux API 几乎一致的包 @tarojs/redux 来让开发人员获得更加良好的开发体验。
开发前需要安装 redux 和 @tarojs/redux ,开发者可自行选择安装 Redux 中间件,本文以如下中间件为例:
$ yarn add redux @tarojs/redux redux-logger
# 或者使用 npm
$ npm install --save redux @tarojs/redux redux-logger
通过目录划分我们的store/reducers/actions
分别在三个文件夹里创建index.js,作为三个模块的入口文件
// store/index.js
import { createStore, applyMiddleware } from 'redux'
// 引入需要的中间件
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
// 引入根reducers
import rootReducer from '../reducers'
const middlewares = [
thunkMiddleware,
createLogger()
]
// 创建 store
export default function configStore () {
const store = createStore(rootReducer, applyMiddleware(...middlewares))
return store
}
首先,定义好store,然后在app.js中引入。使用@tarojs/redux中提供的Provider组件将前面写好的store接入应用中,这样一来,被Provider包裹的页面都能访问到应用的store。
Provider 组件使组件层级中的 connect() 方法都能够获得 Redux store。
import Taro, { Component } from '@tarojs/taro' import { Provider } from '@tarojs/redux'
import configStore from './store' import Index from './pages/index'
import './app.scss'
const store = configStore()
class App extends Component { ... render () { return (
)
} }
***
>新建constants文件夹来定义一系列所需的action type常量。例如 Todos 我们可以先增加ADD和DELETE两个action type来区分新增和删除 Todo 指令
```ts
// src/constants/todos.js
export const ADD = 'ADD'
export const DELETE = 'DELETE'
创建处理这两个指令的reducer
// src/reducers/index.js
import { combineReducers } from 'redux' import { ADD, DELETE } from '../constants/todos'
// 定义初始状态 const INITIAL_STATE = { todos: [ {id: 0, text: '第一条todo'} ] }
function todos (state = INITIAL_STATE, action) { // 获取当前todos条数,用以id自增 const todoNum = state.todos.length
switch (action.type) {
// 根据指令处理todos
case ADD:
return {
...state,
todos: state.todos.concat({
id: todoNum,
text: action.data
})
}
case DELETE:
let newTodos = state.todos.filter(item => {
return item.id !== action.id
})
return {
...state,
todos: newTodos
}
default:
return state
} }
export default combineReducers({ todos })
***
>在action中定义函数对应的指令
```ts
// src/actions/index.js
import { ADD, DELETE } from '../constants/todos'
export const add = (data) => {
return {
data,
type: ADD
}
}
export const del = (id) => {
return {
id,
type: DELETE
}
}
在 Todos 应用的主页使用相应action修改并取得新的store数据了。来看一眼 Todos 的index.js
// src/pages/index/index.js
import Taro, { Component } from '@tarojs/taro' import { View, Input, Text } from '@tarojs/components' import { connect } from '@tarojs/redux' import './index.scss'
import { add, del } from '../../actions/index'
class Index extends Component { config = { navigationBarTitleText: '首页' }
constructor () { super ()
this.state = {
newTodo: ''
}
}
saveNewTodo (e) { let { newTodo } = this.state if (!e.detail.value || e.detail.value === newTodo) return
this.setState({
newTodo: e.detail.value
})
}
addTodo () { let { newTodo } = this.state let { add } = this.props
if (!newTodo) return
add(newTodo)
this.setState({
newTodo: ''
})
}
delTodo (id) { let { del } = this.props del(id) }
render () { // 获取未经处理的todos并展示 let { newTodo } = this.state let { todos, add, del } = this.props
const todosJsx = todos.map(todo => {
return (
<View className='todos_item'><Text>{todo.text}</Text><View className='del' onClick={this.delTodo.bind(this, todo.id)}>-</View></View>
)
})
return (
<View className='index todos'>
<View className='add_wrap'>
<Input placeholder="填写新的todo" onBlur={this.saveNewTodo.bind(this)} value={newTodo} />
<View className='add' onClick={this.addTodo.bind(this)}>+</View>
</View>
<View>{ todosJsx }</View>
</View>
)
} }
export default connect (({ todos }) => ({ todos: todos.todos }), (dispatch) => ({ add (data) { dispatch(add(data)) }, del (id) { dispatch(del(id)) } }))(Index)
***
>在搭建类似商城这样的大型应用,我们非常建议你采用 Redux 管理数据状态,而譬如开发单页应用这类小型的站点,使用 Redux 则有可能会增加你的工作量。
小程序的数据驱动模板更新的思想与实现机制,与 React 类似; React 采用 JSX 作为自身模板,JSX 相比字符串模板来说更自由,更自然,更具表现力,不需要依赖字符串模板的各种语法糖,也能完成复杂的处理
在 Taro 中采用的是编译原理的思想,所谓编译原理,就是一个对输入的源代码进行语法分析,语法树构建,随后对语法树进行转换操作再解析生成目标代码的过程。
小程序和 Web 端上组件标准与 API 标准有很大差异,这些差异仅仅通过代码编译手段是无法抹平的,例如你不能直接在编译时将小程序的
,因为他们虽然看上去有些类似,但是他们的组件属性有很大不同的,仅仅依靠代码编译,无法做到一致,同理,众多 API 也面临一样的情况。针对这样的情况,Taro 采用了定制一套运行时标准来抹平不同平台之间的差异。直接编译成 这一套标准主要以三个部分组成,包括标准运行时框架、标准基础组件库、标准端能力 API,其中运行时框架和 API 对应 @taro/taro,组件库对应 @tarojs/components,通过在不同端实现这些标准,从而达到去差异化的目的。
在所有端中,我们挑选了微信小程序的组件库和 API 来作为 Taro 的运行时标准,因为微信小程序的文档非常完善,而且组件与 API 也是非常丰富,同时最重要的是,百度小程序以及支付宝小程序都是遵循的微信小程序的标准,这样一来,Taro 在实现这两个平台的转换上成本就大大降低了。
taro-cli 负责 Taro 脚手架初始化和项目构建的的命令行工具 https://www.npmjs.com/package/@tarojs/cli
Taro 工程
. ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build ├── docs ├── lerna-debug.log ├── lerna.json // Lerna 配置文件 ├── package.json ├── packages │ ├── eslint-config-taro │ ├── eslint-plugin-taro │ ├── postcss-plugin-constparse │ ├── postcss-pxtransform │ ├── taro │ ├── taro-async-await │ ├── taro-cli │ ├── taro-components │ ├── taro-components-rn │ ├── taro-h5 │ ├── taro-plugin-babel │ ├── taro-plugin-csso │ ├── taro-plugin-sass │ ├── taro-plugin-uglifyjs │ ├── taro-redux │ ├── taro-redux-h5 │ ├── taro-rn │ ├── taro-rn-runner │ ├── taro-router │ ├── taro-transformer-wx │ ├── taro-weapp │ └── taro-webpack-runner └── yarn.lock
Taro 项目主要是由一系列 NPM 包组成,位于工程的 Packages 目录下。它的包管理方式和 Babel 项目一样,将整个项目作为一个 monorepo 来进行管理,并且同样使用了包管理工具 Lerna。
Lerna 是一个用来优化托管在 Git/NPM 上的多 package 代码库的工作流的一个管理工具,可以让你在主项目下管理多个子项目,从而解决了多个包互相依赖,且发布时需要手动维护多个包的问题。
Packages 目录下十几个包中,最常用的项目初始化与构建的命令行工具 Taro CLI 就是其中一个。在 Taro 工程根目录运行 lerna publish 命令之后,lerna.json 里面配置好的所有的包会被发布到 NPM 上。
taro-cli 包的目录结构如下: 主要目录有:/bin、/src、/template
./ ├── bin // 命令行 │ ├── taro // taro 命令 │ ├── taro-build // taro build 命令 │ ├── taro-update // taro update 命令 │ └── taro-init // taro init 命令 ├── package.json ├── node_modules ├── src │ ├── build.js // taro build 命令调用,根据 type 类型调用不同的脚本 │ ├── config │ │ ├── babel.js // Babel 配置 │ │ ├── babylon.js // JavaScript 解析器 babylon 配置 │ │ ├── browser_list.js // autoprefixer browsers 配置 │ │ ├── index.js // 目录名及入口文件名相关配置 │ │ └── uglify.js │ ├── creator.js │ ├── h5.js // 构建h5 平台代码 │ ├── project.js // taro init 命令调用,初始化项目 │ ├── rn.js // 构建React Native 平台代码 │ ├── util // 一系列工具函数 │ │ ├── index.js │ │ ├── npm.js │ │ └── resolve_npm_files.js │ └── weapp.js // 构建小程序代码转换 ├── templates // 脚手架模版 │ └── default │ ├── appjs │ ├── config │ │ ├── dev │ │ ├── index │ │ └── prod │ ├── editorconfig │ ├── eslintrc │ ├── gitignore │ ├── index.js // 初始化文件及目录,copy模版等 │ ├── indexhtml │ ├── npmrc │ ├── pagejs │ ├── pkg │ └── scss └── yarn-error.log
>用到的核心库
>tj/commander.js Node.js - 命令行接口全面的解决方案,灵感来自于 Ruby's commander。可以自动的解析命令和参数,合并多选项,处理短参等等,功能强大,上手简单。
>jprichardson/node-fs-extra - 在 Node.js 的 fs 基础上增加了一些新的方法,更好用,还可以拷贝模板。
>chalk/chalk - 可以用于控制终端输出字符串的样式。
>SBoudrias/Inquirer.js - Node.js 命令行交互工具,通用的命令行用户界面集合,可以和用户进行交互。
>sindresorhus/ora - 实现加载中的状态是一个 Loading 加前面转起来的小圈圈,成功了是一个 Success 加前面一个小钩钩。
>SBoudrias/mem-fs-editor - 提供一系列 API,方便操作模板文件。
>shelljs/shelljs - ShellJS 是 Node.js 扩展,用于实现 Unix shell 命令执行。
>Node.js child_process - 模块用于新建子进程。子进程的运行结果储存在系统缓存之中(最大 200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。
#### Taro Init 命令主要的流程
***
>Taro 命令是怎样添加进去的呢?其原因在于 package.json 里面的 bin 字段:
```JSON
"bin": {
"taro": "bin/taro"
},
上面代码指定,Taro 命令对应的可执行文件为 bin/taro 。NPM 会寻找这个文件,在 [prefix]/bin 目录下建立符号链接。在上面的例子中,Taro 会建立符号链接 [prefix]/bin/taro。由于 [prefix]/bin 目录会在运行时加入系统的 PATH 变量,因此在运行 NPM 时,就可以不带路径,直接通过命令来调用这些脚本。
关于prefix,可以通过npm config get prefix获取。
$ npm config get prefix /usr/local
通过下列命令可以更加清晰的看到它们之间的符号链接:
$ ls -al `which taro` lrwxr-xr-x 1 chengshuai admin 40 6 15 10:51 /usr/local/bin/taro -> ../lib/node_modules/@tarojs/cli/bin/taro
Taro 子命令 命令关联与参数解析 这里就不得不提到一个有用的包:tj/commander.js ,Node.js 命令行接口全面的解决方案,灵感来自于 Ruby's commander。可以自动的解析命令和参数,合并多选项,处理短参等等,功能强大,上手简单。具体的使用方法可以参见项目的 README。
https://github.com/SBoudrias/Inquirer.js https://github.com/tj/commander.js/ https://github.com/commander-rb/commander
更主要的,commander 支持 Git 风格的子命令处理,可以根据子命令自动引导到以特定格式命名的命令执行文件,文件名的格式是 [command]-[subcommand],例如:
taro init => taro-init taro build => taro-build
/bin/taro 文件内容不多,核心代码也就那几行 .command() 命令:
#! /usr/bin/env node
const program = require('commander') const {getPkgVersion} = require('../src/util')
program
.version(getPkgVersion())
.usage('
>init,build ,update等命令都是通过.command(name, description)方法定义的,然后通过 .parse(arg) 方法解析参数。具体可以查看 Commander.js API 文档。
>http://tj.github.io/commander.js/
>注意第一行#!/usr/bin/env node,有个关键词叫 Shebang
>http://smilejay.com/2012/03/linux_shebang/
***
>参数解析及与用户交互
>这里使用的是 SBoudrias/Inquirer.js 来处理命令行交互
>https://github.com/SBoudrias/Inquirer.js/
>用法
```ts
const inquirer = require('inquirer') // npm i inquirer -D
if (typeof conf.description !== 'string') {
prompts.push({
type: 'input',
name: 'description',
message: '请输入项目介绍!'
})
}
prompt()接受一个问题对象的数据,在用户与终端交互过程中,将用户的输入存放在一个答案对象中,然后返回一个Promise,通过then()获取到这个答案对象。
借此,新项目的名称、版本号、描述等信息可以直接通过终端交互插入到项目模板中,完善交互流程。
当然,交互的问题不仅限于此,可以根据自己项目的情况,添加更多的交互问题。inquirer.js 强大的地方在于,支持很多种交互类型,除了简单的input,还有confirm、list、password、checkbox等
此外,你在执行异步操作的过程中,还可以使用 sindresorhus/ora 来添加一下 Loading 效果。使用 chalk/chalk 给终端的输出添加各种样式。 https://github.com/sindresorhus/ora https://github.com/chalk/chalk
Taro ts 正确解析 state
type PageState = {test:string}
type IProps = PageStateProps & PageDispatchProps & PageOwnProps
interface MiToast {
props: IProps;
state:PageState // 关键
}
import {ComponentClass} from 'react'
import Taro, {Component} from '@tarojs/taro'
import {View, Canvas, Image} from '@tarojs/components'
//import {connect} from '@tarojs/redux'
import './index.less'
import SpriteImage from '../../images/slideshow/main-sprite-3.png'
import A1Image from '../../images/slideshow/a_1.png'
import A2Image from '../../images/slideshow/a_2.png'
import A3Image from '../../images/slideshow/a_3.png'
import A4Image from '../../images/slideshow/a_4.png'
import A5Image from '../../images/slideshow/a_5.png'
import A6Image from '../../images/slideshow/a_6.png'
let loadedImageList: any[] = []
type PageStateProps = {}
type PageDispatchProps = {}
type PageOwnProps = {
config: any
}
type PageState = {
imageList: any[];
}
type IProps = PageStateProps & PageDispatchProps & PageOwnProps
interface Slideshow {
props: IProps;
state: PageState;
}
//@connect(({}) => ({}), (dispatch) => ({}))
class Slideshow extends Component {
ctx
constructor() {
super(...arguments)
this.state = {
imageList: [],
}
}
async componentDidMount() {
this.ctx = Taro.createCanvasContext('canvas', this.$scope)
const imageList = this.generateImageList()
this.setState({imageList})
console.log('图片加载开始')
await this.checkImageLoad()
console.log('图片加载完成')
}
componentWillReceiveProps() {
}
componentWillUnmount() {
}
componentDidShow() {
}
componentDidHide() {
}
prevent(e) {
e.preventDefault()
e.stopPropagation()
}
generateImageList() {
return [
SpriteImage,
A1Image,
A2Image,
A3Image,
A4Image,
A5Image,
A6Image,
]
}
imageLoaded(e) {
console.log('success')
loadedImageList.push('')
}
imageLoadedError(e) {
console.warn('图片加载失败', e)
}
checkImageLoad() {
return new Promise((resolve) => {
let timer = setInterval(() => {
if (loadedImageList.length === this.state.imageList.length) {
clearInterval(timer)
resolve(true)
}
}, 300)
})
}
handleClick(){}
render() {
return (
<View className='slideshow-container' onClick={this.prevent.bind(this)}>
{
this.state.imageList.map((image, index) => (
<Image
className='image'
src={image}
onLoad={this.imageLoaded.bind(this)}
onError={this.imageLoadedError.bind(this)}
key={index}/>
))
}
<Canvas
className='main-canvas'
canvasId="canvas"
onClick={this.handleClick.bind(this)}
/>
</View>
)
}
}
export default Slideshow as ComponentClass<PageOwnProps, PageState>
.main-canvas {
width: 750px;
height: 324px;
background-color: #baf091;
}
.image{
display: none;
}
import {ComponentClass} from 'react'
import Taro, {Component} from '@tarojs/taro'
import {View, Canvas, Image} from '@tarojs/components'
//import {connect} from '@tarojs/redux'
import './index.less'
const baseUrl = `https://cdn.xxx.com/wsx/slideshow/`
const slideList = [
'main-sprite-3.png',
'a_1.png',
'a_2.png',
'a_3.png',
'a_4.png',
'a_5.png',
'a_6.png',
]
type PageStateProps = {}
type PageDispatchProps = {}
type PageOwnProps = {
config: any
}
type PageState = {
}
type IProps = PageStateProps & PageDispatchProps & PageOwnProps
interface Slideshow {
props: IProps;
state: PageState;
}
//@connect(({}) => ({}), (dispatch) => ({}))
class Slideshow extends Component {
ctx
constructor() {
super(...arguments)
this.state = {
}
}
async componentDidMount() {
this.ctx = Taro.createCanvasContext('canvas', this.$scope)
await this.asyncDownloadImage()
}
componentWillReceiveProps() {
}
componentWillUnmount() {
}
componentDidShow() {
}
componentDidHide() {
}
prevent(e) {
e.preventDefault()
e.stopPropagation()
}
handleClick(e) {
console.log('e,', e)
}
async asyncDownloadImage() {
Promise.all(this.downloadImagePromise(baseUrl, slideList))
.then((data) => {
console.log('加载图片成功', data)
})
.catch((error) => {
console.warn('加载图片失败', error)
})
}
downloadImagePromise(baseUrl, imageUrlList) {
return imageUrlList.map((imageUrl) => {
const url = `${baseUrl}${imageUrl}`
console.log('url', url)
return Taro.downloadFile({
url: `${baseUrl}${imageUrl}`
})
})
}
render() {
return (
<View className='slideshow-container' onClick={this.prevent.bind(this)}>
<Canvas
className='main-canvas'
canvasId="canvas"
onClick={this.handleClick.bind(this)}
/>
</View>
)
}
}
export default Slideshow as ComponentClass<PageOwnProps, PageState>
适配 h5
https://taro-ui.aotu.io/#/docs/quickstart https://taro-ui.aotu.io/#/docs/questions
增加配置项:
h5: {
esnextModules: ['taro-ui']
}
config/index.js
const outputRootStrtegy = { h5: 'dist_h5', weapp: 'dist_weapp', alipay: 'dist_alipay', swan: 'dist_swan', ['undefined']: 'dist' } const env = JSON.parse(process.env.npm_config_argv)['cooked'][1].split(':')[1] const outputRoot = outputRootStrtegy[env]
const config = { projectName: 'time-paper-mini', date: '2018-12-11', designWidth: 750, deviceRatio: { '640': 2.34 / 2, '750': 1, '828': 1.81 / 2 }, sourceRoot: 'src', outputRoot: outputRoot, plugins: { babel: { sourceMap: true, presets: [ 'env' ], plugins: [ 'transform-decorators-legacy', 'transform-class-properties', 'transform-object-rest-spread' ] } }, defineConstants: {}, copy: { patterns: [], options: {} }, weapp: { module: { postcss: { autoprefixer: { enable: true, config: { browsers: [ 'last 3 versions', 'Android >= 4.1', 'ios >= 8' ] } }, pxtransform: { enable: true, config: {} }, url: { enable: true, config: { limit: 10240 // 设定转换尺寸上限 } } } } }, h5: { publicPath: '/', staticDirectory: 'static', module: { postcss: { autoprefixer: { enable: true } } }, h5: { esnextModules: ['taro-ui'] } } }
module.exports = function (merge) { if (process.env.NODE_ENV === 'development') { return merge({}, config, require('./dev')) } return merge({}, config, require('./prod')) }
/config/dev.js
const isH5 = process.env.CLIENT_ENV === 'h5' const HOST = '"https://test.xxx.com/api/"'
module.exports = { env: { NODE_ENV: '"development"' }, defineConstants: { HOST: isH5 ? '"/api"' : HOST }, weapp: {}, h5: { devServer: { proxy: { '/api/': { target: JSON.parse(HOST), pathRewrite: { '^/api/': '/' }, changeOrigin: true, secure: false, logLevel: "debug" } } } } }
>/config/index.js
```ts
const outputRootStrtegy = {
h5: 'dist_h5',
weapp: 'dist_weapp',
alipay: 'dist_alipay',
swan: 'dist_swan',
tt: 'dist_tt',
['undefined']: 'dist'
}
const env = JSON.parse(process.env.npm_config_argv)['cooked'][1].split(':')[1]
const outputRoot = outputRootStrtegy[env]
const config = {
projectName: 'time-paper-mini',
date: '2018-12-11',
designWidth: 750,
deviceRatio: {
'640': 2.34 / 2,
'750': 1,
'828': 1.81 / 2
},
sourceRoot: 'src',
outputRoot: outputRoot,
plugins: {
babel: {
sourceMap: true,
presets: [
'env'
],
plugins: [
'transform-decorators-legacy',
'transform-class-properties',
'transform-object-rest-spread'
]
}
},
defineConstants: {},
copy: {
patterns: [],
options: {}
},
weapp: {
module: {
postcss: {
autoprefixer: {
enable: true,
config: {
browsers: [
'last 3 versions',
'Android >= 4.1',
'ios >= 8'
]
}
},
pxtransform: {
enable: true,
config: {}
},
url: {
enable: true,
config: {
limit: 10240 // 设定转换尺寸上限
}
}
}
}
},
h5: {
publicPath: '/',
staticDirectory: 'static',
module: {
postcss: {
autoprefixer: {
enable: true
}
}
},
output: {
filename: 'js/[name].[hash].js',
chunkFilename: 'js/[name].[chunkhash].js'
},
imageUrlLoaderOption: {
limit: 5000,
name: 'static/images/[name].[hash].[ext]'
},
miniCssExtractPluginOption: {
filename: 'css/[name].[hash].css',
chunkFilename: 'css/[name].[chunkhash].css'
},
esnextModules: ['taro-ui']
}
}
module.exports = function (merge) {
if (process.env.NODE_ENV === 'development') {
return merge({}, config, require('./dev'))
}
return merge({}, config, require('./prod'))
}
/src/api.js
const API = process.env.NODE_ENV === 'production' ? 'xxx' : 'test' const isH5 = process.env.TARO_ENV === 'h5' export const BaseUrl = isH5 ? `http://127.0.0.1:10086/api/` : `https://${API}.xxxxx.com/api/`
页面 类
static options = {
addGlobalClass: true
};
,如果是函数式组件,
函数名.options = {addGlobalClass: true}
import { View, Text } from "@tarojs/components";
import "./TabIcon.less";
export default function TabIcon(icon) {
return (
<View className="tab-icon-container">
<View
className={"tab-icon " + icon.iconClass}
style={{ color: "blue" }}
></View>
<Text className={icon.active ? "tab-name active" : "tab-name"}>
{icon.iconName}
</Text>
</View>
);
}
TabIcon.options = { addGlobalClass: true };
需要自己根据mime type来生成个随机文件名
现在 chooseImage 还不能返回完整的文件名
Taro.uploadFile
上传参数 const buildUploadImageOption = chooseImageRes => {
const extInfo = extName.mime(chooseImageRes.tempFiles[0].type);
const tempName = Math.ceil(Math.random() * 10000);
let fileName = `${tempName}.${extInfo[0].ext}`;
return {
url: UploadImageApi,
filePath: chooseImageRes.tempFilePaths[0],
name: "img",
fileName,
formData: {}
};
};
import Taro from "@tarojs/taro";
// import { useState } from "@tarojs/taro";
import "./TakePhoto.less";
import { View } from "@tarojs/components";
import extName from "ext-name";
const IMAGE_EXTS = ["image/jpg", "image/jpeg", "image/png"];
const BaseUrl =
process.env.NODE_ENV === "production"
? "https://prod.xxx.com"
: "https://test.xxx.com";
const UploadImageApi = BaseUrl + "/api/UploadImg";
export default function TakePhoto({ onTakePhoto }) {
const takePhoto = async () => {
Taro.chooseImage({ count: 1 })
.then(async res => {
console.log(res);
if (!checkImage(res)) return false;
await asyncUploadImage(res);
onTakePhoto(res.tempFiles[0].path);
})
.catch(error => {
console.warn(error);
Taro.showToast({
title: "请重新选择图片",
icon: "none",
duration: 2000
});
});
};
const checkImage = res => {
if (
!res.tempFiles ||
!res.tempFiles[0] ||
!res.tempFiles[0].type ||
!IMAGE_EXTS.includes(res.tempFiles[0].type)
) {
Taro.showToast({
title: "请选择 jpg/jpeg/png 类型的图片",
icon: "none",
duration: 2000
});
return false;
} else {
return true;
}
};
const buildUploadImageOption = chooseImageRes => {
const extInfo = extName.mime(chooseImageRes.tempFiles[0].type);
const tempName = Math.ceil(Math.random() * 10000);
let fileName = `${tempName}.${extInfo[0].ext}`;
return {
url: UploadImageApi,
filePath: chooseImageRes.tempFilePaths[0],
name: "img",
fileName,
formData: {}
};
};
const asyncUploadImage = async chooseImageRes => {
if (
!chooseImageRes ||
!chooseImageRes.tempFilePaths ||
!chooseImageRes.tempFilePaths[0]
)
return false;
const newOption = buildUploadImageOption(chooseImageRes);
return new Promise(resolve => {
Taro.uploadFile(newOption)
.then(res => {
console.log(res);
if (isUploadSuccess(res)) {
resolve(res);
} else {
resolve(false);
}
})
.catch(error => {
console.warn("error--", error);
Taro.showToast({
title: "上传错误" + JSON.stringify(error),
icon: "none"
});
resolve(false);
});
});
};
const isUploadSuccess = response => {
const newData = JSON.parse(response.data);
return (
response.statusCode === 200 && newData.code === 2000 && newData.img_path
);
};
return (
<View className="take-photo-container">
<View className="icon-upload take-photo" onClick={takePhoto}></View>
</View>
);
}
<Image src={editedImage} className="edited-image" style={{width:width,height:height}}></Image>
h5: {
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
|--client [客户端根目录-在此目录下执行编译]
|--|--node_modules [客户端依赖]
|--|--config []
|--|--|--dev.js []
|--|--|--index.js []
|--|--|--prod.js []
|--|--src [客户端源码]
|--|--.editorconfig []
|--|--.eslintrc []
|--|--global.d.ts []
|--|--package.json []
|--|--tsconfig.json []
|--|--.gitignore []
|--|--.gitlab-ci.yml []
|--|--.variables.yml []
|--|--.npmrc []
|--|--mini.project.json [微信小程序配置]
|--|--project.config.json [淘宝小程序配置]
|--cloud [微信 云函数 源码]
|--|--functions [云函数文件夹]
|--|--|--login [云函数名称-在此目录安装依赖-执行编译]
|--|--|--|--node_modules [独立的云函数依赖]
|--|--|--|--index.ts []
|--|--|--|--package.json []
|--server [淘宝 云函数 源码]
|--|--login [云函数名称-在此目录安装依赖-执行编译]
|--|--|--node_modules []
|--|--|--cloud.json []
|--|--|--index.js []
|--|--|--package.json []
|--dist_alipay [编译后的客户端文件-淘宝端]
|--|--client [客户端根目录]
|--|--|--app.js []
|--|--server [云函数根目录-从外部手动拷贝]
|--|--|--login []
|--|--|--|--node_modules []
|--|--|--|--cloud.json []
|--|--|--|--index.js []
|--|--|--|--package.json []
config/prerelease.js
module.exports = {
env: {
NODE_ENV: '"prerelease"'
},
defineConstants: {
},
weapp: {},
h5: {
}
};
config/index.js
// 省略
module.exports = function (merge) {
if (process.env.NODE_ENV === 'development') {
return merge({}, config, require('./dev'))
}
if (process.env.NODE_ENV === 'prerelease') {
return merge({}, config, require('./prerelease'))
}
return merge({}, config, require('./prod'))
}
package.json
"scripts": {
"build:weapp": "taro build --type weapp",
"build:swan": "taro build --type swan",
"build:alipay": " taro build --type alipay",
"build:tt": "taro build --type tt",
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"dev:weapp": "npm run build:weapp -- --watch",
"dev:swan": "npm run build:swan -- --watch",
"dev:alipay": "npm run build:alipay -- --watch",
"dev:tt": "npm run build:tt -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch",
"pre:weapp": "NODE_ENV=prerelease taro build --type weapp --watch",
"pre:swan": "NODE_ENV=prerelease taro build --type swan --watch",
"pre:alipay": "NODE_ENV=prerelease taro build --type alipay --watch",
"pre:tt": "NODE_ENV=prerelease taro build --type tt --watch",
"pre:h5": "NODE_ENV=prerelease taro build --type h5 --watch",
"pre:rn": "NODE_ENV=prerelease taro build --type rn --watch",
"mock:taobao": "json-server --watch src/mock/taobao/db.json"
},
执行
npm run pre:weapp
显示
console.log('process.env.NODE_ENV',process.env.NODE_ENV)
// process.env.NODE_ENV prerelease
<ScrollView
className={styles['patient-chat-scrollview']}
scrollY
scrollWithAnimation
onScrollToUpper={handleScrollToUpper}
>
<PatientChatList messages={messages} />
<View className={'bottom-blank'}></View>
<View className={styles['test-height']}></View>
</ScrollView>
& > .patient-chat-scrollview {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: $scrollHeight;
background: #f3f3f5;
white-space: nowrap;
& > .bottom-blank {
width: 100%;
height: 100px; // 94px;
}
}
取消后重复展示N次:
import { useEffect } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { createLogger } from '@/utils/common'
const logger = createLogger()
const SUBSCRIBE_MESSAGE_SUCCESS = 'requestSubscribeMessage:ok'
interface Props {
tmplIds: string[]
lazy: boolean
}
interface Return {
subscribeMessage: () => Promise<boolean>
}
export const useSubscribeMessage = ({ tmplIds, lazy }: Props): Return => {
const subscribeMessage = (tmplIds: string[]): Promise<boolean> => {
return new Promise((resolve, reject) => {
Taro.requestSubscribeMessage({
tmplIds: tmplIds,
entityIds: [],
success: (res) => {
const { errMsg } = res
if (errMsg === SUBSCRIBE_MESSAGE_SUCCESS) {
// 检查每个模板消息的订阅状态
const results = tmplIds.map((tmplId) => res[tmplId])
const isAllAccept = results.every((status) => status === 'accept')
if (isAllAccept) {
resolve(true)
} else {
logger.warn('用户拒绝订阅部分或全部消息')
resolve(false)
}
} else {
resolve(false)
}
},
fail: (error) => {
resolve(false)
},
complete: (res: TaroGeneral.CallbackResult) => {}
})
})
}
const run = async () => {
const result = await subscribeMessage(tmplIds)
logger.info('run result:', result)
return result
}
useEffect(() => {
if (lazy) return
run()
}, [lazy])
return {
subscribeMessage: run
}
}
import { useEffect } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { useSubscribeMessage } from './useSubscribeMessage'
import { createLogger } from '@/utils/common'
import { emitter, useFDialog } from '@/components/common/FDialog/useFDialog'
const logger = createLogger()
const subscribeMessageFlag = 'subscribeMessageFlag'
interface Props {
tmplIds: string[]
lazy: boolean
}
interface Return {
subscribeMessage: () => Promise<boolean>
rendeSubscribeMessageTip: (msg: string) => JSX.Element
}
export const useSubscribeMessageModal = ({ tmplIds, lazy }: Props): Return => {
const { subscribeMessage } = useSubscribeMessage({ tmplIds, lazy: true })
const { render: rendeSubscribeMessageTip, setVisible: showSubscribeMessageTip } = useFDialog(
subscribeMessageFlag,
true
)
const subscribeMessageModal = async (): Promise<boolean> => {
let isSubscribe = false
let attempt = 0
const maxAttempts = 999 // 设置最大尝试次数,避免无限循环
while (!isSubscribe && attempt < maxAttempts) {
attempt++
isSubscribe = await subscribeMessage()
logger.info(`第${attempt}次订阅尝试,结果:`, isSubscribe)
if (!isSubscribe) {
logger.warn('用户拒绝订阅,显示二次弹窗')
showSubscribeMessageTip(true)
await new Promise((resolve) => {
const handleConfirm = async () => {
emitter.off(subscribeMessageFlag, handleConfirm)
resolve(null)
}
emitter.on(subscribeMessageFlag, handleConfirm)
})
}
}
if (!isSubscribe) {
logger.warn('用户多次拒绝订阅,放弃请求')
}
return isSubscribe
}
const run = async (): Promise<boolean> => {
return await subscribeMessageModal()
}
useEffect(() => {
if (lazy) return
run()
}, [lazy])
return {
subscribeMessage: run,
rendeSubscribeMessageTip
}
}
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'
const isProduction = process.env.NODE_ENV === 'production'
const logLevels: Record<LogLevel, { level: LogLevel; style: string }> = {
DEBUG: { level: 'DEBUG', style: 'background-color: #25cbe9; color: white;' },
INFO: { level: 'INFO', style: 'background-color: #28a745; color: white;' },
WARN: { level: 'WARN', style: 'background-color: #ffc107; color: black;' },
ERROR: { level: 'ERROR', style: 'background-color: #dc3545; color: white;' }
}
const getCurrentTimestamp = (): string => {
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = now.getDate().toString().padStart(2, '0')
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
const seconds = now.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
export const createLogger = (isEnabled: boolean = !isProduction) => {
const log = (level: LogLevel, message: unknown, ...optionalParams: unknown[]): void => {
if (!isEnabled) return
//console.trace() // 显示堆栈追踪
const timestamp = getCurrentTimestamp()
const { style } = logLevels[level]
// Format message depending on its type
let formattedMessage: string
if (typeof message === 'string') {
formattedMessage = message
} else {
try {
formattedMessage = JSON.stringify(message, null, 2)
} catch (error) {
console.error('解析日志失败:', error)
formattedMessage = ''
}
}
console.log(`%c[${timestamp}] [${level}] ${formattedMessage}`, style, ...optionalParams)
}
return {
debug: (message: unknown, ...optionalParams: unknown[]): void => {
log(logLevels.DEBUG.level, message, ...optionalParams)
},
info: (message: unknown, ...optionalParams: unknown[]): void => {
log(logLevels.INFO.level, message, ...optionalParams)
},
warn: (message: unknown, ...optionalParams: unknown[]): void => {
log(logLevels.WARN.level, message, ...optionalParams)
},
error: (message: unknown, ...optionalParams: unknown[]): void => {
log(logLevels.ERROR.level, message, ...optionalParams)
}
}
}
// 示例使用
// const isProduction = process.env.NODE_ENV === 'production';
// const logger = createLogger();
// logger.debug("This is a debug message");
// logger.info({ key: "value", anotherKey: [1, 2, 3] });
// logger.warn(["This", "is", "a", "warning", "message"]);
// logger.error(new Error("This is an error message"));
import React, { useState } from 'react'
import FDialog from './index'
import { EventEmitter } from 'events'
const emitter = new EventEmitter()
interface Return {
render: (msg: string) => React.ReactElement
setVisible: React.Dispatch<React.SetStateAction<boolean>>
}
export const useFDialog = (
emitFlag?: string,
hideCancelButton: boolean = false,
hideConfirmButton: boolean = false
) => {
const [visible, setVisible] = useState(false)
const handleClose = () => {
setVisible(false)
}
const handleConfirm = () => {
if (!emitFlag) return
emitter.emit(emitFlag, '')
}
const render = (msg: string) => {
return (
<FDialog
onClose={handleClose}
msg={msg}
visible={visible}
hideCancelButton={hideCancelButton}
hideConfirmButton={hideConfirmButton}
setVisible={setVisible}
onConfirm={handleConfirm}
/>
)
}
return {
render,
setVisible
}
}
export { emitter }
import React, { useState } from 'react'
import { Cell, Dialog } from '@nutui/nutui-react-taro'
import { on } from 'events'
interface Props {
msg: string
visible: boolean
setVisible: React.Dispatch<React.SetStateAction<boolean>>
hideConfirmButton: boolean
hideCancelButton: boolean
onConfirm?: () => void
onCancel?: () => void
onClose: () => void
}
const FDialog = ({
msg,
visible,
setVisible,
hideConfirmButton,
hideCancelButton,
onClose,
onConfirm,
onCancel
}: Props) => {
const handleConfirm = () => {
setVisible(false)
onConfirm && onConfirm()
}
const handleCancel = () => {
setVisible(false)
onCancel && onCancel()
}
return (
<>
{/* <Cell
title="提示"
onClick={() => {
setVisible(true)
}}
/> */}
<Dialog
onClose={onClose}
className="test-dialog"
title="提示"
visible={visible}
hideConfirmButton={hideConfirmButton}
hideCancelButton={hideCancelButton}
closeIcon={false}
closeIconPosition="top-right"
style={{
'--nutui-dialog-close-color': '#8c8c8c'
}}
onConfirm={handleConfirm}
onCancel={handleCancel}
>
{msg}
</Dialog>
</>
)
}
export default FDialog
import { lazy, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text, RichText, Button } from '@tarojs/components'
import { useSubscribeMessageModal } from '../common/hooks'
import { TemplateID } from '../common/config'
import { createLogger } from '@/utils/common'
const logger = createLogger()
const tmplIds = [TemplateID.ConsultationReminder]
const Page = () => {
const { subscribeMessage, rendeSubscribeMessageTip } = useSubscribeMessageModal({
tmplIds,
lazy: true
})
const handleButtonClick = async () => {
const isSubscribe = await subscribeMessage()
if (!isSubscribe) {
logger.warn('用户拒绝订阅')
return
}
}
return (
<View>
<Button onClick={handleButtonClick}>我同意</Button>
{rendeSubscribeMessageTip('请确认允许“aaa”向您发送“问诊提醒”')}
</View>
)
}
export default Page
点击UI即消失
src\hooks\useVersionDisplay.tsx
import React, { useEffect } from 'react'
import Taro from '@tarojs/taro'
import { render, unmountComponentAtNode } from '@tarojs/react'
import { View, Text, RootPortal } from '@tarojs/components'
import { document } from '@tarojs/runtime'
const isProduction = process.env.NODE_ENV === 'production'
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 示例:生成一个在 1 到 10 之间的随机整数
//const randomInt = getRandomInt(1, 10);
//console.log(randomInt);
const VersionDisplay = ({ version, onClose }: { version: string; onClose: () => void }) => {
const containerStyle = {
position: 'fixed' as 'fixed',
top: 0,
left: 0,
width: '100vw',
padding: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
color: '#fff',
textAlign: 'center' as 'center',
zIndex: 9999
}
const handleClick = (event) => {
event.stopPropagation() // 阻止事件冒泡
onClose() // 关闭版本号 UI
}
return (
<RootPortal>
<View style={containerStyle} onClick={handleClick}>
<Text>版本号: {version}</Text>
</View>
</RootPortal>
)
}
export const destroy = (node) => {
const currentPages = Taro.getCurrentPages()
const currentPage = currentPages[currentPages.length - 1]
const path = currentPage.$taroPath
const pageElement = document.getElementById(path)
unmountComponentAtNode(node)
pageElement?.removeChild(node)
}
export const useVersionDisplay = (version: string) => {
console.log(`版本号: ${version}`)
const init = () => {
console.log(`版本号: ${version}`)
if (isProduction) return
const id = `version-display-${getRandomInt(10000,10000000)}`
let view = document.getElementById(id)
const currentPages = Taro.getCurrentPages()
const currentPage = currentPages[currentPages.length - 1]
const path = currentPage.$taroPath
const pageElement = document.getElementById(path)
const handleClose = () => {
view && destroy(view)
}
if (!view) {
// 如果节点不存在,则创建并挂载
view = document.createElement('view')
view.id = id
render(<VersionDisplay version={version} onClose={handleClose} />, view)
pageElement?.appendChild(view)
} else {
// 如果节点已存在,更新版本号和样式
render(
<VersionDisplay version={version} onClose={handleClose} />,
view
)(
// 重置 z-index 使其显示
view.style as any
).zIndex = '9999'
}
}
useEffect(() => {
try {
init()
} catch (error) {
console.error('渲染版本号UI错误:', error)
}
// 不再移除节点,避免报错
return () => {
// 如果需要在组件卸载时隐藏或处理节点,可以在这里处理
}
}, [version])
}
import { useVersionDisplay } from '@/hooks/useVersionDisplay';
const MyApp = () => {
useVersionDisplay('1.0.0');
return (
<View>
{/* 其他内容 */}
</View>
);
};
export default MyApp;
& > .f-notice-bar-text {
width: 333px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
//color: #d3700d;
line-height: 17px;
text-align: left;
font-style: normal;
word-break: break-all;
word-wrap: break-word;
white-space: normal;
}
Taro
微信小程序
百度小程序
支付宝小程序
Taro-UI
环境判断