vite源码弱鸡版

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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容