Taro+dva+Typescript создает архитектуру апплета WeChat

Taro

Оригинальная ссылка

Персональный блог-Добро пожаловать в гости

Предварительный просмотр эффекта:

Taro

Разработка мини-программ WeChat в настоящее время является очень горячей областью.Существует множество режимов разработки.Найдя свой собственный метод, разработка пойдет гладко.

Эта архитектура предназначена для использования Taro + dva + typescript для разработки интерфейса.

  • Платформа React Taro от JD.com Labs очень зрелая, и это крупная фабрика, которая постоянно поддерживает и обновляет ее. Не беспокойтесь о том, что никто не поддерживает ее. У нее есть собственный пользовательский интерфейс и сообщество материалов, которое намного удобнее, чем собственный апплет, код в нескольких местах, запуск в нескольких местах, апплет WeChat, апплет H5, апплет Baidu, апплет Alipay, апплет ByteDance, легкое приложение QQ, быстрое приложение, ReactNative;
  • Управление данными — это фреймворк dva, интегрированный с Redux, схема потока данных, основанная на redux и redux-saga, и для упрощения процесса разработки dva также имеет дополнительный встроенный react-router и fetch, так что его тоже можно понять как легкая рамка приложения;
  • TypeScript — это то, что называют надмножеством JavaScript. Это не замена JavaScript и не добавление каких-либо новых функций в код JavaScript. Напротив, TypeScript позволяет программистам использовать в своем коде объектно-ориентированные конструкции, которые затем переводятся в JavaScript. Он также включает удобные функции, такие как безопасность типов и проверка типов во время компиляции.

Taro

материал

Адрес официального сайта Таро: https://taro.aotu.io/

адрес официального сайта dva: https://dvajs.com/guide/

Начинать

Предварительная подготовка к работе

установка инструмента кли:

# 使用 npm 安装 cli
$ npm install -g @tarojs/cli

# OR 使用 yarn 安装 cli
$ yarn global add @tarojs/cli

# OR 安装了 cnpm,使用 cnpm 安装 cli
$ cnpm install -g @tarojs/cli


Создайте шаблон проекта с помощью команды:


$ taro init Taro_dva_Typescript

Taro

установить файл конфигурации

установить два

cnpm install --save dva-core dva-loading

  • dva-core: Плагин, который инкапсулирует redux и redux-saga.
  • dva-loading: управлять статусом загрузки страницы

установить @tarojs/редукс

cnpm install --save redux @tarojs/redux @tarojs/redux-h5 redux-thunk redux-logger

файл проекта конфигурации

Удалите ненужные файлы, добавьте некоторые файлы на фактическую потребность, удалить./ssrc/pageИндексная папка в командной строке используется для создания папки с полной структурой позже.

В каталоге ``/src` настройте следующее в соответствии с вашими потребностями:

  • assets: некоторые статические ресурсы, такие как: изображение, шрифт значка
  • config: файл конфигурации проекта
  • components: Некоторые общие компоненты, написанные проектом
  • types: Объявления типа машинописного текста, общие для проекта.
  • models: ссылка на модельную функцию плагина dva проекта или некоторые общие файлы js.
  • utils: Некоторые плагины упакованы в проект

Некоторые специфические операции конфигурации проекта

1. В./src/configСоздайте index.ts, добавьте информацию о конфигурации проекта.

/** 
 * 这里为了方便测试使用 Easy Mock 模拟接口数据
 * 
 * https://www.easy-mock.com/mock/5d38269ffb233553ab0d10ad/getlist
*/

export const ONLINEHOST = 'https://www.easy-mock.com/mock/5d38269ffb233553ab0d10ad/getlist';

/** 
 * mock 接口
 * */ 
export const MOCKHOST = 'https://www.easy-mock.com/mock/5d38269ffb233553ab0d10ad/getlist';

/** 
 * 是否mock
*/

export const ISMOCK = true;


/**
 * 这是一个全局的分享信息 不用每一个都去写
 */
export const SHAREINFO = {
    'title': '分享标题',
    'path': '路径',
    'imageUrl': '图片'
  }


2. В./src/utilsСоздайте два.ц под, настройте два


import { create } from "dva-core";
import { createLogger } from "redux-logger";
import  createLoading  from "dva-loading";



let app
let store
let dispatch
let registered

function createApp(opt) {
    // redux 的日志
    opt.onAction = [createLogger()]
    app = create(opt)
    app.use(createLoading({}))

    if (!registered) {
        opt.models.forEach(model => app.model(model));
    }
    registered = true;
    app.start()

    store = app._store;
    app.getStore = () => store;
    app.use({
        onError(err){
            console.log(err);
        }
    })

    dispatch = store.dispatch;
    app.dispatch = dispatch;
    return app;
}

export default{
    createApp,
    getDispatch(){
        return app.dispatch
    }
}

3. В./src/utilsСоздайте подсказки.ts ниже, интегрируйте и упакуйте родное всплывающее окно WeChat.


import Taro from "@tarojs/taro";
import { node } from "_@types_prop-types@15.7.1@@types/prop-types";

/** 
 * 整合封装微信的原生弹窗
 * 提示、加载、工具类
*/

export default class Tips {
	static isLoading = false;

	/** 
	 * 提示信息
	*/
	static toast(title: string, onHide?: () => void) {
		Taro.showToast({
			title: title,
			icon: 'node',
			mask: true,
			duration: 1500
		});
		// 去除结束回调函数
		if (onHide) {
			setTimeout(() => {
				onHide();
			}, 500);
		}
	}

	/** 
	 * 加载提示弹窗
	*/

	static loding(title:'加载中',force = false){
		if (this.isLoading && !force) {
			return
		}

		this.isLoading = true;
		if (Taro.showLoading) {
			Taro.showLoading({
				title:title,
				mask:true
			})
		}else{
			Taro.showNavigationBarLoading() //导航条加载动画
		}
	}

	/** 
	 * 加载完成
	*/
	static loaded(){
		let duration = 0;
		if (this.isLoading) {
			this.isLoading = false;
			if (Taro.hideLoading) {
				Taro.hideLoading()
			} else {
				Taro.hideNavigationBarLoading(); //导航条加载动画
			}
			duration = 500;
		}
		// 设定隐藏的动画时长为500ms,防止直接toast时出现问题
		return new Promise(resolve => setTimeout(resolve,duration))
	}

	/** 
	 * 弹出提示框
	*/

	static success(title,duration = 1500){
		Taro.showToast({
			title: title,
			icon: 'success',
			duration: duration,
			mask:true
		})
		if (duration > 0) {
			return new Promise(resolve => setTimeout(resolve,duration))
		}
	}

}



4. В./src/configСоздайте ниже requestConfig.ts, чтобы единообразно настроить интерфейс запроса.

/** 
 * 请求公共参数
*/

export const commonParame = {}

/** 
 * 请求的映射文件
*/

export const requestConfig = {
    loginUrl:'/api/user/wechat-auth' // 微信的登陆接口
}


5. В./src/utilsСоздайте common.ts под общей функцией


/** 
 * 共用函数
*/

export const repeat = (str = '0', times) => (new Array(times + 1)).join(str);
// 时间前面 +0 
export const pad = (num, maxLength = 2) => repeat('0', maxLength - num.toString().length) + num;

// 全局的公共变量
export let globalData: any = {

}

// 时间格式装换函数

export const formatTime = time => {
    `${pad(time.getHours())}:${pad(time.getMinutes())}:${pad(time.getSeconds())}.${pad(time.getMilliseconds(), 3)}`
}

6. В./src/utilsСоздайте logger.ts ниже, инкапсулируйте функцию журнала


/** 
 * 封装logo函数
*/

import { formatTime } from './common';

const defaults = {
	level: 'log',
	logger: console,
	logErrors: true,
	colors: {
		title:'logger',
		req:'#9e9e9e',
		res:'#4caf50',
		error:'#f20404',
	}
}

function printBuffer(logEntry, options){
	const {logger,colors} = options;
	let {title,started,req,res} = logEntry;
	
	// Message
	const headerCSS = ['color:gray; font-weight:lighter;']
	const styles = s => `color ${s}; font-weight: bold`;

	// render
	logger.group(`%c ${title} @${formatTime(started)}`, ...headerCSS);
	logger.log('%c req', styles(colors.req), req)
	logger.log('%c res', styles(colors.res), res)
	logger.groupEnd()

}

interface LogEntry{
	started ? : object  // 触发时间
}

function createLogger(options: LogEntry = {}){
	const loggerOptions = Object.assign({}, defaults, options)
	const logEntry = options
	logEntry.started = new Date();
	printBuffer(logEntry, Object.assign({}, loggerOptions))
}

export {
	defaults,
	createLogger,
}

7. В./src/utilsСоздайте ниже request.ts для инкапсуляции HTTP-запросов.


import Taro,{ Component } from "@tarojs/taro";
import { ISMOCK,MAINHOST } from "../config";
import { commonParame,requestConfig } from "../config/requestConfig";
import Tips from "./tips";


// 封装请求


declare type Methohs = "GET" | "OPTIONS" | "HEAD" | "PUT" | "DELETE" | "TRACE" | "CONNECT";
declare type Headers = { [key :string]:string};
declare type Datas = {method : Methohs; [key: string] : any;};
interface Options{
    url: string;
    host?: string;
    method?: Methohs;
    data?: Datas;
    header?: Headers;
}

export class Request {
    // 登陆时的promise
    static loginReadyPromise: Promise<any> = Promise.resolve()

    // 正在登陆
    static isLoading: boolean = false

    // 导出的API对象
    static apiLists: { [key: string]: () => any;} = {}

    // token
    static token: string = ''

    // 开始处理options
    static conbineOptions(opts, data: Datas, method: Methohs): Options {
        typeof opts ===  'string' && (opts = {url: opts})
        return {
            data: { ...commonParame, ...opts.data, ...data },
            method: opts.method || data.method || method || 'GET',
            url: `${opts.host || MAINHOST}${opts.url}`
        }
    }

    static getToken(){
        !this.token && (this.token = Taro.getStorageSync('token'))
        return this.token
    }


    // 登陆
    static login(){
        if (!this.isLoading) {
            this.loginReadyPromise = this.onLogining()
        }
        return this.loginReadyPromise
    }

    static onLogining(){
        this.isLoading = true;
        return new Promise(async (resolve, reject) => {
            // 获取code
            const { code } = await Taro.login();

            const { data } = await Taro.request({
                url: `${MAINHOST}${requestConfig.loginUrl}`,
                data:{code: code}
            })

            if (data.code !== 0 || !data.data || !data.data.token) {
                reject()
                return
            }
        })

    }

    /** 
     * 基于 Taro.request 的 request 请求
     * 
     * */ 
    static async request(opts: Options) {
        
        // Taro.request 请求
        const res = await Taro.request(opts);

        // 是否mock
        if(ISMOCK) return res.data;

        // 请求失败
        if (res.data.code === 99999) {
            await this.login();
            return this.request(opts)
        }

        // 请求成功
        if (res.data) {
            return res.data
        }

        // 请求错误
        const edata = { ...res.data, err : (res.data && res.data.msg) || '网络错误 ~'}
        Tips.toast(edata.err)
        throw new Error(edata.err)

    }


    /** 
     * 创建请求函数
    */
   static creatRequests(opts: Options | string) : () => {} {
       console.log('opts==>',opts);
       return async (data={}, method: Methods = "GET") => {
           const _opts = this.conbineOptions(opts, data, method)
           const res = await this.request(_opts)
            return res;
        }
   }

   /** 
    * 抛出API方法
   */

   static getApiList(requestConfig){
        if (!Object.keys(requestConfig).length) {
            return {}
        }
        Object.keys(requestConfig).forEach((key)=>{
            this.apiLists[key] = this.creatRequests(requestConfig[key])
        })
        return this.apiLists
   }


}

const Api = Request.getApiList(requestConfig)
Component.prototype.$api = Api
export default Api as any


Примечание:

Здесь tslint сообщит об этой ошибке:类型“Component<any, any>”上不存在属性“$api”. , поскольку объявление не добавляется, его необходимо создать в каталоге ./srcapp-shim.d.ts


/** 
 * 添加taro等自定义类型
*/

import Taro,{ Component } from '@tarojs/taro'

// 在Component上定义自定义方法类型
declare module '@tarojs/taro' {
    interface Component {
        $api: any
    }
}

// 声明
declare let require: any;
declare let dispatch: any

8. В./src/configНекоторые методы, созданные в taroConfig.ts, упаковывающие апплет таро

import Taro,{ Component } from '@tarojs/taro'
import { SHAREINFO } from '../config/index'



/** 
 * 封装taro小程序的一些方法
 *  - 方法改写
 *  - utils 挂载
*/


// navigateTo 超过8次后,强行进行redirectTo,避免页面卡顿

 const nav = Taro.navigateTo
 Taro.navigateTo = (data) => {
     if (Taro.getCurrentPages().length > 8) {
         return Taro.redirectTo(data)
     }
     return nav(data)
 }


// 挂载分享方法 Component

Component.prototype.onShareAppMessage = function () {
    return SHAREINFO
}


Сценарий генерации файлов конфигурации

1. Создайте папку scripts в корневом каталоге и добавьте./scripts/template.js


/** 
 *  pages 页面快速生成脚本
 *  
 *  npm run tem '文件名‘
*/

const fs = require('fs')
const dirName = process.argv[2]
const capPirName = dirName.substring(0, 1).toUpperCase() + dirName.substring(1);

if (!dirName) {
    console.log('文件名不能为空');
    console.log('用法:npm run tem test');
    process.exit(0);
}

// 页面模板构建

const indexTep = `
    import Taro, { Component, Config } from '@tarojs/taro'
    import { View } from '@tarojs/components'
    // import { connect } from '@tarojs/redux'
    // import Api from '../../utils/request'
    // import Tips from '../../utils/tips'
    import { ${capPirName}Props, ${capPirName}State } from './${dirName}.interface'
    import './${dirName}.scss'
    // import {  } from '../../components'

    // @connect(({ ${dirName} }) => ({
    //     ...${dirName},
    // }))

    class ${capPirName} extends Component<${capPirName}Props,${capPirName}State > {
    config:Config = {
        navigationBarTitleText: '页面标题'
    }
    constructor(props: ${capPirName}Props) {
        super(props)
        this.state = {}
    }

    componentDidMount() {
        
    }

    render() {
        return (
        <View className='fx-${dirName}-wrap'>
            页面内容
        </View>
        )
    }
    }
    export default ${capPirName}
`

// scss 文件模板

const scssTep = `
    @import "../../assets/scss/variables";
    .#{$prefix} {
        &-${dirName}-wrap {
            width: 100%;
            min-height: 100Vh;
        }
    }
`

// config 接口地址配置模板

const configTep =`
    export default {
        test:'/wechat/perfect-info',  //XX接口
    }
`

// 接口请求模板

const serviceTep =`
    import Api from '../../utils/request'
    export const testApi = data => Api.test(
        data
    )
`

// model 模板

const modelTep = `
    // import Taro from '@tarojs/taro';
    // import * as ${dirName}Api from './service';
    export default {
        namespace: '${dirName}',
        state: {
        },
        
        effects: {},
        
        reducers: {}
    
    }

`

const interfaceTep = `
/**
 * ${dirName}.state 参数类型
 *
 * @export
 * @interface ${capPirName}State
 */
export interface ${capPirName}State {}

/**
 * ${dirName}.props 参数类型
 *
 * @export
 * @interface ${capPirName}Props
 */
export interface ${capPirName}Props {}
`

fs.mkdirSync(`./src/pages/${dirName}`); // mkdir $1
process.chdir(`./src/pages/${dirName}`); // cd $1

fs.writeFileSync(`${dirName}.tsx`, indexTep); //tsx
fs.writeFileSync(`${dirName}.scss`, scssTep); // scss
fs.writeFileSync('config.ts', configTep); // config
fs.writeFileSync('service.ts', serviceTep); // service
fs.writeFileSync('model.ts', modelTep); // model
fs.writeFileSync(`${dirName}.interface.ts`, interfaceTep); // interface
process.exit(0);



наконец

в корневом каталогеpackage.jsonДобавьте соответствующую команду в скрипты


"scripts": {
  ...
  "tep": "node scripts/template",
  "com": "node scripts/component"
}

2. Автоматически создавать папку скриптов

cnpm run tep index

Индексная папка создается в папке страницы, которая содержит

  • config.ts
  • index.interface.ts
  • index.scss
  • index.tsx
  • model.ts
  • service.ts

Настроить бизнес-код

1. первыйsrcСоздано в каталогеmodelsпапка, в элементе коллекцииmodelсвязь.


import index from '../pages/index/model';


export default[
    index
]

В настоящее время проект толькоindexстраница,export defaultМассив здесь толькоindex, следует отметить, что здесь[]множество.

2. Измените очень важные файлыapp.tsx


import Taro, { Component, Config } from '@tarojs/taro'
import "@tarojs/async-await";
import { Provider } from "@tarojs/redux";
import dva from './utils/dva';
import './utils/request';
import { globalData } from './utils/common';

import models from './models'
import Index from './pages/index'
import './app.scss'

// 如果需要在 h5 环境中开启 React Devtools
// 取消以下注释:
// if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5')  {
//   require('nerv-devtools')
// }


const dvaApp = dva.createApp({
  initialState:{},
  models:  models,
})

const store = dvaApp.getStore();

class App extends Component {

  /**
   * 指定config的类型声明为: Taro.Config
   *
   * 由于 typescript 对于 object 类型推导只能推出 Key 的基本类型
   * 对于像 navigationBarTextStyle: 'black' 这样的推导出的类型是 string
   * 提示和声明 navigationBarTextStyle: 'black' | 'white' 类型冲突, 需要显示声明类型
   */
  config: Config = {
    pages: [
      'pages/index/index'
    ],
    window: {
      backgroundTextStyle: 'light',
      navigationBarBackgroundColor: '#fff',
      navigationBarTitleText: 'WeChat',
      navigationBarTextStyle: 'black'
    }
  }

  /**
   *
   *  1.小程序打开的参数 globalData.extraData.xx
   *  2.从二维码进入的参数 globalData.extraData.xx
   *  3.获取小程序的设备信息 globalData.systemInfo
   */
  async componentDidMount () {
    // 获取参数
    const referrerInfo = this.$router.params.referrerInfo
    const query = this.$router.params.query
    !globalData.extraData && (globalData.extraData = {})
    if (referrerInfo && referrerInfo.extraData) {
      globalData.extraData = referrerInfo.extraData
    }
    if (query) {
      globalData.extraData = {
        ...globalData.extraData,
        ...query
      }
    }

    // 获取设备信息
    const sys = await Taro.getSystemInfo()
    sys && (globalData.systemInfo = sys)
  }

  componentDidShow () {}

  componentDidHide () {}

  componentDidCatchError () {}

  render () {
    return (
      <Provider store={store}>
        <Index />
      </Provider>
    )
  }
}

Taro.render(<App />, document.getElementById('app'))


3. Изменить запрос интерфейса./src/pages/index/config.tsдокумент

Интерфейс для получения данных списка


export default {
  getList: '/getlist', //getlist接口
}

4. Изменить./src/config/requestConfig.tsсопоставление файлов

представлятьindexСтраница только что созданаconfigдокумент


import index from "../pages/index/config"; // index的接口



/** 
 * 请求公共参数
*/
export const commonParame = {}

/** 
 * 请求的映射文件
*/

export const requestConfig = {
    loginUrl:'/api/user/wechat-auth', // 微信的登陆接口
    ...index
}


5. Изменить./src/pages/index/service.tsзапрос интерфейса в

все еще основано на предыдущемgetlistинтерфейс


import Api from '../../utils/request'

export const getList = (data) => {

  return Api.getList(data)

}
  

6. Изменить./src/pages/index/index.interface.tsтип параметра в

По конкретным параметрам проекта настройте его самостоятельно


/**
 * index.state 参数类型
 * @interface IndexState
 */
export interface IndexState {

}

/**
 * index.props 参数类型
 *
 * @export
 * @interface IndexProps
 */
export interface IndexProps {
    dispatch?: any,
    data?: Array<DataInterface>
}

export interface DataInterface {
    des:string,
    lunar:string,
    thumbnail_pic_s:string,
    title:string,
    _id:string
}

7. Изменить./src/pages/index/model.tsвнутриeffectsфункция

Создайте интерфейс, который страница должна запрашивать здесь, ссылкаserviceИнтерфейс в файле инициирует запрос данных, здесьgetListНапример.


// import Taro from '@tarojs/taro';
import * as indexApi from './service';

export default {
  namespace: 'index',
  state: {
    data:[],
    v:'1.0',
  },

  effects: {
    *getList({ payload },{select, call, put}){
      const { error, result} = yield call(indexApi.getList,{
        ...payload
      })
      console.log('数据接口返回',result);
      
      if (!error) {
        yield put({
          type: 'save',
          payload: {
            data:result.data
          },
        })
      }
    }
  },

  reducers: {
    save(state, { payload }) {
      return { ...state, ...payload };
    },
  }

}

8. Модификация./src/pages/index/index.tsxструктура страницы

Вот простая реализация страницы списка новостей.


import Taro, { Component, Config } from '@tarojs/taro'
import { View, Text} from '@tarojs/components'
import { connect } from '@tarojs/redux'
// import Api from '../../utils/request'
// import Tips from '../../utils/tips'
import { IndexProps, IndexState } from './index.interface'
import './index.scss'
// import {  } from '../../components'

@connect(({ index }) => ({
    ...index,
}))

class Index extends Component<IndexProps,IndexState > {
  config:Config = {
    navigationBarTitleText: 'taro_dva_typescript'
  }
  constructor(props: IndexProps) {
    super(props)
    this.state = {}
  }

  async getList() {
    await this.props.dispatch({
      type: 'index/getList',
      payload: {}
    })
  }

  componentDidMount() {
    this.getList()
  }

  render() {
    const { data } = this.props
    console.log('this.props===>>',data);
    
    return (
      <View className='fx-index-wrap'>
          <View className='index-topbar'>New资讯</View>
          <View className='index-data'>
            {
              data && data.map((item,index) => {
                return (
                  <View className='index-list' key={index}>
                    <View className='index-title'>{item.title}</View>
                    <View className='index-img' style={`background-image: url(${item.thumbnail_pic_s})`}></View>
                  </View>
                )
              })
            }
          </View>
      </View>
    )
  }
}

export default Index


9. Изменить./src/pages/index/index.scssСтиль домашней страницы

Надпись здесьsassсинтаксический сахар для


@import "../../assets/scss/variables";

.#{$prefix} {

  &-index-wrap {
    width: 100%;
    min-height: 100vh;
    .index {
      &-topbar {
        padding: 10rpx 50rpx;
        text-align: center;
        font-weight: bold;
        color: #333;
        font-size: 30rpx;
      }
  
      // &-data {
      // }
       
      &-title {
        font-size: 28rpx;
        color: #666;
        width: 100%;
        font-weight: bold;
      }
      &-list{
        border-bottom: 1rpx solid #eee;
        padding-bottom: 20rpx;
        margin: 20rpx 24rpx;
        display: flex;
        flex-direction: row;
        justify-content: space-between;
        align-items: center
      }
  
      &-img {
        width: 70%;
        height: 200rpx;
        background-repeat: no-repeat;
        background-size: contain;
        background-position: right center;
      }
    }
  }
 
}


Начало проекта

Запустите команду компиляции апплета

cnpm run dev:weapp

В ожидании компиляции проекта в корневом каталоге проекта будет сгенерирован файлdist, Откройте разработчик апплета WeChat в соответствии с ним, импортируйте только что созданный локальныйdistфайл, проект был успешно запущен.

Предварительный просмотр эффекта:

Taro


Если у вас есть какие-либо вопросы, пожалуйста, обсудите и изучите вместе.

Пример проекта Адрес Github:GitHub.com/D U An Ruilong…