阅读 1936

带你揭开自动化构建的神秘面纱

摸鱼酱的文章声明:内容保证原创,纯技术干货分享交流,不打广告不吹牛逼。

前言:对于我们这些日常基于脚手架项目开发,使用yarn/npm run start、yarn/npm run build等命令完成自动化构建的开发者来说,重要的自动化构建仿佛变成了前端的一个黑盒知识。但是,掌握前端工程的自动化构建,是学习前端工程化以及进阶高级前端所必不可缺少的部分。

通过这篇文章,我尝试把我自己对自动化构建知识体系的系统化认识托盘而出,希望能够对阁下有所帮助。同样,我更喜欢的是您能批判我的一些观点或者指出我的一些问题,因为忠言逆耳利于行。

注意:文章的侧重点是对自动化构建知识的系统化探讨,不会是对某一个具体工具使用上的面面俱到,毕竟那是官方文档该做的事情。

对于自动化构建知识体系的系统化认识,从个人认识出发,我用脑图做了以下整理:

接下来的行文,我都会围绕这副脑图展开,如果您有兴趣继续往下看下去,我希望您能在这幅图上停留多一些时间。

好地,按照上述脑图中的逻辑,接下来我会分成以下几个部分来展开探讨本文。

  • 理解前端工程的自动化构建
  • 实现前端工程的自动化构建:npm script方式
  • 实现前端工程的自动化构建:gulp方式
  • 其它方式实现前端工程的自动化构建

好的,理清楚行文思路之后,进入第一点,理解前端工程的自动化构建。

一:理解前端工程的自动化构建

1.先理解一下为什么会出现这玩意

简单来说,随着前端需求和项目的日益复杂,出于提高开发效率、用户体验以及其它工程上的需要,我们通常会借助很多更高阶的语法(如es6、ts、less等)或者服务(如web server)等来帮助我们更快更好的开发、调试、增强一个前端工程。但是,这会导致一个问题,就是我们写的代码会离浏览器或node可解析运行的代码越来越远。

为了解决这个问题,前端工程构建的概念就逐渐丰富完整了起来。也就是说,前端工程构建就是指前端项目从源代码到一个能按需运行(开发环境、生产环境)的前端工程所需要做的所有事情。由于前端工程的构建过程中会包含很多任务并且工程需要频繁构建,所以按照任何简单机械的重复劳动都应该让机器去完成的思想,我们应该自动化去完成工程的构建,提高构建效率

2.从这玩意的具体实践角度来再理解一次

前面提到了前端工程自动化构建的本质,但是这个理解离我们具体实践它还是有点远,下面再说说我对自动化构建实践上的理解吧。

我认为,前端工程构建的具体实践形式就是一个任务流,完成了这个任务流中的所有任务即完成了前端工程构建。而自动化构建,也就是不用手动的执行这个任务流中的一个个任务。

好的,经过上面两点讲解,我觉得我已经把我对它的所有理解都已经倾囊相授了。

为了引导接下来对自动化构建具体实现的讲解,下面我们再从自动化构建就是完成一个任务流这个实践角度理解来展开细致探讨,也就是以下这两点,即:

  • 理解构建任务流中的任务
  • 理解构建任务流

3.理解构建任务流中的任务

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

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

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

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

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

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

任务名任务职责
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兼容兼容不同浏览器
......

除了上述表格中列举的任务之外,在不同的项目不同的场景中还会有不同的构建任务,这里就不再一一赘述了。上面说到构建其实就是完成一个任务流,在理解和认识了常见任务之后,接下来我们理解一下前端工程当中的任务流。

4.理解构建任务流

任务流的理解可不只是多个任务这么简单,任务流是要为目的服务的,就好比生产一个产品,必须完整跑完生产流水线一样。所以我们这里得从构建目的的角度来理解任务流。

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

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

我们先来理解一下开发环境的构建任务流。

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

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

功能项包含任务
语法检查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,就可以方便地调用任务脚本或者cli模块。

cli模块提供了脚本命令,可以使用npm/npx/yarn运行该模块所提供的脚本。

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

对于任务流的调用,我们则可以借助一些可以帮助任务组合(并行和串行)的库,而后在npm script中配置一条组合任务,调用它以启动构建任务流。

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

1.单任务注册调用示例

下面是sass转换和ES6转换的两个单任务示例:

  • 注册任务:package.json中配置script
  "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"
  }
复制代码
  • 调用任务:
# sass转换
yarn sass
# es6转换
yarn es6
复制代码
  • 调用原理追溯

最基本的脚本调用也就是用某个命令(如node)去执行一个文件,这里直接使用yarn就可以触发script中的任务难免会让人有点疑惑。下面是我为您整理的任务调用追溯,理解它有时候能帮助你定位解决一些问题。

  • sass转换任务追溯:yarn sass(触发任务) -> sass scss/main.scss css/style.css -> node_modules/.bin/sass.cmd -> node_modules/sass/sass.js -> ...code

  • es6转换任务追溯: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

2.简单任务流构建示例

这里需要再重申一点,任务流不等同于任务组合,它与构建目的有关。这里我们假设我们前端工程的构建目的就只是sass转换和es6转换,那么我们以如下形式实现自动化构建。

  • 注册任务:package.json中配置script

注意:对于任务流中的任务组合我们这里通过npm-run-all这个库来帮助我们实现,这个库提供了两个cmd文件,nun-p.cmd实现任务的并行,nun-s.cmd实现任务的串行。

  "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"
  }
复制代码
  • 调用任务:
yarn build
复制代码

3.具体工程构建示例

下图即是我们想要构建的简单前端项目:

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

简单项目的自动化构建就是npm script实现自动化构建的使用场景。

与日常开发一样,我们这里也把这个工程构建分为两个部分,即开发环境构建和生产环境构建。下面先讲讲开发环境构建的实现:

(一):开发环境构建工程

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

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

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

好的经过上面任务分析之后,我们可能会把package.json的scripts以及devDependencies写成如下样子(核心关注scripts中的start命令):

  "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"
  }
复制代码

(二):生产环境构建工程

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

  • 删除上一次构建结果任务
  • 编译任务
  • html模板信息注入任务
  • 文件压缩任务
  • ...

好的经过上面任务分析之后,我们可能会把package.json的scripts以及devDependencies写成如下样子(核心关注scripts中的build命令):

  "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"
  }
复制代码

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

4.npm script构建总结

在进入Gulp实现前端工程的自动化构建之前,我觉得有必要再重申一点:在项目以及构建需求不复杂时,npm scripts就可以满足我们的构建需求了,无需借助其它工具。

好的,下面进入gulp方式实现前端工程的自动化构建。

文章都写到这份上了,不值得你点个赞和关注鼓励一下吗?

三:实现前端工程的自动化构建:gulp方式

有些看官可能不太了解gulp,这里我先简单介绍一下它吧。

Gulp是一个基于流的自动化构建工具,相比较于Grunt,它的构建速度更快,任务编写也更加简单灵活。

安装好gulp之后,使用gulp的流程也就基本是如下这样:

  • 首先需要在项目根目录下创建一个Gulp入口文件gulpfile.js
  • 然后在这个入口文件中通过暴露函数的方式注册任务
  • 最后以根目录作为工作目录执行gulp命令即可执行注册任务。

有了以上了解之后,下面我们就探讨如何借助gulp这个工具来实现自动化构建吧。

1.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'))
}
复制代码

(4): 自定义gulp任务

如果需要定制任务,或者对于我们的需求没有较好的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) // 写入
}
复制代码

2.gulp完成构建任务流

如下gulpfile文件,分为开发环境构建(develop任务)和生产环境构建(build任务)。相对于理论认识,工具的使用只是不同实现方式,这里就不多赘述了,具体的逻辑可以看下面代码中的注释,我为您写的很清楚了哦。

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
}

复制代码

四:其它方式实现前端工程的自动化构建

在前端工程的自动化构建发展历程中,出现了很多的自动化工具。如grunt、gulp、webpack,包括现在正处在风口浪尖的vite等等。webpack的相关内容会在另外一篇文章中探讨,对于其它的构建工具以及过时的工具我觉得没有现在学习的必要,具体生产需要使用时学习即可。

总的来说,工具很多,但是最重要的其实是对于自动化构建本身的理论认识,而对于自动化构建的实现及其工具而言,我觉得,包括比较需要掌握的gulp和webpack,完全理解npm script方式是更重要的。

行文结束,祝各位看官牛年大吉,别忘了给个点赞和关注哦。么么哒(✿◡‿◡)

文章分类
前端
文章标签