一、前言每次用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@nextyarn 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 -Dwebpack.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']      }    }]  ]}3. 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').defaultconst 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了!2. 打造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');})3. 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_everhttps://juejin.cn/post/7267791228872671247五、小结webpack可以说是把所有模块的依赖关系打包成一个大文件,速度比较慢。Vite 利用现代浏览器中原生 ES 模块的特性,将开发和构建过程拆分为更小的单位,通过浏览器运行时发送的http请求实现了来实现文件的按需加载。快速的冷启动和实时的模块热更新。Vite 提供了更高效的开发体验和更快的开发环境速度。vite开发环境使用 esbuild 做esm 转换,不做打包处理,生产用rollup作为打包工具。虽然vite也有一些小瑕疵(首屏,懒加载),不过和webpack相比体验感确实提升了不少。

  1. '"

北京蔓云科技
4 声望1 粉丝