手把手带你撸一遍vue-loader源码

本文将带你一步步解析vue-loader的源码,从Vue Loader的介绍到其在webpack中的工作原理,包括SFC(单文件组件)的处理过程,以及模块热载的原理。通过实际项目的例子,详细阐述vue-loader如何将.vue文件转化为可执行的组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

前面写过两篇webpack实战的文章:

强烈建议小伙伴们去看一下前面几个章节的内容,

这一节我们研究一下vue-loader

介绍

Vue Loader 是什么?

Vue Loader 是一个 webpack 的 loader,它允许你以一种名为单文件组件 (SFCs)的格式撰写 Vue 组件:

<template>
  <div class="example">{
   { msg }}</div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>

<style>
.example {
  color: red;
}
</style>

Vue Loader 还提供了很多酷炫的特性:

  • 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 `` 的部分使用 Sass 和在 ` 的部分使用 Pug;
  • 允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
  • 使用 webpack loader 将 `` 和 ` 中引用的资源当作模块依赖来处理;
  • 为每个组件模拟出 scoped CSS;
  • 在开发过程中使用热重载来保持状态。

简而言之,webpack 和 Vue Loader 的结合为你提供了一个现代、灵活且极其强大的前端工作流,来帮助撰写 Vue.js 应用。


以上内容都是vue-loader官网的内容,基础用法大家可以自己去看vue-loader的官网,我就不在这里详细介绍了。

开始

我们还是用之前章节的webpack-vue-demo项目做测试demo,大家可以直接clone。

我们首先看一下demo项目的入口文件src/main.ts:

import Vue from "vue";
// import App from "./app.vue";
import App from "./app.vue";

new Vue({
  el: "#app",
  render: h => h(App)
});

可以看到,直接引用了一个app.vue组件,

src/app.vue:

<template>
  <div class="app-container">{
  { msg }}</div>
</template>

<script lang="ts">
import { Vue, Component } from "vue-property-decorator";

@Component
export default class App extends Vue {
  msg = "hello world";
  user = {
    name: "yasin"
  };
  created(): void {
    // const name = this.user?.name;
    // console.log("name");
  }
}
</script>

<style scoped lang="scss">
.app-container {
  color: red;
}
</style>

代码很简单,就是一个普通的vue组件,然后输出一个“hello world”,大家可以直接在项目根目录执行"npm run dev"命令来运行项目。

ok,然后看一下webpack的配置文件webpack.config.js:

const path = require("path");
const config = new (require("webpack-chain"))();
const isDev = !!process.env.WEBPACK_DEV_SERVER;
config
    .context(path.resolve(__dirname, ".")) //webpack上下文目录为项目根目录
    .entry("app") //入口文件名称为app
        .add("./src/main.ts") //入口文件为./src/main.ts
        .end()
    .output
        .path(path.join(__dirname,"./dist")) //webpack输出的目录为根目录的dist目录
        .filename("[name].[hash:8].js")
        .end()
    .resolve
        .extensions
            .add(".js").add(".jsx").add(".ts").add(".tsx").add(".vue") //配置以.js等结尾的文件当模块使用的时候都可以省略后缀
            .end()
        .end()
    .module
        .rule('js')
            .test(/\.m?jsx?$/) //对mjs、mjsx、js、jsx文件进行babel配置
            .exclude
                .add(filepath => {
   
                    // Don't transpile node_modules
                    return /node_modules/.test(filepath)
                })
                .end()
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .end()
        .rule("type-script")
            .test(/\.tsx?$/) //loader加载的条件是ts或tsx后缀的文件
            .use("babel-loader")
                .loader("babel-loader")
                .end()
            .use("ts-loader")
                .loader("ts-loader")
                .options({
    //ts-loader相关配置
                    transpileOnly: true,  // disable type checker - we will use it in fork plugin
                    appendTsSuffixTo: ['\\.vue$']
                })
                .end()
            .end()
        .rule("vue")
            .test(/\.vue$/)// 匹配.vue文件
                .use("vue-loader")
                .loader("vue-loader")
                .end()
            .end()
        .rule("sass")
            .test( /\.(sass|scss)$/)//sass和scss文件
            .use("extract-loader")//提取css样式到单独css文件
                .loader(require('mini-css-extract-plugin').loader)
                .options({
   
                    hmr: isDev //开发环境开启热载
                })
                .end()
            .use("css-loader")//加载css模块
                .loader("css-loader")
                .end()
            .use("postcss-loader")//处理css样式
                .loader("postcss-loader")
                .options( {
   
                    config: {
   
                       path: path.resolve(__dirname, "./postcss.config.js")
                    }
                })
                .end()
            .use("sass-loader")//sass语法转css语法
                .loader("sass-loader")
                .end()
            .end()
        .rule('eslint')//添加eslint-loader
            .exclude
                .add(/node_modules/)//校验的文件除node_modules以外
                .end()
            .test(/\.(vue|(j|t)sx?)$/)//加载.vue、.js、.jsx、.ts、.tsx文件
                .use('eslint-loader')
                .loader(require.resolve('eslint-loader'))
                .options({
   
                    emitWarning: true, //把eslint报错当成webpack警告
                    emitError: !isDev, //把eslint报错当成webapck的错误
                })
                .end()
            .end()
         .end()
    .plugin("vue-loader-plugin")//vue-loader必须要添加vue-loader-plugin
        .use(require("vue-loader").VueLoaderPlugin,[])
        .end()
    .plugin("html")// 添加html-webpack-plugin插件
        .use(require("html-webpack-plugin"),[{
   
            template: path.resolve(__dirname,"./public/index.html"), //指定模版文件
            chunks: ["runtime", "chunk-vendors", "chunk-common", "app"], //指定需要加载的chunks
            inject: "body" //指定script脚本注入的位置为body
        }])
        .end()
    .plugin("extract-css")//提取css样式到单独css文件
        .use(require('mini-css-extract-plugin'), [{
   
            filename: "css/[name].css",
            chunkFilename: "css/[name].css"
        }])
        .end()
    .plugin('fork-ts-checker') //配置fork-ts-checker
        .use(require('fork-ts-checker-webpack-plugin'), [{
   
            eslint: {
   
                files: './src/**/*.{ts,tsx,js,jsx,vue}' // required - same as command `eslint ./src/**/*.{ts,tsx,js,jsx} --ext .ts,.tsx,.js,.jsx`
            },
            typescript: {
   
                extensions: {
   
                    vue: {
   
                        enabled: true,
                        compiler: "vue-template-compiler"
                    },
                }
            }
        }])
        .end()
    .devServer
        .host("0.0.0.0") //为了让外部服务访问
        .port(8090) //当前端口号
        .hot(true) //热载
        .open(true) //开启页面
        .overlay({
   
            warnings: false,
            errors: true
        }) //webpack错误和警告信息显示到页面
config.when(!isDev,()=>{
   
    config.optimization
            .minimize(true)
            .minimizer("terser")
            .use(require("terser-webpack-plugin"),[{
   
                extractComments: false, //去除注释
                terserOptions:{
   
                    output: {
   
                        comments: false //去除注释
                    }
                }
            }]);
},()=>{
   
    config.devtool("eval-cheap-module-source-map");
});
config.optimization
            .splitChunks({
   
                cacheGroups: {
   
                    vendors: {
    //分离入口文件引用node_modules的module(vue、@babel/xxx)
                        name: `chunk-vendors`,
                        test: /[\\/]node_modules[\\/]/,
                        priority: -10,
                        chunks: 'initial'
                    },
                    common: {
    //分离入口文件引用次数>=2的module
                        name: `chunk-common`,
                        minChunks: 2,
                        priority: -20,
                        chunks: 'initial',
                        reuseExistingChunk: true
                    }
                }
            })
            .runtimeChunk("single"); //分离webpack的一些帮助函数,比如webpackJSONP等等

module.exports = config.toConfig();

每一个配置项前面章节都有详细介绍的,我就不一一解析了,我们继续往下。

SFC

我们直接打开src/app.vue文件:

<template>
  <div class="app-container">{
  { msg }}</div>
</template>

<script lang="ts">
import { Vue, Component } from "vue-property-decorator";

@Component
export default class App extends Vue {
  msg = "hello world";
  user = {
    name: "yasin"
  };
  created(): void {
    // const name = this.user?.name;
    // console.log("name");
  }
}
</script>

<style scoped lang="scss">
.app-container {
  color: red;
}
</style>

ok! 那么webpack是怎么加载我们的“.vue”结尾的文件呢?

因为我们在webpack.config.js中配置了loader,也就是说是我们的loader去加载的".vue"文件,那么我们项目中的".vue"文件到底被哪些loader加载了呢?

我们直接利用IDE断点看看,我们直接定位到webpack的“NormalModule”,然后当webpack加载到app.vue模块的时候,

node_modules/webpack/lib/NormalModule.js:

在这里插入图片描述

ok! 可以看到,当webpack在加载app.vue模块的时候,webpack使用的loader有(loader执行顺序为从下往上):

  1. vue-loader
  2. eslint-loader

我直接知道vue-loader的源码,我这里的版本是“[email protected]”,

node_modules/vue-loader/lib/index.js:

const path = require('path')
const hash = require('hash-sum')
const qs = require('querystring')
const plugin = require('./plugin')
const selectBlock = require('./select')
const loaderUtils = require('loader-utils')
const {
    attrsToQuery } = require('./codegen/utils')
const {
    parse } = require('@vue/component-compiler-utils')
const genStylesCode = require('./codegen/styleInjection')
const {
    genHotReloadCode } = require('./codegen/hotReload')
const genCustomBlocksCode = require('./codegen/customBlocks')
const componentNormalizerPath = require.resolve('./runtime/componentNormalizer')
const {
    NS } = require('./plugin')

let errorEmitted = false

function loadTemplateCompiler (loaderContext) {
   
  try {
   
    return require('vue-template-compiler')
  } catch 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值