6
头图

I don’t need to say more about what Vite is. Friends who have used Vue must know that this article will use a very simple beggar version by handwriting Vite to learn about Vite basic implementation principle of Vite to the earliest version of ---b64ccf79500695fcf2ab9aeda905ba1e--- ( vite-1.0.0-rc.5 version, Vue version 3.0.0-rc.10 ) It is already the version of 3.x , why not directly refer to the latest version, because it is difficult to understand the source code of this relatively complete tool as soon as it comes up, anyway, the author can't, so we can start from the earliest Version to spy on the principle, friends with strong ability can ignore~

This article will be divided into two parts, the first one mainly discusses how to successfully run the project, and the second one mainly discusses hot updates.

Front-end test project

The front-end test project structure is as follows:

VueOptions Api ,不css语言、 ts js语言, So it is a very simple project, our goal is very simple, is to write a Vite service to make this project run!

Build basic services

vite The basic structure of the service is as follows:

First let's start a service, HTTP application framework we use connect :

 // app.js
const connect = require("connect");
const http = require("http");

const app = connect();

app.use(function (req, res) {
  res.end("Hello from Connect!\n");
});

http.createServer(app).listen(3000);

The next thing we need to do is to intercept various types of requests for different processing.

intercept html

The entry address of the project access is http://localhost:3000/index.html , so the first request received is the html file request, we temporarily return html the content of the file :

 // app.js
const path = require("path");
const fs = require("fs");

const basePath = path.join("../test/");
const typeAlias = {
  js: "application/javascript",
  css: "text/css",
  html: "text/html",
  json: "application/json",
};

app.use(function (req, res) {
  // 提供html页面
  if (req.url === "/index.html") {
    let html = fs.readFileSync(path.join(basePath, "index.html"), "utf-8");
    res.setHeader("Content-Type", typeAlias.html);
    res.statusCode = 200;
    res.end(html);
  } else {
    res.end('')
  }
});

Now the access page is definitely still blank, because the request initiated by the page main.js has not been processed yet. The content of main.js is as follows:

Intercept js requests

main.js request needs to be processed a little, because the browser does not support naked import, so we need to convert the naked import statement, convert import xxx from 'xxx' to import xxx from '/@module/xxx' , and then intercept the /@module request, obtain the module to be imported from node_modules and return it.

To resolve import statements we use es-module-lexer :

 // app.js
const { init, parse: parseEsModule } = require("es-module-lexer");

app.use(async function (req, res) {
    if (/\.js\??[^.]*$/.test(req.url)) {
        // js请求
        let js = fs.readFileSync(path.join(basePath, req.url), "utf-8");
        await init;
        let parseResult = parseEsModule(js);
        // ...
    }
});

The result of parsing is:

The parsing result is an array, the first item is also an array representing imported data, and the second item represents export, main.js no, so it is empty. se位置, ssse位置。

Next, we check that when the import source is not . or / , it is converted to the form of /@module/xxx :

 // app.js
const MagicString = require("magic-string");

app.use(async function (req, res) {
    if (/\.js\??[^.]*$/.test(req.url)) {
        // js请求
        let js = fs.readFileSync(path.join(basePath, req.url), "utf-8");
        await init;
        let parseResult = parseEsModule(js);
        let s = new MagicString(js);
        // 遍历导入语句
        parseResult[0].forEach((item) => {
            // 不是裸导入则替换
            if (item.n[0] !== "." && item.n[0] !== "/") {
                s.overwrite(item.s, item.e, `/@module/${item.n}`);
            }
        });
        res.setHeader("Content-Type", typeAlias.js);
        res.statusCode = 200;
        res.end(s.toString());
    }
});

Modification js string We use magic-string , from this simple example you should be able to see the magic of it, that is, even if the string has changed, the index calculated from the original string is used It's still correct to modify it, because the index is still relative to the original string.

You can see that vue has been successfully modified to /@module/vue .

Then we need to intercept /@module request:

 // app.js
const { buildSync } = require("esbuild");

app.use(async function (req, res) {
    if (/^\/@module\//.test(req.url)) {
        // 拦截/@module请求
        let pkg = req.url.slice(9);
        // 获取该模块的package.json
        let pkgJson = JSON.parse(
            fs.readFileSync(
                path.join(basePath, "node_modules", pkg, "package.json"),
                "utf8"
            )
        );
        // 找出该模块的入口文件
        let entry = pkgJson.module || pkgJson.main;
        // 使用esbuild编译
        let outfile = path.join(`./esbuild/${pkg}.js`);
        buildSync({
            entryPoints: [path.join(basePath, "node_modules", pkg, entry)],
            format: "esm",
            bundle: true,
            outfile,
        });
        let js = fs.readFileSync(outfile, "utf8");
        res.setHeader("Content-Type", typeAlias.js);
        res.statusCode = 200;
        res.end(js);
    }
})

We first obtained the package.json file of the package, the purpose is to find its entry file, and then read and use esbuild to convert, of course Vue Yes ES模块 However, some packages may not be available, so they are handled directly and uniformly.

intercept css requests

css There are two kinds of requests, one is from the link tag, the other is from the import 89bade3b1df1377b5116ae877738ed97---tag, ---8a0f034834c74bca9 css link css请求我们直接返回css即可, importcss的, ES模块只支持js , so we need to convert it to js type. The main logic is to manually insert css into the page, so we need to handle these two requests separately.

In order to distinguish import request, let's modify the code of the previous interception js , and add ?import query parameters to each import source:

 // ...
    // 遍历导入语句
    parseResult[0].forEach((item) => {
      // 不是裸导入则替换
      if (item.n[0] !== "." && item.n[0] !== "/") {
        s.overwrite(item.s, item.e, `/@module/${item.n}?import`);
      } else {
        s.overwrite(item.s, item.e, `${item.n}?import`);
      }
    });
//...

Don't forget to modify the place where you intercept /@module :

 // ...
let pkg = removeQuery(req.url.slice(9));// 从/@module/vue?import中解析出vue
// ...

// 去除url的查询参数
const removeQuery = (url) => {
  return url.split("?")[0];
};

In this way, requests for import will have a flag:

Then process the css request separately according to this flag:

 // app.js

app.use(async function (req, res) {
    if (/\.css\??[^.]*$/.test(req.url)) {
        // 拦截css请求
        let cssRes = fs.readFileSync(
            path.join(basePath, req.url.split("?")[0]),
            "utf-8"
        );
        if (checkQueryExist(req.url, "import")) {
            // import请求,返回js文件
            cssRes = `
                const insertStyle = (css) => {
                    let el = document.createElement('style')
                    el.setAttribute('type', 'text/css')
                    el.innerHTML = css
                    document.head.appendChild(el)
                }
                insertStyle(\`${cssRes}\`)
                export default insertStyle
            `;
            res.setHeader("Content-Type", typeAlias.js);
        } else {
            // link请求,返回css文件
            res.setHeader("Content-Type", typeAlias.css);
        }
        res.statusCode = 200;
        res.end(cssRes);
    }
})

// 判断url的某个query名是否存在
const checkQueryExist = (url, key) => {
  return new URL(path.resolve(basePath, url)).searchParams.has(key);
};

If it is import imported css then convert it to a response of type js and provide a create style The method of the page is executed immediately, then this css will be inserted into the page. Generally, this method will be injected into the page in advance.

If it is link labeled css request to directly return css .

Intercept vue requests

最后,就是Vue单文件的请求了,这个会稍微复杂一点, Vue单文件@vue/compiler-sfc3.0.0-rc.10版本,首先需要把Vue单文件的templatejsstyle三部分解析出来:

 // app.js
const { parse: parseVue } = require("@vue/compiler-sfc");

app.use(async function (req, res) {
    if (/\.vue\??[^.]*$/.test(req.url)) {
    // Vue单文件
    let vue = fs.readFileSync(
      path.join(basePath, removeQuery(req.url)),
      "utf-8"
    );
    let { descriptor } = parseVue(vue);
  }
})

Then parse the three parts separately, the template and css parts will be converted into a import request.

Process js section

 // ...
const { compileScript, rewriteDefault } = require("@vue/compiler-sfc");

let code = "";
// 处理js部分
let script = compileScript(descriptor);
if (script) {
    code += rewriteDefault(script.content, "__script");
}

The rewriteDefault method is used to convert export default into a new variable definition so that we can inject more data like:

 // 转换前
let js = `
    export default {
        data() {
            return {}
        }
    }
`

// 转换后
let js = `
    const __script = {
        data() {
            return {}
        }
    }
`

//然后可以给__script添加更多属性,最后再手动添加到导出即可
js += `\n__script.xxx = xxx`
js += `\nexport default __script`

Processing template section

 // ...
// 处理模板
if (descriptor.template) {
    let templateRequest = removeQuery(req.url) + `?type=template`;
    code += `\nimport { render as __render } from ${JSON.stringify(
        templateRequest
    )}`;
    code += `\n__script.render = __render`;
}

import dfa6605766e6cee7056b595c291eb4a7---语句, render函数挂载到__script上, type=template request, returns the compilation result of the template.

Processing style section

 // ...
// 处理样式
if (descriptor.styles) {
    descriptor.styles.forEach((s, i) => {
        const styleRequest = removeQuery(req.url) + `?type=style&index=${i}`;
        code += `\nimport ${JSON.stringify(styleRequest)}`
    })
}

Like templates, styles are also converted into a single request.

Finally export __script and return the data:

 // ...
// 导出
code += `\nexport default __script`;
res.setHeader("Content-Type", typeAlias.js);
res.statusCode = 200;
res.end(code);

__script其实就是Vue的组件选项对象,模板部分编译的结果就是组件render ,相当于把js and template sections into a complete component options object.

Handling template requests

When Vue single file request url exists type=template parameter, we compile the template and return:

 // app.js
const { compileTemplate } = require("@vue/compiler-sfc");

app.use(async function (req, res) {
    if (/\.vue\??[^.]*$/.test(req.url)) {
        // vue单文件
        // 处理模板请求
        if (getQuery(req.url, "type") === "template") {
            // 编译模板为渲染函数
            code = compileTemplate({
                source: descriptor.template.content,
            }).code;
            res.setHeader("Content-Type", typeAlias.js);
            res.statusCode = 200;
            res.end(code);
            return;
        }
        // ...
    }
})

// 获取url的某个query值
const getQuery = (url, key) => {
  return new URL(path.resolve(basePath, url)).searchParams.get(key);
};

Handling style requests

The style is the same as the previous style request we intercepted, and it also needs to be converted into js and then manually inserted into the page:

 // app.js
const { compileTemplate } = require("@vue/compiler-sfc");

app.use(async function (req, res) {
    if (/\.vue\??[^.]*$/.test(req.url)) {
        // vue单文件
    }
    // 处理样式请求
    if (getQuery(req.url, "type") === "style") {
        // 获取样式块索引
        let index = getQuery(req.url, "index");
        let styleContent = descriptor.styles[index].content;
        code = `
            const insertStyle = (css) => {
                let el = document.createElement('style')
                el.setAttribute('type', 'text/css')
                el.innerHTML = css
                document.head.appendChild(el)
            }
            insertStyle(\`${styleContent}\`)
            export default insertStyle
        `;
        res.setHeader("Content-Type", typeAlias.js);
        res.statusCode = 200;
        res.end(code);
        return;
    }
})

The logic of the style conversion to js is used in two places, so we can extract it into a function:

 // app.js
// css to js
const cssToJs = (css) => {
  return `
    const insertStyle = (css) => {
        let el = document.createElement('style')
        el.setAttribute('type', 'text/css')
        el.innerHTML = css
        document.head.appendChild(el)
    }
    insertStyle(\`${css}\`)
    export default insertStyle
  `;
};

Fix single file bare import issue

The js part in a single file can also import modules, so there will also be a problem of bare import. The processing method of bare import was introduced earlier, that is to replace the import source first, so the single file js After the partial parsing, we also need to perform a replacement operation. We first extract the replacement logic into a public method:

 // 处理裸导入
const parseBareImport = async (js) => {
  await init;
  let parseResult = parseEsModule(js);
  let s = new MagicString(js);
  // 遍历导入语句
  parseResult[0].forEach((item) => {
    // 不是裸导入则替换
    if (item.n[0] !== "." && item.n[0] !== "/") {
      s.overwrite(item.s, item.e, `/@module/${item.n}?import`);
    } else {
      s.overwrite(item.s, item.e, `${item.n}?import`);
    }
  });
  return s.toString();
};

Then process it immediately after compiling the js part:

 // 处理js部分
let script = compileScript(descriptor);
if (script) {
    let scriptContent = await parseBareImport(script.content);// ++
    code += rewriteDefault(scriptContent, "__script");
}

In addition, there will also be a bare import Vue in the compiled template code, which also needs to be processed:

 // 处理模板请求
if (
    new URL(path.resolve(basePath, req.url)).searchParams.get("type") ===
    "template"
) {
    code = compileTemplate({
        source: descriptor.template.content,
    }).code;
    code = await parseBareImport(code);// ++
    res.setHeader("Content-Type", typeAlias.js);
    res.statusCode = 200;
    res.end(code);
    return;
}

Handling static files

App.vue Introduced two pictures:

The result after compilation is:

ES模块 can only import js files, so the import of static files, the response result also needs to be js :

 // vite/app.js
app.use(async function (req, res) {
    if (isStaticAsset(req.url) && checkQueryExist(req.url, "import")) {
        // import导入的静态文件
        res.setHeader("Content-Type", typeAlias.js);
        res.statusCode = 200;
        res.end(`export default ${JSON.stringify(removeQuery(req.url))}`);
    }
})

// 检查是否是静态文件
const imageRE = /\.(png|jpe?g|gif|svg|ico|webp)(\?.*)?$/;
const mediaRE = /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/;
const fontsRE = /\.(woff2?|eot|ttf|otf)(\?.*)?$/i;
const isStaticAsset = (file) => {
  return imageRE.test(file) || mediaRE.test(file) || fontsRE.test(file);
};

import The imported static file processing is very simple, just export the url string of the static file as the default.

In this way, we will receive two requests for static files:

For simplicity, we consider static files that do not match any of the above rules, and use serve-static to serve static files:

 // vite/app.js
const serveStatic = require("serve-static");

app.use(async function (req, res, next) {
    if (xxx) {
        // xxx
    } else if (xxx) {
        // xxx
        // ...
    } else {
        next();// ++
    }
})

// 静态文件服务
app.use(serveStatic(path.join(basePath, "public")));
app.use(serveStatic(path.join(basePath)));

The middleware of the static file service is placed at the end, so that the routes that are not matched will come here. The effect of this step is as follows:

You can see that the page has been loaded.

In the next article, we will introduce the implementation of hot updates, See you later~


街角小林
883 声望771 粉丝