前端工程化2-webpack使用與學習
起因
最近遇到系統優化的需求,webpack作為模組打包工具可以幫助我們。作為webpack的小白,該如何系統的學習呢?首先當然是學會webpack的基本使用,能做出些效果。我們先從最簡單的構建目標開始,打包一個helloworld應用程式。原始碼地址
使用webpack構建一個helloworld程式
新建一個目錄webpack-demo-helloworld,切至該目錄下
npm init -y
npm install --save webpack webpack-cli webpack-dev-server
再安裝一個html的外掛 html-webpack-plugin
npm install --save html-webpack-plugin
webpack專案基本的結構
- src
- index.js
- .gitignore
- package.json
- webpack.config.js
index.js裡的程式碼邏輯為,建立一個div,div的innerHTML設定為"hello world",然後將這個div新增至body下。.gitignore檔案是設定需要忽略提交的目錄或檔案。package.json是專案的清單,其中羅列了專案的名稱、版本、依賴包以及一些命令等等。webpack.config.js是webpack應用的配置檔案,該檔案不是必須的,簡單的配置可以直接寫到命令當中。如果專案複雜度較高,那麼webpack.config.js可以幫助開發者梳理webpack的各項配置,同時也能靈活的進行修改。webpack.config.js檔案主要包含入口配置,輸出配置,loader和外掛配置等等。
最終展現出的效果,通過“npm run start”命令可以啟動專案,在瀏覽器頁面中會打印出"hello world"。
webpack的組成部分
我們看到安裝了三個包:webpack webpack-cli webpack-dev-server。這三個工具是一個webpack應用的必不可少的組成部分。
webpack作為一個打包工具,它的職責聚焦在js模組打包,對外開放loader和plugin來豐富其生態圈。webpack-cli是webpack的命令列工具,有了這個工具我們可以使用shell命令輕鬆的控制我們的專案工程,例如啟動、構建打包等。webpack-dev-server是為我們的應用配套的本地web伺服器,例如熱載入等。這兩個工具可以提升我們的開發體驗和提高除錯效率。
下面我們來看下這三個工具是如何配合使用的,找到./node_modules/.bin目錄,裡面有三個檔案分別是webpack、webpack-cli、webpack-dev-server,三個檔案分別對應./node_modules包下webpack、webpack-cli、webpack-dev-server三個目錄下bin的檔案。切到我們的目錄下執行webpack或webpack-cli或webpack-dev-server,那麼該應用會自動執行對應的檔案。
- webpack
#!/usr/bin/env node
/**
* @param {string} command process to run
* @param {string[]} args command line arguments
* @returns {Promise<void>} promise
*/
const runCommand = (command, args) => {
const cp = require("child_process");
return new Promise((resolve, reject) => {
const executedCommand = cp.spawn(command, args, {
stdio: "inherit",
shell: true
});
executedCommand.on("error", error => {
reject(error);
});
executedCommand.on("exit", code => {
if (code === 0) {
resolve();
} else {
reject();
}
});
});
};
/**
* @param {string} packageName name of the package
* @returns {boolean} is the package installed?
*/
const isInstalled = packageName => {
if (process.versions.pnp) {
return true;
}
const path = require("path");
const fs = require("graceful-fs");
let dir = __dirname;
do {
try {
if (
fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()
) {
return true;
}
} catch (_error) {
// Nothing
}
} while (dir !== (dir = path.dirname(dir)));
return false;
};
/**
* @param {CliOption} cli options
* @returns {void}
*/
const runCli = cli => {
const path = require("path");
const pkgPath = require.resolve(`${cli.package}/package.json`);
// eslint-disable-next-line node/no-missing-require
const pkg = require(pkgPath);
// eslint-disable-next-line node/no-missing-require
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};
/**
* @typedef {Object} CliOption
* @property {string} name display name
* @property {string} package npm package name
* @property {string} binName name of the executable file
* @property {boolean} installed currently installed?
* @property {string} url homepage
*/
/** @type {CliOption} */
const cli = {
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
installed: isInstalled("webpack-cli"),
url: "https://github.com/webpack/webpack-cli"
};
if (!cli.installed) {
const path = require("path");
const fs = require("graceful-fs");
const readLine = require("readline");
const notify =
"CLI for webpack must be installed.\n" + ` ${cli.name} (${cli.url})\n`;
console.error(notify);
let packageManager;
if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
packageManager = "yarn";
} else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
packageManager = "pnpm";
} else {
packageManager = "npm";
}
const installOptions = [packageManager === "yarn" ? "add" : "install", "-D"];
console.error(
`We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
" "
)} ${cli.package}".`
);
const question = `Do you want to install 'webpack-cli' (yes/no): `;
const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr
});
// In certain scenarios (e.g. when STDIN is not in terminal mode), the callback function will not be
// executed. Setting the exit code here to ensure the script exits correctly in those cases. The callback
// function is responsible for clearing the exit code if the user wishes to install webpack-cli.
process.exitCode = 1;
questionInterface.question(question, answer => {
questionInterface.close();
const normalizedAnswer = answer.toLowerCase().startsWith("y");
if (!normalizedAnswer) {
console.error(
"You need to install 'webpack-cli' to use webpack via CLI.\n" +
"You can also install the CLI manually."
);
return;
}
process.exitCode = 0;
console.log(
`Installing '${
cli.package
}' (running '${packageManager} ${installOptions.join(" ")} ${
cli.package
}')...`
);
runCommand(packageManager, installOptions.concat(cli.package))
.then(() => {
runCli(cli);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
});
} else {
runCli(cli);
}
存在3個方法runCommand、isInstalled、runCli。
首先會判斷是否安裝webpack-cli,如果沒有安裝
-
第一會列印一條錯誤提示我們需要安裝cli,給出名稱和url。
-
第二判斷當前的包管理工具是yarn、pmpm還是npm,判斷方式就是檢視本地是否存在yarn.lock、pnpm-lock.yaml等檔案;然後給出安裝方式,如果是yarn後面的操作就是add,其他即是install;最後打印出一條提示資訊需要使用什麼包管理工具通過什麼命令進行安裝
-
第三會詢問你是否進行安裝webpack-cli,會建立一個逐行讀取的介面,不理解readLine的可以看這裡,它主要看你輸入的字串的第一個字是不是“y”,不區分大小寫。這裡判斷如果不是"y",則列印一條提示資訊告訴我們"需要通過命令列安裝webpack-cli,也可以手動自行安裝cli"。如果是“y”,則先列印一條正在安裝的資訊,然後執行runCommand,安裝完成之後自動執行runCli,安裝失敗則會提示報錯資訊。
如果檢測到安裝了則直接執行runCli.
我們再來看下runCommand和runCli這兩個函式,runCommand主要實現方式是通過node子程序的spawn方法來執行命令。runCli主要實現方式是通過require函式來呼叫bin檔案來執行命令列。
- webpack-cli
#!/usr/bin/env node
"use strict";
const Module = require("module");
const originalModuleCompile = Module.prototype._compile;
require("v8-compile-cache");
const importLocal = require("import-local");
const runCLI = require("../lib/bootstrap");
const utils = require("../lib/utils");
if (!process.env.WEBPACK_CLI_SKIP_IMPORT_LOCAL) {
// Prefer the local installation of `webpack-cli`
if (importLocal(__filename)) {
return;
}
}
process.title = "webpack";
if (utils.packageExists("webpack")) {
runCLI(process.argv, originalModuleCompile);
} else {
const { promptInstallation, logger, colors } = utils;
promptInstallation("webpack", () => {
utils.logger.error(`It looks like ${colors.bold("webpack")} is not installed.`);
})
.then(() => {
logger.success(`${colors.bold("webpack")} was installed successfully.`);
runCLI(process.argv, originalModuleCompile);
})
.catch(() => {
logger.error(
`Action Interrupted, Please try once again or install ${colors.bold(
"webpack",
)} manually.`,
);
process.exit(2);
});
}
webpack-cli核心程式碼就一行“runCLI(process.argv, originalModuleCompile);”,本質上是使用commander.js來執行node.js命令。
- webpack-dev-server
#!/usr/bin/env node
'use strict';
/* eslint-disable no-shadow, no-console */
const fs = require('fs');
const net = require('net');
const debug = require('debug')('webpack-dev-server');
const importLocal = require('import-local');
const yargs = require('yargs');
const webpack = require('webpack');
const Server = require('../lib/Server');
const setupExitSignals = require('../lib/utils/setupExitSignals');
const colors = require('../lib/utils/colors');
const processOptions = require('../lib/utils/processOptions');
const createLogger = require('../lib/utils/createLogger');
const getVersions = require('../lib/utils/getVersions');
const options = require('./options');
let server;
const serverData = {
server: null,
};
// we must pass an object that contains the server object as a property so that
// we can update this server property later, and setupExitSignals will be able to
// recognize that the server has been instantiated, because we will set
// serverData.server to the new server object.
setupExitSignals(serverData);
// Prefer the local installation of webpack-dev-server
if (importLocal(__filename)) {
debug('Using local install of webpack-dev-server');
return;
}
try {
require.resolve('webpack-cli');
} catch (err) {
console.error('The CLI moved into a separate package: webpack-cli');
console.error(
"Please install 'webpack-cli' in addition to webpack itself to use the CLI"
);
console.error('-> When using npm: npm i -D webpack-cli');
console.error('-> When using yarn: yarn add -D webpack-cli');
process.exitCode = 1;
}
yargs.usage(
`${getVersions()}\nUsage: https://webpack.js.org/configuration/dev-server/`
);
// [email protected] path : 'webpack-cli/bin/config/config-yargs'
let configYargsPath;
try {
require.resolve('webpack-cli/bin/config/config-yargs');
configYargsPath = 'webpack-cli/bin/config/config-yargs';
} catch (e) {
configYargsPath = 'webpack-cli/bin/config-yargs';
}
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line import/no-dynamic-require
require(configYargsPath)(yargs);
// It is important that this is done after the webpack yargs config,
// so it overrides webpack's version info.
yargs.version(getVersions());
yargs.options(options);
const argv = yargs.argv;
// [email protected] path : 'webpack-cli/bin/utils/convert-argv'
let convertArgvPath;
try {
require.resolve('webpack-cli/bin/utils/convert-argv');
convertArgvPath = 'webpack-cli/bin/utils/convert-argv';
} catch (e) {
convertArgvPath = 'webpack-cli/bin/convert-argv';
}
// eslint-disable-next-line import/no-extraneous-dependencies
// eslint-disable-next-line import/no-dynamic-require
const config = require(convertArgvPath)(yargs, argv, {
outputFilename: '/bundle.js',
});
function startDevServer(config, options) {
const log = createLogger(options);
let compiler;
try {
compiler = webpack(config);
} catch (err) {
if (err instanceof webpack.WebpackOptionsValidationError) {
log.error(colors.error(options.stats.colors, err.message));
// eslint-disable-next-line no-process-exit
process.exit(1);
}
throw err;
}
try {
server = new Server(compiler, options, log);
serverData.server = server;
} catch (err) {
if (err.name === 'ValidationError') {
log.error(colors.error(options.stats.colors, err.message));
// eslint-disable-next-line no-process-exit
process.exit(1);
}
throw err;
}
if (options.socket) {
server.listeningApp.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
const clientSocket = new net.Socket();
clientSocket.on('error', (err) => {
if (err.code === 'ECONNREFUSED') {
// No other server listening on this socket so it can be safely removed
fs.unlinkSync(options.socket);
server.listen(options.socket, options.host, (error) => {
if (error) {
throw error;
}
});
}
});
clientSocket.connect({ path: options.socket }, () => {
throw new Error('This socket is already used');
});
}
});
server.listen(options.socket, options.host, (err) => {
if (err) {
throw err;
}
// chmod 666 (rw rw rw)
const READ_WRITE = 438;
fs.chmod(options.socket, READ_WRITE, (err) => {
if (err) {
throw err;
}
});
});
} else {
server.listen(options.port, options.host, (err) => {
if (err) {
throw err;
}
});
}
}
processOptions(config, argv, (config, options) => {
startDevServer(config, options);
});
看到上面三個檔案的開頭第一行都會有“#!/usr/bin/env node”,這行程式碼代表什麼意思呢。為了滿足我的好奇心,特意去google了一下。在stackoverflow有人解釋了,原來這是一個shebang行。它的作用是告訴作業系統,將純文字檔案通過什麼編譯器編譯該檔案。本檔案中是使用node來變異檔案。在./node_modules/.bin目錄下的檔案都是純文字檔案,通過加上“#!”字首來表示shebang line。有了shebang line,我們可以指定特定的編譯器來編譯我們的檔案。
下面來大概看一下webpack-dev-server檔案,該檔案主要是startDevServer函式,啟動開發環境下的伺服器。通過new Server()來得到一個例項,通過追溯原始碼可以發現,Server是通過sockjs或者ws(即websocket)封裝實現的。後面我會對webpack-dev-server這個包詳細解讀的。
webpack使用總結
要完整的使用webpack打包我們的前端應用,最少要安裝三個包webpack、webpack-dev-server和webapck-cli。我們可以在專案根目錄下建立webpack.config.js來配置我們的webpack應用,基本的使用可以檢視官網指南,一步一步來學習。下一節我們深入的學習一下webapck更高階的應用,webpack本質上是一個tapable類,對吧!那麼tapable是什麼?有什麼用呢?webpack的plugin和loader如何使用、是怎麼工作的?