文末
我一直觉得技术面试不是考试,考前背背题,发给你一张考卷,答完交卷等通知。
首先,技术面试是一个 认识自己 的过程,知道自己和外面世界的差距。
更重要的是,技术面试是一个双向了解的过程,要让对方发现你的闪光点,同时也要 试图去找到对方的闪光点,因为他以后可能就是你的同事或者领导,所以,面试官问你有什么问题的时候,不要说没有了,要去试图了解他的工作内容、了解这个团队的氛围。
前端面试题汇总
JavaScript
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
性能
linux
前端资料汇总
而在Vite之前,还有Snowpack也同样采用了No-Bundler构建方案。那么No-Bundler模式与传统老牌构建工具Webpack孰优孰劣呢?能否实现平滑迁移和完美取代?
下面就带着问题一起分析一下 Vite2、Snowpack3 和 Webpack5 吧!
Webpack
Webpack是近年来使用量最大,同时社区最完善的前端打包构建工具,5.x版本对构建细节进行了优化,某些场景下打包速度提升明显,但也没能解决之前一直被人诟病的大项目编译慢的问题,这也和Webpack本身的机制相关。
已经有很多文章讲解Webpack的运行原理了,本文就不再赘述,我们着重分析一下后起之秀。
Snowpack
什么是Snowpack?
首次提出利用浏览器原生ESM能力的工具并非是Vite,而是一个叫做Snowpack的工具。前身是@pika/web
,从1.x版本开始更名为Snowpack。
Snowpack在其官网是这样进行自我介绍的:“Snowpack是一种闪电般快速的前端构建工具,专为现代Web设计。 它是开发工作流程较重,较复杂的打包工具(如Webpack或Parcel)的替代方案。Snowpack利用JavaScript的本机模块系统(称为ESM)来避免不必要的工作并保持流畅的开发体验”。
Snowpack的理念是减少或避免整个bundle的打包,每次保存单个文件时,传统的JavaScript构建工具(例如Webpack和Parcel)都需要重新构建和重新打包应用程序的整个bundle。重新打包时增加了在保存更改和看到更改反映在浏览器之间的时间间隔。在开发过程中,Snowpack为你的应用程序提供unbundled server。每个文件只需要构建一次,就可以永久缓存。文件更改时,Snowpack会重新构建该单个文件。在重新构建每次变更时没有任何的时间浪费,只需要在浏览器中进行HMR更新。
再了解一下发明Snowpack的团队Pika,Pika团队有一个宏伟的使命:让Web应用提速90%:
为此,Pika团队开发并维护了两个技术体系:构建相关的Snowpack和造福大众的Skypack。
在这里我们简单聊一下Skypack的初衷,当前许多Web应用都是在不同NPM包的基础上进行构建的,而这些NPM包都被Webpack之类的打包工具打成了一个bundle,如果这些NPM包都来源于同一个CDN地址,且支持跨域缓存,那么这些NPM包在缓存生效期内都只需要加载一次,其他网站用到了同样的NPM包,就不需要重新下载,而是直接读取本地缓存。
例如,智联的官网与B端都是基于vue+vuex开发的,当HR在B端发完职位后,进入官网预览自己的公司对外主页都不用重新下载,只需要下载智联官网相关的一些业务代码即可。为此,Pika专门建立了一个CDN(Skypack)用来下载NPM上的ESM模块。
后来Snowpack发布的时候,Pika团队顺便发表了一篇名为《A Future Without Webpack》 的文章,告诉大家可以尝试抛弃Webpack,采用全新的打包构建方案,下图取自其官网,展示了bundled与unbundled之间的区别。
在HTTP/2和5G网络的加持下,我们可以预见到HTTP请求数量不再成为问题,而随着Web领域新标准的普及,浏览器也在逐步支持ESM。
源码分析
启动构建时会调用源码src/index.ts
中的cli方法,该方法的代码删减版如下:
import {command as buildCommand} from’./commands/build’;
exportasyncfunction cli(args: string[]) {
const cliFlags = yargs(args, {
array: [‘install’, ‘env’, ‘exclude’, ‘external’]
}) as CLIFlags;
if (cmd === ‘build’) {
await buildCommand(commandOptions);
return process.exit(0);
}
}
进入commands/build文件,执行大致逻辑如下:
exportasyncfunction build(commandOptions: CommandOptions): Promise {
// 读取config代码
// …
for (const runPlugin of config.plugins) {
if (runPlugin.run) {
// 执行插件
}
}
// 将 import.meta.env
的内容写入文件
await fs.writeFile(
path.join(internalFilESbuildLoc, ‘env.js’),
generateEnvModule({mode: ‘production’, isSSR}),
);
// 如果 HMR,则加载 hmr 工具文件
if (getIsHmrEnabled(config)) {
await fs.writeFile(path.resolve(internalFilESbuildLoc, ‘hmr-client.js’), HMR_CLIENT_CODE);
await fs.writeFile(
path.resolve(internalFilESbuildLoc, ‘hmr-error-overlay.js’),
HMR_OVERLAY_CODE,
);
hmrEngine = new EsmHmrEngine({port: config.devOptions.hmrPort});
}
// 开始构建源文件
logger.info(colors.yellow(‘! building source files…’));
const buildStart = performance.now();
const buildPipelineFiles: Record<string, FileBuilder> = {};
// 根据主 buildPipelineFiles 列表安装所有需要的依赖项,对应下面第三部
asyncfunction installDependencies() {
const scannedFiles = Object.values(buildPipelineFiles)
.map((f) =>Object.values(f.filesToResolve))
.reduce((flat, item) => flat.concat(item), []);
// 指定安装的目标文件夹
const installDest = path.join(buildDirectoryLoc, config.buildOptions.metaUrlPath, ‘pkg’);
// installOptimizedDependencies 方法调用了 esinstall 包,包内部调用了 rollup 进行模块分析及 commonjs 转 esm
const installResult = await installOptimizedDependencies(
scannedFiles,
installDest,
commandOptions,
);
return installResult
}
// 下面因代码繁多,仅展示源码中的注释
// 0. Find all source files.
// 1. Build all files for the first time, from source.
// 2. Install all dependencies. This gets us the import map we need to resolve imports.
// 3. Resolve all built file imports.
// 4. Write files to disk.
// 5. Optimize the build.
// “–watch” mode - Start watching the file system.
// Defer “chokidar” loading to here, to reduce impact on overall startup time
logger.info(colors.cyan(‘watching for changes…’));
const chokidar = awaitimport(‘chokidar’);
// 本地文件删除时清除 buildPipelineFiles 对应的文件
function onDeleteEvent(fileLoc: string) {
delete buildPipelineFiles[fileLoc];
}
// 本地文件创建及修改时触发
asyncfunction onWatchEvent(fileLoc: string) {
// 1. Build the file.
// 2. Resolve any ESM imports. Handle new imports by triggering a re-install.
// 3. Write to disk. If any proxy imports are needed, write those as well.
// 触发 HMR
if (hmrEngine) {
hmrEngine.broadcastMessage({type: ‘reload’});
}
}
// 创建文件监听器
const watcher = chokidar.watch(Object.keys(config.mount), {
ignored: config.exclude,
ignoreInitial: true,
persistent: true,
disableGlobbing: false,
useFsEvents: isFsEventsEnabled(),
});
watcher.on(‘add’, (fileLoc) => onWatchEvent(fileLoc));
watcher.on(‘change’, (fileLoc) => onWatchEvent(fileLoc));
watcher.on(‘unlink’, (fileLoc) => onDeleteEvent(fileLoc));
// 返回一些方法给 plugin 使用
return {
result: buildResultManifest,
onFileChange: (callback) => (onFileChangeCallback = callback),
async shutdown() {
await watcher.close();
},
};
}
exportasyncfunction command(commandOptions: CommandOptions) {
try {
await build(commandOptions);
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
process.exit(1);
}
if (commandOptions.config.buildOptions.watch) {
// We intentionally never want to exit in watch mode!
returnnewPromise(() => {});
}
}
所有的模块会经过install进行安装,此处的安装是将模块转换成ESM放在pkg目录下,并不是npm包安装的概念。
在Snowpack3中增加了一些老版本不支持的能力,如:内部默认集成Node服务、支持CSS Modules、支持HMR等。
Vite
什么是Vite?
Vite(法语单词“ fast”,发音为/vit/)是一种构建工具,旨在为现代Web项目提供更快,更精简的开发体验。它包括两个主要部分:
-
开发服务器,它在本机ESM上提供了丰富的功能增强,例如,极快的Hot Module Replacement(HMR)。
-
构建命令,它将代码使用Rollup进行构建。
随着vue3的推出,Vite也随之成名,起初是一个针对Vue3的打包编译工具,目前2.x版本发布面向了任何前端框架,不局限于Vue,在Vite的README中也提到了在某些想法上参考了Snowpack。
在已有方案上Vue本可以抛弃Webpack选择Snowpack,但选择开发Vite来造一个新的轮子也有Vue团队自己的考量。
在Vite官方文档列举了Vite与Snowpack的异同,其实本质是说明Vite相较于Snowpack的优势。
相同点,引用Vite官方的话:
Snowpack is also a no-bundle native ESM dev server that is very similar in scope to Vite。
不同点:
-
Snowpack的build默认是不打包的,好处是可以灵活选择Rollup、Webpack等打包工具,坏处就是不同打包工具带来了不同的体验,当前ESbuild作为生产环境打包尚不稳定,Rollup也没有官方支持Snowpack,不同的工具会产生不同的配置文件;
-
Vite支持多page打包;
-
Vite支持Library Mode;
-
Vite支持CSS代码拆分,Snowpack默认是CSS in JS;
-
Vite优化了异步代码加载;
-
Vite支持动态引入 polyfill;
-
Vite官方的 legacy mode plugin,可以同时生成 ESM 和 NO ESM;
-
First Class Vue Support。
第5点Vite官网有详细介绍,在非优化方案中,当A
导入异步块时,浏览器必须先请求并解析,A
然后才能确定它也需要公共块C
。这会导致额外的网络往返:
Entry —> A —> C
Vite通过预加载步骤自动重写代码拆分的动态导入调用,以便在A
请求时并行C
获取:
Entry —> (A + C)
可能C
会多次导入,这将导致在未优化的情况下发出多次请求。Vite的优化将跟踪所有import,以完全消除重复请求,示意图如下:
第8点的First Class Vue Support,虽然在列表的最后一项,实则是点睛之笔。
源码分析
Vite在启动时,如果不是中间件模式,内部默认会启一个http server。
exportasyncfunction createServer(
inlineConfig: InlineConfig = {}
): Promise {
// 获取 config
const config = await resolveConfig(inlineConfig, ‘serve’, ‘development’)
const root = config.root
const serverConfig = config.server || {}
// 判断是否是中间件模式
const middlewareMode = !!serverConfig.middlewareMode
const middlewares = connect() as Connect.Server
// 中间件模式不创建 http 服务,允许外部以中间件形式调用:https://Vitejs.dev/guide/api-javascript.html#using-the-Vite-server-as-a-middleware
const httpServer = middlewareMode
-
? null
- await resolveHttpServer(serverConfig, middlewares)
// 创建 websocket 服务
const ws = createWebSocketServer(httpServer, config)
// 创建文件监听器
const { ignored = [], …watchOptions } = serverConfig.watch || {}
const watcher = chokidar.watch(path.resolve(root), {
ignored: [‘/node_modules/’, ‘/.git/’, …ignored],
ignoreInitial: true,
ignorePermissionErrors: true,
…watchOptions
}) as FSWatcher
const plugins = config.plugins
const container = await createPluginContainer(config, watcher)
const moduleGraph = new ModuleGraph(container)
const closeHttpServer = createSeverCloseFn(httpServer)
const server: ViteDevServer = {
// 前面定义的常量,包含:config、中间件、websocket、文件监听器、ESbuild 等
}
// 监听进程关闭
process.once(‘SIGTERM’, async () => {
try {
await server.close()
} finally {
process.exit(0)
}
})
watcher.on(‘change’, async (file) => {
file = normalizePath(file)
// 文件更改时使模块图缓存无效
moduleGraph.onFileChange(file)
if (serverConfig.hmr !== false) {
try {
// 大致逻辑是修改 env 文件时直接重启 server,根据 moduleGraph 精准刷新,必要时全部刷新
await handleHMRUpdate(file, server)
} catch (err) {
ws.send({
type: ‘error’,
err: prepareError(err)
})
}
}
})
// 监听文件创建
watcher.on(‘add’, (file) => {
handleFileAddUnlink(normalizePath(file), server)
})
// 监听文件删除
watcher.on(‘unlink’, (file) => {
handleFileAddUnlink(normalizePath(file), server, true)
})
// 挂载插件的服务配置钩子
const postHooks: ((() => void) | void)[] = []
for (const plugin of plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server))
}
}
// 加载多个中间件,包含 cors、proxy、open-in-editor、静态文件服务等
// 运行post钩子,在html中间件之前应用的,这样外部中间件就可以提供自定义内容取代 index.html
postHooks.forEach((fn) => fn && fn())
if (!middlewareMode) {
// 转换 html
middlewares.use(indexHtmlMiddleware(server, plugins))
// 处理 404
middlewares.use((_, res) => {
res.statusCode = 404
res.end()
})
}
// errorHandler 中间件
middlewares.use(errorMiddleware(server, middlewareMode))
// 执行优化逻辑
const runOptimize = async () => {
if (config.optimizeCacheDir) {
// 将使用 ESbuild 将依赖打包并写入 node_modules/.Vite/xxx
await optimizeDeps(config)
// 更新 metadata 文件
const dataPath = path.resolve(config.optimizeCacheDir, ‘metadata.json’)
if (fs.existsSync(dataPath)) {
server._optimizeDepsMetadata = JSON.parse(
fs.readFileSync(dataPath, ‘utf-8’)
)
}
}
}
if (!middlewareMode && httpServer) {
// 在服务器启动前覆盖listen方法并运行优化器
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number, …args: any[]) => {
await container.buildStart({})
await runOptimize()
return listen(port, …args)
}) as any
httpServer.once(‘listening’, () => {
// 更新实际端口,因为这可能与初始端口不同
serverConfig.port = (httpServer.address() as AddressInfo).port
})
} else {
await runOptimize()
}
// 最后返回服务
return server
}
访问Vite服务的时候,默认会返回index.html:
处理 import 文件逻辑,在 node/plugins/importAnalysis.ts 文件内:
exportfunction importAnalysisPlugin(config: ResolvedConfig): Plugin {
const clientPublicPath = path.posix.join(config.base, CLIENT_PUBLIC_PATH)
let server: ViteDevServer
return {
name: ‘Vite:import-analysis’,
configureServer(_server) {
server = _server
},
async transform(source, importer, ssr) {
const rewriteStart = Date.now()
// 使用 es-module-lexer 进行语法解析
await init
let imports: ImportSpecifier[] = []
try {
imports = parseImports(source)[0]
最后
其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)
《前端开发四大模块核心知识笔记》
最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。