神族九帝's blog 神族九帝's blog
首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)

神族九帝,永不言弃

首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)
  • 工作笔记

  • 前端基础建设与架构 30 讲

    • 开篇词 像架构师一样思考,突破技术成长瓶颈
    • npm 安装机制及企业级部署私服原理
    • Yarn 的安装理念及如何破解依赖管理困境
    • CI 环境上的 npm 优化及更多工程化问题解析
    • 横向对比主流构建工具,了解构建工具的设计考量
    • Vite 实现:从源码分析出发,构建 bundlele 开发工程
      • core-j 及垫片理念:设计一个“最完美”的 Polyfill 方案
      • 梳理混乱的 Babel,不再被编译报错困扰
      • 探索前端工具链生态,制定一个统一标准化 babel-preet
      • 从实战出发,从 0 到 1 构建一个符合标准的公共库
      • 代码拆分和按需加载:缩减 bundle ize,把性能做到极致
      • Tree Shaking:移除 JavaScript 上下文中的未引用代码
      • 如何理解 AST 实现和编译原理?
      • 工程化思维处理方案:如何实现应用主题切换功能?
      • 解析 Webpack 源码,实现自己的构建工具
      • 从编译到运行,跨端解析小程序多端方案
      • 原生跨平台技术:移动端跨平台到 Flutter 的技术变革
      • 学习 axio:封装一个结构清晰的 Fetch 库
      • 对比 Koa 和 Redux:分析前端中的中间件理念
      • 如何理解软件开发灵活性和高定制性?
      • 如何理解前端中面向对象的思想?
      • 如何利用 JavaScript 实现经典数据结构?
      • 剖析前端中的数据结构应用场景
      • npm cript:打造一体化的构建和部署流程
      • 自动化代码检查:剖析 Lint 工具和工程化接入&优化方案
      • 如何设计一个前端 + 移动端离线包方案?
      • 如何设计一个“万能”项目脚手架?
      • 同构渲染架构:实现一个 SSR 应用
      • 设计性能守卫系统:完善 CICD 流程
      • 实践打造网关:改造企业 BFF 方案
      • 实现高可用:使用 Puppeteer 生成性能最优的海报系统
      • 结束语 再谈项目的基建和架构,个人的价值和方向
    • vue2源码学习

    • 剖析vuejs内部运行机制

    • TypeScript 入门实战笔记

    • vue3源码学习

    • 2周刷完100道前端优质面试真题

    • 思维导图

    • npm发包

    • 重学node

    • 前端性能优化方法与实战

    • webpack原理与实战

    • webGl

    • 前端优化

    • Web3

    • React

    • 更多

    • 笔记
    • 前端基础建设与架构 30 讲
    wu529778790
    2024-04-07

    Vite 实现:从源码分析出发,构建 bundlele 开发工程

    通过上一讲的内容,相信你已经了解了现代化构建流程和处理内容。这一讲,我将结合 Webpack 为主的成熟方案现阶段的“不足”,从源码实现角度带你分析 Vite 的设计哲学,同时为“解析 Webpack 源码,实现自己的构建工具”一讲内容打下基础,循序渐进,最终你将能够开发一个自己的构建工具。

    Vite 的“横空出世”

    Vite 是由 Vue 作者尤雨溪开发的 Web 开发工具,尤雨溪在微博上推广时对 Vite 做了简短介绍:

    Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 Rollup 打包。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。

    从这段话中我们能够提炼一些关键点:

    • Vite 基于 ESM,因此实现了快速启动和即时模块热更新能力;

    • Vite 在服务端实现了按需编译。

    经验丰富的开发者通过上述介绍,似乎就能给出 Vite 的基本流程,甚至可以说得更直白一些:Vite 在开发环境下并没有打包和构建过程。

    开发者在代码中写到的 ESM 导入语法会直接发送给服务器,而服务器也直接将 ESM 模块内容运行处理后,下发给浏览器。接着,现代浏览器通过解析 script module,对每一个 import 到的模块进行 HTTP 请求,服务器继续对这些 HTTP 请求进行处理并响应。

    Vite 实现原理解读

    Vite 思想比较容易理解,实现起来也并不复杂。接下来,我们就对 Vite 源码进行分析,帮助你更好地体会它的设计哲学和实现技巧。

    首先,我们打造一个学习环境,创建一个基于 Vite 的应用,并启动:

    npm init vite-app vite-app
    cd vite-app
    npm install
    npm run dev
    

    得到以下目录结构和页面内容:

    Lark20201225-174521.png

    Drawing 1.png

    其中浏览器请求:http://localhost:3000/,得到的内容即是我们应用项目中的 index.html 内容。

    在项目 packaga.json 中,我们看到:

    "scripts": {
        "dev": "vite",
        // ...
     },
    

    找到 Vite 源码中,命令行实现部分:

    if (!options.command || options.command === 'serve') {
     runServe(options)
    } else if (options.command === 'build') {
     runBuild(options)
    } else if (options.command === 'optimize') {
     runOptimize(options)
    } else {
     console.error(chalk.red(`unknown command: ${options.command}`))
     process.exit(1)
    }
    

    上面代码,根据不同的命令行命令,执行不同的入口函数。

    在开发模式下,Vite 通过 runServe 方法,启动了一个 koaServer,来实现对浏览器请求的响应,runServer 实现如下:

    const server = require('./server').createServer(options)
    

    createServer 方法实现,我们可以精简为以下内容:

    export function createServer(config: ServerConfig): Server {
      const {
        root = process.cwd(),
        configureServer = [],
        resolvers = [],
        alias = {},
        transforms = [],
        vueCustomBlockTransforms = {},
        optimizeDeps = {},
        enableEsbuild = true
      } = config
      // 创建 Koa 实例
      const app = new Koa<State, Context>()
      const server = resolveServer(config, app.callback())
      const resolver = createResolver(root, resolvers, alias)
      // 相关上下文信息 
      const context: ServerPluginContext = {
        root,
        app,
        server,
        resolver,
        config,
        port: config.port || 3000
      }
      // 一个简单中间件,扩充 context 上下文内容
      app.use((ctx, next) => {
        Object.assign(ctx, context)
        ctx.read = cachedRead.bind(null, ctx)
        return next()
      })
      const resolvedPlugins = [
        // ...
      ]
    resolvedPlugins.forEach((m) => m && m(context))
    const listen = server.listen.bind(server)
    server.listen = (async (port: number, ...args: any[]) => {
    if (optimizeDeps.auto !== false) {
    await require('../optimizer').optimizeDeps(config)
    }
    const listener = listen(port, ...args)
    context.port = server.address().port
    return listener
    }) as any
    return server
    }
    

    浏览器在访问http://localhost:3000/后,得到了主体为:

    <body>
      <di v id="app"></div>
      <script type="module" src="/src/main.js"></script>
    </body>
    

    的内容。

    依据 ESM 规范在浏览器 script 标签中的实现,对于<script type="module" src="./bar.js"></script>内容:当出现 script 标签 type 属性为 module 时,浏览器将会请求模块相应内容。

    另一种 ESM 规范在浏览器 script 标签中的实现为:

    <script type="module">
      import { bar } from './bar.js‘
    </script>
    

    浏览器会发起 HTTP 请求,请求 HTTP Server 托管的 bar.js。

    我们可以看到,经过 Vite Server 处理 http://localhost:3000/src/main.js 请求后,最终返回了:

    Lark20201225-174524.png

    返回内容和我们项目中的 ./src/main.js 略有差别:

    import { createApp } from 'vue'
    import App from './App.vue'
    import './index.css'
    

    现在变为:

    import { createApp } from '/@modules/vue.js'
    import App from '/src/App.vue'
    import '/src/index.css?import'
    

    这里我们拆成两部分来看。

    其中import { createApp } from 'vue'改为import { createApp } from '/@modules/vue.js',原因很明显:import 对应的路径只支持 "/""./"或者 "../" 开头的内容,直接使用模块名 import,会立即报错。

    所以在 Vite Server 处理请求时,通过 serverPluginModuleRewrite 这个中间件来给 import from 'A' 的 A 添加 /@module/ 前缀为 from '/@modules/A',源码部分对应:

    const resolvedPlugins = [
      // ...
      moduleRewritePlugin,
      // ...
    ]
    resolvedPlugins.forEach((m) => m && m(context))
    

    而 moduleRewritePlugin 插件的实现也并不困难,主要通过 rewriteImports 方法,来执行 resolveImport 方法,并进行改写。这里已经添加了相关源码链接,我们不再一一展开,你可以在课后进一步学习。

    整个过程和调用链路较长,我对 Vite 处理 import 方法做一个简单总结:

    • 在 koa 中间件里获取请求 path 对应的 body 内容;

    • 通过 es-module-lexer 解析资源 AST,并拿到 import 的内容;

    • 如果判断 import 的资源是绝对路径,即可认为该资源为 npm 模块,并返回处理后的资源路径。比如上述代码中,vue → /@modules/vue。

    对于形如:import App from './App.vue'和import './index.css'的处理,与上述情况类似:

    • 在 koa 中间件里获取请求 path 对应的 body 内容;

    • 通过 es-module-lexer 解析资源 AST,并拿到 import 的内容;

    • 如果判断 import 的资源是相对路径,即可认为该资源为项目应用中资源,并返回处理后的资源路径。比如上述代码中,./App.vue → /src/App.vue。

    接下来浏览器根据 main.js 的内容,分别请求:

    /@modules/vue.js
    /src/App.vue
    /src/index.css?import
    

    对于 /@module/ 类请求较为容易,我们只需要完成下面三步:

    • 在 koa 中间件里获取请求 path 对应的 body 内容;

    • 判断路径是否以 /@module/ 开头,如果是,取出包名(这里为 vue.js);

    • 去 node_modules 文件中找到对应的 npm 库,并返回内容。

    上述步骤在 Vite 中使用 serverPluginModuleResolve 中间件实现,点击这里可以访问对应源码。

    接着,就是对 /src/App.vue 类请求进行处理,这就涉及 Vite 服务器的编译能力了。

    我们先看结果,对比项目中的 App.vue,浏览器请求得到的结果显然出现了大变样:

    Drawing 3.png

    实际上,App.vue 这样的单文件组件对应 script、style 和 template,在经过 Vite Server 处理时,服务端对 script、style 和 template 三部分分别处理,对应中间件为 serverPluginVue。这个中间件的实现很简单,即对 .vue 文件请求进行处理,通过 parseSFC 方法解析单文件组件,并通过 compileSFCMain 方法将单文件组件拆分为形如上图内容,对应中间件关键内容可在源码 vuePlugin 中找到。源码中,涉及 parseSFC 具体所做的事情,是调用 @vue/compiler-sfc 进行单文件组件解析。精简为我自己的逻辑,帮助你理解:

    if (!query.type) {
      ctx.body = `
        const __script = ${descriptor.script.content.replace('export default ', '')}
        // 单文件组件中,对于 style 部分的编译,编译为对应 style 样式的 import 请求
        ${descriptor.styles.length ? `import "${url}?type=style"` : ''}
        // 单文件组件中,对于 template 部分的编译,编译为对应 template 样式的 import 请求
        import { render as __render } from "${url}?type=template"
        // 渲染 template 的内容
        __script.render = __render;
        export default __script;
      `;
    }
    

    总而言之,每一个 .vue 单文件组件都被拆分成多个请求。比如对应上面场景,浏览器接收到 App.vue 对应的实际内容后,发出 HelloWorld.vue 以及 App.vue?type=template 的请求(通过 type 这个 query 来表示是 template 还是 style)。koa server 进行分别处理并返回,这些请求依然分别被上面提到的 serverPluginVue 中间件处理:对于 template 的请求,服务使用 @vue/compiler-dom 进行编译 template 并返回内容。

    精简为我自己的逻辑,帮助你理解:

    if (query.type === 'template') {
     const template = descriptor.template;
     const render = require('@vue/compiler-dom').compile(template.content, {
       mode: 'module',
     }).code;
     ctx.type = 'application/javascript';
     ctx.body = render;
    }
    

    对于上面提到的 http://localhost:3000/src/index.css?import 请求稍微特殊,需通过 serverPluginVue 来实现解析:

    // style 类型请求
    if (query.type === 'style') {
      const index = Number(query.index)
      const styleBlock = descriptor.styles[index]
      if (styleBlock.src) {
        filePath = await resolveSrcImport(root, styleBlock, ctx, resolver)
      }
      const id = hash_sum(publicPath)
      // 调用 compileSFCStyle 方法编译当文件组件样式部分
      const result = await compileSFCStyle(
        root,
        styleBlock,
        index,
        filePath,
        publicPath,
        config
      )
      ctx.type = 'js'
      // 返回样式内容
      ctx.body = codegenCss(`${id}-${index}`, result.code, result.modules)
      return etagCacheCheck(ctx)
    }
    

    调用 serverPluginCss 中间件的 codegenCss 方法:

    export function codegenCss(
      id: string,
      css: string,
      modules?: Record<string, string>
    ): string {
      // 样式代码模板
      let code =
        `import { updateStyle } from "${clientPublicPath}"\n` +
        `const css = ${JSON.stringify(css)}\n` +
        `updateStyle(${JSON.stringify(id)}, css)\n`
      if (modules) {
        code += dataToEsm(modules, { namedExports: true })
      } else {
        code += `export default css`
      }
      return code
    }
    

    该方法会在浏览器中执行 updateStyle 方法,源码如下:

    const supportsConstructedSheet = (() => {
      try {
        // 生成 CSSStyleSheet 实例,试探是否支持 ConstructedSheet
        new CSSStyleSheet()
        return true
      } catch (e) {}
      return false
    })()
    export function updateStyle(id: string, content: string) {
      let style = sheetsMap.get(id)
      if (supportsConstructedSheet && !content.includes('@import')) {
        if (style && !(style instanceof CSSStyleSheet)) {
          removeStyle(id)
          style = undefined
        }
        if (!style) {
          // 生成 CSSStyleSheet 实例
          style = new CSSStyleSheet()
          style.replaceSync(content)
          document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]
        } else {
          style.replaceSync(content)
        }
      } else {
        if (style && !(style instanceof HTMLStyleElement)) {
          removeStyle(id)
          style = undefined
        }
        if (!style) {
          // 生成新的 style 标签并插入到 document 挡住
          style = document.createElement('style')
          style.setAttribute('type', 'text/css')
          style.innerHTML = content
          document.head.appendChild(style)
        } else {
          style.innerHTML = content
        }
      }
      sheetsMap.set(id, style)
    }
    

    最终完成在浏览器中插入样式。

    至此,我们解析并列举了较多源码内容。以上内容需要你跟着思路,一步步梳理,我也强烈建议你打开 Vite 源码自己动手剖析。如果看到这里你仍然也有些“云里雾里”,不要心急,结合我下面这个图示,再次进行阅读,相信会更有收获。

    Vite 这种 bundleless 方案的运行原理图:

    Lark20201225-174527.png

    Lark20201225-174517.png

    接下来我们再做一些更细节的总结。

    • Vite 利用浏览器原生支持 ESM 这一特性,省略了对模块的打包,也就不需要生成 bundle,因此初次启动更快,HMR 特性友好。

    • Vite 开发模式下,通过启动 koa 服务器,在服务端完成模块的改写(比如单文件的解析编译等)和请求处理,实现真正的按需编译。

    • Vite Server 所有逻辑基本都依赖中间件实现。这些中间件,拦截请求之后,完成了如下内容:

      • 处理 ESM 语法,比如将业务代码中的 import 第三方依赖路径转为浏览器可识别的依赖路径;

      • 对 .ts、.vue 等文件进行即时编译;

      • 对 Sass/Less 的需要预编译的模块进行编译;

      • 和浏览器端建立 socket 连接,实现 HMR。

    Vite HMR 实现原理

    Vite 的打包命令使用了 Rollup 进行,这里并没有什么特别之处,我们不再展开讲解。而 Vite 的 HMR 特性,主要是围绕着:

    • 通过 watcher 监听文件改动

    • 通过 server 端编译资源,并推送新模块内容给浏览器

    • 浏览器收到新的模块内容,执行框架层面的 rerender/reload

    三步进行。

    当浏览器请求 HTML 页面时,服务端通过 serverPluginHtml 插件向 HTML 内容注入一段脚本。如下图所示,我们可以看到, index.html 中就有一段引入 /vite/client 代码,进行 WebSocket 的注册和监听。

    Drawing 6.png

    Drawing 7.png

    对于 /vite/client 请求的处理,服务端由 serverPluginClient 插件进行处理:

    export const clientPlugin: ServerPlugin = ({ app, config }) => {
      const clientCode = fs
        .readFileSync(clientFilePath, 'utf-8')
        .replace(`__MODE__`, JSON.stringify(config.mode || 'development'))
        .replace(
          `__DEFINES__`,
          JSON.stringify({
            ...defaultDefines,
            ...config.define
          })
        )
      // 相应中间件处理
      app.use(async (ctx, next) => {
        if (ctx.path === clientPublicPath) {
          ctx.type = 'js'
          ctx.status = 200
          // 返回具体内容
          ctx.body = clientCode.replace(`__PORT__`, ctx.port.toString())
        } else {
          // 兼容历史逻辑,并进行错误提示
          if (ctx.path === legacyPublicPath) {
            console.error(
              chalk.red(
                `[vite] client import path has changed from "/vite/hmr" to "/vite/client". ` +
                  `please update your code accordingly.`
              )
            )
          }
          return next()
        }
      })
    }
    

    返回的 /vite/src/client/client.js 代码在浏览器端主要通过 WebSocket 监听了一些更新的类型(vue 组件更新/vue template 更新/vue style 更新/css 更新/css 移除/js 更新/页面 roload),分别进行处理。

    在服务端,通过 chokidar 创建了一个监听文件改动的 watcher 来监听文件改动:

    const watcher = chokidar.watch(root, {
     ignored: [/node_modules/, /\.git/],
     // #610
     awaitWriteFinish: {
       stabilityThreshold: 100,
       pollInterval: 10
     }
    }) as HMRWatcher
    

    并通过 serverPluginHmr 发布变动,通知浏览器。

    更多源码不再一一贴出。这里我总结了一张流程图供你参考:

    Lark20201225-175233.png

    Vite 实现 HMR 流程图

    总结

    这一讲我们聚焦 Vite 实现,分析了如何利用 ESM,构建一个 bundleless 风格的现代化开发工程方案。源码内容较多,也涉及一定工程化架构设计内容,但 Vite 实现流程清晰,易读性高,是源码阅读类很好的资源。

    事实上,Vite 依赖优化的灵感来自 Snowpack,这类 bundleless 工具也代表着一种新趋势、新方向。我认为,技术功底是很重要的一方面,而技术敏感度的培养也非常关键。希望与你共勉!

    到此,新编译工具理念——Vite 我们就介绍到这里。接下来我们将进入代码降级编译环节的学习,我们下一讲再见。


    # 精选评论

    # **龙

    老师我想问下 Vite 是怎么保证 dev 环境和生产环境,最后得到的结果是一样的啊,会不会出现 dev 看到的是一种现象,生产环境看到的是另一种现象还有如果我同时 import 两个 js 文件,这两个文件的加载执行顺序会有先后吗?是必须前一个加载完成后,才会去 import 另外一个吗?

    #     讲师回复

        不是的,请求不一定是串行关系,这个可以自己动手试验一下

    # **龙

    老师我有几个疑惑点 1 Vite 是怎么保证本地环境运行出的结果和最后生产环境是一致的啊 2 我在一个 js 文件中,同时有两个 import,这个 js 就会导致发送两个请求,是不是只有等一个请求发送接收到返回以后,才回去发送下一个请求,是一种同步的行为

    #     讲师回复

        不是的,请求不一定是串行关系,这个可以自己动手试验一下

    # **勇

    vite@2.x不使用 koa 来创建服务和管理中间件了,而是使用 connect。是处于什么考虑呢?TJ 不维护 Koa 了哇

    #     讲师回复

        这是目前看到最好的问题之一,不过提问者应该更进一步,提升自己解决问题,找到答案的能力。具体原因在 https://github.com/vitejs/vite/blob/91dbb017091c175a54bcd1c93a69f8458d1bde8d/docs/guide/migration.md#for-plugin-authors (opens new window) 中有所体现了其实,简单总结一下是 vite@2.x 主要是用基于 hooks 的插件,对于 koa 中间件的需求大幅度减少,从依赖成本上看,old school 的 connect 即可方便轻巧满足需求了

    # *锋

    什么时候更新呀

    #     编辑回复

        每周一、三更新哈~

    # **8621

    第一个提出 bundleless 理念的人,绝对是十分善于思考的人。

    # **勇

    SnowPack 和 Vite 的区别主要是对打包的处理吗?Vite 是使用 Rollup 来 build,Vite 还是使用 Webpack 来完成 build。后续会对这两者的区别展开讲解吗?

    #     讲师回复

        对于 SnowPack 和 Vite 的发展路线其实是比较有趣的话题,也会随着时间演进有所变化。可以与我保持联系,随时沟通

    # **0231

    大佬太强了,大佬是参考了相关文章进行的学习,还是直接梳理源码进行的分析

    #     讲师回复

        知识获取方式:看源码,看官网,去 github 提 issue/pr,多参与国际社区讨论,国内毕竟面比较有限

    # **贤

    学到了, 都没听说过 Vite, 我是不是太菜了 😂

    #     编辑回复

        不是哦,学习是个持久的过程,每个知识都是从无到有。现在掌握也不晚呐~

    # cxl

    请问下老师,react 有 bundless 吗

    #     讲师回复

        有的,vite 可以改造支持,另外建议找一下 snowpack 的 react 方案

    编辑 (opens new window)
    上次更新: 2025/03/17, 12:21:00
    横向对比主流构建工具,了解构建工具的设计考量
    core-j 及垫片理念:设计一个“最完美”的 Polyfill 方案

    ← 横向对比主流构建工具,了解构建工具的设计考量 core-j 及垫片理念:设计一个“最完美”的 Polyfill 方案→

    最近更新
    01
    Code Review
    10-14
    02
    ComfyUI
    10-11
    03
    vscode插件开发
    08-24
    更多文章>
    Power by vuepress | Copyright © 2015-2025 神族九帝
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×