Webpack的使用——进阶篇
Webpack的使用-進階篇
目錄
- Webpack的使用-進階篇
- 一、create-react-app react-project
- 二、vue create vue-project
- 三、自定義loader
- 3.1預備知識
- 3.2自定義babel-loader
- 四、自定義plugin
- 4.1 預備知識-compiler鉤子
- 4.1.2 tapable
- 4.1.2 compiler鉤子
- 4.2 預備知識-compilation鉤子
- 4.2.1 小插曲:nodejs環境中調試
- 4.2.2 compilation鉤子
- 4.3 自定義CopyWebpackPlugin
- 五、自定義Webpack
- 5.1 Webpack 執行流程
- 5.2 準備工作
- 5.3 使用babel解析文件
- 5.4 模塊化
- 5.5 收集所有的依賴
- 5.6 生成打包之后的bundle
- 參考資料
主要包含以下幾部分內容:
- React/Vue腳手架的詳細配置
- 基于Webpack5自定義loader/plugin
- 自己實現一個簡易的Webpack5
在學習本章之前可以先學習Webpack 的使用-基礎篇
且可配合源碼使用(⊙o⊙)… Webpack 的使用-基礎篇源碼
基礎篇主要講述如下內容:
- Webpack 簡介
- Webpack 初體驗
- Webpack 開發環境的基本配置
- Webpack 生產環境的基本配置
- Webpack 優化配置
- Webpack 配置詳情
- Webpack5 使用
更詳細的Webpack配置可以查看官網Webpack官網
Webpack 的使用——進階篇源碼
一、create-react-app react-project
本部分只講述通過腳手架創建的項目的分析路線及步驟,具體每個文件夾里面講述了什么內容分別在源碼中進行注釋講解。
通過 npm run eject將配置文件暴露出來
二、vue create vue-project
這里只講述通過腳手架創建的項目的分析路線及步驟,具體每個文件夾里面講述了什么內容分別在源碼中進行注釋。
- 通過vue inspect --mode=development > webpack.dev.js將vue開發環境配置打包一起放在webpack.dev.js文件下面,開發環境代碼只需要研究webpack.dev.js文件即可
- 通過vue inspect --mode=production > webpack.prod.js將vue生產環境配置打包一起放在webpack.prod.js文件下面,生產環境代碼只需要研究webpack.prod.js文件即可
開發環境文件webpack.dev.js 生產環境文件webpack.prod.js(除了在css上面以及多線程打包上面進行了一些修改,其余和開發環境是一樣的)
三、自定義loader
3.1預備知識
loader本質上是一個函數
安裝loader-utils:cnpm install loader-utils 在loader中引入并使用 6. 校驗options庫: 在loader中從schema-utils引入validate并使用 創建schema.json文件校驗規則并引入使用
loader3.js中代碼
// 1.1 獲取options 引入 const {getOptions } = require('loader-utils'); // 2.1 獲取validate(校驗options是否合法)引入 const {validate } = require('schema-utils');// 2.3創建schema.json文件校驗規則并引入使用 const schema = require('./schema');module.exports = function(content, map, meta) {// 1.2 獲取options 使用const options = getOptions(this);console.log(333, options);// 2.2校驗options是否合法 使用validate(schema, options, {name: 'loader3'})return content; }module.exports.pitch = function() {console.log('pitch 333'); }schema.json中代碼
{"type": "object","properties": {"name": {"type": "string","description": "名稱~"}},"additionalProperties": false // 如果設置為true表示除了校驗前面寫的string類型還可以 接著 校驗其余類型,如果為false表示校驗了string類型之后不可以再校驗其余類型 }webpack.config.js中代碼
const path = require('path');module.exports = {module: {rules: [{test: /\.js$/,use: [{loader: 'loader3',// options部分options: {name: 'jack',age: 18}}]}]},// 配置loader解析規則:我們的loader去哪個文件夾下面尋找(這里表示的是同級目錄的loaders文件夾下面尋找)resolveLoader: {modules: ['node_modules',path.resolve(__dirname, 'loaders')]}}3.2自定義babel-loader
babelSchema.json
{"type": "object","properties": {"presets": {"type": "array"}},"addtionalProperties": true }babelLoader.js
const { getOptions } = require('loader-utils'); const { validate } = require('schema-utils'); const babel = require('@babel/core'); const util = require('util');const babelSchema = require('./babelSchema.json');// babel.transform用來編譯代碼的方法 // 是一個普通異步方法 // util.promisify將普通異步方法轉化成基于promise的異步方法 const transform = util.promisify(babel.transform);module.exports = function (content, map, meta) {// 獲取loader的options配置const options = getOptions(this) || {};// 校驗babel的options的配置validate(babelSchema, options, {name: 'Babel Loader'});// 創建異步const callback = this.async();// 使用babel編譯代碼transform(content, options).then(({code, map}) => callback(null, code, map, meta)).catch((e) => callback(e))}webpack.config.js
const path = require('path');module.exports = {module: {rules: [{test: /\.js$/,loader: 'babelLoader',options: {presets: ['@babel/preset-env']}}]},// 配置loader解析規則:我們的loader去哪個文件夾下面尋找(這里表示的是同級目錄的loaders文件夾下面尋找)resolveLoader: {modules: ['node_modules',path.resolve(__dirname, 'loaders')]}}四、自定義plugin
4.1 預備知識-compiler鉤子
4.1.2 tapable
hooks tapable
文件tapable.test.js
const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable');class Lesson { constructor() {// 初始化hooks容器this.hooks = {// 同步hooks,任務會依次執行// go: new SyncHook(['address'])// SyncBailHook:一旦有返回值就會退出~go: new SyncBailHook(['address']),// 異步hooks// AsyncParallelHook:異步并行// leave: new AsyncParallelHook(['name', 'age']),// AsyncSeriesHook: 異步串行leave: new AsyncSeriesHook(['name', 'age'])} } tap() {// 往hooks容器中注冊事件/添加回調函數this.hooks.go.tap('class0318', (address) => {console.log('class0318', address);return 111;})this.hooks.go.tap('class0410', (address) => {console.log('class0410', address);})// tapAsync常用,有回調函數this.hooks.leave.tapAsync('class0510', (name, age, cb) => {setTimeout(() => {console.log('class0510', name, age);cb();}, 2000)})// 需要返回promisethis.hooks.leave.tapPromise('class0610', (name, age) => {return new Promise((resolve) => {setTimeout(() => {console.log('class0610', name, age);resolve();}, 1000)})}) }start() {// 觸發hooksthis.hooks.go.call('c318');this.hooks.leave.callAsync('jack', 18, function () {// 代表所有leave容器中的函數觸發完了,才觸發console.log('end~~~');}); } }const l = new Lesson(); l.tap(); l.start();4.1.2 compiler鉤子
4.2 預備知識-compilation鉤子
4.2.1 小插曲:nodejs環境中調試
便可以調試了,和在網頁中調試代碼一樣的
4.2.2 compilation鉤子
4.3 自定義CopyWebpackPlugin
CopyWebpackPlugin的功能:將public文件夾中的文件復制到dist文件夾下面(忽略index.html文件)
編碼思路 下載schema-utils和globby:npm install globby schema-utils -D 將from中的資源復制到to中,輸出出去 1. 過濾掉ignore的文件 2. 讀取paths中所有資源 3. 生成webpack格式的資源 4. 添加compilation中,輸出出去
const path = require('path'); const fs = require('fs'); const {promisify} = require('util')const { validate } = require('schema-utils'); const globby = require('globby');// globby用來匹配文件目標 const webpack = require('webpack');const schema = require('./schema.json'); const { Compilation } = require('webpack');const readFile = promisify(fs.readFile); const {RawSource} = webpack.sourcesclass CopyWebpackPlugin {constructor(options = {}) {// 驗證options是否符合規范validate(schema, options, {name: 'CopyWebpackPlugin'})this.options = options;}apply(compiler) {// 初始化compilationcompiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {// 添加資源的hookscompilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {// 將from中的資源復制到to中,輸出出去const { from, ignore } = this.options;const to = this.options.to ? this.options.to : '.';// context就是webpack配置// 運行指令的目錄const context = compiler.options.context; // process.cwd()// 將輸入路徑變成絕對路徑const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);// 1. 過濾掉ignore的文件// globby(要處理的文件夾,options)const paths = await globby(absoluteFrom, { ignore });console.log(paths); // 所有要加載的文件路徑數組// 2. 讀取paths中所有資源const files = await Promise.all(paths.map(async (absolutePath) => {// 讀取文件const data = await readFile(absolutePath);// basename得到最后的文件名稱const relativePath = path.basename(absolutePath);// 和to屬性結合// 沒有to --> reset.css// 有to --> css/reset.css(對應webpack.config.js中CopyWebpackPlugin插件的to的名稱css)const filename = path.join(to, relativePath);return {// 文件數據data,// 文件名稱filename}}))// 3. 生成webpack格式的資源const assets = files.map((file) => {const source = new RawSource(file.data);return {source,filename: file.filename}})// 4. 添加compilation中,輸出出去assets.forEach((asset) => {compilation.emitAsset(asset.filename, asset.source);})cb();})})}}module.exports = CopyWebpackPlugin;五、自定義Webpack
5.1 Webpack 執行流程
5.2 準備工作
5.3 使用babel解析文件
npm install @babel/parser -D用來將代碼解析成ast抽象語法樹 npm install @babel/traverse -D用來遍歷ast抽象語法樹代碼 npm install @babel/core-D用來將代碼中瀏覽器不能識別的語法進行編譯 3. 編碼思路 1. 讀取入口文件內容 2. 將其解析成ast抽象語法樹 3. 收集依賴 4. 編譯代碼:將代碼中瀏覽器不能識別的語法進行編譯
index.js
const fs = require('fs'); const path = require('path');// babel的庫 const babelParser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const { transformFromAst } = require('@babel/core');function myWebpack(config) {return new Compiler(config); }class Compiler {constructor(options = {}) {this.options = options;}// 啟動webpack打包run() {// 1. 讀取入口文件內容// 入口文件路徑const filePath = this.options.entry;const file = fs.readFileSync(filePath, 'utf-8');// 2. 將其解析成ast抽象語法樹const ast = babelParser.parse(file, {sourceType: 'module' // 解析文件的模塊化方案是 ES Module})// debugger;console.log(ast);// 獲取到文件文件夾路徑const dirname = path.dirname(filePath);// 定義存儲依賴的容器const deps = {}// 3. 收集依賴traverse(ast, {// 內部會遍歷ast中program.body,判斷里面語句類型// 如果 type:ImportDeclaration 就會觸發當前函數ImportDeclaration({node}) {// 文件相對路徑:'./add.js'const relativePath = node.source.value;// 生成基于入口文件的絕對路徑const absolutePath = path.resolve(dirname, relativePath);// 添加依賴deps[relativePath] = absolutePath;}})console.log(deps);// 4. 編譯代碼:將代碼中瀏覽器不能識別的語法進行編譯const { code } = transformFromAst(ast, null, {presets: ['@babel/preset-env']})console.log(code);} }module.exports = myWebpack;5.4 模塊化
我們開發代碼過程中講究的是模塊化開發,不同功能的代碼放在不同的文件中 創建myWebpack2–>parser.js(放入解析代碼)/Compiler.js(放入編譯代碼)/index.js(主文件)
5.5 收集所有的依賴
所有代碼位于myWebpack文件夾中 Compiler.js文件中build函數用于構建代碼,run函數中modules通過遞歸遍歷收集所有的依賴,depsGraph用于將依賴整理更好依賴關系圖(具體的代碼功能都在代碼中進行了注釋)
5.6 生成打包之后的bundle
代碼位于myWebpack–>Compiler.js中的bundle部分 整個myWebpack–>Compiler.js代碼
const path = require('path'); const fs = require('fs'); const {getAst,getDeps,getCode } = require('./parser')class Compiler {constructor(options = {}) {// webpack配置對象this.options = options;// 所有依賴的容器this.modules = [];}// 啟動webpack打包run() {// 入口文件路徑const filePath = this.options.entry;// 第一次構建,得到入口文件的信息const fileInfo = this.build(filePath);this.modules.push(fileInfo);// 遍歷所有的依賴this.modules.forEach((fileInfo) => {/**{'./add.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/add.js','./count.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/count.js'} */// 取出當前文件的所有依賴const deps = fileInfo.deps;// 遍歷for (const relativePath in deps) {// 依賴文件的絕對路徑const absolutePath = deps[relativePath];// 對依賴文件進行處理const fileInfo = this.build(absolutePath);// 將處理后的結果添加modules中,后面遍歷就會遍歷它了~(遞歸遍歷)this.modules.push(fileInfo);}})console.log(this.modules);// 將依賴整理更好依賴關系圖/*{'index.js': {code: 'xxx',deps: { 'add.js': "xxx" }},'add.js': {code: 'xxx',deps: {}}}*/const depsGraph = this.modules.reduce((graph, module) => {return {...graph,[module.filePath]: {code: module.code,deps: module.deps}}}, {})console.log(depsGraph);this.generate(depsGraph)}// 開始構建build(filePath) {// 1. 將文件解析成astconst ast = getAst(filePath);// 2. 獲取ast中所有的依賴const deps = getDeps(ast, filePath);// 3. 將ast解析成codeconst code = getCode(ast);return {// 文件路徑filePath,// 當前文件的所有依賴deps,// 當前文件解析后的代碼code}}// 生成輸出資源generate(depsGraph) {/* index.js的代碼"use strict";\n' +'\n' +'var _add = _interopRequireDefault(require("./add.js"));\n' +'\n' +'var _count = _interopRequireDefault(require("./count.js"));\n' +'\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'\n' +'console.log((0, _add["default"])(1, 2));\n' +'console.log((0, _count["default"])(3, 1));*/const bundle = `(function (depsGraph) {// require目的:為了加載入口文件function require(module) {// 定義模塊內部的require函數function localRequire(relativePath) {// 為了找到要引入模塊的絕對路徑,通過require加載return require(depsGraph[module].deps[relativePath]);}// 定義暴露對象(將來我們模塊要暴露的內容)var exports = {};(function (require, exports, code) {eval(code);})(localRequire, exports, depsGraph[module].code);// 作為require函數的返回值返回出去// 后面的require函數能得到暴露的內容return exports;}// 加載入口文件require('${this.options.entry}');})(${JSON.stringify(depsGraph)})`// 生成輸出文件的絕對路徑const filePath = path.resolve(this.options.output.path, this.options.output.filename)// 寫入文件fs.writeFileSync(filePath, bundle, 'utf-8');} }module.exports = Compiler;參考資料
1: 感謝熊健老師的視頻講解!
2:注釋很清楚的一篇關于webpack基礎的博客
總結
以上是生活随笔為你收集整理的Webpack的使用——进阶篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阻止tweak依附
- 下一篇: establish connection