关于前端:一文讲清楚webpack和vite原理

22次阅读

共计 10553 个字符,预计需要花费 27 分钟才能阅读完成。

​一、前言

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

目录:


webpack 根本应用


webpack 打包原理


vite 工作原理


小结


二、webpack 根本应用

webpack 的呈现次要是解决浏览器里的 javascript 没有一个很好的形式去引入其它的文件这个问题的。话说必定有小伙伴不记得 webpack 打包是咋应用的(分明的话能够跳过这一大节),那么我以一个小 demo 来实现一下:

  1. 搭建根本目录构造

咱们在 vue 我的项目中初始化后全局装置 webpack 和 webpack-cli:

yarn add webpack webpack-cli -g


创立 vue 所需的目录文件,以及 webpack 配置文件。

目录构造如下:

编辑

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

不分明 webpack 配置项的敌人能够进官网文档瞅一眼:webpack 中文文档

看完之后,咱们晓得 webpack 次要蕴含的几个概念就开始编写配置文件了!

(1)打包 main.js

代码如下:

const path = require(‘path’)

module.exports = {
  mode: ‘development’,  // 设置开发模式
  entry: path.resolve(__dirname, ‘./src/main.js’),   // 打包入口
  output: {// 打包到哪里去
    path: path.resolve(__dirname, ‘dist’),
    filename: ‘js/[name].js’,  // 默认文件名 main.js
  }
}

为了不便咱们运行,咱们去 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

引入并应用它:

const HtmlWebpackPlugin = require(‘html-webpack-plugin’)

  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, ‘index.html’),    // 须要被打包的 html
      filename: ‘index.html’,  // 文件打包名
      title: ‘ 手动搭建 vue’ //html 传进去的变量
    }),
  ]

index.html 代码如下:

<!DOCTYPE html>
<html lang=”en”>
<head>
  <meta charset=”UTF-8″>
  <meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
  <title><%= htmlWebpackPlugin.options.title%></title>
</head>
<body>
  <div id=”app”></div>
</body>
</html>

好啦,咱们再次运行打包命令,发现 dist 目录下多出 index.html 文件,打包胜利!

(3)打包 vue 文件

首先,咱们须要去装置 vue 的源码:

yarn add vue

新建一个 App.vue:

<template>
  <div>
    vue 我的项目测试
  </div>
</template>

<script setup>
</script>

<style lang=”css” scoped>
</style>

main.js 中写入:

import {createApp} from ‘vue’
import App from ‘./App.vue’

const app = createApp(App)
app.mount(‘#app’)

咱们再去打包,发现报错了,依据提醒,咱们能够推断 webpack 是不能解决且不能编译.vue 后缀的文件的,这就须要引入 loader 及 vue 编译插件了!装它!

yarn add vue-loader@next
yarn add vue-template-compiler -D

持续在配置文件中引入并应用:

const {VueLoaderPlugin} = require(‘vue-loader’)

  module: {
    rules: [
      {
        test: /.vue$/,  //.vue 后缀的文件
        use: [‘vue-loader’]  // 启用 vue-loader
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]

再次打包,打包胜利!咱们能够测试一下,用 live server 运行打包后的 index.html 看看,会发现写在 vue 中的文字在页面胜利展现!

编辑

(4)打包 css

那么咱们如果要在 vue 中写 css 款式呢?显然 webpack 是辨认不了的,还得 loader 来帮忙:

yarn add css-loader style-loader -D

配置文件中退出新的一条 css 规定:

module: {
    rules: [
      {
        test: /.css$/,  //.css 后缀的文件
        use: [‘style-loader’, ‘css-loader’]
      }
    ]
  }

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

编辑

(5)配置 babel

为了避免 webpack 辨认不了高版本的 js 代码,咱们去装 babel:

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

webpack.config.js 配置文件增加新的一条 js 规定:

module: {
    rules: [
      {
        test: /.js$/,  //.js 后缀的文件
        exclude: /node_modules/, // 不蕴含 node_modules
        use: [‘babel-loader’]
      }
    ]
  }

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

module.exports={
  presets:[
    [‘@babel/preset-env’,{
      ‘targets’:{
        ‘browsers’:[‘last 2 versions’]
      }
    }]
  ]
}

  1. webpack 热重载

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

yarn add webpack-dev-server -D

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

devServer: {
    static:{
      directory: path.resolve(__dirname, ‘./dist’)
    },
    port:8080,  // 端口
    hot: true, // 主动打包
    host:’localhost’, 
    open:true // 主动跳到浏览器
  }

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

“dev”: “webpack server –progress –config ./webpack.config.js”

咱们应用 yarn dev 再次运行,相熟的一幕来了!主动跳转到浏览器且将 vue 文件的内容展现在页面上,批改 vue 内容也会主动打包!

三、webpack 打包原理

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


读取入口文件内容(应用 fs)剖析入口文件,递归的形式去读取模块所依赖的文件并且生成 AST 语法树


    
    装置 @babel/parser 转 AST 树)依据 AST 语法树,生成浏览器能够运行的代码(遍历 AST 树)装置 @babel/traverse 做依赖收集
        
        
        装置 @babel/core 和 @babel/preset-env 让 es6 转 es5
        
    
    



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

编辑

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

const fs = require(‘fs’);
const path = require(‘path’);
const parser = require(‘@babel/parser’)
const traverse = require(‘@babel/traverse’).default
const babel = require(‘@babel/core’)

const getModuleInfo = (file) => {
  //1 读文件
  const body = fs.readFileSync(file, ‘utf8’);  // 读到门路下的文件内容

  //2 剖析文件转 AST 树
  const ast = parser.parse(body, {   //body 为须要解析的代码
    sourceType: ‘module’ // 以 es6 的模块化语法解析
  })
  // console.log(ast.program.body);  //[{},{},{}…]

  //3 依赖收集 
  const deps = {}
  traverse(ast, { // 遍历 ast
    ImportDeclaration({node}) {// 把 import 类型的对象找进去
      const dirname = path.dirname(file)  // 拿到 index.js 所在文件夹门路
      const abspath = ‘./’ + path.posix.join(dirname, node.source.value) //add.js 文件的绝对路径
      deps[node.source.value] = abspath //key:’./add.js’  value:’xxx/add.js’
    }
  })

  //4. 把 ast->code
  const {code} = babel.transformFromAst(ast, null, {
    presets: [‘@babel/preset-env’]
  })
  console.log(code);

  const moduleInfo = {file, deps, code}
  return moduleInfo
}

//5. 递归获取所有依赖
const parseModules = (file) => {
  const entry = getModuleInfo(file)
  const temp = [entry]
  const depsGraph = {}

  for (let i = 0; i < temp.length; i++) {
    const deps = temp[i].deps //{‘./add.js’: ‘./src/add.js’, ‘./minus.js’: ‘./src/minus.js’}
    if (deps) {
      for (const key in deps) {
        if (deps.hasOwnProperty(key)) {
          temp.push(getModuleInfo(deps[key]))
        }
      }
    }
  }

  temp.forEach(moduleInfo => {
    depsGraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code
    }
  })
  // console.log(temp);
  return depsGraph
}

// 打包
const bundle = (file) => {
  const depsGraph = JSON.stringify(parseModules(file));
  // 手写一个 require 借助 eval
  return `(function(grash) {

    function require(file) {
      function absRequire(relPath) {
        return require(grash[file].deps[relPath])
      }

      var exports = {};

      (function(require, code) {
        eval(code)
      })(absRequire, grash[file].code)

      return exports
    }

    require(‘${file}’)

  })(${depsGraph})`

}

const result=bundle(‘./src/index.js’)
fs.mkdirSync(‘./dist’)
fs.writeFileSync(‘./dist/bundle.js’, result)

咱们应用 node 去运行这个文件,去到 html 页面上,发现控制台能输入加减法对应的后果,阐明打包胜利!

然而,webpack 有一个毛病,如果在这个文件中须要改变一点点再保留,webpack 的热重载又会从新主动打包一次,这对于大型项目是极不敌对的,这工夫预计等的花都要谢了。那么 vite 呈现了!

四、vite 工作原理

咱们晓得,当申明一个 script 标签类型为 module 时,浏览器会对其外部的 import 援用发动 HTTP 申请获取模块内容。那么,vite 会劫持这些申请并进行相应解决。因为浏览器只会对用到的模块发送 http 申请,所以 vite 不必对我的项目中所有文件都打包,而是按需加载,大大减少了 AST 树的生成和代码转换,升高服务启动的工夫和我的项目复杂度的耦合,晋升了开发者的体验。

  1. 须要解决的问题

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

import {createApp} from ‘vue’ // 浏览器无奈辨认 vue 门路
import App from ‘./App.vue’ // 浏览器无奈解析.vue 文件
import ‘./index.css’ //index.css 不是一个非法的 js 文件,因为 import 只能引入 js 文件

const app = createApp(App)
app.mount(‘#app’)

晓得怎么解决这几个问题,咱们就能打造一个 vite 了!

  1. 打造 vite

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

// 用 node 启一个服务
const Koa = require(‘koa’);
const app = new Koa()
const fs = require(‘fs’)
const path = require(‘path’)
const compilerDom = require(‘@vue/compiler-dom’)  // 引入 vue 源码  能辨认 template 中的代码
const compilerSfc = require(‘@vue/compiler-sfc’)  // 能辨认 script 中的代码

function rewriteImport(content) {
  return content.replace(/ from ‘|”[‘|”]/g, (s0, s1) => {
    // 若以 ./  ../  / 结尾的相对路径
    console.log(s0, s1);
    if (s1[0] !== ‘.’ && s1[0] !== ‘/’) {//’vue
      return  from '/@modules/${s1}'   // 去 http://localhost:5173/@modules/vue
    } else {
      return s0
    }
  })
}

app.use((ctx) => {
  const {request: { url, query} } = ctx
  if (url === ‘/’) {
    // 读 index.html
    ctx.type = ‘text/html’  // 设置类型
    let content = fs.readFileSync(‘./index.html’, ‘utf8’)  // 读文件
    // console.log(content);

    ctx.body = content//content 输入给前端
  }
  else if (url.endsWith(‘.js’)) {//js 文件  /src/main.js
    const p = path.resolve(__dirname, url.slice(1))  //   src/main.js  拿到文件的绝对路径
    ctx.type = ‘application/javascript’
    const content = fs.readFileSync(p, ‘utf8’)
    ctx.body = rewriteImport(content)
  }
  else if (url.startsWith(‘/@modules’)) {//  ‘/@modules/vue’
    const prefix = path.resolve(__dirname, ‘node_modules’, url.replace(‘/@modules/’, ”))  // ‘vue’
    const module = require(prefix + ‘/package.json’).module // 读取 package.json 中的 module 字段   拿到 vue 的模块源码地址
    const p = path.resolve(prefix, module)  // 拿到 vue 的模块源码的终极地址
    const ret = fs.readFileSync(p, ‘utf8’)  // 读取文件
    ctx.type = ‘application/javascript’
    ctx.body = rewriteImport(ret)  // 递归 避免 vue 源码又用到了其它模块
  }
  else if (url.indexOf(‘.vue’) > -1) {
    const p = path.resolve(__dirname, url.split(‘?’)[0].slice(1)) // src/App.vue
    const {descriptor} = compilerSfc.parse(fs.readFileSync(p, ‘utf8’))
    
    console.log(descriptor);
    if (!query.type) {// 返回.vue 文件的 js 局部
      ctx.type = ‘application/javascript’
      ctx.body = `
        ${rewriteImport(descriptor.script.content.replace(‘export default ‘, ‘const __script = ‘))}
        import {render as __render} from “${url}?type=template”
        __script.render = __render
        export default __script
      `
    } else if (query.type === ‘template’) {// 返回.vue 文件的 html 局部
      const template = descriptor.template
      const render = compilerDom.compile(template.content, {mode: ‘module’}).code
      ctx.type = ‘application/javascript’
      ctx.body = rewriteImport(render)
    }

  }
  else if (url.endsWith(‘.css’)) {
    const p = path.resolve(__dirname, url.slice(1))
    const file = fs.readFileSync(p, ‘utf8’)
    const content = `
      const css=”${file.replace(/\n/g, ”)}”
      let link=document.createElement(‘style’)
      link.setAttribute(‘type’,’text/css’)
      document.head.appendChild(link)
      link.innerHTML = css
      export default css
    `
    ctx.type = “application/javascript”
    ctx.body = content
  }
})

app.listen(5173, () => {
  console.log(‘listening on port 5173’);
})

  1. vite 热更新

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

咱们能够应用 chokidar 库来监听某个文件夹的变更,只有监听到有文件变更,就用 websocket 告诉浏览器从新发一个申请,浏览器就会在代码每次变更之后立即从新申请这份资源。

(1)装置 chokidar 库:

yarn add chokidar -D

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

const chokidar = require(‘chokidar’);

export function watch() {
  const watcher = chokidar.watch(‘../src’, {
    ignored: [‘/node_modules/‘, ‘/.git/‘],  // 不监听哪些文件
    ignorePermissionErrors: true,
    disableGlobbing: true
  })
  return watcher
}

export function handleHMRupdate(opts) {// 创立 websocket 连贯 客户端不给服务端发申请,服务端能够通过 websocket 来发数据
  const {file, ws} = opts
  const shortFile = getShortName(file, appRoot)
  const timestamp = Date.now()
  let updates;
  if (shortFile.endsWith(‘.css’)) {//css 文件的热更新
    updates = [
      {
        type: ‘js-update’,
        timestamp,
        path: ${shortFile},
        acceptPath: ${shortFile}
      }
    ]
  }

  ws.send({
    type: ‘update’,
    updates
  })
}

文章转载于:zt_ever

https://juejin.cn/post/7267791228872671247

五、小结

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

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

vite 开发环境应用 esbuild 做 esm 转换,不做打包解决,生产用 rollup 作为打包工具。尽管 vite 也有一些小瑕疵(首屏,懒加载),不过和 webpack 相比体验感的确晋升了不少。


  1. ‘” ↩

正文完
 0