Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack loader #31

Open
6 of 10 tasks
r2099c2 opened this issue Jun 13, 2018 · 0 comments
Open
6 of 10 tasks

webpack loader #31

r2099c2 opened this issue Jun 13, 2018 · 0 comments

Comments

@r2099c2
Copy link

r2099c2 commented Jun 13, 2018

webpack-loader

@(前端)[webpack]

一些不错的文章
https://doc.webpack-china.org/contribute/writing-a-loader/
http://taobaofed.org/blog/2016/09/09/webpack-flow/
youngwind/blog#99
https://img.alicdn.com/tps/TB1GVGFNXXXXXaTapXXXXXXXXXX-4436-4244.jpg
https://github.com/jamiebuilds/the-super-tiny-compiler/blob/master/the-super-tiny-compiler.js

[toc]

1.任务

webpack的出现改变了前端组织代码的方式,它基于loader的插件体系让任何前端资源都能被打包和加载,我们使用一样的语法加载js, css, svg, png甚至现在还不知道的文件类型。但是正因为功能太强大,多数人对它的配置也是望而却步。你能去研究下webpack的loader是如和工作和组织的吗。

任务:

  1. 学习webpack是如果工作的
  2. 尝试编写一个简单的webpack插件

技能

  • 构建工具

解题思路

  • 什么是loader,有什么功能和特性
  • 在webpack内部是如何工作的
  • 如何使用loader
  • 如何编写loader
  • 写一个loader

延伸

  • AST acorn
  • 工厂模式
  • tapable & hook
  • 比loader更强大的plugin又是什么
  • webpack4.0带来了新特性 如何使用

2.关于loader


1.什么是loader

loader 用于对模块的源代码进行转换。
可以允许webpack去处理那些非js文件webpack 自身只理解js。loader 可以将所有类型的文件转换成webpack能够处理的有效模块。比如css、font、各种image、xml等

2.loader特性

  • loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何可能的操作。
  • loader 接收查询参数。用于对 loader 传递配置。
  • loader 也能够使用 options 对象进行配置。
  • *???除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。*我查了npm文档是没有loader这个字段的,不影响主流程,暂时不管他
  • 插件(plugin)可以为 loader 带来更多特性。
  • loader 能够产生额外的任意文件。

3.loader在webpack内部是如何工作的

或者说webpack是如何调用loader的

以下都是重点都是在loader相关的方向

1. 从头分析webpack

这部分就必须去分析webpack源码了。首先我们知道loader是为了处理模块而产生的,那么目标就很明确了,我们猜想webpack应该是在打包过程中,生成最终模块之前引入模块之后这段时间开始使用loader的,因为这个时候正是处理模块的时候。所以我们首先尝试去找到webpack中引入模块的地方

从头说起,我们在启动一个项目的时候使用的命令是webpack所以,自然的想到从bin/webpack开始。这里是以当前的版本4.6.0的master开始的。

bin/webpack非常简单仅仅是简单的分析了如果有webpck-cli就去require否则就让用户去安装。

ps: 4.0的webpack将cli拆分出一个独立的库去维护了
pps: 源码比较简单的地方就不管了 只标出一些值得注意的地方,也是为了标记下回头查的方便

接下来在webpack-cli源码中,(通过package.json知道入口是bin/webpack.js)。这里代码可以看到使用了yargs 一个node的库用来处理命令行的参数。然后调用convert-argv来获取到你的webpack.config.js文件中的配置,然后去处理下参数。一直到434行左右,可以看到require了webpack并且传递配置开始编译,最后调用了processOptions()这个方法就作用是处理了输出相关的参数,并且执行了编译函数。

...
// 这里调用了convert-argv 在这个文件里可以找到引入我们webpack.config.js的地方
// line:237
let options;
try {
  options = require("./convert-argv")(argv);
...
// 其他代码都是在处理参数
// line: 434
// 定义了compiler
let compiler;
try {
compiler = webpack(options);
...
// 后面定义了回调函数,定义了如果有watch参数执行complile的watch 否则执行run
if (firstOptions.watch || options.watch) {
...
compiler.watch(watchOptions, compilerCallback);
...
} else compiler.run(compilerCallback);
// 最后执行上面定义的这个方法

然后根据初始化compiler的代码,我们先回到了webpack的入口文件"main": "lib/webpack.js"。同样也是引入了一些文件然后就是最核心的webpack的定义,最后export了很多的自带的Plugin,源码也比较简单。

// 这里的webpackOptionsApply处理了options里的所有数据,也就是我们配置文件里的内容如果需要都会在这边被处理一遍,这里就先不展开了
compiler.options = new WebpackOptionsApply().process(options, compiler);

回到cli文件最后的run这里很容易看出来最主要的东西就是这个Compiler文件,打开Compiler文件后我们会看到Compiler文件的所有的hooks。
这里官方文档有一些简单解释
https://webpack.js.org/api/compiler-hooks/

this.hooks = {
	shouldEmit: new SyncBailHook(["compilation"]),
	done: new AsyncSeriesHook(["stats"]),
	additionalPass: new AsyncSeriesHook([]),
	beforeRun: new AsyncSeriesHook(["compilation"]),
	run: new AsyncSeriesHook(["compilation"]),
	emit: new AsyncSeriesHook(["compilation"]),
	afterEmit: new AsyncSeriesHook(["compilation"]),
	thisCompilation: new SyncHook(["compilation", "params"]),
	compilation: new SyncHook(["compilation", "params"]),
	normalModuleFactory: new SyncHook(["normalModuleFactory"]),
	contextModuleFactory: new SyncHook(["contextModulefactory"]),
	beforeCompile: new AsyncSeriesHook(["params"]),
	compile: new SyncHook(["params"]),
	make: new AsyncParallelHook(["compilation"]),
	afterCompile: new AsyncSeriesHook(["compilation"]),
	watchRun: new AsyncSeriesHook(["compiler"]),
	failed: new SyncHook(["error"]),
	invalid: new SyncHook(["filename", "changeTime"]),
	watchClose: new SyncHook([]),

	// TODO the following hooks are weirdly located here
	// TODO move them for webpack 5
	environment: new SyncHook([]),
	afterEnvironment: new SyncHook([]),
	afterPlugins: new SyncHook(["compiler"]),
	afterResolvers: new SyncHook(["compiler"]),
	entryOption: new SyncBailHook(["context", "entry"])
};

另外在顶部可以看到引入了tapable,这个是webpack里非常重要的一个库。

2. tapable

webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例

const {
	Tapable,
	SyncHook,
	SyncBailHook,
	AsyncParallelHook,
	AsyncSeriesHook
} = require("tapable");

class Compiler extends Tapable 

可以看到整个Compiler都是继承自Tapable

tapable 用他的规范保证了webpack体系的有序性,使整个系统有非常好的扩展性。但是坏处是文件比较细,各种引用所以看源码很痛苦。

这个库的用法我理解起来比较费劲,因为完全不了解Hook机制,所以又花时间去研究了下hook机制。
这个库现在的版本是1.0.2 在webpack发布4.0后这个库同时进行了很大的升级,以至于网络上的大部分文章都不合适了(我没有找到一个分析1.x版本的)。
对于tapable只是大概读懂了一部分,花了点时间写了个例子。感觉以后如果有机会写大型的库可以考虑用tapable或者他的这种思想,这里只提一下他的大概的用法,具体等我例子完成了研究下源码在分享。因为和本篇关系不大放到另一个文章中了。

3. 调试webpack

插播一条如何去调试webpack,因为在看webpack源码的时候不懂tapable又不懂hook看起来非常费劲,所以知道怎么调试webpack省去了很多力气。

比较简单的调试方式:

  1. 安装了node chrome,node版本要新我本人是9.11.1是没问题的
  2. 浏览器输入chrome://inspect/#devices
  3. 点击Open dedicated DevTools for Node
  4. "debug": "node --inspect-brk node_modules/.bin/webpack --config webpack.config.js"
  5. 然后就可以yarn debug调试了

4. webpack流程图(loader相关)

这里根据我打了一堆debugger和console后得出的结论来绘制了一个loader相关的流程图,不包括其他webpack流程。请配合源码食用。

st=>start: 我们执行webpack命令

cli=>operation: 根据package.json执行bin/webpack.js

bwp=>operation: 根据package.json执行webpack-cli中webpack.js

cli-wp=>operation: require("webpack"):webpack中的lib/webpack.js
进行一些初始化 比如挂载hook、初始化env
然后执行run

run=>operation: run方法来自compiler文件,执行beforerun
这个hook是在初始化的时候挂载的
这时run并没有任何方法挂载,直接执行回调函数
然后执行this.compile(onCompiled)

compile=>operation: new Compilation()
compilation负责整个编译过程,包含了每个编译环节对应的方法
new完成后他在compile的make上挂载了几个tap
其中根据入口文件的数量和类型挂载了singleEntryPlugin或者多入口的plugin

sep=>operation: 然后在singleEntryPlugin里面循环入口创建createDependency & 执行addEntry方法

addEntry=>operation: addEntry里面有几个比较重要的方法_addModuleChain开始,
把当前入口的模块添加进入compilation对象上。
这里的module是在compiler中newCompilationParams下面根据对应的工厂方法创建的。
buildModule 这个方法里面调用了对应的module (normalModule contextModule)中的build方法去build。

build=>operation: 这里执行了dobuild 里面调用了loader-runner这个库的runLoaders方法去执行loader。

afterBuild=>operation: 在完全对一个入口文件执行完Build之后,module的factory会调用acorn处理文件生成AST然后会遍历AST
在遇到依赖的时候会添加到dependency中,最后会循环构建。
最后就是组装chunck,压缩代码等操作
end=>end

st->cli->bwp->cli-wp->run->compile->sep->addEntry->build->afterBuild->end

github 不支持 flow写法 补上图
image
image

额外需要仔细研究的:AST,工厂模式,

4.如何使用loader

loader有2个属性

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
  2. use 属性,表示进行转换时,应该使用哪个 loader。
module: {
  rules: [
    { test: /\.txt$/, use: 'raw-loader' }
  ]
}

module 对象下面的rules对象接收数组,表示可以定义多个rule,每一个rule必须包含test和use属性

同时use属性可以是一个数组 允许配置多个loader,以及这些loader需要的配置参数
你可能需要首先安装这些loader

 module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        { loader: 'style-loader' },
        {
          loader: 'css-loader',
          options: {
            modules: true
          }
        }
      ]
    }
  ]
}

以及如果不需要配置额外参数,可以简写

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        style-loader,
        css-loader
      ]
    }
  ]
}

虽然webpack同时支持内联的方式和CLI方式 但是实际项目中还是要尽量避免使用这两种方式 https://doc.webpack-china.org/concepts/loaders/

5. 如何写一个loader

说了这么多,如何写一个loader呢?
官方给了很明确的例子和开发方式:https://webpack.js.org/contribute/writing-a-loader/

这里基本没什么可说的,比较重要的一点是:webpack对于开发loader的很多建议同样也适用于我们开发组件。

loader的处理方式大致可以分成2类:

  1. 使用loader来处理一个自定义的后缀的文件,比如某个场景下我们定义了一个文件后缀是.hlj 就可以使用loader来处理。
  2. 处理现有的文件类型中的问题。

6. 写一个loader

因为没有合适的业务需求,所以这个反而是最难的一步:找一个需要写loader的业务场景。因为loader的作用是转化文件,让webpack可以处理。 最后只能自己创造一个假的场景来完成任务了,将来找到可以写loader的场景在写真的吧。

比如有一个场景是:我们很多旧图片的都是来自域名hlj-img.b0.upaiyun.com/zmw
新的都是在https://img-ucdn-static.helijia.com/zmw

那么假如我们需要替换这个图片域名可以怎么处理呢?

最彻底的解决办法当然是找到代码中的这些图片替换掉,但是比较消耗人力,或者写一个脚本扫一遍文件去替换?总之肯定是有办法的。

这个场景刚好可以来尝试写一个简单的loader。

首先我们搭一个项目,因为功能超级简单,所以手写就行。


  1. 创建文件,存放我们的loaders src/loaders/hlj-img-url-loader
  2. 添加需要的库
yarn add webpck webpack-cli -D
yarn add react react-dom babel-loader babel-core babel-preset-react babel-preset-env
  1. webpack.config.js
const path = require('path');

module.exports = {
  entry: [
    './src/index.js'
  ],
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development',
  module: {
    rules: [{
      test: /\.js$/,
      use: [
        'hlj-img-url-loader',
        {
          loader: 'babel-loader',
          options: {
            presets: ['env', 'react']
          }
        }
      ]
    }]
  },

  resolveLoader: {
    // 这里用到的babel-loader在node_modules里面
    modules: [path.join(__dirname, './src/loaders'), 'node_modules']
  }
};
  1. npm init 后 package.json里加上一个scripts
"scripts": {
	"build": "webpack"
}
  1. 随便写个react的例子
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>

  <script src="../dist/main.js"></script>
</body>
</html>
// index.js
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return (
    <img src="https://hlj-img.b0.upaiyun.com/zmw/upload/mobile/star/star5.gif" alt="star"/>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

export default App;
  1. 最后我们的loader
// loader 就是个js模块 
module.exports = function(source) {
  // 这里写处理逻辑
  const result = source;
  return result;
}

现在yarn start就可以打包出来最终的js了。我们的loader暂时没有做任何事情。
这里可以用console.log看到source返给我们的东西,但是由于引用了react,代码还是比较乱的。

接下来就是处理loader,方法很简单,replace一下就好了

module.exports = function(source) {
  return source.replace('hlj-img.b0.upaiyun.com', 'img-ucdn-static.helijia.com');
}

我们的图片已经替换了url。齐活。

优化:如果我们有一天希望换个域名呢?所以最好能做到可配置。

  1. 修改配置文件添加options参数
{
  loader: 'hlj-img-url-loader',
  options: {
    target: 'img-ucdn-static.helijia.com'
  }
}
  1. 根据文档提示 使用 loader-utils 来获取参数
const loaderUtils = require('loader-utils');

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  const newUrl = options.target;
  return newUrl ? source.replace('hlj-img.b0.upaiyun.com', newUrl) : source;
}

这样就好了,因为是个无用的功能所以就不发布了。。

我们这个是例子,所以包放到了loaders文件夹,其实真是项目开发最好是创建一个单独文件夹来存放loader项目,配置上package.json,就和Npm包的开发一样。然后link到项目中测试。 这样发布比较方便。

发布方式和发布一个npm包一样 这里就不赘述了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant