首页
前端项目升级到 gulp4 + babel7

背景

前端项目从之前的jQuery + ES5到现在的ES6一直没有做到一个很平滑地过渡,导致基础设施分为了两块。一块是使用webpack来管理的ES6代码,另一块是使用gulp管理的ES5老代码。对于ES6这块,我们经历了从webpack3webpack4的大版本变化,构建速度也是得到了很大的提升。但是gulp管理的ES5代码一直是我们的一个痛点。所以打算把整个前端的基础设施梳理一下,都更新到最新的稳定版。

image-20210317134549570

这次更新主要涉及到了gulp4的一个任务流语法的重写以及一些其他的调整,整个过程还是蛮顺利的,差不多一天的时间搞定。

升级gulp到v4.x

gulp4 新特性

gulp changelog文档可以看到 4.0 的主要变化是任务系统的一个改变,之前的 orchestrator 系统被替换为 bachbach 项目的简介是 Compose your async functions with elegance(优雅地组合异步函数),那我们先来看一下它是如何优雅地组合异步函数。

parallel 和 series

在 4.0 中,我们可以通过series()parallel()这两个强大的函数来组合任务。series()用于让任务按顺序执行,parallel()用于让任务并发执行。这两个函数都可以接收任意数目的任务函数或已经组合好的操作。

series()对于处理顺序的任务流确实优雅了很多。在 3.x 中,如果我们需要按顺序执行任务,需要手动地去维护一个队列。

var gulp = require('gulp');

// 返回一个 callback,因此系统可以知道它什么时候完成
gulp.task('one', function(cb) {
    // 做一些事 -- 异步的或者其他的
    cb(err); // 如果 err 不是 null 或 undefined,则会停止执行,且注意,这样代表执行失败了
});

// 定义一个所依赖的 task 必须在这个 task 执行之前完成
gulp.task('two', ['one'], function() {
    // 'one' 完成后
});

gulp.task('default', ['one', 'two']);

在 4.0 中,我们只要按顺序把任务当成参数传入series()函数即可。

const task1 = ()=>{
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            console.log("2s后执行任务1")
            resolve()
        }, 2000)
    })
}

const task2 = ()=>{
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            console.log("1s后执行任务2")
            resolve()
        }, 1000)
    })
}
exports.test = parallel(task1, task2);

对于这两个任务,如果我们并行地去执行,应该是先输出task2的信息,再输出task1的信息。我们改成series()后,可以看到先执行task1再执行task2。

exports.test = series(task1, task2);

image-20210317180709663

任务(task)

在 3.x 中,我们需要通过gulp.task函数来创建一个任务。在 4.0 中,我们只需要定义一个异步的函数即可,该函数是一个可以接收 callback 作为参数的函数,或者是一个返回 stream、promise、event emitter、child process 或 observable 类型值的函数。

比如我们使用src函数,它返回的是一个流,所以我们定义的fontCss函数就是一个任务。

const fontCss = ()=>{
    return src(['src/css/font-awesome.min.css', 'src/css/iconfont.css'])
        .pipe(concat('font.css'))
        .pipe(autoprefixer())
        .pipe(cleanCSS({compatibility: 'ie8'}))
        .pipe(rename({suffix: '.min'}))
        .pipe(dest('dist/css'))
}

或者我们接收一个 callback,还有前面写的返回一个 Promise,都是可以的。

const clean = (cb) => {
  // body omitted
  cb();
}

公开任务和私有任务

在 3.x 中,每个任务都是可以直接被执行的。4.0 中为了方便管理,有了公开任务和私有任务之分。私有任务可以被当成参数传入到series()parallel()函数里面。公开任务需要通过exports导出。

const { series } = require('gulp');

// `clean` 函数并未被导出(export),因此被认为是私有任务(private task)。
// 它仍然可以被用在 `series()` 组合中。
function clean(cb) {
  // body omitted
  cb();
}

// `build` 函数被导出(export)了,因此它是一个公开任务(public task),并且可以被 `gulp` 命令直接调用。
// 它也仍然可以被用在 `series()` 组合中。
function build(cb) {
  // body omitted
  cb();
}

exports.build = build;
exports.default = series(clean, build);

如何升级

gulp的升级相比webpack来说容易多了,可能也是因为项目的gulpfile.js配置比较简单吧。只需要卸载老的版本,再安装一下新的版本,把gulpfile.js语法替换一下就可以了。

//4.0 中直接导出模块函数
const {src, dest, parallel} = require('gulp');  

//任务函数的修改,不需要使用 task 函数
const vendorJs = ()=>{
    return src(['src/js/jquery.min.js', 'src/js/bootstrap.min.js', 'src/js/iconfont.js'])
        .pipe(uglify())
        .pipe(concat('vendor_core.js'))
        .pipe(rename({suffix: '.min'}))
        .pipe(dest('dist/js'))
}

//使用组合函数并发构建,发挥最大的性能
const build = parallel(cssCompress, jsCompress, jsonCompress)

//使用 exports 导出公开任务
exports.build = build

升级babel到7.x

babel7相对于babel6也是做了很大的改动,升级起来并不是很容易,虽然官网还专门写了一篇升级的指南:升级到 Babel 7

升级依赖

babel的依赖跟gulp比起来就复杂的多了。首先是它使用了@babelmonorepo管理方式,需要把之前的一些依赖都重新安装一遍。

"devDependencies": {
    "@babel/cli": "^7.10.5",
    "@babel/core": "^7.11.4",
    "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/plugin-syntax-jsx": "^7.10.4",
    "@babel/plugin-transform-modules-commonjs": "^7.10.4",
    "@babel/plugin-transform-runtime": "^7.11.0",
    "@babel/polyfill": "^7.10.4",
    "@babel/preset-env": "^7.11.0",
    "@babel/runtime": "^7.11.2",
    ...
},

配置文件

之前 babel 的配置是写在 webpack 的 module 配置项里的,使用 babel-loader

{
    test: /\.m?js$/,
    include: /src/,
    exclude:
        /(node_modules|bower_components)/,
    use:
        {
            loader: 'babel-loader',
            options:
                {
                    cacheDirectory: true,
                    presets: ['env'],
                    plugins:
                        ['transform-runtime', 'transform-remove-strict-mode',
                            'transform-vue-jsx',
                            'transform-object-rest-spread', ["component", {
                            "libraryName": "element-ui",
                            "styleLibraryName": "theme-chalk"
                        }]
                        ]
                }
        }
},

这次直接把它独立成一个.babelrc文件

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "loose": true
      }
    ],
    [
      "@vue/babel-preset-jsx"
    ]
  ],
  "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-transform-runtime",
    [
      "@babel/plugin-transform-modules-commonjs",
      {
        "strictMode": false
      }
    ],
    "@babel/plugin-proposal-object-rest-spread",
    "@babel/plugin-syntax-jsx"
  ]
}

webpack 里面就不做配置了

 {
     test: /\.m?js$/,
     include: /src/,
     exclude:
         /(node_modules|bower_components)/,
     use: {
         loader: 'babel-loader',
         options: {cacheDirectory: true}
     }
 },

babel-polyfill

因为 babel 改为了 monorepo 的方式,所以之前项目中使用的

import "babel-polyfill"

需要改为

import "@babel/polyfill";

babel-plugin-component

elementui 的 babel-plugin-component一直没有做 babel7 的适配,导致通过这种方式引入的组件会报错。有一个 issue 提到了这个问题,目前并没有什么好的解决方案,只有全局引入 elementui,废弃掉这种方式了。

其他升级项

使用 dart-sass 替代 node-sass

node-sass一直有一个问题,就是下载它的包经常失败。对于做持续部署来说,经常会在它上面花费很多时间。使用dart-sass替代node-sass后会导致一个问题,v-loader 的深度选择器/deep/会出错,>>>也无效,目前替换为v-deep

使用 yarn 来替换 npm

通过yarn来安装依赖比npm快多了,这是一个最直观的感受。它的命令跟npm也有一些区别。

//安装依赖
npm install
yarn
//安装包
npm install react --save
yarn add react
...
//运行任务
npm run build
yarn build