Webpack 基础知识点
Webpack 三大件
"devDependencies": {
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.7.2"
}
核心概念
- mode
- entry
- output
- loader
- plugin
- devServer
Loader
什么是Loader
- loader 用于对模块的源代码进行转换:把源模块转换成通用模块。
- loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
注意,loader 能够
import
导入任何类型的模块(例如.css
文件),这是 webpack 特有的功能,其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是有很必要的,因为这可以使开发人员创建出更准确的依赖关系图。
- webpack loader的顺序是
从下到上
,从右到左
。 当链式调用多个 loader 的时候,请记住它们会以相反的顺序
执行。取决于数组写法格式,从右向左
或者从下向上
执行。
在 webpack.config.js 中的配置
在更高层面,在 webpack
的配置中 loader
有两个目标:
test
属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。use
属性,表示进行转换时,应该使用哪个 loader。 webpack.config.js:
const path = require('path');
const config = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
}
};
module.exports = config;
以上配置中,对一个单独的 module 对象定义了 rules
属性,里面包含两个必须属性:test
和 use
。这告诉 webpack 编译器(compiler) 如下信息:
“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先使用 raw-loader 转换一下。”
简单用法
🌟🌟 只能传入一个参数
当一个 loader 在资源中使用,这个 loader 只能传入一个参数
- 这个参数是一个包含资源文件内容的字符串
。
🌟🌟 返回值
同步 loader 可以简单的返回一个代表模块转化后的值。
在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...)
函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么扔进同步 loader 中。
🌟🌟 loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串
或者 buffer
。第二个参数值是 SourceMap,它是个 JavaScript 对象。
复杂用法
当链式调用
多个 loader 的时候,请记住它们会以相反的顺序
执行。取决于数组写法格式,从右向左
或者从下向上
执行。
- 最后的 loader 最早调用,将会传入原始资源内容。
- 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。
- 中间的 loader 执行时,会传入前一个 loader 传出的结果。
所以,在接下来的例子,foo-loader 被传入原始资源,bar-loader 将接收 foo-loader 的产出,返回最终转化后的模块和一个 source map(可选)
webpack.config.js
{
test: /\.js/,
use: [
'bar-loader',
'foo-loader'
]
}
用法准则
编写 loader 时应该遵循以下准则。它们按重要程度排序,有些仅适用于某些场景。
- 简单易用
每个 loader 只做单一任务。
- 使用链式传递。
loader 可以被链式调用意味着不一定要输出 JavaScript。只要下一个 loader 可以处理这个输出,这个 loader 就可以返回任意类型的模块。
- 模块化的输出。
保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。
-
确保无状态。
-
使用 loader utilities。
充分利用 loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项。schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验。
loader.js
import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';
const schema = {
type: 'object',
properties: {
test: {
type: 'string'
}
}
}
export default function(source) {
const options = getOptions(this);
validateOptions(schema, options, 'Example Loader');
// 对资源应用一些转换……
return `export default ${ JSON.stringify(source) }`;
};
-
记录 loader 的依赖。
-
解析模块依赖关系。
-
提取通用代码。
不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。loader-utils 中的 stringifyRequest 方法,可以将绝对路径转化为相对路径。
- 避免绝对路径。
- 使用 peer dependencies。
手写 模板编译 tpl-loader
package.json
按照这里的版本安装依赖,否则会因为版本问题报错。
{
"name": "tpl-loader-creator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.14.0",
"babel-loader": "^8.2.2",
"html-webpack-plugin": "^4.5.0",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.7.2"
}
}
webpack.config.js
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 是一个构造函数
module.exports = {
mode: 'development',
entry: resolve(__dirname, 'src/app.js'),
output: {
path: resolve(__dirname, 'build'),
filename: 'app.js'
},
devtool: 'source-map',
module: {
rules: [
// 模块规则(配置 loader、解析器等选项)
{
test: /\.tpl$/,
// 这里是匹配条件,每个选项都接收一个正则表达式或字符串
// test 和 include 具有相同的作用,都是必须匹配选项
// exclude 是必不匹配选项(优先于 test 和 include)
// 最佳实践:
// - 只在 test 和 文件名匹配 中使用正则表达式
// - 在 include 和 exclude 中使用绝对路径数组
// - 尽量避免 exclude,更倾向于使用 include
use: [
// 应用多个 loader 和选项
// loader的顺序是 `从下到上`,`从右到左`。
'babel-loader',
{
loader: './loaders/tpl-loader',
options: {
log: true
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: resolve(__dirname, 'index.html')
})
],
devServer: {
port: '3333'
}
}
info.tpl
<div>
<h1>{{ name }}</h1>
<p>{{ age }}</p>
<p>{{ career }}</p>
<p>{{ hobby }}</p>
</div>
app.js
import tpl from './info.tpl'
const oApp = document.querySelector('#app')
const info = tpl({
name: '小朝',
age: 18,
career: '前端开发工程师',
hobby: '美食'
})
oApp.innerHTML = info
tpl-loader/index.js
const { tplReplace } = require('../util')
const { getOptions } = require('loader-utils')
function tplLoader(source) {
source = source.replace(/\s+/g, '')
const { log } = getOptions(this)
const _log = log
? `console.log('compiled the file which is from ${this.resourcePath}')`
: ''
/**
* source
* <div><h1>{{name}}</h1><p>{{age}}</p><p>{{career}}</p><p>{{hobby}}</p></div>
*/
/**
* options
* {name: "小朝", age: 18, career: "前端开发工程师", hobby: "美食"}
*/
return `
export default options => {
// 需要被 babel-loader 转成 js程序
${tplReplace.toString()}
${_log.toString()}
console.log('*******')
console.log('${source}')
console.log(options)
return tplReplace('${source}', options)
}
`
}
module.exports = tplLoader
🌟🌟 loader 返回的结果
注意:如果是处理顺序排在最后一个的 loader,那么它的返回值将最终交给 webpack 的 require,换句话说,它一定是一段可执行的 JS 脚本 (用
字符串
来存储),更准确来说,是一个 node 模块的 JS 脚本
// 处理顺序排在最后的 loader
module.exports = function (source) {
// 这个 loader 的功能是把源模块转化为字符串交给 require 的调用方
return `module.exports = ${JSON.stringify(source)}`
}
util.js:
function tplReplace (template, replaceObject) {
return template.replace(/\{\{(.*?)\}\}/g, function(node, key) {
return replaceObject[key]
})
}
module.exports = {
tplReplace
}