前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >vue 随记(6):构建的艺术

vue 随记(6):构建的艺术

作者头像
一粒小麦
发布2020-07-28 15:50:37
9620
发布2020-07-28 15:50:37
举报
文章被收录于专栏:一Li小麦一Li小麦

vite的构建艺术

Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。 ——https://zhuanlan.zhihu.com/p/150083887?from_voters_page=true

1. 打包vs解析

做过vue项目的人都知道,当项目越变越大,或者变成多页面应用时,热更新打包速度奇慢无比,每次保存都要几分钟。

尤雨溪在B站直播提到他最近在做的这工具 vite[1] ,一个实验性的no build的vue开发服务器。(这个小工具可以支持热更新,且不用预编译)。它的特性有:

•基于浏览器原生JS module功能(补白阅读:在浏览器中使用javascript module(译)),因而有更快的冷启动和热更新,整体速度与模块数量无关(无论项目多大,都是O(1)复杂度)•没有打包的过程,源码直接传输给浏览器,使用原生的 <script module> 语法进行引入,开发服务器拦截请求和对需要转换的代码进行转换。•实现了真正的按需编译。打开哪个页面,就解析哪些模块。•生产环境提供了 vite build 脚本进行打包,它基于 rollup 进行打包

vite构建的简单过程可以看到如下:

此过程可以理解为“只解析,不打包”。理论上支持react等任意前端开发框架。作者甚至在社交网络直言:他以后再不想用 webpack 了。

但是,因为JS module是“现代浏览器”支持功能,对于远古浏览器是不支持的。因此,只建议在开发环境下使用。对于生产环境,还是只能走打包那套。

2. mini版的实现

项目需求:基于现代浏览器实现一个mini版的vite工具。

代码语言:javascript
复制
npm init  -y

在项目中新建一个index.html。通过script type="module"引入main.js文件。

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vite-mini</title>
</head>
<body>
  <!-- 单页面容器 -->
  <div id="app"></div>
  <script type="module" src="src/main.js"></script>
</body>
</html>

新建src/main.js,直接输入console.log('main.js'),直接打开index.html,发现这种操作被同源策略阻止了。

代码语言:javascript
复制
Access to script at 'file:///Users/dangjingtao/Desktop/vite-min/src/main.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
index.html:12 GET file:///Users/dangjingtao/Desktop/vite-min/src/main.js net::ERR_FAILED

2.1 启服务

所以还是得起服务器。此时可以选用koa启服务。

代码语言:javascript
复制
npm i koa -S

新建server.js简单写一个服务:

代码语言:javascript
复制
//  ./server.js
// 用最传统的方式
const fs = require('fs');
const path = require('path');
const Koa = require('koa');

const app = new Koa();

app.use(async ctx => {
  const { request: { url, query } } = ctx;
  let content = '';
  if (url == '/') {
    content = fs.readFileSync('./index.html', 'UTF-8');
    ctx.type = 'text/html';
  } else if (url.endsWith('.js')) {
    // 获取绝对路径
    const p = path.resolve(__dirname, url.slice(1));
    content = fs.readFileSync(p, 'UTF-8');
    ctx.type = 'application/javascript';
  } else {

  }
  ctx.body = content;
});

app.listen(3001, err => {
  if (!err) {
    console.log('server started..');
  }
});

访问http://localhost:3001就看到引入main.js的html了。现在验证之。

在src下新建log.js写一个模块服务:log

代码语言:javascript
复制
const {log} = console;
export default log;

然后在main.js引用它:

代码语言:javascript
复制
import log from './log.js';
log('aaa');

再次访问网址,发现log被导入进来了。显然,现代浏览器显然不需要打包了。

2.2 支持导入vue框架

首先安装vue 3:

代码语言:javascript
复制
npm i vue@next -S
2.2.1 允许/@modules/vue语法(rewriteImport)

在做vue开发时,我们通常怎样使用的脚手架?

代码语言:javascript
复制
import { ref, watchEffect } from "vue"
console.log(ref)

let count = ref(0)

watchEffect(() => {
  console.log("监测到数据变化")
})

我们在main.js写入上述代码,发现报错(只支持路径模式):

因此想要支持导入vue,首先要改造import xx from 'vue'。正确应该是:

代码语言:javascript
复制
import xx from '/@modules/vue';

写一个匹配xx的方法,用来重写/@modules/vue这样的代码:

代码语言:javascript
复制
const rewriteImport = (content) => {
  return content.replace(/from ['"]([^'"]+)['"]/g, (s0, s1) => {
    // 只改写需要去node_module找的
    if (s1[0] !== '.' && s1[0] !== '/') {
      return `from '/@modules/${s1}'`
    }
    return s0;
  });
}

然后,修改js解析方法:

代码语言:javascript
复制
app.use(async ctx => {
  const { request: { url, query } } = ctx;
  let content = '';
  if (url == '/') {
    // 访问index.html
    // ...
  } else if (url.endsWith('.js')) {
    // ...
    // 1. 支持import xx from '/@modules/vue';
    content = rewriteImport(content);
  } else {

  }
  ctx.body = content;
});

然后再访问,发现代码被替换为想要的样子了:

小结:接下来只要有import语法的地方都需要调用这个方法“预编译”一下。

2.2.2 从module中找vue

但是页面依然报错。因为服务器生成了一个http://localhost:3001/@modules/vue的请求。

接下来便是拦截来自/@modules/的路由,去找node_modules内的依赖——找依赖一般看的就是:

1.先去node_modules文件夹下检索依赖名字2.找到对应依赖,看package.json,找到里面的module属性:

显然需要的就是这个文件。把它return 回去即可。

代码语言:javascript
复制
app.use(async ctx => {
  const { request: { url, query } } = ctx;
  let content = '';
  if (url == '/') {
    // 访问index.html
    // ...
  } else if (url.endsWith('.js')) {
    // ...
    // 1. 支持import xx from '/@modules/vue';
    content = rewriteImport(content);
  } else if (url.startsWith('/@modules/')) {
    // 2. 去node_module找依赖
    // prefix 是相关依赖在node_modules下的绝对路径。
    const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
    const module = require(prefix + '/package.json').module;
    const p = path.resolve(prefix, module);
    content = fs.readFileSync(p, 'UTF-8');
    ctx.type = 'application/javascript';
    content = rewriteImport(content);
  } else {

  }
  ctx.body = content;
});

再跑一下,发现控制台已经多了很多依赖了。

访问http://localhost:3001:

访问http://localhost:3001/@modules/vue

#### 2.2.3 屏蔽报错

各个请求是已经在network里发出去了。但是引入的vue作为前端代码依然还在报错。(报错原因:node环境全局变量process在浏览器端位undefined)。

这时可以用一个比较low的方式屏蔽错误。

代码语言:javascript
复制
app.use(async ctx => {
  const { request: { url, query } } = ctx;
  let content = '';
  if (url == '/') {
    // 访问index.html
    content = fs.readFileSync('./index.html', 'UTF-8');
    // 3.解决报错,把node环境参数发给前端:
    content = content.replace('<script', `
      <script>
        window.process = {env:{NODE_ENV:'DEV'}}
      </script>
      <script
    `);
    ctx.type = 'text/html';
  } else if (url.endsWith('.js')) {
    // ...
    // 1. 支持import xx from '/@modules/vue';
    content = rewriteImport(content);
  } else if (url.startsWith('/@modules/')) {
    // 2. 去node_module找依赖
    // prefix 是相关依赖在node_modules下的绝对路径。
    // ..
  } else {

  }
  ctx.body = content;
});

修改main.js,写一段代码:

代码语言:javascript
复制
import { ref, watchEffect } from "vue";

console.log(ref);

let count = ref(0);

watchEffect(() => {
  console.log("监测到数据变化", count.value);
})

setInterval(() => {
  count.value++;
}, 1000);

再次访问http://localhost:9001:

调用vue成功。

在vite中解析import语法用的是第三方模块es-module-lexer

2.3 支持.vue单文件

Main.js作为全局的入口,通常是这么写的:

代码语言:javascript
复制
import { createApp } from "vue";
import App from './App.vue';

createApp(App).mount('#app');

应当允许引入App.vue,期望有里面有这些内容:

代码语言:javascript
复制
<template>
  <div>
    <h2>count: {{ count }}</h2>
    <h2>double: {{ double }}</h2>
    <button @click="add">add</button>
  </div>
</template>
<script>
import { ref, computed } from "vue"
export default {
  setup() {
    const count = ref(1)
    const add = () => {
      count.value++
    }
    const double = computed(() => count.value * 2)
    return { count, double, add }
  },
}
</script>

解析vue文件的思路是:

•vue当中分为template和script两个标签。•把script拿出来解析js,把template拿出来解析模版。

观察vite的实现,发现vite是把style,script,template单独作为一个网络请求。借助vite的工具compiler-sfc可以实现。

代码语言:javascript
复制
npm i @vue/compiler-sfc -S
2.3.1 解析vue中的script

在server.js中再加一个else if:当import语句以.vue结尾时:

代码语言:javascript
复制
// ...
const compilerSfc = require('@vue/compiler-sfc');
// ...

app.use(async ctx => {
  const { request: { url, query } } = ctx;
  let content = '';
  console.log(url)
  if (url == '/') {
    // 访问index.html
    // ...
  } else if (url.endsWith('.js')) {
    // 支持import xx from '/@modules/vue';
    // ...
  } else if (url.startsWith('/@modules/')) {
    //  去node_module找依赖
    // ...

  } else if (url.endsWith('.vue')) {
    // 引入vue文件
    const p = path.resolve(__dirname, url.slice(1));
    let _content = compilerSfc.parse(fs.readFileSync(p, 'UTF-8'));
    console.log(_content)

  } else { }
  ctx.body = content;
});

这时打印_content值是:

代码语言:javascript
复制
{
  descriptor: {
    filename: 'component.vue',
    source: '<template>\n' +
      '  <div>\n' +
            // (略)...
      '</script>\n',
    template: {
      type: 'template',
      content: '\n' +
        '  <div>\n' +
        '    <h2>count: {{ count }}</h2>\n' +
        '    <h2>double: {{ double }}</h2>\n' +
        '    <button @click="add"></button>\n' +
        '  </div>\n',
      loc: [Object],
      attrs: {},
      map: [Object]
    },
    script: {
      type: 'script',
      content: '\n' +
        'import { ref, computed } from "vue"\n' +
        'export default {\n' +
        '  setup() {\n' +
        '    const count = ref(1)\n' +
        '    const add = () => {\n' +
        '      count.value++\n' +
        '    }\n' +
        '    const double = computed(() => count.value * 2)\n' +
        '    return { count, double, add }\n' +
        '  },\n' +
        '}\n',
      loc: [Object],
      attrs: {},
      map: [Object]
    },
    scriptSetup: null,
    styles: [],
    customBlocks: []
  },
  errors: []
}

发现不但有解析的内容,包括script,template的内容都打印出来了。我们需要的就是这段js。

代码语言:javascript
复制
    // 引入vue文件
    const p = path.resolve(__dirname, url.slice(1));
    let _content = compilerSfc.parse(fs.readFileSync(p, 'UTF-8'));

    ctx.type = 'application/javascript';
    content = rewriteImport(_content.descriptor.script.content)
      .replace('export default', 'const __script = ');

再刷新,看到app.vue的网络请求实际上变成了script里的那段js:

2.3.2 解析模板

App.vue还有template模板。需要把它抽离出来,解析为一个js,让它去生成html。

我们观察官方的vite项目,发现模板也是发起了一个请求,请求地址同样是App.vue,但是get参数不同(?type=template&t=时间戳)。时间戳是做缓存用的,不需要,我们保留template即可。

再然后,发现这是一个请求,所以不能用endsWith了,只能用indexOf。并且需要对type进行分类讨论。

拿到template -> 需要一个插件把template解析为render函数:

代码语言:javascript
复制
npm i @vue/compiler-dom -S

届时可以调用compileDom.compile方法,抓取里面的html。

综上:

代码语言:javascript
复制
  // ...
} else if (url.indexOf('.vue') > -1) {

  // 引入vue文件,分离get请求参数
  const p = path.resolve(__dirname, url.split('?')[0].slice(1))
  let _content = compilerSfc.parse(fs.readFileSync(p, 'UTF-8'));

  if (!query.type) {
    // 处理.vue中的script
    ctx.type = 'application/javascript';
    _content = rewriteImport(_content.descriptor.script.content)
      .replace('export default', 'const __script = ');

    content = `${_content}\n` +
      `import { render as __render } from "${url}?type=template"\n` +
      `__script.render = __render\n` +
      `export default __script\n`;
  } else if (query.type == 'template') {

    // 处理.vue中的模板template
    const template = _content.descriptor.template;
    const render = compileDom.compile(template.content, { mode: 'module' }).code;
    ctx.type = 'application/javascript';
    content = rewriteImport(render);

  }

}

再运行:

可见该部分内容已经被解析为js。

这时再运行:已经看到计数器了。

2.4 支持其它模块

实际上你需要解析其它许多内容,比如style,sass,less,typescript等等。实际上就是原来webpack中各种loader的功能

此处以样式为例。

在main.js中引入样式:

代码语言:javascript
复制
import { createApp } from "vue";
import App from './App.vue';
import style from 'App.css';

createApp(App).mount('#app');

src/App.css代码如下:

代码语言:javascript
复制
h2 {
  color: red;
}

思路就是在app.use中继续加else if。

代码语言:javascript
复制
 else if (url.endsWith('.css')) {
  const p = path.resolve(__dirname, url.slice(1));
  const _content = fs.readFileSync(p, 'UTF-8');
  ctx.type = 'text/css';
  content = _content;
}

再运行,你会发现,从ctx返回这段css是没卵用的。——思路还是返回一段js。通过js去创建全局的script标签。然后把样式塞进去。

代码语言:javascript
复制
else if (url.endsWith('.css')) {
    const p = path.resolve(__dirname, url.slice(1));
    let _content = fs.readFileSync(p, 'UTF-8');

    _content =
      `const css = '${_content.replace(/\n/g, '')}';\n` +
      `let link = document.createElement('style');\n` +
      `link.setAttribute('type','text/css');\n` +
      `link.innerHTML = css;\n` +
      `document.head.appendChild(link);\n` +
      `export default css;`

    ctx.type = 'application/javascript';
    content = _content;
  }

那么样式引入功能就实现了。所以你这里import的实际是一段js

你也许会说,那么多else if已经很难看了。但在vite的真实实践中,这是通过中间件“链”起来的。中间件版可自行实现。此处不多赘述了。

References

[1] vite: https://github.com/vuejs/vite

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • vite的构建艺术
    • 1. 打包vs解析
      • 2. mini版的实现
        • 2.1 启服务
        • 2.2 支持导入vue框架
        • 2.3 支持.vue单文件
        • 2.4 支持其它模块
        • References
    相关产品与服务
    消息队列 TDMQ
    消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档