Have you ever been frustrated by projects that are so complex that you have to wait half a day for each startup? Have you ever been angry at the delay in updating your browser after a code change? Don’t worry! With Vite, these problems can be solved.
Introduction to the
Vite(pronounced like [weɪt], fast), a browser-native ES Modules development server. It mainly consists of two parts:
- A development server that provides rich built-in features based on native ES modules, such as surprisingly fast module hot updates.
- A set of build instructions that use Rollup to package your code and that are pre-configured to output optimized static resources for production.
Concept of pre –
ES Module
ES Module is a Module system supported by the browser itself, which is supported by most mainstream browsers.
To use ES Module, the script tag needs to be marked with type=” Module “.
<script type="module" src="/index.ts"></script>
Copy the code
The browser will treat the import syntax as a request, as shown in the following example. When import App from ‘./ app.ts’ is executed, the browser will request the file app.ts according to the relative path to obtain the corresponding module contents.
ESBuild
ESBuild is a js packaging tool that supports Babel, compression, etc. It is fast (dozens of times faster than rollup etc.).
Why Vite
Compared to the old development mode (use webPack and other compilation and packaging tools), using Vite provides a better development experience in the following aspects:
- Faster start – The start phase does not perform any compilation operations other than dependent precompilation
- Faster compilation – Use ESBuild for compilation
- Faster hot updates — repackage bundle.js every time you update without having to analyze dependencies
Source code analysis
Vite’s Github repository is github.com/vitejs/vite
Start the API
Vite project when it starts, just perform Vite this directive, locating the packages/Vite/SRC/node/cli. Ts file, you can see Vite is carried out according to user’s incoming parameters createServer this API.
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanOptions(options) as ServerOptions
})
await server.listen()
Copy the code
CreateServer code located on the packages/vite/SRC/node/server/index. The ts.
Integration of configuration
First, the user-specific configuration is consolidated and assigned to the config variable.
const config = await resolveConfig(inlineConfig, 'serve'.'development')
Copy the code
ResolveConfig reads configuration items from the configuration file and merges them with the parameters passed in.
if(configFile ! = =false) {
const loadResult = await loadConfigFromFile(
configEnv,
configFile,
config.root,
config.logLevel
)
if (loadResult) {
config = mergeConfig(loadResult.config, config)
configFile = loadResult.path
configFileDependencies = loadResult.dependencies
}
}
Copy the code
Can pass vite — config XXX. Ts to specify the configuration file, or you will, in turn, looking for the root directory vite. Config. Js | vite. Config. MJS | vite. Config. Ts as the default configuration file.
if (configFile) {
resolvedPath = path.resolve(configFile)
} else {
const jsconfigFile = path.resolve(configRoot, 'vite.config.js')
if (fs.existsSync(jsconfigFile)) {
resolvedPath = jsconfigFile
}
if(! resolvedPath) {const mjsconfigFile = path.resolve(configRoot, 'vite.config.mjs')
if (fs.existsSync(mjsconfigFile)) {
resolvedPath = mjsconfigFile
}
}
if(! resolvedPath) {const tsconfigFile = path.resolve(configRoot, 'vite.config.ts')
if (fs.existsSync(tsconfigFile)) {
resolvedPath = tsconfigFile
}
}
}
if(! resolvedPath) { debug('no config file found.')
return null
}
Copy the code
Merge the read configuration file with the passed config file, and save the configuration file path.
config = mergeConfig(loadResult.config, config)
configFile = loadResult.path
Copy the code
Take out the plug-ins in the configuration content and sort them by plug-in type.
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(
rawUserPlugins
)
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
Copy the code
Execute the plug-in’s Config hook method.
for (const p of userPlugins) {
if (p.config) {
const res = await p.config(config, configEnv)
if (res) {
config = mergeConfig(config, res)
}
}
}
Copy the code
It is important to note that the config hook is executed after the plugin is integrated, so it will not take effect to process plugins in the Config hook when we write the plugin ourselves
The user configuration and Vite default plugin for integration, the default contain plug-ins can go to the packages/Vite/SRC/node/plugins/index. The ts resolvePlugins method in the view.
(resolved.plugins as Plugin[]) = await resolvePlugins(
resolved,
prePlugins,
normalPlugins,
postPlugins
)
Copy the code
At this point, the configuration items are basically unchanged and the configResolved hook in the plug-in is executed.
await Promise.all(userPlugins.map((p) = >p.configResolved? .(resolved)))Copy the code
Start the service
After getting the configuration, initialize HTTP and websocket services. HTTP is mainly used to start the local server, and Websocket is mainly used for hot update during development.
const middlewares = connect() as Connect.Server
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares)
const ws = createWebSocketServer(httpServer, config)
Copy the code
A series of middleware is then loaded, but I won’t go into details here.
middlewares.use(proxyMiddleware(httpServer, config))
// ...
middlewares.use(baseMiddleware(server))
/ /...
middlewares.use('/__open-in-editor', launchEditorMiddleware())
/ /...
middlewares.use('/__vite_ping'.(_, res) = > res.end('pong'))
/ /...
middlewares.use(decodeURIMiddleware())
/ /...
middlewares.use(servePublicMiddleware(config.publicDir))
/ /...
middlewares.use(transformMiddleware(server))
/ /...
middlewares.use(serveRawFsMiddleware())
/ /...
middlewares.use(serveStaticMiddleware(root, config))
/ /...
middlewares.use(indexHtmlMiddleware(server))
/ /...
Copy the code
Packages/vite/SRC/node/server/index. The ts startServer used to start the service, in port port 3000 by default, the default boot host 127.0.0.1.
async function startServer(server: ViteDevServer, inlinePort? :number,
isRestart: boolean = false
) :Promise<ViteDevServer> {
/ /...
return new Promise((resolve, reject) = > {
/ /...
httpServer.listen(port, options.host, () = > {
/ /...
});
});
}
Copy the code
The httpServer.listen method is re-executed, executing all of the plugin’s buildStart hooks and runOptimize dependencies pre-packaged.
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number. args:any[]) = > {try {
await container.buildStart({})
await runOptimize()
} catch (e) {
httpServer.emit('error', e)
return
}
returnlisten(port, ... args) })as any
Copy the code
Rely on prepackaging
There are two main reasons to rely on prepackaging:
-
CommonJS and UMD compatibility: During development, Vite’s development server treats all code as native ES modules. Therefore, Vite must first convert dependencies published as CommonJS or UMD to ESM.
-
Vite transforms ESM dependencies that have many internal modules into a single module to improve subsequent page loading performance.
Depend on the collection
By default, Vite looks for an HTML file in the project as an entry. If you want to change it, you can set optimizedeps. entries or build.rollupoptions in the configuration file. The input.
constexplicitEntryPatterns = config.optimizeDeps? .entriesconstbuildInput = config.build.rollupOptions? .inputif (explicitEntryPatterns) {
entries = await globEntries(explicitEntryPatterns, config)
} else if (buildInput) {
const resolvePath = (p: string) = > path.resolve(config.root, p)
if (typeof buildInput === 'string') {
entries = [resolvePath(buildInput)]
} else if (Array.isArray(buildInput)) {
entries = buildInput.map(resolvePath)
} else if (isObject(buildInput)) {
entries = Object.values(buildInput).map(resolvePath)
} else {
throw new Error('invalid rollupOptions.input value.')}}else {
entries = await globEntries('**/*.html', config)
}
Copy the code
Perform an esbuild compilation based on entries:
const plugin = esbuildScanPlugin(config, container, deps, missing, entries)
await Promise.all(
entries.map((entry) = >
build({
write: false.entryPoints: [entry],
bundle: true.format: 'esm'.logLevel: 'error'.plugins: [...plugins, plugin], ... esbuildOptions }) ) )Copy the code
There is an additional esBuild plugin for the esbuildScanPlugin to collect dependencies compared to build in production.
function esbuildScanPlugin(
config: ResolvedConfig,
container: PluginContainer,
depImports: Record<string.string>,
missing: Record<string.string>,
entries: string[]
) :Plugin {
// ...
return {
name: 'vite:dep-scan'.setup(build) {
// ...}}}Copy the code
If entry is an HTML file, the script tags in it will be extracted and transformed into a JS entry file. For example, the following script tags are found in the HTML file:
<script src="/main.tsx" />
Copy the code
Then it will be transformed into:
import "/main.tsx";
export default {};
Copy the code
The implementation code
build.onLoad(
{ filter: htmlTypesRE, namespace: 'html' },
async ({ path }) => {
let raw = fs.readFileSync(path, 'utf-8')
const isHtml = path.endsWith('.html')
const regex = isHtml ? scriptModuleRE : scriptRE
regex.lastIndex = 0
let js = ' '
let loader: Loader = 'js'
let match
while ((match = regex.exec(raw))) {
const [, openTag, htmlContent, scriptContent] = match
const content = isHtml ? htmlContent : scriptContent
const srcMatch = openTag.match(srcRE)
if (srcMatch) {
const src = srcMatch[1] || srcMatch[2] || srcMatch[3]
js += `import The ${JSON.stringify(src)}\n`
} else if (content.trim()) {
js += content + '\n'}}if(! js.includes(`export default`)) {
js += `\nexport default {}`
}
return {
loader,
contents: js
}
}
)
Copy the code
At each build, the path to the compile target is checked. If it comes from node_modules or if optimizedeps.include is set, it is stored in depImports for later prepackaging.
if (resolved.includes('node_modules') || include? .includes(id)) {if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
depImports[id] = resolved
}
return externalUnlessEntry({ path: id })
} else {
return {
path: path.resolve(resolved)
}
}
Copy the code
Here’s a trick: if you reference a package during development, show that the exported variables in the package do not exist, most likely because the package was not precompiledoptimizeDeps.include
Adding this package name to it will solve the problem temporarily
Rely on prepackaging
Through dependency analysis, we have the NPM packages that need to be pre-packaged.
{ deps, missing } = await scanImports(config)
Copy the code
Package the converted dependencies as esbuild entryPoints and place the output in node_modules/.vite:
const result = await build({
entryPoints: Object.keys(flatIdDeps),
bundle: true.format: 'esm'.external: config.optimizeDeps? .exclude,logLevel: 'error'.splitting: true.sourcemap: true.outdir: cacheDir,
treeShaking: 'ignore-annotations'.metafile: true,
define,
plugins: [ ...plugins, esbuildDepPlugin(flatIdDeps, flatIdToExports, config) ], ... esbuildOptions })Copy the code
One of the key points is the esbuildDepPlugin, which generates dependent entry content:
let contents = ' '
const data = exportsData[id]
const [imports, exports] = data
if(! imports.length && !exports.length) {
// cjs
contents += `export default require("${relativePath}"); `
} else {
if (exports.includes('default')) {
contents += `import d from "${relativePath}"; export default d; `
}
if (
data.hasReExports ||
exports.length > 1 ||
exports[0]! = ='default'
) {
contents += `\nexport * from "${relativePath}"`}}Copy the code
If the content of the dependent package is in CJS mode, it will be converted to ES6 form, assuming that the dependent package is called func and the entry content is as follows:
// func
const func = () = > { console.log('Hello')};module.exports = func;
Copy the code
After the transformation, the actual packaged entry content becomes:
export default require("./node_modules/func/index.js");
Copy the code
If it is an ES6 module, the compiled entry content simply imports and exports it, again using func as an example:
// func
const func = () = > { console.log('Hello')};export default func;
Copy the code
Real package entry content after conversion:
import d from "./node_modules/func/index.js";
export default d;
Copy the code
Prevent secondary prepacking
During the second boot, contentHash is generated based on the contents of the package.json project, and the generated hash value is compared with the hash value in node_modules/.vite/_metadata.json generated last time. If there is no change, it will not be packaged:
// Generate hash based on package.json content
function getDepHash(root: string, config: ResolvedConfig) :string {
// ...
let content = lookupFile(root, lockfileFormats) || ' '
// ...
return createHash('sha256').update(content).digest('hex').substr(0.8)}Copy the code
Compare hash changes:
const dataPath = path.join(cacheDir, '_metadata.json')
const mainHash = getDepHash(root, config)
const data: DepOptimizationMetadata = {
hash: mainHash,
browserHash: mainHash,
optimized: {}}// ...
prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'))
if (prevData && prevData.hash === data.hash) {
log('Hash is consistent. Skipping. Use --force to override.')
return prevData
}
Copy the code
conclusion
The main process of Vite project in the start-up stage is as follows:
Help wanted!!
Bytedance interactive Entertainment infrastructure team is hiring! There are posts in Beijing, Shenzhen and Hangzhou!
Who we are
Byte established the earliest front-end architecture team, which is now the largest and most professional. There are hundreds of front-end business team directly in hand, and the product DAU reaches 100 million level. There is no quarrel with PM and UI every day, and there is a good technical atmosphere.
Work at ordinary times
Responsible for front-end architecture design, implementation and optimization of large-scale complex business scenarios such as Douyin, Douyin Volcano edition and Live Broadcasting
- Responsible for the architecture of one or several technical scenarios such as PC, H5, Hybrid, App Native, BFF, RPC, etc.
- Formulate development specifications, build and optimize engineering system, improve development efficiency, quality and performance, and ensure stable operation of business;
- Identify problems with existing processes and structures and continuously improve them;
- Solve the technical pain points and difficulties encountered in the business;
- Follow up the industry’s cutting-edge technology, to ensure the advancement of team technology.
Job requirements
- 1. Bachelor degree or above, major in computer science or related; Good computer skills, familiar with data structure, network, etc.
- Have certain ability and experience of architecture and scheme design, have certain ability of scheme communication and promotion;
- Have some knowledge of back-end technology, familiar with a back-end language (Java/GO etc.);
- Experience in front-end engineering (e.g. webpack, rollup, etc.), Nodejs, rendering framework (e.g. React or Vue, etc.), middle and background building system is preferred.
- Experience in large website architecture is preferred; High technical enthusiasm and initiative is preferred.
pluses
- Participated in or led excellent open source projects;
- Have excellent technical blog, blog.
Interested parties can add my wechat to explain the purpose of the visit: