微信小程序 — 翻译小工具

在看了微信小程序文档之后,觉得挺有意思的,就尝试着写了第一个微信小程序,主页面样式是这样的:

主页面

关键词: Promise、百度翻译api、微信小程序文档、移动端、iconfont、md5加密。

描述: 搜索微信小程序“鹏城翻译”,可以翻译中文—外语外语—中文,可以实现语言切换功能,翻译多个国家语言,同时存储搜索过的历史记录,可以之后查看。

代码步骤:

  • 首先要了解微信小程序文档、框架、组件【微信小程序文档
  • 整理思绪,分析页面功能,理清所需页面

一、页面的实现

  • 将页面分成三个页面,分别是index、change、history
  • 将三个页面所需要的共同样式都写入app.wxss中,如下
@import "./assets/iconfont/iconfont.wxss";
.container {
  padding: 0;
  background-color: #f5fafe;
  height: 100vh;
  display: flex;
  flex-direction: column;
  /* align-items: center; */
  box-sizing: border-box;
  font-size: 30rpx;
  color: #333;
} 
.copyright {
  align-self: center;
  flex: 1;
  display: flex;
  align-items: flex-end;
  padding-bottom: 20rpx;
  font-size: 28rpx;
  color: #999;
}
.view-hover,
.navigator-hover {
  background-color: #f3f3f3!important;
}
  • 配置好app.json如下
{
  "pages":[
    "pages/index/index",
    "pages/change/change",
    "pages/history/history"
  ],
  "window":{
    "backgroundColor":"#333",
    "backgroundTextStyle":"light",
    "navigationBarBackgroundColor": "#1c1b21",
    "navigationBarTitleText": "鹏城翻译",
    "navigationBarTextStyle":"#fff"
  },
  "tabBar":{
    "borderStyle":"white",
    "position":"top",
    "color":"#595959",
    "selectedColor":"#1c1b21",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text":"翻译"
      },
      {
        "pagePath":"pages/history/history",
        "text":"历史"
      }
    ]
  }
}
  • pages
    用于指定小程序由哪些页面组成,每一项都对应一个页面的 路径+文件名 信息。文件名不需要写文件后缀,框架会自动去寻找对于位置的 .json, .js, .wxml, .wxss 四个文件进行处理
    数组的第一项代表小程序的初始页面(首页)。小程序中新增/减少页面,都需要对 pages 数组进行修改

  • window
    用于设置小程序的状态栏、导航条、标题、窗口背景色

  • tabBar
    如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面

index页面的实现

index

1、 在小程序中一个index页面主要是由index.wxml、index.wxss、index.js、等组成的。
2、小程序中的WXML 中的动态数据均来自index.js中对应 Page 的 data。
3、看下我的index.wxml页面代码:

<!--index.wxml-->
<view class="container page-index">
  <view class='change'>
    <navigator url='/pages/change/change' hover-class='navigator-hover'>
      <text>到{{curLang.chs}}</text>
      <text class='iconfont icon-down'></text>
    </navigator>
  </view>
  <view class='input-area'>
    <text class='iconfont icon-close' hidden='{{hideClearIcon}}' bindtap='onTapClose'></text>
    <view class='textarea-wrap'>
      <textarea placeholder='请输入要翻译的文本' placeholder-style='color:#8995a1' bindinput='onInput' bindconfirm='onConfirm' bindblur='onConfirm' value='{{query}}'></textarea>
    </view>
    <view class='text-area'>
      <view class='text-title'>译文</view>
      <view class='text-result' wx:for="{{result}}" wx:key="index">
        <text selectable='true'>{{item.dst}}</text>
      </view>
    </view>
  </view>
  <view class='copyright'>
    <text>@ 鹏城</text>
  </view>
</view>

在小程序中

  • 视图容器用<view></view>
  • navigator是页面链接,相当于html中的a标签
  • hover-class指定按下去的样式类。当 hover-class="none" 时,没有点击态效果
  • 数据绑定使用 Mustache 语法(双大括号)将变量包起来
  • 当用到icon图标的时候,可以新建一个文件夹,其中放入icon图标的代码文件,在app.wxss中应用,以便于所有文件引用,icon图标代码如下图:


    icon.wxss
  • hidden属性,后面值默认为false,在页面中显示出这条代码体现是效果,但在js中可以对hidden属性的默认值进行设置,是否在页面表现效果
  • bindtap在小程序中是代表此容器是可以点击的,在js中对bindtap的值进行操作就可以设置点击后要执行的事件
  • bindinput键盘输入时触发,event.detail = {value, cursor, keyCode}keyCode 为键值,2.1.0 起支持,处理函数可以直接 return 一个字符串,将替换输入框的内容。
  • bindconfirm点击完成按钮时触发,event.detail = {value: value}
  • bindblur输入框失去焦点时触发,event.detail = {value: value}
  • textarea多行输入框。该组件是原生组件,使用时请注意相关限制
    (1) placeholder输入框为空时占位符,与css相同
    (2) placeholder-style指定 placeholder 的样式
  • wx:for在组件上使用 wx:for 控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item

那么index.wxml页面,差不多就可以完成了,然后就是index.wxss和index.js,与css大同小异,遇到不能用的属性是可以看下微信小程序api,总有你需要的效果,如下是index.wxss代码:

/**index.wxss**/
.change {
  color: #8995a1;
  font-size: 24rpx;
  padding: 20rpx 40rpx;
  display: flex;
  align-items: center;
}
.change .icon-down {
  color: #8995a1; 
  font-size: 20rpx;
}
input-area {
  position: relative;
}
.textarea-wrap {
  background: #fff;
  border-bottom: 1px solid #c7cee0;
}
.input-area textarea {
  background-color: #fff;
  padding: 30rpx 0 30rpx 30rpx;
  width: calc(100% - 48rpx);
  margin: 0;
  box-sizing: border-box;
}
.input-area .icon-close {
  position: absolute;
  right: 12rpx;
  top: 90rpx;
  z-index: 100;
  font-size: 40rpx;
  color: #888;
}
.input-area .text-area {
  min-height: 80rpx;
  padding: 40rpx;
  background-color: #fff;
}
.input-area .text-title {
  font-size: 28rpx;
  color: #8995a1;
}
.input-area .text-result {
  padding: 20rpx 0;
}

这里需要说的就是因为在文本中输入内容后会有一个icon显示,以便于我们删除文本中的内容,但如果输入框的宽度撑满,这个icon会被输入框遮住,所以我在写输入框的样式时,让输入框的宽度没有撑满,让出一些,以便于icon的点击

index.js
//index.js
import { translate } from '../../utils/api.js'
const app = getApp()
Page({
  data:{
    query:'',
    hideClearIcon: true,
    result: [],
    curLang: {}
  },
  onLoad: function (options){
    // console.log('lonload')
    // console.log(options,55)
    if(options.query) {
      this.setData({query: options.query})
    }
  },
  onShow: function(){
    // console.log(this.data.curLang.lang)
    // console.log(app.globalData.curLang.lang)
    if(this.data.curLang.lang !== app.globalData.curLang.lang) {
      this.setData({curLang: app.globalData.curLang})
      this.onConfirm()
    }
  },
  onInput: function(e) {
    // console.log(e.detail.value)
    this.setData({'query': e.detail.value})
    if(this.data.query.length > 0) {
      this.setData({'hideClearIcon':false})
    }else{
      this.setData({ 'hideClearIcon': true})
    }
  },
  onTapClose: function(){
    this.setData({query: '',hideClearIcon: true})
  },
  onConfirm: function(){
    // console.log(this.data,'55')
    if(!this.data.query) return
    translate(this.data.query,{from:'auto',to: this.data.curLang.lang}).then(res=>{
      // console.log(res.trans_result,'99')
      //console.log(res,'5555')
      this.setData({'result': res.trans_result})
      
      //console.log(wx.getStorageSync('history'))
      let history = wx.getStorageSync('history') || []
      history.unshift({query: this.data.query, result: res.trans_result[0].dst})
      history.length = history.length > 10 ? 10 : history.length
      wx.setStorageSync('history', history)
    })
  }
})
Page()

看了微信小程序的文档后都知道,在js文件中,Page(Object) 函数用来注册一个页面。接受一个 Object 类型参数,其指定页面的初始数据、生命周期回调、事件处理函数等。

  • data:
    data 是页面第一次渲染使用的初始数据
    将会以JSON字符串的形式由逻辑层传至渲染层,因此data中的数据必须是可以转成JSON的类型:字符串,数字,布尔值,对象,数组。
    渲染层可以通过 WXML 对数据进行绑定。

  • onLoad:
    onLoad(Object query)
    页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数。

  • onShow:
    onShow()
    页面显示/切入前台时触发。

  • Page.prototype.setData(Object data, Function callback)
    setData 函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data 的值(同步)
    Object 以 key: value 的形式表示,将 this.data 中的 key 对应的值改变成 value。

其中 key 可以以数据路径的形式给出,支持改变数组中的某一项或对象的某个属性,如 array[2].message,a.b.c.d,并且不需要在 this.data 中预先定义。

注意:
1、 直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。
2、 仅支持设置可 JSON 化的数据。
3、 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。
4、 请不要把 data 中任何一项的 value 设为 undefined ,否则这一项将不被设置并可能遗留一些潜在问题

  • onInput onTapClose onConfirm 这三个key都是WXML
    bindinputbindtapbindconfir/bindblur的值,然后在js中进行事件绑定

页面逻辑

1、实现在输入框中输入内容时,对onInput进行绑定,显示icon,通过设置hideClearIcon的值为false还是true来控制icon
2、在icon显示后,点击icon删除输入内容,对onTapClose进行事件绑定
3、在输入确定内容后对内容进行翻译,对onConfirm进行绑定,准备翻译,然后我想到翻译是一件事情,所以我对它进行了一个封装,在utils里加了一个api.js,这里我用到百度翻译的接口,所以需要查阅一下百度翻译api,以下是api.js:

import md5 from './md5.min.js'

const appid = '20180912000205732'
const key = 'oowRg0jxzoSgwCfAfyVn'

function translate(q, { from = 'auto', to = 'auto' } = { from: 'auto', to: 'auto'}) {
  return new Promise((resolve, reject) => {
    let salt = Date.now()
    let sign = md5(`${appid}${q}${salt}${key}`)
    wx.request({
      url: 'https://fanyi-api.baidu.com/api/trans/vip/translate',
      data: {
        q,
        from,
        to,
        appid,
        salt,
        sign
      },
      success(res) {
        if(res.data && res.data.trans_result) {
          // console.log(res,'1')
          // console.log(res.data,'2')
          // console.log(res.data.trans_result,'3')
          resolve(res.data)
        } else{
          reject({status: 'error', msg: '翻译失败'})
          wx.showToast({
            title: '翻译失败',
            icon: 'none',
            duration: 3000
          })
        }
      },
      fail() {
        reject({status: 'error', msg: '翻译失败'})
        wx.showToast({
          title: '网络异常',
          icon: 'none',
          duration: 3000
        })
      }
    })
  })
}
module.exports.translate = translate

在微信小程序中用wx.request请求数据,需要注意的是百度翻译api要用md5进行加密,所以也需要在utils文件夹中再加入一个md5.min.js文件,这里md5文件最好是用压缩过的,再由import来引入api.js文件中,但我没有直接去使用它,而是用Promise对它进行一个封装

代码解析:
1、 wx.request发起 HTTPS 网络请求。使用前请注意阅读相关说明
2、 wx.showToast(Object object)显示消息提示框
3、 一个模块要想对外暴露其内部的私有变量与函数,只能通过 module.exports 实现。exports: 通过该属性,可以对外共享本模块的私有变量与函数

最后在index.js页面中用import引入api.js,这样就完成了index页面的接口问题,再就是index页面的参数问题,如要翻译成什么语言,这就需要change页面来实现了

change页面的实现

change

先看下我的change.wxml代码:

<view class="container lang-list">
  <view class='title'>翻译成</view>
  <view class='item' data-chs="{{language.chs}}" data-lang="{{language.lang}}" data-index="{{index}}" wx:for="{{langList}}" wx:key="index" wx:for-item="language" bindtap='onTapItem' hover-class='view-hover'>
    <view class='item-inner'>
      <text>{{language.chs}}</text>
      <text class='iconfont icon-duihao' wx:if="{{index===curLang.index}}"></text>
    </view>
  </view>
</view>
  • 其中data-chs data-lang data-index三个都是自定义属性
  • wx:key 如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 <input/> 中的输入内容,<switch/> 的选中状态),需要使用 wx:key 来指定列表中项目的唯一的标识符。
    wx:key的值以两种形式提供:
    1、字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变。
    2、保留关键字*this 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字,如:
    当数据改变触发渲染层重新渲染的时候,会校正带有 key 的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。
    如不提供 wx:key,会报一个warning, 如果明确知道该列表是静态,或者不必关注其顺序,可以选择忽略。
  • wx:for-item 可以指定数组当前元素的变量名,
  • wx:if在框架中,使用 wx:if="{{condition}}" 来判断是否需要渲染该代码块

再看下change.js代码:

//change.js
const util = require('../../utils/util.js')
const app = getApp()
Page({
  data: {
    curLang: {},
    langList: app.globalData.langList
  },
  onShow: function(){
    this.setData({ curLang: app.globalData.curLang})
  },
  onTapItem: function(e) {
    let langObj = e.currentTarget.dataset  //存储自定义属性
    // console.log(e.currentTarget.dataset)
    // console.log(e)
    wx.setStorageSync('curLang',langObj)
    this.setData({'curLang': langObj})
    app.globalData.curLang = langObj
    wx.switchTab({
      url: '/pages/index/index',
    })
  }
})

写到这里的时候,我们发现需要一个函数来保存变量,以便于在index页面翻译,也便于在change页面选择翻译语言的时候引用,所以我把这个函数写在app.js中,方便其他页面调用,调用方法便是const app = getApp(),以下是app.js:

//app.js
App({
    onLaunch: function () {
      //展示本地存储能力
        this.globalData.curLang = wx.getStorageSync('curLang') || this.globalData.langList[0]
    },
    globalData: {
      curLang: {},
      langList: [
        {
          'chs': '英文',
          'lang': 'en',
          "index": 0
        },
        {
          'chs': '中文',
          'lang': 'zh',
          "index": 1
        },
        {
          'chs': '日语',
          'lang': 'jp',
          "index": 2
        },
        {
          'chs': '韩语',
          'lang': 'kor',
          "index": 3
        },
        {
          'chs': '法语',
          'lang': 'fra',
          "index": 4
        },
        {
          'chs': '西班牙语',
          'lang': 'spa',
          "index": 5
        },
        {
          'chs': '阿拉伯语',
          'lang': 'ara',
          "index": 6
        },
        {
          'chs': '文言文',
          'lang': 'wyw',
          "index": 7
        },
        {
          'chs': '泰语',
          'lang': 'th',
          "index": 8
        },
        {
          'chs': '繁体中文',
          'lang': 'cht',
          "index": 9
        }
      ]
    }
})

以上两个文件中的js需要注意的代码:

  • wx.setStorageSync

wx.setStorageSync(string key, Object|string data)
(1) string key
本地缓存中指定的 key
(2) Object|string data
需要存储的内容

  • wx.getStorageSync

Object|string wx.getStorageSync(string key)
(1) string key
本地缓存中指定的 key
(2)Object|string data
key对应的内容

  • wx.switchTab跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
  • 在执行onTapItem事件时,可以得到参数e,并且e.currentTarget.dataset可以储存自定义的属性,以便于页面使用

然后在联系上最初的index页面,我们就可以选择需要翻译的语言了,同时在本地缓存中存储了当前翻译的语言,下次打开小程序时,还是上次保存的翻译语言

history页面实现

history

看一看history.wxml:

<!--pages/history/history.wxml-->
<scroll-view scroll-y class="container">
  <view class="history-list">
    <view class="title">翻译历史</view>
    <view class="item" wx:for="{{history}}" wx:key="index" bindtap='onTapItem' data-query="{{item.query}}" data-langId="{{item.langIndex}}">
      <view class="query">{{item.query}}</view>
      <view class="result">{{item.result}}</view>
    </view>
  </view>
</scroll-view>

history.wxml中主要的就是把它当做一个数组去渲染,然后点击对应数组,跳转到index页面,进行当前语言翻译,而这些功能需要在history.js和change.js中去实现

看一看history.js:

//pages/history/history.js
const app = getApp()

Page({
  data: {
    history: []
  },
  onShow: function() {
    this.setData({history: wx.getStorageSync('history')})
  },
  onTapItem: function(e) {
    wx.reLaunch({
      url: `/pages/index/index?query=${e.currentTarget.dataset.query}`
    })
  }
})

当执行以上代码时,便会用到index.js中onConfirm事件的这一段代码:

let history = wx.getStorageSync('history') || []
      history.unshift({query: this.data.query, result: res.trans_result[0].dst})
      history.length = history.length > 10 ? 10 : history.length
      wx.setStorageSync('history', history)

其中存储好history的值,便于history页面去使用

  • db.command.unshift
    更新指令,对一个值为数组的字段,往数组头部添加一个或多个值。或字段原为空,则创建该字段并设数组为传入值。
  • wx.reLaunch(Object object)
    关闭所有页面,打开到应用内的某个页面
    (1) url : 需要跳转的应用内页面路径 , 路径后可以带参数。参数与路径之间使用?分隔,参数键与参数值用=相连,不同参数用&分隔;如 path?key=value&key2=value2,如果跳转的页面路径是 tabBar 页面则不能带参数

当切换到index页面时,会用到index.js中的这些代码:

 onLoad: function (options){
    // console.log('lonload')
    // console.log(options,55)
    if(options.query) {
      this.setData({query: options.query})
    }
  },
  onShow: function(){
    // console.log(this.data.curLang.lang)
    // console.log(app.globalData.curLang.lang)
    if(this.data.curLang.lang !== app.globalData.curLang.lang) {
      this.setData({curLang: app.globalData.curLang})
      this.onConfirm()
    }
  }

其中onLoad在加载页面中设置query的值,onShow在显示页面后翻译query的值

这样就构成了整个小程序目前需要的需求,如果还想有其他更多的需求,可以在此代码上继续添加

The early bird catches the worm

推荐阅读更多精彩内容