腳手架工具
常用的腳手架工具:
- React.js 項目 - create-react-app
- Vue.js 項目 - vue-cli
- Angular 項目 - angular-cli
- Yeoman
- Plop
Yeoman
Yeoman 是一款比較通用的腳手架,可以搭配不同的 generator 生成不同的腳手架。
基礎使用
- 在全局范圍安裝 yo
yarn global add yo
- 安裝對應的 generator扁藕,這里安裝的是 node 對應的 generator
yarn global add generator-node
- 生成項目結(jié)構(gòu)
mkdir my-module
cd my-module
yo node
yarn
Sub Generator
有的時候我們并不需要創(chuàng)建完整的項目結(jié)構(gòu),只是需要在已有項目基礎上創(chuàng)建特定文件吮旅,比如 readme,eslint等味咳,這些文件都有特定的格式庇勃,如果自己手動寫很容易寫錯,可以通過生成器自動生成槽驶,這個時候可以使用 Yeoman 提供的 Sub Generator 來實現(xiàn)责嚷。
具體就是通過運行 Sub Generator 的命令來實現(xiàn)。我們這里可以用 generator-node 里邊所提供的的一個子集的生成器 cli 幫我們生成一個 cli 應用所需要的的文件掂铐。
運行 sub generator 的方式就是在原有的 generator 名字后面跟上 :罕拂,跟上 sub generator 的名字,在這里就是 node:cli
yo node:cli
將模塊作為全局的命令行模塊去使用
yarn link
運行my-module --help
這就是 Generator 的子集 Sub Generator 的特性全陨。
并不是每個 Generator 都提供子集的生成器爆班,可以通過 Generator 的官方文檔查看它有哪些自己生成器。
常規(guī)使用步驟
- 明確你的需求
- 找到合適的 Generator
- 全局范圍安裝找到的 Generator
- 通過 Yo 運行對應的 Generator
- 通過命令行交互填寫選項
- 生成你所需要的項目結(jié)構(gòu)
自定義 Generator
Generator 本質(zhì)上就是一個NPM模塊辱姨。
簡單實現(xiàn)
- 首先創(chuàng)建生成器文件夾柿菩,初始化 package.json 文件
mkdir generator-sample
cd generator-sample
yarn init
注意: yarn init 在 git bash 不生效,可以在 cmd 中執(zhí)行雨涛。
- 安裝依賴 yeoman-generator@4.0.1
yarn add yeoman-generator@4.0.1
- 在根目錄下創(chuàng)建文件 generators/app/index.js
- 在 index.js 文件中實現(xiàn)一個文件寫入的功能
// 此文件作為 Generator 的核心入口
// 需要導出一個繼承自 Yeoman Generator 的類型
// Yeoman Generator 在工作時會自動調(diào)用在此類型中定義的一些生命周期方法
// 我們在這些方法中可以通過調(diào)用父類提供的一些工具方法實現(xiàn)一些功能枢舶,比如文件寫入
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
write() {
// Yeoman 在生成文件時調(diào)用此方法
// 這里給項目中寫入一個文件
this.fs.write(
this.destinationPath('tmp.txt'),
Math.random().toString()
)
}
}
- 使用
yarn link
將這個模塊鏈接到全局范圍,使它成為一個全局模塊包镜悉。 - 創(chuàng)建一個新的項目 my-proj,在 my-proj 中運行
yo sample
就會在 my-proj 項目下生成一個 tmp.txt 文件。
根據(jù)模板創(chuàng)建文件
在 app 文件夾下創(chuàng)建 templates 目錄医瘫,將要生成的文件都放入 templates 目錄作為模板侣肄,模板中是完全遵循 EJS 語法。
這個時候我們寫入文件就不需要借助 fs 的 write 方法了醇份,而是借助 fs 的一個專門使用模板引擎的方法 copyTpl稼锅。
// 模板文件路徑
const tmpl = this.templatePath('foo.txt')
// 輸出目標路徑
const output = this.destinationPath('foo.txt')
// 模板數(shù)據(jù)上下文
const context = { title: 'Hello wl~', success: false }
this.fs.copyTpl(tmpl, output, context)
然后進入命令行吼具,在 my-proj 項目下運行 yo sample,會在項目下生成文件 foo.txt矩距。
相對于手動創(chuàng)建每一個文件拗盒,模板的方式大大提高了效率。
接受用戶輸入數(shù)據(jù)
對于項目的動態(tài)數(shù)據(jù)锥债,例如標題陡蝇,名稱,這樣的數(shù)據(jù)一般通過命令行交互的方式詢問我們的使用者從而得到哮肚。 在 Generator 中想要實現(xiàn)命令行交互可以用 Generator 中的 prompting 方法登夫。
// 此文件作為 Generator 的核心入口
// 需要導出一個繼承自 Yeoman Generator 的類型
// Yeoman Generator 在工作時會自動調(diào)用在此類型中定義的一些生命周期方法
// 我們在這些方法中可以通過調(diào)用父類提供的一些工具方法實現(xiàn)一些功能,比如文件寫入
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
prompting() {
// Yeoman 在詢問用戶環(huán)節(jié)會自動調(diào)用此方法
// 在此方法中可以調(diào)用父類的 prompt() 方法發(fā)出對用戶的命令行詢問
// this.prompt 方法接收一個數(shù)組作為參數(shù)允趟,數(shù)組的每一項都是一個問題對象
// 這個問題對象中 type 表示用什么方式接受信息恼策, name 為得到結(jié)果的鍵
// message 是給用戶的提示,也就是問題潮剪,default: this.appname 拿到的是當前生成項目的文件夾的名字
// answers 就是拿到的結(jié)果涣楷,鍵就是數(shù)組中每一項設置的 name,值就是用戶的輸入抗碰。
// 將結(jié)果掛載到 this.answers 上狮斗,以便后續(xù)使用。
return this.prompt([
{
type: 'input',
name: 'projectName',
message: 'Your project name',
default: this.appname // appname 為項目生成目錄名稱
}
])
.then(answers => {
// answers => { projectName: 'user input value'}
this.answers = answers
})
}
write() {
// Yeoman 自動在生成文件時調(diào)用此方法
// 這里給項目中寫入一個文件
// this.fs.write(
// this.destinationPath('tmp.txt'),
// Math.random().toString()
// )
// 通過模板方式寫入文件到目標目錄
// 模板文件路徑
const tmpl = this.templatePath('bar.html')
// 輸出目標路徑
const output = this.destinationPath('bar.html')
// 模板數(shù)據(jù)上下文
const context = this.answers
this.fs.copyTpl(tmpl, output, context)
}
}
然后在 my-proj 項目下執(zhí)行 yo sample改含,就創(chuàng)建了 bar.html 文件情龄,并且拿到了項目名稱。
Vue Generator 案例
mkdir generator-vue-wl
cd generator-vue-wl
yarn init
yarn add yeoman-generator@4.0.1
在項目中添加文件 generators/app/index.js
在項目中添加文件夾 templates捍壤,將準備好的 vue 的模板放入這個文件夾中骤视,將模板中名字的不用使用<%= name %>
替代,eg:
在 index.js 文件中寫入代碼:
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
prompting() {
return this.prompt([
{
type: 'input',
name: 'name',
message: 'Your project name',
default: this.appname
}
])
.then(answers => {
this.answers = answers
})
}
writing() {
// 把每一個文件都通過模板轉(zhuǎn)換到目標路徑
const templates = [
'.browserslistrc',
'.editorconfig',
'.env.development',
'.env.production',
'.eslintrc.js',
'.gitignore',
'babel.config.js',
'package.json',
'postcss.config.js',
'README.md',
'public/favicon.ico',
'public/index.html',
'src/App.vue',
'src/main.js',
'src/router.js',
'src/assets/logo.png',
'src/components/HelloWorld.vue',
'src/store/actions.js',
'src/store/getters.js',
'src/store/index.js',
'src/store/mutations.js',
'src/store/state.js',
'src/utils/request.js',
'src/views/About.vue',
'src/views/Home.vue'
]
// 通過遍歷所有的文件路徑鹃觉,為每個模板生成對應文件
templates.forEach(item => {
// item => 每個文件路徑
this.fs.copyTpl(
this.templatePath(item),
this.destinationPath(item),
this.answer
)
})
}
}
在命令行執(zhí)行 yarn link专酗。然后定位到全新的目錄,創(chuàng)建新項目盗扇。
mkdir vue-demo
cd vue-demo
yo vue-wl
發(fā)布 Generator
發(fā)布 Generator 就是發(fā)布一個公開的 npm祷肯。
- 將自己的 Generator 項目提交到遠程倉庫
- 在項目下使用
npm publish
或yarn publish
Plop
除了 Yeoman 這樣大型的腳手架工具外,還有一些小型的腳手架工具也很好用疗隶,比如 Plop佑笋。它是用于在項目中創(chuàng)建特定類型文件的小工具,類似于 Yeoman 中的 Sub Generator斑鼻。不過它一般不會獨立去使用蒋纬,都是集成到項目中,用來自動化的創(chuàng)建同類型的項目文件。
步驟:
- 將 plop 模塊作為項目開發(fā)依賴安裝
- 在項目根目錄下創(chuàng)建一個 plopfile.js 文件
- 在 plopfile.js 文件中定義腳手架任務
- 編寫用于生成特定類型文件的模板
- 通過 Plop 提供的 CLI 運行腳手架任務
demo:https://gitee.com/sun_wl/plop-demo.git
通過 nodejs 開發(fā)一個小型腳手架
腳手架工具就是一個 node cli 應用蜀备。
mkdir node-scaffold
cd node-scaffold
yarn init
在 package.json 中添加一個 bin 字段关摇,用于指定 cli 應用的入口文件,然后創(chuàng)建這個文件碾阁。
自動化構(gòu)建
常見自動化構(gòu)建工具介紹
- Grunt 最早的前端構(gòu)建系統(tǒng)输虱,插件生態(tài)非常完善。但是工作過程基于臨時文件脂凶,所以構(gòu)建速度相對較慢宪睹。例如使用它區(qū)完成項目中 sass 文件的構(gòu)建,先對文件進行編譯艰猬,再去自動添加一些私有屬性的前綴横堡,最后壓縮代碼。每一步都會有磁盤操作冠桃,sass 文件編譯完成命贴,會將結(jié)果寫入一個臨時文件,然后下一個插件在讀取臨時文件進行下一步食听,處理的環(huán)節(jié)越多胸蛛,讀取磁盤次數(shù)越多,大型項目就會特別慢樱报。
- Gulp 很好的解決了Grunt 構(gòu)建速度慢的問題葬项,它處理文件都是在內(nèi)存中完成的,相對于磁盤讀寫快了很多迹蛤,而且它默認支持同時執(zhí)行多個任務民珍,使用方式也簡單易懂,生態(tài)系統(tǒng)也很多盗飒,是目前市面上最流行的前端構(gòu)建系統(tǒng)嚷量。
- FIS 百度開源的構(gòu)建系統(tǒng)。相對于前兩個微內(nèi)核的特點逆趣,F(xiàn)IS更像捆綁套餐蝶溶,把項目中典型的需求都集成在內(nèi)部了。
webpack 是一個模塊打包工具宣渗。
Grunt
首先在項目中安裝 grunt 依賴抖所,在根目錄創(chuàng)建 gruntfile.js 文件:
// Grunt 的入口文件
// 用于定義一些需要 Grunt 自動執(zhí)行的任務
// 需要導出一個函數(shù)
// 此函數(shù)接受一個 grunt 的形參,內(nèi)部提供一些創(chuàng)建任務時可以用到的 API
module.exports = grunt => {
// registerTask: 用于注冊任務
// 注冊一個 foo 任務
// 使用 yarn grunt foo 執(zhí)行
grunt.registerTask('foo', () => {
console.log('hello grunt~')
})
// 第二個參數(shù)如果是字符串痕囱,就是這個任務的描述
// 可以通過 yarn grunt --help 看到描述信息
grunt.registerTask('bar', '任務描述', () => {
console.log('other task~')
})
// 如果在構(gòu)建任務的邏輯代碼中發(fā)生錯誤田轧,例如文件找不見,可以將任務標記為失敗的任務
// 實現(xiàn)方式是在函數(shù)中 return false
grunt.registerTask('bad', () => {
console.log('bad working')
return false
})
// 執(zhí)行 yarn grunt 自動調(diào)用 default
// grunt.registerTask('default', () => {
// console.log('default task')
// })
// default 任務的第二個參數(shù)可以傳入一個數(shù)組作為映射
// 自動執(zhí)行數(shù)組中的任務
// grunt.registerTask('default', ['foo', 'bar'])
// 如果失敗的任務在任務列表中鞍恢,會導致后續(xù)的任務不會再執(zhí)行
// 如果想要失敗任務后的任務仍然能夠執(zhí)行傻粘,可以執(zhí)行 yarn grunt --force
grunt.registerTask('default', ['foo', 'bad', 'bar'])
// grunt 默認支持同步模式巷查,所以這里的 log 不會被打印
// grunt.registerTask('async-task', () => {
// setTimeout(() => {
// console.log('async task working')
// }, 1000)
// })
// 想要執(zhí)行異步任務,需要使用 this.async() 得到一個回調(diào)函數(shù)
// 在異步操作完成后調(diào)用這個回調(diào)函數(shù)抹腿,標識一下這個任務已經(jīng)完成
// 要使用 this 就不能使用箭頭函數(shù)了
grunt.registerTask('async-task', function () {
const done = this.async()
setTimeout(() => {
console.log('async task working')
done()
}, 1000)
})
// 異步任務無法通過 return false 標記失敗,可以通過給回調(diào)函數(shù)傳遞一個 false
grunt.registerTask('bad-async', function () {
const done = this.async()
setTimeout(() => {
console.log('bad-async')
done(false)
}, 1000)
})
// initConfig:用于添加配置選項的 API
// 例如grunt需要對文件進行壓縮時旭寿,可以通過這個配置需要壓縮的文件路徑
// 接受一個參數(shù)為對象警绩,對象的鍵和任務的名稱保持一致,值可以是任意類型的數(shù)據(jù)
// grunt.config() 可以獲取配置的值
// 值也可以是個對象盅称,在使用 grunt.config() 獲取時使用 . 的形式
grunt.initConfig({
wl: 'bar'
// wl:{
// bar:123
// }
})
grunt.registerTask('wl', () => {
console.log(grunt.config('wl')) // bar
// console.log(grunt.config('wl.bar')) // 123
})
// 多目標任務
// 使用 grunt.registerMultiTask 注冊多目標任務
// 多目標任務必須通過 initConfig 來配置這個任務和它的多目標
// 執(zhí)行 yarn grunt build 會執(zhí)行 build 中配置的多個任務
// 要運行指定目標可以通過 yarn grunt build:js 來執(zhí)行
// 在任務的函數(shù)中可以通過 this.target 拿到任務的目標肩祥,通過 this.data 拿到數(shù)據(jù)
// 在 build 中指定的屬性的每一個鍵都會成為一個目標,除了 options
// options 是任務的配置選項缩膝,可以在任務執(zhí)行的函數(shù)中通過 this.options() 拿到
// 在子目標中也可以配置 options 選項混狠,會覆蓋對象中的 options
grunt.initConfig({
build: {
options: {
foo: 'bar'
},
css: {
options: {
foo: 'baz'
}
},
// css: 1,
js: 2
}
})
// registerMultiTask: 多目標任務
grunt.registerMultiTask('build', function () {
console.log('options', this.options())
console.log(`target:${this.target}, data:${this.data}`)
})
// 插件的使用
// eg: grunt-contrib-clean 用來清除臨時文件
// 通過 grunt.loadNPmTasks() 方法加載插件中提供的任務
// grunt 的插件的命名方式都是 `grunt-contrib-${taskName}`
// 直接運行 yarn grunt clean 發(fā)現(xiàn)報錯提示 No "clean" targets found.
// 說明 clean 任務是一個多目標任務,需要配置 initConfig
// grunt.loadNpmTasks('grunt-contrib-clean')
// 給 clean 任務配置一個 temp 任務疾层,值為需要清除的文件目錄
// 可以先創(chuàng)建一個這樣的文件用來測試
// 執(zhí)行 yarn grunt clean 后将饺,會發(fā)現(xiàn)這個文件被清除了
// temp 文件路徑可以使用通配符
grunt.initConfig({
clean: {
// temp: 'temp/app.js'
// temp: 'temp/*.txt'
temp: 'temp/**' // temp/** 表示 temp 下的所有文件
}
})
grunt.loadNpmTasks('grunt-contrib-clean')
// 常用插件
// 隨著引入模塊越多,loadNpmTasks 的使用也會越多
// 有一個依賴痛黎,可以減少 loadNpmTasks 的使用:load-grunt-tasks
const loadGruntTasks = require('load-grunt-tasks')
// yarn add grunt-sass sass --dev
// yarn add grunt-babel @babel/core @babel/preset-env --dev
// yarn add grunt-contrib-watch --dev
const sass = require('sass')
grunt.initConfig({
sass: {
options: {
sourceMap: true,
// 用來指定使用哪個模塊來進行 sass 的編譯
implementation: sass
},
main: {
files: {
// 輸出文件路徑: 輸入文件源路徑
'dist/css/main.css': 'src/sass/main.scss'
}
}
},
babel: {
options: {
sourceMap: true,
presets: ['@babel/preset-env'] // 將最新的 ECMAScript 加載進來
},
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.loadNpmTasks('grunt-sass')
// 自動加載所有的 grunt 插件中的任務
loadGruntTasks(grunt)
// watch 是監(jiān)聽文件變化才會執(zhí)行 babel 和 sass予弧,第一次變異并不會執(zhí)行
// 所以需要給任務做個映射,在啟動的時候先執(zhí)行一次
grunt.registerTask('default', ['babel', 'sass'])
}
Gulp
基本使用
安裝依賴: yarn add gulp --dev
在根目錄創(chuàng)建 gulpfile.js 文件:
// gulp 的入口文件
// 基本使用
// 因為文件運行在 nodejs 環(huán)境中湖饱,可以使用 commonJS 的規(guī)范
// 定義構(gòu)建任務的方式就是通過導出函數(shù)成員的方式
// 在最新的 gulp 中取消了同步代碼模式掖蛤,每個任務都必須是異步任務
// 任務執(zhí)行完成需要通過調(diào)用回調(diào)函數(shù)來標記完成
// 函數(shù)的形參就是一個函數(shù),調(diào)用這個形參的函數(shù)就是標識任務結(jié)束
// 執(zhí)行 yarn gulp foo
exports.foo = done => {
console.log('foo')
done() // 標識任務完成
}
// 默認任務 yarn gulp
exports.default = done => {
console.log('default')
done()
}
// 以下是在 gulp@4.0 以前注冊 gulp 任務的方法
// 執(zhí)行 yarn gulp bar 可以看到結(jié)果
// gulp@4.0 以后的版本保留了之前的使用方式井厌,但是不推薦使用了
const gulp = require('gulp')
gulp.task('bar', done => {
console.log('bar')
done()
})
// 組合任務:并行任務和串行任務
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)
}
// series: 串行任務
// 執(zhí)行 yarn gulp foo 會發(fā)現(xiàn)三個任務按照順序依次執(zhí)行
exports.foo = series(task1, task2, task3)
// parallel: 并行任務
// 執(zhí)行 yarn gulp bar 會發(fā)現(xiàn)三個任務同時啟動
exports.bar = parallel(task1, task2, task3)
// 異步任務 通知外部任務完成的方式:
// 通過回調(diào)的方式解決
// 錯誤優(yōu)先蚓庭,如果有多個任務,其中一個錯誤仅仆,后邊的就不會執(zhí)行了
exports.callback = done => {
console.log('callback task')
done()
}
exports.callback_error = done => {
console.log('callback task')
done(new Error('task failed'))
}
// Promise 的方式器赞,避免了回調(diào)地獄
exports.promise = () => {
console.log('promise task')
return Promise.resolve()
}
exports.promise_error = () => {
console.log('promise')
return Promise.reject(new Error('task failed'))
}
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
// async await 的方式
exports.async = async () => {
await timeout(1000)
console.log('async task')
}
// stream 的方式最為常見,任務函數(shù)中返回一個 stream 對象
const fs = require('fs')
exports.stream = () => {
const readStream = fs.createReadStream('package.json') // 讀取文件的文件流
const writeStream = fs.createWriteStream('temp.txt') // 寫入文件的文件流
readStream.pipe(writeStream) // 將 readStream 導到 writeStream 中
return readStream // 返回 readStream 的文件流蝇恶,相當于在 readStream 的 end 事件中執(zhí)行結(jié)束任務
}
// exports.stream = done => {
// const readStream = fs.createReadStream('package.json')
// const writeStream = fs.createWriteStream('temp.txt')
// readStream.pipe(writeStream)
// readStream.on('end', () => {
// done()
// })
// }
通過 node 底層API 實現(xiàn)構(gòu)建過程工作原理
const fs = require('fs')
const { Transform } = require('stream')
exports.default = () => {
// 文件讀取流
const read = fs.createReadStream('normalize.css')
// 文件寫入流
const write = fs.createWriteStream('normalize.min.css')
// 文件轉(zhuǎn)換流
const transform = new Transform({
transform: (chunk, encoding, callback) => {
// 轉(zhuǎn)換流的核心轉(zhuǎn)換過程
// chunk => 讀取流中讀取到的內(nèi)容
const input = chunk.toString()
const output = input.replace(/\s+/g, '').replace('/\/\*.+?\*\//g', '')
callback(null, output) // 將 output 通過回調(diào)函數(shù)返回出去拳魁,第一個參數(shù)為錯誤參數(shù),沒有錯誤傳 null
}
})
// 把讀取出來的文件流導入寫入文件流
read
.pipe(transform) // 轉(zhuǎn)換
.pipe(write) // 寫入
return read
}
Gulp 文件操作 API
Gulp 也提供了文件讀取和寫入的 API撮弧,相比于 node 底層的 API 更容易理解和使用潘懊。文件轉(zhuǎn)換是通過插件來完成的。
// yarn add gulp-clean-css --dev
// yarn add gulp-rename --dev
const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')
// 通過 src 創(chuàng)建文件讀取流
// 通過 pipe 導出到 dest 創(chuàng)建的寫入流贿衍,寫入流只需要指定目錄
exports.default = () => {
return src('src/*.css')
.pipe(cleanCss()) // 先經(jīng)過轉(zhuǎn)換
.pipe(rename({ extname: '.min.css' })) // 重命名
.pipe(dest('dist'))
}