前端工程化之自动化构建--Gulp案例与封装、FIS使用


前端工程化之自动化构建–Gulp案例与封装、FIS使用

相关文件下载:传送门

1、gulp案例-样式编译

// gulpfile.js
const { src, dest } = require('gulp')
// 先执行 `yarn add gulp-sass --dev` 去安装gulp—sass插件
// 再执行 `yarn add sass --dev` 去安装sass解释器
// 另外下载时可能会失败,可以通过淘宝镜像源为sass配置镜像
const sass = require('gulp-sass')(require('sass'));

const style = () => {
    // 用base选项去指定基准路径,输出到dist目录后也能保留src下的目录结构
    return src('src/assets/style/*.scss', { base:'src' })
    //基本上每一个插件提供的都是一个函数,而函数的调用结果则会返回一个文件的转换流
    //注意sass模块工作时会认为"_"下划线开头的样式文件都是主文件依赖的文件而不去转换
    //而outputStyle: 'expanded'是为了设置css结束的括弧“}”到新的一行而不是末尾
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

module.exports = {
    style
}

2、gulp案例-脚本编译

// gulpfile.js
const { src, dest } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
// 需要先执行`yarn add gulp-babel --dev`去添加gulp-babel到开发依赖中
const babel = require('gulp-babel')

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    //gulp-babel只是去唤醒babel-core模块中的转换过程,
    //所以还需要先执行`yarn add @babel/core '@babel/preset-env' --dev`安装需要的模块。
    //其中'@babel/preset-env'模块可以把ES的全部新特性都进行转换
    //注意如果在babel中忘了传presets的话,则会出现转换没有效果的情况出现,
    //因为babel只是一个ES的转换平台,实际上具体做转换的是babel里的一些插件,
    //而'@babel/preset-env'就是插件的集合,是最新es所有整体特性的打包。
    //当然我们也可以根据需要去安装对应的babel转换插件,然后在presets里传入对应插件即可
    //在dest之前先pipe到babel插件中
    .pipe(babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
}
module.exports = {
    style,
    script,
}

另外我们对babel的配置也可以单独添加一个babelrc文件,我们上面只是写在了代码里,实际上功能上没有什么区别。

3、gulp案例-页面模板编译及任务组合

模板文件实际上就是html文件,我们可以用模板引擎去把页面中重用的部分抽象出来,这里我们使用swig模板引擎。通过执行yarn add gulp-swig --dev把gulp-swig加入开发依赖。

// gulpfile.js
// 因样式,脚本,页面可以同时编译故可通过parallel组合任务
const { src, dest, parallel } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const babel = require('gulp-babel')
// 执行`yarn add gulp-swig --dev`安装gulp-swig后引入对应插件
const swig = require('gulp-swig')

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://philojs.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

const page = () => {
    //注意若我们需要选择src目录及其子目录下的所有HTML文件的话可以使用'src/**/*.html'
    return src('src/*.html', { base:'src' })
    //在swig中指定data参数并传入相关的数据供模板的数据标记使用
    .pipe(swig({ data }))
    .pipe(dest('dist'))
}

// 通过parallel组合三个任务使其可以同时执行
const compile = parallel(style, script, page)

module.exports = {
    compile,
}

4、gulp案例-图片和字体文件转换

// gulpfile.js
const { src, dest, parallel } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const babel = require('gulp-babel')
const swig = require('gulp-swig')
// 执行`yarn add gulp-imagemin@7.1.0 --dev`安装gulp-imagemin(注意版本过高可能后续不能用)
// 另外gulp-imagemin内部依赖的模块也是一些通过C++完成的模块,需要去GitHub上下载对应的二进制程序集
// 又因国内对GitHub的访问不太友好,所以我们还需要用一些其他的办法解决。
const imagemin = require('gulp-imagemin')

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://dipphilojscheese.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    .pipe(swig({ data }))
    .pipe(dest('dist'))
}

const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    //导入gulp-imagemin模块后就可以pipe进去
    //图片是无损压缩,只是删除了一些元数据信息,而对于svg这种,则是做了一些代码的格式化
    .pipe(imagemin())
    .pipe(dest('dist'))
}

//字体文件中同样可能会出现svg,所以也可以用imagemin去处理,对于不能处理的文件也会照原样输出
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'))
}

// 通过parallel组合任务使各种任务可以同时执行
const compile = parallel(style, script, page, image, font)

module.exports = {
    compile,
}

5、gulp案例-其他文件及文件清除

我们已经处理完成了所有的src目录下的文件,接下来我们还需要对public目录下的所有文件做一个拷贝。并在之后添加文件清除与构建(build)任务。

// gulpfile.js
// 因为文件清除不能和其他任务同时执行,所以需要导入series
const { src, dest, parallel, series } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const babel = require('gulp-babel')
const swig = require('gulp-swig')
const imagemin = require('gulp-imagemin')
// 先执行`yarn add del --dev`去添加一个del模块,注意他不是gulp模块,但能在gulp中导入使用
const del = require('del')


// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://dipcheese.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    .pipe(swig({ data }))
    .pipe(dest('dist'))
}

//图像与字体转换压缩
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'))
}
const font = () => {
    return src(routes:'src/assets/fonts/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'))
}

// 通过extra任务对public目录下的所有文件做一个拷贝
const extra = () => {
    return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

const clean = () => {
    // del方法返回一个promise,所以gulp在del完成后可以标记clean任务完成
    return del(['dist'])
}

// 一般compile只是对src下的文件做一个转换,所以我们单独添加一个新的任务build
const compile = parallel(style, script, page, image, font)

// build任务需要先清除dist目录,所以需要先执行clean然后再同时转换输出src与public下的文件
const build = series(clean, parallel(compile, extra))

module.exports = {
    compile,
    build,
}

6、gulp案例-自动加载插件

随着我们的构建任务越来越复杂,我们使用到的插件也会越来越多,如果都是用require来手动载入插件的话就不太利于后期回顾代码。不过我们可以通过一个叫gulp-load-plugins的插件解决,通过执行yarn add gulp-load-plugins --dev来把gulp-load-plugins安装到开发依赖。

// gulpfile.js
const { src, dest, parallel, series } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const del = require('del')
// 把gulp-load-plugins安装到开发依赖后进行导入,注意这里require导出的是一个方法
const loadPlugins = require('gulp-load-plugins')

// 通过loadPlugins方法得到一个plugins对象,所有的插件会成为对象上的一个属性。
// plugins对象的属性对应插件的命名方式是把原来的‘gulp-’去掉,并用驼峰命名法
const plugins = loadPlugins()

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    // sass需要指定解释器,不能直接使用插件
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    // 自动导入插件后使用插件
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://philojs.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    // 自动导入插件后使用插件
    .pipe(plugins.swig({ data }))
    .pipe(dest('dist'))
}

//图像与字体转换压缩
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    // 自动导入插件后使用插件
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
    // 自动导入插件后使用插件
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 拷贝public目录下的文件及其子目录文件
const extra = () => {
    return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

// 清除dist目录
const clean = () => {
    return del(['dist'])
}

const compile = parallel(style, script, page, image, font)

const build = series(clean, parallel(compile, extra))

module.exports = {
    compile,
    build,
}

7、gulp案例-开发服务器

除了对文件的构建操作外,还需要一个开发服务器用于在开发阶段调试我们的应用,并且我们也可以用gulp去启动并管理这个服务器。之后我们就可以在后续配合其他的构建任务实现在代码修改后自动去编译并刷新浏览器页面,从而大大提高我们在开发阶段的效率,减少我们在开发阶段的重复操作。

首先我们需要先执行yarn add browser-sync --dev安装一下browser-sync模块到开发依赖。这个模块可以提供一个开发服务器,相较于普通使用express创建的服务器来说,browser-sync有更加强大的功能,具体就是他支持代码更新过后自动热更新到浏览器中,让我们可以即时看到页面的最新效果。

// gulpfile.js
const { src, dest, parallel, series } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const del = require('del')
// 由于browser-sync并不是gulp插件,只不过我们是用gulp去管理他而已(与上面的del类似)
// 所以在安装后还需要单独去引入
const browserSync = require('browser-sync')
// browser-sync模块提供了一个create方法用于去创建一个服务器
const bs = browserSync.create()

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://philojs.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    .pipe(plugins.swig({ data }))
    .pipe(dest('dist'))
}

//图像与字体转换压缩
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 拷贝public目录下的文件及其子目录文件
const extra = () => {
    return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

// 清除dist目录
const clean = () => {
    return del(['dist'])
}

// 将开发服务器单独定义到一个任务中启动
const serve = () => {
    // 在任务中用bs的init方法去初始化一下web服务器的相关配置,并按配置启动服务器
    bs.init({
        //配置里最核心的就是server
        server: {
            // server中需要指定一下web服务器的网站根目录,通过baseDir属性设置
            // src中都是未经加工的代码,而加工后的结果会放在dist中,所以这里是dist目录
            baseDir: 'dist',
            // 再给browserSync加一个特殊的路由,使之对页面中出现的“node_module”等
            // 都能定向到指定目录下。通过routes去配置,并且routes会优先于baseDir,
            // 当请求发出后会先看routes下有没有对应配置,没有时再找baseDir下文件。
            routes: {
            	// 键代表请求的前缀,值为相对项目路径下的查找名称
                '/node_modules': 'node_modules',
            }
        },
        // 另外在init方法里还可以指定一些其他的小选项。
        // 如notify属性可以设置是否在页面右上方提示browser-sync的连接状态,
        // 这个提示可能会影响到我们调试界面的样式这些,就可以关闭
        notify: false,
        // 通过port属性去设置web服务器启动端口(默认是3000)
        port: 8888,
        // 通过open属性控制是否在browser-sync启动时自动打开浏览器
        open: false,
        // 通过files属性指定browser-sync的监听文件(通配符),
        // 当监听的文件发生改变后,就会自动更新浏览器
        // 注意本节中只是监听dist目录下的变化,下节将介绍src目录下文件变化的操作。
        files: 'dist/**'
    })
}

const compile = parallel(style, script, page, image, font)

const build = series(clean, parallel(compile, extra))

module.exports = {
    compile,
    build,
    serve,
}

8、gulp案例-监视变化以及构建优化

有了开发服务器之后,我们接下来重点要考虑的就是如何在src下的源代码修改过后自动去编译。

// 我们需要借助gulp提供的watch API,他会自动监视一个文件路径的通配符,
// 然后根据文件的变化决定是否需要重新执行某个任务
const { src, dest, parallel, series, watch } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const del = require('del')
const browserSync = require('browser-sync')
const bs = browserSync.create()

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://philojs.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    // 注意因swig模板引擎有缓存的机制,可能会导致页面不会发生变化,
    // 这时需要额外将swig选项中的cache设置为false
    .pipe(plugins.swig({ data, defaults: { cache: false } }))
    .pipe(dest('dist'))
}

//图像与字体转换压缩
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 拷贝public目录下的文件及其子目录文件
const extra = () => {
    return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

// 清除dist目录
const clean = () => {
    return del(['dist'])
}

// 开发服务器启动任务
const serve = () => {
    // 在serve开始的时候用watch监视一些文件,watch的第一个参数是监视的文件路径通配符,
    // 第二个参数是监视文件变化后执行的对应任务
    watch('src/assets/styles/*.scss', style)
    watch('src/assets/scripts/*.js', script)
    watch('src/*.html', page)
    // 注意我们在开发阶段时其实并不需要压缩相关文件的体积与拷贝public目录下文件,
    // 并且进行这些额外任务还会降低开发时构建的效率
    // 所以我们一般采用另外一种方式,即修改下面的baseDir属性
    //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({
        server: {
            // 设置baseDir为数组,请求的文件按照数组索引顺序去查找
            // 当dist里找不到时,就会依次到src与public里寻找源文件
            baseDir: ['dist', 'src', 'public'],
            routes: {
                '/node_modules': 'node_modules',
            }
        },
        notify: false,
        port: 8888,
        open: false,
        files: 'dist/**'
    })
}

// compile相当于每次构建前的子任务
const compile = parallel(style, script, page)

// build任务专门用于上线前的构建,最大化性能
const build = series(clean, parallel(compile, image, font, extra))

// 再添加一个develop组合任务,用于开发时使用,最小化构建时间
const develop = series(compile, serve)

module.exports = {
    compile,
    build,
    develop,
}

注意我们在平时还可能遇到一种情况,就是在bs.init中不使用files属性去监听文件变化从而更新浏览器而都是用bs.reload去更新浏览器。这个原理也很简单,因为每次文件的变化都可以在watch中监视到,虽然在watch中不能传多个任务,但我们还可以在任务的pipe中进行浏览器的更新操作。

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
    // reload执行完的结果不是文件读写流,而只是把内部文件流的信息推到浏览器
    // 指定stream: true参数来以流的方式往浏览器推
    .pipe(bs.reload({ stream: true }))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } }))
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

// 之后在下面的bs.init中就不需要指定files属性了

9、gulp案例-useref文件应用处理

到目前为止,实际上大部分的构建任务都已经完成了,这些任务基本上都已经完成了核心的工作,但对于dist下生成的文件还有一些小问题。在执行完yarn gulp build后,dist目录下就会按照最终上线的状态呈现所有生成的文件,而在这些文件中就会出现在“node_module”中的一些文件依赖。而这些文件并没有拷贝到dist目录,当我们将dist目录部署上线的话就会出现问题,会出现找不到这些文件的情况。而我们在开发阶段运行没有问题的原因则是因为我们在serve命令里做过路由的映射,但这并不能在线上的环境这样做,所以我们还要单独去处理这样的情况。

针对上述问题的处理方法其实有很多,有一种简单方法就是在HTML中先写入一个不存在的路径然后通过构建的方式把这些文件拷贝到对应的路径中去。这种方法虽然简单,但比较low,本节将为大家介绍一个更为常见与强大的方式,即借助useref插件,useref插件会自动去处理HTML中的构建注释:

<!-- 构建注释的开始标签为build,结束标签为endbuild。 -->
<!-- 在开始标签后标记引入的文件类型,最后再指定一个文件路径, -->
<!-- useref会自动将开始标签与结束标签之间引入的文件都合并打包到当前指定的文件路径中 -->
<!-- build:css assets/styles/vendor.css -->
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.css">
<!-- endbuild -->

<!-- build:css assets/styles/main.css -->
<link rel="stylesheet" href="assets/styles/main.css">
<!-- endbuild -->

<!-- build:js assets/scripts/vendor.js -->
<scripts src="/node_modules/jquery/dist/jquery.js"></scripts>
<scripts src="/node_modules/popper.js/dist/umd/popper.js"></scripts>
<scripts src="/node_modules/bootstrap/dist/js/bootstrap.js"></scripts>
<!-- endbuild -->

<!-- build:js assets/scripts/main.js -->
<scripts src="assets/scripts/main.js"></scripts>
<!-- endbuild -->

在gulpfile.js中我们也要添加一个新任务,不过需要先执行yarn add gulp-useref --dev去安装useref插件到开发依赖中。注意,useref可以理解为引用关系。

const { src, dest, parallel, series, watch } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const del = require('del')
const browserSync = require('browser-sync')
const bs = browserSync.create()

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://philojs.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } }))
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

//图像与字体转换压缩
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 拷贝public目录下的文件及其子目录文件
const extra = () => {
    return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

// 清除dist目录
const clean = () => {
    return del(['dist'])
}

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

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

const useref = () => {
    // 在useref任务中也和普通的任务类似,不同的是在src中我们要找的是dist下的html,
    // 因为在src下的html是模板,模板里做useref是没有意义的,只有当文件都生成后才有意义。
    // 注意下面的base指定与否都是一样的,因都在dist目录下,但为了保持一致在此还是指定。
    return src('dist/*.html', { base: 'dist' })
    // useref会被自动加载,然后再通过useref创建转换流转换构建注释,
    // 其中还需要设置一个searchPath参数去指定由构建注释包裹的对应文件的寻找路径。
    // 注意对于数组型的参数,一般会把使用概率更大的情况放前面
    .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
    // 最后pipe到dest写入流中,注意这里指定dist目录存在一定的不合理
    .pipe(dest('dist'))
    
}

// compile相当于每次构建前的子任务
const compile = parallel(style, script, page)

// build任务专门用于上线前的构建,最大化性能
const build = series(clean, parallel(compile, image, font, extra))

// develop任务,用于开发时使用,最小化构建时间
const develop = series(compile, serve)

module.exports = {
    compile,
    build,
    develop,
    useref,
}

回到命令行执行yarn gulp useref去运行useref任务,之后dist目录下的文件就会发生变化。

当然,因为useref在运行之后会自动修改html,并由html中依赖的文件创建了一些新的文件生成到dist中,这个过程会在读取流中创建一些新文件,而我们则可以对这些新文件做一些压缩之类的操作,具体内容还请看下回分解。

10、gulp案例-文件压缩

useref可以自动帮我们把依赖的文件都合并生成到dist下,但我们还是需要对这些生成的文件做一个压缩。

而我们需要压缩的文件有三种:JS、CSS、HTML,对于这三种文件类型我们要分别去做不同的压缩工作。首先执行yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev去分别安装压缩html,js,css的插件到开发依赖。并且因为读取流中会出现三种文件,而我们得分别对这些类型的文件做不同的操作,所以我们还得去判断一下读取流中是什么文件,这可以通过gulp-if插件完成。我们可以执行yarn add gulp-if --dev去安装该插件到开发依赖,有了这些插件后我们就可以去完成相关的压缩任务了。

const { src, dest, parallel, series, watch } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const del = require('del')
const browserSync = require('browser-sync')
const bs = browserSync.create()

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://philojs.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } }))
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

//图像与字体转换压缩
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 拷贝public目录下的文件及其子目录文件
const extra = () => {
    return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

// 清除dist目录
const clean = () => {
    return del(['dist'])
}

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

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

const useref = () => {
    return src('dist/*.html', { base: 'dist' })
    .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
    // if会自动创建一个转换流,不过在转换流的内部会根据if指定的条件去决定是否去执行具体的转换流。
    // if的第一个参数指定一个正则,自动匹配文件读取流中的文件路径
    // if的第二个参数指定需要具体去工作的转换流
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    // 注意htmlmin默认只会压缩属性中的空白字符,而对于换行符这些都不会压缩,
    // 所以我们需要额外去指定选项。
    // 其中用collapseWhitespace: true指定压缩html中的空白字符和换行符,
    // 设置minifyCSS和minifyJS为true指定压缩html内部的CSS和JS脚本。
    // 另外htmlmin还有其他参数如移除注释,删除空属性等,可以参看官方文档(见本节末)
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
        collapseWhitespace: true,
        minifyCSS: true,
        minifyJS: true,
    })))
    // 注意如果我们的读取流和写入流都在dist中就可能会出现文件读写冲突,
    // 从而产生文件写不进去的情况。这时我们可以换一个目录写入最终的转换结果
    //.pipe(dest('dist'))
    .pipe(dest('release'))
    
}

// compile相当于每次构建前的子任务
const compile = parallel(style, script, page)

// build任务专门用于上线前的构建,最大化性能
const build = series(clean, parallel(compile, image, font, extra))

// develop任务,用于开发时使用,最小化构建时间
const develop = series(compile, serve)

module.exports = {
    compile,
    build,
    develop,
    useref,
}

此时我们的useref已经完成了引用文件的合并与压缩并生成到新文件中了,但现在的构建结构好像就被打破了。而下回我们就来分解如何重新去规划构建过程。

另附htmlmin官方文档:传送门

11、gulp案例-重新规划构建过程

上节说到,useref打破了我们构建的目录结构。我们之前曾约定在开发阶段写的代码放在src目录下,而在编译完要打包上线的结果则放在dist目录下。但之前我们往dist目录中读文件并写入dist中时产生了文件冲突,所以我们又不得已把文件放在另一个目录。而这时我们真正上线的则应该是release目录下的文件,但release中又没有图片和字体文件,所以我们的目录结构就被打破了,我们需要重新去规整一下。

回顾代码,我们发现其实在useref之前我们一些生成的文件应该算是一个中间产物,即原来我们编译src下的文件生成到dist下后又去通过useref进行转换,这之间的文件其实算是中间文件。而我们如果把这些中间文件都放到临时的目录中,在useref时通过临时目录把这些文件拿出来做一些转换操作最后再放到dist里则会更合适一些。

const { src, dest, parallel, series, watch } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const del = require('del')
const browserSync = require('browser-sync')
const bs = browserSync.create()

// 样式编译
const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    // style任务执行后还需要useref去合并、压缩相关的引用文件,所以需要转换后放到temp目录下
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

// 脚本编译
const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    // 和style类似,需要转换后放到temp目录下
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

// 提供给模板使用的数据
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: 'Home',
          link: 'https://philojs.com'
        },
        {
          name: 'divider'
        },
        {
          name: 'About',
          link: 'https://github.com/GeorgeSmith215'
        }
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

// 页面模板编译
const page = () => {
    return src('src/*.html', { base:'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } }))
    // 和style类似,需要转换后放到temp目录下
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

//图像与字体转换压缩
const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    // image任务并不需要用到useref而只在build时做相关转换所以可以不用放到temp下
    .pipe(dest('dist'))
}
const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    // 和image任务类似,不需要放到temp下
    .pipe(dest('dist'))
}

// 拷贝public目录下的文件及其子目录文件
const extra = () => {
    return src('public/**', { base: 'public' })
    // 和image任务类似,不需要放到temp下
    .pipe(dest('dist'))
}

// 清除dist目录
const clean = () => {
    // 在clean任务中可以加一个temp目录去清空中间文件所在目录
    return del(['dist', 'temp'])
}

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

    bs.init({
        server: {
            // 因为在style,script,html任务(compile)执行后生成的文件都在temp下,
            // 所以在baseDir中就应该去temp中找对应的文件了,
            // 而dist目录只是我们最终需要上线打包的目录
            baseDir: ['temp', 'src', 'public'],
            routes: {
                '/node_modules': 'node_modules',
            }
        },
        notify: false,
        port: 8888,
        //open: false,
        //files: 'dist/**'
    })
}

const useref = () => {
    // 在useref中我们需要从temp中去取文件,并把最终的结果放到dist里
    return src('temp/*.html', { base: 'temp' })
    .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
    .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'))
    
}

// compile相当于每次构建前的子任务
const compile = parallel(style, script, page)

// build任务专门用于上线前的构建,最大化性能
// 最后useref还应该放到我们的组合任务中去使用,注意useref必须要放在compile之后,
// 所以compile和useref应该是一个串行的结构
const build = series(clean, parallel(series(compile, useref), image, font, extra))

// develop任务,用于开发时使用,最小化构建时间
// 注意develop和dist没有关系,他只是生成temp下的临时文件并通过serve找temp下的文件
const develop = series(compile, serve)

module.exports = {
    compile,
    build,
    develop,
    useref,
}

以上,我们完整的构建过程就已经告一段落了,当然在上面使用到的插件可能还会有额外的选项,我们在持续学习这部分内容的时候可以单独针对不同的插件去参考他们的官方文档,而上文中只是用到了最为常用的开发选项。

12、gulp案例-补充

在了解了完整的构建过程后,其实还留下了两个小问题,第一个问题就是对于我们的构建任务完成之后,如果不想配文档和说明的话,最好需要去调整一下我们导出的任务。

// 其实我们只需要导出clean,build,develop任务就够了。
module.exports = {
    clean,
    build,
    develop,
}

同时,我们还可以把这三个任务都放在package.json中,定义到scripts里,这样可以更容易让别人更理解一点。

"scripts": {
    "clean": "gulp clean",
    "build": "gulp build",
    "develop": "gulp develop",
}

另外我们还需要注意的就是,我们在npm的scripts中会自动找所执行的命令在node_modules下的可执行的命令文件,所以我们不需要通过yarnnpm run的方式去启动这些命令。之后我们想要启动相关的命令就可以直接使用yarn <相关命令>即可,可以更加方便些。

此外我们还需要注意的一个小点就是在.gitignore中去忽略一下生成的目录。

dist
temp

第二个问题是,我们在开发过程中创建的这种自动化工作流其实只要在相同类型的项目中都会重复被使用到,而我们应该如何去提取多个项目中共同的自动化过程?当然我们可以把这些代码放到代码段或笔记中记录下来,要用的时候再粘贴过去,这样虽然可以,但若要修改时就可能会非常麻烦,而之后的几节我们就会介绍如何去封装一个自己的工作流。

13、封装工作流 - 准备

接下来我们就来重点考虑一下在项目中gulpfile的复用问题。

如果我们要开发多个相同类型的项目,我们的自动化工作流其实应该是一样的,这时候就涉及到我们在多个项目中重复去使用这些构建任务。而这些构建任务在绝大多数情况下都是相同的,所以我们就面临一个需要去复用相同gulpfile的问题。对于这个问题我们当然可以用代码段的方式把这段代码保存起来,最后再copy到需要用到的地方,但这种方式会让我们的gulpfile散落在各个项目中,一旦当这个gulpfile有些问题需要我们去修复或者升级,就需要对每一个项目中的文件做相同的操作,十分不利于我们对整体的维护。所以我们要重点看如何去提取一个可复用的自动化构建工作流。

具体的解决方法也挺简单,就是通过创建一个新的模块去包装一下gulp,把自动化构建的工作流包装进去。因为gulp只是一个自动化构建工作流的一个平台,不负责帮我们提供任何的构建任务,我们的任何构建任务需要通过gulpfile去定义。而现在我们有了gulpfile也有了gulp,我们可以把他们通过一个模块结合在一起,之后我们就可以在以后的同类型项目中使用我们的模块去提供自动化构建的工作流就行了。

具体的做法就是我们先去创建一个模块,然后把这个模块发布到npm仓库上,最后在我们的项目中去使用即可。

首先,我们可以先到GitHub上创建一个仓库,这样我们就可以把我们新创建的模块托管到GitHub上,在新建时仓库的名字和模块的名字保持一致即可。创建完成后复制模块地址,回到命令行中,此时我们需要创建一个新的module。这里,我们当然可以使用传统的方式自己去初始化一个package.json这一系列文件,但我们之前已经接触过了脚手架,对于创建相同类型的项目,一般都可以使用脚手架来完成。经过之前的学习(忘了的同学参看此处),我们完全可以自己去实现一个脚手架,但在此,我们使用一个叫做caz的脚手架来完成创建module,通过执行yarn global add caz去全局安装caz模块。

在安装完caz后,可以通过执行caz nm <模块名>去创建一个node模块,他会和大多数的脚手架一样,先去GitHub上下载模板,之后询问一些问题,我们可以根据情况填写,需要注意的是,对于features我们全部不选,后续我们需要什么特性的时候可以自己去加,熟悉一步步建立的过程。之后caz会自动帮我们创建好项目,我们进入创建的目录,执行git init去初始化一下仓库。初始化完成后,执行git remote add origin <之前复制的模块仓库地址>为远程地址添加别名,之后执行git add.去添加文件到暂存区,接着执行git commit -m "init"去提交暂存区的文件到本地仓库,最后执行git push -u origin master以流的方式把本地仓库的文件推到origin别名的远程地址的master分支。回到GitHub中,刷新一下,就会发现所有的文件都已经更新了。

接下来看一看node module的大概结构,这个结构也即脚手架提供的默认约定。项目根目录下的文件主要就是特定工具的配置文件,而lib目录下的index.js则是项目的入口文件,后续需要在模块里实现的代码都放到这里。其实这些都是一个约定,也就是caz帮我们生成的node module提供的约定,我们就按照这个约定去创建之前所说的模块,把之前创建的自动化工作流的实现结合gulp形成新的模块,之后在我们后续使用同类型的项目时就可以使用该模块去提高效率。

14、封装工作流 - 提取gulpfile

在上节中我们用脚手架创建了一个新的node模块,而接下来就将介绍如何实现具体的模块功能。

我们大致需要做的事情就是把之前创建的自动化构建的工作流提取到node模块中,然后在node模块中封装好工作流,接着再把一系列需要解决的问题都解决掉,最后我们就可以在多个项目中使用这个工作流了。

首先,我们知道要在node模块中包装gulp和gulpfile里提供的工作流的任务定制,而我们第一件要做的事情就是把gulpfile.js里的内容整体复制到node模块下lib目录下的index.js入口文件。模块的入口文件有了之后,里面还有不同的构建任务,而这些任务又是依赖一些模块的,所以我们还需要把这些模块作为我们的node模块的依赖去安装,这样后续我们在别的项目中使用到这个模块时,他就会自动去安装要用的模块。所以我们需要把我们之前工作流项目的package.json中的devDependencies的内容都复制到node模块的package.json中的dependencies里。注意因为每个工作流项目都不同所以不需要复制工作流中的dependencies,而至于为什么是复制到node模块的dependencies里是因为当我们去安装这个node模块时,会去安装该node模块的dependencies里的模块,而不是devDependencies,devDependencies开发依赖只是我们开发这个node模块所需要的东西,最终工作环节里只会有dependencies里的模块。

在将对应的dependencies复制到node模块中后,先执行yarn去安装一下复制过去的依赖模块,安装完成之后,node模块的基本工作就完成了。只不过对于node模块的入口文件,还有很多需要修改的地方。

以上就完成了该node模块第一步的工作,之后,我们回到原始的工作流项目中,把gulpfile.js里的内容以及相关的开发依赖devDependencies与工作流项目中的相关node_modules文件夹都删除掉(node_modules文件夹若在VSCode运行时删除不了的话可以先退出VSCode后再删除),用我们新的node模块去提供自动化构建的工作流。

在执行完清空后,若想用我们新的node模块去提供自动化构建的工作流,一般流程下我们需要先把这个node模块发布到npm仓库中,然后回到项目里安装这个模块,但现在是开发阶段,我们的模块还没开发完成,需要本地去调试。所以最简单的一种方式就是使用yarn link的方式把这个node模块link到当前工作流项目的node_modules中。首先,我们需要在node模块中打开命令行终端,执行yarn link去link一下我们的node模块,yarn会自动把我们的模块link到全局。之后我们就可以在任何一个项目中再通过yarn link <模块名>去把这个node模块link到该项目中。我们再到工作流项目的命令行窗口,执行yarn link "<我们的node模块名>"后就能发现在工作流项目下就有了node_modules文件夹,并在里面可以找到以我们的node模块名命名的文件夹,也可以看到在文件夹旁有一个标志着软链接的小图标。link了我们的node模块后,我们就可以在我们原本的工作流项目中使用这个模块了。注意我们的node模块导出的是gulpfile里的内容,而我们的工作流项目中缺的也正是相关的gulpfile,所以一拍即合,我们就可以在gulpfile.js直接写:module.exports = require('<我们的node模块名>')去把我们从node模块里读取的内容直接进行导出。

这时我们的gulpfile按理来讲就应该ok了,但因为之前我们删除了所有node_modules里的内容,所以这里还需要执行一下yarn去安装工作流相关的dependencies。安装完依赖之后,因为gulpfile实际上已经提供了从node模块中导入的任务,所以按道理来讲这个工作流项目是可以正常运行的。回到工作流的命令行中执行yarn build去运行导入的任务,可是运行之后会报一个错误,提示我们没有找到gulp命令。这个错误表示node在执行script时,没有办法找到script中使用的gulp命令,因为在使用yarn时会自动找项目目录下的node_modules目录下的bin文件夹下有没有yarn这个可执行文件,而现在其实并没有这个文件,所以gulp就使用不了。这里我们先使用一个比较low的办法,执行yarn add gulp-cli --dev把gulp的命令行可执行文件先安装到开发依赖,之后我们再考虑怎么去掉这个步骤。之后我们刷新一下项目目录,就会发现在node_modules目录的.bin文件夹下就有了相关的命令了。

现在,我们有了gulp命令,有了gulpfile,而且gulpfile里的内容是通过我们的node模块提供的,所以我们再执行一下yarn build应该就没问题了,结果命令行还是会报错提示找不到gulp,没办法,我们也只好先执行yarn add gulp --dev去安装一下gulp到开发依赖中。但其实这个问题在后续我们真正将模块发布出去之后就不存在了,因为当我们发布我们的这个node模块之后,当想安装该模块时,就会自动安装gulp模块,我们这里只是因为我们还没有完成才需要用这种方式先解决一下。最后,让我们再次执行yarn build去运行build任务,这时命令行中则会报一个找不到’./package.json’的错误,如果我们还有点印象的话,应该能想起来在我们之前的gulpfile中曾经定义了一个data数据变量,并在data中将模块的一些信息包裹进去(其中就包括了引用了package.json),最后在模板编译的时候使用了这些数据信息。但现在我们已经将gulpfile提取出来了,那data里面的相关require也就不存在了,另外,其实data是我们的项目才知道的,对于我们封装的通用模块是不应该知道的,如果有多个项目都使用了我们的通用模块,他们的data也应该是不一样的。这时我们可以用一种约定大于配置的方式去解决,即在我们的项目中抽象一个配置文件,然后在我们的node模块中读取一下相关的配置文件。这种方式是合理的,而至于如何去操作,还请看下回分解。

15、封装工作流 - 解决模块中的问题

上节讲到,我们想在我们的项目中抽象一个配置文件,然后在我们的node模块中读取一下相关的配置文件。现在我们需要做的事情就是把我们node模块中不应该被提取的东西全部都给抽出来。其中第一个就是我们上节讲到的data,这个data就可以通过约定大于配置的方式,在项目根目录下创建一个配置文件,然后在我们的node模块中尝试读取这个配置文件。而这个配置文件则可以有一个名字的约定,比如在这里我们就使用pages.config.js,这个其实也在很多常见成熟的自动化工作流或库中有所体现,比如vue-cli在工作时就会读取项目根目录下的vue.config.js,本质上都是一样的。

这时我们约定了有一个pages.config.js,我们就可以在这个文件中抽象一下那些不应该在公共模块中出现的内容。首先第一个需要导出的就是一个数据成员data,也即我们原本项目中的data。

// pages.config.js
module.exports = {
  data: {
    menus: [
      {
        name: 'Home',
        icon: 'aperture',
        link: 'index.html'
      },
      {
        name: 'Features',
        link: 'features.html'
      },
      {
        name: 'About',
        link: 'about.html'
      },
      {
        name: 'Contact',
        link: '#',
        children: [
          {
            name: 'Home',
            link: 'https://philojs.com'
          },
          {
            name: 'divider'
          },
          {
            name: 'About',
            link: 'https://github.com/GeorgeSmith215'
          }
        ]
      }
    ],
    pkg: require('./package.json'),
    date: new Date()
  }
}

这时pages.config.js是在我们当前导入node模块的项目文件夹下,他自然可以找到当前目录下的package.json文件。我们把data放到pages.config.js中也不仅仅是为了可以运行,这也更符合逻辑,因为data本应属于实际要用到node模块的项目,而不应该在node公共模块自身中。

之后我们再回到node项目中,这时node模块就会缺少一个data,而这个data就需要我们动态require一下使用我们node模块项目下的pages.config.js了。

// lib/index.js
const { src, dest, parallel, series, watch } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const del = require('del')
const browserSync = require('browser-sync')
const bs = browserSync.create()

// 动态require一下使用我们node模块项目下的pages.config.js
// process.cwd()会返回当前命令行所在的工作目录,而工作目录下就应有pages.config.js文件
const cwd = process.cwd()
// 用let是因为可能工作目录中没有相关配置文件,但也不能报错,所以应该要有一些默认配置
let config = {
    // 这里可以写一些默认配置
}

// 因为require一个不存在的地址会报错,所以这里用try...catch去包装一下
try {
    // 这里最好的方式是用path.join去连接路径,不过现在为了方便就直接使用模板字符串了
    config = require(`${cwd}/pages.config.js`)
    // 用Object.assign去合并两个对象,config在前,这样loadConfig就会覆盖config里的成员
    config = Object.assign({}, config, loadConfig)
} catch (e) {} //catch并不需要去处理,因为如果读取失败了其实也有默认配置

const style = () => {
    return src('src/assets/style/*.scss', { base:'src' })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
    // 注意还需要修改presets的配置。presets最简单的方式是传一个字符串,
    // babel工作时会自动去node_modules里找,还有一种方式就是直接载入一个presets对象,
    // 这时我们就可以通过require的方式去载入。用require去载入模块会先到当前文件所在目录找,
    // 之后依次往上找,直到项目根目录下有node_modules,其下就有@babel/preset-env
    .pipe(plugins.babel({ presets: [require('@babel/preset-env')] }))
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

const page = () => {
    return src('src/*.html', { base:'src' })
    // 在读取了配置文件后,就可以把data换成config.data
    .pipe(plugins.swig({ data: config.data, defaults: { cache: false } }))
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

const image = () => {
    return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('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'))
}

const clean = () => {
    return del(['dist', 'temp'])
}

const serve = () => {
    watch('src/assets/styles/*.scss', style)
    watch('src/assets/scripts/*.js', script)
    watch('src/*.html', page)
    watch([
        'src/assets/images/**',
        'src/assets/fonts/**',
        'public/**'
    ], bs.reload)

    bs.init({
        server: {
            baseDir: ['temp', 'src', 'public'],
            routes: {
                '/node_modules': 'node_modules',
            }
        },
        notify: false,
        port: 8888,
    })
}

const useref = () => {
    return src('temp/*.html', { base: 'temp' })
    .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
    .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'))
    
}

const compile = parallel(style, script, page)
const build = series(clean, parallel(series(compile, useref), image, font, extra))
const develop = series(compile, serve)

module.exports = {
    compile,
    build,
    develop,
}

完成相关配置之后,执行yarn build就可以运行相关的build任务了。另外注意在build任务运行时会有build显示,但中间的组合任务名都会被省略,因为gulp是根据我们的gulpfile去推断我们的任务名字,而我们是把这些任务都包装进我们的node模块中,对于gulpfile就不知道相关的任务名,他唯一清楚的任务名就是我们启动的任务名。如果我们想要显示具体的组合任务的话,可以在gulpfile中解构对应的任务在进行组合导出,这样gulpfile就可以推断出这些任务名了,但对于我们这里并没有什么太大的意义,这里就不做具体的演示了。

16、封装工作流 - 抽象路径配置

以上,我们的自动化构建工作流的node模块就算大致完成了,但这里其实还有一些地方可以做深度的包装。具体而言就是我们在代码里写死的路径,这些路径在我们使用的项目中其实可以看做是一种约定,约定固然好,但有时提供可以配置的能力也非常重要。因为在我们的项目中,如果我们要求项目的源代码目录不叫src目录的话,这时就可以通过配置的方式去覆盖,提供更灵活的配置。接下来我们就看一下怎样把灵活的配置抽象出来。

其实我们要做的也就是将index.js中写死的src,temp这些路径全部抽象出来形成配置,然后在刚刚约定的配置文件里去覆盖。

// lib/index.js
const { src, dest, parallel, series, watch } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const del = require('del')
const browserSync = require('browser-sync')
const bs = browserSync.create()

const cwd = process.cwd()
let config = {
    // 这里可以先加一些默认的配置,然后后续再通过pages.config.js去覆盖
    build: {
    src: 'src',
    dist: 'dist',
    temp: 'temp',
    public: 'public',
    // 有了基础路径后还需要把文件所在路径也提取出来以便后续灵活去配置
    paths: {
      styles: 'assets/styles/*.scss',
      scripts: 'assets/scripts/*.js',
      pages: '*.html',
      images: 'assets/images/**',
      fonts: 'assets/fonts/**'
    }
  }
}

try {
    config = require(`${cwd}/pages.config.js`)
    config = Object.assign({}, config, loadConfig)
} catch (e) {}

const style = () => {
    // 可以在src方法的选项中添加cwd选项,指定寻找相关文件的路径(默认为当前项目所在目录)
    return src(config.build.paths.styles, { base:config.build.src, cwd:config.build.src })
    .pipe(sass({ outputStyle: 'expanded' }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true }))
}

const script = () => {
    // 可以在src方法的选项中添加cwd选项,指定寻找相关文件的路径(默认为当前项目所在目录)
    return src(config.build.paths.scripts, { base:config.build.src, cwd:config.build.src })
    .pipe(plugins.babel({ presets: [require('@babel/preset-env')] }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true }))
}

const page = () => {
    // 可以在src方法的选项中添加cwd选项,指定寻找相关文件的路径(默认为当前项目所在目录)
    return src(config.build.paths.pages, { base:config.build.src, cwd:config.build.src })
    .pipe(plugins.swig({ data: config.data, defaults: { cache: false } }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true }))
}

const image = () => {
    // 可以在src方法的选项中添加cwd选项,指定寻找相关文件的路径(默认为当前项目所在目录)
    return src(config.build.paths.images, { base:config.build.src, cwd:config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}
const font = () => {
    // 可以在src方法的选项中添加cwd选项,指定寻找相关文件的路径(默认为当前项目所在目录)
    return src(config.build.paths.fonts, { base:config.build.src, cwd:config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}

const extra = () => {
    // 对于public可以直接去通配一下所有文件即可
    return src('**', { base:config.build.public, cwd:config.build.public  })
    .pipe(dest(config.build.dist))
}

const clean = () => {
    return del([config.build.dist, config.build.temp])
}

const serve = () => {
    // 在watch中也可以通过第二参数去传cwd进去
    watch(config.build.paths.styles, { cwd: config.build.src }, style)
    watch(config.build.paths.scripts, { cwd: config.build.src }, script)
    watch(config.build.paths.pages, { cwd: config.build.src }, page)
    watch([
        config.build.paths.images,
        config.build.paths.fonts
    ], { cwd: config.build.src }, bs.reload)
    
    // 对于public的cwd目录是不一样的,所以需要单独抽出来一个watch
    watch('**', { cwd: config.build.public }, bs.reload)

    bs.init({
        server: {
            baseDir: [config.build.temp, config.build.dist, config.build.public],
            routes: {
                '/node_modules': 'node_modules',
            }
        },
        notify: false,
        port: 8888,
    })
}

const useref = () => {
    return src(config.build.paths.pages, { base: config.build.temp, cwd: config.build.temp })
    // '.'目录因为是项目根目录,肯定不会变,所以不需要修改
    .pipe(plugins.useref({ searchPath: [config.build.temp, '.'] }))
    .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(config.build.dist))
    
}

const compile = parallel(style, script, page)
const build = series(clean, parallel(series(compile, useref), image, font, extra))
const develop = series(compile, serve)

module.exports = {
    compile,
    build,
    develop,
}

在抽象出来默认配置后,我们还可以尝试在项目的配置文件中也添加一个配置选项,去覆盖任意默认的路径。

// pages.config.js
module.exports = {
  build: {
    src: 'src',
    dist: 'dist',
    // 一些项目的临时文件可以放在'.'开头的目录下,因为在mac中'.'开头的目录默认都是隐藏目录,
    // 像这些临时文件一般都不需要开发者去管理
    temp: '.tmp',
    public: 'release',
    // 有了基础路径后还需要把文件所在路径也提取出来以便后续灵活去配置
    paths: {
      styles: 'assets/styles/*.scss',
      scripts: 'assets/scripts/*.js',
      pages: '*.html',
      images: 'assets/images/**',
      fonts: 'assets/fonts/**'
    }
  },
  data: {
    menus: [
      {
        name: 'Home',
        icon: 'aperture',
        link: 'index.html'
      },
      {
        name: 'Features',
        link: 'features.html'
      },
      {
        name: 'About',
        link: 'about.html'
      },
      {
        name: 'Contact',
        link: '#',
        children: [
          {
            name: 'Home',
            link: 'https://philojs.com'
          },
          {
            name: 'divider'
          },
          {
            name: 'About',
            link: 'https://github.com/GeorgeSmith215'
          }
        ]
      }
    ],
    pkg: require('./package.json'),
    date: new Date()
  }
}

之后我们再去执行yarn build以及yarn develop就能按照配置正常运行相关的任务了。

现在,对于我们的模块,其中完全抽象出来的东西都已经完成了。

在补充句题外话,其实对于开发者来讲,一开始可能还需要一些技能,之后还是需要我们有想法,想法建立在技能之上,当技能能满足我们的想法的时候,这时我们的想法越多越好,想法越多,尝试就会越多,获得的东西也会越多。

17、封装工作流 - 包装Gulp CLI

至此,我们的自动化构建工作流的node模块大致上就基本完成了,但我们还可以做一些更多的操作让我们在使用他时能更加方便些。回顾我们使用这个node项目的过程,我们需要先把他安装到我们的项目中,然后在我们的项目中添加配置文件,这个配置文件是必要的。而我们的项目目录下还需要一个gulpfile.js,去把我们在node模块中工作流相关的任务导出,才可以通过gulp去运行。不过其实这个gulpfile对于我们的项目来讲,存在的价值就是把我们node模块中提供的任务导出去,这个就显得有些冗余了,而我们就希望在项目根目录下没有这个gulpfile也能正常工作。

我们先去把gulpfile.js删除,这时再执行yarn gulp就会报错,提示我们没有找到gulpfile。不过其实gulp的cli提供了一个命令行参数–gulpfile,可以指定gulpfile的所在路径,而对应的gulpfile其实就是安装了我们node模块的项目的./node_modules/<我们的模块名>/lib/index.js(我们之前删除的gulpfile.js其实也只是导出了这里面的内容)。我们可以通过执行yarn gulp build --gulpfile ./node_modules/<我们的模块名>/lib/index.js去运行对应的build任务。但还存在一个小问题就是这时我们的工作目录也已经变到了lib目录下,因为我们的gulpfile在lib下,yarn就会认为我们的工作目录也在lib目录,这时就不会把我们项目的根目录作为当前的工作目录了。不过,gulp也提供了–cwd参数去指定我们的工作目录,我们可以执行yarn gulp build --gulpfile ./node_modules/<我们的模块名>/lib/index.js --cwd .去指定我们的gulpfile以及工作目录('.'就代表项目根目录)。此时,我们的工作目录就是当前目录,我们就可以正常去使用这个工作流,只不过任务的执行需要传递参数就显得比较复杂了。不过,我们也自然而然的产生了一些想法,就是在我们的node模块中也提供一个cli,在这个cli中传递需要的参数,然后在内部调用gulp-cli提供的可执行程序,这样的话,我们在外界使用时就不用再使用gulp了,就相当于把gulp完全包装于我们的node模块中了。

我们先在我们的node模块下面添加一个cli程序,注意对于node模块而言,一般我们的项目模块代码会放在lib目录下,而对项目的cli代码则一般放在bin目录下,所以我们要先在node模块目录下新建一个bin目录。在bin目录下我们新建一个<我们的模块名>.js文件,这个文件将会作为cli的执行入口,且因他是cli的执行入口,所以在我们node项目的package.json文件中还需要额外添加一个字段"bin": "bin/<我们的模块名>.js"去将我们的模块名作为cli命令。另外,其实我们还可以自定义cli命令,如在package.json中添加:"bin": {"czs": "<我们的模块名>.js"},之后我们cli命令名就是czs了,不过这样容易产生冲突,故这里还是使用模块名字保险些。

编辑完package.json之后,我们在bin目录下的<我们的模块名>.js文件就会作为我们cli的入口。注意cli入口还需要有一个声明式注释#!/usr/bin/env node,并且在mac下还需要将文件的读写权限修改为755。

#!/usr/bin/env node

// ./bin/<我们的模块名>.js中
console.log('hello node');

我们先用上面的代码去简单使用一下我们的cli。首先我们要进入我们node模块的所在目录,执行yarn unlike

先去取消之前的link,之后再执行yarn link去重新对我们的模块进行link,注册全局。link完我们的node模块后,我们就可以在命令行中执行<我们的模块名>去运行我们的cli了。而我们只需要把对gulp-cli的调用以及之前传的复杂参数都放在当前的cli入口文件中就行了。

我们先来看下gulp的cli是怎么工作的,可以在我们node模块的node_modules/.bin目录下找到一个gulp.cmd文件,在这个cmd文件中就是按照cmd语法写的一段代码。虽然我们可能不会写,但看懂他应该没什么大问题:

// '%~dp0'表示的是当前cmd所在的目录,也就是.bin目录
// 刚开始的if语句就用来判断当前目录下有没有node.exe文件,有的话执行if后的语句,反之执行else后的语句
@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\..\gulp\bin\gulp.js" %*
) ELSE (
  // 下面两个操作实际上就是配置了环境变量,让我们的可执行文件加了.js扩展名
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  // 下面一行是重点,就是用node去执行当前目录的上一目录的gulp\bin\gulp.js
  node  "%~dp0\..\gulp\bin\gulp.js" %*
)

再让我们去看看这个”gulp\bin\gulp.js”里是什么内容。

#!/usr/bin/env node

// 这里实际上只是require了一下gulp-cli里的方法然后调用
require('gulp-cli')();

根据上面的代码,如果我们想在我们的cli中执行gulp的话也很简单,只要require一下gulp\bin\gulp.js就可以了,但问题是我们要怎么传递参数去指定gulpfile路径以及cwd路径。这里,其实我们在命令行中传递的参数可以通过process.argv去拿到,process.argv是一个数组,我们就可以在代码运行之前先往process.argv中push我们要传递的参数。

#!/usr/bin/env node

// 工作目录应该是当前命令行所在的目录
process.argv.push('--cwd')
process.argv.push(process.cwd())
process.argv.push('--gulpfile')
// gulpfile所在路径就是当前node项目下的lib目录下的index.js,
// 对于我们现在的这个项目,可以直接传require.resolve('..'),
// require是载入相关模块,而require.resolve是找到这个模块对应文件的绝对路径。
// resolve会自动去找package.json中main字段里对应的文件,同样也是对应的index.js文件
process.argv.push(require.resolve('..'))

require('gulp/bin/gulp')

之后我们进入到安装了我们node模块的项目中,在该目录下执行<我们的模块名> build就可以运行对应的build任务了,我们的cli会自动找到位于lib/index.js的gulpfile,且gulp的工作目录也是当前目录。

完成了包装gulp cli后,我们的模块就不再要求我们的项目根目录下必须要有package.json,而且如果说我们把我们的node模块作为全局模块去安装的话,我们甚至在项目本地都不需要去安装依赖,这样也会让我们在后续的使用更加方便。相当于我们把gulp完全集成到我们的node模块中,就不需要再去安装gulp,gulp-cli这些模块,我们已经把这些都全包装到我们的node模块中了。

18、封装工作流 - 发布并使用模块

在完成了我们的模块后,接下来我们就把这个模块发布到npm的仓库中,然后再到一个新的项目中去使用一下我们模块提供的cli以及相关的工作流。不过在我们发布之前,可能还需要做一个小小的改动,因为我们的node模块中还存在一个小小的问题,我们在通过npm去publish的时候,会默认把项目根目录下的文件和我们在package.json中的files属性中配置的对应目录发布到npm的仓库中。而我们现在的files中只有一个lib,所以我们还需要增加一个bin目录,之后npm再去发布的时候就会把bin目录,lib目录还有下面根目录的一些文件都发布出去。

在publish我们的模块之前,先用git对代码进行一个提交,执行git add .之后执行git commit -m "feat: update package"最后执行git push去把我们的代码push到仓库里。然后我们就可以publish我们的模块了,执行yarn publish --registry https://registry.yarnpkg.com去把我们的模块publish到yarn镜像源上,yarn镜像源和npm是保持同步的。publish之后,我们就可以在我们的新项目中使用这个模块了。

我们新建一个文件夹,然后把工作流项目的public,src目录都复制到这个文件夹中,另外也复制我们的模块配置文件pages.config.js文件。之后执行yarn init --yes去初始化一个package.json文件,然后再执行yarn add <我们的模块名> --dev去安装我们的模块到开发依赖中。这里补充一个可能会出现的小问题,就是我们在之前publish上去的是到官方的镜像中,而我们可能会使用淘宝的镜像源,这就可能导致在淘宝镜像源中的还是老版本或者根本找不到。这时我们就可以到淘宝镜像源的地址(传送门),搜索我们的模块名,在点击模块页中的SYNC去手动同步一下版本。

在安装完我们的模块之后,我们就可以在项目的node_modules下的.bin目录中找到我们的<模块名>.cmd文件,里面就是执行我们把gulp包装到内部的工具。回到项目中,我们就可以执行yarn <我们的模块名> build通过yarn去运行经我们模块包装过后的build任务。并且在我们项目的package.json中的scripts中也可以去更简单地定义相关的任务,关键是我们都不需要考虑gulp和gulp-cli的安装这些。

19、FIS的基本使用

FIS是百度的前端团队推出的一款构建系统,最早只在他们团队的内部使用,后来开源后在国内也流行过一段时间,但现在用的人越来越少,并且官方也已经很久没有更新版本了。但这些并不妨碍我们去了解他,因为fis完全属于另外一种类型的构建系统,相比于gulp和grunt,FIS的核心特点是高度集成,因为他把前端日常开发过程中的常见构建任务和调试任务都集成在了内部,开发者可以通过简单配置文件的方式去配置我们构建过程需要完成的工作。也就是说我们在FIS中不需要像gulp或grunt中一样,去定义一些任务,FIS中有一些内置的任务,这些内置的任务会根据开发者的配置自动完成整个构建的过程。除此之外,FIS中还内置了一款用于调试的webServer,可以很方便地调试我们的构建结果,而像这一系列的东西,在gulp或grunt中都是需要我们通过一些插件去实现的。接下来我们就一起简单了解一下FIS这款工具的基本使用。

首先,我们可以执行yarn add fis3 --dev去把一个叫fis3的模块安装在本地的开发依赖中,当然,我们也可以执行yarn global add fis3在全局范围安装这个模块,但这里还是建议安装在本地的开发依赖中,因为当我们的项目移植到别的机器上或别人开发时就可以更容易去了解我们使用的这些工具。至于为什么是fis3而不是fis,因为fis3相对于以前的版本做了很大的变化,所以单独为这个包起了一个新的名字fis3。安装完成之后我们就多出了一个fis3的命令。

通过执行fis3 release -d output可以将项目目录下的所有文件都编译到项目根目录下的output文件夹下,但fis在这个过程中并没有对sass以及ES6语法的js文件进行相关的转换,整个过程默认只会将代码中对于资源文件的相对路径引用自动转为绝对路径,实现资源的定位。资源定位其实是fis中的核心特性,他的作用就是将我们开发阶段的路径彻底与部署之间的关系分离开,我们只需要在开发阶段使用相对路径引入资源,fis构建完之后的结果会自动将这些资源文件的引用路径转换为绝对路径。

另外,我们可以为项目添加一个fis-conf.js文件,并在该文件中用fis.match方法为我们在构建过程中匹配到的文件添加一些指定的配置。

// fis-conf.js
// 这里用fis.match匹配js、scss、png文件
fis.match('*.{js,scss,png}', {
    // 将匹配到的文件的发布结果放到'assets/$0'下,$0表示当前文件原始的目录结构,
    // 之后,我们输出的资源文件都会出现在assets这个目录下了
    release: 'assets/$0'
})

我们通过fis资源定位的能力,就能大大提高我们代码的可移植性,因为不管我们部署在哪个后端项目中,只需要知道我们需要生成的结构是什么,然后我们根据生成的结构去配置我们的fis-conf.js就行了。

20、FIS编译与压缩

除了资源定位外,fis当然还能做更多的事情,如果我们需要在构建的过程中对文件做编译的处理,同样需要通过配置文件的方式配置我们如何去处理文件的编译。比如我们尝试对项目中的sass文件做一些编译,就可以在配置文件中再添加一个配置。而对于整个配置文件的书写方式,fis官方的设计思路就是把他形成一种类似css的声明式的方式去做配置。具体的感觉就是我们通过fis.match方法的第一个参数去指定一个选择器,这个选择器选择到我们构建过程中那些文件,而后面的选项就是对于这些文件的配置。

如果我们想要对sass文件做一些单独配置的话也可以通过fis.match去选择sass文件。

// fis-conf.js
// 这里用fis.match匹配js、scss、png文件
fis.match('*.{js,scss,png}', {
    // 将匹配到的文件的发布结果放到'assets/$0'下,$0表示当前文件原始的目录结构,
    // 之后,我们输出的资源文件都会出现在assets这个目录下了
    release: 'assets/$0'
})

// 选择sass文件,'**/*.scss'表示任意目录下的sass文件
fis.match('**/*.scss', {
    // 用parser去指定对应的处理插件,这里还需要先执行`yarn add fis-parse-node-sass --dev`
    // 去安装对应的插件(注意,如果fis是全局安装的话插件也需要全局安装)
    // 通过fis.plugin方法去自动载入插件,注意插件的前缀是不需要的
    parser: fis.plugin('node-sass')
})

保存之后再执行fis3 release -d output,fis会默认将生成的结果覆盖到output目录中,输出的sass文件中就会自动去转换sass语法,只不过扩展名没有修改,而至于扩展名的修改,我们可以在配置中再添加一个属性rExt。

// fis-conf.js
fis.match('*.{js,scss,png}', {
    release: 'assets/$0'
})

fis.match('**/*.scss', {
    // rExt可以记忆成rename Extension(修改扩展名),用rExt去指定修改的扩展名。
    rExt: '.css',
    parser: fis.plugin('node-sass')
})

保存之后再执行fis3 release -d output,我们就会发现在输出的目录中就会有转换后的css文件了。而且,不光是帮我们把文件编译了,最终在我们使用这个文件的地方也会自动定位到我们编译后的结果资源,这也是fis资源定位核心能力的体现。对于sass的转换实际上添加这样一个配置就可以了,以此类推,如果我们想要转换es6代码的话,我们也可以借助这种方式去配置。

// fis-conf.js
fis.match('*.{js,scss,png}', {
    release: 'assets/$0'
})

fis.match('**/*.scss', {
    rExt: '.css',
    parser: fis.plugin('node-sass')
})

fis.match('**/*.js', {
    // 注意这里还是使用babel6.x,因为fis官方发布的插件还是基于babel6的版本,
    // 这里需要先执行`yarn add fis-parser-babel-6.x --dev`去安装这个插件到开发依赖中。
    parser: fis.plugin('babel-6.x')
})

保存之后再执行fis3 release -d output,我们就会发现在输出的output目录中就会有转换后的相关文件。除此之外如果我们还想要做一些压缩的操作的话也可以再去使用一些插件来做压缩。而对于压缩,我们要使用optimizer去指定另外的一些插件。

// fis-conf.js
fis.match('*.{js,scss,png}', {
    release: 'assets/$0'
})

fis.match('**/*.scss', {
    rExt: '.css',
    parser: fis.plugin('node-sass'),
    // 压缩的插件其实是fis中内置的,可以直接使用
    optimizer: fis.plugin('clean-css')
})

fis.match('**/*.js', {
    parser: fis.plugin('babel-6.x'),
    // 压缩的插件其实是fis中内置的,可以直接使用
    optimizer: fis.plugin('uglify-js')
})

保存之后再执行fis3 release -d output,我们就会发现在输出的output目录中就会有转换后的相关文件,并且也做了压缩操作。

以上就是我们针对fis中要想做一些编译和转换的使用方式,具体的操作一般就是通过配置文件中的声明就可以了。另外,我们可以在命令行中执行fis3 inspect去查看相关的转换过程,以便于调试我们的配置文件。

对于大家新开发的项目而言,还是建议大家使用gulp去构建,因为gulp的生态更完善,且他背后有专业的团队去维护,也就不会出现长时间不更新的问题。不过我们学习的目的也不是什么火我们学什么,我们决定学习更多是参考他能给我们带来多少思考与启发。


文章作者: 智升
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 智升 !
评论
  目录