前端进阶:前端工程化之自动化构建

790 阅读14分钟

前言:博主2020.3正式进军前端,目标高级前端工程师,经验尚浅,文章内容如若有误,欢迎指正。

自动化构建 并不是一个句子,有必要在行文前先说明一下它在本文中的宾语是前端工程

一:认识自动化构建

随着前端需求和项目的日益复杂,出于提高开发效率、用户体验以及其它工程上的需要,我们通常会借助很多更高阶的语法或者工具来帮助我们更快更好的开发、部署一个前端工程。

那么如何理解构建以及自动化构建呢?个人观点,简单务实点说,构建就是指项目从源代码到一个能按需运行的工程(开发环境、生产环境)所需要做的所有事情。通常来说,项目多次构建的每次构建过程都包含很多任务并且基本一致,按照任何简单机械的重复劳动都应该让机器去完成的思想,我们应该自动化去完成工程的构建,提高构建效率

那么如何实现自动化构建呢?个人理解,前端工程构建其实就是一个任务流,完成了这个任务流中的所有任务即完成了构建。说到任务以及任务流,我们有必要先好好认识一下它们。

1.理解前端构建过程中的任务

对比于JavaScript的函数,个人对任务是这么分类的:

  • 单任务:同步任务和异步任务
  • 多任务:并行任务、串行任务

同步任务和异步任务无须解释,这里说说并行任务和串行任务。任务并行可以用于缩短多个任务的执行时间。因为node是单线程执行的,我认为它并不能缩短多个同步任务并行的执行时间,但是构建过程中的任务通常都会涉及到IO流,所以大部分任务都是异步任务,IO任务并行可以避免IO阻塞线程执行,进而缩短多个任务的执行时间。

而任务串行可以保证任务之间的有序执行,比如在开发环境下,我们肯定要先执行编译任务,然后才能执行启动开发服务器的任务。

理解了构建过程中的任务之后,下面再列举一些在日常开发当中,我们常见到的构建任务。

2.前端构建过程中的常见任务

任务名任务职责
Eslint检查统一代码风格
babel转换ES6 -> ES5
typescript转换Ts -> Js
sass转换sass -> css
less转换less -> css
html、css、js压缩.html -> .min.html、.css->.min.css、.js->.min.js
web server开发服务器
js、css兼容兼容不同浏览器
......

除了上述表格中列举的任务之外,在不同的项目中还会有不同的构建任务,这里不再一一赘述。上面说到自动化构建其实就是一个任务流,再理解和认识了常见任务之后,我们再来理一理什么是前端的构建任务流。

3.理解前端构建任务流

构建是为工程服务的,而工程又是为用户服务的。对应于开发环境和生产环境,前端工程可以分为开发环境工程和生产环境工程,其中开发环境工程为开发者服务,生产环境工程为用户服务。

满足工程使用者的需求是我们构建工程的终极目的,所以有必要投其所好,根据工程的使用者不同,完成他所需要的的一连串任务,也就是任务流。这时可以根据构建后工程的目标使用者来划分,把任务流分为开发环境构建任务流和生产环境构建任务流两种。

4.理解开发环境构建任务流

开发环境构建任务流构建后的工程是为开发者服务的。开发者需要开发调试代码,所以开发环境任务流构建的工程需要实现以下功能:

功能项包含任务
语法检查Eslint
语法转换ES6 -> ES5、Sass -> less、Ts->Js等等
模拟生产环境web开发服务器:devServer
易于调试sourceMap
......

开发者需要不断修改代码查看效果,所以除了满足功能之外,还需要加快构建速度并且自动刷新,以保证良好的使用体验。

优化方式实现方案
加快构建速度devServer热模块替换
自动刷新devServer 监听源代码
......

关于web开发服务器devServer

使用web开发服务器可以模拟像使用nginx、tomcat等服务器软件一样的线上环境,它在功能以及配置上都与nginx以及tomcat类似, 最简单的配置就是指明资源路径baseUrl以及服务启动ip和端口port即可。在开发环境启动本地服务时,配置代理可以在符合同源策略的情况下解决跨域问题

开发服务器除了可以模拟线上环境之外,更加强大的一点是它可以监听源代码,实现热部署和自动刷新功能

5.理解生产环境构建任务流

生产环境构建任务流构建后的工程是为用户服务的。与开发环境相比,它也需要语法检查以及编译功能,但不需要考虑修改以及调试代码的问题,它关注的是浏览器兼容以及运行速度等问题。

功能项包含任务
语法检查Eslint
语法转换ES6 -> ES5、Sass -> less、Ts->Js等等
语法兼容不同浏览器的js、css语法兼容
下载速度资源压缩与合并
......

生产环境的优化除了资源的下载速度之外,还可以从很多方面入手,下面是其中的一些方面以及实现方案。

优化方面实现方式
下载优化treeshaking、代码分割、懒加载
运行优化代码上优化性能
离线访问pwa技术
......

终于把任务以及任务流浅显粗陋的讲完了,接下来我们先是使用npm scripts来实现简单项目的自动化构建,而后学习一下Gulp工具如何实现复杂项目的自动化构建

二:npm script实现自动化构建任务流

任务流由任务组成,任务由脚本实现。在定义好任务脚本或者安装好任务cli模块之后,我们只需在package.json的scripts选项中配置一条script,就可以方便地调用任务脚本或者任务模块。对于任务流的npm script定义,我们可以借助一些可以帮助任务组合的库,这样就可以实现多个任务之间的并行和串行。

这里不得不提一下node_modules/.bin文件夹,我们在项目中安装的cli模块都会有一个cmd文件出现在这里。当我们在项目中需要调用这些cli模块时,只需yarn/npx cli模块名的方式就可以很方便的调用这些cli模块。

好的,通过上面的分析之后,我们接下来展开讲述一下npm scripts如何实现任务以及任务流的构建。

1.npm script实现任务、任务流构建

单任务构建

对于单任务的构建,只需配置一条简单的script即可,如以下sass和ES6转换的script示例(package.json):

  "scripts": {
    "sass": "sass scss/main.scss css/style.css",
    "es6": "babel es6 --out-dir es5",
  },
  "devDependencies": {
    "@babel/cli": "^7.12.8",
    "@babel/core": "^7.12.9",
    "sass": "^1.29.0"
  }

配置以上scripts之后,我们就可以使用以下命令执行任务:

# sass转换
yarn sass
# es6转换
yarn es6

这里提一提在执行上述命令之后到最后调用sass和es6编译工具的调用过程:

  • yarn sass -> sass scss/main.scss css/style.css -> node_modules/.bin/sass.cmd -> node_modules/sass/sass.js -> ...code

  • yarn es6 -> babel es6 --out-dir es5 -> node_modules/.bin/babel.cmd -> node_modules/@babel\cli\bin\babel.js -> node_modules/@babel\cli\lib\index.js -> ...code

任务流构建

对于任务流的构建,除了准备基本任务之外,我们还需要考虑这些任务之间是否有序,如果有序我们借助任务串行实现,如果无序我们通过任务并行加快构建速度。通常我们会借助npm-run-all 这个库来实现任务的并行和串行,如以下通过任务并行实现sass转换以及ES6转换的简单示例(package.json):

  "scripts": {
    "sass": "sass scss/main.scss css/style.css",
    "es6": "babel es6 --out-dir es5",
    "build": "run-p sass es6"
  },
  "devDependencies": {
    "npm-run-all": "^4.1.5",
    "@babel/cli": "^7.12.8",
    "@babel/core": "^7.12.9",
    "sass": "^1.29.0"
  }

配置以上scripts之后,我们就可以使用以下命令执行任务:

yarn build

执行yarn build之后,就可以借助npm-run-all库的nun-p.cmd实现sass和es6任务的并行。对于任务的串行,则通过npm-run-all库的nun-s.cmd实现。

好的,在通过以上两个示例理解了npm script实现构建任务以及任务流之后,我们接下来通过npm script实现一个简单前端项目的开发环境自动化构建和生产环境的自动化构建。

2.npm scripts构建工程示例

1):需构建的项目源代码

这个项目很简单,它只包含一个html文件,一个使用了ES6语法js文件以及一个使用了sass语法的样式文件,接下来我们就用npm script来实现这个简单项目的自动化构建(也即开发环境构建任务流和生产环境构建任务流)。事实上,简单项目的自动化构建就是npm script实现自动化构建的使用场景。

2):实现开发环境自动化构建任务流

通过上面我们对开发环境构建任务流的认识,我们先理一理在这个项目中,开发环境任务流至少应该包含哪些任务:

  • 需要web开发服务器模拟生产环境以及实现源码监听和自动刷新。
  • 对于sass文件,需要实时sass转换,并且监听源码变化重启开发服务器、刷新浏览器。
  • 对于ES6文件,需要实时babel转换,并且监听源码变化重启开发服务器、刷新浏览器。
  • 对于html文件,需要监听源码变化重启开发服务器、刷新浏览器。
  • ...

对于sass和ES6修改源代码后的实时转换,我们可以通过加上一个watch参数实现。而对于所有这些需要监听变化的文件,我们则统一放入temp文件夹下(角色好比如nginx和Tomcat的应用存放目录),而后让web开发服务器监听这个temp文件夹下所有文件的变化,一旦变化即重启并刷新浏览器。

好的经过上面任务分析之后,我们可能会把package.json的scripts以及devDependencies写成如下样子:

  "scripts": {
    "sassDev": "sass scss/main.scss temp/css/style.css --watch",
    "babelDev": "babel es6/script.js --out-dir temp/es5/script.js --watch",
    "copyHtmlDev": "copyfiles index.html temp",
    "serve": "browser-sync temp --files \"temp\"",
    "start": "run-p sassDev babelDev copyHtmlDev serve"
  },
  "devDependencies": {
    "@babel/cli": "^7.12.8",
    "@babel/core": "^7.12.9",
    "browser-sync": "^2.26.13",
    "copyfiles": "^2.4.1",
    "npm-run-all": "^4.1.5",
    "sass": "^1.29.0"
  }

3):实现生产环境自动化构建任务流

通过上面我们对生产环境构建任务流的认识,我们先理一理在这个项目中,生产环境任务流应该包含哪些任务:

  • 删除上一次构建结果任务
  • 编译任务
  • html模板信息注入任务
  • 文件压缩任务
  • ...
  "scripts": {
    "sass": "sass scss/main.scss dist/css/style.css",
    "babel": "babel es6 --out-dir dist/es5",
    "copyHtml": "copyfiles index.html dist",
    "build": "run-p sass babel copyHtml"
  },
  "devDependencies": {
    "@babel/cli": "^7.12.8",
    "@babel/core": "^7.12.9",
    "browser-sync": "^2.26.13",
    "copyfiles": "^2.4.1",
    "npm-run-all": "^4.1.5",
    "sass": "^1.29.0"
  }

上述代码实现不全,按道理说,在生产环境下,至少需要做代码的兼容以及压缩。这时我们就需要找到对应的工具库或者自己实现,另外对于压缩而言至少需要在编译之后完成,所以需要注意多个任务间的关系。思路很简单,博主偷个懒当前就不花时间去实践了,需要时再实现就行。

3.npm scripts构建总结

在介绍Gulp之前,我们有必要再重申一点。在项目以及构建需求不复杂时,npm scripts就可以满足我们的构建需求了,无需借助其它工具。

三:成熟工具Gulp实现自动化构建任务流

1.简单认识Gulp

Gulp是一个基于流的自动化构建工具,相比较于Grunt,它的构建速度更快,任务编写也更加简单灵活(Grunt博主没用过也不感兴趣)。要使用Gulp,首先需要在项目根目录下创建一个Gulp入口文件gulpfile.js,然后在这个入口文件中通过暴露函数的方式注册任务。

对于一个工具,其它的不多比比,我们接下来看看它是怎么实现前端项目的自动化构建的。

2.Gulp实现任务、任务流构建

1):实现同步任务和异步任务

对于新版本的Gulp来说,所有任务都是异步任务,所以任务需要告诉Gulp什么时候执行结束。以下是gulp异步任务实现的几种方式(关注它们是如何通知Gulp异步任务结束的)。

// 方式1:调用done方法主动通知任务结束
exports.foo = done => {
  console.log('foo task working~')
  done() // 标识任务执行完成
}
// 方式2:返回Promise,通过它的resolve/reject方法通知任务结束
const timeout = time => {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}
// 方式3:返回读取流对象,流完即自动通知任务结束
exports.stream = () => {
  const read = fs.createReadStream('yarn.lock')
  const write = fs.createWriteStream('a.txt')
  read.pipe(write)
  return read
}
// 更多方式

2):实现并行任务和串行任务

并行任务和串行任务可以通过gulp提供的series(串行), parallel(并行)实现。

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

const task1 = done => {
  setTimeout(() => {
    console.log('task1 working~');
    done();
  }, 1000)
}

const task2 = done => {
  setTimeout(() => {
    console.log('task2 working~');
    done();
  }, 1000)  
}

exports.bar = parallel(task1, task2); // 并行任务bar

exports.foo = series(task1, task2); // 串行任务foo

3):Gulp插件任务和自定义任务

插件任务

Gulp生态中有很多成熟的gulp任务插件,使用它们可以很好地提高效率,如以下示例:

const { src, dest } = require('gulp');
const cleanCSS = require('gulp-clean-css');
const rename = require('gulp-rename');

exports.default = () => {
  return src('src/*.css')
    .pipe(cleanCSS())
    .pipe(rename({ extname: '.min.css' }))
    .pipe(dest('dist'))
}
自定义任务

如果需要定制任务,或者对于我们的需求没有较好的gulp插件,那么我们就需要自定义任务,如下示例:

const fs = require('fs')
const { Transform } = require('stream')

exports.default = () => {
  const readStream = fs.createReadStream('normalize.css');
  const writeStream = fs.createWriteStream('normalize.min.css');
  // 文件转换流
  const transformStream = new Transform({
    // 核心转换过程
    transform: (chunk, encoding, callback) => {
      const input = chunk.toString();
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '');
      callback(null, output);
    }
  })

  return readStream
    .pipe(transformStream) // 转换
    .pipe(writeStream) // 写入
}

3.Gulp实现开发环境和生产环境自动化构建

gulp只是一个帮助我们实现自动化构建的工具,思想在上文中已经探讨了很多,而且对比于gulp,个人对后面的webpack更感兴趣,这个示例我就不多做叙述了。

好吧,都是借口,现在已经深夜了,用心地写了那么长,我坦白我熬不住了。对了,如果认可文章内容,点赞收藏鼓励一下吧,每周一篇,后面会有我的更多学习记录哦。

下例是经过个人整理过的,课程中老师使用gulp对开发环境和生产环境自动化构建的示例实现:

const {
  src,
  dest,
  parallel,
  series,
  watch
} = require('gulp')

const del = require('del')
const browserSync = require('browser-sync')

const loadPlugins = require('gulp-load-plugins')

const plugins = loadPlugins()
const bs = browserSync.create()

const data = {
  menus: [{
      name: 'Home',
      icon: 'aperture',
      link: 'index.html'
    },
    {
      name: 'Features',
      link: 'features.html'
    },
    {
      name: 'About',
      link: 'about.html'
    },
    {
      name: 'Contact',
      link: '#',
      children: [{
          name: 'Twitter',
          link: 'https://twitter.com/w_zce'
        },
        {
          name: 'About',
          link: 'https://weibo.com/zceme'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/zce'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}
// css编译  src => temp
const style = () => {
  return src('src/assets/styles/*.scss', {
      base: 'src'
    })
    .pipe(plugins.sass({
      outputStyle: 'expanded'
    }))
    .pipe(dest('temp'))
    .pipe(bs.reload({
      stream: true
    }))
}
// js编译   src => temp
const script = () => {
  return src('src/assets/scripts/*.js', {
      base: 'src'
    })
    .pipe(plugins.babel({
      presets: ['@babel/preset-env']
    }))
    .pipe(dest('temp'))
    .pipe(bs.reload({
      stream: true
    }))
}

// html模板解析     src => temp
const page = () => {
  return src('src/*.html', {
      base: 'src'
    })
    .pipe(plugins.swig({
      data,
      defaults: {
        cache: false
      }
    })) // 防止模板缓存导致页面不能及时更新
    .pipe(dest('temp'))
    .pipe(bs.reload({
      stream: true
    }))
}

// 串行编译、模板解析
const compile = parallel(style, script, page)

// 开发环境开发服务器
const serve = () => {
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)
  // watch('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', extra)
  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload)

  bs.init({
    notify: false,
    port: 2080,
    // open: false,
    // files: 'dist/**',
    server: {
      baseDir: ['temp', 'src', 'public'],
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

// 开发环境构建流:编译 + 启动开发服务器    src => temp
const develop = series(compile, serve)



// 生产环境下清空文件夹
const clean = () => {
  return del(['dist', 'temp'])
}
// 生产环境js、css、html压缩后构建  temp => dist
const useref = () => {
  return src('temp/*.html', {
      base: 'temp'
    })
    .pipe(plugins.useref({
      searchPath: ['temp', '.']
    }))
    // html js css
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(dest('dist'))
}
// 生产环境图片压缩后构建   src => dist
const image = () => {
  return src('src/assets/images/**', {
      base: 'src'
    })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
// 生产环境字体压缩后构建   src => dist
const font = () => {
  return src('src/assets/fonts/**', {
      base: 'src'
    })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
// 生产环境静态资源构建
const extra = () => {
  return src('public/**', {
      base: 'public'
    })
    .pipe(dest('dist'))
}

// 上线之前执行的任务   src => (temp =>) => dist 
const build = series(
  clean,
  parallel(
    series(compile, useref),
    image,
    font,
    extra
  )
)

module.exports = {
  clean,
  build,
  develop
}


本文结束,观众老爷您慢走,欢迎下次光临。