實現一個vite(react版)

本文參考了vite的0.x版本的源碼,因爲0.x版本只支持vue和esm依賴,react並無esm包,因此這裏並進行了一些react支持方面的改造,供學習和交流 閱讀完本文,讀者應該可以瞭解:javascript

  • es module相關的知識
  • koa的基本使用
  • vite的核心原理
  • dev-server自動更新的原理
  • esbuild的基礎用法

預備知識

es modules

vite經過新版本瀏覽器支持的es modules來加載依賴
你須要把 type="module" 放到 script標籤中, 來聲明這個腳本是一個模塊css

<script type="module">
    // index.js能夠經過export導出模塊,也能夠在其中繼續使用import加載其餘依賴 
    import App from './index.js'
</script>
複製代碼


遇到import時,會自動發送一個http請求,來獲取對應模塊的內容,相應類型content-type=text/javascript html

基本架構

image.png

vite原理

首先咱們建立一下vite項目跑一下vue

yarn create vite my-react-app --template react
yarn dev
複製代碼

能夠看到:
image.png
瀏覽器發出了一個請求,請求了main.jsx
image.png
image.png
查看main.jsx的內容,咱們能夠發現,vite啓動的·服務器對引入模塊的路徑進行了處理,對jsx寫法也進行了處理,轉化成了瀏覽器能夠運行的代碼
繼續看
image.png
image.png
在client中,咱們看到了websocket的代碼,因此能夠理解爲vite服務器注入客戶端的websocket代碼,用來獲取服務器中代碼的變化的通知,從而達到熱更新的效果
綜上,咱們知道了vite服務器作的幾件事:java

  • 讀取本地代碼文件
  • 解析引入模塊的路徑並重寫
  • websocket代碼注入客戶端

代碼實現

本文的完整代碼在:github.com/yklydxtt/vi…
這裏咱們分五步:node

  1. 建立服務
  2. 讀取本地靜態資源
  3. 並重寫模塊路徑
  4. 解析模塊路徑
  5. 處理css文件
  6. websocket代碼注入客戶端

1.建立服務

建立index.jsreact

// index.js
const  Koa = require('koa');
const serveStaticPlugin = require('./plugins/server/serveStaticPlugin');
const rewriteModulePlugin=require('./plugins/server/rewriteModulePlugin');
const moduleResolvePlugin=require('./plugins/server/moduleResolvePlugin');

function createServer() {
    const app = new Koa();
    const root = process.cwd();
    const context = {
        app,
        root
    }
    const resolvePlugins = [
        // 重寫模塊路徑
        rewriteModulePlugin,
        // 解析模塊內容
        moduleResolvePlugin,
        // 配置靜態資源服務
        serveStaticPlugin,
    ]
    resolvePlugins.forEach(f => f(context));
    return app;
}
module.exports = createServer;
createServer().listen(3001);
複製代碼

這裏咱們使用koa建立了一個服務,
還註冊了三個插件,分別用來配置靜態資源,解析模塊內容,重寫模塊裏import其餘模塊路徑
咱們來分別實現這三個插件的功能 git

2.配置靜態資源,讀取本地代碼

const  KoaStatic = require('koa-static');
const path = require('path');

module.exports = function(context) {
    const { app, root } = context;
    app.use(KoaStatic(root));
    app.use(KoaStatic(path.join(root,'static')));
}
複製代碼

咱們建立一個static目錄
image.png
咱們用koa-static代理static目錄下的靜態資源
index.html中的內容以下:
image.png
執行github

node index.js
複製代碼

訪問loaclhost:3001
能夠到咱們剛剛寫的index.html的內容
image.png web

3.重寫模塊路徑

咱們來實現rewriteModulePlugin.js,做用是重寫import後的路徑
把這樣的路徑
import React,{ReactDOM } from 'es-react'
改成
import React,{ReactDOM } from '/__module/es-react'

// plugins/server/rewriteModulePlugin.js

const {readBody,rewriteImports}=require('./utils');


module.exports=function({app,root}){
    app.use(async (ctx,next)=>{
        await next();

        if (ctx.url === '/index.html') {
        		// 修改script標籤中的路徑
            const html = await readBody(ctx.body)
            ctx.body = html.replace(
              /(<script\b[^>]*>)([\s\S]*?)<\/script>/gm,
              (_, openTag, script) => {
                return `${openTag}${rewriteImports(script)}</script>`
              }
            )
          }

        if(ctx.body&&ctx.response.is('js')){
            //  修改js中的路徑
            const content=await readBody(ctx.body);
            ctx.body=rewriteImports(content,ctx.path);
        }
    });
}
複製代碼

實現一下rewriteImports函數和readBody

const path = require('path');
const { parse } = require('es-module-lexer');
const {Readable} =require('stream');
const resolve=require('resolve-from');
const MagicString = require('magic-string');

async function readBody(stream){
    if(stream instanceof Readable){
        return new Promise((resolve,reject)=>{
            let res='';
            stream.on('data',(data)=>res+=data);
            stream.on('end',()=>resolve(res));
            stream.on('error',(e)=>reject(e));
        })
    }else{
        return stream.toString();
    }
}

function rewriteImports(source,modulePath){
    const imports=parse(source)[0];
    const magicString=new MagicString(source);
    imports.forEach(item=>{
        const {s,e}=item;
        let id = source.substring(s,e);
        const reg = /^[^\/\.]/;
        const moduleReg=/^\/__module\//;
        if(moduleReg.test(modulePath)){
        		// 若是有/__module/前綴,就不用加了
            // 處理node_modules包中的js
            if(modulePath.endsWith('.js')){
                id=`${path.dirname(modulePath)}/${id}`
            }else{
                id=`${modulePath}/${id}`;
            }
            magicString.overwrite(s,e,id);
            return;
        }
        if(reg.test(id)){
        		// 對於前面沒有/__module/前綴的node_modules模塊的import,加上前綴
            id=`/__module/${id}`;
            magicString.overwrite(s,e,id);
        }
    });
    return magicString.toString();
}
複製代碼


4.讀取node_modules模塊內容

咱們來實現moduleResolvePlugin
由於咱們只代理了static目錄下的文件,因此須要讀取node_modules的文件,就須要處理一下
主要功能是解析到/__module前綴,就去node_modules讀取模塊內容

// ./plugins/server/moduleResolvePlugin.js
const { createReadStream } = require('fs');
const { Readable } = require('stream');
const { rewriteImports, resolveModule } = require('./utils');


module.exports = function ({ app, root }) {
  app.use(async (ctx, next) => {
    // koa的洋蔥模型
    await next();

		// 讀取node_modules中的文件內容
    const moduleReg = /^\/__module\//;
    if (moduleReg.test(ctx.path)) {
      const id = ctx.path.replace(moduleReg, '');
      ctx.type = 'js';
      const modulePath = resolveModule(root, id);
      if (id.endsWith('.js')) {
        ctx.body = createReadStream(modulePath);
        return;
      } else {
        ctx.body = createReadStream(modulePath);
        return;
      }
    }
  });
}
複製代碼

獲取node模塊的路徑:

// ./plugins/server/utils.js
const path = require('path');
const { parse } = require('es-module-lexer');
const {Readable} =require('stream');
const resolve=require('resolve-from');  // 這個包的功能相似require,返回值是require的路徑
const MagicString = require('magic-string');

// 返回node_modules依賴的絕對路徑
function resolveModule(root,moduleName){
    let modulePath;
    if(moduleName.endsWith('.js')){
        modulePath=path.join(path.dirname(resolve(root,moduleName)),path.basename(moduleName));
        return modulePath;
    }
    const userModulePkg=resolve(root,`${moduleName}/package.json`);
    modulePath=path.join(path.dirname(userModulePkg),'index.js');
    return modulePath;
}

複製代碼

至此,基本功能完成
在static下添加代碼:

// static/add.js
// 由於react沒有esm格式的包,因此這裏用es-react代替react
import  React,{ReactDOM } from 'es-react'
import LikeButton from './like_button.js';

const e = React.createElement;
const domContainer=document.getElementById("like_button_container");

ReactDOM.render(e(LikeButton), domContainer);

export default function add(a, b) {
    return a + b;
}
複製代碼
// static/like_button.js
import React from 'es-react'

const e = React.createElement;

export default class LikeButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = { liked: false };
  }

  render() {
    if (this.state.liked) {
      return 'You liked this.';
    }
    // 由於沒有用babel解析,因此這裏沒有用jsx,使用createElement的寫法
    return e(
      'button',
      { onClick: () => this.setState({ liked: true }) },
      'Like'
    );
  }
}



複製代碼

試着執行

node index.js
複製代碼

看到以下頁面
image.png

5.處理css文件

添加一個like_button.css

// ./static.like_button.css

h1{
  color: #ff0
}
複製代碼

在like_button.js中引入

// like_button.js

import './like_button.css';
複製代碼

刷新頁面會看到這樣的報錯:
image.png
es modules並不支持css,因此須要將css文件轉爲js.或者轉爲在link標籤中引入
在rewriteModulePlugin.js中添加處理css的判斷

const {readBody,rewriteImports}=require('./utils');


module.exports=function({app,root}){
    app.use(async (ctx,next)=>{
        await next();

        if (ctx.url === '/index.html') {
            const html = await readBody(ctx.body)
            ctx.body = html.replace(
              /(<script\b[^>]*>)([\s\S]*?)<\/script>/gm,
              (_, openTag, script) => {
                return `${openTag}${rewriteImports(script)}</script>`
              }
            )
          }

        if(ctx.body&&ctx.response.is('js')){
            const content=await readBody(ctx.body);
            ctx.body=rewriteImports(content,ctx.path);
        }

				// 處理css
        if(ctx.type==='text/css'){
          ctx.type='js';
          const code=await readBody(ctx.body);
          ctx.body=`
          const style=document.createElement('style');
          style.type='text/css';
          style.innerHTML=${JSON.stringify(code)};
          document.head.appendChild(style)
          `
        }
    });
}

複製代碼

從新啓動服務
image.png
樣式就有了
like_button.css的請求body變成了以下的樣子
image.png

6.實現熱更新

熱更新藉助websocket來實現
客戶端代碼

// ./plugins/client/hrmClient.js
const socket = new WebSocket(`ws://${location.host}`)

socket.addEventListener('message',({data})=>{
    const {type}=JSON.parse(data);
    switch(type){
        case 'update':
            location.reload();
            break;
    }
})
複製代碼

服務端添加一箇中間件hmrWatcherPlugin.js
做用是將hrmClient.js的內容發送給客戶端,並監聽代碼的變化,若是有變化,就經過ws發消息給客戶端

// ./plugins/server/hmrWatcherPlugin.js
const fs = require('fs');
const path = require('path');
const chokidar =require('chokidar');

module.exports = function ({ app,root }) {
    const hmrClientCode = fs.readFileSync(path.resolve(__dirname, '../client/hmrClient.js'))
    app.use(async (ctx, next) => {
        await next();
        將hrmClient.js的內容發送給客戶端
        if (ctx.url === '/__hmrClient') {
            ctx.type = 'js';
            ctx.body = hmrClientCode;
        }
            if(ctx.ws){
            		// 監聽本地代碼的變化
                const ws=await ctx.ws();
                const watcher = chokidar.watch(root, {
                    ignored: [/node_modules/]
                });
                watcher.on('change',async ()=>{
                        ws.send(JSON.stringify({ type: 'update' }));
                })
            }
    })
}
複製代碼

對rewriteModulePlugin.js中對index.html的處理進行修改

// plugins/server/rewriteModulePlugin.js

...
app.use(async (ctx,next)=>{
        await next();
        if (ctx.url === '/') {
            const html = await readBody(ctx.body);

            ctx.body = html.replace(
              /(<script\b[^>]*>)([\s\S]*?)<\/script>/gm,
              (_, openTag, script) => {
              // 添加對websock代碼的請求
                return `${openTag}import "/__hmrClient"\n${rewriteImports(script)}</script>`
              }
            )
          }
 ...
複製代碼

添加完成後重啓服務
對like_button.js進行修改,給button加一個感嘆號,保存

...
return e(
      'button',
      { onClick: () => this.setState({ liked: true }) },
      'Like!'
    );
...
複製代碼

能夠看到頁面有了更新,感嘆號有了
image.png

7.對jsx代碼處理

vite中是經過esbuild來處理的
在對rewriteImports進行改造,使用esbuild把jsx轉化成React.createElement的形式

// plugins/server/utils.js

function rewriteImports(source,modulePath){
 		// ...
        const code=esbuild.transformSync(source, {
        loader: 'jsx',
      }).code;
    const imports=parse(code)[0];
    const magicString=new MagicString(code);
    imports.forEach(item=>{
        const {s,e}=item;
        let id = code.substring(s,e);
        const reg = /^[^\/\.]/;
        const moduleReg=/^\/__module\//;
        if(moduleReg.test(modulePath)){
            if(modulePath.endsWith('.js')){
                id=`${path.dirname(modulePath)}/${id}`
            }else{
                id=`${modulePath}/${id}`;
            }
            magicString.overwrite(s,e,id);
            return;
        }
    // ...
}
複製代碼

image.png
jsx代碼也渲染出來了

尾聲

本文是經過閱讀vite源碼並加上一點本身的理解寫出來的,爲了方便你們理解,只實現了核心功能,細節方便沒有作過多說明,若是有錯誤但願獲得指正。
若是你們有收穫,請給我點個贊Thanks♪(・ω・)ノ

陌小路:Vite原理分析

相關文章
相關標籤/搜索