Vite的介绍

514 阅读3分钟

Vite

  • Vite是一个面向现代浏览器的一个更轻、更快的web开发应用
  • 基于ECMAScript标准原生模块系统(ESM)实现 Vite项目依赖项
  • Vite
  • @vue/compiler-sfc

Vite的特点

  • 快速冷启动
  • 模块热更新
  • 按需编译
  • 开箱即用 vite serve

1608286660.jpg

vue-cli-service serve

1608286806.jpg

vite只有当文件请求时才会去编译相应的模块

  • vite只有在请求时才会去编译文件,文件修改也只会编译当前文件
  • webpack HMR会以这个文件为入口重新build一次,所有涉及的依赖都会被重新加载一遍

vite build

  • Rollup 打包
  • 动态导入

使用webpack的原因

  • 浏览器环境不支持模块化
  • 零散的模块文件会产生大量的http请求(http2可以解决通过复用链接)

开箱即用

  • Typescript内置支持
  • less/sass/stylus/postcss内置支持 需要单独安装依赖
  • jsx
  • Web Assembly

Vite的核心功能

vite在启动时,会把当前项目作为静态服务器的根目录,并拦截服务器的部分文件请求,当遇到浏览器不能识别的模块时会进行编译,通过websocked实现hmr

Vite的实现原理

静态web服务器

npm init -y

在package.json文件中设置name为vite-cli即脚手架的启动命令为vite-cli

首先启动一个web服务器

#!/usr/bin/env node
const Koa = require('koa')
const send = require('koa-send')

const app = new Koa()

app.use(async (ctx,next) => {
    await send(ctx, ctx.path, {
        root: process.cwd(),
        index: 'index.html'
    })
    await next()
})

app.listen(3000, () => {
    console.log('serve listen 3000')
})

使用npm link或yarn link 将这个脚手架工具安装到本地全局

npm link
# 启动
vite-cli

处理javascript请求

// stream转换成字符串
function streamToString(text) {
    return new Promise((resolve, reject) => {
        let chunks = []
        text.on('data', chunk => chunks.push(chunk))
        text.on('error', reject)
        text.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
    })
}

app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        //   ctx.body 是stream类型,需转换成字符串
        ctx.body = await streamToString(ctx.body)
    }
    await next()
})

加载第三方模块

首先需要将上面JavaScript代码中import的第三方包路径修改为'/@modules/',然后对第三方模块路径进行处理

// 处理第三方包路径  应该放在最前面
app.use(async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        let moduleName = ctx.path.substr(10)
        let modulePath = path.resolve(process.cwd(), 'node_modules', moduleName)
        let pkg = require(path.resolve(modulePath, 'package.json'))
        ctx.path = path.join('/node_modules', moduleName, pkg.module)
    }
    await next()
})
...
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        // ctx.body 是stream类型
        let text = await streamToString(ctx.body)
        // 修改第三方模块包的路径
        ctx.body = text.replace(/(from\s+['"])(?!.\/)/g, '$1/@modules/')
    }
    await next()
})

编译单文件组件

首先需要修改vue文件格式为以下格式

import {render as __render} from "/aa.vue?type=template"
 __script.render = __render
export default __script

第二次请求vue文件时,输出编译后的代码

// 编译文件
const build = async (ctx, next) => {
    // 编译单文件组件
    if (ctx.path.endsWith('.vue')) {
        const contents = await streamToString(ctx.body)
        const {
            descriptor
        } = compilerSfc.parse(contents)
        let code
        if (!ctx.query.type) {
            code = descriptor.script.content

            code = code.replace(/export\s+default\s+/g, 'const __script = ')
            code += `
            import {render as __render} from "${ctx.path}?type=template"
            __script.render = __render
            export default __script
            `

        } else if (ctx.query.type === 'template') {
            let template = compilerSfc.compileTemplate({
                source: descriptor.template.content,
                filename: descriptor.filename,
                id: ctx.path
            })
            code = template.code
        }
        ctx.body = stringToStream(code)
        ctx.type = 'application/javascript'
    }
    await next()
}

处理外部引入的css文件 首先需要将js代码中引入的css文件路径替换为xxx@import

app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        // ctx.body 是stream类型
        let text = await streamToString(ctx.body)
        // 修改第三方模块包的路径
        ctx.body = text.replace(/(from\s+['"])(?![.\/])/g, '$1/@modules/')
            .replace(/process\.env\.NODE_ENV/g, '"develop"')
            // css
            .replace(/(import\s+['"][\w\.\/]+\.css)(['"])/g,'$1?import$2')
    }
    await next()
})

然后把css代码添加到html样式中

...
if (ctx.path.endsWith('.css')) {
    const css = await streamToString(ctx.body)
    let code = `
        import {updateStyle} from '/build/util/style.js'
        const css = ${JSON.stringify(css)}
        updateStyle(css)
        export default css
    `
    ctx.body = stringToStream(code)
    ctx.type = 'application/javascript'
}
...
export function updateStyle(content,id){
    const style = document.createElement('style')
    style.setAttribute('type', 'text/css')
    style.innerHTML = content
    document.head.appendChild(style)
}

完整代码

入口文件

#!/usr/bin/env node

const Koa = require('koa')
const send = require('koa-send')
const path = require('path')
const {
    static,
    npmHandler,
    build
} = require('./middlewares/index.js')
const {
    streamToString
} = require('./util')

const app = new Koa()



// 处理第三方依赖
app.use(npmHandler)
// 处理静态资源
app.use(static)

app.use(build)
// 修改第三方模块依赖
app.use(async (ctx, next) => {
    if (ctx.type === 'application/javascript') {
        // ctx.body 是stream类型
        let text = await streamToString(ctx.body)
        // 修改第三方模块包的路径
        ctx.body = text.replace(/(from\s+['"])(?![.\/])/g, '$1/@modules/')
            .replace(/process\.env\.NODE_ENV/g, '"develop"')
            .replace(/(import\s+['"][\w\.\/]+\.css)(['"])/g,'$1?import$2')
    }
    await next()
})

app.listen(3000)
console.log('server listen http://localhost:3000')

middlewares

const send = require('koa-send')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const {
    streamToString,
    stringToStream
} = require('../util')

// 静态资源
const static = async (ctx, next) => {
    await send(ctx, ctx.path, {
        root: process.cwd(),
        index: 'index.html'
    })
    await next()
}
// 处理第三方依赖包
const npmHandler = async (ctx, next) => {
    if (ctx.path.startsWith('/@modules/')) {
        let moduleName = ctx.path.substr(10)
        let modulePath = path.resolve(process.cwd(), 'node_modules', moduleName)
        let pkg = require(path.resolve(modulePath, 'package.json'))
        ctx.path = path.join('/node_modules', moduleName, pkg.module)
    }
    await next()
}
// 编译文件
const build = async (ctx, next) => {
    // 编译单文件组件
    if (ctx.path.endsWith('.vue')) {
        const contents = await streamToString(ctx.body)
        const {
            descriptor
        } = compilerSfc.parse(contents)
        let code
        if (!ctx.query.type) {
            code = descriptor.script.content


            code = code.replace(/export\s+default\s+/g, 'const __script = ')
            code += `
            import {render as __render} from "${ctx.path}?type=template"
            __script.render = __render
            export default __script
            `

        } else if (ctx.query.type === 'template') {
            let template = compilerSfc.compileTemplate({
                source: descriptor.template.content,
                filename: descriptor.filename,
                id: ctx.path
            })
            code = template.code
            console.log(descriptor.filename)
        }
          ctx.body = stringToStream(code)
        ctx.type = 'application/javascript'
    } else if (ctx.path.endsWith('.css')) {

        const css = await streamToString(ctx.body)
        let code = `
         import {updateStyle} from '/build/util/style.js'
           const css = ${JSON.stringify(css)}
           updateStyle(css)
            export default css
        `
        ctx.body = stringToStream(code)
        ctx.type = 'application/javascript'
    }
    await next()
}

module.exports = {
    static,
    npmHandler,
    build
}

utils

const {
    Readable
} = require('stream')
// stream 转换成 string
function streamToString(text) {
    return new Promise((resolve, reject) => {
        let chunks = []
        text.on('data', chunk => chunks.push(chunk))
        text.on('error', reject)
        text.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
    })
}

function stringToStream(text) {
    const stream = new Readable()
    stream.push(text)
    stream.push(null)
    return stream
}

module.exports = {
    streamToString,
    stringToStream
}

style

export function updateStyle(content,id){
    const style = document.createElement('style')
    style.setAttribute('type', 'text/css')
    style.innerHTML = content
    document.head.appendChild(style)
}