предисловие
Умелое использование webpack стало необходимым навыком выживания для современных фронтенд-инженеров. Нет никаких сомнений в том, что webpack стал лидером среди инструментов для сборки интерфейса, а техническая документация по использованию webpack бесконечна в Интернете. Но немногие могут четко объяснить процесс сборки веб-пакета. В этой статье делается попытка шаг за шагом изучить, как веб-пакет создает ресурсы, интерпретируя исходный код и отлаживая его с помощью точек останова.
На момент написания этой статьи последняя версия веб-пакетаwebpack 5.0.0-beta.1
, то есть исходный код этой статьи взят из последнейwebpack v5
.
В частности, исходный код, указанный в этой статье, был оптимизирован.Если вы хотите увидеть конкретный код, вы можете посетить официальную библиотеку веб-пакетов в соответствии с именем файла исходного кода, которое я определил. Сокращенная часть этой статьи:
- Убран импорт модуля, т.е.
const xxx = require('XXX')
; - Итоговый код исключения, хотя обработка исключений тоже очень важна, в этой статье в основном анализируется основной процесс нормальной работы вебпака, если нельзя игнорировать обработку исключений, я конкретно объясню;
Как отлаживать веб-пакет
Я всегда думаю, что изучение исходного кода — это не чтение кода построчно, для зрелого проекта с открытым исходным кодом должно быть много замысловатых ветвей. Попытка отладить код шаг за шагом, чтобы проследить путь программы, — это самый быстрый способ быстро понять базовую структуру проекта.
Идеальная функция Debugger в редакторе VS Code — лучший инструмент для отладки программ Node.
- Во-первых, чтобы изучить исходный код веб-пакета, вы должны сначала клонировать исходный код из библиотеки веб-пакета в локальный:
git clone https://github.com/webpack/webpack.git
- Установите зависимости проекта; VS Code открывает локальный репозиторий веб-пакетов
npm install
cd webpack/
code .
- Чтобы не загрязнять корневой каталог проекта, создайте новый в корневом каталоге
debug
Папка для хранения кода отладки,debug
Структура папок следующая:
debug-|
|--dist // 打包后输出文件
|--src
|--index.js // 源代码入口文件
|--package.json // debug时需要安装一些loader和plugin
|--start.js // debug启动文件
|--webpack.config.js // webpack配置文件
Подробный код отладки выглядит следующим образом:
//***** debug/src/index.js *****
import is from 'object.is' // 这里引入一个小而美的第三方库,以此观察webpack如何处理第三方包
console.log('很高兴认识你,webpack')
console.log(is(1,1))
//***** debug/start.js *****
const webpack = require('../lib/index.js') // 直接使用源码中的webpack函数
const config = require('./webpack.config')
const compiler = webpack(config)
compiler.run((err, stats)=>{
if(err){
console.error(err)
}else{
console.log(stats)
}
})
//***** debug/webpack.config.js *****
const path = require('path')
module.exports = {
context: __dirname,
mode: 'development',
devtool: 'source-map',
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/,
}
]
}
}
- в VS-коде
Debug
bar добавить конфигурацию отладки:
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动webpack调试程序",
"program": "${workspaceFolder}/debug/start.js"
}
]
}
После завершения настройки попробуйте один раз щелкнуть ► (Пуск), чтобы проверить, правильно ли работает отладчик (в случае успеха вdebug/dist
упакуетmain.js
документ).
Если у вас есть время, я надеюсь, что вы сможете пройти процесс отладки веб-пакета самостоятельно, я думаю, вы что-то выиграете. Человеку свойственно исследовать.
Далее давайте пошагово проанализируем, как работает webpack, посредством отладки точки останова.
Интерпретация исходного кода
Как запустить вебпак
Есть два способа запустить вебпак:
- пройти через
webpack-cli
Леса для начала, т.е. можноTerminal
Терминал работает напрямую;
webpack ./debug/index.js --config ./debug/webpack.config.js
Этот метод является наиболее часто используемым и самым быстрым методом из коробки.
- пройти через
require('webpack')
Выполнить, импортировав пакет;
Фактически, первый метод в конечном итоге будет использоватьсяrequire
Способ запуска вебпака, можете посмотреть если интересно./bin/webpack.js
документ.
Отправная точка для компиляции веб-пакета
все отconst compiler = webpack(config)
Начинать.
исходный код функции webpack (./lib/webpack.js
):
const webpack = (options, callback) => {
let compiler = createCompiler(options)
// 如果传入callback函数,则自启动
if(callback){
compiler.run((err, states) => {
compiler.close((err2)=>{
callbacl(err || err2, states)
})
})
}
return compiler
}
возврат после выполнения функции webpackcompiler
Object, в webpack есть два очень важных основных объекта, а именноcompiler
иcompilation
, они широко используются в процессе компиляции.
-
Compilerсвоего рода(
./lib/Compiler.js
): основной механизм веб-пакета, полная информация о среде веб-пакета записывается в объект компилятора от начала до конца в веб-пакете,compiler
будет сгенерирован только один раз. ты сможешьcompiler
читать на объектеwebpack config
Информация,outputPath
Ждать; -
Compilationсвоего рода(
./lib/Compilation.js
): представляет сборку одной версии и ресурс сборки.compilation
Работа компиляции может быть выполнена несколько раз, например WebPack работает вwatch
В режиме каждый раз, когда в исходном файле обнаруживается изменение, будет повторно создаваться новый экземпляр.compilation
объект. Одинcompilation
Объекты представляют текущие ресурсы модуля, скомпилированные ресурсы, измененные файлы и информацию о состоянии отслеживаемых зависимостей.
Разница между ними?
Компилятор представляет неизменную среду webpack; компиляция представляет собой задание компиляции, и каждая компиляция может быть разной;Возьмите каштан 🌰:
Компилятор подобен производственной линии мобильных телефонов, он может начать работать после включения питания, ожидая инструкций по производству мобильных телефонов; Комплиация похожа на производство мобильного телефона.Производственный процесс в основном такой же, но производимый мобильный телефон может быть телефоном Xiaomi или телефоном Meizu. Разные материалы имеют разную производительность.
Compiler
класс в функцииcreateCompiler
экземпляр в (./lib/index.js
):
const createCompiler = options => {
const compiler = new Compiler(options.context)
// 注册所有的自定义插件
if(Array.isArray(options.plugins)){
for(const plugin of options.plugins){
if(typeof plugin === 'function'){
plugin.call(compiler, compiler)
}else{
plugin.apply(compiler)
}
}
}
compiler.hooks.environment.call()
compiler.hooks.afterEnvironment.call()
compiler.options = new WebpackOptionsApply().process(options, compiler) // process中注册所有webpack内置的插件
return compiler
}
Compiler
После создания экземпляра класса, если функция веб-пакета получает обратный вызовcallback
, затем выполнить напрямуюcompiler.run()
метод, то webpack автоматически начинает процесс компиляции. если не указаноcallback
Обратный вызов, который должен быть вызван пользователемrun
способ инициировать компиляцию.
Из приведенного выше исходного кода можно извлечь некоторую информацию:
-
компилятор
Compiler
Instantiation, свойства и методы в нем будут упомянуты в следующем разделе, наиболее важными из которых являютсяcompiler.run()
метод; -
траверс
webpack config
серединамассив плагинов, вот я смелыйplugins数组
, поэтому не настраивайте его как объект при настройке плагинов. (На самом деле опции делаются в функции webpackobject schema
Калибровка). -
plugin
: Если подключаемый модуль является функцией, вызовите его напрямую; если подключаемый модуль относится к другим типам (в основном типам объектов), выполните объект подключаемого модуля.applyметод. применить сигнатуру функции:(compiler) => {}
.Webpack очень строго требует, чтобы элементы нашего массива плагинов были функциями, или объект с полем применения, а применение — это функция, и вот почему.
{ plugins: [ new HtmlWebpackPlugin() ] }
-
Вызвать крючок:
compiler.hooks.environment.call()
а такжеcompiler.hooks.afterEnvironment.call()
Это первый вызов хука, с которым мы столкнулись при чтении исходного кода, при последующем чтении вы встретите больше регистраций и вызовов хука. Чтобы понять применение хуков webpack, вам нужно сначала понятьTapable
, который является основой для написания плагинов.Что касается Tapable, я с этим «разберусь». --> У меня есть это в файле, иди и посмотри«Написание собственного плагина веб-пакета начинается с понимания Tapable»
-
process(options)
: в конфиге вебпака, кромеplugins
Есть много других полей, затемprocess(options)
Роль состоит в том, чтобы обрабатывать эти поля одно за другим.
Пока что мы понимаем, какие приготовления делает webpack на этапе инициализации. когда горит предохранительcompiler.run()
Когда наступит время, когда веб-пакет станет действительно мощным. "Войска и кони не двигаются, еда и трава идут первыми", перед этим нужно сначала посмотреть на этоnew WebpackOptionsApply().process(options, compiler)
Какая бы подготовительная работа ни была проделана, она обеспечивает важную логистическую защиту для последующего этапа компиляции.
process(options, compiler)
WebpackOptionsApply
Классная работа правильнаяwebpack options
для инициализации.
Файл с открытым исходным кодомlib/WebpackOptionsApply.js
, вы обнаружите, что все первые пятьдесят строк встроены в различные веб-пакеты.Plugin
, то можно предположить, чтоprocess
методы должны быть разнымиnew SomePlugin().apply()
операция, как есть.
Оптимизированный исходный код (lib/WebpackOptionsApply.js):
class WebpackOptionsApply extends OptionsApply {
constructor() {
super();
}
process(options, compiler){
// 当传入的配置信息满足要求,处理与配置项相关的逻辑
if(options.target) {
new OnePlugin().apply(compiler)
}
if(options.devtool) {
new AnotherPlugin().apply(compiler)
}
if ...
new JavascriptModulesPlugin().apply(compiler);
new JsonModulesPlugin().apply(compiler);
new ...
compiler.hooks.afterResolvers.call(compiler);
}
}
в исходном коде...
Многоточие пропускает многие подобные операции,process
Функция очень длинная, почти 500 строк кода, и в основном она делает две вещи:
-
new
ПолноPlugin
,иapply
Oни.В предыдущем разделе мы знали, что подключаемый модуль веб-пакета на самом деле является классом, который предоставляет метод применения, который будет создан веб-пакетом и при необходимости выполнит метод применения. И метод apply получает
compiler
Объект, удобный для прослушивания сообщений на хуках. в то же времяprocess
Каждый экземпляр функцииPlugin
Все они поддерживаются самим веб-пакетом, поэтому вы обнаружите, что в корневом каталоге проекта веб-пакета есть много файлов.Plugin
конечный файл. Пользовательский подключаемый модуль был зарегистрирован ранее. Разные плагины имеют свои разные задачи, их задача — зацепитьcompiler.hooks
В сообщении, как только сообщение инициировано, обратные вызовы, зарегистрированные в сообщении, вызываются по очереди в соответствии с типом ловушки. Три способа так называемой «зацепки»:tap
tapAsync
tapPromise
, ты должен знатьTapable
О, как это работает. -
в соответствии с
options.xxx
элементы конфигурации, выполнить работу по инициализации, и большая часть работы по инициализации по-прежнему выполняется выше 👆
Это резюме подводит итог:process
После того, как функция выполнена, webpack регистрирует все сообщения ловушек, о которых он заботится, и ждет, пока последующий процесс компиляции будет запущен одно за другим.
воплощать в жизньprocess
Метод Загрузите боеприпасы и дождитесь начала боя.
compiler.run()
Сначала вставьте исходный код (./lib/Compiler.js
):
class Compiler {
constructor(context){
// 所有钩子都是由`Tapable`提供的,不同钩子类型在触发时,调用时序也不同
this.hooks = {
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
done: new AsyncSeriesHook(["stats"]),
// ...
}
}
// ...
run(callback){
const onCompiled = (err, compilation) => {
if(err) return
const stats = new Stats(compilation);
this.hooks.done.callAsync(stats, err => {
if(err) return
callback(err, stats)
this.hooks.afterDone.call(stats)
})
}
this.hooks.beforeRun.callAsync(this, err => {
if(err) return
this.hooks.run.callAsync(this, err => {
if(err) return
this.compile(onCompiled)
})
})
}
}
прочитатьrun
функции, вы обнаружите, что она перехватывает некоторые этапы процесса компиляции и вызывает предварительно зарегистрированную функцию-перехватчик на соответствующем этапе (this.hooks.xxxx.call(this)
), эффект такой же, как у функции жизненного цикла в React. существуетrun
Хуки, которые появляются в функции:beforeRun --> run --> done --> afterDone
. Сторонние плагины могут подключаться к различным жизненным циклам, получатьcompiler
Объекты, которые обрабатывают другую логику.
run
Функция перехватывает ранние и поздние этапы компиляции веб-пакета, поэтому наиболее важный процесс компиляции кода в среднесрочной перспективе передается наthis.compile()
заканчивать. существуетthis.comille()
, еще один главный геройcompilationПоявились чернила.
compiler.compile()
compiler.compile
Функции — это основное поле битвы при компиляции модулей.
compile(callback){
const params = this.newCompilationParams() // 初始化模块工厂对象
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params)
// compilation记录本次编译作业的环境信息
const compilation = new Compilation(this)
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err=>{
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation)
})
})
})
})
})
}
compile
функция иrun
Например, запустить серию функций ловушек, вcompile
Хуки, которые появляются в функции:beforeCompile --> compile --> make --> afterCompile
.
вmake
Это процесс компиляции, о котором мы заботимся. Но здесь он срабатывает только от хука, очевидно, что реальное выполнение компиляции регистрируется на обратном вызове этого хука.
веб-пакет, потому что естьTapable
С благословения , написание кода очень гибкое, популярный механизм обратного вызова в узле (то есть ад обратного вызова) и непревзойденное использование вебпака, если вы отлаживаете с точками останова, его может быть непросто поймать. Здесь я использую метод поиска ключевых слов для обратного просмотраmake
Где прописан хук.
поиск по ключевым словамhooks.make.tapAsync
мы нашли вlib/EntryPlugin.js
нашел свою фигуру.
В зависимости от поисковых ключевых слов будет перечислено больше отвлекающих факторов, и вам нужно проявить смекалку, чтобы определить, какой вариант наиболее близок к реальной ситуации.
На данный момент, мы собираемся проверить этоEntryPlugin
Когда это было вызвано, продолжить ключевые словаnew EntryPlugin
искать вlib/EntryOptionPlugin.js
нашел его в , и в нем вы нашли знакомые "вещи":
if(typeof entry === "string" || Array.isArray(entry)){
applyEntryPlugins(entry, "main")
}else if (typeof entry === "object") {
for (const name of Object.keys(entry)) {
applyEntryPlugins(entry[name], name);
}
} else if (typeof entry === "function") {
new DynamicEntryPlugin(context, entry).apply(compiler);
}
помнить вwebpack.config.js
середина,entry
Как настроены поля? В этот момент вы пойметеentry
Когда это строка или массив, упакованные ресурсы коллективно называютсяmain.js
это имя.
Наше отступление еще не закончено, продолжаем искать ключевые словаnew EntryOptionPlugin
, К сожалению, искомый файлlib/WebpackOptionsApply.js
. Итак, все ясно,make
зацепитьprocess
Функция уже зарегистрирована и ждет вашего звонка.
назадlib/EntryPlugin.js
Взгляниcompiler.hooks.make.tapAsync
Что вы наделали. На самом деле он запущенcompiliation.addEntry
метод, продолжайте изучатьcompiliation.addEntry
.
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name);
// entryDependencies中的每一项都代表了一个入口,打包输出就会有多个文件
let entriesArray = this.entryDependencies.get(name)
entriesArray.push(entry)
this.addModuleChain(context, entry, (err, module) => {
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
})
}
addEntry
Функция состоит в том, чтобы передать входную информацию модуля в цепочку модулей, то естьaddModuleChain
, а затем продолжить вызовcompiliation.factorizeModule
, эти вызовы в конечном итогеentry
Входная информация «переводится» в модуль (строго говоря, модульNormalModule
созданный объект). Когда я читал этот исходный код, мне было немного трудно его понять.entry
Обработка должна быть синхронной, позже выяснилосьprocess.nextTick
Использование многих обратных вызовов вызывается асинхронно. Здесь рекомендуется иметь больше точек останова и больше отладки, чтобы понять извилистые асинхронные обратные вызовы.
Здесь я перечисляю порядок вызова соответствующих функций:this.addEntry --> this.addModuleChain --> this.handleModuleCreation --> this.addModule --> this.buildModule --> this._buildModule --> module.build
(this指代compiliation
)`.
в конце концов пойдетNormalModule
объект (./lib/NormalModule.js
), выполнятьbuild
метод.
существуетnormalModule.build
метод сначала вызовет сам себяdoBuild
метод:
const { runLoaders } = require("loader-runner");
doBuild(options, compilation, resolver, fs, callback){
// runLoaders从包'loader-runner'引入的方法
runLoaders({
resource: this.resource, // 这里的resource可能是js文件,可能是css文件,可能是img文件
loaders: this.loaders,
}, (err, result) => {
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
const extraInfo = result.length >= 2 ? result[2] : null;
// ...
})
}
фактическиdoBuild
заключается в выборе подходящегоloader
загрузитьresource
, цель состоит в том, чтобы преобразовать этоresource
Преобразование в модули JS (причина в том, что webpack распознает только модули JS). Наконец, верните загруженный исходный файлsource
продолжить обработку.
webpack очень хорошо справляется со стандартными модулями JS, но бессилен обрабатывать другие типы файлов (css, scss, json, jpg) и т. д. В данный момент ему нужна помощь загрузчика. Роль загрузчика — преобразовать исходный код в JS-модули, чтобы веб-пакет мог их правильно идентифицировать.
loader
Эта функция похожа на конвейер потока информации в Linux: она получает исходный поток строк, обрабатывает его, а затем возвращает обработанную исходную строку следующему потоку.loader
Продолжить обработку.loader
Основная парадигма:(code, sourceMap, meta) => string
прошедшийdoBuild
После этого любые модули конвертируются в стандартные модули JS.
Вы можете попробовать внедрить css-код в js-код и понаблюдать за структурой данных преобразованного стандартного JS-модуля.
Следующим шагом будет компиляция стандартного кода JS. во входящемdoBuild
Это обрабатывается в функции обратного вызоваsource
:
const result = this.parser.parse(source)
А вотthis.parser
На самом деле этоJavascriptParser
Объект экземпляра финалаJavascriptParser
будет вызывать сторонние пакетыacorn
который предоставилparse
Метод анализирует исходный код JS.
parse(code, options){
// 调用第三方插件`acorn`解析JS模块
let ast = acorn.parse(code)
// 省略部分代码
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectStrictMode(ast.body)
this.prewalkStatements(ast.body)
this.blockPrewalkStatements(ast.body)
// 这里webpack会遍历一次ast.body,其中会收集这个模块的所有依赖项,最后写入到`module.dependencies`中
this.walkStatements(ast.body)
}
}
Есть онлайн гаджетAST explorerПреобразовать JS-код в синтаксическое дерево AST можно онлайн, выбрав в качестве парсераacorn
Вот и все. будет отлаживать код./debug/src/index.js
использоватьacron
Разбираем грамматику и получаем следующую структуру данных:
Вы можете быть немного сбиты с толку, обычно мы используем что-то вродеbabel-loader
Подождите, пока загрузчик предварительно обработает исходные файлы, затем веб-пакет здесьparse
Какова конкретная роль?parse
Самая большая роль заключается в сборе зависимостей модулей, таких как те, которые появляются в отладочном коде.import {is} from 'object-is'
илиconst xxx = require('XXX')
Оператор импорта модуля , webpack запишет эти зависимости, записанные вmodule.dependencies
в массиве.
compilation.seal()
На данный момент, начиная с входного файла, webpack собрал полную информацию и зависимости модуля, и следующим шагом является дальнейшая упаковка модуля.
в исполнении
compilation.seal
(./lib/Compliation
), вы можете нажать точку останова, чтобы просмотретьcompilation.modules
Случай. В настоящее времяcompilation.modules
Есть три подмодуля, а именно./src/index.js
node_modules/object.is/index.js
а такжеnode_modules/object.is/is.is
compilation.seal
Есть много шагов, сначала закройте модуль, сгенерируйте ресурсы, эти ресурсы хранятся вcompilation.assets
, compilation.chunks
.
Вы увидите его в большинстве сторонних плагинов для веб-пакетов.
compilation.assets
иcompilation.chunks
фигура.
тогда позвониcompilation.createChunkAssets
Метод отображает все зависимости в конкатенированную строку через соответствующий шаблон:
createChunkAssets(callback){
asyncLib.forEach(
this.chunks,
(chunk, callback) => {
// manifest是数组结构,每个manifest元素都提供了 `render` 方法,提供后续的源码字符串生成服务。至于render方法何时初始化的,在`./lib/MainTemplate.js`中
let manifest = this.getRenderManifest()
asyncLib.forEach(
manifest,
(fileManifest, callback) => {
...
source = fileManifest.render()
this.emitAsset(file, source, assetInfo)
},
callback
)
},
callback
)
}
допустимыйcreateChunkAssets
в теле методаthis.emitAsset(file, source, assetInfo)
Поставьте точку останова на строку кода и наблюдайте за этим временем.source
структура данных в . существуетsource._source
Поле впервые увидело прототип упакованного исходного кода:
Стоит отметить, чтоcreateChunkAssets
В процессе выполнения он сначала прочитает, есть ли уже ресурс с таким хешем в кеше, и если есть, то вернет содержимое напрямую, в противном случае продолжит выполнение логики, сгенерированной модулем, и сохранит его в кеш.
compiler.hooks.emit.callAsync()
После выполнения запечатывания вся информация о модуле и запакованный исходный код сохраняются в памяти, и наступает время их вывода в виде файла. Далее идет серия обратных вызовов, и, наконец, мы приходим кcompiler.emitAssets
в теле метода. существуетcompiler.emitAssets
позвонит первымthis.hooks.emit
Жизненный цикл, а затем вывод файла в указанную папку в соответствии с атрибутом пути выходной конфигурации файла конфигурации веб-пакета. В этот момент вы можете./debug/dist
Просмотрите упакованный файл кода отладки в формате .
this.hooks.emit.callAsync(compilation, () => {
outputPath = compilation.getPath(this.outputPath, {})
mkdirp(this.outputFileSystem, outputPath, emitFiles)
})
Суммировать
Большое спасибо, что дочитали до конца.Эта статья длинная и кратко описывает основной процесс компиляции модулей webpack:
- перечислить
webpack
функция полученияconfig
информацию о конфигурации и инициализироватьcompiler
, в этот период будетapply
Все встроенные плагины webpack; - перечислить
compiler.run
Войдите в стадию компиляции модуля; - Каждая новая компиляция создает экземпляр
compilation
объект, который записывает основную информацию этой компиляции; - Входить
make
стадия, т.е. триггерcompilation.hooks.make
крючок, изentry
для входа: а. Вызовите соответствующийloader
Предварительно обработать исходный код модуля и преобразовать его в стандартный JS-модуль; б. Задействовать сторонние плагиныacorn
Анализирует стандартные модули JS и собирает зависимости модулей. В то же время он будет продолжать рекурсию каждой зависимости, собирать информацию о зависимости и продолжать рекурсию, в итоге будет получено дерево зависимостей 🌲; - последний звонок
compilation.seal
Модуль рендеринга интегрирует различные зависимости и, наконец, выводит один или несколько фрагментов;
Ниже приведена простая временная диаграмма:
Вышеупомянутый процесс не полностью суммирует весь процесс webpack.webpack.config
Конфигурация становится все более и более сложной, и веб-пакет будет генерировать больше процессов для решения различных ситуаций.
Вебпак сложен? Очень сложный,Tabable
иNode回调
Существуют различные направления для всего процесса, и благодаря своей системе плагинов webpack легко настраивается.
Вебпак прост? Это также просто, он делает только одну вещь, компилирует и упаковывает модули JS, и делает это отлично.
Наконец
Кодовые слова непросты, если:
- Эта статья полезна для вас, пожалуйста, не скупитесь на свои ручонки, чтобы понравиться мне;
- Если есть что-то, что вы не понимаете или ошибаетесь, пожалуйста, прокомментируйте, и я активно отвечу или внесу опечатки;
- Я с нетерпением жду продолжения изучения технических знаний вместе со мной, пожалуйста, следуйте за мной;
- Пожалуйста, укажите источник;
Ваша поддержка и внимание - самая большая мотивация для меня продолжать творить!