前端项目构建实战
使用 Gulp 构建完整的前端项目
📋 目录
项目结构
推荐的项目结构
project/
├── src/ # 源文件
│ ├── assets/ # 静态资源
│ │ ├── fonts/ # 字体文件
│ │ └── icons/ # 图标
│ ├── images/ # 图片
│ │ └── sprites/ # 雪碧图
│ ├── js/ # JavaScript
│ │ ├── components/ # 组件
│ │ ├── utils/ # 工具函数
│ │ ├── app.js # 入口文件
│ │ └── main.js # 主逻辑
│ ├── scss/ # Sass 样式
│ │ ├── base/ # 基础样式
│ │ ├── components/ # 组件样式
│ │ ├── layout/ # 布局样式
│ │ ├── pages/ # 页面样式
│ │ ├── utils/ # 工具样式
│ │ ├── vendors/ # 第三方样式
│ │ └── app.scss # 主样式文件
│ └── templates/ # 模板文件
│ ├── layouts/ # 布局模板
│ ├── partials/ # 局部模板
│ └── pages/ # 页面模板
├── dist/ # 构建输出(自动生成)
├── public/ # 静态文件(直接复制)
├── gulpfile.js # Gulp 配置
├── gulpfile.config.js # Gulp 配置文件
├── package.json # 项目配置
└── README.md # 项目说明
初始化项目
1. 创建项目目录
mkdir my-project
cd my-project2. 初始化 npm
npm init -y3. 安装 Gulp 和插件
# 安装 Gulp
npm install --save-dev gulp gulp-cli
# 安装核心插件
npm install --save-dev \
gulp-uglify \
gulp-babel @babel/core @babel/preset-env \
gulp-sass sass \
gulp-postcss autoprefixer cssnano \
gulp-clean-css \
gulp-htmlmin \
gulp-imagemin \
gulp-concat \
gulp-rename \
gulp-sourcemaps \
browser-sync \
del \
gulp-plumber \
gulp-notify \
gulp-size \
gulp-changed4. 创建配置文件
// gulpfile.config.js
module.exports = {
paths: {
src: {
js: 'src/js/**/*.js',
scss: 'src/scss/**/*.scss',
html: 'src/*.html',
images: 'src/images/**/*',
fonts: 'src/assets/fonts/**/*'
},
dest: {
js: 'dist/js',
css: 'dist/css',
html: 'dist',
images: 'dist/images',
fonts: 'dist/fonts'
}
},
options: {
uglify: {
compress: {
drop_console: true
}
},
babel: {
presets: ['@babel/preset-env']
},
sass: {
outputStyle: 'expanded'
},
postcss: [
require('autoprefixer')(),
require('cssnano')()
],
htmlmin: {
collapseWhitespace: true,
removeComments: true,
minifyCSS: true,
minifyJS: true
},
imagemin: {
quality: 85
}
}
};配置开发环境
开发环境特点
- 快速构建
- 生成 Source Map
- 保留未压缩代码
- 热重载
- 错误提示
开发环境配置
// gulpfile.js
const { src, dest, watch, series, parallel } = require('gulp');
const browserSync = require('browser-sync').create();
const config = require('./gulpfile.config.js');
// JavaScript 处理(开发环境)
function scriptsDev() {
const babel = require('gulp-babel');
const sourcemaps = require('gulp-sourcemaps');
const plumber = require('gulp-plumber');
const notify = require('gulp-notify');
return src(config.paths.src.js, { sourcemaps: true })
.pipe(plumber({
errorHandler: notify.onError('Error: <%= error.message %>')
}))
.pipe(sourcemaps.init())
.pipe(babel(config.options.babel))
.pipe(sourcemaps.write('.'))
.pipe(dest(config.paths.dest.js))
.pipe(browserSync.stream());
}
// CSS 处理(开发环境)
function stylesDev() {
const sass = require('gulp-sass')(require('sass'));
const sourcemaps = require('gulp-sourcemaps');
const postcss = require('gulp-postcss');
const plumber = require('gulp-plumber');
const notify = require('gulp-notify');
return src(config.paths.src.scss, { sourcemaps: true })
.pipe(plumber({
errorHandler: notify.onError('Error: <%= error.message %>')
}))
.pipe(sourcemaps.init())
.pipe(sass(config.options.sass))
.pipe(postcss([require('autoprefixer')()]))
.pipe(sourcemaps.write('.'))
.pipe(dest(config.paths.dest.css))
.pipe(browserSync.stream());
}
// HTML 处理(开发环境)
function htmlDev() {
const fileInclude = require('gulp-file-include');
const plumber = require('gulp-plumber');
return src(config.paths.src.html)
.pipe(plumber())
.pipe(fileInclude({
prefix: '@@',
basepath: '@file'
}))
.pipe(dest(config.paths.dest.html))
.pipe(browserSync.stream());
}
// 图片处理(开发环境)
function imagesDev() {
return src(config.paths.src.images)
.pipe(dest(config.paths.dest.images));
}
// 启动开发服务器
function server() {
browserSync.init({
server: {
baseDir: './dist'
},
port: 3000,
open: true,
notify: false
});
}
// 文件监听
function watchFiles() {
watch(config.paths.src.js, scriptsDev);
watch(config.paths.src.scss, stylesDev);
watch(config.paths.src.html, htmlDev);
watch(config.paths.src.images, imagesDev);
}
// 开发任务
exports.dev = series(
parallel(scriptsDev, stylesDev, htmlDev, imagesDev),
parallel(server, watchFiles)
);配置生产环境
生产环境特点
- 代码压缩
- 图片优化
- 文件哈希
- 资源优化
- 性能优化
生产环境配置
// JavaScript 处理(生产环境)
function scriptsProd() {
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const concat = require('gulp-concat');
const rename = require('gulp-rename');
const sourcemaps = require('gulp-sourcemaps');
const size = require('gulp-size');
return src(config.paths.src.js)
.pipe(sourcemaps.init())
.pipe(babel(config.options.babel))
.pipe(concat('app.js'))
.pipe(dest(config.paths.dest.js))
.pipe(uglify(config.options.uglify))
.pipe(rename({ suffix: '.min' }))
.pipe(sourcemaps.write('.'))
.pipe(size({ showFiles: true, gzip: true }))
.pipe(dest(config.paths.dest.js));
}
// CSS 处理(生产环境)
function stylesProd() {
const sass = require('gulp-sass')(require('sass'));
const postcss = require('gulp-postcss');
const cleanCSS = require('gulp-clean-css');
const concat = require('gulp-concat');
const rename = require('gulp-rename');
const sourcemaps = require('gulp-sourcemaps');
const size = require('gulp-size');
return src(config.paths.src.scss)
.pipe(sourcemaps.init())
.pipe(sass(config.options.sass))
.pipe(postcss(config.options.postcss))
.pipe(concat('app.css'))
.pipe(dest(config.paths.dest.css))
.pipe(cleanCSS())
.pipe(rename({ suffix: '.min' }))
.pipe(sourcemaps.write('.'))
.pipe(size({ showFiles: true, gzip: true }))
.pipe(dest(config.paths.dest.css));
}
// HTML 处理(生产环境)
function htmlProd() {
const htmlmin = require('gulp-htmlmin');
const fileInclude = require('gulp-file-include');
return src(config.paths.src.html)
.pipe(fileInclude({
prefix: '@@',
basepath: '@file'
}))
.pipe(htmlmin(config.options.htmlmin))
.pipe(dest(config.paths.dest.html));
}
// 图片处理(生产环境)
function imagesProd() {
const imagemin = require('gulp-imagemin');
return src(config.paths.src.images)
.pipe(imagemin([
imagemin.gifsicle({ interlaced: true }),
imagemin.mozjpeg({ quality: 85, progressive: true }),
imagemin.optipng({ optimizationLevel: 5 }),
imagemin.svgo({
plugins: [
{ removeViewBox: true },
{ cleanupIDs: false }
]
})
]))
.pipe(dest(config.paths.dest.images));
}
// 字体处理
function fonts() {
return src(config.paths.src.fonts)
.pipe(dest(config.paths.dest.fonts));
}
// 清理构建目录
function clean() {
const del = require('del');
return del(['dist/**/*']);
}
// 构建任务
exports.build = series(
clean,
parallel(scriptsProd, stylesProd, htmlProd, imagesProd, fonts)
);优化构建性能
1. 增量构建
const changed = require('gulp-changed');
function images() {
return src('src/images/**/*')
.pipe(changed('dist/images')) // 只处理修改过的图片
.pipe(imagemin())
.pipe(dest('dist/images'));
}2. 缓存处理结果
const cache = require('gulp-cache');
function images() {
return src('src/images/**/*')
.pipe(cache(imagemin([
imagemin.jpegtran({ progressive: true }),
imagemin.optipng({ optimizationLevel: 5 })
])))
.pipe(dest('dist/images'));
}3. 并行执行任务
// 串行(慢)
exports.build = series(scripts, styles, html, images);
// 并行(快)
exports.build = parallel(scripts, styles, html, images);
// 混合(推荐)
exports.build = series(
clean,
parallel(scripts, styles, html, images)
);4. 使用 gulp.lastRun
const { lastRun } = require('gulp');
function scripts() {
return src('src/js/**/*.js', {
since: lastRun(scripts) // 只返回自上次运行后修改的文件
})
.pipe(babel())
.pipe(dest('dist/js'));
}5. 多核构建
const os = require('os');
const cluster = require('cluster');
function parallelBuild() {
const cpuCount = os.cpus().length;
if (cluster.isMaster) {
// 创建工作进程
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
} else {
// 工作进程执行任务
return parallel(scripts, styles, images);
}
}完整配置示例
package.json
{
"name": "gulp-project",
"version": "1.0.0",
"description": "Gulp 前端项目构建",
"scripts": {
"dev": "gulp dev",
"build": "gulp build",
"clean": "gulp clean",
"serve": "gulp serve"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"autoprefixer": "^10.3.0",
"browser-sync": "^2.27.0",
"cssnano": "^5.0.0",
"del": "^6.0.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-file-include": "^2.3.0",
"gulp-htmlmin": "^5.0.1",
"gulp-if": "^3.0.0",
"gulp-imagemin": "^8.0.0",
"gulp-notify": "^4.0.0",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^9.0.0",
"gulp-rename": "^2.0.0",
"gulp-sass": "^5.0.0",
"gulp-size": "^4.0.1",
"gulp-sourcemaps": "^3.0.0",
"gulp-uglify": "^3.0.2",
"sass": "^1.38.0"
}
}完整 gulpfile.js
const { src, dest, watch, series, parallel, lastRun } = require('gulp');
const browserSync = require('browser-sync').create();
const config = require('./gulpfile.config.js');
// 判断是否为生产环境
const isProduction = process.env.NODE_ENV === 'production';
// 清理构建目录
function clean() {
const del = require('del');
return del(['dist/**/*']);
}
// JavaScript 处理
function scripts() {
const babel = require('gulp-babel');
const uglify = require('gulp-uglify');
const concat = require('gulp-concat');
const rename = require('gulp-rename');
const sourcemaps = require('gulp-sourcemaps');
const plumber = require('gulp-plumber');
const size = require('gulp-size');
return src(config.paths.src.js, {
sourcemaps: !isProduction,
since: lastRun(scripts)
})
.pipe(plumber({
errorHandler: require('gulp-notify').onError('Error: <%= error.message %>')
}))
.pipe(sourcemaps.init())
.pipe(babel(config.options.babel))
.pipe(concat('app.js'))
.pipe(dest(config.paths.dest.js))
.pipe(gulpif(isProduction, uglify(config.options.uglify)))
.pipe(gulpif(isProduction, rename({ suffix: '.min' })))
.pipe(sourcemaps.write('.'))
.pipe(gulpif(isProduction, size({ showFiles: true, gzip: true })))
.pipe(dest(config.paths.dest.js))
.pipe(gulpif(!isProduction, browserSync.stream()));
}
// CSS 处理
function styles() {
const sass = require('gulp-sass')(require('sass'));
const postcss = require('gulp-postcss');
const cleanCSS = require('gulp-clean-css');
const concat = require('gulp-concat');
const rename = require('gulp-rename');
const sourcemaps = require('gulp-sourcemaps');
const plumber = require('gulp-plumber');
const size = require('gulp-size');
return src(config.paths.src.scss, {
sourcemaps: !isProduction,
since: lastRun(styles)
})
.pipe(plumber({
errorHandler: require('gulp-notify').onError('Error: <%= error.message %>')
}))
.pipe(sourcemaps.init())
.pipe(sass(config.options.sass))
.pipe(postcss(gulpif(isProduction, config.options.postcss : [require('autoprefixer')()])))
.pipe(concat('app.css'))
.pipe(dest(config.paths.dest.css))
.pipe(gulpif(isProduction, cleanCSS()))
.pipe(gulpif(isProduction, rename({ suffix: '.min' })))
.pipe(sourcemaps.write('.'))
.pipe(gulpif(isProduction, size({ showFiles: true, gzip: true })))
.pipe(dest(config.paths.dest.css))
.pipe(gulpif(!isProduction, browserSync.stream()));
}
// HTML 处理
function html() {
const htmlmin = require('gulp-htmlmin');
const fileInclude = require('gulp-file-include');
return src(config.paths.src.html)
.pipe(fileInclude({
prefix: '@@',
basepath: '@file'
}))
.pipe(gulpif(isProduction, htmlmin(config.options.htmlmin)))
.pipe(dest(config.paths.dest.html))
.pipe(gulpif(!isProduction, browserSync.stream()));
}
// 图片处理
function images() {
const imagemin = require('gulp-imagemin');
const changed = require('gulp-changed');
return src(config.paths.src.images, {
since: lastRun(images)
})
.pipe(changed(config.paths.dest.images))
.pipe(gulpif(isProduction, imagemin([
imagemin.gifsicle({ interlaced: true }),
imagemin.mozjpeg({ quality: 85, progressive: true }),
imagemin.optipng({ optimizationLevel: 5 }),
imagemin.svgo({
plugins: [
{ removeViewBox: true },
{ cleanupIDs: false }
]
})
])))
.pipe(dest(config.paths.dest.images));
}
// 字体处理
function fonts() {
return src(config.paths.src.fonts)
.pipe(dest(config.paths.dest.fonts));
}
// 开发服务器
function server() {
browserSync.init({
server: {
baseDir: './dist'
},
port: 3000,
open: true,
notify: false
});
}
// 文件监听
function watchFiles() {
watch(config.paths.src.js, scripts);
watch(config.paths.src.scss, styles);
watch(config.paths.src.html, html);
watch(config.paths.src.images, images);
watch(config.paths.src.fonts, fonts);
}
// 任务
exports.clean = clean;
exports.scripts = scripts;
exports.styles = styles;
exports.html = html;
exports.images = images;
exports.fonts = fonts;
exports.watch = watchFiles;
// 开发任务
exports.dev = series(
clean,
parallel(scripts, styles, html, images, fonts),
parallel(server, watchFiles)
);
// 生产构建
exports.build = series(
clean,
parallel(scripts, styles, html, images, fonts)
);
// 默认任务
exports.default = isProduction ? exports.build : exports.dev;