美文网首页gulp自动化构建前端杂货铺程序员
强化:构建易用易扩展的工作流

强化:构建易用易扩展的工作流

作者: Jack_Lo | 来源:发表于2016-08-22 17:49 被阅读0次

    一、回顾与思考

    在上一节的【进阶:构建具备版本管理能力的项目】中我们讲解了如何用webpack去搭建一个工作流。

    我们说webpack有很多的loader用来编译打包静态资源,而gulp也有很多的以gulp-*格式命名的工作模块用来处理各种资源文件,那webpack和gulp是什么样的联系?有什么样的区别?

    这也是很多初学者没有搞明白的,webpack和gulp是不是同类型的工具?

    webpack专注于处理各种资源,而gulp专注于任务管理,两者的职能是不同的。打个比方:

    gulp好比是大boss,平时要管理包括运营、产品、设计、开发、财务、后勤等等各个部门的工作,但是其实大boss只想知道产品做成什么样、财务剩下多少钱,其他的部门他能管理,但是太琐碎了。现在来了一个叫做webpack的小伙子,告诉大boss说,我能帮你管理运营、设计、开发、后勤这几个部门的工作,你给我个副总当当,然后你就只需要管理产品、财务,还有我这三个对象就好了。两人一拍即合,从此过上了幸福的生活。

    没错,我不骗你,就是这么狗血。

    虽然单靠webpack也可以搭建一套像模像样的工作流出来,gulp没有webpack一样也活得很好。但是我们拨开表象看本质,gulp的任务管理能力很强,webpack处理资源很方便,为何不结合起来使用呢?

    嗯,就这么干!这一节我们就来尝试使用gulp+webpack构建一个又好用又容易扩展功能模块的工作流。我们以gulp为大框架,整合webpack的方式来开展。

    二、编译打包

    我们先把第一节的gulpfile.js文件搬出来,老规矩,我们先来实现打包的工作,所以我们先把dev相关的内容剃掉:

    var gulp = require('gulp')
    var sass = require('gulp-sass')
    var swig = require('gulp-swig')
    
    gulp.task('sass', function () {
      return gulp.src('src/sass/*.scss')
      .pipe(sass({
        outputStyle: 'compressed'  // 此配置使文件编译并输出压缩过的文件
      }))
      .pipe(gulp.dest('dist/static'))
    })
    
    gulp.task('js', function () {
      return gulp.src('src/js/*.js')
      .pipe(gulp.dest('dist/static'))
    })
    
    gulp.task('tpl', function () {
      return gulp.src('src/tpl/*.swig')
      .pipe(swig({
        defaults: {
          cache: false  // 此配置强制编译文件不缓存
        }
      }))
      .pipe(gulp.dest('dist'))
    })
    
    gulp.task('build', ['sass', 'js', 'tpl'])
    

    我们说好相关资源的处理工作是要交给webpack的,所以我们还需要把sassjstpl三个gulp任务给去掉,取而代之的是webpack的loader:

    var gulp = require('gulp')
    var webpack = require('webpack')
    
    gulp.task('webpack', function () {
      // do something...
    })
    
    gulp.task('build', ['webpack'])
    

    嗯,我们已经基本能够预料到接下来的步骤了,填充一下这个webpack任务,而我们在上一节已经有过直接使用webpack的api来工作的尝试,我们直接使用webpack_base里的webpack.config.js文件:

    var gulp = require('gulp')
    var webpack = require('webpack')
    var config = require('./webpack.config.js')
    
    gulp.task('webpack', function () {
      return webpack(config)
    })
    
    gulp.task('build', ['webpack'])
    

    好像好简单的样子,一切都好顺利肿么办,心情好到飞起,于是迫不及待地在命令行输入:

    gulp webpack
    

    愉快地回车!

    好,居然没有报错,完美地兼容了!

    回头看一眼根目录!啊。。。。。。说好的dist目录呢?为啥什么都没有生成?!!!

    由于webpack处理资源的时候是一系列的异步操作,而gulp并不知道你什么时候处理完了资源,所以对于gulp来说,你webpack的任务从开始到结束我默认当你是同步的,这个任务开始之后,不等你处理完我就已经结束掉了这个工作。所以webpack需要一个操作来告诉gulp,我webpack什么时候处理完了这些资源。

    我们先来看最终的代码:

    var gulp = require('gulp')
    var webpack = require('webpack')
    var config = require('./webpack.config.js')
    
    gulp.task('webpack', function (cb) {
      webpack(config, function () {
        cb()
      })
    })
    
    gulp.task('build', ['webpack'])
    

    这里的cb(callback简写)就是一个回调操作,我们通过在webpack完成编译之后的回调里,调用gulp的回调函数,来达到通知gulp任务完成的目的。这里可能一时不好理解,大家花点心思琢磨一下。

    之后我们在命令行里执行编译操作,就能看到根目录下生成的dist文件夹了,里面也存放了被打包了的文件。

    这里有个问题,我们在命令行里只能看到:

    [23:35:49] Using gulpfile ~/gulp-webpack_base/gulpfile.js
    [23:35:49] Starting 'webpack'...
    [23:35:49] Finished 'webpack' after 20 ms
    

    然后就没有其他信息了。我们期待什么呢?我们期待能够看到webpack的编译信息,上面我提到过,webpack没有任何的报错信息,事实上就算是真的有错误,也完全不会有任何提示信息出现,在gulp中如果需要输出模块自己的信息,我们需要借助于 gulp-util ,这个工具我们在创建ftp任务的时候已经见过面了,具体修改如下:

    var gulp = require('gulp')
    var webpack = require('webpack')
    var config = require('./webpack.config.js')
    var gutil = require('gulp-util')
    
    gulp.task('webpack', function (cb) {
      webpack(config, function (err, stats) {
        if (err) {
          throw new gutil.PluginError('webpack', err)
        }
    
        gutil.log('[webpack]', stats.toString({
          colors: true,
          chunks: false
        }))
    
        cb()
      })
    })
    
    gulp.task('build', ['webpack'])
    

    这样一来,打包的工作就完成了。

    开发环境

    不知道大家有没有听过express

    Express 是一个基于 Node.js 平台的极简、灵活的 web 应用开发框架,它提供一系列强大的特性,帮助你创建各种 Web 和移动设备应用。 —— express中文网

    简单点说,express就是一个基于nodejs的web框架,我们可以用它来很方便地构建一个web项目。而我们这一次使用它的目的是用它来构建一个本地开发服务器。事实上我们上次演示用到的 webpack-dev-server 底层就是用express+webpack-dev-middleware实现的,并且我们还要结合webpack的webpack-dev-middlewarewebpack-hot-middleware两个中间件,来了却之前我们没有完成的心愿——热更新。

    如果没了解过express,可以在看完本节之后,再去官网补充一些知识点,本节只是用到了一些简单用法。

    二话不说,我们先在gulpfile.js中把需要的几个依赖包引入,并且为了方便,我们也直接使用上一节中webpack的开发配置文件webpack.dev.config.js

    ...
    var webpackHotMiddleware =  require('webpack-hot-middleware')
    var webpackDevMiddleware =  require('webpack-dev-middleware')
    var devConfig = require('./webpack.dev.config.js')
    var express = require('express')
    var app = express()
    ...
    

    这里的app便是我们要使用的本地服务器,相对应上一节中的webpackDevServer,然后我们来书写一下server任务:

    gulp.task('server', function () {
      app.listen(8080, function (err) {
        if (err) {
          console.log(err)
          return
        }
        console.log('listening at http://localhost:8080')
      })
    })
    

    其实这时候我们已经基本完成了一个服务器搭建,我们在命令行里输入:

    gulp server
    

    然后回车,然后在浏览器里输入http://localhost:8080,我们可以看到其实这个服务已经跑起来了,而我们只能在页面上看到Cannot GET /,是因为我们还没有定义路由规则,接下来,我们的工作基本就集中在定于路由规则上。

    我们先来试试随便指定根路径,返回一个hello, world!试试:

    gulp.task('server', function () {
      app.use('/', function (req, res) {
        res.send('hello, world!')
      })
    
      app.listen(8080, function (err) {
        if (err) {
          console.log(err)
          return
        }
        console.log('listening at http://localhost:8080')
      })
    })
    

    现在我们重复上面的操作,命令行输入gulp server然后回车一下,刷新浏览器,是不是就看到了hello, world!

    聪明的人一下子就想到了express的另外一个用途:为项目写接口,返回一些假数据。这样做当然可以,大家可以自己去亲手尝试一遍。

    回到正题,我们希望代码更加清晰,功能更加专一,我们把定义路由的工作,从任务server迁移出来,放到另外一个任务server:init去做,这样可以使得任务的功能更加单薄,更容易开发调试:

    gulp.task('server:init', function () {
      app.use('/', function (req, res) {
        res.send('hello, world!')
      })
    })
    
    gulp.task('server', ['server:init'], function () {
      app.listen(8080, function (err) {
        if (err) {
          console.log(err)
          return
        }
        console.log('listening at http://localhost:8080')
      })
    })
    

    好了,我们来插播一段知识点,关于middleware(中间件):

    中间件是一种独立的系统软件或服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源。 —— 百度百科

    说的有点抽象,简单说来,middleware就是一个提供某种服务的组件,它遵循某种公共的协议或约定,可以整合到各种框架当中。

    webpack-dev-middleware和webpack-hot-middleware就是两个中间件,前者提供webpack编译打包的服务,后者提供热更新的服务,两者组合在一起,就是我们之前接触过的webpack-dev-server。

    我们单独拿出来,是因为我们需要去订制一套自己的『webpack-dev-server』。上一节我们讲到webpack-dev-server的热更新的时候,遇到一个问题,那就是html模板无法自动刷新的问题,所以我们只能放弃hot-replacement的功能,使用reload的形式,虽然效果也不会差到哪里去,但是既然我们用着webpack,那就没理由舍弃这个特性,我们的目标是:装逼装到底 送佛送到西。

    我们已经借助于express搭建了一个本地服务器,我们把测试的路由给去掉,接下来我们来试着整合webpack-dev-middleware:

    gulp.task('server:init', function () {
      var compiler = webpack(devConfig)
      var devMiddleware = webpackDevMiddleware(compiler, {
        stats: {
          colors: true,
          chunks: false
        }
      })
    
      app.use(devMiddleware)
    })
    

    是不是跟webDevServer的配置十分相似?

    我们命令行里跑一下,刷新浏览器,效果出来啦啦啦啦啦!完美。

    我们接着整合webpack-hot-middleware,文档是这么要求的:

    1. 增加以下plugin:new webpack.optimize.OccurenceOrderPlugin()new webpack.HotModuleReplacementPlugin()new webpack.NoErrorsPlugin()
    2. 每个入口增加webpack-hot-middleware/client?reload=true

    操作下来,得到:

    gulp.task('server:init', function () {
      for (var key in devConfig.entry) {
        var entry = devConfig.entry[key]
        entry.unshift('webpack-hot-middleware/client?reload=true')
      }
    
      devConfig.plugins.unshift(
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin()
      )
    
      var compiler = webpack(devConfig)
      var devMiddleware = webpackDevMiddleware(compiler, {
        hot: true,
        stats: {
          colors: true,
          chunks: false
        }
      })
    
      var hotMiddleware = webpackHotMiddleware(compiler)
    
      app.use(devMiddleware)
      app.use(hotMiddleware)
    })
    

    我们命令行跑一下,然后修改一下index.scss,保存,我们可以在浏览器里直接看到修改了!

    但是我们如果修改index.swig,保存后,还是看不到浏览器刷新,这跟我们上次使用webpackDevServer遇到的情况一样。

    解决这个问题的方式比较曲折,我们需要解决两个问题:

    1. 如何知道用户修改并保存了html;
    2. 如何手动通知浏览器刷新页面。

    问题一
    第一个问题比较好解决,那就是为html-webpack-plugin在修改文件并保存之后,注册一个回调,用来告诉我们文件被修改了:

    compiler.plugin('compilation', function(compilation) {
      compilation.plugin('html-webpack-plugin-after-emit', function(data, callback) {
        // 需要在这里通知浏览器刷新页面
        callback()
      })
    })
    

    详细用法可以html-webpack-plugin的参考官方文档,内容太多,这里不详细介绍。

    问题二
    第二个问题,我们首先需要了解webpack-hot-middleware/client?reload=true到底是什么?

    为了方便我们就简称它为client

    事实上,client就是我们注入到浏览器中的脚本,我们在编辑器里进行的一系列修改,浏览器自动更新,这中间的通讯过程就是由它来完成的,简单来说,我项目的文件修改了,服务器(hotMiddleware)便发送指令给client,client在接收到指令之后,根据指令的内容,相对应地完成工作,如刷新页面,更新资源等等。

    我们假设它有一个指令叫做 reload ,那我们可以这样操作:

    compiler.plugin('compilation', function(compilation) {
      compilation.plugin('html-webpack-plugin-after-emit', function(data, callback) {
        hotMiddleware.publish({ action: 'reload' })
        callback()
      })
    })
    

    我们通过使用hotMiddleware来发布(publish)一个action为reload的指令。嗯,这样是可行的,接下来我们需要来实现这个 reload

    因为client本没有这个指令的相关内容,所以我们需要来对它进行扩展,我们在根目录下新建一个 client.js 文件,内容如下:

    var client = require('webpack-hot-middleware/client?reload=true')
    
    client.subscribe(function (obj) {
      if (obj.action === 'reload') {
        window.location.reload()
      }
    })
    

    我们引入了之前在入口的时候配置的client,然后对它扩展了一个action为reload的类型,并且定义了刷新的脚本。这样我们就完成了对client的功能扩展,以及在修改html的时候,对client发布一个reload的指令这样一个过程。

    最后一步,我们把之前我们引入的client(也就是webpack-hot-middleware/client?reload=true)替换成我们自己的client,得到最终的server:init

    gulp.task('server:init', function () {
      for (var key in devConfig.entry) {
        var entry = devConfig.entry[key]
        entry.unshift('./client.js')
      }
    
      devConfig.plugins.unshift(
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin()
      )
    
      var compiler = webpack(devConfig)
      var devMiddleware = webpackDevMiddleware(compiler, {
        hot: true,
        stats: {
          colors: true,
          chunks: false
        }
      })
    
      var hotMiddleware = webpackHotMiddleware(compiler)
    
      compiler.plugin('compilation', function(compilation) {
        compilation.plugin('html-webpack-plugin-after-emit', function(data, callback) {
          hotMiddleware.publish({ action: 'reload' })
          callback()
        })
      })
    
      app.use(devMiddleware)
      app.use(hotMiddleware)
    })
    

    ok,我们命令行里输入gulp server,然后回车一下!

    修改文件,保存,浏览器自动刷新了!css是热更新的,swig文件也可以自动刷新页面了!

    到这里为止,我们的开发环境也已经是构建完成了,可以应付开发与打包的工作。

    但是由于我的字数还没达到要求不能交卷,所以我需要继续扯下去。

    既然我们标题已经说好了是要构建一个 易用易扩展 的工作流,那怎么的也得扩展点东西来看看吧?

    嗯,好吧,自己装的逼,怎么的也得自圆其说才行。

    mock&proxy

    我们来谈谈,项目开发中,如何mock数据。

    我们现在讨论的是 基于通过api获取数据的前后端分离模式 。假设我们当前有以下两种情况:

    1. 后端还没写好接口,我们需要自己来生成一些假数据;
    2. 后端已经写好接口,我们本地开发调用的时候需要解决跨域问题。

    第一个问题很好解决,我们在之前整合express的时候已经稍微提到了一下,我们可以自己写路由来满足调用,我们首先拦截路由/api/:method,然后写一个mock-middleware来专门处理它的请求,任务变成了这样(注:这里我们为了专注于mock,把前面middleware的内容给省略掉):

    var mockMiddleware = require('./mock-middleware.js')
    
    gulp.task('server:init', function () {
      app.use('/api/:method', mockMiddleware)
    })
    

    我们来实现这个mock-middleware,其实很简单:

    var map = {
      hello: {
        data: [1, 2, 3],
        msg: null,
        status: 0
      }
    }
    
    module.exports = function (req, res, next) {
      var apiKey = req.params.method
    
      if (apiKey in map) {
        res.json(map[apiKey])
      } else {
        res.status(404).send('api no found!')
      }
    }
    

    代码也不多,而且都是字面上的意思,咱们简单一点介绍过去:我们首先获取url中的method保存为apiKey,然后我们预先定义好一个map,这个map包含了所有的mock数据,我们定义了一个hello的接口,最后拿apiKey匹配map,如果匹配则返回预设的数据,如果不匹配则返回一个404页面。

    第一个问题我们就这么解决了,我们看第二个问题,重点在于:解决跨域问题。

    如何解决跨域问题?用服务器代理(proxy)接口。

    代理(英语:Proxy),也称网络代理,是一种特殊的网络服务,允许一个网络终端(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。 —— 百度百科

    百度这解释太晦涩难懂了,还是我来说吧。比如你需要访问服务器A的数据,但是某些原因导致你无法直接访问到或者访问很困难,那么这时候有一台服务器B,你访问B没有障碍,而B访问A也没有障碍,那么我就让B帮我去访问A,我只要访问B,B接收我的这次访问内容,然后去A上面相对应地取数据,取回数据之后返回给我。这个B就是传说中的 黄牛党 代理服务器。

    现在我本地页面去访问另外一台服务器上的后端接口,遇到了跨域问题,那么我可以通过服务器代理接口的方式,把接口代理到我本地,我访问本地的接口,就相当于访问了后端服务器的接口,并且没有跨域问题。

    这里随便找了一个proxy-middleware,这类包相当多,大家可以自行选择,实现如下:

    var url = require('url')
    var proxy = require('proxy-middleware')
    
    app.use(proxy(url.parse('http://tx2.biz.lizhi.fm')))
    

    跑起来之后,试着在浏览器访问localhost:8080/audio/hot?page=1,可以看到结果:

    {
      data: {
        content: [
          {
            coverBig: "http://cdn103.img.lizhi.fm/audio_cover/2016/07/29/30278047895714567.jpg",
            coverThumb: "http://cdn103.img.lizhi.fm/audio_cover/2016/07/29/30278047895714567_80x80.jpg",
            createTime: "2016-07-29 11:48:39",
            duration: 49,
            file: "http://cdn5.lizhi.fm/audio/2016/07/29/2548065522170814982_hd.mp3",
            id: 9,
            mediaId: "2548065522170814982",
            name: "#天下骄傲#伏羲",
            status: 0,
            type: "app",
            uid: "2543827817207562796",
            vote: 42559
          },
          ...
        ],
        pageIndex: 1,
        pageSize: 10,
        queryAll: false,
        totalCount: 200,
        totalPage: 20
      },
      msg: null,
      status: 0
    }
    

    搞定!(这接口别玩得太过啊,万一我项目服务器挂了我怨你们,建议拿百度的练手~)

    附上最终server:init代码:

    gulp.task('server:init', function () {
      for (var key in devConfig.entry) {
        var entry = devConfig.entry[key]
        entry.unshift('./client.js')
      }
    
      devConfig.plugins.unshift(
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin()
      )
    
      var compiler = webpack(devConfig)
      var devMiddleware = webpackDevMiddleware(compiler, {
        hot: true,
        stats: {
          colors: true,
          chunks: false
        }
      })
    
      var hotMiddleware = webpackHotMiddleware(compiler)
    
      compiler.plugin('compilation', function(compilation) {
        compilation.plugin('html-webpack-plugin-after-emit', function(data, callback) {
          hotMiddleware.publish({ action: 'reload' })
          callback()
        })
      })
    
      app.use('/api/:method', mockMiddleware)
      app.use(proxy(url.parse('http://tx2.biz.lizhi.fm')))
    
      app.use(devMiddleware)
      app.use(hotMiddleware)
    })
    

    好,到这里为止,我们已经完成了大部分的使用场景,基本上是啥需求都能满足了,最后再加上个我们在介绍gulp的时候就已经讲到过的ftp任务,那就算功德圆满了。

    总结

    抛开我们中间的一些扩展的知识点不讲,我们只看大框架gulp+webpack,这样一个组合是不是具有很多的优点?我们可以看到,单单对于gulp项目来说,项目对资源的处理能力提升了,而对于webpack的项目来说,项目的功能更加齐备了而且扩展也相当方便。

    事到如今,你还会觉得gulp和webpack是一回事吗?你还会感到困惑吗?

    会的话我也没法怎么着了,你自己看着办吧。

    最后的最后,再给大家布置一些任务,由于篇幅关系,我们很多细节其实没有做好:

    1. 随着功能越来越多,根目录下的配置文件也越来越多,像gulpfile.js、webpack.config.js、webpack.dev.config.js、server.js等等,有些散乱,而且这些都是项目无关的文件,属于工具,我们可以建个 build 文件夹来收纳一下,这里相对应的就有很多路径需要修改,这操劳的事就大家自觉去做了;
    2. 除了第一节的gulp讲解之外,我为了省事都没有提醒大家把诸如gulp buildgulp server等这些命令封装在package.json中作为预设脚本,一定程度上影响了雅观,事实上我自己是做了的,也希望大家要自觉去做;
    3. 每次运行编译打包(build)相关命令之前,都要加上rimraf dist,清除过期的内容;
    4. 有一些类型的资源我没有讲到,比如image、font,甚至是react,等等,其实这是留给大家自己去尝试的,大家要能举一反三,何况这又是很简单的事。

    那这一系列就到此完结了,希望大家看完最终都能有所收获。

    本次演示项目的git地址:gulp-webpack_base

    【上一篇:进阶:构建具备版本管理能力的项目】

    (文章有任何谬误之处,欢迎留言指出)

    相关文章

      网友评论

        本文标题:强化:构建易用易扩展的工作流

        本文链接:https://www.haomeiwen.com/subject/gxdksttx.html