Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
ebpack是一個(gè)模塊捆綁器,適用于最大的單頁Web應(yīng)用程序,可以與單獨(dú)的任務(wù)運(yùn)行器捆綁在一起,它可以處理JavaScript,CSS等。webpack是前端開發(fā)人員工具集的一個(gè)很好的補(bǔ)充,使用webpack,Web開發(fā)更快,更高效,更有趣。
什么是模塊捆綁器?
在大多數(shù)編程語言中,我們可以將代碼分成多個(gè)文件,并將這些文件導(dǎo)入到應(yīng)用程序中,以使用其中包含的功能。這并不是內(nèi)置在瀏覽器中的,因此構(gòu)建模塊捆綁器會(huì)將這種功能以多種形式提供:通過異步加載模塊并在加載完成后運(yùn)行它們;或者通過將所有必需的文件組合到一個(gè)JavaScript文件中,在通過HTML中的<script>標(biāo)簽加載。
如果沒有模塊加載器和捆綁器,我們也可以隨時(shí)手動(dòng)組合文件或?qū)TML加載到無數(shù)<script>標(biāo)簽中,但這有幾個(gè)缺點(diǎn):
1、我們需要跟蹤文件應(yīng)加載的正確順序,包括哪些文件取決于哪些其他文件,并確保不包含不需要的任何文件。
2、多個(gè)<script>標(biāo)簽意味著多次調(diào)用服務(wù)器來加載所有代碼,這對(duì)性能來說更糟糕。
3、需要大量的手動(dòng)工作,而不是讓計(jì)算機(jī)為你做這些工作。
為什么選擇Webpack?
選擇Webpack,有以下幾個(gè)原因:
1、它相對(duì)新穎,因?yàn)樗軌蚪鉀Q或避免其前輩中出現(xiàn)的缺點(diǎn)和問題。
2、入門很簡單。如果我們只是想將一堆JavaScript文件捆綁在一起而沒有任何其他花哨的東西,甚至不需要配置文件。
3、它的插件系統(tǒng)使它能夠做得更多,使它非常強(qiáng)大。因此,它可能是我們需要的唯一構(gòu)建工具。
Webpack的執(zhí)行過程
Webpack從條目(entry)開始工作,通常這些是JavaScript模塊,其中webpack開始其遍歷過程。在此過程中,webpack會(huì)根據(jù)加載器配置評(píng)估條目(entry)匹配,這些配置告訴webpack如何轉(zhuǎn)換每個(gè)匹配。
解析流程
條目(entry)本身就是一個(gè)模塊。當(dāng)webpack遇到一個(gè)條目時(shí),webpack會(huì)嘗試使用條目的resolve配置將條目與文件系統(tǒng)匹配。除了node_modules之外,我們還可以告訴webpack對(duì)特定目錄執(zhí)行查找。也可以調(diào)整webpack與文件擴(kuò)展名匹配的方式,并且可以為目錄定義特定的別名。該耗竭與包章涵蓋了更詳細(xì)的這些想法。
如果解析通過失敗,webpack會(huì)引發(fā)運(yùn)行時(shí)錯(cuò)誤。如果webpack設(shè)法正確解析文件,webpack將根據(jù)加載器定義對(duì)匹配的文件執(zhí)行處理。每個(gè)加載器對(duì)模塊內(nèi)容應(yīng)用特定的轉(zhuǎn)換。
可以通過多種方式配置加載程序與已解析文件匹配的方式,包括文件類型和文件系統(tǒng)中的位置。Webpack的靈活性甚至允許我們根據(jù)文件導(dǎo)入項(xiàng)目的位置對(duì)文件應(yīng)用特定的轉(zhuǎn)換。
對(duì)webpack的加載器執(zhí)行相同的解析過程。Webpack允許我們?cè)诖_定應(yīng)使用哪個(gè)加載器時(shí)應(yīng)用類似的邏輯。由于這個(gè)原因,裝載程序已經(jīng)解析了自己的配置。如果webpack無法執(zhí)行加載程序查找,則會(huì)引發(fā)運(yùn)行時(shí)錯(cuò)誤。
總結(jié):以上就是本篇文章的全部內(nèi)容,希望能對(duì)大家的學(xué)習(xí)有所幫助。
ebpack 迭代到4.x版本后,其源碼已經(jīng)十分龐大,對(duì)各種開發(fā)場景進(jìn)行了高度抽象,閱讀成本也愈發(fā)昂貴。但是為了了解其內(nèi)部的工作原理,讓我們嘗試從一個(gè)最簡單的 webpack 配置入手,從工具設(shè)計(jì)者的角度開發(fā)一款低配版的 Webpack 。
假設(shè)某一天,我們接到了需求,需要開發(fā)一個(gè) react 單頁面應(yīng)用,頁面中包含一行文字和一個(gè)按鈕,需要支持每次點(diǎn)擊按鈕的時(shí)候讓文字發(fā)生變化。于是我們新建了一個(gè)項(xiàng)目,并且在 [根目錄]/src 下新建 JS 文件。為了模擬 Webpack 追蹤模塊依賴進(jìn)行打包的過程,我們新建了 3 個(gè) React 組件,并且在他們之間建立起一個(gè)簡單的依賴關(guān)系。
// index.js 根組件
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
ReactDom.render(<App />, document.querySelector('#container'))
復(fù)制代碼
// App.js 頁面組件
import React from 'react'
import Switch from './Switch.js'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
toggle: false
}
}
handleToggle() {
this.setState(prev => ({
toggle: !prev.toggle
}))
}
render() {
const { toggle } = this.state
return (
<div>
<h1>Hello, { toggle ? 'NervJS' : 'O2 Team'}</h1>
<Switch handleToggle={this.handleToggle.bind(this)} />
</div>
)
}
}
復(fù)制代碼
// Switch.js 按鈕組件
import React from 'react'
export default function Switch({ handleToggle }) {
return (
<button onClick={handleToggle}>Toggle</button>
)
}
復(fù)制代碼
接著我們需要一個(gè)配置文件讓 Webpack 知道我們期望它如何工作,于是我們?cè)诟夸浵滦陆ㄒ粋€(gè)文件 webpack.config.js 并且向其中寫入一些基礎(chǔ)的配置。(如果不太熟悉配置內(nèi)容可以先學(xué)習(xí) webpack中文文檔 )
// webpack.config.js
const resolve = dir => require('path').join(__dirname, dir)
module.exports = {
// 入口文件地址
entry: './src/index.js',
// 輸出文件地址
output: {
path: resolve('dist'),
fileName: 'bundle.js'
},
// loader
module: {
rules: [
{
test: /\.(js|jsx)$/,
// 編譯匹配include路徑的文件
include: [
resolve('src')
],
use: 'babel-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin()
]
}
復(fù)制代碼
其中 module 的作用是在 test 字段和文件名匹配成功時(shí)就用對(duì)應(yīng)的 loader 對(duì)代碼進(jìn)行編譯, Webpack 本身只認(rèn)識(shí) .js 、 .json 這兩種類型的文件,而通過loader,我們就可以對(duì)例如 css 等其他格式的文件進(jìn)行處理。
而對(duì)于 React 文件而言,我們需要將 JSX 語法轉(zhuǎn)換成純 JS 語法,即 React.createElement 方法,代碼才可能被瀏覽器所識(shí)別。平常我們是通過 babel-loader 并且配置好 react 的解析規(guī)則來做這一步。
經(jīng)過以上處理之后。瀏覽器真正閱讀到的按鈕組件代碼其實(shí)大概是這個(gè)樣子的。
...
function Switch(_ref) {
var handleToggle = _ref.handleToggle;
return _nervjs["default"].createElement("button", {
onClick: handleToggle
}, "Toggle");
}
復(fù)制代碼
而至于 plugin 則是一些插件,這些插件可以將對(duì)編譯結(jié)果的處理函數(shù)注冊(cè)在 Webpack 的生命周期鉤子上,在生成最終文件之前對(duì)編譯的結(jié)果做一些處理。比如大多數(shù)場景下我們需要將生成的 JS 文件插入到 Html 文件中去。就需要使用到 html-webpack-plugin 這個(gè)插件,我們需要在配置中這樣寫。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpackConfig = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
// 向plugins數(shù)組中傳入一個(gè)HtmlWebpackPlugin插件的實(shí)例
plugins: [new HtmlWebpackPlugin()]
};
復(fù)制代碼
這樣, html-webpack-plugin 會(huì)被注冊(cè)在打包的完成階段,并且會(huì)獲取到最終打包完成的入口 JS 文件路徑,生成一個(gè)形如 <script src="./dist/bundle_[hash].js"></script> 的 script 標(biāo)簽插入到 Html 中。這樣瀏覽器就可以通過 html 文件來展示頁面內(nèi)容了。
ok,寫到這里,對(duì)于一個(gè)開發(fā)者而言,所有配置項(xiàng)和需要被打包的工程代碼文件都已經(jīng)準(zhǔn)備完畢,接下來需要的就是將工作交給打包工具 Webpack ,通過 Webpack 將代碼打包成我們和瀏覽器希望看到的樣子
首先,我們需要了解Webpack打包的流程
從 Webpack 的工作流程中可以看出,我們需要實(shí)現(xiàn)一個(gè) Compiler 類,這個(gè)類需要收集開發(fā)者傳入的所有配置信息,然后指揮整體的編譯流程。我們可以把 Compiler 理解為公司老板,它統(tǒng)領(lǐng)全局,并且掌握了全局信息(客戶需求)。在了解了所有信息后它會(huì)調(diào)用另一個(gè)類 Compilation 生成實(shí)例,并且將所有的信息和工作流程托付給它, Compilation 其實(shí)就相當(dāng)于老板的秘書,需要去調(diào)動(dòng)各個(gè)部門按照要求開始工作,而 loader 和 plugin 則相當(dāng)于各個(gè)部門,只有在他們專長的工作( js , css , scss , jpg , png...)出現(xiàn)時(shí)才會(huì)去處理
為了既實(shí)現(xiàn) Webpack 打包的功能,又只實(shí)現(xiàn)核心代碼。我們對(duì)這個(gè)流程做一些簡化
首先我們新建了一個(gè) webpack 函數(shù)作為對(duì)外暴露的方法,它接受兩個(gè)參數(shù),其中一個(gè)是配置項(xiàng)對(duì)象,另一個(gè)則是錯(cuò)誤回調(diào)。
const Compiler = require('./compiler')
function webpack(config, callback) {
// 此處應(yīng)有參數(shù)校驗(yàn)
const compiler = new Compiler(config)
// 開始編譯
compiler.run()
}
module.exports = webpack
復(fù)制代碼
我們需要先在 Compiler 類的構(gòu)造方法里面收集用戶傳入的信息
class Compiler {
constructor(config, _callback) {
const {
entry,
output,
module,
plugins
} = config
// 入口
this.entryPath = entry
// 輸出文件路徑
this.distPath = output.path
// 輸出文件名稱
this.distName = output.fileName
// 需要使用的loader
this.loaders = module.rules
// 需要掛載的plugin
this.plugins = plugins
// 根目錄
this.root = process.cwd()
// 編譯工具類Compilation
this.compilation = {}
// 入口文件在module中的相對(duì)路徑,也是這個(gè)模塊的id
this.entryId = getRootPath(this.root, entry, this.root)
}
}
復(fù)制代碼
同時(shí),我們?cè)跇?gòu)造函數(shù)中將所有的 plugin 掛載到實(shí)例的 hooks 屬性中去。 Webpack 的生命周期管理基于一個(gè)叫做 tapable 的庫,通過這個(gè)庫,我們可以非常方便的創(chuàng)建一個(gè)發(fā)布訂閱模型的鉤子,然后通過將函數(shù)掛載到實(shí)例上(鉤子事件的回調(diào)支持同步觸發(fā)、異步觸發(fā)甚至進(jìn)行鏈?zhǔn)交卣{(diào)),在合適的時(shí)機(jī)觸發(fā)對(duì)應(yīng)事件的處理函數(shù)。我們?cè)?hooks 上聲明一些生命周期鉤子:
const { AsyncSeriesHook } = require('tapable') // 此處我們創(chuàng)建了一些異步鉤子
constructor(config, _callback) {
...
this.hooks = {
// 生命周期事件
beforeRun: new AsyncSeriesHook(['compiler']), // compiler代表我們將向回調(diào)事件中傳入一個(gè)compiler參數(shù)
afterRun: new AsyncSeriesHook(['compiler']),
beforeCompile: new AsyncSeriesHook(['compiler']),
afterCompile: new AsyncSeriesHook(['compiler']),
emit: new AsyncSeriesHook(['compiler']),
failed: new AsyncSeriesHook(['compiler']),
}
this.mountPlugin()
}
// 注冊(cè)所有的plugin
mountPlugin() {
for(let i=0;i<this.plugins.length;i++) {
const item = this.plugins[i]
if ('apply' in item && typeof item.apply === 'function') {
// 注冊(cè)各生命周期鉤子的發(fā)布訂閱監(jiān)聽事件
item.apply(this)
}
}
}
// 當(dāng)運(yùn)行run方法的邏輯之前
run() {
// 在特定的生命周期發(fā)布消息,觸發(fā)對(duì)應(yīng)的訂閱事件
this.hooks.beforeRun.callAsync(this) // this作為參數(shù)傳入,對(duì)應(yīng)之前的compiler
...
}
復(fù)制代碼
冷知識(shí):
每一個(gè) plugin Class 都必須實(shí)現(xiàn)一個(gè) apply 方法,這個(gè)方法接收 compiler 實(shí)例,然后將真正的鉤子函數(shù)掛載到 compiler.hook 的某一個(gè)聲明周期上。
如果我們聲明了一個(gè)hook但是沒有掛載任何方法,在 call 函數(shù)觸發(fā)的時(shí)候是會(huì)報(bào)錯(cuò)的。但是實(shí)際上 Webpack 的每一個(gè)生命周期鉤子除了掛載用戶配置的 plugin ,都會(huì)掛載至少一個(gè) Webpack 自己的 plugin ,所以不會(huì)有這樣的問題。更多關(guān)于 tapable 的用法也可以移步 Tapable
接下來我們需要聲明一個(gè) Compilation 類,這個(gè)類主要是執(zhí)行編譯工作。在 Compilation 的構(gòu)造函數(shù)中,我們先接收來自老板 Compiler 下發(fā)的信息并且掛載在自身屬性中。
class Compilation {
constructor(props) {
const {
entry,
root,
loaders,
hooks
} = props
this.entry = entry
this.root = root
this.loaders = loaders
this.hooks = hooks
}
// 開始編譯
async make() {
await this.moduleWalker(this.entry)
}
// dfs遍歷函數(shù)
moduleWalker = async () => {}
}
復(fù)制代碼
因?yàn)槲覀冃枰獙⒋虬^程中引用過的文件都編譯到最終的代碼包里,所以需要聲明一個(gè)深度遍歷函數(shù) moduleWalker (這個(gè)名字是筆者取的,不是webpack官方取的),顧名思義,這個(gè)方法將會(huì)從入口文件開始,依次對(duì)文件進(jìn)行第一步和第二步編譯,并且收集引用到的其他模塊,遞歸進(jìn)行同樣的處理。
編譯步驟分為兩步
一句話解釋 __webpack_require__ 的作用就是,將模塊之間原本 文件地址 -> 文件內(nèi)容 的關(guān)系替換成了 對(duì)象的key -> 對(duì)象的value(文件內(nèi)容) 這樣的關(guān)系。
在完成第二步編譯的同時(shí),會(huì)對(duì)當(dāng)前模塊內(nèi)的引用進(jìn)行收集,并且返回到 Compilation 中, 這樣 moduleWalker 才能對(duì)這些依賴模塊進(jìn)行遞歸的編譯。當(dāng)然其中大概率存在循環(huán)引用和重復(fù)引用,我們會(huì)根據(jù)引用文件的路徑生成一個(gè)獨(dú)一無二的 key 值,在 key 值重復(fù)時(shí)進(jìn)行跳過。
// 存放處理完畢的模塊代碼Map
moduleMap = {}
// 根據(jù)依賴將所有被引用過的文件都進(jìn)行編譯
async moduleWalker(sourcePath) {
if (sourcePath in this.moduleMap) return
// 在讀取文件時(shí),我們需要完整的以.js結(jié)尾的文件路徑
sourcePath = completeFilePath(sourcePath)
const [ sourceCode, md5Hash ] = await this.loaderParse(sourcePath)
const modulePath = getRootPath(this.root, sourcePath, this.root)
// 獲取模塊編譯后的代碼和模塊內(nèi)的依賴數(shù)組
const [ moduleCode, relyInModule ] = this.parse(sourceCode, path.dirname(modulePath))
// 將模塊代碼放入ModuleMap
this.moduleMap[modulePath] = moduleCode
this.assets[modulePath] = md5Hash
// 再依次對(duì)模塊中的依賴項(xiàng)進(jìn)行解析
for(let i=0;i<relyInModule.length;i++) {
await this.moduleWalker(relyInModule[i], path.dirname(relyInModule[i]))
}
}
復(fù)制代碼
如果將dfs的路徑給log出來,我們就可以看到這樣的流程
async loaderParse(entryPath) {
// 用utf8格式讀取文件內(nèi)容
let [ content, md5Hash ] = await readFileWithHash(entryPath)
// 獲取用戶注入的loader
const { loaders } = this
// 依次遍歷所有l(wèi)oader
for(let i=0;i<loaders.length;i++) {
const loader = loaders[i]
const { test : reg, use } = loader
if (entryPath.match(reg)) {
// 判斷是否滿足正則或字符串要求
// 如果該規(guī)則需要應(yīng)用多個(gè)loader,從最后一個(gè)開始向前執(zhí)行
if (Array.isArray(use)) {
while(use.length) {
const cur = use.pop()
const loaderHandler =
typeof cur.loader === 'string'
// loader也可能來源于package包例如babel-loader
? require(cur.loader)
: (
typeof cur.loader === 'function'
? cur.loader : _ => _
)
content = loaderHandler(content)
}
} else if (typeof use.loader === 'string') {
const loaderHandler = require(use.loader)
content = loaderHandler(content)
} else if (typeof use.loader === 'function') {
const loaderHandler = use.loader
content = loaderHandler(content)
}
}
}
return [ content, md5Hash ]
}
復(fù)制代碼
然而這里遇到了一個(gè)小插曲,就是我們平常使用的 babel-loader 似乎并不能在 Webpack 包以外的場景被使用,在 babel-loader 的文檔中看到了這樣一句話
This package allows transpiling JavaScript files using Babel and webpack.
不過好在 @babel/core 和 webpack 并無聯(lián)系,所以只能辛苦一下,再手寫一個(gè) loader 方法去解析 JS 和 ES6 的語法。
const babel = require('@babel/core')
module.exports = function BabelLoader (source) {
const res = babel.transform(source, {
sourceType: 'module' // 編譯ES6 import和export語法
})
return res.code
}
復(fù)制代碼
當(dāng)然,編譯規(guī)則可以作為配置項(xiàng)傳入,但是為了模擬真實(shí)的開發(fā)場景,我們需要配置一下 babel.config.js 文件
module.exports = function (api) {
api.cache(true)
return {
"presets": [
['@babel/preset-env', {
targets: {
"ie": "8"
},
}],
'@babel/preset-react', // 編譯JSX
],
"plugins": [
["@babel/plugin-transform-template-literals", {
"loose": true
}]
],
"compact": true
}
}
復(fù)制代碼
于是,在獲得了 loader 處理過的代碼之后,理論上任何一個(gè)模塊都已經(jīng)可以在瀏覽器或者單元測試中直接使用了。但是我們的代碼是一個(gè)整體,還需要一種合理的方式來組織代碼之間互相引用的關(guān)系。
上面也解釋了我們?yōu)槭裁匆褂?__webpack_require__ 函數(shù)。這里我們得到的代碼仍然是字符串的形式,為了方便我們使用 eval 函數(shù)將字符串解析成直接可讀的代碼。當(dāng)然這只是求快的方式,對(duì)于 JS 這種解釋型語言,如果一個(gè)一個(gè)模塊去解釋編譯的話,速度會(huì)非常慢。事實(shí)上真正的生產(chǎn)環(huán)境會(huì)將模塊內(nèi)容封裝成一個(gè) IIFE (立即自執(zhí)行函數(shù)表達(dá)式)
總而言之,在第二部編譯 parse 函數(shù)中我們需要做的事情其實(shí)很簡單,就是將所有模塊中的 require 方法的函數(shù)名稱替換成 __webpack_require__ 即可。我們?cè)谶@一步使用的是 babel 全家桶。 babel 作為業(yè)內(nèi)頂尖的JS編譯器,分析代碼的步驟主要分為兩步,分別是詞法分析和語法分析。簡單來說,就是對(duì)代碼片段進(jìn)行逐詞分析,根據(jù)當(dāng)前單詞生成一個(gè)上下文語境。然后進(jìn)行再判斷下一個(gè)單詞在上下文語境中所起的作用。
注意,在這一步中我們還可以“順便”搜集模塊的依賴項(xiàng)數(shù)組一同返回(用于 dfs 遞歸)
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const generator = require('@babel/generator').default
...
// 解析源碼,替換其中的require方法來構(gòu)建ModuleMap
parse(source, dirpath) {
const inst = this
// 將代碼解析成ast
const ast = parser.parse(source)
const relyInModule = [] // 獲取文件依賴的所有模塊
traverse(ast, {
// 檢索所有的詞法分析節(jié)點(diǎn),當(dāng)遇到函數(shù)調(diào)用表達(dá)式的時(shí)候執(zhí)行,對(duì)ast樹進(jìn)行改寫
CallExpression(p) {
// 有些require是被_interopRequireDefault包裹的
// 所以需要先找到_interopRequireDefault節(jié)點(diǎn)
if (p.node.callee && p.node.callee.name === '_interopRequireDefault') {
const innerNode = p.node.arguments[0]
if (innerNode.callee.name === 'require') {
inst.convertNode(innerNode, dirpath, relyInModule)
}
} else if (p.node.callee.name === 'require') {
inst.convertNode(p.node, dirpath, relyInModule)
}
}
})
// 將改寫后的ast樹重新組裝成一份新的代碼, 并且和依賴項(xiàng)一同返回
const moduleCode = generator(ast).code
return [ moduleCode, relyInModule ]
}
/**
* 將某個(gè)節(jié)點(diǎn)的name和arguments轉(zhuǎn)換成我們想要的新節(jié)點(diǎn)
*/
convertNode = (node, dirpath, relyInModule) => {
node.callee.name = '__webpack_require__'
// 參數(shù)字符串名稱,例如'react', './MyName.js'
let moduleName = node.arguments[0].value
// 生成依賴模塊相對(duì)【項(xiàng)目根目錄】的路徑
let moduleKey = completeFilePath(getRootPath(dirpath, moduleName, this.root))
// 收集module數(shù)組
relyInModule.push(moduleKey)
// 替換__webpack_require__的參數(shù)字符串,因?yàn)檫@個(gè)字符串也是對(duì)應(yīng)模塊的moduleKey,需要保持統(tǒng)一
// 因?yàn)閍st樹中的每一個(gè)元素都是babel節(jié)點(diǎn),所以需要使用'@babel/types'來進(jìn)行生成
node.arguments = [ types.stringLiteral(moduleKey) ]
}
復(fù)制代碼
執(zhí)行到這一步, compilation 的使命其實(shí)就已經(jīng)完成了。如果我們平時(shí)有去觀察生成的 js 文件的話,會(huì)發(fā)現(xiàn)打包出來的樣子是一個(gè)立即執(zhí)行函數(shù),主函數(shù)體是一個(gè)閉包,閉包中緩存了已經(jīng)加載的模塊 installedModules ,以及定義了一個(gè) __webpack_require__ 函數(shù),最終返回的是函數(shù)入口所對(duì)應(yīng)的模塊。而函數(shù)的參數(shù)則是各個(gè)模塊的 key-value 所組成的對(duì)象。
我們?cè)谶@里通過 ejs 模板去進(jìn)行拼接,將之前收集到的 moduleMap 對(duì)象進(jìn)行遍歷,注入到ejs模板字符串中去。
模板代碼
// template.ejs
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})({
<%for(let key in modules) {%>
"<%-key%>":
(function(module, exports, __webpack_require__) {
eval(
`<%-modules[key]%>`
);
}),
<%}%>
});
復(fù)制代碼
生成bundle.js
/**
* 發(fā)射文件,生成最終的bundle.js
*/
emitFile() { // 發(fā)射打包后的輸出結(jié)果文件
// 首先對(duì)比緩存判斷文件是否變化
const assets = this.compilation.assets
const pastAssets = this.getStorageCache()
if (loadsh.isEqual(assets, pastAssets)) {
// 如果文件hash值沒有變化,說明無需重寫文件
// 只需要依次判斷每個(gè)對(duì)應(yīng)的文件是否存在即可
// 這一步省略!
} else {
// 緩存未能命中
// 獲取輸出文件路徑
const outputFile = path.join(this.distPath, this.distName);
// 獲取輸出文件模板
// const templateStr = this.generateSourceCode(path.join(__dirname, '..', "bundleTemplate.ejs"));
const templateStr = fs.readFileSync(path.join(__dirname, '..', "template.ejs"), 'utf-8');
// 渲染輸出文件模板
const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.compilation.moduleMap});
this.assets = {};
this.assets[outputFile] = code;
// 將渲染后的代碼寫入輸出文件中
fs.writeFile(outputFile, this.assets[outputFile], function(e) {
if (e) {
console.log('[Error] ' + e)
} else {
console.log('[Success] 編譯成功')
}
});
// 將緩存信息寫入緩存文件
fs.writeFileSync(resolve(this.distPath, 'manifest.json'), JSON.stringify(assets, null, 2))
}
}
復(fù)制代碼
在這一步中我們根據(jù)文件內(nèi)容生成的 Md5Hash 去對(duì)比之前的緩存來加快打包速度,細(xì)心的同學(xué)會(huì)發(fā)現(xiàn) Webpack 每次打包都會(huì)生成一個(gè)緩存文件 manifest.json ,形如
{
"main.js": "./js/main7b6b4.js",
"main.css": "./css/maincc69a7ca7d74e1933b9d.css",
"main.js.map": "./js/main7b6b4.js.map",
"vendors~main.js": "./js/vendors~main3089a.js",
"vendors~main.css": "./css/vendors~maincc69a7ca7d74e1933b9d.css",
"vendors~main.js.map": "./js/vendors~main3089a.js.map",
"js/28505f.js": "./js/28505f.js",
"js/28505f.js.map": "./js/28505f.js.map",
"js/34c834.js": "./js/34c834.js",
"js/34c834.js.map": "./js/34c834.js.map",
"js/4d218c.js": "./js/4d218c.js",
"js/4d218c.js.map": "./js/4d218c.js.map",
"index.html": "./index.html",
"static/initGlobalSize.js": "./static/initGlobalSize.js"
}
復(fù)制代碼
這也是文件斷點(diǎn)續(xù)傳中常用到的一個(gè)判斷,這里就不做詳細(xì)的展開了
做完這一步,我們已經(jīng)基本大功告成了(誤:如果不考慮令人智息的debug過程的話),接下來我們?cè)?package.json 里面配置好打包腳本
"scripts": {
"build": "node build.js"
}
復(fù)制代碼
運(yùn)行 yarn build
( @ο@ ) 哇~激動(dòng)人心的時(shí)刻到了。
然而...
看著打包出來的這一坨奇怪的東西報(bào)錯(cuò),心里還是有點(diǎn)想笑的。檢查了一下發(fā)現(xiàn)是因?yàn)榉匆?hào)遇到注釋中的反引號(hào)于是拼接字符串提前結(jié)束了。好吧,那么我在 babel traverse 時(shí)加了幾句代碼,刪除掉了代碼中所有的注釋。但是隨之而來的又是一些其他的問題。
好吧,可能在實(shí)際 react 生產(chǎn)打包中還有一些其他的步驟,但是這不在今天討論的話題當(dāng)中。此時(shí),鬼魅的框架涌上心頭。我腦中想起了京東凹凸實(shí)驗(yàn)室自研的高性能,兼容性優(yōu)秀,緊跟 react 版本的類react框架 NervJS ,或許 NervJS 平易近人(誤)的代碼能夠支持這款令人抱歉的打包工具
于是我們?cè)?babel.config.js 中配置alias來替換 react 依賴項(xiàng)。( React 項(xiàng)目轉(zhuǎn) NervJS 就是這么簡單)
module.exports = function (api) {
api.cache(true)
return {
...
"plugins": [
...
[
"module-resolver", {
"root": ["."],
"alias": {
"react": "nervjs",
"react-dom": "nervjs",
// Not necessary unless you consume a module using `createClass`
"create-react-class": "nerv-create-class"
}
}
]
],
"compact": true
}
}
復(fù)制代碼
運(yùn)行 yarn build
( @ο@ ) 哇~代碼終于成功運(yùn)行了起來,雖然存在著許多的問題,但是至少這個(gè) webpack 在設(shè)計(jì)如此簡單的情況下已經(jīng)有能力支持大部分JS框架了。感興趣的同學(xué)也可以自己嘗試寫一寫,或者直接從 這里 clone下來看
毫無疑問, Webpack 是一個(gè)非常優(yōu)秀的代碼模塊打包工具(雖然它的官網(wǎng)非常低調(diào)的沒有任何slogen)。一款非常優(yōu)秀的工具,必然是在保持了自己本身的特性的同時(shí),同時(shí)能夠賦予其他開發(fā)者在其基礎(chǔ)上拓展設(shè)想之外作品的能力。如果有能力深入學(xué)習(xí)這些工具,對(duì)于我們?cè)诖a工程領(lǐng)域的認(rèn)知也會(huì)有很大的提升。
大家平時(shí)學(xué)習(xí)web前端的時(shí)候肯定會(huì)遇到很多問題,小編我為大家準(zhǔn)備了web前端學(xué)習(xí)資料,將這些免費(fèi)分享給大家!如果想要的可以找我領(lǐng)取
領(lǐng)取方式:
如果想獲取這些學(xué)習(xí)資料,先關(guān)注我然后私信小編“01”即可免費(fèi)領(lǐng)取!(私信方法:點(diǎn)擊我頭像進(jìn)我主頁右上面有個(gè)私信按鈕)
如果這篇文章對(duì)你有幫助,請(qǐng)記得給我來個(gè)評(píng)論+轉(zhuǎn)發(fā)噢
本定義
wbpack 是一個(gè)打包模塊化 javascript 的工具,在 webpack 里一切文件皆模塊,通過 loader轉(zhuǎn)換文件,通過 plugin 注入鉤子,最后輸出由多個(gè)模塊組合成的文件,webpack 專注構(gòu)建模塊化項(xiàng)目,webpack 可以看做是模塊打包機(jī):它做的事情是,分析你的項(xiàng)目結(jié)構(gòu),找到 JavaScript 模塊以及其它的一些瀏覽器不能直接運(yùn)行的拓展語言(Scss,TypeScript 等),并將其打包為合適的格式以供瀏覽器使用。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。