前言
前面写过两篇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执行顺序为从下往上):
- vue-loader
- 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