概览
主要功能
- 静态 Web 服务器
- 第三方模块加载
- SFC 文件编译
准备
服务端初始化
1 2 3 4 5
| mkdir vite-cli pnpm init mkdir src touch src/index.js
|
安装依赖
server 目录结构
1 2 3 4 5 6 7
| ├── node_modules │ ├── koa -> .pnpm/koa@2.15.3/node_modules/koa │ └── koa-send -> .pnpm/koa-send@5.0.1/node_modules/koa-send ├── package.json ├── pnpm-lock.yaml └── src └── index.js
|
client 目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ├── index.html ├── index.js ├── node_modules │ ├── @vue │ │ ├── reactivity -> ../.pnpm/@vue+reactivity@3.4.27/node_modules/@vue/reactivity │ │ ├── runtime-core -> ../.pnpm/@vue+runtime-core@3.4.27/node_modules/@vue/runtime-core │ │ ├── runtime-dom -> ../.pnpm/@vue+runtime-dom@3.4.27/node_modules/@vue/runtime-dom │ │ └── shared -> ../.pnpm/@vue+shared@3.4.27/node_modules/@vue/shared │ └── vue -> .pnpm/vue@3.4.27/node_modules/vue ├── package.json ├── pnpm-lock.yaml └── src ├── App.vue ├── main.js └── views └── ChildCom.vue
|
开始搭建静态 Web 服务器
我们搭建的 vue-cli
是基于 Node
的命令行工具, 指定 Node
环境的安装位置
1 2 3 4 5 6 7 8 9 10 11 12 13
| #!/usr/bin/env node const koa = require('koa'); const send = require('koa-send');
app.use(async (ctx, next) => { await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' }); await next(); })
app.listen(3000); console.log('服务已运行在 localhost:3000');
|
1 2 3 4 5
| { ... "bin": "src/index.js" }
|
测试
1 2 3 4
| ╰─ node src/index.js ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with EACCES: src/index.js SFCwn src/index.js EACCES
|
设置执行权限
使用 pnpm link
链接到全局, 可以在全局使用 vite-cli
会在打开的路径开启一个静态服务器
1 2 3 4 5 6
| ╰─ pnpm link --global Progress: resolved 17, reused 17, downloaded 0, added 0, done WARN link:/Users/Tony/Sync/Code/Nodejs/Vite/vite-cli/server has no binaries
/Users/Tony/Library/pnpm/global/5: + vite-cli 1.0.0 <- ../../../../Sync/Code/Nodejs/Vite/vite-cli/server
|
已生成 vite-cli
命令行
![img_0523215843](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
客户端测试
在 client
目录下使用 vite-cli
会开启一个静态服务器
打开服务器地址, 页面显示正常
1 2 3 4 5 6 7
| // client/index.html ... <body> HelloWorld <div id="app"></div> <script type="module" src="/src/main.js"></script> </body>
|
![img_0523213306.webp](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
控制台报错是正常情况
![img_0523213217.webp](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
浏览器无法识别从 node_modules
导入的第三方模块, 而默认路径都包含 "/"
, 浏览器没识别到, 所以抛出了异常
1 2 3 4 5 6 7 8
| import { createApp } from 'vue'
import { createApp } from '/@modules/vue.js'
http:
|
浏览器加载第三方模块
修改第三方模块引入路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
const streamToString = (stream, encoding = 'uft-8') => new Promise((resolve, reject) => { const chunks = []; stream.on('data', chunk => chunks.push(chunk)); stream.on('end', () => resolve(Buffer.concat(chunks).toString(encoding))); stream.on('error', error => reject(error)); })
...
app.use(async (ctx, next) => { if (ctx.type === 'application/javascript') { const content = await streamToString(ctx.body);
ctx.body = content. replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/') } await next(); })
|
启动后发现路径引入路径已经发生了变化, 但是还找不到模块引入的真实路径
![img_0523230810.webp](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
加载第三方模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const path = require('path'); ...
app.use(async (ctx, next) => { if (ctx.path.startsWith('/@modules/')) { const moduleName = ctx.path.slice(10); const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json'); const pkg = require(pkgPath);
ctx.path = path.join('/node_modules', moduleName, pkg.module); } await next(); })
|
模块加载成功
![img_0524010258.webp](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
可以看见无法识别 SFC
, 需要编译 template
模板
![img_0524011714.webp](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
响应体格式是一个字节流, 浏览器无法处理, 默认只会下载, 还需要将 SFC
文件编译后, 将格式类型设置为 'application/javascript'
![img_0524011847.webp](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
模板编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
const compilerSFC = require('@vue/compiler-sfc'); const { Readable } = require('stream'); ...
const stringToStream = (text) => { const stream = new Readable(); stream.push(text); stream.push(null); return stream; }
app.use(async (ctx, next) => { if (ctx.path.endsWith('.vue')) { const content = await streamToString(ctx.body); const { descriptor } = compilerSFC.parse(content);
let code;
if (!ctx.query.type) { console.log(ctx.query.type); code = descriptor.script.content; code = code.replace(/export\s+default\s+/g, 'const _script = '); code += ` import { render as _render } from '${ctx.path}?type=template'; _script.render = _render; export default _script; ` } else if (ctx.query.type === 'template') { const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content }); code = templateRender.code; } ctx.type = 'application/javascript'; ctx.body = stringToStream(code); } await next(); })
|
模板已经渲染, 浏览器没有 process
, 被阻断了; 就差这一步
![img_0524025236.webp](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
1 2
| const EMPTY_OBJ = !!(process.env.NODE_ENV !== "production") ? Object.freeze({}) : {};
|
将判断是否生产环境这段逻辑, 直接替换为开发环境 "development" !== "production"
1 2 3 4 5
| ... ctx.body = content. replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/'). + replace(/process\.env\.NODE_ENV/g, '"development"')
|
测试
成功编译并加载 SFC
文件, 完成!
![img_0524032700.webp](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAAaADAAQAAAABAAAAAQAAAADa6r/EAAAAC0lEQVQIHWNgAAIAAAUAAY27m/MAAAAASUVORK5CYII=)
完整代码
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| #!/usr/bin/env node
const koa = require('koa'); const send = require('koa-send'); const path = require('path'); const compilerSFC = require('@vue/compiler-sfc'); const { Readable } = require('stream');
const app = new koa();
const streamToString = (stream, encoding = 'utf-8') => new Promise((resolve, reject) => { const chunks = []; stream.on('data', chunk => chunks.push(chunk)); stream.on('end', () => resolve(Buffer.concat(chunks).toString(encoding))); stream.on('error', error => reject(error)); })
const stringToStream = (text) => { const stream = new Readable(); stream.push(text); stream.push(null); return stream; }
app.use(async (ctx, next) => { if (ctx.path.startsWith('/@modules/')) { const moduleName = ctx.path.slice(10); const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json'); const pkg = require(pkgPath); console.log(pkg.module); ctx.path = path.join('/node_modules', moduleName, pkg.module); } await next(); })
app.use(async (ctx, next) => { await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' }); await next(); })
app.use(async (ctx, next) => { if (ctx.path.endsWith('.vue')) { const content = await streamToString(ctx.body); const { descriptor } = compilerSFC.parse(content);
let code;
if (!ctx.query.type) { console.log(ctx.query.type); code = descriptor.script.content; code = code.replace(/export\s+default\s+/g, 'const _script = '); code += ` import { render as _render } from '${ctx.path}?type=template'; _script.render = _render; export default _script; ` } else if (ctx.query.type === 'template') { const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content }); code = templateRender.code; } ctx.type = 'application/javascript'; ctx.body = stringToStream(code); } await next(); })
app.use(async (ctx, next) => { if (ctx.type === 'application/javascript') { const content = await streamToString(ctx.body);
ctx.body = content. replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/'). replace(/process\.env\.NODE_ENV/g, '"development"') } await next(); })
app.listen(3000); console.log('服务已运行在 localhost:3000');
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script> import ChildCom from "./views/ChildCom.vue";
export default { name: 'App', components: { ChildCom } } </script>
<template> <ChildCom></ChildCom> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <script> export default { name: "ChildCom", setup(){ let sayHi = "hello world"; return { sayHi }; } };
</script>
<template> {{ sayHi }} </template>
|