“ 本文正在参加「金石计划」 ”
文章导航
这小节主要做的就是当引入了第三方包的时候,使用之前预编译在.vite
缓存目录下面的文件。具体来说就是,
import { ref } from 'vue'
// 将其转换成下面的路径
import { ref } from '/node_modules/.vite3/deps/vue.js'
下面要写的内容多部分结合着这个图里面的东西,包含插件容器的实现,以及插件在http
请求中间件中调用插件容器的方法,从而执行插件容器中所有的方法。还是比较容易懵的。写完就会明白的,大家跟上,同时在后面也会写一部分的调试方法,来帮助理解整个流程。
1.创建server入口
创建插件容器,并将插件容器保存到当前服务上面作为属性,同时编写一个用来转换http
请求的中间件(在内部执行vite插件的钩子)
lib\server\index.js
const connect = require('connect')
const resolveConfig = require('../config')
const serveStaticMiddleWare = require('./middlewares/static')
const { createOptimizeDepsRun } = require('../optimizer')
+ const transformMiddleware = require('./middlewares/transform')
+ const { createPluginContainer } = require('./pluginContainer')
async function createServer() {
// connect 本事也可以最为 http 的中间件使用
const middlewares = connect()
const config = await resolveConfig()
+ // 创建一个插件容器,
+ const pluginContainer = await createPluginContainer(config)
// 构造一个用来创建服务的对象
const server = {
+ pluginContainer,
async listen(port) {
// 在创建服务器之前
await runOptimize(config, server)
require('http').createServer(middlewares)
.listen(port, async () => {
console.log(`开发环境启动成功请访问:http://localhost:${port}`)
})
}
}
+ for (const plugin of config.plugins) {
+ if (plugin.configureServer) {
+ await plugin.configureServer(server)
+ }
+ }
//
+ middlewares.use(transformMiddleware(server))
middlewares.use(serveStaticMiddleWare(config))
return server
}
async function runOptimize(config, server) {
const optimizeDeps = await createOptimizeDepsRun(config)
// 把生成的优化信息保存在 server 上面
server._optimizeDepsMetadata = optimizeDeps.metadata
}
exports.createServer = createServer
2.请求转换中间件(transform.js)
这里就主要负责将被vite
插件处理过的代码返回给客户端。
lib\server\middlewares\transform.js
// 转换请求插件
const { normalizePath, isJSRequest } = require('../../utils')
const send = require('../send')
const transformRequest = require('../transformRequest')
const { parse } = require('url')
// 请求转化中间件
function transformMiddleware(server) {
return async function (req, res, next) {
// 如果请求方式不是 get 请求的话, 则直接放行
if (req.method !== 'GET') {
return next()
}
let url = parse(req.url).pathname;
// 如果请求的是 js 文件
if (isJSRequest(url)) {
debugger
const result = await transformRequest(url, server)
if (result) {
const type = 'js'
return send(req, res, result.code, type)
}
} else {
next()
}
}
}
module.exports = transformMiddleware
3.插件容器
在插件容器中执行plugins
中传入的load
,transform
钩子函数。插件上下文中的方法是为了拿到插件钩子执行的结果。
lib\server\pluginContainer.js
const { normalizePath } = require("../utils")
+ const path = require('path')
async function createPluginContainer({ plugins, root }) {
class PluginContext {
// 调用插件容器身上的方法
+ async resolve(id, importer = path.join(root, 'index.html')) {
+ // 由插件容器进行路径解析,返回绝对路径
+ return await container.resolveId(id, importer)
+ }
+ }
// 创建一个插件容器, 插件容器只是用来管理插件的
const container = {
async resolveId(id, importer) {
let ctx = new PluginContext()
let resolveId = id
// 遍历用户传进来的插件
for (const plugin of plugins) {
// 如果插件中没有 resolveId 方法,则执行下一个插件
if (!resolveId) continue
const result = await plugin.resolveId.call(ctx, id, importer)
if (result) {
resolveId = result.id || result;
break;
}
}
return {
id: normalizePath(resolveId)
}
},
+ async load(id) {
+ const ctx = new PluginContext()
+ for (const plugin of plugins) {
+ // 如果当前插件没有 load 方法,则跳过当前插件
+ if (!plugin.load) continue
+ const result = await plugin.load.call(ctx, id)
+ if (result !== null) {
+ return result
+ }
+ }
+ },
+ // async sequential
+ // 异步串行钩子
+ async transform(code, id) {
+ for (const plugin of plugins) {
+ if (!plugin.transform) continue
+ const ctx = new PluginContext()
+ const result = await plugin.transform.call(ctx, code, id)
+ if (!result) continue
+ // 将上一次的结果给下一次
+ code = result.code || result
+ }
+ return { code }
+ }
}
return container
}
exports.createPluginContainer = createPluginContainer
4.工具函数
lib/utils.js
新增
// 判断是否是 js 或者 .vue 结尾的文件
const knownJsSrcRE = /\.(js|vue)/;
function isJSRequest(url) {
if (knownJsSrcRE.test(url)) {
return true
}
return false
}
module.exports = {
normalizePath,
isJSRequest
}
5.插件容器调用插件钩子
lib/server/transformRequest.js
// 转换请求
const fs = require('fs-extra')
// 转换请求中间件调用的函数
async function transformRequest(url, server) {
const { pluginContainer } = server
// 调用插件容器的 解析路径方法
const { id } = await pluginContainer.resolveId(url)
// 调用插件容器的 加载路径文件的方法
const loadResult = await pluginContainer.load(id)
let code
if (loadResult) {
code = loadResult.code
} else {
// 如果在 load 函数里面没有返回结果,则直接读取文件
code = await fs.readFile(id, 'utf-8')
}
// 调用插件容器的转换方法
const transformResult = await pluginContainer.transform(code, id)
return transformResult
}
module.exports = transformRequest
6.设置正确的响应头
lib/server/send.js
const alias = {
js: 'application/javascript',
css: 'text/css',
html: 'text/html',
json: 'application/json'
}
// 设置正确的响应头
function send(req, res, content, type) {
res.setHeader('Content-Type', alias[type] || type);
res.statusCode = 200
return res.end(content)
}
module.exports = send
7.整合内部插件
lib/plugins/index.js
// 导入分析插件
const importAnalysisPlugin = require('./importAnalysis')
const preAliasPlugin = require('./preAlias')
const resolvePlugin = require('./resolve')
async function resolvePlugins(config) {
return [
preAliasPlugin(config),
resolvePlugin(config),
importAnalysisPlugin(config)
]
}
exports.resolvePlugins = resolvePlugins
8. 导入分析插件
lib/plugins/importAnalysis.js
通过es-module-laxer
插件解析源代码,分析当前文件中import
了那些东西,对导入的路径进行重写。
// 导入分析
const { init, parse } = require('es-module-lexer')
const MagicString = require('magic-string')
function importAnalysisPlugin(config) {
// 拿到根目录
const { root } = config
return {
name: 'vite:import-analysis',
async transform(source, importer) {
await init
let imports = parse(source)[0]
if (!imports.length) {
return source
}
let ms = new MagicString(source)
const normalizeUrl = async (url) => {
// 获取绝对路径
const resolved = await this.resolve(url, importer)
if (resolved.id.startsWith(root + '/')) {
url = resolved.id.slice(root.length)
}
return url
}
// 遍历解析出来的 import
for (let index = 0; index < imports.length; index++) {
const { s: start, e: end, n: specifier } = imports[index]
if (specifier) {
const normalizedUrl = await normalizeUrl(specifier)
if (normalizedUrl !== specifier) {
// 重写导入路径的字符串
ms.overwrite(start, end, normalizedUrl)
}
}
}
return ms.toString()
}
}
}
module.exports = importAnalysisPlugin
9.预代理插件
lib/plugins/preAlias.js
const path = require('path')
const fs = require('fs-extra')
// 从预编译信息中 读取第三方依赖文件存在的真实路径进行返回。
function preAliasPlugin() {
let server
return {
name: 'vite:pre-alias',
configureServer(_server) {
server = _server
},
resolveId(id) {
const metadata = server._optimizeDepsMetadata;
const isOptimized = metadata.optimized[id]
if (isOptimized) {
return {
id: isOptimized.file
}
}
}
}
}
module.exports = preAliasPlugin
10.将插件信息挂载到配置信息中去
lib/config.js
const path = require('path')
const { normalizePath } = require("./utils")
+ const { resolvePlugins } = require('./plugins')
async function resolveConfig() {
// 获取当前进程执行的目录
let root = normalizePath(process.cwd())
// 存放预编译的信息
const cacheDir = normalizePath(path.resolve(`node_modules/.vite3`))
let config = {
root,
cacheDir
}
+ // 取出注册的插件
+ const plugins = await resolvePlugins(config)
+ config.plugins = plugins
return config
}
module.exports = resolveConfig
11.测试
我们新建一个项目分别使用原始的vite
和我们编写的vite3
启动项目
{
"name": "use-vite3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite",
"start": "vite3"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"lodash": "^4.17.21",
"vite": "^4.2.1",
"vue": "^3.2.47"
}
}
main.js
在其中引入vue
,我们观察在浏览器端返回导入的是node_modules
下缓存的路径,就代表我们成功了。
如下
import { ref } from 'vue'
let a = ref('value')
a.value = 100
console.log('main.js', a.value)
12.调试方法
如下面我们可以通过断点的形式进行调试,这里拿到的import xx from 'vue'
中的路径就是取自preAlias
插件返回的metadata信息中的真实路径。
创建lunch.json文件
lunch.json文件如下,
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": ["run-script", "start"], // start 就是我们要运行的npm命令
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "node"
}
]
}
配置好之后我们就可以点下面这个三角形启动调试了。
点赞 👍
通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~
原文链接:https://juejin.cn/post/7214831216028041271 作者:一咻