自动化构建
源代码自动转换为生产环境代码
NPM Scripts脚本实现自动化
利用npm scripts脚本实现自动化,npm scripts原理参照阮一峰老师博客
钩子机制实现构建流:
pre-<name>:该命令在name命令之前先启动post-<name>:该命令在name命令之后才启动 npm-run-all模块:同时并行执行多个脚本命令,如npm-p build serve同时启动build和serve脚本命令
常用自动化构建工具
gulp:基于虚拟文件对象的流程,内存中操作文件,而不需要和磁盘交互。grunt:基于临时文件的流程,每一步都要将文件内容写入磁盘,构建速度慢fis:百度推出的前端构建系统,捆绑套餐(集成了很多常用构建流程)
Grunt
安装npm install grunt创建gruntfile.js,用来导出grunt要运行的任务在命令行中使用npx grunt <taskname>来启动任务,默认任务则直接使用npx grunt启动即可。
module
.exports = grunt
=> {
grunt
.registerTask('foo', '任务描述', () => {
})
grunt
.registerTask('bar', () => {
})
grunt
.registerTask('default', ['foo', 'bar']);
grunt
.registerTask('async-task', function() {
const done
= this.async();
setTimeout(() => {
done();
}, 1000);
})
grunt
.registerTask('fail', () => {
return false;
})
grunt
.registerTask('async-fail', function() {
const done
= this.async();
setTimeout(() => {
done(false);
}, 1000);
})
}
Grunt的配置选项方法
module
.exports = grunt
=> {
grunt
.initConfig({
foo
: {
bar
: '123'
}
})
grunt
.registerTask('foo', () => {
grunt
.config('foo.bar');
})
}
Grunt多目标模式任务
多目标模式,可以让任务根据配置形成多个子任务在命令行中使用npx grunt build启动多目标任务时,会显示出两个子任务的执行也可以直接执行子任务npx grunt build:css
module
.exports = grunt
=> {
grunt
.initConfig({
build
: {
options
: {
foo
: 'bar'
}
css
: 'css',
js
: 'js',
other
: {
options
: {
foo
: 'other'
}
}
}
})
grunt
.registerMultiTask('build', function() {
console
.log(this.options());
console
.log(`build task target: ${this.target}, data: ${this.data}`);
})
}
Grunt插件的使用
安装插件,绝大多数插件的命名规范都是grunt-contrib-<name>在gruntfile.js中导入插件提供的任务根据插件文档进行配置使用npx grunt <name>来运行插件任务
module
.exports = grunt
=> {
grunt
.initConfig({
clean
: {
temp
: 'temp/app.js'
}
})
grunt
.loadNpmTasks('grunt-contrib-clean');
}
常用的Grunt构建插件演示
安装load-grunt-tasks,用于减少loadNpmTasks的调用安装grunt-sass和sassnpm install grunt-sass sass -D,编译sass文件安装grunt-babel和@babel/core @babel/preset-env,编译ES6+安装grunt-contrib-watch,启动任务监控
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
: {
sourceMap
: true,
presets
: ['@babel/preset-env']
}
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插件中的任务
loadGruntTasks(grunt
);
// 用默认任务来第一次任务
grunt
.registerTask('default', ['sass', 'babel', 'watch'])
}
Gulp
基本使用
安装gulpnpm install gulp -D,同时也会自动安装好gulp-cli创建gulpfile.js作为gulp的入口文件,由外界启动的tasks挂载到exports对象上(遵循CommonJS规范),或者作为私有任务不暴露给外部。命令行运行npx gulp <task-name>启动任务。
任务定义与挂载
每一个任务都是一个函数gulp默认都是异步任务,因此,任务函数接收一个done参数,用于在任务结束时调用done()来通知gulp任务结束。任务函数如果返回流、Promise等,可以不调用done。任务函数通过挂载到exports上才能通过命令行启动运行,没有挂载到exports上的都是私有函数,只能由gulp在内部处理。
任务的组合
series顺序执行
const { series
} = require('gulp');
exports
.default = series(task1
, task2
, task3
);
parallel并行执行
const { parallel
} = require('gulp');
exports
.default = parallel(task1
, task2
, task3
);
Gulp异步任务的三种方式
如何通知gulp异步任务的完成
回调方式
exports
.callback = done
=> {
console
.log('回调函数方式');
done();
}
exports
.callback_err = done
=> {
console
.log('出现错误时的异步任务');
done(new Error('task failed'))
}
Promise方式
exports
.promise = () => {
console
.log('promise task');
return Promise
.resolve();
}
exports
.promise_err = () => {
console
.log('promise task failed');
return Promise
.reject(new Error('task failed'));
}
const timeout = (time
) => {
return new Promise(resolve
=> {
setTimeout(resolve
, time
);
})
}
exports
.async_task
= async () => {
await timeout(1000);
console
.log('async task');
}
返回流的方式
在任务中返回流是gulp中最常用的异步任务结束方式。在gulp中最频繁的操作就是读取文件流、转换文件流、写入文件流等待,返回一个文件流,实际上会在流的end事件触发时,通知gulp异步任务完成了。因此,返回流、返回EmitEvent类型的对象都是可以的。
const fs
= require('fs');
exports
.stream = () => {
const readStream
= fs
.createReadStream('package.json');
const writeStream
= fs
.createWriteStream('temp.txt');
readStream
.pipe(writeStream
);
return readStream
;
}
exports
.stream = (done
) => {
const readStream
= fs
.createReadStream('package.json');
const writeStream
= fs
.createWriteStream('temp.txt');
readStream
.pipe(writeStream
);
readStream
.on('end', () => { done(); });
}
Gulp构建过程的核心工作原理
const fs
= require('fs');
const { Transform
} = require('stream');
export.default = () => {
const read
= fs
.createReadStream('normalize.css')
const write
= fs
.createWriteStream('normalize.min.css');
const transform
= new Transform({
transform
: (chunk
, encoding
, callback
) => {
const input
= chunk
.toString();
const output
= input
.replace(/\s+/g, '')
.replace(/\/\*.+?\*\//g, '')
callback(null, output
);
}
})
read
.pipe(transform
)
.pipe(write
);
return read
;
}
Gulp文件操作API和插件的使用
构建流程:Gulp读取流 + 插件的转换流 + Gulp的写入流 Gulp采用同名文件覆盖的策略。
npm install gulp-clean-css --dev
npm install gulp-rename --dev
const { src
, dest
} = require('gulp');
const cleanCss
= require('gulp-clean-css');
const rename
= require('gulp-rename');
exports
.default = () => {
return src('src/normalize.css')
.pipe(cleanCss())
.pipe(rename({ extname
: '.min.css' }))
.pipe(dest('dist'));
}
Gulp案例
样式编译
npm install gulp-sass --save-dev
src(globs, [options])
options.base:默认的base为glob base,即特殊字符之前的路径。在dest()写入流时,会将base删去。因此源文件的目录结构在编译后就会丢失。如果手动指定base,则可以保留源文件的目录结构。
const { src
, dest
} = require('gulp');
const sass
= require('gulp-sass');
const style = () => {
return src('src/assets/styles/*.scss', { base
: 'src' })
.pipe(sass({ outputStyle
: 'expanded' }))
.pipe(dest('dist'))
}
module
.exports
= { style
}
脚本编译
npm install gulp-babel @babel/core @babel/preset-env --save-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
}
模板文件(HTML文件)的编译
这里使用了swig模板引擎
npm install gulp-swig --save-dev
const { series
, parallel
} = require('gulp');
const swig
= require('gulp-swig');
const data
= {
};
const page = () => {
return src('src/*.html', { base
: 'src' })
.pipe(swig({
data
: data
,
cache
: false
}))
.pipe(dest('dist'))
}
// module
.exports
= { style
, script
, page
}
const compile
= parallel(style
, script
, page
)
module
.exports
= { compile
}
图片和字体文件的转换
图片进行压缩,对图片中的元信息进行删除
npm install gulp-imagemin --save-dev
const { series
, parallel
} = require('gulp');
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'))
}
const compile
= parallel(style
, script
, page
, image
, font
)
module
.exports
= { compile
}
其他文件以及文件清除
npm install del --save-dev
const { series
, parallel
} = require('gulp');
const del
= require('del');
const extra = () => {
return src('public/**', { base
: 'public' })
.pipe(dest('dist'))
}
const clean = () => {
return del(['dist'])
}
const build
= series(clean
, parallel(compile
, extra
));
module
.exports
= { compile
, build
}
自动载入插件
npm install gulp-load-plugins --save-dev
const loadPlugins
= require('gulp-load-plugins');
const plugins
= loadPlugins();
开发服务器与热更新
npm install browser-sync --save-dev
const { series
, parallel
, watch
} = require('gulp');
const browserSync
= require('browser-sync');
const bs
= browserSync
.create();
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({
notify
: false,
port
: 8080,
open
: false,
files
: 'dist/**',
server
: {
baseDir
: ['dist', 'src', 'public'],
routes
: {
'/node_modules': 'node_modules'
}
}
})
}
const compile
= parallel(style
, script
, page
)
const build
= series(clean
, parallel(compile
, image
, font
, extra
));
const develop
= series(compile
, serve
);
module
.exports
= { compile
, build
, serve
}
useref文件引用处理
在这里,我们将上文中在HTML文件中引用的node_modules中的bootstrap.css文件处理一下。
useref的使用方式
将HTML文件中的CSS和JS文件引用进行合并(但不压缩),文件流传递下去解析HTML文件中的build block语法规则来进行文件合并,解析之后在新生成的HTML文件中将build block规则注释去掉生成引用了合并文件的新的HTML文件,以及合并后的文件。build blocks规则如下所示:
... HTML Markup, list of script / link tags.
<type>:css | js | remove,remove只会移除注释,并不产生新文件alternate search path:默认情况下,输入文件的路径是相对于当前解析的文件。alternate search path提供一个更改该路径的值。path:表示输出的文件路径,也就是合成后的文件路径parameters:表示额外的参数
例如: 处理前的HTML:
css/one.css和css/two.css合并为css/combined.cssscripts/one.js和scripts/two.js合并为scripts/combined.js
<html>
<head>
<link href="css/one.css" rel="stylesheet">
<link href="css/two.css" rel="stylesheet">
</head>
<body>
<script type="text/javascript" src="scripts/one.js"></script>
<script type="text/javascript" src="scripts/two.js"></script>
</body>
</html>
处理后的HTML,引用了合并后的文件
<html>
<head>
<link rel="stylesheet" href="css/combined.css"/>
</head>
<body>
<script src="scripts/combined.js"></script>
</body>
</html>
npm install gulp-useref --save-dev
const useref = () => {
return src('dist/*.html', { base
: 'dist' })
.pipe(plugins
.useref({
searchPath
: ['dist', '.']
}))
.pipe(dest('dist'))
}
module
.exports
= { clean
, compile
, build
, develop
, useref
}
文件压缩(HTML、CSS和JS)
延续自上文的useref插件,从useref插件返回的流中开始压缩工作。由于三种文件采用不同的压缩插件,因此需要使用gulp-if来进行文件类型判断。
npm install gulp-htmlmin gulp-uglify gulp-clean-css --save-dev
npm install gulp-if --save-dev
const useref = () => {
return src('dist/*.html', { base
: 'dist' })
.pipe(plugins
.useref({
searchPath
: ['dist', '.']
}))
.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('release'))
}
module
.exports
= { clean
, compile
, build
, develop
, useref
}
重新规划构建过程
前面由于useref的使用,为了防止写入与读取的冲突,导致更换到release目录作为发布目录,但release目录中没有图片字体等静态资源,这些静态资源是构建在dist目录下的,因此需要对构建过程重新规划一下。 整个构建过程:编译 -> 临时目录temp: useref + 图片等静态资源编译 -> dist目录
const build
= series(
clean
,
parallel(
series(compile
, useref
),
image
,
font
,
extra
)
)
任务导出与私有任务
在完成整个构建流程后,对哪些任务需要导出给外部使用(接口),哪些任务作为内部私有任务进行规划。我们这里选择了clean、develop、build三个任务导出给外部作为接口使用,其他私有任务都封装在这三个任务中。在package.json中使用npm scripts来运行gulp任务。
封装工作流:在多个项目中复用自动化构建过程
封装工作流:将完成的一套构建流程在多个项目中复用(不是简单复制粘贴) 将已经构建好的gulpfile.js、gulp和依赖项作为一个新的NPM模块(比如我们命名为gulp-build)发布,在新的项目中安装gulp-build模块即可使用封装好的工作流。
封装流程
将gulpfile.js的内容添加到gulp-build模块的入口文件index.js中,通常目录结构为lib/index.js。开发依赖项拷贝到gulp-build模块的依赖项中,这样当我们安装gulp-build模块时,就会自动安装那些依赖模块。入口文件中还应该提供一个配置项,用于针对不同项目的不同情况(我们在进行模板编译的时候需要这个配置项来作为模板数据的上下文对象)。每个项目都可以提供一个配置文件,gulp-build模块读取该配置文件,与自身的默认配置项做一个合并,作为整体的配置项。
抽象路径的配置:在原来的gulpfile.js中,对于文件的读取和写入的路径都是固定的,无法针对不同项目的目录结构灵活配置。因此,我们将这些路径做一个抽象配置,在默认配置对象中定义默认的路径,通过读取项目中的配置文件来替换这些路径。 包装gulp-cli,构建模块自己的CLI。
因为我们的gulp-build模块内部已经有gulpfile.js文件(就是模块的入口文件index.js)了,没有必要在使用环境中再次提供一个gulpfile.js来导入gulp-build中的index.js文件,因此,我们只需要在使用环境下,让gulp运行gulp-build中的index.js即可,这就需要提供一个命令行接口在使用环境下直接运行index.js。gulp-cli提供了在命令行中传入参数的方式来指定gulpfile.js的路径gulp <task-name> --gulpfile <gulpfile-path>。同时,可能还需要对工作目录做一下指定(因为gulp默认将gulpfile.js所在路径作为工作目录)gulp <task-name> --gulpfile <gulpfile-path> --cwd <cwd-path>。但是在命令行中传参数会显得很麻烦,所有我们给gulp-build模块提供一个自定义CLI用以代替,将命令行传参转换为CLI脚本文件的执行。CLI的入口脚本文件gulp-build.js一般放在包的bin目录。
package.json中bin字段用以指定CLI的方式执行的入口文件,main字段表示用require()加载模块时的入口文件。
#
!/usr
/bin
/env node
process
.argv
.push('--cwd');
process
.argv
.push(process
.cwd());
process
.argv
.push('--gulpfile');
process
.argv
.push(require
.resolve('..'));
require('gulp/bin/gulp')
const cwd
= process
.cwd();
let config
= {
build
: {
}
};
try {
const loadConfig
= require(path
.join(cwd
, 'page.config.js'));
config
= Object
.assign({}, config
, loadConfig
);
} catch(e
) {
}
.pipe(plugins
.babel({ presets
: ['@babel/preset-env'] }));
.pipe(plugins
.babel({ presets
: [ require('@babel/preset-env') ] }));
测试该构建模块
测试gulp-build模块,使用npm link到全局,然后在测试环境的项目下npm link gulp-build。没有包装gulp-cli时
测试项目下,在gulpfile.js中导入gulp-build包,因为gulp-build包的入口文件就是原来的gulpfile.js,所以gulp-build包默认导出了一些任务(这里导出了clean、develop、build三个任务)。安装gulp-cli,从而可以在命令行启动gulp安装gulp,从而可以启动任务 包装gulp-cli,有了自己的CLI时,就可以直接使用了
"scripts": {
"clean": "gulp clean",
"develop": "gulp develop",
"build": "gulp build"
}
module
.exports
= require('gulp-build');
FIS
高度集成常见的构建流程内置webServer
基本使用
npm install fis3 -g --save-dev
资源定位:将开发文件中的相对路径,在构建后,生产文件中变为绝对路径。使用配置文件进行声明式构建流程
fis
.match('*.{js, scss, png}', {
release
: 'assets/$0'
})
fis
.match('**/*.scss', {
rExt
: '.css',
parser
: fis
.plugin('node-sass'),
optimizer
: fis
.plugin('clean-css')
})
fis
.match('**/*.js', {
parser
: fis
.plugin('babel-6.x'),
optimizer
: fis
.plugin('uglify-js')
})