加载CSS文件

思路

  1. bundler只能加载JS
  2. 想要加载CSS
  3. 把CSS变成JS就可以加载CSS了

用正则对文件进行匹配

  1. let code = readFileSync(filepath).toString()
  2. if(/\.css$/.test(filepath)){ // 如何文件路径以 .css 结尾
  3. code = `
  4. const str = ${JSON.stringify(code)}
  5. if(document){
  6. const style = document.createElement('style')
  7. style.innerHTML = str
  8. document.head.appendChild(style)
  9. }
  10. export default str
  11. `
  12. }

bundler_css.ts

  1. // 请确保你的 Node 版本大于等于 14
  2. // 请先运行 yarn 或 npm i 来安装依赖
  3. // 然后使用 node -r ts-node/register 文件路径 来运行,
  4. // 如果需要调试,可以加一个选项 --inspect-brk,再打开 Chrome 开发者工具,点击 Node 图标即可调试
  5. import { parse } from "@babel/parser"
  6. import traverse from "@babel/traverse"
  7. import { writeFileSync, readFileSync } from 'fs'
  8. import { resolve, relative, dirname, join } from 'path';
  9. import * as babel from '@babel/core'
  10. import {mkdir} from 'shelljs'
  11. // 设置根目录
  12. const projectName = 'project_css'
  13. const projectRoot = resolve(__dirname, projectName)
  14. // 类型声明
  15. type DepRelation = { key: string, deps: string[], code: string }[]
  16. // 初始化一个空的 depRelation,用于收集依赖
  17. const depRelation: DepRelation = [] // 数组!
  18. // 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
  19. collectCodeAndDeps(resolve(projectRoot, 'index.js'))
  20. // 先创建 dist 目录
  21. const dir = `./${projectName}/dist`
  22. mkdir('-p', dir)
  23. // 再创建 bundle 文件
  24. writeFileSync(join(dir, 'bundle.js'), generateCode())
  25. console.log('done')
  26. function generateCode() {
  27. let code = ''
  28. code += 'var depRelation = [' + depRelation.map(item => {
  29. const { key, deps, code } = item
  30. return `{
  31. key: ${JSON.stringify(key)},
  32. deps: ${JSON.stringify(deps)},
  33. code: function(require, module, exports){
  34. ${code}
  35. }
  36. }`
  37. }).join(',') + '];\n'
  38. code += 'var modules = {};\n'
  39. code += `execute(depRelation[0].key)\n`
  40. code += `
  41. function execute(key) {
  42. if (modules[key]) { return modules[key] }
  43. var item = depRelation.find(i => i.key === key)
  44. if (!item) { throw new Error(\`\${item} is not found\`) }
  45. var pathToKey = (path) => {
  46. var dirname = key.substring(0, key.lastIndexOf('/') + 1)
  47. var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
  48. return projectPath
  49. }
  50. var require = (path) => {
  51. return execute(pathToKey(path))
  52. }
  53. modules[key] = { __esModule: true }
  54. var module = { exports: modules[key] }
  55. item.code(require, module, module.exports)
  56. return modules[key]
  57. }
  58. `
  59. return code
  60. }
  61. function collectCodeAndDeps(filepath: string) {
  62. const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
  63. if (depRelation.find(i => i.key === key)) {
  64. // 注意,重复依赖不一定是循环依赖
  65. return
  66. }
  67. // 获取文件内容,将内容放至 depRelation
  68. let code = readFileSync(filepath).toString()
  69. if(/\.css$/.test(filepath)){ // 如何文件路径以 .css 结尾
  70. code = `
  71. const str = ${JSON.stringify(code)}
  72. if(document){
  73. const style = document.createElement('style')
  74. style.innerHTML = str
  75. document.head.appendChild(style)
  76. }
  77. export default str
  78. `
  79. }
  80. const { code: es5Code } = babel.transform(code, {
  81. presets: ['@babel/preset-env']
  82. })
  83. // 初始化 depRelation[key]
  84. const item = { key, deps: [], code: es5Code }
  85. depRelation.push(item)
  86. // 将代码转为 AST
  87. const ast = parse(code, { sourceType: 'module' })
  88. // 分析文件依赖,将内容放至 depRelation
  89. traverse(ast, {
  90. enter: path => {
  91. if (path.node.type === 'ImportDeclaration') {
  92. // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
  93. const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
  94. // 然后转为项目路径
  95. const depProjectPath = getProjectPath(depAbsolutePath)
  96. // 把依赖写进 depRelation
  97. item.deps.push(depProjectPath)
  98. collectCodeAndDeps(depAbsolutePath)
  99. }
  100. }
  101. })
  102. }
  103. // 获取文件相对于根目录的相对路径
  104. function getProjectPath(path: string) {
  105. return relative(projectRoot, path).replace(/\\/g, '/')
  106. }

创建style.css 在index.js中引入css
$ node -r ts-node/register bundler_css.ts
在dist中创建index.html,引入bundler.js
image.png

css-loader

把上面的代码优化成loader

/loaders/css-loader.js

  1. const transform = (code) => `
  2. const str = ${JSON.stringify(code)}
  3. if(document){
  4. const style = document.createElement('style')
  5. style.innerHTML = str
  6. document.head.appendChild(style)
  7. }
  8. export default str
  9. `
  10. module.exports = transform;

/bundler_css_loader.ts

  1. ...
  2. let code = readFileSync(filepath).toString()
  3. if (/\.css$/.test(filepath)) { // 如何文件路径以 .css 结尾
  4. code = require('./loaders/css-loader')(code)
  5. }
  6. ...

loader是什么

  • 一个loader可以是一个普通函数

    1. function transform(code){
    2. const code2=doSomething(code)
    3. return code2
    4. }
    5. module.exports = transform //兼容Node.js
  • 一个loader也可以是一个异步函数

    1. async function transform(code){
    2. const code2 = await doSomething(code)
    3. return code2
    4. }
    5. module.exports = transform //旧版本Node.js不支持exports
  • 为什么用require不用import,主要是为了动态加载和旧版node不支持import

image.png

单一职责原则

webpack里每个loader只做一件事

优化css-loader

现在的css-loader做了两件事

  1. 把css变成js字符串
  2. 把js字符串放到style标签里

    style-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

官方推介loader
社区推介loader

思考

import logo from ‘./images/logo.png’
React : 45.Loader 原理 - 图3

  • 用什么loader?

两种思路

  1. 导出相对路径
    • 把后缀名为png的文件放到一个public文件夹中
    • 导出相对路径
  2. 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区别在哪

如何自己写一个loader

  • 按照文档初始化一个项目
  • 看别人怎么写的
  • 复制过来
  • 改一改,有问题就翻自定义插件文档
  • 测试(文档里有示例,也可以抄别人的思路)
  • 发布到 npm
  • 在项目里使用它 markdown-loader