目录

4实战篇1小程序开发环境搭建

本资源由 itjc8.com 收集整理

实战篇 1:小程序开发环境搭建

微信小程序虽然提供自己的 IDE 开发工具,但是对于用惯了 VS Code、Sublime 等编辑器的前端工程师来说,其体验还是挺差的,因此本项目中,只将微信小程序开发者工具作为模拟器、调试和代码上传的工具,其他开发使用自己熟练的编辑器 / IDE 即可。

除了选择自己熟练的编辑器 / IDE 之外,还应该在代码层面提高编码体验,本实战项目使用 Sass 和 ES6 语法来写代码,通过构建工具编译成小程序可以识别的 WXSS 和 ES5,最后也使用构建工具压缩和优化静态资源。

对于小程序云开发(腾讯云)的测试,本项目使用官方提供的 SCF-Cli 来本地测试云函数,这样云函数的修改就不需要每次都上传到云端之后再测试了,可以提高研发效率。

整体技术选型如下:

  • VS Code: 编辑器,用于代码编写
  • Gulp:前端项目构建工具
  • Sass:小程序样式表
  • ES6:采用 ES6 语法编写 JS 代码,Babel 做编译处理

本节重点介绍使用 Gulp 搭建小程序开发环境。

Gulp 和 webpack

目前,前端最火的打包工具无疑是 webpack,而 webpack 的产品定位是模块打包工具,对于小程序开发,涉及项目资源分类管理,所以 GulpGruntFIS 这类前端工程构建工具很合适。

Gulp 可以对不同的文件类型、文件夹、文件等多种方式进行不同的处理流程,像小程序项目中多种文件类型需要不同的构建流程,使用 Gulp 的 task 就非常方便管理。

另外 Gulp 的 watch 功能也可以监控源文件,当源码发生变化时,立即执行对应 task,将修改后的代码编译到小程序开发工具监控的目录中;在生态建设上,Gulp 工具链也很完善。小程序开发本来就是本地开发模式,代码必须在小程序开发者工具提供的 Runtime 中才可以跑起来,不涉及服务搭建相关的知识,所以 webpack 的 devserver 也没有用武之地。

综上,本小册采用 Gulp 来搭建小程序开发环境。

项目目录结构

首先介绍下项目的目录结构,下面的目录结构是最开始的目录结构,注释中描述了文件夹(或文件)具体是做什么用的。

├── cloud-functions // 云函数文件夹
├── dist            // 构建工具 release 之后的文件夹
├── gulpfile.js     // Gulp 配置文件
├── node_modules
├── package.json    // npm 描述文件
└── src             // 实际开发的源代码文件夹
    ├── app.js      // 入口 js
    ├── app.json    // App 配置
    ├── app.scss    // App 整体样式
    ├── components  // 小程序组件,例如 icon 类这些通用组件
    ├── images      // 小程序静态图片
    ├── lib         // 公共 lib
    ├── pages       // 小程序 page 页面
    │   ├── index.js
    │   ├── index.json
    │   ├── index.scss
    │   ├── index.wxml
    │   └── index.wxs
    └── project.config.json // 小程序项目配置

Gulp 工程化打包方案

针对上面的开发目录,我们要达到的目标是:将 src 目录下的文件,编译到小程序开发者工具实际运行的 dist 目录下,先在 gulpfile.js 中定义这两个目录的变量:

const src = './src'
const dist = './dist'

Gulp 是以 task 为核心的打包工具,针对不同的文件类型(比如通过正则过滤)可以配置不同的流程控制。小程序打包主要解决的是 WXML、WXSS、WXS 以及 JS 的编译,另外针对小程序开发中常见的问题进行工具化处理,例如 px 转 rpx、压缩优化等,下面笔者来一一介绍。

wxml task

wxml 语法实际就是 html 的语法,不需要做额外的处理,直接 release 到目标目录即可:

gulp.task('wxml', () => {
  return gulp
    .src(`${src}/**/*.wxml`)
    .pipe(gulp.dest(dist))
})

wxss task

为了更好地维护和提供更加灵活的 CSS 开发体验,笔者在项目中使用了 sass 作为 wxss 的开发语言,然后通过 Gulp 的 wxss task 将scss/sass文件编译成 wxss,在处理样式文件的时候,笔者还解决了两个问题:

  • px 转 rpx:使用postcss-px2rpx,将px按照 2 倍算法转化成 rpx,px 和 rpx 的详细介绍可以参考前面章节的内容
  • 将 webfont 转化成 base64 引入:在小程序内,webfont 不允许访问小程序内部地址,所以只能将其转化成 bas64 方式引入

sass/scss 文件处理完之后,在最后一步,利用 rename 工具,将 .sass/.scss 改名为 .wxss

const rename = require('gulp-rename')
const postcss = require('gulp-postcss')
const pxtorpx = require('postcss-px2rpx')
const base64 = require('postcss-font-base64')
const combiner = require('stream-combiner2')

gulp.task('wxss', () => {
  const combined = combiner.obj([
    gulp.src(`${src}/**/*.{wxss,scss}`),
    sass().on('error', sass.logError),
    postcss([pxtorpx(), base64()]),
    rename((path) => (path.extname = '.wxss')),
    gulp.dest(dist)
  ])

  combined.on('error', handleError)
})

可以不使用 CSS 的自动添加浏览器兼容前缀的 autoprefixer 插件,而直接用小程序开发者工具的「详情 -> 项目设置 -> 上传代码时样式自动补全」功能。

https://user-gold-cdn.xitu.io/2018/8/13/1653140645f84aaa?w=377&h=361&f=png&s=34728

js task

微信的 js 文件使用的是 ES5 语法,为了更好的开发体验,笔者开发中使用了 ES6/7 语法,在 Gulp 编译时引入了 babel 插件对 js 进行编译,并且还引入了 sourcemap 以方便本地 debug 代码。

gulp.task('js', () => {
  gulp
    .src(`${src}/**/*.js`)
    .pipe(sourcemaps.init())
    .pipe(
      babel({
        presets: ['env']
      })
    )
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest(dist))
})

其他 task

对于 jsonimageswxs 类文件,主要采取的方式是按照当前路径复制到目标目录,所以它们的 task 配置是:

gulp.task('json', () => {
  return gulp.src(`${src}/**/*.json`).pipe(gulp.dest(dist))
})
gulp.task('images', () => {
  return gulp.src(`${src}/images/**`).pipe(gulp.dest(`${dist}/images`))
})
gulp.task('wxs', () => {
  return gulp.src(`${src}/**/*.wxs`).pipe(gulp.dest(dist))
})

给每个 task 增加生产发布打包配置

针对开发和生产两种不同的发布环境,可以通过自定义 Gulp 命令参数来区分,这里使用 --type 来区分,即:

  • –type prod:代表生产发布打包
  • 默认:为开发发布打包

在生产发布打包的流程中,增加了对资源的压缩(js、html、json、css)和 jdists 的代码块预处理,下面以 js task 为例,解释下怎么配置生产发布的流程(详细解释见注释):

// 引入需要用到的 npm 包
const sourcemaps = require('gulp-sourcemaps')
const jdists = require('gulp-jdists')
const through = require('through2')
const babel = require('gulp-babel')
const uglify = require('gulp-uglify')
const argv = require('minimist')(process.argv.slice(2))
// 判断 gulp --type prod 命名 type 是否是生产打包
const isProd = argv.type === 'prod'
const src = './client'
const dist = './dist'

gulp.task('js', () => {
  gulp
    .src(`${src}/**/*.js`)
    // 如果是 prod,则触发 jdists 的 prod trigger
    // 否则则为 dev trigger,后面讲解
    .pipe(
      isProd
        ? jdists({
            trigger: 'prod'
          })
        : jdists({
            trigger: 'dev'
          })
    )
    // 如果是 prod,则传入空的流处理方法,不生成 sourcemap
    .pipe(isProd ? through.obj() : sourcemaps.init())
    // 使用 babel 处理js 文件
    .pipe(
      babel({
        presets: ['env']
      })
    )
    // 如果是 prod,则使用 uglify 压缩 js
    .pipe(
      isProd
        ? uglify({
            compress: true
          })
        : through.obj()
    )
    // 如果是 prod,则传入空的流处理方法,不生成 sourcemap
    .pipe(isProd ? through.obj() : sourcemaps.write('./'))
    .pipe(gulp.dest(dist))
})

说下 jdists 代码块预处理工具,jdists是一种通过注释的方式,将不同的代码块根据不同的指令进行处理的工具,详细功能见 jdists 文档

本项目中主要用到了:

  1. 根据 trigger 触发 remove 操作;
  2. 根据 import 将媒介(资源)嵌入到文件的固定位置。

例如:

/*<remove trigger="prod">*/
import {getMood, geocoder} from '../../lib/api'
import {getWeather, getAir} from '../../lib/api-mock'
/*</remove>*/

/*<jdists trigger="prod">
import {getMood, geocoder, getWeather, getAir} from '../../lib/api'
</jdists>*/

上面的代码片段中,/*<remove trigger="prod">*/.../*</remove>*/ 之间是默认代码,从命名来看,实际 getWeathergetAir 两个方法来自 api-mock 这个 js 文件,api-mock 是接口的 mock 实现。真实上线的时候,我们希望暴露的是底部 <jdists trigger="prod">...</jdists>*/ 中间的代码,这样在下面 Gulp 的配置中:

.pipe(
      isProd
        ? jdists({
            trigger: 'prod'
          })
        : jdists({
            trigger: 'dev'
          })
    )

isProd 成立时,则触发 trigger=prod,即将顶部代码库移出,底部注释中的代码暴露出来,最终得到的代码如下:

import {getMood, geocoder, getWeather, getAir} from '../../lib/api'

而普通打包(dev 开发方式时)则保持原样:

/*<remove trigger="prod">*/
import {getMood, geocoder} from '../../lib/api'
import {getWeather, getAir} from '../../lib/api-mock'
/*</remove>*/

/*<jdists trigger="prod">
import {getMood, geocoder, getWeather, getAir} from '../../lib/api'
</jdists>*/

通过上面的讲解,你应该明白了,在 gulp --type prod 下,getWeathergetAir 来自 lib/api 文件,而在本地开发调试的时候,则来自 api-mock 这个 mock 的文件中,至于这俩文件有什么区别,以及 jdistsimport 用法,见本小节的「mock server 实现」部分。

根据发布环境不同,对 task 进行聚合

上面单个 task 配置完毕,需要添加聚合类的 task 和 watch task,详细配置如下:

gulp.task('watch', () => {
  ;['wxml', 'wxss', 'js', 'json', 'wxs'].forEach((v) => {
    gulp.watch(`${src}/**/*.${v}`, [v])
  })
  gulp.watch(`${src}/images/**`, ['images'])
  gulp.watch(`${src}/**/*.scss`, ['wxss'])
})

gulp.task('clean', () => {
  return del(['./dist/**'])
})

gulp.task('dev', ['clean'], () => {
  runSequence('json', 'images', 'wxml', 'wxss', 'js', 'wxs', 'cloud', 'watch')
})

gulp.task('build', ['clean'], () => {
  runSequence('json', 'images', 'wxml', 'wxss', 'js', 'wxs', 'cloud')
})

mock server 实现

小程序云函数的联调测试是相当麻烦的,每次修改代码,都需要跑到小程序开发者工具的编辑器中,选择云函数文件夹「上传并部署」:

https://user-gold-cdn.xitu.io/2018/8/13/165313fec208c69b?w=442&h=400&f=png&s=67432

这样的开发效率是十分低的,所以笔者自研了一套云函数本地 mock 的方法,使用 mock server 可以在本地开发的时候直接使用 wx.request 方法调用 mock server 的接口,而真正上线的时候(或者发布测试的时候),则使用 wx.cloud.callFunction 方式调用。

mock server 的职责:

  • 本地开发时,将云函数代理到 localserver,免除每次上传云函数测试效果的低效率研发方式
  • 要设计一套方案,将云函数文件单独提取出来,做到 mock server 和上线后代码统一,不做二次开发(修改),降低开发成本
  • 把将来放到服务器管理的静态资源(如图片 icon 类等)暂时放到本地托管,方便本地开发使用

基于上面的职责,笔者将小程序项目结构调整如下:

├── README.md
├── client                    // 小程序 client 部分,主要编写内容
│   ├── app.js
│   ├── app.json
│   ├── app.scss
│   ├── project.config.json  // 小程序项目配置,如云函数文件夹
│   ├── components           // 组件
│   ├── images               // 图片资源
│   ├── lib
│   │   ├── api-mock.js      // api-mock 功能,详见文档「云函数 mock」部分
│   │   ├── api.js           // 实际 api
│   │   ├── bluebird.js
│   │   └── util.js
│   └── pages
│       └── index
├── config.server.json
├── dist
├── gulpfile.js
├── package.json
├── server                   // 小程序 server 部分,主要是静态资源和云函数
│   ├── cloud-functions
│   │   ├── test
│   │   └── test2
│   ├── index.js
│   ├── inline               // 云函数公共模块,打包的时候会 inline 进引入的云函数
│   │   └── utils.js
│   └── static
│       └── gulp.png
└── test                     // 测试文件夹
    └── functions            // 存储小程序云函数测试用的参数模板
        └── test.json

主要变化如下:

  1. 跟前端相关的文件都放入了 client 中,编译后放到 dist 目录中,小程序开发者工具开发目录选择 dist 文件夹
  2. 跟 mock server 相关的放入 server 中,server 下文件不做打包处理,即不 release 到 dist 文件下
  3. 其中 server/cloud-functions 是云函数文件夹,编译之后放到 dist/cloud-functions 下
  4. server/static 文件夹是静态资源文件夹,将来上传到小程序云开发的「文件管理」中维护(小程序云开发 CDN 静态资源服务器)

使用 Express 来实现 mock server

笔者使用 Express 来在本地实现一个 mock server:

const express = require('express')
const {PORT} = require('../config.server.json')
const app = express()

app.listen(PORT, () => {
  console.log(`开发服务器启动成功:http://127.0.0.1:${PORT}`)
})

这样就开启了一个端口号为 3000 的本地服务。

实现静态资源服务

下面要做的就是使用 express.staticserver/static 目录设置为静态资源服务器:

// 添加static
app.use(
  '/static',
  express.static(path.join(__dirname, 'static'), {
    index: false,
    maxage: '30d'
  })
)

静态资源服务器添加好之后,访问 http://127.0.0.1:3000/static/xxx 就可以直接访问 static 文件夹下的静态资源了。

实现云函数服务

为了满足「云函数文件线上和 mock server 使用一份,不二次开发」的需求,我们直接按照云函数的写法写代码即可,比如 cloud-functions/test/ 模块:

exports.main = async (event) => {
  let {a, b} = event
  return new Promise((resolve, reject) => {
    resolve({result: parseInt(a) + parseInt(b)})
  })
}

server/index.js 中引入对应的模块,然后分配一个路由即可:

const test = require('./cloud-functions/test/').main

app.get('/api/test', (req, res, next) => {
  // 将 req.query 传入
  test(req.query).then(res.json.bind(res)).catch((e) => {
    console.error(e)
    next(e)
  })
  // next()
})

上面代码中,将 req.query 传入 test.main,构造一个云函数的 event 参数,用于获取云函数的参数,最后通过 Promisethen 传递给 res.json 输出。

写完上面代码,再访问 http://127.0.0.1:3000/api/test?a=1&b=2 就会输出:

https://user-gold-cdn.xitu.io/2018/8/13/165313f947bd3ad9?w=350&h=105&f=png&s=12377

使用 nodemon 对 server 进行自动重启

在云函数开发中,当文件改动了,需要重启 Node.js 服务,如果每次都手动操作就太消耗时间和精力了,所以引入了 nodemon 对 server 目录下文件进行监控,发现文件修改,则重启 Node.js 服务。nodemon 的重启命令放在 package.json 中维护:

// 启动
"scripts": {
  "server": "nodemon ./server/index.js"
},
// nodemon 配置
"nodemonConfig": {
  "ignore": ["test/*", "book/*", "client/*", "bin/*", "node_modules", "dist/*", "package.json"],
  "delay": "1000"
},

效果如下图所示。

https://user-gold-cdn.xitu.io/2018/8/13/165313f69b72a696?w=372&h=182&f=png&s=42545

前端对云函数的调用

mock server 中的云函数实现了一套代码在本地和线上都可以跑通,但是 client 中页面引用云函数使用 wx.cloud.callFunction 却不能实现一套代码通用,为解决这个问题,笔者通过 jdistsremovetrigger 方式来实现差异化管理,即

  1. 将云函数调用等 API 接口请求调用方法,统一放入 client/lib/api.js 中实现,api.js 中使用 wx.cloud.callFunction 方法
  2. 将云函数相关的再用 wx.request 方法实现一下,请求本地 127.0.0.1:3000/api/ 接口,代码在 api-mock.js 中实现
  3. api.jsapi-mock.js 输入的参数和输出的结果是一致的,而内部实现是不同的
  4. 使用某个云函数时,通过上文提到的 jdistsremovetrigger 分别引入

继续拿 test 这个云函数做说明,api.js 中直接使用:

export const test = (a, b) => {
  return wx.cloud.callFunction({
    name: 'test',
    data: {
      a, b
    }
  })
}

然后在 api-mock.js 中实现一次:

// 因为小程序的 callfunction 是 Promisify 的,所以这里需要用 Promise 处理一下
// 小程序中不支持 Promise,所以引入了 bluebird 这个库
import Promise from './bluebird'
export const test = (a,b) => {
  return new Promise((resolve, reject) => {
    wx.request({
      url: 'http://127.0.0.1:3000/api/test',
      {a,b},
      success: (res) => {
        resolve({result: res.data})
      },
      fail: (e) => {
        reject(e)
      }
    })
  })
}

小结

本节主要讲解了 Gulp 构建小程序开发脚手架,从 Gulp 的配置说起,介绍了 WXML、Sass、ES6/7 编写小程序前端代码,然后针对云函数开发测试体验不好的问题,介绍了使用 Express 实现本地 mock server 的方式,将云函数和静态资源文件在本地服务器统一管理,实现「一套代码,多处执行」的效果。

关于上面小程序开发用到的环境搭建代码,笔者从天气小程序项目中整理了出来,作为一个小程序开发脚手架放到了 GitHub 上,方便读者快速创建自己的小程序开发环境。