增加loader,less-loader、style-loader
本节来给自己写的webpack编写一下loader
- 实现less-loader:将less语法转换为css
- 实现style-loader:将css链接到html的header里
编写loader
准备工作
安装第三方包less,好把less转换为css
shell
yarn add less@^3.9.0 -D
修改配置文件
js
// webpack\webpack-dev-3\webpack.config.js
let path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.less$/,
// 引入自己写的loader
use: [
path.resolve(__dirname, 'loader', 'style-loader'),
path.resolve(__dirname, 'loader', 'less-loader')
]
}
]
}
}
新建index.less
js
// webpack\webpack-dev-3\src\index.less
body {
background-color: green;
}
引入index.less
js
// webpack\webpack-dev-3\src\index.js
let str = require('./a.js')
require('./index.less')
console.log(str); // aaabbb
编写less-loader
js
// webpack\webpack-dev-3\loader\less-loader.js
let less = require('less')
// less-loader
function loader(source) {
let css = '' // 转换后的css
// 虽然是以回调的方式写的,其实是同步执行的
less.render(source, (err, c) => {
css = c.css
})
// webpack中是使用eval执行的代码片段
// 编写less中的 \n 应该替换成 \\n
// 否则就会出错
css = css.replace(/\n/g, '\\n')
return css
}
module.exports = loader
编写style-loader
js
// webpack\webpack-dev-3\loader\style-loader.js
// style-loader
function loader(source) {
let style = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source)}
document.head.appendChild(style)
`
return style
}
module.exports = loader
修改自己写的webpack中的代码
加载自定义loader
咱们自己写好了loader,自己的webpack中应该识别一下,当遇到这些文件时,用自己写的loader去转换文件
js
// webpack\cheny-pack\lib\Complier.js
let path = require('path')
let fs = require('fs')
let babylon = require('babylon') // 把源码转换为ast语法树
let t = require('@babel/types') // 生成ast节点,可以生成各种类型
let traverse = require('@babel/traverse').default // 遍历ast语法树,替换节点
let generator = require('@babel/generator').default // 根据ast语法树,生成源代码
let ejs = require('ejs') // 模板引擎
class Complier {
constructor(config) {
// 将配置文件挂载到实例上,所有实例都能拿到了
// webpack.config.js
this.config = config
// 1. 保存入口文件的路径
this.entryId; // './src/index.js'
// 2. 需要保存所有模块的依赖
// 解析文件的依赖,变成webpack打包传递的参数,key value的形式
// key 是路径名,value就是模块代码
this.modules = {}
this.entry = config.entry // 入口路径
this.root = process.cwd() // 工作路径 运行 npx cheny-pack 时候的路径
}
run() {
// 执行,并且创建模块的依赖关系
// 从入口开始执行
this.buildModule(path.resolve(this.root, this.entry), true) // true 表示解析的是主模块
console.log(this.modules, this.entryId);
// 发射一个文件 打包后的文件
this.emitFile()
}
// 创建模块的依赖关系
buildModule(modulePath, isEntry) {
// 模块的key是相对路径 value是模块中的代码,不过需要做一些替换
// require都变成了webpack_require等
let source = this.getSource(modulePath) // 模块内容
// 模块id是一个相对路径 总路径 减去 rootPath
// 总路径modulePath F:\code\note_code\webpack\webpack-dev-3\src\index.js
// 工作路径this.root F:\code\note_code\webpack\webpack-dev-3
let moduleName = './' + path.relative(this.root, modulePath) // ./src/index.js 模块id
if (isEntry) { // 如果是主入口的话,记录一下主入口的id
this.entryId = moduleName // 保存入口的名字
}
// path.dirname(moduleName)获取父路径 ./src/index.js -> ./src
// 解析模块代码,替换相应的内容
let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode
// 在这里需要递归继续解析依赖项
dependencies.forEach(dep => { // 加载附模块
// 参数是一个绝对路径
this.buildModule(path.join(this.root, dep), false) // false 表示不是主入口文件
})
}
// 解析文件
// 需要把source源码进行改造 返回一个依赖列表
// 因为每一个模块里面还可能会引另外的模块,所以需要一个依赖列表
parse(source, parentPath) { // AST 解析语法树
let ast = babylon.parse(source) // 源码先解析成ast语法树
// https://astexplorer.net/
let dependencies = [] // 存放当源代码依赖的数组,即当前文件引入的第三方模块
// 遍历ast语法树
traverse(ast, {
CallExpression(p) { // 调用表达式,比如 a() require() ...
let node = p.node // 拿到对应的节点
// 如果是require 就改为 __webpack_require__
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
let moduleName = node.arguments[0].value // 取到的就是模块的引用名字 let a = require('./a') -> a
// 有时候我们引用的时候,没有带后缀,我们先把后缀加上
// path.extname('./a') === '' path.extname('./a.js') === '.js'
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js
// webpack的模板所有的路径都需要加上 ./src 所以需要把 ./a.js 变成 ./src/a.js
moduleName = './' + path.join(parentPath, moduleName) // ./src\a.js
// 把依赖的第三方模块 塞进依赖数组中
dependencies.push(moduleName)
// 替换原来的ast节点
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
// 生成新的代码
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
// 发射文件
emitFile() {
// 用转换后的数据,渲染模板
// 拿到输出路径
let main = path.join(this.config.output.path, this.config.output.filename)
// 获取到模板资源
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
// 根据处理过的模块 渲染新代码
let code = ejs.render(templateStr, {
entryId: this.entryId,
modules: this.modules
})
// 如果是多入口打包,所以声明一个assets
this.assets = {}
this.assets[main] = code
console.log(main);
// 生成打包结果
this.writeFile(main, this.assets[main])
}
// 生成打包结果
writeFile(filePath, code) {
// filePath: F:\code\note_code\webpack\webpack-dev-3\dist\bundle.js
// 先判断文件夹是否存在,不存在的话先创建文件夹
// path.dirname(filePath) F:\code\note_code\webpack\webpack-dev-3\dist
let outputFolder = path.dirname(filePath)
if (!fs.existsSync(outputFolder)) {
fs.mkdirSync(outputFolder) // 没有文件夹先创建文件夹
}
// 输出打包文件
fs.writeFileSync(filePath, code)
}
// 获取模块代码
// 我们是通过路径来读取代码的,实际上可能这个代码会匹配到自己配置的loader
// 比如 .less .js 等
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8') // 读取到的源文件
// 看看是否需要用自定义的loader处理
let rules = this.config.module.rules // 拿到配置的所有loader
// 如果路径匹配上了,就用自己的loader去加载文件
for (let i = 0; i < rules.length; i++) {
let rule = rules[i] // 配置的每一种规则 比如 /\.js$/ /\.css$/ /\.less$/ 等
let { test, use } = rule // 将规则和配置的loader解构出来
// loader是倒着执行,拿到最后的索引
let count = use.length - 1
if (test.test(modulePath)) { // 匹配到当前这个模块需要使用loader来处理
// 倒着依次执行loader
function normalLoader() {
let loader = require(use[count]) // 配置loader时使用的是绝对路径
// 递归调用loader
content = loader(content)
count-- // 倒着挨个执行loader
if (count >= 0) {
normalLoader()
}
}
// 刚进来 先执行一次,然后就递归执行loader
normalLoader()
}
}
return content
}
}
module.exports = Complier
运行一下,测试结果 npx cheny-pack
打包结果
js
// webpack\cheny-pack\lib\main.ejs
(function (modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src\index.js");
})
({
"./src\index.js":
(function (module, exports, __webpack_require__) {
eval(`// webpack\webpack-dev-3\src\index.js
let str = __webpack_require__("./src\\a.js");
__webpack_require__("./src\\index.less");
console.log(str); // aaabbb`);
}),
"./src\a.js":
(function (module, exports, __webpack_require__) {
eval(`// webpack\webpack-dev-3\src\a.js
let b = __webpack_require__("./src\\base\\b.js");
module.exports = 'aaa' + b; // aaabbb`);
}),
"./src\base\b.js":
(function (module, exports, __webpack_require__) {
eval(`// webpack\webpack-dev-3\src\base\b.js
module.exports = 'bbb';`);
}),
"./src\index.less":
(function (module, exports, __webpack_require__) {
eval(`let style = document.createElement('style');
style.innerHTML = "body {\\n background-color: green;\\n}\\n";
document.head.appendChild(style);`);
}),
});
将这个文件引入html中,发现是好使的,编写成功。
总结
自己手写less-loader,style-loader
less-loader
jslet less = require('less') // less-loader function loader(source) { let css = '' // 转换后的css // 虽然是以回调的方式写的,其实是同步执行的 less.render(source, (err, c) => { css = c.css }) // webpack中是使用eval执行的代码片段 // 编写less中的 \n 应该替换成 \\n // 否则就会出错 css = css.replace(/\n/g, '\\n') return css } module.exports = loader
style-loader
js// style-loader function loader(source) { let style = ` let style = document.createElement('style') style.innerHTML = ${JSON.stringify(source)} document.head.appendChild(style) ` return style } module.exports = loader
配置loader时,加载自己的loader,配置的是全局路径
js
let path = require('path')
module.exports = {
module: {
rules: [
{
test: /\.less$/,
// 引入自己写的loader
use: [
path.resolve(__dirname, 'loader', 'style-loader'),
path.resolve(__dirname, 'loader', 'less-loader')
]
}
]
}
}
自定义打包工具时,在读取文件资源的时候,看看路径是否是需要自定义loader来加载
js
// 获取模块代码
// 我们是通过路径来读取代码的,实际上可能这个代码会匹配到自己配置的loader
// 比如 .less .js 等
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8') // 读取到的源文件
// 看看是否需要用自定义的loader处理
let rules = this.config.module.rules // 拿到配置的所有loader
// 如果路径匹配上了,就用自己的loader去加载文件
for (let i = 0; i < rules.length; i++) {
let rule = rules[i] // 配置的每一种规则 比如 /\.js$/ /\.css$/ /\.less$/ 等
let { test, use } = rule // 将规则和配置的loader解构出来
// loader是倒着执行,拿到最后的索引
let count = use.length - 1
if (test.test(modulePath)) { // 匹配到当前这个模块需要使用loader来处理
// 倒着依次执行loader
function normalLoader() {
let loader = require(use[count]) // 配置loader时使用的是绝对路径
// 递归调用loader
content = loader(content)
count-- // 倒着挨个执行loader
if (count >= 0) {
normalLoader()
}
}
// 刚进来 先执行一次,然后就递归执行loader
normalLoader()
}
}
return content
}
参考
https://www.bilibili.com/video/BV1a4411e7Bz?p=38&spm_id_from=pageDriver