此篇由浅入深介绍webpack如何自定义一个loader

相关文章
webpack 常用的 plugin 和 loader --- 面试题
webpack(3) - loader 文件加载器
b站视频讲解(磨刀不误砍柴工--前置知识)
b站视频讲解(实现babel-loader)
b站视频讲解(实现markdown-loader)


创建自己的Loader

思考

  1. Loader是什么?

Loader是用于对模块的源代码进行转换(处理),之前我们使用过许多Loader,比如css-loader、style-loader等......

现在我们来学习如何自定义自己的Loader(先了解基础):

  • Loader本质是一个导出为函数的JavaScript模块;
  • Loader runner库会调用这个函数,然后将上一个loader产生的结果或者资源文件传入进去;

创建 loader 文件夹

javascript 复制代码
mkdir loader

在创建三个文件在loader下:myLoader1,myLoader2,myLoader3 在 myLoader1, myLoader2, myLoader3 三个文件里分别做以打印(以便测试):

javascript 复制代码
module.exports = function(ctx, map, meta) {
  console.log('------'loader1', ctx)
  return ctx
}

配置 webpack.config.js

javascript 复制代码
const path = require('path')

module.exports = {
  mode: 'development',
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, './build'), // 打包进build文件夹
    filename: 'bundle.js' // 打包后的文件名
  },
  module:{
    rules:[
      {
        test: /\.js$/,
        use: ['./loader/myLoader1.js','./loader/myLoader2.js','./loader/myLoader3.js'] // 匹配这三个文件
      }
    ]
  }
}

打包(npx webpack),发现打包成功,当然你的index.js一定要存在

精简 rules配置

思考:每次都有写完整的路径不麻烦么?为什么官方Loader可以直接使用?比如css-loader。

javascript 复制代码
module: {
  rules: [
    {
      test: /\.js$/,
      use: ['myLoader1', 'myLoader2', 'myLoader3']
    }
  ]
}

进行打包(npx webpack)却发现出错了? 他意思找不到 myLoader3...... 所以我们需要了解loader是从哪里加载的?

resolveLoader 配置项用于告诉Webpack在哪里查找加载器(即本地加载器)。通常,Webpack会首先从 node_modules 目录中查找加载器,但有时你可能会有自定义的加载器,它们不在 node_modules 中,而在本地项目的某个目录下,这时就需要配置 resolveLoader 来告诉Webpack去哪里查找这些自定义加载器。 所以,resolveLoader 的作用是为了确保Webpack可以找到本地项目中的加载器。在你的配置中,它告诉Webpack首先从 node_modules 目录查找加载器,然后从 ./loader 目录查找加载器。这样,你可以将自定义加载器放在 ./loader 目录中,并在配置中引用它们,而Webpack会正确地查找并加载这些加载器。

现在使我们完整的webpack.config.js配置:🔽

javascript 复制代码
const path = require('path')
module.exports = {
  mode: 'production',
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, './build'),
    filename: 'bundle.js'
  },
  resolveLoader:{
    modules: ['node_modules', './loader'] // 这里是主要配置,去找loader文件夹的loader
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['myLoader1', 'myLoader2', 'myLoader3']
      }
    ]
  }
}

再次打包(npx webpack):

加载顺序为什么是倒序?

我们看到我们的打印信息loader3是先打印的,但loader1是先引入的

Webpack 的加载器(loader)加载顺序实际上是从右向左(从尾部到头部)的,这与常规的代码执行顺序相反。这意味着在加载器数组中,最后一个加载器会首先执行,然后向左依次执行前面的加载器。

执行顺序和enforce

loader的执行顺序是相反的:

  • run-loader先优先执行PitchLoader,在执行PitchLoader时候进行loaderIndex++;
  • run-loader之后回执行NormalLoader,在执行NormalLoader时进行loaderIndex--;

我现在在每个loader文件里加一个pitchLoader测试(高亮打印),并执行(npx webpack)

javascript 复制代码
module.exports = function(ctx, map, meta) {
  console.log('------loader1', ctx)
  return ctx
}
module.exports.pitch = function() {
  console.log('=====> loader1')
}
// loader2、loader3同上

哦,明白了!先从左到右执行pitchLoader,最后index跑在了最后,normalLoader重新开始从右向左执行回来。


那么,能不能改变他们的执行顺序呢?

  • 可以拆分为多个Rule对象,通过enforce来改变他们的顺序;

enfore一共有四种方式:

  • 默认所有的 loader 都是 normal;
  • 在行内设置是 inline
javascript 复制代码
import'./styles.css'; 
// @inline css-loader!style-loader!./styles.css
  • 通过 enforce 设置 pre 和 post
javascript 复制代码
rules: [
  {
    test: /\.js$/,
    use: 'myLoader1'
  },
  {
    test: /\.js$/,
    use: 'myLoader2',
    enforce: 'pre' // 设置提前
  },
  {
    test: /\.js$/,
    use: 'myLoader3'
  }
]

loader2先加载了! 在pitching和normal他们的执行顺序是:

  • post,inline,normal,pre;
  • pre,normal,inline,post

同步的loader

我在loader文件里使用setTimeout模拟延时

myLoader3.js

javascript 复制代码
module.exports = function (ctx) {
  setTimeout(() => {
    console.log('------loader3', ctx)
    return ctx + 'xxxx'
  }, 2000);
}

我们根本拿不到loader3传来的值,因为loader3还没有执行就已经执行好了loader2和loader1. 随后2s后,loader3才执行,这就是同步loader。

异步的loader

用async开启异步loader,打包查看结果:

myLoader3.js

javascript 复制代码
module.exports = function (ctx) {
  // const callback = this.callback
  const callback = this.async() // async是异步的函数
  console.log('------loader3', ctx)
  setTimeout(() => {
    callback(null, ctx + 'xxxx')
  }, 2000);
}

myLoader2.js

javascript 复制代码
module.exports = function(ctx, map, meta) {
  const callback = this.async()
  console.log('-----loader2', ctx)
  setTimeout(() => {
    callback(null, ctx + 'yyyy')
  }, 3000);
}

myLoader1.js

javascript 复制代码
module.exports = function(ctx, map, meta) {
  console.log('------loader1', ctx)
  return ctx
}

结果过程:

  1. 先打印loader3
  2. 隔两秒打印loader2
  3. 隔三秒打印loader1

实现了异步过程

给loader传入参数

新建一个myLoader4,用来传递参数:

webpack.config.js

javascript 复制代码
rules:[
  {
    loader: 'myLoader4',
    options: {
      name: 'colin',
      age: '21'
    }
  }
]

myLoader4.js

javascript 复制代码
module.exports = function(ctx) {
  const args = this.getOptions() // getOptions获取传递的参数
  console.log(args)
  console.log('-------myLoader4', ctx)

  return ctx
}

可以看到我们正确拿到了参数,就可以自定义操作啦

校验参数

  • 我们可以通过一个webpack官方提供的校验库 schema-utils;
javascript 复制代码
npm i schema-utils -D
  • 创建一个schema文件夹,并在里面创建一个loader4的校验文件,格式为 .json

进行字段类型的配置:

javascript 复制代码
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "name必须为字符串类型"
    },
    "age": {
      "type": "number",
      "description": "age必须为数字类型"
    }
  }
}

myLoader4.js文件

javascript 复制代码
const { validate } = require('schema-utils') // 引入
const loader4Schema = require('./schema/loader4_schema.json') // 引入

module.exports = function(ctx) {
  const args = this.getOptions()
  console.log(args)
  console.log('-------myLoader4', ctx)

  /* 校验格式 */
  validate(loader4Schema, args)
  return ctx
}

如图效果:

实现一个 babel-loader

babel-loader

  • 将新的JavaScript代码编译为老的JavaScript代码

编写一个箭头函数

javascript 复制代码
console.log('src/index.js')

const x = () => {
  console.log('---x')
}
x()

打包-> 可以看到并没有解析我们的代码(箭头函数Es6)?

创建自己的babel-loader

webpack.config.js 配置

javascript 复制代码
module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
    ]
  }

babel-loader.js

javascript 复制代码
module.exports = function (ctx) {
  return ctx
}

了解babel

ES6代码是如何转换为ES5的?

javascript 复制代码
词法分析 -> 语法分析 -> AST -> 操作

注:这是babel的操作 我们这里可以引用babel来实现babel-loader 并非自己实现一个babel

安装@babel/core第三方库

javascript 复制代码
npm i @babel/core -D   

使用 babel-loader.js

javascript 复制代码
const babel = require('@babel/core') // 引入@babel/core

module.exports = function (ctx) {

  // 使用异步,防止拿到数据不为同步
  const callback = this.async()
  // 使用transform进行代码转换
  babel.transform(ctx, {}, (err, result) => {
    if(err){
      callback(err)
    }else{
      callback(null, result.code) // 结果在 result.code 里
    }
  })

  // return ctx
}

没有这一步,你还真不行~

先安装@babel/plugin-transform-arrow-functions (支持箭头函数的插件)

javascript 复制代码
npm i @babel/plugin-transform-arrow-functions -D

webpack.config.js

javascript 复制代码
module: {
    rules: [
      {
        test: /\.jsx$/,
        use: {
          loader: 'babel-loader',
          options:{
            plugins: [
              "@babel/plugin-transform-arrow-functions"
            ]
          }
        }
      }
    ]
  }

babel-loader.js 里增加options配置

javascript 复制代码
const babel = require('@babel/core')

module.exports = function (ctx) {
  const options = this.getOptions() // 拿到options 放进transform第二个参数里
  const callback = this.async()
  babel.transform(ctx, options, (err, result) => {
    if(err){
      callback(err)
    }else{
      callback(null, result.code)
    }
  })

  // return ctx
}

打包查看结果发现箭头函数已经处理为普通函数->

用 @babel/preset-env 代替 plugin-transform-arrow-functions等等

同样,先进行安装@babel/preset-env

javascript 复制代码
npm i @babel/preset-env -D

这个时候我们就不需要传递plugins了,而是presets

javascript 复制代码
module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options:{
            // plugins: [
            //   "@babel/plugin-transform-arrow-functions"
            // ]
            presets: [
              "@babel/preset-env"
            ]
          }
        }
      }
    ]
  }

引入const来测试present 的替代性如何? babel-loader.js文件新增代码

javascript 复制代码
console.log('src/index.js')
const name = 'colin' // 新增const,试测试转为var

const x = () => {
  console.log('---x')
}
x()

打包结果如下-> 可以看到我们的const、箭头函数统统进行了代码转换

动态引入@babel/preset-env

书写babel-loader.js,动态require一个写好的babel.config.js文件作为options

javascript 复制代码
const babel = require('@babel/core') // 引入@babel/core

module.exports = function (ctx) {
  let options = this.getOptions() // 拿到options 放进transform第二个参数里
  if (!Object.keys(options).length) {
    options = require('../babel.config') // 引入options
  }
  console.log(options) // 打印测试

  // 使用异步,防止拿到数据不为同步
  const callback = this.async()
  // 使用transform进行代码转换
  babel.transform(ctx, options, (err, result) => {
    if (err) {
      callback(err)
    } else {
      callback(null, result.code) // 结果在 result.code 里
    }
  })
}

本地根目录创建一个babel.config.js文件并引入@babel/preset-env

javascript 复制代码
module.exports = {
  presets: ["@babel/preset-env"]
}

那么这个时候就可以删除掉webpack.config.js文件的引入了

javascript 复制代码
module: {
  rules: [
    {
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        // options:{
        // plugins: [
        //   "@babel/plugin-transform-arrow-functions"
        // ]
        // presets: [
        //   "@babel/preset-env"
        // ]
        // } 
      }
    }
  ]
}

实现一个markdown的Loader

markdown展示

我们webpack是无法打包.md文件的。在这个过程中会进行报错,如:

因为我们在index.js文件里引入了index.md文件:

javascript 复制代码
import './index.md' // 这里引入一个markdown的文件
console.log('src/index.js')
const name = 'colin'

const x = () => {
  console.log('---x')
}
x()

index.md文件:

markdown 复制代码
# webpack study

## loader
+ 这里是md-loader

```js
const x = {
	name: 'ckj' 
}
lua 复制代码
#### 创建md-loader,并初步配置
> md-loader.js

```javascript
module.exports = function(ctx) {
  return ctx
}

webpack.config.js

javascript 复制代码
{
  test: /\.md$/,
  use: 'md-loader'
}

了解marked并使用

先下载marked

markdown 复制代码
npm i marked -D

在md-loader.js文件里使用

javascript 复制代码
const {marked} = require('marked')// 引用
module.exports = function(ctx) {

  // 转换
  const content = marked(ctx)
  console.log(content)
  return content
}

发现已经处理好我们的md文件特殊语法了! 但打包还是没有成功......

返回的结果必须是模块化的内容

所以,我们需要将我们生成的html内容插进JavaScript里,并导出

javascript 复制代码
const {marked} = require('marked')
module.exports = function(ctx) {

  // 转换
  const content = marked(ctx)

  // 返回的结果必须是模块化的内容
  const innerContent = "`" + content + "`"
  const moduleContent = `var code = ${innerContent}; export default code;`
  return moduleContent
}

index.js文件引入index.md

javascript 复制代码
import code from './index.md'
console.log(code)

console.log('src/index.js')
const name = 'colin'

const x = () => {
  console.log('---x')
}
x()

所以整个流程就是: 先配对md文件 -> 告诉他使用md-loader -> md-loader去转换代码为html -> html转换为模版导入 -> 最终打包

这个时候我们可以去看打包好的代码里面我们转换后的代码 当然我们需要在webpack.config.js文件里将devtools设置为false(为了看的清晰)

javascript 复制代码
module.exports = {
  mode: 'development',
  devtool: false,
  entry: "./src/index.js",
}

bundle.js源码里的代码:

使用HtmlWebpackPLugin来查看结果

先安装

javascript 复制代码
npm i html-webpack-plugin -D

webpack.config.js 导入使用

javascript 复制代码
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 引用

module.exports = {
  mode: 'development',
  devtool: false,
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, './build'),
    filename: 'bundle.js'
  },
  resolveLoader: {
    modules: ['node_modules', './loader']
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
        }
      },
      {
        test: /\.md$/,
        use: 'md-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin() // 注册使用
  ]
}

index.js插入code

javascript 复制代码
import code from './index.md'

console.log('src/index.js')
const name = 'colin'

const x = () => {
  console.log('---x')
}
x()
document.querySelector('body').insertAdjacentHTML('beforeend', code) // 插入到body上

// 或者 document.body.innerHTML = code

这个时候打包,会自动生成一个html文件,我们启动html文件预览,控制台也能打印出我们经过转换后的md文件源代码

改变样式

安装css-loader和style-loader

javascript 复制代码
npm i css-loader style-loader -D

webpack.config.js配置loader

javascript 复制代码
 module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
        }
      },
      {
        test: /\.md$/,
        use: 'md-loader'
      },
      {
        test: /\.css$/,
        use: ['style-loader','css-loader'] // 先引入style-loader
      }
    ]
  },

index.css编写代码块样式,并引入index.js

javascript 复制代码
pre{
  background: #eff0f0;
  padding: 1rem;
  border-radius: 8px;
}

打包->

第三方库-高亮代码块!

这里下载需要助理版本!!

javascript 复制代码
npm i marked@7.0.5 -D // 退版本
npm i highlight.js // 高亮js包

md-loader.js引入highlight并配置

javascript 复制代码
const {marked} = require('marked')
const hljs = require('highlight.js')
module.exports = function(ctx) {
  // marked解析前 设置高亮标识
  
  marked.setOptions({
    highlight: function(code, lang){  // lang是语言,code是代码块
      return hljs.highlight(lang, code).value
    }
  })

  // 转换
  const content = marked(ctx)

  // 返回的结果必须是模块化的内容
  const innerContent = "`" + content + "`"
  const moduleContent = `var code = ${innerContent}; export default code;`
  return moduleContent
}

打包看调试工具-> 可以看到分成了几个模块,那我们就可以拿到选择器做css了?不做过多解释咯~

或者!在index.css文件里引入highlight库带给我们的默认样式。其实库内还有需要样式

javascript 复制代码
import 'highlight.js/styles/default.css'

比如这个好看的样式就是我们github的样式

javascript 复制代码
import 'highlight.js/styles/github.css' // 即可
相关推荐
余生H7 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍10 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai14 分钟前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默26 分钟前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_8572979136 分钟前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
Ink1 小时前
从底层看 path.resolve 实现
前端·node.js
金灰1 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
茶卡盐佑星_1 小时前
说说你对es6中promise的理解?
前端·ecmascript·es6
Promise5201 小时前
总结汇总小工具
前端·javascript