vite源码弱鸡版

2021-04-03  本文已影响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;
}
上一篇下一篇

猜你喜欢

热点阅读