Написано на передний план: поэтому, хотя я также предварительно проконсультировался со многими колонками блогов, но поскольку многие учебные пособия были выпущены несколько лет назад, многие из его плагинов или загрузчиков были обновлены и итерированы, что больше не является прежним использованием, и его новый Все методы использования требуют ручного запроса один за другим. Если есть ошибка, подскажите. благодарный!
Написано в начале, я думаю, вы должны знать это заранее
1. Что делает webpack и какой статус у него сегодня во фронтэнде?
Излишне говорить, что в современных интерфейсных проектах нет необходимости в инструментах для упаковки, и есть ли причина, по которой мы не понимаем, что это король инструментов для упаковки — webpack.
2. Разве вы не понимаете commonjs и модуль ES6?
скажем так
Обычно в узле используется commonjs, а его импорт используетrequire, экспортировать с помощьюmodule.exports
Импорт и использование в модуле ES6import, есть два вида экспорта
- именованный экспорт
- По умолчанию экспортируется экспорт по умолчанию, а переменная с именем по умолчанию экспортируется во внешний мир, поэтому нет необходимости объявлять переменные, такие как именованный экспорт, и вы можете напрямую экспортировать значение. В файле может быть только один
Обратите внимание, что при импорте модуля для CommonJS получается копия экспортируемого значения, в ES6 Module это динамическая карта значения, и эта карта доступна только для чтения.
Есть также некоторые AMD и UMD, давайте сначала узнаем о модуле. В конце концов, это прокладывает путь для веб-пакетов.
3. Взаимосвязь между entry&chunk&bundle
Эта картина довольно ясная.
Основная концепция 4 учебника
entry
Он используется для указания адреса этого пакета веб-пакета (относительный адрес в порядке), например:
单入口
entry:'./src/index.js'
或:
entry:{
main:'./src/index.js'
}
多入口
entry:{
main:'./src/index.js',
other:'./src/other.js'
}
output
Он используется для указания адреса выходного файла и имени файла после завершения упаковки.文件地址使用绝对地址
单文件
output:{
filename:'bundle.js',
path:path.join(__dirname,'dist')
}
多文件
output:{
filename:'[name].js',
path:path.join(__dirname,'dist')
}
mode
Используется для указания текущей среды сборки
Есть три основных варианта:
- production
- development
- none
Режим настройки автоматически запускает некоторые встроенные функции веб-пакета, если не написано, по умолчанию нет.
loaders
По умолчанию webpack может распознавать .json, .js и модули.Для других модулей нам нужно использовать загрузчики, чтобы помочь нам поместить их в граф зависимостей.
По сути, это функция, которая получает исходный файл в качестве параметра и возвращает преобразованный результат.
plugins
Плагины могут делать некоторые вещи, которые нам нужны, когда webpack доходит до определенного этапа (webpack использует tapable для участия во многих конструкциях жизненного цикла, так что мы можем использовать плагины, чтобы делать подходящие для нас вещи в нужное время). Такие какclean-webpack-pluginИсходный выходной файл под dist будет удален первым, когда мы его упакуем.
Один: основное использование
1.1 Обработка html, css, js
использоватьwebpack-dev-server
Мы также надеемся, что упакованные файлы можно будет запустить на локальном сервере,webpack-dev-serverЭто одна из таких вещей.
Установить:npm i webpack-dev-server -D
Новая конфигурация webpack.config.js adevServerАтрибуты
следующим образом:
devServer: {
port: 3000,
contentBase: './dist' // 起服务的地址(即定位到我们的输出文件地址)
open: true // 自动打开浏览器
compress: true // gzip压缩
}
Чтобы упростить команду, добавьтеdevЗаказ
В то же время он также может быть оснащен командой упаковки, но я вообще люблю использовать npx
{
"name": "webpack-test02",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build":"webpack",
"dev": "webpack-dev-server",
},
}
использоватьhtml-webpack-plugin
Возможно, вы нашли выше, что нет файла html. Даже если сервер запущен, это бесполезно.Далее мы можем написать html-шаблон в файл (только сгенерировать скелет) и затем использоватьhtml-webpack-pluginОдновременно упакуйте этот шаблон в каталог dist и импортируйте выходной файл bundl.js.
Установить:npm i html-webpack-plugin -D
использовать:
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',//模板文件地址
filename: 'index.html',//指定打包后的文件名字
hash: true,//也可给其生成一个hash值
}),
],
还有以下选项
minify: { // 压缩打包后的html文件
removeAttributeQuotes: true, // 删除属性双引号
collapseWhitespace: true // 折叠空行变成一行
}
обрабатывать css
Базовая обработка
Здесь необходимо установить два загрузчика, а именно css-loader (используется для обработки синтаксиса @import в css) и style-loader, используемый для вставки css в тег head.
Установить: npm i css-loader style-loader -D
использовать:
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader','css-loader']
},
]
}
或指定参数,下面是每次将css插入到head标签的前面。以保证我们后面自己在模板中的设置可覆盖
module: {
rules: [{
test: /\.css$/,
use: [{
loader: 'style-loader',
options: {
insert: function insertAtTop(element) {
var parent = document.querySelector('head');
// eslint-disable-next-line no-underscore-dangle
var lastInsertedElement =
window._lastElementInsertedByStyleLoader;
if (!lastInsertedElement) {
parent.insertBefore(element, parent.firstChild);
} else if (lastInsertedElement.nextSibling) {
parent.insertBefore(element, lastInsertedElement.nextSibling);
} else {
parent.appendChild(element);
}
// eslint-disable-next-line no-underscore-dangle
window._lastElementInsertedByStyleLoader = element;
},
}
},
'css-loader'
]
},
]}
извлечь css
Используйте плагиныmini-css-extract-plugin
Установить:npm i mini-css-extract-plugin -D
Он извлекается в один файл, то есть его не нужно использовать.style-loaderохватывать
использовать:
const MinniCssExtractPlugin = require('mini-css-extract-plugin')
plugins: [
new HtmlWebpackPlugin({
// 指定模板文件
template: './src/index.html',
// 指定输出的文件名
filename: 'index.html',
// 添加hash解决缓存
}),
new MinniCssExtractPlugin({
//指定输出的文件名
filename: 'main.css'
})
],
module: {
rules: [{
test: /\.css$/,
use: [
MinniCssExtractPlugin.loader,//本质就是创建一个style标签再将输出的css地址引入
'css-loader',
]
},
]
}
Сжать css, js
Сжать выходной файл css выше
Используйте плагиныoptimize-css-assets-webpack-pluginНо после использования этого плагина для сжатия css необходимо использовать jsuglifyjs-webpack-pluginпродолжать сжимать
Установить:npm i optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
Использование: (Обратите внимание, что этот плагин больше не используется в плагине, позиция использования оптимизированаoptimizationхарактеристики)
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
parallel: true,
sourceMap: true
}),
new OptimizeCSSAssetsPlugin({})
]
},
Добавить префикс поставщика
Требуется загрузчик и инструмент стилизации: postcss-loader autoprefixer
Сначала поговорим о том, что такое postCss.
Это контейнер для компиляции плагинов, и режим его работы — получение исходного кода для обработки путем компиляции плагинов и, наконец, вывод css.
postcss-loaderЭто коннекторы POSTCSS и WebPack. Postcss-Loader можно использовать вместе с Css-Loader или отдельно. Обратите внимание, что синтаксис @Import не рекомендуется использовать только с Postcss-Loader, иначе будет сгенерирован избыточный код.
postCss также требует отдельного файла конфигурацииpostcss.config.js
Давайте также рассмотрим использование post-loader и autoprefixer для создания префиксов поставщиков.
Установить:npm i postcss-loader autoprefixer -D
Использование: webpack.config.js, в правилах модуля
{
test: /\.css$/,
use: [
MinniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
]
},
postcss.config.js
module.exports = {
plugins: [
require("autoprefixer")({
overrideBrowserslist: ["last 2 versions", ">1%"]
})
]
};
обрабатывать меньше
Вам нужно использовать less-loader, и в то же время less-loader использует меньше, поэтому вам нужно установить less less-loader
Установить:npm i less-loader less -D
использовать:
{
test: /\.less$/,
use: [
MinniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'less-loader'
]
}
сас такой же
обрабатывать js
от es6 до es5
Установить:npm i babel-loader @babel/core @babel/preset-env -D
использовать:
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
],
}
}
},
es7 в синтаксис класса
Установить:npm i @babel/plugin-proposal-class-properties -D
использовать:
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
],
plugins:[
["@babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
}
},
Другие подробности см. на официальном сайте babel.
1.2 Обработка изображений
Есть три способа процитировать картинку
- Представлено в js Create Импорт тега изображения
- URL-адрес импорта css
- импортировать img в html
с помощью загрузчика файлов
Третий кейс недоступен, нужно использовать дополнительный загрузчик, чтобы увидеть каштаны ниже
Установить:npm i file-loader -D
Суть в том, что ссылка должна вернуть адрес изображения, но это изображение уже есть в бандле.
использовать:js в
import './index.css'
import img from '../public/img/img01.jpg'
const image = new Image()
image.src = img
document.body.appendChild(image)
в css
div{
p{
width: 100px;
height: 100px;
transform: rotate(45deg);
background-image: url('./img01.jpg');
}
}
в webpack.config.js
module.export={
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader'
}
]
}
}
используя URL-загрузчик
Его можно использовать как обновленную версию загрузчика файлов.Мы можем использовать base64 для установки размера изображения или использовать загрузчик файлов для упаковки исходного изображения.
Установить:npm i url-loader -D
Также здесь вы можете использоватьhtml-withimg-loaderПосле обработки картинок в html обратите внимание на установку свойства esModule в false. В противном случае адрес картинки в ссылке типа html не верный То же верно и для вышеуказанного.
Установить:npm i html-withimg-loader -D
использовать:
{
test: /\.html$/,
use: 'html-withimg-loader'
}, {
test: /\.(png|jpg|gif)$/,
use: {
loader: 'file-loader',
options: {
limit: 50 * 1024,
loader: 'file-loader',
esModule: false,
outputPath: '/img/', // 打包后输出地址
// publicPath: '' // 给资源加上域名路径
}
}
},
1.3 eslint
1.4 Общие виджеты
cleanWebpackPlugin
Автоматически удалять файлы в выходном каталоге каждый раз, когда вы упаковываете
Установить:npm i clean-webpack-plugin -D
использовать:
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins:[
new CleanWebpackPlugin(),
]
copyWebpackPlugin
Некоторые могут от этого не зависеть, но нужно выводить файлы под дист
Установить:npm i copy-webpack-plugin -D
const CopyWebpackPlugin = require('copy-webpack-plugin')
plugins:[
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: path.join(__dirname, 'public'),
to: 'dist'
}],
}),
]
bannerPlugin
Добавить копирайт в заголовок кода
Встроенный плагин в webpack
использовать:
const webpack = require('webpack')
plugins:[
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{
from: path.join(__dirname, 'public'),
to: 'dist'
}],
}),
new webpack.BannerPlugin('core by gxb'),
]
Два: использование покупной цены
2.1 Многостраничная упаковка
То есть несколько записей, каждая запись соответствует дереву зависимостей сборки.
Конфигурация очень простая, со входом. Формат записи — chunkNmae:path.
entry: {
index: './src/index.js',
other: './src/other.js'
},
output: {
filename: '[name].js',
path: path.join(__dirname, './dist')
},
2.2 devtool :'source-map'
То есть соответствие между исходным кодом и упакованным кодом заключается в том, что когда в коде возникает проблема,source-mapПерейти к исходному блоку кода
В основном используется в среде разработки
Конфигурация выглядит следующим образом
devtool: 'source-map'
Существует множество типов значений атрибутов devtool. Ниже приведены наиболее часто используемые из них.
- source-map сгенерирует файл сопоставления .map, а также может найти строку и столбец
- Cheap-module-source-map, файл .map не будет сгенерирован, его можно расположить в строке [рекомендуемая конфигурация]
- Eval-Source-map, не генерирует файлы .map, можно найти строки и столбцы
Обратите внимание: если вы хотите найти исходный код для css, less и scss, вам необходимо настроить его в опции загрузчика.
Такие как: (Обратите внимание: я ленив, этот код не тестировался, я не знаю, устарел ли он сейчас)
test:\/.css$\,
use:[
'style-loader',
{
loader:'css-loader',
options:{
sourceMap:true
}
}
]
2.3 watch
В webpack watch listeners тоже можно настроить на пакетирование время от времени, то есть нам не нужно каждый раз вводить команды самим после изменения кода, просто сохраняем напрямую клавишей c+s.
Конфигурация выглядит следующим образом:
module.exports = {
watch: true,
watchOptions: {
poll: 1000, // 每秒询问多少次
aggregateTimeout: 500, //防抖 多少毫秒后再次触发防止重复按键
ignored: /node_modules/ //忽略时时监听
}
}
2.4 resolve
Все мы знаем, что вебпак начнет искать все зависимости из файла входа после запуска, но при поиске некоторых сторонних пакетов он всегда найдет этот файл main.js в качестве файла входа по умолчанию, но, как и бутстрап, мы иногда просто нужно Цитировать свой стиль. Не было бы слишком расточительно упаковать все это?
Здесь resole указывает, как webpack ищет файл объекта модуля.
Конфигурация выглядит следующим образом:
resolve:{
modules:[path.join('node_modules')],//指定寻找目录
mainFields: ['style', 'main'],//优先用style没有则使用main
extensions: ['.js', '.css', '.json']//如果导入的文件没有后缀名,那么先看js文件有无,再看css...
alias:{
components: './src/components/'//增加别名,即使用import Button from 'components/button 导入时,实际上被 alias 等价替换成了 import Button from './src/components/button' 。
}
}
👉Другие варианты смотрите в этой статье
2.5 Разделение среды
Вещи, которые необходимо настроить в среде разработки и онлайн-среде, обычно различаются, поэтому вы можете использовать webpack-merge, чтобы разделить файлы конфигурации на один базовый общедоступный, один для разработки и один онлайн.
Когда мы будем упаковывать в будущем, мы можем указать указанный файл конфигурации для упаковки в среде разработки или упаковки в производственной среде.
Установить:npm i webpack-merge -D
Это написано следующим образом:
Основание
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
entry: './src/index.js',
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new CleanWebpackPlugin(),
],
output: {
filename: 'bundle.js',
path: path.join(__dirname, './dist')
},
}
Разработка: webpack.dev.js
const merge = require('webpack-merge')
const common = require('./webpack.config')
module.exports = merge(base, {
mode: 'development',
devServer: {},
devtool: 'source-map'
})
Онлайн: webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');
module.exports = merge(common, {
mode: 'production',
});
2.6 Работа с несколькими доменами
Предыдущая статья автора резюмировала кроссдоменность, то есть идея использования прокси для обработки кроссдоменности такова: политика одного и того же происхождения существует только в браузерах и не существует между серверами. Таким образом, мы можем сначала отправить данные на прокси-сервер.
Каштан: отправляйте запросы как другой сервер домена
const xhr = new XMLHttpRequest();
xhr.open('get', '/api/user', true);
xhr.send();
xhr.onload = function () {
console.log(xhr.response)
}
Код сервера:
const express = require('express')
const app = express()
app.get('/test', (req, res) => {
res.json({ msg: 11 })
})
app.listen(3001, function() {
console.log('启动服务');
})
webpack.config.js настроить прокси
devServer: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:3001',
pathRewrite: { '^/api': '' }
}
},
progress: true,
contentBase: './dist',
open: true
},
Три: оптимизация
3.1 noparse
При ссылке на некоторые сторонние пакеты нам не нужно вводить эти пакеты для поиска зависимостей, потому что они, как правило, независимы.
Так что вам не придется тратить время на разбор.
Конфигурация выглядит следующим образом:
module: {
noParse: /jquery/, // 不用解析某些包的依赖
rules:[]
}
3.2 include&exclude
Мы также можем указать объем поиска
Такие как:
rules: [
{
test: /\.js$/,
exclude: '/node_modules/', // 排除node_modules
include: path.resolve('src'), // 在src文件寻找
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
]
}
}
}
Не рекомендуется для использованияexclude, чтобы использовать абсолютный путь
3.3 lgnorePluginплагин
В некоторых сторонних библиотеках есть много вещей, которые мы не используем, например, некоторые библиотеки с языковыми пакетами. В общем, нам нужно использовать в нем только китайский пакет, а другие языковые пакеты нам не нужны.
Итак, это плагин, который может быть предоставлен в веб-пакете.
const webpack = require('webpack')
plugins: [
new webpack.IgnorePlugin(/\.\/locale/, /moment/)
]
3.4 Многопоточная упаковка
Используйте плагиныhappypack
Установить:npm i happypack
использовать:
const Happypack = require('happypack')
module:{
rules: [{
test: /\.css$/,
use: 'happypack/loader?id=css'
}]
}
plugins:[
new Happypack({
id: 'css',
use: ['style-loader', 'css-loader']
})
]
3.5 Ленивая загрузка (загрузка по требованию)
То есть нам не нужно сразу что-то импортировать в наше дерево зависимостей, а импортировать это после того, как мы это используем (аналогично загрузке по требованию)
нужно здесь@babel/plugin-syntax-dynamic-importИспользуется для разбора и идентификации синтаксиса динамического импорта import()
Установить:npm i @babel/plugin-syntax-dynamic-import -D
Каштан:
В index.js:
const button = document.createElement('button')
button.innerHTML = '按钮'
button.addEventListener('click', () => {
console.log('click')
import ('./source.js').then(data => {
console.log(data.default)
})
})
document.body.appendChild(button)
source.js
export default 'gxb'
webpack.config.js
{
test: /\.js$/,
include: path.resolve('src'),
use: [{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
],
plugins: [
'@babel/plugin-syntax-dynamic-import'
]
}
}]
}
которыйsource.jsМодуль будет добавлять зависимости только тогда, когда мы нажмем кнопку, иначе он не будет упакован сразу
3.6 Горячее обновление
то есть обновление кода - это когда страница будет обновлять только это обновление вместо повторного рендеринга всей страницы, т.е. повторно обновлять страницу
Плагин горячего обновления также входит в состав веб-пакета.
Конфигурация выглядит следующим образом:
devServer: {
hot: true,
port: 3000,
contentBase: './dist',
open: true
},
plugins:[
new webpack.HotModuleReplacementPlugin()
]
3.7 Динамическая библиотека DllPlugin
Когда мы используем vue или react, нам нужно вводить их каждый раз, когда мы упаковываем файлы. Но такого рода сторонний пакет не имеет значения, его внутренности никак не изменятся при компиляции.
Так почему бы нам просто не напечатать их при первой упаковке и не связать с нашими файлами?
Эти предварительно упакованные файлы называются dll, которые на самом деле можно понимать как кеш.
Как написать кеш? Идея может быть такой
- сохранить вещи в первую очередь
- Создайте таблицу сопоставления, проверяйте содержимое таблицы сопоставления, когда она понадобится позже, и используйте ее напрямую.
Но эта штука пока достаточно хлопотная в настройке, но есть и хорошие новости. dll устарела
Но так как я написал этот заголовок, давайте сделаем это как понимание.
Используйте плагин напрямую👉autodll-webpack-plugin
Установитьnpm i autodll-webpack-plugin
использовать:
const path = require('path');
const AutoDllPlugin = require('autodll-webpack-plugin');
module.exports = {
plugins: [
// 配置要打包为 dll 的文件
new AutoDllPlugin({
inject: true, // 设为 true即 DLL bundles 插到 index.html 里
filename: '[name].dll.js',
context: path.resolve(__dirname, '../'), // AutoDllPlugin 的 context 必须和 package.json 的同级目录,要不然会链接失败
entry: {
react: [
'react',
'react-dom'
]
}
})
]
}
После Dll можно использовать HardSourceWebpackPlugin, который быстрее и проще, чем dll
Установить:npm i hard-source-webpack-plugin
использовать
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
plugins: [
new HardSourceWebpackPlugin()
]
}
3.8 Извлечение общего кода
В частности, в некоторых файлах с несколькими записями, таких как запись index.js, ссылается на a.js, а other.js другой записи ссылается на a.js, тогда обычно a.js будет напечатан дважды. Эм. . это не обязательно
Извлекать:
module.exports = {
optimization: {
splitChunks: { // 分割代码块,针对多入口
cacheGroups: { // 缓存组
common: { // 公共模块
minSize: 0, // 大于0k抽离
minChunks: 2, // 被引用多少次以上抽离抽离
chunks: 'initial' // 从什么地方开始, 从入口开始
}
}
}
},
}
Извлеките некоторые сторонние пакеты, такие как jquery, на которые обычно ссылаются много раз.
optimization: {
splitChunks: { // 分割代码块,针对多入口
cacheGroups: { // 缓存组
common: { // 公共模块
minSize: 0, // 大于多少抽离
minChunks: 2, // 使用多少次以上抽离抽离
chunks: 'initial' // 从什么地方开始,刚开始
},
vendor: {
priority: 1, // 增加权重, (先抽离第三方)
test: /node_modules/, // 把此目录下的抽离
minSize: 0, // 大于多少抽离
minChunks: 2, // 使用多少次以上抽离抽离
chunks: 'initial' // 从什么地方开始,刚开始
}
}
},
},
3.8 webpackВстроены некоторые оптимизации
Автоматически понимаю, здесь ничего нет
Четыре:tapable- Почерк давно известен
tapableаналогичныйnodejsизeventEmitterОсновная функция библиотеки — контролировать публикацию и подписку различных функций-ловушек.webpackсистема плагинов
4.1 Синхронизация
4.1.1 SyncHookиспользование и реализация
Один из самых нехарактерных хуков — синхронный последовательный.
const { SyncHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new SyncHook(['name']),
}
}
start() {
this.hooks.arch.call('gxb')
}
tap() {
this.hooks.arch.tap('node', function(name) {
console.log('node', name)
})
this.hooks.arch.tap('react', function(name) {
console.log('react', name)
})
}
}
const l = new Lesson()
l.tap();
l.start()
// 实现
class SyncHook {
constructor() {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
this.tasks.forEach(item => {
item.cb(...arg)
})
}
}
const syncHook = new SyncHook()
syncHook.tap('node', name => {
console.log('node', name)
})
syncHook.tap('vue', name => {
console.log('vue', name)
})
syncHook.call('gxb')
4.1.2 SyncBailHookиспользование и реализация
/**
* 订阅的处理函数有一个的返回值不是undefined就停止向下跑
*/
const { SyncBailHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new SyncBailHook(['name'])
}
}
tap() {
this.hooks.arch.tap('node', (name) => {
console.log('node', name);
return 'error'
})
this.hooks.arch.tap('vue', (name) => {
console.log('vue', name);
return undefined
})
}
start() {
this.hooks.arch.call('gxb')
}
}
const l = new Lesson()
l.tap()
l.start()
/**
* 方法实现
*/
class SyncBailHook {
//一般是可以接收一个数组参数的,但是下面没有用到
constructor(args) {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
let ret
let index = 0
do {
ret = this.tasks[index++].cb(...arg)
} while (ret === undefined && index < this.tasks.length);
}
}
const syncBailHook = new SyncBailHook()
syncBailHook.tap('node', name => {
console.log('node', name);
return 'error'
})
syncBailHook.tap('vue', name => {
console.log('vue', name);
})
syncBailHook.call('gxb')
4.1.3 SyncWaterfallHookиспользование и реализация
/**
* 上一个处理函数的返回值是下一个的输入
*/
/**
* 订阅的处理函数有一个的返回值不是undefined就停止向下跑
*/
const { SyncWaterfallHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new SyncWaterfallHook(['name'])
}
}
tap() {
this.hooks.arch.tap('node', (name) => {
console.log('node', name);
return 'node ok'
})
this.hooks.arch.tap('vue', (data) => {
console.log('vue', data);
})
}
start() {
this.hooks.arch.call('gxb')
}
}
const l = new Lesson()
l.tap()
l.start()
/**
* 方法实现
*/
class SyncWaterfallHook {
//一般是可以接收一个数组参数的,但是下面没有用到
constructor(args) {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
let [first, ...others] = this.tasks
let ret = first.cb(...arg)
others.reduce((pre, next) => {
return next.cb(pre)
}, ret)
}
}
const syncWaterfallHook = new SyncWaterfallHook()
syncWaterfallHook.tap('node', data => {
console.log('node', data);
return 'error'
})
syncWaterfallHook.tap('vue', data => {
console.log('vue', data);
})
syncBailHook.call('gxb')
4.1.4 SyncLoopHookиспользование и реализация
/**
* 订阅的处理函数有一个的返回值不是undefined就一直循环它
*/
const { SyncLoopHook } = require('tapable')
class Lesson {
constructor() {
this.index = 0
this.hooks = {
arch: new SyncLoopHook(['name'])
}
}
tap() {
this.hooks.arch.tap('node', (name) => {
console.log('node', name);
return ++this.index === 3 ? undefined : this.index
})
this.hooks.arch.tap('vue', (name) => {
console.log('vue', name);
return undefined
})
}
start() {
this.hooks.arch.call('gxb')
}
}
const l = new Lesson()
l.tap()
l.start()
/**
* 方法实现
*/
class SyncLoopHook {
//一般是可以接收一个数组参数的,但是下面没有用到
constructor(args) {
this.tasks = []
}
tap(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
call(...arg) {
this.tasks.forEach(item => {
let ret;
do {
ret = item.cb(...arg)
} while (ret !== undefined);
})
}
}
const syncLoopHook = new SyncLoopHook()
let index = 0
syncLoopHook.tap('node', name => {
console.log('node', name);
return ++index === 3 ? undefined : index
})
syncLoopHook.tap('vue', name => {
console.log('vue', name);
})
syncLoopHook.call('gxb')
4.2 Асинхронный
Асинхронный параллелизм
4.2.1 AsyncParallelHook
/**
* 异步并发,
串行与并发的关系
即并发需要把处理函数全部执行完再走最后的回调,而串行是一个处理函数执行完才走第二个
*/
const { AsyncParallelHook } = require("tapable")
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncParallelHook(['name'])
}
}
tap() {
this.hooks.arch.tapAsync('node', (name, cb) => {
setTimeout(() => {
console.log("node", name);
cb();
}, 1000);
})
this.hooks.arch.tapAsync('vue', (name, cb) => {
// 使用宏任务
setTimeout(() => {
console.log('vue', name)
cb()
}, 1000)
})
}
start() {
this.hooks.arch.callAsync('gxb', function() {
console.log('end')
})
}
}
let l = new Lesson();
l.tap();
l.start();
/**
* 实现
* 异步并发回忆特点,callAsync传入的回调是在所有异步任务执行完成之后执行的
*/
class SyncParralleHook {
constructor() {
this.tasks = []
}
tapAsync(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
//主要就是每一个异步处理函数执行完都在跑一下done,看是否该执行最后的回调了吗
callAsync(...arg) {
// 先拿出callAsync传过来的回调
const lastCb = arg.pop()
// 开始执行其他的异步任务
let index = 0
// done函数用来判断是不是要去执行lastCb了
const done = () => {
//根据异步函数的数量来执行lastCb
index++
if (index === this.tasks.length) {
lastCb()
}
}
this.tasks.forEach(item => item.cb(...arg, done))
}
}
const hook = new SyncParralleHook()
hook.tapAsync('node', (name, cb) => {
setTimeout(function() {
console.log('node', name)
cb()
}, 1000)
})
hook.tapAsync('vue', (name, cb) => {
setTimeout(function() {
console.log('vue', name)
cb()
}, 1000)
})
hook.callAsync('gxb', function() {
console.log('end')
})
/**
* 处理函数是用微任务promis,即处理函数可以不传回调cb但是需要返回一个promise
*/
const { AsyncParallelHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncParallelHook(['name'])
}
}
start() {
this.hooks.arch.promise('gxb').then(function() {
console.log('end')
})
}
tap() {
this.hooks.arch.tapPromise('node', name => {
return new Promise((resove, reject) => {
console.log('node', name)
resove()
})
})
this.hooks.arch.tapPromise('vue', name => {
return new Promise((resove, reject) => {
console.log('vue', name)
resove()
})
})
}
}
const l = new Lesson()
l.tap()
l.start()
/**
* 实现
* tapPromise
* promise
*/
class AsyncParallelHook {
constructor() {
this.tasks = []
}
tapPromise(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
promise(...arg) {
// task中的处理函数部分返回值都是promise,将它们映射一个数组中。使用promise的all全部执行他们
const newTasks = this.tasks.map(item => item.cb(...arg))
return Promise.all(newTasks)
}
}
// 测试
const hook = new AsyncParallelHook()
hook.tapPromise('node', name => {
return new Promise((res, rej) => {
console.log('node', name)
res()
})
})
hook.tapPromise('vue', name => {
return new Promise((res, rej) => {
console.log('vue', name)
res()
})
})
hook.promise('gxb').then(function() {
console.log('end')
})
4.2.2AsyncParallelBailHook
то же, что синхронизация
Асинхронный последовательный
4.2.3 AsyncSeriesHook
/**
* 异步串行
* 一个by一个的执行
*
*/
const { AsyncSeriesHook } = require('tapable')
class Lesson {
constructor() {
this.hooks = {
arch: new AsyncSeriesHook(['name'])
}
}
start() {
this.hooks.arch.callAsync('gxb', function() {
console.log('end')
})
}
tap() {
this.hooks.arch.tapAsync('node', (name, cb) => {
setTimeout(() => {
console.log('node', name)
cb()
}, 1000)
})
this.hooks.arch.tapAsync('vue', (name, cb) => {
setTimeout(() => {
console.log('node', name)
cb()
}, 1000)
})
}
}
const l = new Lesson()
l.tap()
l.start()
/**
* 实现
* 特点:一个函数执行完再执行再一个
*/
class AsyncSeriesHook {
constructor() {
this.tasks = []
}
tapAsync(name, cb) {
let obj = {}
obj.name = name
obj.cb = cb
this.tasks.push(obj)
}
callAsync(...arg) {
const finalCb = arg.pop()
let index = 0
let next = () => {
if (this.tasks.length === index) return finalCb()
let task = this.tasks[index++].cb
task(...arg, next)
}
next()
}
}
const hook = new AsyncSeriesHook()
hook.tapAsync('node', (name, cb) => {
setTimeout(() => {
console.log('node', name)
cb()
}, 1000)
})
hook.tapAsync('vue', (name, cb) => {
setTimeout(() => {
console.log('vue', name)
cb()
}, 1000)
})
hook.callAsync('gxb', function() {
console.log('end')
})
4.2.4 AsyncSeriesBailHook
эталонная синхронизация
4.2.5 AsyncSeriesWaterfallHook
эталонная синхронизация
Пятое: написать простой веб-пакет вручную
5.1 Создайте файл записи, то есть ссылку на локальный
Чтобы инициализировать новый проект, сначалаnpm init -yсгенерировать package.json
напишите команду bin
"bin": {
"mypack": "./bin/index.js"
},
./bin/index.jsкак наш входной файл
Затем запустите ссылку npm, свяжите модуль npm с соответствующим запущенным проектом и легко отладьте и протестируйте модуль.
Затем создайте проект с написанным файлом webpack.config.js (назовем его файлом проекта, который нужно упаковать --> проект с исходным кодом). Запустите команду `npm link mypack
Это для запуска команды в проекте, который нужно упаковать.npx mypack, который мы написали от руки
5.2 Сборка ядра, написание класса компилятора
Вернитесь к написанному от руки проекту веб-пакета, чтобы инициализировать класс компилятора.
В файле ввода ничего нет
Просто нужно получить webpack.config.js под исходным кодом проекта, а затем вызвать метод компилятора, чтобы передать адрес полученного файла конфигурации
#! /usr/bin/env node
const path = require('path')
// 拿配置文件
const config = require(path.resolve('webpack.config.js'))
const Compiler = require('../lib/compiler.js')
const compiler = new Compiler(config)
compiler.run()
Базовый скелет класса Compiler
const path = require('path')
const fs = require('fs')
const tapable = require('tapable')
class Compiler {
constructor(config) {
this.config = config
this.entryId = ''
this.modules = {}
this.entry = config.entry
this.root = process.cwd() //拿到当前项目地址
this.asserts = {} // 存储chunkName与输出代码块
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afteremit: new tapable.SyncHook(),
}
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
// 每个均是实例,调用实例上的一个方法即可,传入当前Compiler实例
item.run(this)
})
}
}
// 构建模块
buildMoudle(modulePath, isEntry) {}
//写的输出位置
emitFile() {}
run() {
this.hooks.entryInit.call()
this.buildMoudle(path.resolve(this.root, this.entry), true)
this.hooks.afterCompile.call()
this.emitFile()
}
}
шаблон сборки
Во-первых, в run передается адрес входа и логическое значение, указывающее, является ли это входом. Что нам нужно сделать, так это сначала назначить относительный адрес записи для this.entryId.
Затем получите исходный код файла и поместите его в виде пары ключ-значение, например {относительный адрес файла: измененный исходный код}
// 构建模块
buildMoudle(modulePath, isEntry) {
let source = this.getSource(modulePath)
// 其实就是this.entry 啊
let moduleName = './' + path.relative(this.root, modulePath)
if (isEntry) {
this.entryId = moduleName
}
// 开始改造源码,主要针对require
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) //就是./src
// 改造完成放入模块中
this.modules[moduleName] = sourceCode
// 递归构造依赖模板
dependencies.forEach(item => {
this.buildMoudle(path.resolve(this.root, item), false)
})
}
// 获取源码
getSource(modulePath) {
// 事先拿module中的匹配规则与路径进行匹配
let content = fs.readFileSync(modulePath, 'utf8')
return content
}
Изменить исходный код
Вот некоторые инструменты
-
babylonПревратите исходный код в абстрактное синтаксическое дерево AST. -
@babel/traverseИспользуется для замены узлов на AST -
@babel/generatorгенерация результатов -
@babel/typesУтилита Lodash-esque для узлов AST
Установить:npm i babylon @babel/traverse @babel/types @babel/generator
Исходный код: заменить require на__webpack_require__, при преобразовании внутренних параметров пути require. На самом деле, суть требований к упаковке веб-пакетов заключается в том, чтобы вручную реализовать требуемый метод.
Примечание. На этот раз я просто переписал require, но модули es6 по-прежнему не поддерживаются.
// 源码改造
parse(source, parentPath) {
// 生成ast语法树
let ast = babylon.parse(source)
// 用于存取依赖
let dependencies = []
traverse(ast, {
CallExpression(p) {
let node = p.node
// 对require方法进行改名
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
let moduledName = node.arguments[0].value // 取到require方法里的路径
// 改造其后缀名
moduledName = moduledName + (path.extname(moduledName) ? '' : '.js')
// 加上其父路径./src
moduledName = './' + path.join(parentPath, moduledName)
// 加入依赖数组
dependencies.push(moduledName)
// 源码替换
node.arguments = [type.stringLiteral(moduledName)]
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
Вывод в указанное место вывода
//写到输出位置
emitFile() {
// 拿到输出地址
let outPath = path.join(this.config.output.path, this.config.output.filename)
// 拿模板
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
// 填充模板数据
let code = ejs.render(templateStr, {
entryId: this.entryId,
modules: this.modules
})
this.asserts[outPath] = code
console.log(code);
// 写入
fs.writeFileSync(outPath, this.asserts[outPath])
}
Данные в шаблоне ejs (потому что макет один раз сломался)(function (modules) {
var installedModules = {};
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;
}
// 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] %>`);
}),
<% } %>
});</div>
Присоединяйсяloader
Пишите less-loader и style-loader, css-loader как раз обрабатывает часть синтаксиса @import в css, так что не пишите сюда ради простоты css-loader
less-loader: обратите внимание, что если вы берете меньше, преобразуйте файл less в файл css.
const less = require('less')
function loader(source) {
let css = ''
less.render(source, function(err, output) {
css = output.css
})
css = css.replace(/\n/g, '\\n')
return css
}
module.exports = loader
style-loader: создайте тег и поместите в него css
function loader(source) {
let style = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source)}
document.head.appendChild(style)
`
return style
}
module.exports = loader
webpack.config.js на данный момент
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.join(__dirname, './dist')
},
module: {
rules: [{
test: /\.less$/,
use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')]
}]
},
plugins: [
new TestPlugins(),
new InitPlugin()
]
}
В настоящее время функция получения исходного файла изменена следующим образом.
// 获取源码
getSource(modulePath) {
// 事先拿module中的匹配规则与路径进行匹配
const rules = this.config.module.rules
let content = fs.readFileSync(modulePath, 'utf8')
for (let i = 0; i < rules.length; i++) {
let { test, use } = rules[i]
let len = use.length
// 匹配到了开始走loader,特点从后往前
if (test.test(modulePath)) {
console.log(111);
function normalLoader() {
// 先拿最后一个
let loader = require(use[--len])
content = loader(content)
if (len > 0) {
normalLoader()
}
}
normalLoader()
}
}
return content
}
и добавить плагин
Напишите его прямо в файле webpack.config.js исходного проекта.
const path = require('path')
class TestPlugins {
run(compiler) {
// 将自身方法订阅到hook以备使用
//假设它的运行期在编译完成之后
compiler.hooks.afterCompile.tap('TestPlugins', function() {
console.log(`this is TestPlugins,runtime ->afterCompile `);
})
}
}
class InitPlugin {
run(compiler) {
// 将的在执行期放到刚开始解析入口前
compiler.hooks.entryInit.tap('Init', function() {
console.log(`this is InitPlugin,runtime ->entryInit `);
})
}
}
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.join(__dirname, './dist')
},
module: {
rules: [{
test: /\.less$/,
use: [path.join(__dirname, './loader/style-loader.js'), path.join(__dirname, './loader/less-loader.js')]
}]
},
plugins: [
new TestPlugins(),
new InitPlugin()
]
}
На самом деле, рукописную логику все равно сложно объяснить внятно, лучше смотреть код напрямую, ниже я вставил весь код класса компилятора, а комментарии написаны очень подробно.
Весь код класса компилятора
const path = require('path')
const fs = require('fs')
const { assert } = require('console')
// babylon 主要把源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。
// @babel/traverse 对ast解析遍历语法树 负责替换,删除和添加节点
// @babel/types 用于AST节点的Lodash-esque实用程序库
// @babel/generator 结果生成
const babylon = require('babylon')
const traverse = require('@babel/traverse').default;
const type = require('@babel/types');
const generator = require('@babel/generator').default
const ejs = require('ejs')
const tapable = require('tapable')
class Compiler {
constructor(config) {
this.config = config
this.entryId = ''
this.modules = {}
this.entry = config.entry
this.root = process.cwd() //当前项目地址
this.asserts = {} // 存储chunkName与输出代码块
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afteremit: new tapable.SyncHook(),
}
const plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
// 每个均是实例,调用实例上的一个方法即可,传入当前Compiler实例
item.run(this)
})
}
}
// 获取源码
getSource(modulePath) {
// 事先拿module中的匹配规则与路径进行匹配
const rules = this.config.module.rules
let content = fs.readFileSync(modulePath, 'utf8')
for (let i = 0; i < rules.length; i++) {
let { test, use } = rules[i]
let len = use.length
// 匹配到了开始走loader,特点从后往前
if (test.test(modulePath)) {
console.log(111);
function normalLoader() {
// 先拿最后一个
let loader = require(use[--len])
content = loader(content)
if (len > 0) {
normalLoader()
}
}
normalLoader()
}
}
return content
}
// 源码改造
parse(source, parentPath) {
// ast语法树
let ast = babylon.parse(source)
// 用于存取依赖
let dependencies = []
traverse(ast, {
CallExpression(p) {
let node = p.node
// 对require方法进行改名
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
let moduledName = node.arguments[0].value // 取到require方法里的路径
// 改造其后缀名
moduledName = moduledName + (path.extname(moduledName) ? '' : '.js')
// 加上其父路径./src
moduledName = './' + path.join(parentPath, moduledName)
// 加入依赖数组
dependencies.push(moduledName)
// 源码替换
node.arguments = [type.stringLiteral(moduledName)]
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
// 构建模块
buildMoudle(modulePath, isEntry) {
let source = this.getSource(modulePath)
// 其实就是this.entry 啊
let moduleName = './' + path.relative(this.root, modulePath)
if (isEntry) {
this.entryId = moduleName
}
// 开始改造源码,主要针对require
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) //就是./src
// 改造完成放入模块中
this.modules[moduleName] = sourceCode
// 递归构造依赖模板
dependencies.forEach(item => {
this.buildMoudle(path.resolve(this.root, item), false)
})
}
//写的输出位置
emitFile() {
// 拿到输出地址
let outPath = path.join(this.config.output.path, this.config.output.filename)
// 拿模板
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
// 填充模板数据
let code = ejs.render(templateStr, {
entryId: this.entryId,
modules: this.modules
})
this.asserts[outPath] = code
console.log(code);
// 写入
fs.writeFileSync(outPath, this.asserts[outPath])
}
run() {
this.hooks.entryInit.call()
this.buildMoudle(path.resolve(this.root, this.entry), true)
this.hooks.afterCompile.call()
this.emitFile()
}
}
module.exports = Compiler
/**
* 到此为止一个简单的webpack就已经完成了
*/
В конце написания сюда должен быть официально внесен webpack. Следующие вещи будут подытожены позже. Карта мозга не будет загружена.
Справочная благодарность:
👉Легко понять, что означает devtool в веб-пакете: «исходная карта» на двух примерах
👉Это официальная китайская документация.
👉сводка использования ссылки npm
👉Как разработать загрузчики веб-пакетов
👉Бой Webpack: начало работы, дополнительные возможности и настройка
В этой статье используется👉mdniceнабор текста