開篇
很多人都或多或少使用過 webpack,但是很少有人能夠系統(tǒng)的學習 webpack 配置,遇到錯誤的時候就會一臉懵仍翰,不知道從哪查起憔鬼?性能優(yōu)化時也不知道能做什么龟劲,網(wǎng)上的優(yōu)化教程是不是符合自己的項目?等一系列問題轴或!本文從最基礎(chǔ)配置一步步到一個完善的大型項目的過程昌跌。讓你對 webpack 再也不會畏懼,讓它真正成為你的得力助手照雁!
本文從下面幾個課題來實現(xiàn)
- 課題 1:<a href="#0_1">初探 webpack蚕愤?探究 webpack 打包原理</a>
- 課題 2:<a href="#0_2">搭建開發(fā)環(huán)境跟生產(chǎn)環(huán)境</a>
- 課題 3:<a href="#0_3">基礎(chǔ)配置之loader</a>
- 課時 4:<a href="#0_4">webpack性能優(yōu)化</a>
- 課時 5:<a href="#0_5">手寫loader實現(xiàn)可選鏈</a>
- 課時 6:<a href="#0_6">webpack編譯優(yōu)化</a>
- 課時 7:<a href="#0_7">多頁面配置</a>
- 課時 8:<a href="#0_8">手寫一個webpack插件</a>
- 課時 9:<a href="#0_9">構(gòu)建 ssr</a>
項目地址
https://github.com/luoxue-victor/learn_webpack/
我把每一課都切成了不同的分支,大家可以根據(jù)課時一步步學習
腳手架
npm i -g webpack-box
使用
webpack-box dev # 開發(fā)環(huán)境
webpack-box build # 生產(chǎn)環(huán)境
webpack-box dll # 編譯差分包
webpack-box dev index # 指定頁面編譯(多頁面)
webpack-box build index # 指定頁面編譯(多頁面)
webpack-box build index --report # 開啟打包分析
webpack-box build:ssr # 編譯ssr
webpack-box ssr:server # 在 server 端運行
在 package.json 中使用
{
"scripts": {
"dev": "webpack-box dev",
"build": "webpack-box build",
"dll": "webpack-box dll",
"build:ssr": "webpack-box build:ssr",
"ssr:server": "webpack-box ssr:server"
}
}
使用
npm run build --report # 開啟打包分析
擴展配置
box.config.js
module.exports = function (config) {
/**
* @param {object} dll 開啟差分包
* @param {object} pages 多頁面配置 通過 box run/build index 來使用
* @param {function} chainWebpack
* @param {string} entry 入口
* @param {string} output 出口
* @param {string} publicPath
* @param {string} port
*/
return {
entry: 'src/main.js',
output: 'dist',
publicPath: '/common/',
port: 8888,
dll: {
venders: ['vue', 'react']
},
pages: {
index: {
entry: 'src/main.js',
template: 'public/index.html',
filename: 'index.html',
},
index2: {
entry: 'src/main.js',
template: 'public/index2.html',
filename: 'index2.html',
}
},
chainWebpack(config) {
}
}
}
<a name="0_1">課題 1:初探 webpack饺蚊?探究 webpack 打包原理</a>
想要學好 webpack
萍诱,我們首先要了解 webpack
的機制,我們先從js加載css開始學習污呼。
我們從下面這個小練習開始走進 webpack
吧
在 index.js
中引入 index.css
const css = require('./index.css')
console.log(css)
css 文件并不能被 js 識別裕坊,webpack 也不例外,上述的寫法不出意外會報錯
我們?nèi)绾巫?webpack 識別 css 呢曙求,答案就在 webpack 給我們提供了 loader 機制碍庵,可以讓我們通過loader 將任意的文件轉(zhuǎn)成 webpack 可以識別的文件
本章主要講解
- <a href="#1_1">webpack 基礎(chǔ)配置</a>
- <a href="#1_2">解析 bundle 如何加載模塊</a>
- <a href="#1_3">動態(tài) import 加載原理</a>
- <a href="#1_4">使用 webpack-chain 重寫配置</a>
- <a href="#1_5">課時 1 小結(jié)</a>
<a name="1_1">webpack 基礎(chǔ)配置</a>
需要的依賴包
package.json
{
"scripts": {
"dev": "cross-env NODE_ENV=development webpack", // 開發(fā)環(huán)境
"build": "cross-env NODE_ENV=production webpack" // 生產(chǎn)環(huán)境
},
"dependencies": {
"cross-env": "^6.0.3", // 兼容各種環(huán)境
"css-loader": "^3.2.0",
"rimraf": "^3.0.0", // 刪除文件
"webpack": "^4.41.2"
},
"devDependencies": {
"webpack-cli": "^3.3.10"
}
}
webpack 基礎(chǔ)配置
webpack.config.js
const path = require('path');
const rimraf = require('rimraf');
// 刪除 dist 目錄
rimraf.sync('dist');
// webpack 配置
module.exports = {
entry: './src/index',
mode: process.env.NODE_ENV,
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
css 引入到 js
src/index.js
const css = require('css-loader!./index.css');
const a = 100;
console.log(a, css);
測試 css
src/index.css
body {
width: 100%;
height: 100vh;
background-color: orange;
}
<a name="1_2">解析 bundle 如何加載模塊</a>
我刪掉了一些注釋跟一些干擾內(nèi)容,這樣看起來會更清晰一點
-
bundle
是一個立即執(zhí)行函數(shù)悟狱,可以認為它是把所有模塊捆綁在一起的一個巨型模塊静浴。 -
webpack
將所有模塊打包成了bundle
的依賴,通過一個對象注入 -
0 模塊
就是入口 -
webpack
通過__webpack_require__
引入模塊 -
__webpack_require__
就是我們使用的require
挤渐,被webpack
封裝了一層
dist/bundle.js
(function(modules) {
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
});
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
module.l = true;
return module.exports;
}
return __webpack_require__((__webpack_require__.s = 0));
})({
'./src/index.js': function(module, exports, __webpack_require__) {
eval(`
const css = __webpack_require__("./src/style/index.css")
const a = 100;
console.log(a, css)
`);
},
'./src/style/index.css': function(module, exports, __webpack_require__) {
eval(`
exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false);
exports.push([module.i, "body {
width: 100%;
height: 100vh;
background-color: orange;
}", ""]);
`);
},
0: function(module, exports, __webpack_require__) {
module.exports = __webpack_require__('./src/index.js');
}
});
<a name="1_3">動態(tài) import 加載原理</a>
如果我們把 index.js 的 require 改成 import 會發(fā)生什么苹享?
我們知道 import
跟 require
的區(qū)別是,import
是動態(tài)加載只有在用到的時候才會去加載,而 require
只要聲明了就會加載得问,webpack
遇到了 require
就會把它當成一個模塊加載到 bundle
的依賴里
那么問題來了囤攀,如果我們使用了 import 去引用一個模塊,它是如何加載的呢宫纬?
require 改成 import()
src/index.js
// const css = require('css-loader!./index.css');
const css = import('css-loader!./index.css');
const a = 100;
console.log(a, css);
動態(tài)加載打包結(jié)果
除了正常的 bundle
之外焚挠,我們還可以看見一個 0.boundle.js
0.boundle.js
就是我們的動態(tài)加載的 index.css
模塊
|-- bundle.js
|-- 0.boundle.js
動態(tài)模塊
0.boundle.js
這個文件就是把我們 import
的模塊放進了一個單獨的 js
文件中
(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
[0],
{
'./node_modules/css-loader/dist/runtime/api.js': function(
module,
exports,
__webpack_require__
) {
'use strict';
eval(`
...
`);
},
'./src/style/index.css': function(module, exports, __webpack_require__) {
eval(`
exports = module.exports = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js")(false));
exports.push([module.i, \`body {
width: 100%;
height: 100vh;
background-color: orange;
},"\`]
`);
}
}
]);
動態(tài)模塊加載邏輯
我們再看下 dist/bundle.js
方便理解,我把大部分代碼和注釋都刪掉了
原理很簡單漓骚,就是利用的 jsonp 的實現(xiàn)原理加載模塊蝌衔,只是在這里并不是從 server 拿數(shù)據(jù)而是從其他模塊中
- 調(diào)用模塊時會在
window
上注冊一個webpackJsonp
數(shù)組,window['webpackJsonp'] = window['webpackJsonp'] || [] - 當我們
import
時蝌蹂,webpack
會調(diào)用__webpack_require__.e(0)
方法噩斟,也就是requireEnsure
-
webpack
會動態(tài)創(chuàng)建一個script
標簽去加載這個模塊,加載成功后會將該模塊注入到webpackJsonp
中 -
webpackJsonp.push
會調(diào)用webpackJsonpCallback
拿到模塊 - 模塊加載完(then)再使用
__webpack_require__
獲取模塊
(function(modules) {
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId,
chunkId,
i = 0,
resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (
Object.prototype.hasOwnProperty.call(installedChunks, chunkId) &&
installedChunks[chunkId]
) {
resolves.push(installedChunks[chunkId][0]);
}
// 模塊安裝完
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
// 執(zhí)行所有 promise 的 resolve 函數(shù)
resolves.shift()();
}
}
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + '' + ({}[chunkId] || chunkId) + '.bundle.js';
}
function __webpack_require__(moduleId) {
// ...
}
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// ...
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
script.src = jsonpScriptSrc(chunkId);
onScriptComplete = function(event) {
// 處理異常孤个,消除副作用
// ...
};
var timeout = setTimeout(function() {
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
// ...
// 動態(tài)加載模塊
return Promise.all(promises);
};
var jsonpArray = (window['webpackJsonp'] = window['webpackJsonp'] || []);
// 重寫數(shù)組 push 方法
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++)
webpackJsonpCallback(jsonpArray[i]);
return __webpack_require__((__webpack_require__.s = 0));
})({
'./src/index.js': function(module, exports, __webpack_require__) {
eval(`
const css = __webpack_require__.e(0).then(__webpack_require__.t.bind(null, "./src/style/index.css", 7))
const a = 100;
console.log(a, css)
`);
},
0: function(module, exports, __webpack_require__) {
eval(`module.exports = __webpack_require__("./src/index.js");`);
}
});
<a name="1_4">使用 webpack-chain 重寫配置</a>
我們用 webpack-chain 來寫 webpack 的配置剃允,原因是 webpack-chain 的方式更加靈活
官方解釋
webpack-chain
嘗試通過提供可鏈式或順流式的API
創(chuàng)建和修改webpack
配置。API
的Key
部分可以由用戶指定的名稱引用齐鲤,這有助于跨項目修改配置方式的標準化斥废。
const path = require('path');
const rimraf = require('rimraf');
const Config = require('webpack-chain');
const config = new Config();
const resolve = src => {
return path.join(process.cwd(), src);
};
// 刪除 dist 目錄
rimraf.sync('dist');
config
// 入口
.entry('src/index')
.add(resolve('src/index.js'))
.end()
// 模式
// .mode(process.env.NODE_ENV) 等價下面
.set('mode', process.env.NODE_ENV)
// 出口
.output.path(resolve('dist'))
.filename('[name].bundle.js');
config.module
.rule('css')
.test(/\.css$/)
.use('css')
.loader('css-loader');
module.exports = config.toConfig();
<a name="1_5">課時 1 小結(jié)</a>
至此課時 1 已經(jīng)結(jié)束了,我們主要做了以下事情
- webpack 基礎(chǔ)配置
- 將 css 通過 css-loader 打包進 js 中
- 解析 bundle 如何加載模塊的
- webpack 如何實現(xiàn)的動態(tài)加載模塊
學習一個工具我們不僅要看懂它的配置给郊,還要對它的原理一起了解营袜,只有學到框架的精髓,我們才能應對如今大前端如此迅猛的發(fā)展丑罪。
<a name="0_2">課題 2:搭建開發(fā)環(huán)境跟生產(chǎn)環(huán)境</a>
本章提要:
- <a href="#2_1">目錄</a>
- <a href="#2_2">實現(xiàn)可插拔配置</a>
- <a href="#2_3">構(gòu)建生產(chǎn)環(huán)境</a>
- <a href="#2_4">構(gòu)建開發(fā)環(huán)境(devServer)</a>
- <a href="#2_5">提取 css</a>
- <a href="#2_6">自動生成 html</a>
- <a href="#2_7">項目測試</a>
<a name="2_1">目錄</a>
│── build
│ │── base.js // 公共部分
│ │── build.js
│ └── dev.js
│── config
│ │── base.js // 基礎(chǔ)配置
│ │── css.js // css 配置
│ │── HtmlWebpackPlugin.js // html 配置
│ └── MiniCssExtractPlugin.js // 提取css
│── public // 公共資源
│ └── index.html // html 模版
└── src // 開發(fā)目錄
│── style
│ └── index.css
└── main.js // 主入口
<a name="2_2">實現(xiàn)可插拔配置</a>
package.json
{
"scripts": {
"dev": "cross-env NODE_ENV=development node build/dev.js",
"build": "cross-env NODE_ENV=production node build/build.js"
},
"dependencies": {
"cross-env": "^6.0.3",
"css-loader": "^3.2.0",
"cssnano": "^4.1.10",
"ora": "^4.0.3",
"rimraf": "^3.0.0",
"webpack": "^4.41.2"
},
"devDependencies": {
"extract-text-webpack-plugin": "^3.0.2",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"vue-cli-plugin-commitlint": "^1.0.4",
"webpack-chain": "^6.0.0",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0"
}
}
build/base.js
const { findSync } = require('../lib');
const Config = require('webpack-chain');
const config = new Config();
const files = findSync('config');
const path = require('path');
const resolve = p => {
return path.join(process.cwd(), p);
};
module.exports = () => {
const map = new Map();
files.map(_ => {
const name = _.split('/')
.pop()
.replace('.js', '');
return map.set(name, require(_)(config, resolve));
});
map.forEach(v => v());
return config;
};
<a name="2_3">構(gòu)建生產(chǎn)環(huán)境</a>
build/build.js
const rimraf = require('rimraf');
const ora = require('ora');
const chalk = require('chalk');
const path = require('path');
// 刪除 dist 目錄
rimraf.sync(path.join(process.cwd(), 'dist'));
const config = require('./base')();
const webpack = require('webpack');
const spinner = ora('開始構(gòu)建項目...');
spinner.start();
webpack(config.toConfig(), function(err, stats) {
spinner.stop();
if (err) throw err;
process.stdout.write(
stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n'
);
if (stats.hasErrors()) {
console.log(chalk.red('構(gòu)建失敗\n'));
process.exit(1);
}
console.log(chalk.cyan('build完成\n'));
});
<a name="2_4">構(gòu)建開發(fā)環(huán)境(devServer)</a>
build/dev.js
const config = require('./base')();
const webpack = require('webpack');
const chalk = require('chalk');
const WebpackDevServer = require('webpack-dev-server');
const port = 8080;
const publicPath = '/common/';
config.devServer
.quiet(true)
.hot(true)
.https(false)
.disableHostCheck(true)
.publicPath(publicPath)
.clientLogLevel('none');
const compiler = webpack(config.toConfig());
// 拿到 devServer 參數(shù)
const chainDevServer = compiler.options.devServer;
const server = new WebpackDevServer(
compiler,
Object.assign(chainDevServer, {})
);
['SIGINT', 'SIGTERM'].forEach(signal => {
process.on(signal, () => {
server.close(() => {
process.exit(0);
});
});
});
// 監(jiān)聽端口
server.listen(port);
new Promise(() => {
compiler.hooks.done.tap('dev', stats => {
const empty = ' ';
const common = `App running at:
- Local: http://127.0.0.1:${port}${publicPath}\n`;
console.log(chalk.cyan('\n' + empty + common));
});
});
<a name="2_5">提取 css</a>
config/css.js
css 提取 loader 配置
module.exports = (config, resolve) => {
return (lang, test) => {
const baseRule = config.module.rule(lang).test(test);
const normalRule = baseRule.oneOf('normal');
applyLoaders(normalRule);
function applyLoaders(rule) {
rule
.use('extract-css-loader')
.loader(require('mini-css-extract-plugin').loader)
.options({
publicPath: './'
});
rule
.use('css-loader')
.loader('css-loader')
.options({});
}
};
};
css 提取插件 MiniCssExtractPlugin
config/MiniCssExtractPlugin.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (config, resolve) => {
return () => {
config
.oneOf('normal')
.plugin('mini-css-extract')
.use(MiniCssExtractPlugin);
};
};
<a name="2_6">自動生成 html</a>
config/HtmlWebpackPlugin.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = (config, resolve) => {
return () => {
config.plugin('html').use(HtmlWebpackPlugin, [
{
template: 'public/index.html'
}
]);
};
};
<a name="2_7">項目測試</a>
測試 html 模板
public/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>learn_webpack</title>
<body></body>
</html>
測試 css 模板
src/style/index.css
.test {
width: 200px;
height: 200px;
color: red;
background-color: orange;
}
程序入口
src/main.js
require('./style/index.css');
const h2 = document.createElement('h2');
h2.className = 'test';
h2.innerText = 'test';
document.body.append(h2);
<a name="0_3">課題 3:基礎(chǔ)配置之loader</a>
本章提要:
- <a href="#3_1">配置 babel</a>
- <a href="#3_2">使用 babel 配置 ts</a>
- <a href="#3_3">ts 靜態(tài)類型檢查</a>
- <a href="#3_4">友好錯誤提示插件</a>
- <a href="#3_5">配置樣式荚板,style,css吩屹、less跪另、sass、postcss 等</a>
- <a href="#3_6">postcss 配置</a>
- <a href="#3_7">編譯前后 css 對比</a>
- <a href="#3_8">配置 autoprefixer</a>
- <a href="#3_9">開啟 source map</a>
目錄
增加以下文件
│──── config // 配置目錄
│ │── babelLoader.js // babel-loader 配置
│ │── ForkTsChecker.js // ts 靜態(tài)檢查
│ │── FriendlyErrorsWebpackPlugin.js // 友好錯誤提示
│ └── style
│──── src // 開發(fā)目錄
│ │── style
│ │ │── app.css
│ │ │── index.less // 測試 less
│ │ │── index.scss // 測試 sass
│ │ └── index.postcss // 測試 postcss
│ └── ts
│ └── index.ts // 測試 ts
│── babel.js
│── postcss.config.js // postcss 配置
│── tsconfig.json // ts 配置
└──── dist // 打包后的目錄
│── app.bundle.js
│── app.css
└── index.html
<a name="3_1">配置 babel</a>
config/babelLoader.js
module.exports = (config, resolve) => {
const baseRule = config.module.rule('js').test(/.js│.tsx?$/);
const babelPath = resolve('babel.js');
const babelConf = require(babelPath);
const version = require(resolve('node_modules/@babel/core/package.json'))
.version;
return () => {
baseRule
.use('babel')
.loader(require.resolve('babel-loader'))
.options(babelConf({ version }));
};
};
<a name="3_2">使用 babel 配置 ts</a>
這里我們使用 babel
插件 @babel/preset-typescript
將 ts
轉(zhuǎn)成 js煤搜,并使用
ForkTsCheckerWebpackPlugin
免绿、ForkTsCheckerNotifierWebpackPlugin
插件進行錯誤提示。
babel.js
module.exports = function(api) {
return {
presets: [
[
'@babel/preset-env',
{
targets: {
chrome: 59,
edge: 13,
firefox: 50,
safari: 8
}
}
],
[
'@babel/preset-typescript',
{
allExtensions: true
}
]
],
plugins: [
'@babel/plugin-transform-typescript',
'transform-class-properties',
'@babel/proposal-object-rest-spread'
]
};
};
<a name="3_3">ts 靜態(tài)類型檢查</a>
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin');
module.exports = (config, resolve) => {
return () => {
config.plugin('ts-fork').use(ForkTsCheckerWebpackPlugin, [
{
// 將async設(shè)為false擦盾,可以阻止Webpack的emit以等待類型檢查器/linter嘲驾,并向Webpack的編譯添加錯誤。
async: false
}
]);
// 將TypeScript類型檢查錯誤以彈框提示
// 如果fork-ts-checker-webpack-plugin的async為false時可以不用
// 否則建議使用迹卢,以方便發(fā)現(xiàn)錯誤
config.plugin('ts-notifier').use(ForkTsCheckerNotifierWebpackPlugin, [
{
title: 'TypeScript',
excludeWarnings: true,
skipSuccessful: true
}
]);
};
};
<a name="3_4">友好錯誤提示插件</a>
config/FriendlyErrorsWebpackPlugin.js
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
module.exports = (config, resolve) => {
return () => {
config.plugin('error').use(FriendlyErrorsWebpackPlugin);
};
};
<a name="3_5">配置樣式辽故,style,css腐碱、less誊垢、sass、postcss 等</a>
module.exports = (config, resolve) => {
const createCSSRule = (lang, test, loader, options = {}) => {
const baseRule = config.module.rule(lang).test(test);
const normalRule = baseRule.oneOf('normal');
normalRule
.use('extract-css-loader')
.loader(require('mini-css-extract-plugin').loader)
.options({
hmr: process.env.NODE_ENV === 'development',
publicPath: '/'
});
normalRule
.use('css-loader')
.loader(require.resolve('css-loader'))
.options({});
normalRule.use('postcss-loader').loader(require.resolve('postcss-loader'));
if (loader) {
const rs = require.resolve(loader);
normalRule
.use(loader)
.loader(rs)
.options(options);
}
};
return () => {
createCSSRule('css', /\.css$/, 'css-loader', {});
createCSSRule('less', /\.less$/, 'less-loader', {});
createCSSRule('scss', /\.scss$/, 'sass-loader', {});
createCSSRule('postcss', /\.p(ost)?css$/);
};
};
<a name="3_6">postcss 配置</a>
module.exports = {
plugins: {
'postcss-px-to-viewport': {
unitToConvert: 'px',
viewportWidth: 750,
unitPrecision: 5,
propList: ['*'],
viewportUnit: 'vw',
fontViewportUnit: 'vw',
selectorBlackList: [],
minPixelValue: 1,
mediaQuery: false,
replace: true,
exclude: [],
landscape: false,
landscapeUnit: 'vw',
landscapeWidth: 568
}
}
};
<a name="3_7">編譯前后 css 對比</a>
src/style/index.less
/* index.less */
.test {
width: 300px;
}
dist/app.css
/* index.css */
.test {
width: 36.66667vw;
height: 26.66667vw;
color: red;
background-color: orange;
}
/* app.css */
.test {
font-size: 8vw;
}
/* index.less */
.test {
width: 40vw;
}
/* index.scss */
.test {
height: 40vw;
}
/* index.postcss */
.test {
background: green;
height: 26.66667vw;
}
<a name="3_8">配置 autoprefixer</a>
自動添加 css 前綴
postcss.config.js
module.exports = {
plugins: {
autoprefixer: {
overrideBrowserslist: [
'> 1%',
'last 3 versions',
'iOS >= 8',
'Android >= 4',
'Chrome >= 40'
]
}
}
};
轉(zhuǎn)換前
/* index.css */
.test {
width: 200px;
height: 200px;
color: red;
display: flex;
background-color: orange;
}
轉(zhuǎn)換后
/* index.css */
.test {
width: 26.66667vw;
height: 26.66667vw;
color: red;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
background-color: orange;
}
<a name="3_9">開啟 source map</a>
config.devtool('cheap-source-map');
└── dist
│── app.bundle.js
│── app.bundle.js.map
│── app.css
│── app.css.map
└── index.html
在源文件下會有一行注釋,證明開啟了 sourcemap
/*# sourceMappingURL=app.css.map*/
<a name="0_4">課時 4:webpack性能優(yōu)化</a>
本章講解
- <a href="#4_1">分離 Manifest</a>
- <a href="#4_2">Code Splitting(代碼分割)</a>
- <a href="#4_3">Bundle Splitting(打包分割)</a>
- <a href="#4_4">Tree Shaking(刪除死代碼)</a>
- <a href="#4_5">開啟 gzip</a>
<a href="#4_1">分離 Manifest</a>
module.exports = (config, resolve) => {
return () => {
config
.optimization
.runtimeChunk({
name: "manifest"
})
}
}
<a href="#4_2">Code Splitting</a>
- 使用動態(tài) import 或者 require.ensure 語法喂走,在第一節(jié)已經(jīng)講解
- 使用
babel-plugin-import
插件按需引入一些組件庫
<a href="#4_3">Bundle Splitting</a>
將公共的包提取到 chunk-vendors
里面殃饿,比如你require('vue'),webpack 會將 vue 打包進 chunk-vendors.bundle.js
module.exports = (config, resolve) => {
return () => {
config
.optimization.splitChunks({
chunks: 'async',
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 3,
maxInitialRequests: 3,
cacheGroups: {
vendors: {
name: `chunk-vendors`,
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: `chunk-common`,
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
})
config.optimization.usedExports(true)
}
}
<a href="#4_4">Tree Shaking</a>
config/optimization.js
config.optimization.usedExports(true);
src/treeShaking.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
在 main.js 中只引用了 cube
import { cube } from './treeShaking';
console.log(cube(2));
未使用 Tree Shaking
{
"./src/treeShaking.js": function(
module,
__webpack_exports__,
__webpack_require__
) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "square", function() {
return square;
});
__webpack_require__.d(__webpack_exports__, "cube", function() {
return cube;
});
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
}
}
使用了 Tree Shaking
這里只導出了 cube 函數(shù)芋肠,并沒有將 square 導出去
當然你可以看見 square 函數(shù)還是在 bundle 里面乎芳,但是在壓縮的時候就會被干掉了,因為它并沒有被引用
{
"./src/treeShaking.js": function(
module,
__webpack_exports__,
__webpack_require__
) {
"use strict";
__webpack_require__.d(__webpack_exports__, "a", function() {
return cube;
});
function square(x) {
return x * x;
}
function cube(x) {
return x * x * x;
}
}
}
只有當函數(shù)給定輸入后帖池,產(chǎn)生相應的輸出秒咐,且不修改任何外部的東西,才可以安全做shaking的操作
如何使用tree-shaking碘裕?
- 確保代碼是es6格式,即 export,import
- package.json中攒钳,設(shè)置 sideEffects
- 確保 tree-shaking 的函數(shù)沒有副作用
- babelrc中設(shè)置presets [["@babel/preset-env", { "modules": false }]] 禁止轉(zhuǎn)換模塊帮孔,交由webpack進行模塊化處理
- 結(jié)合uglifyjs-webpack-plugin
其實在 webpack4
我們根本不需要做這些操作了,因為 webpack
在生產(chǎn)環(huán)境已經(jīng)幫我們默認添加好了不撑,開箱即用文兢!
<a href="#4_5">開啟 gzip</a>
CompressionWebpackPlugin.js
const CompressionWebpackPlugin = require('compression-webpack-plugin');
module.exports = (config, resolve) => {
return () => {
config.plugin('CompressionWebpackPlugin').use(CompressionWebpackPlugin, [
{
algorithm: 'gzip',
test: /\.js(\?.*)?$/i,
threshold: 10240,
minRatio: 0.8
}
]);
};
};
<a name="0_5">課時 5:手寫loader實現(xiàn)可選鏈</a>
本章內(nèi)容
- <a href="#5_1">什么是 webpack loader</a>
- <a href="#5_2">可選鏈介紹</a>
- <a href="#5_3">loader 實現(xiàn)可選鏈</a>
<a name="5_1">什么是 webpack loader</a>
webpack loader
是 webpack
為了處理各種類型文件的一個中間層,webpack
本質(zhì)上就是一個 node
模塊焕檬,它不能處理 js
以外的文件姆坚,那么 loader
就幫助 webpack
做了一層轉(zhuǎn)換,將所有文件都轉(zhuǎn)成字符串实愚,你可以對字符串進行任意操作/修改兼呵,然后返回給 webpack
一個包含這個字符串的對象,讓 webpack
進行后面的處理腊敲。如果把 webpack
當成一個垃圾工廠的話击喂,那么 loader
就是這個工廠的垃圾分類!
<a name="5_2">可選鏈介紹</a>
這里并不是純粹意義上的可選鏈碰辅,因為 babel
跟 ts
都已經(jīng)支持了懂昂,我們也沒有必要去寫一個完整的可選鏈,只是來加深一下對 loader
的理解没宾, loader
在工作當中能幫助我們做什么凌彬?
用途
當我們訪問一個對象屬性時不必擔心這個對象是 undefined
而報錯,導致程序不能繼續(xù)向下執(zhí)行
解釋
在 ?
之前的所有訪問鏈路都是合法的循衰,不會產(chǎn)生報錯
const obj = {
foo: {
bar: {
baz: 2
}
}
}
console.log(obj.foo.bar?.baz) // 2
// 被轉(zhuǎn)成 obj && obj.foo && obj.foo.bar && obj.foo.bar.baz
console.log(obj.foo.err?.baz) // undefined
// 被轉(zhuǎn)成 obj && obj.foo && obj.foo.err && obj.foo.err.baz
<a name="5_3">loader 實現(xiàn)可選鏈</a>
配置loader铲敛,options-chain-loader
config/OptionsChainLoader.js
module.exports = (config, resolve) => {
const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
const normalRule = baseRule.oneOf('normal');
return () => {
normalRule
.use('options-chain')
.loader(resolve('options-chain-loader'))
}
}
其實就是正則替換,loader
將整個文件全部轉(zhuǎn)換成字符串会钝,content
就是整個文件的內(nèi)容原探,對 content
進行修改,修改完成后再返回一個新的 content
就完成了一個 loader
轉(zhuǎn)換。是不是很簡單咽弦?
下面的操作意思就是徒蟆,我們匹配 obj.foo.bar?.
并把它轉(zhuǎn)成 obj && obj.foo && obj.foo.bar && obj.foo.bar.
options-chain-loader.js
module.exports = function(content) {
return content.replace(new RegExp(/([\$_\w\.]+\?\.)/,'g'),function(res) {
let str = res.replace(/\?\./,'');
let arrs = str.split('.');
let strArr = [];
for(let i = 1; i <= arrs.length; i++) {
strArr.push(arrs.slice(0,i).join('.'));
}
let compile = strArr.join('&&');
const done = compile + '&&' + str + '.'
return done;
});
};
<a name="0_6">課時 6:webpack編譯優(yōu)化</a>
本章內(nèi)容
- <a href="#6_1">cache-loader</a>
- <a href="#6_2">DllPlugin</a>
- <a href="#6_3">threadLoader</a>
<a name="6_1">cache-loader</a>
cache-loader
主要是將打包好的文件緩存在硬盤的一個目錄里,一般存在 node_modules/.cache
下型型,當你再次 build
的時候如果此文件沒有修改就會從緩存中讀取已經(jīng)編譯過的文件段审,只有有改動的才會被編譯,這樣就大大降低了編譯的時間闹蒜。尤其是項目越大時越明顯寺枉。
此項目使用前后數(shù)據(jù)對比 3342ms --> 2432ms 效果還是比較明顯
這里只對 babel 加入了 cache-loader,因為我們的 ts/js 都是由 babel 進行編譯的绷落,不需要對 ts-loader 緩存(我們也沒有用到)
config/cacheLoader.js
module.exports = (config, resolve) => {
const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
const babelPath = resolve('babel.js')
const babelConf = require(babelPath);
const version = require(resolve('node_modules/@babel/core/package.json')).version
return () => {
baseRule
.exclude
.add(filepath => {
// 不緩存 node_modules 下的文件
return /node_modules/.test(filepath)
})
.end()
.use('cache-loader')
.loader('cache-loader')
.options({
// 緩存位置
cacheDirectory: resolve('node_modules/.cache/babel')
})
}
}
<a name="6_2">DllPlugin</a>
DllPlugin 是將第三方長期不變的包與實際項目隔離開來并分別打包姥闪,當我們 build 時再將已經(jīng)打包好的 dll 包引進來就 ok 了
我提取了兩個包 vue、react砌烁,速度差不多提升了 200ms筐喳,從 2698ms 到 2377ms
打包 dll
build/dll.js
const path = require("path");
const dllPath = path.join(process.cwd(), 'dll');
const Config = require('webpack-chain');
const config = new Config();
const webpack = require('webpack')
const rimraf = require('rimraf');
const ora = require('ora')
const chalk = require('chalk')
const BundleAnalyzerPlugin = require('../config/BundleAnalyzerPlugin')(config)
BundleAnalyzerPlugin()
config
.entry('dll')
.add('vue')
.add('react')
.end()
.set('mode', "production")
.output
.path(dllPath)
.filename('[name].js')
.library("[name]")
.end()
.plugin('DllPlugin')
.use(webpack.DllPlugin, [{
name: "[name]",
path: path.join(process.cwd(), 'dll', 'manifest.json'),
}])
.end()
rimraf.sync(path.join(process.cwd(), 'dll'))
const spinner = ora('開始構(gòu)建項目...')
spinner.start()
webpack(config.toConfig(), function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red('構(gòu)建失敗\n'))
process.exit(1)
}
console.log(chalk.cyan('build完成\n'))
})
將 dll 包合并
const webpack = require('webpack')
module.exports = (config, resolve) => {
return () => {
config.plugin('DllPlugin')
.use(webpack.DllReferencePlugin, [{
context: process.cwd(),
manifest: require(resolve('dll/manifest.json'))
}])
}
}
<a name="6_3">threadLoader</a>
測試效果變差了 ??,線程數(shù)越小編譯速度越快
config/threadLoader.js
module.exports = (config, resolve) => {
const baseRule = config.module.rule('js').test(/.js|.tsx?$/);
return () => {
const useThreads = true;
if (useThreads) {
const threadLoaderConfig = baseRule
.use('thread-loader')
.loader('thread-loader');
threadLoaderConfig.options({ workers: 3 })
}
}
}
<a name="0_7">課時 7:多頁面配置</a>
注意
-
棄用
npm run build & npm run dev & npm run dll -
改成
box build & box dev & box dll -
link
npm link 將 box 命令鏈接到全局
本章內(nèi)容
- <a href="#7_1">使用</a>
- <a href="#7_2">改造為腳手架</a>
- <a href="#7_3">多頁面配置</a>
<a name="7_1">使用</a>
box build # 不加參數(shù)則會編譯所有頁面函喉,并清空 dist
box dev # 默認編譯 index 頁面
參數(shù)
# index2 是指定編譯的頁面避归。不會清空 dist
# report 開啟打包分析
box build index2 --report
box dev index2 --report
<a name="7_2">改造為腳手架</a>
分成三個命令,進行不同操作
- build
- dev
- dll
bin/box.js
#!/usr/bin/env node
const chalk = require('chalk')
const program = require('commander')
const packageConfig = require('../package.json');
const { cleanArgs } = require('../lib')
const path = require('path')
const __name__ = `build,dev,dll`
let boxConf = {}
let lock = false
try {
boxConf = require(path.join(process.cwd(), 'box.config.js'))()
} catch (error) { }
program
.usage('<command> [options]')
.version(packageConfig.version)
.command('build [app-page]')
.description(`構(gòu)建開發(fā)環(huán)境`)
.option('-r, --report', '打包分析報告')
.option('-d, --dll', '合并差分包')
.action(async (name, cmd) => {
const options = cleanArgs(cmd)
const args = Object.assign(options, { name }, boxConf)
if (lock) return
lock = true;
if (boxConf.pages) {
Object.keys(boxConf.pages).forEach(page => {
args.name = page;
require('../build/build')(args)
})
} else {
require('../build/build')(args)
}
})
program
.usage('<command> [options]')
.version(packageConfig.version)
.command('dev [app-page]')
.description(`構(gòu)建生產(chǎn)環(huán)境`)
.option('-d, --dll', '合并差分包')
.action(async (name, cmd) => {
const options = cleanArgs(cmd)
const args = Object.assign(options, { name }, boxConf)
if (lock) return
lock = true;
require('../build/dev')(args)
})
program
.usage('<command> [options]')
.version(packageConfig.version)
.command('dll [app-page]')
.description(`編譯差分包`)
.action(async (name, cmd) => {
const options = cleanArgs(cmd)
const args = Object.assign(options, { name }, boxConf)
if (lock) return
lock = true;
require('../build/dll')(args)
})
program.parse(process.argv).args && program.parse(process.argv).args[0];
program.commands.forEach(c => c.on('--help', () => console.log()))
if (process.argv[2] && !__name__.includes(process.argv[2])) {
console.log()
console.log(chalk.red(` 沒有找到 ${process.argv[2]} 命令`))
console.log()
program.help()
}
if (!process.argv[2]) {
program.help()
}
<a name="7_3">多頁面配置</a>
box.config.js
module.exports = function (config) {
return {
entry: 'src/main.js', // 默認入口
dist: 'dist', // 默認打包目錄
publicPath: '/',
port: 8888,
pages: {
index: {
entry: 'src/main.js',
template: 'public/index.html',
filename: 'index.html',
},
index2: {
entry: 'src/main.js',
template: 'public/index2.html',
filename: 'index2.html',
}
},
chainWebpack(config) {
}
}
}
<a name="0_8">課時 8:手寫一個webpack插件</a>
如果把 webpack 當成一個垃圾工廠管呵,loader 就是垃圾分類梳毙,將所有垃圾整理好交給 webpack。plugin 就是如何去處理這些垃圾捐下。
webpack 插件寫起來很簡單账锹,就是你要知道各種各樣的鉤子在什么時候觸發(fā),然后你的邏輯寫在鉤子里面就ok了
-
apply
函數(shù)是 webpack 在調(diào)用 plugin 的時候執(zhí)行的坷襟,你可以認為它是入口 -
compiler
暴露了和 webpack 整個生命周期相關(guān)的鉤子 -
Compilation
暴露了與模塊和依賴有關(guān)的粒度更小的事件鉤子
本節(jié)概要
- <a href="#8_1">實現(xiàn)一個 CopyPlugin</a>
- <a href="#8_2">使用</a>
<a name="8_1">實現(xiàn)一個 CopyPlugin</a>
我們今天寫一個 copy 的插件牌废,在webpack構(gòu)建完成之后,將目標目錄下的文件 copy 到另一個目錄下
const fs = require('fs-extra')
const globby = require('globby')
class CopyDirWebpackPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
const opt = this.options
compiler.plugin('done', (stats) => {
if (process.env.NODE_ENV === 'production') {
(async ()=>{
const toFilesPath = await globby([`${opt.to}/**`, '!.git/**'])
toFilesPath.forEach(filePath => fs.removeSync(filePath))
const fromFilesPath = await globby([`${opt.from}/**`])
fromFilesPath.forEach(fromPath => {
const cachePath = fromPath
fromPath = fromPath.replace('dist', opt.to)
const dirpaths = fromPath.substring(0, fromPath.lastIndexOf('/'))
fs.mkdirpSync(dirpaths)
fs.copySync(cachePath, fromPath)
})
console.log(` 完成copy ${opt.from} to ${opt.to}`)
})()
}
});
}
}
module.exports = CopyDirWebpackPlugin
<a name="8_2">使用</a>
將打包出來的 dist 目錄下的內(nèi)容 copy 到 dist2 目錄下
const CopyPlugin = require('../webapck-plugin-copy');
module.exports = ({ config }) => {
return () => {
config.plugin('copy-dist')
.use(CopyPlugin, [{
from: 'dist',
to: 'dist2'
}])
}
}
<a name="0_9">課時 9:構(gòu)建 ssr</a>
ssr 就是服務(wù)端渲染啤握,做 ssr 的好處就是為了處理 spa 的不足鸟缕,比如 seo 優(yōu)化,服務(wù)端緩存等問題排抬。
今天主要用 react 的 ssr 來做一個簡單的實例懂从,讓大家更清晰的入門
本章概要
- <a href="#8_1">創(chuàng)建 box build:ssr</a>
- <a href="#8_1">編譯 ssr</a>
- <a href="#8_1">編譯 jsx 語法</a>
- <a href="#8_1">入口區(qū)分服務(wù)端/客戶端</a>
- <a href="#8_1">服務(wù)端渲染</a>
- <a href="#8_1">小結(jié)</a>
<a name="8_1">創(chuàng)建 box build:ssr</a>
老規(guī)矩,先來一個 box build:ssr
命令讓程序可以執(zhí)行
執(zhí)行 box build:ssr
會調(diào)用 build/ssr
執(zhí)行編譯
program
.usage('<command> [options]')
.version(packageConfig.version)
.command('build:ssr [app-page]')
.description(`服務(wù)端渲染`)
.action(async (name, cmd) => {
const options = cleanArgs(cmd);
const args = Object.assign(options, { name }, boxConf);
if (lock) return;
lock = true;
require('../build/ssr')(args);
});
<a name="8_1">編譯 ssr</a>
與其他的編譯沒有什么區(qū)別蹲蒲,值得住的是
- target 指定為 umd 模式
- globalObject 為 this
- 入口改為 ssr.jsx
.libraryTarget('umd')
.globalObject('this')
build/ssr.js
module.exports = function(options) {
const path = require('path');
const Config = require('webpack-chain');
const config = new Config();
const webpack = require('webpack');
const rimraf = require('rimraf');
const ora = require('ora');
const chalk = require('chalk');
const PATHS = {
build: path.join(process.cwd(), 'static'),
ssrDemo: path.join(process.cwd(), 'src', 'ssr.jsx')
};
require('../config/babelLoader')({ config, tsx: true })();
require('../config/HtmlWebpackPlugin')({
config,
options: {
publicPath: '/',
filename: 'client.ssr.html'
}
})();
config
.entry('ssr')
.add(PATHS.ssrDemo)
.end()
.set('mode', 'development') // production
.output.path(PATHS.build)
.filename('[name].js')
.libraryTarget('umd')
.globalObject('this')
.library('[name]')
.end();
rimraf.sync(path.join(process.cwd(), PATHS.build));
const spinner = ora('開始構(gòu)建項目...');
spinner.start();
webpack(config.toConfig(), function(err, stats) {
spinner.stop();
if (err) throw err;
process.stdout.write(
stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n'
);
if (stats.hasErrors()) {
console.log(chalk.red('構(gòu)建失敗\n'));
process.exit(1);
}
console.log(chalk.cyan('build完成\n'));
});
};
<a name="8_1">編譯 jsx 語法</a>
因為我們是用 react 寫的番甩,避免不了會用到 jsx 語法,所以我們需要在 babel-loader
中使用 @babel/preset-react
npm i @babel/preset-react -D
config/babelLoader.js
if (tsx) {
babelConf.presets.push('@babel/preset-react');
}
<a name="8_1">入口區(qū)分服務(wù)端/客戶端</a>
區(qū)分服務(wù)端跟客戶端分別渲染
const React = require("react");
const ReactDOM = require("react-dom");
const SSR = <div onClick={() => alert("hello")}>Hello world</div>;
if (typeof document === "undefined") {
console.log('在服務(wù)端渲染')
module.exports = SSR;
} else {
console.log('在客戶端渲染')
const renderMethod = !module.hot ? ReactDOM.render : ReactDOM.hydrate;
renderMethod(SSR, document.getElementById("app"));
}
<a name="8_1">服務(wù)端渲染</a>
- 將打包出來的 static 文件夾作為一個服務(wù)
- 訪問 http://127.0.0.1:8080届搁,進入服務(wù)端渲染的頁面
- 再執(zhí)行一遍 ssr.js 進行事件綁定
module.exports = function (options) {
const express = require("express");
const { renderToString } = require("react-dom/server");
const chalk = require('chalk')
const SSR = require("../static/ssr");
const port = process.env.PORT || 8080;
server(port);
function server(port) {
const app = express();
app.use(express.static("static"));
app.get("/", (req, res) =>
res.status(200).send(renderMarkup(renderToString(SSR)))
);
const empty = ' '
const common = `App running at:
- Local: http://127.0.0.1:${port}\n`
console.log(chalk.cyan('\n' + empty + common))
app.listen(port, () => process.send && process.send("online"));
}
function renderMarkup(html) {
return `<!DOCTYPE html>
<html>
<head>
<title>Webpack SSR Demo</title>
<meta charset="utf-8" />
</head>
<body>
<div id="app">${html}</div>
<script src="./ssr.js"></script>
</body>
</html>`;
}
}
<a name="8_1">小結(jié)</a>
至此 ssr 已經(jīng)結(jié)束了缘薛,其實所有看起來很高大上的技術(shù)都是從一點一滴積累起來的窍育,只要我們明白原理,你也能做出更優(yōu)秀的框架
完結(jié)
這個可能大概寫了兩個多星期宴胧,每天寫一點點積少成多漱抓,自我感覺提升了很大,如果有興趣跟我一起學習的同學可以來加我進群恕齐,我在群里會每天組織不同的課題來學習乞娄。
接下來的課題大概是:
- 手寫 vue-next 源碼
- ts 從入門到放棄
- node 入門到哭泣
哈哈,開玩笑显歧,大概就是這樣仪或,半個月差不多一個專題,如果你有好的專題也可以一起來討論
最后兩件小事
- 有想入群的學習前端進階的加我微信
luoxue2479
回復加群即可 - 喜歡這篇文章的話士骤,點個
在看
范删,讓更多的人看到 - 有寫錯的地方和更好的建議可以在下面
留言
,一起討論