文章内容输出来源:拉勾大前端高薪训练营
开发脚手架及封装自动化构建工作流
工程化概述
工程化的定义和主要解决的问题
-
全副武装:通过工程化提升战斗力。
-
问题1: 想要使用ES6+新特性,但是兼容有问题。
-
问题2: 想要使用Less/Sass/PostCSS增强CSS的编程性但是运行环境不能直接支持。
-
问题3: 想要使用模块化的方式提高项目的可维护性但运行环境不能直接支持。
-
问题4: 部署上线前需要手动压缩代码和资源文件。
-
问题5: 部署过程需要手动上传代码到服务器。
-
问题6: 多人协作开发时,无法硬性统一大家的代码风格。
-
问题7: 从仓库中pull回来的代码质量无法保证。
-
问题8: 部分功能开发时需要等待后端服务接口提前完成。
-
问题归类
- 传统语言或语法的弊端。
- 无法使用模块化/组件化。
- 重复的机械式工作。
- 代码风格统一、质量保证。
- 依赖后端服务接口支持。
- 整体依赖后端项目。
一个项目过程中工程化的表现。
-
一切以降本增效、质量保证为目的的手段都属于☛工程化☚
-
一切重复的工作都应该被自动化。
-
创建项目
- 创建项目结构。
- 创建特定类型文件。
-
编码
- 格式化代码
- 检验代码风格
- 编译/构建/打包
-
预览/测试
- Web Server/Mock
- Live Reloading/HMR
- Source Map
-
提交
- Git Hooks
- Lint-staged
- 持续集成
-
部署
- CI/CD
- 自动发布
-
工程化 ≠ 某个工具
- 工具不是工程化的核心
- 工程化的核心是对项目整体的规划或者说架构
- 工具只是落地规划或架构过程的一种手段。
工程化与Node.js
-
工程化 Powered by Node.js
-
Node.js让前端有了一个新舞台。
-
前端工程化由Node.js强烈驱动的。
-
落实工程化
- 脚手架工具开发
- 自动化构建系统
- 模块化打包
- 项目代码规范化
- 自动化部署
脚手架工具
脚手架的作用
-
前端工程化的发起者
-
用于创建项目基础结构
-
提供项目规范和约定
- 相同的文件组织结构
- 相同的开发范式
- 相同的模块依赖
- 相同的工具配置
- 相同的基础代码
-
可以快速搭建特定类型的项目骨架
-
示例:IDE创建项目的过程就是一个脚手架的工作流程。
-
前端脚手架都是以一个独立的工具存在。
-
目标: 都是为了解决我们在创建项目过程当中那些复杂的工作。
常用的脚手架工具
-
创建项目时才用到的脚手架工具
-
服务于特定项目类型的脚手架工具
- React项目↝create-react-app
- Vue.js项目↝vue-cli
- Agular项目↝angular-cli
- 共同点:根据你提供的信息生成对应的项目基础结构。
- 不同处:一般只适用于自身所服务那个框架的项目。
-
通用型项目脚手架工具
- 以Yeomen为代表。
- 可以根据一套模板生成一个对应额额项目结构。
- 很灵活
- 很容易扩展
-
-
另一类脚手架
- 用于在项目开发过程中去创建一些特定类型的文件
- 以Plop为代表
- 示例:创建一个组件/模块所需要的文件。
-
重点关注几个有代表性的工具
- Yeoman
- Plop
-
脚手架的工作原理
- 启动过后,会询问一些预设的问题,然后将回答的结果结合一些模板文件来生成项目结构。
- 脚手架实质上就是一个node cli 应用。
通用脚手架工具剖析
-
Yeoman
-
The web’s scaffolding tool for modern webapps
-
作为最老牌最强大最通用的一款工具,有很多值得我们借鉴的地方。
-
更像一款脚手架运行平台
-
官方定义: 用于创建现代web应用的脚手架工具。
-
可以搭配Generator去创建任何类型的项目。
-
缺点:在很多专注基于框架的人眼中Yeoman过于通用,不够专注。他们更愿意使用像vue-cli等工具。
-
基本使用
-
依赖Node环境
-
yarn global add yo
-
配合Generator: yarn global add generator-node
-
新项目使用: yo node
-
总结
-
在全局范围安装yo
- npm install yo --global
- yarn global add yo
-
安装对应的generator
- npm install generator-node --global
- yarn global add generator-node
-
通过yo运行generator
- cd path/to/project-dir
- mkdir my-module
- yo node
-
-
-
Sub Generator
- 子级生成器
- 使用: yo node:cli
-
常规使用步骤
- 1,明确你的需求
- 2,找到合适的Generator
- 3,全局范围安装找到的Generator
- 4,通过yo运行对应的Generator
- 5,通过命令行交互填写选项
- 6,生成你所需要的项目结构
- 示例: web应用生成器
yarn global add generator-webapp
yo webapp
-
自定义Generator
-
基于Yeoman搭建自己的脚手架。
-
创建Generator模块
-
Generator本质上就是一个NPM模块。
-
Generator基本结构
- generator/app/index.js
- generator/component/index.js
- package.json
- 命名: generator-
-
案例演示
-
mkdir generator-sample
-
cd generator-sample
-
yarn init
- 初始化package.json文件
-
yarn add yeoman-generator
-
VSCode打开: code .
-
创建文件: generator/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().toString())
}
} -
yarn link
- cd …
- mkdir my-proj
- cd my-proj
- yo sample
-
-
根据模板创建文件
-
创建模板文件: app/templates/foo.txt
-
模板遵循ejs模板语法
- <%= name %>
- <% if(success) { %>
生成文件内容
<% } %>
-
-
通过模板方式写入文件到目标
-
模板文件路径获取: const temp1=this.templatePath(‘foo.txt’)
-
输出目标路径: const output= this.destinationPath(‘foo’)
-
模板数据上下文: const context = { name: ‘sjz’ }
-
this.fs.copyTpl(temp1, output, context)
-
yo sample
-
相对于手动创建每一个文件,模板的方式大大提高了效率。
-
-
接收用户输入数据
- module.exports = class extends Generator {
prompting () {
// Yeoman 在询问用户环节会自动调用此方法。
// 在此方法中可以调用父类的prompt() 方法发出对用户的命令行询问。
return this.prompt([
{
type:‘input’,
name: ‘name’,
message: ‘sjz’,
default: this.appname
}
])
.then(answers ⥤ {
console.log(answers)
this.answers = answers
})
}
}
- module.exports = class extends Generator {
-
Vue Generator 案例
-
mkdir generator-sjz-vue
-
cd generator-sjz-vue
-
yarn init
-
yarn add yeoman-generator
-
code .
-
创建主入口文件: generator/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 = []
templates.forEach(item ⥤ {
// item ⥤每个文件路径
this.fs.copyTpl(
this.templatePath(item),
this.destinationPath(item),
this.answers
)
})
}
} -
创建templates目录,并把准备好的文件及目录结构拷贝到templates目录中。
-
yarn link
- cd …
- mkdir my-proj
- cd my-proj
- yo sjz-vue
-
-
发布Generator
-
将项目源代码托管到公开的源代码仓库上面。
-
先创建本地仓库
-
echo node-modules >.gitignore
-
git init
- 初始化本地空仓库
-
git status
-
git add .
-
git commit -m “feat: initial commit”
-
创建远端仓库
-
git remote add origin 远端仓库地址
-
git push -u origin master
-
npm publish/yarn publish
- yarn publish --registry=https://registry.yarnpkg.com
-
问题:淘宝镜像是只读镜像
-
-
-
-
-
-
-
-
Plop
-
一个小而美的脚手架工具
-
是一款主要用于创建项目中特定类型文件的小工具,有些类似Yeoman中的子生成器Sub Generator。
-
一般不会独立使用
-
一般集成到项目中用来自动化创建同类型的文件
-
使用场景: 需要重复创建相同类型的文件,例如react中创建组件都要重复创建3个文件(css/js/html)
-
Plop的具体使用
-
1,yarn add plop --dev
-
2,新建plopfile.js文件
-
plop工作的入口文件
-
需要导出一个函数
-
函数中接收一个plop的对象参数
-
参数plop对象中提供了一系列的工具函数
-
这些工具函数用于创建生成器任务。
-
上代码
- module.exports = plop ⥤ {
plop.setGenerator(‘generatorName’, {
description: ‘生成器描述’,
prompts: [
{
type: ‘input’,
name: ‘name’,
message: ‘屏幕提示’,
default: ‘默认答案’,
}
],
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’,
templateFile: ‘plop-templates/component.test.hbs’,
},
]
})
}
- module.exports = plop ⥤ {
-
运行
- yarn plop generatorName
-
-
总结
- 1,将plop模块作为项目开发依赖安装
- 2,在项目根目录下创建一个plopfile.js文件
- 3,在plopfile.js文件中定义脚手架任务
- 4,编写用于生成特定类型文件的模板
- 5,通过plop提供的CLI运行脚手架任务
-
-
-
开发一款脚手架
-
mkdir sample-scaffolding
-
cd sample-scaffolding
-
yarn init
- 初始化package.json文件。
-
VsCode打开: code .
-
在package.json中添加bin属性
- 值为’cli.js’
-
根目录新建文件: cli.js
-
此文件必须要有特定文件头: #!/usr/bin/env node
-
Linux/macOS系统下
- 还需要修改此文件的读写权限为 755
- 具体就是通过 chmod 755 cli.js 实现修改
-
-
内容:console.log(‘sjz’)
-
-
yarn link
- 将脚手架link到全局
-
然后就可以在命令行执行: sample-scaffolding
- 运行结果: sjz
-
实现脚手架的具体过程
-
1,通过命令行交互询问用户问题
-
安装询问模块inquirer
- yarn add inquirer
-
-
2,根据用户回答的结果生成文件
-
cli.js中代码实现
-
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 ⥤ {
// 根据用户回答的结果生成文件
// 模板目录
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) ⥤ {
if(err) throw err
fs.writeFileSync(path.join(destDir, file), result)
})
})
}
})
-
-
创建模板文件
- templates/index.html
- templates/style.css
-
安装模板引擎
- yarn add ejs
-
自动化构建
简介
-
一切重复工作本应自动化
-
名字解释
-
自动化
- 机器代替手工完成一些工作
-
构建
- 可以理解为转换
- 把一个东西转换为另一些东西。
-
-
就是把源代码自动化构建或转换为生产代码
- 这个转换或构建过程称为自动化构建工作流。
-
作用
-
可以脱离运行环境兼容带来的问题
-
允许使用提高效率的语法、规范和标准
-
ECMAScript Next
-
Sass
-
模板引擎
- 抽象源码文件中重复的代码
-
以上这些用法大都不被浏览器直接支持
-
可以使用自动化构建工具构建转换那些不被支持的☞特性。
-
-
可以提高开发效率
-
自动化构建初体验
-
新建sass文件: scss/main.scss
-
内容
- $body-bg: #f8f9fb
$body-color: #333
- $body-bg: #f8f9fb
-
body {
margin: 0 auto;
padding: 20px;
max-width: 800px;
background-color: $body-bg;
color: $body-color;
}
-
yarn add sass --dev
-
运行:
./node_modules/.bin/sass scss/main.scss css/style.css- style.css
- style.css.map
-
NPM Scripts
-
包装构建命令
-
package.json中增加scripts配置
-
“scripts”: {
“build”: “sass scss/main.scss css/style.css --watch”,
// “preserve”: “yarn build”, // NPM Scripts 的钩子机制
“serve”: “browser-sync . --files “css/*.css””,
“start”: “run-p build serve”,
}- yarn build
- yarn serve
- yarn start
-
-
是实现自动化构建工作流的最简方式
-
yarn add browser-sync --dev
- 用于启动一个测试服务器来运行项目。
-
yarn add npm-run-all --dev
- 同时运行多个任务
-
面对相对复杂的构建过程,就显得有些吃力了。
-
常用的自动化构建工具
Grunt
- 简介
- 最早的前端构建系统
- 插件生态非常完善
- 其插件几乎可以帮你自动化完成任何你想完成的事情。
- 缺点
- 工作过程是基于临时文件去实现的,所以说构建速度相应较慢。
- 例如去完成sass文件的构建
- 先对sass文件做编译操作
- 再自动添加一些私有属性的前缀。
- 之后再压缩代码
- 在这些过程中Grunt每一步都有磁盘操作。
- 基本使用
- 初始化package.json文件
- yarn init --yes
- 添加grunt模块
- yarn add grunt
- 添加gruntfile.js的文件
- code gruntfile.js
- Grunt的入口文件
- 用于定义一些需要 Grunt 自动执行的任务
- 需要导出一个函数
- 此函数接收一个 grunt 的形参
- grunt是一个对象,内部提供一些创建任务时用到的 API
- 上代码
- module.exports = grunt ⥤ {
grunt.registerTask('taskName', () ⥤ {
console.log('sjz')
})
grunt.registerTask('taskName1', '任务描述', () ⥤ {
console.log('sjz515')
})
// grunt.registerTask('default', '默认任务描述', () ⥤ {
// console.log('sjz515')
// })
grunt.registerTask('default', ['taskName', 'taskName1'])
grunt.registerTask('async-task', () ⥤ {
setTimeout(()⥤{
console.log('sjz515')
}, 1000)
})
grunt.registerTask('async-task1', function () {
const done = this.async()
setTimeout(()⥤{
console.log('sjz515')
done()
}, 1000)
})
}
- yarn grunt taskName
- yarn grunt taskName1
-
yarn grunt
- 默认任务不需要指定
- yarn grunt async-task
- grunt默认支持同步模式
- 异步任务中console.log未执行。
- yarn grunt async-task1
- 可以成功执行
- 标记任务失败
- module.exports = grunt ⥤ {
grunt.registerTask('bad', () ⥤ {
console.log('sjz')
return false
})
grunt.registerTask('async-task1', function () {
const done = this.async()
setTimeout(()⥤{
console.log('sjz515')
done(false)
}, 1000)
})
}
- 一旦任务失败,后续任务将不会执行
- 可以通过指定--force参数控制即使某个任务执行失败,后续任务依然会保证执行
- Grunt 配置选项方法
- module.exports = grunt ⥤ {
grunt.initConfig({
taskName: 'sjz',
foo: {
bar: 'sjz'
}
})
grunt.registerTask('taskName', () ⥤ {
console.log(grunt.config('taskName'))
})
grunt.registerTask('foo', () ⥤ {
console.log(grunt.config('foo.bar'))
})
}
- yarn grunt taskName
- yarn grunt foo
- Grunt多目标任务
- 多目标模式,可以任务根据配置形成多个子任务。
- module.exports = grunt ⥤ {
grunt.initConfig({
build: {
options: {
foo: 'bar'
},
css: {
options: {
foo: 'baz'
}
},
js: '2'
}
})
grunt.registerMultiTask('build', function() {
console.log('multi task~')
console.log(`target: ${this.target}, data:${this.data}, options: ${this.options()}`)
})
}
- yarn grunt build
- yarn grunt build:css
- Grunt插件的使用
- module.exports = grunt ⥤ {
grunt.initConfig({
clean: {
temp: 'temp/*.js',
temp1: 'temp/**',
}
})
grunt.loadNpmTasks('grunt-contrib-clean')
}
- yarn grunt clean
- yarn add grunt-contrib-clean
- 实现常用的构建任务
- yarn add grunt-sass sass --dev
- const sass = require('sass')
const loadGruntTask = 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: {
sourceMap: true,
presets: [’@babel/preset-env’]
},
main: {
files: {
‘dist/js/app.js’: ‘src/js/app.js’
}
}
},
watch: {
js: {
files: [’'src/is/.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 sass
- yarn grunt babel
- yarn grunt watch
- yarn grunt
- yarn add grunt-babel @babel/core @babel/preset-env --dev
- yarn add load-grunt-tasks --dev
- yarn add grunt-contrib-watch --dev
- 基本已经退出历史舞台
Gulp
- 简介
- 推荐使用
- 很好的解决了Grunt构建慢的问题
- 每个环节都是在内存中完成。
- 默认支持同时执行多个任务。
- 使用方式相对于Grunt更加直观易懂。
- 插件生态也非常完善。
- 后来居上
- 目前市面上最流行的前端构建系统
- 基本使用
- 核心特点: 高效、易用
- yarn init --yes
- 初始化package.json文件
- 安装Gulp模块
- yarn add gulp --dev
- 会同时安装gulp-cli模块
- 创建gulpfile.js文件
- code gulpfile.js
- gulp的入口文件
- 通过导出成员函数的方式定义任务
- 在最新的Gulp中去掉了同步代码模式,约定每个任务都必须是异步任务。
- 上代码
- gulpfile.js
- exports.foo = done ⥤ {
console.log('sjz')
done() // 标识任务完成
}
- yarn gulp foo
- exports.default = done ⥤ {
console.log('default sjz')
done() // 标识任务完成
}
- yarn gulp
- gulp4.0以前
- const gulp = require('gulp')
- gulp.task('bar', done ⥤{
console.log(‘bar’)
done()
})
- yarn gulp bar
- Gulp创建组合任务
- const {series, parallel} = require ('gulp')
exports.task1 = done ⥤ {
setTimeout(()⥤{
console.log(‘sjz1’)
done() // 标识任务完成
}, 1000)
exports.task2 = done ⥤ {
setTimeout(()⥤{
console.log(‘sjz2’)
done() // 标识任务完成
}, 1000)
exports.task3 = done ⥤ {
setTimeout(()⥤{
console.log(‘sjz3’)
done() // 标识任务完成
}, 1000)
}
exports.foo = series(task1, task2, task3)
exports.bar = parallel(task1, task2, task3)
- yarn gulp foo
- yarn gulp bar
- Gulp异步任务的三种方式
- exports.callback = done ⥤ {
console.log('sjz callback')
done() // 标识任务完成
}
exports.callback_error = done ⥤ {
console.log(‘sjz callback_error’)
done(new Error(‘task failed’)) // 标识任务完成
}
- yarn gulp callback
- yarn gulp callback_error
- exports.promise = done ⥤ {
console.log('sjz promise')
return Promise.resolve()
}
exports.promise_error = done ⥤ {
console.log(‘sjz Promise error’)
return Promise.reject(new Error(‘task failed’))
}
- yarn gulp promise
- yarn gulp promise_error
- const timeout = time ⥤ {
return new Promise(resolve ⥤ {
setTimeout(resolve, time)
})
}
exports.async = async () ⥤ {
console.log(‘async task’)
await timeout(1000)
}
- yarn gulp async
- const fs = require('fs')
exports.stream = () ⥤ {
console.log(‘sjz stream’)
const readStream = fs.createReadStream(‘package.json’)
const writeStream = fs.createReadStream(‘package.json’)
return
}
exports.stream1 = done ⥤ {
console.log(‘sjz stream1’)
const readStream = fs.createReadStream(‘package.json’)
const writeStream = fs.createWriteStream(‘temp.txt’)
readStream.pipe(writeStream)
readStream.on(‘end’, () ⥤ {
done()
})
}
- yarn gulp stream
- Gulp构建过程核心工作原理
- const fs = require('fs')
const {} = 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()
input.replace(/\s+/g, ‘’).replace(//*.+?*//g, ‘’)
callback(null, input)
}
})
// 把读取出来的文件流导入写入文件流
read
.pipe(transform) // 转换
.pipe(write) // 写入
return read
}
- yarn gulp
- the streaming build system
- Gulp希望实现构建管道的概念
- Gulp文件操作API + 插件的使用
- const { src, dest } = require('gulp')
const cleanCss = require(‘gulp-clean-css’)
const rename = require(‘gulp-rename’)
exports.default = () ⥤{
return src(‘src/*.css’)
.pipe(cleanCss())
.pipe(rename({ extname: ‘.min.css’ }))
.pipe(dest(‘dist’))
}
- yarn gulp
- yarn add gulp-clean-css --dev
- yarn add gulp-rename --dev
- Gulp自动化构建案例
- 准备需要构建工作流的网页应用案例
- git clone https://github.com/zce/zce-gulp-demo
- 用vscode打开目录
- code zce-gulp-demo/
- 网页应用案例目录剖析
- public目录
- 存放那些不需要被加工且会被直接拷贝到生成文件夹的文件
- src目录
- 存放开发阶段所编写代码的
- 此文件夹目录下所有文件都将被构建。
- 此目录下文件都会被转换生成到生成文件夹中
- yarn add gulp --dev
- 新建gulpfile.js文件作为入口文件
- 样式编译任务
- const { src, dest } = require('gulp')
const sass = require(‘gulp-sass’)
exports.style =() ⥤ {
return src(‘src/assets/styles/*.scss’, { base: ‘src’ } )
.pipe(sass({ outputStyle: ‘expanded’ }))
.pipe(dest(‘dist’))
}
module.exports = {
style
}
- yarn gulp style
- yarn add gulp-sass --dev
- 脚本文件编译任务
- const { src, dest } = require('gulp')
const babel = require(‘gulp-babel’)
exports.script = () ⥤ {
return src(‘src/assets/scripts/*.js’, { base: ‘src’ } )
.pipe(babel({ presets: [’@babel/preset-env’] }))
.pipe(dest(‘dist’))
}
module.exports = {
script
}
- yarn gulp script
- yarn add gulp-babel --dev
- yarn add @babel/core @babel/preset-env --dev
- 页面文件编译任务
- const { src, dest } = require('gulp')
const swig = require(‘gulp-swig’)
const data = {
menus: [],
pkg: require(‘package.json’),
date: new Date()
}
exports.page = () ⥤ {
return src(‘src/*.html’, { base: ‘src’ } )
.pipe(swig({ data }))
.pipe(dest(‘dist’))
}
module.exports = {
page
}
- yarn gulp page
- 安装模板引擎
- yarn add gulp-swig --dev
- 图片和字体文件的转换
- const { src, dest } = require('gulp')
const imagemin = require(‘gulp-imagemin’)
exports.image = () ⥤ {
return src(‘src/assets/images/’, { base: ‘src’ } )
.pipe(imagemin())
.pipe(dest(‘dist’))
}
exports.font = () ⥤ {
return src('src/assets/fonts/’, { base: ‘src’ } )
.pipe(imagemin())
.pipe(dest(‘dist’))
}
module.exports = {
image,
font
}
- yarn gulp image
- yarn gulp font
- yarn add gulp-imagemin --dev
- 其他文件及文件清除
- const { src, dest } = require('gulp')
exports.extra = () ⥤ {
return src(‘public/**’, { base: ‘public’ } )
.pipe(dest(‘dist’))
}
module.exports = {
extra
}
- 自动加载插件
- yarn add gulp-load-plugins --dev
- const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
- 替换
- 样式编译任务
- const { src, dest } = require('gulp')
exports.style =() ⥤ {
return src(‘src/assets/styles/*.scss’, { base: ‘src’ } )
.pipe(plugins.sass({ outputStyle: ‘expanded’ }))
.pipe(dest(‘dist’))
}
module.exports = {
style
}
- yarn gulp style
- yarn add gulp-sass --dev
- 脚本文件编译任务
- const { src, dest } = require('gulp')
exports.script = () ⥤ {
return src(‘src/assets/scripts/*.js’, { base: ‘src’ } )
.pipe(plugins.babel({ presets: [’@babel/preset-env’] }))
.pipe(dest(‘dist’))
}
module.exports = {
script
}
- yarn gulp script
- yarn add gulp-babel --dev
- yarn add @babel/core @babel/preset-env --dev
- 页面文件编译任务
- const { src, dest } = require('gulp')
const data = {
menus: [],
pkg: require(‘package.json’),
date: new Date()
}
exports.page = () ⥤ {
return src(‘src/*.html’, { base: ‘src’ } )
.pipe(plugins.swig({ data }))
.pipe(dest(‘dist’))
}
module.exports = {
page
}
- yarn gulp page
- 安装模板引擎
- yarn add gulp-swig --dev
- 图片和字体文件的转换
- const { src, dest } = require('gulp')
exports.image = () ⥤ {
return src(‘src/assets/images/’, { base: ‘src’ } )
.pipe(plugins.imagemin())
.pipe(dest(‘dist’))
}
exports.font = () ⥤ {
return src('src/assets/fonts/’, { base: ‘src’ } )
.pipe(plugins.imagemin())
.pipe(dest(‘dist’))
}
module.exports = {
image,
font
}
- yarn gulp image
- yarn gulp font
- yarn add gulp-imagemin --dev
- 热更新开发服务器
- yarn add browser-sync --dev
- const browserSync = require('browser-sync')
const bs = browserSync.create()
exports.serve = () ⥤ {
bs.init({
notify: false,
port: 2080,
files: ‘dist/**’,
open: true,
server: {
baseDir: ‘dist’,
routes: {
‘/node_modules’: ‘node_modules’
}
}
})
}
module.exports = {
serve
}
- yarn gulp serve
- 监视变化以及构建过程变化
- const { src, dest, parallel, series, watch } = require('gulp')
const browserSync = require(‘browser-sync’)
const bs = browserSync.create()
exports.serve = () ⥤ {
watch(‘src/assets/styles/.scss’, style)
watch('src/assets/scripts/.js’, script)
watch(‘src/*.html’, page)
// 以下监听开发时无意义,需注释掉
// watch('src/assets/images/**', image)
// watch('src/assets/fonts/**', font)
// watch('public/**', extra)
watch([
'src/assets/images/**',
'src/assets/fonts/**',
'public/**'
], bs.reload)
bs.init({
notify: false,
port: 2080,
files: 'dist/**',
open: true,
server: {
baseDir: ['dist', 'src', 'public'],
routes: {
'/node_modules': 'node_modules'
}
}
})
}
module.exports = {
clean,
serve
}
- yarn gulp clean
- yarn gulp serve
- 问题
- 这里可能会因为swig模板引擎缓存的机制导致页面不会变化,此时需要额外将swig选项中的cache设置为false, 具体参考源代码72行
- 组合任务
- 编译任务
- const { src, dest, parallel } = require('gulp')
const compile = parallel(style, script, page, image, font)
module.exports = {
compile
}
- yarn gulp compile
- 构建任务
-
const { src, dest, parallel, series } = require(‘gulp’)
const del = require(‘del’)
const clean = () ⥤ {
return del([‘dist’])
}
const compile = parallel(style, script, page)
// 上线之前执行的任务
const build = series(clean, parallel(compile, image, font, extra))
module.exports = {
build,
clean,
develop
}
- yarn gulp build
- yarn gulp clean
- yarn add del --dev
- 开发任务
- const develop = series(compile, serve)
- yarn gulp develop
- be.reload
- 样式编译任务
- const { src, dest } = require('gulp')
exports.style =() ⥤ {
return src(‘src/assets/styles/*.scss’, { base: ‘src’ } )
.pipe(plugins.sass({ outputStyle: ‘expanded’ }))
.pipe(dest(‘dist’))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
style
}
- yarn gulp style
- yarn add gulp-sass --dev
- 脚本文件编译任务
- const { src, dest } = require('gulp')
exports.script = () ⥤ {
return src(‘src/assets/scripts/*.js’, { base: ‘src’ } )
.pipe(plugins.babel({ presets: [’@babel/preset-env’] }))
.pipe(dest(‘dist’))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
script
}
- yarn gulp script
- yarn add gulp-babel --dev
- yarn add @babel/core @babel/preset-env --dev
- 页面文件编译任务
- const { src, dest } = require('gulp')
const data = {
menus: [],
pkg: require(‘package.json’),
date: new Date()
}
exports.page = () ⥤ {
return src(‘src/*.html’, { base: ‘src’ } )
.pipe(plugins.swig({ data }))
.pipe(dest(‘dist’))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
page
}
- yarn gulp page
- 安装模板引擎
- yarn add gulp-swig --dev
- const { src, dest, parallel, series, watch } = require('gulp')
const browserSync = require(‘browser-sync’)
const bs = browserSync.create()
exports.serve = () ⥤ {
watch(‘src/assets/styles/.scss’, style)
watch('src/assets/scripts/.js’, script)
watch(‘src/*.html’, page)
// 以下监听开发时无意义,需注释掉
// watch('src/assets/images/**', image)
// watch('src/assets/fonts/**', font)
// watch('public/**', extra)
watch([
'src/assets/images/**',
'src/assets/fonts/**',
'public/**'
], bs.reload)
bs.init({
notify: false,
port: 2080,
// files: 'dist/**',
open: true,
server: {
baseDir: ['dist', 'src', 'public'],
routes: {
'/node_modules': 'node_modules'
}
}
})
}
const develop = series(compile, serve)
module.exports = {
clean,
serve,
develop
}
- yarn gulp develop
- useref文件引用处理
- yarn gulp build
- yarn add gulp-useref --dev
- const { src, dest } = require('gulp')
exports.useref = () ⥤ {
return src(‘dist/*.html’, { base: ‘dist’ } )
.pipe(plugins.useref({ searchPath: [‘dist’, ‘.’] }))
.pipe(dest(‘dist’))
}
module.exports = {
useref
}
- yarn gulp useref
- 分别压缩HTML、CSS、JS
- const { src, dest } = require('gulp')
exports.useref = () ⥤ {
return src(‘dist/*.html’, { base: ‘dist’ } )
.pipe(plugins.useref({ searchPath: [‘dist’, ‘.’] }))
// html,js,css
.pipe(plugins.if(/.jsKaTeX parse error: Can't use function '\.' in math mode at position 49: …pe(plugins.if(/\̲.̲css/, plugins.cleanCss()))
.pipe(plugins.if(/.html$/, plugins.htmlmin({
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
})))
.pipe(dest(‘release’))
}
module.exports = {
useref
}
- yarn gulp compile
- yarn gulp useref
- yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev
- yarn add gulp-if --dev
- 重新规划构建过程
- clean
-
const { src, dest, parallel, series } = require(‘gulp’)
const del = require(‘del’)
const clean = () ⥤ {
return del([‘dist’, ‘temp’])
}
const compile = parallel(style, script, page)
// 上线之前执行的任务
const build = series(clean, parallel(compile, image, font, extra))
module.exports = {
build,
clean,
develop
}
- 样式编译任务
- const { src, dest } = require('gulp')
exports.style =() ⥤ {
return src(‘src/assets/styles/*.scss’, { base: ‘src’ } )
.pipe(plugins.sass({ outputStyle: ‘expanded’ }))
.pipe(dest(‘temp’))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
style
}
- yarn gulp style
- yarn add gulp-sass --dev
- 脚本文件编译任务
- const { src, dest } = require('gulp')
exports.script = () ⥤ {
return src(‘src/assets/scripts/*.js’, { base: ‘src’ } )
.pipe(plugins.babel({ presets: [’@babel/preset-env’] }))
.pipe(dest(‘temp’))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
script
}
- yarn gulp script
- yarn add gulp-babel --dev
- yarn add @babel/core @babel/preset-env --dev
- 页面文件编译任务
- const { src, dest } = require('gulp')
const data = {
menus: [],
pkg: require(‘package.json’),
date: new Date()
}
exports.page = () ⥤ {
return src(‘src/*.html’, { base: ‘src’ } )
.pipe(plugins.swig({ data }))
.pipe(dest(‘temp’))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
page
}
- yarn gulp page
- 安装模板引擎
- yarn add gulp-swig --dev
- serve
- const { src, dest, parallel, series, watch } = require('gulp')
const browserSync = require(‘browser-sync’)
const bs = browserSync.create()
exports.serve = () ⥤ {
watch(‘src/assets/styles/.scss’, style)
watch('src/assets/scripts/.js’, script)
watch(‘src/*.html’, page)
// 以下监听开发时无意义,需注释掉
// watch('src/assets/images/**', image)
// watch('src/assets/fonts/**', font)
// watch('public/**', extra)
watch([
'src/assets/images/**',
'src/assets/fonts/**',
'public/**'
], bs.reload)
bs.init({
notify: false,
port: 2080,
// files: 'dist/**',
open: true,
server: {
baseDir: ['temp', 'src', 'public'],
routes: {
'/node_modules': 'node_modules'
}
}
})
}
const develop = series(compile, serve)
module.exports = {
clean,
serve,
develop
}
- useref
- const { src, dest } = require('gulp')
exports.useref = () ⥤ {
return src(‘temp/*.html’, { base: ‘temp’ } )
.pipe(plugins.useref({ searchPath: [‘temp’, ‘.’] }))
// html,js,css
.pipe(plugins.if(/.jsKaTeX parse error: Can't use function '\.' in math mode at position 49: …pe(plugins.if(/\̲.̲css/, plugins.cleanCss()))
.pipe(plugins.if(/.html$/, plugins.htmlmin({
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
})))
.pipe(dest(‘dist’))
}
module.exports = {
useref
}
- 组合任务
- const build = series(clean, parallel(series(compile, useref), image, font, extra)
- yarn gulp build
- yarn gulp develop
- 补充
-
module.exports = {
clean,
build,
develop
}
- package.json中添加scripts属性
- "scripts": {
"clean": "gulp clean",
"build": "gulp build",
"develop": "gulp develop",
}
- yarn clean
- yarn build
- yarn develop
- .gitignore中忽略文件
- dist
- temp
- 如何提取多个项目中共同的自动化构建过程?
- 封装自动化构建工作流
- 准备
- 思路: Gulpfile + Gulp = 构建工作流
- Gulpfile + Gulp CLI ⥤ zce-pages
- 新建仓库zce-pages
- cd ..
- 安装应用脚手架
- yarn global add zce-cli
- 使用
- zce init nm zce-pages
- cd zce-pages
- git init
- git remote add origin 远程仓库地址
- git status
- git add .
- git commit -m "feat: initial commit"
- git push -u origin master
- 提取Gulpfile到模块
- code . -a
- 把zce-gulp-demo下gulpfile.js中内容全部拷贝到zce-pages目录下的lib/index.js中。
- 把zce-gulp-demo下package.json中的开发依赖全部拷贝到zce-pages目录下的package.json中作为项目依赖。
- zce-pages目录下安装依赖
- yarn
- 删除zce-gulp-demo下gulpfile.js中内容
- 删除zce-gulp-demo下package.json中的开发依赖
- 删除zce-gulp-demo下node_modules文件夹
- 在zce-gulp-demo中使用zce-pages
- 在zce-pages中使用yarn link
- yarn link "zce-pages"
- 修改gulpfile.js文件
- 删除所有内容
- 添加: module.exports = require ('zce-pages')
- yarn
- yarn build
- 报错: gulp不是内部命令
- yarn add gulp-cli --dev
- yarn build
- 报错: Local gulp not found
- yarn add gulp --dev
- yarn build
- 报错: Cannot find module './package.json'
- 解决模块中的问题
- 在zce-gulp-demo中新建pages.config.js文件
- module.exports = {
data: {
menus: [],
pkg: require('package.json'),
date: new Date()
}
}
- 修改lib/index.js文件
- 页面文件编译任务
- const { src, dest } = require('gulp')
const cwd = process.cwd()
let config = {
// default config
}
try {
const loadConfg = require(${cwd}/pages.config.js
)
config = Object.assign({}, config, loadConfig)
} catch(e) {}
exports.page = () ⥤ {
return src(‘src/*.html’, { base: ‘src’ } )
.pipe(plugins.swig({ data: config.data }))
.pipe(dest(‘temp’))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
page
}
- yarn gulp page
- yarn build
- Cannot find module '@babel/preset-env'
- const { src, dest } = require('gulp')
exports.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 }))
}
module.exports = {
script
}
- yarn build
- 抽象路径配置
- 修改lib/index.js
- const { src, dest } = require('gulp')
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/’,
styles: 'assets/styles/.scss’,
},
}
}
try {
const loadConfg = require(${cwd}/pages.config.js
)
config = Object.assign({}, config, loadConfig)
} catch(e) {}
exports.page = () ⥤ {
return src(‘src/*.html’, { base: ‘src’ } )
.pipe(plugins.swig({ data: config.data }))
.pipe(dest(‘temp’))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
page
}
- 更换写死的常量
- clean
-
const { src, dest, parallel, series } = require(‘gulp’)
const del = require(‘del’)
const clean = () ⥤ {
return del(config.build.dist, config.build.temp])
}
const compile = parallel(style, script, page)
// 上线之前执行的任务
const build = series(clean, parallel(compile, image, font, extra))
module.exports = {
build,
clean,
develop
}
- style
- const { src, dest } = require('gulp')
exports.style =() ⥤ {
return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src } )
.pipe(plugins.sass({ outputStyle: ‘expanded’ }))
.pipe(dest(config.build.temp))
.pipe(bs.reload({ stream: true }))
}
module.exports = {
style
}
- script
- const { src, dest } = require('gulp')
exports.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 }))
}
module.exports = {
script
}
- page
- const { src, dest } = require('gulp')
const cwd = process.cwd()
let config = {
// default config
}
try {
const loadConfg = require(${cwd}/pages.config.js
)
config = Object.assign({}, config, loadConfig)
} catch(e) {}
exports.page = () ⥤ {
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 }))
}
module.exports = {
page
}
- image/font
- const { src, dest } = require('gulp')
const imagemin = require(‘gulp-imagemin’)
exports.image = () ⥤ {
return src(config.build.paths.images, { base: config.build.src, cwd: config.build.src } )
.pipe(imagemin())
.pipe(dest(config.build.dist))
}
exports.font = () ⥤ {
return src(config.build.paths.fonts, { base: config.build.src, cwd: config.build.src } )
.pipe(imagemin())
.pipe(dest(config.build.dist))
}
module.exports = {
image,
font
}
- extra
- const { src, dest } = require('gulp')
exports.extra = () ⥤ {
return src(’**’, { base: config.build.public } )
.pipe(dest(config.build.dist))
}
module.exports = {
extra
}
- serve
- const { src, dest, parallel, series, watch } = require('gulp')
const browserSync = require(‘browser-sync’)
const bs = browserSync.create()
exports.serve = () ⥤ {
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,
// 'public/**'
], { cwd: config.build.src }, bs.reload)
watch([
'**'
], { cwd: config.build.public }, bs.reload)
bs.init({
notify: false,
port: 2080,
// files: 'dist/**',
open: true,
server: {
baseDir: [config.build.temp, config.build.src, config.build.public],
routes: {
'/node_modules': 'node_modules'
}
}
})
}
const develop = series(compile, serve)
module.exports = {
clean,
serve,
develop
}
- useref
- const { src, dest } = require('gulp')
exports.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(/.jsKaTeX parse error: Can't use function '\.' in math mode at position 49: …pe(plugins.if(/\̲.̲css/, plugins.cleanCss()))
.pipe(plugins.if(/.html$/, plugins.htmlmin({
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
})))
.pipe(dest(config.build.dist))
}
module.exports = {
useref
}
- yarn build
- zce-gulp-demo下pages.config.js中增加配置
- module.exports = {
build: {
src: ‘src’,
dist: ‘release’,
temp: ‘.tmp’,
public: ‘public’,
paths: {
styles: ‘assets/styles/.scss’,
scripts: 'assets/scripts/.js’,
pages: ‘.html’,
images: ‘assets/images/’,
fonts: 'assets/fonts/’,
styles: 'assets/styles/.scss’,
},
}
data: {
menus: [],
pkg: require(‘package.json’),
date: new Date()
}
}
- 包装Gulp CLI
- 删除zce-gulp-demo中的gulpfile.js文件
- yarn gulp
- No gulpfile found
- yarn gulp --gulpfile ./node_modules/zce-pages/lib/index.js
- 问题: Task never defined: default
- yarn gulp build --gulpfile ./node_modules/zce-pages/lib/index.js
- 问题: Working directory change to D:\zce\Desktop\zce-gulp-demo\node_modules\zce-pages\lib
- yarn gulp build --gulpfile ./node_modules/zce-pages/lib/index.js --cwd .
- 可以正常运行,但传参复杂
- 在zce-pages目录下新建文件bin/zce-pages.js文件作为cli入口文件。
- 在zce-pages目录下的package.json文件中添加bin属性
- "bin": "bin/zce-pages.js"
- "bin": {
zp: “zce-pages”
}
- 容易产生冲突
- 内容
- #!/usr/bin/env node
console.log(“zce/pages”)
- 使用
- 1,yarn unlink
- 2,yarn link
- 可以直接zce-pages运行命令行文件bin/zce-pages.js。控制台输出: zce/pages
- 内容
- #!/usr/bin/env node
require(‘gulp/bin/gulp’)
- 使用
- 命令行运行: zce-pages
- 问题: No gulpfile found
- 内容
- #!/usr/bin/env node
console.log(process.agv)
require(‘gulp/bin/gulp’)
- 使用
- 命令行运行: zce-pages --sdfs sdfs
- 结果: [
‘C:\Develop\node\node’,
‘C:\Users\zce\AppData\Local\Yarn\Data\link\zce-pages\bin\zce-pages.js’,
‘–sdfs’,
‘sdfs’
]
- 问题: No gulpfile found
- 内容
- #!/usr/bin/env node
process.agv.push(’–cwd’)
process.agv.push(process.cwd())
process.agv.push(’–gulpfile’)
process.agv.push(require.resolve(’…’))
require(‘gulp/bin/gulp’)
- 使用
- cd ..
- cd zce-gulp-demo
- zce-pages build
- Ok~
- 发布并使用模块
- 修改zce-pages目录下的package.json中的files属性
- "files": [
"bin",
"lib"
]
- cd ../
- cd zce-pages
- git add .
- git commit -m "feat: update package"
- git push
- yarn publish
- 问题: 淘宝镜像是只读镜像,直接发布会失败。
- yarn publish --registry https://registry.yranpkg.com
- 使用
- cd ..
- mkdir zce-pages-demo
- cd zce-pages-demo
- Vscode打开
- code .
- 拷贝zce-gulp-demo中的目录public/src/pages.config.js到zce-demo目录中
- 初始化package.json
- yarn init --yes
- yarn add zce-pages --dev
- yarn zce-pages build
- 在package.json中添加scripts
- "scripts": {
"clean": "zce-pages clean",
"build": "zce-pages build",
"develop": "zce-pages develop",
}
FIS
- 简介
- 前端团队推出的一款构建系统。
- 最早只在他们内部项目中使用。
- 后来开源过后,在国内快速流行。
- 相对于Grunt/Gulp这种微内核特点的构建系统,FIS更像是一种捆绑套餐,它把在我们系统中一些典型的需求尽可能都集成到内部了。
- 例如我们在FIS当中可以很轻松的处理像资源加载、模块化开发、代码部署,甚至是性能优化
- 正因为大而全,所以在国内很多项目中就流行开了。
- FIS基本使用
- FIS的核心特点是高度集成
- 把前端日常开发过程当中常见的构建任务和调试任务都集成到了内部。
- 开发者可以通过简单的配置文件的方式去配置我们构建过程中需要完成的工作
- 存在很多内置任务
- 内置了用于调试的web server,可以很方便的调试构建结果
- yarn global add fis3
- 用Vscode打开提前准备好的web应用
- code fis-sample -r
- fis3 release
- 自动构建项目到操作系统当前登录用户所在目录下的临时目录:.fis3-temp
- 将构建结果放入当前目录
- fis3 release -d output
- 只做了资源定位
- 未做代码转换
- 添加fis-conf.js文件
- 内容
- fis.match('*.{js, scss, png}', {
release: ‘/assets/$0’
})
- fis
- 特殊的全局对象
- $0
- 表示当前文件的原始目录结构
- 编译与压缩
- fis.match('*.{js, scss, png}', {
release: ‘/assets/$0’
})
fis.match(’**/*.scss’, {
rExt: ‘.css’,
parser: fis.plugin(‘node-sass’),
optimized: fis.plugin(‘clean-css’)
})
fis.match(’**/*.js’, {
parser: fis.plugin(‘babel-6.x’) ,
optimized: fis.plugin(‘uglify-js’)
})
- fis3 release -d output
- fis3 inspect
- yarn global add fis-parser-node-sass
- yarn global add fis-parser-babel-6.x
-
小结
-
如果你是初学者,FIS更适合
- 但是如果你的要求灵活多变的话,Gulp/Grunt应该是你更好的选择。
-
新手是需要规则的,而老手呢一般都渴望自由。
- 也是因为这个原因,像Grunt/Gulp这些小而美的工具才得以流行。
-