原文連接css
效果預覽圖:node
微信小程序的開發目前是很熱的一個領域,有不少的開發模式,找到一種屬於本身的方法纔會使得開發順心順利。react
此架構是使用 Taro + dva + typescript 構建前端開發git
資料程序員
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地址:github.com/Duanruilong…