分享版本: webpackV4.X (企企项目PC端-webpack: 4.29.6、webpack-cli: 3.1.1)
分享初衷: 本文我们结合企企项目(下面相关代码片段主要取至我们项目) 讲一下Webpack,
它能干什么及为什么要使用它,把我整理的笔记分享给大家,和大家共同学习。
分享目录:
1.webpack是什么
2.webpack能干什么
3.webpack使用场景
4.webpack常用配置
5.分享扩展
6.分享总结

一问?

  • 与webpack类似的工具还有哪些? 谈谈你为什么选择使用或放弃webpack?
一、webpack是什么

webpack是一个现代JavaScript应用程序的静态模块打包器(module bundler), 当webpack处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。

二、webpack能干什么

使用Webpack作为前端构建工具:

  • 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

构建其实是工程化、自动化思想在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。 构建给前端开发注入了更大的活力,解放了我们的生产力。

三、webpack使用场景

主要结合企企项目,结合各位大佬(宋永强、欧兴扬、TonyJiang、王濯、郭俊、陆海波、雷远亮、袁烈权、李鹏飞 等) 配置的代码进行分析。主要简单过下在项目中webpack都做了什么,有哪些配置文件,使用的场景是什么。

企企项目目录:

  • link => src => main => app - index.tsx (入口文件目录)、router-defines (路由定义)
  • link => src => main => screens (前端的入口大多在这里, 供应链模块 - archive、inventory、inventory-acctg、purchase、report...)

webpack相关配置文件:

  • package.json
  • webpack.config.js
  • webpack.dev.config.js
  • webpack.dll.config.js
  • webpack.serve.config.js
  • postcss.config.js
  • tsconfig.json
  • proxy.js
  • global.d.ts
  • .babelrc
  • .eslintrc
  • .eslintignore
  • .gitignore
  • .......

四、webpack常用配置
1.入口(entry)

每个 HTML 页面都有一个入口起点

  • 单页应用(SPA):一个入口起点
  • 多页应用(MPA):多个入口起点
module.exports = {
entry: {
app: ['./src/main/index'], // 企企项目入口配置
},
};
1.1 单入口打包
  • 自动产生html,并引入打包后的文件
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins:[
new HtmlWebpackPlugin({
filename:'index.html', // 打包出来的文件名
template:path.resolve(__dirname,'./src/index.html'),
hash:true, // 在引用资源的后面增加hash戳
})
]
1.2 多入口打包
  • 根据不同入口生成多个js文件,引入到不同html中
── src
├── entry-1.js
└── entry-2.js
  • 多入口需要配置多个entry
entry:{
jquery:['jquery'], // 打包jquery
entry1:path.resolve(__dirname,'../src/entry-1.js'),
entry2:path.resolve(__dirname,'../src/entry-2.js')
},
output:{
filename:'[name].js',
path:path.resolve(__dirname,'../dist')
},
  • 产生多个Html文件
new HtmlWebpackPlugin({
filename:'index.html',
template:path.resolve(__dirname,'../public/template.html'),
hash:true,
chunks:['jquery','entry1'], // 引入的chunk 有jquery,entry
}),
new HtmlWebpackPlugin({
filename:'login.html',
template:path.resolve(__dirname,'../public/template.html'),
hash:true,
inject:false, // inject 为false表示不注入js文件
chunksSortMode:'manual', // 手动配置代码块顺序
chunks:['entry2','jquery']
}) inject有四个值: true body head false
- true 默认值,script标签位于html文件的 body 底部
- body script标签位于html文件的 body 底部
- head script标签位于html文件的 head中
- false 不插入生成的js文件,这个几乎不会用到的

以上的方式不是很优雅,每次都需要手动添加HtmlPlugin应该动态产生html文件,像这样:

let htmlPlugins = [
{
entry: "entry1",
html: "index.html"
},
{
entry: "entry2",
html: "login.html"
}
].map(
item =>
new HtmlWebpackPlugin({
filename: item.html,
template: path.resolve(__dirname, "../public/template.html"),
hash: true,
chunks: ["jquery", item.entry]
})
);
plugins: [...htmlPlugins]
2.出口(output)
  • path
  • filename
  • chunkFilename
  • publicPath
  • hashDigestLength
module.exports = {
const ROOT_PATH = path.resolve(__dirname);
const entryPointName = 'app';
const version = require('./package.json').version; => "version": "1.0.0", output: {
publicPath: '',
filename: `[name].[chunkhash:8].min.js`,
path: path.resolve(ROOT_PATH, dist),
chunkFilename: `${entryPointName}-${version}-[name]-[chunkhash:8].min.js`,
hashDigestLength: 8, // 散列摘要的前缀长度,默认为 20。 N.B: 缓存优化 chunkhash contenthash 不配置该属性的话,HashOutputWebpackPlugin无法生效;其需要用到hashDigestLength属性
},
};

chunkFilename 和 filename 的区别:

  • filename 指列在 entry 中,打包后输出的文件的名称,打包入口文件
  • chunkFilename 指未列在 entry 中,却又需要被打包出来的文件的名称(如一些懒加载的代码) 用来打包import('module')方法中引入的模块。

https://www.cnblogs.com/skychx/p/webpack-filename-chunkFilename.html

3.模式(mode)

webpack的mode配置用于提供模式配置选项告诉webpack相应地使用其内置的优化,mode有以下三个可选值

  • development
  • production
  • none
// webpack.dev.config.js
module.exports = env => {
return resources.createServeConfig({
....
mode: 'development',
....
});
}; //webpack.config.js
module.exports = function (argv) {
const configs = [];
const prodConfig = resources.createServeConfig(
....
mode: 'production',
....
},
isProduction,
);
configs.push(prodConfig);
return configs;
}; // webpack.dev.resource.js
const path = require("path");
const merge = require("webpack-merge");
const argv = process.argv.slice(2);
const needSourceMap = argv.includes('--sourcemap');
const CODE_MINIFY = dist === 'dist'; module.exports = {
createServeConfig(customConfig, isProduction) {
const ret = merge(
{
devServer:{....},
mode: 'development',
output:{....},
resolveLoader:{....},
resolve:{....},
module: {....},
plugins: [....],
},customConfig
) /**
* N.B: sourcemap 开启和关闭
* 背景:web2 inte 等开发提测环境需要source-map,以方便调试。(souce-map开启后无法去除注释)
* 线上环境需要关闭sourcemap以获得更好的压缩效果(会去除注释,减小更多的体积)。(等迭代或者产品稳定了,bug少了再去掉source-map)
*
* 设想1:用source-map模式,将.map文件独立出来,在线上的时候不上传.map文件,以保证测试环境和线上环境的代码是一致的。
* 后果:额,source-map模式只要上到外网,其它人可访问,就会导致源码泄漏(可通过manifest拿到所有的js文件,进而得到.map文件,就拿到源码了)
*
* 设想2:用eval模式
* 后果:代码体积大些,外网用户可看到被编译成es5的源码(应该是可以接受的)
*
* 所以只有内网环境才可以上source-map模式,外网环境要source-map的话,就只能用eval模式了(就是体积大些,等稳定了可以去掉eval)(不会泄漏源码,代码为es5的source-map)
*
* 结论: 只能选择用与不用eval模式。
*/
let config = ret;
if (needSourceMap) {
config = Object.assign(
{
// 自动化测试使用eval模式
devtool: CODE_MINIFY ? 'source-map' : 'eval',
},
ret,
);
} /**
* N.B: eval also belongs to cheap-source-map, 导致无法去掉注释
* issues => [Removing comments does not work when setting devtool to eval](https://github.com/webpack-contrib/uglifyjs-webpack-plugin/issues/180)
*/
config = merge(
isProduction ? {}: {devtool: 'eval'},
config,
);
console.log('config.devtool', config.devtool);
return config;
},
}; [package.json]
"scripts":{
"dev":"webpack-dev-server --env.development --config ./build/webpack.base.js"
"build":"webpack --env.production --config ./build/webpack.base.js"
}

二问?

  • Loader和Plugin的不同? 有哪些常见的Loader和Plugin?他们是解决什么问题的?
4.加载器(loaders)

通过使用不同的Loader,Webpack可以把不同的文件都转成JS文件,比如CSS、ES6/7、JSX等

  • test:匹配处理文件的扩展名的正则表达式
  • use:loader名称,就是你要使用模块的名称
  • include/exclude:手动指定必须处理的文件夹或屏蔽不需要处理的文件夹
  • options:为loaders提供额外的设置选项

loader三种写法:

  • loader
  • use
  • use + loader
// loader
module: {
rules: [
{
test: /\.css/,
loader:['style-loader','css-loader']
}
]
} // use
module: {
rules: [
{
test: /\.css/,
use:['style-loader','css-loader']
}
]
} // use + loader
module: {
rules: [
{
test: /\.css/,
include: path.resolve(__dirname,'src'),
exclude: /node_modules/,
use: [{
loader: 'style-loader',
options: {
insert:'top'
}
},{
loader:'css-loader',
options:{
// 如果css文件引入其他文件@import
simportLoaders:2
}
},'postcss-loader','sass-loader']
}
]
}

样式相关Loaders

4.1 cache-loader
  • 在一些性能开销较大的 loader 之前添加 cache-loader,以便将结果缓存到磁盘里。
  • 请注意,保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader。
4.2 thread-loader

使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。

在 worker 池中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能生成新的文件。
  • 这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
  • 这些 loader 无法获取 webpack 的配置。

每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。

请仅在耗时的操作中使用此 loader!

4.3 style-loader + css-loader
参考地址:
  • 推荐将style-loader与css-loader一起使用,然后把loader添加到你的webpack配置中。
  • webpack默认是可以找包js文件的,当要打包css的时候就需要引入style-loader和css-loader。css-loader的作用是把css文件进行转码,而style-loader的作用是把转码后的css文件插入到相应的文件中去。
module.exports = {
module: {
rules: [
// css文件
{
test: /\.css$/,
use: [
'cache-loader',
'thread-loader',
'css-hot-loader',
MiniCssExtractPlugin.loader, //=> 抽离CSS为独立的文件
// 'style-loader', //=>把CSS插入到HEAD中
'css-loader', //=>编译解析@import/URL()这种语法
{
loader: 'postcss-loader',
options: {
plugins: function () {
return [
postcssPresetEnv({
stage: 0,
}),
require('autoprefixer'),
];
},
},
},
],
},
],
},
}
4.4 postcss-loader

为了浏览器的兼容性,有时候我们必须加入-webkit,-ms,-o,-moz这些前缀

  • Trident内核:主要代表为IE浏览器, 前缀为-ms
  • Gecko内核:主要代表为Firefox, 前缀为-moz
  • Presto内核:主要代表为Opera, 前缀为-o
  • Webkit内核:产要代表为Chrome和Safari, 前缀为-webkit

PostCSS 的主要功能只有两个

  • 第一个就是前面提到的把 CSS 解析成 JavaScript 可以操作的 抽象语法树结构(Abstract Syntax Tree,AST)
  • 第二个就是调用插件来处理 AST 并得到结果
// index.css
::placeholder {
color: red;
} // postcss.config.js
const autoprefixer = require('autoprefixer');
const mqpacker = require('css-mqpacker');
module.exports = (ctx) => ({
plugins: [
autoprefixer({
browsers: [ '>0.5%', 'last 2 versions', 'not dead', 'not op_mini all' ]
}),
mqpacker()
]
}); // webpack.config.js
{
test:/\.css$/,
use:[MiniCssExtractPlugin.loader,'css-loader','postcss-loader'],
include:path.join(__dirname,'./src'),
exclude:/node_modules/
}
4.5 less/sass/stylus-Loader
参考地址:
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
},
{
test: /[^(.g)]+\.scss$/,
enforce: 'pre', //=>强制在下面的loader之前执行
use: [
.....
{
loader: 'sass-loader',
},
{
loader: 'sass-resources-loader', // 通过 sass-resources-loader 全局注册 Sass 变量
options: {
resources: ['./src/theme/custom-variables.scss'],
},
},
],
},
}

图片相关loaders

4.6 file/url/svg-sprite-loader
参考地址:
  • file-loader: 解决CSS等文件中的引入图片路径问题。
  • url-loader: 当图片小于limit的时候会把图片BASE64编码(1024bit),大于limit参数的时候还是使用file-loader进行拷贝。
  • svg-sprite-loader: 会把引入的svg塞到一个个symbol中,合成一个大的svg,最后将这个大的svg放入body中。symbol的id如果不特别指定,就是你的文件名。
module: {
//=>模块规则:使用加载器(默认从右向左执行)
rules: [
{
test: /\.(woff|woff2|eot|ttf)(\?.*$|$)/,
use: ['url-loader'],
},
{
test: /\.(svg)$/i,
use: ['svg-sprite-loader'],
include: svgDirs, // 把 svgDirs 路径下的所有 svg 文件交给 svg-sprite-loader 插件处理
},
{
test: /\.(png|jpg|gif|svg)$/,
use: ['url-loader?limit=8192&name=images/[contenthash:8].[name].[ext]'],
exclude: svgDirs,
},
{
test: /\.html$/,
use: ['html-withimg-loader']
}
]
}

问题: 使用html-withimg-loader对html中引入的图片打包,打包后图片无法显示,路径上多出default对象

https://blog.csdn.net/zc135565/article/details/104166781

脚本(JS、TS、TSX)相关loader

4.7 babel/esbuild/ts-loader
参考地址:
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
babelrc: false,
compact: false,
sourceMap: false,
comments: false, // 布尔类型,表示打包编译之后不产生注释内容
},
},
],
include: [/node_modules/, /\/packages\//],
},
{
test: [/\.tsx?$/],
use: [
{
loader: 'babel-loader',
options: {
babelrc: false,
plugins: [require.resolve('@q7/build-scripts/lib/plugins/babel/babel-plugin-dynamic-router')]
}
},
{
loader: 'esbuild-loader',
options: {
tsconfigRaw: require('./tsconfig.json'), //=> 如果您有一个tsconfig.json文件,esbuild loader将自动检测它。或者也可以通过tsconfigRaw选项直接传递:
loader: 'tsx',
minify: false,
},
},
{
loader: 'ts-loader',
options: {
happyPackMode: true,
experimentalWatchApi: true,
transpileOnly: true, // IMPORTANT! use transpileOnly mode to speed-up compilation
},
},
],
include: [/router-defines(\/|\\)index/],
exclude: [/node_modules/, /\.scss.ts$/, /\.test.tsx?$/],
},
  • .babelrc
{
"presets": ["@babel/preset-typescript"],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"react-hot-loader/babel"
]
}
  • tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"inlineSources": true,
"sourceRoot": "/",
"target": "es5",
"module": "esnext",
"moduleResolution": "node",
"downlevelIteration": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"removeComments": false,
"baseUrl": ".",
"outDir": "lib",
"allowJs": true,
"checkJs": false,
"paths": {
"@axios": ["./src/axios"],
....
},
"plugins": [{ "transform": "ts-optchain/transform" }],
"jsx": "react",
"alwaysStrict": true,
"noUnusedLocals": false,
"importHelpers": true,
"noImplicitThis": false,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"lib": ["esnext", "es7", "es6", "es5", "es2015.promise", "es2015.generator", "dom"],
"skipLibCheck": true,
"typeRoots": ["node", "node_modules/@types", "global.d.ts"]
},
"exclude": ["**/node_modules", "staticLib", ".cache", ".idea", "dist", "jest"]
}
5.插件(Plugins)
5.1 html-webpack-plugin
  • 我们希望自动能产出HTML文件,并在里面引入产出后的资源
  • chunksSortMode还可以控制引入的顺序
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path'); module.exports = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html', //=> 指定自己的模板
filename: 'index.html', //=> 输出的文件名
hash: true,
minify: {//=> 压缩HTMl文件
collapseWhitespace: true, //=> 删除空白符和换行符
removeComments: true, //=> 去掉注释
removeAttributeQuotes: true, //=> 去掉属性的双引号
removeEmptyAttributes: true //=> 去掉空属性
},
chunks:['common','index'],
chunksSortMode:'manual'//对引入代码块进行排序的模式
})
]
};
5.2 clean-webpack-plugin
  • 打包前先清空输出目录
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const path = require('path'); module.exports = {
plugins: [
new CleanWebpackPlugin(['build/dll'], { root: path.resolve() })
],
};
5.3 mini-css-extract-plugin
  • 样式提取插件,分离CSS为独立的文件。
  • 因为CSS的下载和JS可以并行,当一个HTML文件很大的时候,可以把CSS单独提取出来加载
  • 建议 mini-css-extract-plugin 与 css-loader 一起使用。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require('path'); module.exports = {
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
};
5.4 压缩CSS和JS
参考文档:
  • optimize-css-assets-webpack-plugin 样式优化压缩/配合添加前缀等
  • terser-webpack-plugin 压缩JavaScript插件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const TerserWebpackPlugin = require('terser-webpack-plugin');
const path = require('path'); module.exports = {
mode: 'production',
optimization: {
minimizer: [ // 压缩JS资源的
new TerserWebpackPlugin({
parallel: true,
}),
new OptimizeCSSAssetsPlugin({ //压缩css资源的
assetNameRegExp:/\.css$/g,
cssProcessor:require('cssnano') //cssnano是PostCSS的CSS优化和分解插件。cssnano采用格式很好的CSS,并通过许多优化,以确保最终的生产环境尽可能小。
})
]
},
},
5.5 webpack-retry-chunk-load-plugin
  • 一个Webpack插件,可重试加载失败的块
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new RetryChunkLoadPlugin({
retryDelay: 10000, // 间隔多久重启一次
maxRetries: 5, // 最多重启次数
}),
]
}
5.6 copy-webpack-plugin
  • 拷贝静态文件
  • 有时项目中没有引用的文件也需要打包到目标目录
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path'); module.exports = {
plugins: [
new CopyWebpackPlugin([{
from: path.resolve(__dirname,'src/assets'), // 静态资源目录源地址
to:path.resolve(__dirname,'dist/assets') // 目标地址,相对于output的path目录
}])
]
}
5.7 webpack-bundle-analyzer
  • 是一个webpack的插件,需要配合webpack和webpack-cli一起使用。这个插件的功能是生成代码分析报告,帮助提升代码质量和网站性能.
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
generateStatsFile: true, // 是否生成stats.json文件
})
]
}

5.8 webpack-manifest-plugin
  • webpack-manifest-plugin  生成一个manifest.json 默认文件名。
  • 也是一个文件清单, 内容是打包前文件对应打包后的文件名。
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
module.exports = {
plugins: [
new WebpackManifestPlugin({
fileName: 'manifest.json',
}),
]
}
5.9 webpack-plugin-hash-output
  • 用于使用最终文件内容的md5哈希替换webpack chunkhash的插件
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
module.exports = {
plugins: [
/**
* HashOutputWebpackPlugin
* N.B: 缓存优化:contentHash [chunkhash][contenthash] 用真实的基于内容的contenthash来替换webpack自带的contenthash(自带的在每次打包时,有几个chunk的contenthash会变化(尽管chunk里的内容没变))
* issues => [Webpack 4 chunkhash/contenthash can vary between builds](https://github.com/webpack/webpack/issues/7179)
* HashOutput必须放在首位
* 必须设置 output.hashDigestLength 为[chunkhash:8]这里所保留的hash长度(8)
*/
new HashOutputWebpackPlugin(),
]
}
5.10 webpack内置插件
参考文档:
  • WatchIgnorePlugin:当处于监视模式(watch mode)下,符合给定地址的文件或者满足给定正则表达式的文件的改动不会触发重编译。表示符合条件的文件将不会被监视。
  • ProgressPlugin: 构建进度插件。
  • HashedModuleIdsPlugin: 该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 建议用于生产环境。
  • DefinePlugin:创建一些在编译时可以配置的全局常量。
  • IgnorePlugin:用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去,第一个是匹配引入模块路径的正则表达式,第二个是匹配模块的对应上下文,即所在目录名。
const webpack = require('webpack');

module.exports = {
plugins: [
new webpack.ProgressPlugin(), // 构建进度条
new webpack.WatchIgnorePlugin([/\.js$/, /\.d\.ts$/]), // 忽略掉 d.ts 文件,避免因为编译生成 d.ts 文件导致又重新检查。
new webpack.HashedModuleIdsPlugin(), // N.B: 缓存优化: moduleId 用hashId来替换数字id索引(用数字id来当作manifest里的索引,可能会因为顺序变化而变化,导致缓存失效)
new webpack.DefinePlugin({
'process.env.HMR': `"none"`,
'process.env.isProd': `"${isProduction}"`,
'process.env.publicPath': `"${publicPath}"`,
'process.env.sp_pro_path': `"${SP_PRO_PATH}"`,
}),
new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
]
}
6.解析(resolve)
resolve: {
alias: Object.assign({
'react': path.resolve(path.join(__dirname, './node_modules/react')),
'react-dom': path.resolve(path.join(__dirname, './node_modules/react-dom')),
'react-hot-loader': path.resolve(path.join(__dirname, './node_modules/react-hot-loader')),
......
},
createKaledioAlias({
'@q7/athena': (project, p) => {
Object.assign(p, {
'@athena-ui': path.resolve(ROOT_PATH, './node_modules/@q7/athena/src'),
'@grid': path.resolve(ROOT_PATH, './node_modules/@q7/athena/src/components/Grid'),
'@ag-grid': path.resolve(ROOT_PATH, './node_modules/@q7/athena/src/components/ag-grid'),
'@q7/athena/lib': path.resolve(ROOT_PATH, './node_modules/@q7/athena/src'),
})
}
})
),
plugins: [
new TsconfigPathsPlugin({
configFile: './tsconfig.json',
logLevel: 'info',
extensions: ['.ts', '.tsx'],
mainFields: ['main', 'browser'],
// baseUrl: "/foo"
}),
],
},
6.1 extensions

指定extension之后可以不用在require或是import的时候加 文件扩展名,会依次尝试添加扩展名进行匹配

resolve: {
extensions: [".js",".jsx",".json",".css"]
},
6.2 alias

配置别名可以加快webpack查找模块的速度

resolve: {
alias: {
'react': path.resolve(path.join(__dirname, './node_modules/react')),
'react-dom': path.resolve(path.join(__dirname, './node_modules/react-dom')),
'react-hot-loader': path.resolve(path.join(__dirname, './node_modules/react-hot-loader')),
'@mobx': path.resolve(ROOT_PATH, 'node_modules/mobx'),
'@q7/athena/src': path.join(__dirname, 'src'),
'@q7/athena/lib': path.join(__dirname, 'lib'),
.....
},
}
6.3 mainFields

当从 npm 包中导入模块时(例如,import * as D3 from 'd3'),此选项将决定在 package.json 中使用哪个字段导入模块。根据 webpack 配置中指定的 target 不同,默认值也会有所不同。

resolve: {
mainFields: ['browser', 'module', 'main'], // 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
mainFields: ["module", "main"], // target 的值为其他时,mainFields 默认值为:
}
6.4 resolveLoader

resolve.resolveLoader用于配置解析 loader 时的 resolve 配置,默认的配置

module.exports = {
resolveLoader: {
modules: [
path.resolve(__dirname, '../node_modules'),
path.resolve(process.cwd(), 'node_modules'),
],
},
};

三问?

  • 你做过webpack优化吗 ?那如何利用webpack来优化前端性能?
7.优化(optimization)

代码分割的意义

  • 对于大的Web应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被用到。
  • webpack有一个功能就是将你的代码库分割成chunks语块,当代码运行到需要它们的时候再进行加载。 适用的场景
  • 抽离相同代码到一个共享块
  • 脚本懒加载,使得初始下载的代码更小

提取公共代码

  • 大网站有多个页面,每个页面由于采用相同技术栈和样式代码,会包含很多公共代码,如果都包含进来会有问题
  • 相同的资源被重复的加载,浪费用户的流量和服务器的成本;
  • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
  • 如果能把公共代码抽离成单独文件进行加载能进行优化,可以减少网络传输流量,降低服务器成本

如何提取

  • 基础类库,方便长期缓存
  • 页面之间的公用代码
  • 各个页面单独生成文件
//=> 从 os.cpu 获取并行数量,如果取不到,默认按照4核(需要和运维确认)
const cpuNumber = 1; optimization: {
usedExports: true, //=> 开启,只要导出的模块都tree shaking(摇树),意思是把没有使用到的文件删除,去除无效的代码,减小文件体积
sideEffects: true, //=> 允许我们通过配置标识我们的代码是否有副作用,从而提供更大的压缩空间。(模块的副作用指的就是模块执行的时候除了导出成员,是否还做了其他的事情。)
occurrenceOrder: false, //=> 标记模块的加载顺序,使初始包更小
removeEmptyChunks: false, //=> 检测或移除空的chunk,设置为 false 以禁用这项优化
removeAvailableModules: true, //=> 删除已可用模块
minimizer: [ //=> 来最小化包
new TerserWebpackPlugin({ //=> 该插件使用 terser 来压缩 JavaScript。
exclude: [/^manifest\..*\.js/], //=> 匹配不需要压缩的文件。
sourceMap: true,
parallel: cpuNumber || true, //=> 使用多进程并发运行以提高构建速度。 并发运行的默认数量: os.cpus().length - 1 。
extractComments: true, //=> 是否将注释剥离到单独的文件中
}),
new OptimizeCSSAssetsPlugin({ //=> 主要是用来压缩css文件
cssProcessor: require('cssnano'), //=> 用于优化\最小化CSS的CSS处理器,默认为cssnano
cssProcessorPluginOptions: { //=> 传递给cssProcessor的插件选项
preset: ['advanced', {
discardComments: { removeAll: true },
reduceIdents: {
gridTemplate: false,
},
}],
},
})
],
runtimeChunk: { //=> 打包时生成一个体积很小runtime.xxx.js文件,用作映射其他chunk文件,目的是更新后,以较小的代价利用缓存,提升页面加载速度。
name: 'manifest',
},
splitChunks: { /=> 代码分割
cacheGroups: { //=> 设置缓存组用来抽取满足不同规则的chunk
css: {
name: 'initial-all-vendor',
test: /\.s?css$/,
chunks: 'all',
},
}
},
},
8.开发中serve(devServer)
const bodyParser = require('body-parser');
const devServerConfig = env => {
const { DEPLOY_ENV = 'local' } = env; let isLocal = true;
let host = DEPLOY_ENV; return {
inline: true, //=> 在dev-server的两种不同模式之间切换。默认情况下,应用程序启用内联模式(inline mode)
hot: true, //=> 启用webpack的模块热替换功能
host: '0.0.0.0', //=> 指定使用一个 host。默认是 localhost。
publicPath: '/', //=> 此路径下的打包文件可在浏览器中访问
disableHostCheck: true, //=>设置为true时,此选项绕过主机检查。不建议这样做,因为不检查主机的应用程序容易受到DNS重新连接攻击。
compress: false, //=> 启用gzip压缩
watch:true, //=> 开启监听
watchOptions: { //=> 监控的选项,可以监听文件变化,当它们修改后会重新编译
ignored: /node_modules\/(?!@q7)/, //=> 不需要进行监控哪个文件,对于某些系统,监听大量文件系统会导致大量的 CPU 或内存占用。这个选项可以排除一些巨大的文件夹
poll: 1233, //=> 每x秒检查一次变动, 通过传递true开启polling,或者指定毫秒为单位进行轮询。
aggregateTimeout:500, // 防抖 我一直输入代码
},
port: 3010, //=> 指定要监听请求的端口号
stats: { //=> 通过此选项,可以精确控制要显示的 bundle 信息。如果你想要显示一些打包信息,但又不是显示全部,这可能是一个不错的妥协
......
},
};
}; module.exports = {
devServerConfig: devServerConfig,
};
9.外部扩展(externals)

如果我们想引用一个库,但是又不想让webpack打包,并且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用,那就可以通过配置externals

const jQuery = require("jquery");
import jQuery from 'jquery'; // 通过下面externals配置,jQuery就不会被打包 <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script> externals: {
jquery: 'jQuery'//如果要在浏览器中运行,那么不用添加什么前缀,默认设置就是global
},
module: {}
10.统计信息(stats)
  • 日志太多太少都不美观
  • 可以修改stats

预设

替代

描述

errors-only

none

只在错误时输出

minimal

none

发生错误和新的编译时输出

none

false

没有输出

normal

true

标准输出

verbose

none

全部输出

module.exports = {
//...
stats: 'verbose' //=> 全部输出
// stats: 'errors-only' //=> 只在错误时输出
}; stats:{
progress: true, // 显示打包进度
colors: true, // 打包日志显示颜色
assets: true, // 添加资源信息
version: true, // 添加 webpack 版本信息
hash: true, // 添加 compilation 的哈希值
timings: true, // 添加时间信息
chunks: false, // 添加 chunk 信息(设置为 `false` 能允许较少的冗长输出)
chunkModules: false, // 将构建模块信息添加到 chunk 信息
warnings: false, // 添加警告
}
11.devtool
  • 此选项控制是否生成,以及如何生成 source map。
  • 选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

类型

含义

source-map

原始代码 最好的sourcemap质量有完整的结果,但是会很慢

eval-source-map

原始代码 同样道理,但是最高的质量和最低的性能

cheap-module-eval-source-map

原始代码(只有行内) 同样道理,但是更高的质量和更低的性能

cheap-eval-source-map

转换代码(行内) 每个模块被eval执行,并且sourcemap作为eval的一个dataurl

eval

生成代码 每个模块都被eval执行,并且存在@sourceURL,带eval的构建模式能cache SourceMap

cheap-source-map

转换代码(行内) 生成的sourcemap没有列映射,从loaders生成的sourcemap没有被使用

cheap-module-source-map

原始代码(只有行内) 与上面一样除了每行特点的从loader中进行映射

看似配置项很多, 其实只是五个关键字eval、source-map、cheap、module和inline任意组合

关键字

含义

eval

使用eval包裹模块代码

source-map

产生.map文件

cheap

不包含列信息(关于列信息的解释下面会有详细介绍)也不包含loader的sourcemap

module

包含loader的sourcemap(比如jsx to js ,babel的sourcemap),否则无法定义源文件

inline

将.map作为DataURI嵌入,不单独生成.map文件

  • eval eval执行
  • eval-source-map 生成sourcemap
  • cheap-module-eval-source-map 不包含列
  • cheap-eval-source-map 无法看到真正的源码

下次分享主题:source-map的使用以及如何调式打包后/线上的代码?

五、分享扩展
与webpack类似的工具还有哪些? 谈谈你为什么选择使用或放弃webpack?
1.grunt
  • 一句话:自动化。对于需要反复重复的任务,例如压缩(minification)、编译、单元测试、linting等,自动化工具可以减轻你的劳动,简化你的工作。当你在 Gruntfile 文件正确配置好了任务,任务运行器就会自动帮你或你的小组完成大部分无聊的工作。
  • 最老牌的打包工具,它运用配置的思想来写打包脚本,一切皆配置

优点

  • 出现的比较早

缺点

  • 配置项太多
  • 而且不同的插件可能会有自己扩展字段
  • 学习成本高,运用的时候需要明白各种插件的配置规则和配合方式

执行任务

  • npm run build
2.gulp
  • 基于 nodejs 的 steam 流打包
  • 定位是基于任务流的自动化构建工具
  • Gulp是通过task对整个开发过程进行构建

优点

  • 流式的写法简单直观
  • API简单,代码量少
  • 易于学习和使用
  • 适合多页面应用开发

缺点

  • 异常处理比较麻烦
  • 工作流程顺序难以精细控制
  • 不太适合单页或者自定义模块的开发

执行任务

  • npm run build
3.webpack
  • webpack 是模块化管理工具和打包工具。通过 loader 的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、AMD 模块、ES6 模块、CSS、图片等。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源
  • 还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载
  • 它定位是模块打包器,而 Gulp/Grunt 属于构建工具。Webpack 可以代替 Gulp/Grunt 的一些功能,但不是一个职能的工具,可以配合使用

优点

  • 可以模块化的打包任何资源
  • 适配任何模块系统
  • 适合SPA单页应用的开发

缺点

  • 学习成本高,配置复杂
  • 通过babel编译后的js代码打包后体积过大

执行任务

  • npm run build
4.rollup
  • rollup 下一代 ES6 模块化工具,最大的亮点是利用 ES6 模块设计,利用 tree-shaking生成更简洁、更简单的代码
  • 一般而言,对于应用使用 Webpack,对于类库使用 Rollup
  • 需要代码拆分(Code Splitting),或者很多静态资源需要处理,再或者构建的项目需要引入很多 CommonJS 模块的依赖时,使用 webpack
  • 代码库是基于 ES6 模块,而且希望代码能够被其他人直接使用,使用 Rollup

优点

  • 用标准化的格式(es6)来写代码,通过减少死代码尽可能地缩小包体积

缺点

  • 对代码拆分、静态资源、CommonJS模块支持不好

执行任务

  • npm run build
5.parcel
  • Parcel 是快速、零配置的 Web 应用程序打包器
  • 目前 Parcel 只能用来构建用于运行在浏览器中的网页,这也是他的出发点和专注点

优点

  • Parcel 内置了常见场景的构建方案及其依赖,无需再安装各种依赖
  • Parcel 能以 HTML 为入口,自动检测和打包依赖资源
  • Parcel 默认支持模块热替换,真正的开箱即用

缺点

  • 不支持 SourceMap
  • 不支持剔除无效代码(TreeShaking)
  • 配置不灵活

执行任务

  • npm run start
Loader和Plugin的不同? 有哪些常见的Loader和Plugin?他们是解决什么问题的?
  • Loader直译为加载器。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。
  • Plugin直译为插件。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
1.loader

loader

解决问题

babel-loader

把 ES6 或React转换成 ES5

css-loader

加载 CSS,支持模块化、压缩、文件导入等特性

eslint-loader

通过 ESLint 检查 JavaScript 代码

file-loader

把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件

url-loader

和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去

sass-loader

把Sass/SCSS文件编译成CSS

postcss-loader

使用PostCSS处理CSS

css-loader

主要来处理background:(url)还有@import这些语法。让webpack能够正确的对其路径进行模块化处理

style-loader

把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。

2.plugin

插件

解决问题

case-sensitive-paths-webpack-plugin

如果路径有误则直接报错

terser-webpack-plugin

使用terser来压缩JavaScript

pnp-webpack-plugin

Yarn Plug'n'Play插件

html-webpack-plugin

自动生成带有入口文件引用的index.html

webpack-manifest-plugin

生产资产的显示清单文件

optimize-css-assets-webpack-plugin

用于优化或者压缩CSS资源

mini-css-extract-plugin

将CSS提取为独立的文件的插件,对每个包含css的js文件都会创建一个CSS文件,支持按需加载css和sourceMap

ModuleScopePlugin

如果引用了src目录外的文件报警插件

InterpolateHtmlPlugin

和HtmlWebpackPlugin串行使用,允许在index.html中添加变量

ModuleNotFoundPlugin

找不到模块的时候提供一些更详细的上下文信息

DefinePlugin

创建一个在编译时可配置的全局常量,如果你自定义了一个全局变量PRODUCTION,可在此设置其值来区分开发还是生产环境

HotModuleReplacementPlugin

启用模块热替换

WatchMissingNodeModulesPlugin

此插件允许你安装库后自动重新构建打包文件

如何利用webpack来优化前端性能?
1.压缩CSS
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
optimization: {
minimize: true,
minimizer: [
//压缩CSS
new OptimizeCSSAssetsPlugin({}),
]
},
2.压缩JS
const TerserWebpackPlugin = require('terser-webpack-plugin');
optimization: {
minimize: true,
minimizer: [
//压缩JS
new TerserWebpackPlugin({})
]
}
3.压缩图片
{
test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
use: [
"file-loader",
{
loader: "image-webpack-loader",
options: {
mozjpeg: {
progressive: true,
quality: 65,
},
optipng: {
enabled: false,
},
pngquant: {
quality: "65-90",
speed: 4,
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75,
}
}
}
]
}
4.清除无用的CSS
  • 单独提取CSS并清除用不到的CSS
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgecssPlugin = require("purgecss-webpack-plugin"); module.exports = {
module: {
rules: [
{
test: /\.css$/,
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
"css-loader",
],
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new PurgecssPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
})
]
devServer: {},
};
5.Tree Shaking
  • 一个模块可以有多个方法,只要其中某个方法使用到了,则整个文件都会被打到bundle里面去,tree shaking就是只把用到的方法打入bundle,没用到的方法会uglify阶段擦除掉
  • 原理是利用es6模块的特点,只能作为模块顶层语句出现,import的模块名只能是字符串常量
  • webpack默认支持,在.babelrc里设置module:false即可在production mode下默认开启
module.exports = {
mode:'production',
devtool:false,
module: {
rules: [
{
test: /\.js/,
include: path.resolve(__dirname, "src"),
use: [
{
loader: "babel-loader",
options: {
presets: [["@babel/preset-env", { "modules": false }]],
},
},
],
}
],
}
}
6.Scope Hoisting
  • Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 "作用域提升",是在 Webpack3 中新推出的功能。
  • scope hoisting的原理是将所有的模块按照引用顺序放在一个函数作用域里,然后适当地重命名一些变量以防止命名冲突
  • 这个功能在mode为production下默认开启,开发环境要用 webpack.optimize.ModuleConcatenationPlugin插件

hello.js

export default 'Hello';

index.js

import str from './hello.js';
console.log(str);

main.js

var hello = ('hello');
console.log(hello);
7.代码分割
  • 对于大的Web应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被用到。
  • webpack有一个功能就是将你的代码库分割成chunks语块,当代码运行到需要它们的时候再进行加载
7-1.入口点分割
  • Entry Points:入口文件设置的时候可以配置
  • 这种方法的问题
    • 如果入口 chunks 之间包含重复的模块(lodash),那些重复模块都会被引入到各个 bundle 中
    • 不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
entry: {
index: "./src/index.js",
login: "./src/login.js"
}
7-2.动态导入和懒加载
  • 用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载 在给单页应用做按需加载优化时
  • 一般采用以下原则:
    • 对网站功能进行划分,每一类一个chunk
    • 对于首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载
    • 被分割出去的代码需要一个按需加载的时机
 7-3.preload(预先加载)
  • preload通常用于本页面要用到的关键资源,包括关键js、字体、css文件
  • preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度
  • 在资源上添加预先加载的注释,你指明该模块需要立即被使用
  • 一个资源的加载的优先级被分为五个级别,分别是
    • Highest 最高
    • High 高
    • Medium 中等
    • Low 低
    • Lowest 最低
  • 异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是 Low
<link rel="preload" as="script" href="utils.js">
import(
`./utils.js`
/* webpackPreload: true */
/* webpackChunkName: "utils" */
)
7-4.prefetch(预先拉取)
  • prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
<link rel="prefetch" href="utils.js" as="script">
button.addEventListener('click', () => {
import(
`./utils.js`
/* webpackPrefetch: true */
/* webpackChunkName: "utils" */
).then(result => {
result.default.log('hello');
})
});
7-5.preload vs prefetch
  • preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源
  • 而 prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源
  • 所以建议:对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch
7-6.提取公共代码 splitChunks
entry: {
page1: "./src/page1.js",
page2: "./src/page2.js",
page3: "./src/page3.js",
},
optimization: {
splitChunks: {
chunks: "all", //默认作用于异步chunk,值为all/initial/async
minSize: 0, //默认值是30kb,代码块的最小尺寸
minChunks: 1, //被多少模块共享,在分割之前模块的被引用次数
maxAsyncRequests: 2, //限制异步模块内部的并行最大请求数的,说白了你可以理解为是每个import()它里面的最大并行请求数量
maxInitialRequests: 4, //限制入口的拆分数量
name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
automaticNameDelimiter: "~", //默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
cacheGroups: {
//设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
vendors: {
chunks: "all",
test: /node_modules/, //条件
priority: -10, ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
},
commons: {
chunks: "all",
minSize: 0, //最小提取字节数
minChunks: 2, //最少被几个chunk引用
priority: -20
}
}
}
}
8.CDN
  • 最影响用户体验的是网页首次打开时的加载等待。 导致这个问题的根本是网络传输过程耗时大,CDN的作用就是加速网络传输。
  • CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度
  • 用户使用浏览器第一次访问我们的站点时,该页面引入了各式各样的静态资源,如果我们能做到持久化缓存的话,可以在 http 响应头加上 Cache-control 或 Expires 字段来设置缓存,浏览器可以将这些资源一一缓存到本地
  • 用户在后续访问的时候,如果需要再次请求同样的静态资源,且静态资源没有过期,那么浏览器可以直接走本地缓存而不用再通过网络请求资源
  • 缓存配置
    • HTML文件不缓存,放在自己的服务器上,关闭自己服务器的缓存,静态资源的URL变成指向CDN服务器的地址
    • 静态的JavaScript、CSS、图片等文件开启CDN和缓存,并且文件名带上HASH值
    • 为了并行加载不阻塞,把不同的静态资源分配到不同的CDN服务器上
  • 域名限制
    • 同一时刻针对同一个域名的资源并行请求是有限制;
    • 可以把这些静态资源分散到不同的 CDN 服务上去;
    • 多个域名后会增加域名解析时间;
    • 可以通过在 HTML HEAD 标签中 加入去预解析域名,以降低域名解析带来的延迟;
六、分享总结
  • 每天坚持半个小时阅读,十分钟冥想,学习更多工作之外的技能;
  • 培养工作之外的兴趣并持之以恒的坚持下去,终会有所回报;
 
 

最新文章

  1. HDU1671——前缀树的一点感触
  2. 正则表达式中的exec和match方法的区别
  3. js中的逻辑与(&amp;&amp;)和逻辑或(||)(转载)
  4. GPS部标监控平台的架构设计(八)-基于WCF的平台数据通信设计
  5. Ubuntu 14 安装Java(JRE、JDK)
  6. DLL函数中内存分配及释放的问题
  7. Qt 之 自定义窗口标题栏(非常完善)
  8. Selenium Grid原理
  9. 自己动手搭建 Redis 环境,并建立一个 .NET HelloWorld 程序测试(转)
  10. c++对象内存布局
  11. WebMethod 属性
  12. HTML高级标签(2)————窗体分帧(2)————后台管理页面
  13. SQL Server 一些重要视图4
  14. 竖向折叠二级导航JS代码(可防刷新ul/li结构)
  15. Angularjs -- 核心概念
  16. 微软宣布.NET开发环境将开源 支持Mac OS X和Linux
  17. TileMap Editer 编辑器工具介绍
  18. submit text3的激活与使用
  19. leetcode02大数相加
  20. select大表报错

热门文章

  1. nacos服务端和客户端版本不一致的时候,客户端无限刷日志
  2. 使用ESP8266连接中文wifi
  3. UE4大地图(流关卡、无缝地图)
  4. Java基础-注释、标识符和关键字、数据类型及拓展
  5. openGL 学习笔记 (二) 使用GL API 绘制出属于自己的矩形
  6. 复杂SQL语句及其优化
  7. nginx(二) の 配置静态资源网站
  8. Ehlib的DBGridEh 控件导出到Excel
  9. 深度剖析CPython解释器》Python内存管理深度剖析Python内存管理架构、内存池的实现原理
  10. 【FPGA &amp; Verilog】手把手教你实现一个DDS信号发生器