主要解决的问题
- 传统语言或语法的弊端
- 无法使用模块化/组件化
- 重复的机械式工作
- 代码风格统一、质量保证
- 依赖后端服务接口支持
- 整体依赖后端项目
工程化表现
- 一切以提高效率、降低成本、质量保证为目的的手段都属于[工程化]
- 一切重复的工作都应该被自动化
工程化 !== 工具
脚手架
本质作用
创建项目基本结构、提供项目规范和约定
常用的脚手架工具
- React项目->create-react-app
- Vue.js项目->vue-cli
- Angular项目->angular-ui
- Yeoman
- Plop(创建一个组件/模块所需要的文件)
Yeoman
基本使用
- 在全局范围安装 yo
npm install yo -g // or yarn global add yo
- 安装对应的 generator
npm install generator-node -g // or yarn global add generator-node
- 通过 yo 运行 generator
yo node
Sub Generator
yo node:cli
yarn link // 链接到全局范围,使之成为全局安装包,便于yeoman查找到
yarn // 安装依赖
my-module --help
Yeoman 使用步骤总结
- 明确你的需求
- 找到合适的 Generator
- 全局范围安装找到的Generator
- 通过 yo 运行对应的 Generator
- 通过命令行交互填写选项
- 生成你所需要的项目结构
创建Generator模块
- Generator 本质上就是一个 NPM 模块
- 命名:generator-<name>
- 代码演示:
mkdir generator-sample
yarn init // 创建package.json
yarn add yeoman-generator // 添加生成器基类
// 创建 generators/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 // 链接到全局范围,使之成为全局模块包,这样yeoman就能找到 generator-sample
mkdir my-proj // 创建新文件夹
cd my-proj
yo sample // 运行生成器,生成temp.txt文件
根据模板创建文件
// 在 generators/app 文件夹下创建 templates(固定名称)/foo.txt 模板文件
这是一个模板文件
内部可以使用 EJS 模板标记输出数据
例如 <%= title %>
其他的 EJS 语法也支持
<% if (success) { %>
哈哈哈
<% } %>
// 在 generators/app/index.js 文件使用模板创建文件
const Generator = require("yeoman-generator");
module.exports = class extends Generator {
writing() {
// 通过模板方式写入文件到目标目录
// 模板文件路径
const tmpl = this.templatePath("foo.txt");
// 输出目标路径
const output = this.destinationPath("foo.txt");
// 模板数据上下文
const context = {
title: "Hello",
success: false,
};
this.fs.copyTpl(tmpl, output, context);
}
}
yo sample // 在 my-proj 文件夹执行命令即创建 foo.txt 文件
接收用户输入
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 => this.answers = answers)
}
writing() {
// 模板文件路径
const tmpl = this.templatePath("bar.html");
// 输出目标路径
const output = this.destinationPath("bar.html");
// 模板数据上下文
const context = this.answers;
this.fs.copyTpl(tmpl, output, context);
}
}
发布 Generator
- 将代码推送至远程 git 仓库
- yarn publish 发布到 npm (不能使用淘宝镜像,要切换至 npm 镜像,可采取 yarn publish --registry=https://registry.yarnpkg.com)
- 去 npm 官网 npmjs.com/package/项目名称 则可访问
- 如果需在 yeoman 官网中能发现此项目,可以添加 yeoman-generator 关键词
Plop
一个小而美的脚手架工具
plop的具体使用
- 将 plop 模块作为项目开发依赖安装(yarn add plop --dev)
- 在项目根目录下创建一个 plopfile.js 文件
- 在 plopfile.js 文件中定义脚手架任务
// 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 文件)
- 通过 Plop 提供的 CLI 运行脚手架任务(yarn plop component,component 代表 plop.setGenerator 方法的第一个参数)
脚手架工作原理
- 通过命令行交互询问用户问题
- 根据用户回答的结果生成文件
使用 node 搭建简易的脚手架
- 创建 sample-scaffolding 文件夹
- yarn init 生成 package.json 文件
- yarn link 全局链接
- package.json 文件添加 "bin": "cli.js"
- 在根目录下创建 cli.js,yarn 引入相应模块
#!/usr/bin/env node // Node CLI 应用入口文件必须要有这样的文件头 // 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755 // 具体就是通过 chmod 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(anwsers => { // console.log(anwsers) // 根据用户回答的结果生成文件 // 模板目录 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), anwsers, (err, result) => { if (err) throw err // 将结果写入目标文件路径 fs.writeFileSync(path.join(destDir, file), result) }) }) }) })
- 创建 demo 文件夹,执行 sample-scaffolding 命令生成模板
自动化构建
NPM Scripts
-
定义与项目相关的脚本命令, 代码与命令同时维护,方便后续开发
- 实现自动化构建工作流的最简方式
{ "name": "my-web-app", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "build": "sass scss/main.scss css/style.css --watch", // 监听 sass 变化,编译 sass "serve": "browser-sync . --files \"css/*.css\"", // 启动一个 web 服务,当文件变化刷新浏览器 "start": "run-p build serve" // 同时运行 build 和 serve 命令 }, "devDependencies": { "browser-sync": "^2.27.5", "npm-run-all": "^4.1.5", "sass": "^1.42.1" } }
常见的自动化构建工具
- Grunt:插件完善,基于临时文件实现,构建速度相对较慢,磁盘读写操作,适合灵活多变
- Gulp:很好地解决了 Grunt 构建速度很慢的问题,基于内存实现,支持多任务同时执行,生态系统完善,市面上最流行的构建系统,适合灵活多变
- FIS:百度团队推出,已集成项目中典型的需求,大而全,适合初学者
Grunt
- 基本使用
yarn init
yarn add grunt
- 创建 Grunt 的入口文件 gruntfile.js
// Grunt 的入口文件
// 用于定义一些需要 Grunt 自动执行的任务
// 需要导出一个函数
// 此函数接收一个 grunt 的对象类型的形参
// grunt 对象中提供一些创建任务时会用到的 API
module.exports = grunt => {
grunt.registerTask('foo', 'a sample task', () => {
console.log('hello grunt')
})
grunt.registerTask('bar', () => {
console.log('other task')
})
// // default 是默认任务名称
// // 通过 grunt 执行时可以省略
// grunt.registerTask('default', () => {
// console.log('default task')
// })
// 第二个参数可以指定此任务的映射任务,
// 这样执行 default 就相当于执行对应的任务
// 这里映射的任务会按顺序依次执行,不会同步执行
grunt.registerTask('default', ['foo', 'bar'])
// 也可以在任务函数中执行其他任务
grunt.registerTask('run-other', () => {
// foo 和 bar 会在当前任务执行完成过后自动依次执行
grunt.task.run('foo', 'bar')
console.log('current task runing~')
})
// 默认 grunt 采用同步模式编码
// 如果需要异步可以使用 this.async() 方法创建回调函数
// grunt.registerTask('async-task', () => {
// setTimeout(() => {
// console.log('async task working~')
// }, 1000)
// })
// 由于函数体中需要使用 this,所以这里不能使用箭头函数
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 working~')
})
grunt.registerTask('bar', () => {
console.log('bar working~')
})
// 如果一个任务列表中的某个任务执行失败
// 则后续任务默认不会运行
// 除非 grunt 运行时指定 --force 参数强制执行
grunt.registerTask('default', ['foo', 'bad', 'bar'])
// 异步函数中标记当前任务执行失败的方式是为回调函数指定一个 false 的实参
grunt.registerTask('bad-async', function () {
const done = this.async()
setTimeout(() => {
console.log('async task working~')
done(false)
}, 1000)
})
}
- 配置方法
module.exports = grunt => {
// grunt.initConfig() 用于为任务添加一些配置选项
grunt.initConfig({
// 键一般对应任务的名称
// 值可以是任意类型的数据
foo: {
bar: 'baz'
}
})
grunt.registerTask('foo', () => {
// 任务中可以使用 grunt.config() 获取配置
console.log(grunt.config('foo'))
// 如果属性值是对象的话,config 中可以使用点的方式定位对象中属性的值
console.log(grunt.config('foo.bar'))
})
}
- 多目标任务
module.exports = grunt => {
// 多目标模式,可以让任务根据配置形成多个子任务
// grunt.initConfig({
// build: {
// foo: 100,
// bar: '456'
// }
// })
// grunt.registerMultiTask('build', function () {
// console.log(`task: build, target: ${this.target}, data: ${this.data}`)
// })
grunt.initConfig({
build: {
options: {
msg: 'task options'
},
foo: {
options: {
msg: 'foo target options'
}
},
bar: '456'
}
})
grunt.registerMultiTask('build', function () {
console.log(this.options())
})
}
- 插件的使用
- npm 安装插件
- 使用 grunt.loadNpmTasks 加载插件
- grunt.initConfig 配置插件选项
module.exports = grunt => {
grunt.initConfig({
clean: {
temp: 'temp/**'
}
})
grunt.loadNpmTasks('grunt-contrib-clean')
}
Gulp
- 基本使用
yarn init
yarn add gulp --dev
- 创建 gulpfile.js 的入口函数
// gulp 的入口文件
exports.foo = done => {
console.log("foo working~");
done(); // 标识任务完成
}
exports.default = done => {
console.log("default 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'))
}
exports.promise = () => {
console.log('promise task')
return Promise.resolve()
}
exports.promise_error = () => {
console.log('promise task')
return Promise.reject(new Error('task failed'))
}
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
exports.async = async () => {
await timeout(1000)
console.log('async task')
}
exports.stream = () => {
const read = fs.createReadStream('yarn.lock')
const write = fs.createWriteStream('a.txt')
read.pipe(write)
return read
}
// exports.stream = done => {
// const read = fs.createReadStream('yarn.lock')
// const write = fs.createWriteStream('a.txt')
// read.pipe(write)
// read.on('end', () => {
// done()
// })
// }
- 构建过程核心工作原理
- 输入-读取流
- 加工-转换流
- 输出-写入流
const fs = require('fs')
const { Transform } = require('stream')
exports.default = () => {
// 文件读取流
const readStream = fs.createReadStream('normalize.css')
// 文件写入流
const writeStream = fs.createWriteStream('normalize.min.css')
// 文件转换流
const transformStream = new Transform({
// 核心转换过程
transform: (chunk, encoding, callback) => {
const input = chunk.toString()
const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
callback(null, output)
}
})
return readStream
.pipe(transformStream) // 转换
.pipe(writeStream) // 写入
}
- 文件操作 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')) }
Gulp 案例
- 创建 zce-gulp-demo 文件夹
- 样式编译:gulp-sass 将 scss 编译成 css
// gulpfile.js 文件
const { src, dest } = require("gulp")
const sass = require("gulp-sass")(require("sass"))
const style = () => {
return src("src/assets/styles/*.scss", { base: "src" })
.pipe(sass({ outputStyle: "expanded" }))
.pipe(dest("dist"))
}
module.exports = {
style,
}
- 脚本编译:gulp-babel、@babel/core、@babel/preset-env 编译 js 代码,转换 es6 新特性
const { src, dest } = require("gulp")
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 = {
script,
}
- 页面模板编译:gulp-swig 编译 html
const { src, dest, parallel } = require("gulp")
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 = () => {
return src("src/*.html", { base: "src" })
.pipe(swig({ data }))
.pipe(dest("dist"))
}
module.exports = {
page,
}
- 图片和字体文件转换
const { src, dest, parallel } = require("gulp")
const imagemin = require("gulp-imagemin")
const image = () => {
return src("src/assets/images/**", { base: "src" })
.pipe(dest("dist"))
}
const font = () => {
return src("src/assets/fonts/**", { base: "src" })
.pipe(dest("dist"))
}
const compile = parallel(style, script, page, image, font)
module.exports = {
compile,
}
- 拷贝 public 文件
const extra = () => { return src("public/**", { base: "public" }) .pipe(dest("dist")) } const compile = parallel(style, script, page, image, font) const build = parallel(compile, extra) module.exports = { compile, build, }
- 清除文件再重新构建
const { src, dest, parallel, series } = require("gulp")
const del = require("del")
const clean = () => {
return del(["dist"])
}
const compile = parallel(style, script, page, image, font)
const build = series(clean, parallel(compile, extra))
module.exports = {
compile,
build,
}
- 自动加载插件:gulp-load-plugins,运用的插件越来越多,手动加载不方便,直接使用 plugins.**(去掉 gulp- 前缀)
- 启动开发服务器:browser-sync,监视变化以及构建优化
const { src, dest, parallel, series, watch } = require("gulp")
const browserSync = require("browser-sync")
const bs = browserSync.create()
const serve = () => {
watch("src/assets/styles/*.scss", style)
watch("src/assets/styles/*.js", script)
watch("src/*.html", page)
// watch("src/assets/images/**", image)
// watch("src/assets/fonts/**", font)
// watch("pubilc/*", extra)
watch([
"src/assets/images/**",
"src/assets/fonts/**",
"pubilc/*",
], bs.reload)
bs.init({
notify: false,
port: 2080,
// 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)
- 根据构建注释构建css、js路径
const useref = () => {
return src("dist/*.html", { base: "src" })
.pipe(plugins.useref({ searchPath: ["dist", "."] }))
.pipe(dest("dist"))
}
- 文件压缩:gulp-if 判断文件类型;gulp-htmlmin 压缩 html;gulp-cleanCss 压缩 css;gulp-uglify 压缩 js
const useref = () => {
return src("dist/*.html", { base: "dist" })
.pipe(plugins.useref({ searchPath: ["dist", "."] }))
// html js css
.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"))
}
- 重新规划构建过程:开发 develop 先把文件(html、css、js)放至 temp,经过处理再放至 dist
- 在 package.json 暴露给外部使用的命令
"scripts": {
"clean": "gulp clean",
"build": "gulp build",
"develop": "gulp develop"
},
- 在 .gitignore 文件添加 temp dist
封装自动化构建工作流
- 创建一个 node 脚手架,将其发布 npm ,新项目文件夹 zce-pages 使用 npm (可参考已发布的脚手架 caz )
- 将 gulpfile.js 代码替换到入口文件 lib/index.js
- 将 zce-gulp-demo 文件夹中的 package.json 依赖拷贝到新 package.json dependencies 依赖
- yarn link 到全局
- 在 zce-gulp-demo 文件夹运行 yarn link zce-pages
- 在 lib/index.js 提取公共配置文件
const cwd = process.cwd() let config = { // default config build: { src: "src", dist: "dist", temp: "temp", public: "public", paths: { style: "assets/styles/*.scss", scripts: "src/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(error) {}
- 在 zce-gulp-demo 文件夹创建 pages.config.js 暴露 build、data,用于项目的数据、构建等专用配置
- 包装 Gulp CLI:创建 bin/zce-pages.js
#!/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")
- 发布:package.json files 添加 "bin";yarn publish
- 同步时间差问题:nap.taobao.org 找到刚发布的 zce-pages,点击 SYNC
- 使用发布的构建包:yarn zce-pages build(clean/develop),也可以在新项目中的 package.json 中使用 scripts 声明