背景

公司有几个小程序项目,使用的原生方式开发。原生方式有它自己的好处,比如不用踩框架的坑了,但弊端也很明显,无法使用框架带来的效率的提升。基于此,就简单的使用gulp,配置了一套工作流,帮我们在开发过程中省去一些重复的操作。

为什么使用gulp而没有用webpack?在对这两个工具都实践了一番后,发现针对我们的问题,使用gulp更简单。。。

关于gulp本文不会做过多的介绍,侧重点在于功能的实现。

功能列表

  • 支持使用less书写css
  • css 自动补全
  • js 语法支持
  • css和js代码压缩
  • 图片自动上传到cdn,监听到图片变化后,自动上传cdn
  • 支持静态资源目录,例如di三方JS、需要放到本地的图片
  • 小程序原生npm功能,通过命令自动构建小程序适用的npm包
  • 单位默认转换,即px自动转为rpx,同时支持不同端屏幕的适配
  • 支持source map,能够定位构建后的代码是在哪里出错的
  • 通过命令创建page和component,自动生成四个文件
  • 支持环境变量,通过命令自动生成不同环境的代码
  • 引入miniprogram-ci,通过上传代码到微信后台

目录结构

先看下配置好后的目录结构,其中src目录内容就是微信小程序的代码,外部的都是一些配置文件。

image.png

命令定义

下面是在package.json中定义的命令,接下来会一个个实现这些功能。

"scripts": {
    "start": "rm -rf ./dist && cross-env NODE_ENV=development gulp",
    "preview": "gulp preview",
    "build-pro": "rm -rf ./dist && cross-env NODE_ENV=production gulp build",
    "build-dev": "rm -rf ./dist && cross-env NODE_ENV=development gulp build",
    "build-npm": "gulp npm",
    "deploy-dev": "npm run build-dev && gulp upload",
    "deploy-pro": "npm run build-pro && gulp upload",
    "cp": "create-wxapp-page --type=page --dir=src --indent=2 --style=less",
    "cc": "create-wxapp-page --type=component --dir=src --indent=2 --style=less"
  },

gulpfile

gulpfile文件是所有的配置代码,后面会对每个task做详细解释。

const path = require('path');
const fileExists = require('file-exists');
const gulp = require('gulp');
const less = require('gulp-less');
const uglify = require('gulp-uglify');
const cleanCSS = require('gulp-clean-css');
const rename = require('gulp-rename');
const del = require('del');
const qcloud = require('qcloud-upload');
const gulpif = require('gulp-if');
const gutil = require('gulp-util');
const replace = require('gulp-replace');
const px2rpx = require('gulp-px2rpx');
const ci = require('miniprogram-ci');
const sourcemaps = require('gulp-sourcemaps');

const alias = require('gulp-path-alias');
const pkg = require('./package.json')
const projectConfig = require('./project.config.json')

const buildPath = path.join(__dirname, 'dist/')

const uploadFolder = path.join(__dirname, './src/images')

const isPro = process.env.NODE_ENV === 'production'

const config = {
// 腾讯CDN油漆桶配置
  assetsCDN: 'https://xxx.cos.ap-guangzhou.myqcloud.com/',
  cos: {
    Bucket: 'xxx',
    Region: 'xxx',
    SecretId: 'xxx',
    SecretKey: 'xxx',
    prefix: `${pkg.name}/images`, // 上传到油漆桶的哪个文件夹
    src: uploadFolder, // 上传哪个文件夹到油漆桶
    overWrite: 1,
  },
  enablePx2Rpx: true,
  enableCleanCSS: false,
  enableAuto: true, // 自动补全css
  enableUglify: false,
  enableSourcemap: true,
};


const paths = {
  styles: {
    src: ['src/**/*.less'],
    dest: buildPath
  },
  images: {
    src: 'src/images/**/*.{png,jpg,jpeg,svg,gif}',
    dest: buildPath
  },
  scripts: {
    src: 'src/**/*.js',
    dest: buildPath
  },
  copy: {
    src: ['src/**', '!src/**/*.less', '!src/images/**', '!src/**/*.js', 'package.json'],
    dest: buildPath
  },
}


// 删除构建
function clean() {
  return del([buildPath])
}

function log() {
  const data = Array.prototype.slice.call(arguments)
  gutil.log.apply(false, data)
}

function upload() {
  return new Promise(function (resolve, reject) {
    // 普通函数,resolve()的时候,qcloud不一定执行结束
    qcloud(config.cos)
    resolve()
  })
}


// 任务处理函数
function styles() {
  return gulp
    .src(paths.styles.src, { base: 'src' })
    .pipe(alias({
      paths: {
        '@': path.resolve(__dirname, './src/'),
      }
    }))
    .pipe(less())
    .pipe(replace('%CDN_IMG%/', config.assetsCDN + config.cos.prefix + '/'))
    .pipe(gulpif(config.enableCleanCSS, cleanCSS()))
    .pipe(gulpif(config.enablePx2Rpx, px2rpx({
      screenWidth: 375, // 设计稿屏幕, 默认750
      wxappScreenWidth: 750, // 微信小程序屏幕, 默认750
      remPrecision: 6
    })))
    .pipe(replace('PX', 'px'))
    .pipe(rename(path => (path.extname = '.wxss')))
    .pipe(gulp.dest(paths.styles.dest))
}

function scripts() {
  return gulp
    .src(paths.scripts.src, { base: 'src' })
    .pipe(alias({
      paths: {
        '@': path.resolve(__dirname, './src/'), // src 目录
      }
    }))
    .pipe(gulpif(config.enableSourcemap, sourcemaps.init()))
    .pipe(gulpif(isPro, replace('%ENV%', 'production'), replace('%ENV%', 'development'))) // 环境变量静态替换
    .pipe(replace('%CDN_IMG%/', config.assetsCDN + config.cos.prefix + '/'))
    .pipe(replace('%VERSION%', pkg.version))
    .pipe(gulpif(config.enableUglify, uglify()))
    .pipe(gulpif(config.enableSourcemap, sourcemaps.write('.')))
    .pipe(gulp.dest(paths.scripts.dest))
}

// 不需要处理的文件直接复制过去
function copy() {
  return gulp
    .src(paths.copy.src)
    .pipe(replace('%CDN_IMG%/', config.assetsCDN + config.cos.prefix + '/'))
    .pipe(gulp.dest(paths.copy.dest))
}

function watchFiles() {
  const w1 = gulp.watch(paths.styles.src, styles).on('unlink', function (file) {
    log(gutil.colors.yellow(file) + ' is deleted')
    const filePath = file.replace(/src\\/, 'dist\\')
    del([filePath])
  });

  const w2 = gulp.watch(paths.scripts.src, scripts).on('unlink', function (file) {
    log(gutil.colors.yellow(file) + ' is deleted')
    const filePath = file.replace(/src\\/, 'dist\\')
    del([filePath])
  });


  const w3 = gulp.watch(paths.copy.src, copy).on('unlink', function (file) {
    log(gutil.colors.yellow(file) + ' is deleted')
    const filePath = file.replace(/src\\/, 'dist\\')
    del([filePath])
  });

  const w4 = gulp.watch(paths.images.src, upload).on('unlink', function (file) {
    log(gutil.colors.yellow(file) + ' is deleted')
    const filePath = file.replace(/src\\/, 'tmp\\')
    del([filePath])
  });
  return Promise.all([w1, w2, w3, w4])
}

/**
 * 小程序ci相关函数
 */
let project = {}

const keyFile = fileExists.sync(`./private.${projectConfig.appid}.key`)
if (keyFile) {
  project = new ci.Project({
    appid: projectConfig.appid,
    type: 'miniProgram',
    projectPath: './dist',
    privateKeyPath: `./private.${projectConfig.appid}.key`,
    ignores: [],
  })
}
async function npmBuild() {
  await ci.packNpmManually({
    packageJsonPath: './package.json',
    miniprogramNpmDistDir: './src/',
  })
}

async function mpUpload() {

  const uploadResult = await ci.upload({
    project,
    version: pkg.version,
    desc: pkg.description,
    setting: {
      es6: true,
      minify: true,
      autoPrefixWXSS: true,
    },
    onProgressUpdate: console.log,
  })
  console.log('[uploadResult:]', uploadResult)
}

async function preview() {
  const previewResult = await ci.preview({
    project,
    desc: pkg.description, // 此备注将显示在“小程序助手”开发版列表中
    qrcodeFormat: 'image',
    qrcodeOutputDest: './preview.jpg',
    onProgressUpdate: console.log,
  })
  console.log('[previewResult:]', previewResult)
}
exports.watch = watchFiles
exports.preview = preview
// ci 自动构建npm
exports.npm = npmBuild
exports.upload = mpUpload

exports.default = gulp.series(styles, scripts, copy, upload, watchFiles)

exports.build = gulp.series(clean, styles, scripts, copy, upload)

自动创建组件和页面

自动创建组件和页面使用了一个npm包,叫做create-wxapp-page,有了它就能在命令行直接创建一组文件了。

npm run cc # 然后输入组件名字 
npm run cp # 然后输入页面名字

自动构建npm

小程序是支持npm的,但是需要在开发者工具开启后,手动点击编译,才能生成对应的npm包。还好微信提供了miniprogram-ci包,可以省去手动的过程。

const ci = require('miniprogram-ci');

async function npmBuild() {
  await ci.packNpmManually({
    packageJsonPath: './package.json',
    miniprogramNpmDistDir: './src/',
  })
}

exports.npm = npmBuild

执行npm run build-npm,我们安装的包就会构建到src的miniprogram_npm目录下。

环境变量支持

环境变量可以帮我们区分不同环境的要执行的代码,比如API地址。实现方式是静态替换。

pipe(gulpif(isPro, replace('%ENV%', 'production'), replace('%ENV%', 'development'))
const isPro = '%ENV%' === 'production'

if (isPro) {
  // 生产环境
  
} else {
  // 测试,体验环境
 
}

图片上传到CDN

当监听到images文件夹中有图片变更,会执行上传操作。

function upload() {
  return new Promise(function (resolve, reject) {
    // 普通函数,resolve()的时候,qcloud不一定执行结束。qcloud没有提供上传成功的callback
    qcloud(config.cos)
    resolve()
  })
}

使用CND图片

<image src="%CDN_IMG%/index/empty" mode="aspectFit" class="empty__img" />

sourcemap

sourcemap也很简单,同样是一个包,gulp-sourcemap

.pipe(gulpif(config.enableSourcemap, sourcemaps.init())

静态目录

对于一些特殊的静态资源,比如tabbar的icon图片,可以放到src/static目录下,这个目录的文件会原样copy到dist目录。

js语法支持

js新语法建议使用小程序工具自带的“es6转es5”和“增强编译”,实践发现使用glup-babel会导致js编译太慢。工具自带的已经够用了。

使用CI上传代码

miniprogram-ci提供了上传代码到小程序管理后台。第一步是先下载密钥,然后把文件命命名为private.{appId}.key,最后放置到项目根目录。处于安全考虑,更好的做法到是把ci功能放到服务器端,这里就先放到前端了。

image.png

执行npm run deploy-dev就可以把代码上传到体验版了,最后体验版选为ci机器人。

image.png

project.config.json变更

{
   "description": "项目配置文件",
  "miniprogramRoot": "dist/",
  "packOptions": {
    "ignore": [
      {
        "type": "folder",
        "value": "images"
      }
    ]
  }
  }

dist这个目录是构建出来的小程序代码,因此要指定 miniprogramRoot为“dist/”

由于图片上传到cdn了,src/images文件夹就没有必要打包进小程序了,这样也减少了体积。

### 结语

其实glupfile文件的代码比较简单,解释的太多感觉有点多余了。以上配置在我司的已上线小程序上平稳运行,改造成本也很低。如果有小伙伴想在自家小程序上使用,遇到任何问题欢迎留言。

以上,感谢阅读!


wmui
1.9k 声望177 粉丝

一人一世界