( 开篇 )仿写'Vue生态'系列___'你webpack溜么?'

( 开篇 )仿写'Vue生态'系列___'你webpack溜么?'

关于这个系列

作者离职深造也有一个月了, 前端相关的视频与资料学了非常多, 自己感觉到现在的知识之间只是呈现出一种相互之间的弱联系, 也就是还不成'体系', 每一个知识点我都学过我都会用, 但是统一起来就有些地方不是很明朗了,相信很多前端仔也会有同样的感受.

比如说:
1: 你知道你经常用的插件的原理么? 🤔
2: 你能写出一个与他功能相仿的插件给别人去使用么? 🤷‍♀️
3: 不借助插件你能正常开发么? 🕊
4: 面试时候面试官问你,某些技术的原理, 你是否只能回答"我只是站在巨人的肩膀上?"┑( ̄Д  ̄)┍

为了把这些'围绕着框架'展开的一系列知识弄懂, 所以本次与大家一起从0开始, 写一套简易版的vue框架, 不但要写框架, 我们还要写 vuex, vue-router, axios, 简易的webpack, webpack-loader, webpack-plugin等等, 涉及到的知识点都会拿出来详谈一番, 如果有哪里没有讨论到位的我们可以私信继续死磕, 本次项目会在github上同步更新 github

本人去年至今年年初在公司一直负责面试前端, 所以文章中不但涉及技术, 中间还会穿插着我的理解与之前做过的笔记, 以及面试时候我遇到的一些问题, 毕竟再过一个月或者两个月我也就结束'闭关'去找工作了, 所以对面试这方面也会有所讨论, 相关知识与问题 大家都可以一起交流, 共同学习,共同进步, 早日实现自我价值!!

一. 前端与webpack

(本次工程师基于webpack4)
webpack现在已经可以说是前端必会的技能了, 如果有人跟你说, 那东西看看文档就行不用学, 那他就是在害你, webpack是一门需要手熟的技术, 最基本的结构你必须不看资料能纯手打出来, 现在框架都帮我们集成好了开发环境, 从另一方面来说也会温水煮青蛙麻痹我们的神经, '只要会用就可以了'并不适合webpack, 这门技术可以帮我们更好的了解前端这个职位的定义, 工欲善其事必先利其器, 那么本期就从webpack的配置开始吧!

需求分析
1: 本次实现的简易框架 暂定名字为 'C', 开发使用class的形式, 所以需要支持es6
2: 可以加载css文件, 因为有了样式, 写代码才有乐趣
3: 热更新
4: 区分生产环境与开发环境, 就是为了巩固webpack优化的相关知识

基础的搭建--逐行解析!

1: 创建一个dist文件夹存放打包文件
2: 创建一个src文件夹存放开发信息
3: src里面index.js 主要的导出文件
4: 安装依赖 npm install webpack webpack-cli -D

下面这段最基础的代码你能不看资料自己手敲么
标题的'溜'不是指多么的深入, 而是能把最基础的东西准确的做出来.

const path = require('path');
module.exports = {
   mode:'development',
   entry:{
    main:'./src/index.js'
   },
   output:{
     filename: '[name].js',
     path: path.resolve(__dirname, '../dist')
   }
}

知识点汇总( 死磕知识点... )

一: path

  1. node的原生自带模块.
  2. 与其一同出现的有 resolve join 两个方法
    join:
    windows下文件路径分隔符使用的是"", Linux下文件路径分隔符使用的是"/", path.join() 方法使用平台特定的分隔符把全部给定的 path 片段连接到一起,并规范化生成的路径。长度为零的 path 片段会被忽略。 如果连接后的路径字符串是一个长度为零的字符串,则返回 '.',表示当前工作目录.
    onsole.log(path.join('a','b','c','..','d')) 打印 a/b/d 被拼接为一体, ..把c干掉了;
    resolve:
    方法会把一个路径或路径片段的序列解析为一个绝对路径, 如果没有传入 path 片段,则 path.resolve() 会返回当前工作目录的'绝对路径'。
    console.log(path.resolve('a','b','c')) /Users/lulu/web/cc_vue/a/b/c 精确到了工程

二: module.exports
node的模块导出方式, 为什么他是用'.'链接, 而我们使用的es6 是' '空格,比如:export default
原因: 学过node的朋友会很好理解, node可以随时的读取文件, 而module只是个变量而已, exports是module上面的一个属性, 每次读取文件之后, 用户想要输出的文件挂载到这个对象上行了, 所以它使用'.'的这种方式, 实则是在操作对象. 而es6的语法在浏览器上是实现不了的, 需要编译之后再执行, 它采用的是关键字模式, 比如我来编写一个打包插件来打包export default这种语法, 我需要先解析出语法树, 然后把它的文件绝对路径转换为key, 内容是value的一个闭包(这方面后面会做一下), 所以es6的引入方式才是同步, 无法随处的写;

三: mode:webpack新增的模式属性

第一. none 不使用webpack自带的模式
第二. development : 开发模式

 1. 不压缩
 2. 不摇树(为什么开发环境不'摇树'? 原因是如果去除了多余的代码, 那么调试的时候就找不到准确的段落了;)
 3. 有完善的错误提示
 4. 可以设置完备的source-map
// 添加了下面的两句
plugins: [
   // 当开启 HMR 的时候使用该插件会显示模块的相对路径,而不是文件的id。
   new webpack.NamedModulesPlugin(),
   // 更改全局的环境变量
   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
      ]

第三. production 线上模式

// 自动添加
   plugins: [
   // 压缩js代码
    new UglifyJsPlugin(/* ... */),
   // 设定环境变量
    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
   // 过去 webpack 打包时的一个取舍是将 bundle 中各个模块单独打包成闭包。这些打包函数使你的 JavaScript 在浏览器中处理的更慢。相比之下,这个插件 可以提升或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。
    new webpack.optimize.ModuleConcatenationPlugin(),
// 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段。这样可以确保输出资源不会包含错误。对于所有资源,统计资料(stat)的 emitted 标识都是 false。
    new webpack.NoEmitOnErrorsPlugin()
]

四: entry

  1. 直接写字符串 entry:./xx/index 此时默认名为main
  2. 多入口文件写数组[./xx1/index, ./xx1/index]
  3. 对象, 也是官方推荐的写法, 每个入口都定义名字, 配合打包输出使用
 entry :{
  main:'./src/index.js',
  beas:'./src/base.js'
}

五: output 这个比较单一

// 这里可以自定义名字, 就是'写死'就行
// 下面这种方式意思就是 上面entry的名字是啥, 这里就是啥
// 一般多入口打包都这么写
  filename: '[name].js',
// 这个形式结合上面的知识点肯定都没问题了, 
// __dirname: 当前文件所在文件夹
  path: path.resolve(__dirname, '../dist')

看了这些感受如何??? 虽就这么几行代码, 但也要死磕一下

babel相关

npm install babel-loader -D
module里面的rules选项是专门玩loader的
只有一个key, 还写成对象形式, 估计以后会有所动作

 module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader?cacheDirectory=true'
      }
    ]
  }
  1. test 也就是指定的匹配规则, 后面有机会的话 我讲一下正则相关的技巧
  2. exclude 不处理的文件,也就是说依赖全部不翻译, 不加这句会很慢的
  3. loader也就是具体用哪种方式处理匹配出来的文件内容, 上面这种写法是官网推荐的写法, 默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程

babel-loader干么的?
看过他的源码才知道, 他是个领导, 负责安排专门的人来执行编译工作, 而他自己只负责调度与输出.

核心员工"@babel/core"
npm install @babel/core -D
他的命名式7.x 开始规范下来的, 有的人可能见过'-'链接的, 都是早期的版本了, 因为他只是一个执行具体动作的模块而已, 所以需要@babel开头
实际上, 具体的工作都是这个模块去完成的, 所以你只加载babel-loader是没有用的.
其实babel团队这样设计才是符合设计模式的, webpack也分出 webpack-cli, 现在很多插件都不是单一一个文件了.

.babelrc
webpack的配置项实在是太多了, 如果都集中在一个文件里面就不符合设计原则了, 像这种很常用, 而且配置项比较多的, 会被单独抽离出来配置, babelrc(和.babelrc.js/ package.json#babel)文件名 都可以

@babel/preset-env 指定目标的需求🤔
npm install @babel/preset-env -D
babel转换js代码的功能其实可以很细化, 比如说有的人需要全部转为可以兼容 ie7的语法, 而我平时转换为 Chrome 最近5个版本 能兼容的就可以, 所以没必要转换太多导致性能占用,

就要在.babelrc这个文件里面 设置在 presets选项下
指定我到底要解析到什么地步
他有一个对照表, 把配置项合并计算出交叉配置

"presets": [
    [
      "@babel/preset-env",
      {
        // 写法1
        "targets": {
            // 用户占比大于1%的浏览器的 最后两个版本
          "browsers": ["> 1%", "last 2 versions"]
        }
        // 写法2
        "targets": {
          "chrome": "58",
          "ie": "11"
         }
      }
    ]
  ],

下面这种形式: 如果不进行任何配置, preset 所包含的插件将支持所有最新的 JavaScript (ES2015、ES2016 等)特性。

"presets": ["@babel/preset-env"],

官方维护的类型
@babel/preset-env
@babel/preset-flow
@babel/preset-react
@babel/preset-typescript
Stage 0 - 设想(Strawman):只是一个想法,可能有 Babel插件。
Stage 1 - 建议(Proposal):这是值得跟进的。
Stage 2 - 草案(Draft):初始规范。
Stage 3 - 候选(Candidate):完成规范并在浏览器上初步实现。
Stage 4 - 完成(Finished):将添加到下一个年度版本发布中。
preset-es2015 等等 被@babel/preset-env取代, babel7版本

上面我们只是做到了比如说 箭头函数这种转换, 但是像promise这种实例方法是没有的, babel分工很明确, 上面的配置只做这方面的转换

霸道的polyfill
运行时的库, 所以要是save!!!!
npm install --save @babel/polyfill
这个库将会模拟一个完全的 ES2015+ 的环境。
使用就是取对应的页面上require他
为什么说他霸道?
这家伙就是个铁憨憨, 一股脑的把所有配置都注入进来, 有了它, 打包后的体积大的吓人, 但他也是最全面的, 导致我没用到的功能他都给我写好了, 那就非常没有必要了, 所以babel给了配置项, 可以只注入我们用到了的类与方法, 这样体积会减小很多.
还是要交给'需求'去做.

 "presets": [
   ["@babel/preset-env",{
        "useBuiltIns":"usage"  // 使用polyfill了时候只加载使用到的方法
    }]
 ]

他还有一个致命的缺点, 就是污染全局, 比如说我开发一个插件给你用, 我就把你的全局变量都改成最新版的了, 开发插件的准则就是尽量不要动用户的数据, 万一用户也引入了另一个版本的他, 那就太可怕了, 所以开发插件babel提供了下面的方案

@babel/plugin-transform-runtime
plugin-transform-runtime 已经默认包括了 @babel/polyfill,因此不用在独立引入。
npm install @babel/plugin-transform-runtime -D
他也算是个分配至, 需要安装一个干活的
具体代码注入都是在他文件里面取出来的
npm install --save @babel/runtime

避免多次编译出helper函数:
Babel转移后的代码想要实现和原来代码一样的功能需要借助一些帮助函数
@babel/runtime包就声明了所有需要用到的帮助函数,而@babel/plugin-transform-runtime的作用就是将所有需要helper函数的文件,依赖@babel/runtime包

不会污染全局变量
多次使用只会打包一次
依赖统一按需引入,无重复引入,无多余引入

// 缺点
不支持实例化的方法Array.includes(x) 就不能转化, 因为如果实现了这个就是污染全局了啊
如果使用的API用的次数不是很多,那么transform-runtime 引入polyfill的包会比不是transform-runtime 时大

这个要在.babelrc文件夹,plugins选项里面配置

"plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        // 如果配置他为2, false不用
        // 需安装 @babel/runtime-corejs2
        // 则不用安装 @babel/runtime
        // 作用: promise 代码变成了一个独立的变量 _promise,不会影响全局的 Promise。
        "corejs": 2,
        // 默认就是true  写出来让大家知晓
        "regenerator": true, // 切换生成器函数是否转换为使用不会污染全局范围的再生器运行时。
      }
    ]
  ]

上面的配置好了, 就可以正常玩耍es6了, 如果上面的你全都看完了, 那你还怕以后别人问你这方面知识么??

css样式方面

开篇也说了, 有个好的造型, 代码写起来也愉悦

 module: { // 只有一个key, 还写成value, 估计以后会有所动作

    rules: [
      {
        test: /.(css|sass|scss|less)$/,
        use: ['style-loader', 'css-loader', 'postcss-loader']
      }
    ]
  }

配置大同小异, 我来具体解释下每一个loader

  1. style-loader: 使用<style>标签将css-loader内部样式注入到我们的HTML页面
  2. css-loader: 主要用于处理图片路径 与 导入css文件的路径 然后义字符串的形式存入js
  3. postcss-loader: 为css样式加上必要的前缀
    他需要配合autoprefixer使用, npm install -autoprefixerD, 很有必要的优秀软件
    与.babelrc类似的配置文件如下, 名称为
// 他需要导出一下, 不想.babelrc直接读取{}内的
module.exports = {
  plugins: [
    require('autoprefixer')({
        // 1: 最新版的话 browsers改成overrideBrowserslist就不报错了
        // 2: 部分用户出现不写[]里面的内容就不编译的bug
      overrideBrowserslist: [
          'iOS 7.1', 
          'ff > 31', 
          'ie >= 8',
          'Chrome > 31', 
          'Android 4.1', 
        ]
    })
  ]
};

遇到的知识都做了梳理, 工欲善其事必先利其器, 每一个知识点都值得探究, 这边写的多了电脑就好卡, 下篇把剩下的几个知识点说一下就可以正式做框架了, 写文章的过程也是对自己的一种审视, 把很多问题拿到面前质问自己到底会不会, 挺累的但也是收获满满.

end
大家都可以一起交流, 共同学习,共同进步, 早日实现自我价值!!

github: github
个人技术博客: 个人技术博客

阅读 1.5k

推荐阅读