前言
上一篇文章 基于webpack打包多页应用,对前端工程化的思考 107👍
这也是我第一个 点赞破百 的文章,感谢掘友的支持,开心😃
总结起来,上一篇文章并没有写什么实际性的东西,可能大家对工程化比较感兴趣,其中也有不少掘友热情的探讨
十分感谢大家不同的意见,如有问题,都可以在下面留言哦!
- 完整源码已放github,并配有完整注释,欢迎直接去github上看源码
- 如有帮助,欢迎star,万分感谢
下面我们接着上一篇文章,谈论些更多有意思的东西。
按照约定,路由自动生成
单页面应用中,有很多框架都做了该功能,比如基于vue的Nuxt.js,基于react的UmiJS。比起单页面,多页面中并没有路由嵌套,路由传参等复杂的路由结构,所以在多页面中实现路由自动生成要比单页面简单些。
为了使用更加"野性",我在新建 文件夹 的时候会自动生成路由和配套的html,css,js,效果如下:
实现思路
代码实现
自动生成路由最主要的是文件的自动监听,我使用的是chokidar(速度还可以,比watch模块强太多了)。为了便于维护,我把此功能抽离成独立的 webpack插件 大家可在源码文件plugins/router-auto-plugin.js中查看插件全部源码。核心代码如下:
compiler.hooks.entryOption.tap("invoke", () => {
// 初始化chokidar
const watcher = chokidar.watch(this.watchPath, { persistent: true });
watcher.on("addDir", async (pathname, store) => {
// 判断文件格式,只要创建文件夹,自动生成里面的文件
const p = pathname.split(path.sep).pop();
if (p === "pages") return false;
// 生成对应的html css 和 js
this.writeInit(p);
// 跟新路由
this.changeRouterTemplate();
});
watcher.on("unlinkDir", async () => {
console.log(chalk.blue(`删除成功!`));
this.changeRouterTemplate();
});
});
因为不存在路由嵌套问题,每次新增文件相当于循环一次特定的配置模板
//生成动态配置核心方法
const changeRouterCompile = (meta, filePath, templatePath, text) => {
if (fs.existsSync(templatePath)) {
const content = fs.readFileSync(templatePath).toString();
const reslut = handlebars.compile(content)(meta);
fs.writeFileSync(filePath, beautify(reslut));
}
console.log(chalk.blue(`${text}`));
};
// 根据模板生成路由信息
changeRouterTemplate() {
const list = fs.readdirSync(this.watchPath).map((v) => ({
name: v,
}));
changeRouterCompile(
{ list },
`${root}/config/routerTemplate.js`,
`${root}/config/template/routerTemplate.js.hbs`,
`路由生成成功!`
);
changeRouterCompile(
{ list },
`${root}/config/entryTemplate.js`,
`${root}/config/template/entryTemplate.js.hbs`,
`entry生成成功!`
);
}
.hbs文件就是要循环的模板
// entryTemplate.js.hbs
module.exports = {
entry: {
{{#each list}}
{{name}}: path.join(__dirname, "../src/pages/{{name}}/{{name}}.js"),
{{/each}}
},
};
当然此方法只能用于简单场景,复杂的路由生成还需要使用ast(抽象语法树)来实现。
每生成一次页面就意味着更改一次webpack配置,所有就需要重启webpack,硬伤😐
js,css tree-shaking
tree-shaking 就是把没用到的js和css不打包到 生成环境 中,这样可以大大减少我们代码体积
js tree-shaking
webpack5已经自带js的tree-shaking,在webpack4中当我们把mode设置为production时已经开启了tree-shaking,但是只对ES6模块的代码有效,所以我们还需设置babel在处理js时不让他转化为CommonJS
.babelrc
{
"presets": [
["@babel/preset-env",
{
"modules": false
}
]
]
}
关于为什么不在项目中使用webpack5,我劝你在等等,说多了都是泪😭
css tree-shaking
css tree-shaking可能是最容易被大家忽略的优化点,但在实际开发中有很多无用的css充斥在代码里,我们可以进行如下配置来进行tree-shaking,主要用到以下插件
"purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0",
"glob-all": "^3.2.1" // 用于匹配路径,简化操作
配置如下
plugin:[
...
new PurifyCss({
paths: glob.sync([
path.resolve(__dirname, "./src*.html"),
path.resolve(__dirname, "./src/*.js"),
]),
}),
...
]
其他优化
自动生成css前缀
增加css前缀,再也不被兼容性所烦恼,具体配置如下:
npm i postcss-loader autoprefixer -D
// webpack.pro.config.js
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: "../../",
plugins: [require("autoprefixer")],
},
},
"css-loader",
],
},
使用HappyPack提高打包速度
webpack在打包时最耗时的就是众多的loader转化处理,这时我们可以使用HappyPack开启多个线程从而提高打包速度
npm i happypack -D
// webpack.pro.js
const HappyPack = require('happypack')
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); // 自动获取线程数
rules:[
...
{
test: /\.js$/,
exclude: "/node_modules/",
loader: "HappyPack/loader?id=js", // 注意不要在rules中使用options配置,需要在plugins中配置
},
...
]
plugins:[
new HappyPack({
id: "js", // 对应上面loader?id=js
loaders: [
{
loader: "babel-loader",
options: {
plugins: ["dynamic-import-webpack"],
cacheDirectory: true,
},
},
],
threadPool: happyThreadPool,
}),
]
使用cache-loader设置缓存
我们每次执行构建都会把所有的文件都重复编译一遍,那对于那些不变的文件,可以不可以缓存起来呢?使用cache-loader就可以做到
请注意,保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader。
比如我们缓存babel-loader
{
test: /\.ext$/,
use: ["cache-loader", "babel-loader"],
include: path.resolve("src"),
},
使用expose-loader将模块暴露为全局变量
开发多页面少不了会使用到jquery,那我们如何把jquery暴露到全局,不用在每个页面都引用呢
npm i jquery -S
npm i expose-loader -D
// webpack.base.config
rules:[
...
{
test: require.resolve("jquery"),
use: "expose-loader?$",
}
...
]
plugins:[
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
}),
]
制作为命令行工具
类比创建vue项目,我们使用vue-cli提供的命令行vue create <app-name>就可以创建一个完整的vue项目。那么我们可不可以做一个自己的命令行工具呢?下面我们就一步一步来实现。效果如下:
新建项目
新建文件夹lyh-cli使用npm init -y初始化项目,这时会生成一个package.json文件,新增bin配置项,配置脚手架名称
{
"name": "lyh-cli",
"version": "1.0.0",
"description": "lyh-pages 专属脚手架",
"main": "index.js",
"bin": {
"lyh": "./bin/index.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^6.2.1"
}
}
新建bin文件夹和index.js文件,此时项目文件如下所示:
下面两个步骤至关重要:
- 打开
bin/index.js文件,在文件头部加上一下标识,及测试代码:
#!/usr/bin/env node
console.log("init");
-
命令行执行
npm link将npm模块链接到对应项目中去 -
在命令行输入
lyh,可以成功显示出我们在bin/index.js写的测试代码
现在我们的脚手架架子已经搭建完毕✌。
实现lyh create <project name>命令
安装依赖
npm i commander -S
commander完整的 node.js 命令行解决方案,是开发脚手架必不可少的利器
下面我们先来思考,在执行npm create命令时,都需要干哪些事
具体代码实现
// 项目所用依赖
## commander node 命令行插件 必须
## figlet 给文字加特效
## clear 清除命令行
## chalk 画笔工具
## download-git-repo 从git上clone代码
## ora 显示 loading
#!/usr/bin/env node
// bin/index.js
const program = require("commander");
program.version(require("../package.json").version);
const init = require("../lib/init");
program
.command("create <name>")
.description("初始化项目")
.action((name) => {
init(name);
});
program.parse(process.argv);
// lib/init.js
const { promisify } = require("util");
const figlet = promisify(require("figlet"));
const chalk = require("chalk"); // 画笔
const clear = require("clear");
const { clone } = require("./clone");
const init = async (projectName) => {
clear(); // 清理命令行
console.log(chalk.green(await figlet("lyh-cli")));
await clone("github:lyh0371/lyh-pages", projectName);
console.log(
chalk.green(`
下载成功!
进入项目: cd ${projectName}
安装依赖: cnpm/npm install
运行项目: npm run dev
打包项目: npm run build
`)
);
};
module.exports = init;
//lib/clone.js
const { promisify } = require("util");
module.exports.clone = async (repo, desc) => {
const download = promisify(require("download-git-repo"));
const ora = require("ora");
const process = ora(`下载.....${repo}`);
process.start();
await download(repo, desc);
process.succeed();
};
发布到npm
发布到npm很简单,只要你有npm账号(没有的先在官网注册,在这里就不赘述了),只要以下两步即可发布一个npm包
- 使用
npm login输入个人信息登录到npm - 使用
npm publish发布
- 如果在发布的时候报错,可能是你起的包名已经被别人占用,在
package.json换一个name即可- 每次
npm publish前都需要改一个版本(package.json的version)字段
最后
- 完整源码已放github,并配有完整注释,欢迎直接去github上看源码
- 如有帮助,欢迎star,万分感谢
如有帮助,欢迎点赞关注哟😁