9

写在开头

  • 由于 vite这个构建工具被用在了vue3上门,而且它的构建思路我觉得优于webpack,底层也是使用了esbuild,性能上更优
  • 那么为了照顾一些小伙伴之前没有学习过vite的,我们先来看看什么是vite

什么是vite

  • Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用,支持热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打包
  • vite的天然优势:

    • 快速冷启动服务器
    • 即时热模块更换(HMR)
    • 真正的按需编译

vite工作原理

  • 当声明一个 script 标签类型为 module 时
如:  <script type="module" src="/src/main.js"></script>
  • 浏览器就会像服务器发起一个GET http://localhost:3000/src/main.js请求main.js文件:
// /src/main.js:
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
  • 浏览器请求到了main.js文件,检测到内部含有import引入的包,又会对其内部的 import 引用发起 HTTP 请求获取模块的内容文件
  • 如: GET http://localhost:3000/@modules/vue.js
  • 如: GET http://localhost:3000/src/App.vue
  • 其Vite 的主要功能就是通过劫持浏览器的这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器渲染页面,vite整个过程中没有对文件进行打包编译,所以其运行速度比原始的webpack开发编译速度快出许多

简单实现vite

  • 由于代码量有一些大,我就不自己去写了,直接拿了别人的代码过来,原文地址是:
https://juejin.cn/post/689811...
  • 首先是koa启动监听端口,用于访问热更新服务
function createServer() {
    let app = new Koa()
    const context = {     // 直接创建一个上下文 来给不同的插件共享功能
        app,
        root: process.cwd() //执行node命令的那个命令路径
    }
    // 运行koa中间件(就是我们的vite插件)
    resolvePlugin.forEach(plugin => plugin(context))
    return app
}
createServer().listen(4000, () => {
})
  • 编写对应插件处理
  • 首先处理模块的引用,因为浏览器只有相对路径和绝对路径
这里readBody其实就是一个读取文件流的方法,封装过而已,看成普通的读取流方法即可
koa中间件处理
  • 首先处理重写路径,因为浏览器只有绝对路径和相对路径

        app.use(async (ctx, next) => {
            await next(); // 静态服务
            // 默认会先执行 静态服务中间件 会将结果放到 ctx.body
            // 需要将流转换成字符串 , 只需要处理js中的引用问题
            if (ctx.body && ctx.response.is('js')) {
                let r = await readBody(ctx.body); // vue => /@modules
                const result = rewriteImports(r);
                ctx.body = result;
            }
        })
    },
  • 重写完了路径后,需要拦截.vue文件和带@module(重写路径之前就是node_modules里面的文件)

    // 2. 拦截含有/@modules/vue的请求, 去node_modules引入对应的模块并返回
    ({ app, root }) => {
        const reg = /^\/@modules\//
        app.use(async (ctx, next) => {
            // 如果没有匹配到 /@modules/vue 就往下执行即可
            if (!reg.test(ctx.path)) {
                return next();
            }
            const id = ctx.path.replace(reg, '');

            let mapping = {
                vue: path.resolve(root, 'node_modules', '@vue/runtime-dom/dist/runtime-dom.esm-browser.js'),
            }
            const content = await fs.readFile(mapping[id], 'utf8');
            ctx.type = 'js'; // 返回的文件是js
            ctx.body = content;
        })
    },
  • 当解析处理完路径后,我们需要解析vue的模板文件,(如果是react的jsx代码,同理)
    // 3. 解析.vue文件
    ({ app, root }) => {
        app.use(async (ctx, next) => {
            if (!ctx.path.endsWith('.vue')) {
                return next();
            }
            const filePath = path.join(root, ctx.path);
            const content = await fs.readFile(filePath, 'utf8');
            // 引入.vue文件解析模板
            const { compileTemplate, parse } = require(path.resolve(root, 'node_modules', '@vue/compiler-sfc/dist/compiler-sfc.cjs'))
            let { descriptor } = parse(content);
            if (!ctx.query.type) {
                //App.vue
                let code = ''
                if (descriptor.script) {
                    let content = descriptor.script.content;
                    code += content.replace(/((?:^|\n|;)\s*)export default/, '$1const __script=');
                }
                if (descriptor.template) {
                    const requestPath = ctx.path + `?type=template`;
                    code += `\nimport { render as __render } from "${requestPath}"`;
                    code += `\n__script.render = __render`
                }
                code += `\nexport default __script`
                ctx.type = 'js';
                ctx.body = code
            }
            if (ctx.query.type == 'template') {
                ctx.type = 'js';
                let content = descriptor.template.content
                const { code } = compileTemplate({ source: content }); // 将app.vue中的模板 转换成render函数
                ctx.body = code;
            }
        })
    },

    // 4. 静态服务插件 实现可以返回文件的功能
    ({ app, root }) => {
        app.use(static(root))
        app.use(static(path.resolve(root, 'public')))
    }
]

function createServer() {
    let app = new Koa()
    const context = {     // 直接创建一个上下文 来给不同的插件共享功能
        app,
        root: process.cwd() // C:\Users\...\my-vite-vue3
    }

    // 运行中间件
    resolvePlugin.forEach(plugin => plugin(context))

    return app
}
  • 下面是两个工具函数:一个是流的读取,一个是重写路径的函数
//读取body方法
async function readBody(stream) {
    if (stream instanceof Readable) {
        return new Promise((resolve) => {
            let res = ''
            stream.on('data', function (chunk) {
                res += chunk
            });
            stream.on('end', function () {
                resolve(res)
            })
        })
    } else {
        return stream;
    }
}
  • 重写路径中间件
const resolvePlugin = [
    // 1. 重写引入模块路径前面加上/@modules/vue, 重写后浏览器会再次发送请求
    ({ app, root }) => {
        function rewriteImports(source) {
            let imports = parse(source)[0];
            let ms = new MagicString(source);
            if (imports.length > 0) {
                for (let i = 0; i < imports.length; i++) {
                    let { s, e } = imports[i];
                    let id = source.slice(s, e); // 应用的标识 vue  ./App.vue
                    // 不是./ 或者 /
                    if (/^[^\/\.]/.test(id)) {
                        id = `/@modules/${id}`;
                        ms.overwrite(s, e, id)
                    }
                }
            }
            return ms.toString();
        }
  
这样一个简单的vite就完成了

开始在react中使用

  • vite算是一个新的技术,而且在国内目前不够流行,为了避免踩坑,我们直接采用官方推荐的模板生成
npm init vite-app --template react
  • 生成模板完成后,执行命令启动项目
yarn
yarn dev
  • 这样一个react的项目就搭建好了,默认使用的是17.0.0版本的react,这样createElement方法再也不用从react里面导出了,我想这样jsx风格代码也会更容易被迁移到其他框架项目中
  "dependencies": {
    "react": "^17.0.0",
    "react-dom": "^17.0.0"
  },
  "devDependencies": {
    "vite": "^1.0.0-rc.13",
    "vite-plugin-react": "^4.0.0"
  }
  • 这个模板生成的是自带热更新的,相对比较简单,如果是有特殊需求,可以使用更多的plugin,在vite.config.js中设置
  • 默认的配置
// @ts-check
import reactPlugin from 'vite-plugin-react'

/**
 * @type { import('vite').UserConfig }
 */
const config = {
  jsx: 'react',
  plugins: [reactPlugin]
}

export default config

写在最后

  • 本文更多是在讲vite的实现原理,目前我还没有把它使用在生产环境中
  • 在我看来,vite如果生态能发展起来,可能我们就用不到wepback6这个版本了(当然未来不可猜测)
  • 通过阅读本文,你肯定能清楚了解vite的原理和react构建使用了,感觉不错的话,帮我点个赞/在看,关注一下【前端巅峰】公众号吧
参考资料:

PeterTan
14.4k 声望30k 粉丝