文章内容输出来源:拉勾教育前端高薪训练营
前端工程化是什么
工程化是使用软件工程的技术和方法来进行前端的开发流程、技术、工具、经验等规范化、标准化,其以提高效率、降低成本、质量保证为目的。工程化不等于某个具体的工具,工程化是指项目整体的规划和架构,工具只是落地实施的手段。
工程化主要解决的问题
工程化的一般流程
# 安装依赖
$ yarn install
gulpfile.js
// 引入gulp读写流
const { src, dest, parallel, series, watch } = require('gulp')
// 引入自动加载gulp插件依赖
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
gulpfile.js
const browserSync = require('browser-sync')
const bs = browserSync.create()
// 实时监听sass、js、html变化,更新页面
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,
files: 'temp/**',
server: {
baseDir: ['temp', 'src', 'public'],
routes: {
'/node_modules': 'node_modules',
},
},
})
}
将css、js、html文件打包到临时目录temp,以便useref压缩拷贝到dist
此处为防止useref在压缩合并代码时,在同一个目录下同时进行读写产生错误,先将打包的html、js、css代码暂存在temp目录下,然后通过useref拷贝到dist下
gulpfile.js
// sass编译打包
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' })
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest('temp'))
}
// js编译打包
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' })
.pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
.pipe(dest('temp'))
}
// html编译打包,设置swig缓存为false,防止页面不刷新
const page = () => {
return src('src/*.html', { base: 'src' })
.pipe(plugins.swig({ data, defaults: { cache: false } }))
.pipe(dest('temp'))
}
gulpfile.js
// 编译css、js、html编译可以同步进行
const compile = parallel(style, script, page)
// dev开发启动需要在编译完成后
const dev = series(compile, serve)
module.exports = {
dev,
}
清除插件的作用在于删除之前的dist和temp文件
gulpfile.js
const del = require('del')
const clean = () => {
return del(['dist', 'temp'])
}
gulpfile.js
// 编译图片文件
const image = () => {
return src('src/assets/images/**', { base: 'src' })
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// 编译字体文件
const font = () => {
return src('src/assets/fonts/**', { base: 'src' })
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// 编译public下的文件
const extra = () => {
return src('public/**', { base: 'public' })
.pipe(dest('dist'))
}
gulpfile.js
const useref = () => {
return src('temp/**.html')
.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('dist'))
}
const build = series(
clean,
parallel(
series(compile, useref),
image,
font,
extra,
))
module.exports = {
build,
clean,
}
yarn gulp clean
yarn gulp build
yarn gulp dev
or
将三种任务在package.json中配置成script
package.json
"scripts": {
"clean": "gulp clean",
"dev": "gulp dev",
"build": "gulp build",
}
yarn clean
yarn build
yarn dev
完整gulp配置代码
const { src, dest, parallel, series, watch } = require('gulp')
const del = require('del')
const browserSync = require('browser-sync')
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
const bs = browserSync.create()
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/'
},
{
name: 'About',
link: 'https://weibo.com/'
},
]
}
],
pkg: require('./package.json'),
date: new Date()
}
const clean = () => {
return del(['dist', 'temp'])
}
// sass编译打包
const style = () => {
return src('src/assets/styles/*.scss', { base: 'src' })
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest('temp'))
}
// js编译打包
const script = () => {
return src('src/assets/scripts/*.js', { base: 'src' })
.pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
.pipe(dest('temp'))
}
// html编译打包
const page = () => {
return src('src/*.html', { base: 'src' })
.pipe(plugins.swig({ data, defaults: { cache: false } }))
.pipe(dest('temp'))
}
// 编译图片文件
const image = () => {
return src('src/assets/images/**', { base: 'src' })
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// 编译字体文件
const font = () => {
return src('src/assets/fonts/**', { base: 'src' })
.pipe(plugins.imagemin())
.pipe(dest('dist'))
}
// 编译public下的文件
const extra = () => {
return src('public/**', { base: 'public' })
.pipe(dest('dist'))
}
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,
files: 'temp/**', // 监听该文件夹下的文件变化,自动更新页面
server: {
baseDir: ['temp', 'src', 'public'],
routes: {
'/node_modules': 'node_modules',
},
},
})
}
const useref = () => {
return src('temp/**.html')
.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('dist'))
}
const compile = parallel(style, script, page)
const build = series(
clean,
parallel(
series(compile, useref),
image,
font,
extra,
))
const dev = series(compile, serve)
module.exports = {
build,
dev,
clean,
}
前端工程化的发起者,创建项目基础结构、提供项目规范和约定,避免重复工作,其主要意义在于
常用的脚手架工具
动手开发一个脚手架
# 初始化项目
$ yarn init
# 安装各种依赖包
$ yarn install
├── README.md // 项目自述文档
├── bin
│ └── cli.js // 命令行文件
├── lib
│ ├── create.js // 创建模板
│ ├── main.js // 脚手架入口文件
│ └── utils
│ ├── constants.js // 公共常量
│ └── utils.js // 工具文件
└── package.json
在package.json中设置命令路径
"bin": {
"yhzzy-cli": "./bin/cli.js"
}
cli.js添加node环境执行命令,并使用main.js作为入口
#!/usr/bin/env node
require('../lib/main')
将yhzzy-cli命令链接到全局,之后在main.js中实现脚手架逻辑
yarn link
utils/constants.js
const { name, version } = require('../../package.json')
module.exports = {
name,
version,
}
main.js
const { program } = require('commander')
const { version } = require('./utils/constants')
program.version(version)
.parse(process.argv)
main.js
const actions = {
create: {
description: 'create a project',
alias: 'cr',
examples: [
'yh-cli create '
],
},
}
Object.keys(actions).forEach(action => {
program
.command(action)
.description(actions[action].description)
.alias(actions[action].alias)
.action(() => {
require(path.resolve(__dirname, action))(...process.argv.slice(3))
})
})
main.js
program.on('--help', () => {
console.log('Examples')
Object.keys(actions).forEach(action => {
(actions[action].examples || []).forEach(ex => {
console.log(`${ex}`)
})
})
})
create命令的作用:根据用户选择的模板选项,从git仓库中下载对应的模板到本地目录,并渲染生成到执行命令的目标目录
utils/constants.js文件中添加以下代码,commonQuestions为不同模板共同的问题
const commonQuestions = [
{
type: 'input',
name: 'name',
message: 'Project name?',
default: process.cwd().split('/').pop(),
},
{
type: 'input',
name: 'version',
message: 'Project version?',
default: '0.1.0',
},
{
type: 'input',
name: 'author',
message: 'Who is the author?',
},
{
type: 'input',
name: 'description',
message: 'Project description?',
default: 'this is a template',
},
{
type: 'confirm',
name: 'private',
message: 'Is private?',
default: false,
},
{
type: 'list',
name: 'license',
message: 'please choice a license',
choices: ['MIT', 'ISC', 'Apache'],
},
]
const questions = {
'template-vue-cli': [
...commonQuestions,
{
type: 'input',
name: 'displayName',
message: 'Display for webpack title name?',
default: process.cwd().split('/').pop(),
},
],
'template-nm-cli': [
...commonQuestions,
],
}
module.exports = {
questions,
}
main.js
const actions = {
create: {
description: 'create a project',
alias: 'cr',
examples: [
'yhzzy-cli create || yhzzy-cli cr'
],
},
}
create.js
const path = require('path')
const axios = require('axios')
// 模板列表
let repos = []
// 模板下载地址
const templatePath = path.resolve(__dirname, '../templates')
// 获取仓库地址
const fetchRepoList = async () => {
const { data } = await axios.get('https://api.github.com/users/yhzzy/repos')
return data
}
// 获取最新版本信息
const fetchReleasesLatest = async (repo) => {
const { data } = await axios.get(`https://api.github.com/repos/yhzzy/${repo}/releases/latest`)
return data
}
create.js
const fs = require('fs')
const ejs = require('ejs')
const { promisify } = require('util')
const { loading, isDirectory } = require('../lib/utils/utils')
const downLoadGitRepo = require('download-git-repo')
const downLoadGit = promisify(downLoadGitRepo)
// 下载模板
const download = async (path, repo) => {
const repoPath = `yhzzy/${repo}`
await downLoadGit(repoPath, path)
}
// 获取当前已下载模板版本号
const getTemplateVersion = dir => {
const packageJson = fs.readFileSync(path.join(dir, 'package.json'))
return JSON.parse(packageJson)
}
// 往目标目录中写入模板文件并完成渲染
const writeTemplateFile = (tempDir, destDir, answers, file) => {
// 判断是否媒体文件,如果是媒体文件则直接复制过去
const isMedia = tempDir.split('/').pop() === 'img'
if (isMedia) {
const sourceFile = path.join(tempDir, file)
const destPath = path.join(destDir, file)
const readStream = fs.createReadStream(sourceFile)
const writeStream = fs.createWriteStream(destPath)
return readStream.pipe(writeStream)
}
ejs.renderFile(path.join(tempDir, file), answers, (err, result) => {
if (err) throw err
fs.writeFileSync(path.join(destDir, file), result)
})
}
// 读取模板文件目录并完成写入并渲染
const writeTemplateFiles = async (tempDir, destDir, answers) => {
fs.readdir(tempDir, (err, files) => {
if (err) throw err
files.forEach(async (file) => {
// 判断复制的文件是否为文件夹
const isDir = isDirectory(path.join(tempDir, file))
if (isDir) {
// 判断目标文件夹下是否有此名称文件夹
const destDirHasThisDir = isDirectory(path.join(destDir, file))
// 如果没有此名称文件夹则新建文件夹
if (!destDirHasThisDir) {
fs.mkdirSync(path.join(destDir, file))
}
writeTemplateFiles(path.join(tempDir, file), path.join(destDir, file), answers)
} else {
writeTemplateFile(tempDir, destDir, answers, file)
}
})
})
}
根据用户选择的选项进行对应的模板下载,如果本地已存在模板,则和git仓库中的模板进行版本对比,有新版本发布时从新下载模板,否则不进行模板下载操作
create.js
const del = require('del')
const inquirer = require('inquirer')
const { questions } = require('./utils/constants')
module.exports = async (projectName) => {
repos = await loading(fetchRepoList, 'fetching repo list')()
repos = repos.filter(item => item.name.split('-')[0] === 'template')
inquirer.prompt([
{
type: 'list',
name: 'templateType',
message: 'please choice a template to create project',
choices: repos,
},
])
.then(answer => {
const { templateType } = answer
inquirer.prompt([
...questions[templateType],
])
.then(async (answers) => {
// 判断是否存在模板文件夹,不存在则新建文件夹
const templatesFolder = isDirectory(templatePath)
if (!templatesFolder) {
fs.mkdirSync(templatePath)
}
const downloadPath = path.join(templatePath, templateType)
const destDir = process.cwd()
// 判断模板文件夹中是否存在需要下载的模板文件夹
if (fs.existsSync(downloadPath)) {
// 获取模板最新的发布版本号,然后和当前已下载的模板进行比对,如果版本已更新则更新本地模板文件,如果版本一致则不进行下载模板操作
const { name } = await loading(fetchReleasesLatest, `view the latest ${templateType} version in guthub now...`)(templateType)
const { releaseVersion } = getTemplateVersion(downloadPath)
if (name !== releaseVersion) {
del(downloadPath, {
force: true,
})
await loading(download, 'download the template now...')(downloadPath, templateType)
}
} else {
await loading(download, 'download the template now...')(downloadPath, templateType)
}
writeTemplateFiles(downloadPath, destDir, answers)
})
})
}
# global install
yarn global add yhzzy-cli
# global installed or local link
yhzzy-cli cr
完整脚手架代码
bin/cli.js
#!/usr/bin/env node
require('../lib/main')
utils/utils.js
const fs = require('fs')
const ora = require('ora')
/**
*
* @param {*} fn 执行的方法
* @param {*} msg 提示语言
* @returns
*/
const loading = (fn, msg) => async (...args) => {
const spinner = ora(msg)
spinner.start()
const res = await fn(...args)
spinner.succeed()
return res
}
/**
*
* @param {*} dir 文件路径
* @returns
*/
const isDirectory = dir => {
try{
return fs.statSync(dir).isDirectory()
} catch(e) {
return false
}
}
module.exports = {
loading,
isDirectory,
}
utils/constants.js
const { name, version } = require('../../package.json')
const commonQuestions = [
{
type: 'input',
name: 'name',
message: 'Project name?',
default: process.cwd().split('/').pop(),
},
{
type: 'input',
name: 'version',
message: 'Project version?',
default: '0.1.0',
},
{
type: 'input',
name: 'author',
message: 'Who is the author?',
},
{
type: 'input',
name: 'description',
message: 'Project description?',
default: 'this is a template',
},
{
type: 'confirm',
name: 'private',
message: 'Is private?',
default: false,
},
{
type: 'list',
name: 'license',
message: 'please choice a license',
choices: ['MIT', 'ISC', 'Apache'],
},
]
const questions = {
'template-vue-cli': [
...commonQuestions,
{
type: 'input',
name: 'displayName',
message: 'Display for webpack title name?',
default: process.cwd().split('/').pop(),
},
],
'template-nm-cli': [
...commonQuestions,
],
}
module.exports = {
name,
version,
questions,
}
lib/main.js
const path = require('path')
const fs = require('fs')
const { program } = require('commander')
const { version } = require('./utils/constants')
const actions = {
create: {
description: 'create a project',
alias: 'cr',
examples: [
'yhzzy-cli create || yhzzy-cli cr'
],
},
}
Object.keys(actions).forEach(action => {
program
.command(action)
.description(actions[action].description)
.alias(actions[action].alias)
.action(() => {
require(path.resolve(__dirname, action))(...process.argv.slice(3))
})
})
program.on('--help', () => {
console.log('Examples')
Object.keys(actions).forEach(action => {
(actions[action].examples || []).forEach(ex => {
console.log(`${ex}`)
})
})
})
program.version(version)
.parse(process.argv)
lib/create.js
const path = require('path')
const fs = require('fs')
const del = require('del')
const axios = require('axios')
const inquirer = require('inquirer')
const ejs = require('ejs')
const { promisify } = require('util')
const { loading, isDirectory } = require('../lib/utils/utils')
const { questions } = require('./utils/constants')
const downLoadGitRepo = require('download-git-repo')
const downLoadGit = promisify(downLoadGitRepo)
// 模板列表
let repos = []
// 模板下载地址
const templatePath = path.resolve(__dirname, '../templates')
// 获取仓库地址
const fetchRepoList = async () => {
const { data } = await axios.get('https://api.github.com/users/yhzzy/repos')
return data
}
// 获取最新版本信息
const fetchReleasesLatest = async (repo) => {
const { data } = await axios.get(`https://api.github.com/repos/yhzzy/${repo}/releases/latest`)
return data
}
// 下载模板
const download = async (path, repo) => {
const repoPath = `yhzzy/${repo}`
await downLoadGit(repoPath, path)
}
// 获取当前已下载模板版本号
const getTemplateVersion = dir => {
const packageJson = fs.readFileSync(path.join(dir, 'package.json'))
return JSON.parse(packageJson)
}
// 往目标目录中写入模板文件并完成渲染
const writeTemplateFile = (tempDir, destDir, answers, file) => {
// 判断是否媒体文件,如果是媒体文件则直接复制过去
const isMedia = tempDir.split('/').pop() === 'img'
if (isMedia) {
const sourceFile = path.join(tempDir, file)
const destPath = path.join(destDir, file)
const readStream = fs.createReadStream(sourceFile)
const writeStream = fs.createWriteStream(destPath)
return readStream.pipe(writeStream)
}
ejs.renderFile(path.join(tempDir, file), answers, (err, result) => {
if (err) throw err
fs.writeFileSync(path.join(destDir, file), result)
})
}
// 读取模板文件目录并完成写入并渲染
const writeTemplateFiles = async (tempDir, destDir, answers) => {
fs.readdir(tempDir, (err, files) => {
if (err) throw err
files.forEach(async (file) => {
// 判断复制的文件是否为文件夹
const isDir = isDirectory(path.join(tempDir, file))
if (isDir) {
// 判断目标文件夹下是否有此名称文件夹
const destDirHasThisDir = isDirectory(path.join(destDir, file))
// 如果没有此名称文件夹则新建文件夹
if (!destDirHasThisDir) {
fs.mkdirSync(path.join(destDir, file))
}
writeTemplateFiles(path.join(tempDir, file), path.join(destDir, file), answers)
} else {
writeTemplateFile(tempDir, destDir, answers, file)
}
})
})
}
module.exports = async () => {
repos = await loading(fetchRepoList, 'fetching repo list')()
repos = repos.filter(item => item.name.split('-')[0] === 'template')
inquirer.prompt([
{
type: 'list',
name: 'templateType',
message: 'please choice a template to create project',
choices: repos,
},
])
.then(answer => {
const { templateType } = answer
inquirer.prompt([
...questions[templateType],
])
.then(async (answers) => {
// 判断是否存在模板文件夹,不存在则新建文件夹
const templatesFolder = isDirectory(templatePath)
if (!templatesFolder) {
fs.mkdirSync(templatePath)
}
const downloadPath = path.join(templatePath, templateType)
const destDir = process.cwd()
// 判断模板文件夹中是否存在需要下载的模板文件夹
if (fs.existsSync(downloadPath)) {
// 获取模板最新的发布版本号,然后和当前已下载的模板进行比对,如果版本已更新则更新本地模板文件,如果版本一致则不进行下载模板操作
const { name } = await loading(fetchReleasesLatest, `view the latest ${templateType} version in guthub now...`)(templateType)
const { releaseVersion } = getTemplateVersion(downloadPath)
if (name !== releaseVersion) {
del(downloadPath, {
force: true,
})
await loading(download, 'download the template now...')(downloadPath, templateType)
}
} else {
await loading(download, 'download the template now...')(downloadPath, templateType)
}
writeTemplateFiles(downloadPath, destDir, answers)
})
})
}