文章内容输出来源:拉勾教育大前端高薪训练营
工程化概述
-
前端面临的问题
- ES6+ 兼容问题
- Less、Sass等 不支持
- 模块化、组件化不支持
- 手动压缩
- 手动上传代码
- 多人开发,难以统一风格
- 等待后端接口完成
- …
-
工程化主要解决的问题
- 传统语言或语法的弊端
- 无法使用模块化、组件化
- 重复的机械式工作
- 代码风格统一、质量保证
- 依赖后端服务接口支持
- 整体依赖后端项目
-
工程化的表现
- 一切重复的工作都应该被自动化
- 一切重复的工作都应该被自动化
-
工程化不等于某个工具
- vue-cli
- create-react-app
- angular-cli
- …
-
工程化与node
- 前端工程化由node驱动
脚手架工具开发
-
脚手架的本质
- 创建项目基础结构、提供项目规范和约定
- 相同的组织结构
- 相同的开发范式
- 相同的模块依赖
- 相同的工具配置
- 相同的基础代码
- 前端脚手架
- 创建项目基础结构、提供项目规范和约定
-
常用的脚手架工具
- create-react-app
- vue-cli
- angular-cli
- yeoman
- plop(用于创建特定类型文件)
-
yeoman
- 基本使用
- 在全局安装yo
yarn global add yo
- 安装对应的generator 如:生成node模块
yarn global add generator-node
- 通过yo运行generator
yo node
- 在全局安装yo
- generator
- sub generator
yo node:cli
- sub generator
- 基本使用
-
yeoman 常规使用步骤
- 明确需求
- 找到合适的generator
- 全局安装找到generator
- 通过yo运行对应的generator
- 通过命令式交互行填写选项
- 生成所需的项目结构
-
自定义generator (基于yeoman)的实现
-
创建generator本质上就是创建一个npm模块
-
generator基本结构
-
名称约定
generator-<name>
-
操作
- 新建目录generator-sample
- yarn init
- yarn add yeoman-generator
- 按目录结构创建文件
// ./generator-sample/generators/app/index.js /** 此文件作为generator的核心入口 * 需要导出一个继承自Yeoman Generator的类型 * Yeoman Generator 在工作时会自动调用我们在此类型中定义的一些生命周期方法 * 我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能,如文件写入 */ const Generator = require('yeoman-generator') module.exports = class extends Generator{ // Yeoman在询问用户环节会自动调用该方法 // 该方法中可以调用父类的prompt方法发出对用户的命令行询问 prompting(){ return this.prompt([ { type:'input', name:'name', message:'your project name', default:this.appname // appname 为项目生成目录名称 } ]).then(answers=>{ // 用户输入 this.answers = answers; }) } writing(){ // this.fs.write(this.destinationPath('temp.txt'),Math.random().toString()) // 通过模板的方式写入文件到目标目录 const tmpl = this.templatePath('bar.txt') const output = this.destinationPath('bar.txt') // const context = {title:'模板文件名称',success:false} const context = this.answers; this.fs.copyTpl(tmpl,output,context) } }
- npm link
- yo sample
-
根据模块创建文件
-
接收用户输入
-
vue Generator案例
const Generator = require('yeoman-generator') module.exports = class extends Generator { prompting() { console.log('123'); return this.prompt([ { type: 'input', name: 'name', message: 'your project name', default: this.appname } ]).then(answers => { this.answers = answers }) } writing() { const templates = [ 'README.md', 'package.json', 'babel.config.js', '.gitignore', 'public/favicon.ico', 'public/index.html', 'src/App.vue', 'src/main.js', 'src/assets/logo.png', 'src/components/HelloWorld.vue' ] templates.forEach(item => { this.fs.copyTpl(this.templatePath(item), this.destinationPath(item), this.answers) }) } }
- 注意 使用
yo <name>
之前- 使用npm link
- 安装yeoman-generator
- 注意 使用
-
发布generator(实际就是发布npm模块)(淘宝镜像是只读的)
- npm public
- yarn public --registry=https://registry.yarnpkg.com
-
-
plop (小型脚手架工具)
-
重复创建相同类型的文件
-
基本使用
-
npm i plop -D
-
plopfile.js文件 – 定义任务
//Plop 入口文件,需要导出一个函数 接受一个plop对象 ,用于创建生成器任务 ./plop/plopfile.js module.exports = plop =>{ plop.setGenerator('component',{ description:'create a component', prompts:[ { type:'input', name:'name', message:'componet name', default:'MyComponent' } ], actions:[ { type:'add',//添加文件 path:'src/com/{{name}}//{{name}}.vue', templateFile:'plop-templates/components.hbs' } ] }) }
-
编写模块
// /plop/components.hbs <template> <div id="app" class="{{name}}"> <HelloWorld msg="Welcome to Your Vue.js App"/> </div> </template> <script> export default { name: 'App', components: { HelloWorld } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
- yarn plop component
-
-
-
脚手架的工作原理
-
实操
- 创建目录cli-test
- npm init
- 修改package.json 增加
"bin": "cli.js",
- 创建cli.js文件
#! /usr/bin/env node // Node cli 应用入口文件必须有这样的文件头 // 如果是Linux或者macOS系统下还需要修改此文件的读写权限为 755 // 具体就是通过chmod 755 cli.js 修改 // /cli-test/cli.js // console.log('cli working'); // 脚手架的工作过程 // 1、通过命令行交互询问用户问题 // 2、根据用户回答生成文件 const inquirer = require('inquirer') const path = require('path') const fs = require('fs') const ejs = require('ejs') inquirer.prompt([ { type:'input', name:'name', message:'Project name ?' } ]).then(answer =>{ console.log(answer); // 模板路径 const temlDir = path.join(__dirname,'templates') // 输出路径 const distDir = process.cwd(); fs.readdir(temlDir,(err,files)=>{ if(err) throw err; files.forEach(file=>{ // 通过模板引擎渲染文件 ejs.renderFile(path.join(temlDir,file),answer,(err,res)=>{ if(err) throw err; fs.writeFileSync(path.join(distDir,file),res) }) }) }) })
- npm link
- 命令行运行 cli-test
-
原理
- 通过命令行交互询问用户问题
- 根据用户回答生成文件
-
自动化构建系统
源代码—(自动化构建)–>生产代码
1、初体验
- 创建auto-build目录
- 创建index.html \ main.scss文件
- 安装sass
.\node_modules\.bin\sass
查看sass命令.\node_modules\.bin\sass .\part02\module01\auto-build\main.scss .\part02\module01\auto-build\style.css
编译scss文件为css文件- 安装browser-sync
- 配置npm script
- 安装npm-run-all(run-p 命令)
"build":"sass .\\part02\\module01\\auto-build\\main.scss .\\part02\\module01\\auto-build\\style.css",
"serve":"browser-sync ./part02/module01/auto-build",
"start":"run-p build serve"
2、常用的自动化构建工具
- grunt
- 构建速度慢
- gulp
- 同时执行多个任务
- 生产文件存于内存
- fis
- 资源加载、性能优化等集成
3、grunt
-
grunt 基本使用
- 安装grunt
- 目录下新建gruntfile.js文件
/** * grunt 的入口文件 * 用于定义一些需要grunt自动执行的任务 * 需要导出一个函数 此函数有一个grunt参数, * 内部提供一些创建任务时可以用的的API * ./grunt-demo/gruntfile.js * */ const sass = require('sass') const loadGruntTasks = require('load-grunt-task') module.exports = grunt =>{ grunt.initConfig({ babel:{ options:{ 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'] } } }) loadGruntTasks(grunt) // grunt.initConfig({ // sass:{ // options:{ // sourceMap:true, // implementation:sass // }, // main:{ // files:{ // 'dist/css/main.css':'src/scss/main.scss' // } // } // } // }) // grunt.loadNpmTasks('grunt-sass') grunt.registerTask('foo',()=>{ console.log('hello grunt'); }) grunt.registerTask('bar','任务描述',()=>{ console.log('hello grunt bar'); }) // grunt.registerTask('default',()=>{ // console.log('默认任务'); // }) // grunt.registerTask('default',['foo','bar']) // 异步任务 grunt.registerTask('async-task',function(){ const done = this.async(); setTimeout(()=>{ console.log('async task'); done() },1000) }) // 任务失败 grunt.registerTask('bad',()=>{ console.log('任务失败'); return false }) // 任务失败后的任务列表不执行 可以用过--force命令强制执行 grunt.registerTask('default',['foo','bad','bar']) // 异步任务失败 grunt.registerTask('async-bad',function(){ const done = this.async(); setTimeout(()=>{ console.log('async bad'); done(false) },1000) }) // 配置方法 // grunt.initConfig({ // foo:'bar' // }) // grunt.registerTask('config',()=>{ // console.log(grunt.config('foo')); // }) // 多目标任务 options不会当做子任务 而是配置 子任务的option会覆盖上层的options // grunt.initConfig({ // build:{ // options:{ // foo:'foo' // }, // js:1, // css:{ // options:{ // foo:'bar' // } // } // } // }) // grunt.registerMultiTask('build',function(){ // console.log(this.options()); // console.log(`target:${this.target},data:${this.data}`); // }) // 插件的使用 grunt.initConfig({ clean:{ // temp:'temp/app.js', // temp:'temp/*.js', temp:'temp/**' } }) grunt.loadNpmTasks('grunt-contrib-clean') }
yarn grunt foo
执行
-
grunt 标记任务失败
- return false
- done(false)异步任务
grunt.registerTask('bad',()=>{ console.log('任务失败'); return false }) // 任务失败后的任务列表不执行 可以用过--force命令强制执行 grunt.registerTask('default',['foo','bad','bar']) // 异步任务失败 grunt.registerTask('async-bad',function(){ const done = this.async(); setTimeout(()=>{ console.log('async bad'); done(false) },1000) })
-
grunt的配置方法
// 配置方法
// grunt.initConfig({
// foo:'bar'
// })
// grunt.registerTask('config',()=>{
// console.log(grunt.config('foo'));
// })
- grunt多目标任务
// 多目标任务 options不会当做子任务 而是配置 子任务的option会覆盖上层的options grunt.initConfig({ build:{ options:{ foo:'foo' }, js:1, css:{ options:{ foo:'bar' } } } }) grunt.registerMultiTask('build',function(){ console.log(this.options()); console.log(`target:${this.target},data:${this.data}`); })
- grunt插件的使用
- 安装grunt-contrib-clean
- grunt.loadNpmTasks(‘grunt-contrib-clean’) 加载
grunt.initConfig({ clean:{ // temp:'temp/app.js', // temp:'temp/*.js', temp:'temp/**' } }) grunt.loadNpmTasks('grunt-contrib-clean')
- grunt常用插件
- grunt-sass
- 依赖sass模块
- grunt-sass
const sass = require('sass')
module.exports = grunt =>{
grunt.initConfig({
sass:{
options:{
implementation:sass
},
main:{
file:{
'dist/css/main.css':'src/scss/main.scss'
}
}
}
})
grunt.loadNpmTasks('grunt-sass')
}
- grunt-babel
- 依赖@babel/core @babel/preset-env
const loadGruntTasks = require('load-grunt-task') // 自动加载
module.exports = grunt =>{
grunt.initConfig({
babel:{
options:{
presets:['@babel/preset-env']
},
main:{
files:{
'dist/js/app.js':'src/js/app.js'
}
}
}
})
loadGruntTasks(grunt)
}
- grunt-contrib-watch
watch:{
js:{
files:['src/js/*.js'],
tasks:['babel']
},
css:{
files:['src/scss/*.scss'],
tasks:['sass']
}
}
4、gulp
-
基本使用
- 安装gulp
- 创建入口文件 gulpfile.js文件
// 实现这个项目的构建任务 const { src, dest ,watch,series,parallel} = require('gulp') const del = require('del') // const sass = require('gulp-sass') // const babel = require('gulp-babel') // const imagemin = require('gulp-imagemin') // const swig = require('gulp-swig') // const ifg = require('gulp-if') // const useref = require('gulp-useref') // const uglify = require('gulp-uglify') // const cleanCss = require('gulp-clean-css') const gulpLoadPlugins = require('gulp-load-plugins') const plugins = gulpLoadPlugins() // const browserSync = require('browser-sync') const bs = browserSync.create() const data = require('./package.json') // 编译scss const style = () => { return src('src/assets/styles/*.scss') .pipe(plugins.sass({ outputStyle: 'expanded' })) .pipe(dest('dist/css')) } // 编译JS const js = () => { return src('src/assets/scripts/*.js') .pipe(plugins.babel({ presets: ['@babel/preset-env'] })) .pipe(dest('dist/js')) } // 处理图片 const img = () => { return src('src/assets/images/**') .pipe(plugins.imagemin()) .pipe(dest('dist/img')) } // 处理字体 const font = () => { return src('src/assets/fonts/**') .pipe(plugins.imagemin()) .pipe(dest('dist/font')) } // 拷贝静态资源 const extra = () => { return src('public/**') .pipe(dest('dist/public')) } // 处理HTML文件 const html = ()=>{ return src(['src/*.html','src/layouts/*.html','src/partials/*.html']) .pipe(plugins.swig({data:{pkg:data}})) .pipe(dest('dist')) } // 清除构建后的目录 const clean = () => { return del('dist') } const server = ()=>{ watch('src/assets/styles/*.scss',style) watch('src/assets/scripts/*.js',js) watch(['src/assets/fonts/**','src/assets/images/**'],bs.reload) bs.init({ notify:false, files:'dist', server:{ baseDir:['dist','src','public'], routes:{ '/node_modules':'node_modules' } } }) } // 文件引用处理 const ref = ()=>{ return src('dist/*.html') .pipe(plugins.useref({searchPath:['dist','.']})) .pipe(plugins.if(/\.js$/,plugins.uglify())) .pipe(plugins.if(/\.html$/,plugins.htmlmin({collapseWhitespace:true,minifyCSS:true,minifyJS:true}))) .pipe(plugins.if(/\.css$/,plugins.cleanCss())) .pipe(dest('dist')) } const develop = series(parallel(style,js,html),ref, server) const build = series(clean,parallel(style,js,html,img,extra,font)) module.exports = { clean, develop, build }
yarn gulp <taskName>
-
组合任务
const {series,parallel} = require('gulp')
const task1 = done=>{
setTimeout(()=>{
console.log('task1');
done();
},1000)
}
const task2 = done=>{
setTimeout(()=>{
console.log('task2');
done();
},1000)
}
const task3 = done=>{
setTimeout(()=>{
console.log('task3');
done();
},1000)
}
exports.foo = series(task1,task2,task3); // 串行
exports.bar = parallel(task1,task2,task3);//并行同步执行
- 异步任务
const fs = require('fs')
exports.cb = done=>{
console.log('cb task');
done()
}
exports.cb_error = done =>{
console.log();
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 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')
exports.default = ()=>{
// 文件读取
const read = fs.createReadStream('a.css')
// 文件写入流
const write = fs.createWriteStream('a.min.css')
const transform = new Transform({
transform:(chunk,encoding,cb)=>{
const input = chunk.toString();
const output = input.replace(/\s+/g,'').replace(/\/\*.+?\*\//g,'')
cb(null,output)
}
})
// 把读取出来的文件流导入写入文件流
read.pipe(transform).pipe(write);
return read
}
- 文件操作API + 插件的使用
const {src,dest} = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')
exports.default = ()=>{
return src('a.css')
.pipe(cleanCss())
.pipe(rename({extname:'.min.css'}))
.pipe(dest('dist'))
}
- 案例:样式编译
const { src, dest } = require('gulp')
const sass = require('gulp-sass')
const style = () => {
return src('src/assets/style/*.scss', { base: 'src' })
.pipe(sass({outputStyle:'expanded'}))
.pipe(dest('dist'))
}
module.exports = {
style
}
- 案例:脚本编译
const babel = require('gulp-babel')
const script = ()=>{
return src('src/assets/scripts/*.js', { base: 'src' })
.pipe(babel({presets:['@babel/preset-env']}))
.pipe(dest('dist'))
}
- 案例:页面模板编译
const swig = require('gulp-swig')
const page = ()=>{
return src('src/*.html', { base: 'src' })
.pipe(swig())
.pipe(dest('dist'))
}
- 案例:图片和字体文件转换
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/font/**', { base: 'src' })
.pipe(imagemin())
.pipe(dest('dist'))
}
- 案例:其他文件及文件清除
// 主要是拷贝文件
const extra = ()=>{
return src('public/**', { base: 'public' })
.pipe(dest('dist'))
}
// del插件
const del = require('del')
const clean = ()=>{
return del(['dist'])
}
// 构建之前清除
const build = series(clean,parallel(compile,extra))
- 案例:自动加载插件
const gulpLoadPlugins = require('gulp-load-plugins')
const plugins = gulpLoadPlugins()
// 使用时 通过plugins.sass 的方式
const style = () => {
return src('src/assets/style/*.scss', { base: 'src' })
.pipe(plugins.sass({ outputStyle: 'expanded' }))
.pipe(dest('dist'))
}
- 案例:开发服务器 (热更新)
const browserSync = require('browser-sync')
const bs = browserSync.create()
const server = () => {
bs.init({
server:{
baseDir:'dist'
}
})
}
- 案例:监视变化及构建优化
const { src, dest, parallel ,series,watch} = require('gulp')
const server = () => {
watch('src/assets/style/*.scss',style)
watch('src/assets/scripts/*.js',script)
watch(['src/assets/images/**','src/assets/font/**'],bs.reload)
bs.init({
notify:false,//关闭右上角的tip
port:2080,//端口
open:false,//关闭自动打开浏览器
files:'dist/**',// 监听的目录
server:{
baseDir:'dist',
routes:{
'/node_modules':'node_modules'
}
}
})
}
- 案例:useref文件引用处理
<!-- 构建注释 -->
<!-- build:css assets/styles/vendor.css -->
<link rel='stylesheet' href='node_modules/bootstrap/dist/css/bootstrap.css'>
<!-- endbuild-->
const useref = ()=>{
return src('dist/*.html',{base:'dist'})
.pipe(plugins.useref({searchPath:['dist','.']}))
.pipe(dest('dist'))
}
- 案例:文件压缩
const useref = ()=>{
return src('dist/*.html',{base:'dist'})
.pipe(plugins.useref({searchPath:['dist','.']}))
// html js css
// gulp-html gulp-uglify gulp-clean-css
.pipe(plugins.if(/\.js$/,plugins.uglify()))
.pipe(plugins.if(/\.html$/,plugins.htmlmin({collapseWhitespace:true})))
.pipe(plugins.if(/\.css$/,plugins.cleanCss()))
.pipe(dest('release'))
}
useref打乱了之前的构建流程。
5、封装自动化构建工作流
-
准备
- 新建一个项目(含远程仓库)
- 安装zce-cli
-
提取gulpfile
- 将gulp-demo中的gulpfile.js 文件内容复制到gxw-pages项目下的lib/index.js(入口文件)中
- 将gulp-demo中的package.json中安装的依赖复制到gxw-pages的package.json的dependencies
- 删除gulp-demo项目中的依赖、清空gulpfile.js
- gxw-pages项目通过yarn link 链接到本地全局
- 在gulp-demo项目中通过
yarn link "gxw-pages"
链接到本项目 - gulp-demo项目中gulpfile.js添加代码
module.exports = require('gxw-pages')
- gulp-demo项目中安装一下依赖(原本项目依赖)
- 安装一下gulp-cl、gulp
- 运行脚本
yarn gulp clean
-
解决模块中的问题
- gulp-demo项目中创建page.config.js文件(目的是抽离出一些配置信息)
- 在gxw-pages项目中lib/index.js加载配置文件
const cwd = process.cwd();// 返回当前命令行工作目录 let config = { // default config } try { const loadConfig = require(`${cwd}/pages.config.js`) config = Object.assign({},config,loadConfig) } catch (error) { }
- 将gxw-pages项目中lib/index.js用到的相关配置改成加载过来的数据
const page = () => { return src('src/*.html', { base: 'src' }) .pipe(swig({ data: config.data })) .pipe(dest('dist')) }
-
抽象路径配置
- 把写死的路径改成可配置的
let config = { // default config build:{ src:'src', dist:'dist', temp:'temp', public:'public', paths:{ styles:'assets/style/*.scss', scripts:'assets/scripts/*.js', pages:'*.html', images:'assets/images/**', fonts:'assets/font/**' } } } const style = () => { return src(config.build.paths.styles, { base: config.build.src,cwd:config.build.src}) .pipe(sass({ outputStyle: 'expanded' })) .pipe(dest('dist')) }
-
包装gulp cli
- gulp-demo项目中gulpfile.js删除
yarn gulp build --gulpfile .\node_modules\gxw-pages\lib\index.js
- yarn gulp build --gulpfile .\node_modules\gxw-pages\lib\index.js --cwd . (指定当前目录为工作目录)
- 上面的方法传参太多
- 解决:在gxw-pages项目中提供一个cli
- 在gxw-pages项目新建bin/gxw-pages.js
- 在package.json中配置"bin":“bin/gxw-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')
- gxw-pages clean·
-
发布使用 gwx-pages
- package.json文件files增加
"files": [ "lib", "bin" ],
- npm publish (要先登录)
- npm i gwx-pages 在其他项目中使用
6、Fis (高度集成、内置webserver)
- 基本使用
- 安装fis3
- yarn fis3 release (默认构建任务)
- yarn fis3 release -d dist (指定输出目录)
- 配置文件fis-conf.js
- 编译与压缩
- yarn fis3 inspect 查看编译过程
// 安装 fis-parser-node-sass
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')
})