前言:博主2020.3正式進軍前端,目標高級前端工程師,經驗尚淺,文章內容如如有誤,歡迎指正。css
自動化構建 並非一個句子,有必要在行文前先說明一下它在本文中的賓語是前端工程。html
隨着前端需求和項目的日益複雜,出於提升開發效率、用戶體驗以及其它工程上的須要,咱們一般會藉助不少更高階的語法或者工具來幫助咱們更快更好的開發、部署一個前端工程。前端
那麼如何理解構建以及自動化構建呢?我的觀點,簡單務實點說,構建就是指項目從源代碼到一個能按需運行的工程(開發環境、生產環境)所須要作的全部事情。一般來講,項目屢次構建的每次構建過程都包含不少任務而且基本一致,按照任何簡單機械的重複勞動都應該讓機器去完成的思想,咱們應該自動化去完成工程的構建,提升構建效率。node
那麼如何實現自動化構建呢?我的理解,前端工程構建其實就是一個任務流,完成了這個任務流中的全部任務即完成了構建。說到任務以及任務流,咱們有必要先好好認識一下它們。webpack
對比於JavaScript的函數,我的對任務是這麼分類的:nginx
同步任務和異步任務無須解釋,這裏說說並行任務和串行任務。任務並行能夠用於縮短多個任務的執行時間。由於node是單線程執行的,我認爲它並不能縮短多個同步任務並行的執行時間,可是構建過程當中的任務一般都會涉及到IO流,因此大部分任務都是異步任務,IO任務並行能夠避免IO阻塞線程執行,進而縮短多個任務的執行時間。git
而任務串行能夠保證任務之間的有序執行,好比在開發環境下,咱們確定要先執行編譯任務,而後才能執行啓動開發服務器的任務。es6
理解了構建過程當中的任務以後,下面再列舉一些在平常開發當中,咱們常見到的構建任務。github
任務名 | 任務職責 |
---|---|
Eslint檢查 | 統一代碼風格 |
babel轉換 | ES6 -> ES5 |
typescript轉換 | Ts -> Js |
sass轉換 | sass -> css |
less轉換 | less -> css |
html、css、js壓縮 | .html -> .min.html、.css->.min.css、.js->.min.js |
web server | 開發服務器 |
js、css兼容 | 兼容不一樣瀏覽器 |
... | ... |
除了上述表格中列舉的任務以外,在不一樣的項目中還會有不一樣的構建任務,這裏再也不一一贅述。上面說到自動化構建其實就是一個任務流,再理解和認識了常見任務以後,咱們再來理一理什麼是前端的構建任務流。web
構建是爲工程服務的,而工程又是爲用戶服務的。對應於開發環境和生產環境,前端工程能夠分爲開發環境工程和生產環境工程,其中開發環境工程爲開發者服務,生產環境工程爲用戶服務。
知足工程使用者的需求是咱們構建工程的終極目的,因此有必要投其所好,根據工程的使用者不一樣,完成他所須要的的一連串任務,也就是任務流。這時能夠根據構建後工程的目標使用者來劃分,把任務流分爲開發環境構建任務流和生產環境構建任務流兩種。
開發環境構建任務流構建後的工程是爲開發者服務的。開發者須要開發調試代碼,因此開發環境任務流構建的工程須要實現如下功能:
功能項 | 包含任務 |
---|---|
語法檢查 | Eslint |
語法轉換 | ES6 -> ES五、Sass -> less、Ts->Js等等 |
模擬生產環境 | web開發服務器:devServer |
易於調試 | sourceMap |
... | ... |
開發者須要不斷修改代碼查看效果,因此除了知足功能以外,還須要加快構建速度而且自動刷新,以保證良好的使用體驗。
優化方式 | 實現方案 |
---|---|
加快構建速度 | devServer熱模塊替換 |
自動刷新 | devServer 監聽源代碼 |
... | ... |
關於web開發服務器devServer
使用web開發服務器能夠模擬像使用nginx、tomcat等服務器軟件同樣的線上環境,它在功能以及配置上都與nginx以及tomcat相似, 最簡單的配置就是指明資源路徑baseUrl以及服務啓動ip和端口port便可。在開發環境啓動本地服務時,配置代理能夠在符合同源策略的狀況下解決跨域問題。
開發服務器除了能夠模擬線上環境以外,更增強大的一點是它能夠監聽源代碼,實現熱部署和自動刷新功能!
生產環境構建任務流構建後的工程是爲用戶服務的。與開發環境相比,它也須要語法檢查以及編譯功能,但不須要考慮修改以及調試代碼的問題,它關注的是瀏覽器兼容以及運行速度等問題。
功能項 | 包含任務 |
---|---|
語法檢查 | Eslint |
語法轉換 | ES6 -> ES五、Sass -> less、Ts->Js等等 |
語法兼容 | 不一樣瀏覽器的js、css語法兼容 |
下載速度 | 資源壓縮與合併 |
... | ... |
生產環境的優化除了資源的下載速度以外,還能夠從不少方面入手,下面是其中的一些方面以及實現方案。
優化方面 | 實現方式 |
---|---|
下載優化 | treeshaking、代碼分割、懶加載 |
運行優化 | 代碼上優化性能 |
離線訪問 | pwa技術 |
... | ... |
終於把任務以及任務流淺顯粗陋的講完了,接下來咱們先是使用npm scripts來實現簡單項目的自動化構建,然後學習一下Gulp工具如何實現複雜項目的自動化構建。
任務流由任務組成,任務由腳本實現。在定義好任務腳本或者安裝好任務cli模塊以後,咱們只需在package.json的scripts選項中配置一條script,就能夠方便地調用任務腳本或者任務模塊。對於任務流的npm script定義,咱們能夠藉助一些能夠幫助任務組合的庫,這樣就能夠實現多個任務之間的並行和串行。
這裏不得不提一下node_modules/.bin文件夾,咱們在項目中安裝的cli模塊都會有一個cmd文件出如今這裏。當咱們在項目中須要調用這些cli模塊時,只需yarn/npx cli模塊名的方式就能夠很方便的調用這些cli模塊。
好的,經過上面的分析以後,咱們接下來展開講述一下npm scripts如何實現任務以及任務流的構建。
對於單任務的構建,只需配置一條簡單的script便可,如如下sass和ES6轉換的script示例(package.json):
"scripts": {"sass": "sass scss/main.scss css/style.css","es6": "babel es6 --out-dir es5", }, "devDependencies": {"@babel/cli": "^7.12.8","@babel/core": "^7.12.9","sass": "^1.29.0" }複製代碼
配置以上scripts以後,咱們就可使用如下命令執行任務:
# sass轉換yarn sass# es6轉換yarn es6複製代碼
這裏提一提在執行上述命令以後到最後調用sass和es6編譯工具的調用過程:
yarn sass -> sass scss/main.scss css/style.css -> node_modules/.bin/sass.cmd -> node_modules/sass/sass.js -> ...code
yarn es6 -> babel es6 --out-dir es5 -> node_modules/.bin/babel.cmd -> node_modules/@babel\cli\bin\babel.js -> node_modules/@babel\cli\lib\index.js -> ...code
對於任務流的構建,除了準備基本任務以外,咱們還須要考慮這些任務之間是否有序,若是有序咱們藉助任務串行實現,若是無序咱們經過任務並行加快構建速度。一般咱們會藉助npm-run-all 這個庫來實現任務的並行和串行,如如下經過任務並行實現sass轉換以及ES6轉換的簡單示例(package.json):
"scripts": {"sass": "sass scss/main.scss css/style.css","es6": "babel es6 --out-dir es5","build": "run-p sass es6" }, "devDependencies": {"npm-run-all": "^4.1.5","@babel/cli": "^7.12.8","@babel/core": "^7.12.9","sass": "^1.29.0" }複製代碼
配置以上scripts以後,咱們就可使用如下命令執行任務:
yarn build複製代碼
執行yarn build以後,就能夠藉助npm-run-all庫的nun-p.cmd實現sass和es6任務的並行。對於任務的串行,則經過npm-run-all庫的nun-s.cmd實現。
好的,在經過以上兩個示例理解了npm script實現構建任務以及任務流以後,咱們接下來經過npm script實現一個簡單前端項目的開發環境自動化構建和生產環境的自動化構建。
這個項目很簡單,它只包含一個html文件,一個使用了ES6語法js文件以及一個使用了sass語法的樣式文件,接下來咱們就用npm script來實現這個簡單項目的自動化構建(也即開發環境構建任務流和生產環境構建任務流)。事實上,簡單項目的自動化構建就是npm script實現自動化構建的使用場景。
經過上面咱們對開發環境構建任務流的認識,咱們先理一理在這個項目中,開發環境任務流至少應該包含哪些任務:
對於sass和ES6修改源代碼後的實時轉換,咱們能夠經過加上一個watch參數實現。而對於全部這些須要監聽變化的文件,咱們則統一放入temp文件夾下(角色比如如nginx和Tomcat的應用存放目錄),然後讓web開發服務器監聽這個temp文件夾下全部文件的變化,一旦變化即重啓並刷新瀏覽器。
好的通過上面任務分析以後,咱們可能會把package.json的scripts以及devDependencies寫成以下樣子:
"scripts": {"sassDev": "sass scss/main.scss temp/css/style.css --watch","babelDev": "babel es6/script.js --out-dir temp/es5/script.js --watch","copyHtmlDev": "copyfiles index.html temp","serve": "browser-sync temp --files \"temp\"","start": "run-p sassDev babelDev copyHtmlDev serve" }, "devDependencies": {"@babel/cli": "^7.12.8","@babel/core": "^7.12.9","browser-sync": "^2.26.13","copyfiles": "^2.4.1","npm-run-all": "^4.1.5","sass": "^1.29.0" }複製代碼
經過上面咱們對生產環境構建任務流的認識,咱們先理一理在這個項目中,生產環境任務流應該包含哪些任務:
"scripts": {"sass": "sass scss/main.scss dist/css/style.css","babel": "babel es6 --out-dir dist/es5","copyHtml": "copyfiles index.html dist","build": "run-p sass babel copyHtml" }, "devDependencies": {"@babel/cli": "^7.12.8","@babel/core": "^7.12.9","browser-sync": "^2.26.13","copyfiles": "^2.4.1","npm-run-all": "^4.1.5","sass": "^1.29.0" }複製代碼
上述代碼實現不全,按道理說,在生產環境下,至少須要作代碼的兼容以及壓縮。這時咱們就須要找到對應的工具庫或者本身實現,另外對於壓縮而言至少須要在編譯以後完成,因此須要注意多個任務間的關係。思路很簡單,博主偷個懶當前就不花時間去實踐了,須要時再實現就行。
在介紹Gulp以前,咱們有必要再重申一點。在項目以及構建需求不復雜時,npm scripts就能夠知足咱們的構建需求了,無需藉助其它工具。
Gulp是一個基於流的自動化構建工具,相比較於Grunt,它的構建速度更快,任務編寫也更加簡單靈活(Grunt博主沒用過也不感興趣)。要使用Gulp,首先須要在項目根目錄下建立一個Gulp入口文件gulpfile.js,而後在這個入口文件中經過暴露函數的方式註冊任務。
對於一個工具,其它的很少比比,咱們接下來看看它是怎麼實現前端項目的自動化構建的。
對於新版本的Gulp來講,全部任務都是異步任務,因此任務須要告訴Gulp何時執行結束。如下是gulp異步任務實現的幾種方式(關注它們是如何通知Gulp異步任務結束的)。
// 方式1:調用done方法主動通知任務結束exports.foo = done => { console.log('foo task working~') done() // 標識任務執行完成}// 方式2:返回Promise,經過它的resolve/reject方法通知任務結束const timeout = time => { return new Promise(resolve => {setTimeout(resolve, time) }) }// 方式3:返回讀取流對象,流完即自動通知任務結束exports.stream = () => { const read = fs.createReadStream('yarn.lock') const write = fs.createWriteStream('a.txt') read.pipe(write) return read }// 更多方式複製代碼
並行任務和串行任務能夠經過gulp提供的series(串行), parallel(並行)實現。
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) }exports.bar = parallel(task1, task2); // 並行任務barexports.foo = series(task1, task2); // 串行任務foo複製代碼
Gulp生態中有不少成熟的gulp任務插件,使用它們能夠很好地提升效率,如如下示例:
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插件,那麼咱們就須要自定義任務,以下示例:
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) // 寫入}複製代碼
gulp只是一個幫助咱們實現自動化構建的工具,思想在上文中已經探討了不少,並且對比於gulp,我的對後面的webpack更感興趣,這個示例我就很少作敘述了。
好吧,都是藉口,如今已經深夜了,用心地寫了那麼長,我坦白我熬不住了。對了,若是承認文章內容,點贊收藏鼓勵一下吧,每週一篇,後面會有個人更多學習記錄哦。
下例是通過我的整理過的,課程中老師使用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/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() }// css編譯 src => tempconst style = () => { return src('src/assets/styles/*.scss', { base: 'src'}) .pipe(plugins.sass({ outputStyle: 'expanded'})) .pipe(dest('temp')) .pipe(bs.reload({ stream: true})) }// js編譯 src => tempconst script = () => { return src('src/assets/scripts/*.js', { base: 'src'}) .pipe(plugins.babel({ presets: ['@babel/preset-env'] })) .pipe(dest('temp')) .pipe(bs.reload({ stream: true})) }// html模板解析 src => tempconst page = () => { return src('src/*.html', { base: 'src'}) .pipe(plugins.swig({ data, defaults: {cache: false } })) // 防止模板緩存致使頁面不能及時更新.pipe(dest('temp')) .pipe(bs.reload({ stream: true})) }// 串行編譯、模板解析const compile = parallel(style, script, page)// 開發環境開發服務器const serve = () => { watch('src/assets/styles/*.scss', style) watch('src/assets/scripts/*.js', script) watch('src/*.html', page) // watch('src/assets/images/**', image) // watch('src/assets/fonts/**', font) // watch('public/**', extra) watch(['src/assets/images/**','src/assets/fonts/**','public/**' ], bs.reload) bs.init({notify: false,port: 2080,// open: false,// files: 'dist/**',server: { baseDir: ['temp', 'src', 'public'], routes: {'/node_modules': 'node_modules' } } }) }// 開發環境構建流:編譯 + 啓動開發服務器 src => tempconst develop = series(compile, serve)// 生產環境下清空文件夾const clean = () => { return del(['dist', 'temp']) }// 生產環境js、css、html壓縮後構建 temp => distconst useref = () => { return src('temp/*.html', { base: 'temp'}) .pipe(plugins.useref({ searchPath: ['temp', '.'] }))// 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('dist')) }// 生產環境圖片壓縮後構建 src => distconst image = () => { return src('src/assets/images/**', { base: 'src'}) .pipe(plugins.imagemin()) .pipe(dest('dist')) }// 生產環境字體壓縮後構建 src => distconst font = () => { return src('src/assets/fonts/**', { base: 'src'}) .pipe(plugins.imagemin()) .pipe(dest('dist')) }// 生產環境靜態資源構建const extra = () => { return src('public/**', { base: 'public'}) .pipe(dest('dist')) }// 上線以前執行的任務 src => (temp =>) => dist const build = series( clean, parallel( series(compile, useref), image, font, extra ) )module.exports = { clean, build, develop }複製代碼
本文結束,觀衆老爺您慢走,歡迎下次光臨。