• 一文讲清楚webpack和vite原理


    一、前言

    每次用vite创建项目秒建好,用vue-cli创建了一个项目,却有点久,那为什么用 vite 比 webpack 要快呢,这篇文章带你梳理清楚它们的原理及不同之处!文章有一点长,看完绝对有收获!

    目录:

    1. webpack基本使用

    2. webpack打包原理

    3. vite工作原理

    4. 小结

    二、webpack基本使用

    webpack 的出现主要是解决浏览器里的 javascript 没有一个很好的方式去引入其它的文件这个问题的。话说肯定有小伙伴不记得 webpack 打包是咋使用的(清楚的话可以跳过这一小节),那么我以一个小 demo 来实现一下:

    1. 搭建基本目录结构

    • 我们在vue项目中初始化全局安装 webpack 和 webpack-cli :

    yarn add webpack webpack-cli -g
    • 创建vue所需的目录文件,以及webpack配置文件

    目录结构如下:

    图片

    2. webpack.config.js配置文件编写

    不清楚webpack配置项的朋友可以进官方文档瞅一眼:webpack 中文文档

    看完之后,我们知道webpack主要包含的几个概念就开始编写配置文件了!

    (1)打包main.js

    代码如下:

    1. const path = require('path')
    2. module.exports = {
    3.   mode'development',  //设置开发模式
    4.   entry: path.resolve(__dirname, './src/main.js'),   //打包入口
    5.   output: {   //打包到哪里去
    6.     path: path.resolve(__dirname, 'dist'),
    7.     filename: 'js/[name].js',  //默认文件名main.js
    8.   }
    9. }

    为了方便我们运行,我们去package.json中配置命令,只需yarn dev就能运行了:

    "dev""webpack server --progress --config ./webpack.config.js"

    运行后我们发现根目录多出了一个dist文件夹,我们进到main.js中查看发现打包成功了!

    (2)打包index.html

    问题❓:我们知道vue项目中是有一个index.html文件的,我们如果要打包这个html文件咋办呢?
    我们就需要借助plugin插件去扩展webpack的能力,去装它:

    yarn add html-webpack-plugin -D

    引入并使用它:

    1. const HtmlWebpackPlugin = require('html-webpack-plugin')
    2.   plugins: [
    3.     new HtmlWebpackPlugin({
    4.       template: path.resolve(__dirname, 'index.html'),    //需要被打包的html
    5.       filename: 'index.html',  //文件打包名
    6.       title: '手动搭建vue' //html传进去的变量
    7.     }),
    8.   ]

    index.html 代码如下:

    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4.   <meta charset="UTF-8">
    5.   <meta name="viewport" content="width=device-width, initial-scale=1.0">
    6.   <title><%= htmlWebpackPlugin.options.title%></title>
    7. </head>
    8. <body>
    9.   <div id="app"></div>
    10. </body>
    11. </html>

    好啦,我们再次运行打包命令,发现dist目录下多出index.html文件,打包成功

    (3)打包vue文件

    首先,我们需要去安装vue的源码:

    yarn add vue

    新建一个App.vue:

    1. <template>
    2.   <div>
    3.     vue项目测试
    4.   </div>
    5. </template>
    6. <script setup>
    7. </script>
    8. <style lang="css" scoped>
    9. </style>

    main.js中写入:

    1. import { createApp } from 'vue'
    2. import App from './App.vue'
    3. const app = createApp(App)
    4. app.mount('#app')

    我们再去打包,发现报错了,根据提示,我们可以推断webpack是不能处理不能编译.vue后缀的文件的,这就需要引入loadervue编译插件了!装它!

    1. yarn add vue-loader@next
    2. yarn add vue-template-compiler -D

    继续在配置文件中引入并使用:

    1. const { VueLoaderPlugin } = require('vue-loader')
    2.   module: {
    3.     rules: [
    4.       {
    5.         test/\.vue$/,  //.vue后缀的文件
    6.         use: ['vue-loader']  //启用vue-loader
    7.       }
    8.     ]
    9.   },
    10.   plugins: [
    11.     new VueLoaderPlugin()
    12.   ]

    再次打包,打包成功!我们可以测试一下,用live server运行打包后的index.html看看,会发现写在vue中的文字在页面成功展示!

    图片

    (4)打包css

    那么我们如果要在vue中写css样式呢?显然webpack是识别不了的,还得loader来帮忙:

    yarn add css-loader style-loader -D

    配置文件中加入新的一条css规则:

    1. module: {
    2.     rules: [
    3.       {
    4.         test/\.css$/,  //.css后缀的文件
    5.         use: ['style-loader''css-loader']
    6.       }
    7.     ]
    8.   }

    去vue文件中把字体样式改为红色后,打包并测试一下,成功!:

    图片

    (5)配置babel

    为了防止 webpack 识别不了高版本的 js 代码,我们去装 babel :

    yarn add @babel/core @babel/cdpreset-env babel-loader -D

    webpack.config.js 配置文件添加新的一条 js 规则:

    1. module: {
    2.     rules: [
    3.       {
    4.         test/\.js$/,  //.js后缀的文件
    5.         exclude: /node_modules///不包含node_modules
    6.         use: ['babel-loader']
    7.       }
    8.     ]
    9.   }

    babel.config.js 配置文件代码如下:

    1. module.exports={
    2.   presets:[
    3.     ['@babel/preset-env',{
    4.       'targets':{
    5.         'browsers':['last 2 versions']
    6.       }
    7.     }]
    8.   ]
    9. }

    3. webpack热重载

    热重载它是webpack的一个超级nice的插件,让你不用每次都去执行打包命令,装它:

    yarn add webpack-dev-server -D

    之后,我们去webpack.config.js中配置:

    1. devServer: { 
    2.     static:{
    3.       directory: path.resolve(__dirname, './dist')
    4.     },
    5.     port:8080,  //端口
    6.     hot: true//自动打包
    7.     host:'localhost'
    8.     open:true //自动跳到浏览器
    9.   }

    此时还需要将package.json中的命令改改:

    "dev""webpack server --progress --config ./webpack.config.js"

    我们使用yarn dev再次运行,熟悉的一幕来了!自动跳转到浏览器且将vue文件的内容展示在页面上,修改vue内容也会自动打包!

    三、webpack打包原理

    实现一个webpack的思路主要有三步:

    • 读取入口文件内容(使用 fs )

    • 分析入口文件,递归的方式去读取模块所依赖的文件并且生成AST语法树

      1. 安装 @babel/parser 转AST树)

      2. 根据AST语法树生成浏览器可以运行的代码(遍历AST树)

        1. 安装 @babel/traverse 做依赖收集

        2. 安装 @babel/core 和 @babel/preset-env 让es6转es5

    我们去新建一个目录,结构如下(其中 add.js 和 minus.js 定义了两个值相加减的函数并将其抛出,index.js中引入这两个函数并打印结果,代码就不附上了,比较简单):

    图片

    bundle.js 是我们用来打造 webpack 的文件,代码如下:

    1. const fs = require('fs');
    2. const path = require('path');
    3. const parser = require('@babel/parser')
    4. const traverse = require('@babel/traverse').default
    5. const babel = require('@babel/core')
    6. const getModuleInfo = (file=> {
    7.   //1 读文件
    8.   const body = fs.readFileSync(file'utf8');  //读到路径下的文件内容
    9.   //2 分析文件转AST树
    10.   const ast = parser.parse(body, {   //body为需要解析的代码
    11.     sourceType: 'module' //以es6的模块化语法解析
    12.   })
    13.   // console.log(ast.program.body);  //[{},{},{}...]
    14.   //3 依赖收集 
    15.   const deps = {}
    16.   traverse(ast, { //遍历ast
    17.     ImportDeclaration({ node }) {  //把import类型的对象找出来
    18.       const dirname = path.dirname(file)  //拿到index.js所在文件夹路径
    19.       const abspath = './' + path.posix.join(dirname, node.source.value//add.js文件的绝对路径
    20.       deps[node.source.value= abspath //key:'./add.js'  value:'xxx/add.js'
    21.     }
    22.   })
    23.   //4.把ast->code
    24.   const { code } = babel.transformFromAst(ast, null, {
    25.     presets: ['@babel/preset-env']
    26.   })
    27.   console.log(code);
    28.   const moduleInfo = { file, deps, code }
    29.   return moduleInfo
    30. }
    31. //5. 递归获取所有依赖
    32. const parseModules = (file=> {
    33.   const entry = getModuleInfo(file)
    34.   const temp = [entry]
    35.   const depsGraph = {}
    36.   for (let i = 0; i < temp.length; i++) {
    37.     const deps = temp[i].deps //'./add.js''./src/add.js''./minus.js''./src/minus.js' }
    38.     if (deps) {
    39.       for (const key in deps) {
    40.         if (deps.hasOwnProperty(key)) {
    41.           temp.push(getModuleInfo(deps[key]))
    42.         }
    43.       }
    44.     }
    45.   }
    46.   temp.forEach(moduleInfo => {
    47.     depsGraph[moduleInfo.file= {
    48.       deps: moduleInfo.deps,
    49.       code: moduleInfo.code
    50.     }
    51.   })
    52.   // console.log(temp);
    53.   return depsGraph
    54. }
    55. //打包
    56. const bundle = (file=> {
    57.   const depsGraph = JSON.stringify(parseModules(file));
    58.   //手写一个require 借助eval
    59.   return `(function(grash) {
    60.     function require(file) {
    61.       function absRequire(relPath) {
    62.         return require(grash[file].deps[relPath])
    63.       }
    64.       var exports = {};
    65.       (function(require, code) {
    66.         eval(code)
    67.       })(absRequire, grash[file].code)
    68.       return exports
    69.     }
    70.     require('${file}')
    71.   })(${depsGraph})`
    72. }
    73. const result=bundle('./src/index.js')
    74. fs.mkdirSync('./dist')
    75. fs.writeFileSync('./dist/bundle.js', result)

    我们使用node去运行这个文件,去到 html 页面上,发现控制台能输出加减法对应的结果,说明打包成功

    但是,webpack有一个缺点,如果在这个文件中需要改动一点点再保存,webpack的热重载又会重新自动打包一次,这对于大型项目是极不友好的,这时间估计等的花都要谢了。那么vite出现了!

    四、vite工作原理

    我们知道,当声明一个 script 标签类型为 module 时,浏览器会对其内部的 import 引用发起 HTTP 请求获取模块内容。那么,vite 会劫持这些请求并进行相应处理。因为浏览器只会对用到的模块发送http请求,所以vite不用对项目中所有文件都打包,而是按需加载,大大减少了AST树的生成和代码转换,降低服务启动的时间和项目复杂度的耦合,提升了开发者的体验。

    1. 需要解决的问题

    那么,要打包一个vue项目,它的入口文件是main.js,浏览器会遇到三个问题:

    1. import { createApp } from 'vue' //浏览器无法识别vue路径
    2. import App from './App.vue' //浏览器无法解析.vue文件
    3. import './index.css' //index.css不是一个合法的js文件,因为import只能引入js文件
    4. const app = createApp(App)
    5. app.mount('#app')

    知道怎么解决这几个问题,我们就能打造一个vite了!

    2. 打造vite

    我们使用 koa 去搭建一个本地服务让其可以运行,新建一个 server.js 文件用来打造 vite ,代码如下:

    1. //用node启一个服务
    2. const Koa = require('koa');
    3. const app = new Koa()
    4. const fs = require('fs')
    5. const path = require('path')
    6. const compilerDom = require('@vue/compiler-dom')  //引入vue源码  能识别template中的代码
    7. const compilerSfc = require('@vue/compiler-sfc')  // 能识别script中的代码
    8. function rewriteImport(content) {
    9.   return content.replace(/ from ['|"]([^'"]+)['|"]/g, (s0, s1=> {
    10.     //若以 ./  ../  / 开头的相对路径
    11.     console.log(s0, s1);
    12.     if (s1[0] !== '.' && s1[0] !== '/') {   //'vue
    13.       return ` from '/@modules/${s1}'`   //去http://localhost:5173/@modules/vue
    14.     } else {
    15.       return s0
    16.     }
    17.   })
    18. }
    19. app.use((ctx) => {
    20.   const { request: { url, query } } = ctx
    21.   if (url === '/') {
    22.     //读index.html
    23.     ctx.type = 'text/html'  //设置类型
    24.     let content = fs.readFileSync('./index.html', 'utf8')  //读文件
    25.     // console.log(content);
    26.     ctx.body = content//content输出给前端
    27.   }
    28.   else if (url.endsWith('.js')) {  //js文件  /src/main.js
    29.     const p = path.resolve(__dirname, url.slice(1))  //   src/main.js  拿到文件的绝对路径
    30.     ctx.type = 'application/javascript'
    31.     const content = fs.readFileSync(p, 'utf8')
    32.     ctx.body = rewriteImport(content)
    33.   }
    34.   else if (url.startsWith('/@modules')) { //  '/@modules/vue'
    35.     const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))  // 'vue'
    36.     const module = require(prefix + '/package.json').module //读取package.json中的module字段   拿到vue的模块源码地址
    37.     const p = path.resolve(prefix, module)  // 拿到vue的模块源码的终极地址
    38.     const ret = fs.readFileSync(p, 'utf8')  //读取文件
    39.     ctx.type = 'application/javascript'
    40.     ctx.body = rewriteImport(ret)  //递归 防止vue源码又用到了其它模块
    41.   }
    42.   else if (url.indexOf('.vue') > -1) {
    43.     const p = path.resolve(__dirname, url.split('?')[0].slice(1)) // src/App.vue
    44.     const { descriptor } = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
    45.     
    46.     console.log(descriptor);
    47.     if (!query.type) { // 返回.vue文件的js部分
    48.       ctx.type = 'application/javascript'
    49.       ctx.body = `
    50.         ${rewriteImport(descriptor.script.content.replace('export default ', 'const __script = '))}
    51.         import { render as __render } from "${url}?type=template"
    52.         __script.render = __render
    53.         export default __script
    54.       `
    55.     } else if (query.type === 'template') { // 返回.vue文件的html部分
    56.       const template = descriptor.template
    57.       const render = compilerDom.compile(template.content, {mode: 'module'}).code
    58.       ctx.type = 'application/javascript'
    59.       ctx.body = rewriteImport(render)
    60.     }
    61.   }
    62.   else if (url.endsWith('.css')) {
    63.     const p = path.resolve(__dirname, url.slice(1))
    64.     const file = fs.readFileSync(p, 'utf8')
    65.     const content = `
    66.       const css="${file.replace(/\n/g, '')}"
    67.       let link=document.createElement('style')
    68.       link.setAttribute('type','text/css')
    69.       document.head.appendChild(link)
    70.       link.innerHTML = css
    71.       export default css
    72.     `
    73.     ctx.type = "application/javascript"
    74.     ctx.body = content
    75.   }
    76. })
    77. app.listen(5173, () => {
    78.   console.log('listening on port 5173');
    79. })

    3. vite热更新

    那么,vite 的热更新怎么实现呢?

    我们可以使用chokidar库来监听某个文件夹的变更,只要监听到有文件变更,就用websocket通知浏览器重新发一个请求,浏览器就会在代码每次变更之后立刻重新请求这份资源。

    (1) 安装chokidar库:

    yarn add chokidar -D

    (2) 之后去新建一个文件夹chokidar,在其中新建 handleHMRUpdate.js 用于实现监听:

    1. const chokidar = require('chokidar');
    2. export function watch() {
    3.   const watcher = chokidar.watch('../src', {
    4.     ignored: ['**/node_modules/**''**/.git/**'],  //不监听哪些文件
    5.     ignorePermissionErrorstrue,
    6.     disableGlobbingtrue
    7.   })
    8.   return watcher
    9. }
    10. export function handleHMRupdate(opts) {   //创建websocket连接 客户端不给服务端发请求,服务端可以通过websocket来发数据
    11.   const { file, ws } = opts
    12.   const shortFile = getShortName(file, appRoot)
    13.   const timestamp = Date.now()
    14.   let updates;
    15.   if (shortFile.endsWith('.css')) {  //css文件的热更新
    16.     updates = [
    17.       {
    18.         type'js-update',
    19.         timestamp,
    20.         path`${shortFile}`,
    21.         acceptPath`${shortFile}`
    22.       }
    23.     ]
    24.   }
    25.   ws.send({
    26.     type'update',
    27.     updates
    28.   })
    29. }

    文章转载于:zt_ever

    https://juejin.cn/post/7267791228872671247

    五、小结

    webpack可以说是把所有模块的依赖关系打包成一个大文件,速度比较慢。

    Vite 利用现代浏览器中原生 ES 模块的特性,将开发和构建过程拆分为更小的单位,通过浏览器运行时发送的http请求实现了来实现文件的按需加载。快速的冷启动和实时的模块热更新。Vite 提供了更高效的开发体验和更快的开发环境速度。

    vite开发环境使用 esbuild 做esm 转换,不做打包处理,生产用rollup作为打包工具。虽然vite也有一些小瑕疵(首屏,懒加载),不过和webpack相比体验感确实提升了不少。

  • 相关阅读:
    Navicat 携手华为云 GaussDB,联合打造便捷高效的数据库开发和建模工具方案
    一文看懂推荐系统:物品冷启05:流量调控
    ARM通用中断控制器GIC之中断控制
    QT基础入门【QSS】QT伪状态类型和实例
    vue项目的学习周报03
    vue3项目中使用富文本编辑器
    Vue3最佳实践 第七章 TypeScript 中
    https网址大部分电脑没问题,部分就是提示下面的各种试试
    el-upload实现上传文件夹
    南卡和益博思电容笔哪一款值得入手?两款电容笔对比评测
  • 原文地址:https://blog.csdn.net/2301_79226238/article/details/133302305