模塊化
模塊化是指把一個復(fù)雜的系統(tǒng)分解到多個模塊以方便編碼决左。
缺點
- 命名空間沖突,兩個庫可能會使用同一個名稱疗锐,例如
Zepto
和jQuery
都被放在window.$
下点待; - 無法合理地管理項目的依賴和版本爱榕;
- 無法方便地控制依賴的加載順序。
CommonJS
CommonJS
是一種使用廣泛的 JavaScript
模塊化規(guī)范花履,核心思想是通過 require
方法來同步地加載依賴的其他模塊芽世,通過 module.exports
導(dǎo)出需要暴露的接口。
優(yōu)點:代碼可復(fù)用于 Node.js
環(huán)境下并運行诡壁,例如做同構(gòu)應(yīng)用济瓢;通過 NPM
發(fā)布的很多第三方模塊都采用了 CommonJS
規(guī)范。
缺點:代碼無法直接運行在瀏覽器環(huán)境下妹卿,必須通過工具轉(zhuǎn)換成標準的 ES5
旺矾。
// 導(dǎo)入
const moduleA = require('./moduleA');
// 導(dǎo)出
module.exports = moduleA.someFunc;
ES6 模塊化
它在語言的層面上實現(xiàn)了模塊化。瀏覽器廠商和 Node.js
都宣布要原生支持該規(guī)范夺克。它將逐漸取代 CommonJS
和 AMD
規(guī)范箕宙,成為瀏覽器和服務(wù)器通用的模塊解決方案。
// 導(dǎo)入
import { readFile } from 'fs';
import React from 'react';
// 導(dǎo)出
export function hello() {};
export default {
// ...
};
樣式文件中的模塊化
// util.scss 文件
// 定義樣式片段
@mixin center {
// 水平豎直居中
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
// main.scss 文件
// 導(dǎo)入和使用 util.scss 中定義的樣式片段
@import "util";
#box{
@include center;
}
常見的構(gòu)建工具及對比
各種可以提高開發(fā)效率的新思想和框架被發(fā)明铺纽。但是這些東西都有一個共同點:源代碼無法直接運行柬帕,必須通過轉(zhuǎn)換后才可以正常運行。
構(gòu)建就是做這件事情狡门,把源代碼轉(zhuǎn)換成發(fā)布到線上的可執(zhí)行 JavaScrip
陷寝、CSS
、HTML
代碼融撞,包括如下內(nèi)容盼铁。
- 代碼轉(zhuǎn)換:
TypeScript
編譯成JavaScript
、SCSS
編譯成CSS
等尝偎。 - 文件優(yōu)化:壓縮
JavaScript
饶火、CSS
、HTML
代碼致扯,壓縮合并圖片等肤寝。 - 代碼分割:提取多個頁面的公共代碼、提取首屏不需要執(zhí)行部分的代碼讓其異步加載抖僵。
- 模塊合并:在采用模塊化的項目里會有很多個模塊和文件鲤看,需要構(gòu)建功能把模塊分類合并成一個文件。
- 自動刷新:監(jiān)聽本地源代碼的變化耍群,自動重新構(gòu)建义桂、刷新瀏覽器找筝。
- 代碼校驗:在代碼被提交到倉庫前需要校驗代碼是否符合規(guī)范,以及單元測試是否通過慷吊。
- 自動發(fā)布:更新完代碼后袖裕,自動構(gòu)建出線上發(fā)布代碼并傳輸給發(fā)布系統(tǒng)。
構(gòu)建其實是工程化溉瓶、自動化思想在前端開發(fā)中的體現(xiàn)急鳄,把一系列流程用代碼去實現(xiàn),讓代碼自動化地執(zhí)行這一系列復(fù)雜的流程堰酿。 構(gòu)建給前端開發(fā)注入了更大的活力疾宏,解放了我們的生產(chǎn)力
Npm Script
Npm Script
是一個任務(wù)執(zhí)行者。Npm
是在安裝 Node.js
時附帶的包管理器触创,Npm Script
則是 Npm
內(nèi)置的一個功能坎藐,允許在 package.json
文件里面使用 scripts
字段定義任務(wù):
{
"scripts": {
"dev": "node dev.js",
"pub": "node build.js"
}
}
// 每個屬性對應(yīng)一段 Shell 腳本其底層實現(xiàn)原理是通過調(diào)用 Shell 去運行腳本命令
// 例如執(zhí)行 npm run pub 命令等同于執(zhí)行命令 node build.js。
Grunt
Grunt
和 Npm Script
類似嗅榕,也是一個任務(wù)執(zhí)行者顺饮。Grunt
有大量現(xiàn)成的插件封裝了常見的任務(wù),也能管理任務(wù)之間的依賴關(guān)系凌那,自動化執(zhí)行依賴的任務(wù)兼雄,每個任務(wù)的具體執(zhí)行代碼和依賴關(guān)系寫在配置文件 Gruntfile.js
里。
module.exports = function(grunt) {
// 所有插件的配置信息
grunt.initConfig({
// uglify 插件的配置信息
uglify: {
app_task: {
files: {
'build/app.min.js': ['lib/index.js', 'lib/test.js']
}
}
},
// watch 插件的配置信息
watch: {
another: {
files: ['lib/*.js'],
}
}
});
// 告訴 grunt 我們將使用這些插件
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
// 告訴grunt當(dāng)我們在終端中啟動 grunt 時需要執(zhí)行哪些任務(wù)
grunt.registerTask('dev', ['uglify','watch']);
};
// 在項目根目錄下執(zhí)行命令 grunt dev 就會啟動 JavaScript 文件壓縮和自動刷新功能帽蝶。
優(yōu)點:靈活赦肋,它只負責(zé)執(zhí)行你定義的任務(wù);大量的可復(fù)用插件封裝好了常見的構(gòu)建任務(wù)励稳。
缺點:集成度不高佃乘,要寫很多配置后才可以用,無法做到開箱即用驹尼。
Gulp
Gulp
是一個基于流的自動化構(gòu)建工具趣避。 除了可以管理和執(zhí)行任務(wù),還支持監(jiān)聽文件新翎、讀寫文件程帕。Gulp
被設(shè)計得非常簡單,只通過下面5個方法就可以勝任幾乎所有構(gòu)建場景:
- 通過
gulp.task
注冊一個任務(wù)地啰; - 通過
gulp.run
執(zhí)行任務(wù)愁拭; - 通過
gulp.watch
監(jiān)聽文件變化; - 通過
gulp.src
讀取文件亏吝; - 通過
gulp.dest
寫文件岭埠。
// 引入 Gulp
var gulp = require('gulp');
// 引入插件
var jshint = require('gulp-jshint');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
// 編譯 SCSS 任務(wù)
gulp.task('sass', function() {
// 讀取文件通過管道喂給插件
gulp.src('./scss/*.scss')
// SCSS 插件把 scss 文件編譯成 CSS 文件
.pipe(sass())
// 輸出文件
.pipe(gulp.dest('./css'));
});
// 合并壓縮 JS
gulp.task('scripts', function() {
gulp.src('./js/*.js')
.pipe(concat('all.js'))
.pipe(uglify())
.pipe(gulp.dest('./dist'));
});
// 監(jiān)聽文件變化
gulp.task('watch', function(){
// 當(dāng) scss 文件被編輯時執(zhí)行 SCSS 任務(wù)
gulp.watch('./scss/*.scss', ['sass']);
gulp.watch('./js/*.js', ['scripts']);
});
優(yōu)點:好用又不失靈活,既可以單獨完成構(gòu)建也可以和其它工具搭配使用。相對于Grunt
惜论,Gulp
增加了監(jiān)聽文件许赃、讀寫文件、流式處理的功能来涨。
缺點:集成度不高图焰,要寫很多配置后才可以用启盛,無法做到開箱即用蹦掐。
webpack
Webpack
是一個打包模塊化 JavaScript
的工具,在 Webpack
里一切文件皆模塊僵闯,通過 Loader
轉(zhuǎn)換文件卧抗,通過 Plugin
注入鉤子,最后輸出由多個模塊組合成的文件鳖粟。Webpack
專注于構(gòu)建模塊化項目社裆。
優(yōu)點:專注于處理模塊化的項目,能做到開箱即用一步到位向图;通過 Plugin
擴展泳秀,完整好用又不失靈活;使用場景不僅限于Web
開發(fā)榄攀;社區(qū)龐大活躍嗜傅,經(jīng)常引入緊跟時代發(fā)展的新特性,能為大多數(shù)場景找到已有的開源擴展檩赢;良好的開發(fā)體驗吕嘀。
缺點:只能用于采用模塊化開發(fā)的項目
Rollup
Rollup
是一個和 Webpack
很類似但專注于 ES6
的模塊打包工具。 Rollup
的亮點在于能針對 ES6
源碼進行 Tree Shaking
以去除那些已被定義但沒被使用的代碼贞瞒,以及 Scope Hoisting
以減小輸出文件大小提升運行性能偶房。 然而 Rollup
的這些亮點隨后就被 Webpack
模仿和實現(xiàn)。Rollup
在用于打包 JavaScript
庫時比 Webpack
更加有優(yōu)勢军浆,因為其打包出來的代碼更小更快棕洋。 但功能不夠完善,很多場景都找不到現(xiàn)成的解決方案乒融。
-
Rollup
是在Webpack
流行后出現(xiàn)的替代品掰盘; -
Rollup
生態(tài)鏈還不完善,體驗不如Webpack
簇抵; -
Rollup
功能不如Webpack
完善庆杜,但其配置和使用更加簡單; -
Rollup
不支持Code Spliting
碟摆,但好處是打包出來的代碼中沒有Webpack
那段模塊的加載晃财、執(zhí)行和緩存的代碼
安裝與使用
# 安裝最新穩(wěn)定版 -D 安裝到開發(fā)依賴
npm i -D webpack
安裝完后你可以通過這些途徑運行安裝到本項目的 Webpack
:
- 在項目根目錄下對應(yīng)的命令行里通過輸入
node_modules/.bin/webpack
運行 - 在
Npm Script
里定義的任務(wù)會優(yōu)先使用本項目下的Webpack
,代碼如下:
"scripts": {
"start": "webpack --config webpack.config.js"
}
Webpack
在執(zhí)行構(gòu)建時默認會從項目根目錄下的webpack.config.js
文件讀取配置。
const path = require('path');
// 通過 CommonJS 規(guī)范導(dǎo)出一個描述如何構(gòu)建的 Object 對象断盛。
module.exports = {
// JavaScript 執(zhí)行入口文件
entry: './main.js',
output: {
// 把所有依賴的模塊合并輸出到一個 bundle.js 文件
filename: 'bundle.js',
// 輸出文件都放到 dist 目錄下
path: path.resolve(__dirname, './dist'),
}
};
執(zhí)行 webpack
命令運行 Webpack
構(gòu)建罗洗,你會發(fā)現(xiàn)目錄下多出一個 dist
目錄,里面有個 bundle.js
文件钢猛, bundle.js
文件是一個可執(zhí)行的 JavaScript
文件伙菜,它包含頁面所依賴的模塊及內(nèi)置的 webpackBootstrap
啟動函數(shù)。 這時你用瀏覽器打開 index.html
網(wǎng)頁將會看到正常的展示命迈。
Webpack
是一個打包模塊化 JavaScript
的工具贩绕,它會從 main.js
出發(fā),識別出源碼中的模塊化導(dǎo)入語句壶愤, 遞歸的尋找出入口文件的所有依賴淑倾,把入口和其所有依賴打包到一個單獨的文件中。
使用Loader
Webpack
不原生支持解析 如CSS
等文件征椒。要支持非 JavaScript
類型的文件娇哆,需要使用 Webpack
的 Loader
機制。
const path = require('path');
module.exports = {
// JavaScript 執(zhí)行入口文件
entry: './main.js',
output: {
// 把所有依賴的模塊合并輸出到一個 bundle.js 文件
filename: 'bundle.js',
// 輸出文件都放到 dist 目錄下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
// 用正則去匹配要用該 loader 轉(zhuǎn)換的 CSS 文件
test: /\.css$/,
use: ['style-loader', 'css-loader?minimize'],
},
// 或者
{
test: /\.css$/,
use: [
'style-loader',
{
loader:'css-loader',
options: {
minimize: true,
}
}
],
}
]
}
};
Loader
可以看作具有文件轉(zhuǎn)換功能的翻譯員勃救,配置里的 module.rules
數(shù)組配置了一組規(guī)則碍讨,告訴 Webpack 在遇到哪些文件時使用哪些 Loader 去加載和轉(zhuǎn)換。 如上配置告訴 Webpack
在遇到以 .css
結(jié)尾的文件時先使用 css-loader
讀取 CSS
文件蒙秒,再交給 style-loader 把 CSS
內(nèi)容注入到 JavaScript 里勃黍。 在配置 Loader
時需要注意的是:
-
use
屬性的值需要是一個由Loader
名稱組成的數(shù)組,Loader
的執(zhí)行順序是由后到前的税肪; - 每一個
Loader
都可以通過URL querystring
的方式傳入?yún)?shù)溉躲,例如css-loader?minimize
中的minimize
告訴css-loader
要開啟 CSS 壓縮。
在重新執(zhí)行 Webpack
構(gòu)建前要先安裝新引入的 Loader
:
npm i -D style-loader css-loader
安裝成功后重新執(zhí)行構(gòu)建時益兄,你會發(fā)現(xiàn) bundle.js
文件被更新了锻梳,里面注入了在 main.css
中寫的 CSS
,而不是會額外生成一個 CSS
文件净捅。 但是重新刷新 index.html
網(wǎng)頁時將會發(fā)現(xiàn)樣式生效了疑枯!這其實都是 style-loader
的功勞,它的工作原理大概是把 CSS
內(nèi)容用 JavaScript
里的字符串存儲起來蛔六, 在網(wǎng)頁執(zhí)行 JavaScript
時通過 DOM
操作動態(tài)地往 HTML head
標簽里插入 HTML style
標簽荆永。 也許你認為這樣做會導(dǎo)致 JavaScript
文件變大并導(dǎo)致加載網(wǎng)頁時間變長,想讓 Webpack
單獨輸出 CSS
文件国章【咴浚可使用 Webpack Plugin
機制來實現(xiàn)。
還可以在源碼中指定用什么 Loader
去處理文件液兽。 以加載 CSS
文件為例骂删,在 main.js
加上以下代碼即可。
require('style-loader!css-loader?minimize!./main.css');
// 指定對 ./main.css 這個文件先采用 css-loader 再采用 style-loader 轉(zhuǎn)換。
使用Plugin
Plugin
是用來擴展 Webpack
功能的宁玫,通過在構(gòu)建流程里注入鉤子實現(xiàn)粗恢,它給 Webpack
帶來了很大的靈活性。
下例展示通過 Plugin
把注入到 bundle.js
文件里的 CSS
提取到單獨的文件中欧瘪。
安裝插件:npm i -D extract-text-webpack-plugin
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
// JavaScript 執(zhí)行入口文件
entry: './main.js',
output: {
// 把所有依賴的模塊合并輸出到一個 bundle.js 文件
filename: 'bundle.js',
// 把輸出文件都放到 dist 目錄下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
// 用正則去匹配要用該 loader 轉(zhuǎn)換的 CSS 文件
test: /\.css$/,
use: ExtractTextPlugin.extract({
// 轉(zhuǎn)換 .css 文件需要使用的 Loader
use: ['css-loader'],
}),
}
]
},
plugins: [
new ExtractTextPlugin({
// 從 .js 文件中提取出來的 .css 文件的名稱
filename: `[name]_[contenthash:8].css`,
}),
]
};
重新構(gòu)建眷射,你會發(fā)現(xiàn) dist
目錄下多出一個 main_1a87a56a.css
文件,bundle.js
里也沒有 CSS
代碼了佛掖,再把該 CSS
文件引入到 index.html
里就完成了妖碉。
從以上代碼可以看出, Webpack
是通過 plugins
屬性來配置需要使用的插件列表的苦囱。 plugins
屬性是一個數(shù)組嗅绸,里面的每一項都是插件的一個實例,在實例化一個組件時可以通過構(gòu)造函數(shù)傳入這個組件支持的配置屬性撕彤。
例如 ExtractTextPlugin
插件的作用是提取出 JavaScript
代碼里的 CSS
到一個單獨的文件。 對此你可以通過插件的 filename
屬性猛拴,告訴插件輸出的 CSS
文件名稱是通過 [name]_[contenthash:8].css
字符串模版生成的羹铅,里面的 [name]
代表文件名稱, [contenthash:8]
代表根據(jù)文件內(nèi)容算出的8位 hash
值愉昆。
使用 DevServer
前面只是讓 Webpack
正常運行起來了职员,但在實際開發(fā)中你可能會需要:
- 提供
HTTP
服務(wù)而不是使用本地文件預(yù)覽; - 監(jiān)聽文件的變化并自動刷新網(wǎng)頁跛溉,做到實時預(yù)覽焊切;
- 支持
Source Map
,以方便調(diào)試芳室。
Webpack
原生支持上述第2专肪、3點內(nèi)容,再結(jié)合官方提供的開發(fā)工具 DevServer
也可以很方便地做到第1點堪侯。DevServer
會啟動一個 HTTP
服務(wù)器用于服務(wù)網(wǎng)頁請求嚎尤,同時會幫助啟動Webpack
,并接收 Webpack
發(fā)出的文件更變信號伍宦,通過 WebSocket
協(xié)議自動刷新網(wǎng)頁做到實時預(yù)覽芽死。
安裝:npm i -D webpack-dev-server
安裝成功后執(zhí)行 webpack-dev-server
命令, DevServer
就啟動了次洼。
DevServer
啟動的 HTTP
服務(wù)器監(jiān)聽在 http://localhost:8080/
(端口可以設(shè)置)关贵,DevServer
啟動后會一直駐留在后臺保持運行,訪問這個網(wǎng)址你就能獲取項目根目錄下的 index.html
卖毁。 用瀏覽器打開這個地址你會發(fā)現(xiàn)頁面空白錯誤原因是 ./dist/bundle.js
加載404了揖曾。 同時你會發(fā)現(xiàn)并沒有文件輸出到 dist
目錄,原因是 DevServer
會把 Webpack
構(gòu)建出的文件保存在內(nèi)存中,在要訪問輸出的文件時翩肌,必須通過HTTP
服務(wù)訪問模暗。 由于 DevServer
不會理會 webpack.config.js
里配置的 output.path
屬性,所以要獲取 bundle.js
的正確 URL
是 http://localhost:8080/bundle.js
所以在 index.html
中這樣引入 <script src="bundle.js"></script>
而不是引入預(yù)料中打包后的dist
文件下的bundle.js
實時預(yù)覽
Webpack
在啟動時可以開啟監(jiān)聽模式念祭,開啟監(jiān)聽模式后 Webpack
會監(jiān)聽本地文件系統(tǒng)的變化兑宇,發(fā)生變化時重新構(gòu)建出新的結(jié)果。Webpack
默認是關(guān)閉監(jiān)聽模式的粱坤,你可以在啟動 Webpack
時通過 webpack --watch
來開啟監(jiān)聽模式隶糕。
通過 DevServer
啟動的 Webpack
會開啟監(jiān)聽模式,當(dāng)發(fā)生變化時重新執(zhí)行完構(gòu)建后通知 DevServer
站玄。 DevServer
會讓 Webpack
在構(gòu)建出的 JavaScript
代碼里注入一個代理客戶端用于控制網(wǎng)頁枚驻,網(wǎng)頁和 DevServer
之間通過 WebSocket
協(xié)議通信, 以方便 DevServer
主動向客戶端發(fā)送命令株旷。 DevServer
在收到來自 Webpack
的文件變化通知時通過注入的客戶端控制網(wǎng)頁刷新再登。
如果嘗試修改 index.html
文件并保存,你會發(fā)現(xiàn)這并不會觸發(fā)以上機制晾剖,導(dǎo)致這個問題的原因是 Webpack
在啟動時會以配置里的 entry
為入口去遞歸解析出 entry
所依賴的文件锉矢,只有 entry
本身和依賴的文件才會被 Webpack
添加到監(jiān)聽列表里。 而 index.html
文件是脫離了JavaScript
模塊化系統(tǒng)的齿尽,所以 Webpack
不知道它的存在沽损。
模塊熱替換
除了通過重新刷新整個網(wǎng)頁來實現(xiàn)實時預(yù)覽,DevServer
還有一種被稱作模塊熱替換的刷新技術(shù)循头。 模塊熱替換能做到在不重新加載整個網(wǎng)頁的情況下绵估,通過將被更新過的模塊替換老的模塊,再重新執(zhí)行一次來實現(xiàn)實時預(yù)覽卡骂。 模塊熱替換相對于默認的刷新機制能提供更快的響應(yīng)和更好的開發(fā)體驗国裳。 模塊熱替換默認是關(guān)閉的,要開啟模塊熱替換偿警,你只需在啟動 DevServer
時帶上 --hot
參數(shù)躏救。
支持 Source Map
在瀏覽器中運行的 JavaScript
代碼都是編譯器輸出的代碼,你可能需要通過斷點調(diào)試去找出問題螟蒸。 在編譯器輸出的代碼上進行斷點調(diào)試是一件辛苦和不優(yōu)雅的事情盒使, 調(diào)試工具可以通過 Source Map
映射代碼,讓你在源代碼上斷點調(diào)試七嫌。 Webpack
支持生成 Source Map
少办,只需在啟動時帶上 --devtool source-map
參數(shù)。
核心概念
-
Entry
:入口诵原,Webpack
執(zhí)行構(gòu)建的第一步將從Entry
開始英妓,可抽象成輸入挽放。 -
Module
:模塊,在Webpack
里一切皆模塊蔓纠,一個模塊對應(yīng)著一個文件辑畦。Webpack
會從配置的Entry
開始遞歸找出所有依賴的模塊。 -
Chunk
:代碼塊腿倚,一個Chunk
由多個模塊組合而成纯出,用于代碼合并與分割。 -
Loader
:模塊轉(zhuǎn)換器敷燎,用于把模塊原內(nèi)容按照需求轉(zhuǎn)換成新內(nèi)容暂筝。 -
Plugin
:擴展插件,在Webpack
構(gòu)建流程中的特定時機注入擴展邏輯來改變構(gòu)建結(jié)果或做你想要的事情硬贯。 -
Output
:輸出結(jié)果焕襟,在Webpack
經(jīng)過一系列處理并得出最終想要的代碼后輸出結(jié)果。
Webpack
啟動后會從 Entry
里配置的 Module
開始遞歸解析 Entry
依賴的所有 Module
饭豹。 每找到一個 Module
鸵赖, 就會根據(jù)配置的 Loader
去找出對應(yīng)的轉(zhuǎn)換規(guī)則,對 Module
進行轉(zhuǎn)換后墨状,再解析出當(dāng)前 Module
依賴的 Module
卫漫。 這些模塊會以 Entry
為單位進行分組,一個 Entry
和其所有依賴的 Module
被分到一個組也就是一個 Chunk
肾砂。最后 Webpack
會把所有 Chunk
轉(zhuǎn)換成文件輸出。 在整個流程中 Webpack
會在恰當(dāng)?shù)臅r機執(zhí)行 Plugin
里定義的邏輯宏悦。