上文我们写了一个打包器,但是只能加载 JS 文件,现在我们尝试让他可以加载 CSS
如何加载 CSS
思路
- 我们的 bundle 只能加载 JS
- 我们想要加载 CSS
- 如果我们能把 CSS 变成 JS。那么就可以加载 CSS 了
// 获取文件内容,将内容放至 depRelation
let code = readFileSync(filepath).toString()
if (/\.css$/.test(filepath)) {
code = `
const code = ${JSON.stringify(code)};
export default code;
`
}
如此一来,我们的 CSS 文件就变成了 js文件,但是目前并没有用,CSS 并不会生效。
再加一个骚操作即可让 CSS 生效
// 获取文件内容,将内容放至 depRelation
let code = readFileSync(filepath).toString()
if (/\.css$/.test(filepath)) {
code = `
const code = ${JSON.stringify(code)};
if (document) {
const style = document.createElement('style');
style.innerText = code;
document.head.appendChild(style);
}
export default code;
`
}
完整代码
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import { writeFileSync, readFileSync } from 'fs'
import { resolve, relative, dirname, join } from 'path';
import * as babel from '@babel/core'
import {mkdir} from 'shelljs'
// 设置根目录
const projectName = 'project_css'
const projectRoot = resolve(__dirname, projectName)
// 类型声明
type DepRelation = { key: string, deps: string[], code: string }[]
// 初始化一个空的 depRelation,用于收集依赖
const depRelation: DepRelation = [] // 数组!
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'))
// 先创建 dist 目录
const dir = `./${projectName}/dist`
mkdir('-p', dir)
// 再创建 bundle 文件
writeFileSync(join(dir, 'bundle.js'), generateCode())
console.log('done')
function generateCode() {
let code = ''
code += 'var depRelation = [' + depRelation.map(item => {
const { key, deps, code } = item
return `{
key: ${JSON.stringify(key)},
deps: ${JSON.stringify(deps)},
code: function(require, module, exports){
${code}
}
}`
}).join(',') + '];\n'
code += 'var modules = {};\n'
code += `execute(depRelation[0].key)\n`
code += `
function execute(key) {
if (modules[key]) { return modules[key] }
var item = depRelation.find(i => i.key === key)
if (!item) { throw new Error(\`\${item} is not found\`) }
var pathToKey = (path) => {
var dirname = key.substring(0, key.lastIndexOf('/') + 1)
var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
return projectPath
}
var require = (path) => {
return execute(pathToKey(path))
}
modules[key] = { __esModule: true }
var module = { exports: modules[key] }
item.code(require, module, module.exports)
return modules[key]
}
`
return code
}
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
if (depRelation.find(i => i.key === key)) {
// 注意,重复依赖不一定是循环依赖
return
}
// 获取文件内容,将内容放至 depRelation
let code = readFileSync(filepath).toString()
if (/\.css$/.test(filepath)) {
code = `
const code = ${JSON.stringify(code)};
if (document) {
const style = document.createElement('style');
style.innerText = code;
document.head.appendChild(style);
}
export default code;
`
}
const { code: es5Code } = babel.transform(code, {
presets: ['@babel/preset-env']
})
// 初始化 depRelation[key]
const item = { key, deps: [], code: es5Code }
depRelation.push(item)
// 将代码转为 AST
const ast = parse(code, { sourceType: 'module' })
// 分析文件依赖,将内容放至 depRelation
traverse(ast, {
enter: path => {
if (path.node.type === 'ImportDeclaration') {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath)
// 把依赖写进 depRelation
item.deps.push(depProjectPath)
collectCodeAndDeps(depAbsolutePath)
}
}
})
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
return relative(projectRoot, path).replace(/\\/g, '/')
}
让我们搞个页面试试代码就知道了
// index.js
import a from './a.js'
import b from './b.js'
import './index.css'
console.log(a.getB())
console.log(b.getA())
// index.css
body {background-color: #c03;}s
运行 node -r ts-node/register bundler_css.ts ,然后新建一个页面引入即可
Document
到此我们已经成功加载一个 CSS 文件,但是我们没有使用 loader,我们目前是写死在打包器的。
创建 CSS loader
其实很简单
只需要创建文件 css-loader.js,并把代码复制过去即可
// css-loader.js
const cssLoader = code => `
const code = ${JSON.stringify(code)};
if (document) {
const style = document.createElement('style');
style.innerText = code;
document.head.appendChild(style);
}
export default code;
`
module.exports = cssLoader
之前的代码变成引入的文件
if (/\.css$/.test(filepath)) {
code = require('./loader/css-loader.js')(code)
}
为什么要用 require,因为很多 loader 的名字都是从配置文件中读取的,主要是为了方便动态加载
loader 长什么样子
- 一个loader 可以是普通函数
function transform(code){ const code2 = doSomething(code) return code } modules.exports = transform
- 一个 loader 也可以是一个异步函数
async function transform(code){ const code2 = await doSomething(code) return code } modules.exports = transform
简单的 loader 搞定,开始优化
单一职责原则
- webpack 里每个 loader 只做一件事
- 目前我们的 css-loader 做了两件事
- 一是把 CSS 变为 JS 字符串
- 二是把 JS 字符串放到 style 标签里
不浮于表面,是 P6 的觉悟
如果你知道的东西跟别人差不多,很难进大公司
很显然我们只要把我们的 loader 拆成两个 loader 就可以了
// css-loader
const cssLoader = code => `
const code = ${JSON.stringify(code)};
export default code;
`
module.exports = cssLoader
// style-loader
const styleLoader = code => `
if (document) {
const style = document.createElement('style');
style.innerText = ${JSON.stringify(code)};
document.head.appendChild(style);
}
`
module.exports = styleLoader
// bundle_css_loader_1
if (/\.css$/.test(filepath)) {
code = require('./loader1/css-loader.js')(code)
code = require('./loader1/style-loader.js')(code)
}
运行发现检查代码发现这是行不通的
经过 style-loader 转换过的代码,并不是我们想要的结果,说明我们的思路存在一些问题
分析
我的代码错在哪儿呢?
- style-loader 不是转译
- sass-loader、less-loader 这些 loader 是把代码从一种语言转译为另一种语言
- 因此将这样的 loader 连接起来不会出问题
- 但 style-loader 是在插入代码,不是转译,所以需要寻找插入时机和插入位置
- 插入代码的时机应该是在获取到 css-loader 的结果之后
- 插入代码的位置应该是在就代码的下面
目前缺乏一种机制可以让我们随意插入代码,而 webpack 是可以的,所以目前来说我们做不到--写不出 style-loader
- Webpack 官方 style-loader 的思路
- style-loader 在 pitch 钩子里通过 css-loader 来 require 文件内容
- 然后在文件内容后面添加 injectStyleIntoStyleTag(content, ...) 代码
- 接下来看看 webpack 的核心代码在哪儿
- 并分析
阅读 style-loader 源码理解 webpack
- 不推荐这么做
- 直接看源码
- 应该这么做
- 不看源码,大胆假设
- 遇到问题,小心求证
- 带着问题看源码唯一正确的方式(我认为)
- 一定要自己先想一次
- 当你的思路无法满足需求的时候,去看别人的实现
- 看懂了,就悟了
全部折叠以后,代码结构十分清晰,首先他声明了一个 loaderApi 函数,然后添加了一个 pitch 函数(非常重要)
这个 style-loader 非常奇怪,本身竟然是一个空函数,所有的逻辑都在 pitch 函数里面。
由于我折叠了所有代码,逻辑结构得长清楚,首先获取所有的选项,然后验证这些选项,之后声明一些变量,最终会在一个 switch case 负责主要的逻辑
一般来说我们会把代码插入到 style 标签里面
返回的东西中,会判断是否是最新的模块系统,是的话就es6的,否则就用 nodeJS 的模块,所以我们只展开最新的
首先引入一个 api 函数,直接看后面的英文('runtime/injectStylesIntoStyleTag.js'),我们可以很容易发现这个 api 函数就是一个把 style 插入 styleTag 的函数
其次是引入 content,很显然这就是要插入的 style 内容,
我们有了一个插入函数和插入内容,有了这两样东西,那就逻辑上来说就很简单了,直接一结合就是想要的结果
果然,把 content 和 options 传给 api 这个函数,这样之后页面就有想要的样式了,所以这个style-loader 核心代码就三块。
其他代码基本就是在做各种兼容
这个架构看起来好像和我们写的 style-loader 一样的,但是为什么我们就很难实现了,关键在于 webpack 的 style-loader 可以去加载这个request,但是我们没有这个 request 对象,
再来看看我们的问题
我们这个地方能写啥呢?我们拿不到内容,而webpack 比我们多传入了一个 request 对象,它可以拿到代码之外的东西。
webpack 到底有多少个 loader
- 官方的推荐列表
- 社区的推荐列表