vite 打包 px 转 rem_css


vite是一个开发构建工具,开发过程中它利用浏览器native ES Module特性按需导入源码,预打包依赖。

特点: 启动快,更新快

一、按需加载

工作原理

1.浏览器利用es module imports,关键变化是index.html中的入口导入方式(开发阶段需要考虑浏览器兼容)

vite 打包 px 转 rem_javascript_02


所以,开发阶段不需要进行打包操作

本地文件使用 type=module,不支持本地路径,需要开启一个本地serve

2.第三方依赖模块预打包,并将导入地址修改为相对地址,从而统一所有请求为相对地址

预打包:第三方依赖提前打包存在node_modules中,程序启动时,直接从node_modules中去下载(浏览器只能加载相对路径)

vite 打包 px 转 rem_vite 打包 px 转 rem_03

二、解析

vite需要根据请求资源类型的不同做不同的解析工作,解析的最后结果是js对象
比如app.vue 文件

import HelloWorld from "./components/HelloWorld.vue";

解析为
const _script = {
	name: "App",
	components: {
		HelloWorld
	}
}
// template 部分转换为了一个模板请求,解析结果是一个渲染函数
import {render as _render} from "src/App.vue?type=template"
// 将解析得到的render函数设置到组件配置对象上
_script.render=_render
_script._hmrId="src/App.vue""
_script._file="/Users/...../src/App.vue"

vue文件 在服务端提前被vite编译之后,直接输出给浏览器,直接使用

三、vite手写实现

1、基础实现
基于koa框架,创建node服务器
基本配置:

// 一个node服务器,相当于devServer  基于koa
const Koa = require("koa");
const app = new Koa();
const fs = require("fs");

const path = require("path");

const compilerSfc = require("@vue/compiler-sfc"); // 编译器,得到一个组件配置对象,解析vue文件
const compilerDom = require("@vue/compiler-dom"); // 编译模板


app.use(async (ctx) => {
	// 处理文件解析
});

// 设置端口监听
app.listen(4002, () => {
  console.log("vite start");
});

2、解析html
利用fs对文件进行读取

// 首页
    ctx.type = "text/html";
    ctx.body = fs.readFileSync("./index.html", "utf8");

3、解析js
跟html解析区别在意文本类型的不同

// 响应js请求
    const p = path.join(__dirname, url);
    ctx.type = "text/javascript";
    ctx.body = fs.readFileSync(p, "utf-8");

4、第三方库的支持,比如vue

我们在代码里一般这样引入

import { createApp, h } from “vue”;

这里引入会报错

vite 打包 px 转 rem_css_04


表示期望路径是绝对路径或者相对路径

第三方的依赖我们都会安装在node_modules中,所以需要去node_modules中去寻找对应的js文件,因此我们需要对路径进行重写

比如vue,我们可以给它加一个别名----/@module/vue,
一个改写函数

// 重写导入,让引用的第三方文件,变成相对地址
function rewriteImport(content) {
  return content.replace(/ from ['|"](.*)['|"]/g, function (s0, s1) {
    // s0 - 匹配字符串
    // s1 - 分组内容
    // 看看是不是相对地址
    if (s1.startsWith(".") || s1.startsWith("../") || s1.startsWith("/")) {
      // 不处理
      return s0;
    } else {
      // 是外部文件
      return ` from '/@module/${s1}'`;
    }
  });
}

名称改写好之后,我们要去node_modules里面对应的js文件

比如vue,可以先看一下vue的位置

vite 打包 px 转 rem_学习_05


dist/vue.runtime.esm-bundler.js 是vue文件真正的路径

所以我们需要把

import XX from “vue” 变成 … from “baseurl + /node_moudle/dist/vue.runtime.esm-bundler.js”

// 获取模块名称,就是@module/ 后面的内容
    const moduleName = url.replace("/@module/", "");
    // 在node_module 中找到对应的模块
    const prefix = path.join(__dirname, "../node_modules", moduleName);
    // 要加载文件的地址
    const module = require(prefix + "/package.json").module;
    const filePath = path.join(prefix, module);
    const ret = fs.readFileSync(filePath, "utf-8");
    ctx.type = "text/javascript";
    ctx.body = rewriteImport(ret);

看一下处理后的路径地址,对filePath的打印

vite 打包 px 转 rem_css_06


如果要保证运行,还需要在index.html添加

// 这是vue的第三方包中的某个配置,这里相当于欺骗了浏览器
 <script>
      window.process = {
        env: {
          NODE_ENV: 'dev'
        }
      }
    </script>

5、单文件组件支持,.vue文件(sfc)
实现思路:*.vue文件 => template 模板 => render函数

  1. *.vue文件 => template 模板 需要借助 compiler-sfc
    这里主要提取js代码,生产render函数
  2. template => render函数 需要借助 compiler-dom
// 读取vue文件的内容
    const p = path.join(__dirname, url.split("?")[0]);
    const ret = compilerSfc.parse(fs.readFileSync(p, "utf-8")); // 将vue文件中的块进行解析,得到一个ast
    if (!query.type) {
      // 没有type说明是sfc(单文件组件)
      // 解析sfc,处理内部的js
      console.log(ret);
      // 获取脚本内容
      const scriptContent = ret.descriptor.script.content;
      ctx.type = "text/javascript";
      ctx.body = `
      ${rewriteImport(scriptContent)}
      // template解析转换为另一个请求单独做
      import { render as  __render } from '${url}?type=template'
      // __script.render = __render
      export default __render
      `;
    } else if (query.type === "template") {
      // 2.template => render 函数
      const tpl = ret.descriptor.template.content;
      // 编译为包含render的模块
      const render = compilerDom.compile(tpl, { mode: "module" }).code;
      ctx.type = "text/javascript";
      ctx.body = rewriteImport(render);
    }

对ret进行打印,看一下script和template

vite 打包 px 转 rem_css_07


对其中的content部分进行处理

6、处理.css文件

思路:创建style便签,将css文件的内容进行读取,添加到style标签中

const p = path.join(__dirname, url.split("?")[0]);
    const file = fs.readFileSync(p, "utf-8");
    // css 转化为js代码
    // 利用js创建style标签
    const content = `
    const css = "${file.replace(/\n/g, "")}"
    let link = document.createElement('style')
    link.setAttribute('type', 'text/css')
    document.head.appendChild(link)
    link.innerHTML = css
    export default css
    `;
    ctx.type = "application/javascript";
    ctx.body = content;

四、vite插件

vite插件可以扩展vite能力,比如解析用户自定义的文件输入,在打包代码前转义代码,或者查找第三方模块

1、 插件钩子

开发时,vite dev server 创建一个插件容器,按照Rollup调用创建钩子的规则请求各个钩子函数

在服务启动时调用一次:

optipns  替换或操控rollup选项(只在打包时有用,开发时为空)
buildStart 开始创建(只是一个信号)

每次有模块请求时都会调用:

resolveId 创建自定义确认函数,常用语句定位第三方依赖
load 创建自定义加载函数,可用于返回自定义的内容
transform 可用于装换已加载的模块内容

在服务器关闭时调用一次:

buildEnd
closeBundle

vite 特有钩子

config:修改Vite配置(可以配置别名)
configResolved :vite配置确认
configureServer:用于配置dev server
transformIndexHtml: 用于转换宿主页(可以注入或者删除内容)
handleHotUpdate:自定义HMR更新时调用

2、钩子调用顺序
config → configResolved → optipns → configureServer → buildStart → transform → load → resolveId → transformIndexHtml → vite dev server

3、插件顺序
别名处理 Alias
用户插件执行(如果设置 enforce : ‘pre’)
Vite 核心插件(plugin-vue)
用户插件执行(如果未设置 enforce)
Vite 构建插件
用户插件执行(如果设置 enforce : ‘post’)
Vite 构建后置插件

4、实现一个mock服务器—vite-plugin-mock

实现思路:

给开发服务器实例(concent)配置一个中间件,这个中间件可以存储用户配置接口映射信息,并提前处理输入请求,如果请求的url和路由表匹配则接管,按用户配置的handler返回结果

import path from "path";

let mockRouteMap = {};

function matchRoute(req) {
  let url = req.url;
  let method = req.method.toLowerCase();
  let routeList = mockRouteMap[method];

  return routeList && routeList.find((item) => item.path === url);
}

// 默认导出的插件工厂函数
export default function (options = {}) {
  // 获取mock文件入口,默认是index
  options.entry = options.entry || "./mock/index.js";
  // 转换为绝对路径
  if (!path.isAbsolute(options.entry)) {
    options.entry = path.resolve(process.cwd(), options.entry);
  }
  // 返回的插件
  return {
    configureServer: function ({ app }) {
      // 定义路由表
      const mockObj = require(options.entry);
      // 创建路由表
      createRoute(mockObj);

      // 定义中间件:路由匹配
      const middleware = (req, res, next) => {
        // 1.执行匹配过程
        let route = matchRoute(req);
        //2. 存在匹配,是一个mock请求
        if (route) {
          console.log("mock req", route.method, route.path);
          res.send = send;
          route.handler(req, res);
        } else {
          next();
        }
      };

      // 最终目标,给app注册一个中间件
      app.use(middleware);
    },
  };
}

function createRoute(mockConfList) {
  mockConfList.forEach((mockConf) => {
    let method = mockConf.method || "get";
    let path = mockConf.url;
    let handler = mockConf.response;
    // 路由对象
    let route = { path, method: method.toLowerCase(), handler };
    if (!mockRouteMap[method]) {
      mockRouteMap[method] = [];
    }
    console.log("create mock api");
    // 存入映射对象中
    mockRouteMap[method].push(route);
  });
}

// 实现一个send方法
function send(body) {
  let chunk = JSON.stringify(body);
  if (chunk) {
    chunk = Buffer.from(chunk, "utf-8");
    this.setHeader("Content-Length", chunk.length);
  }
  this.setHeader("Content-Type", "application/json");
  this.statusCode = 200;
  this.end(chunk, "utf8");
}