本文參考了vite的0.x版本的源碼,因爲0.x版本只支持vue和esm依賴,react並無esm包,因此這裏並進行了一些react支持方面的改造,供學習和交流 閱讀完本文,讀者應該可以瞭解:javascript
- es module相關的知識
- koa的基本使用
- vite的核心原理
- dev-server自動更新的原理
- esbuild的基礎用法
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
首先咱們建立一下vite項目跑一下vue
yarn create vite my-react-app --template react
yarn dev
複製代碼
能夠看到:
瀏覽器發出了一個請求,請求了main.jsx
查看main.jsx的內容,咱們能夠發現,vite啓動的·服務器對引入模塊的路徑進行了處理,對jsx寫法也進行了處理,轉化成了瀏覽器能夠運行的代碼
繼續看
在client中,咱們看到了websocket的代碼,因此能夠理解爲vite服務器注入客戶端的websocket代碼,用來獲取服務器中代碼的變化的通知,從而達到熱更新的效果
綜上,咱們知道了vite服務器作的幾件事:java
本文的完整代碼在:github.com/yklydxtt/vi…
這裏咱們分五步:node
建立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
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目錄
咱們用koa-static代理static目錄下的靜態資源
index.html中的內容以下:
執行github
node index.js
複製代碼
訪問loaclhost:3001
能夠到咱們剛剛寫的index.html的內容 web
咱們來實現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();
}
複製代碼
咱們來實現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
複製代碼
添加一個like_button.css
// ./static.like_button.css
h1{
color: #ff0
}
複製代碼
在like_button.js中引入
// like_button.js
import './like_button.css';
複製代碼
刷新頁面會看到這樣的報錯:
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)
`
}
});
}
複製代碼
從新啓動服務
樣式就有了
like_button.css的請求body變成了以下的樣子
熱更新藉助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!'
);
...
複製代碼
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;
}
// ...
}
複製代碼
本文是經過閱讀vite源碼並加上一點本身的理解寫出來的,爲了方便你們理解,只實現了核心功能,細節方便沒有作過多說明,若是有錯誤但願獲得指正。
若是你們有收穫,請給我點個贊Thanks♪(・ω・)ノ
陌小路:Vite原理分析