加载CSS文件
思路
- bundler只能加载JS
- 想要加载CSS
- 把CSS变成JS就可以加载CSS了
用正则对文件进行匹配
let code = readFileSync(filepath).toString()
if(/\.css$/.test(filepath)){ // 如何文件路径以 .css 结尾
code = `
const str = ${JSON.stringify(code)}
if(document){
const style = document.createElement('style')
style.innerHTML = str
document.head.appendChild(style)
}
export default str
`
}
bundler_css.ts
// 请确保你的 Node 版本大于等于 14
// 请先运行 yarn 或 npm i 来安装依赖
// 然后使用 node -r ts-node/register 文件路径 来运行,
// 如果需要调试,可以加一个选项 --inspect-brk,再打开 Chrome 开发者工具,点击 Node 图标即可调试
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)){ // 如何文件路径以 .css 结尾
code = `
const str = ${JSON.stringify(code)}
if(document){
const style = document.createElement('style')
style.innerHTML = str
document.head.appendChild(style)
}
export default str
`
}
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, '/')
}
创建style.css 在index.js中引入css$ node -r ts-node/register bundler_css.ts
在dist中创建index.html,引入bundler.js
css-loader
把上面的代码优化成loader
/loaders/css-loader.js
const transform = (code) => `
const str = ${JSON.stringify(code)}
if(document){
const style = document.createElement('style')
style.innerHTML = str
document.head.appendChild(style)
}
export default str
`
module.exports = transform;
/bundler_css_loader.ts
...
let code = readFileSync(filepath).toString()
if (/\.css$/.test(filepath)) { // 如何文件路径以 .css 结尾
code = require('./loaders/css-loader')(code)
}
...
loader是什么
一个loader可以是一个普通函数
function transform(code){
const code2=doSomething(code)
return code2
}
module.exports = transform //兼容Node.js
一个loader也可以是一个异步函数
async function transform(code){
const code2 = await doSomething(code)
return code2
}
module.exports = transform //旧版本Node.js不支持exports
为什么用require不用import,主要是为了动态加载和旧版node不支持import
单一职责原则
webpack里每个loader只做一件事
优化css-loader
现在的css-loader做了两件事
- sass-loader、less-loader 这些loader是把代码从一种语言翻译成另一种
- 这种loader,按照顺序连接起来不会出问题
- 但style-loader是在插入代码,不是转译,所以需要寻找插入时机和插入位置
- 插入代码的时机应该是在获取到css-loader的结果之后
-
Webpack官方style-loader的思路
style-loader 在 pitch 钩子里通过css-loader来require 文件内容
- 然后在文件内容后面添加
injectStylesIntoStyleTag(content,...)
代码
其他加载
- 加载 .scss 文件
- 写个 sass-loader 把 SCSS 文件转为 CSS
- 再交给 css-loader 转为 JS
- 最后用 style-loader 创建 style 标签
- 加载 .less 文件
- 写个less-loader把LESS文件转为CSS
- 交给css-loader 转为 JS
- 最后用 style-loader 创建 style 标签
- 加载 .styl 文件
- 加载 .ts 文件
- awesome-typescript-loader
- 或者ts-loader
- 加载 .md 文件
- markdown-loader
- 加载 .html 文件
- html-loader
- 加载 .txt 文件
- raw-loader
- 加载 .vue 文件
- vue-loader
思考
import logo from ‘./images/logo.png’
React :
- 用什么loader?
两种思路
- 导出相对路径
- 把后缀名为png的文件放到一个public文件夹中
- 导出相对路径
- base64
- 解析图片后可以将图片转换为base64编码
- 导出base64编码
raw-loader源码
github page
src/index.js
- webpack 提供 loader-utils 和 schema-utils 作为辅助工具
- webpack 通过 this 来传递上下文
- getOptions(this) 可以获取 options
- validate 可以验证 options 是否合法
- JSON 的 2028 和 2029 问题
css-loader源码
github page
src/index.js
- 跳过大部分代码,看核心代码
- this.async() 用于获取回调
- 核心内容只占 1/10 都不到,大部分内容都是插件和细节
面试题
Webpack 的 loader 是什么?
- webpack自带的打包器只支持JS文件
- 当我们想要加载css/less/scss/stylus/ts/md 文件就要用 loader
- loader 的原理就是把文件内容包装成能运行的 JS
- 加载 css 需要用到 style-loader 和 css-loader
- css-loader把代码从 CSS 变成 export default str 形式的JS代码
- style-loader 把代码挂载到head里的 style 标签里
这里可以深入说下style-loader用到了 pitch 钩子和 request 对象
自己写一个loader
- 说一下思路,说一下自己的loader和webpack推介loader区别在哪