美文网首页
vite源码弱鸡版

vite源码弱鸡版

作者: 唐吉柯德 | 来源:发表于2021-04-03 17:24 被阅读0次

    cli.js

    /*
     * @Descripttion: my vite
     * @version 1.0.0
     * @Author: cfwang 
     * @Date: 2021-04-04 17:33:25 
     */
    console.log('my-vite');
    const koa = require('koa');
    // const send = require('koa-send');
    const path = require('path');
    const fs = require('fs');
    //vue compiler-sfc 
    //compilerSFC.parse获取代码字符串 
    //compilerSFC.compileTemplate生成render函数
    const compilerSFC = require('@vue/compiler-sfc');
    const createServer = require('./server');
    
    const app = new koa();
    //Node.js 进程的当前工作目录。
    const fileDir = process.cwd();
    //执行esmodule规范,将不是./或者../或者/开头的目录转换为/@modules+之前的路径
    function rewriteImport(content) {
        return content.replace(/ from ['"](.*)['"]/g, function(s1, s2) {
            if (s2.startsWith(".") || s2.startsWith("/")) {
                return s1;
            } else {
                const modulePkg = require(path.join(fileDir, 'node_modules', s2, 'package.json'));
                let truePath = path.join('./node_modules', s2, modulePkg.module);
                //let truePath= path.join('./node_modules', '.vite', `${s2}.js`);
                truePath = truePath.replace(/\\/g, '/');
                return ` from '/${truePath}'`;
            }
        })
    }
    
    //将/@modules开头的请求路径,在node_modules中找到其加载的真实路径,替换ctx.path
    app.use(async(ctx, next) => {
        // if (ctx.path.startsWith('/@modules')) {
        //     const moduleName = ctx.path.replace('/@modules', '');
        //     const modulePkg = require(path.join(fileDir, 'node_modules', moduleName, 'package.json'));
        //     ctx.path = path.join('./node_modules', moduleName, modulePkg.module);
        // }else 
        if (ctx.path.startsWith('/@vite')) {
            const moduleName = ctx.path.replace('/@vite', '');
            const clientCode = fs.readFileSync(path.join(__dirname, `${moduleName}.js`));
            ctx.type = "application/javascript";
            ctx.body = clientCode;
        }
        await next();
    })
    
    
    //加载首页文件index.html
    app.use(async(ctx, next) => {
        //ctx.path http://localhost:3000 index.html
        // await send(ctx, ctx.path, {
        //     //设置工作目录为查找文件夹
        //     root: fileDir,
        //     //设置查找文件
        //     index: 'index.html'
        // })
        if (ctx.path === '/') {
            const _path = path.join(fileDir, 'index.html');
            let indexContent = fs.readFileSync(_path, 'utf-8');
            indexContent += `<script type="module" src="/@vite/client"></script>
                <script>
                    process= {
                        env: {
                            NODE_ENV: 'development'
                        }
                    }
                </script>`;
            ctx.body = indexContent;
            ctx.type = 'text/html; charset=utf-8';
        }
        await next();
    })
    
    
    //解析.vue文件
    //1)通过compilerSFC.parse获取.vue文件代码,通过路径生成hashid挂载在descriptor上面,用于后面的热更新
    //2)通过compilerSFC.compileTemplate传入模版代码生成render函数
    //3)拼接import _sfc_main from "${ctx.path}?vue&type=script";获取script代码对象,将上面生成的render函数挂载上去
    //4)拼接热更新相关代码,收集热更新依赖信息
    //5)当query.type === 'script'直接返回descriptor.script.content
    //6)注:__VUE_HMR_RUNTIME__ 源码在@vue/runtime-core/dist/runtime-core.esm-bundler.js 428是vue框架在开发环境下挂载的全局API
    app.use(async(ctx, next) => {
                if (ctx.path.endsWith('.vue')) {
                    const _path = path.join(fileDir, ctx.path);
                    const { descriptor } = compilerSFC.parse(fs.readFileSync(_path, 'utf-8'), {
                        filename: _path,
                        sourceMap: true
                    });
                    let code = '';
                    if (ctx.query.type === 'script') {
                        code = descriptor.script.content;
                    } else {
                        descriptor.id = require('./hash')(ctx.path);
                        const render = compilerSFC.compileTemplate({
                            source: descriptor.template.content
                        })
                        code = render.code;
                        // code = descriptor.script.content.replace('export default', 'const __script=');
                        // console.log('descriptor.styles', descriptor.styles);
                        // if (descriptor.styles.length > 0) {
    
                        // }
                        code = `
                    import { createHotContext as __vite__createHotContext } from "/@vite/client";
                    import.meta.hot = __vite__createHotContext("${ctx.path}");
                    import _sfc_main from "${ctx.path}?vue&type=script";//${ctx.query.t? `&t=${ctx.query.t}`: ''}
                    export * from "${ctx.path}?vue&type=script";
                    ${code}
                    _sfc_main.render= render;
                    _sfc_main.__scopeId= ${JSON.stringify(`data-v-${descriptor.id}`)};
                    _sfc_main.__file = ${JSON.stringify(`${_path.replace(/\\/g, '/')}`)};
                    export default _sfc_main;
                    _sfc_main.__hmrId = ${JSON.stringify(descriptor.id)};
                    typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main);
                    import.meta.hot.accept(({ default: updated, _rerender_only }) => {
                        console.log('render enter', updated, ${ctx.query.t});
                        if (_rerender_only) {
                             __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render);
                        } else {
                            __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated);
                        }
                    })
                `
            }
            ctx.type = "application/javascript";
            ctx.body = code;
        }
        await next();
    })
    
    
    //当请求文件是.ts或者.js时添加content-type为application/javascript
    app.use(async(ctx, next) => {
        if (ctx.path.endsWith('.ts') || ctx.path.endsWith('.js')) {
            ctx.type = "application/javascript";
        }
        await next();
    })
    
    //当文件中的文件导入方式不符合esmodule规范时,调用rewriteImport修改请求路径
    app.use(async(ctx, next) => {
        if (!ctx.path.startsWith('/@vite') && ctx.type === "application/javascript") {
            if (ctx.path.endsWith('.vue')) {
                ctx.body = rewriteImport(ctx.body);
            } else {
                const _path = path.join(fileDir, ctx.path);
                let _content = fs.readFileSync(_path, 'utf-8');
                ctx.body = rewriteImport(_content);
            }
        }
        await next();
    })
    
    const webServer= createServer(app);
    webServer.listen(3000);
    
    console.log('server running @ https://localhost:3000')
    

    client.js

    /*
     * @Descripttion: websocket client用于本地node服务和网页进行通讯
     * @version 1.0.0
     * @Author: cfwang 
     * @Date: 2021-04-05 22:15:25 
     */
    let isFirstUpdate = true;
    const base = "/" || '/';
    const hotModulesMap = new Map();
    let pending = false;
    let queued = [];
    
    const socket = new WebSocket("wss://localhost:3000", 'vite-hmr');
    // Listen for messages
    socket.addEventListener('message', async({ data }) => {
        handleMessage(JSON.parse(data));
    })
    
    async function handleMessage(payload) {
        switch (payload.type) {
            case 'connected':
                console.log('client ws connected');
                setInterval(() => socket.send('ping'), 30000);
                break;
            case 'update':
                // if (isFirstUpdate) {
                //     window.location.reload();
                //     return;
                // }else {
                //     isFirstUpdate = false;
                // }
                // [{
                //     acceptedPath: "/src/App.vue",
                //     path: "/src/App.vue",
                //     timestamp: 1617763173350,
                //     type: "js-update"
                // }]
                payload.updates.forEach((update) => {
                    if (update.type === 'js-update') {
                        queueUpdate(fetchUpdate(update));
                    }
                });
                break;
        }
    }
    
    
    async function fetchUpdate({ path, acceptedPath, timestamp }) {
        //hotModulesMap里面查找页面注入createHotContext之后通过import.meta.hot.accept收集的路径和render函数信息
        const mod = hotModulesMap.get(path);
        if (!mod) {
            return;
        }
        const moduleMap = new Map();
        const isSelfUpdate = path === acceptedPath;
        const modulesToUpdate = new Set();
        if (isSelfUpdate) {
            modulesToUpdate.add(path);
        } else {
            for (const { deps }
                of mod.callbacks) {
                deps.forEach((dep) => {
                    if (acceptedPath === dep) {
                        modulesToUpdate.add(dep);
                    }
                });
            }
        }
    
        //在mod.callbacks中查找需要本次更新调用的callback
        // callbacks: [{
        //     deps: ['/src/App.vue'],
        //     fn: ([mod])=> cli注入render函数(mod)
        // }]
        const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
            return deps.some((dep) => modulesToUpdate.has(dep));
        });
    
        //modulesToUpdate  ['/src/App.vue']
        //解析modulesToUpdate,链接上面添加时间戳重新发起模块请求获取到的newMod 存储在moduleMap里面
        await Promise.all(Array.from(modulesToUpdate).map(async(dep) => {
                        const [path, query] = dep.split(`?`);
                        try {
                            //newMod
                            // {
                            //     default: {...},
                            //     render: ()=>{...}
                            // }
                            const newMod = await
                            import (
                                /* @vite-ignore */
                                base +
                                path.slice(1) +
                                `?import&t=${timestamp}${query ? `&${query}` : ''}`);
                moduleMap.set(dep, newMod);
            }
            catch (e) {
                warnFailedFetch(e, dep);
            }
        }));
        return () => {
            //qualifiedCallbacks
            //[{
            //     deps: ['/src/App.vue'],
            //     fn: ([mod])=> cli注入render函数(mod)
            // }]   
            for (const { deps, fn } of qualifiedCallbacks) {
                 // fn(deps.map((dep) => moduleMap.get(dep)));
                let depsPararm= deps.map((dep) => {
                    return  moduleMap.get(dep)
                })
                //depsPararm [newMod]
                fn(depsPararm);
            }
            // const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`;
            console.log(`[vite] hot updated: ${path}`);
        };
    }
    
    async function queueUpdate(p) {
        queued.push(p);
        if (!pending) {
            pending = true;
            await Promise.resolve();
            pending = false;
            const loading = [...queued];
            queued = [];
            console.log('queueUpdate called');
            (await Promise.all(loading)).forEach((fn) => fn && fn());
        }
    }
    
    //创建热更新的上下文环境
    const createHotContext = (ownerPath) => {
        const mod = hotModulesMap.get(ownerPath);
        //当已经存在render回调时,证明之前已经加载过一次这个模块,设置回调为空
        if (mod) {
            mod.callbacks = [];
        }
        function acceptDeps(deps, callback = () => { }) {
            const mod = hotModulesMap.get(ownerPath) || {
                id: ownerPath,
                callbacks: []
            };
            mod.callbacks.push({
                deps,
                fn: callback
            });
            hotModulesMap.set(ownerPath, mod);
        }
        const hot = {
            //创建了热更新上下文环境之后,在hotModulesMap中存储
            // {
            //     key: '/src/App.vue',
            //     value: {
            //         id: '/src/App.vue',
            //         callbacks: [{
            //              deps: ['/src/App.vue'],
            //              fn: ([mod])=> cli注入render函数(mod)
            //          }]
            //     }
            // }
            accept(deps, callback) {
                if (typeof deps === 'function' || !deps) {
                    acceptDeps([ownerPath], ([mod]) => deps && deps(mod));
                }
            }
        };
        return hot;
    };
    export { createHotContext };
    

    server.js

    /*
     * @Descripttion: 创建服务,监听本地文件变化,本地node服务和客户端进行通讯
     * @version 1.0.0
     * @Author: cfwang 
     * @Date: 2021-04-06 21:45:25 
     */
    //watch files change module
    const chokidar = require('chokidar');
    const http2 = require('http2');
    const WebSocket = require('ws');
    const fs = require('fs');
    const path = require('path');
    const fileDir = process.cwd();
    
    module.exports = function createServer(app) {
        //读取https本地私钥和证书
        const cert = fs.readFileSync(path.join(__dirname, './cert.pem'));
        //创建http2请求
        const webServer = http2.createSecureServer({ cert, key: cert, allowHTTP1: true }, app.callback());
        //创建websocket服务用于nodejs进程和浏览器通讯,用于热更新
        const wss = new WebSocket.Server({ noServer: true });
        webServer.on('upgrade', (req, socket, head) => {
            wss.handleUpgrade(req, socket, head, (ws) => {
                wss.emit('connection', ws, req)
            })
        })
        wss.on('connection', (socket) => {
            //发送链接请求
            socket.send(JSON.stringify({ type: 'connected' }))
        })
        wss.on('error', (e) => {
                console.log('wss error', e);
            })
            //监听文件状态
        const watcher = chokidar.watch(fileDir, {
                ignored: ['**/node_modules/**', '**/.git/**'],
                ignoreInitial: true,
                ignorePermissionErrors: true,
                disableGlobbing: true
            })
            //文件change事件
        watcher.on('change', async(file) => {
    
            const faPath = file.replace(`${process.cwd()}`, '').replace(/\\/g, '/');
            wss.clients.forEach((client) => {
                if (client.readyState === WebSocket.OPEN) {
                    client.send(JSON.stringify({
                        type: 'update',
                        updates: [{
                            type: 'js-update',
                            timestamp: Date.now(),
                            path: faPath,
                            acceptedPath: faPath
                        }]
                    }))
                }
            })
        })
        return webServer;
    }
    

    相关文章

      网友评论

          本文标题:vite源码弱鸡版

          本文链接:https://www.haomeiwen.com/subject/dpylkltx.html