text:
Taro

Taro

一套基于 NodeJS 遵循 React 语法规范的多端统一开发框架

https://github.com/NervJS/taro
https://taro.js.org/
https://nervjs.github.io/taro/
https://github.com/NervJS/awesome-taro

https://github.com/NervJS/awesome-taro. 多端统一开发框架 Taro 优秀学习资源汇总

安装

/** Quick Start With NPM Or Yarn **/
$ sudo npm install -g @tarojs/cli
$ sudo yarn global add @tarojs/cli

更新 Taro

更新 taro-cli 工具：

# taro
$ taro update self
# npm
npm i -g @tarojs/cli@latest
# yarn
yarn global add @tarojs/cli@latest

更新项目中 Taro 相关的依赖，这个需要在你的项目下执行。

$ taro update project

使用命令创建模板项目

taro init myApp

NPM 5.2+ 也可在不全局安装的情况下使用 npx 创建模板项目：

npx @tarojs/cli init myApp

目前 Taro 已经支持微信/百度/支付宝小程序、H5 以及 ReactNative 等端的代码转换，针对不同端的启动以及预览、打包方式并不一致。

微信小程序

微信开发者工具的项目设置：

设置关闭 ES6 转 ES5 功能
设置关闭上传代码时样式自动补全
设置关闭代码压缩上传

需要自行下载并打开微信开发者工具，然后选择项目根目录进行预览
微信小程序编译预览及打包：

# npm script
$ npm run dev:weapp
$ npm run build:weapp
# 仅限全局安装
$ taro build --type weapp --watch
$ taro build --type weapp
# npx 用户也可以使用
$ npx taro build --type weapp --watch
$ npx taro build --type weapp

百度小程序

选择百度小程序模式，需要自行下载并打开百度开发者工具，然后在项目编译完后选择项目根目录下 dist 目录进行预览。
百度小程序编译预览及打包：

# npm script
$ npm run dev:swan
$ npm run build:swan
# 仅限全局安装
$ taro build --type swan --watch
$ taro build --type swan
# npx 用户也可以使用
$ npx taro build --type swan --watch
$ npx taro build --type swan

支付宝小程序

选择支付宝小程序模式，需要自行下载并打开支付宝小程序开发者工具，然后在项目编译完后选择项目根目录下 dist 目录进行预览。

支付宝小程序编译预览及打包：

# npm script
$ npm run dev:alipay
$ npm run build:alipay
# 仅限全局安装
$ taro build --type alipay --watch
$ taro build --type alipay
# npx 用户也可以使用
$ npx taro build --type alipay --watch
$ npx taro build --type alipay

H5

H5 模式，无需特定的开发者工具，在执行完下述命令之后即可通过浏览器进行预览。

H5 编译预览及打包：

# npm script
$ npm run dev:h5
# 仅限全局安装
$ taro build --type h5 --watch
# npx 用户也可以使用
$ npx taro build --type h5 --watch

React Native

React Native 端运行需执行如下命令，React Native 端相关的运行说明请参见 React Native 教程。

# npm script
$ npm run dev:rn
# 仅限全局安装
$ taro build --type rn --watch
# npx 用户也可以使用
$ npx taro build --type rn --watch

Taro-UI

npm install taro-ui
或者使用自定义主题版本
npm install taro-ui@next

安装最新预览版
npm install taro-ui@next

Noticebar 默认换行
禁止换行需要增加 css

white-space:nowrap;

环境判断

process.env.TARO_ENV

虽然 this.props 由 Taro 本身设置以及 this.state 具有特殊的含义，但如果需要存储不用于视觉输出的东西，则可以手动向类中添加其他字段。

如果你不在 render() 中使用某些东西，它就不应该在状态中。

React 核心语法

JSX 是 React 的核心组成部分，React 认为组件化才是正确的代码分离方式，它要比模板与组件逻辑分开的方式更好，所以就有了 JSX 语法。它把 HTML 模板直接嵌入到 JS 代码里面，这样就做到了模板和组件关联。JSX 允许在 JS 中直接使用 XML 标记的方式来声明界面

在 Taro 中使用 Redux

为了更方便地使用 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

Todolist 快速上手 Redux

通过目录划分我们的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 (
      <Provider store={store}>
        <Index />
      </Provider>
    )
  }
}

新建constants文件夹来定义一系列所需的action type常量。例如 Todos 我们可以先增加ADD和DELETE两个action type来区分新增和删除 Todo 指令

// 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中定义函数对应的指令

// 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 则有可能会增加你的工作量。

Taro 设计思想及架构

小程序的数据驱动模板更新的思想与实现机制，与 React 类似；
React 采用 JSX 作为自身模板，JSX 相比字符串模板来说更自由，更自然，更具表现力，不需要依赖字符串模板的各种语法糖，也能完成复杂的处理

在 Taro 中采用的是编译原理的思想，所谓编译原理，就是一个对输入的源代码进行语法分析，语法树构建，随后对语法树进行转换操作再解析生成目标代码的过程。

CLI 原理及不同端的运行机制

taro-cli 负责 Taro 脚手架初始化和项目构建的的命令行工具
https://www.npmjs.com/package/@tarojs/cli

小程序 图片预加载
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;
}

Taro-UI

适配 h5

https://taro-ui.aotu.io/#/docs/quickstart
https://taro-ui.aotu.io/#/docs/questions

增加配置项：

h5: {
  esnextModules: ['taro-ui']
}

Taro 不同平台 打包到不同目录

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'))
}

Taro h5 防止跨域 devServer配置

/config/dev.js

const isH5 = process.env.CLIENT_ENV === 'h5'
const HOST = '"https://testxxx.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

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/`

Taro 使用图标字体
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 };

Taro 上传图片 [h5]
Taro.uploadFile 上传参数
···ts
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: {}
};

};

Taro h5 端 资源相对路径
h5: {
    publicPath: process.env.NODE_ENV === 'production' ? './' : '/',

Taro v1.3.19 配置云函数
目录结构

Taro 增加自定义环境 prerelease env

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
ScrollView 的高度必须固定才能触发ScrollView的onScrollToUpper滚动到顶部事件

Taro React 小程序 长期订阅组件
组件
useSubscribeMessage.ts
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
  }
}

useSubscribeMessageModal.tsx
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
  }
}

createLogger
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"));

useFDialog
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 }

FDialog
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

使用
  const handleButtonClick = async () => {
    const isSubscribe = await subscribeMessage()
    if (!isSubscribe) {
      logger.warn('用户拒绝订阅')
      return
    }
  }

效果

取消后重复展示N次：

版本号 hooks
点击UI即消失

hooks
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'

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 useVersionDisplay = (version: string) => {
  useEffect(() => {
    console.log(`版本号: ${version}`)
    if (isProduction) return
    const id = `version-display`
    const view = document.createElement('view')
    view.id = id

    const currentPages = Taro.getCurrentPages()
    const currentPage = currentPages[currentPages.length - 1]
    const path = currentPage.$taroPath
    const pageElement = document.getElementById(path)

    const handleClose = () => {
      unmountComponentAtNode(view)
      pageElement?.removeChild(view)
    }

    render(<VersionDisplay version={version} onClose={handleClose} />, view)
    pageElement?.appendChild(view)

    return () => {
      unmountComponentAtNode(view)
      pageElement?.removeChild(view)
    }
  }, [version])
}

使用
import { useVersionDisplay } from '@/hooks/useVersionDisplay';

const MyApp = () => {
  useVersionDisplay('1.0.0');

  return (
    <View>
      {/* 其他内容 */}
    </View>
  );
};

export default MyApp;

text 换行
    & > .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;
    }


text:
federated site link syntax ?

How do you build a link to a federated site?
Is there a special link syntax I should use??
tx, Andy

One way is to drag the location field for one site to a new item factory (use [+] to get one) of the other. Current drop handler only works with Webkit browsers: chrome or safari.
Another way is to fork pages that already have links. This will work better when there are already many to choose from.
I've been known to edit them into database files with a text editor.

Soon internal links (made with [[link]] syntax) will automatically search your neighborhood of the federation. You define that neighborhood with the mechanisms mentioned in my previous comment.

Ward-
Thanks for starting this... its really interesting. Just to clarify: there is currently no syntax to define an external link to a federated page so that you can see them in the "Miller columns" view, right?
I was able to drag a page from one federated wiki to another using Chrome, but all I got in the target page was the text of the initial page. It had no link knowledge of where it came from. Is that the proper behavior?
Thanks,
Daryl

I expect most linking will come from following forks in the journal. However, there must be some way to introduce a new site to the federation so that this forking can get started. The current approach is as follows (coded only for chrome):

create a new Factory paragraph by clicking the [+] at the bottom of a page.
drag-and-drop the location bar contents of another federated wiki page to this Factory.

This creates a paragraph of type of FederatedWiki. I've encountered a number of issues that make it hard to understand what is going on when dealing with content from multiple sites. Some of these are known bugs. Others are as yet unresolved usability traps. I'm working through these slowly so as to arrive at the best possible solutions.

Ok, thanks, Ward. I was able to create the paragraph of type "federatedWiki" but it only displays a portion of the included page and when its clicked I a new "page" appears to the right, but it is blank.
I am really excited and looking forward to see where this project goes, thanks again!

Daryl -- Some of this sounds right. A type federatedWiki story item is meant to refer to a page on another site. I copy the first paragraph of that page to make a "fat" link. You can edit that text if the first paragraph doesn't make a suitable citation.
The blank page sounds wrong. When you follow the fat link the client-side javascript appeals to the origin server for help retrieving the remote wiki page. This has the origin server working as a proxy to the other server. (One advantage of this scheme is that the origin server cache that page too so you will always have it.)
Are you using the Ruby/Sinatra server? I'm not sure this proxy behavior has been implemented in the Node/Express server yet.
I'm also considering having the client-side javascript fetch the page directly from the remote server. It does this to retrieve the first paragraph when it makes the fat link citation. Such direct access is referred to as Cross-Origin Resource Sharing (CORS for short). I didn't understand CORS when I wrote the server-side proxy code to access remote pages.
You now find yourself in an area of active development where the "right way" to do something isn't always obvious. I've written up some help pages that include a sample of a working citation. See if this works for you:
http://fed.wiki.org/view/how-to-wiki/view/add-paragraphs/view/example-citation

Ward-
Yes, I am using Sinatra. Using your example link, I now have the remote wiki linking working and it doesn't come up with a blank page.
Thanks again for the help!
Daryl

Thanks for your patience. There is lots here that can be made much smoother.

No apologies necessary. This is an exciting project that I will be watching closely. I intend to set up a "federation" with my programming Team once it matures and stabilizes a bit more and will continue to give you feedback on my progress.

source: gharchive/issue

authors: WardCunningham andyl ngzax

repo: WardCunningham/Smallest-Federated-Wiki

license: mit

license_type: permissive

license_source: bigquery


text:
simple parser works

We'll leave this out on a branch until we have a reliable parse and an approach to evaluation.
For now we are happy to have a simple parsing scheme in javascript  arrow functions with trace wrappers on every rule that can be enabled rule by rule. Here we see our first test case: cypher to match all employees.

This parse guided by these rules and a dozen variations on terminals.

New commit c2e262d adds functions to:

repeat a rule any number of times
find one matching rule among many.

Use these to match queries strung together by many arrows pointing left or right.

New commit 755c7a5 adds script to pretty print most important grammar rules.
root = 'match' node any(arrow node) eot
node = '(' elem ')'
arrow = one(left, right)
left = '<-[' elem ']-'
right = '-[' elem ']->'
elem = bind ':' type
bind = word
type = word

New commit f5f5e5a builds provisional syntax tree.
This tree for same query traced above.
match (e:Employee)

New commit 679f5d2 revises the grammar and tree structure for query convenience.
OpenCypher shows that relation matches are always surrounded by node matches to the point that their grammar identifies a relation and subsequent node match as a PatternElementChain. grammar
match (proj:Project)-[r1:Manager]->(mngr:Employee)-[r2:Manager]->(exec:Employee)

 | node
 | | elem
 | | | bind "proj"
 | | | type "Project"
 | chain
 | | rel
 | | | out
 | | | | elem
 | | | | | bind "r1"
 | | | | | type "Manager"
 | | node
 | | | elem
 | | | | bind "mngr"
 | | | | type "Employee"
 | | chain
 | | | rel
 | | | | out
 | | | | | elem
 | | | | | | bind "r2"
 | | | | | | type "Manager"
 | | | node
 | | | | elem
 | | | | | bind "exec"
 | | | | | type "Employee"
 | | | chain
 | end

We hand-crafted nested "code" that follows the above structure. A recursive evaluator assembles a "maybe" result row that is discarded unless the query completes. To do: copy this result row with each update.
┌───────┬─────────────────┬─────────────────┬───────────────────┐
│ (idx) │ proj            │ mngr            │ exec              │
├───────┼─────────────────┼─────────────────┼───────────────────┤
│     0 │ "Delta Xi"      │ "T. M. Barnes"  │ "W. F. Lewis"     │
│     1 │ "Epsilon Phi"   │ "C. P. Murphy"  │ "W. F. Lewis"     │
│     2 │ "Nu Lambda"     │ "M. A. Baker"   │ "A. J. Flores"    │
│     3 │ "Upsilon Phi"   │ "L. I. Sanders" │ "A. J. Flores"    │
│     4 │ "Sigma Beta"    │ "G. S. Walker"  │ "O. C. Cooper"    │
│     5 │ "Gamma Lambda"  │ "B. B. Clark"   │ "O. C. Cooper"    │
│     6 │ "Delta Epsilon" │ "S. B. Garcia"  │ "I. R. Evans"     │
│     7 │ "Psi Psi"       │ "B. J. Lewis"   │ "I. R. Evans"     │
│     8 │ "Iota Lambda"   │ "F. A. Foster"  │ "R. N. Rodriguez" │
│     9 │ "Rho Chi"       │ "P. P. Barnes"  │ "R. N. Rodriguez" │
│    10 │ "Lambda Alpha"  │ "J. L. Davis"   │ "P. B. Bennett"   │
│    11 │ "Gamma Sigma"   │ "G. M. Scott"   │ "P. B. Bennett"   │
│    12 │ "Mu Alpha"      │ "D. R. Wilson"  │ "N. A. Morgan"    │
│    13 │ "Chi Xi"        │ "W. M. Turner"  │ "N. A. Morgan"    │
│    14 │ "Alpha Gamma"   │ "L. W. Stewart" │ "W. C. Mitchell"  │
│    15 │ "Alpha Omega"   │ "L. G. Scott"   │ "W. C. Mitchell"  │
│    16 │ "Upsilon Delta" │ "E. P. Jackson" │ "A. T. Jones"     │
│    17 │ "Lambda Psi"    │ "L. S. Flores"  │ "A. T. Jones"     │
│    18 │ "Beta Kappa"    │ "L. W. King"    │ "I. J. Reed"      │
│    19 │ "Eta Tau"       │ "D. P. Murphy"  │ "I. J. Reed"      │
│    20 │ "Rho Rho"       │ "P. F. Nguyen"  │ "E. T. Anderson"  │
│    21 │ "Alpha Theta"   │ "H. H. Wilson"  │ "E. T. Anderson"  │
│    22 │ "Sigma Pi"      │ "M. F. Garcia"  │ "B. P. Gomez"     │
│    23 │ "Nu Omicron"    │ "P. O. Reyes"   │ "B. P. Gomez"     │
└───────┴─────────────────┴─────────────────┴───────────────────┘

New commit 9ed8dbf connects the parser to the code gen and that fed with a graph into apply. We run short queries forward and backwards and get similar results with distinctly different internal states. We're not so lucky with longer queries so we might need some new state visualization to figure that out.
The observed schema for our "mock graph" will help us write interesting queries.

New commit 789b7c8 corrects mishandling of result records when matching backs up to find more results from a mid point in the path. We noticed this when we reversed the order of simple queries to make the matching work a little harder.
As the path passes through a node we collect the list of relation ids (rids) to be explored next. At this point we have a potential result partially filled which must be shallow copied to protect subsequent matching from each other. The second of these two lines is today's discovery.
        rids.forEach(rid => {
          maybe = {...maybe}

New commit 4fe1907 adds optional node spec for specific properties, makes other parts optional, and generalizes relations to include both ways.
These improvements follow a three step process.

add or modify parse rules in the grammar
add code gen cases for new syntax tokens
add logic in apply to interpret new cases

We test these new options by writing three queries which vary only in the way the relationship arrow points.
Who manages Clark?
match (mngr: Employee {name: "B. B. Clark"}) -[r:Manager]-> (stuff)
┌───────┬───────────────┬────┬────────────────┐
│ (idx) │ mngr          │ r  │ stuff          │
├───────┼───────────────┼────┼────────────────┤
│     0 │ "B. B. Clark" │ 29 │ "O. C. Cooper" │
└───────┴───────────────┴────┴────────────────┘

Who and what does Clark manage?
match (mngr: Employee {name: "B. B. Clark"}) <-[r:Manager]- (stuff)
┌───────┬───────────────┬─────┬────────────────┐
│ (idx) │ mngr          │ r   │ stuff          │
├───────┼───────────────┼─────┼────────────────┤
│     0 │ "B. B. Clark" │  30 │ "D. O. Jones"  │
│     1 │ "B. B. Clark" │  31 │ "H. M. Flores" │
│     2 │ "B. B. Clark" │  32 │ "T. T. Harris" │
│     3 │ "B. B. Clark" │  33 │ "K. S. Lee"    │
│     4 │ "B. B. Clark" │ 255 │ "Gamma Lambda" │
└───────┴───────────────┴─────┴────────────────┘

Both queries together by looking both ways.
match (mngr: Employee {name: "B. B. Clark"}) -[r:Manager]- (stuff)
┌───────┬───────────────┬─────┬────────────────┐
│ (idx) │ mngr          │ r   │ stuff          │
├───────┼───────────────┼─────┼────────────────┤
│     0 │ "B. B. Clark" │  30 │ "D. O. Jones"  │
│     1 │ "B. B. Clark" │  31 │ "H. M. Flores" │
│     2 │ "B. B. Clark" │  32 │ "T. T. Harris" │
│     3 │ "B. B. Clark" │  33 │ "K. S. Lee"    │
│     4 │ "B. B. Clark" │ 255 │ "Gamma Lambda" │
│     5 │ "B. B. Clark" │  29 │ "O. C. Cooper" │
└───────┴───────────────┴─────┴────────────────┘

I am simply in awe.

New commit f50c915 makes binding optional and add prop match for rel matches just like node matches.
Pretty printed grammar grows a little bit.
match = sp 'match' node chain eot
node = '(' elem  ')'
chain = any(rel node chain)
rel = one(in, out, both)
in = '<-' '[' elem ']' '-'
out = '-' '[' elem ']' '->'
both = '-' '[' elem ']' '-'
elem = opt(bind) opt(':' type) opt(prop)
prop = '{' word ':'  expr '}'
expr = strn
bind = word
type = word

Terminal symbols accept some limitations for now.
word = match(/^[A-Za-z][A-Za-z0-9_]*/) sp
strn = '"' match(/^[^"]{0,20}/) '"' sp
sp = match(/^\s*/)
eot = !right.length

This query uses most of these new features.
What production status is reported for services own by projects B. B. Clark manages?
match (mngr:Employee{name: "B. B. Clark"})-[]-(:Project)-[]-(:Service)-[:Traffic{environment:"production"}]->(stat)
┌───────┬───────────────┬──────────────────────────────────────────────────────────────────────────────────┐
│ (idx) │ mngr          │ stat                                                                             │
├───────┼───────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│     0 │ "B. B. Clark" │ "Gamma Index calculus algorithm (production)"                                    │
│     1 │ "B. B. Clark" │ "Gamma Grover's algorithm (production)"                                          │
│     2 │ "B. B. Clark" │ "Gamma Spectral layout (production)"                                             │
│     3 │ "B. B. Clark" │ "Gamma root mean squared deviation between two protein structures. (production)" │
│     4 │ "B. B. Clark" │ "Gamma Multi level feedback queue (production)"                                  │
│     5 │ "B. B. Clark" │ "Gamma Watershed transformation (production)"                                    │
└───────┴───────────────┴──────────────────────────────────────────────────────────────────────────────────┘

source: gharchive/pull-request

repo: WardCunningham/graph

license_source: github-api


text:
release preparation

In this PR, some required tasks are done for the coming release (Rust SDK 0.9.0):

Updated docs and rustdocs
Updated release CI workflows
Bumped versions

Hello, I am a code review bot on flows.network.
It could take a few minutes for me to analyze this PR. Relax, grab a cup of coffee and check back later. Thanks!

@L-jasmine Thanks for the review!

authors: apepkuss juntao

repo: WasmEdge/wasmedge-rust-sdk


text:
wysihtml5 is not a function

I'm pretty sure I have the required files referenced but think there's a version mismatch as I'm getting the following error when I try to initalise my text area:
Uncaught TypeError: $(...).wysihtml5 is not a function
The error is thrown on this line:
$('#WelcomeText').wysihtml5();
I have the latest version of jQuery referenced (v2.1.4) and Bootstrap (v3.1.1).
Here's my JS references in totality:
<script src="/Scripts/jquery-2.1.4.js"></script>
<script src="/Scripts/jquery.validate.js"></script>
<script src="/Scripts/jquery.validate.unobtrusive.js"></script>
<script src="/Content/admin/js/libs/bootstrap.js"></script>
<script src="/Content/admin/js/mvpready-core.js"></script>
<script src="/Content/admin/js/mvpready-admin.js"></script>
<script src="/Content/admin/js/libs/moment-with-locales.js"></script>
<script src="/Content/admin/js/libs/jquery.blockUI.js"></script>
<script src="/Content/admin/js/plugins/dataTables/jquery.dataTables.js"></script>
<script src="/Content/admin/js/plugins/dataTables/dataTables.bootstrap.js"></script>
<script src="/Content/admin/js/libs/bootstrap-datepicker.js"></script>
<script src="/Content/admin/js/libs/knockout-3.2.0.js"></script>
<script src="/Content/admin/js/custom/knockout-ext.js"></script>
<script src="/Content/admin/js/custom/common.js"></script>
<script src="/Content/admin/js/custom/app.js"></script>
<script src="/Content/admin/js/custom/audit-history.js"></script>
<script src="/Content/admin/js/custom/notifications.js"></script>
<script src="/Content/admin/js/custom/logged-in-user.js"></script>
 <script src="/Content/admin/js/libs/require.js"></script>
<script src="/Content/admin/js/libs/bootstrap3-wysiwyg/bootstrap3-wysihtml5.js"></script>
Is there something that I'm missing here?
Thanks

+1

Sorry that I can't help at the moment. Please ask over here:
https://github.com/bootstrap-wysiwyg/bootstrap3-wysiwyg
This repo is deprecated.

authors: JohnMcAvinue Waxolunist ideaoforder