小程序工程化尝试(一)- gulp

为什么不用wepy、mpvue、taro、uni-app等框架

基本情况:

  • 组内:没做过小程序,会Vue;React本人会一点点,其他成员暂未接触
  • 组外:声称要得急,声称只做微信端

分析:

  • wepy: 小程序本来就是一套新语法,又整一套DSL,声称要得急,来不及学
  • taro: 个人觉得看上去还不错,限于React
  • mpvue uni-app: 都是Vue系列,风评参半
  • 个人考虑:小程序本来就是坑,再套一层框架不知道会带来多大的问题;其次第一次做小程序,且只要微信端,想直接上原生进行尝试有多坑

但是,微信小程序的工程化真的菜,开发体验是真的差…

存在的问题

  • ES6+: 大部分都没什么问题,小程序开发工具提供了ES6转ES5的选项。是不是全支持我不知道,但我知道勾选以后async/await是用不了的…会报regeneratorRuntime is not defined的错误。网传方法是:引入runtime,我试了,亲测无效…
  • WXSS: 被SCSS惯坏了,觉得这玩意儿写起来好累;其次,组内大佬问我,SCSS前插全局的@import能实现不?
  • rpx: 小程序搞的单位,按照我司一贯375px的设计稿,自己手写rpx需要乘2,当然是不愿意手写的..
  • 图片: 组内大佬问我,能自动把小图片转base64么?
  • 别名(alias): 文件层次深了以后就会出现../../../../../a.js,过于精污
  • 环境变量: 查遍小程序文档,只字不提环境变量。
  • 其他: ESLint,Prettier,stylelint,git flow,git hooks 这些配置在前端组内已经蛮成熟了,好说。

选型

其实一开始觉得存在的问题不是很大,所以觉得gulp就能解决了,实际做了以后会发现没想象的那么简单,一怒之下第二版直接上了webpack…这里先说一下第一版的思路

项目结构

1
2
3
4
5
6
7
...
├── dist
├── gulpfile.js
├── package.json
├── postcss.config.js
├── project.config.json
└── src

主要思路是src下面写代码,通过gulp处理生成dist目录,因此需要在小程序的project.config.json文件中添加"miniprogramRoot": "dist/",告诉开发者工具小程序的入口是dist

gulpfile

准备工作:

1
2
const { src, dest, parallel, watch, series, task } = require('gulp')
const path = require('path')

文件glob

1
2
3
4
5
6
const paths = {
scss: ['src/**/*.scss', '!src/**/_*.scss'],
js: 'src/**/*.js',
wxml: 'src/**/*.wxml',
base: 'src/**/*.{json,wxs}'
}

scss

  1. scss编译为css
  2. postcss处理rpx
  3. 前插全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// gulpfile.js
const sass = require('gulp-sass')
sass.compiler = require('node-sass')
const rename = require('gulp-rename')
const postcss = require('gulp-postcss')
const postcssConfig = require('./postcss.config')
const header = require('gulp-header')

const sassConfig = {
header: `
@import "@/assets/styles/_variable.scss";
@import "@/assets/styles/_mixins.scss";
`
}

function scssTack() {
return src(path.scss)
.pipe(header(sassConfig.header))
.pipe(sass({ errorLogToConsole: true }))
.on('error', sass.logError)
.pipe(postcss(postcssConfig))
.pipe(rename({ extname: '.wxss' }))
.pipe(dest('dist'))
}

插件写的比较简陋..proportion是比率,minPixelValue是需要转换的最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// postcss.config.js
const postcss = require('postcss')
const pxRegExp = /"[^"]+"|'[^']+'|url\([^\)]+\)|(\d*\.?\d+)px/g

const px2rpx = postcss.plugin('postcss-px2rpx', (opts = {}) => {
const { proportion = 1, minPixelValue = 0 } = opts

return root => {
root.replaceValues(pxRegExp, { fast: 'px' }, string => {
const pixels = parseInt(string)
if (pixels < minPixelValue) return `${pixels}px`
return `${proportion * parseInt(string)}rpx`
})
}
})

module.exports = [
px2rpx({
proportion: 2,
minPixelValue: 5
})
]

js

只搞了eslint

1
2
3
4
5
6
7
// gulpfile.js
function jsTask() {
return src(paths.js)
.pipe(eslint({ fix: true }))
.pipe(eslint.format())
.pipe(dest('dist'))
}

直接复制的

1
2
3
4
// gulpfile.js
function copyTask() {
return src(paths.base).pipe(dest('dist'))
}

环境变量

原生不支持,自己写env文件,用替换的方式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// gulpfile.js
const dotenv = require('dotenv')

const env = process.env.NODE_ENV
const envFilePath = path.resolve(__dirname, `.env.${env}`)
dotenv.config({ path: envFilePath })
const { API_BASE_URL } = process.env


function jsTask() {
return src(paths.js)
.pipe(aliases(aliasConfig))
.pipe(replace('__API_BASE_URL__', API_BASE_URL)) // 加这里
.pipe(eslint({ fix: true }))
.pipe(eslint.format())
.pipe(dest('dist'))
}
1
2
3
// package.json
"dev": "cross-env NODE_ENV=development gulp",
"build": "cross-env NODE_ENV=production gulp build"
1
2
3
4
5
6
7
# .env.development
API_BASE_URL = '/dev/'
...

# .env.production
API_BASE_URL = '/prod/'
...

使用方法: const baseURL = '__API_BASE_URL__'

alias

用的别人的插件 gulp-wechat-weapp-src-alisa

1
2
3
4
5
6
7
8
9
10
const aliases = require('gulp-wechat-weapp-src-alisa')
const aliasConfig = {
'@': path.join(__dirname, 'src')
}

function wxmlTask() {
return src(paths.wxml)
.pipe(aliases(aliasConfig))
.pipe(dest('dist'))
}

图片

有点麻烦…

一开始做了一个插件,去解析wxml的image标签中的src:

/(<image.*?src=['"])([^{}]*?)(['"]>)/g

然后判断要不要转换base64,需要转换的就把src的url替换成base64:

1
2
3
4
5
6
function image2base64(picturePath) {
const buffer = fs.readFileSync(picturePath)
const mimetype = mime.getType(path.extname(picturePath))

return `data:${mimetype};base64,${buffer.toString('base64')}`
}

不需要的就复制图片。

但是!因为wxml语法的关系 有时候会这么写:

src="./images/icon_{{ type }}.png"

假如我在另外一个地方写了

src="./images/icon_warning.png"

这个图片又很小,那么这里的src会变成base64,前面动态url就会找不到图片…

解决办法:没有,第二版用了webpack解决

watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// watch任务分发
const del = require('del')
const logger = require('gulplog')

function watchHandler(type, file) {
const { sep } = path
const extname = path.extname(file)
const delPath = file.replace(`src${sep}`, `dist${sep}`)

const categories = {
add: {
scss: scssTask,
js: jsTask,
wxml: wxmlTask,
default: copyTask
},
change: {
scss: scssTask,
js: jsTask,
wxml: wxmlTask,
default: copyTask
},
remove: {
scss: () => del([delPath.replace(extname, '.wxss')]),
js: () => del([delPath]),
wxml: () => del([delPath]),
default: () => del([delPath])
}
}

const category = categories[type]
const task = category[extname.slice(1)] || category.default

task()
}

// watch
function watchTask(cb) {
const watcher = watch(['src'], { ignored: /[\/\\]\./ })
const { info } = logger

watcher
.on('change', file => {
watchHandler('change', file)
info('File changed: ' + file)
})
.on('add', file => {
watchHandler('add', file)
info('Add file: ' + file)
})
.on('unlink', file => {
watchHandler('remove', file)
info('Remove file: ' + file)
})

cb()
}

注册task

1
2
3
task('default', series(cleanTask, parallel(scssTask, jsTask, wxmlTask, copyTask), watchTask))

task('build', series(cleanTask, parallel(scssTask, jsTask, wxmlTask, copyTask)))

使用

开发: npm run dev

生产打包: npm run build

结论

  • 图片转base64问题没有好的解决办法
  • async/await无法使用
  • 让小程序开发工具做了压缩、转码、sourcemap的工作,不可控

其他功能大致上都能实现

为了体验能更好一点,第二版开始探索webpack..