美文网首页
Loader 原理

Loader 原理

作者: littleyu | 来源:发表于2021-03-24 22:53 被阅读0次

    上文我们写了一个打包器,但是只能加载 JS 文件,现在我们尝试让他可以加载 CSS

    如何加载 CSS

    思路

    1. 我们的 bundle 只能加载 JS
    2. 我们想要加载 CSS
    3. 如果我们能把 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 ,然后新建一个页面引入即可

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <script src="./dist/bundle.js"></script>
    </body>
    </html>
    

    到此我们已经成功加载一个 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

    相关文章

      网友评论

          本文标题:Loader 原理

          本文链接:https://www.haomeiwen.com/subject/viocqltx.html