在一个jsl中引入多个js文件然后在页面调用一个js_教你搭建一个自己的脚手架(一)...

72d9b71837a2fbcac82a88a31d3234e7.png

【写在前面】

最近工作中不断的有场景,需要有一个脚手架进行快速的原型开发和idea验证。在尝试了Vue-cli等开源工具后,还是觉得离自己期望的有些diff。所以就动了想要搞一个脚手架的想法,希望它足够轻量,足够快。下载快,安装快,打包构建快,跑起来也快,最重要的是自己可以随时根据需求diy。

【看这里】

于是,我开始动手了,完整的代码在这里GitHub[1]  

首先,最常用的技术栈是基于vue的,然后看到了最近webpack更新到了版本5,相传打包构建速度有很大提升。所以就基于webpack5来构建一个自己的脚手架吧。  

在正式开始code之前,我勾画了一下它大概的样子和应该具备的能力。它最后应该长这样子:

 基础版标准版

dev-server

处理html/JS/VUE/CSS/LESS/SASS/IMG/JPG等各种文件

路由配置

mock数据

代理接口

打包分析

单元测试

编译构建

dev-server

处理html/JS/VUE/CSS/LESS/SASS/IMG/JPG等各种文件

路由配置

mock数据

代理接口

打包分析

单元测试

编译构建

基本布局

导航配置

支持TS

支持Markdown

风格检查

嗯,先从基础版开干。。

// 初始化yarn init// 安装webpack相关依赖yarn add webpack webpack-cli webpack-dev-server webpack-merge --dev

来解释一下,webpack-cli[2]是为了支持在scripts脚本中直接调用webpack命令的。webpack-dev-server[3]是提供开发的server服务,webpack-merge[4]是可以合并多个webpack配置的。

// 建立文件目录,单元测试/mock数据/配置文件/业务逻辑mkdir test mock build src// 针对不同的场景,应该有不同的打包配置touch build/webpack.config.base.js build/webpack.config.dev.js build/webpack.config.prod.js// 再来一个公共文件,抽取一些公共配置touch build/config.js

到这里,文件的目录结构是这样子:

379f2e2c765980592e346c1c026f21f2.png

继续添加依赖

// vue三件套yarn add vue vue-router vuex// 加个工具库,网络库,组件库yarn add lodash axios view-design// 安装polyfill, 处理es6+语法yarn add @babel/core @babel/polyfill babel-loader

webpack中需要指定入口文件[5]来构建依赖树,我们使用`src/main.js`作为入口文件。

// src/main.jsimport '@babel/polyfill';import Vue from 'vue';import VueRouter from 'vue-router';import ViewUI from 'view-design';import routes from './common/router';import ApiClient from './common/client';import 'view-design/dist/styles/iview.css';Vue.use(VueRouter);Vue.use(ViewUI);Vue.prototype.$client = new ApiClient();const router = new VueRouter({routes});const app = new Vue({  router}).$mount('#app');

引入vue三件套,babel依赖,使用Vue.use()安装`VueRouter`和`ViewUI`, 其原理就是调用了VueRouter的内部install方法。需要注意,ViewUI的样式文件需要单独引入。除此之外,可以留意到有一个ApiClient的东西,这个是封装了一个网络请求的Client,并挂载到了Vue的原型链上,后面会详细解释。

Vue实例需要有一个挂载点,单页面应用也需要有一个html文件来渲染页面内容。所以我们需要再编辑一个index.html,提供基本的容器。

<html>    <head>        <meta charset="utf-8">        <title>Zero-Xtitle>    head>    <body>          <div id="app">            <router-view>router-view>        div>         body>html>

文件很简单,提供了一个`id="app"`的挂载点和一个用于切换路由的`router-view`标签。但是我们在打包的时候实际上是动态生成的html文件,为什么会这样呢?因为单页面应用的原理,其实就是指定一个标签作为容器,动态的切换其内容,即动态的挂载/移除其子节点来完成的改变页面内容。而可以配合完成对页面动态切换子节点的控制逻辑,对应的就是路由插件。这些控制逻辑和样式等,最后都会被打包成一个个的文件,插入到html中,来实现页面的首次加载和渲染。它最后生成的样子,应该是这样的:

a406e4af9757da87a2eb31743462f7f9.png

你可以看到有一些js文件和css样式文件被插入到了页面中,以我们最熟悉的原始的方式,使用`script`和`link`标签来加载。而支持完成这一些自动化转换工作的就是一个叫html-webpack-plugin[6]的插件。

它的大致的用法如下:

// 以index.html为模板,生成一个叫index.html的文件,将其它的依赖注入到页面的合适位置const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {    plugins: [        new HtmlWebpackPlugin({            filename: 'index.html',            template: path.resolve(__dirname, '../index.html'),            favicon: 'src/common/assets/img/favicon.ico',            inject: true        })    ],    ... };

好了,我们要开始完善webpack的配置了

有了入口文件,还要指定最后打包的产出放到哪个位置,所以要有一个output[7]的选项,指定构建产出的一些配置:

const config = require('./config');module.exports = {    entry: {        app: './src/main.js'    },    output: {        path: config.common.assetsRoot,        publicPath: config.common.assetsPublicPath,        filename: utils.genFilePathWithName('[name].js'),        chunkFilename: utils.genFilePathWithName('[name].bundle.js')    },};

`output.filename`和`output.path`属性指定了,最后生成的文件名称和放置的位置。`output.publicPath`和`output.chunkFilename`属性指定了文件的引用路径。

module.exports = {  //...  output: {    publicPath: '/assets/',    chunkFilename: '[id].chunk.js'  }};

一段这样的配置,最后引用的资源路径有可能是这样的`/assets/5.chunk.js`,然后它插入到html文件中就变成了这个样子:

<script src="/assets/5.chunk.js">script>

关于`output.filename`和`output.chunkFilename`的区别,filename主要是指主控逻辑(运行时依赖)的文件名称,这里的取值`[name].js`是和入口文件保持一致的意思,即生成的文件应该是app.js。chunkFilename则指定了按需加载的其它文件的命名方式。`utils.genFilePathWithName`方法是实现了一个将文件按照文件类型进行归类的逻辑。

/** * @file utils.js * @author nimingdexiaohai(nimingdexiaohai@163.com) */const path = require('path');const config = require('./config');module.exports = {    // 按文件后缀分组到同类型文件目录下    genFilePathWithName: function(fileName) {        return path.posix.join(config.common.assetsSubDirectory, path.extname(fileName).substring(1), fileName);    }};

更直白点讲,就是把js文件都放到js目录下,css文件都放到css目录下。这样就实现了一个简单的归类,从这里也可以看出,`output.filename`是支持给定一个路径作为值的。最后文件放置的位置就应该是`output.path` + `output.filename`,比如/assets/js/app.js等。

/** * @file config.js * @author nimingdexiaohai(nimingdexiaohai@163.com) */const path = require('path');module.exports = {    dev: {    },    prod: {    },    common: {        assetsRoot: path.resolve(__dirname, '../dist'),        assetsPublicPath: '/',        assetsSubDirectory: 'static'    }};

config.js中抽取了一些公共的配置。指定完了产出配置,再来看下这个需求"处理html/JS/VUE/CSS/LESS/SASS/IMG/JPG等各种文件", 之所以先搞定这个需求,而不是按照顺序先看dev-server,是因为dev-server只有开发时才用得到,我们尽量先搞定一些公共基础配置,这些配置可以复用到其它各种场景,比如开发、测试、生产环境等。  

不同文件的打包处理是靠各种loader来实现的,这也是webpack的伟大之处,像处理js文件那样处理其它类型的文件,构建完整的依赖图。它的实现,需要配置`moudle`字段。

const MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = {    module: {        rules: [            {                test: /\.vue$/,                use: 'vue-loader'            },            {                test: /\.js$/,                use: [{                    loader: 'babel-loader',                    options: {                        cacheDirectory: true                    }                }],                exclude: /node_modules/,            },            {                test: /\.css$/,                use: [MiniCssExtractPlugin.loader, 'css-loader']            },            {                test: /\.less$/,                use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']            },            {                test: /\.(png|gif|svg|ico|jpe?g|woff2?|eot|ttf|otf)(\?.*)?$/,                use: [{                    loader: 'url-loader',                    options: {                        limit: 8192,                    }                }]            }        ]    },};

`module`字段支持配置多个项,每个项包含一个正则和loader配置,意为满足正则匹配的文件使用该loader处理。我们倒着来看,`url-loader`用来处理各种图片、字体文件等,当它小于8kb时,文件会被转换成base64编码,直接硬编码进产出中;当文件大于8kb时,使用file-loader来处理,这个是url-loader的内在实现原理,所以也需要自己安装file-loader。  

`.less`文件的处理相对复杂一点,它指定了三个loader,loader是按照配置顺序的逆序加载的,也就是会从后到前依次处理,先将less处理为css,再将css传入MiniCssExtractPlugin.loader处理。mini-css-extract-plugin[8]这个插件是将样式文件单独抽离,单独打包用的。  

`mini-css-extract-plugin`这个插件的使用,还需要在plugins中指定抽取出来的样式文件名称,当然你也可以不配置,使用默认规则[name].css。

const VueLoaderPlugin = require('vue-loader/lib/plugin');const HtmlWebpackPlugin = require('html-webpack-plugin');const {CleanWebpackPlugin} = require('clean-webpack-plugin');plugins: [    new VueLoaderPlugin(),    new CleanWebpackPlugin(),    new MiniCssExtractPlugin({        filename: utils.genFilePathWithName('[name].css')    }),    new HtmlWebpackPlugin({        filename: 'index.html',        template: path.resolve(__dirname, '../index.html'),        favicon: 'src/common/assets/img/favicon.ico',        inject: true    })],

clean-webpack-plugin[9]插件的作用是每次打包前,都将上次output目录中清理一下。这样做的目的,是为了应对当文件命名中包含了hash的时候,每次打包出来的文件名称都不一样,长久积累下去,就会出现很多无用的废弃文件,所以打包之前先清理下文件目录,只保留当前编译的产出。

到这里,我们的公共配置就快要完成了,别着急,还有一个事情。当文件目录特别长的时候,或者我们想要引入文件,而不用写后缀的时候,我们可以配置一些别名来实现小小的偷懒行为。

resolve: {        symlinks: false,        modules: [path.resolve(__dirname, '../node_modules')],        extensions: ['.js', '.vue', '.less', 'css'],        alias: {            'vue': 'vue/dist/vue.esm.js',            '@': path.resolve(__dirname, '../src')        }    },

`resolve.symlinks`字段用于指定解析软链的规则。启用时,符号链接资源将解析为其实际路径,而不是其符号链接位置。请注意,当使用symlink软件包的工具时,这可能会导致模块解析失败,因此不推荐开启这个配置。  

`resolve.modules`是说当直接匹配你导入的文件路径失败时,去哪些地方继续搜索。比如`import vue form vue;` 直接匹配当前目录下的vue肯定是没有这个文件的,所以就去node_modules中去找,就可以找到了。大部分的三方依赖都是这样被解析到的。  

`resolve.extensions`是当你导入一个文件时,可以省略文件后缀,webpack会尝试依次搜索拼接了这些后缀的文件。  

`resolve.alias`这个就是给一些目录指定了别名,一般是为了不用写冗长的路径而设置的。  

到这里,基本上所有的基础配置就完成了,我们看下它完整的样子:

/** * @file webpack.config.base.js * @author nimingdexiaohai(nimingdexiaohai@163.com) */const path = require('path');const utils = require('./utils');const VueLoaderPlugin = require('vue-loader/lib/plugin');const HtmlWebpackPlugin = require('html-webpack-plugin');const {CleanWebpackPlugin} = require('clean-webpack-plugin');const MiniCssExtractPlugin = require('mini-css-extract-plugin');const config = require('./config');module.exports = {    entry: {        app: './src/main.js'    },    output: {        path: config.common.assetsRoot,        publicPath: config.common.assetsPublicPath,        filename: utils.genFilePathWithName('[name].js'),        chunkFilename: utils.genFilePathWithName('[name].bundle.js')    },    resolve: {        symlinks: false,        modules: [path.resolve(__dirname, '../node_modules')],        extensions: ['.js', '.vue', '.less', 'css'],        alias: {            'vue': 'vue/dist/vue.esm.js',            '@': path.resolve(__dirname, '../src')        }    },    module: {        rules: [            {                test: /\.vue$/,                use: 'vue-loader'            },            {                test: /\.js$/,                use: [{                    loader: 'babel-loader',                    options: {                        cacheDirectory: true                    }                }],                exclude: /node_modules/,            },            {                test: /\.css$/,                use: [MiniCssExtractPlugin.loader, 'css-loader']            },            {                test: /\.less$/,                use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']            },            {                test: /\.(png|gif|svg|ico|jpe?g|woff2?|eot|ttf|otf)(\?.*)?$/,                use: [{                    loader: 'url-loader',                    options: {                        limit: 8192,                    }                }]            }        ]    },    plugins: [        new VueLoaderPlugin(),        new CleanWebpackPlugin(),        new MiniCssExtractPlugin({            filename: utils.genFilePathWithName('[name].css')        }),        new HtmlWebpackPlugin({            filename: 'index.html',            template: path.resolve(__dirname, '../index.html'),            favicon: 'src/common/assets/img/favicon.ico',            inject: true        })    ],    optimization: {        splitChunks: {            cacheGroups: {                vendor: {                    test: /[\\/]node_modules[\\/]/,                    name: 'vendor',                    chunks: 'initial',                    priority: -10                },                default: {                    minChunks: 2,                    priority: -20,                    reuseExistingChunk: true                }            }        }    }};

【相关文献】

[1] Github: https://github.com/hi-sunshine/Zero-X

[2] webpack-cli: https://www.npmjs.com/package/webpack-cli

[3] webpack-dev-server: https://www.npmjs.com/package/webpack-dev-server

[4] webpack-merge: https://www.npmjs.com/package/webpack-merge

[5] webpack entry: https://webpack.js.org/concepts/#entry

[6] html-webpack-plugin: https://www.npmjs.com/package/html-webpack-plugin

[7] webpack output: https://webpack.js.org/concepts/output/

[8] mini-css-extract-plugin: https://www.npmjs.com/package/mini-css-extract-plugin

[9] clean-webpack-plugin: https://www.npmjs.com/package/clean-webpack-plugin

【小结】

是不是看起来还不错?下一节,我们搞一下开发环境和生产环境的差异化配置。请持续关注《教你自己搭建一个脚手架(二)》哦,敬请期待~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值