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 的产品定位是模块打包工具,对于小程序开发,涉及项目资源分类管理,所以 Gulp、Grunt、FIS 这类前端工程构建工具很合适。
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
插件,而直接用小程序开发者工具的「详情 -> 项目设置 -> 上传代码时样式自动补全」功能。
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
对于 json
、images
和 wxs
类文件,主要采取的方式是按照当前路径复制到目标目录,所以它们的 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 文档。
本项目中主要用到了:
- 根据
trigger
触发remove
操作; - 根据
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>*/
之间是默认代码,从命名来看,实际 getWeather
和 getAir
两个方法来自 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
下,getWeather
和 getAir
来自 lib/api
文件,而在本地开发调试的时候,则来自 api-mock
这个 mock 的文件中,至于这俩文件有什么区别,以及 jdists
的 import
用法,见本小节的「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 实现
小程序云函数的联调测试是相当麻烦的,每次修改代码,都需要跑到小程序开发者工具的编辑器中,选择云函数文件夹「上传并部署」:
这样的开发效率是十分低的,所以笔者自研了一套云函数本地 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
主要变化如下:
- 跟前端相关的文件都放入了 client 中,编译后放到 dist 目录中,小程序开发者工具开发目录选择 dist 文件夹
- 跟 mock server 相关的放入 server 中,server 下文件不做打包处理,即不 release 到 dist 文件下
- 其中 server/cloud-functions 是云函数文件夹,编译之后放到 dist/cloud-functions 下
- 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.static
将 server/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
参数,用于获取云函数的参数,最后通过 Promise
的 then
传递给 res.json
输出。
写完上面代码,再访问 http://127.0.0.1:3000/api/test?a=1&b=2
就会输出:
使用 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"
},
效果如下图所示。
前端对云函数的调用
mock server 中的云函数实现了一套代码在本地和线上都可以跑通,但是 client
中页面引用云函数使用 wx.cloud.callFunction
却不能实现一套代码通用,为解决这个问题,笔者通过 jdists
的 remove
和 trigger
方式来实现差异化管理,即
- 将云函数调用等 API 接口请求调用方法,统一放入
client/lib/api.js
中实现,api.js
中使用wx.cloud.callFunction
方法 - 将云函数相关的再用
wx.request
方法实现一下,请求本地127.0.0.1:3000/api/
接口,代码在api-mock.js
中实现 api.js
和api-mock.js
输入的参数和输出的结果是一致的,而内部实现是不同的- 使用某个云函数时,通过上文提到的
jdists
的remove
和trigger
分别引入
继续拿 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 上,方便读者快速创建自己的小程序开发环境。