一切始于一个想法。。。
这个想法来源于这篇文章,A Future Without Webpack 好的想法,也需要天时地利人和。
此图来自 How Snowpack Works
Html中加载打包后的文件
-
启动: 打包完整的应用代码后启动
-
热更新 有更新的模块,以及依赖这个模块的模块,重新编译,打包,整体推动更新
-
模块管理 commonjs
通过esm加载js文件
-
启动 直接启动,用到才更新,然后,可以偷偷的在后台预热
-
热更新 有更新的模块,以及依赖这个模块的模块,重新编译,逐个推动更新
-
模块管理 ESM
思考一下,变量如果没有作用域,代码怎么写?你能坚持写多少行?
有了作用域,你就可以只关心变量在它自身上下文之间的关系。 在作用域外去共享你的变量怎么办呢?你需要放到外,还要保证顺序,你要在用它之前定义好它。像这样,
<script src="/tua.js"></script>
<script>
tua.start();
console.log('hello, tua!')
</script>
如果共享的变量相互依赖太多,你能维护多少个变量的顺序而不混乱?和没有作用域难度基本等同。
后来出现了模块化Modules/1.1.1 - CommonJS Spec Wiki。它给了我们一个方式去组织这些变量和函数。通过模块化,你可以把变量和函数合理的进行分组归类。再后来,出现了esm规范 ECMAScript® 2022 Language Specification,也就是我们现在熟知的import, export。
浏览器或node 遇到 import,可以知道加载哪些代码。你只需要提供一个入口文件,通过解析,就能循着import语句,顺藤摸瓜找到所有的代码。
ESM文件不能直接被浏览器所用,需要解析,创建模块实例。模块的加载是通过入口人间,找到整个模块实例的关系表。过程如下:
构建(查找下载解析)实例化 (链接exportsimport) 求值 (运行,变量赋值)
其中第二步的实例化, 首先在内存中指定位置给各个模块的export导出的变量或者函数,接着将模块中对应的import部分同样指向对应的export的内存地址。
入口要怎么加载,通过script标签告知浏览器:
<script src="main.js" type="module"></script>
注意,上面的解析过程中,浏览器主动发起请求import的文件。
- ES modules 是静态依赖,在执行模块解析和求值操作之前,就建立好了整个模块依赖关系表。
- ES modules 将模块声明算法拆分到各个阶段去执行。不阻塞主线程。
- ES modules 动态绑定
它是 Vite 的核心模块。
首先看下面这段代码,思考一下,这段代码在浏览器里执行会怎样?
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
代码要怎样才能正常的运行?
如果是这样的代码呢,是不是简单了些:
import { createApp } from '/node_modules/vue'
import App from '/src/App.vue'
import '/src/index.css'
createApp(App).mount('#app')
如果这些文件和HTML都在同一域下,我们就可以使用esm愉快的打开这个页面了,等等。。。 .vue 怎么办?
如果 App.vue 是:
import HelloWorld from '/src/components/HelloWorld.vue'
const __script = {
name: 'App',
components: {
HelloWorld
}
}
import { render as __render } from "/src/App.vue?type=template"
__script.render = __render
__script.__hmrId = "/src/App.vue"
__script.__file = "/Users/knight/workspace/vite_test/vite_test/src/App.vue"
export default __script
不要被文件扩展名欺骗,同样还有css:
import { updateStyle } from "/vite/client"
const css = "#app {\n font-family: Avenir, Helvetica, Arial, sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e50;\n margin-top: 60px;\n}\n"
updateStyle("\"2418ba23\"", css)
export default css
一切都是esm模块。
看来,我们需要一个服务器,能拦截这些请求,处理源码,返回 content-type:application/javascript;
的 ESM 模块的服务器。koa就是Vite所选的承担这一重任的服务端。
// src/server/index.ts 1.0.0 rc15
const app = new Koa<State, Context>()
const server = resolveServer(config, app.callback())
const watcher = chokidar.watch(root, {
ignored: [/node_modules/, /\.git/],
// #610
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 10
}
}) as HMRWatcher
const resolver = createResolver(root, resolvers, alias)
这里启动了一个koa服务器,处理所有的文件和请求,利用中间件作为插件来。那都用了那些中间件呢?
// 有删减
const resolvedPlugins = [
sourceMapPlugin,
moduleRewritePlugin,
htmlRewritePlugin,
// user plugins
vuePlugin,
cssPlugin,
enableEsbuild ? esbuildPlugin : null,
jsonPlugin,
assetPathPlugin,
webWorkerPlugin,
wasmPlugin,
serveStaticPlugin
]
resolvedPlugins.forEach((m) => m && m(context))
// 插件格式
import { ServerPlugin } from '.'
export const vitexxPlugin: ServerPlugin = ({ app }) => {
app.use(async (ctx, next) => {
// 插件逻辑
return next()
})
}
这里主要看下 moduleRewritePlugin,负责模块的重写
- 将命名的模块导入重写为
/@modules/:id
请求,例如。“vue” => “/@modules/vue” - 重写包含HMR代码的文件(参考
import.meta.hot
)到* 注入import.meta.hot
并跟踪HMR边界接受白名单。* - 在重写过程中还跟踪 importer/importee 关系。* 该图被HMR插件用来对文件变化进行分析。*
app.use(async (ctx, next) => {
await next()
if (ctx.status === 304) {
return
}
// 我们在所有其他中间件完成后再进行js重写。
// 这使我们能够对用户中间件产生的javascript进行后处理。
// 无论原始文件的扩展名是什么。
const publicPath = ctx.path
if (
ctx.body &&
ctx.response.is('js') &&
!isCSSRequest(ctx.path) &&
!ctx.url.endsWith('.map') &&
!resolver.isPublicRequest(ctx.path) &&
// skip internal client
// 跳过内部客户端, 过滤掉不处理
publicPath !== clientPublicPath &&
//需要重写vue文件中的<script>/<template>部分
!((ctx.path.endsWith('.vue') || ctx.vue) && ctx.query.type === 'style')
) {
const content = await readBody(ctx.body)
// 用路径和字符串内容当key
const cacheKey = publicPath + content
// ? t
const isHmrRequest = !!ctx.query.t
if (!isHmrRequest && rewriteCache.has(cacheKey)) {
debug(`(cached) ${ctx.url}`)
// 返回缓存中的内容
ctx.body = rewriteCache.get(cacheKey)
} else {
await initLexer
// 动态导入可能包含无扩展的路径。
// (.e.g import(runtimePathString))
// 所以我们需要在我们进行 hmr 分析之前,对importer进行规范化处理,确保它包含扩展名。
// 另一方面,静态导入可以保证包含扩展名。
// 因为它们肯定都经过了模块重写。
const importer = removeUnRelatedHmrQuery(
resolver.normalizePublicPath(ctx.url)
)
ctx.body = rewriteImports(
root,
content!,
importer,
resolver,
ctx.query.t
)
if (!isHmrRequest) {
rewriteCache.set(cacheKey, ctx.body)
}
}
} else {
debug(`(skipped) ${ctx.url}`)
}
})
这里的debug信息可以看出路径怎么转换的:
vite:rewrite "vue" --> "/@modules/vue?import" +3ms
vite:rewrite "./App.vue" --> "/src/App.vue" +2ms
vite:rewrite "./index.css" --> "/src/index.css?import" +0ms
- 有缓存,返回缓存中内容。
// 用路径和字符串内容当key
const cacheKey = publicPath + content
// ? t
const isHmrRequest = !!ctx.query.t
if (!isHmrRequest && rewriteCache.has(cacheKey)) {
debug(`(cached) ${ctx.url}`)
// 返回缓存中的内容
ctx.body = rewriteCache.get(cacheKey)
}
- 分析依赖,获取内容,缓存之 动态导入可能包含无扩展的路径。比如import(runtimePathString), 所以我们需要在我们进行 hmr 分析之前,对importer进行规范化处理,确保它包含扩展名。另一方面,静态导入可以保证包含扩展名。因为它们肯定都经过了模块重写。
await initLexer
const importer = removeUnRelatedHmrQuery(
resolver.normalizePublicPath(ctx.url)
)
ctx.body = rewriteImports(
root,
content!,
importer,
resolver,
ctx.query.t
)
if (!isHmrRequest) {
rewriteCache.set(cacheKey, ctx.body)
}
在 rewriteImports 方法中使用 es-module-lexer 来进行词法分析。并且将最终已经 replace 模块路径的结果赋值给 ctx.body
if (imports.length || hasHMR || hasEnv) {
const s = new MagicString(source)
let hasReplaced = false
// 这里找引用此模块的模块列表, 找出它影响到的模块
const prevImportees = importeeMap.get(importer)
const currentImportees = new Set<string>()
importeeMap.set(importer, currentImportees) // 更新影响到的模块
for (let i = 0; i < imports.length; i++) { // 遍历 import
const { s: start, e: end, d: dynamicIndex } = imports[i]
let id = source.substring(start, end)
let hasLiteralDynamicId = false
console.log(id, dynamicIndex)
if (dynamicIndex >= 0) {
const literalIdMatch = id.match(/^(?:'([^']+)'|"([^"]+)")$/)
if (literalIdMatch) {
hasLiteralDynamicId = true
id = literalIdMatch[1] || literalIdMatch[2]
}
}
if (dynamicIndex === -1 || hasLiteralDynamicId) {
// do not rewrite external imports
if (isExternalUrl(id)) {
continue
}
const resolved = resolveImport(
root,
importer,
id,
resolver,
timestamp
)
if (resolved !== id) {
debug(` "${id}" --> "${resolved}"`)
s.overwrite(
start,
end,
// 由于lexer的分析结果对于动态导入的情况会包含外层的引号,所以这里我们需要手动添加,否则最终的结果将不存在引号导致报错
hasLiteralDynamicId ? `'${resolved}'` : resolved
)
hasReplaced = true
}
// save the import chain for hmr analysis
const importee = cleanUrl(resolved)
if (
importee !== importer &&
// no need to track hmr client or module dependencies
importee !== clientPublicPath
) {
currentImportees.add(importee)
debugHmr(` ${importer} imports ${importee}`)
ensureMapEntry(importerMap, importee).add(importer)
}
} else if (id !== 'import.meta') {
console.warn(
chalk.yellow(`[vite] ignored dynamic import(${id}) in ${importer}.`)
)
}
}
if (hasHMR) {
debugHmr(`rewriting ${importer} for HMR.`)
rewriteFileWithHMR(root, source, importer, resolver, s)
hasReplaced = true
}
if (hasEnv) {
debug(` injecting import.meta.env for ${importer}`)
s.prepend(
`import __VITE_ENV__ from "${envPublicPath}"; ` +
`import.meta.env = __VITE_ENV__; `
)
hasReplaced = true
}
// 因为importees可能因为编辑而发生了变化。
// 检查我们是否需要从某些进口商那里删除这个importees。
if (prevImportees) {
prevImportees.forEach((importee) => {
if (!currentImportees.has(importee)) {
const importers = importerMap.get(importee)
if (importers) {
importers.delete(importer)
}
}
})
}
if (!hasReplaced) {
debug(` nothing needs rewriting.`)
}
return hasReplaced ? s.toString() : source
}
上面的代码主要做了两件事:
- 遍历所有imports依赖,建立依赖关系,解析真实路径,返回代码
- 删掉老依赖
获取真实路径部分是 resolveImport。
直接将裸模块名称解析为其入口路径,以便从它那里的相对的导入(包括源码地图的URL)可以正常工作。 eg, import from 'vue' 。 处理裸模块。从模块的 package.json 中找到entry字段并且返回,这里 Vue 的 entry 是 '@vue/shared/dist/shared.esm-bundler.js', 由于 Vite 在预优化时对所有 package.json 中的 dependencies 模块进行了预优化,所以返回的是统一 optimize 后的路径。
if (bareImportRE.test(id)) {
id = `/@modules/${resolveBareModuleRequest(root, id, importer, resolver)}`
} else {
// 1. relative to absolute 相对路径变成绝对路径
// ./foo -> /some/path/foo
let { pathname, query } = resolver.resolveRelativeRequest(importer, id)
// 2. 标准化路径,兼容不同的操作系统
pathname = resolver.normalizePublicPath(pathname)
// 3. mark non-src imports 记录没有query参数且后缀名不是js jsx vue .mjs 的操作。例如 import './index.css' import png from 'xxx.png' 在后面加上 import query
if (!query && path.extname(pathname) && !jsSrcRE.test(pathname)) { // 不是模板 && 有扩展名 && 没有query
query += `?import`
}
id = pathname + query
}
// 4. 通过添加时间戳来强制重新获取脏的导入数据
if (timestamp) {
const dirtyFiles = hmrDirtyFilesMap.get(timestamp)
const cleanId = cleanUrl(id)
if (dirtyFiles && dirtyFiles.has(cleanId)) {
// 1. 标志是来自脏文件的更新
id += `${id.includes(`?`) ? `&` : `?`}t=${timestamp}`
} else if (latestVersionsMap.has(cleanId)) {
// 2. 这个文件以前是热更新的,有一个更新的版本
id += `${id.includes(`?`) ? `&` : `?`}t=${latestVersionsMap.get(cleanId)}`
console.log('latestVersionsMap:::', id, timestamp)
}
}
return id
参考: https://vite-design.surge.sh/ 面向未来的前端构建工具 - vite - 知乎 Vite 2.0 预构建源码解析 - 知乎 https://zhuanlan.zhihu.com/p/371363712 https://www.qiyuandi.com/zhanzhang/zonghe/13757.html How Snowpack Works 聊聊 ESM、Bundle 、Bundleless 、Vite 、Snowpack - SegmentFault 思否 Vite源码解析(三)之热更新篇 - 知乎
React 18 发布计划 坑都占好了 https://github.com/reactwg/react-18
https://juejin.cn/post/6971311349267709966
https://eslint.org/blog/2021/06/whats-coming-in-eslint-8.0.0
https://blog.rust-lang.org/2021/05/11/edition-2021.html
https://developer.mozilla.org/en-US/docs/Web/CSS/inset
/* <length> values */
inset: 10px; /* value applied to all edges */
inset: 4px 8px; /* top/bottom left/right */
inset: 5px 15px 10px; /* top left/right bottom */
inset: 2.4em 3em 3em 3em; /* top right bottom left */
/* <percentage>s of the width (left/right) or height (top/bottom) of the containing block */
inset: 10% 5% 5% 5%;
/* Keyword value */
inset: auto;
/* Global values */
inset: inherit;
inset: initial;
inset: unset;