初探webpack之编写loader
初探webpack之编写loader
loader加载器是webpack的核心之一,其用于将不同类型的文件转换为webpack可识别的模块,即用于把模块原内容按照需求转换成新内容,用以加载非js模块,通过配合扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果,从而完成一次完整的构建。
描述
webpack是一个现代JavaScript应用程序的静态模块打包器module bundler,当webpack处理应用程序时,它会递归地构建一个依赖关系图dependency graph,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。
使用webpack作为前端构建工具通常可以做到以下几个方面的事情:
- 代码转换:
TypeScript编译成JavaScript、SCSS编译成CSS等。 - 文件优化: 压缩
JavaScript、CSS、HTML代码,压缩合并图片等。 - 代码分割: 提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
- 模块合并: 在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
- 自动刷新: 监听本地源代码的变化,自动重新构建、刷新浏览器页面,通常叫做模块热替换
HMR。 - 代码校验: 在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
- 自动发布: 更新完代码后,自动构建出线上发布代码并传输给发布系统。
对于webpack来说,一切皆模块,而webpack仅能处理出js以及json文件,因此如果要使用其他类型的文件,都需要转换成webpack可识别的模块,即js或json模块。也就是说无论什么后缀的文件例如png、txt、vue文件等等,都需要当作js来使用,但是直接当作js来使用肯定是不行的,因为这些文件并不符合js的语法结构,所以就需需要webpack loader来处理,帮助我们将一个非js文件转换为js文件,例如css-loader、ts-loader、file-loader等等。
在这里编写一个简单的webpack loader,设想一个简单的场景,在这里我们关注vue2,从实例出发,在平时我们构建vue项目时都是通过编写.vue文件来作为模块的,这种单文件组件的方式虽然比较清晰,但是如果一个组件比较复杂的话,就会导致整个文件相当大。当然vue中给我们提供了在.vue文件中引用js、css的方式,但是这样用起来毕竟还是稍显麻烦,所以我们可以通过编写一个webpack loader,在编写代码时将三部分即html、js、css进行分离,之后在loader中将其合并,再我们编写的loader完成处理之后再交与vue-loader去处理之后的事情。当然,关注点分离不等于文件类型分离,将一个单文件分成多个文件也只是对于代码编写过程中可读性的倾向问题,在这里我们重点关注的是编写一个简单的loader而不在于对于文件是否应该分离的探讨。文中涉及到的所有代码都在https://github.com/WindrunnerMax/webpack-simple-environment。
实现
搭建环境
在这里直接使用我之前的 初探webpack之从零搭建Vue开发环境 中搭建的简单vue + ts开发环境,环境的相关的代码都在https://github.com/WindrunnerMax/webpack-simple-environment中的webpack--vue-cli分支中,我们直接将其clone并安装。
git clone https://github.com/WindrunnerMax/webpack-simple-environment.git
git checkout webpack--vue-cli
yarn install --registry https://registry.npm.taobao.org/
之后便可以通过运行yarn dev来查看效果,在这里我们先打印一下此时的目录结构。
webpack--vue-cli
├── dist
│ ├── static
│ │ └── vue-large.b022422b.png
│ ├── index.html
│ ├── index.js
│ └── index.js.LICENSE.txt
├── public
│ └── index.html
├── src
│ ├── common
│ │ └── styles.scss
│ ├── components
│ │ ├── tab-a.vue
│ │ └── tab-b.vue
│ ├── router
│ │ └── index.ts
│ ├── static
│ │ ├── vue-large.png
│ │ └── vue.jpg
│ ├── store
│ │ └── index.ts
│ ├── views
│ │ └── framework.vue
│ ├── App.vue
│ ├── index.ts
│ ├── main.ts
│ ├── sfc.d.ts
│ └── sum.ts
├── LICENSE
├── README.md
├── babel.config.js
├── package.json
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
编写loader
在编写loader之前,我们先关注一下上边目录结构中的.vue文件,因为此时我们需要将其拆分,但是如何将其拆分是需要考虑一下的,为了尽量不影响正常的使用,在这里采用了如下的方案。
- 将
template部分留在了.vue文件中,因为一些插件例如Vetur是会检查template中的一些语法,例如将其抽离出作为html文件,对于@click等语法prettier是会有error提醒的,而且如果不存在.vue文件的话,对于在TS中使用declare module "*.vue"也需要修改,所以本着最小影响的原则我们将template部分留在了.vue文件中,保存了.vue这个声明的文件。 - 对于
script部分,我们将其抽出,如果是使用js编写的,那么就将其命名为.vue.js,同样ts编写的就命名为.vue.ts。 - 对于
style部分,我们将其抽出,与script部分采用同样的方案,使用css、scss、less也分别命名为.vue.css、.vue.scss、.vue.less,而对于scoped我们通过注释的方式来实现。
通过以上的修改,我们将文件目录再次打印出来,重点关注于.vue文件的分离。
webpack--loader
├── dist
│ ├── static
│ │ └── vue-large.b022422b.png
│ ├── index.html
│ ├── index.js
│ └── index.js.LICENSE.txt
├── public
│ └── index.html
├── src
│ ├── common
│ │ └── styles.scss
│ ├── components
│ │ ├── tab-a
│ │ │ ├── tab-a.vue
│ │ │ └── tab-a.vue.ts
│ │ └── tab-b
│ │ ├── tab-b.vue
│ │ └── tab-b.vue.ts
│ ├── router
│ │ └── index.ts
│ ├── static
│ │ ├── vue-large.png
│ │ └── vue.jpg
│ ├── store
│ │ └── index.ts
│ ├── views
│ │ └── framework
│ │ ├── framework.vue
│ │ ├── framework.vue.scss
│ │ └── framework.vue.ts
│ ├── App.vue
│ ├── index.ts
│ ├── main.ts
│ ├── sfc.d.ts
│ └── sum.ts
├── LICENSE
├── README.md
├── babel.config.js
├── package.json
├── tsconfig.json
├── vue-multiple-files-loader.js
├── webpack.config.js
└── yarn.lock
现在我们开始正式编写这个loader了,首先需要简单说明一下loader的输入与输出以及常用的模块。
- 简单来说
webpack loader是一个从string到string的函数,输入的是字符串的代码,输出也是字符串的代码。 - 通常来说对于各种文件的处理
loader已经都有很好的轮子了,我们自己来编写的loader通常是用来做代码处理的,也就是说在loader中拿到source之后,我们将其转换为AST树,然后在这个AST上进行一些修改,之后再将其转换为字符串代码之后进行返回。 - 从字符串到
AST语法分析树是为了得到计算机容易识别的数据结构,在webpack中自带了一些工具,acorn是代码转AST的工具,estraverse是AST遍历工具,escodegen是转换AST到字符串代码的工具。 - 既然
loader是字符串到字符串的,那么在代码转换为AST处理之后需要转为字符串,然后再传递到下一个loader,下一个loader可能又要进行相同的转换,这样还是比较耗费时间的,所以可以通过speed-measure-webpack-plugin进行速率打点,以及cache-loader来存储AST。 loader-utils是在loader中常用的辅助类,常用的有urlToRequest绝对路径转webpack请求的相对路径,urlToRequest来获取配置loader时传递的参数。
由于我们在这里这个需求是用不到AST相关的处理的,所以还是比较简单的一个实例,首先我们需要写一个loader文件,然后配置在webpack.config.js中,在根目录我们建立一个vue-multiple-files-loader.js,然后在webpack.config.js的module.rule部分找到test: /\.vue$/,将这部分修改为如下配置。
// ...
{
test: /\.vue$/,
use: [
"vue-loader",
{
loader: "./vue-multiple-files-loader",
options: {
// 匹配的文件拓展名
style: ["scss", "css"],
script: ["ts"],
},
},
],
}
// ...
首先可以看到在"vue-loader"之后我们编写了一个对象,这个对象的loader参数是一个字符串,这个字符串是将来要被传递到require当中的,也就是说在webpack中他会自动帮我们把这个模块require即require("./vue-multiple-files-loader")。webpack loader是有优先级的,在这里我们的目标是首先经由vue-multiple-files-loader这个loader将代码处理之后再交与vue-loader进行处理,所以我们要将vue-multiple-files-loader写在vue-loader后边,这样就会首先使用vue-multiple-files-loader代码了。我们通过options这个对象传递参数,这个参数可以在loader中拿到。
关于webpack loader的优先级,首先定义loader配置的时候,除了loader与options选项,还有一个enforce选项,其可接受的参数分别是pre: 前置loader、normal: 普通loader、inline: 内联loader、post: 后置loader,其优先级也是pre > normal > inline > post,那么相同优先级的loader就是从右到左、从下到上,从上到下很好理解,至于从右到左,只是webpack选择了compose方式,而不是pipe的方式而已,在技术上实现从左往右也不会有难度,就是函数式编程中的两种组合方式而已。此外,我们在require的时候还可以跳过某些loader,!跳过normal loader、-!跳过pre和normal loader、!!跳过pre normal和post loader,比如require("!!raw!./script.coffee"),关于loader的跳过,webpack官方的建议是,除非从另一个loader处理生成的,一般不建议主动使用。
现在我们已经处理好vue-multiple-files-loader.js这个文件的创建以及loader的引用了,那么我们可以通过他来编写代码了,通常来说,loader一般是比较耗时的应用,所以我们通过异步来处理这个loader,通过this.async告诉loader-runner这个loader将会异步地回调,当我们处理完成之后,使用其返回值将处理后的字符串代码作为参数执行即可。
module.exports = async function (source) {
const done = this.async();
// do something
done(null, source);
}
对于文件的操作,我们使用promisify来处理,以便我们能够更好地使用async/await。
const fs = require("fs");
const { promisify } = require("util");
const readDir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
下面我们回到上边的需求上来,思路很简单,首先我们在这个loader中仅会收到以.vue结尾的文件,这是在webpack.config.js中配置的,所以我们在这里仅关注.vue文件,那么在这个文件下,我们需要获取这个文件所在的目录,然后将其遍历,通过webpack.config.js中配置的options来构建正则表达式去匹配同级目录下的script与style的相关文件,对于匹配成功的文件我们将其读取然后按照.vue文件的规则拼接到source中,然后将其返回之后将代码交与vue-loader处理即可。
那么我们首先处理一下当前目录,以及当前处理的文件名,还有正则表达式的构建,在这里我们传递了scss、css和ts,那么对于App.vue这个文件来说,将会构建/App\.vue\.css$|App\.vue\.scss$/和App\.vue\.ts$这两个正则表达式。
const filePath = this.context;
const fileName = this.resourcePath.replace(filePath + "/", "");
const options = loaderUtils.getOptions(this) || {};
const styleRegExp = new RegExp(options.style.map(it => `${fileName}\\.${it}$`).join("|"));
const scriptRegExp = new RegExp(options.script.map(it => `${fileName}\\.${it}$`).join("|"));
之后我们通过遍历目录的方式,来匹配符合要求的script和style的文件路径。
let stylePath = null;
let scriptPath = null;
const files = await readDir(filePath);
files.forEach(file => {
if (styleRegExp.test(file)) stylePath = path.join(filePath, file);
if (scriptRegExp.test(file)) scriptPath = path.join(filePath, file);
});
之后对于script部分,存在匹配节点且原.vue文件不存在script标签,则异步读取文件之后将代码进行拼接,如果拓展名不为js的话,例如是ts编写的那么就会将其作为lang="ts"去处理,之后将其拼接到source这个字符串中。
if (scriptPath && !/<script[\s\S]*?>/.test(source)) {
const extName = scriptPath.split(".").pop();
if (extName) {
const content = await readFile(scriptPath, "utf8");
const scriptTagContent = [
"<script ",
extName === "js" ? "" : `lang="${extName}" `,
">\n",
content,
"</script>",
].join("");
source = source + "\n" + scriptTagContent;
}
}
之后对于style部分,存在匹配节点且原.vue文件不存在style标签,则异步读取文件之后将代码进行拼接,如果拓展名不为css的话,例如是scss编写的那么就会将其作为lang="scss"去处理,如果代码中存在单行的// scoped字样的话,就会将这个style部分作scoped处理,之后将其拼接到source这个字符串中。
if (stylePath && !/<style[\s\S]*?>/.test(source)) {
const extName = stylePath.split(".").pop();
if (extName) {
const content = await readFile(stylePath, "utf8");
const scoped = /\/\/[\s]scoped[\n]/.test(content) ? true : false;
const styleTagContent = [
"<style ",
extName === "css" ? "" : `lang="${extName}" `,
scoped ? "scoped " : " ",
">\n",
content,
"</style>",
].join("");
source = source + "\n" + styleTagContent;
}
}
在之后使用done(null, source)触发回调完成loader的流程,相关代码如下所示,完整代码在https://github.com/WindrunnerMax/webpack-simple-environment中的webpack--loader分支当中。
const fs = require("fs");
const path = require("path");
const { promisify } = require("util");
const loaderUtils = require("loader-utils");
const readDir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
module.exports = async function (source) {
const done = this.async();
const filePath = this.context;
const fileName = this.resourcePath.replace(filePath + "/", "");
const options = loaderUtils.getOptions(this) || {};
const styleRegExp = new RegExp(options.style.map(it => `${fileName}\\.${it}$`).join("|"));
const scriptRegExp = new RegExp(options.script.map(it => `${fileName}\\.${it}$`).join("|"));
let stylePath = null;
let scriptPath = null;
const files = await readDir(filePath);
files.forEach(file => {
if (styleRegExp.test(file)) stylePath = path.join(filePath, file);
if (scriptRegExp.test(file)) scriptPath = path.join(filePath, file);
});
// 存在匹配节点且原`.vue`文件不存在`script`标签
if (scriptPath && !/<script[\s\S]*?>/.test(source)) {
const extName = scriptPath.split(".").pop();
if (extName) {
const content = await readFile(scriptPath, "utf8");
const scriptTagContent = [
"<script ",
extName === "js" ? "" : `lang="${extName}" `,
">\n",
content,
"</script>",
].join("");
source = source + "\n" + scriptTagContent;
}
}
// 存在匹配节点且原`.vue`文件不存在`style`标签
if (stylePath && !/<style[\s\S]*?>/.test(source)) {
const extName = stylePath.split(".").pop();
if (extName) {
const content = await readFile(stylePath, "utf8");
const scoped = /\/\/[\s]scoped[\n]/.test(content) ? true : false;
const styleTagContent = [
"<style ",
extName === "css" ? "" : `lang="${extName}" `,
scoped ? "scoped " : " ",
">\n",
content,
"</style>",
].join("");
source = source + "\n" + styleTagContent;
}
}
// console.log(stylePath, scriptPath, source);
done(null, source);
};
每日一题
https://github.com/WindrunnerMax/EveryDay
参考
https://webpack.js.org/api/loaders/
https://juejin.cn/post/6844904054393405453
https://segmentfault.com/a/1190000014685887
https://segmentfault.com/a/1190000021657031
https://webpack.js.org/concepts/loaders/#inline
http://t.zoukankan.com/hanshuai-p-11287231.html
https://v2.vuejs.org/v2/guide/single-file-components.html
初探webpack之编写loader的更多相关文章
- 初探webpack之编写plugin
初探webpack之编写plugin webpack通过plugin机制让其使用更加灵活,以适应各种应用场景,当然也大大增加了webpack的复杂性,在webpack运行的生命周期中会广播出许多事件, ...
- 初探webpack之从零搭建Vue开发环境
初探webpack之搭建Vue开发环境 平时我们可以用vue-cli很方便地搭建Vue的开发环境,vue-cli确实是个好东西,让我们不需要关心webpack等一些繁杂的配置,然后直接开始写业务代码, ...
- Webpack学习-Loader
什么是Loader? 继上两篇文章webpack工作原理介绍(上篇.下篇),我们了解到Loader:模块转换器,也就是将模块的内容按照需求装换成新内容,而且每个Loader的职责都是单一,只会完成一种 ...
- webpack CSS处理loader
loader概念: 首先来介绍一下loader,之前我们用webpack来处理我们写的js代码,并且webpack会自动处理js之间相关的依赖.但是,在开发中我们不仅仅有基本的js代码处理,我们也需要 ...
- webpack系列之loader的基本使用
可以访问 这里 查看更多关于大数据平台建设的原创文章. webpack系列之loader及简单的使用 一. loader有什么用 webpack本身只能打包Javascript文件,对于其他资源例如 ...
- webpack练手项目之easySlide(一):初探webpack (转)
最近在学习webpack,正好拿了之前做的一个小组件,图片轮播来做了下练手,让我们一起来初步感受下webpack的神奇魅力. webpack是一个前端的打包管理工具,大家可以前往:http:/ ...
- Vue系列之 => webpack的url loader
安装: npm i url-loader file-loader -D //url-loader内部依赖file-loader webpack.config.js const path = requ ...
- webpack配置常用loader加载器
webapck中使用loader的方法有三种 使用loader之前必须运行安装 : npm install --save-dev xxx-loader (1)通过CLI : 命令行中运行 webpac ...
- webpack 中,loader、plugin 的区别
loader 和 plugin 的主要区别: loader 用于加载某些资源文件. 因为 webpack 只能理解 JavaScript 和 JSON 文件,对于其他资源例如 css,图片,或者其他的 ...
随机推荐
- java中的修饰符和基本数据类型
1.java中的修饰符 java中的修饰符主要是用来对类资源进行一个权限控制,上面表格表现的很清晰,无需多言. 2.java中的基本数据类型 java中的数据类型分为引用类型和基本类型.基本数据类型有 ...
- composer安装报错
问题报错:Fatal error: Declaration of Fxp\Composer\AssetPlugin\Repository\AbstractAssetsRepository::searc ...
- spring重点知识分享
前言: spring是一个轻量级的开源的控制反转(Inversion of Control,IOC)和面向切面(AOP)的容器框架,它的主要目的是简化企业开发.这两个模块使得java开发更加简单.IO ...
- leedcode_13 罗马数字转整数
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M. 字符 数值I 1V 5X 10L 50C 100D 500M 1000例如, 罗马数字 2 写做 II ,即为两个并列的 1 .12 ...
- 大数据学习之路之ambari的安装
之前按照正常方式安装的hbase不能插入数据 所以今天来尝试下ambari能不能行 已经打了快照 如果不能还能恢复之前的样子
- 每日所学之自学习大数据的Linux环境配置2
今天设置网络 出现报错 明天找时间解决 不用解决了 刚才试了以下 又能下载了 描述一下问题: cannot find a valid baseurl for repo:base/7/x86_64 如果 ...
- zabbix自定义自动发现模板
需求: 自定义发现磁盘io,并实现监控.其他的业务组件自动发现监控其实也和这个大同小异,自动发现主要逻辑就是你要根据组件规则自动匹配出需要监控的所有组件,再通过传参的方式获取对应组件数据. 自动发现无 ...
- Spring基于注解自动装配
前面我们介绍Spring IoC装载的时候,使用XML配置这种方法来装配Bean,这种方法可以很直观的看到每个Bean的依赖,但缺点也很明显:写起来非常繁琐,每增加一个组件,就必须把新的Bean配置到 ...
- HTTP请求头格式和响应格式
HTTP请求头格式 提示: 回车符 \r 换行符 \n 请求首行分析: 请求方式: GET 和 POST 方式: GET请求:地址栏访问.超链接访问都是get请求方式,get请求方式不安全,地址栏大小 ...
- 安卓记账本开发学习day5之版本兼容问题
安卓5.0以上版本想要隐藏DatePicker头布局的写法比较复杂,需要一层一层隐藏 int headerId = getContext().getResources().getIdentifier( ...