Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vite #132

Open
magicdawn opened this issue Apr 10, 2022 · 7 comments
Open

vite #132

magicdawn opened this issue Apr 10, 2022 · 7 comments

Comments

@magicdawn
Copy link
Owner

No description provided.

@magicdawn
Copy link
Owner Author

问题

cjs dep 中 require('xxx.css') 导致变成动态 require 的问题

如 node_modules/react-command-palette/dist/command-palette.js 里 require('xxx.css'), 经过 pre-bundle 变成了运行时 require(xxx.css)

解决

dev: vite-plugin-commonjs
build: ???

@magicdawn
Copy link
Owner Author

vite + electron

dev

开发时, 需要使用 https://github.com/electron-vite/vite-plugin-electron#vite-plugin-electronrenderer

他会给 arr = [node builtin modules, + electron] 配置

  • optimizeDeps.exclude(arr) 不在 node_modules/.vite 生成这个 pre-bundle 模块
  • 自己在 node_modules/.vite-plugin-electron-renderer 文件下生成这些模块的代码
  • 配置 resolve.alias 到 生成的 optimize 代码上

比如 node_modules/.vite-plugin-electron-renderer/fs.js

const M = require("fs");
export default M;
export const appendFile = M.appendFile;
export const appendFileSync = M.appendFileSync;
export const access = M.access;
export const accessSync = M.accessSync;
export const chown = M.chown;
export const chownSync = M.chownSync;
export const chmod = M.chmod;
export const chmodSync = M.chmodSync;
export const close = M.close;
export const closeSync = M.closeSync;
export const copyFile = M.copyFile;
export const copyFileSync = M.copyFileSync;
export const cp = M.cp;
export const cpSync = M.cpSync;
export const createReadStream = M.createReadStream;
export const createWriteStream = M.createWriteStream;
export const exists = M.exists;
export const existsSync = M.existsSync;
export const fchown = M.fchown;
export const fchownSync = M.fchownSync;
export const fchmod = M.fchmod;
export const fchmodSync = M.fchmodSync;
export const fdatasync = M.fdatasync;
export const fdatasyncSync = M.fdatasyncSync;
export const fstat = M.fstat;
export const fstatSync = M.fstatSync;
export const fsync = M.fsync;
export const fsyncSync = M.fsyncSync;
export const ftruncate = M.ftruncate;
export const ftruncateSync = M.ftruncateSync;
export const futimes = M.futimes;
export const futimesSync = M.futimesSync;
export const lchown = M.lchown;
export const lchownSync = M.lchownSync;
export const lchmod = M.lchmod;
export const lchmodSync = M.lchmodSync;
export const link = M.link;
export const linkSync = M.linkSync;
export const lstat = M.lstat;
export const lstatSync = M.lstatSync;
export const lutimes = M.lutimes;
export const lutimesSync = M.lutimesSync;
export const mkdir = M.mkdir;
export const mkdirSync = M.mkdirSync;
export const mkdtemp = M.mkdtemp;
export const mkdtempSync = M.mkdtempSync;
export const open = M.open;
export const openSync = M.openSync;
export const opendir = M.opendir;
export const opendirSync = M.opendirSync;
export const readdir = M.readdir;
export const readdirSync = M.readdirSync;
export const read = M.read;
export const readSync = M.readSync;
export const readv = M.readv;
export const readvSync = M.readvSync;
export const readFile = M.readFile;
export const readFileSync = M.readFileSync;
export const readlink = M.readlink;
export const readlinkSync = M.readlinkSync;
export const realpath = M.realpath;
export const realpathSync = M.realpathSync;
export const rename = M.rename;
export const renameSync = M.renameSync;
export const rm = M.rm;
export const rmSync = M.rmSync;
export const rmdir = M.rmdir;
export const rmdirSync = M.rmdirSync;
export const stat = M.stat;
export const statSync = M.statSync;
export const symlink = M.symlink;
export const symlinkSync = M.symlinkSync;
export const truncate = M.truncate;
export const truncateSync = M.truncateSync;
export const unwatchFile = M.unwatchFile;
export const unlink = M.unlink;
export const unlinkSync = M.unlinkSync;
export const utimes = M.utimes;
export const utimesSync = M.utimesSync;
export const watch = M.watch;
export const watchFile = M.watchFile;
export const writeFile = M.writeFile;
export const writeFileSync = M.writeFileSync;
export const write = M.write;
export const writeSync = M.writeSync;
export const writev = M.writev;
export const writevSync = M.writevSync;
export const Dir = M.Dir;
export const Dirent = M.Dirent;
export const Stats = M.Stats;
export const ReadStream = M.ReadStream;
export const WriteStream = M.WriteStream;
export const FileReadStream = M.FileReadStream;
export const FileWriteStream = M.FileWriteStream;
export const _toUnixTimestamp = M._toUnixTimestamp;
export const F_OK = M.F_OK;
export const R_OK = M.R_OK;
export const W_OK = M.W_OK;
export const X_OK = M.X_OK;
export const constants = M.constants;
export const promises = M.promises;

配置

// vite.config.ts
{
  resolve: {
    alias: [
      {find: /(node:)?fs/, replacement: 'node_modules/.vite-plugin-electron-renderer/fs.js'}
    ]
  }
}
  • 这样费老大劲, 是因为开发过程中使用的 esbuild, 而 esbuild transform API 缺少一种 external 机制
  • 相当于开发者直到在 runtime 如何 resolve 这些 deps, 手动生成这些代码

build

build 使用的是 rollup + @rollup/plugin-commonjs, 可以使用 rollup 的机制做 external
比如

// vite.config.ts
{
  build: {
    rollupOptions: {
      external: ['fs', 'http', ...]
    }
  }
}

@magicdawn
Copy link
Owner Author

magicdawn commented Jul 8, 2022

vite-plugin-monkey

react preamble code

import react from '@vitejs/plugin-react'
import { defineConfig, Plugin, ResolvedConfig } from 'vite'

// https://github.com/vitejs/vite/blob/v2.9.9/packages/plugin-react/src/index.ts#L324-L334
// https://github.com/vitejs/vite/blob/v2.9.9/packages/plugin-react/src/fast-refresh.ts#L29-L35
function viteReactPreamble(): Plugin {
  const virtualModuleId = 'virtual:vite-react-preamble'
  const resolvedVirtualModuleId = '\0' + virtualModuleId

  let resolvedConfig: ResolvedConfig

  return {
    name: 'vite-react-preamble', // required, will show up in warnings and errors
    configResolved(config) {
      resolvedConfig = config
    },

    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },

    load(id) {
      if (id === resolvedVirtualModuleId) {
        if (resolvedConfig.mode === 'development') {
          const preambleCode = react.preambleCode.replace(`__BASE__`, resolvedConfig.base || '/')
          return `
            ${preambleCode}
            ;console.log('[vite-react-preamble]: preamble loaded')
          `
        } else {
          return ''
        }
      }
    },
  }
}

build 输出 file:///path/to/xxx.user.js 方便安装

function printDistScriptFileUrl(): Plugin {
  let resolvedConfig: ResolvedConfig

  return {
    name: 'monkey:print-dist-script-file-url',
    configResolved(config) {
      resolvedConfig = config
    },
    closeBundle() {
      // @ts-ignore
      // const fileName = resolvedConfig.build.lib?.fileName?.()
      // const dir = resolvedConfig.build.outDir
      // const targetDir = path.join(__dirname, dir)
      // const targetFile = path.join(targetDir, fileName || '')

      const targetFile = path.join(__dirname, 'dist', `${pkg.name}.user.js`)
      const u = pathToFileURL(targetFile)
      console.log('File URL: %s', u)
    },
  }
}

不想写死的话

  • resolvedCongig.build.lib.fileName() 读取文件名 = {pkg.name}.user.js
  • resolvedConfig.build.outDir 读取, 默认 = dist

externalGlobals

举例

exterbalGlobals: { lodash: ['_', (version, name) => `https://unpkg.com/${name}@${version}`] }

version 是由 vite-plugin-monkey 在 node_modules 目录通过 require('lodash/package.json').version 得到的, 不一定与项目根目录使用的 lodash 版本一致, 比如在 monorepo + pnpm + react@18 的场景下, 碰到解析得到的 react 总是 16 (monorepo 下有的老项目在用 16)

可以通过自己处理好, 再传递给 vite-plugin-monkey

import monkey, { MonkeyOption, MonkeyUserScript } from 'vite-plugin-monkey'

// 手动处理 external global 的 version
type ExternalGlobals = NonNullable<NonNullable<MonkeyOption['build']>['externalGlobals']>
function processExternalGlobals(config: ExternalGlobals) {
  for (const [k, v] of Object.entries(config)) {
    if (!Array.isArray(v) || typeof v[1] !== 'function') continue

    const [globalVar, pathFn] = v
    const { version } = require(`${k}/package.json`)
    const newV = pathFn(version, k)

    config[k] = [globalVar, newV]
  }

  console.log(config)
  return config
}

export default defineConfig({
  plugins: [
    monkey({
      build: {
        externalGlobals: processExternalGlobals({
          lodash: ['_', (name, version) => `https://unpkg.com/${name}@${version}`]
        })
      }
    })
  ]
})

external helper

const UNPKG = 'https://unpkg.com/'
const scriptUrlFactory =
  (subpath = '') =>
  (name: string, version: string) => {
    if (subpath && !subpath.startsWith('/')) subpath = '/' + subpath
    return `${UNPKG}${name}@${version}${subpath}`
  }

const x: ExtrenalGlobals = {
  lodash: ['_', scriptUrlFactory()]
}

Repository owner deleted a comment from caoxiemeihao Jul 8, 2022
Repository owner locked and limited conversation to collaborators Jul 8, 2022
@magicdawn
Copy link
Owner Author

magicdawn commented Jul 19, 2022

Vite HMR

console.log('Loading mount', state.visible, root, import.meta.url)

const hot = import.meta.hot
if (hot) {
  hot.dispose(() => {
    console.log('Loading hot dispose', import.meta.url)
  })
  hot.accept(() => {
    console.log('Loading hot accepts', state.visible, root, hot.data, import.meta.url)
  })
}

image

  • 旧版本 dispose-callback 执行, 新版本代码执行, 旧版本 accept-callback 执行
  • callback 只在旧版本中执行, 这点需要注意.
  • 可以通过 dispost-callback 设置 import.meta.hot.data, 在新版本代码中读取 import.meta.hot.data

本地状态恢复

理解了 dispose & accept 总是在 old version 执行, new version 代码在 dispose 和 accept 中间执行, 就可以使用热更新保持本地变量了

import {proxy} from 'valtio'
let state = proxy({ some: 'state' })

let root: React.Root | undefined

if (import.meta.hot) {
  const hot = import.meta.hot

  // in new version, resume data from old version
  if (hot.data.root) root = hot.data.root
  if (hot.data.state) state = hot.data.state

  hot.dispose(() => {
    // save state when dispose old version
    hot.data.root = root
    hot.data.state = state
  })

  hot.accept(() => {
    // console.log('Loading hot accepts', import.meta.url)
  })
}

和下面 defineHotResume 相比

  • 优点: 直观
  • 缺点: 为了避免初始化开销, 比如 const state = import.meta.hot?.data.state || proxy(initialState), 代码需拆开

抽象成 defineHotResume 方法

import { ViteHotContext } from 'vite/types/hot'
type InitOf<T> = {
  [K in keyof T]: () => T[K] | undefined
}
function defineHotResume<T>(get: () => T, init: InitOf<T>, hot?: ViteHotContext): T {
  const ret: { [K in keyof T]?: T[K] | undefined } = {}

  if (hot) {
    // load in new
    if (hot.data) {
      Object.assign(ret, hot.data)
    }

    // save when dispose old
    hot.dispose(() => {
      Object.assign(hot.data, get())
    })
  }

  for (let k of Object.keys(init)) {
    if (typeof ret[k] === 'undefined') {
      ret[k] = init[k]?.()
    }
  }

  return ret as T
}

使用

const initialState = { visible: false }
type IState = typeof initialState

const { state, root } = defineHotResume<{ state: IState; root: Root | undefined }>(
  () => ({ state, root }),
  {
    state: () => proxy(initialState),
    root: () => undefined as Root | undefined,
  },
  import.meta.hot
)
  • 因为 import.meta 是文件关联的, 只能作为参数传递
  • 逻辑简单说即是: 优先从 import.meta.hot.data[key] 中取出热更新前的值, 如果为 undefined, 则调用 initk

@magicdawn
Copy link
Owner Author

magicdawn commented Jul 19, 2022

plugin react

react refresh boundary 判断规则: 所有的导出"像是"一个 React 组件, 即只要第一个字母大写即可.
https://github.com/vitejs/vite/blob/v3.0.2/packages/plugin-react/src/fast-refresh.ts#L87

除了通过命名骗过 plugin, 还可以自己写

if (import.meta.hot) {
  import.meta.hot.accept()
}
  • 可以加 callback
  • 必须使用全名 import.meta.hot.accept(() => { /* blabla */ }}), 使用 hot=import.meta.hot; hot.accept() 无效, 可能是用正则匹配还是啥的, 不是这个方法在起作用...

@magicdawn
Copy link
Owner Author

node builtin modules

e.g util

https://medium.com/@ftaioli/using-node-js-builtin-modules-with-vite-6194737c2cd2

只要设置 alias 即可

resolve: {
  alias: {
    util: 'rollup-plugin-node-polyfills/polyfills/util',
  }
}

@magicdawn
Copy link
Owner Author

rollup-plugin-cleanup

filter = createFilter(include, exclude)
(filter(name) && exts.indexOf(justExt(name)) > -1)

两层: filter + ext 白名单

使用

command === 'build' &&
      cleanup({
        extensions: ['js', 'jsx', 'mjs', 'ts', 'tsx'],
        include: [import.meta.dirname + '/src/**/*'],
      }),

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant