相关文章
webpack 常用的 plugin 和 loader --- 面试题
webpack(3) - loader 文件加载器
b站视频讲解(磨刀不误砍柴工--前置知识)
b站视频讲解(实现babel-loader)
b站视频讲解(实现markdown-loader)
创建自己的Loader
思考
- 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
}
结果过程:
- 先打印loader3
- 隔两秒打印loader2
- 隔三秒打印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
}
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' // 即可