最近写一个微信小程序的项目,由于是协同开发,前期的搭建工作由另一个妹子完成,现在项目阶段一完成了,为了备忘回顾,做一个阶段性小结。
在写小程序之前经过对比最后采用了京东凹凸实验室开发的类react
框架Taro
,用框架的好处就不多说了,比直接写原生小程序方便太多。数据管理采用的是封装了redux
的dva
框架,如果没有学过的同学可以去看看文档。先声明篇幅比较长,如果你需要,还请看完,相信一定有帮助,不想看的同学文末放了GitHub地址,自己去下。
附上文档链接:
taro文档:https://nervjs.github.io/taro/docs/README.html
dva文档:https://dvajs.com/guide/
1.基础步骤
// 全局安装taro (cnpm为淘宝镜像)
cnpm install -g @tarojs/cli
// 创建项目
taro init taro-demo
如下配置(推荐为项目配上ts
):
安装与 react-redux API 几乎一致的包 @tarojs/redux
cnpm install --save redux @tarojs/redux @tarojs/redux-h5 redux-thunk redux-logger
安装dva
cnpm install --save dva-core dva-loading
-
dva-core
:封装了 redux 和 redux-saga的一个插件 -
dva-loading
:管理页面的loading状态
2.整理项目文件
删除
- 删除
./src/page
文件夹下的index
文件夹
添加
- 在
./src
文件夹下添加如下文件夹(根据自己实际情况和项目需求进行配置,只罗列一些必要的):
assets
:静态资源,如images、scss、iconfont...
components
:编写共用组件
config
:项目配置文件
models
:dva插件model
函数引用或者共用的js
types
:公共typescript类型申明
utils
:封装的插件
3.编写插件(主要且常用的)
1.在./src/config
下创建index.ts
,添加项目配置信息,例如:
/**
* 线上环境
* 为了方便测试,使用的是聚合数据免费接口
* 网址:https://www.juhe.cn/
*/
export const ONLINEHOST = 'http://api.juheapi.com'
/**
* 测试环境
*/
export const QAHOST = 'http://xxx.cn'
/**
* 线上mock
*/
export const MOCKHOST = 'http://xxx/mock'
/**
* 是否mock
*/
export const ISMOCK = false
/**
* 当前的host ONLINEHOST | QAHOST | MOCKHOST
*/
export const MAINHOST = ONLINEHOST
/**
* 全局的分享信息 不用每一个都去写
*/
export const SHAREINFO = {
'title': '分享标题',
'path': '路径',
'imageUrl': '图片'
}
2.在./src/utils
下创建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/config
下创建requestConfig.ts
,统一配置请求接口,内容如下:
/**
* 请求的公共参数
*/
export const commonParame = {}
/**
* 请求映射文件
*/
export const requestConfig = {
loginUrl: '/api/user/wechat-auth', // 微信登录接口
}
5.在./src/utils
下创建tips.ts
,整合封装微信原生弹窗,内容如下:
import Taro from '@tarojs/taro'
/**
* 提示与加载工具类
*/
export default class Tips {
static isLoading = false
/**
* 信息提示
*/
static toast(title: string, onHide?: () => void) {
Taro.showToast({
title: title,
icon: 'none',
mask: true,
duration: 1500
});
// 隐藏结束回调
if (onHide) {
setTimeout(() => {
onHide();
}, 500);
}
}
/**
* 弹出加载提示
*/
static loading(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时的显示bug
return new Promise(resolve => setTimeout(resolve, duration))
}
/**
* 弹出提示框
*/
static success(title, duration = 1500) {
Taro.showToast({
title: title,
icon: 'success',
mask: true,
duration: duration
});
if (duration > 0) {
return new Promise(resolve => setTimeout(resolve, duration));
}
}
}
5.在./src/utils
下创建common.ts
,共用函数,内容如下:
/** 时间格式的转换 */
export const formatTime = time => {
`${pad(time.getHours())}:${pad(time.getMinutes())}:${pad(time.getSeconds())}.${pad(time.getMilliseconds(), 3)}`
}
export var globalData: any = {} // 全局公共变量
6.在./src/utils
下创建logger.ts
,封装log函数,内容如下:
import {
formatTime
} from './common'
const defaults = {
level: 'log',
logger: console,
logErrors: true,
colors: {
title: 'inherit',
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'
// import { createLogger } from './logger'
declare type Methods = "GET" | "OPTIONS" | "HEAD" | "POST" | "PUT" | "DELETE" | "TRACE" | "CONNECT";
declare type Headers = { [key: string]: string };
declare type Datas = { method: Methods;[key: string]: any; };
interface Options {
url: string;
host?: string;
method?: Methods;
data?: Datas;
header?: Headers;
}
export class Request {
//登陆的promise
static loginReadyPromise: Promise<any> = Promise.resolve()
// 正在登陆
static isLogining: boolean = false
// 导出的api对象
static apiLists: { [key: string]: () => any; } = {}
// token
static token: string = ''
// constructor(setting) {
// }
/**
* @static 处理options
* @param {Options | string} opts
* @param {Datas} data
* @returns {Options}
* @memberof Request
*/
static conbineOptions(opts, data: Datas, method: Methods): Options {
typeof opts === 'string' && (opts = { url: opts })
return {
data: { ...conmomPrams, ...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 request请求 基于 Taro.request
* @param {Options} opts
*/
static async request(opts: Options) {
// token不存在
// if (!this.getToken()) { await this.login() }
// token存在
// let options = Object.assign(opts, { header: { 'token': this.getToken() } })
// 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 && res.data.code === 0 || res.data.succ === 0) { return res.data }
if (res.data) { return res.data }
// 请求错误
const d = { ...res.data, err: (res.data && res.data.msg) || `网络错误~` }
Tips.toast(d.err);
throw new Error(d.err)
}
/**
*
* @static 登陆
* @returns promise
* @memberof Request
*/
static login() {
if (!this.isLogining) { this.loginReadyPromise = this.onLogining() }
return this.loginReadyPromise
}
/**
*
* @static 登陆的具体方法
* @returns
* @memberof Request
*/
static onLogining() {
this.isLogining = 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.setStorageSync('token', data.data.token)
this.isLogining = false
resolve()
})
}
/**
*
* @static 创建请求函数
* @param {(Options | string)} opts
* @returns
* @memberof Request
*/
static creatRequests(opts: Options | string): () => {} {
return async (data = {}, method: Methods = "GET") => {
const _opts = this.conbineOptions(opts, data, method)
const res = await this.request(_opts)
// createLogger({ title: 'request', req: _opts, res: res })
return res
}
}
/**
*
* @static 抛出整个项目的api方法
* @returns
* @memberof Request
*/
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
Tip
:这时候tslint会报这样的错:类型“Component<any, any>”上不存在属性“$api”。
,因为我们没有添加声明,我们可以这样解决,在./src
目录下创建app-shim.d.ts
,内容如下:
/**
*
* @static 添加taro等自定义类型
* @interface Component
*/
import Taro, { Component } from '@tarojs/taro'
// 在Component上定义自定义方法类型
declare module '@tarojs/taro' {
interface Component {
$api: any
}
}
//声明
declare var require: any
declare var dispach: any
这时候应该不报错了。
8.在./src/config
下创建taroConfig.ts
,封装taro小程序的一些方法,内容如下:
/**
* 进行taro的处理
* 1.方法的改写
* 2.utils的挂载
*
*/
import Taro, { Component } from "@tarojs/taro";
import { SHAREINFO } from '../config/index'
/**
* 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
}
4.编写node命令快速创建page
和component
先来看一张图,就明白为什么需要编写这样一个命令了
当你每次需要创建一个页面的时候需要不断的创建,这样太麻烦了,而且容易出错,所以写个node命令快速生成如图中index文件夹下的5个文件,一条命令的事情,下面上代码:
首先在
根目录
下创建scripts
文件夹,在该文件夹下添加如下文件:
- 添加
./scripts/template.js
,内容如下:
/**
* pages页面快速生成脚本
* 用法:npm run tep `文件名`
*/
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 tep 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='${dirName}-wrap'>
</View>
)
}
}
export default ${capPirName}
`
// scss文件模版
const scssTep = `
${dirName}-wrap {
width: 100%;
min-height: 100vh;
}
`
// config 接口地址配置模板
const configTep = `
export default {
test: '/wechat/perfect-info', //xxx接口
}
`
// 接口请求模板
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);
- 添加
./scripts/component.js
,内容如下:
/**
* pages页面快速生成脚本
* 用法:npm run com `文件名`
*/
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 com test');
process.exit(0);
}
//页面模板
const indexTep = `import Taro, { Component } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { ${capPirName}Props, ${capPirName}State } from './${dirName}.interface'
import './${dirName}.scss'
class ${capPirName} extends Component<${capPirName}Props,${capPirName}State > {
constructor(props: ${capPirName}Props) {
super(props)
this.state = {}
}
static options = {
addGlobalClass: true
}
static defaultProps:${capPirName}Props = {}
render() {
return (
<View className='fx-${dirName}-wrap'>
</View>
)
}
}
export default ${capPirName}
`
// scss文件模版
const scssTep = `
${dirName}-wrap {
width: 100%;
}
`
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/components/${dirName}`); // mkdir $1
process.chdir(`./src/components/${dirName}`); // cd $1
fs.writeFileSync(`${dirName}.tsx`, indexTep); //tsx
fs.writeFileSync(`${dirName}.scss`, scssTep); // scss
fs.writeFileSync(`${dirName}.interface.ts`, interfaceTep); // interface
Tip
:最后也是重点,记得在根目录的package.json
的scripts
里加上如下内容:
"scripts": {
...
"tep": "node scripts/template",
"com": "node scripts/component"
}
5.编写业务代码
上面4个步骤基本已经配置完了,接下去进入正题,可以愉快的撸代码了。
运行我们上面写的快速生成脚本,在命令行里输入:
cnpm run tep index
ok,这时候tslint应该不报找不到index的错了,可以看到我们page
文件夹下生成了一个index
的文件夹,里面包含config.ts
、index.interface.ts
、index.scss
、index.tsx
、model.ts
、service.ts
1.改写./src/app.tsx
首先先下载taro的@tarojs/async-await
,在命令行输入如下:
cnpm i --save @tarojs/async-await
下载完了之后,按照如下改写app.tsx
import Taro, { Component, Config } from "@tarojs/taro";
import "@tarojs/async-await";
import { Provider } from "@tarojs/redux";
import "./utils/fxTaroInit.js"
import "./utils/request";
import Index from "./pages/index";
import dva from './utils/dva'
import models from './models'
import './app.scss'
import { globalData } from "./utils/common";
const dvaApp = dva.createApp({
initialState: {},
models: models,
});
const store = dvaApp.getStore();
class App extends Component {
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
* @memberof App
*/
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() { }
// 在 App 类中的 render() 函数没有实际作用
// 请勿修改此函数
render() {
return (
<Provider store={store}>
<Index />
</Provider>
)
}
}
Taro.render(<App />, document.getElementById('app'))
发现tslint
报找不到模块“./models”。
这样的错,不要急,我们在./models
文件夹下创建index.ts
这样一个文件夹,内容如下:
import index from '../pages/index/model';// index 页面的model
// 这里记得export的是数组,不是对象
export default [
index
]
可以发现,tslint
已经不报错了。
2.改写./src/pages/index/config.ts
export default {
getLists: '/japi/toh', // 获取历史上的今天接口
}
3.在./src/config/requestConfig.ts
引入上面配置的接口,如下修改:
import index from '../pages/index/config' // index接口
/**
* 请求的公共参数
*/
export const commonParame = {}
/**
* 请求映射文件
*/
export const requestConfig = {
loginUrl: '/api/user/wechat-auth', // 微信登录接口
...index
}
4.改写./src/pages/index/service.ts
,如下:
import Api from '../../utils/request'
export const getLists = (data) => {
return Api.getLists(data)
}
5.改写./src/pages/index/index.interface.ts
,如下:
/**
* index.state 参数类型
*
* @export
* @interface IndexState
*/
export interface IndexState {
month: number
day: number
}
/**
* index.props 参数类型
*
* @export
* @interface IndexProps
*/
export interface IndexProps {
dispatch?: any,
data?: Array<DataInterface>
}
export interface DataInterface {
day: number
des: string
lunar: string
month: number
pic: string
title: string
year: number
_id: string
}
6.改写./src/pages/index/model.ts
,如下:
// import Taro from '@tarojs/taro';
import * as indexApi from './service';
export default {
namespace: 'index',
state: {
data: [],
key: '72eed010c976e448971655b56fc2324e',
v: '1.0'
},
effects: {
* getLists({ payload }, { select, call, put }) {
const { key, v } = yield select(state => state.index)
const { error, result } = yield call(indexApi.getLists, {
key,
v,
...payload
})
if (!error) {
yield put({
type: 'updateState',
payload: {
data: result
}
})
}
}
},
reducers: {
updateState(state, { payload: data }) {
return { ...state, ...data }
}
}
}
7.改写./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 demo'
}
constructor(props: IndexProps) {
super(props)
this.state = {
month: 0,
day: 0
}
}
// 获取今日数据
async getData(month: number, day: number) {
await this.props.dispatch({
type: 'index/getLists',
payload: {
month: month,
day: day
}
})
}
// 获取系统当前时间并请求参数
getDate() {
const myDate = new Date()
const m = myDate.getMonth() + 1
const d = myDate.getDate()
this.setState({
month: m,
day: d
})
this.getData(m, d)
}
componentDidMount() {
this.getDate()
}
render() {
const { month, day } = this.state
const { data } = this.props
return (
<View className='fx-index-wrap'>
<View className='index-topbar'>
<Text>{`${month}月${day}日`}</Text>
<View>历史上的今天都发生了这些大事</View>
</View>
<View className='index-list'>
{
data && data.map((item, index) => {
return <View className='index-li' key={index}>
<View className='index-bg' style={`background-image: url(${item.pic})`}></View>
<View className='index-info'>
<View className='index-title'>{item.title}</View>
<View className='index-des'>{item.des}</View>
</View>
</View>
})
}
</View>
</View>
)
}
}
export default Index
5.接下来写样式./src/pages/index/index.scss
,如下:
.index {
&-wrap {
width: 100%;
min-height: 100vh;
}
&-topbar {
padding: 10rpx 50rpx;
text-align: center;
font-weight: bold;
Text {
color: rgb(248, 122, 3);
font-size: 40rpx;
}
View {
color: #333;
font-size: 30rpx;
}
}
&-list {
padding: 50rpx;
}
&-li {
box-shadow: 0 4rpx 20rpx rgba($color: #000000, $alpha: 0.1);
margin-bottom: 50rpx;
border-radius: 8rpx;
overflow: hidden;
}
&-bg {
width: 100%;
height: 300rpx;
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
background-color: #f5f5f5;
}
&-info {
padding: 15rpx;
}
&-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 10rpx;
}
&-des {
font-size: 26rpx;
color: #666;
}
}
这时候基本结束了,在命令行运行:
cnpm run dev:weapp
如下显示,说明编译成功(tip
:以后记得先编译,我是之前写好了的,不然很有可能一堆报错,那时候估计你会绝望的)
最后的最后,打开微信开发者工具,选择微信小程序,选择taro-demo文件夹下编译成功的
dist
,appid就用微信提供给你测试的,名字随便输入一个,点击确定,之前步骤都没问题的话,最后显示的结果如下图:小程序界面图
最后,恭喜你,配置完了,可以满足基本开发和需求了,如果有什么错误还望指出
。
网友评论