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
}
复制代码