前言
前端模塊化是一種開(kāi)發(fā)管理規(guī)范,前端開(kāi)發(fā)發(fā)展到現(xiàn)在,已經(jīng)有很多成熟的構(gòu)建工具可以幫助我們完成模塊化的開(kāi)發(fā)需求,但我們?nèi)孕枰钊胩骄恳幌略懒矗@些模塊化構(gòu)建工具到底幫助我們做了哪些事情,這要我們才能更好的利用它們劲件,從而提高我們的開(kāi)發(fā)效率掸哑,本篇我們將以 webpack 為例约急,進(jìn)行分析。
webpack 究竟解決了什么問(wèn)題
如何在前端項(xiàng)目中更高效的管理和維護(hù)項(xiàng)目中的每一個(gè)資源
-
模塊化的演化進(jìn)程
-
Stage 1 - 文件劃分方式
- 好處:提高了代碼復(fù)用性苗分,代碼可抽離厌蔽,可維護(hù),方便模塊間組合分解摔癣。
- 弊端:所有 JS 文件共用全局作用域奴饮,會(huì)有命名沖突,污染全局環(huán)境择浊;
沒(méi)有私有的模塊空間戴卜,可以在外面任意修改。
// a.js var a = "hello a"; console.log(a); // b.js var a = "hello b"; console.log(a);
<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> </head> <body> <script src="a.js"></script> <script src="b.js"></script> </body> </html>
-
Stage 2 - 命名空間方式
- 好處:解決了命名沖突問(wèn)題琢岩。
- 弊端:模塊成員依然可以被修改投剥。
// module-a.js window.moduleA = { var a = 'hello a' console.log(a) } // module-b.js window.moduleB = { var a = 'hello b' console.log(b) }
<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> </head> <body> <script src="module-a.js"></script> <script src="module-b.js"></script> </body> </html>
-
Stage 3 - IIFE 依賴參數(shù)
- 好處:解決了命名沖突問(wèn)題,全局作用域的問(wèn)題担孔,模塊依賴
- 弊端:模塊加載順序江锨,文件數(shù)量過(guò)多
// module-a.js ;(function(){ window.moduleA = { var name = 'module-a'; console.log(name) } })() // module-b.js ;(function(){ window.moduleB = { var name = 'module-b'; console.log(name) } })()
-
-
由模塊化產(chǎn)生的規(guī)范
-
CommonJS、AMD糕篇、 ESModules 規(guī)范
// CommonJS 服務(wù)端規(guī)范(node環(huán)境) // lib.js var counter = 3; function incCounter() { counter++; } module.exports = { counter, incCounter, }; // main.js var counter = require("./lib").counter; var incCounter = require("./lib").incCounter; console.log(counter); // 3 incCounter(); console.log(counter); // 3
//AMD規(guī)范來(lái)源于 require.js // 使用步驟 // 1. index.html中引入(require.js):https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js(cdn) // 2. script中設(shè)置啄育,amd.js 是自己的代碼文件 // <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js" data-main="./amd.js"></script> // 3. 代碼示例 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js" data-main="./amd.js"></script> <title>Document</title> </head> <body> </body> </html> // amd.js requirejs.config({ baseUrl: './', paths: { app: './app' } }); requirejs(['app/main']); // app/main.js define(function (require) { var messages = require('./messages'); console.log(messages.getHello()); }); // app/messages.js define(function () { return { getHello: function () { return 'Hello World'; } }; });
// ESModules 瀏覽器環(huán)境 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script type="module" src="./app.js"></script> </body> </html> // app.js import { name, age } from './module.js' console.log(name, age); // module.js const name = 'xinmin' const age = 18 export { name, age }
-
總結(jié):
我們所使用的 ES Modules 模塊系統(tǒng)本身就存在環(huán)境兼容問(wèn)題。盡管現(xiàn)如今主流瀏覽器的最新版本都支持這一特性拌消,但是目前還無(wú)法保證用戶的瀏覽器使用情況挑豌。所以我們還需要解決兼容問(wèn)題。隨著前端業(yè)務(wù)復(fù)雜度的增加墩崩,開(kāi)發(fā)過(guò)程中浮毯,模塊化是必須的,所以我們需要引入工具來(lái)解決模塊化所帶來(lái)的兼容性問(wèn)題泰鸡。因此,各類(lèi)如 webpack壳鹤、gulp盛龄、vite 等打包工具就產(chǎn)生了。
ES Modules 采用的是編譯時(shí)就會(huì)確定模塊依賴關(guān)系的方式芳誓。
CommonJS 的模塊規(guī)范中余舶,Node 在對(duì) JS 文件進(jìn)行編譯的過(guò)程中,會(huì)對(duì)文件中的內(nèi)容進(jìn)行頭尾包裝锹淌,在頭部添加
(function(export, require, modules, __filename, __dirname){\nxxxxxx\n})
-
-
更為理想的方式
- 在頁(yè)面中引入一個(gè) js 入口文件匿值,其余用到的模塊通過(guò)代碼控制,按需加載
- 同時(shí)在編碼代碼的過(guò)程中有著相應(yīng)的約束規(guī)范保證所有的開(kāi)發(fā)者實(shí)現(xiàn)一致
-
引申出兩點(diǎn)需求
- 一個(gè)統(tǒng)一的模塊化標(biāo)準(zhǔn)規(guī)范
- 一個(gè)可以自動(dòng)加載模塊的基礎(chǔ)庫(kù)
如何使用 webpack 實(shí)現(xiàn)模塊化打包
本質(zhì)上赂摆,webpack 是一個(gè)用于現(xiàn)代 JavaScript 應(yīng)用程序的 靜態(tài)模塊打包工具挟憔。當(dāng) webpack 處理應(yīng)用程序時(shí)钟些,它會(huì)在內(nèi)部從一個(gè)或多個(gè)入口點(diǎn)構(gòu)建一個(gè) 依賴圖(dependency graph),然后將你項(xiàng)目中所需的每一個(gè)模塊組合成一個(gè)或多個(gè) bundles绊谭,它們均為靜態(tài)資源政恍,用于展示你的內(nèi)容。
-
核心概念
入口(entery):指示 webpack 應(yīng)該使用哪個(gè)模塊來(lái)作為構(gòu)建內(nèi)部依賴圖的開(kāi)始达传,可以配置單入口或者多入口
// 單個(gè)入口(簡(jiǎn)單)寫(xiě)法 const config = { entry: "./path/to/my/entry/file.js", }; // 單個(gè)入口篙耗,對(duì)象寫(xiě)法 const config = { entry: { main: "./path/to/my/entry/file.js", }, }; // 多頁(yè)面應(yīng)用 const config = { entry: { pageOne: "./src/pageOne/index.js", pageTwo: "./src/pageTwo/index.js", pageThree: "./src/pageThree/index.js", }, };
輸出(output):指定打包輸出文件路徑與名稱(chēng)
// 基礎(chǔ)使用 const config = { output: { filename: 'bundle.js', path: '/home/proj/public/assets' } }; // 多入口起點(diǎn)(使用占位符) const config = { entry: { app: './src/app.js', search: './src/search.js' }, output: { filename: '[name].js', path: __dirname + '/dist' } } // 使用cdn和資源hash output: { path: "/home/proj/cdn/assets/[hash]", publicPath: "http://cdn.example.com/assets/[hash]/" }
Module:模塊,在 webpack 里一切皆模塊,一個(gè)模塊對(duì)應(yīng)著一個(gè)文件。webpack 會(huì)從配置的 Entry 開(kāi)始遞歸找出所有依賴的模塊宪赶。
Chunk:代碼塊,一個(gè) Chunk 由多個(gè)模塊組合而成宗弯,用于代碼合并與分割。
loader:loader 用于對(duì)模塊的源代碼進(jìn)行轉(zhuǎn)換(安裝相應(yīng)處理的 loader)
- 三種使用方式(配置(推薦)搂妻、內(nèi)聯(lián)蒙保、CLI)
// 配置 module.exports = { module: { rules: [ { test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { modules: true } } ] }, { test: /\.ts$/, use: 'ts-loader' } ] } }; // 內(nèi)聯(lián) import Styles from 'style-loader!css-loader?modules!./styles.css'; // CLI webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
插件(plugin):插件目的在于解決 loader 無(wú)法實(shí)現(xiàn)的其他事。
const HtmlWebpackPlugin = require("html-webpack-plugin"); //通過(guò) npm 安裝 const webpack = require("webpack"); //訪問(wèn)內(nèi)置的插件 const path = require("path"); const config = { entry: "./path/to/my/entry/file.js", output: { filename: "my-first-webpack.bundle.js", path: path.resolve(__dirname, "dist"), }, module: { rules: [ { test: /\.(js|jsx)$/, use: "babel-loader", }, ], }, plugins: [ new webpack.optimize.UglifyJsPlugin(), new HtmlWebpackPlugin({ template: "./src/index.html" }), ], }; module.exports = config;
模式(mode):根據(jù)開(kāi)發(fā)和生產(chǎn)環(huán)境加載不同的插件進(jìn)行處理
-
<b> webpack 的構(gòu)建流程 </b>
webpack 打包的執(zhí)行流程
- 在 webpack 函數(shù)中如傳入配置信息叽讳,返回 compiler 實(shí)例
- 調(diào)用 compiler 實(shí)例的 run 方法進(jìn)行編譯
插件處理
- 插件是在 complier 創(chuàng)建之后完成掛載的追他,但是掛在不意味著執(zhí)行、
- 某些插件是在整個(gè)流程的某些時(shí)間點(diǎn)上觸發(fā)的岛蚤,所以這種情況就要是使用到鉤子 tapable
- 插件其實(shí)就是一個(gè)具有 apply 函數(shù)的類(lèi)
處理入口
- 從配置文件中讀取 entry 的值邑狸,內(nèi)部轉(zhuǎn)化為對(duì)象進(jìn)行處理
新增屬性
- 整個(gè)打包結(jié)束之后,會(huì)產(chǎn)生出很多的內(nèi)容涤妒,這些內(nèi)容需要存儲(chǔ)
初始化編譯
- 定位入口文件的絕對(duì)路徑
- 統(tǒng)一路徑分隔符
- 調(diào)用自己的方法來(lái)實(shí)現(xiàn)編譯
loader 參與打包工作
- 讀取被打包模塊的源文件
- 使用 loader 來(lái)處理源文件(依賴的模塊)
- loader 就是一個(gè)函數(shù)单雾,接受原始數(shù)據(jù),處理之后返回給 webpack 繼續(xù)使用
- 以降序的方式的方式來(lái)執(zhí)行 loader
模塊編譯實(shí)現(xiàn)(單模塊)
- webpack 找到 a.js 模塊之后她紫,就是對(duì)它進(jìn)行處理硅堆,處理之后的內(nèi)容就是一個(gè)鍵值對(duì)
- 鍵:
./src/a.js
,而值就是 a.js 的源代碼 - 獲取被打包模塊的模塊 id
ast 語(yǔ)法樹(shù)贿讹,實(shí)現(xiàn) ast 遍歷(webpack 中解析使用 acorn)
- @babel/parser 解析器渐逃,將源代碼轉(zhuǎn)化成 ast 語(yǔ)法樹(shù)
- @babel/traverse 實(shí)現(xiàn) ast 語(yǔ)法樹(shù)遍歷
- @babel/generator 將處理后 ast 轉(zhuǎn)換成可執(zhí)行的源代碼
- @babel/core 和 @babel/preset-env 將 AST 語(yǔ)法樹(shù)轉(zhuǎn)換為瀏覽器可執(zhí)行代碼
-
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 loader
const marked = require("marked"); module.exports = (source) => { const html = marked.parse(source); const code = `module.exports = ${JSON.stringify(html)}`; // const code = `exports =${JSON.stringify(html)}` // const code = `export default = ${JSON.stringify(html)}` return code; };
-
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 plugin
// 去除開(kāi)發(fā)環(huán)境打包中多余的注釋 class RemoveCommentsPlugin { apply(compiler) { compiler.hooks.emit.tap("RemoveCommentsPlugin", (compilation) => { // compilation 可以理解為此次打包的上下文 for (const name in compilation.assets) { console.log("compilation", compilation.assets[name].source()); if (name.endsWith("js")) { const contents = compilation.assets[name].source(); const noComments = contents.replace(/\/\*{2,}\/\s?/g, ""); compilation.assets[name] = { source: () => noComments, size: () => noComments.length, }; } } }); } } module.exports = RemoveCommentsPlugin;
-
實(shí)現(xiàn)一個(gè) min-pack
const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default; const babel = require("@babel/core"); const { SyncHook } = require("tapable"); const fs = require("fs"); const path = require("path"); class Compiler { constructor(options) { this.options = options; // this.entries = new Set(); // 保存打包過(guò)程中的入口信息 webpack4中是數(shù)組 this.modules = []; // 保存打包過(guò)程中出現(xiàn)的module信息 // this.chunks = new Set(); // 保存代碼塊信息 // this.files = new Set(); // 保存所有產(chǎn)出文件的名稱(chēng) this.assets = {}; // 資源清單 this.context = this.options.context || process.cwd(); this.hooks = { entryInit: new SyncHook(["compilation"]), beforeCompile: new SyncHook(), afterCompile: new SyncHook(), afterPlugins: new SyncHook(), emit: new SyncHook(), afterEmit: new SyncHook(), }; } // 構(gòu)建啟動(dòng) run() { // 執(zhí)行 plugins // this.hooks.entryInit.call(this.assets); /// 1. 確定入口信息 let entry = {}; if (typeof this.options.entry === "string") { entry.main = this.options.entry; } else { entry = this.options.entry; } /// 2. 確定入口文件的絕對(duì)路徑 for (let entryName in entry) { // TODO: 調(diào)用自定義的方法來(lái)實(shí)現(xiàn)具體的編譯過(guò)程,得到結(jié)果 const entryModule = this.build(entry[entryName]); // 添加到module中 this.modules.push(entryModule); } /// 3. 遞歸調(diào)用獲取所有依賴內(nèi)容 this.modules.forEach(({ dependecies }) => { if (Object.keys(dependecies).length > 0) { Object.keys(dependecies).forEach((deps) => { this.modules.push(this.build(dependecies[deps])); }); } }); /// 4. 生成依賴關(guān)系圖 const dependencyGraph = this.modules.reduce( (graph, item) => ({ ...graph, [item.filename]: { dependecies: item.dependecies, code: item.code, }, }), {} ); // console.log('dependencyGraph', dependencyGraph); this.assets = dependencyGraph; // console.log('this.assets', this.assets) // 執(zhí)行 plugins this.hooks.entryInit.call(this.assets); /// 5. 生成 bundle this.generate(dependencyGraph); } // 獲取ast getAst(filePath) { let code = fs.readFileSync(filePath, "utf-8"); let loaders = []; // console.log('filePath', filePath); const rules = this.options.module?.rules; for (let i = 0; i < rules?.length; i++) { // 從眾多的 rules 當(dāng)中找到 匹配的文件的配置 if (rules[i].test.test(filePath)) { loaders = [...loaders, ...rules[i].use]; } } //* 調(diào)用loader for (let i = loaders.length - 1; i >= 0; i--) { let abPath = loaders?.[i]; if (loaders[i]?.includes("./")) { abPath = path.resolve(this.context, loaders[i]); } code = require(abPath)(code); } const ast = parser.parse(code, { sourceType: "module" }); return ast; } // 獲取依賴關(guān)系 getDependecies(ast, fileName) { const dependencies = {}; traverse(ast, { CallExpression: (nodePath) => { const dirname = path.dirname(fileName); const node = nodePath.node; // 在ast中找到了require if (node.callee.name === "require") { const rPath = node.arguments[0].value; // 獲取相對(duì)路徑 const aPath = path.resolve(dirname, rPath); dependencies[rPath] = aPath; } }, // 在ast中找到import ImportDeclaration: (nodePath) => { const dirname = path.dirname(fileName); const rPath = nodePath.node.source.value; const aPath = path.resolve(dirname, rPath); dependencies[rPath] = aPath; }, }); return dependencies; } // 編譯ast getTranslateCode(ast) { const { code } = babel.transformFromAst(ast, null, { presets: ["@babel/preset-env"], }); return code; } // 編譯 build(filename) { const ast = this.getAst(filename); const dependecies = this.getDependecies(ast, filename); const code = this.getTranslateCode(ast); return { filename, dependecies, code, }; } // 生成 generate(code) { const filePath = path.join(this.options.output.path, "main.js"); const bundle = `(function(graph){ function require(moduleId){ function localRequire(relativePath){ return require(graph[moduleId].dependecies[relativePath]) } var exports = {}; (function(require,exports,code){ eval(code) })(localRequire,exports,graph[moduleId]?.code); return exports; } require('${this.options.entry}') })(${JSON.stringify(code)})`; // console.log('filePath', filePath, bundle); fs.writeFileSync(filePath, bundle, "utf-8"); // try { // fs.writeFileSync(filePath, bundle, "utf-8") // } catch (e) { // fs.mkdirSync(path.dirname(filePath)) // fs.writeFileSync(filePath, bundle, "utf-8") // } } } module.exports = Compiler;
-
配合 min-pack 實(shí)現(xiàn) css-loader
module.exports = (source) => { // console.log('source', source); let str = ` let style = document.createElement("style"); style.innerHTML = ${JSON.stringify(source)}; document.head.appendChild(style); `; return str; };
-
配合 min-pack 實(shí)現(xiàn) DemoPlugin
class DemoPlugin { apply(compiler) { compiler.hooks.entryInit.tap("DemoPlugin", (compilation) => { if ( Array.isArray(Object.keys(compilation)) && Object.keys(compilation).length > 0 ) { for (let k in compilation) { if (k.endsWith("b.js")) { compilation[k].code = compilation[k].code + `console.log('min-webpack v1.1')`; console.log(" compilation[k].code"); } } } // compilation 可以理解為此次打包的上下文 return compilation; }); } } module.exports = DemoPlugin;
webpack 的性能優(yōu)化
優(yōu)化方向:構(gòu)建性能民褂、傳輸性能茄菊、運(yùn)行性能
-
構(gòu)建性能
-
優(yōu)化開(kāi)發(fā)體驗(yàn)
- 自動(dòng)更新:watch,webpack-dev-server赊堪,webpack-dev-middleware
- 熱更新:@pmmmwh/react-refresh-webpack-plugin react-refresh
-
加快編譯速度
- 使用最新 node面殖,npm,webpack 版本哭廉,有助于提升性能
- cache:提升二次構(gòu)建速度脊僚,緩存 webpack 模版和 chunk(webpack5)
- 減少非必要 loader、plugins 的使用遵绰,都會(huì)增加編譯時(shí)間
- 使用 loader 時(shí)辽幌,配置 rule.exclude:排除模塊范圍增淹,減少 loader 的應(yīng)用范圍
- 使用 webpack 資源模塊代替原來(lái)的 assets loader(如:file-loader/url-loader)(webpack5)
- 優(yōu)化 resolve 配置(配置別名,根據(jù)項(xiàng)目中的文件類(lèi)型定義 extensions舶衬,加快解析速度埠通。(如:resolve: { extensions: ['.tsx', '.ts', '.js'] }
- 多進(jìn)程(如 babel-loader 構(gòu)建時(shí)間較長(zhǎng),使用 thread-loader 可將 loader 放在獨(dú)立的 work 池中運(yùn)行逛犹,僅對(duì)非常耗時(shí)的 loader 使用)
- 其他:區(qū)分環(huán)境([fullhash]/[chunkhash]/[contenthash])devtool 設(shè)置
-
-
傳輸性能
-
減小打包體積
- js 壓縮(webpack5 開(kāi)箱即用端辱,默認(rèn)開(kāi)啟多進(jìn)程與緩存:terser-webpack- plugin)
- css 壓縮( optimize-css-assets-webpack-plugin)
- splitChunks
3.1 新的 chunk 可以被共享,或者模塊來(lái)自于 node_modules 文件夾
3.2 新的 chunk 體積大于 20kb(在進(jìn)行 min+gz 之前的體積)
3.3 當(dāng)按需加載 chunks 時(shí)虽画,并行請(qǐng)求的最大數(shù)量小于或等于 30
當(dāng)加載初始化頁(yè)面時(shí)舞蔽,并發(fā)請(qǐng)求的最大數(shù)量小于或等于 30 - css 文件分離(mini-css-extract-plugin)
Tree Shaking(搖樹(shù))通過(guò)配置:sideEffects,只能清除無(wú)副作用的引用,有副作用需要通過(guò)優(yōu)化引用的方式码撰。(css Tree Shaking:purgecss-webpack-plugin)
- CDN 加速:將字體渗柿,圖片等靜態(tài)資源上傳 CDN
-
-
運(yùn)行性能
-
加快加載速度
- import 動(dòng)態(tài)導(dǎo)入
- 瀏覽器緩存,創(chuàng)建 hash id
- moduleIds: "deterministic", 公共包 hash 不因?yàn)樾碌囊蕾嚩淖?/li>
- 靜態(tài)資源使用 cdn 緩存
-
-
總結(jié)
小型項(xiàng)目脖岛,添加過(guò)多優(yōu)化配置朵栖,反而會(huì)因?yàn)樘砑宇~外的 loader 與 plugin 增加構(gòu)建時(shí)間
構(gòu)建階段,使用 cache柴梆,可大大加快二次構(gòu)建速度
減少打包體積陨溅,作用最大的是壓縮代碼,分離重復(fù)代碼绍在,Tree Shaking 作用也比較大
加載速度:按需加載门扇,瀏覽器緩存,CDN 緩存效果都不錯(cuò)