webpack發展史
- 在沒有ajax和jQuery之前,前端是不存在打包這個說法的,js沒有大規模使用,只做簡單的時鐘、mp3等效果,直接弄一個js文件引入就行
- 之後,人們開始使用iframe和flash等於服務器通信,因爲這兩種方式太過於tricky(棘手),直到google退出gmail的時候,人們發現了XMLHttpRequest,也就是AJAX,從此開始,前端出現了jquery等各種插件和庫,js文件越來越多。
- 隨着js做的事越來越多,js文件越來越大,U管理費用US等js文件壓縮合並工具陸續誕生,但是也有很多問題,比如:
- 庫和插件爲了要給他人調用,肯定要找個地方註冊,一般就是在 window 下申明一個全局的函數或對象。難保哪天用的兩個庫在全局用同樣的名字,那就衝突了
- 庫和插件如果還依賴其他的庫和插件,就要告知使用人,需要先引哪些依賴庫,那些依賴庫也有自己的依賴庫的話,就要先引依賴庫的依賴庫,以此類推
- 2009年,後端js發展,人們提出了CommonJS模塊化規範,也就是exports和require語法。但是它並不適用於瀏覽器,require是同步的,堵塞js腳本的執行,所以人們基於CommonJS定義了AMD規範(2011年),使用異步回調的語法來並行下載多個依賴項,也就是define函數(必須返回值)。現在出了ES6,7之後,已經差不多淘汰AMD了
- 2012年,webpack誕生,Browserify同期誕生,但webpack比它的優點:多文件打包、可以關心所有文件的打包、對資源文件的加載支持完善、支持CommonJS、AMD和ES6模塊規範等
簡單搞一個SPA應用
頁面搭建
- 接下來我們簡單搞一個SPA應用,弄一個webpack配置
- 首先,本地肯定需要安裝nodeJS,因爲webpack是基於nodeJS的
- 然後,搭建項目目錄,npm init初始化package.json文件,曬一下目錄結構
├─ .eslintrc.js ├─ .gitignore ├─ dist │ ├─ index.html │ ├─ index.js │ └─ index.js.map ├─ index.html ├─ package-lock.json ├─ package.json ├─ src │ ├─ index.js │ ├─ router.js │ ├─ untils │ │ └─ axios.js │ └─ views │ ├─ about │ └─ home └─ webpack.config.js
- 接下來,需要安裝依賴的包了
- 先安裝eslint進行語法檢查
npm install eslint eslint-config-enough babel-eslint eslint-loader --save-dev
// package.json裏面插入如下配置 "eslintConfig": { "extends": "enough", "env": { "browser": true, "node": true } }
- 先安裝eslint進行語法檢查
- 然後編輯index.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>首頁</title> </head> <body> <div id="app">首頁</div> </body> </html>
- 編輯src下的index.js文件
// 引入 router import router from './router' // 啓動 router router.start()
- 編輯src下的router.js文件
// 引入頁面文件 import home from './views/home' import about from './views/about' const routes = { '/home': home, '/about': about } // Router 類,用來控制頁面根據當前 URL 切換 class Router { start () { // 點擊瀏覽器後退 / 前進按鈕時會觸發 window.onpopstate 事件,我們在這時切換到相應頁面 // https://developer.mozilla.org/en-US/docs/Web/Events/popstate window.addEventListener('popstate', () => { this.load(window.location.pathname) }) // 打開頁面時加載當前頁面 this.load(window.location.pathname) } // 前往 path,變更地址欄 URL,並加載相應頁面 go (path) { // 變更地址欄 URL window.history.pushState({}, '', path) // 加載頁面 this.load(path) } // 加載 path 路徑的頁面 load (path) { // 首頁 if (path === '/') path = '/home' // 創建頁面實例 const view = new routes[path]() // 調用頁面方法,把頁面加載到 document.body 中 view.mount(document.body) } } // 導出 router 實例 export default new Router()
- 然後在src的view下建立home和about兩個文件夾,並在倆文件夾下新建index.js文件和style.css文件
// home下的index.js舉例 // 引入 router import router from '../../router' // 引入 html 模板,會被作爲字符串引入 import template from '../../../index.html' // 引入 css, 會生成 <style> 塊插入到 <head> 頭中 import './style.css' // 導出類 export default class { mount (container) { document.title = 'foo' container.innerHTML = template container.querySelector('#app').addEventListener('click', () => { // 調用 router.go 方法加載 /bar 頁面 router.go('/about') }) } }
webpack配置
-
安裝webpack和它的插件
npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev
- 安裝babel以支持打包生成ES5
npm install @babel/core @babel/preset-env babel-loader --save-dev
- 在package.json裏面添加配置項
"babel": { "presets": ["env"] }
- 配置webpack
const { resolve } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const history = require('connect-history-api-fallback') const convert = require('koa-connect') // 使用 WEBPACK_SERVE 環境變量檢測當前是否是在 webpack-server 啓動的開發環境中 const dev = Boolean(process.env.WEBPACK_SERVE) module.exports = { /* webpack 執行模式 development:開發環境,它會在配置文件中插入調試相關的選項,比如 moduleId 使用文件路徑方便調試 production:生產環境,webpack 會將代碼做壓縮等優化 */ mode: dev ? 'development' : 'production', /* 配置 source map 開發模式下使用 cheap-module-eval-source-map, 生成的 source map 能和源碼每行對應,方便打斷點調試 生產模式下使用 hidden-source-map, 生成獨立的 source map 文件,並且不在 js 文件中插入 source map 路徑,用於在 error report 工具中查看 (比如 Sentry) */ devtool: dev ? 'cheap-module-eval-source-map' : 'hidden-source-map', // 配置頁面入口 js 文件 entry: './src/index.js', // 配置打包輸出相關 output: { // 打包輸出目錄 path: resolve(__dirname, 'dist'), // 入口 js 的打包輸出文件名 filename: 'index.js' }, module: { /* 配置各種類型文件的加載器,稱之爲 loader webpack 當遇到 import ... 時,會調用這裏配置的 loader 對引用的文件進行編譯 */ rules: [ { /* 使用 babel 編譯 ES6 / ES7 / ES8 爲 ES5 代碼 使用正則表達式匹配後綴名爲 .js 的文件 */ test: /\.js$/, // 排除 node_modules 目錄下的文件,npm 安裝的包不需要編譯 exclude: /node_modules/, /* use 指定該文件的 loader, 值可以是字符串或者數組。 這裏先使用 eslint-loader 處理,返回的結果交給 babel-loader 處理。loader 的處理順序是從最後一個到第一個。 eslint-loader 用來檢查代碼,如果有錯誤,編譯的時候會報錯。 babel-loader 用來編譯 js 文件。 */ use: ['babel-loader', 'eslint-loader'] }, { // 匹配 html 文件 test: /\.html$/, /* 使用 html-loader, 將 html 內容存爲 js 字符串,比如當遇到 import htmlString from './template.html'; template.html 的文件內容會被轉成一個 js 字符串,合併到 js 文件裏。 */ use: 'html-loader' }, { // 匹配 css 文件 test: /\.css$/, /* 先使用 css-loader 處理,返回的結果交給 style-loader 處理。 css-loader 將 css 內容存爲 js 字符串,並且會把 background, @font-face 等引用的圖片, 字體文件交給指定的 loader 打包,類似上面的 html-loader, 用什麼 loader 同樣在 loaders 對象中定義,等會下面就會看到。 */ use: ['style-loader', 'css-loader'] }, { /* 匹配各種格式的圖片和字體文件 上面 html-loader 會把 html 中 <img> 標籤的圖片解析出來,文件名匹配到這裏的 test 的正則表達式, css-loader 引用的圖片和字體同樣會匹配到這裏的 test 條件 */ test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/, /* 使用 url-loader, 它接受一個 limit 參數,單位爲字節(byte) 當文件體積小於 limit 時,url-loader 把文件轉爲 Data URI 的格式內聯到引用的地方 當文件大於 limit 時,url-loader 會調用 file-loader, 把文件儲存到輸出目錄,並把引用的文件路徑改寫成輸出後的路徑 比如 views/foo/index.html 中 <img src="smallpic.png"> 會被編譯成 <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAA..."> 而 <img src="largepic.png"> 會被編譯成 <img src="/f78661bef717cf2cc2c2e5158f196384.png"> */ use: [ { loader: 'url-loader', options: { limit: 10000 } } ] } ] }, /* 配置 webpack 插件 plugin 和 loader 的區別是,loader 是在 import 時根據不同的文件名,匹配不同的 loader 對這個文件做處理, 而 plugin, 關注的不是文件的格式,而是在編譯的各個階段,會觸發不同的事件,讓你可以干預每個編譯階段。 */ plugins: [ /* html-webpack-plugin 用來打包入口 html 文件 entry 配置的入口是 js 文件,webpack 以 js 文件爲入口,遇到 import, 用配置的 loader 加載引入文件 但作爲瀏覽器打開的入口 html, 是引用入口 js 的文件,它在整個編譯過程的外面, 所以,我們需要 html-webpack-plugin 來打包作爲入口的 html 文件 */ new HtmlWebpackPlugin({ /* template 參數指定入口 html 文件路徑,插件會把這個文件交給 webpack 去編譯, webpack 按照正常流程,找到 loaders 中 test 條件匹配的 loader 來編譯,那麼這裏 html-loader 就是匹配的 loader html-loader 編譯後產生的字符串,會由 html-webpack-plugin 儲存爲 html 文件到輸出目錄,默認文件名爲 index.html 可以通過 filename 參數指定輸出的文件名 html-webpack-plugin 也可以不指定 template 參數,它會使用默認的 html 模板。 */ template: './src/index.html', /* 因爲和 webpack 4 的兼容性問題,chunksSortMode 參數需要設置爲 none https://github.com/jantimon/html-webpack-plugin/issues/870 */ chunksSortMode: 'none' }) ] } /* 配置開發時用的服務器,讓你可以用 http://127.0.0.1:8080/ 這樣的 url 打開頁面來調試 並且帶有熱更新的功能,打代碼時保存一下文件,瀏覽器會自動刷新。比 nginx 方便很多 如果是修改 css, 甚至不需要刷新頁面,直接生效。這讓像彈框這種需要點擊交互後纔會出來的東西調試起來方便很多。 因爲 webpack-cli 無法正確識別 serve 選項,使用 webpack-cli 執行打包時會報錯。 因此我們在這裏判斷一下,僅當使用 webpack-serve 時插入 serve 選項。 issue:https://github.com/webpack-contrib/webpack-serve/issues/19 */ if (dev) { module.exports.serve = { // 配置監聽端口,默認值 8080 port: 8080, // add: 用來給服務器的 koa 實例注入 middleware 增加功能 add: app => { /* 配置 SPA 入口 SPA 的入口是一個統一的 html 文件,比如 http://localhost:8080/foo 我們要返回給它 http://localhost:8080/index.html 這個文件 */ app.use(convert(history())) } } }
- 啓動和打包
- 用命令行啓動 node_modules\.bin\webpack-serve webpack.config.js
- 用命令行打包 /node_modules/.bin/webpack-cli
- script配置
- 每次都寫那麼多有點麻煩,用script快捷命令入口啓動,在package.json裏面配置
"scripts": { "dev": "webpack-serve webpack.config.js", "build": "webpack-cli" }
- 每次都寫那麼多有點麻煩,用script快捷命令入口啓動,在package.json裏面配置
進階配置
-
以上就完成了簡單的配置,但是還是有很多點可以優化,比如 設置靜態資源的 url 路徑前綴、各個頁面分開打包、第三方庫和業務代碼分開打包、輸出的 entry 文件加上 hash、開發環境關閉 performance.hints、配置 favicon、開發環境允許其他電腦訪問、打包時自定義部分參數、webpack-serve 處理路徑帶後綴名的文件的特殊規則、代碼中插入環境變量、簡化 import 路徑、優化 babel 編譯後的代碼性能、使用 webpack 自帶的 ES6 模塊處理功能、使用 autoprefixer 自動創建 css 的 vendor prefixes