脚手架及自动化

1.工程化概述

1.1.面临的问题

技术是为了解决问题而存在的

  • 想要使用 ES6+ 新特性,但是兼容有问题

  • 想要使用Less/Sass/PostCSS 增强CSS的编程性,但是运行环境不能直接支持

  • 想要使用模块化的方式提高项目的可维护性,但是运行环境不能直接支持

  • 部署上线前需要手动压缩代码及资源文件,部署过程中需要手动上传代码到服务器

  • 多人协作开发,无法硬性统一大家的代码风格,从仓库中pull回来的代码质量无法保证

  • 部分功能开发时需要等待后台服务接口提前完成

1.2.主要解决的问题

  • 传统语言或语法的弊端

  • 无法使用模块化/组件化

  • 重复的机械式工作

  • 代码风格统一、质量保证

  • 依赖后端服务接口支持

  • 整体依赖后端项目

1.3.工程化表现

一切提高效率、降低成本、质量保证为目的的手段都属于工程化,一切重复的工作都应该被自动化

  • 创建项目

    • 创建项目结构

    • 创建特定类型文件

  • 编码

    • 格式化代码

    • 校验代码风格

    • 编译/构建/打包

  • 预览/测试

    • Web Server/Mock

    • Live Reloading/HMR

    • Source Map

  • 提交

    • Git Hooks

    • Lint-staged

    • 持续集成

  • 部署

    • CI/CD

    • 自动发布

2.脚手架工具

2.1.脚手架工具概要

  • 脚手架的本质作用:创建项目基础结构、提供项目规范和约定

    • 相同的组织结构

    • 相同的开发范式

    • 相同的模块依赖

    • 相同的工具配置

    • 相同的基础代码

  • 常用的脚手架工具:根据信息创建对应的项目基础结构

    • React 项目:create-react-app

    • Vue 项目:vue-cli

    • Angular 项目:angular-cli

    • 通用脚手架工具:Yeoman

    • Plop:开发过程中创建特定类型的文件,例如创建一个组件/模块所需要的文件

2.2.Yeoman 基础使用

  • 官网:https://yeoman.io/

  • 安装Yeoman:yarn global add yo

  • 安装generator-node: yarn global add generator-node

  • 运行命令:yo -v

    • 如果提示找不到yo,需要运行yarn global bin查询yo的脚本地址,将地址添加到Path环境变量,并重启命令行工具
  • 切换到代码目录,运行命令:yo node

得到以下结构代码:

  • 创建特定类型文件sub Generator

    • 例如cli应用,运行:yo node:cli

    • 链接到全局:yarn link

  • 运行我们创建的cli命令

  • Yeoman 使用步骤:

    • 明确你的需求

    • 找到合适的Generator

    • 全局范围安装找到 Generator

    • 通过yo 运行对应的Generator

    • 通过命令行交互填写选项

    • 生成你所需的项目结构

2.3.自定义 Generator

创建Generator:

  • Generator本质上就是一个NPM模块

  • Generator基本结构

  • sub Generator结构:

  • 名称必须是generator-的格式

  • 步骤:

mkdir generator-sample
cd generator-sample
yarn init
yarn add yeoman-generator
mkdir generators
cd generators
mkdir app
  • 在app目录下新建index.js文件
// 此文件作为 Generator 的核心入口
// 需要导出一个继承自 Yeoman Generator的类型
// Yeoman Generator 在工作时会自动调用我们在此类型中定义的一些声明周期方法
// 我们在这个这些方法中可以通过调用父类提供的一些工具方法实现一些功能。例如文件写入

const Generator = require('yeoman-generator')
module.exports = class extends Generator {
  writing() {
    // Yeoman 自动在生成文件阶段调用此方法
    // 我们这里尝试往项目目录中写入文件
    this.fs.write(
      this.destinationPath('temp.txt'),
      Math.random()
    )
  }
}
  • link到全局:yarn link

  • 测试:

mkdir my-pro
cd my-pro
yo sample

根据模板创建文件:

  • 在app目录下新建template目录,在该目录下新建foo.txt文件
这是一个模板文件
内部可以使用 EJS 模板标记输出数据
例如:<%= title %>

其他的 EJS 语法也支持
<% if(success) { %>
哈哈哈
<% } %>
  • 修改app/index.js的writing方法:
// 通过模板方式写入文件到目标目录
// 模板文件目录
const tmpl = this.templatePath('foo.txt')
// 输出路径
const output = this.destinationPath('foo.txt')
const context = { title: 'Hello zxc~', success: false }
this.fs.copyTpl(tmpl, output, context)
  • 在my-pro项目下,运行yo sample

相对于手动创建每一个文件,模板的方式大大提高了效率

接收用户输入:

  • templates目录下增加bar.html模板
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title><%= name %></title>
</head>
<body>
  <h1><%= name %></h1>
</body>
</html>
  • app下的index.js文件增加 prompting 方法
// 此文件作为 Generator 的核心入口
// 需要导出一个继承自 Yeoman Generator的类型
// Yeoman Generator 在工作时会自动调用我们在此类型中定义的一些声明周期方法
// 我们在这个这些方法中可以通过调用父类提供的一些工具方法实现一些功能。例如文件写入

const Generator = require('yeoman-generator')
module.exports = class extends Generator {

  prompting(){
    // Yeoman 在询问用户环节会自动调用此方法
    // 在此方法中可以调用父类的prompt()方法发出对用户的命令询问
    return this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name',
        default: this.appname // appname 为项目生成目录名称
      }
    ]).then(answers => {
      // answers => { name: 'user input value' }
      this.answers = answers
    })
  }

  writing() {
    // Yeoman 自动在生成文件阶段调用此方法
    // 我们这里尝试往项目目录中写入文件
    // this.fs.write(
    //   this.destinationPath('temp.txt'),
    //   Math.random().toString()
    // )
    // 通过模板方式写入文件到目标目录
    // 模板文件目录
    // const tmpl = this.templatePath('foo.txt')
    const tmpl = this.templatePath('bar.html')
    // 输出路径
    // const output = this.destinationPath('foo.txt')
    const output = this.destinationPath('bar.html')
    // const context = { title: 'Hello zxc~', success: false }
    const context = this.answers
    this.fs.copyTpl(tmpl, output, context)
  }
}

运行yo sample

Vue Generator 案例:

  • 创建基本结构
mkdir generator-yzc-vue
cd generator-yzc-vue
yarn init
yarn add yeoman-generator
  • app下新建templates文件夹,将需要初始化的代码拷贝到该目录,将需要替换的地方使用表达式替代,例如项目名称

  • 新建入口文件/generators/app/index.js
const Generator = require('yeoman-generator')

module.exports = class extends Generator {
  prompting() {
    return this.prompt([
      {
        type: 'input',
        name: 'name',
        message: 'Your project name',
        default: this.appname
      }
    ]).then(answers => {
      this.answers = answers
    })
  }

  writing() {
    // 把每一个文件都通过模板转换到目标路径
    const templates = [
      '.browserslistrc',
      '.editorconfig',
      '.env.development',
      '.env.production',
      '.eslintrc.js',
      '.gitignore',
      'babel.config.js',
      'package.json',
      'postcss.config.js',
      'README.md',
      'public/favicon.ico',
      'public/index.html',
      'src/App.vue',
      'src/main.js',
      'src/router.js',
      'src/assets/logo.png',
      'src/components/HelloWorld.vue',
      'src/store/actions.js',
      'src/store/getters.js',
      'src/store/index.js',
      'src/store/mutations.js',
      'src/store/state.js',
      'src/utils/request.js',
      'src/views/About.vue',
      'src/views/Home.vue'
    ]
    templates.forEach(item => {
      // item => 每个文件路径
      this.fs.copyTpl(
        this.templatePath(item),
        this.destinationPath(item),
        this.answers
      )
    })
  }
}
  • 链接到全局:yarn link

  • 切换到my-pro项目,运行yo yzc-vue

? Your project name project-name

  • 结果,文件生成,而且变量也替换了

发布 Generator:

创建.gitignore文件:echo node_modules > .gitignore
git init
git add ./
git commit -m 'init'
// 创建git仓并添加
git remote add origin https://github.com/guduqiucai/generator-yzc-vue.git
git push -u origin master
// 发布,一般默认使用的npm淘宝镜像,所以这里要指定yarn镜像
yarn publish --registry=https://registry.yarnpkg.com

成功后就可以在npm中搜索到了

2.4.Plop 的基本使用

创建同类型文件,一般不单独使用。

  • 创建一个react项目:npx create-react-app my-app

  • 添加Plop包:yarn add plop --dev

  • 项目根目录下新建plopfile.js文件

// Plop 入口文件,需要导出一个函数
// 此函数接收一个Plop对象,用于创建生成器任务
module.exports = plop => {
  plop.setGenerator('component', {
    description: 'create a component',
    prompts:[
      {
        type: 'input',
        name: 'name',
        message: 'component name',
        default: 'MyComponent'
      }
    ],
    actions:[
      {
        type: 'add', // 代表添加文件
        path: 'src/components/{{name}}/{{name}}.js',
        templateFile: 'plop-templates/component.hbs'
      },
      {
        type: 'add',
        path: 'src/components/{{name}}/{{name}}.css',
        templateFile: 'plop-templates/component.css.hbs'
      },
      {
        type: 'add',
        path: 'src/components/{{name}}/{{name}}.test.js',
        templateFile: 'plop-templates/component.test.hbs'
      }
    ]
  })
}
  • 项目根目录下新建plop-templates文件夹,在该目录下新建component.hbs、component.css.hbs、component.test.hbs文件
// component.hbs
import React from 'react';

export default () => (
    <div className="{{name}}">
        <h1>{{name}} Component</h1>
    </div>
)
// component.css.hbs
.{{name}}{

}
// component.test.hbs
import React from 'react';
import ReactDOM from 'react-dom';
import {{name}} from './{{name}}';

it('renders without crashing', () => {
    const div = document.createElement('div');
    ReactDOM.render(<{{name}} />, div);
    ReactDOM.unmountComponentAtNode(div);
});
  • 运行命令:yarn plop component

2.5.脚手架的工作原理

  • 创建
mkdir sample-scaffolding
cd sample-scaffolding
yarn init
  • 修改package.json 文件,增加入口文件配置:
{
  "name": "sample-scaffolding",
  "version": "1.0.0",
  "main": "index.js",
  "bin": "cli.js",
  "license": "MIT"
}
  • 根目录下新建templates文件,并新建模板文件
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><%= name %></title>
</head>
<body>

</body>
</html>
// style.css
body{
    margin: 0;
    background-color: #f8f9fb;
}
  • 安装inquirer包:yarn add inquirer@^8.0.0这里指定版本是因为 inquirer 的新版本只能用import的方式

  • 安装ejs包:yarn add ejs模板引擎

  • 根目录下创建cli.js文件

// Node CLI应用入口文件必须有这样的文件头
// 如果是 Linux 或者 MacOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chomd 755 cli.js 实现修改

// 脚手架工作过程
// 1. 通过命令行交互询问用户问题
// 2. 根据用户回答的结果生成文件
const fs = require('fs')
const path = require('path')
const inquirer = require('inquirer')
const ejs = require('ejs')

inquirer.prompt([
  {
    type: 'input',
    name: 'name',
    message: 'Project name?'
  }
]).then(answers => {
  // console.log(answers)
  // 根据用户回答的结果生成文件
  // 模板目录
  const tmplDir = path.join(__dirname, 'templates')
  // 目标目录
  const destDir = process.cwd()
  // 将模板下面的文件全部转换到目标目录
  fs.readdir(tmplDir, (err, files) => {
    if(err) throw err
    files.forEach(file => {
      // 通过模板引擎渲染文件
      ejs.renderFile(path.join(tmplDir, file), answers, (err, result) => {
        // 将结果写入目标文件路径
        fs.writeFileSync(path.join(destDir, file), result)
      })
    })
  })
})
  • 链接到全局:yarn link

  • 执行命令:sample-scaffolding

  • 结果:

3.自动化构建

一切重复工作本应自动化,自动化构建工作流的作用就是可以脱离运行环境兼容带来的问题,开发阶段可以使用提高效率的语法、规范和标准。

3.1.Grunt

基本使用

  • 初始化
mkdir grunt-sample
cd grunt-sample
yarn init
yarn add grunt
  • 根目录下新建gruntfile.js
// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接收一个 grunt 形参,内部提供一些创建任务时可以用到的 API
module.exports = grunt => {
  grunt.registerTask('foo', () => {
    console.log('hello grunt~')
  })
  grunt.registerTask('bar', '任务描述', () => {
    console.log('other task~')
  })
  // grunt.registerTask('default', () => {
  //   console.log('default task~')
  // })
  grunt.registerTask('default', ['foo', 'bar'])
  grunt.registerTask('async-task', function () {
    const done = this.async()
    setTimeout(() => {
      console.log('async task working')
      done()
    }, 1000)
  })
}

标记任务失败:

module.exports = grunt => {
  // 返回false,标记是一个失败的任务
  grunt.registerTask('bad', () => {
    console.log('bad working')
    return false
  })
  grunt.registerTask('foo', () => {
    console.log('foo task')
  })
  grunt.registerTask('bar', () => {
    console.log('bar task')
  })
  // 异步任务标记需要失败的任务时,调用done(false)
  grunt.registerTask('bad-async', function (){
    const done = this.async()
    setTimeout(() => {
      console.log('bad async  working')
      done(false)
    })
  })
  // 失败的任务执行后,后面的任务不会再执行,除非加--force参数,强制继续执行
  grunt.registerTask('default', ['foo', 'bad', 'bar', 'bad-async'])
}

配置方法:

module.exports = grunt => {
  grunt.initConfig({
    foo: 'bar',
    yzc: {
      qq: '123'
    }
  })
  grunt.registerTask('foo', () => {
    console.log(grunt.config('foo'))
    console.log(grunt.config('yzc.qq'))
  })
}

多目标任务:

module.exports = grunt => {
  grunt.initConfig({
    'build': {
      // options为特殊键不会生成目标任务,是任务的配置选项
      options: {
        foo: 'bar'
      },
      css: '1',
      js: '2',
      ts: {
        // 可以定义自己子任务的选项配置
        options: {
          foo: 'baz'
        },
      }
    }
  })
  // 多目标模式,可以让任何任务根据配置形成多个子任务
  grunt.registerMultiTask('build', function() {
    console.log(this.options())
    console.log(`target: ${this.target}, data: ${this.data}`)
  })
}

插件的使用:

  • 安装插件:yarn add grunt-contrib-clean

  • 使用插件

module.exports = grunt => {
  grunt.initConfig({
    clean: {
      temp: 'src/*.html',
      temp: 'src/**',
      temp: 'src/*.js'
    }
  })
  grunt.loadNpmTasks('grunt-contrib-clean')
}

插件grunt-sass(编译scss为css):

  • 安装:yarn add grunt-sassyarn add saaa
const sass = require('sass')
module.exports = grunt => {
  grunt.initConfig({
    sass: {
      options: {
        implementation: sass
      },
      main: {
        files: {
          // 键是目标文件,值是需要编译的源文件
          'dist/css/main.css': 'src/scss/main.scss'
        }
      }
    }
  })
  grunt.loadNpmTasks('grunt-sass')
}

插件grunt-babel(es6新特性降级):

  • 安装:yarn add grunt-babel @babel/core @babel/preset-env

  • 安装一个多插件管理工具:yarn add load-grunt-tasks

  • 修改gruntfile.js文件

const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
  grunt.initConfig({
    sass: {
      options: {
        sourceMap:true,
        implementation: sass
      },
      main: {
        files: {
          // 键是目标文件,值是需要编译的源文件
          'dist/css/main.css': 'src/scss/main.scss'
        }
      }
    },
    babel: {
      options: {
        presets:['@babel/preset-env']
      },
      main: {
        files: {
          'dist/js/app.js': 'src/js/app.js'
        }
      }
    }
  })
  // grunt.loadNpmTasks('grunt-sass')
  loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
}
  • 结果:

插件grunt-contrib-watch(热更新):

  • 安装:yarn add grunt-contrib-watch

  • 修改gruntfile.js文件

const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
  grunt.initConfig({
    sass: {
      options: {
        sourceMap:true,
        implementation: sass
      },
      main: {
        files: {
          // 键是目标文件,值是需要编译的源文件
          'dist/css/main.css': 'src/scss/main.scss'
        }
      }
    },
    babel: {
      options: {
        presets:['@babel/preset-env'],
        sourceMap: true
      },
      main: {
        files: {
          'dist/js/app.js': 'src/js/app.js'
        }
      }
    },
    watch: {
      js: {
        files: ['src/js/*.js'],
        tasks: ['babel']
      },
      css: {
        files: ['src/scss/*.scss'],
        tasks: ['sass']
      },
    }
  })
  // grunt.loadNpmTasks('grunt-sass')
  loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务

  // 确保启动的时候先执行一次
  grunt.registerTask('default', ['sass', 'babel', 'watch'])
}
  • 运行:yarn grunt

  • 修改src下的文件,保存后可以发现dist下的文件也会改变

3.2.Gulp

基本使用:

  • 初始化项目
mkdir gulp-sample
cd gulp-sample
yarn init
yarn add gulp
  • 创建gulpfile.js文件:
// gulp 的入口文件
exports.foo = done =>{
  console.log('foo task working~')
  done()
}

exports.default = done => {
  console.log('default task working~')
  done()
}
// gulp 4.0之前的用法,不推荐
const gulp = require('gulp')
gulp.task('bar', done => {
  console.log('bar working~')
  done()
})

结果:

组合任务:

  • 代码:
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)
}
const task3 = done => {
  setTimeout(() => {
    console.log('task3 working~')
    done()
  },1000)
}
// 串行
exports.foo = series(task1, task2, task3)
exports.bar = parallel(task1, task2, task3)

  • 结果

异步任务的三种方式:

const fs = require("fs");
// 回调函数方式
exports.callback = done => {
  console.log('callback task~')
  done()
}

exports.callback_error = done => {
  console.log('callback task~')
  done(new Error('task failed'))
}

// Promise方式
exports.promise = () => {
  console.log('promise task~')
  return Promise.resolve()
}

exports.promise_error = () => {
  console.log('promise task~')
  return Promise.reject(new Error('task failed'))
}

// async-await只是promise的语法糖
const timeout = time => {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}
exports.async = async () => {
  await timeout(1000)
  console.log('async task~')
}

// 最常用的方式:stream
exports.stream = done => {
  const readStream = fs.createReadStream('package.json')
  const writeStream = fs.createWriteStream('temp.txt')
  readStream.pipe(writeStream)
// return readStream 内部实现了下面的操作,两种写法都可以
  readStream.on('end', () => {
    done()
  })
}

构建过程核心工作原理:

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

exports.default = () => {
  // 文件读取流
  const read = fs.createReadStream('normalize.css')
  // 文件写入流
  const write = fs.createWriteStream('normalize.min.css')
  // 文件转化流
  const transform = new Transform({
    transform(chunk, encoding, callback) {
      // 核心转换过程实现
      // chunk => 读取流中读取到的内容(Buffer)
      const input = chunk.toString()
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
      callback(null, output)
    }
  })
  // 把读取出来的内容 => 转换 => 写入到目标文件中
  read.pipe(transform).pipe(write)
  return read
}

文件操作 API:

  • 安装css压缩插件:yarn add gulp-clean-css

  • 安装重命名插件:yarn add gulp-rename

// gulp提供的文件操作api相对于node的api更方便更强大
const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')

exports.default = () => {
  // return src('src/normalize.css')
  //   .pipe(dest('dist'))

  // 可以使用通配符, src文件读取流,dest文件写入流
  return src('src/*.css')
    .pipe(cleanCss())
    .pipe(rename({ extname: '.min.css' }))
    .pipe(dest('dist'))
}

3.3.Gulp案例

样式编译:

  • 安装scss插件:yarn add gulp-sass sass --dev
const { src, dest } = require('gulp')
const sass = require('gulp-sass')(require('sass'));
const style = () => {
 return src('src/assets/styles/*.scss', { base: 'src' })
   .pipe(sass())
   .pipe(dest('dist'))
}

module.exports = {
 style
}

脚本编译:

  • 安装插件:yarn add gulp-babel @babel/core @babel/preset-env --dev
const babel = require('gulp-babel')
const script = () => {
 return src('src/assets/scripts/*.js', { base: 'src' })
   .pipe(babel({ presets: ['@babel/preset-env'] }))
   .pipe(dest('dist'))
}

module.exports = {
 style,
 script
}

页面模板编译:

  • 因为代码使用的swig模板,所以安装插件:yarn add gulp-swig --dev
const swig = require('gulp-swig')
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_yzc'
    },
    {
     name: 'About',
     link: 'https://weibo.com/yzcme'
    },
    {
     name: 'divider'
    },
    {
     name: 'About',
     link: 'https://github.com/yzc'
    }
   ]
  }
 ],
 pkg: require('./package.json'),
 date: new Date()
}
const page = () => {
 // 如果子目录下也有需要编译的html文件,使用/src/**/*.html
 return src('src/*.html', { base: 'src' })
   .pipe(swig({ data: data }))
   .pipe(dest('dist'))
}
// 组合这3个任务,因为没有关联关系,所以使用并行处理
const compile = parallel(style, script, page)

module.exports = {
 compile
}

图片和字体文件转换:

  • 安装图片压缩插件(新版本只能import):yarn add gulp-imagemin@7.1.0 --dev
const imagemin = require('gulp-imagemin')
// 图片
const image = () => {
 return src('src/assets/images/**', { base: 'src' })
   .pipe(imagemin())
   .pipe(dest('dist'))
}
// 字体
const font = () => {
 return src('src/assets/fonts/**', { base: 'src' })
   .pipe(imagemin())
   .pipe(dest('dist'))
}

// 组合这3个任务,因为没有关联关系,所以使用并行处理
const compile = parallel(style, script, page, image, font)

module.exports = {
 compile
}
  • 这里src下的文件就进行了编译并保存到了dist下

其他文件及文件清除:

  • 安装清除文件的插件: yarn add del@6.0.0 --dev
// 其他文件
const extra = () => {
 return src('public/**', { base: 'public' })
   .pipe(dest('dist'))
}

// 删除文件
const del = require('del')
const clean = () => {
 return del(['dist'])
}

// compile处理需要编译的文件
const compile = parallel(style, script, page, image, font)
const build = series(clean, parallel(compile, extra))

module.exports = {
 compile,
 build
}

自动加载插件:

安装插件:yarn add gulp-load-plugins --dev

const { src, dest, parallel, series } = require('gulp')
// 可以加载所有的gulp插件
const loadPlugins = require('gulp-load-plugins')
// 使用plugins.babel()
const plugins = loadPlugins()
const sass = require('gulp-sass')(require('sass'));
const style = () => {
 return src('src/assets/styles/*.scss', { base: 'src' })
   .pipe(sass())
   .pipe(dest('dist'))
}

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

// const swig = require('gulp-swig')
const data = {
 // menu省略
 pkg: require('./package.json'),
 date: new Date()
}
const page = () => {
 // 如果子目录下也有需要编译的html文件,使用/src/**/*.html
 return src('src/*.html', { base: 'src' })
   .pipe(plugins.swig({ data: data }))
   .pipe(dest('dist'))
}

// const imagemin = require('gulp-imagemin')
// 图片
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 del = require('del')
const clean = () => {
 return del(['dist'])
}

// compile处理需要编译的文件
const compile = parallel(style, script, page, image, font)
const build = series(clean, parallel(compile, extra))

module.exports = {
 compile,
 build
}

热更新开发服务器:

  • 安装插件: yarn add browser-sync --dev

  • 因为使用了bootstrap,安装:yarn add bootstrap --dev

const browserSync = require('browser-sync')
const bs = browserSync.create()
const serve = () => {
 // gulp提供的watch方法,解构出来就可以使用,第一个参数是路径,第二个参数是变化后执行的任务
 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,
  // ,这个配置也可以在每个任务执行完写入后调用.pipe(bs.reload({ stream: true }))方式代替
  // files: 'dist/**', // 监听的路径通配符
  server: {
   // 按照顺序依次查找文件
   baseDir: ['dist', 'src', 'public'],
   routes: {
    // 映射
    '/node_modules': 'node_modules',
   }
  }
 })
}

// compile处理需要编译的文件,image, font, extra开发阶段没有执行的必要,所以放在build中
const compile = parallel(style, script, page)
// 上线之前执行的任务
const build = series(clean, parallel(compile, image, font, extra))
const develop = series(compile, serve)

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

ueref 文件引用处理:

src/layouts/basic.html文件中引入的node_module下的css文件

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="author" content="zce">
  <title>{{ current | title }} · {{ pkg.name | replace('[-_]', ' ', 'g') | title }}</title>

  <link rel="canonical" href="https://zce.me">

  <!-- 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 -->

  {% block styles %}{% endblock %}
</head>

<body>
  {% include '../partials/header.html' %}

  {% block body %}{% endblock %}

  {% include '../partials/footer.html' %}

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

  {% block scripts %}{% endblock %}
</body>

</html>

注释是有开始和结束的,并且有输出路径,使用插件 gulp-useref 可以根据注释进行合并然后输出到执行路径

安装useref插件:yarn add gulp useref --dev

安装三种文件压缩插件:yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev

安装判断文件类型插件:yarn add gulp-if --dev

const useref = () => {
 return src('dist/*.html', { base: 'dist'})
   .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
   // 会有三种情况的文件:html js css 分别压缩
   .pipe(plugins.if(/\.html$/, plugins.htmlmin({
    collapseWhitespace: true,
    minifyCSS: true,
    minifyJS: true
   })))
   .pipe(plugins.if(/\.js$/, plugins.uglify()))
   .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
   // 避免文件写入冲突,加个中转站
   .pipe(dest('release'))
}

重新规划构建过程:

使用useref之前的文件都是放在dist目录的,useref输出的文件又放在了release目录,所以整个构建流程需要整理一下,增加一个temp中转目录,3个编译任务style、script、page需要输出到temp目录中,image、font、extra不会被useref影响,所以还是输出到dist,

const { src, dest, parallel, series, watch } = require('gulp')
// 可以加载所有的gulp插件
const loadPlugins = require('gulp-load-plugins')

// 删除文件
const del = require('del')
const clean = () => {
 return del(['dist', 'temp'])
}

// 使用plugins.babel()
const plugins = loadPlugins()
const sass = require('gulp-sass')(require('sass'));
const style = () => {
 return src('src/assets/styles/*.scss', { base: 'src' })
   .pipe(sass())
   .pipe(dest('temp'))
   .pipe(bs.reload({ stream: true }))
}

// const babel = require('gulp-babel')
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 }))
}

// const swig = require('gulp-swig')
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()
}
const page = () => {
 // 如果子目录下也有需要编译的html文件,使用/src/**/*.html
 return src('src/*.html', { base: 'src' })
   .pipe(plugins.swig({ data: data }))
   .pipe(dest('temp'))
   .pipe(bs.reload({ stream: true }))
}

// const imagemin = require('gulp-imagemin')
// 图片
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 browserSync = require('browser-sync')
const bs = browserSync.create()
const serve = () => {
 // gulp提供的watch方法,解构出来就可以使用,第一个参数是路径,第二个参数是变化后执行的任务
 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,
  // ,这个配置也可以在每个任务执行完写入后调用.pipe(bs.reload({ stream: true }))方式代替
  // files: 'dist/**', // 监听的路径通配符
  server: {
   // 按照顺序依次查找文件
   baseDir: ['temp', 'src', 'public'],
   routes: {
    // 映射
    '/node_modules': 'node_modules',
   }
  }
 })
}

const useref = () => {
 return src('temp/*.html', { base: 'temp'})
   .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
   // 会有三种情况的文件:html js css 分别压缩
   .pipe(plugins.if(/\.html$/, plugins.htmlmin({
    collapseWhitespace: true,
    minifyCSS: true,
    minifyJS: true
   })))
   .pipe(plugins.if(/\.js$/, plugins.uglify()))
   .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
   // 避免文件写入冲突,加个中转站
   .pipe(dest('dist'))
}

// compile处理需要编译的文件,image, font, extra开发阶段没有执行的必要,所以放在build中
const compile = parallel(style, script, page)
// 上线之前执行的任务
const build = series(clean, parallel(series(compile, useref), image, font, extra))
const develop = series(compile, serve)

module.exports = {
 build,
 develop,
 clean
}

package.json文件中增加scripts

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

3.4.封装工作流

准备:

- github新建仓库:[__https://github.com/guduqiucai/yzc-pages.git__](https://github.com/guduqiucai/yzc-pages.git)

- 初始化项目:`yarn create @zce/nm yzc-pages`这个项目作为我们要封装的cli工具项目,然后在上个yzc-gulp-demo项目中使用这个工具。

- 提交到GitHub:
git init
git add ./
git commit -m 'init'
// 创建git仓并添加
git remote add origin https://github.com/guduqiucai/yzc-pages.git
git push -u origin master
// 发布,一般默认使用的npm淘宝镜像,所以这里要指定yarn镜像
yarn publish --registry=https://registry.yarnpkg.com

提取 gulpfile:

  • 将yzc-gulp-demo项目的gulpfile.js文件的内容先复制到yzc-pages项目中lib目录下的index.js文件,作为入口文件,删除gulpfile.js文件内容。

  • 将yzc-gulp-demo项目的开发依赖项package.json中的也复制到当前项目中并安装,删除yzc-gulp-demo项目package.json中的devDependencies部分。

  • 将yzc-gulp-demo项目的node_modules的文件删除,然后再执行yarn安装剩余的生产依赖

  • 在yzc-pages项目控制台运行 yarn link 到全局

  • 在yzc-gulp-demo项目控制台运行 yarn link yzc-pages 链接到yzc-pages项目

  • 然后就会看到yzc-gulp-demo项目下的node_modules下多了一个yzc-pages文件夹,并且有个图标表示是软链接。

  • 然后我们就可以在yzc-gulp-demo项目的gulpfile.js中导入node_modules/yzc-pages/lib/index.js

  • 因为现在只是软链接,所以需要安装gulp和gulp-cli插件,发布后就不会存在了,因为发布后再安装yzc-pages,就会将依赖项全部安装到项目的node_modules目录下

  • 运行yarn build,会提示找不到package.json文件,因为之前我们在gulpfile.js中模拟配置数据data的时候,读取了package.json文件,所以会报错。

  • 使用约定大于配置的方法,我们约定项目根目录下必须有一个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: '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()
  }
}
  • yzc-pages项目中lib目录下的index.js文件中的就可以引入这个文件
// yzc-pages\lib\index.js
const cwd = process.cwd()
let config = {
  // default config
}
try {
  const loadConfig = require(`${cwd}/pages.config.js`)
  config = Object.assign({}, config, loadConfig)
} catch (e) {}

const page = () => {
  // 如果子目录下也有需要编译的html文件,使用/src/**/*.html
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data: config.data }))
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}
// 这里'@babel/preset-env'需要使用require的方式引入,否则会提示找不到
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: [require('@babel/preset-env')] }))
    .pipe(dest('temp'))
    .pipe(bs.reload({ stream: true }))
}

  • 这时在yzc-gulp-demo项目下运行yarn build可以看到运行成功

抽象路径配置:

修改yzc-pages项目中lib目录下的index.js文件,将写死的路径都提取为配置项

const { src, dest, parallel, series, watch } = require('gulp')
// 可以加载所有的gulp插件
const loadPlugins = require('gulp-load-plugins')

const cwd = process.cwd()
let config = {
  // default config
  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 {
  const loadConfig = require(`${cwd}/pages.config.js`)
  config = Object.assign({}, config, loadConfig)
} catch (e) {}

// 删除文件
const del = require('del')
const clean = () => {
  return del([config.build.dist, config.build.temp])
}

// 使用plugins.babel()
const plugins = loadPlugins()
const sass = require('gulp-sass')(require('sass'))
const style = () => {
  return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src })
    .pipe(sass())
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true }))
}

// const babel = require('gulp-babel')
const script = () => {
  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 swig = require('gulp-swig')
const page = () => {
  // 如果子目录下也有需要编译的html文件,使用/src/**/*.html
  return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.swig({ data: config.data }))
    .pipe(dest(config.build.temp))
    .pipe(bs.reload({ stream: true }))
}

// const imagemin = require('gulp-imagemin')
// 图片
const image = () => {
  return src(config.build.paths.images, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}
// 字体
const font = () => {
  return src(config.build.paths.fonts, { base: config.build.src, cwd: config.build.src })
    .pipe(plugins.imagemin())
    .pipe(dest(config.build.dist))
}

// 其他文件
const extra = () => {
  return src('**', { base: config.build.public, cwd: config.build.public })
    .pipe(dest(config.build.dist))
}

const browserSync = require('browser-sync')
const bs = browserSync.create()
const serve = () => {
  // gulp提供的watch方法,解构出来就可以使用,第一个参数是路径,第二个参数是变化后执行的任务
  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('src/assets/images/**', image)
  // watch('src/assets/fonts/**', font)
  // watch('public/**', extra)
  // 优化一下,下面三个路径中的文件变化,引起浏览器重新请求资源
  watch([
    config.build.paths.images,
    config.build.paths.fonts
  ], { cwd: config.build.src }, bs.reload)
  watch('**', { cwd: config.build.public }, bs.reload)

  bs.init({
    notify: false,
    port: 2080,
    open: false,
    // ,这个配置也可以在每个任务执行完写入后调用.pipe(bs.reload({ stream: true }))方式代替
    // files: 'dist/**', // 监听的路径通配符
    server: {
      // 按照顺序依次查找文件
      baseDir: [config.build.temp, config.build.dist, config.build.public],
      routes: {
        // 映射
        '/node_modules': 'node_modules'
      }
    }
  })
}

const useref = () => {
  return src(config.build.paths.pages, { base: config.build.temp, cwd: config.build.temp })
    .pipe(plugins.useref({ searchPath: [config.build.temp, '.'] }))
    // 会有三种情况的文件:html js css 分别压缩
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    // 避免文件写入冲突,加个中转站
    .pipe(dest(config.build.dist))
}

// compile处理需要编译的文件,image, font, extra开发阶段没有执行的必要,所以放在build中
const compile = parallel(style, script, page)
// 上线之前执行的任务
const build = series(clean, parallel(series(compile, useref), image, font, extra))
const develop = series(compile, serve)

module.exports = {
  build,
  develop,
  clean
}
  • 这时就可以在yzc-gulp-demo项目的pages.config.js中修改这些配置了,运行yarn build也是可以正常打包的
module.exports = {
  build: {
    src: 'src',
    dist: 'dist',
    temp: 'temp1',
    public: 'public',
    paths: {
      styles: 'assets/styles/*.scss',
      scripts: 'assets/scripts/*.js',
      pages: '*.html',
      images: 'assets/images/**',
      fonts: 'assets/fonts/**',
    },
    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()
  }
}

包装 Gulp CLI:

  • 我们会发现yzc-gulp-demo项目的gulpfile.js这个文件只是加载了我们工具下的lib/index.js,感觉没有存在的必要,删除gulpfile.js文件,运行yarn build --gulpfile ./node_modules/yzc-pages/lib/index.js --cwd .也是可以正常运行的,但是需要传参数,看起来就不简洁了

  • 这时可以在工具yzc-pages项目package.json文件中增加bin的配置"bin": "bin/yzc-pages.js",根目录下新建一个bin目录,在该目录下新建一个yzc-pages.js文件,作为cli的执行入口

#!/usr/bin/env node
process.argv.push('--cwd')
process.argv.push(process.cwd())
process.argv.push('--gulpfile')
process.argv.push(require.resolve('..'))
// process.argv 可以拿到执行命令的参数,也可以push参数
console.log(process.argv)

require('gulp/bin/gulp')
  • 切换到使用项目yzc-gulp-demo中,运行yzc-pages build也可以成功运行

发布并使用模块:

  • 因为发布的时候默认上传的是package.json中files配置项的目录,以及根目录下的文件,所以我们需要修改package.json文件,把刚才新加的bin目录也配置进去"files": ["lib", "bin"]

  • 提交代码,然后发布 yarn publish

  • 新建一个yzc-pages-demo的项目,将yzc-gulp-demo项目的src、public目录下的文件以及pages.config.js配置文件拷贝过来

  • 安装我们发布的插件:yarn add yzc-pages --dev

  • 运行:yzc-pages build可以打包成功

  • yzc-pages项目的地址:https://github.com/guduqiucai/yzc-pages

4.总结

  • Yeoman存在的意义:

    • 基于Yeoman实现自己项目的Generator,模板文件中保存自己项目的基本文件以及一些通用方法的文件,例如可以将vue-cli生成的文件,增加自己项目的通用文件后,生成一个Generator,以便后续其他项目初始化时使用。
  • gulp使用场景:

    • 传统静态页面开发(多页应用),注重的是页面的结构和样式

    • 小程序项目中使用Less/Sass

    • 日常综合事务,文件重命名/前后缀

posted @ 2023-01-16 09:59  菜菜123521  阅读(55)  评论(0编辑  收藏  举报