原文連接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