效果預(yù)覽圖:
微信小程序的開(kāi)發(fā)目前是很熱的一個(gè)領(lǐng)域,有很多的開(kāi)發(fā)模式,找到一種屬于自己的方法才會(huì)使得開(kāi)發(fā)順心順利淫奔。
此架構(gòu)是使用 Taro + dva + typescript 構(gòu)建前端開(kāi)發(fā)
- 京東凹凸實(shí)驗(yàn)室的React框架Taro很成熟灶平,又是大廠在維護(hù)更新迭代,不用擔(dān)心沒(méi)人維護(hù)的問(wèn)題雅宾,他有自己的UI還有物料社區(qū)养涮,比起原生小程序方便很多,支持多端,一處代碼贯吓,多處運(yùn)行懈凹,微信小程序、H5悄谐、百度小程序介评、支付寶小程序、字節(jié)跳動(dòng)小程序爬舰、QQ輕應(yīng)用们陆、快應(yīng)用、ReactNative情屹;
- 數(shù)據(jù)管理是Redux集成的dva框架坪仇,是一個(gè)基于 redux 和 redux-saga 的數(shù)據(jù)流方案,然后為了簡(jiǎn)化開(kāi)發(fā)體驗(yàn)屁商,dva 還額外內(nèi)置了 react-router 和 fetch烟很,所以也可以理解為一個(gè)輕量級(jí)的應(yīng)用框架;
- TypeScript就是所謂的JavaScript超集蜡镶。它不是JavaScript的替代品雾袱,也不會(huì)為JavaScript代碼添加任何新功能。相反官还,TypeScript允許程序員在其代碼中使用面向?qū)ο蟮臉?gòu)造芹橡,然后將其轉(zhuǎn)換為JavaScript。它還包括類型安全和編譯時(shí)類型檢查等便利功能望伦。
資料
Taro官網(wǎng)地址:https://taro.aotu.io/
dva官網(wǎng)地址:https://dvajs.com/guide/
開(kāi)始
前期工作準(zhǔn)備
cli 工具安裝:
# 使用 npm 安裝 cli
$ npm install -g @tarojs/cli
# OR 使用 yarn 安裝 cli
$ yarn global add @tarojs/cli
# OR 安裝了 cnpm林说,使用 cnpm 安裝 cli
$ cnpm install -g @tarojs/cli
使用命令創(chuàng)建模板項(xiàng)目:
$ taro init Taro_dva_Typescript
安裝配置文件
安裝dva
cnpm install --save dva-core dva-loading
-
dva-core
:封裝了 redux 和 redux-saga的一個(gè)插件 -
dva-loading
:管理頁(yè)面的loading狀態(tài)
安裝@tarojs/redux
cnpm install --save redux @tarojs/redux @tarojs/redux-h5 redux-thunk redux-logger
配置項(xiàng)目文件
去除不需要的文件,添加實(shí)際需要的一些文件屯伞,先刪除./ssrc/page
下的index文件夾腿箩,后期使用命令行生成完整結(jié)構(gòu)的文件夾。
在``/src`目錄下根據(jù)自己的實(shí)際需求進(jìn)行一下配置:
-
assets
: 一些靜態(tài)資源劣摇,比如:image珠移、iconfont -
config
: 項(xiàng)目配置文件 -
components
: 項(xiàng)目編寫(xiě)的一些共用組件 -
types
: 項(xiàng)目公共的Typescript類型聲明 -
models
: 項(xiàng)目dva插件model函數(shù)的引用或者是一些共用的js文件 -
utils
: 項(xiàng)目里封裝的一些插件
項(xiàng)目一些具體配置操作
1、在./src/config
下創(chuàng)建index.ts末融,添加項(xiàng)目配置信息
/**
* 這里為了方便測(cè)試使用 Easy Mock 模擬接口數(shù)據(jù)
*
* 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;
/**
* 這是一個(gè)全局的分享信息 不用每一個(gè)都去寫(xiě)
*/
export const SHAREINFO = {
'title': '分享標(biāo)題',
'path': '路徑',
'imageUrl': '圖片'
}
2钧惧、在./src/utils
下創(chuàng)建dva.ts,配置dva
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
下創(chuàng)建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
});
// 去除結(jié)束回調(diào)函數(shù)
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() //導(dǎo)航條加載動(dòng)畫(huà)
}
}
/**
* 加載完成
*/
static loaded(){
let duration = 0;
if (this.isLoading) {
this.isLoading = false;
if (Taro.hideLoading) {
Taro.hideLoading()
} else {
Taro.hideNavigationBarLoading(); //導(dǎo)航條加載動(dòng)畫(huà)
}
duration = 500;
}
// 設(shè)定隱藏的動(dòng)畫(huà)時(shí)長(zhǎng)為500ms,防止直接toast時(shí)出現(xiàn)問(wèn)題
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
下創(chuàng)建requestConfig.ts涂乌,統(tǒng)一配置請(qǐng)求接口
/**
* 請(qǐng)求公共參數(shù)
*/
export const commonParame = {}
/**
* 請(qǐng)求的映射文件
*/
export const requestConfig = {
loginUrl:'/api/user/wechat-auth' // 微信的登陸接口
}
5、在./src/utils
下創(chuàng)建common.ts钮孵,共用函數(shù)
/**
* 共用函數(shù)
*/
export const repeat = (str = '0', times) => (new Array(times + 1)).join(str);
// 時(shí)間前面 +0
export const pad = (num, maxLength = 2) => repeat('0', maxLength - num.toString().length) + num;
// 全局的公共變量
export let globalData: any = {
}
// 時(shí)間格式裝換函數(shù)
export const formatTime = time => {
`${pad(time.getHours())}:${pad(time.getMinutes())}:${pad(time.getSeconds())}.${pad(time.getMilliseconds(), 3)}`
}
6骂倘、在./src/utils
下創(chuàng)建logger.ts,封裝log函數(shù)
/**
* 封裝logo函數(shù)
*/
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 // 觸發(fā)時(shí)間
}
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
下創(chuàng)建request.ts历涝,封裝http請(qǐng)求
import Taro,{ Component } from "@tarojs/taro";
import { ISMOCK,MAINHOST } from "../config";
import { commonParame,requestConfig } from "../config/requestConfig";
import Tips from "./tips";
// 封裝請(qǐng)求
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 {
// 登陸時(shí)的promise
static loginReadyPromise: Promise<any> = Promise.resolve()
// 正在登陸
static isLoading: boolean = false
// 導(dǎo)出的API對(duì)象
static apiLists: { [key: string]: () => any;} = {}
// token
static token: string = ''
// 開(kāi)始處理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 請(qǐng)求
*
* */
static async request(opts: Options) {
// Taro.request 請(qǐng)求
const res = await Taro.request(opts);
// 是否mock
if(ISMOCK) return res.data;
// 請(qǐng)求失敗
if (res.data.code === 99999) {
await this.login();
return this.request(opts)
}
// 請(qǐng)求成功
if (res.data) {
return res.data
}
// 請(qǐng)求錯(cuò)誤
const edata = { ...res.data, err : (res.data && res.data.msg) || '網(wǎng)絡(luò)錯(cuò)誤 ~'}
Tips.toast(edata.err)
throw new Error(edata.err)
}
/**
* 創(chuàng)建請(qǐng)求函數(shù)
*/
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會(huì)報(bào)這樣的錯(cuò):類型“Component<any, any>”上不存在屬性“$api”
。漾唉,因?yàn)闆](méi)有添加聲明荧库,需在./src目錄下創(chuàng)建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
8、在./src/config
下創(chuàng)建taroConfig.ts赵刑,封裝taro小程序的一些方法
import Taro,{ Component } from '@tarojs/taro'
import { SHAREINFO } from '../config/index'
/**
* 封裝taro小程序的一些方法
* - 方法改寫(xiě)
* - utils 掛載
*/
// navigateTo 超過(guò)8次后分衫,強(qiáng)行進(jìn)行redirectTo,避免頁(yè)面卡頓
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、在根目錄下創(chuàng)建scripts文件夾般此,添加./scripts/template.js
/**
* pages 頁(yè)面快速生成腳本
*
* 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);
}
// 頁(yè)面模板構(gòu)建
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: '頁(yè)面標(biāo)題'
}
constructor(props: ${capPirName}Props) {
super(props)
this.state = {}
}
componentDidMount() {
}
render() {
return (
<View className='fx-${dirName}-wrap'>
頁(yè)面內(nèi)容
</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接口
}
`
// 接口請(qǐng)求模板
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 參數(shù)類型
*
* @export
* @interface ${capPirName}State
*/
export interface ${capPirName}State {}
/**
* ${dirName}.props 參數(shù)類型
*
* @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里加上對(duì)應(yīng)的命令
"scripts": {
...
"tep": "node scripts/template",
"com": "node scripts/component"
}
2蚪战、自動(dòng)生成腳本文件夾
cnpm run tep index
page文件夾下生成了一個(gè)index的文件夾,里面包含
- config.ts
- index.interface.ts
- index.scss
- index.tsx
- model.ts
- service.ts
配置業(yè)務(wù)代碼
1铐懊、先在src
目錄下創(chuàng)建models
文件夾邀桑,集合項(xiàng)目里的model
關(guān)系壁畸。
import index from '../pages/index/model';
export default[
index
]
項(xiàng)目目前只有index
頁(yè)面,export default
這里的數(shù)組就只有index
空闲,需要注意這里是[]
數(shù)組碴倾。
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 環(huán)境中開(kāi)啟 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 對(duì)于 object 類型推導(dǎo)只能推出 Key 的基本類型
* 對(duì)于像 navigationBarTextStyle: 'black' 這樣的推導(dǎo)出的類型是 string
* 提示和聲明 navigationBarTextStyle: 'black' | 'white' 類型沖突, 需要顯示聲明類型
*/
config: Config = {
pages: [
'pages/index/index'
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black'
}
}
/**
*
* 1.小程序打開(kāi)的參數(shù) globalData.extraData.xx
* 2.從二維碼進(jìn)入的參數(shù) globalData.extraData.xx
* 3.獲取小程序的設(shè)備信息 globalData.systemInfo
*/
async componentDidMount () {
// 獲取參數(shù)
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
}
}
// 獲取設(shè)備信息
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残邀、修改接口請(qǐng)求./src/pages/index/config.ts
文件
一個(gè)獲取列表數(shù)據(jù)接口
export default {
getList: '/getlist', //getlist接口
}
4、修改./src/config/requestConfig.ts
文件的映射關(guān)系
引入index
頁(yè)面的剛剛創(chuàng)建的config
文件
import index from "../pages/index/config"; // index的接口
/**
* 請(qǐng)求公共參數(shù)
*/
export const commonParame = {}
/**
* 請(qǐng)求的映射文件
*/
export const requestConfig = {
loginUrl:'/api/user/wechat-auth', // 微信的登陸接口
...index
}
5耻台、修改./src/pages/index/service.ts
里的接口請(qǐng)求
還是依據(jù)之前的getlist
接口
import Api from '../../utils/request'
export const getList = (data) => {
return Api.getList(data)
}
6盆耽、修改./src/pages/index/index.interface.ts
里的參數(shù)類型
根據(jù)項(xiàng)目具體的參數(shù)摄杂,自行進(jìn)行配置
/**
* index.state 參數(shù)類型
* @interface IndexState
*/
export interface IndexState {
}
/**
* index.props 參數(shù)類型
*
* @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
函數(shù)
在這里創(chuàng)建頁(yè)面需要請(qǐng)求的接口,鏈接service
里的接口發(fā)起數(shù)據(jù)請(qǐng)求,這里以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('數(shù)據(jù)接口返回',result);
if (!error) {
yield put({
type: 'save',
payload: {
data:result.data
},
})
}
}
},
reducers: {
save(state, { payload }) {
return { ...state, ...payload };
},
}
}
8椎组、修改./src/pages/index/index.tsx
里頁(yè)面結(jié)構(gòu)
這里簡(jiǎn)單的實(shí)現(xiàn)列表新聞頁(yè)面寸癌。
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
首頁(yè)的樣式
這里的寫(xiě)法是sass
的語(yǔ)法糖
@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;
}
}
}
}
項(xiàng)目啟動(dòng)
運(yùn)行小程序編譯命令
cnpm run dev:weapp
等待項(xiàng)目編譯完成,會(huì)在項(xiàng)目根目錄下生成一個(gè)dist
,打開(kāi)微信小程序開(kāi)發(fā)者根據(jù)檬嘀,導(dǎo)入本地剛剛生成的dist
文件鸳兽,就成功啟動(dòng)了項(xiàng)目揍异。
效果預(yù)覽圖:
如有啥問(wèn)題歡迎討論衷掷,共同學(xué)習(xí)。
項(xiàng)目示例Github地址:https://github.com/Duanruilong/taro_dva_typescript