原文连接css
效果预览图:node
微信小程序的开发目前是很热的一个领域,有不少的开发模式,找到一种属于本身的方法才会使得开发顺心顺利。react
此架构是使用 Taro + dva + typescript 构建前端开发git
<!--more-->程序员
资料
Taro官网地址:https://taro.aotu.io/github
dva官网地址:https://dvajs.com/guide/typescript
cli 工具安装:npm
# 使用 npm 安装 cli $ npm install -g @tarojs/cli # OR 使用 yarn 安装 cli $ yarn global add @tarojs/cli # OR 安装了 cnpm,使用 cnpm 安装 cli $ cnpm install -g @tarojs/cli
使用命令建立模板项目:json
$ taro init Taro_dva_Typescript
安装dva
cnpm install --save dva-core dva-loading
dva-core
:封装了 redux 和 redux-saga的一个插件dva-loading
:管理页面的loading状态安装@tarojs/redux
cnpm install --save redux @tarojs/redux @tarojs/redux-h5 redux-thunk redux-logger
去除不须要的文件,添加实际须要的一些文件,先删除./ssrc/page
下的index文件夹,后期使用命令行生成完整结构的文件夹。
在`/src
目录下根据本身的实际需求进行一下配置:
assets
: 一些静态资源,好比:image、iconfontconfig
: 项目配置文件components
: 项目编写的一些共用组件types
: 项目公共的Typescript类型声明models
: 项目dva插件model函数的引用或者是一些共用的js文件utils
: 项目里封装的一些插件./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': '图片' }
./src/utils
下建立dva.ts,配置dvaimport { 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 } }
./src/utils
下建立tips.ts,整合封装微信原生弹窗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)) } } }
./src/config
下建立requestConfig.ts,统一配置请求接口/** * 请求公共参数 */ export const commonParame = {} /** * 请求的映射文件 */ export const requestConfig = { loginUrl:'/api/user/wechat-auth' // 微信的登录接口 }
./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)}` }
./src/utils
下建立logger.ts,封装log函数/** * 封装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, }
./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”
。,由于没有添加声明,需在./src目录下建立app-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
./src/config
下建立taroConfig.ts,封装taro小程序的一些方法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 }
./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里加上对应的命令
"scripts": { ... "tep": "node scripts/template", "com": "node scripts/component" }
cnpm run tep index
page文件夹下生成了一个index的文件夹,里面包含
src
目录下建立models
文件夹,集合项目里的model
关系。import index from '../pages/index/model'; export default[ index ]
项目目前只有index
页面,export default
这里的数组就只有index
,须要注意这里是[]
数组。
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'))
./src/pages/index/config.ts
文件一个获取列表数据接口
export default { getList: '/getlist', //getlist接口 }
./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 }
./src/pages/index/service.ts
里的接口请求仍是依据以前的getlist
接口
import Api from '../../utils/request' export const getList = (data) => { return Api.getList(data) }
./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 }
./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 }; }, } }
./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
./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
,打开微信小程序开发者根据,导入本地刚刚生成的dist
文件,就成功启动了项目。
效果预览图:
若有啥问题欢迎讨论,共同窗习。
项目示例Github地址:https://github.com/Duanruilong/taro_dva_typescript