月度归档:2023年03月

P/Invoke之C#调用动态链接库DLL

本编所涉及到的工具以及框架:
1、Visual Studio 2022
2、.net 6.0

P/Invok是什么?

P/Invoke全称为Platform Invoke(平台调用),其实际上就是一种函数调用机制,通过P/Invoke就可以实现调用非托管Dll中的函数。

在开始之前,我们首先需要了解C#中有关托管与非托管的区别

托管(Collocation),即在程序运行时会自动释放内存;
非托管,即在程序运行时不会自动释放内存。

废话不多说,直接实操

第一步:

  1. 打开VS2022,新建一个C#控制台应用
  2. 右击解决方案,添加一个新建项,新建一个”动态链接库(DLL)”,新建完之后需要右击当前项目–> 属性 –> C/C++ –> 预编译头 –> 选择”不使用编译头”
  3. 在新建的DLL中我们新建一个头文件,用于编写我们的方法定义,然后再次新建一个C++文件,后缀以.c 结尾

第二步:

  1. 在我们DLL中的头文件(Native.h)中定义相关的Test方法,具体代码如下: #pragma once // 定义一些宏 #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN #endif #define CallingConvention _cdecl // 判断用户是否有输入,从而定义区分使用dllimport还是dllexport #ifdef DLL_IMPORT #define HEAD EXTERN __declspec(dllimport) #else #define HEAD EXTERN __declspec(dllexport) #endif HEAD int CallingConvention Sum(int a, int b);
  2. 之后需要去实现头文件中的方法,在Native.c中实现,具体实现如下: #include "Native.h" // 导入头部文件 #include "stdio.h" HEAD int Add(int a, int b) { return a+b; }
  3. 在这些步骤做完后,可以尝试生成解决方案,检查是否报错,没有报错之后,将进入项目文件中,检查是否生成DLL (../x64/Debug)

第三步:

  1. 在这里之后,就可以在C#中去尝试调用刚刚所声明的方法,以便验证是否调用DLL成功,其具体实现如下:
   using System.Runtime.InteropServices;

   class Program
   {
       [DllImport(@"C:\My_project\C#_Call_C\CSharp_P_Invoke_Dll\x64\Debug\NativeDll.dll")]
       public static extern int Add(int a, int b);

       public static void Main(string[] args)
       {
           int sum = Add(23, 45);
           Console.WriteLine(sum);
           Console.ReadKey();
       }
   }

运行结果为:68,证明我们成功调用了DLL动态链库

C#中通过P/Invoke调用DLL动态链库的流程

  通过上述一个简单的例子,我们大致了解到了在C#中通过P/Invoke调用DLL动态链库的流程,接下我们将对C#中的代码块做一些改动,便于维护

  1. 在改动中我们将用到NativeLibrary类中的一个方法,用于设置回调,解析从程序集进行的本机库导入,并实现通过设置DLL的相对路径进行加载,其方法如下: public static void SetDllImportResolver (System.Reflection.Assembly assembly, System.Runtime.InteropServices.DllImportResolver resolver);
  2. 在使用这个方法前,先查看一下其参数 a、assembly: 主要是获取包含当前正在执行的代码的程序集(不过多讲解)

    b、resolber: 此参数是我们要注重实现的,我们可以通过查看他的元代码,发现其实现的是一个委托,因此我们对其进行实现。

    原始方法如下: public delegate IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath);
  3. 实现resolver方法: const string NativeLib = "NativeDll.dll"; static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); // 此处为Dll的路径 //Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } 该方法主要是用于区分在加载DLL时不一定只能是设置绝对路径,也可以使用相对路径对其加载,本区域代码是通过使用委托去实现加载相对路径对其DLL加载,这样做的好处是,便于以后需要更改DLL的路径时,只需要在这个方法中对其相对路径进行修改即可。
  4. 更新C#中的代码,其代码如下: using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll"; [DllImport(NativeLib)] public static extern int Add(int a, int b); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } public static void Main(string[] args) { NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); int sum = Add(23, 45); Console.WriteLine(sum); Console.ReadKey(); } }
  5. 最后重新编译,检查其是否能顺利编译通过,最终我们的到的结果为:68

至此,我们就完成了一个简单的C#调用动态链接库的案例

  下面将通过一个具体实例,讲述为什么要这样做?(本实例通过从性能方面进行对比)

  1. 在DLL中的头文件中,加入如下代码: HEAD void CBubbleSort(int* array, int length);
  2. 在.c文件中加入如下代码: HEAD void CBubbleSort(int* array, int length) { int temp = 0; for (int i = 0; i < length; i++) { for (int j = i + 1; j < length; j++) { if (array[i] > array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } }
  3. C#中的代码修改:
    using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll";[DllImport(NativeLib)] public unsafe static extern void CBubbleSort(int* arr, int length); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64", "Release", "NativeDll.dll"); //Console.WriteLine(dll); return libraryName switch { NativeLib =&gt; NativeLibrary.Load(dll, assembly, searchPath), _ =&gt; IntPtr.Zero }; } public unsafe static void Main(string[] args) { int num = 1000; int[] arr = new int[num]; int[] cSharpResult = new int[num]; //随机生成num数量个(0-10000)的数字 Random random = new Random(); for (int i = 0; i &lt; arr.Length; i++) { arr[i] = random.Next(10000); } //利用冒泡排序对其数组进行排序 Stopwatch sw = Stopwatch.StartNew(); Array.Copy(arr, cSharpResult, arr.Length); cSharpResult = BubbleSort(cSharpResult); Console.WriteLine($"\n C#实现排序所耗时:{sw.ElapsedMilliseconds}ms\n"); // 调用Dll中的冒泡排序算法 NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); fixed (int* ptr = &amp;arr[0]) { sw.Restart(); CBubbleSort(ptr, arr.Length); } Console.WriteLine($"\n C实现排序所耗时:{sw.ElapsedMilliseconds}ms"); Console.ReadKey(); } //冒泡排序算法 public static int[] BubbleSort(int[] array) { int temp = 0; for (int i = 0; i &lt; array.Length; i++) { for (int j = i + 1; j &lt; array.Length; j++) { if (array[i] &gt; array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } return array; }}
  4. 执行结果: C#实现排序所耗时: 130ms C实现排序所耗时:3ms 在实现本案例中,可能在编译后,大家所看到的结果不是很出乎意料,但这只是一种案例,希望通过此案例的分析,能给大家带来一些意想不到的收获叭。

最后

简单做一下总结叭,通过上述所描述的从第一步如何创建一个DLL到如何通过C#去调用的一个简单实例,也应该能给正在查阅相关资料的你有所收获,也希望能给在这方面有所研究的你有一些相关的启发,同时也希望能给目前对这方面毫无了解的你有一个更进一步的学习。

作者:百宝门-后端组-刘忠帅

使用Net将HTML简历导出为PDF格式

现在有许多将HTML导出PDF的第三方包,这里介绍使用的是Select.HtmlToPdf.NetCore

使用Select.HtmlToPdf.NetCore

  1. 整体思路是将cshtml内容读出来,然后再转为Pdf文档
  2. 读取cshtml内容有两种方法,第一种使用第三方包 RazorEngine.NetCore,第二种使用官方方法进行读取。(注意两种方法的cshtml内容略有不同)

效果图展示

在线演示地址

我把所有的源代码都上传到了我的个人Github,有需要的请自取:https://github.com/WeiMing0803/ExportPdf

首先使用ChatGPT生成个人简历信息

代码部分

HomeController.cs :

public async Task<IActionResult> ToPdf()
{
    PdfDocument pdfDocument = new PdfDocument();
    HtmlToPdf converter = new HtmlToPdf();//实例化一个html到pdf转换器对象
    converter.Options.PdfPageOrientation = PdfPageOrientation.Portrait;//设置页面方向
    converter.Options.PdfPageSize = PdfPageSize.A4;//设置页面大小
    converter.Options.MarginTop = 10;//设置页边距
    converter.Options.MarginBottom = 10;
    converter.Options.MarginLeft = 10;
    converter.Options.MarginRight = 10;

    PdfReportModel model = new PdfReportModel { Name = "彭于晏", Email = "pengyuyan@outlook.com" };
    //string htmlResult = readByEngineRazor(model);//第一种方法,使用RazorEngine.NetCore读取Cshtml文件
    string htmlResult = await readCshtml(model);//第二种方法

    if (!string.IsNullOrEmpty(htmlResult))
    {
        pdfDocument = converter.ConvertHtmlString(htmlResult);
    }

    string savePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $@"ExportPDF\{DateTime.Now.ToString("yyyyMMdd")}");
    Directory.CreateDirectory(savePath);
    string filename = Path.Combine(savePath, $"{DateTime.Now.ToString("yyyyMMddHHmmssffff")}.pdf");
    pdfDocument.Save(filename);

    byte[] bytes = System.IO.File.ReadAllBytes(filename);
    return File(bytes, "application/pdf", Path.GetFileName(filename));
}

 private string readByEngineRazor(PdfReportModel model)
{
    string template = System.IO.File.ReadAllText("Views/Report/PdfReport.cshtml");
    string htmlResult = Engine.Razor.RunCompile(template, "PdfReport", typeof(PdfReportModel), model);
    return htmlResult;
}

private async Task<string> readCshtml(PdfReportModel model)
{
    string htmlResult = await _viewRenderService.RenderToStringAsync("Report/PdfReport", model);
    return htmlResult;
}

TemplateGadgetProvider.cs :

public class TemplateGadgetProvider
{
    public static TemplateGadgetProvider _instance;
    public static TemplateGadgetProvider Instance
    {
        get
        {
            if (_instance == null)
                _instance = new TemplateGadgetProvider();
            return _instance;
        }
    }

    public string Load(string virtualPath)
    {
        return File.ReadAllText(virtualPath);
    }
}

pdfReport.css :
Css样式文件:点击查看详细内容

html {
    font-family: 'Open Sans', sans-serif;
    background: whitesmoke;
}

a {
    text-decoration: none;
    color: black;
}

hr {
    background: grey;
}

#container {
    position: relative;
    display: flex;
}

#profile {
    flex: 15%;
    display: block;
    position: relative;
    margin: 5% 2% 0 10%;
    width: 100%;
    height: 100%;
}

#info-cards {
    flex: 55%;
    display: block;
    margin-top: 5%;
    margin-right: 10%;
    width: 100%;
    height: 100%;
}

#image {
    position: relative;
    overflow: hidden;
}

#image,
#profile-photo {
    position: relative;
    width: 80px;
    height: 80px;
    border-radius: 10px;
}

    #image > a {
        position: absolute;
        top: 0;
        left: 0;
        background: rgba(0, 0, 0, 0.5) !important;
        height: 100%;
        width: 100%;
        display: none;
    }

        #image > a > i {
            -webkit-text-stroke: 1px #ffffffdd;
            padding: 40%;
        }

    #image:hover a {
        display: block;
    }

#name {
    font-size: 23px !important;
    line-height: 20px !important;
}

#about,
.card > ul > li {
    padding: 0 0 0 15px;
    position: relative;
    display: inline-block;
    width: 100%;
}

#about {
    font-size: 20px !important;
    padding: 0 !important;
}

    #name,
    #about > p {
        font-weight: bolder;
        font-family: 'Open Sans', sans-serif;
    }

#email {
    font-size: 15px !important;
    font-weight: bold !important;
    font-family: 'Cutive Mono', monospace;
}

#college,
#email,
#year-graduation,
#education,
#more-about,
#telephone,
#fax {
    color: #555;
    font-size: 13.5px;
}

strong,
span {
    color: black;
    font-size: 16px;
}

#social-links,
#about {
    display: inline-block;
}

#social-links {
    margin-bottom: 12px;
}

    #social-links a {
        margin: 0 10px;
    }

#edit-intro {
    display: block;
    color: #097bbf;
    font-family: 'Nunito', sans-serif;
}

.fab {
    font-size: 1.1em;
}

.fab,
.fas {
    color: whitesmoke;
}

#about > a {
    top: 4px;
    right: 8px;
}

.edit {
    top: 19px;
    right: 10px;
}

#about > a,
.edit {
    position: absolute;
    font-size: 15px !important;
}

.stroke-transparent {
    -webkit-text-stroke: 1px #000;
    -webkit-text-fill-color: transparent;
}

.blue {
    color: #097bbf !important;
    font-size: 13px;
}

.stroke-transparent-blue {
    -webkit-text-stroke: 1px #097bbf;
    -webkit-text-fill-color: transparent;
}

.card {
    box-shadow: 0 3px 10px 0 rgba(0, 0, 0, .1);
    overflow-x: hidden;
    margin-bottom: 30px;
    padding: 15px 30px 30px 30px;
    background-color: #fff;
}

    .card > p {
        color: #0e141e;
        font-weight: bolder;
        font-size: 18px;
        line-height: 2;
    }

        .card > p > i {
            font-size: 18px;
        }

    .card > a {
        font-weight: 400;
        font-size: 15px;
        margin: 0;
        margin-left: 25px;
        padding: 0;
        border: 0;
        height: auto;
        background: transparent;
        color: #097bbf;
        outline: none;
        cursor: pointer;
    }

    .card > ul {
        list-style-type: none;
    }

.tags {
    font-size: 17px;
    font-weight: bolder;
}

    .tags ~ a {
        display: none !important;
    }

    .tags span {
        font-size: 14px;
        font-weight: normal;
        color: #0e141e;
    }

        .tags span span {
            color: #738f93;
        }

@media screen and (max-width:1090px) {
    #profile {
        margin-left: 5%;
    }
}

@media screen and (max-width:850px) {
    #container {
        display: block;
    }

    #profile {
        width: 90%;
    }

    .card {
        margin: 0 5%;
        margin-bottom: 30px;
    }
}

PdfReport.cshtml :

使用RazorEngine.NetCore需要修改下面两处地方

  1. 删除 @model PdfReportModel
  2. @Html.Raw(@style) 修改为 @@Raw(@style)

视图文件:点击查看详细内容

@using exportPdf.common
@model PdfReportModel   

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    @{
        string style = TemplateGadgetProvider.Instance.Load(@"wwwroot\css\pdfReport.css");
    }
    <style>@Html.Raw(@style)</style>
</head>

<body>
    <div id="inner-nav"></div>
    <div id="container">
        <div id="profile">
            <div id="image">
                <img id="profile-photo" src="https://img2023.cnblogs.com/blog/233608/202303/233608-20230308165653594-2049775608.jpg" alt="Profile-Image">
                <a href="#"><i class="fas fa-pen stroke-transparent"></i></a>
            </div>
            <p id="name">@Model.Name<br><span id="email">@Model.Email</span></p>
            <p id="designation">前端开发工程师<br><span id="college">天将降大任于斯人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为也,所以动心忍性,增益其所不能。——《孟子》 </span></p>
            <div id="social-links"><a href="#"><i class="fab fa-facebook-f stroke-transparent"></i></a><a><i
                        class="fab fa-twitter stroke-transparent"></i></a><a><i
                        class="fab fa-linkedin-in stroke-transparent"></i></a><a><i
                        class="fab fa-github stroke-transparent"></i></a></div>
            <a id="edit-intro" href="#"><i class="fas fa-pen-alt blue"></i>&nbsp;&nbsp;</a>
            <hr width="100%">
            <div id="about">
                <p style="display:inline;">个人详情</p>
                <a href="#"><i class="fas fa-pen stroke-transparent-blue"></i></a>
            </div>
            <p id="year-graduation">预计毕业年份<br><strong>2023年6月</strong></p>
            <p id="education">学历<br><strong>湖南大学 本科</strong></p>
            <p id="more-about">专业<br><strong> 计算机科学与技术专业</strong></p>
            <p id="telephone">电话<br><strong>0532-2271351</strong></p>
            <p id="fax">传真<br><strong>+91-532-25453441</strong></p>
        </div>
        <div id="info-cards">
            <div class="card">
                <p><i class="fas fa-briefcase stroke-transparent"></i>&nbsp;&nbsp;&nbsp;专业技能</p>
                <ul>
                    <li>
                        <p class="tags">1. 熟练掌握HTML、CSS、JavaScript等前端基础技术</p>
                    </li>
                    <li>
                        <p class="tags">2. 熟悉jQuery、Bootstrap等常用前端框架和库</p>
                    </li>
                    <li>
                        <p class="tags">3. 了解Node.js、Express等后端开发技术</p>
                    </li>
                    <li>
                        <p class="tags">4. 掌握Git、Webpack等常用开发工具</p>
                    </li>
                    <li>
                        <p class="tags">5. 具备良好的编码风格和文档习惯</p>
                    </li>
                </ul>
            </div>
            <div class="card">
                <p><i class="fas fa-briefcase stroke-transparent"></i>&nbsp;&nbsp;&nbsp;工作检验</p>
                <ul>
                    <li>
                        <p class="tags">1. 依帆网站首页制作(个人项目)<br>
    - 使用HTML、CSS、JavaScript实现了一个响应式的网站首页<br>
    - 使用Bootstrap进行布局和样式美化,使用jQuery实现轮播图和导航栏效果<br>
    - 使用Webpack进行打包和优化,使用Git进行版本控制和部署</p>
                    </li>
                    <li>
                        <p class="tags">2. 艺风网站后台管理系统(实习项目)<br>
    - 参与了一个基于Node.js和Express的后台管理系统的开发<br>
    - 负责前端页面的编写,使用EJS模板引擎渲染数据<br>
    - 使用Ajax和Fetch进行数据交互,使用Element UI组件库提升用户体验<br>
    - 遵循MVC架构,使用Mongoose操作MongoDB数据库</p>
                    </li>
                </ul>
            </div>
            <div class="card">
                <p><i class="fas fa-graduation-cap stroke-transparent"></i>&nbsp;&nbsp;&nbsp;自我评价</p>
                <ul>
                    <li>
                        <p class="tags">具备较强的学习能力和逻辑思维能力,喜欢接触新技术和新知识</p>
                    </li>
                    <li>
                        <p class="tags">具备良好的沟通能力和团队协作能力,能够积极配合团队完成任务</p>
                    </li>
                    <li>
                        <p class="tags">具备一定的创新能力和解决问题能力,能够针对不同需求提出合理方案</p>
                    </li>
                </ul>
                <a href="#">+ Add new</a>
            </div>
        </div>
    </div>
</body>
</html>

ViewRenderService :

public class ViewRenderService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;

    public ViewRenderService(IRazorViewEngine razorViewEngine,
        ITempDataProvider tempDataProvider,
        IServiceProvider serviceProvider)
    {
        _razorViewEngine = razorViewEngine;
        _tempDataProvider = tempDataProvider;
        _serviceProvider = serviceProvider;
    }

    public async Task<string> RenderToStringAsync(string viewName, object model)
    {
        var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

        using (var sw = new StringWriter())
        {
            var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);

            if (viewResult.View == null)
            {
                throw new ArgumentNullException($"{viewName} does not match any available view");
            }

            var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
            {
                Model = model
            };

            var viewContext = new ViewContext(
                actionContext,
                viewResult.View,
                viewDictionary,
                new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                sw,
                new HtmlHelperOptions()
            );

            await viewResult.View.RenderAsync(viewContext);
            return sw.ToString();
        }
    }
}

Program.cs :

builder.Services.AddTransient<ViewRenderService>();

以上就是使用Select.HtmlToPdf.NetCore将HTML导出为PDF的全部内容!

作者:百宝门-后端组-明维

我们为什么要阅读webpack源码


相信很多人都有这个疑问,为什么要阅读源码,仅仅只是一个打包工具,会用不就行了,一些配置项在官网,或者谷歌查一查不就好了吗,诚然在大部分的时候是这样的,但这样在深入时也会遇到以下几种问题。

  1. webpack 配置繁琐,具有 100 多个内置插件,200 多个钩子函数,在保持灵活配置的同时,也把问题抛给了开发者。如不同的配置项会不会对同一个功能产生影响,引用 Plugin 的先后顺序会不会影响打包结果?这些问题,不看源码是无法真正清晰的。
  2. plugin 也就是插件,是 webpack 的支柱功能。开发者可以自己使用钩子函数写出插件,来丰富 webpack 的生态,也可以在自己或公司的项目中引用自己开发的插件,来去解决实际的工程问题,不去探究源码,无法理解 webpack 插件的运行,也无法写出高质量的插件。
  3. 从前端整体来看,现代前端的生态与打包工具高度相关,webpack 作为其中的佼佼者,了解源码,也就是在了解前端的生态圈。

Tapable浅析

首先我们要先明白什么是 Tapable,这个小型库是 webpack 的一个核心工具。在 webpack 的编译过程中,本质上通过 Tapable 实现了在编译过程中的一种发布订阅者模式的插件机制。它提供了一系列事件的发布订阅 API ,通过 Tapable 可以注册事件,从而在不同时机去触发注册的事件进行执行。

下面将会有一个模拟 webpack 注册插件的例子来尝试帮助理解。

compiler.js

const { SyncHook, AsyncParallelHook }  = require('tapable');

class Compiler {
  constructor(options) {
    this.hooks = {
      testSyncHook: new SyncHook(['name', 'age']),
      testAsyncHook: new AsyncParallelHook(['name', 'age'])
    }

    let plugins = options.plugins;

    plugins.forEach(plugin => {
      plugin.apply(this);
    });
  }

  run() {
    this.testSyncHook('ggg', 25);
    this.testAsyncHook('hhh', 24);
  }

  testSyncHook(name, age) {
    this.hooks.testSyncHook.call(name, age);
  }

  testAsyncHook(name, age) {
    this.hooks.testAsyncHook.callAsync(name, age);
  }
}

module.exports = Compiler;

index.js

const Compiler = require('./complier');
const MockWebpackPlugin = require('./mock-webpack-plugin');

const complier = new Compiler({
  plugins: [
    new MockWebpackPlugin(),
  ]
});

complier.run();

mock-webpack-plugin.js

class MockWebpackPlugin {

  apply(compiler) {

    compiler.hooks.testSyncHook.tap('MockWebpackPlugin', (name, age) => {
      console.log('同步事件', name, age);
    })

    compiler.hooks.testAsyncHook.tapAsync('MockWebpackPlugin', (name, age) => {
      setTimeout(() => {
        console.log('异步事件', name, age)
      }, 3000)
    })
  }
}

module.exports = MockWebpackPlugin;

我相信有些小伙伴看到上述代码,就已经明白了大概的逻辑,我们只需要抓住发布订阅这两个词,在代码中呈现的就是 tap 和 call,如果是异步钩子,使用 tapAsync, tapPromise 注册(发布),就要用 callAsync, promise(注意这里的 promise 是 Tapable 钩子实例方法,不要跟 Promise API 搞混) 触发(订阅)。

发布

    compiler.hooks.testSyncHook.tap('MockWebpackPlugin', (name, age) => {
      console.log('同步事件', name, age);
    })

    compiler.hooks.testAsyncHook.tapAsync('MockWebpackPlugin', (name, age) => {
      setTimeout(() => {
        console.log('异步事件', name, age)
      }, 3000)
    })

这里可以看到使用 tab 和 tabAsync 进行注册,在什么时机注册的呢,在 Compiler 类的初始化时期,也就是在通过 new 命令生成对象实例的时候,下面的代码已经在 constructor 中被调用并执行了,当然这个时候并没有像函数一样被调用,打印出来姓名和年龄,这时我们只需要先知道,它们已经被注册了。

### 订阅 ###

  run() {
    this.testSyncHook('ggg', 25);
    this.testAsyncHook('hhh', 24);
  }

  testSyncHook(name, age) {
    this.hooks.testSyncHook.call(name, age);
  }

  testAsyncHook(name, age) {
    this.hooks.testAsyncHook.callAsync(name, age);
  }

通过 compiler.run() 命令将会执行下面两个函数,使用 call 和 callAsync 订阅。这个时候就会执行 console.log 来打印姓名和年龄了,所以说此时我们就能明白 webpack 中 compiler 和 compilation 中的钩子函数是以触发的时期进行区分,归根结底,是注册的钩子在 webpack 不同的编译时期被触发。

注意事项

这里要注意在初始化 Tapable Hook 的同时,要加上参数,传入参数的数量需要与实例化时传递给钩子类构造函数的数组长度保持一致。

    this.hooks = {
      testSyncHook: new SyncHook(['name', 'age']),
      testAsyncHook: new AsyncParallelHook(['name', 'age'])
    }

这里并非要严格的传入 [‘name’, ‘age’],你也可以取其它的名字,如 [‘fff’, ‘ggg],但是为了语义化,还是要进行规范,如下方代码,截取自源码中的 lib/Compiler.js 片段,它们在初始化中也是严格按照了这个规范。

    /** @type {AsyncSeriesHook<[Compiler]>} */
    beforeRun: new AsyncSeriesHook(["compiler"]),
    /** @type {AsyncSeriesHook<[Compiler]>} */
    run: new AsyncSeriesHook(["compiler"]),
    /** @type {AsyncSeriesHook<[Compilation]>} */
    emit: new AsyncSeriesHook(["compilation"]),

更具体的可以查看这篇文章 走进 Tapable – 掘金 (juejin.cn)

如何调试

想调试 webpack 源码,一般有两种方式,一种是 clone 调试,一种是 npm 包调试,笔者这里选择通过 clone 调试,运行 webpack 也有两种方式,一是通过 webpack-cli 输入命令启动,另外一种如下,引入 webapck,使用 webpack.run() 启动。

准备工作

首先可以用 https 从 github 上克隆 webpack 源码。

    git clone https://github.com/webpack/webpack
    npm install

之后可以在根目录创建一个名为 source 的文件夹,source 文件夹目录如下

-- webpack
    -- source 
        -- src 
             -- foo.js
             -- main.js
        -- index.html
        -- index.js
        -- webpack.config.js 

index.js

const webpack = require('../lib/index.js');
const config = require('./webpack.config.js');

const complier = webpack(config);
complier.run((err, stats) => {
  if (err) {
    console.error(err);
  } else {
    console.log(stats);
  }
})

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: './src/main.js',
  output: {
      path: path.join(__dirname, './dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        exclude: /node_modules/,
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Test Webpack',
      template: './index.html',
      filename: 'template.html'
    })
  ]
}

引用 html-webpack-plugin 和 babel-loader 主要是想更清晰看到在构建过程中 webpack 会如何处理引入的 plugin 和 loader。

main.js

import foo from './foo.js';
import { isEmpty } from 'lodash';

foo();

const obj = {};
console.log(isEmpty(obj));
console.log('main.js');

foo.js

export default function foo() {
  console.log('foo');
}

文件创建好了,这里使用 Vscode 进行调试, 打开 JavaScript 调试终端。

image.png

源码阅读

按照下面命令,启动 webpack

    cd source
    node index.js

这里为了更加清晰, 可以打上一个断点。如在 lib/webpack.js 中,将断点打在 158 行,查看是如何生成的 compiler 实例。

image.png

这里需要点击单步调试,这样才能进入 create 函数中,一步步调试可以看到,首先会对传入的 options 进行校验, 如果不符合规范,将会抛出错误,由于这里的 options 是一个对象,将会进入到 createCompiler 函数内。

image.png

在这个函数内将会创造 Compiler 实例,以及注册引入的插件和内置插件。

image.png

笔者将会一步步的讲解这个函数都做了什么事,如

applyWebpackOptionsBaseDefaults:给没设置的基本配置加上默认值。

new Compiler:生成 compiler 实例,初始化一些钩子和参数。

NodeEnvironmentPlugin:主要是对文件模块进行了封装和优化,感兴趣的读者可以打断点,详细去查看。

接下来要做的事情就是注册钩子,如上文中引入了 html-webpack-plugin, 这里将会调用 HtmlWebpackplugin 实例的 apply 函数,这样就能明白为什么以 class 类的方式,写插件,为什么里面一定要加上 apply。紧接着创建完 compiler 实例后,正如官网上描述的,关于 compiler.hooks.environment 的订阅时期,在编译器准备环境时调用,时机就在配置文件中初始化插件之后。我们就能知其然,也能知所以然了。

image.png

再往下,

new WebpackOptionsApply().process(options, compiler):注册了内部插件,如 DllPlugin, HotModuleReplacementPlugin 等。

小技巧分享

这里简单分享了笔者看源码的步骤,然后还有两个技巧分享。

一是由于 webpack 运用了大量回调函数,一步步打断点是很难看的清楚的,可直接在 Vscode 中全局搜索 compiler.hooks.xxx 和 compilation.hooks.xxx, 去看 tap 中回调函数的执行。

二是可在 Vscode 调试中的 watch 模块,添加上 compiler 和 compilation,这样也是更方便观察回调函数的执行。如

image.png

总结

webpack 中的细节很是繁多,里面有大量的异常处理,在看的时候要有重点的看,有选择的看,如果你要看 make 阶段所做的事情, 可以重点去看如何生成模块,模块分为几种,如何递归处理依赖,如何使用 loader 解析文件等。笔者认为看源码还有一个好处,那就是让你对这些知名开源库没有畏惧心理,它们也是用 js 一行行写的,里面会有一些代码片段,可能写的也没有那么优美,我们在阅读代码的同时,说不定也能成为代码贡献者,能够在简历上留下浓墨重彩的一笔。

作者:百宝门-前端组-闫磊刚