Giter Club home page Giter Club logo

fe-library's Introduction

fe-library's People

Contributors

zhanyi233 avatar

Watchers

 avatar  avatar

fe-library's Issues

介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

1. npm 模块安装机制:

  • 发出npm install命令
  • 查询node_modules目录之中是否已经存在指定模块
    • 若存在,不再重新安装
    • 若不存在
      • npm 向 registry 查询模块压缩包的网址
      • 下载压缩包,存放在根目录下的.npm目录里
      • 解压压缩包到当前项目的node_modules目录

2. npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

  1. 执行工程自身 preinstall

当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

  1. 确定首层依赖模块

首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

  1. 获取模块

获取模块是一个递归的过程,分为以下几步:

  • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
  • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
  • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
  1. 模块扁平化(dedupe)

上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。

从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

这里需要对重复模块进行一个定义,它指的是模块名相同semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。

而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。

举个例子,假设一个依赖树原本是这样:

node_modules
-- foo
---- lodash@version1

-- bar
---- lodash@version2

假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:

node_modules
-- foo

-- bar

-- lodash(保留的版本为兼容版本)

假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:

node_modules
-- foo
-- lodash@version1

-- bar
---- lodash@version2

  1. 安装模块

这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

  1. 执行工程自身生命周期

当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

最后一步是生成或更新版本描述文件,npm install 过程完成。

参考 npm 模块安装机制简介

详解npm的模块安装机制

npm install的实现原理

探寻 webpack 插件机制

webpack 可谓是让人欣喜又让人忧,功能强大但需要一定的学习成本。在探寻 webpack 插件机制前,首先需要了解一件有意思的事情,webpack 插件机制是整个 webpack 工具的骨架,而 webpack 本身也是利用这套插件机制构建出来的。因此在深入认识 webpack 插件机制后,再来进行项目的相关优化,想必会大有裨益。

webpack 插件

先来瞅瞅 webpack 插件在项目中的运用

const MyPlugin = require('myplugin')

const webpack = require('webpack')

webpack({
...,
plugins: [new MyPlugin()]
...,
})

那么符合什么样的条件能作为 webpack 插件呢?一般来说,webpack 插件有以下特点:

  1. 独立的 JS 模块,暴露相应的函数

  2. 函数原型上的 apply 方法会注入 compiler 对象

  3. compiler 对象上挂载了相应的 webpack 事件钩子

  4. 事件钩子的回调函数里能拿到编译后的 compilation 对象,如果是异步钩子还能拿到相应的 callback

下面结合代码来看看:

function MyPlugin(options) {}
// 2.函数原型上的 apply 方法会注入 compiler 对象
MyPlugin.prototype.apply = function(compiler) {
// 3.compiler 对象上挂载了相应的 webpack 事件钩子 4.事件钩子的回调函数里能拿到编译后的 compilation 对象
compiler.plugin('emit', (compilation, callback) => {
...
})
}
// 1.独立的 JS 模块,暴露相应的函数
module.exports = MyPlugin

这样子,webpack 插件的基本轮廓就勾勒出来了,此时疑问点有几点,

  1. 疑问 1:函数的原型上为什么要定义 apply 方法?阅读源码后发现源码中是通过 plugin.apply() 调用插件的。
const webpack = (options, callback) => {
...
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
...
}
  1. 疑问 2:compiler 对象是什么呢?

  2. 疑问 3:compiler 对象上的事件钩子是怎样的?

  3. 疑问 4:事件钩子的回调函数里能拿到的 compilation 对象又是什么呢?

这些疑问也是本文的线索,让我们一个个探索。

compiler 对象

compiler 即 webpack 的编辑器对象,在调用 webpack 时,会自动初始化 compiler 对象,源码如下:

// webpack/lib/webpack.js
const Compiler = require("./Compiler")

const webpack = (options, callback) => {
...
options = new WebpackOptionsDefaulter().process(options) // 初始化 webpack 各配置参数
let compiler = new Compiler(options.context) // 初始化 compiler 对象,这里 options.context 为 process.cwd()
compiler.options = options // 往 compiler 添加初始化参数
new NodeEnvironmentPlugin().apply(compiler) // 往 compiler 添加 Node 环境相关方法
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
...
}

终上,compiler 对象中包含了所有 webpack 可配置的内容,开发插件时,我们可以从 compiler 对象中拿到所有和 webpack 主环境相关的内容。

compilation 对象

compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。

结合源码来理解下上面这段话,首先 webpack 在每次执行时会调用 compiler.run() (源码位置),接着追踪 onCompiled 函数传入的 compilation 参数,可以发现 compilation 来自构造函数 Compilation。

// webpack/lib/Compiler.js
const Compilation = require("./Compilation");

newCompilation(params) {
const compilation = new Compilation(this);
...
return compilation;
}

不得不提的 tapable 库

再介绍完 compiler 对象和 compilation 对象后,不得不提的是 tapable 这个库,这个库暴露了所有和事件相关的 pub/sub 的方法。而且函数 Compiler 以及函数 Compilation 都继承自 Tapable。

事件钩子

事件钩子其实就是类似 MVVM 框架的生命周期函数,在特定阶段能做特殊的逻辑处理。了解一些常见的事件钩子是写 webpack 插件的前置条件,下面列举些常见的事件钩子以及作用:

钩子 作用 参数 类型
after-plugins 设置完一组初始化插件之后 compiler sync
after-resolvers 设置完 resolvers 之后 compiler sync
run 在读取记录之前 compiler async
compile 在创建新 compilation 之前 compilationParams sync
compilation compilation 创建完成 compilation sync
emit 在生成资源并输出到目录之前 compilation async
after-emit 在生成资源并输出到目录之后 compilation async
done 完成编译 stats sync

完整地请参阅官方文档手册,同时浏览相关源码 也能比较清晰地看到各个事件钩子的定义。

插件流程浅析

拿 emit 钩子为例,下面分析下插件调用源码:

compiler.plugin('emit', (compilation, callback) => {
  // 在生成资源并输出到目录之前完成某些逻辑
})

此处调用的 plugin 函数源自上文提到的 tapable 库,其最终调用栈指向了 hook.tapAsync(),其作用类似于 EventEmitter 的 on,源码如下:

// Tapable.js
options => {
  ...
  if(hook !== undefined) {
    const tapOpt = {
      name: options.fn.name || "unnamed compat plugin",
      stage: options.stage || 0
    };
    if(options.async)
      hook.tapAsync(tapOpt, options.fn); // 将插件中异步钩子的回调函数注入
    else
      hook.tap(tapOpt, options.fn);
    return true;
  }
};

有注入必有触发的地方,源码中通过 callAsync 方法触发之前注入的异步事件,callAsync 类似 EventEmitter 的 emit,相关源码如下:

this.hooks.emit.callAsync(compilation, err => {
	if (err) return callback(err);
	outputPath = compilation.getPath(this.outputPath);
	this.outputFileSystem.mkdirp(outputPath, emitFiles);
});

一些深入细节这里就不展开了,说下关于阅读比较大型项目的源码的两点体会,

  • 要抓住一条主线索去读,忽视细节。否则会浪费很多时间而且会有挫败感;

  • 结合调试工具来分析,很多点不用调试工具的话很容易顾此失彼;

动手实现个 webpack 插件

结合上述知识点的分析,不难写出自己的 webpack 插件,关键在于想法。为了统计项目中 webpack 各包的有效使用情况,在 fork webpack-visualizer 的基础上对代码升级了一番,项目地址。效果如下:

插件核心代码正是基于上文提到的 emit 钩子,以及 compiler 和 compilation 对象。代码如下:

class AnalyzeWebpackPlugin {
  constructor(opts = { filename: 'analyze.html' }) {
    this.opts = opts
  }

apply(compiler) {
const self = this
compiler.plugin("emit", function (compilation, callback) {
let stats = compilation.getStats().toJson({ chunkModules: true }) // 获取各个模块的状态
let stringifiedStats = JSON.stringify(stats)
// 服务端渲染
let html = &lt;!doctype html&gt;</span> <span class="pl-s" style="box-sizing: border-box; color: var(--color-prettylights-syntax-string);"> &lt;meta charset="UTF-8"&gt;</span> <span class="pl-s" style="box-sizing: border-box; color: var(--color-prettylights-syntax-string);"> &lt;title&gt;AnalyzeWebpackPlugin&lt;/title&gt;</span> <span class="pl-s" style="box-sizing: border-box; color: var(--color-prettylights-syntax-string);"> &lt;style&gt;<span class="pl-s1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-storage-modifier-import);"><span class="pl-kos" style="box-sizing: border-box;">${</span><span class="pl-s1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-storage-modifier-import);">cssString</span><span class="pl-kos" style="box-sizing: border-box;">}</span></span>&lt;/style&gt;</span> <span class="pl-s" style="box-sizing: border-box; color: var(--color-prettylights-syntax-string);"> &lt;div id="App"&gt;&lt;/div&gt;</span> <span class="pl-s" style="box-sizing: border-box; color: var(--color-prettylights-syntax-string);"> &lt;script&gt;window.stats = <span class="pl-s1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-storage-modifier-import);"><span class="pl-kos" style="box-sizing: border-box;">${</span><span class="pl-s1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-storage-modifier-import);">stringifiedStats</span><span class="pl-kos" style="box-sizing: border-box;">}</span></span>;&lt;/script&gt;</span> <span class="pl-s" style="box-sizing: border-box; color: var(--color-prettylights-syntax-string);"> &lt;script&gt;<span class="pl-s1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-storage-modifier-import);"><span class="pl-kos" style="box-sizing: border-box;">${</span><span class="pl-s1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-storage-modifier-import);">jsString</span><span class="pl-kos" style="box-sizing: border-box;">}</span></span>&lt;/script&gt;</span> <span class="pl-s" style="box-sizing: border-box; color: var(--color-prettylights-syntax-string);">
compilation.assets[<span class="pl-s1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-storage-modifier-import);"><span class="pl-kos" style="box-sizing: border-box;">${</span><span class="pl-s1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-storage-modifier-import);">self</span><span class="pl-kos" style="box-sizing: border-box;">.</span><span class="pl-c1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-constant);">opts</span><span class="pl-kos" style="box-sizing: border-box;">.</span><span class="pl-c1" style="box-sizing: border-box; color: var(--color-prettylights-syntax-constant);">filename</span><span class="pl-kos" style="box-sizing: border-box;">}</span></span>] = { // 生成文件路径
source: () => html,
size: () => html.length
}
callback()
})
}
}

参考资料

看清楚真正的 Webpack 插件

webpack 官网

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.